From 0753a55c078ac33f40a2802f838abe2c3c7f6d63 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 8 Sep 2026 23:00:54 +0200 Subject: [PATCH] feat: add regenerateChunkFromSeed to SaveIntegrity --- core/src/SaveIntegrity.cpp | 156 +++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/core/src/SaveIntegrity.cpp b/core/src/SaveIntegrity.cpp index 1387ee7..43ad1a5 100644 --- a/core/src/SaveIntegrity.cpp +++ b/core/src/SaveIntegrity.cpp @@ -159,3 +159,159 @@ int regeneratedSurfaceHeight(uint64_t seed, int worldX, int worldZ) { int fx = floorMod(worldX, 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(size)) + + " bytes, too short for the " + decimal(static_cast(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(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(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(header.chunkCount)) + + " chunks but only " + decimal(static_cast(availableEntries)) + + " table entries fit in the buffer")); + } + + SaveHeader readable = header; + readable.chunkCount = static_cast(availableEntries); + std::vector 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(header.tableChecksum)) + + " but the table hashes to " + decimal(static_cast(computed)))); + } + } + + RegionCoord expectedRegion{header.regionX, header.regionZ}; + std::size_t headerSectors = saveHeaderSectors(readable); + std::vector 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(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(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; + } +