forked from Hay1tsme/artemis
Compare commits
2 Commits
fix/chuni/
...
fix/perfor
Author | SHA1 | Date | |
---|---|---|---|
7fe3e83943 | |||
769bd2a45f |
24
changelog.md
24
changelog.md
@ -1,30 +1,6 @@
|
||||
# Changelog
|
||||
Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to.
|
||||
|
||||
## 20240620
|
||||
### CHUNITHM
|
||||
+ CHUNITHM LUMINOUS support
|
||||
|
||||
## 20240616
|
||||
### 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
|
||||
|
180
contributing.md
180
contributing.md
@ -1,182 +1,8 @@
|
||||
# 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.
|
||||
|
||||
This document is a work in progress. If you have any questions or notice any errors, please report it to the discord.
|
||||
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.
|
||||
|
||||
## Adding games
|
||||
### Step 0
|
||||
+ Follow the "n-1" rule of thumb. PRs for game versions that are currently active in arcades will be deleted. If you're unsure, ask!
|
||||
+ Always PR against the `develop` branch.
|
||||
+ Check to see if somebody else is already PRing the features/games you want to add. If they are, consider contributing to them rather then making an entirely new PR.
|
||||
+ We don't technically have a written code style guide (TODO) but try to keep your code consistant with code that's already there where possible.
|
||||
|
||||
### Step 1 (Setup)
|
||||
1) Fork the gitea repo, clone your fork, and checkout the develop branch.
|
||||
2) Make a new folder in the `titles` folder, name it some recogniseable shorthand for your game (Chunithm becomes chuni, maimai dx is mai2, etc)
|
||||
3) In this new folder, create a file named `__init__.py`. This is the first thing that will load when your title module is loaded by the core system, and it acts as sort of a directory for where everything lives in your module. This file will contain the following required items:
|
||||
+ `index`: must point to a subclass of `BaseServlet` that will handle setup and dispatching of your game.
|
||||
+ `game_codes`: must be a list of 4 letter SEGA game codes as strings.
|
||||
|
||||
It can also contain the following optional fields:
|
||||
+ `database`: points to a subclass of `Data` that contains one or more subclasses of `BaseData` that act as database transaction handlers. Required for the class to store and retrieve data from the database.
|
||||
+ `reader`: points to a subclass of `BaseReader` that handles importing static data from game files into the database.
|
||||
+ `frontend`: points to a subclass of `FE_Base` that handles frontend routes for your game.
|
||||
|
||||
The next step will focus on `index`
|
||||
|
||||
### Step 2 (Index)
|
||||
1) Create another file in your game's folder. By convention, it should be called `index.py`.
|
||||
|
||||
2) Inside `index.py`, add the following code, replacing {Game name here} with the name of your game, without spaces or special characters. Look at other titles for examples.
|
||||
```py
|
||||
from core.title import BaseServlet
|
||||
from core import CoreConfig
|
||||
|
||||
class {Game name here}Servlet(BaseServlet):
|
||||
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
|
||||
pass
|
||||
```
|
||||
3) The `__init__` function should acomplish the following:
|
||||
+ Reading your game's config
|
||||
+ Setting up your games logger
|
||||
+ Instancing your games versions
|
||||
|
||||
It's usually safe to copy and paste the `__init__` functions from other games, just make sure you change everything that needs to be changed!
|
||||
|
||||
4) Go back to the `__init__.py` that you created and add the following:
|
||||
```py
|
||||
from .index import {Game name here}Servlet
|
||||
|
||||
index = {Game name here}Servlet
|
||||
```
|
||||
|
||||
5) Going back to `index.py`, within the Servlet class, define the following functions from `BaseServlet` as needed (see function documentation):
|
||||
+ `is_game_enabled`: Returns true if the game is enabled and should be served, false otherwise. Returns false by default, so override this to allow your game to be served.
|
||||
+ `get_routes`: Returns a list of Starlette routes that your game will serve.
|
||||
+ `get_allnet_info`: Returns a tuple of strings where the first is the allnet uri and the second is the allnet host. The function takes the game ID, version and keychip ID as parameters, so you can send different responses if need be.
|
||||
+ `get_mucha_info`: Only used by games that use Mucha as authentication. Returns a tuple where the first is a bool that is weather or not the game is enabled, the 2nd is a list of game CDs as strings that this servlet should handle, and the 3rd is a list of netID prefixes that each game CD should use. If your game does not use mucha, do not define this function.
|
||||
+ `setup`: Preforms any setup your servlet requires, such as spinning up matching servers. It is run once when the server starts. If you don't need any setup, do not define.
|
||||
|
||||
6) Make sure any functions you specify to handle routes in `get_routes` are defined as async, as follows: `async def handle_thing(self, request: Request) -> Response:` where Response is whatever kind of Response class you'll be returning. Make sure all paths in this function return some subclass of Response, otherwise you'll get an error when serving.
|
||||
|
||||
### Step 3 (Constants)
|
||||
1) In your game's folder, create a file to store static values for your game. By convention, we call this `const.py`
|
||||
|
||||
2) Inside, create a class called `{Game name here}Constants`. Do not define an `__init__` function.
|
||||
|
||||
3) Put constants related to your game here. A good example of something to put here is game codes.
|
||||
```py
|
||||
class {Game name here}Constants:
|
||||
GAME_CODE = "SBXX"
|
||||
CONFIG_NAME = "{game name}.yaml"
|
||||
```
|
||||
|
||||
4) If you choose to put game codes in here, add this to your `__init__.py` file:
|
||||
```py
|
||||
from .const import {Game name here}Constants
|
||||
...
|
||||
game_codes = [{Game name here}Constants.GAME_CODE]
|
||||
```
|
||||
|
||||
### Step 4 (Config)
|
||||
1) Make a file to store your game's config. By convention, it should be called `config.py`
|
||||
|
||||
2) Inside that file, add the following:
|
||||
```py
|
||||
from core.config import CoreConfig
|
||||
|
||||
class {game name}ServerConfig:
|
||||
def __init__(self, parent_config: "{game name}Config") -> None:
|
||||
self.__config = parent_config
|
||||
|
||||
@property
|
||||
def enable(self) -> bool:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "{game name}", "server", "enable", default=True
|
||||
)
|
||||
|
||||
@property
|
||||
def loglevel(self) -> int:
|
||||
return CoreConfig.str_to_loglevel(
|
||||
CoreConfig.get_config_field(
|
||||
self.__config, "{game name}", "server", "loglevel", default="info"
|
||||
)
|
||||
)
|
||||
|
||||
class {game name}Config(dict):
|
||||
def __init__(self) -> None:
|
||||
self.server = {game name}ServerConfig(self)
|
||||
```
|
||||
|
||||
3) In the `example_config` folder, create a yaml file for your game. By convention, it should be called `{game folder name}.ymal`. Add the following:
|
||||
```yaml
|
||||
server:
|
||||
enable: True
|
||||
loglevel: "info"
|
||||
```
|
||||
|
||||
4) Add any additional config options that you feel the game needs. Look to other games for config examples.
|
||||
|
||||
5) In `index.py` import your config and instance it in `__init__` with:
|
||||
```py
|
||||
self.game_cfg = {game folder name}Config()
|
||||
if path.exists(f"{cfg_dir}/{game folder name}Constants.CONFIG_NAME}"):
|
||||
self.game_cfg.update(
|
||||
yaml.safe_load(open(f"{cfg_dir}/{game folder name}Constants.CONFIG_NAME}"))
|
||||
)
|
||||
```
|
||||
This will attempt to load the config file you specified in your constants, and if not, go with the defaults specified in `config.py`. This game_cfg object can then be passed down to your handlers when you create them.
|
||||
|
||||
At this stage your game should be loaded by allnet, and serve whatever routes you put in `get_routes`. See the next section about adding versions and handlers.
|
||||
|
||||
### Step 5 (Database)
|
||||
TODO
|
||||
|
||||
### Step 6 (Frontend)
|
||||
TODO
|
||||
|
||||
### Step 7 (Reader)
|
||||
TODO
|
||||
Guide WIP
|
||||
|
||||
## Adding game versions
|
||||
See the above section about code expectations and how to PR.
|
||||
1) In the game's folder, create a python file to contain the version handlers. By convention, the first version is version 0, and is stored in `base.py`. Versions following that increment the version number, and are stored in `{short version name}.py`. See Wacca's folder for an example of how to name versions.
|
||||
|
||||
2) Internal version numbers should be defined in `const.py`. The version should change any time the game gets a major update (i.e. a new version or plus version.)
|
||||
```py
|
||||
# in const.py
|
||||
VERSION_{game name} = 0
|
||||
VERSION_{game name}_PLUS = 1
|
||||
```
|
||||
|
||||
3) Inside `base.py` (or whatever your version is named) add the following:
|
||||
```py
|
||||
class {game name}Base:
|
||||
def __init__(self, cfg: CoreConfig, game_cfg: {game name}Config) -> None:
|
||||
self.game_config = game_cfg
|
||||
self.core_config = cfg
|
||||
self.version = {game name}Constants.VERSION_{game name}
|
||||
self.data = {game name}Data(cfg)
|
||||
# Any other initialization stuff
|
||||
```
|
||||
|
||||
4) Define your handlers. This will vary wildly by game, but best practice is to keep the naming consistant, so that the main dispatch function in `index.py` can use `getattr` to get the handler, rather then having a static list of what endpoint or request type goes to which handler. See Wacca's `index.py` and `base.py` for examples of how to do this.
|
||||
|
||||
5) If your version is not the base version, make sure it inherits from the base version:
|
||||
```py
|
||||
class {game name}Plus({game name}Base):
|
||||
def __init__(self, cfg: CoreConfig, game_cfg: {game name}Config) -> None:
|
||||
super().__init__(cfg, game_cfg)
|
||||
self.version = {game name}Constants.VERSION_{game name}_PLUS
|
||||
```
|
||||
|
||||
6) Back in `index.py` make sure to import your new class, and add it to `__init__`. Some games may opt to just a single list called `self.versions` that contains all the version classes at their internal version's index. Others may simply define them as seperate members. See Wacca for an example of `self.versions`
|
||||
|
||||
7) Add your version to your game's dispatching logic.
|
||||
|
||||
8) Test to make sure your game is being handled properly.
|
||||
|
||||
9) Submit a PR.
|
||||
|
||||
## Adding/improving core services
|
||||
If you intend to submit improvements or additions to core services (allnet, mucha, billing, aimedb, database, etc) please get in touch with a maintainer.
|
||||
Guide WIP
|
@ -10,14 +10,13 @@ class ADBFelicaLookupRequest(ADBBaseRequest):
|
||||
self.pmm = hex(pmm)[2:].upper()
|
||||
|
||||
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)
|
||||
self.access_code = access_code if access_code is not None else "00000000000000000000"
|
||||
self.idx = idx
|
||||
|
||||
@classmethod
|
||||
def from_req(cls, req: ADBHeader, access_code: str = None, idx: int = 0) -> "ADBFelicaLookupResponse":
|
||||
c = cls(access_code, idx, req.game_id, req.store_id, req.keychip_id)
|
||||
def from_req(cls, req: ADBHeader, access_code: str = None) -> "ADBFelicaLookupResponse":
|
||||
c = cls(access_code, req.game_id, req.store_id, req.keychip_id)
|
||||
c.head.protocol_ver = req.protocol_ver
|
||||
return c
|
||||
|
||||
@ -27,7 +26,7 @@ class ADBFelicaLookupResponse(ADBBaseResponse):
|
||||
"access_code" / Int8ub[10],
|
||||
Padding(2)
|
||||
).build(dict(
|
||||
felica_idx = self.idx,
|
||||
felica_idx = 0,
|
||||
access_code = bytes.fromhex(self.access_code)
|
||||
))
|
||||
|
||||
|
@ -194,9 +194,6 @@ class AimedbServlette():
|
||||
|
||||
if user_id and user_id > 0:
|
||||
await self.data.card.update_card_last_login(req.access_code)
|
||||
if req.access_code.startswith("010") or req.access_code.startswith("3"):
|
||||
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}")
|
||||
return ret
|
||||
|
||||
async def handle_lookup_ex(self, data: bytes, resp_code: int) -> ADBBaseResponse:
|
||||
@ -232,24 +229,15 @@ class AimedbServlette():
|
||||
|
||||
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
|
||||
database. We do not have access to that database so we have to make due with what we got.
|
||||
Interestingly, namco games are able to read S_PAD0 and send the server the correct access
|
||||
code, but aimedb doesn't. Until somebody either enters the correct code manually, or scans
|
||||
on a game that reads it correctly from the card, this will have to do. It's the same conversion
|
||||
used on the big boy networks.
|
||||
On official, I think a card has to be registered for this to actually work, but
|
||||
I'm making the executive decision to not implement that and just kick back our
|
||||
faux generated access code. The real felica IDm -> access code conversion is done
|
||||
on the ADB server, which we do not and will not ever have access to. Because we can
|
||||
assure that all IDms will be unique, this basic 0-padded hex -> int conversion will
|
||||
be fine.
|
||||
"""
|
||||
req = ADBFelicaLookupRequest(data)
|
||||
card = await self.data.card.get_card_by_idm(req.idm)
|
||||
if not card:
|
||||
ac = self.data.card.to_access_code(req.idm)
|
||||
test = await self.data.card.get_card_by_access_code(ac)
|
||||
if test:
|
||||
await self.data.card.set_idm_by_access_code(ac, req.idm)
|
||||
|
||||
else:
|
||||
ac = card['access_code']
|
||||
|
||||
ac = self.data.card.to_access_code(req.idm)
|
||||
self.logger.info(
|
||||
f"idm {req.idm} ipm {req.pmm} -> access_code {ac}"
|
||||
)
|
||||
@ -257,8 +245,7 @@ class AimedbServlette():
|
||||
|
||||
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
|
||||
because we don't implement felica_lookup properly.
|
||||
I've never seen this used.
|
||||
"""
|
||||
req = ADBFelicaLookupRequest(data)
|
||||
ac = self.data.card.to_access_code(req.idm)
|
||||
@ -292,18 +279,8 @@ class AimedbServlette():
|
||||
|
||||
async def handle_felica_lookup_ex(self, data: bytes, resp_code: int) -> bytes:
|
||||
req = ADBFelicaLookup2Request(data)
|
||||
user_id = None
|
||||
card = await self.data.card.get_card_by_idm(req.idm)
|
||||
if not card:
|
||||
access_code = self.data.card.to_access_code(req.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, req.idm)
|
||||
|
||||
else:
|
||||
user_id = card['user']
|
||||
access_code = card['access_code']
|
||||
access_code = self.data.card.to_access_code(req.idm)
|
||||
user_id = await self.data.card.get_user_id_from_card(access_code=access_code)
|
||||
|
||||
if user_id is None:
|
||||
user_id = -1
|
||||
@ -313,14 +290,6 @@ class AimedbServlette():
|
||||
)
|
||||
|
||||
resp = ADBFelicaLookup2Response.from_req(req.head, user_id, access_code)
|
||||
|
||||
if user_id > 0:
|
||||
if card['is_banned'] and card['is_locked']:
|
||||
resp.head.status = ADBStatus.BAN_SYS_USER
|
||||
elif card['is_banned']:
|
||||
resp.head.status = ADBStatus.BAN_SYS
|
||||
elif card['is_locked']:
|
||||
resp.head.status = ADBStatus.LOCK_USER
|
||||
|
||||
if user_id and user_id > 0 and self.config.aimedb.id_secret:
|
||||
auth_key = create_sega_auth_key(user_id, req.head.game_id, req.head.store_id, req.head.keychip_id, self.config.aimedb.id_secret, self.config.aimedb.id_lifetime_seconds)
|
||||
@ -368,16 +337,6 @@ class AimedbServlette():
|
||||
self.logger.info(
|
||||
f"Registration blocked!: access code {req.access_code}"
|
||||
)
|
||||
|
||||
if user_id > 0:
|
||||
if req.access_code.startswith("010") or req.access_code.startswith("3"):
|
||||
await self.data.card.set_chip_id_by_access_code(req.access_code, req.serial_number)
|
||||
self.logger.info(f"Attempt to set chip id to {req.serial_number} for access code {req.access_code}")
|
||||
|
||||
elif req.access_code.startswith("0008"):
|
||||
idm = self.data.card.to_idm(req.access_code)
|
||||
await self.data.card.set_idm_by_access_code(req.access_code, idm)
|
||||
self.logger.info(f"Attempt to set IDm to {idm} for access code {req.access_code}")
|
||||
|
||||
resp = ADBLookupResponse.from_req(req.head, user_id)
|
||||
if resp.user_id <= 0:
|
||||
|
143
core/allnet.py
143
core/allnet.py
@ -132,7 +132,7 @@ class AllnetServlet:
|
||||
async def handle_poweron(self, request: Request):
|
||||
request_ip = Utils.get_ip_addr(request)
|
||||
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()
|
||||
|
||||
try:
|
||||
@ -171,7 +171,7 @@ class AllnetServlet:
|
||||
if machine is None and not self.config.server.allow_unregistered_serials:
|
||||
msg = f"Unrecognised serial {req.serial} attempted allnet auth from {request_ip}."
|
||||
await self.data.base.log_event(
|
||||
"allnet", "ALLNET_AUTH_UNKNOWN_SERIAL", logging.WARN, msg, {"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)
|
||||
|
||||
@ -183,9 +183,9 @@ class AllnetServlet:
|
||||
arcade = await self.data.arcade.get_arcade(machine["arcade"])
|
||||
if self.config.server.check_arcade_ip:
|
||||
if arcade["ip"] and arcade["ip"] is not None and arcade["ip"] != req.ip:
|
||||
msg = f"{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(
|
||||
"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)
|
||||
|
||||
@ -194,9 +194,9 @@ class AllnetServlet:
|
||||
return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n")
|
||||
|
||||
elif (not arcade["ip"] or arcade["ip"] is None) and self.config.server.strict_ip_checking:
|
||||
msg = f"{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(
|
||||
"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)
|
||||
|
||||
@ -204,17 +204,7 @@ class AllnetServlet:
|
||||
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
|
||||
return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n")
|
||||
|
||||
if machine['game'] and machine['game'] != req.game_id:
|
||||
msg = f"{req.serial} attempted allnet auth with bad game ID {req.game_id} (expected {machine['game']})."
|
||||
await self.data.base.log_event(
|
||||
"allnet", "ALLNET_AUTH_BAD_GAME", logging.ERROR, msg, {}, None, arcade['id'], machine['id'], request_ip, req.game_id, req.ver
|
||||
)
|
||||
self.logger.warning(msg)
|
||||
|
||||
resp.stat = ALLNET_STAT.bad_game.value
|
||||
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
|
||||
return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n")
|
||||
|
||||
country = (
|
||||
arcade["country"] if machine["country"] is None else machine["country"]
|
||||
)
|
||||
@ -246,14 +236,11 @@ class AllnetServlet:
|
||||
arcade["timezone"] if arcade["timezone"] is not None else "+0900" if req.format_ver == 3 else "+09:00"
|
||||
)
|
||||
|
||||
else:
|
||||
arcade = None
|
||||
|
||||
if req.game_id not in TitleServlet.title_registry:
|
||||
if not self.config.server.is_develop:
|
||||
msg = f"Unrecognised game {req.game_id} attempted allnet auth from {request_ip}."
|
||||
await self.data.base.log_event(
|
||||
"allnet", "ALLNET_AUTH_UNKNOWN_GAME", logging.WARN, msg, {}, 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)
|
||||
|
||||
@ -284,37 +271,25 @@ class AllnetServlet:
|
||||
resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
|
||||
return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n")
|
||||
|
||||
if machine and arcade:
|
||||
msg = f"{req.serial} authenticated from {request_ip}: {req.game_id} v{req.ver}"
|
||||
await self.data.base.log_event(
|
||||
"allnet", "ALLNET_AUTH_SUCCESS", logging.INFO, msg, {}, 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
|
||||
)
|
||||
|
||||
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)
|
||||
self.logger.info(msg)
|
||||
|
||||
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}")
|
||||
resp_str += "\n"
|
||||
|
||||
if is_dfi:
|
||||
return PlainTextResponse(
|
||||
content=self.to_dfi(resp_str) + b"\r\n",
|
||||
headers={
|
||||
"Pragma": "DFI",
|
||||
},
|
||||
)
|
||||
"""if is_dfi:
|
||||
request.responseHeaders.addRawHeader('Pragma', 'DFI')
|
||||
return self.to_dfi(resp_str)"""
|
||||
|
||||
return PlainTextResponse(resp_str)
|
||||
|
||||
async def handle_dlorder(self, request: Request):
|
||||
request_ip = Utils.get_ip_addr(request)
|
||||
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()
|
||||
|
||||
try:
|
||||
@ -351,11 +326,7 @@ class AllnetServlet:
|
||||
):
|
||||
return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n")
|
||||
|
||||
else:
|
||||
machine = await self.data.arcade.get_machine(req.serial)
|
||||
if not machine or not machine['ota_enable'] or not machine['is_cab'] or machine['is_blacklisted']:
|
||||
return PlainTextResponse(urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n")
|
||||
|
||||
else: # TODO: Keychip check
|
||||
if path.exists(
|
||||
f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver.replace('.', '')}-app.ini"
|
||||
):
|
||||
@ -366,38 +337,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"
|
||||
|
||||
if resp.uri:
|
||||
self.logger.info(f"Sending download uri {resp.uri}")
|
||||
await self.data.base.log_event(
|
||||
"allnet", "DLORDER_REQ_SUCCESS", logging.INFO, f"Send download URI to {req.serial} for {req.game_id} v{req.ver} from {Utils.get_ip_addr(request)}", {"uri": resp.uri}, None,
|
||||
machine['arcade'], machine['id'], request_ip, req.game_id, req.ver
|
||||
)
|
||||
# Maybe add a log event for checkin but no url sent?
|
||||
self.logger.debug(f"Sending download uri {resp.uri}")
|
||||
await self.data.base.log_event("allnet", "DLORDER_REQ_SUCCESS", logging.INFO, f"{Utils.get_ip_addr(request)} requested DL Order for {req.serial} {req.game_id} v{req.ver}")
|
||||
|
||||
res_str = urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n"
|
||||
|
||||
if is_dfi:
|
||||
return PlainTextResponse(
|
||||
content=self.to_dfi(res_str) + b"\r\n",
|
||||
headers={
|
||||
"Pragma": "DFI",
|
||||
},
|
||||
)
|
||||
"""if is_dfi:
|
||||
request.responseHeaders.addRawHeader('Pragma', 'DFI')
|
||||
return self.to_dfi(res_str)"""
|
||||
|
||||
return PlainTextResponse(res_str)
|
||||
|
||||
async def handle_dlorder_ini(self, request: Request) -> bytes:
|
||||
req_file = request.path_params.get("file", "").replace("%0A", "").replace("\n", "")
|
||||
request_ip = Utils.get_ip_addr(request)
|
||||
|
||||
if not req_file:
|
||||
return PlainTextResponse(status_code=404)
|
||||
|
||||
if path.exists(f"{self.config.allnet.update_cfg_folder}/{req_file}"):
|
||||
self.logger.info(f"Request for DL INI file {req_file} from {request_ip} successful")
|
||||
await self.data.base.log_event(
|
||||
"allnet", "DLORDER_INI_SENT", logging.INFO, f"{request_ip} successfully recieved {req_file}", {"file": req_file}, ip=request_ip
|
||||
)
|
||||
self.logger.info(f"Request for DL INI file {req_file} from {Utils.get_ip_addr(request)} successful")
|
||||
await self.data.base.log_event("allnet", "DLORDER_INI_SENT", logging.INFO, f"{Utils.get_ip_addr(request)} successfully recieved {req_file}")
|
||||
|
||||
return PlainTextResponse(open(
|
||||
f"{self.config.allnet.update_cfg_folder}/{req_file}", "r", encoding="utf-8"
|
||||
@ -435,13 +393,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}:"\
|
||||
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)
|
||||
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)
|
||||
await self.data.base.log_event("allnet", "DL_REPORT", logging.INFO, msg, dl_data)
|
||||
self.logger.info(msg)
|
||||
|
||||
return PlainTextResponse("OK")
|
||||
@ -461,24 +413,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:
|
||||
return PlainTextResponse("NG")
|
||||
|
||||
msg = f"LoaderStateRecorder Request from {ip} {serial}: {num_files_dld}/{num_files_to_dl} Files download (State: {dl_state})"
|
||||
machine = await self.data.arcade.get_machine(serial)
|
||||
if machine:
|
||||
await self.data.base.log_event("allnet", "LSR_REPORT", logging.INFO, msg, req_dict, None, machine['arcade'], machine['id'], ip)
|
||||
|
||||
else:
|
||||
msg = "Unregistered " + msg
|
||||
await self.data.base.log_event("allnet", "LSR_REPORT_UNREG", logging.INFO, msg, req_dict, None, None, None, ip)
|
||||
|
||||
self.logger.info(msg)
|
||||
self.logger.info(f"LoaderStateRecorder Request from {ip} {serial}: {num_files_dld}/{num_files_to_dl} Files download (State: {dl_state})")
|
||||
return PlainTextResponse("OK")
|
||||
|
||||
async def handle_alive(self, request: Request) -> bytes:
|
||||
return PlainTextResponse("OK")
|
||||
|
||||
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")
|
||||
|
||||
def allnet_req_to_dict(self, data: str) -> Optional[List[Dict[str, Any]]]:
|
||||
@ -608,35 +550,18 @@ class BillingServlet:
|
||||
if machine is None and not self.config.server.allow_unregistered_serials:
|
||||
msg = f"Unrecognised serial {req.keychipid} attempted billing checkin from {request_ip} for {req.gameid} v{req.gamever}."
|
||||
await self.data.base.log_event(
|
||||
"allnet", "BILLING_CHECKIN_NG_SERIAL", logging.WARN, msg, ip=request_ip, game=req.gameid, version=req.gamever
|
||||
"allnet", "BILLING_CHECKIN_NG_SERIAL", logging.WARN, msg
|
||||
)
|
||||
self.logger.warning(msg)
|
||||
|
||||
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:
|
||||
self.logger.warn(f"{req.traceleft} unsent tracelogs")
|
||||
kc_playlimit = req.playlimit
|
||||
@ -960,7 +885,7 @@ class DLReport:
|
||||
|
||||
return True
|
||||
|
||||
cfg_dir = environ.get("ARTEMIS_CFG_DIR", "config")
|
||||
cfg_dir = environ.get("DIANA_CFG_DIR", "config")
|
||||
cfg: CoreConfig = CoreConfig()
|
||||
if path.exists(f"{cfg_dir}/core.yaml"):
|
||||
cfg.update(yaml.safe_load(open(f"{cfg_dir}/core.yaml")))
|
||||
|
@ -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 ###
|
@ -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 ###
|
@ -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 ###
|
@ -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 ###
|
@ -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(None, 'aime_card', type_='unique')
|
||||
op.drop_constraint(None, 'aime_card', type_='unique')
|
||||
op.drop_constraint(None, '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 ###
|
@ -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 ###
|
@ -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 ###
|
@ -1,7 +1,7 @@
|
||||
"""mai2_buddies_support
|
||||
|
||||
Revision ID: 81e44dd6047a
|
||||
Revises: 6a7e8277763b
|
||||
Revises: d8950c7ce2fc
|
||||
Create Date: 2024-03-12 19:10:37.063907
|
||||
|
||||
"""
|
||||
|
@ -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)
|
@ -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")
|
@ -8,8 +8,7 @@ from sqlalchemy.engine.base import Connection
|
||||
from sqlalchemy.sql import text, func, select
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy import MetaData, Table, Column
|
||||
from sqlalchemy.types import Integer, String, TIMESTAMP, JSON, INTEGER, TEXT
|
||||
from sqlalchemy.schema import ForeignKey
|
||||
from sqlalchemy.types import Integer, String, TIMESTAMP, JSON
|
||||
from sqlalchemy.dialects.mysql import insert
|
||||
|
||||
from core.config import CoreConfig
|
||||
@ -23,12 +22,6 @@ event_log = Table(
|
||||
Column("system", String(255), nullable=False),
|
||||
Column("type", String(255), nullable=False),
|
||||
Column("severity", Integer, nullable=False),
|
||||
Column("user", INTEGER, ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
|
||||
Column("arcade", INTEGER, ForeignKey("arcade.id", ondelete="cascade", onupdate="cascade")),
|
||||
Column("machine", INTEGER, ForeignKey("machine.id", ondelete="cascade", onupdate="cascade")),
|
||||
Column("ip", TEXT(39)),
|
||||
Column("game", TEXT(4)),
|
||||
Column("version", TEXT(24)),
|
||||
Column("message", String(1000), nullable=False),
|
||||
Column("details", JSON, nullable=False),
|
||||
Column("when_logged", TIMESTAMP, nullable=False, server_default=func.now()),
|
||||
@ -82,19 +75,12 @@ class BaseData:
|
||||
return randrange(10000, 9999999)
|
||||
|
||||
async def log_event(
|
||||
self, system: str, type: str, severity: int, message: str, details: Dict = {}, user: int = None,
|
||||
arcade: int = None, machine: int = None, ip: str = None, game: str = None, version: str = None
|
||||
self, system: str, type: str, severity: int, message: str, details: Dict = {}
|
||||
) -> Optional[int]:
|
||||
sql = event_log.insert().values(
|
||||
system=system,
|
||||
type=type,
|
||||
severity=severity,
|
||||
user=user,
|
||||
arcade=arcade,
|
||||
machine=machine,
|
||||
ip=ip,
|
||||
game=game,
|
||||
version=version,
|
||||
message=message,
|
||||
details=json.dumps(details),
|
||||
)
|
||||
@ -108,8 +94,8 @@ class BaseData:
|
||||
|
||||
return result.lastrowid
|
||||
|
||||
async def get_event_log(self, entries: int = 100) -> Optional[List[Row]]:
|
||||
sql = event_log.select().order_by(event_log.c.id.desc()).limit(entries)
|
||||
async def get_event_log(self, entries: int = 100) -> Optional[List[Dict]]:
|
||||
sql = event_log.select().limit(entries).all()
|
||||
result = await self.execute(sql)
|
||||
|
||||
if result is None:
|
||||
|
@ -1,6 +1,6 @@
|
||||
from typing import Dict, List, Optional
|
||||
from sqlalchemy import Table, Column, UniqueConstraint
|
||||
from sqlalchemy.types import Integer, String, Boolean, TIMESTAMP, BIGINT
|
||||
from sqlalchemy.types import Integer, String, Boolean, TIMESTAMP
|
||||
from sqlalchemy.sql.schema import ForeignKey
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy.engine import Row
|
||||
@ -11,10 +11,12 @@ aime_card = Table(
|
||||
"aime_card",
|
||||
metadata,
|
||||
Column("id", Integer, primary_key=True, nullable=False),
|
||||
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
|
||||
Column("access_code", String(20), nullable=False, unique=True),
|
||||
Column("idm", String(16), unique=True),
|
||||
Column("chip_id", BIGINT, unique=True),
|
||||
Column(
|
||||
"user",
|
||||
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
||||
nullable=False,
|
||||
),
|
||||
Column("access_code", String(20)),
|
||||
Column("created_date", TIMESTAMP, server_default=func.now()),
|
||||
Column("last_login_date", TIMESTAMP, onupdate=func.now()),
|
||||
Column("is_locked", Boolean, server_default="0"),
|
||||
@ -119,27 +121,7 @@ class CardData(BaseData):
|
||||
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}")
|
||||
|
||||
|
||||
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
|
||||
@ -150,4 +132,4 @@ class CardData(BaseData):
|
||||
"""
|
||||
Given a 20 digit access code as a string, return the 16 hex character luid
|
||||
"""
|
||||
return f"{int(access_code):0{16}X}"
|
||||
return f"{int(access_code):0{16}x}"
|
||||
|
@ -120,7 +120,3 @@ class UserData(BaseData):
|
||||
|
||||
result = await self.execute(sql)
|
||||
return result is not None
|
||||
|
||||
async def get_user_by_username(self, username: str) -> Optional[Row]:
|
||||
result = await self.execute(aime_user.select(aime_user.c.username == username))
|
||||
if result: return result.fetchone()
|
||||
|
@ -44,13 +44,11 @@ class ShopOwner():
|
||||
self.permissions = perms
|
||||
|
||||
class UserSession():
|
||||
def __init__(self, usr_id: int = 0, ip: str = "", perms: int = 0, ongeki_ver: int = 7, chunithm_ver: int = -1, 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.current_ip = ip
|
||||
self.permissions = perms
|
||||
self.ongeki_version = ongeki_ver
|
||||
self.chunithm_version = chunithm_ver
|
||||
self.maimai_version = maimai_version
|
||||
|
||||
class FrontendServlet():
|
||||
def __init__(self, cfg: CoreConfig, config_dir: str) -> None:
|
||||
@ -134,7 +132,6 @@ class FrontendServlet():
|
||||
]),
|
||||
Mount("/sys", routes=[
|
||||
Route("/", self.system.render_GET, methods=['GET']),
|
||||
Route("/logs", self.system.render_logs, methods=['GET']),
|
||||
Route("/lookup.user", self.system.lookup_user, methods=['GET']),
|
||||
Route("/lookup.shop", self.system.lookup_shop, methods=['GET']),
|
||||
Route("/add.user", self.system.add_user, methods=['POST']),
|
||||
@ -193,7 +190,7 @@ class FE_Base():
|
||||
), media_type="text/html; charset=utf-8")
|
||||
|
||||
if sesh is None:
|
||||
resp.delete_cookie("ARTEMIS_SESH")
|
||||
resp.delete_cookie("DIANA_SESH")
|
||||
return resp
|
||||
|
||||
def get_routes(self) -> List[Route]:
|
||||
@ -216,10 +213,7 @@ class FE_Base():
|
||||
sesh.user_id = tk['user_id']
|
||||
sesh.current_ip = tk['current_ip']
|
||||
sesh.permissions = tk['permissions']
|
||||
sesh.chunithm_version = tk['chunithm_version']
|
||||
sesh.maimai_version = tk['maimai_version']
|
||||
sesh.ongeki_version = tk['ongeki_version']
|
||||
|
||||
|
||||
if sesh.user_id <= 0:
|
||||
self.logger.error("User session failed to validate due to an invalid ID!")
|
||||
return UserSession()
|
||||
@ -244,7 +238,7 @@ class FE_Base():
|
||||
return UserSession()
|
||||
|
||||
def validate_session(self, request: Request) -> Optional[UserSession]:
|
||||
sesh = request.cookies.get('ARTEMIS_SESH', "")
|
||||
sesh = request.cookies.get('DIANA_SESH', "")
|
||||
if not sesh:
|
||||
return None
|
||||
|
||||
@ -258,22 +252,12 @@ class FE_Base():
|
||||
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}")
|
||||
return None
|
||||
|
||||
|
||||
return usr_sesh
|
||||
|
||||
def encode_session(self, sesh: UserSession, exp_seconds: int = 86400) -> str:
|
||||
try:
|
||||
return jwt.encode({
|
||||
"user_id": sesh.user_id,
|
||||
"current_ip": sesh.current_ip,
|
||||
"permissions": sesh.permissions,
|
||||
"ongeki_version": sesh.ongeki_version,
|
||||
"chunithm_version": sesh.chunithm_version,
|
||||
"maimai_version": sesh.maimai_version,
|
||||
"exp": int(datetime.now(tz=timezone.utc).timestamp()) + exp_seconds },
|
||||
b64decode(self.core_config.frontend.secret),
|
||||
algorithm="HS256"
|
||||
)
|
||||
return jwt.encode({ "user_id": sesh.user_id, "current_ip": sesh.current_ip, "permissions": sesh.permissions, "ongeki_version": sesh.ongeki_version, "exp": int(datetime.now(tz=timezone.utc).timestamp()) + exp_seconds }, b64decode(self.core_config.frontend.secret), algorithm="HS256")
|
||||
except jwt.InvalidKeyError:
|
||||
self.logger.error("Failed to encode User session because the secret is invalid!")
|
||||
return ""
|
||||
@ -305,7 +289,7 @@ class FE_Gate(FE_Base):
|
||||
error=err,
|
||||
sesh=vars(UserSession()),
|
||||
), media_type="text/html; charset=utf-8")
|
||||
resp.delete_cookie("ARTEMIS_SESH")
|
||||
resp.delete_cookie("DIANA_SESH")
|
||||
return resp
|
||||
|
||||
async def render_login(self, request: Request):
|
||||
@ -321,12 +305,8 @@ class FE_Gate(FE_Base):
|
||||
|
||||
uid = await self.data.card.get_user_id_from_card(access_code)
|
||||
if uid is None:
|
||||
user = await self.data.user.get_user_by_username(access_code) # Lookup as username
|
||||
if not user:
|
||||
self.logger.debug(f"Failed to find user for card/username {access_code}")
|
||||
return RedirectResponse("/gate/?e=1", 303)
|
||||
|
||||
uid = user['id']
|
||||
self.logger.debug(f"Failed to find user for card {access_code}")
|
||||
return RedirectResponse("/gate/?e=1", 303)
|
||||
|
||||
user = await self.data.user.get_user(uid)
|
||||
if user is None:
|
||||
@ -355,7 +335,7 @@ class FE_Gate(FE_Base):
|
||||
usr_sesh = self.encode_session(sesh)
|
||||
self.logger.debug(f"Created session with JWT {usr_sesh}")
|
||||
resp = RedirectResponse("/user/", 303)
|
||||
resp.set_cookie("ARTEMIS_SESH", usr_sesh)
|
||||
resp.set_cookie("DIANA_SESH", usr_sesh)
|
||||
|
||||
return resp
|
||||
|
||||
@ -394,7 +374,7 @@ class FE_Gate(FE_Base):
|
||||
usr_sesh = self.encode_session(sesh)
|
||||
self.logger.debug(f"Created session with JWT {usr_sesh}")
|
||||
resp = RedirectResponse("/user/", 303)
|
||||
resp.set_cookie("ARTEMIS_SESH", usr_sesh)
|
||||
resp.set_cookie("DIANA_SESH", usr_sesh)
|
||||
|
||||
return resp
|
||||
|
||||
@ -512,7 +492,7 @@ class FE_User(FE_Base):
|
||||
|
||||
async def render_logout(self, request: Request):
|
||||
resp = RedirectResponse("/gate/", 303)
|
||||
resp.delete_cookie("ARTEMIS_SESH")
|
||||
resp.delete_cookie("DIANA_SESH")
|
||||
return resp
|
||||
|
||||
async def edit_card(self, request: Request) -> RedirectResponse:
|
||||
@ -801,35 +781,6 @@ class FE_System(FE_Base):
|
||||
cabadd={"id": cab_id, "serial": serial},
|
||||
), media_type="text/html; charset=utf-8")
|
||||
|
||||
async def render_logs(self, request: Request):
|
||||
template = self.environment.get_template("core/templates/sys/logs.jinja")
|
||||
events = []
|
||||
|
||||
usr_sesh = self.validate_session(request)
|
||||
if not usr_sesh or not self.test_perm(usr_sesh.permissions, PermissionOffset.SYSADMIN):
|
||||
return RedirectResponse("/sys/?e=11", 303)
|
||||
|
||||
logs = await self.data.base.get_event_log()
|
||||
if not logs:
|
||||
logs = []
|
||||
|
||||
for log in logs:
|
||||
evt = log._asdict()
|
||||
if not evt['user']: evt["user"] = "NONE"
|
||||
if not evt['arcade']: evt["arcade"] = "NONE"
|
||||
if not evt['machine']: evt["machine"] = "NONE"
|
||||
if not evt['ip']: evt["ip"] = "NONE"
|
||||
if not evt['game']: evt["game"] = "NONE"
|
||||
if not evt['version']: evt["version"] = "NONE"
|
||||
evt['when_logged'] = evt['when_logged'].strftime("%x %X")
|
||||
events.append(evt)
|
||||
|
||||
return Response(template.render(
|
||||
title=f"{self.core_config.server.name} | Event Logs",
|
||||
sesh=vars(usr_sesh),
|
||||
events=events
|
||||
), media_type="text/html; charset=utf-8")
|
||||
|
||||
class FE_Arcade(FE_Base):
|
||||
async def render_GET(self, request: Request):
|
||||
template = self.environment.get_template("core/templates/arcade/index.jinja")
|
||||
@ -896,7 +847,7 @@ class FE_Machine(FE_Base):
|
||||
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()
|
||||
if path.exists(f"{cfg_dir}/core.yaml"):
|
||||
cfg.update(yaml.safe_load(open(f"{cfg_dir}/core.yaml")))
|
||||
|
@ -15,18 +15,18 @@
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
</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">
|
||||
<label for="access_code">Access Code or Username</label><br>
|
||||
<input form="login" class="form-control" name="access_code" id="access_code" placeholder="00000000000000000000" maxlength="20" required aria-describedby="access_code_help">
|
||||
<div id="access_code_help" class="form-text">20 Digit access code from a card registered to your account, or your account username. (NOT your username from a game!)</div>
|
||||
<label for="access_code">Card Access Code</label><br>
|
||||
<input form="login" class="form-control" name="access_code" id="access_code" type="number" placeholder="00000000000000000000" maxlength="20" required>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="passwd">Password</label><br>
|
||||
<input id="passwd" class="form-control" name="passwd" type="password" placeholder="password" aria-describedby="passwd_help">
|
||||
<div id="passwd_help" class="form-text">Leave blank if registering for the webui. Your card must have been used on a game connected to this server to register.</div>
|
||||
<input id="passwd" class="form-control" name="passwd" type="password" placeholder="password">
|
||||
</div>
|
||||
<p></p>
|
||||
<input id="submit" class="btn btn-primary" style="display: block; margin: 0 auto;" form="login" type="submit" value="Login">
|
||||
</form>
|
||||
<h6>*To register for the webui, type in the access code of your card, as shown in a game, and leave the password field blank.</h6>
|
||||
<h6>*If you have not registered a card with this server, you cannot create a webui account.</h6>
|
||||
{% endblock content %}
|
@ -51,9 +51,6 @@
|
||||
<button type="submit" class="btn btn-primary">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-sm-6" style="max-width: 25%;">
|
||||
<a href="/sys/logs"><button class="btn btn-primary">Event Logs</button></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="row" id="rowResult" style="margin: 10px;">
|
||||
|
@ -1,198 +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>
|
||||
|
||||
<button class="btn btn-primary" id="btn_prev" disabled onclick="chg_page(-1)"><<</button>
|
||||
<button class="btn btn-primary" id="btn_next" onclick="chg_page(1)">>></button>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
{% if events is defined %}
|
||||
const TBL_DATA = {{events}};
|
||||
{% else %}
|
||||
const TBL_DATA = [];
|
||||
{% endif %}
|
||||
|
||||
var per_page = 0;
|
||||
var page = 0;
|
||||
|
||||
function update_tbl() {
|
||||
if (TBL_DATA.length == 0) { return; }
|
||||
var tbl = document.getElementById("tbl_events");
|
||||
|
||||
for (var i = 0; i < per_page; i++) {
|
||||
try{
|
||||
tbl.deleteRow(1);
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
per_page = document.getElementById("sel_per_page").value;
|
||||
|
||||
if (per_page >= TBL_DATA.length) {
|
||||
page = 0;
|
||||
document.getElementById("btn_next").disabled = true;
|
||||
document.getElementById("btn_prev").disabled = true;
|
||||
}
|
||||
|
||||
for (var i = 0; i < per_page; i++) {
|
||||
let off = (page * per_page) + i;
|
||||
if (off >= TBL_DATA.length) {
|
||||
if (page != 0) {
|
||||
document.getElementById("btn_next").disabled = true;
|
||||
document.getElementById("btn_prev").disabled = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
var data = TBL_DATA[off];
|
||||
var row = tbl.insertRow(i + 1);
|
||||
var cell_severity = row.insertCell(0);
|
||||
switch (data.severity) {
|
||||
case 10:
|
||||
cell_severity.innerHTML = "DEBUG";
|
||||
row.classList.add("table-success");
|
||||
break;
|
||||
case 20:
|
||||
cell_severity.innerHTML = "INFO";
|
||||
row.classList.add("table-info");
|
||||
break;
|
||||
case 30:
|
||||
cell_severity.innerHTML = "WARN";
|
||||
row.classList.add("table-warning");
|
||||
break;
|
||||
case 40:
|
||||
cell_severity.innerHTML = "ERROR";
|
||||
row.classList.add("table-danger");
|
||||
break;
|
||||
case 50:
|
||||
cell_severity.innerHTML = "CRITICAL";
|
||||
row.classList.add("table-danger");
|
||||
break;
|
||||
default:
|
||||
cell_severity.innerHTML = "---";
|
||||
row.classList.add("table-primary");
|
||||
break;
|
||||
}
|
||||
|
||||
var cell_ts = row.insertCell(1);
|
||||
cell_ts.innerHTML = data.when_logged;
|
||||
|
||||
var cell_mod = row.insertCell(2);
|
||||
cell_mod.innerHTML = data.system;
|
||||
|
||||
var cell_name = row.insertCell(3);
|
||||
cell_name.innerHTML = data.type;
|
||||
|
||||
var cell_usr = row.insertCell(4);
|
||||
if (data.user == 'NONE') {
|
||||
cell_usr.innerHTML = "---";
|
||||
} else {
|
||||
cell_usr.innerHTML = "<a href=\"/user/" + data.user + "\">" + data.user + "</a>";
|
||||
}
|
||||
|
||||
var cell_arcade = row.insertCell(5);
|
||||
if (data.arcade == 'NONE') {
|
||||
cell_arcade.innerHTML = "---";
|
||||
} else {
|
||||
cell_arcade.innerHTML = "<a href=\"/shop/" + data.arcade + "\">" + data.arcade + "</a>";
|
||||
}
|
||||
|
||||
var cell_machine = row.insertCell(6);
|
||||
if (data.arcade == 'NONE') {
|
||||
cell_machine.innerHTML = "---";
|
||||
} else {
|
||||
cell_machine.innerHTML = "<a href=\"/cab/" + data.machine + "\">" + data.machine + "</a>";
|
||||
}
|
||||
|
||||
var cell_game = row.insertCell(7);
|
||||
if (data.game == 'NONE') {
|
||||
cell_game.innerHTML = "---";
|
||||
} else {
|
||||
cell_game.innerHTML = data.game;
|
||||
}
|
||||
|
||||
var cell_version = row.insertCell(8);
|
||||
if (data.version == 'NONE') {
|
||||
cell_version.innerHTML = "---";
|
||||
} else {
|
||||
cell_version.innerHTML = data.version;
|
||||
}
|
||||
|
||||
var cell_msg = row.insertCell(9);
|
||||
if (data.message == '') {
|
||||
cell_msg.innerHTML = "---";
|
||||
} else {
|
||||
cell_msg.innerHTML = data.message;
|
||||
}
|
||||
|
||||
var cell_deets = row.insertCell(10);
|
||||
if (data.details == '{}') {
|
||||
cell_deets.innerHTML = "---";
|
||||
} else {
|
||||
cell_deets.innerHTML = data.details;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function chg_page(num) {
|
||||
var max_page = TBL_DATA.length / per_page;
|
||||
console.log(max_page);
|
||||
page = page + num;
|
||||
|
||||
|
||||
if (page > max_page && max_page >= 1) {
|
||||
page = max_page;
|
||||
document.getElementById("btn_next").disabled = true;
|
||||
document.getElementById("btn_prev").disabled = false;
|
||||
return;
|
||||
} else if (page < 0) {
|
||||
page = 0;
|
||||
document.getElementById("btn_next").disabled = false;
|
||||
document.getElementById("btn_prev").disabled = true;
|
||||
return;
|
||||
} else if (page == 0) {
|
||||
document.getElementById("btn_next").disabled = false;
|
||||
document.getElementById("btn_prev").disabled = true;
|
||||
} else {
|
||||
document.getElementById("btn_next").disabled = false;
|
||||
document.getElementById("btn_prev").disabled = false;
|
||||
}
|
||||
|
||||
update_tbl();
|
||||
}
|
||||
|
||||
update_tbl();
|
||||
</script>
|
||||
{% endblock content %}
|
@ -21,8 +21,6 @@ New Nickname too long
|
||||
You must be logged in to preform this action
|
||||
{% elif error == 10 %}
|
||||
Invalid serial number
|
||||
{% elif error == 11 %}
|
||||
Access Denied
|
||||
{% else %}
|
||||
An unknown error occoured
|
||||
{% endif %}
|
||||
|
@ -80,8 +80,7 @@ class BaseServlet:
|
||||
cfg_dir (str): Config directory
|
||||
|
||||
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
|
||||
by this servlette, and offset 2 is mucha netID prefixes that should be used for each game CD.
|
||||
Tuple[bool, str]: Tuple where offset 0 is true if the game is enabled, false otherwise, and offset 1 is the game CD
|
||||
"""
|
||||
return (False, [], [])
|
||||
|
||||
|
@ -49,7 +49,7 @@ class Utils:
|
||||
def get_title_port_ssl(cls, cfg: CoreConfig):
|
||||
if cls.real_title_port_ssl is not None: return cls.real_title_port_ssl
|
||||
|
||||
cls.real_title_port_ssl = cfg.server.proxy_port_ssl if cfg.server.is_using_proxy and cfg.server.proxy_port_ssl else 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
|
||||
|
||||
|
@ -63,7 +63,6 @@ Games listed below have been tested and confirmed working.
|
||||
| 12 | CHUNITHM NEW PLUS!! |
|
||||
| 13 | CHUNITHM SUN |
|
||||
| 14 | CHUNITHM SUN PLUS |
|
||||
| 15 | CHUNITHM LUMINOUS |
|
||||
|
||||
|
||||
### Importer
|
||||
|
@ -22,9 +22,6 @@ version:
|
||||
14:
|
||||
rom: 2.15.00
|
||||
data: 2.15.00
|
||||
15:
|
||||
rom: 2.20.00
|
||||
data: 2.20.00
|
||||
|
||||
crypto:
|
||||
encrypted_only: False
|
||||
|
@ -12,6 +12,3 @@ uploads:
|
||||
photos_dir: ""
|
||||
movies: False
|
||||
movies_dir: ""
|
||||
|
||||
crypto:
|
||||
encrypted_only: False
|
@ -10,7 +10,6 @@ Games listed below have been tested and confirmed working. Only game versions ol
|
||||
|
||||
+ CHUNITHM INTL
|
||||
+ SUPERSTAR
|
||||
+ SUPERSTAR PLUS
|
||||
+ NEW
|
||||
+ NEW PLUS
|
||||
+ SUN
|
||||
|
@ -941,31 +941,6 @@ class ChuniBase:
|
||||
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"}
|
||||
|
||||
@ -994,4 +969,4 @@ class ChuniBase:
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
"userNetBattleData": {"recentNBSelectMusicList": []},
|
||||
}
|
||||
}
|
@ -1,6 +1,3 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ChuniConstants:
|
||||
GAME_CODE = "SDBT"
|
||||
GAME_CODE_NEW = "SDHD"
|
||||
@ -23,7 +20,6 @@ class ChuniConstants:
|
||||
VER_CHUNITHM_NEW_PLUS = 12
|
||||
VER_CHUNITHM_SUN = 13
|
||||
VER_CHUNITHM_SUN_PLUS = 14
|
||||
VER_CHUNITHM_LUMINOUS = 15
|
||||
VERSION_NAMES = [
|
||||
"CHUNITHM",
|
||||
"CHUNITHM PLUS",
|
||||
@ -39,22 +35,9 @@ class ChuniConstants:
|
||||
"CHUNITHM NEW!!",
|
||||
"CHUNITHM NEW PLUS!!",
|
||||
"CHUNITHM SUN",
|
||||
"CHUNITHM SUN PLUS",
|
||||
"CHUNITHM LUMINOUS",
|
||||
"CHUNITHM SUN PLUS"
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def game_ver_to_string(cls, ver: int):
|
||||
return cls.VERSION_NAMES[ver]
|
||||
|
||||
|
||||
class MapAreaConditionType(Enum):
|
||||
UNLOCKED = 0
|
||||
MAP_CLEARED = 1
|
||||
MAP_AREA_CLEARED = 2
|
||||
TROPHY_OBTAINED = 3
|
||||
|
||||
|
||||
class MapAreaConditionLogicalOperator(Enum):
|
||||
AND = 1
|
||||
OR = 2
|
||||
return cls.VERSION_NAMES[ver]
|
@ -1,5 +1,5 @@
|
||||
from typing import List
|
||||
from starlette.routing import Route, Mount
|
||||
from starlette.routing import Route
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response, RedirectResponse
|
||||
from os import path
|
||||
@ -29,13 +29,7 @@ class ChuniFrontend(FE_Base):
|
||||
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:
|
||||
@ -45,165 +39,27 @@ class ChuniFrontend(FE_Base):
|
||||
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 music_chart:
|
||||
if (song.score < 800000):
|
||||
song_rating = 0
|
||||
elif (song.score >= 800000 and song.score < 900000):
|
||||
song_rating = music_chart.level / 2 - 5
|
||||
elif (song.score >= 900000 and song.score < 925000):
|
||||
song_rating = music_chart.level - 5
|
||||
elif (song.score >= 925000 and song.score < 975000):
|
||||
song_rating = music_chart.level - 3
|
||||
elif (song.score >= 975000 and song.score < 1000000):
|
||||
song_rating = (song.score - 975000) / 2500 * 0.1 + music_chart.level
|
||||
elif (song.score >= 1000000 and song.score < 1005000):
|
||||
song_rating = (song.score - 1000000) / 1000 * 0.1 + 1 + music_chart.level
|
||||
elif (song.score >= 1005000 and song.score < 1007500):
|
||||
song_rating = (song.score - 1005000) / 500 * 0.1 + 1.5 + music_chart.level
|
||||
elif (song.score >= 1007500 and song.score < 1009000):
|
||||
song_rating = (song.score - 1007500) / 100 * 0.01 + 2 + music_chart.level
|
||||
elif (song.score >= 1009000):
|
||||
song_rating = 2.15 + music_chart.level
|
||||
song_rating = int(song_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,
|
||||
"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)
|
||||
|
||||
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)
|
||||
), media_type="text/html; charset=utf-8")
|
||||
|
||||
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: str = request.query_params.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:
|
||||
@ -216,31 +72,12 @@ class ChuniFrontend(FE_Base):
|
||||
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):
|
||||
|
||||
if not await self.data.profile.update_name(usr_sesh, 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.info(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.info(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)
|
||||
return RedirectResponse("/gate/?s=1", 303)
|
@ -1,8 +1,7 @@
|
||||
from starlette.requests import Request
|
||||
from starlette.routing import Route
|
||||
from starlette.responses import Response
|
||||
import logging
|
||||
import coloredlogs
|
||||
import logging, coloredlogs
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
import zlib
|
||||
import yaml
|
||||
@ -35,7 +34,6 @@ from .new import ChuniNew
|
||||
from .newplus import ChuniNewPlus
|
||||
from .sun import ChuniSun
|
||||
from .sunplus import ChuniSunPlus
|
||||
from .luminous import ChuniLuminous
|
||||
|
||||
class ChuniServlet(BaseServlet):
|
||||
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
|
||||
@ -63,7 +61,6 @@ class ChuniServlet(BaseServlet):
|
||||
ChuniNewPlus,
|
||||
ChuniSun,
|
||||
ChuniSunPlus,
|
||||
ChuniLuminous,
|
||||
]
|
||||
|
||||
self.logger = logging.getLogger("chuni")
|
||||
@ -106,9 +103,7 @@ class ChuniServlet(BaseServlet):
|
||||
for method in method_list:
|
||||
method_fixed = inflection.camelize(method)[6:-7]
|
||||
# number of iterations was changed to 70 in SUN and then to 36
|
||||
if version == ChuniConstants.VER_CHUNITHM_LUMINOUS:
|
||||
iter_count = 8
|
||||
elif version == ChuniConstants.VER_CHUNITHM_SUN_PLUS:
|
||||
if version == ChuniConstants.VER_CHUNITHM_SUN_PLUS:
|
||||
iter_count = 36
|
||||
elif version == ChuniConstants.VER_CHUNITHM_SUN:
|
||||
iter_count = 70
|
||||
@ -200,23 +195,19 @@ class ChuniServlet(BaseServlet):
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS
|
||||
elif version >= 210 and version < 215: # SUN
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_SUN
|
||||
elif version >= 215 and version < 220: # SUN PLUS
|
||||
elif version >= 215: # SUN
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS
|
||||
elif version >= 220: # LUMINOUS
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS
|
||||
elif game_code == "SDGS": # Int
|
||||
if version < 110: # SUPERSTAR / SUPERSTAR PLUS
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE # SUPERSTAR / SUPERSTAR PLUS worked fine with it
|
||||
if version < 110: # SUPERSTAR
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE # FIXME: Not sure what was intended to go here? was just "PARADISE"
|
||||
elif version >= 110 and version < 115: # NEW
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_NEW
|
||||
elif version >= 115 and version < 120: # NEW PLUS!!
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_NEW_PLUS
|
||||
elif version >= 120 and version < 125: # SUN
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_SUN
|
||||
elif version >= 125 and version < 130: # SUN PLUS
|
||||
elif version >= 125: # SUN PLUS
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_SUN_PLUS
|
||||
elif version >= 130: # LUMINOUS
|
||||
internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS
|
||||
|
||||
if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32:
|
||||
# If we get a 32 character long hex string, it's a hash and we're
|
||||
@ -281,13 +272,7 @@ class ChuniServlet(BaseServlet):
|
||||
self.logger.info(f"v{version} {endpoint} request from {client_ip}")
|
||||
self.logger.debug(req_data)
|
||||
|
||||
if game_code == "SDGS" and version >= 110:
|
||||
endpoint = endpoint.replace("C3Exp", "")
|
||||
elif game_code == "SDGS" and version < 110:
|
||||
endpoint = endpoint.replace("Exp", "")
|
||||
else:
|
||||
endpoint = endpoint
|
||||
|
||||
endpoint = endpoint.replace("C3Exp", "") if game_code == "SDGS" else endpoint
|
||||
func_to_find = "handle_" + inflection.underscore(endpoint) + "_request"
|
||||
handler_cls = self.versions[internal_ver](self.core_cfg, self.game_cfg)
|
||||
|
||||
@ -304,7 +289,7 @@ class ChuniServlet(BaseServlet):
|
||||
self.logger.error(f"Error handling v{version} method {endpoint} - {e}")
|
||||
return Response(zlib.compress(b'{"stat": "0"}'))
|
||||
|
||||
if resp is None:
|
||||
if resp == None:
|
||||
resp = {"returnCode": 1}
|
||||
|
||||
self.logger.debug(f"Response {resp}")
|
||||
@ -322,4 +307,4 @@ class ChuniServlet(BaseServlet):
|
||||
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]),
|
||||
)
|
||||
|
||||
return Response(crypt.encrypt(padded))
|
||||
return Response(crypt.encrypt(padded))
|
@ -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,
|
||||
}
|
@ -32,8 +32,6 @@ class ChuniNew(ChuniBase):
|
||||
return "210"
|
||||
if self.version == ChuniConstants.VER_CHUNITHM_SUN_PLUS:
|
||||
return "215"
|
||||
if self.version == ChuniConstants.VER_CHUNITHM_LUMINOUS:
|
||||
return "220"
|
||||
|
||||
async def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
|
||||
# use UTC time and convert it to JST time by adding +9
|
||||
|
@ -48,8 +48,9 @@ class ChuniReader(BaseReader):
|
||||
for root, dirs, files in walk(f"{root_dir}loginBonusPreset"):
|
||||
for dir in dirs:
|
||||
if path.exists(f"{root}/{dir}/LoginBonusPreset.xml"):
|
||||
with open(f"{root}/{dir}/LoginBonusPreset.xml", "r", encoding="utf-8") as fp:
|
||||
strdata = fp.read()
|
||||
with open(f"{root}/{dir}/LoginBonusPreset.xml", "rb") as fp:
|
||||
bytedata = fp.read()
|
||||
strdata = bytedata.decode("UTF-8")
|
||||
|
||||
xml_root = ET.fromstring(strdata)
|
||||
for name in xml_root.findall("name"):
|
||||
@ -120,8 +121,9 @@ class ChuniReader(BaseReader):
|
||||
for root, dirs, files in walk(evt_dir):
|
||||
for dir in dirs:
|
||||
if path.exists(f"{root}/{dir}/Event.xml"):
|
||||
with open(f"{root}/{dir}/Event.xml", "r", encoding="utf-8") as fp:
|
||||
strdata = fp.read()
|
||||
with open(f"{root}/{dir}/Event.xml", "rb") as fp:
|
||||
bytedata = fp.read()
|
||||
strdata = bytedata.decode("UTF-8")
|
||||
|
||||
xml_root = ET.fromstring(strdata)
|
||||
for name in xml_root.findall("name"):
|
||||
@ -142,8 +144,9 @@ class ChuniReader(BaseReader):
|
||||
for root, dirs, files in walk(music_dir):
|
||||
for dir in dirs:
|
||||
if path.exists(f"{root}/{dir}/Music.xml"):
|
||||
with open(f"{root}/{dir}/Music.xml", "r", encoding='utf-8') as fp:
|
||||
strdata = fp.read()
|
||||
with open(f"{root}/{dir}/Music.xml", "rb") as fp:
|
||||
bytedata = fp.read()
|
||||
strdata = bytedata.decode("UTF-8")
|
||||
|
||||
xml_root = ET.fromstring(strdata)
|
||||
for name in xml_root.findall("name"):
|
||||
@ -207,8 +210,9 @@ class ChuniReader(BaseReader):
|
||||
for root, dirs, files in walk(charge_dir):
|
||||
for dir in dirs:
|
||||
if path.exists(f"{root}/{dir}/ChargeItem.xml"):
|
||||
with open(f"{root}/{dir}/ChargeItem.xml", "r", encoding='utf-8') as fp:
|
||||
strdata = fp.read()
|
||||
with open(f"{root}/{dir}/ChargeItem.xml", "rb") as fp:
|
||||
bytedata = fp.read()
|
||||
strdata = bytedata.decode("UTF-8")
|
||||
|
||||
xml_root = ET.fromstring(strdata)
|
||||
for name in xml_root.findall("name"):
|
||||
@ -236,8 +240,9 @@ class ChuniReader(BaseReader):
|
||||
for root, dirs, files in walk(avatar_dir):
|
||||
for dir in dirs:
|
||||
if path.exists(f"{root}/{dir}/AvatarAccessory.xml"):
|
||||
with open(f"{root}/{dir}/AvatarAccessory.xml", "r", encoding='utf-8') as fp:
|
||||
strdata = fp.read()
|
||||
with open(f"{root}/{dir}/AvatarAccessory.xml", "rb") as fp:
|
||||
bytedata = fp.read()
|
||||
strdata = bytedata.decode("UTF-8")
|
||||
|
||||
xml_root = ET.fromstring(strdata)
|
||||
for name in xml_root.findall("name"):
|
||||
|
@ -243,36 +243,6 @@ matching = Table(
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
cmission = Table(
|
||||
"chuni_item_cmission",
|
||||
metadata,
|
||||
Column("id", Integer, primary_key=True, nullable=False),
|
||||
Column(
|
||||
"user",
|
||||
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
||||
nullable=False,
|
||||
),
|
||||
Column("missionId", Integer, nullable=False),
|
||||
Column("point", Integer),
|
||||
UniqueConstraint("user", "missionId", name="chuni_item_cmission_uk"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
cmission_progress = Table(
|
||||
"chuni_item_cmission_progress",
|
||||
metadata,
|
||||
Column("id", Integer, primary_key=True, nullable=False),
|
||||
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
|
||||
Column("missionId", Integer, nullable=False),
|
||||
Column("order", Integer),
|
||||
Column("stage", Integer),
|
||||
Column("progress", Integer),
|
||||
UniqueConstraint(
|
||||
"user", "missionId", "order", name="chuni_item_cmission_progress_uk"
|
||||
),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
|
||||
class ChuniItemData(BaseData):
|
||||
async def get_oldest_free_matching(self, version: int) -> Optional[Row]:
|
||||
@ -624,66 +594,3 @@ class ChuniItemData(BaseData):
|
||||
)
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def put_cmission_progress(
|
||||
self, user_id: int, mission_id: int, progress_data: Dict
|
||||
) -> Optional[int]:
|
||||
progress_data["user"] = user_id
|
||||
progress_data["missionId"] = mission_id
|
||||
|
||||
sql = insert(cmission_progress).values(**progress_data)
|
||||
conflict = sql.on_duplicate_key_update(**progress_data)
|
||||
result = await self.execute(conflict)
|
||||
|
||||
if result is None:
|
||||
return None
|
||||
|
||||
return result.lastrowid
|
||||
|
||||
async def get_cmission_progress(
|
||||
self, user_id: int, mission_id: int
|
||||
) -> Optional[List[Row]]:
|
||||
sql = cmission_progress.select(
|
||||
and_(
|
||||
cmission_progress.c.user == user_id,
|
||||
cmission_progress.c.missionId == mission_id,
|
||||
)
|
||||
).order_by(cmission_progress.c.order.asc())
|
||||
result = await self.execute(sql)
|
||||
|
||||
if result is None:
|
||||
return None
|
||||
|
||||
return result.fetchall()
|
||||
|
||||
async def get_cmission(self, user_id: int, mission_id: int) -> Optional[Row]:
|
||||
sql = cmission.select(
|
||||
and_(cmission.c.user == user_id, cmission.c.missionId == mission_id)
|
||||
)
|
||||
result = await self.execute(sql)
|
||||
|
||||
if result is None:
|
||||
return None
|
||||
|
||||
return result.fetchone()
|
||||
|
||||
async def put_cmission(self, user_id: int, mission_data: Dict) -> Optional[int]:
|
||||
mission_data["user"] = user_id
|
||||
|
||||
sql = insert(cmission).values(**mission_data)
|
||||
conflict = sql.on_duplicate_key_update(**mission_data)
|
||||
result = await self.execute(conflict)
|
||||
|
||||
if result is None:
|
||||
return None
|
||||
|
||||
return result.lastrowid
|
||||
|
||||
async def get_cmissions(self, user_id: int) -> Optional[List[Row]]:
|
||||
sql = cmission.select(cmission.c.user == user_id)
|
||||
result = await self.execute(sql)
|
||||
|
||||
if result is None:
|
||||
return None
|
||||
|
||||
return result.fetchall()
|
||||
|
@ -412,18 +412,6 @@ rating = Table(
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
net_battle = Table(
|
||||
"chuni_profile_net_battle",
|
||||
metadata,
|
||||
Column("id", Integer, primary_key=True, nullable=False),
|
||||
Column("user", Integer, ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, unique=True),
|
||||
Column("isRankUpChallengeFailed", Boolean),
|
||||
Column("highestBattleRankId", Integer),
|
||||
Column("battleIconId", Integer),
|
||||
Column("battleIconNum", Integer),
|
||||
Column("avatarEffectPoint", Integer),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
class ChuniProfileData(BaseData):
|
||||
async def update_name(self, user_id: int, new_name: str) -> bool:
|
||||
@ -769,54 +757,3 @@ class ChuniProfileData(BaseData):
|
||||
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, user_id: int, net_battle_data: Dict) -> Optional[int]:
|
||||
sql = insert(net_battle).values(
|
||||
user=user_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 {user_id}")
|
||||
|
||||
async def get_net_battle(self, user_id: int, net_battle_data: Dict) -> Optional[Row]:
|
||||
result = await self.execute(net_battle.select(net_battle.c.user == user_id))
|
||||
if result:
|
||||
return result.fetchone()
|
||||
|
@ -190,23 +190,6 @@ class ChuniScoreData(BaseData):
|
||||
return None
|
||||
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]:
|
||||
# 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
|
||||
@ -242,8 +225,6 @@ class ChuniScoreData(BaseData):
|
||||
# Calculates the ROM version that should be fetched for rankings, based on the game version being retrieved
|
||||
# This prevents tracks that are not accessible in your version from counting towards the 10 results
|
||||
romVer = {
|
||||
15: "2.20%",
|
||||
14: "2.15%",
|
||||
13: "2.10%",
|
||||
12: "2.05%",
|
||||
11: "2.00%",
|
||||
|
@ -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>
|
@ -1,150 +1,43 @@
|
||||
{% 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.");
|
||||
});
|
||||
<h1>Chunithm</h1>
|
||||
{% if profile is defined and profile is not none and profile.id > 0 %}
|
||||
<script type="text/javascript">
|
||||
function toggle_new_name_form() {
|
||||
let frm = document.getElementById("new_name_form");
|
||||
let btn = document.getElementById("btn_toggle_form");
|
||||
|
||||
if (frm.style['display'] != "") {
|
||||
frm.style['display'] = "";
|
||||
frm.style['max-height'] = "";
|
||||
btn.innerText = "Cancel";
|
||||
} else {
|
||||
frm.style['display'] = "none";
|
||||
frm.style['max-height'] = "0px";
|
||||
btn.innerText = "Edit";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<h3>Profile for {{ profile.userName }} <button onclick="toggle_new_name_form()" class="btn btn-secondary" id="btn_toggle_form">Edit</button></h3>
|
||||
{% if error is defined %}
|
||||
{% include "core/templates/widgets/err_banner.jinja" %}
|
||||
{% endif %}
|
||||
{% if success is defined and success == 1 %}
|
||||
<div style="background-color: #00AA00; padding: 20px; margin-bottom: 10px; width: 15%;">
|
||||
Update successful
|
||||
</div>
|
||||
{% endif %}
|
||||
<form style="max-width: 33%; display: none; max-height: 0px;" action="/game/chuni/update.name" method="post" id="new_name_form">
|
||||
<div class="mb-3">
|
||||
<label for="new_name" class="form-label">New Trainer Name</label>
|
||||
<input type="text" class="form-control" id="new_name" name="new_name" aria-describedby="new_name_help" maxlength="14">
|
||||
<div id="new_name_help" class="form-text">Must be 14 characters or less</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
{% 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 %}
|
||||
{% endblock content %}
|
@ -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] }}  {{ 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>
|
||||
 
|
||||
</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 %}
|
@ -1,79 +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 }}    Player 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>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 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>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 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 %}
|
@ -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%);
|
||||
}
|
||||
}
|
@ -51,7 +51,7 @@ class CardMakerBase:
|
||||
{
|
||||
"modelKind": 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
|
||||
{
|
||||
|
@ -40,7 +40,6 @@ class CxbRev(CxbBase):
|
||||
score_data["slow2"],
|
||||
score_data["fail"],
|
||||
score_data["combo"],
|
||||
score_data["grade"],
|
||||
)
|
||||
return {"data": True}
|
||||
return {"data": True}
|
||||
|
@ -39,7 +39,6 @@ playlog = Table(
|
||||
Column("slow2", Integer),
|
||||
Column("fail", Integer),
|
||||
Column("combo", Integer),
|
||||
Column("grade", Integer),
|
||||
Column("date_scored", TIMESTAMP, server_default=func.now()),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
@ -105,7 +104,6 @@ class CxbScoreData(BaseData):
|
||||
this_slow2: int,
|
||||
fail: int,
|
||||
combo: int,
|
||||
grade: int,
|
||||
) -> Optional[int]:
|
||||
"""
|
||||
Add an entry to the user's play log
|
||||
@ -125,7 +123,6 @@ class CxbScoreData(BaseData):
|
||||
slow2=this_slow2,
|
||||
fail=fail,
|
||||
combo=combo,
|
||||
grade=grade,
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
|
@ -2,10 +2,8 @@ from titles.diva.index import DivaServlet
|
||||
from titles.diva.const import DivaConstants
|
||||
from titles.diva.database import DivaData
|
||||
from titles.diva.read import DivaReader
|
||||
from .frontend import DivaFrontend
|
||||
|
||||
index = DivaServlet
|
||||
database = DivaData
|
||||
reader = DivaReader
|
||||
frontend = DivaFrontend
|
||||
game_codes = [DivaConstants.GAME_CODE]
|
||||
|
@ -431,7 +431,7 @@ class DivaBase:
|
||||
profile = await self.data.profile.get_profile(data["pd_id"], self.version)
|
||||
profile_shop = await self.data.item.get_shop(data["pd_id"], self.version)
|
||||
if profile is None:
|
||||
return {}
|
||||
return
|
||||
|
||||
mdl_have = "F" * 250
|
||||
# generate the mdl_have string if "unlock_all_modules" is disabled
|
||||
|
@ -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 = "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)
|
@ -79,7 +79,7 @@ class DivaServlet(BaseServlet):
|
||||
|
||||
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()
|
||||
url_header = request.headers
|
||||
|
||||
@ -98,17 +98,8 @@ class DivaServlet(BaseServlet):
|
||||
self.logger.info(f"Binary {bin_req_data['cmd']} Request")
|
||||
self.logger.debug(bin_req_data)
|
||||
|
||||
try:
|
||||
handler = getattr(self.base, f"handle_{bin_req_data['cmd']}_request")
|
||||
resp = handler(bin_req_data)
|
||||
|
||||
except AttributeError as e:
|
||||
self.logger.warning(f"Unhandled {bin_req_data['cmd']} request {e}")
|
||||
return PlainTextResponse(f"cmd={bin_req_data['cmd']}&req_id={bin_req_data['req_id']}&stat=ok")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error handling method {e}")
|
||||
return PlainTextResponse(f"cmd={bin_req_data['cmd']}&req_id={bin_req_data['req_id']}&stat=ok")
|
||||
handler = getattr(self.base, f"handle_{bin_req_data['cmd']}_request")
|
||||
resp = handler(bin_req_data)
|
||||
|
||||
self.logger.debug(
|
||||
f"Response cmd={bin_req_data['cmd']}&req_id={bin_req_data['req_id']}&stat=ok{resp}"
|
||||
|
@ -183,11 +183,7 @@ class DivaReader(BaseReader):
|
||||
pv_list[pv_id] = self.add_branch(pv_list[pv_id], key_args, val)
|
||||
|
||||
for pv_id, pv_data in pv_list.items():
|
||||
try:
|
||||
song_id = int(pv_id.split("_")[1])
|
||||
except ValueError:
|
||||
self.logger.error(f"Invalid song ID format: {pv_id}")
|
||||
continue
|
||||
song_id = int(pv_id.split("_")[1])
|
||||
if "songinfo" not in pv_data:
|
||||
continue
|
||||
if "illustrator" not in pv_data["songinfo"]:
|
||||
|
@ -54,7 +54,7 @@ class DivaCustomizeItemData(BaseData):
|
||||
Given a game version and an aime id, return the cstmz_itm_have hex string
|
||||
required for diva directly
|
||||
"""
|
||||
items_list = await self.get_customize_items(aime_id, version)
|
||||
items_list = self.get_customize_items(aime_id, version)
|
||||
if items_list is None:
|
||||
items_list = []
|
||||
item_have = 0
|
||||
|
@ -50,7 +50,7 @@ class DivaModuleData(BaseData):
|
||||
Given a game version and an aime id, return the mdl_have hex string
|
||||
required for diva directly
|
||||
"""
|
||||
module_list = await self.get_modules(aime_id, version)
|
||||
module_list = self.get_modules(aime_id, version)
|
||||
if module_list is None:
|
||||
module_list = []
|
||||
module_have = 0
|
||||
|
@ -90,7 +90,7 @@ class DivaProfileData(BaseData):
|
||||
return None
|
||||
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
|
||||
which are the diva_profile Columns
|
||||
@ -102,9 +102,7 @@ class DivaProfileData(BaseData):
|
||||
self.logger.error(
|
||||
f"update_profile: failed to update profile! profile: {aime_id}"
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
return None
|
||||
|
||||
async def get_profile(self, aime_id: int, version: int) -> Optional[List[Dict]]:
|
||||
"""
|
||||
|
@ -239,23 +239,3 @@ class DivaScoreData(BaseData):
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def get_playlogs(self, aime_id: int, idx: int = 0, limit: int = 0) -> Optional[Row]:
|
||||
sql = select(playlog).where(playlog.c.user == aime_id).order_by(playlog.c.date_scored.desc())
|
||||
|
||||
if limit:
|
||||
sql = sql.limit(limit)
|
||||
if idx:
|
||||
sql = sql.offset(idx)
|
||||
|
||||
result = await self.execute(sql)
|
||||
if result:
|
||||
return result.fetchall()
|
||||
|
||||
async def get_user_playlogs_count(self, aime_id: int) -> Optional[int]:
|
||||
sql = select(func.count()).where(playlog.c.user == aime_id)
|
||||
result = await self.execute(sql)
|
||||
if result is None:
|
||||
self.logger.warning(f"aimu_id {aime_id} has no scores ")
|
||||
return None
|
||||
return result.scalar()
|
||||
|
@ -1,195 +0,0 @@
|
||||
.diva-header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
ul.diva-navi {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background-color: #333;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
ul.diva-navi li {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
ul.diva-navi li a {
|
||||
display: block;
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 14px 16px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
ul.diva-navi li a:hover:not(.active) {
|
||||
background-color: #111;
|
||||
}
|
||||
|
||||
ul.diva-navi li a.active {
|
||||
background-color: #4CAF50;
|
||||
}
|
||||
|
||||
ul.diva-navi li.right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
|
||||
ul.diva-navi li.right,
|
||||
ul.diva-navi li {
|
||||
float: none;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
border-collapse: separate;
|
||||
overflow: hidden;
|
||||
background-color: #555555;
|
||||
|
||||
}
|
||||
|
||||
th, td {
|
||||
text-align: left;
|
||||
border: none;
|
||||
|
||||
}
|
||||
|
||||
th {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.table-rowdistinct tr:nth-child(even) {
|
||||
background-color: #303030;
|
||||
}
|
||||
|
||||
.table-rowdistinct tr:nth-child(odd) {
|
||||
background-color: #555555;
|
||||
}
|
||||
|
||||
caption {
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.table-large {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.table-large th,
|
||||
.table-large td {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.table-small {
|
||||
width: 100%;
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
.table-small th,
|
||||
.table-small td {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.bg-card {
|
||||
background-color: #555555;
|
||||
}
|
||||
|
||||
.card-hover {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.basic {
|
||||
color: #28a745;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hard {
|
||||
color: #ffc107;
|
||||
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.expert {
|
||||
color: #dc3545;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.master {
|
||||
color: #dd09e8;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ultimate {
|
||||
color: #000000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.score {
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.rainbow {
|
||||
background: linear-gradient(to right, red, yellow, lime, aqua, blue, fuchsia) 0 / 5em;
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.platinum {
|
||||
color: #FFFF00;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.gold {
|
||||
color: #FFFF00;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.scrolling-text {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scrolling-text p {
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
|
||||
}
|
||||
|
||||
.scrolling-text h6 {
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
|
||||
}
|
||||
|
||||
.scrolling-text h5 {
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
|
||||
}
|
||||
|
||||
.scrolling {
|
||||
animation: scroll 10s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes scroll {
|
||||
0% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
<div class="diva-header">
|
||||
<h1>diva</h1>
|
||||
<ul class="diva-navi">
|
||||
<li><a class="nav-link" href="/game/diva/">PROFILE</a></li>
|
||||
<li><a class="nav-link" href="/game/diva/playlog/">RECORD</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
var currentPath = window.location.pathname;
|
||||
if (currentPath === '/game/diva/') {
|
||||
$('.nav-link[href="/game/diva/"]').addClass('active');
|
||||
} else if (currentPath.startsWith('/game/diva/playlog/')) {
|
||||
$('.nav-link[href="/game/diva/playlog/"]').addClass('active');
|
||||
}
|
||||
});
|
||||
</script>
|
@ -1,111 +0,0 @@
|
||||
{% extends "core/templates/index.jinja" %}
|
||||
{% block content %}
|
||||
<style>
|
||||
{% include 'titles/diva/templates/css/diva_style.css' %}
|
||||
</style>
|
||||
<div class="container">
|
||||
{% include 'titles/diva/templates/diva_header.jinja' %}
|
||||
{% if profile is defined and profile is not none and profile|length > 0 %}
|
||||
<div class="row">
|
||||
<div class="col-lg-8 m-auto mt-3">
|
||||
<div class="card bg-card rounded">
|
||||
<table class="table-large table-rowdistinct">
|
||||
<caption align="top" class="text-center">OVERVIEW</caption>
|
||||
<tr>
|
||||
<th>Player name:</th>
|
||||
<th>{{ profile[3] }}</th>
|
||||
<th>
|
||||
<button type="button" class="btn btn-primary btn-sm" data-bs-toggle="modal"
|
||||
data-bs-target="#name_change">Edit</button>
|
||||
</th>
|
||||
<th>Level string:</th>
|
||||
<th>{{ profile[4] }}</th>
|
||||
<th>
|
||||
<button type="button" class="btn btn-primary btn-sm" data-bs-toggle="modal"
|
||||
data-bs-target="#lv_change">Edit</button>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Lvl:</td>
|
||||
<td>{{ profile[5] }}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Lvl points:</td>
|
||||
<td>{{ profile[6] }}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Vocaloid points:</td>
|
||||
<td>{{ profile[7] }}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if error is defined %}
|
||||
{% include "core/templates/widgets/err_banner.jinja" %}
|
||||
{% endif %}
|
||||
{% elif sesh is defined and sesh is not none and sesh.user_id > 0 %}
|
||||
No profile information found for this account.
|
||||
{% else %}
|
||||
Login to view profile information.
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="modal fade" id="name_change" tabindex="-1" aria-labelledby="name_change_label" data-bs-theme="dark"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Name change</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="new_name_form" action="/game/diva/update.name" method="post" style="outline: 0;">
|
||||
<label class="form-label" for="new_name">new name:</label>
|
||||
<input class="form-control" aria-describedby="newNameHelp" form="new_name_form" id="new_name"
|
||||
name="new_name" maxlength="14" type="text" required>
|
||||
<div id="newNameHelp" class="form-text">name must be full-width character string.
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<input type=submit class="btn btn-primary" type="button" form="new_name_form">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="lv_change" tabindex="-1" aria-labelledby="lv_change_label" data-bs-theme="dark"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Level string change</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="new_lv_form" action="/game/diva/update.lv" method="post" style="outline: 0;">
|
||||
<label class="form-label" for="new_lv">new level string:</label>
|
||||
<input class="form-control" aria-describedby="newLvHelp" form="new_lv_form" id="new_lv" name="new_lv"
|
||||
maxlength="14" type="text" required>
|
||||
<div id="newLvHelp" class="form-text">level string must be full-width character string.
|
||||
</div>
|
||||
</form>
|
||||
<div class="modal-footer">
|
||||
<input type=submit class="btn btn-primary" type="button" form="new_lv_form">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,169 +0,0 @@
|
||||
{% extends "core/templates/index.jinja" %}
|
||||
{% block content %}
|
||||
<style>
|
||||
{% include 'titles/diva/templates/css/diva_style.css' %}
|
||||
</style>
|
||||
<div class="container">
|
||||
{% include 'titles/diva/templates/diva_header.jinja' %}
|
||||
{% if playlog is defined and playlog is not none %}
|
||||
<div class="row">
|
||||
<h4 style="text-align: center;">Score counts: {{ playlog_count }}</h4>
|
||||
{% set difficultyName = ['easy', 'normal', 'hard', 'extreme', 'extra extreme'] %}
|
||||
{% set clearState = ['MISSxTAKE', 'STANDARD', 'GREAT', 'EXELLENT', 'PERFECT'] %}
|
||||
{% for record in playlog %}
|
||||
<div class="col-lg-6 mt-3">
|
||||
<div class="card bg-card rounded card-hover">
|
||||
<div class="card bg-card rounded card-hover">
|
||||
<div class="card-header row">
|
||||
<div class="col-8 scrolling-text">
|
||||
<h5 class="card-text">{{ record.title }}</h5>
|
||||
<br>
|
||||
<h6 class="card-text">{{ record.vocaloid_arranger }}</h6>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<h6 class="card-text">{{record.raw.date_scored}}</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body row">
|
||||
<div class="col-3" style="text-align: center;">
|
||||
<h4 class="card-text">{{ record.raw.score }}</h4>
|
||||
<h2>{{ record.raw.atn_pnt / 100 }}%</h2>
|
||||
<h6>{{ difficultyName[record.raw.difficulty] }}</h6>
|
||||
</div>
|
||||
<div class="col-6" style="text-align: center;">
|
||||
<table class="table-small table-rowdistinc">
|
||||
<tr>
|
||||
<td>COOL</td>
|
||||
<td>{{ record.raw.cool }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>FINE</td>
|
||||
<td>{{ record.raw.fine }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SAFE</td>
|
||||
<td>{{ record.raw.safe }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SAD</td>
|
||||
<td>{{ record.raw.sad }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WORST</td>
|
||||
<td>{{ record.raw.worst }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-3" style="text-align: center;">
|
||||
<h6>{{ record.raw.max_combo }}</h6>
|
||||
{% if record.raw.clr_kind == -1 %}
|
||||
<h6>{{ clearState[0] }}</h6>
|
||||
{% elif record.raw.clr_kind == 2 %}
|
||||
<h6>{{ clearState[1] }}</h6>
|
||||
{% elif record.raw.clr_kind == 3 %}
|
||||
<h6>{{ clearState[2] }}</h6>
|
||||
{% elif record.raw.clr_kind == 4 %}
|
||||
<h6>{{ clearState[3] }}</h6>
|
||||
{% elif record.raw.clr_kind == 5 %}
|
||||
<h6>{{ clearState[4] }}</h6>
|
||||
{% endif %}
|
||||
{% if record.raw.clr_kind == -1 %}
|
||||
<h6>NOT CLEAR</h6>
|
||||
{% else %}
|
||||
<h6>CLEAR</h6>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% set playlog_pages = playlog_count // 20 + 1 %}
|
||||
{% elif sesh is defined and sesh is not none and sesh.user_id > 0 %}
|
||||
No Score information found for this account.
|
||||
{% else %}
|
||||
Login to view profile information.
|
||||
{% endif %}
|
||||
</div>
|
||||
<footer class="navbar-fixed-bottom">
|
||||
<nav aria-label="Score page navication">
|
||||
<ul class="pagination justify-content-center mt-3">
|
||||
<li class="page-item"><a id="prev_page" class="page-link" href="#">Previous</a></li>
|
||||
<li class="page-item"><a id="first_page" class="page-link" href="/game/diva/playlog/">1</a></li>
|
||||
<li class="page-item"><a id="prev_3_page" class="page-link" href="">...</a></li>
|
||||
<li class="page-item"><a id="front_page" class="page-link" href="">2</a></li>
|
||||
<li class="page-item"><a id="cur_page" class="page-link active" href="">3</a></li>
|
||||
<li class="page-item"><a id="back_page" class="page-link" href="">4</a></li>
|
||||
<li class="page-item"><a id="next_3_page" class="page-link" href="">...</a></li>
|
||||
<li class="page-item"><a id="last_page" class="page-link" href="/game/diva/playlog/{{ playlog_pages }}">{{
|
||||
playlog_pages }}</a></li>
|
||||
<li class="page-item"><a id="next_page" class="page-link" href="#">Next</a></li>
|
||||
 
|
||||
</ul>
|
||||
</nav>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('.scrolling-text p, .scrolling-text h1, .scrolling-text h2, .scrolling-text h3, .scrolling-text h4, .scrolling-text h5, .scrolling-text h6').each(function () {
|
||||
var parentWidth = $(this).parent().width();
|
||||
var elementWidth = $(this).outerWidth();
|
||||
var elementWidthWithPadding = $(this).outerWidth(true);
|
||||
|
||||
if (elementWidthWithPadding > parentWidth) {
|
||||
$(this).addClass('scrolling');
|
||||
}
|
||||
});
|
||||
|
||||
var currentUrl = window.location.pathname;
|
||||
var currentPage = parseInt(currentUrl.split('/').pop());
|
||||
var rootUrl = '/game/diva/playlog/';
|
||||
var scorePages = {{ playlog_pages }};
|
||||
if (Number.isNaN(currentPage)) {
|
||||
currentPage = 1;
|
||||
}
|
||||
$('#cur_page').text(currentPage);
|
||||
$('#prev_page').attr('href', rootUrl + (currentPage - 1))
|
||||
$('#next_page').attr('href', rootUrl + (currentPage + 1))
|
||||
$('#front_page').attr('href', rootUrl + (currentPage - 1))
|
||||
$('#front_page').text(currentPage - 1);
|
||||
$('#back_page').attr('href', rootUrl + (currentPage + 1))
|
||||
$('#back_page').text(currentPage + 1);
|
||||
$('#prev_3_page').attr('href', rootUrl + (currentPage - 3))
|
||||
$('#next_3_page').attr('href', rootUrl + (currentPage + 3))
|
||||
if ((currentPage - 1) < 3) {
|
||||
$('#prev_3_page').hide();
|
||||
if ((currentPage - 1) < 2) {
|
||||
$('#front_page').hide();
|
||||
if (currentPage === 1) {
|
||||
$('#first_page').hide();
|
||||
$('#prev_page').addClass('disabled');
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((scorePages - currentPage) < 3) {
|
||||
$('#next_3_page').hide();
|
||||
if ((scorePages - currentPage) < 2) {
|
||||
$('#back_page').hide();
|
||||
if (currentPage === scorePages) {
|
||||
$('#last_page').hide();
|
||||
$('#next_page').addClass('disabled');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$('#go_button').click(function () {
|
||||
var pageNumber = parseInt($('#page_input').val());
|
||||
|
||||
if (!Number.isNaN(pageNumber) && pageNumber <= scorePages && pageNumber >= 0) {
|
||||
var url = '/game/diva/playlog/' + pageNumber;
|
||||
window.location.href = url;
|
||||
} else {
|
||||
$('#page_input').val('');
|
||||
$('#page_input').attr('placeholder', 'invalid input!');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock content %}
|
@ -2,12 +2,10 @@ from titles.mai2.index import Mai2Servlet
|
||||
from titles.mai2.const import Mai2Constants
|
||||
from titles.mai2.database import Mai2Data
|
||||
from titles.mai2.read import Mai2Reader
|
||||
from .frontend import Mai2Frontend
|
||||
|
||||
index = Mai2Servlet
|
||||
database = Mai2Data
|
||||
reader = Mai2Reader
|
||||
frontend = Mai2Frontend
|
||||
game_codes = [
|
||||
Mai2Constants.GAME_CODE_DX,
|
||||
Mai2Constants.GAME_CODE_FINALE,
|
||||
|
@ -4,7 +4,6 @@ import logging
|
||||
from base64 import b64decode
|
||||
from os import path, stat, remove
|
||||
from PIL import ImageFile
|
||||
from random import randint
|
||||
|
||||
import pytz
|
||||
from core.config import CoreConfig
|
||||
@ -27,10 +26,10 @@ class Mai2Base:
|
||||
self.date_time_format = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
if not self.core_config.server.is_using_proxy and Utils.get_title_port(self.core_config) != 80:
|
||||
self.old_server = f"http://{self.core_config.server.hostname}:{Utils.get_title_port(cfg)}/SDEY/197/MaimaiServlet/"
|
||||
self.old_server = f"http://{self.core_config.server.hostname}:{Utils.get_title_port(cfg)}/197/MaimaiServlet/"
|
||||
|
||||
else:
|
||||
self.old_server = f"http://{self.core_config.server.hostname}/SDEY/197/MaimaiServlet/"
|
||||
self.old_server = f"http://{self.core_config.server.hostname}/197/MaimaiServlet/"
|
||||
|
||||
async def handle_get_game_setting_api_request(self, data: Dict):
|
||||
# if reboot start/end time is not defined use the default behavior of being a few hours ago
|
||||
@ -887,45 +886,3 @@ class Mai2Base:
|
||||
self.logger.error(f"Failed to delete {out_name}.bin, please remove it manually")
|
||||
|
||||
return {'returnCode': ret_code, 'apiName': 'UploadUserPhotoApi'}
|
||||
|
||||
async def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict:
|
||||
user_id = data.get("userId", 0)
|
||||
kind = data.get("kind", 0) # 1 is fav music, 2 is rival user IDs
|
||||
next_index = data.get("nextIndex", 0)
|
||||
max_ct = data.get("maxCount", 100) # always 100
|
||||
is_all = data.get("isAllFavoriteItem", False) # always false
|
||||
id_list: List[Dict] = []
|
||||
|
||||
if user_id:
|
||||
if kind == 1:
|
||||
fav_music = await self.data.item.get_fav_music(user_id)
|
||||
if fav_music:
|
||||
for fav in fav_music:
|
||||
id_list.append({"orderId": 0, "id": fav["musicId"]})
|
||||
if len(id_list) >= 100: # Lazy but whatever
|
||||
break
|
||||
|
||||
elif kind == 2:
|
||||
rivals = await self.data.profile.get_rivals_game(user_id)
|
||||
if rivals:
|
||||
for rival in rivals:
|
||||
id_list.append({"orderId": 0, "id": rival["rival"]})
|
||||
|
||||
return {
|
||||
"userId": user_id,
|
||||
"kind": kind,
|
||||
"nextIndex": 0,
|
||||
"userFavoriteItemList": id_list,
|
||||
}
|
||||
|
||||
async def handle_get_user_recommend_rate_music_api_request(self, data: Dict) -> Dict:
|
||||
"""
|
||||
userRecommendRateMusicIdList: list[int]
|
||||
"""
|
||||
return {"userId": data["userId"], "userRecommendRateMusicIdList": []}
|
||||
|
||||
async def handle_get_user_recommend_select_music_api_request(self, data: Dict) -> Dict:
|
||||
"""
|
||||
userRecommendSelectionMusicIdList: list[int]
|
||||
"""
|
||||
return {"userId": data["userId"], "userRecommendSelectionMusicIdList": []}
|
||||
|
@ -17,3 +17,16 @@ class Mai2Buddies(Mai2FestivalPlus):
|
||||
# hardcode lastDataVersion for CardMaker
|
||||
user_data["lastDataVersion"] = "1.40.00"
|
||||
return user_data
|
||||
|
||||
async def handle_get_user_new_item_api_request(self, data: Dict) -> Dict:
|
||||
# TODO: Added in 1.41, implement this?
|
||||
user_id = data["userId"]
|
||||
version = data.get("version", 1041000)
|
||||
user_playlog_list = data.get("userPlaylogList", [])
|
||||
|
||||
return {
|
||||
"userId": user_id,
|
||||
"itemKind": -1,
|
||||
"itemId": -1,
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
from typing import Dict
|
||||
|
||||
from core.config import CoreConfig
|
||||
|
||||
|
||||
@ -72,32 +70,8 @@ class Mai2UploadsConfig:
|
||||
)
|
||||
|
||||
|
||||
class Mai2CryptoConfig:
|
||||
def __init__(self, parent_config: "Mai2Config") -> None:
|
||||
self.__config = parent_config
|
||||
|
||||
@property
|
||||
def keys(self) -> Dict[int, list[str]]:
|
||||
"""
|
||||
in the form of:
|
||||
internal_version: [key, iv, salt]
|
||||
key and iv are hex strings
|
||||
salt is a normal UTF-8 string
|
||||
"""
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "mai2", "crypto", "keys", default={}
|
||||
)
|
||||
|
||||
@property
|
||||
def encrypted_only(self) -> bool:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "mai2", "crypto", "encrypted_only", default=False
|
||||
)
|
||||
|
||||
|
||||
class Mai2Config(dict):
|
||||
def __init__(self) -> None:
|
||||
self.server = Mai2ServerConfig(self)
|
||||
self.deliver = Mai2DeliverConfig(self)
|
||||
self.uploads = Mai2UploadsConfig(self)
|
||||
self.crypto = Mai2CryptoConfig(self)
|
@ -5,7 +5,6 @@ import json
|
||||
from random import randint
|
||||
|
||||
from core.config import CoreConfig
|
||||
from core.utils import Utils
|
||||
from titles.mai2.base import Mai2Base
|
||||
from titles.mai2.config import Mai2Config
|
||||
from titles.mai2.const import Mai2Constants
|
||||
@ -16,15 +15,6 @@ class Mai2DX(Mai2Base):
|
||||
super().__init__(cfg, game_cfg)
|
||||
self.version = Mai2Constants.VER_MAIMAI_DX
|
||||
|
||||
# DX earlier version need a efficient old server uri to work
|
||||
# game will auto add MaimaiServlet endpoint behind return uri
|
||||
# so do not add "MaimaiServlet"
|
||||
if not self.core_config.server.is_using_proxy and Utils.get_title_port(self.core_config) != 80:
|
||||
self.old_server = f"http://{self.core_config.server.hostname}:{Utils.get_title_port(cfg)}/SDEY/197/"
|
||||
|
||||
else:
|
||||
self.old_server = f"http://{self.core_config.server.hostname}/SDEY/197/"
|
||||
|
||||
async def handle_get_game_setting_api_request(self, data: Dict):
|
||||
# if reboot start/end time is not defined use the default behavior of being a few hours ago
|
||||
if self.core_config.title.reboot_start_time == "" or self.core_config.title.reboot_end_time == "":
|
||||
@ -58,10 +48,10 @@ class Mai2DX(Mai2Base):
|
||||
"rebootEndTime": reboot_end,
|
||||
"movieUploadLimit": 100,
|
||||
"movieStatus": 1,
|
||||
"movieServerUri": "",
|
||||
"deliverServerUri": "",
|
||||
"oldServerUri": self.old_server,
|
||||
"usbDlServerUri": "",
|
||||
"movieServerUri": self.old_server + "movie/",
|
||||
"deliverServerUri": self.old_server + "deliver/" if self.can_deliver and self.game_config.deliver.enable else "",
|
||||
"oldServerUri": self.old_server + "old",
|
||||
"usbDlServerUri": self.old_server + "usbdl/" if self.can_deliver and self.game_config.deliver.udbdl_enable else "",
|
||||
"rebootInterval": 0,
|
||||
},
|
||||
"isAouAccession": False,
|
||||
@ -563,76 +553,33 @@ class Mai2DX(Mai2Base):
|
||||
return {"userId": data["userId"], "length": 0, "userRegionList": []}
|
||||
|
||||
async def handle_get_user_rival_data_api_request(self, data: Dict) -> Dict:
|
||||
user_id = data.get("userId", 0)
|
||||
rival_id = data.get("rivalId", 0)
|
||||
user_id = data["userId"]
|
||||
rival_id = data["rivalId"]
|
||||
|
||||
if not user_id or not rival_id: return {}
|
||||
|
||||
rival_pf = await self.data.profile.get_profile_detail(rival_id, self.version)
|
||||
if not rival_pf: return {}
|
||||
|
||||
return {
|
||||
"userId": user_id,
|
||||
"userRivalData": {
|
||||
"rivalId": rival_id,
|
||||
"rivalName": rival_pf['userName']
|
||||
}
|
||||
}
|
||||
"""
|
||||
class UserRivalData:
|
||||
rivalId: int
|
||||
rivalName: str
|
||||
"""
|
||||
return {"userId": user_id, "userRivalData": {}}
|
||||
|
||||
async def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict:
|
||||
user_id = data.get("userId", 0)
|
||||
rival_id = data.get("rivalId", 0)
|
||||
next_index = data.get("nextIndex", 0)
|
||||
max_ct = 100
|
||||
upper_lim = next_index + max_ct
|
||||
rival_music_list: Dict[int, List] = {}
|
||||
|
||||
songs = await self.data.score.get_best_scores(rival_id)
|
||||
if songs is None:
|
||||
self.logger.debug("handle_get_user_rival_music_api_request: get_best_scores returned None!")
|
||||
return {
|
||||
"userId": user_id,
|
||||
"rivalId": rival_id,
|
||||
"nextIndex": 0,
|
||||
"userRivalMusicList": [] # musicId userRivalMusicDetailList -> level achievement deluxscoreMax
|
||||
}
|
||||
|
||||
num_user_songs = len(songs)
|
||||
|
||||
for x in range(next_index, upper_lim):
|
||||
if x >= num_user_songs:
|
||||
break
|
||||
|
||||
tmp = songs[x]._asdict()
|
||||
if tmp['musicId'] in rival_music_list:
|
||||
rival_music_list[tmp['musicId']].append([{"level": tmp['level'], 'achievement': tmp['achievement'], 'deluxscoreMax': tmp['deluxscoreMax']}])
|
||||
|
||||
else:
|
||||
if len(rival_music_list) >= max_ct:
|
||||
break
|
||||
rival_music_list[tmp['musicId']] = [{"level": tmp['level'], 'achievement': tmp['achievement'], 'deluxscoreMax': tmp['deluxscoreMax']}]
|
||||
|
||||
next_index = 0 if len(rival_music_list) < max_ct or num_user_songs == upper_lim else upper_lim
|
||||
self.logger.info(f"Send rival {rival_id} songs {next_index}-{upper_lim} ({len(rival_music_list)}) out of {num_user_songs} for user {user_id} (next idx {next_index})")
|
||||
|
||||
return {
|
||||
"userId": user_id,
|
||||
"rivalId": rival_id,
|
||||
"nextIndex": next_index,
|
||||
"userRivalMusicList": [{"musicId": x, "userRivalMusicDetailList": y} for x, y in rival_music_list.items()]
|
||||
}
|
||||
|
||||
async def handle_get_user_new_item_api_request(self, data: Dict) -> Dict:
|
||||
# TODO: Added in 1.41, implement this?
|
||||
user_id = data["userId"]
|
||||
version = data.get("version", 1041000)
|
||||
user_playlog_list = data.get("userPlaylogList", [])
|
||||
|
||||
return {
|
||||
"userId": user_id,
|
||||
"itemKind": -1,
|
||||
"itemId": -1,
|
||||
}
|
||||
rival_id = data["rivalId"]
|
||||
next_idx = data["nextIndex"]
|
||||
rival_music_levels = data["userRivalMusicLevelList"]
|
||||
|
||||
"""
|
||||
class UserRivalMusicList:
|
||||
class UserRivalMusicDetailList:
|
||||
level: int
|
||||
achievement: int
|
||||
deluxscoreMax: int
|
||||
|
||||
musicId: int
|
||||
userRivalMusicDetailList: list[UserRivalMusicDetailList]
|
||||
"""
|
||||
return {"userId": user_id, "nextIndex": 0, "userRivalMusicList": []}
|
||||
|
||||
async def handle_get_user_music_api_request(self, data: Dict) -> Dict:
|
||||
user_id = data.get("userId", 0)
|
||||
@ -679,208 +626,3 @@ class Mai2DX(Mai2Base):
|
||||
return ret
|
||||
ret['loginId'] = ret.get('loginCount', 0)
|
||||
return ret
|
||||
|
||||
# CardMaker support added in Universe
|
||||
async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||
p = await self.data.profile.get_profile_detail(data["userId"], self.version)
|
||||
if p is None:
|
||||
return {}
|
||||
|
||||
return {
|
||||
"userName": p["userName"],
|
||||
"rating": p["playerRating"],
|
||||
# hardcode lastDataVersion for CardMaker
|
||||
"lastDataVersion": "1.20.00", # Future versiohs should replace this with the correct version
|
||||
# checks if the user is still logged in
|
||||
"isLogin": False,
|
||||
"isExistSellingCard": True,
|
||||
}
|
||||
|
||||
async def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict:
|
||||
# user already exists, because the preview checks that already
|
||||
p = await self.data.profile.get_profile_detail(data["userId"], self.version)
|
||||
|
||||
cards = await self.data.card.get_user_cards(data["userId"])
|
||||
if cards is None or len(cards) == 0:
|
||||
# This should never happen
|
||||
self.logger.error(
|
||||
f"handle_get_user_data_api_request: Internal error - No cards found for user id {data['userId']}"
|
||||
)
|
||||
return {}
|
||||
|
||||
# get the dict representation of the row so we can modify values
|
||||
user_data = p._asdict()
|
||||
|
||||
# remove the values the game doesn't want
|
||||
user_data.pop("id")
|
||||
user_data.pop("user")
|
||||
user_data.pop("version")
|
||||
|
||||
return {"userId": data["userId"], "userData": user_data}
|
||||
|
||||
async def handle_cm_login_api_request(self, data: Dict) -> Dict:
|
||||
return {"returnCode": 1}
|
||||
|
||||
async def handle_cm_logout_api_request(self, data: Dict) -> Dict:
|
||||
return {"returnCode": 1}
|
||||
|
||||
async def handle_cm_get_selling_card_api_request(self, data: Dict) -> Dict:
|
||||
selling_cards = await self.data.static.get_enabled_cards(self.version)
|
||||
if selling_cards is None:
|
||||
return {"length": 0, "sellingCardList": []}
|
||||
|
||||
selling_card_list = []
|
||||
for card in selling_cards:
|
||||
tmp = card._asdict()
|
||||
tmp.pop("id")
|
||||
tmp.pop("version")
|
||||
tmp.pop("cardName")
|
||||
tmp.pop("enabled")
|
||||
|
||||
tmp["startDate"] = datetime.strftime(
|
||||
tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT
|
||||
)
|
||||
tmp["endDate"] = datetime.strftime(
|
||||
tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT
|
||||
)
|
||||
tmp["noticeStartDate"] = datetime.strftime(
|
||||
tmp["noticeStartDate"], Mai2Constants.DATE_TIME_FORMAT
|
||||
)
|
||||
tmp["noticeEndDate"] = datetime.strftime(
|
||||
tmp["noticeEndDate"], Mai2Constants.DATE_TIME_FORMAT
|
||||
)
|
||||
|
||||
selling_card_list.append(tmp)
|
||||
|
||||
return {"length": len(selling_card_list), "sellingCardList": selling_card_list}
|
||||
|
||||
async def handle_cm_get_user_card_api_request(self, data: Dict) -> Dict:
|
||||
user_cards = await self.data.item.get_cards(data["userId"])
|
||||
if user_cards is None:
|
||||
return {"returnCode": 1, "length": 0, "nextIndex": 0, "userCardList": []}
|
||||
|
||||
max_ct = data["maxCount"]
|
||||
next_idx = data["nextIndex"]
|
||||
start_idx = next_idx
|
||||
end_idx = max_ct + start_idx
|
||||
|
||||
if len(user_cards[start_idx:]) > max_ct:
|
||||
next_idx += max_ct
|
||||
else:
|
||||
next_idx = 0
|
||||
|
||||
card_list = []
|
||||
for card in user_cards:
|
||||
tmp = card._asdict()
|
||||
tmp.pop("id")
|
||||
tmp.pop("user")
|
||||
|
||||
tmp["startDate"] = datetime.strftime(
|
||||
tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT
|
||||
)
|
||||
tmp["endDate"] = datetime.strftime(
|
||||
tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT
|
||||
)
|
||||
card_list.append(tmp)
|
||||
|
||||
return {
|
||||
"returnCode": 1,
|
||||
"length": len(card_list[start_idx:end_idx]),
|
||||
"nextIndex": next_idx,
|
||||
"userCardList": card_list[start_idx:end_idx],
|
||||
}
|
||||
|
||||
async def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict:
|
||||
await self.handle_get_user_item_api_request(data)
|
||||
|
||||
async def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict:
|
||||
characters = await self.data.item.get_characters(data["userId"])
|
||||
|
||||
chara_list = []
|
||||
for chara in characters:
|
||||
chara_list.append(
|
||||
{
|
||||
"characterId": chara["characterId"],
|
||||
# no clue why those values are even needed
|
||||
"point": 0,
|
||||
"count": 0,
|
||||
"level": chara["level"],
|
||||
"nextAwake": 0,
|
||||
"nextAwakePercent": 0,
|
||||
"favorite": False,
|
||||
"awakening": chara["awakening"],
|
||||
"useCount": chara["useCount"],
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"returnCode": 1,
|
||||
"length": len(chara_list),
|
||||
"userCharacterList": chara_list,
|
||||
}
|
||||
|
||||
async def handle_cm_get_user_card_print_error_api_request(self, data: Dict) -> Dict:
|
||||
return {"length": 0, "userPrintDetailList": []}
|
||||
|
||||
async def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict:
|
||||
user_id = data["userId"]
|
||||
upsert = data["userPrintDetail"]
|
||||
|
||||
# set a random card serial number
|
||||
serial_id = "".join([str(randint(0, 9)) for _ in range(20)])
|
||||
|
||||
# calculate start and end date of the card
|
||||
start_date = datetime.utcnow()
|
||||
end_date = datetime.utcnow() + timedelta(days=15)
|
||||
|
||||
user_card = upsert["userCard"]
|
||||
await self.data.item.put_card(
|
||||
user_id,
|
||||
user_card["cardId"],
|
||||
user_card["cardTypeId"],
|
||||
user_card["charaId"],
|
||||
user_card["mapId"],
|
||||
# add the correct start date and also the end date in 15 days
|
||||
start_date,
|
||||
end_date,
|
||||
)
|
||||
|
||||
# get the profile extend to save the new bought card
|
||||
extend = await self.data.profile.get_profile_extend(user_id, self.version)
|
||||
if extend:
|
||||
extend = extend._asdict()
|
||||
# parse the selectedCardList
|
||||
# 6 = Freedom Pass, 4 = Gold Pass (cardTypeId)
|
||||
selected_cards: List = extend["selectedCardList"]
|
||||
|
||||
# if no pass is already added, add the corresponding pass
|
||||
if not user_card["cardTypeId"] in selected_cards:
|
||||
selected_cards.insert(0, user_card["cardTypeId"])
|
||||
|
||||
extend["selectedCardList"] = selected_cards
|
||||
await self.data.profile.put_profile_extend(user_id, self.version, extend)
|
||||
|
||||
# properly format userPrintDetail for the database
|
||||
upsert.pop("userCard")
|
||||
upsert.pop("serialId")
|
||||
upsert["printDate"] = datetime.strptime(upsert["printDate"], "%Y-%m-%d")
|
||||
|
||||
await self.data.item.put_user_print_detail(user_id, serial_id, upsert)
|
||||
|
||||
return {
|
||||
"returnCode": 1,
|
||||
"orderId": 0,
|
||||
"serialId": serial_id,
|
||||
"startDate": datetime.strftime(start_date, Mai2Constants.DATE_TIME_FORMAT),
|
||||
"endDate": datetime.strftime(end_date, Mai2Constants.DATE_TIME_FORMAT),
|
||||
}
|
||||
|
||||
async def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict:
|
||||
return {
|
||||
"returnCode": 1,
|
||||
"orderId": 0,
|
||||
"serialId": data["userPrintlog"]["serialId"],
|
||||
}
|
||||
|
||||
async def handle_cm_upsert_buy_card_api_request(self, data: Dict) -> Dict:
|
||||
return {"returnCode": 1}
|
||||
|
@ -20,6 +20,18 @@ class Mai2Festival(Mai2UniversePlus):
|
||||
|
||||
async def handle_user_login_api_request(self, data: Dict) -> Dict:
|
||||
user_login = await super().handle_user_login_api_request(data)
|
||||
# TODO: Make use of this
|
||||
# useless?
|
||||
user_login["Bearer"] = "ARTEMiSTOKEN"
|
||||
return user_login
|
||||
|
||||
async def handle_get_user_recommend_rate_music_api_request(self, data: Dict) -> Dict:
|
||||
"""
|
||||
userRecommendRateMusicIdList: list[int]
|
||||
"""
|
||||
return {"userId": data["userId"], "userRecommendRateMusicIdList": []}
|
||||
|
||||
async def handle_get_user_recommend_select_music_api_request(self, data: Dict) -> Dict:
|
||||
"""
|
||||
userRecommendSelectionMusicIdList: list[int]
|
||||
"""
|
||||
return {"userId": data["userId"], "userRecommendSelectionMusicIdList": []}
|
||||
|
@ -17,3 +17,22 @@ class Mai2FestivalPlus(Mai2Festival):
|
||||
# hardcode lastDataVersion for CardMaker
|
||||
user_data["lastDataVersion"] = "1.35.00"
|
||||
return user_data
|
||||
|
||||
async def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict:
|
||||
user_id = data.get("userId", 0)
|
||||
kind = data.get("kind", 2)
|
||||
next_index = data.get("nextIndex", 0)
|
||||
max_ct = data.get("maxCount", 100)
|
||||
is_all = data.get("isAllFavoriteItem", False)
|
||||
|
||||
"""
|
||||
class userFavoriteItemList:
|
||||
orderId: int
|
||||
id: int
|
||||
"""
|
||||
return {
|
||||
"userId": user_id,
|
||||
"kind": kind,
|
||||
"nextIndex": 0,
|
||||
"userFavoriteItemList": [],
|
||||
}
|
||||
|
@ -1,190 +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 Mai2Data
|
||||
from .config import Mai2Config
|
||||
from .const import Mai2Constants
|
||||
|
||||
class Mai2Frontend(FE_Base):
|
||||
def __init__(
|
||||
self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str
|
||||
) -> None:
|
||||
super().__init__(cfg, environment)
|
||||
self.data = Mai2Data(cfg)
|
||||
self.game_cfg = Mai2Config()
|
||||
if path.exists(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"):
|
||||
self.game_cfg.update(
|
||||
yaml.safe_load(open(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"))
|
||||
)
|
||||
self.nav_name = "maimai"
|
||||
|
||||
def get_routes(self) -> List[Route]:
|
||||
return [
|
||||
Route("/", self.render_GET, methods=['GET']),
|
||||
Mount("/playlog", routes=[
|
||||
Route("/", self.render_GET_playlog, methods=['GET']),
|
||||
Route("/{index}", self.render_GET_playlog, methods=['GET']),
|
||||
]),
|
||||
Route("/update.name", self.update_name, methods=['POST']),
|
||||
Route("/version.change", self.version_change, methods=['POST']),
|
||||
]
|
||||
|
||||
async def render_GET(self, request: Request) -> bytes:
|
||||
template = self.environment.get_template(
|
||||
"titles/mai2/templates/mai2_index.jinja"
|
||||
)
|
||||
usr_sesh = self.validate_session(request)
|
||||
if not usr_sesh:
|
||||
usr_sesh = UserSession()
|
||||
|
||||
if usr_sesh.user_id > 0:
|
||||
versions = await self.data.profile.get_all_profile_versions(usr_sesh.user_id)
|
||||
profile = []
|
||||
if versions:
|
||||
# maimai_version is -1 means it is not initialized yet, select a default version from existing.
|
||||
if usr_sesh.maimai_version < 0:
|
||||
usr_sesh.maimai_version = versions[0]['version']
|
||||
profile = await self.data.profile.get_profile_detail(usr_sesh.user_id, usr_sesh.maimai_version)
|
||||
versions = [x['version'] for x in versions]
|
||||
|
||||
resp = Response(template.render(
|
||||
title=f"{self.core_config.server.name} | {self.nav_name}",
|
||||
game_list=self.environment.globals["game_list"],
|
||||
sesh=vars(usr_sesh),
|
||||
user_id=usr_sesh.user_id,
|
||||
profile=profile,
|
||||
version_list=Mai2Constants.VERSION_STRING,
|
||||
versions=versions,
|
||||
cur_version=usr_sesh.maimai_version
|
||||
), media_type="text/html; charset=utf-8")
|
||||
|
||||
if usr_sesh.maimai_version >= 0:
|
||||
encoded_sesh = self.encode_session(usr_sesh)
|
||||
resp.delete_cookie("ARTEMIS_SESH")
|
||||
resp.set_cookie("ARTEMIS_SESH", encoded_sesh)
|
||||
return resp
|
||||
|
||||
else:
|
||||
return RedirectResponse("/gate/", 303)
|
||||
|
||||
async def render_GET_playlog(self, request: Request) -> bytes:
|
||||
template = self.environment.get_template(
|
||||
"titles/mai2/templates/mai2_playlog.jinja"
|
||||
)
|
||||
usr_sesh = self.validate_session(request)
|
||||
if not usr_sesh:
|
||||
print("wtf")
|
||||
usr_sesh = UserSession()
|
||||
|
||||
if usr_sesh.user_id > 0:
|
||||
if usr_sesh.maimai_version < 0:
|
||||
print(usr_sesh.maimai_version)
|
||||
return RedirectResponse("/game/mai2/", 303)
|
||||
path_index = request.path_params.get('index')
|
||||
if not path_index or int(path_index) < 1:
|
||||
index = 0
|
||||
else:
|
||||
index = int(path_index) - 1 # 0 and 1 are 1st page
|
||||
user_id = usr_sesh.user_id
|
||||
playlog_count = await self.data.score.get_user_playlogs_count(user_id)
|
||||
if playlog_count < index * 20 :
|
||||
return Response(template.render(
|
||||
title=f"{self.core_config.server.name} | {self.nav_name}",
|
||||
game_list=self.environment.globals["game_list"],
|
||||
sesh=vars(usr_sesh),
|
||||
playlog_count=0
|
||||
), media_type="text/html; charset=utf-8")
|
||||
playlog = await self.data.score.get_playlogs(user_id, index, 20)
|
||||
playlog_with_title = []
|
||||
for record in playlog:
|
||||
music_chart = await self.data.static.get_music_chart(usr_sesh.maimai_version, record.musicId, record.level)
|
||||
if music_chart:
|
||||
difficultyNum=music_chart.chartId
|
||||
difficulty=music_chart.difficulty
|
||||
artist=music_chart.artist
|
||||
title=music_chart.title
|
||||
else:
|
||||
difficultyNum=0
|
||||
difficulty=0
|
||||
artist="unknown"
|
||||
title="musicid: " + str(record.musicId)
|
||||
playlog_with_title.append({
|
||||
"raw": record,
|
||||
"title": title,
|
||||
"difficultyNum": difficultyNum,
|
||||
"difficulty": difficulty,
|
||||
"artist": artist,
|
||||
})
|
||||
return Response(template.render(
|
||||
title=f"{self.core_config.server.name} | {self.nav_name}",
|
||||
game_list=self.environment.globals["game_list"],
|
||||
sesh=vars(usr_sesh),
|
||||
user_id=usr_sesh.user_id,
|
||||
playlog=playlog_with_title,
|
||||
playlog_count=playlog_count
|
||||
), media_type="text/html; charset=utf-8")
|
||||
else:
|
||||
return RedirectResponse("/gate/", 303)
|
||||
|
||||
async def update_name(self, request: Request) -> bytes:
|
||||
usr_sesh = self.validate_session(request)
|
||||
if not usr_sesh:
|
||||
return RedirectResponse("/gate/", 303)
|
||||
|
||||
form_data = await request.form()
|
||||
new_name: str = form_data.get("new_name")
|
||||
new_name_full = ""
|
||||
|
||||
if not new_name:
|
||||
return RedirectResponse("/gate/?e=4", 303)
|
||||
|
||||
if len(new_name) > 8:
|
||||
return RedirectResponse("/gate/?e=8", 303)
|
||||
|
||||
for x in new_name: # FIXME: This will let some invalid characters through atm
|
||||
o = ord(x)
|
||||
try:
|
||||
if o == 0x20:
|
||||
new_name_full += chr(0x3000)
|
||||
elif o < 0x7F and o > 0x20:
|
||||
new_name_full += chr(o + 0xFEE0)
|
||||
elif o <= 0x7F:
|
||||
self.logger.warn(f"Invalid ascii character {o:02X}")
|
||||
return RedirectResponse("/gate/?e=4", 303)
|
||||
else:
|
||||
new_name_full += x
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Something went wrong parsing character {o:04X} - {e}")
|
||||
return RedirectResponse("/gate/?e=4", 303)
|
||||
|
||||
if not await self.data.profile.update_name(usr_sesh.user_id, new_name_full):
|
||||
return RedirectResponse("/gate/?e=999", 303)
|
||||
|
||||
return RedirectResponse("/game/mai2/?s=1", 303)
|
||||
|
||||
async def version_change(self, request: Request):
|
||||
usr_sesh = self.validate_session(request)
|
||||
if not usr_sesh:
|
||||
usr_sesh = UserSession()
|
||||
|
||||
if usr_sesh.user_id > 0:
|
||||
form_data = await request.form()
|
||||
maimai_version = form_data.get("version")
|
||||
self.logger.info(f"version change to: {maimai_version}")
|
||||
if(maimai_version.isdigit()):
|
||||
usr_sesh.maimai_version=int(maimai_version)
|
||||
encoded_sesh = self.encode_session(usr_sesh)
|
||||
self.logger.info(f"Created session with JWT {encoded_sesh}")
|
||||
resp = RedirectResponse("/game/mai2/", 303)
|
||||
resp.set_cookie("ARTEMIS_SESH", encoded_sesh)
|
||||
return resp
|
||||
else:
|
||||
return RedirectResponse("/gate/", 303)
|
@ -6,13 +6,9 @@ import inflection
|
||||
import yaml
|
||||
import logging, coloredlogs
|
||||
import zlib
|
||||
import string
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
from os import path, mkdir
|
||||
from typing import Tuple, List, Dict
|
||||
from Crypto.Hash import MD5
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import pad
|
||||
|
||||
from core.config import CoreConfig
|
||||
from core.utils import Utils
|
||||
@ -36,7 +32,6 @@ class Mai2Servlet(BaseServlet):
|
||||
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
|
||||
super().__init__(core_cfg, cfg_dir)
|
||||
self.game_cfg = Mai2Config()
|
||||
self.hash_table: Dict[int, Dict[str, str]] = {}
|
||||
if path.exists(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"):
|
||||
self.game_cfg.update(
|
||||
yaml.safe_load(open(f"{cfg_dir}/{Mai2Constants.CONFIG_NAME}"))
|
||||
@ -91,37 +86,6 @@ class Mai2Servlet(BaseServlet):
|
||||
level=self.game_cfg.server.loglevel, logger=self.logger, fmt=log_fmt_str
|
||||
)
|
||||
self.logger.initted = True
|
||||
|
||||
for version, keys in self.game_cfg.crypto.keys.items():
|
||||
if version < Mai2Constants.VER_MAIMAI_DX:
|
||||
continue
|
||||
|
||||
if len(keys) < 3:
|
||||
continue
|
||||
|
||||
self.hash_table[version] = {}
|
||||
method_list = [
|
||||
method
|
||||
for method in dir(self.versions[version])
|
||||
if not method.startswith("__")
|
||||
]
|
||||
|
||||
for method in method_list:
|
||||
# handle_method_api_request -> HandleMethodApiRequest
|
||||
# remove the first 6 chars and the final 7 chars to get the canonical
|
||||
# endpoint name.
|
||||
method_fixed = inflection.camelize(method)[6:-7]
|
||||
hash = MD5.new((method_fixed + keys[2]).encode())
|
||||
|
||||
# truncate unused bytes like the game does
|
||||
hashed_name = hash.hexdigest()
|
||||
self.hash_table[version][hashed_name] = method_fixed
|
||||
|
||||
self.logger.debug(
|
||||
"Hashed v%s method %s with %s to get %s",
|
||||
version, method_fixed, keys[2], hashed_name
|
||||
)
|
||||
|
||||
|
||||
@classmethod
|
||||
def is_game_enabled(
|
||||
@ -141,26 +105,31 @@ class Mai2Servlet(BaseServlet):
|
||||
|
||||
def get_routes(self) -> List[Route]:
|
||||
return [
|
||||
Route("/{game:str}/{version:int}/MaimaiServlet/api/movie/{endpoint:str}", self.handle_movie, methods=['GET', 'POST']),
|
||||
Route("/{game:str}/{version:int}/MaimaiServlet/old/{endpoint:str}", self.handle_old_srv),
|
||||
Route("/{game:str}/{version:int}/MaimaiServlet/old/{endpoint:str}/{placeid:str}/{keychip:str}/{userid:int}", self.handle_old_srv_userdata),
|
||||
Route("/{game:str}/{version:int}/MaimaiServlet/old/{endpoint:str}/{userid:int}", self.handle_old_srv_userdata),
|
||||
Route("/{game:str}/{version:int}/MaimaiServlet/old/{endpoint:str}/{userid:int}", self.handle_old_srv_userdata),
|
||||
Route("/{game:str}/{version:int}/MaimaiServlet/usbdl/{endpoint:str}", self.handle_usbdl),
|
||||
Route("/{game:str}/{version:int}/MaimaiServlet/deliver/{endpoint:str}", self.handle_deliver),
|
||||
Route("/{game:str}/{version:int}/MaimaiServlet/{endpoint:str}", self.handle_mai, methods=['POST']),
|
||||
Route("/{version:int}/MaimaiServlet/api/movie/{endpoint:str}", self.handle_movie, methods=['GET', 'POST']),
|
||||
Route("/{version:int}/MaimaiServlet/old/{endpoint:str}", self.handle_old_srv),
|
||||
Route("/{version:int}/MaimaiServlet/old/{endpoint:str}/{placeid:str}/{keychip:str}/{userid:int}", self.handle_old_srv_userdata),
|
||||
Route("/{version:int}/MaimaiServlet/old/{endpoint:str}/{userid:int}", self.handle_old_srv_userdata),
|
||||
Route("/{version:int}/MaimaiServlet/old/{endpoint:str}/{userid:int}", self.handle_old_srv_userdata),
|
||||
Route("/{version:int}/MaimaiServlet/usbdl/{endpoint:str}", self.handle_usbdl),
|
||||
Route("/{version:int}/MaimaiServlet/deliver/{endpoint:str}", self.handle_deliver),
|
||||
Route("/{version:int}/MaimaiServlet/{endpoint:str}", self.handle_mai, methods=['POST']),
|
||||
Route("/{game:str}/{version:int}/Maimai2Servlet/{endpoint:str}", self.handle_mai2, methods=['POST']),
|
||||
]
|
||||
|
||||
def get_allnet_info(self, game_code: str, game_ver: int, keychip: str) -> Tuple[str, str]:
|
||||
if game_code in {Mai2Constants.GAME_CODE_DX, Mai2Constants.GAME_CODE_DX_INT}:
|
||||
path = f"{game_code}/{game_ver}"
|
||||
else:
|
||||
path = game_ver
|
||||
|
||||
if not self.core_cfg.server.is_using_proxy and Utils.get_title_port(self.core_cfg) != 80:
|
||||
return (
|
||||
f"http://{self.core_cfg.server.hostname}:{Utils.get_title_port(self.core_cfg)}/{game_code}/{game_ver}/",
|
||||
f"http://{self.core_cfg.server.hostname}:{Utils.get_title_port(self.core_cfg)}/{path}/",
|
||||
f"{self.core_cfg.server.hostname}",
|
||||
)
|
||||
|
||||
return (
|
||||
f"http://{self.core_cfg.server.hostname}/{game_code}/{game_ver}/",
|
||||
f"http://{self.core_cfg.server.hostname}/{path}/",
|
||||
f"{self.core_cfg.server.hostname}",
|
||||
)
|
||||
|
||||
@ -265,7 +234,7 @@ class Mai2Servlet(BaseServlet):
|
||||
self.logger.error(f"Error handling v{version} method {endpoint} - {e}")
|
||||
return Response(zlib.compress(b'{"returnCode": "0"}'))
|
||||
|
||||
if resp is None:
|
||||
if resp == None:
|
||||
resp = {"returnCode": 1}
|
||||
|
||||
self.logger.debug(f"Response {resp}")
|
||||
@ -283,93 +252,38 @@ class Mai2Servlet(BaseServlet):
|
||||
req_raw = await request.body()
|
||||
internal_ver = 0
|
||||
client_ip = Utils.get_ip_addr(request)
|
||||
encrypted = False
|
||||
|
||||
if game_code == "SDEZ": # JP
|
||||
if version < 110: # 1.0
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX
|
||||
elif version >= 110 and version < 114: # PLUS
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_PLUS
|
||||
elif version >= 114 and version < 117: # Splash
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH
|
||||
elif version >= 117 and version < 120: # Splash PLUS
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS
|
||||
elif version >= 120 and version < 125: # UNiVERSE
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE
|
||||
elif version >= 125 and version < 130: # UNiVERSE PLUS
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS
|
||||
elif version >= 130 and version < 135: # FESTiVAL
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL
|
||||
elif version >= 135 and version < 140: # FESTiVAL PLUS
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS
|
||||
elif version >= 140: # BUDDiES
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES
|
||||
elif game_code == "SDGA": # Int
|
||||
if version < 105: # 1.0
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX
|
||||
elif version >= 105 and version < 110: # PLUS
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_PLUS
|
||||
elif version >= 110 and version < 115: # Splash
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH
|
||||
elif version >= 115 and version < 120: # Splash PLUS
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS
|
||||
elif version >= 120 and version < 125: # UNiVERSE
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE
|
||||
elif version >= 125 and version < 130: # UNiVERSE PLUS
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS
|
||||
elif version >= 130 and version < 135: # FESTiVAL
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL
|
||||
elif version >= 135 and version < 140: # FESTiVAL PLUS
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS
|
||||
if version < 105: # 1.0
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX
|
||||
elif version >= 105 and version < 110: # PLUS
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_PLUS
|
||||
elif version >= 110 and version < 115: # Splash
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH
|
||||
elif version >= 115 and version < 120: # Splash PLUS
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS
|
||||
elif version >= 120 and version < 125: # UNiVERSE
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE
|
||||
elif version >= 125 and version < 130: # UNiVERSE PLUS
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS
|
||||
elif version >= 130 and version < 135: # FESTiVAL
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL
|
||||
elif version >= 135 and version < 140: # FESTiVAL PLUS
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS
|
||||
elif version >= 140: # BUDDiES
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES
|
||||
|
||||
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
|
||||
# dealing with an encrypted request. False positives shouldn't happen
|
||||
# as long as requests are suffixed with `Api`.
|
||||
if internal_ver not in self.hash_table:
|
||||
self.logger.error(
|
||||
"v%s does not support encryption or no keys entered",
|
||||
version,
|
||||
)
|
||||
return Response(zlib.compress(b'{"stat": "0"}'))
|
||||
elif endpoint.lower() not in self.hash_table[internal_ver]:
|
||||
self.logger.error(
|
||||
"No hash found for v%s endpoint %s",
|
||||
version, endpoint
|
||||
)
|
||||
return Response(zlib.compress(b'{"stat": "0"}'))
|
||||
|
||||
endpoint = self.hash_table[internal_ver][endpoint.lower()]
|
||||
|
||||
try:
|
||||
crypt = AES.new(
|
||||
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][0]),
|
||||
AES.MODE_CBC,
|
||||
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]),
|
||||
)
|
||||
|
||||
req_raw = crypt.decrypt(req_raw)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
"Failed to decrypt v%s request to %s",
|
||||
version, endpoint,
|
||||
exc_info=e,
|
||||
)
|
||||
return Response(zlib.compress(b'{"stat": "0"}'))
|
||||
|
||||
encrypted = True
|
||||
|
||||
if (
|
||||
not encrypted
|
||||
and self.game_cfg.crypto.encrypted_only
|
||||
and version >= 110
|
||||
request.headers.get("Mai-Encoding") is not None
|
||||
or request.headers.get("X-Mai-Encoding") is not None
|
||||
):
|
||||
self.logger.error(
|
||||
"Unencrypted v%s %s request, but config is set to encrypted only: %r",
|
||||
version, endpoint, req_raw
|
||||
# The has is some flavor of MD5 of the endpoint with a constant bolted onto the end of it.
|
||||
# See cake.dll's Obfuscator function for details. Hopefully most DLL edits will remove
|
||||
# these two(?) headers to not cause issues, but given the general quality of SEGA data...
|
||||
enc_ver = request.headers.get("Mai-Encoding")
|
||||
if enc_ver is None:
|
||||
enc_ver = request.headers.get("X-Mai-Encoding")
|
||||
self.logger.debug(
|
||||
f"Encryption v{enc_ver} - User-Agent: {request.headers.get('User-Agent')}"
|
||||
)
|
||||
return Response(zlib.compress(b'{"stat": "0"}'))
|
||||
|
||||
try:
|
||||
unzip = zlib.decompress(req_raw)
|
||||
@ -406,26 +320,12 @@ class Mai2Servlet(BaseServlet):
|
||||
self.logger.error(f"Error handling v{version} method {endpoint} - {e}")
|
||||
return Response(zlib.compress(b'{"stat": "0"}'))
|
||||
|
||||
if resp is None:
|
||||
if resp == None:
|
||||
resp = {"returnCode": 1}
|
||||
|
||||
self.logger.debug(f"Response {resp}")
|
||||
|
||||
zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8"))
|
||||
|
||||
if not encrypted or version < 110:
|
||||
return Response(zipped)
|
||||
|
||||
padded = pad(zipped, 16)
|
||||
|
||||
crypt = AES.new(
|
||||
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][0]),
|
||||
AES.MODE_CBC,
|
||||
bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]),
|
||||
)
|
||||
|
||||
return Response(crypt.encrypt(padded))
|
||||
|
||||
return Response(zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")))
|
||||
|
||||
async def handle_old_srv(self, request: Request) -> bytes:
|
||||
endpoint = request.path_params.get('endpoint')
|
||||
|
@ -134,20 +134,6 @@ favorite = Table(
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
fav_music = Table(
|
||||
"mai2_item_favorite_music",
|
||||
metadata,
|
||||
Column("id", Integer, primary_key=True, nullable=False),
|
||||
Column(
|
||||
"user",
|
||||
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
||||
nullable=False,
|
||||
),
|
||||
Column("musicId", Integer, nullable=False),
|
||||
UniqueConstraint("user", "musicId", name="mai2_item_favorite_music_uk"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
charge = Table(
|
||||
"mai2_item_charge",
|
||||
metadata,
|
||||
@ -465,30 +451,6 @@ class Mai2ItemData(BaseData):
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def get_fav_music(self, user_id: int) -> Optional[List[Row]]:
|
||||
result = await self.execute(fav_music.select(fav_music.c.user == user_id))
|
||||
if result:
|
||||
return result.fetchall()
|
||||
|
||||
async def add_fav_music(self, user_id: int, music_id: int) -> Optional[int]:
|
||||
sql = insert(fav_music).values(
|
||||
user = user_id,
|
||||
musicId = music_id
|
||||
)
|
||||
|
||||
conflict = sql.on_duplicate_key_do_nothing()
|
||||
|
||||
result = await self.execute(conflict)
|
||||
if result:
|
||||
return result.lastrowid
|
||||
|
||||
self.logger.error(f"Failed to add music {music_id} as favorite for user {user_id}!")
|
||||
|
||||
async def remove_fav_music(self, user_id: int, music_id: int) -> None:
|
||||
result = await self.execute(fav_music.delete(and_(fav_music.c.user == user_id, fav_music.c.musicId == music_id)))
|
||||
if not result:
|
||||
self.logger.error(f"Failed to remove music {music_id} as favorite for user {user_id}!")
|
||||
|
||||
async def put_card(
|
||||
self,
|
||||
user_id: int,
|
||||
|
@ -491,31 +491,8 @@ consec_logins = Table(
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
rival = Table(
|
||||
"mai2_user_rival",
|
||||
metadata,
|
||||
Column("id", Integer, primary_key=True, nullable=False),
|
||||
Column(
|
||||
"user",
|
||||
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
||||
nullable=False,
|
||||
),
|
||||
Column(
|
||||
"rival",
|
||||
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
||||
nullable=False,
|
||||
),
|
||||
Column("show", Boolean, nullable=False, server_default="0"),
|
||||
UniqueConstraint("user", "rival", name="mai2_user_rival_uk"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
class Mai2ProfileData(BaseData):
|
||||
async def get_all_profile_versions(self, user_id: int) -> Optional[List[Row]]:
|
||||
result = await self.execute(detail.select(detail.c.user == user_id))
|
||||
if result:
|
||||
return result.fetchall()
|
||||
|
||||
async def put_profile_detail(
|
||||
self, user_id: int, version: int, detail_data: Dict, is_dx: bool = True
|
||||
) -> Optional[Row]:
|
||||
@ -866,52 +843,3 @@ class Mai2ProfileData(BaseData):
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
||||
async def get_rivals(self, user_id: int) -> Optional[List[Row]]:
|
||||
result = await self.execute(rival.select(rival.c.user == user_id))
|
||||
if result:
|
||||
return result.fetchall()
|
||||
|
||||
async def get_rivals_game(self, user_id: int) -> Optional[List[Row]]:
|
||||
result = await self.execute(rival.select(and_(rival.c.user == user_id, rival.c.show == True)).limit(3))
|
||||
if result:
|
||||
return result.fetchall()
|
||||
|
||||
async def set_rival_shown(self, user_id: int, rival_id: int, is_shown: bool) -> None:
|
||||
sql = rival.update(and_(rival.c.user == user_id, rival.c.rival == rival_id)).values(
|
||||
show = is_shown
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
if not result:
|
||||
self.logger.error(f"Failed to set rival {rival_id} shown status to {is_shown} for user {user_id}")
|
||||
|
||||
async def add_rival(self, user_id: int, rival_id: int) -> Optional[int]:
|
||||
sql = insert(rival).values(
|
||||
user = user_id,
|
||||
rival = rival_id
|
||||
)
|
||||
|
||||
conflict = sql.on_duplicate_key_do_nothing()
|
||||
|
||||
result = await self.execute(conflict)
|
||||
if result:
|
||||
return result.lastrowid
|
||||
|
||||
self.logger.error(f"Failed to add music {rival_id} as favorite for user {user_id}!")
|
||||
|
||||
async def remove_rival(self, user_id: int, rival_id: int) -> None:
|
||||
result = await self.execute(rival.delete(and_(rival.c.user == user_id, rival.c.rival == rival_id)))
|
||||
if not result:
|
||||
self.logger.error(f"Failed to remove rival {rival_id} for user {user_id}!")
|
||||
|
||||
async def update_name(self, user_id: int, new_name: str) -> bool:
|
||||
sql = detail.update(detail.c.user == user_id).values(
|
||||
userName=new_name
|
||||
)
|
||||
result = await self.execute(sql)
|
||||
|
||||
if result is None:
|
||||
self.logger.warning(f"Failed to set user {user_id} name to {new_name}")
|
||||
return False
|
||||
return True
|
||||
|
@ -319,16 +319,16 @@ class Mai2ScoreData(BaseData):
|
||||
sql = best_score.select(
|
||||
and_(
|
||||
best_score.c.user == user_id,
|
||||
(best_score.c.musicId == song_id) if song_id is not None else True,
|
||||
(best_score.c.song_id == song_id) if song_id is not None else True,
|
||||
)
|
||||
).order_by(best_score.c.musicId).order_by(best_score.c.level)
|
||||
)
|
||||
else:
|
||||
sql = best_score_old.select(
|
||||
and_(
|
||||
best_score_old.c.user == user_id,
|
||||
(best_score_old.c.musicId == song_id) if song_id is not None else True,
|
||||
(best_score_old.c.song_id == song_id) if song_id is not None else True,
|
||||
)
|
||||
).order_by(best_score.c.musicId).order_by(best_score.c.level)
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
if result is None:
|
||||
@ -398,23 +398,3 @@ class Mai2ScoreData(BaseData):
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def get_playlogs(self, user_id: int, idx: int = 0, limit: int = 0) -> Optional[List[Row]]:
|
||||
sql = playlog.select(playlog.c.user == user_id)
|
||||
|
||||
if limit:
|
||||
sql = sql.limit(limit)
|
||||
if idx:
|
||||
sql = sql.offset(idx * limit)
|
||||
|
||||
result = await self.execute(sql)
|
||||
if result:
|
||||
return result.fetchall()
|
||||
|
||||
async def get_user_playlogs_count(self, aime_id: int) -> Optional[Row]:
|
||||
sql = select(func.count()).where(playlog.c.user == aime_id)
|
||||
result = await self.execute(sql)
|
||||
if result is None:
|
||||
self.logger.warning(f"aime_id {aime_id} has no playlog ")
|
||||
return None
|
||||
return result.scalar()
|
||||
|
@ -1,195 +0,0 @@
|
||||
.mai2-header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
ul.mai2-navi {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background-color: #333;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
ul.mai2-navi li {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
ul.mai2-navi li a {
|
||||
display: block;
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 14px 16px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
ul.mai2-navi li a:hover:not(.active) {
|
||||
background-color: #111;
|
||||
}
|
||||
|
||||
ul.mai2-navi li a.active {
|
||||
background-color: #4CAF50;
|
||||
}
|
||||
|
||||
ul.mai2-navi li.right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
|
||||
ul.mai2-navi li.right,
|
||||
ul.mai2-navi li {
|
||||
float: none;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
border-collapse: separate;
|
||||
overflow: hidden;
|
||||
background-color: #555555;
|
||||
|
||||
}
|
||||
|
||||
th, td {
|
||||
text-align: left;
|
||||
border: none;
|
||||
|
||||
}
|
||||
|
||||
th {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.table-rowdistinct tr:nth-child(even) {
|
||||
background-color: #303030;
|
||||
}
|
||||
|
||||
.table-rowdistinct tr:nth-child(odd) {
|
||||
background-color: #555555;
|
||||
}
|
||||
|
||||
caption {
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.table-large {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.table-large th,
|
||||
.table-large td {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.table-small {
|
||||
width: 100%;
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
.table-small th,
|
||||
.table-small td {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.bg-card {
|
||||
background-color: #555555;
|
||||
}
|
||||
|
||||
.card-hover {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.basic {
|
||||
color: #28a745;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hard {
|
||||
color: #ffc107;
|
||||
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.expert {
|
||||
color: #dc3545;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.master {
|
||||
color: #dd09e8;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ultimate {
|
||||
color: #000000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.score {
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.rainbow {
|
||||
background: linear-gradient(to right, red, yellow, lime, aqua, blue, fuchsia) 0 / 5em;
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.platinum {
|
||||
color: #FFFF00;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.gold {
|
||||
color: #FFFF00;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.scrolling-text {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scrolling-text p {
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
|
||||
}
|
||||
|
||||
.scrolling-text h6 {
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
|
||||
}
|
||||
|
||||
.scrolling-text h5 {
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
|
||||
}
|
||||
|
||||
.scrolling {
|
||||
animation: scroll 10s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes scroll {
|
||||
0% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
<div class="mai2-header">
|
||||
<h1>maimai</h1>
|
||||
<ul class="mai2-navi">
|
||||
<li><a class="nav-link" href="/game/mai2/">PROFILE</a></li>
|
||||
<li><a class="nav-link" href="/game/mai2/playlog/">RECORD</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
var currentPath = window.location.pathname;
|
||||
if (currentPath === '/game/mai2/') {
|
||||
$('.nav-link[href="/game/mai2/"]').addClass('active');
|
||||
} else if (currentPath.startsWith('/game/mai2/playlog/')) {
|
||||
$('.nav-link[href="/game/mai2/playlog/"]').addClass('active');
|
||||
}
|
||||
});
|
||||
</script>
|
@ -1,134 +0,0 @@
|
||||
{% extends "core/templates/index.jinja" %}
|
||||
{% block content %}
|
||||
<style>
|
||||
{% include 'titles/mai2/templates/css/mai2_style.css' %}
|
||||
</style>
|
||||
<div class="container">
|
||||
{% include 'titles/mai2/templates/mai2_header.jinja' %}
|
||||
{% if profile is defined and profile is not none and profile|length > 0 %}
|
||||
<div class="row">
|
||||
<div class="col-lg-8 m-auto mt-3">
|
||||
<div class="card bg-card rounded">
|
||||
<table class="table-large table-rowdistinct">
|
||||
<caption align="top">OVERVIEW</caption>
|
||||
<tr>
|
||||
<th>{{ profile.userName }}</th>
|
||||
<th>
|
||||
<button type="button" class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#name_change">Edit</button>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>version:</td>
|
||||
<td>
|
||||
<select name="version" id="version" onChange="changeVersion(this)">
|
||||
{% for ver in versions %}
|
||||
{% if ver == cur_version %}
|
||||
<option value="{{ ver }}" selected>{{ version_list[ver] }}</option>
|
||||
{% else %}
|
||||
<option value="{{ ver }}">{{ version_list[ver] }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% if versions | length > 1 %}
|
||||
<p style="margin-block-end: 0;">You have {{ versions | length }} versions.</p>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Rating:</td>
|
||||
<td>
|
||||
<span class="{% if profile.playerRating >= 15000 %}rainbow{% elif profile.playerRating < 15000 and profile.playerRating >= 14500 %}platinum{% elif profile.playerRating < 14500 and profile.playerRating >=14000 %}platinum{% endif %}">
|
||||
{{ profile.playerRating }}
|
||||
</span>
|
||||
<span>
|
||||
(highest: {{ profile.highestRating }})
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Play Counts:</td>
|
||||
<td>{{ profile.playCount }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Last Play Date:</td>
|
||||
<td>{{ profile.lastPlayDate }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-8 m-auto mt-3">
|
||||
<div class="card bg-card rounded">
|
||||
<table class="table-large table-rowdistinct">
|
||||
<caption align="top">SCORE</caption>
|
||||
<tr>
|
||||
<td>Total Delux Score:</td>
|
||||
<td>{{ profile.totalDeluxscore }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total Basic Delux Score:</td>
|
||||
<td>{{ profile.totalBasicDeluxscore }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total Advanced Delux Score:</td>
|
||||
<td>{{ profile.totalAdvancedDeluxscore }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total Expert Delux Score:</td>
|
||||
<td>{{ profile.totalExpertDeluxscore }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total Master Delux Score:</td>
|
||||
<td>{{ profile.totalMasterDeluxscore }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total ReMaster Delux Score:</td>
|
||||
<td>{{ profile.totalReMasterDeluxscore }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if error is defined %}
|
||||
{% include "core/templates/widgets/err_banner.jinja" %}
|
||||
{% endif %}
|
||||
{% elif sesh is defined and sesh is not none and sesh.user_id > 0 %}
|
||||
No profile information found for this account.
|
||||
{% else %}
|
||||
Login to view profile information.
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="modal fade" id="name_change" tabindex="-1" aria-labelledby="name_change_label" data-bs-theme="dark"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Name change</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="new_name_form" action="/game/mai2/update.name" method="post" style="outline: 0;">
|
||||
<label class="form-label" for="new_name">new name:</label>
|
||||
<input class="form-control" aria-describedby="newNameHelp" form="new_name_form" id="new_name"
|
||||
name="new_name" maxlength="14" type="text" required>
|
||||
<div id="newNameHelp" class="form-text">name must be full-width character string.
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<input type=submit class="btn btn-primary" type="button" form="new_name_form">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function changeVersion(sel) {
|
||||
$.post("/game/mai2/version.change", { version: sel.value })
|
||||
.done(function (data) {
|
||||
location.reload();
|
||||
})
|
||||
.fail(function () {
|
||||
alert("Failed to update version.");
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock content %}
|
@ -1,225 +0,0 @@
|
||||
{% extends "core/templates/index.jinja" %}
|
||||
{% block content %}
|
||||
<style>
|
||||
{% include 'titles/mai2/templates/css/mai2_style.css' %}
|
||||
</style>
|
||||
<div class="container">
|
||||
{% include 'titles/mai2/templates/mai2_header.jinja' %}
|
||||
{% if playlog is defined and playlog is not none %}
|
||||
<div class="row">
|
||||
<h4 style="text-align: center;">Playlog counts: {{ playlog_count }}</h4>
|
||||
{% set rankName = ['D', 'C', 'B', 'BB', 'BBB', 'A', 'AA', 'AAA', 'S', 'S+', 'SS', 'SS+', 'SSS', 'SSS+'] %}
|
||||
{% set difficultyName = ['basic', 'hard', 'expert', 'master', 'ultimate'] %}
|
||||
{% for record in playlog %}
|
||||
<div class="col-lg-6 mt-3">
|
||||
<div class="card bg-card rounded card-hover">
|
||||
<div class="card-header row">
|
||||
<div class="col-8 scrolling-text">
|
||||
<h5 class="card-text"> {{ record.title }} </h5>
|
||||
<br>
|
||||
<h6 class="card-text"> {{ record.artist }} </h6>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<h6 class="card-text">{{ record.raw.userPlayDate }}</h6>
|
||||
<h6 class="card-text">TRACK {{ record.raw.trackNo }}</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body row">
|
||||
<div class="col-3" style="text-align: center;">
|
||||
<h4 class="card-text">{{ record.raw.deluxscore }}</h4>
|
||||
<h2>{{ rankName[record.raw.rank] }}</h2>
|
||||
<h6
|
||||
class="{% if record.raw.level == 0 %}basic{% elif record.raw.level == 1 %}advanced{% elif record.raw.level == 2 %}expert{% elif record.raw.level == 3 %}master{% elif record.raw.level == 4 %}remaster{% endif %}">
|
||||
{{ difficultyName[record.raw.level] }}  {{ record.difficulty }}
|
||||
</h6>
|
||||
</div>
|
||||
<div class="col-6" style="text-align: center;">
|
||||
<table class="table-small table-rowdistinc">
|
||||
<tr>
|
||||
<td>CRITICAL PERFECT</td>
|
||||
<td>
|
||||
Tap: {{ record.raw.tapCriticalPerfect }}<br>
|
||||
Hold: {{ record.raw.holdCriticalPerfect }}<br>
|
||||
Slide: {{ record.raw.slideCriticalPerfect }}<br>
|
||||
Touch: {{ record.raw.touchCriticalPerfect }}<br>
|
||||
Break: {{ record.raw.breakCriticalPerfect }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>PERFECT</td>
|
||||
<td>
|
||||
Tap: {{ record.raw.tapPerfect }}<br>
|
||||
Hold: {{ record.raw.holdPerfect }}<br>
|
||||
Slide: {{ record.raw.slidePerfect }}<br>
|
||||
Touch: {{ record.raw.touchPerfect }}<br>
|
||||
Break: {{ record.raw.breakPerfect }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>GREAT</td>
|
||||
<td>
|
||||
Tap: {{ record.raw.tapGreat }}<br>
|
||||
Hold: {{ record.raw.holdGreat }}<br>
|
||||
Slide: {{ record.raw.slideGreat }}<br>
|
||||
Touch: {{ record.raw.touchGreat }}<br>
|
||||
Break: {{ record.raw.breakGreat }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>GOOD</td>
|
||||
<td>
|
||||
Tap: {{ record.raw.tapGood }}<br>
|
||||
Hold: {{ record.raw.holdGood }}<br>
|
||||
Slide: {{ record.raw.slideGood }}<br>
|
||||
Touch: {{ record.raw.touchGood }}<br>
|
||||
Break: {{ record.raw.breakGood }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MISS</td>
|
||||
<td>
|
||||
Tap: {{ record.raw.tapMiss }}<br>
|
||||
Hold: {{ record.raw.holdMiss }}<br>
|
||||
Slide: {{ record.raw.slideMiss }}<br>
|
||||
Touch: {{ record.raw.touchMiss }}<br>
|
||||
Break: {{ record.raw.breakMiss }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-3" style="text-align: center;">
|
||||
{%if record.raw.comboStatus == 1 %}
|
||||
<h6>FULL COMBO</h6>
|
||||
{% endif %}
|
||||
{%if record.raw.comboStatus == 2 %}
|
||||
<h6>FULL COMBO +</h6>
|
||||
{% endif %}
|
||||
{%if record.raw.comboStatus == 3 %}
|
||||
<h6>ALL PERFECT</h6>
|
||||
{% endif %}
|
||||
{%if record.raw.comboStatus == 4 %}
|
||||
<h6>ALL PERFECT +</h6>
|
||||
{% endif %}
|
||||
{%if record.raw.syncStatus == 1 %}
|
||||
<h6>FULL SYNC</h6>
|
||||
{% endif %}
|
||||
{%if record.raw.syncStatus == 2 %}
|
||||
<h6>FULL SYNC +</h6>
|
||||
{% endif %}
|
||||
{%if record.raw.syncStatus == 3 %}
|
||||
<h6>FULL SYNC DX</h6>
|
||||
{% endif %}
|
||||
{%if record.raw.syncStatus == 4 %}
|
||||
<h6>FULL SYNC DX +</h6>
|
||||
{% endif %}
|
||||
{%if record.raw.isAchieveNewRecord == 1 or record.raw.isDeluxscoreNewRecord == 1 %}
|
||||
<h6>NEW RECORD</h6>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% set playlog_pages = playlog_count // 20 + 1 %}
|
||||
{% elif sesh is defined and sesh is not none and sesh.user_id > 0 %}
|
||||
No Playlog information found for this account.
|
||||
{% else %}
|
||||
Login to view profile information.
|
||||
{% endif %}
|
||||
</div>
|
||||
<footer class="navbar-fixed-bottom">
|
||||
<nav aria-label="Playlog page navigation">
|
||||
<ul class="pagination justify-content-center mt-3">
|
||||
<li class="page-item"><a id="prev_page" class="page-link" href="#">Previous</a></li>
|
||||
<li class="page-item"><a id="first_page" class="page-link" href="/game/mai2/playlog/">1</a></li>
|
||||
<li class="page-item"><a id="prev_3_page" class="page-link" href="">...</a></li>
|
||||
<li class="page-item"><a id="front_page" class="page-link" href="">2</a></li>
|
||||
<li class="page-item"><a id="cur_page" class="page-link active" href="">3</a></li>
|
||||
<li class="page-item"><a id="back_page" class="page-link" href="">4</a></li>
|
||||
<li class="page-item"><a id="next_3_page" class="page-link" href="">...</a></li>
|
||||
<li class="page-item"><a id="last_page" class="page-link" href="/game/mai2/playlog/{{ playlog_pages }}">{{
|
||||
playlog_pages }}</a></li>
|
||||
<li class="page-item"><a id="next_page" class="page-link" href="#">Next</a></li>
|
||||
 
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="row">
|
||||
<div class="col-5"></div>
|
||||
<div class="col-2">
|
||||
<div class="input-group rounded">
|
||||
<input id="page_input" type="text" class="form-control" placeholder="go to page">
|
||||
<span class="input-group-btn">
|
||||
<button id="go_button" class="btn btn-light" type="button">
|
||||
Go!
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-5"></div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('.scrolling-text p, .scrolling-text h1, .scrolling-text h2, .scrolling-text h3, .scrolling-text h4, .scrolling-text h5, .scrolling-text h6').each(function () {
|
||||
var parentWidth = $(this).parent().width();
|
||||
var elementWidth = $(this).outerWidth();
|
||||
var elementWidthWithPadding = $(this).outerWidth(true);
|
||||
|
||||
if (elementWidthWithPadding > parentWidth) {
|
||||
$(this).addClass('scrolling');
|
||||
}
|
||||
});
|
||||
|
||||
var currentUrl = window.location.pathname;
|
||||
var currentPage = parseInt(currentUrl.split('/').pop());
|
||||
var rootUrl = '/game/mai2/playlog/';
|
||||
var playlogPages = {{ playlog_pages }};
|
||||
if (Number.isNaN(currentPage)) {
|
||||
currentPage = 1;
|
||||
}
|
||||
$('#cur_page').text(currentPage);
|
||||
$('#prev_page').attr('href', rootUrl + (currentPage - 1))
|
||||
$('#next_page').attr('href', rootUrl + (currentPage + 1))
|
||||
$('#front_page').attr('href', rootUrl + (currentPage - 1))
|
||||
$('#front_page').text(currentPage - 1);
|
||||
$('#back_page').attr('href', rootUrl + (currentPage + 1))
|
||||
$('#back_page').text(currentPage + 1);
|
||||
$('#prev_3_page').attr('href', rootUrl + (currentPage - 3))
|
||||
$('#next_3_page').attr('href', rootUrl + (currentPage + 3))
|
||||
if ((currentPage - 1) < 3) {
|
||||
$('#prev_3_page').hide();
|
||||
if ((currentPage - 1) < 2) {
|
||||
$('#front_page').hide();
|
||||
if (currentPage === 1) {
|
||||
$('#first_page').hide();
|
||||
$('#prev_page').addClass('disabled');
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((playlogPages - currentPage) < 3) {
|
||||
$('#next_3_page').hide();
|
||||
if ((playlogPages - currentPage) < 2) {
|
||||
$('#back_page').hide();
|
||||
if (currentPage === playlogPages) {
|
||||
$('#last_page').hide();
|
||||
$('#next_page').addClass('disabled');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$('#go_button').click(function () {
|
||||
var pageNumber = parseInt($('#page_input').val());
|
||||
|
||||
if (!Number.isNaN(pageNumber) && pageNumber <= playlogPages && pageNumber >= 0) {
|
||||
var url = '/game/mai2/playlog/' + pageNumber;
|
||||
window.location.href = url;
|
||||
} else {
|
||||
$('#page_input').val('');
|
||||
$('#page_input').attr('placeholder', 'invalid input!');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock content %}
|
@ -1,6 +1,8 @@
|
||||
from typing import Any, List, Dict
|
||||
from random import randint
|
||||
from datetime import datetime, timedelta
|
||||
import pytz
|
||||
import json
|
||||
|
||||
from core.config import CoreConfig
|
||||
from titles.mai2.splashplus import Mai2SplashPlus
|
||||
@ -12,3 +14,207 @@ class Mai2Universe(Mai2SplashPlus):
|
||||
def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None:
|
||||
super().__init__(cfg, game_cfg)
|
||||
self.version = Mai2Constants.VER_MAIMAI_DX_UNIVERSE
|
||||
|
||||
async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||
p = await self.data.profile.get_profile_detail(data["userId"], self.version)
|
||||
if p is None:
|
||||
return {}
|
||||
|
||||
return {
|
||||
"userName": p["userName"],
|
||||
"rating": p["playerRating"],
|
||||
# hardcode lastDataVersion for CardMaker
|
||||
"lastDataVersion": "1.20.00",
|
||||
# checks if the user is still logged in
|
||||
"isLogin": False,
|
||||
"isExistSellingCard": True,
|
||||
}
|
||||
|
||||
async def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict:
|
||||
# user already exists, because the preview checks that already
|
||||
p = await self.data.profile.get_profile_detail(data["userId"], self.version)
|
||||
|
||||
cards = await self.data.card.get_user_cards(data["userId"])
|
||||
if cards is None or len(cards) == 0:
|
||||
# This should never happen
|
||||
self.logger.error(
|
||||
f"handle_get_user_data_api_request: Internal error - No cards found for user id {data['userId']}"
|
||||
)
|
||||
return {}
|
||||
|
||||
# get the dict representation of the row so we can modify values
|
||||
user_data = p._asdict()
|
||||
|
||||
# remove the values the game doesn't want
|
||||
user_data.pop("id")
|
||||
user_data.pop("user")
|
||||
user_data.pop("version")
|
||||
|
||||
return {"userId": data["userId"], "userData": user_data}
|
||||
|
||||
async def handle_cm_login_api_request(self, data: Dict) -> Dict:
|
||||
return {"returnCode": 1}
|
||||
|
||||
async def handle_cm_logout_api_request(self, data: Dict) -> Dict:
|
||||
return {"returnCode": 1}
|
||||
|
||||
async def handle_cm_get_selling_card_api_request(self, data: Dict) -> Dict:
|
||||
selling_cards = await self.data.static.get_enabled_cards(self.version)
|
||||
if selling_cards is None:
|
||||
return {"length": 0, "sellingCardList": []}
|
||||
|
||||
selling_card_list = []
|
||||
for card in selling_cards:
|
||||
tmp = card._asdict()
|
||||
tmp.pop("id")
|
||||
tmp.pop("version")
|
||||
tmp.pop("cardName")
|
||||
tmp.pop("enabled")
|
||||
|
||||
tmp["startDate"] = datetime.strftime(
|
||||
tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT
|
||||
)
|
||||
tmp["endDate"] = datetime.strftime(
|
||||
tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT
|
||||
)
|
||||
tmp["noticeStartDate"] = datetime.strftime(
|
||||
tmp["noticeStartDate"], Mai2Constants.DATE_TIME_FORMAT
|
||||
)
|
||||
tmp["noticeEndDate"] = datetime.strftime(
|
||||
tmp["noticeEndDate"], Mai2Constants.DATE_TIME_FORMAT
|
||||
)
|
||||
|
||||
selling_card_list.append(tmp)
|
||||
|
||||
return {"length": len(selling_card_list), "sellingCardList": selling_card_list}
|
||||
|
||||
async def handle_cm_get_user_card_api_request(self, data: Dict) -> Dict:
|
||||
user_cards = await self.data.item.get_cards(data["userId"])
|
||||
if user_cards is None:
|
||||
return {"returnCode": 1, "length": 0, "nextIndex": 0, "userCardList": []}
|
||||
|
||||
max_ct = data["maxCount"]
|
||||
next_idx = data["nextIndex"]
|
||||
start_idx = next_idx
|
||||
end_idx = max_ct + start_idx
|
||||
|
||||
if len(user_cards[start_idx:]) > max_ct:
|
||||
next_idx += max_ct
|
||||
else:
|
||||
next_idx = 0
|
||||
|
||||
card_list = []
|
||||
for card in user_cards:
|
||||
tmp = card._asdict()
|
||||
tmp.pop("id")
|
||||
tmp.pop("user")
|
||||
|
||||
tmp["startDate"] = datetime.strftime(
|
||||
tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT
|
||||
)
|
||||
tmp["endDate"] = datetime.strftime(
|
||||
tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT
|
||||
)
|
||||
card_list.append(tmp)
|
||||
|
||||
return {
|
||||
"returnCode": 1,
|
||||
"length": len(card_list[start_idx:end_idx]),
|
||||
"nextIndex": next_idx,
|
||||
"userCardList": card_list[start_idx:end_idx],
|
||||
}
|
||||
|
||||
async def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict:
|
||||
await super().handle_get_user_item_api_request(data)
|
||||
|
||||
async def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict:
|
||||
characters = await self.data.item.get_characters(data["userId"])
|
||||
|
||||
chara_list = []
|
||||
for chara in characters:
|
||||
chara_list.append(
|
||||
{
|
||||
"characterId": chara["characterId"],
|
||||
# no clue why those values are even needed
|
||||
"point": 0,
|
||||
"count": 0,
|
||||
"level": chara["level"],
|
||||
"nextAwake": 0,
|
||||
"nextAwakePercent": 0,
|
||||
"favorite": False,
|
||||
"awakening": chara["awakening"],
|
||||
"useCount": chara["useCount"],
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"returnCode": 1,
|
||||
"length": len(chara_list),
|
||||
"userCharacterList": chara_list,
|
||||
}
|
||||
|
||||
async def handle_cm_get_user_card_print_error_api_request(self, data: Dict) -> Dict:
|
||||
return {"length": 0, "userPrintDetailList": []}
|
||||
|
||||
async def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict:
|
||||
user_id = data["userId"]
|
||||
upsert = data["userPrintDetail"]
|
||||
|
||||
# set a random card serial number
|
||||
serial_id = "".join([str(randint(0, 9)) for _ in range(20)])
|
||||
|
||||
# calculate start and end date of the card
|
||||
start_date = datetime.utcnow()
|
||||
end_date = datetime.utcnow() + timedelta(days=15)
|
||||
|
||||
user_card = upsert["userCard"]
|
||||
await self.data.item.put_card(
|
||||
user_id,
|
||||
user_card["cardId"],
|
||||
user_card["cardTypeId"],
|
||||
user_card["charaId"],
|
||||
user_card["mapId"],
|
||||
# add the correct start date and also the end date in 15 days
|
||||
start_date,
|
||||
end_date,
|
||||
)
|
||||
|
||||
# get the profile extend to save the new bought card
|
||||
extend = await self.data.profile.get_profile_extend(user_id, self.version)
|
||||
if extend:
|
||||
extend = extend._asdict()
|
||||
# parse the selectedCardList
|
||||
# 6 = Freedom Pass, 4 = Gold Pass (cardTypeId)
|
||||
selected_cards: List = extend["selectedCardList"]
|
||||
|
||||
# if no pass is already added, add the corresponding pass
|
||||
if not user_card["cardTypeId"] in selected_cards:
|
||||
selected_cards.insert(0, user_card["cardTypeId"])
|
||||
|
||||
extend["selectedCardList"] = selected_cards
|
||||
await self.data.profile.put_profile_extend(user_id, self.version, extend)
|
||||
|
||||
# properly format userPrintDetail for the database
|
||||
upsert.pop("userCard")
|
||||
upsert.pop("serialId")
|
||||
upsert["printDate"] = datetime.strptime(upsert["printDate"], "%Y-%m-%d")
|
||||
|
||||
await self.data.item.put_user_print_detail(user_id, serial_id, upsert)
|
||||
|
||||
return {
|
||||
"returnCode": 1,
|
||||
"orderId": 0,
|
||||
"serialId": serial_id,
|
||||
"startDate": datetime.strftime(start_date, Mai2Constants.DATE_TIME_FORMAT),
|
||||
"endDate": datetime.strftime(end_date, Mai2Constants.DATE_TIME_FORMAT),
|
||||
}
|
||||
|
||||
async def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict:
|
||||
return {
|
||||
"returnCode": 1,
|
||||
"orderId": 0,
|
||||
"serialId": data["userPrintlog"]["serialId"],
|
||||
}
|
||||
|
||||
async def handle_cm_upsert_buy_card_api_request(self, data: Dict) -> Dict:
|
||||
return {"returnCode": 1}
|
||||
|
@ -31,8 +31,7 @@ class OngekiFrontend(FE_Base):
|
||||
|
||||
def get_routes(self) -> List[Route]:
|
||||
return [
|
||||
Route("/", self.render_GET),
|
||||
Route("/version.change", self.render_POST, methods=['POST'])
|
||||
Route("/", self.render_GET)
|
||||
]
|
||||
|
||||
async def render_GET(self, request: Request) -> bytes:
|
||||
@ -70,34 +69,29 @@ class OngekiFrontend(FE_Base):
|
||||
return RedirectResponse("/gate/", 303)
|
||||
|
||||
async def render_POST(self, request: Request):
|
||||
uri = request.url.path
|
||||
frm = await request.form()
|
||||
uri = request.uri.decode()
|
||||
usr_sesh = self.validate_session(request)
|
||||
if not usr_sesh:
|
||||
usr_sesh = UserSession()
|
||||
|
||||
if usr_sesh.user_id > 0:
|
||||
if uri == "/game/ongeki/rival.add":
|
||||
rival_id = frm.get("rivalUserId")
|
||||
rival_id = request.args[b"rivalUserId"][0].decode()
|
||||
await self.data.profile.put_rival(usr_sesh.user_id, rival_id)
|
||||
# self.logger.info(f"{usr_sesh.user_id} added a rival")
|
||||
return RedirectResponse(b"/game/ongeki/", 303)
|
||||
|
||||
elif uri == "/game/ongeki/rival.delete":
|
||||
rival_id = frm.get("rivalUserId")
|
||||
rival_id = request.args[b"rivalUserId"][0].decode()
|
||||
await self.data.profile.delete_rival(usr_sesh.user_id, rival_id)
|
||||
# self.logger.info(f"{response}")
|
||||
return RedirectResponse(b"/game/ongeki/", 303)
|
||||
|
||||
elif uri == "/game/ongeki/version.change":
|
||||
ongeki_version=frm.get("version")
|
||||
ongeki_version=request.args[b"version"][0].decode()
|
||||
if(ongeki_version.isdigit()):
|
||||
usr_sesh.ongeki_version=int(ongeki_version)
|
||||
enc = self.encode_session(usr_sesh)
|
||||
resp = RedirectResponse("/game/ongeki/", 303)
|
||||
resp.delete_cookie('ARTEMIS_SESH')
|
||||
resp.set_cookie('ARTEMIS_SESH', enc)
|
||||
return resp
|
||||
return RedirectResponse("/game/ongeki/", 303)
|
||||
|
||||
else:
|
||||
Response("Something went wrong", status_code=500)
|
||||
|
@ -216,20 +216,16 @@ class OngekiServlet(BaseServlet):
|
||||
)
|
||||
return Response(zlib.compress(b'{"stat": "0"}'))
|
||||
|
||||
if version < 105:
|
||||
# O.N.G.E.K.I base don't use zlib
|
||||
req_data = json.loads(req_raw)
|
||||
else:
|
||||
try:
|
||||
unzip = zlib.decompress(req_raw)
|
||||
|
||||
except zlib.error as e:
|
||||
self.logger.error(
|
||||
f"Failed to decompress v{version} {endpoint} request -> {e}"
|
||||
)
|
||||
return Response(zlib.compress(b'{"stat": "0"}'))
|
||||
try:
|
||||
unzip = zlib.decompress(req_raw)
|
||||
|
||||
req_data = json.loads(unzip)
|
||||
except zlib.error as e:
|
||||
self.logger.error(
|
||||
f"Failed to decompress v{version} {endpoint} request -> {e}"
|
||||
)
|
||||
return Response(zlib.compress(b'{"stat": "0"}'))
|
||||
|
||||
req_data = json.loads(unzip)
|
||||
|
||||
self.logger.info(
|
||||
f"v{version} {endpoint} request from {client_ip}"
|
||||
@ -255,12 +251,9 @@ class OngekiServlet(BaseServlet):
|
||||
|
||||
self.logger.debug(f"Response {resp}")
|
||||
|
||||
resp_raw = json.dumps(resp, ensure_ascii=False).encode("utf-8")
|
||||
zipped = zlib.compress(resp_raw)
|
||||
zipped = zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8"))
|
||||
|
||||
if not encrtped or version < 120:
|
||||
if version < 105:
|
||||
return Response(resp_raw)
|
||||
return Response(zipped)
|
||||
|
||||
padded = pad(zipped, 16)
|
||||
|
@ -30,7 +30,7 @@ score_best = Table(
|
||||
Column("isFullCombo", Boolean, nullable=False),
|
||||
Column("isAllBreake", Boolean, nullable=False),
|
||||
Column("isLock", Boolean, nullable=False),
|
||||
Column("clearStatus", Integer, nullable=False),
|
||||
Column("clearStatus", Boolean, nullable=False),
|
||||
Column("isStoryWatched", Boolean, nullable=False),
|
||||
Column("platinumScoreMax", Integer),
|
||||
UniqueConstraint("user", "musicId", "level", name="ongeki_best_score_uk"),
|
||||
@ -131,7 +131,7 @@ class OngekiScoreData(BaseData):
|
||||
async def get_tech_count(self, aime_id: int) -> Optional[List[Dict]]:
|
||||
sql = select(tech_count).where(tech_count.c.user == aime_id)
|
||||
|
||||
result = await self.execute(sql)
|
||||
result = self.execute(sql)
|
||||
|
||||
if result is None:
|
||||
return None
|
||||
|
@ -1,7 +1,9 @@
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
import json, logging
|
||||
from typing import Any, Dict, List
|
||||
import random
|
||||
|
||||
from core.data import Data
|
||||
from core import CoreConfig
|
||||
from .config import PokkenConfig
|
||||
from .proto import jackal_pb2
|
||||
@ -16,6 +18,7 @@ class PokkenBase:
|
||||
self.version = 0
|
||||
self.logger = logging.getLogger("pokken")
|
||||
self.data = PokkenData(core_cfg)
|
||||
self.SUPPORT_SET_NONE = 4294967295
|
||||
|
||||
async def handle_noop(self, request: Any) -> bytes:
|
||||
res = jackal_pb2.Response()
|
||||
@ -35,30 +38,7 @@ class PokkenBase:
|
||||
res = jackal_pb2.Response()
|
||||
res.result = 1
|
||||
res.type = jackal_pb2.MessageType.REGISTER_PCB
|
||||
pcbid = request.register_pcb.pcb_id
|
||||
if not pcbid.isdigit() or len(pcbid) != 12 or \
|
||||
not pcbid.startswith(f"{PokkenConstants.SERIAL_IDENT[0]}{PokkenConstants.SERIAL_REGIONS[0]}{PokkenConstants.SERIAL_ROLES[0]}{PokkenConstants.SERIAL_CAB_IDENTS[0]}"):
|
||||
self.logger.warn(f"Bad PCBID {pcbid}")
|
||||
res.result = 0
|
||||
return res
|
||||
|
||||
netid = PokkenConstants.NETID_PREFIX[0] + pcbid[5:]
|
||||
|
||||
self.logger.info(f"Register PCB {pcbid} (netID {netid})")
|
||||
|
||||
minfo = await self.data.arcade.get_machine(netid)
|
||||
|
||||
if not minfo and not self.core_cfg.server.allow_unregistered_serials:
|
||||
self.logger.warn(f"netID {netid} does not belong to any shop!")
|
||||
res.result = 0
|
||||
return res
|
||||
|
||||
elif not minfo:
|
||||
self.logger.warn(f"Orphaned netID {netid} allowed to connect")
|
||||
locid = 0
|
||||
|
||||
else:
|
||||
locid = minfo['arcade']
|
||||
self.logger.info(f"Register PCB {request.register_pcb.pcb_id}")
|
||||
|
||||
regist_pcb = jackal_pb2.RegisterPcbResponseData()
|
||||
regist_pcb.server_time = int(datetime.now().timestamp())
|
||||
@ -66,7 +46,7 @@ class PokkenBase:
|
||||
"MatchingServer": {
|
||||
"host": f"https://{self.game_cfg.server.hostname}",
|
||||
"port": self.game_cfg.ports.game,
|
||||
"url": "/pokken/matching",
|
||||
"url": "/SDAK/100/matching",
|
||||
},
|
||||
"StunServer": {
|
||||
"addr": self.game_cfg.server.stun_server_host,
|
||||
@ -76,10 +56,10 @@ class PokkenBase:
|
||||
"addr": self.game_cfg.server.stun_server_host,
|
||||
"port": self.game_cfg.server.stun_server_port,
|
||||
},
|
||||
"AdmissionUrl": f"ws://{self.game_cfg.server.hostname}:{self.game_cfg.ports.admission}/pokken/admission",
|
||||
"locationId": locid,
|
||||
"logfilename": "J:\\JackalMatchingLibrary.log",
|
||||
"biwalogfilename": "J:\\biwa_log.log",
|
||||
"AdmissionUrl": f"ws://{self.game_cfg.server.hostname}:{self.game_cfg.ports.admission}",
|
||||
"locationId": 123, # FIXME: Get arcade's ID from the database
|
||||
"logfilename": "JackalMatchingLibrary.log",
|
||||
"biwalogfilename": "./biwa.log",
|
||||
}
|
||||
regist_pcb.bnp_baseuri = f"{self.core_cfg.server.hostname}/bna"
|
||||
regist_pcb.biwa_setting = json.dumps(biwa_setting)
|
||||
@ -115,11 +95,12 @@ class PokkenBase:
|
||||
res.type = jackal_pb2.MessageType.LOAD_CLIENT_SETTINGS
|
||||
settings = jackal_pb2.LoadClientSettingsResponseData()
|
||||
|
||||
# TODO: Make configurable
|
||||
settings.money_magnification = 1
|
||||
settings.continue_bonus_exp = 100
|
||||
settings.continue_fight_money = 100
|
||||
settings.event_bonus_exp = 100
|
||||
settings.level_cap = 100
|
||||
settings.level_cap = 999
|
||||
settings.op_movie_flag = 0xFFFFFFFF
|
||||
settings.lucky_bonus_rate = 1
|
||||
settings.fail_support_num = 10
|
||||
@ -151,13 +132,9 @@ class PokkenBase:
|
||||
res.type = jackal_pb2.MessageType.LOAD_USER
|
||||
access_code = request.load_user.access_code
|
||||
load_usr = jackal_pb2.LoadUserResponseData()
|
||||
load_usr.load_hash = 1
|
||||
load_usr.access_code = access_code
|
||||
load_usr.precedent_release_flag = 0xFFFFFFFF
|
||||
load_usr.cardlock_status = False
|
||||
card = await self.data.card.get_card_by_access_code(access_code)
|
||||
user_id = await self.data.card.get_user_id_from_card(access_code)
|
||||
|
||||
if card is None and self.game_cfg.server.auto_register:
|
||||
if user_id is None and self.game_cfg.server.auto_register:
|
||||
user_id = await self.data.user.create_user()
|
||||
card_id = await self.data.card.create_card(user_id, access_code)
|
||||
|
||||
@ -165,38 +142,54 @@ class PokkenBase:
|
||||
f"Register new card {access_code} (UserId {user_id}, CardId {card_id})"
|
||||
)
|
||||
|
||||
await self.data.card.set_chip_id_by_access_code(access_code, int(request.load_user.chip_id[:8], 16))
|
||||
|
||||
elif card is None:
|
||||
elif user_id is None:
|
||||
self.logger.info(f"Registration of card {access_code} blocked!")
|
||||
res.load_user.CopyFrom(load_usr)
|
||||
return res.SerializeToString()
|
||||
|
||||
else:
|
||||
user_id = card['user']
|
||||
card_id = card['id']
|
||||
if not card['chip_id']:
|
||||
await self.data.card.set_chip_id_by_access_code(access_code, int(request.load_user.chip_id[:8], 16))
|
||||
|
||||
"""
|
||||
TODO: Unlock all supports? Probably
|
||||
TODO: Add repeated values
|
||||
tutorial_progress_flag
|
||||
rankmatch_progress
|
||||
support_pokemon_list
|
||||
support_set_1
|
||||
support_set_2
|
||||
support_set_3
|
||||
aid_skill_list
|
||||
achievement_flag
|
||||
event_achievement_flag
|
||||
event_achievement_param
|
||||
"""
|
||||
profile = await self.data.profile.get_profile(user_id)
|
||||
load_usr.commidserv_result = 1
|
||||
load_usr.load_hash = 1
|
||||
load_usr.cardlock_status = False
|
||||
load_usr.banapass_id = user_id
|
||||
load_usr.access_code = access_code
|
||||
load_usr.precedent_release_flag = 0xFFFFFFFF
|
||||
|
||||
if profile is None or profile['trainer_name'] is None:
|
||||
if profile is None:
|
||||
profile_id = await self.data.profile.create_profile(user_id)
|
||||
self.logger.info(f"Create new profile {profile_id} for user {user_id}")
|
||||
profile_dict = {"id": profile_id, "user": user_id}
|
||||
pokemon_data = []
|
||||
tutorial_progress = []
|
||||
rankmatch_progress = []
|
||||
achievement_flag = []
|
||||
event_achievement_flag = []
|
||||
event_achievement_param = []
|
||||
load_usr.new_card_flag = True
|
||||
|
||||
else:
|
||||
profile_dict = {k: v for k, v in profile._asdict().items() if v is not None}
|
||||
self.logger.info(f"Card-in user {user_id} (Trainer name {profile_dict.get('trainer_name', '')})")
|
||||
self.logger.info(
|
||||
f"Card-in user {user_id} (Trainer name {profile_dict.get('trainer_name', '')})"
|
||||
)
|
||||
pokemon_data = await self.data.profile.get_all_pokemon_data(user_id)
|
||||
tutorial_progress = []
|
||||
rankmatch_progress = []
|
||||
achievement_flag = []
|
||||
event_achievement_flag = []
|
||||
event_achievement_param = []
|
||||
load_usr.new_card_flag = False
|
||||
|
||||
load_usr.navi_newbie_flag = profile_dict.get("navi_newbie_flag", True)
|
||||
@ -208,9 +201,9 @@ class PokkenBase:
|
||||
load_usr.trainer_name = profile_dict.get(
|
||||
"trainer_name", f"Newb{str(user_id).zfill(4)}"
|
||||
)
|
||||
load_usr.trainer_rank_point = profile_dict.get("trainer_rank_point", 0) # determines rank
|
||||
load_usr.wallet = profile_dict.get("wallet", 0) # pg count
|
||||
load_usr.fight_money = profile_dict.get("fight_money", 0) # ?
|
||||
load_usr.trainer_rank_point = profile_dict.get("trainer_rank_point", 0)
|
||||
load_usr.wallet = profile_dict.get("wallet", 0)
|
||||
load_usr.fight_money = profile_dict.get("fight_money", 0)
|
||||
load_usr.score_point = profile_dict.get("score_point", 0)
|
||||
load_usr.grade_max_num = profile_dict.get("grade_max_num", 0)
|
||||
load_usr.extra_counter = profile_dict.get("extra_counter", 0)
|
||||
@ -225,18 +218,18 @@ class PokkenBase:
|
||||
load_usr.rank_event = profile_dict.get("rank_event", 0)
|
||||
load_usr.awake_num = profile_dict.get("awake_num", 0)
|
||||
load_usr.use_support_num = profile_dict.get("use_support_num", 0)
|
||||
load_usr.rankmatch_flag = profile_dict.get("rankmatch_flag", 0) # flags that next rank match will be rank up
|
||||
load_usr.rankmatch_flag = profile_dict.get("rankmatch_flag", 0)
|
||||
load_usr.rankmatch_max = profile_dict.get("rankmatch_max", 0)
|
||||
load_usr.rankmatch_success = profile_dict.get("rankmatch_success", 0)
|
||||
load_usr.beat_num = profile_dict.get("beat_num", 0)
|
||||
load_usr.title_text_id = profile_dict.get("title_text_id", 2)
|
||||
load_usr.title_plate_id = profile_dict.get("title_plate_id", 31)
|
||||
load_usr.title_decoration_id = profile_dict.get("title_decoration_id", 1)
|
||||
load_usr.title_text_id = profile_dict.get("title_text_id", 0)
|
||||
load_usr.title_plate_id = profile_dict.get("title_plate_id", 0)
|
||||
load_usr.title_decoration_id = profile_dict.get("title_decoration_id", 0)
|
||||
load_usr.navi_trainer = profile_dict.get("navi_trainer", 0)
|
||||
load_usr.navi_version_id = profile_dict.get("navi_version_id", 0)
|
||||
load_usr.aid_skill = profile_dict.get("aid_skill", 0)
|
||||
load_usr.comment_text_id = profile_dict.get("comment_text_id", 1)
|
||||
load_usr.comment_word_id = profile_dict.get("comment_word_id", 1)
|
||||
load_usr.comment_text_id = profile_dict.get("comment_text_id", 0)
|
||||
load_usr.comment_word_id = profile_dict.get("comment_word_id", 0)
|
||||
load_usr.latest_use_pokemon = profile_dict.get("latest_use_pokemon", 0)
|
||||
load_usr.ex_ko_num = profile_dict.get("ex_ko_num", 0)
|
||||
load_usr.wko_num = profile_dict.get("wko_num", 0)
|
||||
@ -244,11 +237,11 @@ class PokkenBase:
|
||||
load_usr.cool_ko_num = profile_dict.get("cool_ko_num", 0)
|
||||
load_usr.perfect_ko_num = profile_dict.get("perfect_ko_num", 0)
|
||||
load_usr.record_flag = profile_dict.get("record_flag", 0)
|
||||
load_usr.site_register_status = profile_dict.get("site_register_status", 1)
|
||||
load_usr.site_register_status = profile_dict.get("site_register_status", 0)
|
||||
load_usr.continue_num = profile_dict.get("continue_num", 0)
|
||||
|
||||
load_usr.avatar_body = profile_dict.get("avatar_body", 0)
|
||||
load_usr.avatar_gender = profile_dict.get("avatar_gender", 1)
|
||||
load_usr.avatar_gender = profile_dict.get("avatar_gender", 0)
|
||||
load_usr.avatar_background = profile_dict.get("avatar_background", 0)
|
||||
load_usr.avatar_head = profile_dict.get("avatar_head", 0)
|
||||
load_usr.avatar_battleglass = profile_dict.get("avatar_battleglass", 0)
|
||||
@ -290,31 +283,6 @@ class PokkenBase:
|
||||
pkm.bp_point_sp = pkmn_d.get('bp_point_sp', 0)
|
||||
|
||||
load_usr.pokemon_data.append(pkm)
|
||||
|
||||
for x in profile_dict.get("tutorial_progress_flag", []):
|
||||
load_usr.tutorial_progress_flag.append(x)
|
||||
|
||||
for x in profile_dict.get("achievement_flag", []):
|
||||
load_usr.achievement_flag.append(x)
|
||||
|
||||
for x in profile_dict.get("aid_skill_list", []):
|
||||
load_usr.aid_skill_list.append(x)
|
||||
|
||||
for x in profile_dict.get("rankmatch_progress", []):
|
||||
load_usr.rankmatch_progress.append(x)
|
||||
|
||||
for x in profile_dict.get("event_achievement_flag", []):
|
||||
load_usr.event_achievement_flag.append(x)
|
||||
|
||||
for x in profile_dict.get("event_achievement_param", []):
|
||||
load_usr.event_achievement_param.append(x)
|
||||
|
||||
load_usr.support_set_1.append(profile_dict.get("support_set_1_1", 587))
|
||||
load_usr.support_set_1.append(profile_dict.get("support_set_1_2", 653))
|
||||
load_usr.support_set_2.append(profile_dict.get("support_set_2_1", 495))
|
||||
load_usr.support_set_2.append(profile_dict.get("support_set_2_2", 131))
|
||||
load_usr.support_set_3.append(profile_dict.get("support_set_3_1", 657))
|
||||
load_usr.support_set_3.append(profile_dict.get("support_set_3_2", 133))
|
||||
|
||||
res.load_user.CopyFrom(load_usr)
|
||||
return res.SerializeToString()
|
||||
@ -332,8 +300,6 @@ class PokkenBase:
|
||||
|
||||
req = request.save_user
|
||||
user_id = req.banapass_id
|
||||
|
||||
self.logger.info(f"Save user data for {user_id}")
|
||||
|
||||
tut_flgs: List[int] = []
|
||||
ach_flgs: List[int] = []
|
||||
@ -373,7 +339,7 @@ class PokkenBase:
|
||||
for ach_flg in req.achievement_flag:
|
||||
ach_flgs.append(ach_flg)
|
||||
|
||||
await self.data.profile.update_profile_achievement_flags(user_id, ach_flgs)
|
||||
await self.data.profile.update_profile_tutorial_flags(user_id, ach_flg)
|
||||
|
||||
for evt_flg in req.event_achievement_flag:
|
||||
evt_flgs.append(evt_flg)
|
||||
@ -387,23 +353,18 @@ class PokkenBase:
|
||||
await self.data.item.add_reward(user_id, reward.get_category_id, reward.get_content_id, reward.get_type_id)
|
||||
|
||||
await self.data.profile.add_profile_points(user_id, get_rank_pts, get_money, get_score_pts, grade_max)
|
||||
|
||||
# Inconsistant underscore use AND a typo??
|
||||
#await self.data.profile.update_rankmatch_data(user_id, req.rankmatch_flag, req.rank_match_max, req.rank_match_success, req.rank_match_process)
|
||||
|
||||
await self.data.profile.update_support_team(user_id, 1, req.support_set_1[0], req.support_set_1[1])
|
||||
await self.data.profile.update_support_team(user_id, 2, req.support_set_2[0], req.support_set_2[1])
|
||||
await self.data.profile.update_support_team(user_id, 3, req.support_set_3[0], req.support_set_3[1])
|
||||
|
||||
await self.data.profile.put_pokemon(user_id, mon.char_id, mon.illustration_book_no, mon.bp_point_atk, mon.bp_point_res, mon.bp_point_def, mon.bp_point_sp)
|
||||
await self.data.profile.add_pokemon_xp(user_id, mon.illustration_book_no, mon.get_pokemon_exp)
|
||||
await self.data.profile.set_latest_mon(user_id, mon.illustration_book_no)
|
||||
await self.data.profile.add_pokemon_xp(user_id, mon.char_id, mon.get_pokemon_exp)
|
||||
|
||||
for x in range(len(battle.play_mode)):
|
||||
self.logger.info(f"Save {PokkenConstants.BATTLE_TYPE(battle.play_mode[x]).name} battle {PokkenConstants.BATTLE_RESULT(battle.result[x]).name} for {user_id} with mon {mon.illustration_book_no}")
|
||||
await self.data.profile.put_pokemon_battle_result(
|
||||
user_id,
|
||||
mon.illustration_book_no,
|
||||
mon.char_id,
|
||||
PokkenConstants.BATTLE_TYPE(battle.play_mode[x]),
|
||||
PokkenConstants.BATTLE_RESULT(battle.result[x])
|
||||
)
|
||||
@ -430,6 +391,7 @@ class PokkenBase:
|
||||
last_evt
|
||||
)
|
||||
|
||||
|
||||
return res.SerializeToString()
|
||||
|
||||
async def handle_save_ingame_log(self, data: jackal_pb2.Request) -> bytes:
|
||||
@ -457,13 +419,6 @@ class PokkenBase:
|
||||
async def handle_matching_is_matching(
|
||||
self, data: Dict = {}, client_ip: str = "127.0.0.1"
|
||||
) -> Dict:
|
||||
"""
|
||||
"sessionId":"12345678",
|
||||
"A":{
|
||||
"pcb_id": data["data"]["must"]["pcb_id"],
|
||||
"gip": client_ip
|
||||
},
|
||||
"""
|
||||
return {
|
||||
"data": {
|
||||
"sessionId":"12345678",
|
||||
@ -480,11 +435,6 @@ class PokkenBase:
|
||||
) -> Dict:
|
||||
return {}
|
||||
|
||||
async def handle_matching_obtain_matching(
|
||||
self, data: Dict = {}, client_ip: str = "127.0.0.1"
|
||||
) -> Dict:
|
||||
return {}
|
||||
|
||||
async def handle_admission_noop(self, data: Dict, req_ip: str = "127.0.0.1") -> Dict:
|
||||
return {}
|
||||
|
||||
|
@ -1,140 +0,0 @@
|
||||
{
|
||||
"448": {
|
||||
"name_en": "Lucario",
|
||||
"name_jp": "ルカリオ",
|
||||
"type": 0,
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/448.png"
|
||||
},
|
||||
"25": {
|
||||
"name_en": "Pikachu",
|
||||
"name_jp": "ピカチュウ",
|
||||
"type": 0,
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/25.png"
|
||||
},
|
||||
"68": {
|
||||
"name_en": "Machamp",
|
||||
"name_jp": "カイリキー",
|
||||
"type": 1,
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/68.png"
|
||||
},
|
||||
"282": {
|
||||
"name_en": "Gardevoir",
|
||||
"name_jp": "サーナイト",
|
||||
"type": 2,
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/282.png"
|
||||
},
|
||||
"461": {
|
||||
"name_en": "Weavile",
|
||||
"name_jp": "マニューラ",
|
||||
"type": 3,
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/461.png"
|
||||
},
|
||||
"245": {
|
||||
"name_en": "Suicune",
|
||||
"name_jp": "スイクン",
|
||||
"type": 0,
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/245.png"
|
||||
},
|
||||
"6": {
|
||||
"name_en": "Charizard",
|
||||
"name_jp": "リザードン",
|
||||
"type": 1,
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/6.png"
|
||||
},
|
||||
"94": {
|
||||
"name_en": "Gengar",
|
||||
"name_jp": "ゲンガー",
|
||||
"type": 2,
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/94.png"
|
||||
},
|
||||
"257": {
|
||||
"name_en": "Blaziken",
|
||||
"name_jp": "バシャーモ",
|
||||
"type": 0,
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/257.png"
|
||||
},
|
||||
"10025": {
|
||||
"name_en": "Pikachu Libre",
|
||||
"name_jp": "マスクド・ピカチュウ",
|
||||
"type": 3,
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/10084.png"
|
||||
},
|
||||
"254": {
|
||||
"name_en": "Sceptile",
|
||||
"name_jp": "ジュカイン",
|
||||
"type": 3,
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/254.png"
|
||||
},
|
||||
"609": {
|
||||
"name_en": "Chandelure",
|
||||
"name_jp": "シャンデラ",
|
||||
"type": 1,
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/609.png"
|
||||
},
|
||||
"150": {
|
||||
"name_en": "Mewtwo",
|
||||
"name_jp": "ミュウツー",
|
||||
"type": 0,
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/150.png"
|
||||
},
|
||||
"10150": {
|
||||
"name_en": "Shadow Mewtwo",
|
||||
"name_jp": "ダークミュウツー",
|
||||
"type": 2,
|
||||
"artwork": "https://archives.bulbagarden.net/media/upload/7/7a/Pokk%C3%A9n_Shadow_Mewtwo.png"
|
||||
},
|
||||
"445": {
|
||||
"name_en": "Garchomp",
|
||||
"name_jp": "ガブリアス",
|
||||
"type": 1,
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/445.png"
|
||||
},
|
||||
"654": {
|
||||
"name_en": "Braixen",
|
||||
"name_jp": "テールナー",
|
||||
"type": 0,
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/654.png"
|
||||
},
|
||||
"491": {
|
||||
"name_en": "Darkrai",
|
||||
"name_jp": "ダークライ",
|
||||
"type": 2,
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/491.png"
|
||||
},
|
||||
"212": {
|
||||
"name_en": "Scizor",
|
||||
"name_jp": "ハッサム",
|
||||
"type": 1,
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/212.png"
|
||||
},
|
||||
"453": {
|
||||
"name_en": "Croagunk",
|
||||
"name_jp": "グレッグル",
|
||||
"type": 3,
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/453.png"
|
||||
},
|
||||
"395": {
|
||||
"name_en": "Empoleon",
|
||||
"name_jp": "エンペルト",
|
||||
"type": 0,
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/395.png"
|
||||
},
|
||||
"724": {
|
||||
"name_en": "Decidueye",
|
||||
"name_jp": "ジュナイパー",
|
||||
"type": 0,
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/724.png"
|
||||
},
|
||||
"681": {
|
||||
"name_en": "Aegislash",
|
||||
"name_jp": "ギルガルド",
|
||||
"type": 2,
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/681.png"
|
||||
},
|
||||
"9": {
|
||||
"name_en": "Blastoise",
|
||||
"name_jp": "カメックス",
|
||||
"type": 1,
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/9.png"
|
||||
}
|
||||
}
|
@ -1,218 +0,0 @@
|
||||
{
|
||||
"587": {
|
||||
"name_en": "Emolga",
|
||||
"name_jp": "エモンガ",
|
||||
"desc": "Uses Shock Wave to shock the opponent and temporarily decrease its speed.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/587.png"
|
||||
},
|
||||
"653": {
|
||||
"name_en": "Fennekin",
|
||||
"name_jp": "フォッコ",
|
||||
"desc": "Uses Ember to surround itself with fire, creating a trap.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/653.png"
|
||||
},
|
||||
"495": {
|
||||
"name_en": "Snivy",
|
||||
"name_jp": "ツタージャ",
|
||||
"desc": "Uses Leaf Tornado to perform an anti-air attack and send the opponent flying.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/495.png"
|
||||
},
|
||||
"131": {
|
||||
"name_en": "Lapras",
|
||||
"name_jp": "ラプラス",
|
||||
"desc": "Uses Surf as it enters the stage, damaging the enemy with a wave of water.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/131.png"
|
||||
},
|
||||
"657": {
|
||||
"name_en": "Frogadier",
|
||||
"name_jp": "ゲコガシラ",
|
||||
"desc": "Uses Water Pulse to attack from a distance by firing water bullets. Effective when striking from long distance.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/657.png"
|
||||
},
|
||||
"133": {
|
||||
"name_en": "Eevee",
|
||||
"name_jp": "イーブイ",
|
||||
"desc": "Uses Helping Hand to heal the user and temporarily increase their attack power.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/133.png"
|
||||
},
|
||||
"385": {
|
||||
"name_en": "Jirachi",
|
||||
"name_jp": "ジラーチ",
|
||||
"desc": "Uses Wish to restore the Synergy Gauge and temporarily strengthen the user's attack power during Synergy Burst.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/385.png"
|
||||
},
|
||||
"547": {
|
||||
"name_en": "Whimsicott",
|
||||
"name_jp": "エルフーン",
|
||||
"desc": "Uses Substitute to render attacks from opponents useless and heal the user.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/547.png"
|
||||
},
|
||||
"38": {
|
||||
"name_en": "Ninetales",
|
||||
"name_jp": "キュウコン",
|
||||
"desc": "Uses Will-O-Wisp to send small flames in front of the user. Enemy's attack power decreased temporarily when contacted.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/38.png"
|
||||
},
|
||||
"429": {
|
||||
"name_en": "Mismagius",
|
||||
"name_jp": "ムウマージ",
|
||||
"desc": "Uses Ominous Wind to attack the opponent and temporarily increase the user's attack power.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/429.png"
|
||||
},
|
||||
"83": {
|
||||
"name_en": "Farfetch'd",
|
||||
"name_jp": "カモネギ",
|
||||
"desc": "Uses Fury Cutter to perform a flurry of attacks toward the opponent.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/83.png"
|
||||
},
|
||||
"101": {
|
||||
"name_en": "Electrode",
|
||||
"name_jp": "マルマイン",
|
||||
"desc": "Uses Explosion to counter an opponent's attack upon defending.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/101.png"
|
||||
},
|
||||
"479": {
|
||||
"name_en": "Rotom",
|
||||
"name_jp": "ロトム",
|
||||
"desc": "Uses Thunder Shock to target enemies in the air and temporarily decrease their speed.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/479.png"
|
||||
},
|
||||
"468": {
|
||||
"name_en": "Togekiss",
|
||||
"name_jp": "トゲキッス",
|
||||
"desc": "Uses Tailwind to temporarily increase the user's speed and recover some health.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/468.png"
|
||||
},
|
||||
"149": {
|
||||
"name_en": "Dragonite",
|
||||
"name_jp": "カイリュー",
|
||||
"desc": "Uses Draco Meteor to attack multiple times over a wide area.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/149.png"
|
||||
},
|
||||
"494": {
|
||||
"name_en": "Victini",
|
||||
"name_jp": "ビクティニ",
|
||||
"desc": "Uses V-create to temporarily make the user's attacks critical hits, restores some of the user's health, and increases the user's Synergy Gauge. Unlike other Enhance Pokémon, Victini can actually damage the foe if they're above it when flying off the screen.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/494.png"
|
||||
},
|
||||
"453": {
|
||||
"name_en": "Croagunk",
|
||||
"name_jp": "グレッグル",
|
||||
"desc": "Uses Toxic to attack opponent and temporarily decrease its defense.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/453.png"
|
||||
},
|
||||
"700": {
|
||||
"name_en": "Sylveon",
|
||||
"name_jp": "ニンフィア",
|
||||
"desc": "Uses Reflect to heal user and temporarily increase their defense.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/700.png"
|
||||
},
|
||||
"417": {
|
||||
"name_en": "Pachirisu",
|
||||
"name_jp": "パチリス",
|
||||
"desc": "Uses Follow Me to eliminate long distance attacks. Effective when get in close.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/417.png"
|
||||
},
|
||||
"129": {
|
||||
"name_en": "Magikarp",
|
||||
"name_jp": "コイキング",
|
||||
"desc": "Uses Bounce to disrupt the enemy's attack when hit by an opponent. Effective for interrupting combos.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/129.png"
|
||||
},
|
||||
"104": {
|
||||
"name_en": "Cubone",
|
||||
"name_jp": "カラカラ",
|
||||
"desc": "Uses Bonemerang to attack from a distance and can pull an enemy in.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/104.png"
|
||||
},
|
||||
"50": {
|
||||
"name_en": "Diglett",
|
||||
"name_jp": "ディグダ",
|
||||
"desc": "Uses Dig to attack from below, making easy to aim for a combo.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/50.png"
|
||||
},
|
||||
"82": {
|
||||
"name_en": "Magneton",
|
||||
"name_jp": "レアコイル",
|
||||
"desc": "Uses Tri Attack to attack from a distance diagonally upward and inflict two random negative statuses.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/82.png"
|
||||
},
|
||||
"195": {
|
||||
"name_en": "Quagsire",
|
||||
"name_jp": "ヌオー",
|
||||
"desc": "Uses Mud Bomb to attack opponent on the ground, even when blocked.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/195.png"
|
||||
},
|
||||
"196": {
|
||||
"name_en": "Espeon",
|
||||
"name_jp": "エーフィ",
|
||||
"desc": "Uses Morning Sun to remove any statuses and recover health, with more health recovered with less time remaining in the round.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/196.png"
|
||||
},
|
||||
"197": {
|
||||
"name_en": "Umbreon",
|
||||
"name_jp": "ブラッキー",
|
||||
"desc": "Uses Snarl to absorb an opponent's Synergy Gauge and prevent them from performing any critical hits.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/197.png"
|
||||
},
|
||||
"643": {
|
||||
"name_en": "Reshiram",
|
||||
"name_jp": "レシラム",
|
||||
"desc": "Uses Blue Flare to attack straight forward with a powerful flame. In the DX version, it can only be called once per round.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/643.png"
|
||||
},
|
||||
"488": {
|
||||
"name_en": "Cresselia",
|
||||
"name_jp": "クレセリア",
|
||||
"desc": "Uses Lunar Dance to heal the user of any negative status, recovers health and Synergy Gauge, but can only be used once per round.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/488.png"
|
||||
},
|
||||
"717": {
|
||||
"name_en": "Yveltal",
|
||||
"name_jp": "イベルタル",
|
||||
"desc": "Uses Oblivion Wing to attack from the sky and seal off the opponent's Synergy Burst. In the DX version, it can only be called once per round.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/717.png"
|
||||
},
|
||||
"381": {
|
||||
"name_en": "Latios",
|
||||
"name_jp": "ラティオス",
|
||||
"desc": "Uses Luster Purge to place attacks around the enemy in order to restrict their movements. In the DX version, it can only be called once per round.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/381.png"
|
||||
},
|
||||
"725": {
|
||||
"name_en": "Litten",
|
||||
"name_jp": "ニャビー",
|
||||
"desc": "Uses Fire Fang to attack toward the enemy. Damage increases when the player's at lower HP.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/725.png"
|
||||
},
|
||||
"728": {
|
||||
"name_en": "Popplio",
|
||||
"name_jp": "アシマリ",
|
||||
"desc": "Uses Bubble Beam to temporarily increase attack and grant a double jump while in midair.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/728.png"
|
||||
},
|
||||
"10079": {
|
||||
"name_en": "Mega Rayquaza",
|
||||
"name_jp": "レックウザ",
|
||||
"desc": "Uses Dragon Ascent to attack from a distance at tremendous speed. It also consumes the user's Synergy Gauge. It can only be called once per round.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/10079.png"
|
||||
},
|
||||
"778": {
|
||||
"name_en": "Mimikyu",
|
||||
"name_jp": "ミミッキュ",
|
||||
"desc": "Uses Play Rough to attack continuously from behind and inflict double negative status.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/778.png"
|
||||
},
|
||||
"151": {
|
||||
"name_en": "Mew",
|
||||
"name_jp": "ミュウ",
|
||||
"desc": "Uses Miraculous Power to randomly increase the user's Synergy Gauge, temporarily makes the user's attacks critical hits, and/or gives the user additional random positive status.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/151.png"
|
||||
},
|
||||
"251": {
|
||||
"name_en": "Celebi",
|
||||
"name_jp": "セレビィ",
|
||||
"desc": "Uses Time Travel (Japanese: ときわたり Time Travel) to switch between Phases at almost any given moment, even when enemy guards an attack.",
|
||||
"artwork": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/251.png"
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
from typing import Optional, Dict, List, Union
|
||||
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, case
|
||||
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, INTEGER
|
||||
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON
|
||||
from sqlalchemy.schema import ForeignKey
|
||||
from sqlalchemy.sql import func, select, update, delete
|
||||
from sqlalchemy.sql.functions import coalesce
|
||||
@ -16,8 +16,13 @@ profile = Table(
|
||||
"pokken_profile",
|
||||
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("trainer_name", String(14)), # optional
|
||||
Column(
|
||||
"user",
|
||||
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
||||
nullable=False,
|
||||
unique=True,
|
||||
),
|
||||
Column("trainer_name", String(16)), # optional
|
||||
Column("home_region_code", Integer),
|
||||
Column("home_loc_name", String(255)),
|
||||
Column("pref_code", Integer),
|
||||
@ -61,7 +66,7 @@ profile = Table(
|
||||
Column("navi_trainer", Integer),
|
||||
Column("navi_version_id", Integer),
|
||||
Column("aid_skill_list", JSON), # Repeated, Integer
|
||||
Column("aid_skill", Integer), # Cheer skill, 6 of them, unlocked by lucky bonus
|
||||
Column("aid_skill", Integer),
|
||||
Column("comment_text_id", Integer),
|
||||
Column("comment_word_id", Integer),
|
||||
Column("latest_use_pokemon", Integer),
|
||||
@ -100,16 +105,20 @@ profile = Table(
|
||||
Column("battle_num_vs_cpu", Integer), # 2
|
||||
Column("win_cpu", Integer),
|
||||
Column("battle_num_tutorial", Integer), # 1?
|
||||
mysql_charset="utf8mb4"
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
pokemon_data = Table(
|
||||
"pokken_pokemon_data",
|
||||
metadata,
|
||||
Column("id", Integer, primary_key=True, nullable=False),
|
||||
Column("user", Integer, ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
|
||||
Column("char_id", Integer),
|
||||
Column("illustration_book_no", Integer, nullable=False), # This is the fucking pokedex number????
|
||||
Column(
|
||||
"user",
|
||||
ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"),
|
||||
nullable=False,
|
||||
),
|
||||
Column("char_id", Integer, nullable=False),
|
||||
Column("illustration_book_no", Integer),
|
||||
Column("pokemon_exp", Integer),
|
||||
Column("battle_num_vs_wan", Integer), # 4?
|
||||
Column("win_vs_wan", Integer),
|
||||
@ -123,8 +132,8 @@ pokemon_data = Table(
|
||||
Column("bp_point_res", Integer),
|
||||
Column("bp_point_def", Integer),
|
||||
Column("bp_point_sp", Integer),
|
||||
UniqueConstraint("user", "illustration_book_no", name="pokken_pokemon_uk"),
|
||||
mysql_charset="utf8mb4"
|
||||
UniqueConstraint("user", "char_id", name="pokken_pokemon_data_uk"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
|
||||
@ -148,8 +157,8 @@ class PokkenProfileData(BaseData):
|
||||
return result.lastrowid
|
||||
|
||||
async def set_profile_name(self, user_id: int, new_name: str, gender: Union[int, None] = None) -> None:
|
||||
sql = profile.update(profile.c.user == user_id).values(
|
||||
trainer_name=new_name if new_name else profile.c.trainer_name,
|
||||
sql = update(profile).where(profile.c.user == user_id).values(
|
||||
trainer_name=new_name,
|
||||
avatar_gender=gender if gender is not None else profile.c.avatar_gender
|
||||
)
|
||||
result = await self.execute(sql)
|
||||
@ -170,12 +179,12 @@ class PokkenProfileData(BaseData):
|
||||
aid_skill: int,
|
||||
last_evt: int
|
||||
) -> None:
|
||||
sql = profile.update(profile.c.user == user_id).values(
|
||||
sql = update(profile).where(profile.c.user == user_id).values(
|
||||
extra_counter=extra_counter,
|
||||
event_reward_get_flag=evt_reward_get_flg,
|
||||
total_play_days=coalesce(profile.c.total_play_days, 0) + total_play_days,
|
||||
awake_num=coalesce(profile.c.awake_num, 0) + awake_num,
|
||||
use_support_num=coalesce(profile.c.use_support_num, 0) + use_support_ct,
|
||||
total_play_days=total_play_days,
|
||||
awake_num=awake_num,
|
||||
use_support_num=use_support_ct,
|
||||
beat_num=beat_num,
|
||||
aid_skill=aid_skill,
|
||||
last_play_event_id=last_evt
|
||||
@ -186,7 +195,7 @@ class PokkenProfileData(BaseData):
|
||||
self.logger.error(f"Failed to put extra data for user {user_id}")
|
||||
|
||||
async def update_profile_tutorial_flags(self, user_id: int, tutorial_flags: List) -> None:
|
||||
sql = profile.update(profile.c.user == user_id).values(
|
||||
sql = update(profile).where(profile.c.user == user_id).values(
|
||||
tutorial_progress_flag=tutorial_flags,
|
||||
)
|
||||
result = await self.execute(sql)
|
||||
@ -196,7 +205,7 @@ class PokkenProfileData(BaseData):
|
||||
)
|
||||
|
||||
async def update_profile_achievement_flags(self, user_id: int, achievement_flags: List) -> None:
|
||||
sql = profile.update(profile.c.user == user_id).values(
|
||||
sql = update(profile).where(profile.c.user == user_id).values(
|
||||
achievement_flag=achievement_flags,
|
||||
)
|
||||
result = await self.execute(sql)
|
||||
@ -206,7 +215,7 @@ class PokkenProfileData(BaseData):
|
||||
)
|
||||
|
||||
async def update_profile_event(self, user_id: int, event_state: List, event_flags: List[int], event_param: List[int], last_evt: int = None) -> None:
|
||||
sql = profile.update(profile.c.user == user_id).values(
|
||||
sql = update(profile).where(profile.c.user == user_id).values(
|
||||
event_state=event_state,
|
||||
event_achievement_flag=event_flags,
|
||||
event_achievement_param=event_param,
|
||||
@ -221,16 +230,12 @@ class PokkenProfileData(BaseData):
|
||||
async def add_profile_points(
|
||||
self, user_id: int, rank_pts: int, money: int, score_pts: int, grade_max: int
|
||||
) -> None:
|
||||
sql = profile.update(profile.c.user == user_id).values(
|
||||
trainer_rank_point = coalesce(profile.c.trainer_rank_point, 0) + rank_pts,
|
||||
wallet = coalesce(profile.c.wallet, 0) + money,
|
||||
score_point = coalesce(profile.c.score_point, 0) + score_pts,
|
||||
sql = update(profile).where(profile.c.user == user_id).values(
|
||||
trainer_rank_point = profile.c.trainer_rank_point + rank_pts,
|
||||
fight_money = profile.c.fight_money + money,
|
||||
score_point = profile.c.score_point + score_pts,
|
||||
grade_max_num = grade_max
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
|
||||
async def get_profile(self, user_id: int) -> Optional[Row]:
|
||||
sql = profile.select(profile.c.user == user_id)
|
||||
@ -243,7 +248,7 @@ class PokkenProfileData(BaseData):
|
||||
self,
|
||||
user_id: int,
|
||||
pokemon_id: int,
|
||||
pokedex_number: int,
|
||||
illust_no: int,
|
||||
atk: int,
|
||||
res: int,
|
||||
defe: int,
|
||||
@ -252,7 +257,7 @@ class PokkenProfileData(BaseData):
|
||||
sql = insert(pokemon_data).values(
|
||||
user=user_id,
|
||||
char_id=pokemon_id,
|
||||
illustration_book_no=pokedex_number,
|
||||
illustration_book_no=illust_no,
|
||||
pokemon_exp=0,
|
||||
battle_num_vs_wan=0,
|
||||
win_vs_wan=0,
|
||||
@ -269,7 +274,7 @@ class PokkenProfileData(BaseData):
|
||||
)
|
||||
|
||||
conflict = sql.on_duplicate_key_update(
|
||||
illustration_book_no=pokedex_number,
|
||||
illustration_book_no=illust_no,
|
||||
bp_point_atk=pokemon_data.c.bp_point_atk + atk,
|
||||
bp_point_res=pokemon_data.c.bp_point_res + res,
|
||||
bp_point_def=pokemon_data.c.bp_point_def + defe,
|
||||
@ -288,7 +293,7 @@ class PokkenProfileData(BaseData):
|
||||
pokemon_id: int,
|
||||
xp: int
|
||||
) -> None:
|
||||
sql = pokemon_data.update(and_(pokemon_data.c.user==user_id, pokemon_data.c.illustration_book_no==pokemon_id)).values(
|
||||
sql = update(pokemon_data).where(and_(pokemon_data.c.user==user_id, pokemon_data.c.char_id==pokemon_id)).values(
|
||||
pokemon_exp=coalesce(pokemon_data.c.pokemon_exp, 0) + xp
|
||||
)
|
||||
|
||||
@ -297,7 +302,7 @@ class PokkenProfileData(BaseData):
|
||||
self.logger.warning(f"Failed to add {xp} XP to pokemon ID {pokemon_id} for user {user_id}")
|
||||
|
||||
async def get_pokemon_data(self, user_id: int, pokemon_id: int) -> Optional[Row]:
|
||||
sql = pokemon_data.select(and_(pokemon_data.c.user == user_id, pokemon_data.c.illustration_book_no == pokemon_id))
|
||||
sql = pokemon_data.select(and_(pokemon_data.c.user == user_id, pokemon_data.c.char_id == pokemon_id))
|
||||
result = await self.execute(sql)
|
||||
if result is None:
|
||||
return None
|
||||
@ -310,14 +315,6 @@ class PokkenProfileData(BaseData):
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def set_latest_mon(self, user_id: int, pokedex_no: int) -> None:
|
||||
sql = profile.update(profile.c.user == user_id).values(
|
||||
latest_use_pokemon=pokedex_no
|
||||
)
|
||||
result = await self.execute(sql)
|
||||
if result is None:
|
||||
self.logger.warning(f"Failed to update user {user_id}'s last used pokemon {pokedex_no}")
|
||||
|
||||
async def put_pokemon_battle_result(
|
||||
self, user_id: int, pokemon_id: int, match_type: PokkenConstants.BATTLE_TYPE, match_result: PokkenConstants.BATTLE_RESULT
|
||||
) -> None:
|
||||
@ -325,7 +322,7 @@ class PokkenProfileData(BaseData):
|
||||
Records the match stats (type and win/loss) for the pokemon and profile
|
||||
coalesce(pokemon_data.c.win_vs_wan, 0)
|
||||
"""
|
||||
sql = pokemon_data.update(and_(pokemon_data.c.user==user_id, pokemon_data.c.illustration_book_no==pokemon_id)).values(
|
||||
sql = update(pokemon_data).where(and_(pokemon_data.c.user==user_id, pokemon_data.c.char_id==pokemon_id)).values(
|
||||
battle_num_tutorial=coalesce(pokemon_data.c.battle_num_tutorial, 0) + 1 if match_type==PokkenConstants.BATTLE_TYPE.TUTORIAL else coalesce(pokemon_data.c.battle_num_tutorial, 0),
|
||||
battle_all_num_tutorial=coalesce(pokemon_data.c.battle_all_num_tutorial, 0) + 1 if match_type==PokkenConstants.BATTLE_TYPE.TUTORIAL else coalesce(pokemon_data.c.battle_all_num_tutorial, 0),
|
||||
|
||||
@ -356,7 +353,7 @@ class PokkenProfileData(BaseData):
|
||||
"""
|
||||
Records profile stats
|
||||
"""
|
||||
sql = profile.update(profile.c.user==user_id).values(
|
||||
sql = update(profile).where(profile.c.user==user_id).values(
|
||||
ex_ko_num=coalesce(profile.c.ex_ko_num, 0) + exkos,
|
||||
wko_num=coalesce(profile.c.wko_num, 0) + wkos,
|
||||
timeup_win_num=coalesce(profile.c.timeup_win_num, 0) + timeout_wins,
|
||||
@ -370,12 +367,7 @@ class PokkenProfileData(BaseData):
|
||||
self.logger.warning(f"Failed to update stats for user {user_id}")
|
||||
|
||||
async def update_support_team(self, user_id: int, support_id: int, support1: int = None, support2: int = None) -> None:
|
||||
if support1 == 4294967295:
|
||||
support1 = None
|
||||
|
||||
if support2 == 4294967295:
|
||||
support2 = None
|
||||
sql = profile.update(profile.c.user==user_id).values(
|
||||
sql = update(profile).where(profile.c.user==user_id).values(
|
||||
support_set_1_1=support1 if support_id == 1 else profile.c.support_set_1_1,
|
||||
support_set_1_2=support2 if support_id == 1 else profile.c.support_set_1_2,
|
||||
support_set_2_1=support1 if support_id == 2 else profile.c.support_set_2_1,
|
||||
@ -387,15 +379,3 @@ class PokkenProfileData(BaseData):
|
||||
result = await self.execute(sql)
|
||||
if result is None:
|
||||
self.logger.warning(f"Failed to update support team {support_id} for user {user_id}")
|
||||
|
||||
async def update_rankmatch_data(self, user_id: int, flag: int, rm_max: Optional[int], success: Optional[int], progress: List[int]) -> None:
|
||||
sql = profile.update(profile.c.user==user_id).values(
|
||||
rankmatch_flag=flag,
|
||||
rankmatch_max=rm_max,
|
||||
rankmatch_progress=progress,
|
||||
rankmatch_success=success,
|
||||
)
|
||||
|
||||
result = await self.execute(sql)
|
||||
if result is None:
|
||||
self.logger.warning(f"Failed to update rankmatch data for user {user_id}")
|
||||
|
@ -83,10 +83,6 @@ class SaoBase:
|
||||
if not user_id:
|
||||
user_id = await self.data.user.create_user() #works
|
||||
card_id = await self.data.card.create_card(user_id, req.access_code)
|
||||
if req.access_code.startswith("5"):
|
||||
await self.data.card.set_idm_by_access_code(card_id, req.chip_id[:16])
|
||||
elif req.access_code.startswith("010") or req.access_code.startswith("3"):
|
||||
await self.data.card.set_chip_id_by_access_code(card_id, int(req.chip_id[:8], 16))
|
||||
|
||||
if card_id is None:
|
||||
user_id = -1
|
||||
|
@ -833,14 +833,14 @@ class WaccaBase:
|
||||
# TODO: Coop and vs data
|
||||
async def handle_user_music_updateCoop_request(self, data: Dict) -> Dict:
|
||||
coop_info = data["params"][4]
|
||||
return await self.handle_user_music_update_request(data)
|
||||
return self.handle_user_music_update_request(data)
|
||||
|
||||
async def handle_user_music_updateVersus_request(self, data: Dict) -> Dict:
|
||||
vs_info = data["params"][4]
|
||||
return await self.handle_user_music_update_request(data)
|
||||
return self.handle_user_music_update_request(data)
|
||||
|
||||
async def handle_user_music_updateTrial_request(self, data: Dict) -> Dict:
|
||||
return await self.handle_user_music_update_request(data)
|
||||
return self.handle_user_music_update_request(data)
|
||||
|
||||
async def handle_user_mission_update_request(self, data: Dict) -> Dict:
|
||||
req = UserMissionUpdateRequest(data)
|
||||
|
Reference in New Issue
Block a user