Merge remote-tracking branch 'origin/develop' into fork_develop

This commit is contained in:
Dniel97 2023-03-03 23:31:46 +01:00
commit 8fe0acae93
Signed by untrusted user: Dniel97
GPG Key ID: 6180B3C768FB2E08
42 changed files with 750 additions and 1914 deletions

View File

@ -14,6 +14,7 @@ from time import strptime
from core.config import CoreConfig from core.config import CoreConfig
from core.data import Data from core.data import Data
from core.utils import Utils from core.utils import Utils
from core.const import *
class AllnetServlet: class AllnetServlet:
def __init__(self, core_cfg: CoreConfig, cfg_folder: str): def __init__(self, core_cfg: CoreConfig, cfg_folder: str):
@ -115,6 +116,7 @@ class AllnetServlet:
else: else:
resp = AllnetPowerOnResponse2() resp = AllnetPowerOnResponse2()
self.logger.debug(f"Allnet request: {vars(req)}")
if req.game_id not in self.uri_registry: if req.game_id not in self.uri_registry:
msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}." msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}."
self.data.base.log_event("allnet", "ALLNET_AUTH_UNKNOWN_GAME", logging.WARN, msg) self.data.base.log_event("allnet", "ALLNET_AUTH_UNKNOWN_GAME", logging.WARN, msg)
@ -136,15 +138,19 @@ class AllnetServlet:
if machine is not None: if machine is not None:
arcade = self.data.arcade.get_arcade(machine["arcade"]) arcade = self.data.arcade.get_arcade(machine["arcade"])
resp.country = arcade["country"] if machine["country"] is None else machine["country"] country = arcade["country"] if machine["country"] is None else machine["country"]
if country is None:
country = AllnetCountryCode.JAPAN.value
resp.country = country
resp.place_id = arcade["id"] resp.place_id = arcade["id"]
resp.allnet_id = machine["id"] resp.allnet_id = machine["id"]
resp.name = arcade["name"] resp.name = arcade["name"] if arcade["name"] is not None else ""
resp.nickname = arcade["nickname"] resp.nickname = arcade["nickname"] if arcade["nickname"] is not None else ""
resp.region0 = arcade["region_id"] resp.region0 = arcade["region_id"] if arcade["region_id"] is not None else AllnetJapanRegionId.AICHI.value
resp.region_name0 = arcade["country"] resp.region_name0 = arcade["country"] if arcade["country"] is not None else AllnetCountryCode.JAPAN.value
resp.region_name1 = arcade["state"] resp.region_name1 = arcade["state"] if arcade["state"] is not None else AllnetJapanRegionId.AICHI.name
resp.region_name2 = arcade["city"] resp.region_name2 = arcade["city"] if arcade["city"] is not None else ""
resp.client_timezone = arcade["timezone"] if arcade["timezone"] is not None else "+0900" resp.client_timezone = arcade["timezone"] if arcade["timezone"] is not None else "+0900"
int_ver = req.ver.replace(".", "") int_ver = req.ver.replace(".", "")
@ -154,6 +160,7 @@ class AllnetServlet:
msg = f"{req.serial} authenticated from {request_ip}: {req.game_id} v{req.ver}" msg = f"{req.serial} authenticated from {request_ip}: {req.game_id} v{req.ver}"
self.data.base.log_event("allnet", "ALLNET_AUTH_SUCCESS", logging.INFO, msg) self.data.base.log_event("allnet", "ALLNET_AUTH_SUCCESS", logging.INFO, msg)
self.logger.info(msg) self.logger.info(msg)
self.logger.debug(f"Allnet response: {vars(resp)}")
return self.dict_to_http_form_string([vars(resp)]).encode("utf-8") return self.dict_to_http_form_string([vars(resp)]).encode("utf-8")
@ -191,7 +198,7 @@ class AllnetServlet:
self.logger.debug(f"request {req_dict}") self.logger.debug(f"request {req_dict}")
rsa = RSA.import_key(open(self.config.billing.sign_key, 'rb').read()) rsa = RSA.import_key(open(self.config.billing.signing_key, 'rb').read())
signer = PKCS1_v1_5.new(rsa) signer = PKCS1_v1_5.new(rsa)
digest = SHA.new() digest = SHA.new()

View File

@ -31,6 +31,62 @@ class KeychipPlatformsCodes():
NU = ("A60E", "A60E", "A60E") NU = ("A60E", "A60E", "A60E")
NUSX = ("A61X", "A69X") NUSX = ("A61X", "A69X")
ALLS = "A63E" ALLS = "A63E"
class RegionIDs(Enum): class AllnetCountryCode(Enum):
pass JAPAN = "JPN"
UNITED_STATES = "USA"
HONG_KONG = "HKG"
SINGAPORE = "SGP"
SOUTH_KOREA = "KOR"
TAIWAN = "TWN"
CHINA = "CHN"
class AllnetJapanRegionId(Enum):
NONE = 0
AICHI = 1
AOMORI = 2
AKITA = 3
ISHIKAWA = 4
IBARAKI = 5
IWATE = 6
EHIME = 7
OITA = 8
OSAKA = 9
OKAYAMA = 10
OKINAWA = 11
KAGAWA = 12
KAGOSHIMA = 13
KANAGAWA = 14
GIFU = 15
KYOTO = 16
KUMAMOTO = 17
GUNMA = 18
KOCHI = 19
SAITAMA = 20
SAGA = 21
SHIGA = 22
SHIZUOKA = 23
SHIMANE = 24
CHIBA = 25
TOKYO = 26
TOKUSHIMA = 27
TOCHIGI = 28
TOTTORI = 29
TOYAMA = 30
NAGASAKI = 31
NAGANO = 32
NARA = 33
NIIGATA = 34
HYOGO = 35
HIROSHIMA = 36
FUKUI = 37
FUKUOKA = 38
FUKUSHIMA = 39
HOKKAIDO = 40
MIE = 41
MIYAGI = 42
MIYAZAKI = 43
YAMAGATA = 44
YAMAGUCHI = 45
YAMANASHI = 46
WAKAYAMA = 47

File diff suppressed because it is too large Load Diff

View File

