Merge pull request 'Chunithm Fixes and Maintenance for all!' (#46) from EmmyHeart/artemis:develop into develop

Reviewed-on: Hay1tsme/artemis#46
This commit is contained in:
Midorica 2023-10-17 17:00:08 +00:00
commit eaa2652647
11 changed files with 180 additions and 122 deletions

View File

@ -147,7 +147,7 @@ class AllnetServlet:
resp_dict = {k: v for k, v in vars(resp).items() if v is not None} resp_dict = {k: v for k, v in vars(resp).items() if v is not None}
return (urllib.parse.unquote(urllib.parse.urlencode(resp_dict)) + "\n").encode("utf-8") 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)." 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( self.data.base.log_event(
"allnet", "ALLNET_AUTH_NO_SHOP_IP", logging.ERROR, msg "allnet", "ALLNET_AUTH_NO_SHOP_IP", logging.ERROR, msg

View File

@ -85,6 +85,18 @@ class TitleConfig:
self.__config, "core", "title", "port", default=8080 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: class DatabaseConfig:
def __init__(self, parent_config: "CoreConfig") -> None: def __init__(self, parent_config: "CoreConfig") -> None:

View File

@ -6,11 +6,15 @@
- `name`: Name for the server, used by some games in their default MOTDs. Default `ARTEMiS` - `name`: Name for the server, used by some games in their default MOTDs. Default `ARTEMiS`
- `is_develop`: Flags that the server is a development instance without a proxy standing in front of it. Setting to `False` tells the server not to listen for SSL, because the proxy should be handling all SSL-related things, among other things. Default `True` - `is_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` - `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` - `log_dir`: Directory to store logs. Server MUST have read and write permissions to this directory or you will have issues. Default `logs`
## Title ## Title
- `loglevel`: Logging level for the title server. Default `info` - `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` - `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` - `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 ## Database
- `host`: Host of the database. Default `localhost` - `host`: Host of the database. Default `localhost`
- `username`: Username of the account the server should connect to the database with. Default `aime` - `username`: Username of the account the server should connect to the database with. Default `aime`

View File

@ -13,6 +13,9 @@ title:
loglevel: "info" loglevel: "info"
hostname: "localhost" hostname: "localhost"
port: 8080 port: 8080
reboot_start_time: "04:00"
reboot_end_time: "05:00"
database: database:
host: "localhost" host: "localhost"

View File

@ -200,12 +200,30 @@ class ChuniBase:
return {"type": data["type"], "length": 0, "gameSaleList": []} return {"type": data["type"], "length": 0, "gameSaleList": []}
def handle_get_game_setting_api_request(self, data: Dict) -> Dict: def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
reboot_start = datetime.strftime( # if reboot start/end time is not defined use the default behavior of being a few hours ago
datetime.now() - timedelta(hours=4), self.date_time_format if self.core_cfg.title.reboot_start_time == "" or self.core_cfg.title.reboot_end_time == "":
) reboot_start = datetime.strftime(
reboot_end = datetime.strftime( datetime.utcnow() + timedelta(hours=6), self.date_time_format
datetime.now() - timedelta(hours=3), 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 { return {
"gameSetting": { "gameSetting": {
"dataVersion": "1.00.00", "dataVersion": "1.00.00",
@ -385,26 +403,24 @@ class ChuniBase:
} }
def handle_get_user_rival_music_api_request(self, data: Dict) -> Dict: 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"]) rival_id = data["rivalId"]
if m is None: next_index = int(data["nextIndex"])
return {} max_count = int(data["maxCount"])
user_rival_music_list = [] user_rival_music_list = []
for music in m:
actual_music_id = self.data.static.get_song(music["musicId"]) # Fetch all the rival music entries for the user
if actual_music_id is None: all_entries = self.data.score.get_rival_music(rival_id)
music_id = music["musicId"]
else: # Process the entries based on max_count and nextIndex
music_id = actual_music_id["songId"] for music in all_entries[next_index:]:
music_id = music["musicId"]
level = music["level"] level = music["level"]
score = music["score"] score = music["score"]
rank = music["rank"] 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) music_entry = next((entry for entry in user_rival_music_list if entry["musicId"] == music_id), None)
if music_entry is None: if music_entry is None:
# If the entry doesn't exist, create a new entry
music_entry = { music_entry = {
"musicId": music_id, "musicId": music_id,
"length": 0, "length": 0,
@ -412,52 +428,32 @@ class ChuniBase:
} }
user_rival_music_list.append(music_entry) user_rival_music_list.append(music_entry)
# Check if the current score is higher than the previous highest score for the level # Create a level entry for the current level if it's unique or has a higher score
level_entry = next( level_entry = next((entry for entry in music_entry["userRivalMusicDetailList"] if entry["level"] == level), None)
( if level_entry is None:
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
level_entry = { level_entry = {
"level": level, "level": level,
"scoreMax": score, "scoreMax": score,
"scoreRank": rank "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"]: # Calculate the length for each "musicId" by counting the unique levels
music_entry["userRivalMusicDetailList"].append(level_entry) for music_entry in user_rival_music_list:
music_entry["length"] = len(music_entry["userRivalMusicDetailList"]) music_entry["length"] = len(music_entry["userRivalMusicDetailList"])
# Prepare the result dictionary with user rival music data
result = { result = {
"userId": data["userId"], "userId": data["userId"],
"rivalId": data["rivalId"], "rivalId": data["rivalId"],
"nextIndex": -1, "nextIndex": str(next_index + len(all_entries) if len(all_entries) <= len(user_rival_music_list) else -1),
"userRivalMusicList": user_rival_music_list "userRivalMusicList": user_rival_music_list[:max_count]
} }
return result 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: def handle_get_user_favorite_item_api_request(self, data: Dict) -> Dict:
user_fav_item_list = [] user_fav_item_list = []
@ -711,11 +707,7 @@ class ChuniBase:
if team: if team:
team_id = team["id"] team_id = team["id"]
team_name = team["teamName"] team_name = team["teamName"]
# Determine whether to use scaled ranks, or original system team_rank = self.data.profile.get_team_rank(team["id"])
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"])
# Don't return anything if no team name has been defined for defaults and there is no team set for the player # Don't return anything if no team name has been defined for defaults and there is no team set for the player
if not profile["teamId"] and team_name == "": if not profile["teamId"] and team_name == "":
@ -888,4 +880,4 @@ class ChuniBase:
return { return {
"userId": data["userId"], "userId": data["userId"],
"userNetBattleData": {"recentNBSelectMusicList": []}, "userNetBattleData": {"recentNBSelectMusicList": []},
} }

View File

@ -3,6 +3,7 @@ from datetime import datetime, timedelta
from random import randint from random import randint
from typing import Dict from typing import Dict
import pytz
from core.config import CoreConfig from core.config import CoreConfig
from titles.chuni.const import ChuniConstants from titles.chuni.const import ChuniConstants
from titles.chuni.database import ChuniData from titles.chuni.database import ChuniData
@ -31,12 +32,29 @@ class ChuniNew(ChuniBase):
match_end = datetime.strftime( match_end = datetime.strftime(
datetime.utcnow() + timedelta(hours=16), self.date_time_format datetime.utcnow() + timedelta(hours=16), self.date_time_format
) )
reboot_start = datetime.strftime( # if reboot start/end time is not defined use the default behavior of being a few hours ago
datetime.utcnow() + timedelta(hours=6), self.date_time_format if self.core_cfg.title.reboot_start_time == "" or self.core_cfg.title.reboot_end_time == "":
) reboot_start = datetime.strftime(
reboot_end = datetime.strftime( datetime.utcnow() + timedelta(hours=6), self.date_time_format
datetime.utcnow() + timedelta(hours=7), 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 { return {
"gameSetting": { "gameSetting": {
"isMaintenance": False, "isMaintenance": False,

View File

@ -646,7 +646,7 @@ class ChuniProfileData(BaseData):
return None return None
return result.fetchone() 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 # Normal ranking system, likely the one used in the real servers
# Query all teams sorted by 'teamPoint' # Query all teams sorted by 'teamPoint'
result = self.execute( result = self.execute(
@ -663,42 +663,8 @@ class ChuniProfileData(BaseData):
# Return the rank if found, or a default rank otherwise # Return the rank if found, or a default rank otherwise
return rank if rank is not None else 0 return rank if rank is not None else 0
def get_team_rank(self, team_id: int) -> int: # RIP scaled team ranking. Gone, but forgotten
# Scaled ranking system, designed for smaller instances. # def get_team_rank_scaled(self, team_id: int) -> int:
# 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
def update_team(self, team_id: int, team_data: Dict) -> bool: def update_team(self, team_id: int, team_data: Dict) -> bool:
team_data["id"] = team_id team_data["id"] = team_id
@ -736,4 +702,4 @@ class ChuniProfileData(BaseData):
total_play_count += row[0] total_play_count += row[0]
return { return {
"total_play_count": total_play_count "total_play_count": total_play_count
} }

View File

@ -201,9 +201,9 @@ class ChuniScoreData(BaseData):
return None return None
return result.lastrowid return result.lastrowid
def get_rival_music(self, rival_id: int, index: int, max_count: int) -> Optional[List[Dict]]: def get_rival_music(self, rival_id: int) -> Optional[List[Dict]]:
sql = select(playlog).where(playlog.c.user == rival_id).limit(max_count).offset(index) sql = select(playlog).where(playlog.c.user == rival_id)
result = self.execute(sql) result = self.execute(sql)
if result is None: if result is None:
return None return None
return result.fetchall() return result.fetchall()

View File

@ -4,6 +4,7 @@ import json
import logging import logging
from enum import Enum from enum import Enum
import pytz
from core.config import CoreConfig from core.config import CoreConfig
from core.data.cache import cached from core.data.cache import cached
from titles.cm.const import CardMakerConstants from titles.cm.const import CardMakerConstants
@ -61,12 +62,29 @@ class CardMakerBase:
} }
def handle_get_game_setting_api_request(self, data: Dict) -> Dict: def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
reboot_start = date.strftime( # if reboot start/end time is not defined use the default behavior of being a few hours ago
datetime.now() + timedelta(hours=3), self.date_time_format if self.core_cfg.title.reboot_start_time == "" or self.core_cfg.title.reboot_end_time == "":
) reboot_start = datetime.strftime(
reboot_end = date.strftime( datetime.utcnow() + timedelta(hours=6), self.date_time_format
datetime.now() + timedelta(hours=4), 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 # grab the dict with all games version numbers from user config
games_ver = self.game_cfg.version.version(self.version) games_ver = self.game_cfg.version.version(self.version)

View File

@ -5,6 +5,7 @@ from base64 import b64decode
from os import path, stat, remove from os import path, stat, remove
from PIL import ImageFile from PIL import ImageFile
import pytz
from core.config import CoreConfig from core.config import CoreConfig
from titles.mai2.const import Mai2Constants from titles.mai2.const import Mai2Constants
from titles.mai2.config import Mai2Config from titles.mai2.config import Mai2Config
@ -21,22 +22,47 @@ class Mai2Base:
self.can_deliver = False self.can_deliver = False
self.can_usbdl = False self.can_usbdl = False
self.old_server = "" self.old_server = ""
if self.core_config.server.is_develop and self.core_config.title.port > 0: 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/" self.old_server = f"http://{self.core_config.title.hostname}:{self.core_config.title.port}/SDEY/197/"
else: else:
self.old_server = f"http://{self.core_config.title.hostname}/SDEY/197/" self.old_server = f"http://{self.core_config.title.hostname}/SDEY/197/"
def handle_get_game_setting_api_request(self, data: Dict): 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, "isDevelop": False,
"isAouAccession": False, "isAouAccession": False,
"gameSetting": { "gameSetting": {
"isMaintenance": False, "isMaintenance": False,
"requestInterval": 1800, "requestInterval": 1800,
"rebootStartTime": "2020-01-01 07:00:00.0", "rebootStartTime": reboot_start,
"rebootEndTime": "2020-01-01 07:59:59.0", "rebootEndTime": reboot_end,
"movieUploadLimit": 100, "movieUploadLimit": 100,
"movieStatus": 1, "movieStatus": 1,
"movieServerUri": self.old_server + "api/movie" if self.game_config.uploads.movies else "movie", "movieServerUri": self.old_server + "api/movie" if self.game_config.uploads.movies else "movie",

View File

@ -4,6 +4,7 @@ import json
import logging import logging
from enum import Enum from enum import Enum
import pytz
from core.config import CoreConfig from core.config import CoreConfig
from core.data.cache import cached from core.data.cache import cached
from titles.ongeki.const import OngekiConstants from titles.ongeki.const import OngekiConstants
@ -103,12 +104,30 @@ class OngekiBase:
self.version = OngekiConstants.VER_ONGEKI self.version = OngekiConstants.VER_ONGEKI
def handle_get_game_setting_api_request(self, data: Dict) -> Dict: def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
reboot_start = date.strftime( # if reboot start/end time is not defined use the default behavior of being a few hours ago
datetime.now() + timedelta(hours=3), self.date_time_format if self.core_cfg.title.reboot_start_time == "" or self.core_cfg.title.reboot_end_time == "":
) reboot_start = datetime.strftime(
reboot_end = date.strftime( datetime.utcnow() + timedelta(hours=6), self.date_time_format
datetime.now() + timedelta(hours=4), 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 { return {
"gameSetting": { "gameSetting": {
"dataVersion": "1.00.00", "dataVersion": "1.00.00",