#!/usr/bin/env python3
# -*- coding: utf-8 -*-
print('\033[93m' + "========================================" + '\033[0m')
print('\033[93m' + "            勿辞品质修改工具" + '\033[0m')
print('\033[93m' + "========================================" + '\033[0m')
import os
import sys
import struct
import zlib
import time
import threading
import mmap
import shutil
import re
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from collections import namedtuple, defaultdict
try:
    import zopfli
except ImportError:
    print("\033[31m[错误] 未安装zopfli库！请先执行：pip3 install zopfli -i https://pypi.tuna.tsinghua.edu.cn/simple\033[0m")
    sys.exit(10)
# ====================== 全局配置 =======================
PAK_DIR = "/storage/emulated/0/勿辞制作区/pak/"
UNPACK_DIR = "/storage/emulated/0/勿辞制作区/uexp解包/"
PACK_DIR = "/storage/emulated/0/勿辞制作区/uexp打包/"
TARGET_FILE_PATTERN = "Item.uexp"
ENCRYPT_KEY = 0x79
ZLIB_LEVELS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
MAX_WORKERS = 4
MIN_PRINT = True
DIVIDER = "----------------------------------------"
# ====================== 颜色工具类（按要求优化） =======================
class ColorManager:
    @staticmethod
    def red(text): return f"\033[31m{text}\033[0m"
    @staticmethod
    def green(text): return f"\033[32m{text}\033[0m"
    @staticmethod
    def bright_yellow(text): return f"\033[93m{text}\033[0m"
    @staticmethod
    def cyan(text): return f"\033[36m{text}\033[0m"
    @staticmethod
    def white(text): return f"\033[37m{text}\033[0m"
    
    @staticmethod
    def print_error(text): print(f"{ColorManager.red('[错误]')} {text}")
    @staticmethod
    def print_success(text): print(f"{ColorManager.green('[成功]')} {text}")
    @staticmethod
    def print_info(text): print(f"{ColorManager.cyan('[信息]')} {text}")
    @staticmethod
    def print_warning(text): print(f"{ColorManager.bright_yellow('[警告]')} {text}")
    @staticmethod
    def print_progress(current, total):
        if total == 0: return
        bar_width = 40
        progress = current / total
        pos = int(bar_width * progress)
        sys.stdout.write(f"\r{ColorManager.cyan('[进度]')} [")
        for i in range(bar_width):
            if i < pos: sys.stdout.write(ColorManager.green("█"))
            elif i == pos: sys.stdout.write(ColorManager.bright_yellow("█"))
            else: sys.stdout.write("░")
        sys.stdout.write(f"] {ColorManager.bright_yellow(f'{current}/{total}')} {ColorManager.green(f'{progress*100:.1f}%')}")
        sys.stdout.flush()
# ====================== 工具函数类 =======================
class Utils:
    @staticmethod
    def read_file(file_path):
        try:
            with open(file_path, 'rb') as f:
                return f.read()
        except Exception as e:
            ColorManager.print_error(f"读取文件失败: {file_path} - {str(e)}")
            return b''
    
    @staticmethod
    def format_size(bytes_size):
        units = ['B', 'KB', 'MB', 'GB']
        unit_idx = 0
        size = bytes_size
        while size >= 1024 and unit_idx < len(units)-1:
            size /= 1024
            unit_idx += 1
        return f"{size:.2f} {units[unit_idx]}"
    
    @staticmethod
    def format_time(seconds):
        return f"{seconds:.2f} 秒"
    
    @staticmethod
    def get_file_count(dir_path):
        count = 0
        try:
            for root, dirs, files in os.walk(dir_path):
                count += len(files)
        except Exception as e:
            ColorManager.print_error(f"统计文件数失败: {str(e)}")
        return count
# ====================== 旋转进度条（粉色改为绿色） ======================
class RomanticSpinner:
    def __init__(self, message: str = "处理中", spinner_chars: str = "|/-\\", delay: float = 0.1):
        self.message = message
        self.spinner_chars = spinner_chars
        self.delay = delay
        self.running = False
        self.spinner_thread = None
    
    def spin(self):
        spinner_index = 0
        while self.running:
            current_char = self.spinner_chars[spinner_index % len(self.spinner_chars)]
            output = f"\r{self.message} {current_char} "
            print(f"\033[32m{output}\033[0m", end="")
            sys.stdout.flush()
            spinner_index = (spinner_index + 1) % len(self.spinner_chars)
            time.sleep(self.delay)
    
    def start(self):
        if not self.running:
            self.running = True
            self.spinner_thread = threading.Thread(target=self.spin, daemon=True)
            self.spinner_thread.start()
    
    def stop(self, final_message: str = ""):
        if self.running:
            self.running = False
            if self.spinner_thread and self.spinner_thread.is_alive():
                self.spinner_thread.join()
            if final_message:
                print(f"\033[32m\r{final_message}\n\033[0m", end="")
            else:
                print(f"\033[32m\r{self.message} 完成！\n\033[0m", end="")
            sys.stdout.flush()
