1.0.1 • Published 2 years ago

replay-parser v1.0.1

Weekly downloads
-
License
ISC
Repository
github
Last release
2 years ago

RA3-Replay-Parser

用于解析红色警戒三(Command & Conquer: Red Alert 3)的回放文件 .RA3Replay

Installation

NPM

npm install replay-parser

手动编译

已测试基于Ubuntu20.04使用 Node-Gyp 进行编译

npm install -g node-gyp
node-gyp configure
node-gyp build

输出位置: build/Release/replayParser.node

Usage

const parser = require("replay-parser");
// parseReplay为一个同步方法
const data = parser.parseReplay("absolute-path-to-replay");

返回值参考以下API Doc

API Doc

C++模块:namespace: ReplayParser

  • 方法: parseReplayFile

    • 接受参数

      • fileName: char[],回放文件的绝对路径
    • 返回值:

      • jsonData: string, 一段JSON字符串
    • 返回值格式

      • status: boolean, 表示是否解析成功
      • message: string, 若 status 为false,则表示错误信息

        以下属性在 statustrue时出现

      • gameVersion: string

      • mapCRC: string, 以键值对字符串形式出现,格式为 MC=CRC校验码,CRC校验码以16进制形式表示
      • mapID: string
      • mapName: string
      • mapRealName: string, 建议后续截取最后一段
      • matchConf: string, 以键值对字符串的形式出现,键为 RU,具体格式表示的信息可查看下文
      • matchDescription: string
      • matchSeed: string, 以键值对形式出现,对战的初始随机数种子(猜测)
      • modInfo: Array<string>
      • playerDetailedInfo: Array<PlayerInfo>, PlayerInfo格式见下文
      • playerNumber: int
      • replayTitle: string
      • timestamp: uint32_t, 保存录像时的时间戳,使用时需乘以1000
    • PlayerDetailedInfo

      • faction: int, 阵营

        std::string getFaction(unsigned int f) {
            switch(f)
            {
                case 1:  return "天眼帝国";
                case 3:  return "Commentator";
                case 4:  return "盟军";
                case 8:  return "苏联";
                case 2:  return "帝国";
                case 7:  return "随机";
                case 9:  return "神州";
                default: return "未知";
            }
        }
      • isPlayer: boolean, 是否为玩家

      • otherData: 见下文中的 S=字段,从第三个开始

RA3 Replay文件格式

主体结构

.ra3replay 包含以下的结构:

  • Header
  • Chunks(长度不固定)
  • End-of-chunks terminator
  • Footer

主要数据类型

  • char: 长度一个字节,用来表示纯文本
  • byte: 长度一个字节,用来表示数值变量
  • uint16_t: 无符号的两字节长的整型,以小端排序存储
  • uint32_t: 无符号的四字节长的整型,以小段排序存储
  • tb_ch: 无符号的双字节值,用于表示BMP Unicode码位(?),以小端排序存储
  • tb_str: 双字节小端排序的unicode字符串,以两个separator结尾

  • player_t: 玩家部分信息

    • player_id: uint32_t
    • player_name: tb_str
    • team_number: byte

常量

  • MAGIC_SIZE = 17
  • U1_SIZE = 31
  • U2_SIZE = 20

Header部分格式

值得注意一点,录像的Header部分在多人游戏和遭遇战的情况下有些许不同,根据字段 hnumber 判断是否是多人游戏。

字段如下(顺序从前到后):

  • str_magic: char, 固定为 RA3 REPLAY HEADER,长度为 MAGTIC_SIZE 字节

  • hnumber1: byte, 若为遭遇战(skirmish)则值为 0x04, 若为多人游戏则值 0x05==(待验证)==

  • vermajor: uint32_t

  • verminor: uint32_t

  • buildmajor: uint32_t

  • buildminor: uint32_t

  • hnumber2: byte, 有评论值为 0x1E, 无评论值为 0x06

  • zero1: byte, 值固定为 0x00, ==但实际有0x20的情况==,根据版本号不同分隔符也不同

  • match_title: tb_str,初步观察 0x E0 65 F9 5B 18

  • match_description: tb_str

  • match_map_name: tb_str

  • match_map_id: tb_str

  • number_of_players: byte

  • player_data: player_t[number_ofplayers + 1]

    • uint32_t player_id
    • tb_str player_name
    • IF hnumber1 == 0x05 byte team_number
    • ENDIF
  • offset: uint32_t, 是介于第一个chunk的开始和str_repl_magic 的开始的分隔符?

  • str_repl_leng: uint32_t,固定为0x08

  • str_repl_magic: char[str_repl_leng]

  • mod_info: char[22], 默认为 RA3

  • timestamp: uint32_t, 是GMT格式的标准Unix时间戳

  • unknow1: byte[U1_SIZE], 全零, 文档为31,==但实测为35,且会出现非全0的情况==

  • header_len: uint32_t

  • header: char[header_len]

  • replay_saver: byte, 是从0开始的玩家数组中保存录像者的索引号

  • zero3: uint32_t, 0x00000000

  • zero4: uint32_t, 0x00000000

  • filename_length: uint32_t

  • filename: tb_ch[filename_length]

  • date_time: tb_ch[8]

    根据位置对应数字有以下意义:

    0: year, 1: month, 2: weekday(0-6=Sun-Sat), 3: day, 4: hour, 5: minute, 6: second, 7: unknown

  • vermagic_len: uint32_t

  • vermagic: char[vermagic_len], 包含了版本号

  • magic_hash: uint32_t, not clear

  • zero4: byte, 0x00

