Skip to content

Commit 2e2eb8c

Browse files
committed
Refactored GMCP combat tracking and room mapping
Added: - Centralized combat tracking system with shared combatUsers map - GetLowestRoomId() method to mapper for stable map identifiers - Map_id field to GMCP room data based on lowest connected room ID - Exit details for cross-map navigation (leads_to_map, leads_to_area) - ANSI code stripping for all GMCP text fields - CleanupUser() function for unified combat state cleanup - Support for non-compass direction exits in GMCP Fixed: - Race conditions in combat module cleanup by centralizing PlayerDespawn handling - Synchronous cooldown updates to prevent state inconsistencies - Combat round handlers to only process users currently in combat Changed: - Cooldown timer interval from 250ms to 200ms - NPC targeting from boolean to array of player names - Room.Info.Exits structure to remove redundant wrapper - GMCP module handling to always send all modules Removed: - Individual PlayerDespawn handlers from each combat module - GMCPUserCleanup event type and related infrastructure - Core.Supports.Set/Remove module tracking - Mob health tracking from Target module - Restriction on non-compass direction exits Breaking Changes: - Room.Info.Exits now exist as a primary node instead of Room.Info.Exits.exits - NPC targeting_you changed from boolean to targeting array
1 parent e79babd commit 2e2eb8c

11 files changed

+394
-558
lines changed

