diff --git a/client.go b/client.go index 0b808cc..37be244 100644 --- a/client.go +++ b/client.go @@ -11,7 +11,6 @@ import ( "github.com/golang/protobuf/proto" devents "github.com/paralin/go-dota2/events" // gcmm "github.com/paralin/go-dota2/protocol/dota_gcmessages_common_match_management" - gcclm "github.com/paralin/go-dota2/protocol/dota_gcmessages_client" gccm "github.com/paralin/go-dota2/protocol/dota_gcmessages_common" gcm "github.com/paralin/go-dota2/protocol/dota_gcmessages_msgid" gcsdkm "github.com/paralin/go-dota2/protocol/gcsdk_gcmessages" @@ -51,7 +50,9 @@ func New(client *steam.Client, le *logrus.Entry) *Dota2 { c := &Dota2{ le: le, client: client, - state: state.Dota2State{ConnectionStatus: gcsdkm.GCConnectionStatus_GCConnectionStatus_NO_SESSION}, + state: state.Dota2State{ + ConnectionStatus: gcsdkm.GCConnectionStatus_GCConnectionStatus_NO_SESSION, + }, profileResponseHandlers: make(map[uint32][]chan<- *gccm.CMsgDOTAProfileCard), } c.buildHandlerMap() @@ -106,137 +107,6 @@ func (d *Dota2) accessState(cb func(nextState *state.Dota2State) (bool, error)) return nil } -// setConnectionStatus sets the connection status, and emits an event. -// NOTE: do not call from inside accessState. -func (d *Dota2) setConnectionStatus( - connStatus gcsdkm.GCConnectionStatus, - update *gcsdkm.CMsgConnectionStatus, -) { - _ = d.accessState(func(ns *state.Dota2State) (changed bool, err error) { - if ns.ConnectionStatus == connStatus { - return false, nil - } - - oldState := ns.ConnectionStatus - d.le.WithField("old", oldState.String()). - WithField("new", connStatus.String()). - Debug("connection status changed") - d.emit(&devents.GCConnectionStatusChanged{ - OldState: oldState, - NewState: connStatus, - Update: update, - }) - - ns.ClearState() // every time the state changes, we lose the lobbies / etc - ns.ConnectionStatus = connStatus - d.connectionCtxMtx.Lock() - if d.connectionCtxCancel != nil { - d.connectionCtxCancel() - d.connectionCtxCancel = nil - d.connectionCtx = nil - } - if connStatus == gcsdkm.GCConnectionStatus_GCConnectionStatus_HAVE_SESSION { - d.connectionCtx, d.connectionCtxCancel = context.WithCancel(context.Background()) - } - d.connectionCtxMtx.Unlock() - return true, nil - }) -} - -// SetPlaying informs Steam we are playing / not playing Dota 2. -func (d *Dota2) SetPlaying(playing bool) { - if playing { - d.client.GC.SetGamesPlayed(AppID) - } else { - d.client.GC.SetGamesPlayed() - _ = d.accessState(func(ns *state.Dota2State) (changed bool, err error) { - ns.ClearState() - return true, nil - }) - } -} - -// SayHello says hello to the Dota2 server, in an attempt to get a session. -func (d *Dota2) SayHello() { - partnerAccType := gcsdkm.PartnerAccountType_PARTNER_NONE - engine := gcsdkm.ESourceEngine_k_ESE_Source2 - var clientSessionNeed uint32 = 104 - d.write(uint32(gcsm.EGCBaseClientMsg_k_EMsgGCClientHello), &gcsdkm.CMsgClientHello{ - ClientLauncher: &partnerAccType, - Engine: &engine, - ClientSessionNeed: &clientSessionNeed, - }) -} - -// RequestProfileCard sends a request to the DOTA gc for a card and waits for the response. -// If you want to enable timeouts, use a context.WithTimeout -func (d *Dota2) RequestProfileCard(ctx context.Context, accountId uint32) (*gccm.CMsgDOTAProfileCard, error) { - cctx, err := d.validateConnectionContext() - if err != nil { - return nil, err - } - - handler := make(chan *gccm.CMsgDOTAProfileCard) - { - d.profileResponseHandlersMtx.Lock() - handlerList := d.profileResponseHandlers[accountId] - handlerList = append(handlerList, handler) - d.profileResponseHandlers[accountId] = handlerList - d.profileResponseHandlersMtx.Unlock() - } - - // When returning, delete the handler from the list - defer func() { - d.profileResponseHandlersMtx.Lock() - defer d.profileResponseHandlersMtx.Unlock() - - handlers := d.profileResponseHandlers[accountId] - for i, h := range handlers { - if h == handler { - handlers[i] = handlers[len(handlers)-1] - handlers = handlers[:len(handlers)-1] - break - } - } - if len(handlers) == 0 { - delete(d.profileResponseHandlers, accountId) - } else { - d.profileResponseHandlers[accountId] = handlers - } - }() - - d.write(uint32(gcm.EDOTAGCMsg_k_EMsgClientToGCGetProfileCard), &gcclm.CMsgClientToGCGetProfileCard{ - AccountId: &accountId, - }) - - select { - case <-ctx.Done(): - return nil, context.Canceled - case <-cctx.Done(): - return nil, NotReadyErr - case r := <-handler: - return r, nil - } -} - -// validateConnectionContext checks if the client is ready or not. -func (d *Dota2) validateConnectionContext() (context.Context, error) { - d.connectionCtxMtx.Lock() - defer d.connectionCtxMtx.Unlock() - - cctx := d.connectionCtx - if cctx == nil { - return nil, NotReadyErr - } - - select { - case <-cctx.Done(): - return nil, NotReadyErr - default: - return cctx, nil - } -} - // unmarshalBody attempts to unmarshal a packet body. func (d *Dota2) unmarshalBody(packet *gamecoordinator.GCPacket, msg proto.Message) (parseErr error) { defer func() { @@ -248,59 +118,6 @@ func (d *Dota2) unmarshalBody(packet *gamecoordinator.GCPacket, msg proto.Messag return proto.Unmarshal(packet.Body, msg) } -// handleClientWelcome handles an incoming client welcome event. -func (d *Dota2) handleClientWelcome(packet *gamecoordinator.GCPacket) error { - welcome := &gcsdkm.CMsgClientWelcome{} - if err := d.unmarshalBody(packet, welcome); err != nil { - return err - } - - d.setConnectionStatus(gcsdkm.GCConnectionStatus_GCConnectionStatus_HAVE_SESSION, nil) - d.emit(&devents.ClientWelcomed{Welcome: welcome}) - return nil -} - -// handleConnectionStatus handles the connection status update event. -func (d *Dota2) handleConnectionStatus(packet *gamecoordinator.GCPacket) error { - stat := &gcsdkm.CMsgConnectionStatus{} - if err := d.unmarshalBody(packet, stat); err != nil { - return err - } - - if stat.Status == nil { - return nil - } - - d.setConnectionStatus(*stat.Status, stat) - return nil -} - -// handleGetProfileCardResponse handles the response to the get profile card request -func (d *Dota2) handleGetProfileCardResponse(packet *gamecoordinator.GCPacket) error { - card := &gccm.CMsgDOTAProfileCard{} - - if err := d.unmarshalBody(packet, card); err != nil { - return err - } - if card.AccountId == nil { - return errors.New("received a profile card response with nil account id") - } - - accountID := *card.AccountId - d.profileResponseHandlersMtx.Lock() - handlers := d.profileResponseHandlers[accountID] - d.profileResponseHandlersMtx.Unlock() - - for _, handler := range handlers { - select { - case handler <- card: - default: - } - } - - return nil -} - // HandleGCPacket handles an incoming game coordinator packet. func (d *Dota2) HandleGCPacket(packet *gamecoordinator.GCPacket) { if packet.AppId != AppID { diff --git a/client_profile.go b/client_profile.go new file mode 100644 index 0000000..78b979d --- /dev/null +++ b/client_profile.go @@ -0,0 +1,89 @@ +package dota2 + +import ( + "context" + "errors" + + "github.com/Philipp15b/go-steam/protocol/gamecoordinator" + // gcmm "github.com/paralin/go-dota2/protocol/dota_gcmessages_common_match_management" + gcclm "github.com/paralin/go-dota2/protocol/dota_gcmessages_client" + gccm "github.com/paralin/go-dota2/protocol/dota_gcmessages_common" + gcm "github.com/paralin/go-dota2/protocol/dota_gcmessages_msgid" +) + +// handleGetProfileCardResponse handles the response to the get profile card request +func (d *Dota2) handleGetProfileCardResponse(packet *gamecoordinator.GCPacket) error { + card := &gccm.CMsgDOTAProfileCard{} + + if err := d.unmarshalBody(packet, card); err != nil { + return err + } + if card.AccountId == nil { + return errors.New("received a profile card response with nil account id") + } + + accountID := *card.AccountId + d.profileResponseHandlersMtx.Lock() + handlers := d.profileResponseHandlers[accountID] + d.profileResponseHandlersMtx.Unlock() + + for _, handler := range handlers { + select { + case handler <- card: + default: + } + } + + return nil +} + +// RequestProfileCard sends a request to the DOTA gc for a card and waits for the response. +// If you want to enable timeouts, use a context.WithTimeout +func (d *Dota2) RequestProfileCard(ctx context.Context, accountId uint32) (*gccm.CMsgDOTAProfileCard, error) { + cctx, err := d.validateConnectionContext() + if err != nil { + return nil, err + } + + handler := make(chan *gccm.CMsgDOTAProfileCard) + { + d.profileResponseHandlersMtx.Lock() + handlerList := d.profileResponseHandlers[accountId] + handlerList = append(handlerList, handler) + d.profileResponseHandlers[accountId] = handlerList + d.profileResponseHandlersMtx.Unlock() + } + + // When returning, delete the handler from the list + defer func() { + d.profileResponseHandlersMtx.Lock() + defer d.profileResponseHandlersMtx.Unlock() + + handlers := d.profileResponseHandlers[accountId] + for i, h := range handlers { + if h == handler { + handlers[i] = handlers[len(handlers)-1] + handlers = handlers[:len(handlers)-1] + break + } + } + if len(handlers) == 0 { + delete(d.profileResponseHandlers, accountId) + } else { + d.profileResponseHandlers[accountId] = handlers + } + }() + + d.write(uint32(gcm.EDOTAGCMsg_k_EMsgClientToGCGetProfileCard), &gcclm.CMsgClientToGCGetProfileCard{ + AccountId: &accountId, + }) + + select { + case <-ctx.Done(): + return nil, context.Canceled + case <-cctx.Done(): + return nil, NotReadyErr + case r := <-handler: + return r, nil + } +} diff --git a/client_session.go b/client_session.go new file mode 100644 index 0000000..583d8ab --- /dev/null +++ b/client_session.go @@ -0,0 +1,119 @@ +package dota2 + +import ( + "context" + + "github.com/Philipp15b/go-steam/protocol/gamecoordinator" + devents "github.com/paralin/go-dota2/events" + // gcmm "github.com/paralin/go-dota2/protocol/dota_gcmessages_common_match_management" + gcsdkm "github.com/paralin/go-dota2/protocol/gcsdk_gcmessages" + gcsm "github.com/paralin/go-dota2/protocol/gcsystemmsgs" + "github.com/paralin/go-dota2/state" +) + +// SetPlaying informs Steam we are playing / not playing Dota 2. +func (d *Dota2) SetPlaying(playing bool) { + if playing { + d.client.GC.SetGamesPlayed(AppID) + } else { + d.client.GC.SetGamesPlayed() + _ = d.accessState(func(ns *state.Dota2State) (changed bool, err error) { + ns.ClearState() + return true, nil + }) + } +} + +// SayHello says hello to the Dota2 server, in an attempt to get a session. +func (d *Dota2) SayHello() { + partnerAccType := gcsdkm.PartnerAccountType_PARTNER_NONE + engine := gcsdkm.ESourceEngine_k_ESE_Source2 + var clientSessionNeed uint32 = 104 + d.write(uint32(gcsm.EGCBaseClientMsg_k_EMsgGCClientHello), &gcsdkm.CMsgClientHello{ + ClientLauncher: &partnerAccType, + Engine: &engine, + ClientSessionNeed: &clientSessionNeed, + }) +} + +// validateConnectionContext checks if the client is ready or not. +func (d *Dota2) validateConnectionContext() (context.Context, error) { + d.connectionCtxMtx.Lock() + defer d.connectionCtxMtx.Unlock() + + cctx := d.connectionCtx + if cctx == nil { + return nil, NotReadyErr + } + + select { + case <-cctx.Done(): + return nil, NotReadyErr + default: + return cctx, nil + } +} + +// handleClientWelcome handles an incoming client welcome event. +func (d *Dota2) handleClientWelcome(packet *gamecoordinator.GCPacket) error { + welcome := &gcsdkm.CMsgClientWelcome{} + if err := d.unmarshalBody(packet, welcome); err != nil { + return err + } + + d.setConnectionStatus(gcsdkm.GCConnectionStatus_GCConnectionStatus_HAVE_SESSION, nil) + d.emit(&devents.ClientWelcomed{Welcome: welcome}) + return nil +} + +// handleConnectionStatus handles the connection status update event. +func (d *Dota2) handleConnectionStatus(packet *gamecoordinator.GCPacket) error { + stat := &gcsdkm.CMsgConnectionStatus{} + if err := d.unmarshalBody(packet, stat); err != nil { + return err + } + + if stat.Status == nil { + return nil + } + + d.setConnectionStatus(*stat.Status, stat) + return nil +} + +// setConnectionStatus sets the connection status, and emits an event. +// NOTE: do not call from inside accessState. +func (d *Dota2) setConnectionStatus( + connStatus gcsdkm.GCConnectionStatus, + update *gcsdkm.CMsgConnectionStatus, +) { + _ = d.accessState(func(ns *state.Dota2State) (changed bool, err error) { + if ns.ConnectionStatus == connStatus { + return false, nil + } + + oldState := ns.ConnectionStatus + d.le.WithField("old", oldState.String()). + WithField("new", connStatus.String()). + Debug("connection status changed") + d.emit(&devents.GCConnectionStatusChanged{ + OldState: oldState, + NewState: connStatus, + Update: update, + }) + + ns.ClearState() // every time the state changes, we lose the lobbies / etc + ns.ConnectionStatus = connStatus + d.connectionCtxMtx.Lock() + if d.connectionCtxCancel != nil { + d.connectionCtxCancel() + d.connectionCtxCancel = nil + d.connectionCtx = nil + } + if connStatus == gcsdkm.GCConnectionStatus_GCConnectionStatus_HAVE_SESSION { + d.connectionCtx, d.connectionCtxCancel = context.WithCancel(context.Background()) + } + d.connectionCtxMtx.Unlock() + return true, nil + }) +}