Merge branch 'develop'
This commit is contained in:
@@ -158,5 +158,6 @@ cert/*
|
||||
!cert/server.pem
|
||||
config/*
|
||||
deliver/*
|
||||
*.gz
|
||||
|
||||
dbdump-*.json
|
||||
@@ -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
|
||||
+51
-5
@@ -10,6 +10,7 @@ from Crypto.PublicKey import RSA
|
||||
from Crypto.Hash import SHA
|
||||
from Crypto.Signature import PKCS1_v1_5
|
||||
from time import strptime
|
||||
from os import path
|
||||
|
||||
from core.config import CoreConfig
|
||||
from core.utils import Utils
|
||||
@@ -55,7 +56,7 @@ class AllnetServlet:
|
||||
self.logger.error("No games detected!")
|
||||
|
||||
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:
|
||||
enabled, uri, host = mod.index.get_allnet_info(
|
||||
code, self.config, self.config_folder
|
||||
@@ -95,6 +96,7 @@ class AllnetServlet:
|
||||
|
||||
self.logger.debug(f"Allnet request: {vars(req)}")
|
||||
if req.game_id not in self.uri_registry:
|
||||
if not self.config.server.is_develop:
|
||||
msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}."
|
||||
self.data.base.log_event(
|
||||
"allnet", "ALLNET_AUTH_UNKNOWN_GAME", logging.WARN, msg
|
||||
@@ -104,6 +106,14 @@ class AllnetServlet:
|
||||
resp.stat = 0
|
||||
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]
|
||||
|
||||
machine = self.data.arcade.get_machine(req.serial)
|
||||
@@ -181,15 +191,51 @@ class AllnetServlet:
|
||||
self.logger.error(e)
|
||||
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()
|
||||
|
||||
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)])
|
||||
|
||||
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)])
|
||||
|
||||
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):
|
||||
req_dict = self.billing_req_to_dict(request.content.getvalue())
|
||||
request_ip = Utils.get_ip_addr(request)
|
||||
@@ -412,7 +458,7 @@ class AllnetDownloadOrderRequest:
|
||||
|
||||
|
||||
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.serial = serial
|
||||
self.uri = uri
|
||||
|
||||
@@ -188,6 +188,12 @@ class AllnetConfig:
|
||||
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:
|
||||
def __init__(self, parent_config: "CoreConfig") -> None:
|
||||
|
||||
+72
-16
@@ -1,5 +1,5 @@
|
||||
import logging, coloredlogs
|
||||
from typing import Optional
|
||||
from typing import Optional, Dict, List
|
||||
from sqlalchemy.orm import scoped_session, sessionmaker
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy import create_engine
|
||||
@@ -32,7 +32,7 @@ class Data:
|
||||
self.arcade = ArcadeData(self.config, self.session)
|
||||
self.card = CardData(self.config, self.session)
|
||||
self.base = BaseData(self.config, self.session)
|
||||
self.schema_ver_latest = 4
|
||||
self.current_schema_version = 4
|
||||
|
||||
log_fmt_str = "[%(asctime)s] %(levelname)s | Database | %(message)s"
|
||||
log_fmt = logging.Formatter(log_fmt_str)
|
||||
@@ -71,7 +71,9 @@ class Data:
|
||||
games = Utils.get_all_titles()
|
||||
for game_dir, game_mod in games.items():
|
||||
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)
|
||||
metadata.create_all(self.__engine.connect())
|
||||
|
||||
@@ -84,8 +86,8 @@ class Data:
|
||||
f"Could not load database schema from {game_dir} - {e}"
|
||||
)
|
||||
|
||||
self.logger.info(f"Setting base_schema_ver to {self.schema_ver_latest}")
|
||||
self.base.set_schema_ver(self.schema_ver_latest)
|
||||
self.logger.info(f"Setting base_schema_ver to {self.current_schema_version}")
|
||||
self.base.set_schema_ver(self.current_schema_version)
|
||||
|
||||
self.logger.info(
|
||||
f"Setting user auto_incrememnt to {self.config.database.user_table_autoincrement_start}"
|
||||
@@ -129,9 +131,32 @@ class Data:
|
||||
|
||||
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)
|
||||
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:
|
||||
self.logger.error(
|
||||
@@ -263,17 +288,48 @@ class Data:
|
||||
self.user.delete_user(user["id"])
|
||||
|
||||
def autoupgrade(self) -> None:
|
||||
all_games = self.base.get_all_schema_vers()
|
||||
if all_games is None:
|
||||
all_game_versions = self.base.get_all_schema_vers()
|
||||
if all_game_versions is None:
|
||||
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()
|
||||
update_ver = 1
|
||||
for y in range(2, 100):
|
||||
if os.path.exists(f"core/data/schema/versions/{game}_{y}_upgrade.sql"):
|
||||
update_ver = y
|
||||
else:
|
||||
break
|
||||
update_ver = int(x["version"])
|
||||
latest_ver = all_games_list.get(game, 1)
|
||||
if game == "CORE":
|
||||
latest_ver = self.current_schema_version
|
||||
|
||||
self.migrate_database(game, update_ver, "upgrade")
|
||||
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"):
|
||||
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:
|
||||
self.logger.warning(f"Could not find script {game}_{y}_upgrade.sql")
|
||||
failed = True
|
||||
|
||||
if not failed:
|
||||
self.base.set_schema_ver(latest_ver, game)
|
||||
|
||||
@@ -47,7 +47,7 @@ class BaseData:
|
||||
res = None
|
||||
|
||||
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)
|
||||
|
||||
except SQLAlchemyError as e:
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,2 @@
|
||||
SET FOREIGN_KEY_CHECKS=0;
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE wacca_profile DROP COLUMN playcount_time_free;
|
||||
@@ -0,0 +1 @@
|
||||
DELETE FROM wacca_item WHERE type=17 AND item_id=312002;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE wacca_profile ADD playcount_time_free int(11) DEFAULT 0 NULL AFTER playcount_stageup;
|
||||
+5
-2
@@ -71,8 +71,11 @@ class FrontendServlet(resource.Resource):
|
||||
game_fe = game_mod.frontend(cfg, self.environment, config_dir)
|
||||
self.game_list.append({"url": game_dir, "name": game_fe.nav_name})
|
||||
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.putChild(b"gate", FE_Gate(cfg, self.environment))
|
||||
|
||||
+3
-9
@@ -46,9 +46,7 @@ class MuchaServlet:
|
||||
if enabled:
|
||||
self.mucha_registry.append(game_cd)
|
||||
|
||||
self.logger.info(
|
||||
f"Serving {len(self.mucha_registry)} games"
|
||||
)
|
||||
self.logger.info(f"Serving {len(self.mucha_registry)} games")
|
||||
|
||||
def handle_boardauth(self, request: Request, _: Dict) -> bytes:
|
||||
req_dict = self.mucha_preprocess(request.content.getvalue())
|
||||
@@ -62,9 +60,7 @@ class MuchaServlet:
|
||||
|
||||
req = MuchaAuthRequest(req_dict)
|
||||
self.logger.debug(f"Mucha request {vars(req)}")
|
||||
self.logger.info(
|
||||
f"Boardauth request from {client_ip} for {req.gameVer}"
|
||||
)
|
||||
self.logger.info(f"Boardauth request from {client_ip} for {req.gameVer}")
|
||||
|
||||
if req.gameCd not in self.mucha_registry:
|
||||
self.logger.warn(f"Unknown gameCd {req.gameCd}")
|
||||
@@ -92,9 +88,7 @@ class MuchaServlet:
|
||||
|
||||
req = MuchaUpdateRequest(req_dict)
|
||||
self.logger.debug(f"Mucha request {vars(req)}")
|
||||
self.logger.info(
|
||||
f"Updatecheck request from {client_ip} for {req.gameVer}"
|
||||
)
|
||||
self.logger.info(f"Updatecheck request from {client_ip} for {req.gameVer}")
|
||||
|
||||
if req.gameCd not in self.mucha_registry:
|
||||
self.logger.warn(f"Unknown gameCd {req.gameCd}")
|
||||
|
||||
+8
-1
@@ -16,6 +16,9 @@ class Utils:
|
||||
if not dir.startswith("__"):
|
||||
try:
|
||||
mod = importlib.import_module(f"titles.{dir}")
|
||||
if hasattr(mod, "game_codes") and hasattr(
|
||||
mod, "index"
|
||||
): # Minimum required to function
|
||||
ret[dir] = mod
|
||||
|
||||
except ImportError as e:
|
||||
@@ -25,4 +28,8 @@ class Utils:
|
||||
|
||||
@classmethod
|
||||
def get_ip_addr(cls, req: Request) -> str:
|
||||
return req.getAllHeaders()[b"x-forwarded-for"].decode() if b"x-forwarded-for" in req.getAllHeaders() else req.getClientAddress().host
|
||||
return (
|
||||
req.getAllHeaders()[b"x-forwarded-for"].decode()
|
||||
if b"x-forwarded-for" in req.getAllHeaders()
|
||||
else req.getClientAddress().host
|
||||
)
|
||||
|
||||
+16
-7
@@ -1,5 +1,6 @@
|
||||
import yaml
|
||||
import argparse
|
||||
import logging
|
||||
from core.config import CoreConfig
|
||||
from core.data import Data
|
||||
from os import path, mkdir, access, W_OK
|
||||
@@ -32,7 +33,9 @@ if __name__ == "__main__":
|
||||
|
||||
cfg = CoreConfig()
|
||||
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):
|
||||
mkdir(cfg.server.log_dir)
|
||||
@@ -45,7 +48,6 @@ if __name__ == "__main__":
|
||||
|
||||
data = Data(cfg)
|
||||
|
||||
|
||||
if args.action == "create":
|
||||
data.create_database()
|
||||
|
||||
@@ -54,15 +56,22 @@ if __name__ == "__main__":
|
||||
|
||||
elif args.action == "upgrade" or args.action == "rollback":
|
||||
if args.version is None:
|
||||
data.logger.error("Must set game and version to migrate to")
|
||||
exit(0)
|
||||
data.logger.warn("No version set, upgrading to latest")
|
||||
|
||||
if args.game is None:
|
||||
data.logger.info("No game set, upgrading core schema")
|
||||
data.migrate_database("CORE", int(args.version), args.action)
|
||||
data.logger.warn("No game set, upgrading core schema")
|
||||
data.migrate_database(
|
||||
"CORE",
|
||||
int(args.version) if args.version is not None else None,
|
||||
args.action,
|
||||
)
|
||||
|
||||
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":
|
||||
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:
|
||||
|
||||
```shell
|
||||
python dbutils.py --game SDBT --version 2 upgrade
|
||||
python dbutils.py --game SDBT --version 3 upgrade
|
||||
python dbutils.py --game SDBT upgrade
|
||||
```
|
||||
|
||||
## crossbeats REV.
|
||||
@@ -114,6 +113,7 @@ Config file is located in `config/cxb.yaml`.
|
||||
| 3 | maimai DX Splash PLUS |
|
||||
| 4 | maimai DX Universe |
|
||||
| 5 | maimai DX Universe PLUS |
|
||||
| 6 | maimai DX Festival |
|
||||
|
||||
### 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.
|
||||
|
||||
**NOTE: It is required to use the importer because the game will
|
||||
crash without it!**
|
||||
crash without Events!**
|
||||
|
||||
### Database upgrade
|
||||
|
||||
Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see which version is the latest, f.e. `SDEZ_2_upgrade.sql`. In order to upgrade to version 2 in this case you need to perform all previous updates as well:
|
||||
|
||||
```shell
|
||||
python dbutils.py --game SDEZ --version 2 upgrade
|
||||
python dbutils.py --game SDEZ upgrade
|
||||
```
|
||||
|
||||
## 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:
|
||||
|
||||
```shell
|
||||
python dbutils.py --game SBZV --version 2 upgrade
|
||||
python dbutils.py --game SBZV --version 3 upgrade
|
||||
python dbutils.py --game SBZV --version 4 upgrade
|
||||
python dbutils.py --game SBZV upgrade
|
||||
```
|
||||
|
||||
## 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:
|
||||
|
||||
```shell
|
||||
python dbutils.py --game SDDT --version 2 upgrade
|
||||
python dbutils.py --game SDDT --version 3 upgrade
|
||||
python dbutils.py --game SDDT --version 4 upgrade
|
||||
python dbutils.py --game SDDT upgrade
|
||||
```
|
||||
|
||||
## 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:
|
||||
|
||||
```shell
|
||||
python dbutils.py --game SDFE --version 2 upgrade
|
||||
python dbutils.py --game SDFE --version 3 upgrade
|
||||
python dbutils.py --game SDFE upgrade
|
||||
```
|
||||
|
||||
@@ -2,5 +2,19 @@ server:
|
||||
enable: True
|
||||
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:
|
||||
encrypted_only: False
|
||||
@@ -32,6 +32,7 @@ allnet:
|
||||
loglevel: "info"
|
||||
port: 80
|
||||
allow_online_updates: False
|
||||
update_cfg_folder: ""
|
||||
|
||||
billing:
|
||||
port: 8443
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
server:
|
||||
enable: True
|
||||
loglevel: "info"
|
||||
hostname: ""
|
||||
news: ""
|
||||
aes_key: ""
|
||||
|
||||
ports:
|
||||
userdb: 10000
|
||||
match: 10010
|
||||
echo: 10020
|
||||
@@ -6,3 +6,4 @@ server:
|
||||
port_stun: 9001
|
||||
port_turn: 9002
|
||||
port_admission: 9003
|
||||
auto_register: True
|
||||
@@ -29,5 +29,8 @@ gates:
|
||||
- 17
|
||||
- 18
|
||||
- 19
|
||||
- 20
|
||||
- 21
|
||||
- 22
|
||||
- 23
|
||||
- 24
|
||||
|
||||
@@ -26,6 +26,22 @@ class HttpDispatcher(resource.Resource):
|
||||
self.title = TitleServlet(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(
|
||||
"allnet_ping",
|
||||
"/naomitest.html",
|
||||
@@ -95,6 +111,7 @@ class HttpDispatcher(resource.Resource):
|
||||
)
|
||||
|
||||
def render_GET(self, request: Request) -> bytes:
|
||||
self.logger.debug(request.uri)
|
||||
test = self.map_get.match(request.uri.decode())
|
||||
client_ip = Utils.get_ip_addr(request)
|
||||
|
||||
|
||||
@@ -135,8 +135,7 @@ if __name__ == "__main__":
|
||||
|
||||
for dir, mod in titles.items():
|
||||
if args.series in mod.game_codes:
|
||||
handler = mod.reader(config, args.version,
|
||||
bin_arg, opt_arg, args.extra)
|
||||
handler = mod.reader(config, args.version, bin_arg, opt_arg, args.extra)
|
||||
handler.read()
|
||||
|
||||
logger.info("Done")
|
||||
|
||||
@@ -9,8 +9,8 @@ Games listed below have been tested and confirmed working. Only game versions ol
|
||||
+ Crossbeats Rev
|
||||
+ All versions + omnimix
|
||||
|
||||
+ Maimai
|
||||
+ All versions up to Universe Plus
|
||||
+ maimai DX
|
||||
+ All versions up to Festival
|
||||
|
||||
+ Hatsune Miku Arcade
|
||||
+ All versions
|
||||
@@ -26,6 +26,8 @@ Games listed below have been tested and confirmed working. Only game versions ol
|
||||
+ Lily R
|
||||
+ Reverse
|
||||
|
||||
+ Pokken
|
||||
+ Final Online
|
||||
|
||||
## Requirements
|
||||
- python 3 (tested working with 3.9 and 3.10, other versions YMMV)
|
||||
|
||||
@@ -7,4 +7,4 @@ index = ChuniServlet
|
||||
database = ChuniData
|
||||
reader = ChuniReader
|
||||
game_codes = [ChuniConstants.GAME_CODE, ChuniConstants.GAME_CODE_NEW]
|
||||
current_schema_version = 1
|
||||
current_schema_version = 3
|
||||
|
||||
+147
-30
@@ -23,7 +23,98 @@ class ChuniBase:
|
||||
self.version = ChuniConstants.VER_CHUNITHM
|
||||
|
||||
def handle_game_login_api_request(self, data: Dict) -> Dict:
|
||||
# self.data.base.log_event("chuni", "login", logging.INFO, {"version": self.version, "user": data["userId"]})
|
||||
"""
|
||||
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}
|
||||
|
||||
def handle_game_logout_api_request(self, data: Dict) -> Dict:
|
||||
@@ -130,7 +221,7 @@ class ChuniBase:
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
"length": len(activity_list),
|
||||
"kind": data["kind"],
|
||||
"kind": int(data["kind"]),
|
||||
"userActivityList": activity_list,
|
||||
}
|
||||
|
||||
@@ -309,26 +400,29 @@ class ChuniBase:
|
||||
}
|
||||
|
||||
def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict:
|
||||
"""
|
||||
Unsure how to get this to trigger...
|
||||
"""
|
||||
user_id = data["userId"]
|
||||
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 {
|
||||
"userId": data["userId"],
|
||||
"length": 2,
|
||||
"userLoginBonusList": [
|
||||
{
|
||||
"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",
|
||||
},
|
||||
],
|
||||
"userId": user_id,
|
||||
"length": len(user_login_list),
|
||||
"userLoginBonusList": user_login_list,
|
||||
}
|
||||
|
||||
def handle_get_user_map_api_request(self, data: Dict) -> Dict:
|
||||
@@ -451,13 +545,13 @@ class ChuniBase:
|
||||
"playerLevel": profile["playerLevel"],
|
||||
"rating": profile["rating"],
|
||||
"headphone": profile["headphone"],
|
||||
"chargeState": "1",
|
||||
"chargeState": 1,
|
||||
"userNameEx": profile["userName"],
|
||||
}
|
||||
|
||||
def handle_get_user_recent_rating_api_request(self, data: Dict) -> Dict:
|
||||
recet_rating_list = self.data.profile.get_profile_recent_rating(data["userId"])
|
||||
if recet_rating_list is None:
|
||||
recent_rating_list = self.data.profile.get_profile_recent_rating(data["userId"])
|
||||
if recent_rating_list is None:
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
"length": 0,
|
||||
@@ -466,8 +560,8 @@ class ChuniBase:
|
||||
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
"length": len(recet_rating_list["recentRating"]),
|
||||
"userRecentRatingList": recet_rating_list["recentRating"],
|
||||
"length": len(recent_rating_list["recentRating"]),
|
||||
"userRecentRatingList": recent_rating_list["recentRating"],
|
||||
}
|
||||
|
||||
def handle_get_user_region_api_request(self, data: Dict) -> Dict:
|
||||
@@ -479,9 +573,25 @@ class ChuniBase:
|
||||
}
|
||||
|
||||
def handle_get_user_team_api_request(self, data: Dict) -> Dict:
|
||||
# TODO: Team
|
||||
# TODO: use the database "chuni_profile_team" with a GUI
|
||||
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:
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
@@ -580,9 +690,18 @@ class ChuniBase:
|
||||
for emoney in upsert["userEmoneyList"]:
|
||||
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"}
|
||||
|
||||
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"}
|
||||
|
||||
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:
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
"userNetBattleData": {
|
||||
"recentNBSelectMusicList": []
|
||||
}
|
||||
"userNetBattleData": {"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:
|
||||
def __init__(self, parent_config: "ChuniConfig") -> None:
|
||||
self.__config = parent_config
|
||||
@@ -46,4 +82,7 @@ class ChuniCryptoConfig:
|
||||
class ChuniConfig(dict):
|
||||
def __init__(self) -> None:
|
||||
self.server = ChuniServerConfig(self)
|
||||
self.team = ChuniTeamConfig(self)
|
||||
self.mods = ChuniModsConfig(self)
|
||||
self.version = ChuniVersionConfig(self)
|
||||
self.crypto = ChuniCryptoConfig(self)
|
||||
|
||||
+27
-9
@@ -89,14 +89,26 @@ class ChuniServlet:
|
||||
|
||||
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:
|
||||
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.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
|
||||
def get_allnet_info(
|
||||
@@ -167,11 +179,15 @@ class ChuniServlet:
|
||||
|
||||
else:
|
||||
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"}')
|
||||
|
||||
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"}')
|
||||
|
||||
endpoint = self.hash_table[internal_ver][endpoint.lower()]
|
||||
@@ -193,7 +209,11 @@ class ChuniServlet:
|
||||
|
||||
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(
|
||||
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)
|
||||
|
||||
self.logger.info(
|
||||
f"v{version} {endpoint} request from {client_ip}"
|
||||
)
|
||||
self.logger.info(f"v{version} {endpoint} request from {client_ip}")
|
||||
self.logger.debug(req_data)
|
||||
|
||||
func_to_find = "handle_" + inflection.underscore(endpoint) + "_request"
|
||||
|
||||
+6
-12
@@ -49,8 +49,8 @@ class ChuniNew(ChuniBase):
|
||||
"matchEndTime": match_end,
|
||||
"matchTimeLimit": 99,
|
||||
"matchErrorLimit": 9999,
|
||||
"romVersion": "2.00.00",
|
||||
"dataVersion": "2.00.00",
|
||||
"romVersion": self.game_cfg.version.version(self.version)["rom"],
|
||||
"dataVersion": self.game_cfg.version.version(self.version)["data"],
|
||||
"matchingUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
|
||||
"matchingUriX": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
|
||||
"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()
|
||||
print_list.append(tmp["cardId"])
|
||||
|
||||
if len(user_print_list) >= max_ct:
|
||||
if len(print_list) >= max_ct:
|
||||
break
|
||||
|
||||
if len(user_print_list) >= max_ct:
|
||||
if len(print_list) >= max_ct:
|
||||
next_idx = next_idx + max_ct
|
||||
else:
|
||||
next_idx = -1
|
||||
@@ -454,9 +454,7 @@ class ChuniNew(ChuniBase):
|
||||
|
||||
# set the card print state to success and use the orderId as the key
|
||||
self.data.item.put_user_print_state(
|
||||
user_id,
|
||||
id=upsert["orderId"],
|
||||
hasCompleted=True
|
||||
user_id, id=upsert["orderId"], hasCompleted=True
|
||||
)
|
||||
|
||||
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
|
||||
for order_id in order_ids:
|
||||
self.data.item.put_user_print_state(
|
||||
user_id,
|
||||
id=order_id,
|
||||
hasCompleted=True
|
||||
)
|
||||
self.data.item.put_user_print_state(user_id, id=order_id, hasCompleted=True)
|
||||
|
||||
return {"returnCode": "1", "apiName": "CMUpsertUserPrintCancelApi"}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Any
|
||||
import pytz
|
||||
|
||||
from core.config import CoreConfig
|
||||
from titles.chuni.new import ChuniNew
|
||||
@@ -15,8 +13,12 @@ class ChuniNewPlus(ChuniNew):
|
||||
|
||||
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
ret = super().handle_get_game_setting_api_request(data)
|
||||
ret["gameSetting"]["romVersion"] = "2.05.00"
|
||||
ret["gameSetting"]["dataVersion"] = "2.05.00"
|
||||
ret["gameSetting"]["romVersion"] = self.game_cfg.version.version(self.version)[
|
||||
"rom"
|
||||
]
|
||||
ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version(self.version)[
|
||||
"data"
|
||||
]
|
||||
ret["gameSetting"][
|
||||
"matchingUri"
|
||||
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/205/ChuniServlet/"
|
||||
|
||||
@@ -42,6 +42,80 @@ class ChuniReader(BaseReader):
|
||||
self.read_music(f"{dir}/music")
|
||||
self.read_charges(f"{dir}/chargeItem")
|
||||
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:
|
||||
for root, dirs, files in walk(evt_dir):
|
||||
|
||||
@@ -184,8 +184,73 @@ print_detail = Table(
|
||||
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):
|
||||
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]:
|
||||
character_data["user"] = user_id
|
||||
|
||||
@@ -335,7 +400,7 @@ class ChuniItemData(BaseData):
|
||||
sql = print_state.select(
|
||||
and_(
|
||||
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_(
|
||||
print_state.c.user == aime_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
|
||||
)
|
||||
|
||||
conflict = sql.on_duplicate_key_update(
|
||||
user=aime_id, **user_print_data
|
||||
)
|
||||
conflict = sql.on_duplicate_key_update(user=aime_id, **user_print_data)
|
||||
result = self.execute(conflict)
|
||||
|
||||
if result is None:
|
||||
|
||||
@@ -558,8 +558,10 @@ class ChuniProfileData(BaseData):
|
||||
return result.lastrowid
|
||||
|
||||
def get_profile_activity(self, aime_id: int, kind: int) -> Optional[List[Row]]:
|
||||
sql = select(activity).where(
|
||||
and_(activity.c.user == aime_id, activity.c.kind == kind)
|
||||
sql = (
|
||||
select(activity)
|
||||
.where(and_(activity.c.user == aime_id, activity.c.kind == kind))
|
||||
.order_by(activity.c.sortNumber.desc()) # to get the last played track
|
||||
)
|
||||
|
||||
result = self.execute(sql)
|
||||
|
||||
+149
-10
@@ -122,8 +122,150 @@ gacha_cards = Table(
|
||||
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):
|
||||
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(
|
||||
self, version: int, event_id: int, type: int, name: str
|
||||
) -> Optional[int]:
|
||||
@@ -390,20 +532,17 @@ class ChuniStaticData(BaseData):
|
||||
return None
|
||||
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 = (
|
||||
select(cards.c.cardId)
|
||||
.filter(
|
||||
cards.c.charaId == chara_id
|
||||
)
|
||||
.scalar_subquery()
|
||||
select(cards.c.cardId).filter(cards.c.charaId == chara_id).scalar_subquery()
|
||||
)
|
||||
|
||||
# Perform the main query, also rename the resulting column to ranking
|
||||
sql = gacha_cards.select(and_(
|
||||
gacha_cards.c.gachaId == gacha_id,
|
||||
gacha_cards.c.cardId == sql_sub
|
||||
))
|
||||
sql = gacha_cards.select(
|
||||
and_(gacha_cards.c.gachaId == gacha_id, gacha_cards.c.cardId == sql_sub)
|
||||
)
|
||||
|
||||
result = self.execute(sql)
|
||||
if result is None:
|
||||
|
||||
+4
-1
@@ -12,6 +12,7 @@ from twisted.web.http import Request
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
|
||||
from core.config import CoreConfig
|
||||
from core.utils import Utils
|
||||
from titles.cm.config import CardMakerConfig
|
||||
from titles.cm.const import CardMakerConstants
|
||||
from titles.cm.base import CardMakerBase
|
||||
@@ -82,6 +83,7 @@ class CardMakerServlet:
|
||||
url_split = url_path.split("/")
|
||||
internal_ver = 0
|
||||
endpoint = url_split[len(url_split) - 1]
|
||||
client_ip = Utils.get_ip_addr(request)
|
||||
|
||||
print(f"version: {version}")
|
||||
|
||||
@@ -107,7 +109,8 @@ class CardMakerServlet:
|
||||
|
||||
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"
|
||||
|
||||
|
||||
+3
-2
@@ -80,7 +80,7 @@ class CardMakerReader(BaseReader):
|
||||
for dir in data_dirs:
|
||||
self.read_chuni_card(f"{dir}/CHU/card")
|
||||
self.read_chuni_gacha(f"{dir}/CHU/gacha")
|
||||
|
||||
self.read_mai2_card(f"{dir}/MAI/card")
|
||||
self.read_ongeki_gacha(f"{dir}/MU3/gacha")
|
||||
|
||||
def read_chuni_card(self, base_dir: str) -> None:
|
||||
@@ -90,7 +90,7 @@ class CardMakerReader(BaseReader):
|
||||
"v2_00": ChuniConstants.VER_CHUNITHM_NEW,
|
||||
"v2_05": ChuniConstants.VER_CHUNITHM_NEW_PLUS,
|
||||
# Chunithm SUN, ignore for now
|
||||
"v2_10": ChuniConstants.VER_CHUNITHM_NEW_PLUS + 1
|
||||
"v2_10": ChuniConstants.VER_CHUNITHM_NEW_PLUS + 1,
|
||||
}
|
||||
|
||||
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.20": Mai2Constants.VER_MAIMAI_DX_UNIVERSE,
|
||||
"1.25": Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS,
|
||||
"1.30": Mai2Constants.VER_MAIMAI_DX_FESTIVAL,
|
||||
}
|
||||
|
||||
for root, dirs, files in os.walk(base_dir):
|
||||
|
||||
+1
-3
@@ -101,9 +101,7 @@ class CxbServlet(resource.Resource):
|
||||
f"Ready on ports {self.game_cfg.server.port} & {self.game_cfg.server.port_secure}"
|
||||
)
|
||||
else:
|
||||
self.logger.info(
|
||||
f"Ready on port {self.game_cfg.server.port}"
|
||||
)
|
||||
self.logger.info(f"Ready on port {self.game_cfg.server.port}")
|
||||
|
||||
def render_POST(self, request: Request):
|
||||
version = 0
|
||||
|
||||
@@ -7,4 +7,4 @@ index = DivaServlet
|
||||
database = DivaData
|
||||
reader = DivaReader
|
||||
game_codes = [DivaConstants.GAME_CODE]
|
||||
current_schema_version = 1
|
||||
current_schema_version = 4
|
||||
|
||||
@@ -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
|
||||
@@ -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=[])
|
||||
@@ -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]
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user