4 Commits

Author SHA1 Message Date
Lost-MSth
1c58aeb526 Merge pull request #99 from Lost-MSth/dev
Update to v2.11.1
2023-03-24 19:11:32 +08:00
Lost-MSth
6f82e9b269 Update to v2.11.1 2023-03-24 19:09:47 +08:00
Lost-MSth
f92d8a9642 [Enhance] Skill of Mithra
- Add support for the skill of Mithra
2023-03-23 23:17:02 +08:00
Lost-MSth
1672d337ff [Enhance] [Refactor] Add API about characters
- Add API endpoints about characters
- Some small changes in refactoring
2023-03-22 22:27:21 +08:00
48 changed files with 654 additions and 403 deletions

View File

@@ -73,18 +73,12 @@ It is just so interesting. What it can do is under exploration.
> >
> Tips: When updating, please keep the original database in case of data loss. > Tips: When updating, please keep the original database in case of data loss.
### Version 2.11.0 ### Version 2.11.1
- 适用于Arcaea 4.3.0版本 For Arcaea 4.3.0 - 适用于Arcaea 4.4.0版本 For Arcaea 4.4.0
- 新搭档 **霞玛(大~宇~宙)**、**米露可(大~宇~宙)**、**紫黑**、**百合咲美香** 已解锁 Unlock the character **Shama(UNiVERSE)**, **Milk(UNiVERSE)**, **Shikoku**, **Mika Yurisaki**. - 新搭档 **密特拉·泰尔塞拉**、**不来方斗亚** 已解锁 Unlock the character **Mithra Tercera** and **Toa Kozukata**.
- 搭档 **依莉丝** 已觉醒 Uncap the character **Ilith**. - **密特拉·泰尔塞拉** 的技能提供支持 Add support for the skill of **Mithra Tercera**.
- 为觉醒 **依莉丝** 以及 **百合咲美香** 的技能提供支持 Add support for the skills of uncapped **Ilith** and **Mika Yurisaki**. - 新增修改搭档的API接口 Add some API endpoints about characters.
- 为 Beyond 图倍增提供支持 Add support for beyond gauge boost.
- 为 Beyond 连锁图提供支持 Add support for beyond chain maps.
- 修复联机时无人房间仍可进入的问题 Fix a logic bug that the room without anyone can be entered in multiplayer.
- 对一些数值的算法进行了更改 Some changes in some values' algorithms.
- 小重构 Link Play 子程序 Refactor simply for Link Play subprogram.
- 新增增删改兑换码、购买项目、登陆奖励、物品的API接口 Add some API endpoints, including creating, changing, deleting about redeem, purchase, login present and item.
## 运行环境与依赖 Running environment and requirements ## 运行环境与依赖 Running environment and requirements

View File

@@ -1,7 +1,7 @@
from flask import Blueprint from flask import Blueprint
from . import (users, songs, token, system, items, from . import (users, songs, token, system, items,
purchases, presents, redeems) purchases, presents, redeems, characters)
bp = Blueprint('api', __name__, url_prefix='/api/v1') bp = Blueprint('api', __name__, url_prefix='/api/v1')
bp.register_blueprint(users.bp) bp.register_blueprint(users.bp)
@@ -12,3 +12,4 @@ bp.register_blueprint(items.bp)
bp.register_blueprint(purchases.bp) bp.register_blueprint(purchases.bp)
bp.register_blueprint(presents.bp) bp.register_blueprint(presents.bp)
bp.register_blueprint(redeems.bp) bp.register_blueprint(redeems.bp)
bp.register_blueprint(characters.bp)

View File

@@ -52,11 +52,12 @@ def role_required(request, powers=[]):
def request_json_handle(request, required_keys: list = [], optional_keys: list = [], must_change: bool = False, is_batch: bool = False): def request_json_handle(request, required_keys: list = [], optional_keys: list = [], must_change: bool = False, is_batch: bool = False):
''' '''
提取post参数返回dict写成了修饰器\ 提取post参数返回dict写成了修饰器
parameters: \
`request`: `Request` - 当前请求\ parameters:
`required_keys`: `list` - 必须的参数\ `request`: `Request` - 当前请求
`optional_keys`: `list` - 可选的参数\ `required_keys`: `list` - 必须的参数
`optional_keys`: `list` - 可选的参数
`must_change`: `bool` - 当全都是可选参数时,是否必须有至少一项修改 `must_change`: `bool` - 当全都是可选参数时,是否必须有至少一项修改
''' '''
@@ -113,8 +114,7 @@ def api_try(view):
data = view(*args, **kwargs) data = view(*args, **kwargs)
if data is None: if data is None:
return error_return() return error_return()
else: return data
return data
except ArcError as e: except ArcError as e:
if Config.ALLOW_WARNING_LOG: if Config.ALLOW_WARNING_LOG:
current_app.logger.warning(format_exc()) current_app.logger.warning(format_exc())

View File

@@ -23,6 +23,8 @@ CODE_MSG = {
-122: 'Item already exists', -122: 'Item already exists',
-123: 'The collection already has this item', -123: 'The collection already has this item',
-124: 'The collection does not have this item', -124: 'The collection does not have this item',
-130: 'No such character',
-131: 'Invalid skill ID',
-200: 'No permission', # 2xx用户相关错误 -200: 'No permission', # 2xx用户相关错误
-201: 'Wrong username or password', -201: 'Wrong username or password',
-202: 'User is banned', -202: 'User is banned',

View File

@@ -0,0 +1,134 @@
from flask import Blueprint, request
from core.error import InputError, NoData
from core.item import ItemFactory
from core.character import Character
from core.sql import Connect, Query, Sql
from .api_auth import api_try, request_json_handle, role_required
from .api_code import success_return
from .constant import Constant
bp = Blueprint('characters', __name__, url_prefix='/characters')
@bp.route('', methods=['GET'])
@role_required(request, ['select'])
@request_json_handle(request, optional_keys=Constant.QUERY_KEYS)
@api_try
def characters_get(data, user):
'''查询全角色信息'''
A = ['character_id', 'name', 'skill_id',
'skill_id_uncap', 'char_type', 'is_uncapped']
B = ['name', 'skill_id', 'skill_id_uncap']
C = ['name', 'frag1', 'prog1', 'overdrive1', 'frag20',
'prog20', 'overdrive20', 'frag30', 'prog30', 'overdrive30']
with Connect() as c:
query = Query(A, B, C).from_dict(data)
x = Sql(c).select('character', query=query)
r = [Character().from_list(i) for i in x]
if not r:
raise NoData(api_error_code=-2)
return success_return([x.to_dict() for x in r])
@bp.route('/<int:character_id>', methods=['GET'])
@role_required(request, ['select'])
@api_try
def characters_character_get(user, character_id: int):
# 包含core
with Connect() as c:
c = Character(c).select(character_id)
c.select_character_core()
return success_return(c.to_dict(has_cores=True))
@bp.route('/<int:character_id>', methods=['PUT'])
@role_required(request, ['change'])
@request_json_handle(request, optional_keys=['max_level', 'skill_id', 'skill_id_uncap', 'skill_unlock_level', 'skill_requires_uncap', 'char_type', 'is_uncapped', 'frag1', 'prog1', 'overdrive1', 'frag20', 'prog20', 'overdrive20', 'frag30', 'prog30', 'overdrive30'], must_change=True)
@api_try
def characters_character_put(data, user, character_id: int):
'''修改角色信息'''
if ('skill_id' in data and data['skill_id'] != '' and data['skill_id'] not in Constant.SKILL_IDS) or ('skill_id_uncap' in data and data['skill_id_uncap'] != '' and data['skill_id_uncap'] not in Constant.SKILL_IDS):
raise InputError('Invalid skill_id', api_error_code=-131)
with Connect() as c:
c = Character(c).select(character_id)
try:
if 'max_level' in data:
c.max_level = int(data['max_level'])
if 'skill_id' in data:
c.skill_id = data['skill_id']
if 'skill_id_uncap' in data:
c.skill_id_uncap = data['skill_id_uncap']
if 'skill_unlock_level' in data:
c.skill_unlock_level = int(data['skill_unlock_level'])
if 'skill_requires_uncap' in data:
c.skill_requires_uncap = data['skill_requires_uncap'] == 1
if 'char_type' in data:
c.char_type = int(data['char_type'])
if 'is_uncapped' in data:
c.is_uncapped = data['is_uncapped'] == 1
t = ['frag1', 'prog1', 'overdrive1', 'frag20', 'prog20',
'overdrive20', 'frag30', 'prog30', 'overdrive30']
for i in t:
if i not in data:
continue
if i.endswith('1'):
x = getattr(c, i[:-1])
x.start = float(data[i])
elif i.endswith('20'):
x = getattr(c, i[:-2])
x.mid = float(data[i])
else:
x = getattr(c, i[:-2])
x.end = float(data[i])
except ValueError as e:
raise InputError('Invalid input', api_error_code=-101) from e
c.update()
return success_return(c.to_dict())
@bp.route('/<int:character_id>/cores', methods=['GET'])
@role_required(request, ['select'])
@api_try
def characters_character_cores_get(user, character_id: int):
with Connect() as c:
c = Character(c)
c.character_id = character_id
c.select_character_core()
return success_return(c.uncap_cores_to_dict())
@bp.route('/<int:character_id>/cores', methods=['PATCH'])
@role_required(request, ['change'])
@request_json_handle(request, is_batch=True)
@api_try
def characters_character_cores_patch(data, user, character_id: int):
'''修改角色觉醒cores'''
def force_type_core(x: dict) -> dict:
x['item_type'] = 'core'
x['type'] = 'core'
return x
with Connect() as c:
ch = Character(c)
ch.character_id = character_id
ch.select_character_core()
ch.remove_items([ItemFactory.from_dict(x, c=c)
for x in map(force_type_core, data.get('remove', []))])
ch.add_items([ItemFactory.from_dict(x, c=c)
for x in map(force_type_core, data.get('create', []))])
updates = list(map(force_type_core, data.get('update', [])))
for x in updates:
if 'amount' not in x:
raise InputError('`amount` is required in `update`')
if not isinstance(x['amount'], int) or x['amount'] <= 0:
raise InputError(
'`amount` must be a positive integer', api_error_code=-101)
ch.update_items(
[ItemFactory.from_dict(x, c=c) for x in updates])
return success_return(ch.uncap_cores_to_dict())

View File

@@ -2,3 +2,6 @@ class Constant:
QUERY_KEYS = ['limit', 'offset', 'query', 'fuzzy_query', 'sort'] QUERY_KEYS = ['limit', 'offset', 'query', 'fuzzy_query', 'sort']
PATCH_KEYS = ['create', 'update', 'remove'] PATCH_KEYS = ['create', 'update', 'remove']
SKILL_IDS = ['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']

View File

@@ -17,21 +17,22 @@ bp = Blueprint('token', __name__, url_prefix='/token')
@api_try @api_try
def token_post(data): def token_post(data):
''' '''
登录获取token\ 登录获取token
{'auth': base64('<user_id>:<password>')} {'auth': base64('<user_id>:<password>')}
''' '''
try: try:
auth_decode = bytes.decode(b64decode(data['auth'])) auth_decode = bytes.decode(b64decode(data['auth']))
except: except Exception as e:
raise PostError(api_error_code=-100) raise PostError(api_error_code=-100) from e
if not ':' in auth_decode: if ':' not in auth_decode:
raise PostError(api_error_code=-100) raise PostError(api_error_code=-100)
name, password = auth_decode.split(':', 1) name, password = auth_decode.split(':', 1)
with Connect() as c: with Connect() as c:
user = APIUser(c) user = APIUser(c)
user.login(name, password, request.remote_addr) user.login(name, password, request.remote_addr)
return success_return({'token': user.token, 'user_id': user.user_id}) return success_return({'token': user.api_token, 'user_id': user.user_id})
@bp.route('', methods=['GET']) @bp.route('', methods=['GET'])

View File

@@ -128,7 +128,7 @@ def users_user_b30_get(user, user_id):
f'No best30 data of user `{user_id}`', api_error_code=-3) f'No best30 data of user `{user_id}`', api_error_code=-3)
x.select_song_name() x.select_song_name()
r = x.to_dict_list() r = x.to_dict_list()
rating_sum = sum([i.rating for i in x.scores]) rating_sum = sum(i.rating for i in x.scores)
return success_return({'user_id': user_id, 'b30_ptt': rating_sum / 30, 'data': r}) return success_return({'user_id': user_id, 'b30_ptt': rating_sum / 30, 'data': r})

View File

@@ -55,8 +55,8 @@ class Role:
{'a': self.role_id}) {'a': self.role_id})
x = self.c.fetchone() x = self.c.fetchone()
if x is None: if x is None:
raise NoData('The role `%s` does not exist.' % raise NoData(
self.role_id, api_error_code=-200) f'The role `{self.role_id}` does not exist.', api_error_code=-200)
self.caption = x[0] self.caption = x[0]
return self return self
@@ -133,19 +133,19 @@ class APIUser(UserOnline):
'a': self.name}) 'a': self.name})
x = self.c.fetchone() x = self.c.fetchone()
if x is None: if x is None:
raise NoData('The user `%s` does not exist.' % raise NoData(
self.name, api_error_code=-201, status=401) f'The user `{self.name}` does not exist.', api_error_code=-201, status=401)
if x[1] == '': if x[1] == '':
raise UserBan('The user `%s` is banned.' % self.name) raise UserBan(f'The user `{self.name}` is banned.')
if self.hash_pwd != x[1]: if self.hash_pwd != x[1]:
raise NoAccess('The password is incorrect.', raise NoAccess('The password is incorrect.',
api_error_code=-201, status=401) api_error_code=-201, status=401)
self.user_id = x[0] self.user_id = x[0]
now = int(time() * 1000) now = int(time() * 1000)
self.token = sha256( self.api_token = sha256(
(str(self.user_id) + str(now)).encode("utf8") + urandom(8)).hexdigest() (str(self.user_id) + str(now)).encode("utf8") + urandom(8)).hexdigest()
self.logout() self.logout()
self.c.execute('''insert into api_login values(?,?,?,?)''', self.c.execute('''insert into api_login values(?,?,?,?)''',
(self.user_id, self.token, now, self.ip)) (self.user_id, self.api_token, now, self.ip))

