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 }