Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
2a9bc2c
chore: add mathematical foundation for cluster handling as markdown doc
spectrachrome Apr 17, 2026
d4e39bd
doc: add plans for debugger and cluster sync
spectrachrome Apr 18, 2026
da16907
doc: add implementation order notes
spectrachrome Apr 18, 2026
65b5393
feat: implement Phase 1a measurement primitives for cluster sync
spectrachrome Apr 18, 2026
9f93873
feat: add Lua console command stubs for measurement system
spectrachrome Apr 18, 2026
767e050
docs: revise cluster sync model to sync all body states without joint…
spectrachrome Apr 18, 2026
aa71681
feat: implement Phase 1b core single-vehicle sync with prediction and…
spectrachrome Apr 18, 2026
b8f0df2
Remove dead quaternion math code from shared crate
spectrachrome Apr 18, 2026
c368a7d
feat: implement phase 1c node-position deformation sync
spectrachrome Apr 18, 2026
649d11e
fix: add deformation field to server VehicleUpdate construction
spectrachrome Apr 18, 2026
0b3a697
feat: implement force-based velocity-matched state replay
spectrachrome Apr 19, 2026
1c89a36
fix: add teleport detection with debounced reset handling
spectrachrome Apr 19, 2026
07049fa
feat: replace body-twist velocity estimator with direct per-node replay
spectrachrome Apr 19, 2026
4333743
fix: cluster_nodes uses HashMap to match Lua's cid-keyed tables
spectrachrome Apr 19, 2026
06ff458
fix: coerce node CIDs from string to number in apply_nodes
spectrachrome Apr 19, 2026
38be996
feat: fragment cluster_nodes into independent per-tick datagrams
spectrachrome Apr 20, 2026
336979f
Revert "feat: fragment cluster_nodes into independent per-tick datagr…
spectrachrome Apr 20, 2026
c2e3b1c
fix: try to use ordered tcp channel instead of udp to get around data…
spectrachrome Apr 20, 2026
8a87f60
feat: shrink cluster_nodes — send rest-subtracted offsets, quantize t…
spectrachrome Apr 20, 2026
62c8177
feat: layered per-node sync with deviation capture + receiver-side ga…
spectrachrome Apr 21, 2026
48eb90e
feat: checkpoint motion-first sync before heading-hold test
spectrachrome Apr 23, 2026
39a5f7e
feat: checkpoint trajectory sync with RTT timing
spectrachrome Apr 24, 2026
7b4d5f3
feat: COG-space motion-first sync with dead-reckoning + PD correction
spectrachrome Apr 28, 2026
bac807d
feat: sender-derived acceleration on the wire + tuning sliders
spectrachrome Apr 29, 2026
def5063
fix: drop receiver acceleration lerp now that sender ships smoothed a…
spectrachrome Apr 29, 2026
d3d226e
fix: stabilize vehicle replay sync
spectrachrome Apr 30, 2026
b4aa675
fix: send vehicle updates unreliably
spectrachrome Apr 30, 2026
1ae0796
fix: align vehicle sync scheduling
spectrachrome Apr 30, 2026
1a9f661
fix: remove angular acceleration prediction
spectrachrome Apr 30, 2026
e1b2397
fix: suppress local input on remote vehicles
spectrachrome Apr 30, 2026
6b5928a
fix: damp remote yaw replay
spectrachrome May 1, 2026
cc67bb9
fix: stabilize vehicle lifecycle handling
spectrachrome May 1, 2026
56c61df
chore: use better variable names
spectrachrome May 1, 2026
d210599
chore: remove guides
spectrachrome May 1, 2026
ea659ed
fix: restore accidentally deleted unrelated code
spectrachrome May 1, 2026
9af3c4e
chore: remove now-invalidated doc
spectrachrome May 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion KISSMultiplayer/lua/ge/extensions/kissconfig.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
local M = {}
local imgui = ui_imgui

local function clamp_min(value, minimum)
if value == nil then return nil end
return math.max(minimum, value)
end

local function clamp_int_min(value, minimum)
if value == nil then return nil end
return math.max(minimum, math.floor(value))
end

