求助卡厄思梦境的解包问题关于sct怎么转png的

我参考论坛里的大佬写的代码能解一部分的包,但是有一部分sct解出来有轮廓但是颜色不对,有一部分是干脆都大半看不见,想请大佬们看看什么问题 ,文件在夸克网盘
链接: https://pan.quark.cn/s/5d841601a0a0

这是我参考大佬写的,只有一部分的解出来,还有些解不出来,请大佬们看看

from pathlib import Path

from io import BufferedReader

from lz4.block import decompress as decLz4Block

import numpy as np

from PIL import Image

from texture2ddecoder import decode_astc as dastc

import sys

import subprocess

def process_file(file_path: str) → bool:

try:

    with open(file_path, 'rb') as f:

        \# 1. 基础头部解析(保留类型分析)

        if f.read(4) != b'SCT2':

            print(f"非SCT2格式: {file_path}")

            return False

        

        data_length = int.from_bytes(f.read(4), 'little')

        f.read(4)  # CRC

        offset = int.from_bytes(f.read(4), 'little')

        tex_type = int.from_bytes(f.read(4), 'little')  # 保留类型判断

        f.read(4)  # 未知字段

        width = int.from_bytes(f.read(2), 'little')

        height = int.from_bytes(f.read(2), 'little')

        crop_w = int.from_bytes(f.read(2), 'little')

        crop_h = int.from_bytes(f.read(2), 'little')



        act_w = crop_w if 0 < crop_w <= 2048 else min(width, 2048)

        act_h = crop_h if 0 < crop_h <= 2048 else min(height, 2048)



        \# 2. 读取数据区

        f.seek(offset)

        raw_data = f.read(data_length)  # 读取完整数据区



        \# 3. 尝试所有可能的解压组合(核心:处理多层压缩)

        decompress_results = try_decompress(raw_data)

        for algo, data in decompress_results:

            \# 4. 尝试ASTC解码(多块大小)

            block_sizes = \[(4,4), (8,8), (6,6),(5,5), (10,10), (3,3), (7,7)\]

            for block in block_sizes:

                try:

                    img_buffer = dastc(data, act_w, act_h, block\[0\], block\[1\])

                except:

                    continue

                

                \# 5. 验证解码结果(字节长度校验)

                expected_len = act_w \* act_h \* 4  # RGBA格式预期长度

                if len(img_buffer) != expected_len:

                    continue  # 长度不符,跳过



                \# 6. 尝试所有通道顺序

                channel_orders = \[

                    \[0,1,2,3\], \[2,1,0,3\], \[1,0,2,3\], \[2,0,1,3\]

                \]

                for order in channel_orders:

                    img_array = np.frombuffer(img_buffer, dtype=np.uint8)

                    img_array = img_array.reshape((act_h, act_w, 4))\[:, :, order\]

                    

                    \# 7. 保存尝试结果(方便对比)

                    save_path = f"{file_path\[:-3\]}\_{algo}\_{block\[0\]}x{block\[1\]}\_{order}.png"

                    Image.fromarray(img_array).save(save_path)

                    print(f"生成尝试结果: {save_path}")

                    return True  # 成功生成一个有效结果即返回



        \# 所有尝试失败

        print(f"所有解码组合均失败: {file_path}")

        return False



except Exception as e:

    print(f"处理错误: {file_path}, {str(e)}")

    return False

# 2. 批量处理函数,用命令行参数传递文件路径(避免函数导入)

def batch_convert(dat_path: str):

path = Path(dat_path)

sct_files = \[str(f) for f in path.glob('\*.sct') if f.is_file()\]

if not sct_files:

    print("No .sct files found")

    return



