409 lines
14 KiB
Python
409 lines
14 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
|
|
- SpeechHistoryService: Manages speech history for navigation
|
|
"""
|
|
|
|
import configparser
|
|
import os
|
|
import time
|
|
from collections import deque
|
|
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
|
|
|
|
|
|
class SpeechHistoryService:
|
|
"""Speech history management service for message navigation."""
|
|
|
|
_instance = None
|
|
|
|
@classmethod
|
|
def get_instance(cls):
|
|
"""Get or create the singleton instance."""
|
|
if cls._instance is None:
|
|
cls._instance = SpeechHistoryService()
|
|
return cls._instance
|
|
|
|
def __init__(self, max_size=10):
|
|
"""Initialize speech history.
|
|
|
|
Args:
|
|
max_size (int): Maximum number of messages to keep in history
|
|
"""
|
|
self.history = deque(maxlen=max_size)
|
|
self.current_index = -1
|
|
self.max_size = max_size
|
|
|
|
def add_message(self, text):
|
|
"""Add a message to the speech history.
|
|
|
|
Args:
|
|
text (str): The spoken message to add to history
|
|
"""
|
|
if not text or not text.strip():
|
|
return
|
|
|
|
# Add message with timestamp
|
|
message_entry = {
|
|
'text': text.strip(),
|
|
'timestamp': time.time()
|
|
}
|
|
|
|
self.history.append(message_entry)
|
|
# Reset current index when new message is added
|
|
self.current_index = -1
|
|
|
|
def get_current(self):
|
|
"""Get the current message in history.
|
|
|
|
Returns:
|
|
str or None: Current message text, or None if no history
|
|
"""
|
|
if not self.history:
|
|
return None
|
|
|
|
if self.current_index == -1:
|
|
# Return most recent message
|
|
return self.history[-1]['text']
|
|
else:
|
|
# Return message at current index
|
|
if 0 <= self.current_index < len(self.history):
|
|
return self.history[self.current_index]['text']
|
|
|
|
return None
|
|
|
|
def move_previous(self):
|
|
"""Navigate to the previous message in history.
|
|
|
|
Returns:
|
|
str or None: Previous message text, or None if at beginning
|
|
"""
|
|
if not self.history:
|
|
return None
|
|
|
|
if self.current_index == -1:
|
|
# Start from the most recent message
|
|
self.current_index = len(self.history) - 1
|
|
elif self.current_index > 0:
|
|
# Move backward in history
|
|
self.current_index -= 1
|
|
else:
|
|
# Already at the beginning, wrap to the end
|
|
self.current_index = len(self.history) - 1
|
|
|
|
return self.get_current()
|
|
|
|
def move_next(self):
|
|
"""Navigate to the next message in history.
|
|
|
|
Returns:
|
|
str or None: Next message text, or None if at end
|
|
"""
|
|
if not self.history:
|
|
return None
|
|
|
|
if self.current_index == -1:
|
|
# Already at most recent, wrap to beginning
|
|
self.current_index = 0
|
|
elif self.current_index < len(self.history) - 1:
|
|
# Move forward in history
|
|
self.current_index += 1
|
|
else:
|
|
# At the end, wrap to beginning
|
|
self.current_index = 0
|
|
|
|
return self.get_current()
|
|
|
|
def clear_history(self):
|
|
"""Clear all messages from history."""
|
|
self.history.clear()
|
|
self.current_index = -1
|
|
|
|
def set_max_size(self, max_size):
|
|
"""Change the maximum history size.
|
|
|
|
Args:
|
|
max_size (int): New maximum size for history buffer
|
|
"""
|
|
if max_size > 0:
|
|
self.max_size = max_size
|
|
# Create new deque with new max size, preserving recent messages
|
|
old_history = list(self.history)
|
|
self.history = deque(old_history[-max_size:], maxlen=max_size)
|
|
# Adjust current index if needed
|
|
if self.current_index >= len(self.history):
|
|
self.current_index = len(self.history) - 1
|
|
|
|
def get_history_size(self):
|
|
"""Get the current number of messages in history.
|
|
|
|
Returns:
|
|
int: Number of messages in history
|
|
"""
|
|
return len(self.history)
|