fix(stage): fix race condition with stages.

This commit is contained in:
Houmgaor
2025-10-21 00:00:08 +02:00
parent 488e8fa045
commit e654bc47bf

View File

@@ -111,10 +111,20 @@ func doStageTransfer(s *Session, ackHandle uint32, stageID string) {
if !s.userEnteredStage {
s.userEnteredStage = true
// Lock server to safely iterate over sessions map
// We need to copy the session list first to avoid holding the lock during packet building
s.server.Lock()
var sessionList []*Session
for _, session := range s.server.sessions {
if s == session {
continue
}
sessionList = append(sessionList, session)
}
s.server.Unlock()
// Build packets for each session without holding the lock
for _, session := range sessionList {
temp = &mhfpacket.MsgSysInsertUser{CharID: session.charID}
newNotif.WriteUint16(uint16(temp.Opcode()))
temp.Build(newNotif, s.clientContext)
@@ -132,12 +142,22 @@ func doStageTransfer(s *Session, ackHandle uint32, stageID string) {
if s.stage != nil { // avoids lock up when using bed for dream quests
// Notify the client to duplicate the existing objects.
s.logger.Info(fmt.Sprintf("Sending existing stage objects to %s", s.Name))
// Lock stage to safely iterate over objects map
// We need to copy the objects list first to avoid holding the lock during packet building
s.stage.RLock()
var temp mhfpacket.MHFPacket
var objectList []*Object
for _, obj := range s.stage.objects {
if obj.ownerCharID == s.charID {
continue
}
objectList = append(objectList, obj)
}
s.stage.RUnlock()
// Build packets for each object without holding the lock
var temp mhfpacket.MHFPacket
for _, obj := range objectList {
temp = &mhfpacket.MsgSysDuplicateObject{
ObjID: obj.id,
X: obj.x,
@@ -149,7 +169,6 @@ func doStageTransfer(s *Session, ackHandle uint32, stageID string) {
newNotif.WriteUint16(uint16(temp.Opcode()))
temp.Build(newNotif, s.clientContext)
}
s.stage.RUnlock()
}
// FIX: Always send stage transfer packet, even if empty.
@@ -166,7 +185,12 @@ func destructEmptyStages(s *Session) {
for _, stage := range s.server.stages {
// Destroy empty Quest/My series/Guild stages.
if stage.id[3:5] == "Qs" || stage.id[3:5] == "Ms" || stage.id[3:5] == "Gs" || stage.id[3:5] == "Ls" {
if len(stage.reservedClientSlots) == 0 && len(stage.clients) == 0 {
// Lock stage to safely check its client and reservation counts
stage.Lock()
isEmpty := len(stage.reservedClientSlots) == 0 && len(stage.clients) == 0
stage.Unlock()
if isEmpty {
delete(s.server.stages, stage.id)
s.logger.Debug("Destructed stage", zap.String("stage.id", stage.id))
}
@@ -183,6 +207,7 @@ func removeSessionFromStage(s *Session) {
delete(s.stage.clients, s)
// Collect objects to delete while holding lock
// We must copy the objects to delete to avoid modifying the map while iterating
s.logger.Info("Sending notification to old stage clients")
var objectsToDelete []*Object
for _, object := range s.stage.objects {
@@ -209,6 +234,30 @@ func removeSessionFromStage(s *Session) {
destructEmptySemaphores(s)
}
func isStageFull(s *Session, StageID string) bool {
s.server.Lock()
stage, exists := s.server.stages[StageID]
s.server.Unlock()
if exists {
// Lock stage to safely check client counts
// Read the values we need while holding RLock, then release immediately
// to avoid deadlock with other functions that might hold server lock
stage.RLock()
reserved := len(stage.reservedClientSlots)
clients := len(stage.clients)
_, hasReservation := stage.reservedClientSlots[s.charID]
maxPlayers := stage.maxPlayers
stage.RUnlock()
if hasReservation {
return false
}
return reserved+clients >= int(maxPlayers)
}
return false
}
func handleMsgSysEnterStage(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgSysEnterStage)