Skip to content
This repository has been archived by the owner on Jun 9, 2022. It is now read-only.

Commit

Permalink
ADD - basic skill data parsing functionality
Browse files Browse the repository at this point in the history
Signed-off-by: RaenonX <[email protected]>
  • Loading branch information
RaenonX committed Nov 20, 2020
0 parents commit 1b2c776
Show file tree
Hide file tree
Showing 54 changed files with 1,971 additions and 0 deletions.
24 changes: 24 additions & 0 deletions .github/workflows/ci-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: DL Data Parser - CI Tests

on: [push]

jobs:
pytest:
name: Run tests

runs-on: windows-latest

continue-on-error: true

steps:
- uses: actions/checkout@v2

- uses: actions/setup-python@v2

- name: Install required packages
run: |
pip install -r requirements-dev.txt
- name: Run tests
run: |
pytest
28 changes: 28 additions & 0 deletions .github/workflows/cqa.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: DL Data Parser - CQA

on: [push]

jobs:
cqa:
name: CQA

runs-on: windows-latest

continue-on-error: true

steps:
- uses: actions/checkout@v2

- uses: actions/setup-python@v2

- name: Install required packages
run: |
pip install -r requirements-dev.txt
- name: pydocstyle checks (`dlparse`)
run: |
pydocstyle dlparse --count
- name: pylint checks (`dlparse`)
run: |
pylint dlparse
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Game assets (excluding images)
.data/media/assets/_gluonresources/resources/images

# Test cache
.pytest_cache

# IntelliJ project files
.idea
30 changes: 30 additions & 0 deletions .pydocstyle
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[pydocstyle]
ignore =
# Public method missing docstring - `pylint` will check if there's really missing the docstring
D102,
# Magic method missing docstring - no need for it
D105,
# __init__ missing docstring - optional. add details to class docstring
D107,
# Blank line required before docstring - mutually exclusive to D204
D203,
# Multi-line docstring summary should start at the first line - mutually exclusive to D213
D212,
# Section underline is over-indented
D215,
# First line should be in imperative mood
D401,
# First word of the docstring should not be This
D404,
# Section name should end with a newline
D406,
# Missing dashed underline after section
D407,
# Section underline should be in the line following the section’s name
D408,
# Section underline should match the length of its name
D409,
# No blank lines allowed between a section header and its content
D412,
# Missing blank line after last section
D413
38 changes: 38 additions & 0 deletions .pylintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
[BASIC]

# Reason of the good names:
# - _
# often used as dummy variable during unpacking
# - T
# often used to for TypeVar
# - f
# often used as a file stream name
# - i, j, k
# often used in for loops
# - s
# often used to represent "string"
# - v
# often used to represent "value"
# - dt, tz
# often used in datetime handling (dt for datetime, tz for timezone)
# - ex
# often used as the var name of exception caught by try..except
# - fn
# often used to represent a function address

good-names=_,T,f,i,j,k,s,v,dt,ex,fn,tz

[DESIGN]

max-args=10

[FORMAT]

max-line-length=119

[MESSAGES CONTROL]

# TODO: `unsubscriptable-object` generates false positives for python 3.9 and pylint==2.6.
# https://github.com/PyCQA/pylint/issues/3882
# Re-enable it when the issue is fixed.
disable=unsubscriptable-object
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# dragalia-data-parse

This parses the original Dragalia Lost assets to be the file usable for [DL info website][DL-info].

Developed under Python 3.9.

[DL-info]: http://dl.raenonx.cc
[RaenonX-DL]: https://github.com/RaenonX-DL
1 change: 1 addition & 0 deletions dlparse/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Main parser for the Dragalia Lost data."""
2 changes: 2 additions & 0 deletions dlparse/enums/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
"""Various in-asset enums."""
from .affliction import Affliction
23 changes: 23 additions & 0 deletions dlparse/enums/affliction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Affliction enums."""
from enum import IntEnum

__all__ = ("Affliction",)


class Affliction(IntEnum):
"""Affliction enums used in the assets."""

NONE = 0
POISON = 1
BURN = 2
FREEZE = 3
PARALYZE = 4
BLIND = 5
STUN = 6
CURSE = 7
BOG = 9
SLEEP = 10
FROSTBITE = 11
FLASHBURN = 12
CRASHWIND = 13
SHADOWBLIGHT = 14
25 changes: 25 additions & 0 deletions dlparse/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Error classes to be raised during runtime."""
from typing import Optional

__all__ = ("SkillDataNotFound", "ActionDataNotFound")


class SkillDataNotFound(ValueError):
"""Error to be raised if the skill data is not found."""

def __init__(self, skill_id: int):
super().__init__(f"Skill data of ID `{skill_id}` not found")

self._skill_id = skill_id

@property
def skill_id(self):
"""Get the skill ID that causes this error."""
return self._skill_id


class ActionDataNotFound(ValueError):
"""Error to be raised if the action data file is not found."""

def __init__(self, action_id: int, skill_id: Optional[int] = None):
super().__init__(f"Action data of action ID `{action_id}` / skill ID `{skill_id}` not found")
2 changes: 2 additions & 0 deletions dlparse/model/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
"""Various data models to be output."""
from .skill import AttackingSkillData
53 changes: 53 additions & 0 deletions dlparse/model/skill.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Models for character skills."""
from dataclasses import dataclass, field

from dlparse.mono.asset import HitAttrEntry

__all__ = ("AttackingSkillData",)


