package game import ( "fmt" "sort" "strings" "sync" "time" "thehouseoficarus/internal/combat" "thehouseoficarus/internal/config" "thehouseoficarus/internal/engine" "thehouseoficarus/internal/game/hacking" "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" "thehouseoficarus/internal/object" "thehouseoficarus/internal/player" "thehouseoficarus/internal/world" ) type CommandClass int const ( ClassInstant CommandClass = iota ClassFree ClassActive ClassUnknown ) // Deps holds the long-lived data stores and engines a Game depends on. They are // constructed once at startup and never replaced. Deps is embedded in Game, so // game code accesses them directly (e.g. g.World, g.ItemStore). type Deps struct { World *world.World ObjectStore *object.ObjectStore ItemStore *item.ItemStore AccountStore *player.AccountStore MobStore *world.MobStore CraftIndex *CraftIndex CourseStore *CourseStore Ticks *engine.Engine ColorConfig *config.ColorsConfig ConstantColorConfig *config.ColorsConfig DataDir string } // Game is the central orchestrator: it owns the data stores (via the embedded // Deps), the shared mutable game state (global flags, combat tracker, command queue, // safespots), and the per-session runtime bookkeeping. type Game struct { Deps Hub *net.Hub GlobalFlags *GlobalFlagStore Combat *combat.Tracker flagIndex *flagTriggerIndex queue *CommandQueue safespot *SafespotManager ValidationConfig config.ValidationConfig StartingRoom int // Per-tick / per-session runtime bookkeeping. charsMu sync.Mutex loggedInChars map[string]*net.Session restTimers map[string]uint64 guardWatchTimers map[string]int hackingStates map[string]*hacking.Session pendingDepletions []pendingDepletion farmTickCounter int seqMu sync.Mutex sequences map[string]*sequence globalSeqs []*sequence shutdownCancel chan struct{} shutdownActive bool shutdownMu sync.Mutex } func New(dataDir string, colorConfig *config.ColorsConfig, constantColorConfig *config.ColorsConfig, valConfig config.ValidationConfig, startingRoom int) *Game { g := &Game{ Deps: Deps{ World: world.New(dataDir), ObjectStore: object.NewObjectStore(dataDir), ItemStore: item.NewItemStore(dataDir), AccountStore: player.NewAccountStore(dataDir), MobStore: world.NewMobStore(dataDir), CraftIndex: NewCraftIndex(), CourseStore: NewCourseStore(dataDir), Ticks: engine.New(), ColorConfig: colorConfig, ConstantColorConfig: constantColorConfig, DataDir: dataDir, }, GlobalFlags: NewGlobalFlagStore(), Combat: combat.NewTracker(), flagIndex: newFlagTriggerIndex(), queue: NewCommandQueue(), safespot: NewSafespotManager(), ValidationConfig: valConfig, StartingRoom: startingRoom, loggedInChars: make(map[string]*net.Session), restTimers: make(map[string]uint64), guardWatchTimers: make(map[string]int), hackingStates: make(map[string]*hacking.Session), sequences: make(map[string]*sequence), } g.CourseStore.SetWorld(g.World) g.CourseStore.LoadAll() g.LoadMods() g.LoadTechs() g.buildCraftIndex() g.loadAllFlagTriggers() g.ValidateAndLog() return g } func (g *Game) SetHub(hub *net.Hub) { g.Hub = hub hub.OnRemove(func(sess *net.Session) { if p := sess.Player; p != nil { g.restoreGodPlayer(p) g.persistLockedOnDisconnect(p) g.AccountStore.SaveCharacter(p) p.DeactivateAllTechs() g.charsMu.Lock() delete(g.loggedInChars, p.Name) g.charsMu.Unlock() delete(g.hackingStates, p.Name) if ss, ok := g.safespot.Get(p.Name); ok { g.forceLeaveSafespot(sess, p, &ss, "") } } }) g.GlobalFlags.OnChange(func(name string, value any) { g.fireGlobalFlagTriggers(name, value) }) } 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.handleNewPass(sess, input) case net.StateNewAccount: g.handleNewAccount(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.StateChangeDesc: g.handleDescChange(sess, input) case net.StateTalk, net.StateTalkSequence: g.handleTalkInput(sess, input) case net.StateBank: g.handleBankInput(sess, input) case net.StateDropAllConfirm: g.handleDropAll(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) case net.StateHacking: g.handleHackingInput(sess, input) case net.StateDangerConfirm: g.handleDangerConfirm(sess, input) case net.StateUndigConfirm: g.handleUndigConfirm(sess, input) } } func (g *Game) writePrompt(sess *net.Session) { sess.WritePrompt(g.promptStr(sess)) } func (g *Game) reprompt(sess *net.Session) { sess.Reprompt(g.promptStr(sess)) } func (g *Game) handleGameCommand(sess *net.Session, input string) { if input == "" { g.reprompt(sess) return } if sess.Player.OptionString("prompt_break") == "off" { sess.ClearPrompt() } 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 if p == nil { return } if class == ClassFree { g.queue.EnqueueFree(p.Name, QueuedCommand{ Session: sess, Command: cmd, Args: strings.Join(parts[1:], " "), Timestamp: time.Now(), }) if p.OptionBool("show_queued_cmds") { sess.WriteLine(fmt.Sprintf("You prepare to %s.", cmd)) } return } if class == ClassUnknown { sess.WriteLine("Unknown command.") g.writePrompt(sess) return } g.queue.EnqueueActive(p.Name, QueuedCommand{ Session: sess, Command: cmd, Args: strings.Join(parts[1:], " "), Timestamp: time.Now(), }) g.cancelRest(p.Name) if p.OptionBool("show_queued_cmds") { sess.WriteLine(fmt.Sprintf("You prepare to %s.", cmd)) } } func (g *Game) ProcessQueuedCommands() { if g.Hub == nil { return } for _, sess := range g.Hub.AllSessions() { p := sess.Player if p == nil { continue } cmds := g.queue.DrainFree(p.Name) for _, qc := range cmds { g.cancelRest(p.Name) raw := qc.Command if qc.Args != "" { raw += " " + qc.Args } parts := strings.Fields(raw) if len(parts) > 0 { g.executeCommand(qc.Session, parts[0], parts[1:], raw) } if p.BackgroundAction == nil { g.writePrompt(qc.Session) } } if len(p.WalkSequence) > 0 { g.advanceWalk(sess, p) if len(p.WalkSequence) == 0 && p.MoveTicks == 0 { g.writePrompt(sess) } } } actives := g.queue.DrainActive() sort.Slice(actives, func(i, j int) bool { return actives[i].Timestamp.Before(actives[j].Timestamp) }) for _, qc := range actives { raw := qc.Command if qc.Args != "" { raw += " " + qc.Args } parts := strings.Fields(raw) p := qc.Session.Player if p == nil { continue } if p.MoveTicks > 0 { p.ClearMoveState() } if len(parts) > 0 { g.executeCommand(qc.Session, parts[0], parts[1:], raw) } ss, _ := g.safespot.Get(p.Name) isHiding := ss.Active isBusy := p.Action != nil || len(p.WalkSequence) > 0 || g.Combat.Get(p.Name) != nil || p.MoveTicks > 0 || isHiding _, isResting := g.restTimers[p.Name] if !isResting && !isBusy && qc.Session.State == net.StateGame { g.writePrompt(qc.Session) } } g.flushPendingDepletions() } func (g *Game) buildCraftIndex() { items, err := g.ItemStore.LoadAll() if err != nil { return } g.CraftIndex.Build(items) } type pendingDepletion struct { instanceKey string objDefID string targetName string playerNames []string }