载入中...
搜索中...
未找到
WorldSnapshot.cpp
浏览该文件的文档.
1#include "physics/World.h"
2
3#include "physics/Body.h"
4#include "physics/Body3D.h"
5#include "physics/Fixture.h"
6#include "physics/Joint3D.h"
7#include "physics/Shape3D.h"
9#include "physics/World3D.h"
10
11#include <Box2D/Box2D.h>
12#include <box3d/box3d.h>
13
14#include <algorithm>
15#include <charconv>
16#include <cmath>
17#include <cstdint>
18#include <exception>
19#include <initializer_list>
20#include <limits>
21#include <set>
22#include <string>
23#include <string_view>
24#include <utility>
25#include <vector>
26
27namespace eve::physics {
28
32 object.emplace("bodyId", eve::Value(std::to_string(shape.body_ ? shape.body_->getId() : -1)));
33 object.emplace("id", eve::Value(std::to_string(shape.id_)));
34 object.emplace("kind", eve::Value(shape.getKind()));
35 object.emplace("a", eve::Value(shape.a_));
36 object.emplace("b", eve::Value(shape.b_));
37 object.emplace("c", eve::Value(shape.c_));
38 auto floats = [](const std::vector<float>& source) {
40 values.reserve(source.size());
41 for (float value : source) values.emplace_back(value);
42 return eve::Value(std::move(values));
43 };
44 auto integers = [](const auto& source) {
46 values.reserve(source.size());
47 for (auto value : source) values.emplace_back(std::to_string(static_cast<std::int64_t>(value)));
48 return eve::Value(std::move(values));
49 };
50 object.emplace("hullVertices", floats(shape.hullVertices_));
51 object.emplace("hullMaxVertices", eve::Value(std::to_string(shape.hullMaxVertices_)));
52 object.emplace("meshVertices", floats(shape.meshVertices_));
53 object.emplace("meshIndices", integers(shape.meshIndices_));
54 object.emplace("meshWeldVertices", eve::Value(shape.meshWeldVertices_));
55 object.emplace("meshWeldTolerance", eve::Value(shape.meshWeldTolerance_));
56 object.emplace("meshIdentifyEdges", eve::Value(shape.meshIdentifyEdges_));
57 object.emplace("meshUseMedianSplit", eve::Value(shape.meshUseMedianSplit_));
58 object.emplace("meshMaterialIndices", integers(shape.meshMaterialIndices_));
59 object.emplace("heightValues", floats(shape.heightValues_));
60 object.emplace("heightCountX", eve::Value(std::to_string(shape.heightCountX_)));
61 object.emplace("heightCountZ", eve::Value(std::to_string(shape.heightCountZ_)));
62 object.emplace("heightCellSizeX", eve::Value(shape.heightCellSizeX_));
63 object.emplace("heightCellSizeZ", eve::Value(shape.heightCellSizeZ_));
64 object.emplace("heightGlobalMin", eve::Value(shape.heightGlobalMin_));
65 object.emplace("heightGlobalMax", eve::Value(shape.heightGlobalMax_));
66 object.emplace("heightClockwise", eve::Value(shape.heightClockwise_));
67 return eve::Value(std::move(object));
68 }
69
70 static void setBodyId(Body& body, int id) { body.id_ = id; }
71 static void setBodyId(Body3D& body, int id) { body.id_ = id; }
72 static void setShapeId(Shape3D& shape, int id) { shape.id_ = id; }
73 static void setJointId(Joint3D& joint, int id) { joint.id_ = id; }
74 static std::unique_ptr<World3D> makeDetachedWorld3D(float gx, float gy, float gz,
75 eve::PersistentId instanceId) {
76 return std::unique_ptr<World3D>(new World3D(gx, gy, gz, false, instanceId, false));
77 }
79 const SimulationObservation& observation) {
80 world.nextId_ = nextId;
81 auto restored = world.simulation_->restoreObservation(observation);
82 if (!restored) return restored;
83 world.simulationTick_ = observation.lastTick;
85 }
86 static eve::Result<void> finishPrepared(World3D& world, int nextBodyId, int nextShapeId, int nextJointId,
87 const SimulationObservation& observation) {
88 world.nextId_ = nextBodyId;
89 world.nextShapeId_ = nextShapeId;
90 world.nextJointId_ = nextJointId;
91 auto restored = world.simulation_->restoreObservation(observation);
92 if (!restored) return restored;
93 world.simulationTick_ = observation.lastTick;
95 }
96
97 static void adopt(World& live, World& prepared) { live.adoptPreparedTopology(prepared); }
98 static void adopt(World3D& live, World3D& prepared) { live.adoptPreparedTopology(prepared); }
99};
100namespace {
101
102constexpr std::string_view kWorld2DType = "physics.world2d";
103constexpr std::string_view kWorld2DSchema = "physics:world2d";
104constexpr std::string_view kWorld3DType = "physics.world3d";
105constexpr std::string_view kWorld3DSchema = "physics:world3d";
106constexpr std::uint64_t kSnapshotVersion = 2;
107
108bool hasExactFields(const eve::Value::Object& object, std::initializer_list<std::string_view> expected) {
109 if (object.size() != expected.size()) return false;
110 for (const std::string_view field : expected) {
111 if (!object.contains(std::string(field))) return false;
112 }
113 return true;
114}
115
116const eve::Value* field(const eve::Value::Object& object, std::string_view name) {
117 const auto found = object.find(std::string(name));
118 return found == object.end() ? nullptr : &found->second;
119}
120
121eve::Result<std::string> readString(const eve::Value::Object& object, std::string_view name) {
122 const eve::Value* value = field(object, name);
123 if (!value || !value->isString())
125 eve::DiagnosticCode::ParseError, "snapshot field must be a string", std::string(name)));
126 return eve::Result<std::string>::success(value->asString());
127}
128
129eve::Result<std::uint64_t> readUint64(const eve::Value::Object& object, std::string_view name) {
130 auto text = readString(object, name);
131 if (!text) return eve::Result<std::uint64_t>::failure(text.status());
132 std::uint64_t output = 0;
133 const std::string& value = text.value();
134 const auto [end, error] = std::from_chars(value.data(), value.data() + value.size(), output);
135 if (value.empty() || error != std::errc{} || end != value.data() + value.size())
137 eve::DiagnosticCode::ParseError, "snapshot integer is not a uint64 decimal string", std::string(name)));
139}
140
141eve::Result<std::int64_t> readInt64(const eve::Value::Object& object, std::string_view name) {
142 auto text = readString(object, name);
143 if (!text) return eve::Result<std::int64_t>::failure(text.status());
144 std::int64_t output = 0;
145 const std::string& value = text.value();
146 const auto [end, error] = std::from_chars(value.data(), value.data() + value.size(), output);
147 if (value.empty() || error != std::errc{} || end != value.data() + value.size())
149 eve::DiagnosticCode::ParseError, "snapshot integer is not an int64 decimal string", std::string(name)));
151}
152
153eve::Result<double> readNumber(const eve::Value::Object& object, std::string_view name) {
154 const eve::Value* value = field(object, name);
155 if (!value || !value->isNumeric())
157 eve::DiagnosticCode::ParseError, "snapshot field must be numeric", std::string(name)));
158 const double result = value->isDouble() ? value->asDouble() : static_cast<double>(value->asInt());
159 if (!std::isfinite(result))
161 eve::DiagnosticCode::InvalidArgument, "snapshot numeric field must be finite", std::string(name)));
162 return eve::Result<double>::success(result);
163}
164
165eve::Result<float> readFloat(const eve::Value::Object& object, std::string_view name) {
166 auto number = readNumber(object, name);
167 if (!number) return eve::Result<float>::failure(number.status());
168 if (number.value() < -std::numeric_limits<float>::max() || number.value() > std::numeric_limits<float>::max())
170 eve::DiagnosticCode::InvalidArgument, "snapshot numeric field is outside float range", std::string(name)));
171 return eve::Result<float>::success(static_cast<float>(number.value()));
172}
173
174eve::Result<bool> readBool(const eve::Value::Object& object, std::string_view name) {
175 const eve::Value* value = field(object, name);
176 if (!value || !value->isBool())
178 "snapshot field must be boolean", std::string(name)));
179 return eve::Result<bool>::success(value->asBool());
180}
181
182eve::Result<eve::LogicalId> snapshotSchema(std::string_view text) {
183 const auto parsed = eve::LogicalId::parse(text);
184 if (!parsed)
186 eve::DiagnosticCode::InvariantViolation, "physics snapshot schema constant is invalid", "schema"));
188}
189
190eve::Result<void> checkEnvelope(const eve::SnapshotEnvelope& snapshot, std::string_view type, std::string_view schema) {
191 if (snapshot.type != type || snapshot.schema.format() != schema)
194 "snapshot type or schema does not belong to this physics world", "snapshot.type"));
195 if (snapshot.schemaVersion.value() != kSnapshotVersion && snapshot.schemaVersion.value() != 1)
198 "physics world snapshot schema version is not supported", "snapshot.schemaVersion"));
200}
201
202struct ObservationState {
203 SimulationObservation value;
204};
205
206eve::Result<ObservationState> parseObservation(const eve::Value::Object& object,
207 const eve::SnapshotEnvelope& snapshot) {
208 auto tick = readUint64(object, "tick");
209 if (!tick) return eve::Result<ObservationState>::failure(tick.status());
210 auto revision = readUint64(object, "revision");
212 auto stepCount = readUint64(object, "stepCount");
213 if (!stepCount) return eve::Result<ObservationState>::failure(stepCount.status());
214 auto duration = readInt64(object, "simulatedDurationNs");
216 auto lastDelta = readFloat(object, "lastDeltaSeconds");
217 if (!lastDelta) return eve::Result<ObservationState>::failure(lastDelta.status());
218 if (duration.value() < 0 || revision.value() != stepCount.value() || tick.value() != snapshot.tick.value() ||
219 stepCount.value() != snapshot.revision.value()) {
221 eve::DiagnosticCode::Conflict, "physics snapshot payload progress disagrees with its envelope",
222 "payload.observation"));
223 }
224
225 ObservationState result;
226 result.value.stepCount = stepCount.value();
227 result.value.lastTick = eve::SimulationTick(tick.value());
228 result.value.simulatedDuration = eve::Duration::fromNanoseconds(duration.value());
229 result.value.simulatedSeconds = result.value.simulatedDuration.seconds();
230 result.value.lastDeltaSeconds = lastDelta.value();
231 auto valid = detail::validateSimulationObservation(result.value, "physics.snapshot.observation");
233 return eve::Result<ObservationState>::success(std::move(result));
234}
235
236struct Body2DState {
237 int id = 0;
238 std::string type;
239 float x = 0.f, y = 0.f, angle = 0.f;
240 float vx = 0.f, vy = 0.f, angularVelocity = 0.f;
241 bool active = false, bullet = false, awake = false, fixedRotation = false;
243};
244
245struct Body3DState {
246 int id = 0;
247 std::string type;
248 float x = 0.f, y = 0.f, z = 0.f;
249 float qx = 0.f, qy = 0.f, qz = 0.f, qw = 1.f;
250 float vx = 0.f, vy = 0.f, vz = 0.f;
251 float wx = 0.f, wy = 0.f, wz = 0.f;
252 bool active = false, bullet = false, awake = false, fixedRotation = false;
253};
254
255bool validBodyType(const std::string& type) { return type == "static" || type == "kinematic" || type == "dynamic"; }
256
257eve::Result<Body2DState> parseBody2D(const eve::Value& value) {
258 const auto* object = value.getIf<eve::Value::Object>();
259 if (!object || !hasExactFields(*object, {"active", "angle", "awake", "bullet", "fixedRotation", "fixtures", "id", "type", "vx",
260 "vy", "angularVelocity", "x", "y"}))
263 "2D physics snapshot body has unknown or missing fields", "payload.bodies"));
264 Body2DState state;
265 auto id = readUint64(*object, "id");
266 if (!id || id.value() > static_cast<std::uint64_t>(std::numeric_limits<int>::max()))
268 eve::DiagnosticCode::ParseError, "2D physics snapshot body id is invalid", "payload.bodies.id"));
269 state.id = static_cast<int>(id.value());
270 auto type = readString(*object, "type");
271 if (!type || !validBodyType(type.value()))
273 eve::DiagnosticCode::InvalidArgument, "2D physics snapshot body type is invalid", "payload.bodies.type"));
274 state.type = std::move(type).takeValue();
275 auto x = readFloat(*object, "x");
276 if (!x) return eve::Result<Body2DState>::failure(x.status());
277 state.x = x.value();
278 auto y = readFloat(*object, "y");
279 if (!y) return eve::Result<Body2DState>::failure(y.status());
280 state.y = y.value();
281 auto angle = readFloat(*object, "angle");
282 if (!angle) return eve::Result<Body2DState>::failure(angle.status());
283 state.angle = angle.value();
284 auto vx = readFloat(*object, "vx");
285 if (!vx) return eve::Result<Body2DState>::failure(vx.status());
286 state.vx = vx.value();
287 auto vy = readFloat(*object, "vy");
288 if (!vy) return eve::Result<Body2DState>::failure(vy.status());
289 state.vy = vy.value();
290 auto angular = readFloat(*object, "angularVelocity");
291 if (!angular) return eve::Result<Body2DState>::failure(angular.status());
292 state.angularVelocity = angular.value();
293 auto active = readBool(*object, "active");
294 if (!active) return eve::Result<Body2DState>::failure(active.status());
295 state.active = active.value();
296 auto bullet = readBool(*object, "bullet");
297 if (!bullet) return eve::Result<Body2DState>::failure(bullet.status());
298 state.bullet = bullet.value();
299 auto awake = readBool(*object, "awake");
300 if (!awake) return eve::Result<Body2DState>::failure(awake.status());
301 state.awake = awake.value();
302 auto fixed = readBool(*object, "fixedRotation");
303 if (!fixed) return eve::Result<Body2DState>::failure(fixed.status());
304 state.fixedRotation = fixed.value();
305 const eve::Value* fixtures = field(*object, "fixtures");
306 if (!fixtures || !fixtures->isArray())
308 "2D physics snapshot fixtures must be an array",
309 "payload.bodies.fixtures"));
310 state.fixtures = *fixtures;
311 return eve::Result<Body2DState>::success(std::move(state));
312}
313
314eve::Result<Body3DState> parseBody3D(const eve::Value& value) {
315 const auto* object = value.getIf<eve::Value::Object>();
316 if (!object || !hasExactFields(*object, {"active", "awake", "bullet", "fixedRotation", "id", "type", "x", "y", "z",
317 "qx", "qy", "qz", "qw", "vx", "vy", "vz", "wx", "wy", "wz"}))
320 "3D physics snapshot body has unknown or missing fields", "payload.bodies"));
321 Body3DState state;
322 auto id = readUint64(*object, "id");
323 if (!id || id.value() > static_cast<std::uint64_t>(std::numeric_limits<int>::max()))
325 eve::DiagnosticCode::ParseError, "3D physics snapshot body id is invalid", "payload.bodies.id"));
326 state.id = static_cast<int>(id.value());
327 auto type = readString(*object, "type");
328 if (!type || !validBodyType(type.value()))
330 eve::DiagnosticCode::InvalidArgument, "3D physics snapshot body type is invalid", "payload.bodies.type"));
331 state.type = std::move(type).takeValue();
332#define EV_READ_BODY3D_FLOAT(name) \
333 do { \
334 auto value = readFloat(*object, #name); \
335 if (!value) return eve::Result<Body3DState>::failure(value.status()); \
336 state.name = value.value(); \
337 } while (false)
351#undef EV_READ_BODY3D_FLOAT
352 const double quaternionLengthSquared =
353 static_cast<double>(state.qx) * state.qx + static_cast<double>(state.qy) * state.qy +
354 static_cast<double>(state.qz) * state.qz + static_cast<double>(state.qw) * state.qw;
355 if (!(quaternionLengthSquared > 1e-16))
357 "3D physics snapshot rotation must be non-zero",
358 "payload.bodies.rotation"));
359#define EV_READ_BODY3D_BOOL(name) \
360 do { \
361 auto value = readBool(*object, #name); \
362 if (!value) return eve::Result<Body3DState>::failure(value.status()); \
363 state.name = value.value(); \
364 } while (false)
369#undef EV_READ_BODY3D_BOOL
370 return eve::Result<Body3DState>::success(std::move(state));
371}
372
373eve::Value bodyValue(const Body& body) {
375 object.emplace("active", eve::Value(body.isActive()));
376 object.emplace("angle", eve::Value(body.getAngle()));
377 object.emplace("awake", eve::Value(body.isAwake()));
378 object.emplace("bullet", eve::Value(body.isBullet()));
379 object.emplace("fixedRotation", eve::Value(body.isFixedRotation()));
380 object.emplace("id", eve::Value(std::to_string(body.getId())));
381 object.emplace("type", eve::Value(body.getType()));
382 object.emplace("vx", eve::Value(body.getLinearVelocityX()));
383 object.emplace("vy", eve::Value(body.getLinearVelocityY()));
384 object.emplace("angularVelocity", eve::Value(body.getAngularVelocity()));
385 object.emplace("x", eve::Value(body.getX()));
386 object.emplace("y", eve::Value(body.getY()));
387 return eve::Value(std::move(object));
388}
389
390eve::Value bodyValue(const Body3D& body) {
392 object.emplace("active", eve::Value(body.isActive()));
393 object.emplace("awake", eve::Value(body.isAwake()));
394 object.emplace("bullet", eve::Value(body.isBullet()));
395 object.emplace("fixedRotation", eve::Value(body.isFixedRotation()));
396 object.emplace("id", eve::Value(std::to_string(body.getId())));
397 object.emplace("type", eve::Value(body.getType()));
398 object.emplace("x", eve::Value(body.getX()));
399 object.emplace("y", eve::Value(body.getY()));
400 object.emplace("z", eve::Value(body.getZ()));
401 object.emplace("qx", eve::Value(body.getRotX()));
402 object.emplace("qy", eve::Value(body.getRotY()));
403 object.emplace("qz", eve::Value(body.getRotZ()));
404 object.emplace("qw", eve::Value(body.getRotW()));
405 object.emplace("vx", eve::Value(body.getLinearVelocityX()));
406 object.emplace("vy", eve::Value(body.getLinearVelocityY()));
407 object.emplace("vz", eve::Value(body.getLinearVelocityZ()));
408 object.emplace("wx", eve::Value(body.getAngularVelocityX()));
409 object.emplace("wy", eve::Value(body.getAngularVelocityY()));
410 object.emplace("wz", eve::Value(body.getAngularVelocityZ()));
411 return eve::Value(std::move(object));
412}
413
414eve::Value fixtureTopologyValue(const Body& body) {
416 const b2Body* rawBody = body.raw();
417 for (const b2Fixture* fixture = rawBody ? rawBody->GetFixtureList() : nullptr; fixture;
418 fixture = fixture->GetNext()) {
420 const b2Shape* shape = fixture->GetShape();
421 object.emplace("type", eve::Value(std::to_string(static_cast<int>(shape->GetType()))));
422 object.emplace("radius", eve::Value(shape->m_radius));
423 eve::Value::Array geometry;
424 switch (shape->GetType()) {
425 case b2Shape::e_circle: {
426 const auto* circle = static_cast<const b2CircleShape*>(shape);
427 geometry.emplace_back(circle->m_p.x);
428 geometry.emplace_back(circle->m_p.y);
429 break;
430 }
431 case b2Shape::e_polygon: {
432 const auto* polygon = static_cast<const b2PolygonShape*>(shape);
433 for (int index = 0; index < polygon->m_count; ++index) {
434 geometry.emplace_back(polygon->m_vertices[index].x);
435 geometry.emplace_back(polygon->m_vertices[index].y);
436 }
437 break;
438 }
439 case b2Shape::e_chain: {
440 const auto* chain = static_cast<const b2ChainShape*>(shape);
441 for (int index = 0; index < chain->m_count; ++index) {
442 geometry.emplace_back(chain->m_vertices[index].x);
443 geometry.emplace_back(chain->m_vertices[index].y);
444 }
445 geometry.emplace_back(chain->m_hasPrevVertex);
446 geometry.emplace_back(chain->m_hasNextVertex);
447 break;
448 }
449 case b2Shape::e_edge: {
450 const auto* edge = static_cast<const b2EdgeShape*>(shape);
451 geometry.emplace_back(edge->m_vertex1.x);
452 geometry.emplace_back(edge->m_vertex1.y);
453 geometry.emplace_back(edge->m_vertex2.x);
454 geometry.emplace_back(edge->m_vertex2.y);
455 break;
456 }
457 default: break;
458 }
459 object.emplace("geometry", eve::Value(std::move(geometry)));
460 fixtures.emplace_back(std::move(object));
461 }
462 return eve::Value(std::move(fixtures));
463}
464
465eve::Value shapesTopologyValue(const std::vector<Shape3D*>& shapes) {
466 std::vector<Shape3D*> sorted = shapes;
467 std::sort(sorted.begin(), sorted.end(), [](const Shape3D* left, const Shape3D* right) {
468 return left->getId() < right->getId();
469 });
471 for (const Shape3D* shape : sorted)
472 if (shape && shape->isValid()) values.push_back(WorldSnapshotAccess::shapeSource(*shape));
473 return eve::Value(std::move(values));
474}
475
476eve::Value jointsTopologyValue(const std::vector<Joint3D*>& joints) {
477 std::vector<Joint3D*> sorted = joints;
478 std::sort(sorted.begin(), sorted.end(), [](const Joint3D* left, const Joint3D* right) {
479 return left->getId() < right->getId();
480 });
482 for (const Joint3D* joint : sorted) {
483 if (!joint || !joint->isValid()) continue;
485 object.emplace("id", eve::Value(std::to_string(joint->getId())));
486 object.emplace("kind", eve::Value(joint->getKind()));
487 object.emplace("bodyAId", eve::Value(std::to_string(joint->getBodyAId())));
488 object.emplace("bodyBId", eve::Value(std::to_string(joint->getBodyBId())));
489 const b3Transform frameA = b3Joint_GetLocalFrameA(joint->raw());
490 const b3Transform frameB = b3Joint_GetLocalFrameB(joint->raw());
491 eve::Value::Array frames;
492 for (float component : {static_cast<float>(frameA.p.x), static_cast<float>(frameA.p.y),
493 static_cast<float>(frameA.p.z), frameA.q.v.x, frameA.q.v.y, frameA.q.v.z, frameA.q.s,
494 static_cast<float>(frameB.p.x), static_cast<float>(frameB.p.y),
495 static_cast<float>(frameB.p.z), frameB.q.v.x, frameB.q.v.y, frameB.q.v.z, frameB.q.s})
496 frames.emplace_back(component);
497 object.emplace("localFrames", eve::Value(std::move(frames)));
498 values.emplace_back(std::move(object));
499 }
500 return eve::Value(std::move(values));
501}
502
503eve::Value::Object observationFields(const SimulationObservation& observation, eve::SimulationTick tick) {
505 object.emplace("tick", eve::Value(std::to_string(tick.value())));
506 object.emplace("revision", eve::Value(std::to_string(observation.stepCount)));
507 object.emplace("stepCount", eve::Value(std::to_string(observation.stepCount)));
508 object.emplace("simulatedDurationNs", eve::Value(std::to_string(observation.simulatedDuration.nanoseconds())));
509 object.emplace("lastDeltaSeconds", eve::Value(observation.lastDeltaSeconds));
510 return object;
511}
512
513eve::Value make2DPayload(const World& world, const std::vector<Body*>& bodies) {
514 const auto observation = world.simulationObservation();
515 eve::Value::Object object = observationFields(observation, world.simulationTick());
516 object.emplace("gravityX", eve::Value(world.getGravityX()));
517 object.emplace("gravityY", eve::Value(world.getGravityY()));
518 object.emplace("meter", eve::Value(world.getMeter()));
519 std::vector<Body*> sortedBodies = bodies;
520 std::sort(sortedBodies.begin(), sortedBodies.end(),
521 [](const Body* left, const Body* right) { return left->getId() < right->getId(); });
523 values.reserve(sortedBodies.size());
524 for (const Body* body : sortedBodies)
525 if (body && body->isValid()) {
526 eve::Value value = bodyValue(*body);
527 value.getIf<eve::Value::Object>()->emplace("fixtures", fixtureTopologyValue(*body));
528 values.push_back(std::move(value));
529 }
530 object.emplace("bodies", eve::Value(std::move(values)));
531 return eve::Value(std::move(object));
532}
533
534eve::Value make3DPayload(const World3D& world, const std::vector<Body3D*>& bodies,
535 const std::vector<Shape3D*>& shapes, const std::vector<Joint3D*>& joints) {
536 const auto observation = world.simulationObservation();
537 eve::Value::Object object = observationFields(observation, world.simulationTick());
538 object.emplace("gravityX", eve::Value(world.getGravityX()));
539 object.emplace("gravityY", eve::Value(world.getGravityY()));
540 object.emplace("gravityZ", eve::Value(world.getGravityZ()));
541 std::vector<Body3D*> sortedBodies = bodies;
542 std::sort(sortedBodies.begin(), sortedBodies.end(),
543 [](const Body3D* left, const Body3D* right) { return left->getId() < right->getId(); });
545 values.reserve(sortedBodies.size());
546 for (const Body3D* body : sortedBodies)
547 if (body && body->isValid()) values.push_back(bodyValue(*body));
548 object.emplace("bodies", eve::Value(std::move(values)));
549 object.emplace("shapes", shapesTopologyValue(shapes));
550 object.emplace("joints", jointsTopologyValue(joints));
551 return eve::Value(std::move(object));
552}
553
554template <typename BodyState>
555eve::Result<void> matchBodyIds(const std::vector<BodyState>& states, std::set<int>& ids) {
556 for (const BodyState& state : states) {
557 if (!ids.insert(state.id).second)
559 eve::DiagnosticCode::Conflict, "physics snapshot contains a duplicate body id", "payload.bodies.id"));
560 }
562}
563
564eve::Result<std::vector<Body2DState>> parseBodies2D(const eve::Value::Object& object) {
565 const eve::Value* value = field(object, "bodies");
566 const auto* array = value ? value->getIf<eve::Value::Array>() : nullptr;
567 if (!array)
569 eve::DiagnosticCode::ParseError, "physics snapshot bodies must be an array", "payload.bodies"));
570 std::vector<Body2DState> result;
571 result.reserve(array->size());
572 for (const eve::Value& entry : *array) {
573 auto body = parseBody2D(entry);
574 if (!body) return eve::Result<std::vector<Body2DState>>::failure(body.status());
575 result.push_back(std::move(body).takeValue());
576 }
577 return eve::Result<std::vector<Body2DState>>::success(std::move(result));
578}
579
580eve::Result<std::vector<Body3DState>> parseBodies3D(const eve::Value::Object& object) {
581 const eve::Value* value = field(object, "bodies");
582 const auto* array = value ? value->getIf<eve::Value::Array>() : nullptr;
583 if (!array)
585 eve::DiagnosticCode::ParseError, "physics snapshot bodies must be an array", "payload.bodies"));
586 std::vector<Body3DState> result;
587 result.reserve(array->size());
588 for (const eve::Value& entry : *array) {
589 auto body = parseBody3D(entry);
590 if (!body) return eve::Result<std::vector<Body3DState>>::failure(body.status());
591 result.push_back(std::move(body).takeValue());
592 }
593 return eve::Result<std::vector<Body3DState>>::success(std::move(result));
594}
595
596eve::Result<std::unique_ptr<World>> prepareWorld2D(const std::vector<Body2DState>& states,
597 float gravityX, float gravityY, float meter,
598 const SimulationObservation& observation,
599 eve::PersistentId instanceId) {
600 try {
601 auto prepared = std::make_unique<World>(gravityX, gravityY, false, meter, instanceId);
602 int maxId = 0;
603 for (const Body2DState& state : states) {
604 Body* body = prepared->newBody(state.type, state.x, state.y);
606 maxId = std::max(maxId, state.id);
607 const auto* fixtures = state.fixtures.getIf<eve::Value::Array>();
608 for (auto it = fixtures->rbegin(); it != fixtures->rend(); ++it) {
609 const auto* fixture = it->getIf<eve::Value::Object>();
610 if (!fixture || !hasExactFields(*fixture, {"geometry", "radius", "type"}))
611 return eve::Result<std::unique_ptr<World>>::failure(
613 "2D fixture has unknown or missing fields", "payload.bodies.fixtures"));
614 auto type = readInt64(*fixture, "type");
615 auto radius = readFloat(*fixture, "radius");
616 const eve::Value* geometryValue = field(*fixture, "geometry");
617 const auto* geometry = geometryValue ? geometryValue->getIf<eve::Value::Array>() : nullptr;
618 if (!type || !radius || !geometry)
619 return eve::Result<std::unique_ptr<World>>::failure(
620 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "2D fixture geometry is malformed",
621 "payload.bodies.fixtures.geometry"));
622 std::vector<float> values;
623 values.reserve(geometry->size());
624 for (const eve::Value& component : *geometry) {
625 if (!component.isNumeric()) {
626 if (component.isBool()) continue;
628 eve::DiagnosticCode::ParseError, "2D fixture coordinate must be numeric",
629 "payload.bodies.fixtures.geometry"));
630 }
631 values.push_back(static_cast<float>(component.isDouble() ? component.asDouble() : component.asInt()) * meter);
632 }
633 Fixture* created = nullptr;
634 if (type.value() == b2Shape::e_circle && values.size() == 2)
635 created = body->newCircleFixture(radius.value() * meter);
636 else if (type.value() == b2Shape::e_polygon && values.size() >= 6)
637 created = body->newPolygonFixture(values);
638 else if (type.value() == b2Shape::e_chain && values.size() >= 4) {
639 const bool loop = geometry->size() >= 2 && (*geometry)[geometry->size() - 2].isBool() &&
640 (*geometry)[geometry->size() - 2].asBool() && geometry->back().isBool() &&
641 geometry->back().asBool();
642 created = body->newChainFixture(values, loop);
643 }
644 if (!created)
646 eve::DiagnosticCode::Unsupported, "2D fixture shape cannot be reconstructed",
647 "payload.bodies.fixtures.type"));
648 }
649 body->setFixedRotation(state.fixedRotation);
650 body->setBullet(state.bullet);
651 body->setActive(state.active);
652 body->setAngle(state.angle);
653 body->setLinearVelocity(state.vx, state.vy);
654 body->setAngularVelocity(state.angularVelocity);
655 body->setAwake(state.awake);
656 }
657 auto restored = WorldSnapshotAccess::finishPrepared(*prepared, maxId + 1, observation);
658 if (!restored) return eve::Result<std::unique_ptr<World>>::failure(restored.status());
659 return eve::Result<std::unique_ptr<World>>::success(std::move(prepared));
660 } catch (const std::exception& error) {
662 eve::DiagnosticCode::Failed, std::string("2D topology preparation failed: ") + error.what(),
663 "physics.world.restore.prepare"));
664 }
665}
666
667eve::Result<std::vector<float>> readFloatArray(const eve::Value::Object& object, std::string_view name) {
668 const eve::Value* value = field(object, name);
669 const auto* array = value ? value->getIf<eve::Value::Array>() : nullptr;
670 if (!array)
672 eve::DiagnosticCode::ParseError, "snapshot field must be an array", std::string(name)));
673 std::vector<float> result;
674 result.reserve(array->size());
675 for (const eve::Value& item : *array) {
676 if (!item.isNumeric())
678 eve::DiagnosticCode::ParseError, "snapshot array item must be numeric", std::string(name)));
679 result.push_back(static_cast<float>(item.isDouble() ? item.asDouble() : item.asInt()));
680 }
681 return eve::Result<std::vector<float>>::success(std::move(result));
682}
683
684eve::Result<std::vector<std::int32_t>> readIntArray(const eve::Value::Object& object, std::string_view name) {
685 const eve::Value* value = field(object, name);
686 const auto* array = value ? value->getIf<eve::Value::Array>() : nullptr;
687 if (!array)
689 eve::DiagnosticCode::ParseError, "snapshot field must be an array", std::string(name)));
690 std::vector<std::int32_t> result;
691 for (const eve::Value& item : *array) {
692 if (!item.isString())
694 eve::DiagnosticCode::ParseError, "snapshot integer array item must be a string", std::string(name)));
695 std::int64_t parsed = 0;
696 const std::string& text = item.asString();
697 const auto [end, ec] = std::from_chars(text.data(), text.data() + text.size(), parsed);
698 if (ec != std::errc{} || end != text.data() + text.size() || parsed < INT32_MIN || parsed > INT32_MAX)
700 eve::DiagnosticCode::ParseError, "snapshot integer array item is invalid", std::string(name)));
701 result.push_back(static_cast<std::int32_t>(parsed));
702 }
703 return eve::Result<std::vector<std::int32_t>>::success(std::move(result));
704}
705
706eve::Result<std::unique_ptr<World3D>> prepareWorld3D(const std::vector<Body3DState>& states,
707 const eve::Value::Array& shapes,
708 const eve::Value::Array& joints,
709 float gravityX, float gravityY, float gravityZ,
710 const SimulationObservation& observation,
711 eve::PersistentId instanceId) {
712 try {
713 auto prepared = WorldSnapshotAccess::makeDetachedWorld3D(gravityX, gravityY, gravityZ, instanceId);
714 std::unordered_map<int, Body3D*> bodyById;
715 int maxBodyId = 0;
716 for (const Body3DState& state : states) {
717 Body3D* body = prepared->newBody(state.type, state.x, state.y, state.z);
719 bodyById.emplace(state.id, body);
720 maxBodyId = std::max(maxBodyId, state.id);
721 body->setFixedRotation(state.fixedRotation);
722 body->setBullet(state.bullet);
723 body->setActive(state.active);
724 body->setRotation(state.qx, state.qy, state.qz, state.qw);
725 body->setLinearVelocity(state.vx, state.vy, state.vz);
726 body->setAngularVelocity(state.wx, state.wy, state.wz);
727 body->setAwake(state.awake);
728 }
729 int maxShapeId = 0;
730 std::set<int> shapeIds;
731 for (const eve::Value& value : shapes) {
732 const auto* object = value.getIf<eve::Value::Object>();
733 if (!object)
735 eve::DiagnosticCode::ParseError, "3D shape must be an object", "payload.shapes"));
736 auto bodyId = readInt64(*object, "bodyId");
737 auto id = readInt64(*object, "id");
738 auto kind = readString(*object, "kind");
739 auto a = readFloat(*object, "a"); auto b = readFloat(*object, "b"); auto c = readFloat(*object, "c");
740 if (!bodyId || !id || id.value() <= 0 || id.value() > std::numeric_limits<int>::max() ||
741 !shapeIds.insert(static_cast<int>(id.value())).second || !kind || !a || !b || !c ||
742 !bodyById.contains(static_cast<int>(bodyId.value())))
744 eve::DiagnosticCode::Conflict, "3D shape references invalid identity or source", "payload.shapes"));
745 Body3D* body = bodyById.at(static_cast<int>(bodyId.value()));
746 Shape3D* shape = nullptr;
747 if (kind.value() == "box") shape = body->newBoxShape(a.value() * 2.f, b.value() * 2.f, c.value() * 2.f);
748 else if (kind.value() == "sphere") shape = body->newSphereShape(a.value());
749 else if (kind.value() == "capsule") shape = body->newCapsuleShape(a.value() * 2.f, b.value());
750 else if (kind.value() == "convexHull") {
751 auto vertices = readFloatArray(*object, "hullVertices"); auto maxVertices = readInt64(*object, "hullMaxVertices");
752 if (!vertices || !maxVertices)
754 eve::DiagnosticCode::ParseError, "convex hull source is invalid", "payload.shapes"));
755 shape = body->newConvexHullShape(vertices.value(), static_cast<int>(maxVertices.value()));
756 } else if (kind.value() == "triangleMesh") {
757 auto vertices = readFloatArray(*object, "meshVertices"); auto indices = readIntArray(*object, "meshIndices");
758 auto weld = readBool(*object, "meshWeldVertices"); auto tolerance = readFloat(*object, "meshWeldTolerance");
759 auto edges = readBool(*object, "meshIdentifyEdges"); auto median = readBool(*object, "meshUseMedianSplit");
760 if (!vertices || !indices || !weld || !tolerance || !edges || !median)
762 eve::DiagnosticCode::ParseError, "mesh source is invalid", "payload.shapes"));
763 shape = body->newTriangleMeshShape(vertices.value(), indices.value(), weld.value(), tolerance.value(), edges.value(), median.value());
764 } else if (kind.value() == "heightField") {
765 auto heights = readFloatArray(*object, "heightValues"); auto cx = readInt64(*object, "heightCountX"); auto cz = readInt64(*object, "heightCountZ");
766 auto sx = readFloat(*object, "heightCellSizeX"); auto sz = readFloat(*object, "heightCellSizeZ"); auto mn = readFloat(*object, "heightGlobalMin"); auto mx = readFloat(*object, "heightGlobalMax"); auto clockwise = readBool(*object, "heightClockwise");
767 if (!heights || !cx || !cz || !sx || !sz || !mn || !mx || !clockwise)
769 eve::DiagnosticCode::ParseError, "height field source is invalid", "payload.shapes"));
770 shape = body->newHeightFieldShape(static_cast<int>(cx.value()), static_cast<int>(cz.value()), sx.value(), sz.value(), heights.value(), mn.value(), mx.value(), clockwise.value());
771 }
772 if (!shape)
774 eve::DiagnosticCode::Unsupported, "3D shape kind cannot be reconstructed", "payload.shapes.kind"));
775 WorldSnapshotAccess::setShapeId(*shape, static_cast<int>(id.value()));
776 maxShapeId = std::max(maxShapeId, static_cast<int>(id.value()));
777 }
778 int maxJointId = 0;
779 std::set<int> jointIds;
780 for (const eve::Value& value : joints) {
781 const auto* object = value.getIf<eve::Value::Object>();
782 if (!object || !hasExactFields(*object, {"bodyAId", "bodyBId", "id", "kind", "localFrames"}))
784 eve::DiagnosticCode::ParseError, "3D joint has unknown or missing fields", "payload.joints"));
785 auto bodyAId = readInt64(*object, "bodyAId"); auto bodyBId = readInt64(*object, "bodyBId");
786 auto id = readInt64(*object, "id");
787 auto kind = readString(*object, "kind"); auto frames = readFloatArray(*object, "localFrames");
788 if (!bodyAId || !bodyBId || !id || id.value() <= 0 || id.value() > std::numeric_limits<int>::max() ||
789 !jointIds.insert(static_cast<int>(id.value())).second || !kind || !frames || frames.value().size() != 14 ||
790 !bodyById.contains(static_cast<int>(bodyAId.value())) || !bodyById.contains(static_cast<int>(bodyBId.value())))
792 eve::DiagnosticCode::Conflict, "3D joint references invalid bodies or frames", "payload.joints"));
793 Body3D* aBody = bodyById.at(static_cast<int>(bodyAId.value())); Body3D* bBody = bodyById.at(static_cast<int>(bodyBId.value()));
794 const auto& f = frames.value();
795 const b3Pos anchorA = b3Body_GetWorldPoint(aBody->raw(), b3Pos{f[0], f[1], f[2]});
796 const b3Pos anchorB = b3Body_GetWorldPoint(bBody->raw(), b3Pos{f[7], f[8], f[9]});
797 const b3Quat qa{{f[3], f[4], f[5]}, f[6]}; const b3Quat qb{{f[10], f[11], f[12]}, f[13]};
798 Joint3D* joint = nullptr;
799 if (kind.value() == "distance") {
800 const b3Vec3 d = anchorB - anchorA; const float length = std::sqrt(b3Dot(d, d));
801 joint = prepared->newDistanceJoint(aBody, bBody, anchorA.x, anchorA.y, anchorA.z, anchorB.x, anchorB.y, anchorB.z, length);
802 } else if (kind.value() == "revolute") {
803 const b3Vec3 axis = b3Body_GetWorldVector(aBody->raw(), b3RotateVector(qa, b3Vec3_axisZ));
804 joint = prepared->newRevoluteJoint(aBody, bBody, anchorA.x, anchorA.y, anchorA.z, axis.x, axis.y, axis.z);
805 } else if (kind.value() == "prismatic") {
806 const b3Vec3 axis = b3Body_GetWorldVector(aBody->raw(), b3RotateVector(qa, b3Vec3_axisX));
807 joint = prepared->newPrismaticJoint(aBody, bBody, anchorA.x, anchorA.y, anchorA.z, axis.x, axis.y, axis.z);
808 } else if (kind.value() == "spherical") {
809 const b3Vec3 axis = b3Body_GetWorldVector(aBody->raw(), b3RotateVector(qa, b3Vec3_axisZ));
810 joint = prepared->newSphericalJoint(aBody, bBody, anchorA.x, anchorA.y, anchorA.z, axis.x, axis.y, axis.z);
811 } else if (kind.value() == "wheel") {
812 const b3Vec3 suspension = b3Body_GetWorldVector(aBody->raw(), b3RotateVector(qa, b3Vec3_axisX));
813 const b3Vec3 wheel = b3Body_GetWorldVector(bBody->raw(), b3RotateVector(qb, b3Vec3_axisZ));
814 joint = prepared->newWheelJoint(aBody, bBody, anchorA.x, anchorA.y, anchorA.z, suspension.x, suspension.y, suspension.z, wheel.x, wheel.y, wheel.z);
815 } else if (kind.value() == "weld") {
816 joint = prepared->newWeldJoint(aBody, bBody, anchorA.x, anchorA.y, anchorA.z);
817 } else if (kind.value() == "motor") {
818 joint = prepared->newMotorJoint(aBody, bBody);
819 } else if (kind.value() == "parallel") {
820 const b3Vec3 axis = b3Body_GetWorldVector(aBody->raw(), b3RotateVector(qa, b3Vec3_axisZ));
821 joint = prepared->newParallelJoint(aBody, bBody, axis.x, axis.y, axis.z);
822 } else if (kind.value() == "filter") {
823 joint = prepared->newFilterJoint(aBody, bBody);
824 }
825 if (!joint)
827 eve::DiagnosticCode::Unsupported, "3D joint kind cannot be reconstructed", "payload.joints.kind"));
828 WorldSnapshotAccess::setJointId(*joint, static_cast<int>(id.value()));
829 maxJointId = std::max(maxJointId, static_cast<int>(id.value()));
830 }
831 auto restored = WorldSnapshotAccess::finishPrepared(*prepared, maxBodyId + 1, maxShapeId + 1,
832 maxJointId + 1, observation);
833 if (!restored) return eve::Result<std::unique_ptr<World3D>>::failure(restored.status());
834 return eve::Result<std::unique_ptr<World3D>>::success(std::move(prepared));
835 } catch (const std::exception& error) {
837 eve::DiagnosticCode::Failed, std::string("3D topology preparation failed: ") + error.what(),
838 "physics.world3d.restore.prepare"));
839 }
840}
841
842} // namespace
843
845 if (!isValid() || !simulation_)
847 eve::DiagnosticCode::PreconditionViolation, "Cannot snapshot a destroyed or uninitialized physics world",
848 "physics.world.snapshot"));
849 auto schema = snapshotSchema(kWorld2DSchema);
850 if (!schema) return eve::Result<eve::SnapshotEnvelope>::failure(schema.status());
851 const auto observation = simulationObservation();
852 std::vector<Body*> bodies(bodies_.begin(), bodies_.end());
853 return eve::makeSnapshotEnvelope(std::string(kWorld2DType), std::move(schema).takeValue(),
854 eve::SchemaVersion(kSnapshotVersion), instanceId_,
855 eve::Revision(observation.stepCount), simulationTick(),
856 make2DPayload(*this, bodies), hashProvider);
857}
858
860 const eve::SnapshotHashProvider& hashProvider) {
861 if (!isValid() || !simulation_)
863 eve::DiagnosticCode::PreconditionViolation, "Cannot restore a destroyed or uninitialized physics world",
864 "physics.world.restore"));
865 auto verified = eve::verifySnapshotEnvelope(snapshotValue, hashProvider);
866 if (!verified) return verified;
867 auto envelope = checkEnvelope(snapshotValue, kWorld2DType, kWorld2DSchema);
868 if (!envelope) return envelope;
869 const bool legacyV1 = snapshotValue.schemaVersion.value() == 1;
870 if (!legacyV1 && (snapshotValue.instanceId.isNil() || snapshotValue.instanceId != instanceId_))
873 "physics snapshot belongs to a different world instance", "snapshot.instanceId"));
874 eve::Value migratedPayload = snapshotValue.payload;
875 if (legacyV1) {
876 auto* migrated = migratedPayload.getIf<eve::Value::Object>();
877 auto* bodyValues = migrated ? (*migrated)["bodies"].getIf<eve::Value::Array>() : nullptr;
878 if (!bodyValues)
880 eve::DiagnosticCode::ParseError, "v1 physics snapshot bodies are malformed", "payload.bodies"));
881 for (eve::Value& value : *bodyValues) {
882 auto* bodyObject = value.getIf<eve::Value::Object>();
883 if (!bodyObject)
885 eve::DiagnosticCode::ParseError, "v1 physics snapshot body is malformed", "payload.bodies"));
886 auto id = readUint64(*bodyObject, "id");
887 Body* live = nullptr;
888 for (Body* candidate : bodies_) if (candidate && id && candidate->getId() == static_cast<int>(id.value())) live = candidate;
889 if (!live)
892 "v1 snapshot requires topology-compatible live bodies", "payload.bodies"));
893 bodyObject->emplace("fixtures", fixtureTopologyValue(*live));
894 }
895 }
896 const auto* object = migratedPayload.getIf<eve::Value::Object>();
897 if (!object || !hasExactFields(*object, {"bodies", "gravityX", "gravityY", "meter", "tick", "revision", "stepCount",
898 "simulatedDurationNs", "lastDeltaSeconds"}))
900 eve::DiagnosticCode::ParseError, "2D physics snapshot payload has unknown or missing fields", "payload"));
901 auto observation = parseObservation(*object, snapshotValue);
902 if (!observation) return eve::Result<void>::failure(observation.status());
903 auto bodies = parseBodies2D(*object);
904 if (!bodies) return eve::Result<void>::failure(bodies.status());
905 auto gravityX = readFloat(*object, "gravityX");
906 if (!gravityX) return eve::Result<void>::failure(gravityX.status());
907 auto gravityY = readFloat(*object, "gravityY");
908 if (!gravityY) return eve::Result<void>::failure(gravityY.status());
909 auto meter = readFloat(*object, "meter");
910 if (!meter) return eve::Result<void>::failure(meter.status());
911 if (meter.value() <= 0.f)
913 eve::DiagnosticCode::InvalidArgument, "physics snapshot meter must be positive", "payload.meter"));
914
915 std::set<int> snapshotIds;
916 auto matched = matchBodyIds(bodies.value(), snapshotIds);
917 if (!matched) return matched;
918 auto handleRefresh = prepareRuntimeHandleRefresh();
919 if (!handleRefresh) return handleRefresh;
920 auto prepared = prepareWorld2D(bodies.value(), gravityX.value(), gravityY.value(), meter.value(),
921 observation.value().value, instanceId_);
922 if (!prepared) return eve::Result<void>::failure(prepared.status());
923 WorldSnapshotAccess::adopt(*this, *prepared.value());
924 refreshRuntimeHandlesAfterRestore();
927}
928
930 if (!isValid() || !simulation_)
932 eve::DiagnosticCode::PreconditionViolation, "Cannot snapshot a destroyed or uninitialized 3D physics world",
933 "physics.world3d.snapshot"));
934 auto schema = snapshotSchema(kWorld3DSchema);
935 if (!schema) return eve::Result<eve::SnapshotEnvelope>::failure(schema.status());
936 const auto observation = simulationObservation();
937 std::vector<Body3D*> bodies(bodies_.begin(), bodies_.end());
938 return eve::makeSnapshotEnvelope(std::string(kWorld3DType), std::move(schema).takeValue(),
939 eve::SchemaVersion(kSnapshotVersion), instanceId_,
940 eve::Revision(observation.stepCount), simulationTick(),
941 make3DPayload(*this, bodies, std::vector<Shape3D*>(shapes_.begin(), shapes_.end()),
942 std::vector<Joint3D*>(joints_.begin(), joints_.end())), hashProvider);
943}
944
946 const eve::SnapshotHashProvider& hashProvider) {
947 if (!isValid() || !simulation_)
949 eve::DiagnosticCode::PreconditionViolation, "Cannot restore a destroyed or uninitialized 3D physics world",
950 "physics.world3d.restore"));
951 auto verified = eve::verifySnapshotEnvelope(snapshotValue, hashProvider);
952 if (!verified) return verified;
953 auto envelope = checkEnvelope(snapshotValue, kWorld3DType, kWorld3DSchema);
954 if (!envelope) return envelope;
955 const bool legacyV1 = snapshotValue.schemaVersion.value() == 1;
956 if (!legacyV1 && (snapshotValue.instanceId.isNil() || snapshotValue.instanceId != instanceId_))
959 "3D physics snapshot belongs to a different world instance", "snapshot.instanceId"));
960 eve::Value migratedPayload = snapshotValue.payload;
961 if (legacyV1) {
962 auto* migrated = migratedPayload.getIf<eve::Value::Object>();
963 if (!migrated)
965 "v1 3D physics payload is malformed", "payload"));
966 migrated->emplace("shapes", shapesTopologyValue(std::vector<Shape3D*>(shapes_.begin(), shapes_.end())));
967 migrated->emplace("joints", jointsTopologyValue(std::vector<Joint3D*>(joints_.begin(), joints_.end())));
968 const eve::Value* bodyValues = field(*migrated, "bodies");
969 if (!bodyValues || bodyValues->arraySize() != bodies_.size())
972 "v1 snapshot requires topology-compatible live bodies", "payload.bodies"));
973 }
974 const auto* object = migratedPayload.getIf<eve::Value::Object>();
975 if (!object || !hasExactFields(*object, {"bodies", "gravityX", "gravityY", "gravityZ", "joints", "shapes", "tick", "revision",
976 "stepCount", "simulatedDurationNs", "lastDeltaSeconds"}))
978 eve::DiagnosticCode::ParseError, "3D physics snapshot payload has unknown or missing fields", "payload"));
979 auto observation = parseObservation(*object, snapshotValue);
980 if (!observation) return eve::Result<void>::failure(observation.status());
981 auto bodies = parseBodies3D(*object);
982 if (!bodies) return eve::Result<void>::failure(bodies.status());
983 auto gravityX = readFloat(*object, "gravityX");
984 if (!gravityX) return eve::Result<void>::failure(gravityX.status());
985 auto gravityY = readFloat(*object, "gravityY");
986 if (!gravityY) return eve::Result<void>::failure(gravityY.status());
987 auto gravityZ = readFloat(*object, "gravityZ");
988 if (!gravityZ) return eve::Result<void>::failure(gravityZ.status());
989 const eve::Value* snapshotShapes = field(*object, "shapes");
990 const eve::Value* snapshotJoints = field(*object, "joints");
991 if (!snapshotShapes || !snapshotShapes->isArray() || !snapshotJoints || !snapshotJoints->isArray())
993 eve::DiagnosticCode::ParseError, "3D physics snapshot topology fields must be arrays", "payload.shapes"));
994 std::set<int> snapshotIds;
995 auto matched = matchBodyIds(bodies.value(), snapshotIds);
996 if (!matched) return matched;
997 auto handleRefresh = prepareRuntimeHandleRefresh();
998 if (!handleRefresh) return handleRefresh;
999 auto prepared = prepareWorld3D(bodies.value(), *snapshotShapes->getIf<eve::Value::Array>(),
1000 *snapshotJoints->getIf<eve::Value::Array>(),
1001 gravityX.value(), gravityY.value(), gravityZ.value(),
1002 observation.value().value, instanceId_);
1003 if (!prepared) return eve::Result<void>::failure(prepared.status());
1004 auto restoredObservation = simulation_->restoreObservation(observation.value().value);
1005 if (!restoredObservation) return restoredObservation;
1006 WorldSnapshotAccess::adopt(*this, *prepared.value());
1007 refreshRuntimeHandlesAfterRestore();
1010}
1011
1012eve::Result<void> World::prepareRuntimeHandleRefresh() const {
1013 const auto available =
1014 static_cast<std::uint64_t>(PhysicsBodyHandle::invalidIndex) - nextBodyHandleIndex_;
1015 if (bodies_.size() > available)
1017 eve::DiagnosticCode::InvariantViolation, "2D physics body handle space cannot represent restored links",
1018 "physics.world.restore.handles"));
1020}
1021
1022void World::refreshRuntimeHandlesAfterRestore() {
1023 for (Body* body : bodies_)
1024 if (body && body->isValid()) body->runtimeHandle_ = nextBodyRuntimeHandle();
1025}
1026
1027eve::Result<void> World3D::prepareRuntimeHandleRefresh() const {
1028 const auto bodyAvailable = static_cast<std::uint64_t>(PhysicsBodyHandle::invalidIndex) - nextBodyHandleIndex_;
1029 const auto shapeAvailable = static_cast<std::uint64_t>(PhysicsShapeHandle::invalidIndex) - nextShapeHandleIndex_;
1030 const auto jointAvailable = static_cast<std::uint64_t>(PhysicsJointHandle::invalidIndex) - nextJointHandleIndex_;
1031 if (bodies_.size() > bodyAvailable || shapes_.size() > shapeAvailable || joints_.size() > jointAvailable)
1033 eve::DiagnosticCode::InvariantViolation, "3D physics handle space cannot represent restored links",
1034 "physics.world3d.restore.handles"));
1036}
1037
1038void World3D::refreshRuntimeHandlesAfterRestore() {
1039 shapeHandles_.clear();
1040 shapeRawHandles_.clear();
1041 jointHandles_.clear();
1042 for (Body3D* body : bodies_)
1043 if (body && body->isValid()) body->runtimeHandle_ = nextBodyRuntimeHandle();
1044 for (Shape3D* shape : shapes_) {
1045 if (!shape || !shape->isValid()) continue;
1046 shape->runtimeHandle_ = nextShapeRuntimeHandle();
1047 shapeHandles_[shape->runtimeHandle_] = shape;
1048 shapeRawHandles_[b3StoreShapeId(shape->raw())] = shape->runtimeHandle_;
1049 }
1050 for (Joint3D* joint : joints_) {
1051 if (!joint || !joint->isValid()) continue;
1052 joint->runtimeHandle_ = nextJointRuntimeHandle();
1053 jointHandles_[joint->runtimeHandle_] = joint;
1054 }
1055}
1056
1057} // namespace eve::physics
double value
bool & active
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
float duration
std::string output
float cx
Definition CardTypes.cpp:33
float length
Definition CaveMesh.cpp:94
std::map< std::string, Var > values
ShaderImageInput shape
glm::uvec4 ids
std::vector< std::uint32_t > indices
HexVec3 left
HexVec3 right
std::int32_t c
std::string text
TokenKind kind
std::string name
bool valid
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
std::string error
Definition Package.cpp:60
float f
World3D * world
std::uint64_t revision
std::vector< Point > vertices
float radius
std::string id
Definition PlayHost.cpp:108
float d
const RoadEdge * edge
double number
bool found
int created
Backend-neutral, observable fixed-step contract for physics domains.
SimulationTick tick
Json object
float size
Definition TreeMesh.cpp:156
std::string body
uint32_t index
const UnitySourceAsset & source
std::vector< int > edges
#define EV_READ_BODY3D_BOOL(name)
eve::Value fixtures
float angularVelocity
float vz
float wz
float wx
float vy
float qy
bool awake
float vx
#define EV_READ_BODY3D_FLOAT(name)
float qx
bool fixedRotation
float qw
float angle
float qz
float wy
bool bullet
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
static constexpr Duration fromNanoseconds(std::int64_t nanoseconds) noexcept
Construct an exact duration from nanoseconds.
Definition Time.h:55
const std::string & format() const noexcept
Returns the canonical namespace:name representation.
Definition Identity.h:436
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
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
static constexpr index_type invalidIndex
Reserved index value shared by all invalid handles.
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
std::map< std::string, Value > Object
Definition Value.h:34
bool isArray() const noexcept
Return true when this value is an array.
Definition Value.h:97
std::vector< Value > Array
Definition Value.h:33
std::size_t arraySize() const
Return the number of array elements.
Definition Value.cpp:100
const T * getIf() const noexcept
Return a typed pointer, or nullptr when the kind differs.
Definition Value.h:180
Id128 public API.
Definition Identity.h:112
constexpr bool isNil() const noexcept
Returns whether this value is the all-zero nil ID.
Definition Identity.h:176
constexpr std::uint64_t value() const noexcept
Returns the underlying value at an explicit protocol boundary.
3D rigid body (Box3D) in meter-space coordinates (+Y up by convention). Owned by a World3D; create pr...
Definition Body3D.h:24
2D rigid body (Box2D) in pixel-space coordinates. Owned by a World; create shapes with newRectangleFi...
Definition Body.h:21
Script-facing Box3D joint owned by a World3D.
Definition Joint3D.h:17
3D shape (box/sphere/capsule) attached to a Body3D with material settings. Created via Body3D::new*Sh...
Definition Shape3D.h:25
Box3D rigid-body world. Script coordinates are meters (Box3D native), unlike 2D World which uses pixe...
Definition World3D.h:63
PhysicsJointHandle nextJointRuntimeHandle()
Internal: next generation-qualified joint handle.
Definition World3D.cpp:896
SimulationObservation simulationObservation() const noexcept
Snapshot of completed backend steps and logical simulation time.
Definition World3D.cpp:761
eve::Result< eve::SnapshotEnvelope > snapshot(const eve::SnapshotHashProvider &hashProvider) const
Captures a versioned, integrity-checked 3D world snapshot.
float getGravityX() const
Definition World3D.cpp:780
eve::Result< void > restore(const eve::SnapshotEnvelope &snapshot, const eve::SnapshotHashProvider &hashProvider)
Restores a verified snapshot without exposing partial state.
float getGravityY() const
Definition World3D.cpp:785
PhysicsBodyHandle nextBodyRuntimeHandle()
Internal: next generation-qualified body handle.
Definition World3D.cpp:884
friend class Body3D
Definition World3D.h:1004
bool isValid() const
True while the underlying Box3D world is alive.
Definition World3D.cpp:430
eve::SimulationTick simulationTick() const noexcept
Current deterministic tick; save data should persist this value.
Definition World3D.h:139
friend class Joint3D
Definition World3D.h:1005
float getGravityZ() const
Definition World3D.cpp:790
void clearContactEvents()
Clears all contact and trigger buffers before the next step.
Definition World3D.cpp:1086
PhysicsShapeHandle nextShapeRuntimeHandle()
Internal: next generation-qualified shape handle.
Definition World3D.cpp:890
friend class Shape3D
Definition World3D.h:1007
Box2D world wrapper (2D physics) with pixel-space coordinates. Handles stepping, gravity,...
Definition World.h:41
SimulationObservation simulationObservation() const noexcept
Snapshot of completed backend steps and logical simulation time.
Definition World.cpp:385
eve::SimulationTick simulationTick() const noexcept
Current deterministic tick; save data should persist this value.
Definition World.h:109
friend class Body
Definition World.h:412
eve::Result< void > restore(const eve::SnapshotEnvelope &snapshot, const eve::SnapshotHashProvider &hashProvider)
Restores a verified snapshot without exposing partial state.
PhysicsBodyHandle nextBodyRuntimeHandle()
Definition World.cpp:425
bool isValid() const
True while the underlying Box2D world is alive.
Definition World.h:306
void clearContactEvents()
Clears collected begin/end contact and impact event buffers.
Definition World.cpp:643
eve::Result< eve::SnapshotEnvelope > snapshot(const eve::SnapshotHashProvider &hashProvider) const
Captures a versioned, integrity-checked world snapshot.
Result< float > readFloat(const Accessor &accessor, std::uint32_t element, std::uint32_t component)
Read one bounded finite FLOAT component.
bool readString(const eve::Value::Object &object, const char *name, std::string &output)
Optional physics backend for vehicle mobility and body attach.
Definition Climbing.h:36
const EditorValue * field(const EditorValue &value, const char *name)
bool readNumber(const EditorValue &value, const char *name, float &output)
int axis(int64_t a, size_t rank)
Axis.
detail::StrongUint64< detail::SimulationTickTag > SimulationTick
Deterministic simulation time step; it is not wall-clock time.
Definition Time.h:31
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
PersistentId instanceId
Definition Snapshot.h:50
SimulationTick tick
Definition Snapshot.h:52
Observable backend progress shared by CPU and accelerator providers.
eve::SimulationTick lastTick
Tick of the most recently completed step.
static void setJointId(Joint3D &joint, int id)
static std::unique_ptr< World3D > makeDetachedWorld3D(float gx, float gy, float gz, eve::PersistentId instanceId)
static void setShapeId(Shape3D &shape, int id)
static void setBodyId(Body3D &body, int id)
static eve::Result< void > finishPrepared(World3D &world, int nextBodyId, int nextShapeId, int nextJointId, const SimulationObservation &observation)
static eve::Result< void > finishPrepared(World &world, int nextId, const SimulationObservation &observation)
static eve::Value shapeSource(const Shape3D &shape)
static void adopt(World3D &live, World3D &prepared)
static void adopt(World &live, World &prepared)
static void setBodyId(Body &body, int id)