# DOCUMENT + REWRITE
from collections import UserList as ListDerive
from pathlib import Path
from attr import attrib, dataclass
from iters import iter
from gd.api.struct import LevelAPI # type: ignore
from gd.iter_utils import extract_iterable_from_tuple
from gd.json import dumps
from gd.text_utils import make_repr, snake_to_camel
from gd.typing import Any, Dict, Iterable, List, Optional, Tuple, TypeVar, Union
from gd.xml_parser import XMLParser
__all__ = ("Part", "Database", "LevelStore", "LevelValues", "LevelCollection")
AnyString = Union[bytes, str]
PathLike = Union[str, Path]
IS_ARRAY = snake_to_camel("_is_arr")
MAIN = "CCGameManager.dat"
LEVELS = "CCLocalLevels.dat"
T = TypeVar("T")
def is_dict(some: Any) -> bool:
return isinstance(some, dict)
[docs]@dataclass
class LevelStore:
"""Values that particular completed levels in the save."""
completed: List[int] = attrib()
stars: List[int] = attrib()
demons: List[int] = attrib()
@classmethod
def create_empty(cls) -> "LevelStore":
return cls(completed=[], stars=[], demons=[])
[docs]@dataclass
class LevelValues:
"""Values that represent completed levels in the save."""
official: List[int] = attrib()
normal: LevelStore = attrib()
timely: LevelStore = attrib()
gauntlet: LevelStore = attrib()
packs: List[int] = attrib()
def get_type_to_array(self) -> Dict[str, List[int]]:
values, normal, timely, gauntlet = self, self.normal, self.timely, self.gauntlet
return {
"n": values.official,
"c": normal.completed,
"d": timely.completed,
"g": gauntlet.completed,
"star": normal.stars,
"dstar": timely.stars,
"gstar": gauntlet.stars,
"demon": normal.demons,
"ddemon": timely.demons,
"gdemon": gauntlet.demons,
"pack": values.packs,
}
def get_prefix_to_array(self) -> Dict[str, List[int]]:
return {f"{type}_": array for type, array in self.get_type_to_array().items()}
@classmethod
def create_empty(cls) -> "LevelValues":
return cls(
official=[],
normal=LevelStore.create_empty(),
timely=LevelStore.create_empty(),
gauntlet=LevelStore.create_empty(),
packs=[],
)
def remove_prefix(string: str, prefix: str) -> str:
if string.startswith(prefix):
return string[len(prefix) :]
return string
[docs]class Part(dict):
@classmethod
def load(cls, stream: AnyString, default: Optional[Dict[str, T]] = None) -> "Part":
self = cls()
try:
self.update(self.parser.load(stream))
except Exception:
if default is None:
default = {}
self.update(default)
return self
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.parser = XMLParser()
def __str__(self) -> str:
return dumps(self, indent=4)
def __repr__(self) -> str:
info = {"length": len(self)}
return make_repr(self, info)
[docs] def copy(self) -> "Part":
return self.__class__(super().copy())
[docs] def set(self, key: str, value: T) -> None:
"""Same as self[key] = value."""
self[key] = value
[docs] def dump_as_bytes(self) -> bytes:
"""Dump the part and return xml data."""
return self.parser.dump_as_bytes(self)
[docs] def dump(self) -> str:
"""Dump the part and return xml string."""
return self.parser.dump(self)
[docs]class Database:
def __init__(
self, main: Optional[AnyString] = None, levels: Optional[AnyString] = None
) -> None:
self.main = Part.load(main) if main else Part()
self.levels = Part.load(levels) if levels else Part()
def __repr__(self) -> str:
info = {"main": repr(self.main), "levels": repr(self.levels)}
return make_repr(self, info)
def __json__(self) -> Dict[str, Part]:
return {"main": self.main, "levels": self.levels}
def __bool__(self) -> bool:
if self.main:
return True
if self.levels:
return True
return False
[docs] def is_empty(self) -> bool:
"""Check if the database is empty."""
return not self
[docs] def get_user_name(self) -> str:
"""Player name."""
return self.main.get("GJA_001", "unknown")
[docs] def set_user_name(self, user_name: str) -> None:
"""Set player name to ``user_name``."""
self.main.set("GJA_001", user_name)
user_name = property(get_user_name, set_user_name)
[docs] def get_password(self) -> str:
"""Player password."""
return self.main.get("GJA_002", "unknown")
[docs] def set_password(self, password: str) -> None:
"""Set player password to ``password``."""
self.main.set("GJA_002", password)
password = property(get_password, set_password)
[docs] def get_account_id(self) -> int:
"""Player Account ID, same as ``account_id`` of users."""
return self.main.get("GJA_003", 0)
[docs] def set_account_id(self, account_id: int) -> None:
"""Set player Account ID to ``account_id``."""
self.main.set("GJA_003", account_id)
account_id = property(get_account_id, set_account_id)
[docs] def get_user_id(self) -> int:
"""Player User ID, same as ``id`` of users."""
return self.main.get("playerUserID", 0)
[docs] def set_user_id(self, user_id: int) -> None:
"""Set player User ID to ``user_id``."""
self.main.set("playerUserID", user_id)
user_id = property(get_user_id, set_user_id)
[docs] def get_udid(self) -> str:
"""Player UDID."""
return self.main.get("playerUDID", "S0")
[docs] def set_udid(self, udid: str) -> None:
"""Set player UDID to ``user_id``."""
self.main.set("playerUDID", udid)
udid = property(get_udid, set_udid)
[docs] def get_bootups(self) -> int:
"""Count of game bootups."""
return self.main.get("bootups", 0)
[docs] def set_bootups(self, bootups: int) -> None:
"""Set bootups to ``bootups``."""
self.main.set("bootups", bootups)
bootups = property(get_bootups, set_bootups)
[docs] def get_followed(self) -> List[int]:
"""List of followed users."""
return list(map(int, self.main.get("GLM_06", {}).keys()))
[docs] def set_followed(self, followed: Iterable[int]) -> None:
"""Set followed users to ``followed``."""
self.main.set("GLM_06", {str(account_id): 1 for account_id in followed})
followed = property(get_followed, set_followed)
[docs] def get_values(self) -> LevelValues: # O(nm), thanks rob
""":class:`~gd.api.database.LevelValues` that represent completed levels."""
values = LevelValues.create_empty()
prefix_to_array = values.get_prefix_to_array()
for string in self.main.get("GS_completed", {}).keys():
for prefix, array in prefix_to_array.items():
id_string = remove_prefix(string, prefix)
if id_string != string:
array.append(int(id_string))
break
return values
[docs] def set_values(self, values: LevelValues) -> None:
"""Set :class:`~gd.api.database.LevelValues` to ``values``."""
mapping = {}
prefix_to_array = values.get_prefix_to_array()
for prefix, array in prefix_to_array.items():
mapping.update({f"{prefix}{value_id}": 1 for value_id in array})
self.main.set("GS_completed", mapping)
values = property(get_values, set_values)
def to_levels(self, raw_levels: Iterable[Dict[str, T]], function: str) -> "LevelCollection":
return LevelCollection.launch(
self, function, map(LevelAPI.from_data, filter(is_dict, raw_levels))
)
[docs] def load_saved_levels(self) -> "LevelCollection":
"""Load saved levels into :class:`~gd.api.LevelCollection`."""
return self.to_levels(self.main.get("GLM_03", {}).values(), "dump_saved_levels")
get_saved_levels = load_saved_levels
[docs] def dump_saved_levels(self, levels: "LevelCollection") -> None:
"""Dump saved levels from :class:`~gd.api.LevelCollection`."""
self.main.set("GLM_03", {str(level.id): level.to_data() for level in levels})
set_saved_levels = dump_saved_levels
saved_levels = property(get_saved_levels, set_saved_levels)
[docs] def load_created_levels(self) -> "LevelCollection":
"""Load created levels into :class:`~gd.api.LevelCollection`."""
return self.to_levels(self.levels.get("LLM_01", {}).values(), "dump_created_levels")
get_created_levels = load_created_levels
[docs] def dump_created_levels(self, levels: "LevelCollection") -> None:
"""Dump created levels from :class:`~gd.api.LevelCollection`."""
store = {IS_ARRAY: True}
store.update({f"k_{index}": level.to_data() for index, level in enumerate(levels)})
self.levels.set("LLM_01", store)
set_created_levels = dump_created_levels
created_levels = property(get_created_levels, set_created_levels)
[docs] @classmethod
def load(
cls,
main: Optional[PathLike] = None,
levels: Optional[PathLike] = None,
main_file: PathLike = MAIN,
levels_file: PathLike = LEVELS,
) -> "Database":
"""Load the database. See :meth:`~gd.api.SaveUtils.load` for more."""
from gd.api.loader import save # ...
return save.load(main=main, levels=levels, main_file=main_file, levels_file=levels_file)
[docs] def dump(
self,
main: Optional[PathLike] = None,
levels: Optional[PathLike] = None,
main_file: PathLike = MAIN,
levels_file: PathLike = LEVELS,
) -> None:
"""Dump the database back. See :meth:`~gd.api.SaveUtils.dump` for more."""
from gd.api.loader import save # I hate circular imports.
save.dump(
self, main=main, levels=levels, main_file=main_file, levels_file=levels_file
)
def as_tuple(self) -> Tuple[Part, Part]:
return (self.main, self.levels)
[docs]class LevelCollection(ListDerive):
"""Collection of :class:`~gd.api.LevelAPI` objects."""
def __init__(self, *args) -> None:
super().__init__(extract_iterable_from_tuple(args)) # type: ignore
self._callback: Optional[Database] = None
self._function: Optional[str] = None
[docs] def get_by_name(self, name: str) -> Optional[LevelAPI]:
"""Fetch a level by ``name``. Returns ``None`` if not found."""
return iter(self).get(name=name)
@classmethod
def launch(cls, callback: Database, function: str, iterable: Iterable[T]) -> "LevelCollection":
self = cls(iterable)
self._callback = callback
self._function = function
return self
[docs] def dump(self, database: Optional[Database] = None) -> None:
"""Dump levels to ``database``, if provided.
Otherwise, try to dump back to the database that created this collection.
"""
if database is None:
database = self._callback # type: ignore
getattr(database, self._function)(self) # type: ignore