@ -4,8 +4,10 @@ from sqlalchemy.sql.schema import ForeignKey, PrimaryKeyConstraint
from sqlalchemy.types import Integer, String, Boolean from sqlalchemy.types import Integer, String, Boolean
from sqlalchemy.sql import func, select from sqlalchemy.sql import func, select
from sqlalchemy.dialects.mysql import insert from sqlalchemy.dialects.mysql import insert
import re
from core.data.schema.base import BaseData, metadata from core.data.schema.base import BaseData, metadata
from core.const import *
arcade = Table( arcade = Table(
"arcade", "arcade",
@ -50,9 +52,20 @@ arcade_owner = Table(
class ArcadeData(BaseData): class ArcadeData(BaseData):
def get_machine(self, serial: str = None, id: int = None) -> Optional[Dict]: def get_machine(self, serial: str = None, id: int = None) -> Optional[Dict]:
if serial is not None: if serial is not None:
sql = machine.select(machine.c.serial == serial) serial = serial.replace("-", "")
if len(serial) == 11:
sql = machine.select(machine.c.serial.like(f"{serial}%"))
elif len(serial) == 15:
sql = machine.select(machine.c.serial == serial)
else:
self.logger.error(f"{__name__ }: Malformed serial {serial}")
return None
elif id is not None: elif id is not None:
sql = machine.select(machine.c.id == id) sql = machine.select(machine.c.id == id)
else: else:
self.logger.error(f"{__name__ }: Need either serial or ID to look up!") self.logger.error(f"{__name__ }: Need either serial or ID to look up!")
return None return None
@ -61,20 +74,28 @@ class ArcadeData(BaseData):
if result is None: return None if result is None: return None
return result.fetchone() return result.fetchone()
def put_machine(self, arcade_id: int, serial: str = None, board: str = None, game: str = None, is_cab: bool = False) -> Optional[int]: def put_machine(self, arcade_id: int, serial: str = "", board: str = None, game: str = None, is_cab: bool = False) -> Optional[int]:
if arcade_id: if arcade_id:
self.logger.error(f"{__name__ }: Need arcade id!") self.logger.error(f"{__name__ }: Need arcade id!")
return None return None
if serial is None:
pass
sql = machine.insert().values(arcade = arcade_id, keychip = serial, board = board, game = game, is_cab = is_cab) sql = machine.insert().values(arcade = arcade_id, keychip = serial, board = board, game = game, is_cab = is_cab)
result = self.execute(sql) result = self.execute(sql)
if result is None: return None if result is None: return None
return result.lastrowid return result.lastrowid
def set_machine_serial(self, machine_id: int, serial: str) -> None:
result = self.execute(machine.update(machine.c.id == machine_id).values(keychip = serial))
if result is None:
self.logger.error(f"Failed to update serial for machine {machine_id} -> {serial}")
return result.lastrowid
def set_machine_boardid(self, machine_id: int, boardid: str) -> None:
result = self.execute(machine.update(machine.c.id == machine_id).values(board = boardid))
if result is None:
self.logger.error(f"Failed to update board id for machine {machine_id} -> {boardid}")
def get_arcade(self, id: int) -> Optional[Dict]: def get_arcade(self, id: int) -> Optional[Dict]:
sql = arcade.select(arcade.c.id == id) sql = arcade.select(arcade.c.id == id)
result = self.execute(sql) result = self.execute(sql)
@ -109,5 +130,31 @@ class ArcadeData(BaseData):
if result is None: return None if result is None: return None
return result.lastrowid return result.lastrowid
def generate_keychip_serial(self, platform_id: int) -> str: def format_serial(self, platform_code: str, platform_rev: int, serial_num: int, append: int = 4152) -> str:
pass return f"{platform_code}{platform_rev:02d}A{serial_num:04d}{append:04d}" # 0x41 = A, 0x52 = R
def validate_keychip_format(self, serial: str) -> bool:
serial = serial.replace("-", "")
if len(serial) != 11 or len(serial) != 15:
self.logger.error(f"Serial validate failed: Incorrect length for {serial} (len {len(serial)})")
return False
platform_code = serial[:4]
platform_rev = serial[4:6]
const_a = serial[6]
num = serial[7:11]
append = serial[11:15]
if re.match("A[7|6]\d[E|X][0|1][0|1|2]A\d{4,8}", serial) is None:
self.logger.error(f"Serial validate failed: {serial} failed regex")
return False
if len(append) != 0 or len(append) != 4:
self.logger.error(f"Serial validate failed: {serial} had malformed append {append}")
return False
if len(num) != 4:
self.logger.error(f"Serial validate failed: {serial} had malformed number {num}")
return False
return True

View File

@ -0,0 +1 @@
ALTER TABLE chuni_score_course DROP COLUMN theoryCount, DROP COLUMN orderId, DROP COLUMN playerRating;

View File

@ -0,0 +1 @@
ALTER TABLE chuni_score_course ADD theoryCount int(11), ADD orderId int(11), ADD playerRating int(11);

View File

@ -0,0 +1,7 @@
SET FOREIGN_KEY_CHECKS=0;
ALTER TABLE ongeki_profile_data DROP COLUMN isDialogWatchedSuggestMemory;
ALTER TABLE ongeki_score_best DROP COLUMN platinumScoreMax;
ALTER TABLE ongeki_score_playlog DROP COLUMN platinumScore;
ALTER TABLE ongeki_score_playlog DROP COLUMN platinumScoreMax;
DROP TABLE IF EXISTS `ongeki_user_memorychapter`;
SET FOREIGN_KEY_CHECKS=1;

View File

@ -0,0 +1,27 @@
SET FOREIGN_KEY_CHECKS=0;
ALTER TABLE ongeki_profile_data ADD COLUMN isDialogWatchedSuggestMemory BOOLEAN;
ALTER TABLE ongeki_score_best ADD COLUMN platinumScoreMax INTEGER;
ALTER TABLE ongeki_score_playlog ADD COLUMN platinumScore INTEGER;
ALTER TABLE ongeki_score_playlog ADD COLUMN platinumScoreMax INTEGER;
CREATE TABLE ongeki_user_memorychapter (
id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
user INT NOT NULL,
chapterId INT NOT NULL,
gaugeId INT NOT NULL,
gaugeNum INT NOT NULL,
jewelCount INT NOT NULL,
isStoryWatched BOOLEAN NOT NULL,
isBossWatched BOOLEAN NOT NULL,
isDialogWatched BOOLEAN NOT NULL,
isEndingWatched BOOLEAN NOT NULL,
isClear BOOLEAN NOT NULL,
lastPlayMusicId INT NOT NULL,
lastPlayMusicLevel INT NOT NULL,
lastPlayMusicCategory INT NOT NULL,
UNIQUE KEY ongeki_user_memorychapter_uk (user, chapterId),
CONSTRAINT ongeki_user_memorychapter_ibfk_1 FOREIGN KEY (user) REFERENCES aime_user (id) ON DELETE CASCADE ON UPDATE CASCADE
);
SET FOREIGN_KEY_CHECKS=1;

View File

@ -53,10 +53,13 @@ class TitleServlet():
code = endpoints["game"] code = endpoints["game"]
if code not in self.title_registry: if code not in self.title_registry:
self.logger.warn(f"Unknown game code {code}") self.logger.warn(f"Unknown game code {code}")
request.setResponseCode(404)
return b""
index = self.title_registry[code] index = self.title_registry[code]
if not hasattr(index, "render_GET"): if not hasattr(index, "render_GET"):
self.logger.warn(f"{code} does not dispatch GET") self.logger.warn(f"{code} does not dispatch GET")
request.setResponseCode(405)
return b"" return b""
return index.render_GET(request, endpoints["version"], endpoints["endpoint"]) return index.render_GET(request, endpoints["version"], endpoints["endpoint"])
@ -65,10 +68,13 @@ class TitleServlet():
code = endpoints["game"] code = endpoints["game"]
if code not in self.title_registry: if code not in self.title_registry:
self.logger.warn(f"Unknown game code {code}") self.logger.warn(f"Unknown game code {code}")
request.setResponseCode(404)
return b""
index = self.title_registry[code] index = self.title_registry[code]
if not hasattr(index, "render_POST"): if not hasattr(index, "render_POST"):
self.logger.warn(f"{code} does not dispatch POST") self.logger.warn(f"{code} does not dispatch POST")
request.setResponseCode(405)
return b"" return b""
return index.render_POST(request, int(endpoints["version"]), endpoints["endpoint"]) return index.render_POST(request, int(endpoints["version"]), endpoints["endpoint"])

View File

@ -32,16 +32,5 @@ if __name__=='__main__':
else: else:
data.migrate_database(args.game, int(args.version), args.action) data.migrate_database(args.game, int(args.version), args.action)
elif args.action == "migrate":
data.logger.info("Migrating from old schema to new schema")
data.restore_from_old_schema()
elif args.action == "dump":
data.logger.info("Dumping old schema to migrate to new schema")
data.dump_db()
elif args.action == "generate":
pass
data.logger.info("Done") data.logger.info("Done")

View File

@ -96,7 +96,7 @@ sudo ufw allow 8443
sudo ufw allow 22345 sudo ufw allow 22345
sudo ufw allow 8090 sudo ufw allow 8090
sudo ufw allow 8444 sudo ufw allow 8444
sudo ufw allow 9000 sudo ufw allow 8080
``` ```
## Running the ARTEMiS instance ## Running the ARTEMiS instance

View File

@ -57,7 +57,7 @@ title:
## Firewall Adjustements ## Firewall Adjustements
Make sure the following ports are open both on your router and local Windows firewall in case you want to use this for public use (NOT recommended): Make sure the following ports are open both on your router and local Windows firewall in case you want to use this for public use (NOT recommended):
> Port 80 (TCP), 443 (TCP), 8443 (TCP), 22345 (TCP), 8090 (TCP) **webui, 8444 (TCP) **mucha, 9000 (TCP) > Port 80 (TCP), 443 (TCP), 8443 (TCP), 22345 (TCP), 8080 (TCP), 8090 (TCP) **webui, 8444 (TCP) **mucha
## Running the ARTEMiS instance ## Running the ARTEMiS instance
> python index.py > python index.py

41
docs/prod.md Normal file
View File

@ -0,0 +1,41 @@
# ARTEMiS Production mode
Production mode is a configuration option that changes how the server listens to be more friendly to a production environment. This mode assumes that a proxy (for this guide, nginx) is standing in front of the server to handle port mapping and TLS. In order to activate production mode, simply change `is_develop` to `False` in `core.yaml`. Next time you start the server, you should see "Starting server in production mode".
## Nginx Configuration
### Port forwarding
Artemis requires that the following ports be forwarded to allow internet traffic to access the server. This will not change regardless of what you set in the config, as many of these ports are hard-coded in the games.
`tcp:80` all.net, non-ssl titles
`tcp:8443` billing
`tcp:22345` aimedb
`tcp:443` frontend, SSL titles
### A note about external proxy services (cloudflare, etc)
Due to the way that artemis functions, it is currently not possible to put the server behind something like Cloudflare. Cloudflare only proxies web traffic on the standard ports (80, 443) and, as shown above, this does not work with artemis. Server administrators should seek other means to protect their network (VPS hosting, VPN, etc)
### SSL Certificates
You will need to generate SSL certificates for some games. The certificates vary in security and validity requirements. Please see the general guide below
- General Title: The certificate for the general title server should be valid, not self-signed and match the CN that the game will be reaching out to (e.i if your games are reaching out to titles.hostname.here, your ssl certificate should be valid for titles.hostname.here, or *.hostname.here)
- CXB: Same requires as the title server. It must not be self-signed, and CN must match. Recomended to get a wildcard cert if possible, and use it for both Title and CXB
- Pokken: Pokken can be self-signed, and the CN doesn't have to match, but it MUST use 2048-bit RSA. Due to the games age, andthing stronger then that will be rejected.
### Port mappings
An example config is provided in the `config` folder called `nginx_example.conf`. It is set up for the following:
`naominet.jp:tcp:80` -> `localhost:tcp:8000` for allnet
`ib.naominet.jp:ssl:8443` -> `localhost:tcp:8444` for the billing server
`your.hostname.here:ssl:443` -> `localhost:tcp:8080` for the SSL title server
`your.hostname.here:tcp:80` -> `localhost:tcp:8080` for the non-SSL title server
`cxb.hostname.here:ssl:443` -> `localhost:tcp:8080` for crossbeats (appends /SDCA/104/ to the request)
`pokken.hostname.here:ssl:443` -> `localhost:tcp:8080` for pokken
`frontend.hostname.here:ssl:443` -> `localhost:tcp:8090` for the frontend, includes https redirection
If you're using this as a guide, be sure to replace your.hostname.here with the hostname you specified in core.yaml under `titles->hostname`. Do *not* change naominet.jp, or allnet/billing will fail. Also remember to specifiy certificate paths correctly, as in the example they are simply placeholders.
### Multi-service ports
It is possible to use nginx to redirect billing and title server requests to the same port that all.net uses. By setting `port` to 0 under billing and title server, you can change the nginx config to serve the following (entries not shown here should be the same)
`ib.naominet.jp:ssl:8443` -> `localhost:tcp:8000` for the billing server
`your.hostname.here:ssl:443` -> `localhost:tcp:8000` for the SSL title server
`your.hostname.here:tcp:80` -> `localhost:tcp:8000` for the non-SSL title server
`cxb.hostname.here:ssl:443` -> `localhost:tcp:8000` for crossbeats (appends /SDCA/104/ to the request)
`pokken.hostname.here:ssl:443` -> `localhost:tcp:8000` for pokken
This will allow you to only use 3 ports locally, but you will still need to forward the same internet-facing ports as before.

View File

@ -99,26 +99,6 @@ server {
} }
} }
# CXB, comment this out if you don't plan on serving crossbeats.
server {
listen 443 ssl;
server_name cxb.hostname.here;
ssl_certificate /path/to/cert/cxb.pem;
ssl_certificate_key /path/to/cert/cxb.key;
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m;
ssl_session_tickets off;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
ssl_ciphers "ALL:@SECLEVEL=1";
ssl_prefer_server_ciphers off;
location / {
proxy_pass http://localhost:8080/SDBT/104/;
}
}
# Frontend, set to redirect to HTTPS. Comment out if you don't intend to use the frontend # Frontend, set to redirect to HTTPS. Comment out if you don't intend to use the frontend
server { server {
listen 80; listen 80;
@ -134,6 +114,7 @@ server {
# Frontend HTTPS. Comment out if you on't intend to use the frontend # Frontend HTTPS. Comment out if you on't intend to use the frontend
server { server {
listen 443 ssl; listen 443 ssl;
server_name frontend.hostname.here;
ssl_certificate /path/to/cert/frontend.pem; ssl_certificate /path/to/cert/frontend.pem;
ssl_certificate_key /path/to/cert/frontend.key; ssl_certificate_key /path/to/cert/frontend.key;

View File

@ -1,6 +1,7 @@
server: server:
enable: True enable: True
loglevel: "info" loglevel: "info"
prefecture_name: "Hokkaido"
mods: mods:
always_vip: True always_vip: True

View File

@ -29,6 +29,7 @@ class HttpDispatcher(resource.Resource):
self.map_post.connect('allnet_poweron', '/sys/servlet/PowerOn', controller="allnet", action='handle_poweron', conditions=dict(method=['POST'])) self.map_post.connect('allnet_poweron', '/sys/servlet/PowerOn', controller="allnet", action='handle_poweron', conditions=dict(method=['POST']))
self.map_post.connect('allnet_downloadorder', '/sys/servlet/DownloadOrder', controller="allnet", action='handle_dlorder', conditions=dict(method=['POST'])) self.map_post.connect('allnet_downloadorder', '/sys/servlet/DownloadOrder', controller="allnet", action='handle_dlorder', conditions=dict(method=['POST']))
self.map_post.connect('allnet_billing', '/request', controller="allnet", action='handle_billing_request', conditions=dict(method=['POST'])) self.map_post.connect('allnet_billing', '/request', controller="allnet", action='handle_billing_request', conditions=dict(method=['POST']))
self.map_post.connect('allnet_billing', '/request/', controller="allnet", action='handle_billing_request', conditions=dict(method=['POST']))
self.map_post.connect('mucha_boardauth', '/mucha/boardauth.do', controller="mucha", action='handle_boardauth', conditions=dict(method=['POST'])) self.map_post.connect('mucha_boardauth', '/mucha/boardauth.do', controller="mucha", action='handle_boardauth', conditions=dict(method=['POST']))
self.map_post.connect('mucha_updatacheck', '/mucha/updatacheck.do', controller="mucha", action='handle_updatacheck', conditions=dict(method=['POST'])) self.map_post.connect('mucha_updatacheck', '/mucha/updatacheck.do', controller="mucha", action='handle_updatacheck', conditions=dict(method=['POST']))

View File

@ -16,7 +16,7 @@ Games listed below have been tested and confirmed working. Only game versions ol
+ All versions + All versions
+ Ongeki + Ongeki
+ All versions up to Bright + All versions up to Bright Memory
+ Wacca + Wacca
+ Lily R + Lily R
@ -29,10 +29,8 @@ Games listed below have been tested and confirmed working. Only game versions ol
- memcached (for non-windows platforms) - memcached (for non-windows platforms)
- mysql/mariadb server - mysql/mariadb server
## Quick start guide ## Setup guides
1) Clone this repository Follow the platform-specific guides for [windows](docs/INSTALL_WINDOWS.md) and [ubuntu](docs/INSTALL_UBUNTU.md) to setup and run the server.
2) Install requirements (see the platform-specific guides for instructions)
3) Install python libraries via `pip` ## Production guide
4) Copy the example configuration files into another folder (by default the server looks for the `config` directory) See the [production guide](docs/prod.md) for running a production server.
5) Edit the newly copied configuration files to your liking, using [this](docs/config.md) doc as a guide.
6) Run the server by invoking `index.py` ex. `python3 index.py`