local function generate_base_secret()
math.randomseed(os.time() + os.clock())
local result = ""
Expand All @@ -24,7 +34,7 @@ local function save_config()
window_opacity = kissui.window_opacity[0],
enable_view_distance = kissui.enable_view_distance[0],
view_distance = kissui.view_distance[0],
base_secret_v2 = secret
base_secret_v2 = secret,
}
local file = io.open("./settings/kissmp_config.json", "w")
file:write(jsonEncode(result))
Expand Down
4 changes: 4 additions & 0 deletions KISSMultiplayer/lua/ge/extensions/kissmp/ui/main.lua
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ local function draw(dt)
kissui.tabs.settings.draw()
imgui.EndTabItem()
end
if imgui.BeginTabItem("Tuning") then
kissui.tabs.tuning.draw()
imgui.EndTabItem()
end
imgui.EndTabBar()
end
end
Expand Down
89 changes: 89 additions & 0 deletions KISSMultiplayer/lua/ge/extensions/kissmp/ui/tabs/tuning.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
local M = {}
local imgui = ui_imgui

-- Receiver-side velocity smoothing rate (Hz cutoff) consumed by kiss_sync.lua.
-- Default must match kiss_sync.M.REMOTE_VEL_SMOOTH_RATE so the slider state
-- matches the vehicle-side state on first open.
local vel_rate = imgui.FloatPtr(2.0)
local prediction_offset_ms = imgui.FloatPtr(0.0)
local linear_pull_scale = imgui.FloatPtr(1.0)
local angular_pull_scale = imgui.FloatPtr(0.65)
local owner_teleport_cooldown_ms = imgui.FloatPtr(500.0)
local remote_teleport_cooldown_ms = imgui.FloatPtr(500.0)
local teleport_reset_delay_ms = imgui.FloatPtr(500.0)

local function build_command()
return string.format(
"kiss_sync.set_smoothing_tuning(%f, %f); kiss_transforms.set_linear_pull_scale(%f); kiss_transforms.set_angular_pull_scale(%f)",
vel_rate[0],
prediction_offset_ms[0] * 0.001,
linear_pull_scale[0],
angular_pull_scale[0]
)
end

local function push_to_vehicle(vehicle)
if not vehicle then return end
vehicle:queueLuaCommand(build_command())
if vehiclemanager and vehiclemanager.set_teleport_tuning then
vehiclemanager.set_teleport_tuning(
owner_teleport_cooldown_ms[0] * 0.001,
remote_teleport_cooldown_ms[0] * 0.001,
teleport_reset_delay_ms[0] * 0.001
)
end
end

local function push_to_all_vehicles()
local cmd = build_command()
if vehiclemanager and vehiclemanager.set_teleport_tuning then
vehiclemanager.set_teleport_tuning(
owner_teleport_cooldown_ms[0] * 0.001,
remote_teleport_cooldown_ms[0] * 0.001,
teleport_reset_delay_ms[0] * 0.001
)
end
for i = 0, be:getObjectCount() do
local vehicle = be:getObject(i)
if vehicle then
vehicle:queueLuaCommand(cmd)
end
end
end

local function draw()
imgui.PushTextWrapPos(0)
imgui.Text("Receiver-side velocity smoothing.")
imgui.Text("Higher = tracks new packets faster, less smoothing.")
imgui.Text("Lower = heavier smoothing, more lag.")
imgui.PopTextWrapPos()
imgui.Separator()

if imgui.SliderFloat("Velocity smooth rate", vel_rate, 0.0, 30.0, "%.1f Hz") then
push_to_all_vehicles()
end
if imgui.SliderFloat("Prediction offset", prediction_offset_ms, -80.0, 80.0, "%.0f ms") then
push_to_all_vehicles()
end
if imgui.SliderFloat("Linear pull scale", linear_pull_scale, 0.5, 1.5, "%.2fx") then
push_to_all_vehicles()
end
if imgui.SliderFloat("Angular pull scale", angular_pull_scale, 0.2, 1.2, "%.2fx") then
push_to_all_vehicles()
end
if imgui.SliderFloat("Owner teleport cooldown", owner_teleport_cooldown_ms, 0.0, 1500.0, "%.0f ms") then
push_to_all_vehicles()
end
if imgui.SliderFloat("Remote teleport cooldown", remote_teleport_cooldown_ms, 0.0, 1500.0, "%.0f ms") then
push_to_all_vehicles()
end
if imgui.SliderFloat("Teleport reset delay", teleport_reset_delay_ms, 0.0, 1500.0, "%.0f ms") then
push_to_all_vehicles()
end
end

