-
Notifications
You must be signed in to change notification settings - Fork 1
/
reader.go
993 lines (906 loc) · 28.9 KB
/
reader.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
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
// Warcrumb - Replay parser library for Warcraft 3
// Copyright (C) 2020 Dmitry Narkevich
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package warcrumb
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"math/bits"
"os"
"strings"
"time"
)
// ParseReplayDebug is the same as ParseReplay but dumps binaries and prints to stdout too
func ParseReplayDebug(file io.Reader) (rep Replay, err error) {
rep.parseOptions.debugMode = true
err = read(file, &rep)
return rep, err
}
// ParseReplay parses an opened .w3g file.
func ParseReplay(file io.Reader) (rep Replay, err error) {
err = read(file, &rep)
return rep, err
}
func read(file io.Reader, rep *Replay) (err error) {
header, err := readHeader(file)
if err != nil {
return fmt.Errorf("error reading header: %w", err)
}
rep.IsMultiplayer = header.IsMultiplayer
rep.Duration = header.Duration
rep.Version = header.GameVersion
rep.Expac = header.Expac
rep.BuildNumber = header.BuildNumber
rep.isReforged = rep.Version >= 10032
// might as well allocate the right size buffer based on the assumption that every block is 8K
buffer := bytes.NewBuffer(make([]byte, 0, header.NumberOfBlocks*0x2000))
for i := 0; i < int(header.NumberOfBlocks); i++ {
b, err := readCompressedBlock(file, rep.isReforged)
if err != nil {
return fmt.Errorf("failed to decompress block i=%d: %w", i, err)
}
buffer.Write(b)
}
bufferCopy := make([]byte, buffer.Len())
copy(bufferCopy, buffer.Bytes())
bufferLen := buffer.Len()
err = readDecompressedData(buffer, rep)
if rep.debugMode {
_ = os.Mkdir("hexdumps", os.ModePerm)
_ = ioutil.WriteFile(fmt.Sprintf("./hexdumps/decompresssed_%s_%s.hex", rep.GameOptions.GameName, rep.GameOptions.CreatorName), bufferCopy, os.ModePerm)
}
if err != nil {
readBytes := bufferLen - buffer.Len()
return fmt.Errorf("error in decompressed data at/before %#x: %w", readBytes, err)
}
return
}
func readHeader(file io.Reader) (header header, err error) {
magicString := make([]byte, 28)
if _, err = file.Read(magicString); err != nil {
return header, fmt.Errorf("error reading magic string: %w", err)
}
expected := []byte("Warcraft III recorded game\x1A\x00")
if !bytes.Equal(magicString, expected) {
return header, fmt.Errorf("does not seem to be a WC3 replay (incorrect magic string at start)")
}
headerSize, err := readDWORD(file)
if err != nil {
return header, fmt.Errorf("error reading header size: %w", err)
}
if headerSize != 0x40 && headerSize != 0x44 {
fmt.Printf("Warning: unexpected header size: 0x%x\n", headerSize)
}
header.Length = headerSize
_, err = readDWORD(file)
if err != nil {
return header, fmt.Errorf("error reading compressed file size: %w", err)
}
replayHeaderVersion, err := readDWORD(file)
if err != nil {
return header, fmt.Errorf("error reading compressed file size: %w", err)
}
if replayHeaderVersion > 0x01 {
fmt.Printf("Warning: unexpected replay header version: 0x%x\n", headerSize)
}
_, err = readDWORD(file)
if err != nil {
return header, fmt.Errorf("error reading decompressed data size: %w", err)
}
nBlocks, err := readDWORD(file)
if err != nil {
return header, fmt.Errorf("error reading number of compressed blocks: %w", err)
}
header.NumberOfBlocks = nBlocks
// subheader
header.HeaderVersion = replayHeaderVersion
if replayHeaderVersion == 0x0 {
// This header was used for all replays saved with WarCraft III patch version v1.06 and below.
_, err = readWORD(file)
if err != nil {
return header, fmt.Errorf("error reading unknown field: %w", err)
}
version, err := readWORD(file)
if err != nil {
return header, fmt.Errorf("error reading version number: %w", err)
}
header.GameVersion = int(version)
buildNum, err := readWORD(file)
if err != nil {
return header, fmt.Errorf("error reading build number: %w", err)
}
header.BuildNumber = int(buildNum)
flags, err := readWORD(file)
if err != nil {
return header, fmt.Errorf("error reading flags: %w", err)
}
header.IsMultiplayer = flags == 0x8000
lenMS, err := readDWORD(file)
if err != nil {
return header, fmt.Errorf("error reading replay duration: %w", err)
}
header.Duration = time.Millisecond * time.Duration(lenMS)
_, err = readDWORD(file)
if err != nil {
return header, fmt.Errorf("error reading checksum: %w", err)
}
} else if replayHeaderVersion == 0x1 {
versionId, err := readLittleEndianString(file, 4)
if err != nil {
return header, fmt.Errorf("error reading version identifier: %w", err)
}
if versionId == "WAR3" {
header.Expac = ReignOfChaos
} else if versionId == "W3XP" {
header.Expac = TheFrozenThrone
}
version, err := readDWORD(file)
if err != nil {
return header, fmt.Errorf("error reading version number: %w", err)
}
header.GameVersion = int(version)
buildNum, err := readWORD(file)
if err != nil {
return header, fmt.Errorf("error reading build number: %w", err)
}
header.BuildNumber = int(buildNum)
flags, err := readWORD(file)
if err != nil {
return header, fmt.Errorf("error reading flags: %w", err)
}
header.IsMultiplayer = flags == 0x8000
lenMS, err := readDWORD(file)
if err != nil {
return header, fmt.Errorf("error reading replay duration: %w", err)
}
header.Duration = time.Millisecond * time.Duration(lenMS)
_, err = readDWORD(file)
if err != nil {
return header, fmt.Errorf("error reading checksum: %w", err)
}
} else {
return header, fmt.Errorf("unsupported header version: 0x%x", replayHeaderVersion)
}
return header, nil
}
func readDecompressedData(buffer *bytes.Buffer, rep *Replay) error {
_, err := readDWORD(buffer)
if err != nil {
return fmt.Errorf("error reading unknown field: %w", err)
}
playerRecords := make(map[int]*playerRecord)
// [playerRecord]
if err = expectByte(buffer, 0); err != nil {
return err
}
p, err := readPlayerRecord(buffer, rep)
if err != nil {
return err
}
playerRecords[p.Id] = &p
gameName, err := buffer.ReadString(0) // read null terminated string
if err != nil {
return fmt.Errorf("error reading game name: %w", err)
}
rep.GameOptions.GameName = strings.TrimRight(gameName, "\000")
// skip null byte normally, but this can also be... "hunter2". srsly
if b, err := buffer.ReadByte(); err != nil {
return err
} else if b != 0 {
str, err := buffer.ReadString(0)
if err != nil {
return fmt.Errorf("error reading mystery string: %w", err)
}
// add the byte we removed back to the beginning
if rep.debugMode {
str = strings.TrimRight(string(append([]byte{b}, str...)), "\000")
fmt.Println("mystery string:", str)
}
}
encodedString, err := buffer.ReadString(0) // read null terminated string
if err != nil {
return fmt.Errorf("error reading encoded string: %w", err)
}
if err = readEncodedString(encodedString, rep); err != nil {
return fmt.Errorf("error reading decoded string: %w", err)
}
// [PlayerCount]
_, err = readDWORD(buffer)
if err != nil {
return fmt.Errorf("error reading player count: %w", err)
}
//rep.Slots = make([]Slot, playerCount)
//fmt.Println("playercount", playerCount)
// [GameType]
gameType, err := buffer.ReadByte()
if err != nil {
return fmt.Errorf("error reading game type: %w", err)
}
rep.GameType = GameType(gameType)
privateFlag, err := buffer.ReadByte()
if err != nil {
return fmt.Errorf("error reading private flag: %w", err)
}
//fmt.Printf("private flag: 0x%x\n", privateFlag)
rep.PrivateGame = privateFlag == 0x08 || privateFlag == 0xc8
// TODO: this can also be 0x20 (in reforged public custom game) or 0x40 (reforged matchmaking)
if err = expectWORD(buffer, 0); err != nil {
var unexpectedValueError UnexpectedValueError
if errors.As(err, &unexpectedValueError) {
//fmt.Printf("Unknown byte in 4.7 [GameType] is not 0 but 0x%x!\n", unexpectedValueError.actual)
} else {
//return err
}
}
// this is called LanguageID in the txt file but don't think there's a use for it
_, err = readDWORD(buffer)
if err != nil {
return fmt.Errorf("error reading LanguageID: %w", err)
}
//fmt.Printf("LanguageID (?) = 0x%x\n", unknownMaybeLangId)
// player record, bnet, break on gamestartrecord
for {
recordId, err := buffer.ReadByte()
if err != nil {
return fmt.Errorf("error reading record id: %w", err)
}
if recordId == 0x16 {
// playerRecord
if pRec, err := readPlayerRecord(buffer, rep); err != nil {
return err
} else {
playerRecords[pRec.Id] = &pRec
}
if _, err = readDWORD(buffer); err != nil {
return err
}
} else if recordId == 0x39 {
if rep.debugMode {
fmt.Println("[*] Battle.net 2.0 data present")
}
// TODO: give this var a more... semantic name
after39, err := buffer.ReadByte()
if err != nil {
return fmt.Errorf("error reading value bnet section kind: %w", err)
}
if after39 == 4 || after39 == 5 {
// some sort of bonus data that needs further investigation
// now, online games seem to have this at 0 while LAN ones have 2 sometimes
bonusDataLength, err := readDWORD(buffer)
if err != nil {
return fmt.Errorf("error reading bnet bonus data length: %w", err)
}
// not sure how to use the following data so skip it for now
bonusData := make([]byte, bonusDataLength)
_, err = buffer.Read(bonusData)
if err != nil {
return fmt.Errorf("error reading bonus data: %w", err)
}
} else if after39 == 3 {
lengthOfBnetBlock, err := readDWORD(buffer)
if err != nil {
return fmt.Errorf("error reading bnet2.0 block length: %w", err)
}
// and indeed if we just read the rest of the bnet block, we go straight to GameStartRecord
bnetBlock := make([]byte, lengthOfBnetBlock)
_, err = buffer.Read(bnetBlock)
if err != nil {
return fmt.Errorf("error reading bnet2.0 block: %w", err)
}
if rep.debugMode {
_ = ioutil.WriteFile("hexdumps/bnetBlock.hex", bnetBlock, os.ModePerm)
}
bnetBuffer := bytes.NewBuffer(bnetBlock)
// now we don't know how many account entries are in the block
// but we know the size of the whole thing, so just read it until it's empty
// each iteration reads one account
for bnetBuffer.Len() > 0 {
var acct BattleNet2Account
// peek ahead
// seems like if there's an 0A, there's a sort of "unwrapping" we have to do
// before calling the "inner" function
// BNet games have 0x0A, but Reforged LAN ones don't.
if bnetBuffer.Bytes()[0] == 0x0A {
acct, err = readBattleNetAcct(bnetBuffer)
} else {
acct, err = readBnetAcctInner(bnetBuffer)
}
if err != nil {
return fmt.Errorf("error reading bnet2.0 accounts: %w", err)
}
pRec, ok := playerRecords[acct.PlayerId]
if !ok {
return fmt.Errorf("bnet2.0 account refers to nonexistent playerRecord: %d", acct.PlayerId)
}
pRec.Bnet2Acc = &acct
playerRecords[acct.PlayerId] = pRec
}
} else {
return fmt.Errorf("unexpected byte after 0x39 in bnet section: %#02x", after39)
}
} else if recordId == 0x19 {
break
} else {
fmt.Printf("Not sure how to handle recordId 0x%x\n", recordId)
break
}
}
// GameStartRecord
if _, err := readWORD(buffer); err != nil {
return err
} else {
//fmt.Println(dataBytes, "data bytes")
}
nr, err := buffer.ReadByte()
if err != nil {
return err
} else {
//fmt.Println(nr, "slot records")
}
rep.Slots = make([]Slot, 0, nr)
for slotId := 0; slotId < int(nr); slotId++ {
playerId, err := buffer.ReadByte()
if err != nil {
return err
}
slotRecord := Slot{Id: slotId}
// if playerId == 0, it's a computer and thus won't be in playerRecords
if playerId != 0 {
pRec, ok := playerRecords[int(playerId)]
if !ok {
return fmt.Errorf("slot references invalid player record: id=%d", playerId)
}
pRec.SlotId = slotId
slotRecord.playerId = pRec.Id
}
if mapDownloadPct, err := buffer.ReadByte(); err != nil {
return err
} else {
slotRecord.MapDownloadPercent = mapDownloadPct
if !(mapDownloadPct == 255 || mapDownloadPct == 100) {
return fmt.Errorf("sanity check failed: playerId = %d, map download %% = 0x%x", playerId, mapDownloadPct)
}
}
if slotStatus, err := buffer.ReadByte(); err != nil {
return err
} else {
slotStatus, ok := slotStatuses[slotStatus]
if !ok {
return fmt.Errorf("invalid slot status: 0x%x", slotStatus)
}
slotRecord.SlotStatus = slotStatus
}
if isCPU, err := buffer.ReadByte(); err != nil {
return err
} else {
slotRecord.IsCPU = isCPU == 1
if slotRecord.IsCPU != (playerId == 0) {
//return fmt.Errorf("iff CPU, playerId should be 0 but it was %d", playerId)
}
}
if teamNumber, err := buffer.ReadByte(); err != nil {
return err
} else {
slotRecord.TeamNumber = int(teamNumber) + 1 // inside warcrumb teams are 1 indexed!!!
}
if color, err := buffer.ReadByte(); err != nil {
return err
} else {
slotRecord.Color = colors[color]
}
if playerRace, err := buffer.ReadByte(); err != nil {
return err
} else {
if playerRace&0x40 > 0 {
slotRecord.raceSelectableOrFixed = true
playerRace -= 0x40
}
race, ok := races[playerRace]
if !ok {
return fmt.Errorf("unknown race: 0x%x", playerRace)
} else {
slotRecord.Race = race
}
}
if rep.Version >= 03 {
if aiStrength, err := buffer.ReadByte(); err != nil {
return err
} else {
slotRecord.AIStrength = AIStrength(aiStrength)
/*
if !slotRecord.IsCPU && aiStrength != 0x01 {
return fmt.Errorf("if not CPU, aiStrshould be 0x01 but it was 0x%x", aiStrength)
}
*/
}
}
if rep.Version >= 07 {
if playerHandicap, err := buffer.ReadByte(); err != nil {
return err
} else {
slotRecord.Handicap = int(playerHandicap)
}
}
rep.Slots = append(rep.Slots, slotRecord)
}
rep.Players = make(map[int]*Player)
for id, pRec := range playerRecords {
if id != pRec.Id || rep.Slots[pRec.SlotId].playerId != id {
return fmt.Errorf("id was not set correctly")
}
rep.Players[id] = &Player{
Id: id,
SlotId: pRec.SlotId,
BattleNet: pRec.Bnet2Acc,
Name: pRec.Name,
slot: &rep.Slots[pRec.SlotId],
}
}
for i, slot := range rep.Slots {
player, ok := rep.Players[slot.playerId]
if ok {
rep.Slots[i].Player = player
}
}
// make sure slots and players refer to each other consistently
// note that unoccupied slots refer to player 0 and aren't checked here
for _, p := range rep.Players {
if p.slot.Player.Id != p.Id {
return fmt.Errorf("player %+v and Slot %+v ids aren't consistent", p, p.slot)
}
}
if randomSeed, err := readDWORD(buffer); err != nil {
return fmt.Errorf("error reading random seed: %w", err)
} else {
rep.RandomSeed = randomSeed
}
if selectMode, err := buffer.ReadByte(); err != nil {
return fmt.Errorf("error reading select mode: %w", err)
} else {
rep.selectMode = selectMode
// TODO
/*
0x00 - team & race selectable (for standard custom games)
0x01 - team not selectable
(map setting: fixed alliances in WorldEditor)
0x03 - team & race not selectable
(map setting: fixed player properties in WorldEditor)
0x04 - race fixed to random
(extended map options: random races selected)
0xcc - Automated Match Making (ladder)
*/
}
if startSpotCount, err := buffer.ReadByte(); err != nil {
return fmt.Errorf("error reading start spot count: %w", err)
} else {
rep.startSpotCount = int(startSpotCount)
}
zeroes := 0
currentTimeMS := 0
var leaveUnknown uint32 // "unknown" variable from LeaveGame that we check for increment
numLeaves := 0
saverWon := false // with LeaveGame{0x0C (not last), 0x09}, we know the saver won, but we don't know who they are yet
// ReplayData blocks
for {
blockId, err := buffer.ReadByte()
if err == io.EOF {
break
} else if err != nil {
return fmt.Errorf("error reading block id: %w", err)
}
if rep.Version < 3 && blockId == 0x20 {
// before 1.03, 0x20 was used instead of 0x22
blockId = 0x22
}
switch blockId {
case 0x00:
// normally zeroes signify the end of the replay
// but I want to make sure there aren't zeroes in between blocks
zeroes++
case 0x17: // LeaveGame
numLeaves++
reason, err := readDWORD(buffer)
if err != nil {
return fmt.Errorf("error reading leavegame reason")
}
playerId, err := buffer.ReadByte()
if err != nil {
return fmt.Errorf("error reading leavegame playerId")
}
result, err := readDWORD(buffer)
if err != nil {
return fmt.Errorf("error reading leavegame result")
}
unknown, err := readDWORD(buffer)
if err != nil {
return fmt.Errorf("error reading leavegame unknown val")
}
inc := unknown > leaveUnknown
leaveUnknown = unknown
playerRecords[int(playerId)].leaveTime = currentTimeMS
curPlayer := rep.Players[int(playerId)]
// last leave action is by the saver
if numLeaves == len(rep.Players) {
rep.Saver = curPlayer
if saverWon {
rep.WinnerTeam = rep.Saver.slot.TeamNumber
}
}
// TODO: maybe store all the losers to help deduce the winner
// until then, we only care about win conditions
switch reason {
case 0x01, 0x0E:
switch result {
case 0x09:
rep.WinnerTeam = curPlayer.slot.TeamNumber
}
case 0x0C:
if rep.Saver == nil { // "not last"
switch result {
case 0x09:
saverWon = true
case 0x0A:
rep.WinnerTeam = -1 // draw
}
} else { // last local leave action => curPlayer == rep.Saver
switch result {
case 0x07, 0x0B:
if inc {
rep.WinnerTeam = rep.Saver.slot.TeamNumber
}
case 0x09:
rep.WinnerTeam = rep.Saver.slot.TeamNumber
}
}
}
case 0x1A: //first startblock
if err := expectDWORD(buffer, 0x01); err != nil {
return fmt.Errorf("error reading first startblock: %w", err)
}
case 0x1B: //second startblock
if err := expectDWORD(buffer, 0x01); err != nil {
return fmt.Errorf("error reading second startblock: %w", err)
}
case 0x1C: //third startblock
if err := expectDWORD(buffer, 0x01); err != nil {
return fmt.Errorf("error reading third startblock: %w", err)
}
case 0x1E, 0x1F: // time slot
timeSlotLen, err := readWORD(buffer)
if err != nil {
return fmt.Errorf("error reading timeslot block len: %w", err)
}
if ms, err := readWORD(buffer); err != nil {
return fmt.Errorf("error reading timeslot time increment: %w", err)
} else {
currentTimeMS += int(ms)
}
if timeSlotLen <= 2 {
break
}
commandDataBlock := make([]byte, timeSlotLen-2)
if _, err := buffer.Read(commandDataBlock); err != nil {
return fmt.Errorf("error reading commanddata block: %w", err)
}
commandDataBuf := bytes.NewBuffer(commandDataBlock)
for commandDataBuf.Len() > 0 {
var player *Player
if playerId, err := commandDataBuf.ReadByte(); err != nil {
return fmt.Errorf("error reading CommandData playerId: %w", err)
} else {
player = rep.Players[int(playerId)]
}
actionBlockLen, err := readWORD(commandDataBuf)
if err != nil {
return fmt.Errorf("error reading action block len: %w", err)
}
actionBlockBytes := make([]byte, actionBlockLen)
if _, err := commandDataBuf.Read(actionBlockBytes); err != nil {
return fmt.Errorf("error reading action block: %w", err)
}
actionBlockBuf := bytes.NewBuffer(actionBlockBytes)
for actionBlockBuf.Len() > 0 {
actionable, err := readActionBlock(actionBlockBuf, rep)
if err != nil {
if err == io.EOF {
// FIXME: this is just so we don't crash from unimplemented actions
break
}
return fmt.Errorf("error parsing action block: %w", err)
}
action := Action{
Ability: actionable,
Time: time.Duration(currentTimeMS) * time.Millisecond,
Player: player,
}
if action.Ability != nil {
rep.Actions = append(rep.Actions, action)
}
}
}
case 0x20: //chat message
playerId, err := buffer.ReadByte()
if err != nil {
return fmt.Errorf("error reading chat message playerId: %w", err)
}
_, err = readWORD(buffer)
if err != nil {
return fmt.Errorf("error reading chat message block len: %w", err)
}
flags, err := buffer.ReadByte()
if err != nil {
return fmt.Errorf("error reading chat message block flags: %w", err)
}
var dest MsgDestination
if flags != 0x10 {
chatMode, err := readDWORD(buffer)
if err != nil {
return fmt.Errorf("error reading chat message block mode: %w", err)
}
switch chatMode {
case 0x00:
dest = MsgToEveryone{}
case 0x01:
dest = MsgToAllies{}
case 0x02:
dest = MsgToObservers{}
default:
dest = MsgToPlayer{*rep.Players[int(chatMode)-2].slot}
}
}
msg, err := buffer.ReadString(0)
if err != nil {
return fmt.Errorf("error reading msg text: %w", err)
}
msg = strings.TrimRight(msg, "\000")
timestamp := time.Duration(currentTimeMS) * time.Millisecond
rep.ChatMessages = append(rep.ChatMessages, ChatMessage{
Timestamp: timestamp,
Author: *rep.Players[int(playerId)].slot,
Body: msg,
Destination: dest,
})
case 0x22: //checksum?
n, err := buffer.ReadByte()
if err != nil {
return fmt.Errorf("error reading checksum block len: %w", err)
}
buffer.Next(int(n))
case 0x23: //unknown
buffer.Next(10)
case 0x2F: // forced game end countdown (map is revealed)
mode, err := readDWORD(buffer)
if err != nil {
return fmt.Errorf("error reading game end cd mode: %w", err)
}
countdownSecs, err := readDWORD(buffer)
if err != nil {
return fmt.Errorf("error reading game end cd secs: %w", err)
}
// TODO
if rep.debugMode {
fmt.Printf("countdown mode %x, %d\n", mode, countdownSecs)
}
default:
if rep.debugMode {
fmt.Printf("unknown block id: 0x%X\n", blockId)
}
}
if blockId != 0 && zeroes > 0 {
fmt.Println(zeroes, "zeroes")
zeroes = 0
}
}
return nil
}
func readBattleNetAcct(bnetBuffer *bytes.Buffer) (account BattleNet2Account, err error) {
if err := expectByte(bnetBuffer, 0x0A); err != nil {
return account, err
}
// then, the length of this account entry
bnetAccountBlockLength, err := bnetBuffer.ReadByte()
if err != nil {
return account, fmt.Errorf("error reading block length: %w", err)
}
bnetAccountBlock := make([]byte, bnetAccountBlockLength)
_, err = bnetBuffer.Read(bnetAccountBlock)
if err != nil {
return account, fmt.Errorf("error reading account block: %w", err)
}
bnetAccountBuffer := bytes.NewBuffer(bnetAccountBlock)
return readBnetAcctInner(bnetAccountBuffer)
}
func readBnetAcctInner(bnetAccountBuffer *bytes.Buffer) (account BattleNet2Account, err error) {
for bnetAccountBuffer.Len() > 0 {
sectionByte, err := bnetAccountBuffer.ReadByte()
if err != nil {
return account, fmt.Errorf("error reading account block's section: %w", err)
}
switch sectionByte {
case 0x08: // id of playerRecord
pRecId, err := bnetAccountBuffer.ReadByte()
if err != nil {
return account, fmt.Errorf("error reading account's playerRecord id: %w", err)
}
account.PlayerId = int(pRecId)
break
case 0x12: // username
bnetUsername, err := readLengthAndThenString(bnetAccountBuffer)
if err != nil {
return account, fmt.Errorf("error reading username: %w", err)
}
account.Username = bnetUsername
break
case 0x22: // avatar
avatarName, err := readLengthAndThenString(bnetAccountBuffer)
if err != nil {
return account, fmt.Errorf("error reading avatar: %w", err)
}
account.Avatar = avatarName
break
case 0x1A: // this seems to always just be the string "clan"
clanName, err := readLengthAndThenString(bnetAccountBuffer)
if err != nil {
return account, fmt.Errorf("error reading clan: %w", err)
}
account.Clan = clanName
case 0x28: // no idea what this represents
account.ExtraData, _ = ioutil.ReadAll(bnetAccountBuffer)
default:
fmt.Printf("[***] Unrecognized byte in block: 0x%x\n", sectionByte)
}
}
if account.Avatar == "" {
account.Avatar = "p003" // make avatar peon by default, as that is the ingame default
}
return account, nil
}
// parses the "encoded string" part of the data
func readEncodedString(encodedStr string, rep *Replay) error {
decodedBytes := decodeString(encodedStr)
decoded := bytes.NewBuffer(decodedBytes)
gameSpeedFlag, err := decoded.ReadByte()
if err != nil {
return fmt.Errorf("error reading game speed: %w", err)
}
rep.GameOptions.Speed = GameSpeed(gameSpeedFlag)
byte2, err := decoded.ReadByte()
if err != nil {
return fmt.Errorf("error reading game settings: %w", err)
}
visibilityBits := byte2 & 0b1111
visibility := Visibility(bits.LeadingZeros8(visibilityBits) - 4)
rep.GameOptions.Visibility = visibility
observerBits := (byte2 >> 4) & 0b11
rep.GameOptions.ObserverSetting = ObserverSetting(observerBits)
teamsTogether := ((byte2 >> 6) & 1) == 1
rep.GameOptions.TeamsTogether = teamsTogether
fixedTeamsByte, err := decoded.ReadByte()
if err != nil {
return fmt.Errorf("error reading game settings: %w", err)
}
fixedTeamsByte = fixedTeamsByte >> 1
lockTeams := (fixedTeamsByte & 0b11) == 3
rep.GameOptions.LockTeams = lockTeams
byte3, err := decoded.ReadByte()
if err != nil {
return fmt.Errorf("error reading game settings: %w", err)
}
fullSharedUnitControl := byte3&1 == 1
randomHero := (byte3>>1)&1 == 1
randomRaces := (byte3>>2)&1 == 1
observerReferees := (byte3>>6)&1 == 1
rep.GameOptions.FullSharedUnitControl = fullSharedUnitControl
rep.GameOptions.RandomHero = randomHero
rep.GameOptions.RandomRaces = randomRaces
if observerReferees {
rep.GameOptions.ObserverSetting = ObsReferees
}
_, _ = decoded.Read(make([]byte, 5+4)) //skip unknown bytes & map checksum (4)
mapName, err := decoded.ReadString(0)
if err != nil {
return fmt.Errorf("error reading map name: %w", err)
}
mapName = strings.ReplaceAll(mapName, "\\", "/")
rep.GameOptions.MapName = strings.TrimRight(mapName, "\000")
gameCreatorName, err := decoded.ReadString(0)
if err != nil {
return fmt.Errorf("error reading game creator name: %w", err)
}
rep.GameOptions.CreatorName = strings.TrimRight(gameCreatorName, "\000")
if s, err := decoded.ReadString(0); err != nil {
return err
} else if s != "\000" {
return fmt.Errorf("third decoded string should have been empty: '%s'", s)
}
return nil
}
func readPlayerRecord(buffer *bytes.Buffer, rep *Replay) (playerRecord playerRecord, err error) {
playerId, err := buffer.ReadByte()
if err != nil {
return playerRecord, fmt.Errorf("error reading player id: %w", err)
}
playerRecord.Id = int(playerId)
playerName, err := buffer.ReadString(0) // read null terminated string
if err != nil {
return playerRecord, fmt.Errorf("error reading player name: %w", err)
}
playerRecord.Name = strings.TrimSuffix(playerName, "\000")
additionalDataSize, err := buffer.ReadByte()
if err != nil {
return playerRecord, fmt.Errorf("error reading player additional data size: %w", err)
}
// seems to be 0 in reforged
if additionalDataSize == 0x1 {
// skip null byte
if err = expectByte(buffer, 0); err != nil {
return playerRecord, err
}
} else if additionalDataSize == 0x8 {
// For ladder games only
// TODO: make use of these
// runtime of players Warcraf.exe in milliseconds
if runtimeMS, err := readDWORD(buffer); err != nil {
return playerRecord, fmt.Errorf("error reading player exe runtime: %w", err)
} else {
//fmt.Println(runtimeMS)
playerRecord.RuntimeMS = runtimeMS
}
// player race flags:
if playerRaceFlags, err := readDWORD(buffer); err != nil {
return playerRecord, fmt.Errorf("error reading player race flags: %w", err)
} else {
if rep.debugMode {
fmt.Printf("player race flag: 0x%x\n", playerRaceFlags)
}
playerRecord.RaceFlags = playerRaceFlags
}
} else if additionalDataSize != 0 {
if rep.debugMode {
fmt.Printf("Warning: unrecognized additional data size: 0x%x\n", additionalDataSize)
}
additionalData := make([]byte, additionalDataSize)
_, err = buffer.Read(additionalData)
if rep.debugMode {
fmt.Println("additional data:", additionalData)
}
}
return
}
// internal struct for gathering together data about a player as we move through the file
// playerRecord is the first thing about the player we encounter
// so it's a good starting point to later append bnet and slot stuff to
type playerRecord struct {
Id int
Name string
RuntimeMS uint32
RaceFlags uint32
Bnet2Acc *BattleNet2Account
SlotId int
leaveTime int
}
type header struct {
GameVersion int
HeaderVersion uint32
NumberOfBlocks uint32
Length uint32
BuildNumber int
IsMultiplayer bool
Duration time.Duration
Expac Expac
}