View File

@ -135,7 +135,8 @@ class ChuniServlet():
req_data = json.loads(unzip) req_data = json.loads(unzip)
self.logger.info(f"v{version} {endpoint} request - {req_data}") self.logger.info(f"v{version} {endpoint} request from {request.getClientAddress().host}")
self.logger.debug(req_data)
func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" func_to_find = "handle_" + inflection.underscore(endpoint) + "_request"
@ -154,7 +155,7 @@ class ChuniServlet():
if resp == None: if resp == None:
resp = {'returnCode': 1} resp = {'returnCode': 1}
self.logger.info(f"Response {resp}") self.logger.debug(f"Response {resp}")
zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8"))

View File

@ -29,6 +29,9 @@ course = Table(
Column("param3", Integer), Column("param3", Integer),
Column("param4", Integer), Column("param4", Integer),
Column("isClear", Boolean), Column("isClear", Boolean),
Column("theoryCount", Integer),
Column("orderId", Integer),
Column("playerRating", Integer),
UniqueConstraint("user", "courseId", name="chuni_score_course_uk"), UniqueConstraint("user", "courseId", name="chuni_score_course_uk"),
mysql_charset='utf8mb4' mysql_charset='utf8mb4'
) )

View File

@ -787,6 +787,10 @@ class OngekiBase():
if "userChapterList" in upsert: if "userChapterList" in upsert:
for x in upsert["userChapterList"]: for x in upsert["userChapterList"]:
self.data.item.put_chapter(user_id, x) self.data.item.put_chapter(user_id, x)
if "userMemoryChapterList" in upsert:
for x in upsert["userMemoryChapterList"]:
self.data.item.put_memorychapter(user_id, x)
if "userItemList" in upsert: if "userItemList" in upsert:
for x in upsert["userItemList"]: for x in upsert["userItemList"]:

View File

@ -0,0 +1,58 @@
from datetime import date, datetime, timedelta
from typing import Any, Dict
import pytz
import json
from core.config import CoreConfig
from titles.ongeki.base import OngekiBase
from titles.ongeki.const import OngekiConstants
from titles.ongeki.config import OngekiConfig
class OngekiBrightMemory(OngekiBase):
def __init__(self, core_cfg: CoreConfig, game_cfg: OngekiConfig) -> None:
super().__init__(core_cfg, game_cfg)
self.version = OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
ret = super().handle_get_game_setting_api_request(data)
ret["gameSetting"]["dataVersion"] = "1.35.00"
ret["gameSetting"]["onlineDataVersion"] = "1.35.00"
ret["gameSetting"]["maxCountCharacter"] = 50
ret["gameSetting"]["maxCountCard"] = 300
ret["gameSetting"]["maxCountItem"] = 300
ret["gameSetting"]["maxCountMusic"] = 50
ret["gameSetting"]["maxCountMusicItem"] = 300
ret["gameSetting"]["maxCountRivalMusic"] = 300
return ret
def handle_get_user_memory_chapter_api_request(self, data: Dict) -> Dict:
memories = self.data.item.get_memorychapters(data["userId"])
if not memories:
return {"userId": data["userId"], "length":6, "userMemoryChapterList":[
{"gaugeId":0, "isClear": False, "gaugeNum": 0, "chapterId": 70001, "jewelCount": 0, "isBossWatched": False, "isStoryWatched": False, "isDialogWatched": False, "isEndingWatched": False, "lastPlayMusicId": 0, "lastPlayMusicLevel": 0, "lastPlayMusicCategory": 0},
{"gaugeId":0, "isClear": False, "gaugeNum": 0, "chapterId": 70002, "jewelCount": 0, "isBossWatched": False, "isStoryWatched": False, "isDialogWatched": False, "isEndingWatched": False, "lastPlayMusicId": 0, "lastPlayMusicLevel": 0, "lastPlayMusicCategory": 0},
{"gaugeId":0, "isClear": False, "gaugeNum": 0, "chapterId": 70003, "jewelCount": 0, "isBossWatched": False, "isStoryWatched": False, "isDialogWatched": False, "isEndingWatched": False, "lastPlayMusicId": 0, "lastPlayMusicLevel": 0, "lastPlayMusicCategory": 0},
{"gaugeId":0, "isClear": False, "gaugeNum": 0, "chapterId": 70004, "jewelCount": 0, "isBossWatched": False, "isStoryWatched": False, "isDialogWatched": False, "isEndingWatched": False, "lastPlayMusicId": 0, "lastPlayMusicLevel": 0, "lastPlayMusicCategory": 0},
{"gaugeId":0, "isClear": False, "gaugeNum": 0, "chapterId": 70005, "jewelCount": 0, "isBossWatched": False, "isStoryWatched": False, "isDialogWatched": False, "isEndingWatched": False, "lastPlayMusicId": 0, "lastPlayMusicLevel": 0, "lastPlayMusicCategory": 0},
{"gaugeId":0, "isClear": False, "gaugeNum": 0, "chapterId": 70099, "jewelCount": 0, "isBossWatched": False, "isStoryWatched": False, "isDialogWatched": False, "isEndingWatched": False, "lastPlayMusicId": 0, "lastPlayMusicLevel": 0, "lastPlayMusicCategory": 0}
]}
memory_chp = []
for chp in memories:
tmp = chp._asdict()
tmp.pop("id")
tmp.pop("user")
memory_chp.append(tmp)
return {
"userId": data["userId"],
"length": len(memory_chp),
"userMemoryChapterList": memory_chp
}
def handle_get_game_music_release_state_api_request(self, data: Dict) -> Dict:
return {
"techScore": 0,
"cardNum": 0
}

View File

