Repository: markus-wa/demoinfocs-golang
Pull request #203 (2020-04-18 14:55:28)

SteamID Improvements
- converter funcs in `common`
- utility funcs on `Player` and `RankUpdate` for quick access to both 32-bit and 64-bit variants

codecov[bot]: # [Codecov](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/203?src=pr&el=h1) Report
> Merging [#203](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/203?src=pr&el=desc) into [v2](https://codecov.io/gh/markus-wa/demoinfocs-golang/commit/f9a5eeb684e2537ef87f02d49b16cedf5257cdb0&el=desc) will **increase** coverage by `0.05%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/203/graphs/tree.svg?width=650&height=150&src=pr&token=20emCaZRHz)](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/203?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##               v2     #203      +/-   ##
==========================================
+ Coverage   89.58%   89.63%   +0.05%     
==========================================
  Files          29       29              
  Lines        2746     2760      +14     
==========================================
+ Hits         2460     2474      +14     
  Misses        227      227              
  Partials       59       59              
```


| [Impacted Files](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/203?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pkg/demoinfocs/common/common.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/203/diff?src=pr&el=tree#diff-cGtnL2RlbW9pbmZvY3MvY29tbW9uL2NvbW1vbi5nbw==) | `97.01% <100.00%> (+0.71%)` | :arrow_up: |
| [pkg/demoinfocs/common/player.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/203/diff?src=pr&el=tree#diff-cGtnL2RlbW9pbmZvY3MvY29tbW9uL3BsYXllci5nbw==) | `68.80% <100.00%> (+0.50%)` | :arrow_up: |
| [pkg/demoinfocs/datatables.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/203/diff?src=pr&el=tree#diff-cGtnL2RlbW9pbmZvY3MvZGF0YXRhYmxlcy5nbw==) | `98.25% <100.00%> (ø)` | |
| [pkg/demoinfocs/events/events.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/203/diff?src=pr&el=tree#diff-cGtnL2RlbW9pbmZvY3MvZXZlbnRzL2V2ZW50cy5nbw==) | `90.00% <100.00%> (+2.50%)` | :arrow_up: |
| [pkg/demoinfocs/game\_events.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/203/diff?src=pr&el=tree#diff-cGtnL2RlbW9pbmZvY3MvZ2FtZV9ldmVudHMuZ28=) | `94.68% <100.00%> (-0.04%)` | :arrow_down: |
| [pkg/demoinfocs/stringtables.go](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/203/diff?src=pr&el=tree#diff-cGtnL2RlbW9pbmZvY3Mvc3RyaW5ndGFibGVzLmdv) | `87.41% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/203?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/203?src=pr&el=footer). Last update [f9a5eeb...a9410dd](https://codecov.io/gh/markus-wa/demoinfocs-golang/pull/203?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/pkg/demoinfocs/common/common.go b/pkg/demoinfocs/common/common.go
index 4aefdb2..c52b181 100644
--- a/pkg/demoinfocs/common/common.go
+++ b/pkg/demoinfocs/common/common.go
@@ -3,6 +3,8 @@ package common
 
 import (
 	"math/rand"
+	"strconv"
+	"strings"
 	"time"
 
 	"github.com/golang/geo/r3"
@@ -205,3 +207,35 @@ func NewTeamState(team Team, membersCallback func(Team) []*Player) TeamState {
 		membersCallback: membersCallback,
 	}
 }
+
+// ConvertSteamIDTxtTo32 converts a Steam-ID in text format to a 32-bit variant.
+// See https://developer.valvesoftware.com/wiki/SteamID
+func ConvertSteamIDTxtTo32(steamID string) (uint32, error) {
+	arr := strings.Split(steamID, ":")
+
+	Y, err := strconv.ParseUint(arr[1], 10, 32)
+	if err != nil {
+		return 0, err
+	}
+
+	Z, err := strconv.ParseUint(arr[2], 10, 32)
+	if err != nil {
+		return 0, err
+	}
+
+	return uint32((Z << 1) + Y), nil
+}
+
+const steamID64IndividualIdentifier = 0x0110000100000000
+
+// ConvertSteamID32To64 converts a Steam-ID in 32-bit format to a 64-bit variant.
+// See https://developer.valvesoftware.com/wiki/SteamID
+func ConvertSteamID32To64(steamID32 uint32) uint64 {
+	return steamID64IndividualIdentifier + uint64(steamID32)
+}
+
+// ConvertSteamID64To32 converts a Steam-ID in 64-bit format to a 32-bit variant.
+// See https://developer.valvesoftware.com/wiki/SteamID
+func ConvertSteamID64To32(steamID64 uint64) uint32 {
+	return uint32(steamID64 - steamID64IndividualIdentifier)
+}
diff --git a/pkg/demoinfocs/common/player.go b/pkg/demoinfocs/common/player.go
index 24681ae..3fde24f 100644
--- a/pkg/demoinfocs/common/player.go
+++ b/pkg/demoinfocs/common/player.go
@@ -21,7 +21,7 @@ const (
 type Player struct {
 	demoInfoProvider demoInfoProvider // provider for demo info such as tick-rate or current tick
 
-	SteamID           int64              // int64 representation of the User's Steam ID
+	SteamID64         uint64             // 64-bit representation of the user's Steam ID. See https://developer.valvesoftware.com/wiki/SteamID
 	LastAlivePosition r3.Vector          // The location where the player was last alive. Should be equal to Position if the player is still alive.
 	UserID            int                // Mostly used in game-events to address this player
 	Name              string             // Steam / in-game user name
@@ -51,6 +51,12 @@ func (p *Player) String() string {
 	return p.Name
 }
 
+// SteamID32 converts SteamID64 to the 32-bit SteamID variant and returns the result.
+// See https://developer.valvesoftware.com/wiki/SteamID
+func (p *Player) SteamID32() uint32 {
+	return ConvertSteamID64To32(p.SteamID64)
+}
+
 // IsAlive returns true if the Hp of the player are > 0.
 func (p *Player) IsAlive() bool {
 	return p.Health() > 0
diff --git a/pkg/demoinfocs/datatables.go b/pkg/demoinfocs/datatables.go
index 72f39ec..9108f58 100644
--- a/pkg/demoinfocs/datatables.go
+++ b/pkg/demoinfocs/datatables.go
@@ -191,7 +191,7 @@ func (p *parser) getOrCreatePlayer(entityID int, rp *playerInfo) (isNew bool, pl
 
 				player = common.NewPlayer(p.demoInfoProvider)
 				player.Name = rp.name
-				player.SteamID = rp.xuid
+				player.SteamID64 = rp.xuid
 				player.IsBot = rp.isFakePlayer || rp.guid == "BOT"
 				player.UserID = rp.userID
 			}
@@ -284,7 +284,7 @@ func (p *parser) bindNewPlayer(playerEntity st.Entity) {
 		playerEntity.Property("m_bSpottedByMask.001").OnUpdate(spottersChanged)
 	}
 
-	if isNew && pl.SteamID != 0 {
+	if isNew && pl.SteamID64 != 0 {
 		p.eventDispatcher.Dispatch(events.PlayerConnect{Player: pl})
 	}
 }
diff --git a/pkg/demoinfocs/events/events.go b/pkg/demoinfocs/events/events.go
index 055d0c7..0282f09 100644
--- a/pkg/demoinfocs/events/events.go
+++ b/pkg/demoinfocs/events/events.go
@@ -410,13 +410,19 @@ type ChatMessage struct {
 // RankUpdate signals the new rank. Not sure if this
 // only occurs if the rank changed.
 type RankUpdate struct {
-	SteamID32  int32
+	SteamID32  int32 // 32-bit variant of the SteamID. See https://developer.valvesoftware.com/wiki/SteamID
 	RankChange float32
 	RankOld    int
 	RankNew    int
 	WinCount   int
 }
 
+// SteamID64 converts SteamID32 to the 64-bit SteamID variant and returns the result.
+// See https://developer.valvesoftware.com/wiki/SteamID
+func (ru RankUpdate) SteamID64() uint64 {
+	return common.ConvertSteamID32To64(uint32(ru.SteamID32))
+}
+
 // ItemEquip signals an item was equipped.
 // This event is not available in all demos.
 type ItemEquip struct {
diff --git a/pkg/demoinfocs/game_events.go b/pkg/demoinfocs/game_events.go
index 900ecb1..ba71ea8 100644
--- a/pkg/demoinfocs/game_events.go
+++ b/pkg/demoinfocs/game_events.go
@@ -3,8 +3,6 @@ package demoinfocs
 import (
 	"errors"
 	"fmt"
-	"strconv"
-
 	"github.com/golang/geo/r3"
 	"github.com/markus-wa/go-unassert"
 
@@ -427,7 +425,7 @@ func (geh gameEventHandler) playerConnect(data map[string]*msg.CSVCMsg_GameEvent
 	}
 
 	var err error
-	pl.xuid, err = getCommunityID(pl.guid)
+	pl.xuid, err = guidToSteamID64(pl.guid)
 
 	if err != nil {
 		geh.parser.setError(fmt.Errorf("failed to parse player XUID: %v", err.Error()))
@@ -734,24 +732,15 @@ func mapGameEventData(d *msg.CSVCMsg_GameEventListDescriptorT, e *msg.CSVCMsg_Ga
 	return data
 }
 
-// We're all better off not asking questions
-const valveMagicNumber = 76561197960265728
-
-func getCommunityID(guid string) (int64, error) {
+func guidToSteamID64(guid string) (uint64, error) {
 	if guid == "BOT" {
 		return 0, nil
 	}
 
-	authSrv, errSrv := strconv.ParseInt(guid[8:9], 10, 64)
-	if errSrv != nil {
-		return 0, errSrv
-	}
-
-	authID, errID := strconv.ParseInt(guid[10:], 10, 64)
-	if errID != nil {
-		return 0, errID
+	steamID32, err := common.ConvertSteamIDTxtTo32(guid)
+	if err != nil {
+		return 0, err
 	}
 
-	// WTF are we doing here?
-	return valveMagicNumber + authID*2 + authSrv, nil
+	return common.ConvertSteamID32To64(steamID32), nil
 }
diff --git a/pkg/demoinfocs/stringtables.go b/pkg/demoinfocs/stringtables.go
index 940a2e1..af5dd92 100644
--- a/pkg/demoinfocs/stringtables.go
+++ b/pkg/demoinfocs/stringtables.go
@@ -21,7 +21,7 @@ const (
 
 type playerInfo struct {
 	version     int64
-	xuid        int64
+	xuid        uint64
 	name        string
 	userID      int
 	guid        string
@@ -247,7 +247,7 @@ func parsePlayerInfo(reader io.Reader) *playerInfo {
 
 	res := &playerInfo{
 		version:     int64(binary.BigEndian.Uint64(br.ReadBytes(8))),
-		xuid:        int64(binary.BigEndian.Uint64(br.ReadBytes(8))),
+		xuid:        binary.BigEndian.Uint64(br.ReadBytes(8)),
 		name:        br.ReadCString(playerNameMaxLength),
 		userID:      int(int32(binary.BigEndian.Uint32(br.ReadBytes(4)))),
 		guid:        br.ReadCString(guidLength),


Test patch:
diff --git a/pkg/demoinfocs/common/common_test.go b/pkg/demoinfocs/common/common_test.go
index 710a6c9..9aab115 100644
--- a/pkg/demoinfocs/common/common_test.go
+++ b/pkg/demoinfocs/common/common_test.go
@@ -113,6 +113,37 @@ func TestTeamState_MoneySpentTotal(t *testing.T) {
 	assert.Equal(t, 300, state.MoneySpentTotal())
 }
 
+func TestConvertSteamIDTxtTo32(t *testing.T) {
+	id, err := ConvertSteamIDTxtTo32("STEAM_0:1:26343269")
+
+	assert.Nil(t, err)
+	assert.Equal(t, uint32(52686539), id)
+}
+
+func TestConvertSteamIDTxtTo32_Error(t *testing.T) {
+	id, err := ConvertSteamIDTxtTo32("STEAM_0:1:a")
+
+	assert.Equal(t, uint32(0), id)
+	assert.NotNil(t, err)
+
+	id, err = ConvertSteamIDTxtTo32("STEAM_0:b:21643603")
+
+	assert.Equal(t, uint32(0), id)
+	assert.NotNil(t, err)
+}
+
+func TestConvertSteamID32To64(t *testing.T) {
+	id := ConvertSteamID32To64(52686539)
+
+	assert.Equal(t, uint64(76561198012952267), id)
+}
+
+func TestConvertSteamID64To32(t *testing.T) {
+	id := ConvertSteamID64To32(76561198012952267)
+
+	assert.Equal(t, uint32(52686539), id)
+}
+
 type demoInfoProviderMock struct {
 	tickRate             float64
 	ingameTick           int
diff --git a/pkg/demoinfocs/common/player_test.go b/pkg/demoinfocs/common/player_test.go
index a3ea7ed..7e8c744 100644
--- a/pkg/demoinfocs/common/player_test.go
+++ b/pkg/demoinfocs/common/player_test.go
@@ -250,6 +250,12 @@ func TestPlayer_ControlledBot(t *testing.T) {
 	assert.Same(t, dave, pl.ControlledBot())
 }
 
+func TestPlayer_SteamID32(t *testing.T) {
+	pl := &Player{SteamID64: 76561198012952267}
+
+	assert.Equal(t, uint32(52686539), pl.SteamID32())
+}
+
 func newPlayer(tick int) *Player {
 	return NewPlayer(mockDemoInfoProvider(128, tick))
 }
diff --git a/pkg/demoinfocs/events/events_test.go b/pkg/demoinfocs/events/events_test.go
index bfeb7dd..8a95ece 100644
--- a/pkg/demoinfocs/events/events_test.go
+++ b/pkg/demoinfocs/events/events_test.go
@@ -43,6 +43,12 @@ func TestBombEvents(t *testing.T) {
 	}
 }
 
+func TestRankUpdate_SteamID64(t *testing.T) {
+	event := RankUpdate{SteamID32: 52686539}
+
+	assert.Equal(t, uint64(76561198012952267), event.SteamID64())
+}
+
 type demoInfoProviderMock struct {
 }
 
diff --git a/pkg/demoinfocs/game_events_test.go b/pkg/demoinfocs/game_events_test.go
index afb197e..29c63b2 100644
--- a/pkg/demoinfocs/game_events_test.go
+++ b/pkg/demoinfocs/game_events_test.go
@@ -216,24 +216,19 @@ func TestGetEquipmentInstance_Grenade_Thrown(t *testing.T) {
 }
 
 func TestGetCommunityId(t *testing.T) {
-	xuid, err := getCommunityID("abcdefgh1:3")
+	xuid, err := guidToSteamID64("STEAM_0:1:26343269")
 	assert.Nil(t, err)
-	expected := int64(76561197960265728 + 6 + 1)
-	assert.Equal(t, expected, xuid)
+	assert.Equal(t, uint64(76561198012952267), xuid)
 }
 
 func TestGetCommunityId_BOT(t *testing.T) {
-	xuid, err := getCommunityID("BOT")
+	xuid, err := guidToSteamID64("BOT")
 	assert.Zero(t, xuid)
 	assert.Nil(t, err)
 }
 
-func TestGetCommunityId_Errors(t *testing.T) {
-	_, err := getCommunityID("12345678a90123")
+func TestGetCommunityId_Error(t *testing.T) {
+	_, err := guidToSteamID64("STEAM_0:1:abc")
 	assert.NotNil(t, err)
-	assert.Equal(t, "strconv.ParseInt: parsing \"a\": invalid syntax", err.Error())
-
-	_, err = getCommunityID("1234567890abc")
-	assert.NotNil(t, err)
-	assert.Equal(t, "strconv.ParseInt: parsing \"abc\": invalid syntax", err.Error())
+	assert.Equal(t, "strconv.ParseUint: parsing \"abc\": invalid syntax", err.Error())
 }