Merge branch 'develop'

This commit is contained in:
Hay1tsme 2023-03-18 23:56:23 -04:00
commit b8b93a8d51
176 changed files with 13186 additions and 4935 deletions

8
contributing.md Normal file
View File

@ -0,0 +1,8 @@
# Contributing to ARTEMiS
If you would like to contribute to artemis, either by adding features, games, or fixing bugs, you can do so by forking the repo and submitting a pull request [here](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls). Please make sure, if you're submitting a PR for a game or game version, that you're following the n-0/y-1 guidelines, or it will be rejected.
## Adding games
Guide WIP
## Adding game versions
Guide WIP

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

@ -12,12 +12,13 @@ from Crypto.Signature import PKCS1_v1_5
from time import strptime
from core.config import CoreConfig
from core.data import Data
from core.utils import Utils
from core.data import Data
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,74 +28,48 @@ 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():
for code in mod.game_codes:
if hasattr(mod, "use_default_title") and mod.use_default_title:
if hasattr(mod, "include_protocol") and mod.include_protocol:
if hasattr(mod, "title_secure") and mod.title_secure:
uri = "https://"
else:
uri = "http://"
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
)
else:
uri = ""
if core_cfg.server.is_develop:
uri += f"{core_cfg.title.hostname}:{core_cfg.title.port}"
else:
uri += f"{core_cfg.title.hostname}"
uri += f"/{code}/$v"
if enabled:
self.uri_registry[code] = (uri, host)
if hasattr(mod, "trailing_slash") and mod.trailing_slash:
uri += "/"
else:
if hasattr(mod, "uri"):
uri = mod.uri
else:
uri = ""
if hasattr(mod, "host"):
host = mod.host
elif hasattr(mod, "use_default_host") and mod.use_default_host:
if core_cfg.server.is_develop:
host = f"{core_cfg.title.hostname}:{core_cfg.title.port}"
else:
host = f"{core_cfg.title.hostname}"
else:
host = ""
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"Serving {len(self.uri_registry)} game codes port {core_cfg.allnet.port}"
)
def handle_poweron(self, request: Request, _: Dict):
request_ip = request.getClientAddress().host
request_ip = Utils.get_ip_addr(request)
try:
req_dict = self.allnet_req_to_dict(request.content.getvalue())
if req_dict is None:
@ -103,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()
@ -119,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
@ -147,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)
@ -165,7 +162,7 @@ class AllnetServlet:
return self.dict_to_http_form_string([vars(resp)]).encode("utf-8")
def handle_dlorder(self, request: Request, _: Dict):
request_ip = request.getClientAddress().host
request_ip = Utils.get_ip_addr(request)
try:
req_dict = self.allnet_req_to_dict(request.content.getvalue())
if req_dict is None:
@ -175,30 +172,34 @@ 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)
return b""
self.logger.info(f"DownloadOrder from {request_ip} -> {req.game_id} v{req.ver} serial {req.serial}")
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):
req_dict = self.billing_req_to_dict(request.content.getvalue())
request_ip = request.getClientAddress()
request_ip = Utils.get_ip_addr(request)
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()
@ -214,30 +215,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_ip}: 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
@ -252,22 +257,22 @@ class AllnetServlet:
return resp_str.encode("utf-8")
def handle_naomitest(self, request: Request, _: Dict) -> bytes:
self.logger.info(f"Ping from {request.getClientAddress().host}")
self.logger.info(f"Ping from {Utils.get_ip_addr(request)}")
return b"naomi ok"
def kvp_to_dict(self, kvp: List[str]) -> List[Dict[str, Any]]:
ret: List[Dict[str, Any]] = []
for x in kvp:
items = x.split('&')
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):
@ -277,8 +282,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:
@ -288,33 +293,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]
@ -322,31 +332,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 = ""
@ -362,12 +370,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.setting = ""
self.utc_time = datetime.now(tz=pytz.timezone("UTC")).strftime(
"%Y-%m-%dT%H:%M:%SZ"
)
self.setting = "1"
self.res_ver = "3"
self.token = str(token)
class AllnetPowerOnResponse2():
class AllnetPowerOnResponse2:
def __init__(self) -> None:
self.stat = 1
self.uri = ""
@ -391,23 +402,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"
@ -419,10 +438,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,24 @@ class MuchaConfig:
@property
def enable(self) -> int:
return CoreConfig.get_config_field(self.__config, 'core', 'mucha', 'enable', default=False)
@property
def loglevel(self) -> int:
return CoreConfig.str_to_loglevel(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", "enable", default=False
)
@property
def port(self) -> int:
return CoreConfig.get_config_field(self.__config, 'core', 'mucha', 'port', default=8444)
def loglevel(self) -> int:
return CoreConfig.str_to_loglevel(
CoreConfig.get_config_field(
self.__config, "core", "mucha", "loglevel", default="info"
)
)
@property
def ssl_cert(self) -> str:
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")
def hostname(self) -> str:
return CoreConfig.get_config_field(
self.__config, "core", "mucha", "hostname", default="localhost"
)
class CoreConfig(dict):
def __init__(self) -> None:
@ -200,20 +283,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

@ -1,18 +1,19 @@
import logging, coloredlogs
from typing import Any, Dict, List
from typing import Optional
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy import create_engine
from logging.handlers import TimedRotatingFileHandler
from datetime import datetime
import importlib, os, json
import importlib, os
import secrets, string
import bcrypt
from hashlib import sha256
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)
@ -31,18 +32,22 @@ class Data:
self.arcade = ArcadeData(self.config, self.session)
self.card = CardData(self.config, self.session)
self.base = BaseData(self.config, self.session)
self.schema_ver_latest = 2
self.schema_ver_latest = 4
log_fmt_str = "[%(asctime)s] %(levelname)s | Database | %(message)s"
log_fmt = logging.Formatter(log_fmt_str)
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,33 @@ 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)
if hasattr(game_mod, "database") and hasattr(game_mod, "current_schema_version"):
game_mod.database(self.config)
metadata.create_all(self.__engine.connect())
self.base.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,55 +102,178 @@ 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)
if hasattr(mod, "database"):
mod.database(self.config)
metadata.drop_all(self.__engine.connect())
except Exception as e:
self.logger.warning(f"Could not load database schema from {dir} - {e}")
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:
sql = f.read()
result = self.base.execute(sql)
if result is None:
self.logger.error("Error execuing sql script!")
return None
if old_ver == version:
self.logger.info(
f"Schema for game {game} is already version {old_ver}, nothing to do"
)
return
if action == "upgrade":
for x in range(old_ver, version):
if not os.path.exists(
f"core/data/schema/versions/{game.upper()}_{x + 1}_{action}.sql"
):
self.logger.error(
f"Could not find {action} script {game.upper()}_{x + 1}_{action}.sql in core/data/schema/versions folder"
)
return
with open(
f"core/data/schema/versions/{game.upper()}_{x + 1}_{action}.sql",
"r",
encoding="utf-8",
) as f:
sql = f.read()
result = self.base.execute(sql)
if result is None:
self.logger.error("Error execuing sql script!")
return None
else:
for x in range(old_ver, version, -1):
if not os.path.exists(
f"core/data/schema/versions/{game.upper()}_{x - 1}_{action}.sql"
):
self.logger.error(
f"Could not find {action} script {game.upper()}_{x - 1}_{action}.sql in core/data/schema/versions folder"
)
return
with open(
f"core/data/schema/versions/{game.upper()}_{x - 1}_{action}.sql",
"r",
encoding="utf-8",
) as f:
sql = f.read()
result = self.base.execute(sql)
if result is None:
self.logger.error("Error execuing sql script!")
return None
result = self.base.set_schema_ver(version, game)
if result is None:
self.logger.error("Error setting version in schema_version table!")
return None
self.logger.info(f"Successfully migrated {game} to schema version {version}")
def create_owner(self, email: Optional[str] = None) -> None:
pw = "".join(
secrets.choice(string.ascii_letters + string.digits) for i in range(20)
)
hash = bcrypt.hashpw(pw.encode(), bcrypt.gensalt())
user_id = self.user.create_user(email=email, permission=255, password=hash)
if user_id is None:
self.logger.error(f"Failed to create owner with email {email}")
return
card_id = self.card.create_card(user_id, "00000000000000000000")
if card_id is None:
self.logger.error(f"Failed to create card for owner with id {user_id}")
return
self.logger.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}."
)
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.card.delete_card(new_card["id"])
self.card.update_access_code(old_ac, new_ac)
hanging_user = self.user.get_user(new_card["user"])
if hanging_user["password"] is None:
self.logger.info(f"Delete hanging user {hanging_user['id']}")
self.user.delete_user(hanging_user["id"])
def delete_hanging_users(self) -> None:
"""
Finds and deletes users that have not registered for the webui that have no cards assocated with them.
"""
unreg_users = self.user.get_unregistered_users()
if unreg_users is None:
self.logger.error("Error occoured finding unregistered users")
for user in unreg_users:
cards = self.card.get_user_cards(user["id"])
if cards is None:
self.logger.error(f"Error getting cards for user {user['id']}")
continue
if not cards:
self.logger.info(f"Delete hanging user {user['id']}")
self.user.delete_user(user["id"])
def autoupgrade(self) -> None:
all_games = self.base.get_all_schema_vers()
if all_games is None:
self.logger.warn("Failed to get schema versions")
for x in all_games:
game = x["game"].upper()
update_ver = 1
for y in range(2, 100):
if os.path.exists(f"core/data/schema/versions/{game}_{y}_upgrade.sql"):
update_ver = y
else:
break
self.migrate_database(game, update_ver, "upgrade")

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

@ -2,6 +2,7 @@ import json
import logging
from random import randrange
from typing import Any, Optional, Dict, List
from sqlalchemy.engine import Row
from sqlalchemy.engine.cursor import CursorResult
from sqlalchemy.engine.base import Connection
from sqlalchemy.sql import text, func, select
@ -19,7 +20,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(
@ -29,18 +30,20 @@ event_log = Table(
Column("system", String(255), nullable=False),
Column("type", String(255), nullable=False),
Column("severity", Integer, nullable=False),
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:
@ -50,7 +53,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
@ -62,7 +65,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
@ -72,53 +75,79 @@ class BaseData():
raise
return res
def generate_id(self) -> int:
"""
Generate a random 5-7 digit id
"""
return randrange(10000, 9999999)
def get_all_schema_vers(self) -> Optional[List[Row]]:
sql = select(schema_ver)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_schema_ver(self, game: str) -> Optional[int]:
sql = select(schema_ver).where(schema_ver.c.game == game)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()["version"]
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, details: Dict) -> Optional[int]:
sql = event_log.insert().values(system = system, type = type, severity = severity, 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}, details = {details}")
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

@ -3,55 +3,92 @@ from sqlalchemy import Table, Column, UniqueConstraint
from sqlalchemy.types import Integer, String, Boolean, TIMESTAMP
from sqlalchemy.sql.schema import ForeignKey
from sqlalchemy.sql import func
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
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
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
)
result = self.execute(sql)
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
"""
sql = aime_card.select(aime_card.c.access_code == access_code)
result = self.execute(sql)
if result is None: return None
card = result.fetchone()
if card is None: return None
card = self.get_card_by_access_code(access_code)
if card is None:
return None
return int(card["user"])
def get_user_cards(self, aime_id: int) -> Optional[List[Dict]]:
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:
self.logger.error(f"Failed to delete card with id {card_id}")
def get_user_cards(self, aime_id: int) -> Optional[List[Row]]:
"""
Returns all cards owned by a user
"""
sql = aime_card.select(aime_card.c.user == aime_id)
result = 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:
@ -64,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

@ -1,14 +1,12 @@
from enum import Enum
from typing import Dict, Optional
from sqlalchemy import Table, Column, and_
from typing import Optional, List
from sqlalchemy import Table, Column
from sqlalchemy.types import Integer, String, TIMESTAMP
from sqlalchemy.sql.schema import ForeignKey
from sqlalchemy.sql import func
from sqlalchemy.dialects.mysql import insert
from sqlalchemy.sql import func, select, Delete
from uuid import uuid4
from datetime import datetime, timedelta
from sqlalchemy.sql import func, select
from sqlalchemy.engine import Row
import bcrypt
from core.data.schema.base import BaseData, metadata
@ -19,107 +17,90 @@ 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",
)
frontend_session = Table(
"frontend_session",
metadata,
Column("id", Integer, primary_key=True, unique=True),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column("ip", String(15)),
Column('session_cookie', String(32), nullable=False, unique=True),
Column("expires", TIMESTAMP, nullable=False),
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]:
if email is None:
permission = 1
class UserData(BaseData):
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
return result.lastrowid
def login(self, user_id: int, passwd: bytes = None, ip: str = "0.0.0.0") -> Optional[str]:
sql = select(aime_user).where(and_(aime_user.c.id == user_id, aime_user.c.password == passwd))
result = self.execute(sql)
if result is None: return None
usr = result.fetchone()
if usr is None: return None
return self.create_session(user_id, ip)
def check_session(self, cookie: str, ip: str = "0.0.0.0") -> Optional[Row]:
sql = select(frontend_session).where(
and_(
frontend_session.c.session_cookie == cookie,
frontend_session.c.ip == ip
)
)
result = self.execute(sql)
if result is None: return None
return result.fetchone()
def delete_session(self, session_id: int) -> bool:
sql = Delete(frontend_session).where(frontend_session.c.id == session_id)
result = self.execute(sql)
if result is None: return False
return True
def create_session(self, user_id: int, ip: str = "0.0.0.0", expires: datetime = datetime.now() + timedelta(days=1)) -> Optional[str]:
cookie = uuid4().hex
sql = insert(frontend_session).values(
user = user_id,
ip = ip,
session_cookie = cookie,
expires = expires
)
result = self.execute(sql)
if result is None:
return None
return cookie
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
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:
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)
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:
self.logger.error(f"Failed to delete user with id {user_id}")
def get_unregistered_users(self) -> List[Row]:
"""
Returns a list of users who have not registered with the webui. They may or may not have cards.
"""
sql = select(aime_user).where(aime_user.c.password == None)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()

View File

@ -0,0 +1 @@
ALTER TABLE `event_log` DROP COLUMN `message`;

View File

@ -0,0 +1,12 @@
CREATE TABLE `frontend_session` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user` int(11) NOT NULL,
`ip` varchar(15) DEFAULT NULL,
`session_cookie` varchar(32) NOT NULL,
`expires` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `id` (`id`),
UNIQUE KEY `session_cookie` (`session_cookie`),
KEY `user` (`user`),
CONSTRAINT `frontend_session_ibfk_1` FOREIGN KEY (`user`) REFERENCES `aime_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4;

View File

@ -0,0 +1 @@
ALTER TABLE `event_log` ADD COLUMN `message` VARCHAR(1000) NOT NULL AFTER `severity`;

View File

@ -0,0 +1 @@
DROP TABLE `frontend_session`;

View File

@ -1,9 +1,9 @@
SET FOREIGN_KEY_CHECKS=0;
ALTER TABLE diva_score DROP COLUMN edition;
ALTER TABLE diva_playlog DROP COLUMN edition;
ALTER TABLE diva_score DROP FOREIGN KEY diva_score_ibfk_1;
ALTER TABLE diva_score DROP CONSTRAINT diva_score_uk;
ALTER TABLE diva_score ADD CONSTRAINT diva_score_uk UNIQUE (user, pv_id, difficulty);
ALTER TABLE diva_score ADD CONSTRAINT diva_score_ibfk_1 FOREIGN KEY (user) REFERENCES aime_user(id) ON DELETE CASCADE;
ALTER TABLE diva_score DROP COLUMN edition;
ALTER TABLE diva_playlog DROP COLUMN edition;
SET FOREIGN_KEY_CHECKS=1;

View File

@ -0,0 +1 @@
ALTER TABLE ongeki_profile_data DROP COLUMN lastEmoneyCredit;

View File

@ -0,0 +1 @@
ALTER TABLE ongeki_profile_data ADD COLUMN lastEmoneyCredit INTEGER DEFAULT 0;

View File

@ -0,0 +1,21 @@
ALTER TABLE mai2_item_card
CHANGE COLUMN cardId card_id INT NOT NULL AFTER user,
CHANGE COLUMN cardTypeId card_kind INT NOT NULL,
CHANGE COLUMN charaId chara_id INT NOT NULL,
CHANGE COLUMN mapId map_id INT NOT NULL,
CHANGE COLUMN startDate start_date TIMESTAMP NULL DEFAULT '2018-01-01 00:00:00',
CHANGE COLUMN endDate end_date TIMESTAMP NULL DEFAULT '2038-01-01 00:00:00';
ALTER TABLE mai2_item_item
CHANGE COLUMN itemId item_id INT NOT NULL AFTER user,
CHANGE COLUMN itemKind item_kind INT NOT NULL,
CHANGE COLUMN isValid is_valid TINYINT(1) NOT NULL DEFAULT '1';
ALTER TABLE mai2_item_character
CHANGE COLUMN characterId character_id INT NOT NULL,
CHANGE COLUMN useCount use_count INT NOT NULL DEFAULT '0';
ALTER TABLE mai2_item_charge
CHANGE COLUMN chargeId charge_id INT NOT NULL,
CHANGE COLUMN purchaseDate purchase_date TIMESTAMP NOT NULL,
CHANGE COLUMN validDate valid_date TIMESTAMP NOT NULL;

View File

@ -0,0 +1,21 @@
ALTER TABLE mai2_item_card
CHANGE COLUMN card_id cardId INT NOT NULL AFTER user,
CHANGE COLUMN card_kind cardTypeId INT NOT NULL,
CHANGE COLUMN chara_id charaId INT NOT NULL,
CHANGE COLUMN map_id mapId INT NOT NULL,
CHANGE COLUMN start_date startDate TIMESTAMP NULL DEFAULT '2018-01-01 00:00:00',
CHANGE COLUMN end_date endDate TIMESTAMP NULL DEFAULT '2038-01-01 00:00:00';
ALTER TABLE mai2_item_item
CHANGE COLUMN item_id itemId INT NOT NULL AFTER user,
CHANGE COLUMN item_kind itemKind INT NOT NULL,
CHANGE COLUMN is_valid isValid TINYINT(1) NOT NULL DEFAULT '1';
ALTER TABLE mai2_item_character
CHANGE COLUMN character_id characterId INT NOT NULL,
CHANGE COLUMN use_count useCount INT NOT NULL DEFAULT '0';
ALTER TABLE mai2_item_charge
CHANGE COLUMN charge_id chargeId INT NOT NULL,
CHANGE COLUMN purchase_date purchaseDate TIMESTAMP NOT NULL,
CHANGE COLUMN valid_date validDate TIMESTAMP NOT NULL;

View File

@ -4,17 +4,34 @@ from twisted.web import resource
from twisted.web.util import redirectTo
from twisted.web.http import Request
from logging.handlers import TimedRotatingFileHandler
from twisted.web.server import Session
from zope.interface import Interface, Attribute, implementer
from twisted.python.components import registerAdapter
import jinja2
import bcrypt
from core.config import CoreConfig
from core import CoreConfig, Utils
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):
self.userId = 0
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'':
self.logger.debug(f"{Utils.get_ip_addr(request)} -> {name.decode()}")
if name == b"":
return self
return resource.Resource.getChild(self, name, request)
@ -27,17 +44,24 @@ 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)
games = Utils.get_all_titles()
@ -49,18 +73,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).encode("utf-16")
self.logger.debug(f"{Utils.get_ip_addr(request)} -> {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")
class FE_Base(resource.Resource):
"""
@ -68,63 +100,81 @@ 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):
self.logger.debug(f"{request.getClientIP()} -> {request.uri.decode()}")
def render_GET(self, request: Request):
self.logger.debug(f"{Utils.get_ip_addr(request)} -> {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).encode("utf-16")
def render_POST(self, request: Request):
uri = request.uri.decode()
ip = request.getClientAddress().host
if uri == "/gate/gate.login":
ip = Utils.get_ip_addr(request)
if uri == "/gate/gate.login":
access_code: str = request.args[b"access_code"][0].decode()
passwd: str = request.args[b"passwd"][0]
passwd: bytes = request.args[b"passwd"][0]
if passwd == b"":
passwd = None
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.login(uid, ip=ip)
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)
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(passwd, salt)
sesh = self.data.user.login(uid, hashed, ip)
if sesh is None:
if not self.data.user.check_password(uid, passwd):
return redirectTo(b"/gate?e=1", request)
request.addCookie('session', sesh)
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]
@ -138,44 +188,56 @@ 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)
sesh = self.data.user.login(uid, hashed, ip)
if sesh is None:
if not self.data.user.check_password(uid, passwd.encode()):
return redirectTo(b"/gate", request)
request.addCookie('session', sesh)
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).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):
template = self.environment.get_template("core/frontend/user/index.jinja")
return template.render().encode("utf-16")
if b'session' not in request.cookies:
sesh: Session = request.getSession()
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")
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

