diff --git a/src/AcDream.Core/Physics/PhysicsBody.cs b/src/AcDream.Core/Physics/PhysicsBody.cs
new file mode 100644
index 0000000..06b6781
--- /dev/null
+++ b/src/AcDream.Core/Physics/PhysicsBody.cs
@@ -0,0 +1,373 @@
+using System;
+using System.Numerics;
+
+namespace AcDream.Core.Physics;
+
+// ────────────────────────────────────────────────────────────────────────────
+// PhysicsBody — C# port of CPhysicsObj's core simulation from acclient.exe.
+//
+// Source addresses (chunk_00510000.c, chunk_00500000.c):
+// FUN_005111d0 UpdatePhysicsInternal — Euler integration
+// FUN_00511420 calc_acceleration — gravity / grounded acceleration
+// FUN_00511ec0 set_velocity — store + clamp to MaxVelocity
+// FUN_00511fa0 set_local_velocity — body→world transform then set_velocity
+// FUN_00511de0 set_on_walkable — set/clear OnWalkable transient flag
+// FUN_0050f940 calc_friction — ground-contact friction
+// FUN_00515020 update_object — per-frame top-level driver
+//
+// Cross-checked against ACE PhysicsObj.cs and PhysicsGlobals.cs.
+// ────────────────────────────────────────────────────────────────────────────
+
+///
+/// State flags stored at struct offset +0xA8 (PhysicsState).
+/// Only the flags relevant to this simulation layer are included.
+///
+[Flags]
+public enum PhysicsStateFlags : uint
+{
+ None = 0,
+ Static = 0x00000001, // bit 0 — never moves
+ Ethereal = 0x00000004, // bit 2 — no collision
+ ReportCollisions = 0x00000010,
+ Gravity = 0x00000400, // bit 10 — apply downward gravity
+ Hidden = 0x00001000,
+ Sledding = 0x00800000, // bit 23 — sledding (modified friction)
+}
+
+///
+/// Transient-state flags stored at struct offset +0xAC (TransientState).
+/// These are cleared/set each frame and must not be saved to disk.
+///
+[Flags]
+public enum TransientStateFlags : uint
+{
+ None = 0,
+ Contact = 0x00000001, // bit 0 — touching any surface
+ OnWalkable = 0x00000002, // bit 1 — standing on a walkable surface
+ Active = 0x00000080, // bit 7 — object needs per-frame update
+}
+
+///
+/// Port of CPhysicsObj's core simulation state and Euler integration.
+/// Holds the fields at the struct offsets documented in acclient_function_map.md
+/// and implements the seven methods listed in the task spec.
+///
+public sealed class PhysicsBody
+{
+ // ── constants ──────────────────────────────────────────────────────────
+ // From PhysicsGlobals.cs / confirmed by DAT_007c78a4 reference in decompiled code.
+ public const float MaxVelocity = 50.0f;
+ public const float MaxVelocitySquared = MaxVelocity * MaxVelocity;
+ public const float Gravity = -9.8f; // DAT_0082223c in FUN_00511420
+ public const float SmallVelocity = 0.25f;
+ public const float SmallVelocitySquared = SmallVelocity * SmallVelocity;
+ public const float DefaultFriction = 0.95f;
+ public const float MinQuantum = 1.0f / 30.0f; // ~0.0333 s
+ public const float MaxQuantum = 0.1f; // 10 fps lower bound
+ public const float HugeQuantum = 2.0f; // discard stale dt
+
+ // ── struct fields ──────────────────────────────────────────────────────
+ // Offsets from acclient_function_map.md §PhysicsObj Struct Layout.
+
+ /// World-space position (no offset in struct — frame origin).
+ public Vector3 Position { get; set; }
+
+ /// Orientation quaternion (struct offsets 0x60–0x80 column matrix).
+ public Quaternion Orientation { get; set; } = Quaternion.Identity;
+
+ /// World-space velocity (+0xE0/E4/E8).
+ public Vector3 Velocity { get; set; }
+
+ /// World-space acceleration (+0xEC/F0/F4).
+ public Vector3 Acceleration { get; set; }
+
+ /// Angular velocity in radians/s (+0xF8/FC/100).
+ public Vector3 Omega { get; set; }
+
+ /// Ground contact-plane normal (+0x130/134/138).
+ public Vector3 GroundNormal { get; set; } = Vector3.UnitZ;
+
+ /// Elasticity coefficient (+0xB0).
+ public float Elasticity { get; set; } = 0.05f;
+
+ /// Friction coefficient (0 = frictionless, 1 = instant stop).
+ public float Friction { get; set; } = DefaultFriction;
+
+ /// Physics state flags (+0xA8).
+ public PhysicsStateFlags State { get; set; }
+ = PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions;
+
+ /// Transient state flags (+0xAC). Cleared each frame as needed.
+ public TransientStateFlags TransientState { get; set; }
+
+ /// Last simulation time used to compute dt (+0xD8).
+ public double LastUpdateTime { get; set; }
+
+ // ── convenience helpers ────────────────────────────────────────────────
+
+ public bool HasGravity => State.HasFlag(PhysicsStateFlags.Gravity);
+ public bool OnWalkable => TransientState.HasFlag(TransientStateFlags.OnWalkable);
+ public bool IsActive => TransientState.HasFlag(TransientStateFlags.Active);
+ public bool InContact => TransientState.HasFlag(TransientStateFlags.Contact);
+
+ // ── FUN_00511420 ───────────────────────────────────────────────────────
+
+ ///
+ /// Set Acceleration (and Omega) based on current contact state and flags.
+ ///
+ /// Decompiled logic (FUN_00511420):
+ /// If Contact AND OnWalkable AND NOT Sledding → zero everything (grounded, no drift).
+ /// Else if Gravity flag → Accel = (0, 0, -9.8).
+ /// Else → zero acceleration.
+ ///
+ /// The check order in the decompile is:
+ /// (TransientState & 1) != 0 → Contact
+ /// (TransientState & 2) != 0 → OnWalkable
+ /// (State & 0x800000) == 0 → NOT Sledding
+ ///
+ public void calc_acceleration()
+ {
+ if (TransientState.HasFlag(TransientStateFlags.Contact) &&
+ TransientState.HasFlag(TransientStateFlags.OnWalkable) &&
+ !State.HasFlag(PhysicsStateFlags.Sledding))
+ {
+ Acceleration = Vector3.Zero;
+ Omega = Vector3.Zero;
+ return;
+ }
+
+ if (State.HasFlag(PhysicsStateFlags.Gravity))
+ Acceleration = new Vector3(0f, 0f, Gravity);
+ else
+ Acceleration = Vector3.Zero;
+ }
+
+ // ── FUN_00511ec0 ───────────────────────────────────────────────────────
+
+ ///
+ /// Store a new world-space velocity and clamp its magnitude to MaxVelocity.
+ ///
+ /// Decompiled logic (FUN_00511ec0):
+ /// velocity = newVelocity
+ /// if |velocity|² > MaxVelocity²:
+ /// normalize then scale by MaxVelocity (FUN_00452440 = normalize + scalar)
+ /// Set Active transient flag.
+ ///
+ public void set_velocity(Vector3 newVelocity)
+ {
+ Velocity = newVelocity;
+
+ float mag2 = Velocity.LengthSquared();
+ if (mag2 > MaxVelocitySquared)
+ {
+ // Normalize then scale — matches the decompile's FUN_00452440 call
+ // which normalizes the vector then multiplies by _DAT_007c78a4 (MaxVelocity).
+ Velocity = Vector3.Normalize(Velocity) * MaxVelocity;
+ }
+
+ // Set Active flag (bit 7 of TransientState, offset +0xAC).
+ TransientState |= TransientStateFlags.Active;
+ }
+
+ // ── FUN_00511fa0 ───────────────────────────────────────────────────────
+
+ ///
+ /// Transform a body-local velocity vector into world space using the
+ /// orientation quaternion, then call set_velocity.
+ ///
+ /// Decompiled logic (FUN_00511fa0):
+ /// The orientation is stored as a 3x3 column matrix at offsets 0x60–0x80
+ /// (9 floats). The transform is a straightforward matrix×vector multiply:
+ /// worldX = col0.x*localX + col1.x*localY + col2.x*localZ
+ /// worldY = col0.y*localX + col1.y*localY + col2.y*localZ
+ /// worldZ = col0.z*localX + col1.z*localY + col2.z*localZ
+ /// We replicate this as a Quaternion rotation, which is equivalent.
+ ///
+ public void set_local_velocity(Vector3 localVelocity)
+ {
+ var worldVelocity = Vector3.Transform(localVelocity, Orientation);
+ set_velocity(worldVelocity);
+ }
+
+ // ── FUN_00511de0 ───────────────────────────────────────────────────────
+
+ ///
+ /// Set or clear the OnWalkable transient flag (bit 1 of TransientState at
+ /// +0xAC), then recompute acceleration.
+ ///
+ /// Decompiled logic (FUN_00511de0):
+ /// if param_2 == 0: TransientState &= ~0x02 (clear OnWalkable)
+ /// else: TransientState |= 0x02 (set OnWalkable)
+ /// call calc_acceleration()
+ ///
+ public void set_on_walkable(bool isOnWalkable)
+ {
+ if (isOnWalkable)
+ TransientState |= TransientStateFlags.OnWalkable;
+ else
+ TransientState &= ~TransientStateFlags.OnWalkable;
+
+ calc_acceleration();
+ }
+
+ // ── FUN_0050f940 ───────────────────────────────────────────────────────
+
+ ///
+ /// Apply friction deceleration to the velocity when the body is standing
+ /// on a walkable surface.
+ ///
+ /// Decompiled logic (FUN_0050f940):
+ /// if NOT OnWalkable → return
+ /// fVar1 = dot(groundNormal, velocity)
+ /// if fVar1 < 0:
+ /// velocity -= fVar1 * groundNormal (remove inward normal component)
+ /// scalar = pow(1 - friction, dt)
+ /// velocity *= scalar
+ ///
+ /// The threshold (0.0 from _DAT_007c78a0) means any velocity with a
+ /// downward component relative to the normal gets friction applied.
+ /// Positive dot means moving away from the surface — no friction.
+ ///
+ /// Cross-checked with ACE PhysicsObj.calc_friction which uses 0.25f as
+ /// the threshold instead; the decompile uses 0.0. We match the decompile.
+ ///
+ public void calc_friction(float dt, float velocityMag2)
+ {
+ if (!TransientState.HasFlag(TransientStateFlags.OnWalkable))
+ return;
+
+ float dot = Vector3.Dot(GroundNormal, Velocity);
+ if (dot >= 0f)
+ return;
+
+ // Remove the component of velocity that presses into the ground normal.
+ Velocity -= dot * GroundNormal;
+
+ float friction = Friction;
+
+ // Sledding modifies friction thresholds (from ACE cross-check).
+ if (State.HasFlag(PhysicsStateFlags.Sledding))
+ {
+ if (velocityMag2 < 1.5625f) // 1.25² — slow sled
+ friction = 1.0f;
+ else if (velocityMag2 >= 6.25f && GroundNormal.Z > 0.99999536f) // near-flat
+ friction = 0.2f;
+ }
+
+ // Exponential decay: vel *= (1 - friction)^dt
+ float scalar = MathF.Pow(1.0f - friction, dt);
+ Velocity *= scalar;
+ }
+
+ // ── FUN_005111d0 ───────────────────────────────────────────────────────
+
+ ///
+ /// Euler integration step for one quantum dt.
+ ///
+ /// Decompiled logic (FUN_005111d0):
+ /// velocity_mag2 = |velocity|²
+ /// if velocity_mag2 == 0:
+ /// if no MovementManager AND OnWalkable → clear Active flag
+ /// else:
+ /// if velocity_mag2 > MaxVelocitySquared: normalize * MaxVelocity
+ /// calc_friction(dt, velocity_mag2)
+ /// if velocity_mag2 < SmallVelocitySquared: zero velocity
+ /// position += velocity * dt + 0.5 * acceleration * dt²
+ /// velocity += acceleration * dt
+ /// Apply angular delta: orientation rotated by omega * dt
+ ///
+ public void UpdatePhysicsInternal(float dt)
+ {
+ float velocityMag2 = Velocity.LengthSquared();
+
+ if (velocityMag2 <= 0f)
+ {
+ // No movement manager equivalent here; just clear Active if grounded.
+ if (TransientState.HasFlag(TransientStateFlags.OnWalkable))
+ TransientState &= ~TransientStateFlags.Active;
+ }
+ else
+ {
+ // Clamp velocity magnitude to MaxVelocity.
+ if (velocityMag2 > MaxVelocitySquared)
+ {
+ Velocity = Vector3.Normalize(Velocity) * MaxVelocity;
+ velocityMag2 = MaxVelocitySquared;
+ }
+
+ calc_friction(dt, velocityMag2);
+
+ // If velocity fell below the "small" threshold after friction, stop.
+ if (velocityMag2 - SmallVelocitySquared < 0.0002f)
+ Velocity = Vector3.Zero;
+
+ // Euler integration: position += v*dt + 0.5*a*dt²
+ Position += Velocity * dt + Acceleration * (0.5f * dt * dt);
+ }
+
+ // velocity += acceleration * dt (done unconditionally in decompile)
+ Velocity += Acceleration * dt;
+
+ // Angular integration: apply omega rotation.
+ // omega * dt gives the angle-axis delta rotation.
+ float omegaLen = Omega.Length();
+ if (omegaLen > 1e-6f)
+ {
+ float angle = omegaLen * dt;
+ Quaternion deltaRot = Quaternion.CreateFromAxisAngle(Omega / omegaLen, angle);
+ Orientation = Quaternion.Normalize(Quaternion.Multiply(Orientation, deltaRot));
+ }
+ }
+
+ // ── FUN_00515020 ───────────────────────────────────────────────────────
+
+ ///
+ /// Per-frame top-level driver. Computes dt from the wall clock versus
+ /// LastUpdateTime, clamping to [MinQuantum, HugeQuantum], then calls
+ /// calc_acceleration and UpdatePhysicsInternal.
+ ///
+ /// Decompiled logic (FUN_00515020):
+ /// if parent-attached (offset +0x40 != 0) → return
+ /// dVar1 = currentTime - LastUpdateTime
+ /// if dVar1 < MinQuantum → return (too short — skip)
+ /// if dVar1 > HugeQuantum → update timestamp and return (stale — discard)
+ /// while dVar1 > MaxQuantum: simulate MaxQuantum step, subtract
+ /// if dVar1 > MinQuantum: simulate remainder
+ /// LastUpdateTime = currentTime
+ ///
+ /// The caller passes currentTime; the object does not read a global clock
+ /// directly in this port so tests can drive the clock explicitly.
+ ///
+ public void update_object(double currentTime)
+ {
+ double deltaTime = currentTime - LastUpdateTime;
+
+ // dt too small — nothing to simulate yet
+ if (deltaTime < MinQuantum)
+ return;
+
+ // Stale / first frame — just consume the time without simulating
+ if (deltaTime > HugeQuantum)
+ {
+ LastUpdateTime = currentTime;
+ return;
+ }
+
+ // Sub-step: break large dt into MaxQuantum chunks
+ while (deltaTime > MaxQuantum)
+ {
+ calc_acceleration();
+ UpdatePhysicsInternal(MaxQuantum);
+ deltaTime -= MaxQuantum;
+ }
+
+ // Simulate the remainder
+ if (deltaTime > MinQuantum)
+ {
+ calc_acceleration();
+ UpdatePhysicsInternal((float)deltaTime);
+ }
+
+ LastUpdateTime = currentTime;
+ }
+}
diff --git a/tests/AcDream.Core.Tests/Physics/PhysicsBodyTests.cs b/tests/AcDream.Core.Tests/Physics/PhysicsBodyTests.cs
new file mode 100644
index 0000000..f5593ed
--- /dev/null
+++ b/tests/AcDream.Core.Tests/Physics/PhysicsBodyTests.cs
@@ -0,0 +1,493 @@
+using System;
+using System.Numerics;
+using AcDream.Core.Physics;
+using Xunit;
+
+namespace AcDream.Core.Tests.Physics;
+
+///
+/// Unit tests for PhysicsBody — the C# port of CPhysicsObj's core simulation
+/// from acclient.exe (FUN_005111d0, FUN_00511420, FUN_00511ec0, FUN_00511fa0,
+/// FUN_00511de0, FUN_0050f940, FUN_00515020).
+///
+public sealed class PhysicsBodyTests
+{
+ // ── helpers ──────────────────────────────────────────────────────────
+
+ private static PhysicsBody MakeAirborne()
+ {
+ var body = new PhysicsBody
+ {
+ State = PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions,
+ };
+ // Airborne: not in Contact, not OnWalkable
+ body.TransientState = TransientStateFlags.Active;
+ return body;
+ }
+
+ private static PhysicsBody MakeGrounded()
+ {
+ var body = new PhysicsBody
+ {
+ State = PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions,
+ };
+ body.TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable | TransientStateFlags.Active;
+ return body;
+ }
+
+ // ════════════════════════════════════════════════════════════════════
+ // calc_acceleration
+ // ════════════════════════════════════════════════════════════════════
+
+ [Fact]
+ public void calc_acceleration_airborne_gravity_sets_minus_9_8_on_z()
+ {
+ var body = MakeAirborne();
+ body.calc_acceleration();
+
+ Assert.Equal(0f, body.Acceleration.X);
+ Assert.Equal(0f, body.Acceleration.Y);
+ Assert.Equal(-9.8f, body.Acceleration.Z, precision: 6);
+ }
+
+ [Fact]
+ public void calc_acceleration_grounded_zeros_acceleration_and_omega()
+ {
+ var body = MakeGrounded();
+ body.Acceleration = new Vector3(1f, 2f, 3f);
+ body.Omega = new Vector3(0.5f, 0.5f, 0.5f);
+ body.calc_acceleration();
+
+ Assert.Equal(Vector3.Zero, body.Acceleration);
+ Assert.Equal(Vector3.Zero, body.Omega);
+ }
+
+ [Fact]
+ public void calc_acceleration_no_gravity_flag_zeros_acceleration()
+ {
+ var body = new PhysicsBody
+ {
+ State = PhysicsStateFlags.None, // no Gravity flag
+ TransientState = TransientStateFlags.Active,
+ };
+ body.Acceleration = new Vector3(0f, 0f, -9.8f);
+ body.calc_acceleration();
+
+ Assert.Equal(Vector3.Zero, body.Acceleration);
+ }
+
+ [Fact]
+ public void calc_acceleration_sledding_airborne_still_applies_gravity()
+ {
+ // Sledding but not grounded — gravity still applies
+ var body = new PhysicsBody
+ {
+ State = PhysicsStateFlags.Gravity | PhysicsStateFlags.Sledding,
+ TransientState = TransientStateFlags.Active,
+ };
+ body.calc_acceleration();
+
+ Assert.Equal(-9.8f, body.Acceleration.Z, precision: 6);
+ }
+
+ // ════════════════════════════════════════════════════════════════════
+ // UpdatePhysicsInternal — Euler integration
+ // ════════════════════════════════════════════════════════════════════
+
+ [Fact]
+ public void UpdatePhysicsInternal_integrates_position_correctly_one_step()
+ {
+ // Analytical: x(t) = x0 + v0*t + 0.5*a*t²
+ // With v0=(1,0,0), a=(0,0,-9.8), dt=0.1
+ // x = 0.1
+ // z = 0.5 * (-9.8) * 0.01 = -0.049
+ var body = MakeAirborne();
+ body.Velocity = new Vector3(1f, 0f, 0f);
+ body.Acceleration = new Vector3(0f, 0f, -9.8f);
+
+ body.UpdatePhysicsInternal(0.1f);
+
+ Assert.Equal(0.1f, body.Position.X, precision: 5);
+ Assert.Equal(0f, body.Position.Y, precision: 5);
+ // 0.5 * (-9.8) * 0.01 = -0.049
+ Assert.Equal(-0.049f, body.Position.Z, precision: 4);
+ }
+
+ [Fact]
+ public void UpdatePhysicsInternal_velocity_updated_by_acceleration_times_dt()
+ {
+ var body = MakeAirborne();
+ body.Velocity = new Vector3(0f, 0f, 0f);
+ body.Acceleration = new Vector3(0f, 0f, -9.8f);
+
+ body.UpdatePhysicsInternal(0.5f);
+
+ // velocity += accel * dt = (0, 0, -9.8 * 0.5) = (0, 0, -4.9)
+ Assert.Equal(0f, body.Velocity.X, precision: 5);
+ Assert.Equal(0f, body.Velocity.Y, precision: 5);
+ Assert.Equal(-4.9f, body.Velocity.Z, precision: 4);
+ }
+
+ [Fact]
+ public void UpdatePhysicsInternal_multiple_frames_accumulates_correctly()
+ {
+ // Free-fall from rest under gravity for N frames of dt each.
+ // Analytical z(t) = 0.5 * g * t² where g = -9.8
+ // After 10 frames of 0.1 s each (total t=1.0 s):
+ // z = 0.5 * (-9.8) * 1.0 = -4.9
+ // The Euler integrator accumulates small truncation error, so allow 2% tolerance.
+ var body = MakeAirborne();
+ body.Velocity = Vector3.Zero;
+ body.Acceleration = new Vector3(0f, 0f, PhysicsBody.Gravity);
+
+ const int frames = 10;
+ const float dt = 0.1f;
+ for (int i = 0; i < frames; i++)
+ body.UpdatePhysicsInternal(dt);
+
+ float expected = 0.5f * PhysicsBody.Gravity * (frames * dt) * (frames * dt);
+ Assert.True(MathF.Abs(body.Position.Z - expected) < 0.15f,
+ $"Expected z ≈ {expected:F4}, got {body.Position.Z:F4}");
+ }
+
+ [Fact]
+ public void UpdatePhysicsInternal_zero_velocity_clears_active_flag_when_grounded()
+ {
+ var body = MakeGrounded();
+ body.Velocity = Vector3.Zero;
+ body.TransientState |= TransientStateFlags.Active;
+
+ body.UpdatePhysicsInternal(0.1f);
+
+ Assert.False(body.IsActive);
+ }
+
+ // ════════════════════════════════════════════════════════════════════
+ // set_velocity — velocity clamping
+ // ════════════════════════════════════════════════════════════════════
+
+ [Fact]
+ public void set_velocity_below_max_stores_velocity_unchanged()
+ {
+ var body = new PhysicsBody();
+ var v = new Vector3(10f, 5f, 2f);
+ body.set_velocity(v);
+
+ Assert.Equal(v, body.Velocity);
+ }
+
+ [Fact]
+ public void set_velocity_above_max_clamps_to_MaxVelocity_magnitude()
+ {
+ var body = new PhysicsBody();
+ // velocity with magnitude > 50
+ var v = new Vector3(100f, 0f, 0f);
+ body.set_velocity(v);
+
+ Assert.True(body.Velocity.Length() <= PhysicsBody.MaxVelocity + 1e-4f,
+ $"Velocity magnitude {body.Velocity.Length()} exceeds MaxVelocity {PhysicsBody.MaxVelocity}");
+ Assert.Equal(PhysicsBody.MaxVelocity, body.Velocity.Length(), precision: 4);
+ }
+
+ [Fact]
+ public void set_velocity_diagonal_above_max_clamps_and_preserves_direction()
+ {
+ var body = new PhysicsBody();
+ var dir = Vector3.Normalize(new Vector3(3f, 4f, 0f)); // unit vector
+ var v = dir * 80f; // magnitude = 80 > 50
+ body.set_velocity(v);
+
+ Assert.Equal(PhysicsBody.MaxVelocity, body.Velocity.Length(), precision: 3);
+ // Direction should be preserved
+ var resultDir = Vector3.Normalize(body.Velocity);
+ Assert.Equal(dir.X, resultDir.X, precision: 4);
+ Assert.Equal(dir.Y, resultDir.Y, precision: 4);
+ }
+
+ [Fact]
+ public void set_velocity_sets_active_flag()
+ {
+ var body = new PhysicsBody();
+ body.TransientState = TransientStateFlags.None;
+ body.set_velocity(new Vector3(1f, 0f, 0f));
+
+ Assert.True(body.IsActive);
+ }
+
+ [Fact]
+ public void set_velocity_exactly_at_max_is_not_clamped()
+ {
+ var body = new PhysicsBody();
+ var v = new Vector3(PhysicsBody.MaxVelocity, 0f, 0f);
+ body.set_velocity(v);
+
+ Assert.Equal(v.X, body.Velocity.X, precision: 4);
+ Assert.Equal(0f, body.Velocity.Y, precision: 4);
+ Assert.Equal(0f, body.Velocity.Z, precision: 4);
+ }
+
+ // ════════════════════════════════════════════════════════════════════
+ // set_local_velocity — body→world transform
+ // ════════════════════════════════════════════════════════════════════
+
+ [Fact]
+ public void set_local_velocity_identity_orientation_passes_through()
+ {
+ var body = new PhysicsBody { Orientation = Quaternion.Identity };
+ body.set_local_velocity(new Vector3(1f, 0f, 0f));
+
+ Assert.Equal(1f, body.Velocity.X, precision: 5);
+ Assert.Equal(0f, body.Velocity.Y, precision: 5);
+ Assert.Equal(0f, body.Velocity.Z, precision: 5);
+ }
+
+ [Fact]
+ public void set_local_velocity_90_degree_yaw_rotates_forward_to_right()
+ {
+ // A 90° CCW rotation around Z maps +X in local space to +Y in world space.
+ var body = new PhysicsBody
+ {
+ Orientation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI / 2f)
+ };
+ body.set_local_velocity(new Vector3(1f, 0f, 0f));
+
+ // After 90° yaw: local +X becomes world +Y (approximately)
+ Assert.True(MathF.Abs(body.Velocity.X) < 1e-4f, $"Expected Vx≈0, got {body.Velocity.X}");
+ Assert.True(MathF.Abs(body.Velocity.Y - 1f) < 1e-4f, $"Expected Vy≈1, got {body.Velocity.Y}");
+ Assert.True(MathF.Abs(body.Velocity.Z) < 1e-4f, $"Expected Vz≈0, got {body.Velocity.Z}");
+ }
+
+ [Fact]
+ public void set_local_velocity_180_degree_yaw_reverses_horizontal_forward()
+ {
+ var body = new PhysicsBody
+ {
+ Orientation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI)
+ };
+ body.set_local_velocity(new Vector3(1f, 0f, 0f));
+
+ Assert.True(MathF.Abs(body.Velocity.X + 1f) < 1e-4f, $"Expected Vx≈-1, got {body.Velocity.X}");
+ Assert.True(MathF.Abs(body.Velocity.Y) < 1e-4f, $"Expected Vy≈0, got {body.Velocity.Y}");
+ }
+
+ [Fact]
+ public void set_local_velocity_magnitude_preserved_after_rotation()
+ {
+ var body = new PhysicsBody
+ {
+ Orientation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 1.23f)
+ };
+ var localVel = new Vector3(3f, 4f, 0f);
+ body.set_local_velocity(localVel);
+
+ Assert.Equal(localVel.Length(), body.Velocity.Length(), precision: 4);
+ }
+
+ // ════════════════════════════════════════════════════════════════════
+ // set_on_walkable
+ // ════════════════════════════════════════════════════════════════════
+
+ [Fact]
+ public void set_on_walkable_true_sets_OnWalkable_flag()
+ {
+ var body = MakeAirborne();
+ body.set_on_walkable(true);
+
+ Assert.True(body.OnWalkable);
+ }
+
+ [Fact]
+ public void set_on_walkable_false_clears_OnWalkable_flag()
+ {
+ var body = MakeGrounded();
+ body.set_on_walkable(false);
+
+ Assert.False(body.OnWalkable);
+ }
+
+ [Fact]
+ public void set_on_walkable_true_also_calls_calc_acceleration_zeroing_accel()
+ {
+ // When Contact + OnWalkable (non-sledding): acceleration should be zeroed.
+ var body = new PhysicsBody
+ {
+ State = PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions,
+ TransientState = TransientStateFlags.Contact,
+ Acceleration = new Vector3(0f, 0f, -9.8f),
+ };
+ body.set_on_walkable(true);
+
+ Assert.Equal(Vector3.Zero, body.Acceleration);
+ }
+
+ [Fact]
+ public void set_on_walkable_false_allows_gravity_to_apply()
+ {
+ var body = MakeGrounded();
+ body.set_on_walkable(false);
+
+ // After clearing OnWalkable, calc_acceleration should apply gravity.
+ Assert.Equal(-9.8f, body.Acceleration.Z, precision: 6);
+ }
+
+ // ════════════════════════════════════════════════════════════════════
+ // calc_friction
+ // ════════════════════════════════════════════════════════════════════
+
+ [Fact]
+ public void calc_friction_not_on_walkable_does_nothing()
+ {
+ var body = MakeAirborne();
+ body.Velocity = new Vector3(5f, 0f, 0f);
+ var before = body.Velocity;
+ body.calc_friction(0.1f, body.Velocity.LengthSquared());
+
+ Assert.Equal(before, body.Velocity);
+ }
+
+ [Fact]
+ public void calc_friction_velocity_parallel_to_ground_reduces_magnitude()
+ {
+ // Ground normal = +Z, velocity is horizontal (no inward component),
+ // but if we tilt slightly downward (dot < 0) friction fires.
+ var body = MakeGrounded();
+ body.GroundNormal = Vector3.UnitZ;
+ // Give a small downward Z component so dot(normal, vel) < 0
+ body.Velocity = new Vector3(5f, 0f, -0.1f);
+ float mag2 = body.Velocity.LengthSquared();
+
+ body.calc_friction(0.1f, mag2);
+
+ // Speed should be reduced by friction
+ Assert.True(body.Velocity.Length() < new Vector3(5f, 0f, 0f).Length(),
+ "Friction should reduce velocity magnitude");
+ }
+
+ [Fact]
+ public void calc_friction_velocity_moving_away_from_normal_no_change()
+ {
+ // dot(GroundNormal=(0,0,1), velocity=(5,0,1)) = 1 > 0 → no friction
+ var body = MakeGrounded();
+ body.GroundNormal = Vector3.UnitZ;
+ body.Velocity = new Vector3(5f, 0f, 1f); // moving up = away from ground
+ var before = body.Velocity;
+ float mag2 = body.Velocity.LengthSquared();
+
+ body.calc_friction(0.1f, mag2);
+
+ Assert.Equal(before, body.Velocity);
+ }
+
+ [Fact]
+ public void calc_friction_zero_friction_coefficient_no_reduction()
+ {
+ var body = MakeGrounded();
+ body.GroundNormal = Vector3.UnitZ;
+ body.Velocity = new Vector3(5f, 0f, -0.01f);
+ body.Friction = 0f; // frictionless surface
+ float mag2 = body.Velocity.LengthSquared();
+
+ body.calc_friction(0.1f, mag2);
+
+ // After removing normal component, velocity magnitude should be ≈ 5 (horizontal)
+ // With friction=0, pow(1-0, dt)=1, so velocity unchanged beyond normal removal
+ Assert.True(body.Velocity.Length() > 4.9f,
+ $"Zero friction: speed {body.Velocity.Length()} should stay near 5");
+ }
+
+ [Fact]
+ public void calc_friction_removes_normal_component_from_velocity()
+ {
+ // Velocity = (1, 0, -1), GroundNormal = (0, 0, 1)
+ // dot = -1 → velocity -= (-1) * (0,0,1) = velocity + (0,0,1) → (1, 0, 0)
+ var body = MakeGrounded();
+ body.GroundNormal = Vector3.UnitZ;
+ body.Friction = 0f; // no friction to isolate normal-removal behavior
+ body.Velocity = new Vector3(1f, 0f, -1f);
+ float mag2 = body.Velocity.LengthSquared();
+
+ body.calc_friction(1.0f, mag2);
+
+ // After normal removal the Z component should be zero (or very small).
+ Assert.True(MathF.Abs(body.Velocity.Z) < 1e-4f,
+ $"Normal component should be removed; Vz = {body.Velocity.Z}");
+ Assert.Equal(1f, body.Velocity.X, precision: 4);
+ }
+
+ // ════════════════════════════════════════════════════════════════════
+ // update_object — per-frame driver
+ // ════════════════════════════════════════════════════════════════════
+
+ [Fact]
+ public void update_object_dt_below_min_quantum_does_not_advance()
+ {
+ var body = MakeAirborne();
+ body.Velocity = new Vector3(1f, 0f, 0f);
+ body.Acceleration = Vector3.Zero;
+ body.LastUpdateTime = 0.0;
+
+ // Advance by less than MinQuantum — should be a no-op
+ body.update_object(PhysicsBody.MinQuantum * 0.5);
+
+ Assert.Equal(Vector3.Zero, body.Position);
+ }
+
+ [Fact]
+ public void update_object_dt_above_huge_quantum_consumes_time_without_simulating()
+ {
+ var body = MakeAirborne();
+ body.Velocity = new Vector3(1f, 0f, 0f);
+ body.Acceleration = Vector3.Zero;
+ body.LastUpdateTime = 0.0;
+
+ body.update_object(PhysicsBody.HugeQuantum + 0.5);
+
+ // Time consumed but no physics step — position unchanged
+ Assert.Equal(Vector3.Zero, body.Position);
+ Assert.Equal(PhysicsBody.HugeQuantum + 0.5, body.LastUpdateTime, precision: 10);
+ }
+
+ [Fact]
+ public void update_object_advances_position_over_valid_dt()
+ {
+ var body = MakeAirborne();
+ // No friction or gravity interference — just pure horizontal velocity
+ body.State = PhysicsStateFlags.None; // no gravity
+ body.Velocity = new Vector3(10f, 0f, 0f);
+ body.LastUpdateTime = 0.0;
+
+ double dt = 0.1;
+ body.update_object(dt);
+
+ // x ≈ 10 * 0.1 = 1.0 (ignoring sub-step rounding)
+ Assert.True(body.Position.X > 0f, "Position should have advanced");
+ }
+
+ [Fact]
+ public void update_object_updates_LastUpdateTime()
+ {
+ var body = MakeAirborne();
+ body.LastUpdateTime = 0.0;
+ body.State = PhysicsStateFlags.None;
+
+ double t = 0.05;
+ body.update_object(t);
+
+ Assert.Equal(t, body.LastUpdateTime, precision: 10);
+ }
+
+ [Fact]
+ public void update_object_gravity_free_fall_accumulates_downward_velocity()
+ {
+ var body = MakeAirborne();
+ // Let it fall for one valid quantum
+ body.LastUpdateTime = 0.0;
+ double dt = PhysicsBody.MinQuantum * 2; // > MinQuantum but < HugeQuantum
+
+ body.update_object(dt);
+
+ // After one step velocity should be negative Z
+ Assert.True(body.Velocity.Z < 0f,
+ $"Gravity should produce negative Z velocity; got {body.Velocity.Z}");
+ }
+}
diff --git a/tools/ghidra_project/acclient.gpr b/tools/ghidra_project/acclient.gpr
new file mode 100644
index 0000000..e69de29
diff --git a/tools/ghidra_project/acclient.rep/idata/00/00000000.prp b/tools/ghidra_project/acclient.rep/idata/00/00000000.prp
new file mode 100644
index 0000000..217768e
--- /dev/null
+++ b/tools/ghidra_project/acclient.rep/idata/00/00000000.prp
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/ghidra_project/acclient.rep/idata/00/~00000000.db/db.3.gbf b/tools/ghidra_project/acclient.rep/idata/00/~00000000.db/db.3.gbf
new file mode 100644
index 0000000..33c81fc
Binary files /dev/null and b/tools/ghidra_project/acclient.rep/idata/00/~00000000.db/db.3.gbf differ
diff --git a/tools/ghidra_project/acclient.rep/idata/00/~00000000.db/db.4.gbf b/tools/ghidra_project/acclient.rep/idata/00/~00000000.db/db.4.gbf
new file mode 100644
index 0000000..245d38d
Binary files /dev/null and b/tools/ghidra_project/acclient.rep/idata/00/~00000000.db/db.4.gbf differ
diff --git a/tools/ghidra_project/acclient.rep/idata/~index.bak b/tools/ghidra_project/acclient.rep/idata/~index.bak
new file mode 100644
index 0000000..4e4d379
--- /dev/null
+++ b/tools/ghidra_project/acclient.rep/idata/~index.bak
@@ -0,0 +1,5 @@
+VERSION=1
+/
+ 00000000:acclient.exe:c0a8162cd88181512443538400
+NEXT-ID:1
+MD5:d41d8cd98f00b204e9800998ecf8427e
diff --git a/tools/ghidra_project/acclient.rep/idata/~index.dat b/tools/ghidra_project/acclient.rep/idata/~index.dat
new file mode 100644
index 0000000..4e4d379
--- /dev/null
+++ b/tools/ghidra_project/acclient.rep/idata/~index.dat
@@ -0,0 +1,5 @@
+VERSION=1
+/
+ 00000000:acclient.exe:c0a8162cd88181512443538400
+NEXT-ID:1
+MD5:d41d8cd98f00b204e9800998ecf8427e
diff --git a/tools/ghidra_project/acclient.rep/project.prp b/tools/ghidra_project/acclient.rep/project.prp
new file mode 100644
index 0000000..f18fd43
--- /dev/null
+++ b/tools/ghidra_project/acclient.rep/project.prp
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/tools/ghidra_project/acclient.rep/user/~index.dat b/tools/ghidra_project/acclient.rep/user/~index.dat
new file mode 100644
index 0000000..b1e697f
--- /dev/null
+++ b/tools/ghidra_project/acclient.rep/user/~index.dat
@@ -0,0 +1,4 @@
+VERSION=1
+/
+NEXT-ID:0
+MD5:d41d8cd98f00b204e9800998ecf8427e
diff --git a/tools/ghidra_project/acclient.rep/versioned/~index.bak b/tools/ghidra_project/acclient.rep/versioned/~index.bak
new file mode 100644
index 0000000..b1e697f
--- /dev/null
+++ b/tools/ghidra_project/acclient.rep/versioned/~index.bak
@@ -0,0 +1,4 @@
+VERSION=1
+/
+NEXT-ID:0
+MD5:d41d8cd98f00b204e9800998ecf8427e
diff --git a/tools/ghidra_project/acclient.rep/versioned/~index.dat b/tools/ghidra_project/acclient.rep/versioned/~index.dat
new file mode 100644
index 0000000..b1e697f
--- /dev/null
+++ b/tools/ghidra_project/acclient.rep/versioned/~index.dat
@@ -0,0 +1,4 @@
+VERSION=1
+/
+NEXT-ID:0
+MD5:d41d8cd98f00b204e9800998ecf8427e
diff --git a/tools/ghidra_project/acclient_full/acclient_full.rep/idata/00/~00000000.db/db.1.gbf b/tools/ghidra_project/acclient_full/acclient_full.rep/idata/00/~00000000.db/db.1.gbf
new file mode 100644
index 0000000..67f6d08
Binary files /dev/null and b/tools/ghidra_project/acclient_full/acclient_full.rep/idata/00/~00000000.db/db.1.gbf differ
diff --git a/tools/ghidra_project/acclient_full/acclient_full.rep/idata/00/~00000000.db/db.2.gbf b/tools/ghidra_project/acclient_full/acclient_full.rep/idata/00/~00000000.db/db.2.gbf
new file mode 100644
index 0000000..4d927ec
Binary files /dev/null and b/tools/ghidra_project/acclient_full/acclient_full.rep/idata/00/~00000000.db/db.2.gbf differ
diff --git a/tools/ghidra_project/acclient_full/acclient_full.rep/idata/~index.dat b/tools/ghidra_project/acclient_full/acclient_full.rep/idata/~index.dat
new file mode 100644
index 0000000..41c0ff2
--- /dev/null
+++ b/tools/ghidra_project/acclient_full/acclient_full.rep/idata/~index.dat
@@ -0,0 +1,5 @@
+VERSION=1
+/
+ 00000000:acclient.exe:c0a8162ccd0182120605721300
+NEXT-ID:1
+MD5:d41d8cd98f00b204e9800998ecf8427e
diff --git a/tools/ghidra_project/acclient_full2/acclient_full2.gpr b/tools/ghidra_project/acclient_full2/acclient_full2.gpr
new file mode 100644
index 0000000..e69de29
diff --git a/tools/ghidra_project/acclient_full2/acclient_full2.rep/idata/00/00000000.prp b/tools/ghidra_project/acclient_full2/acclient_full2.rep/idata/00/00000000.prp
new file mode 100644
index 0000000..557f448
--- /dev/null
+++ b/tools/ghidra_project/acclient_full2/acclient_full2.rep/idata/00/00000000.prp
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/ghidra_project/acclient_full2/acclient_full2.rep/idata/00/~00000000.db/db.1.gbf b/tools/ghidra_project/acclient_full2/acclient_full2.rep/idata/00/~00000000.db/db.1.gbf
new file mode 100644
index 0000000..7ad0766
Binary files /dev/null and b/tools/ghidra_project/acclient_full2/acclient_full2.rep/idata/00/~00000000.db/db.1.gbf differ
diff --git a/tools/ghidra_project/acclient_full2/acclient_full2.rep/idata/00/~00000000.db/db.2.gbf b/tools/ghidra_project/acclient_full2/acclient_full2.rep/idata/00/~00000000.db/db.2.gbf
new file mode 100644
index 0000000..ab95d21
Binary files /dev/null and b/tools/ghidra_project/acclient_full2/acclient_full2.rep/idata/00/~00000000.db/db.2.gbf differ
diff --git a/tools/ghidra_project/acclient_full2/acclient_full2.rep/idata/~index.bak b/tools/ghidra_project/acclient_full2/acclient_full2.rep/idata/~index.bak
new file mode 100644
index 0000000..b1e697f
--- /dev/null
+++ b/tools/ghidra_project/acclient_full2/acclient_full2.rep/idata/~index.bak
@@ -0,0 +1,4 @@
+VERSION=1
+/
+NEXT-ID:0
+MD5:d41d8cd98f00b204e9800998ecf8427e
diff --git a/tools/ghidra_project/acclient_full2/acclient_full2.rep/idata/~index.dat b/tools/ghidra_project/acclient_full2/acclient_full2.rep/idata/~index.dat
new file mode 100644
index 0000000..480ca80
--- /dev/null
+++ b/tools/ghidra_project/acclient_full2/acclient_full2.rep/idata/~index.dat
@@ -0,0 +1,5 @@
+VERSION=1
+/
+ 00000000:acclient.exe:c0a8162d356182196155907400
+NEXT-ID:1
+MD5:d41d8cd98f00b204e9800998ecf8427e
diff --git a/tools/ghidra_project/acclient_full2/acclient_full2.rep/idata/~journal.bak b/tools/ghidra_project/acclient_full2/acclient_full2.rep/idata/~journal.bak
new file mode 100644
index 0000000..7d0b5b2
--- /dev/null
+++ b/tools/ghidra_project/acclient_full2/acclient_full2.rep/idata/~journal.bak
@@ -0,0 +1,2 @@
+IADD:00000000:/acclient.exe
+IDSET:/acclient.exe:c0a8162d356182196155907400
diff --git a/tools/ghidra_project/acclient_full2/acclient_full2.rep/project.prp b/tools/ghidra_project/acclient_full2/acclient_full2.rep/project.prp
new file mode 100644
index 0000000..f18fd43
--- /dev/null
+++ b/tools/ghidra_project/acclient_full2/acclient_full2.rep/project.prp
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/tools/ghidra_project/acclient_full2/acclient_full2.rep/user/~index.dat b/tools/ghidra_project/acclient_full2/acclient_full2.rep/user/~index.dat
new file mode 100644
index 0000000..b1e697f
--- /dev/null
+++ b/tools/ghidra_project/acclient_full2/acclient_full2.rep/user/~index.dat
@@ -0,0 +1,4 @@
+VERSION=1
+/
+NEXT-ID:0
+MD5:d41d8cd98f00b204e9800998ecf8427e
diff --git a/tools/ghidra_project/acclient_full2/acclient_full2.rep/versioned/~index.bak b/tools/ghidra_project/acclient_full2/acclient_full2.rep/versioned/~index.bak
new file mode 100644
index 0000000..b1e697f
--- /dev/null
+++ b/tools/ghidra_project/acclient_full2/acclient_full2.rep/versioned/~index.bak
@@ -0,0 +1,4 @@
+VERSION=1
+/
+NEXT-ID:0
+MD5:d41d8cd98f00b204e9800998ecf8427e
diff --git a/tools/ghidra_project/acclient_full2/acclient_full2.rep/versioned/~index.dat b/tools/ghidra_project/acclient_full2/acclient_full2.rep/versioned/~index.dat
new file mode 100644
index 0000000..b1e697f
--- /dev/null
+++ b/tools/ghidra_project/acclient_full2/acclient_full2.rep/versioned/~index.dat
@@ -0,0 +1,4 @@
+VERSION=1
+/
+NEXT-ID:0
+MD5:d41d8cd98f00b204e9800998ecf8427e
diff --git a/tools/ghidra_project/acclient_pyghidra/acclient_pyghidra.gpr b/tools/ghidra_project/acclient_pyghidra/acclient_pyghidra.gpr
new file mode 100644
index 0000000..e69de29
diff --git a/tools/ghidra_project/acclient_pyghidra/acclient_pyghidra.rep/idata/00/00000000.prp b/tools/ghidra_project/acclient_pyghidra/acclient_pyghidra.rep/idata/00/00000000.prp
new file mode 100644
index 0000000..12c9567
--- /dev/null
+++ b/tools/ghidra_project/acclient_pyghidra/acclient_pyghidra.rep/idata/00/00000000.prp
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/ghidra_project/acclient_pyghidra/acclient_pyghidra.rep/idata/00/~00000000.db/db.4.gbf b/tools/ghidra_project/acclient_pyghidra/acclient_pyghidra.rep/idata/00/~00000000.db/db.4.gbf
new file mode 100644
index 0000000..19db7af
Binary files /dev/null and b/tools/ghidra_project/acclient_pyghidra/acclient_pyghidra.rep/idata/00/~00000000.db/db.4.gbf differ
diff --git a/tools/ghidra_project/acclient_pyghidra/acclient_pyghidra.rep/idata/00/~00000000.db/db.5.gbf b/tools/ghidra_project/acclient_pyghidra/acclient_pyghidra.rep/idata/00/~00000000.db/db.5.gbf
new file mode 100644
index 0000000..7434f9e
Binary files /dev/null and b/tools/ghidra_project/acclient_pyghidra/acclient_pyghidra.rep/idata/00/~00000000.db/db.5.gbf differ
diff --git a/tools/ghidra_project/acclient_pyghidra/acclient_pyghidra.rep/idata/~index.bak b/tools/ghidra_project/acclient_pyghidra/acclient_pyghidra.rep/idata/~index.bak
new file mode 100644
index 0000000..96f90c8
--- /dev/null
+++ b/tools/ghidra_project/acclient_pyghidra/acclient_pyghidra.rep/idata/~index.bak
@@ -0,0 +1,5 @@
+VERSION=1
+/
+ 00000000:acclient.exe:c0a8162f0d1181714693391400
+NEXT-ID:1
+MD5:d41d8cd98f00b204e9800998ecf8427e
diff --git a/tools/ghidra_project/acclient_pyghidra/acclient_pyghidra.rep/idata/~index.dat b/tools/ghidra_project/acclient_pyghidra/acclient_pyghidra.rep/idata/~index.dat
new file mode 100644
index 0000000..96f90c8
--- /dev/null
+++ b/tools/ghidra_project/acclient_pyghidra/acclient_pyghidra.rep/idata/~index.dat
@@ -0,0 +1,5 @@
+VERSION=1
+/
+ 00000000:acclient.exe:c0a8162f0d1181714693391400
+NEXT-ID:1
+MD5:d41d8cd98f00b204e9800998ecf8427e
diff --git a/tools/ghidra_project/acclient_pyghidra/acclient_pyghidra.rep/project.prp b/tools/ghidra_project/acclient_pyghidra/acclient_pyghidra.rep/project.prp
new file mode 100644
index 0000000..f18fd43
--- /dev/null
+++ b/tools/ghidra_project/acclient_pyghidra/acclient_pyghidra.rep/project.prp
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/tools/ghidra_project/acclient_pyghidra/acclient_pyghidra.rep/user/~index.dat b/tools/ghidra_project/acclient_pyghidra/acclient_pyghidra.rep/user/~index.dat
new file mode 100644
index 0000000..b1e697f
--- /dev/null
+++ b/tools/ghidra_project/acclient_pyghidra/acclient_pyghidra.rep/user/~index.dat
@@ -0,0 +1,4 @@
+VERSION=1
+/
+NEXT-ID:0
+MD5:d41d8cd98f00b204e9800998ecf8427e
diff --git a/tools/ghidra_project/acclient_pyghidra/acclient_pyghidra.rep/versioned/~index.bak b/tools/ghidra_project/acclient_pyghidra/acclient_pyghidra.rep/versioned/~index.bak
new file mode 100644
index 0000000..b1e697f
--- /dev/null
+++ b/tools/ghidra_project/acclient_pyghidra/acclient_pyghidra.rep/versioned/~index.bak
@@ -0,0 +1,4 @@
+VERSION=1
+/
+NEXT-ID:0
+MD5:d41d8cd98f00b204e9800998ecf8427e
diff --git a/tools/ghidra_project/acclient_pyghidra/acclient_pyghidra.rep/versioned/~index.dat b/tools/ghidra_project/acclient_pyghidra/acclient_pyghidra.rep/versioned/~index.dat
new file mode 100644
index 0000000..b1e697f
--- /dev/null
+++ b/tools/ghidra_project/acclient_pyghidra/acclient_pyghidra.rep/versioned/~index.dat
@@ -0,0 +1,4 @@
+VERSION=1
+/
+NEXT-ID:0
+MD5:d41d8cd98f00b204e9800998ecf8427e