Repository: markus-wa/demoinfocs-golang
Pull request #417 (2023-09-12 23:36:31)

feat: add player rank properties
- Adds some player properties to retrieve rank information.  
  Values in comments will have to be updated when CS2 is released, and all game modes are available.
- Dispatch `RankUpdate` events

Thanks


Patch:
diff --git a/pkg/demoinfocs/common/player.go b/pkg/demoinfocs/common/player.go
index b2ede42..273cb5e 100644
--- a/pkg/demoinfocs/common/player.go
+++ b/pkg/demoinfocs/common/player.go
@@ -368,6 +368,51 @@ func (p *Player) Armor() int {
 	return getInt(p.Entity, "m_ArmorValue")
 }
 
+// RankType returns the current rank type that the player is playing for.
+// CS:GO values:
+// -1 -> Information not present, the demo is too old
+// 0 -> None/not available
+// 6 -> Classic Competitive
+// 7 -> Wingman 2v2
+// 10 -> Danger zone
+//
+// CS2 values:
+// -1 -> Not available, demo probably not coming from a Valve server
+// 0 -> None?
+// 11 -> Classic Competitive
+func (p *Player) RankType() int {
+	if p.demoInfoProvider.IsSource2() {
+		return getInt(p.Entity, "m_iCompetitiveRankType")
+	}
+
+	// This prop is not available in old demos
+	if prop, exists := p.resourceEntity().PropertyValue("m_iCompetitiveRankType." + p.entityIDStr()); exists {
+		return prop.Int()
+	}
+
+	return -1
+}
+
+// Rank returns the current rank of the player for the current RankType.
+// CS:GO demos -> from 0 to 18 (0 = unranked/unknown, 18 = Global Elite)
+// CS2 demos -> Number representation of the player's rank.
+func (p *Player) Rank() int {
+	if p.demoInfoProvider.IsSource2() {
+		return getInt(p.Entity, "m_iCompetitiveRanking")
+	}
+
+	return getInt(p.resourceEntity(), "m_iCompetitiveRanking."+p.entityIDStr())
+}
+
+// CompetitiveWins returns the amount of competitive wins the player has for the current RankType.
+func (p *Player) CompetitiveWins() int {
+	if p.demoInfoProvider.IsSource2() {
+		return getInt(p.Entity, "m_iCompetitiveWins")
+	}
+
+	return getInt(p.resourceEntity(), "m_iCompetitiveWins."+p.entityIDStr())
+}
+
 // Money returns the amount of money in the player's bank.
 func (p *Player) Money() int {
 	if p.demoInfoProvider.IsSource2() {
diff --git a/pkg/demoinfocs/net_messages.go b/pkg/demoinfocs/net_messages.go
index 1a20506..7dc9def 100644
--- a/pkg/demoinfocs/net_messages.go
+++ b/pkg/demoinfocs/net_messages.go
@@ -5,6 +5,7 @@ import (
 	"encoding/binary"
 	"fmt"
 
+	"github.com/markus-wa/go-unassert"
 	"github.com/markus-wa/ice-cipher-go/pkg/ice"
 	"google.golang.org/protobuf/proto"
 
@@ -108,6 +109,28 @@ func (p *parser) handleServerInfoS2(srvInfo *msgs2.CSVCMsg_ServerInfo) {
 	})
 }
 
+func (p *parser) handleServerRankUpdate(msg *msgs2.CCSUsrMsg_ServerRankUpdate) {
+	for _, v := range msg.RankUpdate {
+		steamID32 := uint32(v.GetAccountId())
+		player, ok := p.gameState.playersBySteamID32[steamID32]
+		if !ok {
+			errMsg := fmt.Sprintf("rank update for unknown player with SteamID32=%d", steamID32)
+
+			p.eventDispatcher.Dispatch(events.ParserWarn{Message: errMsg})
+			unassert.Error(errMsg)
+		}
+
+		p.eventDispatcher.Dispatch(events.RankUpdate{
+			SteamID32:  v.GetAccountId(),
+			RankOld:    int(v.GetRankOld()),
+			RankNew:    int(v.GetRankNew()),
+			WinCount:   int(v.GetNumWins()),
+			RankChange: v.GetRankChange(),
+			Player:     player,
+		})
+	}
+}
+
 func (p *parser) handleEncryptedData(msg *msg.CSVCMsg_EncryptedData) {
 	if msg.GetKeyType() != 2 {
 		return
diff --git a/pkg/demoinfocs/parser.go b/pkg/demoinfocs/parser.go
index 5ca5413..7f545dd 100644
--- a/pkg/demoinfocs/parser.go
+++ b/pkg/demoinfocs/parser.go
@@ -411,6 +411,7 @@ func NewParserWithConfig(demostream io.Reader, config ParserConfig) Parser {
 	p.msgDispatcher.RegisterHandler(p.handleCreateStringTableS2)
 	p.msgDispatcher.RegisterHandler(p.handleUpdateStringTableS2)
 	p.msgDispatcher.RegisterHandler(p.handleSetConVarS2)
+	p.msgDispatcher.RegisterHandler(p.handleServerRankUpdate)
 
 	if config.MsgQueueBufferSize >= 0 {
 		p.initMsgQueue(config.MsgQueueBufferSize)


Test patch:
diff --git a/pkg/demoinfocs/common/player_test.go b/pkg/demoinfocs/common/player_test.go
index d990b70..7541217 100644
--- a/pkg/demoinfocs/common/player_test.go
+++ b/pkg/demoinfocs/common/player_test.go
@@ -561,6 +561,24 @@ func TestPlayer_LastPlaceName(t *testing.T) {
 	assert.Equal(t, "TopofMid", pl.LastPlaceName())
 }
 
+func TestPlayer_RankType(t *testing.T) {
+	pl := playerWithResourceProperty("m_iCompetitiveRankType", st.PropertyValue{IntVal: 6})
+
+	assert.Equal(t, 6, pl.RankType())
+}
+
+func TestPlayer_Rank(t *testing.T) {
+	pl := playerWithResourceProperty("m_iCompetitiveRanking", st.PropertyValue{IntVal: 10})
+
+	assert.Equal(t, 10, pl.Rank())
+}
+
+func TestPlayer_CompetitiveWins(t *testing.T) {
+	pl := playerWithResourceProperty("m_iCompetitiveWins", st.PropertyValue{IntVal: 190})
+
+	assert.Equal(t, 190, pl.CompetitiveWins())
+}
+
 func newPlayer(tick int) *Player {
 	return NewPlayer(mockDemoInfoProvider(128, tick))
 }