Repository: markus-wa/demoinfocs-golang
Pull request #343 (2022-04-25 09:22:13)

make PlayerInfo public


Patch:
diff --git a/pkg/demoinfocs/common/player.go b/pkg/demoinfocs/common/player.go
index baa5557..f690f11 100644
--- a/pkg/demoinfocs/common/player.go
+++ b/pkg/demoinfocs/common/player.go
@@ -208,7 +208,7 @@ func (p *Player) IsUnDuckingInProgress() bool {
 	return p.Flags().Ducking() && !p.Flags().DuckingKeyPressed()
 }
 
-// IsStaning returns true if the player is currently fully standing upright.
+// IsStanding returns true if the player is currently fully standing upright.
 // See also: Flags().Ducking() & Flags().DuckingKeyPressed()
 func (p *Player) IsStanding() bool {
 	return !p.Flags().Ducking() && !p.Flags().DuckingKeyPressed()
@@ -469,3 +469,26 @@ func NewPlayer(demoInfoProvider demoInfoProvider) *Player {
 		demoInfoProvider: demoInfoProvider,
 	}
 }
+
+// PlayerInfo contains information about a player such as their name and SteamID.
+// Primarily intended for internal use.
+type PlayerInfo struct {
+	Version     int64
+	XUID        uint64 // SteamID64
+	Name        string
+	UserID      int
+	GUID        string
+	FriendsID   int
+	FriendsName string
+	// Custom files stuff (CRC)
+	CustomFiles0 int
+	CustomFiles1 int
+	CustomFiles2 int
+	CustomFiles3 int
+	// Amount of downloaded files from the server
+	FilesDownloaded byte
+	// Bots
+	IsFakePlayer bool
+	// HLTV Proxy
+	IsHltv bool
+}
diff --git a/pkg/demoinfocs/datatables.go b/pkg/demoinfocs/datatables.go
index 592639e..fa37af1 100644
--- a/pkg/demoinfocs/datatables.go
+++ b/pkg/demoinfocs/datatables.go
@@ -170,21 +170,21 @@ func (p *parser) bindPlayers() {
 	})
 }
 
