载入中...
搜索中...
未找到
PixelWorld.cpp
浏览该文件的文档.
2
4
5#include "common/Status.h"
6
7#include <algorithm>
8#include <array>
9#include <atomic>
10#include <bit>
11#include <cstring>
12#include <chrono>
13#include <deque>
14#include <limits>
15#include <set>
16#include <string>
17#include <unordered_map>
18#include <utility>
19
20namespace eve::pixelworld {
21namespace {
22
23std::atomic<std::uint64_t> nextPixelWorldId{1};
24
25int floorDiv(int value) noexcept {
26 return value >= 0 ? value / kPixelChunkSize : -((-value + kPixelChunkSize - 1) / kPixelChunkSize);
27}
28
29int floorMod(int value) noexcept {
30 const int result = value % kPixelChunkSize;
31 return result < 0 ? result + kPixelChunkSize : result;
32}
33
34struct ChunkCoord {
35 int x = 0;
36 int y = 0;
37 friend bool operator==(const ChunkCoord&, const ChunkCoord&) = default;
38 friend bool operator<(const ChunkCoord& a, const ChunkCoord& b) {
39 return a.y != b.y ? a.y < b.y : a.x < b.x;
40 }
41};
42
43struct ChunkCoordHash {
44 std::size_t operator()(ChunkCoord coord) const noexcept {
45 const auto x = std::uint64_t(std::uint32_t(coord.x));
46 const auto y = std::uint64_t(std::uint32_t(coord.y));
47 return std::size_t((x << 32U) ^ y);
48 }
49};
50
51struct Chunk {
52 std::array<PixelCell, kPixelChunkSize * kPixelChunkSize> cells{};
53 std::array<std::uint64_t, kPixelChunkSize * kPixelChunkSize> updated{};
54 std::array<std::uint16_t, 64> materialCounts{};
55 std::uint32_t nonAir = 0;
56 std::uint32_t mobile = 0;
57 std::uint32_t thermalRemainderCells = 0;
58 std::uint32_t materialOverflow = 0;
59 std::int16_t minimumTemperature = 32767;
60 std::int16_t maximumTemperature = -32768;
62 std::uint64_t revision = 0;
63 bool active = true;
64 bool touched = true;
65 std::uint8_t idleTicks = 0;
66
67 static std::size_t index(int x, int y) noexcept {
68 return std::size_t(y * kPixelChunkSize + x);
69 }
70};
71
72eve::Status malformed(std::string message) {
74 eve::DiagnosticCode::SerializationError, message, "snapshot", {}, "pixelworld"));
75}
76
77template <class T>
78void append(std::vector<std::byte>& out, T value) {
79 const auto* first = reinterpret_cast<const std::byte*>(&value);
80 out.insert(out.end(), first, first + sizeof(T));
81}
82
83template <class T>
84bool read(std::span<const std::byte> bytes, std::size_t& cursor, T& value) {
85 if (cursor > bytes.size() || bytes.size() - cursor < sizeof(T)) return false;
86 std::memcpy(&value, bytes.data() + cursor, sizeof(T));
87 cursor += sizeof(T);
88 return true;
89}
90
91} // namespace
92
95 : catalog(std::move(ownedCatalog)) {
97 }
98
99 std::unordered_map<ChunkCoord, Chunk, ChunkCoordHash> chunks;
100 std::unordered_map<ChunkCoord, std::uint64_t, ChunkCoordHash> removedChunks;
102 std::vector<std::uint8_t> canDisplaceTable;
103 std::vector<MaterialState> materialStates;
104 std::vector<std::uint8_t> thermalConductivities;
105 std::vector<std::uint16_t> heatCapacities;
106 std::vector<std::vector<std::uint16_t>> reactionRulesByPair;
107 std::size_t materialRuntimeCount = 0;
108 std::uint64_t seed = 1;
109 std::uint64_t revision = 0;
110 std::uint64_t tick = 0;
111 std::uint64_t lastEditSequence = 0;
112 std::uint64_t worldId = nextPixelWorldId.fetch_add(1, std::memory_order_relaxed);
113 std::uint64_t epoch = 1;
114 std::uint64_t nextFragmentId = 1;
115 bool paused = false;
116
117 const Chunk* findChunk(int x, int y) const noexcept {
118 const auto found = chunks.find({floorDiv(x), floorDiv(y)});
119 return found == chunks.end() ? nullptr : &found->second;
120 }
121
122 Chunk* findChunk(int x, int y) noexcept {
123 const auto found = chunks.find({floorDiv(x), floorDiv(y)});
124 return found == chunks.end() ? nullptr : &found->second;
125 }
126
127 Chunk& ensureChunk(int x, int y) { return chunks[{floorDiv(x), floorDiv(y)}]; }
128
129 bool isMobile(MaterialId material) const noexcept {
130 const std::size_t index = std::size_t(material);
131 const auto state = index < materialStates.size() ? materialStates[index]
134 }
135
137 const std::size_t index = std::size_t(material);
139 }
140
148 for (std::size_t source = 0; source < materialRuntimeCount; ++source) {
149 const auto& sourceDef = catalog.definition(MaterialId(source));
150 materialStates[source] = sourceDef.state;
151 thermalConductivities[source] = sourceDef.thermalConductivity;
152 heatCapacities[source] = sourceDef.heatCapacity;
153 for (std::size_t target = 0; target < materialRuntimeCount; ++target) {
154 const auto& targetDef = catalog.definition(MaterialId(target));
155 const bool targetFluid = targetDef.state == MaterialState::Empty ||
156 targetDef.state == MaterialState::Gas ||
157 targetDef.state == MaterialState::Liquid;
159 std::uint8_t(targetFluid && targetDef.density < sourceDef.density);
160 }
161 }
162 const auto reactions = catalog.reactions();
163 for (std::size_t ruleIndex = 0; ruleIndex < reactions.size(); ++ruleIndex) {
164 const auto& rule = reactions[ruleIndex];
165 const std::size_t first = std::size_t(rule.first);
166 const std::size_t second = std::size_t(rule.second);
168 ruleIndex > std::size_t(std::numeric_limits<std::uint16_t>::max()))
169 continue;
171 std::uint16_t(ruleIndex));
172 if (first != second)
174 std::uint16_t(ruleIndex));
175 }
176 }
177
179 const std::size_t sourceIndex = std::size_t(source);
180 const std::size_t targetIndex = std::size_t(target);
181 if (sourceIndex >= materialRuntimeCount || targetIndex >= materialRuntimeCount) return false;
182 return canDisplaceTable[sourceIndex * materialRuntimeCount + targetIndex] != 0;
183 }
184
185 std::span<const std::uint16_t> reactionRules(MaterialId source,
186 MaterialId target) const noexcept {
187 const std::size_t sourceIndex = std::size_t(source);
188 const std::size_t targetIndex = std::size_t(target);
189 if (sourceIndex >= materialRuntimeCount || targetIndex >= materialRuntimeCount) return {};
190 return reactionRulesByPair[sourceIndex * materialRuntimeCount + targetIndex];
191 }
192
193 std::uint8_t thermalConductivity(MaterialId material) const noexcept {
194 const std::size_t index = std::size_t(material);
196 }
197
198 std::uint16_t heatCapacity(MaterialId material) const noexcept {
199 const std::size_t index = std::size_t(material);
200 return index < heatCapacities.size() ? heatCapacities[index] : 1;
201 }
202
203 void addActivity(Chunk& chunk, const PixelCell& cell) const noexcept {
204 if (cell.material == MaterialId::Air) return;
205 ++chunk.nonAir;
206 if (isMobile(cell.material)) ++chunk.mobile;
207 if (cell.thermalRemainder != 0) ++chunk.thermalRemainderCells;
208 const auto material = std::size_t(cell.material);
209 if (material < chunk.materialCounts.size())
210 ++chunk.materialCounts[material];
211 else
212 ++chunk.materialOverflow;
213 chunk.minimumTemperature = std::min(chunk.minimumTemperature, cell.temperature);
214 chunk.maximumTemperature = std::max(chunk.maximumTemperature, cell.temperature);
215 }
216
217 void removeActivity(Chunk& chunk, const PixelCell& cell) const noexcept {
218 if (cell.material == MaterialId::Air) return;
219 --chunk.nonAir;
220 if (isMobile(cell.material)) --chunk.mobile;
221 if (cell.thermalRemainder != 0) --chunk.thermalRemainderCells;
222 const auto material = std::size_t(cell.material);
223 if (material < chunk.materialCounts.size())
224 --chunk.materialCounts[material];
225 else
226 --chunk.materialOverflow;
227 if (cell.temperature == chunk.minimumTemperature || cell.temperature == chunk.maximumTemperature)
228 chunk.temperatureBoundsDirty = true;
229 }
230
231 void rebuildActivity(Chunk& chunk) const noexcept {
232 chunk.materialCounts.fill(0);
233 chunk.nonAir = 0;
234 chunk.mobile = 0;
235 chunk.thermalRemainderCells = 0;
236 chunk.materialOverflow = 0;
237 chunk.minimumTemperature = 32767;
238 chunk.maximumTemperature = -32768;
239 chunk.temperatureBoundsDirty = false;
240 for (const PixelCell& cell : chunk.cells) addActivity(chunk, cell);
241 }
242
243 void refreshTemperatureBounds(Chunk& chunk) const noexcept {
244 if (!chunk.temperatureBoundsDirty) return;
245 chunk.minimumTemperature = 32767;
246 chunk.maximumTemperature = -32768;
247 for (const PixelCell& cell : chunk.cells) {
248 if (cell.material == MaterialId::Air) continue;
249 chunk.minimumTemperature = std::min(chunk.minimumTemperature, cell.temperature);
250 chunk.maximumTemperature = std::max(chunk.maximumTemperature, cell.temperature);
251 }
252 chunk.temperatureBoundsDirty = false;
253 }
254
255 PixelCell get(int x, int y) const noexcept {
256 const Chunk* chunk = findChunk(x, y);
257 return chunk ? chunk->cells[Chunk::index(floorMod(x), floorMod(y))] : PixelCell{};
258 }
259
261 if (cell.material == MaterialId::Air) {
263 return cell;
264 }
265 const std::int64_t capacity = catalog.definition(cell.material).heatCapacity;
266 std::int64_t energy = std::int64_t(cell.temperature) * capacity + cell.thermalRemainder;
267 std::int64_t temperature = energy / capacity;
268 std::int64_t remainder = energy % capacity;
269 if (remainder < 0) {
270 --temperature;
271 remainder += capacity;
272 }
273 if (temperature < std::numeric_limits<std::int16_t>::min()) {
274 temperature = std::numeric_limits<std::int16_t>::min();
275 remainder = 0;
276 } else if (temperature > std::numeric_limits<std::int16_t>::max()) {
277 temperature = std::numeric_limits<std::int16_t>::max();
278 remainder = capacity - 1;
279 }
280 cell.temperature = std::int16_t(temperature);
281 cell.thermalRemainder = std::uint16_t(remainder);
282 return cell;
283 }
284
285 std::uint64_t updatedAt(int x, int y) const noexcept {
286 const Chunk* chunk = findChunk(x, y);
287 return chunk ? chunk->updated[Chunk::index(floorMod(x), floorMod(y))] : 0;
288 }
289
290 void wakeAround(int x, int y) {
291 const int cx = floorDiv(x), cy = floorDiv(y);
292 for (int oy = -1; oy <= 1; ++oy)
293 for (int ox = -1; ox <= 1; ++ox) {
294 const auto found = chunks.find({cx + ox, cy + oy});
295 if (found != chunks.end()) found->second.active = true;
296 }
297 }
298
299 void put(int x, int y, PixelCell cell, std::uint64_t updatedTick, bool markUpdated = true) {
301 removedChunks.erase({floorDiv(x), floorDiv(y)});
302 Chunk& chunk = ensureChunk(x, y);
303 const auto index = Chunk::index(floorMod(x), floorMod(y));
304 const PixelCell oldCell = chunk.cells[index];
305 if (oldCell != cell) {
306 removeActivity(chunk, oldCell);
307 addActivity(chunk, cell);
308 }
309 chunk.cells[index] = cell;
310 if (markUpdated) chunk.updated[index] = updatedTick;
311 chunk.revision = revision + 1;
312 chunk.active = true;
313 chunk.touched = true;
314 }
315
316 void putNormalizedWithinChunk(Chunk& chunk, std::size_t index, const PixelCell& cell,
317 std::uint64_t updatedTick, bool markUpdated = true) {
318 const PixelCell oldCell = chunk.cells[index];
319 if (oldCell != cell) {
320 removeActivity(chunk, oldCell);
321 addActivity(chunk, cell);
322 }
323 chunk.cells[index] = cell;
324 if (markUpdated) chunk.updated[index] = updatedTick;
325 chunk.revision = revision + 1;
326 chunk.active = true;
327 chunk.touched = true;
328 }
329
330 void putWithinChunk(Chunk& chunk, std::size_t index, PixelCell cell,
331 std::uint64_t updatedTick, bool markUpdated = true) {
332 putNormalizedWithinChunk(chunk, index, normalizeCell(cell), updatedTick, markUpdated);
333 }
334
335 bool moveOrSwap(int x, int y, int tx, int ty, std::uint64_t currentTick) {
336 const PixelCell source = get(x, y);
337 return moveOrSwapKnownSource(x, y, source, tx, ty, currentTick);
338 }
339
340 bool moveOrSwapKnownSource(int x, int y, PixelCell source, int tx, int ty,
341 std::uint64_t currentTick) {
342 const PixelCell target = get(tx, ty);
343 if (!canDisplace(source.material, target.material)) return false;
344 put(tx, ty, source, currentTick);
345 put(x, y, target, currentTick);
346 wakeAround(tx, ty);
347 return true;
348 }
349
350 bool moveOrSwapWithinChunk(ChunkCoord coord, Chunk& chunk, int sourceX, int sourceY,
351 PixelCell source, int targetX, int targetY,
352 std::uint64_t currentTick) {
353 const auto sourceIndex = Chunk::index(sourceX, sourceY);
354 const auto targetIndex = Chunk::index(targetX, targetY);
355 const PixelCell target = chunk.cells[targetIndex];
356 if (!canDisplace(source.material, target.material)) return false;
357 chunk.cells[targetIndex] = source;
358 chunk.cells[sourceIndex] = target;
359 chunk.updated[targetIndex] = currentTick;
360 chunk.updated[sourceIndex] = currentTick;
361 chunk.revision = revision + 1;
362 chunk.active = true;
363 chunk.touched = true;
364 const bool touchesBoundary = sourceX == 0 || sourceX == kPixelChunkSize - 1 ||
365 sourceY == 0 || sourceY == kPixelChunkSize - 1 ||
366 targetX == 0 || targetX == kPixelChunkSize - 1 ||
367 targetY == 0 || targetY == kPixelChunkSize - 1;
368 if (touchesBoundary)
370 coord.y * kPixelChunkSize + targetY);
371 return true;
372 }
373
374 bool pseudoBit(int x, int y, std::uint64_t currentTick) const noexcept {
375 std::uint64_t value = seed ^ (std::uint64_t(std::uint32_t(x)) << 32U) ^
376 std::uint64_t(std::uint32_t(y)) ^ (currentTick * 0x9E3779B97F4A7C15ULL);
377 value ^= value >> 30U;
378 value *= 0xBF58476D1CE4E5B9ULL;
379 value ^= value >> 27U;
380 return (value & 1U) != 0;
381 }
382};
383
384PixelWorld::PixelWorld(std::uint64_t seed) : impl_(std::make_unique<Impl>()) {
385 impl_->seed = seed;
386 pixelWorldControlService().registerWorld(*this);
387}
389 : impl_(std::make_unique<Impl>(std::move(catalog))) {
390 impl_->seed = seed;
391 pixelWorldControlService().registerWorld(*this);
392}
394 if (impl_) pixelWorldControlService().unregisterWorld(*this);
395}
396PixelWorld::PixelWorld(PixelWorld&& other) noexcept : impl_(std::move(other.impl_)) {
397 if (impl_) pixelWorldControlService().rebindWorld(&other, *this);
398}
400 if (this == &other) return *this;
401 if (impl_) pixelWorldControlService().unregisterWorld(*this);
402 impl_ = std::move(other.impl_);
403 if (impl_) pixelWorldControlService().rebindWorld(&other, *this);
404 return *this;
405}
406
407PixelCell PixelWorld::getCell(int x, int y) const noexcept { return impl_->get(x, y); }
408int PixelWorld::getMaterial(int x, int y) const noexcept { return int(impl_->get(x, y).material); }
410 return impl_->catalog.definition(material).state == MaterialState::Solid;
411}
413 return impl_->catalog.definition(material).displayRgba;
414}
415
417 MaterialCatalog catalog, std::uint64_t expectedFingerprint) {
418 const auto reject = [](eve::DiagnosticCode code, std::string message, std::string path) {
420 code, message, path, {}, "pixelworld.catalog-reload"));
421 };
422 if (!impl_->paused)
424 "material Catalog reload requires a paused world", "paused");
425 if (expectedFingerprint != impl_->catalog.fingerprint())
426 return reject(eve::DiagnosticCode::Conflict,
427 "material Catalog expected fingerprint is stale", "expectedFingerprint");
428 const auto current = impl_->catalog.definitions();
429 const auto replacement = catalog.definitions();
430 if (current.size() != replacement.size())
431 return reject(eve::DiagnosticCode::Conflict,
432 "live Catalog reload cannot add or remove material ids", "materials");
433 for (std::size_t index = 0; index < current.size(); ++index)
434 if (current[index].id != replacement[index].id || current[index].name != replacement[index].name)
435 return reject(eve::DiagnosticCode::Conflict,
436 "live Catalog reload must preserve every material id and name",
437 "materials[" + std::to_string(index) + "]");
438
440 receipt.fingerprintBefore = impl_->catalog.fingerprint();
441 receipt.fingerprintAfter = catalog.fingerprint();
442 receipt.revisionBefore = impl_->revision;
443 if (receipt.fingerprintBefore == receipt.fingerprintAfter) {
444 receipt.revisionAfter = impl_->revision;
445 receipt.worldEpoch = impl_->epoch;
447 }
448 impl_->catalog = std::move(catalog);
449 impl_->rebuildMaterialRuntimeTables();
450 ++impl_->revision;
451 ++impl_->epoch;
452 impl_->lastEditSequence = 0;
453 for (auto& [coord, chunk] : impl_->chunks) {
454 for (PixelCell& cell : chunk.cells) cell = impl_->normalizeCell(cell);
455 impl_->rebuildActivity(chunk);
456 impl_->refreshTemperatureBounds(chunk);
457 chunk.revision = impl_->revision;
458 chunk.active = true;
459 chunk.touched = true;
460 chunk.idleTicks = 0;
461 ++receipt.chunksRebuilt;
462 }
463 receipt.revisionAfter = impl_->revision;
464 receipt.worldEpoch = impl_->epoch;
465 receipt.replayHistoryInvalidated = true;
467}
468
470 if (cell.material == MaterialId::Air && impl_->findChunk(x, y) == nullptr) return;
471 impl_->put(x, y, cell, impl_->tick);
472 impl_->wakeAround(x, y);
473 ++impl_->revision;
474}
475
476void PixelWorld::setMaterial(int x, int y, std::string_view material) {
477 setMaterialChecked(x, y, material).expect("PixelWorld::setMaterial requires a registered material");
478}
479
481 auto resolved = impl_->catalog.resolve(material);
482 if (!resolved.ok()) return eve::Result<void>::failure(resolved.status());
484 cell.material = resolved.value();
485 cell.temperature = impl_->catalog.definition(cell.material).defaultTemperature;
486 cell.lifetime = std::uint8_t(std::min<std::uint16_t>(impl_->catalog.definition(cell.material).defaultLifetime,
487 std::numeric_limits<std::uint8_t>::max()));
488 setCell(x, y, cell);
490}
491
492std::size_t PixelWorld::paintCircle(int centerX, int centerY, int radius, std::string_view material) {
493 return std::move(paintCircleChecked(centerX, centerY, radius, material))
494 .expect("PixelWorld::paintCircle requires a registered material");
495}
496
498 std::string_view material) {
499 if (radius < 0) return eve::Result<std::size_t>::success(0);
500 std::size_t changed = 0;
501 auto resolved = impl_->catalog.resolve(material);
502 if (!resolved.ok()) return eve::Result<std::size_t>::failure(resolved.status());
503 const MaterialId id = resolved.value();
504 for (int y = centerY - radius; y <= centerY + radius; ++y)
505 for (int x = centerX - radius; x <= centerX + radius; ++x) {
506 const int dx = x - centerX, dy = y - centerY;
507 if (dx * dx + dy * dy > radius * radius || impl_->get(x, y).material == id) continue;
509 cell.material = id;
510 cell.temperature = impl_->catalog.definition(id).defaultTemperature;
511 cell.lifetime = std::uint8_t(std::min<std::uint16_t>(impl_->catalog.definition(id).defaultLifetime,
512 std::numeric_limits<std::uint8_t>::max()));
513 setCell(x, y, cell);
514 ++changed;
515 }
516 return eve::Result<std::size_t>::success(changed);
517}
518
520 const auto reject = [](std::string message, std::string path) {
523 "pixelworld.edit"));
524 };
525 if (command.sequence != impl_->lastEditSequence + 1)
526 return reject("edit sequence must be exactly previous sequence + 1", "sequence");
527 if (command.radius < 0 || command.radius > 4096)
528 return reject("edit radius must be in [0, 4096]", "radius");
529 if (command.centerX < -100'000'000 || command.centerX > 100'000'000 ||
530 command.centerY < -100'000'000 || command.centerY > 100'000'000)
531 return reject("edit center is outside the supported coordinate range", "center");
532 if (command.kind == PixelEditKind::PaintCircle &&
533 std::size_t(command.material) >= impl_->catalog.definitions().size())
534 return reject("paint command references an unknown material id", "material");
535 if (command.kind == PixelEditKind::Explosion && (command.strength < 0 || command.strength > 1'000'000))
536 return reject("explosion strength must be in [0, 1000000]", "strength");
537
538 struct Candidate {
539 int x = 0;
540 int y = 0;
542 bool removed = false;
543 bool heated = false;
544 };
545 std::vector<Candidate> candidates;
546 const std::int64_t radiusSquared = std::int64_t(command.radius) * command.radius;
547 for (int y = command.centerY - command.radius; y <= command.centerY + command.radius; ++y)
548 for (int x = command.centerX - command.radius; x <= command.centerX + command.radius; ++x) {
549 const std::int64_t dx = std::int64_t(x) - command.centerX;
550 const std::int64_t dy = std::int64_t(y) - command.centerY;
551 const std::int64_t distanceSquared = dx * dx + dy * dy;
552 if (distanceSquared > radiusSquared) continue;
553 const PixelCell oldCell = impl_->get(x, y);
554 PixelCell newCell = oldCell;
555 bool removed = false;
556 bool heated = false;
557 if (command.kind == PixelEditKind::PaintCircle) {
558 newCell = {};
559 newCell.material = command.material;
560 const auto& definition = impl_->catalog.definition(command.material);
561 newCell.temperature = definition.defaultTemperature;
562 newCell.lifetime = std::uint8_t(std::min<std::uint16_t>(
563 definition.defaultLifetime, std::numeric_limits<std::uint8_t>::max()));
564 } else if (command.kind == PixelEditKind::HeatCircle) {
565 if (oldCell.material == MaterialId::Air) continue;
566 newCell.temperature = std::int16_t(std::clamp<int>(
567 int(oldCell.temperature) + command.temperatureDelta, -32768, 32767));
568 heated = newCell.temperature != oldCell.temperature;
569 } else {
570 if (oldCell.material == MaterialId::Air) continue;
571 const std::int64_t denominator = std::max<std::int64_t>(1, radiusSquared);
572 const int radialStrength = int(std::int64_t(command.strength) *
573 (radiusSquared - distanceSquared + 1) / denominator);
574 if (radialStrength >= impl_->catalog.definition(oldCell.material).blastResistance) {
575 newCell = {};
576 removed = true;
577 } else if (command.temperatureDelta != 0) {
578 const int radialHeat = int(std::int64_t(command.temperatureDelta) *
579 (radiusSquared - distanceSquared + 1) / denominator);
580 newCell.temperature = std::int16_t(std::clamp<int>(
581 int(oldCell.temperature) + radialHeat, -32768, 32767));
582 heated = newCell.temperature != oldCell.temperature;
583 }
584 }
585 if (newCell == oldCell) continue;
586 candidates.push_back({x, y, newCell, removed, heated});
587 }
588
589 PixelEditReceipt receipt;
590 receipt.sequence = command.sequence;
591 receipt.revisionBefore = impl_->revision;
592 for (const Candidate& candidate : candidates) {
593 impl_->put(candidate.x, candidate.y, candidate.cell, impl_->tick);
594 impl_->wakeAround(candidate.x, candidate.y);
595 ++receipt.cellsChanged;
596 if (candidate.removed) ++receipt.cellsRemoved;
597 if (candidate.heated) ++receipt.cellsHeated;
598 }
599 impl_->lastEditSequence = command.sequence;
600 ++impl_->revision;
601 receipt.revisionAfter = impl_->revision;
603}
604
605PixelEditReceipt PixelWorld::explode(int centerX, int centerY, int radius, int strength,
606 std::int16_t temperatureDelta) {
607 PixelEditCommand command;
608 command.sequence = impl_->lastEditSequence + 1;
610 command.centerX = centerX;
611 command.centerY = centerY;
612 command.radius = radius;
613 command.strength = strength;
614 command.temperatureDelta = temperatureDelta;
615 return std::move(applyEdit(command)).expect("PixelWorld::explode generated an invalid edit command");
616}
617
619 PixelRegion region, int supportY, std::uint32_t minimumCells) {
620 const auto reject = [](eve::DiagnosticCode code, std::string message, std::string path) {
622 code, message, path, {}, "pixelworld.fragment.extract"));
623 };
624 const std::int64_t width = std::int64_t(region.maxX) - region.minX + 1;
625 const std::int64_t height = std::int64_t(region.maxY) - region.minY + 1;
626 if (region.minX < -100'000'000 || region.maxX > 100'000'000 ||
627 region.minY < -100'000'000 || region.maxY > 100'000'000)
629 "fragment scan region is outside the supported coordinate range", "region");
630 if (width <= 0 || height <= 0 || width * height > 16'777'216)
632 "fragment scan region must be non-empty and contain at most 16777216 cells", "region");
633 if (minimumCells == 0)
634 return reject(eve::DiagnosticCode::InvalidArgument, "minimumCells must be positive", "minimumCells");
635
636 using Coord = std::pair<int, int>;
637 std::set<Coord> visited;
638 std::vector<PixelFragment> fragments;
639 constexpr std::array<Coord, 4> neighbors{{{0, -1}, {-1, 0}, {1, 0}, {0, 1}}};
640 const auto isSolid = [this](int x, int y) {
641 return impl_->catalog.definition(impl_->get(x, y).material).state == MaterialState::Solid;
642 };
643
644 for (int y = region.minY; y <= region.maxY; ++y)
645 for (int x = region.minX; x <= region.maxX; ++x) {
646 if (!isSolid(x, y) || visited.contains({x, y})) continue;
647 std::deque<Coord> pending{{x, y}};
648 std::vector<Coord> component;
649 visited.emplace(x, y);
650 bool supported = false;
651 int minX = x, maxX = x, minY = y, maxY = y;
652 while (!pending.empty()) {
653 const auto [cx, cy] = pending.front();
654 pending.pop_front();
655 component.emplace_back(cx, cy);
656 minX = std::min(minX, cx);
657 maxX = std::max(maxX, cx);
658 minY = std::min(minY, cy);
659 maxY = std::max(maxY, cy);
660 if (cy >= supportY) supported = true;
661 for (const auto& [dx, dy] : neighbors) {
662 const int nx = cx + dx, ny = cy + dy;
663 const bool inside = nx >= region.minX && nx <= region.maxX &&
664 ny >= region.minY && ny <= region.maxY;
665 if (!inside) {
666 if (isSolid(nx, ny)) supported = true;
667 continue;
668 }
669 if (isSolid(nx, ny) && visited.emplace(nx, ny).second)
670 pending.emplace_back(nx, ny);
671 }
672 }
673 if (supported || component.size() < minimumCells) continue;
674
675 PixelFragment fragment;
676 fragment.source = worldLink();
677 fragment.id = impl_->nextFragmentId++;
678 fragment.originX = minX;
679 fragment.originY = minY;
680 fragment.width = maxX - minX + 1;
681 fragment.height = maxY - minY + 1;
682 fragment.solidCellCount = std::uint32_t(component.size());
683 fragment.cells.resize(std::size_t(fragment.width) * std::size_t(fragment.height));
684 for (const auto& [cx, cy] : component)
685 fragment.cells[std::size_t(cy - minY) * std::size_t(fragment.width) + std::size_t(cx - minX)] =
686 impl_->get(cx, cy);
687 fragments.push_back(std::move(fragment));
688 }
689
690 if (!fragments.empty()) {
691 for (const PixelFragment& fragment : fragments)
692 for (int y = 0; y < fragment.height; ++y)
693 for (int x = 0; x < fragment.width; ++x) {
694 const PixelCell& cell = fragment.cells[std::size_t(y) * std::size_t(fragment.width) + x];
695 if (cell.material == MaterialId::Air) continue;
696 impl_->put(fragment.originX + x, fragment.originY + y, {}, impl_->tick);
697 impl_->wakeAround(fragment.originX + x, fragment.originY + y);
698 }
699 ++impl_->revision;
700 }
701 return eve::Result<std::vector<PixelFragment>>::success(std::move(fragments));
702}
703
705 const PixelFragment& fragment, int originX, int originY) {
706 const auto reject = [](eve::DiagnosticCode code, std::string message, std::string path) {
708 code, message, path, {}, "pixelworld.fragment.rasterize"));
709 };
710 if (fragment.source != worldLink())
712 "fragment belongs to another world or an invalidated world epoch", "source");
713 if (fragment.id == 0 || fragment.width <= 0 || fragment.height <= 0 ||
714 std::uint64_t(fragment.width) * std::uint64_t(fragment.height) != fragment.cells.size())
715 return reject(eve::DiagnosticCode::InvalidArgument, "fragment bitmap metadata is invalid", "fragment");
716 std::uint32_t cellsPlaced = 0;
717 for (int y = 0; y < fragment.height; ++y)
718 for (int x = 0; x < fragment.width; ++x) {
719 const PixelCell& cell = fragment.cells[std::size_t(y) * std::size_t(fragment.width) + x];
720 if (cell.material == MaterialId::Air) continue;
721 if (std::size_t(cell.material) >= impl_->catalog.definitions().size())
722 return reject(eve::DiagnosticCode::InvalidArgument, "fragment contains an unknown material", "cells");
723 const std::int64_t wx = std::int64_t(originX) + x, wy = std::int64_t(originY) + y;
724 if (wx < std::numeric_limits<int>::min() || wx > std::numeric_limits<int>::max() ||
725 wy < std::numeric_limits<int>::min() || wy > std::numeric_limits<int>::max())
726 return reject(eve::DiagnosticCode::InvalidArgument, "fragment target coordinates overflow", "origin");
727 if (impl_->get(int(wx), int(wy)).material != MaterialId::Air)
728 return reject(eve::DiagnosticCode::Conflict, "fragment target overlaps authoritative terrain", "origin");
729 ++cellsPlaced;
730 }
731 if (cellsPlaced != fragment.solidCellCount)
732 return reject(eve::DiagnosticCode::InvalidArgument, "fragment solid cell count does not match bitmap", "solidCellCount");
733
735 receipt.fragmentId = fragment.id;
736 receipt.revisionBefore = impl_->revision;
737 receipt.cellsPlaced = cellsPlaced;
738 for (int y = 0; y < fragment.height; ++y)
739 for (int x = 0; x < fragment.width; ++x) {
740 const PixelCell& cell = fragment.cells[std::size_t(y) * std::size_t(fragment.width) + x];
741 if (cell.material == MaterialId::Air) continue;
742 impl_->put(originX + x, originY + y, cell, impl_->tick);
743 impl_->wakeAround(originX + x, originY + y);
744 }
745 if (cellsPlaced != 0) ++impl_->revision;
746 receipt.revisionAfter = impl_->revision;
748}
749
750PixelWorldLink PixelWorld::worldLink() const noexcept { return {impl_->worldId, impl_->epoch}; }
751
752void PixelWorld::clear() noexcept {
753 impl_->chunks.clear();
754 impl_->removedChunks.clear();
755 impl_->revision = 0;
756 impl_->tick = 0;
757 impl_->lastEditSequence = 0;
758 ++impl_->epoch;
759}
760
762 if (impl_->paused) return eve::Result<StepStats>::success({eve::SimulationTick(impl_->tick)});
763 return advanceImpl(tick, nullptr);
764}
765
767 PixelWorkScheduler& scheduler) {
768 if (impl_->paused) return eve::Result<StepStats>::success({eve::SimulationTick(impl_->tick)});
769 return advanceImpl(tick, &scheduler);
770}
771
772eve::Result<StepStats> PixelWorld::advanceImpl(eve::SimulationTick tick,
773 PixelWorkScheduler* scheduler) {
774 if (tick.value() <= impl_->tick)
777 "PixelWorld ticks must increase monotonically", "tick", {}, "pixelworld"));
778
779 const auto stepStarted = std::chrono::steady_clock::now();
780 impl_->tick = tick.value();
781 StepStats stats;
782 stats.tick = tick;
783 std::vector<ChunkCoord> order;
784 order.reserve(impl_->chunks.size());
785 for (auto& [coord, chunk] : impl_->chunks) {
786 if (chunk.active) order.push_back(coord);
787 chunk.touched = false;
788 }
789 std::sort(order.begin(), order.end());
790
791 struct ChunkPhaseSummary {
792 std::uint32_t nonAir = 0;
793 std::uint32_t mobile = 0;
794 std::uint32_t thermalRemainderCells = 0;
795 std::uint64_t materialMask = 0;
796 std::int16_t minimumTemperature = 32767;
797 std::int16_t maximumTemperature = -32768;
798 bool materialMaskOverflow = false;
799 };
800 const auto summarize = [](const Chunk& chunk) {
801 ChunkPhaseSummary summary;
802 summary.nonAir = chunk.nonAir;
803 summary.mobile = chunk.mobile;
804 summary.thermalRemainderCells = chunk.thermalRemainderCells;
805 summary.minimumTemperature = chunk.minimumTemperature;
806 summary.maximumTemperature = chunk.maximumTemperature;
807 summary.materialMaskOverflow = chunk.materialOverflow != 0;
808 for (std::size_t material = 0; material < chunk.materialCounts.size(); ++material)
809 if (chunk.materialCounts[material] != 0)
810 summary.materialMask |= std::uint64_t(1) << material;
811 return summary;
812 };
813 std::unordered_map<ChunkCoord, ChunkPhaseSummary, ChunkCoordHash> phaseSummaries;
814 const auto rebuildSummaries = [&] {
815 phaseSummaries.clear();
816 phaseSummaries.reserve(impl_->chunks.size());
817 for (auto& [coord, chunk] : impl_->chunks) {
818 impl_->refreshTemperatureBounds(chunk);
819 phaseSummaries.emplace(coord, summarize(chunk));
820 }
821 };
822 rebuildSummaries();
823
824 struct MovementRows {
825 std::array<std::uint64_t, kPixelChunkSize> mobile{};
826 std::array<std::uint64_t, kPixelChunkSize> rightFirst{};
827 };
828 std::vector<MovementRows> movementCandidates(order.size());
829 std::vector<std::size_t> movementWork;
830 movementWork.reserve(order.size());
831 for (std::size_t index = 0; index < order.size(); ++index) {
832 const auto summary = phaseSummaries.find(order[index]);
833 if (summary != phaseSummaries.end() && summary->second.mobile != 0)
834 movementWork.push_back(index);
835 }
836 const auto generateMovementCandidates = [&](std::size_t workIndex) {
837 const std::size_t orderIndex = movementWork[workIndex];
838 const ChunkCoord coord = order[orderIndex];
839 auto& candidates = movementCandidates[orderIndex];
840 candidates.mobile.fill(0);
841 candidates.rightFirst.fill(0);
842 const auto found = impl_->chunks.find(coord);
843 if (found == impl_->chunks.end()) return;
844 const Chunk& chunk = found->second;
845 for (int ly = kPixelChunkSize - 1; ly >= 0; --ly) {
846 for (int lx = 0; lx < kPixelChunkSize; ++lx) {
847 const auto cellIndex = Chunk::index(lx, ly);
848 if (chunk.updated[cellIndex] == tick.value()) continue;
849 const PixelCell cell = chunk.cells[cellIndex];
850 const auto state = impl_->materialState(cell.material);
851 if (state != MaterialState::Powder && state != MaterialState::Liquid) continue;
852 const int x = coord.x * kPixelChunkSize + lx;
853 const int y = coord.y * kPixelChunkSize + ly;
854 const int direction = impl_->pseudoBit(x, y, tick.value()) ? 1 : -1;
855 const std::uint64_t bit = std::uint64_t(1) << lx;
856 candidates.mobile[std::size_t(ly)] |= bit;
857 if (direction > 0) candidates.rightFirst[std::size_t(ly)] |= bit;
858 }
859 }
860 };
861 if (scheduler && movementWork.size() > 1) {
862 scheduler->parallelFor(movementWork.size(), generateMovementCandidates);
863 stats.parallelTasks += std::uint32_t(movementWork.size());
864 } else {
865 for (std::size_t index = 0; index < movementWork.size(); ++index)
866 generateMovementCandidates(index);
867 }
868 for (std::size_t orderIndex = 0; orderIndex < order.size(); ++orderIndex) {
869 ++stats.chunksVisited;
870 const ChunkCoord sourceCoord = order[orderIndex];
871 auto sourceFound = impl_->chunks.find(sourceCoord);
872 if (sourceFound == impl_->chunks.end()) continue;
873 Chunk& sourceChunk = sourceFound->second;
874 const MovementRows& candidates = movementCandidates[orderIndex];
875 for (int sourceLocalY = kPixelChunkSize - 1; sourceLocalY >= 0; --sourceLocalY) {
876 std::uint64_t remaining = candidates.mobile[std::size_t(sourceLocalY)];
877 const bool leftFirst = ((tick.value() + std::uint64_t(sourceLocalY)) & 1U) == 0;
878 while (remaining != 0) {
879 const int sourceLocalX = leftFirst
880 ? int(std::countr_zero(remaining))
881 : 63 - int(std::countl_zero(remaining));
882 const std::uint64_t candidateBit = std::uint64_t(1) << sourceLocalX;
883 remaining &= ~candidateBit;
884 const int direction =
885 (candidates.rightFirst[std::size_t(sourceLocalY)] & candidateBit) != 0 ? 1 : -1;
886 const auto sourceIndex = Chunk::index(sourceLocalX, sourceLocalY);
887 if (sourceChunk.updated[sourceIndex] == tick.value()) continue;
888 const PixelCell cell = sourceChunk.cells[sourceIndex];
889 const auto state = impl_->materialState(cell.material);
890 if (state != MaterialState::Powder && state != MaterialState::Liquid) continue;
891 ++stats.cellsVisited;
892 const auto tryMove = [&](int targetLocalX, int targetLocalY) {
893 if (targetLocalX >= 0 && targetLocalX < kPixelChunkSize &&
894 targetLocalY >= 0 && targetLocalY < kPixelChunkSize)
895 return impl_->moveOrSwapWithinChunk(
896 sourceCoord, sourceChunk, sourceLocalX, sourceLocalY, cell,
897 targetLocalX, targetLocalY, tick.value());
898 const int sourceX = sourceCoord.x * kPixelChunkSize + sourceLocalX;
899 const int sourceY = sourceCoord.y * kPixelChunkSize + sourceLocalY;
900 return impl_->moveOrSwapKnownSource(
901 sourceX, sourceY, cell,
902 sourceCoord.x * kPixelChunkSize + targetLocalX,
903 sourceCoord.y * kPixelChunkSize + targetLocalY, tick.value());
904 };
905 bool moved = tryMove(sourceLocalX, sourceLocalY + 1);
906 if (!moved)
907 moved = tryMove(sourceLocalX + direction, sourceLocalY + 1);
908 if (!moved)
909 moved = tryMove(sourceLocalX - direction, sourceLocalY + 1);
910 if (!moved && state == MaterialState::Liquid)
911 moved = tryMove(sourceLocalX + direction, sourceLocalY);
912 if (!moved && state == MaterialState::Liquid)
913 moved = tryMove(sourceLocalX - direction, sourceLocalY);
914 if (moved) {
915 ++stats.cellsMoved;
916 stats.cellsChanged += 2;
917 }
918 }
919 }
920 }
921
922 if (stats.cellsMoved != 0) rebuildSummaries();
923
924 // Thermal phase: calculate all pair transfers from the same pre-phase state,
925 // then apply deltas in canonical coordinate order. Integer arithmetic makes
926 // the reference backend bit-exact and independent of unordered-map order.
927 struct ThermalChunkContributions {
928 std::array<std::int64_t, kPixelChunkSize * kPixelChunkSize> local{};
929 std::array<std::int64_t, kPixelChunkSize> rightHalo{};
930 std::array<std::int64_t, kPixelChunkSize> bottomHalo{};
931 };
932 std::vector<ThermalChunkContributions> thermalCandidates(order.size());
933 std::vector<std::uint32_t> transferCounts(order.size());
934 std::vector<std::uint64_t> transferEnergy(order.size());
935 const auto hasPossibleThermalGradient = [&](ChunkCoord coord) {
936 const auto ownSummary = phaseSummaries.find(coord);
937 if (ownSummary == phaseSummaries.end() || ownSummary->second.nonAir == 0) return false;
938 const auto& own = ownSummary->second;
939 if (own.thermalRemainderCells != 0) return true;
940 if (own.minimumTemperature != own.maximumTemperature) return true;
941 for (const auto& [dx, dy] :
942 std::array<std::pair<int, int>, 4>{{{-1, 0}, {1, 0}, {0, -1}, {0, 1}}}) {
943 const auto neighbor = phaseSummaries.find({coord.x + dx, coord.y + dy});
944 if (neighbor == phaseSummaries.end() || neighbor->second.nonAir == 0) continue;
945 if (neighbor->second.minimumTemperature != own.minimumTemperature ||
946 neighbor->second.maximumTemperature != own.maximumTemperature)
947 return true;
948 }
949 return false;
950 };
951 std::vector<std::size_t> thermalWork;
952 thermalWork.reserve(order.size());
953 for (std::size_t index = 0; index < order.size(); ++index)
954 if (hasPossibleThermalGradient(order[index])) thermalWork.push_back(index);
955 const auto& readOnlyChunks = impl_->chunks;
956 const auto calculateThermalChunk = [&](std::size_t workIndex) {
957 const std::size_t orderIndex = thermalWork[workIndex];
958 const ChunkCoord coord = order[orderIndex];
959 const auto ownFound = readOnlyChunks.find(coord);
960 if (ownFound == readOnlyChunks.end()) return;
961 const Chunk& ownChunk = ownFound->second;
962 const auto rightFound = readOnlyChunks.find({coord.x + 1, coord.y});
963 const auto bottomFound = readOnlyChunks.find({coord.x, coord.y + 1});
964 const Chunk* rightChunk = rightFound == readOnlyChunks.end() ? nullptr : &rightFound->second;
965 const Chunk* bottomChunk = bottomFound == readOnlyChunks.end() ? nullptr : &bottomFound->second;
966 auto& candidates = thermalCandidates[orderIndex];
967 transferCounts[orderIndex] = 0;
968 transferEnergy[orderIndex] = 0;
969 for (int ly = 0; ly < kPixelChunkSize; ++ly)
970 for (int lx = 0; lx < kPixelChunkSize; ++lx) {
971 const PixelCell source = ownChunk.cells[Chunk::index(lx, ly)];
972 if (source.material == MaterialId::Air) continue;
973 for (const auto& [ox, oy] : std::array<std::pair<int, int>, 2>{{{1, 0}, {0, 1}}}) {
974 PixelCell target;
975 if (ox != 0 && lx == kPixelChunkSize - 1) {
976 if (rightChunk == nullptr) continue;
977 target = rightChunk->cells[Chunk::index(0, ly)];
978 } else if (oy != 0 && ly == kPixelChunkSize - 1) {
979 if (bottomChunk == nullptr) continue;
980 target = bottomChunk->cells[Chunk::index(lx, 0)];
981 } else {
982 target = ownChunk.cells[Chunk::index(lx + ox, ly + oy)];
983 }
984 if (target.material == MaterialId::Air) continue;
985 const int conductivity = std::min<int>(
986 impl_->thermalConductivity(source.material),
987 impl_->thermalConductivity(target.material));
988 if (conductivity == 0) continue;
989 const std::int64_t sourceCapacity = impl_->heatCapacity(source.material);
990 const std::int64_t targetCapacity = impl_->heatCapacity(target.material);
991 const std::int64_t sourceEnergy =
992 std::int64_t(source.temperature) * sourceCapacity + source.thermalRemainder;
993 const std::int64_t targetEnergy =
994 std::int64_t(target.temperature) * targetCapacity + target.thermalRemainder;
995 const std::int64_t equilibriumTransfer =
996 (targetEnergy * sourceCapacity - sourceEnergy * targetCapacity) /
997 (sourceCapacity + targetCapacity);
998 if (equilibriumTransfer == 0) continue;
999 std::int64_t transfer = equilibriumTransfer * conductivity / 255;
1000 if (transfer == 0) transfer = equilibriumTransfer < 0 ? -1 : 1;
1001 const std::size_t sourceIndex = Chunk::index(lx, ly);
1002 candidates.local[sourceIndex] += transfer;
1003 if (ox != 0 && lx == kPixelChunkSize - 1)
1004 candidates.rightHalo[std::size_t(ly)] -= transfer;
1005 else if (oy != 0 && ly == kPixelChunkSize - 1)
1006 candidates.bottomHalo[std::size_t(lx)] -= transfer;
1007 else
1008 candidates.local[Chunk::index(lx + ox, ly + oy)] -= transfer;
1009 ++transferCounts[orderIndex];
1010 transferEnergy[orderIndex] += std::uint64_t(transfer < 0 ? -transfer : transfer);
1011 }
1012 }
1013 };
1014 if (scheduler && thermalWork.size() > 1) {
1015 scheduler->parallelFor(thermalWork.size(), calculateThermalChunk);
1016 stats.parallelTasks += std::uint32_t(thermalWork.size());
1017 } else {
1018 for (std::size_t index = 0; index < thermalWork.size(); ++index) calculateThermalChunk(index);
1019 }
1020 using ThermalDeltaChunk = std::array<std::int64_t, kPixelChunkSize * kPixelChunkSize>;
1021 std::unordered_map<ChunkCoord, ThermalDeltaChunk, ChunkCoordHash> thermalEnergyDeltas;
1022 thermalEnergyDeltas.reserve(order.size());
1023 for (std::size_t index = 0; index < thermalCandidates.size(); ++index) {
1024 stats.temperatureTransfers += transferCounts[index];
1025 stats.thermalEnergyTransferred += transferEnergy[index];
1026 const ChunkCoord coord = order[index];
1027 const ThermalChunkContributions& contributions = thermalCandidates[index];
1028 auto& localDeltas = thermalEnergyDeltas[coord];
1029 for (std::size_t cellIndex = 0; cellIndex < contributions.local.size(); ++cellIndex)
1030 localDeltas[cellIndex] += contributions.local[cellIndex];
1031 auto& rightDeltas = thermalEnergyDeltas[{coord.x + 1, coord.y}];
1032 auto& bottomDeltas = thermalEnergyDeltas[{coord.x, coord.y + 1}];
1033 for (std::size_t offset = 0; offset < kPixelChunkSize; ++offset) {
1034 rightDeltas[Chunk::index(0, int(offset))] += contributions.rightHalo[offset];
1035 bottomDeltas[Chunk::index(int(offset), 0)] += contributions.bottomHalo[offset];
1036 }
1037 }
1038 std::vector<ChunkCoord> thermalChunkOrder;
1039 thermalChunkOrder.reserve(thermalEnergyDeltas.size());
1040 for (const auto& [coord, deltas] : thermalEnergyDeltas) {
1041 (void)deltas;
1042 thermalChunkOrder.push_back(coord);
1043 }
1044 std::sort(thermalChunkOrder.begin(), thermalChunkOrder.end());
1045 struct ThermalCommitStats {
1046 std::uint32_t cellsChanged = 0;
1047 std::uint64_t energyClamped = 0;
1048 };
1049 std::vector<Chunk*> thermalCommitChunks;
1050 std::vector<const ThermalDeltaChunk*> thermalCommitDeltas;
1051 thermalCommitChunks.reserve(thermalChunkOrder.size());
1052 thermalCommitDeltas.reserve(thermalChunkOrder.size());
1053 for (const ChunkCoord coord : thermalChunkOrder) {
1054 auto chunkFound = impl_->chunks.find(coord);
1055 if (chunkFound == impl_->chunks.end()) continue;
1056 thermalCommitChunks.push_back(&chunkFound->second);
1057 thermalCommitDeltas.push_back(&thermalEnergyDeltas.at(coord));
1058 }
1059 std::vector<ThermalCommitStats> thermalCommitStats(thermalCommitChunks.size());
1060 const auto commitThermalChunk = [&](std::size_t index) {
1061 Chunk& chunk = *thermalCommitChunks[index];
1062 const ThermalDeltaChunk& deltas = *thermalCommitDeltas[index];
1063 ThermalCommitStats& commitStats = thermalCommitStats[index];
1064 for (int localY = 0; localY < kPixelChunkSize; ++localY)
1065 for (int localX = 0; localX < kPixelChunkSize; ++localX) {
1066 const std::size_t cellIndex = Chunk::index(localX, localY);
1067 const std::int64_t energyDelta = deltas[cellIndex];
1068 if (energyDelta == 0) continue;
1069 PixelCell cell = chunk.cells[cellIndex];
1070 const std::int64_t capacity =
1071 impl_->catalog.definition(cell.material).heatCapacity;
1072 const std::int64_t beforeEnergy =
1073 std::int64_t(cell.temperature) * capacity + cell.thermalRemainder;
1074 const std::int64_t requestedEnergy = beforeEnergy + energyDelta;
1075 std::int64_t temperature = requestedEnergy / capacity;
1076 std::int64_t remainder = requestedEnergy % capacity;
1077 if (remainder < 0) {
1078 --temperature;
1079 remainder += capacity;
1080 }
1081 if (temperature < std::numeric_limits<std::int16_t>::min()) {
1082 temperature = std::numeric_limits<std::int16_t>::min();
1083 remainder = 0;
1084 } else if (temperature > std::numeric_limits<std::int16_t>::max()) {
1085 temperature = std::numeric_limits<std::int16_t>::max();
1086 remainder = capacity - 1;
1087 }
1088 cell.temperature = std::int16_t(temperature);
1089 cell.thermalRemainder = std::uint16_t(remainder);
1090 const std::int64_t appliedEnergy =
1091 std::int64_t(cell.temperature) * capacity + cell.thermalRemainder;
1092 if (appliedEnergy != requestedEnergy)
1093 commitStats.energyClamped += std::uint64_t(
1094 appliedEnergy > requestedEnergy ? appliedEnergy - requestedEnergy
1095 : requestedEnergy - appliedEnergy);
1096 if (appliedEnergy == beforeEnergy) continue;
1097 impl_->putNormalizedWithinChunk(chunk, cellIndex, cell, tick.value(), false);
1098 ++commitStats.cellsChanged;
1099 }
1100 };
1101 if (scheduler && thermalCommitChunks.size() > 1) {
1102 scheduler->parallelFor(thermalCommitChunks.size(), commitThermalChunk);
1103 stats.parallelTasks += std::uint32_t(thermalCommitChunks.size());
1104 } else {
1105 for (std::size_t index = 0; index < thermalCommitChunks.size(); ++index)
1106 commitThermalChunk(index);
1107 }
1108 for (const ThermalCommitStats& commitStats : thermalCommitStats) {
1109 stats.cellsChanged += commitStats.cellsChanged;
1110 stats.thermalEnergyClamped += commitStats.energyClamped;
1111 }
1112
1113 // Phase transitions are table-driven and choose the first canonical rule.
1114 for (const ChunkCoord coord : order) {
1115 if (impl_->chunks.find(coord) == impl_->chunks.end()) continue;
1116 const auto summary = phaseSummaries.find(coord);
1117 if (summary != phaseSummaries.end() && !summary->second.materialMaskOverflow) {
1118 bool mayTransition = false;
1119 for (const auto& rule : impl_->catalog.phaseRules()) {
1120 const auto material = std::size_t(rule.source);
1121 if (material >= 64 ||
1122 (summary->second.materialMask & (std::uint64_t(1) << material)) == 0)
1123 continue;
1124 const bool temperatureMayMatch =
1125 rule.direction == TemperatureDirection::AtOrAbove
1126 ? summary->second.maximumTemperature >= rule.threshold
1127 : summary->second.minimumTemperature <= rule.threshold;
1128 if (temperatureMayMatch) {
1129 mayTransition = true;
1130 break;
1131 }
1132 }
1133 if (!mayTransition) continue;
1134 }
1135 for (int ly = 0; ly < kPixelChunkSize; ++ly)
1136 for (int lx = 0; lx < kPixelChunkSize; ++lx) {
1137 const int x = coord.x * kPixelChunkSize + lx;
1138 const int y = coord.y * kPixelChunkSize + ly;
1139 PixelCell cell = impl_->get(x, y);
1140 if (cell.material == MaterialId::Air || !impl_->catalog.canPhaseTransition(cell.material))
1141 continue;
1142 for (const auto& rule : impl_->catalog.phaseRules()) {
1143 if (rule.source != cell.material) continue;
1144 const bool matches = rule.direction == TemperatureDirection::AtOrAbove
1145 ? cell.temperature >= rule.threshold
1146 : cell.temperature <= rule.threshold;
1147 if (!matches) continue;
1148 cell.material = rule.result;
1149 cell.temperature = std::int16_t(std::clamp<int>(
1150 int(cell.temperature) + rule.temperatureDelta, -32768, 32767));
1151 cell.lifetime = std::uint8_t(std::min<std::uint16_t>(
1152 impl_->catalog.definition(cell.material).defaultLifetime,
1153 std::numeric_limits<std::uint8_t>::max()));
1154 impl_->put(x, y, cell, tick.value());
1155 ++stats.phaseChanges;
1156 ++stats.cellsChanged;
1157 break;
1158 }
1159 }
1160 }
1161
1162 if (stats.phaseChanges != 0) rebuildSummaries();
1163
1164 std::uint64_t worldMaterialMask = 0;
1165 bool worldMaterialMaskOverflow = false;
1166 for (const auto& [coord, summary] : phaseSummaries) {
1167 (void)coord;
1168 worldMaterialMask |= summary.materialMask;
1169 worldMaterialMaskOverflow = worldMaterialMaskOverflow || summary.materialMaskOverflow;
1170 }
1171 bool worldCanReact = worldMaterialMaskOverflow;
1172 if (!worldCanReact)
1173 for (const auto& rule : impl_->catalog.reactions()) {
1174 const auto first = std::size_t(rule.first), second = std::size_t(rule.second);
1175 if (first >= 64 || second >= 64 ||
1176 ((worldMaterialMask & (std::uint64_t(1) << first)) != 0 &&
1177 (worldMaterialMask & (std::uint64_t(1) << second)) != 0)) {
1178 worldCanReact = true;
1179 break;
1180 }
1181 }
1182
1183 for (auto it = order.rbegin(); it != order.rend(); ++it) {
1184 const ChunkCoord coord = *it;
1185 auto sourceFound = impl_->chunks.find(coord);
1186 if (sourceFound == impl_->chunks.end()) continue;
1187 Chunk* sourceChunk = &sourceFound->second;
1188 const auto summary = phaseSummaries.find(coord);
1189 if (summary != phaseSummaries.end() && !summary->second.materialMaskOverflow) {
1190 bool requiresScan = worldCanReact;
1191 if (!requiresScan) {
1192 for (std::size_t material = 0; material < 64; ++material) {
1193 if ((summary->second.materialMask & (std::uint64_t(1) << material)) == 0) continue;
1194 const auto state = impl_->catalog.definition(MaterialId(material)).state;
1195 if (state == MaterialState::Gas || state == MaterialState::Energy) {
1196 requiresScan = true;
1197 break;
1198 }
1199 }
1200 }
1201 if (!requiresScan) continue;
1202 }
1203 for (int ly = 0; ly < kPixelChunkSize; ++ly)
1204 for (int lx = 0; lx < kPixelChunkSize; ++lx) {
1205 const int x = coord.x * kPixelChunkSize + lx;
1206 const int y = coord.y * kPixelChunkSize + ly;
1207 const std::size_t sourceIndex = Chunk::index(lx, ly);
1208 if (sourceChunk->updated[sourceIndex] == tick.value()) continue;
1209 PixelCell cell = sourceChunk->cells[sourceIndex];
1210 if (cell.material == MaterialId::Air) continue;
1211 ++stats.cellsVisited;
1212 bool reacted = false;
1213 if (impl_->catalog.canReact(cell.material))
1214 for (const auto& [ox, oy] :
1215 std::array<std::pair<int, int>, 4>{{{1, 0}, {-1, 0}, {0, 1}, {0, -1}}}) {
1216 const int neighborLocalX = lx + ox;
1217 const int neighborLocalY = ly + oy;
1218 const bool sameChunk = neighborLocalX >= 0 &&
1219 neighborLocalX < kPixelChunkSize &&
1220 neighborLocalY >= 0 &&
1221 neighborLocalY < kPixelChunkSize;
1222 const std::size_t neighborIndex =
1223 sameChunk ? Chunk::index(neighborLocalX, neighborLocalY) : 0;
1224 PixelCell neighbor = sameChunk ? sourceChunk->cells[neighborIndex]
1225 : impl_->get(x + ox, y + oy);
1226 for (const std::uint16_t ruleIndex :
1227 impl_->reactionRules(cell.material, neighbor.material)) {
1228 const auto& rule = impl_->catalog.reactions()[ruleIndex];
1229 const bool direct = rule.first == cell.material && rule.second == neighbor.material;
1230 const bool reverse = rule.second == cell.material && rule.first == neighbor.material;
1231 if ((!direct && !reverse) ||
1232 std::max(cell.temperature, neighbor.temperature) < rule.minimumTemperature)
1233 continue;
1234 cell.material = direct ? rule.firstResult : rule.secondResult;
1235 neighbor.material = direct ? rule.secondResult : rule.firstResult;
1236 cell.temperature = std::int16_t(std::clamp<int>(int(cell.temperature) + rule.heatDelta,
1237 -32768, 32767));
1238 neighbor.temperature = std::int16_t(std::clamp<int>(
1239 int(neighbor.temperature) + rule.heatDelta, -32768, 32767));
1240 if (cell.lifetime == 0)
1241 cell.lifetime = std::uint8_t(std::min<std::uint16_t>(
1242 impl_->catalog.definition(cell.material).defaultLifetime,
1243 std::numeric_limits<std::uint8_t>::max()));
1244 if (neighbor.lifetime == 0)
1245 neighbor.lifetime = std::uint8_t(std::min<std::uint16_t>(
1246 impl_->catalog.definition(neighbor.material).defaultLifetime,
1247 std::numeric_limits<std::uint8_t>::max()));
1248 if (sameChunk) {
1249 impl_->putWithinChunk(*sourceChunk, neighborIndex, neighbor,
1250 tick.value());
1251 } else {
1252 impl_->put(x + ox, y + oy, neighbor, tick.value());
1253 // A custom rule may materialize an absent cross-boundary
1254 // Air cell and rehash the sparse Chunk map.
1255 sourceFound = impl_->chunks.find(coord);
1256 if (sourceFound == impl_->chunks.end()) break;
1257 sourceChunk = &sourceFound->second;
1258 }
1259 reacted = true;
1260 ++stats.cellsChanged;
1261 break;
1262 }
1263 }
1264 if (reacted) ++stats.reactions;
1265 const auto state = impl_->catalog.definition(cell.material).state;
1266 if (reacted)
1267 impl_->putWithinChunk(*sourceChunk, sourceIndex, cell, tick.value());
1268 if (state != MaterialState::Gas && state != MaterialState::Energy) continue;
1269 if (cell.lifetime > 0) {
1270 --cell.lifetime;
1271 ++stats.cellsChanged;
1272 if (cell.lifetime == 0) cell.material = MaterialId::Air;
1273 impl_->put(x, y, cell, tick.value());
1274 }
1275 if (cell.material != MaterialId::Air) {
1276 const int direction = impl_->pseudoBit(x, y, tick.value()) ? 1 : -1;
1277 bool moved = impl_->moveOrSwap(x, y, x, y - 1, tick.value());
1278 if (!moved) moved = impl_->moveOrSwap(x, y, x + direction, y - 1, tick.value());
1279 if (moved) {
1280 ++stats.cellsMoved;
1281 stats.cellsChanged += 2;
1282 }
1283 sourceFound = impl_->chunks.find(coord);
1284 if (sourceFound == impl_->chunks.end()) break;
1285 sourceChunk = &sourceFound->second;
1286 }
1287 }
1288 }
1289
1290 std::vector<ChunkCoord> reclaimed;
1291 for (auto& [coord, chunk] : impl_->chunks) {
1292 if (chunk.touched) {
1293 chunk.idleTicks = 0;
1294 chunk.active = true;
1295 } else {
1296 chunk.idleTicks = std::uint8_t(std::min<int>(kPixelSleepHysteresisTicks, chunk.idleTicks + 1));
1297 chunk.active = chunk.idleTicks < kPixelSleepHysteresisTicks;
1298 }
1299 if (!chunk.active && chunk.nonAir == 0)
1300 reclaimed.push_back(coord);
1301 }
1302 for (const ChunkCoord coord : reclaimed) {
1303 impl_->chunks.erase(coord);
1304 impl_->removedChunks[coord] = impl_->revision + 1;
1305 ++stats.chunksReclaimed;
1306 }
1307 if (stats.cellsChanged != 0 || stats.chunksReclaimed != 0) ++impl_->revision;
1308 const auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(
1309 std::chrono::steady_clock::now() - stepStarted)
1310 .count();
1311 pixelWorldControlService().recordStep(impl_->worldId, stats, std::uint64_t(elapsed));
1313}
1314
1316 auto result = advance(eve::SimulationTick(impl_->tick + 1));
1317 return std::move(result).expect("PixelWorld::step generated an invalid tick");
1318}
1319
1320void PixelWorld::setPaused(bool paused) noexcept { impl_->paused = paused; }
1321bool PixelWorld::isPaused() const noexcept { return impl_->paused; }
1322
1323std::uint64_t PixelWorld::seed() const noexcept { return impl_->seed; }
1324std::uint64_t PixelWorld::revision() const noexcept { return impl_->revision; }
1325std::uint64_t PixelWorld::tickValue() const noexcept { return impl_->tick; }
1326std::uint64_t PixelWorld::lastEditSequence() const noexcept { return impl_->lastEditSequence; }
1327int PixelWorld::chunkCount() const noexcept { return int(impl_->chunks.size()); }
1328int PixelWorld::activeChunkCount() const noexcept {
1329 return int(std::count_if(impl_->chunks.begin(), impl_->chunks.end(), [](const auto& entry) {
1330 return entry.second.active;
1331 }));
1332}
1333std::uint64_t PixelWorld::materialCatalogFingerprint() const noexcept { return impl_->catalog.fingerprint(); }
1334
1335std::vector<PixelChunkSnapshot> PixelWorld::snapshotChangedChunks(std::uint64_t sinceRevision) const {
1336 std::vector<ChunkCoord> order;
1337 order.reserve(impl_->chunks.size());
1338 for (const auto& [coord, chunk] : impl_->chunks)
1339 if (chunk.revision > sinceRevision) order.push_back(coord);
1340 for (const auto& [coord, revision] : impl_->removedChunks)
1341 if (revision > sinceRevision) order.push_back(coord);
1342 std::sort(order.begin(), order.end());
1343
1344 std::vector<PixelChunkSnapshot> snapshots;
1345 snapshots.reserve(order.size());
1346 for (const ChunkCoord coord : order) {
1347 PixelChunkSnapshot snapshot;
1348 snapshot.x = coord.x;
1349 snapshot.y = coord.y;
1350 const auto chunk = impl_->chunks.find(coord);
1351 if (chunk != impl_->chunks.end()) {
1352 snapshot.revision = chunk->second.revision;
1353 snapshot.cells.assign(chunk->second.cells.begin(), chunk->second.cells.end());
1354 } else {
1355 snapshot.revision = impl_->removedChunks.at(coord);
1356 snapshot.removed = true;
1357 }
1358 snapshots.push_back(std::move(snapshot));
1359 }
1360 return snapshots;
1361}
1362
1364 PixelChunkRegion region, std::uint64_t sinceRevision) const {
1365 if (region.minX > region.maxX || region.minY > region.maxY)
1367 eve::DiagnosticCode::InvalidArgument, "Chunk region bounds are inverted", "region", {},
1368 "pixelworld.chunk-region"));
1369 const std::uint64_t width = std::uint64_t(std::int64_t(region.maxX) - region.minX) + 1;
1370 const std::uint64_t height = std::uint64_t(std::int64_t(region.maxY) - region.minY) + 1;
1371 if (width > 65'536 || height > 65'536 || width * height > 65'536)
1373 eve::DiagnosticCode::InvalidArgument, "Chunk region exceeds 65536-coordinate budget", "region", {},
1374 "pixelworld.chunk-region"));
1375
1376 std::vector<PixelChunkSnapshot> result;
1377 for (int y = region.minY;; ++y) {
1378 for (int x = region.minX;; ++x) {
1379 const ChunkCoord coord{x, y};
1380 const auto present = impl_->chunks.find(coord);
1381 if (present != impl_->chunks.end() && present->second.revision > sinceRevision) {
1382 PixelChunkSnapshot snapshot;
1383 snapshot.x = x;
1384 snapshot.y = y;
1385 snapshot.revision = present->second.revision;
1386 snapshot.cells.assign(present->second.cells.begin(), present->second.cells.end());
1387 result.push_back(std::move(snapshot));
1388 } else {
1389 const auto removed = impl_->removedChunks.find(coord);
1390 if (removed != impl_->removedChunks.end() && removed->second > sinceRevision)
1391 result.push_back({x, y, removed->second, true, {}});
1392 }
1393 if (x == region.maxX) break;
1394 }
1395 if (y == region.maxY) break;
1396 }
1397 return eve::Result<std::vector<PixelChunkSnapshot>>::success(std::move(result));
1398}
1399
1401 PixelChunkRegion region) const {
1402 if (region.minX > region.maxX || region.minY > region.maxY)
1404 eve::DiagnosticCode::InvalidArgument, "Chunk region bounds are inverted", "region", {},
1405 "pixelworld.chunk-diagnostics"));
1406 const std::uint64_t width = std::uint64_t(std::int64_t(region.maxX) - region.minX) + 1;
1407 const std::uint64_t height = std::uint64_t(std::int64_t(region.maxY) - region.minY) + 1;
1408 if (width > 65'536 || height > 65'536 || width * height > 65'536)
1410 eve::DiagnosticCode::InvalidArgument, "Chunk region exceeds 65536-coordinate budget", "region", {},
1411 "pixelworld.chunk-diagnostics"));
1412 std::vector<PixelChunkDiagnostic> result;
1413 for (int y = region.minY;; ++y) {
1414 for (int x = region.minX;; ++x) {
1415 const auto found = impl_->chunks.find({x, y});
1416 if (found != impl_->chunks.end()) {
1417 const Chunk& chunk = found->second;
1418 PixelChunkDiagnostic diagnostic{x, y, chunk.revision, chunk.nonAir, chunk.mobile,
1419 20, 20, chunk.idleTicks, chunk.active};
1420 if (chunk.nonAir != 0) {
1421 diagnostic.minimumTemperature = 32767;
1422 diagnostic.maximumTemperature = -32768;
1423 for (const PixelCell cell : chunk.cells) {
1424 if (cell.material == MaterialId::Air) continue;
1425 diagnostic.minimumTemperature = std::min(diagnostic.minimumTemperature,
1426 cell.temperature);
1427 diagnostic.maximumTemperature = std::max(diagnostic.maximumTemperature,
1428 cell.temperature);
1429 }
1430 }
1431 result.push_back(diagnostic);
1432 }
1433 if (x == region.maxX) break;
1434 }
1435 if (y == region.maxY) break;
1436 }
1437 return eve::Result<std::vector<PixelChunkDiagnostic>>::success(std::move(result));
1438}
1439
1440eve::Result<PixelChunkApplyReceipt> PixelWorld::applyChunkBatch(
1441 const PixelChunkBatch& batch, std::uint64_t expectedRevision) {
1442 const auto reject = [](eve::DiagnosticCode code, std::string message, std::string path) {
1444 code, message, path, {}, "pixelworld.chunk-batch"));
1445 };
1446 if (impl_->revision != expectedRevision)
1447 return reject(eve::DiagnosticCode::Conflict, "local world revision is stale", "expectedRevision");
1448 if (batch.catalogFingerprint != impl_->catalog.fingerprint())
1449 return reject(eve::DiagnosticCode::Conflict, "material Catalog fingerprint does not match",
1450 "catalogFingerprint");
1451 if (batch.sourceSeed != impl_->seed)
1452 return reject(eve::DiagnosticCode::Conflict, "deterministic world seed does not match", "sourceSeed");
1453 if (!batch.fullResync &&
1454 (batch.sourceRevision < impl_->revision || batch.sourceTick.value() < impl_->tick ||
1455 batch.sourceLastEditSequence < impl_->lastEditSequence))
1456 return reject(eve::DiagnosticCode::Conflict, "authoritative metadata must not rewind local state",
1457 "source");
1458 if (batch.chunks.size() > 1'000'000U)
1459 return reject(eve::DiagnosticCode::InvalidArgument, "Chunk batch exceeds admission budget", "chunks");
1460
1461 std::optional<ChunkCoord> previous;
1462 for (std::size_t index = 0; index < batch.chunks.size(); ++index) {
1463 const auto& snapshot = batch.chunks[index];
1464 const ChunkCoord coord{snapshot.x, snapshot.y};
1465 if (previous && !(previous.value() < coord))
1467 "Chunk batch must be unique canonical y/x order", "chunks");
1468 previous = coord;
1469 if (snapshot.revision == 0 || snapshot.revision > batch.sourceRevision)
1471 "Chunk revision must be positive and no newer than source", "chunks.revision");
1472 if (snapshot.removed) {
1473 if (!snapshot.cells.empty())
1475 "removed Chunk must not contain cells", "chunks.cells");
1476 continue;
1477 }
1478 if (snapshot.cells.size() != std::size_t(kPixelChunkSize * kPixelChunkSize))
1480 "present Chunk must contain exactly 64x64 cells", "chunks.cells");
1481 for (const PixelCell cell : snapshot.cells) {
1482 if (std::size_t(cell.material) >= impl_->catalog.definitions().size())
1484 "Chunk references an unknown material", "chunks.cells.material");
1485 const auto capacity = impl_->catalog.definition(cell.material).heatCapacity;
1486 if ((cell.material == MaterialId::Air && cell.thermalRemainder != 0) ||
1487 cell.thermalRemainder >= capacity)
1489 "Chunk contains a non-canonical thermal remainder",
1490 "chunks.cells.thermalRemainder");
1491 }
1492 }
1493
1494 Impl candidate = batch.fullResync ? Impl(impl_->catalog) : *impl_;
1495 if (batch.fullResync) {
1496 candidate.worldId = impl_->worldId;
1497 candidate.epoch = impl_->epoch;
1498 candidate.nextFragmentId = impl_->nextFragmentId;
1499 candidate.paused = impl_->paused;
1500 }
1501 PixelChunkApplyReceipt receipt;
1502 receipt.revisionBefore = impl_->revision;
1503 for (const auto& snapshot : batch.chunks) {
1504 const ChunkCoord coord{snapshot.x, snapshot.y};
1505 if (snapshot.removed) {
1506 candidate.chunks.erase(coord);
1507 candidate.removedChunks[coord] = snapshot.revision;
1508 ++receipt.chunksRemoved;
1509 continue;
1510 }
1511 Chunk chunk;
1512 std::copy(snapshot.cells.begin(), snapshot.cells.end(), chunk.cells.begin());
1513 chunk.revision = snapshot.revision;
1514 chunk.active = true;
1515 chunk.touched = true;
1516 chunk.idleTicks = 0;
1517 candidate.rebuildActivity(chunk);
1518 candidate.chunks.insert_or_assign(coord, std::move(chunk));
1519 candidate.removedChunks.erase(coord);
1520 ++receipt.chunksReplaced;
1521 }
1522 candidate.revision = batch.sourceRevision;
1523 candidate.tick = batch.sourceTick.value();
1524 candidate.lastEditSequence = batch.sourceLastEditSequence;
1525 ++candidate.epoch;
1526 receipt.revisionAfter = candidate.revision;
1527 receipt.worldEpoch = candidate.epoch;
1528 *impl_ = std::move(candidate);
1530}
1531
1532eve::Result<std::vector<std::byte>> PixelWorld::saveSnapshot() const {
1533 std::vector<ChunkCoord> order;
1534 order.reserve(impl_->chunks.size());
1535 for (const auto& [coord, chunk] : impl_->chunks) {
1536 (void)chunk;
1537 order.push_back(coord);
1538 }
1539 std::sort(order.begin(), order.end());
1540 std::vector<std::byte> out;
1541 out.insert(out.end(), {std::byte{0x45}, std::byte{0x56}, std::byte{0x50}, std::byte{0x57}});
1542 append<std::uint16_t>(out, 4);
1543 append(out, impl_->catalog.fingerprint());
1544 append(out, impl_->seed);
1545 append(out, impl_->revision);
1546 append(out, impl_->tick);
1547 append(out, impl_->lastEditSequence);
1548 append<std::uint32_t>(out, std::uint32_t(order.size()));
1549 for (const ChunkCoord coord : order) {
1550 append<std::int32_t>(out, coord.x);
1551 append<std::int32_t>(out, coord.y);
1552 const Chunk& chunk = impl_->chunks.at(coord);
1553 for (const PixelCell cell : chunk.cells) {
1554 append<std::uint16_t>(out, std::uint16_t(cell.material));
1555 append(out, cell.temperature);
1556 append(out, cell.lifetime);
1557 append(out, cell.thermalRemainder);
1558 }
1559 }
1560 return eve::Result<std::vector<std::byte>>::success(std::move(out));
1561}
1562
1563eve::Result<void> PixelWorld::restoreSnapshot(std::span<const std::byte> bytes) {
1564 std::size_t cursor = 0;
1565 if (bytes.size() < 4 || bytes[0] != std::byte{0x45} || bytes[1] != std::byte{0x56} ||
1566 bytes[2] != std::byte{0x50} || bytes[3] != std::byte{0x57})
1567 return eve::Result<void>::failure(malformed("missing EVPW header"));
1568 cursor = 4;
1569 std::uint16_t version = 0;
1570 Impl candidate(impl_->catalog);
1571 std::uint32_t count = 0;
1572 if (!read(bytes, cursor, version) ||
1573 (version != 1 && version != 2 && version != 3 && version != 4))
1574 return eve::Result<void>::failure(malformed("unsupported pixelworld schema version"));
1575 if (version >= 2) {
1576 std::uint64_t fingerprint = 0;
1577 if (!read(bytes, cursor, fingerprint) || fingerprint != impl_->catalog.fingerprint())
1578 return eve::Result<void>::failure(malformed("material catalog fingerprint mismatch"));
1579 } else if (impl_->catalog.fingerprint() != MaterialCatalog::builtIn().fingerprint()) {
1580 return eve::Result<void>::failure(malformed("version-1 snapshots require the built-in material catalog"));
1581 }
1582 if (!read(bytes, cursor, candidate.seed) || !read(bytes, cursor, candidate.revision) ||
1583 !read(bytes, cursor, candidate.tick))
1584 return eve::Result<void>::failure(malformed("truncated or unreasonable snapshot header"));
1585 if (version >= 3 && !read(bytes, cursor, candidate.lastEditSequence))
1586 return eve::Result<void>::failure(malformed("truncated edit sequence"));
1587 if (!read(bytes, cursor, count) || count > 1'000'000U)
1588 return eve::Result<void>::failure(malformed("truncated or unreasonable chunk count"));
1589 for (std::uint32_t i = 0; i < count; ++i) {
1590 ChunkCoord coord;
1591 if (!read(bytes, cursor, coord.x) || !read(bytes, cursor, coord.y))
1592 return eve::Result<void>::failure(malformed("truncated chunk coordinate"));
1593 Chunk chunk;
1594 for (PixelCell& cell : chunk.cells) {
1595 std::uint16_t material = 0;
1596 if (!read(bytes, cursor, material) || !read(bytes, cursor, cell.temperature) ||
1597 !read(bytes, cursor, cell.lifetime) || material >= candidate.catalog.definitions().size())
1598 return eve::Result<void>::failure(malformed("invalid chunk cell payload"));
1599 cell.material = MaterialId(material);
1600 if (version >= 4 && !read(bytes, cursor, cell.thermalRemainder))
1601 return eve::Result<void>::failure(malformed("truncated thermal remainder"));
1602 const auto capacity = candidate.catalog.definition(cell.material).heatCapacity;
1603 if ((cell.material == MaterialId::Air && cell.thermalRemainder != 0) ||
1604 cell.thermalRemainder >= capacity)
1605 return eve::Result<void>::failure(malformed("non-canonical thermal remainder"));
1606 }
1607 const auto [ignored, inserted] = candidate.chunks.emplace(coord, std::move(chunk));
1608 (void)ignored;
1609 if (!inserted) return eve::Result<void>::failure(malformed("duplicate chunk coordinate"));
1610 }
1611 if (cursor != bytes.size()) return eve::Result<void>::failure(malformed("trailing snapshot bytes"));
1612 for (auto& [coord, chunk] : candidate.chunks) {
1613 (void)coord;
1614 candidate.rebuildActivity(chunk);
1615 chunk.revision = candidate.revision;
1616 }
1617 candidate.worldId = impl_->worldId;
1618 candidate.epoch = impl_->epoch + 1;
1619 candidate.nextFragmentId = impl_->nextFragmentId;
1620 candidate.paused = impl_->paused;
1621 *impl_ = std::move(candidate);
1623}
1624
1625} // namespace eve::pixelworld
LogicalId target
double value
bool & active
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
std::vector< QuestEvent > pending
float cx
Definition CardTypes.cpp:33
float cy
Definition CardTypes.cpp:34
float nx
float ny
std::string message
DiagnosticCode code
std::uint32_t capacity
std::int32_t second
std::int32_t first
float temperature
std::uint32_t height
std::uint32_t width
std::string local
size_t offset
std::uint64_t bytes
std::string name
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
std::vector< std::int32_t > order
graphics::Canvas * previous
OnnxTransferStats stats
Definition OnnxGpgpu.cpp:56
std::array< std::uint64_t, kPixelChunkSize *kPixelChunkSize > updated
std::uint32_t nonAir
std::uint32_t mobile
std::int16_t maximumTemperature
std::uint8_t idleTicks
std::uint64_t revision
std::array< std::uint16_t, 64 > materialCounts
std::uint32_t materialOverflow
bool temperatureBoundsDirty
bool touched
std::int16_t minimumTemperature
std::uint32_t thermalRemainderCells
std::array< PixelCell, kPixelChunkSize *kPixelChunkSize > cells
float radius
std::string path
Definition PlayHost.cpp:110
std::string id
Definition PlayHost.cpp:108
std::uint32_t seed
Definition PointSet.cpp:807
Material * material
RoadLaneDirection direction
bool found
int removed
double current
float dy
float dx
std::uint32_t count
Structured operation status used by the common Result foundation.
float targetX
float sourceX
Cell cell
Battle::Reactions reactions
SimulationTick tick
std::size_t cursor
uint32_t index
const UnitySourceAsset & source
double oy
std::vector< char > inside
double ox
float wx
float wy
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
void expect(std::string_view message) const
Require success for a void operation.
Definition Result.h:548
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
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
Structured status and zero or more diagnostics for an operation.
Definition Status.h:68
static Status failure(StatusCode code, Diagnostic diagnostic)
Construct a failed status with one diagnostic.
Definition Status.h:84
constexpr std::uint64_t value() const noexcept
Returns the underlying value at an explicit protocol boundary.
Owning immutable material and reaction table used by one PixelWorld.
const MaterialDefinition & definition(MaterialId id) const noexcept
Resolve a validated id; invalid ids safely project to air.
static MaterialCatalog builtIn()
Construct the engine's canonical built-in catalog.
std::span< const MaterialReactionRule > reactions() const noexcept
Borrow immutable canonical-priority reaction rules.
std::span< const MaterialDefinition > definitions() const noexcept
Borrow immutable definitions until this catalog is destroyed.
std::uint64_t fingerprint() const noexcept
Stable deterministic fingerprint included in world snapshots.
Synchronous executor for owning PixelWorld candidate buffers.
Definition PixelWorld.h:58
virtual void parallelFor(std::size_t workItems, const std::function< void(std::size_t)> &body)=0
Execute every index in [0, workItems) exactly once and synchronously join.
Sparse, chunked, deterministic 2D falling-material world.
Definition PixelWorld.h:224
int activeChunkCount() const noexcept
Active chunk count.
std::uint64_t lastEditSequence() const noexcept
Last edit sequence.
eve::Result< PixelEditReceipt > applyEdit(const PixelEditCommand &command)
Validate and atomically apply one strictly sequenced edit command.
std::uint64_t materialCatalogFingerprint() const noexcept
Fingerprint of the immutable catalog that gives cell ids their meaning.
void setMaterial(int x, int y, std::string_view material)
Set one cell from a stable built-in material name.
eve::Result< PixelCatalogReloadReceipt > reloadMaterialCatalog(MaterialCatalog catalog, std::uint64_t expectedFingerprint)
Transactionally hot-reload a compatible Catalog while paused.
eve::Result< void > setMaterialChecked(int x, int y, std::string_view material)
Canonical checked name-based setter; unknown names leave the world unchanged.
PixelEditReceipt explode(int centerX, int centerY, int radius, int strength, std::int16_t temperatureDelta)
Script/demo explosion facade that emits the next command sequence.
std::size_t paintCircle(int centerX, int centerY, int radius, std::string_view material)
Paint a filled material circle and return the number of changed cells.
eve::Result< std::size_t > paintCircleChecked(int centerX, int centerY, int radius, std::string_view material)
Canonical checked circle edit; unknown names leave the world unchanged.
eve::Result< std::vector< PixelChunkSnapshot > > snapshotChunksInRegion(PixelChunkRegion region, std::uint64_t sinceRevision=0) const
Copy present Chunks and retained tombstones in one finite Chunk-coordinate region.
std::uint64_t seed() const noexcept
Seed.
std::uint64_t tickValue() const noexcept
Tick value.
void setCell(int x, int y, PixelCell cell)
Set one cell and wake its chunk and neighbors. Air never allocates a missing chunk.
PixelWorld & operator=(PixelWorld &&) noexcept
Operator =.
eve::Result< PixelFragmentRasterReceipt > rasterizeFragment(const PixelFragment &fragment, int originX, int originY)
Transactionally rasterize a detached fragment at a new world-space origin.
StepStats step()
Script/demo facade that advances the next integral tick.
bool isPaused() const noexcept
Whether ordinary simulation entry points are currently paused.
std::uint32_t materialDisplayRgba(MaterialId material) const noexcept
Query the Catalog-owned preview/render color encoded as 0xRRGGBBAA.
PixelWorldLink worldLink() const noexcept
Current runtime link used for stale fragment detection.
int chunkCount() const noexcept
Chunk count.
bool isSolidMaterial(MaterialId material) const noexcept
Query whether a validated material id participates in structural solid collision.
PixelWorld(std::uint64_t seed=1)
Construct an empty world with an explicit deterministic seed.
eve::Result< StepStats > advance(eve::SimulationTick tick)
Advance exactly one scheduler-owned tick.
int getMaterial(int x, int y) const noexcept
Convenience query returning the numeric material id.
eve::Result< std::vector< PixelFragment > > extractUnsupportedFragments(PixelRegion region, int supportY, std::uint32_t minimumCells=1)
Atomically detach unsupported solid components inside a finite region.
PixelCell getCell(int x, int y) const noexcept
Return the cell at world coordinates; absent chunks read as air.
std::uint64_t revision() const noexcept
Revision.
eve::Result< StepStats > advanceScheduled(eve::SimulationTick tick, PixelWorkScheduler &scheduler)
Advance one deterministic tick using a synchronous candidate scheduler.
void clear() noexcept
Remove all chunks and reset the simulation tick.
void setPaused(bool paused) noexcept
Pause or resume ordinary simulation entry points without changing state.
std::vector< PixelChunkSnapshot > snapshotChangedChunks(std::uint64_t sinceRevision) const
Copy chunks changed after sinceRevision in canonical y/x order.
PixelWorldControlService & pixelWorldControlService()
Process-lifetime PixelWorld tooling registry.
MaterialId
Compact stable material identifier stored in each authoritative cell.
constexpr std::uint8_t kPixelSleepHysteresisTicks
Definition PixelWorld.h:21
MaterialState
Broad movement class for one pixel material.
constexpr int kPixelChunkSize
Definition PixelWorld.h:20
T read(const RuntimeTensor &v, size_t i)
Reads read.
DiagnosticCode
Stable machine-readable diagnostic codes.
Definition Diagnostic.h:47
detail::StrongUint64< detail::SimulationTickTag > SimulationTick
Deterministic simulation time step; it is not wall-clock time.
Definition Time.h:31
Receipt for one transactional compatible material Catalog hot reload.
Definition PixelWorld.h:137
Compact authoritative state for one simulated world pixel.
Definition PixelWorld.h:24
std::uint16_t thermalRemainder
Canonical sub-degree thermal energy in [0, material heatCapacity).
Definition PixelWorld.h:29
Receipt for one all-or-nothing authoritative Chunk batch application.
Definition PixelWorld.h:128
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
Read-only per-Chunk simulation diagnostics for tooling and overlays.
Definition PixelWorld.h:115
Inclusive finite rectangle expressed in Chunk coordinates.
Definition PixelWorld.h:104
Owning, immutable projection of one authoritative simulation chunk.
Definition PixelWorld.h:77
std::vector< PixelCell > cells
Definition PixelWorld.h:82
Owning replayable edit command. Sequence must be exactly previous + 1.
Definition PixelWorld.h:155
Deterministic receipt for one atomically accepted edit command.
Definition PixelWorld.h:167
Receipt for one all-or-nothing fragment rasterization.
Definition PixelWorld.h:210
Owning material bitmap detached atomically from a PixelWorld.
Definition PixelWorld.h:198
std::vector< PixelCell > cells
Definition PixelWorld.h:206
Finite inclusive world-space rectangle used by structural queries.
Definition PixelWorld.h:177
MaterialState materialState(MaterialId material) const noexcept
Chunk & ensureChunk(int x, int y)
bool pseudoBit(int x, int y, std::uint64_t currentTick) const noexcept
std::vector< std::uint16_t > heatCapacities
void putWithinChunk(Chunk &chunk, std::size_t index, PixelCell cell, std::uint64_t updatedTick, bool markUpdated=true)
bool isMobile(MaterialId material) const noexcept
std::uint8_t thermalConductivity(MaterialId material) const noexcept
void refreshTemperatureBounds(Chunk &chunk) const noexcept
Chunk * findChunk(int x, int y) noexcept
std::vector< std::vector< std::uint16_t > > reactionRulesByPair
const Chunk * findChunk(int x, int y) const noexcept
void put(int x, int y, PixelCell cell, std::uint64_t updatedTick, bool markUpdated=true)
std::vector< std::uint8_t > canDisplaceTable
std::uint16_t heatCapacity(MaterialId material) const noexcept
std::vector< std::uint8_t > thermalConductivities
std::span< const std::uint16_t > reactionRules(MaterialId source, MaterialId target) const noexcept
void addActivity(Chunk &chunk, const PixelCell &cell) const noexcept
std::unordered_map< ChunkCoord, std::uint64_t, ChunkCoordHash > removedChunks
Impl(MaterialCatalog ownedCatalog=MaterialCatalog::builtIn())
std::uint64_t updatedAt(int x, int y) const noexcept
std::unordered_map< ChunkCoord, Chunk, ChunkCoordHash > chunks
std::vector< MaterialState > materialStates
void putNormalizedWithinChunk(Chunk &chunk, std::size_t index, const PixelCell &cell, std::uint64_t updatedTick, bool markUpdated=true)
bool moveOrSwap(int x, int y, int tx, int ty, std::uint64_t currentTick)
PixelCell normalizeCell(PixelCell cell) const noexcept
bool canDisplace(MaterialId source, MaterialId target) const noexcept
PixelCell get(int x, int y) const noexcept
void rebuildActivity(Chunk &chunk) const noexcept
bool moveOrSwapWithinChunk(ChunkCoord coord, Chunk &chunk, int sourceX, int sourceY, PixelCell source, int targetX, int targetY, std::uint64_t currentTick)
bool moveOrSwapKnownSource(int x, int y, PixelCell source, int tx, int ty, std::uint64_t currentTick)
void removeActivity(Chunk &chunk, const PixelCell &cell) const noexcept
Counters produced by one deterministic simulation step.
Definition PixelWorld.h:36