diff --git a/modules/icon_cache_parser.py b/modules/icon_cache_parser.py new file mode 100644 index 0000000..55ef569 --- /dev/null +++ b/modules/icon_cache_parser.py @@ -0,0 +1,291 @@ +""" +Lemontropia Suite - Icon Cache Parser +Parse Entropia Universe iconcache.dat for item metadata. + +The iconcache.dat file contains mappings between icon IDs and item information. +""" + +import struct +import logging +from pathlib import Path +from typing import Dict, Optional, List, Tuple +from dataclasses import dataclass + +logger = logging.getLogger(__name__) + + +@dataclass +class IconCacheEntry: + """Represents an entry in the icon cache.""" + icon_id: str # e.g., "i0000328" + item_name: str + item_type: str = "" # weapon, armor, etc. + extra_data: bytes = b"" + + +class IconCacheParser: + """ + Parser for Entropia Universe iconcache.dat files. + + The iconcache.dat is a binary file that maps icon IDs to item names. + Format appears to be proprietary/binary. + + Usage: + parser = IconCacheParser() + entries = parser.parse_cache_file("path/to/iconcache.dat") + + # Look up an item name by icon ID + name = parser.get_item_name("i0000328") + """ + + # Common cache file locations + DEFAULT_CACHE_PATHS = [ + Path("C:") / "ProgramData" / "Entropia Universe" / "public_users_data" / "cache" / "icon", + Path.home() / "Documents" / "Entropia Universe" / "cache" / "icons", + ] + + def __init__(self, cache_path: Optional[Path] = None): + """ + Initialize parser. + + Args: + cache_path: Path to cache directory (auto-find if None) + """ + self.cache_path = cache_path + self._entries: Dict[str, IconCacheEntry] = {} + self._parsed = False + + def find_cache_file(self) -> Optional[Path]: + """Find the iconcache.dat file.""" + search_paths = [] + + if self.cache_path: + search_paths.append(self.cache_path) + else: + search_paths = self.DEFAULT_CACHE_PATHS + + for path in search_paths: + if not path.exists(): + continue + + # Look for iconcache.dat in this folder or subfolders + for dat_file in path.rglob("iconcache.dat"): + logger.info(f"Found iconcache.dat: {dat_file}") + return dat_file + + logger.warning("iconcache.dat not found") + return None + + def parse_cache_file(self, filepath: Optional[Path] = None) -> Dict[str, IconCacheEntry]: + """ + Parse the iconcache.dat file. + + Args: + filepath: Path to iconcache.dat (auto-find if None) + + Returns: + Dictionary mapping icon_id to IconCacheEntry + """ + if filepath is None: + filepath = self.find_cache_file() + + if not filepath or not filepath.exists(): + logger.error("iconcache.dat not found") + return {} + + entries = {} + + try: + with open(filepath, 'rb') as f: + data = f.read() + + logger.info(f"Parsing iconcache.dat ({len(data)} bytes)") + + # Try to identify format + # Common binary patterns to look for: + # - Magic headers + # - String tables + # - Index structures + + # Try simple string extraction first (many .dat files have embedded strings) + entries = self._extract_strings(data) + + if not entries: + # Try more sophisticated parsing + entries = self._parse_binary_structure(data) + + self._entries = entries + self._parsed = True + + logger.info(f"Parsed {len(entries)} entries from iconcache.dat") + + except Exception as e: + logger.error(f"Failed to parse iconcache.dat: {e}") + + return entries + + def _extract_strings(self, data: bytes) -> Dict[str, IconCacheEntry]: + """ + Extract readable strings from binary data. + + This is a heuristic approach that looks for: + - Icon IDs (pattern: i0000000) + - Item names (readable ASCII/UTF-8 strings) + """ + entries = {} + + # Look for icon ID patterns (i followed by digits) + import re + + # Find all icon IDs + icon_pattern = rb'i\d{7}' # i0000000 format + icon_ids = re.findall(icon_pattern, data) + + logger.debug(f"Found {len(icon_ids)} potential icon IDs") + + # Look for strings that might be item names + # Item names are typically printable ASCII + string_pattern = rb'[A-Za-z][A-Za-z0-9_\s\-\(\)]{3,50}(?=[\x00\x01\x02])' + strings = re.findall(string_pattern, data) + + logger.debug(f"Found {len(strings)} potential strings") + + # Try to match icon IDs with nearby strings + for i, icon_id_bytes in enumerate(icon_ids[:50]): # Limit to first 50 for testing + icon_id = icon_id_bytes.decode('ascii', errors='ignore') + + # Find position in data + pos = data.find(icon_id_bytes) + if pos == -1: + continue + + # Look for nearby strings (within 200 bytes) + search_start = max(0, pos - 100) + search_end = min(len(data), pos + 200) + nearby_data = data[search_start:search_end] + + # Find strings in this region + nearby_strings = re.findall(string_pattern, nearby_data) + + if nearby_strings: + # Use the longest string as the item name (heuristic) + item_name = max(nearby_strings, key=len).decode('latin-1', errors='ignore') + entries[icon_id] = IconCacheEntry( + icon_id=icon_id, + item_name=item_name.strip() + ) + + return entries + + def _parse_binary_structure(self, data: bytes) -> Dict[str, IconCacheEntry]: + """ + Try to parse the binary structure of the file. + + This attempts to identify record structures based on: + - Fixed-size records + - Offset tables + - Common binary patterns + """ + entries = {} + + if len(data) < 100: + return entries + + # Try to detect record size by looking for repeating patterns + # Many binary cache files have fixed-size records + + # Check header + magic = data[:4] + logger.debug(f"File header (first 4 bytes): {magic.hex()}") + + # Try reading as simple table + # Some formats: [count][record_size][records...] + try: + count = struct.unpack(' 0 else 0 + logger.debug(f"Estimated record size: {record_size}") + except: + pass + + return entries + + def get_item_name(self, icon_id: str) -> Optional[str]: + """Get item name by icon ID.""" + if not self._parsed: + self.parse_cache_file() + + entry = self._entries.get(icon_id) + return entry.item_name if entry else None + + def get_all_entries(self) -> Dict[str, IconCacheEntry]: + """Get all parsed entries.""" + if not self._parsed: + self.parse_cache_file() + return self._entries.copy() + + def print_summary(self): + """Print a summary of the parsed cache.""" + if not self._parsed: + self.parse_cache_file() + + print("=" * 60) + print("Icon Cache Summary") + print("=" * 60) + print(f"Total entries: {len(self._entries)}") + print() + + # Show sample entries + if self._entries: + print("Sample entries:") + for i, (icon_id, entry) in enumerate(list(self._entries.items())[:10]): + print(f" {icon_id}: {entry.item_name}") + else: + print("No entries parsed (format may be unknown)") + + print("=" * 60) + + +# Convenience function +def parse_icon_cache(cache_path: Optional[Path] = None) -> Dict[str, str]: + """ + Quick function to parse icon cache and return icon_id -> name mapping. + + Returns: + Dictionary mapping icon_id to item_name + """ + parser = IconCacheParser(cache_path) + entries = parser.parse_cache_file() + return {k: v.item_name for k, v in entries.items()} + + +def main(): + """CLI to analyze iconcache.dat.""" + import sys + + print("🔍 Entropia Universe Icon Cache Parser") + print("=" * 60) + + parser = IconCacheParser() + + # Try to find cache file + cache_file = parser.find_cache_file() + + if cache_file: + print(f"Found: {cache_file}") + print() + parser.parse_cache_file(cache_file) + parser.print_summary() + else: + print("❌ iconcache.dat not found") + print() + print("Searched in:") + for path in parser.DEFAULT_CACHE_PATHS: + print(f" - {path}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/ui/tga_converter_dialog.py b/ui/tga_converter_dialog.py index 9b6ffca..4e28338 100644 --- a/ui/tga_converter_dialog.py +++ b/ui/tga_converter_dialog.py @@ -247,7 +247,7 @@ class TGAConverterDialog(QDialog): self.canvas_combo.addItem("280x280", (280, 280)) self.canvas_combo.addItem("320x320", (320, 320)) self.canvas_combo.addItem("512x512", (512, 512)) - self.canvas_combo.setCurrentIndex(4) # Default to 320x320 + self.canvas_combo.setCurrentIndex(5) # Default to 320x320 canvas_layout.addWidget(self.canvas_combo) canvas_layout.addSpacing(20) @@ -337,11 +337,20 @@ class TGAConverterDialog(QDialog): if cache_path: self.cache_path_label.setText(str(cache_path)) - tga_files = list(cache_path.glob("*.tga")) + # Use rglob to search ALL subfolders recursively + tga_files = list(cache_path.rglob("*.tga")) self.files_count_label.setText(f"Found {len(tga_files)} TGA files") for tga_file in tga_files: - item = QListWidgetItem(tga_file.name) + # Show relative path in list + try: + display_name = str(tga_file.relative_to(cache_path)) + except: + display_name = tga_file.name + + item = QListWidgetItem(display_name) + item.setData(Qt.ItemDataRole.UserRole, str(tga_file)) + # Try to get TGA info tga_info = self.converter.read_tga_header(tga_file) if tga_info: @@ -353,6 +362,7 @@ class TGAConverterDialog(QDialog): self.cache_path_label.setText("❌ Not found") self.files_count_label.setText("Cache folder not found") self.convert_btn.setEnabled(False) + self.convert_btn.setEnabled(False) def _browse_cache_folder(self): """Browse for cache folder."""