源序空间解包

游戏内ab文件看起来是压缩文件,文件头是NHPKD1。解压后部分文件能正常打开,但有些文件头是KTX11,打不开。
有没有大佬知道如何处理

Vibe Coding

Ktx2Png

python -m pip install pillow texture2ddecoder
import os
import sys
import struct
from PIL import Image
import texture2ddecoder

# KTX文件标识符
KTX_ID = b'\xABKTX 11\xBB\r\n\x1A\n'

# ASTC internalFormat -> (block_width, block_height)
# 参考 GL_KHR_texture_compression_astc_ldr 规范
ASTC_FORMATS = {
    0x93B0: (4, 4), 0x93B1: (5, 4), 0x93B2: (5, 5), 0x93B3: (6, 5),
    0x93B4: (6, 6), 0x93B5: (8, 5), 0x93B6: (8, 6), 0x93B7: (8, 8),
    0x93B8: (10, 5), 0x93B9: (10, 6), 0x93BA: (10, 8), 0x93BB: (10, 10),
    0x93BC: (12, 10), 0x93BD: (12, 12),
}

def parse_ktx_header(filepath):
    """解析 KTX 1.1 头部,返回关键字段字典"""
    with open(filepath, 'rb') as f:
        header = f.read(64)
        if header[:12] != KTX_ID:
            raise ValueError("无效的 KTX 1.1 文件头")

        return {
            'endianness': struct.unpack_from('<I', header, 12)[0],
            'gl_type': struct.unpack_from('<I', header, 16)[0],
            'gl_format': struct.unpack_from('<I', header, 24)[0],
            'gl_internal_format': struct.unpack_from('<I', header, 28)[0],
            'width': struct.unpack_from('<I', header, 36)[0],
            'height': struct.unpack_from('<I', header, 40)[0],
            'kv_bytes': struct.unpack_from('<I', header, 60)[0],
        }

def decode_compressed(internal_fmt, data, width, height):
    """
    根据 glInternalFormat 调用 texture2ddecoder 解码
    返回 BGRA bytes 或 None(表示不支持)
    """
    # --- BC / DXT (S3TC) ---
    if internal_fmt in (0x83F0, 0x83F1):          # BC1 / DXT1
        return texture2ddecoder.decode_bc1(data, width, height)
    elif internal_fmt == 0x83F2:                  # BC2 / DXT3
        return texture2ddecoder.decode_bc2(data, width, height)
    elif internal_fmt == 0x83F3:                  # BC3 / DXT5
        return texture2ddecoder.decode_bc3(data, width, height)
    elif internal_fmt == 0x8C48:                  # BC4 / ATI1
        return texture2ddecoder.decode_bc4(data, width, height)
    elif internal_fmt == 0x8C4A:                  # BC5 / ATI2
        return texture2ddecoder.decode_bc5(data, width, height)
    elif internal_fmt in (0x8E8C, 0x8E8D):        # BC6H
        return texture2ddecoder.decode_bc6h(data, width, height)
    elif internal_fmt in (0x8E8F, 0x8E90):        # BC7
        return texture2ddecoder.decode_bc7(data, width, height)

    # --- ETC ---
    elif internal_fmt == 0x8D64:                  # ETC1
        return texture2ddecoder.decode_etc1(data, width, height)
    elif internal_fmt in (0x9274, 0x9275):        # ETC2 RGB
        return texture2ddecoder.decode_etc2_rgb(data, width, height)
    elif internal_fmt in (0x9276, 0x9277):        # ETC2 EAC RGBA (planar)
        return texture2ddecoder.decode_etc2_rgba(data, width, height)
    elif internal_fmt in (0x9278, 0x9279):        # ETC2 EAC RGBA (interleaved)
        return texture2ddecoder.decode_etc2_rgba(data, width, height)

    # --- ASTC ---
    elif internal_fmt in ASTC_FORMATS:
        bw, bh = ASTC_FORMATS[internal_fmt]
        return texture2ddecoder.decode_astc(data, width, height, bw, bh)

    # --- PVRTC (可选,部分平台可能有问题) ---
    # elif internal_fmt in (0x8C00, 0x8C01, 0x8C02, 0x8C03):
    #     return texture2ddecoder.decode_pvrtc(data, width, height, ...)

    return None

