Skip to content

Commit

Permalink
feat: implement cache, chat, lobby, party (incomplete)
Browse files Browse the repository at this point in the history
  • Loading branch information
paralin committed Jan 28, 2018
1 parent 8714043 commit 6d99899
Show file tree
Hide file tree
Showing 20 changed files with 6,302 additions and 329 deletions.
38 changes: 33 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,44 @@ Currently, the following client features have been implemented in this library:

- [x] GC session state management
- [x] Player profile fetching / call tracking
- [ ] SOCache tracking / state management
- [ ] Lobby tracking / state management
- [ ] Read lobby state correctly
- [x] SOCache tracking / state management
- [x] Basic chat interaction
- [~] Lobby tracking / state management
- [x] Read lobby state correctly
- [ ] Implement normal lobby operations
- [ ] Party tracking / state management
- [ ] Read party and invite state correctly
- [~] Party tracking / state management
- [x] Read party and invite state correctly
- [ ] Implement normal party operations

... and others. This is the current short-term roadmap.

## SOCache Mechanism

The caching mechanism makes it easy to watch for changes to common objects, like `Lobby, LobbyInvite, Party, PartyInvite`.

This mechanism is used everywhere, these objects are not exposed in their own events.

```go
import (
gcmm "github.com/paralin/go-dota2/protocol/dota_gcmessages_common_match_management"
"github.com/paralin/go-dota2/cso"
)

eventCh, eventCancel, err := dota.GetCache().SubscribeType(cso.Lobby)
if err != nil {
return err
}

defer eventCancel()

lobbyEvent := <-eventCh
lobby := lobbyEvent.Object.(*gcmm.CSODOTALobby)
```

Events for the object type are emitted on the eventCh. Be sure to call `eventCancel` once you are done with the channel to prevent resource leaks.

The cache object also adds interfaces to get and list the current objects in the cache.

## go-steam Dependency

This library depends on `go-steam`. Currently we are using the [FACEIT fork](https://github.com/faceit/go-steam).
Expand Down
23 changes: 23 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
gcm "github.com/paralin/go-dota2/protocol/dota_gcmessages_msgid"
gcsdkm "github.com/paralin/go-dota2/protocol/gcsdk_gcmessages"
gcsm "github.com/paralin/go-dota2/protocol/gcsystemmsgs"
"github.com/paralin/go-dota2/socache"
"github.com/paralin/go-dota2/state"
)

