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>)
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.