1
0
forked from Hay1tsme/artemis

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.data import Data
from core.utils import Utils
from core.const import *
class AllnetServlet:
def __init__(self, core_cfg: CoreConfig, cfg_folder: str):
@ -115,6 +116,7 @@ class AllnetServlet:
else:
resp = AllnetPowerOnResponse2()
self.logger.debug(f"Allnet request: {vars(req)}")
if req.game_id not in self.uri_registry:
msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}."
self.data.base.log_event("allnet", "ALLNET_AUTH_UNKNOWN_GAME", logging.WARN, msg)
@ -136,15 +138,19 @@ class AllnetServlet:
if machine is not None:
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.allnet_id = machine["id"]
resp.name = arcade["name"]
resp.nickname = arcade["nickname"]
resp.region0 = arcade["region_id"]
resp.region_name0 = arcade["country"]
resp.region_name1 = arcade["state"]
resp.region_name2 = arcade["city"]
resp.name = arcade["name"] if arcade["name"] is not None else ""
resp.nickname = arcade["nickname"] if arcade["nickname"] is not None else ""
resp.region0 = arcade["region_id"] if arcade["region_id"] is not None else AllnetJapanRegionId.AICHI.value
resp.region_name0 = arcade["country"] if arcade["country"] is not None else AllnetCountryCode.JAPAN.value
resp.region_name1 = arcade["state"] if arcade["state"] is not None else AllnetJapanRegionId.AICHI.name
resp.region_name2 = arcade["city"] if arcade["city"] is not None else ""
resp.client_timezone = arcade["timezone"] if arcade["timezone"] is not None else "+0900"
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}"
self.data.base.log_event("allnet", "ALLNET_AUTH_SUCCESS", logging.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")
@ -191,7 +198,7 @@ class AllnetServlet:
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)
digest = SHA.new()

View File

