package game import ( "fmt" "os" "path/filepath" "regexp" "strconv" "strings" "gopkg.in/yaml.v3" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" "thehouseoficarus/internal/world" ) func (g *Game) executeUndig(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("Undig which direction?") 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 } curRoom, err := g.World.LoadRoom(p.RoomID) if err != nil { sess.WriteLine("Error loading current room.") return } exitDef, ok := curRoom.Exits[dir] if !ok { sess.WriteLine(fmt.Sprintf("There is no exit to the %s from here.", dir)) return } targetID := exitDef.Room targetRoom, err := g.World.LoadRoom(targetID) if err != nil { sess.WriteLine(fmt.Sprintf("Target room %d not found.", targetID)) return } sess.PendingUndigDir = string(dir) sess.PendingUndigRoom = targetID sess.State = net.StateUndigConfirm sess.WriteLine(fmt.Sprintf("You are about to DELETE Room #%d (%s) and all exits leading to it.", targetID, targetRoom.Name)) g.listUndigOrphans(sess, targetID) sess.WriteLine("") sess.WriteLine("Undig? [y/N]") } func (g *Game) listUndigOrphans(sess *net.Session, targetID int) { roomIndex := g.World.RoomIndex() inbound := make(map[int]bool) roomsDir := filepath.Join(g.DataDir, "rooms") re := regexp.MustCompile(fmt.Sprintf(`\b%d\b`, targetID)) _ = filepath.WalkDir(roomsDir, func(path string, d os.DirEntry, err error) error { if err != nil || d.IsDir() || filepath.Ext(path) != ".yaml" { return nil } data, rerr := os.ReadFile(path) if rerr != nil { return nil } if re.MatchString(string(data)) { idStr := strings.TrimSuffix(filepath.Base(path), ".yaml") if rid, serr := strconv.Atoi(idStr); serr == nil && rid != targetID { inbound[rid] = true } } return nil }) if len(inbound) == 0 { return } var orphans []int for rid := range inbound { room, err := g.World.LoadRoom(rid) if err != nil { continue } hasOtherExit := false for _, exitDef := range room.Exits { if roomIndex[exitDef.Room] && exitDef.Room != targetID { hasOtherExit = true break } } if !hasOtherExit { orphans = append(orphans, rid) } } if len(orphans) > 0 { sess.WriteLine(g.colorize(sess, "warning", fmt.Sprintf("WARNING: The following rooms will become unreachable after deletion:"))) for _, rid := range orphans { room, err := g.World.LoadRoom(rid) name := fmt.Sprintf("#%d", rid) if err == nil { name = fmt.Sprintf("#%d (%s)", rid, room.Name) } sess.WriteLine(fmt.Sprintf(" %s", name)) } } } func (g *Game) handleUndigConfirm(sess *net.Session, input string) { p := sess.Player if p == nil { sess.State = net.StateGame g.writePrompt(sess) return } choice := strings.ToLower(strings.TrimSpace(input)) dir := sess.PendingUndigDir targetID := sess.PendingUndigRoom sess.PendingUndigDir = "" sess.PendingUndigRoom = 0 sess.State = net.StateGame if dir == "" || targetID == 0 { g.writePrompt(sess) return } if choice != "y" && choice != "yes" { sess.WriteLine("Undig cancelled.") g.writePrompt(sess) return } g.performUndig(sess, p, targetID) g.writePrompt(sess) } func (g *Game) performUndig(sess *net.Session, p *player.Player, targetID int) { targetRoom, err := g.World.LoadRoom(targetID) roomName := fmt.Sprintf("#%d", targetID) if err == nil { roomName = fmt.Sprintf("#%d (%s)", targetID, targetRoom.Name) } else { sess.WriteLine(fmt.Sprintf("Target room %d no longer exists.", targetID)) return } roomsDir := filepath.Join(g.DataDir, "rooms") re := regexp.MustCompile(fmt.Sprintf(`\b%d\b`, targetID)) var cleaned int _ = filepath.WalkDir(roomsDir, func(path string, d os.DirEntry, err error) error { if err != nil || d.IsDir() || filepath.Ext(path) != ".yaml" { return nil } data, rerr := os.ReadFile(path) if rerr != nil { return nil } if !re.MatchString(string(data)) { return nil } idStr := strings.TrimSuffix(filepath.Base(path), ".yaml") rid, serr := strconv.Atoi(idStr) if serr != nil || rid == targetID { return nil } var room world.Room if yerr := yaml.Unmarshal(data, &room); yerr != nil { return nil } if room.Exits == nil { return nil } changed := false for ed, exitDef := range room.Exits { if exitDef.Room == targetID { delete(room.Exits, ed) changed = true } } if changed { newData, merr := yaml.Marshal(&room) if merr != nil { return nil } if werr := os.WriteFile(path, newData, 0644); werr != nil { return werr } cleaned++ } return nil }) targetPath, ok := g.World.GetRoomPath(targetID) if !ok { sess.WriteLine(fmt.Sprintf("Room file for %d not found.", targetID)) return } if rerr := os.Remove(targetPath); rerr != nil { sess.WriteLine(fmt.Sprintf("Error deleting room file: %v", rerr)) return } g.World.RebuildRoomIndex(g.DataDir) if p.RoomID == targetID { p.RoomID = 0 } for _, other := range g.Hub.AllSessions() { if other.Player != nil && other.Player.RoomID == targetID { other.Player.RoomID = p.RoomID if g.Hub != nil { g.Hub.EnterRoom(other, p.RoomID) } other.WriteLine(fmt.Sprintf("The room around you dissolves. You find yourself elsewhere.")) g.doLook(other) } } g.MobStore.RemoveMobsInRoom(targetID) g.World.ClearRoomState(targetID) sess.WriteLine(fmt.Sprintf("Deleted room %s and %d exit(s) pointing to it.", roomName, cleaned)) }