载入中...
搜索中...
未找到
PixelWorldArchive.cpp
浏览该文件的文档.
2
4
5#include <algorithm>
6#include <bit>
7#include <charconv>
8#include <limits>
9#include <string>
10#include <type_traits>
11
13namespace {
14
15constexpr std::string_view kArchiveType = "pixelworld.chunk-batch";
16constexpr std::uint64_t kArchiveVersion = 3;
17constexpr std::size_t kMaximumArchiveChunks = 65'536;
18constexpr std::uint64_t kMaximumDecodedBytes = 512ULL * 1024ULL * 1024ULL;
19constexpr std::size_t kLegacyDecodedChunkBytes =
21constexpr std::size_t kDecodedChunkBytes =
23
24eve::LogicalId archiveSchema() {
25 const auto parsed = eve::LogicalId::parse("pixelworld:chunk-batch");
26 if (!parsed) std::terminate();
27 return *parsed;
28}
29
33}
34
35std::string hex(std::span<const std::uint8_t> bytes) {
36 static constexpr char digits[] = "0123456789abcdef";
37 std::string result(bytes.size() * 2, '0');
38 for (std::size_t index = 0; index < bytes.size(); ++index) {
39 result[index * 2] = digits[bytes[index] >> 4];
40 result[index * 2 + 1] = digits[bytes[index] & 0xf];
41 }
42 return result;
43}
44
45eve::Result<std::vector<std::uint8_t>> unhex(const eve::Value& value, std::size_t maximumBytes,
46 std::string path) {
47 const auto* text = value.getIf<std::string>();
48 if (!text || text->size() % 2 != 0 || text->size() / 2 > maximumBytes)
49 return eve::Result<std::vector<std::uint8_t>>::failure(
50 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "invalid or oversized hexadecimal payload",
51 path, {}, "pixelworld.chunk-archive"));
52 const auto nibble = [](char value) -> int {
53 if (value >= '0' && value <= '9') return value - '0';
54 if (value >= 'a' && value <= 'f') return value - 'a' + 10;
55 return -1;
56 };
57 std::vector<std::uint8_t> result(text->size() / 2);
58 for (std::size_t index = 0; index < result.size(); ++index) {
59 const int high = nibble((*text)[index * 2]);
60 const int low = nibble((*text)[index * 2 + 1]);
61 if (high < 0 || low < 0)
64 {}, "pixelworld.chunk-archive"));
65 result[index] = std::uint8_t((high << 4) | low);
66 }
67 return eve::Result<std::vector<std::uint8_t>>::success(std::move(result));
68}
69
70template <class T>
71void put(std::vector<std::uint8_t>& bytes, T value) {
72 using Unsigned = std::make_unsigned_t<T>;
73 std::uint64_t encoded = static_cast<std::uint64_t>(static_cast<Unsigned>(value));
74 for (std::size_t index = 0; index < sizeof(T); ++index) {
75 bytes.push_back(std::uint8_t(encoded & 0xff));
76 encoded >>= 8;
77 }
78}
79
80template <class T>
81T take(std::span<const std::uint8_t> bytes, std::size_t& cursor) {
82 using Unsigned = std::make_unsigned_t<T>;
83 Unsigned encoded = 0;
84 for (std::size_t index = 0; index < sizeof(T); ++index)
85 encoded |= Unsigned(bytes[cursor++]) << (index * 8);
86 if constexpr (std::is_signed_v<T>) return std::bit_cast<T>(encoded);
87 return encoded;
88}
89
90std::vector<std::uint8_t> encodeCells(const eve::pixelworld::PixelChunkSnapshot& chunk) {
91 std::vector<std::uint8_t> result;
92 result.reserve(kDecodedChunkBytes);
93 for (const auto cell : chunk.cells) {
94 put(result, std::uint16_t(cell.material));
95 put(result, cell.temperature);
96 put(result, cell.lifetime);
97 put(result, cell.thermalRemainder);
98 }
99 return result;
100}
101
103 std::initializer_list<std::string_view> fields) {
104 const auto* result = value.getIf<eve::Value::Object>();
105 if (!result || result->size() != fields.size())
107 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "archive object has unknown or missing fields",
108 path, {}, "pixelworld.chunk-archive"));
109 for (const auto field : fields)
110 if (!result->contains(std::string(field)))
112 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "archive object has unknown or missing fields",
113 path, {}, "pixelworld.chunk-archive"));
115}
116
117eve::Result<std::uint64_t> decimal(const eve::Value& value, std::string path) {
118 const auto* text = value.getIf<std::string>();
119 if (!text)
121 eve::DiagnosticCode::ParseError, "expected decimal string", path, {}, "pixelworld.chunk-archive"));
122 std::uint64_t result = 0;
123 const auto [end, error] = std::from_chars(text->data(), text->data() + text->size(), result);
124 if (error != std::errc{} || end != text->data() + text->size())
126 "invalid decimal string", path, {},
127 "pixelworld.chunk-archive"));
129}
130
131eve::Result<int> integer(const eve::Value& value, std::string path) {
132 const auto* number = value.getIf<std::int64_t>();
133 if (!number || *number < std::numeric_limits<int>::min() || *number > std::numeric_limits<int>::max())
135 "expected in-range integer", path, {},
136 "pixelworld.chunk-archive"));
137 return eve::Result<int>::success(int(*number));
138}
139
140eve::Result<eve::ContentId> digest(const std::vector<std::uint8_t>& decoded,
141 const eve::SnapshotHashProvider& hashProvider) {
142 if (!hashProvider)
144 eve::DiagnosticCode::Unsupported, "archive hash provider is required", {}, {}, "pixelworld.chunk-archive"));
145 return hashProvider(hex(decoded));
146}
147
148} // namespace
149
152 PixelChunkArchiveCodec codec, const eve::SnapshotHashProvider& hashProvider) {
155 eve::DiagnosticCode::Unsupported, "unknown Chunk archive codec", {}, {}, "pixelworld.chunk-archive"));
156 if (batch.chunks.size() > kMaximumArchiveChunks)
158 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "Chunk archive exceeds record budget",
159 "chunks", {}, "pixelworld.chunk-archive"));
160 eve::Value::Array records;
161 std::optional<std::pair<int, int>> previous;
162 for (const auto& chunk : batch.chunks) {
163 const std::pair<int, int> key{chunk.y, chunk.x};
164 if (previous && !(previous.value() < key))
166 eve::DiagnosticCode::InvalidArgument, "Chunks must be unique canonical y/x order", "chunks", {},
167 "pixelworld.chunk-archive"));
168 previous = key;
169 if (chunk.revision == 0 || chunk.revision > batch.sourceRevision ||
170 (chunk.removed && !chunk.cells.empty()) ||
171 (!chunk.removed && chunk.cells.size() !=
174 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "invalid Chunk revision or cell shape",
175 "chunks", {}, "pixelworld.chunk-archive"));
176 const auto decoded = chunk.removed ? std::vector<std::uint8_t>{} : encodeCells(chunk);
177 auto contentHash = digest(decoded, hashProvider);
178 if (!contentHash) return eve::Result<eve::SnapshotEnvelope>::failure(contentHash.status());
179 const auto physical = chunk.removed ? PixelChunkArchiveCodec::None : codec;
180 auto stored = eve::asset::compressEvpackChunk(assetCodec(physical), decoded);
182 records.emplace_back(eve::Value::Object{
183 {"codec", eve::Value(int(physical))},
184 {"contentHash", eve::Value(contentHash.value().format())},
185 {"decodedSize", eve::Value(std::to_string(decoded.size()))},
186 {"removed", eve::Value(chunk.removed)},
187 {"revision", eve::Value(std::to_string(chunk.revision))},
188 {"stored", eve::Value(hex(stored.value()))},
189 {"x", eve::Value(chunk.x)},
190 {"y", eve::Value(chunk.y)},
191 });
192 }
194 {"catalogFingerprint", eve::Value(std::to_string(batch.catalogFingerprint))},
195 {"chunks", eve::Value(std::move(records))},
196 {"fullResync", eve::Value(batch.fullResync)},
197 {"sourceLastEditSequence", eve::Value(std::to_string(batch.sourceLastEditSequence))},
198 {"sourceSeed", eve::Value(std::to_string(batch.sourceSeed))},
199 });
200 return eve::makeSnapshotEnvelope(std::string(kArchiveType), archiveSchema(),
201 eve::SchemaVersion(kArchiveVersion), instanceId,
203 std::move(payload), hashProvider);
204}
205
207 const eve::SnapshotEnvelope& snapshot, const eve::SnapshotHashProvider& hashProvider) {
208 auto verified = eve::verifySnapshotEnvelope(snapshot, hashProvider);
209 if (!verified) return eve::Result<eve::pixelworld::PixelChunkBatch>::failure(verified.status());
210 if (snapshot.type != kArchiveType || snapshot.schema != archiveSchema())
212 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "Chunk archive type or schema does not match",
213 "schema", {}, "pixelworld.chunk-archive"));
214 if (snapshot.schemaVersion.value() < 1 || snapshot.schemaVersion.value() > kArchiveVersion)
216 eve::Diagnostic::error(eve::DiagnosticCode::UnknownVersion, "unsupported Chunk archive version",
217 "schemaVersion", {}, "pixelworld.chunk-archive"));
218 const bool versionOne = snapshot.schemaVersion.value() == 1;
219 const bool versionTwo = snapshot.schemaVersion.value() == 2;
220 auto root = versionOne
221 ? object(snapshot.payload, "payload", {"catalogFingerprint", "chunks", "sourceSeed"})
222 : versionTwo
223 ? object(snapshot.payload, "payload", {"catalogFingerprint", "chunks",
224 "sourceLastEditSequence", "sourceSeed"})
225 : object(snapshot.payload, "payload", {"catalogFingerprint", "chunks",
226 "fullResync", "sourceLastEditSequence",
227 "sourceSeed"});
229 auto catalog = decimal(root.value()->at("catalogFingerprint"), "payload.catalogFingerprint");
230 auto seed = decimal(root.value()->at("sourceSeed"), "payload.sourceSeed");
231 auto sequence = versionOne
233 : decimal(root.value()->at("sourceLastEditSequence"),
234 "payload.sourceLastEditSequence");
235 const auto* records = root.value()->at("chunks").getIf<eve::Value::Array>();
236 if (!catalog || !seed || !sequence || !records || records->size() > kMaximumArchiveChunks)
238 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "invalid Chunk archive payload", "payload", {},
239 "pixelworld.chunk-archive"));
240
242 batch.catalogFingerprint = catalog.value();
243 batch.sourceSeed = seed.value();
244 batch.sourceRevision = snapshot.revision.value();
245 batch.sourceTick = snapshot.tick;
246 batch.sourceLastEditSequence = sequence.value();
247 if (!versionOne && !versionTwo) {
248 const auto* fullResync = root.value()->at("fullResync").getIf<bool>();
249 if (!fullResync)
251 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "invalid fullResync flag", "payload.fullResync",
252 {}, "pixelworld.chunk-archive"));
253 batch.fullResync = *fullResync;
254 }
255 std::optional<std::pair<int, int>> previous;
256 std::uint64_t decodedTotal = 0;
257 for (std::size_t index = 0; index < records->size(); ++index) {
258 const std::string path = "payload.chunks[" + std::to_string(index) + "]";
259 auto record = object((*records)[index], path,
260 {"codec", "contentHash", "decodedSize", "removed", "revision",
261 "stored", "x", "y"});
262 if (!record) return eve::Result<eve::pixelworld::PixelChunkBatch>::failure(record.status());
263 auto x = integer(record.value()->at("x"), path + ".x");
264 auto y = integer(record.value()->at("y"), path + ".y");
265 auto revision = decimal(record.value()->at("revision"), path + ".revision");
266 auto decodedSize = decimal(record.value()->at("decodedSize"), path + ".decodedSize");
267 const auto* removed = record.value()->at("removed").getIf<bool>();
268 auto codecValue = integer(record.value()->at("codec"), path + ".codec");
269 const auto* hashText = record.value()->at("contentHash").getIf<std::string>();
270 if (!x || !y || !revision || !decodedSize || !removed || !codecValue || !hashText ||
271 revision.value() == 0 || revision.value() > batch.sourceRevision ||
272 codecValue.value() < 0 || codecValue.value() > int(PixelChunkArchiveCodec::Zstd))
274 eve::DiagnosticCode::ParseError, "invalid Chunk archive record", path, {}, "pixelworld.chunk-archive"));
275 const std::pair<int, int> key{y.value(), x.value()};
276 if (previous && !(previous.value() < key))
278 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "Chunk records are not canonical", path, {},
279 "pixelworld.chunk-archive"));
280 previous = key;
281 const bool currentCells = decodedSize.value() == kDecodedChunkBytes;
282 const bool legacyCells = decodedSize.value() == kLegacyDecodedChunkBytes;
283 const std::uint64_t expectedSize = *removed ? 0 : decodedSize.value();
284 if ((!*removed && !currentCells && !legacyCells) || (*removed && decodedSize.value() != 0) ||
285 decodedTotal > kMaximumDecodedBytes - expectedSize)
287 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "Chunk decoded size exceeds budget", path, {},
288 "pixelworld.chunk-archive"));
289 decodedTotal += expectedSize;
290 auto stored = unhex(record.value()->at("stored"), kDecodedChunkBytes + 1024, path + ".stored");
292 const auto codec = static_cast<PixelChunkArchiveCodec>(codecValue.value());
293 if (*removed && (codec != PixelChunkArchiveCodec::None || !stored.value().empty()))
295 eve::DiagnosticCode::ParseError, "removed Chunk must have empty uncompressed payload", path, {},
296 "pixelworld.chunk-archive"));
297 auto decoded = eve::asset::decompressEvpackChunk(assetCodec(codec), stored.value(),
298 expectedSize, kMaximumDecodedBytes);
299 if (!decoded) return eve::Result<eve::pixelworld::PixelChunkBatch>::failure(decoded.status());
300 const auto expectedHash = eve::ContentId::parse(*hashText);
301 auto actualHash = digest(decoded.value(), hashProvider);
302 if (!expectedHash || !actualHash || actualHash.value() != *expectedHash)
304 eve::Diagnostic::error(eve::DiagnosticCode::HashMismatch, "decoded Chunk content hash does not match",
305 path, {}, "pixelworld.chunk-archive"));
306
308 chunk.x = x.value();
309 chunk.y = y.value();
310 chunk.revision = revision.value();
311 chunk.removed = *removed;
312 if (!*removed) {
314 std::size_t cursor = 0;
315 for (auto& cell : chunk.cells) {
316 cell.material = eve::pixelworld::MaterialId(take<std::uint16_t>(decoded.value(), cursor));
317 cell.temperature = take<std::int16_t>(decoded.value(), cursor);
318 cell.lifetime = take<std::uint8_t>(decoded.value(), cursor);
319 if (currentCells)
320 cell.thermalRemainder = take<std::uint16_t>(decoded.value(), cursor);
321 }
322 }
323 batch.chunks.push_back(std::move(chunk));
324 }
326}
327
328} // namespace eve::pixelworld_streaming
double value
Value::Object payload
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
int root
Definition AnimSmr.cpp:119
std::uint64_t decodedSize
Bounded deterministic compression used by the .evpack wire format.
std::vector< std::uint8_t > stored
Definition Evpack.cpp:171
std::uint32_t key
std::string text
std::uint64_t bytes
graphics::Canvas * previous
std::unique_ptr< gpgpu::Sequence > sequence
Definition OnnxGpgpu.cpp:43
std::string error
Definition Package.cpp:60
std::uint64_t revision
std::string path
Definition PlayHost.cpp:110
std::vector< std::string > fields
Definition PlayHost.cpp:111
std::uint32_t seed
Definition PointSet.cpp:807
double number
int removed
std::string digest
Cell cell
std::size_t cursor
Json object
uint32_t index
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.
Definition Diagnostic.h:125
Human-readable, scoped identifier in the form namespace:name.
Definition Identity.h:411
static std::optional< LogicalId > parse(std::string_view text)
Parses a scoped logical name.
Definition Identity.cpp:36
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
const T & value() const &
Borrow the value from a const lvalue after checking success.
Definition Result.h:308
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
std::map< std::string, Value > Object
Definition Value.h:34
std::vector< Value > Array
Definition Value.h:33
Id128 public API.
Definition Identity.h:112
static std::optional< Id128 > parse(std::string_view text) noexcept
Parses canonical UUID text.
Definition Identity.h:151
constexpr std::uint64_t value() const noexcept
Returns the underlying value at an explicit protocol boundary.
EvpackCodec
Physical outer compression applied to one runtime chunk.
Definition Evpack.h:36
Result< std::vector< std::uint8_t > > compressEvpackChunk(EvpackCodec codec, std::span< const std::uint8_t > decoded)
Compress decoded chunk bytes using the selected stable wire codec.
Result< std::vector< std::uint8_t > > decompressEvpackChunk(EvpackCodec codec, std::span< const std::uint8_t > stored, std::uint64_t decodedSize, std::uint64_t maximumDecodedBytes)
Decode one chunk into its authenticated canonical bytes.
eve::Result< eve::SnapshotEnvelope > archiveChunkBatch(const eve::pixelworld::PixelChunkBatch &batch, eve::PersistentId instanceId, PixelChunkArchiveCodec codec, const eve::SnapshotHashProvider &hashProvider)
Encode one authoritative batch as pixelworld:chunk-batch SnapshotEnvelope version 3.
PixelChunkArchiveCodec
Stable physical codec for one independently stored PixelWorld Chunk.
eve::Result< eve::pixelworld::PixelChunkBatch > decodeChunkBatchArchive(const eve::SnapshotEnvelope &snapshot, const eve::SnapshotHashProvider &hashProvider)
Verify and decode a version 1, 2 or 3 Chunk-batch archive into an owning candidate.
MaterialId
Compact stable material identifier stored in each authoritative cell.
constexpr int kPixelChunkSize
Definition PixelWorld.h:20
Result< SnapshotEnvelope > makeSnapshotEnvelope(std::string type, LogicalId schema, SchemaVersion schemaVersion, PersistentId instanceId, Revision revision, SimulationTick tick, Value payload, const SnapshotHashProvider &hashProvider)
Construct and seal a snapshot envelope.
Definition Snapshot.cpp:103
Result< void > verifySnapshotEnvelope(const SnapshotEnvelope &snapshot, const SnapshotHashProvider &hashProvider)
Verify an envelope's content hash without modifying it.
Definition Snapshot.cpp:63
std::function< Result< ContentId >(std::string_view canonicalInput)> SnapshotHashProvider
Injected content-digest implementation used by snapshots.
Definition Snapshot.h:36
Stable outer format shared by persistence and cross-process snapshots.
Definition Snapshot.h:46
SchemaVersion schemaVersion
Definition Snapshot.h:49
std::string type
Definition Snapshot.h:47
SimulationTick tick
Definition Snapshot.h:52
Owning authoritative Chunk correction with source world metadata.
Definition PixelWorld.h:89
bool fullResync
Replace the replica projection instead of applying an incremental correction.
Definition PixelWorld.h:96
std::vector< PixelChunkSnapshot > chunks
Definition PixelWorld.h:97
eve::SimulationTick sourceTick
Definition PixelWorld.h:93
Owning, immutable projection of one authoritative simulation chunk.
Definition PixelWorld.h:77
std::vector< PixelCell > cells
Definition PixelWorld.h:82