# ====================== Zopfli压缩管理器 ======================
class ZopfliManager:
    def try_zlib_compress(self, input_data, max_size):
        input_len = len(input_data)
        if input_len < 64:
            return None
        for level in ZLIB_LEVELS:
            try:
                compressed = zlib.compress(input_data, level)
                if len(compressed) <= max_size:
                    return compressed
            except Exception:
                continue
        return None
    
    def compress_zopfli(self, input_data, max_size):
        if len(input_data) == 0:
            return None
        try:
            compressed = zopfli.compress(input_data, zopfli.ZOPFLI_FORMAT_ZLIB)
            if len(compressed) <= max_size:
                return compressed
        except Exception as e:
            ColorManager.print_warning(f"Zopfli压缩失败: {str(e)[:30]}")
        return None
# ====================== 品质修改核心逻辑 ======================
class ColorOption:
    def __init__(self, name: str, code: str):
        self.name = name
        self.code = code
        self.bytes = self._hex_to_bytes(code)
    
    def _hex_to_bytes(self, hex_str: str) -> list:
        bytes_list = []
        for i in range(0, len(hex_str), 2):
            byte_str = hex_str[i:i+2]
            bytes_list.append(int(byte_str, 16))
        return bytes_list
# ====================== 颜色列表：经典新增 金光2 = 6C00 ======================
COLOR_LIST = [
    # 经典
    ColorOption("白光", "0200"),
    ColorOption("绿光", "0300"),
    ColorOption("蓝光", "0400"),
    ColorOption("紫光", "0500"),
    ColorOption("粉光", "0600"),
    ColorOption("红光", "0700"),
    ColorOption("橙光", "0800"),
    ColorOption("金光1", "0900"),
    ColorOption("金光2", "6C00"),
    ColorOption("彩色", "C800"),
    
    # 地铁
    ColorOption("地铁白光", "6500"),
    ColorOption("地铁绿光", "6600"),
    ColorOption("地铁蓝光", "6700"),
    ColorOption("地铁紫光", "6800"),
    ColorOption("地铁粉光", "6900"),
    ColorOption("地铁红光", "6A00"),
    ColorOption("地铁橙光", "6B00"),
    ColorOption("地铁金光", "6C00"),
    ColorOption("鉴定绿光", "6D00"),
    
    # 古墓
    ColorOption("透明色", "CA00"),
    ColorOption("古墓彩色", "C800")
]
FIXED_PATTERNS = [
    [0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00],
    [0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x04, 0x00],
    [0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x68, 0x00],
    [0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00],
    [0x00, 0x00, 0xCA, 0x00, 0x00, 0x00, 0x6A, 0x00],
    [0x00, 0x00, 0x6B, 0x00, 0x00, 0x00, 0x69, 0x00],
    [0x00, 0x00, 0x66, 0x00, 0x00, 0x00, 0x67, 0x00],
    [0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x6D, 0x00],
    [0x00, 0x00, 0x6C, 0x00, 0x00, 0x00, 0xC8, 0x00]
]
class SelectionResult:
    def __init__(self):
        self.is_batch = False
        self.search_codes = []
        self.search_names = []
        self.display_name = ""
        self.batch_type = ""
def select_color_group() -> int:
    ColorManager.print_info("\n请选择颜色分组:")
    print(ColorManager.cyan("1. 经典"))
    print(ColorManager.cyan("2. 地铁"))
    print(ColorManager.cyan("3. 古墓"))
    while True:
        choice = input(ColorManager.cyan("[提示] 请输入分组序号: ")).strip()
        if choice in ["1", "2", "3"]:
            return int(choice)
        ColorManager.print_error("无效输入，请选择1~3之间的数字")
def show_group_colors(group_idx: int) -> tuple:
    color_list = []
    current_num = 1
    group_name = ""
    if group_idx == 1:
        group_name = "经典"
        color_list = COLOR_LIST[:10]
        color_list.append(ColorOption("经典全部颜色", ""))
    elif group_idx == 2:
        group_name = "地铁"
        color_list = COLOR_LIST[10:19]
        color_list.append(ColorOption("地铁全部颜色", ""))
    else:
        group_name = "古墓"
        color_list = COLOR_LIST[19:]
        color_list.append(ColorOption("古墓全部颜色", ""))
    ColorManager.print_info(f"\n{group_name}组可选颜色:")
    for i, color in enumerate(color_list):
        print(ColorManager.white(f"  {i+1}. {color.name}"))
    return color_list, group_name
