453 lines
16 KiB
Python
453 lines
16 KiB
Python
"""
|
|
Lemontropia Suite - Inventory Scanner Dialog
|
|
UI for scanning and extracting item data from Entropia Universe inventory.
|
|
"""
|
|
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
from PyQt6.QtWidgets import (
|
|
QDialog, QVBoxLayout, QHBoxLayout, QGridLayout,
|
|
QLabel, QPushButton, QListWidget, QListWidgetItem,
|
|
QGroupBox, QSplitter, QTableWidget, QTableWidgetItem,
|
|
QHeaderView, QTextEdit, QMessageBox, QFileDialog,
|
|
QProgressBar, QCheckBox, QSpinBox, QTabWidget,
|
|
QWidget, QScrollArea, QFrame
|
|
)
|
|
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer
|
|
from PyQt6.QtGui import QPixmap, QImage
|
|
|
|
from modules.inventory_scanner import InventoryScanner, InventoryScanResult, ItemStats
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class InventoryScanWorker(QThread):
|
|
"""Background worker for inventory scanning."""
|
|
|
|
scan_complete = pyqtSignal(object) # InventoryScanResult
|
|
scan_error = pyqtSignal(str)
|
|
progress_update = pyqtSignal(str)
|
|
|
|
def __init__(self, scanner: InventoryScanner, extract_icons: bool = True,
|
|
read_details: bool = True):
|
|
super().__init__()
|
|
self.scanner = scanner
|
|
self.extract_icons = extract_icons
|
|
self.read_details = read_details
|
|
self._is_running = True
|
|
|
|
def run(self):
|
|
"""Run the scan."""
|
|
try:
|
|
self.progress_update.emit("Capturing screen...")
|
|
|
|
result = self.scanner.scan_inventory(
|
|
extract_icons=self.extract_icons,
|
|
read_details=self.read_details
|
|
)
|
|
|
|
if self._is_running:
|
|
self.scan_complete.emit(result)
|
|
|
|
except Exception as e:
|
|
if self._is_running:
|
|
self.scan_error.emit(str(e))
|
|
|
|
def stop(self):
|
|
"""Stop the scan."""
|
|
self._is_running = False
|
|
|
|
|
|
class InventoryScannerDialog(QDialog):
|
|
"""
|
|
Dialog for scanning Entropia Universe inventory.
|
|
|
|
Features:
|
|
- Auto-detect inventory window
|
|
- Extract item icons
|
|
- Read item stats from details panel
|
|
- Save results
|
|
"""
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.setWindowTitle("🔍 Inventory Scanner")
|
|
self.setMinimumSize(900, 700)
|
|
self.resize(1100, 800)
|
|
|
|
self.scanner = InventoryScanner()
|
|
self.scan_worker: Optional[InventoryScanWorker] = None
|
|
self.last_result: Optional[InventoryScanResult] = None
|
|
|
|
self._setup_ui()
|
|
self._apply_dark_theme()
|
|
|
|
def _setup_ui(self):
|
|
"""Setup the dialog UI."""
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(15, 15, 15, 15)
|
|
layout.setSpacing(10)
|
|
|
|
# Header
|
|
header = QLabel("Inventory Scanner - Extract items and stats from Entropia Universe")
|
|
header.setStyleSheet("font-size: 16px; font-weight: bold; color: #4caf50;")
|
|
layout.addWidget(header)
|
|
|
|
# Controls
|
|
controls_layout = QHBoxLayout()
|
|
|
|
# Scan buttons
|
|
self.scan_btn = QPushButton("🔍 Scan Inventory")
|
|
self.scan_btn.setMinimumHeight(40)
|
|
self.scan_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #0d47a1;
|
|
font-weight: bold;
|
|
font-size: 12px;
|
|
}
|
|
QPushButton:hover { background-color: #1565c0; }
|
|
""")
|
|
self.scan_btn.clicked.connect(self.on_scan)
|
|
controls_layout.addWidget(self.scan_btn)
|
|
|
|
self.scan_details_btn = QPushButton("📋 Scan Item Details Only")
|
|
self.scan_details_btn.setMinimumHeight(40)
|
|
self.scan_details_btn.clicked.connect(self.on_scan_details_only)
|
|
controls_layout.addWidget(self.scan_details_btn)
|
|
|
|
# Options
|
|
options_group = QGroupBox("Options")
|
|
options_layout = QHBoxLayout(options_group)
|
|
|
|
self.extract_icons_check = QCheckBox("Extract Icons")
|
|
self.extract_icons_check.setChecked(True)
|
|
options_layout.addWidget(self.extract_icons_check)
|
|
|
|
self.read_details_check = QCheckBox("Read Item Details")
|
|
self.read_details_check.setChecked(True)
|
|
options_layout.addWidget(self.read_details_check)
|
|
|
|
controls_layout.addWidget(options_group)
|
|
controls_layout.addStretch()
|
|
|
|
# Save button
|
|
self.save_btn = QPushButton("💾 Save Results")
|
|
self.save_btn.clicked.connect(self.on_save_results)
|
|
self.save_btn.setEnabled(False)
|
|
controls_layout.addWidget(self.save_btn)
|
|
|
|
layout.addLayout(controls_layout)
|
|
|
|
# Progress bar
|
|
self.progress = QProgressBar()
|
|
self.progress.setRange(0, 0) # Indeterminate
|
|
self.progress.setVisible(False)
|
|
layout.addWidget(self.progress)
|
|
|
|
# Status label
|
|
self.status_label = QLabel("Ready to scan")
|
|
self.status_label.setStyleSheet("color: #888; padding: 5px;")
|
|
layout.addWidget(self.status_label)
|
|
|
|
# Results tabs
|
|
self.results_tabs = QTabWidget()
|
|
layout.addWidget(self.results_tabs)
|
|
|
|
# Tab 1: Item Icons
|
|
self.icons_tab = self._create_icons_tab()
|
|
self.results_tabs.addTab(self.icons_tab, "🖼️ Item Icons")
|
|
|
|
# Tab 2: Item Details
|
|
self.details_tab = self._create_details_tab()
|
|
self.results_tabs.addTab(self.details_tab, "📋 Item Details")
|
|
|
|
# Tab 3: Raw Data
|
|
self.raw_tab = self._create_raw_tab()
|
|
self.results_tabs.addTab(self.raw_tab, "📝 Raw Data")
|
|
|
|
# Help text
|
|
help = QLabel(
|
|
"Instructions:\n"
|
|
"1. Open Entropia Universe\n"
|
|
"2. Open your inventory window\n"
|
|
"3. Click an item to show its details panel\n"
|
|
"4. Click 'Scan Inventory' above"
|
|
)
|
|
help.setStyleSheet("color: #888; padding: 10px; background-color: #252525; border-radius: 4px;")
|
|
help.setWordWrap(True)
|
|
layout.addWidget(help)
|
|
|
|
# Close button
|
|
close_btn = QPushButton("Close")
|
|
close_btn.clicked.connect(self.accept)
|
|
layout.addWidget(close_btn)
|
|
|
|
def _create_icons_tab(self) -> QWidget:
|
|
"""Create the item icons tab."""
|
|
tab = QWidget()
|
|
layout = QVBoxLayout(tab)
|
|
|
|
# Icons count
|
|
self.icons_count_label = QLabel("No icons extracted yet")
|
|
layout.addWidget(self.icons_count_label)
|
|
|
|
# Icons grid
|
|
scroll = QScrollArea()
|
|
scroll.setWidgetResizable(True)
|
|
scroll.setStyleSheet("background-color: #1a1a1a;")
|
|
|
|
self.icons_container = QWidget()
|
|
self.icons_grid = QGridLayout(self.icons_container)
|
|
self.icons_grid.setSpacing(10)
|
|
scroll.setWidget(self.icons_container)
|
|
|
|
layout.addWidget(scroll)
|
|
return tab
|
|
|
|
def _create_details_tab(self) -> QWidget:
|
|
"""Create the item details tab."""
|
|
tab = QWidget()
|
|
layout = QVBoxLayout(tab)
|
|
|
|
# Details table
|
|
self.details_table = QTableWidget()
|
|
self.details_table.setColumnCount(2)
|
|
self.details_table.setHorizontalHeaderLabels(["Property", "Value"])
|
|
self.details_table.horizontalHeader().setStretchLastSection(True)
|
|
self.details_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
|
layout.addWidget(self.details_table)
|
|
|
|
return tab
|
|
|
|
def _create_raw_tab(self) -> QWidget:
|
|
"""Create the raw data tab."""
|
|
tab = QWidget()
|
|
layout = QVBoxLayout(tab)
|
|
|
|
self.raw_text = QTextEdit()
|
|
self.raw_text.setReadOnly(True)
|
|
self.raw_text.setFontFamily("Consolas")
|
|
layout.addWidget(self.raw_text)
|
|
|
|
return tab
|
|
|
|
def on_scan(self):
|
|
"""Start full inventory scan."""
|
|
self._start_scan(
|
|
extract_icons=self.extract_icons_check.isChecked(),
|
|
read_details=self.read_details_check.isChecked()
|
|
)
|
|
|
|
def on_scan_details_only(self):
|
|
"""Scan only item details."""
|
|
self._start_scan(extract_icons=False, read_details=True)
|
|
|
|
def _start_scan(self, extract_icons: bool, read_details: bool):
|
|
"""Start scan worker."""
|
|
if self.scan_worker and self.scan_worker.isRunning():
|
|
QMessageBox.warning(self, "Scan in Progress", "A scan is already running.")
|
|
return
|
|
|
|
# Update UI
|
|
self.scan_btn.setEnabled(False)
|
|
self.scan_details_btn.setEnabled(False)
|
|
self.progress.setVisible(True)
|
|
self.status_label.setText("Scanning...")
|
|
self.save_btn.setEnabled(False)
|
|
|
|
# Clear previous results
|
|
self._clear_icons_grid()
|
|
self.details_table.setRowCount(0)
|
|
self.raw_text.clear()
|
|
|
|
# Start worker
|
|
self.scan_worker = InventoryScanWorker(
|
|
self.scanner,
|
|
extract_icons=extract_icons,
|
|
read_details=read_details
|
|
)
|
|
self.scan_worker.scan_complete.connect(self._on_scan_complete)
|
|
self.scan_worker.scan_error.connect(self._on_scan_error)
|
|
self.scan_worker.progress_update.connect(self._on_progress_update)
|
|
self.scan_worker.start()
|
|
|
|
def _on_scan_complete(self, result: InventoryScanResult):
|
|
"""Handle scan completion."""
|
|
self.last_result = result
|
|
|
|
# Update UI
|
|
self.progress.setVisible(False)
|
|
self.scan_btn.setEnabled(True)
|
|
self.scan_details_btn.setEnabled(True)
|
|
self.save_btn.setEnabled(True)
|
|
|
|
self.status_label.setText(
|
|
f"Scan complete: {len(result.items)} icons, "
|
|
f"details: {'Yes' if result.details_item else 'No'}"
|
|
)
|
|
|
|
# Update tabs
|
|
self._update_icons_tab(result)
|
|
self._update_details_tab(result)
|
|
self._update_raw_tab(result)
|
|
|
|
QMessageBox.information(
|
|
self,
|
|
"Scan Complete",
|
|
f"Found {len(result.items)} items\n"
|
|
f"Item details: {'Yes' if result.details_item else 'No'}"
|
|
)
|
|
|
|
def _on_scan_error(self, error: str):
|
|
"""Handle scan error."""
|
|
self.progress.setVisible(False)
|
|
self.scan_btn.setEnabled(True)
|
|
self.scan_details_btn.setEnabled(True)
|
|
self.status_label.setText(f"Error: {error}")
|
|
|
|
QMessageBox.critical(self, "Scan Error", error)
|
|
|
|
def _on_progress_update(self, message: str):
|
|
"""Handle progress update."""
|
|
self.status_label.setText(message)
|
|
|
|
def _clear_icons_grid(self):
|
|
"""Clear icons grid."""
|
|
while self.icons_grid.count():
|
|
item = self.icons_grid.takeAt(0)
|
|
if item.widget():
|
|
item.widget().deleteLater()
|
|
|
|
def _update_icons_tab(self, result: InventoryScanResult):
|
|
"""Update icons tab with results."""
|
|
self.icons_count_label.setText(f"Extracted {len(result.items)} item icons")
|
|
|
|
self._clear_icons_grid()
|
|
|
|
for i, item in enumerate(result.items):
|
|
if item.icon_path and Path(item.icon_path).exists():
|
|
# Create icon widget
|
|
icon_widget = QFrame()
|
|
icon_widget.setStyleSheet("""
|
|
QFrame {
|
|
background-color: #2a2a2a;
|
|
border: 1px solid #444;
|
|
border-radius: 4px;
|
|
padding: 5px;
|
|
}
|
|
""")
|
|
icon_layout = QVBoxLayout(icon_widget)
|
|
|
|
# Image
|
|
pixmap = QPixmap(item.icon_path)
|
|
if not pixmap.isNull():
|
|
scaled = pixmap.scaled(64, 64, Qt.AspectRatioMode.KeepAspectRatio)
|
|
img_label = QLabel()
|
|
img_label.setPixmap(scaled)
|
|
img_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
icon_layout.addWidget(img_label)
|
|
|
|
# Info
|
|
info = QLabel(f"Slot: {item.slot_position}\nHash: {item.icon_hash[:8]}...")
|
|
info.setStyleSheet("font-size: 10px; color: #888;")
|
|
icon_layout.addWidget(info)
|
|
|
|
# Add to grid (5 columns)
|
|
row = i // 5
|
|
col = i % 5
|
|
self.icons_grid.addWidget(icon_widget, row, col)
|
|
|
|
def _update_details_tab(self, result: InventoryScanResult):
|
|
"""Update details tab with results."""
|
|
if not result.details_item:
|
|
self.details_table.setRowCount(1)
|
|
self.details_table.setItem(0, 0, QTableWidgetItem("Status"))
|
|
self.details_table.setItem(0, 1, QTableWidgetItem("No item details detected"))
|
|
return
|
|
|
|
stats = result.details_item
|
|
properties = [
|
|
("Item Name", stats.item_name),
|
|
("Class", stats.item_class),
|
|
("Damage", f"{stats.damage:.2f}" if stats.damage else "N/A"),
|
|
("Range", f"{stats.range:.2f}" if stats.range else "N/A"),
|
|
("Attacks/Min", str(stats.attacks_per_min) if stats.attacks_per_min else "N/A"),
|
|
("Decay (PEC)", f"{stats.decay:.4f}" if stats.decay else "N/A"),
|
|
("Ammo Burn (PEC)", f"{stats.ammo_burn:.4f}" if stats.ammo_burn else "N/A"),
|
|
("DPP", f"{stats.damage_per_pec:.4f}" if stats.damage_per_pec else "N/A"),
|
|
("Weight", f"{stats.weight:.2f}" if stats.weight else "N/A"),
|
|
("Level", str(stats.level) if stats.level else "N/A"),
|
|
("Durability", f"{stats.durability:.1f}%" if stats.durability else "N/A"),
|
|
("Protection Impact", f"{stats.protection_impact:.1f}" if stats.protection_impact else "N/A"),
|
|
("Protection Stab", f"{stats.protection_stab:.1f}" if stats.protection_stab else "N/A"),
|
|
]
|
|
|
|
self.details_table.setRowCount(len(properties))
|
|
for row, (prop, value) in enumerate(properties):
|
|
self.details_table.setItem(row, 0, QTableWidgetItem(prop))
|
|
self.details_table.setItem(row, 1, QTableWidgetItem(value))
|
|
|
|
def _update_raw_tab(self, result: InventoryScanResult):
|
|
"""Update raw data tab."""
|
|
import json
|
|
text = json.dumps(result.to_dict(), indent=2)
|
|
self.raw_text.setText(text)
|
|
|
|
def on_save_results(self):
|
|
"""Save scan results to file."""
|
|
if not self.last_result:
|
|
return
|
|
|
|
filepath, _ = QFileDialog.getSaveFileName(
|
|
self,
|
|
"Save Inventory Scan",
|
|
str(Path.home() / f"inventory_scan_{datetime.now():%Y%m%d_%H%M%S}.json"),
|
|
"JSON Files (*.json)"
|
|
)
|
|
|
|
if filepath:
|
|
try:
|
|
self.last_result.save(filepath)
|
|
QMessageBox.information(self, "Saved", f"Results saved to:\n{filepath}")
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Error", f"Failed to save:\n{e}")
|
|
|
|
def closeEvent(self, event):
|
|
"""Handle dialog close."""
|
|
if self.scan_worker and self.scan_worker.isRunning():
|
|
self.scan_worker.stop()
|
|
self.scan_worker.wait(1000)
|
|
event.accept()
|
|
|
|
def _apply_dark_theme(self):
|
|
"""Apply dark theme."""
|
|
self.setStyleSheet("""
|
|
QDialog {
|
|
background-color: #1e1e1e;
|
|
color: #e0e0e0;
|
|
}
|
|
QGroupBox {
|
|
font-weight: bold;
|
|
border: 1px solid #444;
|
|
border-radius: 6px;
|
|
margin-top: 10px;
|
|
padding-top: 10px;
|
|
}
|
|
QTableWidget {
|
|
background-color: #252525;
|
|
border: 1px solid #444;
|
|
}
|
|
QHeaderView::section {
|
|
background-color: #2d2d2d;
|
|
padding: 6px;
|
|
border: none;
|
|
border-right: 1px solid #444;
|
|
}
|
|
QTextEdit {
|
|
background-color: #1a1a1a;
|
|
border: 1px solid #444;
|
|
color: #d0d0d0;
|
|
}
|
|
""") |