其中,Header的主体是一段包含以键值对出现的信息的纯文本序列,其含义如下:

  • M=

    • unknown: short
    • MapName, 地图名: char[]
  • MC=

    • Map CRC, 地图CRC校验码?: int
  • MS=

    • Map File Size ,地图文件大小: int
  • SD=

    • Seed?: int
  • GSID=

    • GameSpy (Match) ID: short
  • GT=

    • unknown: int
  • PC=

    • Post Commentator: int
  • RU=

    • Initial Camera Player: int
    • Game Speed: int
    • Initial Resources: int
    • Broadcast Game: bool
    • Allow Commentary: bool
    • Tape Delay: int
    • Random Crates: bool
    • Enable VoIP: bool
    • unkwn: int, -1
    • unkwn: int, -1
    • unkwn: int, -1
    • unkwn: int, -1
    • unkwn: int, -1
  • S=

    • player name: char[]
      • HHumanPlayerName || C(CPU)(E(Easy) || M(Medium) || H(Hard) || B(Brutal))
    • IP: int
    • unkwn: int
    • TT|FT

    • unkwn: int

    • Faction: 1 based, 与ini对应

      1: Observer

      3: Commentator

      7: Random

      2: Empire

      4: Allies

      8: Soviets

    • unkwn: int

    • unkwn: int

    • unkwn: int

    • unkwn: int

    • unkwn: int

    • Clan tag: char[]

特殊情况:

  • MS=
    • File Size = 0, 第三方地图
  • GSID
    • GameSpy (Match) ID = 0x5D91, 遭遇战情况
  • RU
    • Initial Camera Player, 1 based ?

Body

.ra3replay的Body部分由多个 chunk 构成,每个 chunk 的结构如下:

  • time_code: uint32_t

  • chunk_type: byte

    值为1、2、3、4

  • chunk_size: uint32_t

  • data: byte[chunk_size]

  • zero: uint32_t

    固定为0x00000000

连续chunk之前存在先序后序的关系,必须按照顺序连续读取,其中最后一个 chunktime_code 的值固定为 0x7FFFFFFF

一个 chunktime_code 对应两帧(1/15 秒)

chunk 的含义

chunk的类型根据 chunk_type 判断

其中 chunk_type=3chunk_type=4 只出现在包含 commentary track 的回放中,type=3 包含音频数据,type=4 包含 telestrator data👴不知道咋翻译

chunk_type=1chunk_type=2data 字段的首位均为1

chunk_type=1data 字段的格式

  • default: byte

    默认为1

  • number_of_commands: uint32_t

  • payload: byte[chunk_size-5]

其中 payload 字段包含命令个数,每个命令之间使用 0xFF 进行分隔

命令的格式:

  • command_id: byte

  • player_id: byte

  • code: byte[command_size - 3]

  • terminator: byte

    固定为0xFF

其中 command_sizecommand_id 决定

  • 部分命令 command_size 固定
  • 部分长度可变但是具有标准布局(standard layout),这个布局依赖于一个单一的偏移量 n

Standard layout 命令

  • payload0: CommandID

  • payload1: PlayerID

    换算算法

    player index = player_id /8 - k, k = 2

  • 如果n > 2,则

    1. Here comes a loop: Letxbe a byte, and set x = payload[n];
      • If x == 0xFF stop, you have reached the end of the command.
      • Let c = (x >> 4) + 1;, i.e. take the upper four bits of x and add one.
      • Read in c values that are 32-bit integers.
      • Read in one more byte and assign it to x, and repeat the loop.

命令具体信息见文件末尾

