diff --git a/ArknightsUID/arknightsuid_resource/__init__.py b/ArknightsUID/arknightsuid_resource/__init__.py index aed4006..cba786c 100644 --- a/ArknightsUID/arknightsuid_resource/__init__.py +++ b/ArknightsUID/arknightsuid_resource/__init__.py @@ -1,3 +1,4 @@ +import asyncio from pathlib import Path from gsuid_core.bot import Bot @@ -7,7 +8,7 @@ from gsuid_core.models import Event from gsuid_core.sv import SV from ..utils.resource.download_all_resource import download_all_resource -from .cachedata import CacheData +from .memoryStore import store from .constants import Excel sv_download_config = SV('下载资源', pm=2) @@ -25,9 +26,11 @@ async def startup(): await download_all_resource() logger.info('[资源文件下载] 检查完毕, 正在加载 gamedata') + tasks = [] for file_path in Path( get_res_path(['ArknightsUID', 'resource', 'gamedata']) ).rglob('*.json'): - CacheData.readFile(file_path) + tasks.append(store.get_file(Path(file_path))) + await asyncio.gather(*tasks) - Excel.preload_table() + await Excel.preload_table() diff --git a/ArknightsUID/arknightsuid_resource/cachedata.py b/ArknightsUID/arknightsuid_resource/cachedata.py deleted file mode 100644 index 567846b..0000000 --- a/ArknightsUID/arknightsuid_resource/cachedata.py +++ /dev/null @@ -1,67 +0,0 @@ -import json -from functools import cache -from pathlib import Path -from time import time -from typing import Any, ClassVar, Dict, Union - -from loguru import logger - -from ..utils.file import read_json - - -class StoreData: - data: Dict[Any, Any] - modification_time: float - - def __init__(self, data: Dict[Any, Any], modification_time: float) -> None: - self.data = data - self.modification_time = modification_time - - -class CacheData: - cached_data: ClassVar[Dict[str, StoreData]] = {} - - @classmethod - @cache - def get_cache(cls, local_path: Path) -> Dict[Any, Any]: - data_name = local_path.stem - if data_name in cls.cached_data: - current_modification_time = local_path.stat().st_mtime - if current_modification_time == cls.cached_data[data_name].modification_time: - return cls.cached_data[data_name].data - return cls.set_cache(local_path, data_name) - - @classmethod - def set_cache( - cls, local_path: Union[Path, None], data_name: str, memory_data: Union[Dict, None] = None - ) -> Dict[Any, Any]: - data = read_json(local_path) if local_path else memory_data - if data is None: - raise FileNotFoundError - modification_time = local_path.stat().st_mtime if local_path else time() - cls.cached_data[data_name] = StoreData(data, modification_time) - return cls.cached_data[data_name].data - - @classmethod - def readFile(cls, local_path: Path) -> Dict[Any, Any]: - try: - if isinstance(local_path, str): - local_path = Path(local_path) - return cls.get_cache(local_path) - except json.decoder.JSONDecodeError as e: - logger.error(f'Could not load file "{local_path}".') - raise FileNotFoundError from e - - @classmethod - def readExcel(cls, table_name: str) -> Dict[Any, Any]: - logger.debug(f'loading: {table_name}.json') - if table_name not in cls.cached_data: - return {} - return cls.cached_data[table_name].data - - @classmethod - def readBytesExcel(cls, table_name: str) -> bytes: - logger.debug(f'loading: {table_name}.json') - if table_name not in cls.cached_data: - return bytes({}) - return json.dumps(cls.cached_data[table_name].data).encode('utf-8') diff --git a/ArknightsUID/arknightsuid_resource/constants.py b/ArknightsUID/arknightsuid_resource/constants.py index 85f18b7..34d2299 100644 --- a/ArknightsUID/arknightsuid_resource/constants.py +++ b/ArknightsUID/arknightsuid_resource/constants.py @@ -1,14 +1,13 @@ +import asyncio import inspect -from typing import Dict, Union -from msgspec import json as msgjson from ..utils.models.gamedata.ActivityTable import ActivityTable from ..utils.models.gamedata.AudioData import AudioData -from ..utils.models.gamedata.BattleEquipTable import BattleEquipData +from ..utils.models.gamedata.BattleEquipTable import BattleEquipTable from ..utils.models.gamedata.BuildingData import BuildingData from ..utils.models.gamedata.CampaignTable import CampaignTable -from ..utils.models.gamedata.ChapterTable import ChapterData -from ..utils.models.gamedata.CharacterTable import CharacterData +from ..utils.models.gamedata.ChapterTable import ChapterTable +from ..utils.models.gamedata.CharacterTable import CharacterTable from ..utils.models.gamedata.CharMetaTable import CharMetaTable from ..utils.models.gamedata.CharmTable import CharmTable from ..utils.models.gamedata.CharPatchTable import CharPatchTable @@ -24,474 +23,516 @@ from ..utils.models.gamedata.GachaTable import GachaTable from ..utils.models.gamedata.GamedataConst import GamedataConst from ..utils.models.gamedata.HandbookInfoTable import HandbookInfoTable from ..utils.models.gamedata.HandbookTable import HandbookTable -from ..utils.models.gamedata.HandbookTeamTable import HandbookTeam +from ..utils.models.gamedata.HandbookTeamTable import HandbookTeamTable from ..utils.models.gamedata.ItemTable import ItemTable from ..utils.models.gamedata.MedalTable import MedalTable from ..utils.models.gamedata.MissionTable import MissionTable from ..utils.models.gamedata.OpenServerTable import OpenServerTable from ..utils.models.gamedata.PlayerAvatarTable import PlayerAvatarTable -from ..utils.models.gamedata.RangeTable import Stage -from ..utils.models.gamedata.ReplicateTable import ReplicateList +from ..utils.models.gamedata.RangeTable import RangeTable +from ..utils.models.gamedata.ReplicateTable import ReplicateTable from ..utils.models.gamedata.RetroTable import RetroTable from ..utils.models.gamedata.RoguelikeTable import RoguelikeTable from ..utils.models.gamedata.RoguelikeTopicTable import RoguelikeTopicTable from ..utils.models.gamedata.SandboxTable import SandboxTable from ..utils.models.gamedata.ShopClientTable import ShopClientTable -from ..utils.models.gamedata.SkillTable import SkillDataBundle +from ..utils.models.gamedata.SkillTable import SkillTable from ..utils.models.gamedata.SkinTable import SkinTable from ..utils.models.gamedata.StageTable import StageTable from ..utils.models.gamedata.StoryReviewMetaTable import StoryReviewMetaTable -from ..utils.models.gamedata.StoryReviewTable import StoryReviewGroupClientData -from ..utils.models.gamedata.StoryTable import StoryData +from ..utils.models.gamedata.StoryReviewTable import StoryReviewTable +from ..utils.models.gamedata.StoryTable import StoryTable from ..utils.models.gamedata.TechBuffTable import TechBuffTable from ..utils.models.gamedata.TipTable import TipTable -from ..utils.models.gamedata.TokenTable import TokenCharacterData +from ..utils.models.gamedata.TokenTable import TokenTable from ..utils.models.gamedata.UniequipData import UniequipData from ..utils.models.gamedata.UniequipTable import UniEquipTable from ..utils.models.gamedata.ZoneTable import ZoneTable -from .cachedata import CacheData +from .memoryStore import store class ExcelTableManager: - activity_table_: Union[ActivityTable, None] = None - audio_data_: Union[AudioData, None] = None - battle_equip_table_: Union[Dict[str, BattleEquipData], None] = None - building_data_: Union[BuildingData, None] = None - campaign_table_: Union[CampaignTable, None] = None - chapter_table_: Union[Dict[str, ChapterData], None] = None - character_table_: Union[Dict[str, CharacterData], None] = None - char_meta_table_: Union[CharMetaTable, None] = None - charm_table_: Union[CharmTable, None] = None - char_patch_table_: Union[CharPatchTable, None] = None - charword_table_: Union[CharwordTable, None] = None - checkin_table_: Union[CheckinTable, None] = None - climb_tower_table_: Union[ClimbTowerTable, None] = None - clue_data_: Union[ClueData, None] = None - crisis_table_: Union[CrisisTable, None] = None - display_meta_table_: Union[DisplayMetaTable, None] = None - enemy_handbook_table_: Union[EnemyHandbookTable, None] = None - favor_table_: Union[FavorTable, None] = None - gacha_table_: Union[GachaTable, None] = None - gamedata_const_: Union[GamedataConst, None] = None - handbook_info_table_: Union[HandbookInfoTable, None] = None - handbook_table_: Union[HandbookTable, None] = None - handbook_team_table_: Union[Dict[str, HandbookTeam], None] = None - item_table_: Union[ItemTable, None] = None - medal_table_: Union[MedalTable, None] = None - mission_table_: Union[MissionTable, None] = None - open_server_table_: Union[OpenServerTable, None] = None - player_avatar_table_: Union[PlayerAvatarTable, None] = None - range_table_: Union[Dict[str, Stage], None] = None - replicate_table_: Union[Dict[str, ReplicateList], None] = None - retro_table_: Union[RetroTable, None] = None - roguelike_table_: Union[RoguelikeTable, None] = None - roguelike_topic_table_: Union[RoguelikeTopicTable, None] = None - sandbox_table_: Union[SandboxTable, None] = None - shop_client_table_: Union[ShopClientTable, None] = None - skill_table_: Union[Dict[str, SkillDataBundle], None] = None - skin_table_: Union[SkinTable, None] = None - stage_table_: Union[StageTable, None] = None - story_review_meta_table_: Union[StoryReviewMetaTable, None] = None - story_review_table_: Union[Dict[str, StoryReviewGroupClientData], None] = None - story_table_: Union[Dict[str, StoryData], None] = None - tech_buff_table_: Union[TechBuffTable, None] = None - tip_table_: Union[TipTable, None] = None - token_table_: Union[Dict[str, TokenCharacterData], None] = None - uniequip_data_: Union[UniequipData, None] = None - uniequip_table_: Union[UniEquipTable, None] = None - zone_table_: Union[ZoneTable, None] = None + activity_table_: ActivityTable + audio_data_: AudioData + battle_equip_table_: BattleEquipTable + building_data_: BuildingData + campaign_table_: CampaignTable + chapter_table_: ChapterTable + character_table_: CharacterTable + char_meta_table_: CharMetaTable + charm_table_: CharmTable + char_patch_table_: CharPatchTable + charword_table_: CharwordTable + checkin_table_: CheckinTable + climb_tower_table_: ClimbTowerTable + clue_data_: ClueData + crisis_table_: CrisisTable + display_meta_table_: DisplayMetaTable + enemy_handbook_table_: EnemyHandbookTable + favor_table_: FavorTable + gacha_table_: GachaTable + gamedata_const_: GamedataConst + handbook_info_table_: HandbookInfoTable + handbook_table_: HandbookTable + handbook_team_table_: HandbookTeamTable + item_table_: ItemTable + medal_table_: MedalTable + mission_table_: MissionTable + open_server_table_: OpenServerTable + player_avatar_table_: PlayerAvatarTable + range_table_: RangeTable + replicate_table_: ReplicateTable + retro_table_: RetroTable + roguelike_table_: RoguelikeTable + roguelike_topic_table_: RoguelikeTopicTable + sandbox_table_: SandboxTable + shop_client_table_: ShopClientTable + skill_table_: SkillTable + skin_table_: SkinTable + stage_table_: StageTable + story_review_meta_table_: StoryReviewMetaTable + story_review_table_: StoryReviewTable + story_table_: StoryTable + tech_buff_table_: TechBuffTable + tip_table_: TipTable + token_table_: TokenTable + uniequip_data_: UniequipData + uniequip_table_: UniEquipTable + zone_table_: ZoneTable + + async def activity_table(self) -> None: + self.activity_table_ = ActivityTable.convert( + await store.get_excel("activity_table") + ) @property def ACTIVITY_TABLE(self) -> ActivityTable: - if not self.activity_table_: - self.activity_table_ = ActivityTable.convert( - CacheData.readExcel('activity_table') - ) return self.activity_table_ + async def audio_data(self) -> None: + self.audio_data_ = AudioData.convert( + await store.get_excel("audio_data") + ) + @property def AUDIO_DATA(self) -> AudioData: - if not self.audio_data_: - self.audio_data_ = AudioData.convert( - CacheData.readExcel('audio_data') - ) return self.audio_data_ + async def battle_equip_table(self) -> None: + self.battle_equip_table_ = BattleEquipTable.convert( + {"equips": await store.get_excel("battle_equip_table")} + ) + @property - def BATTLE_EQUIP_TABLE(self) -> Dict[str, BattleEquipData]: - if not self.battle_equip_table_: - self.battle_equip_table_ = msgjson.decode( - CacheData.readBytesExcel('battle_equip_table'), - type=Dict[str, BattleEquipData] - ) + def BATTLE_EQUIP_TABLE(self) -> BattleEquipTable: return self.battle_equip_table_ + async def building_data(self) -> None: + self.building_data_ = BuildingData.convert( + await store.get_excel("building_data") + ) + @property def BUILDING_DATA(self) -> BuildingData: - if not self.building_data_: - self.building_data_ = BuildingData.convert( - CacheData.readExcel('building_data') - ) return self.building_data_ + async def campaign_table(self) -> None: + self.campaign_table_ = CampaignTable.convert( + await store.get_excel("campaign_table") + ) + @property def CAMPAIGN_TABLE(self) -> CampaignTable: - if not self.campaign_table_: - self.campaign_table_ = CampaignTable.convert( - CacheData.readExcel('campaign_table') - ) return self.campaign_table_ - @property - def CHAPTER_TABLE(self) -> Dict[str, ChapterData]: - if not self.chapter_table_: - self.chapter_table_ = msgjson.decode( - CacheData.readBytesExcel('chapter_table'), - type=Dict[str, ChapterData] - ) - return self.chapter_table_ + async def chapter_table(self) -> None: + self.chapter_table_ = ChapterTable.convert( + {"chapters": await store.get_excel("chapter_table")} + ) @property - def CHARATER_TABLE(self) -> Dict[str, CharacterData]: - if not self.character_table_: - self.character_table_ = msgjson.decode( - CacheData.readBytesExcel('character_table'), - type=Dict[str, CharacterData] - ) + def CHAPTER_TABLE(self) -> ChapterTable: + return self.chapter_table_ + + async def character_table(self) -> None: + self.character_table_ = CharacterTable.convert( + {"chars": await store.get_excel("character_table")} + ) + + @property + def CHARATER_TABLE(self) -> CharacterTable: return self.character_table_ + async def char_meta_table(self) -> None: + self.char_meta_table_ = CharMetaTable.convert( + await store.get_excel("char_meta_table") + ) + @property def CHAR_META_TABLE(self) -> CharMetaTable: - if not self.char_meta_table_: - self.char_meta_table_ = CharMetaTable.convert( - CacheData.readExcel('char_meta_table') - ) return self.char_meta_table_ + async def charm_table(self) -> None: + self.charm_table_ = CharmTable.convert( + await store.get_excel("charm_table") + ) + @property def CHARM_TABLE(self) -> CharmTable: - if not self.charm_table_: - self.charm_table_ = CharmTable.convert( - CacheData.readExcel('charm_table') - ) return self.charm_table_ + async def char_patch_table(self) -> None: + self.char_patch_table_ = CharPatchTable.convert( + await store.get_excel("char_patch_table") + ) + @property def CHAR_PATH_TABLE(self) -> CharPatchTable: - if not self.char_patch_table_: - self.char_patch_table_ = CharPatchTable.convert( - CacheData.readExcel('char_patch_table') - ) return self.char_patch_table_ + async def charword_table(self) -> None: + self.charword_table_ = CharwordTable.convert( + await store.get_excel("charword_table") + ) + @property def CHARWORD_TABLE(self) -> CharwordTable: - if not self.charword_table_: - self.charword_table_ = CharwordTable.convert( - CacheData.readExcel('charword_table') - ) return self.charword_table_ + async def checkin_table(self) -> None: + self.checkin_table_ = CheckinTable.convert( + await store.get_excel("checkin_table") + ) + @property def CHECKIN_TABLE(self) -> CheckinTable: - if not self.checkin_table_: - self.checkin_table_ = CheckinTable.convert( - CacheData.readExcel('checkin_table') - ) return self.checkin_table_ + async def climb_tower_table(self) -> None: + self.climb_tower_table_ = ClimbTowerTable.convert( + await store.get_excel("climb_tower_table") + ) + @property def CLIMB_TOWER_TABLE(self) -> ClimbTowerTable: - if not self.climb_tower_table_: - self.climb_tower_table_ = ClimbTowerTable.convert( - CacheData.readExcel('climb_tower_table') - ) return self.climb_tower_table_ + async def clue_data(self) -> None: + self.clue_data_ = ClueData.convert( + await store.get_excel("clue_data") + ) + @property def CLUE_DATA(self) -> ClueData: - if not self.clue_data_: - self.clue_data_ = ClueData.convert( - CacheData.readExcel('clue_data') - ) return self.clue_data_ + async def crisis_table(self) -> None: + self.crisis_table_ = CrisisTable.convert( + await store.get_excel("crisis_table") + ) + @property def CRISIS_TABLE(self) -> CrisisTable: - if not self.crisis_table_: - self.crisis_table_ = CrisisTable.convert( - CacheData.readExcel('crisis_table') - ) return self.crisis_table_ + async def display_meta_table(self) -> None: + self.display_meta_table_ = DisplayMetaTable.convert( + await store.get_excel("display_meta_table") + ) + @property def DISPLAY_META_TABLE(self) -> DisplayMetaTable: - if not self.display_meta_table_: - self.display_meta_table_ = DisplayMetaTable.convert( - CacheData.readExcel('display_meta_table') - ) return self.display_meta_table_ + async def enemy_handbook_table(self) -> None: + self.enemy_handbook_table_ = EnemyHandbookTable.convert( + await store.get_excel("enemy_handbook_table") + ) + @property def ENEMY_HANDBOOK_TABLE(self) -> EnemyHandbookTable: - if not self.enemy_handbook_table_: - self.enemy_handbook_table_ = EnemyHandbookTable.convert( - CacheData.readExcel('enemy_handbook_table') - ) return self.enemy_handbook_table_ + async def favor_table(self) -> None: + self.favor_table_ = FavorTable.convert( + await store.get_excel("favor_table") + ) + @property def FAVOR_TABLE(self) -> FavorTable: - if not self.favor_table_: - self.favor_table_ = FavorTable.convert( - CacheData.readExcel('favor_table') - ) return self.favor_table_ + async def gacha_table(self) -> None: + self.gacha_table_ = GachaTable.convert( + await store.get_excel("gacha_table") + ) + @property def GACHA_TABLE(self) -> GachaTable: - if not self.gacha_table_: - self.gacha_table_ = GachaTable.convert( - CacheData.readExcel('gacha_table') - ) return self.gacha_table_ + async def gamedata_const(self) -> None: + self.gamedata_const_ = GamedataConst.convert( + await store.get_excel("gamedata_const") + ) + @property def GAMEDATA_CONST(self) -> GamedataConst: - if not self.gamedata_const_: - self.gamedata_const_ = GamedataConst.convert( - CacheData.readExcel('gamedata_const') - ) return self.gamedata_const_ + async def handbook_info_table(self) -> None: + self.handbook_info_table_ = HandbookInfoTable.convert( + await store.get_excel("handbook_info_table") + ) + @property def HANDBOOK_INFO_TABLE(self) -> HandbookInfoTable: - if not self.handbook_info_table_: - self.handbook_info_table_ = HandbookInfoTable.convert( - CacheData.readExcel('handbook_info_table') - ) return self.handbook_info_table_ + async def handbook_table(self) -> None: + self.handbook_table_ = HandbookTable.convert( + await store.get_excel("handbook_table") + ) + @property def HANDBOOK_TABLE(self) -> HandbookTable: - if not self.handbook_table_: - self.handbook_table_ = HandbookTable.convert( - CacheData.readExcel('handbook_table') - ) return self.handbook_table_ + async def handbook_team_table(self) -> None: + self.handbook_team_table_ = HandbookTeamTable.convert( + {"team": await store.get_excel("handbook_team_table")} + ) + @property - def HANDBOOK_TEAM_TABLE(self) -> Dict[str, HandbookTeam]: - if not self.handbook_team_table_: - self.handbook_team_table_ = msgjson.decode( - CacheData.readBytesExcel('handbook_team_table'), - type=Dict[str, HandbookTeam] - ) + def HANDBOOK_TEAM_TABLE(self) -> HandbookTeamTable: return self.handbook_team_table_ + async def item_table(self) -> None: + self.item_table_ = ItemTable.convert( + await store.get_excel("item_table") + ) + @property def ITEM_TABLE(self) -> ItemTable: - if not self.item_table_: - self.item_table_ = ItemTable.convert( - CacheData.readExcel('item_table') - ) return self.item_table_ + async def medal_table(self) -> None: + self.medal_table_ = MedalTable.convert( + await store.get_excel("medal_table") + ) + @property def MEDAL_TABLE(self) -> MedalTable: - if not self.medal_table_: - self.medal_table_ = MedalTable.convert( - CacheData.readExcel('medal_table') - ) return self.medal_table_ + async def mission_table(self) -> None: + self.mission_table_ = MissionTable.convert( + await store.get_excel("mission_table") + ) + @property def MISSION_TABLE(self) -> MissionTable: - if not self.mission_table_: - self.mission_table_ = MissionTable.convert( - CacheData.readExcel('mission_table') - ) return self.mission_table_ + async def open_server_table(self) -> None: + self.open_server_table_ = OpenServerTable.convert( + await store.get_excel("open_server_table") + ) + @property def OPEN_SERVER_TABLE(self) -> OpenServerTable: - if not self.open_server_table_: - self.open_server_table_ = OpenServerTable.convert( - CacheData.readExcel('open_server_table') - ) return self.open_server_table_ + async def player_avatar_table(self) -> None: + self.player_avatar_table_ = PlayerAvatarTable.convert( + await store.get_excel("player_avatar_table") + ) + @property def PLAYER_AVATAR_TABLE(self) -> PlayerAvatarTable: - if not self.player_avatar_table_: - self.player_avatar_table_ = PlayerAvatarTable.convert( - CacheData.readExcel('player_avatar_table') - ) return self.player_avatar_table_ - @property - def RANGE_TABLE(self) -> Dict[str, Stage]: - if not self.range_table_: - self.range_table_ = msgjson.decode( - CacheData.readBytesExcel('range_table'), - type=Dict[str, Stage] - ) - return self.range_table_ + async def range_table(self) -> None: + self.range_table_ = RangeTable.convert( + {"range": await store.get_excel("range_table")} + ) @property - def REPLICATE_TABLE(self) -> Dict[str, ReplicateList]: - if not self.replicate_table_: - self.replicate_table_ = msgjson.decode( - CacheData.readBytesExcel('replicate_table'), - type=Dict[str, ReplicateList] - ) + def RANGE_TABLE(self) -> RangeTable: + return self.range_table_ + + async def replicate_table(self) -> None: + self.replicate_table_ = ReplicateTable.convert( + {"replicate": await store.get_excel("replicate_table")} + ) + + @property + def REPLICATE_TABLE(self) -> ReplicateTable: return self.replicate_table_ + async def retro_table(self) -> None: + self.retro_table_ = RetroTable.convert( + await store.get_excel("retro_table") + ) + @property def RETRO_TABLE(self) -> RetroTable: - if not self.retro_table_: - self.retro_table_ = RetroTable.convert( - CacheData.readExcel('retro_table') - ) return self.retro_table_ + async def roguelike_table(self) -> None: + self.roguelike_table_ = RoguelikeTable.convert( + await store.get_excel("roguelike_table") + ) + @property def ROGUELIKE_TABLE(self) -> RoguelikeTable: - if not self.roguelike_table_: - self.roguelike_table_ = RoguelikeTable.convert( - CacheData.readExcel('roguelike_table') - ) return self.roguelike_table_ + async def roguelike_topic_table(self) -> None: + self.roguelike_topic_table_ = RoguelikeTopicTable.convert( + await store.get_excel("roguelike_topic_table") + ) + @property def ROGUELIKE_TOPIC_TABLE(self) -> RoguelikeTopicTable: - if not self.roguelike_topic_table_: - self.roguelike_topic_table_ = RoguelikeTopicTable.convert( - CacheData.readExcel('roguelike_topic_table') - ) return self.roguelike_topic_table_ + async def sandbox_table(self) -> None: + self.sandbox_table_ = SandboxTable.convert( + await store.get_excel("sandbox_table") + ) + @property def SANDBOX_TABLE(self) -> SandboxTable: - if not self.sandbox_table_: - self.sandbox_table_ = SandboxTable.convert( - CacheData.readExcel('sandbox_table') - ) return self.sandbox_table_ + async def shop_client_table(self) -> None: + self.shop_client_table_ = ShopClientTable.convert( + await store.get_excel("shop_client_table") + ) + @property def SHOP_CLIENT_TABLE(self) -> ShopClientTable: - if not self.shop_client_table_: - self.shop_client_table_ = ShopClientTable.convert( - CacheData.readExcel('shop_client_table') - ) return self.shop_client_table_ + async def skill_table(self) -> None: + self.skill_table_ = SkillTable.convert( + {"skills": await store.get_excel("skill_table")} + ) + @property - def SKILL_TABLE(self) -> Dict[str, SkillDataBundle]: - if not self.skill_table_: - self.skill_table_ = msgjson.decode( - CacheData.readBytesExcel('skill_table'), - type=Dict[str, SkillDataBundle] - ) + def SKILL_TABLE(self) -> SkillTable: return self.skill_table_ + async def skin_table(self) -> None: + self.skin_table_ = SkinTable.convert( + await store.get_excel("skin_table") + ) + @property def SKIN_TABLE(self) -> SkinTable: - if not self.skin_table_: - self.skin_table_ = SkinTable.convert( - CacheData.readExcel('skin_table') - ) return self.skin_table_ + async def stage_table(self) -> None: + self.stage_table_ = StageTable.convert( + await store.get_excel("stage_table") + ) + @property def STAGE_TABLE(self) -> StageTable: - if not self.stage_table_: - self.stage_table_ = StageTable.convert( - CacheData.readExcel('stage_table') - ) return self.stage_table_ + async def story_review_meta_table(self) -> None: + self.story_review_meta_table_ = StoryReviewMetaTable.convert( + await store.get_excel("story_review_meta_table") + ) + @property def STORY_REVIEW_META_TABLE(self) -> StoryReviewMetaTable: - if not self.story_review_meta_table_: - self.story_review_meta_table_ = StoryReviewMetaTable.convert( - CacheData.readExcel('story_review_meta_table') - ) return self.story_review_meta_table_ - @property - def STORY_REVIEW_TABLE(self) -> Dict[str, StoryReviewGroupClientData]: - if not self.story_review_table_: - self.story_review_table_ = msgjson.decode( - CacheData.readBytesExcel('story_review_table'), - type=Dict[str, StoryReviewGroupClientData] - ) - return self.story_review_table_ + async def story_review_table(self) -> None: + self.story_review_table_ = StoryReviewTable.convert( + {"storyreviewtable": await store.get_excel("story_review_table")} + ) @property - def STORY_TABLE(self) -> Dict[str, StoryData]: - if not self.story_table_: - self.story_table_ = msgjson.decode( - CacheData.readBytesExcel('story_table'), - type=Dict[str, StoryData] - ) + def STORY_REVIEW_TABLE(self) -> StoryReviewTable: + return self.story_review_table_ + + async def story_table(self) -> None: + self.story_table_ = StoryTable.convert( + {"stories": await store.get_excel("story_table")} + ) + + @property + def STORY_TABLE(self) -> StoryTable: return self.story_table_ + async def tech_buff_table(self) -> None: + self.tech_buff_table_ = TechBuffTable.convert( + await store.get_excel("tech_buff_table") + ) + @property def TECH_BUFF_TABLE(self) -> TechBuffTable: - if not self.tech_buff_table_: - self.tech_buff_table_ = TechBuffTable.convert( - CacheData.readExcel('tech_buff_table') - ) return self.tech_buff_table_ + async def tip_table(self) -> None: + self.tip_table_ = TipTable.convert( + await store.get_excel("tip_table") + ) + @property def TIP_TABLE(self) -> TipTable: - if not self.tip_table_: - self.tip_table_ = TipTable.convert( - CacheData.readExcel('tip_table') - ) return self.tip_table_ + async def token_table(self) -> None: + self.token_table_ = TokenTable.convert( + {"tokens": await store.get_excel("token_table")} + ) + @property - def TOKEN_TABLE(self) -> Dict[str, TokenCharacterData]: - if not self.token_table_: - self.token_table_ = msgjson.decode( - CacheData.readBytesExcel('token_table'), - type=Dict[str, TokenCharacterData] - ) + def TOKEN_TABLE(self) -> TokenTable: return self.token_table_ + async def uniequip_data(self) -> None: + self.uniequip_data_ = UniequipData.convert( + await store.get_excel("uniequip_data") + ) + @property def UNIEQUIP_DATA(self) -> UniequipData: - if not self.uniequip_data_: - self.uniequip_data_ = UniequipData.convert( - CacheData.readExcel('uniequip_data') - ) return self.uniequip_data_ + async def uniequip_table(self) -> None: + self.uniequip_table_ = UniEquipTable.convert( + await store.get_excel("uniequip_table") + ) + @property def UNIEQUIP_TABLE(self) -> UniEquipTable: - if not self.uniequip_table_: - self.uniequip_table_ = UniEquipTable.convert( - CacheData.readExcel('uniequip_table') - ) return self.uniequip_table_ + async def zone_table(self) -> None: + self.zone_table_ = ZoneTable.convert( + await store.get_excel("zone_table") + ) + @property def ZONE_TABLE(self) -> ZoneTable: - if not self.zone_table_: - self.zone_table_ = ZoneTable.convert( - CacheData.readExcel('zone_table') - ) return self.zone_table_ - def preload_table(self) -> None: - for name, method in inspect.getmembers(self, predicate=inspect.ismethod): - if callable(method) and not name.startswith('__') \ - and name != 'preload_table': - method() + async def preload_table(self) -> None: + tasks = [] + for name, method in inspect.getmembers(self): + if ( + inspect.iscoroutinefunction(method) + and not name.startswith("__") + and name != "preload_table" + ): + await method() + await asyncio.gather(*tasks) Excel = ExcelTableManager() diff --git a/ArknightsUID/arknightsuid_resource/memoryStore.py b/ArknightsUID/arknightsuid_resource/memoryStore.py new file mode 100644 index 0000000..1fa461e --- /dev/null +++ b/ArknightsUID/arknightsuid_resource/memoryStore.py @@ -0,0 +1,179 @@ +from __future__ import annotations +from abc import ABC, abstractmethod +import json + +import os +import pickle +import shutil +from datetime import UTC, datetime, timedelta +from pathlib import Path +from tempfile import mkstemp +from typing import Any + +import anyio +from anyio import Path as anyioPath +from anyio.to_thread import run_sync +from msgspec import Struct + +from gsuid_core.logger import logger + + +def read_json(file_path: Path, **kwargs) -> dict: + """ + Read a JSON file and return its contents as a dictionary. + """ + try: + with Path.open(file_path, encoding="UTF-8", **kwargs) as file: + return json.load(file) + except (FileNotFoundError, json.JSONDecodeError) as e: + logger.error(f"Error reading JSON file: {e}") + return {} + + +class Store(ABC): + """Thread and process safe asynchronous key/value store.""" + + @abstractmethod + async def set(self, key: str, value: str | bytes, expires_in: int | timedelta | None = None) -> None: + raise NotImplementedError + + @abstractmethod + async def get(self, key: str, renew_for: int | timedelta | None = None) -> bytes | None: + raise NotImplementedError + + @abstractmethod + async def delete(self, key: str) -> None: + raise NotImplementedError + + @abstractmethod + async def delete_all(self) -> None: + raise NotImplementedError + + @abstractmethod + async def exists(self, key: str) -> bool: + raise NotImplementedError + + @abstractmethod + async def expires_in(self, key: str) -> int | None: + raise NotImplementedError + + +class StorageObject(Struct): + expires_at: datetime | None + data: Any + + @classmethod + def new(cls, data: Any, expires_in: int | timedelta | None) -> StorageObject: + if expires_in is not None and not isinstance(expires_in, timedelta): + expires_in = timedelta(seconds=expires_in) + return cls( + data=data, + expires_at=(datetime.now(tz=UTC) + expires_in) if expires_in else None, + ) + + @property + def expired(self) -> bool: + return self.expires_at is not None and datetime.now(tz=UTC) >= self.expires_at + + @property + def expires_in(self) -> int: + if self.expires_at: + return int(self.expires_at.timestamp() - datetime.now(tz=UTC).timestamp()) + return -1 + + +class StoreService(Store): + __slots__ = ("store", "lock", "path") + + def __init__(self) -> None: + self.path: anyioPath = anyioPath('data') + self.store_: dict[str, StorageObject] = {} + self.lock: anyio.Lock = anyio.Lock() + self.last_cleared: datetime = datetime.utcnow() + + @staticmethod + async def load_from_path(path: anyioPath) -> dict[str, StorageObject]: + try: + data = await path.read_bytes() + return pickle.loads(data) + except FileNotFoundError: + return {} + + def _write_sync(self, target_file: anyioPath) -> None: + try: + tmp_file_fd, tmp_file_name = mkstemp(dir=self.path, prefix=f"{target_file.name}.cache") + renamed = False + try: + try: + os.write(tmp_file_fd, pickle.dumps(self.store_)) + finally: + os.close(tmp_file_fd) + shutil.move(tmp_file_name, target_file) + renamed = True + finally: + if not renamed: + Path.unlink(Path(tmp_file_name)) + except OSError: + pass + + async def write(self, target_file: anyioPath) -> None: + await run_sync(self._write_sync, target_file) + + async def set(self, key: str, value: Any, expires_in: int | timedelta | None = None) -> None: + if isinstance(value, str): + value = value.encode("UTF-8") + async with self.lock: + self.store_[key] = StorageObject.new(data=value, expires_in=expires_in) + + async def get(self, key: str, renew_for: int | timedelta | None = None, default: Any = None) -> Any: + async with self.lock: + storage_obj = self.store_.get(key) + if not storage_obj: + return default + if storage_obj.expired: + self.store_.pop(key) + return None + if renew_for and storage_obj.expires_at: + storage_obj = StorageObject.new(data=storage_obj.data, expires_in=renew_for) + self.store_[key] = storage_obj + return storage_obj.data + + async def get_excel(self, table_name: str) -> Any: + return await self.get(table_name, default=None) + + async def get_file(self, local_path: Path, expires_in: int | timedelta | None = None) -> Any: + if not await self.exists(local_path.stem): + await self.set(local_path.stem, read_json(local_path), expires_in) + return await self.get(local_path.stem) + + async def delete(self, key: str) -> None: + async with self.lock: + self.store_.pop(key, None) + + async def delete_all(self) -> None: + async with self.lock: + self.store_.clear() + + async def delete_expired(self) -> None: + async with self.lock: + new_store = {} + for i, (key, storage_obj) in enumerate(self.store_.items()): + if not storage_obj.expired: + new_store[key] = storage_obj + if i % 1000 == 0: + await anyio.sleep(0) + self.store_ = new_store + + async def exists(self, key: str) -> bool: + return key in self.store_ + + async def keys(self) -> list[str]: + return list(self.store_.keys()) + + async def expires_in(self, key: str) -> int | None: + if storage_obj := self.store_.get(key): + return storage_obj.expires_in + return None + + +store = StoreService()