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? (n/s/e/w/ne/nw/se/sw/u/d)") return } dir := g.World.ResolveExit(args[0]) if dir == "" { sess.WriteLine("Invalid direction. Use n/s/e/w/ne/nw/se/sw/u/d.") 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)) oldRoom := p.RoomID p.ClearMoveState() if g.Combat.Get(p.Name) != nil { g.stopCombat(p.Name) } p.Action = nil g.cancelRest(p.Name) g.cancelSequences(p.Name) if ss, ok := g.safespot.Get(p.Name); ok { g.forceLeaveSafespot(sess, p, &ss, "") } p.RoomID = conflictID p.HazardTimer = 0 p.Stats.RecordRoomVisit(conflictID) g.AccountStore.SaveCharacter(p) g.World.SeedGroundItems(conflictID) g.seedRoomMobs(conflictID) g.seedRoomObjects(conflictID) 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, conflictID) for _, other := range g.Hub.PlayersInRoom(conflictID) { if other != sess { other.WriteLine(fmt.Sprintf("%s appears from a freshly dug tunnel.", p.Name)) } } } sess.WriteLine(fmt.Sprintf("You step through the new link to %s.", conflictName)) g.doLook(sess) g.runEnterSteps(sess, conflictID) g.checkAggro(sess) 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 = fmt.Sprintf("Room #%d", newID) } 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.cancelSequences(p.Name) 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 } } } func (g *Game) buildGridFrom(fromRoomID int) (coord map[int][3]int, roomAt map[[3]int]int) { roomIndex := g.World.RoomIndex() rg := world.BuildGrid(fromRoomID, func(id int) (*world.Room, bool) { r, err := g.World.LoadRoom(id) if err != nil { return nil, false } return r, true }, func(id int) bool { return roomIndex[id] }, nil, nil, ) return rg.Coord, rg.RoomAt } func (g *Game) findGridConflict(fromRoomID int, dir world.ExitDir) int { delta, ok := world.DirectionDeltas3D[dir] if !ok { return 0 } _, roomAt := g.buildGridFrom(fromRoomID) targetCoord := [3]int{delta[0], delta[1], delta[2]} if occupier, ok := roomAt[targetCoord]; ok && occupier != fromRoomID { return occupier } return 0 }