-- Model collision repair 4.3.1: collision cooked from the SAME render GLB; no sidecar assets. -- Driving edition 1: latest working vehicle + unchanged audio; render-rate cameras, -- retained dashboard, progressive driver-input mapping, explicit optional aero. -- Skyline Physics v4 -- Lua 5.4 / supplied Skate SDK API 2. -- Reduced-order vehicle: one 6-DOF chassis, four rotating tire DOFs, -- passive clutch/differential/brakes, and a combined-slip brush tire law. -- No yaw targets, countersteer assist, slide-recovery state, lateral-velocity -- deletion, handbrake grip multiplier, or upright torque. Aero is an explicit load. -- See README.md for the collision/API contract and validation limits. local BODY, MODEL = "chassis", "skyline.glb" local C = { mass = 1400, gravity = 9.81, -- At nominal ride height: CG is 0.400 m above a level road. -- This is an explicitly tuned chassis, NOT measured factory Skyline data. center = {0, -0.27160, 0.16}, inertia_half = {0.92, 0.55, 2.10}, -- Physics uses the existing rendered body node, cooked into multiple -- convex solids by the host. No separate collision GLB/JSON or point list. entry_radius = 1.20, reentry_delay = 5.0, radius = 0.341, ride_length = 0.20, droop_length = 0.37, front_spring = 37000, rear_spring = 32000, damping_ratio = 0.85, front_antiroll = 8000, rear_antiroll = 6800, antiroll_damping = 450, bump_length = 0.055, bump_stiffness = 110000, maximum_load = 22000, wheel_inertia = 1.85, -- Cornering/longitudinal stiffness in N per unit normalized slip, not N/(m/s). front_stiffness_per_load = 23.0, rear_stiffness_per_load = 24.0, load_exponent = 0.90, front_drive = 0.15, brake_torque_total = 6800, front_brake_fraction = 0.72, handbrake_torque = 3800, rolling_coefficient = 0.012, rear_diff_preload = 12, rear_diff_ramp = 0.18, steer_limit = math.rad(40), steer_rate = math.rad(170), steering_exponent = 1.70, final_drive = 3.545, reverse_ratio = 3.280, gear_ratios = {3.827, 2.360, 1.685, 1.312, 1.000, 0.793}, idle_rpm = 900, redline_rpm = 8000, shift_time = 0.18, engine_inertia = 0.30, clutch_capacity = 620, iterations = 8, tire_root_iterations = 10, aero_drag = 0.42, } -- Model's named left wheels are +X; model forward is +Z. -- A right input therefore requests NEGATIVE yaw, matching a rear chase view. local wheels = { {name="FL", key="wheel_fl", pos={ 0.794235,-0.330559, 1.443641}, front=true, mate=2}, {name="FR", key="wheel_fr", pos={-0.796149,-0.330652, 1.443556}, front=true, mate=1}, {name="RL", key="wheel_rl", pos={ 0.795353,-0.330587,-1.347033}, front=false,mate=4}, {name="RR", key="wheel_rr", pos={-0.796153,-0.330635,-1.347024}, front=false,mate=3}, } local wheelbase = 1.44360 + 1.34703 local track = 1.591 local I = { C.mass/3 * (C.inertia_half[2]^2 + C.inertia_half[3]^2), C.mass/3 * (C.inertia_half[1]^2 + C.inertia_half[3]^2), C.mass/3 * (C.inertia_half[1]^2 + C.inertia_half[2]^2), } local state = { spawned=false, occupied=false, pending=nil, spawn_confirm=nil, gear=1, rpm=C.idle_rpm, engine_omega=C.idle_rpm*math.pi/30, clutch=0, steer=0, shift=0, time=0, enter_after=0, keys={}, buttons=0, hud_time=0, mass_audit=nil, keyboard_axis=0, aero_load=0, enter_requested=false, exit_requested=false, reenter_on_ready=false, hull_debug=false, interaction_text="", mp_hint_time=0, } local function clamp(x,a,b) return math.max(a,math.min(b,x)) end local function finite(x) return type(x)=="number" and x==x and math.abs(x)1e-8 and mul(a,1/l) or nil end local function rotate(q,v) local t=mul(cross({q[1],q[2],q[3]},v),2) return add(v,add(mul(t,q[4]),cross({q[1],q[2],q[3]},t))) end local function unrotate(q,v) return rotate({-q[1],-q[2],-q[3],q[4]},v) end local function inertia(q,v,inverse) local a=unrotate(q,v) for i=1,3 do a[i]=inverse and a[i]/I[i] or a[i]*I[i] end return rotate(q,a) end local function yaw(q) return math.atan(2*(q[4]*q[2]+q[1]*q[3]),1-2*(q[2]^2+q[3]^2)) end local function yaw_rotation(a) return {0,math.sin(a/2),0,math.cos(a/2)} end local function approach(x,target,step) return x+clamp(target-x,-step,step) end local function deadzone(x,threshold) if math.abs(x)<=threshold then return 0 end return (x<0 and -1 or 1)*(math.abs(x)-threshold)/(1-threshold) end local function gear_ratio() if state.gear==0 then return 0 end return (state.gear<0 and -C.reverse_ratio or C.gear_ratios[state.gear])*C.final_drive end local function gear_label() return state.gear<0 and "R" or (state.gear==0 and "N" or tostring(state.gear)) end local function down(key) return sdk.input.down(key)==true end local function edge(key) local held=down(key) local yes=held and not state.keys[key] state.keys[key]=held return yes end local function pressed_button(buttons,mask) return (buttons & mask)~=0 and (state.buttons & mask)==0 end -- AUDIO EXTENSION: single engine sample, pitch-shifted by RPM. local sound = { running=false, supported=false, prepared=false, clock=0, cooldown=0, rpm=900, throttle=0, shift=0, previous_throttle=0, overrun_remaining=0, pop_slot=0, random_state=73641, gain=0, shift_gain=1, load=0, log_pitch=nil, turbo_gain=0, turbo_pitch=1, brake=0, brake_volume=0, drift_slip=0, drift_slip_smooth=0, drift_volume=0, brake_pitch=1, } local DRIFT_SLIP_MIN = 0.28 local ENGINE_PATH = "audio/engine.wav" local ENGINE_BASE_RPM = 3400 local BRAKE_PATH = "audio/brake.wav" local function smooth_toward(current,target,dt,tau) return current+(target-current)*(1-math.exp(-dt/math.max(tau,1e-4))) end local function smooth_rpm_toward(current,target,dt) local delta=target-current local tau=0.20+0.55*clamp(math.abs(delta)/2800,0,1) return smooth_toward(current,target,dt,tau) end local function sound_random() sound.random_state=(sound.random_state*48271)%2147483647 return sound.random_state/2147483647 end local function stop_audio() if sound.supported then sdk.audio.stop_all() end sound.running=false; sound.cooldown=0; sound.overrun_remaining=0 sound.previous_throttle=0 sound.gain=0; sound.shift_gain=1; sound.load=0; sound.log_pitch=nil sound.turbo_gain=0; sound.turbo_pitch=1 sound.brake=0; sound.brake_volume=0 sound.drift_slip=0; sound.drift_slip_smooth=0 sound.drift_volume=0; sound.brake_pitch=1 end local function prepare_audio() sound.supported=type(sdk.audio)=="table" and number(sdk.audio.version,0)>=1 if not sound.supported then sdk.log("Skyline audio unavailable: install the native Audio API update and rebuild the game.") sdk.log("Skyline: sound requires Audio API extension 1.") return end if sound.prepared then return end sdk.audio.preload(ENGINE_PATH) sdk.audio.preload("audio/turbo_loop.wav") sdk.audio.preload("audio/lift.wav") sdk.audio.preload("audio/pop.wav") sdk.audio.preload(BRAKE_PATH) sound.prepared=true sdk.ui.text("skyline_audio","") end local function start_audio() if not sound.supported or sdk.settings.audio_enabled==false or sound.running then return end sound.clock=0; sound.cooldown=0 sound.rpm=math.max(state.rpm,200) sound.throttle=0; sound.previous_throttle=0; sound.overrun_remaining=0 sound.gain=0; sound.shift_gain=1; sound.load=0 sound.log_pitch=math.log(math.max(sound.rpm,200)/ENGINE_BASE_RPM) sound.turbo_gain=0; sound.turbo_pitch=1 local pitch=clamp(math.exp(sound.log_pitch),0.25,3) local pitch_b=clamp(pitch*1.005,0.25,3) sdk.audio.play("engine_main",{ path=ENGINE_PATH,body=BODY,offset={0,-0.1,0.45},loop=true,volume=0, pitch=pitch,spatial=true,spatial_scale=0.10,fade_in=0.08, }) sdk.audio.play("engine_main_b",{ path=ENGINE_PATH,body=BODY,offset={0,-0.1,0.45},loop=true,volume=0, pitch=pitch_b,spatial=true,spatial_scale=0.10,fade_in=0.08, }) sdk.audio.play("turbo",{ path="audio/turbo_loop.wav",body=BODY,offset={0,0,0.8}, loop=true,volume=0,pitch=1,spatial=true,spatial_scale=0.1,fade_in=0.08, }) sdk.audio.play("brake",{ path=BRAKE_PATH,body=BODY,offset={0,-0.25,-0.9}, loop=true,volume=0,pitch=1,spatial=true,spatial_scale=0.12,fade_in=0.10, }) sdk.audio.play("brake_b",{ path=BRAKE_PATH,body=BODY,offset={0,-0.25,-0.9}, loop=true,volume=0,pitch=1.028,spatial=true,spatial_scale=0.12,fade_in=0.10, }) sound.running=true end local function exhaust_pop(intensity) if not sound.running then return end local master=clamp(number(sdk.settings.engine_volume,0.70),0,1) local volume=clamp(number(sdk.settings.pop_volume,0.65),0,1) if master*volume<=0 then return end sound.pop_slot=sound.pop_slot%6+1 sdk.audio.play("exhaust_pop_"..sound.pop_slot,{ path="audio/pop.wav", body=BODY,offset={-0.5,-0.22,-1.95},loop=false, volume=clamp(master*volume*intensity,0,1), pitch=0.90+0.22*sound_random(),spatial=true,spatial_scale=0.1,fade_in=0, }) end local function update_audio(event) if not sound.supported then return end if not state.spawned or sdk.settings.audio_enabled==false then if sound.running then stop_audio() end return end if not sound.running then start_audio() end if sdk.snapshot.paused or sdk.snapshot.replay then return end local dt=clamp(number(event and event.dt,1/60),0,1/30) sound.clock=sound.clock+dt local actual_rpm=clamp(number(state.rpm,0),0,12000) sound.rpm=smooth_rpm_toward(sound.rpm,actual_rpm,dt) local throttle=clamp(number(sound.throttle,0),0,1) local master=clamp(number(sdk.settings.engine_volume,0.70),0,1) sound.cooldown=math.max(0,sound.cooldown-dt) local limiter=actual_rpm>=C.redline_rpm-160 and throttle>0.55 and sound.shift<=0 if limiter and sdk.settings.redline_pops~=false then sound.overrun_remaining=0 if sound.cooldown<=0 then exhaust_pop(0.78+0.20*sound_random()) sound.cooldown=0.095+0.075*sound_random() end else local lifted=sound.previous_throttle>0.6 and throttle<0.2 and actual_rpm>4000 if lifted then if sdk.settings.overrun_pops~=false then sound.overrun_remaining=2+math.floor(sound_random()*2) sound.cooldown=0.04+0.06*sound_random() end if sdk.settings.turbo_audio~=false and master>0 then sdk.audio.play("turbo_lift",{ path="audio/lift.wav",body=BODY,offset={0,0,0.8}, volume=master*0.55,pitch=0.85+0.2*sound_random(), spatial=true,spatial_scale=0.1,fade_in=0, }) end end if throttle>0.3 or actual_rpm<3000 or sdk.settings.overrun_pops==false then sound.overrun_remaining=0 end if sound.overrun_remaining>0 and sound.cooldown<=0 then exhaust_pop(0.36+0.24*sound_random()) sound.overrun_remaining=sound.overrun_remaining-1 sound.cooldown=0.11+0.12*sound_random() end end sound.previous_throttle=throttle local brake_in=clamp(number(sound.brake,0),0,1) local speed=clamp(number(state.speed,0),0,200) local speed_factor=clamp((speed-0.5)/12,0,1) local brake_master=clamp(number(sdk.settings.brake_volume,0.55),0,1) sound.drift_slip_smooth=smooth_toward(sound.drift_slip_smooth,number(sound.drift_slip,0),dt,0.14) local slip=sound.drift_slip_smooth local drift_mix=0 if slip>DRIFT_SLIP_MIN then local norm=clamp((slip-DRIFT_SLIP_MIN)/2.6,0,1) drift_mix=norm*norm*norm end local pedal_brake=brake_in*speed_factor local drift_target=drift_mix*speed_factor*0.68 local drift_tau=drift_target>sound.drift_volume and 0.22 or 0.14 sound.drift_volume=smooth_toward(sound.drift_volume,drift_target,dt,drift_tau) local brake_mix=math.max(pedal_brake,sound.drift_volume) local brake_target=(sdk.settings.brake_audio==false) and 0 or brake_master*brake_mix sound.brake_volume=smooth_toward(sound.brake_volume,brake_target,dt,0.16) local norm_pitch=clamp((slip-DRIFT_SLIP_MIN)/3.5,0,1) local wobble=0.012*math.sin(sound.clock*4.3)+0.008*math.sin(sound.clock*7.1) sound.brake_pitch=smooth_toward(sound.brake_pitch,0.98+0.08*norm_pitch+wobble,dt,0.22) local brake_vol=sound.brake_volume sdk.audio.update("brake",{ volume=brake_vol*0.72, pitch=clamp(sound.brake_pitch,0.25,4), }) sdk.audio.update("brake_b",{ volume=brake_vol*0.28, pitch=clamp(sound.brake_pitch*1.028,0.25,4), }) local rpm=math.max(sound.rpm,200) sound.load=smooth_toward(sound.load,throttle^0.70,dt,0.22) local load=sound.load local alive=clamp((rpm-150)/600,0,1) sound.shift_gain=smooth_toward(sound.shift_gain,sound.shift>0 and 0.82 or 1.0,dt,0.18) local drift_gain=1.0+0.07*drift_mix local target=master*0.50*alive*(0.70+0.30*load)*sound.shift_gain*drift_gain sound.gain=smooth_toward(sound.gain,target,dt,0.20) local gain=sound.gain local target_log=math.log(rpm/ENGINE_BASE_RPM) sound.log_pitch=smooth_toward(sound.log_pitch or target_log,target_log,dt,0.26) local pitch=clamp(math.exp(sound.log_pitch),0.25,3) local pitch_b=clamp(pitch*1.005,0.25,3) sdk.audio.update("engine_main",{ volume=gain*0.70,pitch=pitch, }) sdk.audio.update("engine_main_b",{ volume=gain*0.30,pitch=pitch_b, }) local boost_target=clamp((rpm-2000)/4500,0,1)*load sound.turbo_gain=smooth_toward(sound.turbo_gain,boost_target,dt,0.24) sound.turbo_pitch=smooth_toward(sound.turbo_pitch,clamp(0.65+rpm/9000,0.25,4),dt,0.22) sdk.audio.update("turbo",{ volume=sdk.settings.turbo_audio==false and 0 or master*0.16*sound.turbo_gain, pitch=sound.turbo_pitch, }) end -- END AUDIO EXTENSION -- DRIVING PRESENTATION: no writes to physical body state. local presentation = { mode="chase", supported=false, debug=false, hud_timer=0, speed=0, handbrake=false, active_hud=false, } local function clear_debug_text() for _,key in ipairs({"skyline_help","skyline_status","skyline_handling","skyline_wheels"}) do sdk.ui.text(key,"") end end local function set_debug_visible(visible) presentation.debug=visible clear_debug_text() if visible then sdk.ui.text("skyline_help","F10 spawn | E/Y enter | RT/W gas | LT/S brake | B/Shift handbrake | RB/X up, LB/Z down | N neutral | Xbox X/C camera | H debug | R/R3 reset") end end local function configure_camera() if not state.occupied then return end presentation.hud_timer=0 if not presentation.supported then sdk.camera.follow(BODY,{0,2.1,-5.8}) return end local hood=presentation.mode=="hood" sdk.camera.rig(BODY,{ mode=presentation.mode, distance=clamp(number(sdk.settings.camera_distance,5.8),3.5,10), distance_gain=1.8,height=1.9,height_gain=0.25,target_height=0.78, look_ahead=0.12,velocity_heading=0.18, spring_hz=clamp(number(sdk.settings.camera_spring,2.8),1,6), heading_half_life=0.14,acceleration_lag=0.018,speed_reference=55.556, fov=hood and 60 or clamp(number(sdk.settings.camera_fov,55),40,80), fov_gain=hood and 0 or 8,near=hood and 0.04 or 0.07, collision=true,collision_radius=0.28, hood_offset={0,clamp(number(sdk.settings.hood_height,0.48),0.30,0.9),1.05}, }) end local function clear_presentation() sdk.camera.clear_follow() if presentation.supported then sdk.ui.remove("driving_dashboard") end presentation.active_hud=false;presentation.hud_timer=0 clear_debug_text() end -- GTA-style VFX: textured camera billboards (smoke) + persistent rubber decals (skids). -- Transparent sort is ascending distance: lower bias draws behind, higher draws in front. local SKID_BUFFER_OPTS={blend=true,unlit=true,texture="textures/skid_tread.png",depth_bias=-3.5,tint={0.05,0.05,0.055}} local SMOKE_BUFFER_OPTS={blend=true,unlit=true,texture="textures/smoke_soft.png",depth_bias=4.5,tint={0.82,0.82,0.85}} local SKID_CHUNK_MAX_VERTS=8192 local SKID_POINTS_PER_UPLOAD=24 local SKID_PTS_TAIL=32 -- Host allows 32 mesh buffers/mod; reserve smoke + remote VFX headroom. local MAX_LOCAL_SKID_BUFFERS=12 local MAX_REMOTE_SKID_BUFFERS=6 local MAX_REMOTE_VFX_PEERS=4 local SKID_ALPHA_MIN=0.10 local SKID_ALPHA_MAX=0.72 local MAX_SMOKE_SPAWN_PER_TICK=1 local MAX_SMOKE_PARTICLES=40 local SMOKE_UPLOAD_INTERVAL=2 local REMOTE_VFX_STALE_TICKS=10 local vfx={ supported=false,skid_kappa=0.10,skid_alpha=5.0, skid_width=0.32,min_skid_dist=0.05,skid_lift=0.012, smoke_lifetime=1.35,smoke_size={0.40,1.45},smoke_slip_min=0.30, smoke_mesh_live=false,smoke_dirty=false,smoke_buffers_live=false, smoke_buffer_bound={},upload_tick=0, remote_wheels={},remote_smoke={},remote_smoke_dirty=false, remote_smoke_mesh_live=false,remote_vfx_cache={}, } local REMOTE_WHEEL_KEYS={rr="wheel_rr",rl="wheel_rl"} local function vfx_net_active() return sdk.net and type(sdk.net.info)=="function" and (sdk.net.info().active==true) end local multiplayer_debug_timer=0 local function refresh_multiplayer_debug(dt) if not sdk.capabilities or (sdk.capabilities.multiplayer_debug or 0)<1 then return end multiplayer_debug_timer=multiplayer_debug_timer-dt if multiplayer_debug_timer>0 then return end multiplayer_debug_timer=0.5 local active=vfx_net_active() local lines={ active and "Multiplayer active" or "Multiplayer offline", state.spawned and "Local car: spawned" or "Local car: not spawned (F10)", state.occupied and "Driver: in car" or "Driver: on foot", vfx.supported and (vfx.mesh and "VFX: buffers ready" or "VFX: buffers pending") or "VFX: unsupported", "Vehicle replication requires matching enabled Skyline packages on each player.", } sdk.ui.multiplayer_debug("replication",table.concat(lines,"\n")) end local function clear_mesh(mesh) mesh.positions={};mesh.normals={};mesh.colors={};mesh.uvs={};mesh.indices={} end local function camera_position() local snap=sdk.snapshot return snap and snap.camera and vec(snap.camera.position) and snap.camera.position end local function billboard_axes(center,cam) local to=sub(cam,center) local dist=math.sqrt(dot(to,to)) if dist<1e-4 then return {1,0,0},{0,1,0},{0,0,-1} end to=mul(to,1/dist) local right=cross({0,1,0},to) local rlen=math.sqrt(dot(right,right)) if rlen<1e-4 then right={1,0,0} else right=mul(right,1/rlen) end local up=cross(to,right) return right,up,to end local function push_rgba(mesh,a) mesh.colors[#mesh.colors+1]={1,1,1,a} end local function append_smoke_sprite(mesh,center,right,up,fwd,size,alpha) local hs=size*0.5 local r=mul(right,hs); local u=mul(up,hs) local base=#mesh.positions local n={-fwd[1],-fwd[2],-fwd[3]} mesh.positions[#mesh.positions+1]=sub(sub(center,r),u) mesh.positions[#mesh.positions+1]=add(sub(center,r),u) mesh.positions[#mesh.positions+1]=add(add(center,r),u) mesh.positions[#mesh.positions+1]=sub(add(center,r),u) mesh.uvs[#mesh.uvs+1]={0,1}; mesh.uvs[#mesh.uvs+1]={1,1} mesh.uvs[#mesh.uvs+1]={1,0}; mesh.uvs[#mesh.uvs+1]={0,0} for _=1,4 do mesh.normals[#mesh.normals+1]=n; push_rgba(mesh,alpha) end mesh.indices[#mesh.indices+1]=base; mesh.indices[#mesh.indices+1]=base+1; mesh.indices[#mesh.indices+1]=base+2 mesh.indices[#mesh.indices+1]=base; mesh.indices[#mesh.indices+1]=base+2; mesh.indices[#mesh.indices+1]=base+3 end local function retire_skid_chunk(chunk) if not chunk then return end if chunk.live then sdk.graphics.remove(chunk.key) end chunk.live=false end local function clear_session_skids() for _,w in ipairs(wheels) do if w.skid_chunks then for _,chunk in ipairs(w.skid_chunks) do retire_skid_chunk(chunk) end end w.skid_chunks=nil; w.skid_pts=nil; w.skid_meshed=nil; w.skid_u=nil w.skid_active=nil w.smoke_accum=nil end vfx.skid_dirty=false end local function clear_smoke() vfx.smoke=nil vfx.smoke_dirty=false if vfx.mesh then vfx.mesh.smoke={positions={},normals={},colors={},uvs={},indices={}} end if vfx.smoke_mesh_live then sdk.graphics.set_visible("vfx_smoke",false) vfx.smoke_mesh_live=false end end local function clear_remote_vfx_cache(peer) if vfx.remote_vfx_cache then vfx.remote_vfx_cache[peer]=nil end end local function clear_remote_vfx(peer) if not vfx.supported then return end local wheels_list=vfx.remote_wheels and vfx.remote_wheels[peer] if wheels_list then for _,w in ipairs(wheels_list) do if w.skid_chunks then for _,chunk in ipairs(w.skid_chunks) do retire_skid_chunk(chunk) end end end vfx.remote_wheels[peer]=nil end if vfx.remote_smoke then vfx.remote_smoke[peer]=nil end clear_remote_vfx_cache(peer) vfx.remote_smoke_dirty=true end local function clear_all_remote_vfx() if not vfx.remote_wheels then return end for peer in pairs(vfx.remote_wheels) do clear_remote_vfx(peer) end vfx.remote_wheels={} vfx.remote_smoke={} vfx.remote_vfx_cache={} vfx.remote_smoke_dirty=false vfx.remote_smoke_mesh_live=false if vfx.mesh then vfx.mesh.remote_smoke={positions={},normals={},colors={},uvs={},indices={}} end end local function invalidate_smoke_buffers() vfx.smoke_buffer_bound={} vfx.smoke_buffers_live=false end local function destroy_smoke_buffers() if not vfx.smoke_buffers_live and not vfx.smoke_buffer_bound.vfx_smoke and not vfx.smoke_buffer_bound.vfx_smoke_remote then return end if vfx.supported then sdk.graphics.remove("vfx_smoke") sdk.graphics.remove("vfx_smoke_remote") end invalidate_smoke_buffers() vfx.smoke_mesh_live=false vfx.remote_smoke_mesh_live=false vfx.mesh=nil end local function clear_effects() clear_smoke(); clear_all_remote_vfx() end local function flatten_forward(fwd,normal) if not vec(fwd) or not vec(normal) then return fwd end local flat=sub(fwd,mul(normal,dot(fwd,normal))) return norm(flat) or fwd end local function segment_forward(pt_a,pt_b) if not pt_a or not pt_b or not vec(pt_a.pos) or not vec(pt_b.pos) then return nil end return flatten_forward(sub(pt_b.pos,pt_a.pos),pt_b.normal or pt_a.normal) end local function new_skid_delta() return {positions={},uvs={},indices={}} end local SKID_TEX_REPEAT=1.8 local function push_skid_vertex_color(delta,alpha) delta.colors=delta.colors or {} local a=clamp(number(alpha,SKID_ALPHA_MIN),SKID_ALPHA_MIN,SKID_ALPHA_MAX) delta.colors[#delta.colors+1]=1 delta.colors[#delta.colors+1]=1 delta.colors[#delta.colors+1]=1 delta.colors[#delta.colors+1]=a end local function sane_point(p) if not vec(p) then return false end for i=1,3 do local v=p[i] if v~=v or v<-100000 or v>100000 then return false end end return true end local function skid_uv_u(u) -- Host rejects UVs outside [-1000, 1000]; wrap so long drifts stay valid. local v=number(u,0)%64.0 if v<-1000 then v=-1000 elseif v>1000 then v=1000 end return v end local function push_cross_section(delta,pt,forward,width) local lateral=norm(cross(forward,pt.normal)); if not lateral then return false end if not sane_point(pt.pos) then return false end local half=width*0.5 local u=skid_uv_u(pt.u) local a=pt.intensity or SKID_ALPHA_MIN delta.positions[#delta.positions+1]=add(pt.pos,mul(lateral,half)) push_skid_vertex_color(delta,a) delta.uvs[#delta.uvs+1]=u; delta.uvs[#delta.uvs+1]=0 delta.positions[#delta.positions+1]=sub(pt.pos,mul(lateral,half)) push_skid_vertex_color(delta,a) delta.uvs[#delta.uvs+1]=u; delta.uvs[#delta.uvs+1]=1 return true end local function active_skid_chunk(w) local chunks=w.skid_chunks if not chunks then return nil end local chunk=chunks[#chunks] if chunk and not chunk.sealed then return chunk end return nil end local function reset_skid_pending(w) local chunk=active_skid_chunk(w) if chunk then chunk.pending=new_skid_delta() end end local function skid_delta_valid(chunk,delta) if not delta or #delta.positions==0 then return true end if #delta.indices<3 then return false end if delta.colors and #delta.colors~=#delta.positions*4 then return false end if delta.uvs and #delta.uvs~=#delta.positions*2 then return false end local total=chunk.verts+#delta.positions for _,idx in ipairs(delta.indices) do if idx=total then return false end end return total<=SKID_CHUNK_MAX_VERTS end local function flush_skid_delta(chunk) local delta=chunk.pending if not delta or #delta.positions==0 then return true end if #delta.indices<3 then chunk.pending=new_skid_delta() return true end if not skid_delta_valid(chunk,delta) then chunk.pending=new_skid_delta() return false end if not chunk.live then sdk.graphics.mesh_buffer(chunk.key,SKID_BUFFER_OPTS) chunk.live=true vfx.skid_dirty=true end sdk.graphics.mesh_buffer_append(chunk.key,delta) chunk.verts=chunk.verts+#delta.positions chunk.pending=new_skid_delta() return true end local function iter_skid_wheels(remote) local list={} if remote then for _,wheels_list in pairs(vfx.remote_wheels or {}) do for _,w in ipairs(wheels_list) do list[#list+1]=w end end else for _,w in ipairs(wheels) do list[#list+1]=w end end return list end local function count_skid_buffers(remote) local n=0 for _,w in ipairs(iter_skid_wheels(remote)) do if w.skid_chunks then for _,chunk in ipairs(w.skid_chunks) do if chunk.live then n=n+1 end end end end return n end local function evict_oldest_sealed_skid(remote) local victim_wheel,victim_idx=nil,nil local oldest=math.huge for _,w in ipairs(iter_skid_wheels(remote)) do local chunks=w.skid_chunks if chunks then for i,chunk in ipairs(chunks) do if chunk.sealed and (chunk.serial or 0)=cap do if not evict_oldest_sealed_skid(remote) then return nil end end w.skid_chunk_serial=(w.skid_chunk_serial or 0)+1 local serial=w.skid_chunk_serial local key=serial==1 and ("vfx_skid_"..w.key) or ("vfx_skid_"..w.key.."_"..serial) vfx.skid_buffer_serial=(vfx.skid_buffer_serial or 0)+1 chunk={ key=key,verts=0,pending=new_skid_delta(),sealed=false,live=false, serial=vfx.skid_buffer_serial, } w.skid_chunks[#w.skid_chunks+1]=chunk return chunk end local function trim_meshed_skid_points(w) local pts=w.skid_pts if not pts then return end local meshed=w.skid_meshed or 0 while meshed>2 and #pts>SKID_PTS_TAIL do table.remove(pts,1) meshed=meshed-1 end w.skid_meshed=meshed end local function append_wheel_skids(w) local pts=w.skid_pts if not pts or #pts<2 then return end local meshed=w.skid_meshed or 0 if meshed<1 then meshed=1 end local budget=SKID_POINTS_PER_UPLOAD while meshed<#pts and budget>0 do local chunk=ensure_wheel_skid_chunk(w) if not chunk then break end if chunk.verts+4>SKID_CHUNK_MAX_VERTS then reset_skid_pending(w) seal_skid_chunk(chunk) chunk=ensure_wheel_skid_chunk(w) if not chunk then break end end if not append_skid_segment(chunk,pts[meshed],pts[meshed+1],vfx.skid_width) then reset_skid_pending(w) chunk=recover_skid_chunk(w) if not chunk then break end if not append_skid_segment(chunk,pts[meshed],pts[meshed+1],vfx.skid_width) then break end end meshed=meshed+1 w.skid_meshed=meshed budget=budget-1 end trim_meshed_skid_points(w) if meshed<#pts then vfx.skid_dirty=true end end local function maybe_upload_skids() if not vfx.skid_dirty or not vfx.supported then return end for _,w in ipairs(wheels) do append_wheel_skids(w) end for _,wheels_list in pairs(vfx.remote_wheels or {}) do for _,w in ipairs(wheels_list) do append_wheel_skids(w) end end local pending=false local function pending_wheel(w) local pts=w.skid_pts return pts and (w.skid_meshed or 0)<#pts end for _,w in ipairs(wheels) do if pending_wheel(w) then pending=true; break end end if not pending then for _,wheels_list in pairs(vfx.remote_wheels or {}) do for _,w in ipairs(wheels_list) do if pending_wheel(w) then pending=true; break end end if pending then break end end end vfx.skid_dirty=pending end local function fill_smoke_list(mesh,smoke,cam) if not smoke or #smoke==0 then return false end local right,up,fwd=billboard_axes(smoke[#smoke].pos,cam) local count=0 for i=1,#smoke do local p=smoke[i] local t=clamp(p.age/p.life,0,1) local fade=(1-t)^2.85 local strength=clamp(p.intensity or 0.5,0,1) local birth=0.28+0.72*t local alpha=fade*strength*birth if t>=1 or alpha<=0 then goto next end local s=p.size*(0.60+2.05*(t^0.72)) append_smoke_sprite(mesh,p.pos,right,up,fwd,s,clamp(alpha*0.95,0,0.68)) count=count+1 ::next:: end return count>0 end local function fill_smoke_mesh(mesh,cam) clear_mesh(mesh) return fill_smoke_list(mesh,vfx.smoke,cam) end local function fill_remote_smoke_mesh(mesh,cam) clear_mesh(mesh) local count=0 for _,smoke in pairs(vfx.remote_smoke or {}) do if fill_smoke_list(mesh,smoke,cam) then count=count+1 end end return count>0 end local function remote_smoke_intensity(slip) if slip<(vfx.smoke_slip_min or 0.30) then return 0 end local t=clamp((slip-(vfx.smoke_slip_min or 0.30))/(4.0-(vfx.smoke_slip_min or 0.30)),0,1) return clamp(t*t*14,0,14) end local function remote_skid_intensity(slip) if slip<0.08 then return SKID_ALPHA_MIN end local t=clamp(slip/4.0,0,1) return SKID_ALPHA_MIN+(SKID_ALPHA_MAX-SKID_ALPHA_MIN)*t*t end local function wheel_slip(kappa,alpha,usage) return math.max(kappa,alpha*0.06,math.max(0,usage-0.62)*1.8) end local function wheel_skid_intensity(kappa,alpha,usage) local slip=wheel_slip(kappa,alpha,usage) if slip<0.08 then return SKID_ALPHA_MIN end local t=clamp(slip/4.0,0,1) return SKID_ALPHA_MIN+(SKID_ALPHA_MAX-SKID_ALPHA_MIN)*t*t end local function push_skid_point(w,pos,normal,intensity) if not sane_point(pos) or not vec(normal) then return end local pts=w.skid_pts or {} w.skid_pts=pts local u=w.skid_u or 0 local last=pts[#pts] intensity=clamp(number(intensity,SKID_ALPHA_MIN),SKID_ALPHA_MIN,SKID_ALPHA_MAX) if last and vec(last.pos) then local d=sub(pos,last.pos) local dist_sq=dot(d,d) local min=vfx.min_skid_dist if dist_sqmin*2 then local steps=math.min(math.floor(dist/min)-1,6) for s=1,steps do local t=s/(steps+1) pts[#pts+1]={ pos=add(last.pos,mul(d,t)),normal=normal,u=u+du*t, intensity=i0+(intensity-i0)*t, } end end u=u+du end w.skid_u=u pts[#pts+1]={pos=pos,normal=normal,u=u,intensity=intensity} vfx.skid_dirty=true end local function wheel_smoke_intensity(kappa,alpha,usage) local slip=wheel_slip(kappa,alpha,usage) local min_slip=vfx.smoke_slip_min or 0.30 if slipMAX_SMOKE_PARTICLES do table.remove(bucket,1) end end local function spawn_smoke(w,pos,intensity,dt) if intensity<=0 or not sane_point(pos) then return end w.smoke_accum=(w.smoke_accum or 0)+intensity*dt*8 local spawned=0 local bucket=smoke_bucket(w) while w.smoke_accum>=0.32 and spawned=p.life then table.remove(smoke,i) removed=true else local lift=0.55+(p.intensity or 0.5)*0.85 p.vel[2]=p.vel[2]+lift*dt p.pos=add(p.pos,mul(p.vel,dt)) end end return removed end local function smoke_active() if vfx.smoke and #vfx.smoke>0 then return true end for _,smoke in pairs(vfx.remote_smoke or {}) do if smoke and #smoke>0 then return true end end return false end local function tick_all_smoke(dt) if tick_smoke_list(vfx.smoke,dt) then vfx.smoke_dirty=true end for _,smoke in pairs(vfx.remote_smoke or {}) do if tick_smoke_list(smoke,dt) then vfx.remote_smoke_dirty=true end end vfx.upload_tick=(vfx.upload_tick or 0)+1 if smoke_active() and vfx.upload_tick%SMOKE_UPLOAD_INTERVAL==0 then vfx.smoke_dirty=true vfx.remote_smoke_dirty=true end end local function hide_smoke_mesh() if not vfx.smoke_mesh_live then vfx.smoke_dirty=false return end sdk.graphics.set_visible("vfx_smoke",false) vfx.smoke_mesh_live=false vfx.smoke_dirty=false end local function hide_remote_smoke_mesh() if not vfx.remote_smoke_mesh_live then vfx.remote_smoke_dirty=false return end sdk.graphics.set_visible("vfx_smoke_remote",false) vfx.remote_smoke_mesh_live=false vfx.remote_smoke_dirty=false end local function new_remote_wheel(peer,short) return { key="net_"..peer.."_"..short,remote_peer=peer,front=false, skid_chunks={},skid_pts={},skid_meshed=0,skid_u=0, skid_active=false,skid_off_acc=0,skid_chunk_serial=0,smoke_accum=0, } end local function ensure_remote_wheels(peer) vfx.remote_wheels=vfx.remote_wheels or {} local list=vfx.remote_wheels[peer] if list then return list end list={new_remote_wheel(peer,"rr"),new_remote_wheel(peer,"rl")} vfx.remote_wheels[peer]=list return list end local function apply_wheel_vfx(w,kappa,alpha,usage,tread,gn,dt) local skidding=kappa>vfx.skid_kappa or alpha>vfx.skid_alpha or usage>0.68 if not skidding or not vec(tread) or not vec(gn) then return end w.skidding_now=true if not w.skid_active then w.skid_active=true w.skid_pts={} w.skid_meshed=0 reset_skid_pending(w) end push_skid_point(w,add(tread,mul(gn,vfx.skid_lift)),gn,wheel_skid_intensity(kappa,alpha,usage)) if not w.front then spawn_smoke(w,add(tread,mul(gn,0.004)),wheel_smoke_intensity(kappa,alpha,usage),dt) end end local function apply_remote_wheel_vfx(w,slip,tread,gn,dt) local smoke_i=remote_smoke_intensity(slip) if smoke_i<=0 or not vec(tread) or not vec(gn) then return end w.skidding_now=true if not w.skid_active then w.skid_active=true w.skid_pts={} w.skid_meshed=0 reset_skid_pending(w) end push_skid_point(w,add(tread,mul(gn,vfx.skid_lift)),gn,remote_skid_intensity(slip)) spawn_smoke(w,add(tread,mul(gn,0.004)),smoke_i,dt) end local function remote_vfx_sample_key(sample) if type(sample)~="table" or #sample<7 then return "" end return string.format("%.2f|%.2f|%.2f",sample[2],sample[3],sample[4]) end local function remote_vfx_data_key(data) local parts={} for short in pairs(REMOTE_WHEEL_KEYS) do parts[#parts+1]=short.."="..remote_vfx_sample_key(data[short]) end table.sort(parts) return table.concat(parts,";") end local function remote_vfx_stale(peer,data) local key=remote_vfx_data_key(data) local cache=vfx.remote_vfx_cache[peer] if not cache or cache.key~=key then vfx.remote_vfx_cache[peer]={key=key,ticks=0} return false end cache.ticks=cache.ticks+1 return cache.ticks>REMOTE_VFX_STALE_TICKS end local function publish_net_vfx(contacts) if not vfx_net_active() or not state.spawned or not sdk.net.publish then return end local packet={} for _,c in ipairs(contacts) do local w=c.wheel if w and not w.front then local short=w.key=="wheel_rr" and "rr" or (w.key=="wheel_rl" and "rl" or nil) if short then local slip=wheel_slip(math.abs(w.kappa or 0),math.abs(w.alpha or 0),w.usage or 0) if slip>=(vfx.smoke_slip_min or 0.30)*0.85 then local tread=c.point local gn=c.ground_n or (c.n and c.n.axis) if sane_point(tread) and vec(gn) then packet[short]={slip,tread[1],tread[2],tread[3],gn[1],gn[2],gn[3]} end end end end end if next(packet) then sdk.net.publish("vfx",packet) else sdk.net.publish("vfx",nil) end end local function tick_remote_vfx(dt) if not vfx_net_active() or not vfx.supported then if vfx.remote_wheels and next(vfx.remote_wheels) then clear_all_remote_vfx() end return end local info=sdk.net.info() local states=((info.states or {})[sdk.mod_id] or {}) local live={} local peer_count=0 for peer,peer_states in pairs(states) do if tostring(peer)~=tostring(info.local_id) and type(peer_states)=="table" then local data=peer_states.vfx if type(data)=="table" and next(data) then peer_count=peer_count+1 if peer_count>MAX_REMOTE_VFX_PEERS then break end live[peer]=true local wheels_list=ensure_remote_wheels(peer) local stale=remote_vfx_stale(peer,data) for _,w in ipairs(wheels_list) do w.skidding_now=false end if not stale then for short,wheel_key in pairs(REMOTE_WHEEL_KEYS) do local sample=data[short] if type(sample)=="table" and #sample>=7 then local slip=number(sample[1],0) local tread={sample[2],sample[3],sample[4]} local gn={sample[5],sample[6],sample[7]} local w=nil for _,candidate in ipairs(wheels_list) do if candidate.key=="net_"..peer.."_"..short then w=candidate; break end end if w then apply_remote_wheel_vfx(w,slip,tread,gn,dt) end end end end for _,w in ipairs(wheels_list) do if w.skidding_now then w.skid_off_acc=0 elseif w.skid_active then w.skid_off_acc=(w.skid_off_acc or 0)+dt if w.skid_off_acc>=0.15 then end_skid_stroke(w) end end end end end end for peer in pairs(vfx.remote_wheels or {}) do if not live[peer] then clear_remote_vfx(peer) end end end local function refresh_vfx_support() vfx.supported=sdk.graphics and (sdk.graphics.version or 0)>=4 and type(sdk.graphics.mesh_buffer)=="function" and type(sdk.graphics.mesh_buffer_append)=="function" return vfx.supported end local function bind_smoke_buffer(key) vfx.smoke_buffer_bound=vfx.smoke_buffer_bound or {} if vfx.smoke_buffer_bound[key] then return end sdk.graphics.mesh_buffer(key,SMOKE_BUFFER_OPTS) vfx.smoke_buffer_bound[key]=true vfx.smoke_buffers_live=true end local function write_smoke_mesh(key,mesh) bind_smoke_buffer(key) sdk.graphics.mesh_buffer_write(key,mesh) end local function ensure_vfx_mesh() if not refresh_vfx_support() then return false end vfx.smoke=vfx.smoke or {} vfx.remote_smoke=vfx.remote_smoke or {} vfx.remote_wheels=vfx.remote_wheels or {} if not vfx.mesh then vfx.mesh={ smoke={positions={},normals={},colors={},uvs={},indices={}}, remote_smoke={positions={},normals={},colors={},uvs={},indices={}}, } end return true end local function prepare_effects() if not refresh_vfx_support() then return end clear_session_skids() vfx.skid_buffer_serial=0 vfx.smoke={}; vfx.smoke_mesh_live=false; vfx.smoke_dirty=false vfx.remote_wheels={}; vfx.remote_smoke={}; vfx.remote_vfx_cache={} vfx.remote_smoke_dirty=false; vfx.remote_smoke_mesh_live=false; vfx.upload_tick=0 invalidate_smoke_buffers() vfx.mesh={ smoke={positions={},normals={},colors={},uvs={},indices={}}, remote_smoke={positions={},normals={},colors={},uvs={},indices={}}, } for _,w in ipairs(wheels) do w.skid_chunks={} w.skid_pts={} w.skid_meshed=0 w.skid_u=0; w.skid_active=false; w.skid_off_acc=0 w.skid_chunk_serial=0 w.smoke_accum=0 end end local function tick_wheel_vfx(contacts,dt) if not vfx.supported then return end dt=clamp(number(dt,1/60),0,0.1) for _,w in ipairs(wheels) do w.skidding_now=false end for _,c in ipairs(contacts) do local w=c.wheel local kappa=math.abs(w.kappa or 0) local alpha=math.abs(w.alpha or 0) local usage=w.usage or 0 local tread=c.point local gn=c.ground_n or (c.n and c.n.axis) if not w.front and vec(tread) then sound.drift_slip=math.max(sound.drift_slip,wheel_slip(kappa,alpha,usage)) end apply_wheel_vfx(w,kappa,alpha,usage,tread,gn,dt) end for _,w in ipairs(wheels) do if w.skidding_now then w.skid_off_acc=0 elseif w.skid_active then w.skid_off_acc=(w.skid_off_acc or 0)+dt if w.skid_off_acc>=0.15 then end_skid_stroke(w) end end end maybe_upload_skids() publish_net_vfx(contacts) end local function upload_wheel_vfx(event) if not ensure_vfx_mesh() then return end if sdk.snapshot.paused or sdk.snapshot.replay then return end local dt=clamp(number(event and event.dt,1/60),0,0.1) tick_remote_vfx(dt) tick_all_smoke(dt) local cam=camera_position(); if not cam then return end if state.spawned then local smoke=vfx.smoke if not smoke or #smoke==0 then hide_smoke_mesh() elseif vfx.smoke_dirty then if fill_smoke_mesh(vfx.mesh.smoke,cam) then write_smoke_mesh("vfx_smoke",vfx.mesh.smoke) sdk.graphics.set_visible("vfx_smoke",true) vfx.smoke_mesh_live=true vfx.smoke_dirty=false elseif vfx.smoke_mesh_live then hide_smoke_mesh() end end else hide_smoke_mesh() end if vfx_net_active() then local has_remote=false for _,smoke in pairs(vfx.remote_smoke or {}) do if smoke and #smoke>0 then has_remote=true; break end end if not has_remote then hide_remote_smoke_mesh() elseif vfx.remote_smoke_dirty then if fill_remote_smoke_mesh(vfx.mesh.remote_smoke,cam) then write_smoke_mesh("vfx_smoke_remote",vfx.mesh.remote_smoke) sdk.graphics.set_visible("vfx_smoke_remote",true) vfx.remote_smoke_mesh_live=true vfx.remote_smoke_dirty=false elseif vfx.remote_smoke_mesh_live then hide_remote_smoke_mesh() end end else hide_remote_smoke_mesh() clear_all_remote_vfx() end end local function prepare_presentation() presentation.supported=type(sdk.camera.rig)=="function" and type(sdk.ui.canvas)=="function" set_debug_visible(sdk.settings.show_driving_debug==true) if not presentation.supported then sdk.log("Skyline: camera/canvas extension missing. Rebuild with the supplied Driving API source patch.") sdk.ui.text("skyline_presentation", "") else sdk.ui.text("skyline_presentation","") end end local function update_dashboard(event) if not presentation.supported then return end if not state.occupied or not state.spawned or sdk.settings.show_speedometer==false then if presentation.active_hud then sdk.ui.remove("driving_dashboard");presentation.active_hud=false end return end if sdk.snapshot.paused or sdk.snapshot.replay then return end local dt=clamp(number(event and event.dt,1/60),0,0.1) presentation.hud_timer=presentation.hud_timer-dt if presentation.hud_timer>0 then return end presentation.hud_timer=0.05 local mph=sdk.settings.speed_mph==true local speed=math.floor(math.max(0,presentation.speed)*(mph and 2.236936292 or 3.6)+0.5) local rpm=clamp(number(state.rpm,0),0,12000) local amount=clamp(rpm/C.redline_rpm,0,1) local hot=amount>0.94 local accent=hot and {1.0,0.27,0.18,1} or {0.23,0.79,0.98,1} local white={0.96,0.98,1.0,1};local muted={0.60,0.67,0.75,1} local items={} local function rect(key,x,y,w,h,color) items[#items+1]={key=key,type="rect",position={x,y},size={w,h},color=color} end local function text(key,x,y,w,h,value,size,color) items[#items+1]={key=key,type="text",position={x,y},size={w,h},text=value,font_size=size,color=color} end rect("back",0,0,344,180,{0.018,0.026,0.041,0.88}) rect("accent",0,0,344,3,accent) text("title",16,12,180,20,"SKYLINE",13,muted) text("view",250,12,78,20,presentation.mode=="hood" and "HOOD" or "CHASE",13,muted) text("speed",14,31,200,78,tostring(speed),64,white) text("unit",180,82,68,22,mph and "MPH" or "KM/H",13,muted) rect("gear_box",258,39,70,68,{0.075,0.105,0.15,1}) text("gear",277,39,47,65,gear_label(),50,white) text("gear_label",267,109,61,17,"GEAR",10,muted) text("rpm",16,115,158,22,string.format("%d RPM",math.floor(rpm/10+0.5)*10),15,white) text("brake",170,115,82,22,presentation.handbrake and "BRAKE" or (hot and "SHIFT" or ""),13, {1,0.39,0.25,1}) rect("rpm_bg",16,144,312,9,{0.13,0.18,0.23,1}) rect("rpm_fill",16,144,312*amount,9,accent) rect("redline_tick",16+312*0.94,141,2,15,{1,0.31,0.20,1}) text("hint",16,161,312,16,"X / C CAMERA",10,muted) sdk.ui.canvas("driving_dashboard",{ anchor="bottom_left",offset={24,28},size={344,180}, scale=clamp(number(sdk.settings.dashboard_scale,1),0.65,1.5),visible=true,items=items, }) presentation.active_hud=true end -- END DRIVING PRESENTATION local function reset_simulation() state.gear=1; state.rpm=C.idle_rpm; state.steer=0; state.shift=0; state.keyboard_axis=0; state.aero_load=0 state.engine_omega=C.idle_rpm*math.pi/30; state.clutch=0; state.mass_audit=nil for _,w in ipairs(wheels) do w.omega=0; w.load=0; w.length=C.ride_length; w.contact=false; w.slip=0 w.spin=0; w.previous_omega=0; w.visual_steer=0; w.visual_y=0 -- Front/rear rates match STATIC MOMENT BALANCE, not an equal-load floor. local fraction=w.front and ((C.center[3]+1.34703)/wheelbase) or ((1.44360-C.center[3])/wheelbase) w.static_load=C.mass*C.gravity*fraction/2 w.stiffness=w.front and C.front_spring or C.rear_spring w.rest_length=C.ride_length+w.static_load/w.stiffness w.drive_share=(w.front and C.front_drive or 1-C.front_drive)/2 w.alpha=0; w.kappa=0; w.fx=0; w.fy=0; w.usage=0 w.damping=2*C.damping_ratio*math.sqrt(w.stiffness*w.static_load/C.gravity) end end -- Keep the old keys clean on reload: no orphan wheel/carrier bodies or meshes. local function remove_rig() stop_audio(); clear_presentation(); clear_effects(); clear_session_skids(); clear_all_remote_vfx(); destroy_smoke_buffers() if state.occupied or sdk.player.attached()==BODY then sdk.player.detach(); sdk.camera.clear_follow() end sdk.graphics.remove("skyline_visual") for _,w in ipairs(wheels) do sdk.graphics.remove(w.key) sdk.physics.remove(w.key) sdk.physics.remove("carrier_"..w.key) sdk.physics.remove("knuckle_"..w.key) end sdk.physics.remove(BODY) if state.occupied or sdk.player.attached()==BODY then state.enter_after=math.max(state.enter_after,state.time+C.reentry_delay) end state.spawned=false; state.occupied=false; state.spawn_confirm=nil state.enter_requested=false; state.exit_requested=false; state.reenter_on_ready=false end local function ground_at(position) -- Search locally, not from y=500 (which could choose a roof/upper road). local h=sdk.physics.raycast(add(position,{0,3,0}),{0,-1,0}, {max_distance=12,filter="ground"}) return h and vec(h.point) and h.point[2] or nil end local function spawn_rig(request) local player=sdk.player.read() if not vec(player.position) then sdk.log("Skyline: player position unavailable"); return end local heading=request.heading or number(player.heading,0) local q=yaw_rotation(heading) local position=request.position or add(player.position,mul(rotate(q,{0,0,1}),4.5)) local height=-math.huge for _,w in ipairs(wheels) do local off=rotate(q,w.pos) local y=ground_at(add(position,off)) if not y then sdk.log("Skyline: spawn needs map ground beneath all four wheels"); return end height=math.max(height,y+C.radius-off[2]+0.025) end sdk.commands.request("spawn_chassis",{kind="physics_spawn",key=BODY,body={ shape={type="model",path=MODEL,object="skyline_mesh", options={max_hulls=32,resolution=96,concavity=0.0025}}, deformation={yield_speed=0.5,compliance=0.15,radius=1.2,max_displacement=0.80,max_step=0.40,resolution={9,5,17}}, body_type="dynamic",mass=C.mass,position={position[1],height,position[3]}, heading=heading,friction=0.25,ccd=true, -- Native assembly classification; the SDK treats this as metadata. contact_group=8, center_of_mass=C.center,inertia_half_extents=C.inertia_half, linear_damping=0,angular_damping=0, }}) reset_simulation() state.spawn_confirm={ reenter=request.reenter==true, } end local interaction_text local function confirm_spawn() local pending=state.spawn_confirm if not pending then return end local result=sdk.commands.result("spawn_chassis") if result and result.ok and sdk.bodies.read({kind="mod",key=BODY}) then sdk.graphics.mesh("skyline_visual",{path=MODEL,body=BODY,deform_nodes={"skyline_mesh"}}) prepare_effects() state.spawn_confirm=nil state.spawned=true state.enter_after=math.max(state.enter_after,state.time+0.25) start_audio() state.reenter_on_ready=pending.reenter==true return end if not result or result.ok then return end state.spawn_confirm=nil sdk.log("Skyline: chassis spawn failed: "..tostring(result.error)) interaction_text("Car spawn failed: "..tostring(result.error).." | F10: retry") end local function respawn(car,at_car) local request={} if at_car and car then request={position=car.position,heading=yaw(car.rotation),reenter=state.occupied} end remove_rig() -- Spawn on the next tick: queries cannot see this tick's queued spawns. state.pending=request end local function input_state(buttons,dt) local p=sdk.input.pad(); local t=p.triggers or {}; local stick=p.left or {} local up=edge("KeyX") or pressed_button(buttons,0x0200) local dn=edge("KeyZ") or pressed_button(buttons,0x0100) local neutral=edge("KeyN") local throttle,brake,steer,hand=0,1,0,false if state.occupied then if neutral then state.gear=0; state.shift=C.shift_time elseif up and state.gear<6 then state.gear=state.gear+1; state.shift=C.shift_time elseif dn and state.gear> -1 then state.gear=state.gear-1; state.shift=C.shift_time end throttle=math.max(deadzone(clamp(number(t[2],0),0,1),0.04),down("KeyW") and 1 or 0) brake=math.max(deadzone(clamp(number(t[1],0),0,1),0.04),down("KeyS") and 1 or 0) local digital=(down("KeyD") and 1 or 0)-(down("KeyA") and 1 or 0) state.keyboard_axis=approach(state.keyboard_axis,digital,dt/(digital==0 and 0.22 or 0.45)) steer=clamp(deadzone(clamp(number(stick[1],0),-1,1),0.09)+state.keyboard_axis,-1,1) hand=down("ShiftLeft") or down("ShiftRight") or (buttons & 0x2000)~=0 end state.shift=math.max(0,state.shift-dt) return {throttle=throttle,brake=brake,steer=steer,hand=hand,raw_rt=number(t[2],0)} end local function steering_angles(input,dt,speed) -- Driver-input mapping, NOT steering assistance or a tire/force correction. -- More precision near stick centre at speed; FULL +/-40 deg remains available. -- No measured/target sideslip or yaw rate is used. Zero input requests zero. local blend=clamp((number(speed,0)-15)/40,0,1) blend=blend*blend*(3-2*blend) local precision=clamp(number(sdk.settings.steering_precision,1),0,1.5) local exponent=C.steering_exponent+1.25*precision*blend local shaped=(input<0 and -1 or 1)*math.abs(input)^exponent state.steer=approach(state.steer,-shaped*C.steer_limit,C.steer_rate*dt) if math.abs(state.steer)<1e-6 then return {0,0,0,0} end local radius=wheelbase/math.tan(math.abs(state.steer)) local inner=math.atan(wheelbase/(radius-track/2)) local outer=math.atan(wheelbase/(radius+track/2)) if state.steer<0 then return {-outer,-inner,0,0} end return {inner,outer,0,0} end local function torque_curve(rpm) local points={{900,185},{2000,270},{3500,365},{5000,405},{6500,395},{8000,320}} for i=2,#points do if rpm<=points[i][1] then local a,b=points[i-1],points[i] return a[2]+(b[2]-a[2])*clamp((rpm-a[1])/(b[1]-a[1]),0,1) end end return points[#points][2] end local function prepare_powertrain(input,dt,speed) -- A finite-torque engine. Idle is a torque controller, NOT an RPM floor. local rpm=state.engine_omega*30/math.pi local loss=12+0.025*math.abs(state.engine_omega) +(1-input.throttle)*35*clamp((rpm-C.idle_rpm)/2200,0,1) local fuel=clamp((C.redline_rpm-rpm)/350,0,1) local idle=clamp(loss+0.9*(C.idle_rpm-rpm),0,100) local combustion=math.max(input.throttle*torque_curve(rpm)*fuel,idle) local net=combustion-loss state.engine_omega=math.max(0,state.engine_omega+net*dt/C.engine_inertia) -- Automatic clutch pedal; it does not look at tire slip or sideslip. local wanted=clamp((rpm-1000)/1100,0,1) if speed>4 and rpm>1000 then wanted=1 end if (speed<1 and input.throttle==0) or state.shift>0 or state.gear==0 then wanted=0 end state.clutch=approach(state.clutch,wanted,(wanted0 or state.gear==0 then state.clutch=0 end local ratio=gear_ratio() local inverse=1/C.engine_inertia for _,w in ipairs(wheels) do w.shaft_factor=ratio*w.drive_share inverse=inverse+w.shaft_factor*w.shaft_factor/C.wheel_inertia end return {p=0, cap=C.clutch_capacity*state.clutch*dt, inverse=inverse, diff_p=0, ratio=ratio} end local function solve_powertrain(power,dt) -- Projected passive clutch impulse in gearbox coordinates. local relative=state.engine_omega for _,w in ipairs(wheels) do relative=relative-w.shaft_factor*w.omega end local next_p=clamp(power.p+relative/power.inverse,-power.cap,power.cap) local change=next_p-power.p state.engine_omega=state.engine_omega-change/C.engine_inertia for _,w in ipairs(wheels) do w.omega=w.omega+w.shaft_factor*change/C.wheel_inertia end power.p=next_p -- A torque-limited rear clutch LSD, not a forced equal-speed axle. -- Brake/drive force remains entirely a tire result. Front diff is open. local rear_torque=math.abs(power.p*power.ratio*(1-C.front_drive)/dt) local cap=(C.rear_diff_preload+C.rear_diff_ramp*rear_torque)*dt local delta=wheels[3].omega-wheels[4].omega local next_diff=clamp(power.diff_p-delta*C.wheel_inertia/2,-cap,cap) local dp=next_diff-power.diff_p wheels[3].omega=wheels[3].omega+dp/C.wheel_inertia wheels[4].omega=wheels[4].omega-dp/C.wheel_inertia power.diff_p=next_diff end -- Invert a small SPD matrix (1..4 contacts), with partial pivoting. local function inverse_matrix(matrix) local n=#matrix; local a={} for i=1,n do a[i]={} for j=1,n do a[i][j]=matrix[i][j]; a[i][n+j]=(i==j and 1 or 0) end end for k=1,n do local pivot=k for i=k+1,n do if math.abs(a[i][k])>math.abs(a[pivot][k]) then pivot=i end end if math.abs(a[pivot][k])<1e-12 then return nil end a[k],a[pivot]=a[pivot],a[k] local d=a[k][k] for j=1,2*n do a[k][j]=a[k][j]/d end for i=1,n do if i~=k then local factor=a[i][k] for j=1,2*n do a[i][j]=a[i][j]-factor*a[k][j] end end end end local result={} for i=1,n do result[i]={}; for j=1,n do result[i][j]=a[i][n+j] end end return result end local function constraint_axis(q,arm,axis) local torque=cross(arm,axis) local angular=inertia(q,torque,true) return {axis=axis,torque=torque,angular=angular,inv_mass=1/C.mass+dot(torque,angular)} end local function velocity_along(sim,c) return dot(sim.v,c.axis)+dot(sim.w,c.torque) end local function push(sim,c,impulse) for i=1,3 do sim.v[i]=sim.v[i]+c.axis[i]*impulse/C.mass sim.w[i]=sim.w[i]+c.angular[i]*impulse end end local function collect_contacts(car,angles,dt) local result={}; local by_wheel={} local q=car.rotation; local up=rotate(q,{0,1,0}); local direction=mul(up,-1) local center=add(car.position,rotate(q,C.center)) for i,w in ipairs(wheels) do w.contact=false; w.load=0; w.slip=0; w.length=C.droop_length w.alpha=0; w.kappa=0; w.fx=0; w.fy=0; w.usage=0 local local_mount={w.pos[1],w.pos[2]+C.ride_length,w.pos[3]} local mount=add(car.position,rotate(q,local_mount)) local origin=add(mount,mul(up,0.10)) -- Ground-only is intentional: ignores our chassis, skater proxies, -- and other dynamic objects. Dynamic-prop driving is not implemented. local hit=sdk.physics.raycast(origin,direction, {max_distance=0.10+C.droop_length+C.radius,filter="ground"}) if hit and vec(hit.point) and vec(hit.normal) then local n=norm(hit.normal) local alignment=dot(n,up) local distance=dot(sub(hit.point,mount),direction) local length=distance-C.radius/math.max(0.45,alignment) if alignment>0.45 and n[2]>0.1 and length<=C.droop_length then local forward=rotate(q,{math.sin(angles[i]),0,math.cos(angles[i])}) forward=norm(sub(forward,mul(n,dot(forward,n)))) local lateral=norm(cross(n,forward)) -- Spherical contact-envelope approximation; normal force acts ON the road. local hub=sub(mount,mul(up,length)) local point=sub(hub,mul(n,C.radius)) local arm=sub(point,center) local c={wheel=w,wheel_index=i,point=point,ground=hit.point,ground_n=n, alignment=alignment,length=length,compression=w.rest_length-length, pn=0,px=0,py=0,pb=0, n=constraint_axis(q,arm,n), x=constraint_axis(q,arm,forward),y=constraint_axis(q,arm,lateral)} c.xy=dot(c.x.axis,c.y.axis)/C.mass+dot(c.x.torque,c.y.angular) result[#result+1]=c; by_wheel[i]=#result w.contact=true; w.length=length end end end if #result==0 then return result end -- Backward Euler for ALL corner springs + axle anti-roll springs: -- (W + Gamma) p = beta - v_free; p is unilateral and force-limited. -- Gamma = [dt A^-1(D + dt K)A^-1]^-1, beta=Gamma*dt*A^-1*K*x. -- Axle couplings make anti-roll torque equal/opposite, not free downforce. local matrix,bias={},{ } for i,c in ipairs(result) do local partner=by_wheel[c.wheel.mate] local antiroll=c.wheel.front and C.front_antiroll or C.rear_antiroll local k=c.wheel.stiffness local force=k*c.compression local bump=math.max(0,C.bump_length-c.length) if bump>0 then k=k+C.bump_stiffness; force=force+C.bump_stiffness*bump end local d=c.wheel.damping if partner then k=k+antiroll; d=d+C.antiroll_damping force=force+antiroll*(c.compression-result[partner].compression) end matrix[i]={} for j,other in ipairs(result) do local value=0 if i==j then value=d+dt*k elseif j==partner then value=-C.antiroll_damping-dt*antiroll end matrix[i][j]=dt*value/(c.alignment*other.alignment) end bias[i]=dt*force/c.alignment end local gamma=inverse_matrix(matrix) if not gamma then return {} end for i,c in ipairs(result) do c.gamma=gamma[i]; c.beta=0 for j=1,#result do c.beta=c.beta+gamma[i][j]*bias[j] end end return result end local function solve_normal(sim,contacts,index,dt) local c=contacts[index] local residual=velocity_along(sim,c.n)-c.beta for j,other in ipairs(contacts) do residual=residual+c.gamma[j]*other.pn end local new=clamp(c.pn-residual/(c.n.inv_mass+c.gamma[index]),0,C.maximum_load*dt) push(sim,c.n,new-c.pn); c.pn=new end -- Solve the tire contact for a trial scalar compliance. Brake complementarity -- is solved analytically: first test omega=0, otherwise use a saturated brake. -- vx0,vy0,omega0 have THIS contact's accumulated impulses removed. local function tire_candidate(c,g,vx0,vy0,omega0,brake) local a,b,d=c.x.inv_mass+g,c.xy,c.y.inv_mass+g local determinant=a*d-b*b local px=(-d*vx0+b*vy0)/determinant local py=(b*vx0-a*vy0)/determinant local required=C.radius*px-C.wheel_inertia*omega0 if math.abs(required)<=brake then return px,py,required,0 end local pb=clamp(required,-brake,brake) local free_omega=omega0+pb/C.wheel_inertia local sx=vx0-C.radius*free_omega a=a+C.radius*C.radius/C.wheel_inertia determinant=a*d-b*b px=(-d*sx+b*vy0)/determinant py=(b*sx-a*vy0)/determinant return px,py,pb,free_omega-C.radius*px/C.wheel_inertia end local function brush_inverse_scale(ratio) -- Inverse of F/D = 1 - (1 - demand/(3D))^3. -- Rationalized to avoid cancellation as force approaches zero. local root=(1-clamp(ratio,0,1))^(1/3) return 3/(1+root+root*root) end local function solve_tire(sim,c,input,mu,dt) local w=c.wheel local oldx,oldy=c.px,c.py local vx,vy=velocity_along(sim,c.x),velocity_along(sim,c.y) local vx0=vx-c.x.inv_mass*oldx-c.xy*oldy local vy0=vy-c.xy*oldx-c.y.inv_mass*oldy local omega0=w.omega+(C.radius*oldx-c.pb)/C.wheel_inertia local load=c.pn/dt local load_ratio=math.max(0,load/w.static_load) -- Sublinear load sensitivity, with bounded low-load coefficient. local sensitivity=clamp(math.max(load_ratio,0.01)^(C.load_exponent-1),0.65,1.15) local limit=mu*sensitivity*c.pn local fraction=w.front and C.front_brake_fraction or 1-C.front_brake_fraction local brake=(input.brake*C.brake_torque_total*fraction/2 +C.rolling_coefficient*load*C.radius +((input.hand and not w.front) and C.handbrake_torque or 0))*dt local px,py,pb,omega=0,0,clamp(-C.wheel_inertia*omega0,-brake,brake),0 omega=omega0+pb/C.wheel_inertia if limit>1e-8 then local transport=math.max(math.abs(vx),math.abs(w.omega*C.radius)) -- Static-friction limit near rest. Smooth transition to rolling brush. -- This is local tire-contact regularization, never a chassis assist. local blend=clamp((transport-0.15)/0.85,0,1) blend=blend*blend*(3-2*blend) local stiffness=(w.front and C.front_stiffness_per_load or C.rear_stiffness_per_load) *w.static_load*math.max(load_ratio,0.01)^C.load_exponent local base=blend*math.max(transport,0.15)/(stiffness*dt) local low,high=base,3*base px,py,pb,omega=tire_candidate(c,high,vx0,vy0,omega0,brake) local amount=math.sqrt(px*px+py*py) if base==0 and amount<=limit then -- True sticking branch; a parked vehicle can hold a slope. else local sliding=amount>limit if sliding then low=high high=high+(math.sqrt(vx0*vx0+vy0*vy0)+C.radius*math.abs(omega0))/limit+1e-6 end for _=1,C.tire_root_iterations do local g=(low+high)/2 local tx,ty=tire_candidate(c,g,vx0,vy0,omega0,brake) local magnitude=math.sqrt(tx*tx+ty*ty) local increase if sliding then increase=magnitude>limit else increase=g0 then return end state.hud_time=0.10 if not presentation.debug then sdk.ui.text("skyline_status",""); sdk.ui.text("skyline_handling",""); sdk.ui.text("skyline_wheels",""); return end local velocity=unrotate(car.rotation,car.linvel) local local_up=unrotate(car.rotation,{0,1,0}) local roll=math.deg(math.atan(local_up[1],local_up[2])) local beta=math.deg(math.atan(velocity[1],math.max(0.1,math.abs(velocity[3])))) local loaded=0 for _,w in ipairs(wheels) do if w.load>1 then loaded=loaded+1 end end sdk.ui.text("skyline_status",string.format( "Skyline DRIVE | %+.1f km/h | %s | %.0f rpm | T %.0f%% Brake %.0f%% HB %s", velocity[3]*3.6,gear_label(),state.rpm,input.throttle*100,input.brake*100, input.hand and "ON" or "off")) sdk.ui.text("skyline_handling",string.format( "Rack %+.1f deg | beta %+.1f deg | roll %+.1f deg | mu %.2f | load %d/4 | mass %s | aero %.0fN", math.deg(state.steer),beta,roll,number(sdk.settings.tire_friction,1.3),loaded, state.mass_audit or "pending",state.aero_load)) sdk.ui.text("skyline_wheels",string.format( "FL %.0fN a%+.1f k%+.2f | FR %.0fN a%+.1f k%+.2f | RL %.0fN a%+.1f k%+.2f | RR %.0fN a%+.1f k%+.2f | clutch %.0f%%", wheels[1].load,wheels[1].alpha,wheels[1].kappa,wheels[2].load,wheels[2].alpha,wheels[2].kappa, wheels[3].load,wheels[3].alpha,wheels[3].kappa,wheels[4].load,wheels[4].alpha,wheels[4].kappa,state.clutch*100)) end -- Read-only consistency check against NATIVE effective mass. Snapshot `mass` -- is metadata in this engine, so checking that field alone proves nothing. -- This never changes forces, inertia, pose, or velocity to hide a mismatch. local function audit_mass_properties(car) if state.mass_audit~=nil then return end local q=car.rotation local center=add(car.position,rotate(q,C.center)) local probes={ {{0,0,0},{0,1,0},1/C.mass}, {{0,1,0},{0,0,1},1/C.mass+1/I[1]}, {{0,0,1},{1,0,0},1/C.mass+1/I[2]}, {{1,0,0},{0,1,0},1/C.mass+1/I[3]}, } local error=0 for _,probe in ipairs(probes) do local actual=sdk.physics.effective_inv_mass(BODY, add(center,rotate(q,probe[1])),rotate(q,probe[2])) if not finite(actual) then state.mass_audit="unavailable" sdk.log("Skyline: native mass check unavailable; supplied-source mass properties assumed") return end error=math.max(error,math.abs(actual-probe[3])/probe[3]) end state.mass_audit=error<0.01 and "OK" or "MISMATCH" sdk.log(string.format("Skyline: native mass/inertia check %s (maximum relative error %.3f%%)", state.mass_audit,100*error)) end -- Explicit aerodynamic surface loads. Cl*A values are editable ASSUMPTIONS, -- not measured R34 data. Positive values mean downforce, negative mean lift. -- Forces act at fore/aft locations and enter the existing coupled solver. -- No contact-count condition, global-down force, or tire-friction multiplier. local function apply_aerodynamics(sim,car,dt) state.aero_load=0 if sdk.settings.aero_enabled==false then return end local front=clamp(number(sdk.settings.aero_front_area,0.08),-0.4,0.6) local rear=clamp(number(sdk.settings.aero_rear_area,0.12),-0.4,0.6) local up=rotate(car.rotation,{0,1,0}) local forward=rotate(car.rotation,{0,0,1}) local surface_positions={{0,0.04,1.44360},{0,0.16,-1.34703}} -- Evaluate both surfaces at the same state; avoid update-order dependence. local air_linear,air_angular=sim.v,sim.w for i,area in ipairs({front,rear}) do local arm=rotate(car.rotation,sub(surface_positions[i],C.center)) local velocity=add(air_linear,cross(air_angular,arm)) local speed2=dot(velocity,velocity) if speed2>0.01 and area~=0 then local along=dot(velocity,forward) -- Project body-down perpendicular to local airflow: lift does not -- deliberately add thrust or oppose the body's roll orientation. local flow=mul(velocity,1/math.sqrt(speed2)) local direction=sub(mul(up,-1),mul(flow,dot(mul(up,-1),flow))) local projected2=dot(direction,direction) if projected2>1e-8 then local force=mul(direction,(0.5*1.225*along*along*area)/math.sqrt(projected2)) local impulse=mul(force,dt) sim.v=add(sim.v,mul(impulse,1/C.mass)) sim.w=add(sim.w,inertia(car.rotation,cross(arm,impulse),true)) state.aero_load=state.aero_load-dot(force,up) end end end end local function quat_mul(a,b) return { a[4]*b[1]+a[1]*b[4]+a[2]*b[3]-a[3]*b[2], a[4]*b[2]-a[1]*b[3]+a[2]*b[4]+a[3]*b[1], a[4]*b[3]+a[1]*b[2]-a[2]*b[1]+a[3]*b[4], a[4]*b[4]-a[1]*b[1]-a[2]*b[2]-a[3]*b[3], } end local function update_wheel_visuals(angles,dt) for i,w in ipairs(wheels) do -- The solver owns omega and suspension length. These are read-only -- presentation values: no extra rigid body, force or steering assist. w.spin=(w.spin+0.5*(w.previous_omega+w.omega)*dt)%(2*math.pi) local steer=angles[i] local y=C.ride_length-w.length local steer_q=yaw_rotation(steer) local spin_q={math.sin(w.spin/2),0,0,math.cos(w.spin/2)} local axle=rotate(steer_q,{w.omega,0,0}) sdk.graphics.node_transform("skyline_visual",w.key,{ position={0,y,0}, rotation=quat_mul(steer_q,spin_q), relative=true, linear_velocity={0,(y-w.visual_y)/dt,0}, angular_velocity={axle[1],(steer-w.visual_steer)/dt,axle[3]}, }) w.previous_omega=w.omega; w.visual_y=y; w.visual_steer=steer end end interaction_text=function(text) if state.interaction_text~=text then state.interaction_text=text sdk.ui.text("skyline_interaction", "") if type(sdk.ui.canvas)=="function" then if text=="" then sdk.ui.remove("skyline_interaction_prompt") else sdk.ui.canvas("skyline_interaction_prompt", { anchor="bottom_left",offset={24,216},size={540,34},visible=true, items={{key="prompt",type="text",position={8,6},size={524,28}, text=text,font_size=18,color={0.96,0.98,1.0,1}}}, }) end end end end local function near_door(car) local player=sdk.player.read() if not vec(player.position) then return false end local p=unrotate(car.rotation,sub(player.position,car.position)) if p[2]<-1.3 or p[2]>1.5 then return false end for _,side in ipairs({-1,1}) do local dx,dz=p[1]-side*1.65,p[3]+0.20 if dx*dx+dz*dz <= C.entry_radius*C.entry_radius then return true end end return false end local function request_entry() if state.enter_requested or sdk.player.detaching() or state.time0.1 then return end state.time=state.time+dt sync_occupancy() local p=sdk.input.pad(); local buttons=math.floor(number(p.buttons,0)) -- Sample edges unconditionally; held keys must not become new presses -- merely because another shortcut returned early this tick. local spawn=edge("F10") local reset_key=edge("KeyR") local enter_key=edge("KeyE") local camera_key=edge("KeyC") local debug_key=edge("KeyH") local hull_key=edge("KeyJ") if hull_key then set_collision_debug(not state.hull_debug) end local camera_pressed=camera_key or pressed_button(buttons,0x4000) if debug_key then set_debug_visible(not presentation.debug) end local enter=enter_key or pressed_button(buttons,0x8000) local reset=reset_key or pressed_button(buttons,0x0080) local car=sdk.physics.read(BODY) confirm_spawn() if spawn then respawn(nil,false) elseif state.pending then local request=state.pending; state.pending=nil; spawn_rig(request) elseif car and state.spawned then if reset then respawn(car,true) else if state.reenter_on_ready and state.time>=state.enter_after and not sdk.player.detaching() then request_entry() end if enter then if state.occupied then request_exit() elseif state.time>=state.enter_after and near_door(car) then request_entry() end end if not state.occupied then local remaining=math.max(0,state.enter_after-state.time) if sdk.player.detaching() then interaction_text("Moving to a clear on-foot exit...") elseif remaining>0 then interaction_text(string.format("Re-entry: %.1f s | J: collision hull",remaining)) elseif near_door(car) then interaction_text("E / Y: enter | J: collision hull") else interaction_text("Approach a door to enter | J: collision hull") end elseif not state.exit_requested and type(sdk.player.detach_error())~="string" then interaction_text("E / Y: exit | J: collision hull") end if camera_pressed and state.occupied then presentation.mode=presentation.mode=="chase" and "hood" or "chase" configure_camera();presentation.hud_timer=0 end simulate(car,input_state(buttons,dt),dt) end end state.buttons=buttons end, on_event=function(event) if event.name=="world_changed" then remove_rig(); stop_audio(); clear_presentation(); sound.prepared=false; prepare_audio() state.pending=nil; state.spawn_confirm=nil state.enter_requested=false; state.exit_requested=false; state.reenter_on_ready=false state.enter_after=state.time+C.reentry_delay; interaction_text("") set_collision_debug(state.hull_debug) state.keys={}; state.buttons=0; reset_simulation() sdk.ui.text("skyline_status",""); sdk.ui.text("skyline_handling",""); sdk.ui.text("skyline_wheels","") sdk.ui.text("skyline_mp","") end end, }