@ -2,10 +2,23 @@
{% block content %}
<h1>Gate</h1>
{% include "core/frontend/widgets/err_banner.jinja" %}
<style>
/* Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type=number] {
-moz-appearance: textfield;
}
</style>
<form id="login" style="max-width: 240px; min-width: 10%;" action="/gate/gate.login" method="post">
<div class="form-group row">
<label for="access_code">Card Access Code</label><br>
<input form="login" class="form-control" name="access_code" id="access_code" type="text" placeholder="00000000000000000000" maxlength="20" required>
<input form="login" class="form-control" name="access_code" id="access_code" type="number" placeholder="00000000000000000000" maxlength="20" required>
</div>
<div class="form-group row">
<label for="passwd">Password</label><br>
@ -14,4 +27,6 @@
<p></p>
<input id="submit" class="btn btn-primary" style="display: block; margin: 0 auto;" form="login" type="submit" value="Login">
</form>
<h6>*To register for the webui, type in the access code of your card, as shown in a game, and leave the password field blank.</h6>
<h6>*If you have not registered a card with this server, you cannot create a webui account.</h6>
{% endblock content %}

View File

@ -9,5 +9,10 @@
</div>
</div>
<div style="background: #333; color: #f9f9f9; width: 10%; height: 50px; line-height: 50px; text-align: center; float: left;">
{% if sesh is defined and sesh["userId"] > 0 %}
<a href="/user"><button class="btn btn-primary">Account</button></a>
{% else %}
<a href="/gate"><button class="btn btn-primary">Gate</button></a>
{% endif %}
</div>

View File

@ -1,4 +1,4 @@
from typing import Dict, Any, Optional
from typing import Dict, Any, Optional, List
import logging, coloredlogs
from logging.handlers import TimedRotatingFileHandler
from twisted.web import resource
@ -6,59 +6,101 @@ from twisted.web.http import Request
from datetime import datetime
import pytz
from core.config import CoreConfig
from core import CoreConfig
from core.utils import Utils
class MuchaServlet:
def __init__(self, cfg: CoreConfig) -> 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)
all_titles = Utils.get_all_titles()
for _, mod in all_titles.items():
if hasattr(mod, "index") and hasattr(mod.index, "get_mucha_info"):
enabled, game_cd = mod.index.get_mucha_info(
self.config, self.config_dir
)
if enabled:
self.mucha_registry.append(game_cd)
self.logger.info(
f"Serving {len(self.mucha_registry)} games"
)
def handle_boardauth(self, request: Request, _: Dict) -> bytes:
req_dict = self.mucha_preprocess(request.content.getvalue())
client_ip = Utils.get_ip_addr(request)
if req_dict is None:
self.logger.error(f"Error processing mucha request {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 {client_ip} for {req.gameVer}"
)
if self.config.server.is_develop:
resp = MuchaAuthResponse(mucha_url=f"{self.config.mucha.hostname}:{self.config.mucha.port}")
else:
resp = MuchaAuthResponse(mucha_url=f"{self.config.mucha.hostname}")
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}{':' + str(self.config.allnet.port) if self.config.server.is_develop else ''}"
)
self.logger.debug(f"Mucha response {vars(resp)}")
return self.mucha_postprocess(vars(resp))
def handle_updatecheck(self, request: Request, _: Dict) -> bytes:
req_dict = self.mucha_preprocess(request.content.getvalue())
client_ip = Utils.get_ip_addr(request)
if req_dict is None:
self.logger.error(f"Error processing mucha request {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 {client_ip} for {req.gameVer}"
)
if self.config.server.is_develop:
resp = MuchaUpdateResponse(mucha_url=f"{self.config.mucha.hostname}:{self.config.mucha.port}")
else:
resp = MuchaUpdateResponse(mucha_url=f"{self.config.mucha.hostname}")
if req.gameCd not in self.mucha_registry:
self.logger.warn(f"Unknown gameCd {req.gameCd}")
return b""
resp = MuchaUpdateResponseStub(req.gameVer)
self.logger.debug(f"Mucha response {vars(resp)}")
@ -67,14 +109,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
@ -82,7 +124,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()
@ -91,35 +133,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"]
self.sendDate = "" if "sendDate" not in request else request["sendDate"]
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.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():
def __init__(self, mucha_url: str = "localhost") -> None:
self.RESULTS = "001"
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.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"
@ -130,7 +176,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"
@ -142,24 +188,26 @@ 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():
def __init__(self, game_ver: str = "PKFN0JPN01.01", mucha_url: str = "localhost") -> None:
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
self.UPDATE_URL_1 = f"https://{mucha_url}/updUrl1/"
@ -171,3 +219,11 @@ class MuchaUpdateResponse():
self.COM_SIZE_1 = "0"
self.COM_TIME_1 = "0"
self.LAN_INFO_SIZE_1 = "0"
self.USER_ID = ""
self.PASSWORD = ""
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,36 +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"):
handler_cls = mod.index(self.config, self.config_folder)
if hasattr(handler_cls, "setup"):
handler_cls.setup()
for code in mod.game_codes:
self.title_registry[code] = handler_cls
should_call_setup = True
if hasattr(mod.index, "get_allnet_info"):
for code in mod.game_codes:
enabled, _, _ = mod.index.get_allnet_info(
code, self.config, self.config_folder
)
if 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 ' + str(core_cfg.title.port) if core_cfg.title.port > 0 else ''}"
)
def render_GET(self, request: Request, endpoints: dict) -> bytes:
code = endpoints["game"]
@ -55,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")
@ -63,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

@ -1,16 +1,18 @@
from typing import Dict, Any
from types import ModuleType
from twisted.web.http import Request
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}")
@ -20,3 +22,7 @@ class Utils:
logging.getLogger("core").error(f"get_all_titles: {dir} - {e}")
raise
return ret
@classmethod
def get_ip_addr(cls, req: Request) -> str:
return req.getAllHeaders()[b"x-forwarded-for"].decode() if b"x-forwarded-for" in req.getAllHeaders() else req.getClientAddress().host

View File

@ -2,22 +2,53 @@ import yaml
import argparse
from core.config import CoreConfig
from core.data import Data
from os import path, mkdir, access, W_OK
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("action", type=str, help="DB Action, create, recreate, upgrade, or rollback")
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"
)
args = parser.parse_args()
cfg = CoreConfig()
cfg.update(yaml.safe_load(open(f"{args.config}/core.yaml")))
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)
if not access(cfg.server.log_dir, W_OK):
print(
f"Log directory {cfg.server.log_dir} NOT writable, please check permissions"
)
exit(1)
data = Data(cfg)
if args.action == "create":
data.create_database()
elif args.action == "recreate":
data.recreate_database()
@ -28,9 +59,21 @@ if __name__=='__main__':
if args.game is None:
data.logger.info("No game set, upgrading core schema")
data.migrate_database("CORE", int(args.version))
data.migrate_database("CORE", int(args.version), args.action)
else:
data.migrate_database(args.game, int(args.version), args.action)
elif args.action == "autoupgrade":
data.autoupgrade()
elif args.action == "create-owner":
data.create_owner(args.email)
elif args.action == "migrate-card":
data.migrate_card(args.old_ac, args.new_ac, args.force)
elif args.action == "cleanup":
data.delete_hanging_users()
data.logger.info("Done")

351
docs/game_specific_info.md Normal file
View File

@ -0,0 +1,351 @@
# ARTEMiS Games Documentation
Below are all supported games with supported version ids in order to use
the corresponding importer and database upgrades.
**Important: The described database upgrades are only required if you are using an old database schema, f.e. still
using the megaime database. Clean installations always create the latest database structure!**
# Table of content
- [Supported Games](#supported-games)
- [Chunithm](#chunithm)
- [crossbeats REV.](#crossbeats-rev)
- [maimai DX](#maimai-dx)
- [O.N.G.E.K.I.](#o-n-g-e-k-i)
- [Card Maker](#card-maker)
- [WACCA](#wacca)
# Supported Games
Games listed below have been tested and confirmed working.
## Chunithm
### SDBT
| Version ID | Version Name |
|------------|--------------------|
| 0 | Chunithm |
| 1 | Chunithm+ |
| 2 | Chunithm Air |
| 3 | Chunithm Air + |
| 4 | Chunithm Star |
| 5 | Chunithm Star + |
| 6 | Chunithm Amazon |
| 7 | Chunithm Amazon + |
| 8 | Chunithm Crystal |
| 9 | Chunithm Crystal + |
| 10 | Chunithm Paradise |
### SDHD/SDBT
| Version ID | Version Name |
|------------|-----------------|
| 11 | Chunithm New!! |
| 12 | Chunithm New!!+ |
### Importer
In order to use the importer locate your game installation folder and execute:
```shell
python read.py --series SDBT --version <version ID> --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder
```
The importer for Chunithm will import: Events, Music, Charge Items and Avatar Accesories.
### Database upgrade
Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see
which version is the latest, f.e. `SDBT_3_upgrade.sql`. In order to upgrade to version 3 in this case you need to
perform all previous updates as well:
```shell
python dbutils.py --game SDBT --version 2 upgrade
python dbutils.py --game SDBT --version 3 upgrade
```
## crossbeats REV.
### SDCA
| Version ID | Version Name |
|------------|------------------------------------|
| 0 | crossbeats REV. |
| 1 | crossbeats REV. SUNRISE |
| 2 | crossbeats REV. SUNRISE S2 |
| 3 | crossbeats REV. SUNRISE S2 Omnimix |
### Importer
In order to use the importer you need to use the provided `Export.csv` file:
```shell
python read.py --series SDCA --version <version ID> --binfolder titles/cxb/data
```
The importer for crossbeats REV. will import Music.
### Config
Config file is located in `config/cxb.yaml`.
| Option | Info |
|------------------------|------------------------------------------------------------|
| `hostname` | Requires a proper `hostname` (not localhost!) to run |
| `ssl_enable` | Enables/Disables the use of the `ssl_cert` and `ssl_key` |
| `port` | Set your unsecure port number |
| `port_secure` | Set your secure/SSL port number |
| `ssl_cert`, `ssl_key` | Enter your SSL certificate (requires not self signed cert) |
## maimai DX
### SDEZ
| Version ID | Version Name |
|------------|-------------------------|
| 0 | maimai DX |
| 1 | maimai DX PLUS |
| 2 | maimai DX Splash |
| 3 | maimai DX Splash PLUS |
| 4 | maimai DX Universe |
| 5 | maimai DX Universe PLUS |
### Importer
In order to use the importer locate your game installation folder and execute:
```shell
python read.py --series SDEZ --version <version ID> --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder
```
The importer for maimai DX will import Events, Music and Tickets.
**NOTE: It is required to use the importer because the game will
crash without it!**
### Database upgrade
Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see which version is the latest, f.e. `SDEZ_2_upgrade.sql`. In order to upgrade to version 2 in this case you need to perform all previous updates as well:
```shell
python dbutils.py --game SDEZ --version 2 upgrade
```
## Hatsune Miku Project Diva
### SBZV
| Version ID | Version Name |
|------------|---------------------------------|
| 0 | Project Diva Arcade |
| 1 | Project Diva Arcade Future Tone |
### Importer
In order to use the importer locate your game installation folder and execute:
```shell
python read.py --series SBZV --version <version ID> --binfolder /path/to/game/data/diva --optfolder /path/to/game/data/diva/mdata
```
The importer for Project Diva Arcade will all required data in order to use
the Shop, Modules and Customizations.
### Config
Config file is located in `config/diva.yaml`.
| Option | Info |
|----------------------|-------------------------------------------------------------------------------------------------|
| `unlock_all_modules` | Unlocks all modules (costumes) by default, if set to `False` all modules need to be purchased |
| `unlock_all_items` | Unlocks all items (customizations) by default, if set to `False` all items need to be purchased |
### Database upgrade
Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see
which version is the latest, f.e. `SBZV_4_upgrade.sql`. In order to upgrade to version 4 in this case you need to
perform all previous updates as well:
```shell
python dbutils.py --game SBZV --version 2 upgrade
python dbutils.py --game SBZV --version 3 upgrade
python dbutils.py --game SBZV --version 4 upgrade
```
## O.N.G.E.K.I.
### SDDT
| Version ID | Version Name |
|------------|----------------------------|
| 0 | O.N.G.E.K.I. |
| 1 | O.N.G.E.K.I. + |
| 2 | O.N.G.E.K.I. Summer |
| 3 | O.N.G.E.K.I. Summer + |
| 4 | O.N.G.E.K.I. Red |
| 5 | O.N.G.E.K.I. Red + |
| 6 | O.N.G.E.K.I. Bright |
| 7 | O.N.G.E.K.I. Bright Memory |
### Importer
In order to use the importer locate your game installation folder and execute:
```shell
python read.py --series SDDT --version <version ID> --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder
```
The importer for O.N.G.E.K.I. will all all Cards, Music and Events.
**NOTE: The Importer is required for Card Maker.**
### Config
Config file is located in `config/ongeki.yaml`.
| Option | Info |
|------------------|----------------------------------------------------------------------------------------------------------------|
| `enabled_gachas` | Enter all gacha IDs for Card Maker to work, other than default may not work due to missing cards added to them |
Note: 1149 and higher are only for Card Maker 1.35 and higher and will be ignored on lower versions.
### Database upgrade
Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see
which version is the latest, f.e. `SDDT_4_upgrade.sql`. In order to upgrade to version 4 in this case you need to
perform all previous updates as well:
```shell
python dbutils.py --game SDDT --version 2 upgrade
python dbutils.py --game SDDT --version 3 upgrade
python dbutils.py --game SDDT --version 4 upgrade
```
## Card Maker
### SDED
| Version ID | Version Name |
|------------|-----------------|
| 0 | Card Maker 1.34 |
| 1 | Card Maker 1.35 |
### Support status
* Card Maker 1.34:
* Chunithm New!!: Yes
* maimai DX Universe: Yes
* O.N.G.E.K.I. Bright: Yes
* Card Maker 1.35:
* Chunithm New!!+: Yes
* maimai DX Universe PLUS: Yes
* O.N.G.E.K.I. Bright Memory: Yes
### Importer
In order to use the importer you need to use the provided `.csv` files (which are required for O.N.G.E.K.I.) and the
option folders:
```shell
python read.py --series SDED --version <version ID> --binfolder titles/cm/cm_data --optfolder /path/to/cardmaker/option/folder
```
**If you haven't already executed the O.N.G.E.K.I. importer, make sure you import all cards!**
```shell
python read.py --series SDDT --version <version ID> --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder
```
Also make sure to import all maimai and Chunithm data as well:
```shell
python read.py --series SDED --version <version ID> --binfolder /path/to/cardmaker/CardMaker_Data
```
The importer for Card Maker will import all required Gachas (Banners) and cards (for maimai/Chunithm) and the hardcoded
Cards for each Gacha (O.N.G.E.K.I. only).
**NOTE: Without executing the importer Card Maker WILL NOT work!**
### O.N.G.E.K.I. Gachas
Gacha "無料ガチャ" can only pull from the free cards with the following probabilities: 94%: R, 5% SR and 1% chance of
getting an SSR card
Gacha "無料ガチャSR確定" can only pull from free SR cards with prob: 92% SR and 8% chance of getting an SSR card
Gacha "レギュラーガチャ" can pull from every card added to ongeki_static_cards with the following prob: 77% R, 20% SR
and 3% chance of getting an SSR card
All other (limited) gachas can pull from every card added to ongeki_static_cards but with the promoted cards
(click on the green button under the banner) having a 10 times higher chance to get pulled
### Chunithm Gachas
All cards in Chunithm (basically just the characters) have the same rarity to it just pulls randomly from all cards
from a given gacha but made sure you cannot pull the same card twice in the same 5 times gacha roll.
### Notes
Card Maker 1.34 will only load an O.N.G.E.K.I. Bright profile (1.30). Card Maker 1.35 will only load an O.N.G.E.K.I.
Bright Memory profile (1.35).
The gachas inside the `ongeki.yaml` will make sure only the right gacha ids for the right CM version will be loaded.
Gacha IDs up to 1140 will be loaded for CM 1.34 and all gachas will be loaded for CM 1.35.
**NOTE: There is currently no way to load/use the (printed) maimai DX cards!**
## WACCA
### SDFE
| Version ID | Version Name |
|------------|---------------|
| 0 | WACCA |
| 1 | WACCA S |
| 2 | WACCA Lily |
| 3 | WACCA Lily R |
| 4 | WACCA Reverse |
### Importer
In order to use the importer locate your game installation folder and execute:
```shell
python read.py --series SDFE --version <version ID> --binfolder /path/to/game/WindowsNoEditor/Mercury/Content
```
The importer for WACCA will import all Music data.
### Config
Config file is located in `config/wacca.yaml`.
| Option | Info |
|--------------------|-----------------------------------------------------------------------------|
| `always_vip` | Enables/Disables VIP, if disabled it needs to be purchased manually in game |
| `infinite_tickets` | Always set the "unlock expert" tickets to 5 |
| `infinite_wp` | Sets the user WP to `999999` |
| `enabled_gates` | Enter all gate IDs which should be enabled in game |
### Database upgrade
Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see which version is the latest, f.e. `SDFE_3_upgrade.sql`. In order to upgrade to version 3 in this case you need to perform all previous updates as well:
```shell
python dbutils.py --game SDFE --version 2 upgrade
python dbutils.py --game SDFE --version 3 upgrade
```

View File

@ -0,0 +1,3 @@
server:
enable: True
loglevel: "info"

View File

@ -48,6 +48,3 @@ mucha:
enable: False
hostname: "localhost"
loglevel: "info"
port: 8444
ssl_key: "cert/server.key"
ssl_cert: "cert/server.pem"

View File

@ -4,6 +4,8 @@ server {
server_name naominet.jp;
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass_request_headers on;
proxy_pass http://localhost:8000/;
}
}
@ -14,6 +16,8 @@ server {
server_name your.hostname.here;
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass_request_headers on;
proxy_pass http://localhost:8080/;
}
}
@ -75,6 +79,8 @@ server {
ssl_prefer_server_ciphers off;
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass_request_headers on;
proxy_pass http://localhost:8080/;
}
}
@ -95,6 +101,8 @@ server {
ssl_prefer_server_ciphers off;
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass_request_headers on;
proxy_pass http://localhost:8080/SDBT/104/;
}
}
@ -131,6 +139,8 @@ server {
add_header Strict-Transport-Security "max-age=63072000" always;
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass_request_headers on;
proxy_pass http://localhost:8090/;
}
}

View File

@ -1,3 +1,31 @@
server:
enable: True
loglevel: "info"
gachas:
enabled_gachas:
- 1011
- 1012
- 1043
- 1067
- 1068
- 1069
- 1070
- 1071
- 1072
- 1073
- 1074
- 1075
- 1076
- 1077
- 1081
- 1085
- 1089
- 1104
- 1111
- 1135
# can be used for Card Maker 1.35 and up, else will be ignored
- 1149
- 1156
- 1163
- 1164

View File

@ -1,7 +1,8 @@
server:
hostname: "localhost"
enable: True
loglevel: "info"
port: 9000
port_matching: 9001
ssl_cert: cert/pokken.crt
ssl_key: cert/pokken.key
port_stun: 9001
port_turn: 9002
port_admission: 9003

193
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,130 +21,222 @@ 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)
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())
client_ip = Utils.get_ip_addr(request)
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 {client_ip} 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())
client_ip = Utils.get_ip_addr(request)
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 {client_ip} 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()
cfg.update(yaml.safe_load(open(f"{args.config}/core.yaml")))
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)
if not access(cfg.server.log_dir, W_OK):
print(
f"Log directory {cfg.server.log_dir} NOT writable, please check permissions"
)
exit(1)
logger = logging.getLogger("core")
log_fmt_str = "[%(asctime)s] Core | %(levelname)s | %(message)s"
log_fmt = logging.Formatter(log_fmt_str)
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")
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

86
read.py
View File

@ -3,65 +3,73 @@ import argparse
import re
import os
import yaml
import importlib
import logging, coloredlogs
from os import path
import logging
import coloredlogs
from logging.handlers import TimedRotatingFileHandler
from typing import List, Optional
from core import CoreConfig
from core.utils import Utils
from core import CoreConfig, 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",
@ -79,21 +87,24 @@ if __name__ == "__main__":
args = parser.parse_args()
config = CoreConfig()
config.update(yaml.safe_load(open(f"{args.config}/core.yaml")))
if path.exists(f"{args.config}/core.yaml"):
config.update(yaml.safe_load(open(f"{args.config}/core.yaml")))
log_fmt_str = "[%(asctime)s] Reader | %(levelname)s | %(message)s"
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)
@ -101,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()
@ -112,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:
@ -124,7 +135,8 @@ if __name__ == "__main__":
for dir, mod in titles.items():
if args.series in mod.game_codes:
handler = mod.reader(config, args.version, bin_arg, opt_arg, args.extra)
handler = mod.reader(config, args.version,
bin_arg, opt_arg, args.extra)
handler.read()
logger.info("Done")

View File

@ -15,6 +15,10 @@ Games listed below have been tested and confirmed working. Only game versions ol
+ Hatsune Miku Arcade
+ All versions
+ Card Maker
+ 1.34.xx
+ 1.35.xx
+ Ongeki
+ All versions up to Bright Memory
@ -32,5 +36,8 @@ Games listed below have been tested and confirmed working. Only game versions ol
## Setup guides
Follow the platform-specific guides for [windows](docs/INSTALL_WINDOWS.md) and [ubuntu](docs/INSTALL_UBUNTU.md) to setup and run the server.
## Game specific information
Read [Games specific info](docs/game_specific_info.md) for all supported games, importer settings, configuration option and database upgrades.
## Production guide
See the [production guide](docs/prod.md) for running a production server.

View File

@ -6,13 +6,5 @@ from titles.chuni.read import ChuniReader
index = ChuniServlet
database = ChuniData
reader = ChuniReader
use_default_title = True
include_protocol = True
title_secure = False
game_codes = [ChuniConstants.GAME_CODE, ChuniConstants.GAME_CODE_NEW]
trailing_slash = True
use_default_host = False
host = ""
current_schema_version = 1

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
@ -20,38 +21,48 @@ class ChuniBase():
self.logger = logging.getLogger("chuni")
self.game = ChuniConstants.GAME_CODE
self.version = ChuniConstants.VER_CHUNITHM
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 }
# 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)
if game_charge_list is None or len(game_charge_list) == 0:
return {"length": 0, "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
}
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)
if game_events is None or len(game_events) == 0:
self.logger.warn("No enabled events, did you run the reader?")
return {
"type": data["type"],
"length": 0,
"gameEventList": [],
}
event_list = []
for evt_row in game_events:
tmp = {}
@ -62,26 +73,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",
@ -94,15 +109,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")
@ -111,15 +128,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 = []
@ -131,15 +149,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:
@ -153,21 +173,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"])
@ -180,51 +200,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()
@ -233,18 +250,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:
@ -252,22 +269,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")
@ -277,38 +295,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()
@ -317,19 +344,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"])
@ -340,66 +367,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:
@ -408,8 +429,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
@ -421,14 +442,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"],
@ -436,7 +457,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,
@ -459,11 +480,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"],
@ -486,19 +504,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)
@ -514,7 +543,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)
@ -522,23 +551,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)
@ -551,22 +580,30 @@ 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"}
def handle_get_user_net_battle_data_api_request(self, data: Dict) -> Dict:
return {
"userId": data["userId"],
"userNetBattleData": {
"recentNBSelectMusicList": []
}
}

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,12 +1,14 @@
class ChuniConstants():
class ChuniConstants:
GAME_CODE = "SDBT"
GAME_CODE_NEW = "SDHD"
CONFIG_NAME = "chuni.yaml"
VER_CHUNITHM = 0
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
@ -16,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

@ -8,8 +8,12 @@ import inflection
import string
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Protocol.KDF import PBKDF2
from Crypto.Hash import SHA1
from os import path
from typing import Tuple, Dict
from core import CoreConfig
from core import CoreConfig, Utils
from titles.chuni.config import ChuniConfig
from titles.chuni.const import ChuniConstants
from titles.chuni.base import ChuniBase
@ -26,26 +30,31 @@ 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()
self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/chuni.yaml")))
self.hash_table: Dict[Dict[str, str]] = {}
if path.exists(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),
ChuniPlus(core_cfg, self.game_cfg),
ChuniAir(core_cfg, self.game_cfg),
ChuniAirPlus(core_cfg, self.game_cfg),
ChuniStar(core_cfg, self.game_cfg),
ChuniStarPlus(core_cfg, self.game_cfg),
ChuniAmazon(core_cfg, self.game_cfg),
ChuniAmazonPlus(core_cfg, self.game_cfg),
ChuniCrystal(core_cfg, self.game_cfg),
ChuniCrystalPlus(core_cfg, self.game_cfg),
ChuniParadise(core_cfg, self.game_cfg),
ChuniNew(core_cfg, self.game_cfg),
ChuniNewPlus(core_cfg, self.game_cfg),
ChuniBase,
ChuniPlus,
ChuniAir,
ChuniAirPlus,
ChuniStar,
ChuniStarPlus,
ChuniAmazon,
ChuniAmazonPlus,
ChuniCrystal,
ChuniCrystalPlus,
ChuniParadise,
ChuniNew,
ChuniNewPlus,
]
self.logger = logging.getLogger("chuni")
@ -53,121 +62,191 @@ 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
for version, keys in self.game_cfg.crypto.keys.items():
if len(keys) < 3:
continue
self.hash_table[version] = {}
method_list = [method for method in dir(self.versions[version]) if not method.startswith('__')]
for method in method_list:
method_fixed = inflection.camelize(method)[6:-7]
hash = PBKDF2(method_fixed, bytes.fromhex(keys[2]), 128, count=44, hmac_hash_module=SHA1)
self.hash_table[version][hash.hex()] = method_fixed
self.logger.debug(f"Hashed v{version} method {method_fixed} with {bytes.fromhex(keys[2])} to get {hash.hex()}")
@classmethod
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}"))
)
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}/{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]
client_ip = Utils.get_ip_addr(request)
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]
if internal_ver < ChuniConstants.VER_CHUNITHM_NEW:
endpoint = request.getHeader("User-Agent").split("#")[0]
else:
if internal_ver not in self.hash_table:
self.logger.error(f"v{version} does not support encryption or no keys entered")
return zlib.compress(b'{"stat": "0"}')
elif endpoint.lower() not in self.hash_table[internal_ver]:
self.logger.error(f"No hash found for v{version} endpoint {endpoint}")
return zlib.compress(b'{"stat": "0"}')
endpoint = self.hash_table[internal_ver][endpoint.lower()]
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[internal_ver][0]),
AES.MODE_CBC,
bytes.fromhex(self.game_cfg.crypto.keys[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"))
except Exception as e:
self.logger.error(
f"Failed to decrypt v{version} request to {endpoint} -> {e}"
)
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 and internal_ver >= ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS:
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 {client_ip}"
)
self.logger.debug(req_data)
func_to_find = "handle_" + inflection.underscore(endpoint) + "_request"
handler_cls = self.versions[internal_ver](self.core_cfg, self.game_cfg)
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(handler_cls, func_to_find):
self.logger.warning(f"Unhandled v{version} request {endpoint}")
resp = {"returnCode": 1}
else:
try:
handler = getattr(handler_cls, 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[internal_ver][0]),
AES.MODE_CBC,
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]),
)
return crypt.encrypt(padded)

View File

@ -1,6 +1,6 @@
import logging
from datetime import datetime, timedelta
from random import randint
from typing import Dict
from core.config import CoreConfig
@ -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,20 +113,364 @@ 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",
# Enables favorites and teams
"chargeState": 1,
"userNameEx": "",
"banState": 0,
"classEmblemMedal": profile["classEmblemMedal"],
"classEmblemBase": profile["classEmblemBase"],
"battleRankId": profile["battleRankId"],
}
return data1
def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
p = self.data.profile.get_profile_data(data["userId"], self.version)
if p is None:
return {}
return {
"userName": p["userName"],
"level": p["level"],
"medal": p["medal"],
"lastDataVersion": "2.00.00",
"isLogin": False,
}
def handle_printer_login_api_request(self, data: Dict) -> Dict:
return {"returnCode": 1}
def handle_printer_logout_api_request(self, data: Dict) -> Dict:
return {"returnCode": 1}
def handle_get_game_gacha_api_request(self, data: Dict) -> Dict:
"""
returns all current active banners (gachas)
"""
game_gachas = self.data.static.get_gachas(self.version)
# clean the database rows
game_gacha_list = []
for gacha in game_gachas:
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["noticeStartDate"] = datetime.strftime(
tmp["noticeStartDate"], "%Y-%m-%d %H:%M:%S"
)
tmp["noticeEndDate"] = datetime.strftime(
tmp["noticeEndDate"], "%Y-%m-%d %H:%M:%S"
)
game_gacha_list.append(tmp)
return {
"length": len(game_gacha_list),
"gameGachaList": game_gacha_list,
# no clue
"registIdList": [],
}
def handle_get_game_gacha_card_by_id_api_request(self, data: Dict) -> Dict:
"""
returns all valid cards for a given gachaId
"""
game_gacha_cards = self.data.static.get_gacha_cards(data["gachaId"])
game_gacha_card_list = []
for gacha_card in game_gacha_cards:
tmp = gacha_card._asdict()
tmp.pop("id")
game_gacha_card_list.append(tmp)
return {
"gachaId": data["gachaId"],
"length": len(game_gacha_card_list),
# check isPickup from the chuni_static_gachas?
"isPickup": False,
"gameGachaCardList": game_gacha_card_list,
# again no clue
"emissionList": [],
"afterCalcList": [],
"ssrBookCalcList": [],
}
def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict:
p = self.data.profile.get_profile_data(data["userId"], self.version)
if p is None:
return {}
profile = p._asdict()
profile.pop("id")
profile.pop("user")
profile.pop("version")
return {
"userId": data["userId"],
"userData": profile,
"userEmoney": [
{
"type": 0,
"emoneyCredit": 100,
"emoneyBrand": 1,
"ext1": 0,
"ext2": 0,
"ext3": 0,
}
],
}
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": []}
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")
user_gacha_list.append(tmp)
return {
"userId": data["userId"],
"length": len(user_gacha_list),
"userGachaList": user_gacha_list,
}
def handle_get_user_printed_card_api_request(self, data: Dict) -> Dict:
user_print_list = self.data.item.get_user_print_states(
data["userId"], has_completed=True
)
if user_print_list is None:
return {
"userId": data["userId"],
"length": 0,
"nextIndex": -1,
"userPrintedCardList": [],
}
print_list = []
next_idx = int(data["nextIndex"])
max_ct = int(data["maxCount"])
for x in range(next_idx, len(user_print_list)):
tmp = user_print_list[x]._asdict()
print_list.append(tmp["cardId"])
if len(user_print_list) >= max_ct:
break
if len(user_print_list) >= max_ct:
next_idx = next_idx + max_ct
else:
next_idx = -1
return {
"userId": data["userId"],
"length": len(print_list),
"nextIndex": next_idx,
"userPrintedCardList": print_list,
}
def handle_get_user_card_print_error_api_request(self, data: Dict) -> Dict:
user_id = data["userId"]
user_print_states = self.data.item.get_user_print_states(
user_id, has_completed=False
)
card_print_state_list = []
for card in user_print_states:
tmp = card._asdict()
tmp["orderId"] = tmp["id"]
tmp.pop("user")
tmp["limitDate"] = datetime.strftime(tmp["limitDate"], "%Y-%m-%d")
card_print_state_list.append(tmp)
return {
"userId": user_id,
"length": len(card_print_state_list),
"userCardPrintStateList": card_print_state_list,
}
def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict:
return super().handle_get_user_character_api_request(data)
def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict:
return super().handle_get_user_item_api_request(data)
def handle_roll_gacha_api_request(self, data: Dict) -> Dict:
"""
Handle a gacha roll API request, with:
gachaId: the gachaId where the cards should be pulled from
times: the number of gacha rolls
characterId: the character which the user wants
"""
gacha_id = data["gachaId"]
num_rolls = data["times"]
chara_id = data["characterId"]
rolled_cards = []
# characterId is set after 10 rolls, where the user can select a card
# from all gameGachaCards, therefore the correct cardId for a given
# characterId should be returned
if chara_id != -1:
# get the
card = self.data.static.get_gacha_card_by_character(gacha_id, chara_id)
tmp = card._asdict()
tmp.pop("id")
rolled_cards.append(tmp)
else:
gacha_cards = self.data.static.get_gacha_cards(gacha_id)
# get the card id for each roll
for _ in range(num_rolls):
# get the index from all possible cards
card_idx = randint(0, len(gacha_cards) - 1)
# remove the index from the cards so it wont get pulled again
card = gacha_cards.pop(card_idx)
# remove the "id" fronm the card
tmp = card._asdict()
tmp.pop("id")
rolled_cards.append(tmp)
return {"length": len(rolled_cards), "gameGachaCardList": rolled_cards}
def handle_cm_upsert_user_gacha_api_request(self, data: Dict) -> Dict:
upsert = data["cmUpsertUserGacha"]
user_id = data["userId"]
place_id = data["placeId"]
# save the user data
user_data = upsert["userData"]
user_data.pop("rankUpChallengeResults")
user_data.pop("userEmoney")
self.data.profile.put_profile_data(user_id, self.version, user_data)
# save the user gacha
user_gacha = upsert["userGacha"]
gacha_id = user_gacha["gachaId"]
user_gacha.pop("gachaId")
user_gacha.pop("dailyGachaDate")
self.data.item.put_user_gacha(user_id, gacha_id, user_gacha)
# save all user items
if "userItemList" in upsert:
for item in upsert["userItemList"]:
self.data.item.put_item(user_id, item)
# add every gamegachaCard to database
for card in upsert["gameGachaCardList"]:
self.data.item.put_user_print_state(
user_id,
hasCompleted=False,
placeId=place_id,
cardId=card["cardId"],
gachaId=card["gachaId"],
)
# retrieve every game gacha card which has been added in order to get
# the orderId for the next request
user_print_states = self.data.item.get_user_print_states_by_gacha(
user_id, gacha_id, has_completed=False
)
card_print_state_list = []
for card in user_print_states:
tmp = card._asdict()
tmp["orderId"] = tmp["id"]
tmp.pop("user")
tmp["limitDate"] = datetime.strftime(tmp["limitDate"], "%Y-%m-%d")
card_print_state_list.append(tmp)
return {
"returnCode": "1",
"apiName": "CMUpsertUserGachaApi",
"userCardPrintStateList": card_print_state_list,
}
def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict:
return {
"returnCode": 1,
"orderId": 0,
"serialId": "11111111111111111111",
"apiName": "CMUpsertUserPrintlogApi",
}
def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict:
user_print_detail = data["userPrintDetail"]
user_id = data["userId"]
# generate random serial id
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"
)
# add the entry to the user print table with the random serialId
self.data.item.put_user_print_detail(user_id, serial_id, user_print_detail)
return {
"returnCode": 1,
"orderId": 0,
"serialId": serial_id,
"apiName": "CMUpsertUserPrintApi",
}
def handle_cm_upsert_user_print_subtract_api_request(self, data: Dict) -> Dict:
upsert = data["userCardPrintState"]
user_id = data["userId"]
place_id = data["placeId"]
# save all user items
if "userItemList" in data:
for item in data["userItemList"]:
self.data.item.put_item(user_id, item)
# set the card print state to success and use the orderId as the key
self.data.item.put_user_print_state(
user_id,
id=upsert["orderId"],
hasCompleted=True
)
return {"returnCode": "1", "apiName": "CMUpsertUserPrintSubtractApi"}
def handle_cm_upsert_user_print_cancel_api_request(self, data: Dict) -> Dict:
order_ids = data["orderIdList"]
user_id = data["userId"]
# set the card print state to success and use the orderId as the key
for order_id in order_ids:
self.data.item.put_user_print_state(
user_id,
id=order_id,
hasCompleted=True
)
return {"returnCode": "1", "apiName": "CMUpsertUserPrintCancelApi"}

View File

@ -7,17 +7,33 @@ 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
def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
user_data = super().handle_cm_get_user_preview_api_request(data)
# hardcode lastDataVersion for CardMaker 1.35
user_data["lastDataVersion"] = "2.05.00"
return user_data

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,80 @@ map_area = Table(
Column("statusCount", Integer),
Column("remainGridCount", Integer),
UniqueConstraint("user", "mapAreaId", name="chuni_item_map_area_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
gacha = Table(
"chuni_item_gacha",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("gachaId", Integer, nullable=False),
Column("totalGachaCnt", Integer, server_default="0"),
Column("ceilingGachaCnt", Integer, server_default="0"),
Column("dailyGachaCnt", Integer, server_default="0"),
Column("fiveGachaCnt", Integer, server_default="0"),
Column("elevenGachaCnt", Integer, server_default="0"),
Column("dailyGachaDate", TIMESTAMP, nullable=False, server_default=func.now()),
UniqueConstraint("user", "gachaId", name="chuni_item_gacha_uk"),
mysql_charset="utf8mb4",
)
print_state = Table(
"chuni_item_print_state",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("hasCompleted", Boolean, nullable=False, server_default="0"),
Column(
"limitDate", TIMESTAMP, nullable=False, server_default="2038-01-01 00:00:00.0"
),
Column("placeId", Integer),
Column("cardId", Integer),
Column("gachaId", Integer),
UniqueConstraint("id", "user", name="chuni_item_print_state_uk"),
mysql_charset="utf8mb4",
)
print_detail = Table(
"chuni_item_print_detail",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("cardId", Integer, nullable=False),
Column("printDate", TIMESTAMP, nullable=False),
Column("serialId", String(20), nullable=False),
Column("placeId", Integer, nullable=False),
Column("clientId", String(11), nullable=False),
Column("printerSerialId", String(20), nullable=False),
Column("printOption1", Boolean, server_default="0"),
Column("printOption2", Boolean, server_default="0"),
Column("printOption3", Boolean, server_default="0"),
Column("printOption4", Boolean, server_default="0"),
Column("printOption5", Boolean, server_default="0"),
Column("printOption6", Boolean, server_default="0"),
Column("printOption7", Boolean, server_default="0"),
Column("printOption8", Boolean, server_default="0"),
Column("printOption9", Boolean, server_default="0"),
Column("printOption10", Boolean, server_default="0"),
Column("created", String(255), server_default=""),
UniqueConstraint("serialId", name="chuni_item_print_detail_uk"),
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 +195,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 +226,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 +252,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 +273,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 +294,100 @@ 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()
def get_user_gachas(self, aime_id: int) -> Optional[List[Row]]:
sql = gacha.select(gacha.c.user == aime_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def put_user_gacha(
self, aime_id: int, gacha_id: int, gacha_data: Dict
) -> Optional[int]:
sql = insert(gacha).values(user=aime_id, gachaId=gacha_id, **gacha_data)
conflict = sql.on_duplicate_key_update(
user=aime_id, gachaId=gacha_id, **gacha_data
)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_user_gacha: Failed to insert! aime_id: {aime_id}")
return None
return result.lastrowid
def get_user_print_states(
self, aime_id: int, has_completed: bool = False
) -> Optional[List[Row]]:
sql = print_state.select(
and_(
print_state.c.user == aime_id,
print_state.c.hasCompleted == has_completed
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_user_print_states_by_gacha(
self, aime_id: int, gacha_id: int, has_completed: bool = False
) -> Optional[List[Row]]:
sql = print_state.select(
and_(
print_state.c.user == aime_id,
print_state.c.gachaId == gacha_id,
print_state.c.hasCompleted == has_completed
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def put_user_print_state(self, aime_id: int, **print_data) -> Optional[int]:
sql = insert(print_state).values(user=aime_id, **print_data)
conflict = sql.on_duplicate_key_update(user=aime_id, **print_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(
f"put_user_print_state: Failed to insert! aime_id: {aime_id}"
)
return None
return result.lastrowid
def put_user_print_detail(
self, aime_id: int, serial_id: str, user_print_data: Dict
) -> Optional[int]:
sql = insert(print_detail).values(
user=aime_id, serialId=serial_id, **user_print_data
)
conflict = sql.on_duplicate_key_update(
user=aime_id, **user_print_data
)
result = self.execute(conflict)
if result is None:
self.logger.warn(
f"put_user_print_detail: Failed to insert! aime_id: {aime_id}"
)
return None
return result.lastrowid

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,366 @@ avatar = Table(
Column("iconPath", String(255)),
Column("texturePath", String(255)),
UniqueConstraint("version", "avatarAccessoryId", name="chuni_static_avatar_uk"),
mysql_charset='utf8mb4'
mysql_charset="utf8mb4",
)
gachas = Table(
"chuni_static_gachas",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer, nullable=False),
Column("gachaId", Integer, nullable=False),
Column("gachaName", String(255), nullable=False),
Column("type", Integer, nullable=False, server_default="0"),
Column("kind", Integer, nullable=False, server_default="0"),
Column("isCeiling", Boolean, server_default="0"),
Column("ceilingCnt", Integer, server_default="10"),
Column("changeRateCnt1", Integer, server_default="0"),
Column("changeRateCnt2", Integer, server_default="0"),
Column("startDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"),
Column("endDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"),
Column("noticeStartDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"),
Column("noticeEndDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"),
UniqueConstraint("version", "gachaId", "gachaName", name="chuni_static_gachas_uk"),
mysql_charset="utf8mb4",
)
cards = Table(
"chuni_static_cards",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer, nullable=False),
Column("cardId", Integer, nullable=False),
Column("charaName", String(255), nullable=False),
Column("charaId", Integer, nullable=False),
Column("presentName", String(255), nullable=False),
Column("rarity", Integer, server_default="2"),
Column("labelType", Integer, nullable=False),
Column("difType", Integer, nullable=False),
Column("miss", Integer, nullable=False),
Column("combo", Integer, nullable=False),
Column("chain", Integer, nullable=False),
Column("skillName", String(255), nullable=False),
UniqueConstraint("version", "cardId", name="chuni_static_cards_uk"),
mysql_charset="utf8mb4",
)
gacha_cards = Table(
"chuni_static_gacha_cards",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("gachaId", Integer, nullable=False),
Column("cardId", Integer, nullable=False),
Column("rarity", Integer, nullable=False),
Column("weight", Integer, server_default="1"),
Column("isPickup", Boolean, server_default="0"),
UniqueConstraint("gachaId", "cardId", name="chuni_static_gacha_cards_uk"),
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
def put_gacha(
self,
version: int,
gacha_id: int,
gacha_name: int,
**gacha_data,
) -> Optional[int]:
sql = insert(gachas).values(
version=version,
gachaId=gacha_id,
gachaName=gacha_name,
**gacha_data,
)
conflict = sql.on_duplicate_key_update(
version=version,
gachaId=gacha_id,
gachaName=gacha_name,
**gacha_data,
)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert gacha! gacha_id {gacha_id}")
return None
return result.lastrowid
def get_gachas(self, version: int) -> Optional[List[Dict]]:
sql = gachas.select(gachas.c.version <= version).order_by(
gachas.c.gachaId.asc()
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_gacha(self, version: int, gacha_id: int) -> Optional[Dict]:
sql = gachas.select(
and_(gachas.c.version <= version, gachas.c.gachaId == gacha_id)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def put_gacha_card(
self, gacha_id: int, card_id: int, **gacha_card
) -> Optional[int]:
sql = insert(gacha_cards).values(gachaId=gacha_id, cardId=card_id, **gacha_card)
conflict = sql.on_duplicate_key_update(
gachaId=gacha_id, cardId=card_id, **gacha_card
)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert gacha card! gacha_id {gacha_id}")
return None
return result.lastrowid
def get_gacha_cards(self, gacha_id: int) -> Optional[List[Dict]]:
sql = gacha_cards.select(gacha_cards.c.gachaId == gacha_id)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_gacha_card_by_character(self, gacha_id: int, chara_id: int) -> Optional[Dict]:
sql_sub = (
select(cards.c.cardId)
.filter(
cards.c.charaId == chara_id
)
.scalar_subquery()
)
# Perform the main query, also rename the resulting column to ranking
sql = gacha_cards.select(and_(
gacha_cards.c.gachaId == gacha_id,
gacha_cards.c.cardId == sql_sub
))
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def put_card(self, version: int, card_id: int, **card_data) -> Optional[int]:
sql = insert(cards).values(version=version, cardId=card_id, **card_data)
conflict = sql.on_duplicate_key_update(**card_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"Failed to insert card! card_id {card_id}")
return None
return result.lastrowid
def get_card(self, version: int, card_id: int) -> Optional[Dict]:
sql = cards.select(and_(cards.c.version <= version, cards.c.cardId == card_id))
result = self.execute(sql)
if result is None:
return None
return result.fetchone()

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

12
titles/cm/__init__.py Normal file
View File

@ -0,0 +1,12 @@
from titles.cm.index import CardMakerServlet
from titles.cm.const import CardMakerConstants
from titles.cm.read import CardMakerReader
from titles.cm.database import CardMakerData
index = CardMakerServlet
reader = CardMakerReader
database = CardMakerData
game_codes = [CardMakerConstants.GAME_CODE]
current_schema_version = 1

77
titles/cm/base.py Normal file
View File

@ -0,0 +1,77 @@
from datetime import date, datetime, timedelta
from typing import Any, Dict, List
import json
import logging
from enum import Enum
from core.config import CoreConfig
from core.data.cache import cached
from titles.cm.const import CardMakerConstants
from titles.cm.config import CardMakerConfig
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_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:
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/"},
],
}
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
)
return {
"gameSetting": {
"dataVersion": "1.30.00",
"ongekiCmVersion": "1.30.01",
"chuniCmVersion": "2.00.00",
"maimaiCmVersion": "1.20.00",
"requestInterval": 10,
"rebootStartTime": reboot_start,
"rebootEndTime": reboot_end,
"maxCountCharacter": 100,
"maxCountItem": 100,
"maxCountCard": 100,
"watermark": False,
"isMaintenance": False,
"isBackgroundDistribute": False,
},
"isDumpUpload": False,
"isAou": False,
}
def handle_get_client_bookkeeping_api_request(self, data: Dict) -> Dict:
return {"placeId": data["placeId"], "length": 0, "clientBookkeepingList": []}
def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict:
return {"returnCode": 1, "apiName": "UpsertClientSettingApi"}
def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict:
return {"returnCode": 1, "apiName": "UpsertClientBookkeepingApi"}

38
titles/cm/cm135.py Normal file
View File

@ -0,0 +1,38 @@
from datetime import date, datetime, timedelta
from typing import Any, Dict, List
import json
import logging
from enum import Enum
from core.config import CoreConfig
from core.data.cache import cached
from titles.cm.base import CardMakerBase
from titles.cm.const import CardMakerConstants
from titles.cm.config import CardMakerConfig
class CardMaker135(CardMakerBase):
def __init__(self, core_cfg: CoreConfig, game_cfg: CardMakerConfig) -> None:
super().__init__(core_cfg, game_cfg)
self.version = CardMakerConstants.VER_CARD_MAKER_135
def handle_get_game_connect_api_request(self, data: Dict) -> Dict:
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}"
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)
ret["gameSetting"]["dataVersion"] = "1.35.00"
ret["gameSetting"]["ongekiCmVersion"] = "1.35.03"
ret["gameSetting"]["chuniCmVersion"] = "2.05.00"
ret["gameSetting"]["maimaiCmVersion"] = "1.25.00"
return ret

View File

@ -0,0 +1,501 @@
"gachaId","cardId","rarity","weight","isPickup","isSelect"
1070,100984,4,1,0,1
1070,100997,3,2,0,1
1070,100998,3,2,0,1
1070,101020,2,3,0,1
1070,101021,2,3,0,1
1070,101022,2,3,0,1
1067,100982,4,1,0,0
1067,100983,4,1,0,0
1067,100996,3,2,0,0
1068,100075,2,3,0,0
1068,100182,2,3,0,0
1068,100348,2,3,0,0
1068,100232,2,3,0,0
1068,100417,2,3,0,0
1068,100755,2,3,0,0
1068,100077,3,2,0,0
1068,100271,3,2,0,0
1068,100425,3,2,0,0
1068,100758,3,2,0,0
1068,101000,3,2,0,0
1068,100284,4,1,0,0
1068,100767,4,1,0,0
1068,101293,4,1,0,0
1069,100069,2,3,0,0
1069,100183,2,3,0,0
1069,100349,2,3,0,0
1069,100233,2,3,0,0
1069,100416,2,3,0,0
1069,100071,3,2,0,0
1069,100272,3,2,0,0
1069,100427,3,2,0,0
1069,100805,3,2,0,0
1069,101300,3,2,0,0
1069,100285,4,1,0,0
1069,100768,4,1,0,0
1069,100988,4,1,0,0
1071,100275,4,1,0,0
1071,100437,4,1,0,0
1071,100780,4,1,0,0
1071,100006,3,2,0,0
1071,100007,3,2,0,0
1071,100249,3,2,0,0
1071,100262,3,2,0,0
1071,100418,3,2,0,0
1071,100003,2,3,0,0
1071,100004,2,3,0,0
1071,100173,2,3,0,0
1071,100223,2,3,0,0
1071,100339,2,3,0,0
1071,100692,2,3,0,0
1072,100017,4,1,0,0
1072,100276,4,1,0,0
1072,100760,4,1,0,0
1072,100015,3,2,0,0
1072,100016,3,2,0,0
1072,100250,3,2,0,0
1072,100263,3,2,0,0
1072,100423,3,2,0,0
1072,100765,3,2,0,0
1072,100012,2,3,0,0
1072,100013,2,3,0,0
1072,100174,2,3,0,0
1072,100224,2,3,0,0
1072,100340,2,3,0,0
1072,100693,2,3,0,0
1073,100026,4,1,0,0
1073,100277,4,1,0,0
1073,100761,4,1,0,0
1073,100024,3,2,0,0
1073,100025,3,2,0,0
1073,100251,3,2,0,0
1073,100264,3,2,0,0
1073,100430,3,2,0,0
1073,100021,2,3,0,0
1073,100022,2,3,0,0
1073,100175,2,3,0,0
1073,100225,2,3,0,0
1073,100341,2,3,0,0
1073,100694,2,3,0,0
1011,100454,4,1,0,0
1011,100980,4,1,0,0
1011,101553,4,1,0,0
1011,100253,3,1,0,0
1011,100241,3,1,0,0
1011,100240,3,1,0,0
1011,100239,3,1,0,0
1011,100238,3,1,0,0
1011,100237,3,1,0,0
1011,100236,3,1,0,0
1011,100261,3,1,0,0
1011,100246,3,1,0,0
1011,100245,3,1,0,0
1011,100242,3,1,0,0
1011,100243,3,1,0,0
1011,100254,3,1,0,0
1011,100338,3,1,0,0
1011,100337,3,1,0,0
1011,100336,3,1,0,0
1011,100248,3,1,0,0
1011,100247,3,1,0,0
1011,100244,3,1,0,0
1011,100259,3,1,0,0
1011,100257,3,1,0,0
1011,100258,3,1,0,0
1011,100636,3,1,0,0
1011,100634,3,1,0,0
1011,100255,3,1,0,0
1011,100256,3,1,0,0
1011,100252,3,1,0,0
1011,100638,3,1,0,0
1011,100639,3,1,0,0
1011,100637,3,1,0,0
1011,100772,3,1,0,0
1011,100667,3,1,0,0
1011,100666,3,1,0,0
1011,100665,3,1,0,0
1011,100643,3,1,0,0
1011,100640,3,1,0,0
1011,100641,3,1,0,0
1011,100642,3,1,0,0
1011,100688,3,1,0,0
1011,100645,3,1,0,0
1011,100646,3,1,0,0
1011,100644,3,1,0,0
1012,100644,3,1,0,0
1012,100646,3,1,0,0
1012,100645,3,1,0,0
1012,100688,3,1,0,0
1012,100642,3,1,0,0
1012,100641,3,1,0,0
1012,100640,3,1,0,0
1012,100643,3,1,0,0
1012,100665,3,1,0,0
1012,100666,3,1,0,0
1012,100667,3,1,0,0
1012,100634,3,1,0,0
1012,100636,3,1,0,0
1012,100772,3,1,0,0
1012,100638,3,1,0,0
1012,100637,3,1,0,0
1012,100639,3,1,0,0
1012,100252,3,1,0,0
1012,100256,3,1,0,0
1012,100255,3,1,0,0
1012,100258,3,1,0,0
1012,100257,3,1,0,0
1012,100259,3,1,0,0
1012,100244,3,1,0,0
1012,100247,3,1,0,0
1012,100248,3,1,0,0
1012,100336,3,1,0,0
1012,100337,3,1,0,0
1012,100338,3,1,0,0
1012,100254,3,1,0,0
1012,100243,3,1,0,0
1012,100242,3,1,0,0
1012,100245,3,1,0,0
1012,100246,3,1,0,0
1012,100261,3,1,0,0
1012,100236,3,1,0,0
1012,100237,3,1,0,0
1012,100238,3,1,0,0
1012,100239,3,1,0,0
1012,100240,3,1,0,0
1012,100241,3,1,0,0
1012,100253,3,1,0,0
1012,100454,4,1,0,0
1012,100980,4,1,0,0
1012,101553,4,1,0,0
1074,100985,4,1,0,0
1074,100999,3,1,0,0
1074,101000,3,1,0,0
1074,101023,2,1,0,0
1074,101024,2,1,0,0
1075,100060,4,1,0,0
1075,100434,4,1,0,0
1075,100059,3,1,0,0
1075,100268,3,1,0,0
1075,100420,3,1,0,0
1075,100763,3,1,0,0
1075,101003,3,1,0,0
1075,100057,2,1,0,0
1075,100179,2,1,0,0
1075,100229,2,1,0,0
1075,100345,2,1,0,0
1075,100415,2,1,0,0
1076,100054,4,1,0,0
1076,100282,4,1,0,0
1076,100726,4,1,0,0
1076,100053,3,1,0,0
1076,100269,3,1,0,0
1076,100422,3,1,0,0
1076,100757,3,1,0,0
1076,100051,2,1,0,0
1076,100180,2,1,0,0
1076,100230,2,1,0,0
1076,100346,2,1,0,0
1076,100414,2,1,0,0
1077,100984,4,1,0,1
1077,100997,3,1,0,1
1077,100998,3,1,0,1
1077,100986,4,1,0,1
1077,101001,3,1,0,1
1077,101002,3,1,0,1
1077,101025,2,1,0,1
1077,101026,2,1,0,1
1077,101027,2,1,0,1
1081,100987,4,1,0,0
1081,100988,4,1,0,0
1081,101003,3,1,0,0
1085,100008,4,1,0,1
1085,100017,4,1,0,1
1085,100026,4,1,0,1
1085,100034,4,1,0,1
1085,100041,4,1,0,1
1085,100048,4,1,0,1
1085,100054,4,1,0,1
1085,100060,4,1,0,1
1085,100066,4,1,0,1
1085,100078,4,1,0,1
1085,100072,4,1,0,1
1085,100084,4,1,0,1
1085,100090,4,1,0,1
1085,100282,4,1,0,1
1085,100285,4,1,0,1
1085,100284,4,1,0,1
1085,100286,4,1,0,1
1085,100280,4,1,0,1
1085,100276,4,1,0,1
1085,100277,4,1,0,1
1085,100275,4,1,0,1
1085,100278,4,1,0,1
1085,100431,4,1,0,1
1085,100407,4,1,0,1
1085,100432,4,1,0,1
1085,100433,4,1,0,1
1085,100434,4,1,0,1
1085,100435,4,1,0,1
1085,100436,4,1,0,1
1085,100437,4,1,0,1
1085,100438,4,1,0,1
1085,100439,4,1,0,1
1085,100760,4,1,0,1
1085,100761,4,1,0,1
1085,100779,4,1,0,1
1085,100767,4,1,0,1
1085,100780,4,1,0,1
1085,100784,4,1,0,1
1085,100768,4,1,0,1
1085,100725,4,1,0,1
1085,100726,4,1,0,1
1085,100984,4,1,0,1
1085,100985,4,1,0,1
1085,100987,4,1,0,1
1085,100988,4,1,0,1
1085,100986,4,1,0,1
1085,100989,4,1,0,1
1085,100982,4,1,0,1
1085,100983,4,1,0,1
1085,100787,4,1,0,1
1085,101293,4,1,0,1
1085,101294,4,1,0,1
1085,101295,4,1,0,1
1085,101296,4,1,0,1
1085,101297,4,1,0,1
1085,101320,4,1,0,1
1085,101567,4,1,0,1
1085,101592,4,1,0,1
1085,101593,4,1,0,1
1085,101594,4,1,0,1
1085,101595,4,1,0,1
1089,100989,4,1,0,0
1089,101004,3,1,0,0
1089,101005,3,1,0,0
1104,101293,4,1,0,0
1104,101294,4,1,0,0
1104,101298,3,1,0,0
1111,100008,4,1,0,1
1111,100017,4,1,0,1
1111,100026,4,1,0,1
1111,100034,4,1,0,1
1111,100041,4,1,0,1
1111,100048,4,1,0,1
1111,100054,4,1,0,1
1111,100060,4,1,0,1
1111,100066,4,1,0,1
1111,100078,4,1,0,1
1111,100072,4,1,0,1
1111,100084,4,1,0,1
1111,100090,4,1,0,1
1111,100282,4,1,0,1
1111,100285,4,1,0,1
1111,100284,4,1,0,1
1111,100286,4,1,0,1
1111,100280,4,1,0,1
1111,100276,4,1,0,1
1111,100277,4,1,0,1
1111,100275,4,1,0,1
1111,100278,4,1,0,1
1111,100431,4,1,0,1
1111,100407,4,1,0,1
1111,100432,4,1,0,1
1111,100433,4,1,0,1
1111,100434,4,1,1,1
1111,100435,4,1,1,1
1111,100436,4,1,0,1
1111,100437,4,1,0,1
1111,100438,4,1,0,1
1111,100439,4,1,0,1
1111,100760,4,1,1,1
1111,100761,4,1,0,1
1111,100779,4,1,0,1
1111,100767,4,1,0,1
1111,100780,4,1,1,1
1111,100784,4,1,1,1
1111,100768,4,1,0,1
1111,100725,4,1,1,1
1111,100726,4,1,1,1
1111,100985,4,1,1,1
1111,100988,4,1,1,1
1111,100989,4,1,1,1
1111,100982,4,1,1,1
1111,100983,4,1,1,1
1111,101293,4,1,1,1
1111,101294,4,1,1,1
1111,101295,4,1,1,1
1111,101320,4,1,1,1
1135,101567,4,1,0,0
1135,101592,4,1,0,0
1135,101594,4,1,0,0
1135,101595,4,1,0,0
1135,101566,3,1,0,0
1135,101602,3,1,0,0
1135,101603,3,1,0,0
1135,101619,2,1,0,0
1156,101604,3,1,0,0
1156,101605,3,1,0,0
1156,101607,3,1,0,0
1156,101608,3,1,0,0
1156,101596,4,1,0,0
1156,101597,4,1,0,0
1156,101599,4,1,0,0
1156,101600,4,1,0,0
1149,100003,2,1,0,0
1149,100004,2,1,0,0
1149,100012,2,1,0,0
1149,100013,2,1,0,0
1149,100021,2,1,0,0
1149,100022,2,1,0,0
1149,100173,2,1,0,0
1149,100174,2,1,0,0
1149,100175,2,1,0,0
1149,100339,2,1,0,0
1149,100340,2,1,0,0
1149,100341,2,1,0,0
1149,100223,2,1,0,0
1149,100224,2,1,0,0
1149,100225,2,1,0,0
1149,100692,2,1,0,0
1149,100693,2,1,0,0
1149,100694,2,1,0,0
1149,101020,2,1,0,0
1149,101025,2,1,0,0
1149,100418,3,1,0,0
1149,101005,3,1,0,0
1149,100785,3,1,0,0
1149,100786,3,1,0,0
1149,101602,3,1,0,0
1149,101604,3,1,0,0
1149,100760,4,1,0,0
1149,100780,4,1,0,0
1149,100987,4,1,0,0
1149,101295,4,1,0,0
1149,101296,4,1,0,0
1149,101592,4,1,0,0
1163,100008,4,1,0,1
1163,100017,4,1,0,1
1163,100026,4,1,0,1
1163,100034,4,1,0,1
1163,100041,4,1,0,1
1163,100048,4,1,0,1
1163,100054,4,1,0,1
1163,100060,4,1,0,1
1163,100066,4,1,0,1
1163,100078,4,1,0,1
1163,100072,4,1,0,1
1163,100084,4,1,0,1
1163,100090,4,1,0,1
1163,100282,4,1,0,1
1163,100285,4,1,0,1
1163,100284,4,1,0,1
1163,100286,4,1,0,1
1163,100280,4,1,0,1
1163,100276,4,1,0,1
1163,100277,4,1,0,1
1163,100275,4,1,0,1
1163,100278,4,1,0,1
1163,100431,4,1,0,1
1163,100407,4,1,0,1
1163,100432,4,1,0,1
1163,100433,4,1,0,1
1163,100434,4,1,0,1
1163,100435,4,1,0,1
1163,100436,4,1,0,1
1163,100437,4,1,0,1
1163,100438,4,1,0,1
1163,100439,4,1,0,1
1163,100760,4,1,0,1
1163,100761,4,1,0,1
1163,100779,4,1,0,1
1163,100767,4,1,0,1
1163,100780,4,1,0,1
1163,100784,4,1,0,1
1163,100768,4,1,0,1
1163,100725,4,1,0,1
1163,100726,4,1,0,1
1163,100984,4,1,0,1
1163,100985,4,1,0,1
1163,100987,4,1,0,1
1163,100988,4,1,0,1
1163,100986,4,1,0,1
1163,100989,4,1,0,1
1163,100982,4,1,0,1
1163,100983,4,1,0,1
1163,100787,4,1,0,1
1163,101293,4,1,0,1
1163,101294,4,1,0,1
1163,101295,4,1,0,1
1163,101296,4,1,0,1
1163,101297,4,1,0,1
1163,101320,4,1,0,1
1163,101567,4,1,0,1
1164,100008,4,1,0,1
1164,100017,4,1,0,1
1164,100026,4,1,0,1
1164,100034,4,1,0,1
1164,100041,4,1,0,1
1164,100048,4,1,0,1
1164,100054,4,1,0,1
1164,100060,4,1,0,1
1164,100066,4,1,0,1
1164,100078,4,1,0,1
1164,100072,4,1,0,1
1164,100084,4,1,0,1
1164,100090,4,1,0,1
1164,100282,4,1,0,1
1164,100285,4,1,0,1
1164,100284,4,1,0,1
1164,100286,4,1,0,1
1164,100280,4,1,0,1
1164,100276,4,1,0,1
1164,100277,4,1,0,1
1164,100275,4,1,0,1
1164,100278,4,1,0,1
1164,100431,4,1,0,1
1164,100407,4,1,0,1
1164,100432,4,1,0,1
1164,100433,4,1,0,1
1164,100434,4,1,0,1
1164,100435,4,1,0,1
1164,100436,4,1,0,1
1164,100437,4,1,0,1
1164,100438,4,1,0,1
1164,100439,4,1,0,1
1164,100760,4,1,0,1
1164,100761,4,1,0,1
1164,100779,4,1,0,1
1164,100767,4,1,0,1
1164,100780,4,1,0,1
1164,100784,4,1,0,1
1164,100768,4,1,0,1
1164,100725,4,1,0,1
1164,100726,4,1,0,1
1164,100984,4,1,0,1
1164,100985,4,1,0,1
1164,100987,4,1,0,1
1164,100988,4,1,0,1
1164,100986,4,1,0,1
1164,100989,4,1,0,1
1164,100982,4,1,0,1
1164,100983,4,1,0,1
1164,100787,4,1,0,1
1164,101293,4,1,0,1
1164,101294,4,1,0,1
1164,101295,4,1,0,1
1164,101296,4,1,0,1
1164,101297,4,1,0,1
1164,101320,4,1,0,1
1164,101567,4,1,0,1
1164,101592,4,1,0,1
1164,101593,4,1,0,1
1164,101594,4,1,0,1
1164,101595,4,1,0,1
1164,101598,4,1,0,1
1164,101596,4,1,0,1
1164,101597,4,1,0,1
1164,101599,4,1,0,1
1164,101600,4,1,0,1
1141,101600,4,1,0,1
1141,101608,3,1,0,1
1 gachaId cardId rarity weight isPickup isSelect
2 1070 100984 4 1 0 1
3 1070 100997 3 2 0 1
4 1070 100998 3 2 0 1
5 1070 101020 2 3 0 1
6 1070 101021 2 3 0 1
7 1070 101022 2 3 0 1
8 1067 100982 4 1 0 0
9 1067 100983 4 1 0 0
10 1067 100996 3 2 0 0
11 1068 100075 2 3 0 0
12 1068 100182 2 3 0 0
13 1068 100348 2 3 0 0
14 1068 100232 2 3 0 0
15 1068 100417 2 3 0 0
16 1068 100755 2 3 0 0
17 1068 100077 3 2 0 0
18 1068 100271 3 2 0 0
19 1068 100425 3 2 0 0
20 1068 100758 3 2 0 0
21 1068 101000 3 2 0 0
22 1068 100284 4 1 0 0
23 1068 100767 4 1 0 0
24 1068 101293 4 1 0 0
25 1069 100069 2 3 0 0
26 1069 100183 2 3 0 0
27 1069 100349 2 3 0 0
28 1069 100233 2 3 0 0
29 1069 100416 2 3 0 0
30 1069 100071 3 2 0 0
31 1069 100272 3 2 0 0
32 1069 100427 3 2 0 0
33 1069 100805 3 2 0 0
34 1069 101300 3 2 0 0
35 1069 100285 4 1 0 0
36 1069 100768 4 1 0 0
37 1069 100988 4 1 0 0
38 1071 100275 4 1 0 0
39 1071 100437 4 1 0 0
40 1071 100780 4 1 0 0
41 1071 100006 3 2 0 0
42 1071 100007 3 2 0 0
43 1071 100249 3 2 0 0
44 1071 100262 3 2 0 0
45 1071 100418 3 2 0 0
46 1071 100003 2 3 0 0
47 1071 100004 2 3 0 0
48 1071 100173 2 3 0 0
49 1071 100223 2 3 0 0
50 1071 100339 2 3 0 0
51 1071 100692 2 3 0 0
52 1072 100017 4 1 0 0
53 1072 100276 4 1 0 0
54 1072 100760 4 1 0 0
55 1072 100015 3 2 0 0
56 1072 100016 3 2 0 0
57 1072 100250 3 2 0 0
58 1072 100263 3 2 0 0
59 1072 100423 3 2 0 0
60 1072 100765 3 2 0 0
61 1072 100012 2 3 0 0
62 1072 100013 2 3 0 0
63 1072 100174 2 3 0 0
64 1072 100224 2 3 0 0
65 1072 100340 2 3 0 0
66 1072 100693 2 3 0 0
67 1073 100026 4 1 0 0
68 1073 100277 4 1 0 0
69 1073 100761 4 1 0 0
70 1073 100024 3 2 0 0
71 1073 100025 3 2 0 0
72 1073 100251 3 2 0 0
73 1073 100264 3 2 0 0
74 1073 100430 3 2 0 0
75 1073 100021 2 3 0 0
76 1073 100022 2 3 0 0
77 1073 100175 2 3 0 0
78 1073 100225 2 3 0 0
79 1073 100341 2 3 0 0
80 1073 100694 2 3 0 0
81 1011 100454 4 1 0 0
82 1011 100980 4 1 0 0
83 1011 101553 4 1 0 0
84 1011 100253 3 1 0 0
85 1011 100241 3 1 0 0
86 1011 100240 3 1 0 0
87 1011 100239 3 1 0 0
88 1011 100238 3 1 0 0
89 1011 100237 3 1 0 0
90 1011 100236 3 1 0 0
91 1011 100261 3 1 0 0
92 1011 100246 3 1 0 0
93 1011 100245 3 1 0 0
94 1011 100242 3 1 0 0
95 1011 100243 3 1 0 0
96 1011 100254 3 1 0 0
97 1011 100338 3 1 0 0
98 1011 100337 3 1 0 0
99 1011 100336 3 1 0 0
100 1011 100248 3 1 0 0
101 1011 100247 3 1 0 0
102 1011 100244 3 1 0 0
103 1011 100259 3 1 0 0
104 1011 100257 3 1 0 0
105 1011 100258 3 1 0 0
106 1011 100636 3 1 0 0
107 1011 100634 3 1 0 0
108 1011 100255 3 1 0 0
109 1011 100256 3 1 0 0
110 1011 100252 3 1 0 0
111 1011 100638 3 1 0 0
112 1011 100639 3 1 0 0
113 1011 100637 3 1 0 0
114 1011 100772 3 1 0 0
115 1011 100667 3 1 0 0
116 1011 100666 3 1 0 0
117 1011 100665 3 1 0 0
118 1011 100643 3 1 0 0
119 1011 100640 3 1 0 0
120 1011 100641 3 1 0 0
121 1011 100642 3 1 0 0
122 1011 100688 3 1 0 0
123 1011 100645 3 1 0 0
124 1011 100646 3 1 0 0
125 1011 100644 3 1 0 0
126 1012 100644 3 1 0 0
127 1012 100646 3 1 0 0
128 1012 100645 3 1 0 0
129 1012 100688 3 1 0 0
130 1012 100642 3 1 0 0
131 1012 100641 3 1 0 0
132 1012 100640 3 1 0 0
133 1012 100643 3 1 0 0
134 1012 100665 3 1 0 0
135 1012 100666 3 1 0 0
136 1012 100667 3 1 0 0
137 1012 100634 3 1 0 0
138 1012 100636 3 1 0 0
139 1012 100772 3 1 0 0
140 1012 100638 3 1 0 0
141 1012 100637 3 1 0 0
142 1012 100639 3 1 0 0
143 1012 100252 3 1 0 0
144 1012 100256 3 1 0 0
145 1012 100255 3 1 0 0
146 1012 100258 3 1 0 0
147 1012 100257 3 1 0 0
148 1012 100259 3 1 0 0
149 1012 100244 3 1 0 0
150 1012 100247 3 1 0 0
151 1012 100248 3 1 0 0
152 1012 100336 3 1 0 0
153 1012 100337 3 1 0 0
154 1012 100338 3 1 0 0
155 1012 100254 3 1 0 0
156 1012 100243 3 1 0 0
157 1012 100242 3 1 0 0
158 1012 100245 3 1 0 0
159 1012 100246 3 1 0 0
160 1012 100261 3 1 0 0
161 1012 100236 3 1 0 0
162 1012 100237 3 1 0 0
163 1012 100238 3 1 0 0
164 1012 100239 3 1 0 0
165 1012 100240 3 1 0 0
166 1012 100241 3 1 0 0
167 1012 100253 3 1 0 0
168 1012 100454 4 1 0 0
169 1012 100980 4 1 0 0
170 1012 101553 4 1 0 0
171 1074 100985 4 1 0 0
172 1074 100999 3 1 0 0
173 1074 101000 3 1 0 0
174 1074 101023 2 1 0 0
175 1074 101024 2 1 0 0
176 1075 100060 4 1 0 0
177 1075 100434 4 1 0 0
178 1075 100059 3 1 0 0
179 1075 100268 3 1 0 0
180 1075 100420 3 1 0 0
181 1075 100763 3 1 0 0
182 1075 101003 3 1 0 0
183 1075 100057 2 1 0 0
184 1075 100179 2 1 0 0
185 1075 100229 2 1 0 0
186 1075 100345 2 1 0 0
187 1075 100415 2 1 0 0
188 1076 100054 4 1 0 0
189 1076 100282 4 1 0 0
190 1076 100726 4 1 0 0
191 1076 100053 3 1 0 0
192 1076 100269 3 1 0 0
193 1076 100422 3 1 0 0
194 1076 100757 3 1 0 0
195 1076 100051 2 1 0 0
196 1076 100180 2 1 0 0
197 1076 100230 2 1 0 0
198 1076 100346 2 1 0 0
199 1076 100414 2 1 0 0
200 1077 100984 4 1 0 1
201 1077 100997 3 1 0 1
202 1077 100998 3 1 0 1
203 1077 100986 4 1 0 1
204 1077 101001 3 1 0 1
205 1077 101002 3 1 0 1
206 1077 101025 2 1 0 1
207 1077 101026 2 1 0 1
208 1077 101027 2 1 0 1
209 1081 100987 4 1 0 0
210 1081 100988 4 1 0 0
211 1081 101003 3 1 0 0
212 1085 100008 4 1 0 1
213 1085 100017 4 1 0 1
214 1085 100026 4 1 0 1
215 1085 100034 4 1 0 1
216 1085 100041 4 1 0 1
217 1085 100048 4 1 0 1
218 1085 100054 4 1 0 1
219 1085 100060 4 1 0 1
220 1085 100066 4 1 0 1
221 1085 100078 4 1 0 1
222 1085 100072 4 1 0 1
223 1085 100084 4 1 0 1
224 1085 100090 4 1 0 1
225 1085 100282 4 1 0 1
226 1085 100285 4 1 0 1
227 1085 100284 4 1 0 1
228 1085 100286 4 1 0 1
229 1085 100280 4 1 0 1
230 1085 100276 4 1 0 1
231 1085 100277 4 1 0 1
232 1085 100275 4 1 0 1
233 1085 100278 4 1 0 1
234 1085 100431 4 1 0 1
235 1085 100407 4 1 0 1
236 1085 100432 4 1 0 1
237 1085 100433 4 1 0 1
238 1085 100434 4 1 0 1
239 1085 100435 4 1 0 1
240 1085 100436 4 1 0 1
241 1085 100437 4 1 0 1
242 1085 100438 4 1 0 1
243 1085 100439 4 1 0 1
244 1085 100760 4 1 0 1
245 1085 100761 4 1 0 1
246 1085 100779 4 1 0 1
247 1085 100767 4 1 0 1
248 1085 100780 4 1 0 1
249 1085 100784 4 1 0 1
250 1085 100768 4 1 0 1
251 1085 100725 4 1 0 1
252 1085 100726 4 1 0 1
253 1085 100984 4 1 0 1
254 1085 100985 4 1 0 1
255 1085 100987 4 1 0 1
256 1085 100988 4 1 0 1
257 1085 100986 4 1 0 1
258 1085 100989 4 1 0 1
259 1085 100982 4 1 0 1
260 1085 100983 4 1 0 1
261 1085 100787 4 1 0 1
262 1085 101293 4 1 0 1
263 1085 101294 4 1 0 1
264 1085 101295 4 1 0 1
265 1085 101296 4 1 0 1
266 1085 101297 4 1 0 1
267 1085 101320 4 1 0 1
268 1085 101567 4 1 0 1
269 1085 101592 4 1 0 1
270 1085 101593 4 1 0 1
271 1085 101594 4 1 0 1
272 1085 101595 4 1 0 1
273 1089 100989 4 1 0 0
274 1089 101004 3 1 0 0
275 1089 101005 3 1 0 0
276 1104 101293 4 1 0 0
277 1104 101294 4 1 0 0
278 1104 101298 3 1 0 0
279 1111 100008 4 1 0 1
280 1111 100017 4 1 0 1
281 1111 100026 4 1 0 1
282 1111 100034 4 1 0 1
283 1111 100041 4 1 0 1
284 1111 100048 4 1 0 1
285 1111 100054 4 1 0 1
286 1111 100060 4 1 0 1
287 1111 100066 4 1 0 1
288 1111 100078 4 1 0 1
289 1111 100072 4 1 0 1
290 1111 100084 4 1 0 1
291 1111 100090 4 1 0 1
292 1111 100282 4 1 0 1
293 1111 100285 4 1 0 1
294 1111 100284 4 1 0 1
295 1111 100286 4 1 0 1
296 1111 100280 4 1 0 1
297 1111 100276 4 1 0 1
298 1111 100277 4 1 0 1
299 1111 100275 4 1 0 1
300 1111 100278 4 1 0 1
301 1111 100431 4 1 0 1
302 1111 100407 4 1 0 1
303 1111 100432 4 1 0 1
304 1111 100433 4 1 0 1
305 1111 100434 4 1 1 1
306 1111 100435 4 1 1 1
307 1111 100436 4 1 0 1
308 1111 100437 4 1 0 1
309 1111 100438 4 1 0 1
310 1111 100439 4 1 0 1
311 1111 100760 4 1 1 1
312 1111 100761 4 1 0 1
313 1111 100779 4 1 0 1
314 1111 100767 4 1 0 1
315 1111 100780 4 1 1 1
316 1111 100784 4 1 1 1
317 1111 100768 4 1 0 1
318 1111 100725 4 1 1 1
319 1111 100726 4 1 1 1
320 1111 100985 4 1 1 1
321 1111 100988 4 1 1 1
322 1111 100989 4 1 1 1
323 1111 100982 4 1 1 1
324 1111 100983 4 1 1 1
325 1111 101293 4 1 1 1
326 1111 101294 4 1 1 1
327 1111 101295 4 1 1 1
328 1111 101320 4 1 1 1
329 1135 101567 4 1 0 0
330 1135 101592 4 1 0 0
331 1135 101594 4 1 0 0
332 1135 101595 4 1 0 0
333 1135 101566 3 1 0 0
334 1135 101602 3 1 0 0
335 1135 101603 3 1 0 0
336 1135 101619 2 1 0 0
337 1156 101604 3 1 0 0
338 1156 101605 3 1 0 0
339 1156 101607 3 1 0 0
340 1156 101608 3 1 0 0
341 1156 101596 4 1 0 0
342 1156 101597 4 1 0 0
343 1156 101599 4 1 0 0
344 1156 101600 4 1 0 0
345 1149 100003 2 1 0 0
346 1149 100004 2 1 0 0
347 1149 100012 2 1 0 0
348 1149 100013 2 1 0 0
349 1149 100021 2 1 0 0
350 1149 100022 2 1 0 0
351 1149 100173 2 1 0 0
352 1149 100174 2 1 0 0
353 1149 100175 2 1 0 0
354 1149 100339 2 1 0 0
355 1149 100340 2 1 0 0
356 1149 100341 2 1 0 0
357 1149 100223 2 1 0 0
358 1149 100224 2 1 0 0
359 1149 100225 2 1 0 0
360 1149 100692 2 1 0 0
361 1149 100693 2 1 0 0
362 1149 100694 2 1 0 0
363 1149 101020 2 1 0 0
364 1149 101025 2 1 0 0
365 1149 100418 3 1 0 0
366 1149 101005 3 1 0 0
367 1149 100785 3 1 0 0
368 1149 100786 3 1 0 0
369 1149 101602 3 1 0 0
370 1149 101604 3 1 0 0
371 1149 100760 4 1 0 0
372 1149 100780 4 1 0 0
373 1149 100987 4 1 0 0
374 1149 101295 4 1 0 0
375 1149 101296 4 1 0 0
376 1149 101592 4 1 0 0
377 1163 100008 4 1 0 1
378 1163 100017 4 1 0 1
379 1163 100026 4 1 0 1
380 1163 100034 4 1 0 1
381 1163 100041 4 1 0 1
382 1163 100048 4 1 0 1
383 1163 100054 4 1 0 1
384 1163 100060 4 1 0 1
385 1163 100066 4 1 0 1
386 1163 100078 4 1 0 1
387 1163 100072 4 1 0 1
388 1163 100084 4 1 0 1
389 1163 100090 4 1 0 1
390 1163 100282 4 1 0 1
391 1163 100285 4 1 0 1
392 1163 100284 4 1 0 1
393 1163 100286 4 1 0 1
394 1163 100280 4 1 0 1
395 1163 100276 4 1 0 1
396 1163 100277 4 1 0 1
397 1163 100275 4 1 0 1
398 1163 100278 4 1 0 1
399 1163 100431 4 1 0 1
400 1163 100407 4 1 0 1
401 1163 100432 4 1 0 1
402 1163 100433 4 1 0 1
403 1163 100434 4 1 0 1
404 1163 100435 4 1 0 1
405 1163 100436 4 1 0 1
406 1163 100437 4 1 0 1
407 1163 100438 4 1 0 1
408 1163 100439 4 1 0 1
409 1163 100760 4 1 0 1
410 1163 100761 4 1 0 1
411 1163 100779 4 1 0 1
412 1163 100767 4 1 0 1
413 1163 100780 4 1 0 1
414 1163 100784 4 1 0 1
415 1163 100768 4 1 0 1
416 1163 100725 4 1 0 1
417 1163 100726 4 1 0 1
418 1163 100984 4 1 0 1
419 1163 100985 4 1 0 1
420 1163 100987 4 1 0 1
421 1163 100988 4 1 0 1
422 1163 100986 4 1 0 1
423 1163 100989 4 1 0 1
424 1163 100982 4 1 0 1
425 1163 100983 4 1 0 1
426 1163 100787 4 1 0 1
427 1163 101293 4 1 0 1
428 1163 101294 4 1 0 1
429 1163 101295 4 1 0 1
430 1163 101296 4 1 0 1
431 1163 101297 4 1 0 1
432 1163 101320 4 1 0 1
433 1163 101567 4 1 0 1
434 1164 100008 4 1 0 1
435 1164 100017 4 1 0 1
436 1164 100026 4 1 0 1
437 1164 100034 4 1 0 1
438 1164 100041 4 1 0 1
439 1164 100048 4 1 0 1
440 1164 100054 4 1 0 1
441 1164 100060 4 1 0 1
442 1164 100066 4 1 0 1
443 1164 100078 4 1 0 1
444 1164 100072 4 1 0 1
445 1164 100084 4 1 0 1
446 1164 100090 4 1 0 1
447 1164 100282 4 1 0 1
448 1164 100285 4 1 0 1
449 1164 100284 4 1 0 1
450 1164 100286 4 1 0 1
451 1164 100280 4 1 0 1
452 1164 100276 4 1 0 1
453 1164 100277 4 1 0 1
454 1164 100275 4 1 0 1
455 1164 100278 4 1 0 1
456 1164 100431 4 1 0 1
457 1164 100407 4 1 0 1
458 1164 100432 4 1 0 1
459 1164 100433 4 1 0 1
460 1164 100434 4 1 0 1
461 1164 100435 4 1 0 1
462 1164 100436 4 1 0 1
463 1164 100437 4 1 0 1
464 1164 100438 4 1 0 1
465 1164 100439 4 1 0 1
466 1164 100760 4 1 0 1
467 1164 100761 4 1 0 1
468 1164 100779 4 1 0 1
469 1164 100767 4 1 0 1
470 1164 100780 4 1 0 1
471 1164 100784 4 1 0 1
472 1164 100768 4 1 0 1
473 1164 100725 4 1 0 1
474 1164 100726 4 1 0 1
475 1164 100984 4 1 0 1
476 1164 100985 4 1 0 1
477 1164 100987 4 1 0 1
478 1164 100988 4 1 0 1
479 1164 100986 4 1 0 1
480 1164 100989 4 1 0 1
481 1164 100982 4 1 0 1
482 1164 100983 4 1 0 1
483 1164 100787 4 1 0 1
484 1164 101293 4 1 0 1
485 1164 101294 4 1 0 1
486 1164 101295 4 1 0 1
487 1164 101296 4 1 0 1
488 1164 101297 4 1 0 1
489 1164 101320 4 1 0 1
490 1164 101567 4 1 0 1
491 1164 101592 4 1 0 1
492 1164 101593 4 1 0 1
493 1164 101594 4 1 0 1
494 1164 101595 4 1 0 1
495 1164 101598 4 1 0 1
496 1164 101596 4 1 0 1
497 1164 101597 4 1 0 1
498 1164 101599 4 1 0 1
499 1164 101600 4 1 0 1
500 1141 101600 4 1 0 1
501 1141 101608 3 1 0 1

View File

@ -0,0 +1,69 @@
"version","gachaId","gachaName","type","kind","isCeiling","maxSelectPoint"
6,1011,"無料ガチャ",0,3,0,0
6,1012,"無料ガチャSR確定",0,3,0,0
6,1043,"レギュラーガチャ",0,0,0,0
6,1067,"例えるなら大人のパッションフルーツ
リゾートプールガチャ",0,1,0,0
6,1068,"柏木 咲姫
ピックアップガチャ",0,2,0,0
6,1069,"井之原 小星
ピックアップガチャ",0,2,0,0
6,1070,"目指すは優勝!
炎の体育祭リミテッドガチャ",0,1,1,110
6,1071,"星咲 あかり
ピックアップガチャ",0,2,0,0
6,1072,"藤沢 柚子
ピックアップガチャ",0,2,0,0
6,1073,"三角 葵
ピックアップガチャ",0,2,0,0
6,1074,"おくれてきた
Halloweenガチャ",0,1,0,0
6,1075,"早乙女 彩華
ピックアップガチャ",0,2,0,0
6,1076,"桜井 春菜
ピックアップガチャ",0,2,0,0
6,1077,"ふわふわすぺーす
お仕事体験リミテッドガチャ",0,1,1,110
6,1078,"高瀬 梨緒
ピックアップガチャ",0,2,0,0
6,1079,"結城 莉玖
ピックアップガチャ",0,2,0,0
6,1080,"藍原 椿
ピックアップガチャ",0,2,0,0
6,1081,"今夜はおうちでパーティ☆
メリクリガチャ",0,1,0,0
6,1082,"日向 千夏
ピックアップガチャ",0,2,0,0
6,1083,"柏木 美亜
ピックアップガチャ",0,2,0,0
6,1084,"東雲 つむぎ
ピックアップガチャ",0,2,0,0
6,1085,"謹賀新年
福袋ガチャ",0,0,1,33
6,1086,"逢坂 茜
ピックアップガチャ",0,2,0,0
6,1087,"珠洲島 有栖
ピックアップガチャ",0,2,0,0
6,1088,"九條 楓
ピックアップガチャ",0,2,0,0
6,1089,"冬の魔法
スーパーウルトラウィンターガチャ",0,1,0,0
6,1093,"高瀬 梨緒ピックアップガチャ",0,2,0,0
6,1094,"結城 莉玖ピックアップガチャ",0,2,0,0
6,1095,"藍原 椿ピックアップガチャ",0,2,0,0
6,1096,"早乙女 彩華ピックアップガチャ",0,2,0,0
6,1097,"桜井 春菜ピックアップガチャ",0,2,0,0
6,1098,"逢坂 茜ピックアップガチャ",0,2,0,0
6,1099,"九條 楓ピックアップガチャ",0,2,0,0
6,1100,"珠洲島 有栖ピックアップガチャ",0,2,0,0
6,1101,"LEAF属性オンリーガチャ",0,2,0,0
6,1102,"AQUA属性オンリーガチャ",0,2,0,0
6,1103,"FIRE属性オンリーガチャ",0,2,0,0
6,1104,"夜明け前の双星ガチャ",0,1,0,0
6,1105,"謎の洞窟 黄金は実在した!!ガチャ",0,1,0,0
6,1106,"スウィートブライダルリミテッドガチャ",0,1,0,0
6,1107,"忘れられない、愛(ピュア)とロックがここにある。ガチャ",0,1,0,0
6,1108,"メルティ夜ふかしガチャ",0,1,0,0
6,1109,"絵本の国のシューターズガチャ",0,1,0,0
6,1110,"オンゲキ R.E.D. PLUS 大感謝祭ガチャ",0,1,0,0
6,1111,"オンゲキ 3rd Anniversaryガチャ",0,1,1,33
1 version gachaId gachaName type kind isCeiling maxSelectPoint
2 6 1011 無料ガチャ 0 3 0 0
3 6 1012 無料ガチャ(SR確定) 0 3 0 0
4 6 1043 レギュラーガチャ 0 0 0 0
5 6 1067 例えるなら大人のパッションフルーツ リゾートプールガチャ 0 1 0 0
6 6 1068 柏木 咲姫 ピックアップガチャ 0 2 0 0
7 6 1069 井之原 小星 ピックアップガチャ 0 2 0 0
8 6 1070 目指すは優勝! 炎の体育祭リミテッドガチャ 0 1 1 110
9 6 1071 星咲 あかり ピックアップガチャ 0 2 0 0
10 6 1072 藤沢 柚子 ピックアップガチャ 0 2 0 0
11 6 1073 三角 葵 ピックアップガチャ 0 2 0 0
12 6 1074 おくれてきた Halloweenガチャ 0 1 0 0
13 6 1075 早乙女 彩華 ピックアップガチャ 0 2 0 0
14 6 1076 桜井 春菜 ピックアップガチャ 0 2 0 0
15 6 1077 ふわふわすぺーす お仕事体験リミテッドガチャ 0 1 1 110
16 6 1078 高瀬 梨緒 ピックアップガチャ 0 2 0 0
17 6 1079 結城 莉玖 ピックアップガチャ 0 2 0 0
18 6 1080 藍原 椿 ピックアップガチャ 0 2 0 0
19 6 1081 今夜はおうちでパーティ☆ メリクリガチャ 0 1 0 0
20 6 1082 日向 千夏 ピックアップガチャ 0 2 0 0
21 6 1083 柏木 美亜 ピックアップガチャ 0 2 0 0
22 6 1084 東雲 つむぎ ピックアップガチャ 0 2 0 0
23 6 1085 謹賀新年 福袋ガチャ 0 0 1 33
24 6 1086 逢坂 茜 ピックアップガチャ 0 2 0 0
25 6 1087 珠洲島 有栖 ピックアップガチャ 0 2 0 0
26 6 1088 九條 楓 ピックアップガチャ 0 2 0 0
27 6 1089 冬の魔法 スーパーウルトラウィンターガチャ 0 1 0 0
28 6 1093 高瀬 梨緒ピックアップガチャ 0 2 0 0
29 6 1094 結城 莉玖ピックアップガチャ 0 2 0 0
30 6 1095 藍原 椿ピックアップガチャ 0 2 0 0
31 6 1096 早乙女 彩華ピックアップガチャ 0 2 0 0
32 6 1097 桜井 春菜ピックアップガチャ 0 2 0 0
33 6 1098 逢坂 茜ピックアップガチャ 0 2 0 0
34 6 1099 九條 楓ピックアップガチャ 0 2 0 0
35 6 1100 珠洲島 有栖ピックアップガチャ 0 2 0 0
36 6 1101 LEAF属性オンリーガチャ 0 2 0 0
37 6 1102 AQUA属性オンリーガチャ 0 2 0 0
38 6 1103 FIRE属性オンリーガチャ 0 2 0 0
39 6 1104 夜明け前の双星ガチャ 0 1 0 0
40 6 1105 謎の洞窟 黄金は実在した!!ガチャ 0 1 0 0
41 6 1106 スウィートブライダルリミテッドガチャ 0 1 0 0
42 6 1107 忘れられない、愛(ピュア)とロックがここにある。ガチャ 0 1 0 0
43 6 1108 メルティ夜ふかしガチャ 0 1 0 0
44 6 1109 絵本の国のシューターズガチャ 0 1 0 0
45 6 1110 オンゲキ R.E.D. PLUS 大感謝祭ガチャ 0 1 0 0
46 6 1111 オンゲキ 3rd Anniversaryガチャ 0 1 1 33

25
titles/cm/config.py Normal file
View File

@ -0,0 +1,25 @@
from core.config import CoreConfig
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
)
@property
def loglevel(self) -> int:
return CoreConfig.str_to_loglevel(
CoreConfig.get_config_field(
self.__config, "cardmaker", "server", "loglevel", default="info"
)
)
class CardMakerConfig(dict):
def __init__(self) -> None:
self.server = CardMakerServerConfig(self)

13
titles/cm/const.py Normal file
View File

@ -0,0 +1,13 @@
class CardMakerConstants:
GAME_CODE = "SDED"
CONFIG_NAME = "cardmaker.yaml"
VER_CARD_MAKER = 0
VER_CARD_MAKER_135 = 1
VERSION_NAMES = ("Card Maker 1.34", "Card Maker 1.35")
@classmethod
def game_ver_to_string(cls, ver: int):
return cls.VERSION_NAMES[ver]

8
titles/cm/database.py Normal file
View File

@ -0,0 +1,8 @@
from core.data import Data
from core.config import CoreConfig
class CardMakerData(Data):
def __init__(self, cfg: CoreConfig) -> None:
super().__init__(cfg)
# empty Card Maker database

131
titles/cm/index.py Normal file
View File

@ -0,0 +1,131 @@
import json
import inflection
import yaml
import string
import logging
import coloredlogs
import zlib
from os import path
from typing import Tuple
from twisted.web.http import Request
from logging.handlers import TimedRotatingFileHandler
from core.config import CoreConfig
from titles.cm.config import CardMakerConfig
from titles.cm.const import CardMakerConstants
from titles.cm.base import CardMakerBase
from titles.cm.cm135 import CardMaker135
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.versions = [
CardMakerBase(core_cfg, self.game_cfg),
CardMaker135(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.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
)
@classmethod
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}"))
)
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}/{game_code}/$v/", "")
def render_POST(self, request: Request, version: int, url_path: str) -> bytes:
req_raw = request.content.getvalue()
url_split = url_path.split("/")
internal_ver = 0
endpoint = url_split[len(url_split) - 1]
print(f"version: {version}")
if version >= 130 and version < 135: # Card Maker
internal_ver = CardMakerConstants.VER_CARD_MAKER
elif version >= 135 and version < 136: # Card Maker 1.35
internal_ver = CardMakerConstants.VER_CARD_MAKER_135
if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32:
# If we get a 32 character long hex string, it's a hash and we're
# doing encrypted. The likelyhood of false positives is low but
# technically not 0
self.logger.error("Encryption not supported at this time")
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(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 Exception as e:
self.logger.error(f"Error handling v{version} method {endpoint} - {e}")
return zlib.compress(b'{"stat": "0"}')
if resp is None:
resp = {"returnCode": 1}
self.logger.info(f"Response {resp}")
return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8"))

325
titles/cm/read.py Normal file
View File

@ -0,0 +1,325 @@
from decimal import Decimal
import logging
import os
import re
import csv
import xml.etree.ElementTree as ET
from typing import Any, Dict, List, Optional
from read import BaseReader
from core.config import CoreConfig
from titles.ongeki.database import OngekiData
from titles.cm.const import CardMakerConstants
from titles.ongeki.const import OngekiConstants
from titles.ongeki.config import OngekiConfig
from titles.mai2.database import Mai2Data
from titles.mai2.const import Mai2Constants
from titles.chuni.database import ChuniData
from titles.chuni.const import ChuniConstants
class CardMakerReader(BaseReader):
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)
self.mai2_data = Mai2Data(config)
self.chuni_data = ChuniData(config)
try:
self.logger.info(
f"Start importer for {CardMakerConstants.game_ver_to_string(version)}"
)
except IndexError:
self.logger.error(f"Invalid Card Maker version {version}")
exit(1)
def _get_card_maker_directory(self, directory: str) -> str:
for root, dirs, files in os.walk(directory):
for dir in dirs:
if (
os.path.exists(f"{root}/{dir}/MU3")
and os.path.exists(f"{root}/{dir}/MAI")
and os.path.exists(f"{root}/{dir}/CHU")
):
return f"{root}/{dir}"
def read(self) -> None:
static_datas = {
"static_gachas.csv": "read_ongeki_gacha_csv",
"static_gacha_cards.csv": "read_ongeki_gacha_card_csv",
}
if self.bin_dir is not None:
data_dir = self._get_card_maker_directory(self.bin_dir)
self.read_chuni_card(f"{data_dir}/CHU/Data/A000/card")
self.read_chuni_gacha(f"{data_dir}/CHU/Data/A000/gacha")
self.read_mai2_card(f"{data_dir}/MAI/Data/A000/card")
for file, func in static_datas.items():
if os.path.exists(f"{self.bin_dir}/MU3/{file}"):
read_csv = getattr(CardMakerReader, func)
read_csv(self, f"{self.bin_dir}/MU3/{file}")
else:
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)
# ONGEKI (MU3) cnnot easily access the bin data(A000.pac)
# so only opt_dir will work for now
for dir in data_dirs:
self.read_chuni_card(f"{dir}/CHU/card")
self.read_chuni_gacha(f"{dir}/CHU/gacha")
self.read_ongeki_gacha(f"{dir}/MU3/gacha")
def read_chuni_card(self, base_dir: str) -> None:
self.logger.info(f"Reading cards from {base_dir}...")
version_ids = {
"v2_00": ChuniConstants.VER_CHUNITHM_NEW,
"v2_05": ChuniConstants.VER_CHUNITHM_NEW_PLUS,
# Chunithm SUN, ignore for now
"v2_10": ChuniConstants.VER_CHUNITHM_NEW_PLUS + 1
}
for root, dirs, files in os.walk(base_dir):
for dir in dirs:
if os.path.exists(f"{root}/{dir}/Card.xml"):
with open(f"{root}/{dir}/Card.xml", "r", encoding="utf-8") as f:
troot = ET.fromstring(f.read())
card_id = int(troot.find("name").find("id").text)
chara_name = troot.find("chuniCharaName").find("str").text
chara_id = troot.find("chuniCharaName").find("id").text
version = version_ids[
troot.find("netOpenName").find("str").text[:5]
]
present_name = troot.find("chuniPresentName").find("str").text
rarity = int(troot.find("rareType").text)
label = int(troot.find("labelType").text)
dif = int(troot.find("difType").text)
miss = int(troot.find("miss").text)
combo = int(troot.find("combo").text)
chain = int(troot.find("chain").text)
skill_name = troot.find("skillName").text
self.chuni_data.static.put_card(
version,
card_id,
charaName=chara_name,
charaId=chara_id,
presentName=present_name,
rarity=rarity,
labelType=label,
difType=dif,
miss=miss,
combo=combo,
chain=chain,
skillName=skill_name,
)
self.logger.info(f"Added chuni card {card_id}")
def read_chuni_gacha(self, base_dir: str) -> None:
self.logger.info(f"Reading gachas from {base_dir}...")
version_ids = {
"v2_00": ChuniConstants.VER_CHUNITHM_NEW,
"v2_05": ChuniConstants.VER_CHUNITHM_NEW_PLUS,
# Chunithm SUN, ignore for now
"v2_10": ChuniConstants.VER_CHUNITHM_NEW_PLUS + 1,
}
for root, dirs, files in os.walk(base_dir):
for dir in dirs:
if os.path.exists(f"{root}/{dir}/Gacha.xml"):
with open(f"{root}/{dir}/Gacha.xml", "r", encoding="utf-8") as f:
troot = ET.fromstring(f.read())
name = troot.find("gachaName").text
gacha_id = int(troot.find("name").find("id").text)
version = version_ids[
troot.find("netOpenName").find("str").text[:5]
]
ceiling_cnt = int(troot.find("ceilingNum").text)
gacha_type = int(troot.find("gachaType").text)
is_ceiling = (
True if troot.find("ceilingType").text == "1" else False
)
self.chuni_data.static.put_gacha(
version,
gacha_id,
name,
type=gacha_type,
isCeiling=is_ceiling,
ceilingCnt=ceiling_cnt,
)
self.logger.info(f"Added chuni gacha {gacha_id}")
for gacha_card in troot.find("infos").iter("GachaCardDataInfo"):
# get the card ID from the id element
card_id = gacha_card.find("cardName").find("id").text
# get the weight from the weight element
weight = int(gacha_card.find("weight").text)
# get the pickup flag from the pickup element
is_pickup = (
True if gacha_card.find("pickup").text == "1" else False
)
self.chuni_data.static.put_gacha_card(
gacha_id,
card_id,
weight=weight,
rarity=2,
isPickup=is_pickup,
)
self.logger.info(
f"Added chuni card {card_id} to gacha {gacha_id}"
)
def read_mai2_card(self, base_dir: str) -> None:
self.logger.info(f"Reading cards from {base_dir}...")
version_ids = {
"1.00": Mai2Constants.VER_MAIMAI_DX,
"1.05": Mai2Constants.VER_MAIMAI_DX_PLUS,
"1.09": Mai2Constants.VER_MAIMAI_DX_PLUS,
"1.10": Mai2Constants.VER_MAIMAI_DX_SPLASH,
"1.15": Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS,
"1.20": Mai2Constants.VER_MAIMAI_DX_UNIVERSE,
"1.25": Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS,
}
for root, dirs, files in os.walk(base_dir):
for dir in dirs:
if os.path.exists(f"{root}/{dir}/Card.xml"):
with open(f"{root}/{dir}/Card.xml", "r", encoding="utf-8") as f:
troot = ET.fromstring(f.read())
name = troot.find("name").find("str").text
card_id = int(troot.find("name").find("id").text)
version = version_ids[
troot.find("enableVersion").find("str").text
]
enabled = (
True if troot.find("disable").text == "false" else False
)
self.mai2_data.static.put_card(
version, card_id, name, enabled=enabled
)
self.logger.info(f"Added mai2 card {card_id}")
def read_ongeki_gacha_csv(self, file_path: str) -> None:
self.logger.info(f"Reading gachas from {file_path}...")
with open(file_path, encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
self.ongeki_data.static.put_gacha(
row["version"],
row["gachaId"],
row["gachaName"],
row["kind"],
type=row["type"],
isCeiling=True if row["isCeiling"] == "1" else False,
maxSelectPoint=row["maxSelectPoint"],
)
self.logger.info(f"Added ongeki gacha {row['gachaId']}")
def read_ongeki_gacha_card_csv(self, file_path: str) -> None:
self.logger.info(f"Reading gacha cards from {file_path}...")
with open(file_path, encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
self.ongeki_data.static.put_gacha_card(
row["gachaId"],
row["cardId"],
rarity=row["rarity"],
weight=row["weight"],
isPickup=True if row["isPickup"] == "1" else False,
isSelect=True if row["isSelect"] == "1" else False,
)
self.logger.info(f"Added ongeki card {row['cardId']} to gacha")
def read_ongeki_gacha(self, base_dir: str) -> None:
self.logger.info(f"Reading gachas from {base_dir}...")
# assuming some GachaKinds based on the GachaType
type_to_kind = {
"Normal": "Normal",
"Pickup": "Pickup",
"RecoverFiveShotFlag": "BonusRestored",
"Free": "Free",
"FreeSR": "Free",
}
for root, dirs, files in os.walk(base_dir):
for dir in dirs:
if os.path.exists(f"{root}/{dir}/Gacha.xml"):
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)
# 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"
)
continue
# 1140 is the first bright memory gacha
if gacha_id < 1140:
version = OngekiConstants.VER_ONGEKI_BRIGHT
else:
version = OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY
gacha_kind = OngekiConstants.CM_GACHA_KINDS[
type_to_kind[troot.find("Type").text]
].value
# hardcode which gachas get "Select Gacha" with 33 points
is_ceiling, max_select_point = 0, 0
if gacha_id in {1163, 1164, 1165, 1166, 1167, 1168}:
is_ceiling = 1
max_select_point = 33
self.ongeki_data.static.put_gacha(
version,
gacha_id,
name,
gacha_kind,
isCeiling=is_ceiling,
maxSelectPoint=max_select_point,
)
self.logger.info(f"Added ongeki gacha {gacha_id}")

View File

@ -0,0 +1 @@
__all__ = []

View File

@ -6,16 +6,5 @@ from titles.cxb.read import CxbReader
index = CxbServlet
database = CxbData
reader = CxbReader
use_default_title = False
include_protocol = True
title_secure = True
game_codes = [CxbConstants.GAME_CODE]
trailing_slash = True
use_default_host = False
include_port = True
uri = "http://$h:$p/" # If you care about the allnet response you're probably running with no SSL
host = ""
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

@ -7,7 +7,8 @@ import re
import inflection
import logging, coloredlogs
from logging.handlers import TimedRotatingFileHandler
from typing import Dict
from typing import Dict, Tuple
from os import path
from core.config import CoreConfig
from titles.cxb.config import CxbConfig
@ -16,54 +17,93 @@ 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
self.cfg_dir = cfg_dir
self.core_cfg = core_cfg
self.game_cfg = CxbConfig()
self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/cxb.yaml")))
if path.exists(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]:
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}"))
)
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}/{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"Ready on ports {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"Ready on port {self.game_cfg.server.port}"
)
def render_POST(self, request: Request):
version = 0
@ -80,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""
@ -103,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:
@ -112,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
@ -120,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

@ -6,13 +6,5 @@ from titles.diva.read import DivaReader
index = DivaServlet
database = DivaData
reader = DivaReader
use_default_title = True
include_protocol = True
title_secure = False
game_codes = [DivaConstants.GAME_CODE]
trailing_slash = True
use_default_host = False
host = ""
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,6 +1,8 @@
class DivaConstants():
class DivaConstants:
GAME_CODE = "SBZV"
CONFIG_NAME = "diva.yaml"
VER_PROJECT_DIVA_ARCADE = 0
VER_PROJECT_DIVA_ARCADE_FUTURE_TONE = 1
@ -8,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

@ -6,75 +6,120 @@ import zlib
import json
import urllib.parse
import base64
from os import path
from typing import Tuple
from core.config import CoreConfig
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()
self.game_cfg.update(yaml.safe_load(open(f"{cfg_dir}/diva.yaml")))
if path.exists(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]:
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}"))
)
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}/{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:
@ -91,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:

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