pyxora.assets

  1from .utils import engine,python
  2
  3from dataclasses import dataclass, field
  4from typing import Any,Optional
  5import os
  6
  7import pygame
  8
  9loaders = {
 10    "images":lambda path:pygame.image.load(path).convert_alpha(),
 11    "music": lambda path:path, # pygame.music loads only the last music file
 12    "sfx":lambda path:pygame.mixer.Sound(path),
 13    "fonts": lambda path: {size: pygame.font.Font(path, size) for size in
 14        {1, 2, 4, 8, 10, 12, 14, 16, 18, 24, 32, 48, 64, 72, 96, 128, 144, 192, 256}},
 15    "scenes": lambda path: python.load_class(path,python.get_filename(path).title().replace(" ", "_")),
 16    "scripts": lambda path: python.load_class(path,python.get_filename(path).title().replace(" ", "_"))
 17}
 18"""@private The loaders dictionary"""
 19
 20@dataclass
 21class Data:
 22    """The Data structure"""
 23    files: dict[str, dict[str, str]] = field(default_factory=dict)
 24    images: dict[str, Any] = field(default_factory=dict)
 25    fonts: dict[str, Any] = field(default_factory=dict)
 26    scenes: dict[str, Any] = field(default_factory=dict)
 27    scripts: dict[str, Any] = field(default_factory=dict)
 28    music: dict[str, Any] = field(default_factory=dict)
 29    sfx: dict[str, Any] = field(default_factory=dict)
 30
 31    def __repr__(self) -> str:
 32        return (
 33            f"<Loaded Data | "
 34            f"images: {len(self.images)}, "
 35            f"fonts: {len(self.fonts)}, "
 36            f"scenes: {len(self.scenes)}, "
 37            f"scripts: {len(self.scripts)}, "
 38            f"music: {len(self.music)}, "
 39            f"sfx: {len(self.sfx)}>"
 40        )
 41
 42class Assets:
 43    data = Data()
 44    """@private The game data"""
 45    engine = Data()
 46    """@private The Engine data"""
 47
 48    @classmethod
 49    def init(
 50        cls,
 51        path_images: str = None,path_fonts: str = None,
 52        path_scenes: str = None,path_scripts: str = None,
 53        path_music: str = None,path_sfx: str = None,
 54        pre_load: bool = True
 55    ) -> None:
 56        """
 57        Initialize the Assets system by loading asset files into the Data structure.
 58
 59        Args:
 60            path_images (str, optional): Path to image files.
 61            path_fonts (str, optional): Path to font files.
 62            path_scenes (str, optional): Path to scene files.
 63            path_scripts (str, optional): Path to script files.
 64            path_music (str, optional): Path to song files.
 65            path_sfx (str, optional): Path to sound effect files.
 66            pre_load (bool): Whether to preload the assets immediately. Defaults to True.
 67        """
 68        cls._load_engine_files()
 69        cls.load("engine") # always load the engine data
 70        cls.engine.fonts.update(cls.__get_default_font()) # add default font to engine
 71
 72        cls._load_data_files(
 73            path_images,path_fonts,
 74            path_scenes,path_scripts,
 75            path_music,path_sfx
 76        )
 77        pre_load and cls.load("data")
 78
 79    @classmethod
 80    def get(cls,source: str, *loc) -> Any:
 81        """
 82        Safely retrieve a nested value from a source dictionary.
 83
 84        Args:
 85            source (str): The data name to retrieve data from.
 86            *loc (str): A sequence of keys representing the path to the desired value.
 87
 88        Returns:
 89            Any: The value at the specified nested location, or None if the path is invalid.
 90
 91        Example:
 92            Assets.get("data"images", "player")  # Returns the player Surface if it exists
 93            Assets.get("engine,"images", "icon")  # Returns the engine icon Surface
 94        """
 95
 96        # return None if no location is provided
 97        # as having direct access to a dynamic attribute sounds scary lol
 98        if not loc:
 99            return None
