diff --git a/.gitignore b/.gitignore index 7d6a9df..63ec578 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,9 @@ latest version/config.py # song data latest version/database/songs/ +latest version/database/bundle/ + +!latest version/database/bundle/README.md + +# backup +latest version/database/backup/ diff --git a/latest version/core/bundle.py b/latest version/core/bundle.py index c4667c9..01a230c 100644 --- a/latest version/core/bundle.py +++ b/latest version/core/bundle.py @@ -1,11 +1,13 @@ import json import os +from functools import lru_cache from time import time from flask import url_for +from .config_manager import Config from .constant import Constant -from .error import NoAccess, RateLimit +from .error import NoAccess, NoData, RateLimit from .limiter import ArcLimiter @@ -44,6 +46,8 @@ class ContentBundle: x.prev_version = json_data['previousVersionNumber'] x.app_version = json_data['applicationVersionNumber'] x.uuid = json_data['uuid'] + if x.prev_version is None: + x.prev_version = '0.0.0' return x def to_dict(self) -> dict: @@ -69,8 +73,14 @@ class BundleParser: # {app_version: [ List[ContentBundle] ]} bundles: 'dict[str, list[ContentBundle]]' = {} + # {app_version: max bundle version} max_bundle_version: 'dict[str, str]' = {} + # {bundle version: [next versions]} 宽搜索引 + next_versions: 'dict[str, list[str]]' = {} + # {(bver, b prev version): ContentBundle} 正向索引 + version_tuple_bundles: 'dict[tuple[str, str], ContentBundle]' = {} + def __init__(self) -> None: self.parse() @@ -106,15 +116,57 @@ class BundleParser: f'Bundle file not found: {bundle_path}') x.calculate_size() - if x.app_version not in self.bundles: - self.bundles[x.app_version] = [] - self.bundles[x.app_version].append(x) + self.bundles.setdefault(x.app_version, []).append(x) + + self.version_tuple_bundles[(x.version, x.prev_version)] = x + self.next_versions.setdefault( + x.prev_version, []).append(x.version) # sort by version for k, v in self.bundles.items(): v.sort(key=lambda x: x.version_tuple) self.max_bundle_version[k] = v[-1].version + @staticmethod + @lru_cache(maxsize=128) + def get_bundles(app_ver: str, b_ver: str) -> 'list[ContentBundle]': + if Config.BUNDLE_STRICT_MODE: + return BundleParser.bundles.get(app_ver, []) + + k = b_ver if b_ver else '0.0.0' + + target_version = BundleParser.max_bundle_version.get(app_ver, '0.0.0') + if k == target_version: + return [] + + # BFS + q = [[k]] + ans = None + while True: + qq = [] + for x in q: + if x[-1] == target_version: + ans = x + break + for y in BundleParser.next_versions.get(x[-1], []): + if y in x: + continue + qq.append(x + [y]) + + if ans is not None or not qq: + break + q = qq + + if not ans: + raise NoData( + f'No bundles found for app version: {app_ver}, bundle version: {b_ver}', status=404) + + r = [] + for i in range(1, len(ans)): + r.append(BundleParser.version_tuple_bundles[(ans[i], ans[i-1])]) + + return r + class BundleDownload: @@ -134,8 +186,9 @@ class BundleDownload: self.device_id = device_id def get_bundle_list(self) -> list: - bundles: 'list[ContentBundle]' = BundleParser.bundles.get( - self.client_app_version, []) + bundles: 'list[ContentBundle]' = BundleParser.get_bundles( + self.client_app_version, self.client_bundle_version) + if not bundles: return [] diff --git a/latest version/core/config_manager.py b/latest version/core/config_manager.py index ea12d00..2264f62 100644 --- a/latest version/core/config_manager.py +++ b/latest version/core/config_manager.py @@ -16,6 +16,8 @@ class Config: OLD_GAME_API_PREFIX = [] # str | list[str] ALLOW_APPVERSION = [] # list[str] + + BUNDLE_STRICT_MODE = True SET_LINKPLAY_SERVER_AS_SUB_PROCESS = True diff --git a/latest version/core/constant.py b/latest version/core/constant.py index d732102..988af3b 100644 --- a/latest version/core/constant.py +++ b/latest version/core/constant.py @@ -1,6 +1,6 @@ from .config_manager import Config -ARCAEA_SERVER_VERSION = 'v2.11.3.9' +ARCAEA_SERVER_VERSION = 'v2.11.3.10' ARCAEA_LOG_DATBASE_VERSION = 'v1.1' diff --git a/latest version/core/world.py b/latest version/core/world.py index c5414dd..d1e934e 100644 --- a/latest version/core/world.py +++ b/latest version/core/world.py @@ -86,7 +86,7 @@ class Step: self.restrict_ids = d.get('restrict_ids') self.restrict_type = d.get('restrict_type') self.restrict_difficulty = d.get('restrict_difficulty') - self.step_type = d.get('step_type') + self.step_type = d.get('step_type', []) self.speed_limit_value = d.get('speed_limit_value') self.plus_stamina_value = d.get('plus_stamina_value') if 'items' in d: diff --git a/latest version/database/init/arc_data.py b/latest version/database/init/arc_data.py index 5fe5560..54c1e34 100644 --- a/latest version/database/init/arc_data.py +++ b/latest version/database/init/arc_data.py @@ -70,7 +70,7 @@ class InitData: 'core_ambivalent', 'core_scarlet', 'core_groove', 'core_generic', 'core_binary', 'core_colorful', 'core_course_skip_purchase', 'core_umbral', 'core_wacca', 'core_sunset', 'core_tanoc'] world_songs = ["babaroque", "shadesoflight", "kanagawa", "lucifer", "anokumene", "ignotus", "rabbitintheblackroom", "qualia", "redandblue", "bookmaker", "darakunosono", "espebranch", "blacklotus", "givemeanightmare", "vividtheory", "onefr", "gekka", "vexaria3", "infinityheaven3", "fairytale3", "goodtek3", "suomi", "rugie", "faintlight", "harutopia", "goodtek", "dreaminattraction", "syro", "diode", "freefall", "grimheart", "blaster", - "cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris', 'ultimatetaste', 'rgb', 'matenrou', 'dynitikos', 'amekagura', 'fantasy', 'aloneandlorn', 'felys', 'onandon', 'hotarubinoyuki', 'oblivia3', 'libertas3', 'einherjar3', 'purpleverse3', 'viciousheroism3', 'inkarusi3', 'cyberneciacatharsis3', 'alephzero', 'hellohell', 'ichirin', 'awakeninruins', 'morningloom', 'lethalvoltage'] + "cyberneciacatharsis", "monochromeprincess", "revixy", "vector", "supernova", "nhelv", "purgatorium3", "dement3", "crossover", "guardina", "axiumcrisis", "worldvanquisher", "sheriruth", "pragmatism", "gloryroad", "etherstrike", "corpssansorganes", "lostdesire", "blrink", "essenceoftwilight", "lapis", "solitarydream", "lumia3", "purpleverse", "moonheart3", "glow", "enchantedlove", "take", "lifeispiano", "vandalism", "nexttoyou3", "lostcivilization3", "turbocharger", "bookmaker3", "laqryma3", "kyogenkigo", "hivemind", "seclusion", "quonwacca3", "bluecomet", "energysynergymatrix", "gengaozo", "lastendconductor3", "antithese3", "qualia3", "kanagawa3", "heavensdoor3", "pragmatism3", "nulctrl", "avril", "ddd", "merlin3", "omakeno3", "nekonote", "sanskia", 'altair', 'mukishitsu', 'trapcrow', 'redandblue3', 'ignotus3', 'singularity3', 'dropdead3', 'arcahv', 'freefall3', 'partyvinyl3', 'tsukinimurakumo', 'mantis', 'worldfragments', 'astrawalkthrough', 'chronicle', 'trappola3', 'letsrock', 'shadesoflight3', 'teriqma3', 'impact3', 'lostemotion', 'gimmick', 'lawlesspoint', 'hybris', 'ultimatetaste', 'rgb', 'matenrou', 'dynitikos', 'amekagura', 'fantasy', 'aloneandlorn', 'felys', 'onandon', 'hotarubinoyuki', 'oblivia3', 'libertas3', 'einherjar3', 'purpleverse3', 'viciousheroism3', 'inkarusi3', 'cyberneciacatharsis3', 'alephzero', 'hellohell', 'ichirin', 'awakeninruins', 'morningloom', 'lethalvoltage', 'leaveallbehind', 'desive'] world_unlocks = ["scenery_chap1", "scenery_chap2", "scenery_chap3", "scenery_chap4", "scenery_chap5", "scenery_chap6", "scenery_chap7", "scenery_beyond"] diff --git a/latest version/database/init/singles.json b/latest version/database/init/singles.json index b258f21..cef6ad3 100644 --- a/latest version/database/init/singles.json +++ b/latest version/database/init/singles.json @@ -1720,5 +1720,59 @@ ], "orig_price": 100, "price": 100 + }, + { + "name": "distortedfate", + "items": [ + { + "type": "single", + "id": "distortedfate", + "is_available": true + }, + { + "type": "core", + "amount": 1, + "id": "core_generic", + "is_available": true + } + ], + "orig_price": 100, + "price": 100 + }, + { + "name": "floatingworld", + "items": [ + { + "type": "single", + "id": "floatingworld", + "is_available": true + }, + { + "type": "core", + "amount": 1, + "id": "core_generic", + "is_available": true + } + ], + "orig_price": 100, + "price": 100 + }, + { + "name": "chromafill", + "items": [ + { + "type": "single", + "id": "chromafill", + "is_available": true + }, + { + "type": "core", + "amount": 1, + "id": "core_generic", + "is_available": true + } + ], + "orig_price": 100, + "price": 100 } ] \ No newline at end of file