[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

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