View File

@@ -1,7 +1,7 @@
from .config_manager import Config from .config_manager import Config
from .constant import Constant from .constant import Constant
from .error import ArcError, InputError, ItemNotEnough, NoData from .error import ArcError, InputError, ItemNotEnough, NoData
from .item import Item, ItemCore from .item import CollectionItemMixin, ItemCore
class Level: class Level:
@@ -9,9 +9,9 @@ class Level:
min_level = 1 min_level = 1
def __init__(self) -> None: def __init__(self) -> None:
self.max_level = None self.max_level: int = None
self.level = None self.level: int = None
self.exp = None self.exp: float = None
@property @property
def level_exp(self): def level_exp(self):
@@ -29,9 +29,9 @@ class Level:
a = [] a = []
b = [] b = []
for i in Constant.LEVEL_STEPS: for k, v in Constant.LEVEL_STEPS.items():
a.append(i) a.append(k)
b.append(Constant.LEVEL_STEPS[i]) b.append(v)
if exp < b[0]: # 向下溢出是异常状态不该被try捕获不然数据库无法回滚 if exp < b[0]: # 向下溢出是异常状态不该被try捕获不然数据库无法回滚
raise ValueError('EXP value error.') raise ValueError('EXP value error.')
@@ -46,23 +46,10 @@ class Level:
class Skill: class Skill:
def __init__(self) -> None: def __init__(self) -> None:
self.skill_id = None self.skill_id: str = None
self.skill_id_uncap = None self.skill_id_uncap: str = None
self.skill_unlock_level = None self.skill_unlock_level: int = None
self.skill_requires_uncap = None self.skill_requires_uncap: bool = None
class Core(Item):
item_type = 'core'
def __init__(self, core_type: str = '', amount: int = 0) -> None:
super().__init__()
self.item_id = core_type
self.amount = amount
self.is_available = True
def to_dict(self):
return {'core_type': self.item_id, 'amount': self.amount}
class CharacterValue: class CharacterValue:
@@ -75,8 +62,7 @@ class CharacterValue:
# 4/6859 = 0.00058317539 # 4/6859 = 0.00058317539
if level <= 10: if level <= 10:
return 0.00058317539 * (level - 1) ** 3 * (value_20 - value_1) + value_1 return 0.00058317539 * (level - 1) ** 3 * (value_20 - value_1) + value_1
else: return - 0.00058317539 * (20 - level) ** 3 * (value_20 - value_1) + value_20
return - 0.00058317539 * (20 - level) ** 3 * (value_20 - value_1) + value_20
# @staticmethod # @staticmethod
# def _calc_char_value_20(level: int, stata, statb, lva=1, lvb=20) -> float: # def _calc_char_value_20(level: int, stata, statb, lva=1, lvb=20) -> float:
@@ -95,44 +81,51 @@ class CharacterValue:
return (level - lva) * (statb - stata) / (lvb - lva) + stata return (level - lva) * (statb - stata) / (lvb - lva) + stata
def set_parameter(self, start: float = 0, mid: float = 0, end: float = 0): def set_parameter(self, start: float = 0, mid: float = 0, end: float = 0):
self.start = start self.start: float = start
self.mid = mid self.mid: float = mid
self.end = end self.end: float = end
def get_value(self, level: Level): def get_value(self, level: Level):
if level.min_level <= level.level <= level.mid_level: if level.min_level <= level.level <= level.mid_level:
return self._calc_char_value_20_math(level.level, self.start, self.mid) return self._calc_char_value_20_math(level.level, self.start, self.mid)
elif level.mid_level < level.level <= level.max_level: if level.mid_level < level.level <= level.max_level:
return self._calc_char_value_30(level.level, self.mid, self.end) return self._calc_char_value_30(level.level, self.mid, self.end)
else: return 0
return 0
class Character: class Character(CollectionItemMixin):
database_table_name = None database_table_name = None
collection_item_const = {
'name': 'character',
'table_name': 'char_item',
'table_primary_key': 'character_id',
'id_name': 'character_id',
'items_name': 'uncap_cores'
}
def __init__(self, c=None) -> None: def __init__(self, c=None) -> None:
self.c = c self.c = c
self.character_id = None self.character_id: int = None
self.name = None self.name: str = None
self.char_type = None self.char_type: int = None
self.is_uncapped = None self.is_uncapped: bool = None
self.is_uncapped_override = None self.is_uncapped_override: bool = None
self.skill = Skill() self.skill = Skill()
self.level = Level() self.level = Level()
self.frag = CharacterValue() self.frag = CharacterValue()
self.prog = CharacterValue() self.prog = CharacterValue()
self.overdrive = CharacterValue() self.overdrive = CharacterValue()
self.uncap_cores = [] self.uncap_cores: list = []
self.voice = None self.voice: list = None
@property @property
def skill_id_displayed(self) -> str: def skill_id_displayed(self) -> str:
return None return None
def uncap_cores_to_dict(self): def uncap_cores_to_dict(self):
return [x.to_dict() for x in self.uncap_cores] return [x.to_dict(character_format=True) for x in self.uncap_cores]
@property @property
def is_uncapped_displayed(self) -> bool: def is_uncapped_displayed(self) -> bool:
@@ -144,11 +137,71 @@ class Character:
# 应该是只有对立这样 # 应该是只有对立这样
return self.character_id == 1 return self.character_id == 1
def to_dict(self) -> dict: def to_dict(self, has_cores=False) -> dict:
pass # 用于API里游戏内的数据不太一样故不能super
r = {
'character_id': self.character_id,
'name': self.name,
'char_type': self.char_type,
'is_uncapped': self.is_uncapped,
'max_level': self.level.max_level,
'skill_id': self.skill.skill_id,
'skill_unlock_level': self.skill.skill_unlock_level,
'skill_requires_uncap': self.skill.skill_requires_uncap,
'skill_id_uncap': self.skill.skill_id_uncap,
'frag1': self.frag.start,
'frag20': self.frag.mid,
'frag30': self.frag.end,
'prog1': self.prog.start,
'prog20': self.prog.mid,
'prog30': self.prog.end,
'overdrive1': self.overdrive.start,
'overdrive20': self.overdrive.mid,
'overdrive30': self.overdrive.end,
}
if has_cores:
r['uncap_cores'] = self.uncap_cores_to_dict()
return r
def from_list(self, l: list) -> 'Character': def from_list(self, l: tuple) -> 'Character':
pass self.character_id = l[0]
self.name = l[1]
self.level.max_level = l[2]
self.frag.set_parameter(l[3], l[6], l[9])
self.prog.set_parameter(l[4], l[7], l[10])
self.overdrive.set_parameter(l[5], l[8], l[11])
self.skill.skill_id = l[12]
self.skill.skill_unlock_level = l[13]
self.skill.skill_requires_uncap = l[14] == 1
self.skill.skill_id_uncap = l[15]
self.char_type = l[16]
self.is_uncapped = l[17] == 1
return self
def select(self, character_id: int = None) -> 'Character':
if character_id is not None:
self.character_id = character_id
self.c.execute(
'select * from character where character_id = ?', (self.character_id,))
x = self.c.fetchone()
if not x:
raise NoData(
f'No such character: {self.character_id}', api_error_code=-130)
return self.from_list(x)
def update(self) -> None:
self.c.execute('''update character set name = ?, max_level = ?, frag1 = ?, frag20 = ?, frag30 = ?, prog1 = ?, prog20 = ?, prog30 = ?, overdrive1 = ?, overdrive20 = ?, overdrive30 = ?, skill_id = ?, skill_unlock_level = ?, skill_requires_uncap = ?, skill_id_uncap = ?, char_type = ?, is_uncapped = ? where character_id = ?''', (
self.name, self.level.max_level, self.frag.start, self.frag.mid, self.frag.end, self.prog.start, self.prog.mid, self.prog.end, self.overdrive.start, self.overdrive.mid, self.overdrive.end, self.skill.skill_id, self.skill.skill_unlock_level, self.skill.skill_requires_uncap, self.skill.skill_id_uncap, self.char_type, self.is_uncapped, self.character_id))
def select_character_core(self):
# 获取此角色所需核心
self.c.execute(
'''select item_id, amount from char_item where character_id = ? and type="core"''', (self.character_id,))
x = self.c.fetchall()
if x:
self.uncap_cores = []
for i in x:
self.uncap_cores.append(ItemCore(self.c, i[0], i[1]))
class UserCharacter(Character): class UserCharacter(Character):
@@ -169,27 +222,16 @@ class UserCharacter(Character):
'''对外显示的技能id''' '''对外显示的技能id'''
if self.is_uncapped_displayed and self.skill.skill_id_uncap: if self.is_uncapped_displayed and self.skill.skill_id_uncap:
return self.skill.skill_id_uncap return self.skill.skill_id_uncap
elif self.skill.skill_id and self.level.level >= self.skill.skill_unlock_level: if self.skill.skill_id and self.level.level >= self.skill.skill_unlock_level:
return self.skill.skill_id return self.skill.skill_id
else: return None
return None
def select_character_core(self):
# 获取此角色所需核心
self.c.execute(
'''select item_id, amount from char_item where character_id = ? and type="core"''', (self.character_id,))
x = self.c.fetchall()
if x:
self.uncap_cores = []
for i in x:
self.uncap_cores.append(Core(i[0], i[1]))
def select_character_uncap_condition(self, user=None): def select_character_uncap_condition(self, user=None):
# parameter: user - User类或子类的实例 # parameter: user - User类或子类的实例
# 获取此角色的觉醒信息 # 获取此角色的觉醒信息
if user: if user:
self.user = user self.user = user
self.c.execute('''select is_uncapped, is_uncapped_override from %s where user_id = :a and character_id = :b''' % self.database_table_name, self.c.execute(f'''select is_uncapped, is_uncapped_override from {self.database_table_name} where user_id = :a and character_id = :b''',
{'a': self.user.user_id, 'b': self.character_id}) {'a': self.user.user_id, 'b': self.character_id})
x = self.c.fetchone() x = self.c.fetchone()
@@ -206,7 +248,7 @@ class UserCharacter(Character):
# 获取所给用户此角色信息 # 获取所给用户此角色信息
if user: if user:
self.user = user self.user = user
self.c.execute('''select * from %s a,character b where a.user_id=? and a.character_id=b.character_id and a.character_id=?''' % self.database_table_name, self.c.execute(f'''select * from {self.database_table_name} a,character b where a.user_id=? and a.character_id=b.character_id and a.character_id=?''',
(self.user.user_id, self.character_id)) (self.user.user_id, self.character_id))
y = self.c.fetchone() y = self.c.fetchone()
@@ -228,7 +270,7 @@ class UserCharacter(Character):
self.skill.skill_unlock_level = y[19] self.skill.skill_unlock_level = y[19]
self.skill.skill_requires_uncap = y[20] == 1 self.skill.skill_requires_uncap = y[20] == 1
if self.character_id == 21 or self.character_id == 46: if self.character_id in (21, 46):
self.voice = [0, 1, 2, 3, 100, 1000, 1001] self.voice = [0, 1, 2, 3, 100, 1000, 1001]
self.select_character_core() self.select_character_core()
@@ -267,7 +309,7 @@ class UserCharacter(Character):
# 切换觉醒状态 # 切换觉醒状态
if user: if user:
self.user = user self.user = user
self.c.execute('''select is_uncapped, is_uncapped_override from %s where user_id = :a and character_id = :b''' % self.database_table_name, self.c.execute(f'''select is_uncapped, is_uncapped_override from {self.database_table_name} where user_id = :a and character_id = :b''',
{'a': self.user.user_id, 'b': self.character_id}) {'a': self.user.user_id, 'b': self.character_id})
x = self.c.fetchone() x = self.c.fetchone()
@@ -277,7 +319,7 @@ class UserCharacter(Character):
self.c.execute('''update user set is_char_uncapped_override = :a where user_id = :b''', { self.c.execute('''update user set is_char_uncapped_override = :a where user_id = :b''', {
'a': 1 if x[1] == 0 else 0, 'b': self.user.user_id}) 'a': 1 if x[1] == 0 else 0, 'b': self.user.user_id})
self.c.execute('''update %s set is_uncapped_override = :a where user_id = :b and character_id = :c''' % self.database_table_name, { self.c.execute(f'''update {self.database_table_name} set is_uncapped_override = :a where user_id = :b and character_id = :c''', {
'a': 1 if x[1] == 0 else 0, 'b': self.user.user_id, 'c': self.character_id}) 'a': 1 if x[1] == 0 else 0, 'b': self.user.user_id, 'c': self.character_id})
self.is_uncapped_override = x[1] == 0 self.is_uncapped_override = x[1] == 0
@@ -311,7 +353,7 @@ class UserCharacter(Character):
raise ItemNotEnough('The cores are not enough.') raise ItemNotEnough('The cores are not enough.')
for i in self.uncap_cores: for i in self.uncap_cores:
ItemCore(self.c, i, reverse=True).user_claim_item(self.user) i.user_claim_item(self.user, reverse=True)
self.c.execute('''update user_char set is_uncapped=1, is_uncapped_override=0 where user_id=? and character_id=?''', self.c.execute('''update user_char set is_uncapped=1, is_uncapped_override=0 where user_id=? and character_id=?''',
(self.user.user_id, self.character_id)) (self.user.user_id, self.character_id))
@@ -382,7 +424,7 @@ class UserCharacterList:
def select_user_characters(self): def select_user_characters(self):
self.c.execute( self.c.execute(
'''select character_id from %s where user_id=?''' % self.database_table_name, (self.user.user_id,)) f'''select character_id from {self.database_table_name} where user_id=?''', (self.user.user_id,))
x = self.c.fetchall() x = self.c.fetchall()
self.characters: list = [] self.characters: list = []
if x: if x:

View File

@@ -12,7 +12,7 @@ class Config:
SONG_FILE_HASH_PRE_CALCULATE = True SONG_FILE_HASH_PRE_CALCULATE = True
GAME_API_PREFIX = '/join/21' GAME_API_PREFIX = '/pollen/22'
ALLOW_APPVERSION = [] # list[str] ALLOW_APPVERSION = [] # list[str]

