feat: add regenerateChunkFromSeed to SaveIntegrity

This commit is contained in:
dualfroz committed 2026-09-08 23:00:54 +02:00
1 parent cd9eeeddc0
commit 0753a55c07
1 file changed
+156
+156
View File
@@ -159,3 +159,159 @@ int regeneratedSurfaceHeight(uint64_t seed, int worldX, int worldZ) {
int fx = floorMod(worldX, chunkSize); int fx = floorMod(worldX, chunkSize);
int fz = floorMod(worldZ, chunkSize); int fz = floorMod(worldZ, chunkSize);
int h00 = latticeHeight(seed, gx, gz);
int h10 = latticeHeight(seed, gx + 1, gz);
int h01 = latticeHeight(seed, gx, gz + 1);
int h11 = latticeHeight(seed, gx + 1, gz + 1);
int nearEdge = h00 * (chunkSize - fx) + h10 * fx;
int farEdge = h01 * (chunkSize - fx) + h11 * fx;
return (nearEdge * (chunkSize - fz) + farEdge * fz) / (chunkSize * chunkSize);
}
Chunk regenerateChunkFromSeed(uint64_t seed, const Vec3i& coord) {
Chunk chunk;
int baseY = coord.y * chunkSize;
for (int x = 0; x < chunkSize; ++x) {
for (int z = 0; z < chunkSize; ++z) {
int surface = regeneratedSurfaceHeight(seed, coord.x * chunkSize + x,
coord.z * chunkSize + z);
for (int y = 0; y < chunkSize; ++y) {
int worldY = baseY + y;
Block block;
if (worldY > surface) {
block = Block::Air;
} else if (worldY == surface) {
block = Block::Grass;
} else if (worldY > surface - 4) {
block = Block::Dirt;
} else {
block = Block::Stone;
}
chunk.set(x, y, z, block);
}
}
}
return chunk;
}
IntegrityReport SaveScanner::scan(const ByteBuffer& buffer) const {
return scan(buffer.data(), buffer.size());
}
IntegrityReport SaveScanner::scan(const uint8_t* data, std::size_t size) const {
IntegrityReport report;
report.bufferBytes = size;
SaveHeader header;
if (!readSaveHeader(data, size, header)) {
report.issues.push_back(makeIssue(
IntegrityIssueKind::TruncatedBuffer, IssueSeverity::Fatal, size,
"buffer holds " + decimal(static_cast<long long>(size)) +
" bytes, too short for the " + decimal(static_cast<long long>(saveHeaderBytes)) +
" byte header"));
return report;
}
if (header.magic != saveMagic) {
report.issues.push_back(makeIssue(IntegrityIssueKind::BadMagic, IssueSeverity::Fatal, 0,
"magic is not a save container"));
return report;
}
if (!isKnownSaveVersion(header.version)) {
report.issues.push_back(
makeIssue(IntegrityIssueKind::UnknownVersion, IssueSeverity::Fatal, 4,
"format version " + decimal(static_cast<long long>(header.version)) +
" is outside the supported range v1..v4"));
return report;
}
if (header.sectorBytes == 0) {
report.issues.push_back(makeIssue(IntegrityIssueKind::MalformedHeader, IssueSeverity::Fatal,
20, "header declares a sector size of zero"));
return report;
}
report.headerReadable = true;
report.version = static_cast<SaveVersion>(header.version);
report.regionX = header.regionX;
report.regionZ = header.regionZ;
report.declaredChunks = header.chunkCount;
std::size_t tableEnd = saveHeaderBytes + saveTableBytes(header);
std::size_t availableEntries = header.chunkCount;
if (tableEnd > size) {
availableEntries = size > saveHeaderBytes ? (size - saveHeaderBytes) / saveEntryBytes : 0;
report.issues.push_back(makeIssue(
IntegrityIssueKind::ChunkCountOverrun, IssueSeverity::Recoverable, 16,
"header declares " + decimal(static_cast<long long>(header.chunkCount)) +
" chunks but only " + decimal(static_cast<long long>(availableEntries)) +
" table entries fit in the buffer"));
}
SaveHeader readable = header;
readable.chunkCount = static_cast<uint32_t>(availableEntries);
std::vector<SaveTableEntry> entries;
readSaveTable(data, size, readable, entries);
report.tableEntries = entries.size();
if (versionHasChecksum(report.version) && tableEnd <= size) {
uint32_t computed = fnv1a32(data + saveHeaderBytes, saveTableBytes(header));
if (computed != header.tableChecksum) {
report.issues.push_back(makeIssue(
IntegrityIssueKind::ChecksumMismatch, IssueSeverity::Recoverable, 24,
"entry table checksum is " + decimal(static_cast<long long>(header.tableChecksum)) +
" but the table hashes to " + decimal(static_cast<long long>(computed))));
}
}
RegionCoord expectedRegion{header.regionX, header.regionZ};
std::size_t headerSectors = saveHeaderSectors(readable);
std::vector<SectorSpan> spans;
std::size_t lastSectorEnd = headerSectors;
for (std::size_t i = 0; i < entries.size(); ++i) {
const SaveTableEntry& entry = entries[i];
std::size_t entryAt = saveHeaderBytes + i * saveEntryBytes;
int index = static_cast<int>(i);
auto locate = [&](IntegrityIssue& issue) {
issue.regionX = header.regionX;
issue.regionZ = header.regionZ;
issue.entryIndex = index;
issue.hasChunkCoord = true;
issue.chunkCoord = entry.coord;
};
bool coordBroken = false;
if (!(regionForChunk(entry.coord) == expectedRegion)) {
RegionCoord actual = regionForChunk(entry.coord);
IntegrityIssue issue = makeIssue(
IntegrityIssueKind::ChunkOutsideRegion, IssueSeverity::Recoverable, entryAt,
"chunk " + coordText(entry.coord) + " belongs to region (" + decimal(actual.x) +
", " + decimal(actual.z) + "), not (" + decimal(header.regionX) + ", " +
decimal(header.regionZ) + ")");
locate(issue);
report.issues.push_back(issue);
coordBroken = true;
}
if (entry.coord.y < saveChunkYMin || entry.coord.y > saveChunkYMax) {
IntegrityIssue issue = makeIssue(
IntegrityIssueKind::ChunkHeightOutOfRange, IssueSeverity::Recoverable, entryAt + 4,
"chunk Y " + decimal(entry.coord.y) + " is outside the world range " +
decimal(saveChunkYMin) + ".." + decimal(saveChunkYMax));
locate(issue);
report.issues.push_back(issue);
coordBroken = true;
}
std::size_t payloadAt = static_cast<std::size_t>(entry.sectorOffset) * header.sectorBytes;
if (entry.sectorCount == 0 || payloadAt >= size) {
IntegrityIssue issue = makeIssue(
IntegrityIssueKind::OrphanEntry, IssueSeverity::Recoverable, entryAt + 12,
"entry for chunk " + coordText(entry.coord) + " points at sector " +
decimal(entry.sectorOffset) + " spanning " + decimal(entry.sectorCount) +
" sectors, which holds no payload");
locate(issue);
report.issues.push_back(issue);
continue;
}