libstormgames/services.py
2025-03-22 17:34:35 -04:00

275 lines
10 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Service classes for Storm Games.
Provides centralized services to replace global variables:
- ConfigService: Manages game configuration
- VolumeService: Handles volume settings
- PathService: Manages file paths
"""
import configparser
import os
from xdg import BaseDirectory
# For backward compatibility
from .config import gamePath, globalPath, write_config, read_config
class ConfigService:
"""Configuration management service."""
_instance = None
@classmethod
def get_instance(cls):
"""Get or create the singleton instance."""
if cls._instance is None:
cls._instance = ConfigService()
return cls._instance
def __init__(self):
"""Initialize configuration parsers."""
self.localConfig = configparser.ConfigParser()
self.globalConfig = configparser.ConfigParser()
self.gameTitle = None
self.pathService = None
def set_game_info(self, gameTitle, pathService):
"""Set game information and initialize configs.
Args:
gameTitle (str): Title of the game
pathService (PathService): Path service instance
"""
self.gameTitle = gameTitle
self.pathService = pathService
# Load existing configurations
self.read_local_config()
self.read_global_config()
def read_local_config(self):
"""Read local configuration from file."""
try:
# Try to use pathService if available
if self.pathService and self.pathService.gamePath:
with open(os.path.join(self.pathService.gamePath, "config.ini"), 'r') as configFile:
self.localConfig.read_file(configFile)
# Fallback to global gamePath
elif gamePath:
with open(os.path.join(gamePath, "config.ini"), 'r') as configFile:
self.localConfig.read_file(configFile)
# Delegate to old function as last resort
else:
read_config(False)
self.localConfig = configparser.ConfigParser()
self.localConfig.read_dict(globals().get('localConfig', {}))
except:
pass
def read_global_config(self):
"""Read global configuration from file."""
try:
# Try to use pathService if available
if self.pathService and self.pathService.globalPath:
with open(os.path.join(self.pathService.globalPath, "config.ini"), 'r') as configFile:
self.globalConfig.read_file(configFile)
# Fallback to global globalPath
elif globalPath:
with open(os.path.join(globalPath, "config.ini"), 'r') as configFile:
self.globalConfig.read_file(configFile)
# Delegate to old function as last resort
else:
read_config(True)
self.globalConfig = configparser.ConfigParser()
self.globalConfig.read_dict(globals().get('globalConfig', {}))
except:
pass
def write_local_config(self):
"""Write local configuration to file."""
try:
# Try to use pathService if available
if self.pathService and self.pathService.gamePath:
with open(os.path.join(self.pathService.gamePath, "config.ini"), 'w') as configFile:
self.localConfig.write(configFile)
# Fallback to global gamePath
elif gamePath:
with open(os.path.join(gamePath, "config.ini"), 'w') as configFile:
self.localConfig.write(configFile)
# Delegate to old function as last resort
else:
# Update old global config
globals()['localConfig'] = self.localConfig
write_config(False)
except Exception as e:
print(f"Warning: Failed to write local config: {e}")
def write_global_config(self):
"""Write global configuration to file."""
try:
# Try to use pathService if available
if self.pathService and self.pathService.globalPath:
with open(os.path.join(self.pathService.globalPath, "config.ini"), 'w') as configFile:
self.globalConfig.write(configFile)
# Fallback to global globalPath
elif globalPath:
with open(os.path.join(globalPath, "config.ini"), 'w') as configFile:
self.globalConfig.write(configFile)
# Delegate to old function as last resort
else:
# Update old global config
globals()['globalConfig'] = self.globalConfig
write_config(True)
except Exception as e:
print(f"Warning: Failed to write global config: {e}")
class VolumeService:
"""Volume management service."""
_instance = None
@classmethod
def get_instance(cls):
"""Get or create the singleton instance."""
if cls._instance is None:
cls._instance = VolumeService()
return cls._instance
def __init__(self):
"""Initialize volume settings."""
self.bgmVolume = 0.75 # Default background music volume
self.sfxVolume = 1.0 # Default sound effects volume
self.masterVolume = 1.0 # Default master volume
def adjust_master_volume(self, change, pygameMixer=None):
"""Adjust the master volume for all sounds.
Args:
change (float): Amount to change volume by (positive or negative)
pygameMixer: Optional pygame.mixer module for real-time updates
"""
self.masterVolume = max(0.0, min(1.0, self.masterVolume + change))
# Update real-time audio if pygame mixer is provided
if pygameMixer:
# Update music volume
if pygameMixer.music.get_busy():
pygameMixer.music.set_volume(self.bgmVolume * self.masterVolume)
# Update all sound channels
for i in range(pygameMixer.get_num_channels()):
channel = pygameMixer.Channel(i)
if channel.get_busy():
currentVolume = channel.get_volume()
if isinstance(currentVolume, (int, float)):
# Mono audio
channel.set_volume(currentVolume * self.masterVolume)
else:
# Stereo audio
left, right = currentVolume
channel.set_volume(left * self.masterVolume, right * self.masterVolume)
def adjust_bgm_volume(self, change, pygameMixer=None):
"""Adjust only the background music volume.
Args:
change (float): Amount to change volume by (positive or negative)
pygameMixer: Optional pygame.mixer module for real-time updates
"""
self.bgmVolume = max(0.0, min(1.0, self.bgmVolume + change))
# Update real-time audio if pygame mixer is provided
if pygameMixer and pygameMixer.music.get_busy():
pygameMixer.music.set_volume(self.bgmVolume * self.masterVolume)
def adjust_sfx_volume(self, change, pygameMixer=None):
"""Adjust volume for sound effects only.
Args:
change (float): Amount to change volume by (positive or negative)
pygameMixer: Optional pygame.mixer module for real-time updates
"""
self.sfxVolume = max(0.0, min(1.0, self.sfxVolume + change))
# Update real-time audio if pygame mixer is provided
if pygameMixer:
# Update all sound channels except reserved ones
for i in range(pygameMixer.get_num_channels()):
channel = pygameMixer.Channel(i)
if channel.get_busy():
currentVolume = channel.get_volume()
if isinstance(currentVolume, (int, float)):
# Mono audio
channel.set_volume(currentVolume * self.sfxVolume * self.masterVolume)
else:
# Stereo audio
left, right = currentVolume
channel.set_volume(left * self.sfxVolume * self.masterVolume,
right * self.sfxVolume * self.masterVolume)
def get_bgm_volume(self):
"""Get the current BGM volume with master adjustment.
Returns:
float: Current adjusted BGM volume
"""
return self.bgmVolume * self.masterVolume
def get_sfx_volume(self):
"""Get the current SFX volume with master adjustment.
Returns:
float: Current adjusted SFX volume
"""
return self.sfxVolume * self.masterVolume
class PathService:
"""Path management service."""
_instance = None
@classmethod
def get_instance(cls):
"""Get or create the singleton instance."""
if cls._instance is None:
cls._instance = PathService()
return cls._instance
def __init__(self):
"""Initialize path variables."""
self.globalPath = None
self.gamePath = None
self.gameName = None
# Try to initialize from global variables for backward compatibility
global gamePath, globalPath
if gamePath:
self.gamePath = gamePath
if globalPath:
self.globalPath = globalPath
def initialize(self, gameTitle):
"""Initialize paths for a game.
Args:
gameTitle (str): Title of the game
"""
self.gameName = gameTitle
self.globalPath = os.path.join(BaseDirectory.xdg_config_home, "storm-games")
self.gamePath = os.path.join(self.globalPath,
str.lower(str.replace(gameTitle, " ", "-")))
# Create game directory if it doesn't exist
if not os.path.exists(self.gamePath):
os.makedirs(self.gamePath)
# Update global variables for backward compatibility
global gamePath, globalPath
gamePath = self.gamePath
globalPath = self.globalPath
return self