Merge pull request #114 from Lost-MSth/dev

Dev
This commit is contained in:
Lost-MSth
2023-05-25 22:44:22 +08:00
committed by GitHub
20 changed files with 478 additions and 72 deletions

View File

@@ -17,42 +17,53 @@ This procedure is mainly used for study and research, and shall not be used for
## 特性 Features
有以下 We have
:x: : 不支持 Not supported
:warning: : 可能存在问题 / 可能与官方不一样 Possible issues / may differ from official
:wastebasket: : 不再更新,可能会移除或重构 No longer updated, may be removed or refactored
:construction: : 建设中 In construction
- 登录、注册 Login and registration
- 多设备登录 Multi device login
- 多设备自动封号 Auto-ban of multiple devices
- :warning: 多设备登录 Multi device login
- 登录频次限制 Login rate limit
- :x: 销号 Destroy account
- 成绩上传 Score upload
- PTT
- 世界排名 Global rank
- 排名 Rank
- 成绩校验 Score check
- 成绩排名 Score rank
- 潜力值机制 Potential
- Best 30
- :warning: Recent Top 10
- :warning: 世界排名 Global rank
- 段位系统 Course system
- Link Play
- :warning: Link Play
- 好友系统 Friends
- :x: 好友位提升 Max friend number increase
- 云端存档 Cloud save
- 爬梯 Climbing steps
- 自定义世界模式 Customizable World Mode
- 自定义歌曲下载 Customizable songs download
- 单曲和曲包购买(没啥用) Single songs and song packs purchase(useless)
- 尝试全剧情、曲目解锁 Try to unlock all the stories and songs
- 世界模式 World mode
- 体力系统 Stamina system
- :warning: 普通梯子强化和绳子强化 Normal steps boost & beyond boost
- :warning: 角色技能 Character skills
- 歌曲下载 Songs downloading
- :x: 加密下载 Encrypted downloading
- 下载校验 Download check
- 下载频次限制 Download rate limit
- 购买系统 Purchase system
- 单曲和曲包 Single & Pack
- :x: 捆绑包 Bundle
- 折扣 Discount
- 五周年兑换券 5-th anniversary ticket
- :x: Extend 包自动降价 Extend pack automatic price reduction
- 奖励系统 Present system
- 兑换码系统 Redeem code system
- 角色系统 Character system
- 全剧情解锁 Unlock all the stories
- 后台查询 Background search
- 后台自定义信息 Customize some things in the background
- 成绩校验 Score check
- 下载校验 Download check
- 数据记录 Data recording
- 用户成绩 Users' scores
- 用户每日潜力值 Users' daily potential
- :wastebasket: 简单的网页管理后台 Simple web admin backend
- :construction: API
- 服务器日志 Server log
没有以下 We don't have
- 服务器安全性保证 Server security assurance
可能有问题 There may be problems
- Recent 30
- 一些歌曲的解锁 Some songs' unlocking
- 同设备多共存登录 Multiple app logins on the same device
## 说明 Statement
只是很有趣,用处探索中。
@@ -69,22 +80,28 @@ It is just so interesting. What it can do is under exploration.
只保留最新版本 Only keep the latest version.
> 提醒:更新时请注意保留原先的数据库,以防数据丢失。
>
> 提醒:更新时请注意保留原先的数据库,以防数据丢失。
> Tips: When updating, please keep the original database in case of data loss.
>
> 其它小改动请参考各个 commit 信息
> Please refer to the commit messages for other minor changes.
### Version 2.11.1
### Version 2.11.2
- 适用于Arcaea 4.4.0版本 For Arcaea 4.4.0
- 新搭档 **密特拉·泰尔塞拉**、**不来方斗亚** 已解锁 Unlock the character **Mithra Tercera** and **Toa Kozukata**.
- **密特拉·泰尔塞拉** 的技能提供支持 Add support for the skill of **Mithra Tercera**.
- 新增修改搭档的API接口 Add some API endpoints about characters.
- 适用于 Arcaea 4.4.6 版本 For Arcaea 4.4.6
- 新搭档 **奈美(暮光)** 已解锁 Unlock the character **Nami (Twilight)**.
- 新增用户潜力值每日记录功能 Add support for recording users' potential each day.
- 修复搭档 **光 & 对立Reunion** 无法觉醒的问题 Fix a bug that the character **Hikari & Tairitsu (Reunion)** cannot be uncapped. (#100)
- 添加 `finale/finale_end` 接口尝试修复最终挑战无法解锁结局的问题 Add the `finale/finale_end` endpoint to try to fix the problem that the endings cannot be unlocked correctly in the finale challenge. (#110)
- 新增获取用户潜力值记录的 API 接口 Add an API endpoint for getting the user's rating records.
## 运行环境与依赖 Running environment and requirements
- Windows/Linux/Mac OS/Android...
- Windows / Linux / Mac OS / Android...
- Python >= 3.6
- Flask module >= 2.0, Cryptography module >= 3.0.0, limits >= 2.7.0
- Flask >= 2.0
- Cryptography >= 3.0.0
- limits >= 2.7.0
- Charles, IDA, proxy app... (optional)
<!--
@@ -105,7 +122,7 @@ It is just so interesting. What it can do is under exploration.
## Q&A
[中文/English](https://github.com/Lost-MSth/Arcaea-server/wiki/Q&A)
[中文 / English](https://github.com/Lost-MSth/Arcaea-server/wiki/Q&A)
## 鸣谢 Thanks

View File

@@ -1,6 +1,6 @@
from base64 import b64decode
from flask import Blueprint, request
from flask import Blueprint, current_app, request
from core.api_user import APIUser
from core.error import PostError
@@ -32,6 +32,7 @@ def token_post(data):
with Connect() as c:
user = APIUser(c)
user.login(name, password, request.remote_addr)
current_app.logger.info(f'API user `{user.user_id}` log in')
return success_return({'token': user.api_token, 'user_id': user.user_id})

View File

@@ -1,10 +1,12 @@
from flask import Blueprint, request
from core.api_user import APIUser
from core.config_manager import Config
from core.error import InputError, NoAccess, NoData
from core.score import Potential, UserScoreList
from core.sql import Connect, Query, Sql
from core.user import UserChanger, UserInfo, UserRegister
from core.util import get_today_timestamp
from .api_auth import api_try, request_json_handle, role_required
from .api_code import error_return, success_return
@@ -191,3 +193,32 @@ def users_user_role_get(user, user_id):
x = APIUser(c, user_id)
x.select_role_and_powers()
return success_return({'user_id': x.user_id, 'role': x.role.role_id, 'powers': [i.power_id for i in x.role.powers]})
@bp.route('/<int:user_id>/rating', methods=['GET'])
@role_required(request, ['select', 'select_me'])
@request_json_handle(request, optional_keys=['start_timestamp', 'end_timestamp', 'duration'])
@api_try
def users_user_rating_get(data, user, user_id):
'''查询用户历史rating`duration`是相对于今天的天数'''
# 查别人需要select权限
if user_id != user.user_id and not user.role.has_power('select'):
return error_return(NoAccess('No permission', api_error_code=-1), 403)
start_timestamp = data.get('start_timestamp', None)
end_timestamp = data.get('end_timestamp', None)
duration = data.get('duration', None)
sql = '''select time, rating_ptt from user_rating where user_id = ?'''
sql_data = [user_id]
if start_timestamp is not None and end_timestamp is not None:
sql += ''' and time between ? and ?'''
sql_data += [start_timestamp, end_timestamp]
elif duration is not None:
sql += ''' and time between ? and ?'''
t = get_today_timestamp()
sql_data += [t - duration * 24 * 3600, t]
with Connect(Config.SQLITE_LOG_DATABASE_PATH) as c:
c.execute(sql, sql_data)
r = c.fetchall()
return success_return({'user_id': user_id, 'data': [{'time': i[0], 'rating_ptt': i[1]} for i in r]})

View File

@@ -127,7 +127,8 @@ class APIUser(UserOnline):
if ip is not None:
self.ip = ip
if not self.limiter.hit(name):
raise RateLimit('Too many login attempts', api_error_code=-205)
raise RateLimit(
f'Too many login attempts of username {name}', api_error_code=-205)
self.c.execute('''select user_id, password from user where name = :a''', {
'a': self.name})
@@ -136,9 +137,9 @@ class APIUser(UserOnline):
raise NoData(
f'The user `{self.name}` does not exist.', api_error_code=-201, status=401)
if x[1] == '':
raise UserBan(f'The user `{self.name}` is banned.')
raise UserBan(f'The user `{x[0]}` is banned.')
if self.hash_pwd != x[1]:
raise NoAccess('The password is incorrect.',
raise NoAccess(f'The password of user `{x[0]}` is incorrect.',
api_error_code=-201, status=401)
self.user_id = x[0]

View File

@@ -0,0 +1,50 @@
from atexit import register
from concurrent.futures import ThreadPoolExecutor
from .constant import Constant
from .sql import Connect
class BGTask:
executor = ThreadPoolExecutor(max_workers=1)
def __init__(self, func, *args, **kwargs):
self.future = self.executor.submit(func, *args, **kwargs)
def result(self):
return self.future.result()
def cancel(self) -> bool:
return self.future.cancel()
def done(self) -> bool:
return self.future.done()
@staticmethod
def shutdown(wait: bool = True):
BGTask.executor.shutdown(wait)
@register
def atexit():
BGTask.shutdown()
def logdb_execute_func(sql, *args, **kwargs):
with Connect(Constant.SQLITE_LOG_DATABASE_PATH) as c:
c.execute(sql, *args, **kwargs)
def logdb_execute_many_func(sql, *args, **kwargs):
with Connect(Constant.SQLITE_LOG_DATABASE_PATH) as c:
c.executemany(sql, *args, **kwargs)
def logdb_execute(sql: str, *args, **kwargs):
'''异步执行SQL日志库写入注意不会直接返回结果'''
return BGTask(logdb_execute_func, sql, *args, **kwargs)
def logdb_execute_many(sql: str, *args, **kwargs):
'''异步批量执行SQL日志库写入注意不会直接返回结果'''
return BGTask(logdb_execute_many_func, sql, *args, **kwargs)

View File

@@ -349,7 +349,7 @@ class UserCharacter(Character):
self.c.execute(
'''select amount from user_item where user_id=? and item_id=? and type="core"''', (self.user.user_id, i.item_id))
y = self.c.fetchone()
if not y or i.amount > y[0]:
if i.amount > 0 and (not y or i.amount > y[0]):
raise ItemNotEnough('The cores are not enough.')
for i in self.uncap_cores:

View File

@@ -1,6 +1,7 @@
from .config_manager import Config
ARCAEA_SERVER_VERSION = 'v2.11.1'
ARCAEA_SERVER_VERSION = 'v2.11.2'
ARCAEA_LOG_DATBASE_VERSION = 'v1.1'
class Constant:
@@ -101,4 +102,6 @@ class Constant:
DATABASE_MIGRATE_TABLES = ['user', 'friend', 'best_score', 'recent30', 'user_world', 'item', 'user_item', 'purchase', 'purchase_item', 'user_save',
'login', 'present', 'user_present', 'present_item', 'redeem', 'user_redeem', 'redeem_item', 'api_login', 'chart', 'user_course', 'user_char', 'user_role']
LOG_DATABASE_MIGRATE_TABLES = ['cache', 'user_score', 'user_rating']
UPDATE_WITH_NEW_CHARACTER_DATA = Config.UPDATE_WITH_NEW_CHARACTER_DATA

View File

@@ -7,11 +7,12 @@ from time import time
from traceback import format_exc
from core.config_manager import Config
from core.constant import ARCAEA_SERVER_VERSION
from core.constant import ARCAEA_LOG_DATBASE_VERSION, ARCAEA_SERVER_VERSION
from core.course import Course
from core.download import DownloadList
from core.purchase import Purchase
from core.sql import Connect, DatabaseMigrator, MemoryDatabase
from core.sql import (Connect, DatabaseMigrator, LogDatabaseMigrator,
MemoryDatabase)
from core.user import UserRegister
from core.util import try_rename
@@ -208,6 +209,29 @@ class FileChecker:
self.logger.error(
f'Failed to new the file {Config.SQLITE_LOG_DATABASE_PATH}')
return False
else:
# 检查更新
with Connect(Config.SQLITE_LOG_DATABASE_PATH) as c:
try:
x = c.execute(
'''select value from cache where key="version"''').fetchone()
except:
x = None
if not x or x[0] != ARCAEA_LOG_DATBASE_VERSION:
self.logger.warning(
f'Maybe the file `{Config.SQLITE_LOG_DATABASE_PATH}` is an old version.')
try:
self.logger.info(
f'Try to update the file `{Config.SQLITE_LOG_DATABASE_PATH}`')
self.update_log_database()
self.logger.info(
f'Success to update the file `{Config.SQLITE_LOG_DATABASE_PATH}`')
except Exception as e:
self.logger.error(format_exc())
self.logger.error(
f'Failed to update the file `{Config.SQLITE_LOG_DATABASE_PATH}`')
return False
if not self.check_file(Config.SQLITE_DATABASE_PATH):
# 新建数据库
try:
@@ -275,6 +299,12 @@ class FileChecker:
DatabaseMigrator(old_path, new_path).update_database()
os.remove(old_path)
@staticmethod
def update_log_database(old_path: str = Config.SQLITE_LOG_DATABASE_PATH) -> None:
'''直接更新日志数据库'''
if os.path.isfile(old_path):
LogDatabaseMigrator(old_path).update_database()
def check_song_file(self) -> bool:
'''检查song有关文件并初始化缓存'''
f = self.check_folder(Config.SONG_FILE_FOLDER_PATH)

View File

@@ -2,13 +2,15 @@ from base64 import b64encode
from os import urandom
from time import time
from .bgtask import BGTask, logdb_execute
from .config_manager import Config
from .constant import Constant
from .course import CoursePlay
from .error import NoData, StaminaNotEnough
from .item import ItemCore
from .song import Chart
from .sql import Connect, Query, Sql
from .util import md5
from .util import get_today_timestamp, md5
from .world import WorldPlay
@@ -427,9 +429,20 @@ class UserPlay(UserScore):
def record_score(self) -> None:
'''向log数据库记录分数请注意列名不同'''
with Connect(Constant.SQLITE_LOG_DATABASE_PATH) as c2:
c2.execute('''insert into user_score values(?,?,?,?,?,?,?,?,?,?,?,?,?)''', (self.user.user_id, self.song.song_id, self.song.difficulty, self.time_played,
self.score, self.shiny_perfect_count, self.perfect_count, self.near_count, self.miss_count, self.health, self.modifier, self.clear_type, self.rating))
logdb_execute('''insert into user_score values(?,?,?,?,?,?,?,?,?,?,?,?,?)''', (self.user.user_id, self.song.song_id, self.song.difficulty, self.time_played,
self.score, self.shiny_perfect_count, self.perfect_count, self.near_count, self.miss_count, self.health, self.modifier, self.clear_type, self.rating))
def record_rating_ptt(self, user_rating_ptt: float) -> None:
'''向log数据库记录用户ptt变化'''
today_timestamp = get_today_timestamp()
with Connect(Config.SQLITE_LOG_DATABASE_PATH) as c2:
old_ptt = c2.execute('''select rating_ptt from user_rating where user_id=? and time=?''', (
self.user.user_id, today_timestamp)).fetchone()
old_ptt = 0 if old_ptt is None else old_ptt[0]
if old_ptt != user_rating_ptt:
c2.execute('''insert or replace into user_rating values(?,?,?)''',
(self.user.user_id, today_timestamp, user_rating_ptt))
def upload_score(self) -> None:
'''上传分数包括user的recent更新best更新recent30更新世界模式计算'''
@@ -474,7 +487,9 @@ class UserPlay(UserScore):
self.update_recent30()
# 总PTT更新
self.user.rating_ptt = int(self.ptt.value * 100)
user_rating_ptt = self.ptt.value
self.user.rating_ptt = int(user_rating_ptt * 100)
BGTask(self.record_rating_ptt, user_rating_ptt) # 记录总PTT变换
self.c.execute('''update user set rating_ptt = :a where user_id = :b''', {
'a': self.user.rating_ptt, 'b': self.user.user_id})

View File

@@ -1,8 +1,10 @@
import os
import sqlite3
import traceback
from atexit import register
from .constant import Constant
from .config_manager import Config
from .constant import ARCAEA_LOG_DATBASE_VERSION, Constant
from .error import ArcError, InputError
@@ -404,6 +406,31 @@ class DatabaseMigrator:
self.update_user_char_full(c2) # 更新user_char_full
class LogDatabaseMigrator:
def __init__(self, c1_path: str = Config.SQLITE_LOG_DATABASE_PATH) -> None:
self.c1_path = c1_path
# self.c2_path = c2_path
self.init_folder_path = Config.DATABASE_INIT_PATH
self.c = None
@property
def sql_path(self) -> str:
return os.path.join(self.init_folder_path, 'log_tables.sql')
def table_update(self) -> None:
'''直接更新数据库结构'''
with open(self.sql_path, 'r') as f:
self.c.executescript(f.read())
self.c.execute(
'''insert or replace into cache values("version", :a, -1);''', {'a': ARCAEA_LOG_DATBASE_VERSION})
def update_database(self) -> None:
with Connect(self.c1_path) as c:
self.c = c
self.table_update()
class MemoryDatabase:
conn = sqlite3.connect('file:arc_tmp?mode=memory&cache=shared', uri=True)

View File

@@ -237,7 +237,8 @@ class UserLogin(User):
self.set_ip(ip)
if not self.limiter.hit(name):
raise RateLimit('Too many login attempts.', 123, -203)
raise RateLimit(
f'Too many login attempts of username `{name}`', 123, -203)
self.c.execute('''select user_id, password, ban_flag from user where name = :name''', {
'name': self.name})
@@ -251,7 +252,7 @@ class UserLogin(User):
# 自动封号检查
ban_timestamp = int(x[2].split(':', 1)[1])
if ban_timestamp > self.now:
raise UserBan('Too many devices logging in during 24 hours.', 105, extra_data={
raise UserBan(f'Too many devices user `{self.user_id}` logging in during 24 hours.', 105, extra_data={
'remaining_ts': ban_timestamp-self.now})
if x[1] == '':
@@ -260,7 +261,7 @@ class UserLogin(User):
f'The account `{self.user_id}` has been banned.', 106)
if x[1] != self.hash_pwd:
raise NoAccess('Wrong password.', 104)
raise NoAccess(f'Wrong password of user `{self.user_id}`', 104)
self.token = base64.b64encode(hashlib.sha256(
(str(self.user_id) + str(self.now)).encode("utf8") + urandom(8)).digest()).decode()
@@ -763,7 +764,7 @@ class UserChanger(UserInfo, UserRegister):
if columns is not None:
d = {}
for column in columns:
if column == 'password':
if column == 'password' and self.password != '':
d[column] = self.hash_pwd
else:
d[column] = self.__dict__[column]

View File

@@ -1,5 +1,7 @@
import hashlib
import os
from datetime import date
from time import mktime
def md5(code: str) -> str:
@@ -37,3 +39,8 @@ def try_rename(path: str, new_path: str) -> str:
os.rename(path, final_path)
return final_path
def get_today_timestamp():
'''相对于本机本地时间的今天0点的时间戳'''
return int(mktime(date.today().timetuple()))

View File

@@ -1,45 +1,45 @@
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',
'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']
'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', 'Nami(Twilight)']
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', 'skill_mithra', 'skill_toa']
'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', '', '', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', '', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', '', '', 'visual_ghost_skynotes', 'skill_vita', 'skill_fatalis', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', '', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'skill_mithra', 'skill_toa', 'skill_nami_twilight']
skill_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,
0, 0, 0, 8, 0, 14, 0, 0, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
0, 0, 0, 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]
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, 56, 47, 33, 26, 29, 66]
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, 40]
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, 26, 35]
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, 40]
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, 29, 42.5]
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, 40]
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, 90, 80, 50, 51, 54, 100]
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, 50]
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, 50, 53]
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, 85]
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, 54, 65.5]
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, 60]
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, 90, 80, 50, 51, 64, 100]
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, 50]
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, 60, 53]
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, 85]
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, 64, 65.5]
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, 60]
char_type = [1, 0, 0, 0, 0, 0, 0, 2, 0, 1, 2, 0, 0, 0, 2, 3, 1, 0, 0, 0, 1,
0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 3, 0, 2, 2, 0, 0, 2, 0, 0, 2, 0, 2, 2]
0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 3, 0, 2, 2, 0, 0, 2, 0, 0, 2, 0, 2, 2, 1]
char_core = {
0: [{'core_id': 'core_hollow', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}],
@@ -59,14 +59,15 @@ class InitData:
12: [{'core_id': 'core_binary', 'amount': 25}, {'core_id': 'core_desolate', 'amount': 5}],
19: [{'core_id': 'core_colorful', 'amount': 30}],
10: [{'core_id': 'core_umbral', 'amount': 30}],
66: [{'core_id': 'core_chunithm', 'amount': 15}]
66: [{'core_id': 'core_chunithm', 'amount': 15}],
5: [{'core_id': 'core_hollow', 'amount': 0}]
}
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']
world_songs = ["babaroque", "shadesoflight", "kanagawa", "lucifer", "anokumene", "ignotus", "rabbitintheblackroom", "qualia", "redandblue", "bookmaker", "darakunosono", "espebranch", "blacklotus", "givemeanightmare", "vividtheory", "onefr", "gekka", "vexaria3", "infinityheaven3", "fairytale3", "goodtek3", "suomi", "rugie", "faintlight", "harutopia", "goodtek", "dreaminattraction", "syro", "diode", "freefall", "grimheart", "blaster",
"cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3']
"cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris']
world_unlocks = ["scenery_chap1", "scenery_chap2",
"scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7"]

View File

@@ -17,6 +17,11 @@ clear_type int,
rating real,
primary key(user_id, song_id, difficulty, time_played)
);
create table if not exists user_rating(user_id int,
time int,
rating_ptt real,
primary key(user_id, time)
);
create index if not exists user_score_1 on user_score (song_id, difficulty);
create index if not exists user_score_2 on user_score (time_played);

