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 }