11constexpr uint16_t kVersion = 5;
12constexpr size_t kHeaderSize = 44;
13constexpr size_t kEntrySize = 36;
14constexpr size_t kBytesPerCell = 12;
15size_t bytesPerCell(uint16_t version) {
16 return version == 1 ? 7u : (version == 2 ? 8u :
17 (version == 3 ? 9u : (version == 4 ? 11u : 12u)));
21void putU16(std::vector<uint8_t> &out, uint16_t
v) { out.push_back(uint8_t(
v)); out.push_back(uint8_t(
v >> 8)); }
22void putU32(std::vector<uint8_t> &out, uint32_t
v) {
23 for (
int i = 0; i < 4; ++i) out.push_back(uint8_t(
v >> (i * 8)));
25void putI32(std::vector<uint8_t> &out, int32_t
v) { putU32(out, uint32_t(
v)); }
26void putU64(std::vector<uint8_t> &out, uint64_t
v) {
27 for (
int i = 0; i < 8; ++i) out.push_back(uint8_t(
v >> (i * 8)));
29void putFloat(std::vector<uint8_t> &out,
float v) { uint32_t bits; std::memcpy(&bits, &
v, 4); putU32(out, bits); }
31bool getU16(
const uint8_t *&
p,
const uint8_t *
end, uint16_t &
v) {
32 if (
end -
p < 2)
return false;
33 v = uint16_t(
p[0]) | uint16_t(
p[1] << 8);
p += 2;
return true;
35bool getU32(
const uint8_t *&
p,
const uint8_t *
end, uint32_t &
v) {
36 if (
end -
p < 4)
return false;
37 v = uint32_t(
p[0]) | uint32_t(
p[1]) << 8 | uint32_t(
p[2]) << 16 | uint32_t(
p[3]) << 24;
p += 4;
return true;
39bool getI32(
const uint8_t *&
p,
const uint8_t *
end, int32_t &
v) { uint32_t
u;
if (!getU32(
p,
end,
u))
return false;
v = int32_t(
u);
return true; }
40bool getU64(
const uint8_t *&
p,
const uint8_t *
end, uint64_t &
v) {
41 if (
end -
p < 8)
return false;
42 v = 0;
for (
int i = 0; i < 8; ++i)
v |= uint64_t(
p[i]) << (i * 8);
p += 8;
return true;
44bool getFloat(
const uint8_t *&
p,
const uint8_t *
end,
float &
v) { uint32_t bits;
if (!getU32(
p,
end, bits))
return false; std::memcpy(&
v, &bits, 4);
return true; }
46uint32_t checksum(
const std::vector<uint8_t> &data) {
47 uint32_t
value = 2166136261u;
48 for (uint8_t
byte : data) {
value ^= byte;
value *= 16777619u; }
52std::vector<uint8_t> packBits(
const std::vector<uint8_t> &src) {
53 std::vector<uint8_t> out; out.reserve(src.size());
size_t i = 0;
54 while (i < src.size()) {
56 while (i +
run < src.size() && src[i +
run] == src[i] &&
run < 128) ++
run;
57 if (
run >= 3) { out.push_back(uint8_t(0x80u | uint8_t(
run - 1))); out.push_back(src[i]); i +=
run;
continue; }
59 while (i < src.size() && i -
begin < 128) {
61 while (i + nextRun < src.size() && src[i + nextRun] == src[i] && nextRun < 3) ++nextRun;
62 if (nextRun >= 3)
break;
63 i += std::min(nextRun,
size_t(128) - (i -
begin));
65 out.push_back(uint8_t(i -
begin - 1)); out.insert(out.end(), src.begin() + ptrdiff_t(
begin), src.begin() + ptrdiff_t(i));
70bool unpackBits(
const uint8_t *data,
size_t size,
size_t rawSize, std::vector<uint8_t> &out) {
71 out.clear(); out.reserve(rawSize);
size_t i = 0;
72 while (i <
size && out.size() < rawSize) {
73 const uint8_t
token = data[i++];
const size_t count = size_t(
token & 0x7fu) + 1;
74 if (
count > rawSize - out.size())
return false;
75 if (
token & 0x80u) {
if (i >=
size)
return false; out.insert(out.end(),
count, data[i++]); }
76 else {
if (
count >
size - i)
return false; out.insert(out.end(), data + i, data + i +
count); i +=
count; }
78 return i ==
size && out.size() == rawSize;
81uint8_t quantize8(
float v) {
return uint8_t(std::lround(std::clamp(
v, 0.f, 1.f) * 255.f)); }
82uint16_t quantize16(
float v) {
return uint16_t(std::lround(std::clamp(
v, 0.f, 1.f) * 65535.f)); }
87 if (
offset > bytes_.size() ||
length > bytes_.size() -
static_cast<std::size_t
>(
offset))
90 "procgen.terrain-source"));
91 const auto begin = bytes_.begin() +
static_cast<std::ptrdiff_t
>(
offset);
93 std::vector<std::uint8_t>(
begin,
begin +
static_cast<std::ptrdiff_t
>(
length)));
97 int chunkSize, std::vector<uint8_t> &out, std::string *
error) {
99 if (
w <= 0 ||
h <= 0 || chunkSize <= 0 || chunkSize > 65535) { fail(
error,
"terrain asset: invalid dimensions");
return false; }
109 fail(
error,
"terrain asset: layer dimensions do not match heightmap");
return false;
111 const auto [minIt, maxIt] = std::minmax_element(hm.
data().begin(), hm.
data().end());
112 const float minHeight = *minIt, maxHeight = *maxIt;
115 std::vector<Block> blocks;
116 for (
int cy = 0;
cy * chunkSize <
h; ++
cy)
for (
int cx = 0;
cx * chunkSize <
w; ++
cx) {
117 Block block; block.entry.chunkX =
cx; block.entry.chunkY =
cy;
118 block.entry.width = std::min(chunkSize,
w -
cx * chunkSize);
119 block.entry.height = std::min(chunkSize,
h -
cy * chunkSize);
120 std::vector<uint8_t> raw; raw.reserve(
size_t(block.entry.width) *
size_t(block.entry.height) * kBytesPerCell);
121 for (
int ly = 0; ly < block.entry.height; ++ly)
for (
int lx = 0; lx < block.entry.width; ++lx) {
122 const size_t i = size_t(
cy * chunkSize + ly) * size_t(
w) + size_t(
cx * chunkSize + lx);
123 const float normalizedHeight = maxHeight > minHeight ? (hm.
data()[i] - minHeight) / (maxHeight - minHeight) : 0.f;
124 const uint16_t qh = quantize16(normalizedHeight); putU16(raw, qh);
127 if (direction < -1 || direction > 7) {
128 fail(
error,
"terrain asset: invalid flow direction");
return false;
131 float flowX = 0.f, flowY = 0.f;
135 static constexpr std::array<float, 8> vectorX{
136 -0.70710678f, 0.f, 0.70710678f, -1.f, 1.f,
137 -0.70710678f, 0.f, 0.70710678f};
138 static constexpr std::array<float, 8> vectorY{
139 -0.70710678f, -1.f, -0.70710678f, 0.f, 0.f,
140 0.70710678f, 1.f, 0.70710678f};
143 if (!std::isfinite(flowX) || !std::isfinite(flowY)) {
144 fail(
error,
"terrain asset: invalid continuous flow vector");
return false;
146 raw.push_back(quantize8(flowX * 0.5f + 0.5f));
147 raw.push_back(quantize8(flowY * 0.5f + 0.5f));
150 raw.push_back(quantize8(maxHeight > minHeight ? lakeDepth / (maxHeight - minHeight) : 0.f));
151 raw.push_back(quantize8(
climate.temperature[i])); raw.push_back(quantize8(
climate.moisture[i]));
152 raw.push_back(hydro.
rivers[i] ? 1 : 0); raw.push_back(uint8_t(
climate.biomes[i]));
154 block.entry.rawSize = uint32_t(raw.size()); block.entry.checksum = checksum(raw);
155 std::vector<uint8_t> packed = packBits(raw);
156 block.entry.compressed = packed.size() < raw.size();
157 block.bytes = block.entry.compressed ? std::move(packed) : std::move(raw);
158 block.entry.storedSize = uint32_t(block.bytes.size()); blocks.push_back(std::move(block));
160 const uint64_t payloadStart = kHeaderSize + uint64_t(kEntrySize) * blocks.size(); uint64_t
cursor = payloadStart;
161 for (Block &block : blocks) { block.entry.offset =
cursor;
cursor += block.bytes.size(); }
162 out.clear(); out.reserve(
size_t(
cursor)); out.insert(out.end(), {
'E',
'V',
'T',
'R'});
163 putU16(out, kVersion); putU16(out, 0); putU32(out, uint32_t(
w)); putU32(out, uint32_t(
h));
164 putU32(out, uint32_t(chunkSize)); putU32(out, uint32_t(blocks.size()));
165 putFloat(out, minHeight); putFloat(out, maxHeight); putFloat(out, maxFlow); putU64(out, kHeaderSize);
166 for (
const Block &block : blocks) {
167 const auto &e = block.entry; putI32(out, e.chunkX); putI32(out, e.chunkY); putU16(out, uint16_t(e.width)); putU16(out, uint16_t(e.height));
168 out.push_back(e.compressed ? 1 : 0); out.insert(out.end(), 3, 0); putU64(out, e.offset);
169 putU32(out, e.storedSize); putU32(out, e.rawSize); putU32(out, e.checksum);
171 for (
const Block &block : blocks) out.insert(out.end(), block.bytes.begin(), block.bytes.end());
176 if (!data) { fail(
error,
"terrain asset: invalid source");
return false; }
177 auto source = std::make_shared<MemoryTerrainArchiveSource>(
178 std::span<const std::uint8_t>(data,
size));
180 source_ = std::move(
source);
188 "procgen.terrain-source"));
189 auto header =
source->read(0, kHeaderSize);
191 const auto& headerBytes = header.value();
192 if (std::memcmp(headerBytes.data(),
"EVTR", 4) != 0)
195 "procgen.terrain-source"));
196 const std::uint32_t
count = uint32_t(headerBytes[20]) | (uint32_t(headerBytes[21]) << 8) |
197 (uint32_t(headerBytes[22]) << 16) | (uint32_t(headerBytes[23]) << 24);
198 if (std::uint64_t(
count) * kEntrySize >
source->size() - kHeaderSize)
201 "procgen.terrain-source"));
202 const std::size_t metadataSize = kHeaderSize + std::size_t(
count) * kEntrySize;
203 auto metadata =
source->read(0, metadataSize);
207 if (!next.openMetadata(metadata.value().data(), metadata.value().size(),
source->size(), &
error))
210 next.source_ = std::move(
source);
211 *
this = std::move(next);
215bool TerrainAsset::openMetadata(
const uint8_t *data,
size_t metadataSize,
216 std::uint64_t archiveSize, std::string *
error) {
217 if (!data || metadataSize < kHeaderSize || std::memcmp(data,
"EVTR", 4) != 0) { fail(
error,
"terrain asset: invalid magic or truncated header");
return false; }
218 const uint8_t *
p = data + 4, *
end = data + metadataSize; uint16_t version = 0, flags = 0; uint32_t
w,
h, cs,
count; uint64_t directory;
219 float minH, maxH, maxFlow;
220 if (!getU16(
p,
end, version) || !getU16(
p,
end, flags) || !getU32(
p,
end,
w) || !getU32(
p,
end,
h) ||
221 !getU32(
p,
end, cs) || !getU32(
p,
end,
count) || !getFloat(
p,
end, minH) || !getFloat(
p,
end, maxH) ||
222 !getFloat(
p,
end, maxFlow) || !getU64(
p,
end, directory) ||
223 version < 1 || version > kVersion || flags != 0 ||
224 w == 0 ||
h == 0 || cs == 0 ||
w > uint32_t(std::numeric_limits<int>::max()) ||
225 h > uint32_t(std::numeric_limits<int>::max()) || cs > uint32_t(std::numeric_limits<int>::max()) ||
226 !std::isfinite(minH) || !std::isfinite(maxH) || !std::isfinite(maxFlow) ||
227 minH > maxH || maxFlow < 1.f || directory != kHeaderSize ||
228 uint64_t(
count) != ((uint64_t(
w) + cs - 1) / cs) * ((uint64_t(
h) + cs - 1) / cs) ||
229 directory > metadataSize || uint64_t(
count) * kEntrySize > metadataSize - directory) {
230 fail(
error,
"terrain asset: unsupported or invalid header");
return false;
232 const uint64_t payloadStart = directory + uint64_t(
count) * kEntrySize;
234 for (uint32_t i = 0; i <
count; ++i) {
235 TerrainChunkEntry e; int32_t
x,
y; uint16_t cw, ch; uint8_t compression;
236 if (!getI32(
p,
end,
x) || !getI32(
p,
end,
y) || !getU16(
p,
end, cw) || !getU16(
p,
end, ch) ||
end -
p < 4) {
237 fail(
error,
"terrain asset: truncated directory");
return false;
240 const bool reservedClear =
p[1] == 0 &&
p[2] == 0 &&
p[3] == 0;
241 p += 4; e.chunkX =
x; e.chunkY =
y; e.width = cw; e.height = ch; e.compressed = compression == 1;
242 const bool coordinateValid =
x >= 0 &&
y >= 0 && uint64_t(
x) * cs <
w && uint64_t(
y) * cs <
h;
243 const uint32_t expectedWidth = coordinateValid ? std::min(cs,
w - uint32_t(
x) * cs) : 0;
244 const uint32_t expectedHeight = coordinateValid ? std::min(cs,
h - uint32_t(
y) * cs) : 0;
245 if (compression > 1 || !getU64(
p,
end, e.offset) || !getU32(
p,
end, e.storedSize) || !getU32(
p,
end, e.rawSize) || !getU32(
p,
end, e.checksum) ||
246 !reservedClear || !coordinateValid || uint32_t(e.width) != expectedWidth ||
247 uint32_t(e.height) != expectedHeight ||
248 e.rawSize != uint64_t(e.width) * uint64_t(e.height) * bytesPerCell(version) ||
249 (!e.compressed && e.storedSize != e.rawSize) || e.offset < payloadStart ||
250 e.offset > archiveSize || e.storedSize > archiveSize - e.offset) {
251 fail(
error,
"terrain asset: invalid chunk entry");
return false;
255 std::vector<const TerrainChunkEntry *> intervals;
256 intervals.reserve(
entries.size());
257 for (
const auto &entry :
entries) intervals.push_back(&entry);
258 std::sort(intervals.begin(), intervals.end(), [](
const auto *
a,
const auto *
b) {
259 return a->offset < b->offset;
261 for (
size_t i = 1; i < intervals.size(); ++i) {
262 if (intervals[i - 1]->
offset + intervals[i - 1]->storedSize > intervals[i]->
offset) {
263 fail(
error,
"terrain asset: overlapping chunk payloads");
return false;
267 return left.chunkY < right.chunkY ||
268 (left.chunkY == right.chunkY && left.chunkX < right.chunkX);
273 fail(
error,
"terrain asset: duplicate chunk coordinate");
277 width_ = int(
w); height_ = int(
h); chunkSize_ = int(cs); version_ = version;
278 minHeight_ = minH; maxHeight_ = maxH; maxFlow_ = maxFlow;
279 chunks_ = std::move(
entries);
return true;
284 if (!entry) { fail(
error,
"terrain asset: chunk not found");
return false; }
285 const auto it = entry;
286 std::vector<uint8_t> raw;
287 if (!source_) { fail(
error,
"terrain asset: archive source is unavailable");
return false; }
288 auto storedResult = source_->read(it->offset, it->storedSize);
289 if (!storedResult) { fail(
error,
"terrain asset: chunk source read failed");
return false; }
290 const auto& storedBytes = storedResult.value();
291 const uint8_t *
stored = storedBytes.data();
292 if (it->compressed) {
if (!unpackBits(
stored, it->storedSize, it->rawSize, raw)) { fail(
error,
"terrain asset: invalid compressed chunk");
return false; } }
293 else raw = storedBytes;
294 if (raw.size() != it->rawSize || checksum(raw) != it->checksum) { fail(
error,
"terrain asset: chunk checksum mismatch");
return false; }
297 const uint8_t *
p = raw.data();
298 for (
size_t i = 0; i <
cells; ++i) {
299 const uint16_t qh = uint16_t(
p[0]) | uint16_t(
p[1] << 8);
p += 2;
300 out.
heights.
data()[i] = minHeight_ + (maxHeight_ - minHeight_) * (
float(qh) / 65535.f);
303 const uint8_t encodedDirection = *
p++;
304 if (encodedDirection > 8) { fail(
error,
"terrain asset: invalid flow direction");
return false; }
311 static constexpr std::array<float, 8> vectorX{
312 -0.70710678f, 0.f, 0.70710678f, -1.f, 1.f,
313 -0.70710678f, 0.f, 0.70710678f};
314 static constexpr std::array<float, 8> vectorY{
315 -0.70710678f, -1.f, -0.70710678f, 0.f, 0.f,
316 0.70710678f, 1.f, 0.70710678f};
323 out.
lakeDepth[i] = (float(*
p++) / 255.f) * (maxHeight_ - minHeight_);
334 if (chunkX < 0 || chunkY < 0 || chunkSize_ <= 0)
return nullptr;
335 const std::uint64_t
columns = (std::uint64_t(width_) + std::uint64_t(chunkSize_) - 1) /
336 std::uint64_t(chunkSize_);
337 const std::uint64_t
index = std::uint64_t(chunkY) *
columns + std::uint64_t(chunkX);
338 if (
index >= chunks_.size())
return nullptr;
340 return entry.
chunkX == chunkX && entry.
chunkY == chunkY ? &entry :
nullptr;
std::unordered_map< std::string, QuestRuntime > entries
std::vector< std::uint8_t > stored
std::vector< ClimateData > climate
Current moisture and cloud state per cell.
std::array< PixelCell, kPixelChunkSize *kPixelChunkSize > cells
RoadLaneDirection direction
const UnitySourceAsset & source
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Move-only operation result carrying either a value or Status.
static Result success(T value)
Construct a successful result owning value.
static Result failure(Status status)
Construct a failed result from a structured status.
In-memory terrain heightmap: a dense float grid (row-major, index = y * width + x) materialized from ...
int getHeight() const
Returns the height.
const std::vector< float > & data() const
Data.
void resize(int width, int height)
Resize.
int getWidth() const
Returns the width.
Result< std::vector< std::uint8_t > > read(std::uint64_t offset, std::size_t length) const override
Reads .
Versioned, random-access terrain archive.
const TerrainChunkEntry * findChunk(int chunkX, int chunkY) const noexcept
Find one chunk directory entry without scanning the full directory.
bool open(const uint8_t *data, size_t size, std::string *error=nullptr)
Compatibility operation that opens only the header and directory.
static bool bake(const Heightmap &heightmap, const HydrologyMap &hydrology, const ClimateMap &climate, int chunkSize, std::vector< uint8_t > &out, std::string *error=nullptr)
Compatibility operation that bakes all terrain layers into EVTR.
bool loadChunk(int chunkX, int chunkY, TerrainChunkData &out, std::string *error=nullptr) const
Compatibility operation that loads and verifies one chunk.
Result< void > openSource(std::shared_ptr< const ITerrainArchiveSource > source)
Open only EVTR metadata from a shared immutable random-access source.
void putFloat(std::vector< std::uint8_t > &bytes, float value)
Append IEEE FLOAT to owned storage.
std::vector< float > flowVectorX
std::vector< uint8_t > rivers
std::vector< float > flowAccumulation
std::vector< float > flowVectorY
Normalized continuous downslope direction.
std::vector< int8_t > flowDirection
D8 neighbour index, or -1 for a sink.
std::vector< float > lakeDepth
Priority-Flood water depth; zero on drained terrain.
std::vector< uint8_t > streamOrder
Strahler order; zero outside the resolved river network.
Decoded terrain chunk containing all baked runtime layers.
std::vector< float > flowAccumulation
std::vector< float > moisture
std::vector< float > lakeDepth
std::vector< float > flowVectorX
std::vector< uint8_t > rivers
std::vector< Biome > biomes
std::vector< uint8_t > streamOrder
std::vector< int8_t > flowDirection
std::vector< float > flowVectorY
std::vector< float > temperature
Metadata for one independently compressed chunk in an EVTR archive.