This repository has been archived by the owner on Jan 22, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 9
/
csminify.go
242 lines (194 loc) · 6.85 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
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
// 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"
common "github.com/markus-wa/demoinfocs-golang/v2/pkg/demoinfocs/common"
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 == dem.ErrUnexpectedEndOfDemo {
err = marshal(replay, w)
if err != nil {
return err
}
return dem.ErrUnexpectedEndOfDemo
} else 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, cfg.SnapshotFrequency)
m.replay.Header.MapName = header.MapName
m.tickRate(p.TickRate())
p.RegisterEventHandler(func(events.ConVarsUpdated) {
if tickRate := p.TickRate(); tickRate != 0 {
m.tickRate(tickRate)
}
})
// Register event handlers from collector
for _, h := range cfg.EventCollector.handlers {
m.parser.RegisterEventHandler(h)
}
m.parser.RegisterEventHandler(m.frameDone)
err = p.ParseToEnd()
return m.replay, err
}
type minifier struct {
parser dem.Parser
replay rep.Replay
eventCollector *EventCollector
snapshotFrequency float64
knownPlayerEntityIDs map[int]struct{}
}
func newMinifier(parser dem.Parser, eventCollector *EventCollector, snapshotFrequency float64) minifier {
return minifier{
parser: parser,
eventCollector: eventCollector,
knownPlayerEntityIDs: make(map[int]struct{}),
snapshotFrequency: snapshotFrequency,
}
}
func (m *minifier) frameDone(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(),
Equipment: toEntityEquipment(pl.Weapons()),
Team: int(pl.Team),
}
// 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 (m *minifier) tickRate(rate float64) {
m.replay.Header.TickRate = rate
m.replay.Header.SnapshotRate = int(math.Round(rate / m.snapshotFrequency))
}
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
}
func toEntityEquipment(eq []*common.Equipment) []rep.EntityEquipment {
var equipmentForPlayer = make([]rep.EntityEquipment, 0, len(eq))
for _, equipment := range eq {
equipmentForPlayer = append(equipmentForPlayer, rep.EntityEquipment{
Type: int(equipment.Type),
AmmoInMagazine: equipment.AmmoInMagazine(),
AmmoReserve: equipment.AmmoReserve(),
})
}
return equipmentForPlayer
}