View File

@@ -1,6 +1,6 @@
from .config_manager import Config from .config_manager import Config
ARCAEA_SERVER_VERSION = 'v2.11.0' ARCAEA_SERVER_VERSION = 'v2.11.1'
class Constant: class Constant:

View File

@@ -96,7 +96,7 @@ class Course:
'''select * from course where course_id = ?''', (self.course_id,)) '''select * from course where course_id = ?''', (self.course_id,))
x = self.c.fetchone() x = self.c.fetchone()
if x is None: if x is None:
raise NoData('The course `%s` is not found.' % self.course_id) raise NoData(f'The course `{self.course_id}` is not found.')
return self.from_list(x) return self.from_list(x)
def select_course_chart(self) -> None: def select_course_chart(self) -> None:
@@ -151,7 +151,8 @@ class Course:
class UserCourse(Course): class UserCourse(Course):
''' '''
用户课题类\ 用户课题类
parameter: `user` - `User`类或子类的实例 parameter: `user` - `User`类或子类的实例
''' '''
@@ -200,7 +201,8 @@ class UserCourse(Course):
class UserCourseList: class UserCourseList:
''' '''
用户课题列表类\ 用户课题列表类
parameter: `user` - `User`类或子类的实例 parameter: `user` - `User`类或子类的实例
''' '''
@@ -237,8 +239,9 @@ class UserCourseList:
class CoursePlay(UserCourse): class CoursePlay(UserCourse):
''' '''
课题模式打歌类联动UserPlay\ 课题模式打歌类联动UserPlay
parameter: `user` - `UserOnline`类或子类的实例\
parameter: `user` - `UserOnline`类或子类的实例
'user_play` - `UserPlay`类的实例 'user_play` - `UserPlay`类的实例
''' '''

View File

@@ -88,7 +88,8 @@ class SonglistParser:
class UserDownload: class UserDownload:
''' '''
用户下载类\ 用户下载类
properties: `user` - `User`类或子类的实例 properties: `user` - `User`类或子类的实例
''' '''
@@ -158,8 +159,7 @@ class UserDownload:
if prefix[-1] != '/': if prefix[-1] != '/':
prefix += '/' prefix += '/'
return f'{prefix}{self.song_id}/{self.file_name}?t={self.token}' return f'{prefix}{self.song_id}/{self.file_name}?t={self.token}'
else: return url_for('download', file_path=f'{self.song_id}/{self.file_name}', t=self.token, _external=True)
return url_for('download', file_path=f'{self.song_id}/{self.file_name}', t=self.token, _external=True)
@property @property
def hash(self) -> str: def hash(self) -> str:
@@ -243,7 +243,7 @@ class DownloadList(UserDownload):
re['audio']['3'] = {"checksum": x.hash, "url": x.url} re['audio']['3'] = {"checksum": x.hash, "url": x.url}
else: else:
re['audio']['3'] = {"checksum": x.hash} re['audio']['3'] = {"checksum": x.hash}
elif i == 'video.mp4' or i == 'video_audio.ogg': elif i in ('video.mp4', 'video_audio.ogg'):
if 'additional_files' not in re: if 'additional_files' not in re:
re['additional_files'] = [] re['additional_files'] = []

View File

@@ -41,16 +41,17 @@ class DatabaseInit:
def table_init(self) -> None: def table_init(self) -> None:
'''初始化数据库结构''' '''初始化数据库结构'''
with open(self.sql_path, 'r') as f: with open(self.sql_path, 'r', encoding='utf-8') as f:
self.c.executescript(f.read()) self.c.executescript(f.read())
self.c.execute('''insert into config values("version", :a);''', { self.c.execute('''insert into config values("version", :a);''', {
'a': ARCAEA_SERVER_VERSION}) 'a': ARCAEA_SERVER_VERSION})
def character_init(self) -> None: def character_init(self) -> None:
'''初始化搭档信息''' '''初始化搭档信息'''
uncapped_characters = self.init_data.char_core.keys()
for i in range(0, len(self.init_data.char)): for i in range(0, len(self.init_data.char)):
skill_requires_uncap = 1 if i == 2 else 0 skill_requires_uncap = 1 if i == 2 else 0
if i in [0, 1, 2, 4, 13, 26, 27, 28, 29, 36, 21, 42, 43, 11, 12, 19, 5, 10]: if i in uncapped_characters:
max_level = 30 max_level = 30
uncapable = 1 uncapable = 1
else: else:

View File

@@ -1,5 +1,5 @@
from .config_manager import Config from .config_manager import Config
from .error import InputError, ItemNotEnough, ItemUnavailable, NoData from .error import DataExist, InputError, ItemNotEnough, ItemUnavailable, NoData
class Item: class Item:
@@ -73,7 +73,8 @@ class UserItem(Item):
def select_user_item(self, user=None): def select_user_item(self, user=None):
''' '''
查询用户item\ 查询用户item
parameter: `user` - `User`类或子类的实例 parameter: `user` - `User`类或子类的实例
''' '''
if user is not None: if user is not None:
@@ -102,8 +103,7 @@ class NormalItem(UserItem):
if x[0] == 0: if x[0] == 0:
self.is_available = False self.is_available = False
raise ItemUnavailable('The item is unavailable.') raise ItemUnavailable('The item is unavailable.')
else: self.is_available = True
self.is_available = True
else: else:
raise NoData('No item data.') raise NoData('No item data.')
@@ -142,16 +142,29 @@ class PositiveItem(UserItem):
class ItemCore(PositiveItem): class ItemCore(PositiveItem):
item_type = 'core' item_type = 'core'
def __init__(self, c, core=None, reverse=False) -> None: def __init__(self, c, core_type: str = '', amount: int = 0) -> None:
super().__init__(c) super().__init__(c)
self.is_available = True self.is_available = True
if core: self.item_id = core_type
self.item_id = core.item_id self.amount = amount
self.amount = - core.amount if reverse else core.amount
def __str__(self) -> str: def __str__(self) -> str:
return self.item_id + '_' + str(self.amount) return self.item_id + '_' + str(self.amount)
def to_dict(self, has_is_available: bool = False, has_amount: bool = True, character_format: bool = False) -> dict:
if character_format:
# 搭档的core是特殊格式的
return {'core_type': self.item_id, 'amount': self.amount}
return super().to_dict(has_is_available=has_is_available, has_amount=has_amount)
def user_claim_item(self, user, reverse: bool = False) -> None:
# 骚操作将amount变为负数后使用再变回来
if reverse:
self.amount = -self.amount
super().user_claim_item(user)
if reverse:
self.amount = -self.amount
class ItemCharacter(UserItem): class ItemCharacter(UserItem):
item_type = 'character' item_type = 'character'
@@ -257,16 +270,10 @@ class CourseBanner(NormalItem):
class Single(NormalItem): class Single(NormalItem):
item_type = 'single' item_type = 'single'
def __init__(self, c) -> None:
super().__init__(c)
class Pack(NormalItem): class Pack(NormalItem):
item_type = 'pack' item_type = 'pack'
def __init__(self, c) -> None:
super().__init__(c)
class ProgBoost(UserItem): class ProgBoost(UserItem):
item_type = 'prog_boost_300' item_type = 'prog_boost_300'
@@ -276,7 +283,8 @@ class ProgBoost(UserItem):
def user_claim_item(self, user): def user_claim_item(self, user):
''' '''
世界模式prog_boost\ 世界模式prog_boost
parameters: `user` - `UserOnline`类或子类的实例 parameters: `user` - `UserOnline`类或子类的实例
''' '''
user.update_user_one_column('prog_boost', 300) user.update_user_one_column('prog_boost', 300)
@@ -290,7 +298,7 @@ class Stamina6(UserItem):
def user_claim_item(self, user): def user_claim_item(self, user):
''' '''
世界模式记忆源点或残片买体力+6\ 世界模式记忆源点或残片买体力+6
顺手清一下世界模式过载状态 顺手清一下世界模式过载状态
''' '''
user.select_user_about_stamina() user.select_user_about_stamina()
@@ -386,8 +394,8 @@ class ItemFactory:
class UserItemList: class UserItemList:
''' '''
用户的item列表\ 用户的item列表
注意只能查在user_item里面的character不行\ 注意只能查在user_item里面的character不行
properties: `user` - `User`类或子类的实例 properties: `user` - `User`类或子类的实例
''' '''
@@ -420,3 +428,63 @@ class UserItemList:
self.items.append(ItemFactory.from_dict( self.items.append(ItemFactory.from_dict(
{'item_id': i[0], 'amount': amount, 'item_type': item_type}, self.c)) {'item_id': i[0], 'amount': amount, 'item_type': item_type}, self.c))
return self return self
class CollectionItemMixin:
'''
批量修改一些集合中的items
'''
collection_item_const = {
'name': 'collection',
'table_name': 'collection_item',
'table_primary_key': 'collection_id',
'id_name': 'collection_id',
'items_name': 'items'
}
def add_items(self, items: 'list[Item]') -> None:
collection_id: 'str' = getattr(
self, self.collection_item_const['id_name'])
collection_items: 'list[Item]' = getattr(
self, self.collection_item_const['items_name'])
for i in items:
if not i.select_exists():
raise NoData(
f'No such item `{i.item_type}`: `{i.item_id}`', api_error_code=-121)
if i in collection_items:
raise DataExist(
f'Item `{i.item_type}`: `{i.item_id}` already exists in {self.collection_item_const["name"]} `{collection_id}`', api_error_code=-123)
self.c.executemany(f'''insert into {self.collection_item_const["table_name"]} values (?, ?, ?, ?)''', [
(collection_id, i.item_id, i.item_type, i.amount) for i in items])
collection_items.extend(items)
def remove_items(self, items: 'list[Item]') -> None:
collection_id: 'str' = getattr(
self, self.collection_item_const['id_name'])
collection_items: 'list[Item]' = getattr(
self, self.collection_item_const['items_name'])
for i in items:
if i not in collection_items:
raise NoData(
f'No such item `{i.item_type}`: `{i.item_id}` in {self.collection_item_const["name"]} `{collection_id}`', api_error_code=-124)
self.c.executemany(f'''delete from {self.collection_item_const["table_name"]} where {self.collection_item_const["table_primary_key"]}=? and item_id=? and type=?''', [
(collection_id, i.item_id, i.item_type) for i in items])
for i in items:
collection_items.remove(i)
def update_items(self, items: 'list[Item]') -> None:
collection_id: 'str' = getattr(
self, self.collection_item_const['id_name'])
collection_items: 'list[Item]' = getattr(
self, self.collection_item_const['items_name'])
for i in items:
if i not in collection_items:
raise NoData(
f'No such item `{i.item_type}`: `{i.item_id}` in {self.collection_item_const["name"]} `{collection_id}`', api_error_code=-124)
self.c.executemany(f'''update {self.collection_item_const["table_name"]} set amount=? where {self.collection_item_const["table_primary_key"]}=? and item_id=? and type=?''', [
(i.amount, collection_id, i.item_id, i.item_type) for i in items])
for i in items:
collection_items[collection_items.index(i)].amount = i.amount

View File

@@ -103,9 +103,9 @@ class RemoteMultiPlayer:
sock.sendall(bytes(data + "\n", "utf-8")) sock.sendall(bytes(data + "\n", "utf-8"))
try: try:
received = str(sock.recv(1024), "utf-8").strip() received = str(sock.recv(1024), "utf-8").strip()
except socket.timeout: except socket.timeout as e:
raise Timeout( raise Timeout(
'Timeout when waiting for data from link play server.', status=400) 'Timeout when waiting for data from link play server.', status=400) from e
# print(received) # print(received)
return received return received

View File

@@ -53,8 +53,7 @@ class RefreshAllScoreRating(BaseOperation):
where_values = [] where_values = []
for k in y: for k in y:
ptt = Score.calculate_rating(defnum, k[1]) ptt = Score.calculate_rating(defnum, k[1])
if ptt < 0: ptt = max(ptt, 0)
ptt = 0
values.append((ptt,)) values.append((ptt,))
where_values.append((k[0], i[0], j)) where_values.append((k[0], i[0], j))
if values: if values:
@@ -76,7 +75,7 @@ class RefreshSongFileCache(BaseOperation):
class SaveUpdateScore(BaseOperation): class SaveUpdateScore(BaseOperation):
''' '''
云存档更新成绩,是覆盖式更新\ 云存档更新成绩,是覆盖式更新
提供user参数时只更新该用户的成绩否则更新所有用户的成绩 提供user参数时只更新该用户的成绩否则更新所有用户的成绩
''' '''
_name = 'save_update_score' _name = 'save_update_score'
@@ -125,8 +124,7 @@ class SaveUpdateScore(BaseOperation):
if i['song_id'] in song_chart_const: if i['song_id'] in song_chart_const:
rating = Score.calculate_rating( rating = Score.calculate_rating(
song_chart_const[i['song_id']][i['difficulty']] / 10, i['score']) song_chart_const[i['song_id']][i['difficulty']] / 10, i['score'])
if rating < 0: rating = max(rating, 0)
rating = 0
y = f'{i["song_id"]}{i["difficulty"]}' y = f'{i["song_id"]}{i["difficulty"]}'
if y in clear_state: if y in clear_state:
@@ -143,7 +141,7 @@ class SaveUpdateScore(BaseOperation):
def _all_update(self): def _all_update(self):
with Connect() as c: with Connect() as c:
c.execute( c.execute(
f'''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn from chart''') '''select song_id, rating_pst, rating_prs, rating_ftr, rating_byn from chart''')
song_chart_const = {i[0]: [i[1], i[2], i[3], i[4]] song_chart_const = {i[0]: [i[1], i[2], i[3], i[4]]
for i in c.fetchall()} # chart const * 10 for i in c.fetchall()} # chart const * 10
c.execute('''select user_id from user_save''') c.execute('''select user_id from user_save''')
@@ -162,8 +160,7 @@ class SaveUpdateScore(BaseOperation):
if i['song_id'] in song_chart_const: if i['song_id'] in song_chart_const:
rating = Score.calculate_rating( rating = Score.calculate_rating(
song_chart_const[i['song_id']][i['difficulty']] / 10, i['score']) song_chart_const[i['song_id']][i['difficulty']] / 10, i['score'])
if rating < 0: rating = max(rating, 0)
rating = 0
y = f'{i["song_id"]}{i["difficulty"]}' y = f'{i["song_id"]}{i["difficulty"]}'
if y in clear_state: if y in clear_state:
@@ -180,7 +177,7 @@ class SaveUpdateScore(BaseOperation):
class UnlockUserItem(BaseOperation): class UnlockUserItem(BaseOperation):
''' '''
全解锁/锁定用户物品\ 全解锁/锁定用户物品
提供user参数时只更新该用户的否则更新所有用户的 提供user参数时只更新该用户的否则更新所有用户的
''' '''
_name = 'unlock_user_item' _name = 'unlock_user_item'
@@ -198,7 +195,7 @@ class UnlockUserItem(BaseOperation):
self.user.user_id = int(user_id) self.user.user_id = int(user_id)
if method in ['unlock', 'lock']: if method in ['unlock', 'lock']:
self.method = method self.method = method
if isinstance(item_types, list) and all([i in self.ALLOW_TYPES for i in item_types]): if isinstance(item_types, list) and all(i in self.ALLOW_TYPES for i in item_types):
self.item_types = item_types self.item_types = item_types
def run(self): def run(self):

View File

@@ -1,10 +1,18 @@
from time import time from time import time
from .error import ArcError, DataExist, NoData from .error import ArcError, NoData
from .item import ItemFactory from .item import CollectionItemMixin, ItemFactory
class Present: class Present(CollectionItemMixin):
collection_item_const = {
'name': 'present',
'table_name': 'present_item',
'table_primary_key': 'present_id',
'id_name': 'present_id',
'items_name': 'items'
}
def __init__(self, c=None) -> None: def __init__(self, c=None) -> None:
self.c = c self.c = c
self.present_id: str = None self.present_id: str = None
@@ -109,47 +117,11 @@ class Present:
self.c.execute('''update present set expire_ts=?, description=? where present_id=?''', self.c.execute('''update present set expire_ts=?, description=? where present_id=?''',
(self.expire_ts, self.description, self.present_id)) (self.expire_ts, self.description, self.present_id))
def remove_items(self, items: list) -> None:
'''删除present_item表中的物品'''
for i in items:
if i not in self.items:
raise NoData(
f'No such item `{i.item_type}`: `{i.item_id}` in present `{self.present_id}`', api_error_code=-124)
self.c.executemany('''delete from present_item where present_id=? and item_id=? and type=?''', [
(self.present_id, i.item_id, i.item_type) for i in items])
for i in items:
self.items.remove(i)
def add_items(self, items: list) -> None:
'''添加物品到present_item表'''
for i in items:
if not i.select_exists():
raise NoData(
f'No such item `{i.item_type}`: `{i.item_id}`', api_error_code=-121)
if i in self.items:
raise DataExist(
f'Item `{i.item_type}`: `{i.item_id}` already exists in present `{self.present_id}`', api_error_code=-123)
self.c.executemany('''insert into present_item values(?,?,?,?)''', [
(self.present_id, i.item_id, i.item_type, i.amount) for i in items])
self.items.extend(items)
def update_items(self, items: list) -> None:
'''更新present_item表中的物品'''
for i in items:
if i not in self.items:
raise NoData(
f'No such item `{i.item_type}`: `{i.item_id}` in present `{self.present_id}`', api_error_code=-124)
self.c.executemany('''update present_item set amount=? where present_id=? and item_id=? and type=?''', [
(i.amount, self.present_id, i.item_id, i.item_type) for i in items])
for i in items:
self.items[self.items.index(i)].amount = i.amount
class UserPresent(Present): class UserPresent(Present):
''' '''
用户登录奖励类\ 用户登录奖励类
忽视了description的多语言\ 忽视了description的多语言
properties: `user` - `User`类或子类的实例 properties: `user` - `User`类或子类的实例
''' '''

View File

@@ -1,14 +1,21 @@
from time import time from time import time
from .error import DataExist, InputError, NoData, TicketNotEnough from .error import InputError, NoData, TicketNotEnough
from .item import ItemFactory from .item import CollectionItemMixin, ItemFactory
class Purchase: class Purchase(CollectionItemMixin):
''' '''
购买类\ 购买类
properties: `user` - `User`类或子类的实例 properties: `user` - `User`类或子类的实例
''' '''
collection_item_const = {
'name': 'purchase',
'table_name': 'purchase_item',
'table_primary_key': 'purchase_name',
'id_name': 'purchase_name',
'items_name': 'items'
}
def __init__(self, c=None, user=None): def __init__(self, c=None, user=None):
self.c = c self.c = c
@@ -212,45 +219,11 @@ class Purchase:
self.c.execute('''update purchase set price=:a, orig_price=:b, discount_from=:c, discount_to=:d, discount_reason=:e where purchase_name=:f''', { self.c.execute('''update purchase set price=:a, orig_price=:b, discount_from=:c, discount_to=:d, discount_reason=:e where purchase_name=:f''', {
'a': self.price, 'b': self.orig_price, 'c': self.discount_from, 'd': self.discount_to, 'e': self.discount_reason, 'f': self.purchase_name}) 'a': self.price, 'b': self.orig_price, 'c': self.discount_from, 'd': self.discount_to, 'e': self.discount_reason, 'f': self.purchase_name})
def add_items(self, items: list) -> None:
'''添加purchase_item表'''
for i in items:
if not i.select_exists():
raise NoData(
f'No such item `{i.item_type}`: `{i.item_id}`', api_error_code=-121)
if i in self.items:
raise DataExist(
f'Item `{i.item_type}`: `{i.item_id}` already exists in purchase `{self.purchase_name}`', api_error_code=-123)
self.c.executemany('''insert into purchase_item values (?, ?, ?, ?)''', [
(self.purchase_name, i.item_id, i.item_type, i.amount) for i in items])
self.items.extend(items)
def remove_items(self, items: list) -> None:
'''删除purchase_item表'''
for i in items:
if i not in self.items:
raise NoData(
f'No such item `{i.item_type}`: `{i.item_id}` in purchase `{self.purchase_name}`', api_error_code=-124)
self.c.executemany('''delete from purchase_item where purchase_name=? and item_id=? and type=?''', [
(self.purchase_name, i.item_id, i.item_type) for i in items])
for i in items:
self.items.remove(i)
def update_items(self, items: list) -> None:
'''更新purchase_item表只能更新amount'''
for i in items:
if i not in self.items:
raise NoData(
f'No such item `{i.item_type}`: `{i.item_id}` in purchase `{self.purchase_name}`', api_error_code=-124)
self.c.executemany('''update purchase_item set amount=? where purchase_name=? and item_id=? and type=?''', [
(i.amount, self.purchase_name, i.item_id, i.item_type) for i in items])
for i in items:
self.items[self.items.index(i)].amount = i.amount
class PurchaseList: class PurchaseList:
''' '''
购买列表类\ 购买列表类
property: `user` - `User`类或子类的实例 property: `user` - `User`类或子类的实例
''' '''

View File

@@ -7,8 +7,9 @@ from .user import UserInfo
class RankList: class RankList:
''' '''
排行榜类\ 排行榜类
默认limit=20limit<0认为是all\ 默认limit=20limit<0认为是all
property: `user` - `User`类或者子类的实例 property: `user` - `User`类或者子类的实例
''' '''
@@ -85,7 +86,8 @@ class RankList:
@staticmethod @staticmethod
def get_my_rank_parameter(my_rank: int, amount: int, all_limit: int = 20, max_local_position: int = Constant.MY_RANK_MAX_LOCAL_POSITION, max_global_position: int = Constant.MY_RANK_MAX_GLOBAL_POSITION): def get_my_rank_parameter(my_rank: int, amount: int, all_limit: int = 20, max_local_position: int = Constant.MY_RANK_MAX_LOCAL_POSITION, max_global_position: int = Constant.MY_RANK_MAX_GLOBAL_POSITION):
''' '''
计算我的排名中的查询参数\ 计算我的排名中的查询参数
returns: returns:
`sql_limit`: int - 查询limit参数 `sql_limit`: int - 查询limit参数
`sql_offset`: int - 查询offset参数 `sql_offset`: int - 查询offset参数
@@ -103,7 +105,7 @@ class RankList:
need_myself = True need_myself = True
elif amount - my_rank < all_limit - max_local_position: # 后方人数不足,显示排名 elif amount - my_rank < all_limit - max_local_position: # 后方人数不足,显示排名
sql_offset = amount - all_limit sql_offset = amount - all_limit
elif my_rank >= max_local_position and my_rank <= max_global_position - all_limit + max_local_position - 1: # 前方人数足够,显示排名 elif max_local_position <= my_rank <= max_global_position - all_limit + max_local_position - 1: # 前方人数足够,显示排名
sql_offset = my_rank - max_local_position sql_offset = my_rank - max_local_position
else: # 我已经忘了这是什么了 else: # 我已经忘了这是什么了
sql_offset = max_global_position - all_limit sql_offset = max_global_position - all_limit

View File

@@ -1,8 +1,16 @@
from .error import DataExist, NoData, RedeemUnavailable from .error import NoData, RedeemUnavailable
from .item import ItemFactory from .item import CollectionItemMixin, ItemFactory
class Redeem: class Redeem(CollectionItemMixin):
collection_item_const = {
'name': 'redeem',
'table_name': 'redeem_item',
'table_primary_key': 'code',
'id_name': 'code',
'items_name': 'items'
}
def __init__(self, c=None) -> None: def __init__(self, c=None) -> None:
self.c = c self.c = c
self.code: str = None self.code: str = None
@@ -87,46 +95,11 @@ class Redeem:
self.c.execute('''update redeem set type=? where code=?''', self.c.execute('''update redeem set type=? where code=?''',
(self.redeem_type, self.code)) (self.redeem_type, self.code))
def remove_items(self, items: list) -> None:
'''删除redeem_item表中的物品'''
for i in items:
if i not in self.items:
raise NoData(
f'No such item `{i.item_type}`: `{i.item_id}` in redeem `{self.code}`', api_error_code=-124)
self.c.executemany('''delete from redeem_item where code=? and item_id=? and type=?''', [
(self.code, i.item_id, i.item_type) for i in items])
for i in items:
self.items.remove(i)
def add_items(self, items: list) -> None:
'''添加物品到redeem_item表'''
for i in items:
if not i.select_exists():
raise NoData(
f'No such item `{i.item_type}`: `{i.item_id}`', api_error_code=-121)
if i in self.items:
raise DataExist(
f'Item `{i.item_type}`: `{i.item_id}` already exists in redeem `{self.code}`', api_error_code=-123)
self.c.executemany('''insert into redeem_item values(?,?,?,?)''', [
(self.code, i.item_id, i.item_type, i.amount) for i in items])
self.items.extend(items)
def update_items(self, items: list) -> None:
'''更新redeem_item表中的物品'''
for i in items:
if i not in self.items:
raise NoData(
f'No such item `{i.item_type}`: `{i.item_id}` in redeem `{self.code}`', api_error_code=-124)
self.c.executemany('''update redeem_item set amount=? where code=? and item_id=? and type=?''', [
(i.amount, self.code, i.item_id, i.item_type) for i in items])
for i in items:
self.items[self.items.index(i)].amount = i.amount
class UserRedeem(Redeem): class UserRedeem(Redeem):
''' '''
用户兑换码类\ 用户兑换码类
properties: `user` - `User`类或子类的实例 properties: `user` - `User`类或子类的实例
''' '''
@@ -165,7 +138,7 @@ class UserRedeem(Redeem):
if self.redeem_type == 0: if self.redeem_type == 0:
raise RedeemUnavailable( raise RedeemUnavailable(
'The redeem `%s` is unavailable.' % self.code) 'The redeem `%s` is unavailable.' % self.code)
elif self.redeem_type == 1: if self.redeem_type == 1:
raise RedeemUnavailable( raise RedeemUnavailable(
'The redeem `%s` is unavailable.' % self.code, 506) 'The redeem `%s` is unavailable.' % self.code, 506)

View File

@@ -11,6 +11,8 @@ class SaveData:
def __init__(self, c=None) -> None: def __init__(self, c=None) -> None:
self.c = c self.c = c
self.user = None
self.scores_data = [] self.scores_data = []
self.clearlamps_data = [] self.clearlamps_data = []
self.clearedsongs_data = [] self.clearedsongs_data = []
@@ -127,7 +129,7 @@ class SaveData:
'Property `%s` is not found in the instance of `SaveData` class.' % key) 'Property `%s` is not found in the instance of `SaveData` class.' % key)
if md5(value) == checksum: if md5(value) == checksum:
if key == 'installid_data' or key == 'devicemodelname_data' or key == 'finalestate_data': if key in ('installid_data', 'devicemodelname_data', 'finalestate_data'):
self.__dict__[key] = json.loads(value)['val'] self.__dict__[key] = json.loads(value)['val']
else: else:
self.__dict__[key] = json.loads(value)[''] self.__dict__[key] = json.loads(value)['']

View File

@@ -14,6 +14,8 @@ from .world import WorldPlay
class Score: class Score:
def __init__(self) -> None: def __init__(self) -> None:
self.c = None
self.song: 'Chart' = Chart() self.song: 'Chart' = Chart()
self.score: int = None self.score: int = None
self.shiny_perfect_count: int = None self.shiny_perfect_count: int = None
@@ -45,18 +47,17 @@ class Score:
'''分数转换为评级''' '''分数转换为评级'''
if score >= 9900000: # EX+ if score >= 9900000: # EX+
return 6 return 6
elif 9800000 <= score < 9900000: # EX if score >= 9800000: # EX
return 5 return 5
elif 9500000 <= score < 9800000: # AA if score >= 9500000: # AA
return 4 return 4
elif 9200000 <= score < 9500000: # A if score >= 9200000: # A
return 3 return 3
elif 8900000 <= score < 9200000: # B if score >= 8900000: # B
return 2 return 2
elif 8600000 <= score < 8900000: # C if score >= 8600000: # C
return 1 return 1
else: return 0
return 0
@property @property
def song_grade(self) -> int: def song_grade(self) -> int:
@@ -67,21 +68,24 @@ class Score:
'''clear_type转换为成绩状态用数字大小标识便于比较''' '''clear_type转换为成绩状态用数字大小标识便于比较'''
if clear_type == 3: # PM if clear_type == 3: # PM
return 5 return 5
elif clear_type == 2: # FC if clear_type == 2: # FC
return 4 return 4
elif clear_type == 5: # Hard Clear if clear_type == 5: # Hard Clear
return 3 return 3
elif clear_type == 1: # Clear if clear_type == 1: # Clear
return 2 return 2
elif clear_type == 4: # Easy Clear if clear_type == 4: # Easy Clear
return 1 return 1
else: # Track Lost return 0 # Track Lost
return 0
@property @property
def song_state(self) -> int: def song_state(self) -> int:
return self.get_song_state(self.clear_type) return self.get_song_state(self.clear_type)
@property
def all_note_count(self) -> int:
return self.perfect_count + self.near_count + self.miss_count
@property @property
def is_valid(self) -> bool: def is_valid(self) -> bool:
'''分数有效性检查''' '''分数有效性检查'''
@@ -90,7 +94,7 @@ class Score:
if self.song.difficulty not in (0, 1, 2, 3): if self.song.difficulty not in (0, 1, 2, 3):
return False return False
all_note = self.perfect_count + self.near_count + self.miss_count all_note = self.all_note_count
if all_note == 0: if all_note == 0:
return False return False
@@ -112,8 +116,7 @@ class Score:
ptt = defnum + 2 ptt = defnum + 2
elif score < 9800000: elif score < 9800000:
ptt = defnum + (score-9500000) / 300000 ptt = defnum + (score-9500000) / 300000
if ptt < 0: ptt = max(ptt, 0)
ptt = 0
else: else:
ptt = defnum + 1 + (score-9800000) / 200000 ptt = defnum + 1 + (score-9800000) / 200000
@@ -217,7 +220,10 @@ class UserPlay(UserScore):
self.course_play_state: int = None self.course_play_state: int = None
self.course_play: 'CoursePlay' = None self.course_play: 'CoursePlay' = None
self.combo_interval_bonus: int = None # 不能给 None 以外的默认值
def to_dict(self) -> dict: def to_dict(self) -> dict:
# 不能super
if self.is_world_mode is None or self.course_play_state is None: if self.is_world_mode is None or self.course_play_state is None:
return {} return {}
if self.course_play_state == 4: if self.course_play_state == 4:
@@ -249,10 +255,15 @@ class UserPlay(UserScore):
if songfile_hash and songfile_hash != self.song_hash: if songfile_hash and songfile_hash != self.song_hash:
return False return False
x = self.song_token + self.song_hash + self.song.song_id + str(self.song.difficulty) + str(self.score) + str(self.shiny_perfect_count) + str( x = f'''{self.song_token}{self.song_hash}{self.song.song_id}{self.song.difficulty}{self.score}{self.shiny_perfect_count}{self.perfect_count}{self.near_count}{self.miss_count}{self.health}{self.modifier}{self.clear_type}'''
self.perfect_count) + str(self.near_count) + str(self.miss_count) + str(self.health) + str(self.modifier) + str(self.clear_type) if self.combo_interval_bonus is not None:
y = str(self.user.user_id) + self.song_hash if self.combo_interval_bonus < 0 or self.combo_interval_bonus > self.all_note_count / 150:
return False
x = x + str(self.combo_interval_bonus)
y = f'{self.user.user_id}{self.song_hash}'
checksum = md5(x+md5(y)) checksum = md5(x+md5(y))
if checksum != self.submission_hash: if checksum != self.submission_hash:
return False return False
@@ -302,7 +313,7 @@ class UserPlay(UserScore):
x = self.c.fetchone() x = self.c.fetchone()
if x: if x:
self.prog_boost_multiply = 300 if x[0] == 300 else 0 self.prog_boost_multiply = 300 if x[0] == 300 else 0
if x[1] < self.beyond_boost_gauge_usage or (self.beyond_boost_gauge_usage != 100 and self.beyond_boost_gauge_usage != 200): if x[1] < self.beyond_boost_gauge_usage or self.beyond_boost_gauge_usage not in (100, 200):
# 注意偷懒了没判断是否是beyond图 # 注意偷懒了没判断是否是beyond图
self.beyond_boost_gauge_usage = 0 self.beyond_boost_gauge_usage = 0
@@ -374,8 +385,8 @@ class UserPlay(UserScore):
'''更新此分数对应用户的recent30''' '''更新此分数对应用户的recent30'''
old_recent_10 = self.ptt.recent_10 old_recent_10 = self.ptt.recent_10
if self.is_protected: if self.is_protected:
old_r30 = [x for x in self.ptt.r30] old_r30 = self.ptt.r30.copy()
old_s30 = [x for x in self.ptt.s30] old_s30 = self.ptt.s30.copy()
# 寻找pop位置 # 寻找pop位置
songs = list(set(self.ptt.s30)) songs = list(set(self.ptt.s30))
@@ -479,7 +490,8 @@ class UserPlay(UserScore):
class Potential: class Potential:
''' '''
用户潜力值计算处理类\ 用户潜力值计算处理类
property: `user` - `User`类或子类的实例 property: `user` - `User`类或子类的实例
''' '''
@@ -487,8 +499,8 @@ class Potential:
self.c = c self.c = c
self.user = user self.user = user
self.r30: list = None self.r30: 'list[float]' = None
self.s30: list = None self.s30: 'list[str]' = None
self.songs_selected: list = None self.songs_selected: list = None
self.b30: list = None self.b30: list = None
@@ -503,7 +515,7 @@ class Potential:
'''获取用户best30的总潜力值''' '''获取用户best30的总潜力值'''
self.c.execute('''select rating from best_score where user_id = :a order by rating DESC limit 30''', { self.c.execute('''select rating from best_score where user_id = :a order by rating DESC limit 30''', {
'a': self.user.user_id}) 'a': self.user.user_id})
return sum([x[0] for x in self.c.fetchall()]) return sum(x[0] for x in self.c.fetchall())
def select_recent_30(self) -> None: def select_recent_30(self) -> None:
'''获取用户recent30数据''' '''获取用户recent30数据'''
@@ -578,7 +590,8 @@ class Potential:
class UserScoreList: class UserScoreList:
''' '''
用户分数查询类\ 用户分数查询类
properties: `user` - `User`类或子类的实例 properties: `user` - `User`类或子类的实例
''' '''

View File

@@ -7,6 +7,8 @@ class Chart:
def __init__(self, c=None, song_id: str = None, difficulty: int = None) -> None: def __init__(self, c=None, song_id: str = None, difficulty: int = None) -> None:
self.c = c self.c = c
self.song_id: str = None
self.difficulty: int = None
self.set_chart(song_id, difficulty) self.set_chart(song_id, difficulty)
self.defnum: int = None self.defnum: int = None
self.song_name: str = None self.song_name: str = None

View File

@@ -12,8 +12,8 @@ class Connect:
def __init__(self, file_path: str = Constant.SQLITE_DATABASE_PATH, in_memory: bool = False, logger=None) -> None: def __init__(self, file_path: str = Constant.SQLITE_DATABASE_PATH, in_memory: bool = False, logger=None) -> None:
""" """
数据库连接默认连接arcaea_database.db\ 数据库连接默认连接arcaea_database.db
接受:文件路径\ 接受:文件路径
返回sqlite3连接操作对象 返回sqlite3连接操作对象
""" """
self.file_path = file_path self.file_path = file_path
@@ -21,6 +21,9 @@ class Connect:
if logger is not None: if logger is not None:
self.logger = logger self.logger = logger
self.conn: sqlite3.Connection = None
self.c: sqlite3.Cursor = None
def __enter__(self) -> sqlite3.Cursor: def __enter__(self) -> sqlite3.Cursor:
if self.in_memory: if self.in_memory:
self.conn = sqlite3.connect( self.conn = sqlite3.connect(
@@ -144,19 +147,19 @@ class Query:
raise InputError(api_error_code=-104) raise InputError(api_error_code=-104)
self.__sort = sort self.__sort = sort
def set_value(self, limit=-1, offset=0, query={}, fuzzy_query={}, sort=[]) -> None: def set_value(self, limit=-1, offset=0, query=None, fuzzy_query=None, sort=None) -> None:
self.limit = limit self.limit = limit
self.offset = offset self.offset = offset
self.query = query self.query = query if query is not None else {}
self.fuzzy_query = fuzzy_query self.fuzzy_query = fuzzy_query if fuzzy_query is not None else {}
self.sort = sort self.sort = sort if sort is not None else []
def from_dict(self, d: dict) -> 'Query': def from_dict(self, d: dict) -> 'Query':
self.set_value(d.get('limit', -1), d.get('offset', 0), self.set_value(d.get('limit', -1), d.get('offset', 0),
d.get('query', {}), d.get('fuzzy_query', {}), d.get('sort', [])) d.get('query', {}), d.get('fuzzy_query', {}), d.get('sort', []))
return self return self
def from_args(self, query: dict, limit: int = -1, offset: int = 0, sort: list = [], fuzzy_query: dict = {}) -> 'Query': def from_args(self, query: dict, limit: int = -1, offset: int = 0, sort: list = None, fuzzy_query: dict = None) -> 'Query':
self.set_value(limit, offset, query, fuzzy_query, sort) self.set_value(limit, offset, query, fuzzy_query, sort)
return self return self
@@ -170,7 +173,7 @@ class Sql:
self.c = c self.c = c
@staticmethod @staticmethod
def get_select_sql(table_name: str, target_column: list = [], query: 'Query' = None): def get_select_sql(table_name: str, target_column: list = None, query: 'Query' = None):
'''拼接单表内行查询单句sql语句返回语句和参数列表''' '''拼接单表内行查询单句sql语句返回语句和参数列表'''
sql_list = [] sql_list = []
if not target_column: if not target_column:
@@ -210,8 +213,10 @@ class Sql:
return sql, sql_list return sql, sql_list
@staticmethod @staticmethod
def get_insert_sql(table_name: str, key: list = [], value_len: int = None, insert_type: str = None) -> str: def get_insert_sql(table_name: str, key: list = None, value_len: int = None, insert_type: str = None) -> str:
'''拼接insert语句请注意只返回sql语句insert_type为replace或ignore''' '''拼接insert语句请注意只返回sql语句insert_type为replace或ignore'''
if key is None:
key = []
insert_type = 'replace' if insert_type in [ insert_type = 'replace' if insert_type in [
'replace', 'R', 'r', 'REPLACE'] else 'ignore' 'replace', 'R', 'r', 'REPLACE'] else 'ignore'
return ('insert into ' if insert_type is None else 'insert or ' + insert_type + ' into ') + table_name + ('(' + ','.join(key) + ')' if key else '') + ' values(' + ','.join(['?'] * (len(key) if value_len is None else value_len)) + ')' return ('insert into ' if insert_type is None else 'insert or ' + insert_type + ' into ') + table_name + ('(' + ','.join(key) + ')' if key else '') + ' values(' + ','.join(['?'] * (len(key) if value_len is None else value_len)) + ')'
@@ -281,13 +286,13 @@ class Sql:
return sql, sql_list return sql, sql_list
def select(self, table_name: str, target_column: list = [], query: 'Query' = None) -> list: def select(self, table_name: str, target_column: list = None, query: 'Query' = None) -> list:
'''单表内行select单句sql语句返回fetchall数据''' '''单表内行select单句sql语句返回fetchall数据'''
sql, sql_list = self.get_select_sql(table_name, target_column, query) sql, sql_list = self.get_select_sql(table_name, target_column, query)
self.c.execute(sql, sql_list) self.c.execute(sql, sql_list)
return self.c.fetchall() return self.c.fetchall()
def select_exists(self, table_name: str, target_column: list = [], query: 'Query' = None) -> bool: def select_exists(self, table_name: str, target_column: list = None, query: 'Query' = None) -> bool:
'''单表内行select exists单句sql语句返回bool值''' '''单表内行select exists单句sql语句返回bool值'''
sql, sql_list = self.get_select_sql(table_name, target_column, query) sql, sql_list = self.get_select_sql(table_name, target_column, query)
self.c.execute('select exists(' + sql + ')', sql_list) self.c.execute('select exists(' + sql + ')', sql_list)
@@ -329,7 +334,7 @@ class Sql:
pk = [] pk = []
name = [] name = []
self.c.execute('''pragma table_info ("%s")''' % table_name) # 这里无法参数化 self.c.execute(f'''pragma table_info ("{table_name}")''') # 这里无法参数化
x = self.c.fetchall() x = self.c.fetchall()
if x: if x:
for i in x: for i in x:
@@ -390,8 +395,8 @@ class DatabaseMigrator:
''' '''
with Connect(self.c2_path) as c2: with Connect(self.c2_path) as c2:
with Connect(self.c1_path) as c1: with Connect(self.c1_path) as c1:
[self.update_one_table(c1, c2, i) for i in Constant.DATABASE_MIGRATE_TABLES:
for i in Constant.DATABASE_MIGRATE_TABLES] self.update_one_table(c1, c2, i)
if not Constant.UPDATE_WITH_NEW_CHARACTER_DATA: if not Constant.UPDATE_WITH_NEW_CHARACTER_DATA:
self.update_one_table(c1, c2, 'character') self.update_one_table(c1, c2, 'character')

View File

@@ -13,7 +13,7 @@ class GameInfo:
"stamina_recover_tick": Constant.STAMINA_RECOVER_TICK, "stamina_recover_tick": Constant.STAMINA_RECOVER_TICK,
"core_exp": Constant.CORE_EXP, "core_exp": Constant.CORE_EXP,
"curr_ts": int(time()*1000), "curr_ts": int(time()*1000),
"level_steps": [{'level': i, 'level_exp': Constant.LEVEL_STEPS[i]} for i in Constant.LEVEL_STEPS], "level_steps": [{'level': k, 'level_exp': v} for k, v in Constant.LEVEL_STEPS.items()],
"world_ranking_enabled": True, "world_ranking_enabled": True,
"is_byd_chapter_unlocked": True "is_byd_chapter_unlocked": True
} }

View File

@@ -2,6 +2,7 @@ import base64
import hashlib import hashlib
import time import time
from os import urandom from os import urandom
from random import randint
from .character import UserCharacter, UserCharacterList from .character import UserCharacter, UserCharacterList
from .config_manager import Config from .config_manager import Config
@@ -100,7 +101,6 @@ class UserRegister(User):
def _build_user_code(self): def _build_user_code(self):
# 生成9位的user_code用的自然是随机 # 生成9位的user_code用的自然是随机
from random import randint
random_times = 0 random_times = 0
while random_times <= 1000: while random_times <= 1000:
@@ -309,6 +309,8 @@ class UserInfo(User):
self.beyond_boost_gauge: float = 0 self.beyond_boost_gauge: float = 0
self.next_fragstam_ts: int = None self.next_fragstam_ts: int = None
self.world_mode_locked_end_ts: int = None self.world_mode_locked_end_ts: int = None
self.current_map: 'Map' = None
self.stamina: 'UserStamina' = None
self.__cores: list = None self.__cores: list = None
self.__packs: list = None self.__packs: list = None
@@ -406,7 +408,7 @@ class UserInfo(User):
self.c.execute('''select exists(select * from friend where user_id_me = :x and user_id_other = :y)''', self.c.execute('''select exists(select * from friend where user_id_me = :x and user_id_other = :y)''',
{'x': i[0], 'y': self.user_id}) {'x': i[0], 'y': self.user_id})
is_mutual = True if self.c.fetchone() == (1,) else False is_mutual = self.c.fetchone() == (1,)
you = UserOnline(self.c, i[0]) you = UserOnline(self.c, i[0])
you.select_user() you.select_user()
@@ -660,16 +662,16 @@ class UserInfo(User):
score_sum = 0 score_sum = 0
if len(song_list_ftr) >= 2: if len(song_list_ftr) >= 2:
self.c.execute('''select sum(score) from best_score where user_id=? and difficulty=2 and song_id in ({0})'''.format( self.c.execute(
','.join(['?']*(len(song_list_ftr)-1))), tuple(song_list_ftr)) f'''select sum(score) from best_score where user_id=? and difficulty=2 and song_id in ({','.join(['?']*(len(song_list_ftr)-1))})''', tuple(song_list_ftr))
x = self.c.fetchone() x = self.c.fetchone()
if x[0] is not None: if x[0] is not None:
score_sum += x[0] score_sum += x[0]
if len(song_list_byn) >= 2: if len(song_list_byn) >= 2:
self.c.execute('''select sum(score) from best_score where user_id=? and difficulty=3 and song_id in ({0})'''.format( self.c.execute(
','.join(['?']*(len(song_list_byn)-1))), tuple(song_list_byn)) f'''select sum(score) from best_score where user_id=? and difficulty=3 and song_id in ({','.join(['?']*(len(song_list_byn)-1))})''', tuple(song_list_byn))
x = self.c.fetchone() x = self.c.fetchone()
if x[0] is not None: if x[0] is not None:
@@ -682,13 +684,13 @@ class UserInfo(User):
def select_user_one_column(self, column_name: str, default_value=None) -> None: def select_user_one_column(self, column_name: str, default_value=None) -> None:
''' '''
查询user表的某个属性\ 查询user表的某个属性
请注意必须是一个普通属性,不能是一个类的实例 请注意必须是一个普通属性,不能是一个类的实例
''' '''
if column_name not in self.__dict__: if column_name not in self.__dict__:
raise InputError('No such column.') raise InputError('No such column.')
self.c.execute('''select %s from user where user_id = :a''' % self.c.execute(f'''select {column_name} from user where user_id = :a''', {
column_name, {'a': self.user_id}) 'a': self.user_id})
x = self.c.fetchone() x = self.c.fetchone()
if not x: if not x:
raise NoData('No user.', 108, -3) raise NoData('No user.', 108, -3)
@@ -697,15 +699,15 @@ class UserInfo(User):
def update_user_one_column(self, column_name: str, value=None) -> None: def update_user_one_column(self, column_name: str, value=None) -> None:
''' '''
更新user表的某个属性\ 更新user表的某个属性
请注意必须是一个普通属性,不能是一个类的实例 请注意必须是一个普通属性,不能是一个类的实例
''' '''
if column_name not in self.__dict__: if column_name not in self.__dict__:
raise InputError('No such column.') raise InputError('No such column.')
if value is not None: if value is not None:
self.__dict__[column_name] = value self.__dict__[column_name] = value
self.c.execute('''update user set %s = :a where user_id = :b''' % self.c.execute(f'''update user set {column_name} = :a where user_id = :b''', {
column_name, {'a': self.__dict__[column_name], 'b': self.user_id}) 'a': self.__dict__[column_name], 'b': self.user_id})
class UserOnline(UserInfo): class UserOnline(UserInfo):

View File

@@ -1,6 +1,6 @@
import json
import os import os
from functools import lru_cache from functools import lru_cache
from json import load
from random import random from random import random
from time import time from time import time
@@ -25,15 +25,15 @@ def get_world_name(file_dir: str = Constant.WORLD_MAP_FOLDER_PATH) -> list:
def get_world_info(map_id: str) -> dict: def get_world_info(map_id: str) -> dict:
'''读取json文件内容返回字典''' '''读取json文件内容返回字典'''
world_info = {} world_info = {}
with open(os.path.join(Constant.WORLD_MAP_FOLDER_PATH, map_id+'.json'), 'r') as f: with open(os.path.join(Constant.WORLD_MAP_FOLDER_PATH, f'{map_id}.json'), 'rb') as f:
world_info = json.load(f) world_info = load(f)
return world_info return world_info
def get_world_all(c, user) -> list: def get_world_all(c, user) -> list:
''' '''
读取所有地图信息,返回列表\ 读取所有地图信息,返回列表
parameter: `user` - `User`类或子类的实例 parameter: `user` - `User`类或子类的实例
''' '''
worlds = get_world_name() worlds = get_world_name()
@@ -44,7 +44,7 @@ class Step:
'''台阶类''' '''台阶类'''
def __init__(self) -> None: def __init__(self) -> None:
self.postion: int = None self.position: int = None
self.capture: int = None self.capture: int = None
self.items: list = [] self.items: list = []
self.restrict_id: str = None self.restrict_id: str = None
@@ -198,7 +198,7 @@ class Map:
class UserMap(Map): class UserMap(Map):
''' '''
用户地图类\ 用户地图类
parameters: `user` - `User`类或者子类的实例 parameters: `user` - `User`类或者子类的实例
''' '''
@@ -413,7 +413,8 @@ class Stamina:
class UserStamina(Stamina): class UserStamina(Stamina):
''' '''
用户体力类\ 用户体力类
parameter: `user` - `User`类或子类的实例 parameter: `user` - `User`类或子类的实例
''' '''
@@ -439,8 +440,9 @@ class UserStamina(Stamina):
class WorldPlay: class WorldPlay:
''' '''
世界模式打歌类处理特殊角色技能联动UserMap和UserPlay\ 世界模式打歌类处理特殊角色技能联动UserMap和UserPlay
parameter: `user` - `UserOnline`类或子类的实例\
parameter: `user` - `UserOnline`类或子类的实例
'user_play` - `UserPlay`类的实例 'user_play` - `UserPlay`类的实例
''' '''
@@ -598,8 +600,8 @@ class WorldPlay:
if self.user_play.beyond_gauge == 0: if self.user_play.beyond_gauge == 0:
# 更新byd大招蓄力条 # 更新byd大招蓄力条
self.user.beyond_boost_gauge += self.beyond_boost_gauge_addition self.user.beyond_boost_gauge += self.beyond_boost_gauge_addition
if self.user.beyond_boost_gauge > 200: self.user.beyond_boost_gauge = min(
self.user.beyond_boost_gauge = 200 self.user.beyond_boost_gauge, 200)
self.user.update_user_one_column( self.user.update_user_one_column(
'beyond_boost_gauge', self.user.beyond_boost_gauge) 'beyond_boost_gauge', self.user.beyond_boost_gauge)
elif self.user_play.beyond_boost_gauge_usage != 0 and self.user_play.beyond_boost_gauge_usage <= self.user.beyond_boost_gauge: elif self.user_play.beyond_boost_gauge_usage != 0 and self.user_play.beyond_boost_gauge_usage <= self.user.beyond_boost_gauge:
@@ -656,6 +658,8 @@ class WorldPlay:
self._special_tempest() self._special_tempest()
elif self.character_used.skill_id_displayed == 'ilith_awakened_skill': elif self.character_used.skill_id_displayed == 'ilith_awakened_skill':
self._ilith_awakened_skill() self._ilith_awakened_skill()
elif self.character_used.skill_id_displayed == 'skill_mithra':
self._skill_mithra()
else: else:
if self.character_used.skill_id_displayed == 'skill_vita': if self.character_used.skill_id_displayed == 'skill_vita':
self._skill_vita() self._skill_vita()
@@ -684,7 +688,7 @@ class WorldPlay:
def _skill_vita(self) -> None: def _skill_vita(self) -> None:
''' '''
vita技能overdrive随回忆率提升提升量最多为10\ vita技能overdrive随回忆率提升提升量最多为10
此处采用线性函数 此处采用线性函数
''' '''
self.over_skill_increase = 0 self.over_skill_increase = 0
@@ -745,7 +749,7 @@ class WorldPlay:
''' '''
x: 'Step' = self.user.current_map.steps_for_climbing[0] x: 'Step' = self.user.current_map.steps_for_climbing[0]
if ('randomsong' in x.step_type or 'speedlimit' in x.step_type) and self.user_play.song_grade < 5: if ('randomsong' in x.step_type or 'speedlimit' in x.step_type) and self.user_play.song_grade < 5:
self.character_bonus_progress = -self.step_value / 2 / self.step_times self.character_bonus_progress = -1 * self.step_value / 2 / self.step_times
self.step_value = self.step_value / 2 self.step_value = self.step_value / 2
self.user.current_map.reclimb(self.step_value) self.user.current_map.reclimb(self.step_value)
@@ -765,3 +769,10 @@ class WorldPlay:
self.character_used.level) self.character_used.level)
self.prog_skill_increase = self.character_used.prog.get_value( self.prog_skill_increase = self.character_used.prog.get_value(
self.character_used.level) self.character_used.level)
def _skill_mithra(self) -> None:
'''
mithra 技能,每 150 combo 增加世界模式进度+1直接理解成 prog 值增加
'''
if self.user_play.combo_interval_bonus:
self.prog_skill_increase = self.user_play.combo_interval_bonus

