Files
Erupe/server/channelserver/sys_semaphore.go
2025-11-24 18:41:37 +01:00

127 lines
4.6 KiB
Go

package channelserver
import (
"erupe-ce/common/byteframe"
"erupe-ce/network/mhfpacket"
"sync"
)
// Semaphore is a multiplayer coordination mechanism for quests and events.
//
// Despite the name, Semaphore is NOT an OS synchronization primitive (like sync.Semaphore).
// Instead, it's a game-specific resource lock that coordinates multiplayer activities where:
// - Players must acquire a semaphore before participating
// - A limited number of participants are allowed (maxPlayers)
// - The semaphore tracks both active and reserved participants
//
// Use Cases:
// - Quest coordination: Ensures quest party size limits are enforced
// - Event coordination: Raviente, VS Tournament, Diva Defense
// - Global resources: Prevents multiple groups from starting conflicting events
//
// Semaphore vs Stage:
// - Stages are spatial (game rooms, areas). Players in a stage can see each other.
// - Semaphores are logical (coordination locks). Players in a semaphore are
// participating in the same activity but may be in different stages.
//
// Example: Raviente Event
// - Players acquire the Raviente semaphore to register for the event
// - Multiple quest stages exist (preparation, phase 1, phase 2, carving)
// - All participants share the same semaphore across different stages
// - The semaphore enforces the 32-player limit across all stages
//
// Thread Safety:
// Semaphore embeds sync.RWMutex. Use RLock for reads and Lock for writes.
type Semaphore struct {
sync.RWMutex // Protects semaphore state during concurrent access
// Semaphore identity
id_semaphore string // Semaphore ID string (identifies the resource/activity)
id uint32 // Numeric ID for client communication (auto-generated, starts at 7)
// Active participants
clients map[*Session]uint32 // Sessions actively using this semaphore -> character ID
// Reserved slots
// Players who have acquired the semaphore but may not be actively in the stage yet.
// The value is always nil; only the key (charID) matters. This is a set implementation.
reservedClientSlots map[uint32]interface{} // Character ID -> nil (set of reserved IDs)
// Capacity
maxPlayers uint16 // Maximum concurrent participants (e.g., 4 for quests, 32 for Raviente)
}
// NewSemaphore creates and initializes a new Semaphore for coordinating an activity.
//
// The semaphore is assigned an auto-incrementing ID from the server's semaphoreIndex.
// IDs 0-6 are reserved, so the first semaphore gets ID 7.
//
// Parameters:
// - s: The server (used to generate unique semaphore ID)
// - ID: Semaphore ID string (identifies the activity/resource)
// - MaxPlayers: Maximum participants allowed
//
// Returns a new Semaphore ready for client acquisition.
func NewSemaphore(s *Server, ID string, MaxPlayers uint16) *Semaphore {
sema := &Semaphore{
id_semaphore: ID,
id: s.NextSemaphoreID(),
clients: make(map[*Session]uint32),
reservedClientSlots: make(map[uint32]interface{}),
maxPlayers: MaxPlayers,
}
return sema
}
func (s *Semaphore) BroadcastRavi(pkt mhfpacket.MHFPacket) {
// Broadcast the data.
for session := range s.clients {
// Make the header
bf := byteframe.NewByteFrame()
bf.WriteUint16(uint16(pkt.Opcode()))
// Build the packet onto the byteframe.
pkt.Build(bf, session.clientContext)
// Enqueue in a non-blocking way that drops the packet if the connections send buffer channel is full.
session.QueueSendNonBlocking(bf.Data())
}
}
// BroadcastMHF sends a packet to all active participants in the semaphore.
//
// This is used for event-wide announcements that all participants need to see,
// regardless of which stage they're currently in. Examples:
// - Raviente phase changes
// - Tournament updates
// - Event completion notifications
//
// Only active clients (in the clients map) receive broadcasts. Reserved clients
// who haven't fully joined yet are excluded.
//
// Parameters:
// - pkt: The MHFPacket to broadcast to all participants
// - ignoredSession: Optional session to exclude from broadcast
//
// Thread Safety: Caller should hold semaphore lock when iterating clients.
func (s *Semaphore) BroadcastMHF(pkt mhfpacket.MHFPacket, ignoredSession *Session) {
// Broadcast the data.
for session := range s.clients {
if session == ignoredSession {
continue
}
// Make the header
bf := byteframe.NewByteFrame()
bf.WriteUint16(uint16(pkt.Opcode()))
// Build the packet onto the byteframe.
pkt.Build(bf, session.clientContext)
// Enqueue in a non-blocking way that drops the packet if the connections send buffer channel is full.
session.QueueSendNonBlocking(bf.Data())
}
}