forked from Hay1tsme/artemis
Merge branch 'develop'
This commit is contained in:
commit
02040300b8
1
.gitignore
vendored
1
.gitignore
vendored
@ -158,5 +158,6 @@ cert/*
|
|||||||
!cert/server.pem
|
!cert/server.pem
|
||||||
config/*
|
config/*
|
||||||
deliver/*
|
deliver/*
|
||||||
|
*.gz
|
||||||
|
|
||||||
dbdump-*.json
|
dbdump-*.json
|
49
changelog.md
Normal file
49
changelog.md
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# Changelog
|
||||||
|
Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to.
|
||||||
|
|
||||||
|
## 2023042300
|
||||||
|
### Wacca
|
||||||
|
+ Time free now works properly
|
||||||
|
+ Fix reverse gate mission causing a fatal error
|
||||||
|
+ Other misc. fixes
|
||||||
|
+ Latest DB: 5
|
||||||
|
|
||||||
|
### Pokken
|
||||||
|
+ Added preliminary support
|
||||||
|
+ Nothing saves currently, but the game will boot and function properly.
|
||||||
|
|
||||||
|
### Initial D Zero
|
||||||
|
+ Added preliminary support
|
||||||
|
+ Nothing saves currently, but the game will boot and function for the most part.
|
||||||
|
|
||||||
|
### Mai2
|
||||||
|
+ Added support for Festival
|
||||||
|
+ Lasted DB Version: 4
|
||||||
|
|
||||||
|
### Ongeki
|
||||||
|
+ Misc fixes
|
||||||
|
+ Lasted DB Version: 4
|
||||||
|
|
||||||
|
### Diva
|
||||||
|
+ Misc fixes
|
||||||
|
+ Lasted DB Version: 4
|
||||||
|
|
||||||
|
### Chuni
|
||||||
|
+ Fix network encryption
|
||||||
|
+ Add `handle_remove_token_api_request` for event mode
|
||||||
|
|
||||||
|
### Allnet
|
||||||
|
+ Added download order support
|
||||||
|
+ It is up to the sysop to provide the INI file, and host the files.
|
||||||
|
+ ONLY for use with cabs. It's not checked currently, which it's why it's default disabled
|
||||||
|
+ YMMV, use at your own risk
|
||||||
|
+ When running develop mode, games that are not recognised will still be able to authenticate.
|
||||||
|
|
||||||
|
### Database
|
||||||
|
+ Add autoupgrade command
|
||||||
|
+ Invoke to automatically upgrade all schemas to their latest versions
|
||||||
|
|
||||||
|
+ `version` arg no longer required, leave it blank to update the game schema to latest if it isn't already
|
||||||
|
|
||||||
|
### Misc
|
||||||
|
+ Update example nginx config file
|
@ -10,6 +10,7 @@ from Crypto.PublicKey import RSA
|
|||||||
from Crypto.Hash import SHA
|
from Crypto.Hash import SHA
|
||||||
from Crypto.Signature import PKCS1_v1_5
|
from Crypto.Signature import PKCS1_v1_5
|
||||||
from time import strptime
|
from time import strptime
|
||||||
|
from os import path
|
||||||
|
|
||||||
from core.config import CoreConfig
|
from core.config import CoreConfig
|
||||||
from core.utils import Utils
|
from core.utils import Utils
|
||||||
@ -55,7 +56,7 @@ class AllnetServlet:
|
|||||||
self.logger.error("No games detected!")
|
self.logger.error("No games detected!")
|
||||||
|
|
||||||
for _, mod in plugins.items():
|
for _, mod in plugins.items():
|
||||||
if hasattr(mod.index, "get_allnet_info"):
|
if hasattr(mod, "index") and hasattr(mod.index, "get_allnet_info"):
|
||||||
for code in mod.game_codes:
|
for code in mod.game_codes:
|
||||||
enabled, uri, host = mod.index.get_allnet_info(
|
enabled, uri, host = mod.index.get_allnet_info(
|
||||||
code, self.config, self.config_folder
|
code, self.config, self.config_folder
|
||||||
@ -95,14 +96,23 @@ class AllnetServlet:
|
|||||||
|
|
||||||
self.logger.debug(f"Allnet request: {vars(req)}")
|
self.logger.debug(f"Allnet request: {vars(req)}")
|
||||||
if req.game_id not in self.uri_registry:
|
if req.game_id not in self.uri_registry:
|
||||||
msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}."
|
if not self.config.server.is_develop:
|
||||||
self.data.base.log_event(
|
msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}."
|
||||||
"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)
|
)
|
||||||
|
self.logger.warn(msg)
|
||||||
|
|
||||||
resp.stat = 0
|
resp.stat = 0
|
||||||
return self.dict_to_http_form_string([vars(resp)])
|
return self.dict_to_http_form_string([vars(resp)])
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.logger.info(
|
||||||
|
f"Allowed unknown game {req.game_id} v{req.ver} to authenticate from {request_ip} due to 'is_develop' being enabled. S/N: {req.serial}"
|
||||||
|
)
|
||||||
|
resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/{req.game_id}/{req.ver.replace('.', '')}/"
|
||||||
|
resp.host = f"{self.config.title.hostname}:{self.config.title.port}"
|
||||||
|
return self.dict_to_http_form_string([vars(resp)])
|
||||||
|
|
||||||
resp.uri, resp.host = self.uri_registry[req.game_id]
|
resp.uri, resp.host = self.uri_registry[req.game_id]
|
||||||
|
|
||||||
@ -181,15 +191,51 @@ class AllnetServlet:
|
|||||||
self.logger.error(e)
|
self.logger.error(e)
|
||||||
return b""
|
return b""
|
||||||
|
|
||||||
self.logger.info(f"DownloadOrder from {request_ip} -> {req.game_id} v{req.ver} serial {req.serial}")
|
self.logger.info(
|
||||||
|
f"DownloadOrder from {request_ip} -> {req.game_id} v{req.ver} serial {req.serial}"
|
||||||
|
)
|
||||||
resp = AllnetDownloadOrderResponse()
|
resp = AllnetDownloadOrderResponse()
|
||||||
|
|
||||||
if not self.config.allnet.allow_online_updates:
|
if (
|
||||||
|
not self.config.allnet.allow_online_updates
|
||||||
|
or not self.config.allnet.update_cfg_folder
|
||||||
|
):
|
||||||
return self.dict_to_http_form_string([vars(resp)])
|
return self.dict_to_http_form_string([vars(resp)])
|
||||||
|
|
||||||
else: # TODO: Actual dlorder response
|
else: # TODO: Keychip check
|
||||||
|
if path.exists(
|
||||||
|
f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver}-app.ini"
|
||||||
|
):
|
||||||
|
resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-app.ini"
|
||||||
|
|
||||||
|
if path.exists(
|
||||||
|
f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver}-opt.ini"
|
||||||
|
):
|
||||||
|
resp.uri += f"|http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-opt.ini"
|
||||||
|
|
||||||
|
self.logger.debug(f"Sending download uri {resp.uri}")
|
||||||
return self.dict_to_http_form_string([vars(resp)])
|
return self.dict_to_http_form_string([vars(resp)])
|
||||||
|
|
||||||
|
def handle_dlorder_ini(self, request: Request, match: Dict) -> bytes:
|
||||||
|
if "file" not in match:
|
||||||
|
return b""
|
||||||
|
|
||||||
|
req_file = match["file"].replace("%0A", "")
|
||||||
|
|
||||||
|
if path.exists(f"{self.config.allnet.update_cfg_folder}/{req_file}"):
|
||||||
|
return open(
|
||||||
|
f"{self.config.allnet.update_cfg_folder}/{req_file}", "rb"
|
||||||
|
).read()
|
||||||
|
|
||||||
|
self.logger.info(f"DL INI File {req_file} not found")
|
||||||
|
return b""
|
||||||
|
|
||||||
|
def handle_dlorder_report(self, request: Request, match: Dict) -> bytes:
|
||||||
|
self.logger.info(
|
||||||
|
f"DLI Report from {Utils.get_ip_addr(request)}: {request.content.getvalue()}"
|
||||||
|
)
|
||||||
|
return b""
|
||||||
|
|
||||||
def handle_billing_request(self, request: Request, _: Dict):
|
def handle_billing_request(self, request: Request, _: Dict):
|
||||||
req_dict = self.billing_req_to_dict(request.content.getvalue())
|
req_dict = self.billing_req_to_dict(request.content.getvalue())
|
||||||
request_ip = Utils.get_ip_addr(request)
|
request_ip = Utils.get_ip_addr(request)
|
||||||
@ -412,7 +458,7 @@ class AllnetDownloadOrderRequest:
|
|||||||
|
|
||||||
|
|
||||||
class AllnetDownloadOrderResponse:
|
class AllnetDownloadOrderResponse:
|
||||||
def __init__(self, stat: int = 1, serial: str = "", uri: str = "null") -> None:
|
def __init__(self, stat: int = 1, serial: str = "", uri: str = "") -> None:
|
||||||
self.stat = stat
|
self.stat = stat
|
||||||
self.serial = serial
|
self.serial = serial
|
||||||
self.uri = uri
|
self.uri = uri
|
||||||
|
@ -188,6 +188,12 @@ class AllnetConfig:
|
|||||||
self.__config, "core", "allnet", "allow_online_updates", default=False
|
self.__config, "core", "allnet", "allow_online_updates", default=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def update_cfg_folder(self) -> str:
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "core", "allnet", "update_cfg_folder", default=""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BillingConfig:
|
class BillingConfig:
|
||||||
def __init__(self, parent_config: "CoreConfig") -> None:
|
def __init__(self, parent_config: "CoreConfig") -> None:
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import logging, coloredlogs
|
import logging, coloredlogs
|
||||||
from typing import Optional
|
from typing import Optional, Dict, List
|
||||||
from sqlalchemy.orm import scoped_session, sessionmaker
|
from sqlalchemy.orm import scoped_session, sessionmaker
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
@ -32,7 +32,7 @@ class Data:
|
|||||||
self.arcade = ArcadeData(self.config, self.session)
|
self.arcade = ArcadeData(self.config, self.session)
|
||||||
self.card = CardData(self.config, self.session)
|
self.card = CardData(self.config, self.session)
|
||||||
self.base = BaseData(self.config, self.session)
|
self.base = BaseData(self.config, self.session)
|
||||||
self.schema_ver_latest = 4
|
self.current_schema_version = 4
|
||||||
|
|
||||||
log_fmt_str = "[%(asctime)s] %(levelname)s | Database | %(message)s"
|
log_fmt_str = "[%(asctime)s] %(levelname)s | Database | %(message)s"
|
||||||
log_fmt = logging.Formatter(log_fmt_str)
|
log_fmt = logging.Formatter(log_fmt_str)
|
||||||
@ -71,7 +71,9 @@ class Data:
|
|||||||
games = Utils.get_all_titles()
|
games = Utils.get_all_titles()
|
||||||
for game_dir, game_mod in games.items():
|
for game_dir, game_mod in games.items():
|
||||||
try:
|
try:
|
||||||
if hasattr(game_mod, "database") and hasattr(game_mod, "current_schema_version"):
|
if hasattr(game_mod, "database") and hasattr(
|
||||||
|
game_mod, "current_schema_version"
|
||||||
|
):
|
||||||
game_mod.database(self.config)
|
game_mod.database(self.config)
|
||||||
metadata.create_all(self.__engine.connect())
|
metadata.create_all(self.__engine.connect())
|
||||||
|
|
||||||
@ -84,8 +86,8 @@ class Data:
|
|||||||
f"Could not load database schema from {game_dir} - {e}"
|
f"Could not load database schema from {game_dir} - {e}"
|
||||||
)
|
)
|
||||||
|
|
||||||
self.logger.info(f"Setting base_schema_ver to {self.schema_ver_latest}")
|
self.logger.info(f"Setting base_schema_ver to {self.current_schema_version}")
|
||||||
self.base.set_schema_ver(self.schema_ver_latest)
|
self.base.set_schema_ver(self.current_schema_version)
|
||||||
|
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
f"Setting user auto_incrememnt to {self.config.database.user_table_autoincrement_start}"
|
f"Setting user auto_incrememnt to {self.config.database.user_table_autoincrement_start}"
|
||||||
@ -129,9 +131,32 @@ class Data:
|
|||||||
|
|
||||||
self.create_database()
|
self.create_database()
|
||||||
|
|
||||||
def migrate_database(self, game: str, version: int, action: str) -> None:
|
def migrate_database(self, game: str, version: Optional[int], action: str) -> None:
|
||||||
old_ver = self.base.get_schema_ver(game)
|
old_ver = self.base.get_schema_ver(game)
|
||||||
sql = ""
|
sql = ""
|
||||||
|
if version is None:
|
||||||
|
if not game == "CORE":
|
||||||
|
titles = Utils.get_all_titles()
|
||||||
|
|
||||||
|
for folder, mod in titles.items():
|
||||||
|
if not mod.game_codes[0] == game:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if hasattr(mod, "current_schema_version"):
|
||||||
|
version = mod.current_schema_version
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.logger.warn(
|
||||||
|
f"current_schema_version not found for {folder}"
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
version = self.current_schema_version
|
||||||
|
|
||||||
|
if version is None:
|
||||||
|
self.logger.warn(
|
||||||
|
f"Could not determine latest version for {game}, please specify --version"
|
||||||
|
)
|
||||||
|
|
||||||
if old_ver is None:
|
if old_ver is None:
|
||||||
self.logger.error(
|
self.logger.error(
|
||||||
@ -166,7 +191,7 @@ class Data:
|
|||||||
if result is None:
|
if result is None:
|
||||||
self.logger.error("Error execuing sql script!")
|
self.logger.error("Error execuing sql script!")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
else:
|
else:
|
||||||
for x in range(old_ver, version, -1):
|
for x in range(old_ver, version, -1):
|
||||||
if not os.path.exists(
|
if not os.path.exists(
|
||||||
@ -263,17 +288,48 @@ class Data:
|
|||||||
self.user.delete_user(user["id"])
|
self.user.delete_user(user["id"])
|
||||||
|
|
||||||
def autoupgrade(self) -> None:
|
def autoupgrade(self) -> None:
|
||||||
all_games = self.base.get_all_schema_vers()
|
all_game_versions = self.base.get_all_schema_vers()
|
||||||
if all_games is None:
|
if all_game_versions is None:
|
||||||
self.logger.warn("Failed to get schema versions")
|
self.logger.warn("Failed to get schema versions")
|
||||||
|
return
|
||||||
for x in all_games:
|
|
||||||
|
all_games = Utils.get_all_titles()
|
||||||
|
all_games_list: Dict[str, int] = {}
|
||||||
|
for _, mod in all_games.items():
|
||||||
|
if hasattr(mod, "current_schema_version"):
|
||||||
|
all_games_list[mod.game_codes[0]] = mod.current_schema_version
|
||||||
|
|
||||||
|
for x in all_game_versions:
|
||||||
|
failed = False
|
||||||
game = x["game"].upper()
|
game = x["game"].upper()
|
||||||
update_ver = 1
|
update_ver = int(x["version"])
|
||||||
for y in range(2, 100):
|
latest_ver = all_games_list.get(game, 1)
|
||||||
|
if game == "CORE":
|
||||||
|
latest_ver = self.current_schema_version
|
||||||
|
|
||||||
|
if update_ver == latest_ver:
|
||||||
|
self.logger.info(f"{game} is already latest version")
|
||||||
|
continue
|
||||||
|
|
||||||
|
for y in range(update_ver + 1, latest_ver + 1):
|
||||||
if os.path.exists(f"core/data/schema/versions/{game}_{y}_upgrade.sql"):
|
if os.path.exists(f"core/data/schema/versions/{game}_{y}_upgrade.sql"):
|
||||||
update_ver = y
|
with open(
|
||||||
|
f"core/data/schema/versions/{game}_{y}_upgrade.sql",
|
||||||
|
"r",
|
||||||
|
encoding="utf-8",
|
||||||
|
) as f:
|
||||||
|
sql = f.read()
|
||||||
|
|
||||||
|
result = self.base.execute(sql)
|
||||||
|
if result is None:
|
||||||
|
self.logger.error(
|
||||||
|
f"Error execuing sql script for game {game} v{y}!"
|
||||||
|
)
|
||||||
|
failed = True
|
||||||
|
break
|
||||||
else:
|
else:
|
||||||
break
|
self.logger.warning(f"Could not find script {game}_{y}_upgrade.sql")
|
||||||
|
failed = True
|
||||||
self.migrate_database(game, update_ver, "upgrade")
|
|
||||||
|
if not failed:
|
||||||
|
self.base.set_schema_ver(latest_ver, game)
|
||||||
|
@ -47,7 +47,7 @@ class BaseData:
|
|||||||
res = None
|
res = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.logger.info(f"SQL Execute: {''.join(str(sql).splitlines())} || {opts}")
|
self.logger.info(f"SQL Execute: {''.join(str(sql).splitlines())}")
|
||||||
res = self.conn.execute(text(sql), opts)
|
res = self.conn.execute(text(sql), opts)
|
||||||
|
|
||||||
except SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
@ -81,7 +81,7 @@ class BaseData:
|
|||||||
Generate a random 5-7 digit id
|
Generate a random 5-7 digit id
|
||||||
"""
|
"""
|
||||||
return randrange(10000, 9999999)
|
return randrange(10000, 9999999)
|
||||||
|
|
||||||
def get_all_schema_vers(self) -> Optional[List[Row]]:
|
def get_all_schema_vers(self) -> Optional[List[Row]]:
|
||||||
sql = select(schema_ver)
|
sql = select(schema_ver)
|
||||||
|
|
||||||
|
31
core/data/schema/versions/SDEZ_3_rollback.sql
Normal file
31
core/data/schema/versions/SDEZ_3_rollback.sql
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
ALTER TABLE mai2_profile_option
|
||||||
|
DROP COLUMN tapSe;
|
||||||
|
|
||||||
|
ALTER TABLE mai2_score_best
|
||||||
|
DROP COLUMN extNum1;
|
||||||
|
|
||||||
|
ALTER TABLE mai2_profile_extend
|
||||||
|
DROP COLUMN playStatusSetting;
|
||||||
|
|
||||||
|
ALTER TABLE mai2_playlog
|
||||||
|
DROP COLUMN extNum4;
|
||||||
|
|
||||||
|
ALTER TABLE mai2_static_event
|
||||||
|
DROP COLUMN startDate;
|
||||||
|
|
||||||
|
ALTER TABLE mai2_item_map
|
||||||
|
CHANGE COLUMN mapId map_id INT NOT NULL,
|
||||||
|
CHANGE COLUMN isLock is_lock BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
CHANGE COLUMN isClear is_clear BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
CHANGE COLUMN isComplete is_complete BOOLEAN NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
ALTER TABLE mai2_item_friend_season_ranking
|
||||||
|
CHANGE COLUMN seasonId season_id INT NOT NULL,
|
||||||
|
CHANGE COLUMN rewardGet reward_get BOOLEAN NOT NULL,
|
||||||
|
CHANGE COLUMN userName user_name VARCHAR(8) NOT NULL,
|
||||||
|
CHANGE COLUMN recordDate record_date VARCHAR(255) NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE mai2_item_login_bonus
|
||||||
|
CHANGE COLUMN bonusId bonus_id INT NOT NULL,
|
||||||
|
CHANGE COLUMN isCurrent is_current BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
CHANGE COLUMN isComplete is_complete BOOLEAN NOT NULL DEFAULT 0;
|
31
core/data/schema/versions/SDEZ_4_upgrade.sql
Normal file
31
core/data/schema/versions/SDEZ_4_upgrade.sql
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
ALTER TABLE mai2_profile_option
|
||||||
|
ADD COLUMN tapSe INT NOT NULL DEFAULT 0 AFTER tapDesign;
|
||||||
|
|
||||||
|
ALTER TABLE mai2_score_best
|
||||||
|
ADD COLUMN extNum1 INT NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
ALTER TABLE mai2_profile_extend
|
||||||
|
ADD COLUMN playStatusSetting INT NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
ALTER TABLE mai2_playlog
|
||||||
|
ADD COLUMN extNum4 INT NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
ALTER TABLE mai2_static_event
|
||||||
|
ADD COLUMN startDate TIMESTAMP NOT NULL DEFAULT current_timestamp();
|
||||||
|
|
||||||
|
ALTER TABLE mai2_item_map
|
||||||
|
CHANGE COLUMN map_id mapId INT NOT NULL,
|
||||||
|
CHANGE COLUMN is_lock isLock BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
CHANGE COLUMN is_clear isClear BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
CHANGE COLUMN is_complete isComplete BOOLEAN NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
ALTER TABLE mai2_item_friend_season_ranking
|
||||||
|
CHANGE COLUMN season_id seasonId INT NOT NULL,
|
||||||
|
CHANGE COLUMN reward_get rewardGet BOOLEAN NOT NULL,
|
||||||
|
CHANGE COLUMN user_name userName VARCHAR(8) NOT NULL,
|
||||||
|
CHANGE COLUMN record_date recordDate TIMESTAMP NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE mai2_item_login_bonus
|
||||||
|
CHANGE COLUMN bonus_id bonusId INT NOT NULL,
|
||||||
|
CHANGE COLUMN is_current isCurrent BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
CHANGE COLUMN is_complete isComplete BOOLEAN NOT NULL DEFAULT 0;
|
2
core/data/schema/versions/SDFE_3_rollback.sql
Normal file
2
core/data/schema/versions/SDFE_3_rollback.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
SET FOREIGN_KEY_CHECKS=0;
|
||||||
|
SET FOREIGN_KEY_CHECKS=1;
|
1
core/data/schema/versions/SDFE_4_rollback.sql
Normal file
1
core/data/schema/versions/SDFE_4_rollback.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE wacca_profile DROP COLUMN playcount_time_free;
|
1
core/data/schema/versions/SDFE_4_upgrade.sql
Normal file
1
core/data/schema/versions/SDFE_4_upgrade.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
DELETE FROM wacca_item WHERE type=17 AND item_id=312002;
|
1
core/data/schema/versions/SDFE_5_upgrade.sql
Normal file
1
core/data/schema/versions/SDFE_5_upgrade.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE wacca_profile ADD playcount_time_free int(11) DEFAULT 0 NULL AFTER playcount_stageup;
|
@ -71,8 +71,11 @@ class FrontendServlet(resource.Resource):
|
|||||||
game_fe = game_mod.frontend(cfg, self.environment, config_dir)
|
game_fe = game_mod.frontend(cfg, self.environment, config_dir)
|
||||||
self.game_list.append({"url": game_dir, "name": game_fe.nav_name})
|
self.game_list.append({"url": game_dir, "name": game_fe.nav_name})
|
||||||
fe_game.putChild(game_dir.encode(), game_fe)
|
fe_game.putChild(game_dir.encode(), game_fe)
|
||||||
except:
|
|
||||||
raise
|
except Exception as e:
|
||||||
|
self.logger.error(
|
||||||
|
f"Failed to import frontend from {game_dir} because {e}"
|
||||||
|
)
|
||||||
|
|
||||||
self.environment.globals["game_list"] = self.game_list
|
self.environment.globals["game_list"] = self.game_list
|
||||||
self.putChild(b"gate", FE_Gate(cfg, self.environment))
|
self.putChild(b"gate", FE_Gate(cfg, self.environment))
|
||||||
|
@ -46,9 +46,7 @@ class MuchaServlet:
|
|||||||
if enabled:
|
if enabled:
|
||||||
self.mucha_registry.append(game_cd)
|
self.mucha_registry.append(game_cd)
|
||||||
|
|
||||||
self.logger.info(
|
self.logger.info(f"Serving {len(self.mucha_registry)} games")
|
||||||
f"Serving {len(self.mucha_registry)} games"
|
|
||||||
)
|
|
||||||
|
|
||||||
def handle_boardauth(self, request: Request, _: Dict) -> bytes:
|
def handle_boardauth(self, request: Request, _: Dict) -> bytes:
|
||||||
req_dict = self.mucha_preprocess(request.content.getvalue())
|
req_dict = self.mucha_preprocess(request.content.getvalue())
|
||||||
@ -62,9 +60,7 @@ class MuchaServlet:
|
|||||||
|
|
||||||
req = MuchaAuthRequest(req_dict)
|
req = MuchaAuthRequest(req_dict)
|
||||||
self.logger.debug(f"Mucha request {vars(req)}")
|
self.logger.debug(f"Mucha request {vars(req)}")
|
||||||
self.logger.info(
|
self.logger.info(f"Boardauth request from {client_ip} for {req.gameVer}")
|
||||||
f"Boardauth request from {client_ip} for {req.gameVer}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if req.gameCd not in self.mucha_registry:
|
if req.gameCd not in self.mucha_registry:
|
||||||
self.logger.warn(f"Unknown gameCd {req.gameCd}")
|
self.logger.warn(f"Unknown gameCd {req.gameCd}")
|
||||||
@ -92,9 +88,7 @@ class MuchaServlet:
|
|||||||
|
|
||||||
req = MuchaUpdateRequest(req_dict)
|
req = MuchaUpdateRequest(req_dict)
|
||||||
self.logger.debug(f"Mucha request {vars(req)}")
|
self.logger.debug(f"Mucha request {vars(req)}")
|
||||||
self.logger.info(
|
self.logger.info(f"Updatecheck request from {client_ip} for {req.gameVer}")
|
||||||
f"Updatecheck request from {client_ip} for {req.gameVer}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if req.gameCd not in self.mucha_registry:
|
if req.gameCd not in self.mucha_registry:
|
||||||
self.logger.warn(f"Unknown gameCd {req.gameCd}")
|
self.logger.warn(f"Unknown gameCd {req.gameCd}")
|
||||||
|
@ -16,13 +16,20 @@ class Utils:
|
|||||||
if not dir.startswith("__"):
|
if not dir.startswith("__"):
|
||||||
try:
|
try:
|
||||||
mod = importlib.import_module(f"titles.{dir}")
|
mod = importlib.import_module(f"titles.{dir}")
|
||||||
ret[dir] = mod
|
if hasattr(mod, "game_codes") and hasattr(
|
||||||
|
mod, "index"
|
||||||
|
): # Minimum required to function
|
||||||
|
ret[dir] = mod
|
||||||
|
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
logging.getLogger("core").error(f"get_all_titles: {dir} - {e}")
|
logging.getLogger("core").error(f"get_all_titles: {dir} - {e}")
|
||||||
raise
|
raise
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_ip_addr(cls, req: Request) -> str:
|
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
|
return (
|
||||||
|
req.getAllHeaders()[b"x-forwarded-for"].decode()
|
||||||
|
if b"x-forwarded-for" in req.getAllHeaders()
|
||||||
|
else req.getClientAddress().host
|
||||||
|
)
|
||||||
|
27
dbutils.py
27
dbutils.py
@ -1,5 +1,6 @@
|
|||||||
import yaml
|
import yaml
|
||||||
import argparse
|
import argparse
|
||||||
|
import logging
|
||||||
from core.config import CoreConfig
|
from core.config import CoreConfig
|
||||||
from core.data import Data
|
from core.data import Data
|
||||||
from os import path, mkdir, access, W_OK
|
from os import path, mkdir, access, W_OK
|
||||||
@ -32,11 +33,13 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
cfg = CoreConfig()
|
cfg = CoreConfig()
|
||||||
if path.exists(f"{args.config}/core.yaml"):
|
if path.exists(f"{args.config}/core.yaml"):
|
||||||
cfg.update(yaml.safe_load(open(f"{args.config}/core.yaml")))
|
cfg_dict = yaml.safe_load(open(f"{args.config}/core.yaml"))
|
||||||
|
cfg_dict.get("database", {})["loglevel"] = "info"
|
||||||
|
cfg.update(cfg_dict)
|
||||||
|
|
||||||
if not path.exists(cfg.server.log_dir):
|
if not path.exists(cfg.server.log_dir):
|
||||||
mkdir(cfg.server.log_dir)
|
mkdir(cfg.server.log_dir)
|
||||||
|
|
||||||
if not access(cfg.server.log_dir, W_OK):
|
if not access(cfg.server.log_dir, W_OK):
|
||||||
print(
|
print(
|
||||||
f"Log directory {cfg.server.log_dir} NOT writable, please check permissions"
|
f"Log directory {cfg.server.log_dir} NOT writable, please check permissions"
|
||||||
@ -44,7 +47,6 @@ if __name__ == "__main__":
|
|||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
data = Data(cfg)
|
data = Data(cfg)
|
||||||
|
|
||||||
|
|
||||||
if args.action == "create":
|
if args.action == "create":
|
||||||
data.create_database()
|
data.create_database()
|
||||||
@ -54,15 +56,22 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
elif args.action == "upgrade" or args.action == "rollback":
|
elif args.action == "upgrade" or args.action == "rollback":
|
||||||
if args.version is None:
|
if args.version is None:
|
||||||
data.logger.error("Must set game and version to migrate to")
|
data.logger.warn("No version set, upgrading to latest")
|
||||||
exit(0)
|
|
||||||
|
|
||||||
if args.game is None:
|
if args.game is None:
|
||||||
data.logger.info("No game set, upgrading core schema")
|
data.logger.warn("No game set, upgrading core schema")
|
||||||
data.migrate_database("CORE", int(args.version), args.action)
|
data.migrate_database(
|
||||||
|
"CORE",
|
||||||
|
int(args.version) if args.version is not None else None,
|
||||||
|
args.action,
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
data.migrate_database(args.game, int(args.version), args.action)
|
data.migrate_database(
|
||||||
|
args.game,
|
||||||
|
int(args.version) if args.version is not None else None,
|
||||||
|
args.action,
|
||||||
|
)
|
||||||
|
|
||||||
elif args.action == "autoupgrade":
|
elif args.action == "autoupgrade":
|
||||||
data.autoupgrade()
|
data.autoupgrade()
|
||||||
|
@ -64,8 +64,7 @@ which version is the latest, f.e. `SDBT_3_upgrade.sql`. In order to upgrade to v
|
|||||||
perform all previous updates as well:
|
perform all previous updates as well:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
python dbutils.py --game SDBT --version 2 upgrade
|
python dbutils.py --game SDBT upgrade
|
||||||
python dbutils.py --game SDBT --version 3 upgrade
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## crossbeats REV.
|
## crossbeats REV.
|
||||||
@ -114,6 +113,7 @@ Config file is located in `config/cxb.yaml`.
|
|||||||
| 3 | maimai DX Splash PLUS |
|
| 3 | maimai DX Splash PLUS |
|
||||||
| 4 | maimai DX Universe |
|
| 4 | maimai DX Universe |
|
||||||
| 5 | maimai DX Universe PLUS |
|
| 5 | maimai DX Universe PLUS |
|
||||||
|
| 6 | maimai DX Festival |
|
||||||
|
|
||||||
### Importer
|
### Importer
|
||||||
|
|
||||||
@ -126,14 +126,14 @@ python read.py --series SDEZ --version <version ID> --binfolder /path/to/game/fo
|
|||||||
The importer for maimai DX will import Events, Music and Tickets.
|
The importer for maimai DX will import Events, Music and Tickets.
|
||||||
|
|
||||||
**NOTE: It is required to use the importer because the game will
|
**NOTE: It is required to use the importer because the game will
|
||||||
crash without it!**
|
crash without Events!**
|
||||||
|
|
||||||
### Database upgrade
|
### 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:
|
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
|
```shell
|
||||||
python dbutils.py --game SDEZ --version 2 upgrade
|
python dbutils.py --game SDEZ upgrade
|
||||||
```
|
```
|
||||||
|
|
||||||
## Hatsune Miku Project Diva
|
## Hatsune Miku Project Diva
|
||||||
@ -174,9 +174,7 @@ which version is the latest, f.e. `SBZV_4_upgrade.sql`. In order to upgrade to v
|
|||||||
perform all previous updates as well:
|
perform all previous updates as well:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
python dbutils.py --game SBZV --version 2 upgrade
|
python dbutils.py --game SBZV upgrade
|
||||||
python dbutils.py --game SBZV --version 3 upgrade
|
|
||||||
python dbutils.py --game SBZV --version 4 upgrade
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## O.N.G.E.K.I.
|
## O.N.G.E.K.I.
|
||||||
@ -224,9 +222,7 @@ which version is the latest, f.e. `SDDT_4_upgrade.sql`. In order to upgrade to v
|
|||||||
perform all previous updates as well:
|
perform all previous updates as well:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
python dbutils.py --game SDDT --version 2 upgrade
|
python dbutils.py --game SDDT upgrade
|
||||||
python dbutils.py --game SDDT --version 3 upgrade
|
|
||||||
python dbutils.py --game SDDT --version 4 upgrade
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Card Maker
|
## Card Maker
|
||||||
@ -346,6 +342,5 @@ Config file is located in `config/wacca.yaml`.
|
|||||||
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:
|
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
|
```shell
|
||||||
python dbutils.py --game SDFE --version 2 upgrade
|
python dbutils.py --game SDFE upgrade
|
||||||
python dbutils.py --game SDFE --version 3 upgrade
|
|
||||||
```
|
```
|
||||||
|
@ -2,5 +2,19 @@ server:
|
|||||||
enable: True
|
enable: True
|
||||||
loglevel: "info"
|
loglevel: "info"
|
||||||
|
|
||||||
|
team:
|
||||||
|
name: ARTEMiS
|
||||||
|
|
||||||
|
mods:
|
||||||
|
use_login_bonus: True
|
||||||
|
|
||||||
|
version:
|
||||||
|
11:
|
||||||
|
rom: 2.00.00
|
||||||
|
data: 2.00.00
|
||||||
|
12:
|
||||||
|
rom: 2.05.00
|
||||||
|
data: 2.05.00
|
||||||
|
|
||||||
crypto:
|
crypto:
|
||||||
encrypted_only: False
|
encrypted_only: False
|
@ -32,6 +32,7 @@ allnet:
|
|||||||
loglevel: "info"
|
loglevel: "info"
|
||||||
port: 80
|
port: 80
|
||||||
allow_online_updates: False
|
allow_online_updates: False
|
||||||
|
update_cfg_folder: ""
|
||||||
|
|
||||||
billing:
|
billing:
|
||||||
port: 8443
|
port: 8443
|
||||||
|
11
example_config/idz.yaml
Normal file
11
example_config/idz.yaml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
server:
|
||||||
|
enable: True
|
||||||
|
loglevel: "info"
|
||||||
|
hostname: ""
|
||||||
|
news: ""
|
||||||
|
aes_key: ""
|
||||||
|
|
||||||
|
ports:
|
||||||
|
userdb: 10000
|
||||||
|
match: 10010
|
||||||
|
echo: 10020
|
@ -5,4 +5,5 @@ server:
|
|||||||
port: 9000
|
port: 9000
|
||||||
port_stun: 9001
|
port_stun: 9001
|
||||||
port_turn: 9002
|
port_turn: 9002
|
||||||
port_admission: 9003
|
port_admission: 9003
|
||||||
|
auto_register: True
|
@ -29,5 +29,8 @@ gates:
|
|||||||
- 17
|
- 17
|
||||||
- 18
|
- 18
|
||||||
- 19
|
- 19
|
||||||
|
- 20
|
||||||
- 21
|
- 21
|
||||||
- 22
|
- 22
|
||||||
|
- 23
|
||||||
|
- 24
|
||||||
|
17
index.py
17
index.py
@ -26,6 +26,22 @@ class HttpDispatcher(resource.Resource):
|
|||||||
self.title = TitleServlet(cfg, config_dir)
|
self.title = TitleServlet(cfg, config_dir)
|
||||||
self.mucha = MuchaServlet(cfg, config_dir)
|
self.mucha = MuchaServlet(cfg, config_dir)
|
||||||
|
|
||||||
|
self.map_get.connect(
|
||||||
|
"allnet_downloadorder_ini",
|
||||||
|
"/dl/ini/{file}",
|
||||||
|
controller="allnet",
|
||||||
|
action="handle_dlorder_ini",
|
||||||
|
conditions=dict(method=["GET"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.map_post.connect(
|
||||||
|
"allnet_downloadorder_report",
|
||||||
|
"/dl/report",
|
||||||
|
controller="allnet",
|
||||||
|
action="handle_dlorder_report",
|
||||||
|
conditions=dict(method=["POST"]),
|
||||||
|
)
|
||||||
|
|
||||||
self.map_post.connect(
|
self.map_post.connect(
|
||||||
"allnet_ping",
|
"allnet_ping",
|
||||||
"/naomitest.html",
|
"/naomitest.html",
|
||||||
@ -95,6 +111,7 @@ class HttpDispatcher(resource.Resource):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def render_GET(self, request: Request) -> bytes:
|
def render_GET(self, request: Request) -> bytes:
|
||||||
|
self.logger.debug(request.uri)
|
||||||
test = self.map_get.match(request.uri.decode())
|
test = self.map_get.match(request.uri.decode())
|
||||||
client_ip = Utils.get_ip_addr(request)
|
client_ip = Utils.get_ip_addr(request)
|
||||||
|
|
||||||
|
3
read.py
3
read.py
@ -135,8 +135,7 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
for dir, mod in titles.items():
|
for dir, mod in titles.items():
|
||||||
if args.series in mod.game_codes:
|
if args.series in mod.game_codes:
|
||||||
handler = mod.reader(config, args.version,
|
handler = mod.reader(config, args.version, bin_arg, opt_arg, args.extra)
|
||||||
bin_arg, opt_arg, args.extra)
|
|
||||||
handler.read()
|
handler.read()
|
||||||
|
|
||||||
logger.info("Done")
|
logger.info("Done")
|
||||||
|
@ -9,8 +9,8 @@ Games listed below have been tested and confirmed working. Only game versions ol
|
|||||||
+ Crossbeats Rev
|
+ Crossbeats Rev
|
||||||
+ All versions + omnimix
|
+ All versions + omnimix
|
||||||
|
|
||||||
+ Maimai
|
+ maimai DX
|
||||||
+ All versions up to Universe Plus
|
+ All versions up to Festival
|
||||||
|
|
||||||
+ Hatsune Miku Arcade
|
+ Hatsune Miku Arcade
|
||||||
+ All versions
|
+ All versions
|
||||||
@ -26,6 +26,8 @@ Games listed below have been tested and confirmed working. Only game versions ol
|
|||||||
+ Lily R
|
+ Lily R
|
||||||
+ Reverse
|
+ Reverse
|
||||||
|
|
||||||
|
+ Pokken
|
||||||
|
+ Final Online
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
- python 3 (tested working with 3.9 and 3.10, other versions YMMV)
|
- python 3 (tested working with 3.9 and 3.10, other versions YMMV)
|
||||||
|
@ -7,4 +7,4 @@ index = ChuniServlet
|
|||||||
database = ChuniData
|
database = ChuniData
|
||||||
reader = ChuniReader
|
reader = ChuniReader
|
||||||
game_codes = [ChuniConstants.GAME_CODE, ChuniConstants.GAME_CODE_NEW]
|
game_codes = [ChuniConstants.GAME_CODE, ChuniConstants.GAME_CODE_NEW]
|
||||||
current_schema_version = 1
|
current_schema_version = 3
|
||||||
|
@ -23,7 +23,98 @@ class ChuniBase:
|
|||||||
self.version = ChuniConstants.VER_CHUNITHM
|
self.version = ChuniConstants.VER_CHUNITHM
|
||||||
|
|
||||||
def handle_game_login_api_request(self, data: Dict) -> Dict:
|
def handle_game_login_api_request(self, data: Dict) -> Dict:
|
||||||
# self.data.base.log_event("chuni", "login", logging.INFO, {"version": self.version, "user": data["userId"]})
|
"""
|
||||||
|
Handles the login bonus logic, required for the game because
|
||||||
|
getUserLoginBonus gets called after getUserItem and therefore the
|
||||||
|
items needs to be inserted in the database before they get requested.
|
||||||
|
|
||||||
|
Adds a bonusCount after a user logged in after 24 hours, makes sure
|
||||||
|
loginBonus 30 gets looped, only show the login banner every 24 hours,
|
||||||
|
adds the bonus to items (itemKind 6)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ignore the login bonus if disabled in config
|
||||||
|
if not self.game_cfg.mods.use_login_bonus:
|
||||||
|
return {"returnCode": 1}
|
||||||
|
|
||||||
|
user_id = data["userId"]
|
||||||
|
login_bonus_presets = self.data.static.get_login_bonus_presets(self.version)
|
||||||
|
|
||||||
|
for preset in login_bonus_presets:
|
||||||
|
# check if a user already has some pogress and if not add the
|
||||||
|
# login bonus entry
|
||||||
|
user_login_bonus = self.data.item.get_login_bonus(
|
||||||
|
user_id, self.version, preset["id"]
|
||||||
|
)
|
||||||
|
if user_login_bonus is None:
|
||||||
|
self.data.item.put_login_bonus(user_id, self.version, preset["id"])
|
||||||
|
# yeah i'm lazy
|
||||||
|
user_login_bonus = self.data.item.get_login_bonus(
|
||||||
|
user_id, self.version, preset["id"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# skip the login bonus entirely if its already finished
|
||||||
|
if user_login_bonus["isFinished"]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# make sure the last login is more than 24 hours ago
|
||||||
|
if user_login_bonus["lastUpdateDate"] < datetime.now() - timedelta(
|
||||||
|
hours=24
|
||||||
|
):
|
||||||
|
# increase the login day counter and update the last login date
|
||||||
|
bonus_count = user_login_bonus["bonusCount"] + 1
|
||||||
|
last_update_date = datetime.now()
|
||||||
|
|
||||||
|
all_login_boni = self.data.static.get_login_bonus(
|
||||||
|
self.version, preset["id"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# skip the current bonus preset if no boni were found
|
||||||
|
if all_login_boni is None or len(all_login_boni) < 1:
|
||||||
|
self.logger.warn(
|
||||||
|
f"No bonus entries found for bonus preset {preset['id']}"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
max_needed_days = all_login_boni[0]["needLoginDayCount"]
|
||||||
|
|
||||||
|
# make sure to not show login boni after all days got redeemed
|
||||||
|
is_finished = False
|
||||||
|
if bonus_count > max_needed_days:
|
||||||
|
# assume that all login preset ids under 3000 needs to be
|
||||||
|
# looped, like 30 and 40 are looped, 40 does not work?
|
||||||
|
if preset["id"] < 3000:
|
||||||
|
bonus_count = 1
|
||||||
|
else:
|
||||||
|
is_finished = True
|
||||||
|
|
||||||
|
# grab the item for the corresponding day
|
||||||
|
login_item = self.data.static.get_login_bonus_by_required_days(
|
||||||
|
self.version, preset["id"], bonus_count
|
||||||
|
)
|
||||||
|
if login_item is not None:
|
||||||
|
# now add the present to the database so the
|
||||||
|
# handle_get_user_item_api_request can grab them
|
||||||
|
self.data.item.put_item(
|
||||||
|
user_id,
|
||||||
|
{
|
||||||
|
"itemId": login_item["presentId"],
|
||||||
|
"itemKind": 6,
|
||||||
|
"stock": login_item["itemNum"],
|
||||||
|
"isValid": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.data.item.put_login_bonus(
|
||||||
|
user_id,
|
||||||
|
self.version,
|
||||||
|
preset["id"],
|
||||||
|
bonusCount=bonus_count,
|
||||||
|
lastUpdateDate=last_update_date,
|
||||||
|
isWatched=False,
|
||||||
|
isFinished=is_finished,
|
||||||
|
)
|
||||||
|
|
||||||
return {"returnCode": 1}
|
return {"returnCode": 1}
|
||||||
|
|
||||||
def handle_game_logout_api_request(self, data: Dict) -> Dict:
|
def handle_game_logout_api_request(self, data: Dict) -> Dict:
|
||||||
@ -32,7 +123,7 @@ class ChuniBase:
|
|||||||
|
|
||||||
def handle_get_game_charge_api_request(self, data: Dict) -> Dict:
|
def handle_get_game_charge_api_request(self, data: Dict) -> Dict:
|
||||||
game_charge_list = self.data.static.get_enabled_charges(self.version)
|
game_charge_list = self.data.static.get_enabled_charges(self.version)
|
||||||
|
|
||||||
if game_charge_list is None or len(game_charge_list) == 0:
|
if game_charge_list is None or len(game_charge_list) == 0:
|
||||||
return {"length": 0, "gameChargeList": []}
|
return {"length": 0, "gameChargeList": []}
|
||||||
|
|
||||||
@ -130,7 +221,7 @@ class ChuniBase:
|
|||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"length": len(activity_list),
|
"length": len(activity_list),
|
||||||
"kind": data["kind"],
|
"kind": int(data["kind"]),
|
||||||
"userActivityList": activity_list,
|
"userActivityList": activity_list,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -309,26 +400,29 @@ class ChuniBase:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict:
|
||||||
"""
|
user_id = data["userId"]
|
||||||
Unsure how to get this to trigger...
|
user_login_bonus = self.data.item.get_all_login_bonus(user_id, self.version)
|
||||||
"""
|
# ignore the loginBonus request if its disabled in config
|
||||||
|
if user_login_bonus is None or not self.game_cfg.mods.use_login_bonus:
|
||||||
|
return {"userId": user_id, "length": 0, "userLoginBonusList": []}
|
||||||
|
|
||||||
|
user_login_list = []
|
||||||
|
for bonus in user_login_bonus:
|
||||||
|
user_login_list.append(
|
||||||
|
{
|
||||||
|
"presetId": bonus["presetId"],
|
||||||
|
"bonusCount": bonus["bonusCount"],
|
||||||
|
"lastUpdateDate": datetime.strftime(
|
||||||
|
bonus["lastUpdateDate"], "%Y-%m-%d %H:%M:%S"
|
||||||
|
),
|
||||||
|
"isWatched": bonus["isWatched"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": user_id,
|
||||||
"length": 2,
|
"length": len(user_login_list),
|
||||||
"userLoginBonusList": [
|
"userLoginBonusList": user_login_list,
|
||||||
{
|
|
||||||
"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",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_map_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_map_api_request(self, data: Dict) -> Dict:
|
||||||
@ -451,13 +545,13 @@ class ChuniBase:
|
|||||||
"playerLevel": profile["playerLevel"],
|
"playerLevel": profile["playerLevel"],
|
||||||
"rating": profile["rating"],
|
"rating": profile["rating"],
|
||||||
"headphone": profile["headphone"],
|
"headphone": profile["headphone"],
|
||||||
"chargeState": "1",
|
"chargeState": 1,
|
||||||
"userNameEx": profile["userName"],
|
"userNameEx": profile["userName"],
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict:
|
||||||
recet_rating_list = self.data.profile.get_profile_recent_rating(data["userId"])
|
recent_rating_list = self.data.profile.get_profile_recent_rating(data["userId"])
|
||||||
if recet_rating_list is None:
|
if recent_rating_list is None:
|
||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"length": 0,
|
"length": 0,
|
||||||
@ -466,8 +560,8 @@ class ChuniBase:
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"length": len(recet_rating_list["recentRating"]),
|
"length": len(recent_rating_list["recentRating"]),
|
||||||
"userRecentRatingList": recet_rating_list["recentRating"],
|
"userRecentRatingList": recent_rating_list["recentRating"],
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_region_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_region_api_request(self, data: Dict) -> Dict:
|
||||||
@ -479,8 +573,24 @@ class ChuniBase:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_team_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_team_api_request(self, data: Dict) -> Dict:
|
||||||
# TODO: Team
|
# TODO: use the database "chuni_profile_team" with a GUI
|
||||||
return {"userId": data["userId"], "teamId": 0}
|
team_name = self.game_cfg.team.team_name
|
||||||
|
if team_name == "":
|
||||||
|
return {"userId": data["userId"], "teamId": 0}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"userId": data["userId"],
|
||||||
|
"teamId": 1,
|
||||||
|
"teamRank": 1,
|
||||||
|
"teamName": team_name,
|
||||||
|
"userTeamPoint": {
|
||||||
|
"userId": data["userId"],
|
||||||
|
"teamId": 1,
|
||||||
|
"orderId": 1,
|
||||||
|
"teamPoint": 1,
|
||||||
|
"aggrDate": data["playDate"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
def handle_get_team_course_setting_api_request(self, data: Dict) -> Dict:
|
def handle_get_team_course_setting_api_request(self, data: Dict) -> Dict:
|
||||||
return {
|
return {
|
||||||
@ -580,9 +690,18 @@ class ChuniBase:
|
|||||||
for emoney in upsert["userEmoneyList"]:
|
for emoney in upsert["userEmoneyList"]:
|
||||||
self.data.profile.put_profile_emoney(user_id, emoney)
|
self.data.profile.put_profile_emoney(user_id, emoney)
|
||||||
|
|
||||||
|
if "userLoginBonusList" in upsert:
|
||||||
|
for login in upsert["userLoginBonusList"]:
|
||||||
|
self.data.item.put_login_bonus(
|
||||||
|
user_id, self.version, login["presetId"], isWatched=True
|
||||||
|
)
|
||||||
|
|
||||||
return {"returnCode": "1"}
|
return {"returnCode": "1"}
|
||||||
|
|
||||||
def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict:
|
def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict:
|
||||||
|
# add tickets after they got bought, this makes sure the tickets are
|
||||||
|
# still valid after an unsuccessful logout
|
||||||
|
self.data.profile.put_profile_charge(data["userId"], data["userCharge"])
|
||||||
return {"returnCode": "1"}
|
return {"returnCode": "1"}
|
||||||
|
|
||||||
def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict:
|
def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict:
|
||||||
@ -603,7 +722,5 @@ class ChuniBase:
|
|||||||
def handle_get_user_net_battle_data_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_net_battle_data_api_request(self, data: Dict) -> Dict:
|
||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"userNetBattleData": {
|
"userNetBattleData": {"recentNBSelectMusicList": []},
|
||||||
"recentNBSelectMusicList": []
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -21,6 +21,42 @@ class ChuniServerConfig:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ChuniTeamConfig:
|
||||||
|
def __init__(self, parent_config: "ChuniConfig") -> None:
|
||||||
|
self.__config = parent_config
|
||||||
|
|
||||||
|
@property
|
||||||
|
def team_name(self) -> str:
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "chuni", "team", "name", default=""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ChuniModsConfig:
|
||||||
|
def __init__(self, parent_config: "ChuniConfig") -> None:
|
||||||
|
self.__config = parent_config
|
||||||
|
|
||||||
|
@property
|
||||||
|
def use_login_bonus(self) -> bool:
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "chuni", "mods", "use_login_bonus", default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ChuniVersionConfig:
|
||||||
|
def __init__(self, parent_config: "ChuniConfig") -> None:
|
||||||
|
self.__config = parent_config
|
||||||
|
|
||||||
|
def version(self, version: int) -> Dict:
|
||||||
|
"""
|
||||||
|
in the form of:
|
||||||
|
11: {"rom": 2.00.00, "data": 2.00.00}
|
||||||
|
"""
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "chuni", "version", default={}
|
||||||
|
)[version]
|
||||||
|
|
||||||
|
|
||||||
class ChuniCryptoConfig:
|
class ChuniCryptoConfig:
|
||||||
def __init__(self, parent_config: "ChuniConfig") -> None:
|
def __init__(self, parent_config: "ChuniConfig") -> None:
|
||||||
self.__config = parent_config
|
self.__config = parent_config
|
||||||
@ -46,4 +82,7 @@ class ChuniCryptoConfig:
|
|||||||
class ChuniConfig(dict):
|
class ChuniConfig(dict):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.server = ChuniServerConfig(self)
|
self.server = ChuniServerConfig(self)
|
||||||
|
self.team = ChuniTeamConfig(self)
|
||||||
|
self.mods = ChuniModsConfig(self)
|
||||||
|
self.version = ChuniVersionConfig(self)
|
||||||
self.crypto = ChuniCryptoConfig(self)
|
self.crypto = ChuniCryptoConfig(self)
|
||||||
|
@ -82,21 +82,33 @@ class ChuniServlet:
|
|||||||
level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str
|
level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str
|
||||||
)
|
)
|
||||||
self.logger.inited = True
|
self.logger.inited = True
|
||||||
|
|
||||||
for version, keys in self.game_cfg.crypto.keys.items():
|
for version, keys in self.game_cfg.crypto.keys.items():
|
||||||
if len(keys) < 3:
|
if len(keys) < 3:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self.hash_table[version] = {}
|
self.hash_table[version] = {}
|
||||||
|
|
||||||
method_list = [method for method in dir(self.versions[version]) if not method.startswith('__')]
|
method_list = [
|
||||||
|
method
|
||||||
|
for method in dir(self.versions[version])
|
||||||
|
if not method.startswith("__")
|
||||||
|
]
|
||||||
for method in method_list:
|
for method in method_list:
|
||||||
method_fixed = inflection.camelize(method)[6:-7]
|
method_fixed = inflection.camelize(method)[6:-7]
|
||||||
hash = PBKDF2(method_fixed, bytes.fromhex(keys[2]), 128, count=44, hmac_hash_module=SHA1)
|
hash = PBKDF2(
|
||||||
|
method_fixed,
|
||||||
|
bytes.fromhex(keys[2]),
|
||||||
|
128,
|
||||||
|
count=44,
|
||||||
|
hmac_hash_module=SHA1,
|
||||||
|
)
|
||||||
|
|
||||||
self.hash_table[version][hash.hex()] = method_fixed
|
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()}")
|
self.logger.debug(
|
||||||
|
f"Hashed v{version} method {method_fixed} with {bytes.fromhex(keys[2])} to get {hash.hex()}"
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_allnet_info(
|
def get_allnet_info(
|
||||||
@ -164,18 +176,22 @@ class ChuniServlet:
|
|||||||
# technically not 0
|
# technically not 0
|
||||||
if internal_ver < ChuniConstants.VER_CHUNITHM_NEW:
|
if internal_ver < ChuniConstants.VER_CHUNITHM_NEW:
|
||||||
endpoint = request.getHeader("User-Agent").split("#")[0]
|
endpoint = request.getHeader("User-Agent").split("#")[0]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if internal_ver not in self.hash_table:
|
if internal_ver not in self.hash_table:
|
||||||
self.logger.error(f"v{version} does not support encryption or no keys entered")
|
self.logger.error(
|
||||||
|
f"v{version} does not support encryption or no keys entered"
|
||||||
|
)
|
||||||
return zlib.compress(b'{"stat": "0"}')
|
return zlib.compress(b'{"stat": "0"}')
|
||||||
|
|
||||||
elif endpoint.lower() not in self.hash_table[internal_ver]:
|
elif endpoint.lower() not in self.hash_table[internal_ver]:
|
||||||
self.logger.error(f"No hash found for v{version} endpoint {endpoint}")
|
self.logger.error(
|
||||||
|
f"No hash found for v{version} endpoint {endpoint}"
|
||||||
|
)
|
||||||
return zlib.compress(b'{"stat": "0"}')
|
return zlib.compress(b'{"stat": "0"}')
|
||||||
|
|
||||||
endpoint = self.hash_table[internal_ver][endpoint.lower()]
|
endpoint = self.hash_table[internal_ver][endpoint.lower()]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
crypt = AES.new(
|
crypt = AES.new(
|
||||||
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][0]),
|
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][0]),
|
||||||
@ -193,7 +209,11 @@ class ChuniServlet:
|
|||||||
|
|
||||||
encrtped = True
|
encrtped = True
|
||||||
|
|
||||||
if not encrtped and self.game_cfg.crypto.encrypted_only and internal_ver >= ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS:
|
if (
|
||||||
|
not encrtped
|
||||||
|
and self.game_cfg.crypto.encrypted_only
|
||||||
|
and internal_ver >= ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS
|
||||||
|
):
|
||||||
self.logger.error(
|
self.logger.error(
|
||||||
f"Unencrypted v{version} {endpoint} request, but config is set to encrypted only: {req_raw}"
|
f"Unencrypted v{version} {endpoint} request, but config is set to encrypted only: {req_raw}"
|
||||||
)
|
)
|
||||||
@ -210,9 +230,7 @@ class ChuniServlet:
|
|||||||
|
|
||||||
req_data = json.loads(unzip)
|
req_data = json.loads(unzip)
|
||||||
|
|
||||||
self.logger.info(
|
self.logger.info(f"v{version} {endpoint} request from {client_ip}")
|
||||||
f"v{version} {endpoint} request from {client_ip}"
|
|
||||||
)
|
|
||||||
self.logger.debug(req_data)
|
self.logger.debug(req_data)
|
||||||
|
|
||||||
func_to_find = "handle_" + inflection.underscore(endpoint) + "_request"
|
func_to_find = "handle_" + inflection.underscore(endpoint) + "_request"
|
||||||
|
@ -49,8 +49,8 @@ class ChuniNew(ChuniBase):
|
|||||||
"matchEndTime": match_end,
|
"matchEndTime": match_end,
|
||||||
"matchTimeLimit": 99,
|
"matchTimeLimit": 99,
|
||||||
"matchErrorLimit": 9999,
|
"matchErrorLimit": 9999,
|
||||||
"romVersion": "2.00.00",
|
"romVersion": self.game_cfg.version.version(self.version)["rom"],
|
||||||
"dataVersion": "2.00.00",
|
"dataVersion": self.game_cfg.version.version(self.version)["data"],
|
||||||
"matchingUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
|
"matchingUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
|
||||||
"matchingUriX": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
|
"matchingUriX": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
|
||||||
"udpHolePunchUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
|
"udpHolePunchUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
|
||||||
@ -269,10 +269,10 @@ class ChuniNew(ChuniBase):
|
|||||||
tmp = user_print_list[x]._asdict()
|
tmp = user_print_list[x]._asdict()
|
||||||
print_list.append(tmp["cardId"])
|
print_list.append(tmp["cardId"])
|
||||||
|
|
||||||
if len(user_print_list) >= max_ct:
|
if len(print_list) >= max_ct:
|
||||||
break
|
break
|
||||||
|
|
||||||
if len(user_print_list) >= max_ct:
|
if len(print_list) >= max_ct:
|
||||||
next_idx = next_idx + max_ct
|
next_idx = next_idx + max_ct
|
||||||
else:
|
else:
|
||||||
next_idx = -1
|
next_idx = -1
|
||||||
@ -454,9 +454,7 @@ class ChuniNew(ChuniBase):
|
|||||||
|
|
||||||
# set the card print state to success and use the orderId as the key
|
# set the card print state to success and use the orderId as the key
|
||||||
self.data.item.put_user_print_state(
|
self.data.item.put_user_print_state(
|
||||||
user_id,
|
user_id, id=upsert["orderId"], hasCompleted=True
|
||||||
id=upsert["orderId"],
|
|
||||||
hasCompleted=True
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"returnCode": "1", "apiName": "CMUpsertUserPrintSubtractApi"}
|
return {"returnCode": "1", "apiName": "CMUpsertUserPrintSubtractApi"}
|
||||||
@ -467,10 +465,6 @@ class ChuniNew(ChuniBase):
|
|||||||
|
|
||||||
# set the card print state to success and use the orderId as the key
|
# set the card print state to success and use the orderId as the key
|
||||||
for order_id in order_ids:
|
for order_id in order_ids:
|
||||||
self.data.item.put_user_print_state(
|
self.data.item.put_user_print_state(user_id, id=order_id, hasCompleted=True)
|
||||||
user_id,
|
|
||||||
id=order_id,
|
|
||||||
hasCompleted=True
|
|
||||||
)
|
|
||||||
|
|
||||||
return {"returnCode": "1", "apiName": "CMUpsertUserPrintCancelApi"}
|
return {"returnCode": "1", "apiName": "CMUpsertUserPrintCancelApi"}
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
from datetime import datetime, timedelta
|
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
import pytz
|
|
||||||
|
|
||||||
from core.config import CoreConfig
|
from core.config import CoreConfig
|
||||||
from titles.chuni.new import ChuniNew
|
from titles.chuni.new import ChuniNew
|
||||||
@ -15,8 +13,12 @@ class ChuniNewPlus(ChuniNew):
|
|||||||
|
|
||||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
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"]["romVersion"] = "2.05.00"
|
ret["gameSetting"]["romVersion"] = self.game_cfg.version.version(self.version)[
|
||||||
ret["gameSetting"]["dataVersion"] = "2.05.00"
|
"rom"
|
||||||
|
]
|
||||||
|
ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version(self.version)[
|
||||||
|
"data"
|
||||||
|
]
|
||||||
ret["gameSetting"][
|
ret["gameSetting"][
|
||||||
"matchingUri"
|
"matchingUri"
|
||||||
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/"
|
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/"
|
||||||
|
@ -42,6 +42,80 @@ class ChuniReader(BaseReader):
|
|||||||
self.read_music(f"{dir}/music")
|
self.read_music(f"{dir}/music")
|
||||||
self.read_charges(f"{dir}/chargeItem")
|
self.read_charges(f"{dir}/chargeItem")
|
||||||
self.read_avatar(f"{dir}/avatarAccessory")
|
self.read_avatar(f"{dir}/avatarAccessory")
|
||||||
|
self.read_login_bonus(f"{dir}/")
|
||||||
|
|
||||||
|
def read_login_bonus(self, root_dir: str) -> None:
|
||||||
|
for root, dirs, files in walk(f"{root_dir}loginBonusPreset"):
|
||||||
|
for dir in dirs:
|
||||||
|
if path.exists(f"{root}/{dir}/LoginBonusPreset.xml"):
|
||||||
|
with open(f"{root}/{dir}/LoginBonusPreset.xml", "rb") as fp:
|
||||||
|
bytedata = fp.read()
|
||||||
|
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
|
||||||
|
is_enabled = (
|
||||||
|
True if xml_root.find("disableFlag").text == "false" else False
|
||||||
|
)
|
||||||
|
|
||||||
|
result = self.data.static.put_login_bonus_preset(
|
||||||
|
self.version, id, name, is_enabled
|
||||||
|
)
|
||||||
|
|
||||||
|
if result is not None:
|
||||||
|
self.logger.info(f"Inserted login bonus preset {id}")
|
||||||
|
else:
|
||||||
|
self.logger.warn(f"Failed to insert login bonus preset {id}")
|
||||||
|
|
||||||
|
for bonus in xml_root.find("infos").findall("LoginBonusDataInfo"):
|
||||||
|
for name in bonus.findall("loginBonusName"):
|
||||||
|
bonus_id = name.find("id").text
|
||||||
|
bonus_name = name.find("str").text
|
||||||
|
|
||||||
|
if path.exists(
|
||||||
|
f"{root_dir}/loginBonus/loginBonus{bonus_id}/LoginBonus.xml"
|
||||||
|
):
|
||||||
|
with open(
|
||||||
|
f"{root_dir}/loginBonus/loginBonus{bonus_id}/LoginBonus.xml",
|
||||||
|
"rb",
|
||||||
|
) as fp:
|
||||||
|
bytedata = fp.read()
|
||||||
|
strdata = bytedata.decode("UTF-8")
|
||||||
|
|
||||||
|
bonus_root = ET.fromstring(strdata)
|
||||||
|
|
||||||
|
for present in bonus_root.findall("present"):
|
||||||
|
present_id = present.find("id").text
|
||||||
|
present_name = present.find("str").text
|
||||||
|
|
||||||
|
item_num = int(bonus_root.find("itemNum").text)
|
||||||
|
need_login_day_count = int(
|
||||||
|
bonus_root.find("needLoginDayCount").text
|
||||||
|
)
|
||||||
|
login_bonus_category_type = int(
|
||||||
|
bonus_root.find("loginBonusCategoryType").text
|
||||||
|
)
|
||||||
|
|
||||||
|
result = self.data.static.put_login_bonus(
|
||||||
|
self.version,
|
||||||
|
id,
|
||||||
|
bonus_id,
|
||||||
|
bonus_name,
|
||||||
|
present_id,
|
||||||
|
present_name,
|
||||||
|
item_num,
|
||||||
|
need_login_day_count,
|
||||||
|
login_bonus_category_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result is not None:
|
||||||
|
self.logger.info(f"Inserted login bonus {bonus_id}")
|
||||||
|
else:
|
||||||
|
self.logger.warn(
|
||||||
|
f"Failed to insert login bonus {bonus_id}"
|
||||||
|
)
|
||||||
|
|
||||||
def read_events(self, evt_dir: str) -> None:
|
def read_events(self, evt_dir: str) -> None:
|
||||||
for root, dirs, files in walk(evt_dir):
|
for root, dirs, files in walk(evt_dir):
|
||||||
|
@ -184,8 +184,73 @@ print_detail = Table(
|
|||||||
mysql_charset="utf8mb4",
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
login_bonus = Table(
|
||||||
|
"chuni_item_login_bonus",
|
||||||
|
metadata,
|
||||||
|
Column("id", Integer, primary_key=True, nullable=False),
|
||||||
|
Column(
|
||||||
|
"user",
|
||||||
|
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
Column("version", Integer, nullable=False),
|
||||||
|
Column("presetId", Integer, nullable=False),
|
||||||
|
Column("bonusCount", Integer, nullable=False, server_default="0"),
|
||||||
|
Column("lastUpdateDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"),
|
||||||
|
Column("isWatched", Boolean, server_default="0"),
|
||||||
|
Column("isFinished", Boolean, server_default="0"),
|
||||||
|
UniqueConstraint("version", "user", "presetId", name="chuni_item_login_bonus_uk"),
|
||||||
|
mysql_charset="utf8mb4",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ChuniItemData(BaseData):
|
class ChuniItemData(BaseData):
|
||||||
|
def put_login_bonus(
|
||||||
|
self, user_id: int, version: int, preset_id: int, **login_bonus_data
|
||||||
|
) -> Optional[int]:
|
||||||
|
sql = insert(login_bonus).values(
|
||||||
|
version=version, user=user_id, presetId=preset_id, **login_bonus_data
|
||||||
|
)
|
||||||
|
|
||||||
|
conflict = sql.on_duplicate_key_update(presetId=preset_id, **login_bonus_data)
|
||||||
|
|
||||||
|
result = self.execute(conflict)
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
return result.lastrowid
|
||||||
|
|
||||||
|
def get_all_login_bonus(
|
||||||
|
self, user_id: int, version: int, is_finished: bool = False
|
||||||
|
) -> Optional[List[Row]]:
|
||||||
|
sql = login_bonus.select(
|
||||||
|
and_(
|
||||||
|
login_bonus.c.version == version,
|
||||||
|
login_bonus.c.user == user_id,
|
||||||
|
login_bonus.c.isFinished == is_finished,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = self.execute(sql)
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
return result.fetchall()
|
||||||
|
|
||||||
|
def get_login_bonus(
|
||||||
|
self, user_id: int, version: int, preset_id: int
|
||||||
|
) -> Optional[Row]:
|
||||||
|
sql = login_bonus.select(
|
||||||
|
and_(
|
||||||
|
login_bonus.c.version == version,
|
||||||
|
login_bonus.c.user == user_id,
|
||||||
|
login_bonus.c.presetId == preset_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = self.execute(sql)
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
return result.fetchone()
|
||||||
|
|
||||||
def put_character(self, user_id: int, character_data: Dict) -> Optional[int]:
|
def put_character(self, user_id: int, character_data: Dict) -> Optional[int]:
|
||||||
character_data["user"] = user_id
|
character_data["user"] = user_id
|
||||||
|
|
||||||
@ -335,7 +400,7 @@ class ChuniItemData(BaseData):
|
|||||||
sql = print_state.select(
|
sql = print_state.select(
|
||||||
and_(
|
and_(
|
||||||
print_state.c.user == aime_id,
|
print_state.c.user == aime_id,
|
||||||
print_state.c.hasCompleted == has_completed
|
print_state.c.hasCompleted == has_completed,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -351,7 +416,7 @@ class ChuniItemData(BaseData):
|
|||||||
and_(
|
and_(
|
||||||
print_state.c.user == aime_id,
|
print_state.c.user == aime_id,
|
||||||
print_state.c.gachaId == gacha_id,
|
print_state.c.gachaId == gacha_id,
|
||||||
print_state.c.hasCompleted == has_completed
|
print_state.c.hasCompleted == has_completed,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -380,9 +445,7 @@ class ChuniItemData(BaseData):
|
|||||||
user=aime_id, serialId=serial_id, **user_print_data
|
user=aime_id, serialId=serial_id, **user_print_data
|
||||||
)
|
)
|
||||||
|
|
||||||
conflict = sql.on_duplicate_key_update(
|
conflict = sql.on_duplicate_key_update(user=aime_id, **user_print_data)
|
||||||
user=aime_id, **user_print_data
|
|
||||||
)
|
|
||||||
result = self.execute(conflict)
|
result = self.execute(conflict)
|
||||||
|
|
||||||
if result is None:
|
if result is None:
|
||||||
@ -390,4 +453,4 @@ class ChuniItemData(BaseData):
|
|||||||
f"put_user_print_detail: Failed to insert! aime_id: {aime_id}"
|
f"put_user_print_detail: Failed to insert! aime_id: {aime_id}"
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
return result.lastrowid
|
return result.lastrowid
|
||||||
|
@ -558,8 +558,10 @@ class ChuniProfileData(BaseData):
|
|||||||
return result.lastrowid
|
return result.lastrowid
|
||||||
|
|
||||||
def get_profile_activity(self, aime_id: int, kind: int) -> Optional[List[Row]]:
|
def get_profile_activity(self, aime_id: int, kind: int) -> Optional[List[Row]]:
|
||||||
sql = select(activity).where(
|
sql = (
|
||||||
and_(activity.c.user == aime_id, activity.c.kind == kind)
|
select(activity)
|
||||||
|
.where(and_(activity.c.user == aime_id, activity.c.kind == kind))
|
||||||
|
.order_by(activity.c.sortNumber.desc()) # to get the last played track
|
||||||
)
|
)
|
||||||
|
|
||||||
result = self.execute(sql)
|
result = self.execute(sql)
|
||||||
|
@ -122,8 +122,150 @@ gacha_cards = Table(
|
|||||||
mysql_charset="utf8mb4",
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
login_bonus_preset = Table(
|
||||||
|
"chuni_static_login_bonus_preset",
|
||||||
|
metadata,
|
||||||
|
Column("id", Integer, primary_key=True, nullable=False),
|
||||||
|
Column("version", Integer, nullable=False),
|
||||||
|
Column("presetName", String(255), nullable=False),
|
||||||
|
Column("isEnabled", Boolean, server_default="1"),
|
||||||
|
UniqueConstraint("version", "id", name="chuni_static_login_bonus_preset_uk"),
|
||||||
|
mysql_charset="utf8mb4",
|
||||||
|
)
|
||||||
|
|
||||||
|
login_bonus = Table(
|
||||||
|
"chuni_static_login_bonus",
|
||||||
|
metadata,
|
||||||
|
Column("id", Integer, primary_key=True, nullable=False),
|
||||||
|
Column("version", Integer, nullable=False),
|
||||||
|
Column(
|
||||||
|
"presetId",
|
||||||
|
ForeignKey(
|
||||||
|
"chuni_static_login_bonus_preset.id",
|
||||||
|
ondelete="cascade",
|
||||||
|
onupdate="cascade",
|
||||||
|
),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
Column("loginBonusId", Integer, nullable=False),
|
||||||
|
Column("loginBonusName", String(255), nullable=False),
|
||||||
|
Column("presentId", Integer, nullable=False),
|
||||||
|
Column("presentName", String(255), nullable=False),
|
||||||
|
Column("itemNum", Integer, nullable=False),
|
||||||
|
Column("needLoginDayCount", Integer, nullable=False),
|
||||||
|
Column("loginBonusCategoryType", Integer, nullable=False),
|
||||||
|
UniqueConstraint(
|
||||||
|
"version", "presetId", "loginBonusId", name="chuni_static_login_bonus_uk"
|
||||||
|
),
|
||||||
|
mysql_charset="utf8mb4",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ChuniStaticData(BaseData):
|
class ChuniStaticData(BaseData):
|
||||||
|
def put_login_bonus(
|
||||||
|
self,
|
||||||
|
version: int,
|
||||||
|
preset_id: int,
|
||||||
|
login_bonus_id: int,
|
||||||
|
login_bonus_name: str,
|
||||||
|
present_id: int,
|
||||||
|
present_ame: str,
|
||||||
|
item_num: int,
|
||||||
|
need_login_day_count: int,
|
||||||
|
login_bonus_category_type: int,
|
||||||
|
) -> Optional[int]:
|
||||||
|
sql = insert(login_bonus).values(
|
||||||
|
version=version,
|
||||||
|
presetId=preset_id,
|
||||||
|
loginBonusId=login_bonus_id,
|
||||||
|
loginBonusName=login_bonus_name,
|
||||||
|
presentId=present_id,
|
||||||
|
presentName=present_ame,
|
||||||
|
itemNum=item_num,
|
||||||
|
needLoginDayCount=need_login_day_count,
|
||||||
|
loginBonusCategoryType=login_bonus_category_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
conflict = sql.on_duplicate_key_update(
|
||||||
|
loginBonusName=login_bonus_name,
|
||||||
|
presentName=present_ame,
|
||||||
|
itemNum=item_num,
|
||||||
|
needLoginDayCount=need_login_day_count,
|
||||||
|
loginBonusCategoryType=login_bonus_category_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = self.execute(conflict)
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
return result.lastrowid
|
||||||
|
|
||||||
|
def get_login_bonus(
|
||||||
|
self,
|
||||||
|
version: int,
|
||||||
|
preset_id: int,
|
||||||
|
) -> Optional[List[Row]]:
|
||||||
|
sql = login_bonus.select(
|
||||||
|
and_(
|
||||||
|
login_bonus.c.version == version,
|
||||||
|
login_bonus.c.presetId == preset_id,
|
||||||
|
)
|
||||||
|
).order_by(login_bonus.c.needLoginDayCount.desc())
|
||||||
|
|
||||||
|
result = self.execute(sql)
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
return result.fetchall()
|
||||||
|
|
||||||
|
def get_login_bonus_by_required_days(
|
||||||
|
self, version: int, preset_id: int, need_login_day_count: int
|
||||||
|
) -> Optional[Row]:
|
||||||
|
sql = login_bonus.select(
|
||||||
|
and_(
|
||||||
|
login_bonus.c.version == version,
|
||||||
|
login_bonus.c.presetId == preset_id,
|
||||||
|
login_bonus.c.needLoginDayCount == need_login_day_count,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = self.execute(sql)
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
return result.fetchone()
|
||||||
|
|
||||||
|
def put_login_bonus_preset(
|
||||||
|
self, version: int, preset_id: int, preset_name: str, is_enabled: bool
|
||||||
|
) -> Optional[int]:
|
||||||
|
sql = insert(login_bonus_preset).values(
|
||||||
|
id=preset_id,
|
||||||
|
version=version,
|
||||||
|
presetName=preset_name,
|
||||||
|
isEnabled=is_enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
conflict = sql.on_duplicate_key_update(
|
||||||
|
presetName=preset_name, isEnabled=is_enabled
|
||||||
|
)
|
||||||
|
|
||||||
|
result = self.execute(conflict)
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
return result.lastrowid
|
||||||
|
|
||||||
|
def get_login_bonus_presets(
|
||||||
|
self, version: int, is_enabled: bool = True
|
||||||
|
) -> Optional[List[Row]]:
|
||||||
|
sql = login_bonus_preset.select(
|
||||||
|
and_(
|
||||||
|
login_bonus_preset.c.version == version,
|
||||||
|
login_bonus_preset.c.isEnabled == is_enabled,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = self.execute(sql)
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
return result.fetchall()
|
||||||
|
|
||||||
def put_event(
|
def put_event(
|
||||||
self, version: int, event_id: int, type: int, name: str
|
self, version: int, event_id: int, type: int, name: str
|
||||||
) -> Optional[int]:
|
) -> Optional[int]:
|
||||||
@ -390,20 +532,17 @@ class ChuniStaticData(BaseData):
|
|||||||
return None
|
return None
|
||||||
return result.fetchall()
|
return result.fetchall()
|
||||||
|
|
||||||
def get_gacha_card_by_character(self, gacha_id: int, chara_id: int) -> Optional[Dict]:
|
def get_gacha_card_by_character(
|
||||||
|
self, gacha_id: int, chara_id: int
|
||||||
|
) -> Optional[Dict]:
|
||||||
sql_sub = (
|
sql_sub = (
|
||||||
select(cards.c.cardId)
|
select(cards.c.cardId).filter(cards.c.charaId == chara_id).scalar_subquery()
|
||||||
.filter(
|
|
||||||
cards.c.charaId == chara_id
|
|
||||||
)
|
|
||||||
.scalar_subquery()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Perform the main query, also rename the resulting column to ranking
|
# Perform the main query, also rename the resulting column to ranking
|
||||||
sql = gacha_cards.select(and_(
|
sql = gacha_cards.select(
|
||||||
gacha_cards.c.gachaId == gacha_id,
|
and_(gacha_cards.c.gachaId == gacha_id, gacha_cards.c.cardId == sql_sub)
|
||||||
gacha_cards.c.cardId == sql_sub
|
)
|
||||||
))
|
|
||||||
|
|
||||||
result = self.execute(sql)
|
result = self.execute(sql)
|
||||||
if result is None:
|
if result is None:
|
||||||
|
@ -12,6 +12,7 @@ from twisted.web.http import Request
|
|||||||
from logging.handlers import TimedRotatingFileHandler
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
|
|
||||||
from core.config import CoreConfig
|
from core.config import CoreConfig
|
||||||
|
from core.utils import Utils
|
||||||
from titles.cm.config import CardMakerConfig
|
from titles.cm.config import CardMakerConfig
|
||||||
from titles.cm.const import CardMakerConstants
|
from titles.cm.const import CardMakerConstants
|
||||||
from titles.cm.base import CardMakerBase
|
from titles.cm.base import CardMakerBase
|
||||||
@ -82,6 +83,7 @@ class CardMakerServlet:
|
|||||||
url_split = url_path.split("/")
|
url_split = url_path.split("/")
|
||||||
internal_ver = 0
|
internal_ver = 0
|
||||||
endpoint = url_split[len(url_split) - 1]
|
endpoint = url_split[len(url_split) - 1]
|
||||||
|
client_ip = Utils.get_ip_addr(request)
|
||||||
|
|
||||||
print(f"version: {version}")
|
print(f"version: {version}")
|
||||||
|
|
||||||
@ -107,7 +109,8 @@ class CardMakerServlet:
|
|||||||
|
|
||||||
req_data = json.loads(unzip)
|
req_data = json.loads(unzip)
|
||||||
|
|
||||||
self.logger.info(f"v{version} {endpoint} request - {req_data}")
|
self.logger.info(f"v{version} {endpoint} request from {client_ip}")
|
||||||
|
self.logger.debug(req_data)
|
||||||
|
|
||||||
func_to_find = "handle_" + inflection.underscore(endpoint) + "_request"
|
func_to_find = "handle_" + inflection.underscore(endpoint) + "_request"
|
||||||
|
|
||||||
|
@ -80,7 +80,7 @@ class CardMakerReader(BaseReader):
|
|||||||
for dir in data_dirs:
|
for dir in data_dirs:
|
||||||
self.read_chuni_card(f"{dir}/CHU/card")
|
self.read_chuni_card(f"{dir}/CHU/card")
|
||||||
self.read_chuni_gacha(f"{dir}/CHU/gacha")
|
self.read_chuni_gacha(f"{dir}/CHU/gacha")
|
||||||
|
self.read_mai2_card(f"{dir}/MAI/card")
|
||||||
self.read_ongeki_gacha(f"{dir}/MU3/gacha")
|
self.read_ongeki_gacha(f"{dir}/MU3/gacha")
|
||||||
|
|
||||||
def read_chuni_card(self, base_dir: str) -> None:
|
def read_chuni_card(self, base_dir: str) -> None:
|
||||||
@ -90,7 +90,7 @@ class CardMakerReader(BaseReader):
|
|||||||
"v2_00": ChuniConstants.VER_CHUNITHM_NEW,
|
"v2_00": ChuniConstants.VER_CHUNITHM_NEW,
|
||||||
"v2_05": ChuniConstants.VER_CHUNITHM_NEW_PLUS,
|
"v2_05": ChuniConstants.VER_CHUNITHM_NEW_PLUS,
|
||||||
# Chunithm SUN, ignore for now
|
# Chunithm SUN, ignore for now
|
||||||
"v2_10": ChuniConstants.VER_CHUNITHM_NEW_PLUS + 1
|
"v2_10": ChuniConstants.VER_CHUNITHM_NEW_PLUS + 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
for root, dirs, files in os.walk(base_dir):
|
for root, dirs, files in os.walk(base_dir):
|
||||||
@ -206,6 +206,7 @@ class CardMakerReader(BaseReader):
|
|||||||
"1.15": Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS,
|
"1.15": Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS,
|
||||||
"1.20": Mai2Constants.VER_MAIMAI_DX_UNIVERSE,
|
"1.20": Mai2Constants.VER_MAIMAI_DX_UNIVERSE,
|
||||||
"1.25": Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS,
|
"1.25": Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS,
|
||||||
|
"1.30": Mai2Constants.VER_MAIMAI_DX_FESTIVAL,
|
||||||
}
|
}
|
||||||
|
|
||||||
for root, dirs, files in os.walk(base_dir):
|
for root, dirs, files in os.walk(base_dir):
|
||||||
|
@ -101,9 +101,7 @@ class CxbServlet(resource.Resource):
|
|||||||
f"Ready on ports {self.game_cfg.server.port} & {self.game_cfg.server.port_secure}"
|
f"Ready on ports {self.game_cfg.server.port} & {self.game_cfg.server.port_secure}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.logger.info(
|
self.logger.info(f"Ready on port {self.game_cfg.server.port}")
|
||||||
f"Ready on port {self.game_cfg.server.port}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def render_POST(self, request: Request):
|
def render_POST(self, request: Request):
|
||||||
version = 0
|
version = 0
|
||||||
|
@ -7,4 +7,4 @@ index = DivaServlet
|
|||||||
database = DivaData
|
database = DivaData
|
||||||
reader = DivaReader
|
reader = DivaReader
|
||||||
game_codes = [DivaConstants.GAME_CODE]
|
game_codes = [DivaConstants.GAME_CODE]
|
||||||
current_schema_version = 1
|
current_schema_version = 4
|
||||||
|
8
titles/idz/__init__.py
Normal file
8
titles/idz/__init__.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from titles.idz.index import IDZServlet
|
||||||
|
from titles.idz.const import IDZConstants
|
||||||
|
from titles.idz.database import IDZData
|
||||||
|
|
||||||
|
index = IDZServlet
|
||||||
|
database = IDZData
|
||||||
|
game_codes = [IDZConstants.GAME_CODE]
|
||||||
|
current_schema_version = 1
|
73
titles/idz/config.py
Normal file
73
titles/idz/config.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
from typing import List, Dict
|
||||||
|
|
||||||
|
from core.config import CoreConfig
|
||||||
|
|
||||||
|
|
||||||
|
class IDZServerConfig:
|
||||||
|
def __init__(self, parent_config: "IDZConfig") -> None:
|
||||||
|
self.__config = parent_config
|
||||||
|
|
||||||
|
@property
|
||||||
|
def enable(self) -> bool:
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "idz", "server", "enable", default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def loglevel(self) -> int:
|
||||||
|
return CoreConfig.str_to_loglevel(
|
||||||
|
CoreConfig.get_config_field(
|
||||||
|
self.__config, "idz", "server", "loglevel", default="info"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hostname(self) -> str:
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "idz", "server", "hostname", default=""
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def news(self) -> str:
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "idz", "server", "news", default=""
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def aes_key(self) -> str:
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "idz", "server", "aes_key", default=""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class IDZPortsConfig:
|
||||||
|
def __init__(self, parent_config: "IDZConfig") -> None:
|
||||||
|
self.__config = parent_config
|
||||||
|
|
||||||
|
@property
|
||||||
|
def userdb(self) -> int:
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "idz", "ports", "userdb", default=10000
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def match(self) -> int:
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "idz", "ports", "match", default=10010
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def echo(self) -> int:
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "idz", "ports", "echo", default=10020
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class IDZConfig(dict):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.server = IDZServerConfig(self)
|
||||||
|
self.ports = IDZPortsConfig(self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rsa_keys(self) -> List[Dict]:
|
||||||
|
return CoreConfig.get_config_field(self, "idz", "rsa_keys", default=[])
|
53
titles/idz/const.py
Normal file
53
titles/idz/const.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class IDZConstants:
|
||||||
|
GAME_CODE = "SDDF"
|
||||||
|
|
||||||
|
CONFIG_NAME = "idz.yaml"
|
||||||
|
|
||||||
|
VER_IDZ_110 = 0
|
||||||
|
VER_IDZ_130 = 1
|
||||||
|
VER_IDZ_210 = 2
|
||||||
|
VER_IDZ_230 = 3
|
||||||
|
NUM_VERS = 4
|
||||||
|
|
||||||
|
VERSION_NAMES = (
|
||||||
|
"Initial D Arcade Stage Zero v1.10",
|
||||||
|
"Initial D Arcade Stage Zero v1.30",
|
||||||
|
"Initial D Arcade Stage Zero v2.10",
|
||||||
|
"Initial D Arcade Stage Zero v2.30",
|
||||||
|
)
|
||||||
|
|
||||||
|
class PROFILE_STATUS(Enum):
|
||||||
|
LOCKED = 0
|
||||||
|
UNLOCKED = 1
|
||||||
|
OLD = 2
|
||||||
|
|
||||||
|
HASH_LUT = [
|
||||||
|
# No clue
|
||||||
|
0x9C82E674,
|
||||||
|
0x5A4738D9,
|
||||||
|
0x8B8D7AE0,
|
||||||
|
0x29EC9D81,
|
||||||
|
# These three are from AES TE0
|
||||||
|
0x1209091B,
|
||||||
|
0x1D83839E,
|
||||||
|
0x582C2C74,
|
||||||
|
0x341A1A2E,
|
||||||
|
0x361B1B2D,
|
||||||
|
0xDC6E6EB2,
|
||||||
|
0xB45A5AEE,
|
||||||
|
0x5BA0A0FB,
|
||||||
|
0xA45252F6,
|
||||||
|
0x763B3B4D,
|
||||||
|
0xB7D6D661,
|
||||||
|
0x7DB3B3CE,
|
||||||
|
]
|
||||||
|
HASH_NUM = 0
|
||||||
|
HASH_MUL = [5, 7, 11, 12][HASH_NUM]
|
||||||
|
HASH_XOR = [0xB3, 0x8C, 0x14, 0x50][HASH_NUM]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def game_ver_to_string(cls, ver: int):
|
||||||
|
return cls.VERSION_NAMES[ver]
|
7
titles/idz/database.py
Normal file
7
titles/idz/database.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from core.data import Data
|
||||||
|
from core.config import CoreConfig
|
||||||
|
|
||||||
|
|
||||||
|
class IDZData(Data):
|
||||||
|
def __init__(self, cfg: CoreConfig) -> None:
|
||||||
|
super().__init__(cfg)
|
19
titles/idz/echo.py
Normal file
19
titles/idz/echo.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from twisted.internet.protocol import DatagramProtocol
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from .config import IDZConfig
|
||||||
|
|
||||||
|
|
||||||
|
class IDZEcho(DatagramProtocol):
|
||||||
|
def __init__(self, cfg: CoreConfig, game_cfg: IDZConfig) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.core_config = cfg
|
||||||
|
self.game_config = game_cfg
|
||||||
|
self.logger = logging.getLogger("idz")
|
||||||
|
|
||||||
|
def datagramReceived(self, data, addr):
|
||||||
|
self.logger.debug(
|
||||||
|
f"Echo from from {addr[0]}:{addr[1]} -> {self.transport.getHost().port} - {data.hex()}"
|
||||||
|
)
|
||||||
|
self.transport.write(data, addr)
|
47
titles/idz/handlers/__init__.py
Normal file
47
titles/idz/handlers/__init__.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
from .base import IDZHandlerBase
|
||||||
|
|
||||||
|
from .load_server_info import IDZHandlerLoadServerInfo
|
||||||
|
|
||||||
|
from .load_ghost import IDZHandlerLoadGhost
|
||||||
|
|
||||||
|
from .load_config import IDZHandlerLoadConfigA, IDZHandlerLoadConfigB
|
||||||
|
|
||||||
|
from .load_top_ten import IDZHandlerLoadTopTen
|
||||||
|
|
||||||
|
from .update_story_clear_num import IDZHandlerUpdateStoryClearNum
|
||||||
|
|
||||||
|
from .save_expedition import IDZHandlerSaveExpedition
|
||||||
|
|
||||||
|
from .load_2on2 import IDZHandlerLoad2on2A, IDZHandlerLoad2on2B
|
||||||
|
|
||||||
|
from .load_team_ranking import IDZHandlerLoadTeamRankingA, IDZHandlerLoadTeamRankingB
|
||||||
|
|
||||||
|
from .discover_profile import IDZHandlerDiscoverProfile
|
||||||
|
|
||||||
|
from .lock_profile import IDZHandlerLockProfile
|
||||||
|
|
||||||
|
from .check_team_names import IDZHandlerCheckTeamName
|
||||||
|
|
||||||
|
from .unknown import IDZHandlerUnknown
|
||||||
|
|
||||||
|
from .create_profile import IDZHandlerCreateProfile
|
||||||
|
|
||||||
|
from .create_auto_team import IDZHandlerCreateAutoTeam
|
||||||
|
|
||||||
|
from .load_profile import IDZHandlerLoadProfile
|
||||||
|
|
||||||
|
from .save_profile import IDZHandlerSaveProfile
|
||||||
|
|
||||||
|
from .update_provisional_store_rank import IDZHandlerUpdateProvisionalStoreRank
|
||||||
|
|
||||||
|
from .load_reward_table import IDZHandlerLoadRewardTable
|
||||||
|
|
||||||
|
from .save_topic import IDZHandlerSaveTopic
|
||||||
|
|
||||||
|
from .save_time_attack import IDZHandlerSaveTimeAttack
|
||||||
|
|
||||||
|
from .unlock_profile import IDZHandlerUnlockProfile
|
||||||
|
|
||||||
|
from .update_team_points import IDZHandleUpdateTeamPoints
|
||||||
|
|
||||||
|
from .update_ui_report import IDZHandleUpdateUIReport
|
26
titles/idz/handlers/base.py
Normal file
26
titles/idz/handlers/base.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import logging
|
||||||
|
import struct
|
||||||
|
from core.data import Data
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from ..config import IDZConfig
|
||||||
|
from ..const import IDZConstants
|
||||||
|
|
||||||
|
|
||||||
|
class IDZHandlerBase:
|
||||||
|
name = "generic"
|
||||||
|
cmd_codes = [0x0000] * IDZConstants.NUM_VERS
|
||||||
|
rsp_codes = [0x0001] * IDZConstants.NUM_VERS
|
||||||
|
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None:
|
||||||
|
self.core_config = core_cfg
|
||||||
|
self.game_cfg = game_cfg
|
||||||
|
self.data = Data(core_cfg)
|
||||||
|
self.logger = logging.getLogger("idz")
|
||||||
|
self.game = IDZConstants.GAME_CODE
|
||||||
|
self.version = version
|
||||||
|
self.size = 0x30
|
||||||
|
|
||||||
|
def handle(self, data: bytes) -> bytearray:
|
||||||
|
ret = bytearray([0] * self.size)
|
||||||
|
struct.pack_into("<H", ret, 0x0, self.rsp_codes[self.version])
|
||||||
|
return ret
|
20
titles/idz/handlers/check_team_names.py
Normal file
20
titles/idz/handlers/check_team_names.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import struct
|
||||||
|
|
||||||
|
from .base import IDZHandlerBase
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from ..config import IDZConfig
|
||||||
|
|
||||||
|
|
||||||
|
class IDZHandlerCheckTeamName(IDZHandlerBase):
|
||||||
|
cmd_codes = [0x00A2, 0x00A2, 0x0097, 0x0097]
|
||||||
|
rsp_codes = [0x00A3, 0x00A3, 0x0098, 0x0098]
|
||||||
|
name = "check_team_name"
|
||||||
|
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None:
|
||||||
|
super().__init__(core_cfg, game_cfg, version)
|
||||||
|
self.size = 0x0010
|
||||||
|
|
||||||
|
def handle(self, data: bytes) -> bytearray:
|
||||||
|
ret = super().handle(data)
|
||||||
|
struct.pack_into("<I", ret, 0x4, 0x1)
|
||||||
|
return data
|
63
titles/idz/handlers/create_auto_team.py
Normal file
63
titles/idz/handlers/create_auto_team.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
from operator import indexOf
|
||||||
|
import struct
|
||||||
|
import json
|
||||||
|
from random import choice, randrange
|
||||||
|
|
||||||
|
from .base import IDZHandlerBase
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from ..config import IDZConfig
|
||||||
|
|
||||||
|
AUTO_TEAM_NAMES = ["スピードスターズ", "レッドサンズ", "ナイトキッズ"]
|
||||||
|
FULL_WIDTH_NUMS = [
|
||||||
|
"\uff10",
|
||||||
|
"\uff11",
|
||||||
|
"\uff12",
|
||||||
|
"\uff13",
|
||||||
|
"\uff14",
|
||||||
|
"\uff15",
|
||||||
|
"\uff16",
|
||||||
|
"\uff17",
|
||||||
|
"\uff18",
|
||||||
|
"\uff19",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class IDZHandlerCreateAutoTeam(IDZHandlerBase):
|
||||||
|
cmd_codes = [0x007B, 0x007B, 0x0077, 0x0077]
|
||||||
|
rsp_codes = [0x007C, 0x007C, 0x0078, 0x0078]
|
||||||
|
name = "create_auto_team"
|
||||||
|
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None:
|
||||||
|
super().__init__(core_cfg, game_cfg, version)
|
||||||
|
self.size = 0x0CA0
|
||||||
|
|
||||||
|
def handle(self, data: bytes) -> bytearray:
|
||||||
|
ret = super().handle(data)
|
||||||
|
aime_id = struct.unpack_from("<I", data, 0x04)[0]
|
||||||
|
name = choice(AUTO_TEAM_NAMES)
|
||||||
|
bg = indexOf(AUTO_TEAM_NAMES, name)
|
||||||
|
number = (
|
||||||
|
choice(FULL_WIDTH_NUMS) + choice(FULL_WIDTH_NUMS) + choice(FULL_WIDTH_NUMS)
|
||||||
|
)
|
||||||
|
|
||||||
|
tdata = {
|
||||||
|
"id": aime_id,
|
||||||
|
"bg": bg,
|
||||||
|
"fx": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
tdata = {
|
||||||
|
"id": aime_id,
|
||||||
|
"name": (name + number),
|
||||||
|
"bg": bg,
|
||||||
|
"fx": 0,
|
||||||
|
}
|
||||||
|
tname = tdata["name"].encode("shift-jis")
|
||||||
|
|
||||||
|
struct.pack_into("<I", ret, 0x0C, tdata["id"])
|
||||||
|
struct.pack_into(f"{len(tname)}s", ret, 0x24, tname)
|
||||||
|
struct.pack_into("<I", ret, 0x80, tdata["id"])
|
||||||
|
struct.pack_into(f"<I", ret, 0xD8, tdata["bg"])
|
||||||
|
struct.pack_into(f"<I", ret, 0xDC, tdata["fx"])
|
||||||
|
|
||||||
|
return ret
|
73
titles/idz/handlers/create_profile.py
Normal file
73
titles/idz/handlers/create_profile.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import json
|
||||||
|
import struct
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from .base import IDZHandlerBase
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from ..config import IDZConfig
|
||||||
|
|
||||||
|
|
||||||
|
class IDZHandlerCreateProfile(IDZHandlerBase):
|
||||||
|
cmd_codes = [0x0066, 0x0066, 0x0064, 0x0064]
|
||||||
|
rsp_codes = [0x0067, 0x0065, 0x0065, 0x0065]
|
||||||
|
name = "create_profile"
|
||||||
|
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None:
|
||||||
|
super().__init__(core_cfg, game_cfg, version)
|
||||||
|
self.size = 0x0020
|
||||||
|
|
||||||
|
def handle(self, data: bytes) -> bytearray:
|
||||||
|
ret = super().handle(data)
|
||||||
|
|
||||||
|
aime_id = struct.unpack_from("<L", data, 0x04)[0]
|
||||||
|
name = data[0x1E:0x0034].decode("shift-jis").replace("\x00", "")
|
||||||
|
car = data[0x40:0xA0].hex()
|
||||||
|
chara = data[0xA8:0xBC].hex()
|
||||||
|
|
||||||
|
self.logger.info(f"Create profile for {name} (aime id {aime_id})")
|
||||||
|
|
||||||
|
auto_team = None
|
||||||
|
if not auto_team:
|
||||||
|
team = {"bg": 0, "id": 0, "shop": ""}
|
||||||
|
else:
|
||||||
|
tdata = json.loads(auto_team["data"])
|
||||||
|
|
||||||
|
team = {"bg": tdata["bg"], "id": tdata["fx"], "shop": ""}
|
||||||
|
|
||||||
|
profile_data = {
|
||||||
|
"profile": {
|
||||||
|
"xp": 0,
|
||||||
|
"lv": 1,
|
||||||
|
"fame": 0,
|
||||||
|
"dpoint": 0,
|
||||||
|
"milage": 0,
|
||||||
|
"playstamps": 0,
|
||||||
|
"last_login": int(datetime.now().timestamp()),
|
||||||
|
"car_str": car, # These should probably be chaged to dicts
|
||||||
|
"chara_str": chara, # But this works for now...
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"music": 0,
|
||||||
|
"pack": 13640,
|
||||||
|
"aura": 0,
|
||||||
|
"paper_cup": 0,
|
||||||
|
"gauges": 5,
|
||||||
|
"driving_style": 0,
|
||||||
|
},
|
||||||
|
"missions": {"team": [], "solo": []},
|
||||||
|
"story": {"x": 0, "y": 0, "rows": {}},
|
||||||
|
"unlocks": {
|
||||||
|
"auras": 1,
|
||||||
|
"cup": 0,
|
||||||
|
"gauges": 32,
|
||||||
|
"music": 0,
|
||||||
|
"last_mileage_reward": 0,
|
||||||
|
},
|
||||||
|
"team": team,
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.version > 2:
|
||||||
|
struct.pack_into("<L", ret, 0x04, aime_id)
|
||||||
|
else:
|
||||||
|
struct.pack_into("<L", ret, 0x08, aime_id)
|
||||||
|
return ret
|
0
titles/idz/handlers/create_team.py
Normal file
0
titles/idz/handlers/create_team.py
Normal file
24
titles/idz/handlers/discover_profile.py
Normal file
24
titles/idz/handlers/discover_profile.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import struct
|
||||||
|
from typing import Tuple, List, Dict
|
||||||
|
|
||||||
|
from .base import IDZHandlerBase
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from ..config import IDZConfig
|
||||||
|
|
||||||
|
|
||||||
|
class IDZHandlerDiscoverProfile(IDZHandlerBase):
|
||||||
|
cmd_codes = [0x006B, 0x0067]
|
||||||
|
rsp_codes = [0x006C, 0x0068, 0x0068, 0x0068]
|
||||||
|
name = "discover_profile"
|
||||||
|
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None:
|
||||||
|
super().__init__(core_cfg, game_cfg, version)
|
||||||
|
self.size = 0x0010
|
||||||
|
|
||||||
|
def handle(self, data: bytes) -> bytearray:
|
||||||
|
ret = super().handle(data)
|
||||||
|
user_id = struct.unpack_from("<I", data, 0x04)[0]
|
||||||
|
profile = None
|
||||||
|
|
||||||
|
struct.pack_into("<H", ret, 0x04, int(profile is not None))
|
||||||
|
return ret
|
38
titles/idz/handlers/load_2on2.py
Normal file
38
titles/idz/handlers/load_2on2.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import struct
|
||||||
|
|
||||||
|
from .base import IDZHandlerBase
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from ..config import IDZConfig
|
||||||
|
from ..const import IDZConstants
|
||||||
|
|
||||||
|
|
||||||
|
class IDZHandlerLoad2on2A(IDZHandlerBase):
|
||||||
|
cmd_codes = [0x00B0, 0x00B0, 0x00A3, 0x00A3]
|
||||||
|
rsp_codes = [0x00B1, 0x00B1, 0x00A4, 0x00A4]
|
||||||
|
name = "load_2on2A"
|
||||||
|
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None:
|
||||||
|
super().__init__(core_cfg, game_cfg, version)
|
||||||
|
self.size = 0x04C0
|
||||||
|
|
||||||
|
if version >= IDZConstants.VER_IDZ_210:
|
||||||
|
self.size = 0x1290
|
||||||
|
|
||||||
|
def handle(self, data: bytes) -> bytearray:
|
||||||
|
return super().handle(data)
|
||||||
|
|
||||||
|
|
||||||
|
class IDZHandlerLoad2on2B(IDZHandlerBase):
|
||||||
|
cmd_codes = [0x0132] * 4
|
||||||
|
rsp_codes = [0x0133] * 4
|
||||||
|
name = "load_2on2B"
|
||||||
|
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None:
|
||||||
|
super().__init__(core_cfg, game_cfg, version)
|
||||||
|
self.size = 0x04C0
|
||||||
|
|
||||||
|
if version >= IDZConstants.VER_IDZ_210:
|
||||||
|
self.size = 0x0540
|
||||||
|
|
||||||
|
def handle(self, data: bytes) -> bytearray:
|
||||||
|
return super().handle(data)
|
43
titles/idz/handlers/load_config.py
Normal file
43
titles/idz/handlers/load_config.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import struct
|
||||||
|
|
||||||
|
from .base import IDZHandlerBase
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from ..config import IDZConfig
|
||||||
|
from ..const import IDZConstants
|
||||||
|
|
||||||
|
|
||||||
|
class IDZHandlerLoadConfigA(IDZHandlerBase):
|
||||||
|
cmd_codes = [0x0004] * IDZConstants.NUM_VERS
|
||||||
|
rsp_codes = [0x0005] * IDZConstants.NUM_VERS
|
||||||
|
name = "load_config_a"
|
||||||
|
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None:
|
||||||
|
super().__init__(core_cfg, game_cfg, version)
|
||||||
|
self.size = 0x01A0
|
||||||
|
|
||||||
|
if self.version > 1:
|
||||||
|
self.size = 0x05E0
|
||||||
|
|
||||||
|
def handle(self, data: bytes) -> bytearray:
|
||||||
|
ret = super().handle(data)
|
||||||
|
struct.pack_into("<H", ret, 0x02, 1)
|
||||||
|
struct.pack_into("<I", ret, 0x16, 230)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
class IDZHandlerLoadConfigB(IDZHandlerBase):
|
||||||
|
cmd_codes = [0x00AB, 0x00AB, 0x00A0, 0x00A0]
|
||||||
|
rsp_codes = [0x00AC, 0x00AC, 0x00A1, 0x00A1]
|
||||||
|
name = "load_config_b"
|
||||||
|
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None:
|
||||||
|
super().__init__(core_cfg, game_cfg, version)
|
||||||
|
self.size = 0x0230
|
||||||
|
|
||||||
|
if self.version > 1:
|
||||||
|
self.size = 0x0240
|
||||||
|
|
||||||
|
def handle(self, data: bytes) -> bytearray:
|
||||||
|
ret = super().handle(data)
|
||||||
|
struct.pack_into("<H", ret, 0x02, 1)
|
||||||
|
return ret
|
40
titles/idz/handlers/load_ghost.py
Normal file
40
titles/idz/handlers/load_ghost.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import struct
|
||||||
|
|
||||||
|
from .base import IDZHandlerBase
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from ..config import IDZConfig
|
||||||
|
|
||||||
|
|
||||||
|
class IDZHandlerLoadGhost(IDZHandlerBase):
|
||||||
|
cmd_codes = [0x00A0, 0x00A0, 0x0095, 0x0095]
|
||||||
|
rsp_codes = [0x00A1, 0x00A1, 0x0096, 0x0096]
|
||||||
|
name = "load_ghost"
|
||||||
|
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None:
|
||||||
|
super().__init__(core_cfg, game_cfg, version)
|
||||||
|
self.size = 0x0070
|
||||||
|
|
||||||
|
def handle(self, data: bytes) -> bytearray:
|
||||||
|
ret = super().handle(data)
|
||||||
|
struct.pack_into("<I", ret, 0x02, 0x5)
|
||||||
|
|
||||||
|
struct.pack_into("<L", ret, 0x04, 0x0)
|
||||||
|
struct.pack_into("<l", ret, 0x08, -1)
|
||||||
|
struct.pack_into("<L", ret, 0x0C, 0x1D4C0)
|
||||||
|
struct.pack_into("<L", ret, 0x10, 0x1D4C0)
|
||||||
|
struct.pack_into("<L", ret, 0x14, 0x1D4C0)
|
||||||
|
|
||||||
|
struct.pack_into("<L", ret, 0x38, 0x0)
|
||||||
|
struct.pack_into("<l", ret, 0x3C, -1)
|
||||||
|
struct.pack_into("<L", ret, 0x40, 0x1D4C0)
|
||||||
|
struct.pack_into("<L", ret, 0x44, 0x1D4C0)
|
||||||
|
struct.pack_into("<L", ret, 0x48, 0x1D4C0)
|
||||||
|
|
||||||
|
struct.pack_into("<L", ret, 0x4C, 0x1D4C0)
|
||||||
|
struct.pack_into("<i", ret, 0x50, -1)
|
||||||
|
struct.pack_into("<H", ret, 0x52, 0)
|
||||||
|
struct.pack_into("<H", ret, 0x53, 0)
|
||||||
|
struct.pack_into("<H", ret, 0x54, 0)
|
||||||
|
struct.pack_into("<H", ret, 0x55, 0)
|
||||||
|
struct.pack_into("<H", ret, 0x58, 0)
|
||||||
|
return ret
|
29
titles/idz/handlers/load_profile.py
Normal file
29
titles/idz/handlers/load_profile.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import struct
|
||||||
|
|
||||||
|
from .base import IDZHandlerBase
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from ..config import IDZConfig
|
||||||
|
from ..const import IDZConstants
|
||||||
|
|
||||||
|
|
||||||
|
class IDZHandlerLoadProfile(IDZHandlerBase):
|
||||||
|
cmd_codes = [0x0067, 0x012F, 0x012F, 0x0142]
|
||||||
|
rsp_codes = [0x0065, 0x012E, 0x012E, 0x0141]
|
||||||
|
name = "load_profile"
|
||||||
|
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None:
|
||||||
|
super().__init__(core_cfg, game_cfg, version)
|
||||||
|
|
||||||
|
if self.version == IDZConstants.VER_IDZ_110:
|
||||||
|
self.size = 0x0D30
|
||||||
|
elif self.version == IDZConstants.VER_IDZ_130:
|
||||||
|
self.size = 0x0EA0
|
||||||
|
elif self.version == IDZConstants.VER_IDZ_210:
|
||||||
|
self.size = 0x1360
|
||||||
|
elif self.version == IDZConstants.VER_IDZ_230:
|
||||||
|
self.size = 0x1640
|
||||||
|
|
||||||
|
def handle(self, data: bytes) -> bytearray:
|
||||||
|
ret = super().handle(data)
|
||||||
|
aime_id = struct.unpack_from("<L", data, 0x04)[0]
|
||||||
|
return ret
|
18
titles/idz/handlers/load_reward_table.py
Normal file
18
titles/idz/handlers/load_reward_table.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import struct
|
||||||
|
|
||||||
|
from .base import IDZHandlerBase
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from ..config import IDZConfig
|
||||||
|
|
||||||
|
|
||||||
|
class IDZHandlerLoadRewardTable(IDZHandlerBase):
|
||||||
|
cmd_codes = [0x0086, 0x0086, 0x007F, 0x007F]
|
||||||
|
rsp_codes = [0x0087, 0x0087, 0x0080, 0x0080]
|
||||||
|
name = "load_reward_table"
|
||||||
|
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None:
|
||||||
|
super().__init__(core_cfg, game_cfg, version)
|
||||||
|
self.size = 0x01C0
|
||||||
|
|
||||||
|
def handle(self, data: bytes) -> bytearray:
|
||||||
|
return super().handle(data)
|
96
titles/idz/handlers/load_server_info.py
Normal file
96
titles/idz/handlers/load_server_info.py
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import struct
|
||||||
|
|
||||||
|
from .base import IDZHandlerBase
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from ..config import IDZConfig
|
||||||
|
from ..const import IDZConstants
|
||||||
|
|
||||||
|
|
||||||
|
class IDZHandlerLoadServerInfo(IDZHandlerBase):
|
||||||
|
cmd_codes = [0x0006] * IDZConstants.NUM_VERS
|
||||||
|
rsp_codes = [0x0007] * IDZConstants.NUM_VERS
|
||||||
|
name = "load_server_info1"
|
||||||
|
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None:
|
||||||
|
super().__init__(core_cfg, game_cfg, version)
|
||||||
|
self.size = 0x04B0
|
||||||
|
|
||||||
|
def handle(self, data: bytes) -> bytearray:
|
||||||
|
ret = super().handle(data)
|
||||||
|
offset = 0
|
||||||
|
if self.version >= IDZConstants.VER_IDZ_210:
|
||||||
|
offset = 2
|
||||||
|
|
||||||
|
news_str = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDDF/230/news/news80**.txt"
|
||||||
|
err_str = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDDF/230/error"
|
||||||
|
|
||||||
|
len_hostname = len(self.core_config.title.hostname)
|
||||||
|
len_news = len(news_str)
|
||||||
|
len_error = len(err_str)
|
||||||
|
|
||||||
|
struct.pack_into("<I", ret, 0x2 + offset, 1) # Status
|
||||||
|
struct.pack_into(
|
||||||
|
f"{len_hostname}s",
|
||||||
|
ret,
|
||||||
|
0x4 + offset,
|
||||||
|
self.core_config.title.hostname.encode(),
|
||||||
|
)
|
||||||
|
struct.pack_into("<I", ret, 0x84 + offset, self.game_cfg.ports.userdb)
|
||||||
|
struct.pack_into("<I", ret, 0x86 + offset, self.game_cfg.ports.userdb + 1)
|
||||||
|
|
||||||
|
struct.pack_into(
|
||||||
|
f"{len_hostname}s",
|
||||||
|
ret,
|
||||||
|
0x88 + offset,
|
||||||
|
self.core_config.title.hostname.encode(),
|
||||||
|
)
|
||||||
|
struct.pack_into("<I", ret, 0x108 + offset, self.game_cfg.ports.match - 1)
|
||||||
|
struct.pack_into("<I", ret, 0x10A + offset, self.game_cfg.ports.match - 3)
|
||||||
|
struct.pack_into("<I", ret, 0x10C + offset, self.game_cfg.ports.match - 2)
|
||||||
|
|
||||||
|
struct.pack_into("<I", ret, 0x10E + offset, self.game_cfg.ports.match + 2)
|
||||||
|
struct.pack_into("<I", ret, 0x110 + offset, self.game_cfg.ports.match + 3)
|
||||||
|
struct.pack_into("<I", ret, 0x112 + offset, self.game_cfg.ports.match + 1)
|
||||||
|
|
||||||
|
struct.pack_into(
|
||||||
|
f"{len_hostname}s",
|
||||||
|
ret,
|
||||||
|
0x114 + offset,
|
||||||
|
self.core_config.title.hostname.encode(),
|
||||||
|
)
|
||||||
|
struct.pack_into("<I", ret, 0x194 + offset, self.game_cfg.ports.echo + 2)
|
||||||
|
|
||||||
|
struct.pack_into(
|
||||||
|
f"{len_hostname}s",
|
||||||
|
ret,
|
||||||
|
0x0199 + offset,
|
||||||
|
self.core_config.title.hostname.encode(),
|
||||||
|
)
|
||||||
|
struct.pack_into("<I", ret, 0x0219 + offset, self.game_cfg.ports.echo + 3)
|
||||||
|
|
||||||
|
struct.pack_into(
|
||||||
|
f"{len_hostname}s",
|
||||||
|
ret,
|
||||||
|
0x021C + offset,
|
||||||
|
self.core_config.title.hostname.encode(),
|
||||||
|
)
|
||||||
|
struct.pack_into(
|
||||||
|
f"{len_hostname}s",
|
||||||
|
ret,
|
||||||
|
0x029C + offset,
|
||||||
|
self.core_config.title.hostname.encode(),
|
||||||
|
)
|
||||||
|
struct.pack_into(
|
||||||
|
f"{len_hostname}s",
|
||||||
|
ret,
|
||||||
|
0x031C + offset,
|
||||||
|
self.core_config.title.hostname.encode(),
|
||||||
|
)
|
||||||
|
|
||||||
|
struct.pack_into("<I", ret, 0x39C + offset, self.game_cfg.ports.echo)
|
||||||
|
struct.pack_into("<I", ret, 0x39E + offset, self.game_cfg.ports.echo + 1)
|
||||||
|
|
||||||
|
struct.pack_into(f"{len_news}s", ret, 0x03A0 + offset, news_str.encode())
|
||||||
|
struct.pack_into(f"{len_error}s", ret, 0x0424 + offset, err_str.encode())
|
||||||
|
|
||||||
|
return ret
|
31
titles/idz/handlers/load_team_ranking.py
Normal file
31
titles/idz/handlers/load_team_ranking.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import struct
|
||||||
|
|
||||||
|
from .base import IDZHandlerBase
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from ..config import IDZConfig
|
||||||
|
|
||||||
|
|
||||||
|
class IDZHandlerLoadTeamRankingA(IDZHandlerBase):
|
||||||
|
cmd_codes = [0x00B9, 0x00B9, 0x00A7, 0x00A7]
|
||||||
|
rsp_codes = [0x00B1] * 4
|
||||||
|
name = "load_team_ranking_a"
|
||||||
|
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None:
|
||||||
|
super().__init__(core_cfg, game_cfg, version)
|
||||||
|
self.size = 0x0BA0
|
||||||
|
|
||||||
|
def handle(self, data: bytes) -> bytearray:
|
||||||
|
return super().handle(data)
|
||||||
|
|
||||||
|
|
||||||
|
class IDZHandlerLoadTeamRankingB(IDZHandlerBase):
|
||||||
|
cmd_codes = [0x00BB, 0x00BB, 0x00A9, 0x00A9]
|
||||||
|
rsp_codes = [0x00A8] * 4
|
||||||
|
name = "load_team_ranking_b"
|
||||||
|
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None:
|
||||||
|
super().__init__(core_cfg, game_cfg, version)
|
||||||
|
self.size = 0x0BA0
|
||||||
|
|
||||||
|
def handle(self, data: bytes) -> bytearray:
|
||||||
|
return super().handle(data)
|
35
titles/idz/handlers/load_top_ten.py
Normal file
35
titles/idz/handlers/load_top_ten.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import struct
|
||||||
|
from typing import Tuple, List, Dict
|
||||||
|
|
||||||
|
from .base import IDZHandlerBase
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from ..config import IDZConfig
|
||||||
|
|
||||||
|
|
||||||
|
class IDZHandlerLoadTopTen(IDZHandlerBase):
|
||||||
|
cmd_codes = [0x012C] * 4
|
||||||
|
rsp_codes = [0x00CE] * 4
|
||||||
|
name = "load_top_ten"
|
||||||
|
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None:
|
||||||
|
super().__init__(core_cfg, game_cfg, version)
|
||||||
|
self.size = 0x1720
|
||||||
|
|
||||||
|
def handle(self, data: bytes) -> bytearray:
|
||||||
|
ret = super().handle(data)
|
||||||
|
tracks_dates: List[Tuple[int, int]] = []
|
||||||
|
for i in range(32):
|
||||||
|
tracks_dates.append(
|
||||||
|
(
|
||||||
|
struct.unpack_from("<H", data, 0x04 + (2 * i))[0],
|
||||||
|
"little",
|
||||||
|
struct.unpack_from("<I", data, 0x44 + (4 * i))[0],
|
||||||
|
"little",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# TODO: Best scores
|
||||||
|
for i in range(3):
|
||||||
|
offset = 0x16C0 + 0x1C * i
|
||||||
|
struct.pack_into("<B", ret, offset + 0x02, 0xFF)
|
||||||
|
|
||||||
|
return ret
|
42
titles/idz/handlers/lock_profile.py
Normal file
42
titles/idz/handlers/lock_profile.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import struct
|
||||||
|
from typing import Dict
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from .base import IDZHandlerBase
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from ..config import IDZConfig
|
||||||
|
from ..const import IDZConstants
|
||||||
|
|
||||||
|
|
||||||
|
class IDZHandlerLockProfile(IDZHandlerBase):
|
||||||
|
cmd_codes = [0x0069, 0x0069, 0x0065, 0x0065]
|
||||||
|
rsp_codes = [0x006A, 0x006A, 0x0066, 0x0066]
|
||||||
|
name = "lock_profile"
|
||||||
|
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None:
|
||||||
|
super().__init__(core_cfg, game_cfg, version)
|
||||||
|
self.size = 0x0020
|
||||||
|
|
||||||
|
def handle(self, data: bytes) -> bytearray:
|
||||||
|
ret = super().handle(data)
|
||||||
|
profile_data = {
|
||||||
|
"status": IDZConstants.PROFILE_STATUS.UNLOCKED.value,
|
||||||
|
"expire_time": int(
|
||||||
|
(datetime.now() + timedelta(hours=1)).timestamp() / 1000
|
||||||
|
),
|
||||||
|
}
|
||||||
|
user_id = struct.unpack_from("<I", data, 0x04)[0]
|
||||||
|
profile = None
|
||||||
|
|
||||||
|
if profile is None and self.version > 0:
|
||||||
|
old_profile = None
|
||||||
|
if old_profile is not None:
|
||||||
|
profile_data["status"] = IDZConstants.PROFILE_STATUS.OLD.value
|
||||||
|
|
||||||
|
return self.handle_common(profile_data, ret)
|
||||||
|
|
||||||
|
def handle_common(cls, data: Dict, ret: bytearray) -> bytearray:
|
||||||
|
struct.pack_into("<B", ret, 0x18, data["status"])
|
||||||
|
struct.pack_into("<h", ret, 0x1A, -1)
|
||||||
|
struct.pack_into("<I", ret, 0x1C, data["expire_time"])
|
||||||
|
return ret
|
15
titles/idz/handlers/save_expedition.py
Normal file
15
titles/idz/handlers/save_expedition.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from .base import IDZHandlerBase
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from ..config import IDZConfig
|
||||||
|
from ..const import IDZConstants
|
||||||
|
|
||||||
|
|
||||||
|
class IDZHandlerSaveExpedition(IDZHandlerBase):
|
||||||
|
cmd_codes = [0x008C, 0x013F]
|
||||||
|
name = "save_expedition"
|
||||||
|
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None:
|
||||||
|
super().__init__(core_cfg, game_cfg, version)
|
||||||
|
|
||||||
|
def handle(self, data: bytes) -> bytearray:
|
||||||
|
return super().handle(data)
|
16
titles/idz/handlers/save_profile.py
Normal file
16
titles/idz/handlers/save_profile.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import struct
|
||||||
|
|
||||||
|
from .base import IDZHandlerBase
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from ..config import IDZConfig
|
||||||
|
|
||||||
|
|
||||||
|
class IDZHandlerSaveProfile(IDZHandlerBase):
|
||||||
|
cmd_codes = [0x0068, 0x0138, 0x0138, 0x0143]
|
||||||
|
name = "save_profile"
|
||||||
|
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None:
|
||||||
|
super().__init__(core_cfg, game_cfg, version)
|
||||||
|
|
||||||
|
def handle(self, data: bytes) -> bytearray:
|
||||||
|
return super().handle(data)
|
23
titles/idz/handlers/save_time_attack.py
Normal file
23
titles/idz/handlers/save_time_attack.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import struct
|
||||||
|
|
||||||
|
from .base import IDZHandlerBase
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from ..config import IDZConfig
|
||||||
|
from ..const import IDZConstants
|
||||||
|
|
||||||
|
|
||||||
|
class IDZHandlerSaveTimeAttack(IDZHandlerBase):
|
||||||
|
cmd_codes = [0x00CD, 0x0136, 0x0136, 0x0136]
|
||||||
|
rsp_codes = [0x00CE, 0x00CE, 0x00CD, 0x00CD]
|
||||||
|
name = "save_time_attack"
|
||||||
|
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None:
|
||||||
|
super().__init__(core_cfg, game_cfg, version)
|
||||||
|
self.size = 0x00B0
|
||||||
|
|
||||||
|
if self.version > IDZConstants.VER_IDZ_130:
|
||||||
|
self.size = 0x00F0
|
||||||
|
|
||||||
|
def handle(self, data: bytes) -> bytearray:
|
||||||
|
ret = super().handle(data)
|
||||||
|
return ret
|
18
titles/idz/handlers/save_topic.py
Normal file
18
titles/idz/handlers/save_topic.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import struct
|
||||||
|
|
||||||
|
from .base import IDZHandlerBase
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from ..config import IDZConfig
|
||||||
|
|
||||||
|
|
||||||
|
class IDZHandlerSaveTopic(IDZHandlerBase):
|
||||||
|
cmd_codes = [0x009A, 0x009A, 0x0091, 0x0091]
|
||||||
|
rsp_codes = [0x009B, 0x009B, 0x0092, 0x0092]
|
||||||
|
name = "save_topic"
|
||||||
|
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None:
|
||||||
|
super().__init__(core_cfg, game_cfg, version)
|
||||||
|
self.size = 0x05D0
|
||||||
|
|
||||||
|
def handle(self, data: bytes) -> bytearray:
|
||||||
|
return super().handle(data)
|
13
titles/idz/handlers/unknown.py
Normal file
13
titles/idz/handlers/unknown.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import struct
|
||||||
|
|
||||||
|
from .base import IDZHandlerBase
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from ..config import IDZConfig
|
||||||
|
|
||||||
|
|
||||||
|
class IDZHandlerUnknown(IDZHandlerBase):
|
||||||
|
cmd_codes = [0x00AD, 0x00AD, 0x00A2, 0x00A2]
|
||||||
|
name = "unknown"
|
||||||
|
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None:
|
||||||
|
super().__init__(core_cfg, game_cfg, version)
|
21
titles/idz/handlers/unlock_profile.py
Normal file
21
titles/idz/handlers/unlock_profile.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import struct
|
||||||
|
|
||||||
|
from .base import IDZHandlerBase
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from ..config import IDZConfig
|
||||||
|
from ..const import IDZConstants
|
||||||
|
|
||||||
|
|
||||||
|
class IDZHandlerUnlockProfile(IDZHandlerBase):
|
||||||
|
cmd_codes = [0x006F, 0x006F, 0x006B, 0x006B]
|
||||||
|
rsp_codes = [0x0070, 0x0070, 0x006C, 0x006C]
|
||||||
|
name = "unlock_profile"
|
||||||
|
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None:
|
||||||
|
super().__init__(core_cfg, game_cfg, version)
|
||||||
|
self.size = 0x0010
|
||||||
|
|
||||||
|
def handle(self, data: bytes) -> bytearray:
|
||||||
|
ret = super().handle(data)
|
||||||
|
struct.pack_into("<H", ret, 0x4, 1)
|
||||||
|
return ret
|
21
titles/idz/handlers/update_provisional_store_rank.py
Normal file
21
titles/idz/handlers/update_provisional_store_rank.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import struct
|
||||||
|
|
||||||
|
from .base import IDZHandlerBase
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from ..config import IDZConfig
|
||||||
|
|
||||||
|
|
||||||
|
class IDZHandlerUpdateProvisionalStoreRank(IDZHandlerBase):
|
||||||
|
cmd_codes = [0x0082, 0x0082, 0x007C, 0x007C]
|
||||||
|
rsp_codes = [0x0083, 0x0083, 0x007D, 0x007D]
|
||||||
|
name = "update_provisional_store_ranking"
|
||||||
|
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None:
|
||||||
|
super().__init__(core_cfg, game_cfg, version)
|
||||||
|
self.size = 0x02B0
|
||||||
|
|
||||||
|
def handle(self, data: bytes) -> bytearray:
|
||||||
|
return super().handle(data)
|
||||||
|
|
||||||
|
def handle_common(cls, aime_id: int, ret: bytearray) -> bytearray:
|
||||||
|
pass
|
27
titles/idz/handlers/update_story_clear_num.py
Normal file
27
titles/idz/handlers/update_story_clear_num.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import struct
|
||||||
|
|
||||||
|
from .base import IDZHandlerBase
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from ..config import IDZConfig
|
||||||
|
from ..const import IDZConstants
|
||||||
|
|
||||||
|
|
||||||
|
class IDZHandlerUpdateStoryClearNum(IDZHandlerBase):
|
||||||
|
cmd_codes = [0x007F, 0x097F, 0x013D, 0x0144]
|
||||||
|
rsp_codes = [0x0080, 0x013E, 0x013E, 0x0145]
|
||||||
|
name = "update_story_clear_num"
|
||||||
|
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None:
|
||||||
|
super().__init__(core_cfg, game_cfg, version)
|
||||||
|
|
||||||
|
if self.version == IDZConstants.VER_IDZ_110:
|
||||||
|
self.size = 0x0220
|
||||||
|
elif self.version == IDZConstants.VER_IDZ_130:
|
||||||
|
self.size = 0x04F0
|
||||||
|
elif self.version == IDZConstants.VER_IDZ_210:
|
||||||
|
self.size = 0x0510
|
||||||
|
elif self.version == IDZConstants.VER_IDZ_230:
|
||||||
|
self.size = 0x0800
|
||||||
|
|
||||||
|
def handle(self, data: bytes) -> bytearray:
|
||||||
|
return super().handle(data)
|
18
titles/idz/handlers/update_team_points.py
Normal file
18
titles/idz/handlers/update_team_points.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import struct
|
||||||
|
|
||||||
|
from .base import IDZHandlerBase
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from ..config import IDZConfig
|
||||||
|
from ..const import IDZConstants
|
||||||
|
|
||||||
|
|
||||||
|
class IDZHandleUpdateTeamPoints(IDZHandlerBase):
|
||||||
|
cmd_codes = [0x0081, 0x0081, 0x007B, 0x007B]
|
||||||
|
name = "unlock_profile"
|
||||||
|
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None:
|
||||||
|
super().__init__(core_cfg, game_cfg, version)
|
||||||
|
|
||||||
|
def handle(self, data: bytes) -> bytearray:
|
||||||
|
ret = super().handle(data)
|
||||||
|
return ret
|
18
titles/idz/handlers/update_ui_report.py
Normal file
18
titles/idz/handlers/update_ui_report.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import struct
|
||||||
|
|
||||||
|
from .base import IDZHandlerBase
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from ..config import IDZConfig
|
||||||
|
from ..const import IDZConstants
|
||||||
|
|
||||||
|
|
||||||
|
class IDZHandleUpdateUIReport(IDZHandlerBase):
|
||||||
|
cmd_codes = [0x0084, 0x0084, 0x007E, 0x007E]
|
||||||
|
name = "update_ui_report"
|
||||||
|
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None:
|
||||||
|
super().__init__(core_cfg, game_cfg, version)
|
||||||
|
|
||||||
|
def handle(self, data: bytes) -> bytearray:
|
||||||
|
ret = super().handle(data)
|
||||||
|
return ret
|
18
titles/idz/handlers/update_user_log.py
Normal file
18
titles/idz/handlers/update_user_log.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import struct
|
||||||
|
|
||||||
|
from .base import IDZHandlerBase
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from ..config import IDZConfig
|
||||||
|
from ..const import IDZConstants
|
||||||
|
|
||||||
|
|
||||||
|
class IDZHandleUpdateUserLog(IDZHandlerBase):
|
||||||
|
cmd_codes = [0x00BD, 0x00BD, 0x00AB, 0x00B3]
|
||||||
|
name = "update_user_log"
|
||||||
|
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig, version: int) -> None:
|
||||||
|
super().__init__(core_cfg, game_cfg, version)
|
||||||
|
|
||||||
|
def handle(self, data: bytes) -> bytearray:
|
||||||
|
ret = super().handle(data)
|
||||||
|
return ret
|
174
titles/idz/index.py
Normal file
174
titles/idz/index.py
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
from twisted.web.http import Request
|
||||||
|
import yaml
|
||||||
|
import logging
|
||||||
|
import coloredlogs
|
||||||
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
|
from os import path
|
||||||
|
from typing import Tuple, List
|
||||||
|
from twisted.internet import reactor, endpoints
|
||||||
|
from twisted.web import server, resource
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from .config import IDZConfig
|
||||||
|
from .const import IDZConstants
|
||||||
|
from .userdb import IDZUserDBFactory, IDZUserDBWeb, IDZKey
|
||||||
|
from .echo import IDZEcho
|
||||||
|
from .handlers import IDZHandlerLoadConfigB
|
||||||
|
|
||||||
|
|
||||||
|
class IDZServlet:
|
||||||
|
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
|
||||||
|
self.core_cfg = core_cfg
|
||||||
|
self.game_cfg = IDZConfig()
|
||||||
|
if path.exists(f"{cfg_dir}/{IDZConstants.CONFIG_NAME}"):
|
||||||
|
self.game_cfg.update(
|
||||||
|
yaml.safe_load(open(f"{cfg_dir}/{IDZConstants.CONFIG_NAME}"))
|
||||||
|
)
|
||||||
|
|
||||||
|
self.logger = logging.getLogger("idz")
|
||||||
|
if not hasattr(self.logger, "inited"):
|
||||||
|
log_fmt_str = "[%(asctime)s] IDZ | %(levelname)s | %(message)s"
|
||||||
|
log_fmt = logging.Formatter(log_fmt_str)
|
||||||
|
fileHandler = TimedRotatingFileHandler(
|
||||||
|
"{0}/{1}.log".format(self.core_cfg.server.log_dir, "idz"),
|
||||||
|
encoding="utf8",
|
||||||
|
when="d",
|
||||||
|
backupCount=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.rsa_keys: List[IDZKey] = []
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
self.logger.inited = True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def rsaHashKeyN(cls, data):
|
||||||
|
hash_ = 0
|
||||||
|
for i in data:
|
||||||
|
hash_ = (
|
||||||
|
hash_ * IDZConstants.HASH_MUL + (i ^ IDZConstants.HASH_XOR)
|
||||||
|
^ IDZConstants.HASH_LUT[i & 0xF]
|
||||||
|
)
|
||||||
|
hash_ &= 0xFFFFFFFF
|
||||||
|
return hash_
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_allnet_info(
|
||||||
|
cls, game_code: str, core_cfg: CoreConfig, cfg_dir: str
|
||||||
|
) -> Tuple[bool, str, str]:
|
||||||
|
game_cfg = IDZConfig()
|
||||||
|
if path.exists(f"{cfg_dir}/{IDZConstants.CONFIG_NAME}"):
|
||||||
|
game_cfg.update(
|
||||||
|
yaml.safe_load(open(f"{cfg_dir}/{IDZConstants.CONFIG_NAME}"))
|
||||||
|
)
|
||||||
|
|
||||||
|
if not game_cfg.server.enable:
|
||||||
|
return (False, "", "")
|
||||||
|
|
||||||
|
if len(game_cfg.rsa_keys) <= 0 or not game_cfg.server.aes_key:
|
||||||
|
logging.getLogger("idz").error("IDZ: No RSA/AES keys! IDZ cannot start")
|
||||||
|
return (False, "", "")
|
||||||
|
|
||||||
|
hostname = (
|
||||||
|
core_cfg.title.hostname
|
||||||
|
if not game_cfg.server.hostname
|
||||||
|
else game_cfg.server.hostname
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
True,
|
||||||
|
f"",
|
||||||
|
f"{hostname}:{game_cfg.ports.userdb}",
|
||||||
|
)
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
for key in self.game_cfg.rsa_keys:
|
||||||
|
if "N" not in key or "d" not in key or "e" not in key:
|
||||||
|
self.logger.error(f"Invalid IDZ key {key}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
hashN = self.rsaHashKeyN(str(key["N"]).encode())
|
||||||
|
self.rsa_keys.append(IDZKey(key["N"], key["d"], key["e"], hashN))
|
||||||
|
|
||||||
|
if len(self.rsa_keys) <= 0:
|
||||||
|
self.logger.error("No valid RSA keys provided! IDZ cannot start!")
|
||||||
|
return
|
||||||
|
|
||||||
|
handler_map = [{} for _ in range(IDZConstants.NUM_VERS)]
|
||||||
|
handler_mod = mod = importlib.import_module(f"titles.idz.handlers")
|
||||||
|
|
||||||
|
for cls_name in dir(handler_mod):
|
||||||
|
if cls_name.startswith("__"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
mod = getattr(handler_mod, cls_name)
|
||||||
|
mod_cmds: List = getattr(mod, "cmd_codes")
|
||||||
|
while len(mod_cmds) < IDZConstants.NUM_VERS:
|
||||||
|
mod_cmds.append(None)
|
||||||
|
|
||||||
|
for i in range(len(mod_cmds)):
|
||||||
|
if mod_cmds[i] is None:
|
||||||
|
mod_cmds[i] = mod_cmds[i - 1]
|
||||||
|
|
||||||
|
handler_map[i][mod_cmds[i]] = mod
|
||||||
|
|
||||||
|
except AttributeError as e:
|
||||||
|
continue
|
||||||
|
|
||||||
|
endpoints.serverFromString(
|
||||||
|
reactor,
|
||||||
|
f"tcp:{self.game_cfg.ports.userdb}:interface={self.core_cfg.server.listen_address}",
|
||||||
|
).listen(
|
||||||
|
IDZUserDBFactory(self.core_cfg, self.game_cfg, self.rsa_keys, handler_map)
|
||||||
|
)
|
||||||
|
|
||||||
|
reactor.listenUDP(
|
||||||
|
self.game_cfg.ports.echo, IDZEcho(self.core_cfg, self.game_cfg)
|
||||||
|
)
|
||||||
|
reactor.listenUDP(
|
||||||
|
self.game_cfg.ports.echo + 1, IDZEcho(self.core_cfg, self.game_cfg)
|
||||||
|
)
|
||||||
|
reactor.listenUDP(
|
||||||
|
self.game_cfg.ports.match, IDZEcho(self.core_cfg, self.game_cfg)
|
||||||
|
)
|
||||||
|
reactor.listenUDP(
|
||||||
|
self.game_cfg.ports.userdb + 1, IDZEcho(self.core_cfg, self.game_cfg)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.logger.info(f"UserDB Listening on port {self.game_cfg.ports.userdb}")
|
||||||
|
|
||||||
|
def render_POST(self, request: Request, version: int, url_path: str) -> bytes:
|
||||||
|
req_raw = request.content.getvalue()
|
||||||
|
self.logger.info(f"IDZ POST request: {url_path} - {req_raw}")
|
||||||
|
return b""
|
||||||
|
|
||||||
|
def render_GET(self, request: Request, version: int, url_path: str) -> bytes:
|
||||||
|
self.logger.info(f"IDZ GET request: {url_path}")
|
||||||
|
request.responseHeaders.setRawHeaders(
|
||||||
|
"Content-Type", [b"text/plain; charset=utf-8"]
|
||||||
|
)
|
||||||
|
request.responseHeaders.setRawHeaders(
|
||||||
|
"Last-Modified", [b"Sun, 23 Apr 2023 05:33:20 GMT"]
|
||||||
|
)
|
||||||
|
|
||||||
|
news = (
|
||||||
|
self.game_cfg.server.news
|
||||||
|
if self.game_cfg.server.news
|
||||||
|
else f"Welcome to Initial D Arcade Stage Zero on {self.core_cfg.server.name}!"
|
||||||
|
)
|
||||||
|
news += "\r\n"
|
||||||
|
news = "1979/01/01 00:00:00 2099/12/31 23:59:59 " + news
|
||||||
|
|
||||||
|
return news.encode()
|
0
titles/idz/match.py
Normal file
0
titles/idz/match.py
Normal file
195
titles/idz/userdb.py
Normal file
195
titles/idz/userdb.py
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
from twisted.internet.protocol import Factory, Protocol
|
||||||
|
import logging, coloredlogs
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
import struct
|
||||||
|
from typing import Dict, Optional, List, Type
|
||||||
|
from twisted.web import server, resource
|
||||||
|
from twisted.internet import reactor, endpoints
|
||||||
|
from twisted.web.http import Request
|
||||||
|
from routes import Mapper
|
||||||
|
import random
|
||||||
|
from os import walk
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from .database import IDZData
|
||||||
|
from .config import IDZConfig
|
||||||
|
from .const import IDZConstants
|
||||||
|
from .handlers import IDZHandlerBase
|
||||||
|
|
||||||
|
HANDLER_MAP: List[Dict]
|
||||||
|
|
||||||
|
|
||||||
|
class IDZKey:
|
||||||
|
def __init__(self, n, d, e, hashN: int) -> None:
|
||||||
|
self.N = n
|
||||||
|
self.d = d
|
||||||
|
self.e = e
|
||||||
|
self.hashN = hashN
|
||||||
|
|
||||||
|
|
||||||
|
class IDZUserDBProtocol(Protocol):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
core_cfg: CoreConfig,
|
||||||
|
game_cfg: IDZConfig,
|
||||||
|
keys: List[IDZKey],
|
||||||
|
handlers: List[Dict],
|
||||||
|
) -> None:
|
||||||
|
self.logger = logging.getLogger("idz")
|
||||||
|
self.core_config = core_cfg
|
||||||
|
self.game_config = game_cfg
|
||||||
|
self.rsa_keys = keys
|
||||||
|
self.handlers = handlers
|
||||||
|
self.static_key = bytes.fromhex(self.game_config.server.aes_key)
|
||||||
|
self.version = None
|
||||||
|
self.version_internal = None
|
||||||
|
self.skip_next = False
|
||||||
|
|
||||||
|
def append_padding(self, data: bytes):
|
||||||
|
"""Appends 0s to the end of the data until it's at the correct size"""
|
||||||
|
length = struct.unpack_from("<H", data, 6)
|
||||||
|
padding_size = length[0] - len(data)
|
||||||
|
data += bytes(padding_size)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def connectionMade(self) -> None:
|
||||||
|
self.logger.debug(f"{self.transport.getPeer().host} Connected")
|
||||||
|
base = 0
|
||||||
|
|
||||||
|
for i in range(len(self.static_key) - 1):
|
||||||
|
shift = 8 * i
|
||||||
|
byte = self.static_key[i]
|
||||||
|
|
||||||
|
base |= byte << shift
|
||||||
|
|
||||||
|
rsa_key = random.choice(self.rsa_keys)
|
||||||
|
key_enc: int = pow(base, rsa_key.e, rsa_key.N)
|
||||||
|
result = (
|
||||||
|
key_enc.to_bytes(0x40, "little")
|
||||||
|
+ struct.pack("<I", 0x01020304)
|
||||||
|
+ rsa_key.hashN.to_bytes(4, "little")
|
||||||
|
)
|
||||||
|
|
||||||
|
self.logger.debug(f"Send handshake {result.hex()}")
|
||||||
|
|
||||||
|
self.transport.write(result)
|
||||||
|
|
||||||
|
def connectionLost(self, reason) -> None:
|
||||||
|
self.logger.debug(
|
||||||
|
f"{self.transport.getPeer().host} Disconnected - {reason.value}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def dataReceived(self, data: bytes) -> None:
|
||||||
|
self.logger.debug(f"Receive data {data.hex()}")
|
||||||
|
crypt = AES.new(self.static_key, AES.MODE_ECB)
|
||||||
|
data_dec = crypt.decrypt(data)
|
||||||
|
self.logger.debug(f"Decrypt data {data_dec.hex()}")
|
||||||
|
|
||||||
|
magic = struct.unpack_from("<I", data_dec, 0)[0]
|
||||||
|
|
||||||
|
if magic == 0xFE78571D:
|
||||||
|
# Ignore
|
||||||
|
self.logger.info(f"Userdb serverbox request {data_dec.hex()}")
|
||||||
|
self.skip_next = True
|
||||||
|
|
||||||
|
self.transport.write(b"\x00")
|
||||||
|
return
|
||||||
|
|
||||||
|
elif magic == 0x01020304:
|
||||||
|
self.version = int(data_dec[16:19].decode())
|
||||||
|
|
||||||
|
if self.version == 110:
|
||||||
|
self.version_internal = IDZConstants.VER_IDZ_110
|
||||||
|
elif self.version == 130:
|
||||||
|
self.version_internal = IDZConstants.VER_IDZ_130
|
||||||
|
elif self.version == 210:
|
||||||
|
self.version_internal = IDZConstants.VER_IDZ_210
|
||||||
|
elif self.version == 230:
|
||||||
|
self.version_internal = IDZConstants.VER_IDZ_230
|
||||||
|
else:
|
||||||
|
self.logger.warn(f"Bad version v{self.version}")
|
||||||
|
self.version = None
|
||||||
|
self.version_internal = None
|
||||||
|
|
||||||
|
self.logger.debug(
|
||||||
|
f"Userdb v{self.version} handshake response from {self.transport.getPeer().host}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
elif self.skip_next:
|
||||||
|
self.skip_next = False
|
||||||
|
self.transport.write(b"\x00")
|
||||||
|
return
|
||||||
|
|
||||||
|
elif self.version is None:
|
||||||
|
# We didn't get a handshake before, and this isn't one now, so we're up the creek
|
||||||
|
self.logger.info(
|
||||||
|
f"Bad UserDB request from from {self.transport.getPeer().host}"
|
||||||
|
)
|
||||||
|
self.transport.write(b"\x00")
|
||||||
|
return
|
||||||
|
|
||||||
|
cmd = struct.unpack_from("<H", data_dec, 0)[0]
|
||||||
|
|
||||||
|
handler_cls: Optional[Type[IDZHandlerBase]] = self.handlers[
|
||||||
|
self.version_internal
|
||||||
|
].get(cmd, None)
|
||||||
|
if handler_cls is None:
|
||||||
|
self.logger.warn(f"No handler for v{self.version} {hex(cmd)} cmd")
|
||||||
|
handler_cls = IDZHandlerBase
|
||||||
|
|
||||||
|
handler = handler_cls(self.core_config, self.game_config, self.version_internal)
|
||||||
|
self.logger.info(
|
||||||
|
f"Userdb v{self.version} {handler.name} request from {self.transport.getPeer().host}"
|
||||||
|
)
|
||||||
|
response = handler.handle(data_dec)
|
||||||
|
|
||||||
|
self.logger.debug(f"Response: {response.hex()}")
|
||||||
|
|
||||||
|
crypt = AES.new(self.static_key, AES.MODE_ECB)
|
||||||
|
response_enc = crypt.encrypt(response)
|
||||||
|
|
||||||
|
self.transport.write(response_enc)
|
||||||
|
|
||||||
|
|
||||||
|
class IDZUserDBFactory(Factory):
|
||||||
|
protocol = IDZUserDBProtocol
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
cfg: CoreConfig,
|
||||||
|
game_cfg: IDZConfig,
|
||||||
|
keys: List[IDZKey],
|
||||||
|
handlers: List[Dict],
|
||||||
|
) -> None:
|
||||||
|
self.core_config = cfg
|
||||||
|
self.game_config = game_cfg
|
||||||
|
self.keys = keys
|
||||||
|
self.handlers = handlers
|
||||||
|
|
||||||
|
def buildProtocol(self, addr):
|
||||||
|
return IDZUserDBProtocol(
|
||||||
|
self.core_config, self.game_config, self.keys, self.handlers
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class IDZUserDBWeb(resource.Resource):
|
||||||
|
def __init__(self, core_cfg: CoreConfig, game_cfg: IDZConfig):
|
||||||
|
super().__init__()
|
||||||
|
self.isLeaf = True
|
||||||
|
self.core_config = core_cfg
|
||||||
|
self.game_config = game_cfg
|
||||||
|
self.logger = logging.getLogger("idz")
|
||||||
|
|
||||||
|
def render_POST(self, request: Request) -> bytes:
|
||||||
|
self.logger.info(
|
||||||
|
f"IDZUserDBWeb POST from {request.getClientAddress().host} to {request.uri} with data {request.content.getvalue()}"
|
||||||
|
)
|
||||||
|
return b""
|
||||||
|
|
||||||
|
def render_GET(self, request: Request) -> bytes:
|
||||||
|
self.logger.info(
|
||||||
|
f"IDZUserDBWeb GET from {request.getClientAddress().host} to {request.uri}"
|
||||||
|
)
|
||||||
|
return b""
|
@ -7,4 +7,4 @@ index = Mai2Servlet
|
|||||||
database = Mai2Data
|
database = Mai2Data
|
||||||
reader = Mai2Reader
|
reader = Mai2Reader
|
||||||
game_codes = [Mai2Constants.GAME_CODE]
|
game_codes = [Mai2Constants.GAME_CODE]
|
||||||
current_schema_version = 3
|
current_schema_version = 4
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from datetime import datetime, date, timedelta
|
from datetime import datetime, date, timedelta
|
||||||
from typing import Dict
|
from typing import Any, Dict
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from core.config import CoreConfig
|
from core.config import CoreConfig
|
||||||
@ -52,6 +52,7 @@ class Mai2Base:
|
|||||||
events = self.data.static.get_enabled_events(self.version)
|
events = self.data.static.get_enabled_events(self.version)
|
||||||
events_lst = []
|
events_lst = []
|
||||||
if events is None:
|
if events is None:
|
||||||
|
self.logger.warn("No enabled events, did you run the reader?")
|
||||||
return {"type": data["type"], "length": 0, "gameEventList": []}
|
return {"type": data["type"], "length": 0, "gameEventList": []}
|
||||||
|
|
||||||
for event in events:
|
for event in events:
|
||||||
@ -59,7 +60,11 @@ class Mai2Base:
|
|||||||
{
|
{
|
||||||
"type": event["type"],
|
"type": event["type"],
|
||||||
"id": event["eventId"],
|
"id": event["eventId"],
|
||||||
"startDate": "2017-12-05 07:00:00.0",
|
# actually use the startDate from the import so it
|
||||||
|
# properly shows all the events when new ones are imported
|
||||||
|
"startDate": datetime.strftime(
|
||||||
|
event["startDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0"
|
||||||
|
),
|
||||||
"endDate": "2099-12-31 00:00:00.0",
|
"endDate": "2099-12-31 00:00:00.0",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -79,12 +84,12 @@ class Mai2Base:
|
|||||||
return {"length": 0, "gameChargeList": []}
|
return {"length": 0, "gameChargeList": []}
|
||||||
|
|
||||||
charge_list = []
|
charge_list = []
|
||||||
for x in range(len(game_charge_list)):
|
for i, charge in enumerate(game_charge_list):
|
||||||
charge_list.append(
|
charge_list.append(
|
||||||
{
|
{
|
||||||
"orderId": x,
|
"orderId": i,
|
||||||
"chargeId": game_charge_list[x]["ticketId"],
|
"chargeId": charge["ticketId"],
|
||||||
"price": game_charge_list[x]["price"],
|
"price": charge["price"],
|
||||||
"startDate": "2017-12-05 07:00:00.0",
|
"startDate": "2017-12-05 07:00:00.0",
|
||||||
"endDate": "2099-12-31 00:00:00.0",
|
"endDate": "2099-12-31 00:00:00.0",
|
||||||
}
|
}
|
||||||
@ -93,16 +98,16 @@ class Mai2Base:
|
|||||||
return {"length": len(charge_list), "gameChargeList": charge_list}
|
return {"length": len(charge_list), "gameChargeList": charge_list}
|
||||||
|
|
||||||
def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict:
|
def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict:
|
||||||
pass
|
return {"returnCode": 1, "apiName": "UpsertClientSettingApi"}
|
||||||
|
|
||||||
def handle_upsert_client_upload_api_request(self, data: Dict) -> Dict:
|
def handle_upsert_client_upload_api_request(self, data: Dict) -> Dict:
|
||||||
pass
|
return {"returnCode": 1, "apiName": "UpsertClientUploadApi"}
|
||||||
|
|
||||||
def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict:
|
def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict:
|
||||||
pass
|
return {"returnCode": 1, "apiName": "UpsertClientBookkeepingApi"}
|
||||||
|
|
||||||
def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict:
|
def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict:
|
||||||
pass
|
return {"returnCode": 1, "apiName": "UpsertClientTestmodeApi"}
|
||||||
|
|
||||||
def handle_get_user_preview_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||||
p = self.data.profile.get_profile_detail(data["userId"], self.version)
|
p = self.data.profile.get_profile_detail(data["userId"], self.version)
|
||||||
@ -167,6 +172,24 @@ class Mai2Base:
|
|||||||
|
|
||||||
self.data.score.put_playlog(user_id, playlog)
|
self.data.score.put_playlog(user_id, playlog)
|
||||||
|
|
||||||
|
return {"returnCode": 1, "apiName": "UploadUserPlaylogApi"}
|
||||||
|
|
||||||
|
def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict:
|
||||||
|
user_id = data["userId"]
|
||||||
|
charge = data["userCharge"]
|
||||||
|
|
||||||
|
# remove the ".0" from the date string, festival only?
|
||||||
|
charge["purchaseDate"] = charge["purchaseDate"].replace(".0", "")
|
||||||
|
self.data.item.put_charge(
|
||||||
|
user_id,
|
||||||
|
charge["chargeId"],
|
||||||
|
charge["stock"],
|
||||||
|
datetime.strptime(charge["purchaseDate"], Mai2Constants.DATE_TIME_FORMAT),
|
||||||
|
datetime.strptime(charge["validDate"], Mai2Constants.DATE_TIME_FORMAT),
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"returnCode": 1, "apiName": "UpsertUserChargelogApi"}
|
||||||
|
|
||||||
def handle_upsert_user_all_api_request(self, data: Dict) -> Dict:
|
def handle_upsert_user_all_api_request(self, data: Dict) -> Dict:
|
||||||
user_id = data["userId"]
|
user_id = data["userId"]
|
||||||
upsert = data["upsertUserAll"]
|
upsert = data["upsertUserAll"]
|
||||||
@ -204,15 +227,21 @@ class Mai2Base:
|
|||||||
|
|
||||||
if "userChargeList" in upsert and len(upsert["userChargeList"]) > 0:
|
if "userChargeList" in upsert and len(upsert["userChargeList"]) > 0:
|
||||||
for charge in upsert["userChargeList"]:
|
for charge in upsert["userChargeList"]:
|
||||||
|
# remove the ".0" from the date string, festival only?
|
||||||
|
charge["purchaseDate"] = charge["purchaseDate"].replace(".0", "")
|
||||||
self.data.item.put_charge(
|
self.data.item.put_charge(
|
||||||
user_id,
|
user_id,
|
||||||
charge["chargeId"],
|
charge["chargeId"],
|
||||||
charge["stock"],
|
charge["stock"],
|
||||||
datetime.strptime(charge["purchaseDate"], "%Y-%m-%d %H:%M:%S"),
|
datetime.strptime(
|
||||||
datetime.strptime(charge["validDate"], "%Y-%m-%d %H:%M:%S")
|
charge["purchaseDate"], Mai2Constants.DATE_TIME_FORMAT
|
||||||
|
),
|
||||||
|
datetime.strptime(
|
||||||
|
charge["validDate"], Mai2Constants.DATE_TIME_FORMAT
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if upsert["isNewCharacterList"] and int(upsert["isNewCharacterList"]) > 0:
|
if "userCharacterList" in upsert and len(upsert["userCharacterList"]) > 0:
|
||||||
for char in upsert["userCharacterList"]:
|
for char in upsert["userCharacterList"]:
|
||||||
self.data.item.put_character(
|
self.data.item.put_character(
|
||||||
user_id,
|
user_id,
|
||||||
@ -222,7 +251,7 @@ class Mai2Base:
|
|||||||
char["useCount"],
|
char["useCount"],
|
||||||
)
|
)
|
||||||
|
|
||||||
if upsert["isNewItemList"] and int(upsert["isNewItemList"]) > 0:
|
if "userItemList" in upsert and len(upsert["userItemList"]) > 0:
|
||||||
for item in upsert["userItemList"]:
|
for item in upsert["userItemList"]:
|
||||||
self.data.item.put_item(
|
self.data.item.put_item(
|
||||||
user_id,
|
user_id,
|
||||||
@ -232,7 +261,7 @@ class Mai2Base:
|
|||||||
item["isValid"],
|
item["isValid"],
|
||||||
)
|
)
|
||||||
|
|
||||||
if upsert["isNewLoginBonusList"] and int(upsert["isNewLoginBonusList"]) > 0:
|
if "userLoginBonusList" in upsert and len(upsert["userLoginBonusList"]) > 0:
|
||||||
for login_bonus in upsert["userLoginBonusList"]:
|
for login_bonus in upsert["userLoginBonusList"]:
|
||||||
self.data.item.put_login_bonus(
|
self.data.item.put_login_bonus(
|
||||||
user_id,
|
user_id,
|
||||||
@ -242,7 +271,7 @@ class Mai2Base:
|
|||||||
login_bonus["isComplete"],
|
login_bonus["isComplete"],
|
||||||
)
|
)
|
||||||
|
|
||||||
if upsert["isNewMapList"] and int(upsert["isNewMapList"]) > 0:
|
if "userMapList" in upsert and len(upsert["userMapList"]) > 0:
|
||||||
for map in upsert["userMapList"]:
|
for map in upsert["userMapList"]:
|
||||||
self.data.item.put_map(
|
self.data.item.put_map(
|
||||||
user_id,
|
user_id,
|
||||||
@ -253,24 +282,34 @@ class Mai2Base:
|
|||||||
map["isComplete"],
|
map["isComplete"],
|
||||||
)
|
)
|
||||||
|
|
||||||
if upsert["isNewMusicDetailList"] and int(upsert["isNewMusicDetailList"]) > 0:
|
if "userMusicDetailList" in upsert and len(upsert["userMusicDetailList"]) > 0:
|
||||||
for music in upsert["userMusicDetailList"]:
|
for music in upsert["userMusicDetailList"]:
|
||||||
self.data.score.put_best_score(user_id, music)
|
self.data.score.put_best_score(user_id, music)
|
||||||
|
|
||||||
if upsert["isNewCourseList"] and int(upsert["isNewCourseList"]) > 0:
|
if "userCourseList" in upsert and len(upsert["userCourseList"]) > 0:
|
||||||
for course in upsert["userCourseList"]:
|
for course in upsert["userCourseList"]:
|
||||||
self.data.score.put_course(user_id, course)
|
self.data.score.put_course(user_id, course)
|
||||||
|
|
||||||
if upsert["isNewFavoriteList"] and int(upsert["isNewFavoriteList"]) > 0:
|
if "userFavoriteList" in upsert and len(upsert["userFavoriteList"]) > 0:
|
||||||
for fav in upsert["userFavoriteList"]:
|
for fav in upsert["userFavoriteList"]:
|
||||||
self.data.item.put_favorite(user_id, fav["kind"], fav["itemIdList"])
|
self.data.item.put_favorite(user_id, fav["kind"], fav["itemIdList"])
|
||||||
|
|
||||||
# if "isNewFriendSeasonRankingList" in upsert and int(upsert["isNewFriendSeasonRankingList"]) > 0:
|
if (
|
||||||
# for fsr in upsert["userFriendSeasonRankingList"]:
|
"userFriendSeasonRankingList" in upsert
|
||||||
# pass
|
and len(upsert["userFriendSeasonRankingList"]) > 0
|
||||||
|
):
|
||||||
|
for fsr in upsert["userFriendSeasonRankingList"]:
|
||||||
|
fsr["recordDate"] = (
|
||||||
|
datetime.strptime(
|
||||||
|
fsr["recordDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.data.item.put_friend_season_ranking(user_id, fsr)
|
||||||
|
|
||||||
|
return {"returnCode": 1, "apiName": "UpsertUserAllApi"}
|
||||||
|
|
||||||
def handle_user_logout_api_request(self, data: Dict) -> Dict:
|
def handle_user_logout_api_request(self, data: Dict) -> Dict:
|
||||||
pass
|
return {"returnCode": 1}
|
||||||
|
|
||||||
def handle_get_user_data_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_data_api_request(self, data: Dict) -> Dict:
|
||||||
profile = self.data.profile.get_profile_detail(data["userId"], self.version)
|
profile = self.data.profile.get_profile_detail(data["userId"], self.version)
|
||||||
@ -311,11 +350,7 @@ class Mai2Base:
|
|||||||
def handle_get_user_card_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_card_api_request(self, data: Dict) -> Dict:
|
||||||
user_cards = self.data.item.get_cards(data["userId"])
|
user_cards = self.data.item.get_cards(data["userId"])
|
||||||
if user_cards is None:
|
if user_cards is None:
|
||||||
return {
|
return {"userId": data["userId"], "nextIndex": 0, "userCardList": []}
|
||||||
"userId": data["userId"],
|
|
||||||
"nextIndex": 0,
|
|
||||||
"userCardList": []
|
|
||||||
}
|
|
||||||
|
|
||||||
max_ct = data["maxCount"]
|
max_ct = data["maxCount"]
|
||||||
next_idx = data["nextIndex"]
|
next_idx = data["nextIndex"]
|
||||||
@ -333,25 +368,23 @@ class Mai2Base:
|
|||||||
tmp.pop("id")
|
tmp.pop("id")
|
||||||
tmp.pop("user")
|
tmp.pop("user")
|
||||||
tmp["startDate"] = datetime.strftime(
|
tmp["startDate"] = datetime.strftime(
|
||||||
tmp["startDate"], "%Y-%m-%d %H:%M:%S")
|
tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT
|
||||||
|
)
|
||||||
tmp["endDate"] = datetime.strftime(
|
tmp["endDate"] = datetime.strftime(
|
||||||
tmp["endDate"], "%Y-%m-%d %H:%M:%S")
|
tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT
|
||||||
|
)
|
||||||
card_list.append(tmp)
|
card_list.append(tmp)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"nextIndex": next_idx,
|
"nextIndex": next_idx,
|
||||||
"userCardList": card_list[start_idx:end_idx]
|
"userCardList": card_list[start_idx:end_idx],
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_charge_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_charge_api_request(self, data: Dict) -> Dict:
|
||||||
user_charges = self.data.item.get_charges(data["userId"])
|
user_charges = self.data.item.get_charges(data["userId"])
|
||||||
if user_charges is None:
|
if user_charges is None:
|
||||||
return {
|
return {"userId": data["userId"], "length": 0, "userChargeList": []}
|
||||||
"userId": data["userId"],
|
|
||||||
"length": 0,
|
|
||||||
"userChargeList": []
|
|
||||||
}
|
|
||||||
|
|
||||||
user_charge_list = []
|
user_charge_list = []
|
||||||
for charge in user_charges:
|
for charge in user_charges:
|
||||||
@ -359,45 +392,46 @@ class Mai2Base:
|
|||||||
tmp.pop("id")
|
tmp.pop("id")
|
||||||
tmp.pop("user")
|
tmp.pop("user")
|
||||||
tmp["purchaseDate"] = datetime.strftime(
|
tmp["purchaseDate"] = datetime.strftime(
|
||||||
tmp["purchaseDate"], "%Y-%m-%d %H:%M:%S")
|
tmp["purchaseDate"], Mai2Constants.DATE_TIME_FORMAT
|
||||||
|
)
|
||||||
tmp["validDate"] = datetime.strftime(
|
tmp["validDate"] = datetime.strftime(
|
||||||
tmp["validDate"], "%Y-%m-%d %H:%M:%S")
|
tmp["validDate"], Mai2Constants.DATE_TIME_FORMAT
|
||||||
|
)
|
||||||
|
|
||||||
user_charge_list.append(tmp)
|
user_charge_list.append(tmp)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"length": len(user_charge_list),
|
"length": len(user_charge_list),
|
||||||
"userChargeList": user_charge_list
|
"userChargeList": user_charge_list,
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_item_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_item_api_request(self, data: Dict) -> Dict:
|
||||||
kind = int(data["nextIndex"] / 10000000000)
|
kind = int(data["nextIndex"] / 10000000000)
|
||||||
next_idx = int(data["nextIndex"] % 10000000000)
|
next_idx = int(data["nextIndex"] % 10000000000)
|
||||||
user_items = self.data.item.get_items(data["userId"], kind)
|
user_item_list = self.data.item.get_items(data["userId"], kind)
|
||||||
user_item_list = []
|
|
||||||
next_idx = 0
|
|
||||||
|
|
||||||
for x in range(next_idx, data["maxCount"]):
|
items: list[Dict[str, Any]] = []
|
||||||
try:
|
for i in range(next_idx, len(user_item_list)):
|
||||||
user_item_list.append({
|
tmp = user_item_list[i]._asdict()
|
||||||
"itemKind": user_items[x]["itemKind"],
|
tmp.pop("user")
|
||||||
"itemId": user_items[x]["itemId"],
|
tmp.pop("id")
|
||||||
"stock": user_items[x]["stock"],
|
items.append(tmp)
|
||||||
"isValid": user_items[x]["isValid"]
|
if len(items) >= int(data["maxCount"]):
|
||||||
})
|
|
||||||
except IndexError:
|
|
||||||
break
|
break
|
||||||
|
|
||||||
if len(user_item_list) == data["maxCount"]:
|
xout = kind * 10000000000 + next_idx + len(items)
|
||||||
next_idx = data["nextIndex"] + data["maxCount"] + 1
|
|
||||||
break
|
if len(items) < int(data["maxCount"]):
|
||||||
|
next_idx = 0
|
||||||
|
else:
|
||||||
|
next_idx = xout
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"nextIndex": next_idx,
|
"nextIndex": next_idx,
|
||||||
"itemKind": kind,
|
"itemKind": kind,
|
||||||
"userItemList": user_item_list
|
"userItemList": items,
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_character_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_character_api_request(self, data: Dict) -> Dict:
|
||||||
@ -479,21 +513,12 @@ class Mai2Base:
|
|||||||
tmp.pop("user")
|
tmp.pop("user")
|
||||||
mlst.append(tmp)
|
mlst.append(tmp)
|
||||||
|
|
||||||
return {
|
return {"userActivity": {"playList": plst, "musicList": mlst}}
|
||||||
"userActivity": {
|
|
||||||
"playList": plst,
|
|
||||||
"musicList": mlst
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def handle_get_user_course_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_course_api_request(self, data: Dict) -> Dict:
|
||||||
user_courses = self.data.score.get_courses(data["userId"])
|
user_courses = self.data.score.get_courses(data["userId"])
|
||||||
if user_courses is None:
|
if user_courses is None:
|
||||||
return {
|
return {"userId": data["userId"], "nextIndex": 0, "userCourseList": []}
|
||||||
"userId": data["userId"],
|
|
||||||
"nextIndex": 0,
|
|
||||||
"userCourseList": []
|
|
||||||
}
|
|
||||||
|
|
||||||
course_list = []
|
course_list = []
|
||||||
for course in user_courses:
|
for course in user_courses:
|
||||||
@ -502,11 +527,7 @@ class Mai2Base:
|
|||||||
tmp.pop("id")
|
tmp.pop("id")
|
||||||
course_list.append(tmp)
|
course_list.append(tmp)
|
||||||
|
|
||||||
return {
|
return {"userId": data["userId"], "nextIndex": 0, "userCourseList": course_list}
|
||||||
"userId": data["userId"],
|
|
||||||
"nextIndex": 0,
|
|
||||||
"userCourseList": course_list
|
|
||||||
}
|
|
||||||
|
|
||||||
def handle_get_user_portrait_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_portrait_api_request(self, data: Dict) -> Dict:
|
||||||
# No support for custom pfps
|
# No support for custom pfps
|
||||||
@ -514,96 +535,103 @@ class Mai2Base:
|
|||||||
|
|
||||||
def handle_get_user_friend_season_ranking_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_friend_season_ranking_api_request(self, data: Dict) -> Dict:
|
||||||
friend_season_ranking = self.data.item.get_friend_season_ranking(data["userId"])
|
friend_season_ranking = self.data.item.get_friend_season_ranking(data["userId"])
|
||||||
friend_season_ranking_list = []
|
if friend_season_ranking is None:
|
||||||
next_index = 0
|
return {
|
||||||
|
"userId": data["userId"],
|
||||||
|
"nextIndex": 0,
|
||||||
|
"userFriendSeasonRankingList": [],
|
||||||
|
}
|
||||||
|
|
||||||
for x in range(data["nextIndex"], data["maxCount"] + data["nextIndex"]):
|
friend_season_ranking_list = []
|
||||||
try:
|
next_idx = int(data["nextIndex"])
|
||||||
friend_season_ranking_list.append(
|
max_ct = int(data["maxCount"])
|
||||||
{
|
|
||||||
"mapId": friend_season_ranking_list[x]["map_id"],
|
for x in range(next_idx, len(friend_season_ranking)):
|
||||||
"distance": friend_season_ranking_list[x]["distance"],
|
tmp = friend_season_ranking[x]._asdict()
|
||||||
"isLock": friend_season_ranking_list[x]["is_lock"],
|
tmp.pop("user")
|
||||||
"isClear": friend_season_ranking_list[x]["is_clear"],
|
tmp.pop("id")
|
||||||
"isComplete": friend_season_ranking_list[x]["is_complete"],
|
tmp["recordDate"] = datetime.strftime(
|
||||||
}
|
tmp["recordDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0"
|
||||||
)
|
)
|
||||||
except:
|
friend_season_ranking_list.append(tmp)
|
||||||
|
|
||||||
|
if len(friend_season_ranking_list) >= max_ct:
|
||||||
break
|
break
|
||||||
|
|
||||||
# We're capped and still have some left to go
|
if len(friend_season_ranking) >= next_idx + max_ct:
|
||||||
if (
|
next_idx += max_ct
|
||||||
len(friend_season_ranking_list) == data["maxCount"]
|
else:
|
||||||
and len(friend_season_ranking) > data["maxCount"] + data["nextIndex"]
|
next_idx = 0
|
||||||
):
|
|
||||||
next_index = data["maxCount"] + data["nextIndex"]
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"nextIndex": next_index,
|
"nextIndex": next_idx,
|
||||||
"userFriendSeasonRankingList": friend_season_ranking_list,
|
"userFriendSeasonRankingList": friend_season_ranking_list,
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_map_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_map_api_request(self, data: Dict) -> Dict:
|
||||||
maps = self.data.item.get_maps(data["userId"])
|
maps = self.data.item.get_maps(data["userId"])
|
||||||
map_list = []
|
if maps is None:
|
||||||
next_index = 0
|
return {
|
||||||
|
"userId": data["userId"],
|
||||||
|
"nextIndex": 0,
|
||||||
|
"userMapList": [],
|
||||||
|
}
|
||||||
|
|
||||||
for x in range(data["nextIndex"], data["maxCount"] + data["nextIndex"]):
|
map_list = []
|
||||||
try:
|
next_idx = int(data["nextIndex"])
|
||||||
map_list.append(
|
max_ct = int(data["maxCount"])
|
||||||
{
|
|
||||||
"mapId": maps[x]["map_id"],
|
for x in range(next_idx, len(maps)):
|
||||||
"distance": maps[x]["distance"],
|
tmp = maps[x]._asdict()
|
||||||
"isLock": maps[x]["is_lock"],
|
tmp.pop("user")
|
||||||
"isClear": maps[x]["is_clear"],
|
tmp.pop("id")
|
||||||
"isComplete": maps[x]["is_complete"],
|
map_list.append(tmp)
|
||||||
}
|
|
||||||
)
|
if len(map_list) >= max_ct:
|
||||||
except:
|
|
||||||
break
|
break
|
||||||
|
|
||||||
# We're capped and still have some left to go
|
if len(maps) >= next_idx + max_ct:
|
||||||
if (
|
next_idx += max_ct
|
||||||
len(map_list) == data["maxCount"]
|
else:
|
||||||
and len(maps) > data["maxCount"] + data["nextIndex"]
|
next_idx = 0
|
||||||
):
|
|
||||||
next_index = data["maxCount"] + data["nextIndex"]
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"nextIndex": next_index,
|
"nextIndex": next_idx,
|
||||||
"userMapList": map_list,
|
"userMapList": map_list,
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict:
|
||||||
login_bonuses = self.data.item.get_login_bonuses(data["userId"])
|
login_bonuses = self.data.item.get_login_bonuses(data["userId"])
|
||||||
login_bonus_list = []
|
if login_bonuses is None:
|
||||||
next_index = 0
|
return {
|
||||||
|
"userId": data["userId"],
|
||||||
|
"nextIndex": 0,
|
||||||
|
"userLoginBonusList": [],
|
||||||
|
}
|
||||||
|
|
||||||
for x in range(data["nextIndex"], data["maxCount"] + data["nextIndex"]):
|
login_bonus_list = []
|
||||||
try:
|
next_idx = int(data["nextIndex"])
|
||||||
login_bonus_list.append(
|
max_ct = int(data["maxCount"])
|
||||||
{
|
|
||||||
"bonusId": login_bonuses[x]["bonus_id"],
|
for x in range(next_idx, len(login_bonuses)):
|
||||||
"point": login_bonuses[x]["point"],
|
tmp = login_bonuses[x]._asdict()
|
||||||
"isCurrent": login_bonuses[x]["is_current"],
|
tmp.pop("user")
|
||||||
"isComplete": login_bonuses[x]["is_complete"],
|
tmp.pop("id")
|
||||||
}
|
login_bonus_list.append(tmp)
|
||||||
)
|
|
||||||
except:
|
if len(login_bonus_list) >= max_ct:
|
||||||
break
|
break
|
||||||
|
|
||||||
# We're capped and still have some left to go
|
if len(login_bonuses) >= next_idx + max_ct:
|
||||||
if (
|
next_idx += max_ct
|
||||||
len(login_bonus_list) == data["maxCount"]
|
else:
|
||||||
and len(login_bonuses) > data["maxCount"] + data["nextIndex"]
|
next_idx = 0
|
||||||
):
|
|
||||||
next_index = data["maxCount"] + data["nextIndex"]
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"nextIndex": next_index,
|
"nextIndex": next_idx,
|
||||||
"userLoginBonusList": login_bonus_list,
|
"userLoginBonusList": login_bonus_list,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -629,5 +657,5 @@ class Mai2Base:
|
|||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"nextIndex": next_index,
|
"nextIndex": next_index,
|
||||||
"userMusicList": [{"userMusicDetailList": music_detail_list}]
|
"userMusicList": [{"userMusicDetailList": music_detail_list}],
|
||||||
}
|
}
|
||||||
|
@ -30,14 +30,16 @@ class Mai2Constants:
|
|||||||
VER_MAIMAI_DX_SPLASH_PLUS = 3
|
VER_MAIMAI_DX_SPLASH_PLUS = 3
|
||||||
VER_MAIMAI_DX_UNIVERSE = 4
|
VER_MAIMAI_DX_UNIVERSE = 4
|
||||||
VER_MAIMAI_DX_UNIVERSE_PLUS = 5
|
VER_MAIMAI_DX_UNIVERSE_PLUS = 5
|
||||||
|
VER_MAIMAI_DX_FESTIVAL = 6
|
||||||
|
|
||||||
VERSION_STRING = (
|
VERSION_STRING = (
|
||||||
"maimai Delux",
|
"maimai DX",
|
||||||
"maimai Delux PLUS",
|
"maimai DX PLUS",
|
||||||
"maimai Delux Splash",
|
"maimai DX Splash",
|
||||||
"maimai Delux Splash PLUS",
|
"maimai DX Splash PLUS",
|
||||||
"maimai Delux Universe",
|
"maimai DX Universe",
|
||||||
"maimai Delux Universe PLUS",
|
"maimai DX Universe PLUS",
|
||||||
|
"maimai DX Festival",
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
31
titles/mai2/festival.py
Normal file
31
titles/mai2/festival.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from titles.mai2.universeplus import Mai2UniversePlus
|
||||||
|
from titles.mai2.const import Mai2Constants
|
||||||
|
from titles.mai2.config import Mai2Config
|
||||||
|
|
||||||
|
|
||||||
|
class Mai2Festival(Mai2UniversePlus):
|
||||||
|
def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None:
|
||||||
|
super().__init__(cfg, game_cfg)
|
||||||
|
self.version = Mai2Constants.VER_MAIMAI_DX_FESTIVAL
|
||||||
|
|
||||||
|
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.36
|
||||||
|
user_data["lastDataVersion"] = "1.30.00"
|
||||||
|
return user_data
|
||||||
|
|
||||||
|
def handle_user_login_api_request(self, data: Dict) -> Dict:
|
||||||
|
user_login = super().handle_user_login_api_request(data)
|
||||||
|
# useless?
|
||||||
|
user_login["Bearer"] = "ARTEMiSTOKEN"
|
||||||
|
return user_login
|
||||||
|
|
||||||
|
def handle_get_user_recommend_rate_music_api_request(self, data: Dict) -> Dict:
|
||||||
|
return {"userId": data["userId"], "userRecommendRateMusicIdList": []}
|
||||||
|
|
||||||
|
def handle_get_user_recommend_select_music_api_request(self, data: Dict) -> Dict:
|
||||||
|
return {"userId": data["userId"], "userRecommendSelectionMusicIdList": []}
|
@ -10,6 +10,7 @@ from os import path
|
|||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
from core.config import CoreConfig
|
from core.config import CoreConfig
|
||||||
|
from core.utils import Utils
|
||||||
from titles.mai2.config import Mai2Config
|
from titles.mai2.config import Mai2Config
|
||||||
from titles.mai2.const import Mai2Constants
|
from titles.mai2.const import Mai2Constants
|
||||||
from titles.mai2.base import Mai2Base
|
from titles.mai2.base import Mai2Base
|
||||||
@ -18,6 +19,7 @@ from titles.mai2.splash import Mai2Splash
|
|||||||
from titles.mai2.splashplus import Mai2SplashPlus
|
from titles.mai2.splashplus import Mai2SplashPlus
|
||||||
from titles.mai2.universe import Mai2Universe
|
from titles.mai2.universe import Mai2Universe
|
||||||
from titles.mai2.universeplus import Mai2UniversePlus
|
from titles.mai2.universeplus import Mai2UniversePlus
|
||||||
|
from titles.mai2.festival import Mai2Festival
|
||||||
|
|
||||||
|
|
||||||
class Mai2Servlet:
|
class Mai2Servlet:
|
||||||
@ -30,12 +32,13 @@ class Mai2Servlet:
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.versions = [
|
self.versions = [
|
||||||
Mai2Base(core_cfg, self.game_cfg),
|
Mai2Base,
|
||||||
Mai2Plus(core_cfg, self.game_cfg),
|
Mai2Plus,
|
||||||
Mai2Splash(core_cfg, self.game_cfg),
|
Mai2Splash,
|
||||||
Mai2SplashPlus(core_cfg, self.game_cfg),
|
Mai2SplashPlus,
|
||||||
Mai2Universe(core_cfg, self.game_cfg),
|
Mai2Universe,
|
||||||
Mai2UniversePlus(core_cfg, self.game_cfg),
|
Mai2UniversePlus,
|
||||||
|
Mai2Festival,
|
||||||
]
|
]
|
||||||
|
|
||||||
self.logger = logging.getLogger("mai2")
|
self.logger = logging.getLogger("mai2")
|
||||||
@ -97,6 +100,7 @@ class Mai2Servlet:
|
|||||||
url_split = url_path.split("/")
|
url_split = url_path.split("/")
|
||||||
internal_ver = 0
|
internal_ver = 0
|
||||||
endpoint = url_split[len(url_split) - 1]
|
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 = Mai2Constants.VER_MAIMAI_DX
|
internal_ver = Mai2Constants.VER_MAIMAI_DX
|
||||||
@ -108,8 +112,10 @@ class Mai2Servlet:
|
|||||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS
|
internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS
|
||||||
elif version >= 120 and version < 125: # Universe
|
elif version >= 120 and version < 125: # Universe
|
||||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE
|
internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE
|
||||||
elif version >= 125: # Universe Plus
|
elif version >= 125 and version < 130: # Universe Plus
|
||||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS
|
internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS
|
||||||
|
elif version >= 130: # Festival
|
||||||
|
internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL
|
||||||
|
|
||||||
if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32:
|
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
|
# If we get a 32 character long hex string, it's a hash and we're
|
||||||
@ -128,25 +134,28 @@ class Mai2Servlet:
|
|||||||
|
|
||||||
req_data = json.loads(unzip)
|
req_data = json.loads(unzip)
|
||||||
|
|
||||||
self.logger.info(f"v{version} {endpoint} request - {req_data}")
|
self.logger.info(f"v{version} {endpoint} request from {client_ip}")
|
||||||
|
self.logger.debug(req_data)
|
||||||
|
|
||||||
func_to_find = "handle_" + inflection.underscore(endpoint) + "_request"
|
func_to_find = "handle_" + inflection.underscore(endpoint) + "_request"
|
||||||
|
handler_cls = self.versions[internal_ver](self.core_cfg, self.game_cfg)
|
||||||
|
|
||||||
if not hasattr(self.versions[internal_ver], func_to_find):
|
if not hasattr(handler_cls, func_to_find):
|
||||||
self.logger.warning(f"Unhandled v{version} request {endpoint}")
|
self.logger.warning(f"Unhandled v{version} request {endpoint}")
|
||||||
return zlib.compress(b'{"returnCode": 1}')
|
resp = {"returnCode": 1}
|
||||||
|
|
||||||
try:
|
else:
|
||||||
handler = getattr(self.versions[internal_ver], func_to_find)
|
try:
|
||||||
resp = handler(req_data)
|
handler = getattr(handler_cls, func_to_find)
|
||||||
|
resp = handler(req_data)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Error handling v{version} method {endpoint} - {e}")
|
self.logger.error(f"Error handling v{version} method {endpoint} - {e}")
|
||||||
return zlib.compress(b'{"stat": "0"}')
|
return zlib.compress(b'{"stat": "0"}')
|
||||||
|
|
||||||
if resp == None:
|
if resp == None:
|
||||||
resp = {"returnCode": 1}
|
resp = {"returnCode": 1}
|
||||||
|
|
||||||
self.logger.info(f"Response {resp}")
|
self.logger.debug(f"Response {resp}")
|
||||||
|
|
||||||
return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8"))
|
return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8"))
|
||||||
|
@ -29,7 +29,7 @@ class Mai2Reader(BaseReader):
|
|||||||
f"Start importer for {Mai2Constants.game_ver_to_string(version)}"
|
f"Start importer for {Mai2Constants.game_ver_to_string(version)}"
|
||||||
)
|
)
|
||||||
except IndexError:
|
except IndexError:
|
||||||
self.logger.error(f"Invalid maidx version {version}")
|
self.logger.error(f"Invalid maimai DX version {version}")
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
def read(self) -> None:
|
def read(self) -> None:
|
||||||
@ -43,6 +43,7 @@ class Mai2Reader(BaseReader):
|
|||||||
for dir in data_dirs:
|
for dir in data_dirs:
|
||||||
self.logger.info(f"Read from {dir}")
|
self.logger.info(f"Read from {dir}")
|
||||||
self.get_events(f"{dir}/event")
|
self.get_events(f"{dir}/event")
|
||||||
|
self.disable_events(f"{dir}/information", f"{dir}/scoreRanking")
|
||||||
self.read_music(f"{dir}/music")
|
self.read_music(f"{dir}/music")
|
||||||
self.read_tickets(f"{dir}/ticket")
|
self.read_tickets(f"{dir}/ticket")
|
||||||
|
|
||||||
@ -64,6 +65,64 @@ class Mai2Reader(BaseReader):
|
|||||||
)
|
)
|
||||||
self.logger.info(f"Added event {id}...")
|
self.logger.info(f"Added event {id}...")
|
||||||
|
|
||||||
|
def disable_events(
|
||||||
|
self, base_information_dir: str, base_score_ranking_dir: str
|
||||||
|
) -> None:
|
||||||
|
self.logger.info(f"Reading disabled events from {base_information_dir}...")
|
||||||
|
|
||||||
|
for root, dirs, files in os.walk(base_information_dir):
|
||||||
|
for dir in dirs:
|
||||||
|
if os.path.exists(f"{root}/{dir}/Information.xml"):
|
||||||
|
with open(f"{root}/{dir}/Information.xml", encoding="utf-8") as f:
|
||||||
|
troot = ET.fromstring(f.read())
|
||||||
|
|
||||||
|
event_id = int(troot.find("name").find("id").text)
|
||||||
|
|
||||||
|
self.data.static.toggle_game_event(
|
||||||
|
self.version, event_id, toggle=False
|
||||||
|
)
|
||||||
|
self.logger.info(f"Disabled event {event_id}...")
|
||||||
|
|
||||||
|
for root, dirs, files in os.walk(base_score_ranking_dir):
|
||||||
|
for dir in dirs:
|
||||||
|
if os.path.exists(f"{root}/{dir}/ScoreRanking.xml"):
|
||||||
|
with open(f"{root}/{dir}/ScoreRanking.xml", encoding="utf-8") as f:
|
||||||
|
troot = ET.fromstring(f.read())
|
||||||
|
|
||||||
|
event_id = int(troot.find("eventName").find("id").text)
|
||||||
|
|
||||||
|
self.data.static.toggle_game_event(
|
||||||
|
self.version, event_id, toggle=False
|
||||||
|
)
|
||||||
|
self.logger.info(f"Disabled event {event_id}...")
|
||||||
|
|
||||||
|
# manually disable events wich are known to be problematic
|
||||||
|
for event_id in [
|
||||||
|
1,
|
||||||
|
10,
|
||||||
|
220311,
|
||||||
|
220312,
|
||||||
|
220313,
|
||||||
|
220314,
|
||||||
|
220315,
|
||||||
|
220316,
|
||||||
|
220317,
|
||||||
|
220318,
|
||||||
|
20121821,
|
||||||
|
21121651,
|
||||||
|
22091511,
|
||||||
|
22091512,
|
||||||
|
22091513,
|
||||||
|
22091514,
|
||||||
|
22091515,
|
||||||
|
22091516,
|
||||||
|
22091517,
|
||||||
|
22091518,
|
||||||
|
22091519,
|
||||||
|
]:
|
||||||
|
self.data.static.toggle_game_event(self.version, event_id, toggle=False)
|
||||||
|
self.logger.info(f"Disabled event {event_id}...")
|
||||||
|
|
||||||
def read_music(self, base_dir: str) -> None:
|
def read_music(self, base_dir: str) -> None:
|
||||||
self.logger.info(f"Reading music from {base_dir}...")
|
self.logger.info(f"Reading music from {base_dir}...")
|
||||||
|
|
||||||
|
@ -71,12 +71,12 @@ map = Table(
|
|||||||
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
),
|
),
|
||||||
Column("map_id", Integer, nullable=False),
|
Column("mapId", Integer, nullable=False),
|
||||||
Column("distance", Integer, nullable=False),
|
Column("distance", Integer, nullable=False),
|
||||||
Column("is_lock", Boolean, nullable=False, server_default="0"),
|
Column("isLock", Boolean, nullable=False, server_default="0"),
|
||||||
Column("is_clear", Boolean, nullable=False, server_default="0"),
|
Column("isClear", Boolean, nullable=False, server_default="0"),
|
||||||
Column("is_complete", Boolean, nullable=False, server_default="0"),
|
Column("isComplete", Boolean, nullable=False, server_default="0"),
|
||||||
UniqueConstraint("user", "map_id", name="mai2_item_map_uk"),
|
UniqueConstraint("user", "mapId", name="mai2_item_map_uk"),
|
||||||
mysql_charset="utf8mb4",
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -89,11 +89,11 @@ login_bonus = Table(
|
|||||||
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
),
|
),
|
||||||
Column("bonus_id", Integer, nullable=False),
|
Column("bonusId", Integer, nullable=False),
|
||||||
Column("point", Integer, nullable=False),
|
Column("point", Integer, nullable=False),
|
||||||
Column("is_current", Boolean, nullable=False, server_default="0"),
|
Column("isCurrent", Boolean, nullable=False, server_default="0"),
|
||||||
Column("is_complete", Boolean, nullable=False, server_default="0"),
|
Column("isComplete", Boolean, nullable=False, server_default="0"),
|
||||||
UniqueConstraint("user", "bonus_id", name="mai2_item_login_bonus_uk"),
|
UniqueConstraint("user", "bonusId", name="mai2_item_login_bonus_uk"),
|
||||||
mysql_charset="utf8mb4",
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -106,13 +106,15 @@ friend_season_ranking = Table(
|
|||||||
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
),
|
),
|
||||||
Column("season_id", Integer, nullable=False),
|
Column("seasonId", Integer, nullable=False),
|
||||||
Column("point", Integer, nullable=False),
|
Column("point", Integer, nullable=False),
|
||||||
Column("rank", Integer, nullable=False),
|
Column("rank", Integer, nullable=False),
|
||||||
Column("reward_get", Boolean, nullable=False),
|
Column("rewardGet", Boolean, nullable=False),
|
||||||
Column("user_name", String(8), nullable=False),
|
Column("userName", String(8), nullable=False),
|
||||||
Column("record_date", String(255), nullable=False),
|
Column("recordDate", TIMESTAMP, nullable=False),
|
||||||
UniqueConstraint("user", "season_id", "user_name", name="mai2_item_login_bonus_uk"),
|
UniqueConstraint(
|
||||||
|
"user", "seasonId", "userName", name="mai2_item_friend_season_ranking_uk"
|
||||||
|
),
|
||||||
mysql_charset="utf8mb4",
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -244,16 +246,16 @@ class Mai2ItemData(BaseData):
|
|||||||
) -> None:
|
) -> None:
|
||||||
sql = insert(login_bonus).values(
|
sql = insert(login_bonus).values(
|
||||||
user=user_id,
|
user=user_id,
|
||||||
bonus_id=bonus_id,
|
bonusId=bonus_id,
|
||||||
point=point,
|
point=point,
|
||||||
is_current=is_current,
|
isCurrent=is_current,
|
||||||
is_complete=is_complete,
|
isComplete=is_complete,
|
||||||
)
|
)
|
||||||
|
|
||||||
conflict = sql.on_duplicate_key_update(
|
conflict = sql.on_duplicate_key_update(
|
||||||
point=point,
|
point=point,
|
||||||
is_current=is_current,
|
isCurrent=is_current,
|
||||||
is_complete=is_complete,
|
isComplete=is_complete,
|
||||||
)
|
)
|
||||||
|
|
||||||
result = self.execute(conflict)
|
result = self.execute(conflict)
|
||||||
@ -293,18 +295,18 @@ class Mai2ItemData(BaseData):
|
|||||||
) -> None:
|
) -> None:
|
||||||
sql = insert(map).values(
|
sql = insert(map).values(
|
||||||
user=user_id,
|
user=user_id,
|
||||||
map_id=map_id,
|
mapId=map_id,
|
||||||
distance=distance,
|
distance=distance,
|
||||||
is_lock=is_lock,
|
isLock=is_lock,
|
||||||
is_clear=is_clear,
|
isClear=is_clear,
|
||||||
is_complete=is_complete,
|
isComplete=is_complete,
|
||||||
)
|
)
|
||||||
|
|
||||||
conflict = sql.on_duplicate_key_update(
|
conflict = sql.on_duplicate_key_update(
|
||||||
distance=distance,
|
distance=distance,
|
||||||
is_lock=is_lock,
|
isLock=is_lock,
|
||||||
is_clear=is_clear,
|
isClear=is_clear,
|
||||||
is_complete=is_complete,
|
isComplete=is_complete,
|
||||||
)
|
)
|
||||||
|
|
||||||
result = self.execute(conflict)
|
result = self.execute(conflict)
|
||||||
@ -324,7 +326,7 @@ class Mai2ItemData(BaseData):
|
|||||||
return result.fetchall()
|
return result.fetchall()
|
||||||
|
|
||||||
def get_map(self, user_id: int, map_id: int) -> Optional[Row]:
|
def get_map(self, user_id: int, map_id: int) -> Optional[Row]:
|
||||||
sql = map.select(and_(map.c.user == user_id, map.c.map_id == map_id))
|
sql = map.select(and_(map.c.user == user_id, map.c.mapId == map_id))
|
||||||
|
|
||||||
result = self.execute(sql)
|
result = self.execute(sql)
|
||||||
if result is None:
|
if result is None:
|
||||||
@ -341,16 +343,16 @@ class Mai2ItemData(BaseData):
|
|||||||
) -> None:
|
) -> None:
|
||||||
sql = insert(character).values(
|
sql = insert(character).values(
|
||||||
user=user_id,
|
user=user_id,
|
||||||
character_id=character_id,
|
characterId=character_id,
|
||||||
level=level,
|
level=level,
|
||||||
awakening=awakening,
|
awakening=awakening,
|
||||||
use_count=use_count,
|
useCount=use_count,
|
||||||
)
|
)
|
||||||
|
|
||||||
conflict = sql.on_duplicate_key_update(
|
conflict = sql.on_duplicate_key_update(
|
||||||
level=level,
|
level=level,
|
||||||
awakening=awakening,
|
awakening=awakening,
|
||||||
use_count=use_count,
|
useCount=use_count,
|
||||||
)
|
)
|
||||||
|
|
||||||
result = self.execute(conflict)
|
result = self.execute(conflict)
|
||||||
@ -385,7 +387,25 @@ class Mai2ItemData(BaseData):
|
|||||||
result = self.execute(sql)
|
result = self.execute(sql)
|
||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
return result.fetchone()
|
return result.fetchall()
|
||||||
|
|
||||||
|
def put_friend_season_ranking(
|
||||||
|
self, aime_id: int, friend_season_ranking_data: Dict
|
||||||
|
) -> Optional[int]:
|
||||||
|
sql = insert(friend_season_ranking).values(
|
||||||
|
user=aime_id, **friend_season_ranking_data
|
||||||
|
)
|
||||||
|
|
||||||
|
conflict = sql.on_duplicate_key_update(**friend_season_ranking_data)
|
||||||
|
result = self.execute(conflict)
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
self.logger.warn(
|
||||||
|
f"put_friend_season_ranking: failed to insert",
|
||||||
|
f"friend_season_ranking! aime_id: {aime_id}",
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
return result.lastrowid
|
||||||
|
|
||||||
def put_favorite(
|
def put_favorite(
|
||||||
self, user_id: int, kind: int, item_id_list: List[int]
|
self, user_id: int, kind: int, item_id_list: List[int]
|
||||||
@ -438,7 +458,7 @@ class Mai2ItemData(BaseData):
|
|||||||
result = self.execute(conflict)
|
result = self.execute(conflict)
|
||||||
if result is None:
|
if result is None:
|
||||||
self.logger.warn(
|
self.logger.warn(
|
||||||
f"put_card: failed to insert card! user_id: {user_id}, kind: {kind}"
|
f"put_card: failed to insert card! user_id: {user_id}, kind: {card_kind}"
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
return result.lastrowid
|
return result.lastrowid
|
||||||
|
@ -158,6 +158,7 @@ extend = Table(
|
|||||||
Column("sortMusicSetting", Integer),
|
Column("sortMusicSetting", Integer),
|
||||||
Column("selectedCardList", JSON),
|
Column("selectedCardList", JSON),
|
||||||
Column("encountMapNpcList", JSON),
|
Column("encountMapNpcList", JSON),
|
||||||
|
Column("playStatusSetting", Integer, server_default="0"),
|
||||||
UniqueConstraint("user", "version", name="mai2_profile_extend_uk"),
|
UniqueConstraint("user", "version", name="mai2_profile_extend_uk"),
|
||||||
mysql_charset="utf8mb4",
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
@ -178,6 +179,7 @@ option = Table(
|
|||||||
Column("slideSpeed", Integer),
|
Column("slideSpeed", Integer),
|
||||||
Column("touchSpeed", Integer),
|
Column("touchSpeed", Integer),
|
||||||
Column("tapDesign", Integer),
|
Column("tapDesign", Integer),
|
||||||
|
Column("tapSe", Integer, server_default="0"),
|
||||||
Column("holdDesign", Integer),
|
Column("holdDesign", Integer),
|
||||||
Column("slideDesign", Integer),
|
Column("slideDesign", Integer),
|
||||||
Column("starType", Integer),
|
Column("starType", Integer),
|
||||||
@ -297,8 +299,10 @@ class Mai2ProfileData(BaseData):
|
|||||||
return result.lastrowid
|
return result.lastrowid
|
||||||
|
|
||||||
def get_profile_detail(self, user_id: int, version: int) -> Optional[Row]:
|
def get_profile_detail(self, user_id: int, version: int) -> Optional[Row]:
|
||||||
sql = select(detail).where(
|
sql = (
|
||||||
and_(detail.c.user == user_id, detail.c.version == version)
|
select(detail)
|
||||||
|
.where(and_(detail.c.user == user_id, detail.c.version <= version))
|
||||||
|
.order_by(detail.c.version.desc())
|
||||||
)
|
)
|
||||||
|
|
||||||
result = self.execute(sql)
|
result = self.execute(sql)
|
||||||
@ -322,8 +326,10 @@ class Mai2ProfileData(BaseData):
|
|||||||
return result.lastrowid
|
return result.lastrowid
|
||||||
|
|
||||||
def get_profile_ghost(self, user_id: int, version: int) -> Optional[Row]:
|
def get_profile_ghost(self, user_id: int, version: int) -> Optional[Row]:
|
||||||
sql = select(ghost).where(
|
sql = (
|
||||||
and_(ghost.c.user == user_id, ghost.c.version_int == version)
|
select(ghost)
|
||||||
|
.where(and_(ghost.c.user == user_id, ghost.c.version_int <= version))
|
||||||
|
.order_by(ghost.c.version.desc())
|
||||||
)
|
)
|
||||||
|
|
||||||
result = self.execute(sql)
|
result = self.execute(sql)
|
||||||
@ -347,8 +353,10 @@ class Mai2ProfileData(BaseData):
|
|||||||
return result.lastrowid
|
return result.lastrowid
|
||||||
|
|
||||||
def get_profile_extend(self, user_id: int, version: int) -> Optional[Row]:
|
def get_profile_extend(self, user_id: int, version: int) -> Optional[Row]:
|
||||||
sql = select(extend).where(
|
sql = (
|
||||||
and_(extend.c.user == user_id, extend.c.version == version)
|
select(extend)
|
||||||
|
.where(and_(extend.c.user == user_id, extend.c.version <= version))
|
||||||
|
.order_by(extend.c.version.desc())
|
||||||
)
|
)
|
||||||
|
|
||||||
result = self.execute(sql)
|
result = self.execute(sql)
|
||||||
@ -372,8 +380,10 @@ class Mai2ProfileData(BaseData):
|
|||||||
return result.lastrowid
|
return result.lastrowid
|
||||||
|
|
||||||
def get_profile_option(self, user_id: int, version: int) -> Optional[Row]:
|
def get_profile_option(self, user_id: int, version: int) -> Optional[Row]:
|
||||||
sql = select(option).where(
|
sql = (
|
||||||
and_(option.c.user == user_id, option.c.version == version)
|
select(option)
|
||||||
|
.where(and_(option.c.user == user_id, option.c.version <= version))
|
||||||
|
.order_by(option.c.version.desc())
|
||||||
)
|
)
|
||||||
|
|
||||||
result = self.execute(sql)
|
result = self.execute(sql)
|
||||||
@ -397,8 +407,10 @@ class Mai2ProfileData(BaseData):
|
|||||||
return result.lastrowid
|
return result.lastrowid
|
||||||
|
|
||||||
def get_profile_rating(self, user_id: int, version: int) -> Optional[Row]:
|
def get_profile_rating(self, user_id: int, version: int) -> Optional[Row]:
|
||||||
sql = select(rating).where(
|
sql = (
|
||||||
and_(rating.c.user == user_id, rating.c.version == version)
|
select(rating)
|
||||||
|
.where(and_(rating.c.user == user_id, rating.c.version <= version))
|
||||||
|
.order_by(rating.c.version.desc())
|
||||||
)
|
)
|
||||||
|
|
||||||
result = self.execute(sql)
|
result = self.execute(sql)
|
||||||
|
@ -25,6 +25,7 @@ best_score = Table(
|
|||||||
Column("syncStatus", Integer),
|
Column("syncStatus", Integer),
|
||||||
Column("deluxscoreMax", Integer),
|
Column("deluxscoreMax", Integer),
|
||||||
Column("scoreRank", Integer),
|
Column("scoreRank", Integer),
|
||||||
|
Column("extNum1", Integer, server_default="0"),
|
||||||
UniqueConstraint("user", "musicId", "level", name="mai2_score_best_uk"),
|
UniqueConstraint("user", "musicId", "level", name="mai2_score_best_uk"),
|
||||||
mysql_charset="utf8mb4",
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
@ -143,6 +144,7 @@ playlog = Table(
|
|||||||
Column("isNewFree", Boolean),
|
Column("isNewFree", Boolean),
|
||||||
Column("extNum1", Integer),
|
Column("extNum1", Integer),
|
||||||
Column("extNum2", Integer),
|
Column("extNum2", Integer),
|
||||||
|
Column("extNum4", Integer, server_default="0"),
|
||||||
Column("trialPlayAchievement", Integer),
|
Column("trialPlayAchievement", Integer),
|
||||||
mysql_charset="utf8mb4",
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
|
@ -16,6 +16,7 @@ event = Table(
|
|||||||
Column("eventId", Integer),
|
Column("eventId", Integer),
|
||||||
Column("type", Integer),
|
Column("type", Integer),
|
||||||
Column("name", String(255)),
|
Column("name", String(255)),
|
||||||
|
Column("startDate", TIMESTAMP, server_default=func.now()),
|
||||||
Column("enabled", Boolean, server_default="1"),
|
Column("enabled", Boolean, server_default="1"),
|
||||||
UniqueConstraint("version", "eventId", "type", name="mai2_static_event_uk"),
|
UniqueConstraint("version", "eventId", "type", name="mai2_static_event_uk"),
|
||||||
mysql_charset="utf8mb4",
|
mysql_charset="utf8mb4",
|
||||||
@ -108,17 +109,17 @@ class Mai2StaticData(BaseData):
|
|||||||
return None
|
return None
|
||||||
return result.fetchall()
|
return result.fetchall()
|
||||||
|
|
||||||
def toggle_game_events(
|
def toggle_game_event(
|
||||||
self, version: int, event_id: int, toggle: bool
|
self, version: int, event_id: int, toggle: bool
|
||||||
) -> Optional[List]:
|
) -> Optional[List]:
|
||||||
sql = event.update(
|
sql = event.update(
|
||||||
and_(event.c.version == version, event.c.event_id == event_id)
|
and_(event.c.version == version, event.c.eventId == event_id)
|
||||||
).values(enabled=int(toggle))
|
).values(enabled=int(toggle))
|
||||||
|
|
||||||
result = self.execute(sql)
|
result = self.execute(sql)
|
||||||
if result is None:
|
if result is None:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
f"toggle_game_events: Failed to update event! event_id {event_id} toggle {toggle}"
|
f"toggle_game_event: Failed to update event! event_id {event_id} toggle {toggle}"
|
||||||
)
|
)
|
||||||
return result.last_updated_params()
|
return result.last_updated_params()
|
||||||
|
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
from typing import Any, List, Dict
|
from typing import Dict
|
||||||
from datetime import datetime, timedelta
|
|
||||||
import pytz
|
|
||||||
import json
|
|
||||||
|
|
||||||
from core.config import CoreConfig
|
from core.config import CoreConfig
|
||||||
from titles.mai2.universe import Mai2Universe
|
from titles.mai2.universe import Mai2Universe
|
||||||
|
@ -7,4 +7,4 @@ index = OngekiServlet
|
|||||||
database = OngekiData
|
database = OngekiData
|
||||||
reader = OngekiReader
|
reader = OngekiReader
|
||||||
game_codes = [OngekiConstants.GAME_CODE]
|
game_codes = [OngekiConstants.GAME_CODE]
|
||||||
current_schema_version = 2
|
current_schema_version = 4
|
||||||
|
@ -452,8 +452,7 @@ class OngekiBase:
|
|||||||
tmp.pop("id")
|
tmp.pop("id")
|
||||||
items.append(tmp)
|
items.append(tmp)
|
||||||
|
|
||||||
xout = kind * 10000000000 + \
|
xout = kind * 10000000000 + (data["nextIndex"] % 10000000000) + len(items)
|
||||||
(data["nextIndex"] % 10000000000) + len(items)
|
|
||||||
|
|
||||||
if len(items) < data["maxCount"] or data["maxCount"] == 0:
|
if len(items) < data["maxCount"] or data["maxCount"] == 0:
|
||||||
nextIndex = 0
|
nextIndex = 0
|
||||||
@ -852,8 +851,7 @@ class OngekiBase:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if "userOption" in upsert and len(upsert["userOption"]) > 0:
|
if "userOption" in upsert and len(upsert["userOption"]) > 0:
|
||||||
self.data.profile.put_profile_options(
|
self.data.profile.put_profile_options(user_id, upsert["userOption"][0])
|
||||||
user_id, upsert["userOption"][0])
|
|
||||||
|
|
||||||
if "userPlaylogList" in upsert:
|
if "userPlaylogList" in upsert:
|
||||||
for playlog in upsert["userPlaylogList"]:
|
for playlog in upsert["userPlaylogList"]:
|
||||||
|
@ -97,7 +97,7 @@ class OngekiBright(OngekiBase):
|
|||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"length": 0,
|
"length": 0,
|
||||||
"nextIndex": 0,
|
"nextIndex": 0,
|
||||||
"userCharacterList": []
|
"userCharacterList": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
max_ct = data["maxCount"]
|
max_ct = data["maxCount"]
|
||||||
@ -548,7 +548,7 @@ class OngekiBright(OngekiBase):
|
|||||||
"returnCode": 1,
|
"returnCode": 1,
|
||||||
"orderId": 0,
|
"orderId": 0,
|
||||||
"serialId": "11111111111111111111",
|
"serialId": "11111111111111111111",
|
||||||
"apiName": "CMUpsertUserPrintPlaylogApi"
|
"apiName": "CMUpsertUserPrintPlaylogApi",
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict:
|
def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict:
|
||||||
@ -556,7 +556,7 @@ class OngekiBright(OngekiBase):
|
|||||||
"returnCode": 1,
|
"returnCode": 1,
|
||||||
"orderId": 0,
|
"orderId": 0,
|
||||||
"serialId": "11111111111111111111",
|
"serialId": "11111111111111111111",
|
||||||
"apiName": "CMUpsertUserPrintlogApi"
|
"apiName": "CMUpsertUserPrintlogApi",
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict:
|
def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict:
|
||||||
|
@ -705,9 +705,7 @@ class OngekiItemData(BaseData):
|
|||||||
user=aime_id, serialId=serial_id, **user_print_data
|
user=aime_id, serialId=serial_id, **user_print_data
|
||||||
)
|
)
|
||||||
|
|
||||||
conflict = sql.on_duplicate_key_update(
|
conflict = sql.on_duplicate_key_update(user=aime_id, **user_print_data)
|
||||||
user=aime_id, **user_print_data
|
|
||||||
)
|
|
||||||
result = self.execute(conflict)
|
result = self.execute(conflict)
|
||||||
|
|
||||||
if result is None:
|
if result is None:
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
from titles.pokken.index import PokkenServlet
|
from .index import PokkenServlet
|
||||||
from titles.pokken.const import PokkenConstants
|
from .const import PokkenConstants
|
||||||
from titles.pokken.database import PokkenData
|
from .database import PokkenData
|
||||||
|
from .frontend import PokkenFrontend
|
||||||
|
|
||||||
index = PokkenServlet
|
index = PokkenServlet
|
||||||
database = PokkenData
|
database = PokkenData
|
||||||
game_codes = [PokkenConstants.GAME_CODE]
|
game_codes = [PokkenConstants.GAME_CODE]
|
||||||
current_schema_version = 1
|
current_schema_version = 1
|
||||||
|
frontend = PokkenFrontend
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import json, logging
|
import json, logging
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
import random
|
||||||
|
|
||||||
from core.config import CoreConfig
|
from core.data import Data
|
||||||
from titles.pokken.config import PokkenConfig
|
from core import CoreConfig
|
||||||
from titles.pokken.proto import jackal_pb2
|
from .config import PokkenConfig
|
||||||
|
from .proto import jackal_pb2
|
||||||
|
from .database import PokkenData
|
||||||
|
|
||||||
|
|
||||||
class PokkenBase:
|
class PokkenBase:
|
||||||
@ -13,6 +16,8 @@ class PokkenBase:
|
|||||||
self.game_cfg = game_cfg
|
self.game_cfg = game_cfg
|
||||||
self.version = 0
|
self.version = 0
|
||||||
self.logger = logging.getLogger("pokken")
|
self.logger = logging.getLogger("pokken")
|
||||||
|
self.data = PokkenData(core_cfg)
|
||||||
|
self.SUPPORT_SET_NONE = 4294967295
|
||||||
|
|
||||||
def handle_noop(self, request: Any) -> bytes:
|
def handle_noop(self, request: Any) -> bytes:
|
||||||
res = jackal_pb2.Response()
|
res = jackal_pb2.Response()
|
||||||
@ -69,33 +74,27 @@ class PokkenBase:
|
|||||||
|
|
||||||
return res.SerializeToString()
|
return res.SerializeToString()
|
||||||
|
|
||||||
def handle_save_client_log(
|
def handle_save_client_log(self, request: jackal_pb2.Request) -> bytes:
|
||||||
self, request: jackal_pb2.Request
|
|
||||||
) -> bytes:
|
|
||||||
res = jackal_pb2.Response()
|
res = jackal_pb2.Response()
|
||||||
res.result = 1
|
res.result = 1
|
||||||
res.type = jackal_pb2.MessageType.SAVE_CLIENT_LOG
|
res.type = jackal_pb2.MessageType.SAVE_CLIENT_LOG
|
||||||
|
|
||||||
return res.SerializeToString()
|
return res.SerializeToString()
|
||||||
|
|
||||||
def handle_check_diagnosis(
|
def handle_check_diagnosis(self, request: jackal_pb2.Request) -> bytes:
|
||||||
self, request: jackal_pb2.Request
|
|
||||||
) -> bytes:
|
|
||||||
res = jackal_pb2.Response()
|
res = jackal_pb2.Response()
|
||||||
res.result = 1
|
res.result = 1
|
||||||
res.type = jackal_pb2.MessageType.CHECK_DIAGNOSIS
|
res.type = jackal_pb2.MessageType.CHECK_DIAGNOSIS
|
||||||
|
|
||||||
return res.SerializeToString()
|
return res.SerializeToString()
|
||||||
|
|
||||||
def handle_load_client_settings(
|
def handle_load_client_settings(self, request: jackal_pb2.Request) -> bytes:
|
||||||
self, request: jackal_pb2.Request
|
|
||||||
) -> bytes:
|
|
||||||
res = jackal_pb2.Response()
|
res = jackal_pb2.Response()
|
||||||
res.result = 1
|
res.result = 1
|
||||||
res.type = jackal_pb2.MessageType.LOAD_CLIENT_SETTINGS
|
res.type = jackal_pb2.MessageType.LOAD_CLIENT_SETTINGS
|
||||||
settings = jackal_pb2.LoadClientSettingsResponseData()
|
settings = jackal_pb2.LoadClientSettingsResponseData()
|
||||||
|
|
||||||
settings.money_magnification = 0
|
settings.money_magnification = 1
|
||||||
settings.continue_bonus_exp = 100
|
settings.continue_bonus_exp = 100
|
||||||
settings.continue_fight_money = 100
|
settings.continue_fight_money = 100
|
||||||
settings.event_bonus_exp = 100
|
settings.event_bonus_exp = 100
|
||||||
@ -123,23 +122,196 @@ class PokkenBase:
|
|||||||
ranking.event_end = True
|
ranking.event_end = True
|
||||||
ranking.modify_date = int(datetime.now().timestamp() / 1000)
|
ranking.modify_date = int(datetime.now().timestamp() / 1000)
|
||||||
res.load_ranking.CopyFrom(ranking)
|
res.load_ranking.CopyFrom(ranking)
|
||||||
|
return res.SerializeToString()
|
||||||
def handle_matching_noop(self, data: Dict = {}, client_ip: str = "127.0.0.1") -> Dict:
|
|
||||||
return {}
|
def handle_load_user(self, request: jackal_pb2.Request) -> bytes:
|
||||||
|
res = jackal_pb2.Response()
|
||||||
def handle_matching_start_matching(self, data: Dict = {}, client_ip: str = "127.0.0.1") -> Dict:
|
res.result = 1
|
||||||
|
res.type = jackal_pb2.MessageType.LOAD_USER
|
||||||
|
access_code = request.load_user.access_code
|
||||||
|
load_usr = jackal_pb2.LoadUserResponseData()
|
||||||
|
user_id = self.data.card.get_user_id_from_card(access_code)
|
||||||
|
|
||||||
|
if user_id is None and self.game_cfg.server.auto_register:
|
||||||
|
user_id = self.data.user.create_user()
|
||||||
|
card_id = self.data.card.create_card(user_id, access_code)
|
||||||
|
|
||||||
|
self.logger.info(
|
||||||
|
f"Register new card {access_code} (UserId {user_id}, CardId {card_id})"
|
||||||
|
)
|
||||||
|
|
||||||
|
elif user_id is None:
|
||||||
|
self.logger.info(f"Registration of card {access_code} blocked!")
|
||||||
|
res.load_user.CopyFrom(load_usr)
|
||||||
|
return res.SerializeToString()
|
||||||
|
|
||||||
|
"""
|
||||||
|
TODO: Add repeated values
|
||||||
|
tutorial_progress_flag
|
||||||
|
rankmatch_progress
|
||||||
|
support_pokemon_list
|
||||||
|
support_set_1
|
||||||
|
support_set_2
|
||||||
|
support_set_3
|
||||||
|
aid_skill_list
|
||||||
|
achievement_flag
|
||||||
|
pokemon_data
|
||||||
|
event_achievement_flag
|
||||||
|
event_achievement_param
|
||||||
|
"""
|
||||||
|
profile = self.data.profile.get_profile(user_id)
|
||||||
|
load_usr.commidserv_result = 1
|
||||||
|
load_usr.load_hash = 1
|
||||||
|
load_usr.cardlock_status = False
|
||||||
|
load_usr.banapass_id = user_id
|
||||||
|
load_usr.access_code = access_code
|
||||||
|
load_usr.precedent_release_flag = 0xFFFFFFFF
|
||||||
|
|
||||||
|
if profile is None:
|
||||||
|
profile_id = self.data.profile.create_profile(user_id)
|
||||||
|
profile_dict = {"id": profile_id, "user": user_id}
|
||||||
|
pokemon_data = []
|
||||||
|
tutorial_progress = []
|
||||||
|
rankmatch_progress = []
|
||||||
|
achievement_flag = []
|
||||||
|
event_achievement_flag = []
|
||||||
|
event_achievement_param = []
|
||||||
|
load_usr.new_card_flag = True
|
||||||
|
|
||||||
|
else:
|
||||||
|
profile_dict = {k: v for k, v in profile._asdict().items() if v is not None}
|
||||||
|
self.logger.info(
|
||||||
|
f"Card-in user {user_id} (Trainer name {profile_dict.get('trainer_name', '')})"
|
||||||
|
)
|
||||||
|
pokemon_data = self.data.profile.get_all_pokemon_data(user_id)
|
||||||
|
tutorial_progress = []
|
||||||
|
rankmatch_progress = []
|
||||||
|
achievement_flag = []
|
||||||
|
event_achievement_flag = []
|
||||||
|
event_achievement_param = []
|
||||||
|
load_usr.new_card_flag = False
|
||||||
|
|
||||||
|
load_usr.navi_newbie_flag = profile_dict.get("navi_newbie_flag", True)
|
||||||
|
load_usr.navi_enable_flag = profile_dict.get("navi_enable_flag", True)
|
||||||
|
load_usr.pad_vibrate_flag = profile_dict.get("pad_vibrate_flag", True)
|
||||||
|
load_usr.home_region_code = profile_dict.get("home_region_code", 0)
|
||||||
|
load_usr.home_loc_name = profile_dict.get("home_loc_name", "")
|
||||||
|
load_usr.pref_code = profile_dict.get("pref_code", 0)
|
||||||
|
load_usr.trainer_name = profile_dict.get(
|
||||||
|
"trainer_name", "Newb" + str(random.randint(1111, 999999))
|
||||||
|
)
|
||||||
|
load_usr.trainer_rank_point = profile_dict.get("trainer_rank_point", 0)
|
||||||
|
load_usr.wallet = profile_dict.get("wallet", 0)
|
||||||
|
load_usr.fight_money = profile_dict.get("fight_money", 0)
|
||||||
|
load_usr.score_point = profile_dict.get("score_point", 0)
|
||||||
|
load_usr.grade_max_num = profile_dict.get("grade_max_num", 0)
|
||||||
|
load_usr.extra_counter = profile_dict.get("extra_counter", 0)
|
||||||
|
load_usr.total_play_days = profile_dict.get("total_play_days", 0)
|
||||||
|
load_usr.play_date_time = profile_dict.get("play_date_time", 0)
|
||||||
|
load_usr.lucky_box_fail_num = profile_dict.get("lucky_box_fail_num", 0)
|
||||||
|
load_usr.event_reward_get_flag = profile_dict.get("event_reward_get_flag", 0)
|
||||||
|
load_usr.rank_pvp_all = profile_dict.get("rank_pvp_all", 0)
|
||||||
|
load_usr.rank_pvp_loc = profile_dict.get("rank_pvp_loc", 0)
|
||||||
|
load_usr.rank_cpu_all = profile_dict.get("rank_cpu_all", 0)
|
||||||
|
load_usr.rank_cpu_loc = profile_dict.get("rank_cpu_loc", 0)
|
||||||
|
load_usr.rank_event = profile_dict.get("rank_event", 0)
|
||||||
|
load_usr.awake_num = profile_dict.get("awake_num", 0)
|
||||||
|
load_usr.use_support_num = profile_dict.get("use_support_num", 0)
|
||||||
|
load_usr.rankmatch_flag = profile_dict.get("rankmatch_flag", 0)
|
||||||
|
load_usr.rankmatch_max = profile_dict.get("rankmatch_max", 0)
|
||||||
|
load_usr.rankmatch_success = profile_dict.get("rankmatch_success", 0)
|
||||||
|
load_usr.beat_num = profile_dict.get("beat_num", 0)
|
||||||
|
load_usr.title_text_id = profile_dict.get("title_text_id", 0)
|
||||||
|
load_usr.title_plate_id = profile_dict.get("title_plate_id", 0)
|
||||||
|
load_usr.title_decoration_id = profile_dict.get("title_decoration_id", 0)
|
||||||
|
load_usr.navi_trainer = profile_dict.get("navi_trainer", 0)
|
||||||
|
load_usr.navi_version_id = profile_dict.get("navi_version_id", 0)
|
||||||
|
load_usr.aid_skill = profile_dict.get("aid_skill", 0)
|
||||||
|
load_usr.comment_text_id = profile_dict.get("comment_text_id", 0)
|
||||||
|
load_usr.comment_word_id = profile_dict.get("comment_word_id", 0)
|
||||||
|
load_usr.latest_use_pokemon = profile_dict.get("latest_use_pokemon", 0)
|
||||||
|
load_usr.ex_ko_num = profile_dict.get("ex_ko_num", 0)
|
||||||
|
load_usr.wko_num = profile_dict.get("wko_num", 0)
|
||||||
|
load_usr.timeup_win_num = profile_dict.get("timeup_win_num", 0)
|
||||||
|
load_usr.cool_ko_num = profile_dict.get("cool_ko_num", 0)
|
||||||
|
load_usr.perfect_ko_num = profile_dict.get("perfect_ko_num", 0)
|
||||||
|
load_usr.record_flag = profile_dict.get("record_flag", 0)
|
||||||
|
load_usr.site_register_status = profile_dict.get("site_register_status", 0)
|
||||||
|
load_usr.continue_num = profile_dict.get("continue_num", 0)
|
||||||
|
|
||||||
|
load_usr.avatar_body = profile_dict.get("avatar_body", 0)
|
||||||
|
load_usr.avatar_gender = profile_dict.get("avatar_gender", 0)
|
||||||
|
load_usr.avatar_background = profile_dict.get("avatar_background", 0)
|
||||||
|
load_usr.avatar_head = profile_dict.get("avatar_head", 0)
|
||||||
|
load_usr.avatar_battleglass = profile_dict.get("avatar_battleglass", 0)
|
||||||
|
load_usr.avatar_face0 = profile_dict.get("avatar_face0", 0)
|
||||||
|
load_usr.avatar_face1 = profile_dict.get("avatar_face1", 0)
|
||||||
|
load_usr.avatar_face2 = profile_dict.get("avatar_face2", 0)
|
||||||
|
load_usr.avatar_bodyall = profile_dict.get("avatar_bodyall", 0)
|
||||||
|
load_usr.avatar_wear = profile_dict.get("avatar_wear", 0)
|
||||||
|
load_usr.avatar_accessory = profile_dict.get("avatar_accessory", 0)
|
||||||
|
load_usr.avatar_stamp = profile_dict.get("avatar_stamp", 0)
|
||||||
|
|
||||||
|
load_usr.event_state = profile_dict.get("event_state", 0)
|
||||||
|
load_usr.event_id = profile_dict.get("event_id", 0)
|
||||||
|
load_usr.sp_bonus_category_id_1 = profile_dict.get("sp_bonus_category_id_1", 0)
|
||||||
|
load_usr.sp_bonus_key_value_1 = profile_dict.get("sp_bonus_key_value_1", 0)
|
||||||
|
load_usr.sp_bonus_category_id_2 = profile_dict.get("sp_bonus_category_id_2", 0)
|
||||||
|
load_usr.sp_bonus_key_value_2 = profile_dict.get("sp_bonus_key_value_2", 0)
|
||||||
|
load_usr.last_play_event_id = profile_dict.get("last_play_event_id", 0)
|
||||||
|
|
||||||
|
res.load_user.CopyFrom(load_usr)
|
||||||
|
return res.SerializeToString()
|
||||||
|
|
||||||
|
def handle_set_bnpassid_lock(self, data: jackal_pb2.Request) -> bytes:
|
||||||
|
res = jackal_pb2.Response()
|
||||||
|
res.result = 1
|
||||||
|
res.type = jackal_pb2.MessageType.SET_BNPASSID_LOCK
|
||||||
|
return res.SerializeToString()
|
||||||
|
|
||||||
|
def handle_save_user(self, request: jackal_pb2.Request) -> bytes:
|
||||||
|
res = jackal_pb2.Response()
|
||||||
|
res.result = 1
|
||||||
|
res.type = jackal_pb2.MessageType.SAVE_USER
|
||||||
|
|
||||||
|
return res.SerializeToString()
|
||||||
|
|
||||||
|
def handle_save_ingame_log(self, data: jackal_pb2.Request) -> bytes:
|
||||||
|
res = jackal_pb2.Response()
|
||||||
|
res.result = 1
|
||||||
|
res.type = jackal_pb2.MessageType.SAVE_INGAME_LOG
|
||||||
|
return res.SerializeToString()
|
||||||
|
|
||||||
|
def handle_save_charge(self, data: jackal_pb2.Request) -> bytes:
|
||||||
|
res = jackal_pb2.Response()
|
||||||
|
res.result = 1
|
||||||
|
res.type = jackal_pb2.MessageType.SAVE_CHARGE
|
||||||
|
return res.SerializeToString()
|
||||||
|
|
||||||
|
def handle_matching_noop(
|
||||||
|
self, data: Dict = {}, client_ip: str = "127.0.0.1"
|
||||||
|
) -> Dict:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def handle_matching_is_matching(self, data: Dict = {}, client_ip: str = "127.0.0.1") -> Dict:
|
def handle_matching_start_matching(
|
||||||
|
self, data: Dict = {}, client_ip: str = "127.0.0.1"
|
||||||
|
) -> Dict:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def handle_matching_is_matching(
|
||||||
|
self, data: Dict = {}, client_ip: str = "127.0.0.1"
|
||||||
|
) -> Dict:
|
||||||
"""
|
"""
|
||||||
"sessionId":"12345678",
|
"sessionId":"12345678",
|
||||||
"A":{
|
"A":{
|
||||||
"pcb_id": data["data"]["must"]["pcb_id"],
|
"pcb_id": data["data"]["must"]["pcb_id"],
|
||||||
"gip": client_ip
|
"gip": client_ip
|
||||||
},
|
},
|
||||||
"list":[]
|
"list":[]
|
||||||
"""
|
"""
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def handle_matching_stop_matching(self, data: Dict = {}, client_ip: str = "127.0.0.1") -> Dict:
|
def handle_matching_stop_matching(
|
||||||
return {}
|
self, data: Dict = {}, client_ip: str = "127.0.0.1"
|
||||||
|
) -> Dict:
|
||||||
|
return {}
|
||||||
|
@ -49,6 +49,17 @@ class PokkenServerConfig:
|
|||||||
self.__config, "pokken", "server", "port_admission", default=9003
|
self.__config, "pokken", "server", "port_admission", default=9003
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def auto_register(self) -> bool:
|
||||||
|
"""
|
||||||
|
Automatically register users in `aime_user` on first carding in with pokken
|
||||||
|
if they don't exist already. Set to false to display an error instead.
|
||||||
|
"""
|
||||||
|
return CoreConfig.get_config_field(
|
||||||
|
self.__config, "pokken", "server", "auto_register", default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PokkenConfig(dict):
|
class PokkenConfig(dict):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.server = PokkenServerConfig(self)
|
self.server = PokkenServerConfig(self)
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
class PokkenConstants:
|
class PokkenConstants:
|
||||||
GAME_CODE = "SDAK"
|
GAME_CODE = "SDAK"
|
||||||
|
|
||||||
@ -7,6 +10,16 @@ class PokkenConstants:
|
|||||||
|
|
||||||
VERSION_NAMES = "Pokken Tournament"
|
VERSION_NAMES = "Pokken Tournament"
|
||||||
|
|
||||||
|
class BATTLE_TYPE(Enum):
|
||||||
|
BATTLE_TYPE_TUTORIAL = 1
|
||||||
|
BATTLE_TYPE_AI = 2
|
||||||
|
BATTLE_TYPE_LAN = 3
|
||||||
|
BATTLE_TYPE_WAN = 4
|
||||||
|
|
||||||
|
class BATTLE_RESULT(Enum):
|
||||||
|
BATTLE_RESULT_WIN = 1
|
||||||
|
BATTLE_RESULT_LOSS = 2
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def game_ver_to_string(cls, ver: int):
|
def game_ver_to_string(cls, ver: int):
|
||||||
return cls.VERSION_NAMES[ver]
|
return cls.VERSION_NAMES[ver]
|
||||||
|
@ -1,7 +1,14 @@
|
|||||||
from core.data import Data
|
from core.data import Data
|
||||||
from core.config import CoreConfig
|
from core.config import CoreConfig
|
||||||
|
|
||||||
|
from .schema import *
|
||||||
|
|
||||||
|
|
||||||
class PokkenData(Data):
|
class PokkenData(Data):
|
||||||
def __init__(self, cfg: CoreConfig) -> None:
|
def __init__(self, cfg: CoreConfig) -> None:
|
||||||
super().__init__(cfg)
|
super().__init__(cfg)
|
||||||
|
|
||||||
|
self.profile = PokkenProfileData(cfg, self.session)
|
||||||
|
self.match = PokkenMatchData(cfg, self.session)
|
||||||
|
self.item = PokkenItemData(cfg, self.session)
|
||||||
|
self.static = PokkenStaticData(cfg, self.session)
|
||||||
|
33
titles/pokken/frontend.py
Normal file
33
titles/pokken/frontend.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import yaml
|
||||||
|
import jinja2
|
||||||
|
from twisted.web.http import Request
|
||||||
|
from os import path
|
||||||
|
|
||||||
|
from core.frontend import FE_Base
|
||||||
|
from core.config import CoreConfig
|
||||||
|
from .database import PokkenData
|
||||||
|
from .config import PokkenConfig
|
||||||
|
from .const import PokkenConstants
|
||||||
|
|
||||||
|
|
||||||
|
class PokkenFrontend(FE_Base):
|
||||||
|
def __init__(
|
||||||
|
self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str
|
||||||
|
) -> None:
|
||||||
|
super().__init__(cfg, environment)
|
||||||
|
self.data = PokkenData(cfg)
|
||||||
|
self.game_cfg = PokkenConfig()
|
||||||
|
if path.exists(f"{cfg_dir}/{PokkenConstants.CONFIG_NAME}"):
|
||||||
|
self.game_cfg.update(
|
||||||
|
yaml.safe_load(open(f"{cfg_dir}/{PokkenConstants.CONFIG_NAME}"))
|
||||||
|
)
|
||||||
|
self.nav_name = "Pokken"
|
||||||
|
|
||||||
|
def render_GET(self, request: Request) -> bytes:
|
||||||
|
template = self.environment.get_template(
|
||||||
|
"titles/pokken/frontend/pokken_index.jinja"
|
||||||
|
)
|
||||||
|
return template.render(
|
||||||
|
title=f"{self.core_config.server.name} | {self.nav_name}",
|
||||||
|
game_list=self.environment.globals["game_list"],
|
||||||
|
).encode("utf-16")
|
4
titles/pokken/frontend/pokken_index.jinja
Normal file
4
titles/pokken/frontend/pokken_index.jinja
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{% extends "core/frontend/index.jinja" %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>Pokken</h1>
|
||||||
|
{% endblock content %}
|
@ -87,7 +87,7 @@ class PokkenServlet(resource.Resource):
|
|||||||
if not game_cfg.server.enable:
|
if not game_cfg.server.enable:
|
||||||
return (False, "")
|
return (False, "")
|
||||||
|
|
||||||
return (True, "PKF2")
|
return (True, "PKF1")
|
||||||
|
|
||||||
def setup(self) -> None:
|
def setup(self) -> None:
|
||||||
# TODO: Setup stun, turn (UDP) and admission (WSS) servers
|
# TODO: Setup stun, turn (UDP) and admission (WSS) servers
|
||||||
@ -115,18 +115,18 @@ class PokkenServlet(resource.Resource):
|
|||||||
pokken_request.type
|
pokken_request.type
|
||||||
].name.lower()
|
].name.lower()
|
||||||
|
|
||||||
|
self.logger.debug(pokken_request)
|
||||||
|
|
||||||
handler = getattr(self.base, f"handle_{endpoint}", None)
|
handler = getattr(self.base, f"handle_{endpoint}", None)
|
||||||
if handler is None:
|
if handler is None:
|
||||||
self.logger.warn(f"No handler found for message type {endpoint}")
|
self.logger.warn(f"No handler found for message type {endpoint}")
|
||||||
return self.base.handle_noop(pokken_request)
|
return self.base.handle_noop(pokken_request)
|
||||||
|
|
||||||
self.logger.info(f"{endpoint} request from {Utils.get_ip_addr(request)}")
|
self.logger.info(f"{endpoint} request from {Utils.get_ip_addr(request)}")
|
||||||
self.logger.debug(pokken_request)
|
|
||||||
|
|
||||||
ret = handler(pokken_request)
|
ret = handler(pokken_request)
|
||||||
self.logger.debug(f"Response: {ret}")
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def handle_matching(self, request: Request) -> bytes:
|
def handle_matching(self, request: Request) -> bytes:
|
||||||
content = request.content.getvalue()
|
content = request.content.getvalue()
|
||||||
client_ip = Utils.get_ip_addr(request)
|
client_ip = Utils.get_ip_addr(request)
|
||||||
@ -135,26 +135,37 @@ class PokkenServlet(resource.Resource):
|
|||||||
self.logger.info("Empty matching request")
|
self.logger.info("Empty matching request")
|
||||||
return json.dumps(self.base.handle_matching_noop()).encode()
|
return json.dumps(self.base.handle_matching_noop()).encode()
|
||||||
|
|
||||||
json_content = ast.literal_eval(content.decode().replace('null', 'None').replace('true', 'True').replace('false', 'False'))
|
json_content = ast.literal_eval(
|
||||||
|
content.decode()
|
||||||
|
.replace("null", "None")
|
||||||
|
.replace("true", "True")
|
||||||
|
.replace("false", "False")
|
||||||
|
)
|
||||||
self.logger.info(f"Matching {json_content['call']} request")
|
self.logger.info(f"Matching {json_content['call']} request")
|
||||||
self.logger.debug(json_content)
|
self.logger.debug(json_content)
|
||||||
|
|
||||||
handler = getattr(self.base, f"handle_matching_{inflection.underscore(json_content['call'])}", None)
|
handler = getattr(
|
||||||
|
self.base,
|
||||||
|
f"handle_matching_{inflection.underscore(json_content['call'])}",
|
||||||
|
None,
|
||||||
|
)
|
||||||
if handler is None:
|
if handler is None:
|
||||||
self.logger.warn(f"No handler found for message type {json_content['call']}")
|
self.logger.warn(
|
||||||
|
f"No handler found for message type {json_content['call']}"
|
||||||
|
)
|
||||||
return json.dumps(self.base.handle_matching_noop()).encode()
|
return json.dumps(self.base.handle_matching_noop()).encode()
|
||||||
|
|
||||||
ret = handler(json_content, client_ip)
|
ret = handler(json_content, client_ip)
|
||||||
|
|
||||||
if ret is None:
|
if ret is None:
|
||||||
ret = {}
|
ret = {}
|
||||||
if "result" not in ret:
|
if "result" not in ret:
|
||||||
ret["result"] = "true"
|
ret["result"] = "true"
|
||||||
if "data" not in ret:
|
if "data" not in ret:
|
||||||
ret["data"] = {}
|
ret["data"] = {}
|
||||||
if "timestamp" not in ret:
|
if "timestamp" not in ret:
|
||||||
ret["timestamp"] = int(datetime.now().timestamp() * 1000)
|
ret["timestamp"] = int(datetime.now().timestamp() * 1000)
|
||||||
|
|
||||||
self.logger.debug(f"Response {ret}")
|
self.logger.debug(f"Response {ret}")
|
||||||
|
|
||||||
return json.dumps(ret).encode()
|
return json.dumps(ret).encode()
|
||||||
|
4
titles/pokken/schema/__init__.py
Normal file
4
titles/pokken/schema/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from .profile import PokkenProfileData
|
||||||
|
from .match import PokkenMatchData
|
||||||
|
from .item import PokkenItemData
|
||||||
|
from .static import PokkenStaticData
|
34
titles/pokken/schema/item.py
Normal file
34
titles/pokken/schema/item.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
from typing import Optional, Dict, List
|
||||||
|
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case
|
||||||
|
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON
|
||||||
|
from sqlalchemy.schema import ForeignKey
|
||||||
|
from sqlalchemy.sql import func, select, update, delete
|
||||||
|
from sqlalchemy.engine import Row
|
||||||
|
from sqlalchemy.dialects.mysql import insert
|
||||||
|
|
||||||
|
from core.data.schema import BaseData, metadata
|
||||||
|
|
||||||
|
item = Table(
|
||||||
|
"pokken_item",
|
||||||
|
metadata,
|
||||||
|
Column("id", Integer, primary_key=True, nullable=False),
|
||||||
|
Column(
|
||||||
|
"user",
|
||||||
|
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
||||||
|
nullable=False,
|
||||||
|
unique=True,
|
||||||
|
),
|
||||||
|
Column("category", Integer),
|
||||||
|
Column("content", Integer),
|
||||||
|
Column("type", Integer),
|
||||||
|
UniqueConstraint("user", "category", "content", "type", name="pokken_item_uk"),
|
||||||
|
mysql_charset="utf8mb4",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PokkenItemData(BaseData):
|
||||||
|
"""
|
||||||
|
Items obtained as rewards
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
52
titles/pokken/schema/match.py
Normal file
52
titles/pokken/schema/match.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
from typing import Optional, Dict, List
|
||||||
|
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case
|
||||||
|
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON
|
||||||
|
from sqlalchemy.schema import ForeignKey
|
||||||
|
from sqlalchemy.sql import func, select, update, delete
|
||||||
|
from sqlalchemy.engine import Row
|
||||||
|
from sqlalchemy.dialects.mysql import insert
|
||||||
|
|
||||||
|
from core.data.schema import BaseData, metadata
|
||||||
|
|
||||||
|
# Pokken sends depressingly little match data...
|
||||||
|
match_data = Table(
|
||||||
|
"pokken_match_data",
|
||||||
|
metadata,
|
||||||
|
Column("id", Integer, primary_key=True, nullable=False),
|
||||||
|
Column(
|
||||||
|
"user",
|
||||||
|
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
Column("num_games", Integer),
|
||||||
|
Column("play_modes", JSON),
|
||||||
|
Column("results", JSON),
|
||||||
|
Column("ex_ko_num", Integer),
|
||||||
|
Column("wko_num", Integer),
|
||||||
|
Column("timeup_win_num", Integer),
|
||||||
|
Column("cool_ko_num", Integer),
|
||||||
|
Column("perfect_ko_num", Integer),
|
||||||
|
Column("use_navi", Integer),
|
||||||
|
Column("use_navi_cloth", Integer),
|
||||||
|
Column("use_aid_skill", Integer),
|
||||||
|
Column("play_date", TIMESTAMP),
|
||||||
|
mysql_charset="utf8mb4",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PokkenMatchData(BaseData):
|
||||||
|
"""
|
||||||
|
Match logs
|
||||||
|
"""
|
||||||
|
|
||||||
|
def save_match(self, user_id: int, match_data: Dict) -> Optional[int]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_match(self, match_id: int) -> Optional[Row]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_matches_by_user(self, user_id: int) -> Optional[List[Row]]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_matches(self, limit: int = 20) -> Optional[List[Row]]:
|
||||||
|
pass
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user