aboutsummaryrefslogtreecommitdiff
path: root/internal/game/core_command_queue.go
blob: 8fce71dc767403846a8d1908ff2828ec73969213 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
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
}

// 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
}