100
101        source = getattr(cls, source)
102        target = loc[0]
103
104        data = getattr(source, target, None)
105        if data is None:
106            return None
107
108        for key in loc[1:]:
109            if not isinstance(data, dict):
110                return None
111            data = data.get(key)
112
113        return data
114
115    @classmethod
116    def load(cls, source: "str") -> None:
117        """
118        Load file paths into the data system
119
120        Args:
121            source (str): The data name to retrieve data from.
122        """
123
124        data = getattr(cls, source)
125        for category, loader in loaders.items():
126            file_dict = data.files.get(category)
127            if not file_dict:
128                continue
129
130            asset_store = getattr(data, category)
131            for name, path in file_dict.items():
132                asset_store[name] = loader(path)
133
134    @classmethod
135    def _load_data_files(cls,
136        path_images: str,path_fonts: str ,
137        path_scenes: str,path_scripts: str,
138        path_music: str,path_sfx: str) -> None:
139        """
140        This method scans each provided directory path and organizes the discovered files
141        into a structured dictionary (e.g., `Data.files`).
142
143        Args:
144            path_images (str): Path to image files.
145            path_fonts (str): Path to font files.
146            path_scenes (str): Path to scene files.
147            path_scripts (str): Path to script files.
148            path_music (str): Path to music files.
149            path_sfx (str): Path to sound effect files.
150        """
151
152
153        paths = {}
154        if path_images is not None:
155            paths["images"] = cls.__get_full_path(path_images)
156
157        if path_fonts is not None:
158            paths["fonts"] = cls.__get_full_path(path_fonts)
159
160        if path_music is not None:
161            paths["music"] = cls.__get_full_path(path_music)
162
163        if path_sfx is not None:
164            paths["sfx"] = cls.__get_full_path(path_sfx)
165
166        if path_scenes is not None:
167            paths["scenes"] = cls.__get_full_path(path_scenes)
168
169        if path_scripts is not None:
170            paths["scripts"] = cls.__get_full_path(path_scripts)
171
172        cls.data.files = cls.__get_all_files(paths)
173
174    @classmethod
175    def _load_engine_files(cls) -> None:
176        """Same as _load_data_files but for engine assets."""
177        base = os.path.dirname(__file__)
178        path_images = os.path.normpath(os.path.join(base,"data","images"))
179        paths = {
180            "images": path_images,
181            # More in the Future?
182            # Like basic build-in scripts to speed up development
183        }
184
185        cls.engine.files = cls.__get_all_files(paths)
186
187    @staticmethod
188    def __get_default_font() -> dict[str, dict[int, pygame.font.Font]]:
189        """
190        Loads the default system font in a variety of common sizes.
191
192        Returns:
193            dict[str, dict[int, pygame.font.Font]]:
194                A dictionary mapping the default font name to another dictionary
195                that maps font sizes to `pygame.font.Font` objects.
196        """
197        name = pygame.font.get_default_font().split(".")[0]
198        sizes = {
199            size: pygame.font.SysFont(name, size) for size in
200            {1, 2, 4, 8, 10, 12, 14, 16, 18, 24, 32, 48, 64, 72, 96, 128, 144, 192, 256}
201        }
202        return {name: sizes}
203
204    @staticmethod
205    def __get_all_files(path,ignore=None) -> dict[str, dict[str, str]]:
206        """
207        Recursively scan provided directories and build a nested dictionary of file paths.
208
209        Args:
210            path (dict[str, str]): A dictionary where each key is an asset type
211                                (like "images" or "fonts") and the value is the path to its folder.
212            ignore (set[str], optional): A set of folder names to exclude from scanning.
213                                        "__pycache__" is always ignored.
214
215        Returns:
216            dict[str, dict[str, str]]: A nested dictionary structured like:
217                {
218                    "images": {
219                        "player": "/path/to/images/player.png",
220                        "enemy": "/path/to/images/enemy.png"
221                    },
222                    "fonts": {
223                        "main": "/path/to/fonts/arial.ttf"
224                    },
225                    ...
226                }
227        """
228        if not ignore:
229            ignore = set()
230
231        ignore.update({"__pycache__"})
232
233        data = {}
234        for key,value in path.items():
235            for root, dirs, files in os.walk(value,topdown=False):
236                ftype = os.path.basename(root)
237                if ftype in ignore: continue
238                data[key] = {}
239                for file in files:
240                    full_path = os.path.join(root, file)
241                    name,_ = os.path.splitext(os.path.basename(full_path))
242                    data[key][name] = full_path
243
244        return data
245
246    @staticmethod
247    def __get_full_path(path):
248        """
249        Convert a relative path to an absolute normalized path and verify it exists.
250
251        Args:
252            path (str): The relative or partial path to validate.
253
254        Returns:
255            str: The normalized absolute path.
256
257        Raises:
258            OSError: If the resolved path does not exist.
259        """
260        path = os.path.normpath(os.getcwd()+path)
261        if not os.path.exists(path):
262            engine.error(OSError(f"The path doesn't exist: {path}"))
263            engine.quit()
264        return path
@dataclass
class Data:
21@dataclass
22class Data:
23    """The Data structure"""
24    files: dict[str, dict[str, str]] = field(default_factory=dict)
25    images: dict[str, Any] = field(default_factory=dict)
26    fonts: dict[str, Any] = field(default_factory=dict)
27    scenes: dict[str, Any] = field(default_factory=dict)
28    scripts: dict[str, Any] = field(default_factory=dict)
29    music: dict[str, Any] = field(default_factory=dict)
30    sfx: dict[str, Any] = field(default_factory=dict)
31
32    def __repr__(self) -> str:
33        return (
34            f"<Loaded Data | "
35            f"images: {len(self.images)}, "
36            f"fonts: {len(self.fonts)}, "
37            f"scenes: {len(self.scenes)}, "
38            f"scripts: {len(self.scripts)}, "
39            f"music: {len(self.music)}, "
40            f"sfx: {len(self.sfx)}>"
41        )