View File

@@ -1,45 +1,45 @@
class InitData: class InitData:
char = ['hikari', 'tairitsu', 'kou', 'sapphire', 'lethe', 'hikari&tairitsu(reunion)', 'Tairitsu(Axium)', 'Tairitsu(Grievous Lady)', 'stella', 'Hikari & Fisica', 'ilith', 'eto', 'luna', 'shirabe', 'Hikari(Zero)', 'Hikari(Fracture)', 'Hikari(Summer)', 'Tairitsu(Summer)', 'Tairitsu & Trin', char = ['hikari', 'tairitsu', 'kou', 'sapphire', 'lethe', 'hikari&tairitsu(reunion)', 'Tairitsu(Axium)', 'Tairitsu(Grievous Lady)', 'stella', 'Hikari & Fisica', 'ilith', 'eto', 'luna', 'shirabe', 'Hikari(Zero)', 'Hikari(Fracture)', 'Hikari(Summer)', 'Tairitsu(Summer)', 'Tairitsu & Trin',
'ayu', 'Eto & Luna', 'yume', 'Seine & Hikari', 'saya', 'Tairitsu & Chuni Penguin', 'Chuni Penguin', 'haruna', 'nono', 'MTA-XXX', 'MDA-21', 'kanae', 'Hikari(Fantasia)', 'Tairitsu(Sonata)', 'sia', 'DORO*C', 'Tairitsu(Tempest)', 'brillante', 'Ilith(Summer)', 'etude', 'Alice & Tenniel', 'Luna & Mia', 'areus', 'seele', 'isabelle', 'mir', 'lagrange', 'linka', 'nami', 'Saya & Elizabeth', 'lily', 'kanae(midsummer)', 'alice&tenniel(minuet)', 'tairitsu(elegy)', 'marija', 'vita', 'hikari(fatalis)', 'saki', 'setsuna', 'amane', 'kou(winter)', 'lagrange(aria)', 'lethe(apophenia)', 'shama(UNiVERSE)', 'milk(UNiVERSE)', 'shikoku', 'mika yurisaki'] 'ayu', 'Eto & Luna', 'yume', 'Seine & Hikari', 'saya', 'Tairitsu & Chuni Penguin', 'Chuni Penguin', 'haruna', 'nono', 'MTA-XXX', 'MDA-21', 'kanae', 'Hikari(Fantasia)', 'Tairitsu(Sonata)', 'sia', 'DORO*C', 'Tairitsu(Tempest)', 'brillante', 'Ilith(Summer)', 'etude', 'Alice & Tenniel', 'Luna & Mia', 'areus', 'seele', 'isabelle', 'mir', 'lagrange', 'linka', 'nami', 'Saya & Elizabeth', 'lily', 'kanae(midsummer)', 'alice&tenniel(minuet)', 'tairitsu(elegy)', 'marija', 'vita', 'hikari(fatalis)', 'saki', 'setsuna', 'amane', 'kou(winter)', 'lagrange(aria)', 'lethe(apophenia)', 'shama(UNiVERSE)', 'milk(UNiVERSE)', 'shikoku', 'mika yurisaki', 'Mithra Tercera', 'Toa Kozukata']
skill_id = ['gauge_easy', '', '', '', 'note_mirror', 'skill_reunion', '', 'gauge_hard', 'frag_plus_10_pack_stellights', 'gauge_easy|frag_plus_15_pst&prs', 'gauge_hard|fail_frag_minus_100', 'frag_plus_5_side_light', 'visual_hide_hp', 'frag_plus_5_side_conflict', 'challenge_fullcombo_0gauge', 'gauge_overflow', 'gauge_easy|note_mirror', 'note_mirror', 'visual_tomato_pack_tonesphere', skill_id = ['gauge_easy', '', '', '', 'note_mirror', 'skill_reunion', '', 'gauge_hard', 'frag_plus_10_pack_stellights', 'gauge_easy|frag_plus_15_pst&prs', 'gauge_hard|fail_frag_minus_100', 'frag_plus_5_side_light', 'visual_hide_hp', 'frag_plus_5_side_conflict', 'challenge_fullcombo_0gauge', 'gauge_overflow', 'gauge_easy|note_mirror', 'note_mirror', 'visual_tomato_pack_tonesphere',
'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', '', '', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', '', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', '', '', 'visual_ghost_skynotes', 'skill_vita', 'skill_fatalis', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', '', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika'] '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_id_uncap = ['', '', 'frags_kou', '', 'visual_ink', '', '', '', '', '', 'ilith_awakened_skill', 'eto_uncap', 'luna_uncap', 'shirabe_entry_fee', skill_id_uncap = ['', '', 'frags_kou', '', 'visual_ink', '', '', '', '', '', 'ilith_awakened_skill', 'eto_uncap', 'luna_uncap', 'shirabe_entry_fee',
'', '', '', '', '', 'ayu_uncap', '', 'frags_yume', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', ''] '', '', '', '', '', 'ayu_uncap', '', 'frags_yume', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']
skill_unlock_level = [0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, 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, 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]
frag1 = [55, 55, 60, 50, 47, 79, 47, 57, 41, 22, 50, 54, 60, 56, 78, 42, 41, 61, 52, 50, 52, 32, frag1 = [55, 55, 60, 50, 47, 79, 47, 57, 41, 22, 50, 54, 60, 56, 78, 42, 41, 61, 52, 50, 52, 32,
42, 55, 45, 58, 43, 0.5, 68, 50, 62, 45, 45, 52, 44, 27, 59, 0, 45, 50, 50, 47, 47, 61, 43, 42, 38, 25, 58, 50, 61, 45, 45, 38, 34, 27, 18, 56, 47, 30, 45, 57, 55.5, 47, 33, 26] 42, 55, 45, 58, 43, 0.5, 68, 50, 62, 45, 45, 52, 44, 27, 59, 0, 45, 50, 50, 47, 47, 61, 43, 42, 38, 25, 58, 50, 61, 45, 45, 38, 34, 27, 18, 56, 47, 30, 45, 57, 56, 47, 33, 26, 29, 66]
prog1 = [35, 55, 47, 50, 60, 70, 60, 70, 58, 45, 70, 45, 42, 46, 61, 67, 49, 44, 28, 45, 24, 46, 52, prog1 = [35, 55, 47, 50, 60, 70, 60, 70, 58, 45, 70, 45, 42, 46, 61, 67, 49, 44, 28, 45, 24, 46, 52,
59, 62, 33, 58, 25, 63, 69, 50, 45, 45, 51, 34, 70, 62, 70, 45, 32, 32, 61, 47, 47, 37, 42, 50, 50, 45, 41, 61, 45, 45, 58, 50, 130, 18, 57, 55, 50, 45, 70, 37.5, 29, 44, 26] 59, 62, 33, 58, 25, 63, 69, 50, 45, 45, 51, 34, 70, 62, 70, 45, 32, 32, 61, 47, 47, 37, 42, 50, 50, 45, 41, 61, 45, 45, 58, 50, 130, 18, 57, 55, 50, 45, 70, 37.5, 29, 44, 26, 26, 35]
overdrive1 = [35, 55, 25, 50, 47, 70, 72, 57, 41, 7, 10, 32, 65, 31, 61, 53, 31, 47, 38, 12, 39, 18, overdrive1 = [35, 55, 25, 50, 47, 70, 72, 57, 41, 7, 10, 32, 65, 31, 61, 53, 31, 47, 38, 12, 39, 18,
48, 65, 45, 55, 44, 25, 46, 44, 33, 45, 45, 37, 25, 27, 50, 20, 45, 63, 21, 47, 61, 47, 65, 80, 38, 30, 49, 15, 34, 45, 45, 38, 67, 120, 44, 33, 55, 50, 45, 57, 31, 29, 65, 26] 48, 65, 45, 55, 44, 25, 46, 44, 33, 45, 45, 37, 25, 27, 50, 20, 45, 63, 21, 47, 61, 47, 65, 80, 38, 30, 49, 15, 34, 45, 45, 38, 67, 120, 44, 33, 55, 50, 45, 57, 31, 29, 65, 26, 29, 42.5]
frag20 = [78, 80, 90, 75, 70, 79, 70, 79, 65, 40, 50, 80, 90, 82, 0, 61, 67, 92, 85, 50, 86, 52, frag20 = [78, 80, 90, 75, 70, 79, 70, 79, 65, 40, 50, 80, 90, 82, 0, 61, 67, 92, 85, 50, 86, 52,
65, 85, 67, 88, 64, 0.5, 95, 70, 95, 50, 80, 87, 71, 50, 85, 0, 80, 75, 50, 70, 70, 90, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 89.5, 50, 50, 51] 65, 85, 67, 88, 64, 0.5, 95, 70, 95, 50, 80, 87, 71, 50, 85, 0, 80, 75, 50, 70, 70, 90, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 90, 80, 50, 51, 54, 100]
prog20 = [61, 80, 70, 75, 90, 70, 90, 102, 84, 78, 105, 67, 63, 68, 0, 99, 80, 66, 46, 83, 40, 73, prog20 = [61, 80, 70, 75, 90, 70, 90, 102, 84, 78, 105, 67, 63, 68, 0, 99, 80, 66, 46, 83, 40, 73,
80, 90, 93, 50, 86, 78, 89, 98, 75, 80, 50, 64, 55, 100, 90, 110, 80, 50, 74, 90, 70, 70, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51] 80, 90, 93, 50, 86, 78, 89, 98, 75, 80, 50, 64, 55, 100, 90, 110, 80, 50, 74, 90, 70, 70, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51, 50, 53]
overdrive20 = [61, 80, 47, 75, 70, 70, 95, 79, 65, 31, 50, 59, 90, 58, 0, 78, 50, 70, 62, 49, 64, overdrive20 = [61, 80, 47, 75, 70, 70, 95, 79, 65, 31, 50, 59, 90, 58, 0, 78, 50, 70, 62, 49, 64,
46, 73, 95, 67, 84, 70, 78, 69, 70, 50, 80, 80, 63, 25, 50, 72, 55, 50, 95, 55, 70, 90, 70, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51] 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]
frag30 = [88, 90, 100, 75, 80, 89, 70, 79, 65, 40, 50, 90, 100, 92, 0, 61, 67, 92, 85, 50, 86, 62, frag30 = [88, 90, 100, 75, 80, 89, 70, 79, 65, 40, 50, 90, 100, 92, 0, 61, 67, 92, 85, 50, 86, 62,
65, 85, 67, 88, 74, 0.5, 105, 80, 95, 50, 80, 87, 71, 50, 95, 0, 80, 75, 50, 70, 80, 100, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 89.5, 50, 50, 51] 65, 85, 67, 88, 74, 0.5, 105, 80, 95, 50, 80, 87, 71, 50, 95, 0, 80, 75, 50, 70, 80, 100, 65, 80, 61, 50, 68, 60, 90, 67, 50, 60, 51, 50, 35, 85, 47, 50, 75, 80, 90, 80, 50, 51, 64, 100]
prog30 = [71, 90, 80, 75, 100, 80, 90, 102, 84, 78, 110, 77, 73, 78, 0, 99, 80, 66, 46, 93, 40, 83, prog30 = [71, 90, 80, 75, 100, 80, 90, 102, 84, 78, 110, 77, 73, 78, 0, 99, 80, 66, 46, 93, 40, 83,
80, 90, 93, 50, 96, 88, 99, 108, 75, 80, 50, 64, 55, 100, 100, 110, 80, 50, 74, 90, 80, 80, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51] 80, 90, 93, 50, 96, 88, 99, 108, 75, 80, 50, 64, 55, 100, 100, 110, 80, 50, 74, 90, 80, 80, 56, 80, 79, 55, 65, 59, 90, 50, 90, 90, 75, 210, 35, 86, 92, 80, 75, 100, 60, 50, 68, 51, 60, 53]
overdrive30 = [71, 90, 57, 75, 80, 80, 95, 79, 65, 31, 50, 69, 100, 68, 0, 78, 50, 70, 62, 59, 64, overdrive30 = [71, 90, 57, 75, 80, 80, 95, 79, 65, 31, 50, 69, 100, 68, 0, 78, 50, 70, 62, 59, 64,
56, 73, 95, 67, 84, 80, 88, 79, 80, 50, 80, 80, 63, 25, 50, 82, 55, 50, 95, 55, 70, 100, 80, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51] 56, 73, 95, 67, 84, 80, 88, 79, 80, 50, 80, 80, 63, 25, 50, 82, 55, 50, 95, 55, 70, 100, 80, 99, 80, 61, 40, 69, 62, 51, 90, 67, 60, 100, 200, 85, 50, 92, 50, 75, 80, 49.5, 50, 100, 51, 64, 65.5]
char_type = [1, 0, 0, 0, 0, 0, 0, 2, 0, 1, 2, 0, 0, 0, 2, 3, 1, 0, 0, 0, 1, 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] 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]
char_core = { char_core = {
0: [{'core_id': 'core_hollow', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}], 0: [{'core_id': 'core_hollow', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}],
@@ -58,14 +58,15 @@ class InitData:
11: [{'core_id': 'core_binary', 'amount': 25}, {'core_id': 'core_hollow', 'amount': 5}], 11: [{'core_id': 'core_binary', 'amount': 25}, {'core_id': 'core_hollow', 'amount': 5}],
12: [{'core_id': 'core_binary', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}], 12: [{'core_id': 'core_binary', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}],
19: [{'core_id': 'core_colorful', 'amount': 30}], 19: [{'core_id': 'core_colorful', 'amount': 30}],
10: [{'core_id': 'core_umbral', 'amount': 30}], # TODO: check 10: [{'core_id': 'core_umbral', 'amount': 30}],
66: [{'core_id': 'core_chunithm', 'amount': 15}]
} }
cores = ['core_hollow', 'core_desolate', 'core_chunithm', 'core_crimson', 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_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful', 'core_course_skip_purchase', 'core_umbral']
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", 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'] "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']
world_unlocks = ["scenery_chap1", "scenery_chap2", world_unlocks = ["scenery_chap1", "scenery_chap2",
"scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7"] "scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7"]

View File

@@ -664,5 +664,23 @@
], ],
"orig_price": 400, "orig_price": 400,
"price": 400 "price": 400
},
{
"name": "chunithm_append_2",
"items": [
{
"type": "pack",
"id": "chunithm_append_2",
"is_available": true
},
{
"type": "core",
"amount": 5,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 500,
"price": 500
} }
] ]

