11using CommandTerminal ;
22using DV . HUD ;
33using DV . Simulation . Cars ;
4+ using DV . Simulation . Controllers ;
45using System ;
56using System . Collections ;
67using System . Collections . Generic ;
78using System . Linq ;
9+ using System . Reflection ;
810using System . Runtime . CompilerServices ;
911using System . Text ;
1012using System . Threading . Tasks ;
@@ -42,6 +44,29 @@ public class LocoCruiseControl : IDisposable
4244 private InteriorControlsManager _interiorControls ;
4345 private bool _isDM3 ;
4446
47+ // ── Steam (S060 / S282) state ────────────────────────────────────────
48+ private bool _isSteam ;
49+ private BaseControlsOverrider _steamOverrider ;
50+
51+ // Smoothed control positions
52+ private float _steamRegulator = 0f ;
53+ private float _steamCutoff = 0.5f ;
54+ private float _steamBrakeTarget = 0f ;
55+
56+ // Pulse-braking state
57+ private bool _steamPulseBraking = false ;
58+ private float _steamPulseTimer = 0f ;
59+ private bool _steamPulseHigh = false ;
60+
61+ // Averaged steam-chest pressure (sampled every 0.1 s, window = 10)
62+ private readonly Queue < float > _steamPressureSamples = new Queue < float > ( ) ;
63+ private float _steamPressureTimer = 0f ;
64+
65+ // Reflection cache for simFlow.TryGetPort
66+ private SimController _steamSimCtrl ;
67+ private MethodInfo _tryGetPortMI ;
68+ private PropertyInfo _portValueProp ;
69+
4570 // ────────────────────────────────────────────────────────────────────
4671
4772 public LocoCruiseControl ( ILocomotiveRemoteControl remoteControl , TrainCar car = null )
@@ -54,6 +79,11 @@ public LocoCruiseControl(ILocomotiveRemoteControl remoteControl, TrainCar car =
5479 _isDM3 = car . carLivery ? . parentType ? . id == LOCO_DM3 ;
5580 _indicators = car . GetComponentInChildren < LocoIndicatorReader > ( ) ;
5681 // Interior controls loaded lazily (may be null until player enters cab)
82+
83+ string locoId = car . carLivery ? . parentType ? . id ?? "" ;
84+ _isSteam = locoId . Contains ( "S060" ) || locoId . Contains ( "S282" ) ;
85+ if ( _isSteam )
86+ _steamOverrider = car . SimController ? . controlsOverrider ;
5787 }
5888 }
5989
@@ -77,6 +107,10 @@ public bool StartCruiseControl(float targetSpeed)
77107 //https://en.wikipedia.org/wiki/PID_controller
78108 protected float MaintainSpeed ( float targetAcceleration , float dt , float speed , float acceleration )
79109 {
110+ // Steam locos use direct regulator/cutoff/brake control, not the PID
111+ if ( _isSteam )
112+ return MaintainSpeedSteam ( dt , speed ) ;
113+
80114 if ( remoteControl . GetReverserSymbol ( ) == "N" )
81115 {
82116 running = false ;
@@ -220,6 +254,199 @@ private InteriorControlsManager GetInteriorControls()
220254 return _interiorControls ;
221255 }
222256
257+ // ── Steam loco controller ────────────────────────────────────────────
258+ // Mirrors the algorithm from SteamCruiseControl mod's CruiseControlAI:
259+ // - Direct absolute Set() on regulator, cutoff, brake (no PID)
260+ // - Pressure-aware cutoff: reduce steam admission as chest pressure rises
261+ // - Pulse braking: on 2.5 s / off 1.5 s cycle when overspeeding
262+ private float MaintainSpeedSteam ( float dt , float speed )
263+ {
264+ if ( _steamOverrider == null ) return 0f ;
265+
266+ bool forward = TargetSpeed >= 0f ;
267+ float absTarget = Mathf . Abs ( TargetSpeed ) ;
268+ float absSpeed = Mathf . Abs ( speed ) ;
269+
270+ float pressure = GetSteamChestPressure ( ) ;
271+ float avgPressure = UpdateSteamPressureAvg ( pressure , dt ) ;
272+
273+ // ── Full stop ────────────────────────────────────────────────────
274+ if ( absTarget < Mathf . Epsilon )
275+ {
276+ _steamPulseBraking = false ;
277+ SteamSetBrake ( 1f ) ;
278+ SteamSetRegulatorSmooth ( 0f ) ;
279+ SteamSetCutoffSmooth ( 0.5f , dt ) ;
280+ return 0f ;
281+ }
282+
283+ float speedDiff = absSpeed - absTarget ; // positive → too fast
284+
285+ if ( speedDiff > 0.1f )
286+ {
287+ // ── Pulse braking ─────────────────────────────────────────────
288+ if ( ! _steamPulseBraking )
289+ {
290+ _steamPulseBraking = true ;
291+ _steamPulseHigh = true ;
292+ _steamPulseTimer = 0f ;
293+ }
294+
295+ _steamPulseTimer += dt ;
296+ float overshootFactor = Mathf . Clamp01 ( speedDiff / 5f ) ;
297+
298+ const float PULSE_HIGH_DURATION = 2.5f ;
299+ const float PULSE_LOW_DURATION = 1.5f ;
300+
301+ if ( _steamPulseHigh )
302+ {
303+ if ( _steamPulseTimer >= PULSE_HIGH_DURATION )
304+ { _steamPulseHigh = false ; _steamPulseTimer = 0f ; }
305+ }
306+ else
307+ {
308+ if ( _steamPulseTimer >= PULSE_LOW_DURATION )
309+ { _steamPulseHigh = true ; _steamPulseTimer = 0f ; }
310+ }
311+
312+ float brakeVal = _steamPulseHigh ? Mathf . Lerp ( 0.7f , 1f , overshootFactor ) : 0f ;
313+ SteamSetBrake ( brakeVal ) ;
314+ SteamSetRegulatorSmooth ( 0f ) ;
315+ // Cutoff to neutral while braking — avoids steam fighting brakes
316+ SteamSetCutoffSmooth ( forward ? 0.5f : 0.49f , dt ) ;
317+ }
318+ else
319+ {
320+ // ── Not braking ───────────────────────────────────────────────
321+ _steamPulseBraking = false ;
322+ _steamPulseHigh = false ;
323+ SteamSetBrake ( 0f ) ;
324+
325+ if ( absSpeed < absTarget - 0.1f )
326+ {
327+ // ── Accelerating ──────────────────────────────────────────
328+ float targetRegulator , targetCutoff ;
329+
330+ if ( absSpeed < 10f )
331+ {
332+ // Gentle start — ramp up regulator, full cutoff
333+ targetRegulator = 0.1f + 0.9f * ( absSpeed / 10f ) ;
334+ targetCutoff = forward ? 1f : 0f ;
335+ }
336+ else if ( absSpeed < 20f )
337+ {
338+ // Full regulator, full cutoff until up to speed
339+ targetRegulator = 1f ;
340+ targetCutoff = forward ? 1f : 0f ;
341+ }
342+ else if ( avgPressure < 5f )
343+ {
344+ // Low chest pressure: wide-open cutoff to admit more steam
345+ targetRegulator = 1f ;
346+ targetCutoff = forward ? 0.35f : 0.15f ;
347+ }
348+ else if ( avgPressure < 8f )
349+ {
350+ targetRegulator = 1f ;
351+ targetCutoff = forward
352+ ? ( 0.4f + 0.1f * ( avgPressure - 5f ) / 3f )
353+ : 0.25f ;
354+ }
355+ else if ( avgPressure < 12f )
356+ {
357+ targetRegulator = 1f ;
358+ float t = ( avgPressure - 8f ) / 4f ;
359+ targetCutoff = forward ? ( 0.55f + 0.25f * t ) : 0.35f ;
360+ }
361+ else
362+ {
363+ // Good pressure: efficient cutoff (~75%)
364+ targetRegulator = 1f ;
365+ targetCutoff = forward ? 0.75f : 0.25f ;
366+ }
367+
368+ SteamSetRegulatorSmooth ( targetRegulator ) ;
369+ SteamSetCutoffSmooth ( targetCutoff , dt ) ;
370+ }
371+ else
372+ {
373+ // ── Coasting at target speed ──────────────────────────────
374+ // Keep tiny regulator so steam flows (engine sound + steam heat)
375+ SteamSetRegulatorSmooth ( 0.1f ) ;
376+ SteamSetCutoffSmooth ( 0.5f , dt ) ;
377+ }
378+ }
379+
380+ return 0f ;
381+ }
382+
383+ private void SteamSetRegulatorSmooth ( float target )
384+ {
385+ _steamRegulator = Mathf . Lerp ( _steamRegulator , target , 0.15f ) ;
386+ _steamOverrider ? . Throttle ? . Set ( _steamRegulator ) ;
387+ }
388+
389+ // cutoff 0 = full reverse, 0.5 = neutral, 1 = full forward
390+ private void SteamSetCutoffSmooth ( float target , float dt )
391+ {
392+ float delta = target - _steamCutoff ;
393+ if ( Mathf . Abs ( delta ) > 0.005f )
394+ _steamCutoff += delta * 0.15f * dt ;
395+ else
396+ _steamCutoff = target ;
397+
398+ _steamOverrider ? . Reverser ? . Set ( _steamCutoff ) ;
399+ }
400+
401+ private void SteamSetBrake ( float value )
402+ {
403+ _steamBrakeTarget = value ;
404+ _steamOverrider ? . Brake ? . Set ( value ) ;
405+ }
406+
407+ private float UpdateSteamPressureAvg ( float currentPressure , float dt )
408+ {
409+ _steamPressureTimer += dt ;
410+ if ( _steamPressureTimer >= 0.1f )
411+ {
412+ _steamPressureSamples . Enqueue ( currentPressure ) ;
413+ _steamPressureTimer = 0f ;
414+ while ( _steamPressureSamples . Count > 10 )
415+ _steamPressureSamples . Dequeue ( ) ;
416+ }
417+
418+ if ( _steamPressureSamples . Count == 0 ) return currentPressure ;
419+
420+ float sum = 0f ;
421+ foreach ( float s in _steamPressureSamples ) sum += s ;
422+ return sum / _steamPressureSamples . Count ;
423+ }
424+
425+ private float GetSteamChestPressure ( )
426+ {
427+ try
428+ {
429+ if ( _steamSimCtrl == null )
430+ _steamSimCtrl = trainCar ? . GetComponentInChildren < SimController > ( ) ;
431+ if ( _steamSimCtrl ? . simFlow == null ) return 8f ;
432+
433+ if ( _tryGetPortMI == null )
434+ _tryGetPortMI = _steamSimCtrl . simFlow . GetType ( )
435+ . GetMethod ( "TryGetPort" , BindingFlags . Instance | BindingFlags . Public ) ;
436+ if ( _tryGetPortMI == null ) return 8f ;
437+
438+ var args = new object [ ] { "steamEngine.STEAM_CHEST_PRESSURE" , null , true } ;
439+ if ( ! ( bool ) _tryGetPortMI . Invoke ( _steamSimCtrl . simFlow , args ) || args [ 1 ] == null )
440+ return 8f ;
441+
442+ if ( _portValueProp == null )
443+ _portValueProp = args [ 1 ] . GetType ( ) . GetProperty ( "Value" ) ;
444+
445+ return _portValueProp != null ? ( float ) _portValueProp . GetValue ( args [ 1 ] ) - 1f : 8f ;
446+ }
447+ catch { return 8f ; }
448+ }
449+
223450 // ────────────────────────────────────────────────────────────────────
224451
225452 public void Stop ( )
0 commit comments