@ -10,6 +10,7 @@ class OngekiConstants():
VER_ONGEKI_RED = 4 VER_ONGEKI_RED = 4
VER_ONGEKI_RED_PLUS = 5 VER_ONGEKI_RED_PLUS = 5
VER_ONGEKI_BRIGHT = 6 VER_ONGEKI_BRIGHT = 6
VER_ONGEKI_BRIGHT_MEMORY = 7
EVT_TYPES: Enum = Enum('EVT_TYPES', [ EVT_TYPES: Enum = Enum('EVT_TYPES', [
'None', 'None',
@ -43,7 +44,7 @@ class OngekiConstants():
Lunatic = 10 Lunatic = 10
VERSION_NAMES = ("ONGEKI", "ONGEKI+", "ONGEKI Summer", "ONGEKI Summer+", "ONGEKI Red", "ONGEKI Red+", VERSION_NAMES = ("ONGEKI", "ONGEKI+", "ONGEKI Summer", "ONGEKI Summer+", "ONGEKI Red", "ONGEKI Red+",
"ONGEKI Bright") "ONGEKI Bright", "ONGEKI Bright Memory")
@classmethod @classmethod
def game_ver_to_string(cls, ver: int): def game_ver_to_string(cls, ver: int):

View File

@ -17,6 +17,7 @@ from titles.ongeki.summerplus import OngekiSummerPlus
from titles.ongeki.red import OngekiRed from titles.ongeki.red import OngekiRed
from titles.ongeki.redplus import OngekiRedPlus from titles.ongeki.redplus import OngekiRedPlus
from titles.ongeki.bright import OngekiBright from titles.ongeki.bright import OngekiBright
from titles.ongeki.brightmemory import OngekiBrightMemory
class OngekiServlet(): class OngekiServlet():
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
@ -32,6 +33,7 @@ class OngekiServlet():
OngekiRed(core_cfg, self.game_cfg), OngekiRed(core_cfg, self.game_cfg),
OngekiRedPlus(core_cfg, self.game_cfg), OngekiRedPlus(core_cfg, self.game_cfg),
OngekiBright(core_cfg, self.game_cfg), OngekiBright(core_cfg, self.game_cfg),
OngekiBrightMemory(core_cfg, self.game_cfg),
] ]
self.logger = logging.getLogger("ongeki") self.logger = logging.getLogger("ongeki")
@ -69,8 +71,10 @@ class OngekiServlet():
internal_ver = OngekiConstants.VER_ONGEKI_RED internal_ver = OngekiConstants.VER_ONGEKI_RED
elif version >= 125 and version < 130: # Red Plus elif version >= 125 and version < 130: # Red Plus
internal_ver = OngekiConstants.VER_ONGEKI_RED_PLUS internal_ver = OngekiConstants.VER_ONGEKI_RED_PLUS
elif version >= 130 and version < 135: # Red Plus elif version >= 130 and version < 135: # Bright
internal_ver = OngekiConstants.VER_ONGEKI_BRIGHT internal_ver = OngekiConstants.VER_ONGEKI_BRIGHT
elif version >= 135 and version < 140: # Bright Memory
internal_ver = OngekiConstants.VER_ONGEKI_BRIGHT_MEMORY
if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32: if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32:
# If we get a 32 character long hex string, it's a hash and we're # If we get a 32 character long hex string, it's a hash and we're

View File

@ -107,6 +107,27 @@ chapter = Table(
mysql_charset='utf8mb4' mysql_charset='utf8mb4'
) )
memorychapter = Table(
"ongeki_user_memorychapter",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
Column("chapterId", Integer),
Column("gaugeId", Integer),
Column("gaugeNum", Integer),
Column("jewelCount", Integer),
Column("isStoryWatched", Boolean),
Column("isBossWatched", Boolean),
Column("isDialogWatched", Boolean),
Column("isEndingWatched", Boolean),
Column("isClear", Boolean),
Column("lastPlayMusicId", Integer),
Column("lastPlayMusicLevel", Integer),
Column("lastPlayMusicCategory", Integer),
UniqueConstraint("user", "chapterId", name="ongeki_user_memorychapter_uk"),
mysql_charset='utf8mb4'
)
item = Table( item = Table(
"ongeki_user_item", "ongeki_user_item",
metadata, metadata,
@ -522,5 +543,24 @@ class OngekiItemData(BaseData):
sql = select(boss).where(boss.c.user == aime_id) sql = select(boss).where(boss.c.user == aime_id)
result = self.execute(sql) result = self.execute(sql)
if result is None: return None
return result.fetchall()
def put_memorychapter(self, aime_id: int, memorychapter_data: Dict) -> Optional[int]:
memorychapter_data["user"] = aime_id
sql = insert(memorychapter).values(**memorychapter_data)
conflict = sql.on_duplicate_key_update(**memorychapter_data)
result = self.execute(conflict)
if result is None:
self.logger.warn(f"put_memorychapter: Failed to update! aime_id: {aime_id}")
return None
return result.lastrowid
def get_memorychapters(self, aime_id: int) -> Optional[List[Dict]]:
sql = select(memorychapter).where(memorychapter.c.user == aime_id)
result = self.execute(sql)
if result is None: return None if result is None: return None
return result.fetchall() return result.fetchall()

View File

@ -78,6 +78,7 @@ profile = Table(
Column("overDamageBattlePoint", Integer, server_default="0"), Column("overDamageBattlePoint", Integer, server_default="0"),
Column("bestBattlePoint", Integer, server_default="0"), Column("bestBattlePoint", Integer, server_default="0"),
Column("lastEmoneyBrand", Integer, server_default="0"), Column("lastEmoneyBrand", Integer, server_default="0"),
Column("isDialogWatchedSuggestMemory", Boolean),
UniqueConstraint("user", "version", name="ongeki_profile_profile_uk"), UniqueConstraint("user", "version", name="ongeki_profile_profile_uk"),
mysql_charset='utf8mb4' mysql_charset='utf8mb4'
) )

View File

@ -28,6 +28,7 @@ score_best = Table(
Column("isLock", Boolean, nullable=False), Column("isLock", Boolean, nullable=False),
Column("clearStatus", Boolean, nullable=False), Column("clearStatus", Boolean, nullable=False),
Column("isStoryWatched", Boolean, nullable=False), Column("isStoryWatched", Boolean, nullable=False),
Column("platinumScoreMax", Integer),
UniqueConstraint("user", "musicId", "level", name="ongeki_best_score_uk"), UniqueConstraint("user", "musicId", "level", name="ongeki_best_score_uk"),
mysql_charset='utf8mb4' mysql_charset='utf8mb4'
) )
@ -96,6 +97,8 @@ playlog = Table(
Column("isAllBreak", Boolean), Column("isAllBreak", Boolean),
Column("playerRating", Integer), Column("playerRating", Integer),
Column("battlePoint", Integer), Column("battlePoint", Integer),
Column("platinumScore", Integer),
Column("platinumScoreMax", Integer),
mysql_charset='utf8mb4' mysql_charset='utf8mb4'
) )

View File

@ -1,15 +1,15 @@
from typing import Any, List, Dict from typing import Any, List, Dict
import logging import logging
import inflection
from math import floor from math import floor
from datetime import datetime, timedelta from datetime import datetime, timedelta
from core.config import CoreConfig from core.config import CoreConfig
from titles.wacca.config import WaccaConfig from titles.wacca.config import WaccaConfig
from titles.wacca.const import WaccaConstants from titles.wacca.const import WaccaConstants
from titles.wacca.database import WaccaData from titles.wacca.database import WaccaData
from titles.wacca.handlers import * from titles.wacca.handlers import *
from core.const import AllnetCountryCode
class WaccaBase(): class WaccaBase():
def __init__(self, cfg: CoreConfig, game_cfg: WaccaConfig) -> None: def __init__(self, cfg: CoreConfig, game_cfg: WaccaConfig) -> None:
@ -23,53 +23,61 @@ class WaccaBase():
self.season = 1 self.season = 1
self.OPTIONS_DEFAULTS: Dict[str, Any] = { self.OPTIONS_DEFAULTS: Dict[str, Any] = {
"note_speed": 5, "note_speed": 5,
"field_mask": 0, "field_mask": 0,
"note_sound": 105001, "note_sound": 105001,
"note_color": 203001, "note_color": 203001,
"bgm_volume": 10, "bgm_volume": 10,
"bg_video": 0, "bg_video": 0,
"mirror": 0,
"judge_display_pos": 0,
"judge_detail_display": 0,
"measure_guidelines": 1,
"guideline_mask": 1,
"judge_line_timing_adjust": 10,
"note_design": 3,
"bonus_effect": 1,
"chara_voice": 1,
"score_display_method": 0,
"give_up": 0,
"guideline_spacing": 1,
"center_display": 1,
"ranking_display": 1,
"stage_up_icon_display": 1,
"rating_display": 1,
"player_level_display": 1,
"touch_effect": 1,
"guide_sound_vol": 3,
"touch_note_vol": 8,
"hold_note_vol": 8,
"slide_note_vol": 8,
"snap_note_vol": 8,
"chain_note_vol": 8,
"bonus_note_vol": 8,
"gate_skip": 0,
"key_beam_display": 1,
"left_slide_note_color": 4, "mirror": 0,
"right_slide_note_color": 3, "judge_display_pos": 0,
"forward_slide_note_color": 1, "judge_detail_display": 0,
"back_slide_note_color": 2, "measure_guidelines": 1,
"guideline_mask": 1,
"master_vol": 3, "judge_line_timing_adjust": 10,
"set_title_id": 104001, "note_design": 3,
"set_icon_id": 102001, "bonus_effect": 1,
"set_nav_id": 210001, "chara_voice": 1,
"set_plate_id": 211001 "score_display_method": 0,
} "give_up": 0,
"guideline_spacing": 1,
"center_display": 1,
"ranking_display": 1,
"stage_up_icon_display": 1,
"rating_display": 1,
"player_level_display": 1,
"touch_effect": 1,
"guide_sound_vol": 3,
"touch_note_vol": 8,
"hold_note_vol": 8,
"slide_note_vol": 8,
"snap_note_vol": 8,
"chain_note_vol": 8,
"bonus_note_vol": 8,
"gate_skip": 0,
"key_beam_display": 1,
"left_slide_note_color": 4,
"right_slide_note_color": 3,
"forward_slide_note_color": 1,
"back_slide_note_color": 2,
"master_vol": 3,
"set_title_id": 104001,
"set_icon_id": 102001,
"set_nav_id": 210001,
"set_plate_id": 211001
}
self.allowed_stages = [] self.allowed_stages = []
prefecture_name = inflection.underscore(game_cfg.server.prefecture_name).replace(' ', '_').upper()
if prefecture_name not in [region.name for region in WaccaConstants.Region]:
self.logger.warning(f"Invalid prefecture name {game_cfg.server.prefecture_name} in config file")
self.region_id = WaccaConstants.Region.HOKKAIDO
else:
self.region_id = WaccaConstants.Region[prefecture_name]
def handle_housing_get_request(self, data: Dict) -> Dict: def handle_housing_get_request(self, data: Dict) -> Dict:
req = BaseRequest(data) req = BaseRequest(data)
@ -85,17 +93,30 @@ class WaccaBase():
def handle_housing_start_request(self, data: Dict) -> Dict: def handle_housing_start_request(self, data: Dict) -> Dict:
req = HousingStartRequestV1(data) req = HousingStartRequestV1(data)
resp = HousingStartResponseV1( machine = self.data.arcade.get_machine(req.chipId)
1, if machine is not None:
[ # Recomended songs arcade = self.data.arcade.get_arcade(machine["arcade"])
1269,1007,1270,1002,1020,1003,1008,1211,1018,1092,1056,32, allnet_region_id = arcade["region_id"]
1260,1230,1258,1251,2212,1264,1125,1037,2001,1272,1126,1119,
1104,1070,1047,1044,1027,1004,1001,24,2068,2062,2021,1275, if req.appVersion.country == AllnetCountryCode.JAPAN.value:
1249,1207,1203,1107,1021,1009,9,4,3,23,22,2014,13,1276,1247, if allnet_region_id is not None:
1240,1237,1128,1114,1110,1109,1102,1045,1043,1036,1035,1030, region = WaccaConstants.allnet_region_id_to_wacca_region(allnet_region_id)
1023,1015
] if region is None:
) region_id = self.region_id
else:
region_id = region
else:
region_id = self.region_id
elif req.appVersion.country in WaccaConstants.VALID_COUNTRIES:
region_id = WaccaConstants.Region[req.appVersion.country]
else:
region_id = WaccaConstants.Region.NONE
resp = HousingStartResponseV1(region_id)
return resp.make() return resp.make()
def handle_advertise_GetNews_request(self, data: Dict) -> Dict: def handle_advertise_GetNews_request(self, data: Dict) -> Dict:
@ -110,7 +131,6 @@ class WaccaBase():
def handle_user_status_get_request(self, data: Dict)-> Dict: def handle_user_status_get_request(self, data: Dict)-> Dict:
req = UserStatusGetRequest(data) req = UserStatusGetRequest(data)
resp = UserStatusGetV1Response() resp = UserStatusGetV1Response()
ver_split = req.appVersion.split(".")
profile = self.data.profile.get_profile(aime_id=req.aimeId) profile = self.data.profile.get_profile(aime_id=req.aimeId)
if profile is None: if profile is None:
@ -118,14 +138,11 @@ class WaccaBase():
resp.profileStatus = ProfileStatus.ProfileRegister resp.profileStatus = ProfileStatus.ProfileRegister
return resp.make() return resp.make()
self.logger.info(f"User preview for {req.aimeId} from {req.chipId}") self.logger.info(f"User preview for {req.aimeId} from {req.chipId}")
if profile["last_game_ver"] is None: if profile["last_game_ver"] is None:
profile_ver_split = ver_split resp.lastGameVersion = ShortVersion(str(req.appVersion))
resp.lastGameVersion = req.appVersion
else: else:
profile_ver_split = profile["last_game_ver"].split(".") resp.lastGameVersion = ShortVersion(profile["last_game_ver"])
resp.lastGameVersion = profile["last_game_ver"]
resp.userStatus.userId = profile["id"] resp.userStatus.userId = profile["id"]
resp.userStatus.username = profile["username"] resp.userStatus.username = profile["username"]
@ -145,27 +162,11 @@ class WaccaBase():
set_icon_id = self.OPTIONS_DEFAULTS["set_icon_id"] set_icon_id = self.OPTIONS_DEFAULTS["set_icon_id"]
resp.setIconId = set_icon_id resp.setIconId = set_icon_id
if req.appVersion > resp.lastGameVersion:
if int(ver_split[0]) > int(profile_ver_split[0]):
resp.versionStatus = PlayVersionStatus.VersionUpgrade resp.versionStatus = PlayVersionStatus.VersionUpgrade
elif int(ver_split[0]) < int(profile_ver_split[0]):
resp.versionStatus = PlayVersionStatus.VersionTooNew
else: elif req.appVersion < resp.lastGameVersion:
if int(ver_split[1]) > int(profile_ver_split[1]): resp.versionStatus = PlayVersionStatus.VersionTooNew
resp.versionStatus = PlayVersionStatus.VersionUpgrade
elif int(ver_split[1]) < int(profile_ver_split[1]):
resp.versionStatus = PlayVersionStatus.VersionTooNew
else:
if int(ver_split[2]) > int(profile_ver_split[2]):
resp.versionStatus = PlayVersionStatus.VersionUpgrade
elif int(ver_split[2]) < int(profile_ver_split[2]):
resp.versionStatus = PlayVersionStatus.VersionTooNew
return resp.make() return resp.make()
@ -375,12 +376,12 @@ class WaccaBase():
return resp.make() return resp.make()
self.logger.info(f"Get trial info for user {req.profileId}") self.logger.info(f"Get trial info for user {req.profileId}")
stages = self.data.score.get_stageup(user_id, self.version) stages = self.data.score.get_stageup(user_id, self.version)
if stages is None: if stages is None:
stages = [] stages = []
add_next = True tmp: List[StageInfo] = []
for d in self.allowed_stages: for d in self.allowed_stages:
stage_info = StageInfo(d[0], d[1]) stage_info = StageInfo(d[0], d[1])
@ -393,11 +394,13 @@ class WaccaBase():
stage_info.song3BestScore = score["song3_score"] stage_info.song3BestScore = score["song3_score"]
break break
if add_next or stage_info.danLevel < 9: tmp.append(stage_info)
resp.stageList.append(stage_info)
if stage_info.danLevel >= 9 and stage_info.clearStatus < 1: for x in range(len(tmp)):
add_next = False if tmp[x].danLevel >= 10 and (tmp[x + 1].clearStatus >= 1 or tmp[x].clearStatus >= 1):
resp.stageList.append(tmp[x])
elif tmp[x].danLevel < 10:
resp.stageList.append(tmp[x])
return resp.make() return resp.make()
@ -763,7 +766,7 @@ class WaccaBase():
user_id = self.data.profile.profile_to_aime_user(req.profileId) user_id = self.data.profile.profile_to_aime_user(req.profileId)
for opt in req.optsUpdated: for opt in req.optsUpdated:
self.data.profile.update_option(user_id, opt.id, opt.val) self.data.profile.update_option(user_id, opt.opt_id, opt.opt_val)
for update in req.datesUpdated: for update in req.datesUpdated:
pass pass