@dataclass
class AttackingSkillData:
"""
An attacking skill data.
Both ``hit_count`` and ``mods`` should be sorted by the skill level.
For example, if skill level 1 has 1 hit and 100% mods while skill level 2 has 2 hits and 150% + 200% mods,
``hit_count`` should be ``[1, 2]`` and ``mods`` should be ``[[1.0], [1.5, 2.0]]``.
"""

hit_count: list[int]
mods: list[list[float]]

damage_hit_attrs: list[list[HitAttrEntry]]

total_mod: list[float] = field(init=False)

def __post_init__(self):
self.total_mod = [sum(mods) for mods in self.mods]

@property
def hit_count_at_max(self) -> int:
"""Get the skill hit count at the max level."""
return self.hit_count[-1]

@property
def total_mod_at_max(self) -> float:
"""Get the total skill modifier at the max level."""
return self.total_mod[-1]

@property
def mods_at_max(self) -> list[float]:
"""Get the skill modifiers at the max level."""
return self.mods[-1]

@property
def max_available_level(self) -> int:
"""
Get the max available level of a skill.
This max level does **NOT** reflect the actual max level in-game.
To get such, character data is needed.
"""
return len(self.hit_count)
1 change: 1 addition & 0 deletions dlparse/mono/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Classes for mono behaviors."""
3 changes: 3 additions & 0 deletions dlparse/mono/asset/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Asset classes for mono behavior scripts."""
from .master import * # noqa
from .player_action import * # noqa
3 changes: 3 additions & 0 deletions dlparse/mono/asset/base/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Base classes for mono behavior scripts."""
from .master import MasterEntryBase, MasterAssetBase, MasterParserBase
from .player_action import ActionComponentBase, ActionAssetBase, ActionParserBase, ActionComponentDamageDealerMixin
31 changes: 31 additions & 0 deletions dlparse/mono/asset/base/asset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Base asset class."""
import os
from abc import ABC, abstractmethod
from typing import Type, Optional

from .parser import ParserBase

__all__ = ("AssetBase",)


class AssetBase(ABC):
"""Base class for the mono behavior assets."""

asset_file_name: Optional[str] = None

def __init__(self, parser_cls: Type[ParserBase], file_path: Optional[str] = None, /,
asset_dir: Optional[str] = None):
file_path = file_path or (asset_dir and os.path.join(asset_dir, self.asset_file_name))

if not file_path:
raise ValueError("Either `file_path` or "
"`asset_dir` and `asset_file_name` (class attribute) must be given.")

self._data = parser_cls.parse_file(file_path)

def __len__(self):
return len(self._data)

@abstractmethod
def __iter__(self):
raise NotImplementedError()
23 changes: 23 additions & 0 deletions dlparse/mono/asset/base/entry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Base entry class for mono behavior."""
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime
from typing import Union, Optional

__all__ = ("EntryBase",)


@dataclass
class EntryBase(ABC):
"""Base class for the entries in the mono behavior assets."""

@staticmethod
@abstractmethod
def parse_raw(data: dict[str, Union[str, int, float]]) -> "EntryBase":
"""Parse a raw data entry to be the asset entry class."""
raise NotImplementedError()

@staticmethod
def parse_datetime(datetime_str: str) -> Optional[datetime]:
"""Parse ``datetime_str`` to be :class:`datetime` if it's not an empty string."""
return datetime.strptime(datetime_str, "%Y/%m/%d %H:%M:%S") if datetime_str else None
67 changes: 67 additions & 0 deletions dlparse/mono/asset/base/master.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""Base object for the master assets."""
import json
from abc import ABC
from dataclasses import dataclass
from typing import Type, Optional, Any, Callable, Union

from .asset import AssetBase
from .entry import EntryBase
from .parser import ParserBase

__all__ = ("MasterEntryBase", "MasterAssetBase", "MasterParserBase")


@dataclass
class MasterEntryBase(EntryBase, ABC):
"""Base class for the entries in the master mono behavior asset."""

id: int # pylint: disable=invalid-name


class MasterParserBase(ParserBase, ABC):
"""Base parser class for parsing the master asset files."""

@staticmethod
def get_entries(file_path: str) -> dict[int, dict]:
"""Get a dict of data entries which value needs to be further parsed."""
with open(file_path) as f:
data = json.load(f)

if "dict" not in data:
raise ValueError("Key `dict` not in the data")
data = data["dict"]

if "entriesValue" not in data:
raise ValueError("Key `dict.entriesValue` not in the data")
if "entriesKey" not in data:
raise ValueError("Key `dict.entriesKey` not in the data")

entry_keys = filter(lambda key: key != 0, data["entriesKey"]) # Only the entries with key != 0 is valid
entry_values = data["entriesValue"]

return dict(zip(entry_keys, entry_values))

@staticmethod
def parse_file(file_path: str) -> dict[int, Any]:
"""Parse a file as a :class:`dict` which key is the ID of the value."""
raise NotImplementedError()


class MasterAssetBase(AssetBase, ABC):
"""Base class for a master mono behavior asset."""

def __init__(self, parser_cls: Type[MasterParserBase], file_path: Optional[str] = None, /,
asset_dir: Optional[str] = None):
super().__init__(parser_cls, file_path, asset_dir=asset_dir)

def __iter__(self):
return iter(self._data.values())

def filter(self, condition: Callable[[MasterEntryBase], bool]) -> list[MasterEntryBase]:
"""Get a list of data which matches the ``condition``."""
return [data for data in self if condition(data)]

def get_data_by_id(self, data_id: Union[int, str], default: Optional[MasterEntryBase] = None) \
-> Optional[MasterEntryBase]:
"""Get a data by its ``data_id``. Returns ``default`` if not found."""
return self._data.get(data_id, default)
Loading

0 comments on commit 1b2c776

Please sign in to comment.