internal/hooks/PlayerDespawn_HandleLeave.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,10 @@ func HandleLeave(e events.Event) events.ListenerReturn {
6666
if err := users.LogOutUserByConnectionId(connId); err != nil {
6767
mudlog.Error("Log Out Error", "connectionId", connId, "error", err)
6868
}
69-
69+
7070
// Clean up all GMCP state for this user
7171
gmcp.CleanupUser(evt.UserId)
72-
72+
7373
connections.Remove(connId)
7474

7575
specialRooms := configs.GetSpecialRoomsConfig()

internal/mapper/mapper.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,22 @@ func (r *mapper) RootRoomId() int {
227227
return r.rootRoomId
228228
}
229229

230+
// GetLowestRoomId returns the lowest room ID in the connected map area
231+
// This provides a stable identifier for the map regardless of which room was used to create it
232+
func (r *mapper) GetLowestRoomId() int {
233+
if len(r.crawledRooms) == 0 {
234+
return r.rootRoomId
235+
}
236+
237+
lowestId := 0
238+
for roomId := range r.crawledRooms {
239+
if lowestId == 0 || roomId < lowestId {
240+
lowestId = roomId
241+
}
242+
}
243+
return lowestId
244+
}
245+
230246
func (r *mapper) CrawledRoomIds() []int {
231247
roomIds := []int{}
232248
for roomId := range r.crawledRooms {

modules/gmcp/gmcp.Char.Combat.Cooldown.go

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,7 @@ func InitCombatCooldownTimer() {
5353

5454
// Register the GMCP event handler
5555
events.RegisterListener(GMCPCombatCooldownUpdate{}, handleCombatCooldownUpdate)
56-
57-
// Clean up when player disconnects
58-
events.RegisterListener(events.PlayerDespawn{}, handleCooldownPlayerDespawn)
56+
5957
}
6058

6159
// handleNewRound updates round tracking
@@ -241,8 +239,8 @@ func UntrackCombatPlayer(userId int) {
241239
timingConfig := configs.GetTimingConfig()
242240
maxSeconds := float64(timingConfig.RoundSeconds)
243241

244-
// Queue final update event
245-
events.AddToQueue(GMCPCombatCooldownUpdate{
242+
// Send final update synchronously to avoid race condition
243+
handleCombatCooldownUpdate(GMCPCombatCooldownUpdate{
246244
UserId: userId,
247245
CooldownSeconds: 0.0,
248246
MaxSeconds: maxSeconds,
@@ -286,17 +284,3 @@ func handleCombatCooldownUpdate(e events.Event) events.ListenerReturn {
286284

287285
return events.Continue
288286
}
289-
290-
// handleCooldownPlayerDespawn cleans up when player leaves
291-
func handleCooldownPlayerDespawn(e events.Event) events.ListenerReturn {
292-
evt, typeOk := e.(events.PlayerDespawn)
293-
if !typeOk {
294-
mudlog.Error("GMCPCombatCooldown", "action", "handleCooldownPlayerDespawn", "error", "type assertion failed", "expectedType", "events.PlayerDespawn", "actualType", fmt.Sprintf("%T", e))
295-
return events.Continue
296-
}
297-
298-
// Stop tracking cooldown for this player
299-
UntrackCombatPlayer(evt.UserId)
300-
301-
return events.Continue
302-
}

modules/gmcp/gmcp.Char.Combat.Damage.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func init() {
2929

3030
// Keep the internal event for backward compatibility
3131
events.RegisterListener(GMCPCombatDamageUpdate{}, handleCombatDamageUpdate)
32-
32+
3333
}
3434

3535
func handleCombatDamageUpdate(e events.Event) events.ListenerReturn {
@@ -134,4 +134,3 @@ func SendCombatDamage(userId int, amount int, damageType string, source string,
134134
Target: target,
135135
})
136136
}
137-

modules/gmcp/gmcp.Char.Combat.Enemies.go

Lines changed: 13 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/GoMudEngine/GoMud/internal/mudlog"
1515
"github.com/GoMudEngine/GoMud/internal/rooms"
1616
"github.com/GoMudEngine/GoMud/internal/users"
17+
"github.com/GoMudEngine/GoMud/internal/util"
1718
)
1819

1920
// GMCPCombatEnemiesUpdate is sent when the list of enemies changes
@@ -38,15 +39,15 @@ var (
3839
userEnemies = make(map[int]map[int]bool) // userId -> map[mobInstanceId]bool
3940
)
4041

42+
// NOTE: Race condition mitigated by defensive cleanup in all handlers.
43+
// If user disconnects between validateUserForGMCP and map operations,
44+
// the cleanup functions handle it gracefully without data corruption.
45+
4146
func init() {
42-
// Register listener for combat enemies updates
4347
events.RegisterListener(GMCPCombatEnemiesUpdate{}, handleCombatEnemiesUpdate)
44-
45-
// Listen for events that affect enemy lists
4648
events.RegisterListener(events.NewRound{}, handleEnemiesNewRound)
4749
events.RegisterListener(events.MobDeath{}, handleEnemiesMobDeath)
4850
events.RegisterListener(events.RoomChange{}, handleEnemiesRoomChange)
49-
events.RegisterListener(events.PlayerDespawn{}, handleEnemiesPlayerDespawn)
5051
}
5152

5253
func handleCombatEnemiesUpdate(e events.Event) events.ListenerReturn {
@@ -61,7 +62,6 @@ func handleCombatEnemiesUpdate(e events.Event) events.ListenerReturn {
6162
return events.Continue
6263
}
6364

64-
// Build the payload as an array directly
6565
enemies := make([]map[string]interface{}, len(evt.Enemies))
6666
for i, enemy := range evt.Enemies {
6767
enemies[i] = map[string]interface{}{
@@ -88,11 +88,12 @@ func handleEnemiesNewRound(e events.Event) events.ListenerReturn {
8888
return events.Continue
8989
}
9090

91-
// Check all online users
92-
for _, userId := range users.GetOnlineUserIds() {
91+
// Check all users currently in combat
92+
trackedUsers := GetUsersInCombat()
93+
94+
for _, userId := range trackedUsers {
9395
user := users.GetByUserId(userId)
9496
if user == nil {
95-
// Clean up stale enemy tracking
9697
enemiesMutex.Lock()
9798
if _, exists := userEnemies[userId]; exists {
9899
delete(userEnemies, userId)
@@ -102,7 +103,6 @@ func handleEnemiesNewRound(e events.Event) events.ListenerReturn {
102103
continue
103104
}
104105

105-
// Get current enemies (mobs that have this user as a target)
106106
currentEnemies := make(map[int]bool)
107107

108108
room := rooms.LoadRoom(user.Character.RoomId)
@@ -119,12 +119,10 @@ func handleEnemiesNewRound(e events.Event) events.ListenerReturn {
119119
}
120120
}
121121

122-
// Check if enemies changed
123122
enemiesMutex.RLock()
124123
oldEnemies := userEnemies[userId]
125124
changed := false
126125

127-
// Check if any enemies were added or removed
128126
if len(oldEnemies) != len(currentEnemies) {
129127
changed = true
130128
} else {
@@ -147,7 +145,6 @@ func handleEnemiesNewRound(e events.Event) events.ListenerReturn {
147145
enemiesMutex.Unlock()
148146
}
149147

150-
// Send update if changed
151148
if changed {
152149
sendEnemiesUpdate(userId)
153150
}
@@ -164,7 +161,6 @@ func handleEnemiesMobDeath(e events.Event) events.ListenerReturn {
164161
return events.Continue
165162
}
166163

167-
// Check all users to see if this mob was in their enemy list
168164
enemiesMutex.RLock()
169165
usersToCheck := make(map[int]bool)
170166
for userId, enemies := range userEnemies {
@@ -174,7 +170,6 @@ func handleEnemiesMobDeath(e events.Event) events.ListenerReturn {
174170
}
175171
enemiesMutex.RUnlock()
176172

177-
// Now update the affected users
178173
usersToUpdate := []int{}
179174
if len(usersToCheck) > 0 {
180175
enemiesMutex.Lock()
@@ -192,7 +187,6 @@ func handleEnemiesMobDeath(e events.Event) events.ListenerReturn {
192187
enemiesMutex.Unlock()
193188
}
194189

195-
// Send updates
196190
for _, userId := range usersToUpdate {
197191
sendEnemiesUpdate(userId)
198192
}
@@ -208,7 +202,6 @@ func handleEnemiesRoomChange(e events.Event) events.ListenerReturn {
208202
return events.Continue
209203
}
210204

211-
// Only care about mob movements
212205
if evt.MobInstanceId == 0 {
213206
return events.Continue
214207
}
@@ -254,34 +247,24 @@ func handleEnemiesRoomChange(e events.Event) events.ListenerReturn {
254247
enemiesMutex.Unlock()
255248
}
256249

257-
// Send updates
258250
for _, userId := range usersToUpdate {
259251
sendEnemiesUpdate(userId)
260252
}
261253

262254
return events.Continue
263255
}
264256

265-
// handleEnemiesPlayerDespawn cleans up when player leaves
266-
func handleEnemiesPlayerDespawn(e events.Event) events.ListenerReturn {
267-
evt, typeOk := e.(events.PlayerDespawn)
268-
if !typeOk {
269-
mudlog.Error("GMCPCombatEnemies", "action", "handleEnemiesPlayerDespawn", "error", "type assertion failed", "expectedType", "events.PlayerDespawn", "actualType", fmt.Sprintf("%T", e))
270-
return events.Continue
271-
}
272-
257+
// cleanupCombatEnemies removes all enemy tracking for a user
258+
func cleanupCombatEnemies(userId int) {
273259
enemiesMutex.Lock()
274-
delete(userEnemies, evt.UserId)
260+
delete(userEnemies, userId)
275261
enemiesMutex.Unlock()
276-
277-
return events.Continue
278262
}
279263

280264
// sendEnemiesUpdate sends current enemy list for a user
281265
func sendEnemiesUpdate(userId int) {
282266
user := users.GetByUserId(userId)
283267
if user == nil {
284-
// Clean up stale enemy tracking
285268
enemiesMutex.Lock()
286269
delete(userEnemies, userId)
287270
enemiesMutex.Unlock()
@@ -295,7 +278,6 @@ func sendEnemiesUpdate(userId int) {
295278
enemyMap := userEnemies[userId]
296279
enemiesMutex.RUnlock()
297280

298-
// Build enemy info list
299281
for mobId := range enemyMap {
300282
if mob := mobs.GetInstance(mobId); mob != nil {
301283
isPrimary := false
@@ -304,14 +286,13 @@ func sendEnemiesUpdate(userId int) {
304286
}
305287

306288
enemies = append(enemies, EnemyInfo{
307-
Name: mob.Character.Name,
289+
Name: util.StripANSI(mob.Character.Name),
308290
Id: mobId,
309291
IsPrimary: isPrimary,
310292
})
311293
}
312294
}
313295

314-
// Send update
315296
handleCombatEnemiesUpdate(GMCPCombatEnemiesUpdate{
316297
UserId: userId,
317298
Enemies: enemies,

modules/gmcp/gmcp.Char.Combat.Events.go

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/GoMudEngine/GoMud/internal/events"
1111
"github.com/GoMudEngine/GoMud/internal/mudlog"
12+
"github.com/GoMudEngine/GoMud/internal/util"
1213
)
1314

1415
// GMCPCombatEvent is a generic GMCP event for combat notifications
@@ -30,7 +31,7 @@ func init() {
3031
events.RegisterListener(events.DamageDealt{}, handleDamageDealt)
3132
events.RegisterListener(events.AttackAvoided{}, handleAttackAvoided)
3233
events.RegisterListener(events.CombatantFled{}, handleCombatantFled)
33-
34+
3435
}
3536

3637
// handleCombatEvent sends GMCP combat events
@@ -72,7 +73,7 @@ func handleCombatStarted(e events.Event) events.ListenerReturn {
7273
"role": "attacker",
7374
"targetId": evt.DefenderId,
7475
"targetType": evt.DefenderType,
75-
"targetName": evt.DefenderName,
76+
"targetName": util.StripANSI(evt.DefenderName),
7677
"initiatedBy": evt.InitiatedBy,
7778
},
7879
})
@@ -87,7 +88,7 @@ func handleCombatStarted(e events.Event) events.ListenerReturn {
8788
"role": "defender",
8889
"attackerId": evt.AttackerId,
8990
"attackerType": evt.AttackerType,
90-
"attackerName": evt.AttackerName,
91+
"attackerName": util.StripANSI(evt.AttackerName),
9192
"initiatedBy": evt.InitiatedBy,
9293
},
9394
})
@@ -135,11 +136,11 @@ func handleDamageDealt(e events.Event) events.ListenerReturn {
135136
Data: map[string]interface{}{
136137
"targetId": evt.TargetId,
137138
"targetType": evt.TargetType,
138-
"targetName": evt.TargetName,
139+
"targetName": util.StripANSI(evt.TargetName),
139140
"amount": evt.Amount,
140141
"damageType": evt.DamageType,
141-
"weaponName": evt.WeaponName,
142-
"spellName": evt.SpellName,
142+
"weaponName": util.StripANSI(evt.WeaponName),
143+
"spellName": util.StripANSI(evt.SpellName),
143144
"isCritical": evt.IsCritical,
144145
"isKillingBlow": evt.IsKillingBlow,
145146
},
@@ -154,11 +155,11 @@ func handleDamageDealt(e events.Event) events.ListenerReturn {
154155
Data: map[string]interface{}{
155156
"sourceId": evt.SourceId,
156157
"sourceType": evt.SourceType,
157-
"sourceName": evt.SourceName,
158+
"sourceName": util.StripANSI(evt.SourceName),
158159
"amount": evt.Amount,
159160
"damageType": evt.DamageType,
160-
"weaponName": evt.WeaponName,
161-
"spellName": evt.SpellName,
161+
"weaponName": util.StripANSI(evt.WeaponName),
162+
"spellName": util.StripANSI(evt.SpellName),
162163
"isCritical": evt.IsCritical,
163164
"isKillingBlow": evt.IsKillingBlow,
164165
},
@@ -184,9 +185,9 @@ func handleAttackAvoided(e events.Event) events.ListenerReturn {
184185
Data: map[string]interface{}{
185186
"defenderId": evt.DefenderId,
186187
"defenderType": evt.DefenderType,
187-
"defenderName": evt.DefenderName,
188+
"defenderName": util.StripANSI(evt.DefenderName),
188189
"avoidType": evt.AvoidType,
189-
"weaponName": evt.WeaponName,
190+
"weaponName": util.StripANSI(evt.WeaponName),
190191
},
191192
})
192193
}
@@ -199,9 +200,9 @@ func handleAttackAvoided(e events.Event) events.ListenerReturn {
199200
Data: map[string]interface{}{
200201
"attackerId": evt.AttackerId,
201202
"attackerType": evt.AttackerType,
202-
"attackerName": evt.AttackerName,
203+
"attackerName": util.StripANSI(evt.AttackerName),
203204
"avoidType": evt.AvoidType,
204-
"weaponName": evt.WeaponName,
205+
"weaponName": util.StripANSI(evt.WeaponName),
205206
},
206207
})
207208
}
@@ -232,4 +233,3 @@ func handleCombatantFled(e events.Event) events.ListenerReturn {
232233

233234
return events.Continue
234235
}
235-

0 commit comments

Comments
 (0)