View File

@@ -1360,5 +1360,23 @@
], ],
"orig_price": 100, "orig_price": 100,
"price": 100 "price": 100
},
{
"name": "primitivelights",
"items": [
{
"type": "single",
"id": "primitivelights",
"is_available": true
},
{
"type": "core",
"amount": 1,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 100,
"price": 100
} }
] ]

View File

@@ -61,16 +61,16 @@ def memory_clean(now):
with Store.lock: with Store.lock:
clean_room_list = [] clean_room_list = []
clean_player_list = [] clean_player_list = []
for token in Store.link_play_data: for token, v in Store.link_play_data.items():
room = Store.link_play_data[token]['room'] room = v['room']
if now - room.timestamp >= Config.TIME_LIMIT: if now - room.timestamp >= Config.TIME_LIMIT:
clean_room_list.append(room.room_id) clean_room_list.append(room.room_id)
if now - room.players[Store.link_play_data[token]['player_index']].last_timestamp // 1000 >= Config.TIME_LIMIT: if now - room.players[v['player_index']].last_timestamp // 1000 >= Config.TIME_LIMIT:
clean_player_list.append(token) clean_player_list.append(token)
for room_id in Store.room_id_dict: for room_id, v in Store.room_id_dict.items():
if now - Store.room_id_dict[room_id].timestamp >= Config.TIME_LIMIT: if now - v.timestamp >= Config.TIME_LIMIT:
clean_room_list.append(room_id) clean_room_list.append(room_id)
for room_id in clean_room_list: for room_id in clean_room_list:
@@ -190,10 +190,10 @@ class TCPRouter:
if player_num == 4: if player_num == 4:
# 满人 # 满人
return '1201' return '1201'
elif player_num == 0: if player_num == 0:
# 房间不存在 # 房间不存在
return '1202' return '1202'
elif room.state != 2: if room.state != 2:
# 无法加入 # 无法加入
return '1205' return '1205'

