Files
panel/pkg/regionmedic/scan.go
T
2026-07-14 23:37:25 -07:00

102 lines
2.9 KiB
Go

package regionmedic
import (
"os"
"path/filepath"
"sort"
)
// ChunkCoord is a global chunk coordinate.
type ChunkCoord struct {
X int `json:"x"`
Z int `json:"z"`
}
// AffectedRegion is one region with corruption evidence (error_backup salvage
// files whose chunks fall inside it), plus whether its live region file exists.
type AffectedRegion struct {
Region RegionCoord `json:"region"`
ErrorBackups int `json:"error_backups"`
Chunks []ChunkCoord `json:"chunks,omitempty"`
RegionFile string `json:"region_file,omitempty"` // absolute path if present
FilePresent bool `json:"file_present"`
}
// RegionDirScan is the result of scanning a 7DTD save Region/ directory.
type RegionDirScan struct {
Dir string `json:"dir"`
ErrorBackups int `json:"error_backups"` // total salvage files
ErrorBackupDirs int `json:"error_backup_chunks"` // distinct chunks
Affected []AffectedRegion `json:"affected"` // sorted by count desc
}
// ScanRegionDir lists error_backup_* salvage files in dir, maps each to its
// region, and clusters them. The returned Affected slice is sorted by
// ErrorBackups descending (most-damaged region first). A region appears only if
// it has at least one error_backup chunk.
func ScanRegionDir(dir string) (RegionDirScan, error) {
out := RegionDirScan{Dir: dir}
ents, err := os.ReadDir(dir)
if err != nil {
return out, err
}
// region -> set of distinct chunk coords (dedup .comp/.uncomp pairs)
type acc struct {
files int
chunks map[ChunkCoord]struct{}
}
clusters := map[RegionCoord]*acc{}
for _, e := range ents {
if e.IsDir() {
continue
}
cx, cz, ok := ParseErrorBackup(e.Name())
if !ok {
continue
}
out.ErrorBackups++
rc := RegionForChunk(cx, cz)
a := clusters[rc]
if a == nil {
a = &acc{chunks: map[ChunkCoord]struct{}{}}
clusters[rc] = a
}
a.files++
a.chunks[ChunkCoord{X: cx, Z: cz}] = struct{}{}
}
for rc, a := range clusters {
ar := AffectedRegion{Region: rc, ErrorBackups: a.files}
for c := range a.chunks {
ar.Chunks = append(ar.Chunks, c)
}
sort.Slice(ar.Chunks, func(i, j int) bool {
if ar.Chunks[i].X != ar.Chunks[j].X {
return ar.Chunks[i].X < ar.Chunks[j].X
}
return ar.Chunks[i].Z < ar.Chunks[j].Z
})
out.ErrorBackupDirs += len(ar.Chunks)
rf := filepath.Join(dir, rc.FileName())
if st, err := os.Stat(rf); err == nil && !st.IsDir() {
ar.RegionFile = rf
ar.FilePresent = true
}
out.Affected = append(out.Affected, ar)
}
sort.Slice(out.Affected, func(i, j int) bool {
if out.Affected[i].ErrorBackups != out.Affected[j].ErrorBackups {
return out.Affected[i].ErrorBackups > out.Affected[j].ErrorBackups
}
// stable tiebreak by region id
if out.Affected[i].Region.X != out.Affected[j].Region.X {
return out.Affected[i].Region.X < out.Affected[j].Region.X
}
return out.Affected[i].Region.Z < out.Affected[j].Region.Z
})
return out, nil
}