View File

@ -13,6 +13,10 @@ class WaccaServerConfig():
def loglevel(self) -> int: def loglevel(self) -> int:
return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'wacca', 'server', 'loglevel', default="info")) return CoreConfig.str_to_loglevel(CoreConfig.get_config_field(self.__config, 'wacca', 'server', 'loglevel', default="info"))
@property
def prefecture_name(self) -> str:
return CoreConfig.get_config_field(self.__config, 'wacca', 'server', 'prefecture_name', default="Hokkaido")
class WaccaModsConfig(): class WaccaModsConfig():
def __init__(self, parent_config: "WaccaConfig") -> None: def __init__(self, parent_config: "WaccaConfig") -> None:
self.__config = parent_config self.__config = parent_config

View File

@ -1,4 +1,7 @@
from enum import Enum from enum import Enum
from typing import Optional
from core.const import AllnetJapanRegionId
class WaccaConstants(): class WaccaConstants():
CONFIG_NAME = "wacca.yaml" CONFIG_NAME = "wacca.yaml"
@ -95,19 +98,92 @@ class WaccaConstants():
"set_plate_id": 1005, # ID "set_plate_id": 1005, # ID
} }
DIFFICULTIES = {
"Normal": 1,
"Hard": 2,
"Expert": 3,
"Inferno": 4,
}
class Difficulty(Enum): class Difficulty(Enum):
NORMAL = 1 NORMAL = 1
HARD = 2 HARD = 2
EXPERT = 3 EXPERT = 3
INFERNO = 4 INFERNO = 4
class Region(Enum):
NONE = 0
HOKKAIDO = 1
AOMORI = 2
IWATE = 3
MIYAGI = 4
AKITA = 5
YAMAGATA = 6
FUKUSHIMA = 7
IBARAKI = 8
TOCHIGI = 9
GUNMA = 10
SAITAMA = 11
CHIBA = 12
TOKYO = 13
KANAGAWA = 14
NIIGATA = 15
TOYAMA = 16
ISHIKAWA = 17
FUKUI = 18
YAMANASHI = 19
NAGANO = 20
GIFU = 21
SHIZUOKA = 22
AICHI = 23
MIE = 24
SHIGA = 25
KYOTO = 26
OSAKA = 27
HYOGO = 28
NARA = 29
WAKAYAMA = 30
TOTTORI = 31
SHIMANE = 32
OKAYAMA = 33
HIROSHIMA = 34
YAMAGUCHI = 35
TOKUSHIMA = 36
KAGAWA = 37
EHIME = 38
KOCHI = 39
FUKUOKA = 40
SAGA = 41
NAGASAKI = 42
KUMAMOTO = 43
OITA = 44
MIYAZAKI = 45
KAGOSHIMA = 46
OKINAWA = 47
UNITED_STATES = 48
USA = 48
TAIWAN = 49
TWN = 49
HONG_KONG = 50
HKG = 50
SINGAPORE = 51
SGP = 51
KOREA = 52
KOR = 52
VALID_COUNTRIES = set(["JPN", "USA", "KOR", "HKG", "SGP"])
@classmethod @classmethod
def game_ver_to_string(cls, ver: int): def game_ver_to_string(cls, ver: int):
return cls.VERSION_NAMES[ver] return cls.VERSION_NAMES[ver]
@classmethod
def allnet_region_id_to_wacca_region(cls, region: int) -> Optional[Region]:
try:
return [
cls.Region.NONE, cls.Region.AICHI, cls.Region.AOMORI, cls.Region.AKITA, cls.Region.ISHIKAWA,
cls.Region.IBARAKI, cls.Region.IWATE, cls.Region.EHIME, cls.Region.OITA, cls.Region.OSAKA,
cls.Region.OKAYAMA, cls.Region.OKINAWA, cls.Region.KAGAWA, cls.Region.KAGOSHIMA, cls.Region.KANAGAWA,
cls.Region.GIFU, cls.Region.KYOTO, cls.Region.KUMAMOTO, cls.Region.GUNMA, cls.Region.KOCHI,
cls.Region.SAITAMA, cls.Region.SAGA, cls.Region.SHIGA, cls.Region.SHIZUOKA, cls.Region.SHIMANE,
cls.Region.CHIBA, cls.Region.TOKYO, cls.Region.TOKUSHIMA, cls.Region.TOCHIGI, cls.Region.TOTTORI,
cls.Region.TOYAMA, cls.Region.NAGASAKI, cls.Region.NAGANO, cls.Region.NARA, cls.Region.NIIGATA,
cls.Region.HYOGO, cls.Region.HIROSHIMA, cls.Region.FUKUI, cls.Region.FUKUOKA, cls.Region.FUKUSHIMA,
cls.Region.HOKKAIDO, cls.Region.MIE, cls.Region.MIYAGI, cls.Region.MIYAZAKI, cls.Region.YAMAGATA,
cls.Region.YAMAGUCHI, cls.Region.YAMANASHI, cls.Region.WAKAYAMA,
][region]
except: return None

View File

@ -1,10 +1,11 @@
from typing import Dict, List from typing import Dict, List
from titles.wacca.handlers.helpers import Version
from datetime import datetime from datetime import datetime
class BaseRequest(): class BaseRequest():
def __init__(self, data: Dict) -> None: def __init__(self, data: Dict) -> None:
self.requestNo: int = data["requestNo"] self.requestNo: int = data["requestNo"]
self.appVersion: str = data["appVersion"] self.appVersion: Version = Version(data["appVersion"])
self.boardId: str = data["boardId"] self.boardId: str = data["boardId"]
self.chipId: str = data["chipId"] self.chipId: str = data["chipId"]
self.params: List = data["params"] self.params: List = data["params"]

View File

