panel v0.9.2 — public release
Self-hostable game server control panel: Go controller + agent, 26 game modules, embedded web UI. One-line install via install.sh / install.ps1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user