Compare commits

...

10 Commits

14 changed files with 68 additions and 28 deletions

View File

@ -1,7 +1,5 @@
from core.config import CoreConfig from core.config import CoreConfig
from core.allnet import AllnetServlet, BillingServlet
from core.aimedb import AimedbServlette from core.aimedb import AimedbServlette
from core.title import TitleServlet from core.title import TitleServlet
from core.utils import Utils from core.utils import Utils
from core.mucha import MuchaServlet from core.mucha import MuchaServlet
from core.frontend import FrontendServlet

View File

@ -120,7 +120,7 @@ class ADBHeader:
if self.store_id == 0: if self.store_id == 0:
raise ADBHeaderException(f"Store ID cannot be 0!") raise ADBHeaderException(f"Store ID cannot be 0!")
if re.fullmatch(r"^A[0-9]{2}[E|X][0-9]{2}[A-HJ-NP-Z][0-9]{4}$", self.keychip_id) is None: if re.fullmatch(r"^A[0-9]{2}[A-Z][0-9]{2}[A-HJ-NP-Z][0-9]{4}$", self.keychip_id) is None:
raise ADBHeaderException(f"Keychip ID {self.keychip_id} is invalid!") raise ADBHeaderException(f"Keychip ID {self.keychip_id} is invalid!")
return True return True

View File

@ -9,7 +9,8 @@ from starlette.responses import PlainTextResponse
from os import environ, path, mkdir, W_OK, access from os import environ, path, mkdir, W_OK, access
from typing import List from typing import List
from core import CoreConfig, TitleServlet, MuchaServlet, AllnetServlet, BillingServlet, AimedbServlette from core import CoreConfig, TitleServlet, MuchaServlet
from core.allnet import AllnetServlet, BillingServlet
from core.frontend import FrontendServlet from core.frontend import FrontendServlet
async def dummy_rt(request: Request): async def dummy_rt(request: Request):

View File

@ -232,7 +232,7 @@ class ArcadeData(BaseData):
return f"{platform_code}{'-' if dash else ''}{platform_rev:02d}{serial_letter}{serial_num:04d}{append:04d}" return f"{platform_code}{'-' if dash else ''}{platform_rev:02d}{serial_letter}{serial_num:04d}{append:04d}"
def validate_keychip_format(self, serial: str) -> bool: def validate_keychip_format(self, serial: str) -> bool:
# For the 2nd letter, E and X are the only "real" values that have been observed # For the 2nd letter, E and X are the only "real" values that have been observed (A is used for generated keychips)
if re.fullmatch(r"^A[0-9]{2}[A-Z][-]?[0-9]{2}[A-HJ-NP-Z][0-9]{4}([0-9]{4})?$", serial) is None: if re.fullmatch(r"^A[0-9]{2}[A-Z][-]?[0-9]{2}[A-HJ-NP-Z][0-9]{4}([0-9]{4})?$", serial) is None:
return False return False

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import argparse import argparse
import logging import logging
from os import mkdir, path, access, W_OK from os import mkdir, path, access, W_OK, environ
import yaml import yaml
import asyncio import asyncio
@ -25,10 +25,11 @@ if __name__ == "__main__":
parser.add_argument("action", type=str, help="create, upgrade, downgrade, create-owner, migrate, create-revision, create-autorevision") parser.add_argument("action", type=str, help="create, upgrade, downgrade, create-owner, migrate, create-revision, create-autorevision")
args = parser.parse_args() args = parser.parse_args()
environ["ARTEMIS_CFG_DIR"] = args.config
cfg = CoreConfig() cfg = CoreConfig()
if path.exists(f"{args.config}/core.yaml"): if path.exists(f"{args.config}/core.yaml"):
cfg_dict = yaml.safe_load(open(f"{args.config}/core.yaml")) cfg_dict = yaml.safe_load(open(f"{args.config}/core.yaml"))
cfg_dict.get("database", {})["loglevel"] = "info"
cfg.update(cfg_dict) cfg.update(cfg_dict)
if not path.exists(cfg.server.log_dir): if not path.exists(cfg.server.log_dir):

View File

