package admin import ( "os" "path/filepath" "strconv" ) // nextRoomID allocates a free room ID for a new room that will live in the same // directory as fromRoomID (so new IDs cluster near their neighbors). Unlike the // old per-subdir scan, the candidate is checked against EVERY room ID on disk // (via listRoomIDs, which walks the whole data/rooms tree), so it can never // collide with an ID that lives in a different subdirectory. The starting point // is the local subdir's minimum existing ID (clustering), then the first // globally-free ID scanning upward. func (s *AdminServer) nextRoomID(fromRoomID int) (int, string, error) { base := filepath.Join(s.dataDir, "rooms") subdir, err := findRoomSubdir(base, fromRoomID) if err != nil { subdir = base } localUsed := map[int]bool{} scanDir(subdir, localUsed) start := 1 if len(localUsed) > 0 { minID := 1<<31 - 1 for id := range localUsed { if id < minID { minID = id } } start = minID } globalUsed, _ := listRoomIDs(s.dataDir) used := make(map[int]bool, len(globalUsed)) for _, id := range globalUsed { used[id] = true } id := start if id < 1 { id = 1 } for used[id] { id++ } return id, subdir, nil } func findRoomSubdir(base string, roomID int) (string, error) { fileName := strconv.Itoa(roomID) + ".yaml" var found string filepath.WalkDir(base, func(path string, d os.DirEntry, err error) error { if err != nil || found != "" { return nil } if !d.IsDir() && d.Name() == fileName { found = filepath.Dir(path) } return nil }) if found == "" { return "", os.ErrNotExist } return found, nil } func scanDir(dir string, used map[int]bool) { entries, err := os.ReadDir(dir) if err != nil { return } for _, e := range entries { if !e.IsDir() && filepath.Ext(e.Name()) == ".yaml" { idStr := e.Name()[:len(e.Name())-5] if id, err := strconv.Atoi(idStr); err == nil && id > 0 { used[id] = true } } } }