解析帝国时代游戏记录文件(.mgx)

12

我是一个过时游戏《帝国时代II》(AoE)的粉丝。我想使用Python编写一个AoE游戏记录(.mgx文件)的解析器。

我在GitHub上进行了一些搜索,发现很少有相关项目,其中最有用的是aoc-mgx-format,它提供了.mgx游戏记录文件的一些细节

这里有个问题:

根据参考资料,.mgx文件的结构如下:

| header_len(4字节整数) | next_pos(4字节整数) | header_data | ... ... |

mgx格式中十六进制数据的字节顺序为小端

header_len存储头部部分(header_len + next_post + header_data)的数据长度。

header_data 存储了我需要的有用信息,但它使用 zlib 进行了压缩。

我尝试使用 zlib 模块解压 header_data 中的数据,代码如下:

import struct
import zlib

with open('test.mgx', "rb") as fp:
    # read the header_len bytes and covert it to a int reprents length of Header part
    header_len = struct.unpack("<i", fp.read(4))[0]

    # read next_pos (this is not important for me)
    next_pos = struct.unpack("<i", fp.read(4))[0]

    # then I can get data length of header_data part(compressed with zlib)
    header_data_len = header_len - 8

    compressed_data = fp.read(header_data_len)[::-1] # need to be reversed because byte order is little endian?

    try:
        zlib.decompress(compressed_data)
        print "can be decompressed!"
    except zlib.error as e:
        print e.message

但我运行程序后得到了以下内容:

解压数据时出错,错误代码为-3:头文件检查不正确

PS:可在此处找到示例.mgx文件:https://github.com/stefan-kolb/aoc-mgx-format/tree/master/parser/recs


5
你的问题中有一个错别字,你说“过时的游戏帝国时代”,我认为你是指“非常棒的游戏帝国时代”。 - user764357
@abarnert 谢谢。我只在前8个字节上使用了struct.unpack()。对于header_data,我认为在zlib.decompress()之前需要将其反转。我尝试过不反转它,但问题仍然存在。 - lichifeng
1
@abarnert 你太棒了!!!我用“没有zlib的zlib”进行了谷歌搜索,找到了一些有用的信息!zlib.decompress(compressed_data, -zlib.MAX_WBITS)会起作用。 - lichifeng
1
@abarnert 当然,我在真实项目中会使用解压缩器对象,上面的代码只是测试用的。再次感谢,我想你也玩过这个游戏 XDDD - lichifeng
1
我认为上一个评论是针对@LegoStormtroopr的,而不是我。 :) 我玩过它,但已经很久没玩了。我喜欢Europa Universalis和Crusader Kings这样的策略游戏,所以我的问题是关于如何编写一个迭代解析器来解析人类可读的文本,但文件大小达到了300MB。 :) - abarnert
显示剩余5条评论
2个回答

5
你的第一个问题是不应该反转数据,只需删除[::-1]即可。

但如果你这样做,就会得到一个不同的错误-3,通常是关于未知压缩方法的错误。

问题在于这是无头的zlib数据,就像gzip使用的那样。理论上,这意味着有关压缩方法、窗口、起始字典等信息必须在文件的其他地方提供(在gzip的情况下,通过gzip头中的信息)。但实际上,每个人都使用最大窗口大小和无起始字典的deflate,因此如果我在当每个字节都很重要的时代为游戏设计一种紧凑格式,我只会将它们硬编码。(在现代,类似的标准已经被规范为“DEFLATE 压缩数据格式”,但大多数90年代的PC游戏并没有按照规范设计...)

所以:

>>> uncompressed_data = zlib.decompress(compressed_data, -zlib.MAX_WBITS)
>>> uncompressed_data[:8] # version
b'VER 9.8\x00'
>>> uncompressed_data[8:12] # unknown_const
b'\xf6(<A'

因此,它不仅可以解压缩,看起来像是一个版本和...好吧,我猜任何东西都像是一个未知的常量,但在规范中它是相同的未知常量,所以我认为我们很好。
decompress文档所述,MAX_WBITS是默认/最常见的窗口大小(通常称为“zlib deflate”,而不是“zlib”),传递负值意味着头文件被抑制;其他参数我们可以保持默认设置。
另请参见此答案zlib文档中的高级函数部分以及RFC 1951。(感谢OP找到这些链接。)

1
非常感谢,我用你提供的一些关键词找到了这个链接,也很有用 https://dev59.com/ZHI-5IYBdhLWcg3weoJP#22311297 - lichifeng
@lichifeng:我在答案中添加了链接。很好的发现。 - abarnert

3

虽然有点老,但这是我之前做过的一个样例:

class GameRecordParser:

def __init__(self, filename):
    self.filename = filename
    f = open(filename, 'rb')

    # Get header size
    header_size = struct.unpack('<I', f.read(4))[0]
    sub = struct.unpack('<I', f.read(4))[0]
    if sub != 0 and sub < os.stat(filename).st_size:
        f.seek(4)
        self.header_start = 4
    else:
        self.header_start = 8

    # Get and decompress header
    header = f.read(header_size - self.header_start)
    self.header_data = zlib.decompress(header, -zlib.MAX_WBITS)

    # Get body
    self.body = f.read()
    f.close()

    # Get players data
    sep = b'\x04\x00\x00\x00Gaia'
    pos = self.header_data.find(sep) + len(sep)
    players = []
    for k in range(0, 8):
        id = struct.unpack('<I', self.header_data[pos:pos+4])[0]
        pos += 4
        type = struct.unpack('<I', self.header_data[pos:pos+4])[0]
        pos += 4
        name_size = struct.unpack('<I', self.header_data[pos:pos+4])[0]
        pos += 4
        name = self.header_data[pos:pos+name_size].decode('utf-8')
        pos += name_size
        if id < 9:
            players.append(Player(id, type, name))

希望这能帮助未来的程序员 :)
顺便说一下,我计划编写这样一个库。

你写了那个库吗? - Bastiano
@Bastiano 目前仍在进行中,但是这些日子我没有太多时间 (https://github.com/voblivion/AoE2RecordsParser)。欢迎您提供贡献 ;) 我会检查任何拉取请求 / 建议。 - Victor Drouin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接