View File

@@ -164,8 +164,7 @@ class Room:
return False return False
return True return True
else: return False
return False
def is_finish(self): def is_finish(self):
# 是否全部进入结算 # 是否全部进入结算

View File

@@ -13,8 +13,9 @@ class CommandParser:
self.room = room self.room = room
self.player_index = player_index self.player_index = player_index
self.s = CommandSender(self.room) self.s = CommandSender(self.room)
self.command: bytes = None
def get_commands(self, command): def get_commands(self, command: bytes):
self.command = command self.command = command
r = getattr(self, self.route[self.command[2]])() r = getattr(self, self.route[self.command[2]])()
@@ -47,8 +48,6 @@ class CommandParser:
self.s.random_code = self.command[16:24] self.s.random_code = self.command[16:24]
self.room.command_queue.append(self.s.command_10()) self.room.command_queue.append(self.s.command_10())
return None
def command_02(self): def command_02(self):
self.s.random_code = self.command[16:24] self.s.random_code = self.command[16:24]
song_idx = bi(self.command[24:26]) song_idx = bi(self.command[24:26])
@@ -81,8 +80,6 @@ class CommandParser:
self.room.make_finish() self.room.make_finish()
self.room.command_queue.append(self.s.command_13()) self.room.command_queue.append(self.s.command_13())
return None
def command_04(self): def command_04(self):
# 踢人 # 踢人
self.s.random_code = self.command[16:24] self.s.random_code = self.command[16:24]
@@ -109,7 +106,6 @@ class CommandParser:
self.room.song_idx = 0xffff self.room.song_idx = 0xffff
self.room.command_queue.append(self.s.command_13()) self.room.command_queue.append(self.s.command_13())
return None
def command_07(self): def command_07(self):
self.s.random_code = self.command[16:24] self.s.random_code = self.command[16:24]
@@ -117,15 +113,12 @@ class CommandParser:
self.room.update_song_unlock() self.room.update_song_unlock()
self.room.command_queue.append(self.s.command_14()) self.room.command_queue.append(self.s.command_14())
return None
def command_08(self): def command_08(self):
self.room.round_switch = bi(self.command[24:25]) self.room.round_switch = bi(self.command[24:25])
self.s.random_code = self.command[16:24] self.s.random_code = self.command[16:24]
self.room.command_queue.append(self.s.command_13()) self.room.command_queue.append(self.s.command_13())
return None
def command_09(self): def command_09(self):
re = [] re = []
self.s.random_code = self.command[16:24] self.s.random_code = self.command[16:24]
@@ -196,7 +189,7 @@ class CommandParser:
# 将换房主时间提前到此刻 # 将换房主时间提前到此刻
self.room.make_round() self.room.make_round()
if self.room.state == 4 or self.room.state == 5 or self.room.state == 6: if self.room.state in (4, 5, 6):
timestamp = round(time.time() * 1000) timestamp = round(time.time() * 1000)
self.room.countdown -= timestamp - self.room.timestamp self.room.countdown -= timestamp - self.room.timestamp
self.room.timestamp = timestamp self.room.timestamp = timestamp
@@ -225,10 +218,9 @@ class CommandParser:
self.room.countdown = 0xffffffff self.room.countdown = 0xffffffff
flag_13 = True flag_13 = True
if self.room.countdown <= 0: self.room.countdown = self.room.countdown if self.room.countdown > 0 else 0
self.room.countdown = 0
if self.room.state == 7 or self.room.state == 8: if self.room.state in (7, 8):
if player.timer < bi(self.command[28:32]) or bi(self.command[28:32]) == 0 and player.timer != 0: if player.timer < bi(self.command[28:32]) or bi(self.command[28:32]) == 0 and player.timer != 0:
player.last_timer = player.timer player.last_timer = player.timer
player.last_score = player.score player.last_score = player.score
@@ -270,13 +262,12 @@ class CommandParser:
self.room.command_queue.append(self.s.command_12(self.player_index)) self.room.command_queue.append(self.s.command_12(self.player_index))
if self.room.state == 3 or self.room.state == 2: if self.room.state in (2, 3):
self.room.state = 1 self.room.state = 1
self.room.song_idx = 0xffff self.room.song_idx = 0xffff
# self.room.command_queue.append(self.s.command_11()) # self.room.command_queue.append(self.s.command_11())
self.room.command_queue.append(self.s.command_13()) self.room.command_queue.append(self.s.command_13())
self.room.command_queue.append(self.s.command_14()) self.room.command_queue.append(self.s.command_14())
return None
def command_0b(self): def command_0b(self):
# 推荐歌曲 # 推荐歌曲
@@ -285,5 +276,3 @@ class CommandParser:
if self.player_index != i and self.room.players[i].online == 1: if self.player_index != i and self.room.players[i].online == 1:
self.room.players[i].extra_command_queue.append( self.room.players[i].extra_command_queue.append(
self.s.command_0f(self.player_index, song_idx)) self.s.command_0f(self.player_index, song_idx))
return None

