Files
panel/pkg/regionmedic/sevenrg.go
T
dbledeez a00bd620a1 Panel — public source drop (v0.9.0)
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>
2026-07-14 21:17:39 -07:00

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
}