The Data structure

Data( files: dict[str, dict[str, str]] = <factory>, images: dict[str, typing.Any] = <factory>, fonts: dict[str, typing.Any] = <factory>, scenes: dict[str, typing.Any] = <factory>, scripts: dict[str, typing.Any] = <factory>, music: dict[str, typing.Any] = <factory>, sfx: dict[str, typing.Any] = <factory>)
files: dict[str, dict[str, str]]
images: dict[str, typing.Any]
fonts: dict[str, typing.Any]
scenes: dict[str, typing.Any]
scripts: dict[str, typing.Any]
music: dict[str, typing.Any]
sfx: dict[str, typing.Any]
class Assets:
 43class Assets:
 44    data = Data()
 45    """@private The game data"""
 46    engine = Data()
 47    """@private The Engine data"""
 48
 49    @classmethod
 50    def init(
 51        cls,
 52        path_images: str = None,path_fonts: str = None,
 53        path_scenes: str = None,path_scripts: str = None,
 54        path_music: str = None,path_sfx: str = None,
 55        pre_load: bool = True
 56    ) -> None:
 57        """
 58        Initialize the Assets system by loading asset files into the Data structure.
 59
 60        Args:
 61            path_images (str, optional): Path to image files.
 62            path_fonts (str, optional): Path to font files.
 63            path_scenes (str, optional): Path to scene files.
 64            path_scripts (str, optional): Path to script files.
 65            path_music (str, optional): Path to song files.
 66            path_sfx (str, optional): Path to sound effect files.
 67            pre_load (bool): Whether to preload the assets immediately. Defaults to True.
 68        """
 69        cls._load_engine_files()
 70        cls.load("engine") # always load the engine data
 71        cls.engine.fonts.update(cls.__get_default_font()) # add default font to engine
 72
 73        cls._load_data_files(
 74            path_images,path_fonts,
 75            path_scenes,path_scripts,
 76            path_music,path_sfx
 77        )
 78        pre_load and cls.load("data")
 79
 80    @classmethod
 81    def get(cls,source: str, *loc) -> Any:
 82        """
 83        Safely retrieve a nested value from a source dictionary.
 84
 85        Args:
 86            source (str): The data name to retrieve data from.
 87            *loc (str): A sequence of keys representing the path to the desired value.
 88
 89        Returns:
 90            Any: The value at the specified nested location, or None if the path is invalid.
 91
 92        Example:
 93            Assets.get("data"images", "player")  # Returns the player Surface if it exists
 94            Assets.get("engine,"images", "icon")  # Returns the engine icon Surface
 95        """
 96
 97        # return None if no location is provided
 98        # as having direct access to a dynamic attribute sounds scary lol
 99        if not loc:
100            return None
101
102        source = getattr(cls, source)
103        target = loc[0]
104
105        data = getattr(source, target, None)
106        if data is None:
107            return None
108
109        for key in loc[1:]:
110            if not isinstance(data, dict):
111                return None
112            data = data.get(key)
113
114        return data
115
116    @classmethod
117    def load(cls, source: "str") -> None:
118        """
119        Load file paths into the data system
120
121        Args:
122            source (str): The data name to retrieve data from.
123        """
124
125        data = getattr(cls, source)
126        for category, loader in loaders.items():
127            file_dict = data.files.get(category)
128            if not file_dict:
129                continue
130
131            asset_store = getattr(data, category)
132            for name, path in file_dict.items():
133                asset_store[name] = loader(path)
134
135    @classmethod
136    def _load_data_files(cls,
137        path_images: str,path_fonts: str ,
138        path_scenes: str,path_scripts: str,
139        path_music: str,path_sfx: str) -> None:
140        """
141        This method scans each provided directory path and organizes the discovered files
142        into a structured dictionary (e.g., `Data.files`).
143
144        Args:
145            path_images (str): Path to image files.
146            path_fonts (str): Path to font files.
147            path_scenes (str): Path to scene files.
148            path_scripts (str): Path to script files.
149            path_music (str): Path to music files.
150            path_sfx (str): Path to sound effect files.
151        """
152
153
154        paths = {}
155        if path_images is not None:
156            paths["images"] = cls.__get_full_path(path_images)
157
158        if path_fonts is not None:
159            paths["fonts"] = cls.__get_full_path(path_fonts)
160
161        if path_music is not None:
162            paths["music"] = cls.__get_full_path(path_music)
163
164        if path_sfx is not None:
165            paths["sfx"] = cls.__get_full_path(path_sfx)
166
167        if path_scenes is not None:
168            paths["scenes"] = cls.__get_full_path(path_scenes)
169
170        if path_scripts is not None:
171            paths["scripts"] = cls.__get_full_path(path_scripts)
172
173        cls.data.files = cls.__get_all_files(paths)
174
175    @classmethod
176    def _load_engine_files(cls) -> None:
177        """Same as _load_data_files but for engine assets."""
178        base = os.path.dirname(__file__)
179        path_images = os.path.normpath(os.path.join(base,"data","images"))
180        paths = {
181            "images": path_images,
182            # More in the Future?
183            # Like basic build-in scripts to speed up development
184        }
185
186        cls.engine.files = cls.__get_all_files(paths)
187
188    @staticmethod
189    def __get_default_font() -> dict[str, dict[int, pygame.font.Font]]:
190        """
191        Loads the default system font in a variety of common sizes.
192
193        Returns:
194            dict[str, dict[int, pygame.font.Font]]:
195                A dictionary mapping the default font name to another dictionary
196                that maps font sizes to `pygame.font.Font` objects.
197        """
198        name = pygame.font.get_default_font().split(".")[0]
199        sizes = {
200            size: pygame.font.SysFont(name, size) for size in
201            {1, 2, 4, 8, 10, 12, 14, 16, 18, 24, 32, 48, 64, 72, 96, 128, 144, 192, 256}
202        }
203        return {name: sizes}
204
205    @staticmethod
206    def __get_all_files(path,ignore=None) -> dict[str, dict[str, str]]:
207        """
208        Recursively scan provided directories and build a nested dictionary of file paths.
209
210        Args:
211            path (dict[str, str]): A dictionary where each key is an asset type
212                                (like "images" or "fonts") and the value is the path to its folder.
213            ignore (set[str], optional): A set of folder names to exclude from scanning.
214                                        "__pycache__" is always ignored.
215
216        Returns:
217            dict[str, dict[str, str]]: A nested dictionary structured like:
218                {
219                    "images": {
220                        "player": "/path/to/images/player.png",
221                        "enemy": "/path/to/images/enemy.png"
222                    },
223                    "fonts": {
224                        "main": "/path/to/fonts/arial.ttf"
225                    },
226                    ...
227                }
228        """
229        if not ignore:
230            ignore = set()
231
232        ignore.update({"__pycache__"})
233
234        data = {}
235        for key,value in path.items():
236            for root, dirs, files in os.walk(value,topdown=False):
237                ftype = os.path.basename(root)
238                if ftype in ignore: continue
239                data[key] = {}
240                for file in files:
241                    full_path = os.path.join(root, file)
242                    name,_ = os.path.splitext(os.path.basename(full_path))
243                    data[key][name] = full_path
244
245        return data
246
247    @staticmethod
248    def __get_full_path(path):
249        """
250        Convert a relative path to an absolute normalized path and verify it exists.
251
252        Args:
253            path (str): The relative or partial path to validate.
254
255        Returns:
256            str: The normalized absolute path.
257
258        Raises:
259            OSError: If the resolved path does not exist.
260        """
261        path = os.path.normpath(os.getcwd()+path)
262        if not os.path.exists(path):
263            engine.error(OSError(f"The path doesn't exist: {path}"))
264            engine.quit()
265        return path
@classmethod
def init( cls, path_images: str = None, path_fonts: str = None, path_scenes: str = None, path_scripts: str = None, path_music: str = None, path_sfx: str = None, pre_load: bool = True) -> None:
49    @classmethod
50    def init(
51        cls,
52        path_images: str = None,path_fonts: str = None,
53        path_scenes: str = None,path_scripts: str = None,
54        path_music: str = None,path_sfx: str = None,
55        pre_load: bool = True
56    ) -> None:
57        """
58        Initialize the Assets system by loading asset files into the Data structure.
59
60        Args:
61            path_images (str, optional): Path to image files.
62            path_fonts (str, optional): Path to font files.
63            path_scenes (str, optional): Path to scene files.
64            path_scripts (str, optional): Path to script files.
65            path_music (str, optional): Path to song files.
66            path_sfx (str, optional): Path to sound effect files.
67            pre_load (bool): Whether to preload the assets immediately. Defaults to True.
68        """
69        cls._load_engine_files()
70        cls.load("engine") # always load the engine data
71        cls.engine.fonts.update(cls.__get_default_font()) # add default font to engine
72
73        cls._load_data_files(
74            path_images,path_fonts,
75            path_scenes,path_scripts,
76            path_music,path_sfx
77        )
78        pre_load and cls.load("data")

