Skip to content

Commit

Permalink
Merge pull request #15 from redraskal/dev
Browse files Browse the repository at this point in the history
  • Loading branch information
redraskal authored Feb 5, 2023
2 parents f122bf8 + d9d850c commit f07b47a
Show file tree
Hide file tree
Showing 25 changed files with 1,335 additions and 397 deletions.
6 changes: 5 additions & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,14 @@ jobs:
goarch: [amd64]
steps:
- uses: actions/checkout@v2
- uses: wangyoucao577/go-release-action@v1.20
- shell: bash
run: |
c=$(git rev-parse --short HEAD); b=$(git name-rev --name-only "$c"); echo -n "version=$c ($b branch)" >> $GITHUB_ENV
- uses: wangyoucao577/go-release-action@v1.35
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }}
goversion: "1.17"
extra_files: LICENSE README.md
ldflags: "-X 'main.Version=${{ env.version }}'"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*.rec
# Dissect output files
/*.json
/*.xlsx
*.bin
# Memory dumps
*.dmp
Expand Down
135 changes: 103 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
[![](https://discordapp.com/api/guilds/936737628756271114/widget.png?style=shield)](https://discord.gg/XdEXWQZZAa)
[![Go Reference](https://pkg.go.dev/badge/github.com/redraskal/r6-dissect.svg)](https://pkg.go.dev/github.com/redraskal/r6-dissect)

Match replay API/CLI for Rainbow Six: Siege's Dissect format.
Match replay API/CLI for Rainbow Six: Siege's Dissect (.rec) format.

This is a work in progress. I will be using this resource in an upcoming project :eyes:

Expand All @@ -11,48 +11,55 @@ The data format is subject to change until a stable version is released.
## Current Features
- Parsing match info (Game version, map, gamemode, match type, teams, players)
- Parsing activities with timestamps (Kills, headshots, objective locates, BattlEye bans, DCs)
- Exporting match info to JSON
- Exporting match stats to Excel
- Exporting round stats to JSON

## Planned Features
- Track plants/disables
- UI alternative
- Track bullet hits/misses
- Track movement packets
- Track other player statistics

### See roadmap at https://github.com/users/redraskal/projects/1/views/1?query=is%3Aopen+sort%3Aupdated-desc.

## CLI Usage
An overview of the file can be printed with the following command:
Print a match overview by specifying a match folder or .rec file:
```
r6-dissect "Match-2022-08-28_23-43-24-133-R01.rec"
r6-dissect Match-2023-01-22_01-28-13-135/
# or
r6-dissect Match-2023-01-22_01-28-13-135-R01.rec
```
```
5:37PM INF Version: Y7S2/7040830
5:37PM INF Recording Player: 1f63af29-7ebe-48e7-b570-e820632d9565
5:37PM INF Match ID: caf4a075-ceb7-406e-ae82-234bef5c00f7
5:37PM INF Timestamp: 2022-08-28 18:45:22 -0500 CDT
5:37PM INF Match Type: RANKED
5:37PM INF Game Mode: BOMB
5:37PM INF Map: KAFE_DOSTOYEVSKY
1:15PM INF Version: Y7S4/7338571
1:15PM INF Recording Player: redraskal [1f63af29-7ebe-48e7-b570-e820632d9565]
1:15PM INF Match ID: 324a1950-a760-4844-a392-1635c5876c0a
1:15PM INF Timestamp: 2023-01-21 19:29:58 -0600 CST
1:15PM INF Match Type: UNRANKED
1:15PM INF Game Mode: BOMB
1:15PM INF Map: CLUB_HOUSE
```
You can also write the match info to a JSON file with one of the following commands:
You can export round stats to a JSON file:
```
r6-dissect "Match-2022-08-28_23-43-24-133-R01.rec" -x kafe.json
r6-dissect "Match-2022-08-28_23-43-24-133-R01.rec" -x json kafe.json
r6-dissect Match-2023-01-22_01-28-13-135-R01.rec -x round.json
```
Example:
```
{
"header": {
"gameVersion": "Y7S2",
"codeVersion": 7040830,
"timestamp": "2022-08-28T23:45:22Z",
"gameVersion": "Y7S4",
"codeVersion": 7338571,
"timestamp": "2023-01-22T01:29:58Z",
"matchType": {
"name": "RANKED",
"id": 2
"name": "UNRANKED",
"id": 12
},
"map": {
"name": "KAFE_DOSTOYEVSKY",
"id": 1378191338
"name": "CLUB_HOUSE",
"id": 837214085
},
"recordingPlayerID": "865512328110930947",
"recordingPlayerID": "10079178519866882138",
"recordingProfileID": "1f63af29-7ebe-48e7-b570-e820632d9565",
"additionalTags": "423855620",
"gamemode": {
"name": "BOMB",
Expand All @@ -62,22 +69,86 @@ r6-dissect "Match-2022-08-28_23-43-24-133-R01.rec" -x json kafe.json
"activityFeed": [
{
"type": "KILL",
"username": "ReithYT",
"target": "Zonalbuzzard",
"headshot": true
},
{
"type": "KILL",
"username": "redraskal",
"target": "Moyete",
"headshot": false
"username": "Eilifint.Ve",
"target": "AnOriginalMango",
"headshot": false,
"time": "2:31",
"timeInSeconds": 151
},
{
"type": "LOCATE_OBJECTIVE",
"username": "exoticindo"
"username": "Eilifint.Ve",
"time": "2:16",
"timeInSeconds": 136
},
...
```
Or the entire match:
```
r6-dissect Match-2023-01-22_01-28-13-135/ -x match.json
```
Export an Excel spreadsheet by swapping .json with .xlsx.
```
r6-dissect Match-2023-01-22_01-28-13-135-R01/ -x match.xlsx
```
See example outputs in [/examples](https://github.com/redraskal/r6-dissect/tree/main/examples).

## Importing a .rec file
```go
package main

import (
"log"
"os"

"github.com/redraskal/r6-dissect/dissect"
)

func main() {
f, err := os.Open("Match-2022-08-28_23-43-24-133-R01.rec")
if err != nil {
log.Fatal(err)
}
defer f.Close()
r, err := dissect.NewReader(f)
if err != nil {
log.Fatal(err)
}
// Use r.ReadPartial() for faster reads with less data (designed to fill in data gaps in the header)
// dissect.Ok(err) returns true if the error only pertains to EOF (read was successful)
if err := r.Read(); !dissect.Ok(err) {
log.Fatal(err)
}
print(r.Header.GameVersion) // Y7S4
}
```

## Exporting match statistics
```go
package main

import (
"log"

"github.com/redraskal/r6-dissect/dissect"
)

func main() {
m, err := dissect.NewMatchReader("MatchReplay/Match-2022-08-28_23-43-24-133/")
if err != nil {
log.Fatal(err)
}
defer m.Close()
// dissect.Ok(err) returns true if the error only pertains to EOF (read was successful)
if err := m.Read(); !dissect.Ok(err) {
log.Fatal(err)
}
// You may also try ExportJSON(path string)
if err := m.Export("match.xlsx"); err != nil {
log.Fatal(err)
}
}
```

#
I would like to thank [draguve](https://github.com/draguve) & other contributors at [draguve/R6-Replays](https://github.com/draguve/R6-Replays) for their additional work on reverse engineering the dissect format.
67 changes: 52 additions & 15 deletions reader/activity.go → dissect/activity.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,37 @@
package reader
package dissect

import (
"bytes"
"encoding/json"
"github.com/rs/zerolog/log"
"strings"
)

"github.com/redraskal/r6-dissect/types"
"github.com/rs/zerolog/log"
type ActivityType int

//go:generate stringer -type=ActivityType
const (
KILL ActivityType = iota
DEATH
PLANT // TODO
DEFUSE // TODO
LOCATE_OBJECTIVE
BATTLEYE
PLAYER_LEAVE
)

var activityIndicator = []byte{0x59, 0x34, 0xe5, 0x8b, 0x04}
type Activity struct {
Type ActivityType `json:"type"`
Username string `json:"username,omitempty"`
Target string `json:"target,omitempty"`
Headshot *bool `json:"headshot,omitempty"`
Time string `json:"time"`
TimeInSeconds int `json:"timeInSeconds"`
}

func (i ActivityType) MarshalJSON() (text []byte, err error) {
return json.Marshal(i.String())
}

var activity2 = []byte{0x00, 0x00, 0x00, 0x22, 0xe3, 0x09, 0x00, 0x79}
var killIndicator = []byte{0x22, 0xd9, 0x13, 0x3c, 0xba}
Expand Down Expand Up @@ -40,9 +63,9 @@ func (r *DissectReader) readActivity() error {
if err != nil {
return err
}
if len(username) == 0 {
empty := len(username) == 0
if empty {
log.Debug().Str("warn", "kill username empty").Send()
return nil
}
// No idea what these 15 bytes mean (kill type?)
_, err = r.read(15)
Expand All @@ -53,8 +76,22 @@ func (r *DissectReader) readActivity() error {
if err != nil {
return err
}
activity := types.Activity{
Type: types.KILL,
if empty && len(target) > 0 {
activity := Activity{
Type: DEATH,
Username: target,
Time: r.timeRaw,
TimeInSeconds: r.time,
}
r.Activities = append(r.Activities, activity)
log.Debug().Interface("activity", activity).Send()
log.Debug().Msg("kill username empty because of death")
return nil
} else if empty {
return nil
}
activity := Activity{
Type: KILL,
Username: username,
Target: target,
Time: r.timeRaw,
Expand All @@ -75,7 +112,7 @@ func (r *DissectReader) readActivity() error {
activity.Headshot = headshotPtr
// Ignore duplicates
for _, val := range r.Activities {
if val.Type == types.KILL && val.Username == activity.Username && val.Target == activity.Target {
if val.Type == KILL && val.Username == activity.Username && val.Target == activity.Target {
return nil
}
}
Expand All @@ -88,25 +125,25 @@ func (r *DissectReader) readActivity() error {
return err
}
activityMessage := string(b)
activityType := types.KILL
activityType := KILL
if strings.HasPrefix(activityMessage, "Friendly Fire") {
return nil
}
if strings.Contains(activityMessage, "bombs") || strings.Contains(activityMessage, "objective") {
activityType = types.LOCATE_OBJECTIVE
activityType = LOCATE_OBJECTIVE
}
if strings.Contains(activityMessage, "BattlEye") {
activityType = types.BATTLEYE
activityType = BATTLEYE
}
if strings.Contains(activityMessage, "left") {
activityType = types.PLAYER_LEAVE
activityType = PLAYER_LEAVE
}
username := strings.Split(activityMessage, " ")[0]
log.Debug().Str("activity_msg", activityMessage).Send()
if activityType == types.KILL {
if activityType == KILL {
return nil
}
activity := types.Activity{
activity := Activity{
Type: activityType,
Username: username,
Target: "",
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions dissect/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package dissect

import (
"errors"
"github.com/klauspost/compress/zstd"
"io"
)

var ErrInvalidFile = errors.New("dissect: not a dissect file")
var ErrInvalidFolder = errors.New("dissect: not a match folder")
var ErrInvalidLength = errors.New("dissect: received an invalid length of bytes")
var ErrInvalidStringSep = errors.New("dissect: invalid string separator")

// Ok returns true if err only pertains to EOF (read was successful).
func Ok(err error) bool {
// zstd.ErrMagicMismatch is expected at EOF because .rec files have extra non-compressed data.
return err == nil || err == io.EOF || err == zstd.ErrMagicMismatch
}
2 changes: 1 addition & 1 deletion types/gamemode_string.go → dissect/gamemode_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit f07b47a

Please sign in to comment.