package game import ( "fmt" "sort" "strings" "sync" "time" "thehouseoficarus/internal/action" "thehouseoficarus/internal/combat" "thehouseoficarus/internal/config" "thehouseoficarus/internal/engine" "thehouseoficarus/internal/net" "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" "thehouseoficarus/internal/world" ) type CommandClass int const ( ClassInstant CommandClass = iota ClassFree ClassActive ClassUnknown ) type QueuedCommand struct { Session *net.Session Command string Args string Timestamp time.Time } type Game struct { World *world.World ObjectStore *object.ObjectStore ItemStore *object.ItemStore AccountStore *player.AccountStore MobStore *world.MobStore BehaviorStore *action.Store RecipeStore *action.RecipeStore Hub *net.Hub Ticks *engine.Engine WorldFlags map[string]any ColorConfig *config.ColorsConfig dataDir string restTimers map[string]uint64 charsMu sync.Mutex loggedInChars map[string]*net.Session combatPadWidth int freeQueue map[string][]QueuedCommand activeQueue map[string]*QueuedCommand consumeQueue map[string]*QueuedCommand pendingDepletions []pendingDepletion } func New(dataDir string, colorConfig *config.ColorsConfig) *Game { return &Game{ World: world.New(dataDir), ObjectStore: object.NewObjectStore(dataDir), ItemStore: object.NewItemStore(dataDir), AccountStore: player.NewAccountStore(dataDir), MobStore: world.NewMobStore(dataDir), BehaviorStore: action.NewStore(dataDir), RecipeStore: action.NewRecipeStore(dataDir), Ticks: engine.New(), WorldFlags: make(map[string]any), ColorConfig: colorConfig, dataDir: dataDir, restTimers: make(map[string]uint64), loggedInChars: make(map[string]*net.Session), freeQueue: make(map[string][]QueuedCommand), activeQueue: make(map[string]*QueuedCommand), consumeQueue: make(map[string]*QueuedCommand), pendingDepletions: nil, } } func (g *Game) SetHub(hub *net.Hub) { g.Hub = hub hub.OnRemove(func(sess *net.Session) { if p, ok := sess.Player.(*player.Player); ok { g.charsMu.Lock() delete(g.loggedInChars, p.Name) g.charsMu.Unlock() } }) } func (g *Game) HandleSession(sess *net.Session, input string) { input = player.StripControlCharacters(input) switch sess.State { case net.StateAccountName: g.handleAccountName(sess, input) case net.StatePassword: g.handlePassword(sess, input) case net.StateNewAccountPass: g.handleNewAccountPass(sess, input) case net.StateNewAccountConfirm: g.handleNewAccountConfirm(sess, input) case net.StateMenu: g.handleMenu(sess, input) case net.StateNewCharName: g.handleNewCharName(sess, input) case net.StateRenameAccount: g.handleRenameAccount(sess, input) case net.StateRenameChar: g.handleRenameChar(sess, input) case net.StateRenameCharName: g.handleRenameCharName(sess, input) case net.StateDeleteChar: g.handleDeleteChar(sess, input) case net.StatePurgeAccount: g.handlePurgeAccount(sess, input) case net.StateGame: g.handleGameCommand(sess, input) case net.StateChangeDescription: g.handleDescriptionChange(sess, input) case net.StateTalk: g.handleTalkInput(sess, input) case net.StateDropAllConfirm: g.handleDropAllConfirm(sess, input) case net.StateCookRecipe: g.handleCookRecipe(sess, input) case net.StateColorChoice: g.handleColorChoice(sess, input) } } func classifyCommand(cmd string) CommandClass { switch cmd { case "say", "score", "sc", "inventory", "i", "inv", "equipment", "eq", "look", "l", "exits", "help", "map", "option", "options", "alias", "unalias", "description", "desc", "queued", "color", "colors", "colortable", "prompt", "style": return ClassInstant case "wear", "wield", "remove", "unwear", "unwield": return ClassFree case "get", "take", "grab", "pick", "drop", "attack", "kill", "north", "n", "south", "s", "east", "e", "west", "w", "up", "u", "down", "d", "quit", "use", "burn", "stoke", "search", "walk", "cook": return ClassActive case "eat": return ClassFree } if _, ok := verbAliases[cmd]; ok { return ClassActive } return ClassUnknown } func (g *Game) writePrompt(sess *net.Session) { sess.Write("\r\n" + g.promptStr(sess)) } func (g *Game) reprompt(sess *net.Session) { sess.Write(g.promptStr(sess)) } func (g *Game) handleGameCommand(sess *net.Session, input string) { if input == "" { g.reprompt(sess) return } if sess.Account != nil && sess.Account.Aliases != nil { firstSpace := strings.Index(input, " ") var firstWord, rest string if firstSpace > 0 { firstWord = input[:firstSpace] rest = strings.TrimLeft(input[firstSpace:], " ") } else { firstWord = input } if expansion, ok := sess.Account.Aliases[strings.ToLower(firstWord)]; ok { if rest != "" { input = expansion + " " + rest } else { input = expansion } } } parts := strings.Fields(strings.ToLower(input)) cmd := parts[0] class := classifyCommand(cmd) if class == ClassInstant { g.executeCommand(sess, cmd, parts[1:], input) g.writePrompt(sess) return } p, _ := sess.Player.(*player.Player) if p == nil { return } if class == ClassFree { g.freeQueue[p.Name] = append(g.freeQueue[p.Name], QueuedCommand{ Session: sess, Command: cmd, Args: strings.Join(parts[1:], " "), Timestamp: time.Now(), }) if !p.OptionBool("queue_silently") { sess.WriteLine(fmt.Sprintf("\nYou prepare to %s.", cmd)) } return } if class == ClassUnknown { sess.WriteLine("Unknown command.") g.writePrompt(sess) return } g.activeQueue[p.Name] = &QueuedCommand{ Session: sess, Command: cmd, Args: strings.Join(parts[1:], " "), Timestamp: time.Now(), } g.cancelRest(p.Name) if !p.OptionBool("queue_silently") { sess.WriteLine(fmt.Sprintf("\nYou prepare to %s.", cmd)) } } func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawInput string) { p, _ := sess.Player.(*player.Player) switch cmd { case "get", "take", "grab", "pick": if len(args) == 0 { sess.WriteLine("Get what?") } else if args[0] == "all" { if len(args) == 1 { g.doGetAll(sess) } else { g.doGetAllNamed(sess, strings.Join(args[1:], " ")) } } else { g.doGet(sess, strings.Join(args, " ")) } case "drop": if len(args) == 0 { sess.WriteLine("Drop what?") } else if args[0] == "all" { if len(args) == 1 { g.doDropAll(sess) } else { g.doDropAllNamed(sess, strings.Join(args[1:], " ")) } } else { g.doDrop(sess, strings.Join(args, " ")) } case "attack", "kill": if len(args) == 0 { target := g.resolveDefaultMob(p.RoomID) if target == "" { sess.WriteLine("Attack what?") break } g.doAttack(sess, target) return } else { g.doAttack(sess, strings.Join(args, " ")) return } case "style": if len(args) == 0 { g.doStyle(sess, "") } else { g.doStyle(sess, args[0]) } case "look", "l": if len(args) == 0 { g.doLook(sess) } else { g.doLookTarget(sess, strings.Join(args, " ")) } case "north", "n", "south", "s", "east", "e", "west", "w", "up", "u", "down", "d": g.doMove(sess, cmd) case "say": if len(args) == 0 { sess.WriteLine("Say what?") } else { msgStart := strings.Index(strings.ToLower(rawInput), "say ") + 4 if msgStart >= 4 && msgStart < len(rawInput) { g.doSay(sess, rawInput[msgStart:]) } else { g.doSay(sess, strings.Join(args, " ")) } } case "sc", "score": g.doScore(sess) case "i", "inv", "inventory": g.doInventory(sess) case "eq", "equipment": g.doEquipment(sess) case "quit": g.doQuit(sess) return case "description", "desc": g.doDescription(sess) case "option", "options": g.doOption(sess, strings.Join(args, " ")) case "color", "colors": g.doColor(sess, strings.Join(args, " ")) case "colortable": g.doColortable(sess) case "prompt": if len(args) == 0 { g.doPrompt(sess, "") } else { msgStart := strings.Index(strings.ToLower(rawInput), "prompt ") + 7 if msgStart >= 7 && msgStart < len(rawInput) { g.doPrompt(sess, rawInput[msgStart:]) } else { g.doPrompt(sess, strings.Join(args, " ")) } } case "exits": g.doExits(sess) case "map": g.doMap(sess) case "queued": g.doQueued(sess) case "help": if len(args) == 0 { g.doHelp(sess, "") } else { g.doHelp(sess, strings.Join(args, " ")) } case "mine", "chop", "fish", "cut", "pull", "push": g.CancelAction(p) if len(args) == 0 { target := g.resolveDefaultTarget(p.RoomID, cmd) if target == "" { sess.WriteLine(fmt.Sprintf("%s what?", cmd)) break } g.StartAction(sess, cmd, target) return } else { g.StartAction(sess, cmd, strings.Join(args, " ")) return } case "use": g.doUse(sess, strings.Join(args, " ")) return case "cook": g.doCook(sess, strings.Join(args, " ")) return case "eat": g.doEat(sess, strings.Join(args, " ")) return case "talk", "speak", "ask": g.CancelAction(p) if len(args) == 0 { sess.WriteLine("Talk to whom?") } else { g.StartAction(sess, "talk", strings.Join(args, " ")) return } case "burn": g.CancelAction(p) g.doBurn(sess, p, strings.Join(args, " ")) return case "stoke": g.CancelAction(p) g.doStoke(sess, p, strings.Join(args, " ")) return case "alias": g.doAlias(sess, args) return case "unalias": g.doUnalias(sess, args) return case "search": g.CancelAction(p) if len(args) == 0 { sess.WriteLine("Search what?") } else { g.doSearch(sess, strings.Join(args, " ")) } return case "walk": g.doWalk(sess, args) return case "wear", "wield": g.doWear(sess, strings.Join(args, " ")) return case "remove", "unwear", "unwield": g.doRemove(sess, strings.Join(args, " ")) return default: if a, ok := verbAliases[cmd]; ok { g.CancelAction(p) switch a { case "gather", "toggle": if len(args) == 0 { target := g.resolveDefaultTarget(p.RoomID, cmd) if target == "" { sess.WriteLine(fmt.Sprintf("%s what?", cmd)) break } g.StartAction(sess, cmd, target) return } g.StartAction(sess, cmd, strings.Join(args, " ")) return case "talk": if len(args) == 0 { sess.WriteLine("Talk to whom?") } else { g.StartAction(sess, "talk", strings.Join(args, " ")) return } } } else { sess.WriteLine("Unknown command.") } } } func (g *Game) ProcessQueuedCommands() { if g.Hub == nil { return } for _, sess := range g.Hub.AllSessions() { p, ok := sess.Player.(*player.Player) if !ok || p == nil || p.ActionState == nil { continue } as, ok := p.ActionState.(*ActionState) if !ok { continue } switch as.Type { case ActionGathering, ActionCombating, ActionUsing, ActionTalking, ActionToggling, ActionBurning, ActionStoking, ActionResting, ActionWalking, ActionCooking: default: if as.Type != ActionMoving || p.MoveTicks <= 0 { p.ActionState = nil } } } for _, sess := range g.Hub.AllSessions() { p, ok := sess.Player.(*player.Player) if !ok || p == nil { continue } cmds := g.freeQueue[p.Name] for _, qc := range cmds { g.cancelRest(p.Name) parts := strings.Fields(qc.Command + " " + qc.Args) if len(parts) > 0 { g.executeCommand(qc.Session, parts[0], parts[1:], qc.Command+" "+qc.Args) } g.writePrompt(qc.Session) } delete(g.freeQueue, p.Name) } var actives []QueuedCommand for _, qc := range g.activeQueue { actives = append(actives, *qc) } sort.Slice(actives, func(i, j int) bool { return actives[i].Timestamp.Before(actives[j].Timestamp) }) for _, qc := range actives { parts := strings.Fields(qc.Command + " " + qc.Args) p, ok := qc.Session.Player.(*player.Player) if !ok || p == nil { continue } if p.MoveTicks > 0 { p.ClearMoveState() } wasBusy := p.Action != nil || len(p.WalkSequence) > 0 || combat.GetCombat(p.Name) != nil if len(parts) > 0 { g.executeCommand(qc.Session, parts[0], parts[1:], qc.Command+" "+qc.Args) } isBusy := p.Action != nil || len(p.WalkSequence) > 0 || combat.GetCombat(p.Name) != nil || p.MoveTicks > 0 _, isResting := g.restTimers[p.Name] if !isResting && (wasBusy || !isBusy) { g.writePrompt(qc.Session) } } g.activeQueue = make(map[string]*QueuedCommand) for _, sess := range g.Hub.AllSessions() { p, ok := sess.Player.(*player.Player) if !ok || p == nil || len(p.WalkSequence) == 0 { continue } g.advanceWalk(sess, p) if len(p.WalkSequence) == 0 && p.MoveTicks == 0 { g.writePrompt(sess) } } g.flushPendingDepletions() } type pendingDepletion struct { instanceKey string behaviorID string targetName string playerNames []string } func (g *Game) MoveTick() { if g.Hub == nil { return } for _, sess := range g.Hub.AllSessions() { p, ok := sess.Player.(*player.Player) if !ok || p == nil || p.MoveTicks <= 0 { continue } p.MoveTicks-- if p.MoveTicks > 0 { if combat.GetCombat(p.Name) != nil && p.OptionBool("run_countdown") { if p.MoveTicks > 1 { sess.WriteLine(fmt.Sprintf("Running in %d ticks...", p.MoveTicks)) } else { sess.WriteLine("Running next tick!") } } } else { g.completeMove(sess, p) g.writePrompt(sess) } } }