Merge branch 'develop' into develop

This commit is contained in:
Hay1tsme 2024-06-29 04:03:14 +00:00
commit b2bd73a8f5
252 changed files with 104986 additions and 29073 deletions

View File

@ -1,6 +1,41 @@
# Changelog
Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to.
## 20240628
### maimai
+ Add present support
## 20240627
### SAO
+ Fix ghost items, character and player XP, EX Bonuses, unlocks, and much much more
## 20240620
### CHUNITHM
+ CHUNITHM LUMINOUS support
## 20240616
### CHUNITHM
+ Support network encryption for Export/International versions
### DIVA
+ Working frontend with name and level strings edit and playlog
## 20240530
### DIVA
+ Fix reader for when dificulty is not a int
## 20240526
### DIVA
+ Fixed missing awaits causing coroutine error
## 20240524
### DIVA
+ Fixed new profile start request causing coroutine error
## 20240523
### DIVA
+ Fixed binary handler & render_POST errors
## 20240408
### System
+ Modified the game specific documentation

View File

@ -1,8 +1,182 @@
# Contributing to ARTEMiS
If you would like to contribute to artemis, either by adding features, games, or fixing bugs, you can do so by forking the repo and submitting a pull request [here](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls). Please make sure, if you're submitting a PR for a game or game version, that you're following the n-0/y-1 guidelines, or it will be rejected.
If you would like to contribute to artemis, either by adding features, games, or fixing bugs, you can do so by forking the repo and submitting a pull request [here](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls). This guide assume you're familiar with both git, python, and the libraries that artemis uses.
This document is a work in progress. If you have any questions or notice any errors, please report it to the discord.
## Adding games
Guide WIP
### Step 0
+ Follow the "n-1" rule of thumb. PRs for game versions that are currently active in arcades will be deleted. If you're unsure, ask!
+ Always PR against the `develop` branch.
+ Check to see if somebody else is already PRing the features/games you want to add. If they are, consider contributing to them rather then making an entirely new PR.
+ We don't technically have a written code style guide (TODO) but try to keep your code consistant with code that's already there where possible.
### Step 1 (Setup)
1) Fork the gitea repo, clone your fork, and checkout the develop branch.
2) Make a new folder in the `titles` folder, name it some recogniseable shorthand for your game (Chunithm becomes chuni, maimai dx is mai2, etc)
3) In this new folder, create a file named `__init__.py`. This is the first thing that will load when your title module is loaded by the core system, and it acts as sort of a directory for where everything lives in your module. This file will contain the following required items:
+ `index`: must point to a subclass of `BaseServlet` that will handle setup and dispatching of your game.
+ `game_codes`: must be a list of 4 letter SEGA game codes as strings.
It can also contain the following optional fields:
+ `database`: points to a subclass of `Data` that contains one or more subclasses of `BaseData` that act as database transaction handlers. Required for the class to store and retrieve data from the database.
+ `reader`: points to a subclass of `BaseReader` that handles importing static data from game files into the database.
+ `frontend`: points to a subclass of `FE_Base` that handles frontend routes for your game.
The next step will focus on `index`
### Step 2 (Index)
1) Create another file in your game's folder. By convention, it should be called `index.py`.
2) Inside `index.py`, add the following code, replacing {Game name here} with the name of your game, without spaces or special characters. Look at other titles for examples.
```py
from core.title import BaseServlet
from core import CoreConfig
class {Game name here}Servlet(BaseServlet):
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
pass
```
3) The `__init__` function should acomplish the following:
+ Reading your game's config
+ Setting up your games logger
+ Instancing your games versions
It's usually safe to copy and paste the `__init__` functions from other games, just make sure you change everything that needs to be changed!
4) Go back to the `__init__.py` that you created and add the following:
```py
from .index import {Game name here}Servlet
index = {Game name here}Servlet
```
5) Going back to `index.py`, within the Servlet class, define the following functions from `BaseServlet` as needed (see function documentation):
+ `is_game_enabled`: Returns true if the game is enabled and should be served, false otherwise. Returns false by default, so override this to allow your game to be served.
+ `get_routes`: Returns a list of Starlette routes that your game will serve.
+ `get_allnet_info`: Returns a tuple of strings where the first is the allnet uri and the second is the allnet host. The function takes the game ID, version and keychip ID as parameters, so you can send different responses if need be.
+ `get_mucha_info`: Only used by games that use Mucha as authentication. Returns a tuple where the first is a bool that is weather or not the game is enabled, the 2nd is a list of game CDs as strings that this servlet should handle, and the 3rd is a list of netID prefixes that each game CD should use. If your game does not use mucha, do not define this function.
+ `setup`: Preforms any setup your servlet requires, such as spinning up matching servers. It is run once when the server starts. If you don't need any setup, do not define.
6) Make sure any functions you specify to handle routes in `get_routes` are defined as async, as follows: `async def handle_thing(self, request: Request) -> Response:` where Response is whatever kind of Response class you'll be returning. Make sure all paths in this function return some subclass of Response, otherwise you'll get an error when serving.
### Step 3 (Constants)
1) In your game's folder, create a file to store static values for your game. By convention, we call this `const.py`
2) Inside, create a class called `{Game name here}Constants`. Do not define an `__init__` function.
3) Put constants related to your game here. A good example of something to put here is game codes.
```py
class {Game name here}Constants:
GAME_CODE = "SBXX"
CONFIG_NAME = "{game name}.yaml"
```
4) If you choose to put game codes in here, add this to your `__init__.py` file:
```py
from .const import {Game name here}Constants
...
game_codes = [{Game name here}Constants.GAME_CODE]
```
### Step 4 (Config)
1) Make a file to store your game's config. By convention, it should be called `config.py`
2) Inside that file, add the following:
```py
from core.config import CoreConfig
class {game name}ServerConfig:
def __init__(self, parent_config: "{game name}Config") -> None:
self.__config = parent_config
@property
def enable(self) -> bool:
return CoreConfig.get_config_field(
self.__config, "{game name}", "server", "enable", default=True
)
@property
def loglevel(self) -> int:
return CoreConfig.str_to_loglevel(
CoreConfig.get_config_field(
self.__config, "{game name}", "server", "loglevel", default="info"
)
)
class {game name}Config(dict):
def __init__(self) -> None:
self.server = {game name}ServerConfig(self)
```
3) In the `example_config` folder, create a yaml file for your game. By convention, it should be called `{game folder name}.ymal`. Add the following:
```yaml
server:
enable: True
loglevel: "info"
```
4) Add any additional config options that you feel the game needs. Look to other games for config examples.
5) In `index.py` import your config and instance it in `__init__` with:
```py
self.game_cfg = {game folder name}Config()
if path.exists(f"{cfg_dir}/{game folder name}Constants.CONFIG_NAME}"):
self.game_cfg.update(
yaml.safe_load(open(f"{cfg_dir}/{game folder name}Constants.CONFIG_NAME}"))
)
```
This will attempt to load the config file you specified in your constants, and if not, go with the defaults specified in `config.py`. This game_cfg object can then be passed down to your handlers when you create them.
At this stage your game should be loaded by allnet, and serve whatever routes you put in `get_routes`. See the next section about adding versions and handlers.
### Step 5 (Database)
TODO
### Step 6 (Frontend)
TODO
### Step 7 (Reader)
TODO
## Adding game versions
Guide WIP
See the above section about code expectations and how to PR.
1) In the game's folder, create a python file to contain the version handlers. By convention, the first version is version 0, and is stored in `base.py`. Versions following that increment the version number, and are stored in `{short version name}.py`. See Wacca's folder for an example of how to name versions.
2) Internal version numbers should be defined in `const.py`. The version should change any time the game gets a major update (i.e. a new version or plus version.)
```py
# in const.py
VERSION_{game name} = 0
VERSION_{game name}_PLUS = 1
```
3) Inside `base.py` (or whatever your version is named) add the following:
```py
class {game name}Base:
def __init__(self, cfg: CoreConfig, game_cfg: {game name}Config) -> None:
self.game_config = game_cfg
self.core_config = cfg
self.version = {game name}Constants.VERSION_{game name}
self.data = {game name}Data(cfg)
# Any other initialization stuff
```
4) Define your handlers. This will vary wildly by game, but best practice is to keep the naming consistant, so that the main dispatch function in `index.py` can use `getattr` to get the handler, rather then having a static list of what endpoint or request type goes to which handler. See Wacca's `index.py` and `base.py` for examples of how to do this.
5) If your version is not the base version, make sure it inherits from the base version:
```py
class {game name}Plus({game name}Base):
def __init__(self, cfg: CoreConfig, game_cfg: {game name}Config) -> None:
super().__init__(cfg, game_cfg)
self.version = {game name}Constants.VERSION_{game name}_PLUS
```
6) Back in `index.py` make sure to import your new class, and add it to `__init__`. Some games may opt to just a single list called `self.versions` that contains all the version classes at their internal version's index. Others may simply define them as seperate members. See Wacca for an example of `self.versions`
7) Add your version to your game's dispatching logic.
8) Test to make sure your game is being handled properly.
9) Submit a PR.
## Adding/improving core services
If you intend to submit improvements or additions to core services (allnet, mucha, billing, aimedb, database, etc) please get in touch with a maintainer.

View File

@ -10,13 +10,14 @@ class ADBFelicaLookupRequest(ADBBaseRequest):
self.pmm = hex(pmm)[2:].upper()
class ADBFelicaLookupResponse(ADBBaseResponse):
def __init__(self, access_code: str = None, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x03, length: int = 0x30, status: int = 1) -> None:
def __init__(self, access_code: str = None, idx: int = 0, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x03, length: int = 0x30, status: int = 1) -> None:
super().__init__(code, length, status, game_id, store_id, keychip_id)
self.access_code = access_code if access_code is not None else "00000000000000000000"
self.idx = idx
@classmethod
def from_req(cls, req: ADBHeader, access_code: str = None) -> "ADBFelicaLookupResponse":
c = cls(access_code, req.game_id, req.store_id, req.keychip_id)
def from_req(cls, req: ADBHeader, access_code: str = None, idx: int = 0) -> "ADBFelicaLookupResponse":
c = cls(access_code, idx, req.game_id, req.store_id, req.keychip_id)
c.head.protocol_ver = req.protocol_ver
return c
@ -26,7 +27,7 @@ class ADBFelicaLookupResponse(ADBBaseResponse):
"access_code" / Int8ub[10],
Padding(2)
).build(dict(
felica_idx = 0,
felica_idx = self.idx,
access_code = bytes.fromhex(self.access_code)
))

View File

@ -194,6 +194,9 @@ class AimedbServlette():
if user_id and user_id > 0:
await self.data.card.update_card_last_login(req.access_code)
if (req.access_code.startswith("010") or req.access_code.startswith("3")) and req.serial_number != 0x04030201: # Default segatools sn
await self.data.card.set_chip_id_by_access_code(req.access_code, req.serial_number)
self.logger.info(f"Attempt to set chip id to {req.serial_number:08X} for access code {req.access_code}")
return ret
async def handle_lookup_ex(self, data: bytes, resp_code: int) -> ADBBaseResponse:
@ -229,23 +232,34 @@ class AimedbServlette():
async def handle_felica_lookup(self, data: bytes, resp_code: int) -> bytes:
"""
On official, I think a card has to be registered for this to actually work, but
I'm making the executive decision to not implement that and just kick back our
faux generated access code. The real felica IDm -> access code conversion is done
on the ADB server, which we do not and will not ever have access to. Because we can
assure that all IDms will be unique, this basic 0-padded hex -> int conversion will
be fine.
On official, the IDm is used as a key to look up the stored access code in a large
database. We do not have access to that database so we have to make due with what we got.
Interestingly, namco games are able to read S_PAD0 and send the server the correct access
code, but aimedb doesn't. Until somebody either enters the correct code manually, or scans
on a game that reads it correctly from the card, this will have to do. It's the same conversion
used on the big boy networks.
"""
req = ADBFelicaLookupRequest(data)
ac = self.data.card.to_access_code(req.idm)
idm = req.idm.zfill(16)
card = await self.data.card.get_card_by_idm(idm)
if not card:
ac = self.data.card.to_access_code(idm)
test = await self.data.card.get_card_by_access_code(ac)
if test:
await self.data.card.set_idm_by_access_code(ac, idm)
else:
ac = card['access_code']
self.logger.info(
f"idm {req.idm} ipm {req.pmm} -> access_code {ac}"
f"idm {idm} ipm {req.pmm.zfill(16)} -> access_code {ac}"
)
return ADBFelicaLookupResponse.from_req(req.head, ac)
async def handle_felica_register(self, data: bytes, resp_code: int) -> bytes:
"""
I've never seen this used.
Used to register felica moble access codes. Will never be used on our network
because we don't implement felica_lookup properly.
"""
req = ADBFelicaLookupRequest(data)
ac = self.data.card.to_access_code(req.idm)
@ -279,18 +293,37 @@ class AimedbServlette():
async def handle_felica_lookup_ex(self, data: bytes, resp_code: int) -> bytes:
req = ADBFelicaLookup2Request(data)
access_code = self.data.card.to_access_code(req.idm)
user_id = await self.data.card.get_user_id_from_card(access_code=access_code)
user_id = None
idm = req.idm.zfill(16)
card = await self.data.card.get_card_by_idm(idm)
if not card:
access_code = self.data.card.to_access_code(idm)
card = await self.data.card.get_card_by_access_code(access_code)
if card:
user_id = card['user']
await self.data.card.set_idm_by_access_code(access_code, idm)
else:
user_id = card['user']
access_code = card['access_code']
if user_id is None:
user_id = -1
self.logger.info(
f"idm {req.idm} ipm {req.pmm} -> access_code {access_code} user_id {user_id}"
f"idm {idm} ipm {req.pmm} -> access_code {access_code} user_id {user_id}"
)
resp = ADBFelicaLookup2Response.from_req(req.head, user_id, access_code)
if user_id > 0:
if card['is_banned'] and card['is_locked']:
resp.head.status = ADBStatus.BAN_SYS_USER
elif card['is_banned']:
resp.head.status = ADBStatus.BAN_SYS
elif card['is_locked']:
resp.head.status = ADBStatus.LOCK_USER
if user_id and user_id > 0 and self.config.aimedb.id_secret:
auth_key = create_sega_auth_key(user_id, req.head.game_id, req.head.store_id, req.head.keychip_id, self.config.aimedb.id_secret, self.config.aimedb.id_lifetime_seconds)
if auth_key is not None:
@ -338,6 +371,16 @@ class AimedbServlette():
f"Registration blocked!: access code {req.access_code}"
)
if user_id > 0:
if (req.access_code.startswith("010") or req.access_code.startswith("3")) and req.serial_number != 0x04030201: # Default segatools sn:
await self.data.card.set_chip_id_by_access_code(req.access_code, req.serial_number)
self.logger.info(f"Attempt to set chip id to {req.serial_number} for access code {req.access_code}")
elif req.access_code.startswith("0008"):
idm = self.data.card.to_idm(req.access_code)
await self.data.card.set_idm_by_access_code(req.access_code, idm)
self.logger.info(f"Attempt to set IDm to {idm} for access code {req.access_code}")
resp = ADBLookupResponse.from_req(req.head, user_id)
if resp.user_id <= 0:
resp.head.status = ADBStatus.BAN_SYS # Closest we can get to a "You cannot register"

View File

@ -171,7 +171,7 @@ class AllnetServlet:
if machine is None and not self.config.server.allow_unregistered_serials:
msg = f"Unrecognised serial {req.serial} attempted allnet auth from {request_ip}."
await self.data.base.log_event(
"allnet", "ALLNET_AUTH_UNKNOWN_SERIAL", logging.WARN, msg
"allnet", "ALLNET_AUTH_UNKNOWN_SERIAL", logging.WARN, msg, {"serial": req.serial}, None, None, None, request_ip, req.game_id, req.ver
)
self.logger.warning(msg)
@ -183,9 +183,9 @@ class AllnetServlet:
arcade = await self.data.arcade.get_arcade(machine["arcade"])
if self.config.server.check_arcade_ip:
if arcade["ip"] and arcade["ip"] is not None and arcade["ip"] != req.ip:
msg = f"Serial {req.serial} attempted allnet auth from bad IP {req.ip} (expected {arcade['ip']})."
msg = f"{req.serial} attempted allnet auth from bad IP {req.ip} (expected {arcade['ip']})."
await self.data.base.log_event(
"allnet", "ALLNET_AUTH_BAD_IP", logging.ERROR, msg
"allnet", "ALLNET_AUTH_BAD_IP", logging.ERROR, msg, {}, None, arcade['id'], machine['id'], request_ip, req.game_id, req.ver
)
self.logger.warning(msg)
@ -194,9 +194,9 @@ class AllnetServlet:
return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n")
elif (not arcade["ip"] or arcade["ip"] is None) and self.config.server.strict_ip_checking:
msg = f"Serial {req.serial} attempted allnet auth from bad IP {req.ip}, but arcade {arcade['id']} has no IP set! (strict checking enabled)."
msg = f"{req.serial} attempted allnet auth from bad IP {req.ip}, but arcade {arcade['id']} has no IP set! (strict checking enabled)."
await self.data.base.log_event(
"allnet", "ALLNET_AUTH_NO_SHOP_IP", logging.ERROR, msg
"allnet", "ALLNET_AUTH_NO_SHOP_IP", logging.ERROR, msg, {}, None, arcade['id'], machine['id'], request_ip, req.game_id, req.ver
)
self.logger.warning(msg)
@ -204,6 +204,16 @@ class AllnetServlet:
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n")
if machine['game'] and machine['game'] != req.game_id:
msg = f"{req.serial} attempted allnet auth with bad game ID {req.game_id} (expected {machine['game']})."
await self.data.base.log_event(
"allnet", "ALLNET_AUTH_BAD_GAME", logging.ERROR, msg, {}, None, arcade['id'], machine['id'], request_ip, req.game_id, req.ver
)
self.logger.warning(msg)
resp.stat = ALLNET_STAT.bad_game.value
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n")
country = (
arcade["country"] if machine["country"] is None else machine["country"]
@ -236,11 +246,14 @@ class AllnetServlet:
arcade["timezone"] if arcade["timezone"] is not None else "+0900" if req.format_ver == 3 else "+09:00"
)
else:
arcade = None
if req.game_id not in TitleServlet.title_registry:
if not self.config.server.is_develop:
msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}."
await self.data.base.log_event(
"allnet", "ALLNET_AUTH_UNKNOWN_GAME", logging.WARN, msg
"allnet", "ALLNET_AUTH_UNKNOWN_GAME", logging.WARN, msg, {}, None, arcade['id'] if arcade else None, machine['id'] if machine else None, request_ip, req.game_id, req.ver
)
self.logger.warning(msg)
@ -271,8 +284,17 @@ class AllnetServlet:
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n")
if machine and arcade:
msg = f"{req.serial} authenticated from {request_ip}: {req.game_id} v{req.ver}"
await self.data.base.log_event("allnet", "ALLNET_AUTH_SUCCESS", logging.INFO, msg)
await self.data.base.log_event(
"allnet", "ALLNET_AUTH_SUCCESS", logging.INFO, msg, {}, None, arcade['id'], machine['id'], request_ip, req.game_id, req.ver
)
else:
msg = f"Allow unregistered serial {req.serial} to authenticate from {request_ip}: {req.game_id} v{req.ver}"
await self.data.base.log_event(
"allnet", "ALLNET_AUTH_SUCCESS_UNREG", logging.INFO, msg, {"serial": req.serial}, None, None, None, request_ip, req.game_id, req.ver
)
self.logger.info(msg)
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
@ -329,7 +351,11 @@ class AllnetServlet:
):
return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n")
else: # TODO: Keychip check
else:
machine = await self.data.arcade.get_machine(req.serial)
if not machine or not machine['ota_enable'] or not machine['is_cab'] or machine['is_blacklisted']:
return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n")
if path.exists(
f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver.replace('.', '')}-app.ini"
):
@ -340,8 +366,13 @@ class AllnetServlet:
):
resp.uri += f"|http://{self.config.server.hostname}:{self.config.server.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-opt.ini"
self.logger.debug(f"Sending download uri {resp.uri}")
await 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}")
if resp.uri:
self.logger.info(f"Sending download uri {resp.uri}")
await self.data.base.log_event(
"allnet", "DLORDER_REQ_SUCCESS", logging.INFO, f"Send download URI to {req.serial} for {req.game_id} v{req.ver} from {Utils.get_ip_addr(request)}", {"uri": resp.uri}, None,
machine['arcade'], machine['id'], request_ip, req.game_id, req.ver
)
# Maybe add a log event for checkin but no url sent?
res_str = urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n"
@ -357,13 +388,16 @@ class AllnetServlet:
async def handle_dlorder_ini(self, request: Request) -> bytes:
req_file = request.path_params.get("file", "").replace("%0A", "").replace("\n", "")
request_ip = Utils.get_ip_addr(request)
if not req_file:
return PlainTextResponse(status_code=404)
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")
await self.data.base.log_event("allnet", "DLORDER_INI_SENT", logging.INFO, f"{Utils.get_ip_addr(request)} successfully recieved {req_file}")
self.logger.info(f"Request for DL INI file {req_file} from {request_ip} successful")
await self.data.base.log_event(
"allnet", "DLORDER_INI_SENT", logging.INFO, f"{request_ip} successfully recieved {req_file}", {"file": req_file}, ip=request_ip
)
return PlainTextResponse(open(
f"{self.config.allnet.update_cfg_folder}/{req_file}", "r", encoding="utf-8"
@ -401,7 +435,13 @@ class AllnetServlet:
msg = f"{rep.serial} @ {client_ip} reported {rep.rep_type.name} download state {rep.rf_state.name} for {rep.gd} v{rep.dav}:"\
f" {rep.tdsc}/{rep.tsc} segments downloaded for working files {rep.wfl} with {rep.dfl if rep.dfl else 'none'} complete."
await self.data.base.log_event("allnet", "DL_REPORT", logging.INFO, msg, dl_data)
machine = await self.data.arcade.get_machine(rep.serial)
if machine:
await self.data.base.log_event("allnet", "DL_REPORT", logging.INFO, msg, dl_data, None, machine['arcade'], machine['id'], client_ip, rep.gd, rep.dav)
else:
msg = "Unknown serial " + msg
await self.data.base.log_event("allnet", "DL_REPORT_UNREG", logging.INFO, msg, dl_data, None, None, None, client_ip, rep.gd, rep.dav)
self.logger.info(msg)
return PlainTextResponse("OK")
@ -421,14 +461,24 @@ class AllnetServlet:
if serial is None or num_files_dld is None or num_files_to_dl is None or dl_state is None:
return PlainTextResponse("NG")
self.logger.info(f"LoaderStateRecorder Request from {ip} {serial}: {num_files_dld}/{num_files_to_dl} Files download (State: {dl_state})")
msg = f"LoaderStateRecorder Request from {ip} {serial}: {num_files_dld}/{num_files_to_dl} Files download (State: {dl_state})"
machine = await self.data.arcade.get_machine(serial)
if machine:
await self.data.base.log_event("allnet", "LSR_REPORT", logging.INFO, msg, req_dict, None, machine['arcade'], machine['id'], ip)
else:
msg = "Unregistered " + msg
await self.data.base.log_event("allnet", "LSR_REPORT_UNREG", logging.INFO, msg, req_dict, None, None, None, ip)
self.logger.info(msg)
return PlainTextResponse("OK")
async def handle_alive(self, request: Request) -> bytes:
return PlainTextResponse("OK")
async def handle_naomitest(self, request: Request) -> bytes:
self.logger.info(f"Ping from {Utils.get_ip_addr(request)}")
# This could be spam-able, removing
#self.logger.info(f"Ping from {Utils.get_ip_addr(request)}")
return PlainTextResponse("naomi ok")
def allnet_req_to_dict(self, data: str) -> Optional[List[Dict[str, Any]]]:
@ -558,18 +608,35 @@ class BillingServlet:
if machine is None and not self.config.server.allow_unregistered_serials:
msg = f"Unrecognised serial {req.keychipid} attempted billing checkin from {request_ip} for {req.gameid} v{req.gamever}."
await self.data.base.log_event(
"allnet", "BILLING_CHECKIN_NG_SERIAL", logging.WARN, msg
"allnet", "BILLING_CHECKIN_NG_SERIAL", logging.WARN, msg, ip=request_ip, game=req.gameid, version=req.gamever
)
self.logger.warning(msg)
return PlainTextResponse(f"result=1&requestno={req.requestno}&message=Keychip Serial bad\r\n")
msg = (
f"Billing checkin from {request_ip}: game {req.gameid} ver {req.gamever} keychip {req.keychipid} playcount "
log_details = {
"playcount": req.playcnt,
"billing_type": req.billingtype.name,
"nearfull": req.nearfull,
"playlimit": req.playlimit,
}
if machine is not None:
await self.data.base.log_event("billing", "BILLING_CHECKIN_OK", logging.INFO, "", log_details, None, machine['arcade'], machine['id'], request_ip, req.gameid, req.gamever)
self.logger.info(
f"Unregistered Billing checkin from {request_ip}: game {req.gameid} ver {req.gamever} keychip {req.keychipid} playcount "
f"{req.playcnt} billing_type {req.billingtype.name} nearfull {req.nearfull} playlimit {req.playlimit}"
)
self.logger.info(msg)
await self.data.base.log_event("billing", "BILLING_CHECKIN_OK", logging.INFO, msg)
else:
log_details['serial'] = req.keychipid
await self.data.base.log_event("billing", "BILLING_CHECKIN_OK_UNREG", logging.INFO, "", log_details, None, None, None, request_ip, req.gameid, req.gamever)
self.logger.info(
f"Unregistered Billing checkin from {request_ip}: game {req.gameid} ver {req.gamever} keychip {req.keychipid} playcount "
f"{req.playcnt} billing_type {req.billingtype.name} nearfull {req.nearfull} playlimit {req.playlimit}"
)
if req.traceleft > 0:
self.logger.warn(f"{req.traceleft} unsent tracelogs")
kc_playlimit = req.playlimit
@ -893,7 +960,7 @@ class DLReport:
return True
cfg_dir = environ.get("DIANA_CFG_DIR", "config")
cfg_dir = environ.get("ARTEMIS_CFG_DIR", "config")
cfg: CoreConfig = CoreConfig()
if path.exists(f"{cfg_dir}/core.yaml"):
cfg.update(yaml.safe_load(open(f"{cfg_dir}/core.yaml")))

View File

@ -0,0 +1,27 @@
"""chuni_add_net_battle_uk
Revision ID: 1e150d16ab6b
Revises: b23f985100ba
Create Date: 2024-06-21 22:57:18.418488
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '1e150d16ab6b'
down_revision = 'b23f985100ba'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_unique_constraint(None, 'chuni_profile_net_battle', ['user'])
# ### end Alembic commands ###
def downgrade():
op.drop_constraint(None, 'chuni_profile_net_battle', type_='unique')
# ### end Alembic commands ###

View File

@ -0,0 +1,48 @@
"""add_event_log_info
Revision ID: 2bf9f38d9444
Revises: 81e44dd6047a
Create Date: 2024-05-21 23:00:17.468407
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '2bf9f38d9444'
down_revision = '81e44dd6047a'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('event_log', sa.Column('user', sa.INTEGER(), nullable=True))
op.add_column('event_log', sa.Column('arcade', sa.INTEGER(), nullable=True))
op.add_column('event_log', sa.Column('machine', sa.INTEGER(), nullable=True))
op.add_column('event_log', sa.Column('ip', sa.TEXT(length=39), nullable=True))
op.alter_column('event_log', 'when_logged',
existing_type=mysql.TIMESTAMP(),
server_default=sa.text('now()'),
existing_nullable=False)
op.create_foreign_key(None, 'event_log', 'machine', ['machine'], ['id'], onupdate='cascade', ondelete='cascade')
op.create_foreign_key(None, 'event_log', 'arcade', ['arcade'], ['id'], onupdate='cascade', ondelete='cascade')
op.create_foreign_key(None, 'event_log', 'aime_user', ['user'], ['id'], onupdate='cascade', ondelete='cascade')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'event_log', type_='foreignkey')
op.drop_constraint(None, 'event_log', type_='foreignkey')
op.drop_constraint(None, 'event_log', type_='foreignkey')
op.alter_column('event_log', 'when_logged',
existing_type=mysql.TIMESTAMP(),
server_default=sa.text('current_timestamp()'),
existing_nullable=False)
op.drop_column('event_log', 'ip')
op.drop_column('event_log', 'machine')
op.drop_column('event_log', 'arcade')
op.drop_column('event_log', 'user')
# ### end Alembic commands ###

View File

@ -0,0 +1,46 @@
"""add_event_log_game_version
Revision ID: 2d024cf145a1
Revises: 2bf9f38d9444
Create Date: 2024-05-21 23:41:31.445331
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '2d024cf145a1'
down_revision = '2bf9f38d9444'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('event_log', sa.Column('game', sa.TEXT(length=4), nullable=True))
op.add_column('event_log', sa.Column('version', sa.TEXT(length=24), nullable=True))
op.alter_column('event_log', 'ip',
existing_type=mysql.TINYTEXT(),
type_=sa.TEXT(length=39),
existing_nullable=True)
op.alter_column('event_log', 'when_logged',
existing_type=mysql.TIMESTAMP(),
server_default=sa.text('now()'),
existing_nullable=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('event_log', 'when_logged',
existing_type=mysql.TIMESTAMP(),
server_default=sa.text('current_timestamp()'),
existing_nullable=False)
op.alter_column('event_log', 'ip',
existing_type=sa.TEXT(length=39),
type_=mysql.TINYTEXT(),
existing_nullable=True)
op.drop_column('event_log', 'version')
op.drop_column('event_log', 'game')
# ### end Alembic commands ###

View File

@ -0,0 +1,54 @@
"""pokken_fix_pokemon_uk
Revision ID: 3657efefc5a4
Revises: 4a02e623e5e6
Create Date: 2024-06-13 23:50:57.611998
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '3657efefc5a4'
down_revision = '4a02e623e5e6'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('pokken_pokemon_data', 'char_id',
existing_type=mysql.INTEGER(display_width=11),
nullable=True)
op.alter_column('pokken_pokemon_data', 'illustration_book_no',
existing_type=mysql.INTEGER(display_width=11),
nullable=False)
op.drop_constraint('pokken_pokemon_data_ibfk_1', table_name='pokken_pokemon_data', type_='foreignkey')
op.drop_index('pokken_pokemon_data_uk', table_name='pokken_pokemon_data')
op.create_unique_constraint('pokken_pokemon_uk', 'pokken_pokemon_data', ['user', 'illustration_book_no'])
op.create_foreign_key("pokken_pokemon_data_ibfk_1", "pokken_pokemon_data", "aime_user", ['user'], ['id'])
op.alter_column('pokken_profile', 'trainer_name',
existing_type=mysql.VARCHAR(length=16),
type_=sa.String(length=14),
existing_nullable=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('pokken_profile', 'trainer_name',
existing_type=sa.String(length=14),
type_=mysql.VARCHAR(length=16),
existing_nullable=True)
op.drop_constraint('pokken_pokemon_data_ibfk_1', table_name='pokken_pokemon_data', type_='foreignkey')
op.drop_constraint('pokken_pokemon_uk', 'pokken_pokemon_data', type_='unique')
op.create_index('pokken_pokemon_data_uk', 'pokken_pokemon_data', ['user', 'char_id'], unique=True)
op.create_foreign_key("pokken_pokemon_data_ibfk_1", "pokken_pokemon_data", "aime_user", ['user'], ['id'])
op.alter_column('pokken_pokemon_data', 'illustration_book_no',
existing_type=mysql.INTEGER(display_width=11),
nullable=True)
op.alter_column('pokken_pokemon_data', 'char_id',
existing_type=mysql.INTEGER(display_width=11),
nullable=False)
# ### end Alembic commands ###

View File

@ -0,0 +1,50 @@
"""card_add_idm_chip_id
Revision ID: 48f4acc43a7e
Revises: 1e150d16ab6b
Create Date: 2024-06-21 23:53:34.369134
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '48f4acc43a7e'
down_revision = '1e150d16ab6b'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('aime_card', sa.Column('idm', sa.String(length=16), nullable=True))
op.add_column('aime_card', sa.Column('chip_id', sa.BIGINT(), nullable=True))
op.alter_column('aime_card', 'access_code',
existing_type=mysql.VARCHAR(length=20),
nullable=False)
op.alter_column('aime_card', 'created_date',
existing_type=mysql.TIMESTAMP(),
server_default=sa.text('now()'),
existing_nullable=True)
op.create_unique_constraint(None, 'aime_card', ['chip_id'])
op.create_unique_constraint(None, 'aime_card', ['idm'])
op.create_unique_constraint(None, 'aime_card', ['access_code'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint("chip_id", 'aime_card', type_='unique')
op.drop_constraint("idm", 'aime_card', type_='unique')
op.drop_constraint("access_code", 'aime_card', type_='unique')
op.alter_column('aime_card', 'created_date',
existing_type=mysql.TIMESTAMP(),
server_default=sa.text('CURRENT_TIMESTAMP'),
existing_nullable=True)
op.alter_column('aime_card', 'access_code',
existing_type=mysql.VARCHAR(length=20),
nullable=True)
op.drop_column('aime_card', 'chip_id')
op.drop_column('aime_card', 'idm')
# ### end Alembic commands ###

View File

@ -0,0 +1,48 @@
"""mai2_add_favs_rivals
Revision ID: 4a02e623e5e6
Revises: 8ad40a6e7be2
Create Date: 2024-06-08 19:02:43.856395
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '4a02e623e5e6'
down_revision = '8ad40a6e7be2'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('mai2_item_favorite_music',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user', sa.Integer(), nullable=False),
sa.Column('musicId', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('user', 'musicId', name='mai2_item_favorite_music_uk'),
mysql_charset='utf8mb4'
)
op.create_table('mai2_user_rival',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user', sa.Integer(), nullable=False),
sa.Column('rival', sa.Integer(), nullable=False),
sa.Column('show', sa.Boolean(), server_default='0', nullable=False),
sa.ForeignKeyConstraint(['rival'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'),
sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('user', 'rival', name='mai2_user_rival_uk'),
mysql_charset='utf8mb4'
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('mai2_user_rival')
op.drop_table('mai2_item_favorite_music')
# ### end Alembic commands ###

View File

@ -0,0 +1,41 @@
"""mai2_presents
Revision ID: 5ea363686347
Revises: 680789dabab3
Create Date: 2024-06-28 14:49:07.666879
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '5ea363686347'
down_revision = '680789dabab3'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('mai2_item_present',
sa.Column('id', sa.BIGINT(), nullable=False),
sa.Column('version', sa.INTEGER(), nullable=True),
sa.Column('user', sa.Integer(), nullable=True),
sa.Column('itemKind', sa.INTEGER(), nullable=False),
sa.Column('itemId', sa.INTEGER(), nullable=False),
sa.Column('stock', sa.INTEGER(), server_default='1', nullable=False),
sa.Column('startDate', sa.TIMESTAMP(), nullable=True),
sa.Column('endDate', sa.TIMESTAMP(), nullable=True),
sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('version', 'user', 'itemKind', 'itemId', name='mai2_item_present_uk'),
mysql_charset='utf8mb4'
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('mai2_item_present')
# ### end Alembic commands ###

View File

@ -0,0 +1,295 @@
"""sao_player_changes
Revision ID: 680789dabab3
Revises: a616fd164e40
Create Date: 2024-06-26 23:19:16.863778
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '680789dabab3'
down_revision = 'a616fd164e40'
branch_labels = None
depends_on = None
def upgrade():
op.add_column('sao_equipment_data', sa.Column('is_shop_purchase', sa.BOOLEAN(), server_default='0', nullable=False))
op.add_column('sao_equipment_data', sa.Column('is_protect', sa.BOOLEAN(), server_default='0', nullable=False))
op.add_column('sao_equipment_data', sa.Column('property1_property_id', sa.BIGINT(), server_default='2', nullable=False))
op.add_column('sao_equipment_data', sa.Column('property1_value1', sa.INTEGER(), server_default='0', nullable=False))
op.add_column('sao_equipment_data', sa.Column('property1_value2', sa.INTEGER(), server_default='0', nullable=False))
op.add_column('sao_equipment_data', sa.Column('property2_property_id', sa.BIGINT(), server_default='2', nullable=False))
op.add_column('sao_equipment_data', sa.Column('property2_value1', sa.INTEGER(), server_default='0', nullable=False))
op.add_column('sao_equipment_data', sa.Column('property2_value2', sa.INTEGER(), server_default='0', nullable=False))
op.add_column('sao_equipment_data', sa.Column('property3_property_id', sa.BIGINT(), server_default='2', nullable=False))
op.add_column('sao_equipment_data', sa.Column('property3_value1', sa.INTEGER(), server_default='0', nullable=False))
op.add_column('sao_equipment_data', sa.Column('property3_value2', sa.INTEGER(), server_default='0', nullable=False))
op.add_column('sao_equipment_data', sa.Column('property4_property_id', sa.BIGINT(), server_default='2', nullable=False))
op.add_column('sao_equipment_data', sa.Column('property4_value1', sa.INTEGER(), server_default='0', nullable=False))
op.add_column('sao_equipment_data', sa.Column('property4_value2', sa.INTEGER(), server_default='0', nullable=False))
op.add_column('sao_equipment_data', sa.Column('converted_card_num', sa.INTEGER(), server_default='0', nullable=False))
op.alter_column('sao_equipment_data', 'equipment_id',
existing_type=mysql.INTEGER(),
type_=sa.BIGINT(),
existing_nullable=False)
op.alter_column('sao_equipment_data', 'get_date',
existing_type=mysql.TIMESTAMP(),
server_default=sa.text('now()'),
existing_nullable=False)
op.create_foreign_key(None, 'sao_equipment_data', 'sao_static_property', ['property2_property_id'], ['PropertyId'], onupdate='cascade', ondelete='cascade')
op.create_foreign_key(None, 'sao_equipment_data', 'sao_static_property', ['property4_property_id'], ['PropertyId'], onupdate='cascade', ondelete='cascade')
op.create_foreign_key(None, 'sao_equipment_data', 'sao_static_property', ['property3_property_id'], ['PropertyId'], onupdate='cascade', ondelete='cascade')
op.create_foreign_key(None, 'sao_equipment_data', 'sao_static_property', ['property1_property_id'], ['PropertyId'], onupdate='cascade', ondelete='cascade')
op.create_foreign_key(None, 'sao_equipment_data', 'sao_static_equipment_list', ['equipment_id'], ['EquipmentId'], onupdate='cascade', ondelete='cascade')
op.add_column('sao_hero_log_data', sa.Column('max_level_extend_num', sa.INTEGER(), server_default='0', nullable=False))
op.add_column('sao_hero_log_data', sa.Column('is_awakenable', sa.BOOLEAN(), server_default='0', nullable=False))
op.add_column('sao_hero_log_data', sa.Column('awakening_stage', sa.INTEGER(), server_default='0', nullable=False))
op.add_column('sao_hero_log_data', sa.Column('awakening_exp', sa.INTEGER(), server_default='0', nullable=False))
op.add_column('sao_hero_log_data', sa.Column('is_shop_purchase', sa.BOOLEAN(), server_default='0', nullable=False))
op.add_column('sao_hero_log_data', sa.Column('is_protect', sa.BOOLEAN(), server_default='0', nullable=False))
op.add_column('sao_hero_log_data', sa.Column('property1_property_id', sa.BIGINT(), server_default='2', nullable=False))
op.add_column('sao_hero_log_data', sa.Column('property1_value1', sa.INTEGER(), server_default='0', nullable=False))
op.add_column('sao_hero_log_data', sa.Column('property1_value2', sa.INTEGER(), server_default='0', nullable=False))
op.add_column('sao_hero_log_data', sa.Column('property2_property_id', sa.BIGINT(), server_default='2', nullable=False))
op.add_column('sao_hero_log_data', sa.Column('property2_value1', sa.INTEGER(), server_default='0', nullable=False))
op.add_column('sao_hero_log_data', sa.Column('property2_value2', sa.INTEGER(), server_default='0', nullable=False))
op.add_column('sao_hero_log_data', sa.Column('property3_property_id', sa.BIGINT(), server_default='2', nullable=False))
op.add_column('sao_hero_log_data', sa.Column('property3_value1', sa.INTEGER(), server_default='0', nullable=False))
op.add_column('sao_hero_log_data', sa.Column('property3_value2', sa.INTEGER(), server_default='0', nullable=False))
op.add_column('sao_hero_log_data', sa.Column('property4_property_id', sa.BIGINT(), server_default='2', nullable=False))
op.add_column('sao_hero_log_data', sa.Column('property4_value1', sa.INTEGER(), server_default='0', nullable=False))
op.add_column('sao_hero_log_data', sa.Column('property4_value2', sa.INTEGER(), server_default='0', nullable=False))
op.add_column('sao_hero_log_data', sa.Column('converted_card_num', sa.INTEGER(), server_default='0', nullable=False))
op.alter_column('sao_hero_log_data', 'main_weapon',
existing_type=mysql.INTEGER(),
nullable=True)
op.alter_column('sao_hero_log_data', 'sub_equipment',
existing_type=mysql.INTEGER(),
nullable=True)
op.alter_column('sao_hero_log_data', 'skill_slot1_skill_id',
existing_type=mysql.INTEGER(),
type_=sa.BIGINT(),
nullable=True)
op.alter_column('sao_hero_log_data', 'skill_slot2_skill_id',
existing_type=mysql.INTEGER(),
type_=sa.BIGINT(),
nullable=True)
op.alter_column('sao_hero_log_data', 'skill_slot3_skill_id',
existing_type=mysql.INTEGER(),
type_=sa.BIGINT(),
nullable=True)
op.alter_column('sao_hero_log_data', 'skill_slot4_skill_id',
existing_type=mysql.INTEGER(),
type_=sa.BIGINT(),
nullable=True)
op.alter_column('sao_hero_log_data', 'skill_slot5_skill_id',
existing_type=mysql.INTEGER(),
type_=sa.BIGINT(),
nullable=True)
op.alter_column('sao_hero_log_data', 'get_date',
existing_type=mysql.TIMESTAMP(),
server_default=sa.text('now()'),
existing_nullable=False)
op.alter_column("sao_hero_log_data", "user_hero_log_id",
existing_type=sa.Integer(),
new_column_name="hero_log_id",
type_=sa.BIGINT(),
nullable=False)
op.execute(sa.text("UPDATE sao_hero_log_data SET skill_slot1_skill_id = NULL WHERE skill_slot1_skill_id = 0;"))
op.execute(sa.text("UPDATE sao_hero_log_data SET skill_slot2_skill_id = NULL WHERE skill_slot2_skill_id = 0;"))
op.execute(sa.text("UPDATE sao_hero_log_data SET skill_slot3_skill_id = NULL WHERE skill_slot3_skill_id = 0;"))
op.execute(sa.text("UPDATE sao_hero_log_data SET skill_slot4_skill_id = NULL WHERE skill_slot4_skill_id = 0;"))
op.execute(sa.text("UPDATE sao_hero_log_data SET skill_slot5_skill_id = NULL WHERE skill_slot5_skill_id = 0;"))
op.execute(sa.text("UPDATE sao_hero_log_data SET main_weapon = NULL WHERE main_weapon = 0;"))
op.execute(sa.text("UPDATE sao_hero_log_data SET sub_equipment = NULL WHERE sub_equipment = 0;"))
op.execute(sa.text("UPDATE sao_hero_party SET user_hero_log_id_1 = NULL WHERE user_hero_log_id_1 = 0;"))
op.execute(sa.text("UPDATE sao_hero_party SET user_hero_log_id_2 = NULL WHERE user_hero_log_id_2 = 0;"))
op.execute(sa.text("UPDATE sao_hero_party SET user_hero_log_id_3 = NULL WHERE user_hero_log_id_3 = 0;"))
op.execute(sa.text("UPDATE sao_hero_log_data INNER JOIN sao_equipment_data ON sao_hero_log_data.main_weapon = sao_equipment_data.equipment_id SET sao_hero_log_data.main_weapon = sao_equipment_data.id;"))
op.execute(sa.text("UPDATE sao_hero_log_data INNER JOIN sao_equipment_data ON sao_hero_log_data.sub_equipment = sao_equipment_data.equipment_id SET sao_hero_log_data.sub_equipment = sao_equipment_data.id;"))
op.execute(sa.text("UPDATE sao_hero_party INNER JOIN sao_hero_log_data ON sao_hero_party.user_hero_log_id_1 = sao_hero_log_data.hero_log_id SET sao_hero_party.user_hero_log_id_1 = sao_hero_log_data.id;"))
op.execute(sa.text("UPDATE sao_hero_party INNER JOIN sao_hero_log_data ON sao_hero_party.user_hero_log_id_2 = sao_hero_log_data.hero_log_id SET sao_hero_party.user_hero_log_id_2 = sao_hero_log_data.id;"))
op.execute(sa.text("UPDATE sao_hero_party INNER JOIN sao_hero_log_data ON sao_hero_party.user_hero_log_id_3 = sao_hero_log_data.hero_log_id SET sao_hero_party.user_hero_log_id_3 = sao_hero_log_data.id;"))
op.create_foreign_key(None, 'sao_hero_log_data', 'sao_static_property', ['property4_property_id'], ['PropertyId'], onupdate='cascade', ondelete='cascade')
op.create_foreign_key(None, 'sao_hero_log_data', 'sao_static_skill', ['skill_slot1_skill_id'], ['SkillId'], onupdate='set null', ondelete='set null')
op.create_foreign_key(None, 'sao_hero_log_data', 'sao_static_skill', ['skill_slot5_skill_id'], ['SkillId'], onupdate='set null', ondelete='set null')
op.create_foreign_key(None, 'sao_hero_log_data', 'sao_static_skill', ['skill_slot2_skill_id'], ['SkillId'], onupdate='set null', ondelete='set null')
op.create_foreign_key(None, 'sao_hero_log_data', 'sao_static_skill', ['skill_slot3_skill_id'], ['SkillId'], onupdate='set null', ondelete='set null')
op.create_foreign_key(None, 'sao_hero_log_data', 'sao_equipment_data', ['main_weapon'], ['id'], onupdate='set null', ondelete='set null')
op.create_foreign_key(None, 'sao_hero_log_data', 'sao_static_property', ['property3_property_id'], ['PropertyId'], onupdate='cascade', ondelete='cascade')
op.create_foreign_key(None, 'sao_hero_log_data', 'sao_static_skill', ['skill_slot4_skill_id'], ['SkillId'], onupdate='set null', ondelete='set null')
op.create_foreign_key(None, 'sao_hero_log_data', 'sao_equipment_data', ['sub_equipment'], ['id'], onupdate='set null', ondelete='set null')
op.create_foreign_key(None, 'sao_hero_log_data', 'sao_static_property', ['property1_property_id'], ['PropertyId'], onupdate='cascade', ondelete='cascade')
op.create_foreign_key(None, 'sao_hero_log_data', 'sao_static_hero_list', ['hero_log_id'], ['HeroLogId'], onupdate='cascade', ondelete='cascade')
op.create_foreign_key(None, 'sao_hero_log_data', 'sao_static_property', ['property2_property_id'], ['PropertyId'], onupdate='cascade', ondelete='cascade')
op.create_foreign_key(None, 'sao_hero_party', 'sao_hero_log_data', ['user_hero_log_id_3'], ['id'], onupdate='cascade', ondelete='cascade')
op.create_foreign_key(None, 'sao_hero_party', 'sao_hero_log_data', ['user_hero_log_id_1'], ['id'], onupdate='cascade', ondelete='cascade')
op.create_foreign_key(None, 'sao_hero_party', 'sao_hero_log_data', ['user_hero_log_id_2'], ['id'], onupdate='cascade', ondelete='cascade')
op.alter_column('sao_item_data', 'get_date',
existing_type=mysql.TIMESTAMP(),
server_default=sa.text('now()'),
existing_nullable=False)
op.alter_column('sao_play_sessions', 'play_date',
existing_type=mysql.TIMESTAMP(),
server_default=sa.text('now()'),
existing_nullable=False)
op.add_column('sao_player_quest', sa.Column('quest_type', sa.INTEGER(), server_default='1', nullable=False))
op.alter_column('sao_player_quest', 'play_date',
existing_type=mysql.TIMESTAMP(),
server_default=sa.text('now()'),
existing_nullable=False)
op.alter_column('sao_player_quest', 'episode_id',
existing_type=mysql.INTEGER(),
new_column_name="quest_scene_id",
type_=sa.BIGINT(),
nullable=False)
op.create_foreign_key(None, 'sao_player_quest', 'sao_static_quest', ['quest_scene_id'], ['QuestSceneId'], onupdate='cascade', ondelete='cascade')
op.add_column('sao_profile', sa.Column('my_shop', sa.INTEGER(), nullable=True))
op.add_column('sao_profile', sa.Column('fav_hero', sa.INTEGER(), nullable=True))
op.add_column('sao_profile', sa.Column('when_register', sa.TIMESTAMP(), server_default=sa.text('now()'), nullable=True))
op.add_column('sao_profile', sa.Column('last_login_date', sa.TIMESTAMP(), nullable=True))
op.add_column('sao_profile', sa.Column('last_yui_medal_date', sa.TIMESTAMP(), nullable=True))
op.add_column('sao_profile', sa.Column('last_bonus_yui_medal_date', sa.TIMESTAMP(), nullable=True))
op.add_column('sao_profile', sa.Column('last_comeback_date', sa.TIMESTAMP(), nullable=True))
op.add_column('sao_profile', sa.Column('last_login_bonus_date', sa.TIMESTAMP(), nullable=True))
op.add_column('sao_profile', sa.Column('ad_confirm_date', sa.TIMESTAMP(), nullable=True))
op.add_column('sao_profile', sa.Column('login_ct', sa.INTEGER(), server_default='0', nullable=True))
op.create_foreign_key(None, 'sao_profile', 'sao_hero_log_data', ['fav_hero'], ['id'], onupdate='cascade', ondelete='set null')
def downgrade():
op.drop_constraint("sao_profile_ibfk_2", 'sao_profile', type_='foreignkey')
op.drop_column('sao_profile', 'login_ct')
op.drop_column('sao_profile', 'ad_confirm_date')
op.drop_column('sao_profile', 'last_login_bonus_date')
op.drop_column('sao_profile', 'last_comeback_date')
op.drop_column('sao_profile', 'last_bonus_yui_medal_date')
op.drop_column('sao_profile', 'last_yui_medal_date')
op.drop_column('sao_profile', 'last_login_date')
op.drop_column('sao_profile', 'when_register')
op.drop_column('sao_profile', 'fav_hero')
op.drop_column('sao_profile', 'my_shop')
op.alter_column('sao_player_quest', 'quest_scene_id',
existing_type=mysql.BIGINT(),
new_column_name="episode_id",
type_=sa.INTEGER(),
nullable=False)
op.drop_constraint("sao_player_quest_ibfk_2", 'sao_player_quest', type_='foreignkey')
op.alter_column('sao_player_quest', 'play_date',
existing_type=mysql.TIMESTAMP(),
server_default=sa.text('CURRENT_TIMESTAMP'),
existing_nullable=False)
op.drop_column('sao_player_quest', 'quest_scene_id')
op.drop_column('sao_player_quest', 'quest_type')
op.alter_column('sao_play_sessions', 'play_date',
existing_type=mysql.TIMESTAMP(),
server_default=sa.text('CURRENT_TIMESTAMP'),
existing_nullable=False)
op.alter_column('sao_item_data', 'get_date',
existing_type=mysql.TIMESTAMP(),
server_default=sa.text('CURRENT_TIMESTAMP'),
existing_nullable=False)
op.drop_constraint("sao_hero_party_ibfk_2", 'sao_hero_party', type_='foreignkey')
op.drop_constraint("sao_hero_party_ibfk_3", 'sao_hero_party', type_='foreignkey')
op.drop_constraint("sao_hero_party_ibfk_4", 'sao_hero_party', type_='foreignkey')
op.alter_column("sao_hero_log_data", "hero_log_id",
existing_type=sa.BIGINT(),
new_column_name="user_hero_log_id",
type_=sa.Integer(),
nullable=False)
op.drop_constraint("sao_hero_log_data_ibfk_2", 'sao_hero_log_data', type_='foreignkey')
op.drop_constraint("sao_hero_log_data_ibfk_3", 'sao_hero_log_data', type_='foreignkey')
op.drop_constraint("sao_hero_log_data_ibfk_4", 'sao_hero_log_data', type_='foreignkey')
op.drop_constraint("sao_hero_log_data_ibfk_5", 'sao_hero_log_data', type_='foreignkey')
op.drop_constraint("sao_hero_log_data_ibfk_6", 'sao_hero_log_data', type_='foreignkey')
op.drop_constraint("sao_hero_log_data_ibfk_7", 'sao_hero_log_data', type_='foreignkey')
op.drop_constraint("sao_hero_log_data_ibfk_8", 'sao_hero_log_data', type_='foreignkey')
op.drop_constraint("sao_hero_log_data_ibfk_9", 'sao_hero_log_data', type_='foreignkey')
op.drop_constraint("sao_hero_log_data_ibfk_10", 'sao_hero_log_data', type_='foreignkey')
op.drop_constraint("sao_hero_log_data_ibfk_11", 'sao_hero_log_data', type_='foreignkey')
op.drop_constraint("sao_hero_log_data_ibfk_12", 'sao_hero_log_data', type_='foreignkey')
op.drop_constraint("sao_hero_log_data_ibfk_13", 'sao_hero_log_data', type_='foreignkey')
op.alter_column('sao_hero_log_data', 'get_date',
existing_type=mysql.TIMESTAMP(),
server_default=sa.text('CURRENT_TIMESTAMP'),
existing_nullable=False)
op.alter_column('sao_hero_log_data', 'skill_slot5_skill_id',
existing_type=sa.BIGINT(),
type_=mysql.INTEGER(),
nullable=False)
op.alter_column('sao_hero_log_data', 'skill_slot4_skill_id',
existing_type=sa.BIGINT(),
type_=mysql.INTEGER(),
nullable=False)
op.alter_column('sao_hero_log_data', 'skill_slot3_skill_id',
existing_type=sa.BIGINT(),
type_=mysql.INTEGER(),
nullable=False)
op.alter_column('sao_hero_log_data', 'skill_slot2_skill_id',
existing_type=sa.BIGINT(),
type_=mysql.INTEGER(),
nullable=False)
op.alter_column('sao_hero_log_data', 'skill_slot1_skill_id',
existing_type=sa.BIGINT(),
type_=mysql.INTEGER(),
nullable=False)
op.alter_column('sao_hero_log_data', 'sub_equipment',
existing_type=mysql.INTEGER(),
nullable=False)
op.alter_column('sao_hero_log_data', 'main_weapon',
existing_type=mysql.INTEGER(),
nullable=False)
op.drop_column('sao_hero_log_data', 'converted_card_num')
op.drop_column('sao_hero_log_data', 'property4_value2')
op.drop_column('sao_hero_log_data', 'property4_value1')
op.drop_column('sao_hero_log_data', 'property4_property_id')
op.drop_column('sao_hero_log_data', 'property3_value2')
op.drop_column('sao_hero_log_data', 'property3_value1')
op.drop_column('sao_hero_log_data', 'property3_property_id')
op.drop_column('sao_hero_log_data', 'property2_value2')
op.drop_column('sao_hero_log_data', 'property2_value1')
op.drop_column('sao_hero_log_data', 'property2_property_id')
op.drop_column('sao_hero_log_data', 'property1_value2')
op.drop_column('sao_hero_log_data', 'property1_value1')
op.drop_column('sao_hero_log_data', 'property1_property_id')
op.drop_column('sao_hero_log_data', 'is_protect')
op.drop_column('sao_hero_log_data', 'is_shop_purchase')
op.drop_column('sao_hero_log_data', 'awakening_exp')
op.drop_column('sao_hero_log_data', 'awakening_stage')
op.drop_column('sao_hero_log_data', 'is_awakenable')
op.drop_column('sao_hero_log_data', 'max_level_extend_num')
op.drop_constraint("sao_equipment_data_ibfk_2", 'sao_equipment_data', type_='foreignkey')
op.drop_constraint("sao_equipment_data_ibfk_3", 'sao_equipment_data', type_='foreignkey')
op.drop_constraint("sao_equipment_data_ibfk_4", 'sao_equipment_data', type_='foreignkey')
op.drop_constraint("sao_equipment_data_ibfk_5", 'sao_equipment_data', type_='foreignkey')
op.drop_constraint("sao_equipment_data_ibfk_6", 'sao_equipment_data', type_='foreignkey')
op.alter_column('sao_equipment_data', 'get_date',
existing_type=mysql.TIMESTAMP(),
server_default=sa.text('CURRENT_TIMESTAMP'),
existing_nullable=False)
op.alter_column('sao_equipment_data', 'equipment_id',
existing_type=sa.BIGINT(),
type_=mysql.INTEGER(),
existing_nullable=False)
op.drop_column('sao_equipment_data', 'converted_card_num')
op.drop_column('sao_equipment_data', 'property4_value2')
op.drop_column('sao_equipment_data', 'property4_value1')
op.drop_column('sao_equipment_data', 'property4_property_id')
op.drop_column('sao_equipment_data', 'property3_value2')
op.drop_column('sao_equipment_data', 'property3_value1')
op.drop_column('sao_equipment_data', 'property3_property_id')
op.drop_column('sao_equipment_data', 'property2_value2')
op.drop_column('sao_equipment_data', 'property2_value1')
op.drop_column('sao_equipment_data', 'property2_property_id')
op.drop_column('sao_equipment_data', 'property1_value2')
op.drop_column('sao_equipment_data', 'property1_value1')
op.drop_column('sao_equipment_data', 'property1_property_id')
op.drop_column('sao_equipment_data', 'is_protect')
op.drop_column('sao_equipment_data', 'is_shop_purchase')

View File

@ -0,0 +1,28 @@
"""cxb_add_playlog_grade
Revision ID: 7dc13e364e53
Revises: 2d024cf145a1
Create Date: 2024-05-28 22:31:22.264926
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '7dc13e364e53'
down_revision = '2d024cf145a1'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('cxb_playlog', sa.Column('grade', sa.Integer(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('cxb_playlog', 'grade')
# ### end Alembic commands ###

View File

@ -1,7 +1,7 @@
"""mai2_buddies_support
Revision ID: 81e44dd6047a
Revises: d8950c7ce2fc
Revises: 6a7e8277763b
Create Date: 2024-03-12 19:10:37.063907
"""

View File

@ -0,0 +1,30 @@
"""ongeki: fix clearStatus
Revision ID: 8ad40a6e7be2
Revises: 7dc13e364e53
Create Date: 2024-05-29 19:03:30.062157
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '8ad40a6e7be2'
down_revision = '7dc13e364e53'
branch_labels = None
depends_on = None
def upgrade():
op.alter_column('ongeki_score_best', 'clearStatus',
existing_type=mysql.TINYINT(display_width=1),
type_=sa.Integer(),
existing_nullable=False)
def downgrade():
op.alter_column('ongeki_score_best', 'clearStatus',
existing_type=sa.Integer(),
type_=mysql.TINYINT(display_width=1),
existing_nullable=False)

View File

@ -0,0 +1,437 @@
"""sao_backport
Revision ID: a616fd164e40
Revises: 48f4acc43a7e
Create Date: 2024-06-24 20:28:34.471282
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = 'a616fd164e40'
down_revision = '48f4acc43a7e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('sao_static_quest')
op.create_table('sao_static_quest',
sa.Column('QuestSceneId', sa.BIGINT(), nullable=False),
sa.Column('SortNo', sa.INTEGER(), nullable=False),
sa.Column('Tutorial', sa.BOOLEAN(), nullable=False),
sa.Column('ColRate', sa.DECIMAL(), nullable=False),
sa.Column('LimitDefault', sa.INTEGER(), nullable=False),
sa.Column('LimitResurrection', sa.INTEGER(), nullable=False),
sa.Column('RewardTableSubId', sa.INTEGER(), nullable=False),
sa.Column('PlayerTraceTableSubId', sa.INTEGER(), nullable=False),
sa.Column('SuccessPlayerExp', sa.INTEGER(), nullable=False),
sa.Column('FailedPlayerExp', sa.INTEGER(), nullable=False),
sa.Column('PairExpRate', sa.INTEGER(), nullable=False),
sa.Column('TrioExpRate', sa.INTEGER(), nullable=False),
sa.Column('SingleRewardVp', sa.INTEGER(), nullable=False),
sa.Column('PairRewardVp', sa.INTEGER(), nullable=False),
sa.Column('TrioRewardVp', sa.INTEGER(), nullable=False),
sa.PrimaryKeyConstraint('QuestSceneId'),
mysql_charset='utf8mb4'
)
op.create_table('sao_static_property',
sa.Column('PropertyId', sa.BIGINT(), nullable=False),
sa.Column('PropertyTargetType', sa.INTEGER(), nullable=False),
sa.Column('PropertyName', sa.VARCHAR(length=255), nullable=False),
sa.Column('PropertyName_en', sa.VARCHAR(length=255), nullable=True),
sa.Column('PropertyNameFormat', sa.VARCHAR(length=255), nullable=False),
sa.Column('PropertyNameFormat_en', sa.VARCHAR(length=255), nullable=True),
sa.Column('PropertyTypeId', sa.INTEGER(), nullable=False),
sa.Column('Value1Min', sa.INTEGER(), nullable=False),
sa.Column('Value1Max', sa.INTEGER(), nullable=False),
sa.Column('Value2Min', sa.INTEGER(), nullable=False),
sa.Column('Value2Max', sa.INTEGER(), nullable=False),
sa.PrimaryKeyConstraint('PropertyId'),
mysql_charset='utf8mb4'
)
op.create_table('sao_static_reward',
sa.Column('RewardTableId', sa.BIGINT(), nullable=False),
sa.Column('RewardTableSubId', sa.INTEGER(), nullable=False),
sa.Column('UnanalyzedLogGradeId', sa.INTEGER(), nullable=False),
sa.Column('CommonRewardType', sa.INTEGER(), nullable=False),
sa.Column('CommonRewardId', sa.INTEGER(), nullable=False),
sa.Column('CommonRewardNum', sa.INTEGER(), nullable=False),
sa.Column('StrengthMin', sa.INTEGER(), nullable=False),
sa.Column('StrengthMax', sa.INTEGER(), nullable=False),
sa.Column('PropertyTableSubId', sa.INTEGER(), nullable=False),
sa.Column('QuestInfoDisplayFlag', sa.BOOLEAN(), nullable=False),
sa.Column('Rate', sa.INTEGER(), nullable=False),
sa.PrimaryKeyConstraint('RewardTableId'),
mysql_charset='utf8mb4'
)
op.create_table('sao_static_skill',
sa.Column('SkillId', sa.BIGINT(), nullable=False),
sa.Column('WeaponTypeId', sa.INTEGER(), nullable=False),
sa.Column('Name', sa.VARCHAR(length=255), nullable=False),
sa.Column('Name_en', sa.VARCHAR(length=255), nullable=True),
sa.Column('Attack', sa.BOOLEAN(), nullable=False),
sa.Column('Passive', sa.BOOLEAN(), nullable=False),
sa.Column('Pet', sa.BOOLEAN(), nullable=False),
sa.Column('Level', sa.INTEGER(), nullable=False),
sa.Column('SkillCondition', sa.INTEGER(), nullable=False),
sa.Column('CoolTime', sa.INTEGER(), nullable=False),
sa.Column('SkillIcon', sa.VARCHAR(length=255), nullable=False),
sa.Column('FriendSkillIcon', sa.VARCHAR(length=255), nullable=False),
sa.Column('InfoText', sa.VARCHAR(length=255), nullable=False),
sa.Column('InfoText_en', sa.VARCHAR(length=255), nullable=True),
sa.PrimaryKeyConstraint('SkillId'),
mysql_charset='utf8mb4'
)
op.create_table('sao_static_trace_table',
sa.Column('PlayerTraceTableId', sa.BIGINT(), nullable=False),
sa.Column('PlayerTraceTableSubId', sa.INTEGER(), nullable=False),
sa.Column('CommonRewardType', sa.INTEGER(), nullable=False),
sa.Column('CommonRewardId', sa.INTEGER(), nullable=False),
sa.Column('CommonRewardNum', sa.INTEGER(), nullable=False),
sa.Column('Rate', sa.INTEGER(), nullable=False),
sa.PrimaryKeyConstraint('PlayerTraceTableId'),
mysql_charset='utf8mb4'
)
op.create_table('sao_player_beginner_mission',
sa.Column('id', sa.BIGINT(), nullable=False),
sa.Column('user', sa.INTEGER(), nullable=False),
sa.Column('beginner_mission_id', sa.INTEGER(), nullable=False),
sa.Column('condition_id', sa.INTEGER(), nullable=False),
sa.Column('is_seat', sa.BOOLEAN(), server_default='0', nullable=False),
sa.Column('achievement_num', sa.INTEGER(), nullable=False),
sa.Column('complete_flag', sa.BOOLEAN(), server_default='0', nullable=False),
sa.Column('complete_date', sa.TIMESTAMP(), nullable=True),
sa.Column('reward_received_flag', sa.BOOLEAN(), server_default='0', nullable=False),
sa.Column('reward_received_date', sa.TIMESTAMP(), nullable=True),
sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('user'),
sa.UniqueConstraint('user', 'condition_id', name='sao_player_beginner_mission_uk'),
mysql_charset='utf8mb4'
)
op.create_table('sao_player_resource_card',
sa.Column('id', sa.BIGINT(), nullable=False),
sa.Column('user', sa.INTEGER(), nullable=False),
sa.Column('common_reward_type', sa.INTEGER(), nullable=False),
sa.Column('common_reward_id', sa.INTEGER(), nullable=False),
sa.Column('holographic_flag', sa.BOOLEAN(), server_default='0', nullable=False),
sa.Column('serial', sa.VARCHAR(length=20), nullable=True),
sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('serial'),
mysql_charset='utf8mb4'
)
op.create_table('sao_player_tutorial',
sa.Column('id', sa.BIGINT(), nullable=False),
sa.Column('user', sa.INTEGER(), nullable=False),
sa.Column('tutorial_byte', sa.INTEGER(), nullable=False),
sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('user', 'tutorial_byte', name='sao_player_tutorial_uk'),
mysql_charset='utf8mb4'
)
op.create_table('sao_static_episode',
sa.Column('EpisodeId', sa.BIGINT(), nullable=False),
sa.Column('EpisodeChapterId', sa.INTEGER(), nullable=False),
sa.Column('ReleaseEpisodeId', sa.INTEGER(), nullable=False),
sa.Column('Title', sa.VARCHAR(length=255), nullable=False),
sa.Column('CommentSummary', sa.VARCHAR(length=255), nullable=False),
sa.Column('ExBonusTableSubId', sa.INTEGER(), nullable=False),
sa.Column('QuestSceneId', sa.BIGINT(), nullable=True),
sa.ForeignKeyConstraint(['QuestSceneId'], ['sao_static_quest.QuestSceneId'], onupdate='cascade', ondelete='cascade'),
sa.PrimaryKeyConstraint('EpisodeId'),
mysql_charset='utf8mb4'
)
op.create_table('sao_static_ex_bonus',
sa.Column('ExBonusTableId', sa.BIGINT(), nullable=False),
sa.Column('ExBonusTableSubId', sa.INTEGER(), nullable=False),
sa.Column('ExBonusConditionId', sa.INTEGER(), nullable=False),
sa.Column('ConditionValue1', sa.INTEGER(), nullable=False),
sa.Column('ConditionValue2', sa.INTEGER(), nullable=False),
sa.Column('CommonRewardType', sa.INTEGER(), nullable=False),
sa.Column('CommonRewardId', sa.INTEGER(), nullable=False),
sa.Column('CommonRewardNum', sa.INTEGER(), nullable=False),
sa.Column('Strength', sa.INTEGER(), nullable=False),
sa.Column('Property1PropertyId', sa.BIGINT(), nullable=False),
sa.Column('Property1Value1', sa.INTEGER(), nullable=False),
sa.Column('Property1Value2', sa.INTEGER(), nullable=False),
sa.Column('Property2PropertyId', sa.BIGINT(), nullable=False),
sa.Column('Property2Value1', sa.INTEGER(), nullable=False),
sa.Column('Property2Value2', sa.INTEGER(), nullable=False),
sa.Column('Property3PropertyId', sa.BIGINT(), nullable=False),
sa.Column('Property3Value1', sa.INTEGER(), nullable=False),
sa.Column('Property3Value2', sa.INTEGER(), nullable=False),
sa.Column('Property4PropertyId', sa.BIGINT(), nullable=False),
sa.Column('Property4Value1', sa.INTEGER(), nullable=False),
sa.Column('Property4Value2', sa.INTEGER(), nullable=False),
sa.ForeignKeyConstraint(['Property1PropertyId'], ['sao_static_property.PropertyId'], onupdate='cascade', ondelete='cascade'),
sa.ForeignKeyConstraint(['Property2PropertyId'], ['sao_static_property.PropertyId'], onupdate='cascade', ondelete='cascade'),
sa.ForeignKeyConstraint(['Property3PropertyId'], ['sao_static_property.PropertyId'], onupdate='cascade', ondelete='cascade'),
sa.ForeignKeyConstraint(['Property4PropertyId'], ['sao_static_property.PropertyId'], onupdate='cascade', ondelete='cascade'),
sa.PrimaryKeyConstraint('ExBonusTableId'),
mysql_charset='utf8mb4'
)
op.create_table('sao_static_ex_tower',
sa.Column('ExTowerQuestId', sa.BIGINT(), nullable=False),
sa.Column('ExTowerId', sa.INTEGER(), nullable=False),
sa.Column('ReleaseExTowerQuestId', sa.INTEGER(), nullable=False),
sa.Column('Title', sa.VARCHAR(length=255), nullable=False),
sa.Column('Title_en', sa.VARCHAR(length=255), nullable=True),
sa.Column('ExBonusTableSubId', sa.INTEGER(), nullable=False),
sa.Column('QuestSceneId', sa.BIGINT(), nullable=False),
sa.ForeignKeyConstraint(['QuestSceneId'], ['sao_static_quest.QuestSceneId'], onupdate='cascade', ondelete='cascade'),
sa.PrimaryKeyConstraint('ExTowerQuestId'),
mysql_charset='utf8mb4'
)
op.create_table('sao_static_side_quest',
sa.Column('SideQuestId', sa.BIGINT(), nullable=False),
sa.Column('DisplayName', sa.VARCHAR(length=255), nullable=False),
sa.Column('DisplayName_en', sa.VARCHAR(length=255), nullable=True),
sa.Column('EpisodeNum', sa.INTEGER(), nullable=False),
sa.Column('ExBonusTableSubId', sa.INTEGER(), nullable=False),
sa.Column('QuestSceneId', sa.BIGINT(), nullable=False),
sa.ForeignKeyConstraint(['QuestSceneId'], ['sao_static_quest.QuestSceneId'], onupdate='cascade', ondelete='cascade'),
sa.PrimaryKeyConstraint('SideQuestId'),
sa.UniqueConstraint('SideQuestId'),
mysql_charset='utf8mb4'
)
op.create_table('sao_static_skill_table',
sa.Column('SkillTableId', sa.BIGINT(), nullable=False),
sa.Column('SkillId', sa.BIGINT(), nullable=False),
sa.Column('SkillTableSubId', sa.INTEGER(), nullable=False),
sa.Column('LevelObtained', sa.INTEGER(), nullable=False),
sa.Column('AwakeningId', sa.INTEGER(), nullable=False),
sa.ForeignKeyConstraint(['SkillId'], ['sao_static_skill.SkillId'], onupdate='cascade', ondelete='cascade'),
sa.PrimaryKeyConstraint('SkillTableId'),
mysql_charset='utf8mb4'
)
op.create_table('sao_static_tower',
sa.Column('TowerId', sa.BIGINT(), nullable=False),
sa.Column('ReleaseTowerId', sa.INTEGER(), nullable=False),
sa.Column('ExBonusTableSubId', sa.INTEGER(), nullable=False),
sa.Column('QuestSceneId', sa.BIGINT(), nullable=False),
sa.ForeignKeyConstraint(['QuestSceneId'], ['sao_static_quest.QuestSceneId'], onupdate='cascade', ondelete='cascade'),
sa.PrimaryKeyConstraint('TowerId'),
mysql_charset='utf8mb4'
)
op.create_table('sao_player_ex_bonus',
sa.Column('id', sa.BIGINT(), nullable=False),
sa.Column('user', sa.INTEGER(), nullable=False),
sa.Column('quest_scene_id', sa.BIGINT(), nullable=False),
sa.Column('ex_bonus_table_id', sa.BIGINT(), nullable=False),
sa.Column('quest_clear_flag', sa.BOOLEAN(), server_default='0', nullable=False),
sa.ForeignKeyConstraint(['ex_bonus_table_id'], ['sao_static_ex_bonus.ExBonusTableId'], onupdate='cascade', ondelete='cascade'),
sa.ForeignKeyConstraint(['quest_scene_id'], ['sao_static_quest.QuestSceneId'], onupdate='cascade', ondelete='cascade'),
sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('user', 'quest_scene_id', 'ex_bonus_table_id', name='sao_player_ex_bonus_uk'),
mysql_charset='utf8mb4'
)
op.create_table('sao_player_hero_card',
sa.Column('id', sa.BIGINT(), nullable=False),
sa.Column('user', sa.INTEGER(), nullable=False),
sa.Column('user_hero_id', sa.INTEGER(), nullable=False),
sa.Column('holographic_flag', sa.BOOLEAN(), server_default='0', nullable=False),
sa.Column('serial', sa.VARCHAR(length=20), nullable=True),
sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'),
sa.ForeignKeyConstraint(['user_hero_id'], ['sao_hero_log_data.id'], onupdate='cascade', ondelete='cascade'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('serial'),
mysql_charset='utf8mb4'
)
op.alter_column('sao_end_sessions', 'play_date',
existing_type=mysql.TIMESTAMP(),
server_default=sa.text('now()'),
existing_nullable=False)
op.drop_table('sao_static_equipment_list')
op.create_table("sao_static_equipment_list",
sa.Column("EquipmentId", sa.BIGINT, primary_key=True, nullable=False),
sa.Column("EquipmentType", sa.INTEGER, nullable=False),
sa.Column("WeaponTypeId", sa.INTEGER, nullable=False),
sa.Column("Name", sa.VARCHAR(255), nullable=False),
sa.Column("Name_en", sa.VARCHAR(255)),
sa.Column("Rarity", sa.INTEGER, nullable=False),
sa.Column("Power", sa.INTEGER, nullable=False),
sa.Column("StrengthIncrement", sa.INTEGER, nullable=False),
sa.Column("SkillCondition", sa.INTEGER, nullable=False),
sa.Column("Property1PropertyId", sa.BIGINT, sa.ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False),
sa.Column("Property1Value1", sa.INTEGER, nullable=False),
sa.Column("Property1Value2", sa.INTEGER, nullable=False),
sa.Column("Property2PropertyId", sa.BIGINT, sa.ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False),
sa.Column("Property2Value1", sa.INTEGER, nullable=False),
sa.Column("Property2Value2", sa.INTEGER, nullable=False),
sa.Column("Property3PropertyId", sa.BIGINT, sa.ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False),
sa.Column("Property3Value1", sa.INTEGER, nullable=False),
sa.Column("Property3Value2", sa.INTEGER, nullable=False),
sa.Column("Property4PropertyId", sa.BIGINT, sa.ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False),
sa.Column("Property4Value1", sa.INTEGER, nullable=False),
sa.Column("Property4Value2", sa.INTEGER, nullable=False),
sa.Column("SalePrice", sa.INTEGER, nullable=False),
sa.Column("CompositionExp", sa.INTEGER, nullable=False),
sa.Column("AwakeningExp", sa.INTEGER, nullable=False),
sa.Column("FlavorText", sa.VARCHAR(255), nullable=False),
sa.Column("FlavorText_en", sa.VARCHAR(255)),
mysql_charset="utf8mb4"
)
op.drop_table('sao_static_hero_list')
op.create_table("sao_static_hero_list",
sa.Column("HeroLogId", sa.BIGINT, primary_key=True, nullable=False),
sa.Column("CharaId", sa.INTEGER, nullable=False),
sa.Column("Name", sa.VARCHAR(255), nullable=False),
sa.Column("Nickname", sa.VARCHAR(255), nullable=False),
sa.Column("Name_en", sa.VARCHAR(255)),
sa.Column("Nickname_en", sa.VARCHAR(255)),
sa.Column("Rarity", sa.INTEGER, nullable=False),
sa.Column("WeaponTypeId", sa.INTEGER, nullable=False),
sa.Column("HeroLogRoleId", sa.INTEGER, nullable=False),
sa.Column("CostumeTypeId", sa.INTEGER, nullable=False),
sa.Column("UnitId", sa.INTEGER, nullable=False),
sa.Column("DefaultEquipmentId1", sa.BIGINT, sa.ForeignKey("sao_static_equipment_list.EquipmentId", ondelete="cascade", onupdate="cascade")),
sa.Column("DefaultEquipmentId2", sa.BIGINT, sa.ForeignKey("sao_static_equipment_list.EquipmentId", ondelete="cascade", onupdate="cascade")),
sa.Column("SkillTableSubId", sa.INTEGER, nullable=False),
sa.Column("HpMin", sa.INTEGER, nullable=False),
sa.Column("HpMax", sa.INTEGER, nullable=False),
sa.Column("StrMin", sa.INTEGER, nullable=False),
sa.Column("StrMax", sa.INTEGER, nullable=False),
sa.Column("VitMin", sa.INTEGER, nullable=False),
sa.Column("VitMax", sa.INTEGER, nullable=False),
sa.Column("IntMin", sa.INTEGER, nullable=False),
sa.Column("IntMax", sa.INTEGER, nullable=False),
sa.Column("Property1PropertyId", sa.BIGINT, sa.ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False),
sa.Column("Property1Value1", sa.INTEGER, nullable=False),
sa.Column("Property1Value2", sa.INTEGER, nullable=False),
sa.Column("Property2PropertyId", sa.BIGINT, sa.ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False),
sa.Column("Property2Value1", sa.INTEGER, nullable=False),
sa.Column("Property2Value2", sa.INTEGER, nullable=False),
sa.Column("Property3PropertyId", sa.BIGINT, sa.ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False),
sa.Column("Property3Value1", sa.INTEGER, nullable=False),
sa.Column("Property3Value2", sa.INTEGER, nullable=False),
sa.Column("Property4PropertyId", sa.BIGINT, sa.ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False),
sa.Column("Property4Value1", sa.INTEGER, nullable=False),
sa.Column("Property4Value2", sa.INTEGER, nullable=False),
sa.Column("FlavorText", sa.VARCHAR(255), nullable=False),
sa.Column("FlavorText_en", sa.VARCHAR(255)),
sa.Column("SalePrice", sa.INTEGER, nullable=False),
sa.Column("CompositionExp", sa.INTEGER, nullable=False),
sa.Column("AwakeningExp", sa.INTEGER, nullable=False),
sa.Column("Slot4UnlockLevel", sa.INTEGER, nullable=False),
sa.Column("Slot5UnlockLevel", sa.INTEGER, nullable=False),
sa.Column("CollectionEmptyFrameDisplayFlag", sa.BOOLEAN, nullable=False),
mysql_charset="utf8mb4"
)
op.drop_table('sao_static_item_list')
op.create_table("sao_static_item_list",
sa.Column("ItemId", sa.INTEGER, nullable=False, primary_key=True),
sa.Column("ItemTypeId", sa.INTEGER, nullable=False),
sa.Column("Name", sa.VARCHAR(255), nullable=False),
sa.Column("Name_en", sa.VARCHAR(255)),
sa.Column("Rarity", sa.INTEGER, nullable=False),
sa.Column("Value", sa.INTEGER, nullable=False),
sa.Column("PropertyId", sa.BIGINT, sa.ForeignKey("sao_static_property.PropertyId", ondelete="cascade", onupdate="cascade"), nullable=False),
sa.Column("PropertyValue1Min", sa.INTEGER, nullable=False),
sa.Column("PropertyValue1Max", sa.INTEGER, nullable=False),
sa.Column("PropertyValue2Min", sa.INTEGER, nullable=False),
sa.Column("PropertyValue2Max", sa.INTEGER, nullable=False),
sa.Column("FlavorText", sa.VARCHAR(255), nullable=False),
sa.Column("FlavorText_en", sa.VARCHAR(255)),
sa.Column("SalePrice", sa.INTEGER, nullable=False),
sa.Column("ItemIcon", sa.VARCHAR(255), nullable=False),
mysql_charset="utf8mb4"
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('sao_static_item_list')
op.create_table("sao_static_item_list",
sa.Column("id", sa.Integer, primary_key=True, nullable=False),
sa.Column("version", sa.Integer),
sa.Column("itemId", sa.Integer),
sa.Column("itemTypeId", sa.Integer),
sa.Column("name", sa.String(255)),
sa.Column("rarity", sa.Integer),
sa.Column("flavorText", sa.String(255)),
sa.Column("enabled", sa.Boolean),
sa.UniqueConstraint(
"version", "itemId", name="sao_static_item_list_uk"
),
mysql_charset="utf8mb4"
)
op.drop_table('sao_static_hero_list')
op.create_table("sao_static_hero_list",
sa.Column("id", sa.Integer, primary_key=True, nullable=False),
sa.Column("version", sa.Integer),
sa.Column("heroLogId", sa.Integer),
sa.Column("name", sa.String(255)),
sa.Column("nickname", sa.String(255)),
sa.Column("rarity", sa.Integer),
sa.Column("skillTableSubId", sa.Integer),
sa.Column("awakeningExp", sa.Integer),
sa.Column("flavorText", sa.String(255)),
sa.Column("enabled", sa.Boolean),
sa.UniqueConstraint(
"version", "heroLogId", name="sao_static_hero_list_uk"
),
mysql_charset="utf8mb4",
)
op.drop_table('sao_static_equipment_list')
op.create_table("sao_static_equipment_list",
sa.Column("id", sa.Integer, primary_key=True, nullable=False),
sa.Column("version", sa.Integer),
sa.Column("equipmentId", sa.Integer),
sa.Column("equipmentType", sa.Integer),
sa.Column("weaponTypeId", sa.Integer),
sa.Column("name", sa.String(255)),
sa.Column("rarity", sa.Integer),
sa.Column("flavorText", sa.String(255)),
sa.Column("enabled", sa.Boolean),
sa.UniqueConstraint(
"version", "equipmentId", name="sao_static_equipment_list_uk"
),
mysql_charset="utf8mb4"
)
op.alter_column('sao_end_sessions', 'play_date',
existing_type=mysql.TIMESTAMP(),
server_default=sa.text('CURRENT_TIMESTAMP'),
existing_nullable=False)
op.drop_table('sao_player_hero_card')
op.drop_table('sao_player_ex_bonus')
op.drop_table('sao_static_tower')
op.drop_table('sao_static_skill_table')
op.drop_table('sao_static_side_quest')
op.drop_table('sao_static_ex_tower')
op.drop_table('sao_static_ex_bonus')
op.drop_table('sao_static_episode')
op.drop_table('sao_player_tutorial')
op.drop_table('sao_player_resource_card')
op.drop_table('sao_player_beginner_mission')
op.drop_table('sao_static_trace_table')
op.drop_table('sao_static_skill')
op.drop_table('sao_static_reward')
op.drop_table('sao_static_property')
op.drop_table('sao_static_quest')
op.create_table('sao_static_quest',
sa.Column('id', mysql.INTEGER(), autoincrement=True, nullable=False),
sa.Column('enabled', mysql.TINYINT(display_width=1), autoincrement=False, nullable=True),
sa.Column('version', mysql.INTEGER(), autoincrement=False, nullable=True),
sa.Column('questSceneId', mysql.INTEGER(), autoincrement=False, nullable=True),
sa.Column('sortNo', mysql.INTEGER(), autoincrement=False, nullable=True),
sa.Column('name', mysql.VARCHAR(charset='utf8mb4', collation='utf8mb4_general_ci', length=255), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint("version", "questSceneId", name="sao_static_quest_uk"),
mysql_charset='utf8mb4'
)
# ### end Alembic commands ###

View File

@ -0,0 +1,87 @@
"""CHUNITHM LUMINOUS
Revision ID: b23f985100ba
Revises: 3657efefc5a4
Create Date: 2024-06-20 08:08:08.759261
"""
from alembic import op
from sqlalchemy import Column, Integer, Boolean, UniqueConstraint
# revision identifiers, used by Alembic.
revision = 'b23f985100ba'
down_revision = '3657efefc5a4'
branch_labels = None
depends_on = None
def upgrade():
op.create_table(
"chuni_profile_net_battle",
Column("id", Integer, primary_key=True, nullable=False),
Column("user", Integer, nullable=False),
Column("isRankUpChallengeFailed", Boolean),
Column("highestBattleRankId", Integer),
Column("battleIconId", Integer),
Column("battleIconNum", Integer),
Column("avatarEffectPoint", Integer),
mysql_charset="utf8mb4",
)
op.create_foreign_key(
None,
"chuni_profile_net_battle",
"aime_user",
["user"],
["id"],
ondelete="cascade",
onupdate="cascade",
)
op.create_table(
"chuni_item_cmission",
Column("id", Integer, primary_key=True, nullable=False),
Column("user", Integer, nullable=False),
Column("missionId", Integer, nullable=False),
Column("point", Integer),
UniqueConstraint("user", "missionId", name="chuni_item_cmission_uk"),
mysql_charset="utf8mb4",
)
op.create_foreign_key(
None,
"chuni_item_cmission",
"aime_user",
["user"],
["id"],
ondelete="cascade",
onupdate="cascade",
)
op.create_table(
"chuni_item_cmission_progress",
Column("id", Integer, primary_key=True, nullable=False),
Column("user", Integer, nullable=False),
Column("missionId", Integer, nullable=False),
Column("order", Integer),
Column("stage", Integer),
Column("progress", Integer),
UniqueConstraint(
"user", "missionId", "order", name="chuni_item_cmission_progress_uk"
),
mysql_charset="utf8mb4",
)
op.create_foreign_key(
None,
"chuni_item_cmission_progress",
"aime_user",
["user"],
["id"],
ondelete="cascade",
onupdate="cascade",
)
def downgrade():
op.drop_table("chuni_profile_net_battle")
op.drop_table("chuni_item_cmission")
op.drop_table("chuni_item_cmission_progress")

View File

@ -8,7 +8,8 @@ from sqlalchemy.engine.base import Connection
from sqlalchemy.sql import text, func, select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy import MetaData, Table, Column
from sqlalchemy.types import Integer, String, TIMESTAMP, JSON
from sqlalchemy.types import Integer, String, TIMESTAMP, JSON, INTEGER, TEXT
from sqlalchemy.schema import ForeignKey
from sqlalchemy.dialects.mysql import insert
from core.config import CoreConfig
@ -22,6 +23,12 @@ event_log = Table(
Column("system", String(255), nullable=False),
Column("type", String(255), nullable=False),
Column("severity", Integer, nullable=False),
Column("user", INTEGER, ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
Column("arcade", INTEGER, ForeignKey("arcade.id", ondelete="cascade", onupdate="cascade")),
Column("machine", INTEGER, ForeignKey("machine.id", ondelete="cascade", onupdate="cascade")),
Column("ip", TEXT(39)),
Column("game", TEXT(4)),
Column("version", TEXT(24)),
Column("message", String(1000), nullable=False),
Column("details", JSON, nullable=False),
Column("when_logged", TIMESTAMP, nullable=False, server_default=func.now()),
@ -75,12 +82,19 @@ class BaseData:
return randrange(10000, 9999999)
async def log_event(
self, system: str, type: str, severity: int, message: str, details: Dict = {}
self, system: str, type: str, severity: int, message: str, details: Dict = {}, user: int = None,
arcade: int = None, machine: int = None, ip: str = None, game: str = None, version: str = None
) -> Optional[int]:
sql = event_log.insert().values(
system=system,
type=type,
severity=severity,
user=user,
arcade=arcade,
machine=machine,
ip=ip,
game=game,
version=version,
message=message,
details=json.dumps(details),
)
@ -94,8 +108,8 @@ class BaseData:
return result.lastrowid
async def get_event_log(self, entries: int = 100) -> Optional[List[Dict]]:
sql = event_log.select().limit(entries).all()
async def get_event_log(self, entries: int = 100) -> Optional[List[Row]]:
sql = event_log.select().order_by(event_log.c.id.desc()).limit(entries)
result = await self.execute(sql)
if result is None:

View File

@ -1,6 +1,6 @@
from typing import Dict, List, Optional
from sqlalchemy import Table, Column, UniqueConstraint
from sqlalchemy.types import Integer, String, Boolean, TIMESTAMP
from sqlalchemy.types import Integer, String, Boolean, TIMESTAMP, BIGINT
from sqlalchemy.sql.schema import ForeignKey
from sqlalchemy.sql import func
from sqlalchemy.engine import Row
@ -11,12 +11,10 @@ aime_card = Table(
"aime_card",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("access_code", String(20)),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column("access_code", String(20), nullable=False, unique=True),
Column("idm", String(16), unique=True),
Column("chip_id", BIGINT, unique=True),
Column("created_date", TIMESTAMP, server_default=func.now()),
Column("last_login_date", TIMESTAMP, onupdate=func.now()),
Column("is_locked", Boolean, server_default="0"),
@ -27,6 +25,9 @@ aime_card = Table(
class CardData(BaseData):
moble_os_codes = set([0x06, 0x07, 0x10, 0x12, 0x13, 0x14, 0x15, 0x17, 0x18])
card_os_codes = set([0x20, 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7])
async def get_card_by_access_code(self, access_code: str) -> Optional[Row]:
sql = aime_card.select(aime_card.c.access_code == access_code)
@ -122,6 +123,31 @@ class CardData(BaseData):
if result is None:
self.logger.warn(f"Failed to update last login time for {access_code}")
async def get_card_by_idm(self, idm: str) -> Optional[Row]:
result = await self.execute(aime_card.select(aime_card.c.idm == idm))
if result:
return result.fetchone()
async def get_card_by_chip_id(self, chip_id: int) -> Optional[Row]:
result = await self.execute(aime_card.select(aime_card.c.chip_id == chip_id))
if result:
return result.fetchone()
async def set_chip_id_by_access_code(self, access_code: str, chip_id: int) -> Optional[Row]:
result = await self.execute(aime_card.update(aime_card.c.access_code == access_code).values(chip_id=chip_id))
if not result:
self.logger.error(f"Failed to update chip ID to {chip_id} for {access_code}")
async def set_idm_by_access_code(self, access_code: str, idm: str) -> Optional[Row]:
result = await self.execute(aime_card.update(aime_card.c.access_code == access_code).values(idm=idm))
if not result:
self.logger.error(f"Failed to update IDm to {idm} for {access_code}")
async def set_access_code_by_access_code(self, old_ac: str, new_ac: str) -> None:
result = await self.execute(aime_card.update(aime_card.c.access_code == old_ac).values(access_code=new_ac))
if not result:
self.logger.error(f"Failed to change card access code from {old_ac} to {new_ac}")
def to_access_code(self, luid: str) -> str:
"""
Given a felica cards internal 16 hex character luid, convert it to a 0-padded 20 digit access code as a string
@ -132,4 +158,4 @@ class CardData(BaseData):
"""
Given a 20 digit access code as a string, return the 16 hex character luid
"""
return f"{int(access_code):0{16}x}"
return f"{int(access_code):0{16}X}"

View File

@ -120,3 +120,7 @@ class UserData(BaseData):
result = await self.execute(sql)
return result is not None
async def get_user_by_username(self, username: str) -> Optional[Row]:
result = await self.execute(aime_user.select(aime_user.c.username == username))
if result: return result.fetchone()

View File

@ -44,12 +44,13 @@ class ShopOwner():
self.permissions = perms
class UserSession():
def __init__(self, usr_id: int = 0, ip: str = "", perms: int = 0, ongeki_ver: int = 7, chunithm_ver: int = -1):
def __init__(self, usr_id: int = 0, ip: str = "", perms: int = 0, ongeki_ver: int = 7, chunithm_ver: int = -1, maimai_version: int = -1):
self.user_id = usr_id
self.current_ip = ip
self.permissions = perms
self.ongeki_version = ongeki_ver
self.chunithm_version = chunithm_ver
self.maimai_version = maimai_version
class FrontendServlet():
def __init__(self, cfg: CoreConfig, config_dir: str) -> None:
@ -133,6 +134,7 @@ class FrontendServlet():
]),
Mount("/sys", routes=[
Route("/", self.system.render_GET, methods=['GET']),
Route("/logs", self.system.render_logs, methods=['GET']),
Route("/lookup.user", self.system.lookup_user, methods=['GET']),
Route("/lookup.shop", self.system.lookup_shop, methods=['GET']),
Route("/add.user", self.system.add_user, methods=['POST']),
@ -191,7 +193,7 @@ class FE_Base():
), media_type="text/html; charset=utf-8")
if sesh is None:
resp.delete_cookie("DIANA_SESH")
resp.delete_cookie("ARTEMIS_SESH")
return resp
def get_routes(self) -> List[Route]:
@ -215,6 +217,8 @@ class FE_Base():
sesh.current_ip = tk['current_ip']
sesh.permissions = tk['permissions']
sesh.chunithm_version = tk['chunithm_version']
sesh.maimai_version = tk['maimai_version']
sesh.ongeki_version = tk['ongeki_version']
if sesh.user_id <= 0:
self.logger.error("User session failed to validate due to an invalid ID!")
@ -240,7 +244,7 @@ class FE_Base():
return UserSession()
def validate_session(self, request: Request) -> Optional[UserSession]:
sesh = request.cookies.get('DIANA_SESH', "")
sesh = request.cookies.get('ARTEMIS_SESH', "")
if not sesh:
return None
@ -259,7 +263,17 @@ class FE_Base():
def encode_session(self, sesh: UserSession, exp_seconds: int = 86400) -> str:
try:
return jwt.encode({ "user_id": sesh.user_id, "current_ip": sesh.current_ip, "permissions": sesh.permissions, "ongeki_version": sesh.ongeki_version, "chunithm_version": sesh.chunithm_version, "exp": int(datetime.now(tz=timezone.utc).timestamp()) + exp_seconds }, b64decode(self.core_config.frontend.secret), algorithm="HS256")
return jwt.encode({
"user_id": sesh.user_id,
"current_ip": sesh.current_ip,
"permissions": sesh.permissions,
"ongeki_version": sesh.ongeki_version,
"chunithm_version": sesh.chunithm_version,
"maimai_version": sesh.maimai_version,
"exp": int(datetime.now(tz=timezone.utc).timestamp()) + exp_seconds },
b64decode(self.core_config.frontend.secret),
algorithm="HS256"
)
except jwt.InvalidKeyError:
self.logger.error("Failed to encode User session because the secret is invalid!")
return ""
@ -291,7 +305,7 @@ class FE_Gate(FE_Base):
error=err,
sesh=vars(UserSession()),
), media_type="text/html; charset=utf-8")
resp.delete_cookie("DIANA_SESH")
resp.delete_cookie("ARTEMIS_SESH")
return resp
async def render_login(self, request: Request):
@ -307,9 +321,13 @@ class FE_Gate(FE_Base):
uid = await self.data.card.get_user_id_from_card(access_code)
if uid is None:
self.logger.debug(f"Failed to find user for card {access_code}")
user = await self.data.user.get_user_by_username(access_code) # Lookup as username
if not user:
self.logger.debug(f"Failed to find user for card/username {access_code}")
return RedirectResponse("/gate/?e=1", 303)
uid = user['id']
user = await self.data.user.get_user(uid)
if user is None:
self.logger.error(f"Failed to load user {uid}")
@ -337,7 +355,7 @@ class FE_Gate(FE_Base):
usr_sesh = self.encode_session(sesh)
self.logger.debug(f"Created session with JWT {usr_sesh}")
resp = RedirectResponse("/user/", 303)
resp.set_cookie("DIANA_SESH", usr_sesh)
resp.set_cookie("ARTEMIS_SESH", usr_sesh)
return resp
@ -376,7 +394,7 @@ class FE_Gate(FE_Base):
usr_sesh = self.encode_session(sesh)
self.logger.debug(f"Created session with JWT {usr_sesh}")
resp = RedirectResponse("/user/", 303)
resp.set_cookie("DIANA_SESH", usr_sesh)
resp.set_cookie("ARTEMIS_SESH", usr_sesh)
return resp
@ -494,7 +512,7 @@ class FE_User(FE_Base):
async def render_logout(self, request: Request):
resp = RedirectResponse("/gate/", 303)
resp.delete_cookie("DIANA_SESH")
resp.delete_cookie("ARTEMIS_SESH")
return resp
async def edit_card(self, request: Request) -> RedirectResponse:
@ -783,6 +801,35 @@ class FE_System(FE_Base):
cabadd={"id": cab_id, "serial": serial},
), media_type="text/html; charset=utf-8")
async def render_logs(self, request: Request):
template = self.environment.get_template("core/templates/sys/logs.jinja")
events = []
usr_sesh = self.validate_session(request)
if not usr_sesh or not self.test_perm(usr_sesh.permissions, PermissionOffset.SYSADMIN):
return RedirectResponse("/sys/?e=11", 303)
logs = await self.data.base.get_event_log()
if not logs:
logs = []
for log in logs:
evt = log._asdict()
if not evt['user']: evt["user"] = "NONE"
if not evt['arcade']: evt["arcade"] = "NONE"
if not evt['machine']: evt["machine"] = "NONE"
if not evt['ip']: evt["ip"] = "NONE"
if not evt['game']: evt["game"] = "NONE"
if not evt['version']: evt["version"] = "NONE"
evt['when_logged'] = evt['when_logged'].strftime("%x %X")
events.append(evt)
return Response(template.render(
title=f"{self.core_config.server.name} | Event Logs",
sesh=vars(usr_sesh),
events=events
), media_type="text/html; charset=utf-8")
class FE_Arcade(FE_Base):
async def render_GET(self, request: Request):
template = self.environment.get_template("core/templates/arcade/index.jinja")
@ -849,7 +896,7 @@ class FE_Machine(FE_Base):
arcade={}
), media_type="text/html; charset=utf-8")
cfg_dir = environ.get("DIANA_CFG_DIR", "config")
cfg_dir = environ.get("ARTEMIS_CFG_DIR", "config")
cfg: CoreConfig = CoreConfig()
if path.exists(f"{cfg_dir}/core.yaml"):
cfg.update(yaml.safe_load(open(f"{cfg_dir}/core.yaml")))

View File

@ -15,18 +15,18 @@
-moz-appearance: textfield;
}
</style>
<form id="login" style="max-width: 240px; min-width: 10%;" action="/gate/gate.login" method="post">
<form id="login" style="max-width: 240px; min-width: 15%;" action="/gate/gate.login" method="post">
<div class="form-group row">
<label for="access_code">Card Access Code</label><br>
<input form="login" class="form-control" name="access_code" id="access_code" type="number" placeholder="00000000000000000000" maxlength="20" required>
<label for="access_code">Access Code or Username</label><br>
<input form="login" class="form-control" name="access_code" id="access_code" placeholder="00000000000000000000" maxlength="20" required aria-describedby="access_code_help">
<div id="access_code_help" class="form-text">20 Digit access code from a card registered to your account, or your account username. (NOT your username from a game!)</div>
</div>
<div class="form-group row">
<label for="passwd">Password</label><br>
<input id="passwd" class="form-control" name="passwd" type="password" placeholder="password">
<input id="passwd" class="form-control" name="passwd" type="password" placeholder="password" aria-describedby="passwd_help">
<div id="passwd_help" class="form-text">Leave blank if registering for the webui. Your card must have been used on a game connected to this server to register.</div>
</div>
<p></p>
<input id="submit" class="btn btn-primary" style="display: block; margin: 0 auto;" form="login" type="submit" value="Login">
</form>
<h6>*To register for the webui, type in the access code of your card, as shown in a game, and leave the password field blank.</h6>
<h6>*If you have not registered a card with this server, you cannot create a webui account.</h6>
{% endblock content %}

View File

@ -51,6 +51,9 @@
<button type="submit" class="btn btn-primary">Search</button>
</form>
</div>
<div class="col-sm-6" style="max-width: 25%;">
<a href="/sys/logs"><button class="btn btn-primary">Event Logs</button></a>
</div>
{% endif %}
</div>
<div class="row" id="rowResult" style="margin: 10px;">

View File

@ -0,0 +1,198 @@
{% extends "core/templates/index.jinja" %}
{% block content %}
<h1>Event Logs</h1>
<table class="table table-dark table-striped-columns" id="tbl_events">
<caption>Viewing last 100 logs</caption>
<thead>
<tr>
<th>Severity</th>
<th>Timestamp</th>
<th>System</th>
<th>Name</th>
<th>User</th>
<th>Arcade</th>
<th>Machine</th>
<th>Game</th>
<th>Version</th>
<th>Message</th>
<th>Params</th>
</tr>
</thead>
{% if events is not defined or events|length == 0 %}
<tr>
<td colspan="11" style="text-align:center"><i>No Events</i></td>
</tr>
{% endif %}
</table>
<div id="div_tbl_ctrl">
<select id="sel_per_page" onchange="update_tbl()">
<option value="10" selected>10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
&nbsp;
<button class="btn btn-primary" id="btn_prev" disabled onclick="chg_page(-1)"><<</button>
<button class="btn btn-primary" id="btn_next" onclick="chg_page(1)">>></button>
</div>
<script type="text/javascript">
{% if events is defined %}
const TBL_DATA = {{events}};
{% else %}
const TBL_DATA = [];
{% endif %}
var per_page = 0;
var page = 0;
function update_tbl() {
if (TBL_DATA.length == 0) { return; }
var tbl = document.getElementById("tbl_events");
for (var i = 0; i < per_page; i++) {
try{
tbl.deleteRow(1);
} catch {
break;
}
}
per_page = document.getElementById("sel_per_page").value;
if (per_page >= TBL_DATA.length) {
page = 0;
document.getElementById("btn_next").disabled = true;
document.getElementById("btn_prev").disabled = true;
}
for (var i = 0; i < per_page; i++) {
let off = (page * per_page) + i;
if (off >= TBL_DATA.length) {
if (page != 0) {
document.getElementById("btn_next").disabled = true;
document.getElementById("btn_prev").disabled = false;
}
break;
}
var data = TBL_DATA[off];
var row = tbl.insertRow(i + 1);
var cell_severity = row.insertCell(0);
switch (data.severity) {
case 10:
cell_severity.innerHTML = "DEBUG";
row.classList.add("table-success");
break;
case 20:
cell_severity.innerHTML = "INFO";
row.classList.add("table-info");
break;
case 30:
cell_severity.innerHTML = "WARN";
row.classList.add("table-warning");
break;
case 40:
cell_severity.innerHTML = "ERROR";
row.classList.add("table-danger");
break;
case 50:
cell_severity.innerHTML = "CRITICAL";
row.classList.add("table-danger");
break;
default:
cell_severity.innerHTML = "---";
row.classList.add("table-primary");
break;
}
var cell_ts = row.insertCell(1);
cell_ts.innerHTML = data.when_logged;
var cell_mod = row.insertCell(2);
cell_mod.innerHTML = data.system;
var cell_name = row.insertCell(3);
cell_name.innerHTML = data.type;
var cell_usr = row.insertCell(4);
if (data.user == 'NONE') {
cell_usr.innerHTML = "---";
} else {
cell_usr.innerHTML = "<a href=\"/user/" + data.user + "\">" + data.user + "</a>";
}
var cell_arcade = row.insertCell(5);
if (data.arcade == 'NONE') {
cell_arcade.innerHTML = "---";
} else {
cell_arcade.innerHTML = "<a href=\"/shop/" + data.arcade + "\">" + data.arcade + "</a>";
}
var cell_machine = row.insertCell(6);
if (data.arcade == 'NONE') {
cell_machine.innerHTML = "---";
} else {
cell_machine.innerHTML = "<a href=\"/cab/" + data.machine + "\">" + data.machine + "</a>";
}
var cell_game = row.insertCell(7);
if (data.game == 'NONE') {
cell_game.innerHTML = "---";
} else {
cell_game.innerHTML = data.game;
}
var cell_version = row.insertCell(8);
if (data.version == 'NONE') {
cell_version.innerHTML = "---";
} else {
cell_version.innerHTML = data.version;
}
var cell_msg = row.insertCell(9);
if (data.message == '') {
cell_msg.innerHTML = "---";
} else {
cell_msg.innerHTML = data.message;
}
var cell_deets = row.insertCell(10);
if (data.details == '{}') {
cell_deets.innerHTML = "---";
} else {
cell_deets.innerHTML = data.details;
}
}
}
function chg_page(num) {
var max_page = TBL_DATA.length / per_page;
console.log(max_page);
page = page + num;
if (page > max_page && max_page >= 1) {
page = max_page;
document.getElementById("btn_next").disabled = true;
document.getElementById("btn_prev").disabled = false;
return;
} else if (page < 0) {
page = 0;
document.getElementById("btn_next").disabled = false;
document.getElementById("btn_prev").disabled = true;
return;
} else if (page == 0) {
document.getElementById("btn_next").disabled = false;
document.getElementById("btn_prev").disabled = true;
} else {
document.getElementById("btn_next").disabled = false;
document.getElementById("btn_prev").disabled = false;
}
update_tbl();
}
update_tbl();
</script>
{% endblock content %}

View File

@ -21,6 +21,8 @@ New Nickname too long
You must be logged in to preform this action
{% elif error == 10 %}
Invalid serial number
{% elif error == 11 %}
Access Denied
{% else %}
An unknown error occoured
{% endif %}

View File

@ -80,7 +80,8 @@ class BaseServlet:
cfg_dir (str): Config directory
Returns:
Tuple[bool, str]: Tuple where offset 0 is true if the game is enabled, false otherwise, and offset 1 is the game CD
Tuple[bool, List[str], List[str]]: Tuple where offset 0 is true if the game is enabled, false otherwise, and offset 1 is the game CDs handled
by this servlette, and offset 2 is mucha netID prefixes that should be used for each game CD.
"""
return (False, [], [])

View File

@ -49,7 +49,7 @@ class Utils:
def get_title_port_ssl(cls, cfg: CoreConfig):
if cls.real_title_port_ssl is not None: return cls.real_title_port_ssl
cls.real_title_port_ssl = cfg.server.proxy_port_ssl if cfg.server.is_using_proxy and cfg.server.proxy_port_ssl else Utils.get_title_port(cfg)
cls.real_title_port_ssl = cfg.server.proxy_port_ssl if cfg.server.is_using_proxy and cfg.server.proxy_port_ssl else 443
return cls.real_title_port_ssl

View File

@ -1,129 +0,0 @@
# ARTEMiS - Ubuntu 20.04 LTS Guide
This step-by-step guide assumes that you are using a fresh install of Ubuntu 20.04 LTS, some of the steps can be skipped if you already have an installation with MySQL 5.7 or even some of the modules already present on your environment
# Setup
## Install memcached module
1. sudo apt-get install memcached
2. Under the file /etc/memcached.conf, please make sure the following parameters are set:
```
# Start with a cap of 64 megs of memory. It's reasonable, and the daemon default
# Note that the daemon will grow to this size, but does not start out holding this much
# memory
-I 128m
-m 1024
```
** This is mandatory to avoid memcached overload caused by Crossbeats or by massive profiles
3. Restart memcached using: sudo systemctl restart memcached
## Install MySQL 5.7
```
sudo apt update
sudo apt install wget -y
wget https://dev.mysql.com/get/mysql-apt-config_0.8.12-1_all.deb
sudo dpkg -i mysql-apt-config_0.8.12-1_all.deb
```
1. During the first prompt, select Ubuntu Bionic
2. Select the default option
3. Select MySQL 5.7
4. Select the last option
```
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 467B942D3A79BD29
sudo apt-get update
sudo apt-cache policy mysql-server
sudo apt install -f mysql-client=5.7* mysql-community-server=5.7* mysql-server=5.7*
```
## Default Configuration for MySQL Server
1. sudo mysql_secure_installation
> Make sure to follow the steps that will be prompted such as changing the mysql root password and such
2. Test your MySQL Server login by doing the following command :
> mysql -u root -p
## Create the default ARTEMiS database and user
1. mysql -u root -p
2. Please change the password indicated in the next line for a custom secure one and continue with the next commands
```
CREATE USER 'aime'@'localhost' IDENTIFIED BY 'MyStrongPass.';
CREATE DATABASE aime;
GRANT Alter,Create,Delete,Drop,Index,Insert,References,Select,Update ON aime.* TO 'aime'@'localhost';
FLUSH PRIVILEGES;
exit;
```
3. sudo systemctl restart mysql
## Install Python modules
```
sudo apt-get install python3-dev default-libmysqlclient-dev build-essential mysql-client libmysqlclient-dev libmemcached-dev
sudo apt install libpython3.8-dev
sudo apt-get install python3-software-properties
sudo apt install python3-pip
sudo pip3 install --upgrade pip testresources
sudo pip3 install --upgrade pip setuptools
sudo apt-get install python3-tk
```
7. Change your work path to the ARTEMiS root folder using 'cd' and install the requirements:
> sudo python3 -m pip install -r requirements.txt
## Copy/Rename the folder example_config to config
## Adjust /config/core.yaml
1. Make sure to change the server listen_address to be set to your local machine IP (ex.: 192.168.1.xxx)
2. Adjust the proper MySQL information you created earlier
3. Add the AimeDB key at the bottom of the file
## Create the database tables for ARTEMiS
1. sudo python3 dbutils.py create
2. If you get "No module named Crypto", run the following command:
```
sudo pip uninstall crypto
sudo pip uninstall pycrypto
sudo pip install pycrypto
```
## Firewall Adjustements
```
sudo ufw allow 80
sudo ufw allow 443
sudo ufw allow 8443
sudo ufw allow 22345
sudo ufw allow 8090
sudo ufw allow 8444
sudo ufw allow 8080
```
## Running the ARTEMiS instance
1. sudo python3 index.py
# Troubleshooting
## Game does not connect to ARTEMiS Allnet server
1. Double-check your core.yaml, the listen_address is most likely either not binded to the proper IP or the port is not opened
## Game does not connect to Title Server
1. Verify that your core.yaml is setup properly for both the server listen_address and title hostname
2. Boot your game and verify that an AllNet response does show and if it does, attempt to open the URI that is shown under a browser such as Edge, Chrome & Firefox.
3. If a page is shown, the server is working properly and if it doesn't, double check your port forwarding and also that you have entered the proper local IP under the Title hostname in core.yaml.
## Unhandled command under AimeDB
1. Double check your AimeDB key under core.yaml, it is incorrect.
## Memcache failed, error 3
1. Make sure memcached is properly installed and running. You can check the status of the service using the following command:
> sudo systemctl status memcached
2. If it is failing, double check the /etc/memcached.conf file, it may have duplicated arguments like the -I and -m
3. If it is still not working afterward, you can proceed with a workaround by manually editing the /core/data/cache.py file.
```
# Make memcache optional
try:
has_mc = False
except ModuleNotFoundError:
has_mc = False
```

View File

@ -59,7 +59,7 @@ GRANT Alter,Create,Delete,Drop,Index,Insert,References,Select,Update ON aime.* T
- Put the password you created for the aime user into the `database` section.
- Put in the aimedb key (YOU DO NOT GENERATE THIS KEY, FIND IT SOMEWHERE).
- Set your hostname to be whatever hostname or IP address games can reach your server at (many games reject localhost and 127.0.0.1).
- Optional: generate base64-encoded secrets for aimedb and frontend.
- Optional: generate base64-encoded secrets for aimedb and frontend using something like `openssl rand -base64 64`. It is advised to make all secrets different.
- See [config.md](docs/config.md) for a full list of options.
- edit `idz.yaml`
- If you don't plan on anyone using your server to play Initial D Zero, it is best to disable it to cut down on console spam on boot.

View File

@ -1,102 +0,0 @@
# ARTEMiS - Windows 10/11 Guide
This step-by-step guide assumes that you are using a fresh install of Windows 10/11 without MySQL installed, some of the steps can be skipped if you already have an installation with MySQL 8.0 or even some of the modules already present on your environment
# Setup
## Install Python Python 3.9 (recommended) or 3.10
1. Download Python 3.9 : [Link](https://www.python.org/ftp/python/3.9.13/python-3.9.13-amd64.exe)
2. Install python-3.9.13-amd64.exe
1. Select Customize installation
2. Make sure that pip, tcl/tk, and the for all users are checked and hit Next
3. Make sure that you enable "Create shortcuts for installed applications" and "Add Python to environment variables" and hit Install
## Install MySQL 8.0
1. Download MySQL 8.0 Server : [Link](https://dev.mysql.com/get/Downloads/MySQLInstaller/mysql-installer-community-8.0.34.0.msi)
2. Install mysql-installer-web-community-8.0.34.0.msi
1. Click on "Add ..." on the side
2. Click on the "+" next to MySQL Servers
3. Make sure MySQL Server 8.0.34 - X64 is under the products to be installed.
4. Hit Next and Next once installed
5. Select the configuration type "Development Computer"
6. Hit Next
7. Select "Use Legacy Authentication Method (Retain MySQL 5.x compatibility)" and hit Next
8. Enter a root password and then hit Next >
9. Leave everything under Windows Service as default and hit Next >
10. Click on Execute and for it to finish and hit Next> and then Finish
3. Open MySQL 8.0 Command Line Client and login as your root user
4. Change `<Enter Password Here>` to a new password for the user aime, type those commands to create your user and the database
```sql
CREATE USER 'aime'@'localhost' IDENTIFIED BY '<Enter Password Here>';
CREATE DATABASE aime;
GRANT Alter,Create,Delete,Drop,Index,Insert,References,Select,Update ON aime.* TO 'aime'@'localhost';
FLUSH PRIVILEGES;
exit;
```
## Install Python modules
1. Change your work path to the artemis-master folder using 'cd' and install the requirements:
```shell
pip install -r requirements.txt
```
## Copy/Rename the folder `example_config` to `config`
## Adjust `config/core.yaml`
1. Make sure to change the server `hostname` to be set to your local machine IP (ex.: 192.168.xxx.xxx)
- In case you want to run this only locally, set the following values:
```yaml
server:
listen_address: 0.0.0.0
title:
hostname: 192.168.xxx.xxx
```
1. Adjust the proper MySQL information you created earlier
```yaml
database:
host: "localhost"
username: "aime"
password: "<Enter Password Here>"
name: "aime"
```
3. Add the AimeDB key at the bottom of the file
4. If the webui is needed, change the flag from False to True
## Create the database tables for ARTEMiS
```shell
python dbutils.py create
```
## Firewall Adjustements
Make sure the following ports are open both on your router and local Windows firewall in case you want to use this for public use (NOT recommended):
> Port 80 (TCP), 443 (TCP), 8443 (TCP), 22345 (TCP), 8080 (TCP), 8090 (TCP) **webui, 8444 (TCP) **mucha
## Running the ARTEMiS instance
```shell
python index.py
```
# Troubleshooting
## Game does not connect to ARTEMiS Allnet server
1. Double-check your core.yaml, the listen_address is most likely either not binded to the proper IP or the port is not opened
## Game does not connect to Title Server
1. Verify that your core.yaml is setup properly for both the server listen_address and title hostname
2. Boot your game and verify that an AllNet response does show and if it does, attempt to open the URI that is shown under a browser such as Edge, Chrome & Firefox.
3. If a page is shown, the server is working properly and if it doesn't, double check your port forwarding and also that you have entered the proper local IP under the Title hostname in core.yaml.
## Unhandled command under AimeDB
1. Double check your AimeDB key under core.yaml, it is incorrect.
## AttributeError: module 'collections' has no attribute 'Hashable'
1. This means the pyYAML module is obsolete, simply rerun pip with the -U (force update) flag, as shown below.
- Change your work path to the artemis-master (or artemis-develop) folder using 'cd' and run the following commands:
```shell
pip install -r requirements.txt -U
```

View File

@ -17,8 +17,8 @@
- `strict_ip_checking`: Rejects clients if there is no IP in the `arcade` table for the respective arcade. Default `False`
## Title
- `loglevel`: Logging level for the title server. Default `info`
- `reboot_start_time`: 24 hour JST time that clients will see as the start of maintenance period, ex `04:00`. Leave blank for no maintenance time. Default: `""`
- `reboot_end_time`: 24 hour JST time that clients will see as the end of maintenance period, ex `05:00`. Leave blank for no maintenance time. Default: `""`
- `reboot_start_time`: 24 hour JST time that clients will see as the start of maintenance period, ex `04:00`. A few games or early version will report errors if it is empty, ex maimai DX 1.00
- `reboot_end_time`: 24 hour JST time that clients will see as the end of maintenance period, ex `07:00`. this must be set to 7:00 am for some game, please do not change it.
## Database
- `host`: Host of the database. Default `localhost`
- `username`: Username of the account the server should connect to the database with. Default `aime`

View File

@ -63,6 +63,7 @@ Games listed below have been tested and confirmed working.
| 12 | CHUNITHM NEW PLUS!! |
| 13 | CHUNITHM SUN |
| 14 | CHUNITHM SUN PLUS |
| 15 | CHUNITHM LUMINOUS |
### Importer
@ -87,13 +88,19 @@ Config file is located in `config/chuni.yaml`.
| `crypto` | This option is used to enable the TLS Encryption |
**If you would like to use network encryption, the following will be required underneath but key, iv and hash are required:**
If you would like to use network encryption, add the keys to the `keys` section under `crypto`, where the key
is the version ID for Japanese (SDHD) versions and `"{versionID}_int"` for Export (SDGS) versions, and the value
is an array containing `[key, iv, salt, iter_count]` in order.
`iter_count` is optional for all Japanese (SDHD) versions but may be required for some Export (SDGS) versions.
You will receive an error in the logs if it needs to be specified.
```yaml
crypto:
encrypted_only: False
keys:
13: ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"]
"13_int": ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000", 42]
```
### Database upgrade
@ -175,6 +182,14 @@ Config file is located in `config/cxb.yaml`.
## maimai DX
### Presents
Presents are items given to the user when they login, with a little animation (for example, the KOP song was given to the finalists as a present). To add a present, you must insert it into the `mai2_item_present` table. In that table, a NULL version means any version, a NULL user means any user, a NULL start date means always open, and a NULL end date means it never expires. Below is a list of presents one might wish to add:
| Game Version | Item ID | Item Kind | Item Description | Present Description |
|--------------|---------|-----------|-------------------------------------------------|------------------------------------------------|
| BUDDiES (21) | 409505 | Icon (3) | 旅行スタンプ(月面基地) (Travel Stamp - Moon Base) | Officially obtained on the webui with a serial |
| | | | | number, for project raputa |
### Versions
| Game Code | Version ID | Version Name |

34
docs/migrating.md Normal file
View File

@ -0,0 +1,34 @@
# Migrating from an older build of ARTEMiS
If you haven't updated artemis in a while, you may find that configuration options have moved, been renamed, or no longer exist. This document exists to help migrate from legacy versions of artemis to newer builds.
## Dependancies
Make sure your dependiences are up to date with what's required to run artemis. A simple `pip install -r requirements.txt` will get you up to date.
## Database
Database migration is required if you are using a version of artemis that still uses the old custom-rolled database versioning system (raw SQL scripts). Artemis now uses alembic to manage database versioning, and you will need to move to this new system.
**BEFORE DOING ANY DATABASE WORK, ALWAYS MAKE SURE YOU HAVE FUNCTIONAL, UP-TO-DATE BACKUPS!!**
For almost all situations, simply running `python dbutils.py migrate` will do the job. This will upgrade you to the latest version of the old system, move you over to alembic, then upgrade you to the newest alembic version. If you encounter any errors or data loss, you should report this as a bug to our issue tracker.
## Configuration
Configuration management is the sewage cleaning of the sysadmin world. It sucks and nobody likes to do it, but it needs to be done or everyone ends up in deep shit. This section will walk through what configuration options have changed, and how to set them properly.
### core.yaml
`title`->`hostname` is now `server`->`hostname`. This hostname is what gets sent to clients in response to auth requests, so it should be both accessable from whereever the client is, and point properly to the title server.
With the move to starlette and uvicorn, different services now run as seperate USGI applications. `billing`->`standalone` and `allnet`->`standalone` are flags that determine weather the service runs as a stand-alone service, on it's own seperate port, or as a part of the whole application. For example, setting `billing`->`standalone` to `True` will cause a seperate instance of the billing server to spin up listening on 8443 with SSL using the certs listed in the config file. Setting it to `False` will just allow the main server to also serve `/request/` and assumes that something is standing in front of it proxying 8443 SSL to whatever `server`->`port` is set to.
Beforehand, if `server`->`is_develop` was `False`, the server assumed that there was a proxy standing in front of it, proxying requests to proper channels. This was, in hindsight, a very dumb assumption. Now, `server`->`is_using_proxy` is what flags the server as having nginx or another proxy in front of it. The effects of setting this to true are somewhat game-dependant, but generally artemis will use the port listed in `server`->`proxy_port` (and `server`->`proxy_port_ssl` for SSL connections, as defined by the games) instead of `server`->`port`. If set to 0, `server`->`proxy_port` will default to what `server`->`port` (and `server`->`proxy_port_ssl` will default to 443) make sure to set them accordingly. Note that some title servers have their own needs and specify their own specific ports. Refer to [game_specific_info.md](docs/game_specific_info.md) for more infomation. (For example, pokken requires SSL using an older, weaker certificate, and always requires the port to be sent even if it's port 443)
`index.py`'s args have changed. You can now override what port the title server listens on with `-p` and tell the server to use ssl with `-s`.
Rather then having a `standalone` config variable, the frontend is a seperate wsgi app entirely. Having `enable` be `True` will launch it on the port specified in the config file. Otherwise, the fontend will not run.
`title`->`reboot_start_time`/`reboot_end_time` allow you to specify when the games should be told network maintanence is happening. It's exact implementation depends on the game. Do note that many games will beave unexpectly if `reboot_end_time` is not `07:00`.
If you wish to make use of aimedb's SegaAuthId system to better protect the few title servers that actually use it, set `aimedb`->`id_secret` to base64-encoded random bytes (32 is a good length) using something like `openssl rand -base64 64`. If you intend to use the frontend, the same thing must be done for `frontend`->`secret` or you won't be able to log in.
`mucha`'s only option is now just log level.
`aimedb` now has it's own `listen_address` field, in case you want to proxy everything but aimedb, so it can still listen on `0.0.0.0` instead of `127.0.0.1`.

View File

@ -22,6 +22,9 @@ version:
14:
rom: 2.15.00
data: 2.15.00
15:
rom: 2.20.00
data: 2.20.00
crypto:
encrypted_only: False

View File

@ -18,7 +18,7 @@ server:
title:
loglevel: "info"
reboot_start_time: "04:00"
reboot_end_time: "05:00"
reboot_end_time: "07:00" # this must be set to 7:00 am for some game, please do not change it
database:
host: "localhost"

View File

@ -1,4 +1,4 @@
server:
enable: True
loglevel: "info"
use:https: True
use_https: True

View File

@ -2,12 +2,19 @@ server:
enable: True
loglevel: "info"
auto_register: True
photon_app_id: "7df3a2f6-d69d-4073-aafe-810ee61e1cea"
data_version: 1
game_version: 33
crypt:
enable: False
key: ""
iv: ""
hash:
verify_hash: False
hash_base: ""
card:
enable: True
crypt_password: ""
crypt_salt: ""

View File

@ -10,6 +10,7 @@ Games listed below have been tested and confirmed working. Only game versions ol
+ CHUNITHM INTL
+ SUPERSTAR
+ SUPERSTAR PLUS
+ NEW
+ NEW PLUS
+ SUN

View File

@ -953,6 +953,31 @@ class ChuniBase:
upsert[rating_type],
)
# added in LUMINOUS
if "userCMissionList" in upsert:
for cmission in upsert["userCMissionList"]:
mission_id = cmission["missionId"]
await self.data.item.put_cmission(
user_id,
{
"missionId": mission_id,
"point": cmission["point"],
},
)
for progress in cmission["userCMissionProgressList"]:
await self.data.item.put_cmission_progress(user_id, mission_id, progress)
if "userNetBattleData" in upsert:
net_battle = upsert["userNetBattleData"][0]
# fix the boolean
net_battle["isRankUpChallengeFailed"] = (
False if net_battle["isRankUpChallengeFailed"] == "false" else True
)
await self.data.profile.put_net_battle(user_id, net_battle)
return {"returnCode": "1"}
async def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict:

View File

@ -1,3 +1,6 @@
from enum import Enum
class ChuniConstants:
GAME_CODE = "SDBT"
GAME_CODE_NEW = "SDHD"
@ -20,6 +23,7 @@ class ChuniConstants:
VER_CHUNITHM_NEW_PLUS = 12
VER_CHUNITHM_SUN = 13
VER_CHUNITHM_SUN_PLUS = 14
VER_CHUNITHM_LUMINOUS = 15
VERSION_NAMES = [
"CHUNITHM",
"CHUNITHM PLUS",
@ -35,9 +39,22 @@ class ChuniConstants:
"CHUNITHM NEW!!",
"CHUNITHM NEW PLUS!!",
"CHUNITHM SUN",
"CHUNITHM SUN PLUS"
"CHUNITHM SUN PLUS",
"CHUNITHM LUMINOUS",
]
@classmethod
def game_ver_to_string(cls, ver: int):
return cls.VERSION_NAMES[ver]
class MapAreaConditionType(Enum):
UNLOCKED = 0
MAP_CLEARED = 1
MAP_AREA_CLEARED = 2
TROPHY_OBTAINED = 3
class MapAreaConditionLogicalOperator(Enum):
AND = 1
OR = 2

View File

@ -68,7 +68,7 @@ class ChuniFrontend(FE_Base):
if usr_sesh.chunithm_version >= 0:
encoded_sesh = self.encode_session(usr_sesh)
resp.set_cookie("DIANA_SESH", encoded_sesh)
resp.set_cookie("ARTEMIS_SESH", encoded_sesh)
return resp
else:
@ -240,7 +240,7 @@ class ChuniFrontend(FE_Base):
encoded_sesh = self.encode_session(usr_sesh)
self.logger.info(f"Created session with JWT {encoded_sesh}")
resp = RedirectResponse("/game/chuni/", 303)
resp.set_cookie("DIANA_SESH", encoded_sesh)
resp.set_cookie("ARTEMIS_SESH", encoded_sesh)
return resp
else:
return RedirectResponse("/gate/", 303)

View File

@ -1,7 +1,8 @@
from starlette.requests import Request
from starlette.routing import Route
from starlette.responses import Response
import logging, coloredlogs
import logging
import coloredlogs
from logging.handlers import TimedRotatingFileHandler
import zlib
import yaml
@ -34,12 +35,13 @@ from .new import ChuniNew
from .newplus import ChuniNewPlus
from .sun import ChuniSun
from .sunplus import ChuniSunPlus
from .luminous import ChuniLuminous
class ChuniServlet(BaseServlet):
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
super().__init__(core_cfg, cfg_dir)
self.game_cfg = ChuniConfig()
self.hash_table: Dict[Dict[str, str]] = {}
self.hash_table: Dict[str, Dict[str, str]] = {}
if path.exists(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"):
self.game_cfg.update(
yaml.safe_load(open(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"))
@ -61,6 +63,7 @@ class ChuniServlet(BaseServlet):
ChuniNewPlus,
ChuniSun,
ChuniSunPlus,
ChuniLuminous,
]
self.logger = logging.getLogger("chuni")
@ -89,30 +92,60 @@ class ChuniServlet(BaseServlet):
)
self.logger.inited = True
known_iter_counts = {
ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS: 67,
ChuniConstants.VER_CHUNITHM_PARADISE: 44,
f"{ChuniConstants.VER_CHUNITHM_PARADISE}_int": 25,
ChuniConstants.VER_CHUNITHM_NEW: 54,
f"{ChuniConstants.VER_CHUNITHM_NEW}_int": 49,
ChuniConstants.VER_CHUNITHM_NEW_PLUS: 25,
ChuniConstants.VER_CHUNITHM_SUN: 70,
ChuniConstants.VER_CHUNITHM_SUN_PLUS: 36,
ChuniConstants.VER_CHUNITHM_LUMINOUS: 8,
}
for version, keys in self.game_cfg.crypto.keys.items():
if len(keys) < 3:
continue
self.hash_table[version] = {}
if isinstance(version, int):
version_idx = version
else:
version_idx = int(version.split("_")[0])
salt = bytes.fromhex(keys[2])
if len(keys) >= 4:
iter_count = keys[3]
elif (iter_count := known_iter_counts.get(version)) is None:
self.logger.error(
"Number of iteration rounds for version %s is not known, but it is not specified in the config",
version,
)
continue
self.hash_table[version] = {}
method_list = [
method
for method in dir(self.versions[version])
for method in dir(self.versions[version_idx])
if not method.startswith("__")
]
for method in method_list:
method_fixed = inflection.camelize(method)[6:-7]
# number of iterations was changed to 70 in SUN and then to 36
if version == ChuniConstants.VER_CHUNITHM_SUN_PLUS:
iter_count = 36
elif version == ChuniConstants.VER_CHUNITHM_SUN:
iter_count = 70
else:
iter_count = 44
# This only applies for CHUNITHM NEW International and later for some reason.
# CHUNITHM SUPERSTAR (PLUS) did not add "Exp" to the endpoint when hashing.
if (
isinstance(version, str)
and version.endswith("_int")
and version_idx >= ChuniConstants.VER_CHUNITHM_NEW
):
method_fixed += "C3Exp"
hash = PBKDF2(
method_fixed,
bytes.fromhex(keys[2]),
salt,
128,
count=iter_count,
hmac_hash_module=SHA1,
@ -122,7 +155,7 @@ class ChuniServlet(BaseServlet):
self.hash_table[version][hashed_name] = method_fixed
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 {salt} to get {hashed_name}"
)
@classmethod
@ -195,47 +228,59 @@ class ChuniServlet(BaseServlet):
internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS
elif version >= 210 and version < 215: # SUN
internal_ver = ChuniConstants.VER_CHUNITHM_SUN
elif version >= 215: # SUN
elif version >= 215 and version < 220: # SUN PLUS
internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS
elif version >= 220: # LUMINOUS
internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS
elif game_code == "SDGS": # Int
if version < 110: # SUPERSTAR
internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE # FIXME: Not sure what was intended to go here? was just "PARADISE"
if version < 110: # SUPERSTAR / SUPERSTAR PLUS
internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE # SUPERSTAR / SUPERSTAR PLUS worked fine with it
elif version >= 110 and version < 115: # NEW
internal_ver = ChuniConstants.VER_CHUNITHM_NEW
elif version >= 115 and version < 120: # NEW PLUS!!
internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS
elif version >= 120 and version < 125: # SUN
internal_ver = ChuniConstants.VER_CHUNITHM_SUN
elif version >= 125: # SUN PLUS
elif version >= 125 and version < 130: # SUN PLUS
internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS
elif version >= 130: # LUMINOUS
internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS
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
# doing encrypted. The likelyhood of false positives is low but
# doing encrypted. The likelihood of false positives is low but
# technically not 0
if game_code == "SDGS":
crypto_cfg_key = f"{internal_ver}_int"
hash_table_key = f"{internal_ver}_int"
else:
crypto_cfg_key = internal_ver
hash_table_key = internal_ver
if internal_ver < ChuniConstants.VER_CHUNITHM_NEW:
endpoint = request.headers.get("User-Agent").split("#")[0]
else:
if internal_ver not in self.hash_table:
if hash_table_key not in self.hash_table:
self.logger.error(
f"v{version} does not support encryption or no keys entered"
)
return Response(zlib.compress(b'{"stat": "0"}'))
elif endpoint.lower() not in self.hash_table[internal_ver]:
elif endpoint.lower() not in self.hash_table[hash_table_key]:
self.logger.error(
f"No hash found for v{version} endpoint {endpoint}"
)
return Response(zlib.compress(b'{"stat": "0"}'))
endpoint = self.hash_table[internal_ver][endpoint.lower()]
endpoint = self.hash_table[hash_table_key][endpoint.lower()]
try:
crypt = AES.new(
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][0]),
bytes.fromhex(self.game_cfg.crypto.keys[crypto_cfg_key][0]),
AES.MODE_CBC,
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]),
bytes.fromhex(self.game_cfg.crypto.keys[crypto_cfg_key][1]),
)
req_raw = crypt.decrypt(req_raw)
@ -272,11 +317,13 @@ class ChuniServlet(BaseServlet):
self.logger.info(f"v{version} {endpoint} request from {client_ip}")
self.logger.debug(req_data)
endpoint = (
endpoint.replace("C3Exp", "")
if game_code == "SDGS"
else endpoint
)
if game_code == "SDGS" and version >= 110:
endpoint = endpoint.replace("C3Exp", "")
elif game_code == "SDGS" and version < 110:
endpoint = endpoint.replace("Exp", "")
else:
endpoint = endpoint
func_to_find = "handle_" + inflection.underscore(endpoint) + "_request"
handler_cls = self.versions[internal_ver](self.core_cfg, self.game_cfg)
@ -293,7 +340,7 @@ class ChuniServlet(BaseServlet):
self.logger.error(f"Error handling v{version} method {endpoint} - {e}")
return Response(zlib.compress(b'{"stat": "0"}'))
if resp == None:
if resp is None:
resp = {"returnCode": 1}
self.logger.debug(f"Response {resp}")

298
titles/chuni/luminous.py Normal file
View File

@ -0,0 +1,298 @@
from datetime import timedelta
from typing import Dict
from core.config import CoreConfig
from titles.chuni.sunplus import ChuniSunPlus
from titles.chuni.const import ChuniConstants, MapAreaConditionLogicalOperator, MapAreaConditionType
from titles.chuni.config import ChuniConfig
class ChuniLuminous(ChuniSunPlus):
def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
super().__init__(core_cfg, game_cfg)
self.version = ChuniConstants.VER_CHUNITHM_LUMINOUS
async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
user_data = await super().handle_cm_get_user_preview_api_request(data)
# Does CARD MAKER 1.35 work this far up?
user_data["lastDataVersion"] = "2.20.00"
return user_data
async def handle_get_user_c_mission_api_request(self, data: Dict) -> Dict:
user_id = data["userId"]
mission_id = data["missionId"]
progress_list = []
point = 0
mission_data = await self.data.item.get_cmission(user_id, mission_id)
progress_data = await self.data.item.get_cmission_progress(user_id, mission_id)
if mission_data and progress_data:
point = mission_data["point"]
for progress in progress_data:
progress_list.append(
{
"order": progress["order"],
"stage": progress["stage"],
"progress": progress["progress"],
}
)
return {
"userId": user_id,
"missionId": mission_id,
"point": point,
"userCMissionProgressList": progress_list,
}
async def handle_get_user_net_battle_ranking_info_api_request(self, data: Dict) -> Dict:
user_id = data["userId"]
net_battle = {}
net_battle_data = await self.data.profile.get_net_battle(user_id)
if net_battle_data:
net_battle = {
"isRankUpChallengeFailed": net_battle_data["isRankUpChallengeFailed"],
"highestBattleRankId": net_battle_data["highestBattleRankId"],
"battleIconId": net_battle_data["battleIconId"],
"battleIconNum": net_battle_data["battleIconNum"],
"avatarEffectPoint": net_battle_data["avatarEffectPoint"],
}
return {
"userId": user_id,
"userNetBattleData": net_battle,
}
async def handle_get_game_map_area_condition_api_request(self, data: Dict) -> Dict:
# There is no game data for this, everything is server side.
# However, we can selectively show/hide events as data is imported into the server.
events = await self.data.static.get_enabled_events(self.version)
event_by_id = {evt["eventId"]: evt for evt in events}
conditions = []
# The Mystic Rainbow of LUMINOUS map unlocks when any mainline LUMINOUS area
# (ep. I, ep. II, ep. III) are completed.
mystic_area_1_conditions = {
"mapAreaId": 3229301, # Mystic Rainbow of LUMINOUS Area 1
"length": 0,
"mapAreaConditionList": [],
}
mystic_area_1_added = False
# Secret AREA: MUSIC GAME
if 14029 in event_by_id:
start_date = event_by_id[14029]["startDate"].strftime(self.date_time_format)
mission_in_progress_end_date = "2099-12-31 00:00:00.0"
# The "MISSION in progress" trophy required to trigger the secret area
# is only available in the first CHUNITHM mission. If the second mission
# (event ID 14214) was imported into ARTEMiS, we disable the requirement
# for this trophy.
if 14214 in event_by_id:
mission_in_progress_end_date = (event_by_id[14214]["startDate"] - timedelta(hours=2)).strftime(self.date_time_format)
conditions.extend([
{
"mapAreaId": 2206201, # BlythE ULTIMA
"length": 1,
# Obtain the trophy "MISSION in progress".
"mapAreaConditionList": [
{
"type": MapAreaConditionType.TROPHY_OBTAINED.value,
"conditionId": 6832,
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
"startDate": start_date,
"endDate": mission_in_progress_end_date,
}
],
},
{
"mapAreaId": 2206202, # PRIVATE SERVICE ULTIMA
"length": 1,
# Obtain the trophy "MISSION in progress".
"mapAreaConditionList": [
{
"type": MapAreaConditionType.TROPHY_OBTAINED.value,
"conditionId": 6832,
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
"startDate": start_date,
"endDate": mission_in_progress_end_date,
}
],
},
{
"mapAreaId": 2206203, # New York Back Raise
"length": 1,
# SS NightTheater's EXPERT chart and get the title
# "今宵、劇場に映し出される景色とは――――。"
"mapAreaConditionList": [
{
"type": MapAreaConditionType.TROPHY_OBTAINED.value,
"conditionId": 6833,
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
"startDate": start_date,
"endDate": "2099-12-31 00:00:00.0",
},
],
},
{
"mapAreaId": 2206204, # Spasmodic
"length": 2,
# - Get 1 miss on Random (any difficulty) and get the title "当たり待ち"
# - Get 1 miss on 花たちに希望を (any difficulty) and get the title "花たちに希望を"
"mapAreaConditionList": [
{
"type": MapAreaConditionType.TROPHY_OBTAINED.value,
"conditionId": 6834,
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
"startDate": start_date,
"endDate": "2099-12-31 00:00:00.0",
},
{
"type": MapAreaConditionType.TROPHY_OBTAINED.value,
"conditionId": 6835,
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
"startDate": start_date,
"endDate": "2099-12-31 00:00:00.0",
},
],
},
{
"mapAreaId": 2206205, # ΩΩPARTS
"length": 2,
# - S Sage EXPERT to get the title "マターリ進行キボンヌ"
# - Equip this title and play cab-to-cab with another person with this title
# to get "マターリしようよ". Disabled because it is difficult to play cab2cab
# on data setups. A network operator may consider re-enabling it by uncommenting
# the second condition.
"mapAreaConditionList": [
{
"type": MapAreaConditionType.TROPHY_OBTAINED.value,
"conditionId": 6836,
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
"startDate": start_date,
"endDate": "2099-12-31 00:00:00.0",
},
# {
# "type": MapAreaConditionType.TROPHY_OBTAINED.value,
# "conditionId": 6837,
# "logicalOpe": MapAreaConditionLogicalOperator.AND.value,
# "startDate": start_date,
# "endDate": "2099-12-31 00:00:00.0",
# },
],
},
{
"mapAreaId": 2206206, # Blow My Mind
"length": 1,
# SS on CHAOS EXPERT, Hydra EXPERT, Surive EXPERT and Jakarta PROGRESSION EXPERT
# to get the title "Can you hear me?"
"mapAreaConditionList": [
{
"type": MapAreaConditionType.TROPHY_OBTAINED.value,
"conditionId": 6838,
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
"startDate": start_date,
"endDate": "2099-12-31 00:00:00.0",
},
],
},
{
"mapAreaId": 2206207, # VALLIS-NERIA
"length": 6,
# Finish the 6 other areas
"mapAreaConditionList": [
{
"type": MapAreaConditionType.MAP_AREA_CLEARED.value,
"conditionId": x,
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
"startDate": start_date,
"endDate": "2099-12-31 00:00:00.0",
}
for x in range(2206201, 2206207)
],
},
])
# LUMINOUS ep. I
if 14005 in event_by_id:
start_date = event_by_id[14005]["startDate"].strftime(self.date_time_format)
if not mystic_area_1_added:
conditions.append(mystic_area_1_conditions)
mystic_area_1_added = True
mystic_area_1_conditions["length"] += 1
mystic_area_1_conditions["mapAreaConditionList"].append(
{
"type": MapAreaConditionType.MAP_CLEARED.value,
"conditionId": 3020701,
"logicalOpe": MapAreaConditionLogicalOperator.OR.value,
"startDate": start_date,
"endDate": "2099-12-31 00:00:00.0",
}
)
conditions.append(
{
"mapAreaId": 3229302, # Mystic Rainbow of LUMINOUS Area 2,
"length": 1,
# Unlocks when LUMINOUS ep. I is completed.
"mapAreaConditionList": [
{
"type": MapAreaConditionType.MAP_CLEARED.value,
"conditionId": 3020701,
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
"startDate": start_date,
"endDate": "2099-12-31 00:00:00.0",
},
],
}
)
# LUMINOUS ep. II
if 14251 in event_by_id:
start_date = event_by_id[14251]["startDate"].strftime(self.date_time_format)
if not mystic_area_1_added:
conditions.append(mystic_area_1_conditions)
mystic_area_1_added = True
mystic_area_1_conditions["length"] += 1
mystic_area_1_conditions["mapAreaConditionList"].append(
{
"type": MapAreaConditionType.MAP_CLEARED.value,
"conditionId": 3020702,
"logicalOpe": MapAreaConditionLogicalOperator.OR.value,
"startDate": start_date,
"endDate": "2099-12-31 00:00:00.0",
}
)
conditions.append(
{
"mapAreaId": 3229303, # Mystic Rainbow of LUMINOUS Area 3,
"length": 1,
# Unlocks when LUMINOUS ep. II is completed.
"mapAreaConditionList": [
{
"type": MapAreaConditionType.MAP_CLEARED.value,
"conditionId": 3020702,
"logicalOpe": MapAreaConditionLogicalOperator.AND.value,
"startDate": start_date,
"endDate": "2099-12-31 00:00:00.0",
},
],
}
)
return {
"length": len(conditions),
"gameMapAreaConditionList": conditions,
}

View File

@ -32,6 +32,8 @@ class ChuniNew(ChuniBase):
return "210"
if self.version == ChuniConstants.VER_CHUNITHM_SUN_PLUS:
return "215"
if self.version == ChuniConstants.VER_CHUNITHM_LUMINOUS:
return "220"
async def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
# use UTC time and convert it to JST time by adding +9

View File

@ -48,9 +48,8 @@ class ChuniReader(BaseReader):
for root, dirs, files in walk(f"{root_dir}loginBonusPreset"):
for dir in dirs:
if path.exists(f"{root}/{dir}/LoginBonusPreset.xml"):
with open(f"{root}/{dir}/LoginBonusPreset.xml", "rb") as fp:
bytedata = fp.read()
strdata = bytedata.decode("UTF-8")
with open(f"{root}/{dir}/LoginBonusPreset.xml", "r", encoding="utf-8") as fp:
strdata = fp.read()
xml_root = ET.fromstring(strdata)
for name in xml_root.findall("name"):
@ -121,9 +120,8 @@ class ChuniReader(BaseReader):
for root, dirs, files in walk(evt_dir):
for dir in dirs:
if path.exists(f"{root}/{dir}/Event.xml"):
with open(f"{root}/{dir}/Event.xml", "rb") as fp:
bytedata = fp.read()
strdata = bytedata.decode("UTF-8")
with open(f"{root}/{dir}/Event.xml", "r", encoding="utf-8") as fp:
strdata = fp.read()
xml_root = ET.fromstring(strdata)
for name in xml_root.findall("name"):
@ -144,9 +142,8 @@ class ChuniReader(BaseReader):
for root, dirs, files in walk(music_dir):
for dir in dirs:
if path.exists(f"{root}/{dir}/Music.xml"):
with open(f"{root}/{dir}/Music.xml", "rb") as fp:
bytedata = fp.read()
strdata = bytedata.decode("UTF-8")
with open(f"{root}/{dir}/Music.xml", "r", encoding='utf-8') as fp:
strdata = fp.read()
xml_root = ET.fromstring(strdata)
for name in xml_root.findall("name"):
@ -210,9 +207,8 @@ class ChuniReader(BaseReader):
for root, dirs, files in walk(charge_dir):
for dir in dirs:
if path.exists(f"{root}/{dir}/ChargeItem.xml"):
with open(f"{root}/{dir}/ChargeItem.xml", "rb") as fp:
bytedata = fp.read()
strdata = bytedata.decode("UTF-8")
with open(f"{root}/{dir}/ChargeItem.xml", "r", encoding='utf-8') as fp:
strdata = fp.read()
xml_root = ET.fromstring(strdata)
for name in xml_root.findall("name"):
@ -240,9 +236,8 @@ class ChuniReader(BaseReader):
for root, dirs, files in walk(avatar_dir):
for dir in dirs:
if path.exists(f"{root}/{dir}/AvatarAccessory.xml"):
with open(f"{root}/{dir}/AvatarAccessory.xml", "rb") as fp:
bytedata = fp.read()
strdata = bytedata.decode("UTF-8")
with open(f"{root}/{dir}/AvatarAccessory.xml", "r", encoding='utf-8') as fp:
strdata = fp.read()
xml_root = ET.fromstring(strdata)
for name in xml_root.findall("name"):

View File

@ -243,6 +243,36 @@ matching = Table(
mysql_charset="utf8mb4",
)
cmission = Table(
"chuni_item_cmission",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("missionId", Integer, nullable=False),
Column("point", Integer),
UniqueConstraint("user", "missionId", name="chuni_item_cmission_uk"),
mysql_charset="utf8mb4",
)
cmission_progress = Table(
"chuni_item_cmission_progress",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column("missionId", Integer, nullable=False),
Column("order", Integer),
Column("stage", Integer),
Column("progress", Integer),
UniqueConstraint(
"user", "missionId", "order", name="chuni_item_cmission_progress_uk"
),
mysql_charset="utf8mb4",
)
class ChuniItemData(BaseData):
async def get_oldest_free_matching(self, version: int) -> Optional[Row]:
@ -594,3 +624,66 @@ class ChuniItemData(BaseData):
)
return None
return result.lastrowid
async def put_cmission_progress(
self, user_id: int, mission_id: int, progress_data: Dict
) -> Optional[int]:
progress_data["user"] = user_id
progress_data["missionId"] = mission_id
sql = insert(cmission_progress).values(**progress_data)
conflict = sql.on_duplicate_key_update(**progress_data)
result = await self.execute(conflict)
if result is None:
return None
return result.lastrowid
async def get_cmission_progress(
self, user_id: int, mission_id: int
) -> Optional[List[Row]]:
sql = cmission_progress.select(
and_(
cmission_progress.c.user == user_id,
cmission_progress.c.missionId == mission_id,
)
).order_by(cmission_progress.c.order.asc())
result = await self.execute(sql)
if result is None:
return None
return result.fetchall()
async def get_cmission(self, user_id: int, mission_id: int) -> Optional[Row]:
sql = cmission.select(
and_(cmission.c.user == user_id, cmission.c.missionId == mission_id)
)
result = await self.execute(sql)
if result is None:
return None
return result.fetchone()
async def put_cmission(self, user_id: int, mission_data: Dict) -> Optional[int]:
mission_data["user"] = user_id
sql = insert(cmission).values(**mission_data)
conflict = sql.on_duplicate_key_update(**mission_data)
result = await self.execute(conflict)
if result is None:
return None
return result.lastrowid
async def get_cmissions(self, user_id: int) -> Optional[List[Row]]:
sql = cmission.select(cmission.c.user == user_id)
result = await self.execute(sql)
if result is None:
return None
return result.fetchall()

View File

@ -414,6 +414,18 @@ rating = Table(
mysql_charset="utf8mb4",
)
net_battle = Table(
"chuni_profile_net_battle",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", Integer, ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, unique=True),
Column("isRankUpChallengeFailed", Boolean),
Column("highestBattleRankId", Integer),
Column("battleIconId", Integer),
Column("battleIconNum", Integer),
Column("avatarEffectPoint", Integer),
mysql_charset="utf8mb4",
)
class ChuniProfileData(BaseData):
async def update_name(self, user_id: int, new_name: str) -> bool:
@ -807,3 +819,31 @@ class ChuniProfileData(BaseData):
versions_raw = result.fetchall()
versions = [row[0] for row in versions_raw]
return sorted(versions, reverse=True)
async def put_net_battle(self, aime_id: int, net_battle_data: Dict) -> Optional[int]:
sql = insert(net_battle).values(
user=aime_id,
isRankUpChallengeFailed=net_battle_data['isRankUpChallengeFailed'],
highestBattleRankId=net_battle_data['highestBattleRankId'],
battleIconId=net_battle_data['battleIconId'],
battleIconNum=net_battle_data['battleIconNum'],
avatarEffectPoint=net_battle_data['avatarEffectPoint'],
)
conflict = sql.on_duplicate_key_update(
isRankUpChallengeFailed=net_battle_data['isRankUpChallengeFailed'],
highestBattleRankId=net_battle_data['highestBattleRankId'],
battleIconId=net_battle_data['battleIconId'],
battleIconNum=net_battle_data['battleIconNum'],
avatarEffectPoint=net_battle_data['avatarEffectPoint'],
)
result = await self.execute(conflict)
if result:
return result.inserted_primary_key['id']
self.logger.error(f"Failed to put net battle data for user {aime_id}")
async def get_net_battle(self, aime_id: int) -> Optional[Row]:
result = await self.execute(net_battle.select(net_battle.c.user == aime_id))
if result:
return result.fetchone()

View File

@ -242,6 +242,8 @@ class ChuniScoreData(BaseData):
# Calculates the ROM version that should be fetched for rankings, based on the game version being retrieved
# This prevents tracks that are not accessible in your version from counting towards the 10 results
romVer = {
15: "2.20%",
14: "2.15%",
13: "2.10%",
12: "2.05%",
11: "2.00%",

View File

@ -5,7 +5,7 @@
</style>
<div class="container">
{% include 'titles/chuni/templates/chuni_header.jinja' %}
{% if profile is defined and profile is not none and profile.id > 0 %}
{% if profile is defined and profile is not none and profile|length > 0 %}
<div class="row">
<div class="col-lg-8 m-auto mt-3">
<div class="card bg-card rounded">

View File

@ -40,6 +40,7 @@ class CxbRev(CxbBase):
score_data["slow2"],
score_data["fail"],
score_data["combo"],
score_data["grade"],
)
return {"data": True}
return {"data": True}

View File

@ -39,6 +39,7 @@ playlog = Table(
Column("slow2", Integer),
Column("fail", Integer),
Column("combo", Integer),
Column("grade", Integer),
Column("date_scored", TIMESTAMP, server_default=func.now()),
mysql_charset="utf8mb4",
)
@ -104,6 +105,7 @@ class CxbScoreData(BaseData):
this_slow2: int,
fail: int,
combo: int,
grade: int,
) -> Optional[int]:
"""
Add an entry to the user's play log
@ -123,6 +125,7 @@ class CxbScoreData(BaseData):
slow2=this_slow2,
fail=fail,
combo=combo,
grade=grade,
)
result = await self.execute(sql)

View File

@ -2,8 +2,10 @@ from titles.diva.index import DivaServlet
from titles.diva.const import DivaConstants
from titles.diva.database import DivaData
from titles.diva.read import DivaReader
from .frontend import DivaFrontend
index = DivaServlet
database = DivaData
reader = DivaReader
frontend = DivaFrontend
game_codes = [DivaConstants.GAME_CODE]

View File

@ -431,7 +431,7 @@ class DivaBase:
profile = await self.data.profile.get_profile(data["pd_id"], self.version)
profile_shop = await self.data.item.get_shop(data["pd_id"], self.version)
if profile is None:
return
return {}
mdl_have = "F" * 250
# generate the mdl_have string if "unlock_all_modules" is disabled

182
titles/diva/frontend.py Normal file
View File

@ -0,0 +1,182 @@
from typing import List
from starlette.routing import Route, Mount
from starlette.requests import Request
from starlette.responses import Response, RedirectResponse
from os import path
import yaml
import jinja2
from core.frontend import FE_Base, UserSession
from core.config import CoreConfig
from .database import DivaData
from .config import DivaConfig
from .const import DivaConstants
class DivaFrontend(FE_Base):
def __init__(
self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str
) -> None:
super().__init__(cfg, environment)
self.data = DivaData(cfg)
self.game_cfg = DivaConfig()
if path.exists(f"{cfg_dir}/{DivaConstants.CONFIG_NAME}"):
self.game_cfg.update(
yaml.safe_load(open(f"{cfg_dir}/{DivaConstants.CONFIG_NAME}"))
)
self.nav_name = "diva"
def get_routes(self) -> List[Route]:
return [
Route("/", self.render_GET, methods=['GET']),
Mount("/playlog", routes=[
Route("/", self.render_GET_playlog, methods=['GET']),
Route("/{index}", self.render_GET_playlog, methods=['GET']),
]),
Route("/update.name", self.update_name, methods=['POST']),
Route("/update.lv", self.update_lv, methods=['POST']),
]
async def render_GET(self, request: Request) -> bytes:
template = self.environment.get_template(
"titles/diva/templates/diva_index.jinja"
)
usr_sesh = self.validate_session(request)
if not usr_sesh:
usr_sesh = UserSession()
if usr_sesh.user_id > 0:
profile = await self.data.profile.get_profile(usr_sesh.user_id, 1)
resp = Response(template.render(
title=f"{self.core_config.server.name} | {self.nav_name}",
game_list=self.environment.globals["game_list"],
sesh=vars(usr_sesh),
user_id=usr_sesh.user_id,
profile=profile
), media_type="text/html; charset=utf-8")
return resp
else:
return RedirectResponse("/gate")
async def render_GET_playlog(self, request: Request) -> bytes:
template = self.environment.get_template(
"titles/diva/templates/diva_playlog.jinja"
)
usr_sesh = self.validate_session(request)
if not usr_sesh:
usr_sesh = UserSession()
if usr_sesh.user_id > 0:
path_index = request.path_params.get("index")
if not path_index or int(path_index) < 1:
index = 0
else:
index = int(path_index) - 1 # 0 and 1 are 1st page
user_id = usr_sesh.user_id
playlog_count = await self.data.score.get_user_playlogs_count(user_id)
if playlog_count < index * 20 :
return Response(template.render(
title=f"{self.core_config.server.name} | {self.nav_name}",
game_list=self.environment.globals["game_list"],
sesh=vars(usr_sesh),
score_count=0
), media_type="text/html; charset=utf-8")
playlog = await self.data.score.get_playlogs(user_id, index, 20) #Maybe change to the playlog instead of direct scores
playlog_with_title = []
for record in playlog:
song = await self.data.static.get_music_chart(record[2], record[3], record[4])
if song:
title = song.title
vocaloid_arranger = song.vocaloid_arranger
else:
title = "Unknown"
vocaloid_arranger = "Unknown"
playlog_with_title.append({
"raw": record,
"title": title,
"vocaloid_arranger": vocaloid_arranger
})
return Response(template.render(
title=f"{self.core_config.server.name} | {self.nav_name}",
game_list=self.environment.globals["game_list"],
sesh=vars(usr_sesh),
user_id=usr_sesh.user_id,
playlog=playlog_with_title,
playlog_count=playlog_count
), media_type="text/html; charset=utf-8")
else:
return RedirectResponse("/gate/", 300)
async def update_name(self, request: Request) -> Response:
usr_sesh = self.validate_session(request)
if not usr_sesh:
return RedirectResponse("/gate")
form_data = await request.form()
new_name: str = form_data.get("new_name")
new_name_full = ""
if not new_name:
return RedirectResponse("/gate/?e=4", 303)
if len(new_name) > 8:
return RedirectResponse("/gate/?e=8", 303)
for x in new_name: # FIXME: This will let some invalid characters through atm
o = ord(x)
try:
if o == 0x20:
new_name_full += chr(0x3000)
elif o < 0x7F and o > 0x20:
new_name_full += chr(o + 0xFEE0)
elif o <= 0x7F:
self.logger.warn(f"Invalid ascii character {o:02X}")
return RedirectResponse("/gate/?e=4", 303)
else:
new_name_full += x
except Exception as e:
self.logger.error(f"Something went wrong parsing character {o:04X} - {e}")
return RedirectResponse("/gate/?e=4", 303)
if not await self.data.profile.update_profile(usr_sesh.user_id, player_name=new_name_full):
return RedirectResponse("/gate/?e=999", 303)
return RedirectResponse("/game/diva", 303)
async def update_lv(self, request: Request) -> Response:
usr_sesh = self.validate_session(request)
if not usr_sesh:
return RedirectResponse("/gate")
form_data = await request.form()
new_lv: str = form_data.get("new_lv")
new_lv_full = ""
if not new_lv:
return RedirectResponse("/gate/?e=4", 303)
if len(new_lv) > 8:
return RedirectResponse("/gate/?e=8", 303)
for x in new_lv: # FIXME: This will let some invalid characters through atm
o = ord(x)
try:
if o == 0x20:
new_lv_full += chr(0x3000)
elif o < 0x7F and o > 0x20:
new_lv_full += chr(o + 0xFEE0)
elif o <= 0x7F:
self.logger.warn(f"Invalid ascii character {o:02X}")
return RedirectResponse("/gate/?e=4", 303)
else:
new_lv_full += x
except Exception as e:
self.logger.error(f"Something went wrong parsing character {o:04X} - {e}")
return RedirectResponse("/gate/?e=4", 303)
if not await self.data.profile.update_profile(usr_sesh.user_id, lv_str=new_lv_full):
return RedirectResponse("/gate/?e=999", 303)
return RedirectResponse("/game/diva", 303)

View File

@ -79,7 +79,7 @@ class DivaServlet(BaseServlet):
return True
async def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes:
async def render_POST(self, request: Request) -> bytes:
req_raw = await request.body()
url_header = request.headers
@ -98,9 +98,18 @@ class DivaServlet(BaseServlet):
self.logger.info(f"Binary {bin_req_data['cmd']} Request")
self.logger.debug(bin_req_data)
try:
handler = getattr(self.base, f"handle_{bin_req_data['cmd']}_request")
resp = handler(bin_req_data)
except AttributeError as e:
self.logger.warning(f"Unhandled {bin_req_data['cmd']} request {e}")
return PlainTextResponse(f"cmd={bin_req_data['cmd']}&req_id={bin_req_data['req_id']}&stat=ok")
except Exception as e:
self.logger.error(f"Error handling method {e}")
return PlainTextResponse(f"cmd={bin_req_data['cmd']}&req_id={bin_req_data['req_id']}&stat=ok")
self.logger.debug(
f"Response cmd={bin_req_data['cmd']}&req_id={bin_req_data['req_id']}&stat=ok{resp}"
)

View File

@ -183,7 +183,11 @@ class DivaReader(BaseReader):
pv_list[pv_id] = self.add_branch(pv_list[pv_id], key_args, val)
for pv_id, pv_data in pv_list.items():
try:
song_id = int(pv_id.split("_")[1])
except ValueError:
self.logger.error(f"Invalid song ID format: {pv_id}")
continue
if "songinfo" not in pv_data:
continue
if "illustrator" not in pv_data["songinfo"]:

View File

@ -54,7 +54,7 @@ class DivaCustomizeItemData(BaseData):
Given a game version and an aime id, return the cstmz_itm_have hex string
required for diva directly
"""
items_list = self.get_customize_items(aime_id, version)
items_list = await self.get_customize_items(aime_id, version)
if items_list is None:
items_list = []
item_have = 0

View File

@ -50,7 +50,7 @@ class DivaModuleData(BaseData):
Given a game version and an aime id, return the mdl_have hex string
required for diva directly
"""
module_list = self.get_modules(aime_id, version)
module_list = await self.get_modules(aime_id, version)
if module_list is None:
module_list = []
module_have = 0

View File

@ -90,7 +90,7 @@ class DivaProfileData(BaseData):
return None
return result.lastrowid
async def update_profile(self, aime_id: int, **profile_args) -> None:
async def update_profile(self, aime_id: int, **profile_args) -> bool:
"""
Given an aime_id update the profile corresponding to the arguments
which are the diva_profile Columns
@ -102,7 +102,9 @@ class DivaProfileData(BaseData):
self.logger.error(
f"update_profile: failed to update profile! profile: {aime_id}"
)
return None
return False
return True
async def get_profile(self, aime_id: int, version: int) -> Optional[List[Dict]]:
"""

View File

@ -239,3 +239,23 @@ class DivaScoreData(BaseData):
if result is None:
return None
return result.fetchall()
async def get_playlogs(self, aime_id: int, idx: int = 0, limit: int = 0) -> Optional[Row]:
sql = select(playlog).where(playlog.c.user == aime_id).order_by(playlog.c.date_scored.desc())
if limit:
sql = sql.limit(limit)
if idx:
sql = sql.offset(idx)
result = await self.execute(sql)
if result:
return result.fetchall()
async def get_user_playlogs_count(self, aime_id: int) -> Optional[int]:
sql = select(func.count()).where(playlog.c.user == aime_id)
result = await self.execute(sql)
if result is None:
self.logger.warning(f"aimu_id {aime_id} has no scores ")
return None
return result.scalar()

View File

@ -0,0 +1,195 @@
.diva-header {
text-align: center;
}
ul.diva-navi {
list-style-type: none;
padding: 0;
overflow: hidden;
background-color: #333;
text-align: center;
display: inline-block;
}
ul.diva-navi li {
display: inline-block;
}
ul.diva-navi li a {
display: block;
color: white;
text-align: center;
padding: 14px 16px;
text-decoration: none;
}
ul.diva-navi li a:hover:not(.active) {
background-color: #111;
}
ul.diva-navi li a.active {
background-color: #4CAF50;
}
ul.diva-navi li.right {
float: right;
}
@media screen and (max-width: 600px) {
ul.diva-navi li.right,
ul.diva-navi li {
float: none;
display: block;
text-align: center;
}
}
table {
border-collapse: collapse;
border-spacing: 0;
border-collapse: separate;
overflow: hidden;
background-color: #555555;
}
th, td {
text-align: left;
border: none;
}
th {
color: white;
}
.table-rowdistinct tr:nth-child(even) {
background-color: #303030;
}
.table-rowdistinct tr:nth-child(odd) {
background-color: #555555;
}
caption {
text-align: center;
color: white;
font-size: 18px;
font-weight: bold;
}
.table-large {
margin: 16px;
}
.table-large th,
.table-large td {
padding: 8px;
}
.table-small {
width: 100%;
margin: 4px;
}
.table-small th,
.table-small td {
padding: 2px;
}
.bg-card {
background-color: #555555;
}
.card-hover {
transition: all 0.2s ease-in-out;
}
.card-hover:hover {
transform: scale(1.02);
}
.basic {
color: #28a745;
font-weight: bold;
}
.hard {
color: #ffc107;
font-weight: bold;
}
.expert {
color: #dc3545;
font-weight: bold;
}
.master {
color: #dd09e8;
font-weight: bold;
}
.ultimate {
color: #000000;
font-weight: bold;
}
.score {
color: #ffffff;
font-weight: bold;
}
.rainbow {
background: linear-gradient(to right, red, yellow, lime, aqua, blue, fuchsia) 0 / 5em;
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: bold;
}
.platinum {
color: #FFFF00;
font-weight: bold;
}
.gold {
color: #FFFF00;
font-weight: bold;
}
.scrolling-text {
overflow: hidden;
}
.scrolling-text p {
white-space: nowrap;
display: inline-block;
}
.scrolling-text h6 {
white-space: nowrap;
display: inline-block;
}
.scrolling-text h5 {
white-space: nowrap;
display: inline-block;
}
.scrolling {
animation: scroll 10s linear infinite;
}
@keyframes scroll {
0% {
transform: translateX(100%);
}
100% {
transform: translateX(-100%);
}
}

View File

@ -0,0 +1,17 @@
<div class="diva-header">
<h1>diva</h1>
<ul class="diva-navi">
<li><a class="nav-link" href="/game/diva/">PROFILE</a></li>
<li><a class="nav-link" href="/game/diva/playlog/">RECORD</a></li>
</ul>
</div>
<script>
$(document).ready(function () {
var currentPath = window.location.pathname;
if (currentPath === '/game/diva/') {
$('.nav-link[href="/game/diva/"]').addClass('active');
} else if (currentPath.startsWith('/game/diva/playlog/')) {
$('.nav-link[href="/game/diva/playlog/"]').addClass('active');
}
});
</script>

View File

@ -0,0 +1,111 @@
{% extends "core/templates/index.jinja" %}
{% block content %}
<style>
{% include 'titles/diva/templates/css/diva_style.css' %}
</style>
<div class="container">
{% include 'titles/diva/templates/diva_header.jinja' %}
{% if profile is defined and profile is not none and profile|length > 0 %}
<div class="row">
<div class="col-lg-8 m-auto mt-3">
<div class="card bg-card rounded">
<table class="table-large table-rowdistinct">
<caption align="top" class="text-center">OVERVIEW</caption>
<tr>
<th>Player name:</th>
<th>{{ profile[3] }}</th>
<th>
<button type="button" class="btn btn-primary btn-sm" data-bs-toggle="modal"
data-bs-target="#name_change">Edit</button>
</th>
<th>Level string:</th>
<th>{{ profile[4] }}</th>
<th>
<button type="button" class="btn btn-primary btn-sm" data-bs-toggle="modal"
data-bs-target="#lv_change">Edit</button>
</th>
</tr>
<tr>
<td>Lvl:</td>
<td>{{ profile[5] }}</td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Lvl points:</td>
<td>{{ profile[6] }}</td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Vocaloid points:</td>
<td>{{ profile[7] }}</td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</table>
</div>
</div>
</div>
{% if error is defined %}
{% include "core/templates/widgets/err_banner.jinja" %}
{% endif %}
{% elif sesh is defined and sesh is not none and sesh.user_id > 0 %}
No profile information found for this account.
{% else %}
Login to view profile information.
{% endif %}
</div>
<div class="modal fade" id="name_change" tabindex="-1" aria-labelledby="name_change_label" data-bs-theme="dark"
aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Name change</h5>
</div>
<div class="modal-body">
<form id="new_name_form" action="/game/diva/update.name" method="post" style="outline: 0;">
<label class="form-label" for="new_name">new name:</label>
<input class="form-control" aria-describedby="newNameHelp" form="new_name_form" id="new_name"
name="new_name" maxlength="14" type="text" required>
<div id="newNameHelp" class="form-text">name must be full-width character string.
</div>
</form>
</div>
<div class="modal-footer">
<input type=submit class="btn btn-primary" type="button" form="new_name_form">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="lv_change" tabindex="-1" aria-labelledby="lv_change_label" data-bs-theme="dark"
aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Level string change</h5>
</div>
<div class="modal-body">
<form id="new_lv_form" action="/game/diva/update.lv" method="post" style="outline: 0;">
<label class="form-label" for="new_lv">new level string:</label>
<input class="form-control" aria-describedby="newLvHelp" form="new_lv_form" id="new_lv" name="new_lv"
maxlength="14" type="text" required>
<div id="newLvHelp" class="form-text">level string must be full-width character string.
</div>
</form>
<div class="modal-footer">
<input type=submit class="btn btn-primary" type="button" form="new_lv_form">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,169 @@
{% extends "core/templates/index.jinja" %}
{% block content %}
<style>
{% include 'titles/diva/templates/css/diva_style.css' %}
</style>
<div class="container">
{% include 'titles/diva/templates/diva_header.jinja' %}
{% if playlog is defined and playlog is not none %}
<div class="row">
<h4 style="text-align: center;">Score counts: {{ playlog_count }}</h4>
{% set difficultyName = ['easy', 'normal', 'hard', 'extreme', 'extra extreme'] %}
{% set clearState = ['MISSxTAKE', 'STANDARD', 'GREAT', 'EXELLENT', 'PERFECT'] %}
{% for record in playlog %}
<div class="col-lg-6 mt-3">
<div class="card bg-card rounded card-hover">
<div class="card bg-card rounded card-hover">
<div class="card-header row">
<div class="col-8 scrolling-text">
<h5 class="card-text">{{ record.title }}</h5>
<br>
<h6 class="card-text">{{ record.vocaloid_arranger }}</h6>
</div>
<div class="col-4">
<h6 class="card-text">{{record.raw.date_scored}}</h6>
</div>
</div>
<div class="card-body row">
<div class="col-3" style="text-align: center;">
<h4 class="card-text">{{ record.raw.score }}</h4>
<h2>{{ record.raw.atn_pnt / 100 }}%</h2>
<h6>{{ difficultyName[record.raw.difficulty] }}</h6>
</div>
<div class="col-6" style="text-align: center;">
<table class="table-small table-rowdistinc">
<tr>
<td>COOL</td>
<td>{{ record.raw.cool }}</td>
</tr>
<tr>
<td>FINE</td>
<td>{{ record.raw.fine }}</td>
</tr>
<tr>
<td>SAFE</td>
<td>{{ record.raw.safe }}</td>
</tr>
<tr>
<td>SAD</td>
<td>{{ record.raw.sad }}</td>
</tr>
<tr>
<td>WORST</td>
<td>{{ record.raw.worst }}</td>
</tr>
</table>
</div>
<div class="col-3" style="text-align: center;">
<h6>{{ record.raw.max_combo }}</h6>
{% if record.raw.clr_kind == -1 %}
<h6>{{ clearState[0] }}</h6>
{% elif record.raw.clr_kind == 2 %}
<h6>{{ clearState[1] }}</h6>
{% elif record.raw.clr_kind == 3 %}
<h6>{{ clearState[2] }}</h6>
{% elif record.raw.clr_kind == 4 %}
<h6>{{ clearState[3] }}</h6>
{% elif record.raw.clr_kind == 5 %}
<h6>{{ clearState[4] }}</h6>
{% endif %}
{% if record.raw.clr_kind == -1 %}
<h6>NOT CLEAR</h6>
{% else %}
<h6>CLEAR</h6>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% set playlog_pages = playlog_count // 20 + 1 %}
{% elif sesh is defined and sesh is not none and sesh.user_id > 0 %}
No Score information found for this account.
{% else %}
Login to view profile information.
{% endif %}
</div>
<footer class="navbar-fixed-bottom">
<nav aria-label="Score page navication">
<ul class="pagination justify-content-center mt-3">
<li class="page-item"><a id="prev_page" class="page-link" href="#">Previous</a></li>
<li class="page-item"><a id="first_page" class="page-link" href="/game/diva/playlog/">1</a></li>
<li class="page-item"><a id="prev_3_page" class="page-link" href="">...</a></li>
<li class="page-item"><a id="front_page" class="page-link" href="">2</a></li>
<li class="page-item"><a id="cur_page" class="page-link active" href="">3</a></li>
<li class="page-item"><a id="back_page" class="page-link" href="">4</a></li>
<li class="page-item"><a id="next_3_page" class="page-link" href="">...</a></li>
<li class="page-item"><a id="last_page" class="page-link" href="/game/diva/playlog/{{ playlog_pages }}">{{
playlog_pages }}</a></li>
<li class="page-item"><a id="next_page" class="page-link" href="#">Next</a></li>
&nbsp
</ul>
</nav>
</footer>
<script>
$(document).ready(function () {
$('.scrolling-text p, .scrolling-text h1, .scrolling-text h2, .scrolling-text h3, .scrolling-text h4, .scrolling-text h5, .scrolling-text h6').each(function () {
var parentWidth = $(this).parent().width();
var elementWidth = $(this).outerWidth();
var elementWidthWithPadding = $(this).outerWidth(true);
if (elementWidthWithPadding > parentWidth) {
$(this).addClass('scrolling');
}
});
var currentUrl = window.location.pathname;
var currentPage = parseInt(currentUrl.split('/').pop());
var rootUrl = '/game/diva/playlog/';
var scorePages = {{ playlog_pages }};
if (Number.isNaN(currentPage)) {
currentPage = 1;
}
$('#cur_page').text(currentPage);
$('#prev_page').attr('href', rootUrl + (currentPage - 1))
$('#next_page').attr('href', rootUrl + (currentPage + 1))
$('#front_page').attr('href', rootUrl + (currentPage - 1))
$('#front_page').text(currentPage - 1);
$('#back_page').attr('href', rootUrl + (currentPage + 1))
$('#back_page').text(currentPage + 1);
$('#prev_3_page').attr('href', rootUrl + (currentPage - 3))
$('#next_3_page').attr('href', rootUrl + (currentPage + 3))
if ((currentPage - 1) < 3) {
$('#prev_3_page').hide();
if ((currentPage - 1) < 2) {
$('#front_page').hide();
if (currentPage === 1) {
$('#first_page').hide();
$('#prev_page').addClass('disabled');
}
}
}
if ((scorePages - currentPage) < 3) {
$('#next_3_page').hide();
if ((scorePages - currentPage) < 2) {
$('#back_page').hide();
if (currentPage === scorePages) {
$('#last_page').hide();
$('#next_page').addClass('disabled');
}
}
}
$('#go_button').click(function () {
var pageNumber = parseInt($('#page_input').val());
if (!Number.isNaN(pageNumber) && pageNumber <= scorePages && pageNumber >= 0) {
var url = '/game/diva/playlog/' + pageNumber;
window.location.href = url;
} else {
$('#page_input').val('');
$('#page_input').attr('placeholder', 'invalid input!');
}
});
});
</script>
{% endblock content %}

View File

@ -2,10 +2,12 @@ from titles.mai2.index import Mai2Servlet
from titles.mai2.const import Mai2Constants
from titles.mai2.database import Mai2Data
from titles.mai2.read import Mai2Reader
from .frontend import Mai2Frontend
index = Mai2Servlet
database = Mai2Data
reader = Mai2Reader
frontend = Mai2Frontend
game_codes = [
Mai2Constants.GAME_CODE_DX,
Mai2Constants.GAME_CODE_FINALE,

View File

@ -4,6 +4,7 @@ import logging
from base64 import b64decode
from os import path, stat, remove
from PIL import ImageFile
from random import randint
import pytz
from core.config import CoreConfig
@ -470,7 +471,27 @@ class Mai2Base:
}
async def handle_get_user_present_api_request(self, data: Dict) -> Dict:
return { "userId": data.get("userId", 0), "length": 0, "userPresentList": []}
items: List[Dict[str, Any]] = []
user_pres_list = await self.data.item.get_presents_by_version_user(self.version, data["userId"])
if user_pres_list:
self.logger.debug(f"Found {len(user_pres_list)} possible presents")
for present in user_pres_list:
if (present['startDate'] and present['startDate'].timestamp() > datetime.now().timestamp()):
self.logger.debug(f"Present {present['id']} distribution hasn't started yet (begins {present['startDate']})")
continue # present period hasn't started yet, move onto the next one
if (present['endDate'] and present['endDate'].timestamp() < datetime.now().timestamp()):
self.logger.warn(f"Present {present['id']} ended on {present['endDate']} and should be removed")
continue # present period ended, move onto the next one
test = await self.data.item.get_item(data["userId"], present['itemKind'], present['itemId'])
if not test: # Don't send presents for items the user already has
pres_id = present['itemKind'] * 1000000
pres_id += present['itemId']
items.append({"itemId": pres_id, "itemKind": 4, "stock": present['stock'], "isValid": True})
self.logger.info(f"Give user {data['userId']} {present['stock']}x item {present['itemId']} (kind {present['itemKind']}) as present")
return { "userId": data.get("userId", 0), "length": len(items), "userPresentList": items}
async def handle_get_transfer_friend_api_request(self, data: Dict) -> Dict:
return {}
@ -886,3 +907,45 @@ class Mai2Base:
self.logger.error(f"Failed to delete {out_name}.bin, please remove it manually")
return {'returnCode': ret_code, 'apiName': 'UploadUserPhotoApi'}
async def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict:
user_id = data.get("userId", 0)
kind = data.get("kind", 0) # 1 is fav music, 2 is rival user IDs
next_index = data.get("nextIndex", 0)
max_ct = data.get("maxCount", 100) # always 100
is_all = data.get("isAllFavoriteItem", False) # always false
id_list: List[Dict] = []
if user_id:
if kind == 1:
fav_music = await self.data.item.get_fav_music(user_id)
if fav_music:
for fav in fav_music:
id_list.append({"orderId": 0, "id": fav["musicId"]})
if len(id_list) >= 100: # Lazy but whatever
break
elif kind == 2:
rivals = await self.data.profile.get_rivals_game(user_id)
if rivals:
for rival in rivals:
id_list.append({"orderId": 0, "id": rival["rival"]})
return {
"userId": user_id,
"kind": kind,
"nextIndex": 0,
"userFavoriteItemList": id_list,
}
async def handle_get_user_recommend_rate_music_api_request(self, data: Dict) -> Dict:
"""
userRecommendRateMusicIdList: list[int]
"""
return {"userId": data["userId"], "userRecommendRateMusicIdList": []}
async def handle_get_user_recommend_select_music_api_request(self, data: Dict) -> Dict:
"""
userRecommendSelectionMusicIdList: list[int]
"""
return {"userId": data["userId"], "userRecommendSelectionMusicIdList": []}

View File

@ -17,16 +17,3 @@ class Mai2Buddies(Mai2FestivalPlus):
# hardcode lastDataVersion for CardMaker
user_data["lastDataVersion"] = "1.40.00"
return user_data
async def handle_get_user_new_item_api_request(self, data: Dict) -> Dict:
# TODO: Added in 1.41, implement this?
user_id = data["userId"]
version = data.get("version", 1041000)
user_playlog_list = data.get("userPlaylogList", [])
return {
"userId": user_id,
"itemKind": -1,
"itemId": -1,
}

View File

@ -196,10 +196,17 @@ class Mai2DX(Mai2Base):
if "userItemList" in upsert and len(upsert["userItemList"]) > 0:
for item in upsert["userItemList"]:
if item["itemKind"] == 4:
item_id = item["itemId"] % 1000000
item_kind = item["itemId"] // 1000000
else:
item_id = item["itemId"]
item_kind = item["itemKind"]
await self.data.item.put_item(
user_id,
int(item["itemKind"]),
item["itemId"],
item_kind,
item_id,
item["stock"],
item["isValid"],
)
@ -325,11 +332,32 @@ class Mai2DX(Mai2Base):
}
async def handle_get_user_item_api_request(self, data: Dict) -> Dict:
kind = int(data["nextIndex"] / 10000000000)
next_idx = int(data["nextIndex"] % 10000000000)
user_item_list = await self.data.item.get_items(data["userId"], kind)
kind = data["nextIndex"] // 10000000000
next_idx = data["nextIndex"] % 10000000000
items: List[Dict[str, Any]] = []
if kind == 4: # presents
user_pres_list = await self.data.item.get_presents_by_version_user(self.version, data["userId"])
if user_pres_list:
self.logger.debug(f"Found {len(user_pres_list)} possible presents")
for present in user_pres_list:
if (present['startDate'] and present['startDate'].timestamp() > datetime.now().timestamp()):
self.logger.debug(f"Present {present['id']} distribution hasn't started yet (begins {present['startDate']})")
continue # present period hasn't started yet, move onto the next one
if (present['endDate'] and present['endDate'].timestamp() < datetime.now().timestamp()):
self.logger.warn(f"Present {present['id']} ended on {present['endDate']} and should be removed")
continue # present period ended, move onto the next one
test = await self.data.item.get_item(data["userId"], present['itemKind'], present['itemId'])
if not test: # Don't send presents for items the user already has
pres_id = present['itemKind'] * 1000000
pres_id += present['itemId']
items.append({"itemId": pres_id, "itemKind": 4, "stock": present['stock'], "isValid": True})
self.logger.info(f"Give user {data['userId']} {present['stock']}x item {present['itemId']} (kind {present['itemKind']}) as present")
else:
user_item_list = await self.data.item.get_items(data["userId"], kind)
for i in range(next_idx, len(user_item_list)):
tmp = user_item_list[i]._asdict()
tmp.pop("user")
@ -563,33 +591,76 @@ class Mai2DX(Mai2Base):
return {"userId": data["userId"], "length": 0, "userRegionList": []}
async def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict:
user_id = data["userId"]
rival_id = data["rivalId"]
user_id = data.get("userId", 0)
rival_id = data.get("rivalId", 0)
"""
class UserRivalData:
rivalId: int
rivalName: str
"""
return {"userId": user_id, "userRivalData": {}}
if not user_id or not rival_id: return {}
rival_pf = await self.data.profile.get_profile_detail(rival_id, self.version)
if not rival_pf: return {}
return {
"userId": user_id,
"userRivalData": {
"rivalId": rival_id,
"rivalName": rival_pf['userName']
}
}
async def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict:
user_id = data.get("userId", 0)
rival_id = data.get("rivalId", 0)
next_index = data.get("nextIndex", 0)
max_ct = 100
upper_lim = next_index + max_ct
rival_music_list: Dict[int, List] = {}
songs = await self.data.score.get_best_scores(rival_id)
if songs is None:
self.logger.debug("handle_get_user_rival_music_api_request: get_best_scores returned None!")
return {
"userId": user_id,
"rivalId": rival_id,
"nextIndex": 0,
"userRivalMusicList": [] # musicId userRivalMusicDetailList -> level achievement deluxscoreMax
}
num_user_songs = len(songs)
for x in range(next_index, upper_lim):
if x >= num_user_songs:
break
tmp = songs[x]._asdict()
if tmp['musicId'] in rival_music_list:
rival_music_list[tmp['musicId']].append([{"level": tmp['level'], 'achievement': tmp['achievement'], 'deluxscoreMax': tmp['deluxscoreMax']}])
else:
if len(rival_music_list) >= max_ct:
break
rival_music_list[tmp['musicId']] = [{"level": tmp['level'], 'achievement': tmp['achievement'], 'deluxscoreMax': tmp['deluxscoreMax']}]
next_index = 0 if len(rival_music_list) < max_ct or num_user_songs == upper_lim else upper_lim
self.logger.info(f"Send rival {rival_id} songs {next_index}-{upper_lim} ({len(rival_music_list)}) out of {num_user_songs} for user {user_id} (next idx {next_index})")
return {
"userId": user_id,
"rivalId": rival_id,
"nextIndex": next_index,
"userRivalMusicList": [{"musicId": x, "userRivalMusicDetailList": y} for x, y in rival_music_list.items()]
}
async def handle_get_user_new_item_api_request(self, data: Dict) -> Dict:
# TODO: Added in 1.41, implement this?
user_id = data["userId"]
rival_id = data["rivalId"]
next_idx = data["nextIndex"]
rival_music_levels = data["userRivalMusicLevelList"]
version = data.get("version", 1041000)
user_playlog_list = data.get("userPlaylogList", [])
"""
class UserRivalMusicList:
class UserRivalMusicDetailList:
level: int
achievement: int
deluxscoreMax: int
musicId: int
userRivalMusicDetailList: list[UserRivalMusicDetailList]
"""
return {"userId": user_id, "nextIndex": 0, "userRivalMusicList": []}
return {
"userId": user_id,
"itemKind": -1,
"itemId": -1,
}
async def handle_get_user_music_api_request(self, data: Dict) -> Dict:
user_id = data.get("userId", 0)
@ -636,3 +707,208 @@ class Mai2DX(Mai2Base):
return ret
ret['loginId'] = ret.get('loginCount', 0)
return ret
# CardMaker support added in Universe
async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
p = await 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
"lastDataVersion": "1.20.00", # Future versiohs should replace this with the correct version
# checks if the user is still logged in
"isLogin": False,
"isExistSellingCard": True,
}
async def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict:
# user already exists, because the preview checks that already
p = await self.data.profile.get_profile_detail(data["userId"], self.version)
cards = await 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}
async def handle_cm_login_api_request(self, data: Dict) -> Dict:
return {"returnCode": 1}
async def handle_cm_logout_api_request(self, data: Dict) -> Dict:
return {"returnCode": 1}
async def handle_cm_get_selling_card_api_request(self, data: Dict) -> Dict:
selling_cards = await 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"], Mai2Constants.DATE_TIME_FORMAT
)
tmp["endDate"] = datetime.strftime(
tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT
)
tmp["noticeStartDate"] = datetime.strftime(
tmp["noticeStartDate"], Mai2Constants.DATE_TIME_FORMAT
)
tmp["noticeEndDate"] = datetime.strftime(
tmp["noticeEndDate"], Mai2Constants.DATE_TIME_FORMAT
)
selling_card_list.append(tmp)
return {"length": len(selling_card_list), "sellingCardList": selling_card_list}
async def handle_cm_get_user_card_api_request(self, data: Dict) -> Dict:
user_cards = await 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],
}
async def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict:
await self.handle_get_user_item_api_request(data)
async def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict:
characters = await 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,
}
async def handle_cm_get_user_card_print_error_api_request(self, data: Dict) -> Dict:
return {"length": 0, "userPrintDetailList": []}
async 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"]
await 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 = await 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
await 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")
await 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),
}
async def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict:
return {
"returnCode": 1,
"orderId": 0,
"serialId": data["userPrintlog"]["serialId"],
}
async def handle_cm_upsert_buy_card_api_request(self, data: Dict) -> Dict:
return {"returnCode": 1}

View File

@ -20,18 +20,6 @@ class Mai2Festival(Mai2UniversePlus):
async def handle_user_login_api_request(self, data: Dict) -> Dict:
user_login = await super().handle_user_login_api_request(data)
# useless?
# TODO: Make use of this
user_login["Bearer"] = "ARTEMiSTOKEN"
return user_login
async def handle_get_user_recommend_rate_music_api_request(self, data: Dict) -> Dict:
"""
userRecommendRateMusicIdList: list[int]
"""
return {"userId": data["userId"], "userRecommendRateMusicIdList": []}
async def handle_get_user_recommend_select_music_api_request(self, data: Dict) -> Dict:
"""
userRecommendSelectionMusicIdList: list[int]
"""
return {"userId": data["userId"], "userRecommendSelectionMusicIdList": []}

View File

@ -17,22 +17,3 @@ class Mai2FestivalPlus(Mai2Festival):
# hardcode lastDataVersion for CardMaker
user_data["lastDataVersion"] = "1.35.00"
return user_data
async def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict:
user_id = data.get("userId", 0)
kind = data.get("kind", 2)
next_index = data.get("nextIndex", 0)
max_ct = data.get("maxCount", 100)
is_all = data.get("isAllFavoriteItem", False)
"""
class userFavoriteItemList:
orderId: int
id: int
"""
return {
"userId": user_id,
"kind": kind,
"nextIndex": 0,
"userFavoriteItemList": [],
}

190
titles/mai2/frontend.py Normal file
View File

@ -0,0 +1,190 @@
from typing import List
from starlette.routing import Route, Mount
from starlette.requests import Request
from starlette.responses import Response, RedirectResponse
from os import path
import yaml
import jinja2
from core.frontend import FE_Base, UserSession
from core.config import CoreConfig
from .database import Mai2Data
from .config import Mai2Config
from .const import Mai2Constants
class Mai2Frontend(FE_Base):
def __init__(
self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str
) -> None:
super().__init__(cfg, environment)
self.data = Mai2Data(cfg)
self.game_cfg = Mai2Config()
if path.exists(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"):
self.game_cfg.update(
yaml.safe_load(open(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"))
)
self.nav_name = "maimai"
def get_routes(self) -> List[Route]:
return [
Route("/", self.render_GET, methods=['GET']),
Mount("/playlog", routes=[
Route("/", self.render_GET_playlog, methods=['GET']),
Route("/{index}", self.render_GET_playlog, methods=['GET']),
]),
Route("/update.name", self.update_name, methods=['POST']),
Route("/version.change", self.version_change, methods=['POST']),
]
async def render_GET(self, request: Request) -> bytes:
template = self.environment.get_template(
"titles/mai2/templates/mai2_index.jinja"
)
usr_sesh = self.validate_session(request)
if not usr_sesh:
usr_sesh = UserSession()
if usr_sesh.user_id > 0:
versions = await self.data.profile.get_all_profile_versions(usr_sesh.user_id)
profile = []
if versions:
# maimai_version is -1 means it is not initialized yet, select a default version from existing.
if usr_sesh.maimai_version < 0:
usr_sesh.maimai_version = versions[0]['version']
profile = await self.data.profile.get_profile_detail(usr_sesh.user_id, usr_sesh.maimai_version)
versions = [x['version'] for x in versions]
resp = Response(template.render(
title=f"{self.core_config.server.name} | {self.nav_name}",
game_list=self.environment.globals["game_list"],
sesh=vars(usr_sesh),
user_id=usr_sesh.user_id,
profile=profile,
version_list=Mai2Constants.VERSION_STRING,
versions=versions,
cur_version=usr_sesh.maimai_version
), media_type="text/html; charset=utf-8")
if usr_sesh.maimai_version >= 0:
encoded_sesh = self.encode_session(usr_sesh)
resp.delete_cookie("ARTEMIS_SESH")
resp.set_cookie("ARTEMIS_SESH", encoded_sesh)
return resp
else:
return RedirectResponse("/gate/", 303)
async def render_GET_playlog(self, request: Request) -> bytes:
template = self.environment.get_template(
"titles/mai2/templates/mai2_playlog.jinja"
)
usr_sesh = self.validate_session(request)
if not usr_sesh:
print("wtf")
usr_sesh = UserSession()
if usr_sesh.user_id > 0:
if usr_sesh.maimai_version < 0:
print(usr_sesh.maimai_version)
return RedirectResponse("/game/mai2/", 303)
path_index = request.path_params.get('index')
if not path_index or int(path_index) < 1:
index = 0
else:
index = int(path_index) - 1 # 0 and 1 are 1st page
user_id = usr_sesh.user_id
playlog_count = await self.data.score.get_user_playlogs_count(user_id)
if playlog_count < index * 20 :
return Response(template.render(
title=f"{self.core_config.server.name} | {self.nav_name}",
game_list=self.environment.globals["game_list"],
sesh=vars(usr_sesh),
playlog_count=0
), media_type="text/html; charset=utf-8")
playlog = await self.data.score.get_playlogs(user_id, index, 20)
playlog_with_title = []
for record in playlog:
music_chart = await self.data.static.get_music_chart(usr_sesh.maimai_version, record.musicId, record.level)
if music_chart:
difficultyNum=music_chart.chartId
difficulty=music_chart.difficulty
artist=music_chart.artist
title=music_chart.title
else:
difficultyNum=0
difficulty=0
artist="unknown"
title="musicid: " + str(record.musicId)
playlog_with_title.append({
"raw": record,
"title": title,
"difficultyNum": difficultyNum,
"difficulty": difficulty,
"artist": artist,
})
return Response(template.render(
title=f"{self.core_config.server.name} | {self.nav_name}",
game_list=self.environment.globals["game_list"],
sesh=vars(usr_sesh),
user_id=usr_sesh.user_id,
playlog=playlog_with_title,
playlog_count=playlog_count
), media_type="text/html; charset=utf-8")
else:
return RedirectResponse("/gate/", 303)
async def update_name(self, request: Request) -> bytes:
usr_sesh = self.validate_session(request)
if not usr_sesh:
return RedirectResponse("/gate/", 303)
form_data = await request.form()
new_name: str = form_data.get("new_name")
new_name_full = ""
if not new_name:
return RedirectResponse("/gate/?e=4", 303)
if len(new_name) > 8:
return RedirectResponse("/gate/?e=8", 303)
for x in new_name: # FIXME: This will let some invalid characters through atm
o = ord(x)
try:
if o == 0x20:
new_name_full += chr(0x3000)
elif o < 0x7F and o > 0x20:
new_name_full += chr(o + 0xFEE0)
elif o <= 0x7F:
self.logger.warn(f"Invalid ascii character {o:02X}")
return RedirectResponse("/gate/?e=4", 303)
else:
new_name_full += x
except Exception as e:
self.logger.error(f"Something went wrong parsing character {o:04X} - {e}")
return RedirectResponse("/gate/?e=4", 303)
if not await self.data.profile.update_name(usr_sesh.user_id, new_name_full):
return RedirectResponse("/gate/?e=999", 303)
return RedirectResponse("/game/mai2/?s=1", 303)
async def version_change(self, request: Request):
usr_sesh = self.validate_session(request)
if not usr_sesh:
usr_sesh = UserSession()
if usr_sesh.user_id > 0:
form_data = await request.form()
maimai_version = form_data.get("version")
self.logger.info(f"version change to: {maimai_version}")
if(maimai_version.isdigit()):
usr_sesh.maimai_version=int(maimai_version)
encoded_sesh = self.encode_session(usr_sesh)
self.logger.info(f"Created session with JWT {encoded_sesh}")
resp = RedirectResponse("/game/mai2/", 303)
resp.set_cookie("ARTEMIS_SESH", encoded_sesh)
return resp
else:
return RedirectResponse("/gate/", 303)

View File

@ -2,8 +2,8 @@ from core.data.schema import BaseData, metadata
from datetime import datetime
from typing import Optional, Dict, List
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, or_
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, BIGINT, INTEGER
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select
from sqlalchemy.dialects.mysql import insert
@ -134,6 +134,20 @@ favorite = Table(
mysql_charset="utf8mb4",
)
fav_music = Table(
"mai2_item_favorite_music",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("musicId", Integer, nullable=False),
UniqueConstraint("user", "musicId", name="mai2_item_favorite_music_uk"),
mysql_charset="utf8mb4",
)
charge = Table(
"mai2_item_charge",
metadata,
@ -184,6 +198,20 @@ print_detail = Table(
mysql_charset="utf8mb4",
)
present = Table(
"mai2_item_present",
metadata,
Column('id', BIGINT, primary_key=True, nullable=False),
Column('version', INTEGER),
Column("user", Integer, ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
Column("itemKind", INTEGER, nullable=False),
Column("itemId", INTEGER, nullable=False),
Column("stock", INTEGER, nullable=False, server_default="1"),
Column("startDate", TIMESTAMP),
Column("endDate", TIMESTAMP),
UniqueConstraint("version", "user", "itemKind", "itemId", name="mai2_item_present_uk"),
mysql_charset="utf8mb4",
)
class Mai2ItemData(BaseData):
async def put_item(
@ -451,6 +479,30 @@ class Mai2ItemData(BaseData):
return None
return result.fetchall()
async def get_fav_music(self, user_id: int) -> Optional[List[Row]]:
result = await self.execute(fav_music.select(fav_music.c.user == user_id))
if result:
return result.fetchall()
async def add_fav_music(self, user_id: int, music_id: int) -> Optional[int]:
sql = insert(fav_music).values(
user = user_id,
musicId = music_id
)
conflict = sql.on_duplicate_key_update(musicId = music_id)
result = await self.execute(conflict)
if result:
return result.lastrowid
self.logger.error(f"Failed to add music {music_id} as favorite for user {user_id}!")
async def remove_fav_music(self, user_id: int, music_id: int) -> None:
result = await self.execute(fav_music.delete(and_(fav_music.c.user == user_id, fav_music.c.musicId == music_id)))
if not result:
self.logger.error(f"Failed to remove music {music_id} as favorite for user {user_id}!")
async def put_card(
self,
user_id: int,
@ -548,3 +600,49 @@ class Mai2ItemData(BaseData):
)
return None
return result.lastrowid
async def put_present(self, item_kind: int, item_id: int, version: int = None, user_id: int = None, start_date: datetime = None, end_date: datetime = None) -> Optional[int]:
sql = insert(present).values(
version = version,
user = user_id,
itemKind = item_kind,
itemId = item_id,
startDate = start_date,
endDate = end_date
)
conflict = sql.on_duplicate_key_update(
startDate = start_date,
endDate = end_date
)
result = await self.execute(conflict)
if result:
return result.lastrowid
self.logger.error(f"Failed to add present item {item_id}!")
async def get_presents_by_user(self, user_id: int = None) -> Optional[List[Row]]:
result = await self.execute(present.select(or_(present.c.user == user_id, present.c.user is None)))
if result:
return result.fetchall()
async def get_presents_by_version(self, ver: int = None) -> Optional[List[Row]]:
result = await self.execute(present.select(or_(present.c.version == ver, present.c.version is None)))
if result:
return result.fetchall()
async def get_presents_by_version_user(self, ver: int = None, user_id: int = None) -> Optional[List[Row]]:
result = await self.execute(present.select(
and_(
or_(present.c.user == user_id, present.c.user == None),
or_(present.c.version == ver, present.c.version == None)
)
))
if result:
return result.fetchall()
async def get_present_by_id(self, present_id: int) -> Optional[Row]:
result = await self.execute(present.select(present.c.id == present_id))
if result:
return result.fetchone()

View File

@ -491,8 +491,31 @@ consec_logins = Table(
mysql_charset="utf8mb4",
)
rival = Table(
"mai2_user_rival",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column(
"rival",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("show", Boolean, nullable=False, server_default="0"),
UniqueConstraint("user", "rival", name="mai2_user_rival_uk"),
mysql_charset="utf8mb4",
)
class Mai2ProfileData(BaseData):
async def get_all_profile_versions(self, user_id: int) -> Optional[List[Row]]:
result = await self.execute(detail.select(detail.c.user == user_id))
if result:
return result.fetchall()
async def put_profile_detail(
self, user_id: int, version: int, detail_data: Dict, is_dx: bool = True
) -> Optional[Row]:
@ -843,3 +866,52 @@ class Mai2ProfileData(BaseData):
if result is None:
return None
return result.fetchone()
async def get_rivals(self, user_id: int) -> Optional[List[Row]]:
result = await self.execute(rival.select(rival.c.user == user_id))
if result:
return result.fetchall()
async def get_rivals_game(self, user_id: int) -> Optional[List[Row]]:
result = await self.execute(rival.select(and_(rival.c.user == user_id, rival.c.show == True)).limit(3))
if result:
return result.fetchall()
async def set_rival_shown(self, user_id: int, rival_id: int, is_shown: bool) -> None:
sql = rival.update(and_(rival.c.user == user_id, rival.c.rival == rival_id)).values(
show = is_shown
)
result = await self.execute(sql)
if not result:
self.logger.error(f"Failed to set rival {rival_id} shown status to {is_shown} for user {user_id}")
async def add_rival(self, user_id: int, rival_id: int) -> Optional[int]:
sql = insert(rival).values(
user = user_id,
rival = rival_id
)
conflict = sql.on_duplicate_key_update(rival = rival_id)
result = await self.execute(conflict)
if result:
return result.lastrowid
self.logger.error(f"Failed to add music {rival_id} as favorite for user {user_id}!")
async def remove_rival(self, user_id: int, rival_id: int) -> None:
result = await self.execute(rival.delete(and_(rival.c.user == user_id, rival.c.rival == rival_id)))
if not result:
self.logger.error(f"Failed to remove rival {rival_id} for user {user_id}!")
async def update_name(self, user_id: int, new_name: str) -> bool:
sql = detail.update(detail.c.user == user_id).values(
userName=new_name
)
result = await self.execute(sql)
if result is None:
self.logger.warning(f"Failed to set user {user_id} name to {new_name}")
return False
return True

View File

@ -319,16 +319,16 @@ class Mai2ScoreData(BaseData):
sql = best_score.select(
and_(
best_score.c.user == user_id,
(best_score.c.song_id == song_id) if song_id is not None else True,
)
(best_score.c.musicId == song_id) if song_id is not None else True,
)
).order_by(best_score.c.musicId).order_by(best_score.c.level)
else:
sql = best_score_old.select(
and_(
best_score_old.c.user == user_id,
(best_score_old.c.song_id == song_id) if song_id is not None else True,
)
(best_score_old.c.musicId == song_id) if song_id is not None else True,
)
).order_by(best_score.c.musicId).order_by(best_score.c.level)
result = await self.execute(sql)
if result is None:
@ -398,3 +398,23 @@ class Mai2ScoreData(BaseData):
if result is None:
return None
return result.fetchall()
async def get_playlogs(self, user_id: int, idx: int = 0, limit: int = 0) -> Optional[List[Row]]:
sql = playlog.select(playlog.c.user == user_id)
if limit:
sql = sql.limit(limit)
if idx:
sql = sql.offset(idx * limit)
result = await self.execute(sql)
if result:
return result.fetchall()
async def get_user_playlogs_count(self, aime_id: int) -> Optional[Row]:
sql = select(func.count()).where(playlog.c.user == aime_id)
result = await self.execute(sql)
if result is None:
self.logger.warning(f"aime_id {aime_id} has no playlog ")
return None
return result.scalar()

View File

@ -0,0 +1,195 @@
.mai2-header {
text-align: center;
}
ul.mai2-navi {
list-style-type: none;
padding: 0;
overflow: hidden;
background-color: #333;
text-align: center;
display: inline-block;
}
ul.mai2-navi li {
display: inline-block;
}
ul.mai2-navi li a {
display: block;
color: white;
text-align: center;
padding: 14px 16px;
text-decoration: none;
}
ul.mai2-navi li a:hover:not(.active) {
background-color: #111;
}
ul.mai2-navi li a.active {
background-color: #4CAF50;
}
ul.mai2-navi li.right {
float: right;
}
@media screen and (max-width: 600px) {
ul.mai2-navi li.right,
ul.mai2-navi li {
float: none;
display: block;
text-align: center;
}
}
table {
border-collapse: collapse;
border-spacing: 0;
border-collapse: separate;
overflow: hidden;
background-color: #555555;
}
th, td {
text-align: left;
border: none;
}
th {
color: white;
}
.table-rowdistinct tr:nth-child(even) {
background-color: #303030;
}
.table-rowdistinct tr:nth-child(odd) {
background-color: #555555;
}
caption {
text-align: center;
color: white;
font-size: 18px;
font-weight: bold;
}
.table-large {
margin: 16px;
}
.table-large th,
.table-large td {
padding: 8px;
}
.table-small {
width: 100%;
margin: 4px;
}
.table-small th,
.table-small td {
padding: 2px;
}
.bg-card {
background-color: #555555;
}
.card-hover {
transition: all 0.2s ease-in-out;
}
.card-hover:hover {
transform: scale(1.02);
}
.basic {
color: #28a745;
font-weight: bold;
}
.hard {
color: #ffc107;
font-weight: bold;
}
.expert {
color: #dc3545;
font-weight: bold;
}
.master {
color: #dd09e8;
font-weight: bold;
}
.ultimate {
color: #000000;
font-weight: bold;
}
.score {
color: #ffffff;
font-weight: bold;
}
.rainbow {
background: linear-gradient(to right, red, yellow, lime, aqua, blue, fuchsia) 0 / 5em;
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: bold;
}
.platinum {
color: #FFFF00;
font-weight: bold;
}
.gold {
color: #FFFF00;
font-weight: bold;
}
.scrolling-text {
overflow: hidden;
}
.scrolling-text p {
white-space: nowrap;
display: inline-block;
}
.scrolling-text h6 {
white-space: nowrap;
display: inline-block;
}
.scrolling-text h5 {
white-space: nowrap;
display: inline-block;
}
.scrolling {
animation: scroll 10s linear infinite;
}
@keyframes scroll {
0% {
transform: translateX(100%);
}
100% {
transform: translateX(-100%);
}
}

View File

@ -0,0 +1,17 @@
<div class="mai2-header">
<h1>maimai</h1>
<ul class="mai2-navi">
<li><a class="nav-link" href="/game/mai2/">PROFILE</a></li>
<li><a class="nav-link" href="/game/mai2/playlog/">RECORD</a></li>
</ul>
</div>
<script>
$(document).ready(function () {
var currentPath = window.location.pathname;
if (currentPath === '/game/mai2/') {
$('.nav-link[href="/game/mai2/"]').addClass('active');
} else if (currentPath.startsWith('/game/mai2/playlog/')) {
$('.nav-link[href="/game/mai2/playlog/"]').addClass('active');
}
});
</script>

View File

@ -0,0 +1,134 @@
{% extends "core/templates/index.jinja" %}
{% block content %}
<style>
{% include 'titles/mai2/templates/css/mai2_style.css' %}
</style>
<div class="container">
{% include 'titles/mai2/templates/mai2_header.jinja' %}
{% if profile is defined and profile is not none and profile|length > 0 %}
<div class="row">
<div class="col-lg-8 m-auto mt-3">
<div class="card bg-card rounded">
<table class="table-large table-rowdistinct">
<caption align="top">OVERVIEW</caption>
<tr>
<th>{{ profile.userName }}</th>
<th>
<button type="button" class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#name_change">Edit</button>
</th>
</tr>
<tr>
<td>version:</td>
<td>
<select name="version" id="version" onChange="changeVersion(this)">
{% for ver in versions %}
{% if ver == cur_version %}
<option value="{{ ver }}" selected>{{ version_list[ver] }}</option>
{% else %}
<option value="{{ ver }}">{{ version_list[ver] }}</option>
{% endif %}
{% endfor %}
</select>
{% if versions | length > 1 %}
<p style="margin-block-end: 0;">You have {{ versions | length }} versions.</p>
{% endif %}
</td>
</tr>
<tr>
<td>Rating:</td>
<td>
<span class="{% if profile.playerRating >= 15000 %}rainbow{% elif profile.playerRating < 15000 and profile.playerRating >= 14500 %}platinum{% elif profile.playerRating < 14500 and profile.playerRating >=14000 %}platinum{% endif %}">
{{ profile.playerRating }}
</span>
<span>
(highest: {{ profile.highestRating }})
</span>
</td>
</tr>
<tr>
<td>Play Counts:</td>
<td>{{ profile.playCount }}</td>
</tr>
<tr>
<td>Last Play Date:</td>
<td>{{ profile.lastPlayDate }}</td>
</tr>
</table>
</div>
</div>
<div class="col-lg-8 m-auto mt-3">
<div class="card bg-card rounded">
<table class="table-large table-rowdistinct">
<caption align="top">SCORE</caption>
<tr>
<td>Total Delux Score:</td>
<td>{{ profile.totalDeluxscore }}</td>
</tr>
<tr>
<td>Total Basic Delux Score:</td>
<td>{{ profile.totalBasicDeluxscore }}</td>
</tr>
<tr>
<td>Total Advanced Delux Score:</td>
<td>{{ profile.totalAdvancedDeluxscore }}</td>
</tr>
<tr>
<td>Total Expert Delux Score:</td>
<td>{{ profile.totalExpertDeluxscore }}</td>
</tr>
<tr>
<td>Total Master Delux Score:</td>
<td>{{ profile.totalMasterDeluxscore }}</td>
</tr>
<tr>
<td>Total ReMaster Delux Score:</td>
<td>{{ profile.totalReMasterDeluxscore }}</td>
</tr>
</table>
</div>
</div>
</div>
{% if error is defined %}
{% include "core/templates/widgets/err_banner.jinja" %}
{% endif %}
{% elif sesh is defined and sesh is not none and sesh.user_id > 0 %}
No profile information found for this account.
{% else %}
Login to view profile information.
{% endif %}
</div>
<div class="modal fade" id="name_change" tabindex="-1" aria-labelledby="name_change_label" data-bs-theme="dark"
aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Name change</h5>
</div>
<div class="modal-body">
<form id="new_name_form" action="/game/mai2/update.name" method="post" style="outline: 0;">
<label class="form-label" for="new_name">new name:</label>
<input class="form-control" aria-describedby="newNameHelp" form="new_name_form" id="new_name"
name="new_name" maxlength="14" type="text" required>
<div id="newNameHelp" class="form-text">name must be full-width character string.
</div>
</form>
</div>
<div class="modal-footer">
<input type=submit class="btn btn-primary" type="button" form="new_name_form">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<script>
function changeVersion(sel) {
$.post("/game/mai2/version.change", { version: sel.value })
.done(function (data) {
location.reload();
})
.fail(function () {
alert("Failed to update version.");
});
}
</script>
{% endblock content %}

View File

@ -0,0 +1,225 @@
{% extends "core/templates/index.jinja" %}
{% block content %}
<style>
{% include 'titles/mai2/templates/css/mai2_style.css' %}
</style>
<div class="container">
{% include 'titles/mai2/templates/mai2_header.jinja' %}
{% if playlog is defined and playlog is not none %}
<div class="row">
<h4 style="text-align: center;">Playlog counts: {{ playlog_count }}</h4>
{% set rankName = ['D', 'C', 'B', 'BB', 'BBB', 'A', 'AA', 'AAA', 'S', 'S+', 'SS', 'SS+', 'SSS', 'SSS+'] %}
{% set difficultyName = ['basic', 'hard', 'expert', 'master', 'ultimate'] %}
{% for record in playlog %}
<div class="col-lg-6 mt-3">
<div class="card bg-card rounded card-hover">
<div class="card-header row">
<div class="col-8 scrolling-text">
<h5 class="card-text"> {{ record.title }} </h5>
<br>
<h6 class="card-text"> {{ record.artist }} </h6>
</div>
<div class="col-4">
<h6 class="card-text">{{ record.raw.userPlayDate }}</h6>
<h6 class="card-text">TRACK {{ record.raw.trackNo }}</h6>
</div>
</div>
<div class="card-body row">
<div class="col-3" style="text-align: center;">
<h4 class="card-text">{{ record.raw.deluxscore }}</h4>
<h2>{{ rankName[record.raw.rank] }}</h2>
<h6
class="{% if record.raw.level == 0 %}basic{% elif record.raw.level == 1 %}advanced{% elif record.raw.level == 2 %}expert{% elif record.raw.level == 3 %}master{% elif record.raw.level == 4 %}remaster{% endif %}">
{{ difficultyName[record.raw.level] }}&nbsp&nbsp{{ record.difficulty }}
</h6>
</div>
<div class="col-6" style="text-align: center;">
<table class="table-small table-rowdistinc">
<tr>
<td>CRITICAL PERFECT</td>
<td>
Tap: {{ record.raw.tapCriticalPerfect }}<br>
Hold: {{ record.raw.holdCriticalPerfect }}<br>
Slide: {{ record.raw.slideCriticalPerfect }}<br>
Touch: {{ record.raw.touchCriticalPerfect }}<br>
Break: {{ record.raw.breakCriticalPerfect }}
</td>
</tr>
<tr>
<td>PERFECT</td>
<td>
Tap: {{ record.raw.tapPerfect }}<br>
Hold: {{ record.raw.holdPerfect }}<br>
Slide: {{ record.raw.slidePerfect }}<br>
Touch: {{ record.raw.touchPerfect }}<br>
Break: {{ record.raw.breakPerfect }}
</td>
</tr>
<tr>
<td>GREAT</td>
<td>
Tap: {{ record.raw.tapGreat }}<br>
Hold: {{ record.raw.holdGreat }}<br>
Slide: {{ record.raw.slideGreat }}<br>
Touch: {{ record.raw.touchGreat }}<br>
Break: {{ record.raw.breakGreat }}
</td>
</tr>
<tr>
<td>GOOD</td>
<td>
Tap: {{ record.raw.tapGood }}<br>
Hold: {{ record.raw.holdGood }}<br>
Slide: {{ record.raw.slideGood }}<br>
Touch: {{ record.raw.touchGood }}<br>
Break: {{ record.raw.breakGood }}
</td>
</tr>
<tr>
<td>MISS</td>
<td>
Tap: {{ record.raw.tapMiss }}<br>
Hold: {{ record.raw.holdMiss }}<br>
Slide: {{ record.raw.slideMiss }}<br>
Touch: {{ record.raw.touchMiss }}<br>
Break: {{ record.raw.breakMiss }}
</td>
</tr>
</table>
</div>
<div class="col-3" style="text-align: center;">
{%if record.raw.comboStatus == 1 %}
<h6>FULL COMBO</h6>
{% endif %}
{%if record.raw.comboStatus == 2 %}
<h6>FULL COMBO +</h6>
{% endif %}
{%if record.raw.comboStatus == 3 %}
<h6>ALL PERFECT</h6>
{% endif %}
{%if record.raw.comboStatus == 4 %}
<h6>ALL PERFECT +</h6>
{% endif %}
{%if record.raw.syncStatus == 1 %}
<h6>FULL SYNC</h6>
{% endif %}
{%if record.raw.syncStatus == 2 %}
<h6>FULL SYNC +</h6>
{% endif %}
{%if record.raw.syncStatus == 3 %}
<h6>FULL SYNC DX</h6>
{% endif %}
{%if record.raw.syncStatus == 4 %}
<h6>FULL SYNC DX +</h6>
{% endif %}
{%if record.raw.isAchieveNewRecord == 1 or record.raw.isDeluxscoreNewRecord == 1 %}
<h6>NEW RECORD</h6>
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% set playlog_pages = playlog_count // 20 + 1 %}
{% elif sesh is defined and sesh is not none and sesh.user_id > 0 %}
No Playlog information found for this account.
{% else %}
Login to view profile information.
{% endif %}
</div>
<footer class="navbar-fixed-bottom">
<nav aria-label="Playlog page navigation">
<ul class="pagination justify-content-center mt-3">
<li class="page-item"><a id="prev_page" class="page-link" href="#">Previous</a></li>
<li class="page-item"><a id="first_page" class="page-link" href="/game/mai2/playlog/">1</a></li>
<li class="page-item"><a id="prev_3_page" class="page-link" href="">...</a></li>
<li class="page-item"><a id="front_page" class="page-link" href="">2</a></li>
<li class="page-item"><a id="cur_page" class="page-link active" href="">3</a></li>
<li class="page-item"><a id="back_page" class="page-link" href="">4</a></li>
<li class="page-item"><a id="next_3_page" class="page-link" href="">...</a></li>
<li class="page-item"><a id="last_page" class="page-link" href="/game/mai2/playlog/{{ playlog_pages }}">{{
playlog_pages }}</a></li>
<li class="page-item"><a id="next_page" class="page-link" href="#">Next</a></li>
&nbsp
</ul>
</nav>
<div class="row">
<div class="col-5"></div>
<div class="col-2">
<div class="input-group rounded">
<input id="page_input" type="text" class="form-control" placeholder="go to page">
<span class="input-group-btn">
<button id="go_button" class="btn btn-light" type="button">
Go!
</button>
</span>
</div>
</div>
<div class="col-5"></div>
</div>
</footer>
<script>
$(document).ready(function () {
$('.scrolling-text p, .scrolling-text h1, .scrolling-text h2, .scrolling-text h3, .scrolling-text h4, .scrolling-text h5, .scrolling-text h6').each(function () {
var parentWidth = $(this).parent().width();
var elementWidth = $(this).outerWidth();
var elementWidthWithPadding = $(this).outerWidth(true);
if (elementWidthWithPadding > parentWidth) {
$(this).addClass('scrolling');
}
});
var currentUrl = window.location.pathname;
var currentPage = parseInt(currentUrl.split('/').pop());
var rootUrl = '/game/mai2/playlog/';
var playlogPages = {{ playlog_pages }};
if (Number.isNaN(currentPage)) {
currentPage = 1;
}
$('#cur_page').text(currentPage);
$('#prev_page').attr('href', rootUrl + (currentPage - 1))
$('#next_page').attr('href', rootUrl + (currentPage + 1))
$('#front_page').attr('href', rootUrl + (currentPage - 1))
$('#front_page').text(currentPage - 1);
$('#back_page').attr('href', rootUrl + (currentPage + 1))
$('#back_page').text(currentPage + 1);
$('#prev_3_page').attr('href', rootUrl + (currentPage - 3))
$('#next_3_page').attr('href', rootUrl + (currentPage + 3))
if ((currentPage - 1) < 3) {
$('#prev_3_page').hide();
if ((currentPage - 1) < 2) {
$('#front_page').hide();
if (currentPage === 1) {
$('#first_page').hide();
$('#prev_page').addClass('disabled');
}
}
}
if ((playlogPages - currentPage) < 3) {
$('#next_3_page').hide();
if ((playlogPages - currentPage) < 2) {
$('#back_page').hide();
if (currentPage === playlogPages) {
$('#last_page').hide();
$('#next_page').addClass('disabled');
}
}
}
$('#go_button').click(function () {
var pageNumber = parseInt($('#page_input').val());
if (!Number.isNaN(pageNumber) && pageNumber <= playlogPages && pageNumber >= 0) {
var url = '/game/mai2/playlog/' + pageNumber;
window.location.href = url;
} else {
$('#page_input').val('');
$('#page_input').attr('placeholder', 'invalid input!');
}
});
});
</script>
{% endblock content %}

View File

@ -1,8 +1,6 @@
from typing import Any, List, Dict
from random import randint
from datetime import datetime, timedelta
import pytz
import json
from core.config import CoreConfig
from titles.mai2.splashplus import Mai2SplashPlus
@ -14,207 +12,3 @@ class Mai2Universe(Mai2SplashPlus):
def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None:
super().__init__(cfg, game_cfg)
self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE
async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
p = await 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
"lastDataVersion": "1.20.00",
# checks if the user is still logged in
"isLogin": False,
"isExistSellingCard": True,
}
async def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict:
# user already exists, because the preview checks that already
p = await self.data.profile.get_profile_detail(data["userId"], self.version)
cards = await 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}
async def handle_cm_login_api_request(self, data: Dict) -> Dict:
return {"returnCode": 1}
async def handle_cm_logout_api_request(self, data: Dict) -> Dict:
return {"returnCode": 1}
async def handle_cm_get_selling_card_api_request(self, data: Dict) -> Dict:
selling_cards = await 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"], Mai2Constants.DATE_TIME_FORMAT
)
tmp["endDate"] = datetime.strftime(
tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT
)
tmp["noticeStartDate"] = datetime.strftime(
tmp["noticeStartDate"], Mai2Constants.DATE_TIME_FORMAT
)
tmp["noticeEndDate"] = datetime.strftime(
tmp["noticeEndDate"], Mai2Constants.DATE_TIME_FORMAT
)
selling_card_list.append(tmp)
return {"length": len(selling_card_list), "sellingCardList": selling_card_list}
async def handle_cm_get_user_card_api_request(self, data: Dict) -> Dict:
user_cards = await 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],
}
async def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict:
await super().handle_get_user_item_api_request(data)
async def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict:
characters = await 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,
}
async def handle_cm_get_user_card_print_error_api_request(self, data: Dict) -> Dict:
return {"length": 0, "userPrintDetailList": []}
async 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"]
await 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 = await 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
await 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")
await 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),
}
async def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict:
return {
"returnCode": 1,
"orderId": 0,
"serialId": data["userPrintlog"]["serialId"],
}
async def handle_cm_upsert_buy_card_api_request(self, data: Dict) -> Dict:
return {"returnCode": 1}

View File

@ -31,7 +31,8 @@ class OngekiFrontend(FE_Base):
def get_routes(self) -> List[Route]:
return [
Route("/", self.render_GET)
Route("/", self.render_GET),
Route("/version.change", self.render_POST, methods=['POST'])
]
async def render_GET(self, request: Request) -> bytes:
@ -69,29 +70,34 @@ class OngekiFrontend(FE_Base):
return RedirectResponse("/gate/", 303)
async def render_POST(self, request: Request):
uri = request.uri.decode()
uri = request.url.path
frm = await request.form()
usr_sesh = self.validate_session(request)
if not usr_sesh:
usr_sesh = UserSession()
if usr_sesh.user_id > 0:
if uri == "/game/ongeki/rival.add":
rival_id = request.args[b"rivalUserId"][0].decode()
rival_id = frm.get("rivalUserId")
await self.data.profile.put_rival(usr_sesh.user_id, rival_id)
# self.logger.info(f"{usr_sesh.user_id} added a rival")
return RedirectResponse(b"/game/ongeki/", 303)
elif uri == "/game/ongeki/rival.delete":
rival_id = request.args[b"rivalUserId"][0].decode()
rival_id = frm.get("rivalUserId")
await self.data.profile.delete_rival(usr_sesh.user_id, rival_id)
# self.logger.info(f"{response}")
return RedirectResponse(b"/game/ongeki/", 303)
elif uri == "/game/ongeki/version.change":
ongeki_version=request.args[b"version"][0].decode()
ongeki_version=frm.get("version")
if(ongeki_version.isdigit()):
usr_sesh.ongeki_version=int(ongeki_version)
return RedirectResponse("/game/ongeki/", 303)
enc = self.encode_session(usr_sesh)
resp = RedirectResponse("/game/ongeki/", 303)
resp.delete_cookie('ARTEMIS_SESH')
resp.set_cookie('ARTEMIS_SESH', enc)
return resp
else:
Response("Something went wrong", status_code=500)

View File

@ -216,6 +216,10 @@ class OngekiServlet(BaseServlet):
)
return Response(zlib.compress(b'{"stat": "0"}'))
if version < 105:
# O.N.G.E.K.I base don't use zlib
req_data = json.loads(req_raw)
else:
try:
unzip = zlib.decompress(req_raw)
@ -251,9 +255,12 @@ class OngekiServlet(BaseServlet):
self.logger.debug(f"Response {resp}")
zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8"))
resp_raw = json.dumps(resp, ensure_ascii=False).encode("utf-8")
zipped = zlib.compress(resp_raw)
if not encrtped or version < 120:
if version < 105:
return Response(resp_raw)
return Response(zipped)
padded = pad(zipped, 16)

View File

@ -30,7 +30,7 @@ score_best = Table(
Column("isFullCombo", Boolean, nullable=False),
Column("isAllBreake", Boolean, nullable=False),
Column("isLock", Boolean, nullable=False),
Column("clearStatus", Boolean, nullable=False),
Column("clearStatus", Integer, nullable=False),
Column("isStoryWatched", Boolean, nullable=False),
Column("platinumScoreMax", Integer),
UniqueConstraint("user", "musicId", "level", name="ongeki_best_score_uk"),

View File

@ -1,9 +1,7 @@
from datetime import datetime, timedelta
from datetime import datetime
import json, logging
from typing import Any, Dict, List
import random
from core.data import Data
from core import CoreConfig
from .config import PokkenConfig
from .proto import jackal_pb2
@ -18,7 +16,6 @@ class PokkenBase:
self.version = 0
self.logger = logging.getLogger("pokken")
self.data = PokkenData(core_cfg)
self.SUPPORT_SET_NONE = 4294967295
async def handle_noop(self, request: Any) -> bytes:
res = jackal_pb2.Response()
@ -38,7 +35,30 @@ class PokkenBase:
res = jackal_pb2.Response()
res.result = 1
res.type = jackal_pb2.MessageType.REGISTER_PCB
self.logger.info(f"Register PCB {request.register_pcb.pcb_id}")
pcbid = request.register_pcb.pcb_id
if not pcbid.isdigit() or len(pcbid) != 12 or \
not pcbid.startswith(f"{PokkenConstants.SERIAL_IDENT[0]}{PokkenConstants.SERIAL_REGIONS[0]}{PokkenConstants.SERIAL_ROLES[0]}{PokkenConstants.SERIAL_CAB_IDENTS[0]}"):
self.logger.warn(f"Bad PCBID {pcbid}")
res.result = 0
return res
netid = PokkenConstants.NETID_PREFIX[0] + pcbid[5:]
self.logger.info(f"Register PCB {pcbid} (netID {netid})")
minfo = await self.data.arcade.get_machine(netid)
if not minfo and not self.core_cfg.server.allow_unregistered_serials:
self.logger.warn(f"netID {netid} does not belong to any shop!")
res.result = 0
return res
elif not minfo:
self.logger.warn(f"Orphaned netID {netid} allowed to connect")
locid = 0
else:
locid = minfo['arcade']
regist_pcb = jackal_pb2.RegisterPcbResponseData()
regist_pcb.server_time = int(datetime.now().timestamp())
@ -46,7 +66,7 @@ class PokkenBase:
"MatchingServer": {
"host": f"https://{self.game_cfg.server.hostname}",
"port": self.game_cfg.ports.game,
"url": "/SDAK/100/matching",
"url": "/pokken/matching",
},
"StunServer": {
"addr": self.game_cfg.server.stun_server_host,
@ -56,10 +76,10 @@ class PokkenBase:
"addr": self.game_cfg.server.stun_server_host,
"port": self.game_cfg.server.stun_server_port,
},
"AdmissionUrl": f"ws://{self.game_cfg.server.hostname}:{self.game_cfg.ports.admission}",
"locationId": 123, # FIXME: Get arcade's ID from the database
"logfilename": "JackalMatchingLibrary.log",
"biwalogfilename": "./biwa.log",
"AdmissionUrl": f"ws://{self.game_cfg.server.hostname}:{self.game_cfg.ports.admission}/pokken/admission",
"locationId": locid,
"logfilename": "J:\\JackalMatchingLibrary.log",
"biwalogfilename": "J:\\biwa_log.log",
}
regist_pcb.bnp_baseuri = f"{self.core_cfg.server.hostname}/bna"
regist_pcb.biwa_setting = json.dumps(biwa_setting)
@ -95,12 +115,11 @@ class PokkenBase:
res.type = jackal_pb2.MessageType.LOAD_CLIENT_SETTINGS
settings = jackal_pb2.LoadClientSettingsResponseData()
# TODO: Make configurable
settings.money_magnification = 1
settings.continue_bonus_exp = 100
settings.continue_fight_money = 100
settings.event_bonus_exp = 100
settings.level_cap = 999
settings.level_cap = 100
settings.op_movie_flag = 0xFFFFFFFF
settings.lucky_bonus_rate = 1
settings.fail_support_num = 10
@ -132,9 +151,13 @@ class PokkenBase:
res.type = jackal_pb2.MessageType.LOAD_USER
access_code = request.load_user.access_code
load_usr = jackal_pb2.LoadUserResponseData()
user_id = await self.data.card.get_user_id_from_card(access_code)
load_usr.load_hash = 1
load_usr.access_code = access_code
load_usr.precedent_release_flag = 0xFFFFFFFF
load_usr.cardlock_status = False
card = await self.data.card.get_card_by_access_code(access_code)
if user_id is None and self.game_cfg.server.auto_register:
if card is None and self.game_cfg.server.auto_register:
user_id = await self.data.user.create_user()
card_id = await self.data.card.create_card(user_id, access_code)
@ -142,54 +165,39 @@ class PokkenBase:
f"Register new card {access_code} (UserId {user_id}, CardId {card_id})"
)
elif user_id is None:
if int(request.load_user.chip_id[:8], 16) != 0x04030201:
await self.data.card.set_chip_id_by_access_code(access_code, int(request.load_user.chip_id[:8], 16))
elif card is None:
self.logger.info(f"Registration of card {access_code} blocked!")
res.load_user.CopyFrom(load_usr)
return res.SerializeToString()
else:
user_id = card['user']
card_id = card['id']
if not card['chip_id'] and int(request.load_user.chip_id[:8], 16) != 0x04030201: # Default segatools sn:
await self.data.card.set_chip_id_by_access_code(access_code, int(request.load_user.chip_id[:8], 16))
"""
TODO: Add repeated values
tutorial_progress_flag
rankmatch_progress
TODO: Unlock all supports? Probably
support_pokemon_list
support_set_1
support_set_2
support_set_3
aid_skill_list
achievement_flag
event_achievement_flag
event_achievement_param
"""
profile = await self.data.profile.get_profile(user_id)
load_usr.commidserv_result = 1
load_usr.load_hash = 1
load_usr.cardlock_status = False
load_usr.banapass_id = user_id
load_usr.access_code = access_code
load_usr.precedent_release_flag = 0xFFFFFFFF
if profile is None:
if profile is None or profile['trainer_name'] is None:
profile_id = await self.data.profile.create_profile(user_id)
self.logger.info(f"Create new profile {profile_id} for user {user_id}")
profile_dict = {"id": profile_id, "user": user_id}
pokemon_data = []
tutorial_progress = []
rankmatch_progress = []
achievement_flag = []
event_achievement_flag = []
event_achievement_param = []
load_usr.new_card_flag = True
else:
profile_dict = {k: v for k, v in profile._asdict().items() if v is not None}
self.logger.info(
f"Card-in user {user_id} (Trainer name {profile_dict.get('trainer_name', '')})"
)
self.logger.info(f"Card-in user {user_id} (Trainer name {profile_dict.get('trainer_name', '')})")
pokemon_data = await self.data.profile.get_all_pokemon_data(user_id)
tutorial_progress = []
rankmatch_progress = []
achievement_flag = []
event_achievement_flag = []
event_achievement_param = []
load_usr.new_card_flag = False
load_usr.navi_newbie_flag = profile_dict.get("navi_newbie_flag", True)
@ -201,9 +209,9 @@ class PokkenBase:
load_usr.trainer_name = profile_dict.get(
"trainer_name", f"Newb{str(user_id).zfill(4)}"
)
load_usr.trainer_rank_point = profile_dict.get("trainer_rank_point", 0)
load_usr.wallet = profile_dict.get("wallet", 0)
load_usr.fight_money = profile_dict.get("fight_money", 0)
load_usr.trainer_rank_point = profile_dict.get("trainer_rank_point", 0) # determines rank
load_usr.wallet = profile_dict.get("wallet", 0) # pg count
load_usr.fight_money = profile_dict.get("fight_money", 0) # ?
load_usr.score_point = profile_dict.get("score_point", 0)
load_usr.grade_max_num = profile_dict.get("grade_max_num", 0)
load_usr.extra_counter = profile_dict.get("extra_counter", 0)
@ -218,18 +226,18 @@ class PokkenBase:
load_usr.rank_event = profile_dict.get("rank_event", 0)
load_usr.awake_num = profile_dict.get("awake_num", 0)
load_usr.use_support_num = profile_dict.get("use_support_num", 0)
load_usr.rankmatch_flag = profile_dict.get("rankmatch_flag", 0)
load_usr.rankmatch_flag = profile_dict.get("rankmatch_flag", 0) # flags that next rank match will be rank up
load_usr.rankmatch_max = profile_dict.get("rankmatch_max", 0)
load_usr.rankmatch_success = profile_dict.get("rankmatch_success", 0)
load_usr.beat_num = profile_dict.get("beat_num", 0)
load_usr.title_text_id = profile_dict.get("title_text_id", 0)
load_usr.title_plate_id = profile_dict.get("title_plate_id", 0)
load_usr.title_decoration_id = profile_dict.get("title_decoration_id", 0)
load_usr.title_text_id = profile_dict.get("title_text_id", 2)
load_usr.title_plate_id = profile_dict.get("title_plate_id", 31)
load_usr.title_decoration_id = profile_dict.get("title_decoration_id", 1)
load_usr.navi_trainer = profile_dict.get("navi_trainer", 0)
load_usr.navi_version_id = profile_dict.get("navi_version_id", 0)
load_usr.aid_skill = profile_dict.get("aid_skill", 0)
load_usr.comment_text_id = profile_dict.get("comment_text_id", 0)
load_usr.comment_word_id = profile_dict.get("comment_word_id", 0)
load_usr.comment_text_id = profile_dict.get("comment_text_id", 1)
load_usr.comment_word_id = profile_dict.get("comment_word_id", 1)
load_usr.latest_use_pokemon = profile_dict.get("latest_use_pokemon", 0)
load_usr.ex_ko_num = profile_dict.get("ex_ko_num", 0)
load_usr.wko_num = profile_dict.get("wko_num", 0)
@ -237,11 +245,11 @@ class PokkenBase:
load_usr.cool_ko_num = profile_dict.get("cool_ko_num", 0)
load_usr.perfect_ko_num = profile_dict.get("perfect_ko_num", 0)
load_usr.record_flag = profile_dict.get("record_flag", 0)
load_usr.site_register_status = profile_dict.get("site_register_status", 0)
load_usr.site_register_status = profile_dict.get("site_register_status", 1)
load_usr.continue_num = profile_dict.get("continue_num", 0)
load_usr.avatar_body = profile_dict.get("avatar_body", 0)
load_usr.avatar_gender = profile_dict.get("avatar_gender", 0)
load_usr.avatar_gender = profile_dict.get("avatar_gender", 1)
load_usr.avatar_background = profile_dict.get("avatar_background", 0)
load_usr.avatar_head = profile_dict.get("avatar_head", 0)
load_usr.avatar_battleglass = profile_dict.get("avatar_battleglass", 0)
@ -284,6 +292,31 @@ class PokkenBase:
load_usr.pokemon_data.append(pkm)
for x in profile_dict.get("tutorial_progress_flag", []):
load_usr.tutorial_progress_flag.append(x)
for x in profile_dict.get("achievement_flag", []):
load_usr.achievement_flag.append(x)
for x in profile_dict.get("aid_skill_list", []):
load_usr.aid_skill_list.append(x)
for x in profile_dict.get("rankmatch_progress", []):
load_usr.rankmatch_progress.append(x)
for x in profile_dict.get("event_achievement_flag", []):
load_usr.event_achievement_flag.append(x)
for x in profile_dict.get("event_achievement_param", []):
load_usr.event_achievement_param.append(x)
load_usr.support_set_1.append(profile_dict.get("support_set_1_1", 587))
load_usr.support_set_1.append(profile_dict.get("support_set_1_2", 653))
load_usr.support_set_2.append(profile_dict.get("support_set_2_1", 495))
load_usr.support_set_2.append(profile_dict.get("support_set_2_2", 131))
load_usr.support_set_3.append(profile_dict.get("support_set_3_1", 657))
load_usr.support_set_3.append(profile_dict.get("support_set_3_2", 133))
res.load_user.CopyFrom(load_usr)
return res.SerializeToString()
@ -301,6 +334,8 @@ class PokkenBase:
req = request.save_user
user_id = req.banapass_id
self.logger.info(f"Save user data for {user_id}")
tut_flgs: List[int] = []
ach_flgs: List[int] = []
evt_flgs: List[int] = []
@ -339,7 +374,7 @@ class PokkenBase:
for ach_flg in req.achievement_flag:
ach_flgs.append(ach_flg)
await self.data.profile.update_profile_tutorial_flags(user_id, ach_flg)
await self.data.profile.update_profile_achievement_flags(user_id, ach_flgs)
for evt_flg in req.event_achievement_flag:
evt_flgs.append(evt_flg)
@ -354,17 +389,22 @@ class PokkenBase:
await self.data.profile.add_profile_points(user_id, get_rank_pts, get_money, get_score_pts, grade_max)
# Inconsistant underscore use AND a typo??
#await self.data.profile.update_rankmatch_data(user_id, req.rankmatch_flag, req.rank_match_max, req.rank_match_success, req.rank_match_process)
await self.data.profile.update_support_team(user_id, 1, req.support_set_1[0], req.support_set_1[1])
await self.data.profile.update_support_team(user_id, 2, req.support_set_2[0], req.support_set_2[1])
await self.data.profile.update_support_team(user_id, 3, req.support_set_3[0], req.support_set_3[1])
await self.data.profile.put_pokemon(user_id, mon.char_id, mon.illustration_book_no, mon.bp_point_atk, mon.bp_point_res, mon.bp_point_def, mon.bp_point_sp)
await self.data.profile.add_pokemon_xp(user_id, mon.char_id, mon.get_pokemon_exp)
await self.data.profile.add_pokemon_xp(user_id, mon.illustration_book_no, mon.get_pokemon_exp)
await self.data.profile.set_latest_mon(user_id, mon.illustration_book_no)
for x in range(len(battle.play_mode)):
self.logger.info(f"Save {PokkenConstants.BATTLE_TYPE(battle.play_mode[x]).name} battle {PokkenConstants.BATTLE_RESULT(battle.result[x]).name} for {user_id} with mon {mon.illustration_book_no}")
await self.data.profile.put_pokemon_battle_result(
user_id,
mon.char_id,
mon.illustration_book_no,
PokkenConstants.BATTLE_TYPE(battle.play_mode[x]),
PokkenConstants.BATTLE_RESULT(battle.result[x])
)
@ -391,7 +431,6 @@ class PokkenBase:
last_evt
)
return res.SerializeToString()
async def handle_save_ingame_log(self, data: jackal_pb2.Request) -> bytes:
@ -419,6 +458,13 @@ class PokkenBase:
async def handle_matching_is_matching(
self, data: Dict = {}, client_ip: str = "127.0.0.1"
) -> Dict:
"""
"sessionId":"12345678",
"A":{
"pcb_id": data["data"]["must"]["pcb_id"],
"gip": client_ip
},
"""
return {
"data": {
"sessionId":"12345678",
@ -435,6 +481,11 @@ class PokkenBase:
) -> Dict:
return {}
async def handle_matching_obtain_matching(
self, data: Dict = {}, client_ip: str = "127.0.0.1"
) -> Dict:
return {}
async def handle_admission_noop(self, data: Dict, req_ip: str = "127.0.0.1") -> Dict:
return {}

View File

@ -0,0 +1,140 @@
{
"448": {
"name_en": "Lucario",
"name_jp": "ルカリオ",
"type": 0,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/448.png"
},
"25": {
"name_en": "Pikachu",
"name_jp": "ピカチュウ",
"type": 0,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/25.png"
},
"68": {
"name_en": "Machamp",
"name_jp": "カイリキー",
"type": 1,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/68.png"
},
"282": {
"name_en": "Gardevoir",
"name_jp": "サーナイト",
"type": 2,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/282.png"
},
"461": {
"name_en": "Weavile",
"name_jp": "マニューラ",
"type": 3,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/461.png"
},
"245": {
"name_en": "Suicune",
"name_jp": "スイクン",
"type": 0,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/245.png"
},
"6": {
"name_en": "Charizard",
"name_jp": "リザードン",
"type": 1,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/6.png"
},
"94": {
"name_en": "Gengar",
"name_jp": "ゲンガー",
"type": 2,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/94.png"
},
"257": {
"name_en": "Blaziken",
"name_jp": "バシャーモ",
"type": 0,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/257.png"
},
"10025": {
"name_en": "Pikachu Libre",
"name_jp": "マスクド・ピカチュウ",
"type": 3,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/10084.png"
},
"254": {
"name_en": "Sceptile",
"name_jp": "ジュカイン",
"type": 3,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/254.png"
},
"609": {
"name_en": "Chandelure",
"name_jp": "シャンデラ",
"type": 1,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/609.png"
},
"150": {
"name_en": "Mewtwo",
"name_jp": "ミュウツー",
"type": 0,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/150.png"
},
"10150": {
"name_en": "Shadow Mewtwo",
"name_jp": "ダークミュウツー",
"type": 2,
"artwork": "https://archives.bulbagarden.net/media/upload/7/7a/Pokk%C3%A9n_Shadow_Mewtwo.png"
},
"445": {
"name_en": "Garchomp",
"name_jp": "ガブリアス",
"type": 1,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/445.png"
},
"654": {
"name_en": "Braixen",
"name_jp": "テールナー",
"type": 0,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/654.png"
},
"491": {
"name_en": "Darkrai",
"name_jp": "ダークライ",
"type": 2,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/491.png"
},
"212": {
"name_en": "Scizor",
"name_jp": "ハッサム",
"type": 1,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/212.png"
},
"453": {
"name_en": "Croagunk",
"name_jp": "グレッグル",
"type": 3,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/453.png"
},
"395": {
"name_en": "Empoleon",
"name_jp": "エンペルト",
"type": 0,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/395.png"
},
"724": {
"name_en": "Decidueye",
"name_jp": "ジュナイパー",
"type": 0,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/724.png"
},
"681": {
"name_en": "Aegislash",
"name_jp": "ギルガルド",
"type": 2,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/681.png"
},
"9": {
"name_en": "Blastoise",
"name_jp": "カメックス",
"type": 1,
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/9.png"
}
}

View File

@ -0,0 +1,218 @@
{
"587": {
"name_en": "Emolga",
"name_jp": "エモンガ",
"desc": "Uses Shock Wave to shock the opponent and temporarily decrease its speed.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/587.png"
},
"653": {
"name_en": "Fennekin",
"name_jp": "フォッコ",
"desc": "Uses Ember to surround itself with fire, creating a trap.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/653.png"
},
"495": {
"name_en": "Snivy",
"name_jp": "ツタージャ",
"desc": "Uses Leaf Tornado to perform an anti-air attack and send the opponent flying.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/495.png"
},
"131": {
"name_en": "Lapras",
"name_jp": "ラプラス",
"desc": "Uses Surf as it enters the stage, damaging the enemy with a wave of water.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/131.png"
},
"657": {
"name_en": "Frogadier",
"name_jp": "ゲコガシラ",
"desc": "Uses Water Pulse to attack from a distance by firing water bullets. Effective when striking from long distance.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/657.png"
},
"133": {
"name_en": "Eevee",
"name_jp": "イーブイ",
"desc": "Uses Helping Hand to heal the user and temporarily increase their attack power.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/133.png"
},
"385": {
"name_en": "Jirachi",
"name_jp": "ジラーチ",
"desc": "Uses Wish to restore the Synergy Gauge and temporarily strengthen the user's attack power during Synergy Burst.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/385.png"
},
"547": {
"name_en": "Whimsicott",
"name_jp": "エルフーン",
"desc": "Uses Substitute to render attacks from opponents useless and heal the user.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/547.png"
},
"38": {
"name_en": "Ninetales",
"name_jp": "キュウコン",
"desc": "Uses Will-O-Wisp to send small flames in front of the user. Enemy's attack power decreased temporarily when contacted.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/38.png"
},
"429": {
"name_en": "Mismagius",
"name_jp": "ムウマージ",
"desc": "Uses Ominous Wind to attack the opponent and temporarily increase the user's attack power.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/429.png"
},
"83": {
"name_en": "Farfetch'd",
"name_jp": "カモネギ",
"desc": "Uses Fury Cutter to perform a flurry of attacks toward the opponent.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/83.png"
},
"101": {
"name_en": "Electrode",
"name_jp": "マルマイン",
"desc": "Uses Explosion to counter an opponent's attack upon defending.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/101.png"
},
"479": {
"name_en": "Rotom",
"name_jp": "ロトム",
"desc": "Uses Thunder Shock to target enemies in the air and temporarily decrease their speed.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/479.png"
},
"468": {
"name_en": "Togekiss",
"name_jp": "トゲキッス",
"desc": "Uses Tailwind to temporarily increase the user's speed and recover some health.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/468.png"
},
"149": {
"name_en": "Dragonite",
"name_jp": "カイリュー",
"desc": "Uses Draco Meteor to attack multiple times over a wide area.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/149.png"
},
"494": {
"name_en": "Victini",
"name_jp": "ビクティニ",
"desc": "Uses V-create to temporarily make the user's attacks critical hits, restores some of the user's health, and increases the user's Synergy Gauge. Unlike other Enhance Pokémon, Victini can actually damage the foe if they're above it when flying off the screen.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/494.png"
},
"453": {
"name_en": "Croagunk",
"name_jp": "グレッグル",
"desc": "Uses Toxic to attack opponent and temporarily decrease its defense.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/453.png"
},
"700": {
"name_en": "Sylveon",
"name_jp": "ニンフィア",
"desc": "Uses Reflect to heal user and temporarily increase their defense.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/700.png"
},
"417": {
"name_en": "Pachirisu",
"name_jp": "パチリス",
"desc": "Uses Follow Me to eliminate long distance attacks. Effective when get in close.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/417.png"
},
"129": {
"name_en": "Magikarp",
"name_jp": "コイキング",
"desc": "Uses Bounce to disrupt the enemy's attack when hit by an opponent. Effective for interrupting combos.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/129.png"
},
"104": {
"name_en": "Cubone",
"name_jp": "カラカラ",
"desc": "Uses Bonemerang to attack from a distance and can pull an enemy in.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/104.png"
},
"50": {
"name_en": "Diglett",
"name_jp": "ディグダ",
"desc": "Uses Dig to attack from below, making easy to aim for a combo.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/50.png"
},
"82": {
"name_en": "Magneton",
"name_jp": "レアコイル",
"desc": "Uses Tri Attack to attack from a distance diagonally upward and inflict two random negative statuses.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/82.png"
},
"195": {
"name_en": "Quagsire",
"name_jp": "ヌオー",
"desc": "Uses Mud Bomb to attack opponent on the ground, even when blocked.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/195.png"
},
"196": {
"name_en": "Espeon",
"name_jp": "エーフィ",
"desc": "Uses Morning Sun to remove any statuses and recover health, with more health recovered with less time remaining in the round.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/196.png"
},
"197": {
"name_en": "Umbreon",
"name_jp": "ブラッキー",
"desc": "Uses Snarl to absorb an opponent's Synergy Gauge and prevent them from performing any critical hits.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/197.png"
},
"643": {
"name_en": "Reshiram",
"name_jp": "レシラム",
"desc": "Uses Blue Flare to attack straight forward with a powerful flame. In the DX version, it can only be called once per round.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/643.png"
},
"488": {
"name_en": "Cresselia",
"name_jp": "クレセリア",
"desc": "Uses Lunar Dance to heal the user of any negative status, recovers health and Synergy Gauge, but can only be used once per round.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/488.png"
},
"717": {
"name_en": "Yveltal",
"name_jp": "イベルタル",
"desc": "Uses Oblivion Wing to attack from the sky and seal off the opponent's Synergy Burst. In the DX version, it can only be called once per round.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/717.png"
},
"381": {
"name_en": "Latios",
"name_jp": "ラティオス",
"desc": "Uses Luster Purge to place attacks around the enemy in order to restrict their movements. In the DX version, it can only be called once per round.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/381.png"
},
"725": {
"name_en": "Litten",
"name_jp": "ニャビー",
"desc": "Uses Fire Fang to attack toward the enemy. Damage increases when the player's at lower HP.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/725.png"
},
"728": {
"name_en": "Popplio",
"name_jp": "アシマリ",
"desc": "Uses Bubble Beam to temporarily increase attack and grant a double jump while in midair.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/728.png"
},
"10079": {
"name_en": "Mega Rayquaza",
"name_jp": "レックウザ",
"desc": "Uses Dragon Ascent to attack from a distance at tremendous speed. It also consumes the user's Synergy Gauge. It can only be called once per round.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/10079.png"
},
"778": {
"name_en": "Mimikyu",
"name_jp": "ミミッキュ",
"desc": "Uses Play Rough to attack continuously from behind and inflict double negative status.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/778.png"
},
"151": {
"name_en": "Mew",
"name_jp": "ミュウ",
"desc": "Uses Miraculous Power to randomly increase the user's Synergy Gauge, temporarily makes the user's attacks critical hits, and/or gives the user additional random positive status.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/151.png"
},
"251": {
"name_en": "Celebi",
"name_jp": "セレビィ",
"desc": "Uses Time Travel (Japanese: ときわたり Time Travel) to switch between Phases at almost any given moment, even when enemy guards an attack.",
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/251.png"
}
}

View File

@ -1,6 +1,6 @@
from typing import Optional, Dict, List, Union
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, INTEGER
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql import func, select, update, delete
from sqlalchemy.sql.functions import coalesce
@ -16,13 +16,8 @@ profile = Table(
"pokken_profile",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
unique=True,
),
Column("trainer_name", String(16)), # optional
Column("user", Integer, ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, unique=True),
Column("trainer_name", String(14)), # optional
Column("home_region_code", Integer),
Column("home_loc_name", String(255)),
Column("pref_code", Integer),
@ -66,7 +61,7 @@ profile = Table(
Column("navi_trainer", Integer),
Column("navi_version_id", Integer),
Column("aid_skill_list", JSON), # Repeated, Integer
Column("aid_skill", Integer),
Column("aid_skill", Integer), # Cheer skill, 6 of them, unlocked by lucky bonus
Column("comment_text_id", Integer),
Column("comment_word_id", Integer),
Column("latest_use_pokemon", Integer),
@ -105,20 +100,16 @@ profile = Table(
Column("battle_num_vs_cpu", Integer), # 2
Column("win_cpu", Integer),
Column("battle_num_tutorial", Integer), # 1?
mysql_charset="utf8mb4",
mysql_charset="utf8mb4"
)
pokemon_data = Table(
"pokken_pokemon_data",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("char_id", Integer, nullable=False),
Column("illustration_book_no", Integer),
Column("user", Integer, ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
Column("char_id", Integer),
Column("illustration_book_no", Integer, nullable=False), # This is the fucking pokedex number????
Column("pokemon_exp", Integer),
Column("battle_num_vs_wan", Integer), # 4?
Column("win_vs_wan", Integer),
@ -132,8 +123,8 @@ pokemon_data = Table(
Column("bp_point_res", Integer),
Column("bp_point_def", Integer),
Column("bp_point_sp", Integer),
UniqueConstraint("user", "char_id", name="pokken_pokemon_data_uk"),
mysql_charset="utf8mb4",
UniqueConstraint("user", "illustration_book_no", name="pokken_pokemon_uk"),
mysql_charset="utf8mb4"
)
@ -157,8 +148,8 @@ class PokkenProfileData(BaseData):
return result.lastrowid
async def set_profile_name(self, user_id: int, new_name: str, gender: Union[int, None] = None) -> None:
sql = update(profile).where(profile.c.user == user_id).values(
trainer_name=new_name,
sql = profile.update(profile.c.user == user_id).values(
trainer_name=new_name if new_name else profile.c.trainer_name,
avatar_gender=gender if gender is not None else profile.c.avatar_gender
)
result = await self.execute(sql)
@ -179,12 +170,12 @@ class PokkenProfileData(BaseData):
aid_skill: int,
last_evt: int
) -> None:
sql = update(profile).where(profile.c.user == user_id).values(
sql = profile.update(profile.c.user == user_id).values(
extra_counter=extra_counter,
event_reward_get_flag=evt_reward_get_flg,
total_play_days=total_play_days,
awake_num=awake_num,
use_support_num=use_support_ct,
total_play_days=coalesce(profile.c.total_play_days, 0) + total_play_days,
awake_num=coalesce(profile.c.awake_num, 0) + awake_num,
use_support_num=coalesce(profile.c.use_support_num, 0) + use_support_ct,
beat_num=beat_num,
aid_skill=aid_skill,
last_play_event_id=last_evt
@ -195,7 +186,7 @@ class PokkenProfileData(BaseData):
self.logger.error(f"Failed to put extra data for user {user_id}")
async def update_profile_tutorial_flags(self, user_id: int, tutorial_flags: List) -> None:
sql = update(profile).where(profile.c.user == user_id).values(
sql = profile.update(profile.c.user == user_id).values(
tutorial_progress_flag=tutorial_flags,
)
result = await self.execute(sql)
@ -205,7 +196,7 @@ class PokkenProfileData(BaseData):
)
async def update_profile_achievement_flags(self, user_id: int, achievement_flags: List) -> None:
sql = update(profile).where(profile.c.user == user_id).values(
sql = profile.update(profile.c.user == user_id).values(
achievement_flag=achievement_flags,
)
result = await self.execute(sql)
@ -215,7 +206,7 @@ class PokkenProfileData(BaseData):
)
async def update_profile_event(self, user_id: int, event_state: List, event_flags: List[int], event_param: List[int], last_evt: int = None) -> None:
sql = update(profile).where(profile.c.user == user_id).values(
sql = profile.update(profile.c.user == user_id).values(
event_state=event_state,
event_achievement_flag=event_flags,
event_achievement_param=event_param,
@ -230,13 +221,17 @@ class PokkenProfileData(BaseData):
async def add_profile_points(
self, user_id: int, rank_pts: int, money: int, score_pts: int, grade_max: int
) -> None:
sql = update(profile).where(profile.c.user == user_id).values(
trainer_rank_point = profile.c.trainer_rank_point + rank_pts,
fight_money = profile.c.fight_money + money,
score_point = profile.c.score_point + score_pts,
sql = profile.update(profile.c.user == user_id).values(
trainer_rank_point = coalesce(profile.c.trainer_rank_point, 0) + rank_pts,
wallet = coalesce(profile.c.wallet, 0) + money,
score_point = coalesce(profile.c.score_point, 0) + score_pts,
grade_max_num = grade_max
)
result = await self.execute(sql)
if result is None:
return None
async def get_profile(self, user_id: int) -> Optional[Row]:
sql = profile.select(profile.c.user == user_id)
result = await self.execute(sql)
@ -248,7 +243,7 @@ class PokkenProfileData(BaseData):
self,
user_id: int,
pokemon_id: int,
illust_no: int,
pokedex_number: int,
atk: int,
res: int,
defe: int,
@ -257,7 +252,7 @@ class PokkenProfileData(BaseData):
sql = insert(pokemon_data).values(
user=user_id,
char_id=pokemon_id,
illustration_book_no=illust_no,
illustration_book_no=pokedex_number,
pokemon_exp=0,
battle_num_vs_wan=0,
win_vs_wan=0,
@ -274,7 +269,7 @@ class PokkenProfileData(BaseData):
)
conflict = sql.on_duplicate_key_update(
illustration_book_no=illust_no,
illustration_book_no=pokedex_number,
bp_point_atk=pokemon_data.c.bp_point_atk + atk,
bp_point_res=pokemon_data.c.bp_point_res + res,
bp_point_def=pokemon_data.c.bp_point_def + defe,
@ -293,7 +288,7 @@ class PokkenProfileData(BaseData):
pokemon_id: int,
xp: int
) -> None:
sql = update(pokemon_data).where(and_(pokemon_data.c.user==user_id, pokemon_data.c.char_id==pokemon_id)).values(
sql = pokemon_data.update(and_(pokemon_data.c.user==user_id, pokemon_data.c.illustration_book_no==pokemon_id)).values(
pokemon_exp=coalesce(pokemon_data.c.pokemon_exp, 0) + xp
)
@ -302,7 +297,7 @@ class PokkenProfileData(BaseData):
self.logger.warning(f"Failed to add {xp} XP to pokemon ID {pokemon_id} for user {user_id}")
async def get_pokemon_data(self, user_id: int, pokemon_id: int) -> Optional[Row]:
sql = pokemon_data.select(and_(pokemon_data.c.user == user_id, pokemon_data.c.char_id == pokemon_id))
sql = pokemon_data.select(and_(pokemon_data.c.user == user_id, pokemon_data.c.illustration_book_no == pokemon_id))
result = await self.execute(sql)
if result is None:
return None
@ -315,6 +310,14 @@ class PokkenProfileData(BaseData):
return None
return result.fetchall()
async def set_latest_mon(self, user_id: int, pokedex_no: int) -> None:
sql = profile.update(profile.c.user == user_id).values(
latest_use_pokemon=pokedex_no
)
result = await self.execute(sql)
if result is None:
self.logger.warning(f"Failed to update user {user_id}'s last used pokemon {pokedex_no}")
async def put_pokemon_battle_result(
self, user_id: int, pokemon_id: int, match_type: PokkenConstants.BATTLE_TYPE, match_result: PokkenConstants.BATTLE_RESULT
) -> None:
@ -322,7 +325,7 @@ class PokkenProfileData(BaseData):
Records the match stats (type and win/loss) for the pokemon and profile
coalesce(pokemon_data.c.win_vs_wan, 0)
"""
sql = update(pokemon_data).where(and_(pokemon_data.c.user==user_id, pokemon_data.c.char_id==pokemon_id)).values(
sql = pokemon_data.update(and_(pokemon_data.c.user==user_id, pokemon_data.c.illustration_book_no==pokemon_id)).values(
battle_num_tutorial=coalesce(pokemon_data.c.battle_num_tutorial, 0) + 1 if match_type==PokkenConstants.BATTLE_TYPE.TUTORIAL else coalesce(pokemon_data.c.battle_num_tutorial, 0),
battle_all_num_tutorial=coalesce(pokemon_data.c.battle_all_num_tutorial, 0) + 1 if match_type==PokkenConstants.BATTLE_TYPE.TUTORIAL else coalesce(pokemon_data.c.battle_all_num_tutorial, 0),
@ -353,7 +356,7 @@ class PokkenProfileData(BaseData):
"""
Records profile stats
"""
sql = update(profile).where(profile.c.user==user_id).values(
sql = profile.update(profile.c.user==user_id).values(
ex_ko_num=coalesce(profile.c.ex_ko_num, 0) + exkos,
wko_num=coalesce(profile.c.wko_num, 0) + wkos,
timeup_win_num=coalesce(profile.c.timeup_win_num, 0) + timeout_wins,
@ -367,7 +370,12 @@ class PokkenProfileData(BaseData):
self.logger.warning(f"Failed to update stats for user {user_id}")
async def update_support_team(self, user_id: int, support_id: int, support1: int = None, support2: int = None) -> None:
sql = update(profile).where(profile.c.user==user_id).values(
if support1 == 4294967295:
support1 = None
if support2 == 4294967295:
support2 = None
sql = profile.update(profile.c.user==user_id).values(
support_set_1_1=support1 if support_id == 1 else profile.c.support_set_1_1,
support_set_1_2=support2 if support_id == 1 else profile.c.support_set_1_2,
support_set_2_1=support1 if support_id == 2 else profile.c.support_set_2_1,
@ -379,3 +387,15 @@ class PokkenProfileData(BaseData):
result = await self.execute(sql)
if result is None:
self.logger.warning(f"Failed to update support team {support_id} for user {user_id}")
async def update_rankmatch_data(self, user_id: int, flag: int, rm_max: Optional[int], success: Optional[int], progress: List[int]) -> None:
sql = profile.update(profile.c.user==user_id).values(
rankmatch_flag=flag,
rankmatch_max=rm_max,
rankmatch_progress=progress,
rankmatch_success=success,
)
result = await self.execute(sql)
if result is None:
self.logger.warning(f"Failed to update rankmatch data for user {user_id}")

View File

@ -1,9 +1,11 @@
from .index import SaoServlet
from .const import SaoConstants
from .database import SaoData
from .frontend import SaoFrontend
from .read import SaoReader
index = SaoServlet
database = SaoData
frontend = SaoFrontend
reader = SaoReader
game_codes = [SaoConstants.GAME_CODE]

File diff suppressed because it is too large Load Diff

View File

@ -30,9 +30,21 @@ class SaoServerConfig:
)
@property
def use_https(self) -> bool:
def photon_app_id(self) -> str:
return CoreConfig.get_config_field(
self.__config, "sao", "server", "use_https", default=False
self.__config, "sao", "server", "photon_app_id", default="7df3a2f6-d69d-4073-aafe-810ee61e1cea"
)
@property
def data_version(self) -> int:
return CoreConfig.get_config_field(
self.__config, "sao", "server", "data_version", default=1
)
@property
def game_version(self) -> int:
return CoreConfig.get_config_field(
self.__config, "sao", "server", "game_version", default=33
)
class SaoCryptConfig:
@ -51,12 +63,6 @@ class SaoCryptConfig:
self.__config, "sao", "crypt", "key", default=""
)
@property
def iv(self) -> str:
return CoreConfig.get_config_field(
self.__config, "sao", "crypt", "iv", default=""
)
class SaoHashConfig:
def __init__(self, parent_config: "SaoConfig"):
self.__config = parent_config
@ -73,9 +79,31 @@ class SaoHashConfig:
self.__config, "sao", "hash", "hash_base", default=""
)
class SaoCardConfig:
def __init__(self, parent_config: "SaoConfig"):
self.__config = parent_config
@property
def enable(self) -> bool:
return CoreConfig.get_config_field(
self.__config, "sao", "card", "enable", default=True
)
@property
def crypt_password(self) -> str:
return CoreConfig.get_config_field(
self.__config, "sao", "card", "crypt_password", default=""
)
@property
def crypt_salt(self) -> str:
return CoreConfig.get_config_field(
self.__config, "sao", "card", "crypt_salt", default=""
)
class SaoConfig(dict):
def __init__(self) -> None:
self.server = SaoServerConfig(self)
self.crypt = SaoCryptConfig(self)
self.hash = SaoHashConfig(self)
self.card = SaoCardConfig(self)

View File

@ -1,5 +1,4 @@
from enum import Enum
from enum import IntEnum
class SaoConstants:
GAME_CODE = "SDEW"
@ -9,7 +8,7 @@ class SaoConstants:
VER_SAO = 0
VERSION_NAMES = ("Sword Art Online Arcade")
VERSION_NAMES = ("Sword Art Online Arcade",)
SERIAL_IDENT_SATALITE = 4
SERIAL_IDENT_TERMINAL = 5
@ -23,3 +22,653 @@ class SaoConstants:
@classmethod
def game_ver_to_string(cls, ver: int):
return cls.VERSION_NAMES[ver]
class RewardType(IntEnum):
None_ = 0
HeroLog = 1
Equipment = 2
Item = 3
Col = 4
VP = 5
YuiMadal = 6
VPGashaTicket = 7
SupportLog = 8
EpisodeAppend = 9
EventItem = 10
Ticket = 11
class ItemType(IntEnum):
ADD_HERO_PROPERTY = 1
ADD_WEAPON_PROPERTY = 2
ADD_ARMOR_PROPERTY = 3
UNLOCK_PROPERTY = 4
REMOVE_PROPERTY = 5
ADD_SKILL_SLOT = 6
ADD_XP = 7
REDEMPTION = 8
HERO_LEVEL_LIMIT_BREAK = 9
WEAPON_LEVEL_LIMIT_BREAK = 10
ARMOR_LEVEL_LIMIT_BREAK = 11
ADD_AWAKENING_XP = 12
class ExBonusCondition(IntEnum):
CLEAR_UNDER_X_SECS = 1
DEFEAT_X_MONSTER_Y_TIMES = 2
DEFEAT_X_MONSTERS = 3
CLEAR_X_MISSIONS = 4
CLEAR_MISSION_DIFFICULTY_X = 5
COLLECT_X_LOGS = 6
CLEAR_SKILL_LEVEL_X = 7
NO_LOSSES = 8
ACCEL_X_TIMES = 9
MAX_COMBO_X = 10
MULTIPLAYER_CLEAR_X = 11
class UnanalyzedLogGrade(IntEnum):
WHITE = 1
COPPER = 2
SILVER = 3
GOLD = 4
RAINBOW = 5
class QuestType(IntEnum):
EPISODE = 1
TRIAL_TOWER = 2
SIDE = 3
VERSUS = 4
EX_TOWER = 5
EPISODE_CHAPTER = 6 # Unused
class GameconnectCmd(IntEnum):
TICKET_REQUEST = 0xC000
TICKET_RESPONSE = 0xC001
GET_APP_VERSIONS_REQUEST = 0xC100
GET_APP_VERSIONS_RESPONSE = 0xC101
MASTER_DATA_VERSION_CHECK_REQUEST = 0xC102
MASTER_DATA_VERSION_CHECK_RESPONSE = 0xC103
LOGIN_REQUEST = 0xC104
LOGIN_RESPONSE = 0xC105
LOGOUT_REQUEST = 0xC106
LOGOUT_RESPONSE = 0xC107
LOGOUT_TICKET_UNPURCHASED_REQUEST = 0xC108
LOGOUT_TICKET_UNPURCHASED_RESPONSE = 0xC109
PAYING_PLAY_START_REQUEST = 0xC10A
PAYING_PLAY_START_RESPONSE = 0xC10B
PAYING_PLAY_END_REQUEST = 0xC10C
PAYING_PLAY_END_RESPONSE = 0xC10D
PURCHASE_TICKET_REQUEST = 0xC10E
PURCHASE_TICKET_RESPONSE = 0xC10F
CONSUME_TICKET_REQUEST = 0xC110
CONSUME_TICKET_RESPONSE = 0xC111
ADD_CREDIT_REQUEST = 0xC112
ADD_CREDIT_RESPONSE = 0xC113
CONSUME_CREDIT_REQUEST = 0xC114
CONSUME_CREDIT_RESPONSE = 0xC115
PURCHASE_TICKET_GUEST_REQUEST = 0xC116
PURCHASE_TICKET_GUEST_RESPONSE = 0xC117
CONSUME_TICKET_GUEST_REQUEST = 0xC118
CONSUME_TICKET_GUEST_RESPONSE = 0xC119
ADD_CREDIT_GUEST_REQUEST = 0xC11A
ADD_CREDIT_GUEST_RESPONSE = 0xC11B
CONSUME_CREDIT_GUEST_REQUEST = 0xC11C
CONSUME_CREDIT_GUEST_RESPONSE = 0xC11D
GET_AUTH_CARD_DATA_REQUEST = 0xC11E
GET_AUTH_CARD_DATA_RESPONSE = 0xC11F
GET_ACCESS_CODE_BY_KEITAI_REQUEST = 0xC120
GET_ACCESS_CODE_BY_KEITAI_RESPONSE = 0xC121
GET_MAINTENANCE_INFO_REQUEST = 0xC122
GET_MAINTENANCE_INFO_RESPONSE = 0xC123
GET_RESOURCE_PATH_INFO_REQUEST = 0xC124
GET_RESOURCE_PATH_INFO_RESPONSE = 0xC125
VALIDATION_ERROR_NOTIFICATION_REQUEST = 0xC126
VALIDATION_ERROR_NOTIFICATION_RESPONSE = 0xC127
POWER_CUTTING_RETURN_NOTIFICATION_REQUEST = 0xC128
POWER_CUTTING_RETURN_NOTIFICATION_RESPONSE = 0xC129
GIVE_FREE_TICKET_REQUEST = 0xC12A
GIVE_FREE_TICKET_RESPONSE = 0xC12B
MATCHING_ERROR_NOTIFICATION_REQUEST = 0xC12C
MATCHING_ERROR_NOTIFICATION_RESPONSE = 0xC12D
AC_CABINET_BOOT_NOTIFICATION_REQUEST = 0xC12E
AC_CABINET_BOOT_NOTIFICATION_RESPONSE = 0xC12F
FIRST_TUTORIAL_END_REQUEST = 0xC200
FIRST_TUTORIAL_END_RESPONSE = 0xC201
VARIOUS_TUTORIAL_END_REQUEST = 0xC202
VARIOUS_TUTORIAL_END_RESPONSE = 0xC203
GET_VARIOUS_TUTORIAL_DATA_LIST_REQUEST = 0xC204
GET_VARIOUS_TUTORIAL_DATA_LIST_RESPONSE = 0xC205
DISCHARGE_PROFILE_CARD_REQUEST = 0xC300
DISCHARGE_PROFILE_CARD_RESPONSE = 0xC301
DISCHARGE_RESOURCE_CARD_REQUEST = 0xC302
DISCHARGE_RESOURCE_CARD_RESPONSE = 0xC303
DISCHARGE_RESOURCE_CARD_COMPLETE_REQUEST = 0xC304
DISCHARGE_RESOURCE_CARD_COMPLETE_RESPONSE = 0xC305
SCAN_QR_QUEST_PROFILE_CARD_REQUEST = 0xC306
SCAN_QR_QUEST_PROFILE_CARD_RESPONSE = 0xC307
SCAN_QR_SHOP_RESOURCE_CARD_REQUEST = 0xC308
SCAN_QR_SHOP_RESOURCE_CARD_RESPONSE = 0xC309
SCAN_QR_QUEST_RESOURCE_CARD_REQUEST = 0xC30A
SCAN_QR_QUEST_RESOURCE_CARD_RESPONSE = 0xC30B
CHECK_YUI_MEDAL_GET_CONDITION_REQUEST = 0xC400
CHECK_YUI_MEDAL_GET_CONDITION_RESPONSE = 0xC401
GET_YUI_MEDAL_BONUS_USER_DATA_REQUEST = 0xC402
GET_YUI_MEDAL_BONUS_USER_DATA_RESPONSE = 0xC403
CHECK_COMEBACK_EVENT_REQUEST = 0xC404
CHECK_COMEBACK_EVENT_RESPONSE = 0xC405
CHANGE_MY_STORE_REQUEST = 0xC406
CHANGE_MY_STORE_RESPONSE = 0xC407
CHECK_TITLE_GET_DECISION_REQUEST = 0xC408
CHECK_TITLE_GET_DECISION_RESPONSE = 0xC409
CHECK_PROFILE_CARD_USED_REWARD_REQUEST = 0xC40A
CHECK_PROFILE_CARD_USED_REWARD_RESPONSE = 0xC40B
CHECK_AC_LOGIN_BONUS_REQUEST = 0xC40C
CHECK_AC_LOGIN_BONUS_RESPONSE = 0xC40D
GET_USER_BASIC_DATA_REQUEST = 0xC500
GET_USER_BASIC_DATA_RESPONSE = 0xC501
GET_VP_GASHA_TICKET_DATA_LIST_REQUEST = 0xC502
GET_VP_GASHA_TICKET_DATA_LIST_RESPONSE = 0xC503
GET_PRESENT_BOX_NUM_REQUEST = 0xC504
GET_PRESENT_BOX_NUM_RESPONSE = 0xC505
GET_HERO_LOG_USER_DATA_LIST_REQUEST = 0xC600
GET_HERO_LOG_USER_DATA_LIST_RESPONSE = 0xC601
GET_EQUIPMENT_USER_DATA_LIST_REQUEST = 0xC602
GET_EQUIPMENT_USER_DATA_LIST_RESPONSE = 0xC603
GET_ITEM_USER_DATA_LIST_REQUEST = 0xC604
GET_ITEM_USER_DATA_LIST_RESPONSE = 0xC605
GET_SUPPORT_LOG_USER_DATA_LIST_REQUEST = 0xC606
GET_SUPPORT_LOG_USER_DATA_LIST_RESPONSE = 0xC607
GET_EPISODE_APPEND_DATA_LIST_REQUEST = 0xC608
GET_EPISODE_APPEND_DATA_LIST_RESPONSE = 0xC609
GET_EVENT_ITEM_DATA_LIST_REQUEST = 0xC60A
GET_EVENT_ITEM_DATA_LIST_RESPONSE = 0xC60B
GET_GASHA_MEDAL_USER_DATA_LIST_REQUEST = 0xC60C
GET_GASHA_MEDAL_USER_DATA_LIST_RESPONSE = 0xC60D
GET_SHOP_RESOURCE_SALES_DATA_LIST_REQUEST = 0xC700
GET_SHOP_RESOURCE_SALES_DATA_LIST_RESPONSE = 0xC701
PURCHASE_SHOP_RESOURCE_REQUEST = 0xC702
PURCHASE_SHOP_RESOURCE_RESPONSE = 0xC703
DISCARD_SHOP_RESOURCE_REQUEST = 0xC704
DISCARD_SHOP_RESOURCE_RESPONSE = 0xC705
GET_TITLE_USER_DATA_LIST_REQUEST = 0xC800
GET_TITLE_USER_DATA_LIST_RESPONSE = 0xC801
CHANGE_TITLE_REQUEST = 0xC802
CHANGE_TITLE_RESPONSE = 0xC803
GET_PARTY_DATA_LIST_REQUEST = 0xC804
GET_PARTY_DATA_LIST_RESPONSE = 0xC805
CHANGE_PARTY_REQUEST = 0xC806
CHANGE_PARTY_RESPONSE = 0xC807
GET_SUPPORT_LOG_PARTY_DATA_LIST_REQUEST = 0xC808
GET_SUPPORT_LOG_PARTY_DATA_LIST_RESPONSE = 0xC809
CHANGE_SUPPORT_LOG_PARTY_REQUEST = 0xC80A
CHANGE_SUPPORT_LOG_PARTY_RESPONSE = 0xC80B
CHANGE_HERO_LOG_LAST_SETTING_SKILL_REQUEST = 0xC80C
CHANGE_HERO_LOG_LAST_SETTING_SKILL_RESPONSE = 0xC80D
LOCK_RESOURCE_REQUEST = 0xC80E
LOCK_RESOURCE_RESPONSE = 0xC80F
UNLOCK_RESOURCE_REQUEST = 0xC810
UNLOCK_RESOURCE_RESPONSE = 0xC811
DISPOSAL_RESOURCE_REQUEST = 0xC812
DISPOSAL_RESOURCE_RESPONSE = 0xC813
SYNTHESIZE_ENHANCEMENT_HERO_LOG_REQUEST = 0xC814
SYNTHESIZE_ENHANCEMENT_HERO_LOG_RESPONSE = 0xC815
SYNTHESIZE_ENHANCEMENT_EQUIPMENT_REQUEST = 0xC816
SYNTHESIZE_ENHANCEMENT_EQUIPMENT_RESPONSE = 0xC817
SYNTHESIZE_ENHANCEMENT_SUPPORT_LOG_REQUEST = 0xC818
SYNTHESIZE_ENHANCEMENT_SUPPORT_LOG_RESPONSE = 0xC819
SYNTHESIZE_ABILITY_HERO_LOG_REQUEST = 0xC81A
SYNTHESIZE_ABILITY_HERO_LOG_RESPONSE = 0xC81B
SYNTHESIZE_ABILITY_EQUIPMENT_REQUEST = 0xC81C
SYNTHESIZE_ABILITY_EQUIPMENT_RESPONSE = 0xC81D
SYNTHESIZE_ABILITY_SUPPORT_LOG_REQUEST = 0xC81E
SYNTHESIZE_ABILITY_SUPPORT_LOG_RESPONSE = 0xC820
GET_QUEST_SCENE_USER_DATA_LIST_REQUEST = 0xC900
GET_QUEST_SCENE_USER_DATA_LIST_RESPONSE = 0xC901
GET_QUEST_SCENE_PREV_SCAN_PROFILE_CARD_REQUEST = 0xC902
GET_QUEST_SCENE_PREV_SCAN_PROFILE_CARD_RESPONSE = 0xC903
EPISODE_PLAY_START_REQUEST = 0xC904
EPISODE_PLAY_START_RESPONSE = 0xC905
EPISODE_PLAY_CONTINUE_REQUEST = 0xC906
EPISODE_PLAY_CONTINUE_RESPONSE = 0xC907
EPISODE_PLAY_END_REQUEST = 0xC908
EPISODE_PLAY_END_RESPONSE = 0xC909
EPISODE_PLAY_END_UNANALYZED_LOG_FIXED_REQUEST = 0xC90A
EPISODE_PLAY_END_UNANALYZED_LOG_FIXED_RESPONSE = 0xC90B
SIDE_QUEST_PLAY_START_REQUEST = 0xC90C
SIDE_QUEST_PLAY_START_RESPONSE = 0xC90D
SIDE_QUEST_PLAY_CONTINUE_REQUEST = 0xC90E
SIDE_QUEST_PLAY_CONTINUE_RESPONSE = 0xC90F
SIDE_QUEST_PLAY_END_REQUEST = 0xC910
SIDE_QUEST_PLAY_END_RESPONSE = 0xC911
SIDE_QUEST_PLAY_END_UNANALYZED_LOG_FIXED_REQUEST = 0xC912
SIDE_QUEST_PLAY_END_UNANALYZED_LOG_FIXED_RESPONSE = 0xC913
TRIAL_TOWER_PLAY_START_REQUEST = 0xC914
TRIAL_TOWER_PLAY_START_RESPONSE = 0xC915
TRIAL_TOWER_PLAY_CONTINUE_REQUEST = 0xC916
TRIAL_TOWER_PLAY_CONTINUE_RESPONSE = 0xC917
TRIAL_TOWER_PLAY_END_REQUEST = 0xC918
TRIAL_TOWER_PLAY_END_RESPONSE = 0xC919
TRIAL_TOWER_PLAY_END_UNANALYZED_LOG_FIXED_REQUEST = 0xC91A
TRIAL_TOWER_PLAY_END_UNANALYZED_LOG_FIXED_RESPONSE = 0xC91B
DEFRAG_MATCH_QUEST_PLAY_START_REQUEST = 0xC91C
DEFRAG_MATCH_QUEST_PLAY_START_RESPONSE = 0xC91D
DEFRAG_MATCH_QUEST_PLAY_END_REQUEST = 0xC91E
DEFRAG_MATCH_QUEST_PLAY_END_RESPONSE = 0xC91F
DEFRAG_MATCH_QUEST_PLAY_END_UNANALYZED_LOG_FIXED_REQUEST = 0xC920
DEFRAG_MATCH_QUEST_PLAY_END_UNANALYZED_LOG_FIXED_RESPONSE = 0xC921
EX_TOWER_QUEST_PLAY_START_REQUEST = 0xC922
EX_TOWER_QUEST_PLAY_START_RESPONSE = 0xC923
EX_TOWER_QUEST_PLAY_CONTINUE_REQUEST = 0xC924
EX_TOWER_QUEST_PLAY_CONTINUE_RESPONSE = 0xC925
EX_TOWER_QUEST_PLAY_END_REQUEST = 0xC926
EX_TOWER_QUEST_PLAY_END_RESPONSE = 0xC927
EX_TOWER_QUEST_PLAY_END_UNANALYZED_LOG_FIXED_REQUEST = 0xC928
EX_TOWER_QUEST_PLAY_END_UNANALYZED_LOG_FIXED_RESPONSE = 0xC929
GET_EX_TOWER_USER_DATA_REQUEST = 0xC92A
GET_EX_TOWER_USER_DATA_RESPONSE = 0xC92B
REG_EX_TOWER_HALL_OF_FAME_CONFIRM_FLAG_REQUEST = 0xC92C
REG_EX_TOWER_HALL_OF_FAME_CONFIRM_FLAG_RESPONSE = 0xC92D
REG_EX_TOWER_CONFIRM_FLAG_REQUEST = 0xC92E
REG_EX_TOWER_CONFIRM_FLAG_RESPONSE = 0xC92F
GET_CHAT_SIDE_STORY_USER_DATA_LIST_REQUEST = 0xC930
GET_CHAT_SIDE_STORY_USER_DATA_LIST_RESPONSE = 0xC931
GET_USER_QUEST_EPISODE_STATUS_DATA_LIST_REQUEST = 0xC932
GET_USER_QUEST_EPISODE_STATUS_DATA_LIST_RESPONSE = 0xC933
APPLY_QUEST_DROP_BOOST_REQUEST = 0xC934
APPLY_QUEST_DROP_BOOST_RESPONSE = 0xC935
CREATE_QUEST_SCENE_MULTI_PLAY_ROOM_ID_REQUEST = 0xCA00
CREATE_QUEST_SCENE_MULTI_PLAY_ROOM_ID_RESPONSE = 0xCA01
GET_QUEST_SCENE_MULTI_PLAY_PHOTON_SERVER_REQUEST = 0xCA02
GET_QUEST_SCENE_MULTI_PLAY_PHOTON_SERVER_RESPONSE = 0xCA03
GET_QUEST_SCENE_MULTI_PLAY_PHOTON_SERVER_BY_ROOM_REQUEST = 0xCA04
GET_QUEST_SCENE_MULTI_PLAY_PHOTON_SERVER_BY_ROOM_RESPONSE = 0xCA05
GET_QUEST_BEST_SOCRE_RANKING_LIST_REQUEST = 0xCB00
GET_QUEST_BEST_SOCRE_RANKING_LIST_RESPONSE = 0xCB01
GET_QUEST_HIERARCHY_PROGRESS_DEGREES_RANKING_LIST_REQUEST = 0xCB02
GET_QUEST_HIERARCHY_PROGRESS_DEGREES_RANKING_LIST_RESPONSE = 0xCB03
GET_QUEST_POPULAR_HERO_LOG_RANKING_LIST_REQUEST = 0xCB04
GET_QUEST_POPULAR_HERO_LOG_RANKING_LIST_RESPONSE = 0xCB05
GET_QUEST_EX_TOWER_HIERARCHY_PROGRESS_DEGREES_STORE_RANKING_LIST_REQUEST = 0xCB06
GET_QUEST_EX_TOWER_HIERARCHY_PROGRESS_DEGREES_STORE_RANKING_LIST_RESPONSE = 0xCB07
GET_QUEST_EX_TOWER_HIERARCHY_PROGRESS_DEGREES_NATIONAL_RANKING_LIST_REQUEST = 0xCB08
GET_QUEST_EX_TOWER_HIERARCHY_PROGRESS_DEGREES_NATIONAL_RANKING_LIST_RESPONSE = 0xCB09
GET_TREASURE_HUNT_BASIC_DATA_REQUEST = 0xCC00
GET_TREASURE_HUNT_BASIC_DATA_RESPONSE = 0xCC01
GET_TREASURE_HUNT_WHOLE_TASK_DATA_LIST_REQUEST = 0xCC02
GET_TREASURE_HUNT_WHOLE_TASK_DATA_LIST_RESPONSE = 0xCC03
GET_TREASURE_HUNT_INDIVIDUAL_TASK_DATA_LIST_REQUEST = 0xCC04
GET_TREASURE_HUNT_INDIVIDUAL_TASK_DATA_LIST_RESPONSE = 0xCC05
TREASURE_HUNT_AD_CONFIRM_NOTIFICATION_REQUEST = 0xCC06
TREASURE_HUNT_AD_CONFIRM_NOTIFICATION_RESPONSE = 0xCC07
GET_TREASURE_HUNT_EVENT_POINT_RANKING_USER_DATA_REQUEST = 0xCC08
GET_TREASURE_HUNT_EVENT_POINT_RANKING_USER_DATA_RESPONSE = 0xCC09
GET_TREASURE_HUNT_EVENT_POINT_STORE_RANKING_LIST_REQUEST = 0xCC0A
GET_TREASURE_HUNT_EVENT_POINT_STORE_RANKING_LIST_RESPONSE = 0xCC0B
GET_TREASURE_HUNT_EVENT_POINT_NATIONAL_RANKING_LIST_REQUEST = 0xCC0C
GET_TREASURE_HUNT_EVENT_POINT_NATIONAL_RANKING_LIST_RESPONSE = 0xCC0D
GET_DEFRAG_MATCH_BASIC_DATA_REQUEST = 0xCD00
GET_DEFRAG_MATCH_BASIC_DATA_RESPONSE = 0xCD01
GET_DEFRAG_MATCH_RANKING_USER_DATA_REQUEST = 0xCD02
GET_DEFRAG_MATCH_RANKING_USER_DATA_RESPONSE = 0xCD03
GET_DEFRAG_MATCH_LEAGUE_POINT_RANKING_LIST_REQUEST = 0xCD04
GET_DEFRAG_MATCH_LEAGUE_POINT_RANKING_LIST_RESPONSE = 0xCD05
GET_DEFRAG_MATCH_LEAGUE_SCORE_RANKING_LIST_REQUEST = 0xCD06
GET_DEFRAG_MATCH_LEAGUE_SCORE_RANKING_LIST_RESPONSE = 0xCD07
DEFRAG_MATCH_AD_CONFIRM_NOTIFICATION_REQUEST = 0xCD08
DEFRAG_MATCH_AD_CONFIRM_NOTIFICATION_RESPONSE = 0xCD09
CHECK_GET_SEED_DEFRAG_MATCH_REQUEST = 0xCD0A
CHECK_GET_SEED_DEFRAG_MATCH_RESPONSE = 0xCD0B
REG_DEFRAG_MATCH_HALL_OF_FAME_CONFIRM_FLAG_REQUEST = 0xCD0C
REG_DEFRAG_MATCH_HALL_OF_FAME_CONFIRM_FLAG_RESPONSE = 0xCD0D
GET_EVENT_SCENE_USER_DATA_LIST_REQUEST = 0xCE00
GET_EVENT_SCENE_USER_DATA_LIST_RESPONSE = 0xCE01
REG_PLAYED_EVENT_SCENE_USER_DATA_LIST_REQUEST = 0xCE02
REG_PLAYED_EVENT_SCENE_USER_DATA_LIST_RESPONSE = 0xCE03
GET_GASHA_LIST_REQUEST = 0xCF00
GET_GASHA_LIST_RESPONSE = 0xCF01
EXEC_GASHA_REQUEST = 0xCF02
EXEC_GASHA_RESPONSE = 0xCF03
EXEC_CREDIT_GASHA_START_REQUEST = 0xCF04
EXEC_CREDIT_GASHA_START_RESPONSE = 0xCF05
EXEC_CREDIT_GASHA_EXTRA_PAYING_REQUEST = 0xCF06
EXEC_CREDIT_GASHA_EXTRA_PAYING_RESPONSE = 0xCF07
EXEC_CREDIT_GASHA_END_REQUEST = 0xCF08
EXEC_CREDIT_GASHA_END_RESPONSE = 0xCF09
EXEC_CREDIT_GASHA_CARD_DISCHARGE_STATE_MIGRATION_REQUEST = 0xCF0A
EXEC_CREDIT_GASHA_CARD_DISCHARGE_STATE_MIGRATION_RESPONSE = 0xCF0B
EXEC_CREDIT_GASHA_CARD_DISCHARGE_END_REQUEST = 0xCF0C
EXEC_CREDIT_GASHA_CARD_DISCHARGE_END_RESPONSE = 0xCF0D
GET_GASHA_MEDAL_SHOP_USER_DATA_LIST_REQUEST = 0xCF0E
GET_GASHA_MEDAL_SHOP_USER_DATA_LIST_RESPONSE = 0xCF0F
PURCHASE_GASHA_MEDAL_SHOP_ITEM_REQUEST = 0xCF10
PURCHASE_GASHA_MEDAL_SHOP_ITEM_RESPONSE = 0xCF11
GET_GASHA_FREE_CAMPAIGN_USER_DATA_REQUEST = 0xCF12
GET_GASHA_FREE_CAMPAIGN_USER_DATA_RESPONSE = 0xCF13
GET_ADVENTURE_EXEC_USER_DATA_REQUEST = 0xD000
GET_ADVENTURE_EXEC_USER_DATA_RESPONSE = 0xD001
GET_ADVENTURE_PARTY_DATA_LIST_REQUEST = 0xD002
GET_ADVENTURE_PARTY_DATA_LIST_RESPONSE = 0xD003
GET_YUI_MEDAL_SHOP_USER_DATA_LIST_REQUEST = 0xD100
GET_YUI_MEDAL_SHOP_USER_DATA_LIST_RESPONSE = 0xD101
PURCHASE_YUI_MEDAL_SHOP_ITEM_REQUEST = 0xD102
PURCHASE_YUI_MEDAL_SHOP_ITEM_RESPONSE = 0xD103
PURCHASE_EVENT_SHOP_ITEM_REQUEST = 0xD104
PURCHASE_EVENT_SHOP_ITEM_RESPONSE = 0xD105
GET_BEGINNER_MISSION_USER_DATA_REQUEST = 0xD200
GET_BEGINNER_MISSION_USER_DATA_RESPONSE = 0xD201
GET_BEGINNER_MISSION_PROGRESSES_USER_DATA_LIST_REQUEST = 0xD202
GET_BEGINNER_MISSION_PROGRESSES_USER_DATA_LIST_RESPONSE = 0xD203
GET_BEGINNER_MISSION_SEAT_PROGRESSES_USER_DATA_LIST_REQUEST = 0xD204
GET_BEGINNER_MISSION_SEAT_PROGRESSES_USER_DATA_LIST_RESPONSE = 0xD205
BEGINNER_MISSION_AD_CONFIRM_NOTIFICATION_REQUEST = 0xD206
BEGINNER_MISSION_AD_CONFIRM_NOTIFICATION_RESPONSE = 0xD207
RECEIVE_BEGINNER_MISSION_REWARD_REQUEST = 0xD208
RECEIVE_BEGINNER_MISSION_REWARD_RESPONSE = 0xD209
GET_RES_EARN_CAMPAIGN_SHOP_USER_DATA_LIST_REQUEST = 0xD300
GET_RES_EARN_CAMPAIGN_SHOP_USER_DATA_LIST_RESPONSE = 0xD301
PURCHASE_RES_EARN_CAMPAIGN_SHOP_ITEM_REQUEST = 0xD302
PURCHASE_RES_EARN_CAMPAIGN_SHOP_ITEM_RESPONSE = 0xD303
PAYING_YUI_MEDAL_BONUS_GET_CHECK_REQUEST = 0xD304
PAYING_YUI_MEDAL_BONUS_GET_CHECK_RESPONSE = 0xD305
PAYING_YUI_MEDAL_BONUS_AD_CONFIRM_NOTIFICATION_REQUEST = 0xD306
PAYING_YUI_MEDAL_BONUS_AD_CONFIRM_NOTIFICATION_RESPONSE = 0xD307
GET_PLAY_CAMPAIGN_USER_DATA_LIST_REQUEST = 0xD308
GET_PLAY_CAMPAIGN_USER_DATA_LIST_RESPONSE = 0xD309
GET_PLAY_CAMPAIGN_STORE_USER_DATA_LIST_REQUEST = 0xD30A
GET_PLAY_CAMPAIGN_STORE_USER_DATA_LIST_RESPONSE = 0xD30B
GET_PLAY_CAMPAIGN_REWARD_USER_DATA_LIST_REQUEST = 0xD30C
GET_PLAY_CAMPAIGN_REWARD_USER_DATA_LIST_RESPONSE = 0xD30D
APPLY_FIRST_TICKET_PURCHASE_CAMPAIGN_REQUEST = 0xD30E
APPLY_FIRST_TICKET_PURCHASE_CAMPAIGN_RESPONSE = 0xD30F
GET_FIRST_TICKET_PURCHASE_CAMPAIGN_USER_DATA_REQUEST = 0xD310
GET_FIRST_TICKET_PURCHASE_CAMPAIGN_USER_DATA_RESPONSE = 0xD311
GET_LINKED_SITE_REG_CAMPAIGN_USER_DATA_REQUEST = 0xD312
GET_LINKED_SITE_REG_CAMPAIGN_USER_DATA_RESPONSE = 0xD313
GET_HERO_LOG_UNIT_USER_DATA_LIST_REQUEST = 0xD400
GET_HERO_LOG_UNIT_USER_DATA_LIST_RESPONSE = 0xD401
GET_CHARA_UNIT_USER_DATA_LIST_REQUEST = 0xD402
GET_CHARA_UNIT_USER_DATA_LIST_RESPONSE = 0xD403
BNID_SERIAL_CODE_CHECK_REQUEST = 0xD404
BNID_SERIAL_CODE_CHECK_RESPONSE = 0xD405
BNID_SERIAL_CODE_ENTRY_BY_APPENDIX_CARD_REQUEST = 0xD406
BNID_SERIAL_CODE_ENTRY_BY_APPENDIX_CARD_RESPONSE = 0xD407
GET_M_PLAYER_RANKS_REQUEST = 0xD500
GET_M_PLAYER_RANKS_RESPONSE = 0xD501
GET_M_TITLES_REQUEST = 0xD502
GET_M_TITLES_RESPONSE = 0xD503
GET_M_FRAGMENTS_REQUEST = 0xD504
GET_M_FRAGMENTS_RESPONSE = 0xD505
GET_M_REWARD_TABLES_REQUEST = 0xD506
GET_M_REWARD_TABLES_RESPONSE = 0xD507
GET_M_REWARD_SETS_REQUEST = 0xD508
GET_M_REWARD_SETS_RESPONSE = 0xD509
GET_M_UNANALYZED_LOG_GRADES_REQUEST = 0xD50A
GET_M_UNANALYZED_LOG_GRADES_RESPONSE = 0xD50B
GET_M_APPOINT_LEADER_PARAMS_REQUEST = 0xD50C
GET_M_APPOINT_LEADER_PARAMS_RESPONSE = 0xD50D
GET_M_APPOINT_LEADER_EFFECTS_REQUEST = 0xD50E
GET_M_APPOINT_LEADER_EFFECTS_RESPONSE = 0xD50F
GET_M_APPOINT_LEADER_EFFECT_TYPES_REQUEST = 0xD510
GET_M_APPOINT_LEADER_EFFECT_TYPES_RESPONSE = 0xD511
GET_M_RARITIES_REQUEST = 0xD512
GET_M_RARITIES_RESPONSE = 0xD513
GET_M_COMPOSITION_EVENTS_REQUEST = 0xD514
GET_M_COMPOSITION_EVENTS_RESPONSE = 0xD515
GET_M_COMPOSITION_PARAMS_REQUEST = 0xD516
GET_M_COMPOSITION_PARAMS_RESPONSE = 0xD517
GET_M_GAME_PLAY_PRICES_REQUEST = 0xD518
GET_M_GAME_PLAY_PRICES_RESPONSE = 0xD519
GET_M_BUY_TICKETS_REQUEST = 0xD51A
GET_M_BUY_TICKETS_RESPONSE = 0xD51B
GET_M_TIPS_REQUEST = 0xD51C
GET_M_TIPS_RESPONSE = 0xD51D
GET_M_CAPS_REQUEST = 0xD51E
GET_M_CAPS_RESPONSE = 0xD51F
GET_M_HERO_LOG_REQUEST = 0xD520
GET_M_HERO_LOG_RESPONSE = 0xD521
GET_M_HERO_LOG_LEVELS_REQUEST = 0xD522
GET_M_HERO_LOG_LEVELS_RESPONSE = 0xD523
GET_M_HERO_LOG_ROLES_REQUEST = 0xD524
GET_M_HERO_LOG_ROLES_RESPONSE = 0xD525
GET_M_HERO_LOG_TRUST_RANKS_REQUEST = 0xD526
GET_M_HERO_LOG_TRUST_RANKS_RESPONSE = 0xD527
GET_M_CHARAS_REQUEST = 0xD528
GET_M_CHARAS_RESPONSE = 0xD529
GET_M_CHARA_FRIENDLY_RANKS_REQUEST = 0xD52A
GET_M_CHARA_FRIENDLY_RANKS_RESPONSE = 0xD52B
GET_M_EQUIPMENTS_REQUEST = 0xD52C
GET_M_EQUIPMENTS_RESPONSE = 0xD52D
GET_M_EQUIPMENT_LEVELS_REQUEST = 0xD52E
GET_M_EQUIPMENT_LEVELS_RESPONSE = 0xD52F
GET_M_WEAPON_TYPES_REQUEST = 0xD530
GET_M_WEAPON_TYPES_RESPONSE = 0xD531
GET_M_ITEMS_REQUEST = 0xD532
GET_M_ITEMS_RESPONSE = 0xD533
GET_M_ITEM_TYPES_REQUEST = 0xD534
GET_M_ITEM_TYPES_RESPONSE = 0xD535
GET_M_BUFF_ITEMS_REQUEST = 0xD536
GET_M_BUFF_ITEMS_RESPONSE = 0xD537
GET_M_ENEMIES_REQUEST = 0xD538
GET_M_ENEMIES_RESPONSE = 0xD539
GET_M_ENEMY_SETS_REQUEST = 0xD53A
GET_M_ENEMY_SETS_RESPONSE = 0xD53B
GET_M_ENEMY_KINDS_REQUEST = 0xD53C
GET_M_ENEMY_KINDS_RESPONSE = 0xD53D
GET_M_ENEMY_CATEGORIES_REQUEST = 0xD53E
GET_M_ENEMY_CATEGORIES_RESPONSE = 0xD53F
GET_M_UNITS_REQUEST = 0xD540
GET_M_UNITS_RESPONSE = 0xD541
GET_M_UNIT_GIMMICKS_REQUEST = 0xD542
GET_M_UNIT_GIMMICKS_RESPONSE = 0xD543
GET_M_UNIT_COLLISIONS_REQUEST = 0xD544
GET_M_UNIT_COLLISIONS_RESPONSE = 0xD545
GET_M_UNIT_POWERS_REQUEST = 0xD546
GET_M_UNIT_POWERS_RESPONSE = 0xD547
GET_M_GIMMICK_ATTACKS_REQUEST = 0xD548
GET_M_GIMMICK_ATTACKS_RESPONSE = 0xD549
GET_M_CHARA_ATTACKS_REQUEST = 0xD54A
GET_M_CHARA_ATTACKS_RESPONSE = 0xD54B
GET_M_BOSS_ATTACKS_REQUEST = 0xD54C
GET_M_BOSS_ATTACKS_RESPONSE = 0xD54D
GET_M_MONSTER_ATTACKS_REQUEST = 0xD54E
GET_M_MONSTER_ATTACKS_RESPONSE = 0xD54F
GET_M_MONSTER_ACTIONS_REQUEST = 0xD550
GET_M_MONSTER_ACTIONS_RESPONSE = 0xD551
GET_M_PROPERTIES_REQUEST = 0xD552
GET_M_PROPERTIES_RESPONSE = 0xD553
GET_M_PROPERTY_TABLES_REQUEST = 0xD554
GET_M_PROPERTY_TABLES_RESPONSE = 0xD555
GET_M_PROPERTY_TYPES_REQUEST = 0xD556
GET_M_PROPERTY_TYPES_RESPONSE = 0xD557
GET_M_SKILLS_REQUEST = 0xD558
GET_M_SKILLS_RESPONSE = 0xD559
GET_M_SKILL_TABLES_REQUEST = 0xD55A
GET_M_SKILL_TABLES_RESPONSE = 0xD55B
GET_M_SKILL_LEVELS_REQUEST = 0xD55C
GET_M_SKILL_LEVELS_RESPONSE = 0xD55D
GET_M_AWAKENINGS_REQUEST = 0xD55E
GET_M_AWAKENINGS_RESPONSE = 0xD55F
GET_M_SYNCHRO_SKILLS_REQUEST = 0xD560
GET_M_SYNCHRO_SKILLS_RESPONSE = 0xD561
GET_M_SOUND_SKILL_CUT_IN_VOICES_REQUEST = 0xD562
GET_M_SOUND_SKILL_CUT_IN_VOICES_RESPONSE = 0xD563
GET_M_QUEST_SCENES_REQUEST = 0xD564
GET_M_QUEST_SCENES_RESPONSE = 0xD565
GET_M_QUEST_EXIST_UNITS_REQUEST = 0xD566
GET_M_QUEST_EXIST_UNITS_RESPONSE = 0xD567
GET_M_QUEST_EPISODE_APPEND_REWARDS_REQUEST = 0xD568
GET_M_QUEST_EPISODE_APPEND_REWARDS_RESPONSE = 0xD569
GET_M_SIDE_QUESTS_REQUEST = 0xD56A
GET_M_SIDE_QUESTS_RESPONSE = 0xD56B
GET_M_EPISODES_REQUEST = 0xD56C
GET_M_EPISODES_RESPONSE = 0xD56D
GET_M_EPISODE_CHAPTERS_REQUEST = 0xD56E
GET_M_EPISODE_CHAPTERS_RESPONSE = 0xD56F
GET_M_EPISODE_PARTS_REQUEST = 0xD570
GET_M_EPISODE_PARTS_RESPONSE = 0xD571
GET_M_TRIAL_TOWERS_REQUEST = 0xD572
GET_M_TRIAL_TOWERS_RESPONSE = 0xD573
GET_M_EX_TOWERS_REQUEST = 0xD574
GET_M_EX_TOWERS_RESPONSE = 0xD575
GET_M_EX_TOWER_QUESTS_REQUEST = 0xD576
GET_M_EX_TOWER_QUESTS_RESPONSE = 0xD577
GET_M_MENU_DISPLAY_ENEMIES_REQUEST = 0xD578
GET_M_MENU_DISPLAY_ENEMIES_RESPONSE = 0xD579
GET_M_MISSIONS_REQUEST = 0xD57A
GET_M_MISSIONS_RESPONSE = 0xD57B
GET_M_MISSION_TABLES_REQUEST = 0xD57C
GET_M_MISSION_TABLES_RESPONSE = 0xD57D
GET_M_MISSION_DIFFICULTIES_REQUEST = 0xD57E
GET_M_MISSION_DIFFICULTIES_RESPONSE = 0xD57F
GET_M_BATTLE_CAMERAS_REQUEST = 0xD580
GET_M_BATTLE_CAMERAS_RESPONSE = 0xD581
GET_M_CHAT_MAIN_STORIES_REQUEST = 0xD582
GET_M_CHAT_MAIN_STORIES_RESPONSE = 0xD583
GET_M_CHAT_SIDE_STORIES_REQUEST = 0xD584
GET_M_CHAT_SIDE_STORIES_RESPONSE = 0xD585
GET_M_CHAT_EVENT_STORIES_REQUEST = 0xD586
GET_M_CHAT_EVENT_STORIES_RESPONSE = 0xD587
GET_M_NAVIGATOR_CHARAS_REQUEST = 0xD588
GET_M_NAVIGATOR_CHARAS_RESPONSE = 0xD589
GET_M_NAVIGATOR_COMMENTS_REQUEST = 0xD58A
GET_M_NAVIGATOR_COMMENTS_RESPONSE = 0xD58B
GET_M_EX_BONUS_TABLES_REQUEST = 0xD58C
GET_M_EX_BONUS_TABLES_RESPONSE = 0xD58D
GET_M_EX_BONUS_CONDITIONS_REQUEST = 0xD58E
GET_M_EX_BONUS_CONDITIONS_RESPONSE = 0xD58F
GET_M_QUEST_RARE_DROPS_REQUEST = 0xD590
GET_M_QUEST_RARE_DROPS_RESPONSE = 0xD591
GET_M_QUEST_SPECIAL_RARE_DROP_SETTINGS_REQUEST = 0xD592
GET_M_QUEST_SPECIAL_RARE_DROP_SETTINGS_RESPONSE = 0xD593
GET_M_QUEST_SPECIAL_RARE_DROPS_REQUEST = 0xD594
GET_M_QUEST_SPECIAL_RARE_DROPS_RESPONSE = 0xD595
GET_M_QUEST_TUTORIALS_REQUEST = 0xD596
GET_M_QUEST_TUTORIALS_RESPONSE = 0xD597
GET_M_QUEST_PLAYER_TRACE_TABLES_REQUEST = 0xD598
GET_M_QUEST_PLAYER_TRACE_TABLES_RESPONSE = 0xD599
GET_M_QUEST_STILLS_REQUEST = 0xD59A
GET_M_QUEST_STILLS_RESPONSE = 0xD59B
GET_M_GASHAS_REQUEST = 0xD59C
GET_M_GASHAS_RESPONSE = 0xD59D
GET_M_GASHA_HEADERS_REQUEST = 0xD59E
GET_M_GASHA_HEADERS_RESPONSE = 0xD59F
GET_M_GASHA_LOTTERY_RARITIES_REQUEST = 0xD5A0
GET_M_GASHA_LOTTERY_RARITIES_RESPONSE = 0xD5A1
GET_M_GASHA_PRIZES_REQUEST = 0xD5A2
GET_M_GASHA_PRIZES_RESPONSE = 0xD5A3
GET_M_COMEBACK_EVENTS_REQUEST = 0xD5A4
GET_M_COMEBACK_EVENTS_RESPONSE = 0xD5A5
GET_M_AD_BANNERS_REQUEST = 0xD5A6
GET_M_AD_BANNERS_RESPONSE = 0xD5A7
GET_M_EVENTS_REQUEST = 0xD5A8
GET_M_EVENTS_RESPONSE = 0xD5A9
GET_M_TREASURE_HUNTS_REQUEST = 0xD5AA
GET_M_TREASURE_HUNTS_RESPONSE = 0xD5AB
GET_M_TREASURE_HUNT_WHOLE_TASKS_REQUEST = 0xD5AC
GET_M_TREASURE_HUNT_WHOLE_TASKS_RESPONSE = 0xD5AD
GET_M_TREASURE_HUNT_INDIVIDUAL_TASKS_REQUEST = 0xD5AE
GET_M_TREASURE_HUNT_INDIVIDUAL_TASKS_RESPONSE = 0xD5AF
GET_M_TREASURE_HUNT_SPECIAL_EFFECTS_REQUEST = 0xD5B0
GET_M_TREASURE_HUNT_SPECIAL_EFFECTS_RESPONSE = 0xD5B1
GET_M_TREASURE_HUNT_EVENT_POINT_REWARD_COMMON_REWARDS_REQUEST = 0xD5B2
GET_M_TREASURE_HUNT_EVENT_POINT_REWARD_COMMON_REWARDS_RESPONSE = 0xD5B3
GET_M_TREASURE_HUNT_EVENT_POINT_REWARD_TITLES_REQUEST = 0xD5B4
GET_M_TREASURE_HUNT_EVENT_POINT_REWARD_TITLES_RESPONSE = 0xD5B5
GET_M_TREASURE_HUNT_TASK_TEXTS_REQUEST = 0xD5B6
GET_M_TREASURE_HUNT_TASK_TEXTS_RESPONSE = 0xD5B7
GET_M_BNID_SERIAL_CODES_REQUEST = 0xD5B8
GET_M_BNID_SERIAL_CODES_RESPONSE = 0xD5B9
GET_M_BNID_SERIAL_CODE_REWARDS_REQUEST = 0xD5BA
GET_M_BNID_SERIAL_CODE_REWARDS_RESPONSE = 0xD5BB
GET_M_SUPPORT_LOG_REQUEST = 0xD5BC
GET_M_SUPPORT_LOG_RESPONSE = 0xD5BD
GET_M_SUPPORT_LOG_TYPES_REQUEST = 0xD5BE
GET_M_SUPPORT_LOG_TYPES_RESPONSE = 0xD5BF
GET_M_EPISODE_APPENDS_REQUEST = 0xD5C0
GET_M_EPISODE_APPENDS_RESPONSE = 0xD5C1
GET_M_QUEST_DEFRAG_MATCH_QUESTS_REQUEST = 0xD5C2
GET_M_QUEST_DEFRAG_MATCH_QUESTS_RESPONSE = 0xD5C3
GET_M_QUEST_DEFRAG_MATCH_QUEST_BOSS_TABLES_REQUEST = 0xD5C4
GET_M_QUEST_DEFRAG_MATCH_QUEST_BOSS_TABLES_RESPONSE = 0xD5C5
GET_M_DEFRAG_MATCHES_REQUEST = 0xD5C6
GET_M_DEFRAG_MATCHES_RESPONSE = 0xD5C7
GET_M_DEFRAG_MATCH_SEED_REQUEST = 0xD5C8
GET_M_DEFRAG_MATCH_SEED_RESPONSE = 0xD5C9
GET_M_DEFRAG_MATCH_SPECIAL_EFFECTS_REQUEST = 0xD5CA
GET_M_DEFRAG_MATCH_SPECIAL_EFFECTS_RESPONSE = 0xD5CB
GET_M_DEFRAG_MATCH_GRADES_REQUEST = 0xD5CC
GET_M_DEFRAG_MATCH_GRADES_RESPONSE = 0xD5CD
GET_M_DEFRAG_MATCH_CPU_UNITS_REQUEST = 0xD5CE
GET_M_DEFRAG_MATCH_CPU_UNITS_RESPONSE = 0xD5CF
GET_M_DEFRAG_MATCH_CPU_SUPPORT_LOGS_REQUEST = 0xD5D0
GET_M_DEFRAG_MATCH_CPU_SUPPORT_LOGS_RESPONSE = 0xD5D1
GET_M_DEFRAG_MATCH_PERIOD_BONUSES_REQUEST = 0xD5D2
GET_M_DEFRAG_MATCH_PERIOD_BONUSES_RESPONSE = 0xD5D3
GET_M_DEFRAG_MATCH_RANDOM_BONUS_TABLES_REQUEST = 0xD5D4
GET_M_DEFRAG_MATCH_RANDOM_BONUS_TABLES_RESPONSE = 0xD5D5
GET_M_DEFRAG_MATCH_RANDOM_BONUS_CONDITIONS_REQUEST = 0xD5D6
GET_M_DEFRAG_MATCH_RANDOM_BONUS_CONDITIONS_RESPONSE = 0xD5D7
GET_M_DEFRAG_MATCH_RARE_DROPS_REQUEST = 0xD5D8
GET_M_DEFRAG_MATCH_RARE_DROPS_RESPONSE = 0xD5D9
GET_M_YUI_MEDAL_SHOPS_REQUEST = 0xD5DA
GET_M_YUI_MEDAL_SHOPS_RESPONSE = 0xD5DB
GET_M_YUI_MEDAL_SHOP_ITEMS_REQUEST = 0xD5DC
GET_M_YUI_MEDAL_SHOP_ITEMS_RESPONSE = 0xD5DD
GET_M_EVENT_SCENES_REQUEST = 0xD5DE
GET_M_EVENT_SCENES_RESPONSE = 0xD5DF
GET_M_GENERIC_CAMPAIGN_PERIODS_REQUEST = 0xD5E0
GET_M_GENERIC_CAMPAIGN_PERIODS_RESPONSE = 0xD5E1
GET_M_BEGINNER_MISSIONS_REQUEST = 0xD5E2
GET_M_BEGINNER_MISSIONS_RESPONSE = 0xD5E3
GET_M_BEGINNER_MISSION_CONDITIONS_REQUEST = 0xD5E4
GET_M_BEGINNER_MISSION_CONDITIONS_RESPONSE = 0xD5E5
GET_M_BEGINNER_MISSION_REWARDS_REQUEST = 0xD5E6
GET_M_BEGINNER_MISSION_REWARDS_RESPONSE = 0xD5E7
GET_M_BEGINNER_MISSION_SEAT_CONDITIONS_REQUEST = 0xD5E8
GET_M_BEGINNER_MISSION_SEAT_CONDITIONS_RESPONSE = 0xD5E9
GET_M_BEGINNER_MISSION_SEAT_REWARDS_REQUEST = 0xD5EA
GET_M_BEGINNER_MISSION_SEAT_REWARDS_RESPONSE = 0xD5EB
GET_M_EVENT_ITEMS_REQUEST = 0xD5EC
GET_M_EVENT_ITEMS_RESPONSE = 0xD5ED
GET_M_EVENT_MONSTERS_REQUEST = 0xD5EE
GET_M_EVENT_MONSTERS_RESPONSE = 0xD5EF
GET_M_YUI_MEDAL_BONUSES_REQUEST = 0xD5F0
GET_M_YUI_MEDAL_BONUSES_RESPONSE = 0xD5F1
GET_M_YUI_MEDAL_BONUS_CONDITIONS_REQUEST = 0xD5F2
GET_M_YUI_MEDAL_BONUS_CONDITIONS_RESPONSE = 0xD5F3
GET_M_GASHA_MEDALS_REQUEST = 0xD5F4
GET_M_GASHA_MEDALS_RESPONSE = 0xD5F5
GET_M_GASHA_MEDAL_TYPES_REQUEST = 0xD5F6
GET_M_GASHA_MEDAL_TYPES_RESPONSE = 0xD5F7
GET_M_GASHA_MEDAL_SETTINGS_REQUEST = 0xD5F8
GET_M_GASHA_MEDAL_SETTINGS_RESPONSE = 0xD5F9
GET_M_GASHA_MEDAL_BONUSES_REQUEST = 0xD5FA
GET_M_GASHA_MEDAL_BONUSES_RESPONSE = 0xD5FB
GET_M_GASHA_MEDAL_SHOPS_REQUEST = 0xD5FC
GET_M_GASHA_MEDAL_SHOPS_RESPONSE = 0xD5FD
GET_M_GASHA_MEDAL_SHOP_ITEMS_REQUEST = 0xD5FE
GET_M_GASHA_MEDAL_SHOP_ITEMS_RESPONSE = 0xD5FF
GET_M_RES_EARN_CAMPAIGN_APPLICATIONS_REQUEST = 0xD600
GET_M_RES_EARN_CAMPAIGN_APPLICATIONS_RESPONSE = 0xD601
GET_M_RES_EARN_CAMPAIGN_APPLICATION_PRODUCTS_REQUEST = 0xD602
GET_M_RES_EARN_CAMPAIGN_APPLICATION_PRODUCTS_RESPONSE = 0xD603
GET_M_RES_EARN_CAMPAIGN_SHOPS_REQUEST = 0xD604
GET_M_RES_EARN_CAMPAIGN_SHOPS_RESPONSE = 0xD605
GET_M_RES_EARN_CAMPAIGN_SHOP_ITEMS_REQUEST = 0xD606
GET_M_RES_EARN_CAMPAIGN_SHOP_ITEMS_RESPONSE = 0xD607
GET_M_PAYING_YUI_MEDAL_BONUSES_REQUEST = 0xD608
GET_M_PAYING_YUI_MEDAL_BONUSES_RESPONSE = 0xD609
GET_M_AC_LOGIN_BONUSES_REQUEST = 0xD60A
GET_M_AC_LOGIN_BONUSES_RESPONSE = 0xD60B
GET_M_PLAY_CAMPAIGNS_REQUEST = 0xD60C
GET_M_PLAY_CAMPAIGNS_RESPONSE = 0xD60D
GET_M_PLAY_CAMPAIGN_REWARDS_REQUEST = 0xD60E
GET_M_PLAY_CAMPAIGN_REWARDS_RESPONSE = 0xD60F
GET_M_GASHA_FREE_CAMPAIGNS_REQUEST = 0xD610
GET_M_GASHA_FREE_CAMPAIGNS_RESPONSE = 0xD611
GET_M_QUEST_DROP_BOOST_CAMPAIGNS_REQUEST = 0xD612
GET_M_QUEST_DROP_BOOST_CAMPAIGNS_RESPONSE = 0xD613
GET_M_FIRST_TICKET_PURCHASE_CAMPAIGNS_REQUEST = 0xD614
GET_M_FIRST_TICKET_PURCHASE_CAMPAIGNS_RESPONSE = 0xD615
GET_M_LINKED_SITE_REG_CAMPAIGNS_REQUEST = 0xD616
GET_M_LINKED_SITE_REG_CAMPAIGNS_RESPONSE = 0xD617
GET_M_LINKED_SITE_REG_CAMPAIGN_REWARDS_REQUEST = 0xD618
GET_M_LINKED_SITE_REG_CAMPAIGN_REWARDS_RESPONSE = 0xD619

View File

@ -0,0 +1,3 @@
// AcLoginBonusId,Title,RewardSetSubId,OpenStartDate,OpenEndDate
1,"2020年7月9日アニメリコリス記念",4,"2020/07/09 7:00:00","2020/10/01 2:00:00"
2,"2020年10月6日秋のデビューカムバックCP",5,"2020/10/06 7:00:00","2020/12/01 2:00:00"
1 // AcLoginBonusId Title RewardSetSubId OpenStartDate OpenEndDate
2 1 2020年7月9日~(アニメ&リコリス記念) 4 2020/07/09 7:00:00 2020/10/01 2:00:00
3 2 2020年10月6日~(秋のデビュー&カムバックCP) 5 2020/10/06 7:00:00 2020/12/01 2:00:00

View File

@ -0,0 +1 @@
// AdBannerId,Category,SortNum,StartDate,EndDate,ActiveFlag
1 // AdBannerId Category SortNum StartDate EndDate ActiveFlag

View File

@ -0,0 +1,14 @@
// AppointLeaderEffectId,CharaId,InfoTextFormat,AppointLeaderEffectTypeId,LowEffectValue,MiddleEffectValue,HighEffectValue,MaxEffectValue
1,1,"攻撃スキル威力{0}倍",1,1,1.3,1.4,1.5
2,2,"全ステータス上昇{0}倍",2,1,1.1,1.2,1.3
3,3,"Mob撃破時のスキルEXP上昇{0}倍",3,1,1.1,1.2,1.3
4,4,"後衛時のHP回復速度上昇{0}倍",4,1,1.2,1.3,1.4
5,5,"獲得EXPアップ{0}倍",5,1,1.1,1.2,1.3
6,6,"ピナのスキルのクールタイム短縮{0}倍",6,1,0.9,0.8,0.7
7,7,"攻撃スキルクールタイム短縮{0}倍",7,1,0.9,0.8,0.7
8,8,"獲得Colアップ{0}倍",8,1,1.5,1.6,1.7
9,9,"通常攻撃威力{0}倍",9,1,1.5,1.7,2
10,10,"補助スキルクールタイム短縮{0}倍",10,1,0.9,0.8,0.7
11,11,"シンクロスキル効果時間{0}倍",11,1,1.2,1.5,2
12,12,"青薔薇による継続ダメージ{0}倍",12,1,2,3,4
18,18,"全スキルクールタイム短縮{0}倍",18,1,0.95,0.9,0.85
1 // AppointLeaderEffectId CharaId InfoTextFormat AppointLeaderEffectTypeId LowEffectValue MiddleEffectValue HighEffectValue MaxEffectValue
2 1 1 攻撃スキル威力{0}倍 1 1 1.3 1.4 1.5
3 2 2 全ステータス上昇{0}倍 2 1 1.1 1.2 1.3
4 3 3 Mob撃破時のスキルEXP上昇{0}倍 3 1 1.1 1.2 1.3
5 4 4 後衛時のHP回復速度上昇{0}倍 4 1 1.2 1.3 1.4
6 5 5 獲得EXPアップ{0}倍 5 1 1.1 1.2 1.3
7 6 6 ピナのスキルのクールタイム短縮{0}倍 6 1 0.9 0.8 0.7
8 7 7 攻撃スキルクールタイム短縮{0}倍 7 1 0.9 0.8 0.7
9 8 8 獲得Colアップ{0}倍 8 1 1.5 1.6 1.7
10 9 9 通常攻撃威力{0}倍 9 1 1.5 1.7 2
11 10 10 補助スキルクールタイム短縮{0}倍 10 1 0.9 0.8 0.7
12 11 11 シンクロスキル効果時間{0}倍 11 1 1.2 1.5 2
13 12 12 青薔薇による継続ダメージ{0}倍 12 1 2 3 4
14 18 18 全スキルクールタイム短縮{0}倍 18 1 0.95 0.9 0.85

View File

@ -0,0 +1,14 @@
// AppointLeaderEffectTypeId,Name
1,"攻撃スキル威力上昇"
2,"全ステータス上昇"
3,"雑魚撃破時のスキルEXP上昇"
4,"後衛時のHP回復速度上昇"
5,"獲得EXPアップ"
6,"ピナのスキルのクールタイム短縮"
7,"超攻撃スキルクールタイム短縮"
8,"獲得colアップ"
9,"通常攻撃威力上昇"
10,"補助スキルクールタイム短縮"
11,"シンクロスキル延長"
12,"青薔薇強化"
18,"全スキルクールタイム短縮"
1 // AppointLeaderEffectTypeId Name
2 1 攻撃スキル威力上昇
3 2 全ステータス上昇
4 3 雑魚撃破時のスキルEXP上昇
5 4 後衛時のHP回復速度上昇
6 5 獲得EXPアップ
7 6 ピナのスキルのクールタイム短縮
8 7 超攻撃スキルクールタイム短縮
9 8 獲得colアップ
10 9 通常攻撃威力上昇
11 10 補助スキルクールタイム短縮
12 11 シンクロスキル延長
13 12 青薔薇強化
14 18 全スキルクールタイム短縮

View File

@ -0,0 +1,2 @@
// AppointLeaderParamId,InitialSynchroRate,AppointLeaderIncrementSynchroRate,AwakeningIncrementSynchroRate,FoilAddSynchroRate,AppointLeaderTrustBonus
1,30,1,5,10,1
1 // AppointLeaderParamId InitialSynchroRate AppointLeaderIncrementSynchroRate AwakeningIncrementSynchroRate FoilAddSynchroRate AppointLeaderTrustBonus
2 1 30 1 5 10 1

View File

@ -0,0 +1,6 @@
// AwakeningId,TotalExp,BonusHeroLog,BonusWeapon,BonusArmor
1,100,0.05,0.1,0.1
2,200,0.1,0.2,0.2
3,300,0.15,0.3,0.3
4,400,0.2,0.4,0.4
5,500,0.25,0.5,0.5
1 // AwakeningId TotalExp BonusHeroLog BonusWeapon BonusArmor
2 1 100 0.05 0.1 0.1
3 2 200 0.1 0.2 0.2
4 3 300 0.15 0.3 0.3
5 4 400 0.2 0.4 0.4
6 5 500 0.25 0.5 0.5

View File

@ -0,0 +1,4 @@
// BattleCameraId,OffsetX,OffsetY,OffsetZ,RotH,RotV,Distance,Near,Far,Fov
1,0,1.5,0,50,35,60,1,250,10
2,0,1.5,0,50,35,80,1,250,10
3,0,1.5,0,50,35,90,1,250,10
1 // BattleCameraId OffsetX OffsetY OffsetZ RotH RotV Distance Near Far Fov
2 1 0 1.5 0 50 35 60 1 250 10
3 2 0 1.5 0 50 35 80 1 250 10
4 3 0 1.5 0 50 35 90 1 250 10

View File

@ -0,0 +1,10 @@
// BeginnerMissionConditionId,BeginnerMissionId,SeatNum,MissionNum,DisplayContent,DisplayTips,ConditionType,ConditionParam1,ConditionParam2,ConditionParam3,RequiredAchievementNum
10001,10000,1,1,"武器を装備しよう","《ViSTerステーション》のカスタムの<br>パーティ編成で、キャラクターに<br>武器を装備させよう",1,"0","0","0",1
10002,10000,1,2,"副装備を装備しよう","《ViSTerステーション》のカスタムの<br>パーティ編成で、キャラクターに<br>副装備を装備させよう",2,"0","0","0",1
10003,10000,1,3,"強化合成をしよう","《ViSTerステーション》のカスタムの<br>合成の強化合成で、アイテム等を<br>消費してキャラクターを強化しよう",3,"0","0","0",1
10004,10000,1,4,"リソースカードを印刷しよう","《ViSTerターミナル》のガシャ後、<br>もしくはカード印刷で、リソースを<br>印刷し、覚醒を解放しよう",4,"0","0","0",1
10005,10000,1,5,"換金アイテムを<br>売却しよう","《ViSTerステーション》のカスタムの<br>ストレージ整理やショップのリソース<br>売却で換金アイテムを売却しよう",5,"0","0","0",1
10006,10000,1,6,"ショップのリソース複製で <br>リソースを購入しよう","《ViSTerステーション》のショップの<br>リソース複製でリソースカードを<br>読み込み、リソースを購入しよう",6,"0","0","0",1
10007,10000,1,7,"覚醒させよう","《ViSTerステーション》の合成で<br>覚醒を解放したリソースに、<br>同種リソースを合成しよう",7,"0","0","0",1
10008,10000,1,8,"能力合成をしよう","《ViSTerステーション》のカスタムの<br>合成の能力合成で、アイテムを消費してリソースにプロパティを付与しよう",8,"0","0","0",1
10009,10000,1,9,"トライアルタワーを<br> 1回クリアしよう","《ViSTerステーション》の<br>トライアルタワーのクエストを<br>1回クリアしよう",9,"0","0","0",1
1 // BeginnerMissionConditionId BeginnerMissionId SeatNum MissionNum DisplayContent DisplayTips ConditionType ConditionParam1 ConditionParam2 ConditionParam3 RequiredAchievementNum
2 10001 10000 1 1 武器を装備しよう 《ViSTerステーション》のカスタムの<br>パーティ編成で、キャラクターに<br>武器を装備させよう 1 0 0 0 1
3 10002 10000 1 2 副装備を装備しよう 《ViSTerステーション》のカスタムの<br>パーティ編成で、キャラクターに<br>副装備を装備させよう 2 0 0 0 1
4 10003 10000 1 3 強化合成をしよう 《ViSTerステーション》のカスタムの<br>合成の強化合成で、アイテム等を<br>消費してキャラクターを強化しよう 3 0 0 0 1
5 10004 10000 1 4 リソースカードを印刷しよう 《ViSTerターミナル》のガシャ後、<br>もしくはカード印刷で、リソースを<br>印刷し、覚醒を解放しよう 4 0 0 0 1
6 10005 10000 1 5 換金アイテムを<br>売却しよう 《ViSTerステーション》のカスタムの<br>ストレージ整理やショップのリソース<br>売却で換金アイテムを売却しよう 5 0 0 0 1
7 10006 10000 1 6 ショップのリソース複製で <br>リソースを購入しよう 《ViSTerステーション》のショップの<br>リソース複製でリソースカードを<br>読み込み、リソースを購入しよう 6 0 0 0 1
8 10007 10000 1 7 覚醒させよう 《ViSTerステーション》の合成で<br>覚醒を解放したリソースに、<br>同種リソースを合成しよう 7 0 0 0 1
9 10008 10000 1 8 能力合成をしよう 《ViSTerステーション》のカスタムの<br>合成の能力合成で、アイテムを消費してリソースにプロパティを付与しよう 8 0 0 0 1
10 10009 10000 1 9 トライアルタワーを<br> 1回クリアしよう 《ViSTerステーション》の<br>トライアルタワーのクエストを<br>1回クリアしよう 9 0 0 0 1

View File

@ -0,0 +1,10 @@
// BeginnerMissionRewardId,BeginnerMissionId,BeginnerMissionConditionId,CommonRewardType,CommonRewardId,CommonRewardNum,Strength,Property1PropertyId,Property1Value1,Property1Value2,Property2PropertyId,Property2Value1,Property2Value2,Property3PropertyId,Property3Value1,Property3Value2,Property4PropertyId,Property4Value1,Property4Value2
1,10000,10001,3,180004,1,0,1,0,0,1,0,0,1,0,0,1,0,0
2,10000,10002,3,180004,2,0,1,0,0,1,0,0,1,0,0,1,0,0
3,10000,10003,1,101000230,1,0,1,0,0,1,0,0,2,0,0,2,0,0
4,10000,10004,3,180004,1,0,1,0,0,1,0,0,1,0,0,1,0,0
5,10000,10005,3,180004,2,0,1,0,0,1,0,0,1,0,0,1,0,0
6,10000,10006,3,170004,1,0,1,0,0,1,0,0,1,0,0,1,0,0
7,10000,10007,3,110240,1,0,1,0,0,1,0,0,1,0,0,1,0,0
8,10000,10008,3,170004,1,0,1,0,0,1,0,0,1,0,0,1,0,0
9,10000,10009,1,101000230,1,0,1,0,0,1,0,0,2,0,0,2,0,0
1 // BeginnerMissionRewardId BeginnerMissionId BeginnerMissionConditionId CommonRewardType CommonRewardId CommonRewardNum Strength Property1PropertyId Property1Value1 Property1Value2 Property2PropertyId Property2Value1 Property2Value2 Property3PropertyId Property3Value1 Property3Value2 Property4PropertyId Property4Value1 Property4Value2
2 1 10000 10001 3 180004 1 0 1 0 0 1 0 0 1 0 0 1 0 0
3 2 10000 10002 3 180004 2 0 1 0 0 1 0 0 1 0 0 1 0 0
4 3 10000 10003 1 101000230 1 0 1 0 0 1 0 0 2 0 0 2 0 0
5 4 10000 10004 3 180004 1 0 1 0 0 1 0 0 1 0 0 1 0 0
6 5 10000 10005 3 180004 2 0 1 0 0 1 0 0 1 0 0 1 0 0
7 6 10000 10006 3 170004 1 0 1 0 0 1 0 0 1 0 0 1 0 0
8 7 10000 10007 3 110240 1 0 1 0 0 1 0 0 1 0 0 1 0 0
9 8 10000 10008 3 170004 1 0 1 0 0 1 0 0 1 0 0 1 0 0
10 9 10000 10009 1 101000230 1 0 1 0 0 1 0 0 2 0 0 2 0 0

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