Skip to content

Commit

Permalink
Add HSR Support (#17)
Browse files Browse the repository at this point in the history
* style: Format pyproject.toml

* Move asset stuff to gi folder

* chore(ruff): Update config

Add docs rules

* chore(deps): Update ruff and pytest

* Move models to gi folder

* chore: Add vscode config

* Add some models

* Update ruff config

* Move enums around

* Change stuff

* Update docstrings and organize imports

* Add vscode config

* Fix import errors

* Add element and path enums

* Update

* Update

* chore(ver): Bump to v2.0.0
  • Loading branch information
seriaati authored May 13, 2024
1 parent aaf21fe commit dd86c4e
Show file tree
Hide file tree
Showing 41 changed files with 1,765 additions and 592 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -151,5 +151,5 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

playground.py
test.py
.enka_py/*
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"python.analysis.typeCheckingMode": "standard",
"python.analysis.importFormat": "relative",
"python.analysis.diagnosticMode": "workspace"
}
6 changes: 2 additions & 4 deletions enka/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
from .client import *
from .constants import *
from .enums import *
from .exceptions import *
from . import errors, gi, hsr, utils
from .clients import GenshinClient, HSRClient
73 changes: 73 additions & 0 deletions enka/assets/data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import contextlib
from typing import Any

import aiofiles
import orjson


class AssetData:
"""Base class for asset data."""

def __init__(self) -> None:
self._data: dict[str, Any] | None = None

def __getitem__(self, key: str) -> Any:
if self._data is None:
msg = f"{self.__class__.__name__} not loaded"
raise RuntimeError(msg)

text = self._data.get(str(key))
if text is None:
msg = f"Cannot find text for key {key!r} in `{self.__class__.__name__}._data`, consider calling `update_assets` to update the assets"
raise KeyError(msg)

return text

def __iter__(self) -> Any:
if self._data is None:
msg = f"{self.__class__.__name__} not loaded"
raise RuntimeError(msg)

return iter(self._data)

def values(self) -> Any:
"""Get the values of the data."""
if self._data is None:
msg = f"{self.__class__.__name__} not loaded"
raise RuntimeError(msg)

return self._data.values()

def items(self) -> Any:
"""Get the items of the data."""
if self._data is None:
msg = f"{self.__class__.__name__} not loaded"
raise RuntimeError(msg)

return self._data.items()

async def _open_json(self, path: str) -> dict[str, Any] | None:
with contextlib.suppress(FileNotFoundError):
async with aiofiles.open(path, encoding="utf-8") as f:
return orjson.loads(await f.read())
return None

def get(self, key: str, default: Any = None) -> str | Any:
"""Get a text by key.
Args:
key (str): The key to get the text for.
default (Any): The default value to return if the key is not found.
Returns:
str | Any: The text or the default value.
"""
if self._data is None:
msg = f"{self.__class__.__name__} not loaded"
raise RuntimeError(msg)

text = self._data.get(str(key))
if text is None:
return default

return text
8 changes: 0 additions & 8 deletions enka/assets/file_paths.py

This file was deleted.

17 changes: 17 additions & 0 deletions enka/assets/gi/file_paths.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
ASSET_PATH = ".enka_py/assets"

TEXT_MAP_PATH = f"{ASSET_PATH}/text_map.json"
CHARACTER_DATA_PATH = f"{ASSET_PATH}/characters.json"
NAMECARD_DATA_PATH = f"{ASSET_PATH}/namecards.json"
CONSTS_DATA_PATH = f"{ASSET_PATH}/consts.json"
TALENTS_DATA_PATH = f"{ASSET_PATH}/talents.json"
PFPS_DATA_PATH = f"{ASSET_PATH}/pfps.json"

SOURCE_TO_PATH = {
"https://raw.githubusercontent.com/seriaati/enka-py-assets/main/data/text_map.json": TEXT_MAP_PATH,
"https://raw.githubusercontent.com/seriaati/enka-py-assets/main/data/characters.json": CHARACTER_DATA_PATH,
"https://raw.githubusercontent.com/EnkaNetwork/API-docs/master/store/namecards.json": NAMECARD_DATA_PATH,
"https://raw.githubusercontent.com/seriaati/enka-py-assets/main/data/consts.json": CONSTS_DATA_PATH,
"https://raw.githubusercontent.com/seriaati/enka-py-assets/main/data/talents.json": TALENTS_DATA_PATH,
"https://raw.githubusercontent.com/EnkaNetwork/API-docs/master/store/pfps.json": PFPS_DATA_PATH,
}
86 changes: 86 additions & 0 deletions enka/assets/gi/manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from typing import TYPE_CHECKING

from ..data import AssetData
from .file_paths import (
CHARACTER_DATA_PATH,
CONSTS_DATA_PATH,
NAMECARD_DATA_PATH,
PFPS_DATA_PATH,
TALENTS_DATA_PATH,
TEXT_MAP_PATH,
)

if TYPE_CHECKING:
from ...enums.gi import Language

__all__ = ("AssetManager",)


class AssetManager:
"""Genshin Impact asset manager."""

def __init__(self, lang: "Language") -> None:
self._lang = lang
self.text_map = TextMap(lang)
self.character_data = CharacterData()
self.namecard_data = NamecardData()
self.consts_data = ConstsData()
self.talents_data = TalentsData()
self.pfps_data = PfpsData()

async def load(self) -> bool:
"""Load all assets.
Returns:
bool: Whether all assets were loaded successfully.
"""
return (
await self.text_map.load()
and await self.character_data.load()
and await self.namecard_data.load()
and await self.consts_data.load()
and await self.talents_data.load()
and await self.pfps_data.load()
)


class TextMap(AssetData):
def __init__(self, lang: "Language") -> None:
super().__init__()
self._lang = lang

async def load(self) -> bool:
text_map = await self._open_json(TEXT_MAP_PATH)
if text_map is not None:
self._data = text_map[self._lang.value]
return self._data is not None


class CharacterData(AssetData):
async def load(self) -> bool:
self._data = await self._open_json(CHARACTER_DATA_PATH)
return self._data is not None


class NamecardData(AssetData):
async def load(self) -> bool:
self._data = await self._open_json(NAMECARD_DATA_PATH)
return self._data is not None


class ConstsData(AssetData):
async def load(self) -> bool:
self._data = await self._open_json(CONSTS_DATA_PATH)
return self._data is not None


class TalentsData(AssetData):
async def load(self) -> bool:
self._data = await self._open_json(TALENTS_DATA_PATH)
return self._data is not None


class PfpsData(AssetData):
async def load(self) -> bool:
self._data = await self._open_json(PFPS_DATA_PATH)
return self._data is not None
24 changes: 24 additions & 0 deletions enka/assets/hsr/file_paths.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
ASSET_PATH = ".enka_py/assets/hsr"

TEXT_MAP_PATH = f"{ASSET_PATH}/text_map.json"
CHARACTER_DATA_PATH = f"{ASSET_PATH}/characters.json"
LIGHT_CONE_DATA_PATH = f"{ASSET_PATH}/light_cone.json"
RELIC_DATA_PATH = f"{ASSET_PATH}/relic.json"
SKILL_TREE_DATA_PATH = f"{ASSET_PATH}/skill_tree.json"
META_DATA_PATH = f"{ASSET_PATH}/promotions.json"
AVATAR_DATA_PATH = f"{ASSET_PATH}/avatars.json"
PROPERTY_CONFIG_PATH = f"{ASSET_PATH}/property_config.json"

ENKA_API_DOCS = "https://raw.githubusercontent.com/EnkaNetwork/API-docs/master/store/hsr"
ENKA_PY_ASSETS = "https://raw.githubusercontent.com/seriaati/enka-py-assets/main/data/hsr"

SOURCE_TO_PATH = {
f"{ENKA_API_DOCS}/hsr.json": TEXT_MAP_PATH,
f"{ENKA_API_DOCS}/honker_characters.json": CHARACTER_DATA_PATH,
f"{ENKA_API_DOCS}/honker_weps.json": LIGHT_CONE_DATA_PATH,
f"{ENKA_API_DOCS}/honker_relics.json": RELIC_DATA_PATH,
f"{ENKA_API_DOCS}/honker_meta.json": META_DATA_PATH,
f"{ENKA_API_DOCS}/honker_avatars.json": AVATAR_DATA_PATH,
f"{ENKA_PY_ASSETS}/skill_tree.json": SKILL_TREE_DATA_PATH,
f"{ENKA_PY_ASSETS}/property_config.json": PROPERTY_CONFIG_PATH,
}
105 changes: 105 additions & 0 deletions enka/assets/hsr/manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from typing import TYPE_CHECKING

from ..data import AssetData
from .file_paths import (
AVATAR_DATA_PATH,
CHARACTER_DATA_PATH,
LIGHT_CONE_DATA_PATH,
META_DATA_PATH,
PROPERTY_CONFIG_PATH,
RELIC_DATA_PATH,
SKILL_TREE_DATA_PATH,
TEXT_MAP_PATH,
)

if TYPE_CHECKING:
from ...enums.hsr import Language

__all__ = ("AssetManager",)


class AssetManager:
"""Honkai Star Rail asset manager."""

def __init__(self, lang: "Language") -> None:
self._lang = lang

self.text_map = TextMap(lang)
self.character_data = CharacterData()
self.skill_tree_data = SkillTreeData()
self.light_cones_data = LightConesData()
self.relic_data = RelicData()
self.meta_data = MetaData()
self.avatar_data = AvatarData()
self.property_config_data = PropertyConfigData()

async def load(self) -> bool:
"""Load all assets.
Returns:
bool: Whether all assets were loaded successfully.
"""
return (
await self.text_map.load()
and await self.character_data.load()
and await self.skill_tree_data.load()
and await self.light_cones_data.load()
and await self.relic_data.load()
and await self.meta_data.load()
and await self.avatar_data.load()
and await self.property_config_data.load()
)


class TextMap(AssetData):
def __init__(self, lang: "Language") -> None:
super().__init__()
self._lang = lang

async def load(self) -> bool:
text_map = await self._open_json(TEXT_MAP_PATH)
if text_map is not None:
self._data = text_map[self._lang.value]
return self._data is not None


class CharacterData(AssetData):
async def load(self) -> bool:
self._data = await self._open_json(CHARACTER_DATA_PATH)
return self._data is not None


class SkillTreeData(AssetData):
async def load(self) -> bool:
self._data = await self._open_json(SKILL_TREE_DATA_PATH)
return self._data is not None


class LightConesData(AssetData):
async def load(self) -> bool:
self._data = await self._open_json(LIGHT_CONE_DATA_PATH)
return self._data is not None


class RelicData(AssetData):
async def load(self) -> bool:
self._data = await self._open_json(RELIC_DATA_PATH)
return self._data is not None


class MetaData(AssetData):
async def load(self) -> bool:
self._data = await self._open_json(META_DATA_PATH)
return self._data is not None


class AvatarData(AssetData):
async def load(self) -> bool:
self._data = await self._open_json(AVATAR_DATA_PATH)
return self._data is not None


class PropertyConfigData(AssetData):
async def load(self) -> bool:
self._data = await self._open_json(PROPERTY_CONFIG_PATH)
return self._data is not None
Loading

0 comments on commit dd86c4e

Please sign in to comment.