Skip to content

Commit 6fe1a03

Browse files
authored
Give some love to hostage maps (#269)
1 parent 67a3595 commit 6fe1a03

File tree

10 files changed

+218
-0
lines changed

10 files changed

+218
-0
lines changed

.golangci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ linters:
1818
- exhaustivestruct
1919
- godot
2020
- gofumpt
21+
- testpackage
2122
issues:
2223
exclude-rules:
2324
# Exclude some linters from running on tests files.

pkg/demoinfocs/common/hostage.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package common
2+
3+
import (
4+
"github.com/golang/geo/r3"
5+
6+
st "github.com/markus-wa/demoinfocs-golang/v2/pkg/demoinfocs/sendtables"
7+
)
8+
9+
// HostageState is the type for the various HostageStateXYZ constants.
10+
type HostageState byte
11+
12+
// HostageState constants give information about hostages state.
13+
// e.g. being untied, picked up, rescued etc.
14+
const (
15+
HostageStateIdle HostageState = 0
16+
HostageStateBeingUntied HostageState = 1
17+
HostageStateGettingPickedUp HostageState = 2
18+
HostageStateBeingCarried HostageState = 3
19+
HostageStateFollowingPlayer HostageState = 4
20+
HostageStateGettingDropped HostageState = 5
21+
HostageStateRescued HostageState = 6
22+
HostageStateDead HostageState = 7
23+
)
24+
25+
// Hostage represents a hostage.
26+
type Hostage struct {
27+
Entity st.Entity
28+
demoInfoProvider demoInfoProvider
29+
}
30+
31+
// Position returns the current position of the hostage.
32+
func (hostage *Hostage) Position() r3.Vector {
33+
if hostage.Entity == nil {
34+
return r3.Vector{}
35+
}
36+
37+
return hostage.Entity.Position()
38+
}
39+
40+
// State returns the current hostage's state.
41+
// e.g. being untied, picked up, rescued etc.
42+
// See HostageState for all possible values.
43+
func (hostage *Hostage) State() HostageState {
44+
return HostageState(getInt(hostage.Entity, "m_nHostageState"))
45+
}
46+
47+
// Health returns the hostage's health points.
48+
// ! On Valve MM matches hostages are invulnerable, it will always return 100 unless "mp_hostages_takedamage" is set to 1
49+
func (hostage *Hostage) Health() int {
50+
return getInt(hostage.Entity, "m_iHealth")
51+
}
52+
53+
// Leader returns the possible player leading the hostage.
54+
// Returns nil if the hostage is not following a player.
55+
func (hostage *Hostage) Leader() *Player {
56+
return hostage.demoInfoProvider.FindPlayerByHandle(getInt(hostage.Entity, "m_leader"))
57+
}
58+
59+
// NewHostage creates a hostage.
60+
func NewHostage(demoInfoProvider demoInfoProvider, entity st.Entity) *Hostage {
61+
return &Hostage{
62+
demoInfoProvider: demoInfoProvider,
63+
Entity: entity,
64+
}
65+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package common
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
8+
st "github.com/markus-wa/demoinfocs-golang/v2/pkg/demoinfocs/sendtables"
9+
)
10+
11+
func TestHostage_Leader(t *testing.T) {
12+
player := new(Player)
13+
player.EntityID = 10
14+
provider := demoInfoProviderMock{
15+
playersByHandle: map[int]*Player{10: player},
16+
}
17+
hostage := hostageWithProperty("m_leader", st.PropertyValue{IntVal: 10}, provider)
18+
19+
assert.Equal(t, player, hostage.Leader())
20+
}
21+
22+
func TestHostage_State(t *testing.T) {
23+
hostage := hostageWithProperty("m_nHostageState", st.PropertyValue{IntVal: int(HostageStateFollowingPlayer)}, demoInfoProviderMock{})
24+
25+
assert.Equal(t, HostageStateFollowingPlayer, hostage.State())
26+
}
27+
28+
func TestHostage_Health(t *testing.T) {
29+
hostage := hostageWithProperty("m_iHealth", st.PropertyValue{IntVal: 40}, demoInfoProviderMock{})
30+
31+
assert.Equal(t, 40, hostage.Health())
32+
}
33+
34+
func hostageWithProperty(propName string, value st.PropertyValue, provider demoInfoProviderMock) *Hostage {
35+
return &Hostage{Entity: entityWithProperty(propName, value), demoInfoProvider: provider}
36+
}

pkg/demoinfocs/datatables.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ func (p *parser) bindEntities() {
6666
p.bindWeapons()
6767
p.bindBomb()
6868
p.bindGameRules()
69+
p.bindHostages()
6970
}
7071

7172
func (p *parser) bindBomb() {
@@ -595,3 +596,23 @@ func (p *parser) bindGameRules() {
595596
// "m_nCTTimeOuts"
596597
})
597598
}
599+
600+
func (p *parser) bindHostages() {
601+
p.stParser.ServerClasses().FindByName("CHostage").OnEntityCreated(func(entity st.Entity) {
602+
entityID := entity.ID()
603+
p.gameState.hostages[entityID] = common.NewHostage(p.demoInfoProvider, entity)
604+
605+
entity.OnDestroy(func() {
606+
delete(p.gameState.hostages, entityID)
607+
})
608+
609+
var state common.HostageState
610+
entity.Property("m_nHostageState").OnUpdate(func(val st.PropertyValue) {
611+
oldState := state
612+
state = common.HostageState(val.IntVal)
613+
if oldState != state {
614+
p.eventDispatcher.Dispatch(events.HostageStateChanged{OldState: oldState, NewState: state, Hostage: p.gameState.hostages[entityID]})
615+
}
616+
})
617+
})
618+
}