def convert_ktx(ktx_path, png_path):
    """将单个 KTX 文件转换为 PNG"""
    hdr = parse_ktx_header(ktx_path)

    with open(ktx_path, 'rb') as f:
        f.seek(64 + hdr['kv_bytes'])
        img_size = struct.unpack('<I', f.read(4))[0]
        img_data = f.read(img_size)

    w, h = hdr['width'], hdr['height']

    # ---- 未压缩纹理:直接用 Pillow ----
    if hdr['gl_type'] != 0:
        gl_fmt = hdr['gl_format']
        if gl_fmt == 6408:        # GL_RGBA
            mode, bpp = 'RGBA', 4
        elif gl_fmt == 6407:      # GL_RGB
            mode, bpp = 'RGB', 3
        else:
            raise ValueError(f"不支持的未压缩 glFormat: {gl_fmt}")

        stride = (w * bpp + 3) & ~3   # KTX 行对齐到 4 字节
        img = Image.frombytes(mode, (w, h), img_data, 'raw', mode, stride)
        img.save(png_path)
        return

    # ---- 压缩纹理:texture2ddecoder ----
    decoded = decode_compressed(hdr['gl_internal_format'], img_data, w, h)
    if decoded is None:
        raise ValueError(
            f"不支持的压缩格式 (internalFormat=0x{hdr['gl_internal_format']:X}),"
            "请检查 texture2ddecoder 是否支持该格式"
        )

    # texture2ddecoder 统一输出 BGRA 原始字节 -> 用 Pillow 转存
    img = Image.frombytes('RGBA', (w, h), decoded, 'raw', 'BGRA')
    img.save(png_path)

def process(path):
    """处理单个文件或目录下所有 .ktx"""
    if os.path.isfile(path) and path.lower().endswith('.ktx'):
        out = os.path.splitext(path)[0] + '.png'
        print(f"[转换] {os.path.basename(path)} -> {os.path.basename(out)}")
        try:
            convert_ktx(path, out)
            print("  成功")
        except Exception as e:
            print(f"  失败: {e}")
    elif os.path.isdir(path):
        for f in os.listdir(path):
            if f.lower().endswith('.ktx'):
                process(os.path.join(path, f))

def main():
    script_dir = os.path.dirname(os.path.abspath(__file__))
    if len(sys.argv) > 1:
        target = sys.argv[1]
    else:
        target = script_dir
        print(f"未指定参数,使用脚本目录: {script_dir}")

    if not os.path.exists(target):
        print("路径不存在")
        return

    process(target)

if __name__ == '__main__':
    main()

杂项

1.下载:
B服:
源序空间 - bilibili游戏中心
Apk_from_bilibili

https://pkg.biligame.com/games/yxkj_1.10.3500_4_20260414_115435_09687.apk

官服:
Apk_from_densicraft

https://hsdownload.densicraft.com/yxkj/20260922/PD_2026_9_22_V1.10.4000_encrypted_hnym.apk

2.停运公告:

3.引擎——LayaAir、资源管理——LayaDCC
4.动画——DragonBones、Spine
5.NHPK容器结构(草稿)


6.关于还原文件名及目录结构后的*.zpm文件
(刚刚弄的时候,操作不当整个丢失了 :innocent:,以下凭记忆)应该是一种压缩包,bandizip可以解压,但会提示CRC校验失败,而解压出来的文件看着应该没有问题。

大佬们辛苦了,想请教一下。com.hnym.yxkjgame\LayaCache目录下的文件疑似加密,直接按KTX解析会失败(文件头像 WSTX0,但内部能搜到KTX11)。请问该如何提取?