for file in sct_files:

    print(f"\\nProcessing: {file}")

    \# 修正路径格式

    file_path_fixed = file.replace('\\\\', '/')

    \# 关键:通过命令行参数传递文件路径,子进程直接执行process_file

    \# 将处理逻辑作为字符串传递,避免导入函数

    cmd = f"""

import sys

from lz4.block import decompress as decLz4Block

import numpy as np

from PIL import Image

from texture2ddecoder import decode_astc as dastc

def process_file(file_path):

try:

    with open(file_path, 'rb') as f:

        if f.read(4) != b'SCT2':

            print(f"Failed {{file_path}}: Not SCT2")

            return False

        header_pos = f.tell()

        data_length = int.from_bytes(f.read(4), 'little')

        f.read(4)

        offset = int.from_bytes(f.read(4), 'little')

        tex_type = int.from_bytes(f.read(4), 'little')

        f.read(4)

        width = int.from_bytes(f.read(2), 'little')

        height = int.from_bytes(f.read(2), 'little')

        f.read(4)

        MAX_DIMENSION = 2048

        if width > MAX_DIMENSION or height > MAX_DIMENSION:

            width, height = MAX_DIMENSION, MAX_DIMENSION

        try:

            f.seek(offset)

            dec_len = int.from_bytes(f.read(4), 'little')

            com_len = int.from_bytes(f.read(4), 'little')

            if com_len == data_length - 80 and tex_type == 4:

                compress_data = f.read(com_len)

                data = decLz4Block(compress_data, uncompressed_size=dec_len)

                img_buffer = dastc(data, width, height, 4, 4)

                img_array = np.frombuffer(img_buffer, dtype=np.uint8).reshape((height, width, 4))

                img_array = img_array\[:, :, \[2, 1, 0, 3\]\]

                Image.fromarray(img_array).save(f'{{file_path\[:-3\]}}png')

                print(f"Success {{file_path}}")

                return True

        except:

            pass

        try:

            f.seek(header_pos + 20)

            width = int.from_bytes(f.read(2), 'little')

            height = int.from_bytes(f.read(2), 'little')

            width = MAX_DIMENSION if width <=0 or width > MAX_DIMENSION else width

            height = MAX_DIMENSION if height <=0 or height > MAX_DIMENSION else height

            block_size = 4

            block_count_x = (width + block_size - 1) // block_size

            block_count_y = (height + block_size - 1) // block_size

            theoretical_length = block_count_x \* block_count_y \* 16

            f.seek(72)

            remaining_length = data_length - 72

            actual_read_length = min(theoretical_length, remaining_length, 10\*1024\*1024)

            data = f.read(actual_read_length)

            if len(data) < 16:

                raise ValueError("Data too short")

            try:

                img_buffer = dastc(data, width, height, 4, 4)

            except:

                img_buffer = dastc(data, width, height, 8, 8)

            img_array = np.frombuffer(img_buffer, dtype=np.uint8).reshape((height, width, 4))

            img_array = img_array\[:, :, \[2, 1, 0, 3\]\]

            Image.fromarray(img_array).save(f'{{file_path\[:-3\]}}png')

            print(f"Success {{file_path}}")

            return True

        except Exception as e:

            print(f"Failed {{file_path}}: {{str(e)}}")

            return False

except Exception as e:

    print(f"Failed {{file_path}}: {{str(e)}}")

    return False

process_file(‘{file_path_fixed}’)

“”"

    \# 执行子进程

    result = subprocess.run(

        \[sys.executable, "-c", cmd\],

        capture_output=True,

        text=True

    )

    \# 打印结果

    if result.stdout:

        print(result.stdout)

    if result.stderr:

        print(f"Error: {result.stderr}")

    continue

if _name_ == ‘_main_’:

target_dir = r'D:\\jiemi\\czn'

batch_convert(target_dir)

勉强可用,但是部分图片需要修复,懒得弄了,这游戏新旧格式混杂,还夹杂不同的编码格式。。

import os
import math
import struct
import argparse
import numpy as np
import concurrent.futures
from pathlib import Path
from PIL import Image
from typing import Union
from lz4.block import decompress as lz4_decompress
from texture2ddecoder import decode_astc, decode_etc2, decode_eacr, decode_eacrg
from tqdm import tqdm

def ceil_div(a, b):
    return (a + b - 1) // b

ETC2_SCT2_FORMAT_MAP = {
    1: ('etc2_rgb', False), 3: ('etc2_rgba', True), 4: ('etc2_rgba_punchthrough', True),
    7: ('eacr', False), 8: ('eacrg', False),
}
LEGACY_LZ4_PIXEL_FORMAT_MAP = {
    0: ('RGBA', 4), 1: ('RGB', 3), 2: ('LA', 2), 3: ('L', 1), 5: ('RGB', 2)
}

def decode_sct_v1(sct_data: bytes) -> Union[Image.Image, None]:
    """解码SCT\x01签名的旧版SCT文件。"""
    try:
        if len(sct_data) < 17: return None
        texType = sct_data[4]
        width, height, decLen, comLen = struct.unpack_from('<HHII', sct_data, 5)
        if width == 0 or height == 0 or decLen == 0: return None
        compressed_data = sct_data[17:17 + comLen]
        if len(compressed_data) < comLen: return None
        data = lz4_decompress(compressed_data, uncompressed_size=decLen)

        if texType == 2: # RGBA
            if len(data) < width * height * 4: return None
            return Image.frombytes('RGBA', (width, height), data)
        elif texType == 4: # RGB565 + A
            expected_size = width * height * 3
            if len(data) < expected_size: return None
            rgb_size, alpha_size = width * height * 2, width * height
            rgb = np.frombuffer(data[:rgb_size], dtype=np.uint16).reshape((height, width))
            a = np.frombuffer(data[rgb_size : rgb_size + alpha_size], dtype=np.uint8).reshape((height, width))
            r = (((((rgb >> 11) & 0x1F) * 527) + 23) >> 6).astype(np.uint8)
            g = (((((rgb >> 5) & 0x3F) * 259) + 33) >> 6).astype(np.uint8)
            b = ((((rgb & 0x1F) * 527) + 23) >> 6).astype(np.uint8)
            rgba_bytes = np.stack([r, g, b, a], axis=-1).tobytes()
            return Image.frombytes('RGBA', (width, height), rgba_bytes)
        elif texType == 102: # ETC2/EAC
            expected_size = ceil_div(width, 4) * ceil_div(height, 4) * 8
            if len(data) < expected_size: return None
            decoded_pixels = decode_eacr(data[:expected_size], width, height)
            final_pixels = bytearray(b'\xff' * (width * height * 4))
            final_pixels[::4] = decoded_pixels; final_pixels[1::4] = decoded_pixels; final_pixels[2::4] = decoded_pixels
            return Image.frombytes('RGBA', (width, height), bytes(final_pixels))
        else:
            return None
    except Exception:
        return None

def decode_sct2_variant(sct_data: bytes) -> Union[Image.Image, None]:
    """解码小端序头部的 SCT2 文件(通常包含 'proc' 块)。"""
    try:
        if len(sct_data) < 36: return None
        header_format = '<4x I 4x I I I HH HH i'
        _, data_offset, _, pixel_format, width, height, _, _, flags = struct.unpack_from(header_format, sct_data, 0)
        if width == 0 or height == 0 or data_offset >= len(sct_data): return None
        data_block = sct_data[data_offset:]
        is_compressed = flags < 0 or data_block.startswith(b'proc')
        
        raw_texture_data = None
        if is_compressed:
            lz4_stream = data_block
            if data_block.startswith(b'proc'):
                if len(data_block) < 48 + 8: return None
                lz4_stream = data_block[48:]
            if len(lz4_stream) < 8: return None
            decompressed_size = int.from_bytes(lz4_stream[0:4], 'little')
            if decompressed_size == 0: return None
            compressed_bytes = lz4_stream[8:]
            raw_texture_data = lz4_decompress(compressed_bytes, uncompressed_size=decompressed_size)
        else:
            raw_texture_data = data_block

        if not raw_texture_data: return None

        if pixel_format == 2:
            expected_size = width * height
            if len(raw_texture_data) < expected_size: return None
            pixel_data = raw_texture_data[:expected_size]
            return Image.frombytes('L', (width, height), pixel_data).convert('RGBA')
        elif pixel_format in [19, 102]: # ETC2 RGBA
            expected_size = ceil_div(width, 4) * ceil_div(height, 4) * 16
            if len(raw_texture_data) < expected_size: return None
            decoded_pixels = decode_etc2(raw_texture_data[:expected_size], width, height)
            return Image.frombytes('RGBA', (width, height), decoded_pixels)
        elif pixel_format == 40: # ASTC 4x4
            expected_size = ceil_div(width, 4) * ceil_div(height, 4) * 16
            if len(raw_texture_data) < expected_size: return None
            decoded_pixels = decode_astc(raw_texture_data[:expected_size], width, height, 4, 4)
            return Image.frombytes('RGBA', (width, height), decoded_pixels, 'raw', 'BGRA')
        elif pixel_format in [44, 47]: # ASTC 8x8
            expected_size = ceil_div(width, 8) * ceil_div(height, 8) * 16
            if len(raw_texture_data) < expected_size: return None
            decoded_pixels = decode_astc(raw_texture_data[:expected_size], width, height, 8, 8)
            return Image.frombytes('RGBA', (width, height), decoded_pixels, 'raw', 'BGRA')
        else:
            return None
    except Exception:
        return None

def decode_etc2_sct2(sct_data: bytes) -> Union[Image.Image, None]:
    """解码大端序头部的 SCT2 (主要用于ETC2)"""
    try:
        if len(sct_data) < 28: return None
        header_fields = struct.unpack_from('>14H', sct_data, 4)
        format_id, width, height = header_fields[0:3]
        if format_id not in ETC2_SCT2_FORMAT_MAP or width == 0 or height == 0: return None
        decoder_format, is_rgba_type = ETC2_SCT2_FORMAT_MAP[format_id]
        compressed_data = sct_data[28:]
        block_size = 16 if is_rgba_type else 8
        if decoder_format in ['eacr', 'eacrg']: block_size = 8
        expected_size = ceil_div(width, 4) * ceil_div(height, 4) * block_size
        if len(compressed_data) < expected_size: return None
        final_data = compressed_data[:expected_size]

        if decoder_format.startswith('etc2'): decoded_pixels = decode_etc2(final_data, width, height)
        elif decoder_format == 'eacr': decoded_pixels = decode_eacr(final_data, width, height)
        elif decoder_format == 'eacrg': decoded_pixels = decode_eacrg(final_data, width, height)
        else: return None
        
        if decoder_format == 'eacr':
            final_pixels=bytearray(b'\xff'*(width*height*4));final_pixels[::4]=decoded_pixels;final_pixels[1::4]=decoded_pixels;final_pixels[2::4]=decoded_pixels
            return Image.frombytes('RGBA', (width, height), bytes(final_pixels))
        elif decoder_format == 'eacrg':
            final_pixels=bytearray(b'\x00'*(width*height*4));final_pixels[0::4]=decoded_pixels[0::2];final_pixels[1::4]=decoded_pixels[1::2];final_pixels[3::4]=b'\xff'*(width*height)
            return Image.frombytes('RGBA', (width, height), bytes(final_pixels))
        else: return Image.frombytes('RGBA', (width, height), decoded_pixels)
    except Exception: return None

def decode_legacy_lz4_sct(sct_data: bytes) -> Union[Image.Image, None]:
    """解码通用的旧版SCT格式(非SCT\x01)"""
    try:
        if len(sct_data) < 17: return None
        _, texture_type, width, height = struct.unpack_from('<x B H H', sct_data, 0)
        decompressed_size = int.from_bytes(sct_data[9:13], 'little')
        if decompressed_size == 0: return None
        compressed_data = sct_data[17:]
        if width == 0 or height == 0: return None
        decompressed_data = lz4_decompress(compressed_data, uncompressed_size=decompressed_size)

        if texture_type == 102:
            expected_size = ceil_div(width, 4) * ceil_div(height, 4) * 8
            if len(decompressed_data) < expected_size: return None
            decoded_pixels = decode_eacr(decompressed_data[:expected_size], width, height)
            final_pixels=bytearray(b'\xff'*(width*height*4));final_pixels[::4]=decoded_pixels;final_pixels[1::4]=decoded_pixels;final_pixels[2::4]=decoded_pixels
            return Image.frombytes('RGBA', (width, height), bytes(final_pixels))

        try:
            pixel_mode, bytes_per_pixel = LEGACY_LZ4_PIXEL_FORMAT_MAP[texture_type]
        except KeyError:
            return None
            
        if len(decompressed_data) < width * height * bytes_per_pixel: return None
        pixel_data = decompressed_data[:width*height*bytes_per_pixel]
        if texture_type == 5:
            return Image.frombytes('RGB', (width, height), pixel_data, 'raw', 'RGB;16')
        else:
            return Image.frombytes(pixel_mode, (width, height), pixel_data, 'raw')
    except Exception:
        return None

def convert_sct_to_png(sct_data: bytes) -> Union[Image.Image, None]:
    if len(sct_data) < 4: return None
    
    if sct_data.startswith(b'SCT\x01'):
        return decode_sct_v1(sct_data)
    elif sct_data.startswith(b'SCT2'):
        if b'proc' in sct_data[12:80]:
            return decode_sct2_variant(sct_data)
        else:
            return decode_etc2_sct2(sct_data)
    else:
        return decode_legacy_lz4_sct(sct_data)
    
def process_file(sct_path: Path) -> Union[str, None]:
    try:
        with open(sct_path, 'rb') as f:
            sct_data = f.read()
        image = convert_sct_to_png(sct_data)
        if image:
            output_png_path = sct_path.with_suffix('.png')
            image.save(output_png_path, 'PNG')
            return None
        else:
            return str(sct_path)
    except Exception as e:
        return f"{sct_path} (错误: {e})"

def main():
    parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter)
    parser.add_argument("i")
    args = parser.parse_args()
    input_path = Path(args.i)
    sct_files = list(input_path.rglob('*.sct'))
    if not sct_files:
        print(f"没有找到sct文件")
        return
    failed_files = []
    with tqdm(total=len(sct_files), ncols=100, bar_format='{l_bar}{bar}|{n_fmt}/{total_fmt}[{elapsed}<{remaining}]') as pbar:
        with concurrent.futures.ThreadPoolExecutor(max_workers=32) as executor:
            for future in concurrent.futures.as_completed({executor.submit(process_file, sct_path): sct_path for sct_path in sct_files}):
                result = future.result()
                if result:
                    failed_files.append(result)
                pbar.update(1)

    if failed_files:
        print(f"\n{len(failed_files)} 个文件转换失败:")
        for file_path in sorted(failed_files):
            print(f"{file_path}")

if __name__ == '__main__':
    main()
1 个赞

另外,不要再用夸克这种还要下载客户端才能下载的网盘了行不

2 个赞

主要新注册。。。上传不了附件,下次用其他的,谢谢佬

转了有啥用 scp有转不了skel

那大佬你能发你的转换脚本吗,而不是在这阴阳

没有那个意思单纯的疑惑

这个前面有帖子说了,魔改了spine,得自己对照代码还原了。

不会吧?