# libstormgames A Python library to make creating audio games easier. ## Overview `libstormgames` provides a comprehensive set of tools for developing accessible games with audio-first design. It handles common game development tasks including: - Sound and music playback with positional audio - Text-to-speech integration - Configuration management - Score tracking and high score tables - Input handling and keyboard controls - Menu systems and text display - GUI initialization - **NEW**: Statistics tracking with level/total separation - **NEW**: Save/load management with atomic operations - **NEW**: Combat systems with weapons and projectiles ## Installation ### Requirements - pygame>=2.0.0 - pyperclip>=1.8.0 - requests>=2.25.0 - pyxdg>=0.27 - setproctitle>=1.2.0 - numpy>=1.19.0 - wxpython #### Speech Providers (one required) - Linux/Unix: `python-speechd` or `accessible-output2>=0.14` - Windows/macOS: `accessible-output2>=0.14` ### Install from source If you are on Linux, check your package manager first to see if the packages in requirements.txt are available. ```bash git clone https://git.stormux.org/storm/libstormgames cd libstormgames pip install -e . ``` ## Getting Started You can use libstormgames in two ways: the traditional function-based approach or the new class-based approach. ### Traditional Function-Based Approach ```python #!/usr/bin/env python3 import libstormgames as sg def main(): # Initialize the game sounds = sg.initialize_gui("My First Audio Game") # Welcome message sg.speak("Welcome to My First Audio Game!") # Create a scoreboard scoreboard = sg.Scoreboard() # Main game loop def play_game(): sg.speak("Game started!") scoreboard.increase_score(10) sg.speak(f"Your score is {scoreboard.get_score()}") scoreboard.add_high_score() return "menu" # Return to menu after game ends # Define menu options while True: choice = sg.game_menu(sounds) if choice == "play": play_game() if __name__ == "__main__": main() ``` ### Modern Class-Based Approach ```python #!/usr/bin/env python3 import libstormgames as sg def main(): # Create and initialize a game game = sg.Game("My First Audio Game").initialize() # Welcome message game.speak("Welcome to My First Audio Game!") # Main game loop def play_game(): game.speak("Game started!") game.scoreboard.increase_score(10) game.speak(f"Your score is {game.scoreboard.get_score()}") game.scoreboard.add_high_score() return "menu" # Define menu options while True: choice = sg.game_menu(game.sound.get_sounds()) if choice == "play": play_game() if __name__ == "__main__": main() ``` ## Library Structure The library is organized into modules, each with a specific focus: - **config**: Configuration management - **services**: Core services that replace global variables - **sound**: Sound and music playback - **speech**: Text-to-speech functionality - **scoreboard**: High score tracking - **input**: Input handling and dialogs - **display**: Text display and GUI functionality - **menu**: Menu systems - **utils**: Utility functions and Game class - **stat_tracker**: Statistics tracking system - **save_manager**: Save/load management - **combat**: Weapon and projectile systems ## Core Classes ### Game The Game class provides a central way to manage all game systems: ```python # Create and initialize a game game = sg.Game("My Game").initialize() # Use fluent API for chaining commands game.speak("Hello").play_bgm("music/theme") # Access components directly game.scoreboard.increase_score(10) game.sound.play_random("explosion") # Display text game.display_text(["Line 1", "Line 2"]) # Clean exit game.exit() ``` The initialization process sets up proper configuration paths: - Creates game-specific directories if they don't exist - Ensures all services have access to correct paths - Connects all components for seamless operation ### Services The library includes several service classes that replace global variables: ```python # Volume service manages all volume settings volume = sg.VolumeService.get_instance() volume.adjust_master_volume(0.1) # Path service manages file paths paths = sg.PathService.get_instance() print(paths.game_path) # Config service manages configuration config = sg.ConfigService.get_instance() ``` ### Config Handles configuration file management with local and global settings. ```python # Create a configuration manager config = sg.Config("My Game") # Access the local configuration config.local_config.add_section("settings") config.local_config.set("settings", "difficulty", "easy") config.write_local_config() # Read settings config.read_local_config() difficulty = config.local_config.get("settings", "difficulty") ``` Configuration files are automatically stored in standard system locations: - Linux/Unix: `~/.config/storm-games/` - Windows: `%APPDATA%\storm-games\` - macOS: `~/Library/Application Support/storm-games/` Each game has its own subdirectory based on the game name (lowercase with hyphens), for example, Wicked Quest becomes wicked-quest. ### Sound Manages sound loading and playback with positional audio support. ```python # Create a sound manager sound_system = sg.Sound("sounds/") # Get the dictionary of loaded sounds sounds = sound_system.get_sounds() # Play a sound sg.play_sound(sounds["explosion"]) # Play a sound with positional audio (player at x=5, object at x=10) channel = sg.obj_play(sounds, "footsteps", 5, 10) # Update sound position as player or object moves channel = sg.obj_update(channel, 6, 10) # Player moved to x=6 # Stop the sound channel = sg.obj_stop(channel) # Play background music sg.play_bgm("sounds/background.ogg") # Adjust volume sg.adjust_master_volume(0.1) # Increase master volume sg.adjust_bgm_volume(-0.1) # Decrease background music volume sg.adjust_sfx_volume(0.1) # Increase sound effects volume ``` ### Speech Provides text-to-speech functionality using available speech providers. ```python # Create a speech manager (usually you'll use the global instance) speech = sg.Speech() # Speak text speech.speak("Hello, world!") # Or use the global function for convenience sg.speak("Hello, world!") # Speak without interrupting previous speech sg.speak("This won't interrupt", interrupt=False) # Clean up when done exit_game() or as the class, game.exit_game() ``` ### Scoreboard Tracks scores and manages high score tables. ```python # Create a scoreboard scoreboard = sg.Scoreboard() # Manipulate score scoreboard.increase_score(10) scoreboard.decrease_score(5) current_score = scoreboard.get_score() # Check for high score position = scoreboard.check_high_score() if position: print(f"You're in position {position}!") # Add high score (prompts for player name) scoreboard.add_high_score() # Get all high scores high_scores = scoreboard.get_high_scores() for entry in high_scores: print(f"{entry['name']}: {entry['score']}") ``` The Scoreboard system automatically manages high score data in the game's configuration directory: - Linux/Unix: `~/.config/storm-games/game-name/config.ini` - Windows: `%APPDATA%\storm-games\game-name\config.ini` - macOS: `~/Library/Application Support/storm-games/game-name/config.ini` Where `game-name` is the lowercase, hyphenated version of your game title. For example, "My Awesome Game" would use the directory `my-awesome-game`. ### StatTracker Flexible statistics tracking with separate level and total counters: ```python # Initialize with default stats stats = sg.StatTracker({ "kills": 0, "deaths": 0, "score": 0, "time_played": 0.0 }) # Update stats during gameplay stats.update_stat("kills", 1) # Increment kills stats.update_stat("time_played", 1.5) # Add playtime # Access current values level_kills = stats.get_stat("kills") # Current level kills total_kills = stats.get_stat("kills", from_total=True) # All-time kills # Reset level stats for new level (keeps totals) stats.reset_level() # Add new stats dynamically stats.add_stat("boss_kills", 0) # Serialization for save/load stats_data = stats.to_dict() restored_stats = sg.StatTracker.from_dict(stats_data) ``` ### SaveManager Atomic save/load operations with corruption detection: ```python # Initialize save manager save_manager = sg.SaveManager("my-rpg-game") # Create a comprehensive save game_state = { "player": { "level": 5, "hp": 80, "inventory": ["sword", "health_potion"] }, "world": { "current_area": "enchanted_forest", "completed_quests": ["tutorial", "first_boss"] }, "stats": stats.to_dict() # Include StatTracker data } # Add metadata for save selection screen metadata = { "display_name": "Enchanted Forest - Level 5", "level": 5, "playtime": "2.5 hours" } # Create the save (atomic operation) save_path = save_manager.create_save(game_state, metadata) # List and load saves save_files = save_manager.get_save_files() # Newest first if save_files: loaded_data, loaded_metadata = save_manager.load_save(save_files[0]) restored_stats = sg.StatTracker.from_dict(loaded_data["stats"]) ``` ### Combat Systems #### Weapons ```python # Create weapons using factory methods sword = sg.Weapon.create_sword("Iron Sword", damage=15) dagger = sg.Weapon.create_dagger("Steel Dagger", damage=12) # Custom weapon with stat bonuses bow = sg.Weapon( name="Elvish Bow", damage=18, range_value=8, cooldown=600, stat_bonuses={"speed": 1.15} ) # Combat usage if sword.can_attack(): # Check cooldown damage = sword.attack() # Start attack if sword.can_hit_target(enemy_pos, player_pos, facing_right, "enemy1"): actual_damage = sword.hit_target("enemy1") sg.speak(f"Hit for {actual_damage} damage!") ``` #### Projectiles ```python # Create projectiles arrow = sg.Projectile.create_arrow( start_pos=(player_x, player_y), direction=(1, 0) # Moving right ) # Game loop integration active_projectiles = [] def update_projectiles(): for projectile in active_projectiles[:]: if not projectile.update(): # Move and check range active_projectiles.remove(projectile) continue # Check enemy collisions for enemy in enemies: if projectile.check_collision(enemy.position, enemy.size): damage = projectile.hit() enemy.take_damage(damage) active_projectiles.remove(projectile) break ``` ## Key Functions ### Game Initialization and Control ```python # Initialize game systems (traditional approach) sounds = sg.initialize_gui("Game Title") # Or use the Game class (modern approach) game = sg.Game("Game Title").initialize() # Pause the game (freezes until user presses backspace) sg.pause_game() # Check if user wants to exit if sg.check_for_exit(): sg.exit_game() # Exit the game properly sg.exit_game() ``` ### Menu System ```python # Basic menu example (returns option string) choice = sg.game_menu(sounds) if choice == "play": # Start the game play_game() elif choice == "instructions": sg.instructions() # etc... # Menu with play callback (callback is executed directly) def play_callback(): sg.speak("Game started with callback!") # Game code goes here sg.game_menu(sounds, play_callback) # Menu with custom options choice = sg.game_menu(sounds, None, "practice_mode") if choice == "practice_mode": # Handle practice mode practice_game() elif choice == "difficulty": # Handle difficulty settings set_difficulty() elif choice == "options": # Handle options screen show_options() # etc... ``` ### Complete Menu Example ```python import libstormgames as sg import pygame def main(): # Initialize the game sounds = sg.initialize_gui("Menu Example Game") # Play menu music sg.play_bgm("sounds/music_menu.ogg") # Define play function with callback def play_game(): sg.speak("Starting game!") # Game code here sg.speak("Game over!") return "menu" # Return to menu after game ends # Define custom menu option handlers def practice_mode(): sg.speak("Starting practice mode!") # Practice mode code here return "menu" def difficulty_settings(): # Show difficulty options options = ["easy", "normal", "hard", "back"] current = 0 while True: sg.speak(options[current]) event = pygame.event.wait() if event.type == pygame.KEYDOWN: if event.key == pygame.K_UP and current > 0: current -= 1 elif event.key == pygame.K_DOWN and current < len(options) - 1: current += 1 elif event.key == pygame.K_RETURN: if options[current] == "back": return "menu" sg.speak(f"Difficulty set to {options[current]}") return "menu" elif event.key == pygame.K_ESCAPE: return "menu" # Main menu loop while True: # Display menu with play callback and custom options choice = sg.game_menu(sounds, play_game, "practice_mode", "difficulty") # Handle menu choices if choice == "practice_mode": practice_mode() elif choice == "difficulty": difficulty_settings() # Other options are handled automatically by game_menu if __name__ == "__main__": main() ``` ### Class-Based Menu Example ```python import libstormgames as sg import pygame class MyGame: def __init__(self): # Create and initialize game self.game = sg.Game("Class-Based Menu Example").initialize() self.sounds = self.game.sound.get_sounds() def play_game(self): self.game.speak("Starting game!") # Game code here self.game.speak("Game over!") return "menu" def practice_mode(self): self.game.speak("Starting practice mode!") # Practice mode code here return "menu" def settings(self): # A nested menu example submenu_options = ["graphics", "audio", "controls", "back"] current = 0 while True: self.game.speak(submenu_options[current]) event = pygame.event.wait() if event.type == pygame.KEYDOWN: if event.key == pygame.K_UP and current > 0: current -= 1 elif event.key == pygame.K_DOWN and current < len(submenu_options) - 1: current += 1 elif event.key == pygame.K_RETURN: if submenu_options[current] == "back": return "menu" self.game.speak(f"Selected {submenu_options[current]} settings") return "menu" elif event.key == pygame.K_ESCAPE: return "menu" def run(self): # Main menu loop while True: # Use playCallback parameter to directly call play_game when "play" is selected choice = sg.game_menu(self.sounds, None, "practice_mode", "settings") # Handle other menu options if choice == "practice_mode": self.practice_mode() elif choice == "settings": self.settings() # Run the game if __name__ == "__main__": game = MyGame() game.run() ``` ### Text Display ```python # Display text with navigation sg.display_text([ "Line 1 of instructions", "Line 2 of instructions", "Line 3 of instructions" ]) # Display a simple message box sg.messagebox("Game Over! You scored 100 points.") # Get text input from user name = sg.get_input("Enter your name:", "Player") ``` ### Sound Effects ```python # Play a random variation of a sound sg.play_random(sounds, "explosion") # Play positional sound with distance-based volume sg.play_random_positional(sounds, "footsteps", player_x=5, object_x=10) # Play directional sound (simplified left/right positioning) sg.play_directional_sound(sounds, "voice", player_x=5, object_x=10) # Play a sound as a cutscene (interrupts other sounds, waits until complete) sg.cut_scene(sounds, "intro_speech") # Play or update a falling sound channel = sg.play_random_falling(sounds, "rock", player_x=5, object_x=8, start_y=10, currentY=5) ``` ### Utility Functions ```python # Check for game updates update_info = sg.check_for_updates("1.0.0", "My Game", "https://example.com/version.json") # Check compatibility with library version is_compatible = sg.check_compatibility("1.0.0", "1.2.3") # Sanitize a filename for any OS safe_name = sg.sanitize_filename("User's File.txt") # Calculate distance between points distance = sg.distance_2d(x1=5, y1=10, x2=8, y2=15) # Interpolation functions mid_value = sg.lerp(start=0, end=10, factor=0.5) # Returns 5.0 smooth_value = sg.smooth_step(edge0=0, edge1=10, x=5) # Smooth transition ``` ## Advanced Examples ### Using the Game Class (Modern Approach) ```python import libstormgames as sg import pygame import random def main(): # Create and initialize the game game = sg.Game("My Advanced Game").initialize() # Set up game environment game.play_bgm("sounds/background.ogg") # Main game loop running = True player_x = 5 while running: # Process events for event in pygame.event.get(): if event.type == pygame.KEYDOWN: if event.key == pygame.K_ESCAPE: running = False elif event.key == pygame.K_SPACE: # Score points game.scoreboard.increase_score(5) game.speak(f"Score: {game.scoreboard.get_score()}") # Update game state player_x += random.uniform(-0.2, 0.2) # Add random ambient sounds if random.random() < 0.05: sounds = game.sound.get_sounds() if "ambient" in sounds: sg.play_random_positional(sounds, "ambient", player_x, player_x + random.uniform(-5, 5)) pygame.time.delay(50) # Game over and cleanup game.speak("Game over!") game.exit() if __name__ == "__main__": main() ``` ### Complex Sound Environment ```python import libstormgames as sg import time import random def create_sound_environment(player_x, player_y): sounds = sg.initialize_gui("Sound Environment Demo") # Place sound sources water_x, water_y = 10, 5 fire_x, fire_y = 15, 8 wind_x, wind_y = 3, 12 # Play ambient sounds water_channel = sg.obj_play(sounds, "water", player_x, water_x) fire_channel = sg.obj_play(sounds, "fire", player_x, fire_x) wind_channel = sg.obj_play(sounds, "wind", player_x, wind_x) # Main loop running = True while running: # Simulate player movement player_x += random.uniform(-0.5, 0.5) player_y += random.uniform(-0.5, 0.5) # Update sound positions water_channel = sg.obj_update(water_channel, player_x, water_x) fire_channel = sg.obj_update(fire_channel, player_x, fire_x) wind_channel = sg.obj_update(wind_channel, player_x, wind_x) # Occasionally play random sound if random.random() < 0.1: sg.play_random_positional(sounds, "creature", player_x, player_x + random.uniform(-5, 5)) # Check for exit if sg.check_for_exit(): running = False time.sleep(0.1) # Clean up sg.obj_stop(water_channel) sg.obj_stop(fire_channel) sg.obj_stop(wind_channel) sg.exit_game() ``` ### Complete Game Structure with New Systems ```python import libstormgames as sg import pygame import random class ModernRPGGame: def __init__(self): # Create a Game instance that manages all subsystems self.game = sg.Game("Modern RPG Demo").initialize() # Initialize new game systems self.player_stats = sg.StatTracker({ "level": 1, "exp": 0, "hp": 100, "mp": 50, "kills": 0, "deaths": 0, "playtime": 0.0, "items_found": 0, "gold": 0 }) self.save_manager = sg.SaveManager("modern-rpg-demo") # Combat system self.player_weapon = sg.Weapon.create_sword("Starting Sword", damage=10) self.projectiles = [] self.enemies = [] # Game state self.player_x = 5 self.player_y = 5 self.current_area = "village" self.difficulty = "normal" # Load settings try: self.difficulty = self.game.configService.localConfig.get("settings", "difficulty") except: self.game.configService.localConfig.add_section("settings") self.game.configService.localConfig.set("settings", "difficulty", "normal") self.game.configService.write_local_config() def play_game(self): """Main game loop demonstrating new systems.""" self.game.speak(f"Starting game on {self.difficulty} difficulty") self.game.play_bgm("sounds/game_music.ogg") start_time = pygame.time.get_ticks() # Game loop running = True while running: current_time = pygame.time.get_ticks() # Update playtime stats playtime_hours = (current_time - start_time) / 3600000.0 # Convert to hours self.player_stats.set_stat("playtime", playtime_hours, level_only=True) # Handle input for event in pygame.event.get(): if event.type == pygame.KEYDOWN: if event.key == pygame.K_ESCAPE: running = False elif event.key == pygame.K_SPACE: # Combat example self.player_attack() elif event.key == pygame.K_s: # Quick save self.quick_save() elif event.key == pygame.K_l: # Quick load self.quick_load() # Update game systems self.update_combat() self.update_player_stats() # Random events if random.random() < 0.01: self.random_encounter() pygame.time.delay(50) # Game over - update total stats total_playtime = self.player_stats.get_stat("playtime", from_total=True) self.game.speak(f"Session ended. Total playtime: {total_playtime:.1f} hours") return "menu" def player_attack(self): """Handle player combat.""" if self.player_weapon.can_attack(): damage = self.player_weapon.attack() self.game.speak("Attack!") # Simulate hitting enemies if self.enemies and self.player_weapon.is_attack_active(): enemy = self.enemies[0] # Attack first enemy if self.player_weapon.can_hit_target( enemy.position, (self.player_x, self.player_y), facing_right=True, target_id=enemy.id ): actual_damage = self.player_weapon.hit_target(enemy.id) self.player_stats.update_stat("damage_dealt", actual_damage) # Remove enemy if defeated if enemy.take_damage(actual_damage): self.enemies.remove(enemy) self.player_stats.update_stat("kills", 1) self.player_stats.update_stat("exp", 25) self.game.speak("Enemy defeated!") def update_combat(self): """Update combat systems.""" # Update projectiles for projectile in self.projectiles[:]: if not projectile.update(): self.projectiles.remove(projectile) continue # Check enemy collisions for enemy in self.enemies[:]: if projectile.check_collision(enemy.position, enemy.size): damage = projectile.hit() if enemy.take_damage(damage): self.enemies.remove(enemy) self.player_stats.update_stat("kills", 1) self.projectiles.remove(projectile) break def update_player_stats(self): """Handle player progression.""" exp = self.player_stats.get_stat("exp") level = self.player_stats.get_stat("level") # Level up check exp_needed = level * 100 if exp >= exp_needed: self.player_stats.set_stat("level", level + 1) self.player_stats.set_stat("exp", exp - exp_needed) self.player_stats.set_stat("hp", 100) # Full heal on level up self.game.speak(f"Level up! Now level {level + 1}!") def random_encounter(self): """Create random encounters.""" self.game.speak("An enemy appears!") # Add enemy logic here self.player_stats.update_stat("encounters", 1) def quick_save(self): """Create a quick save.""" try: save_name = f"Quick Save - Level {self.player_stats.get_stat('level')}" self.create_complete_save(save_name) self.game.speak("Game saved!") except Exception as e: self.game.speak(f"Save failed: {e}") def quick_load(self): """Load the most recent save.""" try: saves = self.save_manager.get_save_files() if saves: self.load_complete_save(saves[0]) self.game.speak("Game loaded!") else: self.game.speak("No saves found!") except Exception as e: self.game.speak(f"Load failed: {e}") def create_complete_save(self, save_name): """Create comprehensive save with all systems.""" complete_state = { "player_stats": self.player_stats.to_dict(), "weapon": self.player_weapon.to_dict(), "player_position": (self.player_x, self.player_y), "current_area": self.current_area, "difficulty": self.difficulty, "enemies": [enemy.to_dict() for enemy in self.enemies], "projectiles": [proj.to_dict() for proj in self.projectiles] } metadata = { "display_name": save_name, "level": self.player_stats.get_stat("level"), "location": self.current_area, "playtime": f"{self.player_stats.get_stat('playtime', from_total=True):.1f}h" } return self.save_manager.create_save(complete_state, metadata) def load_complete_save(self, save_path): """Load comprehensive save restoring all systems.""" data, metadata = self.save_manager.load_save(save_path) # Restore all systems self.player_stats = sg.StatTracker.from_dict(data["player_stats"]) self.player_weapon = sg.Weapon.from_dict(data["weapon"]) self.player_x, self.player_y = data["player_position"] self.current_area = data["current_area"] self.difficulty = data["difficulty"] # Restore dynamic objects (implementation depends on your enemy/projectile classes) # self.enemies = [Enemy.from_dict(e) for e in data["enemies"]] # self.projectiles = [sg.Projectile.from_dict(p) for p in data["projectiles"]] def settings(self): options = ["easy", "normal", "hard", "back"] current = options.index(self.difficulty) if self.difficulty in options else 1 while True: self.game.speak(f"Current difficulty: {options[current]}") # Wait for input event = pygame.event.wait() if event.type == pygame.KEYDOWN: if event.key == pygame.K_UP and current > 0: current -= 1 elif event.key == pygame.K_DOWN and current < len(options) - 1: current += 1 elif event.key == pygame.K_RETURN: if options[current] == "back": return self.difficulty = options[current] self.game.config_service.local_config.set("settings", "difficulty", self.difficulty) self.game.config_service.write_local_config() self.game.speak(f"Difficulty set to {self.difficulty}") return elif event.key == pygame.K_ESCAPE: return def run(self): # Main menu loop while True: sounds = self.game.sound.get_sounds() choice = sg.game_menu(sounds, self.play_game, "settings", "instructions", "credits", "donate", "exit_game") if choice == "settings": self.settings() elif choice == "instructions": sg.instructions() elif choice == "credits": sg.credits() elif choice == "donate": sg.donate() elif choice == "exit_game": self.game.exit() # Run the game if __name__ == "__main__": game = MyGame() game.run() ``` ## Best Practices 1. **Modern vs Traditional Approach**: - New projects: Use the Game class for better organization - Existing projects: Continue with global functions for compatibility - Both approaches are fully supported 2. **Always clean up resources**: - Use `exit_game()` or `game.exit()` when exiting to ensure proper cleanup - Stop sounds that are no longer needed 3. **Volume control**: - Implement the Alt+key volume controls in your game - Use volume services for better control 4. **Configuration**: - Save user preferences using the Config class - Load settings at startup 5. **Path initialization**: - Always initialize the framework with a proper game title - Game title is used to determine configuration directory paths - Services are interconnected, so proper initialization ensures correct operation 6. **New game systems**: - Use StatTracker for comprehensive statistics with level/total separation - Implement SaveManager for reliable save/load with metadata - Leverage Combat systems for professional weapon and projectile mechanics - Combine all systems for rich, full-featured games 7. **Performance considerations**: - Reset level stats regularly to prevent memory bloat - Clean up old saves periodically using SaveManager methods - Remove inactive projectiles from update loops - Use weapon cooldowns to prevent spam attacks ## Troubleshooting ### No Sound - Ensure pygame mixer is properly initialized - Check if sound files exist in the correct directory - Verify file formats (OGG and WAV are supported) ### No Speech - Make sure at least one speech provider is installed - Linux/Unix: `python-speechd` or `accessible-output2` - Windows/macOS: `accessible-output2` - Check if pygame display is initialized properly ### Input Issues - Ensure pygame is properly handling events - Check event loop for proper event handling ### Scoreboard/Configuration Issues - Check if path services are properly initialized with a game title - Verify write permissions in the configuration directory - For debugging, use the following code to view path configuration: ```python from libstormgames.services import PathService, ConfigService # Print path information path_service = PathService.get_instance() print(f"Game Name: {path_service.gameName}") print(f"Game Path: {path_service.gamePath}") # Check config service connection config_service = ConfigService.get_instance() print(f"Config connected to path: {hasattr(config_service, 'pathService')}") ``` ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. ## License This project is licensed under the GPL v3 License - see the LICENSE file for details.