def select_color() -> tuple:
    group_idx = select_color_group()
    color_list, group_name = show_group_colors(group_idx)
    while True:
        choice = input(ColorManager.cyan(f"\n[提示] 请选择{group_name}组颜色序号: ")).strip()
        if not choice or not choice.isdigit():
            ColorManager.print_error("无效输入，请选择正确的序号")
            continue
        index = int(choice) - 1
        if 0 <= index < len(color_list):
            selected_color = color_list[index]
            result = SelectionResult()
            if "全部颜色" in selected_color.name:
                result.is_batch = True
                result.batch_type = group_name.lower()
                result.display_name = selected_color.name
                if group_idx == 1:
                    for i in range(10):
                        result.search_codes.append(COLOR_LIST[i].code)
                        result.search_names.append(COLOR_LIST[i].name)
                elif group_idx == 2:
                    for i in range(10, 19):
                        result.search_codes.append(COLOR_LIST[i].code)
                        result.search_names.append(COLOR_LIST[i].name)
                else:
                    for i in range(19, len(COLOR_LIST)):
                        result.search_codes.append(COLOR_LIST[i].code)
                        result.search_names.append(COLOR_LIST[i].name)
                ColorManager.print_success(f"已选择: {result.display_name}（共 {len(result.search_codes)} 种颜色）")
            else:
                result.is_batch = False
                result.search_codes = [selected_color.code]
                result.search_names = [selected_color.name]
                result.display_name = selected_color.name
                ColorManager.print_success(f"已选择: {result.display_name}")
            return result, group_idx
    return None, group_idx
def find_item_uexp() -> str:
    if not Path(UNPACK_DIR).exists():
        raise RuntimeError(f"解包目录不存在: {UNPACK_DIR}")
    for entry in Path(UNPACK_DIR).rglob("Item.uexp"):
        if entry.is_file():
            return str(entry)
    raise RuntimeError("在解包目录中未找到 Item.uexp 文件")
def copy_to_pack_dir(source_file: str) -> bool:
    dest_file = Path(PACK_DIR) / "Item.uexp"
    try:
        Path(PACK_DIR).mkdir(parents=True, exist_ok=True)
        shutil.copy2(source_file, dest_file)
        ColorManager.print_success(f"已复制 Item.uexp 到打包目录: {PACK_DIR}")
        return True
    except Exception as e:
        ColorManager.print_error(f"复制文件失败: {str(e)}")
        return False
def locate_144byte_region(content: bytes) -> tuple:
    first_pattern = bytes(FIXED_PATTERNS[0])
    region_start = content.find(first_pattern)
    if region_start == -1:
        ColorManager.print_error("未找到品质颜色区域")
        return False, 0, 0
    region_end = region_start + (len(FIXED_PATTERNS) * 8)
    return True, region_start, region_end
def replace_colors_in_region(content: bytearray, region_start: int, region_end: int,
                           search_codes: list, search_names: list,
                           replace_code: str, replace_name: str) -> tuple:
    replace_bytes = []
    for i in range(0, len(replace_code), 2):
        byte_str = replace_code[i:i+2]
        replace_bytes.append(int(byte_str, 16))
    replace_bytes = bytes(replace_bytes)
    total_replace_count = 0
    for idx in range(len(search_codes)):
        search_code = search_codes[idx]
        search_name = search_names[idx]
        search_bytes = []
        for i in range(0, len(search_code), 2):
            byte_str = search_code[i:i+2]
            search_bytes.append(int(byte_str, 16))
        search_bytes = bytes(search_bytes)
        pos = region_start
        replace_count = 0
        while pos < region_end:
            pos = content.find(search_bytes, pos, region_end)
            if pos == -1:
                break
            content[pos:pos+len(search_bytes)] = replace_bytes
            replace_count += 1
            pos += len(search_bytes)
        if replace_count > 0:
            ColorManager.print_success(f"颜色 {search_name} 找到 {replace_count} 处匹配")
            total_replace_count += replace_count
    if total_replace_count == 0:
        ColorManager.print_warning("未找到任何匹配的颜色序列")
        return False, 0
    ColorManager.print_success(f"总共找到 {total_replace_count} 处匹配，正在替换为 {replace_name}...")
    return True, total_replace_count
