Fix remote player irradiance and retail renderer updates.

Apply probe SH to online customiser characters after remote bind, restore received sun shadows, and ship Skyline driving, fullscreen, and renderer improvements on the branch.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Ethanw05andCursor committed 2026-09-12 20:05:27 -06:00
1 parent 4a58b0b676
commit e2b85b6489
62 files changed
+5385 -4876

No files matched your search

+155 -74
View File
@@ -59,12 +59,12 @@ local I = {
C.mass/3 * (C.inertia_half[1]^2 + C.inertia_half[2]^2),
}
local state = {
spawned=false, occupied=false, pending=nil, gear=1,
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, mp_warm=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
@@ -367,22 +367,26 @@ end
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=96
local SKID_PTS_TAIL=40
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=20
local MAX_REMOTE_SKID_BUFFERS=8
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=2
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.03,skid_lift=0.012,
smoke_lifetime=2.0,smoke_size={0.47,1.88},smoke_slip_min=0.30,
smoke_mesh_live=false,smoke_dirty=false,
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_smoke_mesh_live=false,remote_vfx_cache={},
}
local REMOTE_WHEEL_KEYS={rr="wheel_rr",rl="wheel_rl"}
local function vfx_net_active()
@@ -390,36 +394,19 @@ local function vfx_net_active()
end
local function refresh_mp_status(dt)
if not sdk.net or type(sdk.net.info)~="function" then
state.mp_warm=false
sdk.ui.text("skyline_mp","")
return
end
local info=sdk.net.info()
if not info or info.active~=true then
state.mp_warm=false
sdk.ui.text("skyline_mp","")
return
end
if not state.mp_warm then
state.mp_warm=true
if state.spawned then
local car=sdk.physics.read(BODY)
state.pending={
position=car and car.position,
heading=car and yaw(car.rotation),
reenter=state.occupied,
}
remove_rig()
sdk.log("Skyline: respawning vehicle for multiplayer replication")
end
end
local car=sdk.physics.read(BODY)
if state.spawned and not car then state.spawned=false; state.occupied=false end
state.mp_hint_time=math.max(0,(state.mp_hint_time or 0)-dt)
if state.mp_hint_time>0 then return end
state.mp_hint_time=0.5
local lines={
car
state.spawned
and "Your car is spawned and should replicate to other players."
or "Press F10 to spawn your car. Other players cannot see your vehicle until you do.",
"Both players need Skyline enabled with identical mod files and the skyline-driving-update build.",
@@ -470,10 +457,15 @@ local function append_smoke_sprite(mesh,center,right,up,fwd,size,alpha)
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 sdk.graphics.remove(chunk.key) end
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
@@ -482,10 +474,16 @@ local function clear_session_skids()
vfx.skid_dirty=false
end
local function clear_smoke()
if not vfx.supported then return end
sdk.graphics.remove("vfx_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
@@ -493,12 +491,13 @@ local function clear_remote_vfx(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 sdk.graphics.remove(chunk.key) end
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()
@@ -506,12 +505,26 @@ local function clear_all_remote_vfx()
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")
vfx.remote_smoke_mesh_live=false
if vfx.mesh then vfx.mesh.remote_smoke={positions={},normals={},colors={},uvs={},indices={}} end
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()
@@ -601,7 +614,7 @@ local function flush_skid_delta(chunk)
if not chunk.live then
sdk.graphics.mesh_buffer(chunk.key,SKID_BUFFER_OPTS)
chunk.live=true
vfx.smoke_dirty=true
vfx.skid_dirty=true
end
sdk.graphics.mesh_buffer_append(chunk.key,delta)
chunk.verts=chunk.verts+#delta.positions
@@ -647,7 +660,7 @@ local function evict_oldest_sealed_skid(remote)
end
if not victim_wheel then return false end
local chunk=victim_wheel.skid_chunks[victim_idx]
sdk.graphics.remove(chunk.key)
retire_skid_chunk(chunk)
table.remove(victim_wheel.skid_chunks,victim_idx)
return true
end
@@ -863,12 +876,17 @@ local function smoke_bucket(w)
vfx.smoke=vfx.smoke or {}
return vfx.smoke
end
local function trim_smoke_bucket(bucket)
while bucket and #bucket>MAX_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*10
w.smoke_accum=(w.smoke_accum or 0)+intensity*dt*8
local spawned=0
local bucket=smoke_bucket(w)
while w.smoke_accum>=0.24 and spawned<MAX_SMOKE_SPAWN_PER_TICK do
while w.smoke_accum>=0.32 and spawned<MAX_SMOKE_SPAWN_PER_TICK do
w.smoke_accum=w.smoke_accum-0.24
spawned=spawned+1
local t=clamp(intensity/14,0.12,1)
@@ -887,30 +905,42 @@ local function spawn_smoke(w,pos,intensity,dt)
}
if w.remote_peer then vfx.remote_smoke_dirty=true else vfx.smoke_dirty=true end
end
trim_smoke_bucket(bucket)
end
local function tick_smoke_list(smoke,dt)
if not smoke or #smoke==0 then return false end
local dirty=false
local removed=false
for i=#smoke,1,-1 do
local p=smoke[i]
p.age=p.age+dt
if p.age>=p.life then
table.remove(smoke,i)
dirty=true
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))
dirty=true
end
end
return dirty
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
@@ -973,6 +1003,28 @@ local function apply_remote_wheel_vfx(w,slip,tread,gn,dt)
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={}
@@ -1011,18 +1063,21 @@ local function tick_remote_vfx(dt)
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
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
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
if w then apply_remote_wheel_vfx(w,slip,tread,gn,dt) end
end
end
for _,w in ipairs(wheels_list) do
@@ -1039,7 +1094,6 @@ local function tick_remote_vfx(dt)
for peer in pairs(vfx.remote_wheels or {}) do
if not live[peer] then clear_remote_vfx(peer) end
end
maybe_upload_skids()
end
local function refresh_vfx_support()
vfx.supported=sdk.graphics and (sdk.graphics.version or 0)>=4
@@ -1047,18 +1101,28 @@ local function refresh_vfx_support()
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
if vfx.mesh then return true end
vfx.smoke=vfx.smoke or {}
vfx.remote_smoke=vfx.remote_smoke or {}
vfx.remote_wheels=vfx.remote_wheels or {}
vfx.mesh={
smoke={positions={},normals={},colors={},uvs={},indices={}},
remote_smoke={positions={},normals={},colors={},uvs={},indices={}},
}
sdk.graphics.mesh_buffer("vfx_smoke",SMOKE_BUFFER_OPTS)
sdk.graphics.mesh_buffer("vfx_smoke_remote",SMOKE_BUFFER_OPTS)
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()
@@ -1066,14 +1130,13 @@ local function prepare_effects()
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_smoke_dirty=false; vfx.remote_smoke_mesh_live=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={}},
}
sdk.graphics.mesh_buffer("vfx_smoke",SMOKE_BUFFER_OPTS)
sdk.graphics.mesh_buffer("vfx_smoke_remote",SMOKE_BUFFER_OPTS)
for _,w in ipairs(wheels) do
w.skid_chunks={}
w.skid_pts={}
@@ -1123,7 +1186,7 @@ local function upload_wheel_vfx(event)
hide_smoke_mesh()
elseif vfx.smoke_dirty then
if fill_smoke_mesh(vfx.mesh.smoke,cam) then
sdk.graphics.mesh_buffer_write("vfx_smoke",vfx.mesh.smoke)
write_smoke_mesh("vfx_smoke",vfx.mesh.smoke)
sdk.graphics.set_visible("vfx_smoke",true)
vfx.smoke_mesh_live=true
vfx.smoke_dirty=false
@@ -1143,7 +1206,7 @@ local function upload_wheel_vfx(event)
hide_remote_smoke_mesh()
elseif vfx.remote_smoke_dirty then
if fill_remote_smoke_mesh(vfx.mesh.remote_smoke,cam) then
sdk.graphics.mesh_buffer_write("vfx_smoke_remote",vfx.mesh.remote_smoke)
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
@@ -1233,7 +1296,7 @@ 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()
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
@@ -1248,7 +1311,7 @@ local function remove_rig()
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.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)
@@ -1283,9 +1346,26 @@ local function spawn_rig(request)
sdk.graphics.mesh("skyline_visual",{path=MODEL,body=BODY})
prepare_effects()
reset_simulation()
state.spawned=true; state.enter_after=math.max(state.enter_after,state.time+0.25)
start_audio()
state.reenter_on_ready=request.reenter==true
state.spawn_confirm={
deadline=state.time+0.15,
reenter=request.reenter==true,
}
end
local function confirm_spawn()
local pending=state.spawn_confirm
if not pending then return end
if sdk.physics.read(BODY) then
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 state.time<pending.deadline then return end
state.spawn_confirm=nil
sdk.log("Skyline: chassis body missing after spawn — stand on flat ground and retry F10")
interaction_text("Car spawn failed. Stand on flat ground and press F10 again.")
end
local function respawn(car,at_car)
local request={}
@@ -1861,7 +1941,7 @@ return {
end,
on_unload=function()
set_collision_debug(false); interaction_text(""); remove_rig()
clear_session_skids()
destroy_smoke_buffers(); clear_session_skids()
sdk.ui.text("skyline_audio",""); sdk.ui.text("skyline_presentation","")
sdk.ui.text("skyline_help",""); sdk.ui.text("skyline_status",""); sdk.ui.text("skyline_handling",""); sdk.ui.text("skyline_wheels","")
sdk.ui.text("skyline_mp","")
@@ -1886,6 +1966,7 @@ return {
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)
@@ -1920,7 +2001,7 @@ return {
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.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)