@ -3,7 +3,94 @@ from enum import Enum
from titles.wacca.const import WaccaConstants from titles.wacca.const import WaccaConstants
class HousingInfo(): class ShortVersion:
def __init__(self, version: str = "", major = 1, minor = 0, patch = 0) -> None:
split = version.split(".")
if len(split) >= 3:
self.major = int(split[0])
self.minor = int(split[1])
self.patch = int(split[2])
else:
self.major = major
self.minor = minor
self.patch = patch
def __str__(self) -> str:
return f"{self.major}.{self.minor}.{self.patch}"
def __int__(self) -> int:
return (self.major * 10000) + (self.minor * 100) + self.patch
def __eq__(self, other: "ShortVersion"):
return self.major == other.major and self.minor == other.minor and self.patch == other.patch
def __gt__(self, other: "ShortVersion"):
if self.major > other.major:
return True
elif self.major == other.major:
if self.minor > other.minor:
return True
elif self.minor == other.minor:
if self.patch > other.patch:
return True
return False
def __ge__(self, other: "ShortVersion"):
if self.major > other.major:
return True
elif self.major == other.major:
if self.minor > other.minor:
return True
elif self.minor == other.minor:
if self.patch > other.patch or self.patch == other.patch:
return True
return False
def __lt__(self, other: "ShortVersion"):
if self.major < other.major:
return True
elif self.major == other.major:
if self.minor < other.minor:
return True
elif self.minor == other.minor:
if self.patch < other.patch:
return True
return False
def __le__(self, other: "ShortVersion"):
if self.major < other.major:
return True
elif self.major == other.major:
if self.minor < other.minor:
return True
elif self.minor == other.minor:
if self.patch < other.patch or self.patch == other.patch:
return True
return False
class Version(ShortVersion):
def __init__(self, version = "", major = 1, minor = 0, patch = 0, country = "JPN", build = 0, role = "C") -> None:
super().__init__(version, major, minor, patch)
split = version.split(".")
if len(split) >= 6:
self.country: str = split[3]
self.build = int(split[4])
self.role: str = split[5]
else:
self.country = country
self.build = build
self.role = role
def __str__(self) -> str:
return f"{self.major}.{self.minor}.{self.patch}.{self.country}.{self.role}.{self.build}"
class HousingInfo:
""" """
1 is lan install role, 2 is country 1 is lan install role, 2 is country
""" """
@ -17,7 +104,7 @@ class HousingInfo():
def make(self) -> List: def make(self) -> List:
return [ self.id, self.val ] return [ self.id, self.val ]
class Notice(): class Notice:
name: str = "" name: str = ""
title: str = "" title: str = ""
message: str = "" message: str = ""
@ -40,7 +127,7 @@ class Notice():
return [ self.name, self.title, self.message, self.unknown3, self.unknown4, int(self.showTitleScreen), return [ self.name, self.title, self.message, self.unknown3, self.unknown4, int(self.showTitleScreen),
int(self.showWelcomeScreen), self.startTime, self.endTime, self.voiceline] int(self.showWelcomeScreen), self.startTime, self.endTime, self.voiceline]
class UserOption(): class UserOption:
def __init__(self, opt_id: int = 0, opt_val: Any = 0) -> None: def __init__(self, opt_id: int = 0, opt_val: Any = 0) -> None:
self.opt_id = opt_id self.opt_id = opt_id
self.opt_val = opt_val self.opt_val = opt_val
@ -48,7 +135,7 @@ class UserOption():
def make(self) -> List: def make(self) -> List:
return [self.opt_id, self.opt_val] return [self.opt_id, self.opt_val]
class UserStatusV1(): class UserStatusV1:
def __init__(self) -> None: def __init__(self) -> None:
self.userId: int = 0 self.userId: int = 0
self.username: str = "" self.username: str = ""
@ -106,7 +193,7 @@ class PlayVersionStatus(Enum):
VersionTooNew = 1 VersionTooNew = 1
VersionUpgrade = 2 VersionUpgrade = 2
class PlayModeCounts(): class PlayModeCounts:
seasonId: int = 0 seasonId: int = 0
modeId: int = 0 modeId: int = 0
playNum: int = 0 playNum: int = 0
@ -123,7 +210,7 @@ class PlayModeCounts():
self.playNum self.playNum
] ]
class SongUnlock(): class SongUnlock:
songId: int = 0 songId: int = 0
difficulty: int = 0 difficulty: int = 0
whenAppeared: int = 0 whenAppeared: int = 0
@ -143,7 +230,7 @@ class SongUnlock():
self.whenUnlocked self.whenUnlocked
] ]
class GenericItemRecv(): class GenericItemRecv:
def __init__(self, item_type: int = 1, item_id: int = 1, quantity: int = 1) -> None: def __init__(self, item_type: int = 1, item_id: int = 1, quantity: int = 1) -> None:
self.itemId = item_id self.itemId = item_id
self.itemType = item_type self.itemType = item_type
@ -152,7 +239,7 @@ class GenericItemRecv():
def make(self) -> List: def make(self) -> List:
return [ self.itemType, self.itemId, self.quantity ] return [ self.itemType, self.itemId, self.quantity ]
class GenericItemSend(): class GenericItemSend:
def __init__(self, itemId: int, itemType: int, whenAcquired: int) -> None: def __init__(self, itemId: int, itemType: int, whenAcquired: int) -> None:
self.itemId = itemId self.itemId = itemId
self.itemType = itemType self.itemType = itemType
@ -180,7 +267,7 @@ class IconItem(GenericItemSend):
self.whenAcquired self.whenAcquired
] ]
class TrophyItem(): class TrophyItem:
trophyId: int = 0 trophyId: int = 0
season: int = 1 season: int = 1
progress: int = 0 progress: int = 0
@ -200,7 +287,7 @@ class TrophyItem():
self.badgeType self.badgeType
] ]
class TicketItem(): class TicketItem:
userTicketId: int = 0 userTicketId: int = 0
ticketId: int = 0 ticketId: int = 0
whenExpires: int = 0 whenExpires: int = 0
@ -233,7 +320,7 @@ class NavigatorItem(IconItem):
self.usesToday self.usesToday
] ]
class SkillItem(): class SkillItem:
skill_type: int skill_type: int
level: int level: int
flag: int flag: int
@ -247,7 +334,7 @@ class SkillItem():
self.badge self.badge
] ]
class UserItemInfoV1(): class UserItemInfoV1:
def __init__(self) -> None: def __init__(self) -> None:
self.songUnlocks: List[SongUnlock] = [] self.songUnlocks: List[SongUnlock] = []
self.titles: List[GenericItemSend] = [] self.titles: List[GenericItemSend] = []
@ -331,7 +418,7 @@ class UserItemInfoV3(UserItemInfoV2):
ret.append(effect) ret.append(effect)
return ret return ret
class SongDetailClearCounts(): class SongDetailClearCounts:
def __init__(self, play_ct: int = 0, clear_ct: int = 0, ml_ct: int = 0, fc_ct: int = 0, def __init__(self, play_ct: int = 0, clear_ct: int = 0, ml_ct: int = 0, fc_ct: int = 0,
am_ct: int = 0, counts: Optional[List[int]] = None) -> None: am_ct: int = 0, counts: Optional[List[int]] = None) -> None:
if counts is None: if counts is None:
@ -351,7 +438,7 @@ class SongDetailClearCounts():
def make(self) -> List: def make(self) -> List:
return [self.playCt, self.clearCt, self.misslessCt, self.fullComboCt, self.allMarvelousCt] return [self.playCt, self.clearCt, self.misslessCt, self.fullComboCt, self.allMarvelousCt]
class SongDetailGradeCountsV1(): class SongDetailGradeCountsV1:
dCt: int dCt: int
cCt: int cCt: int
bCt: int bCt: int
@ -413,7 +500,7 @@ class SongDetailGradeCountsV2(SongDetailGradeCountsV1):
def make(self) -> List: def make(self) -> List:
return super().make() + [self.spCt, self.sspCt, self.ssspCt] return super().make() + [self.spCt, self.sspCt, self.ssspCt]
class BestScoreDetailV1(): class BestScoreDetailV1:
songId: int = 0 songId: int = 0
difficulty: int = 1 difficulty: int = 1
clearCounts: SongDetailClearCounts = SongDetailClearCounts() clearCounts: SongDetailClearCounts = SongDetailClearCounts()
@ -446,7 +533,7 @@ class BestScoreDetailV1():
class BestScoreDetailV2(BestScoreDetailV1): class BestScoreDetailV2(BestScoreDetailV1):
gradeCounts: SongDetailGradeCountsV2 = SongDetailGradeCountsV2() gradeCounts: SongDetailGradeCountsV2 = SongDetailGradeCountsV2()
class SongUpdateJudgementCounts(): class SongUpdateJudgementCounts:
marvCt: int marvCt: int
greatCt: int greatCt: int
goodCt: int goodCt: int
@ -461,7 +548,7 @@ class SongUpdateJudgementCounts():
def make(self) -> List: def make(self) -> List:
return [self.marvCt, self.greatCt, self.goodCt, self.missCt] return [self.marvCt, self.greatCt, self.goodCt, self.missCt]
class SongUpdateDetailV1(): class SongUpdateDetailV1:
def __init__(self, data: List) -> None: def __init__(self, data: List) -> None:
if data is not None: if data is not None:
self.songId = data[0] self.songId = data[0]
@ -491,7 +578,7 @@ class SongUpdateDetailV2(SongUpdateDetailV1):
self.slowCt = data[14] self.slowCt = data[14]
self.flagNewRecord = False if data[15] == 0 else True self.flagNewRecord = False if data[15] == 0 else True
class SeasonalInfoV1(): class SeasonalInfoV1:
def __init__(self) -> None: def __init__(self) -> None:
self.level: int = 0 self.level: int = 0
self.wpObtained: int = 0 self.wpObtained: int = 0
@ -525,7 +612,7 @@ class SeasonalInfoV2(SeasonalInfoV1):
def make(self) -> List: def make(self) -> List:
return super().make() + [self.platesObtained, self.cumulativeGatePts] return super().make() + [self.platesObtained, self.cumulativeGatePts]
class BingoPageStatus(): class BingoPageStatus:
id = 0 id = 0
location = 1 location = 1
progress = 0 progress = 0
@ -538,7 +625,7 @@ class BingoPageStatus():
def make(self) -> List: def make(self) -> List:
return [self.id, self.location, self.progress] return [self.id, self.location, self.progress]
class BingoDetail(): class BingoDetail:
def __init__(self, pageNumber: int) -> None: def __init__(self, pageNumber: int) -> None:
self.pageNumber = pageNumber self.pageNumber = pageNumber
self.pageStatus: List[BingoPageStatus] = [] self.pageStatus: List[BingoPageStatus] = []
@ -553,7 +640,7 @@ class BingoDetail():
status status
] ]
class GateDetailV1(): class GateDetailV1:
def __init__(self, gate_id: int = 1, page: int = 1, progress: int = 0, loops: int = 0, last_used: int = 0, mission_flg = 0) -> None: def __init__(self, gate_id: int = 1, page: int = 1, progress: int = 0, loops: int = 0, last_used: int = 0, mission_flg = 0) -> None:
self.id = gate_id self.id = gate_id
self.page = page self.page = page
@ -569,11 +656,11 @@ class GateDetailV2(GateDetailV1):
def make(self) -> List: def make(self) -> List:
return super().make() + [self.missionFlg] return super().make() + [self.missionFlg]
class GachaInfo(): class GachaInfo:
def make(self) -> List: def make(self) -> List:
return [] return []
class LastSongDetail(): class LastSongDetail:
lastSongId = 90 lastSongId = 90
lastSongDiff = 1 lastSongDiff = 1
lastFolderOrd = 1 lastFolderOrd = 1
@ -592,11 +679,11 @@ class LastSongDetail():
return [self.lastSongId, self.lastSongDiff, self.lastFolderOrd, self.lastFolderId, return [self.lastSongId, self.lastSongDiff, self.lastFolderOrd, self.lastFolderId,
self.lastSongOrd] self.lastSongOrd]
class FriendDetail(): class FriendDetail:
def make(self) -> List: def make(self) -> List:
return [] return []
class LoginBonusInfo(): class LoginBonusInfo:
def __init__(self) -> None: def __init__(self) -> None:
self.tickets: List[TicketItem] = [] self.tickets: List[TicketItem] = []
self.items: List[GenericItemRecv] = [] self.items: List[GenericItemRecv] = []
@ -614,7 +701,7 @@ class LoginBonusInfo():
return [ tks, itms, self.message ] return [ tks, itms, self.message ]
class VipLoginBonus(): class VipLoginBonus:
id = 1 id = 1
unknown = 0 unknown = 0
item: GenericItemRecv item: GenericItemRecv
@ -627,7 +714,7 @@ class VipLoginBonus():
def make(self) -> List: def make(self) -> List:
return [ self.id, self.unknown, self.item.make() ] return [ self.id, self.unknown, self.item.make() ]
class VipInfo(): class VipInfo:
def __init__(self, year: int = 2019, month: int = 1, day: int = 1, num_item: int = 1) -> None: def __init__(self, year: int = 2019, month: int = 1, day: int = 1, num_item: int = 1) -> None:
self.pageYear = year self.pageYear = year
self.pageMonth = month self.pageMonth = month
@ -658,7 +745,7 @@ class PlayType(Enum):
PlayTypeCoop = 3 PlayTypeCoop = 3
PlayTypeStageup = 4 PlayTypeStageup = 4
class StageInfo(): class StageInfo:
danId: int = 0 danId: int = 0
danLevel: int = 0 danLevel: int = 0
clearStatus: int = 0 clearStatus: int = 0
@ -692,7 +779,7 @@ class StageupClearType(Enum):
CLEAR_SILVER = 2 CLEAR_SILVER = 2
CLEAR_GOLD = 3 CLEAR_GOLD = 3
class MusicUpdateDetailV1(): class MusicUpdateDetailV1:
def __init__(self) -> None: def __init__(self) -> None:
self.songId = 0 self.songId = 0
self.difficulty = 1 self.difficulty = 1
@ -730,7 +817,7 @@ class MusicUpdateDetailV3(MusicUpdateDetailV2):
super().__init__() super().__init__()
self.grades = SongDetailGradeCountsV2() self.grades = SongDetailGradeCountsV2()
class SongRatingUpdate(): class SongRatingUpdate:
def __init__(self, song_id: int = 0, difficulty: int = 1, new_rating: int = 0) -> None: def __init__(self, song_id: int = 0, difficulty: int = 1, new_rating: int = 0) -> None:
self.songId = song_id self.songId = song_id
self.difficulty = difficulty self.difficulty = difficulty
@ -743,7 +830,7 @@ class SongRatingUpdate():
self.rating, self.rating,
] ]
class GateTutorialFlag(): class GateTutorialFlag:
def __init__(self, tutorial_id: int = 1, flg_watched: bool = False) -> None: def __init__(self, tutorial_id: int = 1, flg_watched: bool = False) -> None:
self.tutorialId = tutorial_id self.tutorialId = tutorial_id
self.flagWatched = flg_watched self.flagWatched = flg_watched
@ -753,3 +840,11 @@ class GateTutorialFlag():
self.tutorialId, self.tutorialId,
int(self.flagWatched) int(self.flagWatched)
] ]
class DateUpdate:
def __init__(self, date_id: int = 0, timestamp: int = 0) -> None:
self.id = date_id
self.timestamp = timestamp
def make(self) -> List:
return [self.id, self.timestamp]