def modify_quality_color(file_path: str, search_result: SelectionResult,
                        replace_code: str, replace_name: str) -> bool:
    try:
        with open(file_path, 'rb') as f:
            content = bytearray(f.read())
    except Exception as e:
        ColorManager.print_error(f"无法打开文件: {str(e)}")
        return False
    ColorManager.print_info(f"文件大小: {len(content)} 字节")
    success = False
    total_replace_count = 0
    found, region_start, region_end = locate_144byte_region(bytes(content))
    if not found:
        return False
    success, total_replace_count = replace_colors_in_region(
        content, region_start, region_end,
        search_result.search_codes, search_result.search_names,
        replace_code, replace_name
    )
    if not success or total_replace_count == 0:
        return False
    try:
        with open(file_path, 'wb') as f:
            f.write(bytes(content))
    except Exception as e:
        ColorManager.print_error(f"无法保存文件: {str(e)}")
        return False
    ColorManager.print_success(f"成功替换品质颜色为 {replace_name}")
    return True
# ====================== 解包核心类 ======================
class FastPakExtractor:
    def __init__(self):
        self.encrypt = 0
        self.mm = None
        self.selected_pak = None
        self.compression_info = {}
        self.target_files = set()
        self.base_path = "../../../"
        self.file_success = 0
        self.file_fail = 0
    
    def get_pak_files(self, directory):
        pak_dir = Path(directory)
        if not pak_dir.exists():
            ColorManager.print_error(f"目录不存在: {directory}")
            return []
        return list(pak_dir.glob("*.pak"))
    
    def select_pak_file(self, pak_files):
        if not pak_files:
            ColorManager.print_error("未找到.pak文件")
            return None
        print(f"\n{ColorManager.bright_yellow('可用的.pak文件:')}")
        print(DIVIDER)
        for i, pak_file in enumerate(pak_files, 1):
            size_mb = pak_file.stat().st_size / 1024 / 1024
            print(f"{ColorManager.white(f'{i:2d}: {pak_file.name:<30} ({size_mb:6.2f} MB)')}")
        while True:
            try:
                choice = input(f"\n{ColorManager.cyan('[提示] 请选择文件编号 (输入0退出): ')}").strip()
                if choice == "0":
                    print("已退出程序。")
                    sys.exit(0)
                choice = int(choice)
                if 1 <= choice <= len(pak_files):
                    self.selected_pak = pak_files[choice - 1]
                    self.file_success = 0
                    self.file_fail = 0
                    return True
                ColorManager.print_warning(f"无效的选择，请输入1~{len(pak_files)}之间的数字")
            except ValueError:
                ColorManager.print_warning("请输入有效的数字")
    
    def read_string(self, offset, length, encoding='utf-8'):
        data = self.mm[offset:offset+length]
        try:
            return data.decode(encoding).rstrip('\x00')
        except:
            return data
    
    def read_unicode_string(self, offset, length):
        data = self.mm[offset:offset+length*2]
        try:
            return data.decode('utf-16-le').rstrip('\x00')
        except:
            return data
    
    def get_base_path(self, offset):
        name_size = struct.unpack('<I', self.mm[offset:offset+4])[0]
        if name_size == 0:
            return "../../../", offset+4
        base_path = self.read_string(offset+4, name_size)
        if name_size != 0x0A and name_size < 0xFF:
            base_path = "../../../" + base_path
        self.base_path = base_path
        return base_path, offset+4+name_size
    
    def find_magic_offset(self):
        pattern1 = b"\x2E\x2E\x2F\x2E\x2E\x2F\x2E\x2E\x2F"
        pattern2 = b"\x57\x57\x56\x57\x57\x56\x57\x57\x56"
        data = self.mm[:]
        offset1 = data.find(pattern1)
        offset2 = data.find(pattern2)
        if offset1 != -1:
            self.encrypt = 0
            return offset1 - 4
        elif offset2 != -1:
            self.encrypt = 1
            return offset2 - 4
        else:
            ColorManager.print_warning("未找到标准魔法字节，尝试默认偏移")
            return len(self.mm) - 0x2C
    
    def decrypt_data(self, data):
        if not self.encrypt:
            return data
        return bytes([b ^ ENCRYPT_KEY for b in data])
    
    def parse_file_entry(self, offset):
        entry_start = offset
        hash_data = self.mm[offset:offset+20]
        offset += 20
        entry_data = self.mm[offset:offset+49]
        offset += 49
        if len(entry_data) < 49:
            return None, offset
        entry = {
            'hash': hash_data,
            'offset': struct.unpack('<Q', entry_data[0:8])[0],
            'size': struct.unpack('<Q', entry_data[8:16])[0],
            'zip': struct.unpack('<I', entry_data[16:20])[0],
            'zsize': struct.unpack('<Q', entry_data[20:28])[0],
            'chunks': [],
            'chunk_size': 0x10000,
            'encrypted': 0
        }
        if entry['zip'] != 0:
            chunk_count = struct.unpack('<I', self.mm[offset:offset+4])[0]
            offset += 4
            chunk_count = min(chunk_count, 1000)
            for _ in range(chunk_count):
                chunk_data = self.mm[offset:offset+16]
                offset += 16
                if len(chunk_data) >= 16:
                    chunk_offset = struct.unpack('<Q', chunk_data[0:8])[0]
                    chunk_end = struct.unpack('<Q', chunk_data[8:16])[0]
                    entry['chunks'].append((chunk_offset, chunk_end))
        chunk_size_data = self.mm[offset:offset+5]
        offset += 5
        if len(chunk_size_data) >= 5:
            entry['chunk_size'] = struct.unpack('<I', chunk_size_data[0:4])[0]
            entry['encrypted'] = chunk_size_data[4]
        else:
            entry['chunk_size'] = 0x10000
            entry['encrypted'] = 0
        if self.encrypt:
            encrypted_entry = self.mm[entry_start:offset]
            decrypted_entry = self.decrypt_data(encrypted_entry)
            decrypted = decrypted_entry
            hash_data = decrypted[0:20]
            entry_data = decrypted[20:20+49]
            entry['offset'] = struct.unpack('<Q', entry_data[0:8])[0]
            entry['size'] = struct.unpack('<Q', entry_data[8:16])[0]
            entry['zip'] = struct.unpack('<I', entry_data[16:20])[0]
            entry['zsize'] = struct.unpack('<Q', entry_data[20:28])[0]
        return entry, offset
    
    def extract_file(self, entry, output_path):
        try:
            if entry['chunks']:
                result = self.extract_chunked(entry, output_path)
            else:
                result = self.extract_simple(entry, output_path)
            if result:
                self.file_success += 1
            else:
                self.file_fail += 1
            return result
        except Exception as e:
            if not MIN_PRINT:
                ColorManager.print_warning(f"提取失败 {output_path.name}: {str(e)}")
            self.file_fail += 1
            return False
    
    def extract_simple(self, entry, output_path):
        if entry['zip'] != 0:
            compressed_data = self.mm[entry['offset']:entry['offset']+entry['zsize']]
            if self.encrypt and entry.get('encrypted', 0) == 1:
                compressed_data = self.decrypt_data(compressed_data)
            try:
                data = zlib.decompress(compressed_data)
            except:
                data = compressed_data
        else:
            data = self.mm[entry['offset']:entry['offset']+entry['size']]
            if self.encrypt and entry.get('encrypted', 0) == 1:
                data = self.decrypt_data(data)
        output_path.parent.mkdir(parents=True, exist_ok=True)
        with open(output_path, 'wb') as f:
            f.write(data)
        return True
    
    def extract_chunked(self, entry, output_path):
        total_size = entry['size']
        remaining = total_size
        output_path.parent.mkdir(parents=True, exist_ok=True)
        with open(output_path, 'wb') as out:
            for chunk_offset, chunk_end in entry['chunks']:
                chunk_zsize = chunk_end - chunk_offset
                chunk_data = self.mm[chunk_offset:chunk_offset+chunk_zsize]
                if self.encrypt and entry.get('encrypted', 0) == 1:
                    chunk_data = self.decrypt_data(chunk_data)
                if entry['zip'] != 0:
                    try:
                        decompressed = zlib.decompress(chunk_data)
                        write_size = min(len(decompressed), remaining)
                        out.write(decompressed[:write_size])
                        remaining -= write_size
                    except:
                        write_size = min(len(chunk_data), remaining)
                        out.write(chunk_data[:write_size])
                        remaining -= write_size
                else:
                    write_size = min(len(chunk_data), remaining)
                    out.write(chunk_data[:write_size])
                    remaining -= write_size
                if remaining <= 0:
                    break
        return remaining <= 0
    
    def parse_toc(self, toc_offset, toc_size):
        toc_data = self.mm[toc_offset:toc_offset+toc_size]
        pos = 0
        entries = []
        stack = [(1, 0)]
        while stack:
            flag, count = stack.pop()
            if flag == 1:
                dir_count = struct.unpack('<Q', toc_data[pos:pos+8])[0]
                pos += 8
                stack.append((0, dir_count))
                continue
            if count == 0:
                continue
            count -= 1
            name_size = struct.unpack('<i', toc_data[pos:pos+4])[0]
            pos += 4
            if name_size >= 0:
                dir_name = toc_data[pos:pos+name_size].decode('utf-8').rstrip('\x00')
                pos += name_size
            else:
                dir_name = toc_data[pos:pos+abs(name_size)*2].decode('utf-16-le').rstrip('\x00')
                pos += abs(name_size)*2
            file_count = struct.unpack('<Q', toc_data[pos:pos+8])[0]
            pos += 8
            if file_count == 0:
                stack.append((0, count))
                continue
            for _ in range(file_count):
                name_size = struct.unpack('<i', toc_data[pos:pos+4])[0]
                pos += 4
                if name_size > 0:
                    file_name = toc_data[pos:pos+name_size].decode('utf-8').rstrip('\x00')
                    pos += name_size
                else:
                    file_name = toc_data[pos:pos+abs(name_size)*2].decode('utf-16-le').rstrip('\x00')
                    pos += abs(name_size)*2
                full_path = f"{dir_name}{file_name}"
                entry_index = struct.unpack('<I', toc_data[pos:pos+4])[0]
                pos += 4
                entries.append((entry_index, full_path))
            if count > 0:
                stack.append((0, count))
        return entries
    
    def unpack_pak(self):
        if not self.selected_pak:
            ColorManager.print_error("未选择PAK文件")
            return False
        ColorManager.print_info(f"\n开始极速解包: {self.selected_pak.name}")
        ColorManager.print_info(f"查找目标文件: {TARGET_FILE_PATTERN}")
        unpack_path = Path(UNPACK_DIR)
        if unpack_path.exists():
            shutil.rmtree(unpack_path)
        unpack_path.mkdir(parents=True, exist_ok=True)
        start = time.time()
        self.compression_info.clear()
        self.target_files.clear()
        try:
            with open(self.selected_pak, 'rb') as f:
                self.mm = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
            magic_offset = self.find_magic_offset()
            base_path, pos = self.get_base_path(magic_offset)
            file_count = struct.unpack('<I', self.mm[pos:pos+4])[0]
            pos += 4
            ColorManager.print_success(f"PAK内文件总数: {file_count}")
            entries = []
            for i in range(file_count):
                entry, pos = self.parse_file_entry(pos)
                if entry:
                    entries.append(entry)
                else:
                    break
            entries_count = struct.unpack('<Q', self.mm[pos:pos+8])[0]
            pos += 8
            if self.encrypt:
                pos += 1
            toc_offset = pos
            toc_size = len(self.mm) - toc_offset
            toc_entries = self.parse_toc(toc_offset, toc_size)
            tasks = []
            for entry_idx, full_path in toc_entries:
                if TARGET_FILE_PATTERN == full_path.split('/')[-1] and entry_idx < len(entries):
                    entry = entries[entry_idx].copy()
                    rel_path = full_path
                    if self.base_path and self.base_path != "../../../":
                        rel_path = self.base_path + full_path
                    while rel_path.startswith('../'):
                        rel_path = rel_path[3:]
                    output_path = Path(UNPACK_DIR) / rel_path
                    self.compression_info[full_path] = namedtuple('CompressionInfo', [
                        'offset', 'size', 'zip', 'zsize', 'encrypted', 'chunks', 'chunk_size'
                    ])(
                        offset=entry['offset'], size=entry['size'], zip=entry['zip'],
                        zsize=entry['zsize'], encrypted=entry['encrypted'],
                        chunks=entry['chunks'], chunk_size=entry['chunk_size']
                    )
                    self.target_files.add(full_path)
                    tasks.append((entry, output_path, full_path))
            with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
                futures = {executor.submit(self.extract_file, entry, out_path): (entry, out_path, full_path) for entry, out_path, full_path in tasks}
                for future in as_completed(futures):
                    future.result()
            print()
            print(DIVIDER)
            ColorManager.print_success(f"解包完成! 成功提取 {self.file_success} 个文件，失败 {self.file_fail} 个")
            print(DIVIDER)
        except Exception as e:
            print()
            print(DIVIDER)
            ColorManager.print_error(f"解包过程出错: {str(e)}")
            print(DIVIDER)
            import traceback
            traceback.print_exc()
            return False
        finally:
            if self.mm:
                self.mm.close()
        elapsed = time.time() - start
        ColorManager.print_success(f"解包耗时: {elapsed:>6.2f} 秒")
        return True
