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 @@ class DatabaseInit: ('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 @@ class DatabaseInit: 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 @@ class UserItem(Item): class NormalItem(UserItem): - def __init__(self, c) -> None: + def __init__(self, c=None) -> None: super().__init__() self.c = c @@ -115,7 +115,7 @@ class NormalItem(UserItem): class PositiveItem(UserItem): - def __init__(self, c) -> None: + def __init__(self, c=None) -> None: super().__init__() self.c = c @@ -142,7 +142,7 @@ class PositiveItem(UserItem): 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 @@ class Memory(UserItem): 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 @@ class ProgBoost(UserItem): 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 @@ class Stamina6(UserItem): 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 @@ class ItemFactory: 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 @@ class Purchase(CollectionItemMixin): self.items: list = [] - # TODO: "discount_reason": "extend" + # TODO: "discount_reason": extend, sale @property def price_displayed(self) -> int: @@ -44,6 +44,12 @@ class Purchase(CollectionItemMixin): 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 @@ class Purchase(CollectionItemMixin): 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 @@ class Purchase(CollectionItemMixin): 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 @@ class UserPlay(UserScore): 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 @@ class UserPlay(UserScore): @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 @@ class UserPlay(UserScore): '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 @@ class Chart: 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 @@ class 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 @@ class Song: 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 @@ from .error import (ArcError, DataExist, FriendError, InputError, NoAccess, 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 @@ class UserInfo(User): 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 @@ class UserInfo(User): '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 flask import Blueprint, jsonify 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 get_bps(): 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 core.user import UserOnline 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 @@ 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 @@ + 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 @@ + 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 @@ + +
如果没有某个谱面,应该填入-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 all_song(): 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 add_song(): 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 add_song(): 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)