[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.
This commit is contained in:
Lost-MSth
2024-03-10 11:26:21 +08:00
parent e206247c09
commit d65cc3bcbe
24 changed files with 554 additions and 63 deletions

View File

@@ -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)

View File

@@ -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')

View File

@@ -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]

View File

@@ -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'

View File

@@ -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 = []

View File

@@ -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':

View File

@@ -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]

View File

@@ -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:

View File

@@ -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()

View File

@@ -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:

View File

@@ -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':

View File

@@ -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"]

View File

@@ -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
}
]

View File

@@ -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);

View File

@@ -1,3 +1,3 @@
flask>=2.0.2
cryptography>=3.0.0
cryptography>=35.0.0
limits>=2.7.0

View File

@@ -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)]

View File

@@ -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()
})

View File

@@ -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,7 +88,8 @@ def applog_me():
return success_return({})
map_dict = {'/user/me': user_me,
map_dict = {
'/user/me': user_me,
'/purchase/bundle/pack': bundle_pack,
'/serve/download/me/song': download_song,
'/game/info': game_info,
@@ -74,7 +97,9 @@ map_dict = {'/user/me': user_me,
'/world/map/me': world_all,
'/score/song/friend': song_score_friend,
'/purchase/bundle/bundle': bundle_bundle,
'/finale/progress': finale_progress}
'/finale/progress': finale_progress,
'/purchase/bundle/single': get_single
}
@bp.route('/compose/aggregate', methods=['GET']) # 集成式请求

View File

@@ -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;

View File

@@ -34,6 +34,11 @@
<span class="difficulty_byd">BYD</span>
<span class="song-rating">{{song['rating_byn']}}</span>
{% endif %}
<br />
{% if song['rating_etr'] %}
<span class="difficulty_etr">ETR</span>
<span class="song-rating">{{song['rating_etr']}}</span>
{% endif %}
</div>

View File

@@ -28,6 +28,7 @@
<option value='fragment'>Fragment</option>
<option value='memory'>Memory</option>
<option value='anni5tix'>Anniversary 5 ticket</option>
<option value='pick_ticket'>Pick ticket</option>
</select>
</div>
<label for="amount">Amount</label>

View File

@@ -39,6 +39,7 @@
<option value='fragment'>Fragment</option>
<option value='memory'>Memory</option>
<option value='anni5tix'>Anniversary 5 ticket</option>
<option value='pick_ticket'>Pick ticket</option>
</select>
</div>
<label for="amount">Amount</label>

View File

@@ -19,6 +19,8 @@
<input name="rating_ftr" id="rating_ftr" required>
<label for="rating_byd">Beyond chart const</label>
<input name="rating_byd" id="rating_byd" required>
<label for="rating_etr">Eternal chart const</label>
<input name="rating_etr" id="rating_etr" required>
<div class="content">如果没有某个谱面,应该填入-1。</div>
<div class="content">If there is no some chart, fill in -1 please.</div>
<input type="submit" value="Add">

View File

@@ -191,12 +191,14 @@ def all_song():
if x:
posts = []
for i in x:
posts.append({'song_id': i[0],
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_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)