295eb22826
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
102 lines
2.9 KiB
Go
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
|
|
}
|