package game import ( "fmt" "sort" "strings" "sync" "time" "thirdcollapse/internal/action" "thirdcollapse/internal/engine" "thirdcollapse/internal/net" "thirdcollapse/internal/object" "thirdcollapse/internal/player" "thirdcollapse/internal/world" ) type CommandClass int const ( ClassInstant CommandClass = iota ClassFree ClassActive ) 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 Hub *net.Hub Ticks *engine.Engine WorldFlags map[string]any dataDir string restTimers map[string]uint64 charsMu sync.Mutex loggedInChars map[string]*net.Session combatPadWidth int GameSpeed float64 freeQueue map[string][]QueuedCommand activeQueue map[string]*QueuedCommand pendingDepletions []pendingDepletion } func New(dataDir string) *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), Ticks: engine.New(), WorldFlags: make(map[string]any), dataDir: dataDir, restTimers: make(map[string]uint64), loggedInChars: make(map[string]*net.Session), freeQueue: make(map[string][]QueuedCommand), activeQueue: 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) } } 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": return ClassInstant case "wear", "wield", "remove", "unwear", "unwield", "style": return ClassFree default: return ClassActive } } func (g *Game) handleGameCommand(sess *net.Session, input string) { if input == "" { sess.Write("> ") 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) sess.Write("\r\n> ") 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 } 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 p == nil { return } 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 "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", "use", "pull", "push": if p == nil { return } 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 "talk", "speak", "ask": if p == nil { return } g.CancelAction(p) if len(args) == 0 { sess.WriteLine("Talk to whom?") } else { g.StartAction(sess, "talk", strings.Join(args, " ")) return } case "burn": if p == nil { return } g.CancelAction(p) g.doBurn(sess, p, strings.Join(args, " ")) return case "stoke": if p == nil { return } 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": if p == nil { return } g.CancelAction(p) if len(args) == 0 { sess.WriteLine("Search what?") } else { g.doSearch(sess, strings.Join(args, " ")) } return case "wear", "wield": if p == nil { return } g.doWear(sess, strings.Join(args, " ")) return case "remove", "unwear", "unwield": if p == nil { return } g.doRemove(sess, strings.Join(args, " ")) return default: 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: default: 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) } qc.Session.Write("\r\n> ") } 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) if len(parts) > 0 { g.executeCommand(qc.Session, parts[0], parts[1:], qc.Command+" "+qc.Args) } qc.Session.Write("\r\n> ") } g.activeQueue = make(map[string]*QueuedCommand) g.flushPendingDepletions() } type pendingDepletion struct { instanceKey string behaviorID string targetName string playerNames []string } func (g *Game) computeTicks(base float64) int { return engine.FractionalTicks(base, g.GameSpeed) }