-
Notifications
You must be signed in to change notification settings - Fork 37
/
client.go
196 lines (163 loc) · 5.38 KB
/
client.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
package dota2
import (
"context"
"errors"
"sync"
"github.com/paralin/go-steam"
"github.com/paralin/go-steam/protocol/gamecoordinator"
"github.com/golang/protobuf/proto"
"github.com/sirupsen/logrus"
devents "github.com/paralin/go-dota2/events"
bgcm "github.com/paralin/go-dota2/protocol"
gcm "github.com/paralin/go-dota2/protocol"
gcsdkm "github.com/paralin/go-dota2/protocol"
gcsm "github.com/paralin/go-dota2/protocol"
"github.com/paralin/go-dota2/socache"
"github.com/paralin/go-dota2/state"
)
// AppID is the ID for dota2
const AppID = 570
// ErrNotReady is returned when the dota client is not ready.
var ErrNotReady = errors.New("the dota client is not ready to accept requests yet, or has just become unready")
// handlerMap is the map of message types to handler functions.
type handlerMap map[uint32]func(packet *gamecoordinator.GCPacket) error
// Dota2 handles the dota game handler.
type Dota2 struct {
le logrus.FieldLogger
client *steam.Client
cache *socache.SOCache
connectionCtxMtx sync.Mutex
connectionCtx context.Context
connectionCtxCancel context.CancelFunc
stateMtx sync.Mutex
state state.Dota2State
handlers handlerMap
pendReqMtx sync.Mutex
pendReqID uint32
pendReq map[uint32]map[uint32]responseHandler
}
// New builds a new Dota2 handler.
func New(client *steam.Client, le logrus.FieldLogger) *Dota2 {
c := &Dota2{
le: le,
cache: socache.NewSOCache(le),
client: client,
pendReq: make(map[uint32]map[uint32]responseHandler),
state: state.Dota2State{
ConnectionStatus: gcsdkm.GCConnectionStatus_GCConnectionStatus_NO_SESSION,
},
}
c.buildHandlerMap()
client.GC.RegisterPacketHandler(c)
return c
}
// GetCache returns the SO Cache.
func (d *Dota2) GetCache() *socache.SOCache {
return d.cache
}
// Close kills any ongoing calls.
func (d *Dota2) Close() {
d.connectionCtxMtx.Lock()
if d.connectionCtxCancel != nil {
d.connectionCtxCancel()
}
d.connectionCtxMtx.Unlock()
}
// buildHandlerMap builds the map of bound handler functions.
func (d *Dota2) buildHandlerMap() {
d.handlers = handlerMap{
// Welcome and conn status
uint32(gcsm.EGCBaseClientMsg_k_EMsgGCClientWelcome): d.handleClientWelcome,
uint32(gcsm.EGCBaseClientMsg_k_EMsgGCClientConnectionStatus): d.handleConnectionStatus,
// Caching
uint32(gcsm.ESOMsg_k_ESOMsg_CacheSubscribed): d.handleCacheSubscribed,
uint32(gcsm.ESOMsg_k_ESOMsg_UpdateMultiple): d.handleCacheUpdateMultiple,
uint32(gcsm.ESOMsg_k_ESOMsg_CacheUnsubscribed): d.handleCacheUnsubscribed,
uint32(gcsm.ESOMsg_k_ESOMsg_Destroy): d.handleCacheDestroy,
// System events
uint32(gcsm.EGCBaseClientMsg_k_EMsgGCPingRequest): d.handlePingRequest,
// Chat events
uint32(gcm.EDOTAGCMsg_k_EMsgGCChatMessage): d.getEventEmitter(func() devents.Event {
return &devents.ChatMessage{}
}),
uint32(gcm.EDOTAGCMsg_k_EMsgGCJoinChatChannelResponse): d.getEventEmitter(func() devents.Event {
return &devents.JoinedChatChannel{}
}),
// Invites
uint32(bgcm.EGCBaseMsg_k_EMsgGCInvitationCreated): d.getEventEmitter(func() devents.Event {
return &devents.InvitationCreated{}
}),
}
d.registerGeneratedHandlers()
}
// write sends a message to the game coordinator.
func (d *Dota2) write(messageType uint32, msg proto.Message) {
d.client.GC.Write(gamecoordinator.NewGCMsgProtobuf(AppID, messageType, msg))
}
// emit emits an event.
func (d *Dota2) emit(event interface{}) {
d.client.Emit(event)
}
// accessState safely accesses the Dota2 state. return true if the state was changed / otherwise updated during the call.
func (d *Dota2) accessState(cb func(nextState *state.Dota2State) (bool, error)) error {
d.stateMtx.Lock()
defer d.stateMtx.Unlock()
lastState := d.state
changed, err := cb(&d.state)
if err != nil {
return err
}
if changed {
d.emit(devents.ClientStateChanged{
OldState: lastState,
NewState: d.state,
})
}
return nil
}
// unmarshalBody attempts to unmarshal a packet body.
func (d *Dota2) unmarshalBody(packet *gamecoordinator.GCPacket, msg proto.Message) (parseErr error) {
defer func() {
if parseErr != nil {
d.le.WithError(parseErr).WithField("msgtype", packet.MsgType).Warn("unable to parse message")
}
}()
return proto.Unmarshal(packet.Body, msg)
}
// HandleGCPacket handles an incoming game coordinator packet.
func (d *Dota2) HandleGCPacket(packet *gamecoordinator.GCPacket) {
if packet.AppId != AppID {
return
}
le := d.le.WithField("msgtype", packet.MsgType)
handler, ok := d.handlers[packet.MsgType]
if ok && handler != nil {
if err := handler(packet); err != nil {
le.WithError(err).Warn("error handling gc msg")
ok = false
}
}
respHandled := d.handleResponsePacket(packet)
if !ok && !respHandled {
le.Debug("unhandled gc packet")
d.emit(&devents.UnhandledGCPacket{
Packet: packet,
})
}
}
// handlePingRequest handles an incoming ping request from the gc.
func (d *Dota2) handlePingRequest(packet *gamecoordinator.GCPacket) error {
d.write(uint32(gcsm.EGCBaseClientMsg_k_EMsgGCPingResponse), &gcsdkm.CMsgGCClientPing{})
return nil
}
// getEventEmitter returns a handler that emits an event, used by the generated code.
func (d *Dota2) getEventEmitter(ctor func() devents.Event) func(packet *gamecoordinator.GCPacket) error {
return func(packet *gamecoordinator.GCPacket) error {
obj := ctor()
if err := d.unmarshalBody(packet, obj.GetEventBody()); err != nil {
return err
}
d.emit(obj)
return nil
}
}