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
|
package game
import (
"strings"
"thehouseoficarus/internal/net"
)
// resolveCommandPrefix expands an unambiguous, currently usable command prefix
// to its full command name. It is the general (non-casino) counterpart to
// casinoAbbreviation: a prefix such as "inven" runs inventory, while an
// ambiguous prefix such as "in" is left alone.
//
// Exact commands are never rewritten — the registry dispatch already handles
// them, and rewriting would make the result indistinguishable from a real
// command. When several commands share a prefix, the player's own room breaks
// the tie: if exactly one of the candidates is advertised where they are
// standing (an object verb, a mob action, an exit, an obstacle, a fixture, or
// the always-runnable play gateway), that one wins. This is what lets "pl"
// mean play even though "plant" also starts with "pl".
func (g *Game) resolveCommandPrefix(sess *net.Session, cmd string) (string, bool) {
if sess == nil || sess.Player == nil || cmd == "" {
return "", false
}
// Exact commands and action aliases dispatch as typed.
if _, ok := commandRegistry[cmd]; ok {
return "", false
}
if _, ok := verbAliases[cmd]; ok {
return "", false
}
obstacleVerbsMu.RLock()
exactObstacle := obstacleVerbs[cmd]
obstacleVerbsMu.RUnlock()
if exactObstacle {
return "", false
}
var matches []string
add := func(name string) {
if strings.HasPrefix(name, cmd) {
matches = append(matches, name)
}
}
for name := range commandRegistry {
add(name)
}
for name := range verbAliases {
add(name)
}
obstacleVerbsMu.RLock()
for name := range obstacleVerbs {
add(name)
}
obstacleVerbsMu.RUnlock()
switch len(matches) {
case 0:
return "", false
case 1:
return matches[0], true
}
// Ambiguous on the whole command table: prefer a single candidate the
// player can actually use here.
usable := g.roomVerbSet(sess)
only := ""
for _, name := range matches {
if !usable[name] {
continue
}
if only != "" {
return "", false
}
only = name
}
if only != "" {
return only, true
}
return "", false
}
// roomVerbSet returns the verbs the player can use where they are: the verbs
// advertised by the current room's objects, mobs, ground and exits, plus the
// always-runnable play gateway. It deliberately excludes the global
// always-available list so a context-free command cannot shadow a same-prefix
// command that is actually offered here.
func (g *Game) roomVerbSet(sess *net.Session) map[string]bool {
set := map[string]bool{
// play responds anywhere: at a table it opens the game, and away from
// one it reports that there is nothing to play. Making it usable
// everywhere lets "pl" name play instead of tying with plant.
"play": true,
}
if sess == nil || sess.Player == nil {
return set
}
rc := g.collectRoomCommands(sess)
for _, group := range [][]string{rc.objects, rc.mobs, rc.ground} {
for _, entry := range group {
if fields := strings.Fields(entry); len(fields) > 0 {
set[fields[0]] = true
}
}
}
for _, exit := range rc.exits {
for _, dir := range strings.Split(exit, ",") {
if dir = strings.TrimSpace(dir); dir != "" {
set[dir] = true
}
}
}
return set
}
|