M.draw = draw
M.push_to_vehicle = push_to_vehicle
M.push_to_all_vehicles = push_to_all_vehicles

return M
179 changes: 169 additions & 10 deletions KISSMultiplayer/lua/ge/extensions/kisstransform.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,153 @@ M.received_transforms = {}
M.local_transforms = {}
M.raw_positions = {}
M.inactive = {}
M.teleport_cooldowns = {}

M.threshold = 3
M.rot_threshold = 2.5
M.velocity_error_limit = 10

M.hidden = {}

local DEBUG_GLOBAL = false

-- BeamNG auto-loads lua/vehicle/extensions/*.lua but does not recurse into
-- subfolders. The kiss_mp/* extensions need an explicit addModulePath +
-- loadModulesInDirectory call to become available in vehicle Lua. Prepended
-- to every queueLuaCommand into a kiss_mp/* module so the call is self-healing
-- if the vehicle Lua context ever resets.
local VEHICLE_SYNC_BOOTSTRAP = "extensions.addModulePath('lua/vehicle/extensions/kiss_mp'); extensions.loadModulesInDirectory('lua/vehicle/extensions/kiss_mp'); "

local function queue_kiss_command(vehicle, command)
if not vehicle then return end
vehicle:queueLuaCommand(VEHICLE_SYNC_BOOTSTRAP .. command)
end

-- Cluster snap used only for large recovery corrections. The target position
-- passed here must be a refnode/origin position, not COG.
local function apply_cluster_target(vehicle_id,
target_origin_x, target_origin_y, target_origin_z,
target_rotation_x, target_rotation_y, target_rotation_z, target_rotation_w,
target_velocity_x, target_velocity_y, target_velocity_z)
local veh = be:getObjectByID(vehicle_id)
if not veh then return end
local ref_node_id = veh:getRefNodeId()

local current_rotation = quatFromDir(-veh:getDirectionVector(), veh:getDirectionVectorUp())
local target_rotation = quat(target_rotation_x, target_rotation_y, target_rotation_z, target_rotation_w)
local relative_rotation = current_rotation:inversed() * target_rotation

veh:setClusterPosRelRot(ref_node_id, target_origin_x, target_origin_y, target_origin_z,
relative_rotation.x, relative_rotation.y, relative_rotation.z, relative_rotation.w)

local local_velocity = vec3(veh:getVelocity())
local rotated_local_velocity = local_velocity:rotated(relative_rotation)
veh:applyClusterVelocityScaleAdd(ref_node_id, 1,
target_velocity_x - rotated_local_velocity.x,
target_velocity_y - rotated_local_velocity.y,
target_velocity_z - rotated_local_velocity.z)
end

local function queue_cog_snap(vehicle, transform)
local position, rotation = transform.position, transform.rotation
local velocity = transform.velocity or {0, 0, 0}
local angular_velocity = transform.angular_velocity or {0, 0, 0}
if not (position and rotation and #position >= 3 and #rotation >= 4) then return end
queue_kiss_command(vehicle,
"kiss_transforms.snap_to_cog_target("
..position[1]..","..position[2]..","..position[3]..","
..rotation[1]..","..rotation[2]..","..rotation[3]..","..rotation[4]..","
..(velocity[1] or 0)..","..(velocity[2] or 0)..","..(velocity[3] or 0)..","
..(angular_velocity[1] or 0)..","..(angular_velocity[2] or 0)..","..(angular_velocity[3] or 0)..")"
)
end

-- Finite-number guard. Rejects NaN and +/-Inf by checking against a sane
-- world-coordinate range. Used to prevent garbage from flowing into
-- cluster pose application (BeamNG silently accepts NaN and then breaks the
-- vehicle) and as the common shape for future wire-side validation.
local function is_finite_number(x)
if type(x) ~= "number" then return false end
-- NaN != NaN; also reject absurd magnitudes that indicate physics blow-up.
if x ~= x then return false end
if x > 1e8 or x < -1e8 then return false end
return true
end

local function is_finite_transform(position, rotation)
if #position < 3 or #rotation < 4 then return false end
return is_finite_number(position[1]) and is_finite_number(position[2]) and is_finite_number(position[3])
and is_finite_number(rotation[1]) and is_finite_number(rotation[2])
and is_finite_number(rotation[3]) and is_finite_number(rotation[4])
end
local function update(dt)
if not network.connection.connected then return end
-- Get rotation/angular velocity from vehicle lua
if DEBUG_GLOBAL then
print("[kisstransform.update] START dt=" .. tostring(dt) .. " received_transforms=" .. tostring(#M.received_transforms))
end

if not network.connection.connected then
if DEBUG_GLOBAL then print("[kisstransform.update] BLOCKED: not connected") end
return
end

for id, remaining in pairs(M.teleport_cooldowns) do
remaining = remaining - dt
if remaining <= 0 then
M.teleport_cooldowns[id] = nil
else
M.teleport_cooldowns[id] = remaining
end
end

-- Refresh each vehicle's local transform cache. Only owned vehicles send
-- this cache over the network, but remote vehicles still need their vehicle
-- Lua modules loaded before receiver-side correction runs.
for i = 0, be:getObjectCount() do
local vehicle = be:getObject(i)
if vehicle and (not M.inactive[vehicle:getID()]) then
vehicle:queueLuaCommand("kiss_vehicle.update_transform_info()")
local vid = vehicle and vehicle:getID()
if vehicle and (not M.inactive[vid]) then
local owned = vehiclemanager.ownership[vid] ~= nil
queue_kiss_command(vehicle, "kiss_vehicle.update_transform_info(" .. tostring(owned) .. ")")
end
end

-- Don't apply velocity while paused. If we do, velocity gets stored up and released when the game resumes.
local apply_velocity = not bullettime.getPause()
if DEBUG_GLOBAL and not apply_velocity then
print("[kisstransform.update] BLOCKED: game paused (bullettime.getPause()=true)")
end

if DEBUG_GLOBAL then
print("[kisstransform.update] apply_velocity=" .. tostring(apply_velocity) .. " iterating received_transforms...")
end

for id, transform in pairs(M.received_transforms) do
--apply_transform(dt, id, transform, apply_velocity)
if DEBUG_GLOBAL then
print("[kisstransform.update] Processing vehicle id=" .. tostring(id))
end

local vehicle = be:getObjectByID(id)
local p = vec3(transform.position)
if vehicle and apply_velocity and (not vehiclemanager.ownership[id]) then
local teleport_cooldown = M.teleport_cooldowns[id]

if not vehicle then
if DEBUG_GLOBAL then print("[kisstransform.update] BLOCKED: vehicle " .. tostring(id) .. " not found") end
elseif teleport_cooldown and teleport_cooldown > 0 then
if not M.inactive[id] then
vehicle:setActive(0)
M.inactive[id] = true
end
if DEBUG_GLOBAL then print("[kisstransform.update] BLOCKED: teleport cooldown for vehicle " .. tostring(id)) end
elseif not apply_velocity then
if DEBUG_GLOBAL then print("[kisstransform.update] BLOCKED: apply_velocity=false for vehicle " .. tostring(id)) end
elseif vehiclemanager.ownership[id] then
if DEBUG_GLOBAL then print("[kisstransform.update] BLOCKED: we own vehicle " .. tostring(id)) end
else
if ((p:distance(vec3(getCameraPosition())) > kissui.view_distance[0])) and kissui.enable_view_distance[0] then
if DEBUG_GLOBAL then
local dist = p:distance(vec3(getCameraPosition()))
print("[kisstransform.update] BLOCKED: vehicle " .. tostring(id) .. " outside view distance (dist=" .. dist .. ")")
end
if (not M.inactive[id]) then
vehicle:setActive(0)
M.inactive[id] = true
Expand All @@ -41,18 +163,45 @@ local function update(dt)
if M.inactive[id] then
vehicle:setActive(1)
M.inactive[id] = false
-- Reactivated replicas can be far from the authority because
-- setActive(0) freezes local physics. Snap once, then resume the
-- normal per-frame correction path.
queue_cog_snap(vehicle, transform)
if DEBUG_GLOBAL then print("[kisstransform.update] Reactivated vehicle " .. tostring(id)) end
end
if DEBUG_GLOBAL then
print("[kisstransform.update] QUEUING update for vehicle " .. tostring(id))
end
vehicle:queueLuaCommand("kiss_transforms.set_target_transform(" .. string.format("%q", jsonEncode(transform)) .. ")")
vehicle:queueLuaCommand("kiss_transforms.update("..dt..")")
-- Per-frame correction runs from kiss_transforms.updateGFX inside
-- vehicle Lua. GE only handles activity/view-distance state here.
end
end
end

if DEBUG_GLOBAL then
print("[kisstransform.update] END")
end
end

local function update_vehicle_transform(data)
local transform = data.transform
transform.owner = data.vehicle_id
transform.sent_at = data.sent_at
transform.send_timer = data.send_timer
transform.ping_ms = data.ping_ms
transform.send_dt = data.send_dt
transform.receiver_ping_ms = network.connection.rtt_smooth_ms or network.connection.ping or 0

-- Normalize quaternion in place so all downstream consumers (vehicle-Lua
-- try_rude predicted-pose comparison, kiss_sync snapshot buffer, and
-- COG-aware recovery snaps) see a unit quaternion.
local r = transform.rotation
if r and #r >= 4 then
local n = math.sqrt(rotation[1]*rotation[1] + rotation[2]*rotation[2] + rotation[3]*rotation[3] + rotation[4]*rotation[4])
if n > 1e-9 then
rotation[1], rotation[2], rotation[3], rotation[4] = rotation[1]/n, rotation[2]/n, rotation[3]/n, rotation[4]/n
end
end

local id = vehiclemanager.id_map[transform.owner or -1] or -1
if vehiclemanager.ownership[id] then return end
Expand All @@ -61,19 +210,29 @@ local function update_vehicle_transform(data)

local vehicle = be:getObjectByID(id)
if vehicle and (not M.inactive[id]) then
transform.time_past = clamp(vehiclemanager.get_current_time() - transform.sent_at, 0, 0.1) * 0.9 + 0.001
vehicle:queueLuaCommand("kiss_transforms.set_target_transform(" .. string.format("%q", jsonEncode(transform)) .. ")")
-- Packet arrival hands the new authoritative COG pose to kiss_sync.
-- Application happens per-frame from kiss_transforms.update(dt), not on
-- packet arrival.
queue_kiss_command(vehicle, "kiss_transforms.set_target_transform(" .. string.format("%q", jsonEncode(transform)) .. ")")
end
end

local function push_transform(id, t)
M.local_transforms[id] = jsonDecode(t)
end

local function set_teleport_cooldown(vehicle_id, duration)
M.teleport_cooldowns[vehicle_id] = math.max(duration or 0.35, 0)
end

M.send_transform_updates = send_transform_updates
M.send_vehicle_transform = send_vehicle_transform
M.update_vehicle_transform = update_vehicle_transform
M.push_transform = push_transform
M.queue_kiss_command = queue_kiss_command
M.queue_cog_snap = queue_cog_snap
M.set_teleport_cooldown = set_teleport_cooldown
M.apply_cluster_target = apply_cluster_target
M.onUpdate = update

return M
1 change: 1 addition & 0 deletions KISSMultiplayer/lua/ge/extensions/kissui.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ M.tabs = {
settings = require("kissmp.ui.tabs.settings"),
direct_connect = require("kissmp.ui.tabs.direct_connect"),
create_server = require("kissmp.ui.tabs.create_server"),
tuning = require("kissmp.ui.tabs.tuning"),
}

M.dependencies = {"ui_imgui"}
Expand Down
Loading