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
{% endif %}
+
+ {% if song['rating_etr'] %}
+ 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 @@
+
+