diff --git a/server/channelserver/handlers_stage.go b/server/channelserver/handlers_stage.go index 4082f2e30..6bdf61862 100644 --- a/server/channelserver/handlers_stage.go +++ b/server/channelserver/handlers_stage.go @@ -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)