pkg/demoinfocs/events/events.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,36 @@ type BombPickup struct {
343343
Player *common.Player
344344
}
345345

346+
// HostageRecued signals that a hostage has been rescued.
347+
type HostageRecued struct {
348+
Player *common.Player
349+
Hostage *common.Hostage
350+
}
351+
352+
// HostageRescuedAll signals that all hostages have been rescued.
353+
type HostageRescuedAll struct{}
354+
355+
// HostageHurt signals that a hostage has been hurt.
356+
type HostageHurt struct {
357+
Player *common.Player
358+
Hostage *common.Hostage
359+
}
360+
361+
// HostageKilled signals that a hostage has been killed.
362+
type HostageKilled struct {
363+
Killer *common.Player
364+
Hostage *common.Hostage
365+
}
366+
367+
// HostageStateChanged signals that the state of a hostage has changed.
368+
// e.g. being untied, picked up, rescued etc.
369+
// See HostageState for all possible values.
370+
type HostageStateChanged struct {
371+
OldState common.HostageState
372+
NewState common.HostageState
373+
Hostage *common.Hostage
374+
}
375+
346376
// HitGroup is the type for the various HitGroupXYZ constants.
347377
//
348378
// See PlayerHurt.

pkg/demoinfocs/fake/game_state.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,8 @@ func (gs *GameState) Rules() demoinfocs.GameRules {
9999
func (gs *GameState) PlayerResourceEntity() st.Entity {
100100
return gs.Called().Get(0).(st.Entity)
101101
}
102+
103+
// Hostages is a mock-implementation of GameState.Hostages().
104+
func (gs *GameState) Hostages() []*common.Hostage {
105+
return gs.Called().Get(0).([]*common.Hostage)
106+
}

pkg/demoinfocs/game_events.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ func newGameEventHandler(parser *parser) gameEventHandler {
136136
"exit_buyzone": nil, // Dunno, only in locally recorded (POV) demo
137137
"flashbang_detonate": geh.flashBangDetonate, // Flash exploded
138138
"hegrenade_detonate": geh.heGrenadeDetonate, // HE exploded
139+
"hostage_killed": geh.hostageKilled, // Hostage killed
140+
"hostage_hurt": geh.hostageHurt, // Hostage hurt
141+
"hostage_rescued": geh.hostageRescued, // Hostage rescued
142+
"hostage_rescued_all": geh.HostageRescuedAll, // All hostages rescued
139143
"hltv_chase": nil, // Don't care
140144
"hltv_fixed": nil, // Dunno
141145
"hltv_message": nil, // No clue
@@ -476,6 +480,37 @@ func (geh gameEventHandler) infernoExpire(data map[string]*msg.CSVCMsg_GameEvent
476480
})
477481
}
478482

