1 package replay
2
3 import (
4 "bytes"
5 "encoding/binary"
6 "errors"
7 "fmt"
8 "io"
9
10 "go.formulabun.club/functional/array"
11 )
12
13 var demoHeader = "KartReplay"
14
15 func ReadReplayData(data []byte) (result ReplayRaw, err error) {
16 dataReader := bytes.NewReader(data)
17 return ReadReplay(dataReader)
18 }
19
20 func ReadReplay(data io.Reader) (result ReplayRaw, err error) {
21 var headerPreReplays HeaderPreFileEntries
22 err = binary.Read(data, binary.LittleEndian, &headerPreReplays)
23 if err != nil {
24 return result, fmt.Errorf("Could not read the replay header before addons: %s", err)
25 }
26 result.HeaderPreFileEntries = headerPreReplays
27
28 fileCount := int(result.FileCount)
29 result.WadEntries = make([]WadEntry, fileCount)
30 readCount := 0
31 for readCount < fileCount {
32 entry, err := readWadEntry(data)
33 if err != nil {
34 return result, fmt.Errorf("Could not read the file entry number %d: %s", readCount+1, err)
35 }
36 result.WadEntries[readCount] = entry
37 readCount++
38 }
39
40 var headerPostReplays HeaderPostFileEntries
41 err = binary.Read(data, binary.LittleEndian, &headerPostReplays)
42 if err != nil {
43 return result, fmt.Errorf("Could not read the replay header before addons: %s", err)
44 }
45 result.HeaderPostFileEntries = headerPostReplays
46
47 cvarCount := int(result.CVarCount)
48 result.CVarEntries = make([]CVarEntry, cvarCount)
49 readCount = 0
50 for readCount < cvarCount {
51 entry, err := readCVarEntry(data)
52 if err != nil {
53 return result, fmt.Errorf("Could not read cvar entry number %d: %s", readCount+1, err)
54 }
55 result.CVarEntries[readCount] = entry
56 readCount++
57 }
58
59 players, end, err := readPlayerEntries(data)
60 if err != nil {
61 return result, fmt.Errorf("Could not read player entries from file: %s", err)
62 }
63 result.PlayerEntries = players
64 result.PlayerListingEnd = end
65
66 return result, validate(result)
67 }
68
69 func (R *ReplayRaw) Write(writer io.Writer) error {
70 err := binary.Write(writer, binary.LittleEndian, R.HeaderPreFileEntries)
71 if err != nil {
72 return fmt.Errorf("Could not write the replay header: %s", err)
73 }
74 for _, replayEntry := range R.WadEntries {
75 _, err = io.WriteString(writer, replayEntry.FileName)
76 if err != nil {
77 return fmt.Errorf("Could not write replay file name: %s", err)
78 }
79 _, err = writer.Write(replayEntry.WadMd5[:])
80 if err != nil {
81 return fmt.Errorf("Could not write replay file checksum: %s", err)
82 }
83 }
84
85 err = binary.Write(writer, binary.LittleEndian, R.HeaderPostFileEntries)
86 if err != nil {
87 return err
88 }
89
90 for _, cvarEntry := range R.CVarEntries {
91 err = binary.Write(writer, binary.LittleEndian, cvarEntry.CVarId)
92 _, err = io.WriteString(writer, cvarEntry.Value)
93 err = binary.Write(writer, binary.LittleEndian, cvarEntry.False)
94 if err != nil {
95 return err
96 }
97 }
98
99 for _, playerEntry := range R.PlayerEntries {
100 err = binary.Write(writer, binary.LittleEndian, playerEntry)
101 if err != nil {
102 return err
103 }
104 }
105
106 err = binary.Write(writer, binary.LittleEndian, R.PlayerListingEnd)
107 return err
108 }
109
110 func readWadEntry(data io.Reader) (result WadEntry, err error) {
111 filename, extra, err := readNullTerminatedString(data, 16)
112 if err != nil {
113 return result, err
114 }
115
116 result.FileName = filename
117 copy(result.WadMd5[:len(extra)], extra)
118
119 n, err := data.Read(result.WadMd5[len(extra):])
120 if err != nil {
121 return result, fmt.Errorf("Could not read a file entry from the replay: %s", err)
122 }
123 if n < 16-len(extra) {
124 return result, fmt.Errorf("Unexpected end to the replay file.")
125 }
126 return result, nil
127 }
128
129 func readCVarEntry(data io.Reader) (result CVarEntry, err error) {
130 err = binary.Read(data, binary.LittleEndian, &result.CVarId)
131 if err != nil {
132 return result, fmt.Errorf("Could not read CVar ID: %s", err)
133 }
134
135 cvarValue, extra, err := readNullTerminatedString(data, 1)
136 if err != nil {
137 return result, err
138 }
139
140 result.Value = cvarValue
141 if len(extra) != 2 {
142 binary.Read(data, binary.LittleEndian, &result.False)
143 } else {
144 result.False = extra[1]
145 }
146 return result, err
147 }
148
149 func readPlayerEntries(data io.Reader) (result PlayerEntries, end byte, err error) {
150 var spec byte
151
152 err = binary.Read(data, binary.LittleEndian, &spec)
153 if err != nil {
154 return result, spec, fmt.Errorf("Could not read player spec value: %s", err)
155 }
156
157 for spec != 0xff {
158 var playerEntry PlayerEntry
159
160 err = binary.Read(data, binary.LittleEndian, &playerEntry.PlayerEntryData)
161 if err != nil {
162 return result, spec, fmt.Errorf("Could not read player entry: %s", err)
163 }
164 playerEntry.Spec = spec
165 result = append(result, playerEntry)
166
167 err = binary.Read(data, binary.LittleEndian, &spec)
168 if err != nil {
169 return result, spec, fmt.Errorf("Could not read player spec value: %s", err)
170 }
171 }
172 return result, spec, nil
173 }
174
175 func validate(replay ReplayRaw) error {
176 headerText := string(replay.DemoHeader[1:11])
177 badFileError := errors.New("Not a kart replay file")
178
179 if demoHeader != headerText && replay.DemoHeader[0] == 0xf0 && replay.DemoHeader[11] == 0x0f {
180 return badFileError
181 }
182 if string(replay.Play[:]) != "PLAY" {
183 return badFileError
184 }
185 if replay.DemoFlags&0x2 == 0 {
186 return badFileError
187 }
188 return nil
189 }
190
191 func readNullTerminatedString(data io.Reader, bufferSize int) (string, []byte, error) {
192 var result bytes.Buffer
193 buffer := make([]byte, bufferSize)
194
195 for {
196 n, err := data.Read(buffer)
197 if err != nil {
198 return result.String(), buffer, fmt.Errorf("Could not read from the replay: %s", err)
199 }
200 if n < bufferSize {
201 return result.String(), buffer, fmt.Errorf("Unexpected end to the replay file.")
202 }
203
204 found := array.FindFirstIndexMatching(buffer, 0x00)
205 if found >= 0 {
206 result.Write(buffer[:found])
207 return result.String(), buffer[found+1:], nil
208 }
209 result.Write(buffer)
210 }
211 }
212
View as plain text