""" Entropia Universe Icon Extractor A standalone tool for extracting game icons from cache. Description: Extracts item icons from Entropia Universe game cache and converts them to PNG format for use with EntropiaNexus.com wiki submissions. Important: Items must be seen/rendered in-game before they appear in the cache. Usage: python standalone_icon_extractor.py Output Location: Icons are saved to your Documents/Entropia Universe/Icons/ folder (same location where chat.log is normally stored) Developer: ImpulsiveFPS Discord: impulsivefps Website: https://EntropiaNexus.com Disclaimer: Entropia Nexus is a fan-made resource and is not affiliated with MindArk PE AB. Entropia Universe is a trademark of MindArk PE AB. """ import sys import logging from pathlib import Path from typing import Optional, List, Tuple import ctypes try: from PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QComboBox, QListWidget, QListWidgetItem, QFileDialog, QProgressBar, QGroupBox, QMessageBox, QCheckBox, QSplitter, QTextEdit ) from PyQt6.QtCore import Qt, QThread, pyqtSignal, QSettings from PyQt6.QtGui import QIcon, QPixmap, QFont PYQT_AVAILABLE = True except ImportError: PYQT_AVAILABLE = False print("PyQt6 not available. Install with: pip install PyQt6") sys.exit(1) try: from PIL import Image, ImageFilter PIL_AVAILABLE = True except ImportError: PIL_AVAILABLE = False print("Pillow not available. Install with: pip install Pillow") sys.exit(1) # Setup logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) # Application metadata APP_NAME = "Entropia Universe Icon Extractor" APP_VERSION = "1.0.0" DEVELOPER = "ImpulsiveFPS" DISCORD = "impulsivefps" WEBSITE = "https://EntropiaNexus.com" class TGAHeader: """TGA file header structure.""" def __init__(self, data: bytes): self.id_length = data[0] self.color_map_type = data[1] self.image_type = data[2] self.color_map_origin = int.from_bytes(data[3:5], 'little') self.color_map_length = int.from_bytes(data[5:7], 'little') self.color_map_depth = data[7] self.x_origin = int.from_bytes(data[8:10], 'little') self.y_origin = int.from_bytes(data[10:12], 'little') self.width = int.from_bytes(data[12:14], 'little') self.height = int.from_bytes(data[14:16], 'little') self.pixel_depth = data[16] self.image_descriptor = data[17] def __str__(self): return f"{self.width}x{self.height}, {self.pixel_depth}bpp" class TGAConverter: """Converter for TGA files to PNG with 320x320 canvas.""" CANVAS_SIZE = (320, 320) # Hardcoded to 320x320 def __init__(self, output_dir: Optional[Path] = None): # Default to user's Documents/Entropia Universe/Icons/ # This works on any Windows username if output_dir is None: self.output_dir = Path.home() / "Documents" / "Entropia Universe" / "Icons" else: self.output_dir = output_dir self.output_dir.mkdir(parents=True, exist_ok=True) self._cache_path: Optional[Path] = None def find_cache_folder(self) -> Optional[Path]: """Find the Entropia Universe icon cache folder.""" # Hardcoded path - works on any system with EU installed cache_path = Path("C:/ProgramData/Entropia Universe/public_users_data/cache/icon") if cache_path.exists(): self._cache_path = cache_path return cache_path return None def read_tga_header(self, filepath: Path) -> Optional[TGAHeader]: """Read TGA header from file.""" try: with open(filepath, 'rb') as f: header_data = f.read(18) if len(header_data) < 18: return None return TGAHeader(header_data) except Exception as e: logger.error(f"Error reading TGA header: {e}") return None def convert_tga_to_png(self, tga_path: Path, output_name: Optional[str] = None) -> Optional[Path]: """ Convert a TGA file to PNG with 320x320 canvas. Args: tga_path: Path to TGA file output_name: Optional custom output name Returns: Path to output PNG file or None if failed """ try: # Load TGA image = Image.open(tga_path) if image.mode != 'RGBA': image = image.convert('RGBA') # Apply 320x320 canvas (centered, no upscaling) image = self._apply_canvas(image) # Save if output_name is None: output_name = tga_path.stem output_path = self.output_dir / f"{output_name}.png" image.save(output_path, 'PNG') return output_path except Exception as e: logger.error(f"Conversion failed: {e}") return None def _apply_canvas(self, image: Image.Image) -> Image.Image: """ Place image centered on a 320x320 canvas. No upscaling - original size centered on canvas. """ canvas_w, canvas_h = self.CANVAS_SIZE img_w, img_h = image.size # Create transparent canvas canvas = Image.new('RGBA', self.CANVAS_SIZE, (0, 0, 0, 0)) # Center on canvas (no scaling) x = (canvas_w - img_w) // 2 y = (canvas_h - img_h) // 2 canvas.paste(image, (x, y), image if image.mode == 'RGBA' else None) return canvas class ConversionWorker(QThread): """Background worker for batch conversion.""" progress = pyqtSignal(str) file_done = pyqtSignal(str, str) # filename, output_path finished = pyqtSignal(int, int) # success, total error = pyqtSignal(str) def __init__(self, files: List[Path], converter: TGAConverter): super().__init__() self.files = files self.converter = converter self._running = True def run(self): """Run conversion.""" try: success = 0 total = len(self.files) for i, filepath in enumerate(self.files): if not self._running: break self.progress.emit(f"[{i+1}/{total}] {filepath.name}") output = self.converter.convert_tga_to_png(filepath) if output: success += 1 self.file_done.emit(filepath.name, str(output)) self.finished.emit(success, total) except Exception as e: self.error.emit(str(e)) def stop(self): self._running = False class IconExtractorWindow(QMainWindow): """Main window for the standalone icon extractor.""" def __init__(self): super().__init__() self.setWindowTitle(APP_NAME) self.setMinimumSize(950, 750) self.converter = TGAConverter() self.worker: Optional[ConversionWorker] = None self.found_files: List[Path] = [] # Hardcoded base cache path self.base_cache_path = Path("C:/ProgramData/Entropia Universe/public_users_data/cache/icon") self.settings = QSettings("ImpulsiveFPS", "EUIconExtractor") self._setup_ui() self._load_settings() self._detect_subfolders() def _setup_ui(self): """Setup the UI.""" central = QWidget() self.setCentralWidget(central) layout = QVBoxLayout(central) layout.setContentsMargins(15, 15, 15, 15) layout.setSpacing(12) # Header header = QLabel(APP_NAME) header.setStyleSheet("font-size: 22px; font-weight: bold; color: #4caf50;") layout.addWidget(header) # Description desc = QLabel( "Extract item icons from Entropia Universe game cache and convert them to PNG format.\n" "These icons can be submitted to EntropiaNexus.com to help complete the item database." ) desc.setStyleSheet("color: #aaaaaa; padding: 5px;") desc.setWordWrap(True) layout.addWidget(desc) # Important notice notice_group = QGroupBox("Important Information") notice_layout = QVBoxLayout(notice_group) notice_text = QTextEdit() notice_text.setReadOnly(True) notice_text.setMaximumHeight(120) notice_text.setStyleSheet(""" QTextEdit { background-color: #2a2520; color: #ffcc80; border: 1px solid #5d4037; border-radius: 3px; font-size: 12px; } """) notice_text.setText( "REQUIREMENT: Items must be seen/rendered in-game before they appear in the cache!\n" "If an item icon is missing, you need to view the item in your inventory or see it dropped as loot first.\n\n" f"Output Location: Icons are saved to your Documents folder ({self.converter.output_dir})\n" "in the same location where Entropia Universe normally stores your chat.log file." ) notice_layout.addWidget(notice_text) layout.addWidget(notice_group) # Main splitter splitter = QSplitter(Qt.Orientation.Horizontal) # Left panel - Settings left_panel = QWidget() left_layout = QVBoxLayout(left_panel) left_layout.setContentsMargins(0, 0, 0, 0) # Cache folder cache_group = QGroupBox("Cache Source") cache_layout = QVBoxLayout(cache_group) # Base path (hardcoded) base_label = QLabel("Game Cache Location:") cache_layout.addWidget(base_label) self.cache_label = QLabel(str(self.base_cache_path)) self.cache_label.setStyleSheet("font-family: Consolas; font-size: 11px; color: #888; padding: 5px; background: #1a1a1a; border-radius: 3px;") cache_layout.addWidget(self.cache_label) # Subfolder selector subfolder_layout = QHBoxLayout() subfolder_layout.addWidget(QLabel("Version Folder:")) self.subfolder_combo = QComboBox() self.subfolder_combo.setMinimumWidth(200) self.subfolder_combo.currentIndexChanged.connect(self._on_subfolder_changed) subfolder_layout.addWidget(self.subfolder_combo) refresh_btn = QPushButton("Refresh") refresh_btn.clicked.connect(self._detect_subfolders) subfolder_layout.addWidget(refresh_btn) subfolder_layout.addStretch() cache_layout.addLayout(subfolder_layout) # All subfolders checkbox self.all_subfolders_check = QCheckBox("Include ALL version folders") self.all_subfolders_check.setToolTip("Merge icons from all game versions") self.all_subfolders_check.stateChanged.connect(self._on_all_subfolders_changed) cache_layout.addWidget(self.all_subfolders_check) left_layout.addWidget(cache_group) # Output folder output_group = QGroupBox("Output Location") output_layout = QVBoxLayout(output_group) output_info = QLabel( "Icons will be saved to your Documents folder:\n" "(Same location as chat.log)" ) output_info.setStyleSheet("color: #888; font-size: 11px;") output_layout.addWidget(output_info) self.output_label = QLabel(str(self.converter.output_dir)) self.output_label.setStyleSheet("font-family: Consolas; font-size: 11px; color: #888; padding: 5px; background: #1a1a1a; border-radius: 3px;") self.output_label.setWordWrap(True) output_layout.addWidget(self.output_label) change_btn = QPushButton("Change Output Folder...") change_btn.clicked.connect(self._browse_output) output_layout.addWidget(change_btn) left_layout.addWidget(output_group) # Settings (simplified - just 320x320) settings_group = QGroupBox("Export Settings") settings_layout = QVBoxLayout(settings_group) settings_info = QLabel( "Export Format: PNG with transparent background\n" "Canvas Size: 320x320 pixels (centered)\n" "Upscaling: None (original icon size)" ) settings_info.setStyleSheet("color: #888; font-size: 11px;") settings_layout.addWidget(settings_info) left_layout.addWidget(settings_group) # Nexus link nexus_group = QGroupBox("EntropiaNexus.com") nexus_layout = QVBoxLayout(nexus_group) nexus_info = QLabel( "Submit extracted icons to help complete the\n" "Entropia Universe item database and wiki!" ) nexus_info.setStyleSheet("color: #4caf50;") nexus_layout.addWidget(nexus_info) nexus_btn = QPushButton("Open EntropiaNexus.com") nexus_btn.clicked.connect(lambda: self._open_url(WEBSITE)) nexus_layout.addWidget(nexus_btn) left_layout.addWidget(nexus_group) # Convert button self.convert_btn = QPushButton("Start Extracting Icons") self.convert_btn.setMinimumHeight(60) self.convert_btn.setStyleSheet(""" QPushButton { background-color: #0d47a1; font-weight: bold; font-size: 16px; border-radius: 5px; } QPushButton:hover { background-color: #1565c0; } QPushButton:disabled { background-color: #333; color: #666; } """) self.convert_btn.clicked.connect(self._start_conversion) left_layout.addWidget(self.convert_btn) # Progress self.progress_bar = QProgressBar() self.progress_bar.setVisible(False) left_layout.addWidget(self.progress_bar) self.status_label = QLabel("Ready") self.status_label.setStyleSheet("color: #888; padding: 5px;") left_layout.addWidget(self.status_label) left_layout.addStretch() splitter.addWidget(left_panel) # Right panel - File list right_panel = QWidget() right_layout = QVBoxLayout(right_panel) right_layout.setContentsMargins(0, 0, 0, 0) files_group = QGroupBox("Available Icons") files_layout = QVBoxLayout(files_group) files_info = QLabel("Select icons to extract (or leave unselected to extract all)") files_info.setStyleSheet("color: #888; font-size: 11px;") files_layout.addWidget(files_info) self.files_count_label = QLabel("No files found") files_layout.addWidget(self.files_count_label) self.files_list = QListWidget() self.files_list.setSelectionMode(QListWidget.SelectionMode.ExtendedSelection) files_layout.addWidget(self.files_list) # Selection buttons sel_layout = QHBoxLayout() select_all_btn = QPushButton("Select All") select_all_btn.clicked.connect(self.files_list.selectAll) sel_layout.addWidget(select_all_btn) select_none_btn = QPushButton("Select None") select_none_btn.clicked.connect(self.files_list.clearSelection) sel_layout.addWidget(select_none_btn) sel_layout.addStretch() open_folder_btn = QPushButton("Open Output Folder") open_folder_btn.clicked.connect(self._open_output_folder) sel_layout.addWidget(open_folder_btn) files_layout.addLayout(sel_layout) right_layout.addWidget(files_group) splitter.addWidget(right_panel) splitter.setSizes([380, 520]) layout.addWidget(splitter, 1) # Footer footer = QLabel( f"Developed by {DEVELOPER} | Discord: {DISCORD} | {WEBSITE}\n" "Entropia Nexus is a fan-made resource and is not affiliated with MindArk PE AB. " "Entropia Universe is a trademark of MindArk PE AB." ) footer.setStyleSheet("color: #555; font-size: 10px; padding: 5px;") footer.setAlignment(Qt.AlignmentFlag.AlignCenter) footer.setWordWrap(True) layout.addWidget(footer) def _open_url(self, url: str): """Open URL in default browser.""" import webbrowser webbrowser.open(url) def _load_settings(self): """Load saved settings.""" # Output folder saved_output = self.settings.value("output_dir", str(self.converter.output_dir)) self.converter.output_dir = Path(saved_output) self.output_label.setText(saved_output) def _save_settings(self): """Save current settings.""" self.settings.setValue("output_dir", str(self.converter.output_dir)) def _detect_subfolders(self): """Detect version subfolders in the cache directory.""" self.subfolder_combo.clear() if not self.base_cache_path.exists(): self.cache_label.setText(f"Not found: {self.base_cache_path}") self.status_label.setText("Cache folder not found - is Entropia Universe installed?") return # Find all subfolders that contain TGA files subfolders = [] for item in self.base_cache_path.iterdir(): if item.is_dir(): # Check if this folder has TGA files tga_count = len(list(item.glob("*.tga"))) if tga_count > 0: subfolders.append((item.name, tga_count, item)) if not subfolders: self.cache_label.setText(f"No subfolders with icons in {self.base_cache_path}") self.status_label.setText("No version folders found") return # Sort by name (version) subfolders.sort(key=lambda x: x[0]) # Add to combo for name, count, path in subfolders: self.subfolder_combo.addItem(f"{name} ({count} icons)", str(path)) # Add "All folders" option at top total_icons = sum(s[1] for s in subfolders) self.subfolder_combo.insertItem(0, f"All Folders ({total_icons} icons)", "all") self.subfolder_combo.setCurrentIndex(0) self.cache_label.setText(f"{self.base_cache_path}") self.status_label.setText(f"Found {len(subfolders)} version folders") # Load files self._refresh_file_list() def _on_subfolder_changed(self): """Handle subfolder selection change.""" self._refresh_file_list() def _on_all_subfolders_changed(self): """Handle 'all subfolders' checkbox change.""" self.subfolder_combo.setEnabled(not self.all_subfolders_check.isChecked()) self._refresh_file_list() def _browse_output(self): """Browse for output folder.""" folder = QFileDialog.getExistingDirectory( self, "Select Output Folder", str(self.converter.output_dir) ) if folder: self.converter.output_dir = Path(folder) self.output_label.setText(folder) self._save_settings() def _refresh_file_list(self): """Refresh the list of found files based on current selection.""" self.files_list.clear() self.found_files = [] if not self.base_cache_path.exists(): return # Determine which folders to scan if self.all_subfolders_check.isChecked(): # Scan all subfolders folders_to_scan = [d for d in self.base_cache_path.iterdir() if d.is_dir()] else: # Scan selected subfolder path_data = self.subfolder_combo.currentData() if path_data == "all" or path_data is None: folders_to_scan = [d for d in self.base_cache_path.iterdir() if d.is_dir()] else: folders_to_scan = [Path(path_data)] # Collect TGA files tga_files = [] for folder in folders_to_scan: if folder.exists(): tga_files.extend(folder.glob("*.tga")) self.files_count_label.setText(f"Found {len(tga_files)} icon files") self.status_label.setText(f"Found {len(tga_files)} files") for tga_file in sorted(tga_files): # Show folder prefix for clarity try: rel_path = f"{tga_file.parent.name}/{tga_file.name}" except: rel_path = tga_file.name item = QListWidgetItem(rel_path) item.setData(Qt.ItemDataRole.UserRole, str(tga_file)) # Get info header = self.converter.read_tga_header(tga_file) if header: item.setToolTip(f"{header.width}x{header.height}, {header.pixel_depth}bpp") self.files_list.addItem(item) self.found_files.append(tga_file) self.convert_btn.setEnabled(len(tga_files) > 0) def _start_conversion(self): """Start batch conversion.""" # Get selected files or all files selected_items = self.files_list.selectedItems() if selected_items: files_to_convert = [ Path(item.data(Qt.ItemDataRole.UserRole)) for item in selected_items ] else: files_to_convert = self.found_files if not files_to_convert: QMessageBox.warning(self, "No Files", "No files selected for extraction.") return # Save settings self._save_settings() # Setup UI self.convert_btn.setEnabled(False) self.convert_btn.setText("Extracting...") self.progress_bar.setRange(0, len(files_to_convert)) self.progress_bar.setValue(0) self.progress_bar.setVisible(True) # Start worker self.worker = ConversionWorker(files_to_convert, self.converter) self.worker.progress.connect(self._on_progress) self.worker.file_done.connect(self._on_file_done) self.worker.finished.connect(self._on_finished) self.worker.error.connect(self._on_error) self.worker.start() def _on_progress(self, msg: str): self.status_label.setText(msg) self.progress_bar.setValue(self.progress_bar.value() + 1) def _on_file_done(self, filename: str, output_path: str): logger.info(f"Extracted: {filename} -> {output_path}") def _on_finished(self, success: int, total: int): self.convert_btn.setEnabled(True) self.convert_btn.setText("Start Extracting Icons") self.progress_bar.setVisible(False) self.status_label.setText(f"Extracted {success}/{total} icons") QMessageBox.information( self, "Extraction Complete", f"Successfully extracted {success} of {total} icons.\n\n" f"Output location:\n{self.converter.output_dir}\n\n" f"Submit these icons to EntropiaNexus.com to help the community!" ) def _on_error(self, error_msg: str): self.convert_btn.setEnabled(True) self.convert_btn.setText("Start Extracting Icons") self.progress_bar.setVisible(False) self.status_label.setText(f"Error: {error_msg}") QMessageBox.critical(self, "Error", f"Extraction failed:\n{error_msg}") def _open_output_folder(self): """Open output folder in file manager.""" import os import subprocess path = str(self.converter.output_dir) if os.name == 'nt': os.startfile(path) elif sys.platform == 'darwin': subprocess.run(['open', path]) else: subprocess.run(['xdg-open', path]) def closeEvent(self, event): """Save settings on close.""" self._save_settings() if self.worker and self.worker.isRunning(): self.worker.stop() self.worker.wait(1000) event.accept() def set_app_icon(app: QApplication): """Set application icon.""" # Try to load icon from various locations icon_paths = [ Path(__file__).parent / "assets" / "icon.ico", Path(__file__).parent / "assets" / "icon.png", Path(__file__).parent / "icon.ico", Path(__file__).parent / "icon.png", ] for icon_path in icon_paths: if icon_path.exists(): app.setWindowIcon(QIcon(str(icon_path))) return # If no icon file, we can't set one programmatically # User would need to provide an icon file def main(): """Main entry point.""" app = QApplication(sys.argv) app.setStyle('Fusion') # Set application info for proper Windows taskbar icon app.setApplicationName(APP_NAME) app.setApplicationVersion(APP_VERSION) app.setOrganizationName("ImpulsiveFPS") # Try to set icon set_app_icon(app) # Dark theme app.setStyleSheet(""" QMainWindow, QDialog { background-color: #1a1a1a; } QWidget { background-color: #1a1a1a; color: #e0e0e0; } QGroupBox { font-weight: bold; border: 1px solid #333; border-radius: 5px; margin-top: 10px; padding-top: 10px; } QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px; } QPushButton { background-color: #333; border: 1px solid #555; padding: 8px 15px; border-radius: 3px; } QPushButton:hover { background-color: #444; } QComboBox { background-color: #2a2a2a; border: 1px solid #555; padding: 5px; min-width: 150px; } QListWidget { background-color: #222; border: 1px solid #444; border-radius: 3px; } QListWidget::item { padding: 5px; } QListWidget::item:selected { background-color: #0d47a1; } QProgressBar { border: 1px solid #444; border-radius: 3px; text-align: center; } QProgressBar::chunk { background-color: #4caf50; } QTextEdit { background-color: #222; border: 1px solid #444; } """) window = IconExtractorWindow() window.show() sys.exit(app.exec()) if __name__ == "__main__": main()