package game import ( "fmt" "path/filepath" "strings" "gopkg.in/yaml.v3" "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/color" "thehouseoficarus/internal/net" "thehouseoficarus/internal/ui" ) type HelpDef struct { Name string `yaml:"name"` Aliases []string `yaml:"aliases"` Related []string `yaml:"related"` Category string `yaml:"category"` Content string `yaml:"description"` } type cmdEntry struct { Name string Type string Desc string } var commandList = []cmdEntry{ {"alias", "Instant", "Create command shortcuts"}, {"aps", "Active", "Auto-travel using Agility"}, {"attack / kill", "Active", "Attack a mob"}, {"autotrigger / auto", "Instant", "Set a module to auto-trigger"}, {"bank", "Active", "Open bank interface"}, {"browse / list", "Instant", "Browse a shop's stock; at a casino table, list every bet accepted now"}, {"burn", "Active", "Start a fire"}, {"buy", "Instant", "Buy from a shop"}, {"chop / cut", "Active", "Chop trees (Woodcutting)"}, {"clean", "Free", "Clean grimy herbs (Pharmacy)"}, {"color / colors", "Instant", "Customize display colors"}, {"colortable", "Instant", "Display color reference chart"}, {"construct / make", "Active", "Build furniture (Construction)"}, {"cook", "Active", "Cook raw food on a fire or range"}, {"craft", "Active", "Craft jewelry (Crafting)"}, {"cure", "Active", "Cure diseased crops (Farming)"}, {"description / desc", "Instant", "Set your character description"}, {"drink", "Free", "Drink a potion"}, {"drop", "Active", "Drop items to the ground"}, {"eat", "Free", "Eat food to restore hitpoints"}, {"equipment / eq", "Instant", "Show equipped items"}, {"exits", "Instant", "List available exits"}, {"fish", "Active", "Fish at fishing spots (Fishing)"}, {"fletch", "Free", "Fletch logs into bows (Fletching)"}, {"get / take / pick", "Active", "Pick up items from the ground"}, {"global", "Instant", "Chat with everyone on the server"}, {"harvest", "Active", "Harvest a grown farming patch"}, {"help", "Instant", "Show help topics"}, {"hide / safespot", "Active", "Hide in a safespot"}, {"home / recall", "Instant", "Return to your home room"}, {"id / identify", "Active", "Identify herbs (Pharmacy)"}, {"inspect", "Instant", "Inspect a mob, object, or item"}, {"inventory / i / inv", "Instant", "Show your inventory"}, {"jack / jackin", "Active", "Jack into a terminal (Hacking)"}, {"look / l", "Instant", "Look around or examine things"}, {"map", "Instant", "Display an ASCII map of the area"}, {"play", "Active", "Join a game; play bj / play bac; optional amount and target"}, {"table / status", "Instant", "Redisplay table state; table rules / table help for details"}, {"rules", "Instant", "Help page for the casino game you're playing (one per house edition)"}, {"watch", "Instant", "Watch detailed table action; watch off to stop"}, {"sitout", "Instant", "Sit out future table rounds; bet to rejoin"}, {"bet / rebet", "Active", "Repeat a wager, or bet [target]; min/max accepted"}, {"autobet / autospin", "Active", "Automatically repeat a casino wager"}, {"spin", "Active", "Spin the current casino wager"}, {"hit / stand / double / split", "Active", "Play blackjack (shortcuts: h / st / dd / sp)"}, {"insurance / decline / surrender", "Active", "Answer an insurance offer or surrender a blackjack hand"}, {"mine", "Active", "Mine rocks (Mining)"}, {"mix", "Active", "Mix potions (Pharmacy)"}, {"mods / modlist", "Instant", "List available science modules"}, {"north / south / east / west / up / down", "Active", "Move in a direction"}, {"option / options", "Instant", "View or change settings"}, {"plant", "Active", "Plant seeds in a farming patch"}, {"prompt", "Instant", "Set custom command prompt"}, {"pull / push", "Active", "Interact with objects"}, {"queued", "Instant", "Show pending tick actions"}, {"quit", "Active", "Rest and disconnect (\"quit now\" forfeits a mid-game bet)"}, {"rake", "Active", "Rake a farming patch"}, {"remove / unwear / unwield", "Free", "Unequip items"}, {"say", "Instant", "Chat with players in your room"}, {"score / sc", "Instant", "View your stats and skills"}, {"search", "Active", "Search items for loot"}, {"sell", "Instant", "Sell to a shop"}, {"skills / sk", "Instant", "Show your skill levels"}, {"smelt", "Active", "Smelt ore into bars (Smithing)"}, {"smith", "Active", "Smith bars into items (Smithing)"}, {"sneak", "Instant", "Toggle sneak mode"}, {"stats", "Instant", "Show your combat stats"}, {"steal", "Active", "Steal from mobs (Thieving)"}, {"stop", "Instant", "Stop the current action"}, {"stoke", "Active", "Add logs to a fire"}, {"style", "Instant", "Change combat style"}, {"symbol", "Instant", "Set custom map symbol for current room"}, {"talk / speak / ask", "Active", "Talk to mobs"}, {"task", "Instant", "Show your current task"}, {"tech", "Instant", "Toggle technology abilities"}, {"trigger", "Active", "Trigger a science module"}, {"unalias", "Instant", "Remove command shortcuts"}, {"unhide / unsafespot", "Active", "Leave a safespot"}, {"use", "Active", "Use an object (crafting)"}, {"verbs / commands", "Instant", "List every command you can use"}, {"walk", "Active", "Pathfind to a room or multi-step walk"}, {"water", "Active", "Water crops (Farming)"}, {"wear / wield", "Free", "Equip items"}, {"who", "Instant", "List everyone online and their combat level"}, } func LoadHelp(dataDir string) ([]HelpDef, error) { dir := filepath.Join(dataDir, "help") var helps []HelpDef behavior.WalkYAMLDir(dir, func(path, id string, data []byte) error { h := parseHelpDef(data) if h == nil { return nil } helps = append(helps, *h) return nil }) return helps, nil } // helpRaw mirrors the canonical help schema and is also used by data // validation to flag unknown fields. type helpRaw struct { Name string `yaml:"name"` Aliases []string `yaml:"aliases"` Related []string `yaml:"related"` Category string `yaml:"category"` Description string `yaml:"description"` } // parseHelpDef unmarshals a help definition. Returns nil for files that are // not help topics (no name at all). func parseHelpDef(data []byte) *HelpDef { var raw helpRaw if err := yaml.Unmarshal(data, &raw); err != nil { return nil } if raw.Name == "" { return nil } return &HelpDef{ Name: raw.Name, Aliases: raw.Aliases, Related: raw.Related, Category: raw.Category, Content: raw.Description, } } func (g *Game) doHelp(sess *net.Session, topic string) { if topic == "" { unicode := true wrapWidth := 0 if p := sess.Player; p != nil { unicode = p.OptionBool("unicode") wrapWidth = p.OptionInt("wrap_width") } sess.WriteLine("") t := &ui.Table{ Title: "Commands", Columns: []string{"Command", "Type", "Description"}, } commandSpec := g.resolveColor(sess, "command") for _, c := range commandList { name := color.Render(g.colorMode(sess), commandSpec, c.Name) t.Rows = append(t.Rows, []string{name, c.Type, c.Desc}) } for _, line := range t.Render(unicode, wrapWidth) { sess.WriteLine(line) } sess.WriteLine("") sess.WriteLine("Command types: Instant (runs immediately), Free (queued before active),") sess.WriteLine("Active (replaces current action, queued per tick).") sess.WriteLine("") sess.WriteLine("Use 'help ' for detailed usage of a specific command.") sess.WriteLine("Use 'help casino' for quick guides to casino bets, odds and payouts.") sess.WriteLine("Use 'option' to view and change settings, 'color' to customize colors.") return } helps, err := LoadHelp(g.DataDir) if err != nil || len(helps) == 0 { sess.WriteLine("No help available.") return } topic = strings.ToLower(strings.TrimSpace(topic)) for _, h := range helps { if matchesHelpTopic(h, topic) { sess.WriteLine(fmt.Sprintf(" %s", h.Content)) if len(h.Related) > 0 { sess.WriteLine("") sess.WriteLine(fmt.Sprintf(" Related topics: %s", strings.Join(h.Related, ", "))) } return } } // No dedicated help page. If it's a known command, at least show its // summary from the command table instead of a dead end. for _, c := range commandList { for _, name := range strings.Split(c.Name, " / ") { if strings.ToLower(strings.TrimSpace(name)) == topic { sess.WriteLine(fmt.Sprintf(" %s - %s", c.Name, c.Desc)) sess.WriteLine("") sess.WriteLine(" (No detailed help page for this command yet.)") return } } } sess.WriteLine(fmt.Sprintf("No help found for '%s'.", topic)) } // matchesHelpTopic reports whether the help entry answers the given topic, // matching its name or any declared alias (case-insensitive). func matchesHelpTopic(h HelpDef, topic string) bool { if strings.EqualFold(h.Name, topic) { return true } for _, a := range h.Aliases { if strings.EqualFold(strings.TrimSpace(a), topic) { return true } } return false } func (g *Game) executeHelp(sess *net.Session, args []string, rawInput string) { if len(args) == 0 { g.doHelp(sess, "") } else { g.doHelp(sess, strings.Join(args, " ")) } }