diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index a4366e4..c120317 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -15,13 +15,13 @@ jobs: - name: Checkout aephysics uses: actions/checkout@v4 - # Pin: 0.794.0, the latest release (f32x8 arrived in 0.788.0, aether#2428), - # which the wide contact solver is written in. A reproducible bootstrap + # Pin: 0.796.0, the latest release (f32x8 arrived in 0.788.0, aether#2428, + # which the wide contact solver is written in). A reproducible bootstrap # version: the floor this code needs, verified on Linux by this job. - name: Install the released Aether toolchain and aeb run: | curl -fsSL https://raw.githubusercontent.com/aether-lang-dev/aeb/main/get.sh \ - | AE_PIN=0.795.0 AEB_REF=v0.325 sh + | AE_PIN=0.796.0 AEB_REF=v0.325 sh echo "$HOME/.local/bin" >> "$GITHUB_PATH" - name: Run the tests with ae @@ -54,7 +54,7 @@ jobs: - name: Install the released Aether toolchain and aeb run: | curl -fsSL https://raw.githubusercontent.com/aether-lang-dev/aeb/main/get.sh \ - | AE_PIN=0.795.0 AEB_REF=v0.325 sh + | AE_PIN=0.796.0 AEB_REF=v0.325 sh echo "$HOME/.local/bin" >> "$GITHUB_PATH" - name: Run the tests with ae diff --git a/README.md b/README.md index 18ba4d5..7879bba 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,8 @@ so a test written against the reference reads the same here. | `aephysics.joint_solver` | the seven joints solved: each kind's prepare (frames relative to the centres of mass, effective masses, spring softness), warm start and solve (rigid or soft constraints, speculative limits, springs, motors), the kinds' accessors (limits, springs, motors, current angles and translations, forces and torques), joint.c's dispatch with the constraint hertz clamp; beyond the reference, a spherical joint's cone may be centred off frame A's z and may be an ellipse of two half-angles (the circle stays the default, bit for bit) | `test_joint_solver.ae` (181 checks: test_joint.c's accessors on every kind, each kind solved by hand) | [ours alone](bench/RESULTS.md#joint_solver) | | `aephysics.solver` | the Soft Step over the workers, the reference's stages: the work in blocks (the bodies', each colour's joints, wide and scalar constraints) that workers claim by compare-and-swap, the stages published as sync bits by worker 0 -- the constraints prepared, per sub-step the velocities integrated (gravity, damping, the gyroscopic torque), warm start, solve, positions, relax, colour by colour with the overflow first on worker 0; restitution, the impulses stored; the bodies finalised in parallel (sleep velocities, move events, fast bodies swept for the time of impact, bounds and proxies), the joint and hit events, the trees refit, bullets, the sensors' hits, islands put to sleep | `test_solver.ae` (45 checks: free fall against the closed form, resting and sleeping, a stack, a bounce and its hit event, a joint event, a pendulum, a fast sphere stopped by a thin wall, a bullet, a sensor swept, two worlds bit for bit alike) | [1.2-1.3x the reference's step, stage by stage](bench/RESULTS.md#stage-by-stage) | | `aephysics.physics_world` | the world's face: the step (pairs, narrow phase, solve, sensors, events), the events read back, the settings and counters, queries over every shape (overlap of a box or a proxy, the mover's planes, ray, shape and mover casts, the closest ray hit) and against one body at a transform of the caller's, explosions | `test_physics_world.ae` (259 checks: test_world.c whole, among it the hull database (equal hulls one world-owned copy, a caller's hull freed at once, transformed hulls baked once, every holder's release), empty and recycled worlds, every setting, the worker count set between steps, contact, hit, move and sensor events, a compound child's hit events and materials, the overflow colour pile, the explosion near and far, the bullet and sleep flags, the compound count, an enlarged proxy destroyed; test_body_query.c whole: casts, overlaps of hull proxies and turned compounds, mover planes, and the mover's time of impact against 2,000 random sweeps; test_broad_phase.c's static rebuild and its stress test, a swarm through every proxy change with the pair set checked by brute force every step; test_id.c; the world queries; a wave pile stepping alike twice) | [the reference's own benchmark scenes with the same checksums: 1.2x its AVX2 build on the large pyramid, 1.15x on the many pyramids, 1.05x on the joint grid, ours at eight lanes in AVX2 with float records (#121)](bench/RESULTS.md#the-collide-pass-as-the-reference-keeps-it-2026-10-09-121); [the rest of its suite, nine scenes and the SAT runs, at 1.04-1.21x](bench/RESULTS.md#suite-the-references-benchmark-suite) | -| `aephysics.snapshot` | a world as bytes and back (the reference's world_snapshot.c): `save_world` and `load_world` keep everything a step reads (the settings, the id pools, the solver sets, bodies, shapes and their geometry once each, contacts with their manifolds and mesh caches, joints, sensors, islands, the trees and pair set, the graph's colours), so a loaded world steps on as the one it was taken from, bit for bit; a snapshot of another layout or any damage to its bytes loads no world | done, `test_snapshot.ae` (50 checks: a world of every kind of thing loaded at four moments and stepped 150 steps beside its original, the same hash and events every step; a loaded world saves to the same bytes; one worker's world loaded with four steps alike; every truncation and corruption refused, nothing leaked) | +| `aephysics.snapshot` | a world as bytes and back (the reference's world_snapshot.c): `save_world` and `load_world` keep everything a step reads (the settings, the id pools, the solver sets, bodies, shapes and their geometry once each, contacts with their manifolds and mesh caches, joints, sensors, islands, the trees and pair set, the graph's colours), so a loaded world steps on as the one it was taken from, bit for bit; `restore_world` loads one into an existing world in place, its id, workers and callbacks kept (what a replay rewinds with); a snapshot of another layout or any damage to its bytes loads no world | done, `test_snapshot.ae` (63 checks: a world of every kind of thing loaded at four moments and stepped 150 steps beside its original, the same hash and events every step; a loaded world saves to the same bytes; one worker's world loaded with four steps alike; a restore in place stepping alike and saving to the same bytes, a refused restore leaving the world empty and whole; every truncation and corruption refused, nothing leaked) | +| `aephysics.recording` | recording and replay (the reference's recording.c and recording_replay.c): a world records every call that changes it, 160 kinds, field by field, a create's record with the id it made, a state hash after every step and the geometry the calls name once each; a player replays it into a world of its own, restored in place so its id stays, checking every step's hash and every created id, with keyframes under a memory budget for seeking, bodies in the order they were made, a pause before a step that makes bodies, and the recording's info; records it does not know are skipped, as a newer minor version may add some | `test_recording.ae` (116 checks: the snapshot test's scene recorded from an empty world and from mid stream with the host changing it between steps, played on one worker and several, every frame's hash as recorded; every one of the 160 calls recorded and replayed (the reference's AllOps); geometry kept once from two copies; seeking back and forth with and without keyframes and under a tight budget; a file round trip; every truncation, a skipped call, a changed id, a short record and a changed hash each caught, nothing leaked). Not yet: the queries and the name cache (#149) | | `aephysics.human` | the ragdoll: twelve capsule bones on spherical joints with cone and twist limits and revolute joints with angle limits -- a person's ranges by default (shoulders that reach overhead on a cone centred off the rest, hips on an elliptical cone through 125 degrees ahead, 25 back, 45 out and 30 across, knees and elbows that never pass straight), the reference's tight ones on request -- soles that grip, and on request (`Human.ankles`, past the reference) the feet as bones of their own on ankles, an elliptical cone through 50 degrees down, 35 up and 15 aside with 15 of roll, so a crouched figure's soles lie flat, a spring on every joint toward the reference pose, a motor whose torque limit is joint friction, filter joints for the limbs that clash; the align spring, kinematic anchors through motor or parallel joints and their targets driven over a step (the pose drive of an active ragdoll), velocity, kicks, bullets | `test_human.ae` (115 checks: the figure's shape, a fall to rest in one piece, the setters, a kick, standing under the align spring, the pose held on anchors, the anchors driven two metres and the figure walked behind them, the same drop twice bit for bit, a fitted shape, each arm reaching flexion 170, abduction 160 and extension 45 degrees standing and prone, the hips' cone through a person's four ranges and short of a split, the hinges stopping at straight and folding to their range, the soles' grip, the ankles' four ranges measured through the joint and the soles flat under shins tipped 25 degrees over them) | [the reference's rain benchmark, 300 ragdolls over 400 steps, at 0.9x](bench/RESULTS.md#human) | | `aephysics` | the public API | | diff --git a/aephysics/dynamics/module.ae b/aephysics/dynamics/module.ae index b0a0e92..b61a506 100644 --- a/aephysics/dynamics/module.ae +++ b/aephysics/dynamics/module.ae @@ -89,6 +89,9 @@ exports ( body_set_motion_locks, body_get_motion_locks, body_get_flags, create_sphere_shape, create_capsule_shape, create_hull_shape, create_transformed_hull_shape, create_mesh_shape, create_height_field_shape, HullEntry, world_hull_count, add_hull_to_database, allocate_manifolds, OwnedGeometry, adopt_geometry, world_at, destroy_solver_set, + Recording, RecordedGeometry, RECORDING_MAGIC, RECORDING_MAJOR_VERSION, RECORDING_MINOR_VERSION, RECORDING_HEADER_BYTES, recording_on, rec_begin, rec_end, rec_int, rec_long, + rec_float, rec_bool, rec_vec3, rec_quat, rec_transform, rec_id, rec_string, rec_raw, rec_geometry, finish_recording, recorded_geometry_at, + OP_STATE_HASH, OP_BOUNDS, OP_END, OP_CREATE_BODY, OP_DESTROY_BODY, OP_BODY_SET_TRANSFORM, OP_BODY_SET_LINEAR_VELOCITY, OP_BODY_SET_ANGULAR_VELOCITY, OP_BODY_SET_TARGET_TRANSFORM, OP_BODY_APPLY_FORCE, OP_BODY_APPLY_FORCE_TO_CENTER, OP_BODY_APPLY_TORQUE, OP_BODY_APPLY_LINEAR_IMPULSE, OP_BODY_APPLY_LINEAR_IMPULSE_TO_CENTER, OP_BODY_APPLY_ANGULAR_IMPULSE, OP_BODY_SET_BULLET, OP_BODY_SET_MASS_DATA, OP_BODY_APPLY_MASS_FROM_SHAPES, OP_BODY_SET_LINEAR_DAMPING, OP_BODY_SET_ANGULAR_DAMPING, OP_BODY_SET_GRAVITY_SCALE, OP_BODY_SET_AWAKE, OP_BODY_ENABLE_SLEEP, OP_BODY_SET_SLEEP_THRESHOLD, OP_BODY_SET_MOTION_LOCKS, OP_CREATE_SPHERE_SHAPE, OP_CREATE_CAPSULE_SHAPE, OP_CREATE_HULL_SHAPE, OP_CREATE_TRANSFORMED_HULL_SHAPE, OP_CREATE_MESH_SHAPE, OP_CREATE_HEIGHT_FIELD_SHAPE, OP_CREATE_COMPOUND_SHAPE, OP_DESTROY_SHAPE, OP_SHAPE_SET_DENSITY, OP_SHAPE_SET_FILTER, OP_SHAPE_SET_SURFACE_MATERIAL, OP_SHAPE_SET_SPHERE, OP_SHAPE_SET_CAPSULE, OP_SHAPE_SET_HULL, OP_SHAPE_SET_MESH, OP_SHAPE_SET_FRICTION, OP_SHAPE_SET_RESTITUTION, OP_SHAPE_SET_MESH_MATERIAL, OP_SHAPE_ENABLE_PRE_SOLVE_EVENTS, OP_BODY_ENABLE_HIT_EVENTS, OP_BODY_ALLOW_FAST_ROTATION, OP_BODY_ENABLE_CONTACT_RECYCLING, OP_BODY_SET_SAFETY_FACTOR, OP_SHAPE_APPLY_WIND, OP_BODY_SET_NAME, OP_SHAPE_SET_NAME, OP_CREATE_DISTANCE_JOINT, OP_CREATE_MOTOR_JOINT, OP_CREATE_FILTER_JOINT, OP_CREATE_PARALLEL_JOINT, OP_CREATE_PRISMATIC_JOINT, OP_CREATE_REVOLUTE_JOINT, OP_CREATE_SPHERICAL_JOINT, OP_CREATE_WELD_JOINT, OP_CREATE_WHEEL_JOINT, OP_DESTROY_JOINT, OP_JOINT_SET_LOCAL_FRAME_A, OP_JOINT_SET_LOCAL_FRAME_B, OP_JOINT_SET_COLLIDE_CONNECTED, OP_JOINT_WAKE_BODIES, OP_JOINT_SET_CONSTRAINT_TUNING, OP_JOINT_SET_FORCE_THRESHOLD, OP_JOINT_SET_TORQUE_THRESHOLD, OP_BODY_SET_TYPE, OP_BODY_DISABLE, OP_BODY_ENABLE, OP_SHAPE_ENABLE_SENSOR_EVENTS, OP_SHAPE_ENABLE_CONTACT_EVENTS, OP_SHAPE_ENABLE_HIT_EVENTS, OP_STEP, OP_ENABLE_SLEEPING, OP_ENABLE_WARM_STARTING, OP_ENABLE_CONTINUOUS, OP_ENABLE_SPECULATIVE, OP_SET_RESTITUTION_THRESHOLD, OP_SET_RESTITUTION_ITERATIONS, OP_ENABLE_RESTITUTION_PROPAGATION, OP_ENABLE_KINEMATIC_STATIC_SOFTNESS, OP_SET_HIT_EVENT_THRESHOLD, OP_SET_CONTACT_TUNING, OP_SET_CONTACT_RECYCLE_DISTANCE, OP_SET_MAXIMUM_LINEAR_SPEED, OP_SET_GRAVITY, OP_REBUILD_STATIC_TREE, OP_EXPLODE, OP_DISTANCE_JOINT_SET_LENGTH, OP_DISTANCE_JOINT_ENABLE_LIMIT, OP_DISTANCE_JOINT_SET_LENGTH_RANGE, OP_DISTANCE_JOINT_ENABLE_SPRING, OP_DISTANCE_JOINT_SET_SPRING_FORCE_RANGE, OP_DISTANCE_JOINT_SET_SPRING_HERTZ, OP_DISTANCE_JOINT_SET_SPRING_DAMPING_RATIO, OP_DISTANCE_JOINT_ENABLE_MOTOR, OP_DISTANCE_JOINT_SET_MOTOR_SPEED, OP_DISTANCE_JOINT_SET_MAX_MOTOR_FORCE, OP_MOTOR_JOINT_SET_LINEAR_VELOCITY, OP_MOTOR_JOINT_SET_ANGULAR_VELOCITY, OP_MOTOR_JOINT_SET_MAX_VELOCITY_TORQUE, OP_MOTOR_JOINT_SET_MAX_VELOCITY_FORCE, OP_MOTOR_JOINT_SET_LINEAR_HERTZ, OP_MOTOR_JOINT_SET_LINEAR_DAMPING_RATIO, OP_MOTOR_JOINT_SET_ANGULAR_HERTZ, OP_MOTOR_JOINT_SET_ANGULAR_DAMPING_RATIO, OP_MOTOR_JOINT_SET_MAX_SPRING_FORCE, OP_MOTOR_JOINT_SET_MAX_SPRING_TORQUE, OP_PARALLEL_JOINT_SET_SPRING_HERTZ, OP_PARALLEL_JOINT_SET_SPRING_DAMPING_RATIO, OP_PARALLEL_JOINT_SET_MAX_TORQUE, OP_PRISMATIC_JOINT_ENABLE_SPRING, OP_PRISMATIC_JOINT_SET_SPRING_HERTZ, OP_PRISMATIC_JOINT_SET_SPRING_DAMPING_RATIO, OP_PRISMATIC_JOINT_SET_TARGET_TRANSLATION, OP_PRISMATIC_JOINT_ENABLE_LIMIT, OP_PRISMATIC_JOINT_SET_LIMITS, OP_PRISMATIC_JOINT_ENABLE_MOTOR, OP_PRISMATIC_JOINT_SET_MOTOR_SPEED, OP_PRISMATIC_JOINT_SET_MAX_MOTOR_FORCE, OP_REVOLUTE_JOINT_ENABLE_LIMIT, OP_REVOLUTE_JOINT_SET_LIMITS, OP_REVOLUTE_JOINT_ENABLE_SPRING, OP_REVOLUTE_JOINT_SET_TARGET_ANGLE, OP_REVOLUTE_JOINT_SET_SPRING_HERTZ, OP_REVOLUTE_JOINT_SET_SPRING_DAMPING_RATIO, OP_REVOLUTE_JOINT_ENABLE_MOTOR, OP_REVOLUTE_JOINT_SET_MOTOR_SPEED, OP_REVOLUTE_JOINT_SET_MAX_MOTOR_TORQUE, OP_SPHERICAL_JOINT_ENABLE_CONE_LIMIT, OP_SPHERICAL_JOINT_SET_CONE_LIMIT, OP_SPHERICAL_JOINT_SET_CONE_AXIS, OP_SPHERICAL_JOINT_SET_CONE_ELLIPSE, OP_SPHERICAL_JOINT_ENABLE_TWIST_LIMIT, OP_SPHERICAL_JOINT_SET_TWIST_LIMITS, OP_SPHERICAL_JOINT_ENABLE_SPRING, OP_SPHERICAL_JOINT_SET_TARGET_ROTATION, OP_SPHERICAL_JOINT_SET_SPRING_HERTZ, OP_SPHERICAL_JOINT_SET_SPRING_DAMPING_RATIO, OP_SPHERICAL_JOINT_ENABLE_MOTOR, OP_SPHERICAL_JOINT_SET_MOTOR_VELOCITY, OP_SPHERICAL_JOINT_SET_MAX_MOTOR_TORQUE, OP_WELD_JOINT_SET_LINEAR_HERTZ, OP_WELD_JOINT_SET_LINEAR_DAMPING_RATIO, OP_WELD_JOINT_SET_ANGULAR_HERTZ, OP_WELD_JOINT_SET_ANGULAR_DAMPING_RATIO, OP_WHEEL_JOINT_ENABLE_SUSPENSION, OP_WHEEL_JOINT_SET_SUSPENSION_HERTZ, OP_WHEEL_JOINT_SET_SUSPENSION_DAMPING_RATIO, OP_WHEEL_JOINT_ENABLE_SUSPENSION_LIMIT, OP_WHEEL_JOINT_SET_SUSPENSION_LIMITS, OP_WHEEL_JOINT_ENABLE_SPIN_MOTOR, OP_WHEEL_JOINT_SET_SPIN_MOTOR_SPEED, OP_WHEEL_JOINT_SET_MAX_SPIN_TORQUE, OP_WHEEL_JOINT_ENABLE_STEERING, OP_WHEEL_JOINT_SET_STEERING_HERTZ, OP_WHEEL_JOINT_SET_STEERING_DAMPING_RATIO, OP_WHEEL_JOINT_SET_MAX_STEERING_TORQUE, OP_WHEEL_JOINT_ENABLE_STEERING_LIMIT, OP_WHEEL_JOINT_SET_STEERING_LIMITS, OP_WHEEL_JOINT_SET_TARGET_STEERING_ANGLE, create_compound_shape, destroy_shape, shape_is_valid, get_shape_full_id, get_shape, shape_get_body, shape_get_aabb, shape_get_type, shape_get_density, shape_set_density, shape_get_filter, shape_set_filter, shape_get_surface_material, shape_set_surface_material, shape_get_material_count, shape_is_sensor, @@ -149,11 +152,13 @@ exports ( world_body_move_event_count, world_body_move_event, world_contact_hit_event_count, world_contact_hit_event, world_joint_event_count, world_joint_event, world_set_pre_solve, world_clear_pre_solve, shape_user_material_id, make_relative_sweep, body_move_event_at, - TaskContext, task_context, task_context_at, task_sensor_hit_at, enter_world, leave_world + TaskContext, task_context, task_context_at, task_sensor_hit_at, enter_world, leave_world, + reset_world ) extern memset(block: ptr, value: int, size: int) -> ptr extern memcpy(dst: ptr, src: ptr, size: int) -> ptr +extern memcmp(a: ptr, b: ptr, size: int) -> int extern sqrt(x: float) -> float const NULL_INDEX = 0 - 1 @@ -1292,6 +1297,7 @@ struct World { enable_speculative: bool in_use: bool user_data: ptr + recording: ptr // *Recording while the world records (aephysics.recording), else null } var g_worlds: ptr = null // World[MAX_WORLDS] @@ -1430,10 +1436,29 @@ create_world(def: *WorldDef) -> WorldId { w.generation = generation w.in_use = true w.stack = basics.stack_create(2048) - w.manifold_allocators = basics.buffer_create(sizeof(BlockAllocator), 0) - w.manifold_lock = 0 reserve_scratch() create_workers(w, def.scheduler, def.worker_count) + w.friction_callback = def.friction_callback + w.restitution_callback = def.restitution_callback + w.user_data = def.user_data + init_world_contents(w, def) + return WorldId { index1: index + 1, generation: generation } +} + +// Everything a world holds (its bodies, shapes, joints, contacts, sets, +// islands, geometry and events), made fresh as `def` sizes and sets it; +// what the host wires in (workers, callbacks, filters, user data) is not +// touched. +init_world_contents(w: *World, def: *WorldDef) { + memset((&w.profile) as ptr, 0, sizeof(Profile)) + w.manifold_allocators = basics.buffer_create(sizeof(BlockAllocator), 0) + w.manifold_lock = 0 + w.sat_call_count = 0 + w.sat_cache_hit_count = 0 + w.recycled_contact_count = 0 + w.compound_shape_count = 0 + w.inv_h = 0.0 + w.inv_dt = 0.0 w.contact_state = basics.bitset_create(64) w.contact_begin_events = basics.buffer_create(sizeof(ContactBeginTouchEvent), 4) w.contact_end_events0 = basics.buffer_create(sizeof(ContactEndTouchEvent), 4) @@ -1464,8 +1489,6 @@ create_world(def: *WorldDef) -> WorldId { w.sensor_end_events0 = basics.buffer_create(sizeof(SensorEndTouchEvent), 4) w.sensor_end_events1 = basics.buffer_create(sizeof(SensorEndTouchEvent), 4) w.sensor_event_bits = basics.bitset_create(64) - w.has_custom_filter = false - w.has_pre_solve = false w.body_move_events = basics.buffer_create(sizeof(BodyMoveEvent), math.max_int(16, def.dynamic_body_count)) w.contact_hit_events = basics.buffer_create(sizeof(ContactHitEvent), 4) w.joint_events = basics.buffer_create(sizeof(JointEvent), 4) @@ -1493,21 +1516,43 @@ create_world(def: *WorldDef) -> WorldId { w.contact_hertz = def.contact_hertz w.contact_damping_ratio = def.contact_damping_ratio w.contact_recycle_distance = CONTACT_RECYCLE_DISTANCE - w.friction_callback = def.friction_callback - w.restitution_callback = def.restitution_callback w.enable_sleep = def.enable_sleep w.locked = false w.enable_warm_starting = true w.enable_continuous = def.enable_continuous w.enable_speculative = true - w.user_data = def.user_data - return WorldId { index1: index + 1, generation: generation } } // Everything the world holds is freed; ids into it die with its generation. destroy_world(id: WorldId) { if world_is_valid(id) == false { return } w = get_world_from_id(id) + finish_recording(w) + free_world_contents(w) + destroy_workers(w) + basics.stack_destroy(&w.stack) + generation = w.generation + memset(w as ptr, 0, sizeof(World)) + w.generation = generation + 1 + w.in_use = false +} + +// The world emptied in place, as a new world from `def` is (the +// reference's deserialize into a shell): its id, workers, callbacks, +// filters and user data stay, so ids handed out before stay this world's. +// False for a world that is not one, is mid-step or records (the +// recording could not replay what it did not see). +reset_world(id: WorldId, def: *WorldDef) -> bool { + if world_is_valid(id) == false || valid_world_def(def) == false { return false } + w = get_world_from_id(id) + if w.locked || w.recording != null { return false } + free_world_contents(w) + init_world_contents(w, def) + return true +} + +// What init_world_contents made, freed. +free_world_contents(w: *World) { // The shapes' materials and names, the bodies' names, the sensors' // lists and the contacts' caches. i = 0 @@ -1615,16 +1660,10 @@ destroy_world(id: WorldId) { basics.idpool_destroy(&w.island_id_pool) broad_phase.destroy_broad_phase(&w.broad_phase) destroy_graph(&w.constraint_graph) - destroy_workers(w) basics.bitset_destroy(&w.contact_state) basics.buffer_destroy(&w.contact_begin_events) basics.buffer_destroy(&w.contact_end_events0) basics.buffer_destroy(&w.contact_end_events1) - basics.stack_destroy(&w.stack) - generation = w.generation - memset(w as ptr, 0, sizeof(World)) - w.generation = generation + 1 - w.in_use = false } world_body_count(w: *World) -> int { return basics.idpool_count(&w.body_id_pool) } @@ -1859,6 +1898,17 @@ valid_body_def(def: *BodyDef) -> bool { } create_body(world_id: WorldId, def: *BodyDef) -> BodyId { + if recording_on(world_id.index1 - 1) == false { return create_body_internal(world_id, def) } + recorder = rec_begin(world_id.index1 - 1, OP_CREATE_BODY) + rec_body_def(recorder, def) + made = create_body_internal(world_id, def) + rec_id(recorder, made.index1, made.generation) + rec_end(recorder) + return made +} + +// create_body's work, unrecorded. +create_body_internal(world_id: WorldId, def: *BodyDef) -> BodyId { if world_is_valid(world_id) == false { return null_body_id() } if valid_body_def(def) == false { return null_body_id() } w = get_world_from_id(world_id) @@ -1964,6 +2014,7 @@ wake_body(w: *World, body: *Body) -> bool { // The body's joints and contacts, its shapes (their sensors and // proxies), its island, its sim and state, then its id (the generation stays). destroy_body(id: BodyId) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_DESTROY_BODY); rec_id(recorder, id.index1, id.generation); rec_end(recorder) } if body_is_valid(id) == false { return } w = world_at(id.world0) if w.locked { return } @@ -2172,6 +2223,19 @@ valid_sphere(s: Sphere) -> bool { return math.is_valid_vec3(s.center) && valid_n valid_capsule(c: Capsule) -> bool { return math.is_valid_vec3(c.center1) && math.is_valid_vec3(c.center2) && valid_nonnegative(c.radius) } create_sphere_shape(body_id: BodyId, def: *ShapeDef, s: Sphere) -> ShapeId { + if recording_on(body_id.world0) == false { return create_sphere_shape_internal(body_id, def, s) } + recorder = rec_begin(body_id.world0, OP_CREATE_SPHERE_SHAPE) + rec_id(recorder, body_id.index1, body_id.generation) + rec_shape_def(recorder, def) + rec_sphere_value(recorder, s) + made = create_sphere_shape_internal(body_id, def, s) + rec_id(recorder, made.index1, made.generation) + rec_end(recorder) + return made +} + +// create_sphere_shape's work, unrecorded. +create_sphere_shape_internal(body_id: BodyId, def: *ShapeDef, s: Sphere) -> ShapeId { if valid_sphere(s) == false { return null_shape_id() } return create_shape(body_id, def, shape.sphere_shape(s, def.density)) } @@ -2180,6 +2244,19 @@ create_sphere_shape(body_id: BodyId, def: *ShapeDef, s: Sphere) -> ShapeId { // their midpoint, as the reference's b3CreateCapsuleShape does: its segment // has no direction to collide along. create_capsule_shape(body_id: BodyId, def: *ShapeDef, c: Capsule) -> ShapeId { + if recording_on(body_id.world0) == false { return create_capsule_shape_internal(body_id, def, c) } + recorder = rec_begin(body_id.world0, OP_CREATE_CAPSULE_SHAPE) + rec_id(recorder, body_id.index1, body_id.generation) + rec_shape_def(recorder, def) + rec_capsule_value(recorder, c) + made = create_capsule_shape_internal(body_id, def, c) + rec_id(recorder, made.index1, made.generation) + rec_end(recorder) + return made +} + +// create_capsule_shape's work, unrecorded. +create_capsule_shape_internal(body_id: BodyId, def: *ShapeDef, c: Capsule) -> ShapeId { if valid_capsule(c) == false { return null_shape_id() } if math.distance_squared(c.center1, c.center2) <= math.LINEAR_SLOP * math.LINEAR_SLOP { middle = math.lerp(c.center1, c.center2, 0.5) @@ -2188,6 +2265,632 @@ create_capsule_shape(body_id: BodyId, def: *ShapeDef, c: Capsule) -> ShapeId { return create_shape(body_id, def, shape.capsule_shape(c, def.density)) } +// --- recording (#149) --------------------------------------------------------------------------- + +// The bytes a world writes while it records (aephysics.recording starts and +// stops it): every call that changes the world, as its opcode, the length +// of its arguments and the arguments field by field (never a struct's +// bytes, which carry pointers and the language's own bookkeeping), a state +// hash after every step, and at the end the geometry the calls named by +// id. The recording starts with a snapshot of the world, so a player +// begins where the recording did. +struct Recording { + data: ptr + size: int + capacity: int + record_start: int + geometry: Buffer // RecordedGeometry: the blocks the calls name, each once + geometry_heads: LongMap // a content hash to the first entry of that hash + bounds: AABB // every step's world bounds, joined + have_bounds: bool + owner: int // the recording world's index1 while it records, else 0 +} + +// One geometry block the calls named: the recording's own pointer-free +// copy, found again by its content (the hash, then every byte). +struct RecordedGeometry { + hash: long + kind: int + byte_count: int + block: ptr + next: int +} + +// The recording's file header, before the snapshot: a new major version +// is one older players cannot read; a new minor version only adds records, +// which older players skip. +const RECORDING_MAGIC = 0x43525041 // "APRC", little-endian +const RECORDING_MAJOR_VERSION = 1 +const RECORDING_MINOR_VERSION = 0 +const RECORDING_HEADER_BYTES = 32 + +const OP_STATE_HASH = 2 +const OP_BOUNDS = 3 +const OP_END = 4 +const OP_CREATE_BODY = 16 +const OP_DESTROY_BODY = 17 +const OP_BODY_SET_TRANSFORM = 18 +const OP_BODY_SET_LINEAR_VELOCITY = 19 +const OP_BODY_SET_ANGULAR_VELOCITY = 20 +const OP_BODY_SET_TARGET_TRANSFORM = 21 +const OP_BODY_APPLY_FORCE = 22 +const OP_BODY_APPLY_FORCE_TO_CENTER = 23 +const OP_BODY_APPLY_TORQUE = 24 +const OP_BODY_APPLY_LINEAR_IMPULSE = 25 +const OP_BODY_APPLY_LINEAR_IMPULSE_TO_CENTER = 26 +const OP_BODY_APPLY_ANGULAR_IMPULSE = 27 +const OP_BODY_SET_BULLET = 28 +const OP_BODY_SET_MASS_DATA = 29 +const OP_BODY_APPLY_MASS_FROM_SHAPES = 30 +const OP_BODY_SET_LINEAR_DAMPING = 31 +const OP_BODY_SET_ANGULAR_DAMPING = 32 +const OP_BODY_SET_GRAVITY_SCALE = 33 +const OP_BODY_SET_AWAKE = 34 +const OP_BODY_ENABLE_SLEEP = 35 +const OP_BODY_SET_SLEEP_THRESHOLD = 36 +const OP_BODY_SET_MOTION_LOCKS = 37 +const OP_CREATE_SPHERE_SHAPE = 38 +const OP_CREATE_CAPSULE_SHAPE = 39 +const OP_CREATE_HULL_SHAPE = 40 +const OP_CREATE_TRANSFORMED_HULL_SHAPE = 41 +const OP_CREATE_MESH_SHAPE = 42 +const OP_CREATE_HEIGHT_FIELD_SHAPE = 43 +const OP_CREATE_COMPOUND_SHAPE = 44 +const OP_DESTROY_SHAPE = 45 +const OP_SHAPE_SET_DENSITY = 46 +const OP_SHAPE_SET_FILTER = 47 +const OP_SHAPE_SET_SURFACE_MATERIAL = 48 +const OP_SHAPE_SET_SPHERE = 49 +const OP_SHAPE_SET_CAPSULE = 50 +const OP_SHAPE_SET_HULL = 51 +const OP_SHAPE_SET_MESH = 52 +const OP_SHAPE_SET_FRICTION = 53 +const OP_SHAPE_SET_RESTITUTION = 54 +const OP_SHAPE_SET_MESH_MATERIAL = 55 +const OP_SHAPE_ENABLE_PRE_SOLVE_EVENTS = 56 +const OP_BODY_ENABLE_HIT_EVENTS = 57 +const OP_BODY_ALLOW_FAST_ROTATION = 58 +const OP_BODY_ENABLE_CONTACT_RECYCLING = 59 +const OP_BODY_SET_SAFETY_FACTOR = 60 +const OP_SHAPE_APPLY_WIND = 61 +const OP_BODY_SET_NAME = 62 +const OP_SHAPE_SET_NAME = 63 +const OP_CREATE_DISTANCE_JOINT = 64 +const OP_CREATE_MOTOR_JOINT = 65 +const OP_CREATE_FILTER_JOINT = 66 +const OP_CREATE_PARALLEL_JOINT = 67 +const OP_CREATE_PRISMATIC_JOINT = 68 +const OP_CREATE_REVOLUTE_JOINT = 69 +const OP_CREATE_SPHERICAL_JOINT = 70 +const OP_CREATE_WELD_JOINT = 71 +const OP_CREATE_WHEEL_JOINT = 72 +const OP_DESTROY_JOINT = 73 +const OP_JOINT_SET_LOCAL_FRAME_A = 74 +const OP_JOINT_SET_LOCAL_FRAME_B = 75 +const OP_JOINT_SET_COLLIDE_CONNECTED = 76 +const OP_JOINT_WAKE_BODIES = 77 +const OP_JOINT_SET_CONSTRAINT_TUNING = 78 +const OP_JOINT_SET_FORCE_THRESHOLD = 79 +const OP_JOINT_SET_TORQUE_THRESHOLD = 80 +const OP_BODY_SET_TYPE = 81 +const OP_BODY_DISABLE = 82 +const OP_BODY_ENABLE = 83 +const OP_SHAPE_ENABLE_SENSOR_EVENTS = 84 +const OP_SHAPE_ENABLE_CONTACT_EVENTS = 85 +const OP_SHAPE_ENABLE_HIT_EVENTS = 86 +const OP_STEP = 87 +const OP_ENABLE_SLEEPING = 88 +const OP_ENABLE_WARM_STARTING = 89 +const OP_ENABLE_CONTINUOUS = 90 +const OP_ENABLE_SPECULATIVE = 91 +const OP_SET_RESTITUTION_THRESHOLD = 92 +const OP_SET_RESTITUTION_ITERATIONS = 93 +const OP_ENABLE_RESTITUTION_PROPAGATION = 94 +const OP_ENABLE_KINEMATIC_STATIC_SOFTNESS = 95 +const OP_SET_HIT_EVENT_THRESHOLD = 96 +const OP_SET_CONTACT_TUNING = 97 +const OP_SET_CONTACT_RECYCLE_DISTANCE = 98 +const OP_SET_MAXIMUM_LINEAR_SPEED = 99 +const OP_SET_GRAVITY = 100 +const OP_REBUILD_STATIC_TREE = 101 +const OP_EXPLODE = 102 +const OP_DISTANCE_JOINT_SET_LENGTH = 103 +const OP_DISTANCE_JOINT_ENABLE_LIMIT = 104 +const OP_DISTANCE_JOINT_SET_LENGTH_RANGE = 105 +const OP_DISTANCE_JOINT_ENABLE_SPRING = 106 +const OP_DISTANCE_JOINT_SET_SPRING_FORCE_RANGE = 107 +const OP_DISTANCE_JOINT_SET_SPRING_HERTZ = 108 +const OP_DISTANCE_JOINT_SET_SPRING_DAMPING_RATIO = 109 +const OP_DISTANCE_JOINT_ENABLE_MOTOR = 110 +const OP_DISTANCE_JOINT_SET_MOTOR_SPEED = 111 +const OP_DISTANCE_JOINT_SET_MAX_MOTOR_FORCE = 112 +const OP_MOTOR_JOINT_SET_LINEAR_VELOCITY = 113 +const OP_MOTOR_JOINT_SET_ANGULAR_VELOCITY = 114 +const OP_MOTOR_JOINT_SET_MAX_VELOCITY_TORQUE = 115 +const OP_MOTOR_JOINT_SET_MAX_VELOCITY_FORCE = 116 +const OP_MOTOR_JOINT_SET_LINEAR_HERTZ = 117 +const OP_MOTOR_JOINT_SET_LINEAR_DAMPING_RATIO = 118 +const OP_MOTOR_JOINT_SET_ANGULAR_HERTZ = 119 +const OP_MOTOR_JOINT_SET_ANGULAR_DAMPING_RATIO = 120 +const OP_MOTOR_JOINT_SET_MAX_SPRING_FORCE = 121 +const OP_MOTOR_JOINT_SET_MAX_SPRING_TORQUE = 122 +const OP_PARALLEL_JOINT_SET_SPRING_HERTZ = 123 +const OP_PARALLEL_JOINT_SET_SPRING_DAMPING_RATIO = 124 +const OP_PARALLEL_JOINT_SET_MAX_TORQUE = 125 +const OP_PRISMATIC_JOINT_ENABLE_SPRING = 126 +const OP_PRISMATIC_JOINT_SET_SPRING_HERTZ = 127 +const OP_PRISMATIC_JOINT_SET_SPRING_DAMPING_RATIO = 128 +const OP_PRISMATIC_JOINT_SET_TARGET_TRANSLATION = 129 +const OP_PRISMATIC_JOINT_ENABLE_LIMIT = 130 +const OP_PRISMATIC_JOINT_SET_LIMITS = 131 +const OP_PRISMATIC_JOINT_ENABLE_MOTOR = 132 +const OP_PRISMATIC_JOINT_SET_MOTOR_SPEED = 133 +const OP_PRISMATIC_JOINT_SET_MAX_MOTOR_FORCE = 134 +const OP_REVOLUTE_JOINT_ENABLE_LIMIT = 135 +const OP_REVOLUTE_JOINT_SET_LIMITS = 136 +const OP_REVOLUTE_JOINT_ENABLE_SPRING = 137 +const OP_REVOLUTE_JOINT_SET_TARGET_ANGLE = 138 +const OP_REVOLUTE_JOINT_SET_SPRING_HERTZ = 139 +const OP_REVOLUTE_JOINT_SET_SPRING_DAMPING_RATIO = 140 +const OP_REVOLUTE_JOINT_ENABLE_MOTOR = 141 +const OP_REVOLUTE_JOINT_SET_MOTOR_SPEED = 142 +const OP_REVOLUTE_JOINT_SET_MAX_MOTOR_TORQUE = 143 +const OP_SPHERICAL_JOINT_ENABLE_CONE_LIMIT = 144 +const OP_SPHERICAL_JOINT_SET_CONE_LIMIT = 145 +const OP_SPHERICAL_JOINT_SET_CONE_AXIS = 146 +const OP_SPHERICAL_JOINT_SET_CONE_ELLIPSE = 147 +const OP_SPHERICAL_JOINT_ENABLE_TWIST_LIMIT = 148 +const OP_SPHERICAL_JOINT_SET_TWIST_LIMITS = 149 +const OP_SPHERICAL_JOINT_ENABLE_SPRING = 150 +const OP_SPHERICAL_JOINT_SET_TARGET_ROTATION = 151 +const OP_SPHERICAL_JOINT_SET_SPRING_HERTZ = 152 +const OP_SPHERICAL_JOINT_SET_SPRING_DAMPING_RATIO = 153 +const OP_SPHERICAL_JOINT_ENABLE_MOTOR = 154 +const OP_SPHERICAL_JOINT_SET_MOTOR_VELOCITY = 155 +const OP_SPHERICAL_JOINT_SET_MAX_MOTOR_TORQUE = 156 +const OP_WELD_JOINT_SET_LINEAR_HERTZ = 157 +const OP_WELD_JOINT_SET_LINEAR_DAMPING_RATIO = 158 +const OP_WELD_JOINT_SET_ANGULAR_HERTZ = 159 +const OP_WELD_JOINT_SET_ANGULAR_DAMPING_RATIO = 160 +const OP_WHEEL_JOINT_ENABLE_SUSPENSION = 161 +const OP_WHEEL_JOINT_SET_SUSPENSION_HERTZ = 162 +const OP_WHEEL_JOINT_SET_SUSPENSION_DAMPING_RATIO = 163 +const OP_WHEEL_JOINT_ENABLE_SUSPENSION_LIMIT = 164 +const OP_WHEEL_JOINT_SET_SUSPENSION_LIMITS = 165 +const OP_WHEEL_JOINT_ENABLE_SPIN_MOTOR = 166 +const OP_WHEEL_JOINT_SET_SPIN_MOTOR_SPEED = 167 +const OP_WHEEL_JOINT_SET_MAX_SPIN_TORQUE = 168 +const OP_WHEEL_JOINT_ENABLE_STEERING = 169 +const OP_WHEEL_JOINT_SET_STEERING_HERTZ = 170 +const OP_WHEEL_JOINT_SET_STEERING_DAMPING_RATIO = 171 +const OP_WHEEL_JOINT_SET_MAX_STEERING_TORQUE = 172 +const OP_WHEEL_JOINT_ENABLE_STEERING_LIMIT = 173 +const OP_WHEEL_JOINT_SET_STEERING_LIMITS = 174 +const OP_WHEEL_JOINT_SET_TARGET_STEERING_ANGLE = 175 + +// Whether the world of this index is recording: the hooks' test, false +// for an index that is not a world's. +recording_on(world0: int) -> bool { + if g_worlds == null || world0 < 0 || world0 >= MAX_WORLDS { return false } + w = world_at(world0) + return w.in_use && w.recording != null +} + +rec_grow(r: *Recording, n: int) { + if r.size + n <= r.capacity { return } + capacity = math.max_int(math.max_int(2 * r.capacity, r.size + n), 4096) + block = basics.alloc(capacity) + if r.data != null { + memcpy(block, r.data, r.size) + basics.free_bytes(r.data, r.capacity) + } + r.data = block + r.capacity = capacity +} + +rec_raw(r: *Recording, src: ptr, n: int) { + if n <= 0 { return } + rec_grow(r, n) + memcpy(r.data + r.size, src, n) + r.size = r.size + n +} + +// A record begun: its opcode and a length filled in by rec_end. +rec_begin(world0: int, op: int) -> *Recording { + r = world_at(world0).recording as *Recording + rec_int(r, op) + r.record_start = r.size + rec_int(r, 0) + return r +} + +rec_end(r: *Recording) { + length = r.size - r.record_start - 4 + memcpy(r.data + r.record_start, (&length) as ptr, 4) +} + +rec_int(r: *Recording, v: int) { + holder = v + rec_raw(r, (&holder) as ptr, 4) +} + +rec_long(r: *Recording, v: long) { + holder = v + rec_raw(r, (&holder) as ptr, 8) +} + +rec_float(r: *Recording, v: float) { + holder = v + rec_raw(r, (&holder) as ptr, 8) +} + +rec_bool(r: *Recording, v: bool) { + if v { rec_int(r, 1) } else { rec_int(r, 0) } +} + +rec_vec3(r: *Recording, v: Vec3) { + rec_float(r, v.x) + rec_float(r, v.y) + rec_float(r, v.z) +} + +rec_quat(r: *Recording, q: Quat) { + rec_float(r, q.x) + rec_float(r, q.y) + rec_float(r, q.z) + rec_float(r, q.w) +} + +rec_transform(r: *Recording, t: Transform) { + rec_vec3(r, t.p) + rec_quat(r, t.q) +} + +// A body, shape or joint id without its world: the player's world stands in. +rec_id(r: *Recording, index1: int, generation: int) { + rec_int(r, index1) + rec_int(r, generation) +} + +// A string's length and bytes, or -1 for none. +rec_string(r: *Recording, s: string) { + if s == null { + rec_int(r, NULL_INDEX) + return + } + n = string.length(s) + rec_int(r, n) + rec_raw(r, string.aether_string_raw_ptr(s), n) +} + +// A shape def's materials: their count, then each one, or -1 for none. +rec_materials(r: *Recording, materials: ptr, count: int) { + if materials == null || count <= 0 { + rec_int(r, NULL_INDEX) + return + } + rec_int(r, count) + each = materials as SurfaceMaterial[] + i = 0 + while i < count { + rec_surface_material(r, &each[i]) + i = i + 1 + } +} + +// A geometry block by its id in the recording, interned by content: the +// first call to name a block copies it (a compound's tree pointers +// cleared), later ones find it. -1 for none. +rec_geometry(r: *Recording, kind: int, block: ptr) { + if block == null { + rec_int(r, NULL_INDEX) + return + } + byte_count = 0 + if kind == shape.SHAPE_HULL { byte_count = (block as *HullData).byte_count } + else if kind == shape.SHAPE_MESH { byte_count = (block as *MeshData).byte_count } + else if kind == shape.SHAPE_HEIGHT_FIELD { byte_count = (block as *HeightFieldData).byte_count } + else if kind == shape.SHAPE_COMPOUND { byte_count = (block as *CompoundData).byte_count } + copy = basics.alloc(byte_count) + memcpy(copy, block, byte_count) + if kind == shape.SHAPE_COMPOUND { + c = copy as *CompoundData + c.tree.nodes = null + c.tree.proxies = null + c.tree.stack = null + c.tree.stack_dist = null + } + hash = basics.hash_bytes(copy, byte_count) + index = basics.map_get(&r.geometry_heads, hash, NULL_INDEX) + while index != NULL_INDEX { + entry = recorded_geometry_at(r, index) + if entry.kind == kind && entry.byte_count == byte_count && memcmp(entry.block, copy, byte_count) == 0 { + basics.free_bytes(copy, byte_count) + rec_int(r, index) + return + } + index = entry.next + } + index = basics.buffer_push(&r.geometry) + entry = recorded_geometry_at(r, index) + entry.hash = hash + entry.kind = kind + entry.byte_count = byte_count + entry.block = copy + entry.next = basics.map_get(&r.geometry_heads, hash, NULL_INDEX) + basics.map_set(&r.geometry_heads, hash, index) + rec_int(r, index) +} + +recorded_geometry_at(r: *Recording, index: int) -> *RecordedGeometry { return (r.geometry.data + index * sizeof(RecordedGeometry)) as *RecordedGeometry } + +// The recording finished (aephysics.recording's stop, and a recording +// world's destroy): the joined bounds and the end marker, then the +// geometry the calls named, and the header told where that is. +finish_recording(w: *World) { + if w.recording == null { return } + r = w.recording as *Recording + w.recording = null + r.owner = 0 + rec_int(r, OP_BOUNDS) + r.record_start = r.size + rec_int(r, 0) + rec_bool(r, r.have_bounds) + rec_vec3(r, r.bounds.lower) + rec_vec3(r, r.bounds.upper) + rec_end(r) + rec_int(r, OP_END) + rec_int(r, 0) + registry_offset = r.size + rec_int(r, r.geometry.count) + i = 0 + while i < r.geometry.count { + entry = recorded_geometry_at(r, i) + rec_int(r, entry.kind) + rec_int(r, entry.byte_count) + rec_raw(r, entry.block, entry.byte_count) + i = i + 1 + } + registry_bytes = r.size - registry_offset + memcpy(r.data + 12, (®istry_offset) as ptr, 4) + memcpy(r.data + 16, (®istry_bytes) as ptr, 4) +} + +rec_body_def(r: *Recording, v: *BodyDef) { + rec_int(r, v.kind) + rec_vec3(r, v.position) + rec_quat(r, v.rotation) + rec_vec3(r, v.linear_velocity) + rec_vec3(r, v.angular_velocity) + rec_float(r, v.linear_damping) + rec_float(r, v.angular_damping) + rec_float(r, v.gravity_scale) + rec_float(r, v.sleep_threshold) + rec_float(r, v.safety_factor) + rec_string(r, v.name) + rec_motion_locks(r, &v.motion_locks) + rec_bool(r, v.enable_sleep) + rec_bool(r, v.is_awake) + rec_bool(r, v.is_bullet) + rec_bool(r, v.is_enabled) + rec_bool(r, v.allow_fast_rotation) + rec_bool(r, v.enable_contact_recycling) +} +rec_body_def_value(r: *Recording, v: BodyDef) { rec_body_def(r, &v) } + +rec_shape_def(r: *Recording, v: *ShapeDef) { + rec_string(r, v.name) + rec_materials(r, v.materials, v.material_count) + rec_surface_material(r, &v.base_material) + rec_float(r, v.density) + rec_float(r, v.explosion_scale) + rec_filter(r, &v.filter) + rec_bool(r, v.enable_custom_filtering) + rec_bool(r, v.is_sensor) + rec_bool(r, v.enable_sensor_events) + rec_bool(r, v.enable_contact_events) + rec_bool(r, v.enable_hit_events) + rec_bool(r, v.enable_pre_solve_events) + rec_bool(r, v.invoke_contact_creation) + rec_bool(r, v.update_body_mass) + rec_bool(r, v.enable_speculative_contact) +} +rec_shape_def_value(r: *Recording, v: ShapeDef) { rec_shape_def(r, &v) } + +rec_joint_def(r: *Recording, v: *JointDef) { + rec_id(r, v.body_id_a.index1, v.body_id_a.generation) + rec_id(r, v.body_id_b.index1, v.body_id_b.generation) + rec_transform(r, v.local_frame_a) + rec_transform(r, v.local_frame_b) + rec_float(r, v.force_threshold) + rec_float(r, v.torque_threshold) + rec_float(r, v.constraint_hertz) + rec_float(r, v.constraint_damping_ratio) + rec_float(r, v.draw_scale) + rec_bool(r, v.collide_connected) +} +rec_joint_def_value(r: *Recording, v: JointDef) { rec_joint_def(r, &v) } + +rec_distance_joint_def(r: *Recording, v: *DistanceJointDef) { + rec_joint_def(r, &v.base) + rec_float(r, v.length) + rec_bool(r, v.enable_spring) + rec_float(r, v.lower_spring_force) + rec_float(r, v.upper_spring_force) + rec_float(r, v.hertz) + rec_float(r, v.damping_ratio) + rec_bool(r, v.enable_limit) + rec_float(r, v.min_length) + rec_float(r, v.max_length) + rec_bool(r, v.enable_motor) + rec_float(r, v.max_motor_force) + rec_float(r, v.motor_speed) +} +rec_distance_joint_def_value(r: *Recording, v: DistanceJointDef) { rec_distance_joint_def(r, &v) } + +rec_motor_joint_def(r: *Recording, v: *MotorJointDef) { + rec_joint_def(r, &v.base) + rec_vec3(r, v.linear_velocity) + rec_float(r, v.max_velocity_force) + rec_vec3(r, v.angular_velocity) + rec_float(r, v.max_velocity_torque) + rec_float(r, v.linear_hertz) + rec_float(r, v.linear_damping_ratio) + rec_float(r, v.max_spring_force) + rec_float(r, v.angular_hertz) + rec_float(r, v.angular_damping_ratio) + rec_float(r, v.max_spring_torque) +} +rec_motor_joint_def_value(r: *Recording, v: MotorJointDef) { rec_motor_joint_def(r, &v) } + +rec_filter_joint_def(r: *Recording, v: *FilterJointDef) { + rec_joint_def(r, &v.base) +} +rec_filter_joint_def_value(r: *Recording, v: FilterJointDef) { rec_filter_joint_def(r, &v) } + +rec_parallel_joint_def(r: *Recording, v: *ParallelJointDef) { + rec_joint_def(r, &v.base) + rec_float(r, v.hertz) + rec_float(r, v.damping_ratio) + rec_float(r, v.max_torque) +} +rec_parallel_joint_def_value(r: *Recording, v: ParallelJointDef) { rec_parallel_joint_def(r, &v) } + +rec_prismatic_joint_def(r: *Recording, v: *PrismaticJointDef) { + rec_joint_def(r, &v.base) + rec_bool(r, v.enable_spring) + rec_float(r, v.hertz) + rec_float(r, v.damping_ratio) + rec_float(r, v.target_translation) + rec_bool(r, v.enable_limit) + rec_float(r, v.lower_translation) + rec_float(r, v.upper_translation) + rec_bool(r, v.enable_motor) + rec_float(r, v.max_motor_force) + rec_float(r, v.motor_speed) +} +rec_prismatic_joint_def_value(r: *Recording, v: PrismaticJointDef) { rec_prismatic_joint_def(r, &v) } + +rec_revolute_joint_def(r: *Recording, v: *RevoluteJointDef) { + rec_joint_def(r, &v.base) + rec_float(r, v.target_angle) + rec_bool(r, v.enable_spring) + rec_float(r, v.hertz) + rec_float(r, v.damping_ratio) + rec_bool(r, v.enable_limit) + rec_float(r, v.lower_angle) + rec_float(r, v.upper_angle) + rec_bool(r, v.enable_motor) + rec_float(r, v.max_motor_torque) + rec_float(r, v.motor_speed) +} +rec_revolute_joint_def_value(r: *Recording, v: RevoluteJointDef) { rec_revolute_joint_def(r, &v) } + +rec_spherical_joint_def(r: *Recording, v: *SphericalJointDef) { + rec_joint_def(r, &v.base) + rec_bool(r, v.enable_spring) + rec_float(r, v.hertz) + rec_float(r, v.damping_ratio) + rec_quat(r, v.target_rotation) + rec_bool(r, v.enable_cone_limit) + rec_float(r, v.cone_angle) + rec_vec3(r, v.cone_axis) + rec_float(r, v.cone_minor_angle) + rec_vec3(r, v.cone_major_axis) + rec_bool(r, v.enable_twist_limit) + rec_float(r, v.lower_twist_angle) + rec_float(r, v.upper_twist_angle) + rec_bool(r, v.enable_motor) + rec_float(r, v.max_motor_torque) + rec_vec3(r, v.motor_velocity) +} +rec_spherical_joint_def_value(r: *Recording, v: SphericalJointDef) { rec_spherical_joint_def(r, &v) } + +rec_weld_joint_def(r: *Recording, v: *WeldJointDef) { + rec_joint_def(r, &v.base) + rec_float(r, v.linear_hertz) + rec_float(r, v.angular_hertz) + rec_float(r, v.linear_damping_ratio) + rec_float(r, v.angular_damping_ratio) +} +rec_weld_joint_def_value(r: *Recording, v: WeldJointDef) { rec_weld_joint_def(r, &v) } + +rec_wheel_joint_def(r: *Recording, v: *WheelJointDef) { + rec_joint_def(r, &v.base) + rec_bool(r, v.enable_suspension_spring) + rec_float(r, v.suspension_hertz) + rec_float(r, v.suspension_damping_ratio) + rec_bool(r, v.enable_suspension_limit) + rec_float(r, v.lower_suspension_limit) + rec_float(r, v.upper_suspension_limit) + rec_bool(r, v.enable_spin_motor) + rec_float(r, v.max_spin_torque) + rec_float(r, v.spin_speed) + rec_bool(r, v.enable_steering) + rec_float(r, v.steering_hertz) + rec_float(r, v.steering_damping_ratio) + rec_float(r, v.target_steering_angle) + rec_float(r, v.max_steering_torque) + rec_bool(r, v.enable_steering_limit) + rec_float(r, v.lower_steering_limit) + rec_float(r, v.upper_steering_limit) +} +rec_wheel_joint_def_value(r: *Recording, v: WheelJointDef) { rec_wheel_joint_def(r, &v) } + +rec_mass_data(r: *Recording, v: *MassData) { + rec_float(r, v.mass) + rec_vec3(r, v.center) + rec_matrix3(r, &v.inertia) +} +rec_mass_data_value(r: *Recording, v: MassData) { rec_mass_data(r, &v) } + +rec_motion_locks(r: *Recording, v: *MotionLocks) { + rec_bool(r, v.linear_x) + rec_bool(r, v.linear_y) + rec_bool(r, v.linear_z) + rec_bool(r, v.angular_x) + rec_bool(r, v.angular_y) + rec_bool(r, v.angular_z) +} +rec_motion_locks_value(r: *Recording, v: MotionLocks) { rec_motion_locks(r, &v) } + +rec_filter(r: *Recording, v: *Filter) { + rec_long(r, v.category_bits) + rec_long(r, v.mask_bits) + rec_int(r, v.group_index) +} +rec_filter_value(r: *Recording, v: Filter) { rec_filter(r, &v) } + +rec_surface_material(r: *Recording, v: *SurfaceMaterial) { + rec_float(r, v.friction) + rec_float(r, v.restitution) + rec_float(r, v.rolling_resistance) + rec_vec3(r, v.tangent_velocity) + rec_long(r, v.user_material_id) + rec_int(r, v.custom_color) +} +rec_surface_material_value(r: *Recording, v: SurfaceMaterial) { rec_surface_material(r, &v) } + +rec_sphere(r: *Recording, v: *Sphere) { + rec_vec3(r, v.center) + rec_float(r, v.radius) +} +rec_sphere_value(r: *Recording, v: Sphere) { rec_sphere(r, &v) } + +rec_capsule(r: *Recording, v: *Capsule) { + rec_vec3(r, v.center1) + rec_vec3(r, v.center2) + rec_float(r, v.radius) +} +rec_capsule_value(r: *Recording, v: Capsule) { rec_capsule(r, &v) } + +rec_matrix3(r: *Recording, v: *Matrix3) { + rec_vec3(r, v.cx) + rec_vec3(r, v.cy) + rec_vec3(r, v.cz) +} +rec_matrix3_value(r: *Recording, v: Matrix3) { rec_matrix3(r, &v) } + // --- the hull database (the reference's hullDatabase, #147) ------------------------- // One entry of the world's hull database: an owned copy of a hull its @@ -2315,6 +3018,19 @@ world_hull_count(world_id: WorldId) -> int { // world's copy of its hull (the hull database), never the caller's: the // caller may free its hull once the shape is made. create_hull_shape(body_id: BodyId, def: *ShapeDef, h: *HullData) -> ShapeId { + if recording_on(body_id.world0) == false { return create_hull_shape_internal(body_id, def, h) } + recorder = rec_begin(body_id.world0, OP_CREATE_HULL_SHAPE) + rec_id(recorder, body_id.index1, body_id.generation) + rec_shape_def(recorder, def) + rec_geometry(recorder, shape.SHAPE_HULL, h as ptr) + made = create_hull_shape_internal(body_id, def, h) + rec_id(recorder, made.index1, made.generation) + rec_end(recorder) + return made +} + +// create_hull_shape's work, unrecorded. +create_hull_shape_internal(body_id: BodyId, def: *ShapeDef, h: *HullData) -> ShapeId { if h == null || h.version != hull.HULL_VERSION { return null_shape_id() } if body_is_valid(body_id) == false { return null_shape_id() } w = world_at(body_id.world0) @@ -2330,6 +3046,21 @@ create_hull_shape(body_id: BodyId, def: *ShapeDef, h: *HullData) -> ShapeId { // baked into the world's own copy and shared like any other // (b3CreateTransformedHullShape). Refused when the result has no volume. create_transformed_hull_shape(body_id: BodyId, def: *ShapeDef, h: *HullData, transform: Transform, scale: Vec3) -> ShapeId { + if recording_on(body_id.world0) == false { return create_transformed_hull_shape_internal(body_id, def, h, transform, scale) } + recorder = rec_begin(body_id.world0, OP_CREATE_TRANSFORMED_HULL_SHAPE) + rec_id(recorder, body_id.index1, body_id.generation) + rec_shape_def(recorder, def) + rec_geometry(recorder, shape.SHAPE_HULL, h as ptr) + rec_transform(recorder, transform) + rec_vec3(recorder, scale) + made = create_transformed_hull_shape_internal(body_id, def, h, transform, scale) + rec_id(recorder, made.index1, made.generation) + rec_end(recorder) + return made +} + +// create_transformed_hull_shape's work, unrecorded. +create_transformed_hull_shape_internal(body_id: BodyId, def: *ShapeDef, h: *HullData, transform: Transform, scale: Vec3) -> ShapeId { if h == null || h.version != hull.HULL_VERSION { return null_shape_id() } if math.is_valid_transform(transform) == false || math.is_valid_vec3(scale) == false { return null_shape_id() } if body_is_valid(body_id) == false { return null_shape_id() } @@ -2343,14 +3074,54 @@ create_transformed_hull_shape(body_id: BodyId, def: *ShapeDef, h: *HullData, tra return id } create_mesh_shape(body_id: BodyId, def: *ShapeDef, m: *MeshData, scale: Vec3) -> ShapeId { + if recording_on(body_id.world0) == false { return create_mesh_shape_internal(body_id, def, m, scale) } + recorder = rec_begin(body_id.world0, OP_CREATE_MESH_SHAPE) + rec_id(recorder, body_id.index1, body_id.generation) + rec_shape_def(recorder, def) + rec_geometry(recorder, shape.SHAPE_MESH, m as ptr) + rec_vec3(recorder, scale) + made = create_mesh_shape_internal(body_id, def, m, scale) + rec_id(recorder, made.index1, made.generation) + rec_end(recorder) + return made +} + +// create_mesh_shape's work, unrecorded. +create_mesh_shape_internal(body_id: BodyId, def: *ShapeDef, m: *MeshData, scale: Vec3) -> ShapeId { if m == null || m.version != mesh.MESH_VERSION || math.is_valid_vec3(scale) == false { return null_shape_id() } return create_shape(body_id, def, shape.mesh_shape(mesh.mesh(m, math.safe_scale(scale)), math.max_int(def.material_count, 1))) } create_height_field_shape(body_id: BodyId, def: *ShapeDef, h: *HeightFieldData) -> ShapeId { + if recording_on(body_id.world0) == false { return create_height_field_shape_internal(body_id, def, h) } + recorder = rec_begin(body_id.world0, OP_CREATE_HEIGHT_FIELD_SHAPE) + rec_id(recorder, body_id.index1, body_id.generation) + rec_shape_def(recorder, def) + rec_geometry(recorder, shape.SHAPE_HEIGHT_FIELD, h as ptr) + made = create_height_field_shape_internal(body_id, def, h) + rec_id(recorder, made.index1, made.generation) + rec_end(recorder) + return made +} + +// create_height_field_shape's work, unrecorded. +create_height_field_shape_internal(body_id: BodyId, def: *ShapeDef, h: *HeightFieldData) -> ShapeId { if h == null || h.version != height_field.HEIGHT_FIELD_VERSION { return null_shape_id() } return create_shape(body_id, def, shape.height_field_shape(h, math.max_int(def.material_count, 1))) } create_compound_shape(body_id: BodyId, def: *ShapeDef, c: *CompoundData) -> ShapeId { + if recording_on(body_id.world0) == false { return create_compound_shape_internal(body_id, def, c) } + recorder = rec_begin(body_id.world0, OP_CREATE_COMPOUND_SHAPE) + rec_id(recorder, body_id.index1, body_id.generation) + rec_shape_def(recorder, def) + rec_geometry(recorder, shape.SHAPE_COMPOUND, c as ptr) + made = create_compound_shape_internal(body_id, def, c) + rec_id(recorder, made.index1, made.generation) + rec_end(recorder) + return made +} + +// create_compound_shape's work, unrecorded. +create_compound_shape_internal(body_id: BodyId, def: *ShapeDef, c: *CompoundData) -> ShapeId { if c == null || c.version != compound.COMPOUND_VERSION { return null_shape_id() } return create_shape(body_id, def, shape.compound_shape(c)) } @@ -2372,6 +3143,7 @@ get_shape(w: *World, shape_id: int) -> *WorldShape { return world_shape_at(w, sh // The shape leaves its body's list and the broad phase; the body's mass // is updated when asked. destroy_shape(id: ShapeId, update_body_mass: bool) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_DESTROY_SHAPE); rec_id(recorder, id.index1, id.generation); rec_bool(recorder, update_body_mass); rec_end(recorder) } if shape_is_valid(id) == false { return } w = world_at(id.world0) if w.locked { return } @@ -2412,6 +3184,7 @@ shape_get_aabb(id: ShapeId) -> AABB { return get_shape_full_id(world_at(id.world shape_get_type(id: ShapeId) -> int { return get_shape_full_id(world_at(id.world0), id).geometry.kind } shape_get_density(id: ShapeId) -> float { return get_shape_full_id(world_at(id.world0), id).geometry.density } shape_set_density(id: ShapeId, density: float, update_body_mass: bool) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_SHAPE_SET_DENSITY); rec_id(recorder, id.index1, id.generation); rec_float(recorder, density); rec_bool(recorder, update_body_mass); rec_end(recorder) } if valid_nonnegative(density) == false { return } w = world_at(id.world0) // Not mid-step, and an unchanged density spares the mass update (b3Shape_SetDensity). @@ -2431,6 +3204,7 @@ shape_get_filter(id: ShapeId) -> Filter { return get_shape_full_id(world_at(id.w // paired shapes the filter now allows, and queried the old category.) // Sensor overlaps follow on the next step. shape_set_filter(id: ShapeId, filter: Filter) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_SHAPE_SET_FILTER); rec_id(recorder, id.index1, id.generation); rec_filter_value(recorder, filter); rec_end(recorder) } w = world_at(id.world0) if w.locked { return } s = get_shape_full_id(w, id) @@ -2465,6 +3239,13 @@ reset_shape_proxy(w: *World, s: *WorldShape, wake_bodies: bool, remake: bool) { } shape_get_surface_material(id: ShapeId) -> SurfaceMaterial { return get_shape_full_id(world_at(id.world0), id).material } shape_set_surface_material(id: ShapeId, m: SurfaceMaterial) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_SHAPE_SET_SURFACE_MATERIAL); rec_id(recorder, id.index1, id.generation); rec_surface_material_value(recorder, m); rec_end(recorder) } + set_surface_material(id, m) +} + +// The material set without recording it: the setters built on it record +// themselves. +set_surface_material(id: ShapeId, m: SurfaceMaterial) { if valid_material(m) == false { return } s = get_shape_full_id(world_at(id.world0), id) s.material = m @@ -2495,10 +3276,12 @@ shape_get_height_field(id: ShapeId) -> *HeightFieldData { return get_shape_full_ // as in the reference. A compound or height field (static-only shapes) is // not changed, nor changed into. shape_set_sphere(id: ShapeId, s: Sphere) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_SHAPE_SET_SPHERE); rec_id(recorder, id.index1, id.generation); rec_sphere_value(recorder, s); rec_end(recorder) } if valid_sphere(s) == false { return } change_shape_geometry(id, shape.sphere_shape(s, 0.0)) } shape_set_capsule(id: ShapeId, c: Capsule) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_SHAPE_SET_CAPSULE); rec_id(recorder, id.index1, id.generation); rec_capsule_value(recorder, c); rec_end(recorder) } if valid_capsule(c) == false { return } change_shape_geometry(id, shape.capsule_shape(c, 0.0)) } @@ -2506,6 +3289,7 @@ shape_set_capsule(id: ShapeId, c: Capsule) { // `h` may be the shape's own copy; the same copy changes nothing (no // contacts lost, no proxy made again), as in b3Shape_SetHull. shape_set_hull(id: ShapeId, h: *HullData) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_SHAPE_SET_HULL); rec_id(recorder, id.index1, id.generation); rec_geometry(recorder, shape.SHAPE_HULL, h as ptr); rec_end(recorder) } if h == null || h.version != hull.HULL_VERSION { return } w = world_at(id.world0) if w.locked { return } @@ -2520,6 +3304,7 @@ shape_set_hull(id: ShapeId, h: *HullData) { change_shape_geometry(id, shape.hull_shape(shared, 0.0)) } shape_set_mesh(id: ShapeId, m: *MeshData, scale: Vec3) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_SHAPE_SET_MESH); rec_id(recorder, id.index1, id.generation); rec_geometry(recorder, shape.SHAPE_MESH, m as ptr); rec_vec3(recorder, scale); rec_end(recorder) } if m == null || m.version != mesh.MESH_VERSION || math.is_valid_vec3(scale) == false { return } change_shape_geometry(id, shape.mesh_shape(mesh.mesh(m, math.safe_scale(scale)), 1)) } @@ -2544,15 +3329,17 @@ change_shape_geometry(id: ShapeId, geometry: Shape) { // its siblings); a contact takes them on its next update. shape_get_friction(id: ShapeId) -> float { return get_shape_full_id(world_at(id.world0), id).material.friction } shape_set_friction(id: ShapeId, friction: float) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_SHAPE_SET_FRICTION); rec_id(recorder, id.index1, id.generation); rec_float(recorder, friction); rec_end(recorder) } m = shape_get_surface_material(id) m.friction = friction - shape_set_surface_material(id, m) + set_surface_material(id, m) } shape_get_restitution(id: ShapeId) -> float { return get_shape_full_id(world_at(id.world0), id).material.restitution } shape_set_restitution(id: ShapeId, restitution: float) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_SHAPE_SET_RESTITUTION); rec_id(recorder, id.index1, id.generation); rec_float(recorder, restitution); rec_end(recorder) } m = shape_get_surface_material(id) m.restitution = restitution - shape_set_surface_material(id, m) + set_surface_material(id, m) } // A mesh's materials by index (b3Shape_SetMeshMaterial): the first is the @@ -2564,11 +3351,12 @@ shape_get_mesh_surface_material(id: ShapeId, index: int) -> SurfaceMaterial { return materials[index] } shape_set_mesh_material(id: ShapeId, m: SurfaceMaterial, index: int) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_SHAPE_SET_MESH_MATERIAL); rec_id(recorder, id.index1, id.generation); rec_surface_material_value(recorder, m); rec_int(recorder, index); rec_end(recorder) } if valid_material(m) == false { return } s = get_shape_full_id(world_at(id.world0), id) if index < 0 || index >= s.material_count { return } if index == 0 { - shape_set_surface_material(id, m) + set_surface_material(id, m) return } materials = s.materials as SurfaceMaterial[] @@ -2577,6 +3365,7 @@ shape_set_mesh_material(id: ShapeId, m: SurfaceMaterial, index: int) { // Pre-solve events on a shape after its creation; a contact takes the flag when it is made. shape_enable_pre_solve_events(id: ShapeId, flag: bool) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_SHAPE_ENABLE_PRE_SOLVE_EVENTS); rec_id(recorder, id.index1, id.generation); rec_bool(recorder, flag); rec_end(recorder) } s = get_shape_full_id(world_at(id.world0), id) if flag { s.flags = s.flags | SHAPE_ENABLE_PRE_SOLVE_EVENTS } else { s.flags = s.flags & (0 - 1 - SHAPE_ENABLE_PRE_SOLVE_EVENTS) } } @@ -2595,6 +3384,7 @@ shape_are_pre_solve_events_enabled(id: ShapeId) -> bool { return (get_shape_full const AIR_DENSITY = 1.225 shape_apply_wind(id: ShapeId, wind: Vec3, drag: float, lift: float, max_speed: float, wake: bool) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_SHAPE_APPLY_WIND); rec_id(recorder, id.index1, id.generation); rec_vec3(recorder, wind); rec_float(recorder, drag); rec_float(recorder, lift); rec_float(recorder, max_speed); rec_bool(recorder, wake); rec_end(recorder) } w = world_at(id.world0) if w.locked { return } s = get_shape_full_id(w, id) @@ -2679,12 +3469,14 @@ shape_apply_wind(id: ShapeId, wind: Vec3, drag: float, lift: float, max_speed: f // A label for debugging (b3Body_SetName and its siblings): copied, so the // caller's string may go; the old one freed; an empty name for none. body_set_name(id: BodyId, name: string) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_BODY_SET_NAME); rec_id(recorder, id.index1, id.generation); rec_string(recorder, name); rec_end(recorder) } body = get_body_full_id(world_at(id.world0), id) native.name_free(body.name) body.name = native.name_copy(name) } body_get_name(id: BodyId) -> string { return native.name_text(get_body_full_id(world_at(id.world0), id).name) } shape_set_name(id: ShapeId, name: string) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_SHAPE_SET_NAME); rec_id(recorder, id.index1, id.generation); rec_string(recorder, name); rec_end(recorder) } s = get_shape_full_id(world_at(id.world0), id) native.name_free(s.name) s.name = native.name_copy(name) @@ -2875,6 +3667,7 @@ body_get_world_vector(id: BodyId, local_vector: Vec3) -> Vec3 { return math.rota // its shapes' bounds and proxies are updated at once (a static body's // too, so contacts begin without a step). body_set_transform(id: BodyId, position: Vec3, rotation: Quat) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_BODY_SET_TRANSFORM); rec_id(recorder, id.index1, id.generation); rec_vec3(recorder, position); rec_quat(recorder, rotation); rec_end(recorder) } if math.is_valid_position(position) == false || math.is_valid_quat(rotation) == false { return } w = body_world(id) if w.locked { return } @@ -2921,6 +3714,7 @@ body_get_angular_velocity(id: BodyId) -> Vec3 { // A velocity wakes a sleeping body; a static body has none. body_set_linear_velocity(id: BodyId, v: Vec3) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_BODY_SET_LINEAR_VELOCITY); rec_id(recorder, id.index1, id.generation); rec_vec3(recorder, v); rec_end(recorder) } if math.is_valid_vec3(v) == false { return } w = body_world(id) body = get_body_full_id(w, id) @@ -2944,6 +3738,7 @@ awake_for_apply(w: *World, body: *Body, wake: bool) -> bool { // sleeping body stays asleep when the movement would be below its // sleep threshold, unless woken. body_set_target_transform(id: BodyId, target: Transform, time_step: float, wake: bool) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_BODY_SET_TARGET_TRANSFORM); rec_id(recorder, id.index1, id.generation); rec_transform(recorder, target); rec_float(recorder, time_step); rec_bool(recorder, wake); rec_end(recorder) } if math.is_valid_world_transform(target) == false || math.is_valid_float(time_step) == false { return } w = body_world(id) body = get_body_full_id(w, id) @@ -2973,16 +3768,18 @@ body_set_target_transform(id: BodyId, target: Transform, time_step: float, wake: } body_apply_force(id: BodyId, force: Vec3, point: Vec3, wake: bool) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_BODY_APPLY_FORCE); rec_id(recorder, id.index1, id.generation); rec_vec3(recorder, force); rec_vec3(recorder, point); rec_bool(recorder, wake); rec_end(recorder) } if math.is_valid_vec3(force) == false || math.is_valid_position(point) == false { return } w = body_world(id) body = get_body_full_id(w, id) if awake_for_apply(w, body, wake) == false { return } sim = get_body_sim(w, body) - sim.force = math.add(sim.force, force) - sim.torque = math.add(sim.torque, math.cross(math.sub(point, sim.center), force)) + sim.force = math.narrow3(math.add(math.widen3(sim.force), force)) + sim.torque = math.narrow3(math.add(math.widen3(sim.torque), math.cross(math.sub(point, sim.center), force))) } body_apply_force_to_center(id: BodyId, force: Vec3, wake: bool) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_BODY_APPLY_FORCE_TO_CENTER); rec_id(recorder, id.index1, id.generation); rec_vec3(recorder, force); rec_bool(recorder, wake); rec_end(recorder) } if math.is_valid_vec3(force) == false { return } w = body_world(id) body = get_body_full_id(w, id) @@ -2992,6 +3789,7 @@ body_apply_force_to_center(id: BodyId, force: Vec3, wake: bool) { } body_apply_torque(id: BodyId, torque: Vec3, wake: bool) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_BODY_APPLY_TORQUE); rec_id(recorder, id.index1, id.generation); rec_vec3(recorder, torque); rec_bool(recorder, wake); rec_end(recorder) } if math.is_valid_vec3(torque) == false { return } w = body_world(id) body = get_body_full_id(w, id) @@ -3008,6 +3806,7 @@ capped_linear_velocity(w: *World, v: Vec3) -> Vec3 { } body_apply_linear_impulse(id: BodyId, impulse: Vec3, point: Vec3, wake: bool) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_BODY_APPLY_LINEAR_IMPULSE); rec_id(recorder, id.index1, id.generation); rec_vec3(recorder, impulse); rec_vec3(recorder, point); rec_bool(recorder, wake); rec_end(recorder) } if math.is_valid_vec3(impulse) == false || math.is_valid_position(point) == false { return } w = body_world(id) body = get_body_full_id(w, id) @@ -3020,6 +3819,7 @@ body_apply_linear_impulse(id: BodyId, impulse: Vec3, point: Vec3, wake: bool) { } body_apply_linear_impulse_to_center(id: BodyId, impulse: Vec3, wake: bool) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_BODY_APPLY_LINEAR_IMPULSE_TO_CENTER); rec_id(recorder, id.index1, id.generation); rec_vec3(recorder, impulse); rec_bool(recorder, wake); rec_end(recorder) } if math.is_valid_vec3(impulse) == false { return } w = body_world(id) body = get_body_full_id(w, id) @@ -3031,6 +3831,7 @@ body_apply_linear_impulse_to_center(id: BodyId, impulse: Vec3, wake: bool) { // The angular impulse through the local inertia, so it is exact under rotation. body_apply_angular_impulse(id: BodyId, impulse: Vec3, wake: bool) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_BODY_APPLY_ANGULAR_IMPULSE); rec_id(recorder, id.index1, id.generation); rec_vec3(recorder, impulse); rec_bool(recorder, wake); rec_end(recorder) } if math.is_valid_vec3(impulse) == false { return } w = body_world(id) body = get_body_full_id(w, id) @@ -3043,6 +3844,7 @@ body_apply_angular_impulse(id: BodyId, impulse: Vec3, wake: bool) { } body_set_bullet(id: BodyId, flag: bool) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_BODY_SET_BULLET); rec_id(recorder, id.index1, id.generation); rec_bool(recorder, flag); rec_end(recorder) } w = body_world(id) if w == null { return } body = get_body_full_id(w, id) @@ -3069,7 +3871,10 @@ set_body_flag(id: BodyId, bit: int, flag: bool) { // Fast rotation (b3Body_AllowFastRotation): a body allowed it is not capped // at the angular speed that keeps a step's turn small. -body_allow_fast_rotation(id: BodyId, flag: bool) { set_body_flag(id, ALLOW_FAST_ROTATION, flag) } +body_allow_fast_rotation(id: BodyId, flag: bool) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_BODY_ALLOW_FAST_ROTATION); rec_id(recorder, id.index1, id.generation); rec_bool(recorder, flag); rec_end(recorder) } + set_body_flag(id, ALLOW_FAST_ROTATION, flag) +} body_is_fast_rotation_allowed(id: BodyId) -> bool { return (get_body_full_id(body_world(id), id).flags & ALLOW_FAST_ROTATION) != 0 } // Contact recycling (b3Body_EnableContactRecycling): a resting contact's @@ -3077,6 +3882,7 @@ body_is_fast_rotation_allowed(id: BodyId) -> bool { return (get_body_full_id(bod // when it is made, so the body's contacts are brought in line here (the // reference's setter leaves them: one switched off went on recycling). body_enable_contact_recycling(id: BodyId, flag: bool) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_BODY_ENABLE_CONTACT_RECYCLING); rec_id(recorder, id.index1, id.generation); rec_bool(recorder, flag); rec_end(recorder) } set_body_flag(id, ENABLE_CONTACT_RECYCLING, flag) w = body_world(id) if w == null { return } @@ -3095,6 +3901,7 @@ body_is_contact_recycling_enabled(id: BodyId) -> bool { return (get_body_full_id // The share of its smallest extent a body may move in a step before the // continuous pass sweeps it (b3Body_SetSafetyFactor). body_set_safety_factor(id: BodyId, factor: float) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_BODY_SET_SAFETY_FACTOR); rec_id(recorder, id.index1, id.generation); rec_float(recorder, factor); rec_end(recorder) } if valid_nonnegative(factor) == false { return } w = body_world(id) if w == null { return } @@ -3103,6 +3910,7 @@ body_set_safety_factor(id: BodyId, factor: float) { body_get_safety_factor(id: BodyId) -> float { return get_body_full_id(body_world(id), id).safety_factor } body_set_angular_velocity(id: BodyId, v: Vec3) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_BODY_SET_ANGULAR_VELOCITY); rec_id(recorder, id.index1, id.generation); rec_vec3(recorder, v); rec_end(recorder) } if math.is_valid_vec3(v) == false { return } w = body_world(id) body = get_body_full_id(w, id) @@ -3126,8 +3934,8 @@ body_get_local_point_velocity(id: BodyId, local_point: Vec3) -> Vec3 { state = get_body_state(w, body) if state == null { return math.vec3_zero() } sim = get_body_sim(w, body) - r = math.rotate_vector(sim.transform.q, math.sub(local_point, sim.local_center)) - return math.add(state.linear_velocity, math.cross(state.angular_velocity, r)) + r = math.rotate_vector(sim.transform.q, math.sub(local_point, math.widen3(sim.local_center))) + return math.add(math.widen3(state.v), math.cross(math.widen3(state.w), r)) } body_get_world_point_velocity(id: BodyId, world_point: Vec3) -> Vec3 { @@ -3158,6 +3966,7 @@ body_get_max_extent_origin(id: BodyId) -> Vec3 { // world inertia under the body's rotation, the velocity re-referenced // to the new centre, the extents about it; fixed rotation zeroes the inertia. body_set_mass_data(id: BodyId, mass_data: MassData) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_BODY_SET_MASS_DATA); rec_id(recorder, id.index1, id.generation); rec_mass_data_value(recorder, mass_data); rec_end(recorder) } if valid_nonnegative(mass_data.mass) == false || math.is_valid_matrix3(mass_data.inertia) == false || math.is_valid_vec3(mass_data.center) == false { return } w = body_world(id) if w.locked { return } @@ -3211,24 +4020,28 @@ body_get_mass_data(id: BodyId) -> MassData { } body_apply_mass_from_shapes(id: BodyId) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_BODY_APPLY_MASS_FROM_SHAPES); rec_id(recorder, id.index1, id.generation); rec_end(recorder) } w = body_world(id) if w.locked { return } update_body_mass_data(w, get_body_full_id(w, id)) } body_set_linear_damping(id: BodyId, damping: float) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_BODY_SET_LINEAR_DAMPING); rec_id(recorder, id.index1, id.generation); rec_float(recorder, damping); rec_end(recorder) } if valid_nonnegative(damping) == false { return } w = body_world(id) get_body_sim(w, get_body_full_id(w, id)).linear_damping = damping } body_get_linear_damping(id: BodyId) -> float { w = body_world(id); return get_body_sim(w, get_body_full_id(w, id)).linear_damping } body_set_angular_damping(id: BodyId, damping: float) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_BODY_SET_ANGULAR_DAMPING); rec_id(recorder, id.index1, id.generation); rec_float(recorder, damping); rec_end(recorder) } if valid_nonnegative(damping) == false { return } w = body_world(id) get_body_sim(w, get_body_full_id(w, id)).angular_damping = damping } body_get_angular_damping(id: BodyId) -> float { w = body_world(id); return get_body_sim(w, get_body_full_id(w, id)).angular_damping } body_set_gravity_scale(id: BodyId, scale: float) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_BODY_SET_GRAVITY_SCALE); rec_id(recorder, id.index1, id.generation); rec_float(recorder, scale); rec_end(recorder) } if math.is_valid_float(scale) == false { return } w = body_world(id) get_body_sim(w, get_body_full_id(w, id)).gravity_scale = scale @@ -3241,6 +4054,7 @@ body_is_enabled(id: BodyId) -> bool { return get_body_full_id(body_world(id), id body_is_sleep_enabled(id: BodyId) -> bool { return (get_body_full_id(body_world(id), id).flags & ENABLE_SLEEP) != 0 } body_get_sleep_threshold(id: BodyId) -> float { return get_body_full_id(body_world(id), id).sleep_threshold } body_set_sleep_threshold(id: BodyId, threshold: float) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_BODY_SET_SLEEP_THRESHOLD); rec_id(recorder, id.index1, id.generation); rec_float(recorder, threshold); rec_end(recorder) } if valid_nonnegative(threshold) == false { return } get_body_full_id(body_world(id), id).sleep_threshold = threshold } @@ -3251,6 +4065,7 @@ body_get_flags(id: BodyId) -> int { return get_body_full_id(body_world(id), id). // (b3Body_SetAwake): split first if a constraint left it, so only what // is still connected sleeps. body_set_awake(id: BodyId, awake: bool) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_BODY_SET_AWAKE); rec_id(recorder, id.index1, id.generation); rec_bool(recorder, awake); rec_end(recorder) } w = body_world(id) if w.locked { return } w.locked = true @@ -3265,6 +4080,7 @@ body_set_awake(id: BodyId, awake: bool) { } body_enable_sleep(id: BodyId, enable: bool) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_BODY_ENABLE_SLEEP); rec_id(recorder, id.index1, id.generation); rec_bool(recorder, enable); rec_end(recorder) } w = body_world(id) body = get_body_full_id(w, id) // Unchanged is nothing to do: a repeated disable does not wake the body. @@ -3279,6 +4095,7 @@ body_enable_sleep(id: BodyId, enable: bool) { } body_set_motion_locks(id: BodyId, locks: MotionLocks) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_BODY_SET_MOTION_LOCKS); rec_id(recorder, id.index1, id.generation); rec_motion_locks_value(recorder, locks); rec_end(recorder) } w = body_world(id) body = get_body_full_id(w, id) flags = body.flags & (0 - 1 - ALL_LOCKS) @@ -5571,6 +6388,17 @@ valid_spring(hertz: float, damping_ratio: float) -> bool { return valid_nonnegat valid_range(lower: float, upper: float) -> bool { return math.is_valid_float(lower) && math.is_valid_float(upper) && lower <= upper } create_distance_joint(world_id: WorldId, def: *DistanceJointDef) -> JointId { + if recording_on(world_id.index1 - 1) == false { return create_distance_joint_internal(world_id, def) } + recorder = rec_begin(world_id.index1 - 1, OP_CREATE_DISTANCE_JOINT) + rec_distance_joint_def(recorder, def) + made = create_distance_joint_internal(world_id, def) + rec_id(recorder, made.index1, made.generation) + rec_end(recorder) + return made +} + +// create_distance_joint's work, unrecorded. +create_distance_joint_internal(world_id: WorldId, def: *DistanceJointDef) -> JointId { w = joint_world(world_id, &def.base) if w == null { return null_joint_id() } if math.is_valid_float(def.length) == false || def.length <= 0.0 || valid_range(def.lower_spring_force, def.upper_spring_force) == false { return null_joint_id() } @@ -5596,6 +6424,17 @@ create_distance_joint(world_id: WorldId, def: *DistanceJointDef) -> JointId { } create_motor_joint(world_id: WorldId, def: *MotorJointDef) -> JointId { + if recording_on(world_id.index1 - 1) == false { return create_motor_joint_internal(world_id, def) } + recorder = rec_begin(world_id.index1 - 1, OP_CREATE_MOTOR_JOINT) + rec_motor_joint_def(recorder, def) + made = create_motor_joint_internal(world_id, def) + rec_id(recorder, made.index1, made.generation) + rec_end(recorder) + return made +} + +// create_motor_joint's work, unrecorded. +create_motor_joint_internal(world_id: WorldId, def: *MotorJointDef) -> JointId { w = joint_world(world_id, &def.base) if w == null { return null_joint_id() } if math.is_valid_vec3(def.linear_velocity) == false || math.is_valid_vec3(def.angular_velocity) == false { return null_joint_id() } @@ -5620,6 +6459,17 @@ create_motor_joint(world_id: WorldId, def: *MotorJointDef) -> JointId { } create_filter_joint(world_id: WorldId, def: *FilterJointDef) -> JointId { + if recording_on(world_id.index1 - 1) == false { return create_filter_joint_internal(world_id, def) } + recorder = rec_begin(world_id.index1 - 1, OP_CREATE_FILTER_JOINT) + rec_filter_joint_def(recorder, def) + made = create_filter_joint_internal(world_id, def) + rec_id(recorder, made.index1, made.generation) + rec_end(recorder) + return made +} + +// create_filter_joint's work, unrecorded. +create_filter_joint_internal(world_id: WorldId, def: *FilterJointDef) -> JointId { w = joint_world(world_id, &def.base) if w == null { return null_joint_id() } w.locked = true @@ -5629,6 +6479,17 @@ create_filter_joint(world_id: WorldId, def: *FilterJointDef) -> JointId { } create_parallel_joint(world_id: WorldId, def: *ParallelJointDef) -> JointId { + if recording_on(world_id.index1 - 1) == false { return create_parallel_joint_internal(world_id, def) } + recorder = rec_begin(world_id.index1 - 1, OP_CREATE_PARALLEL_JOINT) + rec_parallel_joint_def(recorder, def) + made = create_parallel_joint_internal(world_id, def) + rec_id(recorder, made.index1, made.generation) + rec_end(recorder) + return made +} + +// create_parallel_joint's work, unrecorded. +create_parallel_joint_internal(world_id: WorldId, def: *ParallelJointDef) -> JointId { w = joint_world(world_id, &def.base) if w == null { return null_joint_id() } if valid_spring(def.hertz, def.damping_ratio) == false || valid_nonnegative(def.max_torque) == false { return null_joint_id() } @@ -5643,6 +6504,17 @@ create_parallel_joint(world_id: WorldId, def: *ParallelJointDef) -> JointId { } create_prismatic_joint(world_id: WorldId, def: *PrismaticJointDef) -> JointId { + if recording_on(world_id.index1 - 1) == false { return create_prismatic_joint_internal(world_id, def) } + recorder = rec_begin(world_id.index1 - 1, OP_CREATE_PRISMATIC_JOINT) + rec_prismatic_joint_def(recorder, def) + made = create_prismatic_joint_internal(world_id, def) + rec_id(recorder, made.index1, made.generation) + rec_end(recorder) + return made +} + +// create_prismatic_joint's work, unrecorded. +create_prismatic_joint_internal(world_id: WorldId, def: *PrismaticJointDef) -> JointId { w = joint_world(world_id, &def.base) if w == null { return null_joint_id() } if valid_range(def.lower_translation, def.upper_translation) == false || math.is_valid_float(def.target_translation) == false { return null_joint_id() } @@ -5665,6 +6537,17 @@ create_prismatic_joint(world_id: WorldId, def: *PrismaticJointDef) -> JointId { } create_revolute_joint(world_id: WorldId, def: *RevoluteJointDef) -> JointId { + if recording_on(world_id.index1 - 1) == false { return create_revolute_joint_internal(world_id, def) } + recorder = rec_begin(world_id.index1 - 1, OP_CREATE_REVOLUTE_JOINT) + rec_revolute_joint_def(recorder, def) + made = create_revolute_joint_internal(world_id, def) + rec_id(recorder, made.index1, made.generation) + rec_end(recorder) + return made +} + +// create_revolute_joint's work, unrecorded. +create_revolute_joint_internal(world_id: WorldId, def: *RevoluteJointDef) -> JointId { w = joint_world(world_id, &def.base) if w == null { return null_joint_id() } if math.is_valid_float(def.lower_angle) == false || math.is_valid_float(def.upper_angle) == false || math.is_valid_float(def.target_angle) == false { return null_joint_id() } @@ -5690,6 +6573,17 @@ create_revolute_joint(world_id: WorldId, def: *RevoluteJointDef) -> JointId { } create_spherical_joint(world_id: WorldId, def: *SphericalJointDef) -> JointId { + if recording_on(world_id.index1 - 1) == false { return create_spherical_joint_internal(world_id, def) } + recorder = rec_begin(world_id.index1 - 1, OP_CREATE_SPHERICAL_JOINT) + rec_spherical_joint_def(recorder, def) + made = create_spherical_joint_internal(world_id, def) + rec_id(recorder, made.index1, made.generation) + rec_end(recorder) + return made +} + +// create_spherical_joint's work, unrecorded. +create_spherical_joint_internal(world_id: WorldId, def: *SphericalJointDef) -> JointId { w = joint_world(world_id, &def.base) if w == null { return null_joint_id() } pi = math.PI @@ -5835,6 +6729,17 @@ spherical_cone_push_axis(s: *SphericalJoint, frame_a_q: Quat, twist_axis: Vec3) } create_weld_joint(world_id: WorldId, def: *WeldJointDef) -> JointId { + if recording_on(world_id.index1 - 1) == false { return create_weld_joint_internal(world_id, def) } + recorder = rec_begin(world_id.index1 - 1, OP_CREATE_WELD_JOINT) + rec_weld_joint_def(recorder, def) + made = create_weld_joint_internal(world_id, def) + rec_id(recorder, made.index1, made.generation) + rec_end(recorder) + return made +} + +// create_weld_joint's work, unrecorded. +create_weld_joint_internal(world_id: WorldId, def: *WeldJointDef) -> JointId { w = joint_world(world_id, &def.base) if w == null { return null_joint_id() } if valid_spring(def.angular_hertz, def.angular_damping_ratio) == false || valid_spring(def.linear_hertz, def.linear_damping_ratio) == false { return null_joint_id() } @@ -5850,6 +6755,17 @@ create_weld_joint(world_id: WorldId, def: *WeldJointDef) -> JointId { } create_wheel_joint(world_id: WorldId, def: *WheelJointDef) -> JointId { + if recording_on(world_id.index1 - 1) == false { return create_wheel_joint_internal(world_id, def) } + recorder = rec_begin(world_id.index1 - 1, OP_CREATE_WHEEL_JOINT) + rec_wheel_joint_def(recorder, def) + made = create_wheel_joint_internal(world_id, def) + rec_id(recorder, made.index1, made.generation) + rec_end(recorder) + return made +} + +// create_wheel_joint's work, unrecorded. +create_wheel_joint_internal(world_id: WorldId, def: *WheelJointDef) -> JointId { w = joint_world(world_id, &def.base) if w == null { return null_joint_id() } if valid_range(def.lower_suspension_limit, def.upper_suspension_limit) == false { return null_joint_id() } @@ -5922,6 +6838,7 @@ destroy_joint_internal(w: *World, j: *Joint, wake_bodies: bool) { } destroy_joint(id: JointId, wake_attached: bool) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_DESTROY_JOINT); rec_id(recorder, id.index1, id.generation); rec_bool(recorder, wake_attached); rec_end(recorder) } if joint_is_valid(id) == false { return } w = world_at(id.world0) if w.locked { return } @@ -5946,6 +6863,7 @@ joint_get_world(id: JointId) -> WorldId { return WorldId { index1: id.world0 + 1, generation: w.generation } } joint_set_local_frame_a(id: JointId, frame: Transform) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_JOINT_SET_LOCAL_FRAME_A); rec_id(recorder, id.index1, id.generation); rec_transform(recorder, frame); rec_end(recorder) } if math.is_valid_transform(frame) == false { return } w = world_at(id.world0) get_joint_sim(w, get_joint_full_id(w, id)).local_frame_a = frame @@ -5955,6 +6873,7 @@ joint_get_local_frame_a(id: JointId) -> Transform { return get_joint_sim(w, get_joint_full_id(w, id)).local_frame_a } joint_set_local_frame_b(id: JointId, frame: Transform) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_JOINT_SET_LOCAL_FRAME_B); rec_id(recorder, id.index1, id.generation); rec_transform(recorder, frame); rec_end(recorder) } if math.is_valid_transform(frame) == false { return } w = world_at(id.world0) get_joint_sim(w, get_joint_full_id(w, id)).local_frame_b = frame @@ -5969,6 +6888,7 @@ joint_get_collide_connected(id: JointId) -> bool { return get_joint_full_id(worl joint_is_awake(id: JointId) -> bool { return get_joint_full_id(world_at(id.world0), id).set_index == AWAKE_SET } joint_set_constraint_tuning(id: JointId, hertz: float, damping_ratio: float) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_JOINT_SET_CONSTRAINT_TUNING); rec_id(recorder, id.index1, id.generation); rec_float(recorder, hertz); rec_float(recorder, damping_ratio); rec_end(recorder) } if valid_spring(hertz, damping_ratio) == false { return } w = world_at(id.world0) sim = get_joint_sim(w, get_joint_full_id(w, id)) @@ -5984,6 +6904,7 @@ joint_get_constraint_damping_ratio(id: JointId) -> float { return get_joint_sim(w, get_joint_full_id(w, id)).constraint_damping_ratio } joint_set_force_threshold(id: JointId, threshold: float) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_JOINT_SET_FORCE_THRESHOLD); rec_id(recorder, id.index1, id.generation); rec_float(recorder, threshold); rec_end(recorder) } if valid_nonnegative(threshold) == false { return } w = world_at(id.world0) get_joint_sim(w, get_joint_full_id(w, id)).force_threshold = threshold @@ -5993,6 +6914,7 @@ joint_get_force_threshold(id: JointId) -> float { return get_joint_sim(w, get_joint_full_id(w, id)).force_threshold } joint_set_torque_threshold(id: JointId, threshold: float) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_JOINT_SET_TORQUE_THRESHOLD); rec_id(recorder, id.index1, id.generation); rec_float(recorder, threshold); rec_end(recorder) } if valid_nonnegative(threshold) == false { return } w = world_at(id.world0) get_joint_sim(w, get_joint_full_id(w, id)).torque_threshold = threshold @@ -6006,6 +6928,7 @@ joint_get_torque_threshold(id: JointId) -> float { // phase for their pairs again (the body with fewer shapes is marked // moved), turning it off destroys the contacts between them. joint_set_collide_connected(id: JointId, should_collide: bool) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_JOINT_SET_COLLIDE_CONNECTED); rec_id(recorder, id.index1, id.generation); rec_bool(recorder, should_collide); rec_end(recorder) } w = world_at(id.world0) if w.locked { return } j = get_joint_full_id(w, id) @@ -6027,6 +6950,7 @@ joint_set_collide_connected(id: JointId, should_collide: bool) { } joint_wake_bodies(id: JointId) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_JOINT_WAKE_BODIES); rec_id(recorder, id.index1, id.generation); rec_end(recorder) } w = world_at(id.world0) if w.locked { return } w.locked = true @@ -6378,6 +7302,7 @@ merge_solver_sets(w: *World, set_id1: int, set_id2: int) { // right tree, the joints relinked. Refused for compound and height // field shapes on a body that would move. body_set_type(id: BodyId, kind: int) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_BODY_SET_TYPE); rec_id(recorder, id.index1, id.generation); rec_int(recorder, kind); rec_end(recorder) } if body_is_valid(id) == false { return } w = world_at(id.world0) if w.locked { return } @@ -6464,6 +7389,7 @@ body_set_type(id: BodyId, kind: int) { // bodies woken), its joints unlinked and parked in the disabled set with // it, its proxies removed, its island left. body_disable(id: BodyId) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_BODY_DISABLE); rec_id(recorder, id.index1, id.generation); rec_end(recorder) } if body_is_valid(id) == false { return } w = world_at(id.world0) if w.locked { return } @@ -6505,6 +7431,7 @@ body_disable(id: BodyId) { // island of its own, its joints whose other body is enabled into the // right set and linked (a sleeping other body wakes through the link). body_enable(id: BodyId) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_BODY_ENABLE); rec_id(recorder, id.index1, id.generation); rec_end(recorder) } if body_is_valid(id) == false { return } w = world_at(id.world0) if w.locked { return } @@ -6661,6 +7588,7 @@ destroy_sensor(w: *World, s: *WorldShape) { } shape_enable_sensor_events(id: ShapeId, flag: bool) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_SHAPE_ENABLE_SENSOR_EVENTS); rec_id(recorder, id.index1, id.generation); rec_bool(recorder, flag); rec_end(recorder) } s = get_shape_full_id(world_at(id.world0), id) if flag { s.flags = s.flags | SHAPE_ENABLE_SENSOR_EVENTS } else { s.flags = s.flags & (0 - 1 - SHAPE_ENABLE_SENSOR_EVENTS) } } @@ -6668,12 +7596,14 @@ shape_are_sensor_events_enabled(id: ShapeId) -> bool { return (get_shape_full_id // Contact and hit events on a shape after its creation (b3Shape_EnableContactEvents, b3Shape_EnableHitEvents). shape_enable_contact_events(id: ShapeId, flag: bool) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_SHAPE_ENABLE_CONTACT_EVENTS); rec_id(recorder, id.index1, id.generation); rec_bool(recorder, flag); rec_end(recorder) } s = get_shape_full_id(world_at(id.world0), id) if flag { s.flags = s.flags | SHAPE_ENABLE_CONTACT_EVENTS } else { s.flags = s.flags & (0 - 1 - SHAPE_ENABLE_CONTACT_EVENTS) } } shape_are_contact_events_enabled(id: ShapeId) -> bool { return (get_shape_full_id(world_at(id.world0), id).flags & SHAPE_ENABLE_CONTACT_EVENTS) != 0 } // Hit events on every shape of a body (b3Body_EnableHitEvents). body_enable_hit_events(id: BodyId, flag: bool) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_BODY_ENABLE_HIT_EVENTS); rec_id(recorder, id.index1, id.generation); rec_bool(recorder, flag); rec_end(recorder) } w = world_at(id.world0) shape_id = get_body_full_id(w, id).head_shape_id while shape_id != NULL_INDEX { @@ -6683,6 +7613,7 @@ body_enable_hit_events(id: BodyId, flag: bool) { } } shape_enable_hit_events(id: ShapeId, flag: bool) { + if recording_on(id.world0) { recorder = rec_begin(id.world0, OP_SHAPE_ENABLE_HIT_EVENTS); rec_id(recorder, id.index1, id.generation); rec_bool(recorder, flag); rec_end(recorder) } s = get_shape_full_id(world_at(id.world0), id) if flag { s.flags = s.flags | SHAPE_ENABLE_HIT_EVENTS } else { s.flags = s.flags & (0 - 1 - SHAPE_ENABLE_HIT_EVENTS) } } diff --git a/aephysics/joint_solver/module.ae b/aephysics/joint_solver/module.ae index 5094c47..ade8f7b 100644 --- a/aephysics/joint_solver/module.ae +++ b/aephysics/joint_solver/module.ae @@ -390,6 +390,7 @@ get_distance_joint_force(w: *World, base: *JointSim) -> Vec3 { } distance_joint_set_length(id: JointId, length: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_DISTANCE_JOINT_SET_LENGTH); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, length); dynamics.rec_end(recorder) } if nonnegative(length) == false { return } base = sim_of(id, dynamics.JOINT_DISTANCE) if base == null { return } @@ -401,11 +402,13 @@ distance_joint_set_length(id: JointId, length: float) { } distance_joint_get_length(id: JointId) -> float { return dynamics.distance_joint_of(sim_of(id, dynamics.JOINT_DISTANCE)).length } distance_joint_enable_limit(id: JointId, flag: bool) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_DISTANCE_JOINT_ENABLE_LIMIT); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } base = sim_of(id, dynamics.JOINT_DISTANCE) if base != null { dynamics.distance_joint_of(base).enable_limit = flag } } distance_joint_is_limit_enabled(id: JointId) -> bool { return dynamics.distance_joint_of(sim_of(id, dynamics.JOINT_DISTANCE)).enable_limit } distance_joint_set_length_range(id: JointId, min_length: float, max_length: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_DISTANCE_JOINT_SET_LENGTH_RANGE); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, min_length); dynamics.rec_float(recorder, max_length); dynamics.rec_end(recorder) } if math.is_valid_float(min_length) == false || math.is_valid_float(max_length) == false { return } base = sim_of(id, dynamics.JOINT_DISTANCE) if base == null { return } @@ -429,11 +432,13 @@ distance_joint_get_current_length(id: JointId) -> float { return math.distance(math.transform_point(xf_b, base.local_frame_b.p), math.transform_point(xf_a, base.local_frame_a.p)) } distance_joint_enable_spring(id: JointId, flag: bool) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_DISTANCE_JOINT_ENABLE_SPRING); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } base = sim_of(id, dynamics.JOINT_DISTANCE) if base != null { dynamics.distance_joint_of(base).enable_spring = flag } } distance_joint_is_spring_enabled(id: JointId) -> bool { return dynamics.distance_joint_of(sim_of(id, dynamics.JOINT_DISTANCE)).enable_spring } distance_joint_set_spring_force_range(id: JointId, lower: float, upper: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_DISTANCE_JOINT_SET_SPRING_FORCE_RANGE); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, lower); dynamics.rec_float(recorder, upper); dynamics.rec_end(recorder) } if math.is_valid_float(lower) == false || math.is_valid_float(upper) == false { return } base = sim_of(id, dynamics.JOINT_DISTANCE) if base == null { return } @@ -444,11 +449,13 @@ distance_joint_set_spring_force_range(id: JointId, lower: float, upper: float) { distance_joint_get_lower_spring_force(id: JointId) -> float { return dynamics.distance_joint_of(sim_of(id, dynamics.JOINT_DISTANCE)).lower_spring_force } distance_joint_get_upper_spring_force(id: JointId) -> float { return dynamics.distance_joint_of(sim_of(id, dynamics.JOINT_DISTANCE)).upper_spring_force } distance_joint_set_spring_hertz(id: JointId, hertz: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_DISTANCE_JOINT_SET_SPRING_HERTZ); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, hertz); dynamics.rec_end(recorder) } if nonnegative(hertz) == false { return } base = sim_of(id, dynamics.JOINT_DISTANCE) if base != null { dynamics.distance_joint_of(base).hertz = hertz } } distance_joint_set_spring_damping_ratio(id: JointId, ratio: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_DISTANCE_JOINT_SET_SPRING_DAMPING_RATIO); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, ratio); dynamics.rec_end(recorder) } if nonnegative(ratio) == false { return } base = sim_of(id, dynamics.JOINT_DISTANCE) if base != null { dynamics.distance_joint_of(base).damping_ratio = ratio } @@ -456,6 +463,7 @@ distance_joint_set_spring_damping_ratio(id: JointId, ratio: float) { distance_joint_get_spring_hertz(id: JointId) -> float { return dynamics.distance_joint_of(sim_of(id, dynamics.JOINT_DISTANCE)).hertz } distance_joint_get_spring_damping_ratio(id: JointId) -> float { return dynamics.distance_joint_of(sim_of(id, dynamics.JOINT_DISTANCE)).damping_ratio } distance_joint_enable_motor(id: JointId, flag: bool) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_DISTANCE_JOINT_ENABLE_MOTOR); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } base = sim_of(id, dynamics.JOINT_DISTANCE) if base == null { return } j = dynamics.distance_joint_of(base) @@ -466,6 +474,7 @@ distance_joint_enable_motor(id: JointId, flag: bool) { } distance_joint_is_motor_enabled(id: JointId) -> bool { return dynamics.distance_joint_of(sim_of(id, dynamics.JOINT_DISTANCE)).enable_motor } distance_joint_set_motor_speed(id: JointId, speed: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_DISTANCE_JOINT_SET_MOTOR_SPEED); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, speed); dynamics.rec_end(recorder) } if math.is_valid_float(speed) == false { return } base = sim_of(id, dynamics.JOINT_DISTANCE) if base != null { dynamics.distance_joint_of(base).motor_speed = speed } @@ -477,6 +486,7 @@ distance_joint_get_motor_force(id: JointId) -> float { return dynamics.get_world(id.world0).inv_h * dynamics.distance_joint_of(base).motor_impulse } distance_joint_set_max_motor_force(id: JointId, force: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_DISTANCE_JOINT_SET_MAX_MOTOR_FORCE); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, force); dynamics.rec_end(recorder) } if nonnegative(force) == false { return } base = sim_of(id, dynamics.JOINT_DISTANCE) if base != null { dynamics.distance_joint_of(base).max_motor_force = force } @@ -640,60 +650,70 @@ get_motor_joint_torque(w: *World, base: *JointSim) -> Vec3 { } motor_joint_set_linear_velocity(id: JointId, v: Vec3) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_MOTOR_JOINT_SET_LINEAR_VELOCITY); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_vec3(recorder, v); dynamics.rec_end(recorder) } if math.is_valid_vec3(v) == false { return } base = sim_of(id, dynamics.JOINT_MOTOR) if base != null { dynamics.motor_joint_of(base).linear_velocity = v } } motor_joint_get_linear_velocity(id: JointId) -> Vec3 { return dynamics.motor_joint_of(sim_of(id, dynamics.JOINT_MOTOR)).linear_velocity } motor_joint_set_angular_velocity(id: JointId, v: Vec3) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_MOTOR_JOINT_SET_ANGULAR_VELOCITY); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_vec3(recorder, v); dynamics.rec_end(recorder) } if math.is_valid_vec3(v) == false { return } base = sim_of(id, dynamics.JOINT_MOTOR) if base != null { dynamics.motor_joint_of(base).angular_velocity = v } } motor_joint_get_angular_velocity(id: JointId) -> Vec3 { return dynamics.motor_joint_of(sim_of(id, dynamics.JOINT_MOTOR)).angular_velocity } motor_joint_set_max_velocity_torque(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_MOTOR_JOINT_SET_MAX_VELOCITY_TORQUE); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_MOTOR) if base != null { dynamics.motor_joint_of(base).max_velocity_torque = value } } motor_joint_get_max_velocity_torque(id: JointId) -> float { return dynamics.motor_joint_of(sim_of(id, dynamics.JOINT_MOTOR)).max_velocity_torque } motor_joint_set_max_velocity_force(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_MOTOR_JOINT_SET_MAX_VELOCITY_FORCE); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_MOTOR) if base != null { dynamics.motor_joint_of(base).max_velocity_force = value } } motor_joint_get_max_velocity_force(id: JointId) -> float { return dynamics.motor_joint_of(sim_of(id, dynamics.JOINT_MOTOR)).max_velocity_force } motor_joint_set_linear_hertz(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_MOTOR_JOINT_SET_LINEAR_HERTZ); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_MOTOR) if base != null { dynamics.motor_joint_of(base).linear_hertz = value } } motor_joint_get_linear_hertz(id: JointId) -> float { return dynamics.motor_joint_of(sim_of(id, dynamics.JOINT_MOTOR)).linear_hertz } motor_joint_set_linear_damping_ratio(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_MOTOR_JOINT_SET_LINEAR_DAMPING_RATIO); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_MOTOR) if base != null { dynamics.motor_joint_of(base).linear_damping_ratio = value } } motor_joint_get_linear_damping_ratio(id: JointId) -> float { return dynamics.motor_joint_of(sim_of(id, dynamics.JOINT_MOTOR)).linear_damping_ratio } motor_joint_set_angular_hertz(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_MOTOR_JOINT_SET_ANGULAR_HERTZ); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_MOTOR) if base != null { dynamics.motor_joint_of(base).angular_hertz = value } } motor_joint_get_angular_hertz(id: JointId) -> float { return dynamics.motor_joint_of(sim_of(id, dynamics.JOINT_MOTOR)).angular_hertz } motor_joint_set_angular_damping_ratio(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_MOTOR_JOINT_SET_ANGULAR_DAMPING_RATIO); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_MOTOR) if base != null { dynamics.motor_joint_of(base).angular_damping_ratio = value } } motor_joint_get_angular_damping_ratio(id: JointId) -> float { return dynamics.motor_joint_of(sim_of(id, dynamics.JOINT_MOTOR)).angular_damping_ratio } motor_joint_set_max_spring_force(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_MOTOR_JOINT_SET_MAX_SPRING_FORCE); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_MOTOR) if base != null { dynamics.motor_joint_of(base).max_spring_force = value } } motor_joint_get_max_spring_force(id: JointId) -> float { return dynamics.motor_joint_of(sim_of(id, dynamics.JOINT_MOTOR)).max_spring_force } motor_joint_set_max_spring_torque(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_MOTOR_JOINT_SET_MAX_SPRING_TORQUE); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_MOTOR) if base != null { dynamics.motor_joint_of(base).max_spring_torque = value } @@ -783,12 +803,14 @@ get_parallel_joint_torque(w: *World, base: *JointSim) -> Vec3 { } parallel_joint_set_spring_hertz(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_PARALLEL_JOINT_SET_SPRING_HERTZ); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_PARALLEL) if base != null { dynamics.parallel_joint_of(base).hertz = value } } parallel_joint_get_spring_hertz(id: JointId) -> float { return dynamics.parallel_joint_of(sim_of(id, dynamics.JOINT_PARALLEL)).hertz } parallel_joint_set_spring_damping_ratio(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_PARALLEL_JOINT_SET_SPRING_DAMPING_RATIO); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_PARALLEL) if base != null { dynamics.parallel_joint_of(base).damping_ratio = value } @@ -802,6 +824,7 @@ parallel_joint_get_spring_damping_ratio(id: JointId) -> float { return dynamics. // figure's pelvis anchor, let go as it fell, turned the lying figure's // pelvis and legs for good). Cleared here; nothing else differs from it. parallel_joint_set_max_torque(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_PARALLEL_JOINT_SET_MAX_TORQUE); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_PARALLEL) if base != null { @@ -1032,6 +1055,7 @@ get_prismatic_joint_torque(w: *World, base: *JointSim) -> Vec3 { } prismatic_joint_enable_spring(id: JointId, flag: bool) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_PRISMATIC_JOINT_ENABLE_SPRING); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } base = sim_of(id, dynamics.JOINT_PRISMATIC) if base == null { return } j = dynamics.prismatic_joint_of(base) @@ -1040,24 +1064,28 @@ prismatic_joint_enable_spring(id: JointId, flag: bool) { } prismatic_joint_is_spring_enabled(id: JointId) -> bool { return dynamics.prismatic_joint_of(sim_of(id, dynamics.JOINT_PRISMATIC)).enable_spring } prismatic_joint_set_spring_hertz(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_PRISMATIC_JOINT_SET_SPRING_HERTZ); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_PRISMATIC) if base != null { dynamics.prismatic_joint_of(base).hertz = value } } prismatic_joint_get_spring_hertz(id: JointId) -> float { return dynamics.prismatic_joint_of(sim_of(id, dynamics.JOINT_PRISMATIC)).hertz } prismatic_joint_set_spring_damping_ratio(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_PRISMATIC_JOINT_SET_SPRING_DAMPING_RATIO); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_PRISMATIC) if base != null { dynamics.prismatic_joint_of(base).damping_ratio = value } } prismatic_joint_get_spring_damping_ratio(id: JointId) -> float { return dynamics.prismatic_joint_of(sim_of(id, dynamics.JOINT_PRISMATIC)).damping_ratio } prismatic_joint_set_target_translation(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_PRISMATIC_JOINT_SET_TARGET_TRANSLATION); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if math.is_valid_float(value) == false { return } base = sim_of(id, dynamics.JOINT_PRISMATIC) if base != null { dynamics.prismatic_joint_of(base).target_translation = value } } prismatic_joint_get_target_translation(id: JointId) -> float { return dynamics.prismatic_joint_of(sim_of(id, dynamics.JOINT_PRISMATIC)).target_translation } prismatic_joint_enable_limit(id: JointId, flag: bool) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_PRISMATIC_JOINT_ENABLE_LIMIT); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } base = sim_of(id, dynamics.JOINT_PRISMATIC) if base == null { return } j = dynamics.prismatic_joint_of(base) @@ -1071,6 +1099,7 @@ prismatic_joint_is_limit_enabled(id: JointId) -> bool { return dynamics.prismati prismatic_joint_get_lower_limit(id: JointId) -> float { return dynamics.prismatic_joint_of(sim_of(id, dynamics.JOINT_PRISMATIC)).lower_translation } prismatic_joint_get_upper_limit(id: JointId) -> float { return dynamics.prismatic_joint_of(sim_of(id, dynamics.JOINT_PRISMATIC)).upper_translation } prismatic_joint_set_limits(id: JointId, lower: float, upper: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_PRISMATIC_JOINT_SET_LIMITS); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, lower); dynamics.rec_float(recorder, upper); dynamics.rec_end(recorder) } if math.is_valid_float(lower) == false || math.is_valid_float(upper) == false { return } base = sim_of(id, dynamics.JOINT_PRISMATIC) if base == null { return } @@ -1091,6 +1120,7 @@ prismatic_joint_get_translation(id: JointId) -> float { return math.dot(d, axis) } prismatic_joint_enable_motor(id: JointId, flag: bool) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_PRISMATIC_JOINT_ENABLE_MOTOR); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } base = sim_of(id, dynamics.JOINT_PRISMATIC) if base == null { return } j = dynamics.prismatic_joint_of(base) @@ -1099,12 +1129,14 @@ prismatic_joint_enable_motor(id: JointId, flag: bool) { } prismatic_joint_is_motor_enabled(id: JointId) -> bool { return dynamics.prismatic_joint_of(sim_of(id, dynamics.JOINT_PRISMATIC)).enable_motor } prismatic_joint_set_motor_speed(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_PRISMATIC_JOINT_SET_MOTOR_SPEED); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if math.is_valid_float(value) == false { return } base = sim_of(id, dynamics.JOINT_PRISMATIC) if base != null { dynamics.prismatic_joint_of(base).motor_speed = value } } prismatic_joint_get_motor_speed(id: JointId) -> float { return dynamics.prismatic_joint_of(sim_of(id, dynamics.JOINT_PRISMATIC)).motor_speed } prismatic_joint_set_max_motor_force(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_PRISMATIC_JOINT_SET_MAX_MOTOR_FORCE); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_PRISMATIC) if base != null { dynamics.prismatic_joint_of(base).max_motor_force = value } @@ -1326,6 +1358,7 @@ get_revolute_joint_torque(w: *World, base: *JointSim) -> Vec3 { } revolute_joint_enable_limit(id: JointId, flag: bool) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_REVOLUTE_JOINT_ENABLE_LIMIT); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } base = sim_of(id, dynamics.JOINT_REVOLUTE) if base == null { return } j = dynamics.revolute_joint_of(base) @@ -1339,6 +1372,7 @@ revolute_joint_is_limit_enabled(id: JointId) -> bool { return dynamics.revolute_ revolute_joint_get_lower_limit(id: JointId) -> float { return dynamics.revolute_joint_of(sim_of(id, dynamics.JOINT_REVOLUTE)).lower_angle } revolute_joint_get_upper_limit(id: JointId) -> float { return dynamics.revolute_joint_of(sim_of(id, dynamics.JOINT_REVOLUTE)).upper_angle } revolute_joint_set_limits(id: JointId, lower: float, upper: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_REVOLUTE_JOINT_SET_LIMITS); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, lower); dynamics.rec_float(recorder, upper); dynamics.rec_end(recorder) } if math.is_valid_float(lower) == false || math.is_valid_float(upper) == false { return } base = sim_of(id, dynamics.JOINT_REVOLUTE) if base == null { return } @@ -1360,6 +1394,7 @@ revolute_joint_get_angle(id: JointId) -> float { return math.get_twist_angle(math.inv_mul_quat(quat_a, quat_b)) } revolute_joint_enable_spring(id: JointId, flag: bool) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_REVOLUTE_JOINT_ENABLE_SPRING); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } base = sim_of(id, dynamics.JOINT_REVOLUTE) if base == null { return } j = dynamics.revolute_joint_of(base) @@ -1368,24 +1403,28 @@ revolute_joint_enable_spring(id: JointId, flag: bool) { } revolute_joint_is_spring_enabled(id: JointId) -> bool { return dynamics.revolute_joint_of(sim_of(id, dynamics.JOINT_REVOLUTE)).enable_spring } revolute_joint_set_target_angle(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_REVOLUTE_JOINT_SET_TARGET_ANGLE); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if math.is_valid_float(value) == false { return } base = sim_of(id, dynamics.JOINT_REVOLUTE) if base != null { dynamics.revolute_joint_of(base).target_angle = value } } revolute_joint_get_target_angle(id: JointId) -> float { return dynamics.revolute_joint_of(sim_of(id, dynamics.JOINT_REVOLUTE)).target_angle } revolute_joint_set_spring_hertz(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_REVOLUTE_JOINT_SET_SPRING_HERTZ); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_REVOLUTE) if base != null { dynamics.revolute_joint_of(base).hertz = value } } revolute_joint_get_spring_hertz(id: JointId) -> float { return dynamics.revolute_joint_of(sim_of(id, dynamics.JOINT_REVOLUTE)).hertz } revolute_joint_set_spring_damping_ratio(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_REVOLUTE_JOINT_SET_SPRING_DAMPING_RATIO); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_REVOLUTE) if base != null { dynamics.revolute_joint_of(base).damping_ratio = value } } revolute_joint_get_spring_damping_ratio(id: JointId) -> float { return dynamics.revolute_joint_of(sim_of(id, dynamics.JOINT_REVOLUTE)).damping_ratio } revolute_joint_enable_motor(id: JointId, flag: bool) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_REVOLUTE_JOINT_ENABLE_MOTOR); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } base = sim_of(id, dynamics.JOINT_REVOLUTE) if base == null { return } j = dynamics.revolute_joint_of(base) @@ -1394,12 +1433,14 @@ revolute_joint_enable_motor(id: JointId, flag: bool) { } revolute_joint_is_motor_enabled(id: JointId) -> bool { return dynamics.revolute_joint_of(sim_of(id, dynamics.JOINT_REVOLUTE)).enable_motor } revolute_joint_set_motor_speed(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_REVOLUTE_JOINT_SET_MOTOR_SPEED); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if math.is_valid_float(value) == false { return } base = sim_of(id, dynamics.JOINT_REVOLUTE) if base != null { dynamics.revolute_joint_of(base).motor_speed = value } } revolute_joint_get_motor_speed(id: JointId) -> float { return dynamics.revolute_joint_of(sim_of(id, dynamics.JOINT_REVOLUTE)).motor_speed } revolute_joint_set_max_motor_torque(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_REVOLUTE_JOINT_SET_MAX_MOTOR_TORQUE); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_REVOLUTE) if base != null { dynamics.revolute_joint_of(base).max_motor_torque = value } @@ -1600,6 +1641,7 @@ spherical_relative_rotation(id: JointId, base: *JointSim) -> Quat { } spherical_joint_enable_cone_limit(id: JointId, flag: bool) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_SPHERICAL_JOINT_ENABLE_CONE_LIMIT); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } base = sim_of(id, dynamics.JOINT_SPHERICAL) if base == null { return } j = dynamics.spherical_joint_of(base) @@ -1611,6 +1653,7 @@ spherical_joint_get_cone_limit(id: JointId) -> float { return dynamics.spherical // Clamped as the joint's creation clamps it: to a right angle about frame // A's z, to 0.8 pi about a centre off it. spherical_joint_set_cone_limit(id: JointId, angle: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_SPHERICAL_JOINT_SET_CONE_LIMIT); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, angle); dynamics.rec_end(recorder) } if math.is_valid_float(angle) == false { return } base = sim_of(id, dynamics.JOINT_SPHERICAL) if base == null { return } @@ -1627,6 +1670,7 @@ spherical_joint_get_cone_angle(id: JointId) -> float { // zero or invalid axis is ignored. The cone's angle is clamped again to the // bound the centre allows. spherical_joint_set_cone_axis(id: JointId, axis: Vec3) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_SPHERICAL_JOINT_SET_CONE_AXIS); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_vec3(recorder, axis); dynamics.rec_end(recorder) } base = sim_of(id, dynamics.JOINT_SPHERICAL) if base == null || math.is_valid_vec3(axis) == false || math.length(axis) < 0.001 { return } j = dynamics.spherical_joint_of(base) @@ -1639,6 +1683,7 @@ spherical_joint_get_cone_axis(id: JointId) -> Vec3 { return dynamics.spherical_j // each clamped as the circle's angle is. A circle again when the angles // match or the minor one is 0. spherical_joint_set_cone_ellipse(id: JointId, major_angle: float, minor_angle: float, major_axis: Vec3) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_SPHERICAL_JOINT_SET_CONE_ELLIPSE); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, major_angle); dynamics.rec_float(recorder, minor_angle); dynamics.rec_vec3(recorder, major_axis); dynamics.rec_end(recorder) } if nonnegative(major_angle) == false || nonnegative(minor_angle) == false || math.is_valid_vec3(major_axis) == false { return } j = dynamics.spherical_joint_of(sim_of(id, dynamics.JOINT_SPHERICAL)) j.cone_angle = dynamics.clamp_spherical_cone(j, major_angle) @@ -1652,6 +1697,7 @@ spherical_joint_get_cone_major_axis(id: JointId) -> Vec3 { return math.widen3(dynamics.spherical_joint_of(sim_of(id, dynamics.JOINT_SPHERICAL)).cone_major_axis) } spherical_joint_enable_twist_limit(id: JointId, flag: bool) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_SPHERICAL_JOINT_ENABLE_TWIST_LIMIT); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } base = sim_of(id, dynamics.JOINT_SPHERICAL) if base == null { return } j = dynamics.spherical_joint_of(base) @@ -1665,6 +1711,7 @@ spherical_joint_is_twist_limit_enabled(id: JointId) -> bool { return dynamics.sp spherical_joint_get_lower_twist_limit(id: JointId) -> float { return dynamics.spherical_joint_of(sim_of(id, dynamics.JOINT_SPHERICAL)).lower_twist_angle } spherical_joint_get_upper_twist_limit(id: JointId) -> float { return dynamics.spherical_joint_of(sim_of(id, dynamics.JOINT_SPHERICAL)).upper_twist_angle } spherical_joint_set_twist_limits(id: JointId, lower: float, upper: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_SPHERICAL_JOINT_SET_TWIST_LIMITS); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, lower); dynamics.rec_float(recorder, upper); dynamics.rec_end(recorder) } if math.is_valid_float(lower) == false || math.is_valid_float(upper) == false { return } base = sim_of(id, dynamics.JOINT_SPHERICAL) if base == null { return } @@ -1679,6 +1726,7 @@ spherical_joint_get_twist_angle(id: JointId) -> float { return math.get_twist_angle(spherical_relative_rotation(id, base)) } spherical_joint_enable_spring(id: JointId, flag: bool) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_SPHERICAL_JOINT_ENABLE_SPRING); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } base = sim_of(id, dynamics.JOINT_SPHERICAL) if base == null { return } j = dynamics.spherical_joint_of(base) @@ -1687,24 +1735,28 @@ spherical_joint_enable_spring(id: JointId, flag: bool) { } spherical_joint_is_spring_enabled(id: JointId) -> bool { return dynamics.spherical_joint_of(sim_of(id, dynamics.JOINT_SPHERICAL)).enable_spring } spherical_joint_set_target_rotation(id: JointId, q: Quat) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_SPHERICAL_JOINT_SET_TARGET_ROTATION); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_quat(recorder, q); dynamics.rec_end(recorder) } if math.is_valid_quat(q) == false { return } base = sim_of(id, dynamics.JOINT_SPHERICAL) if base != null { dynamics.spherical_joint_of(base).target_rotation = q } } spherical_joint_get_target_rotation(id: JointId) -> Quat { return dynamics.spherical_joint_of(sim_of(id, dynamics.JOINT_SPHERICAL)).target_rotation } spherical_joint_set_spring_hertz(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_SPHERICAL_JOINT_SET_SPRING_HERTZ); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_SPHERICAL) if base != null { dynamics.spherical_joint_of(base).hertz = value } } spherical_joint_get_spring_hertz(id: JointId) -> float { return dynamics.spherical_joint_of(sim_of(id, dynamics.JOINT_SPHERICAL)).hertz } spherical_joint_set_spring_damping_ratio(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_SPHERICAL_JOINT_SET_SPRING_DAMPING_RATIO); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_SPHERICAL) if base != null { dynamics.spherical_joint_of(base).damping_ratio = value } } spherical_joint_get_spring_damping_ratio(id: JointId) -> float { return dynamics.spherical_joint_of(sim_of(id, dynamics.JOINT_SPHERICAL)).damping_ratio } spherical_joint_enable_motor(id: JointId, flag: bool) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_SPHERICAL_JOINT_ENABLE_MOTOR); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } base = sim_of(id, dynamics.JOINT_SPHERICAL) if base == null { return } j = dynamics.spherical_joint_of(base) @@ -1713,12 +1765,14 @@ spherical_joint_enable_motor(id: JointId, flag: bool) { } spherical_joint_is_motor_enabled(id: JointId) -> bool { return dynamics.spherical_joint_of(sim_of(id, dynamics.JOINT_SPHERICAL)).enable_motor } spherical_joint_set_motor_velocity(id: JointId, v: Vec3) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_SPHERICAL_JOINT_SET_MOTOR_VELOCITY); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_vec3(recorder, v); dynamics.rec_end(recorder) } if math.is_valid_vec3(v) == false { return } base = sim_of(id, dynamics.JOINT_SPHERICAL) if base != null { dynamics.spherical_joint_of(base).motor_velocity = v } } spherical_joint_get_motor_velocity(id: JointId) -> Vec3 { return dynamics.spherical_joint_of(sim_of(id, dynamics.JOINT_SPHERICAL)).motor_velocity } spherical_joint_set_max_motor_torque(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_SPHERICAL_JOINT_SET_MAX_MOTOR_TORQUE); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_SPHERICAL) if base != null { dynamics.spherical_joint_of(base).max_motor_torque = value } @@ -1823,24 +1877,28 @@ get_weld_joint_force(w: *World, base: *JointSim) -> Vec3 { return math.mul_sv(w. get_weld_joint_torque(w: *World, base: *JointSim) -> Vec3 { return math.mul_sv(w.inv_h, dynamics.weld_joint_of(base).angular_impulse) } weld_joint_set_linear_hertz(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WELD_JOINT_SET_LINEAR_HERTZ); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_WELD) if base != null { dynamics.weld_joint_of(base).linear_hertz = value } } weld_joint_get_linear_hertz(id: JointId) -> float { return dynamics.weld_joint_of(sim_of(id, dynamics.JOINT_WELD)).linear_hertz } weld_joint_set_linear_damping_ratio(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WELD_JOINT_SET_LINEAR_DAMPING_RATIO); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_WELD) if base != null { dynamics.weld_joint_of(base).linear_damping_ratio = value } } weld_joint_get_linear_damping_ratio(id: JointId) -> float { return dynamics.weld_joint_of(sim_of(id, dynamics.JOINT_WELD)).linear_damping_ratio } weld_joint_set_angular_hertz(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WELD_JOINT_SET_ANGULAR_HERTZ); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_WELD) if base != null { dynamics.weld_joint_of(base).angular_hertz = value } } weld_joint_get_angular_hertz(id: JointId) -> float { return dynamics.weld_joint_of(sim_of(id, dynamics.JOINT_WELD)).angular_hertz } weld_joint_set_angular_damping_ratio(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WELD_JOINT_SET_ANGULAR_DAMPING_RATIO); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_WELD) if base != null { dynamics.weld_joint_of(base).angular_damping_ratio = value } @@ -2172,6 +2230,7 @@ get_wheel_joint_torque(w: *World, base: *JointSim) -> Vec3 { } wheel_joint_enable_suspension(id: JointId, flag: bool) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WHEEL_JOINT_ENABLE_SUSPENSION); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } base = sim_of(id, dynamics.JOINT_WHEEL) if base == null { return } j = dynamics.wheel_joint_of(base) @@ -2182,18 +2241,21 @@ wheel_joint_enable_suspension(id: JointId, flag: bool) { } wheel_joint_is_suspension_enabled(id: JointId) -> bool { return dynamics.wheel_joint_of(sim_of(id, dynamics.JOINT_WHEEL)).enable_suspension_spring } wheel_joint_set_suspension_hertz(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WHEEL_JOINT_SET_SUSPENSION_HERTZ); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_WHEEL) if base != null { dynamics.wheel_joint_of(base).suspension_hertz = value } } wheel_joint_get_suspension_hertz(id: JointId) -> float { return dynamics.wheel_joint_of(sim_of(id, dynamics.JOINT_WHEEL)).suspension_hertz } wheel_joint_set_suspension_damping_ratio(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WHEEL_JOINT_SET_SUSPENSION_DAMPING_RATIO); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_WHEEL) if base != null { dynamics.wheel_joint_of(base).suspension_damping_ratio = value } } wheel_joint_get_suspension_damping_ratio(id: JointId) -> float { return dynamics.wheel_joint_of(sim_of(id, dynamics.JOINT_WHEEL)).suspension_damping_ratio } wheel_joint_enable_suspension_limit(id: JointId, flag: bool) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WHEEL_JOINT_ENABLE_SUSPENSION_LIMIT); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } base = sim_of(id, dynamics.JOINT_WHEEL) if base == null { return } j = dynamics.wheel_joint_of(base) @@ -2207,6 +2269,7 @@ wheel_joint_is_suspension_limit_enabled(id: JointId) -> bool { return dynamics.w wheel_joint_get_lower_suspension_limit(id: JointId) -> float { return dynamics.wheel_joint_of(sim_of(id, dynamics.JOINT_WHEEL)).lower_suspension_limit } wheel_joint_get_upper_suspension_limit(id: JointId) -> float { return dynamics.wheel_joint_of(sim_of(id, dynamics.JOINT_WHEEL)).upper_suspension_limit } wheel_joint_set_suspension_limits(id: JointId, lower: float, upper: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WHEEL_JOINT_SET_SUSPENSION_LIMITS); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, lower); dynamics.rec_float(recorder, upper); dynamics.rec_end(recorder) } if math.is_valid_float(lower) == false || math.is_valid_float(upper) == false { return } base = sim_of(id, dynamics.JOINT_WHEEL) if base == null { return } @@ -2219,6 +2282,7 @@ wheel_joint_set_suspension_limits(id: JointId, lower: float, upper: float) { } } wheel_joint_enable_spin_motor(id: JointId, flag: bool) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WHEEL_JOINT_ENABLE_SPIN_MOTOR); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } base = sim_of(id, dynamics.JOINT_WHEEL) if base == null { return } j = dynamics.wheel_joint_of(base) @@ -2229,18 +2293,21 @@ wheel_joint_enable_spin_motor(id: JointId, flag: bool) { } wheel_joint_is_spin_motor_enabled(id: JointId) -> bool { return dynamics.wheel_joint_of(sim_of(id, dynamics.JOINT_WHEEL)).enable_spin_motor } wheel_joint_set_spin_motor_speed(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WHEEL_JOINT_SET_SPIN_MOTOR_SPEED); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if math.is_valid_float(value) == false { return } base = sim_of(id, dynamics.JOINT_WHEEL) if base != null { dynamics.wheel_joint_of(base).spin_speed = value } } wheel_joint_get_spin_motor_speed(id: JointId) -> float { return dynamics.wheel_joint_of(sim_of(id, dynamics.JOINT_WHEEL)).spin_speed } wheel_joint_set_max_spin_torque(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WHEEL_JOINT_SET_MAX_SPIN_TORQUE); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_WHEEL) if base != null { dynamics.wheel_joint_of(base).max_spin_torque = value } } wheel_joint_get_max_spin_torque(id: JointId) -> float { return dynamics.wheel_joint_of(sim_of(id, dynamics.JOINT_WHEEL)).max_spin_torque } wheel_joint_enable_steering(id: JointId, flag: bool) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WHEEL_JOINT_ENABLE_STEERING); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } base = sim_of(id, dynamics.JOINT_WHEEL) if base == null { return } j = dynamics.wheel_joint_of(base) @@ -2256,24 +2323,28 @@ wheel_joint_enable_steering(id: JointId, flag: bool) { } wheel_joint_is_steering_enabled(id: JointId) -> bool { return dynamics.wheel_joint_of(sim_of(id, dynamics.JOINT_WHEEL)).enable_steering } wheel_joint_set_steering_hertz(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WHEEL_JOINT_SET_STEERING_HERTZ); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_WHEEL) if base != null { dynamics.wheel_joint_of(base).steering_hertz = value } } wheel_joint_get_steering_hertz(id: JointId) -> float { return dynamics.wheel_joint_of(sim_of(id, dynamics.JOINT_WHEEL)).steering_hertz } wheel_joint_set_steering_damping_ratio(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WHEEL_JOINT_SET_STEERING_DAMPING_RATIO); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_WHEEL) if base != null { dynamics.wheel_joint_of(base).steering_damping_ratio = value } } wheel_joint_get_steering_damping_ratio(id: JointId) -> float { return dynamics.wheel_joint_of(sim_of(id, dynamics.JOINT_WHEEL)).steering_damping_ratio } wheel_joint_set_max_steering_torque(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WHEEL_JOINT_SET_MAX_STEERING_TORQUE); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_WHEEL) if base != null { dynamics.wheel_joint_of(base).max_steering_torque = value } } wheel_joint_get_max_steering_torque(id: JointId) -> float { return dynamics.wheel_joint_of(sim_of(id, dynamics.JOINT_WHEEL)).max_steering_torque } wheel_joint_enable_steering_limit(id: JointId, flag: bool) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WHEEL_JOINT_ENABLE_STEERING_LIMIT); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } base = sim_of(id, dynamics.JOINT_WHEEL) if base == null { return } j = dynamics.wheel_joint_of(base) @@ -2287,6 +2358,7 @@ wheel_joint_is_steering_limit_enabled(id: JointId) -> bool { return dynamics.whe wheel_joint_get_lower_steering_limit(id: JointId) -> float { return dynamics.wheel_joint_of(sim_of(id, dynamics.JOINT_WHEEL)).lower_steering_limit } wheel_joint_get_upper_steering_limit(id: JointId) -> float { return dynamics.wheel_joint_of(sim_of(id, dynamics.JOINT_WHEEL)).upper_steering_limit } wheel_joint_set_steering_limits(id: JointId, lower: float, upper: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WHEEL_JOINT_SET_STEERING_LIMITS); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, lower); dynamics.rec_float(recorder, upper); dynamics.rec_end(recorder) } if math.is_valid_float(lower) == false || math.is_valid_float(upper) == false { return } base = sim_of(id, dynamics.JOINT_WHEEL) if base == null { return } @@ -2302,6 +2374,7 @@ wheel_joint_set_steering_limits(id: JointId, lower: float, upper: float) { } } wheel_joint_set_target_steering_angle(id: JointId, value: float) { + if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WHEEL_JOINT_SET_TARGET_STEERING_ANGLE); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if math.is_valid_float(value) == false { return } base = sim_of(id, dynamics.JOINT_WHEEL) if base != null { dynamics.wheel_joint_of(base).target_steering_angle = value } diff --git a/aephysics/mesh/module.ae b/aephysics/mesh/module.ae index d1fe3fa..46c2f1f 100644 --- a/aephysics/mesh/module.ae +++ b/aephysics/mesh/module.ae @@ -24,6 +24,7 @@ import aephysics.math import aephysics.native import aephysics.basics import aephysics.distance +import aephysics.hull import aephysics.manifold import aephysics.triangle_manifold diff --git a/aephysics/physics_world/module.ae b/aephysics/physics_world/module.ae index 55ca1ed..79855aa 100644 --- a/aephysics/physics_world/module.ae +++ b/aephysics/physics_world/module.ae @@ -133,6 +133,7 @@ is_valid(id: WorldId) -> bool { return dynamics.world_is_valid(id) } // the sensors, the stack grown to the step's peak, the end events // swapped. A zero time step collides without solving. step(id: WorldId, time_step: float, sub_step_count: int) { + if dynamics.recording_on(id.index1 - 1) { recorder = dynamics.rec_begin(id.index1 - 1, dynamics.OP_STEP); dynamics.rec_float(recorder, time_step); dynamics.rec_int(recorder, sub_step_count); dynamics.rec_end(recorder) } w = world_of(id) if w == null { return } // An infinite step would make every sub-step infinite; a NaN one is @@ -193,6 +194,22 @@ step(id: WorldId, time_step: float, sub_step_count: int) { w.profile.step = ms(os.now_monotonic_ns() - t0) w.locked = false dynamics.leave_world(caller_index) + if dynamics.recording_on(id.index1 - 1) { record_step_result(id, w) } +} + +// What a recorded step left: the state's hash, which a player checks its +// own against, and the world's bounds joined into the recording's for a +// viewer to frame it. +record_step_result(id: WorldId, w: *World) { + recorder = dynamics.rec_begin(id.index1 - 1, dynamics.OP_STATE_HASH) + dynamics.rec_long(recorder, dynamics.hash_world_state(w)) + dynamics.rec_end(recorder) + bounds = get_bounds(id) + if recorder.have_bounds { recorder.bounds = math.aabb_union(recorder.bounds, bounds) } + else { + recorder.bounds = bounds + recorder.have_bounds = true + } } ms(ns: long) -> float { return (ns as float) / 1000000.0 } @@ -267,6 +284,7 @@ joint_event(id: WorldId, index: int) -> JointEvent { return dynamics.world_joint // Sleeping off wakes every sleeping set (the sets above the awake one, from the last down). enable_sleeping(id: WorldId, flag: bool) { + if dynamics.recording_on(id.index1 - 1) { recorder = dynamics.rec_begin(id.index1 - 1, dynamics.OP_ENABLE_SLEEPING); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } w = world_of(id) if w == null { return } if flag == w.enable_sleep { return } @@ -282,20 +300,24 @@ enable_sleeping(id: WorldId, flag: bool) { } is_sleeping_enabled(id: WorldId) -> bool { return dynamics.get_world(id.index1 - 1).enable_sleep } enable_warm_starting(id: WorldId, flag: bool) { + if dynamics.recording_on(id.index1 - 1) { recorder = dynamics.rec_begin(id.index1 - 1, dynamics.OP_ENABLE_WARM_STARTING); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } w = world_of(id) if w != null { w.enable_warm_starting = flag } } is_warm_starting_enabled(id: WorldId) -> bool { return dynamics.get_world(id.index1 - 1).enable_warm_starting } enable_continuous(id: WorldId, flag: bool) { + if dynamics.recording_on(id.index1 - 1) { recorder = dynamics.rec_begin(id.index1 - 1, dynamics.OP_ENABLE_CONTINUOUS); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } w = world_of(id) if w != null { w.enable_continuous = flag } } is_continuous_enabled(id: WorldId) -> bool { return dynamics.get_world(id.index1 - 1).enable_continuous } enable_speculative(id: WorldId, flag: bool) { + if dynamics.recording_on(id.index1 - 1) { recorder = dynamics.rec_begin(id.index1 - 1, dynamics.OP_ENABLE_SPECULATIVE); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } w = world_of(id) if w != null { w.enable_speculative = flag } } set_restitution_threshold(id: WorldId, value: float) { + if dynamics.recording_on(id.index1 - 1) { recorder = dynamics.rec_begin(id.index1 - 1, dynamics.OP_SET_RESTITUTION_THRESHOLD); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if math.is_valid_float(value) == false { return } w = world_of(id) if w != null { w.restitution_threshold = math.clamp_float(value, 0.0, math.MAX_FLOAT) } @@ -303,12 +325,14 @@ set_restitution_threshold(id: WorldId, value: float) { get_restitution_threshold(id: WorldId) -> float { return dynamics.get_world(id.index1 - 1).restitution_threshold } // The restitution passes after the sub-steps (0 to 63; Box3D's b3World_SetRestitutionIterations). set_restitution_iterations(id: WorldId, count: int) { + if dynamics.recording_on(id.index1 - 1) { recorder = dynamics.rec_begin(id.index1 - 1, dynamics.OP_SET_RESTITUTION_ITERATIONS); dynamics.rec_int(recorder, count); dynamics.rec_end(recorder) } w = world_of(id) if w != null { w.restitution_iterations = math.clamp_int(count, 0, dynamics.MAX_RESTITUTION_ITERATIONS) } } get_restitution_iterations(id: WorldId) -> int { return dynamics.get_world(id.index1 - 1).restitution_iterations } // Whether the restitution passes reach contacts without restitution too, so a bounce carries through a stack. enable_restitution_propagation(id: WorldId, flag: bool) { + if dynamics.recording_on(id.index1 - 1) { recorder = dynamics.rec_begin(id.index1 - 1, dynamics.OP_ENABLE_RESTITUTION_PROPAGATION); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } w = world_of(id) if w != null { w.enable_restitution_propagation = flag } } @@ -316,17 +340,20 @@ is_restitution_propagation_enabled(id: WorldId) -> bool { return dynamics.get_wo // Whether contacts with a kinematic body take the static softness, so a body // the game drives hard does not sink into what it meets (off: the reference's). enable_kinematic_static_softness(id: WorldId, flag: bool) { + if dynamics.recording_on(id.index1 - 1) { recorder = dynamics.rec_begin(id.index1 - 1, dynamics.OP_ENABLE_KINEMATIC_STATIC_SOFTNESS); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } w = world_of(id) if w != null { w.enable_kinematic_static_softness = flag } } is_kinematic_static_softness_enabled(id: WorldId) -> bool { return dynamics.get_world(id.index1 - 1).enable_kinematic_static_softness } set_hit_event_threshold(id: WorldId, value: float) { + if dynamics.recording_on(id.index1 - 1) { recorder = dynamics.rec_begin(id.index1 - 1, dynamics.OP_SET_HIT_EVENT_THRESHOLD); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if math.is_valid_float(value) == false { return } w = world_of(id) if w != null { w.hit_event_threshold = math.clamp_float(value, 0.0, math.MAX_FLOAT) } } get_hit_event_threshold(id: WorldId) -> float { return dynamics.get_world(id.index1 - 1).hit_event_threshold } set_contact_tuning(id: WorldId, hertz: float, damping_ratio: float, contact_speed: float) { + if dynamics.recording_on(id.index1 - 1) { recorder = dynamics.rec_begin(id.index1 - 1, dynamics.OP_SET_CONTACT_TUNING); dynamics.rec_float(recorder, hertz); dynamics.rec_float(recorder, damping_ratio); dynamics.rec_float(recorder, contact_speed); dynamics.rec_end(recorder) } if math.is_valid_float(hertz) == false || math.is_valid_float(damping_ratio) == false || math.is_valid_float(contact_speed) == false { return } w = world_of(id) if w == null { return } @@ -335,12 +362,14 @@ set_contact_tuning(id: WorldId, hertz: float, damping_ratio: float, contact_spee w.contact_speed = math.clamp_float(contact_speed, 0.0, math.MAX_FLOAT) } set_contact_recycle_distance(id: WorldId, distance: float) { + if dynamics.recording_on(id.index1 - 1) { recorder = dynamics.rec_begin(id.index1 - 1, dynamics.OP_SET_CONTACT_RECYCLE_DISTANCE); dynamics.rec_float(recorder, distance); dynamics.rec_end(recorder) } if math.is_valid_float(distance) == false { return } w = world_of(id) if w != null { w.contact_recycle_distance = math.clamp_float(distance, 0.0, math.MAX_FLOAT) } } get_contact_recycle_distance(id: WorldId) -> float { return dynamics.get_world(id.index1 - 1).contact_recycle_distance } set_maximum_linear_speed(id: WorldId, speed: float) { + if dynamics.recording_on(id.index1 - 1) { recorder = dynamics.rec_begin(id.index1 - 1, dynamics.OP_SET_MAXIMUM_LINEAR_SPEED); dynamics.rec_float(recorder, speed); dynamics.rec_end(recorder) } if math.is_valid_float(speed) == false || speed <= 0.0 { return } w = world_of(id) if w != null { w.max_linear_speed = speed } @@ -348,6 +377,7 @@ set_maximum_linear_speed(id: WorldId, speed: float) { get_maximum_linear_speed(id: WorldId) -> float { return dynamics.get_world(id.index1 - 1).max_linear_speed } // Gravity and the user data change even mid-step, from a callback, as the reference's do (no lock). set_gravity(id: WorldId, gravity: Vec3) { + if dynamics.recording_on(id.index1 - 1) { recorder = dynamics.rec_begin(id.index1 - 1, dynamics.OP_SET_GRAVITY); dynamics.rec_vec3(recorder, gravity); dynamics.rec_end(recorder) } if math.is_valid_vec3(gravity) == false { return } if dynamics.world_is_valid(id) { dynamics.get_world(id.index1 - 1).gravity = gravity } } @@ -436,6 +466,7 @@ get_bounds(id: WorldId) -> AABB { } rebuild_static_tree(id: WorldId) { + if dynamics.recording_on(id.index1 - 1) { recorder = dynamics.rec_begin(id.index1 - 1, dynamics.OP_REBUILD_STATIC_TREE); dynamics.rec_end(recorder) } w = world_of(id) if w != null { dynamic_tree.tree_rebuild(broad_phase.broad_phase_tree(&w.broad_phase, dynamics.BODY_STATIC), true) } } @@ -923,6 +954,15 @@ body_time_of_impact_mover(id: BodyId, origin: Vec3, mover: Capsule, mover_transl // --- explosions ------------------------------------------------------------------------------------------------------ +// An explosion def into a recording, field by field. +rec_explosion_def(r: *Recording, v: *ExplosionDef) { + dynamics.rec_long(r, v.mask_bits) + dynamics.rec_vec3(r, v.position) + dynamics.rec_float(r, v.radius) + dynamics.rec_float(r, v.falloff) + dynamics.rec_float(r, v.impulse_per_area) +} + default_explosion_def() -> ExplosionDef { return ExplosionDef { mask_bits: dynamic_tree.default_mask_bits(), position: math.vec3_zero(), radius: 0.0, falloff: 0.0, impulse_per_area: 0.0 } } @@ -973,6 +1013,7 @@ explosion_visitor(proxy_id: int, user_data: long, context: ptr) -> bool { } explode(id: WorldId, def: *ExplosionDef) { + if dynamics.recording_on(id.index1 - 1) { recorder = dynamics.rec_begin(id.index1 - 1, dynamics.OP_EXPLODE); rec_explosion_def(recorder, def); dynamics.rec_end(recorder) } if math.is_valid_position(def.position) == false || nonnegative(def.radius) == false || nonnegative(def.falloff) == false || math.is_valid_float(def.impulse_per_area) == false { return } w = world_of(id) if w == null { return } diff --git a/aephysics/recording/module.ae b/aephysics/recording/module.ae new file mode 100644 index 0000000..3eef9b9 --- /dev/null +++ b/aephysics/recording/module.ae @@ -0,0 +1,2257 @@ +// Recording and replay (#149, the reference's recording.c and +// recording_replay.c): a world records every call that changes it while a +// recording is started, and a player replays the recording into a world of +// its own, frame by frame (a frame is the calls before a step, the step and +// the state check after it), checking after every step that its world +// hashes as the recorded one did. +// +// A recording is a header, a snapshot of the world it started from +// (aephysics.snapshot), the world's state hash, then one record per call +// (its opcode, its length and its arguments field by field, written by the +// hooks at the top of every recorded function in dynamics, physics_world +// and joint_solver; a create's record ends with the id it made, which the +// replay must make too), a state hash after every step, the joined bounds, +// an end marker, and last the geometry the calls named, each block once. +// +// The player owns a copy of the bytes and a world it restores in place, so +// the world's id stays the same through a restart or a seek backward; it +// keeps keyframes (a snapshot every so many frames, the spacing widening to +// stay under a memory budget) to seek backward without replaying from the +// start, and the bodies in the order they were made (a destroyed one +// leaves a hole) so a viewer's selection survives seeking. A record it does +// not know is skipped, as a newer minor version may add some; a record +// that does not read whole, or a create that makes another id than the one +// recorded, stops the replay (the player has failed); a state hash that +// differs marks the frame it was found at (the player has diverged) and the +// replay goes on. +// +// Not recorded: what the host wires in (user data, callbacks, filters, the +// worker count), and the world queries (#149 leaves them for later). + +import std.string +import std.fs +import aephysics.math +import aephysics.native +import aephysics.basics +import aephysics.hull +import aephysics.mesh +import aephysics.height_field +import aephysics.compound +import aephysics.shape +import aephysics.material +import aephysics.manifold +import aephysics.dynamics +import aephysics.joint_solver +import aephysics.physics_world +import aephysics.snapshot + +exports ( + Player, PlayerInfo, create_recording, destroy_recording, recording_data, recording_size, start_recording, + stop_recording, save_recording_to_file, load_recording_from_file, create_player, destroy_player, player_step_frame, + player_sub_step_frame, player_restart, player_seek_frame, player_world, player_frame, player_frame_count, + player_at_end, player_at_pre_step, player_has_diverged, player_diverge_frame, player_failed, player_info, + player_set_worker_count, player_set_keyframe_policy, player_keyframe_budget, player_keyframe_min_interval, + player_keyframe_interval, player_keyframe_bytes, player_keyframe_count, player_body_count, player_body_id, + validate_replay +) + +extern memcpy(dst: ptr, src: ptr, size: int) -> ptr +extern memset(block: ptr, value: int, size: int) -> ptr + +const NULL_INDEX = 0 - 1 +const STRING_SLOTS = 4 +const MAX_RECORDED_MATERIALS = 65536 +const DEFAULT_KEYFRAME_INTERVAL = 16 +const DEFAULT_KEYFRAME_BUDGET = 536870912 + +// --- recordings ------------------------------------------------------------------------------------- + +// A recording to start a world into: empty until then, the host's to free. +create_recording() -> *Recording { + r = basics.alloc(sizeof(Recording)) as *Recording + r.data = null + r.size = 0 + r.capacity = 0 + r.record_start = 0 + r.geometry = basics.buffer_create(sizeof(RecordedGeometry), 0) + r.geometry_heads = basics.map_create(16) + r.have_bounds = false + r.owner = 0 + return r +} + +// The recording's geometry copies forgotten, ready for a fresh session. +clear_geometry(r: *Recording) { + i = 0 + while i < r.geometry.count { + entry = dynamics.recorded_geometry_at(r, i) + basics.free_bytes(entry.block, entry.byte_count) + i = i + 1 + } + r.geometry.count = 0 + basics.map_clear(&r.geometry_heads) +} + +// The recording freed; a world still recording into it stops first, so it +// never writes into freed memory. +destroy_recording(r: *Recording) { + if r == null { return } + if r.owner != 0 { dynamics.finish_recording(dynamics.get_world(r.owner - 1)) } + clear_geometry(r) + basics.buffer_destroy(&r.geometry) + basics.map_destroy(&r.geometry_heads) + if r.data != null { basics.free_bytes(r.data, r.capacity) } + basics.free_bytes(r as ptr, sizeof(Recording)) +} + +recording_data(r: *Recording) -> ptr { return r.data } +recording_size(r: *Recording) -> int { return r.size } + +// The world starts recording into `r` (b3World_StartRecording): the +// header, a snapshot of the world as it is, its state's hash; then every +// call that changes it until stop_recording or its destroy. False for a +// world that is not one, is mid-step or already records, and for a +// recording another world is writing. A recording started again begins a +// fresh session. +start_recording(world_id: WorldId, r: *Recording) -> bool { + if r == null || r.owner != 0 || dynamics.world_is_valid(world_id) == false { return false } + w = dynamics.get_world(world_id.index1 - 1) + if w.locked || w.recording != null { return false } + snap, snap_count = snapshot.save_world(world_id) + if snap == null { return false } + r.size = 0 + r.have_bounds = false + clear_geometry(r) + dynamics.rec_int(r, dynamics.RECORDING_MAGIC) + dynamics.rec_int(r, dynamics.RECORDING_MAJOR_VERSION) + dynamics.rec_int(r, snap_count) + dynamics.rec_int(r, 0) + dynamics.rec_int(r, 0) + dynamics.rec_int(r, dynamics.RECORDING_MINOR_VERSION) + dynamics.rec_int(r, 0) + dynamics.rec_int(r, 0) + dynamics.rec_raw(r, snap, snap_count) + snapshot.free_snapshot(snap, snap_count) + w.recording = r as ptr + r.owner = world_id.index1 + recorder = dynamics.rec_begin(world_id.index1 - 1, dynamics.OP_STATE_HASH) + dynamics.rec_long(recorder, dynamics.hash_world_state(w)) + dynamics.rec_end(recorder) + return true +} + +// The world stops recording (b3World_StopRecording); the recording is +// whole: its bounds, its end, the geometry the calls named. +stop_recording(world_id: WorldId) { + if dynamics.world_is_valid(world_id) == false { return } + w = dynamics.get_world(world_id.index1 - 1) + if w.locked { return } + dynamics.finish_recording(w) +} + +save_recording_to_file(r: *Recording, path: string) -> bool { + if r == null || r.size <= 0 { return false } + err = fs.write_binary(path, (r.data as byte[])[0..r.size]) + return string.length(err) == 0 +} + +// A recording read from a file, to play; null when there is no such file. +// Whether its bytes are a recording is the player's to find out. +load_recording_from_file(path: string) -> *Recording { + data, length, err = fs.read_binary(path) + if string.length(err) > 0 { return null } + if length <= 0 { + string.free(data) + return null + } + r = create_recording() + dynamics.rec_raw(r, string.aether_string_raw_ptr(data), length) + string.free(data) + return r +} + +// --- the player ------------------------------------------------------------------------------------- + +// What a recording holds, read once when it is opened so a viewer can +// frame and label it (b3RecPlayerInfo). +struct PlayerInfo { + frame_count: int // the recorded steps + worker_count: int // the replay world's + time_step: float // the first step's + sub_step_count: int // the first step's + bounds: AABB // every step's bodies joined, zero when none were recorded +} + +// A geometry block the recording's calls name, the player's own copy. +struct PlayerGeometry { + kind: int + byte_count: int + block: ptr +} + +// The world as it was at a frame, to seek back to: its snapshot, where the +// next frame's records start, the divergence found so far and the bodies +// in the order they were made. +struct Keyframe { + frame: int + cursor: int + block: ptr + byte_count: int + diverged: bool + diverge_frame: int + bodies: ptr // BodyId[body_count] + body_count: int +} + +struct Player { + data: ptr // the recording, the player's own copy + size: int + def: WorldDef // the world it plays into + world_id: WorldId + snapshot_at: int // the recording's snapshot: frame 0 + snapshot_bytes: int + ops_start: int + ops_end: int // where the records end (the geometry's offset) + cursor: int + record_end: int // the end of the record being read + ok: bool // the record being read reads whole so far + failed: bool // a record did not read whole or made another id: nothing after it runs + frame: int + frame_count: int + at_end: bool + at_pre_step: bool + diverged: bool + diverge_frame: int + time_step: float + sub_step_count: int + bounds: AABB + materials: ptr // a shape def's materials while its call runs + material_count: int + strings: ptr // ptr[STRING_SLOTS]: the record's strings while its call runs + string_bytes: ptr // int[STRING_SLOTS] + string_count: int + geometry: Buffer // PlayerGeometry + bodies: Buffer // BodyId in the order they were made, null ids where destroyed + frame0_bodies: Buffer // BodyId: the bodies of the recording's snapshot + keyframes: Buffer // Keyframe + keyframe_interval: int + keyframe_min_interval: int + keyframe_budget: long + keyframe_bytes: long + last_keyframe_frame: int +} + +player_geometry_at(p: *Player, index: int) -> *PlayerGeometry { return (p.geometry.data + index * sizeof(PlayerGeometry)) as *PlayerGeometry } +keyframe_at(p: *Player, index: int) -> *Keyframe { return (p.keyframes.data + index * sizeof(Keyframe)) as *Keyframe } + +read_header_int(data: ptr, at: int) -> int { + holder = 0 + memcpy((&holder) as ptr, data + at, 4) + return holder +} + +read_header_float(data: ptr, at: int) -> float { + holder = 0.0 + memcpy((&holder) as ptr, data + at, 8) + return holder +} + +// A player of the recording in `size` bytes, its world stepped by +// `worker_count` workers (a count other than the recorded one makes the +// state checks a test of determinism across threads); null when the bytes +// are not a recording of this major version and layout, any section fails +// its checks, or the snapshot does not load. +create_player(data: ptr, size: int, worker_count: int) -> *Player { + if data == null || size < dynamics.RECORDING_HEADER_BYTES { return null } + if read_header_int(data, 0) != dynamics.RECORDING_MAGIC || read_header_int(data, 4) != dynamics.RECORDING_MAJOR_VERSION { return null } + snap_bytes = read_header_int(data, 8) + registry_offset = read_header_int(data, 12) + registry_bytes = read_header_int(data, 16) + if snap_bytes <= 0 || snap_bytes > size - dynamics.RECORDING_HEADER_BYTES { return null } + ops_start = dynamics.RECORDING_HEADER_BYTES + snap_bytes + if registry_offset < ops_start || registry_offset > size - 4 || registry_bytes != size - registry_offset { return null } + p = basics.alloc(sizeof(Player)) as *Player + p.data = basics.alloc(size) + memcpy(p.data, data, size) + p.size = size + p.def = dynamics.default_world_def() + p.def.worker_count = math.clamp_int(worker_count, 1, native.MAX_WORKERS) + p.snapshot_at = dynamics.RECORDING_HEADER_BYTES + p.snapshot_bytes = snap_bytes + p.ops_start = ops_start + p.ops_end = registry_offset + p.cursor = ops_start + p.diverge_frame = NULL_INDEX + p.strings = basics.alloc(STRING_SLOTS * 8) + p.string_bytes = basics.alloc(STRING_SLOTS * 4) + p.geometry = basics.buffer_create(sizeof(PlayerGeometry), 0) + p.bodies = basics.buffer_create(sizeof(BodyId), 0) + p.frame0_bodies = basics.buffer_create(sizeof(BodyId), 0) + p.keyframes = basics.buffer_create(sizeof(Keyframe), 0) + p.keyframe_interval = DEFAULT_KEYFRAME_INTERVAL + p.keyframe_min_interval = DEFAULT_KEYFRAME_INTERVAL + p.keyframe_budget = DEFAULT_KEYFRAME_BUDGET as long + p.world_id = WorldId { index1: 0, generation: 0 } + if load_geometry(p, registry_offset, registry_bytes) == false || scan_records(p) == false { + destroy_player(p) + return null + } + p.world_id = snapshot.load_world(&p.def, p.data + p.snapshot_at, p.snapshot_bytes) + if dynamics.world_is_valid(p.world_id) == false { + destroy_player(p) + return null + } + // The snapshot's bodies were not made by a create record: the world is + // walked once for them, in slot order. + w = dynamics.get_world(p.world_id.index1 - 1) + i = 0 + while i < w.bodies.count { + b = dynamics.body_at(w, i) + if b.set_index != NULL_INDEX { + index = basics.buffer_push(&p.frame0_bodies) + each = p.frame0_bodies.data as BodyId[] + each[index] = BodyId { index1: i + 1, world0: p.world_id.index1 - 1, generation: b.generation } + } + i = i + 1 + } + copy_bodies(&p.bodies, p.frame0_bodies.data, p.frame0_bodies.count) + return p +} + +// The player freed: its world first, then the geometry the world used. +destroy_player(p: *Player) { + if p == null { return } + if dynamics.world_is_valid(p.world_id) { dynamics.destroy_world(p.world_id) } + i = 0 + while i < p.geometry.count { + g = player_geometry_at(p, i) + basics.free_bytes(g.block, g.byte_count) + i = i + 1 + } + basics.buffer_destroy(&p.geometry) + clear_keyframes(p) + basics.buffer_destroy(&p.keyframes) + basics.buffer_destroy(&p.bodies) + basics.buffer_destroy(&p.frame0_bodies) + release_record(p) + basics.free_bytes(p.strings, STRING_SLOTS * 8) + basics.free_bytes(p.string_bytes, STRING_SLOTS * 4) + basics.free_bytes(p.data, p.size) + basics.free_bytes(p as ptr, sizeof(Player)) +} + +// The geometry the records name: each block copied out and checked as +// bytes from outside are. +load_geometry(p: *Player, offset: int, byte_count: int) -> bool { + at = offset + end = offset + byte_count + count = read_header_int(p.data, at) + at = at + 4 + if count < 0 || basics.section_fits(at, count, 8, end) == false { return false } + i = 0 + while i < count { + if at + 8 > end { return false } + kind = read_header_int(p.data, at) + n = read_header_int(p.data, at + 4) + at = at + 8 + if n <= 0 || n > end - at { return false } + block = basics.alloc(n) + memcpy(block, p.data + at, n) + at = at + n + index = basics.buffer_push(&p.geometry) + g = player_geometry_at(p, index) + g.kind = kind + g.byte_count = n + g.block = block + valid = false + if kind == shape.SHAPE_HULL { valid = hull.hull_bytes_valid(block as *HullData, n) && (block as *HullData).byte_count == n } + else if kind == shape.SHAPE_MESH { valid = mesh.mesh_bytes_valid(block as *MeshData, n) && (block as *MeshData).byte_count == n } + else if kind == shape.SHAPE_HEIGHT_FIELD { valid = height_field.height_field_bytes_valid(block as *HeightFieldData, n) && (block as *HeightFieldData).byte_count == n } + else if kind == shape.SHAPE_COMPOUND { valid = compound.convert_bytes_to_compound(block, n) != null } + if valid == false { return false } + i = i + 1 + } + return at == end +} + +// The records walked once without running them: their framing checked up +// to the end marker, the frames counted, the first step's tuning and the +// bounds read for the info. +scan_records(p: *Player) -> bool { + at = p.ops_start + frames = 0 + while at < p.ops_end { + if at + 8 > p.ops_end { return false } + op = read_header_int(p.data, at) + length = read_header_int(p.data, at + 4) + if length < 0 || length > p.ops_end - at - 8 { return false } + payload = at + 8 + at = payload + length + if op == dynamics.OP_STEP { + if frames == 0 && length >= 12 { + p.time_step = read_header_float(p.data, payload) + p.sub_step_count = read_header_int(p.data, payload + 8) + } + frames = frames + 1 + } else if op == dynamics.OP_BOUNDS && length >= 52 { + if read_header_int(p.data, payload) == 1 { + lower = math.vec3(read_header_float(p.data, payload + 4), read_header_float(p.data, payload + 12), read_header_float(p.data, payload + 20)) + upper = math.vec3(read_header_float(p.data, payload + 28), read_header_float(p.data, payload + 36), read_header_float(p.data, payload + 44)) + p.bounds = AABB { lower: lower, upper: upper } + } + } else if op == dynamics.OP_END { + p.frame_count = frames + return at == p.ops_end + } + } + return false +} + +copy_bodies(into: *Buffer, from: ptr, count: int) { + into.count = 0 + basics.buffer_reserve(into, count) + if count > 0 { memcpy(into.data, from, count * sizeof(BodyId)) } + into.count = count +} + +// --- reading a record ------------------------------------------------------------------------------- + +need(p: *Player, n: int) -> bool { + if p.ok == false { return false } + if n < 0 || n > p.record_end - p.cursor { + p.ok = false + return false + } + return true +} + +read_int(p: *Player) -> int { + if need(p, 4) == false { return 0 } + holder = 0 + memcpy((&holder) as ptr, p.data + p.cursor, 4) + p.cursor = p.cursor + 4 + return holder +} + +read_long(p: *Player) -> long { + if need(p, 8) == false { return 0 as long } + holder = 0 as long + memcpy((&holder) as ptr, p.data + p.cursor, 8) + p.cursor = p.cursor + 8 + return holder +} + +read_float(p: *Player) -> float { + if need(p, 8) == false { return 0.0 } + holder = 0.0 + memcpy((&holder) as ptr, p.data + p.cursor, 8) + p.cursor = p.cursor + 8 + return holder +} + +read_bool(p: *Player) -> bool { + v = read_int(p) + if v != 0 && v != 1 { p.ok = false } + return v == 1 +} + +read_vec3(p: *Player) -> Vec3 { + x = read_float(p) + y = read_float(p) + z = read_float(p) + return math.vec3(x, y, z) +} + +read_quat(p: *Player) -> Quat { + x = read_float(p) + y = read_float(p) + z = read_float(p) + w = read_float(p) + return Quat { x: x, y: y, z: z, w: w } +} + +read_transform(p: *Player) -> Transform { + position = read_vec3(p) + rotation = read_quat(p) + return Transform { p: position, q: rotation } +} + +// Ids in the player's world: the recorded index and generation, the +// world the player's own. +read_body_id(p: *Player) -> BodyId { + index1 = read_int(p) + generation = read_int(p) + return BodyId { index1: index1, world0: p.world_id.index1 - 1, generation: generation } +} + +read_shape_id(p: *Player) -> ShapeId { + index1 = read_int(p) + generation = read_int(p) + return ShapeId { index1: index1, world0: p.world_id.index1 - 1, generation: generation } +} + +read_joint_id(p: *Player) -> JointId { + index1 = read_int(p) + generation = read_int(p) + return JointId { index1: index1, world0: p.world_id.index1 - 1, generation: generation } +} + +// A create's call made an id; the record says which it made when it was +// recorded. Another one means the worlds have parted: the replay stops. +check_made(p: *Player, index1: int, generation: int, recorded_index1: int, recorded_generation: int) { + if index1 != recorded_index1 || generation != recorded_generation { p.ok = false } +} + +// A string, held until the record's call is done; null for none. +read_string(p: *Player) -> string { + n = read_int(p) + if p.ok == false || n == NULL_INDEX { return null } + if need(p, n) == false || p.string_count >= STRING_SLOTS { + p.ok = false + return null + } + block = basics.alloc(n + 1) + memcpy(block, p.data + p.cursor, n) + memset(block + n, 0, 1) + p.cursor = p.cursor + n + slots = p.strings as ptr[] + sizes = p.string_bytes as int[] + slots[p.string_count] = block + sizes[p.string_count] = n + 1 + p.string_count = p.string_count + 1 + return native.name_text(block) +} + +// A shape def's materials, held until the record's call is done. +read_materials(p: *Player) -> ptr { + count = read_int(p) + p.material_count = 0 + if p.ok == false || count == NULL_INDEX { return null } + if count <= 0 || count > MAX_RECORDED_MATERIALS || p.materials != null { + p.ok = false + return null + } + p.materials = basics.alloc(count * sizeof(SurfaceMaterial)) + p.material_count = count + each = p.materials as SurfaceMaterial[] + i = 0 + while i < count { + each[i] = read_surface_material(p) + i = i + 1 + } + return p.materials +} + +// A geometry block the recording holds, by id, of the kind the call takes. +read_geometry(p: *Player, kind: int) -> ptr { + id = read_int(p) + if p.ok == false || id == NULL_INDEX { return null } + if id < 0 || id >= p.geometry.count || player_geometry_at(p, id).kind != kind { + p.ok = false + return null + } + return player_geometry_at(p, id).block +} + +// What the record's call held, given back. +release_record(p: *Player) { + if p.materials != null { + basics.free_bytes(p.materials, p.material_count * sizeof(SurfaceMaterial)) + p.materials = null + p.material_count = 0 + } + slots = p.strings as ptr[] + sizes = p.string_bytes as int[] + i = 0 + while i < p.string_count { + basics.free_bytes(slots[i], sizes[i]) + i = i + 1 + } + p.string_count = 0 +} + +// A body made: the next ordinal. Ordinals are never reused. +track_body_create(p: *Player, id: BodyId) { + index = basics.buffer_push(&p.bodies) + each = p.bodies.data as BodyId[] + each[index] = id +} + +// A body destroyed leaves a hole, so the ordinals after it stay. +track_body_destroy(p: *Player, id: BodyId) { + if id.index1 == 0 { return } + each = p.bodies.data as BodyId[] + i = 0 + while i < p.bodies.count { + if each[i].index1 == id.index1 && each[i].generation == id.generation { + each[i] = dynamics.null_body_id() + return + } + i = i + 1 + } +} + +// --- playing ---------------------------------------------------------------------------------------- + +// The next record run: its call made, its check done, or skipped when this +// player does not know it. Its opcode, or NULL_INDEX when it does not read +// whole or its create made another id (the player has failed). +run_record(p: *Player) -> int { + if p.failed || p.cursor + 8 > p.ops_end { + p.failed = true + return NULL_INDEX + } + op = read_header_int(p.data, p.cursor) + length = read_header_int(p.data, p.cursor + 4) + if length < 0 || length > p.ops_end - p.cursor - 8 { + p.failed = true + return NULL_INDEX + } + p.cursor = p.cursor + 8 + p.record_end = p.cursor + length + p.ok = true + if op == dynamics.OP_END { + p.cursor = p.record_end + return op + } + known = dispatch(p, op) + release_record(p) + if known == false { + // A newer minor version's record: skipped, as the reference does. + p.cursor = p.record_end + return op + } + if p.ok == false || p.cursor != p.record_end { + p.failed = true + return NULL_INDEX + } + return op +} + +// The frame just stepped kept, when it falls on the keyframe spacing. +keyframe_boundary(p: *Player) { + if p.frame > p.last_keyframe_frame && p.frame % p.keyframe_interval == 0 { capture_keyframe(p) } +} + +// The next frame played (b3RecPlayer_StepFrame): its records up to and +// through its step and the state check after it. False at the end of the +// recording or when the player has failed. +player_step_frame(p: *Player) -> bool { + p.at_pre_step = false + if p.at_end { return false } + stepped = false + while true { + if p.failed || p.cursor >= p.ops_end { + p.at_end = true + return stepped + } + // Once stepped, only the state check still belongs to this frame. + if stepped && read_header_int(p.data, p.cursor) != dynamics.OP_STATE_HASH { + keyframe_boundary(p) + return true + } + op = run_record(p) + if op == NULL_INDEX || op == dynamics.OP_END { + p.at_end = true + return stepped + } + if op == dynamics.OP_STEP { + p.frame = p.frame + 1 + stepped = true + } + } + return stepped +} + +// The next frame played in two calls when it makes a body +// (b3RecPlayer_SubStepFrame): the first stops before its step, so a viewer +// can draw the new bodies where they were made; the next steps. +player_sub_step_frame(p: *Player) { + if p.at_end { return } + stepped = false + made_a_body = false + while true { + if p.failed || p.cursor >= p.ops_end { + p.at_end = true + p.at_pre_step = false + return + } + next = read_header_int(p.data, p.cursor) + if stepped && next != dynamics.OP_STATE_HASH { + keyframe_boundary(p) + return + } + if p.at_pre_step == false && made_a_body && next == dynamics.OP_STEP { + p.at_pre_step = true + return + } + op = run_record(p) + if op == NULL_INDEX || op == dynamics.OP_END { + p.at_end = true + p.at_pre_step = false + return + } + if op == dynamics.OP_CREATE_BODY { made_a_body = true } + if op == dynamics.OP_STEP { + p.at_pre_step = false + p.frame = p.frame + 1 + stepped = true + } + } +} + +// Back to frame 0 (b3RecPlayer_Restart), the world restored in place so its +// id stays. +player_restart(p: *Player) { + if snapshot.restore_world(p.world_id, &p.def, p.data + p.snapshot_at, p.snapshot_bytes) == false { + p.failed = true + p.at_end = true + return + } + p.cursor = p.ops_start + p.failed = false + p.diverged = false + p.diverge_frame = NULL_INDEX + p.frame = 0 + p.at_end = false + p.at_pre_step = false + copy_bodies(&p.bodies, p.frame0_bodies.data, p.frame0_bodies.count) +} + +// The world at `frame` (b3RecPlayer_SeekFrame): backward from the latest +// keyframe before it, or from frame 0; forward through a keyframe when one +// lies between; then frame by frame. It stops at the last frame when asked +// past the end. +player_seek_frame(p: *Player, frame: int) { + p.at_pre_step = false + target = math.max_int(frame, 0) + best = NULL_INDEX + i = 0 + while i < p.keyframes.count { + k = keyframe_at(p, i) + if k.frame < target && (best == NULL_INDEX || k.frame > keyframe_at(p, best).frame) { best = i } + i = i + 1 + } + if target < p.frame { + if best != NULL_INDEX { restore_keyframe(p, keyframe_at(p, best)) } else { player_restart(p) } + } else if best != NULL_INDEX && keyframe_at(p, best).frame > p.frame { + restore_keyframe(p, keyframe_at(p, best)) + } + while p.frame < target && player_step_frame(p) {} +} + +// The world as the keyframe holds it, in place, and the player where it was. +restore_keyframe(p: *Player, k: *Keyframe) { + if snapshot.restore_world(p.world_id, &p.def, k.block, k.byte_count) == false { + p.failed = true + p.at_end = true + return + } + p.cursor = k.cursor + p.failed = false + p.diverged = k.diverged + p.diverge_frame = k.diverge_frame + p.frame = k.frame + p.at_end = false + p.at_pre_step = false + copy_bodies(&p.bodies, k.bodies, k.body_count) +} + +free_keyframe(k: *Keyframe) { + snapshot.free_snapshot(k.block, k.byte_count) + if k.bodies != null { basics.free_bytes(k.bodies, k.body_count * sizeof(BodyId)) } +} + +keyframe_cost(k: *Keyframe) -> long { return (k.byte_count + k.body_count * sizeof(BodyId)) as long } + +clear_keyframes(p: *Player) { + i = 0 + while i < p.keyframes.count { + free_keyframe(keyframe_at(p, i)) + i = i + 1 + } + p.keyframes.count = 0 + p.keyframe_bytes = 0 as long +} + +// The frame just completed kept as a keyframe. Over the budget, the +// spacing doubles and the keyframes off the wider grid go, until it fits +// or nothing more can go (the newest is always kept). +capture_keyframe(p: *Player) { + block, byte_count = snapshot.save_world(p.world_id) + if block == null { return } + cost = (byte_count + p.bodies.count * sizeof(BodyId)) as long + while p.keyframes.count > 0 && p.keyframe_bytes + cost > p.keyframe_budget { + p.keyframe_interval = p.keyframe_interval * 2 + kept = 0 + kept_bytes = 0 as long + i = 0 + while i < p.keyframes.count { + k = keyframe_at(p, i) + if k.frame % p.keyframe_interval == 0 { + if kept != i { memcpy(keyframe_at(p, kept) as ptr, k as ptr, sizeof(Keyframe)) } + kept_bytes = kept_bytes + keyframe_cost(keyframe_at(p, kept)) + kept = kept + 1 + } else { + free_keyframe(k) + } + i = i + 1 + } + progress = kept < p.keyframes.count + p.keyframes.count = kept + p.keyframe_bytes = kept_bytes + if progress == false { break } + } + index = basics.buffer_push(&p.keyframes) + k = keyframe_at(p, index) + k.frame = p.frame + k.cursor = p.cursor + k.block = block + k.byte_count = byte_count + k.diverged = p.diverged + k.diverge_frame = p.diverge_frame + k.body_count = p.bodies.count + k.bodies = null + if p.bodies.count > 0 { + k.bodies = basics.alloc(p.bodies.count * sizeof(BodyId)) + memcpy(k.bodies, p.bodies.data, p.bodies.count * sizeof(BodyId)) + } + p.keyframe_bytes = p.keyframe_bytes + cost + p.last_keyframe_frame = p.frame +} + +// The keyframes' memory budget and finest spacing +// (b3RecPlayer_SetKeyframePolicy): a budget or spacing not above zero keeps +// the one there was. The keyframes kept so far go; a restart fills them +// again under the new policy. +player_set_keyframe_policy(p: *Player, budget_bytes: long, min_interval_frames: int) { + if budget_bytes > (0 as long) { p.keyframe_budget = budget_bytes } + if min_interval_frames > 0 { p.keyframe_min_interval = min_interval_frames } + clear_keyframes(p) + p.keyframe_interval = p.keyframe_min_interval + p.last_keyframe_frame = 0 +} + +// The replay world's workers, now and through every restore. +player_set_worker_count(p: *Player, count: int) { + p.def.worker_count = math.clamp_int(count, 1, native.MAX_WORKERS) + if dynamics.world_is_valid(p.world_id) { physics_world.set_worker_count(p.world_id, p.def.worker_count) } +} + +player_info(p: *Player) -> PlayerInfo { + return PlayerInfo { frame_count: p.frame_count, worker_count: p.def.worker_count, time_step: p.time_step, sub_step_count: p.sub_step_count, bounds: p.bounds } +} + +player_keyframe_budget(p: *Player) -> long { return p.keyframe_budget } +player_keyframe_min_interval(p: *Player) -> int { return p.keyframe_min_interval } +player_keyframe_interval(p: *Player) -> int { return p.keyframe_interval } +player_keyframe_bytes(p: *Player) -> long { return p.keyframe_bytes } +player_keyframe_count(p: *Player) -> int { return p.keyframes.count } +player_world(p: *Player) -> WorldId { return p.world_id } +player_frame(p: *Player) -> int { return p.frame } +player_frame_count(p: *Player) -> int { return p.frame_count } +player_at_end(p: *Player) -> bool { return p.at_end } +player_at_pre_step(p: *Player) -> bool { return p.at_pre_step } +player_has_diverged(p: *Player) -> bool { return p.diverged } +player_diverge_frame(p: *Player) -> int { return p.diverge_frame } +player_failed(p: *Player) -> bool { return p.failed } + +// The bodies made so far, in order, holes included. +player_body_count(p: *Player) -> int { return p.bodies.count } + +// The body made `ordinal`-th, or a null id when there is none or it was +// destroyed. +player_body_id(p: *Player, ordinal: int) -> BodyId { + if ordinal < 0 || ordinal >= p.bodies.count { return dynamics.null_body_id() } + each = p.bodies.data as BodyId[] + return each[ordinal] +} + +// Whether the recording replays whole without diverging (b3ValidateReplay). +validate_replay(data: ptr, size: int, worker_count: int) -> bool { + p = create_player(data, size, worker_count) + if p == null { return false } + while player_step_frame(p) { + if p.diverged { break } + } + result = p.failed == false && p.diverged == false + destroy_player(p) + return result +} + +// --- the records ------------------------------------------------------------------------------------ + +// The record's call made in the player's world, or its check done; false +// for an opcode this player does not know. +dispatch(p: *Player, op: int) -> bool { + if op == dynamics.OP_STATE_HASH { + recorded = read_long(p) + if p.ok && dynamics.hash_world_state(dynamics.get_world(p.world_id.index1 - 1)) != recorded { + if p.diverged == false { p.diverge_frame = p.frame } + p.diverged = true + } + return true + } + if op == dynamics.OP_BOUNDS { + // Read for the info when the player opened. + _have = read_bool(p) + _lower = read_vec3(p) + _upper = read_vec3(p) + return true + } + return dispatch_call(p, op) +} + +read_body_def(p: *Player) -> BodyDef { + v = dynamics.default_body_def() + v.kind = read_int(p) + v.position = read_vec3(p) + v.rotation = read_quat(p) + v.linear_velocity = read_vec3(p) + v.angular_velocity = read_vec3(p) + v.linear_damping = read_float(p) + v.angular_damping = read_float(p) + v.gravity_scale = read_float(p) + v.sleep_threshold = read_float(p) + v.safety_factor = read_float(p) + v.name = read_string(p) + v.motion_locks = read_motion_locks(p) + v.enable_sleep = read_bool(p) + v.is_awake = read_bool(p) + v.is_bullet = read_bool(p) + v.is_enabled = read_bool(p) + v.allow_fast_rotation = read_bool(p) + v.enable_contact_recycling = read_bool(p) + return v +} + +read_shape_def(p: *Player) -> ShapeDef { + v = dynamics.default_shape_def() + v.name = read_string(p) + v.materials = read_materials(p) + v.material_count = p.material_count + v.base_material = read_surface_material(p) + v.density = read_float(p) + v.explosion_scale = read_float(p) + v.filter = read_filter(p) + v.enable_custom_filtering = read_bool(p) + v.is_sensor = read_bool(p) + v.enable_sensor_events = read_bool(p) + v.enable_contact_events = read_bool(p) + v.enable_hit_events = read_bool(p) + v.enable_pre_solve_events = read_bool(p) + v.invoke_contact_creation = read_bool(p) + v.update_body_mass = read_bool(p) + v.enable_speculative_contact = read_bool(p) + return v +} + +read_joint_def(p: *Player) -> JointDef { + v = dynamics.default_joint_def() + v.body_id_a = read_body_id(p) + v.body_id_b = read_body_id(p) + v.local_frame_a = read_transform(p) + v.local_frame_b = read_transform(p) + v.force_threshold = read_float(p) + v.torque_threshold = read_float(p) + v.constraint_hertz = read_float(p) + v.constraint_damping_ratio = read_float(p) + v.draw_scale = read_float(p) + v.collide_connected = read_bool(p) + return v +} + +read_distance_joint_def(p: *Player) -> DistanceJointDef { + v = dynamics.default_distance_joint_def() + v.base = read_joint_def(p) + v.length = read_float(p) + v.enable_spring = read_bool(p) + v.lower_spring_force = read_float(p) + v.upper_spring_force = read_float(p) + v.hertz = read_float(p) + v.damping_ratio = read_float(p) + v.enable_limit = read_bool(p) + v.min_length = read_float(p) + v.max_length = read_float(p) + v.enable_motor = read_bool(p) + v.max_motor_force = read_float(p) + v.motor_speed = read_float(p) + return v +} + +read_motor_joint_def(p: *Player) -> MotorJointDef { + v = dynamics.default_motor_joint_def() + v.base = read_joint_def(p) + v.linear_velocity = read_vec3(p) + v.max_velocity_force = read_float(p) + v.angular_velocity = read_vec3(p) + v.max_velocity_torque = read_float(p) + v.linear_hertz = read_float(p) + v.linear_damping_ratio = read_float(p) + v.max_spring_force = read_float(p) + v.angular_hertz = read_float(p) + v.angular_damping_ratio = read_float(p) + v.max_spring_torque = read_float(p) + return v +} + +read_filter_joint_def(p: *Player) -> FilterJointDef { + v = dynamics.default_filter_joint_def() + v.base = read_joint_def(p) + return v +} + +read_parallel_joint_def(p: *Player) -> ParallelJointDef { + v = dynamics.default_parallel_joint_def() + v.base = read_joint_def(p) + v.hertz = read_float(p) + v.damping_ratio = read_float(p) + v.max_torque = read_float(p) + return v +} + +read_prismatic_joint_def(p: *Player) -> PrismaticJointDef { + v = dynamics.default_prismatic_joint_def() + v.base = read_joint_def(p) + v.enable_spring = read_bool(p) + v.hertz = read_float(p) + v.damping_ratio = read_float(p) + v.target_translation = read_float(p) + v.enable_limit = read_bool(p) + v.lower_translation = read_float(p) + v.upper_translation = read_float(p) + v.enable_motor = read_bool(p) + v.max_motor_force = read_float(p) + v.motor_speed = read_float(p) + return v +} + +read_revolute_joint_def(p: *Player) -> RevoluteJointDef { + v = dynamics.default_revolute_joint_def() + v.base = read_joint_def(p) + v.target_angle = read_float(p) + v.enable_spring = read_bool(p) + v.hertz = read_float(p) + v.damping_ratio = read_float(p) + v.enable_limit = read_bool(p) + v.lower_angle = read_float(p) + v.upper_angle = read_float(p) + v.enable_motor = read_bool(p) + v.max_motor_torque = read_float(p) + v.motor_speed = read_float(p) + return v +} + +read_spherical_joint_def(p: *Player) -> SphericalJointDef { + v = dynamics.default_spherical_joint_def() + v.base = read_joint_def(p) + v.enable_spring = read_bool(p) + v.hertz = read_float(p) + v.damping_ratio = read_float(p) + v.target_rotation = read_quat(p) + v.enable_cone_limit = read_bool(p) + v.cone_angle = read_float(p) + v.cone_axis = read_vec3(p) + v.cone_minor_angle = read_float(p) + v.cone_major_axis = read_vec3(p) + v.enable_twist_limit = read_bool(p) + v.lower_twist_angle = read_float(p) + v.upper_twist_angle = read_float(p) + v.enable_motor = read_bool(p) + v.max_motor_torque = read_float(p) + v.motor_velocity = read_vec3(p) + return v +} + +read_weld_joint_def(p: *Player) -> WeldJointDef { + v = dynamics.default_weld_joint_def() + v.base = read_joint_def(p) + v.linear_hertz = read_float(p) + v.angular_hertz = read_float(p) + v.linear_damping_ratio = read_float(p) + v.angular_damping_ratio = read_float(p) + return v +} + +read_wheel_joint_def(p: *Player) -> WheelJointDef { + v = dynamics.default_wheel_joint_def() + v.base = read_joint_def(p) + v.enable_suspension_spring = read_bool(p) + v.suspension_hertz = read_float(p) + v.suspension_damping_ratio = read_float(p) + v.enable_suspension_limit = read_bool(p) + v.lower_suspension_limit = read_float(p) + v.upper_suspension_limit = read_float(p) + v.enable_spin_motor = read_bool(p) + v.max_spin_torque = read_float(p) + v.spin_speed = read_float(p) + v.enable_steering = read_bool(p) + v.steering_hertz = read_float(p) + v.steering_damping_ratio = read_float(p) + v.target_steering_angle = read_float(p) + v.max_steering_torque = read_float(p) + v.enable_steering_limit = read_bool(p) + v.lower_steering_limit = read_float(p) + v.upper_steering_limit = read_float(p) + return v +} + +read_mass_data(p: *Player) -> MassData { + v = MassData { mass: 0.0, center: math.vec3_zero(), inertia: Matrix3 { cx: math.vec3_zero(), cy: math.vec3_zero(), cz: math.vec3_zero() } } + v.mass = read_float(p) + v.center = read_vec3(p) + v.inertia = read_matrix3(p) + return v +} + +read_motion_locks(p: *Player) -> MotionLocks { + v = MotionLocks { linear_x: false, linear_y: false, linear_z: false, angular_x: false, angular_y: false, angular_z: false } + v.linear_x = read_bool(p) + v.linear_y = read_bool(p) + v.linear_z = read_bool(p) + v.angular_x = read_bool(p) + v.angular_y = read_bool(p) + v.angular_z = read_bool(p) + return v +} + +read_filter(p: *Player) -> Filter { + v = Filter { category_bits: 0 as long, mask_bits: 0 as long, group_index: 0 } + v.category_bits = read_long(p) + v.mask_bits = read_long(p) + v.group_index = read_int(p) + return v +} + +read_surface_material(p: *Player) -> SurfaceMaterial { + v = SurfaceMaterial { friction: 0.0, restitution: 0.0, rolling_resistance: 0.0, tangent_velocity: math.vec3_zero(), user_material_id: 0 as long, custom_color: 0 } + v.friction = read_float(p) + v.restitution = read_float(p) + v.rolling_resistance = read_float(p) + v.tangent_velocity = read_vec3(p) + v.user_material_id = read_long(p) + v.custom_color = read_int(p) + return v +} + +read_sphere(p: *Player) -> Sphere { + v = Sphere { center: math.vec3_zero(), radius: 0.0 } + v.center = read_vec3(p) + v.radius = read_float(p) + return v +} + +read_capsule(p: *Player) -> Capsule { + v = Capsule { center1: math.vec3_zero(), center2: math.vec3_zero(), radius: 0.0 } + v.center1 = read_vec3(p) + v.center2 = read_vec3(p) + v.radius = read_float(p) + return v +} + +read_matrix3(p: *Player) -> Matrix3 { + v = Matrix3 { cx: math.vec3_zero(), cy: math.vec3_zero(), cz: math.vec3_zero() } + v.cx = read_vec3(p) + v.cy = read_vec3(p) + v.cz = read_vec3(p) + return v +} + +read_explosion_def(p: *Player) -> ExplosionDef { + v = physics_world.default_explosion_def() + v.mask_bits = read_long(p) + v.position = read_vec3(p) + v.radius = read_float(p) + v.falloff = read_float(p) + v.impulse_per_area = read_float(p) + return v +} + +// The calls, one case an opcode. +dispatch_call(p: *Player, op: int) -> bool { + if op == dynamics.OP_CREATE_BODY { + a_world_id_world_id = p.world_id + a_body_def_def = read_body_def(p) + recorded_index1 = read_int(p) + recorded_generation = read_int(p) + if p.ok { + made_body = dynamics.create_body(a_world_id_world_id, &a_body_def_def) + check_made(p, made_body.index1, made_body.generation, recorded_index1, recorded_generation) + track_body_create(p, made_body) + } + return true + } + if op == dynamics.OP_DESTROY_BODY { + a_body_id_id = read_body_id(p) + if p.ok { + track_body_destroy(p, a_body_id_id) + dynamics.destroy_body(a_body_id_id) + } + return true + } + if op == dynamics.OP_BODY_SET_TRANSFORM { + a_body_id_id = read_body_id(p) + a_vec3_position = read_vec3(p) + a_quat_rotation = read_quat(p) + if p.ok { dynamics.body_set_transform(a_body_id_id, a_vec3_position, a_quat_rotation) } + return true + } + if op == dynamics.OP_BODY_SET_LINEAR_VELOCITY { + a_body_id_id = read_body_id(p) + a_vec3_v = read_vec3(p) + if p.ok { dynamics.body_set_linear_velocity(a_body_id_id, a_vec3_v) } + return true + } + if op == dynamics.OP_BODY_SET_ANGULAR_VELOCITY { + a_body_id_id = read_body_id(p) + a_vec3_v = read_vec3(p) + if p.ok { dynamics.body_set_angular_velocity(a_body_id_id, a_vec3_v) } + return true + } + if op == dynamics.OP_BODY_SET_TARGET_TRANSFORM { + a_body_id_id = read_body_id(p) + a_transform_target = read_transform(p) + a_float_time_step = read_float(p) + a_bool_wake = read_bool(p) + if p.ok { dynamics.body_set_target_transform(a_body_id_id, a_transform_target, a_float_time_step, a_bool_wake) } + return true + } + if op == dynamics.OP_BODY_APPLY_FORCE { + a_body_id_id = read_body_id(p) + a_vec3_force = read_vec3(p) + a_vec3_point = read_vec3(p) + a_bool_wake = read_bool(p) + if p.ok { dynamics.body_apply_force(a_body_id_id, a_vec3_force, a_vec3_point, a_bool_wake) } + return true + } + if op == dynamics.OP_BODY_APPLY_FORCE_TO_CENTER { + a_body_id_id = read_body_id(p) + a_vec3_force = read_vec3(p) + a_bool_wake = read_bool(p) + if p.ok { dynamics.body_apply_force_to_center(a_body_id_id, a_vec3_force, a_bool_wake) } + return true + } + if op == dynamics.OP_BODY_APPLY_TORQUE { + a_body_id_id = read_body_id(p) + a_vec3_torque = read_vec3(p) + a_bool_wake = read_bool(p) + if p.ok { dynamics.body_apply_torque(a_body_id_id, a_vec3_torque, a_bool_wake) } + return true + } + if op == dynamics.OP_BODY_APPLY_LINEAR_IMPULSE { + a_body_id_id = read_body_id(p) + a_vec3_impulse = read_vec3(p) + a_vec3_point = read_vec3(p) + a_bool_wake = read_bool(p) + if p.ok { dynamics.body_apply_linear_impulse(a_body_id_id, a_vec3_impulse, a_vec3_point, a_bool_wake) } + return true + } + if op == dynamics.OP_BODY_APPLY_LINEAR_IMPULSE_TO_CENTER { + a_body_id_id = read_body_id(p) + a_vec3_impulse = read_vec3(p) + a_bool_wake = read_bool(p) + if p.ok { dynamics.body_apply_linear_impulse_to_center(a_body_id_id, a_vec3_impulse, a_bool_wake) } + return true + } + if op == dynamics.OP_BODY_APPLY_ANGULAR_IMPULSE { + a_body_id_id = read_body_id(p) + a_vec3_impulse = read_vec3(p) + a_bool_wake = read_bool(p) + if p.ok { dynamics.body_apply_angular_impulse(a_body_id_id, a_vec3_impulse, a_bool_wake) } + return true + } + if op == dynamics.OP_BODY_SET_BULLET { + a_body_id_id = read_body_id(p) + a_bool_flag = read_bool(p) + if p.ok { dynamics.body_set_bullet(a_body_id_id, a_bool_flag) } + return true + } + if op == dynamics.OP_BODY_SET_MASS_DATA { + a_body_id_id = read_body_id(p) + a_mass_mass_data = read_mass_data(p) + if p.ok { dynamics.body_set_mass_data(a_body_id_id, a_mass_mass_data) } + return true + } + if op == dynamics.OP_BODY_APPLY_MASS_FROM_SHAPES { + a_body_id_id = read_body_id(p) + if p.ok { dynamics.body_apply_mass_from_shapes(a_body_id_id) } + return true + } + if op == dynamics.OP_BODY_SET_LINEAR_DAMPING { + a_body_id_id = read_body_id(p) + a_float_damping = read_float(p) + if p.ok { dynamics.body_set_linear_damping(a_body_id_id, a_float_damping) } + return true + } + if op == dynamics.OP_BODY_SET_ANGULAR_DAMPING { + a_body_id_id = read_body_id(p) + a_float_damping = read_float(p) + if p.ok { dynamics.body_set_angular_damping(a_body_id_id, a_float_damping) } + return true + } + if op == dynamics.OP_BODY_SET_GRAVITY_SCALE { + a_body_id_id = read_body_id(p) + a_float_scale = read_float(p) + if p.ok { dynamics.body_set_gravity_scale(a_body_id_id, a_float_scale) } + return true + } + if op == dynamics.OP_BODY_SET_AWAKE { + a_body_id_id = read_body_id(p) + a_bool_awake = read_bool(p) + if p.ok { dynamics.body_set_awake(a_body_id_id, a_bool_awake) } + return true + } + if op == dynamics.OP_BODY_ENABLE_SLEEP { + a_body_id_id = read_body_id(p) + a_bool_enable = read_bool(p) + if p.ok { dynamics.body_enable_sleep(a_body_id_id, a_bool_enable) } + return true + } + if op == dynamics.OP_BODY_SET_SLEEP_THRESHOLD { + a_body_id_id = read_body_id(p) + a_float_threshold = read_float(p) + if p.ok { dynamics.body_set_sleep_threshold(a_body_id_id, a_float_threshold) } + return true + } + if op == dynamics.OP_BODY_SET_MOTION_LOCKS { + a_body_id_id = read_body_id(p) + a_motion_locks_locks = read_motion_locks(p) + if p.ok { dynamics.body_set_motion_locks(a_body_id_id, a_motion_locks_locks) } + return true + } + if op == dynamics.OP_CREATE_SPHERE_SHAPE { + a_body_id_body_id = read_body_id(p) + a_shape_def_def = read_shape_def(p) + a_sphere_s = read_sphere(p) + recorded_index1 = read_int(p) + recorded_generation = read_int(p) + if p.ok { + made_shape = dynamics.create_sphere_shape(a_body_id_body_id, &a_shape_def_def, a_sphere_s) + check_made(p, made_shape.index1, made_shape.generation, recorded_index1, recorded_generation) + } + return true + } + if op == dynamics.OP_CREATE_CAPSULE_SHAPE { + a_body_id_body_id = read_body_id(p) + a_shape_def_def = read_shape_def(p) + a_capsule_c = read_capsule(p) + recorded_index1 = read_int(p) + recorded_generation = read_int(p) + if p.ok { + made_shape = dynamics.create_capsule_shape(a_body_id_body_id, &a_shape_def_def, a_capsule_c) + check_made(p, made_shape.index1, made_shape.generation, recorded_index1, recorded_generation) + } + return true + } + if op == dynamics.OP_CREATE_HULL_SHAPE { + a_body_id_body_id = read_body_id(p) + a_shape_def_def = read_shape_def(p) + a_ptr__hull_h = read_geometry(p, shape.SHAPE_HULL) as *HullData + recorded_index1 = read_int(p) + recorded_generation = read_int(p) + if p.ok { + made_shape = dynamics.create_hull_shape(a_body_id_body_id, &a_shape_def_def, a_ptr__hull_h) + check_made(p, made_shape.index1, made_shape.generation, recorded_index1, recorded_generation) + } + return true + } + if op == dynamics.OP_CREATE_TRANSFORMED_HULL_SHAPE { + a_body_id_body_id = read_body_id(p) + a_shape_def_def = read_shape_def(p) + a_ptr__hull_h = read_geometry(p, shape.SHAPE_HULL) as *HullData + a_transform_transform = read_transform(p) + a_vec3_scale = read_vec3(p) + recorded_index1 = read_int(p) + recorded_generation = read_int(p) + if p.ok { + made_shape = dynamics.create_transformed_hull_shape(a_body_id_body_id, &a_shape_def_def, a_ptr__hull_h, a_transform_transform, a_vec3_scale) + check_made(p, made_shape.index1, made_shape.generation, recorded_index1, recorded_generation) + } + return true + } + if op == dynamics.OP_CREATE_MESH_SHAPE { + a_body_id_body_id = read_body_id(p) + a_shape_def_def = read_shape_def(p) + a_ptr__mesh_m = read_geometry(p, shape.SHAPE_MESH) as *MeshData + a_vec3_scale = read_vec3(p) + recorded_index1 = read_int(p) + recorded_generation = read_int(p) + if p.ok { + made_shape = dynamics.create_mesh_shape(a_body_id_body_id, &a_shape_def_def, a_ptr__mesh_m, a_vec3_scale) + check_made(p, made_shape.index1, made_shape.generation, recorded_index1, recorded_generation) + } + return true + } + if op == dynamics.OP_CREATE_HEIGHT_FIELD_SHAPE { + a_body_id_body_id = read_body_id(p) + a_shape_def_def = read_shape_def(p) + a_ptr__height_field_h = read_geometry(p, shape.SHAPE_HEIGHT_FIELD) as *HeightFieldData + recorded_index1 = read_int(p) + recorded_generation = read_int(p) + if p.ok { + made_shape = dynamics.create_height_field_shape(a_body_id_body_id, &a_shape_def_def, a_ptr__height_field_h) + check_made(p, made_shape.index1, made_shape.generation, recorded_index1, recorded_generation) + } + return true + } + if op == dynamics.OP_CREATE_COMPOUND_SHAPE { + a_body_id_body_id = read_body_id(p) + a_shape_def_def = read_shape_def(p) + a_ptr__compound_c = read_geometry(p, shape.SHAPE_COMPOUND) as *CompoundData + recorded_index1 = read_int(p) + recorded_generation = read_int(p) + if p.ok { + made_shape = dynamics.create_compound_shape(a_body_id_body_id, &a_shape_def_def, a_ptr__compound_c) + check_made(p, made_shape.index1, made_shape.generation, recorded_index1, recorded_generation) + } + return true + } + if op == dynamics.OP_DESTROY_SHAPE { + a_shape_id_id = read_shape_id(p) + a_bool_update_body_mass = read_bool(p) + if p.ok { dynamics.destroy_shape(a_shape_id_id, a_bool_update_body_mass) } + return true + } + if op == dynamics.OP_SHAPE_SET_DENSITY { + a_shape_id_id = read_shape_id(p) + a_float_density = read_float(p) + a_bool_update_body_mass = read_bool(p) + if p.ok { dynamics.shape_set_density(a_shape_id_id, a_float_density, a_bool_update_body_mass) } + return true + } + if op == dynamics.OP_SHAPE_SET_FILTER { + a_shape_id_id = read_shape_id(p) + a_filter_filter = read_filter(p) + if p.ok { dynamics.shape_set_filter(a_shape_id_id, a_filter_filter) } + return true + } + if op == dynamics.OP_SHAPE_SET_SURFACE_MATERIAL { + a_shape_id_id = read_shape_id(p) + a_surface_material_m = read_surface_material(p) + if p.ok { dynamics.shape_set_surface_material(a_shape_id_id, a_surface_material_m) } + return true + } + if op == dynamics.OP_SHAPE_SET_SPHERE { + a_shape_id_id = read_shape_id(p) + a_sphere_s = read_sphere(p) + if p.ok { dynamics.shape_set_sphere(a_shape_id_id, a_sphere_s) } + return true + } + if op == dynamics.OP_SHAPE_SET_CAPSULE { + a_shape_id_id = read_shape_id(p) + a_capsule_c = read_capsule(p) + if p.ok { dynamics.shape_set_capsule(a_shape_id_id, a_capsule_c) } + return true + } + if op == dynamics.OP_SHAPE_SET_HULL { + a_shape_id_id = read_shape_id(p) + a_ptr__hull_h = read_geometry(p, shape.SHAPE_HULL) as *HullData + if p.ok { dynamics.shape_set_hull(a_shape_id_id, a_ptr__hull_h) } + return true + } + if op == dynamics.OP_SHAPE_SET_MESH { + a_shape_id_id = read_shape_id(p) + a_ptr__mesh_m = read_geometry(p, shape.SHAPE_MESH) as *MeshData + a_vec3_scale = read_vec3(p) + if p.ok { dynamics.shape_set_mesh(a_shape_id_id, a_ptr__mesh_m, a_vec3_scale) } + return true + } + if op == dynamics.OP_SHAPE_SET_FRICTION { + a_shape_id_id = read_shape_id(p) + a_float_friction = read_float(p) + if p.ok { dynamics.shape_set_friction(a_shape_id_id, a_float_friction) } + return true + } + if op == dynamics.OP_SHAPE_SET_RESTITUTION { + a_shape_id_id = read_shape_id(p) + a_float_restitution = read_float(p) + if p.ok { dynamics.shape_set_restitution(a_shape_id_id, a_float_restitution) } + return true + } + if op == dynamics.OP_SHAPE_SET_MESH_MATERIAL { + a_shape_id_id = read_shape_id(p) + a_surface_material_m = read_surface_material(p) + a_int_index = read_int(p) + if p.ok { dynamics.shape_set_mesh_material(a_shape_id_id, a_surface_material_m, a_int_index) } + return true + } + if op == dynamics.OP_SHAPE_ENABLE_PRE_SOLVE_EVENTS { + a_shape_id_id = read_shape_id(p) + a_bool_flag = read_bool(p) + if p.ok { dynamics.shape_enable_pre_solve_events(a_shape_id_id, a_bool_flag) } + return true + } + if op == dynamics.OP_BODY_ENABLE_HIT_EVENTS { + a_body_id_id = read_body_id(p) + a_bool_flag = read_bool(p) + if p.ok { dynamics.body_enable_hit_events(a_body_id_id, a_bool_flag) } + return true + } + if op == dynamics.OP_BODY_ALLOW_FAST_ROTATION { + a_body_id_id = read_body_id(p) + a_bool_flag = read_bool(p) + if p.ok { dynamics.body_allow_fast_rotation(a_body_id_id, a_bool_flag) } + return true + } + if op == dynamics.OP_BODY_ENABLE_CONTACT_RECYCLING { + a_body_id_id = read_body_id(p) + a_bool_flag = read_bool(p) + if p.ok { dynamics.body_enable_contact_recycling(a_body_id_id, a_bool_flag) } + return true + } + if op == dynamics.OP_BODY_SET_SAFETY_FACTOR { + a_body_id_id = read_body_id(p) + a_float_factor = read_float(p) + if p.ok { dynamics.body_set_safety_factor(a_body_id_id, a_float_factor) } + return true + } + if op == dynamics.OP_SHAPE_APPLY_WIND { + a_shape_id_id = read_shape_id(p) + a_vec3_wind = read_vec3(p) + a_float_drag = read_float(p) + a_float_lift = read_float(p) + a_float_max_speed = read_float(p) + a_bool_wake = read_bool(p) + if p.ok { dynamics.shape_apply_wind(a_shape_id_id, a_vec3_wind, a_float_drag, a_float_lift, a_float_max_speed, a_bool_wake) } + return true + } + if op == dynamics.OP_BODY_SET_NAME { + a_body_id_id = read_body_id(p) + a_string_name = read_string(p) + if p.ok { dynamics.body_set_name(a_body_id_id, a_string_name) } + return true + } + if op == dynamics.OP_SHAPE_SET_NAME { + a_shape_id_id = read_shape_id(p) + a_string_name = read_string(p) + if p.ok { dynamics.shape_set_name(a_shape_id_id, a_string_name) } + return true + } + if op == dynamics.OP_CREATE_DISTANCE_JOINT { + a_world_id_world_id = p.world_id + a_distance_joint_def_def = read_distance_joint_def(p) + recorded_index1 = read_int(p) + recorded_generation = read_int(p) + if p.ok { + made_joint = dynamics.create_distance_joint(a_world_id_world_id, &a_distance_joint_def_def) + check_made(p, made_joint.index1, made_joint.generation, recorded_index1, recorded_generation) + } + return true + } + if op == dynamics.OP_CREATE_MOTOR_JOINT { + a_world_id_world_id = p.world_id + a_motor_joint_def_def = read_motor_joint_def(p) + recorded_index1 = read_int(p) + recorded_generation = read_int(p) + if p.ok { + made_joint = dynamics.create_motor_joint(a_world_id_world_id, &a_motor_joint_def_def) + check_made(p, made_joint.index1, made_joint.generation, recorded_index1, recorded_generation) + } + return true + } + if op == dynamics.OP_CREATE_FILTER_JOINT { + a_world_id_world_id = p.world_id + a_filter_joint_def_def = read_filter_joint_def(p) + recorded_index1 = read_int(p) + recorded_generation = read_int(p) + if p.ok { + made_joint = dynamics.create_filter_joint(a_world_id_world_id, &a_filter_joint_def_def) + check_made(p, made_joint.index1, made_joint.generation, recorded_index1, recorded_generation) + } + return true + } + if op == dynamics.OP_CREATE_PARALLEL_JOINT { + a_world_id_world_id = p.world_id + a_parallel_joint_def_def = read_parallel_joint_def(p) + recorded_index1 = read_int(p) + recorded_generation = read_int(p) + if p.ok { + made_joint = dynamics.create_parallel_joint(a_world_id_world_id, &a_parallel_joint_def_def) + check_made(p, made_joint.index1, made_joint.generation, recorded_index1, recorded_generation) + } + return true + } + if op == dynamics.OP_CREATE_PRISMATIC_JOINT { + a_world_id_world_id = p.world_id + a_prismatic_joint_def_def = read_prismatic_joint_def(p) + recorded_index1 = read_int(p) + recorded_generation = read_int(p) + if p.ok { + made_joint = dynamics.create_prismatic_joint(a_world_id_world_id, &a_prismatic_joint_def_def) + check_made(p, made_joint.index1, made_joint.generation, recorded_index1, recorded_generation) + } + return true + } + if op == dynamics.OP_CREATE_REVOLUTE_JOINT { + a_world_id_world_id = p.world_id + a_revolute_joint_def_def = read_revolute_joint_def(p) + recorded_index1 = read_int(p) + recorded_generation = read_int(p) + if p.ok { + made_joint = dynamics.create_revolute_joint(a_world_id_world_id, &a_revolute_joint_def_def) + check_made(p, made_joint.index1, made_joint.generation, recorded_index1, recorded_generation) + } + return true + } + if op == dynamics.OP_CREATE_SPHERICAL_JOINT { + a_world_id_world_id = p.world_id + a_spherical_joint_def_def = read_spherical_joint_def(p) + recorded_index1 = read_int(p) + recorded_generation = read_int(p) + if p.ok { + made_joint = dynamics.create_spherical_joint(a_world_id_world_id, &a_spherical_joint_def_def) + check_made(p, made_joint.index1, made_joint.generation, recorded_index1, recorded_generation) + } + return true + } + if op == dynamics.OP_CREATE_WELD_JOINT { + a_world_id_world_id = p.world_id + a_weld_joint_def_def = read_weld_joint_def(p) + recorded_index1 = read_int(p) + recorded_generation = read_int(p) + if p.ok { + made_joint = dynamics.create_weld_joint(a_world_id_world_id, &a_weld_joint_def_def) + check_made(p, made_joint.index1, made_joint.generation, recorded_index1, recorded_generation) + } + return true + } + if op == dynamics.OP_CREATE_WHEEL_JOINT { + a_world_id_world_id = p.world_id + a_wheel_joint_def_def = read_wheel_joint_def(p) + recorded_index1 = read_int(p) + recorded_generation = read_int(p) + if p.ok { + made_joint = dynamics.create_wheel_joint(a_world_id_world_id, &a_wheel_joint_def_def) + check_made(p, made_joint.index1, made_joint.generation, recorded_index1, recorded_generation) + } + return true + } + if op == dynamics.OP_DESTROY_JOINT { + a_joint_id_id = read_joint_id(p) + a_bool_wake_attached = read_bool(p) + if p.ok { dynamics.destroy_joint(a_joint_id_id, a_bool_wake_attached) } + return true + } + if op == dynamics.OP_JOINT_SET_LOCAL_FRAME_A { + a_joint_id_id = read_joint_id(p) + a_transform_frame = read_transform(p) + if p.ok { dynamics.joint_set_local_frame_a(a_joint_id_id, a_transform_frame) } + return true + } + if op == dynamics.OP_JOINT_SET_LOCAL_FRAME_B { + a_joint_id_id = read_joint_id(p) + a_transform_frame = read_transform(p) + if p.ok { dynamics.joint_set_local_frame_b(a_joint_id_id, a_transform_frame) } + return true + } + if op == dynamics.OP_JOINT_SET_COLLIDE_CONNECTED { + a_joint_id_id = read_joint_id(p) + a_bool_should_collide = read_bool(p) + if p.ok { dynamics.joint_set_collide_connected(a_joint_id_id, a_bool_should_collide) } + return true + } + if op == dynamics.OP_JOINT_WAKE_BODIES { + a_joint_id_id = read_joint_id(p) + if p.ok { dynamics.joint_wake_bodies(a_joint_id_id) } + return true + } + if op == dynamics.OP_JOINT_SET_CONSTRAINT_TUNING { + a_joint_id_id = read_joint_id(p) + a_float_hertz = read_float(p) + a_float_damping_ratio = read_float(p) + if p.ok { dynamics.joint_set_constraint_tuning(a_joint_id_id, a_float_hertz, a_float_damping_ratio) } + return true + } + if op == dynamics.OP_JOINT_SET_FORCE_THRESHOLD { + a_joint_id_id = read_joint_id(p) + a_float_threshold = read_float(p) + if p.ok { dynamics.joint_set_force_threshold(a_joint_id_id, a_float_threshold) } + return true + } + if op == dynamics.OP_JOINT_SET_TORQUE_THRESHOLD { + a_joint_id_id = read_joint_id(p) + a_float_threshold = read_float(p) + if p.ok { dynamics.joint_set_torque_threshold(a_joint_id_id, a_float_threshold) } + return true + } + if op == dynamics.OP_BODY_SET_TYPE { + a_body_id_id = read_body_id(p) + a_int_kind = read_int(p) + if p.ok { dynamics.body_set_type(a_body_id_id, a_int_kind) } + return true + } + if op == dynamics.OP_BODY_DISABLE { + a_body_id_id = read_body_id(p) + if p.ok { dynamics.body_disable(a_body_id_id) } + return true + } + if op == dynamics.OP_BODY_ENABLE { + a_body_id_id = read_body_id(p) + if p.ok { dynamics.body_enable(a_body_id_id) } + return true + } + if op == dynamics.OP_SHAPE_ENABLE_SENSOR_EVENTS { + a_shape_id_id = read_shape_id(p) + a_bool_flag = read_bool(p) + if p.ok { dynamics.shape_enable_sensor_events(a_shape_id_id, a_bool_flag) } + return true + } + if op == dynamics.OP_SHAPE_ENABLE_CONTACT_EVENTS { + a_shape_id_id = read_shape_id(p) + a_bool_flag = read_bool(p) + if p.ok { dynamics.shape_enable_contact_events(a_shape_id_id, a_bool_flag) } + return true + } + if op == dynamics.OP_SHAPE_ENABLE_HIT_EVENTS { + a_shape_id_id = read_shape_id(p) + a_bool_flag = read_bool(p) + if p.ok { dynamics.shape_enable_hit_events(a_shape_id_id, a_bool_flag) } + return true + } + if op == dynamics.OP_STEP { + a_world_id_id = p.world_id + a_float_time_step = read_float(p) + a_int_sub_step_count = read_int(p) + if p.ok { physics_world.step(a_world_id_id, a_float_time_step, a_int_sub_step_count) } + return true + } + if op == dynamics.OP_ENABLE_SLEEPING { + a_world_id_id = p.world_id + a_bool_flag = read_bool(p) + if p.ok { physics_world.enable_sleeping(a_world_id_id, a_bool_flag) } + return true + } + if op == dynamics.OP_ENABLE_WARM_STARTING { + a_world_id_id = p.world_id + a_bool_flag = read_bool(p) + if p.ok { physics_world.enable_warm_starting(a_world_id_id, a_bool_flag) } + return true + } + if op == dynamics.OP_ENABLE_CONTINUOUS { + a_world_id_id = p.world_id + a_bool_flag = read_bool(p) + if p.ok { physics_world.enable_continuous(a_world_id_id, a_bool_flag) } + return true + } + if op == dynamics.OP_ENABLE_SPECULATIVE { + a_world_id_id = p.world_id + a_bool_flag = read_bool(p) + if p.ok { physics_world.enable_speculative(a_world_id_id, a_bool_flag) } + return true + } + if op == dynamics.OP_SET_RESTITUTION_THRESHOLD { + a_world_id_id = p.world_id + a_float_value = read_float(p) + if p.ok { physics_world.set_restitution_threshold(a_world_id_id, a_float_value) } + return true + } + if op == dynamics.OP_SET_RESTITUTION_ITERATIONS { + a_world_id_id = p.world_id + a_int_count = read_int(p) + if p.ok { physics_world.set_restitution_iterations(a_world_id_id, a_int_count) } + return true + } + if op == dynamics.OP_ENABLE_RESTITUTION_PROPAGATION { + a_world_id_id = p.world_id + a_bool_flag = read_bool(p) + if p.ok { physics_world.enable_restitution_propagation(a_world_id_id, a_bool_flag) } + return true + } + if op == dynamics.OP_ENABLE_KINEMATIC_STATIC_SOFTNESS { + a_world_id_id = p.world_id + a_bool_flag = read_bool(p) + if p.ok { physics_world.enable_kinematic_static_softness(a_world_id_id, a_bool_flag) } + return true + } + if op == dynamics.OP_SET_HIT_EVENT_THRESHOLD { + a_world_id_id = p.world_id + a_float_value = read_float(p) + if p.ok { physics_world.set_hit_event_threshold(a_world_id_id, a_float_value) } + return true + } + if op == dynamics.OP_SET_CONTACT_TUNING { + a_world_id_id = p.world_id + a_float_hertz = read_float(p) + a_float_damping_ratio = read_float(p) + a_float_contact_speed = read_float(p) + if p.ok { physics_world.set_contact_tuning(a_world_id_id, a_float_hertz, a_float_damping_ratio, a_float_contact_speed) } + return true + } + if op == dynamics.OP_SET_CONTACT_RECYCLE_DISTANCE { + a_world_id_id = p.world_id + a_float_distance = read_float(p) + if p.ok { physics_world.set_contact_recycle_distance(a_world_id_id, a_float_distance) } + return true + } + if op == dynamics.OP_SET_MAXIMUM_LINEAR_SPEED { + a_world_id_id = p.world_id + a_float_speed = read_float(p) + if p.ok { physics_world.set_maximum_linear_speed(a_world_id_id, a_float_speed) } + return true + } + if op == dynamics.OP_SET_GRAVITY { + a_world_id_id = p.world_id + a_vec3_gravity = read_vec3(p) + if p.ok { physics_world.set_gravity(a_world_id_id, a_vec3_gravity) } + return true + } + if op == dynamics.OP_REBUILD_STATIC_TREE { + a_world_id_id = p.world_id + if p.ok { physics_world.rebuild_static_tree(a_world_id_id) } + return true + } + if op == dynamics.OP_EXPLODE { + a_world_id_id = p.world_id + a_explosion_def_def = read_explosion_def(p) + if p.ok { physics_world.explode(a_world_id_id, &a_explosion_def_def) } + return true + } + if op == dynamics.OP_DISTANCE_JOINT_SET_LENGTH { + a_joint_id_id = read_joint_id(p) + a_float_length = read_float(p) + if p.ok { joint_solver.distance_joint_set_length(a_joint_id_id, a_float_length) } + return true + } + if op == dynamics.OP_DISTANCE_JOINT_ENABLE_LIMIT { + a_joint_id_id = read_joint_id(p) + a_bool_flag = read_bool(p) + if p.ok { joint_solver.distance_joint_enable_limit(a_joint_id_id, a_bool_flag) } + return true + } + if op == dynamics.OP_DISTANCE_JOINT_SET_LENGTH_RANGE { + a_joint_id_id = read_joint_id(p) + a_float_min_length = read_float(p) + a_float_max_length = read_float(p) + if p.ok { joint_solver.distance_joint_set_length_range(a_joint_id_id, a_float_min_length, a_float_max_length) } + return true + } + if op == dynamics.OP_DISTANCE_JOINT_ENABLE_SPRING { + a_joint_id_id = read_joint_id(p) + a_bool_flag = read_bool(p) + if p.ok { joint_solver.distance_joint_enable_spring(a_joint_id_id, a_bool_flag) } + return true + } + if op == dynamics.OP_DISTANCE_JOINT_SET_SPRING_FORCE_RANGE { + a_joint_id_id = read_joint_id(p) + a_float_lower = read_float(p) + a_float_upper = read_float(p) + if p.ok { joint_solver.distance_joint_set_spring_force_range(a_joint_id_id, a_float_lower, a_float_upper) } + return true + } + if op == dynamics.OP_DISTANCE_JOINT_SET_SPRING_HERTZ { + a_joint_id_id = read_joint_id(p) + a_float_hertz = read_float(p) + if p.ok { joint_solver.distance_joint_set_spring_hertz(a_joint_id_id, a_float_hertz) } + return true + } + if op == dynamics.OP_DISTANCE_JOINT_SET_SPRING_DAMPING_RATIO { + a_joint_id_id = read_joint_id(p) + a_float_ratio = read_float(p) + if p.ok { joint_solver.distance_joint_set_spring_damping_ratio(a_joint_id_id, a_float_ratio) } + return true + } + if op == dynamics.OP_DISTANCE_JOINT_ENABLE_MOTOR { + a_joint_id_id = read_joint_id(p) + a_bool_flag = read_bool(p) + if p.ok { joint_solver.distance_joint_enable_motor(a_joint_id_id, a_bool_flag) } + return true + } + if op == dynamics.OP_DISTANCE_JOINT_SET_MOTOR_SPEED { + a_joint_id_id = read_joint_id(p) + a_float_speed = read_float(p) + if p.ok { joint_solver.distance_joint_set_motor_speed(a_joint_id_id, a_float_speed) } + return true + } + if op == dynamics.OP_DISTANCE_JOINT_SET_MAX_MOTOR_FORCE { + a_joint_id_id = read_joint_id(p) + a_float_force = read_float(p) + if p.ok { joint_solver.distance_joint_set_max_motor_force(a_joint_id_id, a_float_force) } + return true + } + if op == dynamics.OP_MOTOR_JOINT_SET_LINEAR_VELOCITY { + a_joint_id_id = read_joint_id(p) + a_vec3_v = read_vec3(p) + if p.ok { joint_solver.motor_joint_set_linear_velocity(a_joint_id_id, a_vec3_v) } + return true + } + if op == dynamics.OP_MOTOR_JOINT_SET_ANGULAR_VELOCITY { + a_joint_id_id = read_joint_id(p) + a_vec3_v = read_vec3(p) + if p.ok { joint_solver.motor_joint_set_angular_velocity(a_joint_id_id, a_vec3_v) } + return true + } + if op == dynamics.OP_MOTOR_JOINT_SET_MAX_VELOCITY_TORQUE { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.motor_joint_set_max_velocity_torque(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_MOTOR_JOINT_SET_MAX_VELOCITY_FORCE { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.motor_joint_set_max_velocity_force(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_MOTOR_JOINT_SET_LINEAR_HERTZ { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.motor_joint_set_linear_hertz(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_MOTOR_JOINT_SET_LINEAR_DAMPING_RATIO { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.motor_joint_set_linear_damping_ratio(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_MOTOR_JOINT_SET_ANGULAR_HERTZ { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.motor_joint_set_angular_hertz(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_MOTOR_JOINT_SET_ANGULAR_DAMPING_RATIO { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.motor_joint_set_angular_damping_ratio(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_MOTOR_JOINT_SET_MAX_SPRING_FORCE { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.motor_joint_set_max_spring_force(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_MOTOR_JOINT_SET_MAX_SPRING_TORQUE { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.motor_joint_set_max_spring_torque(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_PARALLEL_JOINT_SET_SPRING_HERTZ { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.parallel_joint_set_spring_hertz(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_PARALLEL_JOINT_SET_SPRING_DAMPING_RATIO { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.parallel_joint_set_spring_damping_ratio(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_PARALLEL_JOINT_SET_MAX_TORQUE { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.parallel_joint_set_max_torque(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_PRISMATIC_JOINT_ENABLE_SPRING { + a_joint_id_id = read_joint_id(p) + a_bool_flag = read_bool(p) + if p.ok { joint_solver.prismatic_joint_enable_spring(a_joint_id_id, a_bool_flag) } + return true + } + if op == dynamics.OP_PRISMATIC_JOINT_SET_SPRING_HERTZ { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.prismatic_joint_set_spring_hertz(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_PRISMATIC_JOINT_SET_SPRING_DAMPING_RATIO { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.prismatic_joint_set_spring_damping_ratio(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_PRISMATIC_JOINT_SET_TARGET_TRANSLATION { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.prismatic_joint_set_target_translation(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_PRISMATIC_JOINT_ENABLE_LIMIT { + a_joint_id_id = read_joint_id(p) + a_bool_flag = read_bool(p) + if p.ok { joint_solver.prismatic_joint_enable_limit(a_joint_id_id, a_bool_flag) } + return true + } + if op == dynamics.OP_PRISMATIC_JOINT_SET_LIMITS { + a_joint_id_id = read_joint_id(p) + a_float_lower = read_float(p) + a_float_upper = read_float(p) + if p.ok { joint_solver.prismatic_joint_set_limits(a_joint_id_id, a_float_lower, a_float_upper) } + return true + } + if op == dynamics.OP_PRISMATIC_JOINT_ENABLE_MOTOR { + a_joint_id_id = read_joint_id(p) + a_bool_flag = read_bool(p) + if p.ok { joint_solver.prismatic_joint_enable_motor(a_joint_id_id, a_bool_flag) } + return true + } + if op == dynamics.OP_PRISMATIC_JOINT_SET_MOTOR_SPEED { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.prismatic_joint_set_motor_speed(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_PRISMATIC_JOINT_SET_MAX_MOTOR_FORCE { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.prismatic_joint_set_max_motor_force(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_REVOLUTE_JOINT_ENABLE_LIMIT { + a_joint_id_id = read_joint_id(p) + a_bool_flag = read_bool(p) + if p.ok { joint_solver.revolute_joint_enable_limit(a_joint_id_id, a_bool_flag) } + return true + } + if op == dynamics.OP_REVOLUTE_JOINT_SET_LIMITS { + a_joint_id_id = read_joint_id(p) + a_float_lower = read_float(p) + a_float_upper = read_float(p) + if p.ok { joint_solver.revolute_joint_set_limits(a_joint_id_id, a_float_lower, a_float_upper) } + return true + } + if op == dynamics.OP_REVOLUTE_JOINT_ENABLE_SPRING { + a_joint_id_id = read_joint_id(p) + a_bool_flag = read_bool(p) + if p.ok { joint_solver.revolute_joint_enable_spring(a_joint_id_id, a_bool_flag) } + return true + } + if op == dynamics.OP_REVOLUTE_JOINT_SET_TARGET_ANGLE { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.revolute_joint_set_target_angle(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_REVOLUTE_JOINT_SET_SPRING_HERTZ { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.revolute_joint_set_spring_hertz(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_REVOLUTE_JOINT_SET_SPRING_DAMPING_RATIO { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.revolute_joint_set_spring_damping_ratio(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_REVOLUTE_JOINT_ENABLE_MOTOR { + a_joint_id_id = read_joint_id(p) + a_bool_flag = read_bool(p) + if p.ok { joint_solver.revolute_joint_enable_motor(a_joint_id_id, a_bool_flag) } + return true + } + if op == dynamics.OP_REVOLUTE_JOINT_SET_MOTOR_SPEED { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.revolute_joint_set_motor_speed(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_REVOLUTE_JOINT_SET_MAX_MOTOR_TORQUE { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.revolute_joint_set_max_motor_torque(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_SPHERICAL_JOINT_ENABLE_CONE_LIMIT { + a_joint_id_id = read_joint_id(p) + a_bool_flag = read_bool(p) + if p.ok { joint_solver.spherical_joint_enable_cone_limit(a_joint_id_id, a_bool_flag) } + return true + } + if op == dynamics.OP_SPHERICAL_JOINT_SET_CONE_LIMIT { + a_joint_id_id = read_joint_id(p) + a_float_angle = read_float(p) + if p.ok { joint_solver.spherical_joint_set_cone_limit(a_joint_id_id, a_float_angle) } + return true + } + if op == dynamics.OP_SPHERICAL_JOINT_SET_CONE_AXIS { + a_joint_id_id = read_joint_id(p) + a_vec3_axis = read_vec3(p) + if p.ok { joint_solver.spherical_joint_set_cone_axis(a_joint_id_id, a_vec3_axis) } + return true + } + if op == dynamics.OP_SPHERICAL_JOINT_SET_CONE_ELLIPSE { + a_joint_id_id = read_joint_id(p) + a_float_major_angle = read_float(p) + a_float_minor_angle = read_float(p) + a_vec3_major_axis = read_vec3(p) + if p.ok { joint_solver.spherical_joint_set_cone_ellipse(a_joint_id_id, a_float_major_angle, a_float_minor_angle, a_vec3_major_axis) } + return true + } + if op == dynamics.OP_SPHERICAL_JOINT_ENABLE_TWIST_LIMIT { + a_joint_id_id = read_joint_id(p) + a_bool_flag = read_bool(p) + if p.ok { joint_solver.spherical_joint_enable_twist_limit(a_joint_id_id, a_bool_flag) } + return true + } + if op == dynamics.OP_SPHERICAL_JOINT_SET_TWIST_LIMITS { + a_joint_id_id = read_joint_id(p) + a_float_lower = read_float(p) + a_float_upper = read_float(p) + if p.ok { joint_solver.spherical_joint_set_twist_limits(a_joint_id_id, a_float_lower, a_float_upper) } + return true + } + if op == dynamics.OP_SPHERICAL_JOINT_ENABLE_SPRING { + a_joint_id_id = read_joint_id(p) + a_bool_flag = read_bool(p) + if p.ok { joint_solver.spherical_joint_enable_spring(a_joint_id_id, a_bool_flag) } + return true + } + if op == dynamics.OP_SPHERICAL_JOINT_SET_TARGET_ROTATION { + a_joint_id_id = read_joint_id(p) + a_quat_q = read_quat(p) + if p.ok { joint_solver.spherical_joint_set_target_rotation(a_joint_id_id, a_quat_q) } + return true + } + if op == dynamics.OP_SPHERICAL_JOINT_SET_SPRING_HERTZ { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.spherical_joint_set_spring_hertz(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_SPHERICAL_JOINT_SET_SPRING_DAMPING_RATIO { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.spherical_joint_set_spring_damping_ratio(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_SPHERICAL_JOINT_ENABLE_MOTOR { + a_joint_id_id = read_joint_id(p) + a_bool_flag = read_bool(p) + if p.ok { joint_solver.spherical_joint_enable_motor(a_joint_id_id, a_bool_flag) } + return true + } + if op == dynamics.OP_SPHERICAL_JOINT_SET_MOTOR_VELOCITY { + a_joint_id_id = read_joint_id(p) + a_vec3_v = read_vec3(p) + if p.ok { joint_solver.spherical_joint_set_motor_velocity(a_joint_id_id, a_vec3_v) } + return true + } + if op == dynamics.OP_SPHERICAL_JOINT_SET_MAX_MOTOR_TORQUE { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.spherical_joint_set_max_motor_torque(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_WELD_JOINT_SET_LINEAR_HERTZ { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.weld_joint_set_linear_hertz(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_WELD_JOINT_SET_LINEAR_DAMPING_RATIO { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.weld_joint_set_linear_damping_ratio(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_WELD_JOINT_SET_ANGULAR_HERTZ { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.weld_joint_set_angular_hertz(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_WELD_JOINT_SET_ANGULAR_DAMPING_RATIO { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.weld_joint_set_angular_damping_ratio(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_WHEEL_JOINT_ENABLE_SUSPENSION { + a_joint_id_id = read_joint_id(p) + a_bool_flag = read_bool(p) + if p.ok { joint_solver.wheel_joint_enable_suspension(a_joint_id_id, a_bool_flag) } + return true + } + if op == dynamics.OP_WHEEL_JOINT_SET_SUSPENSION_HERTZ { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.wheel_joint_set_suspension_hertz(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_WHEEL_JOINT_SET_SUSPENSION_DAMPING_RATIO { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.wheel_joint_set_suspension_damping_ratio(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_WHEEL_JOINT_ENABLE_SUSPENSION_LIMIT { + a_joint_id_id = read_joint_id(p) + a_bool_flag = read_bool(p) + if p.ok { joint_solver.wheel_joint_enable_suspension_limit(a_joint_id_id, a_bool_flag) } + return true + } + if op == dynamics.OP_WHEEL_JOINT_SET_SUSPENSION_LIMITS { + a_joint_id_id = read_joint_id(p) + a_float_lower = read_float(p) + a_float_upper = read_float(p) + if p.ok { joint_solver.wheel_joint_set_suspension_limits(a_joint_id_id, a_float_lower, a_float_upper) } + return true + } + if op == dynamics.OP_WHEEL_JOINT_ENABLE_SPIN_MOTOR { + a_joint_id_id = read_joint_id(p) + a_bool_flag = read_bool(p) + if p.ok { joint_solver.wheel_joint_enable_spin_motor(a_joint_id_id, a_bool_flag) } + return true + } + if op == dynamics.OP_WHEEL_JOINT_SET_SPIN_MOTOR_SPEED { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.wheel_joint_set_spin_motor_speed(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_WHEEL_JOINT_SET_MAX_SPIN_TORQUE { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.wheel_joint_set_max_spin_torque(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_WHEEL_JOINT_ENABLE_STEERING { + a_joint_id_id = read_joint_id(p) + a_bool_flag = read_bool(p) + if p.ok { joint_solver.wheel_joint_enable_steering(a_joint_id_id, a_bool_flag) } + return true + } + if op == dynamics.OP_WHEEL_JOINT_SET_STEERING_HERTZ { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.wheel_joint_set_steering_hertz(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_WHEEL_JOINT_SET_STEERING_DAMPING_RATIO { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.wheel_joint_set_steering_damping_ratio(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_WHEEL_JOINT_SET_MAX_STEERING_TORQUE { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.wheel_joint_set_max_steering_torque(a_joint_id_id, a_float_value) } + return true + } + if op == dynamics.OP_WHEEL_JOINT_ENABLE_STEERING_LIMIT { + a_joint_id_id = read_joint_id(p) + a_bool_flag = read_bool(p) + if p.ok { joint_solver.wheel_joint_enable_steering_limit(a_joint_id_id, a_bool_flag) } + return true + } + if op == dynamics.OP_WHEEL_JOINT_SET_STEERING_LIMITS { + a_joint_id_id = read_joint_id(p) + a_float_lower = read_float(p) + a_float_upper = read_float(p) + if p.ok { joint_solver.wheel_joint_set_steering_limits(a_joint_id_id, a_float_lower, a_float_upper) } + return true + } + if op == dynamics.OP_WHEEL_JOINT_SET_TARGET_STEERING_ANGLE { + a_joint_id_id = read_joint_id(p) + a_float_value = read_float(p) + if p.ok { joint_solver.wheel_joint_set_target_steering_angle(a_joint_id_id, a_float_value) } + return true + } + return false +} diff --git a/aephysics/snapshot/module.ae b/aephysics/snapshot/module.ae index 31965b3..f51f80b 100644 --- a/aephysics/snapshot/module.ae +++ b/aephysics/snapshot/module.ae @@ -35,7 +35,7 @@ import aephysics.mesh_contact import aephysics.dynamics exports ( - SNAPSHOT_MAGIC, SNAPSHOT_VERSION, snapshot_layout, save_world, load_world, free_snapshot + SNAPSHOT_MAGIC, SNAPSHOT_VERSION, snapshot_layout, save_world, load_world, restore_world, free_snapshot ) extern memcpy(dst: ptr, src: ptr, size: int) -> ptr @@ -594,6 +594,26 @@ load_world(def: *WorldDef, data: ptr, size: int) -> WorldId { return world_id } +// The world `world_id` made what the snapshot holds, in place: its id, +// workers, callbacks, filters and user data stay (the reference's +// deserialize into a shell, what a replay rewinds with). False when the +// world is not one, is mid-step or records, or when the bytes are not a +// snapshot of this layout or fail any check; the world is then left empty, +// as a new world from `def` is, never half loaded. `def` gives the +// capacities the emptied world starts from. +restore_world(world_id: WorldId, def: *WorldDef, data: ptr, size: int) -> bool { + if data == null || size < 16 { return false } + rd = SnapReader { data: data, size: size, at: 0, ok: true } + if r_int(&rd) != SNAPSHOT_MAGIC || r_int(&rd) != SNAPSHOT_VERSION || r_long(&rd) != snapshot_layout() { return false } + if dynamics.reset_world(world_id, def) == false { return false } + w = dynamics.get_world(world_id.index1 - 1) + if fill_world(w, &rd) == false || rd.at != rd.size || world_valid(w) == false { + dynamics.reset_world(world_id, def) + return false + } + return true +} + // The world's own invariants over everything loaded. world_valid(w: *World) -> bool { if dynamics.validate_solver_sets(w) == false { return false } diff --git a/design.md b/design.md index 3382411..0109577 100644 --- a/design.md +++ b/design.md @@ -365,7 +365,17 @@ started until its tests pass. 15. **recording and replay**, `world_snapshot`: last, since they are the tooling and not the engine. The snapshot is in (`aephysics.snapshot`, #149's first part): a loaded world steps on bit for bit as the one it - was taken from. Recording, replay and the name cache build on it. + was taken from. Recording and replay are in (`aephysics.recording`, + #149's second part): the hooks at the top of every call that changes a + world, generated from the calls' signatures, write the record before + the call runs, so nothing the call does is recorded twice; the player + restores its world in place (`dynamics.reset_world`, the reference's + deserialize into a shell), so ids handed to a viewer stay valid through + a seek. Recording the world queries, and the name cache, are next. Two + functions no test called hid type errors (`body_apply_force`, + `body_get_local_point_velocity`) until the replay dispatcher called + every recorded one: `ae build` checks only what a program reaches + (aether#2613), so the gate now runs `ae check` on every module. 16. **benchmarks** (done, #141): `reference/benchmark/main.c`'s thirteen scenes and its SAT runs, against the reference's own scene functions on the same machine: the large pyramid, the many pyramids and the diff --git a/scripts/test.sh b/scripts/test.sh index 63f19a9..4763df9 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -11,6 +11,18 @@ cd "$root" export AETHER_AEPHYSICS_CFLAGS="${AETHER_AEPHYSICS_CFLAGS:-}" mkdir -p target failures=0 +# Every module checked whole first: building a test checks only what the +# test reaches, so a function nothing calls yet, or a call into a module +# the caller does not import, builds clean until something calls it +# (aether#2613, aether#2614). A warning fails it too. +for module in aephysics/*/module.ae; do + name="$(basename "$(dirname "$module")")" + if ! AETHER_LIB_DIR="$root" ae check "$module" >"target/check_$name.log" 2>&1 || grep -q "warning\[" "target/check_$name.log"; then + echo "FAIL check $name" + grep -A3 "error\[\|warning\[" "target/check_$name.log" | head -12 + failures=$((failures + 1)) + fi +done for test in tests/test_*.ae; do name="$(basename "$test" .ae)" if ! AETHER_LIB_DIR="$root" ae build "$test" -o "target/$name" >"target/$name.log" 2>&1; then diff --git a/tests/test_dynamics.ae b/tests/test_dynamics.ae index 7986701..711d6e7 100644 --- a/tests/test_dynamics.ae +++ b/tests/test_dynamics.ae @@ -469,6 +469,16 @@ test_sets_and_shapes() { pv = dynamics.body_get_world_point_velocity(awake_id, math.add(dynamics.body_get_world_center(awake_id), math.vec3(1.0, 0.0, 0.0))) small("point velocity x", pv.x - 1.0, 0.00001) small("point velocity y", pv.y - 1.0, 0.00001) + // The same by the point's body coordinates (b3Body_GetLocalPointVelocity), + // the body turned a quarter about z: the local x axis points along world y. + dynamics.body_set_transform(awake_id, math.vec3(10.0, 0.0, 0.0), math.make_quat_from_axis_angle(math.vec3(0.0, 0.0, 1.0), 0.5 * math.PI)) + local_point = math.add(dynamics.body_get_local_center(awake_id), math.vec3(1.0, 0.0, 0.0)) + lv = dynamics.body_get_local_point_velocity(awake_id, local_point) + wv2 = dynamics.body_get_world_point_velocity(awake_id, dynamics.body_get_world_point(awake_id, local_point)) + small("local point velocity x", lv.x, 0.00001) + small("local point velocity y", lv.y, 0.00001) + small("local and world point velocities agree", math.length(math.sub(lv, wv2)), 0.00001) + dynamics.body_set_transform(awake_id, math.vec3(10.0, 0.0, 0.0), math.quat_identity()) // Damping, gravity scale, user data, sleep. dynamics.body_set_linear_damping(awake_id, 0.3) dynamics.body_set_angular_damping(awake_id, 0.4) @@ -1248,6 +1258,37 @@ test_shape_flags() { dynamics.destroy_world(world_id) } +// A force at a point (b3Body_ApplyForce): the force, and the torque of +// its arm about the centre of mass, as a force at the centre and that +// torque give. +test_point_force() { + world_id = new_world() + w = dynamics.get_world(world_id.index1 - 1) + sdef = dynamics.default_shape_def() + sdef.density = 1.0 + bdef = dynamic_body_def() + bdef.position = math.vec3(2.0, 3.0, 4.0) + pushed = dynamics.create_body(world_id, &bdef) + dynamics.create_sphere_shape(pushed, &sdef, manifold.sphere(math.vec3_zero(), 0.5)) + bdef.position = math.vec3(12.0, 3.0, 4.0) + twin = dynamics.create_body(world_id, &bdef) + dynamics.create_sphere_shape(twin, &sdef, manifold.sphere(math.vec3_zero(), 0.5)) + force = math.vec3(0.0, 10.0, 0.0) + arm = math.vec3(1.0, 0.0, 0.0) + dynamics.body_apply_force(pushed, force, math.add(dynamics.body_get_world_center(pushed), arm), true) + dynamics.body_apply_force_to_center(twin, force, true) + dynamics.body_apply_torque(twin, math.cross(arm, force), true) + a = dynamics.get_body_sim(w, dynamics.get_body_full_id(w, pushed)) + b = dynamics.get_body_sim(w, dynamics.get_body_full_id(w, twin)) + ensure("point force: the force", a.force.x == b.force.x && a.force.y == b.force.y && a.force.z == b.force.z && a.force.y == (10.0 as f32)) + ensure("point force: the torque of its arm", a.torque.x == b.torque.x && a.torque.y == b.torque.y && a.torque.z == b.torque.z && a.torque.z == (10.0 as f32)) + // Not finite: refused, the accumulators untouched. + zero = math.length(math.vec3_zero()) + dynamics.body_apply_force(pushed, math.vec3(0.0, 0.0 - 1.0 / zero, 0.0), math.vec3_zero(), true) + ensure("point force: a force not finite is refused", a.force.y == (10.0 as f32) && a.torque.z == (10.0 as f32)) + dynamics.destroy_world(world_id) +} + // test_shape.c's ShapeNameTest: no name from a default def, the def's name // carried, the setter overwriting, a long name kept whole (the reference's // name cache keeps it so), null clearing it; a body's name from its def. @@ -1757,6 +1798,7 @@ main() { test_shape_api() test_shape_flags() test_shape_names() + test_point_force() test_sensors() hull.destroy_hull(g_cube as *HullData) hull.destroy_hull(g_slab as *HullData) diff --git a/tests/test_recording.ae b/tests/test_recording.ae new file mode 100644 index 0000000..b59a06a --- /dev/null +++ b/tests/test_recording.ae @@ -0,0 +1,1329 @@ +// aephysics.recording (#149, the reference's test_recording.c): the +// snapshot test's scene recorded from an empty world and from mid stream, +// with the host changing it between steps (velocities, impulses, names, +// materials, filters, types, locks, joints made, driven and destroyed, +// bodies destroyed and their slots reused, geometry the host frees at +// once), played back frame by frame on one worker and several: the +// player's world hashes as the recorded one every frame and its bodies end +// where the recorded ones did. Geometry is kept once however often it is +// named; seeking back and forth through keyframes lands on the recorded +// frames; a recording saved to a file plays the same; bytes that are not a +// recording load no player and leave nothing allocated; a record the +// player does not know stops it and a state hash that differs marks the +// frame it was found at. + +import std.string +import aephysics.math +import aephysics.basics +import aephysics.hull +import aephysics.mesh +import aephysics.height_field +import aephysics.material +import aephysics.compound +import aephysics.shape +import aephysics.manifold +import aephysics.dynamics +import aephysics.joint_solver +import aephysics.physics_world +import aephysics.human +import aephysics.recording + +extern calloc(count: int, size: int) -> ptr +extern exit(code: int) +extern free(p: ptr) +extern memcmp(a: ptr, b: ptr, size: int) -> int +extern memcpy(dst: ptr, src: ptr, size: int) -> ptr + +var failures = 0 +var checks = 0 + +ensure(name: string, ok: bool) { + checks = checks + 1 + if !ok { + println("recording: FAIL ${name}") + failures = failures + 1 + } +} + +// The geometry the scene's shapes are made from, kept until the end: the +// hulls go into the worlds' databases, the rest the worlds point at. +var g_box: ptr = null +var g_rock: ptr = null +var g_wave: ptr = null +var g_field: ptr = null +var g_compound: ptr = null +var g_ragdoll: ptr = null + +fixtures_ready() { + if g_box != null { return } + g_box = hull.make_box_hull(0.4, 0.4, 0.4) as ptr + g_rock = hull.create_rock(0.35) as ptr + g_wave = mesh.create_wave_mesh(20, 20, 1.0, 0.4, 0.3, 0.2) as ptr + g_field = height_field.create_grid(12, 12, math.vec3(1.0, 1.0, 1.0), true) as ptr + hulls_block = calloc(2, sizeof(CompoundHullDef)) + defs = hulls_block as CompoundHullDef[] + mat = material.default_surface_material() + defs[0] = compound.compound_hull_def(g_box as *HullData, Transform { p: math.vec3(0.0 - 1.0, 0.0, 0.0), q: math.quat_identity() }, mat) + defs[1] = compound.compound_hull_def(g_box as *HullData, Transform { p: math.vec3(1.0, 0.5, 0.0), q: math.quat_identity() }, mat) + cdef = compound.compound_def() + cdef.hulls = hulls_block + cdef.hull_count = 2 + g_compound = compound.create_compound(&cdef) as ptr + free(hulls_block) + g_ragdoll = calloc(1, sizeof(Human)) +} + +fixtures_free() { + hull.destroy_hull(g_box as *HullData) + hull.destroy_hull(g_rock as *HullData) + mesh.destroy_mesh(g_wave as *MeshData) + height_field.destroy_height_field(g_field as *HeightFieldData) + compound.destroy_compound(g_compound as *CompoundData) + free(g_ragdoll) +} + +world_def() -> WorldDef { + wdef = dynamics.default_world_def() + wdef.gravity = math.vec3(0.0, 0.0 - 10.0, 0.0) + return wdef +} + +static_body(world_id: WorldId, p: Vec3, name: string) -> BodyId { + bdef = dynamics.default_body_def() + bdef.position = p + bdef.name = name + return dynamics.create_body(world_id, &bdef) +} + +dynamic_body(world_id: WorldId, p: Vec3) -> BodyId { + bdef = dynamics.default_body_def() + bdef.kind = dynamics.BODY_DYNAMIC + bdef.position = p + return dynamics.create_body(world_id, &bdef) +} + +// The scene: every kind of thing a world holds. +build_scene(world_id: WorldId) { + fixtures_ready() + sdef = dynamics.default_shape_def() + floor_def = dynamics.default_shape_def() + floor_def.name = "floor" + ground = static_body(world_id, math.vec3(0.0, 0.0 - 1.0, 0.0), "ground") + floor_hull = hull.make_box_hull(30.0, 1.0, 30.0) + dynamics.create_hull_shape(ground, &floor_def, floor_hull) + hull.destroy_hull(floor_hull) + + // A mesh with three materials, a height field with holes, a compound. + materials_block = calloc(3, sizeof(SurfaceMaterial)) + materials = materials_block as SurfaceMaterial[] + k = 0 + while k < 3 { + materials[k] = material.default_surface_material() + materials[k].friction = 0.2 + 0.3 * (k as float) + k = k + 1 + } + mdef = dynamics.default_shape_def() + mdef.materials = materials_block + mdef.material_count = 3 + dynamics.create_mesh_shape(static_body(world_id, math.vec3(8.0, 0.0, 0.0 - 4.0), "waves"), &mdef, g_wave as *MeshData, math.vec3_one()) + free(materials_block) + dynamics.create_height_field_shape(static_body(world_id, math.vec3(0.0 - 14.0, 0.0, 0.0 - 6.0), "field"), &sdef, g_field as *HeightFieldData) + dynamics.create_compound_shape(static_body(world_id, math.vec3(0.0 - 6.0, 0.4, 6.0), "compound"), &sdef, g_compound as *CompoundData) + + // A sensor over the pyramid, which the boxes report to. + sensor_def = dynamics.default_shape_def() + sensor_def.is_sensor = true + sensor_def.enable_sensor_events = true + sensor_hull = hull.make_box_hull(2.0, 2.0, 2.0) + dynamics.create_hull_shape(static_body(world_id, math.vec3(0.0, 1.0, 0.0), "sensor"), &sensor_def, sensor_hull) + hull.destroy_hull(sensor_hull) + + // A pyramid of boxes, each reporting to the sensor; one named. + box_def = dynamics.default_shape_def() + box_def.enable_sensor_events = true + row = 0 + while row < 5 { + col = 0 + while col < 5 - row { + b = dynamic_body(world_id, math.vec3(0.82 * (col as float) - 0.41 * ((4 - row) as float), 0.4 + 0.81 * (row as float), 0.0)) + if row == 4 { dynamics.body_set_name(b, "apex") } + dynamics.create_hull_shape(b, &box_def, g_box as *HullData) + col = col + 1 + } + row = row + 1 + } + + // Spheres, capsules and rocks onto the mesh, the field and the compound. + i = 0 + while i < 12 { + x = 6.0 + 0.9 * ((i % 4) as float) + z = 0.0 - 6.0 + 1.1 * ((i / 4) as float) + b = dynamic_body(world_id, math.vec3(x, 2.0 + 0.3 * (i as float), z)) + if i % 3 == 0 { dynamics.create_sphere_shape(b, &sdef, manifold.sphere(math.vec3_zero(), 0.3)) } + else if i % 3 == 1 { dynamics.create_capsule_shape(b, &sdef, Capsule { center1: math.vec3(0.0 - 0.3, 0.0, 0.0), center2: math.vec3(0.3, 0.0, 0.0), radius: 0.2 }) } + else { dynamics.create_hull_shape(b, &sdef, g_rock as *HullData) } + f = dynamic_body(world_id, math.vec3(0.0 - 14.0 + 1.3 * ((i % 4) as float), 2.5 + 0.2 * (i as float), 0.0 - 4.0 + 1.2 * ((i / 4) as float))) + dynamics.create_sphere_shape(f, &sdef, manifold.sphere(math.vec3_zero(), 0.35)) + c = dynamic_body(world_id, math.vec3(0.0 - 6.5 + 0.8 * ((i % 3) as float), 3.0 + 0.4 * (i as float), 6.0)) + dynamics.create_hull_shape(c, &sdef, g_rock as *HullData) + i = i + 1 + } + + // A chain of capsules on hinges from an anchor, its first hinge driven. + anchor = static_body(world_id, math.vec3(10.0, 8.0, 8.0), "anchor") + previous = anchor + i = 0 + while i < 6 { + link = dynamic_body(world_id, math.vec3(10.0 + 0.6 * (i as float) + 0.3, 8.0, 8.0)) + dynamics.create_capsule_shape(link, &sdef, Capsule { center1: math.vec3(0.0 - 0.25, 0.0, 0.0), center2: math.vec3(0.25, 0.0, 0.0), radius: 0.1 }) + rdef = dynamics.default_revolute_joint_def() + rdef.base.body_id_a = previous + rdef.base.body_id_b = link + rdef.base.local_frame_a = Transform { p: math.vec3(0.3, 0.0, 0.0), q: math.quat_identity() } + if i == 0 { rdef.base.local_frame_a = math.transform_identity() } + rdef.base.local_frame_b = Transform { p: math.vec3(0.0 - 0.3, 0.0, 0.0), q: math.quat_identity() } + if i == 0 { + rdef.enable_motor = true + rdef.motor_speed = 1.5 + rdef.max_motor_torque = 50.0 + } + dynamics.create_revolute_joint(world_id, &rdef) + previous = link + i = i + 1 + } + + // A ragdoll with ankles, its filter joints among them. + h = g_ragdoll as *Human + h.bones = null + h.filter_joints = null + h.filter_joint_count = 0 + h.is_spawned = false + h.ankles = true + human.create_human(h, world_id, math.vec3(4.0, 3.0, 6.0), 2.0, 1.0, 0.5, 1, null) + + // A pair of each other joint kind, off to the side. + joint_pair(world_id, 0, math.vec3(14.0, 3.0, 0.0 - 10.0)) + joint_pair(world_id, 1, math.vec3(16.0, 3.0, 0.0 - 10.0)) + joint_pair(world_id, 2, math.vec3(18.0, 3.0, 0.0 - 10.0)) + joint_pair(world_id, 3, math.vec3(20.0, 3.0, 0.0 - 10.0)) + joint_pair(world_id, 4, math.vec3(22.0, 3.0, 0.0 - 10.0)) + joint_pair(world_id, 5, math.vec3(24.0, 3.0, 0.0 - 10.0)) + + // A body disabled, in the disabled set with its shape out of the trees. + sleeper = dynamic_body(world_id, math.vec3(0.0 - 20.0, 4.0, 10.0)) + dynamics.create_hull_shape(sleeper, &sdef, g_box as *HullData) + dynamics.body_disable(sleeper) + + // A kinematic paddle sweeping and a bullet at the pyramid. + kdef = dynamics.default_body_def() + kdef.kind = dynamics.BODY_KINEMATIC + kdef.position = math.vec3(0.0 - 3.0, 0.5, 3.0) + kdef.linear_velocity = math.vec3(1.0, 0.0, 0.0) + kdef.angular_velocity = math.vec3(0.0, 1.0, 0.0) + paddle = dynamics.create_body(world_id, &kdef) + paddle_hull = hull.make_box_hull(1.0, 0.2, 0.2) + dynamics.create_hull_shape(paddle, &sdef, paddle_hull) + hull.destroy_hull(paddle_hull) + bdef = dynamics.default_body_def() + bdef.kind = dynamics.BODY_DYNAMIC + bdef.position = math.vec3(0.0, 1.5, 12.0) + bdef.linear_velocity = math.vec3(0.0, 0.0, 0.0 - 60.0) + bdef.is_bullet = true + bdef.name = "bullet" + bullet = dynamics.create_body(world_id, &bdef) + dynamics.create_sphere_shape(bullet, &sdef, manifold.sphere(math.vec3_zero(), 0.1)) +} + +// Two boxes joined by a joint of one kind: a weld, a prismatic spring, a +// rope, a wheel, a motor or a parallel joint. +joint_pair(world_id: WorldId, kind: int, p: Vec3) { + sdef = dynamics.default_shape_def() + a = dynamic_body(world_id, p) + dynamics.create_hull_shape(a, &sdef, g_box as *HullData) + b = dynamic_body(world_id, math.add(p, math.vec3(0.0, 0.0, 1.2))) + dynamics.create_hull_shape(b, &sdef, g_box as *HullData) + frame_a = Transform { p: math.vec3(0.0, 0.0, 0.6), q: math.quat_identity() } + frame_b = Transform { p: math.vec3(0.0, 0.0, 0.0 - 0.6), q: math.quat_identity() } + if kind == 0 { + weld_def = dynamics.default_weld_joint_def() + weld_def.base.body_id_a = a + weld_def.base.body_id_b = b + weld_def.base.local_frame_a = frame_a + weld_def.base.local_frame_b = frame_b + weld_def.linear_hertz = 5.0 + dynamics.create_weld_joint(world_id, &weld_def) + } else if kind == 1 { + prismatic_def = dynamics.default_prismatic_joint_def() + prismatic_def.base.body_id_a = a + prismatic_def.base.body_id_b = b + prismatic_def.base.local_frame_a = frame_a + prismatic_def.base.local_frame_b = frame_b + prismatic_def.enable_spring = true + prismatic_def.hertz = 2.0 + prismatic_def.damping_ratio = 0.3 + prismatic_def.enable_limit = true + prismatic_def.lower_translation = 0.0 - 0.3 + prismatic_def.upper_translation = 0.3 + dynamics.create_prismatic_joint(world_id, &prismatic_def) + } else if kind == 2 { + rope_def = dynamics.default_distance_joint_def() + rope_def.base.body_id_a = a + rope_def.base.body_id_b = b + rope_def.base.local_frame_a = frame_a + rope_def.base.local_frame_b = frame_b + rope_def.length = 0.2 + rope_def.enable_limit = true + rope_def.min_length = 0.1 + rope_def.max_length = 0.5 + dynamics.create_distance_joint(world_id, &rope_def) + } else if kind == 3 { + wheel_def = dynamics.default_wheel_joint_def() + wheel_def.base.body_id_a = a + wheel_def.base.body_id_b = b + wheel_def.base.local_frame_a = frame_a + wheel_def.base.local_frame_b = frame_b + wheel_def.enable_spin_motor = true + wheel_def.spin_speed = 4.0 + wheel_def.max_spin_torque = 10.0 + dynamics.create_wheel_joint(world_id, &wheel_def) + } else if kind == 4 { + motor_def = dynamics.default_motor_joint_def() + motor_def.base.body_id_a = a + motor_def.base.body_id_b = b + motor_def.base.local_frame_a = frame_a + motor_def.base.local_frame_b = frame_b + motor_def.angular_velocity = math.vec3(0.0, 1.0, 0.0) + motor_def.max_velocity_torque = 20.0 + dynamics.create_motor_joint(world_id, &motor_def) + } else { + parallel_def = dynamics.default_parallel_joint_def() + parallel_def.base.body_id_a = a + parallel_def.base.body_id_b = b + parallel_def.base.local_frame_a = frame_a + parallel_def.base.local_frame_b = frame_b + parallel_def.hertz = 3.0 + parallel_def.max_torque = 30.0 + dynamics.create_parallel_joint(world_id, ¶llel_def) + } +} + +// The events of the last step, folded into one number to compare. +events_of(world_id: WorldId) -> long { + w = dynamics.get_world(world_id.index1 - 1) + folded = dynamics.world_contact_begin_event_count(w) as long + folded = folded * (1009 as long) + (dynamics.world_contact_end_event_count(w) as long) + folded = folded * (1009 as long) + (dynamics.world_sensor_begin_event_count(w) as long) + folded = folded * (1009 as long) + (dynamics.world_sensor_end_event_count(w) as long) + folded = folded * (1009 as long) + (dynamics.world_contact_hit_event_count(w) as long) + return folded +} + +state_hash(world_id: WorldId) -> long { return dynamics.hash_world_state(dynamics.get_world(world_id.index1 - 1)) } + +steps(world_id: WorldId, count: int) { + i = 0 + while i < count { + physics_world.step(world_id, 1.0 / 60.0, 4) + i = i + 1 + } +} + + +// A body's first shape. +first_shape(body_id: BodyId) -> ShapeId { + ids = calloc(4, sizeof(ShapeId)) + each = ids as ShapeId[] + dynamics.body_get_shapes(body_id, each, 4) + found = each[0] + free(ids) + return found +} + +same_place(a: Transform, b: Transform) -> bool { + return a.p.x == b.p.x && a.p.y == b.p.y && a.p.z == b.p.z && a.q.x == b.q.x && a.q.y == b.q.y && a.q.z == b.q.z && a.q.w == b.q.w +} + +// --- what a recording session does to the world ----------------------------------------------------- + +// The calls a host makes between steps, by frame: every kind of change the +// recording carries (velocities, impulses, forces, transforms, names, +// materials, filters, a body's type, mass and locks, joints' motors and +// limits, gravity, an explosion, bodies and joints destroyed and their slots +// reused, shapes swapped for geometry the host frees at once). +var g_victims: ptr = null // BodyId[8]: bodies made and destroyed during the session +var g_hinge: ptr = null // JointId[1]: a hinge made, driven and destroyed during it + +mutate(world_id: WorldId, frame: int) { + sdef = dynamics.default_shape_def() + victims = g_victims as BodyId[] + hinge = g_hinge as JointId[] + if frame == 3 { + i = 0 + while i < 8 { + victims[i] = dynamic_body(world_id, math.vec3(0.0 - 10.0 + 0.9 * (i as float), 6.0, 0.0 - 12.0)) + dynamics.create_sphere_shape(victims[i], &sdef, manifold.sphere(math.vec3_zero(), 0.3)) + i = i + 1 + } + } + if frame == 10 { + b = victims[0] + dynamics.body_set_linear_velocity(b, math.vec3(1.0, 4.0, 0.0)) + dynamics.body_set_angular_velocity(b, math.vec3(0.0, 3.0, 1.0)) + dynamics.body_apply_linear_impulse(victims[1], math.vec3(0.0, 2.0, 1.0), math.add(dynamics.body_get_position(victims[1]), math.vec3(0.1, 0.0, 0.0)), true) + dynamics.body_apply_force_to_center(victims[2], math.vec3(30.0, 0.0, 0.0), true) + dynamics.body_apply_torque(victims[3], math.vec3(0.0, 5.0, 0.0), true) + dynamics.body_set_transform(victims[4], math.vec3(0.0 - 3.0, 7.0, 0.0 - 12.0), math.make_quat_from_axis_angle(math.vec3(0.0, 1.0, 0.0), 0.7)) + dynamics.body_set_name(victims[4], "renamed") + dynamics.body_set_gravity_scale(victims[6], 0.5) + dynamics.body_set_linear_damping(victims[7], 0.3) + } + if frame == 20 { + // Slots freed and reused with a new generation. + dynamics.destroy_body(victims[0]) + dynamics.destroy_body(victims[3]) + dynamics.destroy_body(victims[6]) + victims[0] = dynamic_body(world_id, math.vec3(0.0 - 8.0, 8.0, 0.0 - 11.0)) + dynamics.create_hull_shape(victims[0], &sdef, g_rock as *HullData) + victims[3] = dynamic_body(world_id, math.vec3(0.0 - 7.0, 8.0, 0.0 - 11.0)) + dynamics.create_capsule_shape(victims[3], &sdef, Capsule { center1: math.vec3(0.0, 0.0 - 0.2, 0.0), center2: math.vec3(0.0, 0.2, 0.0), radius: 0.2 }) + victims[6] = dynamic_body(world_id, math.vec3(0.0 - 6.0, 8.0, 0.0 - 11.0)) + // A hull the host frees at once: the recording keeps its own copy. + temporary = hull.make_box_hull(0.3, 0.2, 0.5) + dynamics.create_transformed_hull_shape(victims[6], &sdef, temporary, Transform { p: math.vec3(0.0, 0.1, 0.0), q: math.quat_identity() }, math.vec3(1.0, 2.0, 1.0)) + hull.destroy_hull(temporary) + rdef = dynamics.default_revolute_joint_def() + rdef.base.body_id_a = victims[0] + rdef.base.body_id_b = victims[3] + rdef.base.local_frame_a = Transform { p: math.vec3(0.5, 0.0, 0.0), q: math.quat_identity() } + rdef.base.local_frame_b = Transform { p: math.vec3(0.0 - 0.5, 0.0, 0.0), q: math.quat_identity() } + rdef.enable_limit = true + rdef.lower_angle = 0.0 - 0.5 + rdef.upper_angle = 0.5 + hinge[0] = dynamics.create_revolute_joint(world_id, &rdef) + } + if frame == 30 { + shape_id = first_shape(victims[1]) + dynamics.shape_set_friction(shape_id, 0.05) + dynamics.shape_set_restitution(shape_id, 0.8) + dynamics.shape_set_name(shape_id, "slippery") + filter = shape.default_filter() + filter.category_bits = 2 as long + dynamics.shape_set_filter(first_shape(victims[2]), filter) + replacement = hull.make_box_hull(0.25, 0.25, 0.25) + dynamics.shape_set_hull(first_shape(victims[0]), replacement) + hull.destroy_hull(replacement) + physics_world.set_gravity(world_id, math.vec3(0.5, 0.0 - 9.0, 0.0)) + edef = physics_world.default_explosion_def() + edef.position = math.vec3(0.0, 0.5, 0.0) + edef.radius = 3.0 + edef.falloff = 1.0 + edef.impulse_per_area = 4.0 + physics_world.explode(world_id, &edef) + } + if frame == 40 { + joint_solver.revolute_joint_set_motor_speed(hinge[0], 2.0) + joint_solver.revolute_joint_enable_motor(hinge[0], true) + joint_solver.revolute_joint_set_max_motor_torque(hinge[0], 4.0) + joint_solver.revolute_joint_set_limits(hinge[0], 0.0 - 0.3, 0.9) + dynamics.body_set_type(victims[7], dynamics.BODY_KINEMATIC) + dynamics.body_set_linear_velocity(victims[7], math.vec3(0.0, 0.0, 1.0)) + locks = dynamics.body_get_motion_locks(victims[5]) + locks.angular_x = true + locks.angular_z = true + dynamics.body_set_motion_locks(victims[5], locks) + mass = dynamics.body_get_mass_data(victims[4]) + mass.mass = mass.mass * 2.0 + dynamics.body_set_mass_data(victims[4], mass) + } + if frame == 55 { + dynamics.destroy_joint(hinge[0], true) + dynamics.body_set_type(victims[7], dynamics.BODY_DYNAMIC) + dynamics.destroy_shape(first_shape(victims[2]), true) + dynamics.create_sphere_shape(victims[2], &sdef, manifold.sphere(math.vec3_zero(), 0.4)) + dynamics.body_disable(victims[1]) + } + if frame == 70 { + dynamics.body_enable(victims[1]) + dynamics.destroy_body(victims[5]) + victims[5] = dynamic_body(world_id, math.vec3(0.0 - 9.0, 9.0, 0.0 - 12.0)) + dynamics.create_sphere_shape(victims[5], &sdef, manifold.sphere(math.vec3_zero(), 0.25)) + physics_world.set_gravity(world_id, math.vec3(0.0, 0.0 - 10.0, 0.0)) + } +} + + +// The world recorded: `before` steps run first (the recording starts mid +// stream), the scene built before or during the session, `frames` steps +// with the host's changes, every step's hash kept in `hashes`. +record_session(world_id: WorldId, r: *Recording, build_during: bool, before: int, frames: int, hashes: ptr) -> bool { + if build_during == false { build_scene(world_id) } + i = 0 + while i < before { + physics_world.step(world_id, 1.0 / 60.0, 4) + i = i + 1 + } + if recording.start_recording(world_id, r) == false { return false } + // Frame 0 is the world the recording starts from. + each = hashes as long[] + each[0] = state_hash(world_id) + if build_during { build_scene(world_id) } + frame = 0 + while frame < frames { + mutate(world_id, frame) + physics_world.step(world_id, 1.0 / 60.0, 4) + frame = frame + 1 + each[frame] = state_hash(world_id) + } + recording.stop_recording(world_id) + return true +} + +// The recording played frame by frame: the player's world hashes as the +// recorded one every frame and never diverges. +replay_matches(name: string, r: *Recording, frames: int, hashes: ptr, workers: int) { + each = hashes as long[] + p = recording.create_player(recording.recording_data(r), recording.recording_size(r), workers) + ensure("${name}: the player loads", p != null) + if p == null { return } + ensure("${name}: ${frames} frames", recording.player_frame_count(p) == frames) + ensure("${name}: frame 0 as recorded", state_hash(recording.player_world(p)) == each[0]) + matched = 0 + while recording.player_step_frame(p) { + if state_hash(recording.player_world(p)) == each[recording.player_frame(p)] { matched = matched + 1 } + } + ensure("${name}: every frame as recorded (${matched} of ${frames})", matched == frames) + ensure("${name}: no divergence", recording.player_has_diverged(p) == false && recording.player_diverge_frame(p) < 0) + ensure("${name}: at the end", recording.player_at_end(p) && recording.player_frame(p) == frames) + ensure("${name}: no frame past the end", recording.player_step_frame(p) == false) + recording.destroy_player(p) +} + +// A recording of the whole scene built and changed during the session. +round_trips() { + g_victims = calloc(8, sizeof(BodyId)) + g_hinge = calloc(1, sizeof(JointId)) + hashes = calloc(201, 8) + wdef = world_def() + + // Built during the session from an empty world. + world_id = dynamics.create_world(&wdef) + r = recording.create_recording() + ensure("from empty: starts", record_session(world_id, r, true, 0, 120, hashes)) + replay_matches("from empty", r, 120, hashes, 1) + replay_matches("from empty, four workers", r, 120, hashes, 4) + ensure("from empty: validates", recording.validate_replay(recording.recording_data(r), recording.recording_size(r), 2)) + + // The player's world is the recorded one at the end, body by body: the + // bodies in the order they were made, a hole for each destroyed. + p = recording.create_player(recording.recording_data(r), recording.recording_size(r), 1) + info = recording.player_info(p) + ensure("info: frames, workers and the first step", info.frame_count == 120 && info.worker_count == 1 && info.time_step == 1.0 / 60.0 && info.sub_step_count == 4) + ensure("info: the bounds of every step", info.bounds.lower.x < info.bounds.upper.x && info.bounds.lower.y < info.bounds.upper.y && info.bounds.lower.z < info.bounds.upper.z) + ensure("accessors: no bodies before the first frame", recording.player_body_count(p) == 0) + stable = recording.player_world(p) + while recording.player_step_frame(p) {} + live = 0 + holes = 0 + same = 0 + ordinal = 0 + while ordinal < recording.player_body_count(p) { + played = recording.player_body_id(p, ordinal) + if played.index1 == 0 { holes = holes + 1 } + else { + live = live + 1 + original = BodyId { index1: played.index1, world0: world_id.index1 - 1, generation: played.generation } + if dynamics.body_is_valid(played) && dynamics.body_is_valid(original) && same_place(dynamics.body_get_transform(played), dynamics.body_get_transform(original)) { same = same + 1 } + } + ordinal = ordinal + 1 + } + ensure("accessors: every body made, once (${live} live, ${holes} holes)", live == dynamics.world_body_count(dynamics.get_world(world_id.index1 - 1)) && holes == 4) + ensure("accessors: every body at the same place (${same} of ${live})", same == live) + ensure("accessors: no body past the ends", recording.player_body_id(p, recording.player_body_count(p)).index1 == 0 && recording.player_body_id(p, 0 - 1).index1 == 0) + victims = g_victims as BodyId[] + played_victim = recording.player_body_id(p, recording.player_body_count(p) - 1) + ensure("accessors: the last body made is the last ordinal", played_victim.index1 == victims[5].index1 && played_victim.generation == victims[5].generation) + played_renamed = BodyId { index1: victims[4].index1, world0: recording.player_world(p).index1 - 1, generation: victims[4].generation } + ensure("names: a body renamed in the session", dynamics.body_is_valid(played_renamed) && string.equals(dynamics.body_get_name(played_renamed), "renamed")) + played_slippery = BodyId { index1: victims[1].index1, world0: recording.player_world(p).index1 - 1, generation: victims[1].generation } + ensure("names: a shape renamed in the session", string.equals(dynamics.shape_get_name(first_shape(played_slippery)), "slippery")) + played_apex = BodyId { index1: 0, world0: 0, generation: 0 } + ordinal = 0 + while ordinal < recording.player_body_count(p) { + candidate_id = recording.player_body_id(p, ordinal) + if candidate_id.index1 != 0 && string.equals(dynamics.body_get_name(candidate_id), "apex") { played_apex = candidate_id } + ordinal = ordinal + 1 + } + ensure("names: a body named at its creation", played_apex.index1 != 0) + recording.player_restart(p) + ensure("restart: the same world, its bodies those of frame 0", recording.player_world(p).index1 == stable.index1 && recording.player_world(p).generation == stable.generation && recording.player_body_count(p) == 0 && recording.player_frame(p) == 0) + + // A frame that makes bodies played in two calls: the first stops + // before its step, the new bodies where they were made. + recording.player_sub_step_frame(p) + ensure("sub step: paused before the first step", recording.player_at_pre_step(p) && recording.player_frame(p) == 0 && recording.player_body_count(p) > 0) + recording.player_sub_step_frame(p) + ensure("sub step: then stepped", recording.player_at_pre_step(p) == false && recording.player_frame(p) == 1) + recording.player_sub_step_frame(p) + ensure("sub step: a frame that makes no body steps at once", recording.player_at_pre_step(p) == false && recording.player_frame(p) == 2) + // The workers changed mid replay: the state checks still hold. + recording.player_set_worker_count(p, 3) + while recording.player_step_frame(p) {} + ensure("workers changed mid replay: no divergence", recording.player_has_diverged(p) == false && recording.player_failed(p) == false && recording.player_info(p).worker_count == 3) + recording.destroy_player(p) + dynamics.destroy_world(world_id) + recording.destroy_recording(r) + + // Started mid stream: sleeping bodies, warm contacts, joints at work. + world_id = dynamics.create_world(&wdef) + r = recording.create_recording() + ensure("mid stream: starts", record_session(world_id, r, false, 45, 120, hashes)) + replay_matches("mid stream", r, 120, hashes, 1) + replay_matches("mid stream, three workers", r, 120, hashes, 3) + dynamics.destroy_world(world_id) + + // Started again on the same recording: a fresh session. + world_id = dynamics.create_world(&wdef) + ensure("again: starts", record_session(world_id, r, false, 10, 40, hashes)) + replay_matches("again", r, 40, hashes, 1) + dynamics.destroy_world(world_id) + recording.destroy_recording(r) + + // A world destroyed while it records ends the recording whole. + world_id = dynamics.create_world(&wdef) + r = recording.create_recording() + build_scene(world_id) + recording.start_recording(world_id, r) + steps(world_id, 30) + dynamics.destroy_world(world_id) + ensure("destroyed while recording: validates", recording.validate_replay(recording.recording_data(r), recording.recording_size(r), 1)) + recording.destroy_recording(r) + + free(hashes) + free(g_victims) + free(g_hinge) +} + +// What starting and stopping refuse. +sessions() { + wdef = world_def() + world_id = dynamics.create_world(&wdef) + r = recording.create_recording() + other = recording.create_recording() + ensure("sessions: no recording to start into", recording.start_recording(world_id, null) == false) + ensure("sessions: no world", recording.start_recording(WorldId { index1: 0, generation: 0 }, r) == false) + ensure("sessions: starts", recording.start_recording(world_id, r)) + ensure("sessions: one recording at a time", recording.start_recording(world_id, other) == false) + second_world = dynamics.create_world(&wdef) + ensure("sessions: a recording another world writes", recording.start_recording(second_world, r) == false) + ensure("sessions: the other world records into its own", recording.start_recording(second_world, other)) + // A recording freed while its world writes it: the world stops first. + recording.destroy_recording(other) + steps(second_world, 3) + other = recording.create_recording() + ensure("sessions: a world whose recording was freed records again", recording.start_recording(second_world, other)) + recording.stop_recording(second_world) + dynamics.destroy_world(second_world) + recording.stop_recording(world_id) + recording.stop_recording(world_id) + ensure("sessions: stopping twice changes nothing", recording.validate_replay(recording.recording_data(r), recording.recording_size(r), 1)) + + // An empty session: no frames, nothing to play. + p = recording.create_player(recording.recording_data(r), recording.recording_size(r), 1) + ensure("empty: plays", p != null) + if p != null { + ensure("empty: no frames", recording.player_frame_count(p) == 0 && recording.player_step_frame(p) == false && recording.player_at_end(p)) + ensure("empty: no divergence", recording.player_has_diverged(p) == false) + recording.destroy_player(p) + } + dynamics.destroy_world(world_id) + recording.destroy_recording(r) + recording.destroy_recording(other) +} + +// The geometry a session names, each block once however often or from +// however many copies it is used. +registry_count(r: *Recording) -> int { + data = recording.recording_data(r) + holder = 0 + memcpy((&holder) as ptr, data + 12, 4) + offset = holder + memcpy((&holder) as ptr, data + offset, 4) + return holder +} + +geometry_dedup() { + wdef = world_def() + world_id = dynamics.create_world(&wdef) + r = recording.create_recording() + recording.start_recording(world_id, r) + sdef = dynamics.default_shape_def() + first = hull.make_box_hull(0.5, 0.5, 0.5) + twin = hull.make_box_hull(0.5, 0.5, 0.5) + other = hull.make_box_hull(0.5, 0.25, 0.5) + i = 0 + while i < 10 { + b = dynamic_body(world_id, math.vec3(1.1 * (i as float), 1.0, 0.0)) + if i % 2 == 0 { dynamics.create_hull_shape(b, &sdef, first) } else { dynamics.create_hull_shape(b, &sdef, twin) } + i = i + 1 + } + dynamics.create_hull_shape(dynamic_body(world_id, math.vec3(0.0, 3.0, 0.0)), &sdef, other) + steps(world_id, 5) + recording.stop_recording(world_id) + ensure("dedup: two blocks for eleven shapes (${registry_count(r)})", registry_count(r) == 2) + ensure("dedup: replays", recording.validate_replay(recording.recording_data(r), recording.recording_size(r), 1)) + hull.destroy_hull(first) + hull.destroy_hull(twin) + hull.destroy_hull(other) + dynamics.destroy_world(world_id) + recording.destroy_recording(r) +} + +// Seeking: backward and forward through keyframes, the world at every +// frame as it was the first time through. +seeking() { + g_victims = calloc(8, sizeof(BodyId)) + g_hinge = calloc(1, sizeof(JointId)) + hashes = calloc(201, 8) + wdef = world_def() + world_id = dynamics.create_world(&wdef) + r = recording.create_recording() + record_session(world_id, r, true, 0, 100, hashes) + dynamics.destroy_world(world_id) + each = hashes as long[] + p = recording.create_player(recording.recording_data(r), recording.recording_size(r), 1) + ensure("seek: the default policy", recording.player_keyframe_interval(p) == 16 && recording.player_keyframe_min_interval(p) == 16 && recording.player_keyframe_budget(p) == (536870912 as long)) + recording.player_set_keyframe_policy(p, (1 << 30) as long, 8) + stable = recording.player_world(p) + while recording.player_step_frame(p) {} + ensure("seek: a keyframe every 8 frames (${recording.player_keyframe_count(p)})", recording.player_keyframe_count(p) == 12 && recording.player_keyframe_bytes(p) > (0 as long) && recording.player_keyframe_interval(p) == 8) + targets = [77, 3, 64, 0, 100, 41, 41, 8, 99, 16, 15, 250, 0 - 5] + good = 0 + i = 0 + while i < 13 { + recording.player_seek_frame(p, targets[i]) + expected = math.clamp_int(targets[i], 0, 100) + if recording.player_frame(p) == expected && state_hash(recording.player_world(p)) == each[expected] { good = good + 1 } + else { println("seek: frame ${targets[i]} landed at ${recording.player_frame(p)}") } + i = i + 1 + } + ensure("seek: every frame as recorded (${good} of 13)", good == 13) + ensure("seek: no divergence", recording.player_has_diverged(p) == false && recording.player_failed(p) == false) + ensure("seek: the world kept its id", recording.player_world(p).index1 == stable.index1 && recording.player_world(p).generation == stable.generation) + recording.player_restart(p) + ensure("seek: restart at frame 0", recording.player_frame(p) == 0 && state_hash(recording.player_world(p)) == each[0]) + recording.destroy_player(p) + + // No keyframe within reach: seeking back replays from the start. + p = recording.create_player(recording.recording_data(r), recording.recording_size(r), 1) + recording.player_set_keyframe_policy(p, 0 as long, 1000) + recording.player_seek_frame(p, 90) + recording.player_seek_frame(p, 25) + ensure("seek without keyframes: frame 25 as recorded", recording.player_frame(p) == 25 && state_hash(recording.player_world(p)) == each[25] && recording.player_keyframe_count(p) == 0) + recording.destroy_player(p) + + // A budget of a few keyframes: the spacing widens to stay under it, and + // the keyframes left still land on the recorded frames. + p = recording.create_player(recording.recording_data(r), recording.recording_size(r), 1) + recording.player_set_keyframe_policy(p, (1 << 30) as long, 4) + recording.player_seek_frame(p, 5) + one = recording.player_keyframe_bytes(p) + recording.player_set_keyframe_policy(p, one * (3 as long), 4) + recording.player_restart(p) + while recording.player_step_frame(p) {} + ensure("budget: the spacing widened (${recording.player_keyframe_interval(p)})", recording.player_keyframe_interval(p) > 4 && recording.player_keyframe_min_interval(p) == 4) + ensure("budget: few keyframes kept (${recording.player_keyframe_count(p)})", recording.player_keyframe_count(p) >= 1 && recording.player_keyframe_count(p) <= 4) + good = 0 + i = 0 + while i < 13 { + recording.player_seek_frame(p, targets[i]) + expected = math.clamp_int(targets[i], 0, 100) + if recording.player_frame(p) == expected && state_hash(recording.player_world(p)) == each[expected] { good = good + 1 } + i = i + 1 + } + ensure("budget: every frame as recorded (${good} of 13)", good == 13) + recording.destroy_player(p) + recording.destroy_recording(r) + free(hashes) + free(g_victims) + free(g_hinge) +} + +// A recording saved to a file and read back plays the same. +files() { + g_victims = calloc(8, sizeof(BodyId)) + g_hinge = calloc(1, sizeof(JointId)) + hashes = calloc(201, 8) + wdef = world_def() + world_id = dynamics.create_world(&wdef) + r = recording.create_recording() + record_session(world_id, r, true, 0, 60, hashes) + dynamics.destroy_world(world_id) + ensure("file: saved", recording.save_recording_to_file(r, "target/recording.aprc")) + loaded = recording.load_recording_from_file("target/recording.aprc") + ensure("file: read back", loaded != null && recording.recording_size(loaded) == recording.recording_size(r)) + if loaded != null { + ensure("file: the same bytes", memcmp(recording.recording_data(loaded), recording.recording_data(r), recording.recording_size(r)) == 0) + replay_matches("file", loaded, 60, hashes, 2) + recording.destroy_recording(loaded) + } + ensure("file: no such file", recording.load_recording_from_file("target/no_such_recording.aprc") == null) + empty = recording.create_recording() + ensure("file: nothing to save", recording.save_recording_to_file(empty, "target/empty.aprc") == false) + recording.destroy_recording(empty) + recording.destroy_recording(r) + free(hashes) + free(g_victims) + free(g_hinge) +} + +poke_int(data: ptr, at: int, value: int) { + holder = value + memcpy(data + at, (&holder) as ptr, 4) +} + +peek_int(data: ptr, at: int) -> int { + holder = 0 + memcpy((&holder) as ptr, data + at, 4) + return holder +} + +// Bytes that are not a recording load no player and leave nothing +// allocated; a record the player does not know is skipped; a record that +// does not read whole, or a create that makes another id, stops the +// replay; a state hash that differs marks the frame it was found at. +corruptions() { + g_victims = calloc(8, sizeof(BodyId)) + g_hinge = calloc(1, sizeof(JointId)) + hashes = calloc(201, 8) + wdef = world_def() + world_id = dynamics.create_world(&wdef) + r = recording.create_recording() + record_session(world_id, r, true, 0, 30, hashes) + dynamics.destroy_world(world_id) + size = recording.recording_size(r) + copy = calloc(size, 1) + // Warm-up: the solver's lazy buffers are not a leak. + warm = recording.create_player(recording.recording_data(r), size, 1) + while recording.player_step_frame(warm) {} + recording.destroy_player(warm) + before = basics.alloc_count() + + refused = 0 + tries = 0 + cut = 0 + while cut < size { + p = recording.create_player(recording.recording_data(r), cut, 1) + if p == null { refused = refused + 1 } else { recording.destroy_player(p) } + tries = tries + 1 + cut = cut + 1 + cut / 7 + } + ensure("truncated: every cut refused (${refused} of ${tries})", refused == tries) + + memcpy(copy, recording.recording_data(r), size) + poke_int(copy, 0, 0x12345678) + ensure("bad magic refused", recording.create_player(copy, size, 1) == null) + memcpy(copy, recording.recording_data(r), size) + poke_int(copy, 4, 99) + ensure("bad version refused", recording.create_player(copy, size, 1) == null) + memcpy(copy, recording.recording_data(r), size) + poke_int(copy, 12, size - 2) + ensure("bad registry offset refused", recording.create_player(copy, size, 1) == null) + memcpy(copy, recording.recording_data(r), size) + poke_int(copy, peek_int(copy, 12), 1000000) + ensure("bad registry count refused", recording.create_player(copy, size, 1) == null) + memcpy(copy, recording.recording_data(r), size) + poke_int(copy, peek_int(copy, 12) + 8, 0x7fffffff) + ensure("bad geometry size refused", recording.create_player(copy, size, 1) == null) + memcpy(copy, recording.recording_data(r), size) + poke_int(copy, peek_int(copy, 12) + 12, 0x7fffffff) + ensure("bad geometry bytes refused", recording.create_player(copy, size, 1) == null) + memcpy(copy, recording.recording_data(r), size) + poke_int(copy, 8, 7) + ensure("bad snapshot size refused", recording.create_player(copy, size, 1) == null) + + // The first record after the snapshot and its state hash. + first = 32 + peek_int(recording.recording_data(r), 8) + second = first + 8 + peek_int(recording.recording_data(r), first + 4) + memcpy(copy, recording.recording_data(r), size) + poke_int(copy, second + 4, 0x7fffffff) + ensure("bad record length refused", recording.create_player(copy, size, 1) == null) + + // The bounds record made one this player does not know: skipped. + bounds_at = 0 - 1 + at = first + while at < peek_int(recording.recording_data(r), 12) { + if peek_int(recording.recording_data(r), at) == dynamics.OP_BOUNDS { bounds_at = at } + at = at + 8 + peek_int(recording.recording_data(r), at + 4) + } + memcpy(copy, recording.recording_data(r), size) + poke_int(copy, bounds_at, 9999) + ensure("unknown record: skipped", recording.validate_replay(copy, size, 1)) + + // The first call skipped: the creates after it make other ids. + memcpy(copy, recording.recording_data(r), size) + poke_int(copy, second, 9999) + p = recording.create_player(copy, size, 1) + ensure("other ids: loads", p != null) + if p != null { + while recording.player_step_frame(p) {} + ensure("other ids: failed in frame 1, stopped", recording.player_failed(p) && recording.player_at_end(p) && recording.player_frame(p) == 0) + recording.player_restart(p) + ensure("other ids: a restart clears it", recording.player_failed(p) == false && recording.player_at_end(p) == false) + recording.destroy_player(p) + } + ensure("other ids: does not validate", recording.validate_replay(copy, size, 1) == false) + + // The first create's recorded id changed. + memcpy(copy, recording.recording_data(r), size) + third = second + 8 + peek_int(copy, second + 4) + poke_int(copy, third - 4, peek_int(copy, third - 4) + 1) + ensure("changed id: does not validate", recording.validate_replay(copy, size, 1) == false) + + // A call's record taken for a state check: it does not read whole. + memcpy(copy, recording.recording_data(r), size) + poke_int(copy, second, dynamics.OP_STATE_HASH) + p = recording.create_player(copy, size, 1) + recording.player_step_frame(p) + ensure("short read: failed", recording.player_failed(p) && recording.player_at_end(p)) + recording.destroy_player(p) + + // A state hash changed at frame 12: the divergence is found there. + memcpy(copy, recording.recording_data(r), size) + at = first + hashes_seen = 0 + while at < peek_int(copy, 12) && hashes_seen < 13 { + if peek_int(copy, at) == dynamics.OP_STATE_HASH { + hashes_seen = hashes_seen + 1 + if hashes_seen == 13 { poke_int(copy, at + 8, peek_int(copy, at + 8) ^ 1) } + } + at = at + 8 + peek_int(copy, at + 4) + } + p = recording.create_player(copy, size, 1) + while recording.player_step_frame(p) {} + ensure("changed hash: diverged at frame 12 (${recording.player_diverge_frame(p)})", recording.player_has_diverged(p) && recording.player_diverge_frame(p) == 12) + ensure("changed hash: played on to the end", recording.player_frame(p) == 30) + recording.player_seek_frame(p, 20) + ensure("changed hash: kept by the keyframe after it", recording.player_has_diverged(p) && recording.player_diverge_frame(p) == 12 && recording.player_frame(p) == 20) + recording.player_seek_frame(p, 5) + ensure("changed hash: forgotten before it", recording.player_has_diverged(p) == false && recording.player_diverge_frame(p) < 0) + recording.destroy_player(p) + ensure("changed hash: does not validate", recording.validate_replay(copy, size, 1) == false) + + ensure("corruptions: nothing left allocated (${basics.alloc_count() - before})", basics.alloc_count() == before) + free(copy) + recording.destroy_recording(r) + free(hashes) + free(g_victims) + free(g_hinge) +} + +// --- every call the recording carries --------------------------------------------------------------- + +// Two boxes side by side for a joint. +pair_bodies(world_id: WorldId, x: float) -> (BodyId, BodyId) { + sdef = dynamics.default_shape_def() + a = dynamic_body(world_id, math.vec3(x, 2.0, 0.0 - 8.0)) + dynamics.create_hull_shape(a, &sdef, g_box as *HullData) + b = dynamic_body(world_id, math.vec3(x, 2.0, 0.0 - 6.8)) + dynamics.create_hull_shape(b, &sdef, g_box as *HullData) + return a, b +} + +joint_base(a: BodyId, b: BodyId) -> JointDef { + base = dynamics.default_joint_def() + base.body_id_a = a + base.body_id_b = b + base.local_frame_a = Transform { p: math.vec3(0.0, 0.0, 0.6), q: math.quat_identity() } + base.local_frame_b = Transform { p: math.vec3(0.0, 0.0, 0.0 - 0.6), q: math.quat_identity() } + return base +} + +// The records of a recording, each opcode marked in `seen` (int[4096]). +mark_ops(r: *Recording, seen: ptr) { + data = recording.recording_data(r) + marks = seen as int[] + at = 32 + peek_int(data, 8) + end = peek_int(data, 12) + while at < end { + op = peek_int(data, at) + if op >= 0 && op < 4096 { marks[op] = 1 } + at = at + 8 + peek_int(data, at + 4) + } +} + +// Every recorded call made once (Box3D's AllOps), steps between them: the +// recording holds every opcode there is and plays back to the same world. +all_ops() { + fixtures_ready() + wdef = world_def() + world_id = dynamics.create_world(&wdef) + r = recording.create_recording() + recording.start_recording(world_id, r) + sdef = dynamics.default_shape_def() + tinted = material.default_surface_material() + tinted.friction = 0.35 + tinted.restitution = 0.2 + blast = physics_world.default_explosion_def() + blast.position = math.vec3(0.0, 1.0, 0.0) + blast.radius = 2.0 + blast.falloff = 0.5 + blast.impulse_per_area = 2.0 + + ground = static_body(world_id, math.vec3(0.0, 0.0 - 1.0, 0.0), "ground") + floor_hull = hull.make_box_hull(20.0, 1.0, 20.0) + dynamics.create_hull_shape(ground, &sdef, floor_hull) + hull.destroy_hull(floor_hull) + materials_block = calloc(3, sizeof(SurfaceMaterial)) + materials = materials_block as SurfaceMaterial[] + k = 0 + while k < 3 { + materials[k] = material.default_surface_material() + materials[k].friction = 0.1 + 0.2 * (k as float) + k = k + 1 + } + mdef = dynamics.default_shape_def() + mdef.materials = materials_block + mdef.material_count = 3 + mesh_shape = dynamics.create_mesh_shape(static_body(world_id, math.vec3(8.0, 0.0, 0.0 - 4.0), "waves"), &mdef, g_wave as *MeshData, math.vec3_one()) + free(materials_block) + dynamics.create_height_field_shape(static_body(world_id, math.vec3(0.0 - 14.0, 0.0, 0.0 - 6.0), "field"), &sdef, g_field as *HeightFieldData) + dynamics.create_compound_shape(static_body(world_id, math.vec3(0.0 - 6.0, 0.4, 6.0), "compound"), &sdef, g_compound as *CompoundData) + + subject = dynamic_body(world_id, math.vec3(0.0, 2.0, 0.0)) + sphere_shape = dynamics.create_sphere_shape(subject, &sdef, manifold.sphere(math.vec3_zero(), 0.3)) + capsule_shape = dynamics.create_capsule_shape(dynamic_body(world_id, math.vec3(1.5, 2.0, 0.0)), &sdef, Capsule { center1: math.vec3(0.0 - 0.2, 0.0, 0.0), center2: math.vec3(0.2, 0.0, 0.0), radius: 0.2 }) + hull_shape = dynamics.create_hull_shape(dynamic_body(world_id, math.vec3(0.0 - 1.5, 2.0, 0.0)), &sdef, g_box as *HullData) + heavy = dynamic_body(world_id, math.vec3(3.0, 2.0, 0.0)) + dynamics.create_hull_shape(heavy, &sdef, g_box as *HullData) + spare = dynamic_body(world_id, math.vec3(0.0 - 3.0, 2.0, 0.0)) + kdef = dynamics.default_body_def() + kdef.kind = dynamics.BODY_KINEMATIC + kdef.position = math.vec3(0.0 - 3.0, 0.5, 3.0) + paddle = dynamics.create_body(world_id, &kdef) + dynamics.create_hull_shape(paddle, &sdef, g_box as *HullData) + + a, b = pair_bodies(world_id, 0.0 - 8.0) + distance_def = dynamics.default_distance_joint_def() + distance_def.base = joint_base(a, b) + distance_def.length = 0.5 + j_distance = dynamics.create_distance_joint(world_id, &distance_def) + a, b = pair_bodies(world_id, 0.0 - 6.0) + motor_def = dynamics.default_motor_joint_def() + motor_def.base = joint_base(a, b) + j_motor = dynamics.create_motor_joint(world_id, &motor_def) + a, b = pair_bodies(world_id, 0.0 - 4.0) + filter_def = dynamics.default_filter_joint_def() + filter_def.base = joint_base(a, b) + j_filter = dynamics.create_filter_joint(world_id, &filter_def) + a, b = pair_bodies(world_id, 0.0 - 2.0) + parallel_def = dynamics.default_parallel_joint_def() + parallel_def.base = joint_base(a, b) + j_parallel = dynamics.create_parallel_joint(world_id, ¶llel_def) + a, b = pair_bodies(world_id, 0.0) + prismatic_def = dynamics.default_prismatic_joint_def() + prismatic_def.base = joint_base(a, b) + j_prismatic = dynamics.create_prismatic_joint(world_id, &prismatic_def) + a, b = pair_bodies(world_id, 2.0) + revolute_def = dynamics.default_revolute_joint_def() + revolute_def.base = joint_base(a, b) + j_revolute = dynamics.create_revolute_joint(world_id, &revolute_def) + a, b = pair_bodies(world_id, 4.0) + spherical_def = dynamics.default_spherical_joint_def() + spherical_def.base = joint_base(a, b) + j_spherical = dynamics.create_spherical_joint(world_id, &spherical_def) + a, b = pair_bodies(world_id, 6.0) + weld_def = dynamics.default_weld_joint_def() + weld_def.base = joint_base(a, b) + j_weld = dynamics.create_weld_joint(world_id, &weld_def) + a, b = pair_bodies(world_id, 8.0) + wheel_def = dynamics.default_wheel_joint_def() + wheel_def.base = joint_base(a, b) + j_wheel = dynamics.create_wheel_joint(world_id, &wheel_def) + physics_world.step(world_id, 1.0 / 60.0, 4) + + dynamics.body_set_transform(subject, math.vec3(0.0, 3.0, 0.0), math.make_quat_from_axis_angle(math.vec3(0.0, 0.0, 1.0), 0.3)) + dynamics.body_set_linear_velocity(subject, math.vec3(0.1, 0.4, 0.2)) + dynamics.body_set_angular_velocity(subject, math.vec3(0.1, 0.4, 0.2)) + dynamics.body_set_target_transform(paddle, Transform { p: math.vec3(0.0 - 2.0, 0.6, 3.0), q: math.quat_identity() }, 1.0 / 60.0, true) + dynamics.body_apply_force(subject, math.vec3(3.0, 0.0, 0.0), math.vec3(0.1, 3.0, 0.0), true) + dynamics.body_apply_force_to_center(subject, math.vec3(0.1, 0.4, 0.2), true) + physics_world.step(world_id, 1.0 / 60.0, 4) + dynamics.body_apply_torque(subject, math.vec3(0.1, 0.4, 0.2), true) + dynamics.body_apply_linear_impulse(subject, math.vec3(0.0, 0.5, 0.0), math.vec3(0.1, 3.0, 0.0), true) + dynamics.body_apply_linear_impulse_to_center(subject, math.vec3(0.1, 0.4, 0.2), true) + dynamics.body_apply_angular_impulse(subject, math.vec3(0.1, 0.4, 0.2), true) + dynamics.body_set_bullet(subject, true) + dynamics.body_set_mass_data(heavy, MassData { mass: 3.0, center: math.vec3(0.0, 0.1, 0.0), inertia: math.mul_sm(0.2, math.mat3_identity()) }) + physics_world.step(world_id, 1.0 / 60.0, 4) + dynamics.body_apply_mass_from_shapes(heavy) + dynamics.body_set_linear_damping(subject, 0.75) + dynamics.body_set_angular_damping(subject, 0.75) + dynamics.body_set_gravity_scale(subject, 0.75) + dynamics.body_set_awake(subject, true) + dynamics.body_enable_sleep(subject, true) + physics_world.step(world_id, 1.0 / 60.0, 4) + dynamics.body_set_sleep_threshold(subject, 0.04) + dynamics.body_set_motion_locks(heavy, MotionLocks { linear_x: false, linear_y: false, linear_z: true, angular_x: true, angular_y: false, angular_z: false }) + dynamics.create_transformed_hull_shape(spare, &sdef, g_box as *HullData, Transform { p: math.vec3(0.0, 0.2, 0.0), q: math.quat_identity() }, math.vec3(1.0, 0.5, 2.0)) + dynamics.shape_set_density(sphere_shape, 2.0, true) + dynamics.shape_set_filter(sphere_shape, Filter { category_bits: 3 as long, mask_bits: 0 - (1 as long), group_index: 0 - 2 }) + dynamics.shape_set_surface_material(sphere_shape, tinted) + physics_world.step(world_id, 1.0 / 60.0, 4) + dynamics.shape_set_sphere(sphere_shape, manifold.sphere(math.vec3(0.0, 0.05, 0.0), 0.35)) + dynamics.shape_set_capsule(capsule_shape, Capsule { center1: math.vec3(0.0 - 0.3, 0.0, 0.0), center2: math.vec3(0.3, 0.0, 0.0), radius: 0.15 }) + dynamics.shape_set_hull(hull_shape, g_rock as *HullData) + dynamics.shape_set_mesh(mesh_shape, g_wave as *MeshData, math.vec3(1.0, 0.8, 1.0)) + dynamics.shape_set_friction(hull_shape, 0.3) + dynamics.shape_set_restitution(hull_shape, 0.4) + physics_world.step(world_id, 1.0 / 60.0, 4) + dynamics.shape_set_mesh_material(mesh_shape, tinted, 1) + dynamics.shape_enable_pre_solve_events(sphere_shape, true) + dynamics.body_enable_hit_events(subject, true) + dynamics.body_allow_fast_rotation(subject, true) + dynamics.body_enable_contact_recycling(subject, true) + dynamics.body_set_safety_factor(subject, 0.3) + physics_world.step(world_id, 1.0 / 60.0, 4) + dynamics.shape_apply_wind(sphere_shape, math.vec3(2.0, 0.0, 1.0), 1.2, 0.5, 0.1, true) + dynamics.body_set_name(subject, "subject") + dynamics.shape_set_name(sphere_shape, "ball") + dynamics.joint_set_local_frame_a(j_revolute, Transform { p: math.vec3(0.0, 0.0, 0.62), q: math.quat_identity() }) + dynamics.joint_set_local_frame_b(j_revolute, Transform { p: math.vec3(0.0, 0.0, 0.0 - 0.62), q: math.quat_identity() }) + dynamics.joint_set_collide_connected(j_distance, true) + physics_world.step(world_id, 1.0 / 60.0, 4) + dynamics.joint_wake_bodies(j_wheel) + dynamics.joint_set_constraint_tuning(j_weld, 60.0, 5.0) + dynamics.joint_set_force_threshold(j_weld, 1000.0) + dynamics.joint_set_torque_threshold(j_weld, 1000.0) + dynamics.shape_enable_sensor_events(sphere_shape, true) + dynamics.shape_enable_contact_events(sphere_shape, true) + physics_world.step(world_id, 1.0 / 60.0, 4) + dynamics.shape_enable_hit_events(sphere_shape, true) + physics_world.enable_sleeping(world_id, true) + physics_world.enable_warm_starting(world_id, true) + physics_world.enable_continuous(world_id, true) + physics_world.enable_speculative(world_id, true) + physics_world.set_restitution_threshold(world_id, 0.8) + physics_world.step(world_id, 1.0 / 60.0, 4) + physics_world.set_restitution_iterations(world_id, 3) + physics_world.enable_restitution_propagation(world_id, true) + physics_world.enable_kinematic_static_softness(world_id, true) + physics_world.set_hit_event_threshold(world_id, 0.5) + physics_world.set_contact_tuning(world_id, 25.0, 8.0, 2.5) + physics_world.set_contact_recycle_distance(world_id, 0.02) + physics_world.step(world_id, 1.0 / 60.0, 4) + physics_world.set_maximum_linear_speed(world_id, 200.0) + physics_world.set_gravity(world_id, math.vec3(0.2, 0.0 - 9.5, 0.0)) + physics_world.rebuild_static_tree(world_id) + physics_world.explode(world_id, &blast) + joint_solver.distance_joint_set_length(j_distance, 0.75) + joint_solver.distance_joint_enable_limit(j_distance, true) + physics_world.step(world_id, 1.0 / 60.0, 4) + joint_solver.distance_joint_set_length_range(j_distance, 0.3, 0.9) + joint_solver.distance_joint_enable_spring(j_distance, true) + joint_solver.distance_joint_set_spring_force_range(j_distance, 0.0 - 50.0, 50.0) + joint_solver.distance_joint_set_spring_hertz(j_distance, 0.75) + joint_solver.distance_joint_set_spring_damping_ratio(j_distance, 0.75) + joint_solver.distance_joint_enable_motor(j_distance, true) + physics_world.step(world_id, 1.0 / 60.0, 4) + joint_solver.distance_joint_set_motor_speed(j_distance, 0.75) + joint_solver.distance_joint_set_max_motor_force(j_distance, 0.75) + joint_solver.motor_joint_set_linear_velocity(j_motor, math.vec3(0.1, 0.4, 0.2)) + joint_solver.motor_joint_set_angular_velocity(j_motor, math.vec3(0.1, 0.4, 0.2)) + joint_solver.motor_joint_set_max_velocity_torque(j_motor, 0.75) + joint_solver.motor_joint_set_max_velocity_force(j_motor, 0.75) + physics_world.step(world_id, 1.0 / 60.0, 4) + joint_solver.motor_joint_set_linear_hertz(j_motor, 0.75) + joint_solver.motor_joint_set_linear_damping_ratio(j_motor, 0.75) + joint_solver.motor_joint_set_angular_hertz(j_motor, 0.75) + joint_solver.motor_joint_set_angular_damping_ratio(j_motor, 0.75) + joint_solver.motor_joint_set_max_spring_force(j_motor, 0.75) + joint_solver.motor_joint_set_max_spring_torque(j_motor, 0.75) + physics_world.step(world_id, 1.0 / 60.0, 4) + joint_solver.parallel_joint_set_spring_hertz(j_parallel, 0.75) + joint_solver.parallel_joint_set_spring_damping_ratio(j_parallel, 0.75) + joint_solver.parallel_joint_set_max_torque(j_parallel, 0.75) + joint_solver.prismatic_joint_enable_spring(j_prismatic, true) + joint_solver.prismatic_joint_set_spring_hertz(j_prismatic, 0.75) + joint_solver.prismatic_joint_set_spring_damping_ratio(j_prismatic, 0.75) + physics_world.step(world_id, 1.0 / 60.0, 4) + joint_solver.prismatic_joint_set_target_translation(j_prismatic, 0.75) + joint_solver.prismatic_joint_enable_limit(j_prismatic, true) + joint_solver.prismatic_joint_set_limits(j_prismatic, 0.0 - 0.25, 0.35) + joint_solver.prismatic_joint_enable_motor(j_prismatic, true) + joint_solver.prismatic_joint_set_motor_speed(j_prismatic, 0.75) + joint_solver.prismatic_joint_set_max_motor_force(j_prismatic, 0.75) + physics_world.step(world_id, 1.0 / 60.0, 4) + joint_solver.revolute_joint_enable_limit(j_revolute, true) + joint_solver.revolute_joint_set_limits(j_revolute, 0.0 - 0.6, 0.7) + joint_solver.revolute_joint_enable_spring(j_revolute, true) + joint_solver.revolute_joint_set_target_angle(j_revolute, 0.75) + joint_solver.revolute_joint_set_spring_hertz(j_revolute, 0.75) + joint_solver.revolute_joint_set_spring_damping_ratio(j_revolute, 0.75) + physics_world.step(world_id, 1.0 / 60.0, 4) + joint_solver.revolute_joint_enable_motor(j_revolute, true) + joint_solver.revolute_joint_set_motor_speed(j_revolute, 0.75) + joint_solver.revolute_joint_set_max_motor_torque(j_revolute, 0.75) + joint_solver.spherical_joint_enable_cone_limit(j_spherical, true) + joint_solver.spherical_joint_set_cone_limit(j_spherical, 0.75) + joint_solver.spherical_joint_set_cone_axis(j_spherical, math.vec3(0.1, 0.4, 0.2)) + physics_world.step(world_id, 1.0 / 60.0, 4) + joint_solver.spherical_joint_set_cone_ellipse(j_spherical, 0.5, 0.3, math.vec3(1.0, 0.0, 0.0)) + joint_solver.spherical_joint_enable_twist_limit(j_spherical, true) + joint_solver.spherical_joint_set_twist_limits(j_spherical, 0.0 - 0.4, 0.5) + joint_solver.spherical_joint_enable_spring(j_spherical, true) + joint_solver.spherical_joint_set_target_rotation(j_spherical, math.make_quat_from_axis_angle(math.vec3(0.0, 1.0, 0.0), 0.25)) + joint_solver.spherical_joint_set_spring_hertz(j_spherical, 0.75) + physics_world.step(world_id, 1.0 / 60.0, 4) + joint_solver.spherical_joint_set_spring_damping_ratio(j_spherical, 0.75) + joint_solver.spherical_joint_enable_motor(j_spherical, true) + joint_solver.spherical_joint_set_motor_velocity(j_spherical, math.vec3(0.1, 0.4, 0.2)) + joint_solver.spherical_joint_set_max_motor_torque(j_spherical, 0.75) + joint_solver.weld_joint_set_linear_hertz(j_weld, 0.75) + joint_solver.weld_joint_set_linear_damping_ratio(j_weld, 0.75) + physics_world.step(world_id, 1.0 / 60.0, 4) + joint_solver.weld_joint_set_angular_hertz(j_weld, 0.75) + joint_solver.weld_joint_set_angular_damping_ratio(j_weld, 0.75) + joint_solver.wheel_joint_enable_suspension(j_wheel, true) + joint_solver.wheel_joint_set_suspension_hertz(j_wheel, 0.75) + joint_solver.wheel_joint_set_suspension_damping_ratio(j_wheel, 0.75) + joint_solver.wheel_joint_enable_suspension_limit(j_wheel, true) + physics_world.step(world_id, 1.0 / 60.0, 4) + joint_solver.wheel_joint_set_suspension_limits(j_wheel, 0.0 - 0.2, 0.2) + joint_solver.wheel_joint_enable_spin_motor(j_wheel, true) + joint_solver.wheel_joint_set_spin_motor_speed(j_wheel, 0.75) + joint_solver.wheel_joint_set_max_spin_torque(j_wheel, 0.75) + joint_solver.wheel_joint_enable_steering(j_wheel, true) + joint_solver.wheel_joint_set_steering_hertz(j_wheel, 0.75) + physics_world.step(world_id, 1.0 / 60.0, 4) + joint_solver.wheel_joint_set_steering_damping_ratio(j_wheel, 0.75) + joint_solver.wheel_joint_set_max_steering_torque(j_wheel, 0.75) + joint_solver.wheel_joint_enable_steering_limit(j_wheel, true) + joint_solver.wheel_joint_set_steering_limits(j_wheel, 0.0 - 0.5, 0.5) + joint_solver.wheel_joint_set_target_steering_angle(j_wheel, 0.75) + dynamics.destroy_body(spare) + physics_world.step(world_id, 1.0 / 60.0, 4) + dynamics.destroy_shape(capsule_shape, true) + dynamics.destroy_joint(j_filter, true) + dynamics.body_set_type(heavy, dynamics.BODY_KINEMATIC) + dynamics.body_disable(heavy) + dynamics.body_enable(heavy) + physics_world.step(world_id, 1.0 / 60.0, 4) + steps(world_id, 20) + final_hash = state_hash(world_id) + recording.stop_recording(world_id) + dynamics.destroy_world(world_id) + + // Every opcode a recording has. + all_op_count = 160 + all_ops_list = [ + dynamics.OP_CREATE_BODY, dynamics.OP_DESTROY_BODY, dynamics.OP_BODY_SET_TRANSFORM, + dynamics.OP_BODY_SET_LINEAR_VELOCITY, dynamics.OP_BODY_SET_ANGULAR_VELOCITY, + dynamics.OP_BODY_SET_TARGET_TRANSFORM, dynamics.OP_BODY_APPLY_FORCE, dynamics.OP_BODY_APPLY_FORCE_TO_CENTER, + dynamics.OP_BODY_APPLY_TORQUE, dynamics.OP_BODY_APPLY_LINEAR_IMPULSE, + dynamics.OP_BODY_APPLY_LINEAR_IMPULSE_TO_CENTER, dynamics.OP_BODY_APPLY_ANGULAR_IMPULSE, + dynamics.OP_BODY_SET_BULLET, dynamics.OP_BODY_SET_MASS_DATA, dynamics.OP_BODY_APPLY_MASS_FROM_SHAPES, + dynamics.OP_BODY_SET_LINEAR_DAMPING, dynamics.OP_BODY_SET_ANGULAR_DAMPING, + dynamics.OP_BODY_SET_GRAVITY_SCALE, dynamics.OP_BODY_SET_AWAKE, dynamics.OP_BODY_ENABLE_SLEEP, + dynamics.OP_BODY_SET_SLEEP_THRESHOLD, dynamics.OP_BODY_SET_MOTION_LOCKS, dynamics.OP_CREATE_SPHERE_SHAPE, + dynamics.OP_CREATE_CAPSULE_SHAPE, dynamics.OP_CREATE_HULL_SHAPE, dynamics.OP_CREATE_TRANSFORMED_HULL_SHAPE, + dynamics.OP_CREATE_MESH_SHAPE, dynamics.OP_CREATE_HEIGHT_FIELD_SHAPE, dynamics.OP_CREATE_COMPOUND_SHAPE, + dynamics.OP_DESTROY_SHAPE, dynamics.OP_SHAPE_SET_DENSITY, dynamics.OP_SHAPE_SET_FILTER, + dynamics.OP_SHAPE_SET_SURFACE_MATERIAL, dynamics.OP_SHAPE_SET_SPHERE, dynamics.OP_SHAPE_SET_CAPSULE, + dynamics.OP_SHAPE_SET_HULL, dynamics.OP_SHAPE_SET_MESH, dynamics.OP_SHAPE_SET_FRICTION, + dynamics.OP_SHAPE_SET_RESTITUTION, dynamics.OP_SHAPE_SET_MESH_MATERIAL, + dynamics.OP_SHAPE_ENABLE_PRE_SOLVE_EVENTS, dynamics.OP_BODY_ENABLE_HIT_EVENTS, + dynamics.OP_BODY_ALLOW_FAST_ROTATION, dynamics.OP_BODY_ENABLE_CONTACT_RECYCLING, + dynamics.OP_BODY_SET_SAFETY_FACTOR, dynamics.OP_SHAPE_APPLY_WIND, dynamics.OP_BODY_SET_NAME, + dynamics.OP_SHAPE_SET_NAME, dynamics.OP_CREATE_DISTANCE_JOINT, dynamics.OP_CREATE_MOTOR_JOINT, + dynamics.OP_CREATE_FILTER_JOINT, dynamics.OP_CREATE_PARALLEL_JOINT, dynamics.OP_CREATE_PRISMATIC_JOINT, + dynamics.OP_CREATE_REVOLUTE_JOINT, dynamics.OP_CREATE_SPHERICAL_JOINT, dynamics.OP_CREATE_WELD_JOINT, + dynamics.OP_CREATE_WHEEL_JOINT, dynamics.OP_DESTROY_JOINT, dynamics.OP_JOINT_SET_LOCAL_FRAME_A, + dynamics.OP_JOINT_SET_LOCAL_FRAME_B, dynamics.OP_JOINT_SET_COLLIDE_CONNECTED, dynamics.OP_JOINT_WAKE_BODIES, + dynamics.OP_JOINT_SET_CONSTRAINT_TUNING, dynamics.OP_JOINT_SET_FORCE_THRESHOLD, + dynamics.OP_JOINT_SET_TORQUE_THRESHOLD, dynamics.OP_BODY_SET_TYPE, dynamics.OP_BODY_DISABLE, + dynamics.OP_BODY_ENABLE, dynamics.OP_SHAPE_ENABLE_SENSOR_EVENTS, dynamics.OP_SHAPE_ENABLE_CONTACT_EVENTS, + dynamics.OP_SHAPE_ENABLE_HIT_EVENTS, dynamics.OP_STEP, dynamics.OP_ENABLE_SLEEPING, + dynamics.OP_ENABLE_WARM_STARTING, dynamics.OP_ENABLE_CONTINUOUS, dynamics.OP_ENABLE_SPECULATIVE, + dynamics.OP_SET_RESTITUTION_THRESHOLD, dynamics.OP_SET_RESTITUTION_ITERATIONS, + dynamics.OP_ENABLE_RESTITUTION_PROPAGATION, dynamics.OP_ENABLE_KINEMATIC_STATIC_SOFTNESS, + dynamics.OP_SET_HIT_EVENT_THRESHOLD, dynamics.OP_SET_CONTACT_TUNING, + dynamics.OP_SET_CONTACT_RECYCLE_DISTANCE, dynamics.OP_SET_MAXIMUM_LINEAR_SPEED, dynamics.OP_SET_GRAVITY, + dynamics.OP_REBUILD_STATIC_TREE, dynamics.OP_EXPLODE, dynamics.OP_DISTANCE_JOINT_SET_LENGTH, + dynamics.OP_DISTANCE_JOINT_ENABLE_LIMIT, dynamics.OP_DISTANCE_JOINT_SET_LENGTH_RANGE, + dynamics.OP_DISTANCE_JOINT_ENABLE_SPRING, dynamics.OP_DISTANCE_JOINT_SET_SPRING_FORCE_RANGE, + dynamics.OP_DISTANCE_JOINT_SET_SPRING_HERTZ, dynamics.OP_DISTANCE_JOINT_SET_SPRING_DAMPING_RATIO, + dynamics.OP_DISTANCE_JOINT_ENABLE_MOTOR, dynamics.OP_DISTANCE_JOINT_SET_MOTOR_SPEED, + dynamics.OP_DISTANCE_JOINT_SET_MAX_MOTOR_FORCE, dynamics.OP_MOTOR_JOINT_SET_LINEAR_VELOCITY, + dynamics.OP_MOTOR_JOINT_SET_ANGULAR_VELOCITY, dynamics.OP_MOTOR_JOINT_SET_MAX_VELOCITY_TORQUE, + dynamics.OP_MOTOR_JOINT_SET_MAX_VELOCITY_FORCE, dynamics.OP_MOTOR_JOINT_SET_LINEAR_HERTZ, + dynamics.OP_MOTOR_JOINT_SET_LINEAR_DAMPING_RATIO, dynamics.OP_MOTOR_JOINT_SET_ANGULAR_HERTZ, + dynamics.OP_MOTOR_JOINT_SET_ANGULAR_DAMPING_RATIO, dynamics.OP_MOTOR_JOINT_SET_MAX_SPRING_FORCE, + dynamics.OP_MOTOR_JOINT_SET_MAX_SPRING_TORQUE, dynamics.OP_PARALLEL_JOINT_SET_SPRING_HERTZ, + dynamics.OP_PARALLEL_JOINT_SET_SPRING_DAMPING_RATIO, dynamics.OP_PARALLEL_JOINT_SET_MAX_TORQUE, + dynamics.OP_PRISMATIC_JOINT_ENABLE_SPRING, dynamics.OP_PRISMATIC_JOINT_SET_SPRING_HERTZ, + dynamics.OP_PRISMATIC_JOINT_SET_SPRING_DAMPING_RATIO, dynamics.OP_PRISMATIC_JOINT_SET_TARGET_TRANSLATION, + dynamics.OP_PRISMATIC_JOINT_ENABLE_LIMIT, dynamics.OP_PRISMATIC_JOINT_SET_LIMITS, + dynamics.OP_PRISMATIC_JOINT_ENABLE_MOTOR, dynamics.OP_PRISMATIC_JOINT_SET_MOTOR_SPEED, + dynamics.OP_PRISMATIC_JOINT_SET_MAX_MOTOR_FORCE, dynamics.OP_REVOLUTE_JOINT_ENABLE_LIMIT, + dynamics.OP_REVOLUTE_JOINT_SET_LIMITS, dynamics.OP_REVOLUTE_JOINT_ENABLE_SPRING, + dynamics.OP_REVOLUTE_JOINT_SET_TARGET_ANGLE, dynamics.OP_REVOLUTE_JOINT_SET_SPRING_HERTZ, + dynamics.OP_REVOLUTE_JOINT_SET_SPRING_DAMPING_RATIO, dynamics.OP_REVOLUTE_JOINT_ENABLE_MOTOR, + dynamics.OP_REVOLUTE_JOINT_SET_MOTOR_SPEED, dynamics.OP_REVOLUTE_JOINT_SET_MAX_MOTOR_TORQUE, + dynamics.OP_SPHERICAL_JOINT_ENABLE_CONE_LIMIT, dynamics.OP_SPHERICAL_JOINT_SET_CONE_LIMIT, + dynamics.OP_SPHERICAL_JOINT_SET_CONE_AXIS, dynamics.OP_SPHERICAL_JOINT_SET_CONE_ELLIPSE, + dynamics.OP_SPHERICAL_JOINT_ENABLE_TWIST_LIMIT, dynamics.OP_SPHERICAL_JOINT_SET_TWIST_LIMITS, + dynamics.OP_SPHERICAL_JOINT_ENABLE_SPRING, dynamics.OP_SPHERICAL_JOINT_SET_TARGET_ROTATION, + dynamics.OP_SPHERICAL_JOINT_SET_SPRING_HERTZ, dynamics.OP_SPHERICAL_JOINT_SET_SPRING_DAMPING_RATIO, + dynamics.OP_SPHERICAL_JOINT_ENABLE_MOTOR, dynamics.OP_SPHERICAL_JOINT_SET_MOTOR_VELOCITY, + dynamics.OP_SPHERICAL_JOINT_SET_MAX_MOTOR_TORQUE, dynamics.OP_WELD_JOINT_SET_LINEAR_HERTZ, + dynamics.OP_WELD_JOINT_SET_LINEAR_DAMPING_RATIO, dynamics.OP_WELD_JOINT_SET_ANGULAR_HERTZ, + dynamics.OP_WELD_JOINT_SET_ANGULAR_DAMPING_RATIO, dynamics.OP_WHEEL_JOINT_ENABLE_SUSPENSION, + dynamics.OP_WHEEL_JOINT_SET_SUSPENSION_HERTZ, dynamics.OP_WHEEL_JOINT_SET_SUSPENSION_DAMPING_RATIO, + dynamics.OP_WHEEL_JOINT_ENABLE_SUSPENSION_LIMIT, dynamics.OP_WHEEL_JOINT_SET_SUSPENSION_LIMITS, + dynamics.OP_WHEEL_JOINT_ENABLE_SPIN_MOTOR, dynamics.OP_WHEEL_JOINT_SET_SPIN_MOTOR_SPEED, + dynamics.OP_WHEEL_JOINT_SET_MAX_SPIN_TORQUE, dynamics.OP_WHEEL_JOINT_ENABLE_STEERING, + dynamics.OP_WHEEL_JOINT_SET_STEERING_HERTZ, dynamics.OP_WHEEL_JOINT_SET_STEERING_DAMPING_RATIO, + dynamics.OP_WHEEL_JOINT_SET_MAX_STEERING_TORQUE, dynamics.OP_WHEEL_JOINT_ENABLE_STEERING_LIMIT, + dynamics.OP_WHEEL_JOINT_SET_STEERING_LIMITS, dynamics.OP_WHEEL_JOINT_SET_TARGET_STEERING_ANGLE + ] + seen = calloc(4096, 4) + mark_ops(r, seen) + marks = seen as int[] + missing = 0 + i = 0 + while i < all_op_count { + if marks[all_ops_list[i]] == 0 { + println("all ops: opcode ${all_ops_list[i]} not recorded") + missing = missing + 1 + } + i = i + 1 + } + free(seen) + ensure("all ops: every opcode recorded (${missing} missing)", missing == 0) + p = recording.create_player(recording.recording_data(r), recording.recording_size(r), 1) + ensure("all ops: plays", p != null) + if p != null { + while recording.player_step_frame(p) {} + ensure("all ops: no divergence (frame ${recording.player_diverge_frame(p)})", recording.player_has_diverged(p) == false) + ensure("all ops: the same world at the end", state_hash(recording.player_world(p)) == final_hash) + recording.destroy_player(p) + } + recording.destroy_recording(r) +} + + +main() { + round_trips() + sessions() + geometry_dedup() + seeking() + files() + corruptions() + all_ops() + fixtures_free() + if failures > 0 { + println("recording: ${failures} of ${checks} checks failed") + exit(1) + } + println("recording: ${checks} checks passed") +} diff --git a/tests/test_snapshot.ae b/tests/test_snapshot.ae index baf616c..a54078f 100644 --- a/tests/test_snapshot.ae +++ b/tests/test_snapshot.ae @@ -25,6 +25,7 @@ import aephysics.joint_solver import aephysics.physics_world import aephysics.human import aephysics.snapshot +import aephysics.recording extern calloc(count: int, size: int) -> ptr extern exit(code: int) @@ -441,6 +442,64 @@ across_workers() { dynamics.destroy_world(original) } +// A world restored in place keeps its id and its host's wiring, and +// steps on as the world it was saved from; bytes that are not a snapshot, +// a world mid-recording and a world that is not one are refused, the +// refused world left empty and whole. +restore_in_place() { + wdef = world_def() + original = dynamics.create_world(&wdef) + build_scene(original) + steps(original, 25) + block, byte_count = snapshot.save_world(original) + target_def = world_def() + target_def.worker_count = 2 + target_def.user_data = block + target = dynamics.create_world(&target_def) + sdef = dynamics.default_shape_def() + stale = dynamic_body(target, math.vec3(0.0, 5.0, 0.0)) + dynamics.create_sphere_shape(stale, &sdef, manifold.sphere(math.vec3_zero(), 0.5)) + steps(target, 3) + ensure("restore: in place", snapshot.restore_world(target, &target_def, block, byte_count)) + w = dynamics.get_world(target.index1 - 1) + ensure("restore: the id, workers and user data stay", dynamics.world_is_valid(target) && w.worker_count == 2 && w.user_data == block) + ensure("restore: hashes as the original", state_hash(target) == state_hash(original)) + alike = true + i = 0 + while i < 60 { + physics_world.step(original, 1.0 / 60.0, 4) + physics_world.step(target, 1.0 / 60.0, 4) + if state_hash(original) != state_hash(target) || events_of(original) != events_of(target) { alike = false } + i = i + 1 + } + ensure("restore: steps on alike for 60 steps", alike) + ensure("restore: again over a stepped world", snapshot.restore_world(target, &target_def, block, byte_count)) + resaved, resaved_count = snapshot.save_world(target) + ensure("restore: saved again, the same bytes", resaved_count == byte_count && memcmp(resaved, block, byte_count) == 0) + snapshot.free_snapshot(resaved, resaved_count) + + snapshot.restore_world(target, &target_def, block, byte_count) + loaded_count = basics.alloc_count() + refused = snapshot.restore_world(target, &target_def, block, byte_count / 2) == false + ensure("restore: a truncation refused", refused) + ensure("restore: the refused world is empty and whole", dynamics.world_is_valid(target) && dynamics.world_body_count(w) == 0 && w.worker_count == 2) + steps(target, 2) + snapshot.restore_world(target, &target_def, block, byte_count) + ensure("restore: the refusal leaves nothing behind (${basics.alloc_count() - loaded_count})", basics.alloc_count() == loaded_count) + ensure("restore: no world", snapshot.restore_world(WorldId { index1: 0, generation: 0 }, &target_def, block, byte_count) == false) + ensure("restore: no bytes", snapshot.restore_world(target, &target_def, null, 0) == false) + r = recording.create_recording() + recording.start_recording(target, r) + ensure("restore: a world that records is refused", snapshot.restore_world(target, &target_def, block, byte_count) == false) + recording.stop_recording(target) + recording.destroy_recording(r) + ensure("restore: after the recording stops", snapshot.restore_world(target, &target_def, block, byte_count) && state_hash(target) != (0 as long)) + dynamics.destroy_world(target) + snapshot.free_snapshot(block, byte_count) + human.destroy_human(g_ragdoll as *Human) + dynamics.destroy_world(original) +} + // Every damage to the bytes loads no world and leaves nothing behind. corruptions() { wdef = world_def() @@ -513,6 +572,7 @@ main() { round_trip(45) round_trip(200) across_workers() + restore_in_place() corruptions() ensure("every counted allocation was freed (${basics.alloc_count() - before})", basics.alloc_count() == before) fixtures_free()