View File

@@ -1,6 +1,7 @@
from core.config_manager import Config
from flask import Blueprint from flask import Blueprint
from core.config_manager import Config
from . import (auth, course, friend, multiplayer, others, present, purchase, from . import (auth, course, friend, multiplayer, others, present, purchase,
score, user, world) score, user, world)

View File

@@ -1,10 +1,11 @@
import base64 import base64
from functools import wraps from functools import wraps
from flask import Blueprint, current_app, g, jsonify, request
from core.error import ArcError, NoAccess from core.error import ArcError, NoAccess
from core.sql import Connect from core.sql import Connect
from core.user import UserAuth, UserLogin from core.user import UserAuth, UserLogin
from flask import Blueprint, g, jsonify, request, current_app
from .func import arc_try, error_return, header_check from .func import arc_try, error_return, header_check
@@ -35,15 +36,21 @@ def login():
return jsonify({"success": True, "token_type": "Bearer", 'user_id': user.user_id, 'access_token': user.token}) return jsonify({"success": True, "token_type": "Bearer", 'user_id': user.user_id, 'access_token': user.token})
def auth_required(request): @bp.route('/verify', methods=['POST']) # 邮箱验证进度查询
@arc_try
def email_verify():
raise ArcError('Email verification unavailable.', 151, status=404)
def auth_required(req):
# arcaea登录验证写成了修饰器 # arcaea登录验证写成了修饰器
def decorator(view): def decorator(view):
@wraps(view) @wraps(view)
def wrapped_view(*args, **kwargs): def wrapped_view(*args, **kwargs):
headers = request.headers headers = req.headers
e = header_check(request) e = header_check(req)
if e is not None: if e is not None:
current_app.logger.warning( current_app.logger.warning(
f' - {e.error_code}|{e.api_error_code}: {e}') f' - {e.error_code}|{e.api_error_code}: {e}')

