a00bd620a1
Self-hostable game-server control panel: controller + agent + 26 game modules. One-line install (prebuilt release, no Go required): curl -fsSL https://git.pdxtechs.com/dbledeez/panel/raw/branch/main/install.sh | sudo bash Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
190 lines
5.9 KiB
Go
190 lines
5.9 KiB
Go
package regionmedic
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/flate"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
)
|
|
|
|
// Format constants for the 7DTD ".7rg" region file.
|
|
const (
|
|
SectorSize = 4096
|
|
RegionChunks = 1024 // 32 x 32
|
|
RegionSide = 32
|
|
headerSectors = 2 // sector 0 = header, sector 1 = location table
|
|
locTableOffset = 1 * SectorSize
|
|
chunkConstVal = 47
|
|
)
|
|
|
|
var (
|
|
regionMagic = []byte{0x37, 0x72, 0x67, 0x01} // "7rg" + version 1
|
|
chunkMagic = []byte{0x74, 0x74, 0x63, 0x00} // "ttc\0"
|
|
)
|
|
|
|
// RegionReport is the outcome of validating a single .7rg image.
|
|
type RegionReport struct {
|
|
Path string `json:"path,omitempty"`
|
|
SizeBytes int64 `json:"size_bytes"`
|
|
TotalSectors int `json:"total_sectors"`
|
|
PresentChunks int `json:"present_chunks"`
|
|
BadChunks []BadChunk `json:"bad_chunks,omitempty"`
|
|
// Err is set for a fatal, file-level problem (bad magic, non-sector-aligned
|
|
// size, truncated tables) that makes the image unusable as a region at all.
|
|
Err string `json:"err,omitempty"`
|
|
}
|
|
|
|
// Healthy reports whether the region image is structurally sound: no fatal
|
|
// error and no individually bad chunk.
|
|
func (r RegionReport) Healthy() bool { return r.Err == "" && len(r.BadChunks) == 0 }
|
|
|
|
// BadChunk describes one structurally invalid chunk within a region.
|
|
type BadChunk struct {
|
|
Index int `json:"index"`
|
|
LocalX int `json:"local_x"`
|
|
LocalZ int `json:"local_z"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
|
|
// Reasons reported on BadChunk.Reason.
|
|
const (
|
|
ReasonWrongHeader = "wrong chunk header" // the "Wrong chunk header!" trigger
|
|
ReasonBadConst = "bad format constant"
|
|
ReasonOutOfBounds = "sector out of bounds"
|
|
ReasonOverlap = "overlapping sectors"
|
|
ReasonShortRead = "blob truncated"
|
|
ReasonBadLength = "implausible block length"
|
|
ReasonDeflate = "deflate error"
|
|
ReasonCoordMismatch = "embedded coord mismatch"
|
|
)
|
|
|
|
// ValidateRegionFile reads path and validates it. deep=true additionally
|
|
// raw-inflates every present chunk and checks its embedded coordinates.
|
|
func ValidateRegionFile(path string, deep bool) RegionReport {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return RegionReport{Path: path, Err: fmt.Sprintf("read: %v", err)}
|
|
}
|
|
r := ValidateRegionBytes(data, deep)
|
|
r.Path = path
|
|
return r
|
|
}
|
|
|
|
// ValidateRegionBytes validates a .7rg image held in memory.
|
|
//
|
|
// Structural checks (always): sector-aligned size; "7rg" magic; empty header
|
|
// remainder; every present chunk's location entry in-bounds and non-overlapping;
|
|
// each chunk blob's "ttc\0" magic + format constant + plausible block length.
|
|
// deep=true also inflates each chunk and verifies the decompressed payload's
|
|
// chunkX/chunkZ match the table index.
|
|
func ValidateRegionBytes(data []byte, deep bool) RegionReport {
|
|
rep := RegionReport{SizeBytes: int64(len(data))}
|
|
|
|
if len(data) == 0 || len(data)%SectorSize != 0 {
|
|
rep.Err = fmt.Sprintf("size %d is not a positive multiple of %d", len(data), SectorSize)
|
|
return rep
|
|
}
|
|
total := len(data) / SectorSize
|
|
rep.TotalSectors = total
|
|
if total < headerSectors {
|
|
rep.Err = "file shorter than header+location table"
|
|
return rep
|
|
}
|
|
if !bytes.Equal(data[0:4], regionMagic) {
|
|
rep.Err = fmt.Sprintf("bad magic % x (want % x)", data[0:4], regionMagic)
|
|
return rep
|
|
}
|
|
// Header remainder must be empty (cheap corruption tripwire).
|
|
for _, b := range data[4:SectorSize] {
|
|
if b != 0 {
|
|
rep.Err = "non-zero bytes in header block"
|
|
return rep
|
|
}
|
|
}
|
|
|
|
used := make([]bool, total)
|
|
used[0] = true // header
|
|
used[1] = true // location table
|
|
|
|
for idx := 0; idx < RegionChunks; idx++ {
|
|
e := data[locTableOffset+idx*4 : locTableOffset+idx*4+4]
|
|
off := int(e[0]) | int(e[1])<<8 | int(e[2])<<16
|
|
cnt := int(e[3])
|
|
if off == 0 && cnt == 0 {
|
|
continue // absent chunk — legitimate
|
|
}
|
|
rep.PresentChunks++
|
|
lx, lz := idx%RegionSide, idx/RegionSide
|
|
bad := func(reason string) { rep.BadChunks = append(rep.BadChunks, BadChunk{Index: idx, LocalX: lx, LocalZ: lz, Reason: reason}) }
|
|
|
|
if off < headerSectors || cnt < 1 || off+cnt > total {
|
|
bad(ReasonOutOfBounds)
|
|
continue
|
|
}
|
|
overlap := false
|
|
for s := off; s < off+cnt; s++ {
|
|
if used[s] {
|
|
overlap = true
|
|
break
|
|
}
|
|
}
|
|
if overlap {
|
|
bad(ReasonOverlap)
|
|
continue
|
|
}
|
|
for s := off; s < off+cnt; s++ {
|
|
used[s] = true
|
|
}
|
|
|
|
base := off * SectorSize
|
|
capacity := cnt * SectorSize
|
|
// Need at least the 24-byte blob header.
|
|
if capacity < 24 {
|
|
bad(ReasonShortRead)
|
|
continue
|
|
}
|
|
blockLen := int(binary.LittleEndian.Uint32(data[base : base+4]))
|
|
// blob occupies 16 + blockLen bytes; must fit the allocated sectors.
|
|
if blockLen < 8 || 16+blockLen > capacity {
|
|
bad(ReasonBadLength)
|
|
continue
|
|
}
|
|
if !bytes.Equal(data[base+16:base+20], chunkMagic) {
|
|
bad(ReasonWrongHeader) // <-- the "Wrong chunk header!" failure
|
|
continue
|
|
}
|
|
if binary.LittleEndian.Uint32(data[base+20:base+24]) != chunkConstVal {
|
|
bad(ReasonBadConst)
|
|
continue
|
|
}
|
|
if deep {
|
|
// Deep check: confirm the chunk's raw DEFLATE stream actually
|
|
// inflates. A torn chunk whose "ttc" header happens to survive but
|
|
// whose compressed body is garbage is caught here. The DEFLATE
|
|
// stream is exactly blockLen-8 bytes starting at base+24.
|
|
//
|
|
// NOTE: we deliberately do NOT compare the decompressed payload's
|
|
// embedded coordinates. Real pregenerated (RWG-baked) regions store
|
|
// those in a layout that does not match a naive global-coord
|
|
// assumption, so a coord comparison false-positives on perfectly
|
|
// healthy regions — which would make every backup look corrupt and
|
|
// block healing. The "ttc" magic check above is exactly what 7DTD
|
|
// itself validates for "Wrong chunk header!", so structural validity
|
|
// is the trustworthy signal.
|
|
compLen := blockLen - 8
|
|
start := base + 24
|
|
if start+compLen > len(data) {
|
|
bad(ReasonShortRead)
|
|
continue
|
|
}
|
|
if _, err := io.ReadAll(flate.NewReader(bytes.NewReader(data[start : start+compLen]))); err != nil {
|
|
bad(ReasonDeflate)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
return rep
|
|
}
|