View File

@ -2,6 +2,7 @@ from typing import List, Dict
from titles.wacca.handlers.base import BaseRequest, BaseResponse from titles.wacca.handlers.base import BaseRequest, BaseResponse
from titles.wacca.handlers.helpers import HousingInfo from titles.wacca.handlers.helpers import HousingInfo
from titles.wacca.const import WaccaConstants
# ---housing/get---- # ---housing/get----
class HousingGetResponse(BaseResponse): class HousingGetResponse(BaseResponse):
@ -37,12 +38,22 @@ class HousingStartRequestV2(HousingStartRequestV1):
self.info.append(HousingInfo(info[0], info[1])) self.info.append(HousingInfo(info[0], info[1]))
class HousingStartResponseV1(BaseResponse): class HousingStartResponseV1(BaseResponse):
def __init__(self, regionId: int, songList: List[int]) -> None: def __init__(self, regionId: WaccaConstants.Region = WaccaConstants.Region.HOKKAIDO, songList: List[int] = []) -> None:
super().__init__() super().__init__()
self.regionId = regionId self.regionId = regionId
self.songList = songList self.songList = songList # Recomended songs
if not self.songList:
self.songList = [
1269,1007,1270,1002,1020,1003,1008,1211,1018,1092,1056,32,
1260,1230,1258,1251,2212,1264,1125,1037,2001,1272,1126,1119,
1104,1070,1047,1044,1027,1004,1001,24,2068,2062,2021,1275,
1249,1207,1203,1107,1021,1009,9,4,3,23,22,2014,13,1276,1247,
1240,1237,1128,1114,1110,1109,1102,1045,1043,1036,1035,1030,
1023,1015
]
def make(self) -> Dict: def make(self) -> Dict:
self.params = [self.regionId, self.songList] self.params = [self.regionId.value, self.songList]
return super().make() return super().make()

View File

@ -1,7 +1,7 @@
from typing import List, Dict from typing import List, Dict
from titles.wacca.handlers.base import BaseRequest, BaseResponse from titles.wacca.handlers.base import BaseRequest, BaseResponse
from titles.wacca.handlers.helpers import UserOption from titles.wacca.handlers.helpers import UserOption, DateUpdate
# ---user/info/update--- # ---user/info/update---
class UserInfoUpdateRequest(BaseRequest): class UserInfoUpdateRequest(BaseRequest):
@ -9,12 +9,16 @@ class UserInfoUpdateRequest(BaseRequest):
super().__init__(data) super().__init__(data)
self.profileId = int(self.params[0]) self.profileId = int(self.params[0])
self.optsUpdated: List[UserOption] = [] self.optsUpdated: List[UserOption] = []
self.datesUpdated: List = self.params[3] self.unknown2: List = self.params[2]
self.datesUpdated: List[DateUpdate] = []
self.favoritesAdded: List[int] = self.params[4] self.favoritesAdded: List[int] = self.params[4]
self.favoritesRemoved: List[int] = self.params[5] self.favoritesRemoved: List[int] = self.params[5]
for x in self.params[2]: for x in self.params[1]:
self.optsUpdated.append(UserOption(x[0], x[1])) self.optsUpdated.append(UserOption(x[0], x[1]))
for x in self.params[3]:
self.datesUpdated.append(DateUpdate(x[0], x[1]))
# ---user/info/getMyroom--- TODO: Understand this better # ---user/info/getMyroom--- TODO: Understand this better
class UserInfogetMyroomRequest(BaseRequest): class UserInfogetMyroomRequest(BaseRequest):

View File

@ -19,7 +19,7 @@ class UserStatusGetV1Response(BaseResponse):
self.setIconId: int = 0 self.setIconId: int = 0
self.profileStatus: ProfileStatus = ProfileStatus.ProfileGood self.profileStatus: ProfileStatus = ProfileStatus.ProfileGood
self.versionStatus: PlayVersionStatus = PlayVersionStatus.VersionGood self.versionStatus: PlayVersionStatus = PlayVersionStatus.VersionGood
self.lastGameVersion: str = "" self.lastGameVersion: ShortVersion = ShortVersion()
def make(self) -> Dict: def make(self) -> Dict:
self.params = [ self.params = [
@ -29,7 +29,7 @@ class UserStatusGetV1Response(BaseResponse):
self.profileStatus.value, self.profileStatus.value,
[ [
self.versionStatus.value, self.versionStatus.value,
self.lastGameVersion str(self.lastGameVersion)
] ]
] ]
@ -275,11 +275,6 @@ class UserStatusUpdateRequestV1(BaseRequest):
self.itemsRecieved.append(GenericItemRecv(itm[0], itm[1], itm[2])) self.itemsRecieved.append(GenericItemRecv(itm[0], itm[1], itm[2]))
class UserStatusUpdateRequestV2(UserStatusUpdateRequestV1): class UserStatusUpdateRequestV2(UserStatusUpdateRequestV1):
isContinue = False
isFirstPlayFree = False
itemsUsed = []
lastSongInfo: LastSongDetail
def __init__(self, data: Dict) -> None: def __init__(self, data: Dict) -> None:
super().__init__(data) super().__init__(data)
self.isContinue = bool(data["params"][3]) self.isContinue = bool(data["params"][3])

View File

@ -18,6 +18,7 @@ from titles.wacca.lily import WaccaLily
from titles.wacca.s import WaccaS from titles.wacca.s import WaccaS
from titles.wacca.base import WaccaBase from titles.wacca.base import WaccaBase
from titles.wacca.handlers.base import BaseResponse from titles.wacca.handlers.base import BaseResponse
from titles.wacca.handlers.helpers import Version
class WaccaServlet(): class WaccaServlet():
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
@ -55,12 +56,10 @@ class WaccaServlet():
hash = md5(json.dumps(resp, ensure_ascii=False).encode()).digest() hash = md5(json.dumps(resp, ensure_ascii=False).encode()).digest()
request.responseHeaders.addRawHeader(b"X-Wacca-Hash", hash.hex().encode()) request.responseHeaders.addRawHeader(b"X-Wacca-Hash", hash.hex().encode())
return json.dumps(resp).encode() return json.dumps(resp).encode()
version_full = []
try: try:
req_json = json.loads(request.content.getvalue()) req_json = json.loads(request.content.getvalue())
version_full = req_json["appVersion"].split(".") version_full = Version(req_json["appVersion"])
except: except:
self.logger.error(f"Failed to parse request toi {request.uri} -> {request.content.getvalue()}") self.logger.error(f"Failed to parse request toi {request.uri} -> {request.content.getvalue()}")
resp = BaseResponse() resp = BaseResponse()
@ -76,7 +75,7 @@ class WaccaServlet():
func_to_find += f"{url_split[x + start_req_idx]}_" func_to_find += f"{url_split[x + start_req_idx]}_"
func_to_find += "request" func_to_find += "request"
ver_search = (int(version_full[0]) * 10000) + (int(version_full[1]) * 100) + int(version_full[2]) ver_search = int(version_full)
if ver_search < 15000: if ver_search < 15000:
internal_ver = WaccaConstants.VER_WACCA internal_ver = WaccaConstants.VER_WACCA

