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 { p.DeactivateAllTechs() 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.StateRecipeChoice: g.handleRecipeChoice(sess, input) case net.StateHowMany: g.handleHowMany(sess, input) case net.StateSmithProduct, net.StateFletchProduct, net.StateCraftProduct, net.StateProductChoice: g.handleProductChoice(sess, input) case net.StateColorChoice: g.handleColorChoice(sess, input) } } func classifyCommand(cmd string) CommandClass { switch cmd { case "say", "score", "sc", "inventory", "i", "inv", "look", "l", "exits", "help", "map", "option", "options", "alias", "unalias", "description", "desc", "queued", "color", "colors", "colortable", "prompt", "style", "stats", "tech", "t": return ClassInstant case "equip", "eq", "equipment", "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", "smelt", "smith", "craft", "mix", "id", "identify": return ClassActive case "eat", "fletch", "clean": 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] if len(parts) == 1 { switch cmd { case "equip", "eq", "equipment", "wear", "wield": g.doEquipment(sess) g.writePrompt(sess) return } } 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 "stats": g.doStats(sess) case "tech", "t": g.doTech(sess, strings.Join(args, " ")) case "i", "inv", "inventory": g.doInventory(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 "smelt": g.doSmelt(sess, strings.Join(args, " ")) return case "smith": g.doSmith(sess, strings.Join(args, " ")) return case "craft": g.doCraft(sess, strings.Join(args, " ")) return case "eat": g.doEat(sess, strings.Join(args, " ")) return case "fletch": g.doFletch(sess, strings.Join(args, " ")) return case "clean": g.doClean(sess, strings.Join(args, " ")) return case "mix": g.doMix(sess, strings.Join(args, " ")) return case "id", "identify": g.doIdentify(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 "eq", "equipment", "equip", "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, ActionProducing: 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 } if p.BackgroundAction == nil { p.BackgroundActionState = 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() } 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 && !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) } } }