Merge branch 'develop' into finale

This commit is contained in:
Kevin Trocolli
2023-06-25 18:35:12 -04:00
72 changed files with 24042 additions and 347 deletions
+11 -6
View File
@@ -112,6 +112,8 @@ class AllnetServlet:
) )
resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/{req.game_id}/{req.ver.replace('.', '')}/" resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/{req.game_id}/{req.ver.replace('.', '')}/"
resp.host = f"{self.config.title.hostname}:{self.config.title.port}" resp.host = f"{self.config.title.hostname}:{self.config.title.port}"
self.logger.debug(f"Allnet response: {vars(resp)}")
return self.dict_to_http_form_string([vars(resp)]) return self.dict_to_http_form_string([vars(resp)])
resp.uri, resp.host = self.uri_registry[req.game_id] resp.uri, resp.host = self.uri_registry[req.game_id]
@@ -204,16 +206,17 @@ class AllnetServlet:
else: # TODO: Keychip check else: # TODO: Keychip check
if path.exists( if path.exists(
f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver}-app.ini" f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver.replace('.', '')}-app.ini"
): ):
resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-app.ini" resp.uri = f"http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-app.ini"
if path.exists( if path.exists(
f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver}-opt.ini" f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver.replace('.', '')}-opt.ini"
): ):
resp.uri += f"|http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-opt.ini" resp.uri += f"|http://{self.config.title.hostname}:{self.config.title.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-opt.ini"
self.logger.debug(f"Sending download uri {resp.uri}") self.logger.debug(f"Sending download uri {resp.uri}")
self.data.base.log_event("allnet", "DLORDER_REQ_SUCCESS", logging.INFO, f"{Utils.get_ip_addr(request)} requested DL Order for {req.serial} {req.game_id} v{req.ver}")
return self.dict_to_http_form_string([vars(resp)]) return self.dict_to_http_form_string([vars(resp)])
def handle_dlorder_ini(self, request: Request, match: Dict) -> bytes: def handle_dlorder_ini(self, request: Request, match: Dict) -> bytes:
@@ -223,6 +226,8 @@ class AllnetServlet:
req_file = match["file"].replace("%0A", "") req_file = match["file"].replace("%0A", "")
if path.exists(f"{self.config.allnet.update_cfg_folder}/{req_file}"): if path.exists(f"{self.config.allnet.update_cfg_folder}/{req_file}"):
self.logger.info(f"Request for DL INI file {req_file} from {Utils.get_ip_addr(request)} successful")
self.data.base.log_event("allnet", "DLORDER_INI_SENT", logging.INFO, f"{Utils.get_ip_addr(request)} successfully recieved {req_file}")
return open( return open(
f"{self.config.allnet.update_cfg_folder}/{req_file}", "rb" f"{self.config.allnet.update_cfg_folder}/{req_file}", "rb"
).read() ).read()
@@ -410,8 +415,8 @@ class AllnetPowerOnResponse3:
self.uri = "" self.uri = ""
self.host = "" self.host = ""
self.place_id = "123" self.place_id = "123"
self.name = "" self.name = "ARTEMiS"
self.nickname = "" self.nickname = "ARTEMiS"
self.region0 = "1" self.region0 = "1"
self.region_name0 = "W" self.region_name0 = "W"
self.region_name1 = "" self.region_name1 = ""
@@ -435,8 +440,8 @@ class AllnetPowerOnResponse2:
self.uri = "" self.uri = ""
self.host = "" self.host = ""
self.place_id = "123" self.place_id = "123"
self.name = "Test" self.name = "ARTEMiS"
self.nickname = "Test123" self.nickname = "ARTEMiS"
self.region0 = "1" self.region0 = "1"
self.region_name0 = "W" self.region_name0 = "W"
self.region_name1 = "X" self.region_name1 = "X"
+5
View File
@@ -333,3 +333,8 @@ class Data:
if not failed: if not failed:
self.base.set_schema_ver(latest_ver, game) self.base.set_schema_ver(latest_ver, game)
def show_versions(self) -> None:
all_game_versions = self.base.get_all_schema_vers()
for ver in all_game_versions:
self.logger.info(f"{ver['game']} -> v{ver['version']}")
+3
View File
@@ -80,6 +80,9 @@ class UserData(BaseData):
if usr["password"] is None: if usr["password"] is None:
return False return False
if passwd is None or not passwd:
return False
return bcrypt.checkpw(passwd, usr["password"].encode()) return bcrypt.checkpw(passwd, usr["password"].encode())
def reset_autoincrement(self, ai_value: int) -> None: def reset_autoincrement(self, ai_value: int) -> None:
@@ -0,0 +1,30 @@
SET FOREIGN_KEY_CHECKS = 0;
ALTER TABLE chuni_score_playlog
DROP COLUMN regionId,
DROP COLUMN machineType;
ALTER TABLE chuni_static_events
DROP COLUMN startDate;
ALTER TABLE chuni_profile_data
DROP COLUMN rankUpChallengeResults;
ALTER TABLE chuni_static_login_bonus
DROP FOREIGN KEY chuni_static_login_bonus_ibfk_1;
ALTER TABLE chuni_static_login_bonus_preset
DROP PRIMARY KEY;
ALTER TABLE chuni_static_login_bonus_preset
CHANGE COLUMN presetId id INT NOT NULL;
ALTER TABLE chuni_static_login_bonus_preset
ADD PRIMARY KEY(id);
ALTER TABLE chuni_static_login_bonus_preset
ADD CONSTRAINT chuni_static_login_bonus_preset_uk UNIQUE(id, version);
ALTER TABLE chuni_static_login_bonus
ADD CONSTRAINT chuni_static_login_bonus_ibfk_1 FOREIGN KEY(presetId)
REFERENCES chuni_static_login_bonus_preset(id) ON UPDATE CASCADE ON DELETE CASCADE;
SET FOREIGN_KEY_CHECKS = 1;
@@ -0,0 +1,29 @@
SET FOREIGN_KEY_CHECKS = 0;
ALTER TABLE chuni_score_playlog
ADD COLUMN regionId INT,
ADD COLUMN machineType INT;
ALTER TABLE chuni_static_events
ADD COLUMN startDate TIMESTAMP NOT NULL DEFAULT current_timestamp();
ALTER TABLE chuni_profile_data
ADD COLUMN rankUpChallengeResults JSON;
ALTER TABLE chuni_static_login_bonus
DROP FOREIGN KEY chuni_static_login_bonus_ibfk_1;
ALTER TABLE chuni_static_login_bonus_preset
CHANGE COLUMN id presetId INT NOT NULL;
ALTER TABLE chuni_static_login_bonus_preset
DROP PRIMARY KEY;
ALTER TABLE chuni_static_login_bonus_preset
DROP INDEX chuni_static_login_bonus_preset_uk;
ALTER TABLE chuni_static_login_bonus_preset
ADD CONSTRAINT chuni_static_login_bonus_preset_pk PRIMARY KEY (presetId, version);
ALTER TABLE chuni_static_login_bonus
ADD CONSTRAINT chuni_static_login_bonus_ibfk_1 FOREIGN KEY (presetId, version)
REFERENCES chuni_static_login_bonus_preset(presetId, version) ON UPDATE CASCADE ON DELETE CASCADE;
SET FOREIGN_KEY_CHECKS = 1;
+3 -26
View File
@@ -1,26 +1,3 @@
DELETE FROM mai2_static_event WHERE version < 13; ALTER TABLE mai2_item_card
UPDATE mai2_static_event SET version = version - 13 WHERE version >= 13; CHANGE COLUMN startDate startDate TIMESTAMP DEFAULT "2018-01-01 00:00:00.0",
CHANGE COLUMN endDate endDate TIMESTAMP DEFAULT "2038-01-01 00:00:00.0";
DELETE FROM mai2_static_music WHERE version < 13;
UPDATE mai2_static_music SET version = version - 13 WHERE version >= 13;
DELETE FROM mai2_static_ticket WHERE version < 13;
UPDATE mai2_static_ticket SET version = version - 13 WHERE version >= 13;
DELETE FROM mai2_static_cards WHERE version < 13;
UPDATE mai2_static_cards SET version = version - 13 WHERE version >= 13;
DELETE FROM mai2_profile_detail WHERE version < 13;
UPDATE mai2_profile_detail SET version = version - 13 WHERE version >= 13;
DELETE FROM mai2_profile_extend WHERE version < 13;
UPDATE mai2_profile_extend SET version = version - 13 WHERE version >= 13;
DELETE FROM mai2_profile_option WHERE version < 13;
UPDATE mai2_profile_option SET version = version - 13 WHERE version >= 13;
DELETE FROM mai2_profile_ghost WHERE version < 13;
UPDATE mai2_profile_ghost SET version = version - 13 WHERE version >= 13;
DELETE FROM mai2_profile_rating WHERE version < 13;
UPDATE mai2_profile_rating SET version = version - 13 WHERE version >= 13;
+3 -17
View File
@@ -1,17 +1,3 @@
UPDATE mai2_static_event SET version = version + 13 WHERE version < 1000; ALTER TABLE mai2_item_card
CHANGE COLUMN startDate startDate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UPDATE mai2_static_music SET version = version + 13 WHERE version < 1000; CHANGE COLUMN endDate endDate TIMESTAMP NOT NULL;
UPDATE mai2_static_ticket SET version = version + 13 WHERE version < 1000;
UPDATE mai2_static_cards SET version = version + 13 WHERE version < 1000;
UPDATE mai2_profile_detail SET version = version + 13 WHERE version < 1000;
UPDATE mai2_profile_extend SET version = version + 13 WHERE version < 1000;
UPDATE mai2_profile_option SET version = version + 13 WHERE version < 1000;
UPDATE mai2_profile_ghost SET version = version + 13 WHERE version < 1000;
UPDATE mai2_profile_rating SET version = version + 13 WHERE version < 1000;
+16 -3
View File
@@ -182,7 +182,7 @@ class FE_Gate(FE_Base):
access_code: str = request.args[b"access_code"][0].decode() access_code: str = request.args[b"access_code"][0].decode()
username: str = request.args[b"username"][0] username: str = request.args[b"username"][0]
email: str = request.args[b"email"][0].decode() email: str = request.args[b"email"][0].decode()
passwd: str = request.args[b"passwd"][0] passwd: bytes = request.args[b"passwd"][0]
uid = self.data.card.get_user_id_from_card(access_code) uid = self.data.card.get_user_id_from_card(access_code)
if uid is None: if uid is None:
@@ -197,7 +197,7 @@ class FE_Gate(FE_Base):
if result is None: if result is None:
return redirectTo(b"/gate?e=3", request) return redirectTo(b"/gate?e=3", request)
if not self.data.user.check_password(uid, passwd.encode()): if not self.data.user.check_password(uid, passwd):
return redirectTo(b"/gate", request) return redirectTo(b"/gate", request)
return redirectTo(b"/user", request) return redirectTo(b"/user", request)
@@ -228,8 +228,21 @@ class FE_User(FE_Base):
if usr_sesh.userId == 0: if usr_sesh.userId == 0:
return redirectTo(b"/gate", request) return redirectTo(b"/gate", request)
cards = self.data.card.get_user_cards(usr_sesh.userId)
user = self.data.user.get_user(usr_sesh.userId)
card_data = []
for c in cards:
if c['is_locked']:
status = 'Locked'
elif c['is_banned']:
status = 'Banned'
else:
status = 'Active'
card_data.append({'access_code': c['access_code'], 'status': status})
return template.render( return template.render(
title=f"{self.core_config.server.name} | Account", sesh=vars(usr_sesh) title=f"{self.core_config.server.name} | Account", sesh=vars(usr_sesh), cards=card_data, username=user['username']
).encode("utf-16") ).encode("utf-16")
+28 -1
View File
@@ -1,4 +1,31 @@
{% extends "core/frontend/index.jinja" %} {% extends "core/frontend/index.jinja" %}
{% block content %} {% block content %}
<h1>testing</h1> <h1>Management for {{ username }}</h1>
<h2>Cards <button class="btn btn-success" data-bs-toggle="modal" data-bs-target="#card_add">Add</button></h2>
<ul>
{% for c in cards %}
<li>{{ c.access_code }}: {{ c.status }} <button class="btn-danger btn">Delete</button></li>
{% endfor %}
</ul>
<div class="modal fade" id="card_add" tabindex="-1" aria-labelledby="card_add_label" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="card_add_label">Add Card</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
HOW TO:<br>
Scan your card on any networked game and press the "View Access Code" button (varies by game) and enter the 20 digit code below.<br>
!!FOR AMUSEIC CARDS: DO NOT ENTER THE CODE SHOWN ON THE BACK OF THE CARD ITSELF OR IT WILL NOT WORK!!
<p /><label for="card_add_frm_access_code">Access Code:&nbsp;</label><input id="card_add_frm_access_code" maxlength="20" type="text" required>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary">Add</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{% endblock content %} {% endblock content %}
+1 -1
View File
@@ -4,7 +4,7 @@
<div style="background: #333; color: #f9f9f9; width: 80%; height: 50px; line-height: 50px; padding-left: 10px; float: left;"> <div style="background: #333; color: #f9f9f9; width: 80%; height: 50px; line-height: 50px; padding-left: 10px; float: left;">
<a href=/><button class="btn btn-primary">Home</button></a>&nbsp; <a href=/><button class="btn btn-primary">Home</button></a>&nbsp;
{% for game in game_list %} {% for game in game_list %}
<a href=game/{{ game.url }}><button class="btn btn-success">{{ game.name }}</button></a>&nbsp; <a href=/game/{{ game.url }}><button class="btn btn-success">{{ game.name }}</button></a>&nbsp;
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
+2 -2
View File
@@ -33,8 +33,8 @@ class MuchaServlet:
self.logger.addHandler(fileHandler) self.logger.addHandler(fileHandler)
self.logger.addHandler(consoleHandler) self.logger.addHandler(consoleHandler)
self.logger.setLevel(logging.INFO) self.logger.setLevel(cfg.mucha.loglevel)
coloredlogs.install(level=logging.INFO, logger=self.logger, fmt=log_fmt_str) coloredlogs.install(level=cfg.mucha.loglevel, logger=self.logger, fmt=log_fmt_str)
all_titles = Utils.get_all_titles() all_titles = Utils.get_all_titles()
+1 -1
View File
@@ -84,7 +84,7 @@ class TitleServlet:
request.setResponseCode(405) request.setResponseCode(405)
return b"" return b""
return index.render_GET(request, endpoints["version"], endpoints["endpoint"]) return index.render_GET(request, int(endpoints["version"]), endpoints["endpoint"])
def render_POST(self, request: Request, endpoints: dict) -> bytes: def render_POST(self, request: Request, endpoints: dict) -> bytes:
code = endpoints["game"] code = endpoints["game"]
+3
View File
@@ -85,4 +85,7 @@ if __name__ == "__main__":
elif args.action == "cleanup": elif args.action == "cleanup":
data.delete_hanging_users() data.delete_hanging_users()
elif args.action == "version":
data.show_versions()
data.logger.info("Done") data.logger.info("Done")
+135 -33
View File
@@ -9,42 +9,44 @@ using the megaime database. Clean installations always create the latest databas
# Table of content # Table of content
- [Supported Games](#supported-games) - [Supported Games](#supported-games)
- [Chunithm](#chunithm) - [CHUNITHM](#chunithm)
- [crossbeats REV.](#crossbeats-rev) - [crossbeats REV.](#crossbeats-rev)
- [maimai DX](#maimai-dx) - [maimai DX](#maimai-dx)
- [O.N.G.E.K.I.](#o-n-g-e-k-i) - [O.N.G.E.K.I.](#o-n-g-e-k-i)
- [Card Maker](#card-maker) - [Card Maker](#card-maker)
- [WACCA](#wacca) - [WACCA](#wacca)
- [Sword Art Online Arcade](#sao)
# Supported Games # Supported Games
Games listed below have been tested and confirmed working. Games listed below have been tested and confirmed working.
## Chunithm ## CHUNITHM
### SDBT ### SDBT
| Version ID | Version Name | | Version ID | Version Name |
|------------|--------------------| |------------|-----------------------|
| 0 | Chunithm | | 0 | CHUNITHM |
| 1 | Chunithm+ | | 1 | CHUNITHM PLUS |
| 2 | Chunithm Air | | 2 | CHUNITHM AIR |
| 3 | Chunithm Air + | | 3 | CHUNITHM AIR PLUS |
| 4 | Chunithm Star | | 4 | CHUNITHM STAR |
| 5 | Chunithm Star + | | 5 | CHUNITHM STAR PLUS |
| 6 | Chunithm Amazon | | 6 | CHUNITHM AMAZON |
| 7 | Chunithm Amazon + | | 7 | CHUNITHM AMAZON PLUS |
| 8 | Chunithm Crystal | | 8 | CHUNITHM CRYSTAL |
| 9 | Chunithm Crystal + | | 9 | CHUNITHM CRYSTAL PLUS |
| 10 | Chunithm Paradise | | 10 | CHUNITHM PARADISE |
### SDHD/SDBT ### SDHD/SDBT
| Version ID | Version Name | | Version ID | Version Name |
|------------|-----------------| |------------|---------------------|
| 11 | Chunithm New!! | | 11 | CHUNITHM NEW!! |
| 12 | Chunithm New!!+ | | 12 | CHUNITHM NEW PLUS!! |
| 13 | CHUNITHM SUN |
### Importer ### Importer
@@ -60,13 +62,33 @@ The importer for Chunithm will import: Events, Music, Charge Items and Avatar Ac
### Database upgrade ### Database upgrade
Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see
which version is the latest, f.e. `SDBT_3_upgrade.sql`. In order to upgrade to version 3 in this case you need to which version is the latest, f.e. `SDBT_4_upgrade.sql`. In order to upgrade to version 4 in this case you need to
perform all previous updates as well: perform all previous updates as well:
```shell ```shell
python dbutils.py --game SDBT upgrade python dbutils.py --game SDBT upgrade
``` ```
### Online Battle
**Only matchmaking (with your imaginary friends) is supported! Online Battle does not (yet?) work!**
The first person to start the Online Battle (now called host) will create a "matching room" with a given `roomId`, after that max 3 other people can join the created room.
Non used slots during the matchmaking will be filled with CPUs after the timer runs out.
As soon as a new member will join the room the timer will jump back to 60 secs again.
Sending those 4 messages to all other users is also working properly.
In order to use the Online Battle every user needs the same ICF, same rom version and same data version!
If a room is full a new room will be created if another user starts an Online Battle.
After a failed Online Battle the room will be deleted. The host is used for the timer countdown, so if the connection failes to the host the timer will stop and could create a "frozen" state.
#### Information/Problems:
- Online Battle uses UDP hole punching and opens port 50201?
- `reflectorUri` seems related to that?
- Timer countdown should be handled globally and not by one user
- Game can freeze or can crash if someone (especially the host) leaves the matchmaking
## crossbeats REV. ## crossbeats REV.
### SDCA ### SDCA
@@ -253,20 +275,20 @@ python dbutils.py --game SDDT upgrade
| Version ID | Version Name | | Version ID | Version Name |
|------------|-----------------| |------------|-----------------|
| 0 | Card Maker 1.34 | | 0 | Card Maker 1.30 |
| 1 | Card Maker 1.35 | | 1 | Card Maker 1.35 |
### Support status ### Support status
* Card Maker 1.34: * Card Maker 1.30:
* Chunithm New!!: Yes * CHUNITHM NEW!!: Yes
* maimai DX Universe: Yes * maimai DX UNiVERSE: Yes
* O.N.G.E.K.I. Bright: Yes * O.N.G.E.K.I. Bright: Yes
* Card Maker 1.35: * Card Maker 1.35:
* Chunithm New!!+: Yes * CHUNITHM SUN: Yes (NEW PLUS!! up to A032)
* maimai DX Universe PLUS: Yes * maimai DX FESTiVAL: Yes (up to A035) (UNiVERSE PLUS up to A031)
* O.N.G.E.K.I. Bright Memory: Yes * O.N.G.E.K.I. Bright Memory: Yes
@@ -285,19 +307,46 @@ python read.py --series SDED --version <version ID> --binfolder titles/cm/cm_dat
python read.py --series SDDT --version <version ID> --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder python read.py --series SDDT --version <version ID> --binfolder /path/to/game/folder --optfolder /path/to/game/option/folder
``` ```
Also make sure to import all maimai and Chunithm data as well: Also make sure to import all maimai DX and CHUNITHM data as well:
```shell ```shell
python read.py --series SDED --version <version ID> --binfolder /path/to/cardmaker/CardMaker_Data python read.py --series SDED --version <version ID> --binfolder /path/to/cardmaker/CardMaker_Data
``` ```
The importer for Card Maker will import all required Gachas (Banners) and cards (for maimai/Chunithm) and the hardcoded The importer for Card Maker will import all required Gachas (Banners) and cards (for maimai DX/CHUNITHM) and the hardcoded
Cards for each Gacha (O.N.G.E.K.I. only). Cards for each Gacha (O.N.G.E.K.I. only).
**NOTE: Without executing the importer Card Maker WILL NOT work!** **NOTE: Without executing the importer Card Maker WILL NOT work!**
### O.N.G.E.K.I. Gachas ### Config setup
Make sure to update your `config/cardmaker.yaml` with the correct version for each game. To get the current version required to run a specific game, open every opt (Axxx) folder descending until you find all three folders:
- `MU3`: O.N.G.E.K.I.
- `MAI`: maimai DX
- `CHU`: CHUNITHM
Inside each folder is a `DataConfig.xml` file, for example:
`MU3/DataConfig.xml`:
```xml
<cardMakerVersion>
<major>1</major>
<minor>35</minor>
<release>3</release>
</cardMakerVersion>
```
Now update your `config/cardmaker.yaml` with the correct version number, for example:
```yaml
version:
1: # Card Maker 1.35
ongeki: 1.35.03
```
### O.N.G.E.K.I.
Gacha "無料ガチャ" can only pull from the free cards with the following probabilities: 94%: R, 5% SR and 1% chance of Gacha "無料ガチャ" can only pull from the free cards with the following probabilities: 94%: R, 5% SR and 1% chance of
getting an SSR card getting an SSR card
@@ -310,20 +359,24 @@ and 3% chance of getting an SSR card
All other (limited) gachas can pull from every card added to ongeki_static_cards but with the promoted cards All other (limited) gachas can pull from every card added to ongeki_static_cards but with the promoted cards
(click on the green button under the banner) having a 10 times higher chance to get pulled (click on the green button under the banner) having a 10 times higher chance to get pulled
### Chunithm Gachas ### CHUNITHM
All cards in Chunithm (basically just the characters) have the same rarity to it just pulls randomly from all cards All cards in CHUNITHM (basically just the characters) have the same rarity to it just pulls randomly from all cards
from a given gacha but made sure you cannot pull the same card twice in the same 5 times gacha roll. from a given gacha but made sure you cannot pull the same card twice in the same 5 times gacha roll.
### maimai DX
Printed maimai DX cards: Freedom (`cardTypeId=6`) or Gold Pass (`cardTypeId=4`) can now be selected during the login process. You can only have ONE Freedom and ONE Gold Pass active at a given time. The cards will expire after 15 days.
Thanks GetzeAvenue for the `selectedCardList` rarity hint!
### Notes ### Notes
Card Maker 1.34 will only load an O.N.G.E.K.I. Bright profile (1.30). Card Maker 1.35 will only load an O.N.G.E.K.I. Card Maker 1.30-1.34 will only load an O.N.G.E.K.I. Bright profile (1.30). Card Maker 1.35+ will only load an O.N.G.E.K.I.
Bright Memory profile (1.35). Bright Memory profile (1.35).
The gachas inside the `ongeki.yaml` will make sure only the right gacha ids for the right CM version will be loaded. The gachas inside the `config/ongeki.yaml` will make sure only the right gacha ids for the right CM version will be loaded.
Gacha IDs up to 1140 will be loaded for CM 1.34 and all gachas will be loaded for CM 1.35. Gacha IDs up to 1140 will be loaded for CM 1.34 and all gachas will be loaded for CM 1.35.
**NOTE: There is currently no way to load/use the (printed) maimai DX cards!**
## WACCA ## WACCA
### SDFE ### SDFE
@@ -366,3 +419,52 @@ Always make sure your database (tables) are up-to-date, to do so go to the `core
```shell ```shell
python dbutils.py --game SDFE upgrade python dbutils.py --game SDFE upgrade
``` ```
## SAO
### SDEW
| Version ID | Version Name |
|------------|---------------|
| 0 | SAO |
### Importer
In order to use the importer locate your game installation folder and execute:
```shell
python read.py --series SDEW --version <version ID> --binfolder /path/to/game/extractedassets
```
The importer for SAO will import all items, heroes, support skills and titles data.
### Config
Config file is located in `config/sao.yaml`.
| Option | Info |
|--------------------|-----------------------------------------------------------------------------|
| `hostname` | Changes the server listening address for Mucha |
| `port` | Changes the listing port |
| `auto_register` | Allows the game to handle the automatic registration of new cards |
### Database upgrade
Always make sure your database (tables) are up-to-date, to do so go to the `core/data/schema/versions` folder and see which version is the latest, f.e. `SDEW_1_upgrade.sql`. In order to upgrade to version 3 in this case you need to perform all previous updates as well:
```shell
python dbutils.py --game SDEW upgrade
```
### Notes
- Co-Op (matching) is not supported
- Shop is not functionnal
- Player title is currently static and cannot be changed in-game
### Credits for SAO support:
- Midorica - Limited Network Support
- Dniel97 - Helping with network base
- tungnotpunk - Source
+10
View File
@@ -1,3 +1,13 @@
server: server:
enable: True enable: True
loglevel: "info" loglevel: "info"
version:
0:
ongeki: 1.30.01
chuni: 2.00.00
maimai: 1.20.00
1:
ongeki: 1.35.03
chuni: 2.10.00
maimai: 1.30.00
+3
View File
@@ -15,6 +15,9 @@ version:
12: 12:
rom: 2.05.00 rom: 2.05.00
data: 2.05.00 data: 2.05.00
13:
rom: 2.10.00
data: 2.10.00
crypto: crypto:
encrypted_only: False encrypted_only: False
+7 -4
View File
@@ -2,8 +2,11 @@ server:
hostname: "localhost" hostname: "localhost"
enable: True enable: True
loglevel: "info" loglevel: "info"
port: 9000
port_stun: 9001
port_turn: 9002
port_admission: 9003
auto_register: True auto_register: True
enable_matching: False
stun_server_host: "stunserver.stunprotocol.org"
stun_server_port: 3478
ports:
game: 9000
admission: 9001
+6
View File
@@ -0,0 +1,6 @@
server:
hostname: "localhost"
enable: True
loglevel: "info"
port: 9000
auto_register: True
+14 -10
View File
@@ -3,32 +3,36 @@ A network service emulator for games running SEGA'S ALL.NET service, and similar
# Supported games # Supported games
Games listed below have been tested and confirmed working. Only game versions older then the version currently active in arcades, or games versions that have not recieved a major update in over one year, are supported. Games listed below have been tested and confirmed working. Only game versions older then the version currently active in arcades, or games versions that have not recieved a major update in over one year, are supported.
+ Chunithm
+ All versions up to New!! Plus
+ Crossbeats Rev + CHUNITHM
+ All versions up to SUN
+ crossbeats REV.
+ All versions + omnimix + All versions + omnimix
+ maimai DX + maimai DX
+ All versions up to Festival + All versions up to FESTiVAL
+ Hatsune Miku Arcade + Hatsune Miku: Project DIVA Arcade
+ All versions + All versions
+ Card Maker + Card Maker
+ 1.34.xx + 1.30
+ 1.35.xx + 1.35
+ Ongeki + O.N.G.E.K.I.
+ All versions up to Bright Memory + All versions up to Bright Memory
+ Wacca + WACCA
+ Lily R + Lily R
+ Reverse + Reverse
+ Pokken + POKKÉN TOURNAMENT
+ Final Online + Final Online
+ Sword Art Online Arcade (partial support)
+ Final
## Requirements ## Requirements
- python 3 (tested working with 3.9 and 3.10, other versions YMMV) - python 3 (tested working with 3.9 and 3.10, other versions YMMV)
- pip - pip
+1
View File
@@ -16,3 +16,4 @@ Routes
bcrypt bcrypt
jinja2 jinja2
protobuf protobuf
autobahn
+1 -1
View File
@@ -7,4 +7,4 @@ index = ChuniServlet
database = ChuniData database = ChuniData
reader = ChuniReader reader = ChuniReader
game_codes = [ChuniConstants.GAME_CODE, ChuniConstants.GAME_CODE_NEW] game_codes = [ChuniConstants.GAME_CODE, ChuniConstants.GAME_CODE_NEW]
current_schema_version = 3 current_schema_version = 4
+69 -35
View File
@@ -44,13 +44,15 @@ class ChuniBase:
# check if a user already has some pogress and if not add the # check if a user already has some pogress and if not add the
# login bonus entry # login bonus entry
user_login_bonus = self.data.item.get_login_bonus( user_login_bonus = self.data.item.get_login_bonus(
user_id, self.version, preset["id"] user_id, self.version, preset["presetId"]
) )
if user_login_bonus is None: if user_login_bonus is None:
self.data.item.put_login_bonus(user_id, self.version, preset["id"]) self.data.item.put_login_bonus(
user_id, self.version, preset["presetId"]
)
# yeah i'm lazy # yeah i'm lazy
user_login_bonus = self.data.item.get_login_bonus( user_login_bonus = self.data.item.get_login_bonus(
user_id, self.version, preset["id"] user_id, self.version, preset["presetId"]
) )
# skip the login bonus entirely if its already finished # skip the login bonus entirely if its already finished
@@ -66,13 +68,13 @@ class ChuniBase:
last_update_date = datetime.now() last_update_date = datetime.now()
all_login_boni = self.data.static.get_login_bonus( all_login_boni = self.data.static.get_login_bonus(
self.version, preset["id"] self.version, preset["presetId"]
) )
# skip the current bonus preset if no boni were found # skip the current bonus preset if no boni were found
if all_login_boni is None or len(all_login_boni) < 1: if all_login_boni is None or len(all_login_boni) < 1:
self.logger.warn( self.logger.warn(
f"No bonus entries found for bonus preset {preset['id']}" f"No bonus entries found for bonus preset {preset['presetId']}"
) )
continue continue
@@ -83,14 +85,14 @@ class ChuniBase:
if bonus_count > max_needed_days: if bonus_count > max_needed_days:
# assume that all login preset ids under 3000 needs to be # assume that all login preset ids under 3000 needs to be
# looped, like 30 and 40 are looped, 40 does not work? # looped, like 30 and 40 are looped, 40 does not work?
if preset["id"] < 3000: if preset["presetId"] < 3000:
bonus_count = 1 bonus_count = 1
else: else:
is_finished = True is_finished = True
# grab the item for the corresponding day # grab the item for the corresponding day
login_item = self.data.static.get_login_bonus_by_required_days( login_item = self.data.static.get_login_bonus_by_required_days(
self.version, preset["id"], bonus_count self.version, preset["presetId"], bonus_count
) )
if login_item is not None: if login_item is not None:
# now add the present to the database so the # now add the present to the database so the
@@ -108,7 +110,7 @@ class ChuniBase:
self.data.item.put_login_bonus( self.data.item.put_login_bonus(
user_id, user_id,
self.version, self.version,
preset["id"], preset["presetId"],
bonusCount=bonus_count, bonusCount=bonus_count,
lastUpdateDate=last_update_date, lastUpdateDate=last_update_date,
isWatched=False, isWatched=False,
@@ -156,12 +158,18 @@ class ChuniBase:
event_list = [] event_list = []
for evt_row in game_events: for evt_row in game_events:
tmp = {} event_list.append(
tmp["id"] = evt_row["eventId"] {
tmp["type"] = evt_row["type"] "id": evt_row["eventId"],
tmp["startDate"] = "2017-12-05 07:00:00.0" "type": evt_row["type"],
tmp["endDate"] = "2099-12-31 00:00:00.0" # actually use the startDate from the import so it
event_list.append(tmp) # properly shows all the events when new ones are imported
"startDate": datetime.strftime(
evt_row["startDate"], "%Y-%m-%d %H:%M:%S"
),
"endDate": "2099-12-31 00:00:00",
}
)
return { return {
"type": data["type"], "type": data["type"],
@@ -228,29 +236,36 @@ class ChuniBase:
def handle_get_user_character_api_request(self, data: Dict) -> Dict: def handle_get_user_character_api_request(self, data: Dict) -> Dict:
characters = self.data.item.get_characters(data["userId"]) characters = self.data.item.get_characters(data["userId"])
if characters is None: if characters is None:
return {} return {
next_idx = -1 "userId": data["userId"],
"length": 0,
"nextIndex": -1,
"userCharacterList": [],
}
characterList = [] character_list = []
for x in range(int(data["nextIndex"]), len(characters)): next_idx = int(data["nextIndex"])
max_ct = int(data["maxCount"])
for x in range(next_idx, len(characters)):
tmp = characters[x]._asdict() tmp = characters[x]._asdict()
tmp.pop("user") tmp.pop("user")
tmp.pop("id") tmp.pop("id")
characterList.append(tmp) character_list.append(tmp)
if len(characterList) >= int(data["maxCount"]): if len(character_list) >= max_ct:
break break
if len(characterList) >= int(data["maxCount"]) and len(characters) > int( if len(characters) >= next_idx + max_ct:
data["maxCount"] next_idx += max_ct
) + int(data["nextIndex"]): else:
next_idx = int(data["maxCount"]) + int(data["nextIndex"]) + 1 next_idx = -1
return { return {
"userId": data["userId"], "userId": data["userId"],
"length": len(characterList), "length": len(character_list),
"nextIndex": next_idx, "nextIndex": next_idx,
"userCharacterList": characterList, "userCharacterList": character_list,
} }
def handle_get_user_charge_api_request(self, data: Dict) -> Dict: def handle_get_user_charge_api_request(self, data: Dict) -> Dict:
@@ -292,8 +307,8 @@ class ChuniBase:
if len(user_course_list) >= max_ct: if len(user_course_list) >= max_ct:
break break
if len(user_course_list) >= max_ct: if len(user_course_list) >= next_idx + max_ct:
next_idx = next_idx + max_ct next_idx += max_ct
else: else:
next_idx = -1 next_idx = -1
@@ -347,12 +362,23 @@ class ChuniBase:
} }
def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict: def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict:
user_fav_item_list = []
# still needs to be implemented on WebUI
# 1: Music, 3: Character
fav_list = self.data.item.get_all_favorites(
data["userId"], self.version, fav_kind=int(data["kind"])
)
if fav_list is not None:
for fav in fav_list:
user_fav_item_list.append({"id": fav["favId"]})
return { return {
"userId": data["userId"], "userId": data["userId"],
"length": 0, "length": len(user_fav_item_list),
"kind": data["kind"], "kind": data["kind"],
"nextIndex": -1, "nextIndex": -1,
"userFavoriteItemList": [], "userFavoriteItemList": user_fav_item_list,
} }
def handle_get_user_favorite_music_api_request(self, data: Dict) -> Dict: def handle_get_user_favorite_music_api_request(self, data: Dict) -> Dict:
@@ -387,13 +413,13 @@ class ChuniBase:
xout = kind * 10000000000 + next_idx + len(items) xout = kind * 10000000000 + next_idx + len(items)
if len(items) < int(data["maxCount"]): if len(items) < int(data["maxCount"]):
nextIndex = 0 next_idx = 0
else: else:
nextIndex = xout next_idx = xout
return { return {
"userId": data["userId"], "userId": data["userId"],
"nextIndex": nextIndex, "nextIndex": next_idx,
"itemKind": kind, "itemKind": kind,
"length": len(items), "length": len(items),
"userItemList": items, "userItemList": items,
@@ -452,6 +478,7 @@ class ChuniBase:
"nextIndex": -1, "nextIndex": -1,
"userMusicList": [], # 240 "userMusicList": [], # 240
} }
song_list = [] song_list = []
next_idx = int(data["nextIndex"]) next_idx = int(data["nextIndex"])
max_ct = int(data["maxCount"]) max_ct = int(data["maxCount"])
@@ -474,10 +501,10 @@ class ChuniBase:
if len(song_list) >= max_ct: if len(song_list) >= max_ct:
break break
if len(song_list) >= max_ct: if len(song_list) >= next_idx + max_ct:
next_idx += max_ct next_idx += max_ct
else: else:
next_idx = 0 next_idx = -1
return { return {
"userId": data["userId"], "userId": data["userId"],
@@ -623,12 +650,15 @@ class ChuniBase:
self.data.profile.put_profile_data( self.data.profile.put_profile_data(
user_id, self.version, upsert["userData"][0] user_id, self.version, upsert["userData"][0]
) )
if "userDataEx" in upsert: if "userDataEx" in upsert:
self.data.profile.put_profile_data_ex( self.data.profile.put_profile_data_ex(
user_id, self.version, upsert["userDataEx"][0] user_id, self.version, upsert["userDataEx"][0]
) )
if "userGameOption" in upsert: if "userGameOption" in upsert:
self.data.profile.put_profile_option(user_id, upsert["userGameOption"][0]) self.data.profile.put_profile_option(user_id, upsert["userGameOption"][0])
if "userGameOptionEx" in upsert: if "userGameOptionEx" in upsert:
self.data.profile.put_profile_option_ex( self.data.profile.put_profile_option_ex(
user_id, upsert["userGameOptionEx"][0] user_id, upsert["userGameOptionEx"][0]
@@ -672,6 +702,10 @@ class ChuniBase:
if "userPlaylogList" in upsert: if "userPlaylogList" in upsert:
for playlog in upsert["userPlaylogList"]: for playlog in upsert["userPlaylogList"]:
# convert the player names to utf-8
playlog["playedUserName1"] = self.read_wtf8(playlog["playedUserName1"])
playlog["playedUserName2"] = self.read_wtf8(playlog["playedUserName2"])
playlog["playedUserName3"] = self.read_wtf8(playlog["playedUserName3"])
self.data.score.put_playlog(user_id, playlog) self.data.score.put_playlog(user_id, playlog)
if "userTeamPoint" in upsert: if "userTeamPoint" in upsert:
+15 -13
View File
@@ -17,21 +17,23 @@ class ChuniConstants:
VER_CHUNITHM_PARADISE = 10 VER_CHUNITHM_PARADISE = 10
VER_CHUNITHM_NEW = 11 VER_CHUNITHM_NEW = 11
VER_CHUNITHM_NEW_PLUS = 12 VER_CHUNITHM_NEW_PLUS = 12
VER_CHUNITHM_SUN = 13
VERSION_NAMES = [ VERSION_NAMES = [
"Chunithm", "CHUNITHM",
"Chunithm+", "CHUNITHM PLUS",
"Chunithm Air", "CHUNITHM AIR",
"Chunithm Air+", "CHUNITHM AIR PLUS",
"Chunithm Star", "CHUNITHM STAR",
"Chunithm Star+", "CHUNITHM STAR PLUS",
"Chunithm Amazon", "CHUNITHM AMAZON",
"Chunithm Amazon+", "CHUNITHM AMAZON PLUS",
"Chunithm Crystal", "CHUNITHM CRYSTAL",
"Chunithm Crystal+", "CHUNITHM CRYSTAL PLUS",
"Chunithm Paradise", "CHUNITHM PARADISE",
"Chunithm New!!", "CHUNITHM NEW!!",
"Chunithm New!!+", "CHUNITHM NEW PLUS!!",
"CHUNITHM SUN"
] ]
@classmethod @classmethod
+21 -14
View File
@@ -29,6 +29,7 @@ from titles.chuni.crystalplus import ChuniCrystalPlus
from titles.chuni.paradise import ChuniParadise from titles.chuni.paradise import ChuniParadise
from titles.chuni.new import ChuniNew from titles.chuni.new import ChuniNew
from titles.chuni.newplus import ChuniNewPlus from titles.chuni.newplus import ChuniNewPlus
from titles.chuni.sun import ChuniSun
class ChuniServlet: class ChuniServlet:
@@ -55,6 +56,7 @@ class ChuniServlet:
ChuniParadise, ChuniParadise,
ChuniNew, ChuniNew,
ChuniNewPlus, ChuniNewPlus,
ChuniSun,
] ]
self.logger = logging.getLogger("chuni") self.logger = logging.getLogger("chuni")
@@ -96,15 +98,18 @@ class ChuniServlet:
] ]
for method in method_list: for method in method_list:
method_fixed = inflection.camelize(method)[6:-7] method_fixed = inflection.camelize(method)[6:-7]
# number of iterations was changed to 70 in SUN
iter_count = 70 if version >= ChuniConstants.VER_CHUNITHM_SUN else 44
hash = PBKDF2( hash = PBKDF2(
method_fixed, method_fixed,
bytes.fromhex(keys[2]), bytes.fromhex(keys[2]),
128, 128,
count=44, count=iter_count,
hmac_hash_module=SHA1, hmac_hash_module=SHA1,
) )
self.hash_table[version][hash.hex()] = method_fixed hashed_name = hash.hex()[:32] # truncate unused bytes like the game does
self.hash_table[version][hashed_name] = method_fixed
self.logger.debug( self.logger.debug(
f"Hashed v{version} method {method_fixed} with {bytes.fromhex(keys[2])} to get {hash.hex()}" f"Hashed v{version} method {method_fixed} with {bytes.fromhex(keys[2])} to get {hash.hex()}"
@@ -145,30 +150,32 @@ class ChuniServlet:
if version < 105: # 1.0 if version < 105: # 1.0
internal_ver = ChuniConstants.VER_CHUNITHM internal_ver = ChuniConstants.VER_CHUNITHM
elif version >= 105 and version < 110: # Plus elif version >= 105 and version < 110: # PLUS
internal_ver = ChuniConstants.VER_CHUNITHM_PLUS internal_ver = ChuniConstants.VER_CHUNITHM_PLUS
elif version >= 110 and version < 115: # Air elif version >= 110 and version < 115: # AIR
internal_ver = ChuniConstants.VER_CHUNITHM_AIR internal_ver = ChuniConstants.VER_CHUNITHM_AIR
elif version >= 115 and version < 120: # Air Plus elif version >= 115 and version < 120: # AIR PLUS
internal_ver = ChuniConstants.VER_CHUNITHM_AIR_PLUS internal_ver = ChuniConstants.VER_CHUNITHM_AIR_PLUS
elif version >= 120 and version < 125: # Star elif version >= 120 and version < 125: # STAR
internal_ver = ChuniConstants.VER_CHUNITHM_STAR internal_ver = ChuniConstants.VER_CHUNITHM_STAR
elif version >= 125 and version < 130: # Star Plus elif version >= 125 and version < 130: # STAR PLUS
internal_ver = ChuniConstants.VER_CHUNITHM_STAR_PLUS internal_ver = ChuniConstants.VER_CHUNITHM_STAR_PLUS
elif version >= 130 and version < 135: # Amazon elif version >= 130 and version < 135: # AMAZON
internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON
elif version >= 135 and version < 140: # Amazon Plus elif version >= 135 and version < 140: # AMAZON PLUS
internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON_PLUS internal_ver = ChuniConstants.VER_CHUNITHM_AMAZON_PLUS
elif version >= 140 and version < 145: # Crystal elif version >= 140 and version < 145: # CRYSTAL
internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL
elif version >= 145 and version < 150: # Crystal Plus elif version >= 145 and version < 150: # CRYSTAL PLUS
internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS
elif version >= 150 and version < 200: # Paradise elif version >= 150 and version < 200: # PARADISE
internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE
elif version >= 200 and version < 205: # New elif version >= 200 and version < 205: # NEW!!
internal_ver = ChuniConstants.VER_CHUNITHM_NEW internal_ver = ChuniConstants.VER_CHUNITHM_NEW
elif version >= 205 and version < 210: # New Plus elif version >= 205 and version < 210: # NEW PLUS!!
internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS
elif version >= 210: # SUN
internal_ver = ChuniConstants.VER_CHUNITHM_SUN
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
+171 -9
View File
@@ -23,41 +23,44 @@ class ChuniNew(ChuniBase):
self.version = ChuniConstants.VER_CHUNITHM_NEW self.version = ChuniConstants.VER_CHUNITHM_NEW
def handle_get_game_setting_api_request(self, data: Dict) -> Dict: def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
# use UTC time and convert it to JST time by adding +9
# matching therefore starts one hour before and lasts for 8 hours
match_start = datetime.strftime( match_start = datetime.strftime(
datetime.now() - timedelta(hours=10), self.date_time_format datetime.utcnow() + timedelta(hours=8), self.date_time_format
) )
match_end = datetime.strftime( match_end = datetime.strftime(
datetime.now() + timedelta(hours=10), self.date_time_format datetime.utcnow() + timedelta(hours=16), self.date_time_format
) )
reboot_start = datetime.strftime( reboot_start = datetime.strftime(
datetime.now() - timedelta(hours=11), self.date_time_format datetime.utcnow() + timedelta(hours=6), self.date_time_format
) )
reboot_end = datetime.strftime( reboot_end = datetime.strftime(
datetime.now() - timedelta(hours=10), self.date_time_format datetime.utcnow() + timedelta(hours=7), self.date_time_format
) )
return { return {
"gameSetting": { "gameSetting": {
"isMaintenance": "false", "isMaintenance": False,
"requestInterval": 10, "requestInterval": 10,
"rebootStartTime": reboot_start, "rebootStartTime": reboot_start,
"rebootEndTime": reboot_end, "rebootEndTime": reboot_end,
"isBackgroundDistribute": "false", "isBackgroundDistribute": False,
"maxCountCharacter": 300, "maxCountCharacter": 300,
"maxCountItem": 300, "maxCountItem": 300,
"maxCountMusic": 300, "maxCountMusic": 300,
"matchStartTime": match_start, "matchStartTime": match_start,
"matchEndTime": match_end, "matchEndTime": match_end,
"matchTimeLimit": 99, "matchTimeLimit": 60,
"matchErrorLimit": 9999, "matchErrorLimit": 9999,
"romVersion": self.game_cfg.version.version(self.version)["rom"], "romVersion": self.game_cfg.version.version(self.version)["rom"],
"dataVersion": self.game_cfg.version.version(self.version)["data"], "dataVersion": self.game_cfg.version.version(self.version)["data"],
"matchingUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", "matchingUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
"matchingUriX": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", "matchingUriX": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
# might be really important for online battle to connect the cabs via UDP port 50201
"udpHolePunchUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", "udpHolePunchUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
"reflectorUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/", "reflectorUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/200/ChuniServlet/",
}, },
"isDumpUpload": "false", "isDumpUpload": False,
"isAou": "false", "isAou": False,
} }
def handle_remove_token_api_request(self, data: Dict) -> Dict: def handle_remove_token_api_request(self, data: Dict) -> Dict:
@@ -468,3 +471,162 @@ class ChuniNew(ChuniBase):
self.data.item.put_user_print_state(user_id, id=order_id, hasCompleted=True) self.data.item.put_user_print_state(user_id, id=order_id, hasCompleted=True)
return {"returnCode": "1", "apiName": "CMUpsertUserPrintCancelApi"} return {"returnCode": "1", "apiName": "CMUpsertUserPrintCancelApi"}
def handle_ping_request(self, data: Dict) -> Dict:
# matchmaking ping request
return {"returnCode": "1"}
def handle_begin_matching_api_request(self, data: Dict) -> Dict:
room_id = 1
# check if there is a free matching room
matching_room = self.data.item.get_oldest_free_matching(self.version)
if matching_room is None:
# grab the latest roomId and add 1 for the new room
newest_matching = self.data.item.get_newest_matching(self.version)
if newest_matching is not None:
room_id = newest_matching["roomId"] + 1
# fix userName WTF8
new_member = data["matchingMemberInfo"]
new_member["userName"] = self.read_wtf8(new_member["userName"])
# create the new room with room_id and the current user id (host)
# user id is required for the countdown later on
self.data.item.put_matching(
self.version, room_id, [new_member], user_id=new_member["userId"]
)
# get the newly created matching room
matching_room = self.data.item.get_matching(self.version, room_id)
else:
# a room already exists, so just add the new member to it
matching_member_list = matching_room["matchingMemberInfoList"]
# fix userName WTF8
new_member = data["matchingMemberInfo"]
new_member["userName"] = self.read_wtf8(new_member["userName"])
matching_member_list.append(new_member)
# add the updated room to the database, make sure to set isFull correctly!
self.data.item.put_matching(
self.version,
matching_room["roomId"],
matching_member_list,
user_id=matching_room["user"],
is_full=True if len(matching_member_list) >= 4 else False,
)
matching_wait = {
"isFinish": False,
"restMSec": matching_room["restMSec"], # in sec
"pollingInterval": 1, # in sec
"matchingMemberInfoList": matching_room["matchingMemberInfoList"],
}
return {"roomId": 1, "matchingWaitState": matching_wait}
def handle_end_matching_api_request(self, data: Dict) -> Dict:
matching_room = self.data.item.get_matching(self.version, data["roomId"])
members = matching_room["matchingMemberInfoList"]
# only set the host user to role 1 every other to 0?
role_list = [
{"role": 1} if m["userId"] == matching_room["user"] else {"role": 0}
for m in members
]
self.data.item.put_matching(
self.version,
matching_room["roomId"],
members,
user_id=matching_room["user"],
rest_sec=0, # make sure to always set 0
is_full=True, # and full, so no one can join
)
return {
"matchingResult": 1, # needs to be 1 for successful matching
"matchingMemberInfoList": members,
# no idea, maybe to differentiate between CPUs and real players?
"matchingMemberRoleList": role_list,
# TCP/UDP connection?
"reflectorUri": f"{self.core_cfg.title.hostname}",
}
def handle_remove_matching_member_api_request(self, data: Dict) -> Dict:
# get all matching rooms, because Chuni only returns the userId
# not the actual roomId
matching_rooms = self.data.item.get_all_matchings(self.version)
if matching_rooms is None:
return {"returnCode": "1"}
for room in matching_rooms:
old_members = room["matchingMemberInfoList"]
new_members = [m for m in old_members if m["userId"] != data["userId"]]
# if nothing changed go to the next room
if len(old_members) == len(new_members):
continue
# if the last user got removed, delete the matching room
if len(new_members) <= 0:
self.data.item.delete_matching(self.version, room["roomId"])
else:
# remove the user from the room
self.data.item.put_matching(
self.version,
room["roomId"],
new_members,
user_id=room["user"],
rest_sec=room["restMSec"],
)
return {"returnCode": "1"}
def handle_get_matching_state_api_request(self, data: Dict) -> Dict:
polling_interval = 1
# get the current active room
matching_room = self.data.item.get_matching(self.version, data["roomId"])
members = matching_room["matchingMemberInfoList"]
rest_sec = matching_room["restMSec"]
# grab the current member
current_member = data["matchingMemberInfo"]
# only the host user can decrease the countdown
if matching_room["user"] == int(current_member["userId"]):
# cap the restMSec to 0
if rest_sec > 0:
rest_sec -= polling_interval
else:
rest_sec = 0
# update the members in order to recieve messages
for i, member in enumerate(members):
if member["userId"] == current_member["userId"]:
# replace the old user data with the current user data,
# also parse WTF-8 everytime
current_member["userName"] = self.read_wtf8(current_member["userName"])
members[i] = current_member
self.data.item.put_matching(
self.version,
data["roomId"],
members,
rest_sec=rest_sec,
user_id=matching_room["user"],
)
# only add the other members to the list
diff_members = [m for m in members if m["userId"] != current_member["userId"]]
matching_wait = {
# makes no difference? Always use False?
"isFinish": True if rest_sec == 0 else False,
"restMSec": rest_sec,
"pollingInterval": polling_interval,
# the current user needs to be the first one?
"matchingMemberInfoList": [current_member] + diff_members,
}
return {"matchingWaitState": matching_wait}
+1 -1
View File
@@ -36,6 +36,6 @@ class ChuniNewPlus(ChuniNew):
def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
user_data = super().handle_cm_get_user_preview_api_request(data) user_data = super().handle_cm_get_user_preview_api_request(data)
# hardcode lastDataVersion for CardMaker 1.35 # hardcode lastDataVersion for CardMaker 1.35 A028
user_data["lastDataVersion"] = "2.05.00" user_data["lastDataVersion"] = "2.05.00"
return user_data return user_data
+141 -1
View File
@@ -1,5 +1,12 @@
from typing import Dict, List, Optional from typing import Dict, List, Optional
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ from sqlalchemy import (
Table,
Column,
UniqueConstraint,
PrimaryKeyConstraint,
and_,
delete,
)
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON
from sqlalchemy.engine.base import Connection from sqlalchemy.engine.base import Connection
from sqlalchemy.schema import ForeignKey from sqlalchemy.schema import ForeignKey
@@ -203,8 +210,141 @@ login_bonus = Table(
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
favorite = Table(
"chuni_item_favorite",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("favId", Integer, nullable=False),
Column("favKind", Integer, nullable=False, server_default="1"),
UniqueConstraint("version", "user", "favId", name="chuni_item_favorite_uk"),
mysql_charset="utf8mb4",
)
matching = Table(
"chuni_item_matching",
metadata,
Column("roomId", Integer, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("restMSec", Integer, nullable=False, server_default="60"),
Column("isFull", Boolean, nullable=False, server_default="0"),
PrimaryKeyConstraint("roomId", "version", name="chuni_item_matching_pk"),
Column("matchingMemberInfoList", JSON, nullable=False),
mysql_charset="utf8mb4",
)
class ChuniItemData(BaseData): class ChuniItemData(BaseData):
def get_oldest_free_matching(self, version: int) -> Optional[Row]:
sql = matching.select(
and_(
matching.c.version == version,
matching.c.isFull == False
)
).order_by(matching.c.roomId.asc())
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_newest_matching(self, version: int) -> Optional[Row]:
sql = matching.select(
and_(
matching.c.version == version
)
).order_by(matching.c.roomId.desc())
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def get_all_matchings(self, version: int) -> Optional[List[Row]]:
sql = matching.select(
and_(
matching.c.version == version
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_matching(self, version: int, room_id: int) -> Optional[Row]:
sql = matching.select(
and_(matching.c.version == version, matching.c.roomId == room_id)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchone()
def put_matching(
self,
version: int,
room_id: int,
matching_member_info_list: list,
user_id: int = None,
rest_sec: int = 60,
is_full: bool = False
) -> Optional[int]:
sql = insert(matching).values(
roomId=room_id,
version=version,
restMSec=rest_sec,
user=user_id,
isFull=is_full,
matchingMemberInfoList=matching_member_info_list,
)
conflict = sql.on_duplicate_key_update(
restMSec=rest_sec, matchingMemberInfoList=matching_member_info_list
)
result = self.execute(conflict)
if result is None:
return None
return result.lastrowid
def delete_matching(self, version: int, room_id: int):
sql = delete(matching).where(
and_(matching.c.roomId == room_id, matching.c.version == version)
)
result = self.execute(sql)
if result is None:
return None
return result.lastrowid
def get_all_favorites(
self, user_id: int, version: int, fav_kind: int = 1
) -> Optional[List[Row]]:
sql = favorite.select(
and_(
favorite.c.version == version,
favorite.c.user == user_id,
favorite.c.favKind == fav_kind,
)
)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def put_login_bonus( def put_login_bonus(
self, user_id: int, version: int, preset_id: int, **login_bonus_data self, user_id: int, version: int, preset_id: int, **login_bonus_data
) -> Optional[int]: ) -> Optional[int]:
+14 -13
View File
@@ -89,8 +89,6 @@ profile = Table(
Integer, Integer,
ForeignKey("chuni_profile_team.id", ondelete="SET NULL", onupdate="SET NULL"), ForeignKey("chuni_profile_team.id", ondelete="SET NULL", onupdate="SET NULL"),
), ),
Column("avatarBack", Integer, server_default="0"),
Column("avatarFace", Integer, server_default="0"),
Column("eliteRankPoint", Integer, server_default="0"), Column("eliteRankPoint", Integer, server_default="0"),
Column("stockedGridCount", Integer, server_default="0"), Column("stockedGridCount", Integer, server_default="0"),
Column("netBattleLoseCount", Integer, server_default="0"), Column("netBattleLoseCount", Integer, server_default="0"),
@@ -98,10 +96,8 @@ profile = Table(
Column("netBattle4thCount", Integer, server_default="0"), Column("netBattle4thCount", Integer, server_default="0"),
Column("overPowerRate", Integer, server_default="0"), Column("overPowerRate", Integer, server_default="0"),
Column("battleRewardStatus", Integer, server_default="0"), Column("battleRewardStatus", Integer, server_default="0"),
Column("avatarPoint", Integer, server_default="0"),
Column("netBattle1stCount", Integer, server_default="0"), Column("netBattle1stCount", Integer, server_default="0"),
Column("charaIllustId", Integer, server_default="0"), Column("charaIllustId", Integer, server_default="0"),
Column("avatarItem", Integer, server_default="0"),
Column("userNameEx", String(8), server_default=""), Column("userNameEx", String(8), server_default=""),
Column("netBattleWinCount", Integer, server_default="0"), Column("netBattleWinCount", Integer, server_default="0"),
Column("netBattleCorrection", Integer, server_default="0"), Column("netBattleCorrection", Integer, server_default="0"),
@@ -112,7 +108,6 @@ profile = Table(
Column("netBattle3rdCount", Integer, server_default="0"), Column("netBattle3rdCount", Integer, server_default="0"),
Column("netBattleConsecutiveWinCount", Integer, server_default="0"), Column("netBattleConsecutiveWinCount", Integer, server_default="0"),
Column("overPowerLowerRank", Integer, server_default="0"), Column("overPowerLowerRank", Integer, server_default="0"),
Column("avatarWear", Integer, server_default="0"),
Column("classEmblemBase", Integer, server_default="0"), Column("classEmblemBase", Integer, server_default="0"),
Column("battleRankPoint", Integer, server_default="0"), Column("battleRankPoint", Integer, server_default="0"),
Column("netBattle2ndCount", Integer, server_default="0"), Column("netBattle2ndCount", Integer, server_default="0"),
@@ -120,13 +115,19 @@ profile = Table(
Column("skillId", Integer, server_default="0"), Column("skillId", Integer, server_default="0"),
Column("lastCountryCode", String(5), server_default="JPN"), Column("lastCountryCode", String(5), server_default="JPN"),
Column("isNetBattleHost", Boolean, server_default="0"), Column("isNetBattleHost", Boolean, server_default="0"),
Column("avatarFront", Integer, server_default="0"),
Column("avatarSkin", Integer, server_default="0"),
Column("battleRewardCount", Integer, server_default="0"), Column("battleRewardCount", Integer, server_default="0"),
Column("battleRewardIndex", Integer, server_default="0"), Column("battleRewardIndex", Integer, server_default="0"),
Column("netBattlePlayCount", Integer, server_default="0"), Column("netBattlePlayCount", Integer, server_default="0"),
Column("exMapLoopCount", Integer, server_default="0"), Column("exMapLoopCount", Integer, server_default="0"),
Column("netBattleEndState", Integer, server_default="0"), Column("netBattleEndState", Integer, server_default="0"),
Column("rankUpChallengeResults", JSON),
Column("avatarBack", Integer, server_default="0"),
Column("avatarFace", Integer, server_default="0"),
Column("avatarPoint", Integer, server_default="0"),
Column("avatarItem", Integer, server_default="0"),
Column("avatarWear", Integer, server_default="0"),
Column("avatarFront", Integer, server_default="0"),
Column("avatarSkin", Integer, server_default="0"),
Column("avatarHead", Integer, server_default="0"), Column("avatarHead", Integer, server_default="0"),
UniqueConstraint("user", "version", name="chuni_profile_profile_uk"), UniqueConstraint("user", "version", name="chuni_profile_profile_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
@@ -417,8 +418,8 @@ class ChuniProfileData(BaseData):
sql = ( sql = (
select([profile, option]) select([profile, option])
.join(option, profile.c.user == option.c.user) .join(option, profile.c.user == option.c.user)
.filter(and_(profile.c.user == aime_id, profile.c.version == version)) .filter(and_(profile.c.user == aime_id, profile.c.version <= version))
) ).order_by(profile.c.version.desc())
result = self.execute(sql) result = self.execute(sql)
if result is None: if result is None:
@@ -429,9 +430,9 @@ class ChuniProfileData(BaseData):
sql = select(profile).where( sql = select(profile).where(
and_( and_(
profile.c.user == aime_id, profile.c.user == aime_id,
profile.c.version == version, profile.c.version <= version,
)
) )
).order_by(profile.c.version.desc())
result = self.execute(sql) result = self.execute(sql)
if result is None: if result is None:
@@ -461,9 +462,9 @@ class ChuniProfileData(BaseData):
sql = select(profile_ex).where( sql = select(profile_ex).where(
and_( and_(
profile_ex.c.user == aime_id, profile_ex.c.user == aime_id,
profile_ex.c.version == version, profile_ex.c.version <= version,
)
) )
).order_by(profile_ex.c.version.desc())
result = self.execute(sql) result = self.execute(sql)
if result is None: if result is None:
+3 -1
View File
@@ -134,7 +134,9 @@ playlog = Table(
Column("charaIllustId", Integer), Column("charaIllustId", Integer),
Column("romVersion", String(255)), Column("romVersion", String(255)),
Column("judgeHeaven", Integer), Column("judgeHeaven", Integer),
mysql_charset="utf8mb4", Column("regionId", Integer),
Column("machineType", Integer),
mysql_charset="utf8mb4"
) )
+34 -13
View File
@@ -1,11 +1,19 @@
from typing import Dict, List, Optional from typing import Dict, List, Optional
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ from sqlalchemy import (
ForeignKeyConstraint,
Table,
Column,
UniqueConstraint,
PrimaryKeyConstraint,
and_,
)
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, Float from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, Float
from sqlalchemy.engine.base import Connection from sqlalchemy.engine.base import Connection
from sqlalchemy.engine import Row from sqlalchemy.engine import Row
from sqlalchemy.schema import ForeignKey from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select from sqlalchemy.sql import func, select
from sqlalchemy.dialects.mysql import insert from sqlalchemy.dialects.mysql import insert
from datetime import datetime
from core.data.schema import BaseData, metadata from core.data.schema import BaseData, metadata
@@ -17,6 +25,7 @@ events = Table(
Column("eventId", Integer), Column("eventId", Integer),
Column("type", Integer), Column("type", Integer),
Column("name", String(255)), Column("name", String(255)),
Column("startDate", TIMESTAMP, server_default=func.now()),
Column("enabled", Boolean, server_default="1"), Column("enabled", Boolean, server_default="1"),
UniqueConstraint("version", "eventId", name="chuni_static_events_uk"), UniqueConstraint("version", "eventId", name="chuni_static_events_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
@@ -125,11 +134,13 @@ gacha_cards = Table(
login_bonus_preset = Table( login_bonus_preset = Table(
"chuni_static_login_bonus_preset", "chuni_static_login_bonus_preset",
metadata, metadata,
Column("id", Integer, primary_key=True, nullable=False), Column("presetId", Integer, nullable=False),
Column("version", Integer, nullable=False), Column("version", Integer, nullable=False),
Column("presetName", String(255), nullable=False), Column("presetName", String(255), nullable=False),
Column("isEnabled", Boolean, server_default="1"), Column("isEnabled", Boolean, server_default="1"),
UniqueConstraint("version", "id", name="chuni_static_login_bonus_preset_uk"), PrimaryKeyConstraint(
"presetId", "version", name="chuni_static_login_bonus_preset_pk"
),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@@ -138,15 +149,7 @@ login_bonus = Table(
metadata, metadata,
Column("id", Integer, primary_key=True, nullable=False), Column("id", Integer, primary_key=True, nullable=False),
Column("version", Integer, nullable=False), Column("version", Integer, nullable=False),
Column( Column("presetId", Integer, nullable=False),
"presetId",
ForeignKey(
"chuni_static_login_bonus_preset.id",
ondelete="cascade",
onupdate="cascade",
),
nullable=False,
),
Column("loginBonusId", Integer, nullable=False), Column("loginBonusId", Integer, nullable=False),
Column("loginBonusName", String(255), nullable=False), Column("loginBonusName", String(255), nullable=False),
Column("presentId", Integer, nullable=False), Column("presentId", Integer, nullable=False),
@@ -157,6 +160,16 @@ login_bonus = Table(
UniqueConstraint( UniqueConstraint(
"version", "presetId", "loginBonusId", name="chuni_static_login_bonus_uk" "version", "presetId", "loginBonusId", name="chuni_static_login_bonus_uk"
), ),
ForeignKeyConstraint(
["presetId", "version"],
[
"chuni_static_login_bonus_preset.presetId",
"chuni_static_login_bonus_preset.version",
],
onupdate="CASCADE",
ondelete="CASCADE",
name="chuni_static_login_bonus_ibfk_1",
),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@@ -236,7 +249,7 @@ class ChuniStaticData(BaseData):
self, version: int, preset_id: int, preset_name: str, is_enabled: bool self, version: int, preset_id: int, preset_name: str, is_enabled: bool
) -> Optional[int]: ) -> Optional[int]:
sql = insert(login_bonus_preset).values( sql = insert(login_bonus_preset).values(
id=preset_id, presetId=preset_id,
version=version, version=version,
presetName=preset_name, presetName=preset_name,
isEnabled=is_enabled, isEnabled=is_enabled,
@@ -416,6 +429,14 @@ class ChuniStaticData(BaseData):
return None return None
return result.fetchall() return result.fetchall()
def get_music(self, version: int) -> Optional[List[Row]]:
sql = music.select(music.c.version <= version)
result = self.execute(sql)
if result is None:
return None
return result.fetchall()
def get_music_chart( def get_music_chart(
self, version: int, song_id: int, chart_id: int self, version: int, song_id: int, chart_id: int
) -> Optional[List[Row]]: ) -> Optional[List[Row]]:
+37
View File
@@ -0,0 +1,37 @@
from typing import Dict, Any
from core.config import CoreConfig
from titles.chuni.newplus import ChuniNewPlus
from titles.chuni.const import ChuniConstants
from titles.chuni.config import ChuniConfig
class ChuniSun(ChuniNewPlus):
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
super().__init__(core_cfg, game_cfg)
self.version = ChuniConstants.VER_CHUNITHM_SUN
def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
ret = super().handle_get_game_setting_api_request(data)
ret["gameSetting"]["romVersion"] = self.game_cfg.version.version(self.version)["rom"]
ret["gameSetting"]["dataVersion"] = self.game_cfg.version.version(self.version)["data"]
ret["gameSetting"][
"matchingUri"
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/"
ret["gameSetting"][
"matchingUriX"
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/"
ret["gameSetting"][
"udpHolePunchUri"
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/"
ret["gameSetting"][
"reflectorUri"
] = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/210/ChuniServlet/"
return ret
def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
user_data = super().handle_cm_get_user_preview_api_request(data)
# hardcode lastDataVersion for CardMaker 1.35 A032
user_data["lastDataVersion"] = "2.10.00"
return user_data
+31 -7
View File
@@ -23,19 +23,40 @@ class CardMakerBase:
self.game = CardMakerConstants.GAME_CODE self.game = CardMakerConstants.GAME_CODE
self.version = CardMakerConstants.VER_CARD_MAKER self.version = CardMakerConstants.VER_CARD_MAKER
@staticmethod
def _parse_int_ver(version: str) -> str:
return version.replace(".", "")[:3]
def handle_get_game_connect_api_request(self, data: Dict) -> Dict: def handle_get_game_connect_api_request(self, data: Dict) -> Dict:
if self.core_cfg.server.is_develop: if self.core_cfg.server.is_develop:
uri = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}" uri = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}"
else: else:
uri = f"http://{self.core_cfg.title.hostname}" uri = f"http://{self.core_cfg.title.hostname}"
# CHUNITHM = 0, maimai = 1, ONGEKI = 2 # grab the dict with all games version numbers from user config
games_ver = self.game_cfg.version.version(self.version)
return { return {
"length": 3, "length": 3,
"gameConnectList": [ "gameConnectList": [
{"modelKind": 0, "type": 1, "titleUri": f"{uri}/SDHD/200/"}, # CHUNITHM
{"modelKind": 1, "type": 1, "titleUri": f"{uri}/SDEZ/120/"}, {
{"modelKind": 2, "type": 1, "titleUri": f"{uri}/SDDT/130/"}, "modelKind": 0,
"type": 1,
"titleUri": f"{uri}/SDHD/{self._parse_int_ver(games_ver['chuni'])}/",
},
# maimai DX
{
"modelKind": 1,
"type": 1,
"titleUri": f"{uri}/SDEZ/{self._parse_int_ver(games_ver['maimai'])}/",
},
# ONGEKI
{
"modelKind": 2,
"type": 1,
"titleUri": f"{uri}/SDDT/{self._parse_int_ver(games_ver['ongeki'])}/",
},
], ],
} }
@@ -47,12 +68,15 @@ class CardMakerBase:
datetime.now() + timedelta(hours=4), self.date_time_format datetime.now() + timedelta(hours=4), self.date_time_format
) )
# grab the dict with all games version numbers from user config
games_ver = self.game_cfg.version.version(self.version)
return { return {
"gameSetting": { "gameSetting": {
"dataVersion": "1.30.00", "dataVersion": "1.30.00",
"ongekiCmVersion": "1.30.01", "ongekiCmVersion": games_ver["ongeki"],
"chuniCmVersion": "2.00.00", "chuniCmVersion": games_ver["chuni"],
"maimaiCmVersion": "1.20.00", "maimaiCmVersion": games_ver["maimai"],
"requestInterval": 10, "requestInterval": 10,
"rebootStartTime": reboot_start, "rebootStartTime": reboot_start,
"rebootEndTime": reboot_end, "rebootEndTime": reboot_end,
+1 -21
View File
@@ -1,8 +1,4 @@
from datetime import date, datetime, timedelta from typing import Dict
from typing import Any, Dict, List
import json
import logging
from enum import Enum
from core.config import CoreConfig from core.config import CoreConfig
from core.data.cache import cached from core.data.cache import cached
@@ -16,23 +12,7 @@ class CardMaker135(CardMakerBase):
super().__init__(core_cfg, game_cfg) super().__init__(core_cfg, game_cfg)
self.version = CardMakerConstants.VER_CARD_MAKER_135 self.version = CardMakerConstants.VER_CARD_MAKER_135
def handle_get_game_connect_api_request(self, data: Dict) -> Dict:
ret = super().handle_get_game_connect_api_request(data)
if self.core_cfg.server.is_develop:
uri = f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}"
else:
uri = f"http://{self.core_cfg.title.hostname}"
ret["gameConnectList"][0]["titleUri"] = f"{uri}/SDHD/205/"
ret["gameConnectList"][1]["titleUri"] = f"{uri}/SDEZ/125/"
ret["gameConnectList"][2]["titleUri"] = f"{uri}/SDDT/135/"
return ret
def handle_get_game_setting_api_request(self, data: Dict) -> Dict: def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
ret = super().handle_get_game_setting_api_request(data) ret = super().handle_get_game_setting_api_request(data)
ret["gameSetting"]["dataVersion"] = "1.35.00" ret["gameSetting"]["dataVersion"] = "1.35.00"
ret["gameSetting"]["ongekiCmVersion"] = "1.35.03"
ret["gameSetting"]["chuniCmVersion"] = "2.05.00"
ret["gameSetting"]["maimaiCmVersion"] = "1.25.00"
return ret return ret
+16
View File
@@ -1,3 +1,4 @@
from typing import Dict
from core.config import CoreConfig from core.config import CoreConfig
@@ -20,6 +21,21 @@ class CardMakerServerConfig:
) )
class CardMakerVersionConfig:
def __init__(self, parent_config: "CardMakerConfig") -> None:
self.__config = parent_config
def version(self, version: int) -> Dict:
"""
in the form of:
1: {"ongeki": 1.30.01, "chuni": 2.00.00, "maimai": 1.20.00}
"""
return CoreConfig.get_config_field(
self.__config, "cardmaker", "version", default={}
)[version]
class CardMakerConfig(dict): class CardMakerConfig(dict):
def __init__(self) -> None: def __init__(self) -> None:
self.server = CardMakerServerConfig(self) self.server = CardMakerServerConfig(self)
self.version = CardMakerVersionConfig(self)
+1 -1
View File
@@ -6,7 +6,7 @@ class CardMakerConstants:
VER_CARD_MAKER = 0 VER_CARD_MAKER = 0
VER_CARD_MAKER_135 = 1 VER_CARD_MAKER_135 = 1
VERSION_NAMES = ("Card Maker 1.34", "Card Maker 1.35") VERSION_NAMES = ("Card Maker 1.30", "Card Maker 1.35")
@classmethod @classmethod
def game_ver_to_string(cls, ver: int): def game_ver_to_string(cls, ver: int):
+2 -2
View File
@@ -30,7 +30,7 @@ class CardMakerServlet:
self.versions = [ self.versions = [
CardMakerBase(core_cfg, self.game_cfg), CardMakerBase(core_cfg, self.game_cfg),
CardMaker135(core_cfg, self.game_cfg), CardMaker135(core_cfg, self.game_cfg)
] ]
self.logger = logging.getLogger("cardmaker") self.logger = logging.getLogger("cardmaker")
@@ -89,7 +89,7 @@ class CardMakerServlet:
if version >= 130 and version < 135: # Card Maker if version >= 130 and version < 135: # Card Maker
internal_ver = CardMakerConstants.VER_CARD_MAKER internal_ver = CardMakerConstants.VER_CARD_MAKER
elif version >= 135 and version < 136: # Card Maker 1.35 elif version >= 135 and version < 140: # Card Maker 1.35
internal_ver = CardMakerConstants.VER_CARD_MAKER_135 internal_ver = CardMakerConstants.VER_CARD_MAKER_135
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:
+1 -1
View File
@@ -103,7 +103,7 @@ class CxbServlet(resource.Resource):
else: else:
self.logger.info(f"Ready on port {self.game_cfg.server.port}") self.logger.info(f"Ready on port {self.game_cfg.server.port}")
def render_POST(self, request: Request): def render_POST(self, request: Request, version: int, endpoint: str):
version = 0 version = 0
internal_ver = 0 internal_ver = 0
func_to_find = "" func_to_find = ""
+6
View File
@@ -83,7 +83,13 @@ class IDZUserDBProtocol(Protocol):
def dataReceived(self, data: bytes) -> None: def dataReceived(self, data: bytes) -> None:
self.logger.debug(f"Receive data {data.hex()}") self.logger.debug(f"Receive data {data.hex()}")
crypt = AES.new(self.static_key, AES.MODE_ECB) crypt = AES.new(self.static_key, AES.MODE_ECB)
try:
data_dec = crypt.decrypt(data) data_dec = crypt.decrypt(data)
except Exception as e:
self.logger.error(f"Failed to decrypt UserDB request from {self.transport.getPeer().host} because {e} - {data.hex()}")
self.logger.debug(f"Decrypt data {data_dec.hex()}") self.logger.debug(f"Decrypt data {data_dec.hex()}")
magic = struct.unpack_from("<I", data_dec, 0)[0] magic = struct.unpack_from("<I", data_dec, 0)[0]
+1 -1
View File
@@ -16,4 +16,4 @@ game_codes = [
Mai2Constants.GAME_CODE_GREEN, Mai2Constants.GAME_CODE_GREEN,
Mai2Constants.GAME_CODE, Mai2Constants.GAME_CODE,
] ]
current_schema_version = 4 current_schema_version = 5
+26 -9
View File
@@ -632,21 +632,38 @@ class Mai2Base:
return {"userId": data["userId"], "length": 0, "userRegionList": []} return {"userId": data["userId"], "length": 0, "userRegionList": []}
def handle_get_user_music_api_request(self, data: Dict) -> Dict: def handle_get_user_music_api_request(self, data: Dict) -> Dict:
songs = self.data.score.get_best_scores(data["userId"]) user_id = data.get("userId", 0)
next_index = data.get("nextIndex", 0)
max_ct = data.get("maxCount", 50)
upper_lim = next_index + max_ct
music_detail_list = [] music_detail_list = []
next_index = 0
if songs is not None: if user_id <= 0:
for song in songs: self.logger.warn("handle_get_user_music_api_request: Could not find userid in data, or userId is 0")
tmp = song._asdict() return {}
songs = self.data.score.get_best_scores(user_id)
if songs is None:
self.logger.debug("handle_get_user_music_api_request: get_best_scores returned None!")
return {
"userId": data["userId"],
"nextIndex": 0,
"userMusicList": [],
}
num_user_songs = len(songs)
for x in range(next_index, upper_lim):
if num_user_songs <= x:
break
tmp = songs[x]._asdict()
tmp.pop("id") tmp.pop("id")
tmp.pop("user") tmp.pop("user")
music_detail_list.append(tmp) music_detail_list.append(tmp)
if len(music_detail_list) == data["maxCount"]: next_index = 0 if len(music_detail_list) < max_ct or num_user_songs == upper_lim else upper_lim
next_index = data["maxCount"] + data["nextIndex"] self.logger.info(f"Send songs {next_index}-{upper_lim} ({len(music_detail_list)}) out of {num_user_songs} for user {user_id} (next idx {next_index})")
break
return { return {
"userId": data["userId"], "userId": data["userId"],
"nextIndex": next_index, "nextIndex": next_index,
+3 -3
View File
@@ -71,9 +71,9 @@ class Mai2Constants:
"maimai DX PLUS", "maimai DX PLUS",
"maimai DX Splash", "maimai DX Splash",
"maimai DX Splash PLUS", "maimai DX Splash PLUS",
"maimai DX Universe", "maimai DX UNiVERSE",
"maimai DX Universe PLUS", "maimai DX UNiVERSE PLUS",
"maimai DX Festival", "maimai DX FESTiVAL",
) )
@classmethod @classmethod
+9 -3
View File
@@ -39,8 +39,8 @@ card = Table(
Column("cardTypeId", Integer, nullable=False), Column("cardTypeId", Integer, nullable=False),
Column("charaId", Integer, nullable=False), Column("charaId", Integer, nullable=False),
Column("mapId", Integer, nullable=False), Column("mapId", Integer, nullable=False),
Column("startDate", TIMESTAMP, server_default="2018-01-01 00:00:00.0"), Column("startDate", TIMESTAMP, nullable=False, server_default=func.now()),
Column("endDate", TIMESTAMP, server_default="2038-01-01 00:00:00.0"), Column("endDate", TIMESTAMP, nullable=False),
UniqueConstraint("user", "cardId", "cardTypeId", name="mai2_item_card_uk"), UniqueConstraint("user", "cardId", "cardTypeId", name="mai2_item_card_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@@ -444,6 +444,8 @@ class Mai2ItemData(BaseData):
card_kind: int, card_kind: int,
chara_id: int, chara_id: int,
map_id: int, map_id: int,
start_date: datetime,
end_date: datetime,
) -> Optional[Row]: ) -> Optional[Row]:
sql = insert(card).values( sql = insert(card).values(
user=user_id, user=user_id,
@@ -451,9 +453,13 @@ class Mai2ItemData(BaseData):
cardTypeId=card_kind, cardTypeId=card_kind,
charaId=chara_id, charaId=chara_id,
mapId=map_id, mapId=map_id,
startDate=start_date,
endDate=end_date,
) )
conflict = sql.on_duplicate_key_update(charaId=chara_id, mapId=map_id) conflict = sql.on_duplicate_key_update(
charaId=chara_id, mapId=map_id, startDate=start_date, endDate=end_date
)
result = self.execute(conflict) result = self.execute(conflict)
if result is None: if result is None:
+2
View File
@@ -7,6 +7,7 @@ from sqlalchemy.engine import Row
from sqlalchemy.dialects.mysql import insert from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata from core.data.schema import BaseData, metadata
from core.data import cached
best_score = Table( best_score = Table(
"mai2_score_best", "mai2_score_best",
@@ -190,6 +191,7 @@ class Mai2ScoreData(BaseData):
return None return None
return result.lastrowid return result.lastrowid
@cached(2)
def get_best_scores(self, user_id: int, song_id: int = None) -> Optional[List[Row]]: def get_best_scores(self, user_id: int, song_id: int = None) -> Optional[List[Row]]:
sql = best_score.select( sql = best_score.select(
and_( and_(
+199
View File
@@ -14,3 +14,202 @@ class Mai2Universe(Mai2DX):
def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None:
super().__init__(cfg, game_cfg) super().__init__(cfg, game_cfg)
self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE
def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
p = self.data.profile.get_profile_detail(data["userId"], self.version)
if p is None:
return {}
return {
"userName": p["userName"],
"rating": p["playerRating"],
# hardcode lastDataVersion for CardMaker 1.34
"lastDataVersion": "1.20.00",
"isLogin": False,
"isExistSellingCard": False,
}
def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict:
# user already exists, because the preview checks that already
p = self.data.profile.get_profile_detail(data["userId"], self.version)
cards = self.data.card.get_user_cards(data["userId"])
if cards is None or len(cards) == 0:
# This should never happen
self.logger.error(
f"handle_get_user_data_api_request: Internal error - No cards found for user id {data['userId']}"
)
return {}
# get the dict representation of the row so we can modify values
user_data = p._asdict()
# remove the values the game doesn't want
user_data.pop("id")
user_data.pop("user")
user_data.pop("version")
return {"userId": data["userId"], "userData": user_data}
def handle_cm_login_api_request(self, data: Dict) -> Dict:
return {"returnCode": 1}
def handle_cm_logout_api_request(self, data: Dict) -> Dict:
return {"returnCode": 1}
def handle_cm_get_selling_card_api_request(self, data: Dict) -> Dict:
selling_cards = self.data.static.get_enabled_cards(self.version)
if selling_cards is None:
return {"length": 0, "sellingCardList": []}
selling_card_list = []
for card in selling_cards:
tmp = card._asdict()
tmp.pop("id")
tmp.pop("version")
tmp.pop("cardName")
tmp.pop("enabled")
tmp["startDate"] = datetime.strftime(tmp["startDate"], "%Y-%m-%d %H:%M:%S")
tmp["endDate"] = datetime.strftime(tmp["endDate"], "%Y-%m-%d %H:%M:%S")
tmp["noticeStartDate"] = datetime.strftime(
tmp["noticeStartDate"], "%Y-%m-%d %H:%M:%S"
)
tmp["noticeEndDate"] = datetime.strftime(
tmp["noticeEndDate"], "%Y-%m-%d %H:%M:%S"
)
selling_card_list.append(tmp)
return {"length": len(selling_card_list), "sellingCardList": selling_card_list}
def handle_cm_get_user_card_api_request(self, data: Dict) -> Dict:
user_cards = self.data.item.get_cards(data["userId"])
if user_cards is None:
return {"returnCode": 1, "length": 0, "nextIndex": 0, "userCardList": []}
max_ct = data["maxCount"]
next_idx = data["nextIndex"]
start_idx = next_idx
end_idx = max_ct + start_idx
if len(user_cards[start_idx:]) > max_ct:
next_idx += max_ct
else:
next_idx = 0
card_list = []
for card in user_cards:
tmp = card._asdict()
tmp.pop("id")
tmp.pop("user")
tmp["startDate"] = datetime.strftime(
tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT
)
tmp["endDate"] = datetime.strftime(
tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT
)
card_list.append(tmp)
return {
"returnCode": 1,
"length": len(card_list[start_idx:end_idx]),
"nextIndex": next_idx,
"userCardList": card_list[start_idx:end_idx],
}
def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict:
super().handle_get_user_item_api_request(data)
def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict:
characters = self.data.item.get_characters(data["userId"])
chara_list = []
for chara in characters:
chara_list.append(
{
"characterId": chara["characterId"],
# no clue why those values are even needed
"point": 0,
"count": 0,
"level": chara["level"],
"nextAwake": 0,
"nextAwakePercent": 0,
"favorite": False,
"awakening": chara["awakening"],
"useCount": chara["useCount"],
}
)
return {
"returnCode": 1,
"length": len(chara_list),
"userCharacterList": chara_list,
}
def handle_cm_get_user_card_print_error_api_request(self, data: Dict) -> Dict:
return {"length": 0, "userPrintDetailList": []}
def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict:
user_id = data["userId"]
upsert = data["userPrintDetail"]
# set a random card serial number
serial_id = "".join([str(randint(0, 9)) for _ in range(20)])
# calculate start and end date of the card
start_date = datetime.utcnow()
end_date = datetime.utcnow() + timedelta(days=15)
user_card = upsert["userCard"]
self.data.item.put_card(
user_id,
user_card["cardId"],
user_card["cardTypeId"],
user_card["charaId"],
user_card["mapId"],
# add the correct start date and also the end date in 15 days
start_date,
end_date,
)
# get the profile extend to save the new bought card
extend = self.data.profile.get_profile_extend(user_id, self.version)
if extend:
extend = extend._asdict()
# parse the selectedCardList
# 6 = Freedom Pass, 4 = Gold Pass (cardTypeId)
selected_cards: list = extend["selectedCardList"]
# if no pass is already added, add the corresponding pass
if not user_card["cardTypeId"] in selected_cards:
selected_cards.insert(0, user_card["cardTypeId"])
extend["selectedCardList"] = selected_cards
self.data.profile.put_profile_extend(user_id, self.version, extend)
# properly format userPrintDetail for the database
upsert.pop("userCard")
upsert.pop("serialId")
upsert["printDate"] = datetime.strptime(upsert["printDate"], "%Y-%m-%d")
self.data.item.put_user_print_detail(user_id, serial_id, upsert)
return {
"returnCode": 1,
"orderId": 0,
"serialId": serial_id,
"startDate": datetime.strftime(start_date, Mai2Constants.DATE_TIME_FORMAT),
"endDate": datetime.strftime(end_date, Mai2Constants.DATE_TIME_FORMAT),
}
def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict:
return {
"returnCode": 1,
"orderId": 0,
"serialId": data["userPrintlog"]["serialId"],
}
def handle_cm_upsert_buy_card_api_request(self, data: Dict) -> Dict:
return {"returnCode": 1}
+84 -10
View File
@@ -1,6 +1,6 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
import json, logging import json, logging
from typing import Any, Dict from typing import Any, Dict, List
import random import random
from core.data import Data from core.data import Data
@@ -44,19 +44,19 @@ class PokkenBase:
biwa_setting = { biwa_setting = {
"MatchingServer": { "MatchingServer": {
"host": f"https://{self.game_cfg.server.hostname}", "host": f"https://{self.game_cfg.server.hostname}",
"port": self.game_cfg.server.port, "port": self.game_cfg.ports.game,
"url": "/SDAK/100/matching", "url": "/SDAK/100/matching",
}, },
"StunServer": { "StunServer": {
"addr": self.game_cfg.server.hostname, "addr": self.game_cfg.server.stun_server_host,
"port": self.game_cfg.server.port_stun, "port": self.game_cfg.server.stun_server_port,
}, },
"TurnServer": { "TurnServer": {
"addr": self.game_cfg.server.hostname, "addr": self.game_cfg.server.stun_server_host,
"port": self.game_cfg.server.port_turn, "port": self.game_cfg.server.stun_server_port,
}, },
"AdmissionUrl": f"ws://{self.game_cfg.server.hostname}:{self.game_cfg.server.port_admission}", "AdmissionUrl": f"ws://{self.game_cfg.server.hostname}:{self.game_cfg.ports.admission}",
"locationId": 123, "locationId": 123, # FIXME: Get arcade's ID from the database
"logfilename": "JackalMatchingLibrary.log", "logfilename": "JackalMatchingLibrary.log",
"biwalogfilename": "./biwa.log", "biwalogfilename": "./biwa.log",
} }
@@ -94,6 +94,7 @@ class PokkenBase:
res.type = jackal_pb2.MessageType.LOAD_CLIENT_SETTINGS res.type = jackal_pb2.MessageType.LOAD_CLIENT_SETTINGS
settings = jackal_pb2.LoadClientSettingsResponseData() settings = jackal_pb2.LoadClientSettingsResponseData()
# TODO: Make configurable
settings.money_magnification = 1 settings.money_magnification = 1
settings.continue_bonus_exp = 100 settings.continue_bonus_exp = 100
settings.continue_fight_money = 100 settings.continue_fight_money = 100
@@ -274,6 +275,60 @@ class PokkenBase:
res.result = 1 res.result = 1
res.type = jackal_pb2.MessageType.SAVE_USER res.type = jackal_pb2.MessageType.SAVE_USER
req = request.save_user
user_id = req.banapass_id
tut_flgs: List[int] = []
ach_flgs: List[int] = []
evt_flgs: List[int] = []
evt_params: List[int] = []
get_rank_pts: int = req.get_trainer_rank_point if req.get_trainer_rank_point else 0
get_money: int = req.get_money
get_score_pts: int = req.get_score_point if req.get_score_point else 0
grade_max: int = req.grade_max_num
extra_counter: int = req.extra_counter
evt_reward_get_flg: int = req.event_reward_get_flag
num_continues: int = req.continue_num
total_play_days: int = req.total_play_days
awake_num: int = req.awake_num # ?
use_support_ct: int = req.use_support_num
beat_num: int = req.beat_num # ?
evt_state: int = req.event_state
aid_skill: int = req.aid_skill
last_evt: int = req.last_play_event_id
battle = req.battle_data
mon = req.pokemon_data
self.data.profile.update_support_team(user_id, 1, req.support_set_1[0], req.support_set_1[1])
self.data.profile.update_support_team(user_id, 2, req.support_set_2[0], req.support_set_2[1])
self.data.profile.update_support_team(user_id, 3, req.support_set_3[0], req.support_set_3[1])
if req.trainer_name_pending: # we're saving for the first time
self.data.profile.set_profile_name(user_id, req.trainer_name_pending, req.avatar_gender if req.avatar_gender else None)
for tut_flg in req.tutorial_progress_flag:
tut_flgs.append(tut_flg)
self.data.profile.update_profile_tutorial_flags(user_id, tut_flgs)
for ach_flg in req.achievement_flag:
ach_flgs.append(ach_flg)
self.data.profile.update_profile_tutorial_flags(user_id, ach_flg)
for evt_flg in req.event_achievement_flag:
evt_flgs.append(evt_flg)
for evt_param in req.event_achievement_param:
evt_params.append(evt_param)
self.data.profile.update_profile_event(user_id, evt_state, evt_flgs, evt_params, )
for reward in req.reward_data:
self.data.item.add_reward(user_id, reward.get_category_id, reward.get_content_id, reward.get_type_id)
return res.SerializeToString() return res.SerializeToString()
def handle_save_ingame_log(self, data: jackal_pb2.Request) -> bytes: def handle_save_ingame_log(self, data: jackal_pb2.Request) -> bytes:
@@ -307,11 +362,30 @@ class PokkenBase:
"pcb_id": data["data"]["must"]["pcb_id"], "pcb_id": data["data"]["must"]["pcb_id"],
"gip": client_ip "gip": client_ip
}, },
"list":[]
""" """
return {} return {
"data": {
"sessionId":"12345678",
"A":{
"pcb_id": data["data"]["must"]["pcb_id"],
"gip": client_ip
},
"list":[]
}
}
def handle_matching_stop_matching( def handle_matching_stop_matching(
self, data: Dict = {}, client_ip: str = "127.0.0.1" self, data: Dict = {}, client_ip: str = "127.0.0.1"
) -> Dict: ) -> Dict:
return {} return {}
def handle_admission_noop(self, data: Dict, req_ip: str = "127.0.0.1") -> Dict:
return {}
def handle_admission_joinsession(self, data: Dict, req_ip: str = "127.0.0.1") -> Dict:
self.logger.info(f"Admission: JoinSession from {req_ip}")
return {
'data': {
"id": 12345678
}
}
+44 -24
View File
@@ -25,30 +25,6 @@ class PokkenServerConfig:
) )
) )
@property
def port(self) -> int:
return CoreConfig.get_config_field(
self.__config, "pokken", "server", "port", default=9000
)
@property
def port_stun(self) -> int:
return CoreConfig.get_config_field(
self.__config, "pokken", "server", "port_stun", default=9001
)
@property
def port_turn(self) -> int:
return CoreConfig.get_config_field(
self.__config, "pokken", "server", "port_turn", default=9002
)
@property
def port_admission(self) -> int:
return CoreConfig.get_config_field(
self.__config, "pokken", "server", "port_admission", default=9003
)
@property @property
def auto_register(self) -> bool: def auto_register(self) -> bool:
""" """
@@ -59,7 +35,51 @@ class PokkenServerConfig:
self.__config, "pokken", "server", "auto_register", default=True self.__config, "pokken", "server", "auto_register", default=True
) )
@property
def enable_matching(self) -> bool:
"""
If global matching should happen
"""
return CoreConfig.get_config_field(
self.__config, "pokken", "server", "enable_matching", default=False
)
@property
def stun_server_host(self) -> str:
"""
Hostname of the EXTERNAL stun server the game should connect to. This is not handled by artemis.
"""
return CoreConfig.get_config_field(
self.__config, "pokken", "server", "stun_server_host", default="stunserver.stunprotocol.org"
)
@property
def stun_server_port(self) -> int:
"""
Port of the EXTERNAL stun server the game should connect to. This is not handled by artemis.
"""
return CoreConfig.get_config_field(
self.__config, "pokken", "server", "stun_server_port", default=3478
)
class PokkenPortsConfig:
def __init__(self, parent_config: "PokkenConfig"):
self.__config = parent_config
@property
def game(self) -> int:
return CoreConfig.get_config_field(
self.__config, "pokken", "ports", "game", default=9000
)
@property
def admission(self) -> int:
return CoreConfig.get_config_field(
self.__config, "pokken", "ports", "admission", default=9001
)
class PokkenConfig(dict): class PokkenConfig(dict):
def __init__(self) -> None: def __init__(self) -> None:
self.server = PokkenServerConfig(self) self.server = PokkenServerConfig(self)
self.ports = PokkenPortsConfig(self)
+6 -6
View File
@@ -11,14 +11,14 @@ class PokkenConstants:
VERSION_NAMES = "Pokken Tournament" VERSION_NAMES = "Pokken Tournament"
class BATTLE_TYPE(Enum): class BATTLE_TYPE(Enum):
BATTLE_TYPE_TUTORIAL = 1 TUTORIAL = 1
BATTLE_TYPE_AI = 2 AI = 2
BATTLE_TYPE_LAN = 3 LAN = 3
BATTLE_TYPE_WAN = 4 WAN = 4
class BATTLE_RESULT(Enum): class BATTLE_RESULT(Enum):
BATTLE_RESULT_WIN = 1 WIN = 1
BATTLE_RESULT_LOSS = 2 LOSS = 2
@classmethod @classmethod
def game_ver_to_string(cls, ver: int): def game_ver_to_string(cls, ver: int):
+7 -1
View File
@@ -2,8 +2,9 @@ import yaml
import jinja2 import jinja2
from twisted.web.http import Request from twisted.web.http import Request
from os import path from os import path
from twisted.web.server import Session
from core.frontend import FE_Base from core.frontend import FE_Base, IUserSession
from core.config import CoreConfig from core.config import CoreConfig
from .database import PokkenData from .database import PokkenData
from .config import PokkenConfig from .config import PokkenConfig
@@ -27,7 +28,12 @@ class PokkenFrontend(FE_Base):
template = self.environment.get_template( template = self.environment.get_template(
"titles/pokken/frontend/pokken_index.jinja" "titles/pokken/frontend/pokken_index.jinja"
) )
sesh: Session = request.getSession()
usr_sesh = IUserSession(sesh)
return template.render( return template.render(
title=f"{self.core_config.server.name} | {self.nav_name}", title=f"{self.core_config.server.name} | {self.nav_name}",
game_list=self.environment.globals["game_list"], game_list=self.environment.globals["game_list"],
sesh=vars(usr_sesh)
).encode("utf-16") ).encode("utf-16")
+14 -7
View File
@@ -1,6 +1,7 @@
from typing import Tuple from typing import Tuple
from twisted.web.http import Request from twisted.web.http import Request
from twisted.web import resource from twisted.web import resource
from twisted.internet import reactor
import json, ast import json, ast
from datetime import datetime from datetime import datetime
import yaml import yaml
@@ -11,10 +12,11 @@ from os import path
from google.protobuf.message import DecodeError from google.protobuf.message import DecodeError
from core import CoreConfig, Utils from core import CoreConfig, Utils
from titles.pokken.config import PokkenConfig from .config import PokkenConfig
from titles.pokken.base import PokkenBase from .base import PokkenBase
from titles.pokken.const import PokkenConstants from .const import PokkenConstants
from titles.pokken.proto import jackal_pb2 from .proto import jackal_pb2
from .services import PokkenAdmissionFactory
class PokkenServlet(resource.Resource): class PokkenServlet(resource.Resource):
@@ -69,7 +71,7 @@ class PokkenServlet(resource.Resource):
return ( return (
True, True,
f"https://{game_cfg.server.hostname}:{game_cfg.server.port}/{game_code}/$v/", f"https://{game_cfg.server.hostname}:{game_cfg.ports.game}/{game_code}/$v/",
f"{game_cfg.server.hostname}/SDAK/$v/", f"{game_cfg.server.hostname}/SDAK/$v/",
) )
@@ -90,8 +92,10 @@ class PokkenServlet(resource.Resource):
return (True, "PKF1") return (True, "PKF1")
def setup(self) -> None: def setup(self) -> None:
# TODO: Setup stun, turn (UDP) and admission (WSS) servers if self.game_cfg.server.enable_matching:
pass reactor.listenTCP(
self.game_cfg.ports.admission, PokkenAdmissionFactory(self.core_cfg, self.game_cfg)
)
def render_POST( def render_POST(
self, request: Request, version: int = 0, endpoints: str = "" self, request: Request, version: int = 0, endpoints: str = ""
@@ -128,6 +132,9 @@ class PokkenServlet(resource.Resource):
return ret return ret
def handle_matching(self, request: Request) -> bytes: def handle_matching(self, request: Request) -> bytes:
if not self.game_cfg.server.enable_matching:
return b""
content = request.content.getvalue() content = request.content.getvalue()
client_ip = Utils.get_ip_addr(request) client_ip = Utils.get_ip_addr(request)
+13 -1
View File
@@ -31,4 +31,16 @@ class PokkenItemData(BaseData):
Items obtained as rewards Items obtained as rewards
""" """
pass def add_reward(self, user_id: int, category: int, content: int, item_type: int) -> Optional[int]:
sql = insert(item).values(
user=user_id,
category=category,
content=content,
type=item_type,
)
result = self.execute(sql)
if result is None:
self.logger.warn(f"Failed to insert reward for user {user_id}: {category}-{content}-{item_type}")
return None
return result.lastrowid

Some files were not shown because too many files have changed in this diff Show More