33 Commits

Author SHA1 Message Date
Hay1tsme
257b0dae87 diva: fix render_POST 2024-01-12 12:18:23 -05:00
Hay1tsme
c6f2f7e0ef Merge branch 'develop' into diva_handler_classes 2024-01-12 12:15:28 -05:00
Hay1tsme
6b158bf18d diva: fix StageResultResponse 2023-12-06 18:24:20 -05:00
Hay1tsme
1a616cba41 diva: change how requests are decoded 2023-11-26 20:43:10 -05:00
Hay1tsme
d2d02f9483 diva: add list decoders 2023-11-26 19:24:36 -05:00
Hay1tsme
024bc352d7 diva: fix attend response 2023-11-26 02:43:49 -05:00
Hay1tsme
4831aa5628 diva: add encoder helper methods 2023-11-26 01:41:25 -05:00
Hay1tsme
dd1c7667e0 diva: fix ip logging 2023-11-20 11:29:10 -05:00
Hay1tsme
2ec1dcadef Merge branch 'develop' into diva_handler_classes 2023-11-20 10:49:48 -05:00
Hay1tsme
81c13f5008 diva: fix typing 2023-11-08 21:23:54 -05:00
Hay1tsme
572b8ddbe5 Merge branch 'develop' into diva_handler_classes 2023-11-08 21:22:24 -05:00
Kevin Trocolli
2b02ed8684 diva: add stage_result, end handlers 2023-10-05 23:47:50 -04:00
Hay1tsme
b2a01d20d5 diva: add configurable banner_msg 2023-10-05 12:29:02 -04:00
Kevin Trocolli
5e03193819 diva: fix start, spend_credit, and get_pv_pd requests 2023-10-04 23:58:26 -04:00
Kevin Trocolli
f836b5dd21 diva: fix start request 2023-10-04 23:25:10 -04:00
Kevin Trocolli
53c74c6511 diva: add register, fixes, start still busted 2023-10-04 02:18:04 -04:00
Kevin Trocolli
7322bc60dc Merge branch 'develop' into diva_handler_classes 2023-10-04 00:53:10 -04:00
Hay1tsme
c2ee09bce0 diva: integrate latest changes 2023-05-02 11:41:35 -04:00
Hay1tsme
dfea392b03 Merge branch 'develop' into diva_handler_classes 2023-05-02 11:36:14 -04:00
Kevin Trocolli
998aa70929 diva: fix for responses that haven't been updated yet 2023-04-01 23:45:28 -04:00
Kevin Trocolli
8df1325613 diva: implement StartResponse 2023-04-01 23:19:44 -04:00
Kevin Trocolli
6ff7827918 diva: fill out StartResponse 2023-04-01 23:05:10 -04:00
Kevin Trocolli
a36170f2c3 Merge branch 'develop' into diva_handler_classes 2023-04-01 22:23:13 -04:00
Kevin Trocolli
48144b53f0 fix attend 2023-03-17 01:35:51 -04:00
Kevin Trocolli
3b852ea739 Merge branch 'develop' into diva_handler_classes 2023-03-16 22:32:18 -04:00
Kevin Trocolli
21c9b23617 diva: add game_init and attend handlers 2023-03-13 04:04:08 -04:00
Kevin Trocolli
3076bdf575 Merge branch 'develop' into diva_handler_classes 2023-03-13 02:27:12 -04:00
Hay1tsme
5be25f89ff format with black 2023-03-09 11:50:11 -05:00
Hay1tsme
5443d24352 Merge branch 'develop' into diva_handler_classes 2023-03-09 11:49:51 -05:00
Hay1tsme
56927c049f clean up index.py 2023-02-26 11:40:13 -05:00
Hay1tsme
2b81ba206c remove unused code from index.py 2023-02-25 23:40:50 -05:00
Hay1tsme
9718e822f3 add commas to urlencode safe param 2023-02-25 19:12:48 -05:00
Hay1tsme
b07e3d6a94 add base request/response classes 2023-02-25 18:50:35 -05:00
309 changed files with 27510 additions and 108700 deletions

View File

@@ -1,13 +0,0 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.

View File

