From de701b4d1b6fb6bd2c1da30557403583157629c8 Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Thu, 7 Dec 2023 18:00:23 +0800 Subject: [PATCH 01/32] [Bug fix] Course high score record - Fix a logical bug that the course's high score will not update if the user does not complete the whole course challenge. --- latest version/core/course.py | 56 +++++++++++++++++------------------ 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/latest version/core/course.py b/latest version/core/course.py index cf6c30c..d9b62e8 100644 --- a/latest version/core/course.py +++ b/latest version/core/course.py @@ -160,13 +160,17 @@ def __init__(self, c=None, user=None) -> None: super().__init__(c) self.user = user - self.is_completed: bool = False + # self.is_completed: bool = False self.high_score: int = None self.best_clear_type: int = None + @property + def is_completed(self) -> bool: + return self.best_clear_type != 0 + def to_dict(self) -> dict: r = super().to_dict() - if self.is_completed is None: + if self.best_clear_type is None: self.select_user_course() r.update({ 'is_completed': self.is_completed, @@ -182,22 +186,16 @@ def select_user_course(self, course_id: str = None) -> None: (self.user.user_id, self.course_id)) x = self.c.fetchone() if x is None: - self.is_completed = False self.high_score = 0 self.best_clear_type = 0 else: - self.is_completed = True self.high_score = x[2] self.best_clear_type = x[3] - def insert_user_course(self) -> None: - self.c.execute('''insert into user_course values (?,?,?,?)''', + def insert_or_update_user_course(self) -> None: + self.c.execute('''insert or replace into user_course values (?,?,?,?)''', (self.user.user_id, self.course_id, self.high_score, self.best_clear_type)) - def update_user_course(self) -> None: - self.c.execute('''update user_course set high_score = ?, best_clear_type = ? where user_id = ? and course_id = ?''', - (self.high_score, self.best_clear_type, self.user.user_id, self.course_id)) - class UserCourseList: ''' @@ -262,40 +260,42 @@ def to_dict(self) -> dict: def update(self) -> None: '''课题模式更新''' + self.select_user_course() + + self.score += self.user_play.score + + flag = False + if self.score > self.high_score: + self.high_score = self.score + flag = True + if self.user_play.health < 0: # 你挂了 self.user_play.course_play_state = 5 self.score = 0 self.clear_type = 0 self.user_play.update_play_state_for_course() + if flag: + self.insert_or_update_user_course() return None + self.user_play.course_play_state += 1 - self.score += self.user_play.score + from .score import Score if Score.get_song_state(self.clear_type) > Score.get_song_state(self.user_play.clear_type): self.clear_type = self.user_play.clear_type self.user_play.update_play_state_for_course() if self.user_play.course_play_state == 4: - self.user.select_user_about_stamina() - self.select_course_item() - for i in self.items: - i.user_claim_item(self.user) - - self.select_user_course() if not self.is_completed: - self.high_score = self.score - self.best_clear_type = self.clear_type - self.is_completed = True - self.insert_user_course() - return None + self.user.select_user_about_stamina() + self.select_course_item() + for i in self.items: + i.user_claim_item(self.user) - flag = False - if self.score > self.high_score: - self.high_score = self.score - flag = True if Score.get_song_state(self.clear_type) > Score.get_song_state(self.best_clear_type): self.best_clear_type = self.clear_type flag = True - if flag: - self.update_user_course() + + if flag: + self.insert_or_update_user_course() From 746712718bdf023607082ec13e851ae1220afae2 Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Sat, 13 Jan 2024 15:14:54 +0800 Subject: [PATCH 02/32] [Refactor][Bug fix] World mode & skills - Code refactor for world mode - Try to fix some skills to keep it same with the official server (especially `ayu_uncap`), and adjust the order of World Mode progress calculation (maybe still not right). - Uncap Kanae Note: not ready for Kanae's uncapping skill (I do not know its actual effect.) --- latest version/core/config_manager.py | 2 +- latest version/core/constant.py | 2 +- latest version/core/score.py | 5 +- latest version/core/world.py | 535 ++++++++++++---------- latest version/database/init/arc_data.py | 15 +- latest version/database/init/singles.json | 36 ++ latest version/server/others.py | 14 +- latest version/web/index.py | 2 +- 8 files changed, 363 insertions(+), 248 deletions(-) diff --git a/latest version/core/config_manager.py b/latest version/core/config_manager.py index 1e1dbb4..fa5cb2a 100644 --- a/latest version/core/config_manager.py +++ b/latest version/core/config_manager.py @@ -12,7 +12,7 @@ class Config: SONG_FILE_HASH_PRE_CALCULATE = True - GAME_API_PREFIX = '/evolution/23' + GAME_API_PREFIX = '/akeome/24' ALLOW_APPVERSION = [] # list[str] diff --git a/latest version/core/constant.py b/latest version/core/constant.py index 7726cc2..2756606 100644 --- a/latest version/core/constant.py +++ b/latest version/core/constant.py @@ -1,6 +1,6 @@ from .config_manager import Config -ARCAEA_SERVER_VERSION = 'v2.11.2.7' +ARCAEA_SERVER_VERSION = 'v2.11.3.2.test' ARCAEA_LOG_DATBASE_VERSION = 'v1.1' diff --git a/latest version/core/score.py b/latest version/core/score.py index 5174b62..fb5fc86 100644 --- a/latest version/core/score.py +++ b/latest version/core/score.py @@ -11,7 +11,7 @@ from .song import Chart from .sql import Connect, Query, Sql from .util import get_today_timestamp, md5 -from .world import WorldPlay +from .world import WorldPlay, BeyondWorldPlay class Score: @@ -500,7 +500,8 @@ def upload_score(self) -> None: # 世界模式判断 if self.is_world_mode: - self.world_play = WorldPlay(self.c, self.user, self) + self.world_play = WorldPlay( + self.c, self.user, self) if self.beyond_gauge == 0 else BeyondWorldPlay(self.c, self.user, self) self.world_play.update() # 课题模式判断 diff --git a/latest version/core/world.py b/latest version/core/world.py index 4110116..d2ec611 100644 --- a/latest version/core/world.py +++ b/latest version/core/world.py @@ -1,7 +1,7 @@ import os from functools import lru_cache from json import load -from random import random +from random import randint from time import time from .character import Character @@ -438,7 +438,170 @@ def update(self): 'a': self.stamina, 'b': self.max_stamina_ts, 'c': self.user.user_id}) -class WorldPlay: +class WorldSkillMixin: + def before_calculate(self) -> None: + factory_dict = {'skill_vita': self._skill_vita, + 'skill_mika': self._skill_mika, + 'skill_ilith_ivy': self._skill_ilith_ivy, + 'ilith_awakened_skill': self._ilith_awakened_skill, + 'skill_hikari_vanessa': self._skill_hikari_vanessa, + 'skill_mithra': self._skill_mithra + } + if self.user_play.beyond_gauge == 0 and self.character_used.character_id == 35 and self.character_used.skill_id_displayed: + self._special_tempest() + + if self.character_used.skill_id_displayed in factory_dict: + factory_dict[self.character_used.skill_id_displayed]() + + def after_climb(self) -> None: + factory_dict = {'eto_uncap': self._eto_uncap, + 'ayu_uncap': self._ayu_uncap, + 'skill_fatalis': self._skill_fatalis, + 'skill_amane': self._skill_amane, + 'skill_maya': self._skill_maya, + 'luna_uncap': self._luna_uncap, + 'skill_kanae_uncap': self._skill_kanae_uncap + } + if self.character_used.skill_id_displayed in factory_dict: + factory_dict[self.character_used.skill_id_displayed]() + + def _special_tempest(self) -> None: + '''风暴对立技能,prog随全角色等级提升''' + if self.character_used.database_table_name == 'user_char_full': + self.prog_tempest = 60 + else: + self.c.execute( + '''select sum(level) from user_char where user_id=?''', (self.user.user_id,)) + x = self.c.fetchone() + self.prog_tempest = int(x[0]) / 10 if x else 0 + if self.prog_tempest > 60: + self.prog_tempest = 60 + elif self.prog_tempest < 0: + self.prog_tempest = 0 + + def _skill_vita(self) -> None: + ''' + vita技能,overdrive随回忆率提升,提升量最多为10 + 此处采用线性函数 + ''' + self.over_skill_increase = 0 + if 0 < self.user_play.health <= 100: + self.over_skill_increase = self.user_play.health / 10 + + def _eto_uncap(self) -> None: + '''eto觉醒技能,获得残片奖励时世界模式进度加7''' + fragment_flag = False + + for i in self.user.current_map.rewards_for_climbing: + for j in i['items']: + if j.item_type == 'fragment': + fragment_flag = True + break + if fragment_flag: + break + + if fragment_flag: + self.character_bonus_progress_normalized = Constant.ETO_UNCAP_BONUS_PROGRESS + + self.user.current_map.reclimb(self.final_progress) + + def _luna_uncap(self) -> None: + '''luna觉醒技能,限制格开始时世界模式进度加 7,偷懒重爬(因为 map 信息还未获取)''' + x: 'Step' = self.user.current_map.steps_for_climbing[0] + if x.restrict_id and x.restrict_type: + self.self.character_bonus_progress_normalized = Constant.LUNA_UNCAP_BONUS_PROGRESS + self.user.current_map.reclimb(self.final_progress) + + def _ayu_uncap(self) -> None: + '''ayu 觉醒技能,世界模式进度随机变动 [-5, -5],但不会小于 0''' + + self.character_bonus_progress_normalized = randint( + -Constant.AYU_UNCAP_BONUS_PROGRESS, Constant.AYU_UNCAP_BONUS_PROGRESS) + + if self.progress_normalized + self.character_bonus_progress_normalized < 0: + self.character_bonus_progress_normalized = -self.progress_normalized + + self.user.current_map.reclimb(self.final_progress) + + def _skill_fatalis(self) -> None: + '''hikari fatalis技能,世界模式超载,打完休息60分钟''' + + self.user.world_mode_locked_end_ts = int( + time()*1000) + Constant.SKILL_FATALIS_WORLD_LOCKED_TIME + self.user.update_user_one_column('world_mode_locked_end_ts') + + def _skill_amane(self) -> None: + ''' + amane技能,起始格为限速或随机,成绩小于EX时,世界模式进度减半 + ''' + x: 'Step' = self.user.current_map.steps_for_climbing[0] + if ('randomsong' in x.step_type or 'speedlimit' in x.step_type) and self.user_play.song_grade < 5: + self.character_bonus_progress_normalized = -self.progress_normalized / 2 + self.user.current_map.reclimb(self.final_progress) + + def _ilith_awakened_skill(self) -> None: + ''' + ilith 觉醒技能,曲目通关时步数+6,wiki 说是 prog 值+6 + ''' + if self.user_play.health > 0: + self.prog_skill_increase = 6 + + def _skill_mika(self) -> None: + ''' + mika 技能,通关特定曲目能力值翻倍 + ''' + if self.user_play.song.song_id in Constant.SKILL_MIKA_SONGS and self.user_play.clear_type != 0: + self.over_skill_increase = self.character_used.overdrive.get_value( + self.character_used.level) + self.prog_skill_increase = self.character_used.prog.get_value( + self.character_used.level) + + def _skill_mithra(self) -> None: + ''' + mithra 技能,每 150 combo 增加世界模式进度+1 + ''' + if self.user_play.combo_interval_bonus: + self.character_bonus_progress_normalized = self.user_play.combo_interval_bonus + + def _skill_ilith_ivy(self) -> None: + ''' + ilith & ivy 技能,根据 skill_cytusii_flag 来增加三个数值,最高生命每过 20 就对应数值 +10 + ''' + if not self.user_play.skill_cytusii_flag: + return + x = self.user_play.skill_cytusii_flag[: + self.user_play.highest_health // 20] + self.over_skill_increase = x.count('2') * 10 + self.prog_skill_increase = x.count('1') * 10 + + def _skill_hikari_vanessa(self) -> None: + ''' + hikari & vanessa 技能,根据 skill_cytusii_flag 来减少三个数值,最高生命每过 20 就对应数值 -10 + ''' + if not self.user_play.skill_cytusii_flag: + return + x = self.user_play.skill_cytusii_flag[:5 - + self.user_play.lowest_health // 20] + self.over_skill_increase = -x.count('2') * 10 + self.prog_skill_increase = -x.count('1') * 10 + + def _skill_maya(self) -> None: + ''' + maya 技能,skill_flag 为 1 时,世界模式进度翻倍 + ''' + if self.character_used.skill_flag: + self.character_bonus_progress_normalized = self.progress_normalized + self.user.current_map.reclimb(self.final_progress) + self.character_used.change_skill_state() + + def _skill_kanae_uncap(self) -> None: + ''' + kanae 觉醒技能,保存世界模式 progress 并在下次结算 + ''' + pass + + +class BaseWorldPlay(WorldSkillMixin): ''' 世界模式打歌类,处理特殊角色技能,联动UserMap和UserPlay @@ -452,13 +615,13 @@ def __init__(self, c=None, user=None, user_play=None) -> None: self.user_play = user_play self.character_used = None - self.base_step_value: float = None - self.step_value: float = None + self.progress_normalized: float = None + self.character_bonus_progress_normalized: float = None - self.prog_tempest: float = None - self.character_bonus_progress: float = None - self.prog_skill_increase: float = None - self.over_skill_increase: float = None + # kanae_stored_progress: float + # kanae_added_progress: float + # partner_multiply: float + # wpaid: str def to_dict(self) -> dict: arcmap: 'UserMap' = self.user.current_map @@ -466,8 +629,8 @@ def to_dict(self) -> dict: "rewards": arcmap.rewards_for_climbing_to_dict(), "exp": self.character_used.level.exp, "level": self.character_used.level.level, - "base_progress": self.base_step_value, - "progress": self.step_value, + "base_progress": self.base_progress, + "progress": self.final_progress, "user_map": { "user_id": self.user.user_id, "curr_position": arcmap.curr_position, @@ -487,40 +650,22 @@ def to_dict(self) -> dict: "current_stamina": self.user.stamina.stamina, "max_stamina_ts": self.user.stamina.max_stamina_ts, 'world_mode_locked_end_ts': self.user.world_mode_locked_end_ts, - 'beyond_boost_gauge': self.user.beyond_boost_gauge + 'beyond_boost_gauge': self.user.beyond_boost_gauge, + # 'kanae_stored_progress': 7114000, # 往群愿里塞 + # 'kanae_added_progress': 514000, # 群愿往外拿 + # 'wpaid': 'helloworld', # world play id ??? + # 'partner_multiply': 456, # ? } - if self.over_skill_increase is not None: - r['char_stats']['over_skill_increase'] = self.over_skill_increase - if self.prog_skill_increase is not None: - r['char_stats']['prog_skill_increase'] = self.prog_skill_increase - - if self.prog_tempest is not None: - r['char_stats']['prog'] += self.prog_tempest # 没试过要不要这样 - r['char_stats']['prog_tempest'] = self.prog_tempest - - if self.character_bonus_progress is not None: - # 猜的,为了让客户端正确显示 - r['progress'] -= self.character_bonus_progress - r['character_bonus_progress'] = self.character_bonus_progress - if self.character_used.skill_id_displayed == 'skill_maya': r['char_stats']['skill_state'] = self.character_used.skill_state - if self.user_play.beyond_gauge == 0: - r["user_map"]["steps"] = [ - x.to_dict() for x in arcmap.steps_for_climbing] - else: - r["user_map"]["steps"] = len(arcmap.steps_for_climbing) - if self.user_play.stamina_multiply != 1: r['stamina_multiply'] = self.user_play.stamina_multiply if self.user_play.fragment_multiply != 100: r['fragment_multiply'] = self.user_play.fragment_multiply - if self.user_play.prog_boost_multiply != 0: + if self.user_play.prog_boost_multiply != 0: # 源韵强化 r['prog_boost_multiply'] = self.user_play.prog_boost_multiply - if self.user_play.beyond_boost_gauge_usage != 0: - r['beyond_boost_gauge_usage'] = self.user_play.beyond_boost_gauge_usage return r @@ -531,66 +676,27 @@ def beyond_boost_gauge_addition(self) -> float: @property def step_times(self) -> float: - prog_boost_multiply = self.user_play.prog_boost_multiply + 100 - beyond_boost_times = 1 - - if self.user_play.beyond_gauge == 1: - if prog_boost_multiply > 100: - prog_boost_multiply -= 100 - if self.user_play.beyond_boost_gauge_usage == 100: - beyond_boost_times = 2 - elif self.user_play.beyond_boost_gauge_usage == 200: - beyond_boost_times = 3 - - return self.user_play.stamina_multiply * self.user_play.fragment_multiply / 100 * prog_boost_multiply / 100 * beyond_boost_times + raise NotImplementedError @property def exp_times(self) -> float: - prog_boost_multiply = self.user_play.prog_boost_multiply + 100 - beyond_boost_times = 1 - - if self.user_play.beyond_gauge == 1: - if prog_boost_multiply > 100: - prog_boost_multiply -= 100 - if self.user_play.beyond_boost_gauge_usage == 100: - beyond_boost_times = 2 - elif self.user_play.beyond_boost_gauge_usage == 200: - beyond_boost_times = 3 - - return self.user_play.stamina_multiply * prog_boost_multiply / 100 * beyond_boost_times + return self.user_play.stamina_multiply * (self.user_play.prog_boost_multiply / 100 + 1) - def get_step(self) -> None: - if self.user_play.beyond_gauge == 0: - self.base_step_value = 2.5 + 2.45 * self.user_play.rating**0.5 - prog = self.character_used.prog.get_value( - self.character_used.level) - if self.prog_tempest: - prog += self.prog_tempest - if self.prog_skill_increase: - prog += self.prog_skill_increase - - self.step_value = self.base_step_value * prog / 50 * self.step_times - else: - if self.user_play.clear_type == 0: - self.base_step_value = 25/28 + \ - (self.user_play.rating)**0.5 * 0.43 - else: - self.base_step_value = 75/28 + \ - (self.user_play.rating)**0.5 * 0.43 + @property + def character_bonus_progress(self) -> float: + return self.character_bonus_progress_normalized * self.step_times - if self.character_used.character_id in self.user.current_map.character_affinity: - affinity_multiplier = self.user.current_map.affinity_multiplier[self.user.current_map.character_affinity.index( - self.character_used.character_id)] - else: - affinity_multiplier = 1 + @property + def base_progress(self) -> float: + raise NotImplementedError - overdrive = self.character_used.overdrive.get_value( - self.character_used.level) - if self.over_skill_increase: - overdrive += self.over_skill_increase + @property + def final_progress(self) -> float: + raise NotImplementedError - self.step_value = self.base_step_value * overdrive / \ - 50 * self.step_times * affinity_multiplier + def get_step(self) -> None: + # to get self.progress_normalized + raise NotImplementedError def update(self) -> None: '''世界模式更新''' @@ -600,20 +706,6 @@ def update(self) -> None: self.user_play.clear_play_state() self.user.select_user_about_world_play() - if self.user_play.beyond_gauge == 0: - # 更新byd大招蓄力条 - self.user.beyond_boost_gauge += self.beyond_boost_gauge_addition - self.user.beyond_boost_gauge = min( - self.user.beyond_boost_gauge, 200) - self.user.update_user_one_column( - 'beyond_boost_gauge', self.user.beyond_boost_gauge) - elif self.user_play.beyond_boost_gauge_usage != 0 and self.user_play.beyond_boost_gauge_usage <= self.user.beyond_boost_gauge: - self.user.beyond_boost_gauge -= self.user_play.beyond_boost_gauge_usage - if abs(self.user.beyond_boost_gauge) <= 1e-5: - self.user.beyond_boost_gauge = 0 - self.user.update_user_one_column( - 'beyond_boost_gauge', self.user.beyond_boost_gauge) - self.character_used = Character() self.user.character.select_character_info() @@ -630,7 +722,7 @@ def update(self) -> None: self.user.current_map.select_map_info() self.before_calculate() self.get_step() - self.user.current_map.climb(self.step_value) + self.user.current_map.climb(self.final_progress) self.after_climb() for i in self.user.current_map.rewards_for_climbing: # 物品分发 @@ -655,156 +747,135 @@ def update(self) -> None: self.user.current_map.update() - def before_calculate(self) -> None: - factory_dict = {'skill_vita': self._skill_vita, 'skill_mika': self._skill_mika, 'skill_ilith_ivy': self._skill_ilith_ivy, - 'ilith_awakened_skill': self._ilith_awakened_skill, 'skill_hikari_vanessa': self._skill_hikari_vanessa} - if self.user_play.beyond_gauge == 0 and self.character_used.character_id == 35 and self.character_used.skill_id_displayed: - self._special_tempest() - if self.character_used.skill_id_displayed in factory_dict: - factory_dict[self.character_used.skill_id_displayed]() +class WorldPlay(BaseWorldPlay): + def __init__(self, c=None, user=None, user_play=None) -> None: + super().__init__(c, user, user_play) - def after_climb(self) -> None: - factory_dict = {'eto_uncap': self._eto_uncap, 'ayu_uncap': self._ayu_uncap, - 'luna_uncap': self._luna_uncap, 'skill_fatalis': self._skill_fatalis, 'skill_amane': self._skill_amane, 'skill_maya': self._skill_maya, 'skill_mithra': self._skill_mithra} - if self.character_used.skill_id_displayed in factory_dict: - factory_dict[self.character_used.skill_id_displayed]() + self.prog_tempest: float = None + self.prog_skill_increase: float = None - def _special_tempest(self) -> None: - '''风暴对立技能,prog随全角色等级提升''' - if self.character_used.database_table_name == 'user_char_full': - self.prog_tempest = 60 - else: - self.c.execute( - '''select sum(level) from user_char where user_id=?''', (self.user.user_id,)) - x = self.c.fetchone() - self.prog_tempest = int(x[0]) / 10 if x else 0 - if self.prog_tempest > 60: - self.prog_tempest = 60 - elif self.prog_tempest < 0: - self.prog_tempest = 0 + def to_dict(self) -> dict: + r = super().to_dict() - def _skill_vita(self) -> None: - ''' - vita技能,overdrive随回忆率提升,提升量最多为10 - 此处采用线性函数 - ''' - self.over_skill_increase = 0 - if 0 < self.user_play.health <= 100: - self.over_skill_increase = self.user_play.health / 10 + # 基础进度加上搭档倍数 不带 character_bonus_progress 但是带 kanae 技能 + r['progress_partial_after_stat'] = self.progress_normalized - def _eto_uncap(self) -> None: - '''eto觉醒技能,获得残片奖励时世界模式进度加7''' - fragment_flag = False + if self.character_bonus_progress_normalized is not None: + r['character_bonus_progress'] = self.character_bonus_progress_normalized + # 不懂为什么两个玩意一样 + r['character_bonus_progress_normalized'] = self.character_bonus_progress_normalized - for i in self.user.current_map.rewards_for_climbing: - for j in i['items']: - if j.item_type == 'fragment': - fragment_flag = True - break - if fragment_flag: - break + if self.prog_skill_increase is not None: + r['char_stats']['prog_skill_increase'] = self.prog_skill_increase - if fragment_flag: - self.character_bonus_progress = Constant.ETO_UNCAP_BONUS_PROGRESS - self.step_value += self.character_bonus_progress + if self.prog_tempest is not None: + r['char_stats']['prog'] += self.prog_tempest # 没试过要不要这样 + r['char_stats']['prog_tempest'] = self.prog_tempest - self.user.current_map.reclimb(self.step_value) + r['partner_adjusted_prog'] = self.partner_adjusted_prog - def _luna_uncap(self) -> None: - '''luna觉醒技能,限制格开始时世界模式进度加7''' - x: 'Step' = self.user.current_map.steps_for_climbing[0] - if x.restrict_id and x.restrict_type: - self.character_bonus_progress = Constant.LUNA_UNCAP_BONUS_PROGRESS - self.step_value += self.character_bonus_progress + r["user_map"]["steps"] = [x.to_dict() + for x in self.user.current_map.steps_for_climbing] + return r - self.user.current_map.reclimb(self.step_value) + @property + def step_times(self) -> float: + return self.user_play.stamina_multiply * self.user_play.fragment_multiply / 100 * (self.user_play.prog_boost_multiply / 100 + 1) - def _ayu_uncap(self) -> None: - '''ayu觉醒技能,世界模式进度+5或-5,但不会小于0''' + @property + def character_bonus_progress(self) -> float: + return self.character_bonus_progress_normalized * self.step_times - self.character_bonus_progress = Constant.AYU_UNCAP_BONUS_PROGRESS if random( - ) >= 0.5 else -Constant.AYU_UNCAP_BONUS_PROGRESS + @property + def base_progress(self) -> float: + return 2.5 + 2.45 * self.user_play.rating**0.5 - self.step_value += self.character_bonus_progress - if self.step_value < 0: - self.character_bonus_progress += self.step_value - self.step_value = 0 + @property + def final_progress(self) -> float: + return (self.progress_normalized + (self.character_bonus_progress_normalized or 0)) * self.step_times - self.user.current_map.reclimb(self.step_value) + @property + def partner_adjusted_prog(self) -> float: + prog = self.character_used.prog.get_value( + self.character_used.level) + if self.prog_tempest: + prog += self.prog_tempest + if self.prog_skill_increase: + prog += self.prog_skill_increase + return prog - def _skill_fatalis(self) -> None: - '''hikari fatalis技能,世界模式超载,打完休息60分钟''' + def get_step(self) -> None: + self.progress_normalized = self.base_progress * \ + (self.partner_adjusted_prog / 50) - self.user.world_mode_locked_end_ts = int( - time()*1000) + Constant.SKILL_FATALIS_WORLD_LOCKED_TIME - self.user.update_user_one_column('world_mode_locked_end_ts') + def update(self) -> None: + '''世界模式更新''' + super().update() - def _skill_amane(self) -> None: - ''' - amane技能,起始格为限速或随机,成绩小于EX时,世界模式进度减半 - 偷懒在after_climb里面,需要重爬一次 - ''' - x: 'Step' = self.user.current_map.steps_for_climbing[0] - if ('randomsong' in x.step_type or 'speedlimit' in x.step_type) and self.user_play.song_grade < 5: - self.character_bonus_progress = -self.step_value / 2 - self.step_value = self.step_value / 2 - self.user.current_map.reclimb(self.step_value) + # 更新byd大招蓄力条 + self.user.beyond_boost_gauge += self.beyond_boost_gauge_addition + self.user.beyond_boost_gauge = min(self.user.beyond_boost_gauge, 200) + self.user.update_user_one_column( + 'beyond_boost_gauge', self.user.beyond_boost_gauge) - def _ilith_awakened_skill(self) -> None: - ''' - ilith 觉醒技能,曲目通关时步数+6,wiki 说是 prog 值+6 - ''' - if self.user_play.health > 0: - self.prog_skill_increase = 6 - def _skill_mika(self) -> None: - ''' - mika 技能,通关特定曲目能力值翻倍 - ''' - if self.user_play.song.song_id in Constant.SKILL_MIKA_SONGS and self.user_play.clear_type != 0: - self.over_skill_increase = self.character_used.overdrive.get_value( - self.character_used.level) - self.prog_skill_increase = self.character_used.prog.get_value( - self.character_used.level) +class BeyondWorldPlay(BaseWorldPlay): - def _skill_mithra(self) -> None: - ''' - mithra 技能,每 150 combo 增加世界模式进度+1 - ''' - if self.user_play.combo_interval_bonus: - self.character_bonus_progress = self.user_play.combo_interval_bonus - self.step_value += self.character_bonus_progress - self.user.current_map.reclimb(self.step_value) + def __init__(self, c=None, user=None, user_play=None) -> None: + super().__init__(c, user, user_play) + self.over_skill_increase: float = None - def _skill_ilith_ivy(self) -> None: - ''' - ilith & ivy 技能,根据 skill_cytusii_flag 来增加三个数值,最高生命每过 20 就对应数值 +10 - ''' - if not self.user_play.skill_cytusii_flag: - return - x = self.user_play.skill_cytusii_flag[: - self.user_play.highest_health // 20] - self.over_skill_increase = x.count('2') * 10 - self.prog_skill_increase = x.count('1') * 10 + @property + def step_times(self) -> float: + return self.user_play.stamina_multiply * self.user_play.fragment_multiply / 100 * (1 + self.user_play.prog_boost_multiply / 100 + self.user_play.beyond_boost_gauge_usage / 100) - def _skill_hikari_vanessa(self) -> None: - ''' - hikari & vanessa 技能,根据 skill_cytusii_flag 来减少三个数值,最高生命每过 20 就对应数值 -10 - ''' - if not self.user_play.skill_cytusii_flag: - return - x = self.user_play.skill_cytusii_flag[:5 - - self.user_play.lowest_health // 20] - self.over_skill_increase = -x.count('2') * 10 - self.prog_skill_increase = -x.count('1') * 10 + @property + def affinity_multiplier(self) -> float: + if self.user.current_map.character_affinity is not None and self.character_used.character_id is not None and self.character_used.character_id in self.user.current_map.character_affinity: + return self.user.current_map.affinity_multiplier[self.user.current_map.character_affinity.index(self.character_used.character_id)] + return 1 - def _skill_maya(self) -> None: - ''' - maya 技能,skill_flag 为 1 时,世界模式进度翻倍 - ''' - if self.character_used.skill_flag: - self.character_bonus_progress = self.step_value - self.step_value += self.character_bonus_progress - self.user.current_map.reclimb(self.step_value) - self.character_used.change_skill_state() + @property + def base_progress(self) -> float: + return self.user_play.rating**0.5 * 0.43 + (25/28 if self.user_play.clear_type == 0 else 75/28) + + @property + def final_progress(self) -> float: + return self.progress_normalized * self.step_times + + def to_dict(self) -> dict: + r = super().to_dict() + + # byd 进度 没有加上源韵强化 和 boost 的数值 + r['pre_boost_progress'] = self.progress_normalized * \ + self.user_play.fragment_multiply / 100 + + if self.over_skill_increase is not None: + r['char_stats']['over_skill_increase'] = self.over_skill_increase + + r["user_map"]["steps"] = len(self.user.current_map.steps_for_climbing) + r['affinity_multiply'] = self.affinity_multiplier + if self.user_play.beyond_boost_gauge_usage != 0: + r['beyond_boost_gauge_usage'] = self.user_play.beyond_boost_gauge_usage + + return r + + def update(self) -> None: + super().update() + + if self.user_play.beyond_boost_gauge_usage != 0 and self.user_play.beyond_boost_gauge_usage <= self.user.beyond_boost_gauge: + self.user.beyond_boost_gauge -= self.user_play.beyond_boost_gauge_usage + if abs(self.user.beyond_boost_gauge) <= 1e-5: + self.user.beyond_boost_gauge = 0 + self.user.update_user_one_column( + 'beyond_boost_gauge', self.user.beyond_boost_gauge) + + def get_step(self) -> None: + overdrive = self.character_used.overdrive.get_value( + self.character_used.level) + if self.over_skill_increase: + overdrive += self.over_skill_increase + + self.progress_normalized = self.base_progress * \ + (overdrive / 50) * self.affinity_multiplier diff --git a/latest version/database/init/arc_data.py b/latest version/database/init/arc_data.py index d2300ac..3a3f66d 100644 --- a/latest version/database/init/arc_data.py +++ b/latest version/database/init/arc_data.py @@ -6,7 +6,7 @@ class InitData: 'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', '', '', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', '', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', '', '', 'visual_ghost_skynotes', 'skill_vita', 'skill_fatalis', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', '', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'skill_mithra', 'skill_toa', 'skill_nami_twilight', 'skill_ilith_ivy', 'skill_hikari_vanessa', 'skill_maya', 'skill_intruder', 'skill_luin'] skill_id_uncap = ['', '', 'frags_kou', '', 'visual_ink', '', '', '', '', '', 'ilith_awakened_skill', 'eto_uncap', 'luna_uncap', 'shirabe_entry_fee', - '', '', '', '', '', 'ayu_uncap', '', 'frags_yume', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'skill_luin_uncap'] + '', '', '', '', '', 'ayu_uncap', '', 'frags_yume', '', '', '', '', '', '', '', '', 'skill_kanae_uncap', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'skill_luin_uncap'] skill_unlock_level = [0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 14, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] @@ -30,13 +30,13 @@ class InitData: 46, 73, 95, 67, 84, 70, 78, 69, 70, 50, 80, 80, 63, 25, 50, 72, 55, 50, 95, 55, 70, 90, 70, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 54, 65.5, 59.5, 58, 96, 47, 50, 54] frag30 = [88, 90, 100, 75, 80, 89, 70, 79, 65, 40, 50, 90, 100, 92, 0, 61, 67, 92, 85, 50, 86, 62, - 65, 85, 67, 88, 74, 0.5, 105, 80, 95, 50, 80, 87, 71, 50, 95, 0, 80, 75, 50, 70, 80, 100, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 90, 80, 50, 51, 64, 100, 50, 58, 51, 40, 50, 80] + 65, 85, 67, 88, 74, 0.5, 105, 80, 105, 50, 80, 87, 71, 50, 95, 0, 80, 75, 50, 70, 80, 100, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 90, 80, 50, 51, 64, 100, 50, 58, 51, 40, 50, 80] prog30 = [71, 90, 80, 75, 100, 80, 90, 102, 84, 78, 110, 77, 73, 78, 0, 99, 80, 66, 46, 93, 40, 83, - 80, 90, 93, 50, 96, 88, 99, 108, 75, 80, 50, 64, 55, 100, 100, 110, 80, 50, 74, 90, 80, 80, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51, 60, 53, 85, 58, 96, 47, 50, 90] + 80, 90, 93, 50, 96, 88, 99, 108, 85, 80, 50, 64, 55, 100, 100, 110, 80, 50, 74, 90, 80, 80, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51, 60, 53, 85, 58, 96, 47, 50, 90] overdrive30 = [71, 90, 57, 75, 80, 80, 95, 79, 65, 31, 50, 69, 100, 68, 0, 78, 50, 70, 62, 59, 64, - 56, 73, 95, 67, 84, 80, 88, 79, 80, 50, 80, 80, 63, 25, 50, 82, 55, 50, 95, 55, 70, 100, 80, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 64, 65.5, 59.5, 58, 96, 47, 50, 64] + 56, 73, 95, 67, 84, 80, 88, 79, 80, 60, 80, 80, 63, 25, 50, 82, 55, 50, 95, 55, 70, 100, 80, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 64, 65.5, 59.5, 58, 96, 47, 50, 64] char_type = [1, 0, 0, 0, 0, 0, 0, 2, 0, 1, 2, 0, 0, 0, 2, 3, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 3, 0, 2, 2, 0, 0, 2, 0, 0, 2, 0, 2, 2, 1, 0, 2, 0, 0, 2] @@ -61,14 +61,15 @@ class InitData: 10: [{'core_id': 'core_umbral', 'amount': 30}], 66: [{'core_id': 'core_chunithm', 'amount': 15}], 5: [{'core_id': 'core_hollow', 'amount': 0}], - 73: [{'core_id': 'core_wacca', 'amount': 15}] + 73: [{'core_id': 'core_wacca', 'amount': 15}], + 30: [{'core_id': 'core_hollow', 'amount': 5}, {'core_id': 'core_sunset', 'amount': 25}] } cores = ['core_hollow', 'core_desolate', 'core_chunithm', 'core_crimson', - 'core_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful', 'core_course_skip_purchase', 'core_umbral', 'core_wacca'] + 'core_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful', 'core_course_skip_purchase', 'core_umbral', 'core_wacca', 'core_sunset'] world_songs = ["babaroque", "shadesoflight", "kanagawa", "lucifer", "anokumene", "ignotus", "rabbitintheblackroom", "qualia", "redandblue", "bookmaker", "darakunosono", "espebranch", "blacklotus", "givemeanightmare", "vividtheory", "onefr", "gekka", "vexaria3", "infinityheaven3", "fairytale3", "goodtek3", "suomi", "rugie", "faintlight", "harutopia", "goodtek", "dreaminattraction", "syro", "diode", "freefall", "grimheart", "blaster", - "cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris', 'ultimatetaste', 'rgb', 'matenrou', 'dynitikos', 'amekagura', 'fantasy', 'aloneandlorn'] + "cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris', 'ultimatetaste', 'rgb', 'matenrou', 'dynitikos', 'amekagura', 'fantasy', 'aloneandlorn', 'felys', 'onandon', 'hotarubinoyuki'] world_unlocks = ["scenery_chap1", "scenery_chap2", "scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7"] diff --git a/latest version/database/init/singles.json b/latest version/database/init/singles.json index 0f9a18b..68d39f8 100644 --- a/latest version/database/init/singles.json +++ b/latest version/database/init/singles.json @@ -1558,5 +1558,41 @@ ], "orig_price": 100, "price": 100 + }, + { + "name": "overdrive", + "items": [ + { + "type": "single", + "id": "overdrive", + "is_available": true + }, + { + "type": "core", + "amount": 1, + "id": "core_generic", + "is_available": true + } + ], + "orig_price": 100, + "price": 100 + }, + { + "name": "lightmyway", + "items": [ + { + "type": "single", + "id": "lightmyway", + "is_available": true + }, + { + "type": "core", + "amount": 1, + "id": "core_generic", + "is_available": true + } + ], + "orig_price": 100, + "price": 100 } ] \ No newline at end of file diff --git a/latest version/server/others.py b/latest version/server/others.py index 9063770..b8e7c1b 100644 --- a/latest version/server/others.py +++ b/latest version/server/others.py @@ -60,6 +60,12 @@ def finale_end(): return success_return({}) +@bp.route('/applog/me', methods=['POST']) +def applog_me(): + # 异常日志,不处理 + return success_return({}) + + map_dict = {'/user/me': user_me, '/purchase/bundle/pack': bundle_pack, '/serve/download/me/song': download_song, @@ -74,9 +80,9 @@ def finale_end(): @bp.route('/compose/aggregate', methods=['GET']) # 集成式请求 def aggregate(): try: - #global request + # global request finally_response = {'success': True, 'value': []} - #request_ = request + # request_ = request get_list = json.loads(request.args.get('calls')) if len(get_list) > 10: # 请求太多驳回 @@ -101,13 +107,13 @@ def aggregate(): 'error_code'), 'id': i['id']} if "extra" in resp: finally_response['extra'] = resp['extra'] - #request = request_ + # request = request_ return jsonify(finally_response) finally_response['value'].append( {'id': i.get('id'), 'value': resp['value'] if hasattr(resp, 'get') else resp}) - #request = request_ + # request = request_ return jsonify(finally_response) except KeyError: return error_return() diff --git a/latest version/web/index.py b/latest version/web/index.py index a1ba21c..cce268f 100644 --- a/latest version/web/index.py +++ b/latest version/web/index.py @@ -422,7 +422,7 @@ def all_character(): def change_character(): # 修改角色数据 skill_ids = ['No_skill', 'gauge_easy', 'note_mirror', 'gauge_hard', 'frag_plus_10_pack_stellights', 'gauge_easy|frag_plus_15_pst&prs', 'gauge_hard|fail_frag_minus_100', 'frag_plus_5_side_light', 'visual_hide_hp', 'frag_plus_5_side_conflict', 'challenge_fullcombo_0gauge', 'gauge_overflow', 'gauge_easy|note_mirror', 'note_mirror', 'visual_tomato_pack_tonesphere', - 'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', 'frags_kou', 'visual_ink', 'shirabe_entry_fee', 'frags_yume', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', 'eto_uncap', 'luna_uncap', 'frags_preferred_song', 'visual_ghost_skynotes', 'ayu_uncap', 'skill_vita', 'skill_fatalis', 'skill_reunion', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'ilith_awakened_skill', 'skill_mithra', 'skill_toa', 'skill_nami_twilight', 'skill_ilith_ivy', 'skill_hikari_vanessa', 'skill_maya', 'skill_luin', 'skill_luin_uncap'] + 'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', 'frags_kou', 'visual_ink', 'shirabe_entry_fee', 'frags_yume', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', 'eto_uncap', 'luna_uncap', 'frags_preferred_song', 'visual_ghost_skynotes', 'ayu_uncap', 'skill_vita', 'skill_fatalis', 'skill_reunion', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'ilith_awakened_skill', 'skill_mithra', 'skill_toa', 'skill_nami_twilight', 'skill_ilith_ivy', 'skill_hikari_vanessa', 'skill_maya', 'skill_luin', 'skill_luin_uncap', 'skill_kanae_uncap'] return render_template('web/changechar.html', skill_ids=skill_ids) From 2edf5593732b17182fa2a02439a72981cdeb4f0c Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Mon, 15 Jan 2024 23:33:09 +0800 Subject: [PATCH 03/32] [Enhance] Support uncapped Kanae's skill - Add support for `skill_kanae_uncap` - For Arcaea 5.2.6 --- latest version/core/character.py | 12 +++ latest version/core/constant.py | 2 +- latest version/core/user.py | 6 +- latest version/core/world.py | 102 ++++++++++++++---------- latest version/database/init/tables.sql | 3 +- latest version/server/others.py | 2 +- 6 files changed, 83 insertions(+), 44 deletions(-) diff --git a/latest version/core/character.py b/latest version/core/character.py index 72f3e22..66ca7d9 100644 --- a/latest version/core/character.py +++ b/latest version/core/character.py @@ -120,6 +120,18 @@ def __init__(self, c=None) -> None: self.uncap_cores: list = [] self.voice: list = None + @property + def frag_value(self) -> float: + return self.frag.get_value(self.level) + + @property + def prog_value(self) -> float: + return self.prog.get_value(self.level) + + @property + def overdrive_value(self) -> float: + return self.overdrive.get_value(self.level) + @property def skill_id_displayed(self) -> str: return None diff --git a/latest version/core/constant.py b/latest version/core/constant.py index 2756606..8f2b4b4 100644 --- a/latest version/core/constant.py +++ b/latest version/core/constant.py @@ -1,6 +1,6 @@ from .config_manager import Config -ARCAEA_SERVER_VERSION = 'v2.11.3.2.test' +ARCAEA_SERVER_VERSION = 'v2.11.3.2' ARCAEA_LOG_DATBASE_VERSION = 'v1.1' diff --git a/latest version/core/user.py b/latest version/core/user.py index dc135e3..12f712f 100644 --- a/latest version/core/user.py +++ b/latest version/core/user.py @@ -308,6 +308,7 @@ def __init__(self, c, user_id=None) -> None: self.max_stamina_notification_enabled = False self.prog_boost: int = 0 self.beyond_boost_gauge: float = 0 + self.kanae_stored_prog: float = 0 self.next_fragstam_ts: int = None self.world_mode_locked_end_ts: int = None self.current_map: 'Map' = None @@ -501,6 +502,7 @@ def to_dict(self) -> dict: "current_map": self.current_map.map_id, "prog_boost": self.prog_boost, "beyond_boost_gauge": self.beyond_boost_gauge, + "kanae_stored_prog": self.kanae_stored_prog, "next_fragstam_ts": self.next_fragstam_ts, "max_stamina_ts": self.stamina.max_stamina_ts, "stamina": self.stamina.stamina, @@ -559,6 +561,7 @@ def from_list(self, x: list) -> 'UserInfo': self.stamina.set_value(x[32], x[33]) self.world_mode_locked_end_ts = x[34] if x[34] else -1 self.beyond_boost_gauge = x[35] if x[35] else 0 + self.kanae_stored_prog = x[36] if x[36] else 0 return self @@ -616,7 +619,7 @@ def select_user_about_world_play(self) -> None: 查询user表有关世界模式打歌的信息 ''' self.c.execute( - '''select character_id, max_stamina_ts, stamina, is_skill_sealed, is_char_uncapped, is_char_uncapped_override, current_map, world_mode_locked_end_ts, beyond_boost_gauge from user where user_id=?''', (self.user_id,)) + '''select character_id, max_stamina_ts, stamina, is_skill_sealed, is_char_uncapped, is_char_uncapped_override, current_map, world_mode_locked_end_ts, beyond_boost_gauge, kanae_stored_prog from user where user_id=?''', (self.user_id,)) x = self.c.fetchone() if not x: raise NoData('No user.', 108, -3) @@ -630,6 +633,7 @@ def select_user_about_world_play(self) -> None: self.current_map = UserMap(self.c, x[6], self) self.world_mode_locked_end_ts = x[7] if x[7] else -1 self.beyond_boost_gauge = x[8] if x[8] else 0 + self.kanae_stored_prog = x[9] if x[9] else 0 @property def global_rank(self) -> int: diff --git a/latest version/core/world.py b/latest version/core/world.py index d2ec611..93ff3e5 100644 --- a/latest version/core/world.py +++ b/latest version/core/world.py @@ -440,13 +440,14 @@ def update(self): class WorldSkillMixin: def before_calculate(self) -> None: - factory_dict = {'skill_vita': self._skill_vita, - 'skill_mika': self._skill_mika, - 'skill_ilith_ivy': self._skill_ilith_ivy, - 'ilith_awakened_skill': self._ilith_awakened_skill, - 'skill_hikari_vanessa': self._skill_hikari_vanessa, - 'skill_mithra': self._skill_mithra - } + factory_dict = { + 'skill_vita': self._skill_vita, + 'skill_mika': self._skill_mika, + 'skill_ilith_ivy': self._skill_ilith_ivy, + 'ilith_awakened_skill': self._ilith_awakened_skill, + 'skill_hikari_vanessa': self._skill_hikari_vanessa, + 'skill_mithra': self._skill_mithra + } if self.user_play.beyond_gauge == 0 and self.character_used.character_id == 35 and self.character_used.skill_id_displayed: self._special_tempest() @@ -454,14 +455,15 @@ def before_calculate(self) -> None: factory_dict[self.character_used.skill_id_displayed]() def after_climb(self) -> None: - factory_dict = {'eto_uncap': self._eto_uncap, - 'ayu_uncap': self._ayu_uncap, - 'skill_fatalis': self._skill_fatalis, - 'skill_amane': self._skill_amane, - 'skill_maya': self._skill_maya, - 'luna_uncap': self._luna_uncap, - 'skill_kanae_uncap': self._skill_kanae_uncap - } + factory_dict = { + 'eto_uncap': self._eto_uncap, + 'ayu_uncap': self._ayu_uncap, + 'skill_fatalis': self._skill_fatalis, + 'skill_amane': self._skill_amane, + 'skill_maya': self._skill_maya, + 'luna_uncap': self._luna_uncap, + 'skill_kanae_uncap': self._skill_kanae_uncap + } if self.character_used.skill_id_displayed in factory_dict: factory_dict[self.character_used.skill_id_displayed]() @@ -597,8 +599,11 @@ def _skill_maya(self) -> None: def _skill_kanae_uncap(self) -> None: ''' kanae 觉醒技能,保存世界模式 progress 并在下次结算 + 直接加减在 progress 最后 + 技能存储 base_progress * PROG / 50,下一次消耗全部存储值(无视技能和搭档,但需要非技能隐藏状态) ''' - pass + self.kanae_stored_progress = self.progress_normalized + self.user.current_map.reclimb(self.final_progress) class BaseWorldPlay(WorldSkillMixin): @@ -615,12 +620,8 @@ def __init__(self, c=None, user=None, user_play=None) -> None: self.user_play = user_play self.character_used = None - self.progress_normalized: float = None self.character_bonus_progress_normalized: float = None - # kanae_stored_progress: float - # kanae_added_progress: float - # partner_multiply: float # wpaid: str def to_dict(self) -> dict: @@ -651,10 +652,7 @@ def to_dict(self) -> dict: "max_stamina_ts": self.user.stamina.max_stamina_ts, 'world_mode_locked_end_ts': self.user.world_mode_locked_end_ts, 'beyond_boost_gauge': self.user.beyond_boost_gauge, - # 'kanae_stored_progress': 7114000, # 往群愿里塞 - # 'kanae_added_progress': 514000, # 群愿往外拿 # 'wpaid': 'helloworld', # world play id ??? - # 'partner_multiply': 456, # ? } if self.character_used.skill_id_displayed == 'skill_maya': @@ -691,11 +689,11 @@ def base_progress(self) -> float: raise NotImplementedError @property - def final_progress(self) -> float: + def progress_normalized(self) -> float: raise NotImplementedError - def get_step(self) -> None: - # to get self.progress_normalized + @property + def final_progress(self) -> float: raise NotImplementedError def update(self) -> None: @@ -711,6 +709,9 @@ def update(self) -> None: self.user.character.select_character_info() if not self.user.is_skill_sealed: self.character_used = self.user.character + if self.user_play.beyond_gauge == 0 and self.user.kanae_stored_prog > 0: + # 实在不想拆开了,在这里判断一下,注意这段不会在 BeyondWorldPlay 中执行 + self.kanae_added_progress = self.user.kanae_stored_prog else: self.character_used.character_id = self.user.character.character_id self.character_used.level.level = self.user.character.level.level @@ -721,7 +722,6 @@ def update(self) -> None: self.user.current_map.select_map_info() self.before_calculate() - self.get_step() self.user.current_map.climb(self.final_progress) self.after_climb() @@ -755,6 +755,10 @@ def __init__(self, c=None, user=None, user_play=None) -> None: self.prog_tempest: float = None self.prog_skill_increase: float = None + self.kanae_added_progress: float = None # 群愿往外拿 + self.kanae_stored_progress: float = None # 往群愿里塞 + # self.user.kanae_stored_prog: float 群愿有的 + def to_dict(self) -> dict: r = super().to_dict() @@ -773,6 +777,12 @@ def to_dict(self) -> dict: r['char_stats']['prog'] += self.prog_tempest # 没试过要不要这样 r['char_stats']['prog_tempest'] = self.prog_tempest + if self.kanae_added_progress is not None: + r['kanae_added_progress'] = self.kanae_added_progress + + if self.kanae_stored_progress is not None: + r['kanae_stored_progress'] = self.kanae_stored_progress + r['partner_adjusted_prog'] = self.partner_adjusted_prog r["user_map"]["steps"] = [x.to_dict() @@ -793,7 +803,7 @@ def base_progress(self) -> float: @property def final_progress(self) -> float: - return (self.progress_normalized + (self.character_bonus_progress_normalized or 0)) * self.step_times + return (self.progress_normalized + (self.character_bonus_progress_normalized or 0)) * self.step_times + (self.kanae_added_progress or 0) - (self.kanae_stored_progress or 0) @property def partner_adjusted_prog(self) -> float: @@ -805,9 +815,9 @@ def partner_adjusted_prog(self) -> float: prog += self.prog_skill_increase return prog - def get_step(self) -> None: - self.progress_normalized = self.base_progress * \ - (self.partner_adjusted_prog / 50) + @property + def progress_normalized(self) -> float: + return self.base_progress * (self.partner_adjusted_prog / 50) def update(self) -> None: '''世界模式更新''' @@ -819,6 +829,17 @@ def update(self) -> None: self.user.update_user_one_column( 'beyond_boost_gauge', self.user.beyond_boost_gauge) + # 更新kanae存储进度 + if self.kanae_stored_progress is not None: + self.user.kanae_stored_prog = self.kanae_stored_progress + self.user.update_user_one_column( + 'kanae_stored_prog', self.user.kanae_stored_prog) + return + if self.kanae_added_progress is None: + return + self.kanae_stored_progress = 0 + self.user.update_user_one_column('kanae_stored_prog', 0) + class BeyondWorldPlay(BaseWorldPlay): @@ -844,6 +865,14 @@ def base_progress(self) -> float: def final_progress(self) -> float: return self.progress_normalized * self.step_times + @property + def progress_normalized(self) -> float: + overdrive = self.character_used.overdrive_value + if self.over_skill_increase: + overdrive += self.over_skill_increase + + return self.base_progress * (overdrive / 50) * self.affinity_multiplier + def to_dict(self) -> dict: r = super().to_dict() @@ -851,6 +880,8 @@ def to_dict(self) -> dict: r['pre_boost_progress'] = self.progress_normalized * \ self.user_play.fragment_multiply / 100 + # r['partner_multiply'] = self.affinity_multiplier # ? + if self.over_skill_increase is not None: r['char_stats']['over_skill_increase'] = self.over_skill_increase @@ -870,12 +901,3 @@ def update(self) -> None: self.user.beyond_boost_gauge = 0 self.user.update_user_one_column( 'beyond_boost_gauge', self.user.beyond_boost_gauge) - - def get_step(self) -> None: - overdrive = self.character_used.overdrive.get_value( - self.character_used.level) - if self.over_skill_increase: - overdrive += self.over_skill_increase - - self.progress_normalized = self.base_progress * \ - (overdrive / 50) * self.affinity_multiplier diff --git a/latest version/database/init/tables.sql b/latest version/database/init/tables.sql index 5e8d8f2..0a6d3c7 100644 --- a/latest version/database/init/tables.sql +++ b/latest version/database/init/tables.sql @@ -34,7 +34,8 @@ next_fragstam_ts int, max_stamina_ts int, stamina int, world_mode_locked_end_ts int, -beyond_boost_gauge real default 0 +beyond_boost_gauge real default 0, +kanae_stored_prog real default 0 ); create table if not exists login(access_token text, user_id int, diff --git a/latest version/server/others.py b/latest version/server/others.py index b8e7c1b..58d1d47 100644 --- a/latest version/server/others.py +++ b/latest version/server/others.py @@ -60,7 +60,7 @@ def finale_end(): return success_return({}) -@bp.route('/applog/me', methods=['POST']) +@bp.route('/applog/me/log', methods=['POST']) def applog_me(): # 异常日志,不处理 return success_return({}) From 9d096f480b7431d1e02cbcbc1b208e7fcddfe8d9 Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Tue, 30 Jan 2024 15:16:26 +0800 Subject: [PATCH 04/32] [Enhance] Breached World - Add support for Breached World Map - Change the recover time of using fragments buying stamina to 23 hours - For Arcaea 5.3.0 --- latest version/core/config_manager.py | 2 +- latest version/core/constant.py | 3 +- latest version/core/score.py | 12 ++- latest version/core/world.py | 120 +++++++++++++++++++--- latest version/database/init/arc_data.py | 4 +- latest version/database/init/singles.json | 18 ++++ latest version/server/purchase.py | 3 +- 7 files changed, 140 insertions(+), 22 deletions(-) diff --git a/latest version/core/config_manager.py b/latest version/core/config_manager.py index fa5cb2a..6c4fd35 100644 --- a/latest version/core/config_manager.py +++ b/latest version/core/config_manager.py @@ -12,7 +12,7 @@ class Config: SONG_FILE_HASH_PRE_CALCULATE = True - GAME_API_PREFIX = '/akeome/24' + GAME_API_PREFIX = '/samusugiru/26' ALLOW_APPVERSION = [] # list[str] diff --git a/latest version/core/constant.py b/latest version/core/constant.py index 8f2b4b4..7a3045a 100644 --- a/latest version/core/constant.py +++ b/latest version/core/constant.py @@ -1,6 +1,6 @@ from .config_manager import Config -ARCAEA_SERVER_VERSION = 'v2.11.3.2' +ARCAEA_SERVER_VERSION = 'v2.11.3.3' ARCAEA_LOG_DATBASE_VERSION = 'v1.1' @@ -11,6 +11,7 @@ class Constant: MAX_STAMINA = 12 STAMINA_RECOVER_TICK = 1800000 + FRAGSTAM_RECOVER_TICK = 23 * 3600 * 1000 COURSE_STAMINA_COST = 4 diff --git a/latest version/core/score.py b/latest version/core/score.py index fb5fc86..21f23e9 100644 --- a/latest version/core/score.py +++ b/latest version/core/score.py @@ -11,7 +11,7 @@ from .song import Chart from .sql import Connect, Query, Sql from .util import get_today_timestamp, md5 -from .world import WorldPlay, BeyondWorldPlay +from .world import WorldPlay, BeyondWorldPlay, BreachedWorldPlay class Score: @@ -500,8 +500,14 @@ def upload_score(self) -> None: # 世界模式判断 if self.is_world_mode: - self.world_play = WorldPlay( - self.c, self.user, self) if self.beyond_gauge == 0 else BeyondWorldPlay(self.c, self.user, self) + self.user.select_user_about_world_play() + self.user.current_map.select_map_info() + if self.user.current_map.is_breached: + self.world_play = BreachedWorldPlay(self.c, self.user, self) + elif self.user.current_map.is_beyond: + self.world_play = BeyondWorldPlay(self.c, self.user, self) + else: + self.world_play = WorldPlay(self.c, self.user, self) self.world_play.update() # 课题模式判断 diff --git a/latest version/core/world.py b/latest version/core/world.py index 93ff3e5..0ce81f3 100644 --- a/latest version/core/world.py +++ b/latest version/core/world.py @@ -99,6 +99,7 @@ def __init__(self, map_id: str = None) -> None: self.map_id: str = map_id self.is_legacy: bool = None self.is_beyond: bool = None + self.is_breached: bool = None self.beyond_health: int = None self.character_affinity: list = [] self.affinity_multiplier: list = [] @@ -119,6 +120,11 @@ def __init__(self, map_id: str = None) -> None: self.require_localunlock_challengeid: str = None self.chain_info: dict = None + # self.requires: list[dict] = None + + self.disable_over: bool = None + self.new_law: str = None + @property def rewards(self) -> list: if self.__rewards is None: @@ -145,6 +151,7 @@ def to_dict(self) -> dict: 'map_id': self.map_id, 'is_legacy': self.is_legacy, 'is_beyond': self.is_beyond, + 'is_breached': self.is_breached, 'beyond_health': self.beyond_health, 'character_affinity': self.character_affinity, 'affinity_multiplier': self.affinity_multiplier, @@ -165,11 +172,16 @@ def to_dict(self) -> dict: } if self.chain_info is not None: r['chain_info'] = self.chain_info + if self.disable_over: + r['disable_over'] = self.disable_over + if self.new_law is not None and self.new_law != '': + r['new_law'] = self.new_law return r def from_dict(self, raw_dict: dict) -> 'Map': - self.is_legacy = raw_dict.get('is_legacy') - self.is_beyond = raw_dict.get('is_beyond') + self.is_legacy = raw_dict.get('is_legacy', False) + self.is_beyond = raw_dict.get('is_beyond', False) + self.is_breached = raw_dict.get('is_breached', False) self.beyond_health = raw_dict.get('beyond_health') self.character_affinity = raw_dict.get('character_affinity', []) self.affinity_multiplier = raw_dict.get('affinity_multiplier', []) @@ -189,6 +201,9 @@ def from_dict(self, raw_dict: dict) -> 'Map': 'require_localunlock_challengeid', '') self.chain_info = raw_dict.get('chain_info') self.steps = [Step().from_dict(s) for s in raw_dict.get('steps')] + + self.disable_over = raw_dict.get('disable_over') + self.new_law = raw_dict.get('new_law') return self def select_map_info(self): @@ -696,13 +711,12 @@ def progress_normalized(self) -> float: def final_progress(self) -> float: raise NotImplementedError - def update(self) -> None: - '''世界模式更新''' + def before_update(self) -> None: if self.user_play.prog_boost_multiply != 0: self.user.update_user_one_column('prog_boost', 0) self.user_play.clear_play_state() - self.user.select_user_about_world_play() + # self.user.select_user_about_world_play() self.character_used = Character() @@ -720,10 +734,9 @@ def update(self) -> None: self.character_used.prog.set_parameter(50, 50, 50) self.character_used.overdrive.set_parameter(50, 50, 50) - self.user.current_map.select_map_info() - self.before_calculate() - self.user.current_map.climb(self.final_progress) - self.after_climb() + # self.user.current_map.select_map_info() + + def after_update(self) -> None: for i in self.user.current_map.rewards_for_climbing: # 物品分发 for j in i['items']: @@ -747,6 +760,14 @@ def update(self) -> None: self.user.current_map.update() + def update(self) -> None: + '''世界模式更新''' + self.before_update() + self.before_calculate() + self.user.current_map.climb(self.final_progress) + self.after_climb() + self.after_update() + class WorldPlay(BaseWorldPlay): def __init__(self, c=None, user=None, user_play=None) -> None: @@ -819,9 +840,9 @@ def partner_adjusted_prog(self) -> float: def progress_normalized(self) -> float: return self.base_progress * (self.partner_adjusted_prog / 50) - def update(self) -> None: + def after_update(self) -> None: '''世界模式更新''' - super().update() + super().after_update() # 更新byd大招蓄力条 self.user.beyond_boost_gauge += self.beyond_boost_gauge_addition @@ -892,12 +913,83 @@ def to_dict(self) -> dict: return r - def update(self) -> None: - super().update() - + def after_update(self) -> None: + super().after_update() if self.user_play.beyond_boost_gauge_usage != 0 and self.user_play.beyond_boost_gauge_usage <= self.user.beyond_boost_gauge: self.user.beyond_boost_gauge -= self.user_play.beyond_boost_gauge_usage if abs(self.user.beyond_boost_gauge) <= 1e-5: self.user.beyond_boost_gauge = 0 self.user.update_user_one_column( 'beyond_boost_gauge', self.user.beyond_boost_gauge) + + +class WorldLawMixin: + def breached_before_calculate(self) -> None: + factory_dict = { + 'over100_step50': self._over100_step50, + 'frag50': self._frag50, + 'lowlevel': self._lowlevel, + 'antiheroism': self._antiheroism + } + if self.user.current_map.new_law in factory_dict: + factory_dict[self.user.current_map.new_law]() + + def _over100_step50(self) -> None: + '''PROG = OVER + STEP / 2''' + self.new_law_prog = self.character_used.overdrive_value + \ + self.character_used.prog_value / 2 + + def _frag50(self) -> None: + '''PROG x= FRAG''' + self.new_law_prog = self.character_used.frag_value + + def _lowlevel(self) -> None: + '''PROG x= max(1.0, 2.0 - 0.1 x LEVEL)''' + self.new_law_prog = 50 * \ + max(1, 2 - 0.1 * self.character_used.level.level) + + def _antiheroism(self) -> None: + '''PROG = OVER - ||OVER-FRAG|-|OVER-STEP||''' + over = self.character_used.overdrive_value + x = abs(over - self.character_used.frag_value) + y = abs(over - self.character_used.prog_value) + self.new_law_prog = over - abs(x - y) + + +class BreachedWorldPlay(BeyondWorldPlay, WorldLawMixin): + def __init__(self, c=None, user=None, user_play=None) -> None: + super().__init__(c, user, user_play) + self.new_law_prog: float = None + + @property + def new_law_multiply(self) -> float: + if self.new_law_prog is None: + return 1 + return self.new_law_prog / 50 + + @property + def affinity_multiplier(self) -> float: + return 1 + + @property + def progress_normalized(self) -> float: + if self.user.current_map.disable_over: + return self.base_progress * self.new_law_multiply + + overdrive = self.character_used.overdrive_value + if self.over_skill_increase: + overdrive += self.over_skill_increase + return self.base_progress * (overdrive / 50) * self.new_law_multiply + + def to_dict(self) -> dict: + r = super().to_dict() + r['new_law_multiply'] = self.new_law_multiply + return r + + def update(self) -> None: + self.before_update() + self.before_calculate() + self.breached_before_calculate() + self.user.current_map.climb(self.final_progress) + self.after_climb() + self.after_update() diff --git a/latest version/database/init/arc_data.py b/latest version/database/init/arc_data.py index 3a3f66d..df8d1b5 100644 --- a/latest version/database/init/arc_data.py +++ b/latest version/database/init/arc_data.py @@ -69,10 +69,10 @@ class InitData: 'core_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful', 'core_course_skip_purchase', 'core_umbral', 'core_wacca', 'core_sunset'] world_songs = ["babaroque", "shadesoflight", "kanagawa", "lucifer", "anokumene", "ignotus", "rabbitintheblackroom", "qualia", "redandblue", "bookmaker", "darakunosono", "espebranch", "blacklotus", "givemeanightmare", "vividtheory", "onefr", "gekka", "vexaria3", "infinityheaven3", "fairytale3", "goodtek3", "suomi", "rugie", "faintlight", "harutopia", "goodtek", "dreaminattraction", "syro", "diode", "freefall", "grimheart", "blaster", - "cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris', 'ultimatetaste', 'rgb', 'matenrou', 'dynitikos', 'amekagura', 'fantasy', 'aloneandlorn', 'felys', 'onandon', 'hotarubinoyuki'] + "cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris', 'ultimatetaste', 'rgb', 'matenrou', 'dynitikos', 'amekagura', 'fantasy', 'aloneandlorn', 'felys', 'onandon', 'hotarubinoyuki', 'oblivia3', 'libertas3', 'einherjar3', 'purpleverse3', 'viciousheroism3', 'inkarusi3', 'cyberneciacatharsis3'] world_unlocks = ["scenery_chap1", "scenery_chap2", - "scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7"] + "scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7", "scenery_beyond"] course_banners = ['course_banner_' + str(i) for i in range(1, 12)] diff --git a/latest version/database/init/singles.json b/latest version/database/init/singles.json index 68d39f8..1215d4c 100644 --- a/latest version/database/init/singles.json +++ b/latest version/database/init/singles.json @@ -1594,5 +1594,23 @@ ], "orig_price": 100, "price": 100 + }, + { + "name": "lunarossa", + "items": [ + { + "type": "single", + "id": "lunarossa", + "is_available": true + }, + { + "type": "core", + "amount": 1, + "id": "core_generic", + "is_available": true + } + ], + "orig_price": 100, + "price": 100 } ] \ No newline at end of file diff --git a/latest version/server/purchase.py b/latest version/server/purchase.py index b45b315..083eeb8 100644 --- a/latest version/server/purchase.py +++ b/latest version/server/purchase.py @@ -2,6 +2,7 @@ from flask import Blueprint, request +from core.constant import Constant from core.error import InputError, ItemUnavailable, PostError from core.item import ItemFactory, Stamina6 from core.purchase import Purchase, PurchaseList @@ -130,7 +131,7 @@ def purchase_stamina(user_id, buy_stamina_type): return ItemUnavailable('Buying stamina by fragment is not available yet.', 905) user.update_user_one_column( - 'next_fragstam_ts', now + 24 * 3600 * 1000) + 'next_fragstam_ts', now + Constant.FRAGSTAM_RECOVER_TICK) s = Stamina6(c) s.user_claim_item(user) return success_return({ From 50369d1f23f3c1e5f4f28897b11999986b5a8f26 Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Tue, 20 Feb 2024 23:04:58 +0800 Subject: [PATCH 05/32] [Bug fix] API for Link Play - Fix a bug that API for Link Play cannot work - Add an example breached map #148 --- README.md | 1 - latest version/core/world.py | 10 ++- latest version/database/map/byd_inkarusi.json | 80 +++++++++++++++++++ latest version/linkplay_server/store.py | 2 +- latest version/linkplay_server/udp_class.py | 2 +- 5 files changed, 88 insertions(+), 7 deletions(-) create mode 100644 latest version/database/map/byd_inkarusi.json diff --git a/README.md b/README.md index 22185ea..77f7a6b 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,6 @@ It is just so interesting. What it can do is under exploration. [这里 Here](https://github.com/Lost-MSth/Arcaea-server/releases) [Arcaea-CN official](https://arcaea.lowiro.com/zh) -[Arcaea-Konmai Academy](https://616.sb) ## 更新日志 Update log diff --git a/latest version/core/world.py b/latest version/core/world.py index 0ce81f3..c5414dd 100644 --- a/latest version/core/world.py +++ b/latest version/core/world.py @@ -936,8 +936,9 @@ def breached_before_calculate(self) -> None: def _over100_step50(self) -> None: '''PROG = OVER + STEP / 2''' - self.new_law_prog = self.character_used.overdrive_value + \ - self.character_used.prog_value / 2 + over = self.character_used.overdrive_value + self.over_skill_increase + prog = self.character_used.prog_value + self.prog_skill_increase + self.new_law_prog = over + prog / 2 def _frag50(self) -> None: '''PROG x= FRAG''' @@ -950,9 +951,10 @@ def _lowlevel(self) -> None: def _antiheroism(self) -> None: '''PROG = OVER - ||OVER-FRAG|-|OVER-STEP||''' - over = self.character_used.overdrive_value + over = self.character_used.overdrive_value + self.over_skill_increase + prog = self.character_used.prog_value + self.prog_skill_increase x = abs(over - self.character_used.frag_value) - y = abs(over - self.character_used.prog_value) + y = abs(over - prog) self.new_law_prog = over - abs(x - y) diff --git a/latest version/database/map/byd_inkarusi.json b/latest version/database/map/byd_inkarusi.json new file mode 100644 index 0000000..21574f2 --- /dev/null +++ b/latest version/database/map/byd_inkarusi.json @@ -0,0 +1,80 @@ +{ + "map_id": "byd_inkarusi", + "is_legacy": false, + "character_affinity": [], + "affinity_multiplier": [], + "chapter": 2001, + "available_from": -1, + "available_to": 9999999999999, + "is_repeatable": false, + "require_id": "inkarusi2", + "require_type": "chart_unlock", + "is_breached": true, + "stamina_cost": 3, + "new_law": "over100_step50", + "disable_over": true, + "coordinate": "500,0", + "beyond_health": 200, + "step_count": 5, + "custom_bg": "", + "is_beyond": true, + "requires": [ + { + "type": "chart_unlock", + "id": "inkarusi2" + } + ], + "steps": [ + { + "map_id": "byd_inkarusi", + "position": 0, + "capture": 50 + }, + { + "map_id": "byd_inkarusi", + "position": 1, + "capture": 50, + "items": [ + { + "type": "core", + "id": "core_generic", + "amount": 2 + } + ] + }, + { + "map_id": "byd_inkarusi", + "position": 2, + "capture": 50, + "items": [ + { + "type": "fragment", + "amount": 500 + } + ] + }, + { + "map_id": "byd_inkarusi", + "position": 3, + "capture": 50, + "items": [ + { + "type": "core", + "id": "core_generic", + "amount": 2 + } + ] + }, + { + "map_id": "byd_inkarusi", + "position": 4, + "capture": 0, + "items": [ + { + "id": "inkarusi3", + "type": "world_song" + } + ] + } + ] +} \ No newline at end of file diff --git a/latest version/linkplay_server/store.py b/latest version/linkplay_server/store.py index fdb7b71..2ca4e7d 100644 --- a/latest version/linkplay_server/store.py +++ b/latest version/linkplay_server/store.py @@ -120,7 +120,7 @@ def handle(self) -> dict: r = getattr(self, self.endpoint)() except Exception as e: logging.error(e) - return 999 + return {'code': 999} if isinstance(r, int): return {'code': r} return { diff --git a/latest version/linkplay_server/udp_class.py b/latest version/linkplay_server/udp_class.py index 617906b..5ed8f90 100644 --- a/latest version/linkplay_server/udp_class.py +++ b/latest version/linkplay_server/udp_class.py @@ -104,7 +104,7 @@ def __init__(self) -> None: def to_dict(self) -> dict: p = [i.to_dict() for i in self.players if i.player_id != 0] for i in p: - i['is_host'] = i['player_id'] == self.host_id + i['is_host'] = i['multiplay_player_id'] == self.host_id return { 'room_id': self.room_id, 'room_code': self.room_code, From b3bf55407f73d46e73928caadaa107a549ba88d0 Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Sat, 24 Feb 2024 16:39:15 +0800 Subject: [PATCH 06/32] [Enhance] Game new/old API prefix list support - Add some endpoints for old API prefixes to notify users to update the client; add support for multiple game prefixes --- latest version/core/config_manager.py | 3 +- latest version/main.py | 2 +- latest version/server/__init__.py | 45 ++++++++++++++++++++------- latest version/server/func.py | 2 +- 4 files changed, 37 insertions(+), 15 deletions(-) diff --git a/latest version/core/config_manager.py b/latest version/core/config_manager.py index 6c4fd35..2730e5f 100644 --- a/latest version/core/config_manager.py +++ b/latest version/core/config_manager.py @@ -12,7 +12,8 @@ class Config: SONG_FILE_HASH_PRE_CALCULATE = True - GAME_API_PREFIX = '/samusugiru/26' + GAME_API_PREFIX = '/samusugiru/26' # str | list[str] + OLD_GAME_API_PREFIX = [] # str | list[str] ALLOW_APPVERSION = [] # list[str] diff --git a/latest version/main.py b/latest version/main.py index bea401a..f05cc30 100644 --- a/latest version/main.py +++ b/latest version/main.py @@ -54,7 +54,7 @@ app.register_blueprint(web.login.bp) app.register_blueprint(web.index.bp) app.register_blueprint(api.bp) -app.register_blueprint(server.bp) +list(map(app.register_blueprint, server.get_bps())) # app.register_blueprint(webapi.bp) diff --git a/latest version/server/__init__.py b/latest version/server/__init__.py index a6d5e70..df33514 100644 --- a/latest version/server/__init__.py +++ b/latest version/server/__init__.py @@ -1,18 +1,39 @@ -from flask import Blueprint +from flask import Blueprint, jsonify from core.config_manager import Config from . import (auth, course, friend, multiplayer, others, present, purchase, score, user, world) -bp = Blueprint('server', __name__, url_prefix=Config.GAME_API_PREFIX) -bp.register_blueprint(user.bp) -bp.register_blueprint(auth.bp) -bp.register_blueprint(friend.bp) -bp.register_blueprint(score.bp) -bp.register_blueprint(world.bp) -bp.register_blueprint(purchase.bp) -bp.register_blueprint(present.bp) -bp.register_blueprint(others.bp) -bp.register_blueprint(multiplayer.bp) -bp.register_blueprint(course.bp) + +__bp_old = Blueprint('old_server', __name__) + + +@__bp_old.route('/', methods=['GET', 'POST']) # 旧版 API 提示 +def server_hello(any): + return jsonify({"success": False, "error_code": 5}) + + +def get_bps(): + def string_to_list(s): + if isinstance(s, str): + s = [s] + elif not isinstance(s, list): + s = [] + return s + + bp = Blueprint('server', __name__) + list(map(bp.register_blueprint, [user.bp, auth.bp, friend.bp, score.bp, + world.bp, purchase.bp, present.bp, others.bp, multiplayer.bp, course.bp])) + + bps = [Blueprint(x, __name__, url_prefix=x) + for x in string_to_list(Config.GAME_API_PREFIX)] + for x in bps: + x.register_blueprint(bp) + + old_bps = [Blueprint(x, __name__, url_prefix=x) + for x in string_to_list(Config.OLD_GAME_API_PREFIX)] + for x in old_bps: + x.register_blueprint(__bp_old) + + return bps + old_bps diff --git a/latest version/server/func.py b/latest version/server/func.py index c2d96ef..5db87e7 100644 --- a/latest version/server/func.py +++ b/latest version/server/func.py @@ -106,7 +106,7 @@ def header_check(request) -> ArcError: headers = request.headers if Config.ALLOW_APPVERSION: # 版本检查 if 'AppVersion' not in headers or headers['AppVersion'] not in Config.ALLOW_APPVERSION: - return NoAccess('Invalid app version', 1203) + return NoAccess('Invalid app version', 5) if has_arc_hash and not ArcHashChecker(request).check(): return NoAccess('Invalid request') From e206247c09c0208b5a68e44c08183107eda4ec29 Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Sun, 25 Feb 2024 17:28:54 +0800 Subject: [PATCH 07/32] [Enhance] Self account delete - Add support for users destroy their own accounts --- README.md | 2 +- latest version/core/config_manager.py | 3 ++ latest version/core/constant.py | 3 +- latest version/core/init.py | 57 +++++++++++++-------- latest version/core/operation.py | 73 +++++++++++++++++++++++++++ latest version/server/user.py | 7 ++- latest version/web/index.py | 7 ++- latest version/web/system.py | 9 ---- 8 files changed, 125 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 77f7a6b..cc2a867 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ This procedure is mainly used for study and research, and shall not be used for - 多设备自动封号 Auto-ban of multiple devices - :warning: 多设备登录 Multi device login - 登录频次限制 Login rate limit - - :x: 销号 Destroy account + - :warning: 销号 Destroy account - 成绩上传 Score upload - 成绩校验 Score check - 成绩排名 Score rank diff --git a/latest version/core/config_manager.py b/latest version/core/config_manager.py index 2730e5f..5a858b4 100644 --- a/latest version/core/config_manager.py +++ b/latest version/core/config_manager.py @@ -71,6 +71,8 @@ class Config: SAVE_FULL_UNLOCK = False + ALLOW_SELF_ACCOUNT_DELETE = False + # ------------------------------------------ # You can change this to make another PTT mechanism. @@ -86,6 +88,7 @@ class Config: SQLITE_DATABASE_BACKUP_FOLDER_PATH = './database/backup/' DATABASE_INIT_PATH = './database/init/' SQLITE_LOG_DATABASE_PATH = './database/arcaea_log.db' + SQLITE_DATABASE_DELETED_PATH = './database/arcaea_database_deleted.db' GAME_LOGIN_RATE_LIMIT = '30/5 minutes' API_LOGIN_RATE_LIMIT = '10/5 minutes' diff --git a/latest version/core/constant.py b/latest version/core/constant.py index 7a3045a..392b32a 100644 --- a/latest version/core/constant.py +++ b/latest version/core/constant.py @@ -1,6 +1,6 @@ from .config_manager import Config -ARCAEA_SERVER_VERSION = 'v2.11.3.3' +ARCAEA_SERVER_VERSION = 'v2.11.3.4' ARCAEA_LOG_DATBASE_VERSION = 'v1.1' @@ -45,6 +45,7 @@ class Constant: SONGLIST_FILE_PATH = Config.SONGLIST_FILE_PATH SQLITE_DATABASE_PATH = Config.SQLITE_DATABASE_PATH SQLITE_LOG_DATABASE_PATH = Config.SQLITE_LOG_DATABASE_PATH + SQLITE_DATABASE_DELETED_PATH = Config.SQLITE_DATABASE_DELETED_PATH DOWNLOAD_TIMES_LIMIT = Config.DOWNLOAD_TIMES_LIMIT DOWNLOAD_TIME_GAP_LIMIT = Config.DOWNLOAD_TIME_GAP_LIMIT diff --git a/latest version/core/init.py b/latest version/core/init.py index 9510803..fd4bdb6 100644 --- a/latest version/core/init.py +++ b/latest version/core/init.py @@ -177,6 +177,16 @@ def init(self) -> None: self.table_init() +class DeletedDatabaseInit(DatabaseInit): + def __init__(self, db_path: str = Config.SQLITE_DATABASE_DELETED_PATH) -> None: + super().__init__(db_path) + + def init(self) -> None: + with Connect(self.db_path) as c: + self.c = c + self.table_init() + + class FileChecker: '''文件检查及初始化类''' @@ -195,7 +205,7 @@ def check_folder(self, folder_path: str) -> bool: self.logger.warning('Folder `%s` is missing.' % folder_path) return f - def check_update_database(self) -> bool: + def _check_update_database_log(self) -> bool: if not self.check_file(Config.SQLITE_LOG_DATABASE_PATH): # 新建日志数据库 try: @@ -232,22 +242,22 @@ def check_update_database(self) -> bool: f'Failed to update the file `{Config.SQLITE_LOG_DATABASE_PATH}`') return False - if not self.check_file(Config.SQLITE_DATABASE_PATH): + return True + + def _check_update_database_main(self, db_path=Config.SQLITE_DATABASE_PATH, init_class=DatabaseInit) -> bool: + if not self.check_file(db_path): # 新建数据库 try: - self.logger.info( - 'Try to new the file `%s`.' % Config.SQLITE_DATABASE_PATH) - DatabaseInit().init() - self.logger.info( - 'Success to new the file `%s`.' % Config.SQLITE_DATABASE_PATH) + self.logger.info(f'Try to new the file `{db_path}`.') + init_class().init() + self.logger.info(f'Success to new the file `{db_path}`.') except Exception as e: self.logger.error(format_exc()) - self.logger.warning( - 'Failed to new the file `%s`.' % Config.SQLITE_DATABASE_PATH) + self.logger.warning(f'Failed to new the file `{db_path}`.') return False else: # 检查更新 - with Connect() as c: + with Connect(db_path) as c: try: c.execute('''select value from config where id="version"''') x = c.fetchone() @@ -256,42 +266,45 @@ def check_update_database(self) -> bool: # 数据库自动更新,不强求 if not x or x[0] != ARCAEA_SERVER_VERSION: self.logger.warning( - 'Maybe the file `%s` is an old version.' % Config.SQLITE_DATABASE_PATH) + f'Maybe the file `{db_path}` is an old version. Version: {x[0] if x else "None"}') try: self.logger.info( - 'Try to update the file `%s`.' % Config.SQLITE_DATABASE_PATH) + f'Try to update the file `{db_path}` to version {ARCAEA_SERVER_VERSION}.') if not os.path.isdir(Config.SQLITE_DATABASE_BACKUP_FOLDER_PATH): os.makedirs(Config.SQLITE_DATABASE_BACKUP_FOLDER_PATH) - backup_path = try_rename(Config.SQLITE_DATABASE_PATH, os.path.join( - Config.SQLITE_DATABASE_BACKUP_FOLDER_PATH, os.path.split(Config.SQLITE_DATABASE_PATH)[-1] + '.bak')) + backup_path = try_rename(db_path, os.path.join( + Config.SQLITE_DATABASE_BACKUP_FOLDER_PATH, os.path.split(db_path)[-1] + '.bak')) try: - copy2(backup_path, Config.SQLITE_DATABASE_PATH) + copy2(backup_path, db_path) except: - copy(backup_path, Config.SQLITE_DATABASE_PATH) + copy(backup_path, db_path) temp_path = os.path.join( - *os.path.split(Config.SQLITE_DATABASE_PATH)[:-1], 'old_arcaea_database.db') + *os.path.split(db_path)[:-1], 'old_arcaea_database.db') if os.path.isfile(temp_path): os.remove(temp_path) - try_rename(Config.SQLITE_DATABASE_PATH, temp_path) + try_rename(db_path, temp_path) - DatabaseInit().init() - self.update_database(temp_path) + init_class().init() + self.update_database(temp_path, db_path) self.logger.info( - 'Success to update the file `%s`.' % Config.SQLITE_DATABASE_PATH) + f'Success to update the file `{db_path}`.') except Exception as e: self.logger.error(format_exc()) self.logger.warning( - 'Fail to update the file `%s`.' % Config.SQLITE_DATABASE_PATH) + f'Fail to update the file `{db_path}`.') return True + def check_update_database(self) -> bool: + return self._check_update_database_main() and self._check_update_database_log() and self._check_update_database_main(Config.SQLITE_DATABASE_DELETED_PATH, DeletedDatabaseInit) + @staticmethod def update_database(old_path: str, new_path: str = Config.SQLITE_DATABASE_PATH) -> None: '''更新数据库,并删除旧文件''' diff --git a/latest version/core/operation.py b/latest version/core/operation.py index 830a4ef..3b6006d 100644 --- a/latest version/core/operation.py +++ b/latest version/core/operation.py @@ -1,3 +1,4 @@ +from .constant import Constant from .download import DownloadList from .error import NoData from .save import SaveData @@ -245,3 +246,75 @@ def _all_delete(self): with Connect() as c: c.execute( f'''delete from user_item where type in ({','.join(['?'] * len(self.item_types))})''', self.item_types) + + +def _delete_one_table(c, table_name, user_id): + c.execute( + f'''insert into db_deleted.{table_name} select * from {table_name} where user_id = ?''', (user_id,)) + c.execute(f'''delete from {table_name} where user_id = ?''', (user_id,)) + + +class DeleteUserScore(BaseOperation): + ''' + 删除单用户成绩,不包含 recent 数据 + ''' + _name = 'delete_user_score' + + def __init__(self, user=None): + self.user = user + + def set_params(self, user_id: int = None, *args, **kwargs): + if user_id is not None: + self.user = User() + self.user.user_id = int(user_id) + return self + + def run(self): + assert self.user is not None + with Connect() as c: + c.execute('''attach database ? as db_deleted''', + (Constant.SQLITE_DATABASE_DELETED_PATH,)) + _delete_one_table(c, 'best_score', self.user.user_id) + _delete_one_table(c, 'recent30', self.user.user_id) + + +class DeleteOneUser(BaseOperation): + ''' + 删除单用户 + ''' + _name = 'delete_one_user' + + TABLES = ['best_score', 'recent30', 'user_char', 'user_course', 'user_item', + 'user_present', 'user_redeem', 'user_role', 'user_save', 'user_world', 'user'] + + def __init__(self, user=None): + self.user = user + + def set_params(self, user_id: int = None, *args, **kwargs): + if user_id is not None: + self.user = User() + self.user.user_id = int(user_id) + return self + + def run(self): + assert self.user is not None + with Connect() as c: + c.execute('''attach database ? as db_deleted''', + (Constant.SQLITE_DATABASE_DELETED_PATH,)) + + self._clear_login(c) + self._data_save(c) + + def _data_save(self, c): + c.execute( + f'''insert into db_deleted.friend select * from friend where user_id_me = ? or user_id_other = ?''', (self.user.user_id, self.user.user_id)) + c.execute(f'''delete from friend where user_id_me = ? or user_id_other = ?''', + (self.user.user_id, self.user.user_id)) + + [_delete_one_table(c, x, self.user.user_id) for x in self.TABLES] + + def _clear_login(self, c): + c.execute('''delete from login where user_id = ?''', + (self.user.user_id,)) + c.execute('''delete from api_login where user_id = ?''', + (self.user.user_id,)) diff --git a/latest version/server/user.py b/latest version/server/user.py index 05ffa21..c56f792 100644 --- a/latest version/server/user.py +++ b/latest version/server/user.py @@ -1,8 +1,10 @@ from flask import Blueprint, current_app, request from core.character import UserCharacter +from core.config_manager import Config from core.error import ArcError from core.item import ItemCore +from core.operation import DeleteOneUser from core.save import SaveData from core.sql import Connect from core.user import User, UserLogin, UserOnline, UserRegister @@ -164,7 +166,10 @@ def sys_set(user_id, set_arg): @auth_required(request) @arc_try def user_delete(user_id): - raise ArcError('Cannot delete the account.', 151, status=404) + if not Config.ALLOW_SELF_ACCOUNT_DELETE: + raise ArcError('Cannot delete the account.', 151, status=404) + DeleteOneUser().set_params(user_id).run() + return success_return({'user_id': user_id}) @bp.route('/email/resend_verify', methods=['POST']) # 邮箱验证重发 diff --git a/latest version/web/index.py b/latest version/web/index.py index cce268f..7d5319f 100644 --- a/latest version/web/index.py +++ b/latest version/web/index.py @@ -2,7 +2,7 @@ import time from core.init import FileChecker -from core.operation import RefreshAllScoreRating, RefreshSongFileCache, SaveUpdateScore, UnlockUserItem +from core.operation import RefreshAllScoreRating, RefreshSongFileCache, SaveUpdateScore, UnlockUserItem, DeleteUserScore from core.rank import RankList from core.sql import Connect from core.user import User @@ -1366,7 +1366,10 @@ def delete_user_score(): user_id = c.fetchone() if user_id: user_id = user_id[0] - web.system.clear_user_score(c, user_id) + c.execute('''update user set rating_ptt=0, song_id='', difficulty=0, score=0, shiny_perfect_count=0, perfect_count=0, near_count=0, miss_count=0, health=0, time_played=0, rating=0, world_rank_score=0 where user_id=:a''', { + 'a': user_id}) + c.connection.commit() + DeleteUserScore().set_params(user_id=user_id).run() flash("用户成绩删除成功 Successfully delete the user's scores.") else: diff --git a/latest version/web/system.py b/latest version/web/system.py index 7fc6b3e..26e7e07 100644 --- a/latest version/web/system.py +++ b/latest version/web/system.py @@ -261,12 +261,3 @@ def ban_one_user(c, user_id): {'a': user_id}) c.execute('''delete from login where user_id=:a''', {'a': user_id}) return - - -def clear_user_score(c, user_id): - # 清除用户所有成绩,包括best_score和recent30,以及recent数据,但不包括云端存档 - c.execute('''update user set rating_ptt=0, song_id='', difficulty=0, score=0, shiny_perfect_count=0, perfect_count=0, near_count=0, miss_count=0, health=0, time_played=0, rating=0, world_rank_score=0 where user_id=:a''', - {'a': user_id}) - c.execute('''delete from best_score where user_id=:a''', {'a': user_id}) - c.execute('''delete from recent30 where user_id=:a''', {'a': user_id}) - return From d65cc3bcbefbdf4c373025a725bf16c238d05108 Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Sun, 10 Mar 2024 11:26:21 +0800 Subject: [PATCH 08/32] [Enhance] Missions & ETR - Add support for missions - PTT mechanism: Change first play protection to new best protection - Adapt to the new difficulty ETR - Uncap DORO*C - Incomplete support for "pick_ticket" - Fix requirements: cryptography >= 35.0.0 Note: This is an intermediate test version, only for Arcaea 5.4.0c. Next version will adapt to 5.4.0. --- README.md | 10 +- latest version/api/songs.py | 6 +- latest version/core/config_manager.py | 2 +- latest version/core/constant.py | 2 +- latest version/core/init.py | 5 + latest version/core/item.py | 47 +++- latest version/core/mission.py | 240 ++++++++++++++++++ latest version/core/purchase.py | 16 +- latest version/core/score.py | 9 +- latest version/core/song.py | 9 +- latest version/core/user.py | 12 +- latest version/database/init/arc_data.py | 15 +- latest version/database/init/singles.json | 72 ++++++ latest version/database/init/tables.sql | 15 +- latest version/requirements.txt | 2 +- latest version/server/__init__.py | 4 +- latest version/server/mission.py | 68 +++++ latest version/server/others.py | 45 +++- latest version/static/style.css | 6 + latest version/templates/web/allsong.html | 5 + .../templates/web/changepresent.html | 1 + .../templates/web/changeredeem.html | 1 + latest version/templates/web/changesong.html | 2 + latest version/web/index.py | 23 +- 24 files changed, 554 insertions(+), 63 deletions(-) create mode 100644 latest version/core/mission.py create mode 100644 latest version/server/mission.py diff --git a/README.md b/README.md index cc2a867..b989055 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Arcaea-server -一个微型的Arcaea本地服务器 A small local server for Arcaea +一个微型的 Arcaea 本地服务器 A small local server for Arcaea ## 简介 Introduction -这是基于Python以及Flask的微型本地Arcaea服务器,可以模拟游戏的主要功能。这可能是我第一次写这种大程序,若有不妥之处,敬请谅解。 +这是基于 Python 以及 Flask 的微型本地 Arcaea 服务器,可以模拟游戏的主要功能。这可能是我第一次写这种大程序,若有不妥之处,敬请谅解。 本程序主要用于学习研究,不得用于任何商业行为,否则后果自负,这不是强制要求,只是一个提醒与警告。 @@ -50,12 +50,14 @@ This procedure is mainly used for study and research, and shall not be used for - 下载频次限制 Download rate limit - 购买系统 Purchase system - 单曲和曲包 Single & Pack - - :x: 捆绑包 Bundle + - :x: 捆绑包 Pack bundle - 折扣 Discount - 五周年兑换券 5-th anniversary ticket + - 单曲兑换券 Pick ticket - :x: Extend 包自动降价 Extend pack automatic price reduction - 奖励系统 Present system - 兑换码系统 Redeem code system +- 新手任务 Missions - 角色系统 Character system - 数据记录 Data recording - 用户成绩 Users' scores @@ -117,7 +119,7 @@ It is just so interesting. What it can do is under exploration. - Windows / Linux / Mac OS / Android... - Python >= 3.6 - Flask >= 2.0 - - Cryptography >= 3.0.0 + - Cryptography >= 35.0.0 - limits >= 2.7.0 - Charles, IDA, proxy app... (optional) diff --git a/latest version/api/songs.py b/latest version/api/songs.py index 23b2856..cab1ec3 100644 --- a/latest version/api/songs.py +++ b/latest version/api/songs.py @@ -63,7 +63,7 @@ def songs_get(data, user): '''查询全歌曲信息''' A = ['song_id', 'name'] B = ['song_id', 'name', 'rating_pst', - 'rating_prs', 'rating_ftr', 'rating_byn'] + 'rating_prs', 'rating_ftr', 'rating_byn', 'rating_etr'] with Connect() as c: query = Query(A, A, B).from_dict(data) x = Sql(c).select('chart', query=query) @@ -97,8 +97,8 @@ def songs_post(data, user): @api_try def songs_song_difficulty_rank_get(data, user, song_id, difficulty): '''查询歌曲某个难度的成绩排行榜,和游戏内接口相似,只允许limit''' - if difficulty not in [0, 1, 2, 3]: - raise InputError('Difficulty must be 0, 1, 2 or 3') + if difficulty not in [0, 1, 2, 3, 4]: + raise InputError('Difficulty must be 0, 1, 2, 3 or 4') limit = data.get('limit', 20) if not isinstance(limit, int): raise InputError('Limit must be int') diff --git a/latest version/core/config_manager.py b/latest version/core/config_manager.py index 5a858b4..6e82c28 100644 --- a/latest version/core/config_manager.py +++ b/latest version/core/config_manager.py @@ -12,7 +12,7 @@ class Config: SONG_FILE_HASH_PRE_CALCULATE = True - GAME_API_PREFIX = '/samusugiru/26' # str | list[str] + GAME_API_PREFIX = '/saikyoukaze/27' # str | list[str] OLD_GAME_API_PREFIX = [] # str | list[str] ALLOW_APPVERSION = [] # list[str] diff --git a/latest version/core/constant.py b/latest version/core/constant.py index 392b32a..0d1caf4 100644 --- a/latest version/core/constant.py +++ b/latest version/core/constant.py @@ -1,6 +1,6 @@ from .config_manager import Config -ARCAEA_SERVER_VERSION = 'v2.11.3.4' +ARCAEA_SERVER_VERSION = 'v2.11.3.5' ARCAEA_LOG_DATBASE_VERSION = 'v1.1' diff --git a/latest version/core/init.py b/latest version/core/init.py index fd4bdb6..efef5a5 100644 --- a/latest version/core/init.py +++ b/latest version/core/init.py @@ -92,6 +92,8 @@ def item_init(self) -> None: ('memory', 'memory', 1)) self.c.execute('''insert into item values(?,?,?)''', ('anni5tix', 'anni5tix', 1)) + self.c.execute('''insert into item values(?,?,?)''', + ('pick_ticket', 'pick_ticket', 1)) with open(self.pack_path, 'rb') as f: self.insert_purchase_item(load(f)) @@ -99,6 +101,9 @@ def item_init(self) -> None: with open(self.single_path, 'rb') as f: self.insert_purchase_item(load(f)) + self.c.execute('''insert into item values(?,?,?)''', # 新手任务奖励曲 + ('innocence', 'world_song', 1)) + def course_init(self) -> None: '''初始化课题信息''' courses = [] diff --git a/latest version/core/item.py b/latest version/core/item.py index 2b83413..fcf5bd9 100644 --- a/latest version/core/item.py +++ b/latest version/core/item.py @@ -89,7 +89,7 @@ def select_user_item(self, user=None): class NormalItem(UserItem): - def __init__(self, c) -> None: + def __init__(self, c=None) -> None: super().__init__() self.c = c @@ -115,7 +115,7 @@ def user_claim_item(self, user): class PositiveItem(UserItem): - def __init__(self, c) -> None: + def __init__(self, c=None) -> None: super().__init__() self.c = c @@ -142,7 +142,7 @@ def user_claim_item(self, user): class ItemCore(PositiveItem): item_type = 'core' - def __init__(self, c, core_type: str = '', amount: int = 0) -> None: + def __init__(self, c=None, core_type: str = '', amount: int = 0) -> None: super().__init__(c) self.is_available = True self.item_id = core_type @@ -220,10 +220,12 @@ def user_claim_item(self, user): class Fragment(UserItem): item_type = 'fragment' - def __init__(self, c) -> None: + def __init__(self, c=None, amount=0) -> None: super().__init__() self.c = c self.is_available = True + self.item_id = self.item_type + self.amount = amount def user_claim_item(self, user): pass @@ -238,12 +240,24 @@ class Anni5tix(PositiveItem): def __init__(self, c) -> None: super().__init__(c) self.is_available = True + self.item_id = self.item_type + self.amount = 1 + + +class PickTicket(PositiveItem): + item_type = 'pick_ticket' + + def __init__(self, c=None) -> None: + super().__init__(c) + self.is_available = True + self.item_id = self.item_type + self.amount = 1 class WorldSong(NormalItem): item_type = 'world_song' - def __init__(self, c) -> None: + def __init__(self, c=None) -> None: super().__init__(c) self.is_available = True @@ -293,8 +307,10 @@ def user_claim_item(self, user): class Stamina6(UserItem): item_type = 'stamina6' - def __init__(self, c) -> None: + def __init__(self, c=None) -> None: super().__init__(c) + self.item_id = 'stamina6' + self.amount = 1 def user_claim_item(self, user): ''' @@ -307,6 +323,23 @@ def user_claim_item(self, user): user.update_user_one_column('world_mode_locked_end_ts', -1) +class ItemStamina(UserItem): + item_type = 'stamina' + + def __init__(self, c=None, amount=1) -> None: + super().__init__(c) + self.item_id = 'stamina' + self.amount = amount + + def user_claim_item(self, user): + ''' + 新手任务奖励体力 + ''' + user.select_user_about_stamina() + user.stamina.stamina += self.amount + user.stamina.update() + + class ItemFactory: def __init__(self, c=None) -> None: self.c = c @@ -324,6 +357,8 @@ def get_item(self, item_type: str): return Memory(self.c) elif item_type == 'anni5tix': return Anni5tix(self.c) + elif item_type == 'pick_ticket': + return PickTicket(self.c) elif item_type == 'world_song': return WorldSong(self.c) elif item_type == 'world_unlock': diff --git a/latest version/core/mission.py b/latest version/core/mission.py new file mode 100644 index 0000000..726ca0f --- /dev/null +++ b/latest version/core/mission.py @@ -0,0 +1,240 @@ +from .item import Fragment, ItemCore, ItemStamina, PickTicket, WorldSong + + +class Mission: + mission_id: str = None + items: list = [] + + def __init__(self, c=None): + self.c = c + self.user = None + self._status: int = None + + if self.c is not None: + for i in self.items: + i.c = self.c + + def to_dict(self, has_items=False) -> dict: + r = { + 'mission_id': self.mission_id, + 'status': self.status, + } + if has_items: + r['items'] = [x.to_dict() for x in self.items] + return r + + @property + def status(self) -> str: + if self._status == 1: + return 'inprogress' + elif self._status == 2: + return 'cleared' + elif self._status == 3: + return 'prevclaimedfragmission' + elif self._status == 4: + return 'claimed' + + return 'locked' + + def user_claim_mission(self, user): + # param: user - User 类或子类的实例 + if user is not None: + self.user = user + + self.c.execute('''insert or replace into user_mission (user_id, mission_id, status) values (?, ?, 4)''', + (self.user.user_id, self.mission_id)) + for i in self.items: + i.user_claim_item(self.user) + self._status = 4 + + def user_clear_mission(self, user): + # param: user - User 类或子类的实例 + if user is not None: + self.user = user + + self.c.execute('''insert or replace into user_mission (user_id, mission_id, status) values (?, ?, 2)''', + (self.user.user_id, self.mission_id)) + self._status = 2 + + def select_user_mission(self, user): + # param: user - User 类或子类的实例 + if user is not None: + self.user = user + + self._status = 0 + self.c.execute('''select status from user_mission where user_id=? and mission_id=?''', + (self.user.user_id, self.mission_id)) + x = self.c.fetchone() + + if x and x[0]: + self._status = x[0] + + +class M11(Mission): + mission_id = 'mission_1_1_tutorial' + items = [Fragment(amount=10)] + + +class M12(Mission): + mission_id = 'mission_1_2_clearsong' + items = [Fragment(amount=10)] + + +class M13(Mission): + mission_id = 'mission_1_3_settings' + items = [Fragment(amount=10)] + + +class M14(Mission): + mission_id = 'mission_1_4_allsongsview' + items = [Fragment(amount=10)] + + +class M15(Mission): + mission_id = 'mission_1_5_fragunlock' + items = [ItemCore(core_type='core_generic', amount=1)] + + +class M1E(Mission): + mission_id = 'mission_1_end' + items = [Fragment(amount=100)] + + +class M21(Mission): + mission_id = 'mission_2_1_account' + items = [Fragment(amount=20)] + + +class M22(Mission): + mission_id = 'mission_2_2_profile' + items = [Fragment(amount=20)] + + +class M23(Mission): + mission_id = 'mission_2_3_partner' + items = [Fragment(amount=20)] + + +class M24(Mission): + mission_id = 'mission_2_4_usestamina' + items = [ItemCore(core_type='core_generic', amount=1)] + + +class M25(Mission): + mission_id = 'mission_2_5_prologuestart' + items = [ItemCore(core_type='core_generic', amount=1)] + + +class M2E(Mission): + mission_id = 'mission_2_end' + items = [ItemCore(core_type='core_generic', amount=3)] + + +class M31(Mission): + mission_id = 'mission_3_1_prsclear' + items = [Fragment(amount=50)] + + +class M32(Mission): + mission_id = 'mission_3_2_etherdrop' + items = [ItemStamina(amount=2)] + + +class M33(Mission): + mission_id = 'mission_3_3_step50' + items = [Fragment(amount=50)] + + +class M34(Mission): + mission_id = 'mission_3_4_frag60' + items = [ItemStamina(amount=2)] + + +class M3E(Mission): + mission_id = 'mission_3_end' + items = [ItemStamina(amount=6)] + + +class M41(Mission): + mission_id = 'mission_4_1_exgrade' + items = [Fragment(amount=100)] + + +class M42(Mission): + mission_id = 'mission_4_2_potential350' + items = [ItemStamina(amount=2)] + + +class M43(Mission): + mission_id = 'mission_4_3_twomaps' + items = [Fragment(amount=100)] + + +class M44(Mission): + mission_id = 'mission_4_4_worldsongunlock' + items = [ItemCore(core_type='core_generic', amount=3)] + + +class M45(Mission): + mission_id = 'mission_4_5_prologuefinish' + items = [ItemStamina(amount=2)] + + +_innocence = WorldSong() +_innocence.amount = 1 +_innocence.item_id = 'innocence' + + +class M4E(Mission): + mission_id = 'mission_4_end' + items = [_innocence] + + +class M51(Mission): + mission_id = 'mission_5_1_songgrouping' + items = [Fragment(amount=50)] + + +class M52(Mission): + mission_id = 'mission_5_2_partnerlv12' + items = [Fragment(amount=250)] + + +class M53(Mission): + mission_id = 'mission_5_3_cores' + items = [ItemCore(core_type='core_generic', amount=3)] + + +class M54(Mission): + mission_id = 'mission_5_4_courseclear' + items = [ItemCore(core_type='core_generic', amount=3)] + + +class M5E(Mission): + mission_id = 'mission_5_end' + items = [PickTicket()] + + +MISSION_DICT = {i.mission_id: i for i in Mission.__subclasses__()} + + +class UserMissionList: + def __init__(self, c=None, user=None): + self.c = c + self.user = user + + self.missions: list = [] + + def select_all(self): + self.missions = [] + self.c.execute('''select mission_id, status from user_mission where user_id=?''', + (self.user.user_id,)) + for i in self.c.fetchall(): + x = MISSION_DICT[i[0]]() + x._status = i[1] + self.missions.append(x) + + return self + + def to_dict_list(self) -> list: + return [i.to_dict() for i in self.missions] diff --git a/latest version/core/purchase.py b/latest version/core/purchase.py index a3e5075..bad66bb 100644 --- a/latest version/core/purchase.py +++ b/latest version/core/purchase.py @@ -29,7 +29,7 @@ def __init__(self, c=None, user=None): self.items: list = [] - # TODO: "discount_reason": "extend" + # TODO: "discount_reason": extend, sale @property def price_displayed(self) -> int: @@ -44,6 +44,12 @@ def price_displayed(self) -> int: x.select_user_item(self.user) if x.amount >= 1: return 0 + elif self.discount_reason == 'pick_ticket': + x = ItemFactory(self.c).get_item('pick_ticket') + x.item_id = 'pick_ticket' + x.select_user_item(self.user) + if x.amount >= 1: + return 0 return self.price return self.orig_price @@ -60,7 +66,7 @@ def to_dict(self, has_items: bool = True, show_real_price: bool = True) -> dict: if self.discount_from > 0 and self.discount_to > 0: r['discount_from'] = self.discount_from r['discount_to'] = self.discount_to - if not show_real_price or (self.discount_reason == 'anni5tix' and price == 0): + if not show_real_price or (self.discount_reason in ('anni5tix', 'pick_ticket') and price == 0): r['discount_reason'] = self.discount_reason return r @@ -186,10 +192,10 @@ def buy(self) -> None: raise TicketNotEnough( 'The user does not have enough memories.', -6) - if not(self.orig_price == 0 or self.price == 0 and self.discount_from <= int(time() * 1000) <= self.discount_to): + if not (self.orig_price == 0 or self.price == 0 and self.discount_from <= int(time() * 1000) <= self.discount_to): if price_used == 0: - x = ItemFactory(self.c).get_item('anni5tix') - x.item_id = 'anni5tix' + x = ItemFactory(self.c).get_item(self.discount_reason) + x.item_id = self.discount_reason x.amount = -1 x.user_claim_item(self.user) else: diff --git a/latest version/core/score.py b/latest version/core/score.py index 21f23e9..0ffe269 100644 --- a/latest version/core/score.py +++ b/latest version/core/score.py @@ -207,7 +207,7 @@ def __init__(self, c=None, user=None) -> None: self.submission_hash: str = None self.beyond_gauge: int = None self.unrank_flag: bool = None - self.first_protect_flag: bool = None + self.new_best_protect_flag: bool = None self.ptt: 'Potential' = None self.is_world_mode: bool = None @@ -245,7 +245,7 @@ def to_dict(self) -> dict: @property def is_protected(self) -> bool: - return self.health == -1 or int(self.score) >= 9800000 or self.first_protect_flag + return self.health == -1 or int(self.score) >= 9800000 or self.new_best_protect_flag @property def is_valid(self) -> bool: @@ -473,16 +473,17 @@ def upload_score(self) -> None: 'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty}) x = self.c.fetchone() if not x: - self.first_protect_flag = True # 初见保护 + self.new_best_protect_flag = True # 初见保护 self.c.execute('''insert into best_score values(:a,:b,:c,:d,:e,:f,:g,:h,:i,:j,:k,:l,:m,:n)''', { 'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.score, 'e': self.shiny_perfect_count, 'f': self.perfect_count, 'g': self.near_count, 'h': self.miss_count, 'i': self.health, 'j': self.modifier, 'k': self.time_played, 'l': self.clear_type, 'm': self.clear_type, 'n': self.rating}) self.user.update_global_rank() else: - self.first_protect_flag = False + self.new_best_protect_flag = False if self.song_state > self.get_song_state(int(x[1])): # best状态更新 self.c.execute('''update best_score set best_clear_type = :a where user_id = :b and song_id = :c and difficulty = :d''', { 'a': self.clear_type, 'b': self.user.user_id, 'c': self.song.song_id, 'd': self.song.difficulty}) if self.score >= int(x[0]): # best成绩更新 + self.new_best_protect_flag = True self.c.execute('''update best_score set score = :d, shiny_perfect_count = :e, perfect_count = :f, near_count = :g, miss_count = :h, health = :i, modifier = :j, clear_type = :k, rating = :l, time_played = :m where user_id = :a and song_id = :b and difficulty = :c ''', { 'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.score, 'e': self.shiny_perfect_count, 'f': self.perfect_count, 'g': self.near_count, 'h': self.miss_count, 'i': self.health, 'j': self.modifier, 'k': self.clear_type, 'l': self.rating, 'm': self.time_played}) self.user.update_global_rank() diff --git a/latest version/core/song.py b/latest version/core/song.py index 2b391ea..1d82cf3 100644 --- a/latest version/core/song.py +++ b/latest version/core/song.py @@ -33,7 +33,7 @@ def set_chart(self, song_id: str = None, difficulty: int = None) -> None: def select(self) -> None: self.c.execute( - '''select rating_pst, rating_prs, rating_ftr, rating_byn from chart where song_id=:a''', {'a': self.song_id}) + '''select rating_pst, rating_prs, rating_ftr, rating_byn, rating_etr from chart where song_id=:a''', {'a': self.song_id}) x = self.c.fetchone() if x is None: if Config.ALLOW_SCORE_WITH_NO_SONG: @@ -63,11 +63,12 @@ def from_list(self, x: list) -> 'Song': self.song_id = x[0] self.name = x[1] self.charts = [Chart(self.c, self.song_id, 0), Chart(self.c, self.song_id, 1), Chart( - self.c, self.song_id, 2), Chart(self.c, self.song_id, 3)] + self.c, self.song_id, 2), Chart(self.c, self.song_id, 3), Chart(self.c, self.song_id, 4)] self.charts[0].defnum = x[2] self.charts[1].defnum = x[3] self.charts[2].defnum = x[4] self.charts[3].defnum = x[5] + self.charts[4].defnum = x[6] return self def from_dict(self, d: dict) -> 'Song': @@ -89,11 +90,11 @@ def delete(self) -> None: def update(self) -> None: '''全部更新''' self.c.execute( - '''update chart set name=?, rating_pst=?, rating_prs=?, rating_ftr=?, rating_byn=? where song_id=?''', (self.name, self.charts[0].defnum, self.charts[1].defnum, self.charts[2].defnum, self.charts[3].defnum, self.song_id)) + '''update chart set name=?, rating_pst=?, rating_prs=?, rating_ftr=?, rating_byn=?, rating_etr=? where song_id=?''', (self.name, self.charts[0].defnum, self.charts[1].defnum, self.charts[2].defnum, self.charts[3].defnum, self.charts[4].defnum, self.song_id)) def insert(self) -> None: self.c.execute( - '''insert into chart values (?,?,?,?,?,?)''', (self.song_id, self.name, self.charts[0].defnum, self.charts[1].defnum, self.charts[2].defnum, self.charts[3].defnum)) + '''insert into chart values (?,?,?,?,?,?,?)''', (self.song_id, self.name, self.charts[0].defnum, self.charts[1].defnum, self.charts[2].defnum, self.charts[3].defnum, self.charts[4].defnum)) def select_exists(self, song_id: str = None) -> bool: if song_id is not None: diff --git a/latest version/core/user.py b/latest version/core/user.py index 12f712f..7b93d25 100644 --- a/latest version/core/user.py +++ b/latest version/core/user.py @@ -11,6 +11,7 @@ NoData, RateLimit, UserBan) from .item import UserItemList from .limiter import ArcLimiter +from .mission import UserMissionList from .score import Score from .sql import Query, Sql from .world import Map, UserMap, UserStamina @@ -349,6 +350,13 @@ def packs(self) -> list: return self.__packs + @property + def pick_ticket(self) -> int: + x = UserItemList(self.c, self).select_from_type('pick_ticket') + if not x.items: + return 0 + return x.items[0].amount + @property def world_unlocks(self) -> list: if self.__world_unlocks is None: @@ -520,7 +528,9 @@ def to_dict(self) -> dict: 'country': '', 'course_banners': self.course_banners, 'world_mode_locked_end_ts': self.world_mode_locked_end_ts, - 'locked_char_ids': [] # [1] + 'locked_char_ids': [], # [1] + 'user_missions': UserMissionList(self.c, self).select_all().to_dict_list(), + 'pick_ticket': self.pick_ticket } def from_list(self, x: list) -> 'UserInfo': diff --git a/latest version/database/init/arc_data.py b/latest version/database/init/arc_data.py index df8d1b5..8efea5f 100644 --- a/latest version/database/init/arc_data.py +++ b/latest version/database/init/arc_data.py @@ -6,7 +6,7 @@ class InitData: 'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', '', '', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', '', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', '', '', 'visual_ghost_skynotes', 'skill_vita', 'skill_fatalis', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', '', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'skill_mithra', 'skill_toa', 'skill_nami_twilight', 'skill_ilith_ivy', 'skill_hikari_vanessa', 'skill_maya', 'skill_intruder', 'skill_luin'] skill_id_uncap = ['', '', 'frags_kou', '', 'visual_ink', '', '', '', '', '', 'ilith_awakened_skill', 'eto_uncap', 'luna_uncap', 'shirabe_entry_fee', - '', '', '', '', '', 'ayu_uncap', '', 'frags_yume', '', '', '', '', '', '', '', '', 'skill_kanae_uncap', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'skill_luin_uncap'] + '', '', '', '', '', 'ayu_uncap', '', 'frags_yume', '', '', '', '', '', '', '', '', 'skill_kanae_uncap', '', '', '', 'skill_doroc_uncap', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'skill_luin_uncap'] skill_unlock_level = [0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 14, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] @@ -30,13 +30,13 @@ class InitData: 46, 73, 95, 67, 84, 70, 78, 69, 70, 50, 80, 80, 63, 25, 50, 72, 55, 50, 95, 55, 70, 90, 70, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 54, 65.5, 59.5, 58, 96, 47, 50, 54] frag30 = [88, 90, 100, 75, 80, 89, 70, 79, 65, 40, 50, 90, 100, 92, 0, 61, 67, 92, 85, 50, 86, 62, - 65, 85, 67, 88, 74, 0.5, 105, 80, 105, 50, 80, 87, 71, 50, 95, 0, 80, 75, 50, 70, 80, 100, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 90, 80, 50, 51, 64, 100, 50, 58, 51, 40, 50, 80] + 65, 85, 67, 88, 74, 0.5, 105, 80, 105, 50, 80, 87, 81, 50, 95, 0, 80, 75, 50, 70, 80, 100, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 90, 80, 50, 51, 64, 100, 50, 58, 51, 40, 50, 80] prog30 = [71, 90, 80, 75, 100, 80, 90, 102, 84, 78, 110, 77, 73, 78, 0, 99, 80, 66, 46, 93, 40, 83, - 80, 90, 93, 50, 96, 88, 99, 108, 85, 80, 50, 64, 55, 100, 100, 110, 80, 50, 74, 90, 80, 80, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51, 60, 53, 85, 58, 96, 47, 50, 90] + 80, 90, 93, 50, 96, 88, 99, 108, 85, 80, 50, 64, 65, 100, 100, 110, 80, 50, 74, 90, 80, 80, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51, 60, 53, 85, 58, 96, 47, 50, 90] overdrive30 = [71, 90, 57, 75, 80, 80, 95, 79, 65, 31, 50, 69, 100, 68, 0, 78, 50, 70, 62, 59, 64, - 56, 73, 95, 67, 84, 80, 88, 79, 80, 60, 80, 80, 63, 25, 50, 82, 55, 50, 95, 55, 70, 100, 80, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 64, 65.5, 59.5, 58, 96, 47, 50, 64] + 56, 73, 95, 67, 84, 80, 88, 79, 80, 60, 80, 80, 63, 35, 50, 82, 55, 50, 95, 55, 70, 100, 80, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 64, 65.5, 59.5, 58, 96, 47, 50, 64] char_type = [1, 0, 0, 0, 0, 0, 0, 2, 0, 1, 2, 0, 0, 0, 2, 3, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 3, 0, 2, 2, 0, 0, 2, 0, 0, 2, 0, 2, 2, 1, 0, 2, 0, 0, 2] @@ -62,14 +62,15 @@ class InitData: 66: [{'core_id': 'core_chunithm', 'amount': 15}], 5: [{'core_id': 'core_hollow', 'amount': 0}], 73: [{'core_id': 'core_wacca', 'amount': 15}], - 30: [{'core_id': 'core_hollow', 'amount': 5}, {'core_id': 'core_sunset', 'amount': 25}] + 30: [{'core_id': 'core_hollow', 'amount': 5}, {'core_id': 'core_sunset', 'amount': 25}], + 34: [{'core_id': 'core_tanoc', 'amount': 15}], } cores = ['core_hollow', 'core_desolate', 'core_chunithm', 'core_crimson', - 'core_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful', 'core_course_skip_purchase', 'core_umbral', 'core_wacca', 'core_sunset'] + 'core_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful', 'core_course_skip_purchase', 'core_umbral', 'core_wacca', 'core_sunset', 'core_tanoc'] world_songs = ["babaroque", "shadesoflight", "kanagawa", "lucifer", "anokumene", "ignotus", "rabbitintheblackroom", "qualia", "redandblue", "bookmaker", "darakunosono", "espebranch", "blacklotus", "givemeanightmare", "vividtheory", "onefr", "gekka", "vexaria3", "infinityheaven3", "fairytale3", "goodtek3", "suomi", "rugie", "faintlight", "harutopia", "goodtek", "dreaminattraction", "syro", "diode", "freefall", "grimheart", "blaster", - "cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris', 'ultimatetaste', 'rgb', 'matenrou', 'dynitikos', 'amekagura', 'fantasy', 'aloneandlorn', 'felys', 'onandon', 'hotarubinoyuki', 'oblivia3', 'libertas3', 'einherjar3', 'purpleverse3', 'viciousheroism3', 'inkarusi3', 'cyberneciacatharsis3'] + "cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris', 'ultimatetaste', 'rgb', 'matenrou', 'dynitikos', 'amekagura', 'fantasy', 'aloneandlorn', 'felys', 'onandon', 'hotarubinoyuki', 'oblivia3', 'libertas3', 'einherjar3', 'purpleverse3', 'viciousheroism3', 'inkarusi3', 'cyberneciacatharsis3', 'alephzero'] world_unlocks = ["scenery_chap1", "scenery_chap2", "scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7", "scenery_beyond"] diff --git a/latest version/database/init/singles.json b/latest version/database/init/singles.json index 1215d4c..4f51364 100644 --- a/latest version/database/init/singles.json +++ b/latest version/database/init/singles.json @@ -1612,5 +1612,77 @@ ], "orig_price": 100, "price": 100 + }, + { + "name": "ionostream", + "items": [ + { + "type": "single", + "id": "ionostream", + "is_available": true + }, + { + "type": "core", + "amount": 1, + "id": "core_generic", + "is_available": true + } + ], + "orig_price": 100, + "price": 100 + }, + { + "name": "masqueradelegion", + "items": [ + { + "type": "single", + "id": "masqueradelegion", + "is_available": true + }, + { + "type": "core", + "amount": 1, + "id": "core_generic", + "is_available": true + } + ], + "orig_price": 100, + "price": 100 + }, + { + "name": "kyorenromance", + "items": [ + { + "type": "single", + "id": "kyorenromance", + "is_available": true + }, + { + "type": "core", + "amount": 1, + "id": "core_generic", + "is_available": true + } + ], + "orig_price": 100, + "price": 100 + }, + { + "name": "qovat", + "items": [ + { + "type": "single", + "id": "qovat", + "is_available": true + }, + { + "type": "core", + "amount": 1, + "id": "core_generic", + "is_available": true + } + ], + "orig_price": 100, + "price": 100 } ] \ No newline at end of file diff --git a/latest version/database/init/tables.sql b/latest version/database/init/tables.sql index 0a6d3c7..cbabb07 100644 --- a/latest version/database/init/tables.sql +++ b/latest version/database/init/tables.sql @@ -241,10 +241,11 @@ primary key(present_id, item_id, type) ); create table if not exists chart(song_id text primary key, name text, -rating_pst int, -rating_prs int, -rating_ftr int, -rating_byn int +rating_pst int default -1, +rating_prs int default -1, +rating_ftr int default -1, +rating_byn int default -1, +rating_etr int default -1 ); create table if not exists redeem(code text primary key, type int @@ -311,6 +312,12 @@ type text, amount int, primary key(course_id, item_id, type) ); +create table if not exists user_mission( +user_id int, +mission_id text, +status int, +primary key(user_id, mission_id) +); create index if not exists best_score_1 on best_score (song_id, difficulty); diff --git a/latest version/requirements.txt b/latest version/requirements.txt index 4e18428..fb79834 100644 --- a/latest version/requirements.txt +++ b/latest version/requirements.txt @@ -1,3 +1,3 @@ flask>=2.0.2 -cryptography>=3.0.0 +cryptography>=35.0.0 limits>=2.7.0 diff --git a/latest version/server/__init__.py b/latest version/server/__init__.py index df33514..28acf22 100644 --- a/latest version/server/__init__.py +++ b/latest version/server/__init__.py @@ -3,7 +3,7 @@ from core.config_manager import Config from . import (auth, course, friend, multiplayer, others, present, purchase, - score, user, world) + score, user, world, mission) __bp_old = Blueprint('old_server', __name__) @@ -24,7 +24,7 @@ def string_to_list(s): bp = Blueprint('server', __name__) list(map(bp.register_blueprint, [user.bp, auth.bp, friend.bp, score.bp, - world.bp, purchase.bp, present.bp, others.bp, multiplayer.bp, course.bp])) + world.bp, purchase.bp, present.bp, others.bp, multiplayer.bp, course.bp, mission.bp])) bps = [Blueprint(x, __name__, url_prefix=x) for x in string_to_list(Config.GAME_API_PREFIX)] diff --git a/latest version/server/mission.py b/latest version/server/mission.py new file mode 100644 index 0000000..da5e8dc --- /dev/null +++ b/latest version/server/mission.py @@ -0,0 +1,68 @@ +from flask import Blueprint, request + +from core.error import NoData +from core.mission import MISSION_DICT +from core.sql import Connect +from core.user import UserOnline + +from .auth import auth_required +from .func import arc_try, success_return + +bp = Blueprint('mission', __name__, url_prefix='/mission') + + +def parse_mission_form(multidict) -> list: + r = [] + + x = multidict.get('mission_1') + i = 1 + while x: + r.append(x) + x = multidict.get(f'mission_{i + 1}') + i += 1 + return r + + +@bp.route('/me/clear', methods=['POST']) # 新手任务确认完成 +@auth_required(request) +@arc_try +def mission_clear(user_id): + m = parse_mission_form(request.form) + r = [] + for i, mission_id in enumerate(m): + if mission_id not in MISSION_DICT: + return NoData(f'Mission `{mission_id}` not found') + with Connect() as c: + x = MISSION_DICT[mission_id](c) + x.user_clear_mission(UserOnline(c, user_id)) + d = x.to_dict() + d['request_id'] = i + 1 + r.append(d) + + return success_return({'missions': r}) + + +@bp.route('/me/claim', methods=['POST']) # 领取新手任务奖励 +@auth_required(request) +@arc_try +def mission_claim(user_id): + m = parse_mission_form(request.form) + r = [] + + with Connect() as c: + user = UserOnline(c, user_id) + + for i, mission_id in enumerate(m): + if mission_id not in MISSION_DICT: + return NoData(f'Mission `{mission_id}` not found') + + x = MISSION_DICT[mission_id](c) + x.user_claim_mission(user) + d = x.to_dict(has_items=True) + d['request_id'] = i + 1 + r.append(d) + + return success_return({ + 'missions': r, + 'user': user.to_dict() + }) diff --git a/latest version/server/others.py b/latest version/server/others.py index 58d1d47..27aac73 100644 --- a/latest version/server/others.py +++ b/latest version/server/others.py @@ -13,7 +13,7 @@ from .auth import auth_required from .func import arc_try, error_return, success_return from .present import present_info -from .purchase import bundle_bundle, bundle_pack +from .purchase import bundle_bundle, get_single, bundle_pack from .score import song_score_friend from .user import user_me from .world import world_all @@ -26,6 +26,28 @@ def game_info(): return success_return(GameInfo().to_dict()) +# @bp.route('/game/content_bundle', methods=['GET']) # 热更新 +# def game_content_bundle(): +# app_version = request.headers.get('AppVersion') +# bundle_version = request.headers.get('ContentBundle') +# import os +# if bundle_version != '5.4.0': +# r = {'orderedResults': [ +# { +# 'appVersion': '5.4.0', +# 'contentBundleVersion': '5.4.0', +# 'jsonUrl': 'http://192.168.0.110/bundle_download/bundle.json', +# 'jsonSize': os.path.getsize('./database/bundle/bundle.json'), +# 'bundleUrl': 'http://192.168.0.110/bundle_download/bundle', +# 'bundleSize': os.path.getsize('./database/bundle/bundle') +# }, +# ] +# } +# else: +# r = {} +# return success_return(r) + + @bp.route('/serve/download/me/song', methods=['GET']) # 歌曲下载 @auth_required(request) @arc_try @@ -66,15 +88,18 @@ def applog_me(): return success_return({}) -map_dict = {'/user/me': user_me, - '/purchase/bundle/pack': bundle_pack, - '/serve/download/me/song': download_song, - '/game/info': game_info, - '/present/me': present_info, - '/world/map/me': world_all, - '/score/song/friend': song_score_friend, - '/purchase/bundle/bundle': bundle_bundle, - '/finale/progress': finale_progress} +map_dict = { + '/user/me': user_me, + '/purchase/bundle/pack': bundle_pack, + '/serve/download/me/song': download_song, + '/game/info': game_info, + '/present/me': present_info, + '/world/map/me': world_all, + '/score/song/friend': song_score_friend, + '/purchase/bundle/bundle': bundle_bundle, + '/finale/progress': finale_progress, + '/purchase/bundle/single': get_single +} @bp.route('/compose/aggregate', methods=['GET']) # 集成式请求 diff --git a/latest version/static/style.css b/latest version/static/style.css index 0aec47b..6678106 100644 --- a/latest version/static/style.css +++ b/latest version/static/style.css @@ -173,6 +173,12 @@ input[type=submit] { color: white; } +.difficulty_etr { + font-size: 0.9em; + background-color: rgb(161, 132, 181); + color: white; +} + .rank { font-size: 0.8em; margin-left: 4px; diff --git a/latest version/templates/web/allsong.html b/latest version/templates/web/allsong.html index 654a66f..0bb6bfe 100644 --- a/latest version/templates/web/allsong.html +++ b/latest version/templates/web/allsong.html @@ -34,6 +34,11 @@

{% block title %}All songs{% endblock %}

BYD {{song['rating_byn']}} {% endif %} +
+ {% if song['rating_etr'] %} + ETR + {{song['rating_etr']}} + {% endif %} diff --git a/latest version/templates/web/changepresent.html b/latest version/templates/web/changepresent.html index b6aac74..740dc6d 100644 --- a/latest version/templates/web/changepresent.html +++ b/latest version/templates/web/changepresent.html @@ -28,6 +28,7 @@

{% block title %}Change the presents{% endblock %}

+ diff --git a/latest version/templates/web/changeredeem.html b/latest version/templates/web/changeredeem.html index b646312..19f5583 100644 --- a/latest version/templates/web/changeredeem.html +++ b/latest version/templates/web/changeredeem.html @@ -39,6 +39,7 @@

{% block title %}Change the redeem codes{% endblock %}

+ diff --git a/latest version/templates/web/changesong.html b/latest version/templates/web/changesong.html index cd06a7f..09893a3 100644 --- a/latest version/templates/web/changesong.html +++ b/latest version/templates/web/changesong.html @@ -19,6 +19,8 @@

{% block title %}Change the songs{% endblock %}

+ +
如果没有某个谱面,应该填入-1。
If there is no some chart, fill in -1 please.
diff --git a/latest version/web/index.py b/latest version/web/index.py index 7d5319f..456a063 100644 --- a/latest version/web/index.py +++ b/latest version/web/index.py @@ -191,13 +191,15 @@ def defnum(x): if x: posts = [] for i in x: - posts.append({'song_id': i[0], - 'name_en': i[1], - 'rating_pst': defnum(i[2]), - 'rating_prs': defnum(i[3]), - 'rating_ftr': defnum(i[4]), - 'rating_byn': defnum(i[5]) - }) + posts.append({ + 'song_id': i[0], + 'name_en': i[1], + 'rating_pst': defnum(i[2]), + 'rating_prs': defnum(i[3]), + 'rating_ftr': defnum(i[4]), + 'rating_byn': defnum(i[5]), + 'rating_etr': defnum(i[6]) + }) else: error = '没有谱面数据 No song data.' @@ -335,6 +337,7 @@ def get_rating(x): rating_prs = get_rating(request.form['rating_prs']) rating_ftr = get_rating(request.form['rating_ftr']) rating_byd = get_rating(request.form['rating_byd']) + rating_etr = get_rating(request.form['rating_etr']) if len(song_id) >= 256: song_id = song_id[:200] if len(name_en) >= 256: @@ -344,8 +347,8 @@ def get_rating(x): c.execute( '''select exists(select * from chart where song_id=:a)''', {'a': song_id}) if c.fetchone() == (0,): - c.execute('''insert into chart values(:a,:b,:c,:d,:e,:f)''', { - 'a': song_id, 'b': name_en, 'c': rating_pst, 'd': rating_prs, 'e': rating_ftr, 'f': rating_byd}) + c.execute('''insert into chart values(:a,:b,:c,:d,:e,:f,:g)''', { + 'a': song_id, 'b': name_en, 'c': rating_pst, 'd': rating_prs, 'e': rating_ftr, 'f': rating_byd, 'g': rating_etr}) flash('歌曲添加成功 Successfully add the song.') else: error = '歌曲已存在 The song exists.' @@ -422,7 +425,7 @@ def all_character(): def change_character(): # 修改角色数据 skill_ids = ['No_skill', 'gauge_easy', 'note_mirror', 'gauge_hard', 'frag_plus_10_pack_stellights', 'gauge_easy|frag_plus_15_pst&prs', 'gauge_hard|fail_frag_minus_100', 'frag_plus_5_side_light', 'visual_hide_hp', 'frag_plus_5_side_conflict', 'challenge_fullcombo_0gauge', 'gauge_overflow', 'gauge_easy|note_mirror', 'note_mirror', 'visual_tomato_pack_tonesphere', - 'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', 'frags_kou', 'visual_ink', 'shirabe_entry_fee', 'frags_yume', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', 'eto_uncap', 'luna_uncap', 'frags_preferred_song', 'visual_ghost_skynotes', 'ayu_uncap', 'skill_vita', 'skill_fatalis', 'skill_reunion', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'ilith_awakened_skill', 'skill_mithra', 'skill_toa', 'skill_nami_twilight', 'skill_ilith_ivy', 'skill_hikari_vanessa', 'skill_maya', 'skill_luin', 'skill_luin_uncap', 'skill_kanae_uncap'] + 'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', 'frags_kou', 'visual_ink', 'shirabe_entry_fee', 'frags_yume', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', 'eto_uncap', 'luna_uncap', 'frags_preferred_song', 'visual_ghost_skynotes', 'ayu_uncap', 'skill_vita', 'skill_fatalis', 'skill_reunion', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'ilith_awakened_skill', 'skill_mithra', 'skill_toa', 'skill_nami_twilight', 'skill_ilith_ivy', 'skill_hikari_vanessa', 'skill_maya', 'skill_luin', 'skill_luin_uncap', 'skill_kanae_uncap', 'skill_doroc_uncap'] return render_template('web/changechar.html', skill_ids=skill_ids) From 8f66b909122d25f5f6e6b6ae0576b983b2b7fb02 Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Mon, 11 Mar 2024 16:31:21 +0800 Subject: [PATCH 09/32] [Enhance] Content bundle - Add support for content bundles - For Arcaea 5.4.0 play store version and iOS version --- latest version/core/bundle.py | 191 ++++++++++++++++++ latest version/core/config_manager.py | 7 +- latest version/core/constant.py | 4 + latest version/core/error.py | 7 + latest version/core/init.py | 20 +- latest version/core/operation.py | 11 + latest version/core/sql.py | 2 + latest version/main.py | 26 ++- latest version/server/func.py | 14 +- latest version/server/others.py | 34 ++-- .../templates/web/updatedatabase.html | 15 +- latest version/web/index.py | 14 +- 12 files changed, 313 insertions(+), 32 deletions(-) create mode 100644 latest version/core/bundle.py diff --git a/latest version/core/bundle.py b/latest version/core/bundle.py new file mode 100644 index 0000000..c4667c9 --- /dev/null +++ b/latest version/core/bundle.py @@ -0,0 +1,191 @@ +import json +import os +from time import time + +from flask import url_for + +from .constant import Constant +from .error import NoAccess, RateLimit +from .limiter import ArcLimiter + + +class ContentBundle: + + def __init__(self) -> None: + self.version: str = None + self.prev_version: str = None + self.app_version: str = None + self.uuid: str = None + + self.json_size: int = None + self.bundle_size: int = None + self.json_path: str = None # relative path + self.bundle_path: str = None # relative path + + self.json_url: str = None + self.bundle_url: str = None + + @staticmethod + def parse_version(version: str) -> tuple: + try: + r = tuple(map(int, version.split('.'))) + except AttributeError: + r = (0, 0, 0) + return r + + @property + def version_tuple(self) -> tuple: + return self.parse_version(self.version) + + @classmethod + def from_json(cls, json_data: dict) -> 'ContentBundle': + x = cls() + x.version = json_data['versionNumber'] + x.prev_version = json_data['previousVersionNumber'] + x.app_version = json_data['applicationVersionNumber'] + x.uuid = json_data['uuid'] + return x + + def to_dict(self) -> dict: + r = { + 'contentBundleVersion': self.version, + 'appVersion': self.app_version, + 'jsonSize': self.json_size, + 'bundleSize': self.bundle_size, + } + if self.json_url and self.bundle_url: + r['jsonUrl'] = self.json_url + r['bundleUrl'] = self.bundle_url + return r + + def calculate_size(self) -> None: + self.json_size = os.path.getsize(os.path.join( + Constant.CONTENT_BUNDLE_FOLDER_PATH, self.json_path)) + self.bundle_size = os.path.getsize(os.path.join( + Constant.CONTENT_BUNDLE_FOLDER_PATH, self.bundle_path)) + + +class BundleParser: + + # {app_version: [ List[ContentBundle] ]} + bundles: 'dict[str, list[ContentBundle]]' = {} + max_bundle_version: 'dict[str, str]' = {} + + def __init__(self) -> None: + self.parse() + + def re_init(self) -> None: + self.bundles.clear() + self.max_bundle_version.clear() + self.parse() + + def parse(self) -> None: + for root, dirs, files in os.walk(Constant.CONTENT_BUNDLE_FOLDER_PATH): + for file in files: + if not file.endswith('.json'): + continue + + json_path = os.path.join(root, file) + bundle_path = os.path.join(root, f'{file[:-5]}.cb') + + with open(json_path, 'rb') as f: + data = json.load(f) + + x = ContentBundle.from_json(data) + + x.json_path = os.path.relpath( + json_path, Constant.CONTENT_BUNDLE_FOLDER_PATH) + x.bundle_path = os.path.relpath( + bundle_path, Constant.CONTENT_BUNDLE_FOLDER_PATH) + + x.json_path = x.json_path.replace('\\', '/') + x.bundle_path = x.bundle_path.replace('\\', '/') + + if not os.path.isfile(bundle_path): + raise FileNotFoundError( + f'Bundle file not found: {bundle_path}') + x.calculate_size() + + if x.app_version not in self.bundles: + self.bundles[x.app_version] = [] + self.bundles[x.app_version].append(x) + + # sort by version + for k, v in self.bundles.items(): + v.sort(key=lambda x: x.version_tuple) + self.max_bundle_version[k] = v[-1].version + + +class BundleDownload: + + limiter = ArcLimiter( + Constant.BUNDLE_DOWNLOAD_TIMES_LIMIT, 'bundle_download') + + def __init__(self, c_m=None): + self.c_m = c_m + + self.client_app_version = None + self.client_bundle_version = None + self.device_id = None + + def set_client_info(self, app_version: str, bundle_version: str, device_id: str = None) -> None: + self.client_app_version = app_version + self.client_bundle_version = bundle_version + self.device_id = device_id + + def get_bundle_list(self) -> list: + bundles: 'list[ContentBundle]' = BundleParser.bundles.get( + self.client_app_version, []) + if not bundles: + return [] + + now = time() + + if Constant.BUNDLE_DOWNLOAD_LINK_PREFIX: + prefix = Constant.BUNDLE_DOWNLOAD_LINK_PREFIX + if prefix[-1] != '/': + prefix += '/' + + def url_func(x): return f'{prefix}{x}' + else: + def url_func(x): return url_for( + 'bundle_download', token=x, _external=True) + + sql_list = [] + r = [] + for x in bundles: + if x.version_tuple <= ContentBundle.parse_version(self.client_bundle_version): + continue + t1 = os.urandom(64).hex() + t2 = os.urandom(64).hex() + + x.json_url = url_func(t1) + x.bundle_url = url_func(t2) + + sql_list.append((t1, x.json_path, now, self.device_id)) + sql_list.append((t2, x.bundle_path, now, self.device_id)) + r.append(x.to_dict()) + + if not sql_list: + return [] + + self.c_m.executemany( + '''insert into bundle_download_token values (?, ?, ?, ?)''', sql_list) + + return r + + def get_path_by_token(self, token: str, ip: str) -> str: + r = self.c_m.execute( + '''select file_path, time, device_id from bundle_download_token where token = ?''', (token,)).fetchone() + if not r: + raise NoAccess('Invalid token.', status=403) + file_path, create_time, device_id = r + + if time() - create_time > Constant.BUNDLE_DOWNLOAD_TIME_GAP_LIMIT: + raise NoAccess('Expired token.', status=403) + + if file_path.endswith('.cb') and not self.limiter.hit(ip): + raise RateLimit( + f'Too many content bundle downloads, IP: {ip}, DeviceID: {device_id}', status=429) + + return file_path diff --git a/latest version/core/config_manager.py b/latest version/core/config_manager.py index 6e82c28..4f00992 100644 --- a/latest version/core/config_manager.py +++ b/latest version/core/config_manager.py @@ -42,7 +42,8 @@ class Config: API_TOKEN = '' - DOWNLOAD_LINK_PREFIX = '' + DOWNLOAD_LINK_PREFIX = '' # http(s)://host(:port)/download/ + BUNDLE_DOWNLOAD_LINK_PREFIX = '' # http(s)://host(:port)/bundle_download/ DOWNLOAD_USE_NGINX_X_ACCEL_REDIRECT = False NGINX_X_ACCEL_REDIRECT_PREFIX = '/nginx_download/' @@ -52,6 +53,9 @@ class Config: DOWNLOAD_FORBID_WHEN_NO_ITEM = False + BUNDLE_DOWNLOAD_TIMES_LIMIT = '100/60 minutes' + BUNDLE_DOWNLOAD_TIME_GAP_LIMIT = 3000 + LOGIN_DEVICE_NUMBER_LIMIT = 1 ALLOW_LOGIN_SAME_DEVICE = False ALLOW_BAN_MULTIDEVICE_USER_AUTO = True @@ -84,6 +88,7 @@ class Config: WORLD_MAP_FOLDER_PATH = './database/map/' SONG_FILE_FOLDER_PATH = './database/songs/' SONGLIST_FILE_PATH = './database/songs/songlist' + CONTENT_BUNDLE_FOLDER_PATH = './database/bundle/' SQLITE_DATABASE_PATH = './database/arcaea_database.db' SQLITE_DATABASE_BACKUP_FOLDER_PATH = './database/backup/' DATABASE_INIT_PATH = './database/init/' diff --git a/latest version/core/constant.py b/latest version/core/constant.py index 0d1caf4..93d329b 100644 --- a/latest version/core/constant.py +++ b/latest version/core/constant.py @@ -43,6 +43,7 @@ class Constant: WORLD_MAP_FOLDER_PATH = Config.WORLD_MAP_FOLDER_PATH SONG_FILE_FOLDER_PATH = Config.SONG_FILE_FOLDER_PATH SONGLIST_FILE_PATH = Config.SONGLIST_FILE_PATH + CONTENT_BUNDLE_FOLDER_PATH = Config.CONTENT_BUNDLE_FOLDER_PATH SQLITE_DATABASE_PATH = Config.SQLITE_DATABASE_PATH SQLITE_LOG_DATABASE_PATH = Config.SQLITE_LOG_DATABASE_PATH SQLITE_DATABASE_DELETED_PATH = Config.SQLITE_DATABASE_DELETED_PATH @@ -50,6 +51,9 @@ class Constant: DOWNLOAD_TIMES_LIMIT = Config.DOWNLOAD_TIMES_LIMIT DOWNLOAD_TIME_GAP_LIMIT = Config.DOWNLOAD_TIME_GAP_LIMIT DOWNLOAD_LINK_PREFIX = Config.DOWNLOAD_LINK_PREFIX + BUNDLE_DOWNLOAD_TIMES_LIMIT = Config.BUNDLE_DOWNLOAD_TIMES_LIMIT + BUNDLE_DOWNLOAD_TIME_GAP_LIMIT = Config.BUNDLE_DOWNLOAD_TIME_GAP_LIMIT + BUNDLE_DOWNLOAD_LINK_PREFIX = Config.BUNDLE_DOWNLOAD_LINK_PREFIX LINKPLAY_UNLOCK_LENGTH = 512 # Units: bytes LINKPLAY_TIMEOUT = 5 # Units: seconds diff --git a/latest version/core/error.py b/latest version/core/error.py index 439cdec..40913d3 100644 --- a/latest version/core/error.py +++ b/latest version/core/error.py @@ -101,6 +101,13 @@ def __init__(self, message=None, error_code=108, api_error_code=-999, extra_data super().__init__(message, error_code, api_error_code, extra_data, status) +class LowVersion(ArcError): + '''版本过低''' + + def __init__(self, message=None, error_code=5, api_error_code=-999, extra_data=None, status=403) -> None: + super().__init__(message, error_code, api_error_code, extra_data, status) + + class Timeout(ArcError): '''超时''' pass diff --git a/latest version/core/init.py b/latest version/core/init.py index efef5a5..a8152a9 100644 --- a/latest version/core/init.py +++ b/latest version/core/init.py @@ -6,6 +6,7 @@ from time import time from traceback import format_exc +from core.bundle import BundleParser from core.config_manager import Config from core.constant import ARCAEA_LOG_DATBASE_VERSION, ARCAEA_SERVER_VERSION from core.course import Course @@ -331,14 +332,27 @@ def check_song_file(self) -> bool: DownloadList.initialize_cache() if not Config.SONG_FILE_HASH_PRE_CALCULATE: self.logger.info('Song file hash pre-calculate is disabled.') - self.logger.info('Complete!') + self.logger.info('Song data initialization is complete!') except Exception as e: self.logger.error(format_exc()) - self.logger.warning('Initialization error!') + self.logger.warning('Song data initialization error!') + f = False + return f + + def check_content_bundle(self) -> bool: + '''检查 content bundle 有关文件并初始化缓存''' + f = self.check_folder(Config.CONTENT_BUNDLE_FOLDER_PATH) + self.logger.info("Start to initialize content bundle data...") + try: + BundleParser() + self.logger.info('Content bundle data initialization is complete!') + except Exception as e: + self.logger.error(format_exc()) + self.logger.warning('Content bundle data initialization error!') f = False return f def check_before_run(self) -> bool: '''运行前检查,返回布尔值''' MemoryDatabase() # 初始化内存数据库 - return self.check_song_file() & self.check_update_database() + return self.check_song_file() and self.check_content_bundle() and self.check_update_database() diff --git a/latest version/core/operation.py b/latest version/core/operation.py index 3b6006d..741d4af 100644 --- a/latest version/core/operation.py +++ b/latest version/core/operation.py @@ -1,3 +1,4 @@ +from .bundle import BundleParser from .constant import Constant from .download import DownloadList from .error import NoData @@ -74,6 +75,16 @@ def run(self): DownloadList.initialize_cache() +class RefreshBundleCache(BaseOperation): + ''' + 刷新 bundle 缓存 + ''' + _name = 'refresh_content_bundle_cache' + + def run(self): + BundleParser().re_init() + + class SaveUpdateScore(BaseOperation): ''' 云存档更新成绩,是覆盖式更新 diff --git a/latest version/core/sql.py b/latest version/core/sql.py index a19088b..ee4388f 100644 --- a/latest version/core/sql.py +++ b/latest version/core/sql.py @@ -440,6 +440,8 @@ def __init__(self): self.c.execute('''PRAGMA synchronous = 0''') self.c.execute('''create table if not exists download_token(user_id int, song_id text,file_name text,token text,time int,primary key(user_id, song_id, file_name));''') + self.c.execute('''create table if not exists bundle_download_token(token text primary key, + file_path text, time int, device_id text);''') self.c.execute( '''create index if not exists download_token_1 on download_token (song_id, file_name);''') self.conn.commit() diff --git a/latest version/main.py b/latest version/main.py index f05cc30..8f84d60 100644 --- a/latest version/main.py +++ b/latest version/main.py @@ -27,6 +27,7 @@ import web.index import web.login # import webapi +from core.bundle import BundleDownload from core.constant import Constant from core.download import UserDownload from core.error import ArcError, NoAccess, RateLimit @@ -81,7 +82,8 @@ def download(file_path): x.song_id, x.file_name = file_path.split('/', 1) x.select_for_check() if x.is_limited: - raise RateLimit('You have reached the download limit.', 903) + raise RateLimit( + f'User `{x.user.user_id}` has reached the download limit.', 903) if not x.is_valid: raise NoAccess('Expired token.') x.download_hit() @@ -99,6 +101,26 @@ def download(file_path): return error_return() +@app.route('/bundle_download/', methods=['GET']) # 热更新下载 +def bundle_download(token: str): + with Connect(in_memory=True) as c_m: + try: + file_path = BundleDownload(c_m).get_path_by_token( + token, request.remote_addr) + if Config.DOWNLOAD_USE_NGINX_X_ACCEL_REDIRECT: + # nginx X-Accel-Redirect + response = make_response() + response.headers['Content-Type'] = 'application/octet-stream' + response.headers['X-Accel-Redirect'] = Config.NGINX_X_ACCEL_REDIRECT_PREFIX + file_path + return response + return send_from_directory(Constant.CONTENT_BUNDLE_FOLDER_PATH, file_path, as_attachment=True, conditional=True) + except ArcError as e: + if Config.ALLOW_WARNING_LOG: + app.logger.warning(format_exc()) + return error_return(e) + return error_return() + + if Config.DEPLOY_MODE == 'waitress': # 给waitress加个日志 @app.after_request @@ -124,7 +146,7 @@ def tcp_server_run(): elif Config.DEPLOY_MODE == 'waitress': # waitress WSGI server import logging - from waitress import serve + from waitress import serve # type: ignore logger = logging.getLogger('waitress') logger.setLevel(logging.INFO) serve(app, host=Config.HOST, port=Config.PORT) diff --git a/latest version/server/func.py b/latest version/server/func.py index 5db87e7..061ad93 100644 --- a/latest version/server/func.py +++ b/latest version/server/func.py @@ -3,8 +3,9 @@ from flask import current_app, g, jsonify +from core.bundle import BundleParser from core.config_manager import Config -from core.error import ArcError, NoAccess +from core.error import ArcError, LowVersion, NoAccess has_arc_hash = False try: @@ -22,7 +23,9 @@ def error_return(e: ArcError = default_error): # 错误返回 # -4 您的账号已在别处登录 # -3 无法连接至服务器 # 2 Arcaea服务器正在维护 + # 5 请更新 Arcaea 到最新版本 # 9 新版本请等待几分钟 + # 11 有游戏内容需要更新,即将返回主界面 # 100 无法在此ip地址下登录游戏 # 101 用户名占用 # 102 电子邮箱已注册 @@ -41,7 +44,7 @@ def error_return(e: ArcError = default_error): # 错误返回 # 150 非常抱歉您已被限制使用此功能 # 151 目前无法使用此功能 # 160 账户未邮箱认证,请检查邮箱 - # 161 账户认证过期,请重新注册 + # 161 账户认证过期,请重新注册 # 401 用户不存在 # 403 无法连接至服务器 # 501 502 -6 此物品目前无法获取 @@ -65,7 +68,7 @@ def error_return(e: ArcError = default_error): # 错误返回 # 9803 下载已取消 # 9905 没有在云端发现任何数据 # 9907 更新数据时发生了问题 - # 9908 服务器只支持最新的版本,请更新Arcaea + # 9908 服务器只支持最新的版本,请更新 Arcaea # 其它 发生未知错误 r = {"success": False, "error_code": e.error_code} if e.extra_data: @@ -106,7 +109,10 @@ def header_check(request) -> ArcError: headers = request.headers if Config.ALLOW_APPVERSION: # 版本检查 if 'AppVersion' not in headers or headers['AppVersion'] not in Config.ALLOW_APPVERSION: - return NoAccess('Invalid app version', 5) + return LowVersion('Invalid app version', 5) + + if 'ContentBundle' in headers and headers['ContentBundle'] != BundleParser.max_bundle_version[headers['AppVersion']]: + return LowVersion('Invalid content bundle version', 11) if has_arc_hash and not ArcHashChecker(request).check(): return NoAccess('Invalid request') diff --git a/latest version/server/others.py b/latest version/server/others.py index 27aac73..f433d36 100644 --- a/latest version/server/others.py +++ b/latest version/server/others.py @@ -4,6 +4,7 @@ from flask import Blueprint, jsonify, request from werkzeug.datastructures import ImmutableMultiDict +from core.bundle import BundleDownload from core.download import DownloadList from core.error import RateLimit from core.sql import Connect @@ -26,26 +27,19 @@ def game_info(): return success_return(GameInfo().to_dict()) -# @bp.route('/game/content_bundle', methods=['GET']) # 热更新 -# def game_content_bundle(): -# app_version = request.headers.get('AppVersion') -# bundle_version = request.headers.get('ContentBundle') -# import os -# if bundle_version != '5.4.0': -# r = {'orderedResults': [ -# { -# 'appVersion': '5.4.0', -# 'contentBundleVersion': '5.4.0', -# 'jsonUrl': 'http://192.168.0.110/bundle_download/bundle.json', -# 'jsonSize': os.path.getsize('./database/bundle/bundle.json'), -# 'bundleUrl': 'http://192.168.0.110/bundle_download/bundle', -# 'bundleSize': os.path.getsize('./database/bundle/bundle') -# }, -# ] -# } -# else: -# r = {} -# return success_return(r) +@bp.route('/game/content_bundle', methods=['GET']) # 热更新 +@arc_try +def game_content_bundle(): + # error code 5, 9 work + app_version = request.headers.get('AppVersion') + bundle_version = request.headers.get('ContentBundle') + device_id = request.headers.get('DeviceId') + with Connect(in_memory=True) as c_m: + x = BundleDownload(c_m) + x.set_client_info(app_version, bundle_version, device_id) + return success_return({ + 'orderedResults': x.get_bundle_list() + }) @bp.route('/serve/download/me/song', methods=['GET']) # 歌曲下载 diff --git a/latest version/templates/web/updatedatabase.html b/latest version/templates/web/updatedatabase.html index ddc48dd..9b1764a 100644 --- a/latest version/templates/web/updatedatabase.html +++ b/latest version/templates/web/updatedatabase.html @@ -13,7 +13,8 @@

{% block title %}Update databases{% endblock %}

这里可以将旧版本的数据库同步到新版本的数据库,并刷新用户拥有的全角色列表。
可上传文件: arcaea_database.db
新数据库不存在的数据会被添加,存在的重复数据也会被改变。

- Here you can synchronize the old version of the database to the new version of the database and refresh the list of full + Here you can synchronize the old version of the database to the new version of the database and refresh the list of + full characters owned by players.
Uploadable files: arcaea_database.db
Data that does not exist in the new database will be added and the existing duplicate data will also be changed. @@ -40,4 +41,16 @@

{% block title %}Update databases{% endblock %}

Here you can refresh the ratings of all scores in the database. The purpose is to deal with the updating of songs' chart consts.
+
+
+
+
Refresh content bundles
+
+ + +
这里可以刷新储存在内存中的 bundle 文件夹下所有热更新文件的信息。目的是应对热更新文件的修改。
+
Here you can refresh the information of all the files stored in the bundle folder in memory. + The purpose + is to deal with the updating of content bundles.
+
{% endblock %} \ No newline at end of file diff --git a/latest version/web/index.py b/latest version/web/index.py index 456a063..d908402 100644 --- a/latest version/web/index.py +++ b/latest version/web/index.py @@ -2,7 +2,7 @@ import time from core.init import FileChecker -from core.operation import RefreshAllScoreRating, RefreshSongFileCache, SaveUpdateScore, UnlockUserItem, DeleteUserScore +from core.operation import RefreshAllScoreRating, RefreshBundleCache, RefreshSongFileCache, SaveUpdateScore, UnlockUserItem, DeleteUserScore from core.rank import RankList from core.sql import Connect from core.user import User @@ -299,6 +299,18 @@ def update_song_hash(): return render_template('web/updatedatabase.html') +@bp.route('/updatedatabase/refreshsbundle', methods=['POST']) +@login_required +def update_content_bundle(): + # 更新 bundle + try: + RefreshBundleCache().run() + flash('数据刷新成功 Success refresh data.') + except: + flash('Something error!') + return render_template('web/updatedatabase.html') + + @bp.route('/updatedatabase/refreshsongrating', methods=['POST']) @login_required def update_song_rating(): From eeced74f505f47b1c86018b9e137649a02f0b38b Mon Sep 17 00:00:00 2001 From: Tenshi <156825971+tenshi0xx@users.noreply.github.com> Date: Sat, 16 Mar 2024 10:38:47 +0700 Subject: [PATCH 10/32] Fix ETR difficulty download --- latest version/core/download.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/latest version/core/download.py b/latest version/core/download.py index cc358d6..448f6c8 100644 --- a/latest version/core/download.py +++ b/latest version/core/download.py @@ -24,7 +24,7 @@ def get_song_file_md5(song_id: str, file_name: str) -> str: class SonglistParser: '''songlist文件解析器''' - FILE_NAMES = ['0.aff', '1.aff', '2.aff', '3.aff', + FILE_NAMES = ['0.aff', '1.aff', '2.aff', '3.aff', '4.aff', 'base.ogg', '3.ogg', 'video.mp4', 'video_audio.ogg', 'video_720.mp4', 'video_1080.mp4'] has_songlist = False From 2f826e9ffce4175d3b2c62e48162345e3024652e Mon Sep 17 00:00:00 2001 From: Tenshi <156825971+tenshi0xx@users.noreply.github.com> Date: Sat, 16 Mar 2024 10:45:11 +0700 Subject: [PATCH 11/32] Fix ETR Score sending --- latest version/core/score.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/latest version/core/score.py b/latest version/core/score.py index 0ffe269..4f37ca5 100644 --- a/latest version/core/score.py +++ b/latest version/core/score.py @@ -93,7 +93,7 @@ def is_valid(self) -> bool: '''分数有效性检查''' if self.shiny_perfect_count < 0 or self.perfect_count < 0 or self.near_count < 0 or self.miss_count < 0 or self.score < 0 or self.time_played <= 0: return False - if self.song.difficulty not in (0, 1, 2, 3): + if self.song.difficulty not in (0, 1, 2, 3, 4): return False all_note = self.all_note_count From a05bb4854d8dbd79cae2b3abf7819475079318a7 Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Sun, 17 Mar 2024 22:35:52 +0800 Subject: [PATCH 12/32] [Enhance][Bug fix][Doc] ETR for songdb tool & Update notify - Add support for ETR difficulties in the `update_song.py` tool - Make content bundle updates mandatory for client - README update --- README.md | 7 +++++++ latest version/core/config_manager.py | 2 +- latest version/database/bundle/README.md | 5 +++++ latest version/server/func.py | 5 ++--- tools/update_song.py | 18 ++++++++++-------- 5 files changed, 25 insertions(+), 12 deletions(-) create mode 100644 latest version/database/bundle/README.md diff --git a/README.md b/README.md index b989055..0ea8b03 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ This procedure is mainly used for study and research, and shall not be used for - :x: 加密下载 Encrypted downloading - 下载校验 Download check - 下载频次限制 Download rate limit +- 内容捆绑包热更新 Content bundle hot update - 购买系统 Purchase system - 单曲和曲包 Single & Pack - :x: 捆绑包 Pack bundle @@ -129,6 +130,11 @@ It is just so interesting. What it can do is under exploration. [English](https://github.com/Lost-MSth/Arcaea-server/wiki/Environment-construction) --> +## 子项目 Sub repositories + +[Arcaea-Bundler](https://github.com/Lost-MSth/Arcaea-Bundler) +: 用于生成和解包内容捆绑包 Used to pack or unpack content bundles + ## 使用说明 Instruction for use [中文](https://github.com/Lost-MSth/Arcaea-server/wiki/%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E) @@ -148,6 +154,7 @@ It is just so interesting. What it can do is under exploration. 歌曲数据库来自 Using song database from ~~[BotArcAPI releases](https://github.com/TheSnowfield/BotArcAPI/releases)~~ [ArcaeaSongDatabase](https://github.com/Arcaea-Infinity/ArcaeaSongDatabase) +[ArcaeaSongDatabase Fork](https://github.com/CuSO4Deposit/ArcaeaSongDatabase) > 从v2.9开始不再提供歌曲数据 > Since v2.9, song data will not be provided. diff --git a/latest version/core/config_manager.py b/latest version/core/config_manager.py index 4f00992..7ccfde5 100644 --- a/latest version/core/config_manager.py +++ b/latest version/core/config_manager.py @@ -12,7 +12,7 @@ class Config: SONG_FILE_HASH_PRE_CALCULATE = True - GAME_API_PREFIX = '/saikyoukaze/27' # str | list[str] + GAME_API_PREFIX = '/desks/28' # str | list[str] OLD_GAME_API_PREFIX = [] # str | list[str] ALLOW_APPVERSION = [] # list[str] diff --git a/latest version/database/bundle/README.md b/latest version/database/bundle/README.md new file mode 100644 index 0000000..aee86b1 --- /dev/null +++ b/latest version/database/bundle/README.md @@ -0,0 +1,5 @@ +## Arcaea Bundle Folder + +[Arcaea-Bundler](https://github.com/Lost-MSth/Arcaea-Bundler) + +There must be pairs of bundle file and JSON metadata file with the same name. The bundle file's suffix is `.cb` and the metadata file's suffix is `.json`. diff --git a/latest version/server/func.py b/latest version/server/func.py index 061ad93..0a3cd92 100644 --- a/latest version/server/func.py +++ b/latest version/server/func.py @@ -110,9 +110,8 @@ def header_check(request) -> ArcError: if Config.ALLOW_APPVERSION: # 版本检查 if 'AppVersion' not in headers or headers['AppVersion'] not in Config.ALLOW_APPVERSION: return LowVersion('Invalid app version', 5) - - if 'ContentBundle' in headers and headers['ContentBundle'] != BundleParser.max_bundle_version[headers['AppVersion']]: - return LowVersion('Invalid content bundle version', 11) + if 'ContentBundle' in headers and headers['ContentBundle'] != BundleParser.max_bundle_version[headers['AppVersion']]: + return LowVersion('Invalid content bundle version', 11) if has_arc_hash and not ArcHashChecker(request).check(): return NoAccess('Invalid request') diff --git a/tools/update_song.py b/tools/update_song.py index 84f0abe..55de357 100644 --- a/tools/update_song.py +++ b/tools/update_song.py @@ -34,7 +34,7 @@ def __exit__(self, exc_type, exc_val, exc_tb) -> bool: return True -def insert(cursor, song_id, name, a, b, c, d, update_type=0): +def insert(cursor, song_id, name, a, b, c, d, e, update_type=0): '''Insert a new song into database.''' if update_type == 0 or update_type == 1: cursor.execute( @@ -45,12 +45,12 @@ def insert(cursor, song_id, name, a, b, c, d, update_type=0): return elif update_type == 1: # 重复则更新,以`arcsong.db`数据为准 - cursor.execute('''update chart set name=?, rating_pst=?, rating_prs=?, rating_ftr=?, rating_byn=? where song_id=?''', - (name, a, b, c, d, song_id)) + cursor.execute('''update chart set name=?, rating_pst=?, rating_prs=?, rating_ftr=?, rating_byn=?, rating_etr=? where song_id=?''', + (name, a, b, c, d, e, song_id)) return cursor.execute( - '''insert into chart values (?,?,?,?,?,?)''', (song_id, name, a, b, c, d)) + '''insert into chart values (?,?,?,?,?,?,?)''', (song_id, name, a, b, c, d, e)) def from_song_datebase(): @@ -61,17 +61,19 @@ def from_song_datebase(): if 'songs' in tables: c.execute( '''select sid, name_en, rating_pst, rating_prs, rating_ftr, rating_byn from songs''') - data = c.fetchall() + data = [] + for x in c.fetchall(): + data.append((x[0], x[1], x[2], x[3], x[4], x[5], -1)) elif 'charts' in tables: c.execute( '''select song_id, rating_class, name_en, rating from charts''') songs = {} for song_id, rating_class, name_en, rating in c.fetchall(): if song_id not in songs: - songs[song_id] = [-1, -1, -1, -1, name_en] + songs[song_id] = [-1, -1, -1, -1, -1, name_en] songs[song_id][rating_class] = rating - data = [(x, y[-1], y[0], y[1], y[2], y[3]) + data = [(x, y[-1], y[0], y[1], y[2], y[3], y[4]) for x, y in songs.items()] else: print('Error: Cannot find table `songs` or `charts` in the database.') @@ -91,7 +93,7 @@ def from_song_datebase(): # 清空数据表后更新 c.execute('''delete from chart''') for x in data: - insert(c, x[0], x[1], x[2], x[3], x[4], x[5], update_type) + insert(c, x[0], x[1], x[2], x[3], x[4], x[5], x[6], update_type) print('Seems to be done.') From 02bf565ebd560e696e46df591e923470e814f153 Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Mon, 1 Apr 2024 16:41:15 +0800 Subject: [PATCH 13/32] [Bug fix] API song difficulty ETR - Fix a but that some API cannot handle ETR difficulty. - New character "Vita(Cadenza)" - For Arcaea 5.5.0 / 5.5.6 --- latest version/core/config_manager.py | 2 +- latest version/core/constant.py | 2 +- latest version/core/song.py | 4 +-- latest version/database/init/arc_data.py | 30 +++++++++++------------ latest version/database/init/packs.json | 18 ++++++++++++++ latest version/database/init/singles.json | 18 ++++++++++++++ 6 files changed, 55 insertions(+), 19 deletions(-) diff --git a/latest version/core/config_manager.py b/latest version/core/config_manager.py index 7ccfde5..ea12d00 100644 --- a/latest version/core/config_manager.py +++ b/latest version/core/config_manager.py @@ -12,7 +12,7 @@ class Config: SONG_FILE_HASH_PRE_CALCULATE = True - GAME_API_PREFIX = '/desks/28' # str | list[str] + GAME_API_PREFIX = '/hanami/29' # str | list[str] OLD_GAME_API_PREFIX = [] # str | list[str] ALLOW_APPVERSION = [] # list[str] diff --git a/latest version/core/constant.py b/latest version/core/constant.py index 93d329b..eb54a25 100644 --- a/latest version/core/constant.py +++ b/latest version/core/constant.py @@ -1,6 +1,6 @@ from .config_manager import Config -ARCAEA_SERVER_VERSION = 'v2.11.3.5' +ARCAEA_SERVER_VERSION = 'v2.11.3.8' ARCAEA_LOG_DATBASE_VERSION = 'v1.1' diff --git a/latest version/core/song.py b/latest version/core/song.py index 1d82cf3..d6af16f 100644 --- a/latest version/core/song.py +++ b/latest version/core/song.py @@ -75,8 +75,8 @@ def from_dict(self, d: dict) -> 'Song': self.song_id = d['song_id'] self.name = d.get('name', '') self.charts = [Chart(self.c, self.song_id, 0), Chart(self.c, self.song_id, 1), Chart( - self.c, self.song_id, 2), Chart(self.c, self.song_id, 3)] - for i in range(4): + self.c, self.song_id, 2), Chart(self.c, self.song_id, 3), Chart(self.c, self.song_id, 4)] + for i in range(5): self.charts[i].defnum = -10 for chart in d['charts']: self.charts[chart['difficulty']].defnum = round( diff --git a/latest version/database/init/arc_data.py b/latest version/database/init/arc_data.py index 8efea5f..dbb96e8 100644 --- a/latest version/database/init/arc_data.py +++ b/latest version/database/init/arc_data.py @@ -1,45 +1,45 @@ class InitData: char = ['hikari', 'tairitsu', 'kou', 'sapphire', 'lethe', 'hikari&tairitsu(reunion)', 'Tairitsu(Axium)', 'Tairitsu(Grievous Lady)', 'stella', 'Hikari & Fisica', 'ilith', 'eto', 'luna', 'shirabe', 'Hikari(Zero)', 'Hikari(Fracture)', 'Hikari(Summer)', 'Tairitsu(Summer)', 'Tairitsu & Trin', - 'ayu', 'Eto & Luna', 'yume', 'Seine & Hikari', 'saya', 'Tairitsu & Chuni Penguin', 'Chuni Penguin', 'haruna', 'nono', 'MTA-XXX', 'MDA-21', 'kanae', 'Hikari(Fantasia)', 'Tairitsu(Sonata)', 'sia', 'DORO*C', 'Tairitsu(Tempest)', 'brillante', 'Ilith(Summer)', 'etude', 'Alice & Tenniel', 'Luna & Mia', 'areus', 'seele', 'isabelle', 'mir', 'lagrange', 'linka', 'nami', 'Saya & Elizabeth', 'lily', 'kanae(midsummer)', 'alice&tenniel(minuet)', 'tairitsu(elegy)', 'marija', 'vita', 'hikari(fatalis)', 'saki', 'setsuna', 'amane', 'kou(winter)', 'lagrange(aria)', 'lethe(apophenia)', 'shama(UNiVERSE)', 'milk(UNiVERSE)', 'shikoku', 'mika yurisaki', 'Mithra Tercera', 'Toa Kozukata', 'Nami(Twilight)', 'Ilith & Ivy', 'Hikari & Vanessa', 'Maya', 'Insight(Ascendant - 8th Seeker)', 'Luin'] + 'ayu', 'Eto & Luna', 'yume', 'Seine & Hikari', 'saya', 'Tairitsu & Chuni Penguin', 'Chuni Penguin', 'haruna', 'nono', 'MTA-XXX', 'MDA-21', 'kanae', 'Hikari(Fantasia)', 'Tairitsu(Sonata)', 'sia', 'DORO*C', 'Tairitsu(Tempest)', 'brillante', 'Ilith(Summer)', 'etude', 'Alice & Tenniel', 'Luna & Mia', 'areus', 'seele', 'isabelle', 'mir', 'lagrange', 'linka', 'nami', 'Saya & Elizabeth', 'lily', 'kanae(midsummer)', 'alice&tenniel(minuet)', 'tairitsu(elegy)', 'marija', 'vita', 'hikari(fatalis)', 'saki', 'setsuna', 'amane', 'kou(winter)', 'lagrange(aria)', 'lethe(apophenia)', 'shama(UNiVERSE)', 'milk(UNiVERSE)', 'shikoku', 'mika yurisaki', 'Mithra Tercera', 'Toa Kozukata', 'Nami(Twilight)', 'Ilith & Ivy', 'Hikari & Vanessa', 'Maya', 'Insight(Ascendant - 8th Seeker)', 'Luin', 'Vita(Cadenza)'] skill_id = ['gauge_easy', '', '', '', 'note_mirror', 'skill_reunion', '', 'gauge_hard', 'frag_plus_10_pack_stellights', 'gauge_easy|frag_plus_15_pst&prs', 'gauge_hard|fail_frag_minus_100', 'frag_plus_5_side_light', 'visual_hide_hp', 'frag_plus_5_side_conflict', 'challenge_fullcombo_0gauge', 'gauge_overflow', 'gauge_easy|note_mirror', 'note_mirror', 'visual_tomato_pack_tonesphere', - 'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', '', '', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', '', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', '', '', 'visual_ghost_skynotes', 'skill_vita', 'skill_fatalis', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', '', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'skill_mithra', 'skill_toa', 'skill_nami_twilight', 'skill_ilith_ivy', 'skill_hikari_vanessa', 'skill_maya', 'skill_intruder', 'skill_luin'] + 'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', '', '', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', '', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', '', '', 'visual_ghost_skynotes', 'skill_vita', 'skill_fatalis', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', '', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'skill_mithra', 'skill_toa', 'skill_nami_twilight', 'skill_ilith_ivy', 'skill_hikari_vanessa', 'skill_maya', 'skill_intruder', 'skill_luin', ''] skill_id_uncap = ['', '', 'frags_kou', '', 'visual_ink', '', '', '', '', '', 'ilith_awakened_skill', 'eto_uncap', 'luna_uncap', 'shirabe_entry_fee', - '', '', '', '', '', 'ayu_uncap', '', 'frags_yume', '', '', '', '', '', '', '', '', 'skill_kanae_uncap', '', '', '', 'skill_doroc_uncap', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'skill_luin_uncap'] + '', '', '', '', '', 'ayu_uncap', '', 'frags_yume', '', '', '', '', '', '', '', '', 'skill_kanae_uncap', '', '', '', 'skill_doroc_uncap', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'skill_luin_uncap', ''] skill_unlock_level = [0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, - 0, 0, 0, 8, 0, 14, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + 0, 0, 0, 8, 0, 14, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] frag1 = [55, 55, 60, 50, 47, 79, 47, 57, 41, 22, 50, 54, 60, 56, 78, 42, 41, 61, 52, 50, 52, 32, - 42, 55, 45, 58, 43, 0.5, 68, 50, 62, 45, 45, 52, 44, 27, 59, 0, 45, 50, 50, 47, 47, 61, 43, 42, 38, 25, 58, 50, 61, 45, 45, 38, 34, 27, 18, 56, 47, 30, 45, 57, 56, 47, 33, 26, 29, 66, 40, 33, 51, 27, 50, 60] + 42, 55, 45, 58, 43, 0.5, 68, 50, 62, 45, 45, 52, 44, 27, 59, 0, 45, 50, 50, 47, 47, 61, 43, 42, 38, 25, 58, 50, 61, 45, 45, 38, 34, 27, 18, 56, 47, 30, 45, 57, 56, 47, 33, 26, 29, 66, 40, 33, 51, 27, 50, 60, 45] prog1 = [35, 55, 47, 50, 60, 70, 60, 70, 58, 45, 70, 45, 42, 46, 61, 67, 49, 44, 28, 45, 24, 46, 52, - 59, 62, 33, 58, 25, 63, 69, 50, 45, 45, 51, 34, 70, 62, 70, 45, 32, 32, 61, 47, 47, 37, 42, 50, 50, 45, 41, 61, 45, 45, 58, 50, 130, 18, 57, 55, 50, 45, 70, 37.5, 29, 44, 26, 26, 35, 40, 33, 58, 31, 50, 50] + 59, 62, 33, 58, 25, 63, 69, 50, 45, 45, 51, 34, 70, 62, 70, 45, 32, 32, 61, 47, 47, 37, 42, 50, 50, 45, 41, 61, 45, 45, 58, 50, 130, 18, 57, 55, 50, 45, 70, 37.5, 29, 44, 26, 26, 35, 40, 33, 58, 31, 50, 50, 45] overdrive1 = [35, 55, 25, 50, 47, 70, 72, 57, 41, 7, 10, 32, 65, 31, 61, 53, 31, 47, 38, 12, 39, 18, - 48, 65, 45, 55, 44, 25, 46, 44, 33, 45, 45, 37, 25, 27, 50, 20, 45, 63, 21, 47, 61, 47, 65, 80, 38, 30, 49, 15, 34, 45, 45, 38, 67, 120, 44, 33, 55, 50, 45, 57, 31, 29, 65, 26, 29, 42.5, 40, 33, 58, 31, 50, 34] + 48, 65, 45, 55, 44, 25, 46, 44, 33, 45, 45, 37, 25, 27, 50, 20, 45, 63, 21, 47, 61, 47, 65, 80, 38, 30, 49, 15, 34, 45, 45, 38, 67, 120, 44, 33, 55, 50, 45, 57, 31, 29, 65, 26, 29, 42.5, 40, 33, 58, 31, 50, 34, 45] frag20 = [78, 80, 90, 75, 70, 79, 70, 79, 65, 40, 50, 80, 90, 82, 0, 61, 67, 92, 85, 50, 86, 52, - 65, 85, 67, 88, 64, 0.5, 95, 70, 95, 50, 80, 87, 71, 50, 85, 0, 80, 75, 50, 70, 70, 90, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 90, 80, 50, 51, 54, 100, 50, 58, 51, 40, 50, 70] + 65, 85, 67, 88, 64, 0.5, 95, 70, 95, 50, 80, 87, 71, 50, 85, 0, 80, 75, 50, 70, 70, 90, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 90, 80, 50, 51, 54, 100, 50, 58, 51, 40, 50, 70, 50] prog20 = [61, 80, 70, 75, 90, 70, 90, 102, 84, 78, 105, 67, 63, 68, 0, 99, 80, 66, 46, 83, 40, 73, - 80, 90, 93, 50, 86, 78, 89, 98, 75, 80, 50, 64, 55, 100, 90, 110, 80, 50, 74, 90, 70, 70, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51, 50, 53, 85, 58, 96, 47, 50, 80] + 80, 90, 93, 50, 86, 78, 89, 98, 75, 80, 50, 64, 55, 100, 90, 110, 80, 50, 74, 90, 70, 70, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51, 50, 53, 85, 58, 96, 47, 50, 80, 67] overdrive20 = [61, 80, 47, 75, 70, 70, 95, 79, 65, 31, 50, 59, 90, 58, 0, 78, 50, 70, 62, 49, 64, - 46, 73, 95, 67, 84, 70, 78, 69, 70, 50, 80, 80, 63, 25, 50, 72, 55, 50, 95, 55, 70, 90, 70, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 54, 65.5, 59.5, 58, 96, 47, 50, 54] + 46, 73, 95, 67, 84, 70, 78, 69, 70, 50, 80, 80, 63, 25, 50, 72, 55, 50, 95, 55, 70, 90, 70, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 54, 65.5, 59.5, 58, 96, 47, 50, 54, 90] frag30 = [88, 90, 100, 75, 80, 89, 70, 79, 65, 40, 50, 90, 100, 92, 0, 61, 67, 92, 85, 50, 86, 62, - 65, 85, 67, 88, 74, 0.5, 105, 80, 105, 50, 80, 87, 81, 50, 95, 0, 80, 75, 50, 70, 80, 100, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 90, 80, 50, 51, 64, 100, 50, 58, 51, 40, 50, 80] + 65, 85, 67, 88, 74, 0.5, 105, 80, 105, 50, 80, 87, 81, 50, 95, 0, 80, 75, 50, 70, 80, 100, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 90, 80, 50, 51, 64, 100, 50, 58, 51, 40, 50, 80, 50] prog30 = [71, 90, 80, 75, 100, 80, 90, 102, 84, 78, 110, 77, 73, 78, 0, 99, 80, 66, 46, 93, 40, 83, - 80, 90, 93, 50, 96, 88, 99, 108, 85, 80, 50, 64, 65, 100, 100, 110, 80, 50, 74, 90, 80, 80, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51, 60, 53, 85, 58, 96, 47, 50, 90] + 80, 90, 93, 50, 96, 88, 99, 108, 85, 80, 50, 64, 65, 100, 100, 110, 80, 50, 74, 90, 80, 80, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51, 60, 53, 85, 58, 96, 47, 50, 90, 67] overdrive30 = [71, 90, 57, 75, 80, 80, 95, 79, 65, 31, 50, 69, 100, 68, 0, 78, 50, 70, 62, 59, 64, - 56, 73, 95, 67, 84, 80, 88, 79, 80, 60, 80, 80, 63, 35, 50, 82, 55, 50, 95, 55, 70, 100, 80, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 64, 65.5, 59.5, 58, 96, 47, 50, 64] + 56, 73, 95, 67, 84, 80, 88, 79, 80, 60, 80, 80, 63, 35, 50, 82, 55, 50, 95, 55, 70, 100, 80, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 64, 65.5, 59.5, 58, 96, 47, 50, 64, 90] char_type = [1, 0, 0, 0, 0, 0, 0, 2, 0, 1, 2, 0, 0, 0, 2, 3, 1, 0, 0, 0, 1, - 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 3, 0, 2, 2, 0, 0, 2, 0, 0, 2, 0, 2, 2, 1, 0, 2, 0, 0, 2] + 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 3, 0, 2, 2, 0, 0, 2, 0, 0, 2, 0, 2, 2, 1, 0, 2, 0, 0, 2, 0] char_core = { 0: [{'core_id': 'core_hollow', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}], @@ -70,7 +70,7 @@ class InitData: 'core_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful', 'core_course_skip_purchase', 'core_umbral', 'core_wacca', 'core_sunset', 'core_tanoc'] world_songs = ["babaroque", "shadesoflight", "kanagawa", "lucifer", "anokumene", "ignotus", "rabbitintheblackroom", "qualia", "redandblue", "bookmaker", "darakunosono", "espebranch", "blacklotus", "givemeanightmare", "vividtheory", "onefr", "gekka", "vexaria3", "infinityheaven3", "fairytale3", "goodtek3", "suomi", "rugie", "faintlight", "harutopia", "goodtek", "dreaminattraction", "syro", "diode", "freefall", "grimheart", "blaster", - "cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris', 'ultimatetaste', 'rgb', 'matenrou', 'dynitikos', 'amekagura', 'fantasy', 'aloneandlorn', 'felys', 'onandon', 'hotarubinoyuki', 'oblivia3', 'libertas3', 'einherjar3', 'purpleverse3', 'viciousheroism3', 'inkarusi3', 'cyberneciacatharsis3', 'alephzero'] + "cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris', 'ultimatetaste', 'rgb', 'matenrou', 'dynitikos', 'amekagura', 'fantasy', 'aloneandlorn', 'felys', 'onandon', 'hotarubinoyuki', 'oblivia3', 'libertas3', 'einherjar3', 'purpleverse3', 'viciousheroism3', 'inkarusi3', 'cyberneciacatharsis3', 'alephzero', 'hellohell', 'ichirin', 'awakeninruins', 'morningloom', 'lethalvoltage'] world_unlocks = ["scenery_chap1", "scenery_chap2", "scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7", "scenery_beyond"] diff --git a/latest version/database/init/packs.json b/latest version/database/init/packs.json index c5bf078..650680a 100644 --- a/latest version/database/init/packs.json +++ b/latest version/database/init/packs.json @@ -790,5 +790,23 @@ ], "orig_price": 500, "price": 500 + }, + { + "name": "extend_3", + "items": [ + { + "type": "pack", + "id": "extend_3", + "is_available": true + }, + { + "type": "core", + "amount": 7, + "id": "core_generic", + "is_available": true + } + ], + "price": 700, + "orig_price": 700 } ] \ No newline at end of file diff --git a/latest version/database/init/singles.json b/latest version/database/init/singles.json index 4f51364..91fc176 100644 --- a/latest version/database/init/singles.json +++ b/latest version/database/init/singles.json @@ -1684,5 +1684,23 @@ ], "orig_price": 100, "price": 100 + }, + { + "name": "mirinae", + "items": [ + { + "type": "single", + "id": "mirinae", + "is_available": true + }, + { + "type": "core", + "amount": 1, + "id": "core_generic", + "is_available": true + } + ], + "orig_price": 100, + "price": 100 } ] \ No newline at end of file From ab201c9d48f2560f69af526eafae16c97fe0f406 Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Sun, 7 Apr 2024 21:55:52 +0800 Subject: [PATCH 14/32] [Bug fix][Enhance] Songlist parser & Bunder version error message - For Arcaea 5.5.8 (no test) - New character "Ai-chan" - Add a small tool to send error message - A `songlist` parser problem #156 - Fix that header checker of bundle version gives incorrect error messages. --- latest version/core/constant.py | 2 +- latest version/core/download.py | 20 +++++----------- latest version/database/init/arc_data.py | 28 +++++++++++------------ latest version/database/init/singles.json | 18 +++++++++++++++ latest version/server/func.py | 2 +- tools/server_maintenance.py | 19 +++++++++++++++ 6 files changed, 59 insertions(+), 30 deletions(-) create mode 100644 tools/server_maintenance.py diff --git a/latest version/core/constant.py b/latest version/core/constant.py index eb54a25..d732102 100644 --- a/latest version/core/constant.py +++ b/latest version/core/constant.py @@ -1,6 +1,6 @@ from .config_manager import Config -ARCAEA_SERVER_VERSION = 'v2.11.3.8' +ARCAEA_SERVER_VERSION = 'v2.11.3.9' ARCAEA_LOG_DATBASE_VERSION = 'v1.1' diff --git a/latest version/core/download.py b/latest version/core/download.py index 448f6c8..724d7e3 100644 --- a/latest version/core/download.py +++ b/latest version/core/download.py @@ -29,15 +29,7 @@ class SonglistParser: has_songlist = False songs: dict = {} # {song_id: value, ...} - # value: bit 76543210 - # 7: video_audio.ogg - # 6: video.mp4 - # 5: 3.ogg - # 4: base.ogg - # 3: 3.aff - # 2: 2.aff - # 1: 1.aff - # 0: 0.aff + # value: bitmap pack_info: 'dict[str, set]' = {} # {pack_id: {song_id, ...}, ...} free_songs: set = set() # {song_id, ...} @@ -88,10 +80,10 @@ def parse_one(self, song: dict) -> dict: return {} r = 0 if 'remote_dl' in song and song['remote_dl']: - r |= 16 + r |= 32 for i in song.get('difficulties', []): if i['ratingClass'] == 3 and i.get('audioOverride', False): - r |= 32 + r |= 64 r |= 1 << i['ratingClass'] else: if any(i['ratingClass'] == 3 for i in song.get('difficulties', [])): @@ -99,14 +91,14 @@ def parse_one(self, song: dict) -> dict: for extra_file in song.get('additional_files', []): x = extra_file['file_name'] - if x == SonglistParser.FILE_NAMES[6]: - r |= 64 - elif x == SonglistParser.FILE_NAMES[7]: + if x == SonglistParser.FILE_NAMES[7]: r |= 128 elif x == SonglistParser.FILE_NAMES[8]: r |= 256 elif x == SonglistParser.FILE_NAMES[9]: r |= 512 + elif x == SonglistParser.FILE_NAMES[10]: + r |= 1024 return {song['id']: r} diff --git a/latest version/database/init/arc_data.py b/latest version/database/init/arc_data.py index dbb96e8..5fe5560 100644 --- a/latest version/database/init/arc_data.py +++ b/latest version/database/init/arc_data.py @@ -1,45 +1,45 @@ class InitData: char = ['hikari', 'tairitsu', 'kou', 'sapphire', 'lethe', 'hikari&tairitsu(reunion)', 'Tairitsu(Axium)', 'Tairitsu(Grievous Lady)', 'stella', 'Hikari & Fisica', 'ilith', 'eto', 'luna', 'shirabe', 'Hikari(Zero)', 'Hikari(Fracture)', 'Hikari(Summer)', 'Tairitsu(Summer)', 'Tairitsu & Trin', - 'ayu', 'Eto & Luna', 'yume', 'Seine & Hikari', 'saya', 'Tairitsu & Chuni Penguin', 'Chuni Penguin', 'haruna', 'nono', 'MTA-XXX', 'MDA-21', 'kanae', 'Hikari(Fantasia)', 'Tairitsu(Sonata)', 'sia', 'DORO*C', 'Tairitsu(Tempest)', 'brillante', 'Ilith(Summer)', 'etude', 'Alice & Tenniel', 'Luna & Mia', 'areus', 'seele', 'isabelle', 'mir', 'lagrange', 'linka', 'nami', 'Saya & Elizabeth', 'lily', 'kanae(midsummer)', 'alice&tenniel(minuet)', 'tairitsu(elegy)', 'marija', 'vita', 'hikari(fatalis)', 'saki', 'setsuna', 'amane', 'kou(winter)', 'lagrange(aria)', 'lethe(apophenia)', 'shama(UNiVERSE)', 'milk(UNiVERSE)', 'shikoku', 'mika yurisaki', 'Mithra Tercera', 'Toa Kozukata', 'Nami(Twilight)', 'Ilith & Ivy', 'Hikari & Vanessa', 'Maya', 'Insight(Ascendant - 8th Seeker)', 'Luin', 'Vita(Cadenza)'] + 'ayu', 'Eto & Luna', 'yume', 'Seine & Hikari', 'saya', 'Tairitsu & Chuni Penguin', 'Chuni Penguin', 'haruna', 'nono', 'MTA-XXX', 'MDA-21', 'kanae', 'Hikari(Fantasia)', 'Tairitsu(Sonata)', 'sia', 'DORO*C', 'Tairitsu(Tempest)', 'brillante', 'Ilith(Summer)', 'etude', 'Alice & Tenniel', 'Luna & Mia', 'areus', 'seele', 'isabelle', 'mir', 'lagrange', 'linka', 'nami', 'Saya & Elizabeth', 'lily', 'kanae(midsummer)', 'alice&tenniel(minuet)', 'tairitsu(elegy)', 'marija', 'vita', 'hikari(fatalis)', 'saki', 'setsuna', 'amane', 'kou(winter)', 'lagrange(aria)', 'lethe(apophenia)', 'shama(UNiVERSE)', 'milk(UNiVERSE)', 'shikoku', 'mika yurisaki', 'Mithra Tercera', 'Toa Kozukata', 'Nami(Twilight)', 'Ilith & Ivy', 'Hikari & Vanessa', 'Maya', 'Insight(Ascendant - 8th Seeker)', 'Luin', 'Vita(Cadenza)', 'Ai-chan'] skill_id = ['gauge_easy', '', '', '', 'note_mirror', 'skill_reunion', '', 'gauge_hard', 'frag_plus_10_pack_stellights', 'gauge_easy|frag_plus_15_pst&prs', 'gauge_hard|fail_frag_minus_100', 'frag_plus_5_side_light', 'visual_hide_hp', 'frag_plus_5_side_conflict', 'challenge_fullcombo_0gauge', 'gauge_overflow', 'gauge_easy|note_mirror', 'note_mirror', 'visual_tomato_pack_tonesphere', - 'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', '', '', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', '', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', '', '', 'visual_ghost_skynotes', 'skill_vita', 'skill_fatalis', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', '', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'skill_mithra', 'skill_toa', 'skill_nami_twilight', 'skill_ilith_ivy', 'skill_hikari_vanessa', 'skill_maya', 'skill_intruder', 'skill_luin', ''] + 'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', '', '', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', '', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', '', '', 'visual_ghost_skynotes', 'skill_vita', 'skill_fatalis', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', '', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'skill_mithra', 'skill_toa', 'skill_nami_twilight', 'skill_ilith_ivy', 'skill_hikari_vanessa', 'skill_maya', 'skill_intruder', 'skill_luin', '', 'skill_aichan'] skill_id_uncap = ['', '', 'frags_kou', '', 'visual_ink', '', '', '', '', '', 'ilith_awakened_skill', 'eto_uncap', 'luna_uncap', 'shirabe_entry_fee', - '', '', '', '', '', 'ayu_uncap', '', 'frags_yume', '', '', '', '', '', '', '', '', 'skill_kanae_uncap', '', '', '', 'skill_doroc_uncap', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'skill_luin_uncap', ''] + '', '', '', '', '', 'ayu_uncap', '', 'frags_yume', '', '', '', '', '', '', '', '', 'skill_kanae_uncap', '', '', '', 'skill_doroc_uncap', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'skill_luin_uncap', '', ''] skill_unlock_level = [0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, - 0, 0, 0, 8, 0, 14, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + 0, 0, 0, 8, 0, 14, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] frag1 = [55, 55, 60, 50, 47, 79, 47, 57, 41, 22, 50, 54, 60, 56, 78, 42, 41, 61, 52, 50, 52, 32, - 42, 55, 45, 58, 43, 0.5, 68, 50, 62, 45, 45, 52, 44, 27, 59, 0, 45, 50, 50, 47, 47, 61, 43, 42, 38, 25, 58, 50, 61, 45, 45, 38, 34, 27, 18, 56, 47, 30, 45, 57, 56, 47, 33, 26, 29, 66, 40, 33, 51, 27, 50, 60, 45] + 42, 55, 45, 58, 43, 0.5, 68, 50, 62, 45, 45, 52, 44, 27, 59, 0, 45, 50, 50, 47, 47, 61, 43, 42, 38, 25, 58, 50, 61, 45, 45, 38, 34, 27, 18, 56, 47, 30, 45, 57, 56, 47, 33, 26, 29, 66, 40, 33, 51, 27, 50, 60, 45, 50] prog1 = [35, 55, 47, 50, 60, 70, 60, 70, 58, 45, 70, 45, 42, 46, 61, 67, 49, 44, 28, 45, 24, 46, 52, - 59, 62, 33, 58, 25, 63, 69, 50, 45, 45, 51, 34, 70, 62, 70, 45, 32, 32, 61, 47, 47, 37, 42, 50, 50, 45, 41, 61, 45, 45, 58, 50, 130, 18, 57, 55, 50, 45, 70, 37.5, 29, 44, 26, 26, 35, 40, 33, 58, 31, 50, 50, 45] + 59, 62, 33, 58, 25, 63, 69, 50, 45, 45, 51, 34, 70, 62, 70, 45, 32, 32, 61, 47, 47, 37, 42, 50, 50, 45, 41, 61, 45, 45, 58, 50, 130, 18, 57, 55, 50, 45, 70, 37.5, 29, 44, 26, 26, 35, 40, 33, 58, 31, 50, 50, 45, 41] overdrive1 = [35, 55, 25, 50, 47, 70, 72, 57, 41, 7, 10, 32, 65, 31, 61, 53, 31, 47, 38, 12, 39, 18, - 48, 65, 45, 55, 44, 25, 46, 44, 33, 45, 45, 37, 25, 27, 50, 20, 45, 63, 21, 47, 61, 47, 65, 80, 38, 30, 49, 15, 34, 45, 45, 38, 67, 120, 44, 33, 55, 50, 45, 57, 31, 29, 65, 26, 29, 42.5, 40, 33, 58, 31, 50, 34, 45] + 48, 65, 45, 55, 44, 25, 46, 44, 33, 45, 45, 37, 25, 27, 50, 20, 45, 63, 21, 47, 61, 47, 65, 80, 38, 30, 49, 15, 34, 45, 45, 38, 67, 120, 44, 33, 55, 50, 45, 57, 31, 29, 65, 26, 29, 42.5, 40, 33, 58, 31, 50, 34, 45, 41] frag20 = [78, 80, 90, 75, 70, 79, 70, 79, 65, 40, 50, 80, 90, 82, 0, 61, 67, 92, 85, 50, 86, 52, - 65, 85, 67, 88, 64, 0.5, 95, 70, 95, 50, 80, 87, 71, 50, 85, 0, 80, 75, 50, 70, 70, 90, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 90, 80, 50, 51, 54, 100, 50, 58, 51, 40, 50, 70, 50] + 65, 85, 67, 88, 64, 0.5, 95, 70, 95, 50, 80, 87, 71, 50, 85, 0, 80, 75, 50, 70, 70, 90, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 90, 80, 50, 51, 54, 100, 50, 58, 51, 40, 50, 70, 50, 61.6] prog20 = [61, 80, 70, 75, 90, 70, 90, 102, 84, 78, 105, 67, 63, 68, 0, 99, 80, 66, 46, 83, 40, 73, - 80, 90, 93, 50, 86, 78, 89, 98, 75, 80, 50, 64, 55, 100, 90, 110, 80, 50, 74, 90, 70, 70, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51, 50, 53, 85, 58, 96, 47, 50, 80, 67] + 80, 90, 93, 50, 86, 78, 89, 98, 75, 80, 50, 64, 55, 100, 90, 110, 80, 50, 74, 90, 70, 70, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51, 50, 53, 85, 58, 96, 47, 50, 80, 67, 41] overdrive20 = [61, 80, 47, 75, 70, 70, 95, 79, 65, 31, 50, 59, 90, 58, 0, 78, 50, 70, 62, 49, 64, - 46, 73, 95, 67, 84, 70, 78, 69, 70, 50, 80, 80, 63, 25, 50, 72, 55, 50, 95, 55, 70, 90, 70, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 54, 65.5, 59.5, 58, 96, 47, 50, 54, 90] + 46, 73, 95, 67, 84, 70, 78, 69, 70, 50, 80, 80, 63, 25, 50, 72, 55, 50, 95, 55, 70, 90, 70, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 54, 65.5, 59.5, 58, 96, 47, 50, 54, 90, 41] frag30 = [88, 90, 100, 75, 80, 89, 70, 79, 65, 40, 50, 90, 100, 92, 0, 61, 67, 92, 85, 50, 86, 62, - 65, 85, 67, 88, 74, 0.5, 105, 80, 105, 50, 80, 87, 81, 50, 95, 0, 80, 75, 50, 70, 80, 100, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 90, 80, 50, 51, 64, 100, 50, 58, 51, 40, 50, 80, 50] + 65, 85, 67, 88, 74, 0.5, 105, 80, 105, 50, 80, 87, 81, 50, 95, 0, 80, 75, 50, 70, 80, 100, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 90, 80, 50, 51, 64, 100, 50, 58, 51, 40, 50, 80, 50, 61.6] prog30 = [71, 90, 80, 75, 100, 80, 90, 102, 84, 78, 110, 77, 73, 78, 0, 99, 80, 66, 46, 93, 40, 83, - 80, 90, 93, 50, 96, 88, 99, 108, 85, 80, 50, 64, 65, 100, 100, 110, 80, 50, 74, 90, 80, 80, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51, 60, 53, 85, 58, 96, 47, 50, 90, 67] + 80, 90, 93, 50, 96, 88, 99, 108, 85, 80, 50, 64, 65, 100, 100, 110, 80, 50, 74, 90, 80, 80, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51, 60, 53, 85, 58, 96, 47, 50, 90, 67, 41] overdrive30 = [71, 90, 57, 75, 80, 80, 95, 79, 65, 31, 50, 69, 100, 68, 0, 78, 50, 70, 62, 59, 64, - 56, 73, 95, 67, 84, 80, 88, 79, 80, 60, 80, 80, 63, 35, 50, 82, 55, 50, 95, 55, 70, 100, 80, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 64, 65.5, 59.5, 58, 96, 47, 50, 64, 90] + 56, 73, 95, 67, 84, 80, 88, 79, 80, 60, 80, 80, 63, 35, 50, 82, 55, 50, 95, 55, 70, 100, 80, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 64, 65.5, 59.5, 58, 96, 47, 50, 64, 90, 41] char_type = [1, 0, 0, 0, 0, 0, 0, 2, 0, 1, 2, 0, 0, 0, 2, 3, 1, 0, 0, 0, 1, - 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 3, 0, 2, 2, 0, 0, 2, 0, 0, 2, 0, 2, 2, 1, 0, 2, 0, 0, 2, 0] + 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 3, 0, 2, 2, 0, 0, 2, 0, 0, 2, 0, 2, 2, 1, 0, 2, 0, 0, 2, 0, 0] char_core = { 0: [{'core_id': 'core_hollow', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}], diff --git a/latest version/database/init/singles.json b/latest version/database/init/singles.json index 91fc176..b258f21 100644 --- a/latest version/database/init/singles.json +++ b/latest version/database/init/singles.json @@ -1702,5 +1702,23 @@ ], "orig_price": 100, "price": 100 + }, + { + "name": "ultradiaxon", + "items": [ + { + "type": "single", + "id": "ultradiaxon", + "is_available": true + }, + { + "type": "core", + "amount": 1, + "id": "core_generic", + "is_available": true + } + ], + "orig_price": 100, + "price": 100 } ] \ No newline at end of file diff --git a/latest version/server/func.py b/latest version/server/func.py index 0a3cd92..6aa7534 100644 --- a/latest version/server/func.py +++ b/latest version/server/func.py @@ -110,7 +110,7 @@ def header_check(request) -> ArcError: if Config.ALLOW_APPVERSION: # 版本检查 if 'AppVersion' not in headers or headers['AppVersion'] not in Config.ALLOW_APPVERSION: return LowVersion('Invalid app version', 5) - if 'ContentBundle' in headers and headers['ContentBundle'] != BundleParser.max_bundle_version[headers['AppVersion']]: + if 'ContentBundle' in headers and headers['ContentBundle'] != BundleParser.max_bundle_version.get(headers.get('AppVersion', ''), '0.0.0'): return LowVersion('Invalid content bundle version', 11) if has_arc_hash and not ArcHashChecker(request).check(): diff --git a/tools/server_maintenance.py b/tools/server_maintenance.py new file mode 100644 index 0000000..8101705 --- /dev/null +++ b/tools/server_maintenance.py @@ -0,0 +1,19 @@ +from flask import Flask, jsonify + +app = Flask(__name__) + +HOST = '0.0.0.0' +PORT = '80' + +@app.route('/favicon.ico', methods=['GET']) +def favicon(): + return '' + +@app.route('/') +def hello(p): + r = {"success": False, "error_code": 2} + return jsonify(r) + + +if __name__ == '__main__': + app.run(host=HOST, port=PORT) From 2d498ae02b021f7fbb7e7f38866942bb3e48bd31 Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Fri, 12 Apr 2024 16:16:35 +0800 Subject: [PATCH 15/32] [Enhance] Link Play Unlock for ETR - Add support for the new client with ETR difficulty in Link Play #160 --- latest version/core/item.py | 3 ++- latest version/core/linkplay.py | 32 ++++++++------------------------ latest version/core/score.py | 2 +- 3 files changed, 11 insertions(+), 26 deletions(-) diff --git a/latest version/core/item.py b/latest version/core/item.py index fcf5bd9..24278f4 100644 --- a/latest version/core/item.py +++ b/latest version/core/item.py @@ -1,5 +1,6 @@ from .config_manager import Config -from .error import DataExist, InputError, ItemNotEnough, ItemUnavailable, NoData +from .error import (DataExist, InputError, ItemNotEnough, ItemUnavailable, + NoData) class Item: diff --git a/latest version/core/linkplay.py b/latest version/core/linkplay.py index 29bccf2..9184512 100644 --- a/latest version/core/linkplay.py +++ b/latest version/core/linkplay.py @@ -11,33 +11,17 @@ socket.setdefaulttimeout(Constant.LINKPLAY_TIMEOUT) -def get_song_unlock(client_song_map: dict) -> bytes: +def get_song_unlock(client_song_map: 'dict[str, list]') -> bytes: '''处理可用歌曲bit,返回bytes''' user_song_unlock = [0] * Constant.LINKPLAY_UNLOCK_LENGTH - for i in range(0, Constant.LINKPLAY_UNLOCK_LENGTH*2, 2): - x = 0 - y = 0 - if str(i) in client_song_map: - if client_song_map[str(i)][0]: - x += 1 - if client_song_map[str(i)][1]: - x += 2 - if client_song_map[str(i)][2]: - x += 4 - if client_song_map[str(i)][3]: - x += 8 - if str(i+1) in client_song_map: - if client_song_map[str(i+1)][0]: - y += 1 - if client_song_map[str(i+1)][1]: - y += 2 - if client_song_map[str(i+1)][2]: - y += 4 - if client_song_map[str(i+1)][3]: - y += 8 - - user_song_unlock[i // 2] = y*16 + x + + for k, v in client_song_map.items(): + for i in range(5): + if not v[i]: + continue + index = int(k) * 5 + i + user_song_unlock[index // 8] |= 1 << (index % 8) return bytes(user_song_unlock) diff --git a/latest version/core/score.py b/latest version/core/score.py index 4f37ca5..5ad712e 100644 --- a/latest version/core/score.py +++ b/latest version/core/score.py @@ -11,7 +11,7 @@ from .song import Chart from .sql import Connect, Query, Sql from .util import get_today_timestamp, md5 -from .world import WorldPlay, BeyondWorldPlay, BreachedWorldPlay +from .world import BeyondWorldPlay, BreachedWorldPlay, WorldPlay class Score: From efedd969086b6ef9699a0a043b989668c9964204 Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Fri, 26 Apr 2024 17:55:29 +0800 Subject: [PATCH 16/32] [Enhance][Bug fix] Another bundle mode & skill_amane bug - For Arcaea 5.6.0 - Fix a bug that `skill_amane` may arise error when the step type of world map is null. - Add a new bundle update mode, which is same with official server, that the server will find a update path from old version to new version, ignoring the application version restrictions. --- .gitignore | 6 +++ latest version/core/bundle.py | 65 ++++++++++++++++++++--- latest version/core/config_manager.py | 2 + latest version/core/constant.py | 2 +- latest version/core/world.py | 2 +- latest version/database/init/arc_data.py | 2 +- latest version/database/init/singles.json | 54 +++++++++++++++++++ 7 files changed, 124 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 7d6a9df..63ec578 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,9 @@ latest version/config.py # song data latest version/database/songs/ +latest version/database/bundle/ + +!latest version/database/bundle/README.md + +# backup +latest version/database/backup/ diff --git a/latest version/core/bundle.py b/latest version/core/bundle.py index c4667c9..01a230c 100644 --- a/latest version/core/bundle.py +++ b/latest version/core/bundle.py @@ -1,11 +1,13 @@ import json import os +from functools import lru_cache from time import time from flask import url_for +from .config_manager import Config from .constant import Constant -from .error import NoAccess, RateLimit +from .error import NoAccess, NoData, RateLimit from .limiter import ArcLimiter @@ -44,6 +46,8 @@ def from_json(cls, json_data: dict) -> 'ContentBundle': x.prev_version = json_data['previousVersionNumber'] x.app_version = json_data['applicationVersionNumber'] x.uuid = json_data['uuid'] + if x.prev_version is None: + x.prev_version = '0.0.0' return x def to_dict(self) -> dict: @@ -69,8 +73,14 @@ class BundleParser: # {app_version: [ List[ContentBundle] ]} bundles: 'dict[str, list[ContentBundle]]' = {} + # {app_version: max bundle version} max_bundle_version: 'dict[str, str]' = {} + # {bundle version: [next versions]} 宽搜索引 + next_versions: 'dict[str, list[str]]' = {} + # {(bver, b prev version): ContentBundle} 正向索引 + version_tuple_bundles: 'dict[tuple[str, str], ContentBundle]' = {} + def __init__(self) -> None: self.parse() @@ -106,15 +116,57 @@ def parse(self) -> None: f'Bundle file not found: {bundle_path}') x.calculate_size() - if x.app_version not in self.bundles: - self.bundles[x.app_version] = [] - self.bundles[x.app_version].append(x) + self.bundles.setdefault(x.app_version, []).append(x) + + self.version_tuple_bundles[(x.version, x.prev_version)] = x + self.next_versions.setdefault( + x.prev_version, []).append(x.version) # sort by version for k, v in self.bundles.items(): v.sort(key=lambda x: x.version_tuple) self.max_bundle_version[k] = v[-1].version + @staticmethod + @lru_cache(maxsize=128) + def get_bundles(app_ver: str, b_ver: str) -> 'list[ContentBundle]': + if Config.BUNDLE_STRICT_MODE: + return BundleParser.bundles.get(app_ver, []) + + k = b_ver if b_ver else '0.0.0' + + target_version = BundleParser.max_bundle_version.get(app_ver, '0.0.0') + if k == target_version: + return [] + + # BFS + q = [[k]] + ans = None + while True: + qq = [] + for x in q: + if x[-1] == target_version: + ans = x + break + for y in BundleParser.next_versions.get(x[-1], []): + if y in x: + continue + qq.append(x + [y]) + + if ans is not None or not qq: + break + q = qq + + if not ans: + raise NoData( + f'No bundles found for app version: {app_ver}, bundle version: {b_ver}', status=404) + + r = [] + for i in range(1, len(ans)): + r.append(BundleParser.version_tuple_bundles[(ans[i], ans[i-1])]) + + return r + class BundleDownload: @@ -134,8 +186,9 @@ def set_client_info(self, app_version: str, bundle_version: str, device_id: str self.device_id = device_id def get_bundle_list(self) -> list: - bundles: 'list[ContentBundle]' = BundleParser.bundles.get( - self.client_app_version, []) + bundles: 'list[ContentBundle]' = BundleParser.get_bundles( + self.client_app_version, self.client_bundle_version) + if not bundles: return [] diff --git a/latest version/core/config_manager.py b/latest version/core/config_manager.py index ea12d00..2264f62 100644 --- a/latest version/core/config_manager.py +++ b/latest version/core/config_manager.py @@ -16,6 +16,8 @@ class Config: OLD_GAME_API_PREFIX = [] # str | list[str] ALLOW_APPVERSION = [] # list[str] + + BUNDLE_STRICT_MODE = True SET_LINKPLAY_SERVER_AS_SUB_PROCESS = True diff --git a/latest version/core/constant.py b/latest version/core/constant.py index d732102..988af3b 100644 --- a/latest version/core/constant.py +++ b/latest version/core/constant.py @@ -1,6 +1,6 @@ from .config_manager import Config -ARCAEA_SERVER_VERSION = 'v2.11.3.9' +ARCAEA_SERVER_VERSION = 'v2.11.3.10' ARCAEA_LOG_DATBASE_VERSION = 'v1.1' diff --git a/latest version/core/world.py b/latest version/core/world.py index c5414dd..d1e934e 100644 --- a/latest version/core/world.py +++ b/latest version/core/world.py @@ -86,7 +86,7 @@ def from_dict(self, d: dict) -> 'Step': self.restrict_ids = d.get('restrict_ids') self.restrict_type = d.get('restrict_type') self.restrict_difficulty = d.get('restrict_difficulty') - self.step_type = d.get('step_type') + self.step_type = d.get('step_type', []) self.speed_limit_value = d.get('speed_limit_value') self.plus_stamina_value = d.get('plus_stamina_value') if 'items' in d: diff --git a/latest version/database/init/arc_data.py b/latest version/database/init/arc_data.py index 5fe5560..54c1e34 100644 --- a/latest version/database/init/arc_data.py +++ b/latest version/database/init/arc_data.py @@ -70,7 +70,7 @@ class InitData: 'core_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful', 'core_course_skip_purchase', 'core_umbral', 'core_wacca', 'core_sunset', 'core_tanoc'] world_songs = ["babaroque", "shadesoflight", "kanagawa", "lucifer", "anokumene", "ignotus", "rabbitintheblackroom", "qualia", "redandblue", "bookmaker", "darakunosono", "espebranch", "blacklotus", "givemeanightmare", "vividtheory", "onefr", "gekka", "vexaria3", "infinityheaven3", "fairytale3", "goodtek3", "suomi", "rugie", "faintlight", "harutopia", "goodtek", "dreaminattraction", "syro", "diode", "freefall", "grimheart", "blaster", - "cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris', 'ultimatetaste', 'rgb', 'matenrou', 'dynitikos', 'amekagura', 'fantasy', 'aloneandlorn', 'felys', 'onandon', 'hotarubinoyuki', 'oblivia3', 'libertas3', 'einherjar3', 'purpleverse3', 'viciousheroism3', 'inkarusi3', 'cyberneciacatharsis3', 'alephzero', 'hellohell', 'ichirin', 'awakeninruins', 'morningloom', 'lethalvoltage'] + "cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris', 'ultimatetaste', 'rgb', 'matenrou', 'dynitikos', 'amekagura', 'fantasy', 'aloneandlorn', 'felys', 'onandon', 'hotarubinoyuki', 'oblivia3', 'libertas3', 'einherjar3', 'purpleverse3', 'viciousheroism3', 'inkarusi3', 'cyberneciacatharsis3', 'alephzero', 'hellohell', 'ichirin', 'awakeninruins', 'morningloom', 'lethalvoltage', 'leaveallbehind', 'desive'] world_unlocks = ["scenery_chap1", "scenery_chap2", "scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7", "scenery_beyond"] diff --git a/latest version/database/init/singles.json b/latest version/database/init/singles.json index b258f21..cef6ad3 100644 --- a/latest version/database/init/singles.json +++ b/latest version/database/init/singles.json @@ -1720,5 +1720,59 @@ ], "orig_price": 100, "price": 100 + }, + { + "name": "distortedfate", + "items": [ + { + "type": "single", + "id": "distortedfate", + "is_available": true + }, + { + "type": "core", + "amount": 1, + "id": "core_generic", + "is_available": true + } + ], + "orig_price": 100, + "price": 100 + }, + { + "name": "floatingworld", + "items": [ + { + "type": "single", + "id": "floatingworld", + "is_available": true + }, + { + "type": "core", + "amount": 1, + "id": "core_generic", + "is_available": true + } + ], + "orig_price": 100, + "price": 100 + }, + { + "name": "chromafill", + "items": [ + { + "type": "single", + "id": "chromafill", + "is_available": true + }, + { + "type": "core", + "amount": 1, + "id": "core_generic", + "is_available": true + } + ], + "orig_price": 100, + "price": 100 } ] \ No newline at end of file From 5c539bdf5946eb72376d3e27305a8dffe0f6fab8 Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Tue, 30 Apr 2024 00:27:23 +0800 Subject: [PATCH 17/32] [Enhance] Recent 30 - Update Recent 30 mechanism. - Alter Recent 30 table structure. Note: 1. This is a TEST version. Maybe there are many bugs. 2. This special version is a line of demarcation. --- latest version/core/constant.py | 3 +- latest version/core/init.py | 13 +- latest version/core/score.py | 192 +++++++++++------------- latest version/core/song.py | 3 + latest version/core/sql.py | 60 +++++++- latest version/core/user.py | 3 +- latest version/core/util.py | 9 +- latest version/database/init/tables.sql | 77 ++-------- latest version/web/index.py | 20 ++- latest version/web/webscore.py | 36 +---- 10 files changed, 201 insertions(+), 215 deletions(-) diff --git a/latest version/core/constant.py b/latest version/core/constant.py index 988af3b..f9c1a87 100644 --- a/latest version/core/constant.py +++ b/latest version/core/constant.py @@ -1,6 +1,7 @@ from .config_manager import Config -ARCAEA_SERVER_VERSION = 'v2.11.3.10' +ARCAEA_SERVER_VERSION = 'v2.11.3.11' +ARCAEA_DATABASE_VERSION = 'v2.11.3.11' ARCAEA_LOG_DATBASE_VERSION = 'v1.1' diff --git a/latest version/core/init.py b/latest version/core/init.py index a8152a9..cf0b8c4 100644 --- a/latest version/core/init.py +++ b/latest version/core/init.py @@ -8,7 +8,7 @@ from core.bundle import BundleParser from core.config_manager import Config -from core.constant import ARCAEA_LOG_DATBASE_VERSION, ARCAEA_SERVER_VERSION +from core.constant import ARCAEA_DATABASE_VERSION, ARCAEA_LOG_DATBASE_VERSION from core.course import Course from core.download import DownloadList from core.purchase import Purchase @@ -46,7 +46,7 @@ def table_init(self) -> None: with open(self.sql_path, 'r', encoding='utf-8') as f: self.c.executescript(f.read()) self.c.execute('''insert into config values("version", :a);''', { - 'a': ARCAEA_SERVER_VERSION}) + 'a': ARCAEA_DATABASE_VERSION}) def character_init(self) -> None: '''初始化搭档信息''' @@ -141,8 +141,7 @@ def admin_init(self) -> None: character_id, is_skill_sealed, is_char_uncapped, is_char_uncapped_override, is_hide_rating, favorite_character, max_stamina_notification_enabled, current_map, ticket, prog_boost, email) values(:user_id, :name, :password, :join_date, :user_code, 0, 0, 0, 0, 0, 0, -1, 0, '', :memories, 0, :email) ''', {'user_code': x.user_code, 'user_id': x.user_id, 'join_date': now, 'name': x.name, 'password': '41e5653fc7aeb894026d6bb7b2db7f65902b454945fa8fd65a6327047b5277fb', 'memories': 114514, 'email': x.email}) - self.c.execute('''insert into recent30(user_id) values(:user_id)''', { - 'user_id': x.user_id}) + self.c.execute( '''insert into user_role values(?, "admin")''', (x.user_id,)) @@ -175,7 +174,7 @@ def table_init(self) -> None: with open(self.sql_path, 'r') as f: self.c.executescript(f.read()) self.c.execute( - '''insert into cache values("version", :a, -1);''', {'a': ARCAEA_SERVER_VERSION}) + '''insert into cache values("version", :a, -1);''', {'a': ARCAEA_LOG_DATBASE_VERSION}) def init(self) -> None: with Connect(self.db_path) as c: @@ -270,12 +269,12 @@ def _check_update_database_main(self, db_path=Config.SQLITE_DATABASE_PATH, init_ except: x = None # 数据库自动更新,不强求 - if not x or x[0] != ARCAEA_SERVER_VERSION: + if not x or x[0] != ARCAEA_DATABASE_VERSION: self.logger.warning( f'Maybe the file `{db_path}` is an old version. Version: {x[0] if x else "None"}') try: self.logger.info( - f'Try to update the file `{db_path}` to version {ARCAEA_SERVER_VERSION}.') + f'Try to update the file `{db_path}` to version {ARCAEA_DATABASE_VERSION}.') if not os.path.isdir(Config.SQLITE_DATABASE_BACKUP_FOLDER_PATH): os.makedirs(Config.SQLITE_DATABASE_BACKUP_FOLDER_PATH) diff --git a/latest version/core/score.py b/latest version/core/score.py index 5ad712e..485e480 100644 --- a/latest version/core/score.py +++ b/latest version/core/score.py @@ -385,53 +385,9 @@ def update_play_state_for_course(self) -> None: (self.course_play_state, self.course_play.score, self.course_play.clear_type, self.song_token)) def clear_play_state(self) -> None: - self.c.execute('''delete from songplay_token where user_id=:a ''', { + self.c.execute('''delete from songplay_token where user_id=:a''', { 'a': self.user.user_id}) - def update_recent30(self) -> None: - '''更新此分数对应用户的recent30''' - old_recent_10 = self.ptt.recent_10 - if self.is_protected: - old_r30 = self.ptt.r30.copy() - old_s30 = self.ptt.s30.copy() - - # 寻找pop位置 - songs = list(set(self.ptt.s30)) - if '' in self.ptt.s30: - r30_id = 29 - else: - n = len(songs) - if n >= 11: - r30_id = 29 - elif self.song.song_id_difficulty not in songs and n == 10: - r30_id = 29 - elif self.song.song_id_difficulty in songs and n == 10: - i = 29 - while self.ptt.s30[i] == self.song.song_id_difficulty and i > 0: - i -= 1 - r30_id = i - elif self.song.song_id_difficulty not in songs and n == 9: - i = 29 - while self.ptt.s30.count(self.ptt.s30[i]) == 1 and i > 0: - i -= 1 - r30_id = i - else: - r30_id = 29 - - self.ptt.recent_30_update( - r30_id, self.rating, self.song.song_id_difficulty) - if self.is_protected and old_recent_10 > self.ptt.recent_10: - if self.song.song_id_difficulty in old_s30: - # 发现重复歌曲,更新到最高rating - index = old_s30.index(self.song.song_id_difficulty) - if old_r30[index] < self.rating: - old_r30[index] = self.rating - - self.ptt.r30 = old_r30 - self.ptt.s30 = old_s30 - - self.ptt.insert_recent_30() - def record_score(self) -> None: '''向log数据库记录分数,请注意列名不同''' logdb_execute('''insert into user_score values(?,?,?,?,?,?,?,?,?,?,?,?,?)''', (self.user.user_id, self.song.song_id, self.song.difficulty, self.time_played, @@ -490,7 +446,7 @@ def upload_score(self) -> None: self.ptt = Potential(self.c, self.user) if not self.unrank_flag: - self.update_recent30() + self.ptt.r30_push_score(self) # 总PTT更新 user_rating_ptt = self.ptt.value @@ -527,9 +483,8 @@ def __init__(self, c=None, user=None): self.c = c self.user = user - self.r30: 'list[float]' = None - self.s30: 'list[str]' = None - self.songs_selected: list = None + self.r30_tuples: 'list[tuple[int, str, int, float]]' = None + self.r30: 'list[Score]' = None self.b30: list = None @@ -545,75 +500,110 @@ def best_30(self) -> float: 'a': self.user.user_id}) return sum(x[0] for x in self.c.fetchall()) - def select_recent_30(self) -> None: + def select_recent_30_tuple(self) -> None: '''获取用户recent30数据''' self.c.execute( - '''select * from recent30 where user_id = :a''', {'a': self.user.user_id}) - x = self.c.fetchone() - if not x: - raise NoData( - f'No recent30 data for user `{self.user.user_id}`', api_error_code=-3) + '''select r_index, song_id, difficulty, rating from recent30 where user_id = ? order by time_played DESC''', (self.user.user_id,)) + + self.r30_tuples = [x for x in self.c.fetchall() if x[1] != ''] + + def select_recent_30(self) -> None: + self.c.execute( + '''select song_id, difficulty, score, shiny_perfect_count, perfect_count, near_count, miss_count, health, modifier, time_played, clear_type, rating from recent30 where user_id = ? order by time_played DESC''', (self.user.user_id,)) + self.r30 = [] - self.s30 = [] - if not x: - return None - for i in range(1, 61, 2): - if x[i] is not None: - self.r30.append(float(x[i])) - self.s30.append(x[i+1]) - else: - self.r30.append(0) - self.s30.append('') + for x in self.c.fetchall(): + if x[0] == '': + continue + s = Score() + s.song.set_chart(x[0], x[1]) + s.set_score(*x[2:-1]) + s.rating = x[-1] + self.r30.append(s) @property def recent_10(self) -> float: '''获取用户recent10的总潜力值''' - if self.r30 is None: - self.select_recent_30() + if self.r30_tuples is None: + self.select_recent_30_tuple() - rating_sum = 0 - r30, s30 = (list(t) for t in zip( - *sorted(zip(self.r30, self.s30), reverse=True))) + max_dict = {} + for x in self.r30_tuples: + if (x[1], x[2]) not in max_dict or max_dict[(x[1], x[2])] < x[3]: + max_dict[(x[1], x[2])] = x[3] - self.songs_selected = [] - i = 0 - while len(self.songs_selected) < 10 and i <= 29 and s30[i] != '' and s30[i] is not None: - if s30[i] not in self.songs_selected: - rating_sum += r30[i] - self.songs_selected.append(s30[i]) - i += 1 - return rating_sum + top_10_rating = sorted(max_dict.values(), reverse=True)[:10] + return sum(top_10_rating) def recent_30_to_dict_list(self) -> list: if self.r30 is None: self.select_recent_30() - r = [] - for x, y in zip(self.s30, self.r30): - if x: - r.append({ - 'song_id': x[:-1], - 'difficulty': int(x[-1]), - 'rating': y - }) - return r - def recent_30_update(self, pop_index: int, rating: float, song_id_difficulty: str) -> None: - self.r30.pop(pop_index) - self.s30.pop(pop_index) - self.r30.insert(0, rating) - self.s30.insert(0, song_id_difficulty) + return [x.to_dict() for x in self.r30] + + def update_one_r30(self, r_index: int, user_score: 'UserPlay | UserScore') -> None: + '''更新数据表中的一条数据''' + self.c.execute('''insert or replace into recent30 values(?,?,?,?,?,?,?,?,?,?,?,?,?,?)''', + (self.user.user_id, r_index, user_score.time_played, user_score.song.song_id, user_score.song.difficulty, + user_score.score, user_score.shiny_perfect_count, user_score.perfect_count, user_score.near_count, user_score.miss_count, user_score.health, user_score.modifier, user_score.clear_type, user_score.rating)) + + def r30_push_score(self, user_score: 'UserPlay | UserScore') -> None: + '''根据新成绩调整 r30''' + if self.r30_tuples is None: + self.select_recent_30_tuple() + + if len(self.r30_tuples) < 30: + self.update_one_r30(len(self.r30_tuples), user_score) + return None + + if user_score.is_protected: + # 保护,替换最低的最旧的成绩 + f_tuples = list( + filter(lambda x: x[-1] <= user_score.rating, self.r30_tuples)) + f_tuples.reverse() # 从旧到新 + f_tuples = sorted(f_tuples, key=lambda x: x[-1]) + if not f_tuples: + # 找不到更低的成绩,不更新 + return None + + unique_songs: 'dict[tuple[str, int], list[tuple[int, int, float]]]' = {} + for i, x in enumerate(self.r30_tuples): + unique_songs.setdefault((x[1], x[2]), []).append((i, x[0], x[3])) + + new_song = user_score.song.to_tuple() - def insert_recent_30(self) -> None: - '''更新r30表数据''' - sql = '''update recent30 set r0=?,song_id0=?,r1=?,song_id1=?,r2=?,song_id2=?,r3=?,song_id3=?,r4=?,song_id4=?,r5=?,song_id5=?,r6=?,song_id6=?,r7=?,song_id7=?,r8=?,song_id8=?,r9=?,song_id9=?,r10=?,song_id10=?,r11=?,song_id11=?,r12=?,song_id12=?,r13=?,song_id13=?,r14=?,song_id14=?,r15=?,song_id15=?,r16=?,song_id16=?,r17=?,song_id17=?,r18=?,song_id18=?,r19=?,song_id19=?,r20=?,song_id20=?,r21=?,song_id21=?,r22=?,song_id22=?,r23=?,song_id23=?,r24=?,song_id24=?,r25=?,song_id25=?,r26=?,song_id26=?,r27=?,song_id27=?,r28=?,song_id28=?,r29=?,song_id29=? where user_id=?''' - sql_list = [] - for i in range(30): - sql_list.append(self.r30[i]) - sql_list.append(self.s30[i]) + if len(unique_songs) >= 11 or (len(unique_songs) == 10 and new_song not in unique_songs): + if user_score.is_protected: + # 保护,替换最低的最旧的成绩 + self.update_one_r30(f_tuples[0][0], user_score) + else: + self.update_one_r30(self.r30_tuples[-1][0], user_score) + return None - sql_list.append(self.user.user_id) + filtered_songs = dict(filter(lambda x: len( + x[1]) > 1, unique_songs.items())) # 过滤掉只有单个成绩的 - self.c.execute(sql, sql_list) + if new_song in unique_songs and new_song not in filtered_songs: + # 如果新成绩有相同谱面的唯一成绩在 r30 中,则它也应该有可能被替换 + filtered_songs[new_song] = unique_songs[new_song] + + if user_score.is_protected: + # 保护,替换最低的最旧的成绩,此时需在 filtered_songs 中 + for x in f_tuples: + if (x[1], x[2]) in filtered_songs: + self.update_one_r30(x[0], user_score) + return None + else: + # 找到符合条件的最旧成绩 + max_idx = -1 + max_r_index = -1 + for x in filtered_songs.values(): + for y in x: + if y[0] > max_idx: + max_idx = y[0] + max_r_index = y[1] + + self.update_one_r30(max_r_index, user_score) class UserScoreList: diff --git a/latest version/core/song.py b/latest version/core/song.py index d6af16f..8571a45 100644 --- a/latest version/core/song.py +++ b/latest version/core/song.py @@ -18,6 +18,9 @@ def to_dict(self) -> dict: 'difficulty': self.difficulty, 'chart_const': self.chart_const } + + def to_tuple(self) -> tuple: + return (self.song_id, self.difficulty) @property def chart_const(self) -> float: diff --git a/latest version/core/sql.py b/latest version/core/sql.py index ee4388f..ae7784c 100644 --- a/latest version/core/sql.py +++ b/latest version/core/sql.py @@ -6,6 +6,7 @@ from .config_manager import Config from .constant import ARCAEA_LOG_DATBASE_VERSION, Constant from .error import ArcError, InputError +from .util import parse_version class Connect: @@ -349,10 +350,19 @@ def get_table_info(self, table_name: str): class DatabaseMigrator: + SPECIAL_UPDATE_VERSION = { + '2.11.3.11': '_version_2_11_3_11' + } + def __init__(self, c1_path: str, c2_path: str) -> None: self.c1_path = c1_path self.c2_path = c2_path + self.c1 = None + self.c2 = None + + self.tables = Constant.DATABASE_MIGRATE_TABLES + @staticmethod def update_one_table(c1, c2, table_name: str) -> bool: '''从c1向c2更新数据表,c1中存在的信息不变,即c2中的冲突信息会被覆盖''' @@ -397,7 +407,11 @@ def update_database(self) -> None: ''' with Connect(self.c2_path) as c2: with Connect(self.c1_path) as c1: - for i in Constant.DATABASE_MIGRATE_TABLES: + self.c1 = c1 + self.c2 = c2 + self.special_update() + + for i in self.tables: self.update_one_table(c1, c2, i) if not Constant.UPDATE_WITH_NEW_CHARACTER_DATA: @@ -405,6 +419,50 @@ def update_database(self) -> None: self.update_user_char_full(c2) # 更新user_char_full + def special_update(self): + old_version = self.c1.execute( + '''select value from config where id = "version"''').fetchone() + new_version = self.c2.execute( + '''select value from config where id = "version"''').fetchone() + old_version = old_version[0] if old_version else '0.0.0' + new_version = new_version[0] if new_version else '0.0.0' + old_version = parse_version(old_version) + new_version = parse_version(new_version) + + for k, v in self.SPECIAL_UPDATE_VERSION.items(): + if old_version < parse_version(k) <= new_version: + getattr(self, v)() + + def _version_2_11_3_11(self): + ''' + 2.11.3.11 版本特殊更新,调整 recent30 表结构 + recent30 表从 (user_id: int PK, song_id: text, rating: real, ...) \ + 更改为 (user_id: int PK, r_index: int PK, time_played: int, song_id: text, difficulty: int, score: int, sp, p, n, m, hp, mod, clear_type, rating: real) + ''' + + self.tables = [x for x in self.tables if x != 'recent30'] + + x = self.c1.execute('''select * from recent30''') + sql_list = [] + for i in x: + user_id = int(i[0]) + for j in range(30): + rating = i[1 + j * 2] + rating = float(rating) if rating else 0 + song_id_difficulty = i[2 + j * 2] + if song_id_difficulty: + song_id = song_id_difficulty[:-1] + difficulty = int(song_id_difficulty[-1]) + else: + song_id = '' + difficulty = 0 + + sql_list.append( + (user_id, j, 100-j, song_id, difficulty, rating)) + + self.c2.executemany( + '''insert into recent30(user_id, r_index, time_played, song_id, difficulty, rating) values(?,?,?,?,?,?)''', sql_list) + class LogDatabaseMigrator: diff --git a/latest version/core/user.py b/latest version/core/user.py index 7b93d25..914f154 100644 --- a/latest version/core/user.py +++ b/latest version/core/user.py @@ -153,8 +153,7 @@ def register(self): character_id, is_skill_sealed, is_char_uncapped, is_char_uncapped_override, is_hide_rating, favorite_character, max_stamina_notification_enabled, current_map, ticket, prog_boost, email) values(:user_id, :name, :password, :join_date, :user_code, 0, 0, 0, 0, 0, 0, -1, 0, '', :memories, 0, :email) ''', {'user_code': self.user_code, 'user_id': self.user_id, 'join_date': now, 'name': self.name, 'password': self.hash_pwd, 'memories': Config.DEFAULT_MEMORIES, 'email': self.email}) - self.c.execute('''insert into recent30(user_id) values(:user_id)''', { - 'user_id': self.user_id}) + class UserLogin(User): diff --git a/latest version/core/util.py b/latest version/core/util.py index 24c0021..b0ed808 100644 --- a/latest version/core/util.py +++ b/latest version/core/util.py @@ -1,9 +1,10 @@ import hashlib import os -from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from datetime import date from time import mktime +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + def aes_gcm_128_encrypt(key, plaintext, associated_data): iv = os.urandom(12) @@ -65,3 +66,9 @@ def try_rename(path: str, new_path: str) -> str: def get_today_timestamp(): '''相对于本机本地时间的今天0点的时间戳''' return int(mktime(date.today().timetuple())) + + +def parse_version(s: str) -> 'list[int]': + '''解析版本号''' + s_number = "".join(x for x in s if x.isdigit() or x == '.') + return list(map(int, [x for x in s_number.split('.') if x != ''])) diff --git a/latest version/database/init/tables.sql b/latest version/database/init/tables.sql index cbabb07..88c8e17 100644 --- a/latest version/database/init/tables.sql +++ b/latest version/database/init/tables.sql @@ -107,67 +107,22 @@ type text, amount int, primary key(character_id, item_id, type) ); -create table if not exists recent30(user_id int primary key, -r0 real, -song_id0 text, -r1 real, -song_id1 text, -r2 real, -song_id2 text, -r3 real, -song_id3 text, -r4 real, -song_id4 text, -r5 real, -song_id5 text, -r6 real, -song_id6 text, -r7 real, -song_id7 text, -r8 real, -song_id8 text, -r9 real, -song_id9 text, -r10 real, -song_id10 text, -r11 real, -song_id11 text, -r12 real, -song_id12 text, -r13 real, -song_id13 text, -r14 real, -song_id14 text, -r15 real, -song_id15 text, -r16 real, -song_id16 text, -r17 real, -song_id17 text, -r18 real, -song_id18 text, -r19 real, -song_id19 text, -r20 real, -song_id20 text, -r21 real, -song_id21 text, -r22 real, -song_id22 text, -r23 real, -song_id23 text, -r24 real, -song_id24 text, -r25 real, -song_id25 text, -r26 real, -song_id26 text, -r27 real, -song_id27 text, -r28 real, -song_id28 text, -r29 real, -song_id29 text +create table if not exists recent30( +user_id int, +r_index int, +time_played int, +song_id text, +difficulty int, +score int default 0, +shiny_perfect_count int default 0, +perfect_count int default 0, +near_count int default 0, +miss_count int default 0, +health int default 0, +modifier int default 0, +clear_type int default 0, +rating real default 0, +primary key(user_id, r_index) ); create table if not exists user_world(user_id int, map_id text, diff --git a/latest version/web/index.py b/latest version/web/index.py index d908402..cd8e18e 100644 --- a/latest version/web/index.py +++ b/latest version/web/index.py @@ -1,16 +1,19 @@ import os import time -from core.init import FileChecker -from core.operation import RefreshAllScoreRating, RefreshBundleCache, RefreshSongFileCache, SaveUpdateScore, UnlockUserItem, DeleteUserScore -from core.rank import RankList -from core.sql import Connect -from core.user import User from flask import Blueprint, flash, redirect, render_template, request, url_for from werkzeug.utils import secure_filename import web.system import web.webscore +from core.init import FileChecker +from core.operation import (DeleteUserScore, RefreshAllScoreRating, + RefreshBundleCache, RefreshSongFileCache, + SaveUpdateScore, UnlockUserItem) +from core.rank import RankList +from core.score import Potential +from core.sql import Connect +from core.user import User from web.login import login_required UPLOAD_FOLDER = 'database' @@ -97,8 +100,11 @@ def single_player_ptt(): user_id = user_id[0] user = web.webscore.get_user(c, user_id) posts = web.webscore.get_user_score(c, user_id, 30) - recent, recentptt = web.webscore.get_user_recent30( - c, user_id) + u = User() + u.user_id = user_id + p = Potential(c, u) + recentptt = p.recent_10 / 10 + recent = p.recent_30_to_dict_list() if not posts: error = '无成绩 No score.' else: diff --git a/latest version/web/webscore.py b/latest version/web/webscore.py index 517a69c..485aad7 100644 --- a/latest version/web/webscore.py +++ b/latest version/web/webscore.py @@ -1,5 +1,7 @@ import time +from core.score import Potential + def get_user_score(c, user_id, limit=-1, offset=0): # 返回用户的所有歌曲数据,带排名,返回字典列表 @@ -74,37 +76,3 @@ def get_user(c, user_id): } return r - - -def get_user_recent30(c, user_id): - # 获取玩家recent30信息并计算recent10的ptt,返回字典列表和一个值 - c.execute('''select * from recent30 where user_id=:a''', {'a': user_id}) - sumr = 0 - x = c.fetchone() - r = [] - if x is not None: - r30 = [] - s30 = [] - for i in range(1, 61, 2): - if x[i] is not None: - r30.append(float(x[i])) - s30.append(x[i+1]) - else: - r30.append(0) - s30.append('') - r30, s30 = (list(t) for t in zip(*sorted(zip(r30, s30), reverse=True))) - songs = [] - i = 0 - while len(songs) < 10 and i <= 29 and s30[i] != '' and s30[i] is not None: - if s30[i] not in songs: - sumr += r30[i] - songs.append(s30[i]) - i += 1 - for i in range(0, 30): - if s30[i]: - r.append({ - 'song_id': s30[i][:-1], - 'difficulty': int(s30[i][-1]), - 'rating': r30[i] - }) - return r, sumr / 10 From 9636cfcae8f6b38fad32a209a3ffd739bd86dc66 Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Mon, 6 May 2024 18:01:34 +0800 Subject: [PATCH 18/32] [Enhance][Bug fix] ETR world score & Finale & Bundle Update Message - Start to consider the ETR scores when getting the sum of all scores in global ranking. - Add support for automatically adding partner "Hikari & Tairitsu (Reunion)" and "Hikari (Fatalis)", to try to unlock Finale stories correctly. #110 #164 - Fix a bug that the POST requests will be neglected if a new content bundle update exists. --- latest version/core/constant.py | 2 +- latest version/core/user.py | 53 +++++++++++++-------------------- latest version/server/func.py | 2 +- latest version/server/others.py | 27 +++++++++++++---- 4 files changed, 44 insertions(+), 40 deletions(-) diff --git a/latest version/core/constant.py b/latest version/core/constant.py index f9c1a87..64b0a4c 100644 --- a/latest version/core/constant.py +++ b/latest version/core/constant.py @@ -1,6 +1,6 @@ from .config_manager import Config -ARCAEA_SERVER_VERSION = 'v2.11.3.11' +ARCAEA_SERVER_VERSION = 'v2.11.3.12' ARCAEA_DATABASE_VERSION = 'v2.11.3.11' ARCAEA_LOG_DATBASE_VERSION = 'v1.1' diff --git a/latest version/core/user.py b/latest version/core/user.py index 914f154..91787ba 100644 --- a/latest version/core/user.py +++ b/latest version/core/user.py @@ -155,7 +155,6 @@ def register(self): ''', {'user_code': self.user_code, 'user_id': self.user_id, 'join_date': now, 'name': self.name, 'password': self.hash_pwd, 'memories': Config.DEFAULT_MEMORIES, 'email': self.email}) - class UserLogin(User): # 密码和token的加密方式为 SHA-256 limiter = ArcLimiter(Config.GAME_LOGIN_RATE_LIMIT, 'game_login') @@ -663,38 +662,28 @@ def global_rank(self) -> int: def update_global_rank(self) -> None: '''用户世界排名计算,有新增成绩则要更新''' - self.c.execute('''select song_id, rating_ftr, rating_byn from chart''') - x = self.c.fetchall() - - song_list_ftr = [self.user_id] - song_list_byn = [self.user_id] - for i in x: - if i[1] > 0: - song_list_ftr.append(i[0]) - if i[2] > 0: - song_list_byn.append(i[0]) - - score_sum = 0 - if len(song_list_ftr) >= 2: - self.c.execute( - f'''select sum(score) from best_score where user_id=? and difficulty=2 and song_id in ({','.join(['?']*(len(song_list_ftr)-1))})''', tuple(song_list_ftr)) - - x = self.c.fetchone() - if x[0] is not None: - score_sum += x[0] - - if len(song_list_byn) >= 2: - self.c.execute( - f'''select sum(score) from best_score where user_id=? and difficulty=3 and song_id in ({','.join(['?']*(len(song_list_byn)-1))})''', tuple(song_list_byn)) - - x = self.c.fetchone() - if x[0] is not None: - score_sum += x[0] - - self.c.execute('''update user set world_rank_score = :b where user_id = :a''', { - 'a': self.user_id, 'b': score_sum}) + self.c.execute( + ''' + with user_scores as ( + select song_id, difficulty, score from best_score where user_id = ? and difficulty in (2, 3, 4) + ) + select sum(a) from( + select sum(score) as a from user_scores where difficulty = 2 and song_id in (select song_id from chart where rating_ftr > 0) + union + select sum(score) as a from user_scores where difficulty = 3 and song_id in (select song_id from chart where rating_byn > 0) + union + select sum(score) as a from user_scores where difficulty = 4 and song_id in (select song_id from chart where rating_etr > 0) + ) + ''', + (self.user_id,) + ) + x = self.c.fetchone() + if x[0] is None: + return - self.world_rank_score = score_sum + self.c.execute( + '''update user set world_rank_score = ? where user_id = ?''', (x[0], self.user_id)) + self.world_rank_score = x[0] def select_user_one_column(self, column_name: str, default_value=None) -> None: ''' diff --git a/latest version/server/func.py b/latest version/server/func.py index 6aa7534..65be2b0 100644 --- a/latest version/server/func.py +++ b/latest version/server/func.py @@ -110,7 +110,7 @@ def header_check(request) -> ArcError: if Config.ALLOW_APPVERSION: # 版本检查 if 'AppVersion' not in headers or headers['AppVersion'] not in Config.ALLOW_APPVERSION: return LowVersion('Invalid app version', 5) - if 'ContentBundle' in headers and headers['ContentBundle'] != BundleParser.max_bundle_version.get(headers.get('AppVersion', ''), '0.0.0'): + if request.method == 'GET' and 'ContentBundle' in headers and headers['ContentBundle'] != BundleParser.max_bundle_version.get(headers.get('AppVersion', ''), '0.0.0'): return LowVersion('Invalid content bundle version', 11) if has_arc_hash and not ArcHashChecker(request).check(): diff --git a/latest version/server/others.py b/latest version/server/others.py index f433d36..4f5f3fb 100644 --- a/latest version/server/others.py +++ b/latest version/server/others.py @@ -7,6 +7,7 @@ from core.bundle import BundleDownload from core.download import DownloadList from core.error import RateLimit +from core.item import ItemCharacter from core.sql import Connect from core.system import GameInfo from core.user import UserOnline @@ -14,7 +15,7 @@ from .auth import auth_required from .func import arc_try, error_return, success_return from .present import present_info -from .purchase import bundle_bundle, get_single, bundle_pack +from .purchase import bundle_bundle, bundle_pack, get_single from .score import song_score_friend from .user import user_me from .world import world_all @@ -65,15 +66,29 @@ def finale_progress(): @bp.route('/finale/finale_start', methods=['POST']) -def finale_start(): +@auth_required(request) +@arc_try +def finale_start(user_id): # testify开始,对立再见 - # 没数据 - return success_return({}) + # 但是对立不再见 + + with Connect() as c: + item = ItemCharacter(c) + item.set_id('55') # Hikari (Fatalis) + item.user_claim_item(UserOnline(c, user_id)) + return success_return({}) @bp.route('/finale/finale_end', methods=['POST']) -def finale_end(): - return success_return({}) +@auth_required(request) +@arc_try +def finale_end(user_id): + + with Connect() as c: + item = ItemCharacter(c) + item.set_id('5') # Hikari & Tairitsu (Reunion) + item.user_claim_item(UserOnline(c, user_id)) + return success_return({}) @bp.route('/applog/me/log', methods=['POST']) From 338f6579aaf6139436d1c37867f08075d582adfa Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Wed, 8 May 2024 21:17:51 +0800 Subject: [PATCH 19/32] [Bug fix] User delete bug in register - Fix a bug that new user can not register when some users delete their accounts. --- latest version/core/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/latest version/core/user.py b/latest version/core/user.py index 91787ba..3ccc979 100644 --- a/latest version/core/user.py +++ b/latest version/core/user.py @@ -138,7 +138,7 @@ def _insert_user_char(self): if x: for i in x: exp = 25000 if i[1] == 30 else 10000 - self.c.execute('''insert into user_char_full values(?,?,?,?,?,?,0)''', + self.c.execute('''insert or replace into user_char_full values(?,?,?,?,?,?,0)''', (self.user_id, i[0], i[1], exp, i[2], 0)) def register(self): From 2a08c9cd14534805b2c042bf9d14bdcc7edb14f1 Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Fri, 14 Jun 2024 16:06:26 +0800 Subject: [PATCH 20/32] [Enhance] World rank score mechanism - Adjust world rank mechanism to be closer to the official one. Note: You need to refresh rating in web admin backend after updating, and the users need to get a better or new score to refresh world rank. --- latest version/core/config_manager.py | 2 +- latest version/core/constant.py | 4 +- latest version/core/operation.py | 37 +++++++++++----- latest version/core/score.py | 32 +++++++++++--- latest version/core/sql.py | 9 +++- latest version/core/user.py | 18 +++++--- latest version/core/world.py | 4 ++ latest version/database/init/arc_data.py | 2 +- latest version/database/init/singles.json | 54 +++++++++++++++++++++++ latest version/database/init/tables.sql | 3 +- latest version/server/purchase.py | 2 + 11 files changed, 137 insertions(+), 30 deletions(-) diff --git a/latest version/core/config_manager.py b/latest version/core/config_manager.py index 2264f62..9816022 100644 --- a/latest version/core/config_manager.py +++ b/latest version/core/config_manager.py @@ -12,7 +12,7 @@ class Config: SONG_FILE_HASH_PRE_CALCULATE = True - GAME_API_PREFIX = '/hanami/29' # str | list[str] + GAME_API_PREFIX = '/natsugakuru/30' # str | list[str] OLD_GAME_API_PREFIX = [] # str | list[str] ALLOW_APPVERSION = [] # list[str] diff --git a/latest version/core/constant.py b/latest version/core/constant.py index 64b0a4c..e98694b 100644 --- a/latest version/core/constant.py +++ b/latest version/core/constant.py @@ -1,7 +1,7 @@ from .config_manager import Config -ARCAEA_SERVER_VERSION = 'v2.11.3.12' -ARCAEA_DATABASE_VERSION = 'v2.11.3.11' +ARCAEA_SERVER_VERSION = 'v2.11.3.13' +ARCAEA_DATABASE_VERSION = 'v2.11.3.13' ARCAEA_LOG_DATBASE_VERSION = 'v1.1' diff --git a/latest version/core/operation.py b/latest version/core/operation.py index 741d4af..62b2143 100644 --- a/latest version/core/operation.py +++ b/latest version/core/operation.py @@ -27,6 +27,7 @@ def run(self, *args, **kwargs) -> None: class RefreshAllScoreRating(BaseOperation): ''' 刷新所有成绩的评分 + 包括 score_v2 ''' _name = 'refresh_all_score_rating' @@ -44,11 +45,11 @@ def run(self): for i in x: for j in range(0, 4): - defnum = -10 # 没在库里的全部当做定数-10 + defnum = -10 # 没在库里的全部当做定数 -10 if i[j+1] is not None and i[j+1] > 0: defnum = float(i[j+1]) / 10 - c.execute('''select user_id, score from best_score where song_id=:a and difficulty=:b''', { + c.execute('''select user_id, score, shiny_perfect_count, perfect_count, near_count, miss_count from best_score where song_id=:a and difficulty=:b''', { 'a': i[0], 'b': j}) y = c.fetchall() values = [] @@ -56,10 +57,12 @@ def run(self): for k in y: ptt = Score.calculate_rating(defnum, k[1]) ptt = max(ptt, 0) - values.append((ptt,)) + score_v2 = Score.calculate_score_v2( + defnum, k[2], k[3], k[4], k[5]) + values.append((ptt, score_v2,)) where_values.append((k[0], i[0], j)) if values: - Sql(c).update_many('best_score', ['rating'], values, [ + Sql(c).update_many('best_score', ['rating', 'score_v2'], values, [ 'user_id', 'song_id', 'difficulty'], where_values) @@ -133,11 +136,16 @@ def _one_user_update(self): new_scores = [] for i in save.scores_data: rating = 0 + score_v2 = 0 if i['song_id'] in song_chart_const: - rating = Score.calculate_rating( - song_chart_const[i['song_id']][i['difficulty']] / 10, i['score']) + defnum = song_chart_const[i['song_id'] + ][i['difficulty']] / 10 + rating = Score.calculate_rating(defnum, i['score']) rating = max(rating, 0) + score_v2 = Score.calculate_score_v2( + defnum, i['shiny_perfect_count'], i['perfect_count'], i['near_count'], i['miss_count']) + y = f'{i["song_id"]}{i["difficulty"]}' if y in clear_state: clear_type = clear_state[y] @@ -145,10 +153,10 @@ def _one_user_update(self): clear_type = 0 new_scores.append((self.user.user_id, i['song_id'], i['difficulty'], i['score'], i['shiny_perfect_count'], i['perfect_count'], - i['near_count'], i['miss_count'], i['health'], i['modifier'], i['time_played'], clear_type, clear_type, rating)) + i['near_count'], i['miss_count'], i['health'], i['modifier'], i['time_played'], clear_type, clear_type, rating, score_v2)) c.executemany( - '''insert or replace into best_score values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', new_scores) + '''insert or replace into best_score values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', new_scores) def _all_update(self): with Connect() as c: @@ -169,11 +177,16 @@ def _all_update(self): new_scores = [] for i in save.scores_data: rating = 0 + score_v2 = 0 if i['song_id'] in song_chart_const: - rating = Score.calculate_rating( - song_chart_const[i['song_id']][i['difficulty']] / 10, i['score']) + defnum = song_chart_const[i['song_id'] + ][i['difficulty']] / 10 + rating = Score.calculate_rating(defnum, i['score']) rating = max(rating, 0) + score_v2 = Score.calculate_score_v2( + defnum, i['shiny_perfect_count'], i['perfect_count'], i['near_count'], i['miss_count']) + y = f'{i["song_id"]}{i["difficulty"]}' if y in clear_state: clear_type = clear_state[y] @@ -181,10 +194,10 @@ def _all_update(self): clear_type = 0 new_scores.append((user.user_id, i['song_id'], i['difficulty'], i['score'], i['shiny_perfect_count'], i['perfect_count'], - i['near_count'], i['miss_count'], i['health'], i['modifier'], i['time_played'], clear_type, clear_type, rating)) + i['near_count'], i['miss_count'], i['health'], i['modifier'], i['time_played'], clear_type, clear_type, rating, score_v2)) c.executemany( - '''insert or replace into best_score values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', new_scores) + '''insert or replace into best_score values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', new_scores) class UnlockUserItem(BaseOperation): diff --git a/latest version/core/score.py b/latest version/core/score.py index 485e480..095409b 100644 --- a/latest version/core/score.py +++ b/latest version/core/score.py @@ -30,6 +30,7 @@ def __init__(self) -> None: self.best_clear_type: int = None self.clear_type: int = None self.rating: float = None + self.score_v2: float = None # for `world_rank_score` of global rank def set_score(self, score: int, shiny_perfect_count: int, perfect_count: int, near_count: int, miss_count: int, health: int, modifier: int, time_played: int, clear_type: int): self.score = int(score) if score is not None else 0 @@ -124,12 +125,31 @@ def calculate_rating(defnum: float, score: int) -> float: return ptt + @staticmethod + def calculate_score_v2(defnum: float, shiny_perfect_count: int, perfect_count: int, near_count: int, miss_count: int) -> float: + # 计算score_v2 refer: https://www.bilibili.com/video/BV1ys421u7BY + # 谱面定数小于等于 0 视为 unranked,返回值会为 0 + if not defnum or defnum <= 0: + return 0 + + all_note = perfect_count + near_count + miss_count + if all_note == 0: + return 0 + shiny_ratio = shiny_perfect_count / all_note + score_ratio = (perfect_count + near_count/2) / \ + all_note + shiny_perfect_count / 10000000 + acc_rating = max(0, min(shiny_ratio - 0.9, 0.095)) / 9.5 * 25 + score_rating = max(0, min(score_ratio - 0.99, 0.01)) * 75 + return defnum * (acc_rating + score_rating) + def get_rating_by_calc(self) -> float: - # 通过计算得到本成绩的rating + # 通过计算得到本成绩的 rating & score_v2 if not self.song.defnum: self.song.c = self.c self.song.select() self.rating = self.calculate_rating(self.song.chart_const, self.score) + self.score_v2 = self.calculate_score_v2( + self.song.chart_const, self.shiny_perfect_count, self.perfect_count, self.near_count, self.miss_count) return self.rating def to_dict(self) -> dict: @@ -181,6 +201,7 @@ def from_list(self, x: list) -> 'UserScore': self.set_score(x[3], x[4], x[5], x[6], x[7], x[8], x[9], x[10], x[12]) self.best_clear_type = int(x[11]) self.rating = float(x[13]) + self.score_v2 = float(x[14]) return self @@ -430,8 +451,9 @@ def upload_score(self) -> None: x = self.c.fetchone() if not x: self.new_best_protect_flag = True # 初见保护 - self.c.execute('''insert into best_score values(:a,:b,:c,:d,:e,:f,:g,:h,:i,:j,:k,:l,:m,:n)''', { - 'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.score, 'e': self.shiny_perfect_count, 'f': self.perfect_count, 'g': self.near_count, 'h': self.miss_count, 'i': self.health, 'j': self.modifier, 'k': self.time_played, 'l': self.clear_type, 'm': self.clear_type, 'n': self.rating}) + self.c.execute('''insert into best_score values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', + (self.user.user_id, self.song.song_id, self.song.difficulty, self.score, self.shiny_perfect_count, self.perfect_count, self.near_count, self.miss_count, + self.health, self.modifier, self.time_played, self.clear_type, self.clear_type, self.rating, self.score_v2)) self.user.update_global_rank() else: self.new_best_protect_flag = False @@ -440,8 +462,8 @@ def upload_score(self) -> None: 'a': self.clear_type, 'b': self.user.user_id, 'c': self.song.song_id, 'd': self.song.difficulty}) if self.score >= int(x[0]): # best成绩更新 self.new_best_protect_flag = True - self.c.execute('''update best_score set score = :d, shiny_perfect_count = :e, perfect_count = :f, near_count = :g, miss_count = :h, health = :i, modifier = :j, clear_type = :k, rating = :l, time_played = :m where user_id = :a and song_id = :b and difficulty = :c ''', { - 'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.score, 'e': self.shiny_perfect_count, 'f': self.perfect_count, 'g': self.near_count, 'h': self.miss_count, 'i': self.health, 'j': self.modifier, 'k': self.clear_type, 'l': self.rating, 'm': self.time_played}) + self.c.execute('''update best_score set score = :d, shiny_perfect_count = :e, perfect_count = :f, near_count = :g, miss_count = :h, health = :i, modifier = :j, clear_type = :k, rating = :l, time_played = :m, score_v2 = :n where user_id = :a and song_id = :b and difficulty = :c ''', { + 'a': self.user.user_id, 'b': self.song.song_id, 'c': self.song.difficulty, 'd': self.score, 'e': self.shiny_perfect_count, 'f': self.perfect_count, 'g': self.near_count, 'h': self.miss_count, 'i': self.health, 'j': self.modifier, 'k': self.clear_type, 'l': self.rating, 'm': self.time_played, 'n': self.score_v2}) self.user.update_global_rank() self.ptt = Potential(self.c, self.user) diff --git a/latest version/core/sql.py b/latest version/core/sql.py index ae7784c..9e81edf 100644 --- a/latest version/core/sql.py +++ b/latest version/core/sql.py @@ -351,7 +351,8 @@ def get_table_info(self, table_name: str): class DatabaseMigrator: SPECIAL_UPDATE_VERSION = { - '2.11.3.11': '_version_2_11_3_11' + '2.11.3.11': '_version_2_11_3_11', + '2.11.3.13': '_version_2_11_3.13' } def __init__(self, c1_path: str, c2_path: str) -> None: @@ -463,6 +464,12 @@ def _version_2_11_3_11(self): self.c2.executemany( '''insert into recent30(user_id, r_index, time_played, song_id, difficulty, rating) values(?,?,?,?,?,?)''', sql_list) + def _version_2_11_3_13(self): + ''' + 2.11.3.13 版本特殊更新,world_rank_score 机制调整,需清空用户分数 + ''' + self.c1.execute('''update user set world_rank_score = 0''') + class LogDatabaseMigrator: diff --git a/latest version/core/user.py b/latest version/core/user.py index 3ccc979..05ab4ee 100644 --- a/latest version/core/user.py +++ b/latest version/core/user.py @@ -528,7 +528,11 @@ def to_dict(self) -> dict: 'world_mode_locked_end_ts': self.world_mode_locked_end_ts, 'locked_char_ids': [], # [1] 'user_missions': UserMissionList(self.c, self).select_all().to_dict_list(), - 'pick_ticket': self.pick_ticket + 'pick_ticket': self.pick_ticket, + + # 'custom_banner': 'online_banner_2024_06', + # 'subscription_multiplier': 114, + # 'memory_boost_ticket': 5, } def from_list(self, x: list) -> 'UserInfo': @@ -648,8 +652,8 @@ def global_rank(self) -> int: '''用户世界排名,如果超过设定最大值,返回0''' if self.world_rank_score is None: self.select_user_one_column('world_rank_score', 0) - if self.world_rank_score is None: - return 0 + if not self.world_rank_score: + return 0 self.c.execute( '''select count(*) from user where world_rank_score > ?''', (self.world_rank_score,)) @@ -665,14 +669,14 @@ def update_global_rank(self) -> None: self.c.execute( ''' with user_scores as ( - select song_id, difficulty, score from best_score where user_id = ? and difficulty in (2, 3, 4) + select song_id, difficulty, score_v2 from best_score where user_id = ? and difficulty in (2, 3, 4) ) select sum(a) from( - select sum(score) as a from user_scores where difficulty = 2 and song_id in (select song_id from chart where rating_ftr > 0) + select sum(score_v2) as a from user_scores where difficulty = 2 and song_id in (select song_id from chart where rating_ftr > 0) union - select sum(score) as a from user_scores where difficulty = 3 and song_id in (select song_id from chart where rating_byn > 0) + select sum(score_v2) as a from user_scores where difficulty = 3 and song_id in (select song_id from chart where rating_byn > 0) union - select sum(score) as a from user_scores where difficulty = 4 and song_id in (select song_id from chart where rating_etr > 0) + select sum(score_v2) as a from user_scores where difficulty = 4 and song_id in (select song_id from chart where rating_etr > 0) ) ''', (self.user_id,) diff --git a/latest version/core/world.py b/latest version/core/world.py index d1e934e..c87fc53 100644 --- a/latest version/core/world.py +++ b/latest version/core/world.py @@ -265,6 +265,7 @@ def to_dict(self, has_map_info: bool = False, has_steps: bool = False, has_rewar r['curr_capture'] = self.curr_capture r['is_locked'] = self.is_locked r['user_id'] = self.user.user_id + # memory_boost_ticket if not has_steps: del r['steps'] if has_rewards: @@ -668,6 +669,9 @@ def to_dict(self) -> dict: 'world_mode_locked_end_ts': self.user.world_mode_locked_end_ts, 'beyond_boost_gauge': self.user.beyond_boost_gauge, # 'wpaid': 'helloworld', # world play id ??? + # progress_before_sub_boost + # progress_sub_boost_amount + # subscription_multiply } if self.character_used.skill_id_displayed == 'skill_maya': diff --git a/latest version/database/init/arc_data.py b/latest version/database/init/arc_data.py index 54c1e34..d762b70 100644 --- a/latest version/database/init/arc_data.py +++ b/latest version/database/init/arc_data.py @@ -70,7 +70,7 @@ class InitData: 'core_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful', 'core_course_skip_purchase', 'core_umbral', 'core_wacca', 'core_sunset', 'core_tanoc'] world_songs = ["babaroque", "shadesoflight", "kanagawa", "lucifer", "anokumene", "ignotus", "rabbitintheblackroom", "qualia", "redandblue", "bookmaker", "darakunosono", "espebranch", "blacklotus", "givemeanightmare", "vividtheory", "onefr", "gekka", "vexaria3", "infinityheaven3", "fairytale3", "goodtek3", "suomi", "rugie", "faintlight", "harutopia", "goodtek", "dreaminattraction", "syro", "diode", "freefall", "grimheart", "blaster", - "cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris', 'ultimatetaste', 'rgb', 'matenrou', 'dynitikos', 'amekagura', 'fantasy', 'aloneandlorn', 'felys', 'onandon', 'hotarubinoyuki', 'oblivia3', 'libertas3', 'einherjar3', 'purpleverse3', 'viciousheroism3', 'inkarusi3', 'cyberneciacatharsis3', 'alephzero', 'hellohell', 'ichirin', 'awakeninruins', 'morningloom', 'lethalvoltage', 'leaveallbehind', 'desive'] + "cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris', 'ultimatetaste', 'rgb', 'matenrou', 'dynitikos', 'amekagura', 'fantasy', 'aloneandlorn', 'felys', 'onandon', 'hotarubinoyuki', 'oblivia3', 'libertas3', 'einherjar3', 'purpleverse3', 'viciousheroism3', 'inkarusi3', 'cyberneciacatharsis3', 'alephzero', 'hellohell', 'ichirin', 'awakeninruins', 'morningloom', 'lethalvoltage', 'leaveallbehind', 'desive', 'oldschoolsalvage'] world_unlocks = ["scenery_chap1", "scenery_chap2", "scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7", "scenery_beyond"] diff --git a/latest version/database/init/singles.json b/latest version/database/init/singles.json index cef6ad3..e963d38 100644 --- a/latest version/database/init/singles.json +++ b/latest version/database/init/singles.json @@ -1774,5 +1774,59 @@ ], "orig_price": 100, "price": 100 + }, + { + "name": "backtobasics", + "items": [ + { + "type": "single", + "id": "backtobasics", + "is_available": true + }, + { + "type": "core", + "amount": 1, + "id": "core_generic", + "is_available": true + } + ], + "orig_price": 100, + "price": 100 + }, + { + "name": "tasogare", + "items": [ + { + "type": "single", + "id": "tasogare", + "is_available": true + }, + { + "type": "core", + "amount": 1, + "id": "core_generic", + "is_available": true + } + ], + "orig_price": 100, + "price": 100 + }, + { + "name": "beautifuldreamer", + "items": [ + { + "type": "single", + "id": "beautifuldreamer", + "is_available": true + }, + { + "type": "core", + "amount": 1, + "id": "core_generic", + "is_available": true + } + ], + "orig_price": 100, + "price": 100 } ] \ No newline at end of file diff --git a/latest version/database/init/tables.sql b/latest version/database/init/tables.sql index 88c8e17..a2bb234 100644 --- a/latest version/database/init/tables.sql +++ b/latest version/database/init/tables.sql @@ -61,7 +61,8 @@ modifier int, time_played int, best_clear_type int, clear_type int, -rating real, +rating real default 0, +score_v2 real default 0, primary key(user_id, song_id, difficulty) ); create table if not exists user_char(user_id int, diff --git a/latest version/server/purchase.py b/latest version/server/purchase.py index 083eeb8..1e80da1 100644 --- a/latest version/server/purchase.py +++ b/latest version/server/purchase.py @@ -106,6 +106,8 @@ def buy_special(user_id): x.discount_from = -1 x.discount_to = -1 x.items = [ItemFactory(c).get_item(item_id)] + # request.form['ticket_used'] == 'true' + # memory_boost_ticket: x-1 x.buy() r = {'user_id': x.user.user_id, 'ticket': x.user.ticket} From dd0bc64c93705b029d9d2c45d7c85d3b0bd94cd2 Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Fri, 14 Jun 2024 22:20:28 +0800 Subject: [PATCH 21/32] [Bug fix] A typo causes updating error --- latest version/core/sql.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/latest version/core/sql.py b/latest version/core/sql.py index 9e81edf..a402088 100644 --- a/latest version/core/sql.py +++ b/latest version/core/sql.py @@ -352,7 +352,7 @@ class DatabaseMigrator: SPECIAL_UPDATE_VERSION = { '2.11.3.11': '_version_2_11_3_11', - '2.11.3.13': '_version_2_11_3.13' + '2.11.3.13': '_version_2_11_3_13' } def __init__(self, c1_path: str, c2_path: str) -> None: From 64526e3c4b6c439eca808ebd766941d6be3886bf Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Fri, 14 Jun 2024 23:47:52 +0800 Subject: [PATCH 22/32] [Bug fix] video download error with songlist - Fix a bug that the video files cannot be downloaded when the `songlist` file exists. #177 --- latest version/core/download.py | 4 ++-- latest version/core/world.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/latest version/core/download.py b/latest version/core/download.py index 724d7e3..208d22a 100644 --- a/latest version/core/download.py +++ b/latest version/core/download.py @@ -47,8 +47,8 @@ def is_available_file(song_id: str, file_name: str) -> bool: # songlist没有,则只限制文件名 return file_name in SonglistParser.FILE_NAMES rule = SonglistParser.songs[song_id] - for i in range(10): - if file_name == SonglistParser.FILE_NAMES[i] and rule & (1 << i) != 0: + for i, v in enumerate(SonglistParser.FILE_NAMES): + if file_name == v and rule & (1 << i) != 0: return True return False diff --git a/latest version/core/world.py b/latest version/core/world.py index c87fc53..d3c6c46 100644 --- a/latest version/core/world.py +++ b/latest version/core/world.py @@ -660,9 +660,9 @@ def to_dict(self) -> dict: }, "char_stats": { "character_id": self.character_used.character_id, - "frag": self.character_used.frag.get_value(self.character_used.level), - "prog": self.character_used.prog.get_value(self.character_used.level), - "overdrive": self.character_used.overdrive.get_value(self.character_used.level) + "frag": self.character_used.frag_value, + "prog": self.character_used.prog_value, + "overdrive": self.character_used.overdrive_value }, "current_stamina": self.user.stamina.stamina, "max_stamina_ts": self.user.stamina.max_stamina_ts, From c38208832b77906b79538c7bd438fb51ed4a4fd3 Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Thu, 20 Jun 2024 00:13:58 +0800 Subject: [PATCH 23/32] [Enhance] World map sub folder - Make the world maps' folder can have sub folders. --- latest version/core/bundle.py | 6 ++- latest version/core/constant.py | 2 +- latest version/core/init.py | 21 +++++++--- latest version/core/operation.py | 11 ++++++ latest version/core/sql.py | 2 +- latest version/core/world.py | 66 +++++++++++++++++++------------- latest version/server/world.py | 4 +- 7 files changed, 75 insertions(+), 37 deletions(-) diff --git a/latest version/core/bundle.py b/latest version/core/bundle.py index 01a230c..985d93c 100644 --- a/latest version/core/bundle.py +++ b/latest version/core/bundle.py @@ -82,11 +82,15 @@ class BundleParser: version_tuple_bundles: 'dict[tuple[str, str], ContentBundle]' = {} def __init__(self) -> None: - self.parse() + if not self.bundles: + self.parse() def re_init(self) -> None: self.bundles.clear() self.max_bundle_version.clear() + self.next_versions.clear() + self.version_tuple_bundles.clear() + self.get_bundles.cache_clear() self.parse() def parse(self) -> None: diff --git a/latest version/core/constant.py b/latest version/core/constant.py index e98694b..f775f13 100644 --- a/latest version/core/constant.py +++ b/latest version/core/constant.py @@ -1,6 +1,6 @@ from .config_manager import Config -ARCAEA_SERVER_VERSION = 'v2.11.3.13' +ARCAEA_SERVER_VERSION = 'v2.11.3.14' ARCAEA_DATABASE_VERSION = 'v2.11.3.13' ARCAEA_LOG_DATBASE_VERSION = 'v1.1' diff --git a/latest version/core/init.py b/latest version/core/init.py index cf0b8c4..affa6f1 100644 --- a/latest version/core/init.py +++ b/latest version/core/init.py @@ -16,6 +16,7 @@ MemoryDatabase) from core.user import UserRegister from core.util import try_rename +from core.world import MapParser class DatabaseInit: @@ -326,12 +327,11 @@ def update_log_database(old_path: str = Config.SQLITE_LOG_DATABASE_PATH) -> None def check_song_file(self) -> bool: '''检查song有关文件并初始化缓存''' f = self.check_folder(Config.SONG_FILE_FOLDER_PATH) - self.logger.info("Start to initialize song data...") + self.logger.info("Initialize song data...") try: DownloadList.initialize_cache() if not Config.SONG_FILE_HASH_PRE_CALCULATE: self.logger.info('Song file hash pre-calculate is disabled.') - self.logger.info('Song data initialization is complete!') except Exception as e: self.logger.error(format_exc()) self.logger.warning('Song data initialization error!') @@ -341,17 +341,28 @@ def check_song_file(self) -> bool: def check_content_bundle(self) -> bool: '''检查 content bundle 有关文件并初始化缓存''' f = self.check_folder(Config.CONTENT_BUNDLE_FOLDER_PATH) - self.logger.info("Start to initialize content bundle data...") + self.logger.info("Initialize content bundle data...") try: BundleParser() - self.logger.info('Content bundle data initialization is complete!') except Exception as e: self.logger.error(format_exc()) self.logger.warning('Content bundle data initialization error!') f = False return f + def check_world_map(self) -> bool: + '''检查 world map 有关文件并初始化缓存''' + f = self.check_folder(Config.WORLD_MAP_FOLDER_PATH) + self.logger.info("Initialize world map data...") + try: + MapParser() + except Exception as e: + self.logger.error(format_exc()) + self.logger.warning('World map data initialization error!') + f = False + return f + def check_before_run(self) -> bool: '''运行前检查,返回布尔值''' MemoryDatabase() # 初始化内存数据库 - return self.check_song_file() and self.check_content_bundle() and self.check_update_database() + return self.check_song_file() and self.check_content_bundle() and self.check_update_database() and self.check_world_map() diff --git a/latest version/core/operation.py b/latest version/core/operation.py index 62b2143..7e4a834 100644 --- a/latest version/core/operation.py +++ b/latest version/core/operation.py @@ -6,6 +6,7 @@ from .score import Score from .sql import Connect, Sql from .user import User +from .world import MapParser class BaseOperation: @@ -88,6 +89,16 @@ def run(self): BundleParser().re_init() +class RefreshWorldMapCache(BaseOperation): + ''' + 刷新 map 缓存 + ''' + _name = 'refresh_world_map_cache' + + def run(self): + MapParser().re_init() + + class SaveUpdateScore(BaseOperation): ''' 云存档更新成绩,是覆盖式更新 diff --git a/latest version/core/sql.py b/latest version/core/sql.py index a402088..96035bf 100644 --- a/latest version/core/sql.py +++ b/latest version/core/sql.py @@ -437,7 +437,7 @@ def special_update(self): def _version_2_11_3_11(self): ''' 2.11.3.11 版本特殊更新,调整 recent30 表结构 - recent30 表从 (user_id: int PK, song_id: text, rating: real, ...) \ + recent30 表从 (user_id: int PK, rating: real, song_id: text, ...) \ 更改为 (user_id: int PK, r_index: int PK, time_played: int, song_id: text, difficulty: int, score: int, sp, p, n, m, hp, mod, clear_type, rating: real) ''' diff --git a/latest version/core/world.py b/latest version/core/world.py index d3c6c46..a82b48e 100644 --- a/latest version/core/world.py +++ b/latest version/core/world.py @@ -10,34 +10,46 @@ from .item import ItemFactory -@lru_cache(maxsize=128) -def get_world_name(file_dir: str = Constant.WORLD_MAP_FOLDER_PATH) -> list: - '''获取所有地图名称,返回列表''' - file_list = [] - for root, dirs, files in os.walk(file_dir): - for file in files: - if os.path.splitext(file)[1] == '.json': - file_list.append(os.path.splitext(file)[0]) - return file_list +class MapParser: + map_id_path: 'dict[str, str]' = {} -@lru_cache(maxsize=128) -def get_world_info(map_id: str) -> dict: - '''读取json文件内容,返回字典''' - world_info = {} - with open(os.path.join(Constant.WORLD_MAP_FOLDER_PATH, f'{map_id}.json'), 'rb') as f: - world_info = load(f) - - return world_info - - -def get_world_all(c, user) -> list: - ''' - 读取所有地图信息,返回列表 - parameter: `user` - `User`类或子类的实例 - ''' - worlds = get_world_name() - return [UserMap(c, map_id, user) for map_id in worlds] + def __init__(self) -> None: + if not self.map_id_path: + self.parse() + + def parse(self) -> None: + for root, dirs, files in os.walk(Constant.WORLD_MAP_FOLDER_PATH): + for file in files: + if not file.endswith('.json'): + continue + + path = os.path.join(root, file) + self.map_id_path[file[:-5]] = path + + def re_init(self) -> None: + self.map_id_path.clear() + self.get_world_info.cache_clear() + self.parse() + + @staticmethod + @lru_cache(maxsize=128) + def get_world_info(map_id: str) -> dict: + '''读取json文件内容,返回字典''' + world_info = {} + with open(MapParser.map_id_path[map_id], 'rb') as f: + world_info = load(f) + + return world_info + + @staticmethod + def get_world_all(c, user) -> list: + ''' + 读取所有地图信息,返回列表 + parameter: `user` - `User` 类或子类的实例 + `c` - 数据库连接 + ''' + return [UserMap(c, map_id, user) for map_id in MapParser.map_id_path.keys()] class Step: @@ -208,7 +220,7 @@ def from_dict(self, raw_dict: dict) -> 'Map': def select_map_info(self): '''获取地图信息''' - self.from_dict(get_world_info(self.map_id)) + self.from_dict(MapParser.get_world_info(self.map_id)) class UserMap(Map): diff --git a/latest version/server/world.py b/latest version/server/world.py index 8567ea0..4fb1edc 100644 --- a/latest version/server/world.py +++ b/latest version/server/world.py @@ -2,7 +2,7 @@ from core.sql import Connect from core.user import UserOnline -from core.world import UserMap, get_world_all +from core.world import MapParser, UserMap from .auth import auth_required from .func import arc_try, success_return @@ -20,7 +20,7 @@ def world_all(user_id): return success_return({ "current_map": user.current_map.map_id, "user_id": user_id, - "maps": [x.to_dict(has_map_info=True, has_rewards=True) for x in get_world_all(c, user)] + "maps": [x.to_dict(has_map_info=True, has_rewards=True) for x in MapParser.get_world_all(c, user)] }) From 9bb717135465e57ebe21b623dee78ab8620707ed Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Tue, 2 Jul 2024 22:18:00 +0800 Subject: [PATCH 24/32] [Bug fix] Update error & About bundle download - Data for Arcaea 5.8.0 - Fix a bug that the server's database updates from an old version may meet a challenge if `recent30` table has some strange data. - Fix the issue where the bundle downloading memory cache does not clean up. - Add a config term to only change the X-Accel-Redirect prefix for bundle downloading. --- latest version/core/bundle.py | 6 ++++ latest version/core/config_manager.py | 3 +- latest version/core/constant.py | 4 +-- latest version/core/sql.py | 5 ++-- latest version/database/init/packs.json | 18 ++++++++++++ latest version/database/init/singles.json | 36 +++++++++++++++++++++++ latest version/main.py | 2 +- 7 files changed, 68 insertions(+), 6 deletions(-) diff --git a/latest version/core/bundle.py b/latest version/core/bundle.py index 985d93c..265c110 100644 --- a/latest version/core/bundle.py +++ b/latest version/core/bundle.py @@ -226,6 +226,8 @@ def url_func(x): return url_for( if not sql_list: return [] + self.clear_expired_token() + self.c_m.executemany( '''insert into bundle_download_token values (?, ?, ?, ?)''', sql_list) @@ -246,3 +248,7 @@ def get_path_by_token(self, token: str, ip: str) -> str: f'Too many content bundle downloads, IP: {ip}, DeviceID: {device_id}', status=429) return file_path + + def clear_expired_token(self) -> None: + self.c_m.execute( + '''delete from bundle_download_token where time < ?''', (int(time() - Constant.BUNDLE_DOWNLOAD_TIME_GAP_LIMIT),)) diff --git a/latest version/core/config_manager.py b/latest version/core/config_manager.py index 9816022..c59c32e 100644 --- a/latest version/core/config_manager.py +++ b/latest version/core/config_manager.py @@ -16,7 +16,7 @@ class Config: OLD_GAME_API_PREFIX = [] # str | list[str] ALLOW_APPVERSION = [] # list[str] - + BUNDLE_STRICT_MODE = True SET_LINKPLAY_SERVER_AS_SUB_PROCESS = True @@ -49,6 +49,7 @@ class Config: DOWNLOAD_USE_NGINX_X_ACCEL_REDIRECT = False NGINX_X_ACCEL_REDIRECT_PREFIX = '/nginx_download/' + BUNDLE_NGINX_X_ACCEL_REDIRECT_PREFIX = '/nginx_bundle_download/' DOWNLOAD_TIMES_LIMIT = 3000 DOWNLOAD_TIME_GAP_LIMIT = 1000 diff --git a/latest version/core/constant.py b/latest version/core/constant.py index f775f13..bab19dd 100644 --- a/latest version/core/constant.py +++ b/latest version/core/constant.py @@ -1,7 +1,7 @@ from .config_manager import Config -ARCAEA_SERVER_VERSION = 'v2.11.3.14' -ARCAEA_DATABASE_VERSION = 'v2.11.3.13' +ARCAEA_SERVER_VERSION = 'v2.11.3.15' +ARCAEA_DATABASE_VERSION = 'v2.11.3.15' ARCAEA_LOG_DATBASE_VERSION = 'v1.1' diff --git a/latest version/core/sql.py b/latest version/core/sql.py index 96035bf..6a679e6 100644 --- a/latest version/core/sql.py +++ b/latest version/core/sql.py @@ -450,10 +450,11 @@ def _version_2_11_3_11(self): for j in range(30): rating = i[1 + j * 2] rating = float(rating) if rating else 0 - song_id_difficulty = i[2 + j * 2] + song_id_difficulty: str = i[2 + j * 2] if song_id_difficulty: song_id = song_id_difficulty[:-1] - difficulty = int(song_id_difficulty[-1]) + difficulty = song_id_difficulty[-1] + difficulty = int(difficulty) if difficulty.isdigit() else 0 else: song_id = '' difficulty = 0 diff --git a/latest version/database/init/packs.json b/latest version/database/init/packs.json index 650680a..5357e1c 100644 --- a/latest version/database/init/packs.json +++ b/latest version/database/init/packs.json @@ -808,5 +808,23 @@ ], "price": 700, "orig_price": 700 + }, + { + "name": "groovecoaster_append_1", + "items": [ + { + "type": "pack", + "id": "groovecoaster_append_1", + "is_available": true + }, + { + "type": "core", + "amount": 3, + "id": "core_generic", + "is_available": true + } + ], + "price": 300, + "orig_price": 300 } ] \ No newline at end of file diff --git a/latest version/database/init/singles.json b/latest version/database/init/singles.json index e963d38..007cbed 100644 --- a/latest version/database/init/singles.json +++ b/latest version/database/init/singles.json @@ -1828,5 +1828,41 @@ ], "orig_price": 100, "price": 100 + }, + { + "name": "hypervision", + "items": [ + { + "type": "single", + "id": "hypervision", + "is_available": true + }, + { + "type": "core", + "amount": 1, + "id": "core_generic", + "is_available": true + } + ], + "orig_price": 100, + "price": 100 + }, + { + "name": "stargateextreme", + "items": [ + { + "type": "single", + "id": "stargateextreme", + "is_available": true + }, + { + "type": "core", + "amount": 1, + "id": "core_generic", + "is_available": true + } + ], + "orig_price": 100, + "price": 100 } ] \ No newline at end of file diff --git a/latest version/main.py b/latest version/main.py index 8f84d60..ac78ec5 100644 --- a/latest version/main.py +++ b/latest version/main.py @@ -111,7 +111,7 @@ def bundle_download(token: str): # nginx X-Accel-Redirect response = make_response() response.headers['Content-Type'] = 'application/octet-stream' - response.headers['X-Accel-Redirect'] = Config.NGINX_X_ACCEL_REDIRECT_PREFIX + file_path + response.headers['X-Accel-Redirect'] = Config.BUNDLE_NGINX_X_ACCEL_REDIRECT_PREFIX + file_path return response return send_from_directory(Constant.CONTENT_BUNDLE_FOLDER_PATH, file_path, as_attachment=True, conditional=True) except ArcError as e: From 59422f96b5ce1f15405df2565eec2a9900b0d5b2 Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Tue, 30 Jul 2024 19:39:46 +0800 Subject: [PATCH 25/32] [Enhance] Full unlock for "alterego" - Add support for full cloud save unlocking of "alterego" - For Arcaea 5.9.0 --- latest version/core/config_manager.py | 2 +- latest version/core/constant.py | 4 ++-- latest version/core/save.py | 2 ++ latest version/database/init/arc_data.py | 13 +++++++------ latest version/database/init/packs.json | 18 ++++++++++++++++++ latest version/database/init/singles.json | 18 ++++++++++++++++++ latest version/web/index.py | 2 +- 7 files changed, 49 insertions(+), 10 deletions(-) diff --git a/latest version/core/config_manager.py b/latest version/core/config_manager.py index c59c32e..2dd32d9 100644 --- a/latest version/core/config_manager.py +++ b/latest version/core/config_manager.py @@ -12,7 +12,7 @@ class Config: SONG_FILE_HASH_PRE_CALCULATE = True - GAME_API_PREFIX = '/natsugakuru/30' # str | list[str] + GAME_API_PREFIX = '/geriraraiu/31' # str | list[str] OLD_GAME_API_PREFIX = [] # str | list[str] ALLOW_APPVERSION = [] # list[str] diff --git a/latest version/core/constant.py b/latest version/core/constant.py index bab19dd..2c50d02 100644 --- a/latest version/core/constant.py +++ b/latest version/core/constant.py @@ -1,7 +1,7 @@ from .config_manager import Config -ARCAEA_SERVER_VERSION = 'v2.11.3.15' -ARCAEA_DATABASE_VERSION = 'v2.11.3.15' +ARCAEA_SERVER_VERSION = 'v2.11.3.16' +ARCAEA_DATABASE_VERSION = 'v2.11.3.16' ARCAEA_LOG_DATBASE_VERSION = 'v1.1' diff --git a/latest version/core/save.py b/latest version/core/save.py index 640ff82..4360c28 100644 --- a/latest version/core/save.py +++ b/latest version/core/save.py @@ -107,6 +107,8 @@ def select_all(self, user) -> None: i['complete'] = 3 elif x[-5:-2] == '109': i['complete'] = 3 + elif x.endswith('112'): + i['complete'] = 999 else: i['complete'] = 1 diff --git a/latest version/database/init/arc_data.py b/latest version/database/init/arc_data.py index d762b70..87928a0 100644 --- a/latest version/database/init/arc_data.py +++ b/latest version/database/init/arc_data.py @@ -6,7 +6,7 @@ class InitData: 'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', '', '', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', '', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', '', '', 'visual_ghost_skynotes', 'skill_vita', 'skill_fatalis', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', '', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'skill_mithra', 'skill_toa', 'skill_nami_twilight', 'skill_ilith_ivy', 'skill_hikari_vanessa', 'skill_maya', 'skill_intruder', 'skill_luin', '', 'skill_aichan'] skill_id_uncap = ['', '', 'frags_kou', '', 'visual_ink', '', '', '', '', '', 'ilith_awakened_skill', 'eto_uncap', 'luna_uncap', 'shirabe_entry_fee', - '', '', '', '', '', 'ayu_uncap', '', 'frags_yume', '', '', '', '', '', '', '', '', 'skill_kanae_uncap', '', '', '', 'skill_doroc_uncap', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'skill_luin_uncap', '', ''] + '', '', '', '', '', 'ayu_uncap', '', 'frags_yume', '', 'skill_saya_uncap', '', '', '', '', '', '', 'skill_kanae_uncap', '', '', '', 'skill_doroc_uncap', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'skill_luin_uncap', '', ''] skill_unlock_level = [0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 14, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] @@ -30,13 +30,13 @@ class InitData: 46, 73, 95, 67, 84, 70, 78, 69, 70, 50, 80, 80, 63, 25, 50, 72, 55, 50, 95, 55, 70, 90, 70, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 54, 65.5, 59.5, 58, 96, 47, 50, 54, 90, 41] frag30 = [88, 90, 100, 75, 80, 89, 70, 79, 65, 40, 50, 90, 100, 92, 0, 61, 67, 92, 85, 50, 86, 62, - 65, 85, 67, 88, 74, 0.5, 105, 80, 105, 50, 80, 87, 81, 50, 95, 0, 80, 75, 50, 70, 80, 100, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 90, 80, 50, 51, 64, 100, 50, 58, 51, 40, 50, 80, 50, 61.6] + 65, 95, 67, 88, 74, 0.5, 105, 80, 105, 50, 80, 87, 81, 50, 95, 0, 80, 75, 50, 70, 80, 100, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 90, 80, 50, 51, 64, 100, 50, 58, 51, 40, 50, 80, 50, 61.6] prog30 = [71, 90, 80, 75, 100, 80, 90, 102, 84, 78, 110, 77, 73, 78, 0, 99, 80, 66, 46, 93, 40, 83, - 80, 90, 93, 50, 96, 88, 99, 108, 85, 80, 50, 64, 65, 100, 100, 110, 80, 50, 74, 90, 80, 80, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51, 60, 53, 85, 58, 96, 47, 50, 90, 67, 41] + 80, 100, 93, 50, 96, 88, 99, 108, 85, 80, 50, 64, 65, 100, 100, 110, 80, 50, 74, 90, 80, 80, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51, 60, 53, 85, 58, 96, 47, 50, 90, 67, 41] overdrive30 = [71, 90, 57, 75, 80, 80, 95, 79, 65, 31, 50, 69, 100, 68, 0, 78, 50, 70, 62, 59, 64, - 56, 73, 95, 67, 84, 80, 88, 79, 80, 60, 80, 80, 63, 35, 50, 82, 55, 50, 95, 55, 70, 100, 80, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 64, 65.5, 59.5, 58, 96, 47, 50, 64, 90, 41] + 56, 73, 105, 67, 84, 80, 88, 79, 80, 60, 80, 80, 63, 35, 50, 82, 55, 50, 95, 55, 70, 100, 80, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 64, 65.5, 59.5, 58, 96, 47, 50, 64, 90, 41] char_type = [1, 0, 0, 0, 0, 0, 0, 2, 0, 1, 2, 0, 0, 0, 2, 3, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 3, 0, 2, 2, 0, 0, 2, 0, 0, 2, 0, 2, 2, 1, 0, 2, 0, 0, 2, 0, 0] @@ -64,13 +64,14 @@ class InitData: 73: [{'core_id': 'core_wacca', 'amount': 15}], 30: [{'core_id': 'core_hollow', 'amount': 5}, {'core_id': 'core_sunset', 'amount': 25}], 34: [{'core_id': 'core_tanoc', 'amount': 15}], + 23: [{'core_id': 'core_desolate', 'amount': 5}, {'core_id': 'core_serene', 'amount': 25}], } cores = ['core_hollow', 'core_desolate', 'core_chunithm', 'core_crimson', - 'core_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful', 'core_course_skip_purchase', 'core_umbral', 'core_wacca', 'core_sunset', 'core_tanoc'] + 'core_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful', 'core_course_skip_purchase', 'core_umbral', 'core_wacca', 'core_sunset', 'core_tanoc', 'core_serene'] world_songs = ["babaroque", "shadesoflight", "kanagawa", "lucifer", "anokumene", "ignotus", "rabbitintheblackroom", "qualia", "redandblue", "bookmaker", "darakunosono", "espebranch", "blacklotus", "givemeanightmare", "vividtheory", "onefr", "gekka", "vexaria3", "infinityheaven3", "fairytale3", "goodtek3", "suomi", "rugie", "faintlight", "harutopia", "goodtek", "dreaminattraction", "syro", "diode", "freefall", "grimheart", "blaster", - "cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris', 'ultimatetaste', 'rgb', 'matenrou', 'dynitikos', 'amekagura', 'fantasy', 'aloneandlorn', 'felys', 'onandon', 'hotarubinoyuki', 'oblivia3', 'libertas3', 'einherjar3', 'purpleverse3', 'viciousheroism3', 'inkarusi3', 'cyberneciacatharsis3', 'alephzero', 'hellohell', 'ichirin', 'awakeninruins', 'morningloom', 'lethalvoltage', 'leaveallbehind', 'desive', 'oldschoolsalvage'] + "cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris', 'ultimatetaste', 'rgb', 'matenrou', 'dynitikos', 'amekagura', 'fantasy', 'aloneandlorn', 'felys', 'onandon', 'hotarubinoyuki', 'oblivia3', 'libertas3', 'einherjar3', 'purpleverse3', 'viciousheroism3', 'inkarusi3', 'cyberneciacatharsis3', 'alephzero', 'hellohell', 'ichirin', 'awakeninruins', 'morningloom', 'lethalvoltage', 'leaveallbehind', 'desive', 'oldschoolsalvage', 'distortionhuman'] world_unlocks = ["scenery_chap1", "scenery_chap2", "scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7", "scenery_beyond"] diff --git a/latest version/database/init/packs.json b/latest version/database/init/packs.json index 5357e1c..e0bfd6d 100644 --- a/latest version/database/init/packs.json +++ b/latest version/database/init/packs.json @@ -826,5 +826,23 @@ ], "price": 300, "orig_price": 300 + }, + { + "name": "nihil", + "items": [ + { + "type": "pack", + "id": "nihil", + "is_available": true + }, + { + "type": "core", + "amount": 5, + "id": "core_generic", + "is_available": true + } + ], + "orig_price": 500, + "price": 500 } ] \ No newline at end of file diff --git a/latest version/database/init/singles.json b/latest version/database/init/singles.json index 007cbed..8c1c4eb 100644 --- a/latest version/database/init/singles.json +++ b/latest version/database/init/singles.json @@ -1864,5 +1864,23 @@ ], "orig_price": 100, "price": 100 + }, + { + "name": "shrink", + "items": [ + { + "type": "single", + "id": "shrink", + "is_available": true + }, + { + "type": "core", + "amount": 1, + "id": "core_generic", + "is_available": true + } + ], + "orig_price": 100, + "price": 100 } ] \ No newline at end of file diff --git a/latest version/web/index.py b/latest version/web/index.py index cd8e18e..485f642 100644 --- a/latest version/web/index.py +++ b/latest version/web/index.py @@ -443,7 +443,7 @@ def all_character(): def change_character(): # 修改角色数据 skill_ids = ['No_skill', 'gauge_easy', 'note_mirror', 'gauge_hard', 'frag_plus_10_pack_stellights', 'gauge_easy|frag_plus_15_pst&prs', 'gauge_hard|fail_frag_minus_100', 'frag_plus_5_side_light', 'visual_hide_hp', 'frag_plus_5_side_conflict', 'challenge_fullcombo_0gauge', 'gauge_overflow', 'gauge_easy|note_mirror', 'note_mirror', 'visual_tomato_pack_tonesphere', - 'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', 'frags_kou', 'visual_ink', 'shirabe_entry_fee', 'frags_yume', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', 'eto_uncap', 'luna_uncap', 'frags_preferred_song', 'visual_ghost_skynotes', 'ayu_uncap', 'skill_vita', 'skill_fatalis', 'skill_reunion', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'ilith_awakened_skill', 'skill_mithra', 'skill_toa', 'skill_nami_twilight', 'skill_ilith_ivy', 'skill_hikari_vanessa', 'skill_maya', 'skill_luin', 'skill_luin_uncap', 'skill_kanae_uncap', 'skill_doroc_uncap'] + 'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', 'frags_kou', 'visual_ink', 'shirabe_entry_fee', 'frags_yume', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', 'eto_uncap', 'luna_uncap', 'frags_preferred_song', 'visual_ghost_skynotes', 'ayu_uncap', 'skill_vita', 'skill_fatalis', 'skill_reunion', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'ilith_awakened_skill', 'skill_mithra', 'skill_toa', 'skill_nami_twilight', 'skill_ilith_ivy', 'skill_hikari_vanessa', 'skill_maya', 'skill_luin', 'skill_luin_uncap', 'skill_kanae_uncap', 'skill_doroc_uncap', 'skill_saya_uncap'] return render_template('web/changechar.html', skill_ids=skill_ids) From 014531f3f16d21f12f201ce536defa117566413d Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Fri, 6 Sep 2024 22:43:38 +0800 Subject: [PATCH 26/32] [Enhance] Link Play 2.0 e.t.c. - For Arcaea 5.10.1(c) - Add support for Link Play 2.0. - New partners "Luna & Ilot" and "Eto & Hoppe" - Add support for the skill of "Eto & Hoppe". - Add support for refreshing ratings of Recent 30 via API and webpage. Note: This is a bug testing version. --- latest version/core/config_manager.py | 6 +- latest version/core/constant.py | 11 +- latest version/core/linkplay.py | 170 ++++++++- latest version/core/notification.py | 97 +++++ latest version/core/operation.py | 36 +- latest version/core/sql.py | 9 + latest version/core/user.py | 14 + latest version/core/util.py | 4 +- latest version/core/world.py | 11 +- latest version/database/init/arc_data.py | 30 +- latest version/database/init/packs.json | 18 + latest version/linkplay_server/aes.py | 4 +- latest version/linkplay_server/config.py | 13 +- latest version/linkplay_server/main.py | 20 +- latest version/linkplay_server/store.py | 130 ++++++- latest version/linkplay_server/udp_class.py | 362 ++++++++++++++---- latest version/linkplay_server/udp_parser.py | 365 ++++++++++++------- latest version/linkplay_server/udp_sender.py | 41 ++- latest version/server/multiplayer.py | 104 +++++- latest version/server/others.py | 10 + latest version/web/index.py | 2 +- 21 files changed, 1194 insertions(+), 263 deletions(-) create mode 100644 latest version/core/notification.py diff --git a/latest version/core/config_manager.py b/latest version/core/config_manager.py index 2dd32d9..3345934 100644 --- a/latest version/core/config_manager.py +++ b/latest version/core/config_manager.py @@ -12,7 +12,7 @@ class Config: SONG_FILE_HASH_PRE_CALCULATE = True - GAME_API_PREFIX = '/geriraraiu/31' # str | list[str] + GAME_API_PREFIX = '/pastatabetai/32' # str | list[str] OLD_GAME_API_PREFIX = [] # str | list[str] ALLOW_APPVERSION = [] # list[str] @@ -102,6 +102,10 @@ class Config: API_LOGIN_RATE_LIMIT = '10/5 minutes' + NOTIFICATION_EXPIRE_TIME = 3 * 60 * 1000 + + + class ConfigManager: @staticmethod diff --git a/latest version/core/constant.py b/latest version/core/constant.py index 2c50d02..69f6620 100644 --- a/latest version/core/constant.py +++ b/latest version/core/constant.py @@ -1,7 +1,7 @@ from .config_manager import Config -ARCAEA_SERVER_VERSION = 'v2.11.3.16' -ARCAEA_DATABASE_VERSION = 'v2.11.3.16' +ARCAEA_SERVER_VERSION = 'v2.11.3.17' +ARCAEA_DATABASE_VERSION = 'v2.11.3.17' ARCAEA_LOG_DATBASE_VERSION = 'v1.1' @@ -66,6 +66,13 @@ class Constant: LINKPLAY_TCP_SECRET_KEY = Config.LINKPLAY_TCP_SECRET_KEY LINKPLAY_TCP_MAX_LENGTH = 0x0FFFFFFF + + LINKPLAY_MATCH_GET_ROOMS_INTERVAL = 4 # Units: seconds + LINKPLAY_MATCH_PTT_ABS = [5, 20, 50, 100, 200, 500, 1000, 2000] + LINKPLAY_MATCH_UNLOCK_MIN = [1000, 800, 500, 300, 200, 100, 50, 1] + LINKPLAY_MATCH_TIMEOUT = 15 # Units: seconds + LINKPLAY_MATCH_MEMORY_CLEAN_INTERVAL = 60 # Units: seconds + # Well, I can't say a word when I see this. FINALE_SWITCH = [ (0x0015F0, 0x00B032), (0x014C9A, 0x014408), (0x062585, 0x02783B), diff --git a/latest version/core/linkplay.py b/latest version/core/linkplay.py index 9184512..37cd091 100644 --- a/latest version/core/linkplay.py +++ b/latest version/core/linkplay.py @@ -1,6 +1,8 @@ import socket from base64 import b64decode, b64encode from json import dumps, loads +from threading import RLock +from time import time from core.error import ArcError, Timeout @@ -36,6 +38,10 @@ def __init__(self, c=None, user_id=None) -> None: self.__song_unlock: bytes = None self.client_song_map: dict = None + self.last_match_timestamp: int = 0 + self.match_times: int = None # 已匹配次数,减 1 后乘 5 就大致是匹配时间 + self.match_room: Room = None # 匹配到的房间,这个仅用来在两个人同时匹配时使用,一人建房,通知另一个人加入 + def to_dict(self) -> dict: return { 'userId': self.user_id, @@ -55,6 +61,16 @@ def get_song_unlock(self, client_song_map: dict = None) -> bytes: self.client_song_map = client_song_map self.__song_unlock = get_song_unlock(self.client_song_map) + def calc_available_chart_num(self, song_unlock: bytes) -> int: + '''计算交叠后可用谱面数量''' + new_unlock = [i & j for i, j in zip(self.song_unlock, song_unlock)] + s = 0 + for i in range(len(new_unlock)): + for j in range(8): + if new_unlock[i] & (1 << j): + s += 1 + return s + class Room: def __init__(self) -> None: @@ -63,11 +79,14 @@ def __init__(self) -> None: self.song_unlock: bytes = None + self.share_token: str = 'abcde12345' + def to_dict(self) -> dict: return { 'roomId': str(self.room_id), 'roomCode': self.room_code, - 'orderedAllowedSongs': (b64encode(self.song_unlock)).decode() + 'orderedAllowedSongs': (b64encode(self.song_unlock)).decode(), + 'shareToken': self.share_token } @@ -97,7 +116,7 @@ def tcp(data: bytes) -> bytes: raise ArcError( 'Too long body from link play server', status=400) iv = sock.recv(12) - tag = sock.recv(12) + tag = sock.recv(16) ciphertext = sock.recv(cipher_len) received = aes_gcm_128_decrypt( RemoteMultiPlayer.TCP_AES_KEY, b'', iv, ciphertext, tag) @@ -112,7 +131,7 @@ def data_swap(self, data: dict) -> dict: iv, ciphertext, tag = aes_gcm_128_encrypt( self.TCP_AES_KEY, dumps(data).encode('utf-8'), b'') send_data = Constant.LINKPLAY_AUTHENTICATION.encode( - 'utf-8') + len(ciphertext).to_bytes(8, byteorder='little') + iv + tag[:12] + ciphertext + 'utf-8') + len(ciphertext).to_bytes(8, byteorder='little') + iv + tag + ciphertext recv_data = self.tcp(send_data) self.data_recv = loads(recv_data) @@ -126,12 +145,15 @@ def create_room(self, user: 'Player' = None) -> None: '''创建房间''' if user is not None: self.user = user - user.select_user_one_column('name') + user.select_user_about_link_play() self.data_swap({ 'endpoint': 'create_room', 'data': { 'name': self.user.name, - 'song_unlock': b64encode(self.user.song_unlock).decode('utf-8') + 'song_unlock': b64encode(self.user.song_unlock).decode('utf-8'), + 'rating_ptt': self.user.rating_ptt, + 'is_hide_rating': self.user.is_hide_rating, + 'match_times': self.user.match_times } }) @@ -151,13 +173,16 @@ def join_room(self, room: 'Room' = None, user: 'Player' = None) -> None: if room is not None: self.room = room - self.user.select_user_one_column('name') + self.user.select_user_about_link_play() self.data_swap({ 'endpoint': 'join_room', 'data': { 'name': self.user.name, 'song_unlock': b64encode(self.user.song_unlock).decode('utf-8'), - 'room_code': self.room.room_code + 'room_code': self.room.room_code, + 'rating_ptt': self.user.rating_ptt, + 'is_hide_rating': self.user.is_hide_rating, + 'match_times': self.user.match_times } }) x = self.data_recv['data'] @@ -172,10 +197,14 @@ def update_room(self, user: 'Player' = None) -> None: '''更新房间''' if user is not None: self.user = user + + self.user.select_user_about_link_play() self.data_swap({ 'endpoint': 'update_room', 'data': { - 'token': self.user.token + 'token': self.user.token, + 'rating_ptt': self.user.rating_ptt, + 'is_hide_rating': self.user.is_hide_rating } }) @@ -198,3 +227,128 @@ def get_rooms(self, offset=0, limit=50) -> dict: }) return self.data_recv['data'] + + def select_room(self, room_code: str = None, share_token: str = None) -> dict: + self.data_swap({ + 'endpoint': 'select_room', + 'data': { + 'room_code': room_code, + 'share_token': share_token + } + }) + + return self.data_recv['data'] + + def get_match_rooms(self) -> dict: + '''获取一定数量的公共房间列表''' + self.data_swap({ + 'endpoint': 'get_match_rooms', + 'data': { + 'limit': 100 + } + }) + + return self.data_recv['data'] + + +class MatchStore: + + last_get_rooms_timestamp = 0 + room_cache: 'list[Room]' = [] + + player_queue: 'dict[int, Player]' = {} + + lock = RLock() + + last_memory_clean_timestamp = 0 + + def __init__(self, c=None) -> None: + self.c = c + self.remote = RemoteMultiPlayer() + + def refresh_rooms(self): + now = time() + if now - self.last_get_rooms_timestamp < Constant.LINKPLAY_MATCH_GET_ROOMS_INTERVAL: + return + MatchStore.room_cache = self.remote.get_match_rooms()['rooms'] + MatchStore.last_get_rooms_timestamp = now + + def init_player(self, user: 'Player'): + user.match_times = 0 + MatchStore.player_queue[user.user_id] = user + user.last_match_timestamp = time() + user.c = self.c + user.select_user_about_link_play() + user.c = None + + def clear_player(self, user_id: int): + MatchStore.player_queue.pop(user_id, None) + + def clean_room_cache(self): + MatchStore.room_cache = [] + MatchStore.last_get_rooms_timestamp = 0 + + def memory_clean(self): + now = time() + if now - self.last_memory_clean_timestamp < Constant.LINKPLAY_MEMORY_CLEAN_INTERVAL: + return + with self.lock: + for i in MatchStore.player_queue: + if now - i.last_match_timestamp > Constant.LINKPLAY_MATCH_TIMEOUT: + self.clear_player(i) + + def match(self, user_id: int): + user = MatchStore.player_queue.get(user_id) + if user is None: + raise ArcError( + f'User `{user_id}` not found in match queue.', code=999) + + if user.match_room is not None: + # 二人开新房,第二人加入 + user.c = self.c + self.remote.join_room(user.match_room, user) + self.clear_player(user_id) + return self.remote.to_dict() + + self.refresh_rooms() + + rule = min(user.match_times, len(Constant.LINKPLAY_MATCH_PTT_ABS) - + 1, len(Constant.LINKPLAY_MATCH_UNLOCK_MIN) - 1) + ptt_abs = Constant.LINKPLAY_MATCH_PTT_ABS[rule] + unlock_min = Constant.LINKPLAY_MATCH_UNLOCK_MIN[rule] + + # 加入已有房间 + for i in MatchStore.room_cache: + f = True + for j in i['players']: + if j['player_id'] != 0 and abs(user.rating_ptt - j['rating_ptt']) >= ptt_abs: + f = False + break + + if f and user.calc_available_chart_num(b64decode(i['song_unlock'])) >= unlock_min and ((time() + 2) * 1000000 < i['next_state_timestamp'] or i['next_state_timestamp'] <= 0): + room = Room() + room.room_code = i['room_code'] + user.c = self.c + self.remote.join_room(room, user) + self.clean_room_cache() + self.clear_player(user_id) + return self.remote.to_dict() + + now = time() + + # 二人开新房,第一人开房 + for p in MatchStore.player_queue.values(): + if p.user_id == user_id or now - p.last_match_timestamp > Constant.LINKPLAY_MATCH_TIMEOUT: + continue + new_rule = min(rule, p.match_times) + if abs(user.rating_ptt - p.rating_ptt) < Constant.LINKPLAY_MATCH_PTT_ABS[new_rule] and user.calc_available_chart_num(p.song_unlock) >= Constant.LINKPLAY_MATCH_UNLOCK_MIN[new_rule]: + user.c = self.c + self.remote.create_room(user) + self.clear_player(user_id) + p.match_room = self.remote.room + return self.remote.to_dict() + + user.match_times += 1 + user.last_match_timestamp = now + + return None diff --git a/latest version/core/notification.py b/latest version/core/notification.py new file mode 100644 index 0000000..79d096a --- /dev/null +++ b/latest version/core/notification.py @@ -0,0 +1,97 @@ +from .config_manager import Config +from .user import User +from .sql import Connect + +from time import time + + +class BaseNotification: + + notification_type = None + + def __init__(self, c_m=None) -> None: + self.receiver = None + self.sender = None + self.timestamp = None + self.content = None + + self.c_m = c_m + + @property + def is_expired(self) -> bool: + now = round(time() * 1000) + return now - self.timestamp > Config.NOTIFICATION_EXPIRE_TIME + + def to_dict(self) -> dict: + raise NotImplementedError() + + def insert(self): + self.c_m.execute( + '''select max(id) from notification where user_id = ?''', (self.receiver.user_id,)) + x = self.c_m.fetchone() + if x is None or x[0] is None: + x = 0 + else: + x = x[0] + 1 + + self.c_m.execute( + '''insert into notification values (?, ?, ?, ?, ?, ?, ?)''', + (self.receiver.user_id, x, self.notification_type, self.content, + self.sender.user_id, self.sender.name, self.timestamp) + ) + + +class RoomInviteNotification(BaseNotification): + + notification_type = 'room_inv' + + @classmethod + def from_list(cls, l: list, user: User = None) -> 'RoomInviteNotification': + x = cls() + x.sender = User() + x.sender.user_id = l[2] + x.sender.name = l[3] + x.content = l[1] + x.timestamp = l[4] + x.receiver = user + return x + + @classmethod + def from_sender(cls, sender: User, receiver: User, share_token: str, c_m) -> 'RoomInviteNotification': + x = cls() + x.c_m = c_m + x.sender = sender + x.receiver = receiver + x.content = share_token + x.timestamp = round(time() * 1000) + return x + + def to_dict(self) -> dict: + return { + 'sender': self.sender.name, + 'type': self.notification_type, + 'shareToken': self.content, + 'sendTs': self.timestamp + } + + +class NotificationFactory: + def __init__(self, c_m: Connect, user=None): + self.c_m = c_m + self.user = user + + def get_notification(self) -> 'list[BaseNotification]': + r = [] + + self.c_m.execute('''select type, content, sender_user_id, sender_name, timestamp from notification where user_id = ?''', + (self.user.user_id,)) + for i in self.c_m.fetchall(): + x = None + if i[0] == 'room_inv': + x = RoomInviteNotification.from_list(i, self.user) + + if x is not None and not x.is_expired: + r.append(x) + self.c_m.execute( + '''delete from notification where user_id = ?''', (self.user.user_id,)) + return r diff --git a/latest version/core/operation.py b/latest version/core/operation.py index 7e4a834..586188a 100644 --- a/latest version/core/operation.py +++ b/latest version/core/operation.py @@ -37,7 +37,7 @@ def run(self): # 但其实还是很慢 with Connect() as c: c.execute( - '''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn from chart''') + '''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn, rating_etr from chart''') x = c.fetchall() songs = [i[0] for i in x] @@ -45,7 +45,7 @@ def run(self): f'''update best_score set rating=0 where song_id not in ({','.join(['?']*len(songs))})''', songs) for i in x: - for j in range(0, 4): + for j in range(0, 5): defnum = -10 # 没在库里的全部当做定数 -10 if i[j+1] is not None and i[j+1] > 0: defnum = float(i[j+1]) / 10 @@ -66,6 +66,38 @@ def run(self): Sql(c).update_many('best_score', ['rating', 'score_v2'], values, [ 'user_id', 'song_id', 'difficulty'], where_values) + # 更新 recent30 + song_defum: 'dict[str, list[int]]' = {} + for i in x: + song_defum[i[0]] = [] + for j in range(0, 5): + defnum = -10 + if i[j+1] is not None and i[j+1] > 0: + defnum = float(i[j+1]) / 10 + song_defum[i[0]].append(defnum) + + users = c.execute('''select user_id from user''').fetchall() + for i in users: + values = [] + where_values = [] + user_id = i[0] + c.execute( + '''select r_index, song_id, difficulty, score from recent30 where user_id = ?''', (user_id,)) + for j in c.fetchall(): + if j[1] in song_defum: + defnum = song_defum[j[1]][j[2]] + else: + defnum = -10 + ptt = Score.calculate_rating(defnum, j[3]) + ptt = max(ptt, 0) + + values.append((ptt,)) + where_values.append((user_id, j[0])) + + if values: + Sql(c).update_many('recent30', ['rating'], values, [ + 'user_id', 'r_index'], where_values) + class RefreshSongFileCache(BaseOperation): ''' diff --git a/latest version/core/sql.py b/latest version/core/sql.py index 6a679e6..6852427 100644 --- a/latest version/core/sql.py +++ b/latest version/core/sql.py @@ -510,6 +510,15 @@ def __init__(self): file_path text, time int, device_id text);''') self.c.execute( '''create index if not exists download_token_1 on download_token (song_id, file_name);''') + self.c.execute(''' + create table if not exists notification( + user_id int, id int, + type text, content text, + sender_user_id int, sender_name text, + timestamp int, + primary key(user_id, id) + ) + ''') self.conn.commit() diff --git a/latest version/core/user.py b/latest version/core/user.py index 05ab4ee..1974764 100644 --- a/latest version/core/user.py +++ b/latest version/core/user.py @@ -647,6 +647,20 @@ def select_user_about_world_play(self) -> None: self.beyond_boost_gauge = x[8] if x[8] else 0 self.kanae_stored_prog = x[9] if x[9] else 0 + def select_user_about_link_play(self) -> None: + ''' + 查询 user 表有关 link play 的信息 + ''' + self.c.execute( + '''select name, rating_ptt, is_hide_rating from user where user_id=?''', (self.user_id,)) + x = self.c.fetchone() + if not x: + raise NoData('No user.', 108, -3) + + self.name = x[0] + self.rating_ptt = x[1] + self.is_hide_rating = x[2] == 1 + @property def global_rank(self) -> int: '''用户世界排名,如果超过设定最大值,返回0''' diff --git a/latest version/core/util.py b/latest version/core/util.py index b0ed808..1b0aec5 100644 --- a/latest version/core/util.py +++ b/latest version/core/util.py @@ -10,7 +10,7 @@ def aes_gcm_128_encrypt(key, plaintext, associated_data): iv = os.urandom(12) encryptor = Cipher( algorithms.AES(key), - modes.GCM(iv, min_tag_length=12), + modes.GCM(iv, min_tag_length=16), ).encryptor() encryptor.authenticate_additional_data(associated_data) ciphertext = encryptor.update(plaintext) + encryptor.finalize() @@ -20,7 +20,7 @@ def aes_gcm_128_encrypt(key, plaintext, associated_data): def aes_gcm_128_decrypt(key, associated_data, iv, ciphertext, tag): decryptor = Cipher( algorithms.AES(key), - modes.GCM(iv, tag, min_tag_length=12), + modes.GCM(iv, tag, min_tag_length=16), ).decryptor() decryptor.authenticate_additional_data(associated_data) return decryptor.update(ciphertext) + decryptor.finalize() diff --git a/latest version/core/world.py b/latest version/core/world.py index a82b48e..76774c4 100644 --- a/latest version/core/world.py +++ b/latest version/core/world.py @@ -490,7 +490,8 @@ def after_climb(self) -> None: 'skill_amane': self._skill_amane, 'skill_maya': self._skill_maya, 'luna_uncap': self._luna_uncap, - 'skill_kanae_uncap': self._skill_kanae_uncap + 'skill_kanae_uncap': self._skill_kanae_uncap, + 'skill_eto_hoppe': self._skill_eto_hoppe, } if self.character_used.skill_id_displayed in factory_dict: factory_dict[self.character_used.skill_id_displayed]() @@ -633,6 +634,14 @@ def _skill_kanae_uncap(self) -> None: self.kanae_stored_progress = self.progress_normalized self.user.current_map.reclimb(self.final_progress) + def _skill_eto_hoppe(self) -> None: + ''' + eto_hoppe 技能,体力大于等于 6 格时,世界进度翻倍 + ''' + if self.user.stamina.stamina >= 6: + self.character_bonus_progress_normalized = self.progress_normalized + self.user.current_map.reclimb(self.final_progress) + class BaseWorldPlay(WorldSkillMixin): ''' diff --git a/latest version/database/init/arc_data.py b/latest version/database/init/arc_data.py index 87928a0..3a48f51 100644 --- a/latest version/database/init/arc_data.py +++ b/latest version/database/init/arc_data.py @@ -1,45 +1,45 @@ class InitData: char = ['hikari', 'tairitsu', 'kou', 'sapphire', 'lethe', 'hikari&tairitsu(reunion)', 'Tairitsu(Axium)', 'Tairitsu(Grievous Lady)', 'stella', 'Hikari & Fisica', 'ilith', 'eto', 'luna', 'shirabe', 'Hikari(Zero)', 'Hikari(Fracture)', 'Hikari(Summer)', 'Tairitsu(Summer)', 'Tairitsu & Trin', - 'ayu', 'Eto & Luna', 'yume', 'Seine & Hikari', 'saya', 'Tairitsu & Chuni Penguin', 'Chuni Penguin', 'haruna', 'nono', 'MTA-XXX', 'MDA-21', 'kanae', 'Hikari(Fantasia)', 'Tairitsu(Sonata)', 'sia', 'DORO*C', 'Tairitsu(Tempest)', 'brillante', 'Ilith(Summer)', 'etude', 'Alice & Tenniel', 'Luna & Mia', 'areus', 'seele', 'isabelle', 'mir', 'lagrange', 'linka', 'nami', 'Saya & Elizabeth', 'lily', 'kanae(midsummer)', 'alice&tenniel(minuet)', 'tairitsu(elegy)', 'marija', 'vita', 'hikari(fatalis)', 'saki', 'setsuna', 'amane', 'kou(winter)', 'lagrange(aria)', 'lethe(apophenia)', 'shama(UNiVERSE)', 'milk(UNiVERSE)', 'shikoku', 'mika yurisaki', 'Mithra Tercera', 'Toa Kozukata', 'Nami(Twilight)', 'Ilith & Ivy', 'Hikari & Vanessa', 'Maya', 'Insight(Ascendant - 8th Seeker)', 'Luin', 'Vita(Cadenza)', 'Ai-chan'] + 'ayu', 'Eto & Luna', 'yume', 'Seine & Hikari', 'saya', 'Tairitsu & Chuni Penguin', 'Chuni Penguin', 'haruna', 'nono', 'MTA-XXX', 'MDA-21', 'kanae', 'Hikari(Fantasia)', 'Tairitsu(Sonata)', 'sia', 'DORO*C', 'Tairitsu(Tempest)', 'brillante', 'Ilith(Summer)', 'etude', 'Alice & Tenniel', 'Luna & Mia', 'areus', 'seele', 'isabelle', 'mir', 'lagrange', 'linka', 'nami', 'Saya & Elizabeth', 'lily', 'kanae(midsummer)', 'alice&tenniel(minuet)', 'tairitsu(elegy)', 'marija', 'vita', 'hikari(fatalis)', 'saki', 'setsuna', 'amane', 'kou(winter)', 'lagrange(aria)', 'lethe(apophenia)', 'shama(UNiVERSE)', 'milk(UNiVERSE)', 'shikoku', 'mika yurisaki', 'Mithra Tercera', 'Toa Kozukata', 'Nami(Twilight)', 'Ilith & Ivy', 'Hikari & Vanessa', 'Maya', 'Insight(Ascendant - 8th Seeker)', 'Luin', 'Vita(Cadenza)', 'Ai-chan', 'Luna & Ilot', 'Eto & Hoppe'] skill_id = ['gauge_easy', '', '', '', 'note_mirror', 'skill_reunion', '', 'gauge_hard', 'frag_plus_10_pack_stellights', 'gauge_easy|frag_plus_15_pst&prs', 'gauge_hard|fail_frag_minus_100', 'frag_plus_5_side_light', 'visual_hide_hp', 'frag_plus_5_side_conflict', 'challenge_fullcombo_0gauge', 'gauge_overflow', 'gauge_easy|note_mirror', 'note_mirror', 'visual_tomato_pack_tonesphere', - 'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', '', '', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', '', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', '', '', 'visual_ghost_skynotes', 'skill_vita', 'skill_fatalis', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', '', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'skill_mithra', 'skill_toa', 'skill_nami_twilight', 'skill_ilith_ivy', 'skill_hikari_vanessa', 'skill_maya', 'skill_intruder', 'skill_luin', '', 'skill_aichan'] + 'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', '', '', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', '', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', '', '', 'visual_ghost_skynotes', 'skill_vita', 'skill_fatalis', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', '', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'skill_mithra', 'skill_toa', 'skill_nami_twilight', 'skill_ilith_ivy', 'skill_hikari_vanessa', 'skill_maya', 'skill_intruder', 'skill_luin', '', 'skill_aichan', 'skill_luna_ilot', 'skill_eto_hoppe'] skill_id_uncap = ['', '', 'frags_kou', '', 'visual_ink', '', '', '', '', '', 'ilith_awakened_skill', 'eto_uncap', 'luna_uncap', 'shirabe_entry_fee', - '', '', '', '', '', 'ayu_uncap', '', 'frags_yume', '', 'skill_saya_uncap', '', '', '', '', '', '', 'skill_kanae_uncap', '', '', '', 'skill_doroc_uncap', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'skill_luin_uncap', '', ''] + '', '', '', '', '', 'ayu_uncap', '', 'frags_yume', '', 'skill_saya_uncap', '', '', '', '', '', '', 'skill_kanae_uncap', '', '', '', 'skill_doroc_uncap', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'skill_luin_uncap', '', '', '', ''] skill_unlock_level = [0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, - 0, 0, 0, 8, 0, 14, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + 0, 0, 0, 8, 0, 14, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] frag1 = [55, 55, 60, 50, 47, 79, 47, 57, 41, 22, 50, 54, 60, 56, 78, 42, 41, 61, 52, 50, 52, 32, - 42, 55, 45, 58, 43, 0.5, 68, 50, 62, 45, 45, 52, 44, 27, 59, 0, 45, 50, 50, 47, 47, 61, 43, 42, 38, 25, 58, 50, 61, 45, 45, 38, 34, 27, 18, 56, 47, 30, 45, 57, 56, 47, 33, 26, 29, 66, 40, 33, 51, 27, 50, 60, 45, 50] + 42, 55, 45, 58, 43, 0.5, 68, 50, 62, 45, 45, 52, 44, 27, 59, 0, 45, 50, 50, 47, 47, 61, 43, 42, 38, 25, 58, 50, 61, 45, 45, 38, 34, 27, 18, 56, 47, 30, 45, 57, 56, 47, 33, 26, 29, 66, 40, 33, 51, 27, 50, 60, 45, 50, 38, 22] prog1 = [35, 55, 47, 50, 60, 70, 60, 70, 58, 45, 70, 45, 42, 46, 61, 67, 49, 44, 28, 45, 24, 46, 52, - 59, 62, 33, 58, 25, 63, 69, 50, 45, 45, 51, 34, 70, 62, 70, 45, 32, 32, 61, 47, 47, 37, 42, 50, 50, 45, 41, 61, 45, 45, 58, 50, 130, 18, 57, 55, 50, 45, 70, 37.5, 29, 44, 26, 26, 35, 40, 33, 58, 31, 50, 50, 45, 41] + 59, 62, 33, 58, 25, 63, 69, 50, 45, 45, 51, 34, 70, 62, 70, 45, 32, 32, 61, 47, 47, 37, 42, 50, 50, 45, 41, 61, 45, 45, 58, 50, 130, 18, 57, 55, 50, 45, 70, 37.5, 29, 44, 26, 26, 35, 40, 33, 58, 31, 50, 50, 45, 41, 12, 31] overdrive1 = [35, 55, 25, 50, 47, 70, 72, 57, 41, 7, 10, 32, 65, 31, 61, 53, 31, 47, 38, 12, 39, 18, - 48, 65, 45, 55, 44, 25, 46, 44, 33, 45, 45, 37, 25, 27, 50, 20, 45, 63, 21, 47, 61, 47, 65, 80, 38, 30, 49, 15, 34, 45, 45, 38, 67, 120, 44, 33, 55, 50, 45, 57, 31, 29, 65, 26, 29, 42.5, 40, 33, 58, 31, 50, 34, 45, 41] + 48, 65, 45, 55, 44, 25, 46, 44, 33, 45, 45, 37, 25, 27, 50, 20, 45, 63, 21, 47, 61, 47, 65, 80, 38, 30, 49, 15, 34, 45, 45, 38, 67, 120, 44, 33, 55, 50, 45, 57, 31, 29, 65, 26, 29, 42.5, 40, 33, 58, 31, 50, 34, 45, 41, 12, 19] frag20 = [78, 80, 90, 75, 70, 79, 70, 79, 65, 40, 50, 80, 90, 82, 0, 61, 67, 92, 85, 50, 86, 52, - 65, 85, 67, 88, 64, 0.5, 95, 70, 95, 50, 80, 87, 71, 50, 85, 0, 80, 75, 50, 70, 70, 90, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 90, 80, 50, 51, 54, 100, 50, 58, 51, 40, 50, 70, 50, 61.6] + 65, 85, 67, 88, 64, 0.5, 95, 70, 95, 50, 80, 87, 71, 50, 85, 0, 80, 75, 50, 70, 70, 90, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 90, 80, 50, 51, 54, 100, 50, 58, 51, 40, 50, 70, 50, 61.6, 48, 37] prog20 = [61, 80, 70, 75, 90, 70, 90, 102, 84, 78, 105, 67, 63, 68, 0, 99, 80, 66, 46, 83, 40, 73, - 80, 90, 93, 50, 86, 78, 89, 98, 75, 80, 50, 64, 55, 100, 90, 110, 80, 50, 74, 90, 70, 70, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51, 50, 53, 85, 58, 96, 47, 50, 80, 67, 41] + 80, 90, 93, 50, 86, 78, 89, 98, 75, 80, 50, 64, 55, 100, 90, 110, 80, 50, 74, 90, 70, 70, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51, 50, 53, 85, 58, 96, 47, 50, 80, 67, 41, 55, 50] overdrive20 = [61, 80, 47, 75, 70, 70, 95, 79, 65, 31, 50, 59, 90, 58, 0, 78, 50, 70, 62, 49, 64, - 46, 73, 95, 67, 84, 70, 78, 69, 70, 50, 80, 80, 63, 25, 50, 72, 55, 50, 95, 55, 70, 90, 70, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 54, 65.5, 59.5, 58, 96, 47, 50, 54, 90, 41] + 46, 73, 95, 67, 84, 70, 78, 69, 70, 50, 80, 80, 63, 25, 50, 72, 55, 50, 95, 55, 70, 90, 70, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 54, 65.5, 59.5, 58, 96, 47, 50, 54, 90, 41, 34, 30] frag30 = [88, 90, 100, 75, 80, 89, 70, 79, 65, 40, 50, 90, 100, 92, 0, 61, 67, 92, 85, 50, 86, 62, - 65, 95, 67, 88, 74, 0.5, 105, 80, 105, 50, 80, 87, 81, 50, 95, 0, 80, 75, 50, 70, 80, 100, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 90, 80, 50, 51, 64, 100, 50, 58, 51, 40, 50, 80, 50, 61.6] + 65, 95, 67, 88, 74, 0.5, 105, 80, 105, 50, 80, 87, 81, 50, 95, 0, 80, 75, 50, 70, 80, 100, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 90, 80, 50, 51, 64, 100, 50, 58, 51, 40, 50, 80, 50, 61.6, 48, 37] prog30 = [71, 90, 80, 75, 100, 80, 90, 102, 84, 78, 110, 77, 73, 78, 0, 99, 80, 66, 46, 93, 40, 83, - 80, 100, 93, 50, 96, 88, 99, 108, 85, 80, 50, 64, 65, 100, 100, 110, 80, 50, 74, 90, 80, 80, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51, 60, 53, 85, 58, 96, 47, 50, 90, 67, 41] + 80, 100, 93, 50, 96, 88, 99, 108, 85, 80, 50, 64, 65, 100, 100, 110, 80, 50, 74, 90, 80, 80, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51, 60, 53, 85, 58, 96, 47, 50, 90, 67, 41, 55, 50] overdrive30 = [71, 90, 57, 75, 80, 80, 95, 79, 65, 31, 50, 69, 100, 68, 0, 78, 50, 70, 62, 59, 64, - 56, 73, 105, 67, 84, 80, 88, 79, 80, 60, 80, 80, 63, 35, 50, 82, 55, 50, 95, 55, 70, 100, 80, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 64, 65.5, 59.5, 58, 96, 47, 50, 64, 90, 41] + 56, 73, 105, 67, 84, 80, 88, 79, 80, 60, 80, 80, 63, 35, 50, 82, 55, 50, 95, 55, 70, 100, 80, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 64, 65.5, 59.5, 58, 96, 47, 50, 64, 90, 41, 34, 30] char_type = [1, 0, 0, 0, 0, 0, 0, 2, 0, 1, 2, 0, 0, 0, 2, 3, 1, 0, 0, 0, 1, - 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 3, 0, 2, 2, 0, 0, 2, 0, 0, 2, 0, 2, 2, 1, 0, 2, 0, 0, 2, 0, 0] + 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 3, 0, 2, 2, 0, 0, 2, 0, 0, 2, 0, 2, 2, 1, 0, 2, 0, 0, 2, 0, 0, 0, 0] char_core = { 0: [{'core_id': 'core_hollow', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}], @@ -71,7 +71,7 @@ class InitData: 'core_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful', 'core_course_skip_purchase', 'core_umbral', 'core_wacca', 'core_sunset', 'core_tanoc', 'core_serene'] world_songs = ["babaroque", "shadesoflight", "kanagawa", "lucifer", "anokumene", "ignotus", "rabbitintheblackroom", "qualia", "redandblue", "bookmaker", "darakunosono", "espebranch", "blacklotus", "givemeanightmare", "vividtheory", "onefr", "gekka", "vexaria3", "infinityheaven3", "fairytale3", "goodtek3", "suomi", "rugie", "faintlight", "harutopia", "goodtek", "dreaminattraction", "syro", "diode", "freefall", "grimheart", "blaster", - "cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris', 'ultimatetaste', 'rgb', 'matenrou', 'dynitikos', 'amekagura', 'fantasy', 'aloneandlorn', 'felys', 'onandon', 'hotarubinoyuki', 'oblivia3', 'libertas3', 'einherjar3', 'purpleverse3', 'viciousheroism3', 'inkarusi3', 'cyberneciacatharsis3', 'alephzero', 'hellohell', 'ichirin', 'awakeninruins', 'morningloom', 'lethalvoltage', 'leaveallbehind', 'desive', 'oldschoolsalvage', 'distortionhuman'] + "cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris', 'ultimatetaste', 'rgb', 'matenrou', 'dynitikos', 'amekagura', 'fantasy', 'aloneandlorn', 'felys', 'onandon', 'hotarubinoyuki', 'oblivia3', 'libertas3', 'einherjar3', 'purpleverse3', 'viciousheroism3', 'inkarusi3', 'cyberneciacatharsis3', 'alephzero', 'hellohell', 'ichirin', 'awakeninruins', 'morningloom', 'lethalvoltage', 'leaveallbehind', 'desive', 'oldschoolsalvage', 'distortionhuman', 'epitaxy'] world_unlocks = ["scenery_chap1", "scenery_chap2", "scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7", "scenery_beyond"] diff --git a/latest version/database/init/packs.json b/latest version/database/init/packs.json index e0bfd6d..791f749 100644 --- a/latest version/database/init/packs.json +++ b/latest version/database/init/packs.json @@ -844,5 +844,23 @@ ], "orig_price": 500, "price": 500 + }, + { + "name": "rotaeno", + "items": [ + { + "type": "pack", + "id": "rotaeno", + "is_available": true + }, + { + "type": "core", + "amount": 5, + "id": "core_generic", + "is_available": true + } + ], + "orig_price": 500, + "price": 500 } ] \ No newline at end of file diff --git a/latest version/linkplay_server/aes.py b/latest version/linkplay_server/aes.py index 7248382..c167af3 100644 --- a/latest version/linkplay_server/aes.py +++ b/latest version/linkplay_server/aes.py @@ -8,7 +8,7 @@ def encrypt(key, plaintext, associated_data): iv = urandom(12) encryptor = Cipher( algorithms.AES(key), - modes.GCM(iv, min_tag_length=12), + modes.GCM(iv, min_tag_length=16), ).encryptor() encryptor.authenticate_additional_data(associated_data) ciphertext = encryptor.update(plaintext) + encryptor.finalize() @@ -18,7 +18,7 @@ def encrypt(key, plaintext, associated_data): def decrypt(key, associated_data, iv, ciphertext, tag): decryptor = Cipher( algorithms.AES(key), - modes.GCM(iv, tag, min_tag_length=12), + modes.GCM(iv, tag, min_tag_length=16), ).decryptor() decryptor.authenticate_additional_data(associated_data) return decryptor.update(ciphertext) + decryptor.finalize() diff --git a/latest version/linkplay_server/config.py b/latest version/linkplay_server/config.py index de43994..25fd797 100644 --- a/latest version/linkplay_server/config.py +++ b/latest version/linkplay_server/config.py @@ -24,9 +24,16 @@ class Config: COMMAND_INTERVAL = 1000000 - COUNTDOWM_TIME = 3999 - PLAYER_PRE_TIMEOUT = 3000000 - PLAYER_TIMEOUT = 20000000 + PLAYER_TIMEOUT = 15000000 LINK_PLAY_UNLOCK_LENGTH = 512 + + COUNTDOWN_SONG_READY = 4 * 1000000 + COUNTDOWN_SONG_START = 6 * 1000000 + + # 计时模式 + COUNTDOWN_MATCHING = 15 * 1000000 + COUNTDOWN_SELECT_SONG = 45 * 1000000 + COUNTDOWN_SELECT_DIFFICULTY = 45 * 1000000 + COUNTDOWN_RESULT = 60 * 1000000 diff --git a/latest version/linkplay_server/main.py b/latest version/linkplay_server/main.py index 1963f05..292e9f9 100644 --- a/latest version/linkplay_server/main.py +++ b/latest version/linkplay_server/main.py @@ -1,4 +1,4 @@ -# import binascii +import binascii import logging import socketserver import threading @@ -21,11 +21,12 @@ def handle(self): try: token = client_msg[:8] iv = client_msg[8:20] - tag = client_msg[20:32] - ciphertext = client_msg[32:] - if bi(token) not in Store.link_play_data: + tag = client_msg[20:36] + ciphertext = client_msg[36:] + + user = Store.link_play_data.get(bi(token)) + if user is None: return None - user = Store.link_play_data[bi(token)] plaintext = decrypt(user['key'], b'', iv, ciphertext, tag) except Exception as e: @@ -52,8 +53,7 @@ def handle(self): # logging.info( # f'UDP-To-{self.client_address[0]}-{binascii.b2a_hex(i)}') - server.sendto(token + iv + tag[:12] + - ciphertext, self.client_address) + server.sendto(token + iv + tag + ciphertext, self.client_address) AUTH_LEN = len(Config.AUTHENTICATION) @@ -77,7 +77,7 @@ def handle(self): return None iv = self.rfile.read(12) - tag = self.rfile.read(12) + tag = self.rfile.read(16) ciphertext = self.rfile.read(cipher_len) self.data = decrypt(TCP_AES_KEY, b'', iv, ciphertext, tag) @@ -96,8 +96,8 @@ def handle(self): if Config.DEBUG: logging.info(f'TCP-To-{self.client_address[0]}-{r}') iv, ciphertext, tag = encrypt(TCP_AES_KEY, r.encode('utf-8'), b'') - r = len(ciphertext).to_bytes(8, byteorder='little') + \ - iv + tag[:12] + ciphertext + r = len(ciphertext).to_bytes( + 8, byteorder='little') + iv + tag + ciphertext except Exception as e: logging.error(e) return None diff --git a/latest version/linkplay_server/store.py b/latest version/linkplay_server/store.py index 2ca4e7d..7120a44 100644 --- a/latest version/linkplay_server/store.py +++ b/latest version/linkplay_server/store.py @@ -7,15 +7,18 @@ from .config import Config from .udp_class import Player, Room, bi +from .udp_sender import CommandSender class Store: # token: {'key': key, 'room': Room, 'player_index': player_index, 'player_id': player_id} link_play_data = {} - room_id_dict = {} # 'room_id': Room + room_id_dict: "dict[int, Room]" = {} # 'room_id': Room room_code_dict = {} # 'room_code': Room player_dict = {} # 'player_id' : Player + share_token_dict = {} # 'share_token': Room + lock = RLock() @@ -28,6 +31,14 @@ def random_room_code(): return re +def random_share_token(): + CHARSET = 'abcdefghijklmnopqrstuvwxyz0123456789' + re = '' + for _ in range(10): + re += CHARSET[randint(0, 35)] + return re + + def unique_random(dataset, length=8, random_func=None): '''无重复随机,且默认非0,没处理可能的死循环''' if random_func is None: @@ -45,18 +56,27 @@ def clear_player(token): # 清除玩家信息和token player_id = Store.link_play_data[token]['player_id'] logging.info(f'Clean player `{Store.player_dict[player_id].name}`') - del Store.player_dict[player_id] - del Store.link_play_data[token] + with Store.lock: + if player_id in Store.player_dict: + del Store.player_dict[player_id] + if token in Store.link_play_data: + del Store.link_play_data[token] def clear_room(room): # 清除房间信息 room_id = room.room_id room_code = room.room_code + share_token = room.share_token logging.info(f'Clean room `{room_code}`') - del Store.room_id_dict[room_id] - del Store.room_code_dict[room_code] - del room + with Store.lock: + if room_id in Store.room_id_dict: + del Store.room_id_dict[room_id] + if room_code in Store.room_code_dict: + del Store.room_code_dict[room_code] + if share_token in Store.share_token_dict: + del Store.share_token_dict[share_token] + del room def memory_clean(now): @@ -92,6 +112,8 @@ class TCPRouter: 'join_room', 'update_room', 'get_rooms', + 'select_room', + 'get_match_rooms' } def __init__(self, raw_data: 'dict | list'): @@ -115,7 +137,7 @@ def clean_check(): def handle(self) -> dict: self.clean_check() if self.endpoint not in self.router: - return None + return {'code': 999} try: r = getattr(self, self.endpoint)() except Exception as e: @@ -144,7 +166,7 @@ def generate_room() -> Room: room_id = unique_random(Store.room_id_dict) room = Room() room.room_id = room_id - room.timestamp = round(time() * 1000) + room.timestamp = round(time() * 1000000) Store.room_id_dict[room_id] = room room_code = unique_random( @@ -152,6 +174,11 @@ def generate_room() -> Room: room.room_code = room_code Store.room_code_dict[room_code] = room + share_token = unique_random( + Store.share_token_dict, random_func=random_share_token) + room.share_token = share_token + Store.share_token_dict[share_token] = room + return room def create_room(self) -> dict: @@ -160,6 +187,9 @@ def create_room(self) -> dict: # song_unlock: base64 str name = self.data['name'] song_unlock = b64decode(self.data['song_unlock']) + rating_ptt = self.data.get('rating_ptt', 0) + is_hide_rating = self.data.get('is_hide_rating', False) + match_times = self.data.get('match_times', None) key = urandom(16) with Store.lock: @@ -167,6 +197,9 @@ def create_room(self) -> dict: player = self.generate_player(name) player.song_unlock = song_unlock + player.rating_ptt = rating_ptt + player.is_hide_rating = is_hide_rating + player.player_index = 0 room.song_unlock = song_unlock room.host_id = player.player_id room.players[0] = player @@ -174,6 +207,12 @@ def create_room(self) -> dict: token = room.room_id player.token = token + # 匹配模式追加 + if match_times is not None: + room.is_public = 1 + room.round_mode = 3 + room.timed_mode = 1 + Store.link_play_data[token] = { 'key': key, 'room': room, @@ -198,6 +237,9 @@ def join_room(self) -> 'dict | int': key = urandom(16) name = self.data['name'] song_unlock = b64decode(self.data['song_unlock']) + rating_ptt = self.data.get('rating_ptt', 0) + is_hide_rating = self.data.get('is_hide_rating', False) + match_times = self.data.get('match_times', None) with Store.lock: if room_code not in Store.room_code_dict: @@ -212,7 +254,7 @@ def join_room(self) -> 'dict | int': if player_num == 0: # 房间不存在 return 1202 - if room.state != 2: + if room.state not in (0, 1, 2) or (room.is_public and match_times is None): # 无法加入 return 1205 @@ -221,16 +263,18 @@ def join_room(self) -> 'dict | int': player = self.generate_player(name) player.token = token player.song_unlock = song_unlock + player.rating_ptt = rating_ptt + player.is_hide_rating = is_hide_rating room.update_song_unlock() for i in range(4): if room.players[i].player_id == 0: room.players[i] = player - player_index = i + player.player_index = i break Store.link_play_data[token] = { 'key': key, 'room': room, - 'player_index': player_index, + 'player_index': player.player_index, 'player_id': player.player_id } @@ -248,11 +292,23 @@ def update_room(self) -> dict: # 房间信息更新 # data = ['3', token] token = int(self.data['token']) + rating_ptt = self.data.get('rating_ptt', 0) + is_hide_rating = self.data.get('is_hide_rating', False) + with Store.lock: if token not in Store.link_play_data: return 108 r = Store.link_play_data[token] room = r['room'] + + # 更新玩家信息 + player_index = r['player_index'] + player = room.players[player_index] + player.rating_ptt = rating_ptt + player.is_hide_rating = is_hide_rating + cs = CommandSender(room) + room.command_queue.append(cs.command_12(player_index)) + logging.info(f'TCP-Room `{room.room_code}` info update') return { 'room_code': room.room_code, @@ -300,3 +356,55 @@ def get_rooms(self) -> dict: 'has_more': f2, 'rooms': rooms } + + def select_room(self) -> dict: + # 查询房间信息 + + room_code = self.data.get('room_code', None) + share_token = self.data.get('share_token', None) + + if room_code is not None: + room = Store.room_code_dict.get(room_code, None) + elif share_token is not None: + room = Store.share_token_dict.get(share_token, None) + if room is None: + return 108 + + return { + 'room_id': room.room_id, + 'room_code': room.room_code, + 'share_token': room.share_token, + 'is_enterable': room.is_enterable, + 'is_matchable': room.is_matchable, + 'is_playing': room.is_playing, + 'is_public': room.is_public == 1, + 'timed_mode': room.timed_mode == 1, + } + + def get_match_rooms(self): + n = 0 + rooms = [] + + for room in Store.room_id_dict.values(): + if not room.is_matchable: + continue + + rooms.append({ + 'room_id': room.room_id, + 'room_code': room.room_code, + 'share_token': room.share_token, + 'is_matchable': room.is_matchable, + 'next_state_timestamp': room.next_state_timestamp, + 'song_unlock': b64encode(room.song_unlock).decode('utf-8'), + 'players': [{ + 'player_id': i.player_id, + 'name': i.name, + 'rating_ptt': i.rating_ptt + } for i in room.players] + }) + if n >= 100: + break + return { + 'amount': n, + 'rooms': rooms + } diff --git a/latest version/linkplay_server/udp_class.py b/latest version/linkplay_server/udp_class.py index 5ed8f90..5afe482 100644 --- a/latest version/linkplay_server/udp_class.py +++ b/latest version/linkplay_server/udp_class.py @@ -1,5 +1,6 @@ import logging from time import time +from random import randint from .config import Config @@ -12,26 +13,73 @@ def bi(value): return int.from_bytes(value, byteorder='little') -class Player: +class Score: def __init__(self) -> None: - self.player_id = 0 - self.player_name = b'\x45\x6d\x70\x74\x79\x50\x6c\x61\x79\x65\x72\x00\x00\x00\x00\x00' - self.token = 0 + self.difficulty = 0xff - self.character_id = 0xff - self.last_character_id = 0xff - self.is_uncapped = 0 + self.score = 0 + self.cleartype = 0 + self.timer = 0 + self.best_score_flag = 0 # personal best + self.best_player_flag = 0 # high score + + # 5.10 新增 + self.shiny_perfect_count = 0 # 2 bytes + self.perfect_count = 0 # 2 bytes + self.near_count = 0 # 2 bytes + self.miss_count = 0 # 2 bytes + self.early_count = 0 # 2 bytes + self.late_count = 0 # 2 bytes + + self.healthy = 0 # 4 bytes signed? 不确定,但似乎没影响 + + def copy(self, x: 'Score'): + self.difficulty = x.difficulty + self.score = x.score + self.cleartype = x.cleartype + self.timer = x.timer + self.best_score_flag = x.best_score_flag + self.best_player_flag = x.best_player_flag + self.shiny_perfect_count = x.shiny_perfect_count + self.perfect_count = x.perfect_count + self.near_count = x.near_count + self.miss_count = x.miss_count + self.early_count = x.early_count + self.late_count = x.late_count + self.healthy = x.healthy + + def clear(self): self.difficulty = 0xff - self.last_difficulty = 0xff self.score = 0 - self.last_score = 0 - self.timer = 0 - self.last_timer = 0 self.cleartype = 0 - self.last_cleartype = 0 + self.timer = 0 self.best_score_flag = 0 self.best_player_flag = 0 + self.shiny_perfect_count = 0 + self.perfect_count = 0 + self.near_count = 0 + self.miss_count = 0 + self.early_count = 0 + self.late_count = 0 + self.healthy = 0 + + def __str__(self): + return f'Score: {self.score}, Cleartype: {self.cleartype}, Difficulty: {self.difficulty}, Timer: {self.timer}, Best Score Flag: {self.best_score_flag}, Best Player Flag: {self.best_player_flag}, Shiny Perfect: {self.shiny_perfect_count}, Perfect: {self.perfect_count}, Near: {self.near_count}, Miss: {self.miss_count}, Early: {self.early_count}, Late: {self.late_count}, Healthy: {self.healthy}' + + +class Player: + def __init__(self, player_index: int = 0) -> None: + self.player_id = 0 + self.player_name = b'\x45\x6d\x70\x74\x79\x50\x6c\x61\x79\x65\x72\x00\x00\x00\x00\x00' + self.token = 0 + + self.character_id = 0xff + self.is_uncapped = 0 + + self.score = Score() + self.last_score = Score() + self.finish_flag = 0 self.player_state = 1 @@ -45,6 +93,16 @@ def __init__(self) -> None: self.start_command_num = 0 + # 5.10 新增 + + self.voting: int = 0x8000 # 2 bytes, song_idx, 0xffff 为不选择,0x8000 为默认值 + self.player_index: int = player_index # 1 byte 不确定对不对 + self.switch_2: int = 0 # 1 byte + + self.rating_ptt: int = 0 # 2 bytes + self.is_hide_rating: int = 0 # 1 byte + self.switch_4: int = 0 # 1 byte 只能确定有 00 和 01 + @property def name(self) -> str: return self.player_name.decode('ascii').rstrip('\x00') @@ -56,15 +114,23 @@ def to_dict(self) -> dict: 'is_online': self.online == 1, 'character_id': self.character_id, 'is_uncapped': self.is_uncapped == 1, + 'rating_ptt': self.rating_ptt, + 'is_hide_rating': self.is_hide_rating == 1, 'last_song': { - 'difficulty': self.last_difficulty, - 'score': self.last_score, - 'cleartype': self.last_cleartype, + 'difficulty': self.last_score.difficulty, + 'score': self.last_score.score, + 'cleartype': self.last_score.cleartype, + 'shine_perfect': self.last_score.shiny_perfect_count, + 'perfect': self.last_score.perfect_count, + 'near': self.last_score.near_count, + 'miss': self.last_score.miss_count, + 'early': self.last_score.early_count, + 'late': self.last_score.late_count, }, 'song': { - 'difficulty': self.difficulty, - 'score': self.score, - 'cleartype': self.cleartype, + 'difficulty': self.score.difficulty, + 'score': self.score.score, + 'cleartype': self.score.cleartype, }, 'player_state': self.player_state, 'last_timestamp': self.last_timestamp, @@ -77,30 +143,92 @@ def set_player_name(self, player_name: str): else: self.player_name += b'\x00' * (16 - len(self.player_name)) + @property + def info(self) -> bytes: + re = bytearray() + re.extend(b(self.player_id, 8)) + re.append(self.character_id) + re.append(self.is_uncapped) + re.append(self.score.difficulty) + re.extend(b(self.score.score, 4)) + re.extend(b(self.score.timer, 4)) + re.append(self.score.cleartype) + re.append(self.player_state) + re.append(self.download_percent) + re.append(self.online) + + re.extend(b(self.voting, 2)) + re.append(self.player_index) + re.append(self.switch_2) + re.extend(b(self.rating_ptt, 2)) + re.append(self.is_hide_rating) + re.append(self.switch_4) + + return bytes(re) + + @property + def last_score_info(self) -> bytes: + if self.player_id == 0: + return b'\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + x = self.last_score + re = bytearray() + re.append(self.character_id) + re.append(x.difficulty) + re.extend(b(x.score, 4)) + re.append(x.cleartype) + re.append(x.best_score_flag) + re.append(x.best_player_flag) + re.extend(b(x.shiny_perfect_count, 2)) + re.extend(b(x.perfect_count, 2)) + re.extend(b(x.near_count, 2)) + re.extend(b(x.miss_count, 2)) + re.extend(b(x.early_count, 2)) + re.extend(b(x.late_count, 2)) + re.extend(b(x.healthy, 4)) + + return bytes(re) + class Room: + def __init__(self) -> None: self.room_id = 0 self.room_code = 'AAAA00' + self.share_token = 'abcde12345' # 5.10 新增 self.countdown = 0xffffffff self.timestamp = 0 - self.state = 0 - self.song_idx = 0xffff - self.last_song_idx = 0xffff + self._state = 0 + self.song_idx = 0xffff # 疑似 idx * 5 + self.last_song_idx = 0xffff # 疑似 idx * 5 self.song_unlock = b'\xFF' * Config.LINK_PLAY_UNLOCK_LENGTH self.host_id = 0 - self.players = [Player(), Player(), Player(), Player()] + self.players = [Player(0), Player(1), Player(2), Player(3)] self.interval = 1000 - self.times = 100 + self.times = 100 # ??? - self.round_switch = 0 + self.round_mode: int = 1 # 5.10 从 bool 修改为 int 1~3 + self.is_public = 0 # 5.10 新增 + self.timed_mode = 0 # 5.10 新增 + + self.selected_voter_player_id: int = 0 # 5.10 新增 self.command_queue = [] + self.next_state_timestamp = 0 # 计时模式下一个状态时间 + + @property + def state(self) -> int: + return self._state + + @state.setter + def state(self, value: int): + self._state = value + self.countdown = 0xffffffff + def to_dict(self) -> dict: p = [i.to_dict() for i in self.players if i.player_id != 0] for i in p: @@ -108,21 +236,47 @@ def to_dict(self) -> dict: return { 'room_id': self.room_id, 'room_code': self.room_code, + 'share_token': self.share_token, 'state': self.state, 'song_idx': self.song_idx, 'last_song_idx': self.last_song_idx if not self.is_playing else 0xffff, 'host_id': self.host_id, 'players': p, - 'round_switch': self.round_switch == 1, + 'round_mode': self.round_mode, 'last_timestamp': self.timestamp, 'is_enterable': self.is_enterable, + 'is_matchable': self.is_matchable, 'is_playing': self.is_playing, + 'is_public': self.is_public == 1, + 'timed_mode': self.timed_mode == 1, } + @property + def room_info(self) -> bytes: + re = bytearray() + re.extend(b(self.host_id, 8)) + re.append(self.state) + re.extend(b(self.countdown, 4)) + re.extend(b(self.timestamp, 8)) + re.extend(b(self.song_idx, 2)) + re.extend(b(self.interval, 2)) + re.extend(b(self.times, 7)) + re.extend(self.get_player_last_score()) + re.extend(b(self.last_song_idx, 2)) + re.append(self.round_mode) + re.append(self.is_public) + re.append(self.timed_mode) + re.extend(b(self.selected_voter_player_id, 8)) + return bytes(re) + @property def is_enterable(self) -> bool: return 0 < self.player_num < 4 and self.state == 2 + @property + def is_matchable(self) -> bool: + return self.is_public and 0 < self.player_num < 4 and self.state == 1 + @property def is_playing(self) -> bool: return self.state in (4, 5, 6, 7) @@ -133,7 +287,9 @@ def command_queue_length(self) -> int: @property def player_num(self) -> int: - self.check_player_online() + now = round(time() * 1000000) + if now - self.timestamp >= 1000000: + self.check_player_online(now) return sum(i.player_id != 0 for i in self.players) def check_player_online(self, now: int = None): @@ -156,29 +312,18 @@ def check_player_online(self, now: int = None): def get_players_info(self): # 获取所有玩家信息 - re = b'' + re = bytearray() for i in self.players: - re += b(i.player_id, 8) + b(i.character_id) + b(i.is_uncapped) + b(i.difficulty) + b(i.score, 4) + \ - b(i.timer, 4) + b(i.cleartype) + b(i.player_state) + \ - b(i.download_percent) + b(i.online) + b'\x00' + i.player_name - return re + re.extend(i.info) + re.append(0) + re.extend(i.player_name) + return bytes(re) def get_player_last_score(self): # 获取上次曲目玩家分数,返回bytes if self.last_song_idx == 0xffff: - return b'\xff\xff\x00\x00\x00\x00\x00\x00\x00' * 4 - re = b'' - - for i in range(4): - player = self.players[i] - - if player.player_id != 0: - re += b(player.last_character_id) + b(player.last_difficulty) + b(player.last_score, 4) + b( - player.last_cleartype) + b(player.best_score_flag) + b(player.best_player_flag) - else: - re += b'\xff\xff\x00\x00\x00\x00\x00\x00\x00' - - return re + return b'\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' * 4 + return b''.join(i.last_score_info for i in self.players) def make_round(self): # 轮换房主 @@ -203,9 +348,19 @@ def delete_player(self, player_index: int): f'Player `{player.name}` leaves room `{self.room_code}`') self.players[player_index].online = 0 - self.players[player_index] = Player() + self.players[player_index] = Player(player_index) self.update_song_unlock() + if self.state in (2, 3): + self.state = 1 + self.song_idx = 0xffff + self.voting_clear() + + print(self.player_num) + if self.state in (1, 2) and self.timed_mode and self.player_num <= 1: + self.next_state_timestamp = 0 + self.countdown = 0xffffffff + def update_song_unlock(self): # 更新房间可用歌曲 r = bi(b'\xff' * Config.LINK_PLAY_UNLOCK_LENGTH) @@ -245,27 +400,110 @@ def make_finish(self): max_score_i = [] for i in range(4): player = self.players[i] - if player.player_id != 0: - player.finish_flag = 0 - player.last_timer = player.timer - player.last_score = player.score - player.last_cleartype = player.cleartype - player.last_character_id = player.character_id - player.last_difficulty = player.difficulty - player.best_player_flag = 0 - - if player.last_score > max_score: - max_score = player.last_score - max_score_i = [i] - elif player.last_score == max_score: - max_score_i.append(i) + if player.player_id == 0: + continue + player.finish_flag = 0 + player.last_score.copy(player.score) + player.last_score.best_player_flag = 0 + + if player.last_score.score > max_score: + max_score = player.last_score.score + max_score_i = [i] + elif player.last_score.score == max_score: + max_score_i.append(i) for i in max_score_i: - self.players[i].best_player_flag = 1 + self.players[i].last_score.best_player_flag = 1 + + self.voting_clear() + for i in self.players: + i.score.clear() logging.info( f'Room `{self.room_code}` finishes song `{self.song_idx}`') for i in self.players: if i.player_id != 0: - logging.info( - f'- Player `{i.name}` - Score: {i.last_score} Cleartype: {i.last_cleartype} Difficulty: {i.last_difficulty}') + logging.info(f'- Player `{i.name}` - {i.last_score}') + + @property + def is_all_player_voted(self) -> bool: + # 是否所有玩家都投票 + if self.state != 2: + return False + + for i in self.players: + if i.player_id != 0 and i.voting == 0x8000: + return False + + return True + + def random_song(self): + random_list = [] + for i in range(Config.LINK_PLAY_UNLOCK_LENGTH): + for j in range(8): + if self.song_unlock[i] & (1 << j): + random_list.append(i * 8 + j) + + if not random_list: + self.song_idx = 0 + else: + self.song_idx = random_list[randint(0, len(random_list) - 1)] + + def make_voting(self): + # 投票 + self.state = 3 + self.selected_voter_player_id = 0 + + random_list = [] + random_list_player_id = [] + for i in self.players: + if i.player_id == 0 or i.voting == 0xffff or i.voting == 0x8000: + continue + random_list.append(i.voting) + random_list_player_id.append(i.player_id) + + if random_list: + idx = randint(0, len(random_list) - 1) + self.song_idx = random_list[idx] * 5 + self.selected_voter_player_id = random_list_player_id[idx] + else: + self.random_song() + + logging.info( + f'Room `{self.room_code}` votes song `{self.song_idx}`') + + def voting_clear(self): + # 清除投票 + self.selected_voter_player_id = 0 + for i in self.players: + i.voting = 0x8000 + + @property + def should_next_state(self) -> bool: + if not self.timed_mode and self.state not in (4, 5, 6): + self.countdown = 0xffffffff + return False + now = round(time() * 1000000) + if self.countdown == 0xffffffff: + # 还没开始计时 + if self.is_public and self.state == 1: + self.next_state_timestamp = now + Config.COUNTDOWN_MATCHING + elif self.state == 2: + self.next_state_timestamp = now + Config.COUNTDOWN_SELECT_SONG + elif self.state == 3: + self.next_state_timestamp = now + Config.COUNTDOWN_SELECT_DIFFICULTY + elif self.state == 4: + self.next_state_timestamp = now + Config.COUNTDOWN_SONG_READY + elif self.state == 5 or self.state == 6: + self.next_state_timestamp = now + Config.COUNTDOWN_SONG_START + elif self.state == 8: + self.next_state_timestamp = now + Config.COUNTDOWN_RESULT + else: + return False + + # 不是哥们,616 你脑子怎么长的,上个版本是毫秒时间戳,新版本变成了微秒???那你这倒计时怎么还是毫秒啊!!! + self.countdown = (self.next_state_timestamp - now) // 1000 + if self.countdown <= 0: + self.countdown = 0 + return True + return False diff --git a/latest version/linkplay_server/udp_parser.py b/latest version/linkplay_server/udp_parser.py index 18f9b21..95224b3 100644 --- a/latest version/linkplay_server/udp_parser.py +++ b/latest version/linkplay_server/udp_parser.py @@ -1,14 +1,27 @@ import logging -import time -from .udp_class import Room, bi from .config import Config +from .udp_class import Room, bi from .udp_sender import CommandSender class CommandParser: - route = [None, 'command_01', 'command_02', 'command_03', 'command_04', 'command_05', - 'command_06', 'command_07', 'command_08', 'command_09', 'command_0a', 'command_0b'] + + route = { + 0x01: 'command_01', + 0x02: 'command_02', + 0x03: 'command_03', + 0x04: 'command_04', + 0x06: 'command_06', + 0x07: 'command_07', + 0x08: 'command_08', + 0x09: 'command_09', + 0x0a: 'command_0a', + 0x0b: 'command_0b', + 0x20: 'command_20', + 0x22: 'command_22', + 0x23: 'command_23', + } def __init__(self, room: Room, player_index: int = 0) -> None: self.room = room @@ -31,7 +44,7 @@ def get_commands(self, command: bytes): re.append(self.room.command_queue[i]) if self.room.players[self.player_index].extra_command_queue: - re += self.room.players[self.player_index].extra_command_queue + re += self.room.players[self.player_index].extra_command_queue[-12:] self.room.players[self.player_index].extra_command_queue = [] if r: @@ -52,10 +65,14 @@ def command_01(self): self.room.command_queue.append(self.s.command_10()) def command_02(self): + # 房主选歌 + if self.room.round_mode == 3: + logging.warning('Error: round_mode == 3 in command 02') + return None self.s.random_code = self.command[16:24] song_idx = bi(self.command[24:26]) - flag = 2 + flag = 5 if self.room.state == 2: flag = 0 self.room.state = 3 @@ -69,10 +86,17 @@ def command_03(self): # 尝试进入结算 self.s.random_code = self.command[16:24] player = self.room.players[self.player_index] - player.score = bi(self.command[24:28]) - player.cleartype = self.command[28] - player.difficulty = self.command[29] - player.best_score_flag = self.command[30] + player.score.score = bi(self.command[24:28]) + player.score.cleartype = self.command[28] + player.score.difficulty = self.command[29] + player.score.best_score_flag = self.command[30] + player.score.shiny_perfect_count = bi(self.command[31:33]) + player.score.perfect_count = bi(self.command[33:35]) + player.score.near_count = bi(self.command[35:37]) + player.score.miss_count = bi(self.command[37:39]) + player.score.early_count = bi(self.command[39:41]) + player.score.late_count = bi(self.command[41:43]) + player.score.healthy = bi(self.command[43:47]) player.finish_flag = 1 player.last_timestamp -= Config.COMMAND_INTERVAL self.room.last_song_idx = self.room.song_idx @@ -94,19 +118,16 @@ def command_04(self): flag = 1 self.room.delete_player(i) self.room.command_queue.append(self.s.command_12(i)) - self.room.update_song_unlock() self.room.command_queue.append(self.s.command_14()) break return [self.s.command_0d(flag)] - def command_05(self): - pass - def command_06(self): self.s.random_code = self.command[16:24] self.room.state = 1 self.room.song_idx = 0xffff + self.room.voting_clear() self.room.command_queue.append(self.s.command_13()) @@ -117,13 +138,17 @@ def command_07(self): self.room.command_queue.append(self.s.command_14()) + # 07 可能需要一个 0d 响应,code = 0x0b + def command_08(self): - self.room.round_switch = bi(self.command[24:25]) - self.s.random_code = self.command[16:24] - self.room.command_queue.append(self.s.command_13()) + # 可能弃用 + logging.warning('Command 08 is outdated') + pass + # self.room.round_mode = bi(self.command[24:25]) + # self.s.random_code = self.command[16:24] + # self.room.command_queue.append(self.s.command_13()) def command_09(self): - re = [] self.s.random_code = self.command[16:24] player = self.room.players[self.player_index] @@ -133,133 +158,166 @@ def command_09(self): self.room.update_song_unlock() player.start_command_num = self.room.command_queue_length self.room.command_queue.append(self.s.command_15()) - else: - if self.s.timestamp - player.last_timestamp >= Config.COMMAND_INTERVAL: - re.append(self.s.command_0c()) - player.last_timestamp = self.s.timestamp + return None - # 离线判断 - flag_13, player_index_list = self.room.check_player_online( - self.s.timestamp) - for i in player_index_list: - self.room.command_queue.append(self.s.command_12(i)) + flag_0c = False - flag_11 = False - flag_12 = False + if self.s.timestamp - player.last_timestamp >= Config.COMMAND_INTERVAL: + flag_0c = True + player.last_timestamp = self.s.timestamp - if player.online == 0: - flag_12 = True - player.online = 1 + # 离线判断 + flag_13, player_index_list = self.room.check_player_online( + self.s.timestamp) + for i in player_index_list: + self.room.command_queue.append(self.s.command_12(i)) - if self.room.is_ready(1, 1): - flag_13 = True - self.room.state = 2 - - if player.player_state != self.command[32]: - flag_12 = True - player.player_state = self.command[32] - - if player.difficulty != self.command[33] and player.player_state != 5 and player.player_state != 6 and player.player_state != 7 and player.player_state != 8: - flag_12 = True - player.difficulty = self.command[33] + flag_11 = False + flag_12 = False - if player.cleartype != self.command[34] and player.player_state != 7 and player.player_state != 8: - flag_12 = True - player.cleartype = self.command[34] + if player.online == 0: + flag_12 = True + player.online = 1 - if player.download_percent != self.command[35]: - flag_12 = True - player.download_percent = self.command[35] + if self.room.state in (1, 2) and player.player_state == 8: + # 还在结算给踢了 + # 冗余,为了保险 + self.room.delete_player(self.player_index) + self.room.command_queue.append( + self.s.command_12(self.player_index)) + self.room.command_queue.append(self.s.command_14()) + + if self.room.is_ready(1, 1) and ((self.room.player_num > 1 and not self.room.is_public) or (self.room.is_public and self.room.player_num == 4)): + flag_13 = True + self.room.state = 2 + + if self.room.state == 1 and self.room.is_public and self.room.player_num > 1 and self.room.should_next_state: + flag_0c = True + flag_13 = True + self.room.state = 2 + + if self.room.state in (2, 3) and self.room.player_num < 2: + flag_13 = True + self.room.state = 1 - if player.character_id != self.command[36]: - flag_12 = True - player.character_id = self.command[36] + if self.room.state == 2 and self.room.should_next_state: + flag_0c = True + self.room.state = 3 + flag_13 = True + if self.room.round_mode == 3: + self.room.make_voting() + else: + self.room.random_song() + + if player.player_state != self.command[32]: + flag_12 = True + player.player_state = self.command[32] + + if player.score.difficulty != self.command[33] and player.player_state not in (5, 6, 7, 8): + flag_12 = True + player.score.difficulty = self.command[33] + + if player.score.cleartype != self.command[34] and player.player_state != 7 and player.player_state != 8: + flag_12 = True + player.score.cleartype = self.command[34] + + if player.download_percent != self.command[35]: + flag_12 = True + player.download_percent = self.command[35] + + if player.character_id != self.command[36]: + flag_12 = True + player.character_id = self.command[36] + + if player.is_uncapped != self.command[37]: + flag_12 = True + player.is_uncapped = self.command[37] + + if self.room.state == 3 and player.score.score != bi(self.command[24:28]): + flag_12 = True + player.score.score = bi(self.command[24:28]) + + if self.room.is_ready(3, 4) or (self.room.state == 3 and self.room.should_next_state): + flag_13 = True + flag_0c = True + self.room.state = 4 + + if self.room.round_mode == 2: + # 将换房主时间提前到此刻 + self.room.make_round() + logging.info(f'Room `{self.room.room_code}` starts playing') + + if self.room.state == 4: + if player.download_percent != 0xff: + # 有人没下载完把他踢了! + self.room.delete_player(self.player_index) + self.room.command_queue.append( + self.s.command_12(self.player_index)) + self.room.command_queue.append(self.s.command_14()) - if player.is_uncapped != self.command[37]: - flag_12 = True - player.is_uncapped = self.command[37] + if self.room.should_next_state: + self.room.state = 5 + flag_11 = True + flag_13 = True - if self.room.state == 3 and player.score != bi(self.command[24:28]): - flag_12 = True - player.score = bi(self.command[24:28]) + if self.room.state == 5: + flag_13 = True + if self.room.is_ready(5, 6): + self.room.state = 6 + if self.room.is_ready(5, 7): + self.room.state = 7 + + if self.room.state in (5, 6) and self.room.should_next_state: + # 此处不清楚 + self.room.state = 7 + flag_13 = True + + if self.room.state in (7, 8): + player_now_timer = bi(self.command[28:32]) + if player.score.timer < player_now_timer or player_now_timer == 0 and player.score.timer != 0: + player.last_score.timer = player.score.timer + player.last_score.score = player.score.score + player.score.timer = player_now_timer + player.score.score = bi(self.command[24:28]) + + if player.score.timer != 0 or self.room.state != 8: + for i in self.room.players: + i.extra_command_queue.append( + self.s.command_0e(self.player_index)) + + if self.room.is_ready(8, 1): + flag_13 = True + self.room.state = 1 + self.room.song_idx = 0xffff - if self.room.is_ready(3, 4): + if self.room.state == 8 and self.room.should_next_state: + flag_0c = True flag_13 = True - self.room.countdown = Config.COUNTDOWM_TIME - self.room.timestamp = round(time.time() * 1000) - self.room.state = 4 - if self.room.round_switch == 1: - # 将换房主时间提前到此刻 - self.room.make_round() - - logging.info(f'Room `{self.room.room_code}` starts playing') - - if self.room.state in (4, 5, 6): - timestamp = round(time.time() * 1000) - self.room.countdown -= timestamp - self.room.timestamp - self.room.timestamp = timestamp - if self.room.state == 4 and self.room.countdown <= 0: - # 此处不清楚 - self.room.state = 5 - self.room.countdown = 5999 - flag_11 = True - flag_13 = True - - if self.room.state == 5 and self.room.is_ready(5, 6): - self.room.state = 6 - flag_13 = True - - if self.room.state == 5 and self.room.is_ready(5, 7): - self.room.state = 7 - self.room.countdown = 0xffffffff - flag_13 = True - - if self.room.state == 5 and self.room.countdown <= 0: - print('我怎么知道这是啥') - - if self.room.state == 6 and self.room.countdown <= 0: - # 此处不清楚 - self.room.state = 7 - self.room.countdown = 0xffffffff - flag_13 = True - - self.room.countdown = self.room.countdown if self.room.countdown > 0 else 0 - - if self.room.state in (7, 8): - if player.timer < bi(self.command[28:32]) or bi(self.command[28:32]) == 0 and player.timer != 0: - player.last_timer = player.timer - player.last_score = player.score - player.timer = bi(self.command[28:32]) - player.score = bi(self.command[24:28]) - - if player.timer != 0 or self.room.state != 8: - for i in self.room.players: - i.extra_command_queue.append( - self.s.command_0e(self.player_index)) - - if self.room.is_ready(8, 1): - flag_13 = True - self.room.state = 1 - self.room.song_idx = 0xffff - - for i in self.room.players: - i.timer = 0 - i.score = 0 - - if self.room.is_finish(): - # 有人退房导致的结算 - self.room.make_finish() - flag_13 = True - - if flag_11: - self.room.command_queue.append(self.s.command_11()) - if flag_12: + self.room.state = 1 + self.room.song_idx = 0xffff + + if self.room.state in (1, 2) and player.player_state == 8: + # 还在结算给踢了 + self.room.delete_player(self.player_index) self.room.command_queue.append( self.s.command_12(self.player_index)) - if flag_13: - self.room.command_queue.append(self.s.command_13()) + self.room.command_queue.append(self.s.command_14()) - return re + if self.room.is_finish(): + # 有人退房导致的结算 + self.room.make_finish() + flag_13 = True + + if flag_11: + self.room.command_queue.append(self.s.command_11()) + if flag_12: + self.room.command_queue.append( + self.s.command_12(self.player_index)) + if flag_13: + self.room.command_queue.append(self.s.command_13()) + + if flag_0c: + return [self.s.command_0c()] def command_0a(self): # 退出房间 @@ -267,9 +325,6 @@ def command_0a(self): self.room.command_queue.append(self.s.command_12(self.player_index)) - if self.room.state in (2, 3): - self.room.state = 1 - self.room.song_idx = 0xffff # self.room.command_queue.append(self.s.command_11()) self.room.command_queue.append(self.s.command_13()) self.room.command_queue.append(self.s.command_14()) @@ -281,3 +336,45 @@ def command_0b(self): if self.player_index != i and self.room.players[i].online == 1: self.room.players[i].extra_command_queue.append( self.s.command_0f(self.player_index, song_idx)) + + def command_20(self): + # 表情 + sticker_id = bi(self.command[16:18]) + for i in range(4): + if self.player_index != i and self.room.players[i].online == 1: + self.room.players[i].extra_command_queue.append( + self.s.command_21(self.player_index, sticker_id)) + + def command_22(self): + # 房间设置,懒得判断房主 + self.s.random_code = self.command[16:24] + self.room.is_public = self.command[25] + if self.room.is_public == 0: + self.room.round_mode = self.command[24] + self.room.timed_mode = self.command[26] + else: + self.room.round_mode = 3 + self.room.timed_mode = 1 + self.room.state = 1 + self.room.command_queue.append(self.s.command_11()) + self.room.command_queue.append(self.s.command_13()) + return [self.s.command_0d(1)] + + def command_23(self): + # 歌曲投票 + self.s.random_code = self.command[16:24] + if self.room.player_num < 2: + return [self.s.command_0d(6)] + if self.room.state != 2: + return [self.s.command_0d(5)] + player = self.room.players[self.player_index] + player.voting = bi(self.command[24:26]) + logging.info( + f'Player `{player.name}` votes for song `{player.voting}`') + self.room.command_queue.append(self.s.command_12(self.player_index)) + + if self.room.is_all_player_voted: + self.room.make_voting() + self.room.command_queue.append(self.s.command_13()) + + return [self.s.command_0d(1)] diff --git a/latest version/linkplay_server/udp_sender.py b/latest version/linkplay_server/udp_sender.py index 6290eb3..e389d61 100644 --- a/latest version/linkplay_server/udp_sender.py +++ b/latest version/linkplay_server/udp_sender.py @@ -1,28 +1,43 @@ +from os import urandom from time import time from .udp_class import Room, b +PADDING = [b(i) * i for i in range(16)] + [b''] + + class CommandSender: PROTOCOL_NAME = b'\x06\x16' - PROTOCOL_VERSION = b'\x09' + PROTOCOL_VERSION = b'\x0D' def __init__(self, room: Room = None) -> None: self.room = room self.timestamp = round(time() * 1000000) + self.room.timestamp = self.timestamp + 1 + + self._random_code = None + + @property + def random_code(self): + if self._random_code is None: + self._random_code = urandom(4) + b'\x00\x00\x00\x00' + return self._random_code - self.random_code = b'\x11\x11\x11\x11\x00\x00\x00\x00' + @random_code.setter + def random_code(self, value): + self._random_code = value @staticmethod def command_encode(t: tuple): r = b''.join(t) x = 16 - len(r) % 16 - return r + b(x) * x + return r + PADDING[x] def command_prefix(self, command: bytes): length = self.room.command_queue_length - if command >= b'\x10': + if b'\x10' <= command <= b'\x1f': length += 1 return (self.PROTOCOL_NAME, command, self.PROTOCOL_VERSION, b(self.room.room_id, 8), b(length, 4)) @@ -31,12 +46,18 @@ def command_0c(self): return self.command_encode((*self.command_prefix(b'\x0c'), self.random_code, b(self.room.state), b(self.room.countdown, 4), b(self.timestamp, 8))) def command_0d(self, code: int): + # 3 你不是房主 + # 5 有玩家目前无法开始 + # 6 需要更多玩家以开始 + # 7 有玩家无法游玩这首歌 + return self.command_encode((*self.command_prefix(b'\x0d'), self.random_code, b(code))) def command_0e(self, player_index: int): # 分数广播 + # 我猜,616 写错了,首先 4 个 00 大概是分数使用了 8 bytes 转换,其次上一个分数根本就不需要哈哈哈哈哈哈! player = self.room.players[player_index] - return self.command_encode((*self.command_prefix(b'\x0e'), b(player.player_id, 8), b(player.character_id), b(player.is_uncapped), b(player.difficulty), b(player.score, 4), b(player.timer, 4), b(player.cleartype), b(player.player_state), b(player.download_percent), b'\x01', b(player.last_score, 4), b(player.last_timer, 4), b(player.online))) + return self.command_encode((*self.command_prefix(b'\x0e'), player.info, b(player.last_score.score, 4), b'\x00' * 4, b(player.last_score.timer, 4), b'\x00' * 4)) def command_0f(self, player_index: int, song_idx: int): # 歌曲推荐 @@ -52,13 +73,17 @@ def command_11(self): def command_12(self, player_index: int): player = self.room.players[player_index] - return self.command_encode((*self.command_prefix(b'\x12'), self.random_code, b(player_index), b(player.player_id, 8), b(player.character_id), b(player.is_uncapped), b(player.difficulty), b(player.score, 4), b(player.timer, 4), b(player.cleartype), b(player.player_state), b(player.download_percent), b(player.online))) + return self.command_encode((*self.command_prefix(b'\x12'), self.random_code, b(player_index), player.info)) def command_13(self): - return self.command_encode((*self.command_prefix(b'\x13'), self.random_code, b(self.room.host_id, 8), b(self.room.state), b(self.room.countdown, 4), b(self.timestamp, 8), b(self.room.song_idx, 2), b(self.room.interval, 2), b(self.room.times, 7), self.room.get_player_last_score(), b(self.room.last_song_idx, 2), b(self.room.round_switch, 1))) + return self.command_encode((*self.command_prefix(b'\x13'), self.random_code, self.room.room_info)) def command_14(self): return self.command_encode((*self.command_prefix(b'\x14'), self.random_code, self.room.song_unlock)) def command_15(self): - return self.command_encode((*self.command_prefix(b'\x15'), self.room.get_players_info(), self.room.song_unlock, b(self.room.host_id, 8), b(self.room.state), b(self.room.countdown, 4), b(self.timestamp, 8), b(self.room.song_idx, 2), b(self.room.interval, 2), b(self.room.times, 7), self.room.get_player_last_score(), b(self.room.last_song_idx, 2), b(self.room.round_switch, 1))) + return self.command_encode((*self.command_prefix(b'\x15'), self.room.get_players_info(), self.room.song_unlock, self.room.room_info)) + + def command_21(self, player_index: int, sticker_id: int): + player = self.room.players[player_index] + return self.command_encode((*self.command_prefix(b'\x21'), b(player.player_id, 8), b(sticker_id, 2))) diff --git a/latest version/server/multiplayer.py b/latest version/server/multiplayer.py index 7a1e6b9..58b6261 100644 --- a/latest version/server/multiplayer.py +++ b/latest version/server/multiplayer.py @@ -2,7 +2,8 @@ from core.config_manager import Config from core.error import ArcError -from core.linkplay import Player, RemoteMultiPlayer, Room +from core.linkplay import MatchStore, Player, RemoteMultiPlayer, Room +from core.notification import RoomInviteNotification from core.sql import Connect from .auth import auth_required @@ -68,3 +69,104 @@ def multiplayer_update(user_id): ':')[0] if Config.LINKPLAY_DISPLAY_HOST == '' else Config.LINKPLAY_DISPLAY_HOST r['port'] = int(Config.LINKPLAY_UDP_PORT) return success_return(r) + + +@bp.route('/me/room//invite', methods=['POST']) # 邀请 +@auth_required(request) +@arc_try +def room_invite(user_id, room_code): + if not Config.LINKPLAY_HOST: + raise ArcError('The link play server is unavailable.', 151, status=404) + + other_user_id = request.form.get('to', type=int) + + x = RemoteMultiPlayer() + share_token = x.select_room(room_code=room_code)['share_token'] + + with Connect(in_memory=True) as c_m: + with Connect() as c: + sender = Player(c, user_id) + sender.select_user_about_link_play() + n = RoomInviteNotification.from_sender( + sender, Player(c, other_user_id), share_token, c_m) + n.insert() + + return success_return({}) # 无返回 + + +@bp.route('/me/room/status', methods=['POST']) # 房间号码获取 +@auth_required(request) +@arc_try +def room_status(user_id): + if not Config.LINKPLAY_HOST: + raise ArcError('The link play server is unavailable.', 151, status=404) + + share_token = request.form.get('shareToken', type=str) + + x = RemoteMultiPlayer() + room_code = x.select_room(share_token=share_token)['room_code'] + + return success_return({ + 'roomId': room_code, + }) + + +@bp.route('/me/matchmaking/join/', methods=['POST']) # 匹配 +@auth_required(request) +@arc_try +def matchmaking_join(user_id): + if not Config.LINKPLAY_HOST: + raise ArcError('The link play server is unavailable.', 151, status=404) + + with Connect() as c: + user = Player(None, user_id) + user.get_song_unlock(request.json['clientSongMap']) + + x = MatchStore(c) + x.init_player(user) + r = x.match(user_id) + + if r is None: + return success_return({ + 'userId': user_id, + 'status': 2, + }) + + r['endPoint'] = request.host.split( + ':')[0] if Config.LINKPLAY_DISPLAY_HOST == '' else Config.LINKPLAY_DISPLAY_HOST + r['port'] = int(Config.LINKPLAY_UDP_PORT) + return success_return(r) + + +@bp.route('/me/matchmaking/status/', methods=['POST']) # 匹配状态,5s 一次 +@auth_required(request) +@arc_try +def matchmaking_status(user_id): + if not Config.LINKPLAY_HOST: + raise ArcError('The link play server is unavailable.', 151, status=404) + + with Connect() as c: + + r = MatchStore(c).match(user_id) + if r is None: + return success_return({ + 'userId': user_id, + 'status': 0, + }) + + r['endPoint'] = request.host.split( + ':')[0] if Config.LINKPLAY_DISPLAY_HOST == '' else Config.LINKPLAY_DISPLAY_HOST + r['port'] = int(Config.LINKPLAY_UDP_PORT) + return success_return(r) + + +@bp.route('/me/matchmaking/leave/', methods=['POST']) # 退出匹配 +@auth_required(request) +@arc_try +def matchmaking_leave(user_id): + if not Config.LINKPLAY_HOST: + raise ArcError('The link play server is unavailable.', 151, status=404) + + MatchStore().clear_player(user_id) + + return success_return({}) diff --git a/latest version/server/others.py b/latest version/server/others.py index 4f5f3fb..66379d7 100644 --- a/latest version/server/others.py +++ b/latest version/server/others.py @@ -8,6 +8,7 @@ from core.download import DownloadList from core.error import RateLimit from core.item import ItemCharacter +from core.notification import NotificationFactory from core.sql import Connect from core.system import GameInfo from core.user import UserOnline @@ -28,6 +29,15 @@ def game_info(): return success_return(GameInfo().to_dict()) +@bp.route('/notification/me', methods=['GET']) # 通知 +@auth_required(request) +@arc_try +def notification_me(user_id): + with Connect(in_memory=True) as c_m: + x = NotificationFactory(c_m, UserOnline(c_m, user_id)) + return success_return([i.to_dict() for i in x.get_notification()]) + + @bp.route('/game/content_bundle', methods=['GET']) # 热更新 @arc_try def game_content_bundle(): diff --git a/latest version/web/index.py b/latest version/web/index.py index 485f642..128a77a 100644 --- a/latest version/web/index.py +++ b/latest version/web/index.py @@ -443,7 +443,7 @@ def all_character(): def change_character(): # 修改角色数据 skill_ids = ['No_skill', 'gauge_easy', 'note_mirror', 'gauge_hard', 'frag_plus_10_pack_stellights', 'gauge_easy|frag_plus_15_pst&prs', 'gauge_hard|fail_frag_minus_100', 'frag_plus_5_side_light', 'visual_hide_hp', 'frag_plus_5_side_conflict', 'challenge_fullcombo_0gauge', 'gauge_overflow', 'gauge_easy|note_mirror', 'note_mirror', 'visual_tomato_pack_tonesphere', - 'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', 'frags_kou', 'visual_ink', 'shirabe_entry_fee', 'frags_yume', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', 'eto_uncap', 'luna_uncap', 'frags_preferred_song', 'visual_ghost_skynotes', 'ayu_uncap', 'skill_vita', 'skill_fatalis', 'skill_reunion', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'ilith_awakened_skill', 'skill_mithra', 'skill_toa', 'skill_nami_twilight', 'skill_ilith_ivy', 'skill_hikari_vanessa', 'skill_maya', 'skill_luin', 'skill_luin_uncap', 'skill_kanae_uncap', 'skill_doroc_uncap', 'skill_saya_uncap'] + 'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', 'frags_kou', 'visual_ink', 'shirabe_entry_fee', 'frags_yume', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', 'eto_uncap', 'luna_uncap', 'frags_preferred_song', 'visual_ghost_skynotes', 'ayu_uncap', 'skill_vita', 'skill_fatalis', 'skill_reunion', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'ilith_awakened_skill', 'skill_mithra', 'skill_toa', 'skill_nami_twilight', 'skill_ilith_ivy', 'skill_hikari_vanessa', 'skill_maya', 'skill_luin', 'skill_luin_uncap', 'skill_kanae_uncap', 'skill_doroc_uncap', 'skill_saya_uncap', 'skill_luna_ilot', 'skill_eto_hoppe', 'skill_aichan'] return render_template('web/changechar.html', skill_ids=skill_ids) From c3af92f29bed23f4b8191f32103e71ba2190de68 Mon Sep 17 00:00:00 2001 From: Guzi422 <2445152525@qq.com> Date: Sun, 8 Sep 2024 21:24:40 +0800 Subject: [PATCH 27/32] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=BD=93=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E5=86=8D=E6=AC=A1=E9=80=9A=E8=BF=87=E5=B7=B2=E7=BB=8F?= =?UTF-8?q?=E9=80=9A=E8=BF=87=E7=9A=84=E6=AE=B5=E4=BD=8D=E6=97=B6=E6=97=A0?= =?UTF-8?q?=E6=B3=95=E6=AD=A3=E5=B8=B8=E4=B8=8A=E4=BC=A0=E5=88=86=E6=95=B0?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- latest version/core/course.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/latest version/core/course.py b/latest version/core/course.py index d9b62e8..1eff7f1 100644 --- a/latest version/core/course.py +++ b/latest version/core/course.py @@ -1,5 +1,6 @@ from .error import NoData from .item import ItemFactory +from .score import Score from .song import Chart @@ -281,12 +282,13 @@ def update(self) -> None: self.user_play.course_play_state += 1 - from .score import Score if Score.get_song_state(self.clear_type) > Score.get_song_state(self.user_play.clear_type): self.clear_type = self.user_play.clear_type self.user_play.update_play_state_for_course() if self.user_play.course_play_state == 4: + self.user.select_user_about_stamina() + self.items = [] if not self.is_completed: self.user.select_user_about_stamina() self.select_course_item() From 10a816b161bbf4db18ac35e7b0fe3dde34c6d1c8 Mon Sep 17 00:00:00 2001 From: Guzi422 <2445152525@qq.com> Date: Sun, 8 Sep 2024 22:07:59 +0800 Subject: [PATCH 28/32] [Enhance][Bug fix] User settings & /user/me/setting error - Add a new settings 'mp_notification_enabled' - Fix a bug that user can not change online settings. (Show Potential/Stamina Notification/Invite Notification) --- latest version/core/user.py | 19 ++++++++++++++++++- latest version/database/init/tables.sql | 3 ++- latest version/server/user.py | 3 ++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/latest version/core/user.py b/latest version/core/user.py index 1974764..89621fd 100644 --- a/latest version/core/user.py +++ b/latest version/core/user.py @@ -305,6 +305,7 @@ def __init__(self, c, user_id=None) -> None: self.recent_score = Score() self.favorite_character = None self.max_stamina_notification_enabled = False + self.mp_notification_enabled = False self.prog_boost: int = 0 self.beyond_boost_gauge: float = 0 self.kanae_stored_prog: float = 0 @@ -495,7 +496,8 @@ def to_dict(self) -> dict: "settings": { "favorite_character": favorite_character_id, "is_hide_rating": self.is_hide_rating, - "max_stamina_notification_enabled": self.max_stamina_notification_enabled + "max_stamina_notification_enabled": self.max_stamina_notification_enabled, + "mp_notification_enabled": self.mp_notification_enabled }, "user_id": self.user_id, "name": self.name, @@ -560,6 +562,7 @@ def from_list(self, x: list) -> 'UserInfo': self.favorite_character = None if x[23] == - \ 1 else UserCharacter(self.c, x[23]) self.max_stamina_notification_enabled = x[24] == 1 + self.mp_notification_enabled = x[37] == 1 self.current_map = Map(x[25]) if x[25] is not None else Map('') self.ticket = x[26] self.prog_boost = x[27] if x[27] is not None else 0 @@ -661,6 +664,20 @@ def select_user_about_link_play(self) -> None: self.rating_ptt = x[1] self.is_hide_rating = x[2] == 1 + def select_user_about_settings(self) -> None: + ''' + 查询 user 表有关设置的信息 + ''' + self.c.execute( + '''select is_hide_rating, max_stamina_notification_enabled, mp_notification_enabled from user where user_id=?''', (self.user_id,) + x = self.c.fetchone() + if not x: + raise NoData('No user.', 108, -3) + + self.is_hide_rating = x[0] == 1 + self.max_stamina_notification_enabled = x[1] == 1 + self.mp_notification_enabled = x[2] == 1 + @property def global_rank(self) -> int: '''用户世界排名,如果超过设定最大值,返回0''' diff --git a/latest version/database/init/tables.sql b/latest version/database/init/tables.sql index a2bb234..8d827d1 100644 --- a/latest version/database/init/tables.sql +++ b/latest version/database/init/tables.sql @@ -35,7 +35,8 @@ max_stamina_ts int, stamina int, world_mode_locked_end_ts int, beyond_boost_gauge real default 0, -kanae_stored_prog real default 0 +kanae_stored_prog real default 0, +mp_notification_enabled int ); create table if not exists login(access_token text, user_id int, diff --git a/latest version/server/user.py b/latest version/server/user.py index c56f792..9adf41d 100644 --- a/latest version/server/user.py +++ b/latest version/server/user.py @@ -157,7 +157,8 @@ def sys_set(user_id, set_arg): user.change_favorite_character(int(value)) else: value = 'true' == value - if set_arg in ('is_hide_rating', 'max_stamina_notification_enabled'): + if set_arg in ('is_hide_rating', 'max_stamina_notification_enabled', 'mp_notification_enabled'): + user.select_user_about_settings() user.update_user_one_column(set_arg, value) return success_return(user.to_dict()) From 27567b8a5c9b48259fb990718a0908d36996b239 Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Sat, 14 Sep 2024 12:11:54 +0800 Subject: [PATCH 29/32] [Bug fix] Link Play kicked out & last song scores - Fix a bug that players will be kicked out when they haven't downloaded the chart before. - Fix a bug that the last song's scores will disappear when all players return to room. - For Arcaea 5.10.2 --- latest version/core/constant.py | 4 ++-- latest version/database/init/singles.json | 18 ++++++++++++++++++ latest version/linkplay_server/udp_class.py | 7 +++---- latest version/linkplay_server/udp_parser.py | 17 +++++++++-------- 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/latest version/core/constant.py b/latest version/core/constant.py index 69f6620..ef7a39d 100644 --- a/latest version/core/constant.py +++ b/latest version/core/constant.py @@ -1,7 +1,7 @@ from .config_manager import Config -ARCAEA_SERVER_VERSION = 'v2.11.3.17' -ARCAEA_DATABASE_VERSION = 'v2.11.3.17' +ARCAEA_SERVER_VERSION = 'v2.11.3.18' +ARCAEA_DATABASE_VERSION = 'v2.11.3.18' ARCAEA_LOG_DATBASE_VERSION = 'v1.1' diff --git a/latest version/database/init/singles.json b/latest version/database/init/singles.json index 8c1c4eb..9c72f8e 100644 --- a/latest version/database/init/singles.json +++ b/latest version/database/init/singles.json @@ -1882,5 +1882,23 @@ ], "orig_price": 100, "price": 100 + }, + { + "name": "rainofconflict", + "items": [ + { + "type": "single", + "id": "rainofconflict", + "is_available": true + }, + { + "type": "core", + "amount": 1, + "id": "core_generic", + "is_available": true + } + ], + "orig_price": 100, + "price": 100 } ] \ No newline at end of file diff --git a/latest version/linkplay_server/udp_class.py b/latest version/linkplay_server/udp_class.py index 5afe482..c6266d8 100644 --- a/latest version/linkplay_server/udp_class.py +++ b/latest version/linkplay_server/udp_class.py @@ -101,7 +101,7 @@ def __init__(self, player_index: int = 0) -> None: self.rating_ptt: int = 0 # 2 bytes self.is_hide_rating: int = 0 # 1 byte - self.switch_4: int = 0 # 1 byte 只能确定有 00 和 01 + self.is_staff: int = 0 # 1 byte @property def name(self) -> str: @@ -162,7 +162,7 @@ def info(self) -> bytes: re.append(self.switch_2) re.extend(b(self.rating_ptt, 2)) re.append(self.is_hide_rating) - re.append(self.switch_4) + re.append(self.is_staff) return bytes(re) @@ -356,7 +356,6 @@ def delete_player(self, player_index: int): self.song_idx = 0xffff self.voting_clear() - print(self.player_num) if self.state in (1, 2) and self.timed_mode and self.player_num <= 1: self.next_state_timestamp = 0 self.countdown = 0xffffffff @@ -382,7 +381,7 @@ def is_ready(self, old_state: int, player_state: int): def is_finish(self): # 是否全部进入结算 - if self.state == 8: + if self.state != 7: return False for i in self.players: diff --git a/latest version/linkplay_server/udp_parser.py b/latest version/linkplay_server/udp_parser.py index 95224b3..8b8e81b 100644 --- a/latest version/linkplay_server/udp_parser.py +++ b/latest version/linkplay_server/udp_parser.py @@ -179,7 +179,7 @@ def command_09(self): flag_12 = True player.online = 1 - if self.room.state in (1, 2) and player.player_state == 8: + if self.room.timed_mode and self.room.state in (1, 2) and player.player_state == 8: # 还在结算给踢了 # 冗余,为了保险 self.room.delete_player(self.player_index) @@ -248,12 +248,13 @@ def command_09(self): logging.info(f'Room `{self.room.room_code}` starts playing') if self.room.state == 4: - if player.download_percent != 0xff: - # 有人没下载完把他踢了! - self.room.delete_player(self.player_index) - self.room.command_queue.append( - self.s.command_12(self.player_index)) - self.room.command_queue.append(self.s.command_14()) + # 这好像会误判 + # if player.download_percent < 99: + # # 有人没下载完把他踢了! + # self.room.delete_player(self.player_index) + # self.room.command_queue.append( + # self.s.command_12(self.player_index)) + # self.room.command_queue.append(self.s.command_14()) if self.room.should_next_state: self.room.state = 5 @@ -296,7 +297,7 @@ def command_09(self): self.room.state = 1 self.room.song_idx = 0xffff - if self.room.state in (1, 2) and player.player_state == 8: + if self.room.timed_mode and self.room.state in (1, 2) and player.player_state == 8: # 还在结算给踢了 self.room.delete_player(self.player_index) self.room.command_queue.append( From dc1ca344e5caea32dc2819c48aef8c3142277011 Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Fri, 27 Sep 2024 00:28:56 +0800 Subject: [PATCH 30/32] [Enhance][Bug Fix] - For Arcaea 5.10.4 - Add support for the Link Play notification setting. - Fix a bug that public rooms cannot be matched possibly if some players leave the room. - Continue to fix the bug that the player cannot complete one course twice. - Continue to fix the bug that users cannot change online settings. --- latest version/core/config_manager.py | 2 +- latest version/core/constant.py | 4 +-- latest version/core/course.py | 6 ++--- latest version/core/linkplay.py | 13 +++++++--- latest version/core/notification.py | 4 +++ latest version/core/user.py | 30 +++++++++-------------- latest version/database/init/arc_data.py | 2 +- latest version/database/init/singles.json | 18 ++++++++++++++ latest version/database/init/tables.sql | 2 +- latest version/server/user.py | 1 - 10 files changed, 49 insertions(+), 33 deletions(-) diff --git a/latest version/core/config_manager.py b/latest version/core/config_manager.py index 3345934..504832f 100644 --- a/latest version/core/config_manager.py +++ b/latest version/core/config_manager.py @@ -12,7 +12,7 @@ class Config: SONG_FILE_HASH_PRE_CALCULATE = True - GAME_API_PREFIX = '/pastatabetai/32' # str | list[str] + GAME_API_PREFIX = '/autumnequinox/33' # str | list[str] OLD_GAME_API_PREFIX = [] # str | list[str] ALLOW_APPVERSION = [] # list[str] diff --git a/latest version/core/constant.py b/latest version/core/constant.py index ef7a39d..ff066de 100644 --- a/latest version/core/constant.py +++ b/latest version/core/constant.py @@ -1,7 +1,7 @@ from .config_manager import Config -ARCAEA_SERVER_VERSION = 'v2.11.3.18' -ARCAEA_DATABASE_VERSION = 'v2.11.3.18' +ARCAEA_SERVER_VERSION = 'v2.11.3.19' +ARCAEA_DATABASE_VERSION = 'v2.11.3.19' ARCAEA_LOG_DATBASE_VERSION = 'v1.1' diff --git a/latest version/core/course.py b/latest version/core/course.py index 1eff7f1..0ebd0e2 100644 --- a/latest version/core/course.py +++ b/latest version/core/course.py @@ -1,6 +1,5 @@ from .error import NoData from .item import ItemFactory -from .score import Score from .song import Chart @@ -40,7 +39,7 @@ def __init__(self, c=None) -> None: self.requirements: list = [] self.charts: list = [None, None, None, None] - self.items: list = None + self.items: list = [] def to_dict(self) -> dict: if self.course_name is None: @@ -282,15 +281,14 @@ def update(self) -> None: self.user_play.course_play_state += 1 + from .score import Score if Score.get_song_state(self.clear_type) > Score.get_song_state(self.user_play.clear_type): self.clear_type = self.user_play.clear_type self.user_play.update_play_state_for_course() if self.user_play.course_play_state == 4: self.user.select_user_about_stamina() - self.items = [] if not self.is_completed: - self.user.select_user_about_stamina() self.select_course_item() for i in self.items: i.user_claim_item(self.user) diff --git a/latest version/core/linkplay.py b/latest version/core/linkplay.py index 37cd091..9cc8896 100644 --- a/latest version/core/linkplay.py +++ b/latest version/core/linkplay.py @@ -320,12 +320,17 @@ def match(self, user_id: int): # 加入已有房间 for i in MatchStore.room_cache: f = True + num = 0 for j in i['players']: - if j['player_id'] != 0 and abs(user.rating_ptt - j['rating_ptt']) >= ptt_abs: - f = False - break + if j['player_id'] != 0: + num += 1 + if abs(user.rating_ptt - j['rating_ptt']) >= ptt_abs: + f = False + break - if f and user.calc_available_chart_num(b64decode(i['song_unlock'])) >= unlock_min and ((time() + 2) * 1000000 < i['next_state_timestamp'] or i['next_state_timestamp'] <= 0): + # 有玩家非正常退房时,next_state_timestamp 不为 0,有概率新玩家进不来,所以使用 num 统计玩家数量 + + if f and user.calc_available_chart_num(b64decode(i['song_unlock'])) >= unlock_min and ((time() + 2) * 1000000 < i['next_state_timestamp'] or i['next_state_timestamp'] <= 0 or num == 1): room = Room() room.room_code = i['room_code'] user.c = self.c diff --git a/latest version/core/notification.py b/latest version/core/notification.py index 79d096a..df6c2d0 100644 --- a/latest version/core/notification.py +++ b/latest version/core/notification.py @@ -26,6 +26,10 @@ def to_dict(self) -> dict: raise NotImplementedError() def insert(self): + self.receiver.select_user_one_column('mp_notification_enabled', True, bool) + if not self.receiver.mp_notification_enabled: + return + self.c_m.execute( '''select max(id) from notification where user_id = ?''', (self.receiver.user_id,)) x = self.c_m.fetchone() diff --git a/latest version/core/user.py b/latest version/core/user.py index 89621fd..b7aec10 100644 --- a/latest version/core/user.py +++ b/latest version/core/user.py @@ -305,7 +305,7 @@ def __init__(self, c, user_id=None) -> None: self.recent_score = Score() self.favorite_character = None self.max_stamina_notification_enabled = False - self.mp_notification_enabled = False + self.mp_notification_enabled = True self.prog_boost: int = 0 self.beyond_boost_gauge: float = 0 self.kanae_stored_prog: float = 0 @@ -497,7 +497,7 @@ def to_dict(self) -> dict: "favorite_character": favorite_character_id, "is_hide_rating": self.is_hide_rating, "max_stamina_notification_enabled": self.max_stamina_notification_enabled, - "mp_notification_enabled": self.mp_notification_enabled + "mp_notification_enabled": self.mp_notification_enabled, }, "user_id": self.user_id, "name": self.name, @@ -562,7 +562,6 @@ def from_list(self, x: list) -> 'UserInfo': self.favorite_character = None if x[23] == - \ 1 else UserCharacter(self.c, x[23]) self.max_stamina_notification_enabled = x[24] == 1 - self.mp_notification_enabled = x[37] == 1 self.current_map = Map(x[25]) if x[25] is not None else Map('') self.ticket = x[26] self.prog_boost = x[27] if x[27] is not None else 0 @@ -578,6 +577,8 @@ def from_list(self, x: list) -> 'UserInfo': self.beyond_boost_gauge = x[35] if x[35] else 0 self.kanae_stored_prog = x[36] if x[36] else 0 + self.mp_notification_enabled = x[37] == 1 + return self def select_user(self) -> None: @@ -664,20 +665,6 @@ def select_user_about_link_play(self) -> None: self.rating_ptt = x[1] self.is_hide_rating = x[2] == 1 - def select_user_about_settings(self) -> None: - ''' - 查询 user 表有关设置的信息 - ''' - self.c.execute( - '''select is_hide_rating, max_stamina_notification_enabled, mp_notification_enabled from user where user_id=?''', (self.user_id,) - x = self.c.fetchone() - if not x: - raise NoData('No user.', 108, -3) - - self.is_hide_rating = x[0] == 1 - self.max_stamina_notification_enabled = x[1] == 1 - self.mp_notification_enabled = x[2] == 1 - @property def global_rank(self) -> int: '''用户世界排名,如果超过设定最大值,返回0''' @@ -720,7 +707,7 @@ def update_global_rank(self) -> None: '''update user set world_rank_score = ? where user_id = ?''', (x[0], self.user_id)) self.world_rank_score = x[0] - def select_user_one_column(self, column_name: str, default_value=None) -> None: + def select_user_one_column(self, column_name: str, default_value=None, data_type=None) -> None: ''' 查询user表的某个属性 请注意必须是一个普通属性,不能是一个类的实例 @@ -733,7 +720,12 @@ def select_user_one_column(self, column_name: str, default_value=None) -> None: if not x: raise NoData('No user.', 108, -3) - self.__dict__[column_name] = x[0] if x[0] else default_value + data = x[0] if x[0] is not None else default_value + + if data_type is not None: + data = data_type(data) + + self.__dict__[column_name] = data def update_user_one_column(self, column_name: str, value=None) -> None: ''' diff --git a/latest version/database/init/arc_data.py b/latest version/database/init/arc_data.py index 3a48f51..8fb0dd0 100644 --- a/latest version/database/init/arc_data.py +++ b/latest version/database/init/arc_data.py @@ -71,7 +71,7 @@ class InitData: 'core_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful', 'core_course_skip_purchase', 'core_umbral', 'core_wacca', 'core_sunset', 'core_tanoc', 'core_serene'] world_songs = ["babaroque", "shadesoflight", "kanagawa", "lucifer", "anokumene", "ignotus", "rabbitintheblackroom", "qualia", "redandblue", "bookmaker", "darakunosono", "espebranch", "blacklotus", "givemeanightmare", "vividtheory", "onefr", "gekka", "vexaria3", "infinityheaven3", "fairytale3", "goodtek3", "suomi", "rugie", "faintlight", "harutopia", "goodtek", "dreaminattraction", "syro", "diode", "freefall", "grimheart", "blaster", - "cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris', 'ultimatetaste', 'rgb', 'matenrou', 'dynitikos', 'amekagura', 'fantasy', 'aloneandlorn', 'felys', 'onandon', 'hotarubinoyuki', 'oblivia3', 'libertas3', 'einherjar3', 'purpleverse3', 'viciousheroism3', 'inkarusi3', 'cyberneciacatharsis3', 'alephzero', 'hellohell', 'ichirin', 'awakeninruins', 'morningloom', 'lethalvoltage', 'leaveallbehind', 'desive', 'oldschoolsalvage', 'distortionhuman', 'epitaxy'] + "cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris', 'ultimatetaste', 'rgb', 'matenrou', 'dynitikos', 'amekagura', 'fantasy', 'aloneandlorn', 'felys', 'onandon', 'hotarubinoyuki', 'oblivia3', 'libertas3', 'einherjar3', 'purpleverse3', 'viciousheroism3', 'inkarusi3', 'cyberneciacatharsis3', 'alephzero', 'hellohell', 'ichirin', 'awakeninruins', 'morningloom', 'lethalvoltage', 'leaveallbehind', 'desive', 'oldschoolsalvage', 'distortionhuman', 'epitaxy', 'hailstone', 'furetemitai'] world_unlocks = ["scenery_chap1", "scenery_chap2", "scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7", "scenery_beyond"] diff --git a/latest version/database/init/singles.json b/latest version/database/init/singles.json index 9c72f8e..0a032af 100644 --- a/latest version/database/init/singles.json +++ b/latest version/database/init/singles.json @@ -1900,5 +1900,23 @@ ], "orig_price": 100, "price": 100 + }, + { + "name": "gensounosatellite", + "items": [ + { + "type": "single", + "id": "gensounosatellite", + "is_available": true + }, + { + "type": "core", + "amount": 1, + "id": "core_generic", + "is_available": true + } + ], + "orig_price": 100, + "price": 100 } ] \ No newline at end of file diff --git a/latest version/database/init/tables.sql b/latest version/database/init/tables.sql index 8d827d1..2e79e47 100644 --- a/latest version/database/init/tables.sql +++ b/latest version/database/init/tables.sql @@ -36,7 +36,7 @@ stamina int, world_mode_locked_end_ts int, beyond_boost_gauge real default 0, kanae_stored_prog real default 0, -mp_notification_enabled int +mp_notification_enabled int default 1 ); create table if not exists login(access_token text, user_id int, diff --git a/latest version/server/user.py b/latest version/server/user.py index 9adf41d..123c673 100644 --- a/latest version/server/user.py +++ b/latest version/server/user.py @@ -158,7 +158,6 @@ def sys_set(user_id, set_arg): else: value = 'true' == value if set_arg in ('is_hide_rating', 'max_stamina_notification_enabled', 'mp_notification_enabled'): - user.select_user_about_settings() user.update_user_one_column(set_arg, value) return success_return(user.to_dict()) From 9c9af892bd954ef3ec2fdd0889d9146ce59fd7a0 Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Sat, 28 Sep 2024 14:59:24 +0800 Subject: [PATCH 31/32] [Bug Fix][Enhance] PTT update lately & Register rate limiter - Fix a bug that PTT updates delay to next play. - Add the IP and the device rate limiters for user register. --- latest version/api/api_code.py | 1 + latest version/core/config_manager.py | 2 ++ latest version/core/constant.py | 2 +- latest version/core/score.py | 17 +++++++++++++++-- latest version/core/user.py | 14 +++++++++++++- latest version/server/user.py | 6 +++--- 6 files changed, 35 insertions(+), 7 deletions(-) diff --git a/latest version/api/api_code.py b/latest version/api/api_code.py index bf7a807..2a59e37 100644 --- a/latest version/api/api_code.py +++ b/latest version/api/api_code.py @@ -32,6 +32,7 @@ -210: 'Username exists', -211: 'Email address exists', -212: 'User code exists', + -213: 'Too many register attempts', -999: 'Unknown error' } diff --git a/latest version/core/config_manager.py b/latest version/core/config_manager.py index 504832f..5b2aaab 100644 --- a/latest version/core/config_manager.py +++ b/latest version/core/config_manager.py @@ -100,6 +100,8 @@ class Config: GAME_LOGIN_RATE_LIMIT = '30/5 minutes' API_LOGIN_RATE_LIMIT = '10/5 minutes' + GAME_REGISTER_IP_RATE_LIMIT = '10/1 day' + GAME_REGISTER_DEVICE_RATE_LIMIT = '3/1 day' NOTIFICATION_EXPIRE_TIME = 3 * 60 * 1000 diff --git a/latest version/core/constant.py b/latest version/core/constant.py index ff066de..113ce59 100644 --- a/latest version/core/constant.py +++ b/latest version/core/constant.py @@ -1,6 +1,6 @@ from .config_manager import Config -ARCAEA_SERVER_VERSION = 'v2.11.3.19' +ARCAEA_SERVER_VERSION = 'v2.11.3.20' ARCAEA_DATABASE_VERSION = 'v2.11.3.19' ARCAEA_LOG_DATBASE_VERSION = 'v1.1' diff --git a/latest version/core/score.py b/latest version/core/score.py index 095409b..5b030c8 100644 --- a/latest version/core/score.py +++ b/latest version/core/score.py @@ -131,7 +131,7 @@ def calculate_score_v2(defnum: float, shiny_perfect_count: int, perfect_count: i # 谱面定数小于等于 0 视为 unranked,返回值会为 0 if not defnum or defnum <= 0: return 0 - + all_note = perfect_count + near_count + miss_count if all_note == 0: return 0 @@ -281,7 +281,8 @@ def is_valid(self) -> bool: if songfile_hash and songfile_hash != self.song_hash: return False - x = f'''{self.song_token}{self.song_hash}{self.song.song_id}{self.song.difficulty}{self.score}{self.shiny_perfect_count}{self.perfect_count}{self.near_count}{self.miss_count}{self.health}{self.modifier}{self.clear_type}''' + x = f'''{self.song_token}{self.song_hash}{self.song.song_id}{self.song.difficulty}{self.score}{self.shiny_perfect_count}{ + self.perfect_count}{self.near_count}{self.miss_count}{self.health}{self.modifier}{self.clear_type}''' if self.combo_interval_bonus is not None: if self.combo_interval_bonus < 0 or self.combo_interval_bonus > self.all_note_count / 150: return False @@ -569,6 +570,18 @@ def update_one_r30(self, r_index: int, user_score: 'UserPlay | UserScore') -> No (self.user.user_id, r_index, user_score.time_played, user_score.song.song_id, user_score.song.difficulty, user_score.score, user_score.shiny_perfect_count, user_score.perfect_count, user_score.near_count, user_score.miss_count, user_score.health, user_score.modifier, user_score.clear_type, user_score.rating)) + # 更新内存中的数据 + x = (r_index, user_score.song.song_id, + user_score.song.difficulty, user_score.rating) + if len(self.r30_tuples) < 30: + self.r30_tuples.append(x) + return + + for i in range(30): + if self.r30_tuples[i][0] == r_index: + self.r30_tuples[i] = x + break + def r30_push_score(self, user_score: 'UserPlay | UserScore') -> None: '''根据新成绩调整 r30''' if self.r30_tuples is None: diff --git a/latest version/core/user.py b/latest version/core/user.py index b7aec10..ae168a0 100644 --- a/latest version/core/user.py +++ b/latest version/core/user.py @@ -54,6 +54,12 @@ def hash_pwd(self) -> str: class UserRegister(User): + + limiter_ip = ArcLimiter( + Config.GAME_REGISTER_IP_RATE_LIMIT, 'game_register_ip') + limiter_device = ArcLimiter( + Config.GAME_REGISTER_DEVICE_RATE_LIMIT, 'game_register_device') + def __init__(self, c) -> None: super().__init__() self.c = c @@ -141,7 +147,13 @@ def _insert_user_char(self): self.c.execute('''insert or replace into user_char_full values(?,?,?,?,?,?,0)''', (self.user_id, i[0], i[1], exp, i[2], 0)) - def register(self): + def register(self, device_id: str = None, ip: str = None): + if device_id is not None and not self.limiter_device.hit(device_id): + raise RateLimit(f'''Too many register attempts of device `{ + device_id}`''', 124, -213) + if ip is not None and ip != '127.0.0.1' and not self.limiter_ip.hit(ip): + raise RateLimit(f'''Too many register attempts of ip `{ + ip}`''', 124, -213) now = int(time.time() * 1000) if self.user_code is None: self._build_user_code() diff --git a/latest version/server/user.py b/latest version/server/user.py index 123c673..9a08fcc 100644 --- a/latest version/server/user.py +++ b/latest version/server/user.py @@ -32,12 +32,12 @@ def register(): else: device_id = 'low_version' - new_user.register() + ip = request.remote_addr + new_user.register(device_id, ip) # 注册后自动登录 user = UserLogin(c) - user.login(new_user.name, new_user.password, - device_id, request.remote_addr) + user.login(new_user.name, new_user.password, device_id, ip) current_app.logger.info(f'New user `{user.user_id}` registered') return success_return({'user_id': user.user_id, 'access_token': user.token}) From bd40704fa7fb3e632cd0fa89ab789d9417d42d1c Mon Sep 17 00:00:00 2001 From: Lost-MSth Date: Thu, 3 Oct 2024 15:48:33 +0800 Subject: [PATCH 32/32] Update to v2.12.0 --- README.md | 130 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 91 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 0ea8b03..0c659b3 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ This procedure is mainly used for study and research, and shall not be used for - 多设备自动封号 Auto-ban of multiple devices - :warning: 多设备登录 Multi device login - 登录频次限制 Login rate limit + - 注册频次限制 Register rate limit - :warning: 销号 Destroy account - 成绩上传 Score upload - 成绩校验 Score check @@ -33,9 +34,9 @@ This procedure is mainly used for study and research, and shall not be used for - 潜力值机制 Potential - Best 30 - :warning: Recent Top 10 -- :warning: 世界排名 Global rank +- 世界排名 Global rank - 段位系统 Course system -- :warning: Link Play +- :warning: Link Play 2.0 - 好友系统 Friends - :x: 好友位提升 Max friend number increase - 云端存档 Cloud save @@ -85,35 +86,78 @@ It is just so interesting. What it can do is under exploration. > 提醒:更新时请注意保留原先的数据库,以防数据丢失。 > Tips: When updating, please keep the original database in case of data loss. > -> 其它小改动请参考各个 commit 信息 +> 其它小改动请参考各个 commit 信息。 > Please refer to the commit messages for other minor changes. -### Version 2.11.3 - -> v2.11.2.1 ~ v2.11.2.7 for Arcaea 4.5.0 ~ 5.2.0 - -- 适用于 Arcaea 5.2.0 版本 - For Arcaea 5.2.0 -- 新搭档 **Ilith & Ivy**、**Hikari & Vanessa**、**摩耶**、**露恩** 已解锁(注意“ 洞烛(至高:第八探索者)”因客户端因素不可选用) - Unlock the character **Ilith & Ivy**, **Hikari & Vanessa**, **Maya**, and **Luin**. (Note that "Insight(Ascendant - 8th Seeker)" is not available due to the client.) -- 为以上角色的技能提供服务端支持 - Provide server-side support for the skills of the above characters. -- 设置中新增可选选项 `DOWNLOAD_FORBID_WHEN_NO_ITEM` 使得当 `songlist` 文件存在时,没有购买的用户无法下载曲目文件(实验性) - An option `DOWNLOAD_FORBID_WHEN_NO_ITEM` has been added to the config file to make that users cannot download the songs' files if they has not bought them when the `songlist` file exists. (Experimental) -- 支持文件 `video_720.mp4` & `video_1080.mp4` 的下载 - Add support for downloading `video_720.mp4` & `video_1080.mp4`. -- 在存档全解锁和 `songlist` 解析器中支持更多东西,以适应游戏更新 - Support more things in full cloud save unlocking and `songlist` parser, to adapt to game updates. -- Link Play 拥有更详细的控制台日志了 - More detailed console log for Link Play. -- 修复一些搭档的技能在世界模式进度中显示不正确的问题 - Fix a bug that some characters' skill cannot display proper values in world mode progression. -- 修复技能 "skill_mithra" 导致了 `prog` 值增加而不是世界模式进度增加的问题 - Fix a bug that "skill_mithra" results in adding `prog` value instead of world mode progress. -- 重构 Link Play TCP 数据交换部分,以获得更好的安全性和扩展性 - Code refactor of Link Play TCP data transmission for better security and scalability. -- 新增一个 HTTP API 用来获取 Link Play 中当前的房间和玩家信息 - Add an HTTP API endpoint for getting the information of current rooms and players in Link Play. +### Version 2.12.0 + +> v2.11.3.1 ~ v2.11.3.20 for Arcaea 5.2.0 ~ 5.10.4 +> +> Here are not some bug fixes. +> +> 注意:Link Play 2.0 无法兼容旧版本客户端。 Note: Link Play 2.0 is not compatible with older client versions. + +- 适用于 Arcaea 5.10.4 版本 + For Arcaea 5.10.4 +- 添加一些新搭档和搭档的觉醒形态,并支持他们的技能 + Add some new partners, uncap some others, and add support for their skills. +- 支持 Link Play 2.0 的几乎所有功能 + Add almost whole support for Link Play 2.0. +- 支持新谱面难度 ETR + Adapt to the new difficulty ETR. +- 支持内容捆绑包(热更新),包含两种更新模式 + Add support for content bundles (hot update), including two update modes. +- 支持新手任务系统 + Add support for missions. +- 更新 Recent 30 机制,修改其表结构 + Update Recent 30 mechanism. Alter Recent 30 table structure. +- PTT 机制更新:添加了推分保护 + PTT mechanism: Change first play protection to new best protection. +- 调整世界排名机制使其更接近于官服 + Adjust world rank mechanism to be closer to the official one. +- 重构世界模式,并调整了一些搭档的技能效果和进度计算逻辑 + Code refactor for World Mode, and adjust some skills and the logic of progress calculation. +- 支持世界模式的陷落梯子 + Add support for Breached World Map. +- 添加了一个陷落梯子例子(#148) + Add an example breached map. (#148) +- 变更残片购买体力的恢复时间为 23 小时 + Change the recover time of using fragments buying stamina to 23 hours. +- 支持设置多个可使用的和旧的游戏 API 前缀,其中旧的前缀会通知用户更新客户端 + Add some endpoints for old API prefixes to notify users to update the client; add support for multiple game prefixes. +- 支持用户自销毁账号(默认不开启) + Add support for users destroy their own accounts. (default unable) +- 添加对“单曲兑换券”的不完整支持 + Incomplete support for "pick_ticket". +- 世界模式地图文件夹中可以包含子文件夹了 + Make the world maps' folder can have sub folders. +- 支持后台和 API 刷新 Recent 30 的定数评分 + Add support for refreshing ratings of Recent 30 via API and webpage. +- 添加对 IP 及设备的用户注册频率限制 + Add the IP and the device rate limiters for user register. +- 修复当用户再次通过已经通过的段位时无法正常上传分数的问题(by Guzi422) + Fix the bug that the player cannot upload the score when completing a course again. (by Guzi422) +- 修复段位模式最高分在用户未完整完成挑战时不更新的逻辑问题 + Fix a logical bug that the course's high score will not update if the user does not complete the whole course challenge. +- 修复 Link Play 相关 API 接口报错的问题 + Fix a bug that API for Link Play cannot work. +- 修复依赖问题:cryptography >= 35.0.0 + Fix requirements: cryptography >= 35.0.0 +- 修复 `songlist` 解析问题(#156) + Fix a `songlist` parser problem. (#156) +- 修复技能 skill_amane 在世界地图台阶类型为空时报错的问题 + Fix a bug that "skill_amane" may arise error when the step type of world map is null. +- 支持自动添加搭档“光 & 对立 (Reunion)”和“光 (Fatalis)”,以尝试解决最终章的解锁问题(#110 #164) + Add support for automatically adding partner "Hikari & Tairitsu (Reunion)" and "Hikari (Fatalis)", to try to unlock Finale stories correctly. (#110 #164) +- 修复 `songlist` 文件存在时视频文件无法下载的问题(#177) + Fix a bug that the video files cannot be downloaded when +the `songlist` file exists. (#177) +- 修复 Link Play 中玩家全部返回房间后上一首曲子成绩消失的问题 + Fix a bug that the last song's scores will disappear when all players return to room in Link Play. +- 工具 `update_song.py` 支持 ETR 难度 + Add support for ETR difficulties in the `update_song.py` tool. +- 添加发送错误信息的小工具测试服务端 + Add a small tool test server to send error message. ## 运行环境与依赖 Running environment and requirements @@ -124,28 +168,36 @@ It is just so interesting. What it can do is under exploration. - limits >= 2.7.0 - Charles, IDA, proxy app... (optional) - - ## 子项目 Sub repositories +[Arcaea-Server-Wiki](https://arcaea.lost-msth.cn/Arcaea-Server/) +: 项目文档 Project documentation + [Arcaea-Bundler](https://github.com/Lost-MSth/Arcaea-Bundler) : 用于生成和解包内容捆绑包 Used to pack or unpack content bundles -## 使用说明 Instruction for use +[Arcaea-Server-Frontend](https://github.com/Lost-MSth/arcaea_server_frontend) +: In building + +## 旧的说明 Old wiki + + + +### 使用说明 Instruction for use [中文](https://github.com/Lost-MSth/Arcaea-server/wiki/%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E) [English](https://github.com/Lost-MSth/Arcaea-server/wiki/Instruction-for-use) -## 注意 Attentions +### 注意 Attentions [中文](https://github.com/Lost-MSth/Arcaea-server/wiki/%E6%B3%A8%E6%84%8F) [English](https://github.com/Lost-MSth/Arcaea-server/wiki/Attentions) -## Q&A +### Q&A [中文 / English](https://github.com/Lost-MSth/Arcaea-server/wiki/Q&A) @@ -164,7 +216,7 @@ It is just so interesting. What it can do is under exploration. ## 联系方式 Contact 如有必要,可以联系本人 Contact me if necessary -邮箱 Email:th84292@foxmail.com +邮箱 Email:arcaea@lost-msth.cn ## 支持一下 Support me