打开未公开的 90 年代图形格式

逆向工程 文件格式 未知数据
2021-06-15 14:14:52

我设法从旧游戏的存档中提取了一些图形文件。还没有在网上找到任何关于它的信息,所以我在这里试试运气,也许有人知道可以帮助我的类似格式。它是一种使用外部调色板的加密/压缩图形格式。我设法显示了与游戏不同的图形格式,.raw 文件,与 NetPBM 文件非常相似。然而,这种格式并不直接。

这是我所知道的:

文件扩展名:.cgf
文件魔法:CGFF
file:数据
binwalk -E:0.5 或 0.75,取决于“压缩类型”,文件中几乎一致
binwalk -X:DEFLATE 流,有时只有 1 位长(?)

头结构如下。名称是对该领域可能是什么的有根据的猜测。每个字段的长度为 4 个字节,数字是以小端序存储的有符号整数。
文件中的每一行占 4 个字节:

String CGFF (file magic)
Compression type, either "1" or "9" across all files
Number of layers
Number of layers, multiplied by 24
File size. If my guess is right, this is probably an unsigned int
"0" across all files but one, where it is "250"
"0" across all files
X Position                                      |
Y Position                                      |
Width                                           |
Height                                          |
"38" across all files                           |
Offset into the file starting after the headers |

最后 6 个条目似乎构成了一个单元,每层重复一次。

以下是我的猜测:

  • 压缩类型:1-文件与9-文件具有不同的熵和结构。
  • 层数: 像这样的观察:一个名为“Ballammo”的文件被加载到一个游戏中,你有 10 个雪球可以向目标投掷。标题在该字段中包含“10”。这也适用于其他文件。
  • 文件大小:我不得不猜测这一点,因为我从存储的错误文件长度中提取文件的存档,但在大多数情况下,数字足够相似
  • 位置/大小:名为“背景”的文件的值始终为 0 0 640 480。在某些情况下,它们是 640 480 -640 -480,因此我猜测位置。大小对于当时以 4:3 运行的游戏来说是有意义的
  • 层偏移量:猜猜,因为第一个条目始终为 0,随后是更大的数字。它也像这样排列得很好。

关于文件结构,1 文件具有更高的熵,并没有真正显示出任何模式。然而,在 9 个文件中,大多数情况下一个字节后跟 0xff,很少有其他东西。这不是 RLE,我试过了。

根据要求,这里有两个十六进制转储。一个 9 压缩,一个 1 压缩。
https://pastebin.com/NGY79UgU

哦,对了,我应该说这些应该是什么样子的。“Cursor.cgf”应该包含两个手状光标,一个指向一个抓取。关于“Kid.cgf”,数据显示它包含8层。鉴于文件加载的小游戏,这应该包含 4 个咧嘴笑的嘴和 4 对眼睛。

我还忘了提到两个图像都具有透明度,但这可以使用游戏加载的调色板来实现。

注意:我不能保证这些文件是完整的。它们最后可能包含垃圾或不完整。正如我所说,存档的存储文件长度很可能是错误的,因为我发现了一个包含提取后下一个文件的 .wav 标头的调色板。

以下是给定文件的某些层在正确加载时的外观。三个“Kid.cgf”层以红色排列,而“Cursor.cgf”层之一以绿色排列。盒子不准确。
在此处输入图片说明

更新

我设法将游戏指向提取的文件而不是存档文件,并且它加载得很好。在编写了一个完全解析标题的程序,将图层数据提取到单独的文件和一些小实验之后,这是我对类型 1 图像的结果

  • 游戏不介意文件太长。
  • 数据不仅仅是位图。假设我正确地找到了宽度、高度和层偏移的字段,字段的长度(= 两个层偏移之间的差异)总是小于宽度 * 高度。此外,随机更改图像部分中的某些字节会导致该游戏在加载时崩溃。
  • x 和 y 位置与屏幕无关,而是与其他东西有关。这是通过交换两个图像来揭示的;两者仍然绘制在正确的位置。
  • @pythonpython 发现值 0x26 很有趣。这个值也存储在层条目中(我在所有文件中写了“38”,见上文)。
  • 图像数据包含有关“像素线”结束位置的信息。示例:让图层为 32x64。将图像的标题值交换为 64x32 会导致图像在中间被截断但可以正确显示。

我会继续试验并相应地更新。

2个回答

图像被打包,数据在重要的地方是小端的。

一般格式:

struct CGFHeader {
  uint32_t magic;
  uint32_t flags;
  uint32_t frame_count;
  uint32_t frame_metadata_size;
  uint32_t frame_payload_size;
  uint32_t unk1;
  uint32_t unk2; 
};

然后重复frame_count多次,从+0x1c

struct FrameMeta {
  uint32_t unk1;
  uint32_t unk2;
  uint32_t width;
  uint32_t height;
  uint32_t unk3;
  uint32_t payload_offset;
};

对于帧 N,有效载荷数据开始于sizeof(CGFHeader) + cgf_header.frame_count*sizeof(FrameMeta) + frame_meta[N].payload_offset(即在相应的 处payload_offset,紧接在元数据结构之后)。

打包图像数据的每一行/行都是独立打包的。每行都以uint32_t该行长度的 a 为前缀(包括长度字段)。对打包后的数据进行如下处理:

读取一个方法uint8_t和一个长度uint8_t(见n下文),按照下表进行处理。

方法 如何处理
0x00 附加n透明像素。如果n是 0,用透明像素填充线条到预期宽度。
0x01 n从打包数据中读取(palette_index, alpha) 值对,附加到行。
0x02 从打包数据中读取单个 (palette_index, alpha) 对。将其附加到行n时间。
0x03 n从打包数据中读取palette_index 值,附加到该行。
0x04 从打包数据中读取单个 Palette_index。将其附加到行n时间。