@@ -1,111 +1,6 @@
# Changelog # Changelog
Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to. Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to.
## 20240811
### System
+ Change backend from Twisted to Starlette
+ Implement async handlers
+ Reboot times for multiple games have been fixed (thanks zaphkito!)
### Frontend
+ Edit button changed to View on the user page, and is where you can edit the card memo
+ Add card now works as it should
+ Add event log viewer in the `sys` page for sysadmins
+ Add pages for Pokken, SAO, and maimai
### AimeDB
+ Now rejects all-zero access codes
+ Stores card IDm (for AmusementIC) and MiFare ID (for old aime/banapass)
+ ...unless that MiFare ID is 0x01020304 (the default for segatools)
### maimai
+ Add support for BUDDiES
+ Rivals and Favorite Music support
### Wacca
+ Add option to block unregistered serials from accessing the title server
### DIVA
+ Fix for reading modded content (Thanks ThatzOkay!)
### CHUNITHM
+ Save net battle info
## 20240630
### DIVA
+ Added configurable festa options'
## 20240629
### CHUNITHM
+ Add team points
## 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
## 20240407
### Maimai
+ Support maimai DX International [#118](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/118) (Thanks beerpsi!)
+ Fixed the maimai DX reboot time from config [#120](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/120) (Thanks topty!)
## 20240318
### CXB
+ Fixing handle_data_shop_list_detail_request for Sunrise S1
## 20240302
### SAO
+ Fixing new profile creation with right heroes and start VP
+ Fix to the Unanalyzed Log responses returning the wrong rewards
+ Documentation revised
## 20240226
### CXB
+ Fixing paths for rev.py
+ Changed encoding for handle_data_item_list_icon_request
## 20240202
### SAO
+ Added reader assets and edited the game specific documentation
## 20240118
### System
+ Added game version names to the readme
## 20240109 ## 20240109
### System ### System
+ Removed `ADD config config` from dockerfile [#83](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/83) (Thanks zaphkito!) + Removed `ADD config config` from dockerfile [#83](https://gitea.tendokyu.moe/Hay1tsme/artemis/pulls/83) (Thanks zaphkito!)

View File

@@ -1,182 +1,8 @@
# Contributing to ARTEMiS # 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). This guide assume you're familiar with both git, python, and the libraries that artemis uses. 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.
This document is a work in progress. If you have any questions or notice any errors, please report it to the discord.
## Adding games ## Adding games
### Step 0 Guide WIP
+ 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 ## Adding game versions
See the above section about code expectations and how to PR. Guide WIP
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

@@ -2,5 +2,5 @@ from .base import ADBBaseRequest, ADBBaseResponse, ADBHeader, ADBHeaderException
from .base import CompanyCodes, ReaderFwVer, CMD_CODE_GOODBYE, HEADER_SIZE from .base import CompanyCodes, ReaderFwVer, CMD_CODE_GOODBYE, HEADER_SIZE
from .lookup import ADBLookupRequest, ADBLookupResponse, ADBLookupExResponse from .lookup import ADBLookupRequest, ADBLookupResponse, ADBLookupExResponse
from .campaign import ADBCampaignClearRequest, ADBCampaignClearResponse, ADBCampaignResponse, ADBOldCampaignRequest, ADBOldCampaignResponse from .campaign import ADBCampaignClearRequest, ADBCampaignClearResponse, ADBCampaignResponse, ADBOldCampaignRequest, ADBOldCampaignResponse
from .felica import ADBFelicaLookupRequest, ADBFelicaLookupResponse, ADBFelicaLookupExRequest, ADBFelicaLookupExResponse from .felica import ADBFelicaLookupRequest, ADBFelicaLookupResponse, ADBFelicaLookup2Request, ADBFelicaLookup2Response
from .log import ADBLogExRequest, ADBLogRequest, ADBStatusLogRequest, ADBLogExResponse from .log import ADBLogExRequest, ADBLogRequest, ADBStatusLogRequest, ADBLogExResponse

View File

@@ -10,14 +10,13 @@ class ADBFelicaLookupRequest(ADBBaseRequest):
self.pmm = hex(pmm)[2:].upper() self.pmm = hex(pmm)[2:].upper()
class ADBFelicaLookupResponse(ADBBaseResponse): class ADBFelicaLookupResponse(ADBBaseResponse):
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: 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:
super().__init__(code, length, status, game_id, store_id, keychip_id) 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.access_code = access_code if access_code is not None else "00000000000000000000"
self.idx = idx
@classmethod @classmethod
def from_req(cls, req: ADBHeader, access_code: str = None, idx: int = 0) -> "ADBFelicaLookupResponse": def from_req(cls, req: ADBHeader, access_code: str = None) -> "ADBFelicaLookupResponse":
c = cls(access_code, idx, req.game_id, req.store_id, req.keychip_id) c = cls(access_code, req.game_id, req.store_id, req.keychip_id)
c.head.protocol_ver = req.protocol_ver c.head.protocol_ver = req.protocol_ver
return c return c
@@ -27,7 +26,7 @@ class ADBFelicaLookupResponse(ADBBaseResponse):
"access_code" / Int8ub[10], "access_code" / Int8ub[10],
Padding(2) Padding(2)
).build(dict( ).build(dict(
felica_idx = self.idx, felica_idx = 0,
access_code = bytes.fromhex(self.access_code) access_code = bytes.fromhex(self.access_code)
)) ))
@@ -35,7 +34,7 @@ class ADBFelicaLookupResponse(ADBBaseResponse):
return self.head.make() + resp_struct return self.head.make() + resp_struct
class ADBFelicaLookupExRequest(ADBBaseRequest): class ADBFelicaLookup2Request(ADBBaseRequest):
def __init__(self, data: bytes) -> None: def __init__(self, data: bytes) -> None:
super().__init__(data) super().__init__(data)
self.random = struct.unpack_from("<16s", data, 0x20)[0] self.random = struct.unpack_from("<16s", data, 0x20)[0]
@@ -46,7 +45,7 @@ class ADBFelicaLookupExRequest(ADBBaseRequest):
self.company = CompanyCodes(int.from_bytes(company, 'little')) self.company = CompanyCodes(int.from_bytes(company, 'little'))
self.fw_ver = ReaderFwVer.from_byte(fw_ver) self.fw_ver = ReaderFwVer.from_byte(fw_ver)
class ADBFelicaLookupExResponse(ADBBaseResponse): class ADBFelicaLookup2Response(ADBBaseResponse):
def __init__(self, user_id: Union[int, None] = None, access_code: Union[str, None] = None, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x12, length: int = 0x130, status: int = 1) -> None: def __init__(self, user_id: Union[int, None] = None, access_code: Union[str, None] = None, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x12, length: int = 0x130, status: int = 1) -> None:
super().__init__(code, length, status, game_id, store_id, keychip_id) super().__init__(code, length, status, game_id, store_id, keychip_id)
self.user_id = user_id if user_id is not None else -1 self.user_id = user_id if user_id is not None else -1
@@ -56,7 +55,7 @@ class ADBFelicaLookupExResponse(ADBBaseResponse):
self.auth_key = [0] * 256 self.auth_key = [0] * 256
@classmethod @classmethod
def from_req(cls, req: ADBHeader, user_id: Union[int, None] = None, access_code: Union[str, None] = None) -> "ADBFelicaLookupExResponse": def from_req(cls, req: ADBHeader, user_id: Union[int, None] = None, access_code: Union[str, None] = None) -> "ADBFelicaLookup2Response":
c = cls(user_id, access_code, req.game_id, req.store_id, req.keychip_id) c = cls(user_id, access_code, req.game_id, req.store_id, req.keychip_id)
c.head.protocol_ver = req.protocol_ver c.head.protocol_ver = req.protocol_ver
return c return c

View File

@@ -66,8 +66,7 @@ class AimedbServlette():
def start(self) -> None: def start(self) -> None:
self.logger.info(f"Start on port {self.config.aimedb.port}") self.logger.info(f"Start on port {self.config.aimedb.port}")
addr = self.config.aimedb.listen_address if self.config.aimedb.listen_address else self.config.server.listen_address asyncio.create_task(asyncio.start_server(self.dataReceived, self.config.server.listen_address, self.config.aimedb.port))
asyncio.create_task(asyncio.start_server(self.dataReceived, addr, self.config.aimedb.port))
async def dataReceived(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter): async def dataReceived(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
self.logger.debug(f"Connection made from {writer.get_extra_info('peername')[0]}") self.logger.debug(f"Connection made from {writer.get_extra_info('peername')[0]}")
@@ -176,12 +175,6 @@ class AimedbServlette():
async def handle_lookup(self, data: bytes, resp_code: int) -> ADBBaseResponse: async def handle_lookup(self, data: bytes, resp_code: int) -> ADBBaseResponse:
req = ADBLookupRequest(data) req = ADBLookupRequest(data)
if req.access_code == "00000000000000000000":
self.logger.warn(f"All-zero access code from {req.head.keychip_id}")
ret = ADBLookupResponse.from_req(req.head, -1)
ret.head.status = ADBStatus.BAN_SYS
return ret
user_id = await self.data.card.get_user_id_from_card(req.access_code) user_id = await self.data.card.get_user_id_from_card(req.access_code)
is_banned = await self.data.card.get_card_banned(req.access_code) is_banned = await self.data.card.get_card_banned(req.access_code)
is_locked = await self.data.card.get_card_locked(req.access_code) is_locked = await self.data.card.get_card_locked(req.access_code)
@@ -197,22 +190,10 @@ class AimedbServlette():
self.logger.info( self.logger.info(
f"access_code {req.access_code} -> user_id {ret.user_id}" f"access_code {req.access_code} -> user_id {ret.user_id}"
) )
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 return ret
async def handle_lookup_ex(self, data: bytes, resp_code: int) -> ADBBaseResponse: async def handle_lookup_ex(self, data: bytes, resp_code: int) -> ADBBaseResponse:
req = ADBLookupRequest(data) req = ADBLookupRequest(data)
if req.access_code == "00000000000000000000":
self.logger.warn(f"All-zero access code from {req.head.keychip_id}")
ret = ADBLookupExResponse.from_req(req.head, -1)
ret.head.status = ADBStatus.BAN_SYS
return ret
user_id = await self.data.card.get_user_id_from_card(req.access_code) user_id = await self.data.card.get_user_id_from_card(req.access_code)
is_banned = await self.data.card.get_card_banned(req.access_code) is_banned = await self.data.card.get_card_banned(req.access_code)
@@ -238,56 +219,29 @@ class AimedbServlette():
self.logger.debug(f"Generated auth token {auth_key}") self.logger.debug(f"Generated auth token {auth_key}")
ret.auth_key = auth_key_full ret.auth_key = auth_key_full
if user_id and user_id > 0:
await self.data.card.update_card_last_login(req.access_code)
return ret return ret
async def handle_felica_lookup(self, data: bytes, resp_code: int) -> bytes: async def handle_felica_lookup(self, data: bytes, resp_code: int) -> bytes:
""" """
On official, the IDm is used as a key to look up the stored access code in a large On official, I think a card has to be registered for this to actually work, but
database. We do not have access to that database so we have to make due with what we got. I'm making the executive decision to not implement that and just kick back our
Interestingly, namco games are able to read S_PAD0 and send the server the correct access faux generated access code. The real felica IDm -> access code conversion is done
code, but aimedb doesn't. Until somebody either enters the correct code manually, or scans on the ADB server, which we do not and will not ever have access to. Because we can
on a game that reads it correctly from the card, this will have to do. It's the same conversion assure that all IDms will be unique, this basic 0-padded hex -> int conversion will
used on the big boy networks. be fine.
""" """
req = ADBFelicaLookupRequest(data) req = ADBFelicaLookupRequest(data)
idm = req.idm.zfill(16) ac = self.data.card.to_access_code(req.idm)
if idm == "0000000000000000":
self.logger.warn(f"All-zero IDm from {req.head.keychip_id}")
ret = ADBFelicaLookupResponse.from_req(req.head, "00000000000000000000")
ret.head.status = ADBStatus.BAN_SYS
return ret
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( self.logger.info(
f"idm {idm} ipm {req.pmm.zfill(16)} -> access_code {ac}" f"idm {req.idm} ipm {req.pmm} -> access_code {ac}"
) )
return ADBFelicaLookupResponse.from_req(req.head, ac) return ADBFelicaLookupResponse.from_req(req.head, ac)
async def handle_felica_register(self, data: bytes, resp_code: int) -> bytes: async def handle_felica_register(self, data: bytes, resp_code: int) -> bytes:
""" """
Used to register felica moble access codes. Will never be used on our network I've never seen this used.
because we don't implement felica_lookup properly.
""" """
req = ADBFelicaLookupRequest(data) req = ADBFelicaLookupRequest(data)
idm = req.idm.zfill(16)
if idm == "0000000000000000":
self.logger.warn(f"All-zero IDm from {req.head.keychip_id}")
ret = ADBFelicaLookupResponse.from_req(req.head, "00000000000000000000")
ret.head.status = ADBStatus.BAN_SYS
return ret
ac = self.data.card.to_access_code(req.idm) ac = self.data.card.to_access_code(req.idm)
if self.config.server.allow_user_registration: if self.config.server.allow_user_registration:
@@ -313,49 +267,21 @@ class AimedbServlette():
f"Registration blocked!: access code {ac} (IDm: {req.idm} PMm: {req.pmm})" f"Registration blocked!: access code {ac} (IDm: {req.idm} PMm: {req.pmm})"
) )
if user_id > 0:
await self.data.card.update_card_last_login(ac)
return ADBFelicaLookupResponse.from_req(req.head, ac) return ADBFelicaLookupResponse.from_req(req.head, ac)
async def handle_felica_lookup_ex(self, data: bytes, resp_code: int) -> bytes: async def handle_felica_lookup_ex(self, data: bytes, resp_code: int) -> bytes:
req = ADBFelicaLookupExRequest(data) req = ADBFelicaLookup2Request(data)
user_id = None access_code = self.data.card.to_access_code(req.idm)
idm = req.idm.zfill(16) user_id = await self.data.card.get_user_id_from_card(access_code=access_code)
if idm == "0000000000000000":
self.logger.warn(f"All-zero IDm from {req.head.keychip_id}")
ret = ADBFelicaLookupExResponse.from_req(req.head, -1, "00000000000000000000")
ret.head.status = ADBStatus.BAN_SYS
return ret
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: if user_id is None:
user_id = -1 user_id = -1
self.logger.info( self.logger.info(
f"idm {idm} ipm {req.pmm} -> access_code {access_code} user_id {user_id}" f"idm {req.idm} ipm {req.pmm} -> access_code {access_code} user_id {user_id}"
) )
resp = ADBFelicaLookupExResponse.from_req(req.head, user_id, access_code) 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: 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) 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)
@@ -364,9 +290,7 @@ class AimedbServlette():
auth_key_full = auth_key.encode() + (b"\0" * auth_key_extra_len) auth_key_full = auth_key.encode() + (b"\0" * auth_key_extra_len)
self.logger.debug(f"Generated auth token {auth_key}") self.logger.debug(f"Generated auth token {auth_key}")
resp.auth_key = auth_key_full resp.auth_key = auth_key_full
if user_id and user_id > 0:
await self.data.card.update_card_last_login(access_code)
return resp return resp
async def handle_campaign_clear(self, data: bytes, resp_code: int) -> ADBBaseResponse: async def handle_campaign_clear(self, data: bytes, resp_code: int) -> ADBBaseResponse:
@@ -380,12 +304,6 @@ class AimedbServlette():
async def handle_register(self, data: bytes, resp_code: int) -> bytes: async def handle_register(self, data: bytes, resp_code: int) -> bytes:
req = ADBLookupRequest(data) req = ADBLookupRequest(data)
user_id = -1 user_id = -1
if req.access_code == "00000000000000000000":
self.logger.warn(f"All-zero access code from {req.head.keychip_id}")
ret = ADBLookupResponse.from_req(req.head, -1)
ret.head.status = ADBStatus.BAN_SYS
return ret
if self.config.server.allow_user_registration: if self.config.server.allow_user_registration:
user_id = await self.data.user.create_user() user_id = await self.data.user.create_user()
@@ -409,24 +327,11 @@ class AimedbServlette():
self.logger.info( self.logger.info(
f"Registration blocked!: access code {req.access_code}" 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) resp = ADBLookupResponse.from_req(req.head, user_id)
if resp.user_id <= 0: if resp.user_id <= 0:
resp.head.status = ADBStatus.BAN_SYS # Closest we can get to a "You cannot register" resp.head.status = ADBStatus.BAN_SYS # Closest we can get to a "You cannot register"
else:
await self.data.card.update_card_last_login(req.access_code)
return resp return resp
# TODO: Save these in some capacity, as deemed relevant # TODO: Save these in some capacity, as deemed relevant

View File

@@ -94,7 +94,6 @@ class DLI_STATUS(Enum):
return cls.UNKNOWN return cls.UNKNOWN
class AllnetServlet: class AllnetServlet:
allnet_registry: Dict[str, Any] = {}
def __init__(self, core_cfg: CoreConfig, cfg_folder: str): def __init__(self, core_cfg: CoreConfig, cfg_folder: str):
self.config = core_cfg self.config = core_cfg
self.config_folder = cfg_folder self.config_folder = cfg_folder
@@ -126,13 +125,11 @@ class AllnetServlet:
def startup(self) -> None: def startup(self) -> None:
self.logger.info(f"Ready on port {self.config.allnet.port if self.config.allnet.standalone else self.config.server.port}") self.logger.info(f"Ready on port {self.config.allnet.port if self.config.allnet.standalone else self.config.server.port}")
if not TitleServlet.title_registry:
TitleServlet(self.config, self.config_folder)
async def handle_poweron(self, request: Request): async def handle_poweron(self, request: Request):
request_ip = Utils.get_ip_addr(request) request_ip = Utils.get_ip_addr(request)
pragma_header = request.headers.get('Pragma', "") pragma_header = request.headers.get('Pragma', "")
is_dfi = pragma_header == "DFI" is_dfi = pragma_header is not None and pragma_header == "DFI"
data = await request.body() data = await request.body()
try: try:
@@ -171,7 +168,7 @@ class AllnetServlet:
if machine is None and not self.config.server.allow_unregistered_serials: if machine is None and not self.config.server.allow_unregistered_serials:
msg = f"Unrecognised serial {req.serial} attempted allnet auth from {request_ip}." msg = f"Unrecognised serial {req.serial} attempted allnet auth from {request_ip}."
await self.data.base.log_event( await self.data.base.log_event(
"allnet", "ALLNET_AUTH_UNKNOWN_SERIAL", logging.WARN, msg, {"serial": req.serial}, None, None, None, request_ip, req.game_id, req.ver "allnet", "ALLNET_AUTH_UNKNOWN_SERIAL", logging.WARN, msg
) )
self.logger.warning(msg) self.logger.warning(msg)
@@ -183,9 +180,9 @@ class AllnetServlet:
arcade = await self.data.arcade.get_arcade(machine["arcade"]) arcade = await self.data.arcade.get_arcade(machine["arcade"])
if self.config.server.check_arcade_ip: if self.config.server.check_arcade_ip:
if arcade["ip"] and arcade["ip"] is not None and arcade["ip"] != req.ip: if arcade["ip"] and arcade["ip"] is not None and arcade["ip"] != req.ip:
msg = f"{req.serial} attempted allnet auth from bad IP {req.ip} (expected {arcade['ip']})." msg = f"Serial {req.serial} attempted allnet auth from bad IP {req.ip} (expected {arcade['ip']})."
await self.data.base.log_event( await self.data.base.log_event(
"allnet", "ALLNET_AUTH_BAD_IP", logging.ERROR, msg, {}, None, arcade['id'], machine['id'], request_ip, req.game_id, req.ver "allnet", "ALLNET_AUTH_BAD_IP", logging.ERROR, msg
) )
self.logger.warning(msg) self.logger.warning(msg)
@@ -194,9 +191,9 @@ class AllnetServlet:
return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n") 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: elif (not arcade["ip"] or arcade["ip"] is None) and self.config.server.strict_ip_checking:
msg = f"{req.serial} attempted allnet auth from bad IP {req.ip}, but arcade {arcade['id']} has no IP set! (strict checking enabled)." msg = f"Serial {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( await self.data.base.log_event(
"allnet", "ALLNET_AUTH_NO_SHOP_IP", logging.ERROR, msg, {}, None, arcade['id'], machine['id'], request_ip, req.game_id, req.ver "allnet", "ALLNET_AUTH_NO_SHOP_IP", logging.ERROR, msg
) )
self.logger.warning(msg) self.logger.warning(msg)
@@ -204,17 +201,7 @@ class AllnetServlet:
resp_dict = {k: v for k, v in vars(resp).items() if v is not None} 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") 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 = ( country = (
arcade["country"] if machine["country"] is None else machine["country"] arcade["country"] if machine["country"] is None else machine["country"]
) )
@@ -222,7 +209,7 @@ class AllnetServlet:
country = AllnetCountryCode.JAPAN.value country = AllnetCountryCode.JAPAN.value
resp.country = country resp.country = country
resp.place_id = f"{arcade['id']:04X}" resp.place_id = arcade["id"]
resp.allnet_id = machine["id"] resp.allnet_id = machine["id"]
resp.name = arcade["name"] if arcade["name"] is not None else "" resp.name = arcade["name"] if arcade["name"] is not None else ""
resp.nickname = arcade["nickname"] if arcade["nickname"] is not None else "" resp.nickname = arcade["nickname"] if arcade["nickname"] is not None else ""
@@ -246,14 +233,11 @@ class AllnetServlet:
arcade["timezone"] if arcade["timezone"] is not None else "+0900" if req.format_ver == 3 else "+09:00" 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 req.game_id not in TitleServlet.title_registry:
if not self.config.server.is_develop: if not self.config.server.is_develop:
msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}." msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}."
await self.data.base.log_event( await self.data.base.log_event(
"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 "allnet", "ALLNET_AUTH_UNKNOWN_GAME", logging.WARN, msg
) )
self.logger.warning(msg) self.logger.warning(msg)
@@ -276,45 +260,27 @@ class AllnetServlet:
int_ver = req.ver.replace(".", "") int_ver = req.ver.replace(".", "")
try: resp.uri, resp.host = TitleServlet.title_registry[req.game_id].get_allnet_info(req.game_id, int(int_ver), req.serial)
resp.uri, resp.host = TitleServlet.title_registry[req.game_id].get_allnet_info(req.game_id, int(int_ver), req.serial)
except Exception as e:
self.logger.error(f"Error running get_allnet_info for {req.game_id} - {e}")
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")
if machine and arcade: msg = f"{req.serial} authenticated from {request_ip}: {req.game_id} v{req.ver}"
msg = f"{req.serial} authenticated from {request_ip}: {req.game_id} v{req.ver}" 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) self.logger.info(msg)
resp_dict = {k: v for k, v in vars(resp).items() if v is not None} resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
resp_str = urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n" resp_str = urllib.parse.unquote(urllib.parse.urlencode(resp_dict))
self.logger.debug(f"Allnet response: {resp_dict}") self.logger.debug(f"Allnet response: {resp_dict}")
resp_str += "\n"
if is_dfi: """if is_dfi:
return PlainTextResponse( request.responseHeaders.addRawHeader('Pragma', 'DFI')
content=self.to_dfi(resp_str) + b"\r\n", return self.to_dfi(resp_str)"""
headers={
"Pragma": "DFI",
},
)
return PlainTextResponse(resp_str) return PlainTextResponse(resp_str)
async def handle_dlorder(self, request: Request): async def handle_dlorder(self, request: Request):
request_ip = Utils.get_ip_addr(request) request_ip = Utils.get_ip_addr(request)
pragma_header = request.headers.get('Pragma', "") pragma_header = request.headers.get('Pragma', "")
is_dfi = pragma_header == "DFI" is_dfi = pragma_header is not None and pragma_header == "DFI"
data = await request.body() data = await request.body()
try: try:
@@ -351,11 +317,7 @@ class AllnetServlet:
): ):
return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n") return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n")
else: else: # TODO: Keychip check
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( if path.exists(
f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver.replace('.', '')}-app.ini" f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver.replace('.', '')}-app.ini"
): ):
@@ -366,38 +328,25 @@ class AllnetServlet:
): ):
resp.uri += f"|http://{self.config.server.hostname}:{self.config.server.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-opt.ini" resp.uri += f"|http://{self.config.server.hostname}:{self.config.server.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-opt.ini"
if resp.uri: self.logger.debug(f"Sending download uri {resp.uri}")
self.logger.info(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}")
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" res_str = urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n"
"""if is_dfi:
if is_dfi: request.responseHeaders.addRawHeader('Pragma', 'DFI')
return PlainTextResponse( return self.to_dfi(res_str)"""
content=self.to_dfi(res_str) + b"\r\n",
headers={
"Pragma": "DFI",
},
)
return PlainTextResponse(res_str) return PlainTextResponse(res_str)
async def handle_dlorder_ini(self, request: Request) -> bytes: async def handle_dlorder_ini(self, request: Request) -> bytes:
req_file = request.path_params.get("file", "").replace("%0A", "").replace("\n", "") req_file = request.path_params.get("file", "").replace("%0A", "").replace("\n", "")
request_ip = Utils.get_ip_addr(request)
if not req_file: if not req_file:
return PlainTextResponse(status_code=404) return PlainTextResponse(status_code=404)
if path.exists(f"{self.config.allnet.update_cfg_folder}/{req_file}"): if path.exists(f"{self.config.allnet.update_cfg_folder}/{req_file}"):
self.logger.info(f"Request for DL INI file {req_file} from {request_ip} successful") self.logger.info(f"Request for DL INI file {req_file} from {Utils.get_ip_addr(request)} successful")
await self.data.base.log_event( await self.data.base.log_event("allnet", "DLORDER_INI_SENT", logging.INFO, f"{Utils.get_ip_addr(request)} successfully recieved {req_file}")
"allnet", "DLORDER_INI_SENT", logging.INFO, f"{request_ip} successfully recieved {req_file}", {"file": req_file}, ip=request_ip
)
return PlainTextResponse(open( return PlainTextResponse(open(
f"{self.config.allnet.update_cfg_folder}/{req_file}", "r", encoding="utf-8" f"{self.config.allnet.update_cfg_folder}/{req_file}", "r", encoding="utf-8"
@@ -435,13 +384,7 @@ 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}:"\ 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." f" {rep.tdsc}/{rep.tsc} segments downloaded for working files {rep.wfl} with {rep.dfl if rep.dfl else 'none'} complete."
machine = await self.data.arcade.get_machine(rep.serial) await self.data.base.log_event("allnet", "DL_REPORT", logging.INFO, msg, dl_data)
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) self.logger.info(msg)
return PlainTextResponse("OK") return PlainTextResponse("OK")
@@ -461,24 +404,14 @@ class AllnetServlet:
if serial is None or num_files_dld is None or num_files_to_dl is None or dl_state is None: 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") return PlainTextResponse("NG")
msg = f"LoaderStateRecorder Request from {ip} {serial}: {num_files_dld}/{num_files_to_dl} Files download (State: {dl_state})" self.logger.info(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") return PlainTextResponse("OK")
async def handle_alive(self, request: Request) -> bytes: async def handle_alive(self, request: Request) -> bytes:
return PlainTextResponse("OK") return PlainTextResponse("OK")
async def handle_naomitest(self, request: Request) -> bytes: async def handle_naomitest(self, request: Request) -> bytes:
# This could be spam-able, removing self.logger.info(f"Ping from {Utils.get_ip_addr(request)}")
#self.logger.info(f"Ping from {Utils.get_ip_addr(request)}")
return PlainTextResponse("naomi ok") return PlainTextResponse("naomi ok")
def allnet_req_to_dict(self, data: str) -> Optional[List[Dict[str, Any]]]: def allnet_req_to_dict(self, data: str) -> Optional[List[Dict[str, Any]]]:
@@ -608,35 +541,18 @@ class BillingServlet:
if machine is None and not self.config.server.allow_unregistered_serials: 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}." msg = f"Unrecognised serial {req.keychipid} attempted billing checkin from {request_ip} for {req.gameid} v{req.gamever}."
await self.data.base.log_event( await self.data.base.log_event(
"allnet", "BILLING_CHECKIN_NG_SERIAL", logging.WARN, msg, ip=request_ip, game=req.gameid, version=req.gamever "allnet", "BILLING_CHECKIN_NG_SERIAL", logging.WARN, msg
) )
self.logger.warning(msg) self.logger.warning(msg)
return PlainTextResponse(f"result=1&requestno={req.requestno}&message=Keychip Serial bad\r\n") return PlainTextResponse(f"result=1&requestno={req.requestno}&message=Keychip Serial bad\r\n")
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}"
)
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}"
)
msg = (
f"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)
if req.traceleft > 0: if req.traceleft > 0:
self.logger.warn(f"{req.traceleft} unsent tracelogs") self.logger.warn(f"{req.traceleft} unsent tracelogs")
kc_playlimit = req.playlimit kc_playlimit = req.playlimit
@@ -780,7 +696,7 @@ class BillingInfo:
self.boardid = str(data.get("boardid", None)) self.boardid = str(data.get("boardid", None))
self.tenpoip = str(data.get("tenpoip", None)) self.tenpoip = str(data.get("tenpoip", None))
self.libalibver = float(data.get("libalibver", None)) self.libalibver = float(data.get("libalibver", None))
self.datamax = int(data.get("datamax", None)) self.data.max = int(data.get("datamax", None))
self.billingtype = BillingType(int(data.get("billingtype", None))) self.billingtype = BillingType(int(data.get("billingtype", None)))
self.protocolver = float(data.get("protocolver", None)) self.protocolver = float(data.get("protocolver", None))
self.operatingfix = bool(data.get("operatingfix", None)) self.operatingfix = bool(data.get("operatingfix", None))
@@ -960,7 +876,7 @@ class DLReport:
return True return True
cfg_dir = environ.get("ARTEMIS_CFG_DIR", "config") cfg_dir = environ.get("DIANA_CFG_DIR", "config")
cfg: CoreConfig = CoreConfig() cfg: CoreConfig = CoreConfig()
if path.exists(f"{cfg_dir}/core.yaml"): if path.exists(f"{cfg_dir}/core.yaml"):
cfg.update(yaml.safe_load(open(f"{cfg_dir}/core.yaml"))) cfg.update(yaml.safe_load(open(f"{cfg_dir}/core.yaml")))

View File

@@ -81,7 +81,7 @@ class ServerConfig:
is_using_proxy is True and this value is non-zero is_using_proxy is True and this value is non-zero
""" """
return CoreConfig.get_config_field( return CoreConfig.get_config_field(
self.__config, "core", "server", "proxy_port", default=0 self.__config, "core", "title", "proxy_port", default=0
) )
@property @property
@@ -91,7 +91,7 @@ class ServerConfig:
instead of 'port' if is_using_proxy is True and this value is non-zero instead of 'port' if is_using_proxy is True and this value is non-zero
""" """
return CoreConfig.get_config_field( return CoreConfig.get_config_field(
self.__config, "core", "server", "proxy_port_ssl", default=0 self.__config, "core", "title", "proxy_port_ssl", default=0
) )
@property @property
@@ -173,7 +173,7 @@ class DatabaseConfig:
@property @property
def protocol(self) -> str: def protocol(self) -> str:
return CoreConfig.get_config_field( return CoreConfig.get_config_field(
self.__config, "core", "database", "protocol", default="mysql" self.__config, "core", "database", "type", default="mysql"
) )
@property @property
@@ -320,12 +320,6 @@ class AimedbConfig:
self.__config, "core", "aimedb", "enable", default=True self.__config, "core", "aimedb", "enable", default=True
) )
@property
def listen_address(self) -> bool:
return CoreConfig.get_config_field(
self.__config, "core", "aimedb", "listen_address", default=""
)
@property @property
def loglevel(self) -> int: def loglevel(self) -> int:
return CoreConfig.str_to_loglevel( return CoreConfig.str_to_loglevel(

View File

@@ -1,18 +1,16 @@
from enum import Enum from enum import Enum
class MainboardPlatformCodes(Enum): class MainboardPlatformCodes:
RINGEDGE = "AAL" RINGEDGE = "AALE"
RINGEDGE2 = "AAS" RINGWIDE = "AAML"
RINGWIDE = "AAM" NU = "AAVE"
NU = "AAV" NUSX = "AAWE"
NUSX = "AAW" ALLS_UX = "ACAE"
ALLS = "ACA" ALLS_HX = "ACAX"
#ALLS_UX = "ACAE"
#ALLS_HX = "ACAX"
class MainboardRevisions(Enum): class MainboardRevisions:
RINGEDGE = 1 RINGEDGE = 1
RINGEDGE2 = 2 RINGEDGE2 = 2
@@ -31,10 +29,11 @@ class MainboardRevisions(Enum):
ALLS_HX2 = 12 ALLS_HX2 = 12
class KeychipPlatformsCodes(Enum): class KeychipPlatformsCodes:
RING = "72" RING = "A72E"
NU = ("60", "61", "69") NU = ("A60E", "A60E", "A60E")
ALLS = "63" NUSX = ("A61X", "A69X")
ALLS = "A63E"
class AllnetCountryCode(Enum): class AllnetCountryCode(Enum):

View File

@@ -1,27 +0,0 @@
"""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

@@ -1,48 +0,0 @@
"""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

@@ -1,46 +0,0 @@
"""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

@@ -1,54 +0,0 @@
"""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

@@ -1,50 +0,0 @@
"""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

@@ -1,48 +0,0 @@
"""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

@@ -1,41 +0,0 @@
"""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

@@ -1,28 +0,0 @@
"""card_add_memo
Revision ID: 5ea73f89d982
Revises: 745448d83696
Create Date: 2024-07-06 22:46:56.992152
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '5ea73f89d982'
down_revision = '745448d83696'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('aime_card', sa.Column('memo', sa.VARCHAR(length=16), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('aime_card', 'memo')
# ### end Alembic commands ###

View File

@@ -1,295 +0,0 @@
"""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

@@ -1,56 +0,0 @@
"""GekiChu rating tables
Revision ID: 6a7e8277763b
Revises: d8950c7ce2fc
Create Date: 2024-03-13 12:18:53.210018
"""
from alembic import op
from sqlalchemy import Column, Integer, String
# revision identifiers, used by Alembic.
revision = '6a7e8277763b'
down_revision = 'd8950c7ce2fc'
branch_labels = None
depends_on = None
GEKICHU_RATING_TABLE_NAMES = [
"chuni_profile_rating",
"ongeki_profile_rating",
]
def upgrade():
for table_name in GEKICHU_RATING_TABLE_NAMES:
op.create_table(
table_name,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", Integer, nullable=False),
Column("version", Integer, nullable=False),
Column("type", String(255), nullable=False),
Column("index", Integer, nullable=False),
Column("musicId", Integer),
Column("difficultId", Integer),
Column("romVersionCode", Integer),
Column("score", Integer),
mysql_charset="utf8mb4",
)
op.create_foreign_key(
None,
table_name,
"aime_user",
["user"],
["id"],
ondelete="cascade",
onupdate="cascade",
)
op.create_unique_constraint(
f"{table_name}_uk",
table_name,
["user", "version", "type", "index"],
)
def downgrade():
for table_name in GEKICHU_RATING_TABLE_NAMES:
op.drop_table(table_name)

View File

@@ -1,28 +0,0 @@
"""chuni_team_points
Revision ID: 745448d83696
Revises: 5ea363686347
Create Date: 2024-06-29 00:05:22.479187
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '745448d83696'
down_revision = '5ea363686347'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('chuni_profile_team', sa.Column('userTeamPoint', sa.JSON(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('chuni_profile_team', 'userTeamPoint')
# ### end Alembic commands ###

View File

@@ -1,28 +0,0 @@
"""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,68 +0,0 @@
"""mai2_buddies_support
Revision ID: 81e44dd6047a
Revises: 6a7e8277763b
Create Date: 2024-03-12 19:10:37.063907
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = "81e44dd6047a"
down_revision = "6a7e8277763b"
branch_labels = None
depends_on = None
def upgrade():
op.create_table(
"mai2_playlog_2p",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("user", sa.Integer(), nullable=False),
sa.Column("userId1", sa.Integer(), nullable=True),
sa.Column("userId2", sa.Integer(), nullable=True),
sa.Column("userName1", sa.String(length=25), nullable=True),
sa.Column("userName2", sa.String(length=25), nullable=True),
sa.Column("regionId", sa.Integer(), nullable=True),
sa.Column("placeId", sa.Integer(), nullable=True),
sa.Column("user2pPlaylogDetailList", sa.JSON(), nullable=True),
sa.ForeignKeyConstraint(
["user"], ["aime_user.id"], onupdate="cascade", ondelete="cascade"
),
sa.PrimaryKeyConstraint("id"),
mysql_charset="utf8mb4",
)
op.add_column(
"mai2_playlog",
sa.Column(
"extBool1", sa.Boolean(), nullable=True, server_default=sa.text("NULL")
),
)
op.add_column(
"mai2_profile_detail",
sa.Column(
"renameCredit", sa.Integer(), nullable=True, server_default=sa.text("NULL")
),
)
op.add_column(
"mai2_profile_detail",
sa.Column(
"currentPlayCount",
sa.Integer(),
nullable=True,
server_default=sa.text("NULL"),
),
)
def downgrade():
op.drop_table("mai2_playlog_2p")
op.drop_column("mai2_playlog", "extBool1")
op.drop_column("mai2_profile_detail", "renameCredit")
op.drop_column("mai2_profile_detail", "currentPlayCount")

View File

@@ -1,30 +0,0 @@
"""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

@@ -1,437 +0,0 @@
"""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

@@ -1,87 +0,0 @@
"""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

@@ -118,13 +118,7 @@ class Data:
def schema_upgrade(self, ver: str = None): def schema_upgrade(self, ver: str = None):
self.__alembic_cmd( self.__alembic_cmd(
"upgrade", "upgrade",
"head" if not ver else ver, "head",
)
def schema_downgrade(self, ver: str):
self.__alembic_cmd(
"downgrade",
ver,
) )
async def create_owner(self, email: Optional[str] = None, code: Optional[str] = "00000000000000000000") -> None: async def create_owner(self, email: Optional[str] = None, code: Optional[str] = "00000000000000000000") -> None:
@@ -133,7 +127,7 @@ class Data:
) )
hash = bcrypt.hashpw(pw.encode(), bcrypt.gensalt()) hash = bcrypt.hashpw(pw.encode(), bcrypt.gensalt())
user_id = await self.user.create_user(username="sysowner", email=email, password=hash.decode(), permission=255) user_id = await self.user.create_user("sysowner", email, hash.decode(), 255)
if user_id is None: if user_id is None:
self.logger.error(f"Failed to create owner with email {email}") self.logger.error(f"Failed to create owner with email {email}")
return return
@@ -235,19 +229,3 @@ class Data:
"-m", "-m",
message, message,
) )
async def create_revision_auto(self, message: str) -> None:
if not message:
self.logger.info("Message is required for create-revision")
return
for _, mod in Utils.get_all_titles().items():
if hasattr(mod, "database"):
mod.database(self.config)
self.__alembic_cmd(
"revision",
"--autogenerate",
"-m",
message,
)

View File

@@ -94,7 +94,7 @@ class ArcadeData(BaseData):
return None return None
return result.fetchone() return result.fetchone()
async def create_machine( async def put_machine(
self, self,
arcade_id: int, arcade_id: int,
serial: str = "", serial: str = "",
@@ -102,12 +102,12 @@ class ArcadeData(BaseData):
game: str = None, game: str = None,
is_cab: bool = False, is_cab: bool = False,
) -> Optional[int]: ) -> Optional[int]:
if not arcade_id: if arcade_id:
self.logger.error(f"{__name__ }: Need arcade id!") self.logger.error(f"{__name__ }: Need arcade id!")
return None return None
sql = machine.insert().values( sql = machine.insert().values(
arcade=arcade_id, serial=serial, board=board, game=game, is_cab=is_cab arcade=arcade_id, keychip=serial, board=board, game=game, is_cab=is_cab
) )
result = await self.execute(sql) result = await self.execute(sql)
@@ -148,15 +148,15 @@ class ArcadeData(BaseData):
return None return None
return result.fetchall() return result.fetchall()
async def create_arcade( async def put_arcade(
self, self,
name: str = None, name: str,
nickname: str = None, nickname: str = None,
country: str = "JPN", country: str = "JPN",
country_id: int = 1, country_id: int = 1,
state: str = "", state: str = "",
city: str = "", city: str = "",
region_id: int = 1, regional_id: int = 1,
) -> Optional[int]: ) -> Optional[int]:
if nickname is None: if nickname is None:
nickname = name nickname = name
@@ -168,7 +168,7 @@ class ArcadeData(BaseData):
country_id=country_id, country_id=country_id,
state=state, state=state,
city=city, city=city,
region_id=region_id, regional_id=regional_id,
) )
result = await self.execute(sql) result = await self.execute(sql)
@@ -206,6 +206,17 @@ class ArcadeData(BaseData):
return None return None
return result.lastrowid return result.lastrowid
async def format_serial(
self, platform_code: str, platform_rev: int, serial_num: int, append: int = 4152
) -> str:
return f"{platform_code}{platform_rev:02d}A{serial_num:04d}{append:04d}" # 0x41 = A, 0x52 = R
def validate_keychip_format(self, serial: str) -> bool:
if re.fullmatch(r"^A[0-9]{2}[E|X][-]?[0-9]{2}[A-HJ-NP-Z][0-9]{4}([0-9]{4})?$", serial) is None:
return False
return True
async def get_arcade_by_name(self, name: str) -> Optional[List[Row]]: async def get_arcade_by_name(self, name: str) -> Optional[List[Row]]:
sql = arcade.select(or_(arcade.c.name.like(f"%{name}%"), arcade.c.nickname.like(f"%{name}%"))) sql = arcade.select(or_(arcade.c.name.like(f"%{name}%"), arcade.c.nickname.like(f"%{name}%")))
result = await self.execute(sql) result = await self.execute(sql)
@@ -219,53 +230,3 @@ class ArcadeData(BaseData):
if result is None: if result is None:
return None return None
return result.fetchall() return result.fetchall()
async def get_num_generated_keychips(self) -> Optional[int]:
result = await self.execute(select(func.count("serial LIKE 'A69A%'")).select_from(machine))
if result:
return result.fetchone()['count_1']
self.logger.error("Failed to count machine serials that start with A69A!")
def format_serial(
self, platform_code: str, platform_rev: int, serial_letter: str, serial_num: int, append: int, dash: bool = False
) -> str:
return f"{platform_code}{'-' if dash else ''}{platform_rev:02d}{serial_letter}{serial_num:04d}{append:04d}"
def validate_keychip_format(self, serial: str) -> bool:
# For the 2nd letter, E and X are the only "real" values that have been observed
if re.fullmatch(r"^A[0-9]{2}[A-Z][-]?[0-9]{2}[A-HJ-NP-Z][0-9]{4}([0-9]{4})?$", serial) is None:
return False
return True
# Thanks bottersnike!
def get_keychip_suffix(self, year: int, month: int) -> str:
assert year > 1957
assert 1 <= month <= 12
year -= 1957
# Jan/Feb/Mar are from the previous tax year
if month < 4:
year -= 1
assert year >= 1 and year <= 99
month = ((month - 1) + 9) % 12 # Offset so April=0
return f"{year:02}{month // 6:01}{month % 6 + 1:01}"
def parse_keychip_suffix(self, suffix: str) -> tuple[int, int]:
year = int(suffix[0:2])
half = int(suffix[2])
assert half in (0, 1)
period = int(suffix[3])
assert period in (1, 2, 3, 4, 5, 6)
month = half * 6 + (period - 1)
month = ((month + 3) % 12) + 1 # Offset so Jan=1
# Jan/Feb/Mar are from the previous tax year
if month < 4:
year += 1
year += 1957
return (year, month)

View File

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

View File

@@ -1,6 +1,6 @@
from typing import Dict, List, Optional from typing import Dict, List, Optional
from sqlalchemy import Table, Column, UniqueConstraint from sqlalchemy import Table, Column, UniqueConstraint
from sqlalchemy.types import Integer, String, Boolean, TIMESTAMP, BIGINT, VARCHAR from sqlalchemy.types import Integer, String, Boolean, TIMESTAMP
from sqlalchemy.sql.schema import ForeignKey from sqlalchemy.sql.schema import ForeignKey
from sqlalchemy.sql import func from sqlalchemy.sql import func
from sqlalchemy.engine import Row from sqlalchemy.engine import Row
@@ -11,24 +11,22 @@ aime_card = Table(
"aime_card", "aime_card",
metadata, metadata,
Column("id", Integer, primary_key=True, nullable=False), Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), Column(
Column("access_code", String(20), nullable=False, unique=True), "user",
Column("idm", String(16), unique=True), ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
Column("chip_id", BIGINT, unique=True), nullable=False,
),
Column("access_code", String(20)),
Column("created_date", TIMESTAMP, server_default=func.now()), Column("created_date", TIMESTAMP, server_default=func.now()),
Column("last_login_date", TIMESTAMP, onupdate=func.now()), Column("last_login_date", TIMESTAMP, onupdate=func.now()),
Column("is_locked", Boolean, server_default="0"), Column("is_locked", Boolean, server_default="0"),
Column("is_banned", Boolean, server_default="0"), Column("is_banned", Boolean, server_default="0"),
Column("memo", VARCHAR(16)),
UniqueConstraint("user", "access_code", name="aime_card_uk"), UniqueConstraint("user", "access_code", name="aime_card_uk"),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
class CardData(BaseData): 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]: async def get_card_by_access_code(self, access_code: str) -> Optional[Row]:
sql = aime_card.select(aime_card.c.access_code == access_code) sql = aime_card.select(aime_card.c.access_code == access_code)
@@ -76,7 +74,6 @@ class CardData(BaseData):
if card["is_banned"]: if card["is_banned"]:
return True return True
return False return False
async def get_card_locked(self, access_code: str) -> Optional[bool]: async def get_card_locked(self, access_code: str) -> Optional[bool]:
""" """
Given a 20 digit access code as a string, check if the card is locked Given a 20 digit access code as a string, check if the card is locked
@@ -115,45 +112,6 @@ class CardData(BaseData):
return None return None
return result.lastrowid return result.lastrowid
async def update_card_last_login(self, access_code: str) -> None:
sql = aime_card.update(aime_card.c.access_code == access_code).values(
last_login_date=func.now()
)
result = await self.execute(sql)
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}")
async def set_memo_by_access_code(self, access_code: str, memo: str) -> None:
result = await self.execute(aime_card.update(aime_card.c.access_code == access_code).values(memo=memo))
if not result:
self.logger.error(f"Failed to add memo to card {access_code}")
def to_access_code(self, luid: str) -> str: 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 Given a felica cards internal 16 hex character luid, convert it to a 0-padded 20 digit access code as a string
@@ -164,4 +122,4 @@ class CardData(BaseData):
""" """
Given a 20 digit access code as a string, return the 16 hex character luid 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

@@ -108,19 +108,3 @@ class UserData(BaseData):
if result is None: if result is None:
return False return False
return result.fetchall() return result.fetchall()
async def change_password(self, user_id: int, new_passwd: str) -> bool:
sql = aime_user.update(aime_user.c.id == user_id).values(password = new_passwd)
result = await self.execute(sql)
return result is not None
async def change_username(self, user_id: int, new_name: str) -> bool:
sql = aime_user.update(aime_user.c.id == user_id).values(username = new_name)
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

@@ -10,9 +10,6 @@ import bcrypt
import re import re
import jwt import jwt
import yaml import yaml
import secrets
import string
import random
from base64 import b64decode from base64 import b64decode
from enum import Enum from enum import Enum
from datetime import datetime, timezone from datetime import datetime, timezone
@@ -44,13 +41,11 @@ class ShopOwner():
self.permissions = perms self.permissions = perms
class UserSession(): class UserSession():
def __init__(self, usr_id: int = 0, ip: str = "", perms: int = 0, ongeki_ver: int = 7, chunithm_ver: int = -1, maimai_version: int = -1): def __init__(self, usr_id: int = 0, ip: str = "", perms: int = 0, ongeki_ver: int = 7):
self.user_id = usr_id self.user_id = usr_id
self.current_ip = ip self.current_ip = ip
self.permissions = perms self.permissions = perms
self.ongeki_version = ongeki_ver self.ongeki_version = ongeki_ver
self.chunithm_version = chunithm_ver
self.maimai_version = maimai_version
class FrontendServlet(): class FrontendServlet():
def __init__(self, cfg: CoreConfig, config_dir: str) -> None: def __init__(self, cfg: CoreConfig, config_dir: str) -> None:
@@ -134,13 +129,8 @@ class FrontendServlet():
]), ]),
Mount("/sys", routes=[ Mount("/sys", routes=[
Route("/", self.system.render_GET, methods=['GET']), 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.user", self.system.lookup_user, methods=['GET']),
Route("/lookup.shop", self.system.lookup_shop, methods=['GET']), Route("/lookup.shop", self.system.lookup_shop, methods=['GET']),
Route("/add.user", self.system.add_user, methods=['POST']),
Route("/add.card", self.system.add_card, methods=['POST']),
Route("/add.shop", self.system.add_shop, methods=['POST']),
Route("/add.cab", self.system.add_cab, methods=['POST']),
]), ]),
Mount("/shop", routes=[ Mount("/shop", routes=[
Route("/", self.arcade.render_GET, methods=['GET']), Route("/", self.arcade.render_GET, methods=['GET']),
@@ -190,10 +180,10 @@ class FE_Base():
title=self.core_config.server.name, title=self.core_config.server.name,
game_list=self.environment.globals["game_list"], game_list=self.environment.globals["game_list"],
sesh=vars(sesh) if sesh is not None else vars(UserSession()), sesh=vars(sesh) if sesh is not None else vars(UserSession()),
), media_type="text/html; charset=utf-8") ))
if sesh is None: if sesh is None:
resp.delete_cookie("ARTEMIS_SESH") resp.delete_cookie("DIANA_SESH")
return resp return resp
def get_routes(self) -> List[Route]: def get_routes(self) -> List[Route]:
@@ -216,10 +206,7 @@ class FE_Base():
sesh.user_id = tk['user_id'] sesh.user_id = tk['user_id']
sesh.current_ip = tk['current_ip'] sesh.current_ip = tk['current_ip']
sesh.permissions = tk['permissions'] 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: if sesh.user_id <= 0:
self.logger.error("User session failed to validate due to an invalid ID!") self.logger.error("User session failed to validate due to an invalid ID!")
return UserSession() return UserSession()
@@ -244,7 +231,7 @@ class FE_Base():
return UserSession() return UserSession()
def validate_session(self, request: Request) -> Optional[UserSession]: def validate_session(self, request: Request) -> Optional[UserSession]:
sesh = request.cookies.get('ARTEMIS_SESH', "") sesh = request.cookies.get('DIANA_SESH', "")
if not sesh: if not sesh:
return None return None
@@ -258,22 +245,12 @@ class FE_Base():
if usr_sesh.permissions <= 0 or usr_sesh.permissions > 255: if usr_sesh.permissions <= 0 or usr_sesh.permissions > 255:
self.logger.error(f"User session failed to validate due to an invalid permission value! {usr_sesh.permissions}") self.logger.error(f"User session failed to validate due to an invalid permission value! {usr_sesh.permissions}")
return None return None
return usr_sesh return usr_sesh
def encode_session(self, sesh: UserSession, exp_seconds: int = 86400) -> str: def encode_session(self, sesh: UserSession, exp_seconds: int = 86400) -> str:
try: try:
return jwt.encode({ return jwt.encode({ "user_id": sesh.user_id, "current_ip": sesh.current_ip, "permissions": sesh.permissions, "ongeki_version": sesh.ongeki_version, "exp": int(datetime.now(tz=timezone.utc).timestamp()) + exp_seconds }, b64decode(self.core_config.frontend.secret), algorithm="HS256")
"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: except jwt.InvalidKeyError:
self.logger.error("Failed to encode User session because the secret is invalid!") self.logger.error("Failed to encode User session because the secret is invalid!")
return "" return ""
@@ -304,8 +281,8 @@ class FE_Gate(FE_Base):
title=f"{self.core_config.server.name} | Login Gate", title=f"{self.core_config.server.name} | Login Gate",
error=err, error=err,
sesh=vars(UserSession()), sesh=vars(UserSession()),
), media_type="text/html; charset=utf-8") ))
resp.delete_cookie("ARTEMIS_SESH") resp.delete_cookie("DIANA_SESH")
return resp return resp
async def render_login(self, request: Request): async def render_login(self, request: Request):
@@ -321,12 +298,8 @@ class FE_Gate(FE_Base):
uid = await self.data.card.get_user_id_from_card(access_code) uid = await self.data.card.get_user_id_from_card(access_code)
if uid is None: if uid is None:
user = await self.data.user.get_user_by_username(access_code) # Lookup as username self.logger.debug(f"Failed to find user for card {access_code}")
if not user: return RedirectResponse("/gate/?e=1", 303)
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) user = await self.data.user.get_user(uid)
if user is None: if user is None:
@@ -337,7 +310,7 @@ class FE_Gate(FE_Base):
sesh = await self.data.user.check_password(uid) sesh = await self.data.user.check_password(uid)
if sesh is not None: if sesh is not None:
return RedirectResponse(f"/gate/create?ac={access_code}", 303) return RedirectResponse(f"/gate/create?ac={access_code}")
return RedirectResponse("/gate/?e=1", 303) return RedirectResponse("/gate/?e=1", 303)
@@ -355,17 +328,16 @@ class FE_Gate(FE_Base):
usr_sesh = self.encode_session(sesh) usr_sesh = self.encode_session(sesh)
self.logger.debug(f"Created session with JWT {usr_sesh}") self.logger.debug(f"Created session with JWT {usr_sesh}")
resp = RedirectResponse("/user/", 303) resp = RedirectResponse("/user/", 303)
resp.set_cookie("ARTEMIS_SESH", usr_sesh) resp.set_cookie("DIANA_SESH", usr_sesh)
return resp return resp
async def render_create(self, request: Request): async def render_create(self, request: Request):
ip = Utils.get_ip_addr(request) ip = Utils.get_ip_addr(request)
frm = await request.form() access_code: str = request.query_params.get("access_code", "")
access_code: str = frm.get("access_code", "") username: str = request.query_params.get("username", "")
username: str = frm.get("username", "") email: str = request.query_params.get("email", "")
email: str = frm.get("email", "") passwd: bytes = request.query_params.get("passwd", "").encode()
passwd: bytes = frm.get("passwd", "").encode()
if not access_code or not username or not email or not passwd: if not access_code or not username or not email or not passwd:
return RedirectResponse("/gate/?e=1", 303) return RedirectResponse("/gate/?e=1", 303)
@@ -393,13 +365,13 @@ class FE_Gate(FE_Base):
usr_sesh = self.encode_session(sesh) usr_sesh = self.encode_session(sesh)
self.logger.debug(f"Created session with JWT {usr_sesh}") self.logger.debug(f"Created session with JWT {usr_sesh}")
resp = RedirectResponse("/user/", 303) resp = RedirectResponse("/user", 303)
resp.set_cookie("ARTEMIS_SESH", usr_sesh) resp.set_cookie("DIANA_SESH", usr_sesh)
return resp return resp
async def render_create_get(self, request: Request): async def render_create_get(self, request: Request):
ac = request.query_params.get("ac", "") ac = request.query_params.get(b"ac", [b""])[0].decode()
if len(ac) != 20: if len(ac) != 20:
return RedirectResponse("/gate/?e=2", 303) return RedirectResponse("/gate/?e=2", 303)
@@ -420,7 +392,7 @@ class FE_Gate(FE_Base):
title=f"{self.core_config.server.name} | Create User", title=f"{self.core_config.server.name} | Create User",
code=ac, code=ac,
sesh={"user_id": 0, "permissions": 0}, sesh={"user_id": 0, "permissions": 0},
), media_type="text/html; charset=utf-8") ))
class FE_User(FE_Base): class FE_User(FE_Base):
async def render_GET(self, request: Request): async def render_GET(self, request: Request):
@@ -467,7 +439,10 @@ class FE_User(FE_Base):
elif ac.startswith("3"): elif ac.startswith("3"):
c_type = "Banapass" c_type = "Banapass"
elif ac.startswith("010"): elif ac.startswith("010"):
c_type = "Aime" # TODO: Aime verification c_type = "Unknown Aime"
desc, _ = self.data.card.get_aime_ac_key_desc(ac)
if desc is not None:
c_type = desc
elif ac.startswith("0008"): elif ac.startswith("0008"):
c_type = "Generated AIC" c_type = "Generated AIC"
else: else:
@@ -476,11 +451,10 @@ class FE_User(FE_Base):
card_data.append({ card_data.append({
'access_code': ac, 'access_code': ac,
'status': status, 'status': status,
'chip_id': c['chip_id'], 'chip_id': "", #None if c['chip_id'] is None else f"{c['chip_id']:X}",
'idm': c['idm'], 'idm': "",
'type': c_type, 'type': c_type,
"memo": c['memo'], "memo": ""
"id": c['id'],
}) })
if "e" in request.query_params: if "e" in request.query_params:
@@ -509,114 +483,18 @@ class FE_User(FE_Base):
success=succ, success=succ,
username=user['username'], username=user['username'],
arcades=arcade_data arcades=arcade_data
), media_type="text/html; charset=utf-8") ))
async def render_logout(self, request: Request): async def render_logout(self, request: Request):
resp = RedirectResponse("/gate/", 303) resp = RedirectResponse("/gate/", 303)
resp.delete_cookie("ARTEMIS_SESH") resp.delete_cookie("DIANA_SESH")
return resp return resp
async def edit_card(self, request: Request) -> RedirectResponse: async def edit_card(self, request: Request) -> RedirectResponse:
frm = await request.form() return RedirectResponse("/user/", 303)
usr_sesh = self.validate_session(request)
if not usr_sesh or not self.test_perm(usr_sesh.permissions, PermissionOffset.USERMOD):
return RedirectResponse("/gate/", 303)
frm = await request.form()
cid = frm.get("card_edit_frm_card_id", None)
if not cid:
return RedirectResponse("/user/?e=999", 303)
ac = frm.get("card_edit_frm_access_code", None)
if not ac:
return RedirectResponse("/user/?e=999", 303)
card = await self.data.card.get_card_by_id(cid)
if not card:
return RedirectResponse("/user/?e=2", 303)
if card['user'] != usr_sesh.user_id and not self.test_perm_minimum(usr_sesh.permissions, PermissionOffset.USERMOD):
return RedirectResponse("/user/?e=11", 303)
if frm.get("add_memo", None) or frm.get("add_memo", None) == "":
memo = frm.get("add_memo")
if len(memo) > 16:
return RedirectResponse("/user/?e=4", 303)
await self.data.card.set_memo_by_access_code(ac, memo)
if False: # Saving this in case I want to allow editing idm/chip ID down the line
if frm.get("add_felica_idm", None):
idm = frm.get('add_felica_idm')
if not all(c in string.hexdigits for c in idm):
return RedirectResponse("/user/?e=4", 303)
await self.data.card.set_idm_by_access_code(ac, idm)
if frm.get("add_mifare_chip_id", None):
chip_id: str = frm.get('add_mifare_chip_id')
if not all(c in string.hexdigits for c in idm):
return RedirectResponse("/user/?e=4", 303)
await self.data.card.set_chip_id_by_access_code(ac, int(chip_id, 16))
return RedirectResponse("/user/?s=4", 303)
async def add_card(self, request: Request) -> RedirectResponse: async def add_card(self, request: Request) -> RedirectResponse:
frm = await request.form() return RedirectResponse("/user/", 303)
card_type = frm.get("card_add_frm_type", None)
access_code = frm.get("add_access_code", None)
idm = frm.get("add_idm", None)
idm_caps = None
usr_sesh = self.validate_session(request)
if not usr_sesh or not self.test_perm(usr_sesh.permissions, PermissionOffset.USERMOD):
return RedirectResponse("/gate/", 303)
if not len(access_code) == 20 or (not access_code.startswith("5") and not access_code.startswith("3") \
and not access_code.startswith("010") and not access_code.startswith("0008")):
return RedirectResponse("/user/?e=4", 303)
if card_type == "0" and access_code.startswith("5") and len(idm) == 16:
idm_caps = idm.upper()
if not all([x in string.hexdigits for x in idm_caps]):
return RedirectResponse("/user/?e=4", 303)
if access_code.startswith("5") and not idm_caps:
return RedirectResponse("/user/?e=13", 303)
test = await self.data.card.get_card_by_access_code(access_code)
if test:
return RedirectResponse("/user/?e=12", 303)
if idm_caps:
test = await self.data.card.get_card_by_idm(idm_caps)
if test and test['user'] != usr_sesh.user_id:
return RedirectResponse("/user/?e=12", 303)
test = await self.data.card.get_card_by_access_code(self.data.card.to_access_code(idm_caps))
if test:
if test['user'] != usr_sesh.user_id:
return RedirectResponse("/user/?e=12", 303)
await self.data.card.set_access_code_by_access_code(test['access_code'], access_code)
self.logger.info(f"Update card {test['id']} from {test['access_code']} to {access_code} for user {usr_sesh.user_id}")
await self.data.card.set_idm_by_access_code(access_code, idm_caps)
self.logger.info(f"Set IDm for card {access_code} to {idm_caps}")
return RedirectResponse("/user/?s=1", 303)
if card_type == "0" and access_code.startswith("0008"):
test = await self.data.card.get_card_by_idm(self.data.card.to_idm(access_code))
if test:
return RedirectResponse("/user/?e=12", 303)
new_card = await self.data.card.create_card(usr_sesh.user_id, access_code)
self.logger.info(f"Created new card {new_card} with access code {access_code} for user {usr_sesh.user_id}")
if idm_caps:
await self.data.card.set_idm_by_access_code(access_code, idm_caps)
self.logger.info(f"Set IDm for card {access_code} to {idm_caps}")
return RedirectResponse("/user/?s=1", 303)
async def render_POST(self, request: Request): async def render_POST(self, request: Request):
frm = await request.form() frm = await request.form()
@@ -675,17 +553,11 @@ class FE_System(FE_Base):
if not usr_sesh or not self.test_perm_minimum(usr_sesh.permissions, PermissionOffset.USERMOD): if not usr_sesh or not self.test_perm_minimum(usr_sesh.permissions, PermissionOffset.USERMOD):
return RedirectResponse("/gate/", 303) return RedirectResponse("/gate/", 303)
if request.query_params.get("e", None):
err = int(request.query_params.get("e"))
else:
err = 0
return Response(template.render( return Response(template.render(
title=f"{self.core_config.server.name} | System", title=f"{self.core_config.server.name} | System",
sesh=vars(usr_sesh), sesh=vars(usr_sesh),
usrlist=[], usrlist=[],
error = err ))
), media_type="text/html; charset=utf-8")
async def lookup_user(self, request: Request): async def lookup_user(self, request: Request):
template = self.environment.get_template("core/templates/sys/index.jinja") template = self.environment.get_template("core/templates/sys/index.jinja")
@@ -718,7 +590,7 @@ class FE_System(FE_Base):
sesh=vars(usr_sesh), sesh=vars(usr_sesh),
usrlist=usrlist, usrlist=usrlist,
shoplist=[], shoplist=[],
), media_type="text/html; charset=utf-8") ))
async def lookup_shop(self, request: Request): async def lookup_shop(self, request: Request):
shoplist = [] shoplist = []
@@ -752,7 +624,7 @@ class FE_System(FE_Base):
usrlist=[], usrlist=[],
shoplist=shoplist, shoplist=shoplist,
error=4 error=4
), media_type="text/html; charset=utf-8") ))
if sn_search: if sn_search:
sn_search = sn_search.upper().replace("-", "").strip() sn_search = sn_search.upper().replace("-", "").strip()
@@ -781,7 +653,7 @@ class FE_System(FE_Base):
usrlist=[], usrlist=[],
shoplist=shoplist, shoplist=shoplist,
error=10 error=10
), media_type="text/html; charset=utf-8") ))
return Response(template.render( return Response(template.render(
@@ -789,149 +661,8 @@ class FE_System(FE_Base):
sesh=vars(usr_sesh), sesh=vars(usr_sesh),
usrlist=[], usrlist=[],
shoplist=shoplist, shoplist=shoplist,
), media_type="text/html; charset=utf-8") ))
async def add_user(self, request: Request):
template = self.environment.get_template("core/templates/sys/index.jinja")
usr_sesh = self.validate_session(request)
if not usr_sesh or not self.test_perm(usr_sesh.permissions, PermissionOffset.ACMOD):
return RedirectResponse("/gate/", 303)
frm = await request.form()
username = frm.get("userName", None)
email = frm.get("userEmail", None)
perm = frm.get("usrPerm", "1")
passwd = "".join(
secrets.choice(string.ascii_letters + string.digits) for i in range(20)
)
hash = bcrypt.hashpw(passwd.encode(), bcrypt.gensalt())
if not email:
return RedirectResponse("/sys/?e=4", 303)
uid = await self.data.user.create_user(username=username if username else None, email=email, password=hash.decode(), permission=int(perm))
return Response(template.render(
title=f"{self.core_config.server.name} | System",
sesh=vars(usr_sesh),
usradd={"id": uid, "username": username, "password": passwd},
), media_type="text/html; charset=utf-8")
async def add_card(self, request: Request):
template = self.environment.get_template("core/templates/sys/index.jinja")
usr_sesh = self.validate_session(request)
if not usr_sesh or not self.test_perm(usr_sesh.permissions, PermissionOffset.ACMOD):
return RedirectResponse("/gate/", 303)
frm = await request.form()
userid = frm.get("cardUsr", None)
access_code = frm.get("cardAc", None)
idm = frm.get("cardIdm", None)
if userid is None or access_code is None or not userid.isdigit() or not len(access_code) == 20 or not access_code.isdigit:
return RedirectResponse("/sys/?e=4", 303)
cardid = await self.data.card.create_card(int(userid), access_code)
if not cardid:
return RedirectResponse("/sys/?e=99", 303)
if idm is not None:
# TODO: save IDM
pass
return Response(template.render(
title=f"{self.core_config.server.name} | System",
sesh=vars(usr_sesh),
cardadd={"id": cardid, "user": userid, "access_code": access_code},
), media_type="text/html; charset=utf-8")
async def add_shop(self, request: Request):
template = self.environment.get_template("core/templates/sys/index.jinja")
usr_sesh = self.validate_session(request)
if not usr_sesh or not self.test_perm(usr_sesh.permissions, PermissionOffset.ACMOD):
return RedirectResponse("/gate/", 303)
frm = await request.form()
name = frm.get("shopName", None)
country = frm.get("shopCountry", "JPN")
ip = frm.get("shopIp", None)
acid = await self.data.arcade.create_arcade(name if name else None, name if name else None, country)
if not acid:
return RedirectResponse("/sys/?e=99", 303)
if ip:
# TODO: set IP
pass
return Response(template.render(
title=f"{self.core_config.server.name} | System",
sesh=vars(usr_sesh),
shopadd={"id": acid},
), media_type="text/html; charset=utf-8")
async def add_cab(self, request: Request):
template = self.environment.get_template("core/templates/sys/index.jinja")
usr_sesh = self.validate_session(request)
if not usr_sesh or not self.test_perm(usr_sesh.permissions, PermissionOffset.ACMOD):
return RedirectResponse("/gate/", 303)
frm = await request.form()
shopid = frm.get("cabShop", None)
serial = frm.get("cabSerial", None)
game_code = frm.get("cabGame", None)
if not shopid or not shopid.isdigit():
return RedirectResponse("/sys/?e=4", 303)
if not serial:
append = self.data.arcade.get_keychip_suffix(datetime.now().year, datetime.now().month)
generated = await self.data.arcade.get_num_generated_keychips()
if not generated:
generated = 0
serial = self.data.arcade.format_serial("A69A", 1, "A", generated + 1, int(append))
serial_dash = self.data.arcade.format_serial("A69A", 1, "A", generated + 1, int(append), True)
cab_id = await self.data.arcade.create_machine(int(shopid), serial, None, game_code if game_code else None)
return Response(template.render(
title=f"{self.core_config.server.name} | System",
sesh=vars(usr_sesh),
cabadd={"id": cab_id, "serial": serial_dash},
), 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): class FE_Arcade(FE_Base):
async def render_GET(self, request: Request): async def render_GET(self, request: Request):
template = self.environment.get_template("core/templates/arcade/index.jinja") template = self.environment.get_template("core/templates/arcade/index.jinja")
@@ -946,14 +677,14 @@ class FE_Arcade(FE_Base):
return Response(template.render( return Response(template.render(
title=f"{self.core_config.server.name} | Arcade", title=f"{self.core_config.server.name} | Arcade",
sesh=vars(usr_sesh), sesh=vars(usr_sesh),
), media_type="text/html; charset=utf-8") ))
sinfo = await self.data.arcade.get_arcade(shop_id) sinfo = await self.data.arcade.get_arcade(shop_id)
if not sinfo: if not sinfo:
return Response(template.render( return Response(template.render(
title=f"{self.core_config.server.name} | Arcade", title=f"{self.core_config.server.name} | Arcade",
sesh=vars(usr_sesh), sesh=vars(usr_sesh),
), media_type="text/html; charset=utf-8") ))
cabs = await self.data.arcade.get_arcade_machines(shop_id) cabs = await self.data.arcade.get_arcade_machines(shop_id)
cablst = [] cablst = []
@@ -974,7 +705,7 @@ class FE_Arcade(FE_Base):
"cabs": cablst "cabs": cablst
} }
), media_type="text/html; charset=utf-8") ))
class FE_Machine(FE_Base): class FE_Machine(FE_Base):
async def render_GET(self, request: Request): async def render_GET(self, request: Request):
@@ -990,15 +721,15 @@ class FE_Machine(FE_Base):
return Response(template.render( return Response(template.render(
title=f"{self.core_config.server.name} | Machine", title=f"{self.core_config.server.name} | Machine",
sesh=vars(usr_sesh), sesh=vars(usr_sesh),
), media_type="text/html; charset=utf-8") ))
return Response(template.render( return Response(template.render(
title=f"{self.core_config.server.name} | Machine", title=f"{self.core_config.server.name} | Machine",
sesh=vars(usr_sesh), sesh=vars(usr_sesh),
arcade={} arcade={}
), media_type="text/html; charset=utf-8") ))
cfg_dir = environ.get("ARTEMIS_CFG_DIR", "config") cfg_dir = environ.get("DIANA_CFG_DIR", "config")
cfg: CoreConfig = CoreConfig() cfg: CoreConfig = CoreConfig()
if path.exists(f"{cfg_dir}/core.yaml"): if path.exists(f"{cfg_dir}/core.yaml"):
cfg.update(yaml.safe_load(open(f"{cfg_dir}/core.yaml"))) cfg.update(yaml.safe_load(open(f"{cfg_dir}/core.yaml")))

View File

@@ -2,7 +2,6 @@ from typing import Dict, Any, Optional
import logging, coloredlogs import logging, coloredlogs
from logging.handlers import TimedRotatingFileHandler from logging.handlers import TimedRotatingFileHandler
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import PlainTextResponse
from datetime import datetime from datetime import datetime
from Crypto.Cipher import Blowfish from Crypto.Cipher import Blowfish
import pytz import pytz
@@ -10,11 +9,9 @@ import pytz
from .config import CoreConfig from .config import CoreConfig
from .utils import Utils from .utils import Utils
from .title import TitleServlet from .title import TitleServlet
from .data import Data
from .const import *
class MuchaServlet: class MuchaServlet:
mucha_registry: Dict[str, Dict[str, str]] = {} mucha_registry: Dict[str, str] = {}
def __init__(self, cfg: CoreConfig, cfg_dir: str) -> None: def __init__(self, cfg: CoreConfig, cfg_dir: str) -> None:
self.config = cfg self.config = cfg
self.config_dir = cfg_dir self.config_dir = cfg_dir
@@ -38,148 +35,91 @@ class MuchaServlet:
self.logger.setLevel(cfg.mucha.loglevel) self.logger.setLevel(cfg.mucha.loglevel)
coloredlogs.install(level=cfg.mucha.loglevel, logger=self.logger, fmt=log_fmt_str) coloredlogs.install(level=cfg.mucha.loglevel, logger=self.logger, fmt=log_fmt_str)
self.data = Data(cfg)
for _, mod in TitleServlet.title_registry.items(): for _, mod in TitleServlet.title_registry.items():
enabled, game_cds, netids = mod.get_mucha_info(self.config, self.config_dir) if hasattr(mod, "get_mucha_info"):
if enabled: enabled, game_cds, netid_prefixes = mod.get_mucha_info(
for x in range(len(game_cds)): self.config, self.config_dir
self.mucha_registry[game_cds[x]] = { "netid_prefix": netids[x] } )
if enabled:
for x in range(len(game_cds)):
self.mucha_registry[game_cds[x]] = netid_prefixes[x]
self.logger.info(f"Serving {len(self.mucha_registry)} games") self.logger.info(f"Serving {len(self.mucha_registry)} games")
async def handle_boardauth(self, request: Request) -> bytes: def handle_boardauth(self, request: Request, _: Dict) -> bytes:
bod = await request.body() req_dict = self.mucha_preprocess(request.content.getvalue())
req_dict = self.mucha_preprocess(bod)
client_ip = Utils.get_ip_addr(request) client_ip = Utils.get_ip_addr(request)
if req_dict is None: if req_dict is None:
self.logger.error( self.logger.error(
f"Error processing mucha request {bod}" f"Error processing mucha request {request.content.getvalue()}"
) )
return PlainTextResponse("RESULTS=000") return b"RESULTS=000"
req = MuchaAuthRequest(req_dict) req = MuchaAuthRequest(req_dict)
self.logger.debug(f"Mucha request {vars(req)}") self.logger.info(f"Boardauth request from {client_ip} for {req.gameVer}")
if not req.gameCd or not req.gameVer or not req.sendDate or not req.countryCd or not req.serialNum:
self.logger.warn(f"Missing required fields - {vars(req)}")
return PlainTextResponse("RESULTS=000")
minfo = self.mucha_registry.get(req.gameCd, {})
if not minfo:
self.logger.warning(f"Unknown gameCd {req.gameCd} from {client_ip}")
return PlainTextResponse("RESULTS=000")
b_key = b""
for x in range(8):
b_key += req.sendDate[(x - 1) & 7].encode()
b_iv = b_key # what the fuck namco
cipher = Blowfish.new(b_key, Blowfish.MODE_CBC, b_iv)
try:
sn_decrypt = cipher.decrypt(bytes.fromhex(req.serialNum))[:12].decode()
except Exception as e:
self.logger.error(f"Decrypt SN {req.serialNum} failed! - {e}")
return PlainTextResponse("RESULTS=000")
self.logger.info(f"Boardauth request from {sn_decrypt} ({client_ip}) for {req.gameVer}")
resp = MuchaAuthResponse(
f"{self.config.server.hostname}{':' + str(self.config.server.port) if not self.config.server.is_using_proxy else ''}"
)
netid = minfo.get('netid_prefix', "ABxN") + sn_decrypt[5:]
cab = await self.data.arcade.get_machine(netid)
if cab:
arcade = await self.data.arcade.get_arcade(cab['id'])
if not arcade:
self.logger.error(f"Failed to get arcade with id {cab['id']}")
return PlainTextResponse("RESULTS=000")
resp.AREA_0 = arcade["region_id"] or AllnetJapanRegionId.AICHI.name
resp.AREA_0_EN = arcade["region_id"] or AllnetJapanRegionId.AICHI.name
resp.AREA_FULL_0 = arcade["region_id"] or AllnetJapanRegionId.AICHI.name
resp.AREA_FULL_0_EN = arcade["region_id"] or AllnetJapanRegionId.AICHI.name
resp.AREA_1 = arcade["country"] or cab['country'] or AllnetCountryCode.JAPAN.value
resp.AREA_1_EN = arcade["country"] or cab['country'] or AllnetCountryCode.JAPAN.value
resp.AREA_FULL_1 = arcade["country"] or cab['country'] or AllnetCountryCode.JAPAN.value
resp.AREA_FULL_1_EN = arcade["country"] or cab['country'] or AllnetCountryCode.JAPAN.value
resp.AREA_2 = arcade["city"] if arcade["city"] else ""
resp.AREA_2_EN = arcade["city"] if arcade["city"] else ""
resp.AREA_FULL_2 = arcade["city"] if arcade["city"] else ""
resp.AREA_FULL_2_EN = arcade["city"] if arcade["city"] else ""
resp.AREA_3 = ""
resp.AREA_3_EN = ""
resp.AREA_FULL_3 = ""
resp.AREA_FULL_3_EN = ""
resp.PREFECTURE_ID = arcade['region_id']
resp.COUNTRY_CD = arcade['country'] or cab['country'] or AllnetCountryCode.JAPAN.value
resp.PLACE_ID = req.placeId if req.placeId else f"{arcade['country'] or cab['country'] or AllnetCountryCode.JAPAN.value}{arcade['id']:04X}"
resp.SHOP_NAME = arcade['name']
resp.SHOP_NAME_EN = arcade['name']
resp.SHOP_NICKNAME = arcade['nickname']
resp.SHOP_NICKNAME_EN = arcade['nickname']
elif self.config.server.allow_unregistered_serials:
self.logger.info(f"Allow unknown serial {netid} ({sn_decrypt}) to auth")
else:
self.logger.warn(f'Auth failed for NetID {netid}')
return PlainTextResponse("RESULTS=000")
self.logger.debug(f"Mucha response {vars(resp)}")
return PlainTextResponse(self.mucha_postprocess(vars(resp)))
async def handle_updatecheck(self, request: Request) -> bytes:
bod = await request.body()
req_dict = self.mucha_preprocess(bod)
client_ip = Utils.get_ip_addr(request)
if req_dict is None:
self.logger.error(
f"Error processing mucha request {bod}"
)
return PlainTextResponse("RESULTS=000")
req = MuchaUpdateRequest(req_dict)
self.logger.info(f"Updatecheck request from {req.serialNum} ({client_ip}) for {req.gameVer}")
self.logger.debug(f"Mucha request {vars(req)}") self.logger.debug(f"Mucha request {vars(req)}")
if req.gameCd not in self.mucha_registry: if req.gameCd not in self.mucha_registry:
self.logger.warning(f"Unknown gameCd {req.gameCd}") self.logger.warning(f"Unknown gameCd {req.gameCd}")
return PlainTextResponse("RESULTS=000") return b"RESULTS=000"
resp = MuchaUpdateResponse(req.gameVer, f"{self.config.server.hostname}{':' + str(self.config.server.port) if not self.config.server.is_using_proxy else ''}") # TODO: Decrypt S/N
b_key = b""
for x in range(8):
b_key += req.sendDate[(x - 1) & 7].encode()
cipher = Blowfish.new(b_key, Blowfish.MODE_ECB)
sn_decrypt = cipher.decrypt(bytes.fromhex(req.serialNum))
self.logger.debug(f"Decrypt SN to {sn_decrypt.hex()}")
resp = MuchaAuthResponse(
f"{self.config.server.hostname}{':' + str(self.config.server.port) if self.config.server.is_develop else ''}"
)
self.logger.debug(f"Mucha response {vars(resp)}") self.logger.debug(f"Mucha response {vars(resp)}")
return PlainTextResponse(self.mucha_postprocess(vars(resp))) return self.mucha_postprocess(vars(resp))
async def handle_dlstate(self, request: Request) -> bytes: def handle_updatecheck(self, request: Request, _: Dict) -> bytes:
bod = await request.body() req_dict = self.mucha_preprocess(request.content.getvalue())
req_dict = self.mucha_preprocess(bod)
client_ip = Utils.get_ip_addr(request) client_ip = Utils.get_ip_addr(request)
if req_dict is None: if req_dict is None:
self.logger.error( self.logger.error(
f"Error processing mucha request {bod}" f"Error processing mucha request {request.content.getvalue()}"
) )
return PlainTextResponse("RESULTS=000") return b"RESULTS=000"
req = MuchaUpdateRequest(req_dict)
self.logger.info(f"Updatecheck request from {client_ip} for {req.gameVer}")
self.logger.debug(f"Mucha request {vars(req)}")
if req.gameCd not in self.mucha_registry:
self.logger.warning(f"Unknown gameCd {req.gameCd}")
return b"RESULTS=000"
resp = MuchaUpdateResponse(req.gameVer, f"{self.config.server.hostname}{':' + str(self.config.server.port) if self.config.server.is_develop else ''}")
self.logger.debug(f"Mucha response {vars(resp)}")
return self.mucha_postprocess(vars(resp))
def handle_dlstate(self, request: Request, _: Dict) -> bytes:
req_dict = self.mucha_preprocess(request.content.getvalue())
client_ip = Utils.get_ip_addr(request)
if req_dict is None:
self.logger.error(
f"Error processing mucha request {request.content.getvalue()}"
)
return b""
req = MuchaDownloadStateRequest(req_dict) req = MuchaDownloadStateRequest(req_dict)
self.logger.info(f"DownloadState request from {req.serialNum} ({client_ip}) for {req.gameCd} -> {req.updateVer}") self.logger.info(f"DownloadState request from {client_ip} for {req.gameCd} -> {req.updateVer}")
self.logger.debug(f"request {vars(req)}") self.logger.debug(f"request {vars(req)}")
return PlainTextResponse("RESULTS=001") return b"RESULTS=001"
def mucha_preprocess(self, data: bytes) -> Optional[Dict]: def mucha_preprocess(self, data: bytes) -> Optional[Dict]:
try: try:
@@ -229,7 +169,7 @@ class MuchaAuthResponse:
self.RESULTS = "001" self.RESULTS = "001"
self.AUTH_INTERVAL = "86400" self.AUTH_INTERVAL = "86400"
self.SERVER_TIME = datetime.strftime(datetime.now(), "%Y%m%d%H%M") self.SERVER_TIME = datetime.strftime(datetime.now(), "%Y%m%d%H%M")
self.SERVER_TIME_UTC = datetime.strftime(datetime.now(pytz.UTC), "%Y%m%d%H%M") self.UTC_SERVER_TIME = datetime.strftime(datetime.now(pytz.UTC), "%Y%m%d%H%M")
self.CHARGE_URL = f"https://{mucha_url}/charge/" self.CHARGE_URL = f"https://{mucha_url}/charge/"
self.FILE_URL = f"https://{mucha_url}/file/" self.FILE_URL = f"https://{mucha_url}/file/"

View File

@@ -10,7 +10,7 @@ Cab added successfully
{% endif %} {% endif %}
<ul style="font-size: 20px;"> <ul style="font-size: 20px;">
{% for c in arcade.cabs %} {% for c in arcade.cabs %}
<li><a href="/cab/{{ c.id }}">{{ c.serial }}</a> ({{ c.game if c.game else "Any" }})&nbsp;<button class="btn btn-secondary" onclick="prep_edit_form()">Edit</button>&nbsp;<button class="btn-danger btn">Delete</button></li> <li><a href="/cab/{{ c.id }}">{{ c.serial }} ({{ c.game }})</a>&nbsp;<button class="btn btn-secondary" onclick="prep_edit_form()">Edit</button>&nbsp;<button class="btn-danger btn">Delete</button></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% else %} {% else %}

View File

@@ -15,18 +15,18 @@
-moz-appearance: textfield; -moz-appearance: textfield;
} }
</style> </style>
<form id="login" style="max-width: 240px; min-width: 15%;" action="/gate/gate.login" method="post"> <form id="login" style="max-width: 240px; min-width: 10%;" action="/gate/gate.login" method="post">
<div class="form-group row"> <div class="form-group row">
<label for="access_code">Access Code or Username</label><br> <label for="access_code">Card Access Code</label><br>
<input form="login" class="form-control" name="access_code" id="access_code" placeholder="00000000000000000000" maxlength="20" required aria-describedby="access_code_help"> <input form="login" class="form-control" name="access_code" id="access_code" type="number" placeholder="00000000000000000000" maxlength="20" required>
<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>
<div class="form-group row"> <div class="form-group row">
<label for="passwd">Password</label><br> <label for="passwd">Password</label><br>
<input id="passwd" class="form-control" name="passwd" type="password" placeholder="password" aria-describedby="passwd_help"> <input id="passwd" class="form-control" name="passwd" type="password" placeholder="password">
<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> </div>
<p></p> <p></p>
<input id="submit" class="btn btn-primary" style="display: block; margin: 0 auto;" form="login" type="submit" value="Login"> <input id="submit" class="btn btn-primary" style="display: block; margin: 0 auto;" form="login" type="submit" value="Login">
</form> </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 %} {% endblock content %}

View File

@@ -4,7 +4,6 @@
{% if error is defined %} {% if error is defined %}
{% include "core/templates/widgets/err_banner.jinja" %} {% include "core/templates/widgets/err_banner.jinja" %}
{% endif %} {% endif %}
<h2>Search</h2>
<div class="row" id="rowForm"> <div class="row" id="rowForm">
{% if "{:08b}".format(sesh.permissions)[6] == "1" %} {% if "{:08b}".format(sesh.permissions)[6] == "1" %}
<div class="col-sm-6" style="max-width: 25%;"> <div class="col-sm-6" style="max-width: 25%;">
@@ -22,12 +21,7 @@
OR OR
<div class="form-group"> <div class="form-group">
<label for="usrEmail">Email address</label> <label for="usrEmail">Email address</label>
<input type="email" class="form-control" id="usrEmail" name="usrEmail"> <input type="email" class="form-control" id="usrEmail" name="usrEmail" aria-describedby="emailHelp">
</div>
OR
<div class="form-group">
<label for="usrAc">Access Code</label>
<input type="text" class="form-control" id="usrAc" name="usrAc" maxlength="20" placeholder="00000000000000000000">
</div> </div>
<br /> <br />
<button type="submit" class="btn btn-primary">Search</button> <button type="submit" class="btn btn-primary">Search</button>
@@ -51,9 +45,6 @@
<button type="submit" class="btn btn-primary">Search</button> <button type="submit" class="btn btn-primary">Search</button>
</form> </form>
</div> </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 %} {% endif %}
</div> </div>
<div class="row" id="rowResult" style="margin: 10px;"> <div class="row" id="rowResult" style="margin: 10px;">
@@ -72,121 +63,7 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
<h2>Add</h2>
<div class="row" id="rowAdd"> <div class="row" id="rowAdd">
{% if "{:08b}".format(sesh.permissions)[6] == "1" %}
<div class="col-sm-6" style="max-width: 25%;">
<form id="usrAdd" name="usrAdd" action="/sys/add.user" class="form-inline" method="POST">
<h3>Add User</h3>
<div class="form-group">
<label for="usrName">Username</label>
<input type="text" class="form-control" id="usrName" name="usrName">
</div>
<br>
<div class="form-group">
<label for="usrEmail">Email address</label>
<input type="email" class="form-control" id="usrEmail" name="usrEmail" required>
</div>
<br>
<div class="form-group">
<label for="usrPerm">Permission Level</label>
<input type="number" class="form-control" id="usrPerm" name="usrPerm" value="1">
</div>
<br />
<button type="submit" class="btn btn-primary">Add</button>
</form>
</div>
<div class="col-sm-6" style="max-width: 25%;">
<form id="cardAdd" name="cardAdd" action="/sys/add.card" class="form-inline" method="POST">
<h3>Add Card</h3>
<div class="form-group">
<label for="cardUsr">User ID</label>
<input type="number" class="form-control" id="cardUsr" name="cardUsr" required>
</div>
<br>
<div class="form-group">
<label for="cardAc">Access Code</label>
<input type="text" class="form-control" id="cardAc" name="cardAc" maxlength="20" placeholder="00000000000000000000" required>
</div>
<br>
<div class="form-group">
<label for="cardIdm">IDm/Chip ID</label>
<input type="text" class="form-control" id="cardIdm" name="cardIdm" disabled>
</div>
<br />
<button type="submit" class="btn btn-primary">Add</button>
</form>
</div>
{% endif %}
{% if "{:08b}".format(sesh.permissions)[5] == "1" %}
<div class="col-sm-6" style="max-width: 25%;">
<form id="shopAdd" name="shopAdd" action="/sys/add.shop" class="form-inline" method="POST">
<h3>Add Shop</h3>
<div class="form-group">
<label for="shopName">Name</label>
<input type="text" class="form-control" id="shopName" name="shopName">
</div>
<br>
<div class="form-group">
<label for="shopCountry">Country Code</label>
<input type="text" class="form-control" id="shopCountry" name="shopCountry" maxlength="3" placeholder="JPN">
</div>
<br />
<div class="form-group">
<label for="shopIp">VPN IP</label>
<input type="text" class="form-control" id="shopIp" name="shopIp">
</div>
<br />
<button type="submit" class="btn btn-primary">Add</button>
</form>
</div>
<div class="col-sm-6" style="max-width: 25%;">
<form id="cabAdd" name="cabAdd" action="/sys/add.cab" class="form-inline" method="POST">
<h3>Add Machine</h3>
<div class="form-group">
<label for="cabShop">Shop ID</label>
<input type="number" class="form-control" id="cabShop" name="cabShop" required>
</div>
<br>
<div class="form-group">
<label for="cabSerial">Serial</label>
<input type="text" class="form-control" id="cabSerial" name="cabSerial">
</div>
<br />
<div class="form-group">
<label for="cabGame">Game Code</label>
<input type="text" class="form-control" id="cabGame" name="cabGame" maxlength="4" placeholder="SXXX">
</div>
<br />
<button type="submit" class="btn btn-primary">Add</button>
</form>
</div>
{% endif %}
</div>
<div class="row" id="rowAddResult" style="margin: 10px;">
{% if "{:08b}".format(sesh.permissions)[6] == "1" %}
<div id="userAddResult" class="col-sm-6" style="max-width: 25%;">
{% if usradd is defined %}
<pre>Added user {{ usradd.username if usradd.username is not none else "with no name"}} with id {{usradd.id}} and password {{ usradd.password }}</pre>
{% endif %}
</div>
<div id="cardAddResult" class="col-sm-6" style="max-width: 25%;">
{% if cardadd is defined %}
<pre>Added {{ cardadd.access_code }} with id {{cardadd.id}} to user {{ cardadd.user }}</pre>
{% endif %}
</div>
{% endif %}
{% if "{:08b}".format(sesh.permissions)[5] == "1" %}
<div id="shopAddResult" class="col-sm-6" style="max-width: 25%;">
{% if shopadd is defined %}
<pre>Added Shop {{ shopadd.id }}</pre></a>
{% endif %}
</div>
<div id="cabAddResult" class="col-sm-6" style="max-width: 25%;">
{% if cabadd is defined %}
<pre>Added Machine {{ cabadd.id }} with serial {{ cabadd.serial }}</pre></a>
{% endif %}
</div>
{% endif %}
</div> </div>
{% endblock content %} {% endblock content %}

View File

@@ -1,202 +0,0 @@
{% 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) {
document.getElementById("btn_next").disabled = true;
document.getElementById("btn_prev").disabled = true;
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 = TBL_DATA.length == 0;
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

@@ -28,28 +28,12 @@ function toggle_add_card_form() {
} }
} }
function toggle_idm_disabled(is_disabled) { function prep_edit_form(access_code, chip_id, idm, card_type, u_memo) {
document.getElementById("btn_add_card");
let dv = document.getElementById("add_card_container")
if (dv.style['display'] != "") {
btn.innerText = "Cancel";
dv.style['display'] = "";
} else {
btn.innerText = "Add";
dv.style['display'] = "none";
}
}
function prep_edit_form(access_code, chip_id, idm, card_type, u_memo, card_id) {
ac = document.getElementById("card_edit_frm_access_code"); ac = document.getElementById("card_edit_frm_access_code");
cid = document.getElementById("card_edit_frm_chip_id"); cid = document.getElementById("card_edit_frm_chip_id");
fidm = document.getElementById("card_edit_frm_idm"); fidm = document.getElementById("card_edit_frm_idm");
memo = document.getElementById("card_edit_frm_memo"); memo = document.getElementById("card_edit_frm_memo");
document.getElementById("card_edit_frm_card_id").value = card_id;
if (chip_id == "None" || chip_id == undefined) { if (chip_id == "None" || chip_id == undefined) {
chip_id = "" chip_id = ""
} }
@@ -65,14 +49,11 @@ function prep_edit_form(access_code, chip_id, idm, card_type, u_memo, card_id) {
fidm.value = idm; fidm.value = idm;
memo.value = u_memo; memo.value = u_memo;
if (access_code.startsWith("3") || access_code.startsWith("010")) { if (card_type == "AmusementIC") {
cid.disabled = false;
fidm.disabled = true;
} else if (access_code.startsWith("5") || access_code.startsWith("0008")) {
cid.disabled = true; cid.disabled = true;
fidm.disabled = false; fidm.disabled = false;
} else { } else {
cid.disabled = true; cid.disabled = false;
fidm.disabled = true; fidm.disabled = true;
} }
} }
@@ -103,35 +84,16 @@ Card added successfully
{% endif %} {% endif %}
<div id="add_card_container" style="display: none; max-width: 33%;"> <div id="add_card_container" style="display: none; max-width: 33%;">
<form action="/user/add.card" method="post", id="frm_add_card"> <form action="/user/add.card" method="post", id="frm_add_card">
<div class="form-check">
<input type="radio" id="card_add_frm_type_aicc" value="0" name="card_add_frm_type" aria-describedby="aicc_help" onclick="document.getElementById('card_add_frm_idm').disabled = false;">
<label class="form-label" for="card_add_frm_type_aicc">AmusementIC</label>
<div id="aicc_help" class="form-text">Starts with 5. If you don't have the IDm, use the 0008 access code shown in-game</div>
<br>
<input type="radio" id="card_add_frm_type_old" value="1" name="card_add_frm_type" aria-describedby="old_help" onclick="document.getElementById('card_add_frm_idm').disabled = true;">
<label class="form-label" for="card_add_frm_type_old">Old Aime/Banapass</label>
<div id="old_help" class="form-text">Starts with 010 (aime) or 3 (banapass)</div>
</div>
<label class="form-label" for="card_add_frm_access_code">Access Code:</label> <label class="form-label" for="card_add_frm_access_code">Access Code:</label>
<input class="form-control" name="add_access_code" id="card_add_frm_access_code" maxlength="20" type="text" required aria-describedby="ac_help"> <input class="form-control" name="add_access_code" id="card_add_frm_access_code" maxlength="20" type="text" required aria-describedby="ac_help">
<div id="ac_help" class="form-text">20 digit code on the back of the card.</div> <div id="ac_help" class="form-text">20 digit code on the back of the card.</div>
<label class="form-label" for="card_add_frm_access_code">IDm:</label>
<input class="form-control" name="add_idm" id="card_add_frm_idm" maxlength="16" type="text" aria-describedby="idm_help">
<div id="idm_help" class="form-text">AmusementIC cards only! 16 hexidecimal digits, sometimes called the serial number, gotten by scanning the card with a reader.</div>
<br>
<button type="submit" class="btn btn-primary">Add</button> <button type="submit" class="btn btn-primary">Add</button>
</form> </form>
<br> <br>
</div> </div>
{% if success is defined and success == 4 %}
<div style="background-color: #00AA00; padding: 20px; margin-bottom: 10px; width: 15%;">
Update successful
</div>
{% endif %}
<ul style="font-size: 20px;"> <ul style="font-size: 20px;">
{% for c in cards %} {% for c in cards %}
<li>{{ c.access_code }} ({{ c.type if c.memo is none or not c.memo else c.memo }}): {{ c.status }}&nbsp;<button onclick="prep_edit_form('{{ c.access_code }}', '{{ c.chip_id}}', '{{ c.idm }}', '{{ c.type }}', '{{ c.memo }}', '{{ c.id }}')" data-bs-toggle="modal" data-bs-target="#card_edit" class="btn btn-secondary" id="btn_edit_card_{{ c.access_code }}">View</button>&nbsp;{% if c.status == 'Active'%}<button class="btn-warning btn">Lock</button>{% elif c.status == 'Locked' %}<button class="btn-warning btn">Unlock</button>{% endif %}&nbsp;<button class="btn-danger btn" {{ "disabled" if cards|length == 1 else ""}}>Delete</button></li> <li>{{ c.access_code }} ({{ c.type}}): {{ c.status }}&nbsp;<button onclick="prep_edit_form('{{ c.access_code }}', '{{ c.chip_id}}', '{{ c.idm }}', '{{ c.type }}', '{{ c.memo }}')" data-bs-toggle="modal" data-bs-target="#card_edit" class="btn btn-secondary" id="btn_edit_card_{{ c.access_code }}">Edit</button>&nbsp;{% if c.status == 'Active'%}<button class="btn-warning btn">Lock</button>{% elif c.status == 'Locked' %}<button class="btn-warning btn">Unlock</button>{% endif %}&nbsp;<button class="btn-danger btn">Delete</button></li>
{% endfor %} {% endfor %}
</ul> </ul>
@@ -180,31 +142,31 @@ Update successful
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h1 class="modal-title fs-5" id="card_edit_label">Card Information</h1> <h1 class="modal-title fs-5" id="card_edit_label">Edit Card</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form action="/user/edit.card" method="post" id="frm_edit_card"> <form action="/user/edit.card" method="post" id="frm_edit_card">
<input type="hidden" readonly name="card_edit_frm_card_id" id="card_edit_frm_card_id">
<label class="form-label" for="card_edit_frm_access_code">Access Code:</label> <label class="form-label" for="card_edit_frm_access_code">Access Code:</label>
<input class="form-control-plaintext" readonly name="card_edit_frm_access_code" id="card_edit_frm_access_code" maxlength="20" type="text" required aria-describedby="ac_help"> <input class="form-control" readonly name="add_access_code" id="card_edit_frm_access_code" maxlength="20" type="text" required aria-describedby="ac_help">
<div id="ac_help" class="form-text">20 digit code on the back of the card. If this is incorrect, contact a sysadmin.</div> <div id="ac_help" class="form-text">20 digit code on the back of the card. If this is incorrect, contact a sysadmin.</div>
<label class="form-label" for="card_edit_frm_idm" id="card_edit_frm_idm_lbl">FeliCa IDm:</label>
<input class="form-control-plaintext" aria-describedby="idm_help" name="add_felica_idm" id="card_edit_frm_idm" maxlength="16" type="text" readonly>
<div id="idm_help" class="form-text">8 bytes that uniquly idenfites a FeliCa card. Obtained by reading the card with an NFC reader.</div>
<label class="form-label" for="card_edit_frm_chip_id" id="card_edit_frm_chip_id_lbl">Mifare UID:</label>
<input class="form-control-plaintext" aria-describedby="chip_id_help" name="add_mifare_chip_id" id="card_edit_frm_chip_id" maxlength="8" type="text" readonly>
<div id="chip_id_help" class="form-text">4 byte integer that uniquly identifies a Mifare card. Obtained by reading the card with an NFC reader.</div>
<label class="form-label" for="card_edit_frm_memo" id="card_edit_frm_memo_lbl">Memo:</label> <label class="form-label" for="card_edit_frm_memo" id="card_edit_frm_memo_lbl">Memo:</label>
<input class="form-control" aria-describedby="memo_help" name="add_memo" id="card_edit_frm_memo" maxlength="16" type="text"> <input class="form-control" aria-describedby="memo_help" name="add_memo" id="card_edit_frm_memo" maxlength="16" type="text">
<div id="memo_help" class="form-text">Must be 16 characters or less.</div> <div id="memo_help" class="form-text">Must be 16 characters or less.</div>
<label class="form-label" for="card_edit_frm_idm" id="card_edit_frm_idm_lbl">FeliCa IDm:</label>
<input class="form-control" aria-describedby="idm_help" name="add_felica_idm" id="card_edit_frm_idm" maxlength="16" type="text">
<div id="idm_help" class="form-text">8 bytes that uniquly idenfites a FeliCa card. Obtained by reading the card with an NFC reader.</div>
<label class="form-label" for="card_edit_frm_chip_id" id="card_edit_frm_chip_id_lbl">Mifare UID:</label>
<input class="form-control" aria-describedby="chip_id_help" name="add_mifare_chip_id" id="card_edit_frm_chip_id" maxlength="8" type="text">
<div id="chip_id_help" class="form-text">4 byte integer that uniquly identifies a Mifare card. Obtained by reading the card with an NFC reader.</div>
</form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="submit" class="btn btn-primary">Update Memo</button> <button type="submit" class="btn btn-primary" form="frm_edit_card">Edit</button>
</form> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -19,14 +19,9 @@ New password not acceptable
New Nickname too long New Nickname too long
{% elif error == 9 %} {% elif error == 9 %}
You must be logged in to preform this action You must be logged in to preform this action
New Nickname too long
{% elif error == 10 %} {% elif error == 10 %}
Invalid serial number Invalid serial number
{% elif error == 11 %}
Access Denied
{% elif error == 12 %}
Card already registered
{% elif error == 13 %}
AmusementIC Access Codes beginning with 5 must have IDm
{% else %} {% else %}
An unknown error occoured An unknown error occoured
{% endif %} {% endif %}

View File

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

View File

@@ -34,8 +34,7 @@ class Utils:
@classmethod @classmethod
def get_ip_addr(cls, req: Request) -> str: def get_ip_addr(cls, req: Request) -> str:
ip = req.headers.get("x-forwarded-for", req.client.host) return req.headers.get("x-forwarded-for", req.client.host)
return ip.split(", ")[0]
@classmethod @classmethod
def get_title_port(cls, cfg: CoreConfig): def get_title_port(cls, cfg: CoreConfig):
@@ -49,7 +48,7 @@ class Utils:
def get_title_port_ssl(cls, cfg: CoreConfig): def get_title_port_ssl(cls, cfg: CoreConfig):
if cls.real_title_port_ssl is not None: return cls.real_title_port_ssl 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 443 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)
return cls.real_title_port_ssl return cls.real_title_port_ssl

View File

@@ -22,7 +22,7 @@ if __name__ == "__main__":
parser.add_argument("--email", "-e", type=str, help="Email for the new user") parser.add_argument("--email", "-e", type=str, help="Email for the new user")
parser.add_argument("--access_code", "-a", type=str, help="Access code for new/transfer user", default="00000000000000000000") parser.add_argument("--access_code", "-a", type=str, help="Access code for new/transfer user", default="00000000000000000000")
parser.add_argument("--message", "-m", type=str, help="Revision message") parser.add_argument("--message", "-m", type=str, help="Revision message")
parser.add_argument("action", type=str, help="create, upgrade, downgrade, create-owner, migrate, create-revision, create-autorevision") parser.add_argument("action", type=str, help="create, upgrade, create-owner, migrate, create-revision")
args = parser.parse_args() args = parser.parse_args()
cfg = CoreConfig() cfg = CoreConfig()
@@ -42,31 +42,24 @@ if __name__ == "__main__":
data = Data(cfg) data = Data(cfg)
loop = asyncio.get_event_loop()
if args.action == "create": if args.action == "create":
data.create_database() data.create_database()
elif args.action == "upgrade": elif args.action == "upgrade":
data.schema_upgrade(args.version) data.schema_upgrade(args.version)
elif args.action == "downgrade":
if not args.version:
logging.getLogger("database").error(f"Version argument required for downgrade")
exit(1)
data.schema_downgrade(args.version)
elif args.action == "create-owner": elif args.action == "create-owner":
loop = asyncio.get_event_loop()
loop.run_until_complete(data.create_owner(args.email, args.access_code)) loop.run_until_complete(data.create_owner(args.email, args.access_code))
data.schema_upgrade(args.version)
elif args.action == "migrate": elif args.action == "migrate":
loop = asyncio.get_event_loop()
loop.run_until_complete(data.migrate()) loop.run_until_complete(data.migrate())
elif args.action == "create-revision": elif args.action == "create-revision":
loop = asyncio.get_event_loop()
loop.run_until_complete(data.create_revision(args.message)) loop.run_until_complete(data.create_revision(args.message))
elif args.action == "create-autorevision":
loop.run_until_complete(data.create_revision_auto(args.message))
else: else:
logging.getLogger("database").info(f"Unknown action {args.action}") logging.getLogger("database").info(f"Unknown action {args.action}")

View File

@@ -1,107 +0,0 @@
# Installing ARTEMiS on Linux
This guide assumes a fresh install of Debian 12 or Rasperry Pi OS. If you're using a different distrubution, your package manager commands and package names may be different then what's listed below. Please check with your repository's package manager for package names.
## Install prerequisits
### Python
Some installs may come with python already installed. You can verify this by trying the following commands:
- `python --version`
- `python3 --version`
- `python3.<minor version> --version` where `<minor version>` is a python 3 release (eg 11, 10)
If your python version is at least 3.7, you can move to the next step
### Libraries and other software
ARTEMiS depends on mysql and memcached. As stated above, package names may vary by distrubution, but this is generally what you should expect to install.
#### Rasperry Pi OS
`sudo apt install git mariadb-server python3-pip memcached libmemcached-dev `
#### Debian 12
`sudo apt install git mariadb-server python3-pip memcached libmemcached-dev default-libmysqlclient-dev pkg-config`
### Optional: Install proxy
If you intend to use a proxy (recomended for public-facing production setups), we recomend nginx
`sudo apt install nginx`
## Database setup
### mysql_secure_installation
If you already have your database installed and configured, and are able to log in, skip down to the [Creating the database](#creating-the-database) section below. Otherwise, setup your newly installed database.
`sudo mysql_secure_installation`
Leave the root password blank, do not switch to unix socket, do reset the root password to something secure, and answer yes to the rest of the prompts. You can then log into your database with `sudo mysql`
### Creating the database
Once you're logged in, run the following commands, as root, to set up our database. Make sure you note down whatever you decide to make the password for the aime account, as you will need it to configure artemis.
```sql
CREATE USER 'aime'@'localhost' IDENTIFIED BY '<password>';
CREATE DATABASE aime;
GRANT Alter,Create,Delete,Drop,Index,Insert,References,Select,Update ON aime.* TO 'aime'@'localhost';
quit
```
We have now set up our new user, `aime`, created a database called `aime` and given our user all the permissions it needs on every table of that database.
### Configure memcached
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
Restart memcached using: sudo systemctl restart memcached
## Getting ARTEMiS
### Clone from gitea
use `git clone https://gitea.tendokyu.moe/Hay1tsme/artemis.git` to pull down ARTEMiS into a folder called `artemis` created at wherever your current working directory is. `cd` into `artemis`.
### Optional: Create a venv
Python venvs are a way to install and manage packages on a per-project basis and are recomended on systems that will have multiple python scripts running on them to avoid dependancy issues. If this server will be running ARTEMiS and ONLY ARTEMiS, then it is possible to get away without creating one. If you do want to create one, you will have to install an additional package:
`sudo apt install python3-venv` (like above, package name may vary depending on distro and python version)
Now, simply run `python -m venv .venv` (may have to use python3 or python 3.11 instead of python) to create your virtual environment in the folder `.venv`. In order to install packages and run scripts in this environment, you have to 'activate' it by running `source .venv/bin/activate`. Your terminal should now have (venv) appended to it.
### Optional: Use the develop branch
By default, pulling down ARTEMiS from gitea will pull the `master` branch. This branch is updated less frequently, but is considered stable and ready for production use. If you'd rather have more updates, but a possibility for instability or bugs, you can switch to the develop branch by running `git checkout develop`. You can run `git checkout master` to switch back to stable.
## Install python libraries
Run `pip install -r requirements.txt` to install all of ARTEMiS' dependencies. If any installs fail, you may have missed a step in the [Install prerequisits](#install-prerequisits) section above. If you're absolutly sure you didn't, submit an issue on gitea.
## Configuration
### Copy example configs
From the `artemis` directory, run `cp -r example_config config` to copy the example configuration files to a new folder called `config`. All of the config changes you make will be done in the `config` folder.
### Optional: Generate AimeDB and Frontend JWT Secrets
AimeDB and the frontend utalize JSON Web Tokens (JWT) for card authentication and session cookies respectivly. While generating a secret for AimeDB is optional, if you intend to run the frontend, a secret is required. You can generate a secret easily by running:
`openssl rand --base64 64`
With 64 being the number of bytes. You shouldn't need to go higher then 64, but you can if desired. **NOTE: When pasting secrets into the config file, make sure you remove any newlines!**
### Edit `core.yaml`
Before editing `core.yaml`, you should familiarize yourself with the name and function of each of the config options. You can find a full list in [config.md](config.md)
Open `core.yaml` in the `config` folder in your prefered text editor. The only configuration option that it is absolutly mandatory to change is `aimedb`->`key`. This key must be set for the server to start, and the key must be correct, otherwise you will not be able to process aimedb requests. The correct key is floating around online, and finding it is left as an excersie to the reader.
Another option that should be changed is `database`->`password` to be the password you set when you created your database user. You did write it down somewhere, right?
Since you are presumably not running the games on the same computer you're installing this server on, you're going to want to change `server`->`hostname` to be whatever hostname or IP address other PCs can reach this server by. Note that some games reject IPs and require hostnames, so setting a hostname is always recomended over an IP.
### Edit game configs
Every game has their own yaml file with settings that you may want to tweek. `InitialD Zero` and `Pokken` both have `hostname` fields in their config file that you should edit, and some games support encryption, if supplied with proper keys.
### A note about IDZ
InitialD Zero is currently the only game where it is required to specify encryption information (the AES key and at least one RSA key) for the game to start. These keys are, like the aimedb key, floating around online and will not be provided. If you don't have the keys, and don't plan on anybody connecting to your server playing InitialD Zero, it's best to set `enabled` to `False` in idz.yaml to disable the game.
## Create database tables
ARTEMiS uses alembic to manage datbase versioning. `dbutils.py` acts as a wrapper for alembic, and can execute some necessassary database functions. To create the database tables, run `python dbutils.py create`. Confirm that there are no errors, and you're good to go. If you intend to use the frontend, you may also want to run `python dbutils.py create-owner -a <your 20 digit access code here>` to create a superuser account to log in with.
## Run ARTEMiS
Once you have everything configured properly, simply run `python index.py` to start ARTEMiS. Verify that clients can connect to all services (allnet, billing, aimedb, and game servers) and setup is complete.

129
docs/INSTALL_UBUNTU.md Normal file
View File

@@ -0,0 +1,129 @@
# 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

@@ -1,77 +1,102 @@
# Installing ARTEMiS on Windows # ARTEMiS - Windows 10/11 Guide
This guide assumes a fresh install of Windows 10. Please be aware that due to the lack of memcached and the general woes of running a server on Windows, this is only recommended for local setups or small hosting-for-the-homies type servers. 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
## Install prerequisites # Setup
### Python ## Install Python Python 3.9 (recommended) or 3.10
- Python versions from 3.8 to 3.11 work with ARTEMiS. We recommend 3.11. 1. Download Python 3.9 : [Link](https://www.python.org/ftp/python/3.9.13/python-3.9.13-amd64.exe)
- https://www.python.org/ftp/python/3.11.7/python-3.11.7-amd64.exe 2. Install python-3.9.13-amd64.exe
- Install using whichever options best suit your environment, making sure that the Python executable is on path, such that you can open CMD, type `python --version` and see the version of Python you have installed. 1. Select Customize installation
- If you already have a working version of Python installed, skip this step. 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
### MariaDB ## Install MySQL 8.0
- It is always recommended to use MariaDB over MySQL because Oracle is a terrible company. 1. Download MySQL 8.0 Server : [Link](https://dev.mysql.com/get/Downloads/MySQLInstaller/mysql-installer-community-8.0.34.0.msi)
- While the latest release of v10 is recommended, as it is an LTS release, v11 should work fine. 2. Install mysql-installer-web-community-8.0.34.0.msi
- https://ftp.osuosl.org/pub/mariadb//mariadb-10.11.6/winx64-packages/mariadb-10.11.6-winx64.msi 1. Click on "Add ..." on the side
- REMEMBER YOUR ROOT PASSWORD SO YOU CAN LOG IN IN FUTURE STEPS. 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
### Git ```sql
- While technically optional, it is strongly recommended to obtain ARTEMiS via git clone instead of just downloading it. CREATE USER 'aime'@'localhost' IDENTIFIED BY '<Enter Password Here>';
- https://git-scm.com/download/win
- It is recommended to use Notepad++ as the default editor (if you have it installed), other than that, the default settings should be fine.
### Optional: GUI database viewer
- Having a GUI database editor is recommended but not required.
- MariaDB will try to install HeidiSQL, but we recommend DBeaver.
- https://dbeaver.io/download/
## Obtain ARTEMiS
### Via git (recommended)
- `git clone https://gitea.tendokyu.moe/Hay1tsme/artemis.git` via cmd in whatever folder you want to install ARTEMiS.
- You can switch to the develop branch for latest changes via `git checkout develop`.
### Via http download
- Download [here](https://gitea.tendokyu.moe/Hay1tsme/artemis/archive/master.zip).
- Develop branch can be found [here](https://gitea.tendokyu.moe/Hay1tsme/artemis/archive/develop.zip).
- Extract the zip file somewhere.
## Database setup
- Log into your server as root, either via GUI (recommended) or CMD
- Create the `aime` user, replace `<password>` with a password you choose. Remember it!
```
CREATE USER 'aime'@'localhost' IDENTIFIED BY '<password>';
CREATE DATABASE aime; CREATE DATABASE aime;
GRANT Alter,Create,Delete,Drop,Index,Insert,References,Select,Update ON aime.* TO 'aime'@'localhost'; GRANT Alter,Create,Delete,Drop,Index,Insert,References,Select,Update ON aime.* TO 'aime'@'localhost';
FLUSH PRIVILEGES;
exit;
``` ```
- If you create the database via a GUI, make sure you grant all the above permissions.
## Create a venv ## Install Python modules
- Python virtual environments are a good way to manage packages and make dealing with python and pip easier. 1. Change your work path to the artemis-master folder using 'cd' and install the requirements:
- `python -m pip venv venv`
- `venv\Scripts\activate.bat` to activate the venv whenever you need to interact with ARTEMiS.
- All the rest of the steps assume your venv is activated.
## Install pip modules ```shell
- `pip install -r requirements.txt` pip install -r requirements.txt
```
## Setup configuration ## Copy/Rename the folder `example_config` to `config`
- Create a new `config` folder and copy the files in `example_config` over.
- edit `core.yaml`
- 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 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.
- Edit other game yamls
- Add keys, set hostnames, ports, etc. Specific settings will depend on the game. See [game_specific_info](docs/game_specific_info.md).
## Create Database Tables ## Adjust `config/core.yaml`
- `python dbutils.py create`
## Firewall 1. Make sure to change the server `hostname` to be set to your local machine IP (ex.: 192.168.xxx.xxx)
- If you're planning on serving games not on your PC, open at least ports 80, 8443, and 22345 in windows firewall - In case you want to run this only locally, set the following values:
- Also set `listen_address` to either your local IP to serve on your LAN, or `0.0.0.0` for all interfaces, to accept connections from other places.
## Start ARTEMiS ```yaml
- `python index.py` 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

@@ -8,7 +8,7 @@
- `allow_user_registration`: Allows users to register in-game via the AimeDB `register` function. Disable to be able to control who can use cards on your server. Default `True` - `allow_user_registration`: Allows users to register in-game via the AimeDB `register` function. Disable to be able to control who can use cards on your server. Default `True`
- `allow_unregistered_serials`: Allows games that do not have registered keychips to connect and authenticate. Disable to restrict who can connect to your server. Recomended to disable for production setups. Default `True` - `allow_unregistered_serials`: Allows games that do not have registered keychips to connect and authenticate. Disable to restrict who can connect to your server. Recomended to disable for production setups. Default `True`
- `name`: Name for the server, used by some games in their default MOTDs. Default `ARTEMiS` - `name`: Name for the server, used by some games in their default MOTDs. Default `ARTEMiS`
- `is_develop`: Flags that the server is a development instance, and enables some useful development features. Disable for production setups. Default `True`. - `is_develop`: Flags that the server is a development instance without a proxy standing in front of it. Setting to `False` tells the server not to listen for SSL, because the proxy should be handling all SSL-related things, among other things. Default `True`
- `is_using_proxy`: Flags that you'll be using some other software, such as nginx, to proxy requests, and to send `proxy_port` or `proxy_port_ssl` to games instead of `port`. Default `False` - `is_using_proxy`: Flags that you'll be using some other software, such as nginx, to proxy requests, and to send `proxy_port` or `proxy_port_ssl` to games instead of `port`. Default `False`
- `proxy_port`: Which port your front-facing proxy will be listening on. Ignored if `is_using_proxy` is `False` or if set to `0`. Default `0` - `proxy_port`: Which port your front-facing proxy will be listening on. Ignored if `is_using_proxy` is `False` or if set to `0`. Default `0`
- `proxy_port`: Which port your front-facing proxy will be listening for ssl connections on. Ignored if `is_using_proxy` is `False` or if set to `0`. Default `0` - `proxy_port`: Which port your front-facing proxy will be listening for ssl connections on. Ignored if `is_using_proxy` is `False` or if set to `0`. Default `0`
@@ -17,8 +17,8 @@
- `strict_ip_checking`: Rejects clients if there is no IP in the `arcade` table for the respective arcade. Default `False` - `strict_ip_checking`: Rejects clients if there is no IP in the `arcade` table for the respective arcade. Default `False`
## Title ## Title
- `loglevel`: Logging level for the title server. Default `info` - `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`. A few games or early version will report errors if it is empty, ex maimai DX 1.00 - `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 `07:00`. this must be set to 7:00 am for some game, please do not change it. - `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: `""`
## Database ## Database
- `host`: Host of the database. Default `localhost` - `host`: Host of the database. Default `localhost`
- `username`: Username of the account the server should connect to the database with. Default `aime` - `username`: Username of the account the server should connect to the database with. Default `aime`
@@ -26,30 +26,29 @@
- `name`: Name of the database the server should expect. Default `aime` - `name`: Name of the database the server should expect. Default `aime`
- `port`: Port the database server is listening on. Default `3306` - `port`: Port the database server is listening on. Default `3306`
- `protocol`: Protocol used in the connection string, e.i `mysql` would result in `mysql://...`. Default `mysql` - `protocol`: Protocol used in the connection string, e.i `mysql` would result in `mysql://...`. Default `mysql`
- `sha2_password`: Whether or not the password in the connection string should be hashed via SHA2. Default `False` - `sha2_password`: Weather or not the password in the connection string should be hashed via SHA2. Default `False`
- `loglevel`: Logging level for the database. Default `info` - `loglevel`: Logging level for the database. Default `info`
- `memcached_host`: Host of the memcached server. Default `localhost` - `memcached_host`: Host of the memcached server. Default `localhost`
## Frontend ## Frontend
- `enable`: Whether or not the frontend servlet should run. Frontend can still be run via `python -m uvicorn core.frontend:app` even if this is set to `False`. Default `False` - `enable`: Weather or not the frontend should run. Default `False`
- `port`: Port the frontend should listen on. Default `8080` - `port`: Port the frontend should listen on. Default `8080`
- `loglevel`: Logging level for the frontend server. Default `info` - `loglevel`: Logging level for the frontend server. Default `info`
- `secret`: Base64-encoded JWT secret for session cookies, generated by you. Default `""` - `secret`: Base64-encoded JWT secret for session cookies, generated by you. Default `""`
## Allnet ## Allnet
- `standalone`: Whether allnet should launch it's own servlet on it's own port, or be part of the main servlet on the default port. Disable if you either have something proxying `naominet.jp` requests to port 80, or have port 80 set in `server` -> `port` - `standalone`: Weather allnet should be launched as a standalone service on it's own port.
- `port`: Port the allnet server should listen for connections on if it's running standalone. Games are hardcoded to ask for port `80` so only change if you have a proxy redirecting properly. Ignored if `standalone` is `False`. Default `80` - `port`: Port the billing server should listen for connections on. Games are hardcoded to ask for port `80` so only change if you have a proxy redirecting properly. Default `80`
- `loglevel`: Logging level for the allnet server. Default `info` - `loglevel`: Logging level for the allnet server. Default `info`
- `allow_online_updates`: Allow allnet to distribute online updates via DownloadOrders. This system is currently non-functional, so leave it disabled. Default `False` - `allow_online_updates`: Allow allnet to distribute online updates via DownloadOrders. This system is currently non-functional, so leave it disabled. Default `False`
- `update_cfg_folder`: Folder where delivery INI files will be checked for. Ignored if `allow_online_updates` is `False`. Default `""` - `update_cfg_folder`: Folder where delivery INI files will be checked for. Ignored if `allow_online_updates` is `False`. Default `""`
## Billing ## Billing
- `standalone`: Whether the billing server should launch it's own servlet on it's own port, or be part of the main servlet on the default port. Setting this to `True` requires that you have `ssl_key` and `ssl_cert` set. Default `False` - `standalone`: Weather or not the billing server should be launched as a standalone service on it's own port. Setting this to `True` requires that you have `ssl_key` and `ssl_cert` set. Default `False`
- `loglevel`: Logging level for the billing server. Default `info` - `loglevel`: Logging level for the billing server. Default `info`
- `port`: Port the billing server should listen for connections on. Games are hardcoded to ask for port `8443` so only change if you have a proxy redirecting properly. Ignored if `standalone` is `False`. Default `8443` - `port`: Port the billing server should listen for connections on. Games are hardcoded to ask for port `8443` so only change if you have a proxy redirecting properly. Ignored if `standalone` is `False`. Default `8443`
- `ssl_key`: Location of the ssl server key for the billing server. Ignored if `standalone` is `False`. Default `cert/server.key` - `ssl_key`: Location of the ssl server key for the billing server. Ignored if `standalone` is `False`. Default `cert/server.key`
- `ssl_cert`: Location of the ssl server certificate for the billing server. Ignored if `standalone` is `False`. Must match the CA distributed to users or the billing server will not connect. Default `cert/server.pem` - `ssl_cert`: Location of the ssl server certificate for the billing server. Ignored if `standalone` is `False`. Must match the CA distributed to users or the billing server will not connect. Default `cert/server.pem`
- `signing_key`: Location of the RSA Private key used to sign billing requests. Must match the public key distributed to users or the billing server will not connect. Default `cert/billing.key` - `signing_key`: Location of the RSA Private key used to sign billing requests. Must match the public key distributed to users or the billing server will not connect. Default `cert/billing.key`
## Aimedb ## Aimedb
- `enable`: Whether or not aimedb should run. Default `True` - `enable`: Weather or not aimedb should run. Default `True`
- `listen_address`: IP Address or hostname that the aimedb server will listen for connections on. Leave this blank to use the listen address under `server`. Default `""`
- `loglevel`: Logging level for the aimedb server. Default `info` - `loglevel`: Logging level for the aimedb server. Default `info`
- `port`: Port the aimedb server should listen for connections on. Games are hardcoded to ask for port `22345` so only change if you have a proxy redirecting properly. Default `22345` - `port`: Port the aimedb server should listen for connections on. Games are hardcoded to ask for port `22345` so only change if you have a proxy redirecting properly. Default `22345`
- `key`: Key to encrypt/decrypt aimedb requests and responses. MUST be set or the server will not start. If set incorrectly, your server will not properly handle aimedb requests. Default `""` - `key`: Key to encrypt/decrypt aimedb requests and responses. MUST be set or the server will not start. If set incorrectly, your server will not properly handle aimedb requests. Default `""`

View File

@@ -9,15 +9,7 @@ using the megaime database. Clean installations always create the latest databas
To upgrade the core database and the database for every game, execute: To upgrade the core database and the database for every game, execute:
```shell ```shell
python dbutils.py upgrade python dbutils.py autoupgrade
```
If you are using the old master branch that was not setup with alembic, make sure to do the following steps in order:
- Pull down latest master/develop
- Update core.yaml
- Back up your existing database
```shell
python dbutils.py migrate
``` ```
# Table of content # Table of content
@@ -30,7 +22,7 @@ python dbutils.py migrate
- [Card Maker](#card-maker) - [Card Maker](#card-maker)
- [WACCA](#wacca) - [WACCA](#wacca)
- [Sword Art Online Arcade](#sao) - [Sword Art Online Arcade](#sao)
- [Initial D THE ARCADE](#initial-d-the-arcade) - [Initial D THE ARCADE](#initial-d-the-arcade)
# Supported Games # Supported Games
@@ -63,7 +55,6 @@ Games listed below have been tested and confirmed working.
| 12 | CHUNITHM NEW PLUS!! | | 12 | CHUNITHM NEW PLUS!! |
| 13 | CHUNITHM SUN | | 13 | CHUNITHM SUN |
| 14 | CHUNITHM SUN PLUS | | 14 | CHUNITHM SUN PLUS |
| 15 | CHUNITHM LUMINOUS |
### Importer ### Importer
@@ -88,19 +79,13 @@ Config file is located in `config/chuni.yaml`.
| `crypto` | This option is used to enable the TLS Encryption | | `crypto` | This option is used to enable the TLS Encryption |
If you would like to use network encryption, add the keys to the `keys` section under `crypto`, where the key **If you would like to use network encryption, the following will be required underneath but key, iv and hash are required:**
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 ```yaml
crypto: crypto:
encrypted_only: False encrypted_only: False
keys: keys:
13: ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"] 13: ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"]
"13_int": ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000", 42]
``` ```
### Database upgrade ### Database upgrade
@@ -108,7 +93,7 @@ crypto:
Always make sure your database (tables) are up-to-date: Always make sure your database (tables) are up-to-date:
```shell ```shell
python dbutils.py upgrade python dbutils.py --game SDBT upgrade
``` ```
### Online Battle ### Online Battle
@@ -182,14 +167,6 @@ Config file is located in `config/cxb.yaml`.
## maimai DX ## 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 ### Versions
| Game Code | Version ID | Version Name | | Game Code | Version ID | Version Name |
@@ -215,7 +192,6 @@ Presents are items given to the user when they login, with a little animation (f
| SDEZ | 18 | maimai DX UNiVERSE PLUS | | SDEZ | 18 | maimai DX UNiVERSE PLUS |
| SDEZ | 19 | maimai DX FESTiVAL | | SDEZ | 19 | maimai DX FESTiVAL |
| SDEZ | 20 | maimai DX FESTiVAL PLUS | | SDEZ | 20 | maimai DX FESTiVAL PLUS |
| SDEZ | 21 | maimai DX BUDDiES |
### Importer ### Importer
@@ -239,7 +215,7 @@ The importer for maimai Pre-DX will import Events and Music. Not all games will
Always make sure your database (tables) are up-to-date: Always make sure your database (tables) are up-to-date:
```shell ```shell
python dbutils.py upgrade python dbutils.py --game SDEZ upgrade
``` ```
Pre-Dx uses the same database as DX, so only upgrade using the SDEZ game code! Pre-Dx uses the same database as DX, so only upgrade using the SDEZ game code!
@@ -269,14 +245,10 @@ the Shop, Modules and Customizations.
Config file is located in `config/diva.yaml`. Config file is located in `config/diva.yaml`.
| Option | Info | | Option | Info |
| -------------------- | ------------------------------------------------------------------------------------------------ | | -------------------- | ----------------------------------------------------------------------------------------------- |
| `festa_enable` | Enable or disable the ingame festa | | `unlock_all_modules` | Unlocks all modules (costumes) by default, if set to `False` all modules need to be purchased |
| `festa_add_VP` | Set the extra VP you get when clearing a song, if festa is not enabled no extra VP will be given | | `unlock_all_items` | Unlocks all items (customizations) by default, if set to `False` all items need to be purchased |
| `festa_multiply_VP` | Multiplier for festa add VP |
| `festa_end_time` | Set the date time for when festa will end and not show up in game anymore |
| `unlock_all_modules` | Unlocks all modules (costumes) by default, if set to `False` all modules need to be purchased |
| `unlock_all_items` | Unlocks all items (customizations) by default, if set to `False` all items need to be purchased |
### Custom PV Lists (databanks) ### Custom PV Lists (databanks)
@@ -287,7 +259,7 @@ In order to use custom PV Lists, simply drop in your .dat files inside of /title
Always make sure your database (tables) are up-to-date: Always make sure your database (tables) are up-to-date:
```shell ```shell
python dbutils.py upgrade python dbutils.py --game SBZV upgrade
``` ```
## O.N.G.E.K.I. ## O.N.G.E.K.I.
@@ -343,7 +315,7 @@ crypto:
Always make sure your database (tables) are up-to-date: Always make sure your database (tables) are up-to-date:
```shell ```shell
python dbutils.py upgrade python dbutils.py --game SDDT upgrade
``` ```
### Controlling Events (Ranking Event, Technical Challenge Event, Mission Event) ### Controlling Events (Ranking Event, Technical Challenge Event, Mission Event)
@@ -434,7 +406,6 @@ After that, on next login the present should be received (or whenever it suppose
* UNiVERSE PLUS: Yes * UNiVERSE PLUS: Yes
* FESTiVAL: Yes (added in A031) * FESTiVAL: Yes (added in A031)
* FESTiVAL PLUS: Yes (added in A035) * FESTiVAL PLUS: Yes (added in A035)
* BUDDiES: Yes (added in A039)
* O.N.G.E.K.I. bright MEMORY: Yes * O.N.G.E.K.I. bright MEMORY: Yes
@@ -571,7 +542,7 @@ Config file is located in `config/wacca.yaml`.
Always make sure your database (tables) are up-to-date: Always make sure your database (tables) are up-to-date:
```shell ```shell
python dbutils.py upgrade python dbutils.py --game SDFE upgrade
``` ```
### VIP Rewards ### VIP Rewards
@@ -623,7 +594,7 @@ Below is a list of VIP rewards. Currently, VIP is not implemented, and thus thes
In order to use the importer locate your game installation folder and execute: In order to use the importer locate your game installation folder and execute:
```shell ```shell
python read.py --game SDEW --version 0 --binfolder /titles/sao/data/ python read.py --game SDEW --version <version ID> --binfolder /path/to/game/extractedassets
``` ```
The importer for SAO will import all items, heroes, support skills and titles data. The importer for SAO will import all items, heroes, support skills and titles data.
@@ -644,23 +615,21 @@ Config file is located in `config/sao.yaml`.
Always make sure your database (tables) are up-to-date: Always make sure your database (tables) are up-to-date:
```shell ```shell
python dbutils.py upgrade python dbutils.py --game SDEW upgrade
``` ```
### Notes ### Notes
- Defrag Match will crash at loading - Defrag Match will crash at loading
- Co-Op Online is not supported - Co-Op Online is not supported
- Shop is displayed but cannot purchase heroes or items - Shop is not functionnal
- Player title is currently static and cannot be changed in-game - Player title is currently static and cannot be changed in-game
- QR Card Scanning currently only load a static hero - QR Card Scanning currently only load a static hero
- Ex-quests progression not supported yet
- Daily Missions not implemented **Network hashing in GssSite.dll must be disabled**
- EX TOWER 1,2 & 3 are not yet supported
- Daily Yui coin not yet fixed
### Credits for SAO support: ### Credits for SAO support:
- Midorica - Network Support - Midorica - Limited Network Support
- Dniel97 - Helping with network base - Dniel97 - Helping with network base
- tungnotpunk - Source - tungnotpunk - Source
@@ -709,7 +678,7 @@ Config file is located in `config/idac.yaml`.
Always make sure your database (tables) are up-to-date: Always make sure your database (tables) are up-to-date:
```shell ```shell
python dbutils.py upgrade python dbutils.py --game SDGT upgrade
``` ```
### Notes ### Notes

View File

@@ -1,34 +0,0 @@
# 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

@@ -13,13 +13,9 @@ Step 1 is to edit your artemis configuration. Some recomended changes:
- `allow_unregistered_serials` -> `False` - `allow_unregistered_serials` -> `False`
### `billing` ### `billing`
- `standalone` -> `False` - `standalone` -> `False`
### `allnet`
- `standalone` -> `False`
### `frontend` ### `frontend`
- `enable` -> `True` if you want the frontend - `enable` -> `True` if you want the frontend
- `port` -> `8090` if you're using the default nginx config, otherwise whatever port your proxy will be sending requests to - `port` -> `8080` if you're using the default nginx config
### `aimedb`
- `listen_address` -> `0.0.0.0` unless you're proxying aimedb requests (not recomended at this time), in which case, leave this option unchanged
If you plan to serve artemis behind a VPN, these additional settings are also recomended If you plan to serve artemis behind a VPN, these additional settings are also recomended
- `check_arcade_ip` -> `True` - `check_arcade_ip` -> `True`

View File

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

View File

@@ -18,7 +18,7 @@ server:
title: title:
loglevel: "info" loglevel: "info"
reboot_start_time: "04:00" reboot_start_time: "04:00"
reboot_end_time: "07:00" # this must be set to 7:00 am for some game, please do not change it reboot_end_time: "05:00"
database: database:
host: "localhost" host: "localhost"
@@ -55,7 +55,6 @@ billing:
aimedb: aimedb:
enable: True enable: True
listen_address: ""
loglevel: "info" loglevel: "info"
port: 22345 port: 22345
key: "" key: ""

View File

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

View File

@@ -1,10 +1,7 @@
server: server:
enable: True enable: True
loglevel: "info" loglevel: "info"
festa_enable: True banner_msg: ""
festa_add_VP: "20,5"
festa_multiply_VP: "1,2"
festa_end_time: "2029-01-01 00:00:00.0"
mods: mods:
unlock_all_modules: True unlock_all_modules: True

View File

@@ -12,6 +12,3 @@ uploads:
photos_dir: "" photos_dir: ""
movies: False movies: False
movies_dir: "" movies_dir: ""
crypto:
encrypted_only: False

View File

@@ -6,7 +6,7 @@ server {
location / { location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass_request_headers on; proxy_pass_request_headers on;
proxy_pass http://127.0.0.1:8080/; proxy_pass http://localhost:8080/;
} }
} }
@@ -18,7 +18,7 @@ server {
location / { location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass_request_headers on; proxy_pass_request_headers on;
proxy_pass http://127.0.0.1:8080/; proxy_pass http://localhost:8080/;
} }
} }
@@ -38,9 +38,7 @@ server {
ssl_prefer_server_ciphers off; ssl_prefer_server_ciphers off;
location / { location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://localhost:8080/;
proxy_pass_request_headers on;
proxy_pass http://127.0.0.1:8080/;
} }
} }
@@ -60,9 +58,7 @@ server {
ssl_prefer_server_ciphers off; ssl_prefer_server_ciphers off;
location / { location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://localhost:8080/;
proxy_pass_request_headers on;
proxy_pass http://127.0.0.1:8080/;
} }
} }
@@ -74,7 +70,7 @@ server {
location / { location / {
return 301 https://$host$request_uri; return 301 https://$host$request_uri;
# If you don't want https redirection, or are using something like cloudflare to manage HTTPS, comment out the line above and uncomment the line below # If you don't want https redirection, or are using something like cloudflare to manage HTTPS, comment out the line above and uncomment the line below
# proxy_pass http://127.0.0.1:8090/; # proxy_pass http://localhost:8080/;
} }
} }
@@ -100,6 +96,6 @@ server {
location / { location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass_request_headers on; proxy_pass_request_headers on;
proxy_pass http://127.0.0.1:8090/; proxy_pass http://localhost:8080/;
} }
} }

View File

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

View File

@@ -40,8 +40,7 @@ async def launch_billing(cfg: CoreConfig) -> None:
log_level="info" if cfg.server.is_develop else "critical", log_level="info" if cfg.server.is_develop else "critical",
ssl_version=3, ssl_version=3,
ssl_certfile=cfg.billing.ssl_cert, ssl_certfile=cfg.billing.ssl_cert,
ssl_keyfile=cfg.billing.ssl_key, ssl_keyfile=cfg.billing.ssl_key
ssl_ciphers="DEFAULT:!aNULL:!eNULL:!MD5:!3DES:!DES:!RC4:!IDEA:!SEED:!aDSS:!SRP:!PSK",
) )
server = uvicorn.Server(server_cfg) server = uvicorn.Server(server_cfg)
await server.serve() await server.serve()

View File

@@ -4,71 +4,38 @@ A network service emulator for games running SEGA'S ALL.NET service, and similar
# Supported games # Supported games
Games listed below have been tested and confirmed working. Only game versions older then the version currently active in arcades, or games versions that have not recieved a major update in over one year, are supported. Games listed below have been tested and confirmed working. Only game versions older then the version currently active in arcades, or games versions that have not recieved a major update in over one year, are supported.
+ CHUNITHM
+ All versions up to SUN PLUS
+ crossbeats REV.
+ All versions + omnimix
+ maimai DX
+ All versions up to FESTiVAL PLUS
+ Hatsune Miku: Project DIVA Arcade
+ All versions
+ Card Maker + Card Maker
+ 1.30 + 1.30
+ 1.35 + 1.35
+ CHUNITHM INTL
+ SUPERSTAR
+ SUPERSTAR PLUS
+ NEW
+ NEW PLUS
+ SUN
+ SUN PLUS
+ CHUNITHM JP
+ AIR
+ AIR PLUS
+ AMAZON
+ AMAZON PLUS
+ CRYSTAL
+ CRYSTAL PLUS
+ PARADISE
+ PARADISE LOST
+ NEW
+ NEW PLUS
+ SUN
+ SUN PLUS
+ LUMINOUS
+ crossbeats REV.
+ Crossbeats REV.
+ Crossbeats REV. SUNRiSE S1
+ Crossbeats REV. SUNRiSE S2 + omnimix
+ Hatsune Miku: Project DIVA Arcade
+ Future Tone Arcade - All versions
+ Initial D THE ARCADE
+ Season 2
+ maimai DX
+ Splash
+ Splash Plus
+ UNiVERSE
+ UNiVERSE PLUS
+ FESTiVAL
+ FESTiVAL PLUS
+ BUDDiES
+ O.N.G.E.K.I. + O.N.G.E.K.I.
+ SUMMER + All versions up to bright MEMORY
+ SUMMER PLUS
+ R.E.D.
+ R.E.D. PLUS
+ bright
+ bright MEMORY
+ POKKÉN TOURNAMENT
+ Final Online
+ Sword Art Online Arcade
+ Final (Single player only)
+ WACCA + WACCA
+ Lily R + Lily R
+ Reverse + Reverse
+ POKKÉN TOURNAMENT
+ Final Online
+ Sword Art Online Arcade (partial support)
+ Final
+ Initial D THE ARCADE
+ Season 2
## Requirements ## Requirements
- python 3 (tested working with 3.9 and 3.10, other versions YMMV) - python 3 (tested working with 3.9 and 3.10, other versions YMMV)
- pip - pip
@@ -76,7 +43,7 @@ Games listed below have been tested and confirmed working. Only game versions ol
- mysql/mariadb server - mysql/mariadb server
## Setup guides ## Setup guides
Follow the platform-specific guides for [windows](docs/INSTALL_WINDOWS.md), [linux (Debian 12 or Rasperry Pi OS recomended, but anything works)](docs/INSTALL_LINUX.md) or [docker](docs/INSTALL_DOCKER.md) to setup and run the server. Follow the platform-specific guides for [windows](docs/INSTALL_WINDOWS.md), [ubuntu](docs/INSTALL_UBUNTU.md) or [docker](docs/INSTALL_DOCKER.md) to setup and run the server.
## Game specific information ## Game specific information
Read [Games specific info](docs/game_specific_info.md) for all supported games, importer settings, configuration option and database upgrades. Read [Games specific info](docs/game_specific_info.md) for all supported games, importer settings, configuration option and database upgrades.

View File

@@ -1,24 +1,22 @@
mypy mypy
wheel wheel
pytz pytz
pyyaml pyyaml
sqlalchemy==1.4.46 sqlalchemy==1.4.46
mysqlclient mysqlclient
pyopenssl pyopenssl
service_identity service_identity
PyCryptodome PyCryptodome
inflection inflection
coloredlogs coloredlogs
pylibmc; platform_system != "Windows" pylibmc; platform_system != "Windows"
wacky wacky
bcrypt bcrypt
jinja2 jinja2
protobuf protobuf
pillow pillow
pyjwt==2.8.0 pyjwt==2.8.0
websockets websockets
starlette starlette
asyncio asyncio
uvicorn uvicorn
alembic
python-multipart

View File

@@ -1,11 +1,9 @@
from .index import ChuniServlet from titles.chuni.index import ChuniServlet
from .const import ChuniConstants from titles.chuni.const import ChuniConstants
from .database import ChuniData from titles.chuni.database import ChuniData
from .read import ChuniReader from titles.chuni.read import ChuniReader
from .frontend import ChuniFrontend
index = ChuniServlet index = ChuniServlet
database = ChuniData database = ChuniData
reader = ChuniReader reader = ChuniReader
frontend = ChuniFrontend
game_codes = [ChuniConstants.GAME_CODE, ChuniConstants.GAME_CODE_NEW, ChuniConstants.GAME_CODE_INT] game_codes = [ChuniConstants.GAME_CODE, ChuniConstants.GAME_CODE_NEW, ChuniConstants.GAME_CODE_INT]

View File

@@ -720,14 +720,9 @@ class ChuniBase:
team_id = 65535 team_id = 65535
team_name = self.game_cfg.team.team_name team_name = self.game_cfg.team.team_name
team_rank = 0 team_rank = 0
team_user_point = 0
# Get user profile # Get user profile
profile = await self.data.profile.get_profile_data(data["userId"], self.version) profile = await self.data.profile.get_profile_data(data["userId"], self.version)
if profile is None:
return {"userId": data["userId"], "teamId": 0}
if profile and profile["teamId"]: if profile and profile["teamId"]:
# Get team by id # Get team by id
team = await self.data.profile.get_team_by_id(profile["teamId"]) team = await self.data.profile.get_team_by_id(profile["teamId"])
@@ -736,12 +731,7 @@ class ChuniBase:
team_id = team["id"] team_id = team["id"]
team_name = team["teamName"] team_name = team["teamName"]
team_rank = await self.data.profile.get_team_rank(team["id"]) team_rank = await self.data.profile.get_team_rank(team["id"])
team_point = team["teamPoint"]
if team["userTeamPoint"] is not None and team["userTeamPoint"] != "":
user_team_point_data = json.loads(team["userTeamPoint"])
for user_point_data in user_team_point_data:
if user_point_data["user"] == data["userId"]:
team_user_point = int(user_point_data["userPoint"])
# Don't return anything if no team name has been defined for defaults and there is no team set for the player # Don't return anything if no team name has been defined for defaults and there is no team set for the player
if not profile["teamId"] and team_name == "": if not profile["teamId"] and team_name == "":
return {"userId": data["userId"], "teamId": 0} return {"userId": data["userId"], "teamId": 0}
@@ -751,12 +741,11 @@ class ChuniBase:
"teamId": team_id, "teamId": team_id,
"teamRank": team_rank, "teamRank": team_rank,
"teamName": team_name, "teamName": team_name,
"assaultTimeRate": 1, # TODO: Figure out assaultTime, which might be team point boost?
"userTeamPoint": { "userTeamPoint": {
"userId": data["userId"], "userId": data["userId"],
"teamId": team_id, "teamId": team_id,
"orderId": 0, "orderId": 1,
"teamPoint": team_user_point, "teamPoint": 1,
"aggrDate": data["playDate"], "aggrDate": data["playDate"],
}, },
} }
@@ -822,12 +811,6 @@ class ChuniBase:
upsert = data["upsertUserAll"] upsert = data["upsertUserAll"]
user_id = data["userId"] user_id = data["userId"]
if int(user_id) & 0x1000000000001 == 0x1000000000001:
place_id = int(user_id) & 0xFFFC00000000
self.logger.info("Guest play from place ID %d, ignoring.", place_id)
return {"returnCode": "1"}
if "userData" in upsert: if "userData" in upsert:
try: try:
upsert["userData"][0]["userName"] = self.read_wtf8( upsert["userData"][0]["userName"] = self.read_wtf8(
@@ -942,42 +925,6 @@ class ChuniBase:
for rp in upsert["userRecentPlayerList"]: for rp in upsert["userRecentPlayerList"]:
pass pass
for rating_type in {"userRatingBaseList", "userRatingBaseHotList", "userRatingBaseNextList"}:
if rating_type not in upsert:
continue
await self.data.profile.put_profile_rating(
user_id,
self.version,
rating_type,
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"} return {"returnCode": "1"}
async def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: async def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict:
@@ -1005,4 +952,4 @@ class ChuniBase:
return { return {
"userId": data["userId"], "userId": data["userId"],
"userNetBattleData": {"recentNBSelectMusicList": []}, "userNetBattleData": {"recentNBSelectMusicList": []},
} }

View File

@@ -1,6 +1,3 @@
from enum import Enum
class ChuniConstants: class ChuniConstants:
GAME_CODE = "SDBT" GAME_CODE = "SDBT"
GAME_CODE_NEW = "SDHD" GAME_CODE_NEW = "SDHD"
@@ -19,13 +16,10 @@ class ChuniConstants:
VER_CHUNITHM_CRYSTAL = 8 VER_CHUNITHM_CRYSTAL = 8
VER_CHUNITHM_CRYSTAL_PLUS = 9 VER_CHUNITHM_CRYSTAL_PLUS = 9
VER_CHUNITHM_PARADISE = 10 VER_CHUNITHM_PARADISE = 10
VER_CHUNITHM_NEW = 11 VER_CHUNITHM_NEW = 11
VER_CHUNITHM_NEW_PLUS = 12 VER_CHUNITHM_NEW_PLUS = 12
VER_CHUNITHM_SUN = 13 VER_CHUNITHM_SUN = 13
VER_CHUNITHM_SUN_PLUS = 14 VER_CHUNITHM_SUN_PLUS = 14
VER_CHUNITHM_LUMINOUS = 15
VERSION_NAMES = [ VERSION_NAMES = [
"CHUNITHM", "CHUNITHM",
"CHUNITHM PLUS", "CHUNITHM PLUS",
@@ -41,53 +35,9 @@ class ChuniConstants:
"CHUNITHM NEW!!", "CHUNITHM NEW!!",
"CHUNITHM NEW PLUS!!", "CHUNITHM NEW PLUS!!",
"CHUNITHM SUN", "CHUNITHM SUN",
"CHUNITHM SUN PLUS", "CHUNITHM SUN PLUS"
"CHUNITHM LUMINOUS",
]
SCORE_RANK_INTERVALS_OLD = [
(1007500, "SSS"),
(1000000, "SS"),
( 975000, "S"),
( 950000, "AAA"),
( 925000, "AA"),
( 900000, "A"),
( 800000, "BBB"),
( 700000, "BB"),
( 600000, "B"),
( 500000, "C"),
( 0, "D"),
]
SCORE_RANK_INTERVALS_NEW = [
(1009000, "SSS+"), # New only
(1007500, "SSS"),
(1005000, "SS+"), # New only
(1000000, "SS"),
( 990000, "S+"), # New only
( 975000, "S"),
( 950000, "AAA"),
( 925000, "AA"),
( 900000, "A"),
( 800000, "BBB"),
( 700000, "BB"),
( 600000, "B"),
( 500000, "C"),
( 0, "D"),
] ]
@classmethod @classmethod
def game_ver_to_string(cls, ver: int): def game_ver_to_string(cls, ver: int):
return cls.VERSION_NAMES[ver] return cls.VERSION_NAMES[ver]
class MapAreaConditionType(Enum):
UNLOCKED = 0
MAP_CLEARED = 1
MAP_AREA_CLEARED = 2
TROPHY_OBTAINED = 3
class MapAreaConditionLogicalOperator(Enum):
AND = 1
OR = 2

View File

@@ -1,299 +0,0 @@
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 ChuniData
from .config import ChuniConfig
from .const import ChuniConstants
def pairwise(iterable):
# https://docs.python.org/3/library/itertools.html#itertools.pairwise
# but for Python < 3.10. pairwise('ABCDEFG') → AB BC CD DE EF FG
iterator = iter(iterable)
a = next(iterator, None)
for b in iterator:
yield a, b
a = b
def calculate_song_rank(score: int, game_version: int) -> str:
if game_version >= ChuniConstants.VER_CHUNITHM_NEW:
intervals = ChuniConstants.SCORE_RANK_INTERVALS_NEW
else:
intervals = ChuniConstants.SCORE_RANK_INTERVALS_OLD
for (min_score, rank) in intervals:
if score >= min_score:
return rank
return "D"
def calculate_song_rating(score: int, chart_constant: float, game_version: int) -> float:
is_new = game_version >= ChuniConstants.VER_CHUNITHM_NEW
if is_new: # New and later
max_score = 1009000
max_rating_modifier = 2.15
else: # Up to Paradise Lost
max_score = 1007500
max_rating_modifier = 2.0
if (score < 500000):
return 0.0 # D
elif (score >= max_score):
return chart_constant + max_rating_modifier # SSS/SSS+
# Okay, we're doing this the hard way.
# Rating goes up linearly between breakpoints listed below.
# Pick the score interval in which we are in, then calculate
# the position between possible ratings.
score_intervals = [
( 500000, 0.0), # C
( 800000, max(0.0, (chart_constant - 5.0) / 2)), # BBB
( 900000, max(0.0, (chart_constant - 5.0))), # A
( 925000, max(0.0, (chart_constant - 3.0))), # AA
( 975000, chart_constant), # S
(1000000, chart_constant + 1.0), # SS
(1005000, chart_constant + 1.5), # SS+
(1007500, chart_constant + 2.0), # SSS
(1009000, chart_constant + max_rating_modifier), # SSS+!
]
for ((lo_score, lo_rating), (hi_score, hi_rating)) in pairwise(score_intervals):
if not (lo_score <= score < hi_score):
continue
interval_pos = (score - lo_score) / (hi_score - lo_score)
return lo_rating + ((hi_rating - lo_rating) * interval_pos)
class ChuniFrontend(FE_Base):
def __init__(
self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str
) -> None:
super().__init__(cfg, environment)
self.data = ChuniData(cfg)
self.game_cfg = ChuniConfig()
if path.exists(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"):
self.game_cfg.update(
yaml.safe_load(open(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"))
)
self.nav_name = "Chunithm"
def get_routes(self) -> List[Route]:
return [
Route("/", self.render_GET, methods=['GET']),
Route("/rating", self.render_GET_rating, 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/chuni/templates/chuni_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:
# chunithm_version is -1 means it is not initialized yet, select a default version from existing.
if usr_sesh.chunithm_version < 0:
usr_sesh.chunithm_version = versions[0]
profile = await self.data.profile.get_profile_data(usr_sesh.user_id, usr_sesh.chunithm_version)
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=ChuniConstants.VERSION_NAMES,
versions=versions,
cur_version=usr_sesh.chunithm_version
), media_type="text/html; charset=utf-8")
if usr_sesh.chunithm_version >= 0:
encoded_sesh = self.encode_session(usr_sesh)
resp.set_cookie("ARTEMIS_SESH", encoded_sesh)
return resp
else:
return RedirectResponse("/gate/", 303)
async def render_GET_rating(self, request: Request) -> bytes:
template = self.environment.get_template(
"titles/chuni/templates/chuni_rating.jinja"
)
usr_sesh = self.validate_session(request)
if not usr_sesh:
usr_sesh = UserSession()
if usr_sesh.user_id > 0:
if usr_sesh.chunithm_version < 0:
return RedirectResponse("/game/chuni/", 303)
profile = await self.data.profile.get_profile_data(usr_sesh.user_id, usr_sesh.chunithm_version)
rating = await self.data.profile.get_profile_rating(usr_sesh.user_id, usr_sesh.chunithm_version)
hot_list=[]
base_list=[]
if profile and rating:
song_records = []
for song in rating:
music_chart = await self.data.static.get_music_chart(usr_sesh.chunithm_version, song.musicId, song.difficultId)
if not music_chart:
continue
rank = calculate_song_rank(song.score, profile.version)
rating = calculate_song_rating(song.score, music_chart.level, profile.version)
song_rating = int(rating * 10 ** 2) / 10 ** 2
song_records.append({
"difficultId": song.difficultId,
"musicId": song.musicId,
"title": music_chart.title,
"level": music_chart.level,
"score": song.score,
"type": song.type,
"rank": rank,
"song_rating": song_rating,
})
hot_list = [obj for obj in song_records if obj["type"] == "userRatingBaseHotList"]
base_list = [obj for obj in song_records if obj["type"] == "userRatingBaseList"]
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),
profile=profile,
hot_list=hot_list,
base_list=base_list,
), media_type="text/html; charset=utf-8")
else:
return RedirectResponse("/gate/", 303)
async def render_GET_playlog(self, request: Request) -> bytes:
template = self.environment.get_template(
"titles/chuni/templates/chuni_playlog.jinja"
)
usr_sesh = self.validate_session(request)
if not usr_sesh:
usr_sesh = UserSession()
if usr_sesh.user_id > 0:
if usr_sesh.chunithm_version < 0:
return RedirectResponse("/game/chuni/", 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_limited(user_id, index, 20)
playlog_with_title = []
for record in playlog:
music_chart = await self.data.static.get_music_chart(usr_sesh.chunithm_version, record.musicId, record.level)
if music_chart:
difficultyNum=music_chart.level
artist=music_chart.artist
title=music_chart.title
else:
difficultyNum=0
artist="unknown"
title="musicid: " + str(record.musicId)
playlog_with_title.append({
"raw": record,
"title": title,
"difficultyNum": difficultyNum,
"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/chuni/?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()
chunithm_version = form_data.get("version")
self.logger.debug(f"version change to: {chunithm_version}")
if(chunithm_version.isdigit()):
usr_sesh.chunithm_version=int(chunithm_version)
encoded_sesh = self.encode_session(usr_sesh)
self.logger.debug(f"Created session with JWT {encoded_sesh}")
resp = RedirectResponse("/game/chuni/", 303)
resp.set_cookie("ARTEMIS_SESH", encoded_sesh)
return resp
else:
return RedirectResponse("/gate/", 303)

View File

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

View File

@@ -1,298 +0,0 @@
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,8 +32,6 @@ class ChuniNew(ChuniBase):
return "210" return "210"
if self.version == ChuniConstants.VER_CHUNITHM_SUN_PLUS: if self.version == ChuniConstants.VER_CHUNITHM_SUN_PLUS:
return "215" return "215"
if self.version == ChuniConstants.VER_CHUNITHM_LUMINOUS:
return "220"
async def handle_get_game_setting_api_request(self, data: Dict) -> Dict: async def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
# use UTC time and convert it to JST time by adding +9 # use UTC time and convert it to JST time by adding +9
@@ -341,10 +339,10 @@ class ChuniNew(ChuniBase):
} }
async def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict: async def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict:
return await super().handle_get_user_character_api_request(data) return super().handle_get_user_character_api_request(data)
async def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict: async def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict:
return await super().handle_get_user_item_api_request(data) return super().handle_get_user_item_api_request(data)
async def handle_roll_gacha_api_request(self, data: Dict) -> Dict: async def handle_roll_gacha_api_request(self, data: Dict) -> Dict:
""" """

View File

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

View File

@@ -243,36 +243,6 @@ matching = Table(
mysql_charset="utf8mb4", 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): class ChuniItemData(BaseData):
async def get_oldest_free_matching(self, version: int) -> Optional[Row]: async def get_oldest_free_matching(self, version: int) -> Optional[Row]:
@@ -624,66 +594,3 @@ class ChuniItemData(BaseData):
) )
return None return None
return result.lastrowid 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

@@ -1,10 +1,10 @@
import json
from typing import Dict, List, Optional from typing import Dict, List, Optional
from sqlalchemy import Table, Column, UniqueConstraint, and_ from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_
from sqlalchemy.types import Integer, String, Boolean, JSON, BigInteger from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, BigInteger
from sqlalchemy.engine.base import Connection
from sqlalchemy.schema import ForeignKey from sqlalchemy.schema import ForeignKey
from sqlalchemy.engine import Row from sqlalchemy.engine import Row
from sqlalchemy.sql import select, delete from sqlalchemy.sql import func, select
from sqlalchemy.dialects.mysql import insert from sqlalchemy.dialects.mysql import insert
from core.data.schema import BaseData, metadata from core.data.schema import BaseData, metadata
@@ -390,55 +390,11 @@ team = Table(
Column("id", Integer, primary_key=True, nullable=False), Column("id", Integer, primary_key=True, nullable=False),
Column("teamName", String(255)), Column("teamName", String(255)),
Column("teamPoint", Integer), Column("teamPoint", Integer),
Column("userTeamPoint", JSON),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
rating = Table(
"chuni_profile_rating",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column(
"user",
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
nullable=False,
),
Column("version", Integer, nullable=False),
Column("type", String(255), nullable=False),
Column("index", Integer, nullable=False),
Column("musicId", Integer),
Column("difficultId", Integer),
Column("romVersionCode", Integer),
Column("score", Integer),
UniqueConstraint("user", "version", "type", "index", name="chuni_profile_rating_best_uk"),
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): class ChuniProfileData(BaseData):
async def update_name(self, user_id: int, new_name: str) -> bool:
sql = profile.update(profile.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
async def put_profile_data( async def put_profile_data(
self, aime_id: int, version: int, profile_data: Dict self, aime_id: int, version: int, profile_data: Dict
) -> Optional[int]: ) -> Optional[int]:
@@ -707,36 +663,12 @@ class ChuniProfileData(BaseData):
# Return the rank if found, or a default rank otherwise # Return the rank if found, or a default rank otherwise
return rank if rank is not None else 0 return rank if rank is not None else 0
async def update_team(self, team_id: int, team_data: Dict, user_id: str, user_point_delta: int) -> bool: # RIP scaled team ranking. Gone, but forgotten
# Update the team data # def get_team_rank_scaled(self, team_id: int) -> int:
async def update_team(self, team_id: int, team_data: Dict) -> bool:
team_data["id"] = team_id team_data["id"] = team_id
existing_team = self.get_team_by_id(team_id)
if existing_team is None or "userTeamPoint" not in existing_team:
self.logger.warn(
f"update_team: Failed to update team! team id: {team_id}. Existing team data not found."
)
return False
user_team_point_data = []
if existing_team["userTeamPoint"] is not None and existing_team["userTeamPoint"] != "":
user_team_point_data = json.loads(existing_team["userTeamPoint"])
updated = False
# Try to find the user in the existing data and update their points
for user_point_data in user_team_point_data:
if user_point_data["user"] == user_id:
user_point_data["userPoint"] = str(int(user_point_delta))
updated = True
break
# If the user was not found, add them to the data with the new points
if not updated:
user_team_point_data.append({"user": user_id, "userPoint": str(user_point_delta)})
# Update the team's userTeamPoint field in the team data
team_data["userTeamPoint"] = json.dumps(user_team_point_data)
# Update the team in the database
sql = insert(team).values(**team_data) sql = insert(team).values(**team_data)
conflict = sql.on_duplicate_key_update(**team_data) conflict = sql.on_duplicate_key_update(**team_data)
@@ -748,7 +680,6 @@ class ChuniProfileData(BaseData):
) )
return False return False
return True return True
async def get_rival(self, rival_id: int) -> Optional[Row]: async def get_rival(self, rival_id: int) -> Optional[Row]:
sql = select(profile).where(profile.c.user == rival_id) sql = select(profile).where(profile.c.user == rival_id)
result = await self.execute(sql) result = await self.execute(sql)
@@ -772,78 +703,3 @@ class ChuniProfileData(BaseData):
return { return {
"total_play_count": total_play_count "total_play_count": total_play_count
} }
async def put_profile_rating(
self,
aime_id: int,
version: int,
rating_type: str,
rating_data: List[Dict],
):
inserted_values = [
{"user": aime_id, "version": version, "type": rating_type, "index": i, **x}
for (i, x) in enumerate(rating_data)
]
sql = insert(rating).values(inserted_values)
update_dict = {x.name: x for x in sql.inserted if x.name != "id"}
sql = sql.on_duplicate_key_update(**update_dict)
result = await self.execute(sql)
if result is None:
self.logger.warn(
f"put_profile_rating: Could not insert {rating_type}, aime_id: {aime_id}",
)
return
return result.lastrowid
async def get_profile_rating(self, aime_id: int, version: int) -> Optional[List[Row]]:
sql = select(rating).where(and_(
rating.c.user == aime_id,
rating.c.version <= version,
))
result = await self.execute(sql)
if result is None:
self.logger.warning(f"Rating of user {aime_id}, version {version} was None")
return None
return result.fetchall()
async def get_all_profile_versions(self, aime_id: int) -> Optional[List[Row]]:
sql = select([profile.c.version]).where(profile.c.user == aime_id)
result = await self.execute(sql)
if result is None:
self.logger.warning(f"user {aime_id}, has no profile")
return None
else:
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

@@ -190,23 +190,6 @@ class ChuniScoreData(BaseData):
return None return None
return result.fetchall() return result.fetchall()
async def get_playlogs_limited(self, aime_id: int, index: int, count: int) -> Optional[Row]:
sql = select(playlog).where(playlog.c.user == aime_id).order_by(playlog.c.id.desc()).limit(count).offset(index * count)
result = await self.execute(sql)
if result is None:
self.logger.warning(f" aime_id {aime_id} has no playlog ")
return None
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()
async def put_playlog(self, aime_id: int, playlog_data: Dict, version: int) -> Optional[int]: async def put_playlog(self, aime_id: int, playlog_data: Dict, version: int) -> Optional[int]:
# Calculate the ROM version that should be inserted into the DB, based on the version of the ggame being inserted # Calculate the ROM version that should be inserted into the DB, based on the version of the ggame being inserted
# We only need from Version 10 (Plost) and back, as newer versions include romVersion in their upsert # We only need from Version 10 (Plost) and back, as newer versions include romVersion in their upsert
@@ -242,8 +225,6 @@ class ChuniScoreData(BaseData):
# Calculates the ROM version that should be fetched for rankings, based on the game version being retrieved # 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 # This prevents tracks that are not accessible in your version from counting towards the 10 results
romVer = { romVer = {
15: "2.20%",
14: "2.15%",
13: "2.10%", 13: "2.10%",
12: "2.05%", 12: "2.05%",
11: "2.00%", 11: "2.00%",

View File

@@ -16,14 +16,4 @@ class ChuniSun(ChuniNewPlus):
# hardcode lastDataVersion for CardMaker 1.35 A032 # hardcode lastDataVersion for CardMaker 1.35 A032
user_data["lastDataVersion"] = "2.10.00" user_data["lastDataVersion"] = "2.10.00"
return user_data return user_data
#SDGS Exclusive
async def handle_get_user_cto_c_play_api_request(self, data: Dict) -> Dict:
return {
"userId": data["userId"],
"orderBy": "0",
"count": "0",
#game request c2c play history while login but seem unused(?)
"userCtoCPlayList": [],
}

View File

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

View File

@@ -1,150 +0,0 @@
{% extends "core/templates/index.jinja" %}
{% block content %}
<style>
{% include 'titles/chuni/templates/css/chuni_style.css' %}
</style>
<div class="container">
{% include 'titles/chuni/templates/chuni_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>Level:</td>
<td>{{ profile.level }}</td>
</tr>
<tr>
<td>Rating:</td>
<td>
<span class="{% if profile.playerRating >= 1600 %}rainbow{% elif profile.playerRating < 1600 and profile.playerRating >= 1525 %}platinum{% elif profile.playerRating < 1525 and profile.playerRating >=1500 %}platinum{% endif %}">
{{ profile.playerRating|float/100 }}
</span>
<span>
(highest: {{ profile.highestRating|float/100 }})
</span>
</td>
</tr>
<tr>
<td>Over Power:</td>
<td>{{ profile.overPowerPoint|float/100 }}({{ profile.overPowerRate|float/100 }})</td>
</tr>
<tr>
<td>Current Point:</td>
<td>{{ profile.point }}</td>
</tr>
<tr>
<td>Total Point:</td>
<td>{{ profile.totalPoint }}</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 High Score:</td>
<td>{{ profile.totalHiScore }}</td>
</tr>
<tr>
<td>Total Basic High Score:</td>
<td>{{ profile.totalBasicHighScore }}</td>
</tr>
<tr>
<td>Total Advanced High Score:</td>
<td>{{ profile.totalAdvancedHighScore }}</td>
</tr>
<tr>
<td>Total Expert High Score:</td>
<td>{{ profile.totalExpertHighScore }}</td>
</tr>
<tr>
<td>Total Master High Score:</td>
<td>{{ profile.totalMasterHighScore }}</td>
</tr>
<tr>
<td>Total Ultima High Score :</td>
<td>{{ profile.totalUltimaHighScore }}</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/chuni/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/chuni/version.change", { version: sel.value })
.done(function (data) {
location.reload();
})
.fail(function () {
alert("Failed to update version.");
});
}
</script>
{% endblock content %}

View File

@@ -1,184 +0,0 @@
{% extends "core/templates/index.jinja" %}
{% block content %}
<style>
{% include 'titles/chuni/templates/css/chuni_style.css' %}
</style>
<div class="container">
{% include 'titles/chuni/templates/chuni_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 = ['normal', '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.track }}</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>{{ rankName[record.raw.rank] }}</h2>
<h6
class="{% if record.raw.level == 0 %}normal{% elif record.raw.level == 1 %}advanced{% elif record.raw.level == 2 %}expert{% elif record.raw.level == 3 %}master{% elif record.raw.level == 4 %}ultima{% endif %}">
{{ difficultyName[record.raw.level] }}&nbsp&nbsp{{ record.difficultyNum }}
</h6>
</div>
<div class="col-6" style="text-align: center;">
<table class="table-small table-rowdistinc">
<tr>
<td>JUSTICE CRITIAL</td>
<td>
{{ record.raw.judgeCritical + record.raw.judgeHeaven }}
</td>
</tr>
<tr>
<td>JUSTICE</td>
<td>
{{ record.raw.judgeJustice }}
</td>
</tr>
<tr>
<td>ATTACK</td>
<td>
{{ record.raw.judgeAttack }}
</td>
</tr>
<tr>
<td>MISS</td>
<td>
{{ record.raw.judgeGuilty }}
</td>
</tr>
</table>
</div>
<div class="col-3" style="text-align: center;">
{%if record.raw.isFullCombo == 1 %}
<h6>FULL COMBO</h6>
{% endif %}
{%if record.raw.isAllJustice == 1 %}
<h6>ALL JUSTICE</h6>
{% endif %}
{%if record.raw.isNewRecord == 1 %}
<h6>NEW RECORD</h6>
{% endif %}
{%if record.raw.fullChainKind > 0 %}
<h6>FULL CHAIN</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/chuni/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/chuni/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/chuni/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/chuni/playlog/' + pageNumber;
window.location.href = url;
} else {
$('#page_input').val('');
$('#page_input').attr('placeholder', 'invalid input!');
}
});
});
</script>
{% endblock content %}

View File

@@ -1,83 +0,0 @@
{% extends "core/templates/index.jinja" %}
{% block content %}
<style>
{% include 'titles/chuni/templates/css/chuni_style.css' %}
</style>
<div class="container">
{% include 'titles/chuni/templates/chuni_header.jinja' %}
{% if profile is defined and profile is not none and profile.id > 0 %}
<h4 style="text-align: center;">Rating: {{ profile.playerRating|float/100 }}&nbsp&nbsp&nbsp&nbspPlayer Counts: {{
profile.playCount }}</h4>
<div class="row">
{% if hot_list %}
<div class="col-lg-6 mt-3">
<div class="card bg-card rounded">
<table class="table-large table-rowdistinct">
<caption align="top">Recent 10</caption>
<tr>
<th>Music</th>
<th>Difficulty</th>
<th>Score</th>
<th>Rank</th>
<th>Rating</th>
</tr>
{% for row in hot_list %}
<tr>
<td>{{ row.title }}</td>
<td
class="{% if row.difficultId == 0 %}basic{% elif row.difficultId == 1 %}{% elif row.difficultId == 2 %}expert{% elif row.difficultId == 3 %}master{% else %}{% endif %}">
{{ row.level }}
</td>
<td>{{ row.score }}</td>
<td>{{ row.rank }}</td>
<td class="{% if row.song_rating >= 16 %}rainbow{% endif %}">
{{ row.song_rating }}
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
{% else %}
<p>No r10 found</p>
{% endif %}
{% if base_list %}
<div class="col-lg-6 mt-3">
<div class="card bg-card rounded">
<table class="table-large table-rowdistinct">
<caption align="top">Best 30</caption>
<tr>
<th>Music</th>
<th>Difficulty</th>
<th>Score</th>
<th>Rank</th>
<th>Rating</th>
</tr>
{% for row in base_list %}
<tr>
<td>{{ row.title }}</td>
<td
class="{% if row.difficultId == 0 %}normal{% elif row.difficultId == 1 %}hard{% elif row.difficultId == 2 %}expert{% elif row.difficultId == 3 %}master{% else %}{% endif %}">
{{ row.level }}
</td>
<td>{{ row.score }}</td>
<td>{{ row.rank }}</td>
<td class="{% if row.song_rating >= 16 %}rainbow{% endif %}">
{{ row.song_rating }}
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
{% else %}
<p>No b30 found</p>
{% endif %}
</div>
{% 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>
{% endblock content %}

View File

@@ -1,195 +0,0 @@
.chuni-header {
text-align: center;
}
ul.chuni-navi {
list-style-type: none;
padding: 0;
overflow: hidden;
background-color: #333;
text-align: center;
display: inline-block;
}
ul.chuni-navi li {
display: inline-block;
}
ul.chuni-navi li a {
display: block;
color: white;
text-align: center;
padding: 14px 16px;
text-decoration: none;
}
ul.chuni-navi li a:hover:not(.active) {
background-color: #111;
}
ul.chuni-navi li a.active {
background-color: #4CAF50;
}
ul.chuni-navi li.right {
float: right;
}
@media screen and (max-width: 600px) {
ul.chuni-navi li.right,
ul.chuni-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);
}
.normal {
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

@@ -51,7 +51,7 @@ class CardMakerBase:
{ {
"modelKind": 1, "modelKind": 1,
"type": 1, "type": 1,
"titleUri": f"{uri}/SDEZ/{self._parse_int_ver(games_ver['maimai'])}/Maimai2Servlet/", "titleUri": f"{uri}/{self._parse_int_ver(games_ver['maimai'])}/Maimai2Servlet/",
}, },
# ONGEKI # ONGEKI
{ {

View File

@@ -74,7 +74,7 @@ class CardMakerServlet(BaseServlet):
def get_routes(self) -> List[Route]: def get_routes(self) -> List[Route]:
return [ return [
Route("/SDED/{version:int}/{endpoint:str}", self.render_POST, methods=['POST']) Route("/SDED/{version:int}/{endpoint:str}", self.render_POST)
] ]
async def render_POST(self, request: Request) -> bytes: async def render_POST(self, request: Request) -> bytes:
@@ -117,7 +117,7 @@ class CardMakerServlet(BaseServlet):
try: try:
handler = getattr(self.versions[internal_ver], func_to_find) handler = getattr(self.versions[internal_ver], func_to_find)
resp = await handler(req_data) resp = handler(req_data)
except Exception as e: except Exception as e:
self.logger.error(f"Error handling v{version} method {endpoint} - {e}") self.logger.error(f"Error handling v{version} method {endpoint} - {e}")

View File

@@ -59,10 +59,10 @@ class CardMakerReader(BaseReader):
if self.bin_dir is not None: if self.bin_dir is not None:
data_dir = self._get_card_maker_directory(self.bin_dir) data_dir = self._get_card_maker_directory(self.bin_dir)
await self.read_chuni_card(f"{data_dir}/CHU/Data/A000/card") self.read_chuni_card(f"{data_dir}/CHU/Data/A000/card")
await self.read_chuni_gacha(f"{data_dir}/CHU/Data/A000/gacha") self.read_chuni_gacha(f"{data_dir}/CHU/Data/A000/gacha")
await self.read_mai2_card(f"{data_dir}/MAI/Data/A000/card") self.read_mai2_card(f"{data_dir}/MAI/Data/A000/card")
for file, func in static_datas.items(): for file, func in static_datas.items():
if os.path.exists(f"{self.bin_dir}/MU3/{file}"): if os.path.exists(f"{self.bin_dir}/MU3/{file}"):
read_csv = getattr(CardMakerReader, func) read_csv = getattr(CardMakerReader, func)
@@ -206,7 +206,6 @@ class CardMakerReader(BaseReader):
"1.25": Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS, "1.25": Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS,
"1.30": Mai2Constants.VER_MAIMAI_DX_FESTIVAL, "1.30": Mai2Constants.VER_MAIMAI_DX_FESTIVAL,
"1.35": Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS, "1.35": Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS,
"1.40": Mai2Constants.VER_MAIMAI_DX_BUDDIES,
} }
for root, dirs, files in os.walk(base_dir): for root, dirs, files in os.walk(base_dir):
@@ -226,6 +225,12 @@ class CardMakerReader(BaseReader):
True if troot.find("disable").text == "false" else False True if troot.find("disable").text == "false" else False
) )
# check if a date is part of the name and disable the
# card if it is
enabled = (
False if re.search(r"\d{2}/\d{2}/\d{2}", name) else enabled
)
await self.mai2_data.static.put_card( await self.mai2_data.static.put_card(
version, card_id, name, enabled=enabled version, card_id, name, enabled=enabled
) )

View File

@@ -0,0 +1,3 @@
saleID.,<EFBFBD>J<EFBFBD>n<EFBFBD><EFBFBD>,<EFBFBD>I<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,ShopID,Price,
0,1411696799,1443236400,0,7000,
1,1411783199,1443322800,1,7000,
1 saleID. �J�n�� �I���� ShopID Price
2 0 1411696799 1443236400 0 7000
3 1 1411783199 1443322800 1 7000

View File

@@ -0,0 +1,4 @@
shopID.,整理用No.,Ver.,出現フラグ,出現フラグ参照ID,条件,出現時間,消滅時間,ItemCode,価格,表示タイプ,Text,Type,Value(op),Value,対象曲,Difficulty(op),Difficulty,Level(op),Level,Grade(Op),Grade,GaugeType(op),GaugeType,HS(op)i,HS,APP,DAP,F-V,F-H,FullCombo,Combo(op),Combo,ClearRate(op),ClearRate,プレイ日,地域,
3000,1,1.00.00,1,-1,-,1411697520.0288,1443233520.0288,skb0000,10,1,MASTER以上の2曲S+以上フルコンボクリアする。,1,1,1,-,1,2,-1,-1,1,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,,
3001,2,1.00.00,1,-1,-,1411697520.0288,1443233520.0288,skb0001,10,1,Next Frontier (Masterをクリア,0,-1,-1,bleeze,1,2,-1,-1,1,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,,
3002,3,1.00.00,1,-1,-,1412103600.0288,1443639598.992,skb0002,10,2,Master以上を1曲をS+以上でクリアする。,1,1,1,-,1,2,-1,-1,1,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,1,
1 shopID. 整理用No. Ver. 出現フラグ 出現フラグ参照ID 条件 出現時間 消滅時間 ItemCode 価格 表示タイプ Text Type Value(op) Value 対象曲 Difficulty(op) Difficulty Level(op) Level Grade(Op) Grade GaugeType(op) GaugeType HS(op)i HS APP DAP F-V F-H FullCombo Combo(op) Combo ClearRate(op) ClearRate プレイ日 地域
2 3000 1 1.00.00 1 -1 - 1411697520.0288 1443233520.0288 skb0000 10 1 MASTER以上の2曲S+以上フルコンボクリアする。 1 1 1 - 1 2 -1 -1 1 1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
3 3001 2 1.00.00 1 -1 - 1411697520.0288 1443233520.0288 skb0001 10 1 Next Frontier (Master)をクリア 0 -1 -1 bleeze 1 2 -1 -1 1 1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
4 3002 3 1.00.00 1 -1 - 1412103600.0288 1443639598.992 skb0002 10 2 Master以上を1曲をS+以上でクリアする。 1 1 1 - 1 2 -1 -1 1 1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 1

View File

@@ -0,0 +1,11 @@
shopID.,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>pNo.,Ver.,<EFBFBD>o<EFBFBD><EFBFBD><EFBFBD>t<EFBFBD><EFBFBD><EFBFBD>O,<EFBFBD>o<EFBFBD><EFBFBD><EFBFBD>t<EFBFBD><EFBFBD><EFBFBD>O<EFBFBD>Q<EFBFBD><EFBFBD>ID,<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,<EFBFBD>o<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,<EFBFBD><EFBFBD><EFBFBD>Ŏ<EFBFBD><EFBFBD><EFBFBD>,ItemCode,<EFBFBD><EFBFBD><EFBFBD>i,<EFBFBD>\<5C><><EFBFBD>^<5E>C<EFBFBD>v,Text,Type,Value(op),Value,<EFBFBD>Ώۋ<EFBFBD>,Difficulty(op),Difficulty,Level(op),Level,Grade(Op),Grade,GaugeType(op),GaugeType,HS(op)i,HS,APP,DAP,F-V,F-H,FullCombo,Combo(op),Combo,ClearRate(op),ClearRate,<EFBFBD>v<EFBFBD><EFBFBD><EFBFBD>C<EFBFBD><EFBFBD>,<EFBFBD>n<EFBFBD><EFBFBD>,
5000,1,10000,1,-1,-,1411697520,1443233520,ske0000,10,1,MASTER<EFBFBD>ȏ<EFBFBD><EFBFBD>2<EFBFBD><EFBFBD>S+<2B>ȏ<EFBFBD>t<EFBFBD><74><EFBFBD>R<EFBFBD><52><EFBFBD>{<7B>N<EFBFBD><4E><EFBFBD>A<EFBFBD><41><EFBFBD><EFBFBD>B,1,1,1,-,1,2,-1,-1,1,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,,
5001,2,10000,1,-1,-,1411697520,1443233520,ske0001,10,1,Next Frontier (Master<65>j<EFBFBD><6A><EFBFBD>N<EFBFBD><4E><EFBFBD>A,0,-1,-1,megaro,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,,
5002,3,10000,1,-1,-,1412103600,1443639598,ske0002,10,2,Master<EFBFBD>ȏ<EFBFBD><EFBFBD>1<EFBFBD>Ȃ<EFBFBD>S+<2B>ȏ<EFBFBD>ŃN<C583><4E><EFBFBD>A<EFBFBD><41><EFBFBD><EFBFBD>B,1,1,1,-,1,2,-1,-1,1,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,1,
5003,4,10000,1,-1,-,1412103600,1443639598,ske0003,10,0,Master<EFBFBD>ȏ<EFBFBD><EFBFBD>1<EFBFBD>Ȃ<EFBFBD>S+<2B>ȏ<EFBFBD>ŃN<C583><4E><EFBFBD>A<EFBFBD><41><EFBFBD><EFBFBD>B,1,1,1,-,1,2,-1,-1,1,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,,
5004,5,10000,1,-1,-,1412103600,1443639598,ske0004,10,2,2<EFBFBD>ȃN<EFBFBD><EFBFBD><EFBFBD>A,1,1,2,-,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,,
5005,5,10000,1,-1,-,1412103600,1443639598,ske0005,10,2,3<EFBFBD>ȃN<EFBFBD><EFBFBD><EFBFBD>A,1,1,3,-,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,,
5006,5,10000,1,-1,-,1412103600,1443639598,ske0006,10,2,4<EFBFBD>ȃN<EFBFBD><EFBFBD><EFBFBD>A,1,1,4,-,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,,
5007,5,10000,1,-1,-,1412103600,1443639598,ske0007,10,2,5<EFBFBD>ȃN<EFBFBD><EFBFBD><EFBFBD>A,1,1,5,-,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,,
5008,5,10000,1,-1,-,1412103600,1443639598,ske0008,10,2,6<EFBFBD>ȃN<EFBFBD><EFBFBD><EFBFBD>A,1,1,6,-,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,,
5009,5,10000,1,-1,-,1412103600,1443639598,ske0009,10,2,7<EFBFBD>ȃN<EFBFBD><EFBFBD><EFBFBD>A,1,1,7,-,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,,
1 shopID. �����pNo. Ver. �o���t���O �o���t���O�Q��ID ���� �o������ ���Ŏ��� ItemCode ���i �\���^�C�v Text Type Value(op) Value �Ώۋ� Difficulty(op) Difficulty Level(op) Level Grade(Op) Grade GaugeType(op) GaugeType HS(op)i HS APP DAP F-V F-H FullCombo Combo(op) Combo ClearRate(op) ClearRate �v���C�� �n��
2 5000 1 10000 1 -1 - 1411697520 1443233520 ske0000 10 1 MASTER�ȏ��2��S+�ȏ�t���R���{�N���A����B 1 1 1 - 1 2 -1 -1 1 1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
3 5001 2 10000 1 -1 - 1411697520 1443233520 ske0001 10 1 Next Frontier (Master�j���N���A 0 -1 -1 megaro -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
4 5002 3 10000 1 -1 - 1412103600 1443639598 ske0002 10 2 Master�ȏ��1�Ȃ�S+�ȏ�ŃN���A����B 1 1 1 - 1 2 -1 -1 1 1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 1
5 5003 4 10000 1 -1 - 1412103600 1443639598 ske0003 10 0 Master�ȏ��1�Ȃ�S+�ȏ�ŃN���A����B 1 1 1 - 1 2 -1 -1 1 1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
6 5004 5 10000 1 -1 - 1412103600 1443639598 ske0004 10 2 2�ȃN���A 1 1 2 - -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
7 5005 5 10000 1 -1 - 1412103600 1443639598 ske0005 10 2 3�ȃN���A 1 1 3 - -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
8 5006 5 10000 1 -1 - 1412103600 1443639598 ske0006 10 2 4�ȃN���A 1 1 4 - -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
9 5007 5 10000 1 -1 - 1412103600 1443639598 ske0007 10 2 5�ȃN���A 1 1 5 - -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
10 5008 5 10000 1 -1 - 1412103600 1443639598 ske0008 10 2 6�ȃN���A 1 1 6 - -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
11 5009 5 10000 1 -1 - 1412103600 1443639598 ske0009 10 2 7�ȃN���A 1 1 7 - -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1

View File

@@ -0,0 +1,6 @@
shopID.,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>pNo.,Ver.,<EFBFBD>o<EFBFBD><EFBFBD><EFBFBD>t<EFBFBD><EFBFBD><EFBFBD>O,<EFBFBD>o<EFBFBD><EFBFBD><EFBFBD>t<EFBFBD><EFBFBD><EFBFBD>O<EFBFBD>Q<EFBFBD><EFBFBD>ID,<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,<EFBFBD>o<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,<EFBFBD><EFBFBD><EFBFBD>Ŏ<EFBFBD><EFBFBD><EFBFBD>,ItemCode,<EFBFBD><EFBFBD><EFBFBD>i,<EFBFBD>\<5C><><EFBFBD>^<5E>C<EFBFBD>v,Text,Type,Value(op),Value,<EFBFBD>Ώۋ<EFBFBD>,Difficulty(op),Difficulty,Level(op),Level,Grade(Op),Grade,GaugeType(op),GaugeType,HS(op)i,HS,APP,DAP,F-V,F-H,FullCombo,Combo(op),Combo,ClearRate(op),ClearRate,<EFBFBD>v<EFBFBD><EFBFBD><EFBFBD>C<EFBFBD><EFBFBD>,<EFBFBD>n<EFBFBD><EFBFBD>,
4000,1,10000,1,-1,-,1411697520,4096483201,skt0000,10,1,MASTER<EFBFBD>ȏ<EFBFBD><EFBFBD>2<EFBFBD><EFBFBD>S+<2B>ȏ<EFBFBD>t<EFBFBD><74><EFBFBD>R<EFBFBD><52><EFBFBD>{<7B>N<EFBFBD><4E><EFBFBD>A<EFBFBD><41><EFBFBD><EFBFBD>B,1,1,1,-,1,2,-1,-1,1,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,,
4001,2,10000,1,-1,-,1411697520,4096483201,skt0001,10,1,Next Frontier (Master<65>j<EFBFBD><6A><EFBFBD>N<EFBFBD><4E><EFBFBD>A,0,-1,-1,megaro,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,,
4002,3,10000,1,-1,-,1412103600,4096483201,skt0002,10,2,Master<EFBFBD>ȏ<EFBFBD><EFBFBD>1<EFBFBD>Ȃ<EFBFBD>S+<2B>ȏ<EFBFBD>ŃN<C583><4E><EFBFBD>A<EFBFBD><41><EFBFBD><EFBFBD>B,1,1,1,-,1,2,-1,-1,1,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,1,
4003,4,10000,1,-1,-,1412103600,4096483201,skt0003,10,0,Master<EFBFBD>ȏ<EFBFBD><EFBFBD>1<EFBFBD>Ȃ<EFBFBD>S+<2B>ȏ<EFBFBD>ŃN<C583><4E><EFBFBD>A<EFBFBD><41><EFBFBD><EFBFBD>B,1,1,1,-,1,2,-1,-1,1,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,,
4004,5,10000,1,-1,-,1412103600,4096483201,skt0004,10,2,aaaaaaaaaaaaaaaaa,1,1,20,-,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,,
1 shopID. �����pNo. Ver. �o���t���O �o���t���O�Q��ID ���� �o������ ���Ŏ��� ItemCode ���i �\���^�C�v Text Type Value(op) Value �Ώۋ� Difficulty(op) Difficulty Level(op) Level Grade(Op) Grade GaugeType(op) GaugeType HS(op)i HS APP DAP F-V F-H FullCombo Combo(op) Combo ClearRate(op) ClearRate �v���C�� �n��
2 4000 1 10000 1 -1 - 1411697520 4096483201 skt0000 10 1 MASTER�ȏ��2��S+�ȏ�t���R���{�N���A����B 1 1 1 - 1 2 -1 -1 1 1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
3 4001 2 10000 1 -1 - 1411697520 4096483201 skt0001 10 1 Next Frontier (Master�j���N���A 0 -1 -1 megaro -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
4 4002 3 10000 1 -1 - 1412103600 4096483201 skt0002 10 2 Master�ȏ��1�Ȃ�S+�ȏ�ŃN���A����B 1 1 1 - 1 2 -1 -1 1 1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 1
5 4003 4 10000 1 -1 - 1412103600 4096483201 skt0003 10 0 Master�ȏ��1�Ȃ�S+�ȏ�ŃN���A����B 1 1 1 - 1 2 -1 -1 1 1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
6 4004 5 10000 1 -1 - 1412103600 4096483201 skt0004 10 2 aaaaaaaaaaaaaaaaa 1 1 20 - -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1

View File

@@ -98,7 +98,7 @@ class CxbServlet(BaseServlet):
t_port = f":{title_port_int}" if title_port_int != 80 else "" t_port = f":{title_port_int}" if title_port_int != 80 else ""
return ( return (
f"{proto}://{self.core_cfg.server.hostname}{t_port}", f"{proto}://{self.core_cfg.title.hostname}{t_port}",
"", "",
) )

View File

@@ -40,7 +40,6 @@ class CxbRev(CxbBase):
score_data["slow2"], score_data["slow2"],
score_data["fail"], score_data["fail"],
score_data["combo"], score_data["combo"],
score_data["grade"],
) )
return {"data": True} return {"data": True}
return {"data": True} return {"data": True}
@@ -48,7 +47,7 @@ class CxbRev(CxbBase):
@cached(lifetime=86400) @cached(lifetime=86400)
async def handle_data_music_list_request(self, data: Dict) -> Dict: async def handle_data_music_list_request(self, data: Dict) -> Dict:
ret_str = "" ret_str = ""
with open(r"titles/cxb/data/rev/MusicArchiveList.csv") as music: with open(r"titles/cxb/data/rss/MusicArchiveList.csv") as music:
lines = music.readlines() lines = music.readlines()
for line in lines: for line in lines:
line_split = line.split(",") line_split = line.split(",")
@@ -60,7 +59,7 @@ class CxbRev(CxbBase):
async def handle_data_item_list_icon_request(self, data: Dict) -> Dict: async def handle_data_item_list_icon_request(self, data: Dict) -> Dict:
ret_str = "\r\n#ItemListIcon\r\n" ret_str = "\r\n#ItemListIcon\r\n"
with open( with open(
r"titles/cxb/data/rev/Item/ItemArchiveList_Icon.csv", encoding="shift-jis" r"titles/cxb/data/rss/Item/ItemArchiveList_Icon.csv", encoding="utf-8"
) as item: ) as item:
lines = item.readlines() lines = item.readlines()
for line in lines: for line in lines:
@@ -71,7 +70,7 @@ class CxbRev(CxbBase):
async def handle_data_item_list_skin_notes_request(self, data: Dict) -> Dict: async def handle_data_item_list_skin_notes_request(self, data: Dict) -> Dict:
ret_str = "\r\n#ItemListSkinNotes\r\n" ret_str = "\r\n#ItemListSkinNotes\r\n"
with open( with open(
r"titles/cxb/data/rev/Item/ItemArchiveList_SkinNotes.csv", encoding="utf-8" r"titles/cxb/data/rss/Item/ItemArchiveList_SkinNotes.csv", encoding="utf-8"
) as item: ) as item:
lines = item.readlines() lines = item.readlines()
for line in lines: for line in lines:
@@ -82,7 +81,7 @@ class CxbRev(CxbBase):
async def handle_data_item_list_skin_effect_request(self, data: Dict) -> Dict: async def handle_data_item_list_skin_effect_request(self, data: Dict) -> Dict:
ret_str = "\r\n#ItemListSkinEffect\r\n" ret_str = "\r\n#ItemListSkinEffect\r\n"
with open( with open(
r"titles/cxb/data/rev/Item/ItemArchiveList_SkinEffect.csv", encoding="utf-8" r"titles/cxb/data/rss/Item/ItemArchiveList_SkinEffect.csv", encoding="utf-8"
) as item: ) as item:
lines = item.readlines() lines = item.readlines()
for line in lines: for line in lines:
@@ -93,7 +92,7 @@ class CxbRev(CxbBase):
async def handle_data_item_list_skin_bg_request(self, data: Dict) -> Dict: async def handle_data_item_list_skin_bg_request(self, data: Dict) -> Dict:
ret_str = "\r\n#ItemListSkinBg\r\n" ret_str = "\r\n#ItemListSkinBg\r\n"
with open( with open(
r"titles/cxb/data/rev/Item/ItemArchiveList_SkinBg.csv", encoding="utf-8" r"titles/cxb/data/rss/Item/ItemArchiveList_SkinBg.csv", encoding="utf-8"
) as item: ) as item:
lines = item.readlines() lines = item.readlines()
for line in lines: for line in lines:
@@ -104,7 +103,7 @@ class CxbRev(CxbBase):
async def handle_data_item_list_title_request(self, data: Dict) -> Dict: async def handle_data_item_list_title_request(self, data: Dict) -> Dict:
ret_str = "\r\n#ItemListTitle\r\n" ret_str = "\r\n#ItemListTitle\r\n"
with open( with open(
r"titles/cxb/data/rev/Item/ItemList_Title.csv", encoding="shift-jis" r"titles/cxb/data/rss/Item/ItemList_Title.csv", encoding="shift-jis"
) as item: ) as item:
lines = item.readlines() lines = item.readlines()
for line in lines: for line in lines:
@@ -115,7 +114,7 @@ class CxbRev(CxbBase):
async def handle_data_shop_list_music_request(self, data: Dict) -> Dict: async def handle_data_shop_list_music_request(self, data: Dict) -> Dict:
ret_str = "\r\n#ShopListMusic\r\n" ret_str = "\r\n#ShopListMusic\r\n"
with open( with open(
r"titles/cxb/data/rev/Shop/ShopList_Music.csv", encoding="shift-jis" r"titles/cxb/data/rss/Shop/ShopList_Music.csv", encoding="shift-jis"
) as shop: ) as shop:
lines = shop.readlines() lines = shop.readlines()
for line in lines: for line in lines:
@@ -126,7 +125,7 @@ class CxbRev(CxbBase):
async def handle_data_shop_list_icon_request(self, data: Dict) -> Dict: async def handle_data_shop_list_icon_request(self, data: Dict) -> Dict:
ret_str = "\r\n#ShopListIcon\r\n" ret_str = "\r\n#ShopListIcon\r\n"
with open( with open(
r"titles/cxb/data/rev/Shop/ShopList_Icon.csv", encoding="shift-jis" r"titles/cxb/data/rss/Shop/ShopList_Icon.csv", encoding="shift-jis"
) as shop: ) as shop:
lines = shop.readlines() lines = shop.readlines()
for line in lines: for line in lines:
@@ -137,7 +136,7 @@ class CxbRev(CxbBase):
async def handle_data_shop_list_title_request(self, data: Dict) -> Dict: async def handle_data_shop_list_title_request(self, data: Dict) -> Dict:
ret_str = "\r\n#ShopListTitle\r\n" ret_str = "\r\n#ShopListTitle\r\n"
with open( with open(
r"titles/cxb/data/rev/Shop/ShopList_Title.csv", encoding="shift-jis" r"titles/cxb/data/rss/Shop/ShopList_Title.csv", encoding="shift-jis"
) as shop: ) as shop:
lines = shop.readlines() lines = shop.readlines()
for line in lines: for line in lines:
@@ -157,7 +156,7 @@ class CxbRev(CxbBase):
async def handle_data_shop_list_sale_request(self, data: Dict) -> Dict: async def handle_data_shop_list_sale_request(self, data: Dict) -> Dict:
ret_str = "\r\n#ShopListSale\r\n" ret_str = "\r\n#ShopListSale\r\n"
with open( with open(
r"titles/cxb/data/rev/Shop/ShopList_Sale.csv", encoding="shift-jis" r"titles/cxb/data/rss/Shop/ShopList_Sale.csv", encoding="shift-jis"
) as shop: ) as shop:
lines = shop.readlines() lines = shop.readlines()
for line in lines: for line in lines:
@@ -165,19 +164,14 @@ class CxbRev(CxbBase):
return {"data": ret_str} return {"data": ret_str}
async def handle_data_extra_stage_list_request(self, data: Dict) -> Dict: async def handle_data_extra_stage_list_request(self, data: Dict) -> Dict:
ret_str = "" return {"data": ""}
with open(r"titles/cxb/data/rev/ExtraStageList.csv", encoding="shift-jis") as stage:
lines = stage.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
return {"data": ret_str}
@cached(lifetime=86400) @cached(lifetime=86400)
async def handle_data_exxxxx_request(self, data: Dict) -> Dict: async def handle_data_exxxxx_request(self, data: Dict) -> Dict:
extra_num = int(data["dldate"]["filetype"][-4:]) extra_num = int(data["dldate"]["filetype"][-4:])
ret_str = "" ret_str = ""
with open( with open(
rf"titles/cxb/data/rev/Ex000{extra_num}.csv", encoding="shift-jis" rf"titles/cxb/data/rss/Ex000{extra_num}.csv", encoding="shift-jis"
) as stage: ) as stage:
lines = stage.readlines() lines = stage.readlines()
for line in lines: for line in lines:
@@ -193,7 +187,7 @@ class CxbRev(CxbBase):
@cached(lifetime=86400) @cached(lifetime=86400)
async def handle_data_news_list_request(self, data: Dict) -> Dict: async def handle_data_news_list_request(self, data: Dict) -> Dict:
ret_str = "" ret_str = ""
with open(r"titles/cxb/data/rev/NewsList.csv", encoding="UTF-8") as news: with open(r"titles/cxb/data/rss/NewsList.csv", encoding="UTF-8") as news:
lines = news.readlines() lines = news.readlines()
for line in lines: for line in lines:
ret_str += f"{line[:-1]}\r\n" ret_str += f"{line[:-1]}\r\n"
@@ -205,7 +199,7 @@ class CxbRev(CxbBase):
@cached(lifetime=86400) @cached(lifetime=86400)
async def handle_data_license_request(self, data: Dict) -> Dict: async def handle_data_license_request(self, data: Dict) -> Dict:
ret_str = "" ret_str = ""
with open(r"titles/cxb/data/rev/License_Offline.csv", encoding="UTF-8") as lic: with open(r"titles/cxb/data/rss/License_Offline.csv", encoding="UTF-8") as lic:
lines = lic.readlines() lines = lic.readlines()
for line in lines: for line in lines:
ret_str += f"{line[:-1]}\r\n" ret_str += f"{line[:-1]}\r\n"
@@ -215,7 +209,7 @@ class CxbRev(CxbBase):
async def handle_data_course_list_request(self, data: Dict) -> Dict: async def handle_data_course_list_request(self, data: Dict) -> Dict:
ret_str = "" ret_str = ""
with open( with open(
r"titles/cxb/data/rev/Course/CourseList.csv", encoding="UTF-8" r"titles/cxb/data/rss/Course/CourseList.csv", encoding="UTF-8"
) as course: ) as course:
lines = course.readlines() lines = course.readlines()
for line in lines: for line in lines:
@@ -228,7 +222,7 @@ class CxbRev(CxbBase):
extra_num = int(data["dldate"]["filetype"][-4:]) extra_num = int(data["dldate"]["filetype"][-4:])
ret_str = "" ret_str = ""
with open( with open(
rf"titles/cxb/data/rev/Course/Cs000{extra_num}.csv", encoding="shift-jis" rf"titles/cxb/data/rss/Course/Cs000{extra_num}.csv", encoding="shift-jis"
) as course: ) as course:
lines = course.readlines() lines = course.readlines()
for line in lines: for line in lines:
@@ -239,7 +233,7 @@ class CxbRev(CxbBase):
async def handle_data_mission_list_request(self, data: Dict) -> Dict: async def handle_data_mission_list_request(self, data: Dict) -> Dict:
ret_str = "" ret_str = ""
with open( with open(
r"titles/cxb/data/rev/MissionList.csv", encoding="shift-jis" r"titles/cxb/data/rss/MissionList.csv", encoding="shift-jis"
) as mission: ) as mission:
lines = mission.readlines() lines = mission.readlines()
for line in lines: for line in lines:
@@ -256,7 +250,7 @@ class CxbRev(CxbBase):
async def handle_data_event_list_request(self, data: Dict) -> Dict: async def handle_data_event_list_request(self, data: Dict) -> Dict:
ret_str = "" ret_str = ""
with open( with open(
r"titles/cxb/data/rev/Event/EventArchiveList.csv", encoding="shift-jis" r"titles/cxb/data/rss/Event/EventArchiveList.csv", encoding="shift-jis"
) as mission: ) as mission:
lines = mission.readlines() lines = mission.readlines()
for line in lines: for line in lines:
@@ -298,7 +292,7 @@ class CxbRev(CxbBase):
async def handle_data_event_stamp_list_request(self, data: Dict) -> Dict: async def handle_data_event_stamp_list_request(self, data: Dict) -> Dict:
ret_str = "" ret_str = ""
with open( with open(
r"titles/cxb/data/rev/Event/EventStampList.csv", encoding="shift-jis" r"titles/cxb/data/rss/Event/EventStampList.csv", encoding="shift-jis"
) as event: ) as event:
lines = event.readlines() lines = event.readlines()
for line in lines: for line in lines:

View File

@@ -73,6 +73,42 @@ class CxbRevSunriseS1(CxbBase):
for line in lines: for line in lines:
ret_str += f"{line[:-1]}\r\n" ret_str += f"{line[:-1]}\r\n"
# ShopListSale load
ret_str += "\r\n#ShopListSale\r\n"
with open(
r"titles/cxb/data/rss1/Shop/ShopList_Sale.csv", encoding="shift-jis"
) as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
# ShopListSkinBg load
ret_str += "\r\n#ShopListSkinBg\r\n"
with open(
r"titles/cxb/data/rss1/Shop/ShopList_SkinBg.csv", encoding="shift-jis"
) as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
# ShopListSkinEffect load
ret_str += "\r\n#ShopListSkinEffect\r\n"
with open(
r"titles/cxb/data/rss1/Shop/ShopList_SkinEffect.csv", encoding="shift-jis"
) as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
# ShopListSkinNotes load
ret_str += "\r\n#ShopListSkinNotes\r\n"
with open(
r"titles/cxb/data/rss1/Shop/ShopList_SkinNotes.csv", encoding="shift-jis"
) as shop:
lines = shop.readlines()
for line in lines:
ret_str += f"{line[:-1]}\r\n"
# ShopListTitle load # ShopListTitle load
ret_str += "\r\n#ShopListTitle\r\n" ret_str += "\r\n#ShopListTitle\r\n"
with open( with open(

View File

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

View File

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

View File

@@ -8,6 +8,7 @@ from core.config import CoreConfig
from titles.diva.config import DivaConfig from titles.diva.config import DivaConfig
from titles.diva.const import DivaConstants from titles.diva.const import DivaConstants
from titles.diva.database import DivaData from titles.diva.database import DivaData
from titles.diva.handlers import *
class DivaBase: class DivaBase:
@@ -20,8 +21,8 @@ class DivaBase:
self.game = DivaConstants.GAME_CODE self.game = DivaConstants.GAME_CODE
self.version = DivaConstants.VER_PROJECT_DIVA_ARCADE_FUTURE_TONE self.version = DivaConstants.VER_PROJECT_DIVA_ARCADE_FUTURE_TONE
dt = datetime.datetime.now() dt = datetime.now()
self.time_lut = urllib.parse.quote(dt.strftime("%Y-%m-%d %H:%M:%S:16.0")) self.time_lut = parse.quote(dt.strftime("%Y-%m-%d %H:%M:%S:16.0"))
async def handle_test_request(self, data: Dict) -> Dict: async def handle_test_request(self, data: Dict) -> Dict:
return "" return ""
@@ -29,24 +30,32 @@ class DivaBase:
async def handle_game_init_request(self, data: Dict) -> Dict: async def handle_game_init_request(self, data: Dict) -> Dict:
return f"" return f""
async def handle_attend_request(self, data: Dict) -> Dict: async def handle_attend_request(self, data: bytes) -> str:
req = AttendRequest(data)
resp = AttendResponse(req.req_id)
for i in [0, 3, 4, 5, 7, 9, 10, 11, 12, 13]:
resp.atnd_prm1[i] = 0
resp.atnd_prm1[8] = 100
resp.atnd_prm2[:6] = [30, 10, 100, 4, 1, 50]
resp.atnd_prm3[0] = 100
resp.atnd_prm3[1] = 0
resp.atnd_prm3[10:13] = [2, 3, 4]
resp.atnd_prm3[16:19] = [3, 4, 5]
resp.atnd_prm3[22:25] = [4, 5, 6]
resp.atnd_prm3[80:] = [0] * 20
resp.atnd_prm3[28:79] = [5,6,7,4,4,4,9,10,14,5,10,10,25,20,50,30,90,5,
10,10,25,20,50,30,90,5,10,10,25,20,50,30,90,5,10,10,25,20,50,30,90,
5,10,10,25,20,50,30,90,10,30]
return resp.make()
async def handle_ping_request(self, data: bytes) -> str:
encoded = "&" encoded = "&"
params = { params = {
"atnd_prm1": "0,1,1,0,0,0,1,0,100,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1", "ping_b_msg": f"Welcome to {self.core_cfg.server.name} network!" if not self.game_config.server.banner_msg else self.game_config.server.banner_msg,
"atnd_prm2": "30,10,100,4,1,50,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1",
"atnd_prm3": "100,0,1,1,1,1,1,1,1,1,2,3,4,1,1,1,3,4,5,1,1,1,4,5,6,1,1,1,5,6,7,4,4,4,9,10,14,5,10,10,25,20,50,30,90,5,10,10,25,20,50,30,90,5,10,10,25,20,50,30,90,5,10,10,25,20,50,30,90,5,10,10,25,20,50,30,90,10,30,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0",
"atnd_lut": f"{self.time_lut}",
}
encoded += urllib.parse.urlencode(params)
encoded = encoded.replace("%2C", ",")
return encoded
async def handle_ping_request(self, data: Dict) -> Dict:
encoded = "&"
params = {
"ping_b_msg": f"Welcome to {self.core_cfg.server.name} network!",
"ping_m_msg": "xxx", "ping_m_msg": "xxx",
"atnd_lut": f"{self.time_lut}", "atnd_lut": f"{self.time_lut}",
"fi_lut": f"{self.time_lut}", "fi_lut": f"{self.time_lut}",
@@ -82,9 +91,8 @@ class DivaBase:
"nblss_ltt_ed_tm": "2019-09-22 12:00:00.0", "nblss_ltt_ed_tm": "2019-09-22 12:00:00.0",
} }
encoded += urllib.parse.urlencode(params) encoded += parse.urlencode(params, safe=",")
encoded = encoded.replace("+", "%20") encoded = encoded.replace("+", "%20")
encoded = encoded.replace("%2C", ",")
return encoded return encoded
@@ -133,8 +141,8 @@ class DivaBase:
with open(r"titles/diva/data/ShopCatalog.dat", encoding="utf-8") as shop: with open(r"titles/diva/data/ShopCatalog.dat", encoding="utf-8") as shop:
lines = shop.readlines() lines = shop.readlines()
for line in lines: for line in lines:
line = urllib.parse.quote(line) + "," line = parse.quote(line) + ","
catalog += f"{urllib.parse.quote(line)}" catalog += f"{parse.quote(line)}"
else: else:
for shop in shopList: for shop in shopList:
@@ -153,8 +161,8 @@ class DivaBase:
+ "," + ","
+ str(shop["type"]) + str(shop["type"])
) )
line = urllib.parse.quote(line) + "," line = parse.quote(line) + ","
catalog += f"{urllib.parse.quote(line)}" catalog += f"{parse.quote(line)}"
catalog = catalog.replace("+", "%20") catalog = catalog.replace("+", "%20")
@@ -198,8 +206,8 @@ class DivaBase:
with open(r"titles/diva/data/ItemCatalog.dat", encoding="utf-8") as item: with open(r"titles/diva/data/ItemCatalog.dat", encoding="utf-8") as item:
lines = item.readlines() lines = item.readlines()
for line in lines: for line in lines:
line = urllib.parse.quote(line) + "," line = parse.quote(line) + ","
catalog += f"{urllib.parse.quote(line)}" catalog += f"{parse.quote(line)}"
else: else:
for item in itemList: for item in itemList:
@@ -218,8 +226,8 @@ class DivaBase:
+ "," + ","
+ str(item["type"]) + str(item["type"])
) )
line = urllib.parse.quote(line) + "," line = parse.quote(line) + ","
catalog += f"{urllib.parse.quote(line)}" catalog += f"{parse.quote(line)}"
catalog = catalog.replace("+", "%20") catalog = catalog.replace("+", "%20")
@@ -264,11 +272,6 @@ class DivaBase:
return response return response
async def handle_festa_info_request(self, data: Dict) -> Dict: async def handle_festa_info_request(self, data: Dict) -> Dict:
if self.game_config.server.festa_enable:
festa_end_time = self.game_config.server.festa_end_time
else:
festa_end_time = (datetime.datetime.now() - datetime.timedelta(days=365)).strftime("%Y-%m-%d %H:%M:%S") + ".0"
encoded = "&" encoded = "&"
params = { params = {
"fi_id": "1,2", "fi_id": "1,2",
@@ -278,14 +281,14 @@ class DivaBase:
"fi_difficulty": "-1,-1", "fi_difficulty": "-1,-1",
"fi_pv_id_lst": "ALL,ALL", "fi_pv_id_lst": "ALL,ALL",
"fi_attr": "7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", "fi_attr": "7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
"fi_add_vp": f"{self.game_config.server.festa_add_VP}", "fi_add_vp": "20,5",
"fi_mul_vp": f"{self.game_config.server.festa_multiply_VP}", "fi_mul_vp": "1,2",
"fi_st": "2019-01-01 00:00:00.0,2019-01-01 00:00:00.0", "fi_st": "2019-01-01 00:00:00.0,2019-01-01 00:00:00.0",
"fi_et": f"{festa_end_time},{festa_end_time}", "fi_et": "2029-01-01 00:00:00.0,2029-01-01 00:00:00.0", # TODO: make this last longer
"fi_lut": "{self.time_lut}", "fi_lut": f"{self.time_lut}",
} }
encoded += urllib.parse.urlencode(params) encoded += parse.urlencode(params)
encoded = encoded.replace("+", "%20") encoded = encoded.replace("+", "%20")
encoded = encoded.replace("%2C", ",") encoded = encoded.replace("%2C", ",")
@@ -307,7 +310,7 @@ class DivaBase:
with open(r"titles/diva/data/QuestInfo.dat", encoding="utf-8") as shop: with open(r"titles/diva/data/QuestInfo.dat", encoding="utf-8") as shop:
lines = shop.readlines() lines = shop.readlines()
for line in lines: for line in lines:
quest += f"{urllib.parse.quote(line)}," quest += f"{parse.quote(line)},"
response = "" response = ""
response += f"&qi_lut={self.time_lut}" response += f"&qi_lut={self.time_lut}"
@@ -335,7 +338,7 @@ class DivaBase:
+ "," + ","
+ str(quests["quest_enable"]) + str(quests["quest_enable"])
) )
quest += f"{urllib.parse.quote(line)}%0A," quest += f"{parse.quote(line)}%0A,"
responseline = f"{quest[:-1]}," responseline = f"{quest[:-1]},"
for i in range(len(questList), 59): for i in range(len(questList), 59):
@@ -385,130 +388,72 @@ class DivaBase:
async def handle_pstd_item_ng_lst_request(self, data: Dict) -> Dict: async def handle_pstd_item_ng_lst_request(self, data: Dict) -> Dict:
return f"" return f""
async def handle_pre_start_request(self, data: Dict) -> str: async def handle_pre_start_request(self, data: bytes) -> str:
profile = await self.data.profile.get_profile(data["aime_id"], self.version) req = PreStartRequest(data)
profile_shop = await self.data.item.get_shop(data["aime_id"], self.version) resp = PreStartResponse(req.req_id, req.aime_id)
profile = await self.data.profile.get_profile(req.aime_id, self.version)
profile_shop = await self.data.item.get_shop(req.aime_id, self.version)
if profile is None: if profile is None:
return f"&ps_result=-3" return f"&ps_result=-3"
else:
response = "&ps_result=1"
response += "&accept_idx=100"
response += "&nblss_ltt_stts=-1"
response += "&nblss_ltt_tckt=-1"
response += "&nblss_ltt_is_opn=-1"
response += f"&pd_id={data['aime_id']}"
response += f"&player_name={profile['player_name']}"
response += f"&sort_kind={profile['player_name']}"
response += f"&lv_efct_id={profile['lv_efct_id']}"
response += f"&lv_plt_id={profile['lv_plt_id']}"
response += f"&lv_str={profile['lv_str']}"
response += f"&lv_num={profile['lv_num']}"
response += f"&lv_pnt={profile['lv_pnt']}"
response += f"&vcld_pts={profile['vcld_pts']}"
response += f"&skn_eqp={profile['skn_eqp']}"
response += f"&btn_se_eqp={profile['btn_se_eqp']}"
response += f"&sld_se_eqp={profile['sld_se_eqp']}"
response += f"&chn_sld_se_eqp={profile['chn_sld_se_eqp']}"
response += f"&sldr_tch_se_eqp={profile['sldr_tch_se_eqp']}"
response += f"&passwd_stat={profile['passwd_stat']}"
# Store stuff to add to rework profile_dict = profile._asdict()
response += f"&mdl_eqp_tm={self.time_lut}" profile_dict.pop("id")
profile_dict.pop("user")
profile_dict.pop("version")
mdl_eqp_ary = "-999,-999,-999" for k, v in profile_dict.items():
if hasattr(resp, k):
setattr(resp, k, v)
# get the common_modules from the profile shop if profile_shop is not None and profile_shop:
if profile_shop: resp.mdl_eqp_ary = profile_shop["mdl_eqp_ary"]
mdl_eqp_ary = profile_shop["mdl_eqp_ary"]
response += f"&mdl_eqp_ary={mdl_eqp_ary}" return resp.make()
return response async def handle_registration_request(self, data: bytes) -> str:
req = RegisterRequest(data)
async def handle_registration_request(self, data: Dict) -> Dict: pd_id = await self.data.profile.create_profile(
await self.data.profile.create_profile( self.version, req.aime_id, req.player_name
self.version, data["aime_id"], data["player_name"]
) )
return f"&cd_adm_result=1&pd_id={data['aime_id']}" if pd_id is None:
return "&cd_adm_result=-1"
return RegisterResponse(req.req_id, req.aime_id).make()
async def handle_start_request(self, data: Dict) -> Dict: async def handle_start_request(self, data: bytes) -> str:
profile = await self.data.profile.get_profile(data["pd_id"], self.version) req = StartRequest(data)
profile_shop = await self.data.item.get_shop(data["pd_id"], self.version) profile = await self.data.profile.get_profile(req.pd_id, self.version)
profile_shop = await self.data.item.get_shop(req.pd_id, self.version)
if profile is None: if profile is None:
return {} return
resp = StartResponse(req.req_id, req.pd_id, profile['player_name'])
profile_dict = profile._asdict()
profile_dict.pop("id")
profile_dict.pop("user")
profile_dict.pop("version")
for k, v in profile_dict.items():
if hasattr(resp, k):
setattr(resp, k, v)
mdl_have = "F" * 250
# generate the mdl_have string if "unlock_all_modules" is disabled # generate the mdl_have string if "unlock_all_modules" is disabled
if not self.game_config.mods.unlock_all_modules: if not self.game_config.mods.unlock_all_modules:
mdl_have = await self.data.module.get_modules_have_string( resp.mdl_have = await self.data.module.get_modules_have_string(
data["pd_id"], self.version req.pd_id, self.version
) )
cstmz_itm_have = "F" * 250
# generate the cstmz_itm_have string if "unlock_all_items" is disabled # generate the cstmz_itm_have string if "unlock_all_items" is disabled
if not self.game_config.mods.unlock_all_items: if not self.game_config.mods.unlock_all_items:
cstmz_itm_have = await self.data.customize.get_customize_items_have_string( resp.cstmz_itm_have = await self.data.customize.get_customize_items_have_string(
data["pd_id"], self.version req.pd_id, self.version
) )
response = f"&pd_id={data['pd_id']}"
response += "&start_result=1"
response += "&accept_idx=100"
response += f"&hp_vol={profile['hp_vol']}"
response += f"&btn_se_vol={profile['btn_se_vol']}"
response += f"&btn_se_vol2={profile['btn_se_vol2']}"
response += f"&sldr_se_vol2={profile['sldr_se_vol2']}"
response += f"&sort_kind={profile['sort_kind']}"
response += f"&player_name={profile['player_name']}"
response += f"&lv_num={profile['lv_num']}"
response += f"&lv_pnt={profile['lv_pnt']}"
response += f"&lv_efct_id={profile['lv_efct_id']}"
response += f"&lv_plt_id={profile['lv_plt_id']}"
response += f"&mdl_have={mdl_have}"
response += f"&cstmz_itm_have={cstmz_itm_have}"
response += f"&use_pv_mdl_eqp={int(profile['use_pv_mdl_eqp'])}"
response += f"&use_mdl_pri={int(profile['use_mdl_pri'])}"
response += f"&use_pv_skn_eqp={int(profile['use_pv_skn_eqp'])}"
response += f"&use_pv_btn_se_eqp={int(profile['use_pv_btn_se_eqp'])}"
response += f"&use_pv_sld_se_eqp={int(profile['use_pv_sld_se_eqp'])}"
response += f"&use_pv_chn_sld_se_eqp={int(profile['use_pv_chn_sld_se_eqp'])}"
response += f"&use_pv_sldr_tch_se_eqp={int(profile['use_pv_sldr_tch_se_eqp'])}"
response += f"&vcld_pts={profile['lv_efct_id']}"
response += f"&nxt_pv_id={profile['nxt_pv_id']}"
response += f"&nxt_dffclty={profile['nxt_dffclty']}"
response += f"&nxt_edtn={profile['nxt_edtn']}"
response += f"&dsp_clr_brdr={profile['dsp_clr_brdr']}"
response += f"&dsp_intrm_rnk={profile['dsp_intrm_rnk']}"
response += f"&dsp_clr_sts={profile['dsp_clr_sts']}"
response += f"&rgo_sts={profile['rgo_sts']}"
# Contest progress
response += f"&cv_cid=-1,-1,-1,-1"
response += f"&cv_sc=-1,-1,-1,-1"
response += f"&cv_bv=-1,-1,-1,-1"
response += f"&cv_bv=-1,-1,-1,-1"
response += f"&cv_bf=-1,-1,-1,-1"
# Contest now playing id, return -1 if no current playing contest
response += f"&cnp_cid={profile['cnp_cid']}"
response += f"&cnp_val={profile['cnp_val']}"
# border can be 0=bronzem 1=silver, 2=gold
response += f"&cnp_rr={profile['cnp_rr']}"
# only show contest specifier if it is not empty
response += f"&cnp_sp={profile['cnp_sp']}" if profile["cnp_sp"] != "" else ""
# To be fully fixed # To be fully fixed
if "my_qst_id" not in profile: if "my_qst_id" in profile:
response += f"&my_qst_id=-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1" resp.my_qst_id = profile['my_qst_id']
response += f"&my_qst_sts=0,0,0,0,0,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1" resp.my_qst_sts = profile['my_qst_sts']
else:
response += f"&my_qst_id={profile['my_qst_id']}"
response += f"&my_qst_sts={profile['my_qst_sts']}"
response += f"&my_qst_prgrs=0,0,0,0,0,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1"
response += f"&my_qst_et=2022-06-19%2010%3A28%3A52.0,2022-06-19%2010%3A28%3A52.0,2022-06-19%2010%3A28%3A52.0,2100-01-01%2008%3A59%3A59.0,2100-01-01%2008%3A59%3A59.0,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx"
# define a helper class to store all counts for clear, great, # define a helper class to store all counts for clear, great,
# excellent and perfect # excellent and perfect
@@ -529,7 +474,7 @@ class DivaBase:
} }
# get clear status from user scores # get clear status from user scores
pv_records = await self.data.score.get_best_scores(data["pd_id"]) pv_records = await self.data.score.get_best_scores(req.pd_id)
clear_status = "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0" clear_status = "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0"
if pv_records is not None: if pv_records is not None:
@@ -565,46 +510,33 @@ class DivaBase:
clear_status = ",".join(map(str, clear_list)) clear_status = ",".join(map(str, clear_list))
response += f"&clr_sts={clear_status}" resp.clr_sts = clear_status
# Store stuff to add to rework
response += f"&mdl_eqp_tm={self.time_lut}"
mdl_eqp_ary = "-999,-999,-999"
c_itm_eqp_ary = "-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999,-999"
ms_itm_flg_ary = "1,1,1,1,1,1,1,1,1,1,1,1"
# get the common_modules, customize_items and customize_item_flags # get the common_modules, customize_items and customize_item_flags
# from the profile shop # from the profile shop
if profile_shop: if profile_shop:
mdl_eqp_ary = profile_shop["mdl_eqp_ary"] resp.mdl_eqp_ary = profile_shop["mdl_eqp_ary"]
c_itm_eqp_ary = profile_shop["c_itm_eqp_ary"] resp.c_itm_eqp_ary = profile_shop["c_itm_eqp_ary"]
ms_itm_flg_ary = profile_shop["ms_itm_flg_ary"] resp.ms_itm_flg_ary = profile_shop["ms_itm_flg_ary"]
response += f"&mdl_eqp_ary={mdl_eqp_ary}" return resp.make()
response += f"&c_itm_eqp_ary={c_itm_eqp_ary}"
response += f"&ms_itm_flg_ary={ms_itm_flg_ary}"
return response async def handle_pd_unlock_request(self, data: bytes) -> str:
pass
async def handle_pd_unlock_request(self, data: Dict) -> Dict: async def handle_spend_credit_request(self, data: bytes) -> str:
return f"" req = SpendCreditRequest(data)
profile = await self.data.profile.get_profile(req.pd_id, self.version)
async def handle_spend_credit_request(self, data: Dict) -> Dict:
profile = await self.data.profile.get_profile(data["pd_id"], self.version)
if profile is None: if profile is None:
return return
response = "" resp = SpendCreditResponse(req.req_id)
resp.vcld_pts = profile['vcld_pts']
resp.lv_str = profile['lv_str']
resp.lv_efct_id = profile['lv_efct_id']
resp.lv_plt_id = profile['lv_plt_id']
response += "&cmpgn_rslt=-1,-1,x,-1,-1,x,x,-1,x,-1,-1,x,-1,-1,x,x,-1,x,-1,-1,x,-1,-1,x,x,-1,x,-1,-1,x,-1,-1,x,x,-1,x,-1,-1,x,-1,-1,x,x,-1,x,-1,-1,x,-1,-1,x,x,-1,x" return resp.make()
response += "&cmpgn_rslt_num=0"
response += f"&vcld_pts={profile['vcld_pts']}"
response += f"&lv_str={profile['lv_str']}"
response += f"&lv_efct_id={profile['lv_efct_id']}"
response += f"&lv_plt_id={profile['lv_plt_id']}"
return response
def _get_pv_pd_result( def _get_pv_pd_result(
self, self,
@@ -668,31 +600,31 @@ class DivaBase:
return pv_result return pv_result
async def task_generateScoreData(self, data: Dict, pd_by_pv_id, song): async def task_generateScoreData(self, pd_id: int, difficulty: int, pd_by_pv_id: str, song: int):
if int(song) > 0: if int(song) > 0:
# the request do not send a edition so just perform a query best score and ranking for each edition. # the request do not send a edition so just perform a query best score and ranking for each edition.
# 0=ORIGINAL, 1=EXTRA # 0=ORIGINAL, 1=EXTRA
pd_db_song_0 = await self.data.score.get_best_user_score( pd_db_song_0 = await self.data.score.get_best_user_score(
data["pd_id"], int(song), data["difficulty"], edition=0 pd_id, int(song), difficulty, edition=0
) )
pd_db_song_1 = await self.data.score.get_best_user_score( pd_db_song_1 = await self.data.score.get_best_user_score(
data["pd_id"], int(song), data["difficulty"], edition=1 pd_id, int(song), difficulty, edition=1
) )
pd_db_ranking_0, pd_db_ranking_1 = None, None pd_db_ranking_0, pd_db_ranking_1 = None, None
if pd_db_song_0: if pd_db_song_0:
pd_db_ranking_0 = await self.data.score.get_global_ranking( pd_db_ranking_0 = await self.data.score.get_global_ranking(
data["pd_id"], int(song), data["difficulty"], edition=0 pd_id, int(song), difficulty, edition=0
) )
if pd_db_song_1: if pd_db_song_1:
pd_db_ranking_1 = await self.data.score.get_global_ranking( pd_db_ranking_1 = await self.data.score.get_global_ranking(
data["pd_id"], int(song), data["difficulty"], edition=1 pd_id, int(song), difficulty, edition=1
) )
pd_db_customize = await self.data.pv_customize.get_pv_customize( pd_db_customize = await self.data.pv_customize.get_pv_customize(
data["pd_id"], int(song) pd_id, int(song)
) )
# generate the pv_result string with the ORIGINAL edition and the EXTRA edition appended # generate the pv_result string with the ORIGINAL edition and the EXTRA edition appended
@@ -710,14 +642,14 @@ class DivaBase:
pd_by_pv_id.append(",") pd_by_pv_id.append(",")
async def handle_get_pv_pd_request(self, data: Dict) -> Dict: async def handle_get_pv_pd_request(self, data: Dict) -> Dict:
song_id = data["pd_pv_id_lst"].split(",") req = GetPvPdRequest(data)
pv = "" pv = ""
threads = [] threads = []
pd_by_pv_id = [] pd_by_pv_id = []
for song in song_id: for song in req.pd_pv_id_lst:
thread_ScoreData = Thread(target=await self.task_generateScoreData(data, pd_by_pv_id, song)) thread_ScoreData = Thread(target=await self.task_generateScoreData(req.pd_id, req.difficulty, pd_by_pv_id, song))
threads.append(thread_ScoreData) threads.append(thread_ScoreData)
for x in threads: for x in threads:
@@ -729,44 +661,48 @@ class DivaBase:
for x in pd_by_pv_id: for x in pd_by_pv_id:
pv += x pv += x
resp = GetPvPdResponse(req.req_id)
resp.pd_by_pv_id = pv[:-1]
response = "" response = ""
response += f"&pd_by_pv_id={pv[:-1]}" response += f"&pd_by_pv_id={pv[:-1]}"
response += "&pdddt_flg=0" response += "&pdddt_flg=0"
response += f"&pdddt_tm={self.time_lut}" response += f"&pdddt_tm={self.time_lut}"
return response return resp.make()
async def handle_stage_start_request(self, data: Dict) -> Dict: async def handle_stage_start_request(self, data: Dict) -> Dict:
return f"" return f""
async def handle_stage_result_request(self, data: Dict) -> Dict: async def handle_stage_result_request(self, data: bytes) -> str:
profile = await self.data.profile.get_profile(data["pd_id"], self.version) req = StageResultRequest(data)
resp = StageResultResponse(req.cmd, req.req_id)
profile = await self.data.profile.get_profile(req.pd_id, self.version)
pd_song_list = data["stg_ply_pv_id"].split(",") pd_song_list = req.stg_ply_pv_id
pd_song_difficulty = data["stg_difficulty"].split(",") pd_song_difficulty = req.stg_difficulty
pd_song_edition = data["stg_edtn"].split(",") pd_song_edition = req.stg_edtn
pd_song_max_score = data["stg_score"].split(",") pd_song_max_score = req.stg_score
pd_song_max_atn_pnt = data["stg_atn_pnt"].split(",") pd_song_max_atn_pnt = req.stg_atn_pnt
pd_song_ranking = data["stg_clr_kind"].split(",") pd_song_ranking = req.stg_clr_kind
pd_song_sort_kind = data["sort_kind"] pd_song_sort_kind = req.sort_kind
pd_song_cool_cnt = data["stg_cool_cnt"].split(",") pd_song_cool_cnt = req.stg_cool_cnt
pd_song_fine_cnt = data["stg_fine_cnt"].split(",") pd_song_fine_cnt = req.stg_fine_cnt
pd_song_safe_cnt = data["stg_safe_cnt"].split(",") pd_song_safe_cnt = req.stg_safe_cnt
pd_song_sad_cnt = data["stg_sad_cnt"].split(",") pd_song_sad_cnt = req.stg_sad_cnt
pd_song_worst_cnt = data["stg_wt_wg_cnt"].split(",") pd_song_worst_cnt = req.stg_wt_wg_cnt
pd_song_max_combo = data["stg_max_cmb"].split(",") pd_song_max_combo = req.stg_max_cmb
for index, value in enumerate(pd_song_list): for index, value in enumerate(pd_song_list):
if "-1" not in pd_song_list[index]: if pd_song_list[index] > 0:
profile_pd_db_song = await self.data.score.get_best_user_score( profile_pd_db_song = await self.data.score.get_best_user_score(
data["pd_id"], req.pd_id,
pd_song_list[index], pd_song_list[index],
pd_song_difficulty[index], pd_song_difficulty[index],
pd_song_edition[index], pd_song_edition[index],
) )
if profile_pd_db_song is None: if profile_pd_db_song is None:
await self.data.score.put_best_score( await self.data.score.put_best_score(
data["pd_id"], req.pd_id,
self.version, self.version,
pd_song_list[index], pd_song_list[index],
pd_song_difficulty[index], pd_song_difficulty[index],
@@ -783,7 +719,7 @@ class DivaBase:
pd_song_max_combo[index], pd_song_max_combo[index],
) )
await self.data.score.put_playlog( await self.data.score.put_playlog(
data["pd_id"], req.pd_id,
self.version, self.version,
pd_song_list[index], pd_song_list[index],
pd_song_difficulty[index], pd_song_difficulty[index],
@@ -801,7 +737,7 @@ class DivaBase:
) )
elif int(pd_song_max_score[index]) >= int(profile_pd_db_song["score"]): elif int(pd_song_max_score[index]) >= int(profile_pd_db_song["score"]):
await self.data.score.put_best_score( await self.data.score.put_best_score(
data["pd_id"], req.pd_id,
self.version, self.version,
pd_song_list[index], pd_song_list[index],
pd_song_difficulty[index], pd_song_difficulty[index],
@@ -818,7 +754,7 @@ class DivaBase:
pd_song_max_combo[index], pd_song_max_combo[index],
) )
await self.data.score.put_playlog( await self.data.score.put_playlog(
data["pd_id"], req.pd_id,
self.version, self.version,
pd_song_list[index], pd_song_list[index],
pd_song_difficulty[index], pd_song_difficulty[index],
@@ -836,7 +772,7 @@ class DivaBase:
) )
elif int(pd_song_max_score[index]) != int(profile_pd_db_song["score"]): elif int(pd_song_max_score[index]) != int(profile_pd_db_song["score"]):
await self.data.score.put_playlog( await self.data.score.put_playlog(
data["pd_id"], req.pd_id,
self.version, self.version,
pd_song_list[index], pd_song_list[index],
pd_song_difficulty[index], pd_song_difficulty[index],
@@ -865,6 +801,20 @@ class DivaBase:
new_level = (total_atn_pnt // 13979) + 1 new_level = (total_atn_pnt // 13979) + 1
new_level_pnt = round((total_atn_pnt % 13979) / 13979 * 100) new_level_pnt = round((total_atn_pnt % 13979) / 13979 * 100)
resp.lv_num_old = int(profile['lv_num'])
resp.lv_pnt_old = int(profile['lv_pnt'])
resp.lv_num = new_level
resp.lv_str = profile['lv_str']
resp.lv_pnt = new_level_pnt
resp.lv_efct_id = int(profile['lv_efct_id'])
resp.lv_plt_id = int(profile['lv_plt_id'])
resp.vcld_pts = int(profile['vcld_pts'])
resp.prsnt_vcld_pts = int(profile['vcld_pts'])
if "my_qst_id" not in profile:
quests = profile['my_qst_id'].split(",")
for x in range(len(quests)):
resp.my_qst_id[x] = int(quests[x])
response = "&chllng_kind=-1" response = "&chllng_kind=-1"
response += f"&lv_num_old={int(profile['lv_num'])}" response += f"&lv_num_old={int(profile['lv_num'])}"
response += f"&lv_pnt_old={int(profile['lv_pnt'])}" response += f"&lv_pnt_old={int(profile['lv_pnt'])}"
@@ -874,16 +824,16 @@ class DivaBase:
profile["user"], profile["user"],
lv_num=new_level, lv_num=new_level,
lv_pnt=new_level_pnt, lv_pnt=new_level_pnt,
vcld_pts=int(data["vcld_pts"]), vcld_pts=req.vcld_pts,
hp_vol=int(data["hp_vol"]), hp_vol=req.hp_vol,
btn_se_vol=int(data["btn_se_vol"]), btn_se_vol=req.btn_se_vol,
sldr_se_vol2=int(data["sldr_se_vol2"]), sldr_se_vol2=req.sldr_se_vol2,
sort_kind=int(data["sort_kind"]), sort_kind=req.sort_kind,
nxt_pv_id=int(data["ply_pv_id"]), nxt_pv_id=req.ply_pv_id,
nxt_dffclty=int(data["nxt_dffclty"]), nxt_dffclty=req.nxt_dffclty,
nxt_edtn=int(data["nxt_edtn"]), nxt_edtn=req.nxt_edtn,
my_qst_id=data["my_qst_id"], my_qst_id=req.my_qst_id,
my_qst_sts=data["my_qst_sts"], my_qst_sts=req.my_qst_sts,
) )
response += f"&lv_num={new_level}" response += f"&lv_num={new_level}"
@@ -916,17 +866,18 @@ class DivaBase:
response += "&my_ccd_r_hnd=-1,-1,-1,-1,-1" response += "&my_ccd_r_hnd=-1,-1,-1,-1,-1"
response += "&my_ccd_r_vp=-1,-1,-1,-1,-1" response += "&my_ccd_r_vp=-1,-1,-1,-1,-1"
return response return resp.make()
async def handle_end_request(self, data: Dict) -> Dict: async def handle_end_request(self, data: bytes) -> str:
profile = await self.data.profile.get_profile(data["pd_id"], self.version) req = EndRequest(data)
profile = await self.data.profile.get_profile(req.pd_id, self.version)
await self.data.profile.update_profile( await self.data.profile.update_profile(
profile["user"], my_qst_id=data["my_qst_id"], my_qst_sts=data["my_qst_sts"] profile["user"], my_qst_id=req.my_qst_id, my_qst_sts=req.my_qst_sts
) )
return f"" return None
async def handle_shop_exit_request(self, data: Dict) -> Dict: async def handle_shop_exit_request(self, data: bytes) -> str:
await self.data.item.put_shop( await self.data.item.put_shop(
data["pd_id"], data["pd_id"],
self.version, self.version,

View File

@@ -18,30 +18,13 @@ class DivaServerConfig:
self.__config, "diva", "server", "loglevel", default="info" self.__config, "diva", "server", "loglevel", default="info"
) )
) )
@property
def banner_msg(self) -> str:
CoreConfig.get_config_field(
self.__config, "diva", "server", "banner_msg", default=""
)
@property
def festa_enable(self) -> bool:
return CoreConfig.get_config_field(
self.__config, "diva", "server", "festa_enable", default=True
)
@property
def festa_add_VP(self) -> str:
return CoreConfig.get_config_field(
self.__config, "diva", "server", "festa_add_VP", default="20,5"
)
@property
def festa_multiply_VP(self) -> str:
return CoreConfig.get_config_field(
self.__config, "diva", "server", "festa_multiply_VP", default="1,2"
)
@property
def festa_end_time(self) -> str:
return CoreConfig.get_config_field(
self.__config, "diva", "server", "festa_end_time", default="2029-01-01 00:00:00.0"
)
class DivaModsConfig: class DivaModsConfig:
def __init__(self, parent_config: "DivaConfig") -> None: def __init__(self, parent_config: "DivaConfig") -> None:

View File

@@ -6,6 +6,8 @@ class DivaConstants:
VER_PROJECT_DIVA_ARCADE = 0 VER_PROJECT_DIVA_ARCADE = 0
VER_PROJECT_DIVA_ARCADE_FUTURE_TONE = 1 VER_PROJECT_DIVA_ARCADE_FUTURE_TONE = 1
LUT_TIME_FMT = "%Y-%m-%d %H:%M:%S:16.0"
VERSION_NAMES = ("Project Diva Arcade", "Project Diva Arcade Future Tone") VERSION_NAMES = ("Project Diva Arcade", "Project Diva Arcade Future Tone")
@classmethod @classmethod

View File

@@ -1,182 +0,0 @@
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 = "Project 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

@@ -0,0 +1,3 @@
from .base import *
from .user import *
from .pv import *

View File

@@ -0,0 +1,275 @@
from urllib import parse
from urllib.parse import quote
from datetime import datetime
from typing import Union, Dict, List, Any
from ..const import DivaConstants
def lazy_http_form_parse(src: Union[str, bytes]) -> Dict[bytes, bytes]:
out = {}
if type(src) == str:
src = src.encode()
for param in src.split(b"&"):
kvp = param.split(b"=")
out[parse.unquote(kvp[0])] = parse.unquote(kvp[1])
return out
class DivaRequestParseException(Exception):
"""
Exception raised when there is a fault in parsing a diva request,
either due to a malformed request, or missing required items
"""
def __init__(self, message: str) -> None:
self.message = message
super().__init__(self.message)
class BaseBinaryRequest:
cmd: str
req_id: str
def __init__(self, raw: bytes) -> None:
self.raw = raw
self.raw_dict = dict(parse.parse_qsl(raw))
if "cmd" not in self.raw_dict:
raise DivaRequestParseException(f"cmd not in request data {self.raw_dict}")
if "req_id" not in self.raw_dict:
raise DivaRequestParseException(
f"req_id not in request data {self.raw_dict}"
)
for k, v in self.raw_dict.items():
setattr(self, k, v)
class BaseRequest:
def __init__(self, raw: Union[str, bytes]) -> None:
self.raw = raw
try:
self.raw_dict: Dict[str, str] = lazy_http_form_parse(raw)
except UnicodeDecodeError as e:
raise DivaRequestParseException(f"Could not decode data {raw} - {e}")
if "cmd" not in self.raw_dict:
raise DivaRequestParseException(f"cmd not in request data {self.raw_dict}")
if "req_id" not in self.raw_dict:
raise DivaRequestParseException(
f"req_id not in request data {self.raw_dict}"
)
if "place_id" not in self.raw_dict:
raise DivaRequestParseException(
f"place_id not in request data {self.raw_dict}"
)
if "start_up_mode" not in self.raw_dict:
raise DivaRequestParseException(
f"start_up_mode not in request data {self.raw_dict}"
)
if "cmm_dly_mod" not in self.raw_dict:
raise DivaRequestParseException(
f"cmm_dly_mod not in request data {self.raw_dict}"
)
if "cmm_dly_sec" not in self.raw_dict:
raise DivaRequestParseException(
f"cmm_dly_sec not in request data {self.raw_dict}"
)
if "cmm_err_mod" not in self.raw_dict:
raise DivaRequestParseException(
f"cmm_err_mod not in request data {self.raw_dict}"
)
if "region_code" not in self.raw_dict:
raise DivaRequestParseException(
f"region_code not in request data {self.raw_dict}"
)
if "time_stamp" not in self.raw_dict:
raise DivaRequestParseException(
f"time_stamp not in request data {self.raw_dict}"
)
self.cmd: str = self.raw_dict.get('cmd')
self.req_id: str = self.raw_dict.get('req_id')
self.game_id: str = self.raw_dict.get('game_id')
self.r_rev: str = self.raw_dict.get('r_rev')
self.kc_serial: str = self.raw_dict.get('kc_serial')
self.b_serial: str = self.raw_dict.get('b_serial')
self.country_code: str = self.raw_dict.get('country_code')
self.place_id = int(self.raw_dict['place_id'], 16)
self.start_up_mode = int(self.raw_dict.get('start_up_mode'))
self.cmm_dly_mod = int(self.raw_dict.get('cmm_dly_mod'))
self.cmm_dly_sec = int(self.raw_dict.get('cmm_dly_sec'))
self.cmm_err_mod = int(self.raw_dict.get('cmm_err_mod'))
self.region_code = int(self.raw_dict.get('region_code'))
# datetime.now().astimezone().replace(microsecond=0).isoformat()
self.time_stamp = datetime.strptime(self.raw_dict.get('time_stamp'), "%Y-%m-%dT%H:%M:%S%z")
class BaseResponse:
def __init__(self, cmd_id: str, req_id: int) -> None:
self.cmd = cmd_id
self.req_id = req_id
self.stat = "ok"
def make(self) -> str:
itms: List[str] = []
for k, v in vars(self).items():
if type(v) == int:
itms.append(encode_int(k, v))
elif type(v) == bool:
itms.append(encode_bool(k, v))
elif type(v) == datetime:
itms.append(encode_date(k, v))
elif type(v) == list:
itms.append(encode_list(k, v))
else:
itms.append(encode_str(k, v))
return "&".join(itms)
class GameInitRequest(BaseRequest):
def __init__(self, raw: Union[str, bytes]) -> None:
super().__init__(raw)
class AttendRequest(BaseRequest):
def __init__(self, raw: Union[str, bytes]) -> None:
super().__init__(raw)
if 'power_on' not in self.raw_dict:
raise DivaRequestParseException(
f"power_on not in request data {self.raw_dict}"
)
if 'is_bb' not in self.raw_dict:
raise DivaRequestParseException(
f"is_bb not in request data {self.raw_dict}"
)
self.power_on = int(self.raw_dict.get('power_on'))
self.is_bb = bool(int(self.raw_dict.get('is_bb')))
class AttendResponse(BaseResponse):
def __init__(self, req_id: int) -> None:
super().__init__("attend", req_id)
self.atnd_prm1 = [1] * 100
self.atnd_prm2 = [1] * 100
self.atnd_prm3 = [1] * 100
self.atnd_lut = datetime.now()
def make(self) -> str:
return quote(super().make(), safe=",&=")
class SpendCreditRequest(BaseRequest):
def __init__(self, raw: Union[str, bytes]) -> None:
super().__init__(raw)
if 'pd_id' not in self.raw_dict:
raise DivaRequestParseException(
f"pd_id not in request data {self.raw_dict}"
)
if 'my_qst_id' not in self.raw_dict:
raise DivaRequestParseException(
f"my_qst_id not in request data {self.raw_dict}"
)
if 'my_qst_sts' not in self.raw_dict:
raise DivaRequestParseException(
f"my_qst_sts not in request data {self.raw_dict}"
)
if 'crdt_typ' not in self.raw_dict:
raise DivaRequestParseException(
f"crdt_typ not in request data {self.raw_dict}"
)
if 'cmpgn_id' not in self.raw_dict:
raise DivaRequestParseException(
f"cmpgn_id not in request data {self.raw_dict}"
)
if 'cmpgn_pb' not in self.raw_dict:
raise DivaRequestParseException(
f"cmpgn_pb not in request data {self.raw_dict}"
)
self.pd_id = int(self.raw_dict.get('pd_id'))
self.my_qst_id = decode_list_int(self.raw_dict.get('my_qst_id'))
self.my_qst_sts = decode_list_int(self.raw_dict.get('my_qst_sts'))
self.crdt_typ = int(self.raw_dict.get('crdt_typ'))
self.cmpgn_id = decode_list_int(self.raw_dict.get('cmpgn_id'))
self.cmpgn_pb = decode_list_int(self.raw_dict.get('cmpgn_pb'))
class SpendCreditResponse(BaseResponse):
def __init__(self, req_id: int) -> None:
super().__init__("spend_credit", req_id)
self.cmpgn_rslt = ",".join(["-1,-1,x,-1,-1,x,x,-1,x"] * 6)
self.cmpgn_rslt_num = 0
self.vcld_pts = 0
self.lv_str = ""
self.lv_efct_id = 0
self.lv_plt_id = 0
def encode_int(key: str, val: Union[int, None] = None) -> str:
if type(val) != int:
val = -1
return f"{key}={val}"
def encode_bool(key: str, val: Union[bool, None] = None) -> str:
if not val:
return encode_int(key, 0)
return encode_int(key, 1)
def encode_str(key: str, val: Union[str, None] = None, urlencode_val: bool = False) -> str:
if type(val) != str:
val = "xxx"
if urlencode_val:
val = quote(val)
return f"{key}={val}"
def encode_date(key: str, val: Union[datetime, None], urlencode_val: bool = True, fmt: str = DivaConstants.LUT_TIME_FMT) -> str:
if type(val) != datetime:
val = datetime.now().astimezone()
val = val.replace(microsecond=0)
dt_fmt = val.strftime(fmt)
if urlencode_val:
dt_fmt = quote(dt_fmt)
return f"{key}={dt_fmt}"
def encode_list(key: str, val: Union[List[Any], None], urlencode_final_val: bool = False, urlencode_vals: bool = False) -> str:
if not val:
return f"{key}="
for x in range(len(val)):
if val[x] is None:
val[x] = "x"
if type(val[x]) == datetime:
val[x] = val[x].replace(microsecond=0).strftime(DivaConstants.LUT_TIME_FMT)
elif type(val[x]) == bool:
val[x] = str(int(val[x]))
elif type(val[x]) == int:
val[x] = str(val[x])
if urlencode_vals:
val[x] = quote(val[x])
all_vals = ",".join(val)
if urlencode_final_val:
all_vals = quote(all_vals)
return f"{key}={all_vals}"
def decode_list_int(val: str) -> List[int]:
return [int(x) for x in val.split(",")]
def decode_list(val: str) -> List[str]:
return [x for x in val.split(",")]

156
titles/diva/handlers/pv.py Normal file
View File

@@ -0,0 +1,156 @@
from typing import Union, List
from titles.diva.handlers.base import (
BaseRequest,
BaseResponse,
DivaRequestParseException,
decode_list_int
)
from datetime import datetime
from urllib import parse
from ..const import DivaConstants
class GetPvPdRequest(BaseRequest):
def __init__(self, raw: Union[str, bytes]) -> None:
super().__init__(raw)
try:
self.pd_id = int(self.raw_dict.get('pd_id'))
self.accept_idx = int(self.raw_dict.get('accept_idx'))
self.start_idx = int(self.raw_dict.get('start_idx'))
self.difficulty = int(self.raw_dict.get('difficulty'))
self.pd_pv_id_lst: List[int] = [int(x) for x in self.pd_pv_id_lst.split(',')]
except AttributeError as e:
raise DivaRequestParseException(f"GetPvPdRequest: {e}")
class GetPvPdResponse(BaseResponse):
def __init__(self, req_id: int) -> None:
super().__init__("get_pv_pd", req_id)
self.pd_by_pv_id = ""
self.pdddt_flg = 0
self.pdddt_tm = parse.quote(datetime.now().strftime(DivaConstants.LUT_TIME_FMT))
class StageResultRequest(BaseRequest):
def __init__(self, raw: Union[str, bytes]) -> None:
super().__init__(raw)
self.pd_id = int(self.raw_dict.get('pd_id'))
self.accept_idx = int(self.raw_dict.get('accept_idx'))
self.start_idx = int(self.raw_dict.get('start_idx'))
self.hp_vol = int(self.raw_dict.get('hp_vol'))
self.btn_se_vol = int(self.raw_dict.get('btn_se_vol'))
self.btn_se_vol2 = int(self.raw_dict.get('btn_se_vol2'))
self.sldr_se_vol2 = int(self.raw_dict.get('sldr_se_vol2'))
self.use_pv_mdl_eqp = int(self.raw_dict.get('use_pv_mdl_eqp'))
self.vcld_pts = int(self.raw_dict.get('vcld_pts'))
self.nxt_pv_id = int(self.raw_dict.get('nxt_pv_id'))
self.nxt_dffclty = int(self.raw_dict.get('nxt_dffclty'))
self.nxt_edtn = int(self.raw_dict.get('nxt_edtn'))
self.sort_kind = int(self.raw_dict.get('sort_kind'))
self.nblss_ltt_stts = int(self.raw_dict.get('nblss_ltt_stts'))
self.nblss_ltt_tckt = int(self.raw_dict.get('nblss_ltt_tckt'))
self.free_play = int(self.raw_dict.get('free_play'))
self.game_type = int(self.raw_dict.get('game_type'))
self.ply_pv_id = int(self.raw_dict.get('ply_pv_id'))
self.ttl_vp_add = int(self.raw_dict.get('ttl_vp_add'))
self.ttl_vp_sub = int(self.raw_dict.get('ttl_vp_sub'))
self.continue_cnt = int(self.raw_dict.get('continue_cnt'))
self.cr_cid = int(self.raw_dict.get('cr_cid'))
self.cr_sc = int(self.raw_dict.get('cr_sc'))
self.cr_tv = int(self.raw_dict.get('cr_tv'))
self.cr_if = int(self.raw_dict.get('cr_if'))
self.my_qst_id: List[int] = decode_list_int(self.raw_dict.get('my_qst_id'))
self.my_qst_sts: List[int] = decode_list_int(self.raw_dict.get('my_qst_sts'))
self.stg_difficulty: List[int] = decode_list_int(self.raw_dict.get('stg_difficulty'))
self.stg_edtn: List[int] = decode_list_int(self.raw_dict.get('stg_edtn'))
self.stg_ply_pv_id: List[int] = decode_list_int(self.raw_dict.get('stg_ply_pv_id'))
self.stg_sel_pv_id: List[int] = decode_list_int(self.raw_dict.get('stg_sel_pv_id'))
self.stg_scrpt_ver: List[int] = decode_list_int(self.raw_dict.get('stg_scrpt_ver'))
self.stg_score: List[int] = decode_list_int(self.raw_dict.get('stg_score'))
self.stg_chllng_kind: List[int] = decode_list_int(self.raw_dict.get('stg_chllng_kind'))
self.stg_chllng_result: List[int] = decode_list_int(self.raw_dict.get('stg_chllng_result'))
self.stg_clr_kind: List[int] = decode_list_int(self.raw_dict.get('stg_clr_kind'))
self.stg_vcld_pts: List[int] = decode_list_int(self.raw_dict.get('stg_vcld_pts'))
self.stg_cool_cnt: List[int] = decode_list_int(self.raw_dict.get('stg_cool_cnt'))
self.stg_cool_pct: List[int] = decode_list_int(self.raw_dict.get('stg_cool_pct'))
self.stg_fine_cnt: List[int] = decode_list_int(self.raw_dict.get('stg_fine_cnt'))
self.stg_fine_pct: List[int] = decode_list_int(self.raw_dict.get('stg_fine_pct'))
self.stg_safe_cnt: List[int] = decode_list_int(self.raw_dict.get('stg_safe_cnt'))
self.stg_safe_pct: List[int] = decode_list_int(self.raw_dict.get('stg_safe_pct'))
self.stg_sad_cnt: List[int] = decode_list_int(self.raw_dict.get('stg_sad_cnt'))
self.stg_sad_pct: List[int] = decode_list_int(self.raw_dict.get('stg_sad_pct'))
self.stg_wt_wg_cnt: List[int] = decode_list_int(self.raw_dict.get('stg_wt_wg_cnt'))
self.stg_wt_wg_pct: List[int] = decode_list_int(self.raw_dict.get('stg_wt_wg_pct'))
self.stg_max_cmb: List[int] = decode_list_int(self.raw_dict.get('stg_max_cmb'))
self.stg_chance_tm: List[int] = decode_list_int(self.raw_dict.get('stg_chance_tm'))
self.stg_sm_hl: List[int] = decode_list_int(self.raw_dict.get('stg_sm_hl'))
self.stg_atn_pnt: List[int] = decode_list_int(self.raw_dict.get('stg_atn_pnt'))
self.stg_skin_id: List[int] = decode_list_int(self.raw_dict.get('stg_skin_id'))
self.stg_btn_se: List[int] = decode_list_int(self.raw_dict.get('stg_btn_se'))
self.stg_btn_se_vol: List[int] = decode_list_int(self.raw_dict.get('stg_btn_se_vol'))
self.stg_sld_se: List[int] = decode_list_int(self.raw_dict.get('stg_sld_se'))
self.stg_chn_sld_se: List[int] = decode_list_int(self.raw_dict.get('stg_chn_sld_se'))
self.stg_sldr_tch_se: List[int] = decode_list_int(self.raw_dict.get('stg_sldr_tch_se'))
self.stg_mdl_id: List[int] = decode_list_int(self.raw_dict.get('stg_mdl_id'))
self.stg_sel_mdl_id: List[int] = decode_list_int(self.raw_dict.get('stg_sel_mdl_id'))
self.stg_rvl_pd_id: List[int] = decode_list_int(self.raw_dict.get('stg_rvl_pd_id'))
self.stg_rvl_wl: List[int] = decode_list_int(self.raw_dict.get('stg_rvl_wl'))
self.stg_cpt_rslt: List[int] = decode_list_int(self.raw_dict.get('stg_cpt_rslt'))
self.stg_sld_scr: List[int] = decode_list_int(self.raw_dict.get('stg_sld_scr'))
self.stg_is_sr_gm: List[int] = decode_list_int(self.raw_dict.get('stg_is_sr_gm'))
self.stg_pv_brnch_rslt: List[int] = decode_list_int(self.raw_dict.get('stg_pv_brnch_rslt'))
self.stg_vcl_chg: List[int] = decode_list_int(self.raw_dict.get('stg_vcl_chg'))
self.stg_c_itm_id: List[int] = decode_list_int(self.raw_dict.get('stg_c_itm_id'))
self.stg_ms_itm_flg: List[int] = decode_list_int(self.raw_dict.get('stg_ms_itm_flg'))
self.stg_rgo: List[int] = decode_list_int(self.raw_dict.get('stg_rgo'))
self.stg_ss_num: List[int] = decode_list_int(self.raw_dict.get('stg_ss_num'))
self.stg_is_cs_scs: List[int] = decode_list_int(self.raw_dict.get('stg_is_cs_scs'))
self.stg_is_nppg_use: List[int] = decode_list_int(self.raw_dict.get('stg_is_nppg_use'))
self.stg_p_std_lo_id: List[int] = decode_list_int(self.raw_dict.get('stg_p_std_lo_id'))
self.stg_p_std_is_to: List[int] = decode_list_int(self.raw_dict.get('stg_p_std_is_to'))
self.stg_p_std_is_ccu: List[int] = decode_list_int(self.raw_dict.get('stg_p_std_is_ccu'))
self.stg_p_std_is_tiu: List[int] = decode_list_int(self.raw_dict.get('stg_p_std_is_tiu'))
self.stg_p_std_is_iu: List[int] = decode_list_int(self.raw_dict.get('stg_p_std_is_iu'))
self.stg_p_std_is_npu: List[int] = decode_list_int(self.raw_dict.get('stg_p_std_is_npu'))
self.stg_p_std_is_du: List[int] = decode_list_int(self.raw_dict.get('stg_p_std_is_du'))
self.gu_cmd: List[int] = decode_list_int(self.raw_dict.get('gu_cmd'))
self.mdl_eqp_cmn_ary: List[int] = decode_list_int(self.raw_dict.get('mdl_eqp_cmn_ary'))
self.c_itm_eqp_cmn_ary: List[int] = decode_list_int(self.raw_dict.get('c_itm_eqp_cmn_ary'))
self.ms_itm_flg_cmn_ary: List[int] = decode_list_int(self.raw_dict.get('ms_itm_flg_cmn_ary'))
self.mdl_eqp_pv_ary: List[int] = decode_list_int(self.raw_dict.get('mdl_eqp_pv_ary'))
self.c_itm_eqp_pv_ary: List[int] = decode_list_int(self.raw_dict.get('c_itm_eqp_pv_ary'))
self.ms_itm_flg_pv_ary: List[int] = decode_list_int(self.raw_dict.get('ms_itm_flg_pv_ary'))
self.stg_mdl_s_sts: List[int] = decode_list_int(self.raw_dict.get('stg_mdl_s_sts'))
self.cr_sp: List[int] = decode_list_int(parse.unquote(self.raw_dict.get('cr_sp')))
class StageResultResponse(BaseResponse):
def __init__(self, req_id: int) -> None:
super().__init__("stage_result", req_id)
self.chllng_kind = -1
self.lv_num_old = 0
self.lv_pnt_old = 0
self.lv_num = 0
self.lv_str = 0
self.lv_pnt = 0
self.lv_efct_id = 0
self.lv_plt_id = 0
self.vcld_pts = 0
self.prsnt_vcld_pts = 0
self.cerwd_kind = -1
self.cerwd_value = -1
self.cerwd_str_0 = "***"
self.cerwd_str_1 = "***"
self.ttl_str_ary = ["xxx"] * 5
self.ttl_plt_id_ary = ["xxx"] * 5
self.ttl_desc_ary = ["xxx"] * 5
self.skin_id_ary = ["xxx"] * 5
self.skin_name_ary = ["xxx"] * 5
self.skin_illust_ary = ["xxx"] * 5
self.skin_desc_ary = ["xxx"] * 5
self.my_qst_id = [-1] * 25
self.my_qst_r_qid = [-1] * 25
self.my_qst_r_knd = [-1] * 25
self.my_qst_r_vl = [-1] * 25
self.my_qst_r_nflg = [-1] * 25
self.my_ccd_r_qid = [-1] * 5
self.my_ccd_r_hnd = [-1] * 5
self.my_ccd_r_vp = [-1] * 5

View File

@@ -0,0 +1,140 @@
from typing import Union
from titles.diva.handlers.base import (
BaseRequest,
BaseResponse,
DivaRequestParseException,
)
from datetime import datetime
from urllib import parse
from ..const import DivaConstants
class PreStartRequest(BaseRequest):
def __init__(self, raw: str) -> None:
super().__init__(raw)
self.pmm: str = self.raw_dict.get('pmm')
self.idm: str = self.raw_dict.get('idm')
self.mmgameid: str = self.raw_dict.get('mmgameid')
self.mmuid: str = self.raw_dict.get('mmuid')
self.a_code: str = self.raw_dict.get('a_code')
self.aime_id: str = self.raw_dict.get('aime_id')
self.aime_a_code: str = self.raw_dict.get('aime_a_code')
self.key_obj_type = int(self.raw_dict.get('key_obj_type'))
self.exec_vu = int(self.raw_dict.get('exec_vu'))
class PreStartResponse(BaseResponse):
def __init__(self, req_id: int, pd_id: int) -> None:
super().__init__("pre_start", req_id)
self.ps_result = 1
self.pd_id = pd_id
self.accept_idx = 100
self.nblss_ltt_stts = -1
self.nblss_ltt_tckt = -1
self.nblss_ltt_is_opn = -1
self.player_name: str = ""
self.sort_kind: str = ""
self.lv_efct_id: str = ""
self.lv_plt_id: str = ""
self.lv_str: str = ""
self.lv_num: str = ""
self.lv_pnt: str = ""
self.vcld_pts: str = ""
self.skn_eqp: str = ""
self.btn_se_eqp: str = ""
self.sld_se_eqp: str = ""
self.chn_sld_se_eqp: str = ""
self.sldr_tch_se_eqp: str = ""
self.passwd_stat: str = ""
self.mdl_eqp_tm: str = ""
# Ideally this would be a real array that would get converted later
# But this is how it's stored in the db, so w/e for now
self.mdl_eqp_ary = "-999,-999,-999"
class StartRequest(BaseRequest):
def __init__(self, raw: str) -> None:
super().__init__(raw)
self.pd_id = int(self.raw_dict.get('pd_id'))
self.accept_idx = int(self.raw_dict.get('accept_idx'))
class StartResponse(BaseResponse):
def __init__(self, req_id: int, pv_id: int, pv_name: str) -> None:
super().__init__("start", req_id)
self.pd_id: int = pv_id
self.start_result: int = 1
self.accept_idx: int = 100
self.hp_vol: int = 0
self.btn_se_vol: int = 1
self.btn_se_vol2: int = 1
self.sldr_se_vol2: int = 1
self.sort_kind: int = 1
self.player_name: str = pv_name
self.lv_num: int = 1
self.lv_pnt: int = 0
self.lv_efct_id: int = 1
self.lv_plt_id: int = 1
self.mdl_have: str = "F" * 250
self.cstmz_itm_have: str = "F" * 250
self.use_pv_mdl_eqp: int = 0
self.use_mdl_pri: int = 0
self.use_pv_skn_eqp: int = 1
self.use_pv_btn_se_eqp: int = 1
self.use_pv_sld_se_eqp: int = 1
self.use_pv_chn_sld_se_eqp: int = 1
self.use_pv_sldr_tch_se_eqp: int = 1
self.vcld_pts: int = 0
self.nxt_pv_id: int = 1
self.nxt_dffclty: int = 1
self.nxt_edtn: int = 0
self.dsp_clr_brdr: int = 0
self.dsp_intrm_rnk: int = 0
self.dsp_clr_sts: int = 0
self.rgo_sts: int = 0
self.cv_cid: str = "-1,-1,-1,-1"
self.cv_sc: str = "-1,-1,-1,-1"
#self.cv_bv: str = "-1,-1,-1,-1"
self.cv_bv: str = "-1,-1,-1,-1"
self.cv_bf: str = "-1,-1,-1,-1"
self.cnp_cid=-1
self.cnp_val=-1
self.cnp_rr=-1
self.my_qst_id: str = ",".join(["-1"] * 25)
self.my_qst_sts: str = ",".join("0" * 5) + "," + ",".join(["-1"] * 20)
self.my_qst_prgrs: str = ",".join("0" * 5) + "," + ",".join(["-1"] * 20)
self.my_qst_et: str = ",".join([parse.quote(datetime.now().strftime(DivaConstants.LUT_TIME_FMT))] * 5) + "," + ",".join(["xxx"] * 20)
self.clr_sts: str = ",".join("0" * 5) + "," + ",".join(["-1"] * 20)
self.mdl_eqp_tm: str = parse.quote(datetime.now().strftime(DivaConstants.LUT_TIME_FMT))
self.mdl_eqp_ary = ",".join(["-999"] * 3)
self.c_itm_eqp_ary = ",".join(["-999"] * 12)
self.ms_itm_flg_ary = ",".join(["1"] * 12)
class RegisterRequest(BaseRequest):
def __init__(self, raw: str) -> None:
super().__init__(raw)
self.pmm: str = self.raw_dict.get('pmm')
self.idm: str = self.raw_dict.get('idm')
self.mmgameid: str = self.raw_dict.get('mmgameid')
self.mmuid: str = self.raw_dict.get('mmuid')
self.a_code: str = self.raw_dict.get('a_code')
self.aime_a_code: str = self.raw_dict.get('aime_a_code')
self.player_name: str = self.raw_dict.get('player_name')
self.passwd: str = self.raw_dict.get('passwd')
self.aime_id = int(self.raw_dict.get('aime_id'))
self.key_obj_type = int(self.raw_dict.get('key_obj_type'))
class RegisterResponse(BaseResponse):
def __init__(self, req_id: int, pv_id: int) -> None:
super().__init__("register", req_id)
self.cd_adm_result: int = 1
self.pd_id: int = pv_id
class EndRequest(BaseRequest):
def __init__(self, raw: Union[str, bytes]) -> None:
self.my_qst_id: str = self.raw_dict.get('my_qst_id')
self.my_qst_sts: str = self.raw_dict.get('my_qst_sts')
super().__init__(raw)
try:
self.pd_id = int(self.raw_dict.get('pd_id'))
except AttributeError as e:
raise DivaRequestParseException(f"EndRequest: {e}")

View File

@@ -6,11 +6,11 @@ import logging, coloredlogs
from logging.handlers import TimedRotatingFileHandler from logging.handlers import TimedRotatingFileHandler
import zlib import zlib
import json import json
import urllib.parse
import base64 import base64
from os import path from os import path
from typing import Tuple, Dict, List from typing import Tuple, Dict, List
from titles.diva.handlers.base import *
from core.config import CoreConfig from core.config import CoreConfig
from core.title import BaseServlet from core.title import BaseServlet
from core.utils import Utils from core.utils import Utils
@@ -79,88 +79,57 @@ class DivaServlet(BaseServlet):
return True return True
async def render_POST(self, request: Request) -> bytes: async def render_POST(self, request: Request, game_code: str, matchers: Dict) -> bytes:
req_raw = await request.body() req_raw = await request.body()
url_header = request.headers url_header = request.headers
# Ping Dispatch # Ping Dispatch
if "THIS_STRING_SEPARATES" in str(url_header): if "THIS_STRING_SEPARATES" in url_header:
binary_request = req_raw.splitlines() binary_request = req_raw.splitlines()
binary_cmd_decoded = binary_request[3].decode("utf-8") binary_cmd_decoded = binary_request[3].decode("utf-8")
binary_array = binary_cmd_decoded.split("&") url_data = binary_cmd_decoded # for logging
req_cls = BaseBinaryRequest(binary_cmd_decoded)
bin_req_data = {} else:
json_string = json.dumps(
for kvp in binary_array: req_raw.decode("utf-8")
split_bin = kvp.split("=") ) # Take the response and decode as UTF-8 and dump
bin_req_data[split_bin[0]] = split_bin[1] b64string = json_string.replace(
r"\n", "\n"
self.logger.info(f"Binary {bin_req_data['cmd']} Request") ) # Remove all \n and separate them as new lines
self.logger.debug(bin_req_data) gz_string = base64.b64decode(b64string) # Decompressing the base64 string
try: try:
handler = getattr(self.base, f"handle_{bin_req_data['cmd']}_request") url_data = zlib.decompress(gz_string) # Decompressing the gzip
resp = handler(bin_req_data) except zlib.error as e:
self.logger.error(f"Failed to defalte! {e} -> {gz_string}")
except AttributeError as e: return PlainTextResponse("stat=0")
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: try:
self.logger.error(f"Error handling method {e}") req_cls = BaseRequest(url_data)
return PlainTextResponse(f"cmd={bin_req_data['cmd']}&req_id={bin_req_data['req_id']}&stat=ok") except DivaRequestParseException as e:
self.logger.error(e)
return PlainTextResponse("stat=0")
self.logger.debug( self.logger.debug(f"Request: {url_data}\nHeaders: {url_header}")
f"Response cmd={bin_req_data['cmd']}&req_id={bin_req_data['req_id']}&stat=ok{resp}" self.logger.info(
) f"{req_cls.cmd} request from {req_cls.kc_serial}/{req_cls.b_serial} at {Utils.get_ip_addr(request)}"
return PlainTextResponse(f"cmd={bin_req_data['cmd']}&req_id={bin_req_data['req_id']}&stat=ok{resp}")
# Main Dispatch
json_string = json.dumps(
req_raw.decode("utf-8")
) # Take the response and decode as UTF-8 and dump
b64string = json_string.replace(
r"\n", "\n"
) # Remove all \n and separate them as new lines
gz_string = base64.b64decode(b64string) # Decompressing the base64 string
try:
url_data = zlib.decompress(gz_string).decode(
"utf-8"
) # Decompressing the gzip
except zlib.error as e:
self.logger.error(f"Failed to defalte! {e} -> {gz_string}")
return PlainTextResponse("stat=0")
req_kvp = urllib.parse.unquote(url_data)
req_data = {}
# We then need to split each parts with & so we can reuse them to fill out the requests
splitted_request = str.split(req_kvp, "&")
for kvp in splitted_request:
split = kvp.split("=")
req_data[split[0]] = split[1]
self.logger.info(f"{req_data['cmd']} Request")
self.logger.debug(req_data)
func_to_find = f"handle_{req_data['cmd']}_request"
# Load the requests
try:
handler = getattr(self.base, func_to_find)
resp = await handler(req_data)
except AttributeError as e:
self.logger.warning(f"Unhandled {req_data['cmd']} request {e}")
return PlainTextResponse(f"cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok")
except Exception as e:
self.logger.error(f"Error handling method {func_to_find} {e}")
return PlainTextResponse(f"cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok")
self.logger.debug(
f"Response cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok{resp}"
) )
return PlainTextResponse(f"cmd={req_data['cmd']}&req_id={req_data['req_id']}&stat=ok{resp}") handler_str = f"handle_{req_cls.cmd}_request"
if not hasattr(self.base, handler_str):
self.logger.warn(f"Unhandled cmd {req_cls.cmd}")
return PlainTextResponse(BaseResponse(req_cls.cmd, req_cls.req_id).make())
handler = getattr(self.base, handler_str)
response = handler(req_cls.raw)
if response is None or response == "":
response = BaseResponse(req_cls.cmd, req_cls.req_id).make()
if not response.startswith("cmd="):
response = f"cmd={req_cls.cmd}&req_id={req_cls.req_id}&stat=ok" + response
self.logger.debug(f"Response: {response}")
return PlainTextResponse(response)

View File

@@ -183,11 +183,7 @@ class DivaReader(BaseReader):
pv_list[pv_id] = self.add_branch(pv_list[pv_id], key_args, val) pv_list[pv_id] = self.add_branch(pv_list[pv_id], key_args, val)
for pv_id, pv_data in pv_list.items(): for pv_id, pv_data in pv_list.items():
try: song_id = int(pv_id.split("_")[1])
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: if "songinfo" not in pv_data:
continue continue
if "illustrator" not in pv_data["songinfo"]: 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 Given a game version and an aime id, return the cstmz_itm_have hex string
required for diva directly required for diva directly
""" """
items_list = await self.get_customize_items(aime_id, version) items_list = self.get_customize_items(aime_id, version)
if items_list is None: if items_list is None:
items_list = [] items_list = []
item_have = 0 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 Given a game version and an aime id, return the mdl_have hex string
required for diva directly required for diva directly
""" """
module_list = await self.get_modules(aime_id, version) module_list = self.get_modules(aime_id, version)
if module_list is None: if module_list is None:
module_list = [] module_list = []
module_have = 0 module_have = 0

View File

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

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