--[[ XP12_TireSideForce.lua FlyWithLua NG+ script for X-Plane 12 Adds extra lateral / self-centering force on the ground so castering nosewheel aircraft (stock Cirrus SR22 and others) track straighter instead of feeling like they are on ice. This is an approximation of pneumatic tire cornering force + aligning torque. It does NOT replace X-Plane's own tire model. Differential braking still works so you can still pivot 180 degrees on one locked brake. INSTALL 1. Install FlyWithLua NG+ for X-Plane 12 if you do not already have it. 2. Copy this file into: X-Plane 12/Resources/plugins/FlyWithLua/Scripts/ 3. Start X-Plane, or use Plugins > FlyWithLua > Reload all Lua files. 4. Taxi the SR22. If it is too sticky, lower LATERAL_GAIN. If it still feels loose, raise LATERAL_GAIN a little. TUNE LATERAL_GAIN -- overall strength. Start at 1.0 MAX_FORCE_N -- safety cap in newtons MAX_YAW_NM -- safety cap in newton-meters BRAKE_UNLOCK -- how much locked-brake turns reduce the centering --]] -- ============================================================ -- USER SETTINGS -- ============================================================ -- 1.0 is a conservative starting point for a ~3400 lb SR22. -- Try 0.6 if it feels glued. Try 1.4 to 1.8 if it still weaves. local LATERAL_GAIN = 1.0 -- Newtons of extra side force at the airframe. Cap prevents wild spikes. local MAX_FORCE_N = 8000.0 -- Newton-meters of extra yaw restoring moment. local MAX_YAW_NM = 12000.0 -- When either toe brake is above this, reduce centering so you can -- still spin the plane around a locked main wheel. local BRAKE_UNLOCK = 0.55 -- Only apply below this groundspeed in knots. Above that, rudder -- and X-Plane's own model should be enough. local MAX_GS_KTS = 55.0 -- Skip tiny slip so you do not get a twitch while parked. local MIN_SLIP_RAD = 0.008 -- Limit which aircraft this runs on. -- true = only the names in ALLOWED_AIRCRAFT -- false = every airplane local LIMIT_TO_LIST = true -- Case-insensitive substrings matched against AIRCRAFT_FILENAME -- and PLANE_ICAO. Add more types if you want the same feel on -- the SF50, RV-10, etc. local ALLOWED_AIRCRAFT = { "SR22", "Cirrus", "SR20", } -- ============================================================ -- DATAREFS -- ============================================================ dataref("dr_paused", "sim/time/paused", "readonly") dataref("dr_onground", "sim/flightmodel/failures/onground_any", "readonly") dataref("dr_gs", "sim/flightmodel/position/groundspeed", "readonly") dataref("dr_vx", "sim/flightmodel/position/local_vx", "readonly") dataref("dr_vy", "sim/flightmodel/position/local_vy", "readonly") dataref("dr_vz", "sim/flightmodel/position/local_vz", "readonly") dataref("dr_psi", "sim/flightmodel/position/psi", "readonly") dataref("dr_r", "sim/flightmodel/position/R", "readonly") dataref("dr_mass", "sim/flightmodel/weight/m_total", "readonly") dataref("dr_l_brake", "sim/flightmodel/controls/l_brake_add", "readonly") dataref("dr_r_brake", "sim/flightmodel/controls/r_brake_add", "readonly") dataref("dr_park", "sim/cockpit2/controls/parking_brake_ratio", "readonly") dataref("dr_fside", "sim/flightmodel/forces/fside_plug_acf", "writable") dataref("dr_nyaw", "sim/flightmodel/forces/N_plug_acf", "writable") -- ============================================================ -- HELPERS -- ============================================================ local function aircraft_allowed() if not LIMIT_TO_LIST then return true end local name = "" if AIRCRAFT_FILENAME ~= nil then name = string.lower(AIRCRAFT_FILENAME) end local icao = "" if PLANE_ICAO ~= nil then icao = string.lower(PLANE_ICAO) end for i = 1, #ALLOWED_AIRCRAFT do local key = string.lower(ALLOWED_AIRCRAFT[i]) if string.find(name, key, 1, true) or string.find(icao, key, 1, true) then return true end end return false end local script_enabled = aircraft_allowed() local function clamp(v, lo, hi) if v < lo then return lo end if v > hi then return hi end return v end local function zero_plugin_forces() dr_fside = 0 dr_nyaw = 0 end -- Heading psi is degrees true. OpenGL local axes: +Z is south, +X is east. -- Body-axis forward speed and rightward speed from world velocity. local function body_speeds() local psi_rad = math.rad(dr_psi or 0) local vx = dr_vx or 0 local vz = dr_vz or 0 -- aircraft nose direction in OpenGL XZ local nose_x = math.sin(psi_rad) local nose_z = -math.cos(psi_rad) local right_x = math.cos(psi_rad) local right_z = math.sin(psi_rad) local v_fwd = vx * nose_x + vz * nose_z local v_right = vx * right_x + vz * right_z return v_fwd, v_right end -- ============================================================ -- MAIN -- ============================================================ function tire_side_force_update() if not script_enabled then return end if dr_paused == 1 then zero_plugin_forces() return end if dr_onground ~= 1 then zero_plugin_forces() return end if (dr_park or 0) > 0.8 then zero_plugin_forces() return end local gs_kts = (dr_gs or 0) * 1.94384 if gs_kts < 0.4 or gs_kts > MAX_GS_KTS then zero_plugin_forces() return end local v_fwd, v_right = body_speeds() local speed = math.sqrt(v_fwd * v_fwd + v_right * v_right) if speed < 0.2 then zero_plugin_forces() return end -- Slip angle: positive means velocity is to the right of the nose. local slip = math.atan2(v_right, math.max(v_fwd, 0.05)) if math.abs(slip) < MIN_SLIP_RAD then zero_plugin_forces() return end -- Soft saturation so large slip (tight pivot) does not explode force. local slip_sat = math.tanh(slip / 0.18) -- Extra restoring from yaw rate so a sudden swing damps out. local yaw_rate = dr_r or 0 local damp = clamp(yaw_rate * 0.35, -1.5, 1.5) local mass = dr_mass or 1500 if mass < 400 then mass = 400 end -- Scale with weight so a heavier plane gets more force, still capped. local side_n = -slip_sat * mass * 6.5 * LATERAL_GAIN local yaw_nm = (-slip_sat * mass * 4.0 - damp * mass * 2.5) * LATERAL_GAIN -- Ease off when one brake is locked so you can still do a 180. local lb = dr_l_brake or 0 local rb = dr_r_brake or 0 local max_brake = lb if rb > max_brake then max_brake = rb end if math.abs(lb - rb) > 0.25 and max_brake > BRAKE_UNLOCK then local fade = 1.0 - clamp((max_brake - BRAKE_UNLOCK) / (1.0 - BRAKE_UNLOCK), 0, 0.85) side_n = side_n * fade yaw_nm = yaw_nm * fade end dr_fside = clamp(side_n, -MAX_FORCE_N, MAX_FORCE_N) dr_nyaw = clamp(yaw_nm, -MAX_YAW_NM, MAX_YAW_NM) end do_every_frame("tire_side_force_update()") if script_enabled then logMsg("XP12_TireSideForce: loaded for this aircraft. LATERAL_GAIN=" .. tostring(LATERAL_GAIN)) else logMsg("XP12_TireSideForce: loaded, but this aircraft is not in ALLOWED_AIRCRAFT. Idle.") end