chunk_type=2data 字段的格式(没怎么读明白)

  • default1: byte

    固定为1

  • default2: byte

    固定为0

  • n: uint32_t

    玩家的索引号,对应于Header中的玩家信息列表

  • default3: byte

    固定为0x0F

  • time_code: uint32_t

  • payload: byte[chunk_size - 11]

据观察 type=2chunk 只在 chunk_size=24chunk_size=40 时出现。

payload + 12 位置开始的 chunk data由3或7个32位的IEEE 754的浮点型变量构成。前三个与以英尺为单位的地图坐标相关,最后四个决定了相机视角的角度和缩放。

byte      flags;        // 0x01: position, 0x02: rotation
IF flags & 0x01
  float32 position[3];  // (x, z, height) in units of feet
ENDIF
IF flags & 0x02
  float32 rotation[4];  // quaternion components?
ENDIF

目前最明确的是该类的 chunk 包含 视角移动数据,玩家数量和 heartbeat

heartbeats 类型

几种类型的回放数据是自动生成的,并且不包含任何用户操作,因此统一称为 heartbeat,其数据的具体意义是未知的,但是它明确用于确认多人个玩家之间游戏状态的完整性。

每个活跃的玩家每秒和最开始的时候创建一个 chunk_type=2chunk,这个 chunk 的长度固定为40字节,其包含在这个时间点完整的相机视角配置。

chunk_type=1chunk 中也存在 heatbeat: 每三秒当时间码的形式是45k+1(?)时,每个玩家触发一次ID为 0x21heatbeat命令。

command_idcommand_sizeDescription
0x0045Harder secundary ability, like the bunker of Soviet Combat Engineer??
0x01specialFor example at the end of every replay; shows the creator of the replay; also observed in other places.
0x02specialSet rally point.(设置集结点)
0x0317Start/resume research upgrade.(开始/继续研究升级)
0x0417Pause/cancel research upgrade.(暂停/取消研究升级)
0x0520Start/resume unit production.(开始/继续单位生产)
0x0620Pause/cancel unit production.(暂停/取消单位生产)
0x0717Start/resume building construction. (Allies and Soviets only, Empire Cores are treated as units.)(开始/继续建筑建造,帝国核心被当作单位处理)
0x0817Pause/cancel building construction.(取消/暂停建筑建造)
0x0935Place building on map (Allies and Soviets only).
0x0Astd: 2Sell building.(售卖建筑)
0x0CspecialPossibly ungarrison?(可能是取消占据房屋)
0x0Dstd: 2Attack.(攻击)
0x0Estd: 2Force-fire.(强制攻击)
0x0F16
0x10specialGarrison a building.(占据房屋)
0x12std: 2
0x1416Move units.(移动单位)
0x1516Attack-move units.(警戒移动)
0x1616Force-move units.(强制移动?)
0x1Astd: 2Stop command.(停止指令)
0x1Bstd: 2
0x2120A heartbeat that every player generates at 45n + 1 frames (every 3 seconds).
0x28std: 2Start repair building.(开始维修建筑)
0x29std: 2Stop repair building.(停止维修建筑)
0x2Astd: 2‘Q’-select.
0x2C29Formation-move preview.(预览队形?)
0x2Estd: 2Stance change.
0x2Fstd: 2Possibly related to waypoint/planning?
0x3253Harder Security Point usage like Surveillance Sweep.
0x33specialSome UUID followed by an IP address plus port number.
0x3445Some UUID.
0x351049Player info?
0x3616
0x37std: 2“Scrolling”, an irregularly, automatically generated command.
0x47std: 2Unknown, always appears in logical frame 5, and than this logical frame contains this command equally as the number of players.
0x48std: 2
0x4BspecialPlace beacon.
0x4Cstd: 2Delete beacon (F9 has something to do with this??).
0x4D???Place text in beacon.
0x4Estd: 2Player power (Secret Protocols).
0x52std: 2
0x5F11
0xF5std: 5Drag a selection box and/or select units.
0xF6std: 5Unknown. You get this command when building a Empire Dojo Core and deploying it. Than it should appear once, no idea what it does.
0xF8std: 4Left mouse button click.
0xF9std: 2
0xFAstd: 7Create group.
0xFBstd: 2Select group.
0xFCstd: 2
0xFDstd: 7
0xFEstd: 15Simple use of secundary ability, like those of War Bear, Conscript and Flaktrooper.
0xFFstd: 34Simple select and klick Security Point usage like Sleeper Ambush.

感谢

感谢远古大佬 louisdx (Louis Delacroix) (github.com)制作的RA3 Replay文件格式文档。