@ -32,5 +32,61 @@ class KeychipPlatformsCodes():
NUSX = ("A61X", "A69X")
ALLS = "A63E"
class RegionIDs(Enum):
pass
class AllnetCountryCode(Enum):
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.sql import func, select
from sqlalchemy.dialects.mysql import insert
import re
from core.data.schema.base import BaseData, metadata
from core.const import *
arcade = Table(
"arcade",
@ -50,9 +52,20 @@ arcade_owner = Table(
class ArcadeData(BaseData):
def get_machine(self, serial: str = None, id: int = None) -> Optional[Dict]:
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:
sql = machine.select(machine.c.id == id)
else:
self.logger.error(f"{__name__ }: Need either serial or ID to look up!")
return None
@ -61,20 +74,28 @@ class ArcadeData(BaseData):
if result is None: return None
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:
self.logger.error(f"{__name__ }: Need arcade id!")
return None
if serial is None:
pass
sql = machine.insert().values(arcade = arcade_id, keychip = serial, board = board, game = game, is_cab = is_cab)
result = self.execute(sql)
if result is None: return None
return result.lastrowid
def set_machine_serial(self, machine_id: int, serial: str) -> None:
result = self.execute(machine.update(machine.c.id == machine_id).values(keychip = serial))
if result is None:
self.logger.error(f"Failed to update serial for machine {machine_id} -> {serial}")
return result.lastrowid
def set_machine_boardid(self, machine_id: int, boardid: str) -> None:
result = self.execute(machine.update(machine.c.id == machine_id).values(board = boardid))
if result is None:
self.logger.error(f"Failed to update board id for machine {machine_id} -> {boardid}")
def get_arcade(self, id: int) -> Optional[Dict]:
sql = arcade.select(arcade.c.id == id)
result = self.execute(sql)
@ -109,5 +130,31 @@ class ArcadeData(BaseData):
if result is None: return None
return result.lastrowid
def generate_keychip_serial(self, platform_id: int) -> str:
pass
def format_serial(self, platform_code: str, platform_rev: int, serial_num: int, append: int = 4152) -> str:
return f"{platform_code}{platform_rev:02d}A{serial_num:04d}{append:04d}" # 0x41 = A, 0x52 = R
def validate_keychip_format(self, serial: str) -> bool:
serial = serial.replace("-", "")
if len(serial) != 11 or len(serial) != 15:
self.logger.error(f"Serial validate failed: Incorrect length for {serial} (len {len(serial)})")
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"]
if code not in self.title_registry:
self.logger.warn(f"Unknown game code {code}")
request.setResponseCode(404)
return b""
index = self.title_registry[code]
if not hasattr(index, "render_GET"):
self.logger.warn(f"{code} does not dispatch GET")
request.setResponseCode(405)
return b""
return index.render_GET(request, endpoints["version"], endpoints["endpoint"])
@ -65,10 +68,13 @@ class TitleServlet():
code = endpoints["game"]
if code not in self.title_registry:
self.logger.warn(f"Unknown game code {code}")
request.setResponseCode(404)
return b""
index = self.title_registry[code]
if not hasattr(index, "render_POST"):
self.logger.warn(f"{code} does not dispatch POST")
request.setResponseCode(405)
return b""
return index.render_POST(request, int(endpoints["version"]), endpoints["endpoint"])

View File

@ -33,15 +33,4 @@ if __name__=='__main__':
else:
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")

View File

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

View File

@ -57,7 +57,7 @@ title:
## 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):
> 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
> 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
server {
listen 80;
@ -134,6 +114,7 @@ server {
# Frontend HTTPS. Comment out if you on't intend to use the frontend
server {
listen 443 ssl;
server_name frontend.hostname.here;
ssl_certificate /path/to/cert/frontend.pem;
ssl_certificate_key /path/to/cert/frontend.key;

View File

@ -1,6 +1,7 @@
server:
enable: True
loglevel: "info"
prefecture_name: "Hokkaido"
mods:
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_downloadorder', '/sys/servlet/DownloadOrder', controller="allnet", action='handle_dlorder', conditions=dict(method=['POST']))
self.map_post.connect('allnet_billing', '/request', controller="allnet", action='handle_billing_request', conditions=dict(method=['POST']))
self.map_post.connect('allnet_billing', '/request/', controller="allnet", action='handle_billing_request', conditions=dict(method=['POST']))
self.map_post.connect('mucha_boardauth', '/mucha/boardauth.do', controller="mucha", action='handle_boardauth', conditions=dict(method=['POST']))
self.map_post.connect('mucha_updatacheck', '/mucha/updatacheck.do', controller="mucha", action='handle_updatacheck', conditions=dict(method=['POST']))

View File

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

View File

@ -135,7 +135,8 @@ class ChuniServlet():
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"
@ -154,7 +155,7 @@ class ChuniServlet():
if resp == None:
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"))

View File

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

View File

@ -788,6 +788,10 @@ class OngekiBase():
for x in upsert["userChapterList"]:
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:
for x in upsert["userItemList"]:
self.data.item.put_item(user_id, x)

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_PLUS = 5
VER_ONGEKI_BRIGHT = 6
VER_ONGEKI_BRIGHT_MEMORY = 7
EVT_TYPES: Enum = Enum('EVT_TYPES', [
'None',
@ -43,7 +44,7 @@ class OngekiConstants():
Lunatic = 10
VERSION_NAMES = ("ONGEKI", "ONGEKI+", "ONGEKI Summer", "ONGEKI Summer+", "ONGEKI Red", "ONGEKI Red+",
"ONGEKI Bright")
"ONGEKI Bright", "ONGEKI Bright Memory")
@classmethod
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.redplus import OngekiRedPlus
from titles.ongeki.bright import OngekiBright
from titles.ongeki.brightmemory import OngekiBrightMemory
class OngekiServlet():
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
@ -32,6 +33,7 @@ class OngekiServlet():
OngekiRed(core_cfg, self.game_cfg),
OngekiRedPlus(core_cfg, self.game_cfg),
OngekiBright(core_cfg, self.game_cfg),
OngekiBrightMemory(core_cfg, self.game_cfg),
]
self.logger = logging.getLogger("ongeki")
@ -69,8 +71,10 @@ class OngekiServlet():
internal_ver = OngekiConstants.VER_ONGEKI_RED
elif version >= 125 and version < 130: # 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
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 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'
)
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(
"ongeki_user_item",
metadata,
@ -524,3 +545,22 @@ class OngekiItemData(BaseData):
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
return result.fetchall()

View File

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

View File

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

View File

@ -1,15 +1,15 @@
from typing import Any, List, Dict
import logging
import inflection
from math import floor
from datetime import datetime, timedelta
from core.config import CoreConfig
from titles.wacca.config import WaccaConfig
from titles.wacca.const import WaccaConstants
from titles.wacca.database import WaccaData
from titles.wacca.handlers import *
from core.const import AllnetCountryCode
class WaccaBase():
def __init__(self, cfg: CoreConfig, game_cfg: WaccaConfig) -> None:
@ -23,54 +23,62 @@ class WaccaBase():
self.season = 1
self.OPTIONS_DEFAULTS: Dict[str, Any] = {
"note_speed": 5,
"field_mask": 0,
"note_sound": 105001,
"note_color": 203001,
"bgm_volume": 10,
"bg_video": 0,
"note_speed": 5,
"field_mask": 0,
"note_sound": 105001,
"note_color": 203001,
"bgm_volume": 10,
"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,
"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,
"right_slide_note_color": 3,
"forward_slide_note_color": 1,
"back_slide_note_color": 2,
"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
}
"master_vol": 3,
"set_title_id": 104001,
"set_icon_id": 102001,
"set_nav_id": 210001,
"set_plate_id": 211001
}
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:
req = BaseRequest(data)
housing_id = 1337
@ -85,17 +93,30 @@ class WaccaBase():
def handle_housing_start_request(self, data: Dict) -> Dict:
req = HousingStartRequestV1(data)
resp = HousingStartResponseV1(
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
]
)
machine = self.data.arcade.get_machine(req.chipId)
if machine is not None:
arcade = self.data.arcade.get_arcade(machine["arcade"])
allnet_region_id = arcade["region_id"]
if req.appVersion.country == AllnetCountryCode.JAPAN.value:
if allnet_region_id is not None:
region = WaccaConstants.allnet_region_id_to_wacca_region(allnet_region_id)
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()
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:
req = UserStatusGetRequest(data)
resp = UserStatusGetV1Response()
ver_split = req.appVersion.split(".")
profile = self.data.profile.get_profile(aime_id=req.aimeId)
if profile is None:
@ -118,14 +138,11 @@ class WaccaBase():
resp.profileStatus = ProfileStatus.ProfileRegister
return resp.make()
self.logger.info(f"User preview for {req.aimeId} from {req.chipId}")
if profile["last_game_ver"] is None:
profile_ver_split = ver_split
resp.lastGameVersion = req.appVersion
resp.lastGameVersion = ShortVersion(str(req.appVersion))
else:
profile_ver_split = profile["last_game_ver"].split(".")
resp.lastGameVersion = profile["last_game_ver"]
resp.lastGameVersion = ShortVersion(profile["last_game_ver"])
resp.userStatus.userId = profile["id"]
resp.userStatus.username = profile["username"]
@ -145,28 +162,12 @@ class WaccaBase():
set_icon_id = self.OPTIONS_DEFAULTS["set_icon_id"]
resp.setIconId = set_icon_id
if int(ver_split[0]) > int(profile_ver_split[0]):
if req.appVersion > resp.lastGameVersion:
resp.versionStatus = PlayVersionStatus.VersionUpgrade
elif int(ver_split[0]) < int(profile_ver_split[0]):
elif req.appVersion < resp.lastGameVersion:
resp.versionStatus = PlayVersionStatus.VersionTooNew
else:
if int(ver_split[1]) > int(profile_ver_split[1]):
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()
def handle_user_status_login_request(self, data: Dict)-> Dict:
@ -375,12 +376,12 @@ class WaccaBase():
return resp.make()
self.logger.info(f"Get trial info for user {req.profileId}")
stages = self.data.score.get_stageup(user_id, self.version)
if stages is None:
stages = []
add_next = True
tmp: List[StageInfo] = []
for d in self.allowed_stages:
stage_info = StageInfo(d[0], d[1])
@ -393,11 +394,13 @@ class WaccaBase():
stage_info.song3BestScore = score["song3_score"]
break
if add_next or stage_info.danLevel < 9:
resp.stageList.append(stage_info)
tmp.append(stage_info)
if stage_info.danLevel >= 9 and stage_info.clearStatus < 1:
add_next = False
for x in range(len(tmp)):
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()
@ -763,7 +766,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.id, opt.val)
self.data.profile.update_option(user_id, opt.opt_id, opt.opt_val)
for update in req.datesUpdated:
pass

View File

@ -13,6 +13,10 @@ class WaccaServerConfig():
def loglevel(self) -> int:
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():
def __init__(self, parent_config: "WaccaConfig") -> None:
self.__config = parent_config

View File

@ -1,4 +1,7 @@
from enum import Enum
from typing import Optional
from core.const import AllnetJapanRegionId
class WaccaConstants():
CONFIG_NAME = "wacca.yaml"
@ -95,19 +98,92 @@ class WaccaConstants():
"set_plate_id": 1005, # ID
}
DIFFICULTIES = {
"Normal": 1,
"Hard": 2,
"Expert": 3,
"Inferno": 4,
}
class Difficulty(Enum):
NORMAL = 1
HARD = 2
EXPERT = 3
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
def game_ver_to_string(cls, ver: int):
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 titles.wacca.handlers.helpers import Version
from datetime import datetime
class BaseRequest():
def __init__(self, data: Dict) -> None:
self.requestNo: int = data["requestNo"]
self.appVersion: str = data["appVersion"]
self.appVersion: Version = Version(data["appVersion"])
self.boardId: str = data["boardId"]
self.chipId: str = data["chipId"]
self.params: List = data["params"]

View File

@ -3,7 +3,94 @@ from enum import Enum
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
"""
@ -17,7 +104,7 @@ class HousingInfo():
def make(self) -> List:
return [ self.id, self.val ]
class Notice():
class Notice:
name: str = ""
title: str = ""
message: str = ""
@ -40,7 +127,7 @@ class Notice():
return [ self.name, self.title, self.message, self.unknown3, self.unknown4, int(self.showTitleScreen),
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:
self.opt_id = opt_id
self.opt_val = opt_val
@ -48,7 +135,7 @@ class UserOption():
def make(self) -> List:
return [self.opt_id, self.opt_val]
class UserStatusV1():
class UserStatusV1:
def __init__(self) -> None:
self.userId: int = 0
self.username: str = ""
@ -106,7 +193,7 @@ class PlayVersionStatus(Enum):
VersionTooNew = 1
VersionUpgrade = 2
class PlayModeCounts():
class PlayModeCounts:
seasonId: int = 0
modeId: int = 0
playNum: int = 0
@ -123,7 +210,7 @@ class PlayModeCounts():
self.playNum
]
class SongUnlock():
class SongUnlock:
songId: int = 0
difficulty: int = 0
whenAppeared: int = 0
@ -143,7 +230,7 @@ class SongUnlock():
self.whenUnlocked
]
class GenericItemRecv():
class GenericItemRecv:
def __init__(self, item_type: int = 1, item_id: int = 1, quantity: int = 1) -> None:
self.itemId = item_id
self.itemType = item_type
@ -152,7 +239,7 @@ class GenericItemRecv():
def make(self) -> List:
return [ self.itemType, self.itemId, self.quantity ]
class GenericItemSend():
class GenericItemSend:
def __init__(self, itemId: int, itemType: int, whenAcquired: int) -> None:
self.itemId = itemId
self.itemType = itemType
@ -180,7 +267,7 @@ class IconItem(GenericItemSend):
self.whenAcquired
]
class TrophyItem():
class TrophyItem:
trophyId: int = 0
season: int = 1
progress: int = 0
@ -200,7 +287,7 @@ class TrophyItem():
self.badgeType
]
class TicketItem():
class TicketItem:
userTicketId: int = 0
ticketId: int = 0
whenExpires: int = 0
@ -233,7 +320,7 @@ class NavigatorItem(IconItem):
self.usesToday
]
class SkillItem():
class SkillItem:
skill_type: int
level: int
flag: int
@ -247,7 +334,7 @@ class SkillItem():
self.badge
]
class UserItemInfoV1():
class UserItemInfoV1:
def __init__(self) -> None:
self.songUnlocks: List[SongUnlock] = []
self.titles: List[GenericItemSend] = []
@ -331,7 +418,7 @@ class UserItemInfoV3(UserItemInfoV2):
ret.append(effect)
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,
am_ct: int = 0, counts: Optional[List[int]] = None) -> None:
if counts is None:
@ -351,7 +438,7 @@ class SongDetailClearCounts():
def make(self) -> List:
return [self.playCt, self.clearCt, self.misslessCt, self.fullComboCt, self.allMarvelousCt]
class SongDetailGradeCountsV1():
class SongDetailGradeCountsV1:
dCt: int
cCt: int
bCt: int
@ -413,7 +500,7 @@ class SongDetailGradeCountsV2(SongDetailGradeCountsV1):
def make(self) -> List:
return super().make() + [self.spCt, self.sspCt, self.ssspCt]
class BestScoreDetailV1():
class BestScoreDetailV1:
songId: int = 0
difficulty: int = 1
clearCounts: SongDetailClearCounts = SongDetailClearCounts()
@ -446,7 +533,7 @@ class BestScoreDetailV1():
class BestScoreDetailV2(BestScoreDetailV1):
gradeCounts: SongDetailGradeCountsV2 = SongDetailGradeCountsV2()
class SongUpdateJudgementCounts():
class SongUpdateJudgementCounts:
marvCt: int
greatCt: int
goodCt: int
@ -461,7 +548,7 @@ class SongUpdateJudgementCounts():
def make(self) -> List:
return [self.marvCt, self.greatCt, self.goodCt, self.missCt]
class SongUpdateDetailV1():
class SongUpdateDetailV1:
def __init__(self, data: List) -> None:
if data is not None:
self.songId = data[0]
@ -491,7 +578,7 @@ class SongUpdateDetailV2(SongUpdateDetailV1):
self.slowCt = data[14]
self.flagNewRecord = False if data[15] == 0 else True
class SeasonalInfoV1():
class SeasonalInfoV1:
def __init__(self) -> None:
self.level: int = 0
self.wpObtained: int = 0
@ -525,7 +612,7 @@ class SeasonalInfoV2(SeasonalInfoV1):
def make(self) -> List:
return super().make() + [self.platesObtained, self.cumulativeGatePts]
class BingoPageStatus():
class BingoPageStatus:
id = 0
location = 1
progress = 0
@ -538,7 +625,7 @@ class BingoPageStatus():
def make(self) -> List:
return [self.id, self.location, self.progress]
class BingoDetail():
class BingoDetail:
def __init__(self, pageNumber: int) -> None:
self.pageNumber = pageNumber
self.pageStatus: List[BingoPageStatus] = []
@ -553,7 +640,7 @@ class BingoDetail():
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:
self.id = gate_id
self.page = page
@ -569,11 +656,11 @@ class GateDetailV2(GateDetailV1):
def make(self) -> List:
return super().make() + [self.missionFlg]
class GachaInfo():
class GachaInfo:
def make(self) -> List:
return []
class LastSongDetail():
class LastSongDetail:
lastSongId = 90
lastSongDiff = 1
lastFolderOrd = 1
@ -592,11 +679,11 @@ class LastSongDetail():
return [self.lastSongId, self.lastSongDiff, self.lastFolderOrd, self.lastFolderId,
self.lastSongOrd]
class FriendDetail():
class FriendDetail:
def make(self) -> List:
return []
class LoginBonusInfo():
class LoginBonusInfo:
def __init__(self) -> None:
self.tickets: List[TicketItem] = []
self.items: List[GenericItemRecv] = []
@ -614,7 +701,7 @@ class LoginBonusInfo():
return [ tks, itms, self.message ]
class VipLoginBonus():
class VipLoginBonus:
id = 1
unknown = 0
item: GenericItemRecv
@ -627,7 +714,7 @@ class VipLoginBonus():
def make(self) -> List:
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:
self.pageYear = year
self.pageMonth = month
@ -658,7 +745,7 @@ class PlayType(Enum):
PlayTypeCoop = 3
PlayTypeStageup = 4
class StageInfo():
class StageInfo:
danId: int = 0
danLevel: int = 0
clearStatus: int = 0
@ -692,7 +779,7 @@ class StageupClearType(Enum):
CLEAR_SILVER = 2
CLEAR_GOLD = 3
class MusicUpdateDetailV1():
class MusicUpdateDetailV1:
def __init__(self) -> None:
self.songId = 0
self.difficulty = 1
@ -730,7 +817,7 @@ class MusicUpdateDetailV3(MusicUpdateDetailV2):
super().__init__()
self.grades = SongDetailGradeCountsV2()
class SongRatingUpdate():
class SongRatingUpdate:
def __init__(self, song_id: int = 0, difficulty: int = 1, new_rating: int = 0) -> None:
self.songId = song_id
self.difficulty = difficulty
@ -743,7 +830,7 @@ class SongRatingUpdate():
self.rating,
]
class GateTutorialFlag():
class GateTutorialFlag:
def __init__(self, tutorial_id: int = 1, flg_watched: bool = False) -> None:
self.tutorialId = tutorial_id
self.flagWatched = flg_watched
@ -753,3 +840,11 @@ class GateTutorialFlag():
self.tutorialId,
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.helpers import HousingInfo
from titles.wacca.const import WaccaConstants
# ---housing/get----
class HousingGetResponse(BaseResponse):
@ -37,12 +38,22 @@ class HousingStartRequestV2(HousingStartRequestV1):
self.info.append(HousingInfo(info[0], info[1]))
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__()
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:
self.params = [self.regionId, self.songList]
self.params = [self.regionId.value, self.songList]
return super().make()

View File

@ -1,7 +1,7 @@
from typing import List, Dict
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---
class UserInfoUpdateRequest(BaseRequest):
@ -9,13 +9,17 @@ class UserInfoUpdateRequest(BaseRequest):
super().__init__(data)
self.profileId = int(self.params[0])
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.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]))
for x in self.params[3]:
self.datesUpdated.append(DateUpdate(x[0], x[1]))
# ---user/info/getMyroom--- TODO: Understand this better
class UserInfogetMyroomRequest(BaseRequest):
game_id = 0

View File

@ -19,7 +19,7 @@ class UserStatusGetV1Response(BaseResponse):
self.setIconId: int = 0
self.profileStatus: ProfileStatus = ProfileStatus.ProfileGood
self.versionStatus: PlayVersionStatus = PlayVersionStatus.VersionGood
self.lastGameVersion: str = ""
self.lastGameVersion: ShortVersion = ShortVersion()
def make(self) -> Dict:
self.params = [
@ -29,7 +29,7 @@ class UserStatusGetV1Response(BaseResponse):
self.profileStatus.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]))
class UserStatusUpdateRequestV2(UserStatusUpdateRequestV1):
isContinue = False
isFirstPlayFree = False
itemsUsed = []
lastSongInfo: LastSongDetail
def __init__(self, data: Dict) -> None:
super().__init__(data)
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.base import WaccaBase
from titles.wacca.handlers.base import BaseResponse
from titles.wacca.handlers.helpers import Version
class WaccaServlet():
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
@ -56,11 +57,9 @@ class WaccaServlet():
request.responseHeaders.addRawHeader(b"X-Wacca-Hash", hash.hex().encode())
return json.dumps(resp).encode()
version_full = []
try:
req_json = json.loads(request.content.getvalue())
version_full = req_json["appVersion"].split(".")
version_full = Version(req_json["appVersion"])
except:
self.logger.error(f"Failed to parse request toi {request.uri} -> {request.content.getvalue()}")
resp = BaseResponse()
@ -76,7 +75,7 @@ class WaccaServlet():
func_to_find += f"{url_split[x + start_req_idx]}_"
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:
internal_ver = WaccaConstants.VER_WACCA

View File

@ -17,20 +17,20 @@ class WaccaLily(WaccaS):
self.OPTIONS_DEFAULTS["set_nav_id"] = 210002
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),
(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),
(210002, 0),
(210003, 0),
@ -43,23 +43,25 @@ class WaccaLily(WaccaS):
def handle_housing_start_request(self, data: Dict) -> Dict:
req = HousingStartRequestV2(data)
resp = HousingStartResponseV1(
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
]
)
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(region_id)
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:
req = UserStatusGetRequest(data)
resp = UserStatusGetV2Response()
ver_split = req.appVersion.split(".")
profile = self.data.profile.get_profile(aime_id=req.aimeId)
if profile is None:
@ -69,11 +71,9 @@ class WaccaLily(WaccaS):
self.logger.info(f"User preview for {req.aimeId} from {req.chipId}")
if profile["last_game_ver"] is None:
profile_ver_split = ver_split
resp.lastGameVersion = req.appVersion
resp.lastGameVersion = ShortVersion(str(req.appVersion))
else:
profile_ver_split = profile["last_game_ver"].split(".")
resp.lastGameVersion = profile["last_game_ver"]
resp.lastGameVersion = ShortVersion(profile["last_game_ver"])
resp.userStatus.userId = profile["id"]
resp.userStatus.username = profile["username"]
@ -103,27 +103,12 @@ 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()):
resp.userStatus.loginConsecutiveDays = 0
if int(ver_split[0]) > int(profile_ver_split[0]):
if req.appVersion > resp.lastGameVersion:
resp.versionStatus = PlayVersionStatus.VersionUpgrade
elif int(ver_split[0]) < int(profile_ver_split[0]):
elif req.appVersion < resp.lastGameVersion:
resp.versionStatus = PlayVersionStatus.VersionTooNew
else:
if int(ver_split[1]) > int(profile_ver_split[1]):
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:
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:
req = UserStatusGetDetailRequest(data)
ver_split = req.appVersion.split(".")
if int(ver_split[1]) >= 53:
if req.appVersion.minor >= 53:
resp = UserStatusGetDetailResponseV3()
else:
resp = UserStatusGetDetailResponseV2()
@ -252,7 +236,7 @@ class WaccaLily(WaccaS):
for user_gate in profile_gates:
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"],
user_gate["loops"],int(user_gate["last_used"].timestamp()),user_gate["mission_flag"]))
@ -266,7 +250,7 @@ class WaccaLily(WaccaS):
break
if not added_gate:
if int(ver_split[1]) >= 53:
if req.appVersion.minor >= 53:
resp.gateInfo.append(GateDetailV2(gate))
else:

View File

@ -16,20 +16,20 @@ class WaccaLilyR(WaccaLily):
self.OPTIONS_DEFAULTS["set_nav_id"] = 210002
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),
(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),
(210002, 0),
(210003, 0),

View File

@ -18,20 +18,20 @@ class WaccaReverse(WaccaLilyR):
self.OPTIONS_DEFAULTS["set_nav_id"] = 310001
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),
(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
(210001, 0),
(210002, 0),

View File

@ -11,19 +11,19 @@ from titles.wacca.handlers import *
class WaccaS(WaccaBase):
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),
(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:

View File

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