@ -81,10 +81,12 @@ The importer for Chunithm will import: Events, Music, Charge Items and Avatar Ac
Config file is located in `config/chuni.yaml`. Config file is located in `config/chuni.yaml`.
| Option | Info | | Option | Info |
|------------------|----------------------------------------------------------------------------------------------------------------| |------------------|---------------------------------------------------------------------------------------------------------------------|
| `news_msg` | If this is set, the news at the top of the main screen will be displayed (up to Chunithm Paradise Lost) | | `news_msg` | If this is set, the news at the top of the main screen will be displayed (up to Chunithm Paradise Lost) |
| `name` | If this is set, all players that are not on a team will use this one by default. | | `name` | If this is set, all players that are not on a team will use this one by default. |
| `use_login_bonus`| This is used to enable the login bonuses | | `use_login_bonus`| This is used to enable the login bonuses |
| `stock_tickets` | If this is set, specifies tickets to auto-stock at login. Format is a comma-delimited list of IDs. Defaults to None |
| `stock_count` | Ignored if stock_tickets is not specified. Number to stock of each ticket. Defaults to 99 |
| `crypto` | This option is used to enable the TLS Encryption | | `crypto` | This option is used to enable the TLS Encryption |

View File

@ -8,6 +8,10 @@ team:
mods: mods:
use_login_bonus: True use_login_bonus: True
# stock_tickets allows specified ticket IDs to be auto-stocked at login. Format is a comma-delimited string of ticket IDs
# note: quanity is not refreshed on "continue" after set - only on subsequent login
stock_tickets:
stock_count: 99
version: version:
11: 11:

View File

@ -6,7 +6,8 @@ import uvicorn
import logging import logging
import asyncio import asyncio
from core import CoreConfig, AimedbServlette from core.config import CoreConfig
from core.aimedb import AimedbServlette
async def launch_main(cfg: CoreConfig, ssl: bool) -> None: async def launch_main(cfg: CoreConfig, ssl: bool) -> None:
if ssl: if ssl:

View File

@ -24,20 +24,35 @@ class ChuniBase:
async def handle_game_login_api_request(self, data: Dict) -> Dict: async def handle_game_login_api_request(self, data: Dict) -> Dict:
""" """
Handles the login bonus logic, required for the game because Handles the login bonus and ticket stock logic, required for the game
getUserLoginBonus gets called after getUserItem and therefore the because getUserLoginBonus gets called after getUserItem; therefore the
items needs to be inserted in the database before they get requested. items needs to be inserted in the database before they get requested.
Adds a bonusCount after a user logged in after 24 hours, makes sure - Adds a stock for each specified ticket (itemKind 5)
- Adds a bonusCount after a user logged in after 24 hours, makes sure
loginBonus 30 gets looped, only show the login banner every 24 hours, loginBonus 30 gets looped, only show the login banner every 24 hours,
adds the bonus to items (itemKind 6) adds the bonus to items (itemKind 6)
""" """
user_id = data["userId"]
# If we want to make certain tickets always available, stock them now
if self.game_cfg.mods.stock_tickets:
for ticket in self.game_cfg.mods.stock_tickets.split(","):
await self.data.item.put_item(
user_id,
{
"itemId": ticket.strip(),
"itemKind": 5,
"stock": self.game_cfg.mods.stock_count,
"isValid": True,
},
)
# ignore the login bonus if disabled in config # ignore the login bonus if disabled in config
if not self.game_cfg.mods.use_login_bonus: if not self.game_cfg.mods.use_login_bonus:
return {"returnCode": 1} return {"returnCode": 1}
user_id = data["userId"]
login_bonus_presets = await self.data.static.get_login_bonus_presets(self.version) login_bonus_presets = await self.data.static.get_login_bonus_presets(self.version)
for preset in login_bonus_presets: for preset in login_bonus_presets:

View File

@ -53,6 +53,18 @@ class ChuniModsConfig:
self.__config, "chuni", "mods", "use_login_bonus", default=True self.__config, "chuni", "mods", "use_login_bonus", default=True
) )
@property
def stock_tickets(self) -> str:
return CoreConfig.get_config_field(
self.__config, "chuni", "mods", "stock_tickets", default=None
)
@property
def stock_count(self) -> int:
return CoreConfig.get_config_field(
self.__config, "chuni", "mods", "stock_count", default=99
)
class ChuniVersionConfig: class ChuniVersionConfig:
def __init__(self, parent_config: "ChuniConfig") -> None: def __init__(self, parent_config: "ChuniConfig") -> None:

View File

