aboutsummaryrefslogtreecommitdiff
path: root/internal/game/cmd_prefix.go
blob: da3e646e15396c916b699489f40f5a6adf25a025 (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
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
}