Initialize the Assets system by loading asset files into the Data structure.

Arguments:
  • path_images (str, optional): Path to image files.
  • path_fonts (str, optional): Path to font files.
  • path_scenes (str, optional): Path to scene files.
  • path_scripts (str, optional): Path to script files.
  • path_music (str, optional): Path to song files.
  • path_sfx (str, optional): Path to sound effect files.
  • pre_load (bool): Whether to preload the assets immediately. Defaults to True.
@classmethod
def get(cls, source: str, *loc) -> Any:
 80    @classmethod
 81    def get(cls,source: str, *loc) -> Any:
 82        """
 83        Safely retrieve a nested value from a source dictionary.
 84
 85        Args:
 86            source (str): The data name to retrieve data from.
 87            *loc (str): A sequence of keys representing the path to the desired value.
 88
 89        Returns:
 90            Any: The value at the specified nested location, or None if the path is invalid.
 91
 92        Example:
 93            Assets.get("data"images", "player")  # Returns the player Surface if it exists
 94            Assets.get("engine,"images", "icon")  # Returns the engine icon Surface
 95        """
 96
 97        # return None if no location is provided
 98        # as having direct access to a dynamic attribute sounds scary lol
 99        if not loc:
100            return None
101
102        source = getattr(cls, source)
103        target = loc[0]
104
105        data = getattr(source, target, None)
106        if data is None:
107            return None
108
109        for key in loc[1:]:
110            if not isinstance(data, dict):
111                return None
112            data = data.get(key)
113
114        return data

Safely retrieve a nested value from a source dictionary.

Arguments:
  • source (str): The data name to retrieve data from.
  • *loc (str): A sequence of keys representing the path to the desired value.
Returns:

Any: The value at the specified nested location, or None if the path is invalid.

Example:

Assets.get("data"images", "player") # Returns the player Surface if it exists Assets.get("engine,"images", "icon") # Returns the engine icon Surface

@classmethod
def load(cls, source: str) -> None:
116    @classmethod
117    def load(cls, source: "str") -> None:
118        """
119        Load file paths into the data system
120
121        Args:
122            source (str): The data name to retrieve data from.
123        """
124
125        data = getattr(cls, source)
126        for category, loader in loaders.items():
127            file_dict = data.files.get(category)
128            if not file_dict:
129                continue
130
131            asset_store = getattr(data, category)
132            for name, path in file_dict.items():
133                asset_store[name] = loader(path)

Load file paths into the data system

Arguments:
  • source (str): The data name to retrieve data from.