/
header.go
560 lines (531 loc) · 14.8 KB
/
header.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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
package dissect
import (
"bufio"
"bytes"
"encoding/json"
"strconv"
"time"
"github.com/rs/zerolog/log"
)
type Header struct {
GameVersion string `json:"gameVersion"`
CodeVersion int `json:"codeVersion"`
Timestamp time.Time `json:"timestamp"`
MatchType MatchType `json:"matchType"`
Map Map `json:"map"`
Site string `json:"site,omitempty"`
RecordingPlayerID uint64 `json:"recordingPlayerID"`
RecordingProfileID string `json:"recordingProfileID,omitempty"`
AdditionalTags string `json:"additionalTags"`
GameMode GameMode `json:"gamemode"`
RoundsPerMatch int `json:"roundsPerMatch"`
RoundsPerMatchOvertime int `json:"roundsPerMatchOvertime"`
RoundNumber int `json:"roundNumber"`
OvertimeRoundNumber int `json:"overtimeRoundNumber"`
Teams [2]Team `json:"teams"`
Players []Player `json:"players"`
GMSettings []int `json:"gmSettings"`
PlaylistCategory int `json:"playlistCategory,omitempty"`
MatchID string `json:"matchID"`
}
type Team struct {
Name string `json:"name"`
Score int `json:"score"`
Won bool `json:"won"`
WinCondition WinCondition `json:"winCondition,omitempty"`
Role TeamRole `json:"role,omitempty"`
}
type Player struct {
ID uint64 `json:"id,omitempty"`
ProfileID string `json:"profileID,omitempty"` // Ubisoft stats identifier
Username string `json:"username"`
TeamIndex int `json:"teamIndex"`
Operator Operator `json:"operator"`
HeroName int `json:"heroName,omitempty"`
Alliance int `json:"alliance"`
RoleImage int `json:"roleImage,omitempty"`
RoleName string `json:"roleName,omitempty"`
RolePortrait int `json:"rolePortrait,omitempty"`
Spawn string `json:"spawn,omitempty"`
DissectID []byte `json:"-" deep:"-"` // dissect player id at end of packet (4 bytes)
}
type stringerIntMarshal struct {
Name string `json:"name"`
ID int `json:"id"`
}
type MatchType int
type GameMode int
type Map int
type WinCondition string
type TeamRole string
type Operator uint64
//go:generate stringer -type=MatchType
//go:generate stringer -type=GameMode
//go:generate stringer -type=Map
//go:generate stringer -type=Operator
//go:generate go run ./genops.go -type=Operator -atkval=Attack -defval=Defense
const (
QuickMatch MatchType = 1
Ranked MatchType = 2
CustomGameLocal MatchType = 3
CustomGameOnline MatchType = 4
Standard MatchType = 8
Bomb GameMode = 327933806
SecureArea GameMode = 1983085217
Hostage GameMode = 2838806006
ClubHouse Map = 837214085
KafeDostoyevsky Map = 1378191338
Kanal Map = 1460220617
Yacht Map = 1767965020
PresidentialPlane Map = 2609218856
ConsulateY7 Map = 2609221242
BartlettU Map = 2697268122
Coastline Map = 42090092951
Tower Map = 53627213396
Villa Map = 88107330328
Fortress Map = 126196841359
HerefordBase Map = 127951053400
ThemePark Map = 199824623654
Oregon Map = 231702797556
House Map = 237873412352
Chalet Map = 259816839773
Skyscraper Map = 276279025182
Border Map = 305979357167
Favela Map = 329867321446
Bank Map = 355496559878
Outback Map = 362605108559
EmeraldPlains Map = 365284490964
StadiumBravo Map = 270063334510
NighthavenLabs Map = 378595635123
Consulate Map = 379218689149
Lair Map = 388073319671
KilledOpponents WinCondition = "KilledOpponents"
SecuredArea WinCondition = "SecuredArea" // TODO
DisabledDefuser WinCondition = "DisabledDefuser"
DefusedBomb WinCondition = "DefusedBomb"
ExtractedHostage WinCondition = "ExtractedHostage" // TODO
Time WinCondition = "Time"
Attack TeamRole = "Attack"
Defense TeamRole = "Defense"
Recruit Operator = 359656345734
Castle Operator = 92270642682 // May technically refer to the op icon?
Aruni Operator = 104189664704
Kaid Operator = 161289666230
Mozzie Operator = 174977508820
Pulse Operator = 92270642708
Ace Operator = 104189664390
Echo Operator = 92270642214
Azami Operator = 378305069945
Solis Operator = 391752120891
Capitao Operator = 92270644215
Zofia Operator = 92270644189
Dokkaebi Operator = 92270644267
Warden Operator = 104189662920
Mira Operator = 92270644319
Sledge Operator = 92270642344
Melusi Operator = 104189664273
Bandit Operator = 92270642526
Valkyrie Operator = 92270642188
Rook Operator = 92270644059
Kapkan Operator = 92270641980
Zero Operator = 291191151607
Iana Operator = 104189664038
Ash Operator = 92270642656
Blackbeard Operator = 92270642136
Osa Operator = 288200867444
Thorn Operator = 373711624351
Jager Operator = 92270642604
Kali Operator = 104189663920
Thermite Operator = 92270642760
Brava Operator = 288200866821
Amaru Operator = 104189663607
Ying Operator = 92270642292
Lesion Operator = 92270642266
Doc Operator = 92270644007
Lion Operator = 104189661861
Fuze Operator = 92270642032
Smoke Operator = 92270642396
Vigil Operator = 92270644293
Mute Operator = 92270642318
Goyo Operator = 104189663698
Wamai Operator = 104189663803
Ela Operator = 92270644163
Montagne Operator = 92270644033
Nokk Operator = 104189663024
Alibi Operator = 104189662071
Finka Operator = 104189661965
Caveira Operator = 92270644241
Nomad Operator = 161289666248
Thunderbird Operator = 288200867351
Sens Operator = 384797789346
IQ Operator = 92270642578
Blitz Operator = 92270642539
Hibana Operator = 92270642240
Maverick Operator = 104189662384
Flores Operator = 328397386974
Buck Operator = 92270642474
Twitch Operator = 92270644111
Gridlock Operator = 174977508808
Thatcher Operator = 92270642422
Glaz Operator = 92270642084
Jackal Operator = 92270644345
Grim Operator = 374667788042
Tachanka Operator = 291437347686
Oryx Operator = 104189664155
Frost Operator = 92270642500
Maestro Operator = 104189662175
Clash Operator = 104189662280
Fenrir Operator = 288200867339
Ram Operator = 395943091136
Tubarao Operator = 288200867549
Deimos Operator = 374667787816
)
// duplicated code here could be avoided by defining a generic function accepting any Number type.
// that kind of constraint is still experimental though (as of Go 1.20.2): https://pkg.go.dev/golang.org/x/exp/constraints
func (i MatchType) MarshalJSON() (text []byte, err error) {
return json.Marshal(stringerIntMarshal{
Name: i.String(),
ID: int(i),
})
}
func (i *MatchType) UnmarshalJSON(data []byte) (err error) {
var x stringerIntMarshal
if err = json.Unmarshal(data, &x); err != nil {
return
}
*i = MatchType(x.ID)
return
}
func (i GameMode) MarshalJSON() (text []byte, err error) {
return json.Marshal(stringerIntMarshal{
Name: i.String(),
ID: int(i),
})
}
func (i *GameMode) UnmarshalJSON(data []byte) (err error) {
var x stringerIntMarshal
if err = json.Unmarshal(data, &x); err != nil {
return
}
*i = GameMode(x.ID)
return
}
func (i Map) MarshalJSON() (text []byte, err error) {
return json.Marshal(stringerIntMarshal{
Name: i.String(),
ID: int(i),
})
}
func (i *Map) UnmarshalJSON(data []byte) (err error) {
var x stringerIntMarshal
if err = json.Unmarshal(data, &x); err != nil {
return
}
*i = Map(x.ID)
return
}
func (i Operator) MarshalJSON() (text []byte, err error) {
return json.Marshal(stringerIntMarshal{
Name: i.String(),
ID: int(i),
})
}
func (i *Operator) UnmarshalJSON(data []byte) (err error) {
var x stringerIntMarshal
if err = json.Unmarshal(data, &x); err != nil {
return
}
*i = Operator(x.ID)
return
}
func (h Header) RecordingPlayer() Player {
for _, val := range h.Players {
if val.ID == h.RecordingPlayerID {
return val
}
}
return Player{}
}
func testFileCompression(in *bufio.Reader) (chunkedCompression bool, err error) {
magic, err := in.Peek(4)
if err != nil {
return false, err
}
if bytes.Equal(magic, []byte{0x28, 0xB5, 0x2F, 0xFD}) {
return false, nil
} else if bytes.Equal(magic, []byte{0x64, 0x69, 0x73, 0x73}) {
return true, nil
}
return false, ErrInvalidFile
}
// readHeaderMagic reads the header magic of the reader
// and validates the dissect format.
// If there is an error, it will be of type *ErrInvalidFile.
func (r *Reader) readHeaderMagic() error {
// Checks for the dissect header.
b, err := r.Bytes(7)
if err != nil {
return err
}
if string(b[:7]) != "dissect" {
return ErrInvalidFile
}
// Skips to the end of the unknown dissect versioning scheme.
// Probably will be replaced later when more info is uncovered.
// We are skipping to the end of the second sequence of 7 0x00 bytes
// where the string values are stored.
n := 0
t := 0
for t != 2 {
b, err := r.Bytes(1)
if err != nil {
return err
}
if b[0] == 0x00 {
if n != 6 {
n++
} else {
n = 0
t++
}
} else if n > 0 {
n = 0
}
}
return nil
}
func (r *Reader) readHeader() (Header, error) {
props := make(map[string]string)
gmSettings := make([]int, 0)
players := make([]Player, 0)
// Loops until the last property is mapped.
currentPlayer := Player{}
playerData := false
for lastProp := false; !lastProp; {
k, err := r.readHeaderString()
if err != nil {
return Header{}, err
}
v, err := r.readHeaderString()
if err != nil {
return Header{}, err
}
if k == "playerid" {
if playerData {
players = append(players, currentPlayer)
}
playerData = true
currentPlayer = Player{}
}
if (k == "playlistcategory" || k == "id") && playerData {
players = append(players, currentPlayer)
playerData = false
}
if !playerData {
if k != "gmsetting" {
props[k] = v
} else {
n, err := strconv.Atoi(v)
if err != nil {
return Header{}, err
}
gmSettings = append(gmSettings, n)
}
} else {
switch k {
case "playerid":
n, err := strconv.ParseUint(v, 10, 64)
if err != nil {
return Header{}, err
}
currentPlayer.ID = n
case "playername":
currentPlayer.Username = v
case "team":
n, err := strconv.Atoi(v)
if err != nil {
return Header{}, err
}
currentPlayer.TeamIndex = n
case "heroname":
n, err := strconv.Atoi(v)
if err != nil {
return Header{}, err
}
currentPlayer.HeroName = n
case "alliance":
n, err := strconv.Atoi(v)
if err != nil {
return Header{}, err
}
currentPlayer.Alliance = n
case "roleimage":
n, err := strconv.Atoi(v)
if err != nil {
return Header{}, err
}
currentPlayer.RoleImage = n
case "rolename":
currentPlayer.RoleName = v
case "roleportrait":
n, err := strconv.Atoi(v)
if err != nil {
return Header{}, err
}
currentPlayer.RolePortrait = n
}
}
_, lastProp = props["teamscore1"]
}
h := Header{
Teams: [2]Team{},
Players: players,
GMSettings: gmSettings,
}
// Parse game version
h.GameVersion = props["version"]
// Parse code version
n, err := strconv.Atoi(props["code"])
if err != nil {
return h, err
}
h.CodeVersion = n
// Parse timestamp
t, err := time.Parse("2006-01-02-15-04-05", props["datetime"])
if err != nil {
return h, err
}
h.Timestamp = t
// Parse match type
n, err = strconv.Atoi(props["matchtype"])
if err != nil {
return h, err
}
h.MatchType = MatchType(n)
// Parse map
n, err = strconv.Atoi(props["worldid"])
if err != nil {
return h, err
}
h.Map = Map(n)
// Add recording player id
u, err := strconv.ParseUint(props["recordingplayerid"], 10, 64)
if err != nil {
return h, err
}
h.RecordingPlayerID = u
h.RecordingProfileID = props["recordingprofileid"]
// Add additional tags
h.AdditionalTags = props["additionaltags"]
// Parse game mode
n, err = strconv.Atoi(props["gamemodeid"])
if err != nil {
return h, err
}
h.GameMode = GameMode(n)
// Parse rounds per match
n, err = strconv.Atoi(props["roundspermatch"])
if err != nil {
return h, err
}
h.RoundsPerMatch = n
// Parse rounds per match overtime
n, err = strconv.Atoi(props["roundspermatchovertime"])
if err != nil {
return h, err
}
h.RoundsPerMatchOvertime = n
// Parse round number
n, err = strconv.Atoi(props["roundnumber"])
if err != nil {
return h, err
}
h.RoundNumber = n
// Parse overtime round number
n, err = strconv.Atoi(props["overtimeroundnumber"])
if err != nil {
return h, err
}
h.OvertimeRoundNumber = n
// Add team names
h.Teams[0].Name = props["teamname0"]
h.Teams[1].Name = props["teamname1"]
// Add playlist category
if len(props["playlistcategory"]) > 0 {
n, err = strconv.Atoi(props["playlistcategory"])
if err != nil {
log.Debug().Err(err).Msg("omitting playlistcategory")
}
h.PlaylistCategory = n
}
// Add match id
h.MatchID = props["id"]
// Parse team scores
n, err = strconv.Atoi(props["teamscore0"])
if err != nil {
return h, err
}
h.Teams[0].Score = n
n, err = strconv.Atoi(props["teamscore1"])
if err != nil {
return h, err
}
h.Teams[1].Score = n
return h, nil
}
// deriveTeamRoles uses the operators chosen by the players to
// determine the team roles
func (r *Reader) deriveTeamRoles() {
log.Debug().Int("players", len(r.Header.Players)).Msg("deriving team roles")
if len(r.Header.Players) > 10 {
log.Warn().Msg("tracked players greater than 10")
}
players := r.Header.Players[:0]
for _, p := range r.Header.Players {
log.Debug().Interface("player", p).Send()
if p.Operator != 0 {
players = append(players, p)
r.Scoreboard.Players = append(r.Scoreboard.Players, ScoreboardPlayer{
ID: p.DissectID,
})
} else {
log.Warn().Str("username", p.Username).Msg("operator id was 0, removing from list")
}
}
r.Header.Players = players
for _, p := range r.Header.Players {
if p.Operator == Recruit {
continue
}
role := p.Operator.Role()
teamIndex := p.TeamIndex
oppositeTeamIndex := teamIndex ^ 1
if role == Attack {
r.Header.Teams[teamIndex].Role = Attack
r.Header.Teams[oppositeTeamIndex].Role = Defense
} else {
r.Header.Teams[teamIndex].Role = Defense
r.Header.Teams[oppositeTeamIndex].Role = Attack
}
break
}
}
func (r *Reader) readHeaderString() (string, error) {
b, err := r.Bytes(1)
if err != nil {
return "", err
}
len := int(b[0])
b, err = r.Bytes(7)
if err != nil {
return "", err
}
if !bytes.Equal(b, strSep) {
return "", ErrInvalidStringSep
}
b, err = r.Bytes(len)
if err != nil {
return "", err
}
return string(b), nil
}