求助365里的星之守护怎么解包

好像就是寻星旅团

文件名什么的自己还原。
图片匹配我准备用opencv试试。

解密代码

import os

target_bytes = b'?B`o_Dg\\^'
xor_key = bytes.fromhex('30 78 30 30 30 30 30 30')


def process_file(file_path):
    with open(file_path, 'rb') as f:
        content = f.read()

    if content[:9] == target_bytes:
        content = content[9:]

        xor_content = bytes([content[i] ^ xor_key[i % len(xor_key)] for i in range(len(content))])

        with open(file_path, 'wb') as f:
            f.write(xor_content)
        print(f"Processed: {file_path}")
    else:
        print(f"Skipped: {file_path}")


def traverse_directory(directory):
    for root, dirs, files in os.walk(directory):
        for file in files:
            file_path = os.path.join(root, file)
            process_file(file_path)


current_directory = r"C:\Users\usernamegosehere\Desktop\com.xzsh.tw.ero\files\assets\src"
traverse_directory(current_directory)

之前用来分类hcg的代码需要解密文件后根据文件头手动对一些文件归类

import os
import hashlib
import shutil


def calculate_md5(input_string):
    return hashlib.md5(input_string.encode('utf-8')).hexdigest()


def process_spine_files(spine_name):
    assets_path = r"C:\Users\usernamegosehere\Desktop\com.xzsh.tw.ero\files\assets\res"
    output_path = r"C:\Users\usernamegosehere\Desktop\xko"
    pvr_path = r"C:\Users\usernamegosehere\Desktop\1111"


    # 处理atlas文件
    atlas_input = f"res/spine/{spine_name}/{spine_name}.atlastest_fun"
    atlas_md5 = calculate_md5(atlas_input)
    atlas_source = os.path.join(assets_path, f"{atlas_md5}.g")

    if os.path.exists(atlas_source):
        # 创建输出文件夹
        output_dir = os.path.join(output_path, spine_name)
        os.makedirs(output_dir, exist_ok=True)
        atlas_dest = os.path.join(output_dir, f"{spine_name}.atlas")
        shutil.copy2(atlas_source, atlas_dest)
        print(f"成功复制 atlas 文件到 {atlas_dest}")
    else:
        print(f"未找到 atlas 文件: {atlas_source}")

    # 处理skel文件
    skel_input = f"res/spine/{spine_name}/{spine_name}.skeltest_fun"
    skel_md5 = calculate_md5(skel_input)
    skel_source = os.path.join(assets_path, f"{skel_md5}.skel")

    if os.path.exists(skel_source):
        skel_dest = os.path.join(output_dir, f"{spine_name}.skel")
        shutil.copy2(skel_source, skel_dest)
        print(f"成功复制 skel 文件到 {skel_dest}")
    else:
        print(f"未找到 skel 文件: {skel_source}")

    # 处理pvr.ccz文件 (1-10)
    for i in range(1, 11):
        suffix = "" if i == 1 else str(i)
        pvr_input = f"res/spine/{spine_name}/{spine_name}{suffix}.pvr.ccztest_fun"
        pvr_md5 = calculate_md5(pvr_input)
        pvr_source = os.path.join(pvr_path, f"{pvr_md5}.pvr.ccz")
        print(pvr_md5)

        if os.path.exists(pvr_source):
            pvr_dest = os.path.join(output_dir, f"{spine_name}{suffix}.pvr.ccz")
            shutil.copy2(pvr_source, pvr_dest)
            print(f"成功复制 pvr.ccz 文件 #{i} 到 {pvr_dest}")
        else:
            print(f"未找到 pvr.ccz 文件 #{i}: {pvr_source}")


def main():
    spine_name = input("请输入spine字符串名称: ")
    process_spine_files(spine_name)
    print("处理完成!")




if __name__ == "__main__":
    main()

文件路径是审计相关字幕文件时候发现的文件名和相关路径参数

这里你需要找到他的字幕文件使用notepad++对解密文件搜索任意字幕找到他的字幕文件在每个字幕边上会有一个Hxxxx这种编号的就是
这里给份当时解包时审计发现的部分spine