-func (p *parser) getOrCreatePlayer(entityID int, rp *playerInfo) (isNew bool, player *common.Player) {
+func (p *parser) getOrCreatePlayer(entityID int, rp *common.PlayerInfo) (isNew bool, player *common.Player) {
 	player = p.gameState.playersByEntityID[entityID]
 
 	if player == nil {
 		if rp != nil {
-			player = p.gameState.playersByUserID[rp.userID]
+			player = p.gameState.playersByUserID[rp.UserID]
 
 			if player == nil {
 				isNew = true
 
 				player = common.NewPlayer(p.demoInfoProvider)
-				player.Name = rp.name
-				player.SteamID64 = rp.xuid
-				player.IsBot = rp.isFakePlayer || rp.guid == "BOT"
-				player.UserID = rp.userID
+				player.Name = rp.Name
+				player.SteamID64 = rp.XUID
+				player.IsBot = rp.IsFakePlayer || rp.GUID == "BOT"
+				player.UserID = rp.UserID
 
 				p.gameState.indexPlayerBySteamID(player)
 			}
@@ -202,7 +202,7 @@ func (p *parser) getOrCreatePlayer(entityID int, rp *playerInfo) (isNew bool, pl
 	p.gameState.playersByEntityID[entityID] = player
 
 	if rp != nil {
-		p.gameState.playersByUserID[rp.userID] = player
+		p.gameState.playersByUserID[rp.UserID] = player
 	}
 
 	return isNew, player
diff --git a/pkg/demoinfocs/debug_on.go b/pkg/demoinfocs/debug_on.go
index a0112c8..8e069df 100644
--- a/pkg/demoinfocs/debug_on.go
+++ b/pkg/demoinfocs/debug_on.go
@@ -1,4 +1,5 @@
-//+build debugdemoinfocs
+//go:build debugdemoinfocs
+// +build debugdemoinfocs
 
 // Functions to print out debug information if the build tag is enabled
 
diff --git a/pkg/demoinfocs/events/events.go b/pkg/demoinfocs/events/events.go
index 3655d37..1feb9d7 100644
--- a/pkg/demoinfocs/events/events.go
+++ b/pkg/demoinfocs/events/events.go
@@ -418,13 +418,18 @@ type PlayerDisconnected struct {
 	Player *common.Player
 }
 
-// PlayerNameChange signals that a player's name has changed
+// PlayerNameChange signals that a player's name has changed.
 type PlayerNameChange struct {
 	Player  *common.Player
 	OldName string
 	NewName string
 }
 
+// StringTablePlayerUpdateApplied signals that a player has been updated via stringtables.
+type StringTablePlayerUpdateApplied struct {
+	Player *common.Player
+}
+
 // SayText signals a chat message. It contains the raw
 // network message data for admin / console messages.
 // EntIdx will probably always be 0
@@ -616,3 +621,9 @@ type ConVarsUpdated struct {
 type RoundImpactScoreData struct {
 	RawMessage *msg.CCSUsrMsg_RoundImpactScoreData
 }
+
+// PlayerInfo signals that basic player information was read via stringtables.
+type PlayerInfo struct {
+	Index int
+	Info  common.PlayerInfo
+}
diff --git a/pkg/demoinfocs/game_events.go b/pkg/demoinfocs/game_events.go
index c19e3c7..8bdc030 100644
--- a/pkg/demoinfocs/game_events.go
+++ b/pkg/demoinfocs/game_events.go
@@ -517,29 +517,29 @@ func (geh gameEventHandler) HostageRescuedAll(map[string]*msg.CSVCMsg_GameEventK
 }
 
 func (geh gameEventHandler) playerConnect(data map[string]*msg.CSVCMsg_GameEventKeyT) {
-	pl := &playerInfo{
-		userID: int(data["userid"].GetValShort()),
-		name:   data["name"].GetValString(),
-		guid:   data["networkid"].GetValString(),
+	pl := common.PlayerInfo{
+		UserID: int(data["userid"].GetValShort()),
+		Name:   data["name"].GetValString(),
+		GUID:   data["networkid"].GetValString(),
 	}
 
-	if pl.guid != "" {
+	if pl.GUID != "" {
 		var err error
-		pl.xuid, err = guidToSteamID64(pl.guid)
+		pl.XUID, err = guidToSteamID64(pl.GUID)
 
 		if err != nil {
 			geh.parser.setError(fmt.Errorf("failed to parse player XUID: %v", err.Error()))
 		}
 	}
 
-	geh.parser.rawPlayers[int(data["index"].GetValByte())] = pl
+	geh.parser.setRawPlayer(int(data["index"].GetValByte()), pl)
 }
 
 func (geh gameEventHandler) playerDisconnect(data map[string]*msg.CSVCMsg_GameEventKeyT) {
 	uid := int(data["userid"].GetValShort())
 
 	for k, v := range geh.parser.rawPlayers {
-		if v.userID == uid {
+		if v.UserID == uid {
 			delete(geh.parser.rawPlayers, k)
 		}
 	}
diff --git a/pkg/demoinfocs/msg/cstrike15_usermessages.pb.go b/pkg/demoinfocs/msg/cstrike15_usermessages.pb.go
index 8056b2f..84c7630 100644
--- a/pkg/demoinfocs/msg/cstrike15_usermessages.pb.go
+++ b/pkg/demoinfocs/msg/cstrike15_usermessages.pb.go
@@ -17433,7 +17433,7 @@ func (m *CCSUsrMsg_SurvivalStats) Unmarshal(dAtA []byte) error {
 		switch fieldNum {
 		case 1:
 			if wireType != 0 {
-				return fmt.Errorf("proto: wrong wireType = %d for field Xuid", wireType)
+				return fmt.Errorf("proto: wrong wireType = %d for field XUID", wireType)
 			}
 			m.Xuid = 0
 			for shift := uint(0); ; shift += 7 {
@@ -17741,7 +17741,7 @@ func (m *CCSUsrMsg_SurvivalStats_Placement) Unmarshal(dAtA []byte) error {
 		switch fieldNum {
 		case 1:
 			if wireType != 0 {
-				return fmt.Errorf("proto: wrong wireType = %d for field Xuid", wireType)
+				return fmt.Errorf("proto: wrong wireType = %d for field XUID", wireType)
 			}
 			m.Xuid = 0
 			for shift := uint(0); ; shift += 7 {
diff --git a/pkg/demoinfocs/parser.go b/pkg/demoinfocs/parser.go
index 08fdec0..cc34b3d 100644
--- a/pkg/demoinfocs/parser.go
+++ b/pkg/demoinfocs/parser.go
@@ -70,7 +70,7 @@ type parser struct {
 	bombsiteA            bombsite
 	bombsiteB            bombsite
 	equipmentMapping     map[*st.ServerClass]common.EquipmentType        // Maps server classes to equipment-types
-	rawPlayers           map[int]*playerInfo                             // Maps entity IDs to 'raw' player info
+	rawPlayers           map[int]*common.PlayerInfo                      // Maps entity IDs to 'raw' player info
 	modelPreCache        []string                                        // Used to find out whether a weapon is a p250 or cz for example (same id)
 	triggers             map[int]*boundingBoxInformation                 // Maps entity IDs to triggers (used for bombsites)
 	gameEventDescs       map[int32]*msg.CSVCMsg_GameEventListDescriptorT // Maps game-event IDs to descriptors
@@ -338,7 +338,7 @@ func NewParserWithConfig(demostream io.Reader, config ParserConfig) Parser {
 	p.bitReader = bit.NewLargeBitReader(demostream)
 	p.stParser = st.NewSendTableParser()
 	p.equipmentMapping = make(map[*st.ServerClass]common.EquipmentType)
-	p.rawPlayers = make(map[int]*playerInfo)
+	p.rawPlayers = make(map[int]*common.PlayerInfo)
 	p.triggers = make(map[int]*boundingBoxInformation)
 	p.demoInfoProvider = demoInfoProvider{parser: &p}
 	p.gameState = newGameState(p.demoInfoProvider)
diff --git a/pkg/demoinfocs/sendtables/entity.go b/pkg/demoinfocs/sendtables/entity.go
index 1dfe4b9..71bafb9 100644
--- a/pkg/demoinfocs/sendtables/entity.go
+++ b/pkg/demoinfocs/sendtables/entity.go
@@ -324,6 +324,11 @@ func (pe *property) Type() PropertyType {
 	return PropertyType(pe.entry.prop.rawType)
 }
 
+// ArrayElementType returns the data type of array entries, if Property.Type() is PropTypeArray.
+func (pe *property) ArrayElementType() PropertyType {
+	return PropertyType(pe.entry.arrayElementProp.rawType)
+}
+
 // PropertyValueType specifies the type of a PropertyValue
 type PropertyValueType int
 
diff --git a/pkg/demoinfocs/sendtables/fake/property.go b/pkg/demoinfocs/sendtables/fake/property.go
index 2882c5a..fe1298e 100644
--- a/pkg/demoinfocs/sendtables/fake/property.go
+++ b/pkg/demoinfocs/sendtables/fake/property.go
@@ -37,3 +37,8 @@ func (p *Property) OnUpdate(handler st.PropertyUpdateHandler) {
 func (p *Property) Bind(variable interface{}, valueType st.PropertyValueType) {
 	p.Called(variable, valueType)
 }
+
+// ArrayElementType is a mock-implementation of Property.ArrayElementType().
+func (p *Property) ArrayElementType() st.PropertyType {
+	return p.Called().Get(0).(st.PropertyType)
+}
diff --git a/pkg/demoinfocs/sendtables/property_interface.go b/pkg/demoinfocs/sendtables/property_interface.go
index 61edcfb..2c9cf83 100644
--- a/pkg/demoinfocs/sendtables/property_interface.go
+++ b/pkg/demoinfocs/sendtables/property_interface.go
@@ -12,6 +12,8 @@ type Property interface {
 	Value() PropertyValue
 	// Type returns the data type of the property.
 	Type() PropertyType
+	// ArrayElementType returns the data type of array entries, if Property.Type() is PropTypeArray.
+	ArrayElementType() PropertyType
 	// OnUpdate registers a handler for updates of the property's value.
 	//
 	// The handler will be called with the current value upon registration.
diff --git a/pkg/demoinfocs/sendtables/sendtables.go b/pkg/demoinfocs/sendtables/sendtables.go
index 105d934..0ef0555 100644
--- a/pkg/demoinfocs/sendtables/sendtables.go
+++ b/pkg/demoinfocs/sendtables/sendtables.go
@@ -95,6 +95,31 @@ func (sc *ServerClass) PropertyEntries() []string {
 	return names
 }
 
+type PropertyEntry struct {
+	Name    string
+	IsArray bool
+	Type    PropertyType
+}
+
+// PropertyEntryDefinitions returns all property-entries on this server-class.
+func (sc *ServerClass) PropertyEntryDefinitions() []PropertyEntry {
+	propEntryCount := len(sc.flattenedProps)
+	res := make([]PropertyEntry, propEntryCount)
+
+	for i := 0; i < propEntryCount; i++ {
+		res[i].Name = sc.flattenedProps[i].name
+		res[i].IsArray = sc.flattenedProps[i].prop.rawType == propTypeArray
+
+		if res[i].IsArray {
+			res[i].Type = PropertyType(sc.flattenedProps[i].arrayElementProp.rawType)
+		} else {
+			res[i].Type = PropertyType(sc.flattenedProps[i].prop.rawType)
+		}
+	}
+
+	return res
+}
+
 func (sc *ServerClass) newEntity(entityDataReader *bit.BitReader, entityID int) *entity {
 	props := make([]property, len(sc.flattenedProps))
 
diff --git a/pkg/demoinfocs/stringtables.go b/pkg/demoinfocs/stringtables.go
index ddb52d1..0797e37 100644
--- a/pkg/demoinfocs/stringtables.go
+++ b/pkg/demoinfocs/stringtables.go
@@ -21,27 +21,6 @@ const (
 	stNameModelPreCache    = "modelprecache"
 )
 
-type playerInfo struct {
-	version     int64
-	xuid        uint64
-	name        string
-	userID      int
-	guid        string
-	friendsID   int
-	friendsName string
-	// Custom files stuff (CRC)
-	customFiles0 int
-	customFiles1 int
-	customFiles2 int
-	customFiles3 int
-	// Amount of downloaded files from the server
-	filesDownloaded byte
-	// Bots
-	isFakePlayer bool
-	// HLTV Proxy
-	isHltv bool
-}
-
 func (p *parser) parseStringTables() {
 	p.bitReader.BeginChunk(p.bitReader.ReadSignedInt(32) << 3)
 
@@ -55,19 +34,19 @@ func (p *parser) parseStringTables() {
 	p.bitReader.EndChunk()
 }
 
-func (p *parser) updatePlayerFromRawIfExists(index int, raw *playerInfo) {
+func (p *parser) updatePlayerFromRawIfExists(index int, raw common.PlayerInfo) {
 	pl := p.gameState.playersByEntityID[index+1]
 	if pl == nil {
 		return
 	}
 
 	oldName := pl.Name
-	newName := raw.name
-	nameChanged := !pl.IsBot && !raw.isFakePlayer && raw.guid != "BOT" && oldName != newName
+	newName := raw.Name
+	nameChanged := !pl.IsBot && !raw.IsFakePlayer && raw.GUID != "BOT" && oldName != newName
 
-	pl.Name = raw.name
-	pl.SteamID64 = raw.xuid
-	pl.IsBot = raw.isFakePlayer
+	pl.Name = raw.Name
+	pl.SteamID64 = raw.XUID
+	pl.IsBot = raw.IsFakePlayer
 
 	p.gameState.indexPlayerBySteamID(pl)
 
@@ -78,6 +57,10 @@ func (p *parser) updatePlayerFromRawIfExists(index int, raw *playerInfo) {
 			NewName: newName,
 		})
 	}
+
+	p.eventDispatcher.Dispatch(events.StringTablePlayerUpdateApplied{
+		Player: pl,
+	})
 }
 
 func (p *parser) parseSingleStringTable(name string) {
@@ -103,9 +86,7 @@ func (p *parser) parseSingleStringTable(name string) {
 					panic(errors.Wrap(err, "couldn't parse playerIndex from string"))
 				}
 
-				p.rawPlayers[int(playerIndex)] = player
-
-				p.updatePlayerFromRawIfExists(playerIndex, player)
+				p.setRawPlayer(int(playerIndex), player)
 
 			case stNameInstanceBaseline:
 				classID, err := strconv.Atoi(stringName)
@@ -135,6 +116,16 @@ func (p *parser) parseSingleStringTable(name string) {
 		}
 	}
 }
+func (p *parser) setRawPlayer(index int, player common.PlayerInfo) {
+	p.rawPlayers[index] = &player
+
+	p.updatePlayerFromRawIfExists(index, player)
+
+	p.eventDispatcher.Dispatch(events.PlayerInfo{
+		Index: index,
+		Info:  player,
+	})
+}
 
 func (p *parser) handleUpdateStringTable(tab *msg.CSVCMsg_UpdateStringTable) {
 	defer func() {
@@ -247,9 +238,8 @@ func (p *parser) processStringTable(tab *msg.CSVCMsg_CreateStringTable) {
 		switch tab.Name {
 		case stNameUserInfo:
 			player := parsePlayerInfo(bytes.NewReader(userdata))
-			p.rawPlayers[entryIndex] = player
 
-			p.updatePlayerFromRawIfExists(entryIndex, player)
+			p.setRawPlayer(entryIndex, player)
 
 		case stNameInstanceBaseline:
 			classID, err := strconv.Atoi(entry)
@@ -271,7 +261,7 @@ func (p *parser) processStringTable(tab *msg.CSVCMsg_CreateStringTable) {
 	p.poolBitReader(br)
 }
 
-func parsePlayerInfo(reader io.Reader) *playerInfo {
+func parsePlayerInfo(reader io.Reader) common.PlayerInfo {
 	br := bit.NewSmallBitReader(reader)
 
 	const (
@@ -279,24 +269,24 @@ func parsePlayerInfo(reader io.Reader) *playerInfo {
 		guidLength          = 33
 	)
 
-	res := &playerInfo{
-		version:     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),
-		friendsID:   int(int32(binary.BigEndian.Uint32(br.ReadBytes(4)))),
-		friendsName: br.ReadCString(playerNameMaxLength),
+	res := common.PlayerInfo{
+		Version:     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),
+		FriendsID:   int(int32(binary.BigEndian.Uint32(br.ReadBytes(4)))),
+		FriendsName: br.ReadCString(playerNameMaxLength),
 
-		isFakePlayer: br.ReadSingleByte() != 0,
-		isHltv:       br.ReadSingleByte() != 0,
+		IsFakePlayer: br.ReadSingleByte() != 0,
+		IsHltv:       br.ReadSingleByte() != 0,
 
-		customFiles0: int(br.ReadInt(32)),
-		customFiles1: int(br.ReadInt(32)),
-		customFiles2: int(br.ReadInt(32)),
-		customFiles3: int(br.ReadInt(32)),
+		CustomFiles0: int(br.ReadInt(32)),
+		CustomFiles1: int(br.ReadInt(32)),
+		CustomFiles2: int(br.ReadInt(32)),
+		CustomFiles3: int(br.ReadInt(32)),
 
-		filesDownloaded: br.ReadSingleByte(),
+		FilesDownloaded: br.ReadSingleByte(),
 	}
 
 	br.Pool()


Test patch:
diff --git a/pkg/demoinfocs/datatables_test.go b/pkg/demoinfocs/datatables_test.go
index b9b8e73..69d6bf7 100644
--- a/pkg/demoinfocs/datatables_test.go
+++ b/pkg/demoinfocs/datatables_test.go
@@ -22,16 +22,16 @@ func (DevNullReader) Read(p []byte) (n int, err error) {
 func TestParser_BindNewPlayer_Issue98(t *testing.T) {
 	p := newParser()
 
-	p.rawPlayers = map[int]*playerInfo{
+	p.rawPlayers = map[int]*common.PlayerInfo{
 		0: {
-			userID: 1,
-			name:   "Zim",
-			guid:   "BOT",
+			UserID: 1,
+			Name:   "Zim",
+			GUID:   "BOT",
 		},
 		1: {
-			userID: 2,
-			name:   "The Suspect",
-			guid:   "123",
+			UserID: 2,
+			Name:   "The Suspect",
+			GUID:   "123",
 		},
 	}
 
@@ -48,12 +48,12 @@ func TestParser_BindNewPlayer_Issue98(t *testing.T) {
 func TestParser_BindNewPlayer_Issue98_Reconnect(t *testing.T) {
 	p := newParser()
 
-	p.rawPlayers = map[int]*playerInfo{
+	p.rawPlayers = map[int]*common.PlayerInfo{
 		0: {
-			userID: 2,
-			name:   "The Suspect",
-			guid:   "123",
-			xuid:   1,
+			UserID: 2,
+			Name:   "The Suspect",
+			GUID:   "123",
+			XUID:   1,
 		},
 	}
 
@@ -80,12 +80,12 @@ func TestParser_BindNewPlayer_PlayerSpotted_Over32(t *testing.T) {
 func testPlayerSpotted(t *testing.T, propName string) {
 	p := newParser()
 
-	p.rawPlayers = map[int]*playerInfo{
+	p.rawPlayers = map[int]*common.PlayerInfo{
 		0: {
-			userID: 2,
-			name:   "Spotter",
-			guid:   "123",
-			xuid:   1,
+			UserID: 2,
+			Name:   "Spotter",
+			GUID:   "123",
+			XUID:   1,
 		},
 	}