Expand All @@ -31,6 +32,7 @@ type handlerMap map[uint32]func(packet *gamecoordinator.GCPacket) error
type Dota2 struct {
le *logrus.Entry
client *steam.Client
cache *socache.SOCache

connectionCtxMtx sync.Mutex
connectionCtx context.Context
Expand All @@ -43,12 +45,15 @@ type Dota2 struct {

profileResponseHandlersMtx sync.Mutex
profileResponseHandlers map[uint32][]chan<- *gccm.CMsgDOTAProfileCard

joinChatChannelHandlers sync.Map // map[string]chan *gcmcc.CMsgDOTAJoinChatChannelResponse
}

// New builds a new Dota2 handler.
func New(client *steam.Client, le *logrus.Entry) *Dota2 {
c := &Dota2{
le: le,
cache: socache.NewSOCache(le),
client: client,
state: state.Dota2State{
ConnectionStatus: gcsdkm.GCConnectionStatus_GCConnectionStatus_NO_SESSION,
Expand All @@ -60,6 +65,11 @@ func New(client *steam.Client, le *logrus.Entry) *Dota2 {
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()
Expand All @@ -75,6 +85,14 @@ func (d *Dota2) buildHandlerMap() {
uint32(gcsm.EGCBaseClientMsg_k_EMsgGCClientWelcome): d.handleClientWelcome,
uint32(gcsm.EGCBaseClientMsg_k_EMsgGCClientConnectionStatus): d.handleConnectionStatus,
uint32(gcm.EDOTAGCMsg_k_EMsgClientToGCGetProfileCardResponse): d.handleGetProfileCardResponse,
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,
uint32(gcm.EDOTAGCMsg_k_EMsgGCJoinChatChannelResponse): d.handleJoinChatChannelResponse,
uint32(gcm.EDOTAGCMsg_k_EMsgGCChatMessage): d.handleChatMessage,
uint32(gcm.EDOTAGCMsg_k_EMsgGCOtherJoinedChannel): d.handleJoinedChannel,
uint32(gcm.EDOTAGCMsg_k_EMsgGCOtherLeftChannel): d.handleLeftChannel,
}
}

Expand Down Expand Up @@ -138,3 +156,8 @@ func (d *Dota2) HandleGCPacket(packet *gamecoordinator.GCPacket) {
le.WithError(err).Warn("error handling gc msg")
}
}

// Pong responds to a Ping.
func (d *Dota2) Pong() {
d.write(uint32(gcsm.EGCBaseClientMsg_k_EMsgGCPingResponse), &gcsdkm.CMsgGCClientPing{})
}
70 changes: 70 additions & 0 deletions client_cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package dota2

import (
"github.com/Philipp15b/go-steam/protocol/gamecoordinator"
gcsdkm "github.com/paralin/go-dota2/protocol/gcsdk_gcmessages"
gcsm "github.com/paralin/go-dota2/protocol/gcsystemmsgs"
)

// RequestCacheSubscriptionRefresh requests a subscription refresh for a specific cache ID.
func (d *Dota2) RequestCacheSubscriptionRefresh(ownerSoid *gcsdkm.CMsgSOIDOwner) {
d.write(uint32(gcsm.ESOMsg_k_ESOMsg_CacheSubscriptionRefresh), &gcsdkm.CMsgSOCacheSubscriptionRefresh{
OwnerSoid: ownerSoid,
})
}

// handleCacheSubscribed handles a CacheSubscribed packet.
func (d *Dota2) handleCacheSubscribed(packet *gamecoordinator.GCPacket) error {
sub := &gcsdkm.CMsgSOCacheSubscribed{}
if err := d.unmarshalBody(packet, sub); err != nil {
return err
}

if err := d.cache.HandleSubscribed(sub); err != nil {
d.le.WithError(err).Debug("unhandled cache issue (ignore)")
}

return nil
}

// handleCacheUnsubscribed handles a CacheUnsubscribed packet.
func (d *Dota2) handleCacheUnsubscribed(packet *gamecoordinator.GCPacket) error {
sub := &gcsdkm.CMsgSOCacheUnsubscribed{}
if err := d.unmarshalBody(packet, sub); err != nil {
return err
}

if err := d.cache.HandleUnsubscribed(sub); err != nil {
d.le.WithError(err).Debug("unhandled cache issue (ignore)")
}

return nil
}

// handleCacheUpdateMultiple handles when one or more object(s) in a cache is/are updated.
func (d *Dota2) handleCacheUpdateMultiple(packet *gamecoordinator.GCPacket) error {
sub := &gcsdkm.CMsgSOMultipleObjects{}
if err := d.unmarshalBody(packet, sub); err != nil {
return err
}

if err := d.cache.HandleUpdateMultiple(sub); err != nil {
d.le.WithError(err).Debug("unhandled cache issue (ignore)")
}

return nil
}

// handleCacheDestroy handles when an object in a cache is destroyed.
func (d *Dota2) handleCacheDestroy(packet *gamecoordinator.GCPacket) error {
sub := &gcsdkm.CMsgSOSingleObject{}
if err := d.unmarshalBody(packet, sub); err != nil {
return err
}

if err := d.cache.HandleDestroy(sub); err != nil {
d.le.WithError(err).Debug("unhandled cache issue (ignore)")
}

return nil
}
96 changes: 96 additions & 0 deletions client_chat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package dota2

import (
"context"

"github.com/Philipp15b/go-steam/protocol/gamecoordinator"
"github.com/paralin/go-dota2/events"
gcmcc "github.com/paralin/go-dota2/protocol/dota_gcmessages_client_chat"
gcm "github.com/paralin/go-dota2/protocol/dota_gcmessages_msgid"
"github.com/paralin/go-dota2/protocol/dota_shared_enums"
)

// SendChannelMessage attempts to send a message in a channel.
func (d *Dota2) SendChannelMessage(channelID uint64, message string) {
d.write(uint32(gcm.EDOTAGCMsg_k_EMsgGCChatMessage), &gcmcc.CMsgDOTAChatMessage{
ChannelId: &channelID,
Text: &message,
})

}

// SendChannelMessageEx attempts to send a message in a channel with explicit details.
// At minimum ChannelId and Text must be set.
func (d *Dota2) SendChannelMessageEx(channelID uint64, msg *gcmcc.CMsgDOTAChatMessage) {
d.write(uint32(gcm.EDOTAGCMsg_k_EMsgGCChatMessage), msg)
}

// JoinChatChannel attempts to join a chat channel by name and type.
func (d *Dota2) JoinChatChannel(ctx context.Context, name string, channelType dota_shared_enums.DOTAChatChannelTypeT) (*gcmcc.CMsgDOTAJoinChatChannelResponse, error) {
d.write(uint32(gcm.EDOTAGCMsg_k_EMsgGCJoinChatChannel), &gcmcc.CMsgDOTAJoinChatChannel{
ChannelName: &name,
ChannelType: &channelType,
})

defer d.joinChatChannelHandlers.Delete(name)
ch := make(chan *gcmcc.CMsgDOTAJoinChatChannelResponse, 1)
d.joinChatChannelHandlers.Store(name, ch)
select {
case <-ctx.Done():
return nil, ctx.Err()
case v := <-ch:
return v, nil
}
}

// handleJoinChatChannelResponse handles the response to the join chat channel request
func (d *Dota2) handleJoinChatChannelResponse(packet *gamecoordinator.GCPacket) error {
resp := &gcmcc.CMsgDOTAJoinChatChannelResponse{}
if err := d.unmarshalBody(packet, resp); err != nil {
return err
}

chanName := resp.GetChannelName()
reqCh, ok := d.joinChatChannelHandlers.Load(chanName)
if ok {
reqCh.(chan *gcmcc.CMsgDOTAJoinChatChannelResponse) <- resp
}

return nil
}

// handleChatMessage handles an incoming chat message.
func (d *Dota2) handleChatMessage(packet *gamecoordinator.GCPacket) error {
eve := &events.ChatMessage{}
resp := &eve.CMsgDOTAChatMessage
if err := d.unmarshalBody(packet, resp); err != nil {
return err
}

d.emit(eve)
return nil
}

// handleLeftChannel handles a channel leave event.
func (d *Dota2) handleLeftChannel(packet *gamecoordinator.GCPacket) error {
eve := &events.LeftChatChannel{}
resp := &eve.CMsgDOTAOtherLeftChatChannel
if err := d.unmarshalBody(packet, resp); err != nil {
return err
}

d.emit(eve)
return nil
}

// handleJoinedChannel handles a channel join event.
func (d *Dota2) handleJoinedChannel(packet *gamecoordinator.GCPacket) error {
eve := &events.JoinedChatChannel{}
resp := &eve.CMsgDOTAOtherJoinedChatChannel
if err := d.unmarshalBody(packet, resp); err != nil {
return err
}

d.emit(eve)
return nil
}
79 changes: 79 additions & 0 deletions client_lobby.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package dota2

import (
"context"

"github.com/paralin/go-dota2/cso"
gccm "github.com/paralin/go-dota2/protocol/dota_gcmessages_client"
gcccm "github.com/paralin/go-dota2/protocol/dota_gcmessages_client_match_management"
gcmm "github.com/paralin/go-dota2/protocol/dota_gcmessages_common_match_management"
gcm "github.com/paralin/go-dota2/protocol/dota_gcmessages_msgid"
)

// JoinLobby attempts to join a lobby by ID.
func (d *Dota2) JoinLobby(lobbyID uint64, passKey string) {
d.write(uint32(gcm.EDOTAGCMsg_k_EMsgGCPracticeLobbyJoin), &gcccm.CMsgPracticeLobbyJoin{
LobbyId: &lobbyID,
PassKey: &passKey,
})
}

// CreateLobby attempts to create a lobby with details.
func (d *Dota2) CreateLobby(details *gcccm.CMsgPracticeLobbySetDetails) {
// TODO: investigate SearchKey
d.write(uint32(gcm.EDOTAGCMsg_k_EMsgGCPracticeLobbyCreate), &gcccm.CMsgPracticeLobbyCreate{
PassKey: details.PassKey,
LobbyDetails: details,
})
}

// LeaveCreateLobby attempts to leave any current lobby and creates a new one.
func (d *Dota2) LeaveCreateLobby(ctx context.Context, details *gcccm.CMsgPracticeLobbySetDetails) error {
cacheCtr, err := d.cache.GetContainerForTypeID(uint32(cso.Lobby))
if err != nil {
return err
}

eventCh, eventCancel, err := cacheCtr.Subscribe()
if err != nil {
return err
}
defer eventCancel()

var wasInNoLobby bool
for {
lobbyObj := cacheCtr.GetOne()
if lobbyObj != nil {
lob := lobbyObj.(*gcmm.CSODOTALobby)
le := d.le.WithField("lobby-id", lob.GetLobbyId())
if wasInNoLobby {
le.Debug("successfully created lobby")
return nil
}

le.Debug("attempting to leave lobby")
d.LeaveLobby()
} else {
wasInNoLobby = true
d.le.Debug("creating lobby")
d.CreateLobby(details)
}

select {
case <-ctx.Done():
return ctx.Err()
case event := <-eventCh:
_ = event
}
}
}

// LeaveLobby attempts to leave the current lobby.
func (d *Dota2) LeaveLobby() {
d.write(uint32(gcm.EDOTAGCMsg_k_EMsgGCPracticeLobbyLeave), &gcccm.CMsgPracticeLobbyLeave{})
}

// DestroyLobby attempts to destroy the lobby.
func (d *Dota2) DestroyLobby() {
d.write(uint32(gcm.EDOTAGCMsg_k_EMsgDestroyLobbyRequest), &gccm.CMsgDOTADestroyLobbyRequest{})
}
10 changes: 10 additions & 0 deletions client_party.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package dota2

import (
bgcm "github.com/paralin/go-dota2/protocol/base_gcmessages"
)

// LeaveParty attempts to leave the current party.
func (d *Dota2) LeaveParty() {
d.write(uint32(bgcm.EGCBaseMsg_k_EMsgGCLeaveParty), &bgcm.CMsgLeaveParty{})
}
Loading

0 comments on commit 6d99899

Please sign in to comment.