2025/05/14  12:17    <DIR>          H30004_c
2025/05/14  12:17    <DIR>          H30004_c2
2025/05/14  12:17    <DIR>          H30004_r
2025/05/14  12:17    <DIR>          H30009_c
2025/05/14  12:17    <DIR>          H30009_r
2025/05/14  12:17    <DIR>          H30010_c
2025/05/14  12:17    <DIR>          H30010_r
2025/05/14  12:17    <DIR>          H30015_c
2025/05/14  12:17    <DIR>          H30015_r
2025/05/14  12:17    <DIR>          H30019_c
2025/05/14  12:17    <DIR>          H30019_r
2025/05/14  12:17    <DIR>          H30026_c
2025/05/14  12:17    <DIR>          H30026_r
2025/05/14  12:17    <DIR>          H30031_c
2025/05/14  12:17    <DIR>          H30031_r
2025/05/14  12:17    <DIR>          H30041_c
2025/05/14  12:17    <DIR>          H30041_r
2025/05/14  12:17    <DIR>          H30054_c
2025/05/14  12:17    <DIR>          H30054_r
2025/05/14  12:17    <DIR>          H30058_c
2025/05/14  12:17    <DIR>          H30058_r
2025/05/14  12:17    <DIR>          H30059_c
2025/05/14  12:17    <DIR>          H30059_r
2025/05/14  12:17    <DIR>          H30060_c
2025/05/14  12:17    <DIR>          H30060_r
2025/05/14  12:17    <DIR>          H30063_c
2025/05/14  12:17    <DIR>          H30063_r
2025/05/14  12:17    <DIR>          H30064_c
2025/05/14  12:17    <DIR>          H30072_c
2025/05/14  12:17    <DIR>          H30072_r
2025/05/14  12:17    <DIR>          H30074_c
2025/05/14  12:17    <DIR>          H30074_r
2025/05/14  12:17    <DIR>          H30077_c
2025/05/14  12:17    <DIR>          H30077_r
2025/05/14  12:17    <DIR>          H30079_c
2025/05/14  12:17    <DIR>          H30079_r
2025/05/14  12:17    <DIR>          H30091_c
2025/05/14  12:17    <DIR>          H30091_r
2025/05/14  12:17    <DIR>          H30093_c
2025/05/14  12:17    <DIR>          H30093_r
2025/05/14  12:17    <DIR>          H30094_c
2025/05/14  12:17    <DIR>          H30094_r
2025/05/14  12:17    <DIR>          H30095_c
2025/05/14  12:17    <DIR>          H30095_r
2025/05/14  12:17    <DIR>          H30097_c
2025/05/14  12:17    <DIR>          H30097_r
2025/05/14  12:17    <DIR>          H30098_c
2025/05/14  12:17    <DIR>          H30098_r
2025/05/14  12:17    <DIR>          H30099_c
2025/05/14  12:17    <DIR>          H30099_r
2025/05/14  12:17    <DIR>          H30104_c
2025/05/14  12:17    <DIR>          H30104_r
2025/05/14  12:17    <DIR>          H30107_c
2025/05/14  12:17    <DIR>          H30107_r
2025/05/14  12:17    <DIR>          H30108_c
2025/05/14  12:17    <DIR>          H30108_r
2025/05/14  12:17    <DIR>          H30110_c
2025/05/14  12:17    <DIR>          H30110_r
2025/05/14  12:17    <DIR>          H30112_c
2025/05/14  12:17    <DIR>          H30112_r

我还在想通过识别atlas提供的图块区域用opencv识别匹配图像来匹配png图。

1、图片xor解密
import os
import sys
import argparse
import concurrent.futures
import multiprocessing

MAGIC_BYTES = b'\x3f\x42\x60\x6f\x5f\x44\x67\x5c\x5e'
XOR_KEY_STR = "3078303030303030"
XOR_KEY = bytes.fromhex(XOR_KEY_STR)

