diff --git a/core/allnet.py b/core/allnet.py index 6610c23..b0e1c78 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -147,7 +147,7 @@ class AllnetServlet: resp_dict = {k: v for k, v in vars(resp).items() if v is not None} return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8") - elif not arcade["ip"] or arcade["ip"] is None and self.config.server.strict_ip_checking: + elif (not arcade["ip"] or arcade["ip"] is None) and self.config.server.strict_ip_checking: msg = f"Serial {req.serial} attempted allnet auth from bad IP {req.ip}, but arcade {arcade['id']} has no IP set! (strict checking enabled)." self.data.base.log_event( "allnet", "ALLNET_AUTH_NO_SHOP_IP", logging.ERROR, msg diff --git a/core/config.py b/core/config.py index 14f06f5..3a772f7 100644 --- a/core/config.py +++ b/core/config.py @@ -85,6 +85,18 @@ class TitleConfig: self.__config, "core", "title", "port", default=8080 ) + @property + def reboot_start_time(self) -> str: + return CoreConfig.get_config_field( + self.__config, "core", "title", "reboot_start_time", default="" + ) + + @property + def reboot_end_time(self) -> str: + return CoreConfig.get_config_field( + self.__config, "core", "title", "reboot_end_time", default="" + ) + class DatabaseConfig: def __init__(self, parent_config: "CoreConfig") -> None: diff --git a/docs/config.md b/docs/config.md index 18f90eb..9de57f9 100644 --- a/docs/config.md +++ b/docs/config.md @@ -6,11 +6,15 @@ - `name`: Name for the server, used by some games in their default MOTDs. Default `ARTEMiS` - `is_develop`: Flags that the server is a development instance without a proxy standing in front of it. Setting to `False` tells the server not to listen for SSL, because the proxy should be handling all SSL-related things, among other things. Default `True` - `threading`: Flags that `reactor.run` should be called via the `Thread` standard library. May provide a speed boost, but removes the ability to kill the server via `Ctrl + C`. Default: `False` +- `check_arcade_ip`: Checks IPs against the `arcade` table in the database, if one is defined. Default `False` +- `strict_ip_checking`: Rejects clients if there is no IP in the `arcade` table for the respective arcade - `log_dir`: Directory to store logs. Server MUST have read and write permissions to this directory or you will have issues. Default `logs` ## Title - `loglevel`: Logging level for the title server. Default `info` - `hostname`: Hostname that gets sent to clients to tell them where to connect. Games must be able to connect to your server via the hostname or IP you spcify here. Note that most games will reject `localhost` or `127.0.0.1`. Default `localhost` - `port`: Port that the title server will listen for connections on. Set to 0 to use the Allnet handler to reduce the port footprint. Default `8080` +- `reboot_start_time`: 24 hour JST time that clients will see as the start of maintenance period. Leave blank for no maintenance time. Default: "" +- `reboot_end_time`: 24 hour JST time that clients will see as the end of maintenance period. Leave blank for no maintenance time. Default: "" ## Database - `host`: Host of the database. Default `localhost` - `username`: Username of the account the server should connect to the database with. Default `aime` diff --git a/example_config/core.yaml b/example_config/core.yaml index 76ec5b1..7bf097e 100644 --- a/example_config/core.yaml +++ b/example_config/core.yaml @@ -13,6 +13,9 @@ title: loglevel: "info" hostname: "localhost" port: 8080 + reboot_start_time: "04:00" + reboot_end_time: "05:00" + database: host: "localhost" diff --git a/titles/chuni/base.py b/titles/chuni/base.py index 21a6719..775b09b 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -200,12 +200,30 @@ class ChuniBase: return {"type": data["type"], "length": 0, "gameSaleList": []} def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - reboot_start = datetime.strftime( - datetime.now() - timedelta(hours=4), self.date_time_format - ) - reboot_end = datetime.strftime( - datetime.now() - timedelta(hours=3), self.date_time_format - ) + # if reboot start/end time is not defined use the default behavior of being a few hours ago + if self.core_cfg.title.reboot_start_time == "" or self.core_cfg.title.reboot_end_time == "": + reboot_start = datetime.strftime( + datetime.utcnow() + timedelta(hours=6), self.date_time_format + ) + reboot_end = datetime.strftime( + datetime.utcnow() + timedelta(hours=7), self.date_time_format + ) + else: + # get current datetime in JST + current_jst = datetime.now(pytz.timezone('Asia/Tokyo')).date() + + # parse config start/end times into datetime + reboot_start_time = datetime.strptime(self.core_cfg.title.reboot_start_time, "%H:%M") + reboot_end_time = datetime.strptime(self.core_cfg.title.reboot_end_time, "%H:%M") + + # offset datetimes with current date/time + reboot_start_time = reboot_start_time.replace(year=current_jst.year, month=current_jst.month, day=current_jst.day, tzinfo=pytz.timezone('Asia/Tokyo')) + reboot_end_time = reboot_end_time.replace(year=current_jst.year, month=current_jst.month, day=current_jst.day, tzinfo=pytz.timezone('Asia/Tokyo')) + + # create strings for use in gameSetting + reboot_start = reboot_start_time.strftime(self.date_time_format) + reboot_end = reboot_end_time.strftime(self.date_time_format) + return { "gameSetting": { "dataVersion": "1.00.00", @@ -385,26 +403,24 @@ class ChuniBase: } def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict: - m = self.data.score.get_rival_music(data["rivalId"], data["nextIndex"], data["maxCount"]) - if m is None: - return {} - + rival_id = data["rivalId"] + next_index = int(data["nextIndex"]) + max_count = int(data["maxCount"]) user_rival_music_list = [] - for music in m: - actual_music_id = self.data.static.get_song(music["musicId"]) - if actual_music_id is None: - music_id = music["musicId"] - else: - music_id = actual_music_id["songId"] + + # Fetch all the rival music entries for the user + all_entries = self.data.score.get_rival_music(rival_id) + + # Process the entries based on max_count and nextIndex + for music in all_entries[next_index:]: + music_id = music["musicId"] level = music["level"] score = music["score"] rank = music["rank"] - # Find the existing entry for the current musicId in the user_rival_music_list + # Create a music entry for the current music_id if it's unique music_entry = next((entry for entry in user_rival_music_list if entry["musicId"] == music_id), None) - if music_entry is None: - # If the entry doesn't exist, create a new entry music_entry = { "musicId": music_id, "length": 0, @@ -412,52 +428,32 @@ class ChuniBase: } user_rival_music_list.append(music_entry) - # Check if the current score is higher than the previous highest score for the level - level_entry = next( - ( - entry - for entry in music_entry["userRivalMusicDetailList"] - if entry["level"] == level - ), - None, - ) - if level_entry is None or score > level_entry["scoreMax"]: - # If the level entry doesn't exist or the score is higher, update or add the entry + # Create a level entry for the current level if it's unique or has a higher score + level_entry = next((entry for entry in music_entry["userRivalMusicDetailList"] if entry["level"] == level), None) + if level_entry is None: level_entry = { "level": level, "scoreMax": score, "scoreRank": rank } + music_entry["userRivalMusicDetailList"].append(level_entry) + elif score > level_entry["scoreMax"]: + level_entry["scoreMax"] = score + level_entry["scoreRank"] = rank - if level_entry not in music_entry["userRivalMusicDetailList"]: - music_entry["userRivalMusicDetailList"].append(level_entry) - + # Calculate the length for each "musicId" by counting the unique levels + for music_entry in user_rival_music_list: music_entry["length"] = len(music_entry["userRivalMusicDetailList"]) + # Prepare the result dictionary with user rival music data result = { "userId": data["userId"], "rivalId": data["rivalId"], - "nextIndex": -1, - "userRivalMusicList": user_rival_music_list + "nextIndex": str(next_index + len(all_entries) if len(all_entries) <= len(user_rival_music_list) else -1), + "userRivalMusicList": user_rival_music_list[:max_count] } return result - def handle_get_user_rival_music_api_requestded(self, data: Dict) -> Dict: - m = self.data.score.get_rival_music(data["rivalId"], data["nextIndex"], data["maxCount"]) - if m is None: - return {} - - userRivalMusicList = [] - for music in m: - self.logger.debug(music["point"]) - - return { - "userId": data["userId"], - "rivalId": data["rivalId"], - "nextIndex": -1 - - } - def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict: user_fav_item_list = [] @@ -711,11 +707,7 @@ class ChuniBase: if team: team_id = team["id"] team_name = team["teamName"] - # Determine whether to use scaled ranks, or original system - if self.game_cfg.team.rank_scale: - team_rank = self.data.profile.get_team_rank(team["id"]) - else: - team_rank = self.data.profile.get_team_rank_actual(team["id"]) + team_rank = self.data.profile.get_team_rank(team["id"]) # Don't return anything if no team name has been defined for defaults and there is no team set for the player if not profile["teamId"] and team_name == "": @@ -888,4 +880,4 @@ class ChuniBase: return { "userId": data["userId"], "userNetBattleData": {"recentNBSelectMusicList": []}, - } \ No newline at end of file + } diff --git a/titles/chuni/new.py b/titles/chuni/new.py index 40dee9b..8a658bf 100644 --- a/titles/chuni/new.py +++ b/titles/chuni/new.py @@ -3,6 +3,7 @@ from datetime import datetime, timedelta from random import randint from typing import Dict +import pytz from core.config import CoreConfig from titles.chuni.const import ChuniConstants from titles.chuni.database import ChuniData @@ -31,12 +32,29 @@ class ChuniNew(ChuniBase): match_end = datetime.strftime( datetime.utcnow() + timedelta(hours=16), self.date_time_format ) - reboot_start = datetime.strftime( - datetime.utcnow() + timedelta(hours=6), self.date_time_format - ) - reboot_end = datetime.strftime( - datetime.utcnow() + timedelta(hours=7), self.date_time_format - ) + # if reboot start/end time is not defined use the default behavior of being a few hours ago + if self.core_cfg.title.reboot_start_time == "" or self.core_cfg.title.reboot_end_time == "": + reboot_start = datetime.strftime( + datetime.utcnow() + timedelta(hours=6), self.date_time_format + ) + reboot_end = datetime.strftime( + datetime.utcnow() + timedelta(hours=7), self.date_time_format + ) + else: + # get current datetime in JST + current_jst = datetime.now(pytz.timezone('Asia/Tokyo')).date() + + # parse config start/end times into datetime + reboot_start_time = datetime.strptime(self.core_cfg.title.reboot_start_time, "%H:%M") + reboot_end_time = datetime.strptime(self.core_cfg.title.reboot_end_time, "%H:%M") + + # offset datetimes with current date/time + reboot_start_time = reboot_start_time.replace(year=current_jst.year, month=current_jst.month, day=current_jst.day, tzinfo=pytz.timezone('Asia/Tokyo')) + reboot_end_time = reboot_end_time.replace(year=current_jst.year, month=current_jst.month, day=current_jst.day, tzinfo=pytz.timezone('Asia/Tokyo')) + + # create strings for use in gameSetting + reboot_start = reboot_start_time.strftime(self.date_time_format) + reboot_end = reboot_end_time.strftime(self.date_time_format) return { "gameSetting": { "isMaintenance": False, diff --git a/titles/chuni/schema/profile.py b/titles/chuni/schema/profile.py index fa7a4ab..ea70583 100644 --- a/titles/chuni/schema/profile.py +++ b/titles/chuni/schema/profile.py @@ -646,7 +646,7 @@ class ChuniProfileData(BaseData): return None return result.fetchone() - def get_team_rank_actual(self, team_id: int) -> int: + def get_team_rank(self, team_id: int) -> int: # Normal ranking system, likely the one used in the real servers # Query all teams sorted by 'teamPoint' result = self.execute( @@ -663,42 +663,8 @@ class ChuniProfileData(BaseData): # Return the rank if found, or a default rank otherwise return rank if rank is not None else 0 - def get_team_rank(self, team_id: int) -> int: - # Scaled ranking system, designed for smaller instances. - # Query all teams sorted by 'teamPoint' - result = self.execute( - select(team.c.id).order_by(team.c.teamPoint.desc()) - ) - - # Count total number of teams - total_teams = self.execute(select(func.count()).select_from(team)).scalar() - - # Get the rank of the team with the given team_id - rank = None - for i, row in enumerate(result, start=1): - if row.id == team_id: - rank = i - break - - # If the team is not found, return default rank - if rank is None: - return 0 - - # Define rank tiers - tiers = { - 1: range(1, int(total_teams * 0.1) + 1), # Rainbow - 2: range(int(total_teams * 0.1) + 1, int(total_teams * 0.4) + 1), # Gold - 3: range(int(total_teams * 0.4) + 1, int(total_teams * 0.7) + 1), # Silver - 4: range(int(total_teams * 0.7) + 1, total_teams + 1), # Grey - } - - # Assign rank based on tier - for tier_rank, tier_range in tiers.items(): - if rank in tier_range: - return tier_rank - - # Return default rank if not found in any tier - return 0 + # RIP scaled team ranking. Gone, but forgotten + # def get_team_rank_scaled(self, team_id: int) -> int: def update_team(self, team_id: int, team_data: Dict) -> bool: team_data["id"] = team_id @@ -736,4 +702,4 @@ class ChuniProfileData(BaseData): total_play_count += row[0] return { "total_play_count": total_play_count - } \ No newline at end of file + } diff --git a/titles/chuni/schema/score.py b/titles/chuni/schema/score.py index ab26f5f..67cf141 100644 --- a/titles/chuni/schema/score.py +++ b/titles/chuni/schema/score.py @@ -201,9 +201,9 @@ class ChuniScoreData(BaseData): return None return result.lastrowid - def get_rival_music(self, rival_id: int, index: int, max_count: int) -> Optional[List[Dict]]: - sql = select(playlog).where(playlog.c.user == rival_id).limit(max_count).offset(index) + def get_rival_music(self, rival_id: int) -> Optional[List[Dict]]: + sql = select(playlog).where(playlog.c.user == rival_id) result = self.execute(sql) if result is None: return None - return result.fetchall() \ No newline at end of file + return result.fetchall() diff --git a/titles/cm/base.py b/titles/cm/base.py index dae6ecb..587ceb5 100644 --- a/titles/cm/base.py +++ b/titles/cm/base.py @@ -4,6 +4,7 @@ import json import logging from enum import Enum +import pytz from core.config import CoreConfig from core.data.cache import cached from titles.cm.const import CardMakerConstants @@ -61,12 +62,29 @@ class CardMakerBase: } def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - reboot_start = date.strftime( - datetime.now() + timedelta(hours=3), self.date_time_format - ) - reboot_end = date.strftime( - datetime.now() + timedelta(hours=4), self.date_time_format - ) + # if reboot start/end time is not defined use the default behavior of being a few hours ago + if self.core_cfg.title.reboot_start_time == "" or self.core_cfg.title.reboot_end_time == "": + reboot_start = datetime.strftime( + datetime.utcnow() + timedelta(hours=6), self.date_time_format + ) + reboot_end = datetime.strftime( + datetime.utcnow() + timedelta(hours=7), self.date_time_format + ) + else: + # get current datetime in JST + current_jst = datetime.now(pytz.timezone('Asia/Tokyo')).date() + + # parse config start/end times into datetime + reboot_start_time = datetime.strptime(self.core_cfg.title.reboot_start_time, "%H:%M") + reboot_end_time = datetime.strptime(self.core_cfg.title.reboot_end_time, "%H:%M") + + # offset datetimes with current date/time + reboot_start_time = reboot_start_time.replace(year=current_jst.year, month=current_jst.month, day=current_jst.day, tzinfo=pytz.timezone('Asia/Tokyo')) + reboot_end_time = reboot_end_time.replace(year=current_jst.year, month=current_jst.month, day=current_jst.day, tzinfo=pytz.timezone('Asia/Tokyo')) + + # create strings for use in gameSetting + reboot_start = reboot_start_time.strftime(self.date_time_format) + reboot_end = reboot_end_time.strftime(self.date_time_format) # grab the dict with all games version numbers from user config games_ver = self.game_cfg.version.version(self.version) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index ad0433f..26677ac 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -5,6 +5,7 @@ from base64 import b64decode from os import path, stat, remove from PIL import ImageFile +import pytz from core.config import CoreConfig from titles.mai2.const import Mai2Constants from titles.mai2.config import Mai2Config @@ -21,22 +22,47 @@ class Mai2Base: self.can_deliver = False self.can_usbdl = False self.old_server = "" - + if self.core_config.server.is_develop and self.core_config.title.port > 0: self.old_server = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDEY/197/" - + else: self.old_server = f"http://{self.core_config.title.hostname}/SDEY/197/" def handle_get_game_setting_api_request(self, data: Dict): - return { + # if reboot start/end time is not defined use the default behavior of being a few hours ago + if self.core_cfg.title.reboot_start_time == "" or self.core_cfg.title.reboot_end_time == "": + reboot_start = datetime.strftime( + datetime.utcnow() + timedelta(hours=6), self.date_time_format + ) + reboot_end = datetime.strftime( + datetime.utcnow() + timedelta(hours=7), self.date_time_format + ) + else: + # get current datetime in JST + current_jst = datetime.now(pytz.timezone('Asia/Tokyo')).date() + + # parse config start/end times into datetime + reboot_start_time = datetime.strptime(self.core_cfg.title.reboot_start_time, "%H:%M") + reboot_end_time = datetime.strptime(self.core_cfg.title.reboot_end_time, "%H:%M") + + # offset datetimes with current date/time + reboot_start_time = reboot_start_time.replace(year=current_jst.year, month=current_jst.month, day=current_jst.day, tzinfo=pytz.timezone('Asia/Tokyo')) + reboot_end_time = reboot_end_time.replace(year=current_jst.year, month=current_jst.month, day=current_jst.day, tzinfo=pytz.timezone('Asia/Tokyo')) + + # create strings for use in gameSetting + reboot_start = reboot_start_time.strftime(self.date_time_format) + reboot_end = reboot_end_time.strftime(self.date_time_format) + + + return { "isDevelop": False, "isAouAccession": False, "gameSetting": { "isMaintenance": False, "requestInterval": 1800, - "rebootStartTime": "2020-01-01 07:00:00.0", - "rebootEndTime": "2020-01-01 07:59:59.0", + "rebootStartTime": reboot_start, + "rebootEndTime": reboot_end, "movieUploadLimit": 100, "movieStatus": 1, "movieServerUri": self.old_server + "api/movie" if self.game_config.uploads.movies else "movie", diff --git a/titles/ongeki/base.py b/titles/ongeki/base.py index ccf24f5..c72c086 100644 --- a/titles/ongeki/base.py +++ b/titles/ongeki/base.py @@ -4,6 +4,7 @@ import json import logging from enum import Enum +import pytz from core.config import CoreConfig from core.data.cache import cached from titles.ongeki.const import OngekiConstants @@ -103,12 +104,30 @@ class OngekiBase: self.version = OngekiConstants.VER_ONGEKI def handle_get_game_setting_api_request(self, data: Dict) -> Dict: - reboot_start = date.strftime( - datetime.now() + timedelta(hours=3), self.date_time_format - ) - reboot_end = date.strftime( - datetime.now() + timedelta(hours=4), self.date_time_format - ) + # if reboot start/end time is not defined use the default behavior of being a few hours ago + if self.core_cfg.title.reboot_start_time == "" or self.core_cfg.title.reboot_end_time == "": + reboot_start = datetime.strftime( + datetime.utcnow() + timedelta(hours=6), self.date_time_format + ) + reboot_end = datetime.strftime( + datetime.utcnow() + timedelta(hours=7), self.date_time_format + ) + else: + # get current datetime in JST + current_jst = datetime.now(pytz.timezone('Asia/Tokyo')).date() + + # parse config start/end times into datetime + reboot_start_time = datetime.strptime(self.core_cfg.title.reboot_start_time, "%H:%M") + reboot_end_time = datetime.strptime(self.core_cfg.title.reboot_end_time, "%H:%M") + + # offset datetimes with current date/time + reboot_start_time = reboot_start_time.replace(year=current_jst.year, month=current_jst.month, day=current_jst.day, tzinfo=pytz.timezone('Asia/Tokyo')) + reboot_end_time = reboot_end_time.replace(year=current_jst.year, month=current_jst.month, day=current_jst.day, tzinfo=pytz.timezone('Asia/Tokyo')) + + # create strings for use in gameSetting + reboot_start = reboot_start_time.strftime(self.date_time_format) + reboot_end = reboot_end_time.strftime(self.date_time_format) + return { "gameSetting": { "dataVersion": "1.00.00",