View File

@ -17,20 +17,20 @@ class WaccaLily(WaccaS):
self.OPTIONS_DEFAULTS["set_nav_id"] = 210002 self.OPTIONS_DEFAULTS["set_nav_id"] = 210002
self.allowed_stages = [ self.allowed_stages = [
(2001, 1),
(2002, 2),
(2003, 3),
(2004, 4),
(2005, 5),
(2006, 6),
(2007, 7),
(2008, 8),
(2009, 9),
(2010, 10),
(2011, 11),
(2012, 12),
(2013, 13),
(2014, 14), (2014, 14),
(2013, 13),
(2012, 12),
(2011, 11),
(2010, 10),
(2009, 9),
(2008, 8),
(2007, 7),
(2006, 6),
(2005, 5),
(2004, 4),
(2003, 3),
(2002, 2),
(2001, 1),
(210001, 0), (210001, 0),
(210002, 0), (210002, 0),
(210003, 0), (210003, 0),
@ -42,24 +42,26 @@ class WaccaLily(WaccaS):
def handle_housing_start_request(self, data: Dict) -> Dict: def handle_housing_start_request(self, data: Dict) -> Dict:
req = HousingStartRequestV2(data) req = HousingStartRequestV2(data)
if req.appVersion.country != "JPN" and req.appVersion.country in [region.name for region in WaccaConstants.Region]:
region_id = WaccaConstants.Region[req.appVersion.country]
else:
region_id = self.region_id
resp = HousingStartResponseV1( resp = HousingStartResponseV1(region_id)
1,
[ # Recomended songs
1269,1007,1270,1002,1020,1003,1008,1211,1018,1092,1056,32,
1260,1230,1258,1251,2212,1264,1125,1037,2001,1272,1126,1119,
1104,1070,1047,1044,1027,1004,1001,24,2068,2062,2021,1275,
1249,1207,1203,1107,1021,1009,9,4,3,23,22,2014,13,1276,1247,
1240,1237,1128,1114,1110,1109,1102,1045,1043,1036,1035,1030,
1023,1015
]
)
return resp.make() return resp.make()
def handle_user_status_create_request(self, data: Dict)-> Dict:
req = UserStatusCreateRequest(data)
resp = super().handle_user_status_create_request(data)
self.data.item.put_item(req.aimeId, WaccaConstants.ITEM_TYPES["navigator"], 210002) # Lily, Added Lily
return resp
def handle_user_status_get_request(self, data: Dict)-> Dict: def handle_user_status_get_request(self, data: Dict)-> Dict:
req = UserStatusGetRequest(data) req = UserStatusGetRequest(data)
resp = UserStatusGetV2Response() resp = UserStatusGetV2Response()
ver_split = req.appVersion.split(".")
profile = self.data.profile.get_profile(aime_id=req.aimeId) profile = self.data.profile.get_profile(aime_id=req.aimeId)
if profile is None: if profile is None:
@ -69,11 +71,9 @@ class WaccaLily(WaccaS):
self.logger.info(f"User preview for {req.aimeId} from {req.chipId}") self.logger.info(f"User preview for {req.aimeId} from {req.chipId}")
if profile["last_game_ver"] is None: if profile["last_game_ver"] is None:
profile_ver_split = ver_split resp.lastGameVersion = ShortVersion(str(req.appVersion))
resp.lastGameVersion = req.appVersion
else: else:
profile_ver_split = profile["last_game_ver"].split(".") resp.lastGameVersion = ShortVersion(profile["last_game_ver"])
resp.lastGameVersion = profile["last_game_ver"]
resp.userStatus.userId = profile["id"] resp.userStatus.userId = profile["id"]
resp.userStatus.username = profile["username"] resp.userStatus.username = profile["username"]
@ -103,26 +103,11 @@ class WaccaLily(WaccaS):
if profile["last_login_date"].timestamp() < int((datetime.now().replace(hour=0,minute=0,second=0,microsecond=0) - timedelta(days=1)).timestamp()): if profile["last_login_date"].timestamp() < int((datetime.now().replace(hour=0,minute=0,second=0,microsecond=0) - timedelta(days=1)).timestamp()):
resp.userStatus.loginConsecutiveDays = 0 resp.userStatus.loginConsecutiveDays = 0
if int(ver_split[0]) > int(profile_ver_split[0]): if req.appVersion > resp.lastGameVersion:
resp.versionStatus = PlayVersionStatus.VersionUpgrade resp.versionStatus = PlayVersionStatus.VersionUpgrade
elif int(ver_split[0]) < int(profile_ver_split[0]):
resp.versionStatus = PlayVersionStatus.VersionTooNew
else: elif req.appVersion < resp.lastGameVersion:
if int(ver_split[1]) > int(profile_ver_split[1]): resp.versionStatus = PlayVersionStatus.VersionTooNew
resp.versionStatus = PlayVersionStatus.VersionUpgrade
elif int(ver_split[1]) < int(profile_ver_split[1]):
resp.versionStatus = PlayVersionStatus.VersionTooNew
else:
if int(ver_split[2]) > int(profile_ver_split[2]):
resp.versionStatus = PlayVersionStatus.VersionUpgrade
elif int(ver_split[2]) < int(profile_ver_split[2]):
resp.versionStatus = PlayVersionStatus.VersionTooNew
if profile["vip_expire_time"] is not None: if profile["vip_expire_time"] is not None:
resp.userStatus.vipExpireTime = int(profile["vip_expire_time"].timestamp()) resp.userStatus.vipExpireTime = int(profile["vip_expire_time"].timestamp())
@ -178,8 +163,7 @@ class WaccaLily(WaccaS):
def handle_user_status_getDetail_request(self, data: Dict)-> Dict: def handle_user_status_getDetail_request(self, data: Dict)-> Dict:
req = UserStatusGetDetailRequest(data) req = UserStatusGetDetailRequest(data)
ver_split = req.appVersion.split(".") if req.appVersion.minor >= 53:
if int(ver_split[1]) >= 53:
resp = UserStatusGetDetailResponseV3() resp = UserStatusGetDetailResponseV3()
else: else:
resp = UserStatusGetDetailResponseV2() resp = UserStatusGetDetailResponseV2()
@ -252,7 +236,7 @@ class WaccaLily(WaccaS):
for user_gate in profile_gates: for user_gate in profile_gates:
if user_gate["gate_id"] == gate: if user_gate["gate_id"] == gate:
if int(ver_split[1]) >= 53: if req.appVersion.minor >= 53:
resp.gateInfo.append(GateDetailV2(user_gate["gate_id"],user_gate["page"],user_gate["progress"], resp.gateInfo.append(GateDetailV2(user_gate["gate_id"],user_gate["page"],user_gate["progress"],
user_gate["loops"],int(user_gate["last_used"].timestamp()),user_gate["mission_flag"])) user_gate["loops"],int(user_gate["last_used"].timestamp()),user_gate["mission_flag"]))
@ -266,7 +250,7 @@ class WaccaLily(WaccaS):
break break
if not added_gate: if not added_gate:
if int(ver_split[1]) >= 53: if req.appVersion.minor >= 53:
resp.gateInfo.append(GateDetailV2(gate)) resp.gateInfo.append(GateDetailV2(gate))
else: else:

View File

@ -16,20 +16,20 @@ class WaccaLilyR(WaccaLily):
self.OPTIONS_DEFAULTS["set_nav_id"] = 210002 self.OPTIONS_DEFAULTS["set_nav_id"] = 210002
self.allowed_stages = [ self.allowed_stages = [
(2501, 1),
(2502, 2),
(2503, 3),
(2504, 4),
(2505, 5),
(2506, 6),
(2507, 7),
(2508, 8),
(2509, 9),
(2510, 10),
(2511, 11),
(2512, 12),
(2513, 13),
(2514, 14), (2514, 14),
(2513, 13),
(2512, 12),
(2511, 11),
(2510, 10),
(2509, 9),
(2508, 8),
(2507, 7),
(2506, 6),
(2505, 5),
(2504, 4),
(2503, 3),
(2501, 2),
(2501, 1),
(210001, 0), (210001, 0),
(210002, 0), (210002, 0),
(210003, 0), (210003, 0),

View File

@ -18,20 +18,20 @@ class WaccaReverse(WaccaLilyR):
self.OPTIONS_DEFAULTS["set_nav_id"] = 310001 self.OPTIONS_DEFAULTS["set_nav_id"] = 310001
self.allowed_stages = [ self.allowed_stages = [
(3001, 1),
(3002, 2),
(3003, 3),
(3004, 4),
(3005, 5),
(3006, 6),
(3007, 7),
(3008, 8),
(3009, 9),
(3010, 10),
(3011, 11),
(3012, 12),
(3013, 13),
(3014, 14), (3014, 14),
(3013, 13),
(3012, 12),
(3011, 11),
(3010, 10),
(3009, 9),
(3008, 8),
(3007, 7),
(3006, 6),
(3005, 5),
(3004, 4),
(3003, 3),
(3002, 2),
(3001, 1),
# Touhou # Touhou
(210001, 0), (210001, 0),
(210002, 0), (210002, 0),

View File

@ -11,19 +11,19 @@ from titles.wacca.handlers import *
class WaccaS(WaccaBase): class WaccaS(WaccaBase):
allowed_stages = [ allowed_stages = [
(1501, 1),
(1502, 2),
(1503, 3),
(1504, 4),
(1505, 5),
(1506, 6),
(1507, 7),
(1508, 8),
(1509, 9),
(1510, 10),
(1511, 11),
(1512, 12),
(1513, 13), (1513, 13),
(1512, 12),
(1511, 11),
(1510, 10),
(1509, 9),
(1508, 8),
(1507, 7),
(1506, 6),
(1505, 5),
(1514, 4),
(1513, 3),
(1512, 2),
(1511, 1),
] ]
def __init__(self, cfg: CoreConfig, game_cfg: WaccaConfig) -> None: def __init__(self, cfg: CoreConfig, game_cfg: WaccaConfig) -> None:

View File

@ -211,7 +211,7 @@ class WaccaProfileData(BaseData):
) )
conflict = sql.on_duplicate_key_update( conflict = sql.on_duplicate_key_update(
value = sql.inserted.value value = value
) )
result = self.execute(conflict) result = self.execute(conflict)