def process_file(file_path, output_dir=None):
    try:
        with open(file_path, 'rb') as f:
            header = f.read(len(MAGIC_BYTES))
            if header != MAGIC_BYTES:
                print(f"跳过文件: {file_path} (前9字节不匹配)")
                return False, file_path, None
            content = f.read()
        
        processed_content = bytes(a ^ b for a, b in zip(content, XOR_KEY * (len(content) // len(XOR_KEY) + 1)))
        
        if output_dir:
            os.makedirs(output_dir, exist_ok=True)
            relative_path = os.path.relpath(file_path, os.path.dirname(file_path) if os.path.isfile(file_path) else file_path)
            output_path = os.path.join(output_dir, relative_path)
            os.makedirs(os.path.dirname(output_path), exist_ok=True)
        else:
            output_path = file_path
        
        with open(output_path, 'wb') as f:
            f.write(processed_content)
        return True, file_path, output_path
    except Exception as e:
        print(f"处理文件 {file_path} 时出错: {e}")
        return False, file_path, str(e)

def process_path(path, output_dir=None, continue_on_error=True):
    processed_count = 0
    total_count = 0
    errors = []
    
    if os.path.isfile(path):
        total_count = 1
        success, src, dest = process_file(path, output_dir)
        if success:
            processed_count += 1
            print(f"已处理文件: {src} -> {dest}")
        elif not continue_on_error:
            return processed_count, total_count, errors
        else:
            errors.append((src, dest))
    elif os.path.isdir(path):
        files_to_process = []
        for root, _, files in os.walk(path):
            for file in files:
                file_path = os.path.join(root, file)
                files_to_process.append(file_path)
                total_count += 1
        
        # 使用CPU核心数作为线程数
        max_workers = multiprocessing.cpu_count()
        print(f"使用 {max_workers} 个线程处理 {total_count} 个文件")
        
        with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
            future_to_file = {executor.submit(process_file, file_path, output_dir): file_path for file_path in files_to_process}
            
            for future in concurrent.futures.as_completed(future_to_file):
                file_path = future_to_file[future]
                try:
                    success, src, dest = future.result()
                    if success:
                        processed_count += 1
                        print(f"已处理文件: {src} -> {dest}")
                    else:
                        errors.append((src, dest))
                        if not continue_on_error:
                            break
                except Exception as e:
                    errors.append((file_path, str(e)))
                    print(f"处理文件 {file_path} 时出错: {e}")
                    if not continue_on_error:
                        break
    else:
        print(f"错误: 路径不存在或不可访问 - {path}")
    
    return processed_count, total_count, errors

def main():
    while True:
        path = input("\n请输入文件或文件夹路径 (输入'q'退出): ").strip()
        
        if path.lower() == 'q':
            break
        
        output_dir = input("请输入输出目录 (留空则覆盖原文件): ").strip() or None
        continue_on_error = input("发生错误时继续处理其他文件?(y/n): ").strip().lower() == 'y'
        
        processed, total, errors = process_path(path, output_dir, continue_on_error)
        print(f"处理完成: {processed}/{total} 文件已处理")
        
        if errors:
            print(f"共有 {len(errors)} 个错误:")
            for src, error in errors:
                print(f"  - {src}: {error}")

if __name__ == "__main__":
    main()
2、识别文件后缀
import os
import sys
import re
import shutil
from pathlib import Path
import concurrent.futures
import multiprocessing

def process_file(file_path, output_dir, folder_path):
    try:
        # 读取文件的前512字节
        with open(file_path, 'rb') as f:
            header = f.read(512)

            new_ext = None

            # 检查前512字节中是否存在不连续的 "size:"、"format:" 和 "filter:"
            header_text = header.decode('ascii', errors='ignore')
            size_pattern = re.compile(r'size:')
            format_pattern = re.compile(r'format:')
            filter_pattern = re.compile(r'filter:')

            size_match = size_pattern.search(header_text)
            format_match = format_pattern.search(header_text)
            filter_match = filter_pattern.search(header_text)

            if size_match and format_match and filter_match:
                # 确保顺序是 "size:" 在前,"format:" 其次,"filter:" 最后
                if size_match.start() < format_match.start() < filter_match.start():
                    new_ext = '.atlas'

            # 检查 PNG 文件头
            if new_ext is None and len(header) >= 4:
                if header[:4] == b'\x89PNG':
                    new_ext = '.png'

            # 检查 CCZ 文件头
            if new_ext is None and len(header) >= 3:
                if header[:3] == b'CCZ':
                    new_ext = '.pvr.ccz'

            # 检查前64字节中是否包含连续的 "3.4.02"
            if new_ext is None and b'3.4.02' in header[:64]:
                new_ext = '.skel'

            # 如果找到匹配,复制文件并修改扩展名
            if new_ext:
                # 创建与原文件相同的子目录结构
                rel_path = os.path.relpath(os.path.dirname(file_path), folder_path)
                output_subdir = os.path.join(output_dir, rel_path)
                os.makedirs(output_subdir, exist_ok=True)

                # 复制文件
                filename = os.path.basename(file_path)
                new_file_base = os.path.splitext(filename)[0]
                original_copy_path = os.path.join(output_subdir, filename)
                shutil.copy2(file_path, original_copy_path)

                # 修改扩展名
                new_file_path = os.path.splitext(original_copy_path)[0] + new_ext
                os.rename(original_copy_path, new_file_path)

                print(f"已复制并将文件 '{filename}' 重命名为 '{os.path.basename(new_file_path)}'")
                return 1
    except Exception as e:
        filename = os.path.basename(file_path)
        print(f"处理文件 '{filename}' 时出错: {str(e)}")
    return 0

def change_extensions(folder_path):
    """遍历指定文件夹,将符合条件的文件复制到output目录并修改扩展名"""
    # 检查文件夹是否存在
    if not os.path.exists(folder_path):
        print(f"错误:文件夹 '{folder_path}' 不存在!")
        return

    # 创建output目录
    output_dir = os.path.join(os.getcwd(), "output")
    os.makedirs(output_dir, exist_ok=True)

    total_files = 0
    modified_files = 0

    file_paths = []
    # 遍历文件夹中的所有文件
    for root, dirs, files in os.walk(folder_path):
        for filename in files:
            # 跳过非 .g 后缀的文件
            if not filename.endswith('.g'):
                continue

            total_files += 1
            file_path = os.path.join(root, filename)
            file_paths.append(file_path)

    # 获取 CPU 核心数
    num_threads = multiprocessing.cpu_count()
    with concurrent.futures.ThreadPoolExecutor(max_workers=num_threads) as executor:
        futures = [executor.submit(process_file, file_path, output_dir, folder_path) for file_path in file_paths]
        for future in concurrent.futures.as_completed(futures):
            modified_files += future.result()

    print(f"\n处理完成!共扫描 {total_files} 个 .g 文件,复制并修改了 {modified_files} 个文件。")
    print(f"修改后的文件保存在: {os.path.abspath(output_dir)}")

def main():
    while True:
        # 获取用户输入的文件夹路径
        folder_path = input("\n请输入要处理的文件夹路径(直接回车退出): ").strip()

        # 如果用户输入为空,退出程序
        if not folder_path:
            print("程序已退出。")
            break

        # 如果用户输入的是相对路径,转换为绝对路径
        if not os.path.isabs(folder_path):
            folder_path = os.path.join(os.getcwd(), folder_path)

        change_extensions(folder_path)

        # 询问是否继续
        while True:
            choice = input("\n是否继续处理其他文件夹?(y/n): ").lower().strip()
            if choice == 'y':
                break
            elif choice == 'n':
                print("程序已退出。")
                return
            else:
                print("无效输入,请输入 'y' 或 'n'。")

if __name__ == "__main__":
    main()

匹配atlas与skel还是会有一个atlas匹配多个skel的情况,因为有的skel部件名字相同但是多一点,还在完善。

3、匹配atlas与skel
import os
import re
import shutil
import concurrent.futures
import multiprocessing

def extract_atlas_entries(atlas_path):
    """从atlas文件中提取所有条目名称"""
    entries = []
    try:
        with open(atlas_path, 'r', encoding='utf-8') as f:
            content = f.read()
            # 匹配所有"名称\n  rotate"模式
            pattern = r'(\w+)\s*\n\s*rotate'
            entries = re.findall(pattern, content)
    except Exception as e:
        print(f"读取atlas文件 {atlas_path} 时出错: {e}")
    return entries

def check_skel_files(directory, entries):
    """检查skel文件是否包含所有条目(十六进制搜索)"""
    matching_skel_files = []
    for root, _, files in os.walk(directory):
        for filename in files:
            if filename.endswith('.skel'):
                skel_path = os.path.join(root, filename)
                try:
                    with open(skel_path, 'rb') as f:
                        skel_content = f.read()
                        # 检查是否包含所有条目
                        if all(entry.encode('utf-8') in skel_content for entry in entries):
                            matching_skel_files.append(skel_path)
                except Exception as e:
                    print(f"读取skel文件 {skel_path} 时出错: {e}")
    return matching_skel_files

def organize_files(directory, atlas_path, matching_skel_files):
    """根据分析结果组织文件"""
    # 获取脚本所在目录
    script_dir = os.path.dirname(os.path.abspath(__file__))
    # 创建output文件夹
    output_dir = os.path.join(script_dir, 'output')
    os.makedirs(output_dir, exist_ok=True)

    # 提取atlas文件名作为文件夹名
    atlas_filename = os.path.splitext(os.path.basename(atlas_path))[0]
    folder_name = f"{atlas_filename}_sequence"
    folder_path = os.path.join(output_dir, folder_name)
    os.makedirs(folder_path, exist_ok=True)

    # 移动atlas文件到新文件夹
    shutil.move(atlas_path, os.path.join(folder_path, os.path.basename(atlas_path)))

    # 复制skel文件到atlas所在的新文件夹
    for skel_path in matching_skel_files:
        shutil.copy2(skel_path, os.path.join(folder_path, os.path.basename(skel_path)))

    print(f"创建文件夹: {folder_path},包含 {len(matching_skel_files)} 个匹配的skel文件和1个atlas文件")

def process_atlas_file(directory, atlas_file):
    """处理单个atlas文件及其相关skel文件"""
    # 提取当前atlas文件的条目
    entries = extract_atlas_entries(atlas_file)
    print(f"从atlas文件 {atlas_file} 中提取了 {len(entries)} 个条目")

    # 检查skel文件是否包含所有条目
    matching_skel_files = check_skel_files(directory, entries)

    if matching_skel_files:
        print(f"找到 {len(matching_skel_files)} 个匹配的skel文件")
        # 组织文件
        organize_files(directory, atlas_file, matching_skel_files)
    else:
        print("未找到匹配的skel文件")

def process_directory(directory):
    """处理单个目录及其所有子目录"""
    # 检查路径是否存在
    if not os.path.exists(directory):
        print(f"错误: 路径 '{directory}' 不存在")
        return False

    if not os.path.isdir(directory):
        print(f"错误: '{directory}' 不是一个目录")
        return False

    print(f"\n开始处理目录: {directory}")

    # 1. 递归查找所有atlas文件
    atlas_files = []
    for root, _, files in os.walk(directory):
        for filename in files:
            if filename.endswith('.atlas'):
                atlas_files.append(os.path.join(root, filename))

    if not atlas_files:
        print("未找到任何atlas文件")
        return False

    print(f"找到 {len(atlas_files)} 个atlas文件")

    # 2. 获取CPU核心数,设置线程池大小
    cpu_count = multiprocessing.cpu_count()
    max_workers = max(1, cpu_count)  # 至少使用1个线程
    
    print(f"使用 {max_workers} 个线程处理atlas文件")

    # 3. 使用线程池并行处理atlas文件
    with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
        # 创建任务
        futures = [executor.submit(process_atlas_file, directory, atlas_file) 
                  for atlas_file in atlas_files]
        
        # 等待所有任务完成
        for future in concurrent.futures.as_completed(futures):
            try:
                future.result()  # 获取任务结果,处理异常
            except Exception as e:
                print(f"处理atlas文件时发生错误: {e}")

    print("处理完成")
    return True

def main():
    """主函数 - 支持多次处理不同目录"""
    while True:
        # 获取用户输入的文件夹路径
        directory = input("\n请输入要处理的文件夹路径 (输入 'q' 退出): ").strip()

        if directory.lower() == 'q':
            break

        if not directory:
            print("错误: 路径不能为空")
            continue

        # 处理目录
        process_directory(directory)

        # 询问是否继续
        choice = input("\n是否要处理其他目录? (y/n): ").strip().lower()
        if choice != 'y':
            break

    print("\n感谢使用!")

if __name__ == "__main__":
    main()

opencv目前的想法是获取透明图层,然后二值化,再获取atlas的每个部件的范围,然后检测范围内是否只有一个图块,若全部检测通过就是.atlas对应的png,复制顺便改个名(如果有多个就按透明占比最小的图优先)。不过计算量感觉挺大(能跑就行)。

他的spine资源都在这里
文件命名格式就是路径后面加test_fun然后md5
想获取所有spine资源你得去审计他的lua文件里面应该是有列表的
如角色方面有个清单文件里面包含了角色相关的文本和ID其中你可以拿这个"ID"根据这个文件命名格式找到对应的文件,

感谢提醒。找到了角色清单,但是依然没找到寝的,估计是和文本一起传进来的。

角色的地址是类似res/spine/{ico_id}/show.{skel/atlas/pvr.czz}test_fun

你的脚本skel和pvr.czz 的md5处理后依然是原来后缀而不是g后缀。

这是修改后的脚本,添加了角色的spine获取,添加了从清单中直接自动获取spine的功能。

获取角色
import os
import hashlib
import shutil
import logging
from pathlib import Path
import re

def calculate_md5(input_string):
    """计算输入字符串的MD5哈希值"""
    return hashlib.md5(input_string.encode('utf-8')).hexdigest()

def process_spine_files(spine_name, assets_path, output_path, pvr_path):
    """处理单个Spine动画文件集"""
    output_dir = Path(output_path) / spine_name
    output_dir.mkdir(parents=True, exist_ok=True)
    
    # 处理atlas文件
    process_atlas_file(spine_name, assets_path, output_dir)
    
    # 处理skel文件
    process_skel_file(spine_name, assets_path, output_dir)
    
    # 处理pvr.ccz文件
    process_pvr_files(spine_name, pvr_path, output_dir)

def process_atlas_file(spine_name, assets_path, output_dir):
    """处理atlas文件"""
    atlas_input = f"res/spine/{spine_name}/show.atlastest_fun"
    atlas_md5 = calculate_md5(atlas_input)
    atlas_source = Path(assets_path) / f"{atlas_md5}.g"
    atlas_dest = output_dir / f"show.atlas"
    
    copy_file(atlas_source, atlas_dest, "atlas")

def process_skel_file(spine_name, assets_path, output_dir):
    """处理skel文件"""
    skel_input = f"res/spine/{spine_name}/show.skeltest_fun"
    skel_md5 = calculate_md5(skel_input)
    skel_source = Path(assets_path) / f"{skel_md5}.g"
    skel_dest = output_dir / f"show.skel"
    
    copy_file(skel_source, skel_dest, "skel")

def process_pvr_files(spine_name, pvr_path, output_dir):
    """处理pvr.ccz文件"""
    for i in range(1, 11):
        suffix = "" if i == 1 else str(i)
        pvr_input = f"res/spine/{spine_name}/show{suffix}.pvr.ccztest_fun"
        pvr_md5 = calculate_md5(pvr_input)
        pvr_source = Path(pvr_path) / f"{pvr_md5}.g"
        pvr_dest = output_dir / f"show{suffix}.pvr.ccz"
        
        copy_file(pvr_source, pvr_dest, f"pvr.ccz #{i}")

def copy_file(source, destination, file_type):
    """复制文件并处理异常"""
    try:
        if source.exists():
            shutil.copy2(source, destination)
            logging.info(f"成功复制 {file_type} 文件到 {destination}")
        else:
            logging.warning(f"未找到 {file_type} 文件: {source}")
    except Exception as e:
        logging.error(f"复制 {file_type} 文件时出错: {e}")

def get_valid_path(prompt, default=None):
    """获取有效的路径输入"""
    while True:
        path = input(prompt).strip()
        if not path and default:
            return Path(default)
        
        path_obj = Path(path)
        if path_obj.exists():
            return path_obj
        
        print(f"路径不存在: {path}")

def extract_spine_names_from_g_file(g_file_path):
    """从.g文件中提取Spine名称"""
    try:
        with open(g_file_path, 'r', encoding='utf-8') as file:
            content = file.read()
        
        # 使用正则表达式提取ico_id="H3..."部分
        pattern = r'ico_id="(H\d+)"'
        matches = re.findall(pattern, content)
        
        # 去重
        spine_names = list(set(matches))
        
        return spine_names
    except Exception as e:
        print(f"读取文件时出错: {e}")
        return []

def main():
    """主函数 - 控制台交互界面"""
    logging.basicConfig(level=logging.INFO, format='%(message)s')
    
    print("\n=== Spine动画文件处理工具 ===")
    
    print("\n请配置路径:")
    assets_path = get_valid_path("资源路径: ", r"C:\Users\80597\Documents\MuMu共享文件夹\com.xzsh.tw.ero\files\assets\res")
    output_path = get_valid_path("输出路径: ", r"C:\Users\80597\Documents\MuMu共享文件夹\com.xzsh.tw.ero\files\assets\2")
    pvr_path = get_valid_path("PVR路径: ", r"C:\Users\80597\Documents\MuMu共享文件夹\com.xzsh.tw.ero\files\assets\res")
    
    print("\n选择处理模式:")
    print("  1. 手动输入Spine名称")
    print("  2. 从.g文件提取Spine名称")
    print("  q. 退出程序")
    
    mode = input("\n请选择: ").strip().lower()
    
    if mode == 'q':
        return
    
    print("\n提示: 输入 'q' 随时退出处理模式")
    
    while True:
        if mode == '1':
            spine_name = input("\n请输入Spine名称 (或输入 'q' 退出): ").strip()
            if not spine_name:
                continue
            if spine_name.lower() == 'q':
                break
            process_spine_files(spine_name, assets_path, output_path, pvr_path)
        elif mode == '2':

            # 尝试查找同目录下的2fdcc5bd389f0eff24be00de634b6580.g文件
            current_dir = Path.cwd()
            g_file_path = current_dir / "2bccbe34ff085129aec25d2ad181aca7.g"
            if not g_file_path.exists():
                g_file_path = get_valid_path("\n未找到2bccbe34ff085129aec25d2ad181aca7.g文件,请输入.g文件路径: ")
            
            # 提取spine名称
            spine_names = extract_spine_names_from_g_file(g_file_path)
            
            if not spine_names:
                print("未找到任何Spine名称,返回主菜单")
                break
            
            print(f"\n从文件中提取了 {len(spine_names)} 个Spine名称")
            print(f"前10个Spine名称: {', '.join(spine_names[:10])}...")
            
            confirm = input("确认开始处理? (y/n): ").strip().lower()
            if confirm != 'y':
                break
            
            for name in spine_names:
                print(f"\n正在处理: {name}")
                process_spine_files(name, assets_path, output_path, pvr_path)
            
            print("\n批量处理完成,返回主菜单")
            break
    
    print("\n=== 处理完成 ===")

if __name__ == "__main__":
    main()
获取HCG
import os
import hashlib
import shutil
import logging
from pathlib import Path
import re

def calculate_md5(input_string):
    """计算输入字符串的MD5哈希值"""
    return hashlib.md5(input_string.encode('utf-8')).hexdigest()

def process_spine_files(spine_name, assets_path, output_path, pvr_path):
    """处理单个Spine动画文件集"""
    output_dir = Path(output_path) / spine_name
    output_dir.mkdir(parents=True, exist_ok=True)
    
    # 处理atlas文件
    process_atlas_file(spine_name, assets_path, output_dir)
    
    # 处理skel文件
    process_skel_file(spine_name, assets_path, output_dir)
    
    # 处理pvr.ccz文件
    process_pvr_files(spine_name, pvr_path, output_dir)

def process_atlas_file(spine_name, assets_path, output_dir):
    """处理atlas文件"""
    atlas_input = f"res/spine/{spine_name}/{spine_name}.atlastest_fun"
    atlas_md5 = calculate_md5(atlas_input)
    atlas_source = Path(assets_path) / f"{atlas_md5}.g"
    atlas_dest = output_dir / f"{spine_name}.atlas"
    
    copy_file(atlas_source, atlas_dest, "atlas")

def process_skel_file(spine_name, assets_path, output_dir):
    """处理skel文件"""
    skel_input = f"res/spine/{spine_name}/{spine_name}.skeltest_fun"
    skel_md5 = calculate_md5(skel_input)
    skel_source = Path(assets_path) / f"{skel_md5}.g"
    skel_dest = output_dir / f"{spine_name}.skel"
    
    copy_file(skel_source, skel_dest, "skel")

def process_pvr_files(spine_name, pvr_path, output_dir):
    """处理pvr.ccz文件"""
    for i in range(1, 11):
        suffix = "" if i == 1 else str(i)
        pvr_input = f"res/spine/{spine_name}/{spine_name}{suffix}.pvr.ccztest_fun"
        pvr_md5 = calculate_md5(pvr_input)
        pvr_source = Path(pvr_path) / f"{pvr_md5}.g"
        pvr_dest = output_dir / f"{spine_name}{suffix}.pvr.ccz"
        
        copy_file(pvr_source, pvr_dest, f"pvr.ccz #{i}")

def copy_file(source, destination, file_type):
    """复制文件并处理异常"""
    try:
        if source.exists():
            shutil.copy2(source, destination)
            logging.info(f"成功复制 {file_type} 文件到 {destination}")
        else:
            logging.warning(f"未找到 {file_type} 文件: {source}")
    except Exception as e:
        logging.error(f"复制 {file_type} 文件时出错: {e}")

def get_valid_path(prompt, default=None):
    """获取有效的路径输入"""
    while True:
        path = input(prompt).strip()
        if not path and default:
            return Path(default)
        
        path_obj = Path(path)
        if path_obj.exists():
            return path_obj
        
        print(f"路径不存在: {path}")

def extract_spine_names_from_g_file(g_file_path):
    """从.g文件中提取Spine名称"""
    try:
        with open(g_file_path, 'r', encoding='utf-8') as file:
            content = file.read()
        
        # 使用正则表达式提取H3...部分
        pattern = r'\[\[H3([^\]]+)\]\]'
        matches = re.findall(pattern, content)
        
        # 去重并构建完整的spine名称
        spine_names = []
        seen = set()
        for match in matches:
            name = f"H3{match}"  # 重新添加H3前缀
            if name not in seen:
                seen.add(name)
                spine_names.append(name)
        
        return spine_names
    except Exception as e:
        print(f"读取文件时出错: {e}")
        return []

def main():
    """主函数 - 控制台交互界面"""
    logging.basicConfig(level=logging.INFO, format='%(message)s')
    
    print("\n=== Spine动画文件处理工具 ===")
    
    print("\n请配置路径:")
    assets_path = get_valid_path("资源路径: ", r"C:\Users\80597\Documents\MuMu共享文件夹\com.xzsh.tw.ero\files\assets\res")
    output_path = get_valid_path("输出路径: ", r"C:\Users\80597\Documents\MuMu共享文件夹\com.xzsh.tw.ero\files\assets\1")
    pvr_path = get_valid_path("PVR路径: ", r"C:\Users\80597\Documents\MuMu共享文件夹\com.xzsh.tw.ero\files\assets\res")
    
    print("\n选择处理模式:")
    print("  1. 手动输入Spine名称")
    print("  2. 从.g文件提取Spine名称")
    print("  q. 退出程序")
    
    mode = input("\n请选择: ").strip().lower()
    
    if mode == 'q':
        return
    
    print("\n提示: 输入 'q' 随时退出处理模式")
    
    while True:
        if mode == '1':
            spine_name = input("\n请输入Spine名称 (或输入 'q' 退出): ").strip()
            if not spine_name:
                continue
            if spine_name.lower() == 'q':
                break
            process_spine_files(spine_name, assets_path, output_path, pvr_path)
        elif mode == '2':

            # 尝试查找同目录下的2fdcc5bd389f0eff24be00de634b6580.g文件
            current_dir = Path.cwd()
            g_file_path = current_dir / "2fdcc5bd389f0eff24be00de634b6580.g"
            if not g_file_path.exists():
                g_file_path = get_valid_path("\n未找到2fdcc5bd389f0eff24be00de634b6580.g文件,请输入.g文件路径: ")
            
            # 提取spine名称
            spine_names = extract_spine_names_from_g_file(g_file_path)
            
            if not spine_names:
                print("未找到任何Spine名称,返回主菜单")
                break
            
            print(f"\n从文件中提取了 {len(spine_names)} 个Spine名称")
            print(f"前10个Spine名称: {', '.join(spine_names[:10])}...")
            
            confirm = input("确认开始处理? (y/n): ").strip().lower()
            if confirm != 'y':
                break
            
            for name in spine_names:
                print(f"\n正在处理: {name}")
                process_spine_files(name, assets_path, output_path, pvr_path)
            
            print("\n批量处理完成,返回主菜单")
            break
    
    print("\n=== 处理完成 ===")

if __name__ == "__main__":
    main()

星之守护角色&CGspine提取(附清单).zip (121.3 KB)

大佬,为啥运行脚本,提示未找到2fdcc5bd389f0eff24be00de634b6580.g文件
我路径直接复制的文件路径,应该不会错的。

清单在srccom.xzsh.tw.ero\files\assets\src中

试出来了,要把清单文件名也输入进去才行

没有PNG图片只有atsse skle 文件? xor解密脚本解出来的文件没有spine用的png图片?

.pvr.czz

bbd83526e63fa1a3a532d2b0d5043c19这个.pvr.ccz文件,你们都能正常转出来吗?我用命令行怎么转不出来,GUI倒是可以打开,但特别容易未响应
bbd83526e63fa1a3a532d2b0d5043c19.zip (415.1 KB)

这游戏部分文件我命令行也转不了,似乎只有高版本才行但是高版本的慈禧版似乎没有破解命令行的激活只能手动 (淦

.pvr.czz怎么转png 不能用阿?

TexturePacker.exe “bbd83526e63fa1a3a532d2b0d5043c19.pvr.ccz” --sheet “out.png” --format “spritesheet-only”
这样可以转,第一次使用要同意一下条款

估计文件太大了,我用命令转,超过1.5m基本就转不了

上面这个命令可以转,只不过转完分辨率不对,还要额外修正一下

用TexturePacker破解版。地址自己修改下。

3、解压ccz文件
import os
import subprocess
import concurrent.futures
import time

# 设置 TexturePacker 工具的路径
texture_packer_path = r"D:\Tools\TexturePacker\bin\TexturePacker.exe"

def unpack_ccz_file(ccz_file_path):
    """
    解压单个 .ccz 文件
    :param ccz_file_path: .ccz 文件的路径
    """
    print(f"正在解压 {ccz_file_path}...")
    output_png_path = os.path.splitext(ccz_file_path)[0] + '.png'
    command = [
        texture_packer_path,
        ccz_file_path,
        "--sheet", output_png_path,
        "--opt", "RGBA8888",
        "--allow-free-size",
        "--algorithm", "Basic",
        "--no-trim",
        "--dither-fs",
        "--maxrects-heuristics", "Best",
        "--max-size", "6000"
    ]
    try:
        result = subprocess.run(command, capture_output=True, text=True, check=True)
        print(f"{ccz_file_path} 解压成功。")
    except subprocess.CalledProcessError:
        print(f"{ccz_file_path} 解压失败。")
    # 每个任务之间添加 0.25 秒的延迟
    time.sleep(0.25)

def main():
    """
    主函数,处理用户输入并执行解压任务
    """
    # 从控制台获取文件夹路径
    folder_path = input("请输入要处理的文件夹路径: ")
    if not os.path.exists(folder_path):
        print("输入的文件夹路径不存在,请检查后重新运行。")
        return

    # 询问用户是否继续执行
    choice = input("确认要继续执行解压操作吗?(y/n): ").strip().lower()
    if choice != 'y':
        print("操作已取消。")
        return

    # 获取所有 .ccz 文件的路径
    ccz_files = []
    for root, dirs, files in os.walk(folder_path):
        for file in files:
            if file.endswith('.ccz'):
                ccz_files.append(os.path.join(root, file))

    if not ccz_files:
        print("未找到任何 .ccz 文件。")
        return

    # 使用 CPU 核心数相同数量的线程执行任务
    with concurrent.futures.ThreadPoolExecutor() as executor:
        executor.map(unpack_ccz_file, ccz_files)

    print("所有 .ccz 文件解压操作完成。")

if __name__ == "__main__":
    main()

完了后修改atlas文件。

替换atlas文件中的pvr.ccz
import os
import sys
from pathlib import Path

def process_atlas_file(file_path):
    """处理单个atlas文件,替换其中的.pvr.ccz为.pvr.png"""
    try:
        # 读取文件内容
        with open(file_path, 'r', encoding='utf-8') as file:
            content = file.read()
        
        # 替换所有.pvr.ccz为.pvr.png
        new_content = content.replace('.pvr.ccz', '.pvr.png')
        
        # 如果内容有变化,则写回文件
        if new_content != content:
            with open(file_path, 'w', encoding='utf-8') as file:
                file.write(new_content)
            return (True, file_path)
        else:
            return (False, file_path)
    except Exception as e:
        print(f"处理失败 {file_path}: {str(e)}")
        return (False, file_path)

def process_path(input_path):
    """处理用户输入的路径,返回处理结果"""
    path_obj = Path(input_path)
    
    # 验证路径是否存在
    if not path_obj.exists():
        print(f"错误: 路径不存在 - {input_path}")
        return False, 0, 0, 0
    
    atlas_files = []
    
    # 如果输入是.atlas文件,直接处理
    if path_obj.is_file() and path_obj.suffix.lower() == '.atlas':
        atlas_files = [path_obj]
    # 如果输入是文件夹,查找其中的所有.atlas文件
    else:
        if path_obj.is_file():
            parent_dir = path_obj.parent
        else:
            parent_dir = path_obj
        
        atlas_files = list(parent_dir.rglob('**/*.atlas'))
        
        if not atlas_files:
            print(f"错误: 在目录 {parent_dir} 及其子目录中未找到.atlas文件")
            return False, 0, 0, 0
    
    # 处理找到的.atlas文件
    total_files = len(atlas_files)
    modified_count = 0
    skipped_count = 0
    
    print(f"找到 {total_files} 个.atlas文件")
    
    for file in atlas_files:
        result = process_atlas_file(str(file))
        if result[0]:
            modified_count += 1
            print(f"已修改: {result[1]}")
        else:
            skipped_count += 1
    
    # 显示处理结果
    print(f"\n处理完成!")
    print(f"总共修改: {modified_count} 个文件")
    print(f"未修改: {skipped_count} 个文件")
    print(f"总计: {modified_count + skipped_count}/{total_files} 个文件")
    
    return True, total_files, modified_count, skipped_count

def main():
    """主函数,控制整个程序流程"""
    while True:
        # 获取用户输入的路径
        input_path = input("\n请输入文件夹或文件地址 (输入q退出): ").strip()
        
        if input_path.lower() == 'q':
            print("程序已退出。")
            break
        
        if not input_path:
            print("错误: 输入路径为空")
            continue
        
        # 处理路径
        success, total, modified, skipped = process_path(input_path)
        
        # 询问是否继续
        if success:
            choice = input("\n是否要处理另一个文件夹或文件?(Y/N): ").strip().lower()
            if choice != 'y':
                print("程序已退出。")
                break

if __name__ == "__main__":
    main()   

完了后修改纹理。
需要opencv库

根据atlas修改纹理
import os
import re
import cv2
import numpy as np
import concurrent.futures
import multiprocessing

def process_atlas_file(atlas_path, target_dir):
    """处理单个atlas文件,读取其中的图片信息并调整对应图片大小"""
    try:
        with open(atlas_path, 'r', encoding='utf-8') as f:
            lines = f.readlines()
    except Exception as e:
        print(f"无法读取文件 {atlas_path}: {e}")
        return

    image_size_map = {}
    current_image = None

    # 解析atlas文件,建立图片与size的映射关系
    for i, line in enumerate(lines):
        line = line.strip()

        # 检查是否是图片名称行(.png结尾)
        if line.endswith('.png'):
            current_image = line
            # 检查下一行是否存在且包含size信息
            if i + 1 < len(lines):
                next_line = lines[i + 1].strip()
                size_match = re.search(r'size:\s*(\d+),\s*(\d+)', next_line)
                if size_match:
                    width = int(size_match.group(1))
                    height = int(size_match.group(2))
                    image_size_map[current_image] = (width, height)
                else:
                    print(f"警告: 在图片 {current_image} 后未找到对应的size信息")
            else:
                print(f"警告: 在图片 {current_image} 后未找到对应的size信息")

    if not image_size_map:
        print(f"在文件 {atlas_path} 中未找到有效的图片-size映射")
        return

    # 获取atlas文件所在目录
    atlas_dir = os.path.dirname(atlas_path)

    # 处理每个图片
    for image_name, (width, height) in image_size_map.items():
        # 构建图片的完整路径
        image_path = os.path.join(atlas_dir, image_name)

        # 检查图片文件是否存在
        if not os.path.exists(image_path):
            print(f"图片文件不存在: {image_path}")
            continue

        try:
            # 使用支持中文路径的方法读取图片
            with open(image_path, 'rb') as f:
                img_array = np.asarray(bytearray(f.read()), dtype=np.uint8)
                img = cv2.imdecode(img_array, cv2.IMREAD_UNCHANGED)

            if img is None:
                print(f"无法读取图片: {image_path}")
                continue

            # 获取原始图片尺寸
            original_height, original_width = img.shape[:2]

            # 如果图片尺寸与atlas中指定的尺寸不一致,则调整大小
            if original_width != width or original_height != height:
                print(f"调整图片大小: {image_name} 从 {original_width}x{original_height} 到 {width}x{height}")

                # 使用高质量的调整方法
                resized_img = cv2.resize(img, (width, height), interpolation=cv2.INTER_AREA)

                # 使用支持中文路径的方法保存图片
                success, encoded_img = cv2.imencode('.png', resized_img)
                if success:
                    with open(image_path, 'wb') as f:
                        f.write(encoded_img)
                    print(f"成功调整并保存图片: {image_path}")
                else:
                    print(f"无法保存图片: {image_path}")

            else:
                print(f"图片尺寸已符合要求: {image_path}")

        except Exception as e:
            print(f"处理图片 {image_path} 时出错: {e}")


def main():
    while True:
        """主函数:获取用户输入的文件夹路径,遍历处理所有atlas文件"""
        # 获取用户输入的文件夹路径
        folder_path = input("请输入要处理的文件夹路径(输入 'q' 退出): ").strip()

        if folder_path.lower() == 'q':
            break

        # 检查路径是否存在
        if not os.path.exists(folder_path):
            print(f"错误:路径不存在 - {folder_path}")
            continue

        # 检查是否是文件夹
        if not os.path.isdir(folder_path):
            print(f"错误:{folder_path} 不是一个文件夹")
            continue

        # 遍历文件夹中的所有文件,找出所有.atlas文件
        atlas_files = []
        for root, dirs, files in os.walk(folder_path):
            for file in files:
                if file.endswith('.atlas'):
                    atlas_path = os.path.join(root, file)
                    atlas_files.append(atlas_path)

        # 获取 CPU 核心数
        num_cpus = multiprocessing.cpu_count()

        # 使用线程池并行处理.atlas文件
        with concurrent.futures.ThreadPoolExecutor(max_workers=num_cpus) as executor:
            futures = [executor.submit(process_atlas_file, atlas_path, folder_path) for atlas_path in atlas_files]
            for future in concurrent.futures.as_completed(futures):
                try:
                    future.result()
                except Exception as e:
                    print(f"处理过程中出现异常: {e}")

        print("\n处理完成!")


if __name__ == "__main__":
    try:
        # 检查是否安装了OpenCV库
        import cv2
        print("OpenCV库已安装,版本:", cv2.__version__)
    except ImportError:
        print("错误:未安装OpenCV库。请先安装:pip install opencv-python")
    else:
        main()

有没有可以查看3.4版本的spine查看器?我这边有一个但是有点问题。