View File

@@ -1,9 +1,10 @@
from flask import Blueprint, request
from core.constant import Constant from core.constant import Constant
from core.course import UserCourseList from core.course import UserCourseList
from core.item import ItemCore from core.item import ItemCore
from core.sql import Connect from core.sql import Connect
from core.user import UserOnline from core.user import UserOnline
from flask import Blueprint, request
from .auth import auth_required from .auth import auth_required
from .func import arc_try, success_return from .func import arc_try, success_return

View File

@@ -1,6 +1,7 @@
from flask import Blueprint, request
from core.sql import Connect from core.sql import Connect
from core.user import UserOnline, code_get_id from core.user import UserOnline, code_get_id
from flask import Blueprint, request
from .auth import auth_required from .auth import auth_required
from .func import arc_try, success_return from .func import arc_try, success_return

View File

@@ -40,6 +40,8 @@ def error_return(e: ArcError = default_error): # 错误返回
# 124 你今天不能再使用这个IP地址创建新的账号 # 124 你今天不能再使用这个IP地址创建新的账号
# 150 非常抱歉您已被限制使用此功能 # 150 非常抱歉您已被限制使用此功能
# 151 目前无法使用此功能 # 151 目前无法使用此功能
# 160 账户未邮箱认证,请检查邮箱
# 161 账户认证过期,请重新注册
# 401 用户不存在 # 401 用户不存在
# 403 无法连接至服务器 # 403 无法连接至服务器
# 501 502 -6 此物品目前无法获取 # 501 502 -6 此物品目前无法获取
@@ -51,6 +53,7 @@ def error_return(e: ArcError = default_error): # 错误返回
# 604 你不能加自己为好友 # 604 你不能加自己为好友
# 903 下载量超过了限制请24小时后重试 # 903 下载量超过了限制请24小时后重试
# 905 请在再次使用此功能前等待24小时 # 905 请在再次使用此功能前等待24小时
# 910 重新请求验证邮件前需等待x分钟 extra: retry_at
# 1001 设备数量达到上限 # 1001 设备数量达到上限
# 1002 此设备已使用过此功能 # 1002 此设备已使用过此功能
# 1201 房间已满 # 1201 房间已满
@@ -86,8 +89,7 @@ def arc_try(view):
data = view(*args, **kwargs) data = view(*args, **kwargs)
if data is None: if data is None:
return error_return() return error_return()
else: return data
return data
except ArcError as e: except ArcError as e:
if Config.ALLOW_WARNING_LOG: if Config.ALLOW_WARNING_LOG:
current_app.logger.warning(format_exc()) current_app.logger.warning(format_exc())

View File

@@ -1,8 +1,9 @@
from flask import Blueprint, request
from core.config_manager import Config from core.config_manager import Config
from core.error import ArcError from core.error import ArcError
from core.linkplay import Player, RemoteMultiPlayer, Room from core.linkplay import Player, RemoteMultiPlayer, Room
from core.sql import Connect from core.sql import Connect
from flask import Blueprint, request
from .auth import auth_required from .auth import auth_required
from .func import arc_try, success_return from .func import arc_try, success_return

View File

@@ -1,13 +1,14 @@
import json import json
from urllib.parse import parse_qs, urlparse from urllib.parse import parse_qs, urlparse
from flask import Blueprint, jsonify, request
from werkzeug.datastructures import ImmutableMultiDict
from core.download import DownloadList from core.download import DownloadList
from core.error import RateLimit from core.error import RateLimit
from core.sql import Connect from core.sql import Connect
from core.system import GameInfo from core.system import GameInfo
from core.user import UserOnline from core.user import UserOnline
from flask import Blueprint, jsonify, request
from werkzeug.datastructures import ImmutableMultiDict
from .auth import auth_required from .auth import auth_required
from .func import arc_try, error_return, success_return from .func import arc_try, error_return, success_return

View File

@@ -1,7 +1,8 @@
from flask import Blueprint, request
from core.present import UserPresent, UserPresentList from core.present import UserPresent, UserPresentList
from core.sql import Connect from core.sql import Connect
from core.user import UserOnline from core.user import UserOnline
from flask import Blueprint, request
from .auth import auth_required from .auth import auth_required
from .func import arc_try, success_return from .func import arc_try, success_return

View File

@@ -1,12 +1,13 @@
from time import time from time import time
from flask import Blueprint, request
from core.error import InputError, ItemUnavailable, PostError from core.error import InputError, ItemUnavailable, PostError
from core.item import ItemFactory, Stamina6 from core.item import ItemFactory, Stamina6
from core.purchase import Purchase, PurchaseList from core.purchase import Purchase, PurchaseList
from core.redeem import UserRedeem from core.redeem import UserRedeem
from core.sql import Connect from core.sql import Connect
from core.user import UserOnline from core.user import UserOnline
from flask import Blueprint, request
from .auth import auth_required from .auth import auth_required
from .func import arc_try, success_return from .func import arc_try, success_return

View File

@@ -1,12 +1,13 @@
from time import time from time import time
from core.course import CoursePlay
from flask import Blueprint, request
from core.course import CoursePlay
from core.error import InputError from core.error import InputError
from core.rank import RankList from core.rank import RankList
from core.score import UserPlay from core.score import UserPlay
from core.sql import Connect from core.sql import Connect
from core.user import UserOnline from core.user import UserOnline
from flask import Blueprint, request
from .auth import auth_required from .auth import auth_required
from .func import arc_try, success_return from .func import arc_try, success_return
@@ -94,6 +95,8 @@ def song_score_post(user_id):
request.form['miss_count'], request.form['health'], request.form['modifier'], int(time() * 1000), request.form['clear_type']) request.form['miss_count'], request.form['health'], request.form['modifier'], int(time() * 1000), request.form['clear_type'])
x.beyond_gauge = int(request.form['beyond_gauge']) x.beyond_gauge = int(request.form['beyond_gauge'])
x.submission_hash = request.form['submission_hash'] x.submission_hash = request.form['submission_hash']
if 'combo_interval_bonus' in request.form:
x.combo_interval_bonus = int(request.form['combo_interval_bonus'])
if not x.is_valid: if not x.is_valid:
raise InputError('Invalid score.', 107) raise InputError('Invalid score.', 107)
x.upload_score() x.upload_score()

View File

@@ -1,11 +1,11 @@
from flask import Blueprint, request
from core.character import UserCharacter from core.character import UserCharacter
from core.config_manager import Config from core.error import ArcError
from core.error import ArcError, NoAccess
from core.item import ItemCore from core.item import ItemCore
from core.save import SaveData from core.save import SaveData
from core.sql import Connect from core.sql import Connect
from core.user import User, UserLogin, UserOnline, UserRegister from core.user import User, UserLogin, UserOnline, UserRegister
from flask import Blueprint, request
from .auth import auth_required from .auth import auth_required
from .func import arc_try, header_check, success_return from .func import arc_try, header_check, success_return
@@ -16,7 +16,6 @@ bp = Blueprint('user', __name__, url_prefix='/user')
@bp.route('', methods=['POST']) # 注册接口 @bp.route('', methods=['POST']) # 注册接口
@arc_try @arc_try
def register(): def register():
headers = request.headers
error = header_check(request) error = header_check(request)
if error is not None: if error is not None:
raise error raise error
@@ -155,7 +154,7 @@ def sys_set(user_id, set_arg):
user.change_favorite_character(int(value)) user.change_favorite_character(int(value))
else: else:
value = 'true' == value value = 'true' == value
if 'is_hide_rating' == set_arg or 'max_stamina_notification_enabled' == set_arg: if set_arg in ('is_hide_rating', 'max_stamina_notification_enabled'):
user.update_user_one_column(set_arg, value) user.update_user_one_column(set_arg, value)
return success_return(user.to_dict()) return success_return(user.to_dict())
@@ -165,3 +164,9 @@ def sys_set(user_id, set_arg):
@arc_try @arc_try
def user_delete(user_id): def user_delete(user_id):
raise ArcError('Cannot delete the account.', 151, status=404) raise ArcError('Cannot delete the account.', 151, status=404)
@bp.route('/email/resend_verify', methods=['POST']) # 邮箱验证重发
@arc_try
def email_resend_verify():
raise ArcError('Email verification unavailable.', 151, status=404)

View File

@@ -1,7 +1,8 @@
from flask import Blueprint, request
from core.sql import Connect from core.sql import Connect
from core.user import UserOnline from core.user import UserOnline
from core.world import UserMap, get_world_all from core.world import UserMap, get_world_all
from flask import Blueprint, request
from .auth import auth_required from .auth import auth_required
from .func import arc_try, success_return from .func import arc_try, success_return

View File

@@ -422,7 +422,7 @@ def all_character():
def change_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', 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'] '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']
return render_template('web/changechar.html', skill_ids=skill_ids) return render_template('web/changechar.html', skill_ids=skill_ids)