# ====================== 打包核心类 ======================
class PakPacker:
    def __init__(self, extractor):
        self.extractor = extractor
        self.encrypt = extractor.encrypt
        self.success_count = 0
        self.fail_count = 0
        self.file_success = 0
        self.file_fail = 0
        self.zopfli_manager = ZopfliManager()
        self.zlib_used = 0
        self.zopfli_used = 0
        self.CompressionInfo = namedtuple('CompressionInfo', [
            'offset', 'size', 'zip', 'zsize', 'encrypted', 'chunks', 'chunk_size'
        ])
    
    def encrypt_data(self, data, encrypt_flag):
        if self.encrypt and encrypt_flag:
            return bytes([b ^ ENCRYPT_KEY for b in data])
        return data
    
    def replace_non_chunked_file(self, pak_file, info, new_data):
        try:
            if info.zip != 0:
                compressed = self.zopfli_manager.try_zlib_compress(new_data, info.zsize)
                if compressed:
                    self.zlib_used += 1
                else:
                    compressed = self.zopfli_manager.compress_zopfli(new_data, info.zsize)
                    if compressed:
                        self.zopfli_used += 1
                    else:
                        compressed = new_data.ljust(info.zsize, b'\x00')
                        ColorManager.print_warning(f"文件压缩失败，强制填充: {info.zsize} 字节")
                        return False
                compressed = self.encrypt_data(compressed, info.encrypted)
                pak_file.seek(info.offset)
                pak_file.write(compressed)
            else:
                if len(new_data) > info.size:
                    new_data = new_data[:info.size]
                    ColorManager.print_warning(f"文件超出原大小，截断至: {info.size} 字节")
                data_to_write = new_data.ljust(info.size, b'\x00')
                data_to_write = self.encrypt_data(data_to_write, info.encrypted)
                pak_file.seek(info.offset)
                pak_file.write(data_to_write)
            return True
        except Exception as e:
            ColorManager.print_error(f"非分块文件写入失败: {str(e)[:50]}")
            return False
    
    def replace_chunked_file(self, pak_file, info, new_data):
        if not info.chunks:
            return False
        total_size = info.size
        data_pos = 0
        all_success = True
        for i, (chunk_offset, chunk_end) in enumerate(info.chunks):
            chunk_zsize = chunk_end - chunk_offset
            if i == len(info.chunks) - 1:
                chunk_data_size = total_size - data_pos
            else:
                chunk_data_size = min(info.chunk_size, total_size - data_pos)
            if chunk_data_size <= 0:
                break
            chunk_data = new_data[data_pos:data_pos+chunk_data_size]
            data_pos += chunk_data_size
            if info.zip != 0:
                compressed = self.zopfli_manager.try_zlib_compress(chunk_data, chunk_zsize)
                if compressed:
                    self.zlib_used += 1
                else:
                    compressed = self.zopfli_manager.compress_zopfli(chunk_data, chunk_zsize)
                    if compressed:
                        self.zopfli_used += 1
                    else:
                        compressed = chunk_data.ljust(chunk_zsize, b'\x00')
                        ColorManager.print_warning(f"分块压缩失败，强制填充: {chunk_zsize} 字节")
                        all_success = False
                chunk_data = compressed
            chunk_data = self.encrypt_data(chunk_data, info.encrypted)
            if len(chunk_data) < chunk_zsize:
                chunk_data = chunk_data.ljust(chunk_zsize, b'\x00')
            pak_file.seek(chunk_offset)
            pak_file.write(chunk_data)
        return all_success
    
    def pack_files(self, pak_path, pack_dir):
        ColorManager.print_info(f"开始打包，待打包目录: {pack_dir}")
        pack_dir = Path(pack_dir)
        if not pack_dir.exists():
            ColorManager.print_error(f"打包目录不存在: {pack_dir}")
            return False
        file_map = {}
        for root, dirs, files in os.walk(pack_dir):
            for file in files:
                file_path = Path(root) / file
                file_map[file.lower()] = file_path
        ColorManager.print_info(f"打包目录找到 {len(file_map)} 个文件")
        if not file_map:
            ColorManager.print_warning("打包目录无文件，跳过打包")
            return True
        try:
            with open(pak_path, 'r+b') as pak_file:
                total = len(self.extractor.compression_info)
                processed = 0
                for full_path, info in self.extractor.compression_info.items():
                    processed += 1
                    ColorManager.print_progress(processed, total)
                    file_name = Path(full_path).name.lower()
                    if file_name not in file_map:
                        self.file_fail += 1
                        self.fail_count += 1
                        continue
                    new_file_path = file_map[file_name]
                    new_data = Utils.read_file(new_file_path)
                    if not new_data:
                        self.file_fail += 1
                        self.fail_count += 1
                        continue
                    if info.chunks:
                        write_success = self.replace_chunked_file(pak_file, info, new_data)
                    else:
                        write_success = self.replace_non_chunked_file(pak_file, info, new_data)
                    if write_success:
                        self.file_success += 1
                        self.success_count += 1
                    else:
                        self.file_fail += 1
                        self.fail_count += 1
            print()
            return True
        except Exception as e:
            ColorManager.print_error(f"PAK文件写入失败: {str(e)}")
            return False
    
    def run(self, pak_path):
        ColorManager.print_info(f"\n开始打包流程（压缩策略：勿辞独家双压缩）")
        print(DIVIDER)
        start_time = time.time()
        if self.pack_files(pak_path, PACK_DIR):
            duration = time.time() - start_time
            print(DIVIDER)
            ColorManager.print_success("打包完成！")
            ColorManager.print_info(f"总耗时: {Utils.format_time(duration)}")
            ColorManager.print_info(f"成功打包: {self.file_success} 个文件")
            ColorManager.print_info(f"打包失败: {self.file_fail} 个文件")
            ColorManager.print_info(f"zlib压缩使用: {self.zlib_used} 次")
            ColorManager.print_info(f"Zopfli压缩使用: {self.zopfli_used} 次")
        else:
            ColorManager.print_error("打包流程失败")