483+
func (geh gameEventHandler) hostageHurt(data map[string]*msg.CSVCMsg_GameEventKeyT) {
484+
event := events.HostageHurt{
485+
Player: geh.playerByUserID32(data["userid"].GetValShort()),
486+
Hostage: geh.gameState().hostages[int(data["hostage"].GetValShort())],
487+
}
488+
489+
geh.dispatch(event)
490+
}
491+
492+
func (geh gameEventHandler) hostageKilled(data map[string]*msg.CSVCMsg_GameEventKeyT) {
493+
event := events.HostageKilled{
494+
Killer: geh.playerByUserID32(data["userid"].GetValShort()),
495+
Hostage: geh.gameState().hostages[int(data["hostage"].GetValShort())],
496+
}
497+
498+
geh.dispatch(event)
499+
}
500+
501+
func (geh gameEventHandler) hostageRescued(data map[string]*msg.CSVCMsg_GameEventKeyT) {
502+
event := events.HostageRecued{
503+
Player: geh.playerByUserID32(data["userid"].GetValShort()),
504+
Hostage: geh.gameState().hostages[int(data["hostage"].GetValShort())],
505+
}
506+
507+
geh.dispatch(event)
508+
}
509+
510+
func (geh gameEventHandler) HostageRescuedAll(map[string]*msg.CSVCMsg_GameEventKeyT) {
511+
geh.dispatch(events.HostageRescuedAll{})
512+
}
513+
479514
func (geh gameEventHandler) playerConnect(data map[string]*msg.CSVCMsg_GameEventKeyT) {
480515
pl := &playerInfo{
481516
userID: int(data["userid"].GetValShort()),

pkg/demoinfocs/game_state.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type gameState struct {
2525
grenadeProjectiles map[int]*common.GrenadeProjectile // Maps entity-IDs to active nade-projectiles. That's grenades that have been thrown, but have not yet detonated.
2626
infernos map[int]*common.Inferno // Maps entity-IDs to active infernos.
2727
weapons map[int]*common.Equipment // Maps entity IDs to weapons. Used to remember what a weapon is (p250 / cz etc.)
28+
hostages map[int]*common.Hostage // Maps entity-IDs to hostages.
2829
entities map[int]st.Entity // Maps entity IDs to entities
2930
bomb common.Bomb
3031
totalRoundsPlayed int
@@ -101,6 +102,16 @@ func (gs gameState) Rules() GameRules {
101102
return gs.rules
102103
}
103104

105+
// Hostages returns all current hostages.
106+
func (gs gameState) Hostages() []*common.Hostage {
107+
hostages := make([]*common.Hostage, 0, len(gs.hostages))
108+
for _, hostage := range gs.hostages {
109+
hostages = append(hostages, hostage)
110+
}
111+
112+
return hostages
113+
}
114+
104115
// GrenadeProjectiles returns a map from entity-IDs to all live grenade projectiles.
105116
//
106117
// Only constains projectiles currently in-flight or still active (smokes etc.),
@@ -171,6 +182,7 @@ func newGameState(demoInfo demoInfoProvider) *gameState {
171182
grenadeProjectiles: make(map[int]*common.GrenadeProjectile),
172183
infernos: make(map[int]*common.Inferno),
173184
weapons: make(map[int]*common.Equipment),
185+
hostages: make(map[int]*common.Hostage),
174186
entities: make(map[int]st.Entity),
175187
thrownGrenades: make(map[*common.Player][]*common.Equipment),
176188
lastFlash: lastFlash{

pkg/demoinfocs/game_state_interface.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ type GameState interface {
3333
// Rules returns the GameRules for the current match.
3434
// Contains information like freeze time duration etc.
3535
Rules() GameRules
36+
// Hostages returns all current hostages.
37+
Hostages() []*common.Hostage
3638
// GrenadeProjectiles returns a map from entity-IDs to all live grenade projectiles.
3739
//
3840
// Only constains projectiles currently in-flight or still active (smokes etc.),

pkg/demoinfocs/game_state_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ func TestNewGameState(t *testing.T) {
2020
assert.NotNil(t, gs.grenadeProjectiles)
2121
assert.NotNil(t, gs.infernos)
2222
assert.NotNil(t, gs.weapons)
23+
assert.NotNil(t, gs.hostages)
2324
assert.NotNil(t, gs.entities)
2425
assert.NotNil(t, gs.rules.conVars)
2526
assert.Equal(t, common.TeamTerrorists, gs.tState.Team())
@@ -359,3 +360,13 @@ func newDisconnectedPlayer() *common.Player {
359360

360361
return pl
361362
}
363+
364+
func TestGameState_Hostages(t *testing.T) {
365+
hostageA := common.NewHostage(nil, new(stfake.Entity))
366+
hostageB := common.NewHostage(nil, new(stfake.Entity))
367+
hostages := map[int]*common.Hostage{0: hostageA, 1: hostageB}
368+
gs := gameState{hostages: hostages}
369+
370+
expectedHostages := []*common.Hostage{hostageA, hostageB}
371+
assert.Equal(t, expectedHostages, gs.Hostages())
372+
}

0 commit comments

Comments
 (0)