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 }