Merge branch 'develop' into fork_develop

This commit is contained in:
Dniel97 2023-04-30 20:39:35 +02:00
commit 7364181de1
Signed by untrusted user: Dniel97
GPG Key ID: 6180B3C768FB2E08
101 changed files with 2688 additions and 691 deletions

1
.gitignore vendored
View File

@ -158,5 +158,6 @@ cert/*
!cert/server.pem
config/*
deliver/*
*.gz
dbdump-*.json

49
changelog.md Normal file
View 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

View File

@ -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
@ -106,7 +107,9 @@ class AllnetServlet:
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}")
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)])
@ -188,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)
@ -419,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

View File

@ -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:

View File

@ -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)

View File

@ -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:

View 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;

View 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;

View File

@ -0,0 +1,2 @@
SET FOREIGN_KEY_CHECKS=0;
SET FOREIGN_KEY_CHECKS=1;

View File

@ -0,0 +1 @@
ALTER TABLE wacca_profile DROP COLUMN playcount_time_free;

View File

@ -0,0 +1 @@
DELETE FROM wacca_item WHERE type=17 AND item_id=312002;

View File

@ -0,0 +1 @@
ALTER TABLE wacca_profile ADD playcount_time_free int(11) DEFAULT 0 NULL AFTER playcount_stageup;

View File

@ -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))

View File

@ -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}")

View File

@ -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
)

View File

@ -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()

View File

@ -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
```

View File

@ -32,6 +32,7 @@ allnet:
loglevel: "info"
port: 80
allow_online_updates: False
update_cfg_folder: ""
billing:
port: 8443

11
example_config/idz.yaml Normal file
View File

@ -0,0 +1,11 @@
server:
enable: True
loglevel: "info"
hostname: ""
news: ""
aes_key: ""
ports:
userdb: 10000
match: 10010
echo: 10020

View File

@ -29,5 +29,8 @@ gates:
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24

View File

@ -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",

View File

@ -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")

View File

@ -2,15 +2,15 @@
A network service emulator for games running SEGA'S ALL.NET service, and similar.
# Supported games
Games listed below have been tested and confirmed working. Only game versions older then the current one in active use in arcades (n-0) or current game versions older then a year (y-1) are supported.
Games listed below have been tested and confirmed working. Only game versions older then the version currently active in arcades, or games versions that have not recieved a major update in over one year, are supported.
+ Chunithm
+ All versions up to New!! Plus
+ 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)

View File

@ -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"

View File

@ -13,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"] = self.game_cfg.version.version(self.version)["rom"]
ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version(self.version)["data"]
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/"

View File

@ -200,7 +200,9 @@ class ChuniStaticData(BaseData):
return result.lastrowid
def get_login_bonus(
self, version: int, preset_id: int,
self,
version: int,
preset_id: int,
) -> Optional[List[Row]]:
sql = login_bonus.select(
and_(

View File

@ -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"

View File

@ -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):

View File

@ -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

View File

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

8
titles/idz/__init__.py Normal file
View 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
View 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
View 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
View 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
View 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)

View 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

View 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

View 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

View 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

View 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

View File

View 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

View 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)

View 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

View 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

View 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

View 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)

View 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

View 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)

View 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

View 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

View 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)

View 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)

View 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

View 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)

View 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)

View 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

View 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

View 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)

View 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

View 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

View 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
View 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
View File

195
titles/idz/userdb.py Normal file
View 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""

View File

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

View File

@ -1,5 +1,5 @@
from datetime import datetime, date, timedelta
from typing import Dict
from typing import Any, Dict
import logging
from core.config import CoreConfig
@ -52,6 +52,7 @@ class Mai2Base:
events = self.data.static.get_enabled_events(self.version)
events_lst = []
if events is None:
self.logger.warn("No enabled events, did you run the reader?")
return {"type": data["type"], "length": 0, "gameEventList": []}
for event in events:
@ -59,7 +60,11 @@ class Mai2Base:
{
"type": event["type"],
"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",
}
)
@ -79,12 +84,12 @@ class Mai2Base:
return {"length": 0, "gameChargeList": []}
charge_list = []
for x in range(len(game_charge_list)):
for i, charge in enumerate(game_charge_list):
charge_list.append(
{
"orderId": x,
"chargeId": game_charge_list[x]["ticketId"],
"price": game_charge_list[x]["price"],
"orderId": i,
"chargeId": charge["ticketId"],
"price": charge["price"],
"startDate": "2017-12-05 07: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}
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:
pass
return {"returnCode": 1, "apiName": "UpsertClientUploadApi"}
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:
pass
return {"returnCode": 1, "apiName": "UpsertClientTestmodeApi"}
def handle_get_user_preview_api_request(self, data: Dict) -> Dict:
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)
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:
user_id = data["userId"]
upsert = data["upsertUserAll"]
@ -204,15 +227,21 @@ class Mai2Base:
if "userChargeList" in upsert and len(upsert["userChargeList"]) > 0:
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(
user_id,
charge["chargeId"],
charge["stock"],
datetime.strptime(charge["purchaseDate"], "%Y-%m-%d %H:%M:%S"),
datetime.strptime(charge["validDate"], "%Y-%m-%d %H:%M:%S")
datetime.strptime(
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"]:
self.data.item.put_character(
user_id,
@ -222,7 +251,7 @@ class Mai2Base:
char["useCount"],
)
if upsert["isNewItemList"] and int(upsert["isNewItemList"]) > 0:
if "userItemList" in upsert and len(upsert["userItemList"]) > 0:
for item in upsert["userItemList"]:
self.data.item.put_item(
user_id,
@ -232,7 +261,7 @@ class Mai2Base:
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"]:
self.data.item.put_login_bonus(
user_id,
@ -242,7 +271,7 @@ class Mai2Base:
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"]:
self.data.item.put_map(
user_id,
@ -253,24 +282,34 @@ class Mai2Base:
map["isComplete"],
)
if upsert["isNewMusicDetailList"] and int(upsert["isNewMusicDetailList"]) > 0:
if "userMusicDetailList" in upsert and len(upsert["userMusicDetailList"]) > 0:
for music in upsert["userMusicDetailList"]:
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"]:
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"]:
self.data.item.put_favorite(user_id, fav["kind"], fav["itemIdList"])
# if "isNewFriendSeasonRankingList" in upsert and int(upsert["isNewFriendSeasonRankingList"]) > 0:
# for fsr in upsert["userFriendSeasonRankingList"]:
# pass
if (
"userFriendSeasonRankingList" in upsert
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:
pass
return {"returnCode": 1}
def handle_get_user_data_api_request(self, data: Dict) -> Dict:
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:
user_cards = self.data.item.get_cards(data["userId"])
if user_cards is None:
return {
"userId": data["userId"],
"nextIndex": 0,
"userCardList": []
}
return {"userId": data["userId"], "nextIndex": 0, "userCardList": []}
max_ct = data["maxCount"]
next_idx = data["nextIndex"]
@ -333,25 +368,23 @@ class Mai2Base:
tmp.pop("id")
tmp.pop("user")
tmp["startDate"] = datetime.strftime(
tmp["startDate"], "%Y-%m-%d %H:%M:%S")
tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT
)
tmp["endDate"] = datetime.strftime(
tmp["endDate"], "%Y-%m-%d %H:%M:%S")
tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT
)
card_list.append(tmp)
return {
"userId": data["userId"],
"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:
user_charges = self.data.item.get_charges(data["userId"])
if user_charges is None:
return {
"userId": data["userId"],
"length": 0,
"userChargeList": []
}
return {"userId": data["userId"], "length": 0, "userChargeList": []}
user_charge_list = []
for charge in user_charges:
@ -359,45 +392,46 @@ class Mai2Base:
tmp.pop("id")
tmp.pop("user")
tmp["purchaseDate"] = datetime.strftime(
tmp["purchaseDate"], "%Y-%m-%d %H:%M:%S")
tmp["purchaseDate"], Mai2Constants.DATE_TIME_FORMAT
)
tmp["validDate"] = datetime.strftime(
tmp["validDate"], "%Y-%m-%d %H:%M:%S")
tmp["validDate"], Mai2Constants.DATE_TIME_FORMAT
)
user_charge_list.append(tmp)
return {
"userId": data["userId"],
"length": len(user_charge_list),
"userChargeList": user_charge_list
"userChargeList": user_charge_list,
}
def handle_get_user_item_api_request(self, data: Dict) -> Dict:
kind = int(data["nextIndex"] / 10000000000)
next_idx = int(data["nextIndex"] % 10000000000)
user_items = self.data.item.get_items(data["userId"], kind)
user_item_list = []
user_item_list = self.data.item.get_items(data["userId"], kind)
items: list[Dict[str, Any]] = []
for i in range(next_idx, len(user_item_list)):
tmp = user_item_list[i]._asdict()
tmp.pop("user")
tmp.pop("id")
items.append(tmp)
if len(items) >= int(data["maxCount"]):
break
xout = kind * 10000000000 + next_idx + len(items)
if len(items) < int(data["maxCount"]):
next_idx = 0
for x in range(next_idx, data["maxCount"]):
try:
user_item_list.append({
"itemKind": user_items[x]["itemKind"],
"itemId": user_items[x]["itemId"],
"stock": user_items[x]["stock"],
"isValid": user_items[x]["isValid"]
})
except IndexError:
break
if len(user_item_list) == data["maxCount"]:
next_idx = data["nextIndex"] + data["maxCount"] + 1
break
else:
next_idx = xout
return {
"userId": data["userId"],
"nextIndex": next_idx,
"itemKind": kind,
"userItemList": user_item_list
"userItemList": items,
}
def handle_get_user_character_api_request(self, data: Dict) -> Dict:
@ -479,21 +513,12 @@ class Mai2Base:
tmp.pop("user")
mlst.append(tmp)
return {
"userActivity": {
"playList": plst,
"musicList": mlst
}
}
return {"userActivity": {"playList": plst, "musicList": mlst}}
def handle_get_user_course_api_request(self, data: Dict) -> Dict:
user_courses = self.data.score.get_courses(data["userId"])
if user_courses is None:
return {
"userId": data["userId"],
"nextIndex": 0,
"userCourseList": []
}
return {"userId": data["userId"], "nextIndex": 0, "userCourseList": []}
course_list = []
for course in user_courses:
@ -502,11 +527,7 @@ class Mai2Base:
tmp.pop("id")
course_list.append(tmp)
return {
"userId": data["userId"],
"nextIndex": 0,
"userCourseList": course_list
}
return {"userId": data["userId"], "nextIndex": 0, "userCourseList": course_list}
def handle_get_user_portrait_api_request(self, data: Dict) -> Dict:
# No support for custom pfps
@ -514,96 +535,103 @@ class Mai2Base:
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_list = []
next_index = 0
for x in range(data["nextIndex"], data["maxCount"] + data["nextIndex"]):
try:
friend_season_ranking_list.append(
{
"mapId": friend_season_ranking_list[x]["map_id"],
"distance": friend_season_ranking_list[x]["distance"],
"isLock": friend_season_ranking_list[x]["is_lock"],
"isClear": friend_season_ranking_list[x]["is_clear"],
"isComplete": friend_season_ranking_list[x]["is_complete"],
if friend_season_ranking is None:
return {
"userId": data["userId"],
"nextIndex": 0,
"userFriendSeasonRankingList": [],
}
friend_season_ranking_list = []
next_idx = int(data["nextIndex"])
max_ct = int(data["maxCount"])
for x in range(next_idx, len(friend_season_ranking)):
tmp = friend_season_ranking[x]._asdict()
tmp.pop("user")
tmp.pop("id")
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
# We're capped and still have some left to go
if (
len(friend_season_ranking_list) == data["maxCount"]
and len(friend_season_ranking) > data["maxCount"] + data["nextIndex"]
):
next_index = data["maxCount"] + data["nextIndex"]
if len(friend_season_ranking) >= next_idx + max_ct:
next_idx += max_ct
else:
next_idx = 0
return {
"userId": data["userId"],
"nextIndex": next_index,
"nextIndex": next_idx,
"userFriendSeasonRankingList": friend_season_ranking_list,
}
def handle_get_user_map_api_request(self, data: Dict) -> Dict:
maps = self.data.item.get_maps(data["userId"])
map_list = []
next_index = 0
for x in range(data["nextIndex"], data["maxCount"] + data["nextIndex"]):
try:
map_list.append(
{
"mapId": maps[x]["map_id"],
"distance": maps[x]["distance"],
"isLock": maps[x]["is_lock"],
"isClear": maps[x]["is_clear"],
"isComplete": maps[x]["is_complete"],
if maps is None:
return {
"userId": data["userId"],
"nextIndex": 0,
"userMapList": [],
}
)
except:
map_list = []
next_idx = int(data["nextIndex"])
max_ct = int(data["maxCount"])
for x in range(next_idx, len(maps)):
tmp = maps[x]._asdict()
tmp.pop("user")
tmp.pop("id")
map_list.append(tmp)
if len(map_list) >= max_ct:
break
# We're capped and still have some left to go
if (
len(map_list) == data["maxCount"]
and len(maps) > data["maxCount"] + data["nextIndex"]
):
next_index = data["maxCount"] + data["nextIndex"]
if len(maps) >= next_idx + max_ct:
next_idx += max_ct
else:
next_idx = 0
return {
"userId": data["userId"],
"nextIndex": next_index,
"nextIndex": next_idx,
"userMapList": map_list,
}
def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict:
login_bonuses = self.data.item.get_login_bonuses(data["userId"])
login_bonus_list = []
next_index = 0
for x in range(data["nextIndex"], data["maxCount"] + data["nextIndex"]):
try:
login_bonus_list.append(
{
"bonusId": login_bonuses[x]["bonus_id"],
"point": login_bonuses[x]["point"],
"isCurrent": login_bonuses[x]["is_current"],
"isComplete": login_bonuses[x]["is_complete"],
if login_bonuses is None:
return {
"userId": data["userId"],
"nextIndex": 0,
"userLoginBonusList": [],
}
)
except:
login_bonus_list = []
next_idx = int(data["nextIndex"])
max_ct = int(data["maxCount"])
for x in range(next_idx, len(login_bonuses)):
tmp = login_bonuses[x]._asdict()
tmp.pop("user")
tmp.pop("id")
login_bonus_list.append(tmp)
if len(login_bonus_list) >= max_ct:
break
# We're capped and still have some left to go
if (
len(login_bonus_list) == data["maxCount"]
and len(login_bonuses) > data["maxCount"] + data["nextIndex"]
):
next_index = data["maxCount"] + data["nextIndex"]
if len(login_bonuses) >= next_idx + max_ct:
next_idx += max_ct
else:
next_idx = 0
return {
"userId": data["userId"],
"nextIndex": next_index,
"nextIndex": next_idx,
"userLoginBonusList": login_bonus_list,
}
@ -629,5 +657,5 @@ class Mai2Base:
return {
"userId": data["userId"],
"nextIndex": next_index,
"userMusicList": [{"userMusicDetailList": music_detail_list}]
"userMusicList": [{"userMusicDetailList": music_detail_list}],
}

View File

@ -30,14 +30,16 @@ class Mai2Constants:
VER_MAIMAI_DX_SPLASH_PLUS = 3
VER_MAIMAI_DX_UNIVERSE = 4
VER_MAIMAI_DX_UNIVERSE_PLUS = 5
VER_MAIMAI_DX_FESTIVAL = 6
VERSION_STRING = (
"maimai Delux",
"maimai Delux PLUS",
"maimai Delux Splash",
"maimai Delux Splash PLUS",
"maimai Delux Universe",
"maimai Delux Universe PLUS",
"maimai DX",
"maimai DX PLUS",
"maimai DX Splash",
"maimai DX Splash PLUS",
"maimai DX Universe",
"maimai DX Universe PLUS",
"maimai DX Festival",
)
@classmethod

31
titles/mai2/festival.py Normal file
View 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": []}

View File

@ -10,6 +10,7 @@ from os import path
from typing import Tuple
from core.config import CoreConfig
from core.utils import Utils
from titles.mai2.config import Mai2Config
from titles.mai2.const import Mai2Constants
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.universe import Mai2Universe
from titles.mai2.universeplus import Mai2UniversePlus
from titles.mai2.festival import Mai2Festival
class Mai2Servlet:
@ -30,12 +32,13 @@ class Mai2Servlet:
)
self.versions = [
Mai2Base(core_cfg, self.game_cfg),
Mai2Plus(core_cfg, self.game_cfg),
Mai2Splash(core_cfg, self.game_cfg),
Mai2SplashPlus(core_cfg, self.game_cfg),
Mai2Universe(core_cfg, self.game_cfg),
Mai2UniversePlus(core_cfg, self.game_cfg),
Mai2Base,
Mai2Plus,
Mai2Splash,
Mai2SplashPlus,
Mai2Universe,
Mai2UniversePlus,
Mai2Festival,
]
self.logger = logging.getLogger("mai2")
@ -97,6 +100,7 @@ class Mai2Servlet:
url_split = url_path.split("/")
internal_ver = 0
endpoint = url_split[len(url_split) - 1]
client_ip = Utils.get_ip_addr(request)
if version < 105: # 1.0
internal_ver = Mai2Constants.VER_MAIMAI_DX
@ -108,8 +112,10 @@ class Mai2Servlet:
internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS
elif version >= 120 and version < 125: # 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
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 we get a 32 character long hex string, it's a hash and we're
@ -128,16 +134,19 @@ class Mai2Servlet:
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"
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}")
return zlib.compress(b'{"returnCode": 1}')
resp = {"returnCode": 1}
else:
try:
handler = getattr(self.versions[internal_ver], func_to_find)
handler = getattr(handler_cls, func_to_find)
resp = handler(req_data)
except Exception as e:
@ -147,6 +156,6 @@ class Mai2Servlet:
if resp == None:
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"))

View File

@ -29,7 +29,7 @@ class Mai2Reader(BaseReader):
f"Start importer for {Mai2Constants.game_ver_to_string(version)}"
)
except IndexError:
self.logger.error(f"Invalid maidx version {version}")
self.logger.error(f"Invalid maimai DX version {version}")
exit(1)
def read(self) -> None:
@ -43,6 +43,7 @@ class Mai2Reader(BaseReader):
for dir in data_dirs:
self.logger.info(f"Read from {dir}")
self.get_events(f"{dir}/event")
self.disable_events(f"{dir}/information", f"{dir}/scoreRanking")
self.read_music(f"{dir}/music")
self.read_tickets(f"{dir}/ticket")
@ -64,6 +65,64 @@ class Mai2Reader(BaseReader):
)
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:
self.logger.info(f"Reading music from {base_dir}...")

View File

@ -71,12 +71,12 @@ map = Table(
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("map_id", Integer, nullable=False),
Column("mapId", Integer, nullable=False),
Column("distance", Integer, nullable=False),
Column("is_lock", Boolean, nullable=False, server_default="0"),
Column("is_clear", Boolean, nullable=False, server_default="0"),
Column("is_complete", Boolean, nullable=False, server_default="0"),
UniqueConstraint("user", "map_id", name="mai2_item_map_uk"),
Column("isLock", Boolean, nullable=False, server_default="0"),
Column("isClear", Boolean, nullable=False, server_default="0"),
Column("isComplete", Boolean, nullable=False, server_default="0"),
UniqueConstraint("user", "mapId", name="mai2_item_map_uk"),
mysql_charset="utf8mb4",
)
@ -89,11 +89,11 @@ login_bonus = Table(
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("bonus_id", Integer, nullable=False),
Column("bonusId", Integer, nullable=False),
Column("point", Integer, nullable=False),
Column("is_current", Boolean, nullable=False, server_default="0"),
Column("is_complete", Boolean, nullable=False, server_default="0"),
UniqueConstraint("user", "bonus_id", name="mai2_item_login_bonus_uk"),
Column("isCurrent", Boolean, nullable=False, server_default="0"),
Column("isComplete", Boolean, nullable=False, server_default="0"),
UniqueConstraint("user", "bonusId", name="mai2_item_login_bonus_uk"),
mysql_charset="utf8mb4",
)
@ -106,13 +106,15 @@ friend_season_ranking = Table(
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("season_id", Integer, nullable=False),
Column("seasonId", Integer, nullable=False),
Column("point", Integer, nullable=False),
Column("rank", Integer, nullable=False),
Column("reward_get", Boolean, nullable=False),
Column("user_name", String(8), nullable=False),
Column("record_date", String(255), nullable=False),
UniqueConstraint("user", "season_id", "user_name", name="mai2_item_login_bonus_uk"),
Column("rewardGet", Boolean, nullable=False),
Column("userName", String(8), nullable=False),
Column("recordDate", TIMESTAMP, nullable=False),
UniqueConstraint(
"user", "seasonId", "userName", name="mai2_item_friend_season_ranking_uk"
),
mysql_charset="utf8mb4",
)
@ -244,16 +246,16 @@ class Mai2ItemData(BaseData):
) -> None:
sql = insert(login_bonus).values(
user=user_id,
bonus_id=bonus_id,
bonusId=bonus_id,
point=point,
is_current=is_current,
is_complete=is_complete,
isCurrent=is_current,
isComplete=is_complete,
)
conflict = sql.on_duplicate_key_update(
point=point,
is_current=is_current,
is_complete=is_complete,
isCurrent=is_current,
isComplete=is_complete,
)
result = self.execute(conflict)
@ -293,18 +295,18 @@ class Mai2ItemData(BaseData):
) -> None:
sql = insert(map).values(
user=user_id,
map_id=map_id,
mapId=map_id,
distance=distance,
is_lock=is_lock,
is_clear=is_clear,
is_complete=is_complete,
isLock=is_lock,
isClear=is_clear,
isComplete=is_complete,
)
conflict = sql.on_duplicate_key_update(
distance=distance,
is_lock=is_lock,
is_clear=is_clear,
is_complete=is_complete,
isLock=is_lock,
isClear=is_clear,
isComplete=is_complete,
)
result = self.execute(conflict)
@ -324,7 +326,7 @@ class Mai2ItemData(BaseData):
return result.fetchall()
def get_map(self, user_id: int, map_id: int) -> Optional[Row]:
sql = map.select(and_(map.c.user == user_id, map.c.map_id == map_id))
sql = map.select(and_(map.c.user == user_id, map.c.mapId == map_id))
result = self.execute(sql)
if result is None:
@ -341,16 +343,16 @@ class Mai2ItemData(BaseData):
) -> None:
sql = insert(character).values(
user=user_id,
character_id=character_id,
characterId=character_id,
level=level,
awakening=awakening,
use_count=use_count,
useCount=use_count,
)
conflict = sql.on_duplicate_key_update(
level=level,
awakening=awakening,
use_count=use_count,
useCount=use_count,
)
result = self.execute(conflict)
@ -385,7 +387,25 @@ class Mai2ItemData(BaseData):
result = self.execute(sql)
if result is 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(
self, user_id: int, kind: int, item_id_list: List[int]
@ -438,7 +458,7 @@ class Mai2ItemData(BaseData):
result = self.execute(conflict)
if result is None:
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 result.lastrowid

View File

@ -158,6 +158,7 @@ extend = Table(
Column("sortMusicSetting", Integer),
Column("selectedCardList", JSON),
Column("encountMapNpcList", JSON),
Column("playStatusSetting", Integer, server_default="0"),
UniqueConstraint("user", "version", name="mai2_profile_extend_uk"),
mysql_charset="utf8mb4",
)
@ -178,6 +179,7 @@ option = Table(
Column("slideSpeed", Integer),
Column("touchSpeed", Integer),
Column("tapDesign", Integer),
Column("tapSe", Integer, server_default="0"),
Column("holdDesign", Integer),
Column("slideDesign", Integer),
Column("starType", Integer),
@ -297,8 +299,10 @@ class Mai2ProfileData(BaseData):
return result.lastrowid
def get_profile_detail(self, user_id: int, version: int) -> Optional[Row]:
sql = select(detail).where(
and_(detail.c.user == user_id, detail.c.version == version)
sql = (
select(detail)
.where(and_(detail.c.user == user_id, detail.c.version <= version))
.order_by(detail.c.version.desc())
)
result = self.execute(sql)
@ -322,8 +326,10 @@ class Mai2ProfileData(BaseData):
return result.lastrowid
def get_profile_ghost(self, user_id: int, version: int) -> Optional[Row]:
sql = select(ghost).where(
and_(ghost.c.user == user_id, ghost.c.version_int == version)
sql = (
select(ghost)
.where(and_(ghost.c.user == user_id, ghost.c.version_int <= version))
.order_by(ghost.c.version.desc())
)
result = self.execute(sql)
@ -347,8 +353,10 @@ class Mai2ProfileData(BaseData):
return result.lastrowid
def get_profile_extend(self, user_id: int, version: int) -> Optional[Row]:
sql = select(extend).where(
and_(extend.c.user == user_id, extend.c.version == version)
sql = (
select(extend)
.where(and_(extend.c.user == user_id, extend.c.version <= version))
.order_by(extend.c.version.desc())
)
result = self.execute(sql)
@ -372,8 +380,10 @@ class Mai2ProfileData(BaseData):
return result.lastrowid
def get_profile_option(self, user_id: int, version: int) -> Optional[Row]:
sql = select(option).where(
and_(option.c.user == user_id, option.c.version == version)
sql = (
select(option)
.where(and_(option.c.user == user_id, option.c.version <= version))
.order_by(option.c.version.desc())
)
result = self.execute(sql)
@ -397,8 +407,10 @@ class Mai2ProfileData(BaseData):
return result.lastrowid
def get_profile_rating(self, user_id: int, version: int) -> Optional[Row]:
sql = select(rating).where(
and_(rating.c.user == user_id, rating.c.version == version)
sql = (
select(rating)
.where(and_(rating.c.user == user_id, rating.c.version <= version))
.order_by(rating.c.version.desc())
)
result = self.execute(sql)

View File

@ -25,6 +25,7 @@ best_score = Table(
Column("syncStatus", Integer),
Column("deluxscoreMax", Integer),
Column("scoreRank", Integer),
Column("extNum1", Integer, server_default="0"),
UniqueConstraint("user", "musicId", "level", name="mai2_score_best_uk"),
mysql_charset="utf8mb4",
)
@ -143,6 +144,7 @@ playlog = Table(
Column("isNewFree", Boolean),
Column("extNum1", Integer),
Column("extNum2", Integer),
Column("extNum4", Integer, server_default="0"),
Column("trialPlayAchievement", Integer),
mysql_charset="utf8mb4",
)

View File

@ -16,6 +16,7 @@ event = Table(
Column("eventId", Integer),
Column("type", Integer),
Column("name", String(255)),
Column("startDate", TIMESTAMP, server_default=func.now()),
Column("enabled", Boolean, server_default="1"),
UniqueConstraint("version", "eventId", "type", name="mai2_static_event_uk"),
mysql_charset="utf8mb4",
@ -108,17 +109,17 @@ class Mai2StaticData(BaseData):
return None
return result.fetchall()
def toggle_game_events(
def toggle_game_event(
self, version: int, event_id: int, toggle: bool
) -> Optional[List]:
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))
result = self.execute(sql)
if result is None:
self.logger.warning(
f"toggle_game_events: Failed to update event! event_id {event_id} toggle {toggle}"
f"toggle_game_event: Failed to update event! event_id {event_id} toggle {toggle}"
)
return result.last_updated_params()

View File

@ -1,7 +1,4 @@
from typing import Any, List, Dict
from datetime import datetime, timedelta
import pytz
import json
from typing import Dict
from core.config import CoreConfig
from titles.mai2.universe import Mai2Universe

View File

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

View File

@ -452,8 +452,7 @@ class OngekiBase:
tmp.pop("id")
items.append(tmp)
xout = kind * 10000000000 + \
(data["nextIndex"] % 10000000000) + len(items)
xout = kind * 10000000000 + (data["nextIndex"] % 10000000000) + len(items)
if len(items) < data["maxCount"] or data["maxCount"] == 0:
nextIndex = 0
@ -852,8 +851,7 @@ class OngekiBase:
)
if "userOption" in upsert and len(upsert["userOption"]) > 0:
self.data.profile.put_profile_options(
user_id, upsert["userOption"][0])
self.data.profile.put_profile_options(user_id, upsert["userOption"][0])
if "userPlaylogList" in upsert:
for playlog in upsert["userPlaylogList"]:

View File

@ -97,7 +97,7 @@ class OngekiBright(OngekiBase):
"userId": data["userId"],
"length": 0,
"nextIndex": 0,
"userCharacterList": []
"userCharacterList": [],
}
max_ct = data["maxCount"]
@ -548,7 +548,7 @@ class OngekiBright(OngekiBase):
"returnCode": 1,
"orderId": 0,
"serialId": "11111111111111111111",
"apiName": "CMUpsertUserPrintPlaylogApi"
"apiName": "CMUpsertUserPrintPlaylogApi",
}
def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict:
@ -556,7 +556,7 @@ class OngekiBright(OngekiBase):
"returnCode": 1,
"orderId": 0,
"serialId": "11111111111111111111",
"apiName": "CMUpsertUserPrintlogApi"
"apiName": "CMUpsertUserPrintlogApi",
}
def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict:

View File

@ -705,9 +705,7 @@ class OngekiItemData(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:

View File

@ -1,8 +1,10 @@
from titles.pokken.index import PokkenServlet
from titles.pokken.const import PokkenConstants
from titles.pokken.database import PokkenData
from .index import PokkenServlet
from .const import PokkenConstants
from .database import PokkenData
from .frontend import PokkenFrontend
index = PokkenServlet
database = PokkenData
game_codes = [PokkenConstants.GAME_CODE]
current_schema_version = 1
frontend = PokkenFrontend

View File

@ -17,6 +17,7 @@ class PokkenBase:
self.version = 0
self.logger = logging.getLogger("pokken")
self.data = PokkenData(core_cfg)
self.SUPPORT_SET_NONE = 4294967295
def handle_noop(self, request: Any) -> bytes:
res = jackal_pb2.Response()
@ -73,27 +74,21 @@ class PokkenBase:
return res.SerializeToString()
def handle_save_client_log(
self, request: jackal_pb2.Request
) -> bytes:
def handle_save_client_log(self, request: jackal_pb2.Request) -> bytes:
res = jackal_pb2.Response()
res.result = 1
res.type = jackal_pb2.MessageType.SAVE_CLIENT_LOG
return res.SerializeToString()
def handle_check_diagnosis(
self, request: jackal_pb2.Request
) -> bytes:
def handle_check_diagnosis(self, request: jackal_pb2.Request) -> bytes:
res = jackal_pb2.Response()
res.result = 1
res.type = jackal_pb2.MessageType.CHECK_DIAGNOSIS
return res.SerializeToString()
def handle_load_client_settings(
self, request: jackal_pb2.Request
) -> bytes:
def handle_load_client_settings(self, request: jackal_pb2.Request) -> bytes:
res = jackal_pb2.Response()
res.result = 1
res.type = jackal_pb2.MessageType.LOAD_CLIENT_SETTINGS
@ -141,7 +136,9 @@ class PokkenBase:
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})")
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!")
@ -172,7 +169,7 @@ class PokkenBase:
if profile is None:
profile_id = self.data.profile.create_profile(user_id)
profile_dict = {'id': profile_id, 'user': user_id}
profile_dict = {"id": profile_id, "user": user_id}
pokemon_data = []
tutorial_progress = []
rankmatch_progress = []
@ -183,7 +180,9 @@ class PokkenBase:
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', '')})")
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 = []
@ -192,72 +191,74 @@ class PokkenBase:
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.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.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)
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()
@ -281,13 +282,25 @@ class PokkenBase:
res.type = jackal_pb2.MessageType.SAVE_INGAME_LOG
return res.SerializeToString()
def handle_matching_noop(self, data: Dict = {}, client_ip: str = "127.0.0.1") -> Dict:
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 {}
def handle_matching_start_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:
def handle_matching_is_matching(
self, data: Dict = {}, client_ip: str = "127.0.0.1"
) -> Dict:
"""
"sessionId":"12345678",
"A":{
@ -298,5 +311,7 @@ class PokkenBase:
"""
return {}
def handle_matching_stop_matching(self, data: Dict = {}, client_ip: str = "127.0.0.1") -> Dict:
def handle_matching_stop_matching(
self, data: Dict = {}, client_ip: str = "127.0.0.1"
) -> Dict:
return {}

View File

@ -59,6 +59,7 @@ class PokkenServerConfig:
self.__config, "pokken", "server", "auto_register", default=True
)
class PokkenConfig(dict):
def __init__(self) -> None:
self.server = PokkenServerConfig(self)

View File

@ -1,3 +1,6 @@
from enum import Enum
class PokkenConstants:
GAME_CODE = "SDAK"
@ -7,6 +10,16 @@ class PokkenConstants:
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
def game_ver_to_string(cls, ver: int):
return cls.VERSION_NAMES[ver]

View File

@ -3,6 +3,7 @@ from core.config import CoreConfig
from .schema import *
class PokkenData(Data):
def __init__(self, cfg: CoreConfig) -> None:
super().__init__(cfg)

33
titles/pokken/frontend.py Normal file
View 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")

View File

@ -0,0 +1,4 @@
{% extends "core/frontend/index.jinja" %}
{% block content %}
<h1>Pokken</h1>
{% endblock content %}

View File

@ -87,7 +87,7 @@ class PokkenServlet(resource.Resource):
if not game_cfg.server.enable:
return (False, "")
return (True, "PKF2")
return (True, "PKF1")
def setup(self) -> None:
# TODO: Setup stun, turn (UDP) and admission (WSS) servers
@ -115,13 +115,14 @@ class PokkenServlet(resource.Resource):
pokken_request.type
].name.lower()
self.logger.debug(pokken_request)
handler = getattr(self.base, f"handle_{endpoint}", None)
if handler is None:
self.logger.warn(f"No handler found for message type {endpoint}")
return self.base.handle_noop(pokken_request)
self.logger.info(f"{endpoint} request from {Utils.get_ip_addr(request)}")
self.logger.debug(pokken_request)
ret = handler(pokken_request)
return ret
@ -134,13 +135,24 @@ class PokkenServlet(resource.Resource):
self.logger.info("Empty matching request")
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.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:
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()
ret = handler(json_content, client_ip)

View File

@ -9,19 +9,26 @@ from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata
item = Table(
'pokken_item',
"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'),
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

View File

@ -10,28 +10,35 @@ from core.data.schema import BaseData, metadata
# Pokken sends depressingly little match data...
match_data = Table(
'pokken_match_data',
"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('play_mode', Integer),
Column('result', Integer),
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),
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

View File

@ -7,158 +7,135 @@ from sqlalchemy.engine import Row
from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata
from ..const import PokkenConstants
# Some more of the repeated fields could probably be their own tables, for now I just did the ones that made sense to me
# Having the profile table be this massive kinda blows for updates but w/e, **kwargs to the rescue
profile = Table(
'pokken_profile',
"pokken_profile",
metadata,
Column('id', Integer, primary_key=True, nullable=False),
Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, unique=True),
Column('trainer_name', String(16)), # optional
Column('home_region_code', Integer),
Column('home_loc_name', String(255)),
Column('pref_code', Integer),
Column('navi_newbie_flag', Boolean),
Column('navi_enable_flag', Boolean),
Column('pad_vibrate_flag', Boolean),
Column('trainer_rank_point', Integer),
Column('wallet', Integer),
Column('fight_money', Integer),
Column('score_point', Integer),
Column('grade_max_num', Integer),
Column('extra_counter', Integer), # Optional
Column('total_play_days', Integer),
Column('play_date_time', Integer),
Column('lucky_box_fail_num', Integer),
Column('event_reward_get_flag', Integer),
Column('rank_pvp_all', Integer),
Column('rank_pvp_loc', Integer),
Column('rank_cpu_all', Integer),
Column('rank_cpu_loc', Integer),
Column('rank_event', Integer),
Column('awake_num', Integer),
Column('use_support_num', Integer),
Column('rankmatch_flag', Integer),
Column('rankmatch_max', Integer), # Optional
Column('rankmatch_success', Integer), # Optional
Column('beat_num', Integer), # Optional
Column('title_text_id', Integer),
Column('title_plate_id', Integer),
Column('title_decoration_id', Integer),
Column('support_pokemon_list', JSON), # Repeated, Integer
Column('support_set_1', JSON), # Repeated, Integer
Column('support_set_2', JSON), # Repeated, Integer
Column('support_set_3', JSON), # Repeated, Integer
Column('navi_trainer', Integer),
Column('navi_version_id', Integer),
Column('aid_skill_list', JSON), # Repeated, Integer
Column('aid_skill', Integer),
Column('comment_text_id', Integer),
Column('comment_word_id', Integer),
Column('latest_use_pokemon', Integer),
Column('ex_ko_num', Integer),
Column('wko_num', Integer),
Column('timeup_win_num', Integer),
Column('cool_ko_num', Integer),
Column('cool_ko_num', Integer),
Column('perfect_ko_num', Integer),
Column('record_flag', Integer),
Column('continue_num', Integer),
Column('avatar_body', Integer), # Optional
Column('avatar_gender', Integer), # Optional
Column('avatar_background', Integer), # Optional
Column('avatar_head', Integer), # Optional
Column('avatar_battleglass', Integer), # Optional
Column('avatar_face0', Integer), # Optional
Column('avatar_face1', Integer), # Optional
Column('avatar_face2', Integer), # Optional
Column('avatar_bodyall', Integer), # Optional
Column('avatar_wear', Integer), # Optional
Column('avatar_accessory', Integer), # Optional
Column('avatar_stamp', Integer), # Optional
Column('event_state', Integer),
Column('event_id', Integer),
Column('sp_bonus_category_id_1', Integer),
Column('sp_bonus_key_value_1', Integer),
Column('sp_bonus_category_id_2', Integer),
Column('sp_bonus_key_value_2', Integer),
Column('last_play_event_id', Integer), # Optional
mysql_charset="utf8mb4",
)
tutorial_progress = Table(
'pokken_tutorial_progress',
metadata,
Column('id', Integer, primary_key=True, nullable=False),
Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column('flag', Integer),
UniqueConstraint('user', 'flag', name='pokken_tutorial_progress_uk'),
mysql_charset="utf8mb4",
)
rankmatch_progress = Table(
'pokken_rankmatch_progress',
metadata,
Column('id', Integer, primary_key=True, nullable=False),
Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column('progress', Integer),
UniqueConstraint('user', 'progress', name='pokken_rankmatch_progress_uk'),
mysql_charset="utf8mb4",
)
achievement_flag = Table(
'pokken_achievement_flag',
metadata,
Column('id', Integer, primary_key=True, nullable=False),
Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column('flag', Integer),
UniqueConstraint('user', 'flag', name='pokken_achievement_flag_uk'),
mysql_charset="utf8mb4",
)
event_achievement_flag = Table(
'pokken_event_achievement_flag',
metadata,
Column('id', Integer, primary_key=True, nullable=False),
Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column('flag', Integer),
UniqueConstraint('user', 'flag', name='pokken_event_achievement_flag_uk'),
mysql_charset="utf8mb4",
)
event_achievement_param = Table(
'pokken_event_achievement_param',
metadata,
Column('id', Integer, primary_key=True, nullable=False),
Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column('param', Integer),
UniqueConstraint('user', 'param', name='pokken_event_achievement_param_uk'),
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
unique=True,
),
Column("trainer_name", String(16)), # optional
Column("home_region_code", Integer),
Column("home_loc_name", String(255)),
Column("pref_code", Integer),
Column("navi_newbie_flag", Boolean),
Column("navi_enable_flag", Boolean),
Column("pad_vibrate_flag", Boolean),
Column("trainer_rank_point", Integer),
Column("wallet", Integer),
Column("fight_money", Integer),
Column("score_point", Integer),
Column("grade_max_num", Integer),
Column("extra_counter", Integer), # Optional
Column("tutorial_progress_flag", JSON), # Repeated, Integer
Column("total_play_days", Integer),
Column("play_date_time", Integer),
Column("achievement_flag", JSON), # Repeated, Integer
Column("lucky_box_fail_num", Integer),
Column("event_reward_get_flag", Integer),
Column("rank_pvp_all", Integer),
Column("rank_pvp_loc", Integer),
Column("rank_cpu_all", Integer),
Column("rank_cpu_loc", Integer),
Column("rank_event", Integer),
Column("awake_num", Integer),
Column("use_support_num", Integer),
Column("rankmatch_flag", Integer),
Column("rankmatch_max", Integer), # Optional
Column("rankmatch_progress", JSON), # Repeated, Integer
Column("rankmatch_success", Integer), # Optional
Column("beat_num", Integer), # Optional
Column("title_text_id", Integer),
Column("title_plate_id", Integer),
Column("title_decoration_id", Integer),
Column("support_pokemon_list", JSON), # Repeated, Integer
Column("support_set_1_1", Integer), # Repeated, Integer
Column("support_set_1_2", Integer),
Column("support_set_2_1", Integer), # Repeated, Integer
Column("support_set_2_2", Integer),
Column("support_set_3_1", Integer), # Repeated, Integer
Column("support_set_3_2", Integer),
Column("navi_trainer", Integer),
Column("navi_version_id", Integer),
Column("aid_skill_list", JSON), # Repeated, Integer
Column("aid_skill", Integer),
Column("comment_text_id", Integer),
Column("comment_word_id", Integer),
Column("latest_use_pokemon", Integer),
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("record_flag", Integer),
Column("continue_num", Integer),
Column("avatar_body", Integer), # Optional
Column("avatar_gender", Integer), # Optional
Column("avatar_background", Integer), # Optional
Column("avatar_head", Integer), # Optional
Column("avatar_battleglass", Integer), # Optional
Column("avatar_face0", Integer), # Optional
Column("avatar_face1", Integer), # Optional
Column("avatar_face2", Integer), # Optional
Column("avatar_bodyall", Integer), # Optional
Column("avatar_wear", Integer), # Optional
Column("avatar_accessory", Integer), # Optional
Column("avatar_stamp", Integer), # Optional
Column("event_state", Integer),
Column("event_id", Integer),
Column("sp_bonus_category_id_1", Integer),
Column("sp_bonus_key_value_1", Integer),
Column("sp_bonus_category_id_2", Integer),
Column("sp_bonus_key_value_2", Integer),
Column("last_play_event_id", Integer), # Optional
Column("event_achievement_flag", JSON), # Repeated, Integer
Column("event_achievement_param", JSON), # Repeated, Integer
Column("battle_num_vs_wan", Integer), # 4?
Column("win_vs_wan", Integer),
Column("battle_num_vs_lan", Integer), # 3?
Column("win_vs_lan", Integer),
Column("battle_num_vs_cpu", Integer), # 2
Column("win_cpu", Integer),
Column("battle_num_tutorial", Integer), # 1?
mysql_charset="utf8mb4",
)
pokemon_data = Table(
'pokken_pokemon_data',
"pokken_pokemon_data",
metadata,
Column('id', Integer, primary_key=True, nullable=False),
Column('user', ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column('char_id', Integer, nullable=False),
Column('illustration_book_no', Integer, nullable=False),
Column('pokemon_exp', Integer, nullable=False),
Column('battle_num_vs_wan', Integer, nullable=False),
Column('win_vs_wan', Integer, nullable=False),
Column('battle_num_vs_lan', Integer, nullable=False),
Column('win_vs_lan', Integer, nullable=False),
Column('battle_num_vs_cpu', Integer, nullable=False),
Column('win_cpu', Integer, nullable=False),
Column('battle_all_num_tutorial', Integer, nullable=False),
Column('battle_num_tutorial', Integer, nullable=False),
Column('bp_point_atk', Integer, nullable=False),
Column('bp_point_res', Integer, nullable=False),
Column('bp_point_def', Integer, nullable=False),
Column('bp_point_sp', Integer, nullable=False),
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("char_id", Integer, nullable=False),
Column("illustration_book_no", Integer),
Column("pokemon_exp", Integer),
Column("battle_num_vs_wan", Integer), # 4?
Column("win_vs_wan", Integer),
Column("battle_num_vs_lan", Integer), # 3?
Column("win_vs_lan", Integer),
Column("battle_num_vs_cpu", Integer), # 2
Column("win_cpu", Integer),
Column("battle_all_num_tutorial", Integer),
Column("battle_num_tutorial", Integer), # 1?
Column("bp_point_atk", Integer),
Column("bp_point_res", Integer),
Column("bp_point_def", Integer),
Column("bp_point_sp", Integer),
UniqueConstraint("user", "char_id", name="pokken_pokemon_data_uk"),
mysql_charset="utf8mb4",
)
class PokkenProfileData(BaseData):
def create_profile(self, user_id: int) -> Optional[int]:
sql = insert(profile).values(user=user_id)
@ -171,24 +148,43 @@ class PokkenProfileData(BaseData):
return result.lastrowid
def set_profile_name(self, user_id: int, new_name: str) -> None:
sql = update(profile).where(profile.c.user == user_id).values(trainer_name = new_name)
sql = (
update(profile)
.where(profile.c.user == user_id)
.values(trainer_name=new_name)
)
result = self.execute(sql)
if result is None:
self.logger.error(f"Failed to update pokken profile name for user {user_id}!")
self.logger.error(
f"Failed to update pokken profile name for user {user_id}!"
)
def update_profile(self, user_id: int, profile_data: Dict) -> None:
"""
TODO: Find out what comes in on the SaveUserRequestData protobuf and save it!
"""
def update_profile_tutorial_flags(self, user_id: int, tutorial_flags: Dict) -> None:
pass
def add_profile_points(
self, user_id: int, rank_pts: int, money: int, score_pts: int
) -> None:
pass
def get_profile(self, user_id: int) -> Optional[Row]:
sql = profile.select(profile.c.user == user_id)
result = self.execute(sql)
if result is None: return None
if result is None:
return None
return result.fetchone()
def put_pokemon_data(self, user_id: int, pokemon_data: Dict) -> Optional[int]:
def put_pokemon_data(
self,
user_id: int,
pokemon_id: int,
illust_no: int,
get_exp: int,
atk: int,
res: int,
defe: int,
sp: int,
) -> Optional[int]:
pass
def get_pokemon_data(self, user_id: int, pokemon_id: int) -> Optional[Row]:
@ -196,3 +192,26 @@ class PokkenProfileData(BaseData):
def get_all_pokemon_data(self, user_id: int) -> Optional[List[Row]]:
pass
def put_results(
self, user_id: int, pokemon_id: int, match_type: int, match_result: int
) -> None:
"""
Records the match stats (type and win/loss) for the pokemon and profile
"""
pass
def put_stats(
self,
user_id: int,
exkos: int,
wkos: int,
timeout_wins: int,
cool_kos: int,
perfects: int,
continues: int,
) -> None:
"""
Records profile stats
"""
pass

View File

@ -8,5 +8,6 @@ from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata
class PokkenStaticData(BaseData):
pass

View File

@ -9,4 +9,4 @@ database = WaccaData
reader = WaccaReader
frontend = WaccaFrontend
game_codes = [WaccaConstants.GAME_CODE]
current_schema_version = 3
current_schema_version = 5

View File

@ -183,8 +183,6 @@ class WaccaBase:
def handle_user_status_login_request(self, data: Dict) -> Dict:
req = UserStatusLoginRequest(data)
resp = UserStatusLoginResponseV1()
is_new_day = False
is_consec_day = False
is_consec_day = True
if req.userId == 0:
@ -202,29 +200,29 @@ class WaccaBase:
self.logger.info(f"User {req.userId} login on {req.chipId}")
last_login_time = int(profile["last_login_date"].timestamp())
resp.lastLoginDate = last_login_time
# If somebodies login timestamp < midnight of current day, then they are logging in for the first time today
if last_login_time < int(
midnight_today_ts = int(
datetime.now()
.replace(hour=0, minute=0, second=0, microsecond=0)
.timestamp()
):
is_new_day = True
is_consec_day = True
)
# If somebodies login timestamp > midnight of current day + 1 day, then they broke their daily login streak
elif last_login_time > int(
(
datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
+ timedelta(days=1)
).timestamp()
):
# If somebodies login timestamp < midnight of current day, then they are logging in for the first time today
if last_login_time < midnight_today_ts:
resp.firstLoginDaily = True
# If the difference between midnight today and their last login is greater then 1 day (86400 seconds) they've broken their streak
if midnight_today_ts - last_login_time > 86400:
is_consec_day = False
# else, they are simply logging in again on the same day, and we don't need to do anything for that
self.data.profile.session_login(req.userId, is_new_day, is_consec_day)
self.data.profile.session_login(
req.userId, resp.firstLoginDaily, is_consec_day
)
resp.firstLoginDaily = int(is_new_day)
if resp.firstLoginDaily:
# TODO: Daily bonus
pass
# TODO: VIP dialy/monthly rewards
return resp.make()
@ -275,9 +273,6 @@ class WaccaBase:
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["touch_effect"], 312001
) # Added reverse
self.data.item.put_item(
req.aimeId, WaccaConstants.ITEM_TYPES["touch_effect"], 312002
) # Added reverse
return UserStatusCreateResponseV2(profileId, req.username).make()
@ -635,16 +630,25 @@ class WaccaBase:
new_tickets.append([ticket["id"], ticket["ticket_id"], 9999999999])
for item in req.itemsUsed:
if item.itemType == WaccaConstants.ITEM_TYPES["wp"] and not self.game_config.mods.infinite_wp:
if (
item.itemType == WaccaConstants.ITEM_TYPES["wp"]
and not self.game_config.mods.infinite_wp
):
if current_wp >= item.quantity:
current_wp -= item.quantity
self.data.profile.spend_wp(req.profileId, item.quantity)
else:
return BaseResponse().make()
elif item.itemType == WaccaConstants.ITEM_TYPES["ticket"] and not self.game_config.mods.infinite_tickets:
elif (
item.itemType == WaccaConstants.ITEM_TYPES["ticket"]
and not self.game_config.mods.infinite_tickets
):
for x in range(len(new_tickets)):
if new_tickets[x][1] == item.itemId:
self.logger.debug(
f"Remove ticket ID {new_tickets[x][0]} type {new_tickets[x][1]} from {user_id}"
)
self.data.item.spend_ticket(new_tickets[x][0])
new_tickets.pop(x)
break
@ -668,13 +672,8 @@ class WaccaBase:
)
if self.game_config.mods.infinite_tickets:
new_tickets = [
[0, 106002, 0],
[1, 106002, 0],
[2, 106002, 0],
[3, 106002, 0],
[4, 106002, 0],
]
for x in range(5):
new_tickets.append(TicketItem(x, 106002, 0))
if self.game_config.mods.infinite_wp:
current_wp = 999999
@ -836,7 +835,7 @@ class WaccaBase:
resp.songDetail.grades = SongDetailGradeCountsV2(counts=grades)
else:
resp.songDetail.grades = SongDetailGradeCountsV1(counts=grades)
resp.songDetail.lock_state = 1
resp.songDetail.lockState = 1
return resp.make()
# TODO: Coop and vs data
@ -880,7 +879,10 @@ class WaccaBase:
user_id = profile["user"]
resp.currentWp = profile["wp"]
if req.purchaseType == PurchaseType.PurchaseTypeWP and not self.game_config.mods.infinite_wp:
if (
req.purchaseType == PurchaseType.PurchaseTypeWP
and not self.game_config.mods.infinite_wp
):
resp.currentWp -= req.cost
self.data.profile.spend_wp(req.profileId, req.cost)
@ -982,7 +984,7 @@ class WaccaBase:
user_id = self.data.profile.profile_to_aime_user(req.profileId)
for opt in req.optsUpdated:
self.data.profile.update_option(user_id, opt.opt_id, opt.opt_val)
self.data.profile.update_option(user_id, opt.optId, opt.optVal)
for update in req.datesUpdated:
pass
@ -1074,7 +1076,14 @@ class WaccaBase:
)
if not old_score:
self.data.score.put_best_score(
user_id, item.itemId, item.quantity, 0, [0] * 5, [0] * 13, 0, 0
user_id,
item.itemId,
item.quantity,
0,
[0] * 5,
[0] * 13,
0,
0,
)
if item.quantity == 0:

View File

@ -158,11 +158,11 @@ class Notice:
class UserOption:
def __init__(self, opt_id: int = 0, opt_val: Any = 0) -> None:
self.opt_id = opt_id
self.opt_val = opt_val
self.optId = opt_id
self.optVal = opt_val
def make(self) -> List:
return [self.opt_id, self.opt_val]
return [self.optId, self.optVal]
class UserStatusV1:
@ -348,13 +348,35 @@ class NavigatorItem(IconItem):
class SkillItem:
skill_type: int
skillType: int
level: int
flag: int
badge: int
def make(self) -> List:
return [self.skill_type, self.level, self.flag, self.badge]
return [self.skillType, self.level, self.flag, self.badge]
class UserEventInfo:
def __init__(self) -> None:
self.eventId = 0
self.conditionInfo: List[UserEventConditionInfo] = []
def make(self) -> List:
conditions = []
for x in self.conditionInfo:
conditions.append(x.make())
return [self.eventId, conditions]
class UserEventConditionInfo:
def __init__(self) -> None:
self.achievementCondition = 0
self.progress = 0
def make(self) -> List:
return [self.achievementCondition, self.progress]
class UserItemInfoV1:
@ -447,19 +469,19 @@ class UserItemInfoV3(UserItemInfoV2):
class SongDetailClearCounts:
def __init__(
self,
play_ct: int = 0,
clear_ct: int = 0,
ml_ct: int = 0,
fc_ct: int = 0,
am_ct: int = 0,
playCt: int = 0,
clearCt: int = 0,
mlCt: int = 0,
fcCt: int = 0,
amCt: int = 0,
counts: Optional[List[int]] = None,
) -> None:
if counts is None:
self.playCt = play_ct
self.clearCt = clear_ct
self.misslessCt = ml_ct
self.fullComboCt = fc_ct
self.allMarvelousCt = am_ct
self.playCt = playCt
self.clearCt = clearCt
self.misslessCt = mlCt
self.fullComboCt = fcCt
self.allMarvelousCt = amCt
else:
self.playCt = counts[0]
@ -773,8 +795,12 @@ class GateDetailV2(GateDetailV1):
class GachaInfo:
def __init__(self, gacha_id: int = 0, gacha_roll_ct: int = 0) -> None:
self.gachaId = gacha_id
self.rollCt = gacha_roll_ct
def make(self) -> List:
return []
return [self.gachaId, self.rollCt]
class LastSongDetail:
@ -808,9 +834,32 @@ class LastSongDetail:
]
class FriendDetail:
class FriendScoreDetail:
def __init__(
self, song_id: int = 0, difficulty: int = 1, best_score: int = 0
) -> None:
self.songId = song_id
self.difficulty = difficulty
self.bestScore = best_score
def make(self) -> List:
return []
return [self.songId, self.difficulty, self.bestScore]
class FriendDetail:
def __init__(self, user_id: int = 0, username: str = "") -> None:
self.friendId = user_id
self.friendUsername = username
self.friendUserType = 1
self.friendScoreDetail: List[FriendScoreDetail] = []
def make(self) -> List:
scores = []
for x in self.friendScoreDetail:
scores.append(x.make())
return [self.friendId, self.friendUsername, self.friendUserType, scores]
class LoginBonusInfo:
@ -894,6 +943,7 @@ class PlayType(Enum):
PlayTypeVs = 2
PlayTypeCoop = 3
PlayTypeStageup = 4
PlayTypeTimeFree = 5
class StageInfo:
@ -942,7 +992,7 @@ class MusicUpdateDetailV1:
self.score = 0
self.lowestMissCount = 0
self.maxSkillPts = 0
self.lock_state = 0
self.lockState = 0
def make(self) -> List:
return [
@ -954,7 +1004,7 @@ class MusicUpdateDetailV1:
self.score,
self.lowestMissCount,
self.maxSkillPts,
self.lock_state,
self.lockState,
]

View File

@ -10,10 +10,10 @@ class HousingGetResponse(BaseResponse):
def __init__(self, housingId: int) -> None:
super().__init__()
self.housingId: int = housingId
self.regionId: int = 0
self.isNewCab: bool = False
def make(self) -> Dict:
self.params = [self.housingId, self.regionId]
self.params = [self.housingId, int(self.isNewCab)]
return super().make()
@ -32,8 +32,6 @@ class HousingStartRequestV1(BaseRequest):
class HousingStartRequestV2(HousingStartRequestV1):
def __init__(self, data: Dict) -> None:
super(HousingStartRequestV1, self).__init__(data)
self.unknown0: str = self.params[0]
self.errorLog: str = self.params[1]
self.creditLog: str = self.params[2]
self.info: List[HousingInfo] = []

View File

@ -39,12 +39,16 @@ class UserStatusGetV2Response(UserStatusGetV1Response):
def __init__(self) -> None:
super().__init__()
self.userStatus: UserStatusV2 = UserStatusV2()
self.unknownArr: List = []
self.options: List[UserOption] = []
def make(self) -> Dict:
super().make()
opts = []
self.params.append(self.unknownArr)
for x in self.options:
opts.append(x.make())
self.params.append(opts)
return super(UserStatusGetV1Response, self).make()
@ -137,7 +141,7 @@ class UserStatusGetDetailResponseV2(UserStatusGetDetailResponseV1):
self.userItems: UserItemInfoV2 = UserItemInfoV2()
self.favorites: List[int] = []
self.stoppedSongIds: List[int] = []
self.eventInfo: List[int] = []
self.eventInfo: List[UserEventInfo] = []
self.gateInfo: List[GateDetailV1] = []
self.lastSongInfo: LastSongDetail = LastSongDetail()
self.gateTutorialFlags: List[GateTutorialFlag] = []
@ -149,6 +153,8 @@ class UserStatusGetDetailResponseV2(UserStatusGetDetailResponseV1):
gates = []
friends = []
tut_flg = []
evts = []
gacha = []
for x in self.gateInfo:
gates.append(x.make())
@ -163,13 +169,19 @@ class UserStatusGetDetailResponseV2(UserStatusGetDetailResponseV1):
flag_id = len(tut_flg) + 1
tut_flg.append([flag_id, 0])
for x in self.eventInfo:
evts.append(x.make())
for x in self.gatchaInfo:
gacha.append(x.make())
self.params.append(self.favorites)
self.params.append(self.stoppedSongIds)
self.params.append(self.eventInfo)
self.params.append(evts)
self.params.append(gates)
self.params.append(self.lastSongInfo.make())
self.params.append(tut_flg)
self.params.append(self.gatchaInfo)
self.params.append(gacha)
self.params.append(friends)
return super(UserStatusGetDetailResponseV1, self).make()
@ -255,7 +267,9 @@ class UserStatusLoginResponseV3(UserStatusLoginResponseV2):
self, is_first_login_daily: bool = False, last_login_date: int = 0
) -> None:
super().__init__(is_first_login_daily, last_login_date)
self.unk: List = []
self.unk: List = (
[]
) # Ticket info, item info, message, title, voice name (not sure how they fit...)
def make(self) -> Dict:
super().make()

View File

@ -74,6 +74,8 @@ class WaccaLily(WaccaS):
resp.profileStatus = ProfileStatus.ProfileRegister
return resp.make()
opts = self.data.profile.get_options(req.aimeId)
self.logger.info(f"User preview for {req.aimeId} from {req.chipId}")
if profile["last_game_ver"] is None:
resp.lastGameVersion = ShortVersion(str(req.appVersion))
@ -138,13 +140,14 @@ class WaccaLily(WaccaS):
if self.game_config.mods.infinite_wp:
resp.userStatus.wp = 999999
for opt in opts:
resp.options.append(UserOption(opt["opt_id"], opt["value"]))
return resp.make()
def handle_user_status_login_request(self, data: Dict) -> Dict:
req = UserStatusLoginRequest(data)
resp = UserStatusLoginResponseV2()
is_new_day = False
is_consec_day = False
is_consec_day = True
if req.userId == 0:
@ -162,34 +165,28 @@ class WaccaLily(WaccaS):
self.logger.info(f"User {req.userId} login on {req.chipId}")
last_login_time = int(profile["last_login_date"].timestamp())
resp.lastLoginDate = last_login_time
# If somebodies login timestamp < midnight of current day, then they are logging in for the first time today
if last_login_time < int(
midnight_today_ts = int(
datetime.now()
.replace(hour=0, minute=0, second=0, microsecond=0)
.timestamp()
):
is_new_day = True
is_consec_day = True
)
# If somebodies login timestamp > midnight of current day + 1 day, then they broke their daily login streak
elif last_login_time > int(
(
datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
+ timedelta(days=1)
).timestamp()
):
# If somebodies login timestamp < midnight of current day, then they are logging in for the first time today
if last_login_time < midnight_today_ts:
resp.firstLoginDaily = True
# If the difference between midnight today and their last login is greater then 1 day (86400 seconds) they've broken their streak
if midnight_today_ts - last_login_time > 86400:
is_consec_day = False
# else, they are simply logging in again on the same day, and we don't need to do anything for that
self.data.profile.session_login(req.userId, is_new_day, is_consec_day)
self.data.profile.session_login(
req.userId, resp.firstLoginDaily, is_consec_day
)
resp.vipInfo.pageYear = datetime.now().year
resp.vipInfo.pageMonth = datetime.now().month
resp.vipInfo.pageDay = datetime.now().day
resp.vipInfo.numItem = 1
resp.firstLoginDaily = int(is_new_day)
return resp.make()
def handle_user_status_getDetail_request(self, data: Dict) -> Dict:

View File

@ -136,6 +136,12 @@ class WaccaReverse(WaccaLilyR):
resp.seasonalPlayModeCounts.append(
PlayModeCounts(self.season, 4, profile["playcount_stageup"])
)
resp.seasonalPlayModeCounts.append(
PlayModeCounts(self.season, 5, profile["playcount_time_free"])
)
# For some fucking reason if this isn't here time play is disabled
resp.seasonalPlayModeCounts.append(PlayModeCounts(0, 1, 1))
for opt in profile_options:
resp.options.append(UserOption(opt["opt_id"], opt["value"]))

View File

@ -7,6 +7,7 @@ from sqlalchemy.engine import Row
from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata
from ..handlers.helpers import PlayType
profile = Table(
"wacca_profile",
@ -40,6 +41,7 @@ profile = Table(
Column("playcount_multi_vs", Integer, server_default="0"),
Column("playcount_multi_coop", Integer, server_default="0"),
Column("playcount_stageup", Integer, server_default="0"),
Column("playcount_time_free", Integer, server_default="0"),
Column("friend_view_1", Integer),
Column("friend_view_2", Integer),
Column("friend_view_3", Integer),
@ -160,17 +162,20 @@ class WaccaProfileData(BaseData):
) -> None:
sql = profile.update(profile.c.id == profile_id).values(
playcount_single=profile.c.playcount_single + 1
if play_type == 1
if play_type == PlayType.PlayTypeSingle.value
else profile.c.playcount_single,
playcount_multi_vs=profile.c.playcount_multi_vs + 1
if play_type == 2
if play_type == PlayType.PlayTypeVs.value
else profile.c.playcount_multi_vs,
playcount_multi_coop=profile.c.playcount_multi_coop + 1
if play_type == 3
if play_type == PlayType.PlayTypeCoop.value
else profile.c.playcount_multi_coop,
playcount_stageup=profile.c.playcount_stageup + 1
if play_type == 4
if play_type == PlayType.PlayTypeStageup.value
else profile.c.playcount_stageup,
playcount_time_free=profile.c.playcount_time_free + 1
if play_type == PlayType.PlayTypeTimeFree.value
else profile.c.playcount_time_free,
last_game_ver=game_version,
)