package game import ( "sort" "sync" "time" "thehouseoficarus/internal/net" ) // QueuedCommand represents a player command enqueued for next-tick execution. type QueuedCommand struct { Session *net.Session Command string Args string Timestamp time.Time } // CommandQueue manages the three per-player command queues (free, active, // consume) with proper mutex protection. Free commands stack and execute in // order; active commands replace each other (only one survives); consume // commands are a single-slot eat/drink queue. Writers are the command // handlers (owner goroutine); the consumer is ProcessQueuedCommands on the // owner tick. The mutex is kept as a cheap guard for callers that reach a // queue from outside that single-owner path. type CommandQueue struct { mu sync.Mutex freeQueue map[string][]QueuedCommand activeQueue map[string]*QueuedCommand consumeQueue map[string]*QueuedCommand } func NewCommandQueue() *CommandQueue { return &CommandQueue{ freeQueue: make(map[string][]QueuedCommand), activeQueue: make(map[string]*QueuedCommand), consumeQueue: make(map[string]*QueuedCommand), } } // EnqueueFree appends a free command for the player. func (q *CommandQueue) EnqueueFree(name string, qc QueuedCommand) { q.mu.Lock() defer q.mu.Unlock() q.freeQueue[name] = append(q.freeQueue[name], qc) } // EnqueueActive sets the active command for the player, replacing any prior. func (q *CommandQueue) EnqueueActive(name string, qc QueuedCommand) { q.mu.Lock() defer q.mu.Unlock() q.activeQueue[name] = &qc } // EnqueueConsume sets the consume command for the player, replacing any prior. func (q *CommandQueue) EnqueueConsume(name string, qc QueuedCommand) { q.mu.Lock() defer q.mu.Unlock() q.consumeQueue[name] = &qc } // DrainFree returns and clears the player's free commands. func (q *CommandQueue) DrainFree(name string) []QueuedCommand { q.mu.Lock() defer q.mu.Unlock() cmds := q.freeQueue[name] delete(q.freeQueue, name) return cmds } // DrainActive returns all active commands, then clears the map. It does not // order them: ProcessQueuedCommands sorts by Timestamp (first-to-queue wins). func (q *CommandQueue) DrainActive() []QueuedCommand { q.mu.Lock() defer q.mu.Unlock() var actives []QueuedCommand for _, qc := range q.activeQueue { actives = append(actives, *qc) } q.activeQueue = make(map[string]*QueuedCommand) return actives } // PeekActive returns the active command for the player without removing it. func (q *CommandQueue) PeekActive(name string) (*QueuedCommand, bool) { q.mu.Lock() defer q.mu.Unlock() qc, ok := q.activeQueue[name] return qc, ok } // PeekConsume returns the pending consume command without removing it. func (q *CommandQueue) PeekConsume(name string) (*QueuedCommand, bool) { q.mu.Lock() defer q.mu.Unlock() qc, ok := q.consumeQueue[name] return qc, ok } // PeekFree returns a copy of the free commands for the player without removing. func (q *CommandQueue) PeekFree(name string) []QueuedCommand { q.mu.Lock() defer q.mu.Unlock() cmds := q.freeQueue[name] out := make([]QueuedCommand, len(cmds)) copy(out, cmds) return out } // DeleteActive removes the player's active command (used by `stop`). func (q *CommandQueue) DeleteActive(name string) { q.mu.Lock() defer q.mu.Unlock() delete(q.activeQueue, name) } // ActivePositions returns the names of every player holding an active command // in actual resolution order (earliest Timestamp first, matching // ProcessQueuedCommands), and the 1-based position of the named player among // them, or 0 when that player has no active command. Used by the `queued` // command to show queue order. func (q *CommandQueue) ActivePositions(name string) (names []string, pos int) { q.mu.Lock() defer q.mu.Unlock() type entry struct { name string at time.Time } entries := make([]entry, 0, len(q.activeQueue)) for n, qc := range q.activeQueue { entries = append(entries, entry{name: n, at: qc.Timestamp}) } sort.SliceStable(entries, func(i, j int) bool { return entries[i].at.Before(entries[j].at) }) for _, e := range entries { names = append(names, e.name) if e.name == name { pos = len(names) } } return names, pos } // ClearPlayer drops every queued command for the player: free, active, and // consume. Called when a session detaches (logout, quit, disconnect, or // character removal) so leftovers cannot retain the departed session and // player, and cannot replay against a later session for the same name. func (q *CommandQueue) ClearPlayer(name string) { q.mu.Lock() defer q.mu.Unlock() delete(q.freeQueue, name) delete(q.activeQueue, name) delete(q.consumeQueue, name) } // DrainConsume returns and removes the consume command for the player. func (q *CommandQueue) DrainConsume(name string) (*QueuedCommand, bool) { q.mu.Lock() defer q.mu.Unlock() qc, ok := q.consumeQueue[name] delete(q.consumeQueue, name) return qc, ok }