forked from nielsAD/gowarcraft3
/
w3g.go
185 lines (168 loc) · 4.86 KB
/
w3g.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
// Author: Niels A.D.
// Project: gowarcraft3 (https://github.com/nielsAD/gowarcraft3)
// License: Mozilla Public License, v2.0
// Package w3g implements a decoder and encoder for w3g files.
//
// Each record type is mapped to a struct type that implements the Record
// interface. To open a file, use w3g.Open().
//
// Format:
//
// size/type | Description
// -----------+-----------------------------------------------------------
// 28 chars | zero terminated string "Warcraft III recorded game\0x1A\0"
// 1 dword | fileoffset of first compressed data block (header size)
// | 0x40 for WarCraft III with patch <= v1.06
// | 0x44 for WarCraft III patch >= 1.07 and TFT replays
// 1 dword | overall size of compressed file
// 1 dword | replay header version:
// | 0x00 for WarCraft III with patch <= 1.06
// | 0x01 for WarCraft III patch >= 1.07 and TFT replays
// 1 dword | overall size of decompressed data (excluding header)
// 1 dword | number of compressed data blocks in file
//
// * replay header version 0x00:
// 1 word | unknown (always zero so far)
// 1 word | version number (corresponds to patch 1.xx)
// * replay header version 0x01:
// 1 dword | version identifier string reading:
// | 'WAR3' for WarCraft III Classic
// | 'W3XP' for WarCraft III Expansion Set 'The Frozen Throne'
// 1 dword | version number (corresponds to patch 1.xx so far)
//
// 1 word | build number
// 1 word | flags
// | 0x0000 for single player games
// | 0x8000 for multiplayer games (LAN or Battle.net)
// 1 dword | replay length in msec
// 1 dword | CRC32 checksum for the header
// | (the checksum is calculated for the complete header
// | including this field which is set to zero)
//
// For each data block:
// 1 word | size n of compressed data block (excluding header)
// 1 word | size of decompressed data block (currently 8k)
// 1 word | CRC checksum for the header
// 1 word | CRC checksum for the compressed block
// n bytes | compressed data (using zlib)
//
package w3g
import (
"bufio"
"io"
"os"
"github.com/nielsAD/gowarcraft3/protocol/w3gs"
)
// Replay information for Warcraft III recorded game
type Replay struct {
Header
GameInfo
SlotInfo
PlayerInfo []*PlayerInfo
PlayerExtra []*PlayerExtra
Records []Record
}
// Open a w3g file
func Open(name string) (*Replay, error) {
f, err := os.Open(name)
if err != nil {
return nil, err
}
defer f.Close()
var b = bufio.NewReaderSize(f, 8192)
if _, err := FindHeader(b); err != nil {
return nil, ErrBadFormat
}
rep, err := Decode(b)
return rep, err
}
// Save a w3g file
func (r *Replay) Save(name string) error {
f, err := os.Create(name)
if err != nil {
return err
}
defer f.Close()
return r.Encode(f)
}
// Encode to w
func (r *Replay) Encode(w io.Writer) error {
e, err := NewEncoder(w, r.Encoding())
if err != nil {
return err
}
if _, err := e.WriteRecords(&r.GameInfo, &r.SlotInfo); err != nil {
return err
}
for _, p := range r.PlayerInfo {
if p.ID == r.HostPlayer.ID {
// Skip host
continue
}
if _, err := e.WriteRecord(p); err != nil {
return err
}
}
for _, p := range r.PlayerExtra {
if _, err := e.WriteRecord(p); err != nil {
return err
}
}
if _, err := e.WriteRecords(&CountDownStart{}, &CountDownEnd{}, &GameStart{}); err != nil {
return err
}
if _, err := e.WriteRecords(r.Records...); err != nil {
return err
}
e.Header = r.Header
return e.Close()
}
// Decode a w3g file
func Decode(r io.Reader) (*Replay, error) {
hdr, data, _, err := DecodeHeader(r, nil)
if err != nil {
return nil, err
}
var res = Replay{Header: *hdr}
if err := data.ForEach(func(r Record) error {
switch v := r.(type) {
case *GameInfo:
res.GameInfo = *v
res.PlayerInfo = []*PlayerInfo{&res.GameInfo.HostPlayer}
case *SlotInfo:
res.SlotInfo = *v
case *PlayerInfo:
res.PlayerInfo = append(res.PlayerInfo, v)
case *PlayerExtra:
res.PlayerExtra = append(res.PlayerExtra, v)
case *CountDownStart, *CountDownEnd, *GameStart, *TimeSlotAck:
// Ignore
case *ChatMessage:
if v.Type == w3gs.MsgChatExtra {
res.Records = append(res.Records, v)
}
default:
res.Records = append(res.Records, v)
}
return nil
}); err != nil {
return nil, err
}
if len(res.SlotInfo.Slots) == 0 {
for i, p := range res.PlayerInfo {
res.SlotInfo.NumPlayers++
res.SlotInfo.Slots = append(res.SlotInfo.Slots, w3gs.SlotData{
PlayerID: uint8(i + 1),
DownloadStatus: 100,
SlotStatus: w3gs.SlotOccupied,
Computer: false,
Team: uint8(i % 2),
Color: uint8(i),
Race: p.Race,
ComputerType: w3gs.ComputerNormal,
Handicap: 100,
})
}
}
return &res, nil
}