从打包数据中保留处理方法uint8_t和长度uint8_t字节,直到您拥有完整的行宽。

为了解释实际的颜色值,您需要相应的调色板。您提到的两种格式:

  • CPAL254X3STD 只是(在这种情况下)在该标题值之后附加了 254 个 RGB 三元组。
  • CPAL254X3ALPHA是相同的,但附加了一个 0x100000 字节的结构,它被称为“alpha 映射”。我根本没有费心去看它。

粗略的python3处理示例,将帧转储到子目录中的0.png、1.png等:

import errno
import os
import struct
import sys

from PIL import Image

class HugoPalette(object):
  def __init__(self, rawdat):
    assert(len(rawdat) >= 12)
    assert(rawdat[0:4] == b'CPAL') # palette
    self.alpha_map = None
    self.entries = None
    num_entries = int(rawdat[4:7])
    assert(rawdat[7:9] == b'X3') # rgb triples
    if rawdat[9:12] == b'STD': # palette
      assert(len(rawdat) >= 12 + num_entries*3)
      self.entries = []
      offset = 12
      for n in range(num_entries):
        self.entries.append(struct.unpack_from("BBB" ,rawdat, offset))
        offset = offset + 3
    elif rawdat[9:14] == b'ALPHA': # palette and alphamap
      assert(len(rawdat) >= 14 + num_entries*3 + 0x100000)
      self.entries = []
      offset = 14
      for n in range(num_entries):
        self.entries.append(struct.unpack_from("BBB" ,rawdat, offset))
        offset = offset + 3
      self.alphamap = rawdat[14 + num_entries*3:14 + num_entries*3 + 0x100000] # not sure how to interpret
    else:
      raise NotImplementedError("unknown palette type")


class HugoImage(object):
  def __init__(self, width, height, rawdat, offset):
    self.width = width
    self.height = height

    rows = []
    for n in range(height):
      (packed_line_length,) =  struct.unpack_from("<L", rawdat, offset)
      assert(len(rawdat) >= offset + packed_line_length)
      packed_line = rawdat[offset+4:offset+packed_line_length]
      line = []
      index = 0
      # unpacking:
      # 00 nn                   = skip nn pixels [nn=00: skip to end of line]
      # 01 nn pp aa [pp aa ...] = insert nn entries from trailing pp, replacing alpha with aa
      # 02 nn pp aa             = repeat pp for nn pixels, replacing alpha with aa
      # 03 nn pp [pp ...]       = insert nn entries from trailing pp
      # 04 nn pp                = repeat pp for nn pixels
      while True:
        method = packed_line[index]
        pixel_count = packed_line[index+1]
        index = index + 2
        if method == 0:
          if pixel_count == 0:
            while(len(line) < self.width):
              line.append((0, 0))
            break
          line.extend([(0,0)]*pixel_count)
        elif method == 1:
          for p in range(pixel_count):
            line.append((packed_line[index], packed_line[index+1]))
            index = index + 2
        elif method == 2:
          line.extend([(packed_line[index], packed_line[index+1])]*pixel_count)
          index = index + 2
        elif method == 3:
          for p in range(pixel_count):
            line.append((packed_line[index],0xff))
            index = index + 1
        elif packed_line[index] == 4:
          line.extend([(packed_line[index], 0xff)]*pixel_count)
          index = index + 1
      assert(len(line) == self.width)
      rows.append(line)
      offset = offset + 4 + index
    self.rows = rows

def load_images(rawdat):
  HEADER_STRUCT_SIZE=0x1c
  METADATA_STRUCT_SIZE=0x18
  offset = 0
  assert(len(rawdat) >= offset + HEADER_STRUCT_SIZE)
  (magic, _, count, metadata_size, payload_size, _, _) = struct.unpack_from("<LLLLLLL", rawdat, offset)
  offset = offset + HEADER_STRUCT_SIZE
  assert(magic == 0x46464743)
  assert(metadata_size == count * METADATA_STRUCT_SIZE)
  metadata = []
  for n in range(count):
    assert(len(rawdat) >= offset + METADATA_STRUCT_SIZE)
    metadata.append(struct.unpack_from("<LLLLLL", rawdat, offset))
    offset = offset + METADATA_STRUCT_SIZE
  images = []
  for im in metadata:
    images.append(HugoImage(im[2], im[3], rawdat, offset + im[5]))
  return images


def main(args):
  if len(args) != 3:
    print(f"usage: python3 {args[0]} palette.pal image.cgf")
    sys.exit(1)
  with open(args[1], "rb") as infile:
    dat = infile.read()
  pal = HugoPalette(dat)

  with open(args[2], "rb") as infile:
    dat = infile.read()
  images = load_images(dat)

  output_dir = args[2] + ".extracted"
  output_index = 0

  try:
    os.makedirs(output_dir)
  except OSError as e:
    if e.errno != errno.EEXIST:
      raise

  for i in images:
    img = Image.new('RGBA', (i.width, i.height))
    for y in range(i.height):
      for x in range(i.width):
        (index, alpha) = i.rows[y][x]
        pal_entry = pal.entries[index]
        col = (pal_entry[0], pal_entry[1],pal_entry[2], alpha)
        img.putpixel((x,y), col)
    img.save(os.path.join(output_dir, str(output_index) + ".png"), "PNG")
    output_index = output_index + 1

if __name__ == '__main__':
  main(sys.argv)

如果您按值 0x26 拆分文件,则会出现一些有趣的结构。

你是如何得出层数的?

我不认为这些是压缩的。压缩会破坏数据中的大量冗余。您也不会在两个不同输入的压缩结果中看到相同的值。

什么是游戏系统?

设备上显示的kid.cgf 和cursor.cgf 的像素大小是多少?

十六进制分析