1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
|
package game
import "sync"
// SafespotState tracks a player's active safespot (cover) in a room.
type SafespotState struct {
Active bool
ObjectDefID string
ObjectIndex int
RoomID int
HideCountdown int
}
// SafespotManager owns the per-player safespot state map and its mutex.
// Callers receive value copies via Get/Peek so they cannot mutate state
// outside the lock — this avoids the lock-copy-pointer-unlock anti-pattern.
type SafespotManager struct {
mu sync.Mutex
states map[string]*SafespotState
}
func NewSafespotManager() *SafespotManager {
return &SafespotManager{states: make(map[string]*SafespotState)}
}
// Get returns a value copy of the player's safespot state.
func (m *SafespotManager) Get(name string) (SafespotState, bool) {
m.mu.Lock()
defer m.mu.Unlock()
ss, ok := m.states[name]
if !ok {
return SafespotState{}, false
}
return *ss, true
}
// Has reports whether any safespot state exists for the player.
func (m *SafespotManager) Has(name string) bool {
m.mu.Lock()
defer m.mu.Unlock()
_, ok := m.states[name]
return ok
}
// IsActive reports whether the player has an active, fully-hidden safespot
// (hide countdown complete).
func (m *SafespotManager) IsActive(name string) bool {
m.mu.Lock()
defer m.mu.Unlock()
ss, ok := m.states[name]
return ok && ss.Active && ss.HideCountdown <= 0
}
// Set stores a safespot state for the player.
func (m *SafespotManager) Set(name string, ss SafespotState) {
m.mu.Lock()
defer m.mu.Unlock()
m.states[name] = &ss
}
// Delete removes the player's safespot state. Returns true if a state existed.
func (m *SafespotManager) Delete(name string) bool {
m.mu.Lock()
defer m.mu.Unlock()
_, ok := m.states[name]
delete(m.states, name)
return ok
}
// RekeyRooms remaps the stored RoomID of every safespot per idMap so a live
// room rename does not orphan a player's cover (the safespot lookup keys on
// the room ID and force-ejects on a miss).
func (m *SafespotManager) RekeyRooms(idMap map[int]int) {
if len(idMap) == 0 {
return
}
m.mu.Lock()
defer m.mu.Unlock()
for _, ss := range m.states {
if newID, ok := idMap[ss.RoomID]; ok && newID != ss.RoomID {
ss.RoomID = newID
}
}
}
// DecrementHideCountdown decrements the hide countdown for a player who is
// transitioning from hiding to hidden. Returns (newCountdown, ok). A return of
// ok=true with newCountdown > 0 means the player is still counting down;
// newCountdown == 0 means the safespot just activated.
func (m *SafespotManager) DecrementHideCountdown(name string) (int, bool) {
m.mu.Lock()
defer m.mu.Unlock()
ss, ok := m.states[name]
if !ok || !ss.Active {
return 0, false
}
if ss.HideCountdown > 0 {
ss.HideCountdown--
}
return ss.HideCountdown, true
}
|