EU-Utility-Plugins-Repo/plugins/auto_screenshot/plugin.py

736 lines
28 KiB
Python

"""
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()