forked from markus-wa/cs-demo-minifier
-
Notifications
You must be signed in to change notification settings - Fork 0
/
csminify.go
203 lines (167 loc) · 5.9 KB
/
csminify.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
// Package csminify provides functions for parsing CS:GO demos and minifying them into various formats.
package csminify
import (
"bufio"
"bytes"
"io"
"math"
r3 "github.com/golang/geo/r3"
dem "github.com/markus-wa/demoinfocs-golang/v2/pkg/demoinfocs"
events "github.com/markus-wa/demoinfocs-golang/v2/pkg/demoinfocs/events"
rep "github.com/markus-wa/cs-demo-minifier/replay"
)
// ReplayMarshaller is the signature for functions that serialize replay.Replay structs to an io.Writer
type ReplayMarshaller func(rep.Replay, io.Writer) error
// Minify wraps MinifyTo with a bytes.Buffer and returns the written bytes.
func Minify(r io.Reader, snapFreq float64, marshal ReplayMarshaller) ([]byte, error) {
return MinifyWithConfig(r, DefaultReplayConfig(snapFreq), marshal)
}
// MinifyWithConfig wraps MinifyToWithConfig with a bytes.Buffer and returns the written bytes.
func MinifyWithConfig(r io.Reader, cfg ReplayConfig, marshal ReplayMarshaller) ([]byte, error) {
var buf bytes.Buffer
err := MinifyToWithConfig(r, cfg, marshal, bufio.NewWriter(&buf))
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// MinifyTo reads a demo from r, creates a replay and marshals it to w.
// See also: ToReplay
func MinifyTo(r io.Reader, snapFreq float64, marshal ReplayMarshaller, w io.Writer) error {
return MinifyToWithConfig(r, DefaultReplayConfig(snapFreq), marshal, w)
}
// MinifyToWithConfig reads a demo from r, creates a replay and marshals it to w.
// See also: ToReplayWithConfig
func MinifyToWithConfig(r io.Reader, cfg ReplayConfig, marshal ReplayMarshaller, w io.Writer) error {
replay, err := ToReplayWithConfig(r, cfg)
if err != nil {
return err
}
err = marshal(replay, w)
return err
}
// DefaultReplayConfig returns the default configuration with a given snapshot frequency.
// May be overridden.
var DefaultReplayConfig = func(snapFreq float64) ReplayConfig {
ec := new(EventCollector)
EventHandlers.Default.RegisterAll(ec)
return ReplayConfig{
SnapshotFrequency: snapFreq,
EventCollector: ec,
}
}
// ReplayConfig contains the configuration for generating a replay.
type ReplayConfig struct {
SnapshotFrequency float64
EventCollector *EventCollector
// TODO: Smoothify flag?
}
// ToReplay reads a demo from r, takes snapshots (snapFreq/sec) and records events into a Replay.
func ToReplay(r io.Reader, snapFreq float64) (rep.Replay, error) {
return ToReplayWithConfig(r, DefaultReplayConfig(snapFreq))
}
// ToReplayWithConfig reads a demo from r, takes snapshots and records events into a Replay with a custom configuration.
func ToReplayWithConfig(r io.Reader, cfg ReplayConfig) (rep.Replay, error) {
// TODO: Provide a way to pass on warnings to the caller
p := dem.NewParser(r)
header, err := p.ParseHeader()
if err != nil {
return rep.Replay{}, err
}
// Make the parser accessible for the custom event handlers
cfg.EventCollector.parser = p
m := newMinifier(p, cfg.EventCollector)
m.replay.Header.MapName = header.MapName
m.replay.Header.TickRate = header.FrameRate()
m.replay.Header.SnapshotRate = int(math.Round(m.replay.Header.TickRate / cfg.SnapshotFrequency))
// Register event handlers from collector
for _, h := range cfg.EventCollector.handlers {
m.parser.RegisterEventHandler(h)
}
m.parser.RegisterEventHandler(m.frameDone)
err = p.ParseToEnd()
if err != nil {
return rep.Replay{}, err
}
return m.replay, nil
}
type minifier struct {
parser dem.Parser
replay rep.Replay
eventCollector *EventCollector
knownPlayerEntityIDs map[int]struct{}
}
func newMinifier(parser dem.Parser, eventCollector *EventCollector) minifier {
return minifier{
parser: parser,
eventCollector: eventCollector,
knownPlayerEntityIDs: make(map[int]struct{}),
}
}
func (m *minifier) frameDone(e events.FrameDone) {
tick := m.parser.CurrentFrame()
// Is it snapshot o'clock?
if tick%m.replay.Header.SnapshotRate == 0 {
// TODO: There might be a better way to do this than having updateKnownPlayers() here
m.updateKnownPlayers()
snap := m.snapshot()
m.replay.Snapshots = append(m.replay.Snapshots, snap)
}
// Did we collect any events in this frame?
if len(m.eventCollector.events) > 0 {
tickEvents := make([]rep.Event, len(m.eventCollector.events))
copy(tickEvents, m.eventCollector.events)
m.replay.Ticks = append(m.replay.Ticks, rep.Tick{
Nr: tick,
Events: tickEvents,
})
// Clear events for next frame
m.eventCollector.events = m.eventCollector.events[:0]
}
}
func (m *minifier) snapshot() rep.Snapshot {
snap := rep.Snapshot{
Tick: m.parser.CurrentFrame(),
}
for _, pl := range m.parser.GameState().Participants().Playing() {
if pl.IsAlive() {
e := rep.EntityUpdate{
EntityID: pl.EntityID,
Hp: pl.Health(),
Armor: pl.Armor(),
FlashDuration: float32(roundTo(float64(pl.FlashDuration), 0.1)), // Round to nearest 0.1 sec - saves space in JSON
Positions: []rep.Point{r3VectorToPoint(pl.Position())},
AngleX: int(pl.ViewDirectionX()),
AngleY: int(pl.ViewDirectionY()),
HasHelmet: pl.HasHelmet(),
HasDefuseKit: pl.HasDefuseKit(),
}
// FIXME: Smoothify Positions
snap.EntityUpdates = append(snap.EntityUpdates, e)
}
}
return snap
}
func (m *minifier) updateKnownPlayers() {
for _, pl := range m.parser.GameState().Participants().All() {
if pl.EntityID != 0 {
if _, alreadyKnown := m.knownPlayerEntityIDs[pl.EntityID]; !alreadyKnown {
ent := rep.Entity{
ID: pl.EntityID,
Team: int(pl.Team),
Name: pl.Name,
IsNpc: pl.IsBot,
}
m.replay.Entities = append(m.replay.Entities, ent)
m.knownPlayerEntityIDs[pl.EntityID] = struct{}{}
}
}
}
}
func r3VectorToPoint(v r3.Vector) rep.Point {
return rep.Point{X: int(v.X), Y: int(v.Y), Z: int(v.Z)}
}
// roundTo wraps math.Round and allows specifying the rounding precision.
func roundTo(x, precision float64) float64 {
return math.Round(x/precision) * precision
}