View File

@@ -1378,5 +1378,95 @@
],
"orig_price": 100,
"price": 100
},
{
"name": "thesurvivor",
"items": [
{
"type": "single",
"id": "thesurvivor",
"is_available": true
},
{
"type": "core",
"amount": 1,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 100,
"price": 100
},
{
"name": "newyorkbackraise",
"items": [
{
"type": "single",
"id": "newyorkbackraise",
"is_available": true
},
{
"type": "core",
"amount": 1,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 100,
"price": 100
},
{
"name": "lostintheabyss",
"items": [
{
"type": "single",
"id": "lostintheabyss",
"is_available": true
},
{
"type": "core",
"amount": 1,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 100,
"price": 100
},
{
"name": "galacticlove",
"items": [
{
"type": "single",
"id": "galacticlove",
"is_available": true
},
{
"type": "core",
"amount": 1,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 100,
"price": 100
},
{
"name": "tothemilkyway",
"items": [
{
"type": "single",
"id": "tothemilkyway",
"is_available": true
},
{
"type": "core",
"amount": 1,
"id": "core_generic",
"is_available": true
}
],
"orig_price": 100,
"price": 100
}
]

View File

@@ -32,7 +32,7 @@ def login():
user = UserLogin(c)
user.login(name, password, device_id, request.remote_addr)
current_app.logger.info(f'User `{user.user_id}` log in')
return jsonify({"success": True, "token_type": "Bearer", 'user_id': user.user_id, 'access_token': user.token})

View File

@@ -54,6 +54,11 @@ def finale_start():
return success_return({})
@bp.route('/finale/finale_end', methods=['POST'])
def finale_end():
return success_return({})
map_dict = {'/user/me': user_me,
'/purchase/bundle/pack': bundle_pack,
'/serve/download/me/song': download_song,

View File

@@ -1,4 +1,4 @@
from flask import Blueprint, request
from flask import Blueprint, current_app, request
from core.character import UserCharacter
from core.error import ArcError
@@ -36,6 +36,7 @@ def register():
user = UserLogin(c)
user.login(new_user.name, new_user.password,
device_id, request.remote_addr)
current_app.logger.info(f'New user `{user.user_id}` registered')
return success_return({'user_id': user.user_id, 'access_token': user.token})

View File

@@ -422,7 +422,7 @@ def all_character():
def change_character():
# 修改角色数据
skill_ids = ['No_skill', 'gauge_easy', 'note_mirror', 'gauge_hard', 'frag_plus_10_pack_stellights', 'gauge_easy|frag_plus_15_pst&prs', 'gauge_hard|fail_frag_minus_100', 'frag_plus_5_side_light', 'visual_hide_hp', 'frag_plus_5_side_conflict', 'challenge_fullcombo_0gauge', 'gauge_overflow', 'gauge_easy|note_mirror', 'note_mirror', 'visual_tomato_pack_tonesphere',
'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', 'frags_kou', 'visual_ink', 'shirabe_entry_fee', 'frags_yume', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', 'eto_uncap', 'luna_uncap', 'frags_preferred_song', 'visual_ghost_skynotes', 'ayu_uncap', 'skill_vita', 'skill_fatalis', 'skill_reunion', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'ilith_awakened_skill', 'skill_mithra', 'skill_toa']
'frag_rng_ayu', 'gaugestart_30|gaugegain_70', 'combo_100-frag_1', 'audio_gcemptyhit_pack_groovecoaster', 'gauge_saya', 'gauge_chuni', 'kantandeshou', 'gauge_haruna', 'frags_nono', 'gauge_pandora', 'gauge_regulus', 'omatsuri_daynight', 'sometimes(note_mirror|frag_plus_5)', 'scoreclear_aa|visual_scoregauge', 'gauge_tempest', 'gauge_hard', 'gauge_ilith_summer', 'frags_kou', 'visual_ink', 'shirabe_entry_fee', 'frags_yume', 'note_mirror|visual_hide_far', 'frags_ongeki', 'gauge_areus', 'gauge_seele', 'gauge_isabelle', 'gauge_exhaustion', 'skill_lagrange', 'gauge_safe_10', 'frags_nami', 'skill_elizabeth', 'skill_lily', 'skill_kanae_midsummer', 'eto_uncap', 'luna_uncap', 'frags_preferred_song', 'visual_ghost_skynotes', 'ayu_uncap', 'skill_vita', 'skill_fatalis', 'skill_reunion', 'frags_ongeki_slash', 'frags_ongeki_hard', 'skill_amane', 'skill_kou_winter', 'gauge_hard|note_mirror', 'skill_shama', 'skill_milk', 'skill_shikoku', 'skill_mika', 'ilith_awakened_skill', 'skill_mithra', 'skill_toa', 'skill_nami_twilight']
return render_template('web/changechar.html', skill_ids=skill_ids)

121
tools/clean_logdb.py Normal file
View File

@@ -0,0 +1,121 @@
import os
import sqlite3
from datetime import datetime
from time import mktime
LOG_DATABASE_PATH = './arcaea_log.db'
class Connect():
# 数据库连接类,上下文管理
def __init__(self, file_path=LOG_DATABASE_PATH):
"""
数据库连接
接受:文件路径
返回sqlite3连接操作对象
"""
self.file_path = file_path
def __enter__(self):
self.conn = sqlite3.connect(self.file_path)
self.c = self.conn.cursor()
return self.c
def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
if exc_type is not None:
print(f'exc_type: {exc_type}')
print(f'exc_val: {exc_val}')
print(f'exc_tb: {exc_tb}')
if self.conn:
self.conn.rollback()
if self.conn:
self.conn.commit()
self.conn.close()
return True
def clean_by_time(table_name: str, time_colume_name: str, colume_count: int):
with Connect() as c:
today = datetime.now()
print(f'The time now is {today}.')
day = input(
'Please input the number of days the data before which you want to delete: ')
try:
day = int(day)
except ValueError:
print('Invalid input!')
return
time = mktime(today.timetuple()) - day * 24 * 60 * 60
delete_count = c.execute(
f'select count(*) from {table_name} where {time_colume_name} < ?', (time,)).fetchone()[0]
all_count = c.execute(
f'select count(*) from {table_name}').fetchone()[0]
print(
f'Before {day} days, there are {delete_count} records to be deleted, {all_count} records in total.')
flag = input('Are you sure to delete these records? (y/n) ')
if flag == 'y' or flag == 'Y':
if delete_count >= 1000000:
print(
'It will cost a long time to delete these records, please wait patiently...')
print('Deleting...')
c.execute('PRAGMA cache_size = 32768')
c.execute('PRAGMA synchronous = OFF')
c.execute('PRAGMA temp_store = MEMORY')
if delete_count / all_count >= 0.8:
data = c.execute(
f'select * from {table_name} where {time_colume_name} > ?', (time,)).fetchall()
c.execute(f'delete from {table_name}')
c.executemany(
f'insert into {table_name} values ({",".join(["?"]*colume_count)})', data)
else:
c.execute(
f'delete from {table_name} where {time_colume_name} < ?', (time,))
c.execute('PRAGMA temp_store = DEFAULT')
print('Delete successfully!')
else:
print('Delete canceled!')
def vacuum():
print('This operation will release unused space in the database file.')
print('It will cost a long time to release unused space if the database file is so large.')
flag = input('Are you sure to release unused space? (y/n) ')
if flag == 'y' or flag == 'Y':
with Connect() as c:
print('Releasing unused space...')
c.execute('vacuum')
print('Release unused space successfully!')
else:
print('Release unused space canceled!')
def main():
if not os.path.exists(LOG_DATABASE_PATH):
print('The database file `arcaea_log.db` does not exist!')
print('-- Arcaea Server Log Database Cleaner --')
print('Note: It is more recommended to delete the database file directly.')
while True:
print('-' * 40)
print('1. clean `user_score` table')
print('2. clean `user_rating` table')
print('3. release unused space (`vacuum` command)')
print('0. exit')
choice = input('Please input your choice: ')
if choice == '1':
clean_by_time('user_score', 'time_played', 13)
elif choice == '2':
clean_by_time('user_rating', 'time', 3)
elif choice == '3':
vacuum()
elif choice == '0':
break
else:
print('Invalid choice!')
if __name__ == '__main__':
main()
input('Press `Enter` key to exit.')