@ -104,7 +104,8 @@ class ChuniNew(ChuniBase):
return {"returnCode": "1"} return {"returnCode": "1"}
async def handle_get_user_map_area_api_request(self, data: Dict) -> Dict: async def handle_get_user_map_area_api_request(self, data: Dict) -> Dict:
user_map_areas = await self.data.item.get_map_areas(data["userId"]) map_area_ids = [int(area["mapAreaId"]) for area in data["mapAreaIdList"]]
user_map_areas = await self.data.item.get_map_areas(data["userId"], map_area_ids)
map_areas = [] map_areas = []
for map_area in user_map_areas: for map_area in user_map_areas:

View File

@ -36,10 +36,14 @@ class ChuniReader(BaseReader):
if self.opt_dir is not None: if self.opt_dir is not None:
data_dirs += self.get_data_directories(self.opt_dir) data_dirs += self.get_data_directories(self.opt_dir)
we_diff = "4"
if self.version >= ChuniConstants.VER_CHUNITHM_NEW:
we_diff = "5"
for dir in data_dirs: for dir in data_dirs:
self.logger.info(f"Read from {dir}") self.logger.info(f"Read from {dir}")
await self.read_events(f"{dir}/event") await self.read_events(f"{dir}/event")
await self.read_music(f"{dir}/music") await self.read_music(f"{dir}/music", we_diff)
await self.read_charges(f"{dir}/chargeItem") await self.read_charges(f"{dir}/chargeItem")
await self.read_avatar(f"{dir}/avatarAccessory") await self.read_avatar(f"{dir}/avatarAccessory")
await self.read_login_bonus(f"{dir}/") await self.read_login_bonus(f"{dir}/")
@ -138,7 +142,7 @@ class ChuniReader(BaseReader):
else: else:
self.logger.warning(f"Failed to insert event {id}") self.logger.warning(f"Failed to insert event {id}")
async def read_music(self, music_dir: str) -> None: async def read_music(self, music_dir: str, we_diff: str = "4") -> None:
for root, dirs, files in walk(music_dir): for root, dirs, files in walk(music_dir):
for dir in dirs: for dir in dirs:
if path.exists(f"{root}/{dir}/Music.xml"): if path.exists(f"{root}/{dir}/Music.xml"):
@ -169,7 +173,7 @@ class ChuniReader(BaseReader):
chart_type = MusicFumenData.find("type") chart_type = MusicFumenData.find("type")
chart_id = chart_type.find("id").text chart_id = chart_type.find("id").text
chart_diff = chart_type.find("str").text chart_diff = chart_type.find("str").text
if chart_diff == "WorldsEnd" and (chart_id == "4" or chart_id == "5"): # 4 in SDBT, 5 in SDHD if chart_diff == "WorldsEnd" and chart_id == we_diff: # 4 in SDBT, 5 in SDHD
level = float(xml_root.find("starDifType").text) level = float(xml_root.find("starDifType").text)
we_chara = ( we_chara = (
xml_root.find("worldsEndTagName") xml_root.find("worldsEndTagName")

View File

@ -533,8 +533,8 @@ class ChuniItemData(BaseData):
return None return None
return result.lastrowid return result.lastrowid
async def get_map_areas(self, user_id: int) -> Optional[List[Row]]: async def get_map_areas(self, user_id: int, map_area_ids: List[int]) -> Optional[List[Row]]:
sql = select(map_area).where(map_area.c.user == user_id) sql = select(map_area).where(map_area.c.user == user_id, map_area.c.mapAreaId.in_(map_area_ids))
result = await self.execute(sql) result = await self.execute(sql)
if result is None: if result is None:

View File

@ -818,7 +818,8 @@ class Mai2Base:
} }
async def handle_upload_user_portrait_api_request(self, data: Dict) -> Dict: async def handle_upload_user_portrait_api_request(self, data: Dict) -> Dict:
self.logger.debug(data) self.logger.warning("Portrait uploading not supported at this time.")
return {'returnCode': 0, 'apiName': 'UploadUserPortraitApi'}
async def handle_upload_user_photo_api_request(self, data: Dict) -> Dict: async def handle_upload_user_photo_api_request(self, data: Dict) -> Dict:
if not self.game_config.uploads.photos or not self.game_config.uploads.photos_dir: if not self.game_config.uploads.photos or not self.game_config.uploads.photos_dir: