""" EU-Utility - Auto-Screenshot Plugin Automatically capture screenshots on Global, HOF, or other significant game events. Perfect for sharing achievements! """ import os from datetime import datetime from pathlib import Path from typing import Dict, List, Optional, Any from dataclasses import dataclass, field from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QCheckBox, QSpinBox, QGroupBox, QFileDialog, QLineEdit, QComboBox, QListWidget, QListWidgetItem, QTabWidget ) from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QObject from PyQt6.QtGui import QColor from plugins.base_plugin import BasePlugin from core.event_bus import GlobalEvent, LootEvent, SkillGainEvent @dataclass class ScreenshotRule: """Rule for when to take a screenshot.""" event_type: str # 'global', 'hof', 'ath', 'skill', 'loot_value', 'custom' min_value: float = 0.0 # For value-based triggers (PED, skill gain, etc.) enabled: bool = True play_sound: bool = True show_notification: bool = True custom_pattern: str = "" # For custom log patterns class AutoScreenshotSignals(QObject): """Signals for thread-safe UI updates.""" screenshot_taken = pyqtSignal(str) # Path to screenshot event_detected = pyqtSignal(str) # Event description class AutoScreenshotPlugin(BasePlugin): """ Plugin for automatic screenshots on game events. Features: - Screenshot on Global/HOF/ATH - Screenshot on rare loot - Screenshot on significant skill gains - Configurable capture delay - Custom filename patterns - Notification and sound options """ name = "Auto-Screenshot" version = "1.0.0" author = "EU-Utility" description = "Capture screenshots on Global/HOF and other events" icon = "๐Ÿ“ธ" hotkey = "ctrl+shift+c" def __init__(self, overlay_window, config): super().__init__(overlay_window, config) # Settings self.enabled = self.get_config('enabled', True) self.capture_delay_ms = self.get_config('capture_delay_ms', 500) self.save_directory = self.get_config('save_directory', str(Path.home() / "Documents" / "Entropia Universe" / "Screenshots" / "Globals")) # Filename pattern self.filename_pattern = self.get_config('filename_pattern', "{event_type}_{timestamp}_{player}") # Screenshot rules self.rules: Dict[str, ScreenshotRule] = {} self._load_rules() # Statistics self.screenshots_taken = 0 self.events_captured: List[Dict] = [] # UI references self._ui = None self.events_list = None self.status_label = None self.dir_label = None # Signals self.signals = AutoScreenshotSignals() self.signals.screenshot_taken.connect(self._on_screenshot_saved) self.signals.event_detected.connect(self._on_event_logged) # Event subscriptions self._subscriptions: List[str] = [] # Pending screenshot timer self._pending_screenshot = None self._pending_timer = None def initialize(self) -> None: """Initialize plugin.""" self.log_info("Initializing Auto-Screenshot") # Ensure save directory exists Path(self.save_directory).mkdir(parents=True, exist_ok=True) # Subscribe to events if self.enabled: self._subscribe_to_events() self.log_info(f"Auto-Screenshot initialized (enabled={self.enabled})") def _subscribe_to_events(self) -> None: """Subscribe to game events.""" # Global/HOF events self._subscriptions.append( self.subscribe_typed(GlobalEvent, self._on_global_event) ) # Loot events (for high-value loot) self._subscriptions.append( self.subscribe_typed(LootEvent, self._on_loot_event) ) # Skill gain events self._subscriptions.append( self.subscribe_typed(SkillGainEvent, self._on_skill_event) ) def get_ui(self) -> QWidget: """Return the plugin's UI widget.""" if self._ui is None: self._ui = self._create_ui() return self._ui def _create_ui(self) -> QWidget: """Create the plugin UI.""" widget = QWidget() layout = QVBoxLayout(widget) layout.setSpacing(12) layout.setContentsMargins(16, 16, 16, 16) # Header header = QLabel("๐Ÿ“ธ Auto-Screenshot") header.setStyleSheet(""" font-size: 18px; font-weight: bold; color: white; padding-bottom: 8px; """) layout.addWidget(header) # Status self.status_label = QLabel(f"Status: {'Enabled' if self.enabled else 'Disabled'} | Captured: {self.screenshots_taken}") self.status_label.setStyleSheet("color: rgba(255, 255, 255, 150);") layout.addWidget(self.status_label) # Create tabs tabs = QTabWidget() tabs.setStyleSheet(""" QTabWidget::pane { background-color: rgba(30, 35, 45, 150); border: 1px solid rgba(100, 150, 200, 50); border-radius: 8px; } QTabBar::tab { background-color: rgba(50, 60, 75, 200); color: white; padding: 8px 16px; border-top-left-radius: 4px; border-top-right-radius: 4px; } QTabBar::tab:selected { background-color: rgba(100, 150, 200, 200); } """) # === Rules Tab === rules_widget = QWidget() rules_layout = QVBoxLayout(rules_widget) rules_layout.setContentsMargins(12, 12, 12, 12) rules_header = QLabel("Capture Rules") rules_header.setStyleSheet("font-weight: bold; color: white;") rules_layout.addWidget(rules_header) # Global checkbox self.global_checkbox = QCheckBox("Capture on Global (any value)") self.global_checkbox.setChecked(self.rules.get('global', ScreenshotRule('global')).enabled) self.global_checkbox.setStyleSheet("color: white;") self.global_checkbox.stateChanged.connect(lambda: self._update_rule('global', self.global_checkbox.isChecked())) rules_layout.addWidget(self.global_checkbox) # HOF checkbox self.hof_checkbox = QCheckBox("Capture on HOF (50+ PED)") self.hof_checkbox.setChecked(self.rules.get('hof', ScreenshotRule('hof', min_value=50)).enabled) self.hof_checkbox.setStyleSheet("color: white;") self.hof_checkbox.stateChanged.connect(lambda: self._update_rule('hof', self.hof_checkbox.isChecked())) rules_layout.addWidget(self.hof_checkbox) # ATH checkbox self.ath_checkbox = QCheckBox("Capture on ATH (All-Time High)") self.ath_checkbox.setChecked(self.rules.get('ath', ScreenshotRule('ath')).enabled) self.ath_checkbox.setStyleSheet("color: white;") self.ath_checkbox.stateChanged.connect(lambda: self._update_rule('ath', self.ath_checkbox.isChecked())) rules_layout.addWidget(self.ath_checkbox) # Discovery checkbox self.discovery_checkbox = QCheckBox("Capture on Discovery") self.discovery_checkbox.setChecked(self.rules.get('discovery', ScreenshotRule('discovery')).enabled) self.discovery_checkbox.setStyleSheet("color: white;") self.discovery_checkbox.stateChanged.connect(lambda: self._update_rule('discovery', self.discovery_checkbox.isChecked())) rules_layout.addWidget(self.discovery_checkbox) # High value loot loot_layout = QHBoxLayout() self.loot_checkbox = QCheckBox("Capture on loot above") self.loot_checkbox.setChecked(self.rules.get('loot_value', ScreenshotRule('loot_value', min_value=100)).enabled) self.loot_checkbox.setStyleSheet("color: white;") self.loot_checkbox.stateChanged.connect(lambda: self._update_rule('loot_value', self.loot_checkbox.isChecked())) loot_layout.addWidget(self.loot_checkbox) self.loot_spin = QSpinBox() self.loot_spin.setRange(1, 10000) self.loot_spin.setValue(int(self.rules.get('loot_value', ScreenshotRule('loot_value', min_value=100)).min_value)) self.loot_spin.setSuffix(" PED") self.loot_spin.setStyleSheet(self._spinbox_style()) loot_layout.addWidget(self.loot_spin) loot_layout.addStretch() rules_layout.addLayout(loot_layout) # Skill gain skill_layout = QHBoxLayout() self.skill_checkbox = QCheckBox("Capture on skill gain above") self.skill_checkbox.setChecked(self.rules.get('skill', ScreenshotRule('skill', min_value=0.1)).enabled) self.skill_checkbox.setStyleSheet("color: white;") self.skill_checkbox.stateChanged.connect(lambda: self._update_rule('skill', self.skill_checkbox.isChecked())) skill_layout.addWidget(self.skill_checkbox) self.skill_spin = QSpinBox() self.skill_spin.setRange(1, 1000) self.skill_spin.setValue(int(self.rules.get('skill', ScreenshotRule('skill', min_value=0.1)).min_value * 100)) self.skill_spin.setSuffix(" points (x0.01)") self.skill_spin.setStyleSheet(self._spinbox_style()) skill_layout.addWidget(self.skill_spin) skill_layout.addStretch() rules_layout.addLayout(skill_layout) # Sound/notification options options_group = QGroupBox("Notification Options") options_layout = QVBoxLayout(options_group) self.sound_checkbox = QCheckBox("Play sound on capture") self.sound_checkbox.setChecked(self.get_config('play_sound', True)) self.sound_checkbox.setStyleSheet("color: white;") options_layout.addWidget(self.sound_checkbox) self.notification_checkbox = QCheckBox("Show notification on capture") self.notification_checkbox.setChecked(self.get_config('show_notification', True)) self.notification_checkbox.setStyleSheet("color: white;") options_layout.addWidget(self.notification_checkbox) rules_layout.addWidget(options_group) rules_layout.addStretch() tabs.addTab(rules_widget, "๐Ÿ“‹ Rules") # === History Tab === history_widget = QWidget() history_layout = QVBoxLayout(history_widget) history_layout.setContentsMargins(12, 12, 12, 12) history_header = QLabel("Recent Captures") history_header.setStyleSheet("font-weight: bold; color: white;") history_layout.addWidget(history_header) self.events_list = QListWidget() self.events_list.setStyleSheet(""" QListWidget { background-color: rgba(30, 35, 45, 150); border: 1px solid rgba(100, 150, 200, 50); border-radius: 8px; color: white; } QListWidget::item { padding: 8px; border-bottom: 1px solid rgba(100, 150, 200, 30); } QListWidget::item:selected { background-color: rgba(100, 150, 200, 100); } """) history_layout.addWidget(self.events_list) # History buttons history_btn_layout = QHBoxLayout() open_folder_btn = QPushButton("๐Ÿ“ Open Folder") open_folder_btn.setStyleSheet(self._button_style("#2196f3")) open_folder_btn.clicked.connect(self._open_screenshots_folder) history_btn_layout.addWidget(open_folder_btn) clear_history_btn = QPushButton("๐Ÿงน Clear History") clear_history_btn.setStyleSheet(self._button_style()) clear_history_btn.clicked.connect(self._clear_history) history_btn_layout.addWidget(clear_history_btn) history_btn_layout.addStretch() history_layout.addLayout(history_btn_layout) tabs.addTab(history_widget, "๐Ÿ“œ History") # === Settings Tab === settings_widget = QWidget() settings_layout = QVBoxLayout(settings_widget) settings_layout.setContentsMargins(12, 12, 12, 12) settings_header = QLabel("Capture Settings") settings_header.setStyleSheet("font-weight: bold; color: white;") settings_layout.addWidget(settings_header) # Master enable self.enable_checkbox = QCheckBox("Enable Auto-Screenshot") self.enable_checkbox.setChecked(self.enabled) self.enable_checkbox.setStyleSheet("color: #4caf50; font-weight: bold;") self.enable_checkbox.stateChanged.connect(self._toggle_enabled) settings_layout.addWidget(self.enable_checkbox) # Capture delay delay_layout = QHBoxLayout() delay_label = QLabel("Capture Delay:") delay_label.setStyleSheet("color: white;") delay_layout.addWidget(delay_label) self.delay_spin = QSpinBox() self.delay_spin.setRange(0, 5000) self.delay_spin.setValue(self.capture_delay_ms) self.delay_spin.setSuffix(" ms") self.delay_spin.setSingleStep(100) self.delay_spin.setStyleSheet(self._spinbox_style()) delay_layout.addWidget(self.delay_spin) delay_info = QLabel("(time to hide overlay)") delay_info.setStyleSheet("color: rgba(255, 255, 255, 100); font-size: 11px;") delay_layout.addWidget(delay_info) delay_layout.addStretch() settings_layout.addLayout(delay_layout) # Save directory dir_group = QGroupBox("Save Location") dir_layout = QVBoxLayout(dir_group) dir_row = QHBoxLayout() self.dir_label = QLabel(self.save_directory) self.dir_label.setStyleSheet("color: white;") self.dir_label.setWordWrap(True) dir_row.addWidget(self.dir_label, 1) change_dir_btn = QPushButton("๐Ÿ“ Change") change_dir_btn.setStyleSheet(self._button_style()) change_dir_btn.clicked.connect(self._change_save_directory) dir_row.addWidget(change_dir_btn) dir_layout.addLayout(dir_row) settings_layout.addWidget(dir_group) # Filename pattern pattern_group = QGroupBox("Filename Pattern") pattern_layout = QVBoxLayout(pattern_group) pattern_info = QLabel("Available variables: {timestamp}, {event_type}, {player}, {value}") pattern_info.setStyleSheet("color: rgba(255, 255, 255, 100); font-size: 11px;") pattern_layout.addWidget(pattern_info) self.pattern_input = QLineEdit(self.filename_pattern) self.pattern_input.setStyleSheet(self._input_style()) pattern_layout.addWidget(self.pattern_input) settings_layout.addWidget(pattern_group) settings_layout.addStretch() tabs.addTab(settings_widget, "โš™๏ธ Settings") layout.addWidget(tabs) # Save button save_btn = QPushButton("๐Ÿ’พ Save Settings") save_btn.setStyleSheet(self._button_style("#4caf50")) save_btn.clicked.connect(self._save_settings) layout.addWidget(save_btn) # Test button test_btn = QPushButton("๐Ÿ“ธ Test Screenshot") test_btn.setStyleSheet(self._button_style("#ff9800")) test_btn.clicked.connect(lambda: self._take_screenshot("test", "TestUser", 0)) layout.addWidget(test_btn) return widget def _button_style(self, color: str = "#607d8b") -> str: """Generate button stylesheet.""" return f""" QPushButton {{ background-color: {color}; color: white; border: none; border-radius: 8px; padding: 10px 16px; font-weight: bold; }} QPushButton:hover {{ background-color: {color}dd; }} QPushButton:pressed {{ background-color: {color}aa; }} """ def _input_style(self) -> str: """Generate input stylesheet.""" return """ QLineEdit { background-color: rgba(50, 60, 75, 200); color: white; border: 1px solid rgba(100, 150, 200, 100); border-radius: 8px; padding: 8px 12px; } """ def _spinbox_style(self) -> str: """Generate spinbox stylesheet.""" return """ QSpinBox { background-color: rgba(50, 60, 75, 200); color: white; border: 1px solid rgba(100, 150, 200, 100); border-radius: 4px; padding: 4px; } """ def _on_global_event(self, event: GlobalEvent) -> None: """Handle global/HOF events.""" if not self.enabled: return event_type = event.achievement_type.lower() # Check if we should capture if event_type == 'global' and self.rules.get('global', ScreenshotRule('global')).enabled: self._schedule_screenshot('global', event.player_name, event.value) elif event_type in ['hof', 'hall of fame'] and self.rules.get('hof', ScreenshotRule('hof')).enabled: if event.value >= self.rules.get('hof', ScreenshotRule('hof', min_value=50)).min_value: self._schedule_screenshot('hof', event.player_name, event.value) elif event_type in ['ath', 'all time high'] and self.rules.get('ath', ScreenshotRule('ath')).enabled: self._schedule_screenshot('ath', event.player_name, event.value) elif event_type == 'discovery' and self.rules.get('discovery', ScreenshotRule('discovery')).enabled: self._schedule_screenshot('discovery', event.player_name, event.value) def _on_loot_event(self, event: LootEvent) -> None: """Handle loot events.""" if not self.enabled: return rule = self.rules.get('loot_value', ScreenshotRule('loot_value', min_value=100)) if rule.enabled and event.total_tt_value >= rule.min_value: self._schedule_screenshot('loot', 'player', event.total_tt_value) def _on_skill_event(self, event: SkillGainEvent) -> None: """Handle skill gain events.""" if not self.enabled: return rule = self.rules.get('skill', ScreenshotRule('skill', min_value=0.1)) if rule.enabled and event.gain_amount >= rule.min_value: self._schedule_screenshot('skill', 'player', event.gain_amount) def _schedule_screenshot(self, event_type: str, player_name: str, value: float) -> None: """Schedule a screenshot with optional delay.""" if self._pending_timer: self._pending_timer.stop() self._pending_screenshot = (event_type, player_name, value) if self.capture_delay_ms > 0: self._pending_timer = QTimer() self._pending_timer.timeout.connect(lambda: self._execute_screenshot()) self._pending_timer.setSingleShot(True) self._pending_timer.start(self.capture_delay_ms) else: self._execute_screenshot() def _execute_screenshot(self) -> None: """Execute the pending screenshot.""" if not self._pending_screenshot: return event_type, player_name, value = self._pending_screenshot self._pending_screenshot = None self._take_screenshot(event_type, player_name, value) def _take_screenshot(self, event_type: str, player_name: str, value: float) -> None: """Take and save a screenshot.""" try: # Generate filename timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") filename = self.filename_pattern.format( timestamp=timestamp, event_type=event_type.upper(), player=player_name, value=f"{value:.0f}" ) filename = f"{filename}.png" # Clean filename filename = "".join(c for c in filename if c.isalnum() or c in "._- ") filepath = Path(self.save_directory) / filename # Capture screenshot screenshot = self.capture_screen(full_screen=True) # Save screenshot.save(str(filepath), 'PNG') self.screenshots_taken += 1 # Log event event_data = { 'timestamp': datetime.now().isoformat(), 'event_type': event_type, 'player': player_name, 'value': value, 'filepath': str(filepath) } self.events_captured.append(event_data) # Trim history if len(self.events_captured) > 100: self.events_captured = self.events_captured[-100:] # Emit signals self.signals.screenshot_taken.emit(str(filepath)) self.signals.event_detected.emit(f"{event_type.upper()}: {player_name} - {value:.0f} PED") # Play sound if enabled if self.sound_checkbox.isChecked(): self.play_sound('global' if event_type in ['global', 'hof', 'ath'] else 'skill_gain') # Show notification if enabled if self.notification_checkbox.isChecked(): self.notify( f"๐Ÿ“ธ {event_type.upper()} Captured!", f"Saved to {filename}", notification_type='success' ) self.log_info(f"Screenshot saved: {filepath}") except Exception as e: self.log_error(f"Screenshot failed: {e}") self.notify_error("Screenshot Failed", str(e)) def _on_screenshot_saved(self, filepath: str) -> None: """Handle screenshot saved event.""" self.status_label.setText(f"Status: {'Enabled' if self.enabled else 'Disabled'} | Captured: {self.screenshots_taken}") def _on_event_logged(self, description: str) -> None: """Handle event logged.""" if self.events_list: item = QListWidgetItem(f"[{datetime.now().strftime('%H:%M:%S')}] {description}") item.setForeground(QColor("white")) self.events_list.insertItem(0, item) # Trim list while self.events_list.count() > 50: self.events_list.takeItem(self.events_list.count() - 1) def _update_rule(self, rule_name: str, enabled: bool) -> None: """Update a rule's enabled state.""" if rule_name not in self.rules: # Create default rule defaults = { 'global': ScreenshotRule('global'), 'hof': ScreenshotRule('hof', min_value=50), 'ath': ScreenshotRule('ath'), 'discovery': ScreenshotRule('discovery'), 'loot_value': ScreenshotRule('loot_value', min_value=100), 'skill': ScreenshotRule('skill', min_value=0.1) } self.rules[rule_name] = defaults.get(rule_name, ScreenshotRule(rule_name)) self.rules[rule_name].enabled = enabled self._save_rules() def _toggle_enabled(self, state) -> None: """Toggle plugin enabled state.""" self.enabled = state == Qt.CheckState.Checked.value if self.enabled: self._subscribe_to_events() self.status_label.setText(f"Status: Enabled | Captured: {self.screenshots_taken}") self.status_label.setStyleSheet("color: #4caf50;") else: # Unsubscribe for sub_id in self._subscriptions: self.unsubscribe_typed(sub_id) self._subscriptions.clear() self.status_label.setText(f"Status: Disabled | Captured: {self.screenshots_taken}") self.status_label.setStyleSheet("color: #f44336;") def _change_save_directory(self) -> None: """Change the save directory.""" new_dir = QFileDialog.getExistingDirectory( self._ui, "Select Screenshot Directory", self.save_directory ) if new_dir: self.save_directory = new_dir Path(self.save_directory).mkdir(parents=True, exist_ok=True) if self.dir_label: self.dir_label.setText(new_dir) def _open_screenshots_folder(self) -> None: """Open the screenshots folder.""" import subprocess import platform try: if platform.system() == 'Windows': subprocess.run(['explorer', self.save_directory], check=True) elif platform.system() == 'Darwin': subprocess.run(['open', self.save_directory], check=True) else: subprocess.run(['xdg-open', self.save_directory], check=True) except Exception as e: self.log_error(f"Failed to open folder: {e}") def _clear_history(self) -> None: """Clear the events history.""" self.events_captured.clear() if self.events_list: self.events_list.clear() def _save_settings(self) -> None: """Save all settings.""" self.capture_delay_ms = self.delay_spin.value() self.filename_pattern = self.pattern_input.text() # Update rules with values if 'loot_value' in self.rules: self.rules['loot_value'].min_value = self.loot_spin.value() if 'skill' in self.rules: self.rules['skill'].min_value = self.skill_spin.value() / 100 self.set_config('enabled', self.enabled) self.set_config('capture_delay_ms', self.capture_delay_ms) self.set_config('save_directory', self.save_directory) self.set_config('filename_pattern', self.filename_pattern) self.set_config('play_sound', self.sound_checkbox.isChecked()) self.set_config('show_notification', self.notification_checkbox.isChecked()) self._save_rules() self.notify_success("Settings Saved", "Auto-screenshot settings updated") def _save_rules(self) -> None: """Save rules to storage.""" rules_data = { name: { 'event_type': rule.event_type, 'min_value': rule.min_value, 'enabled': rule.enabled, 'play_sound': rule.play_sound, 'show_notification': rule.show_notification } for name, rule in self.rules.items() } self.save_data('rules', rules_data) self.save_data('events_captured', self.events_captured) def _load_rules(self) -> None: """Load rules from storage.""" rules_data = self.load_data('rules', {}) for name, data in rules_data.items(): self.rules[name] = ScreenshotRule( event_type=data['event_type'], min_value=data.get('min_value', 0), enabled=data.get('enabled', True), play_sound=data.get('play_sound', True), show_notification=data.get('show_notification', True) ) # Set defaults if not loaded defaults = { 'global': ScreenshotRule('global'), 'hof': ScreenshotRule('hof', min_value=50), 'ath': ScreenshotRule('ath'), 'discovery': ScreenshotRule('discovery'), 'loot_value': ScreenshotRule('loot_value', min_value=100), 'skill': ScreenshotRule('skill', min_value=0.1) } for name, rule in defaults.items(): if name not in self.rules: self.rules[name] = rule self.events_captured = self.load_data('events_captured', []) self.screenshots_taken = len(self.events_captured) def on_hotkey(self) -> None: """Handle hotkey press - take manual screenshot.""" self._take_screenshot('manual', 'player', 0) def shutdown(self) -> None: """Cleanup on shutdown.""" self._save_settings() if self._pending_timer: self._pending_timer.stop() # Unsubscribe for sub_id in self._subscriptions: self.unsubscribe_typed(sub_id) super().shutdown()