(Experimental) PID sync, axle-based remote steering, cluster sync for coupled vehicles and other things#188
Closed
spectrachrome wants to merge 20 commits into
Closed
Conversation
add a dedicated sync path for coupled trailers that uses angular correction relative to the truck's rotation, with a gentle linear nudge to keep the trailer positioned correctly. previously, trailers used the same full PD sync as regular vehicles, which caused physics explosions and hitch detachments. the new approach lets the coupler constraint handle most positioning while only correcting the hitch angle and providing minimal position assistance. key changes: - track trailer-truck coupling relationships via coupled_to table - separate sync path for coupled trailers (angle + gentle nudge) - sync parking brake and inputs to trailers (fixes stuck landing gear) - prevent fifth wheel hitch from locking remote vehicles via advancedCouplerControl guards and force-detach on spawn - filter self-coupling and duplicate coupler events - allow linear forces even when angular correction is skipped - add nil safety for kiss_transforms commands during vehicle loading - add debug visualization spheres and detailed sync logging
the local was declared after onExtensionLoaded, so table.insert hit nil and killed initialization for every hitched vehicle. this broke electrics handlers for engine state, lights, horn, abs, and more. also add mass-relative scaling for coupled trailer forces so heavier trailers get proportionally more correction and lighter ones don't get pushed into a v-shape at the hitch.
Replace velocity-matching trailer sync with a constraint-preserving approach: GE side computes expected trailer CG from hitch geometry (fifthwheel=yaw-only, ball/pintle=full quaternion), vehicle side does threshold-based hard snap only. Removes tow_boost in favor of per-hitch-type drift thresholds. Transmits coupler_tag through the server so remote clients can classify hitch type.
Contributor
|
Cargo.lock and claude is not needed on the repo, make sure to only push what's necessary ;) |
…stics Remove the constraint-preserving coupled sync (expected position + snap) that fought BeamNG's coupler physics and caused accumulating jackknife drift. Coupled trailers now use the same PD sync as all vehicles — the physical coupler constraint naturally overrides PD forces. Defer coupler attachment on remote by 1-2s to allow vehicles to fully initialize before firing attachCoupler. Buffer CouplerAttached network events that arrive before vehicles are spawned (id_map miss) and retry. Additional changes: - Reactivate trailer advancedCouplerControl on coupling (landing gear) - Attach coupler from both trailer and truck sides - Rotate node offsets to world-space for pre-attach positioning - Increase coupler search radius to 1m for remote attach - Add diagnostic logging for coupler events - Disable per-frame debug spam (debug/debug_log = false) - Remove electrics send logging
Author
|
Sorry, of course! I'll ditch the CLAUDE.md and lock file in the next commits and generally clean this up. Old habits :) Also going to look into the many merge conflicts. |
Coupled trucks and trailers now rely on the physics coupler constraint as the sole source of truth for their relative pose. No path can teleport a coupled vehicle anymore, and the truck's angular PD no longer whip-cracks the hitch. kisstransform / kiss_transforms: - Route coupled trucks to update(dt, true, ang_scale) via new coupled_trucks inverse index in vehiclemanager. - update(dt, skip_rude, ang_scale): skip try_rude entirely for coupled trucks (the 6m teleport was yanking the whole rig under PD lag). Rewrite the angular_force formula to drop the proportional angle-error term (angle_delta_euler * ang_force) — that term was the jackknife driver because it slammed a rigid-body rotation impulse into the chassis every tick, whip-cracking the hitch faster than the trailer pivot could absorb. Keep only angular velocity matching + damping, which gives smooth curve tracking without the per-tick snap. - update_coupled: drop the setPositionNoPhysicsReset snap entirely. The coupler constraint now holds the trailer. vehiclemanager (attach_coupler / detach_coupler): - Guard early-return on ownership of EITHER side of the pair (was only checking obj_a), so inbound rebroadcasts can't yank a vehicle we own. - Make attach_coupler idempotent: skip snap + reattach when pair is already in coupled_to, so duplicate messages become no-ops. - Cache hitch offsets in vehicle-local space (inverse_rot * getNodePosition), not the world-rotated value getNodePosition returns directly. update_coupled's current_rot * offset math was double-rotating whenever the vehicle wasn't aligned with world +Y at attach time, placing the expected-position marker far from the real trailer. - Add coupled_trucks inverse index (truck_id → trailer_id), cleared on detach, vehicle destroy, and mission load. - Add vehicle_masses table + set_vehicle_mass setter. Compute ang_scale = clamp(truck_mass / (truck_mass + trailer_mass), 0.05, 0.5) at attach time and store it on the coupling. Not currently consulted vehicle-side (the rewritten angular formula made it redundant) but plumbed end-to-end for future use. kiss_vehicle: - onExtensionLoaded reports the computed total jbeam mass to GE via vehiclemanager.set_vehicle_mass so the ang_scale computation at attach time has real data.
Coupled ghost trucks with bad ping or physics divergence accumulate
heading drift that projects into lateral position drift. Per-tick
angular PD can't correct this: any proportional angle term (even 1/50
of the non-coupled gain) whip-cracks the hitch node because rigid-body
chassis rotation generates tangential velocity at the hitch the trailer
pivot can't absorb smoothly. Without correction the rig just drifts.
Move the drift-recovery decision to GE-side and apply it atomically to
the whole cluster as a rigid-body snap rather than a per-vehicle
teleport. Per-vehicle was already disabled for coupled trucks because
it rips the hitch apart.
kisstransform.lua:
- Walk coupled_to / coupled_trucks graph from any cluster member via
get_cluster_members; supports road trains transparently.
- In the update loop, for any vehicle in a cluster whose world drift
exceeds CLUSTER_TELEPORT_THRESHOLD (8m), compute the truck's pose
delta (position + rotation) and apply a rigid-body rotation of the
whole cluster around the truck's current position onto the truck's
target position/rotation:
new_pos = pivot_target + rot_delta * (member_pos - pivot_current)
new_rot = rot_delta * member_rot
This preserves relative pose between truck and trailer so the coupler
constraint is not disturbed, while atomically correcting the heading
error that the per-tick PD could not touch.
- Rotation snap was critical: a translation-only snap left the rig
facing the wrong direction, so lateral drift immediately re-
accumulated and teleports fired continuously. With rotation, each
snap is a true reset and the next teleport is rare.
- Per-cluster cooldown (1.5s, keyed by min member id) prevents snap
thrashing during rapid successive drift bursts.
- Solo non-coupled vehicles unchanged — still use vehicle-side try_rude
at 6m.
kiss_transforms.lua:
- Coupled-truck branch of update() remains pure velocity matching +
damping. Empirically confirmed any proportional angle term, even at
1/50 gain with a magnitude cap, reintroduces jackknife because the
mechanism (rigid-body chassis rotation) is the problem regardless of
magnitude.
advancedCouplerControl trailers (tilt beds, most modern rigs) left their landing gear extended after ball-hitch coupling unless the user manually reset the vehicle in place. Root cause: tryAttachGroupImpulse is called from the electrics handler on the ghost the moment the owner flips the notAttached electric, but the C++ coupler latch is asynchronous. If the impulse call races ahead of the latch, the controller's state machine refuses to run its connect sequence (including leg retract) because it doesn't think it's attached yet, and subsequent electrics packets carry the same value so the handler never re-fires. A manual reset re-latched and re-triggered the path. Schedule up to 5 re-invocations of tryAttachGroupImpulse at 100ms intervals after the initial call (~500ms retry window, comfortably past the ball-hitch search poll period). On detach, cancel any pending retries for that controller so a quick connect→disconnect sequence doesn't leave a stale job re-latching against user intent. Also add a best-effort is_controller_attached probe that sniffs likely field/method names (notAttached, attached, groupAttached, isAttached()) and drops the pending entry as soon as the controller reports attached, to minimize redundant impulse calls in the common case. If the field sniff misses (unknown controller API), falls through to the full 5-attempt budget — no regression vs. the pre-guard behavior.
- Pivot the truck's PD angular correction around the hitch node instead of the reference node, so heading correction produces zero tangential velocity at the coupling point and doesn't side-slap the trailer chain. - Yaw-only proportional term via forward-vector math (avoids ambiguous toEulerYXZ components bleeding roll/pitch into the rotation). - Speed-inverse and mass-linear gain scaling: gain grows with truck mass (light rigs no longer over-rotate) and shrinks with speed (KE scales with v² so per-tick rotation energy stays bounded across the speed range). - 3-second grace period after a vehicle loses its coupled_to entry: skips full PD sync during the brief detach→re-attach window (e.g. after a reset) so ang_force=100 doesn't rip the physical coupler apart before the re-attach event arrives. - Remove the `if not coupled_to[id]` gate around kiss_gearbox.apply. The gate froze the truck's gearbox at whatever gear was active at attach time; kiss_gearbox.apply is already a no-op on trailers (no mainController). - kiss_electrics: retry tryAttachGroupImpulse with a 500ms budget so advanced couplers reliably retract landing gear after the async C++ latch completes.
…ate fix Replaces the impulsive angular-torque sync path on coupled rigs (and optionally on solo vehicles) with a lateral force applied at a precomputed front-chassis node subset. The chassis yaws via real beam dynamics and tire slip instead of per-tick velocity snaps at every node, eliminating the hitch whip that made trailer rigs unusable. Core architecture: - New force primitive: apply_force_at_front_chassis distributes a total world-space force across a mass-weighted subset of non-wheel chassis nodes ahead of the CG. Resultant acts at the subset centroid (~35% of vehicle length forward of center). Uses real node mass (new node[5]) rather than the FPS-scaled legacy node[2], so the primitive applies true Newtons via applyForceVector rather than abusing it as a velocity-impulse setter. - front_chassis_subset built at onExtensionLoaded for every vehicle: filters nodes by y > y_center (engine-local forward of CG) and excludes wheel nodes. Top 6 closest to the target point. - Coupled-truck branch in kiss_transforms.update() rewritten: forward- vector yaw error via atan2(cross_z, dot), speed-gated PD signal applied as lateral force via the new primitive. Zero angular torque on coupled rigs — apply_linear_velocity_ang_torque is never called from this branch. Early return prevents fallthrough to legacy PD. - Solo vehicles can opt into the front-puller via use_front_puller_solo toggle. Their vehicle mass is looked up via vehiclemanager.vehicle_masses and passed as truck_mass. PID lateral self-centering: - Added M.lateral_integral state with anti-windup clamp (±5 m·s). Accumulates position error perpendicular to heading each tick; Ki*integral force eliminates steady-state drift that P alone can't close. Reset on cooldown and try_rude teleport. - Yaw-rate-coupled lateral P scaling: lateral_pd_scale is now a "mid-turn floor" that ramps back to 1.0 on straights, soft during hard corners. Auto-balances P authority to the current driving regime without manual tuning. Prediction fixes: - Kinematic position extrapolation now correct: p0 + v0·t + ½·a·t² instead of end-velocity × t (which double-counted acceleration). - Axis-angle rotation extrapolation: q_delta built directly from the angular velocity vector via axis-angle construction, avoiding the BeamNG quatFromEuler/toEulerYXZ slot-convention trap. Right- multiplied onto body rotation, normalized each tick. - Acceleration clamp raised from 5 to 15 m/s² (tunable). The old limit threw away centripetal acceleration during meaningful cornering, making the prediction target lag the real curve. - Split prediction time caps: 100ms linear, 200ms rotational. Normal latency passes through unclipped; only pathological staleness gets capped. Live tuning UI (kisstuning.lua + kissmp/ui/tabs/tuning.lua): - New GE-side extension holding 10 tunables with label/range/desc metadata. Registered in modScript.lua before kissui so the global is available at UI draw time. - New tuning tab in the main window. Sliders for floats, checkboxes for bools. Values pushed into every kiss_transforms.update call as trailing args, so slider changes take effect next physics tick without rebuilding the mod. - Tunables: Kp_yaw, Kd_yaw, force_cap_accel, speed_gate_low, speed_gate_high, accel_clamp, lateral_pd_scale, lateral_integral_gain, deadband_enabled, use_front_puller_solo. - Local-only. No network sync — each player tunes their own remote rendering. Intentional for a dev/testing feature. Spectate input race fix (kiss_input.lua): - Monkey-patches input.event (and kbdSteer/kbdPedal if present) on every vehicle Lua VM to no-op on non-owned vehicles unless a sync_in_progress guard is set. Blocks local joystick/keyboard input from racing against network-synced input on multi-instance A/B test rigs — a major hidden source of "drift" that PD tuning couldn't reach. Status: drivable but imperfect. Trailer rigs no longer disassemble on spawn or during driving. Articulated bus city routes complete without destruction. Residual drift (~0.5-1m over long distances) remains from physics simulation divergence between owner and remote that no amount of force-based sync can fully eliminate. The PID integral term closes steady-state drift at rest; the auto-balanced P closes transient drift during straights; the front-puller handles heading correction via tire dynamics during motion. Parked heading can still be a few degrees off due to the speed gate cutting out before full convergence. Files touched: - KISSMultiplayer/lua/vehicle/extensions/kiss_mp/kiss_vehicle.lua - KISSMultiplayer/lua/vehicle/extensions/kiss_mp/kiss_transforms.lua - KISSMultiplayer/lua/vehicle/extensions/kiss_mp/kiss_input.lua - KISSMultiplayer/lua/ge/extensions/kisstransform.lua - KISSMultiplayer/lua/ge/extensions/kisstuning.lua (new) - KISSMultiplayer/lua/ge/extensions/kissmp/ui/tabs/tuning.lua (new) - KISSMultiplayer/lua/ge/extensions/kissmp/ui/main.lua - KISSMultiplayer/lua/ge/extensions/kissui.lua - KISSMultiplayer/scripts/kiss_mp/modScript.lua
…n fit, OBB debug overlay Stage 1 Phases 0-2 of the cluster sync roadmap. Single-vehicle single-cluster degenerate case: each vehicle is treated as one rigid cluster; owner broadcasts pose via the existing transform channel; remote applies per-node velocity-matching forces to converge. New vehicle-side modules (kiss_mp/cluster_*.lua): - cluster_state: data structures + tuning constants (CONVERGENCE_GAIN, KP_POS) - cluster_discovery: semantic-seeded stiffness-thresholded connected components - cluster_topology: parent-child BFS tree + deterministic topology hash - cluster_frames: mass-weighted centroid, per-node offsets, inertia tensor - cluster_spawn: v.data/obj adapter, region extraction via partOrigin - cluster_math: Horn's quaternion method via Jacobi 4x4 eigendecomposition - cluster_sender: per-cluster pose synthesis (pos, rot, lin_vel, ang_vel) - cluster_receiver: per-node velocity-matching force with position spring - cluster_debug_draw: OBB wireframe (rotates with chassis), region overlay Integration in kiss_transforms.update(): - try_rude (6m teleport snap) runs first with 0.5s cooldown to prevent beam-breaking forces after owner map-teleports - try_apply_cluster_sync branch: gated on cluster_sync_enabled, reads target from existing transform channel, applies per-node forces for root clusters, early-returns before legacy code - Falls through to legacy front-puller/PID if cluster sync unavailable Tuning tab additions: - cluster_sync_enabled (bool, default ON for testing) - cluster_sync_force_fallback (bool, A/B diagnostic toggle) - cluster_convergence_gain (0.05-1.0, default 0.3) - prediction_enabled (bool toggle) - sample_rate_multiplier (outgoing packet rate) - Cluster debug overlay view mode switcher (off/clusters/regions/both) Also includes: - 105 pure-Lua TDD tests (tests/lua/) - Accel smoothing in set_target_transform (70/30 exponential filter) - Electrics log spam removed
…ndividual poses Extends the cluster sync pipeline to broadcast per-cluster pose data for vehicles with more than one cluster (e.g. pickup with separate rear bumper cluster). Single-cluster vehicles continue using the whole-vehicle transform as before (empty clusters vec). Rust schema (requires server rebuild): - ClusterPose struct in shared/src/vehicle/transform.rs: id, position, rotation, linear_velocity, angular_velocity (all world-frame) - VehicleUpdate.clusters: Vec<ClusterPose> with #[serde(default)] Owner send path (kiss_vehicle.lua): - When cluster_state has >1 cluster, compute_all_poses via cluster_sender (Horn rotation + angular momentum) and attach compact cluster pose array to the transform blob GE relay (vehiclemanager.lua → kisstransform.lua): - send_vehicle_update maps compact field names to wire format - update_vehicle_transform attaches clusters to the transform JSON forwarded to the vehicle side Vehicle receive (kiss_transforms.lua): - set_target_transform stashes received_cluster_poses keyed by id - try_apply_cluster_sync looks up per-cluster pose for each cluster; falls back to whole-vehicle transform for root cluster when no per-cluster data available (Phase 2 compat) - Removed is_root gate: all clusters with a received pose get forces
The server stores and relays per-cluster poses but the Vehicle struct, spawn initializer, and incoming VehicleUpdate handler were missing the new clusters field. Fixes compilation error on server rebuild.
…r constants Sender converts non-root cluster poses to parent-relative before sending; receiver reconstructs world poses by composing parent-first in BFS order. Preserves hinge angles exactly (articulated bus, wheel spin) instead of two independent world targets that can disagree. Added parent_id field to ClusterPose (Rust schema change — rebuild server + bridge). New tuning sliders (live push to vehicle side): - Position spring (KP_POS): 1-100, default 50 - Max force per node: 1k-200k N, default 50k - Stiffness threshold: 50k-5M N/m, default 500k - Min cluster size: 3-50 nodes, default 8 - Re-cluster button: re-runs discovery without vehicle reset
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Trailer & coupled-rig multiplayer sync
Added
Coupled trailers run on their physics coupler constraint while the
GE side computes an expected hitch-point position for drift detection
and debug visualization.
articulation), ball hitch (3-DOF quaternion), and pintle (loose
3-DOF with wider drift tolerance). Classified once at attach time
from the coupler tag and cached on the coupling.
per-vehicle
kiss_couplers.onCouplerAttachedhook, GE-levelonCouplerAttacheddispatch, andonObjectCouplingChangediff ofattachedCouplersas a belt-and-suspenders fallback.paths don't send duplicates.
coupled_trucks) so GE-side sync can lookup a truck's trailers and know when to skip
try_rudeor applycoupled-specific scaling.
past the threshold, the whole cluster is translated rigidly to
preserve relative pose and keep the coupler constraint intact.
coupled_to/coupled_trucksso the clusterdiscovery works for road trains transparently.
can size PD gains by the truck/trailer mass ratio at attach time,
plus a mass-based scaling of the coupled rotation gain itself.
coupled_tois cleared,skipping full PD sync during the brief detach→re-attach window
(e.g. after a reset) so the physical coupler isn't ripped before
the re-attach event arrives.
kiss_electrics.set_coupledstate tracking on vehicle side soadvanced coupler electrics only fire when actually coupled via
our tracking. Remote advanced couplers are force-detached at
spawn to prevent auto-attachment to nearby objects.
tryAttachGroupImpulseover a 500 ms budget soadvanced couplers reliably retract landing gear after the async
C++ latch completes.
don't send
VehicleUpdateand wouldn't otherwise get input sync.coupler_tagfield onCouplerAttachednetwork message so theremote can classify hitch type from the same event.
velocity (packed
pitch, roll, yaw) is subtracted against localangular velocity (built
yaw, pitch, roll) in matching slots.line, angular-force indicator, expected/received/current spheres,
drift threshold markers) gated by
M.debug.M.debug_log.Changed
coupled_ang_force = 2(1/50 ofthe non-coupled gain) with a hard cap of 3, giving a proper
restoring force on all three axes without whipping the hitch
per tick.
try_rude(6 m per-vehicle teleport) is skipped for coupled trucksbecause it would yank the rig apart under trailer drag lag. Drift
is handled cluster-wide instead.
kiss_gearbox.applyis always called on synced vehicles; the formerif not coupled_to[id]gate was removing gear sync from the truckside after coupling.
both vehicles, not just
obj_a, so rebroadcast events don'treposition our own rig.
attach_coupleris idempotent on re-received events for thesame pair so duplicates don't disturb the active constraint.
known_couplings,coupled_to, andcoupled_trucksstate is cleared on mission load and vehicledestruction.
Fixed
gain (100) whipping the hitch node per tick.
asymmetric drives — network yaw rate leaking into the remote's roll
slot via mismatched angular-velocity packing.
chain too far to re-latch.
was active at attach time.
flipping at speed, from an experimental hitch-pivot rotation that
injected energy through the wrong axis.
coupled-truck rotation gain was mass-invariant.
back to the sender.
acceleration twice), causing big ang-accel spikes to pass through
unfiltered on lossy networks.
being sent as real couplings.
now land via the GE-level
onCouplerAttacheddispatch or theonObjectCouplingChangediff.remote vehicles at spawn, since their
notAttachedstate wasbeing driven by synced values.
Known issues
of trailer drift.
landing gear drops below the hitch height before the coupler
can re-latch.