# ====================== 主流程 ======================
def main():
    # 清空目录（隐藏打印）
    dirs_to_clear = [UNPACK_DIR, PACK_DIR]
    for dir_path in dirs_to_clear:
        path = Path(dir_path)
        if path.exists():
            try:
                shutil.rmtree(path)
            except Exception as e:
                ColorManager.print_error(f"清空目录 {dir_path} 失败: {str(e)}")
                sys.exit(1)
        path.mkdir(parents=True, exist_ok=True)
    extractor = FastPakExtractor()
    pak_files = extractor.get_pak_files(PAK_DIR)
    if not pak_files:
        ColorManager.print_error("未找到任何.pak文件，程序退出")
        sys.exit(1)
    if not extractor.select_pak_file(pak_files):
        sys.exit(1)
    if not extractor.unpack_pak():
        ColorManager.print_error("解包失败，程序退出")
        sys.exit(1)
    print(f"\n{DIVIDER}")
    ColorManager.print_info("步骤1: 搜索并复制 Item.uexp")
    print(DIVIDER)
    spinner = RomanticSpinner("正在递归搜索 Item.uexp 文件...", "|/-\\")
    spinner.start()
    source_file = ""
    try:
        source_file = find_item_uexp()
        spinner.stop("找到 Item.uexp 文件")
        ColorManager.print_success(f"文件路径: {source_file}")
    except Exception as e:
        spinner.stop("搜索失败")
        ColorManager.print_error(str(e))
        sys.exit(1)
    if not copy_to_pack_dir(source_file):
        sys.exit(1)
    print(f"\n{DIVIDER}")
    ColorManager.print_info("步骤2: 选择要修改的颜色（搜索色）")
    print(DIVIDER)
    search_result, _ = select_color()
    print(f"\n{DIVIDER}")
    ColorManager.print_info("步骤3: 选择修改后的颜色（目标色）")
    print(DIVIDER)
    replace_result, _ = select_color()
    if replace_result.is_batch:
        ColorManager.print_error("替换目标不能是批量选项，请选择单个颜色")
        sys.exit(1)
    print(f"\n{DIVIDER}")
    ColorManager.print_info("步骤4: 修改品质颜色")
    print(DIVIDER)
    target_file = Path(PACK_DIR) / "Item.uexp"
    ColorManager.print_info(f"目标文件: {str(target_file)}")
    ColorManager.print_info(f"搜索: {search_result.display_name}")
    ColorManager.print_info(f"替换为: {replace_result.display_name}")
    modify_spinner = RomanticSpinner("正在修改品质颜色...", "|/-\\")
    modify_spinner.start()
    success = modify_quality_color(
        str(target_file),
        search_result,
        replace_result.search_codes[0],
        replace_result.search_names[0]
    )
    if success:
        modify_spinner.stop("品质修改完成")
    else:
        modify_spinner.stop("品质修改失败")
        sys.exit(1)
    print(f"\n{DIVIDER}")
    ColorManager.print_info("步骤5: 开始打包PAK")
    print(DIVIDER)
    packer = PakPacker(extractor)
    packer.run(extractor.selected_pak)
    # 结尾双等号分隔线（统一样式）
    print(f"\n========================================")
    print(ColorManager.bright_yellow("  勿辞品质修改全流程操作成功完成！  "))
    print(f"========================================")
if __name__ == "__main__":
    os.environ['PYTHONIOENCODING'] = 'utf-8'
    try:
        main()
    except KeyboardInterrupt:
        print(f"\n{DIVIDER}")
        ColorManager.print_error("程序被用户中断")
        print(DIVIDER)
        sys.exit(1)
    except Exception as e:
        print(f"\n{DIVIDER}")
        ColorManager.print_error(f"运行出错: {str(e)}")
        print(DIVIDER)
        import traceback
        traceback.print_exc()
        sys.exit(1)
