From a1b6613c6c6a2f8d6e1ecd47e766bd40f92ac295 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Sun, 28 Jun 2026 02:40:30 -0400 Subject: feat: admin accounts, god mode, OLC commands like dig/room, 'inline' objects changed to 'local' objects --- internal/game/cmd_dig.go | 281 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100644 internal/game/cmd_dig.go (limited to 'internal/game/cmd_dig.go') diff --git a/internal/game/cmd_dig.go b/internal/game/cmd_dig.go new file mode 100644 index 0000000..e7023b2 --- /dev/null +++ b/internal/game/cmd_dig.go @@ -0,0 +1,281 @@ +package game + +import ( + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + + "gopkg.in/yaml.v3" + "thehouseoficarus/internal/behavior" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/world" +) + +func (g *Game) executeDig(sess *net.Session, args []string, rawInput string) { + if !g.checkAdmin(sess) { + sess.WriteLine("Unknown command.") + return + } + p := sess.Player + if p == nil { + return + } + if len(args) == 0 { + sess.WriteLine("Dig where? (north/south/east/west/up/down)") + return + } + + dir := g.World.ResolveExit(args[0]) + if dir == "" { + sess.WriteLine("Invalid direction. Use north/south/east/west/up/down.") + return + } + + curPath, ok := g.World.GetRoomPath(p.RoomID) + if !ok { + sess.WriteLine("Error: can't find current room file.") + return + } + + curRoom, err := g.World.LoadRoom(p.RoomID) + if err != nil { + sess.WriteLine("Error loading current room.") + return + } + if _, hasExit := curRoom.Exits[dir]; hasExit { + sess.WriteLine(fmt.Sprintf("There is already an exit to the %s.", dir)) + return + } + + if conflictID := g.findGridConflict(p.RoomID, dir); conflictID != 0 { + conflictRoom, err := g.World.LoadRoom(conflictID) + conflictName := fmt.Sprintf("#%d", conflictID) + if err == nil { + conflictName = fmt.Sprintf("#%d (%s)", conflictID, conflictRoom.Name) + } + + oppositeDir := world.OppositeExit[dir] + curRoom.Exits[dir] = world.ExitDef{Room: conflictID} + curData, err := yaml.Marshal(curRoom) + if err != nil { + sess.WriteLine("Error updating current room YAML.") + return + } + if err := os.WriteFile(curPath, curData, 0644); err != nil { + sess.WriteLine("Error writing current room file.") + return + } + + if conflictRoom != nil { + if _, exists := conflictRoom.Exits[oppositeDir]; !exists { + conflictRoom.Exits[oppositeDir] = world.ExitDef{Room: p.RoomID} + conflictPath, ok := g.World.GetRoomPath(conflictID) + if ok { + conflictData, cerr := yaml.Marshal(conflictRoom) + if cerr == nil { + os.WriteFile(conflictPath, conflictData, 0644) + } + } + } + } + + sess.WriteLine(fmt.Sprintf("That spot is already occupied by %s. Created a two-way link instead.", conflictName)) + return + } + + newID, err := findNextRoomID(curPath) + if err != nil { + sess.WriteLine("Error scanning room directory.") + return + } + + var name string + if len(args) > 1 { + name = strings.Join(args[1:], " ") + } else { + name = "New Room" + } + + oppositeDir := world.OppositeExit[dir] + dirPath := filepath.Dir(curPath) + newPath := filepath.Join(dirPath, strconv.Itoa(newID)+".yaml") + + newRoom := &world.Room{ + Name: name, + Description: behavior.DescList{ + {Text: "A featureless room."}, + }, + Exits: map[world.ExitDir]world.ExitDef{ + oppositeDir: {Room: p.RoomID}, + }, + } + + data, err := yaml.Marshal(newRoom) + if err != nil { + sess.WriteLine("Error creating room YAML.") + return + } + if err := os.MkdirAll(dirPath, 0755); err != nil { + sess.WriteLine("Error creating directory.") + return + } + if err := os.WriteFile(newPath, data, 0644); err != nil { + sess.WriteLine("Error writing room file.") + return + } + + g.World.AddRoomPath(newID, newPath) + + curRoom.Exits[dir] = world.ExitDef{Room: newID} + curData, err := yaml.Marshal(curRoom) + if err != nil { + sess.WriteLine("Error updating current room YAML.") + return + } + if err := os.WriteFile(curPath, curData, 0644); err != nil { + sess.WriteLine("Error writing current room file.") + return + } + + oldRoom := p.RoomID + p.ClearMoveState() + if g.Combat.Get(p.Name) != nil { + g.stopCombat(p.Name) + } + p.Action = nil + g.cancelRest(p.Name) + g.cancelEnterSeq(p.Name) + if p.EnterSeqRoom != 0 && p.EnterSeqRoom == oldRoom { + p.EnterSeqRoom = 0 + } + if ss, ok := g.safespot.Get(p.Name); ok { + g.forceLeaveSafespot(sess, p, &ss, "") + } + + p.RoomID = newID + p.HazardTimer = 0 + p.Stats.RecordRoomVisit(newID) + g.AccountStore.SaveCharacter(p) + + g.World.SeedGroundItems(newID) + g.seedRoomMobs(newID) + g.seedRoomObjects(newID) + + if g.Hub != nil { + for _, other := range g.Hub.PlayersInRoom(oldRoom) { + if other != sess { + other.WriteLine(fmt.Sprintf("%s digs to the %s and vanishes.", p.Name, dir)) + } + } + g.Hub.EnterRoom(sess, newID) + for _, other := range g.Hub.PlayersInRoom(newID) { + if other != sess { + other.WriteLine(fmt.Sprintf("%s appears from a freshly dug tunnel.", p.Name)) + } + } + } + + sess.WriteLine(fmt.Sprintf("You dig %s and create Room #%d (%s).", dir, newID, name)) + g.doLook(sess) + g.runEnterSteps(sess, newID) + g.checkAggro(sess) +} + +func findNextRoomID(currentRoomPath string) (int, error) { + dir := filepath.Dir(currentRoomPath) + entries, err := os.ReadDir(dir) + if err != nil { + return 0, err + } + + used := make(map[int]bool) + minID := 0 + first := true + for _, entry := range entries { + if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".yaml") { + continue + } + idStr := strings.TrimSuffix(entry.Name(), ".yaml") + id, err := strconv.Atoi(idStr) + if err != nil { + continue + } + used[id] = true + if first || id < minID { + minID = id + first = false + } + } + + if first { + return 1, nil + } + + for id := minID; ; id++ { + if !used[id] { + return id, nil + } + } +} + +var gridDeltas = map[world.ExitDir][2]int{ + world.North: {0, -1}, + world.South: {0, 1}, + world.East: {1, 0}, + world.West: {-1, 0}, +} + +func (g *Game) findGridConflict(fromRoomID int, dir world.ExitDir) int { + delta, ok := gridDeltas[dir] + if !ok { + return 0 + } + + roomIndex := g.World.RoomIndex() + coord := map[int][2]int{fromRoomID: {0, 0}} + roomAt := map[[2]int]int{{0, 0}: fromRoomID} + queue := []int{fromRoomID} + + for len(queue) > 0 { + rid := queue[0] + queue = queue[1:] + room, err := g.World.LoadRoom(rid) + if err != nil { + continue + } + c := coord[rid] + for _, ed := range world.ExitOrder { + exit, ok := room.Exits[ed] + if !ok || exit.Room <= 0 || !roomIndex[exit.Room] { + continue + } + target := exit.Room + if ed == world.Up || ed == world.Down { + continue + } + gd, ok := gridDeltas[ed] + if !ok { + continue + } + want := [2]int{c[0] + gd[0], c[1] + gd[1]} + + if _, exists := coord[target]; exists { + continue + } + if occupier, exists := roomAt[want]; exists && occupier != target { + continue + } + coord[target] = want + roomAt[want] = target + queue = append(queue, target) + } + } + + targetCoord := [2]int{delta[0], delta[1]} + if occupier, ok := roomAt[targetCoord]; ok && occupier != fromRoomID { + return occupier + } + return 0 +} -- cgit v1.2.3