diff --git a/DLSv2/Conditions/GlobalConditions.cs b/DLSv2/Conditions/GlobalConditions.cs index acdfe3b..625a052 100644 --- a/DLSv2/Conditions/GlobalConditions.cs +++ b/DLSv2/Conditions/GlobalConditions.cs @@ -79,4 +79,15 @@ private bool IsWeather(string weather) NativeFunction.Natives.GET_CURR_WEATHER_STATE(out uint weather1, out uint weather2, out float pctWeather2); return (weather1 == Game.GetHashKey(weather) && pctWeather2 <= 0.5f) || (weather2 == Game.GetHashKey(weather) && pctWeather2 >= 0.5f); } -} \ No newline at end of file +} + +public class RecordingCondition : GlobalCondition +{ + [XmlAttribute("active")] + public bool IsRecording { get; set; } = true; + + protected override bool Evaluate() + { + return NativeFunction.Natives.IS_REPLAY_RECORDING() == IsRecording; + } +} diff --git a/DLSv2/Conditions/ModeConditions.cs b/DLSv2/Conditions/ModeConditions.cs index 331f77c..775894a 100644 --- a/DLSv2/Conditions/ModeConditions.cs +++ b/DLSv2/Conditions/ModeConditions.cs @@ -3,6 +3,7 @@ namespace DLSv2.Conditions; using Core; +using System.Linq; public class AudioControlGroupCondition : VehicleCondition { @@ -41,12 +42,23 @@ public class LightControlGroupCondition : VehicleCondition public string ControlGroupName { get; set; } [XmlAttribute("active")] - public bool GroupEnabled { get; set; } + public bool GroupEnabled { get; set; } = true; + + [XmlAttribute("any_mode")] + public bool AnyModeInGroup { get; set; } = true; protected override bool Evaluate(ManagedVehicle veh) { - return veh.LightControlGroups.ContainsKey(ControlGroupName) && - veh.LightControlGroups[ControlGroupName].Enabled == GroupEnabled; + if (!veh.LightControlGroups.ContainsKey(ControlGroupName)) + return false; + + var cg = veh.LightControlGroups[ControlGroupName]; + + if (!AnyModeInGroup) + return cg.Enabled == GroupEnabled; + + // If allowed to check any mode in group + return cg.BaseControlGroup.Modes.Any(m => m.Modes.Any(m => veh.LightModes[m].Enabled)) == GroupEnabled; } } diff --git a/DLSv2/Conditions/SequenceConditions.cs b/DLSv2/Conditions/SequenceConditions.cs new file mode 100644 index 0000000..16e8671 --- /dev/null +++ b/DLSv2/Conditions/SequenceConditions.cs @@ -0,0 +1,31 @@ +using System.Xml.Serialization; + +namespace DLSv2.Conditions; + +using Core; +using System.Linq; + +public class SequenceBeatCondition : VehicleCondition +{ + [XmlAttribute("sequence")] + public string Sequence + { + get => string.Concat(boolSequence.Select(x => x ? '1' : '0')); + + set + { + boolSequence = value.Where(c => (c == '1' || c == '0')).Select(c => c == '1').ToArray(); + } + } + + internal bool[] boolSequence; + + protected override bool Evaluate(ManagedVehicle veh) + { + return + boolSequence.Length > 0 + && veh.sirenInstance.TotalSirenBeats >= 0 + && boolSequence[veh.sirenInstance.TotalSirenBeats % boolSequence.Length]; + } +} + diff --git a/DLSv2/Conditions/VehicleStatusConditions.cs b/DLSv2/Conditions/VehicleStatusConditions.cs index 68fc263..3f5bcfe 100644 --- a/DLSv2/Conditions/VehicleStatusConditions.cs +++ b/DLSv2/Conditions/VehicleStatusConditions.cs @@ -11,6 +11,7 @@ namespace DLSv2.Conditions; public class DriverCondition : VehicleCondition { + protected override uint UpdateWait => 100; [XmlAttribute("has_driver")] public bool HasDriver { get; set; } = true; @@ -143,6 +144,8 @@ public class VehicleOwnerCondition : VehicleCondition public class AtTrafficLightCondition : VehicleCondition { + protected override uint UpdateWait => 100; + [XmlAttribute("stopped_at_light")] public bool IsAtTrafficLight { get; set; } diff --git a/DLSv2/Core/DLSModel.cs b/DLSv2/Core/DLSModel.cs index 640c57a..a0ff1e8 100644 --- a/DLSv2/Core/DLSModel.cs +++ b/DLSv2/Core/DLSModel.cs @@ -5,6 +5,7 @@ namespace DLSv2.Core; +using Rage; using Utils; [XmlRoot("Model")] @@ -110,7 +111,7 @@ public class LightMode : BaseMode public List PaintJobs = new(); [XmlElement("SirenSettings", IsNullable = true)] - public SirenSetting SirenSettings = new(); + public SirenSetting SirenSettings; [XmlArray("Sequences", IsNullable = true)] [XmlArrayItem("Item")] @@ -119,34 +120,41 @@ public SequenceItem[] Sequences get => sequences; set { + + foreach (SequenceItem item in value) { - if (SirenSettings == null) SirenSettings = new SirenSetting(); + // Convert ID string to individual siren IDs + var ids = item.IDs == "all" ? Enumerable.Range(1, EmergencyLighting.MaxLights).Select(x => x.ToString()) : item.IDs.Split(','); // Parse siren ID into one or more integers - foreach (string id in item.IDs.Split(',')) + foreach (string id in ids) { // If siren ID string is a head/tail light sequencer, set and continue to next item switch (id) { case "leftHeadLight": - SirenSettings.LeftHeadLightSequencer = new SequencerWrapper(item.Sequence); + if (SirenSettings == null) SirenSettings = new SirenSetting(); + SirenSettings.LeftHeadLightSequencer = new SequencerWrapper(item.StandardSequence); continue; case "rightHeadLight": - SirenSettings.RightHeadLightSequencer = new SequencerWrapper(item.Sequence); + if (SirenSettings == null) SirenSettings = new SirenSetting(); + SirenSettings.RightHeadLightSequencer = new SequencerWrapper(item.StandardSequence); continue; case "leftTailLight": - SirenSettings.LeftTailLightSequencer = new SequencerWrapper(item.Sequence); + if (SirenSettings == null) SirenSettings = new SirenSetting(); + SirenSettings.LeftTailLightSequencer = new SequencerWrapper(item.StandardSequence); continue; case "rightTailLight": - SirenSettings.RightTailLightSequencer = new SequencerWrapper(item.Sequence); + if (SirenSettings == null) SirenSettings = new SirenSetting(); + SirenSettings.RightTailLightSequencer = new SequencerWrapper(item.StandardSequence); continue; } if (int.TryParse(id.Trim(), out int ID)) { - SirenEntry siren = new SirenEntry(ID) { Flashiness = new LightDetailEntry { Sequence = new Sequencer(item.Sequence) } }; - SirenSettings.SirenList.Add(siren); + if (item.IsExtended) ExtendedSequences.Add(ID, item.Sequence); + else StandardSequences.Add(ID, item.Sequence); } else { $"Mode {Name} siren id {id} is invalid".ToLog(LogLevel.ERROR); @@ -159,6 +167,13 @@ public SequenceItem[] Sequences } private SequenceItem[] sequences; + [XmlIgnore] + // key = siren ID, value = extended sequence string + internal Dictionary ExtendedSequences = new(); + + [XmlIgnore] + internal Dictionary StandardSequences = new(); + public override string ToString() => Name; } @@ -239,7 +254,31 @@ public class SequenceItem public string IDs; [XmlAttribute("sequence")] - public string Sequence; + public string Sequence + { + get => sequence; + set + { + // If initial length is less than 32 bits, repeat the sequence until it's over the min length + string origSeq = value; + origSeq = string.Concat(origSeq.Where(x => x == '1' || x == '0')); + if (string.IsNullOrEmpty(origSeq)) origSeq = "0"; + + string seq = origSeq; + + while(seq.Length < 32) + { + seq += origSeq; + } + sequence = seq; + } + } + + private string sequence; + + public bool IsExtended => Sequence.Length > 32; + + public string StandardSequence => Sequence.Substring(0, 32); } public class LightControlGroup : BaseControlGroup diff --git a/DLSv2/Core/ManagedVehicle.cs b/DLSv2/Core/ManagedVehicle.cs index 9a2e0de..2b389a0 100644 --- a/DLSv2/Core/ManagedVehicle.cs +++ b/DLSv2/Core/ManagedVehicle.cs @@ -20,6 +20,9 @@ public ManagedVehicle(Vehicle vehicle) Vehicle = vehicle; VehicleHandle = vehicle.Handle; + sirenInstance = new SirenInstance(vehicle); + + $"Detected DLS vehicle {VehicleHandle.ToString("X")}".ToLog(LogLevel.DEBUG); Entrypoint.DLSModels.TryGetValue(vehicle.Model, out var _dlsModel); if (_dlsModel == null) return; @@ -50,14 +53,19 @@ public ManagedVehicle(Vehicle vehicle) triggersAndRequirements.GetInstance(this).OnInstanceTriggered += (sender, condition, state) => { LightModes[mode.Name].EnabledByTrigger = state; - UpdateLights(); + // $"trigger changed {mode.Name} to {state} by {condition}".ToLog(LogLevel.DEBUG); + lightsNeedUpdate = true; }; // if requirements become false, turn off the mode mode.Requirements.GetInstance(this).OnInstanceTriggered += (sender, condition, state) => { - if (!state) LightModes[mode.Name].EnabledByTrigger = false; - UpdateLights(); + if (!state && (LightModes[mode.Name].Enabled || LightModes[mode.Name].EnabledByTrigger)) + { + LightModes[mode.Name].EnabledByTrigger = false; + // $"requirements changed {mode.Name} to {state} by {condition}".ToLog(LogLevel.DEBUG); + lightsNeedUpdate = true; + } }; } @@ -92,22 +100,44 @@ public ManagedVehicle(Vehicle vehicle) VehicleOwner.OnIsPlayerVehicleChanged += SetIsPlayerOwned; + // Set initial state of all conditions + foreach (BaseCondition condition in Conditions) condition.Update(this); + SetIsPlayerOwned(vehicle, vehicle.IsPlayerVehicle()); } private void SetIsPlayerOwned(Vehicle v, bool isPlayerOwned) { + if (v != Vehicle) return; + if (isPlayerOwned) { v.DisableSirenSounds(); DefaultMode.EnabledByTrigger = false; + if (Vehicle.IsSirenOn) + { + foreach (var cg in LightControlGroups.Values) + { + for (int m = 0; m < cg.BaseControlGroup.Modes.Count; m++) + { + if (cg.BaseControlGroup.Modes[m].Modes.Contains(DefaultMode.BaseMode.Name)) + { + cg.ActiveIndexes.Add(m); + } + } + } + } + $"Vehicle {v.Handle.Value.ToString("X")} ({v.Model.Name}) set to player owned".ToLog(LogLevel.DEBUG); } else { bool silent = v.IsSirenSilent; + bool lights = v.IsSirenOn; ClearAll(); v.EnableSirenSounds(); DefaultMode.EnabledByTrigger = true; v.IsSirenSilent = silent; + v.IsSirenOn = lights; + $"Vehicle {v.Handle.Value.ToString("X")} ({v.Model.Name}) set to non-player owned".ToLog(LogLevel.DEBUG); } UpdateLights(); @@ -119,10 +149,15 @@ private void SetIsPlayerOwned(Vehicle v, bool isPlayerOwned) public Vehicle Vehicle { get; } public DLSModel dlsModel { get; } public uint VehicleHandle { get; } + public EmergencyLighting eL { get; private set; } + + public SirenInstance sirenInstance; + public Dictionary extendedSequences = new(); + public Dictionary ManagedExtras = new Dictionary(); // Managed Extras - ID, original state public Dictionary ManagedPaint = new Dictionary(); // Managed Paint Settings - Paint index, original color code - private bool areLightsOn; public Animation ActiveAnim; + private bool areLightsOn; /// /// Lights @@ -341,7 +376,7 @@ public void RegisterInputs() cG.ActiveIndexes = [index]; else cG.ActiveIndexes.Add(index); - + UpdateAudio(); }; @@ -436,9 +471,14 @@ public void RegisterInputs() } } + internal bool lightsNeedUpdate = false; public void UpdateLights() { if (!Vehicle) return; + lightsNeedUpdate = false; + + // Ensure correct emergency lighting instance is set + eL = Vehicle.GetDLSEmergencyLighting(); // Start with no modes activated List modes = new(); @@ -482,16 +522,16 @@ public void UpdateLights() item.Enabled = false; // If no active modes, clears EL and disables siren - if (modes.Count == 0) + if (modes.Count == 0 || (!Vehicle.IsPlayerVehicle() && !Vehicle.IsSirenOn)) { - if (Vehicle.IsPlayerVehicle() || !Vehicle.IsSirenOn) LightsOn = false; + LightsOn = false; this.ApplyLightModes(new List()); //AudioController.KillSirens(managedVehicle); return; } // Turns on vehicle siren - if (Vehicle.IsPlayerVehicle() || Vehicle.IsSirenOn) LightsOn = true; + LightsOn = true; // Sets EL with appropriate modes this.ApplyLightModes(modes); @@ -500,7 +540,8 @@ public void UpdateLights() public void UpdateAudio() { if (!Vehicle) return; - + if (this != ActivePlayerVehicle) return; + // Start with no modes activated List modes = new(); @@ -562,6 +603,44 @@ public void UpdateAudio() PlayMode(audioMode); } } + + private int lastSeqChangedBeat = -1; + public void ProcessExtendedSequences(bool force = false) + { + // Only process if starting on beat 0, beat 16, or forced + if (sirenInstance.TotalSirenBeats < 0) return; + if (!force && sirenInstance.CurrentSirenBeat % 16 != 0) return; + if (!force && sirenInstance.CurrentSirenBeat == lastSeqChangedBeat) return; + + lastSeqChangedBeat = sirenInstance.CurrentSirenBeat; + + foreach (var seqItem in extendedSequences) + { + if (seqItem.Key > eL.Lights.Length) return; + + string seq = seqItem.Value; + int l = seq.Length; + seq += seq + seq; + string newSeq; + + int a = ((sirenInstance.TotalSirenBeats - sirenInstance.CurrentSirenBeat) % l); + + // update on beat 16 to avoid flickering when the first bit changes + if (sirenInstance.CurrentSirenBeat >= 16) + { + int b = a + 16; + int c = a + 32; + if (c < 0 || b > seq.Length) continue; + newSeq = seq.Substring(c, 16) + seq.Substring(b, 16); + } else + { + newSeq = seq.Substring(a, 32); + } + + int i = seqItem.Key - 1; + if (i < eL.Lights.Length) eL.Lights[i].FlashinessSequence = newSeq; + } + } public void PlayMode(AudioMode mode) { diff --git a/DLSv2/Core/SirenApply.cs b/DLSv2/Core/SirenApply.cs index d16c7cc..9273ef2 100644 --- a/DLSv2/Core/SirenApply.cs +++ b/DLSv2/Core/SirenApply.cs @@ -7,23 +7,23 @@ internal static class SirenApply { public static void ApplySirenSettingsToEmergencyLighting(SirenSetting setting, EmergencyLighting els) { - els.TimeMultiplier = setting.TimeMultiplier?.Value ?? els.TimeMultiplier; - els.LightFalloffMax = setting.LightFalloffMax?.Value ?? els.LightFalloffMax; - els.LightFalloffExponent = setting.LightFalloffExponent?.Value ?? els.LightFalloffExponent; - els.LightInnerConeAngle = setting.LightInnerConeAngle?.Value ?? els.LightInnerConeAngle; - els.LightOuterConeAngle = setting.LightOuterConeAngle?.Value ?? els.LightOuterConeAngle; - els.LightOffset = setting.LightOffset?.Value ?? els.LightOffset; - els.TextureHash = setting.TextureHash ?? els.TextureHash; - els.SequencerBpm = setting.SequencerBPM?.Value ?? els.SequencerBpm; - els.UseRealLights = setting.UseRealLights?.Value ?? els.UseRealLights; - els.LeftHeadLightSequenceRaw = setting.LeftHeadLightSequencer?.Sequencer?.SequenceRaw ?? els.LeftHeadLightSequenceRaw; - els.LeftHeadLightMultiples = setting.LeftHeadLightMultiples?.Value ?? els.LeftHeadLightMultiples; - els.RightHeadLightSequenceRaw = setting.RightHeadLightSequencer?.Sequencer?.SequenceRaw ?? els.RightHeadLightSequenceRaw; - els.RightHeadLightMultiples = setting.RightHeadLightMultiples?.Value ?? els.RightHeadLightMultiples; - els.LeftTailLightSequenceRaw = setting.LeftTailLightSequencer?.Sequencer?.Value ?? els.LeftTailLightSequenceRaw; - els.LeftTailLightMultiples = setting.LeftTailLightMultiples?.Value ?? els.LeftTailLightMultiples; - els.RightTailLightSequenceRaw = setting.RightTailLightSequencer?.Sequencer?.Value ?? els.RightTailLightSequenceRaw; - els.RightTailLightMultiples = setting.RightTailLightMultiples?.Value ?? els.RightTailLightMultiples; + if (setting.TimeMultiplier != null) els.TimeMultiplier = setting.TimeMultiplier.Value; + if (setting.LightFalloffMax != null) els.LightFalloffMax = setting.LightFalloffMax.Value; + if (setting.LightFalloffExponent != null) els.LightFalloffExponent = setting.LightFalloffExponent.Value; + if (setting.LightInnerConeAngle != null) els.LightInnerConeAngle = setting.LightInnerConeAngle.Value; + if (setting.LightOuterConeAngle != null) els.LightOuterConeAngle = setting.LightOuterConeAngle.Value; + if (setting.LightOffset != null) els.LightOffset = setting.LightOffset.Value; + if (setting.TextureHash != null) els.TextureHash = setting.TextureHash.Value; + if (setting.SequencerBPM != null) els.SequencerBpm = setting.SequencerBPM.Value; + if (setting.UseRealLights != null) els.UseRealLights = setting.UseRealLights.Value; + if (setting.LeftHeadLightSequencer != null) els.LeftHeadLightSequenceRaw = setting.LeftHeadLightSequencer.Sequencer.Value; + if (setting.LeftHeadLightMultiples != null) els.LeftHeadLightMultiples = setting.LeftHeadLightMultiples.Value; + if (setting.RightHeadLightSequencer != null) els.RightHeadLightSequenceRaw = setting.RightHeadLightSequencer.Sequencer.Value; + if (setting.RightHeadLightMultiples != null) els.RightHeadLightMultiples = setting.RightHeadLightMultiples.Value; + if (setting.LeftTailLightSequencer != null) els.LeftTailLightSequenceRaw = setting.LeftTailLightSequencer.Sequencer.Value; + if (setting.LeftTailLightMultiples != null) els.LeftTailLightMultiples = setting.LeftTailLightMultiples.Value; + if (setting.RightTailLightSequencer != null) els.RightTailLightSequenceRaw = setting.RightTailLightSequencer.Sequencer.Value; + if (setting.RightTailLightMultiples != null) els.RightTailLightMultiples = setting.RightTailLightMultiples.Value; foreach (SirenEntry entry in setting.Sirens) { @@ -40,40 +40,40 @@ public static void ApplySirenSettingsToEmergencyLighting(SirenSetting setting, E EmergencyLight light = els.Lights[id - 1]; // Main light settings - light.Color = entry.LightColor ?? light.Color; - light.Intensity = entry.Intensity?.Value ?? light.Intensity; - light.LightGroup = entry.LightGroup?.Value ?? light.LightGroup; - light.Rotate = entry.Rotate?.Value ?? light.Rotate; - light.Scale = entry.Scale?.Value ?? light.Scale; - light.ScaleFactor = entry.ScaleFactor?.Value ?? light.ScaleFactor; - light.Flash = entry.Flash?.Value ?? light.Flash; - light.SpotLight = entry.SpotLight?.Value ?? light.SpotLight; - light.CastShadows = entry.CastShadows?.Value ?? light.CastShadows; - light.Light = entry.Light?.Value ?? light.Light; + if (entry.LightColor != null) light.Color = entry.LightColor.Value; + if (entry.Intensity != null) light.Intensity = entry.Intensity.Value; + if (entry.LightGroup != null) light.LightGroup = entry.LightGroup.Value; + if (entry.Rotate != null) light.Rotate = entry.Rotate.Value; + if (entry.Scale != null) light.Scale = entry.Scale.Value; + if (entry.ScaleFactor != null) light.ScaleFactor = entry.ScaleFactor.Value; + if (entry.Flash != null) light.Flash = entry.Flash.Value; + if (entry.SpotLight != null) light.SpotLight = entry.SpotLight.Value; + if (entry.CastShadows != null) light.CastShadows = entry.CastShadows.Value; + if (entry.Light != null) light.Light = entry.Light.Value; // Corona settings - light.CoronaIntensity = entry.Corona.CoronaIntensity?.Value ?? light.CoronaIntensity; - light.CoronaSize = entry.Corona.CoronaSize?.Value ?? light.CoronaSize; - light.CoronaPull = entry.Corona.CoronaPull?.Value ?? light.CoronaPull; - light.CoronaFaceCamera = entry.Corona.CoronaFaceCamera?.Value ?? light.CoronaFaceCamera; + if (entry.Corona.CoronaIntensity != null) light.CoronaIntensity = entry.Corona.CoronaIntensity.Value; + if (entry.Corona.CoronaSize != null) light.CoronaSize = entry.Corona.CoronaSize.Value; + if (entry.Corona.CoronaPull != null) light.CoronaPull = entry.Corona.CoronaPull.Value; + if (entry.Corona.CoronaFaceCamera != null) light.CoronaFaceCamera = entry.Corona.CoronaFaceCamera.Value; // Rotation settings - light.RotationDelta = entry.Rotation.DeltaDeg ?? light.RotationDelta; - light.RotationStart = entry.Rotation.StartDeg ?? light.RotationStart; - light.RotationSpeed = entry.Rotation.Speed?.Value ?? light.RotationSpeed; - light.RotationSequenceRaw = entry.Rotation.Sequence ?? light.RotationSequenceRaw; - light.RotationMultiples = entry.Rotation.Multiples?.Value ?? light.RotationMultiples; - light.RotationDirection = entry.Rotation.Direction?.Value ?? light.RotationDirection; - light.RotationSynchronizeToBpm = entry.Rotation.SyncToBPM?.Value ?? light.RotationSynchronizeToBpm; + if (entry.Rotation.DeltaDeg.HasValue) light.RotationDelta = entry.Rotation.DeltaDeg.Value; + if (entry.Rotation.StartDeg.HasValue) light.RotationStart = entry.Rotation.StartDeg.Value; + if (entry.Rotation.Speed != null) light.RotationSpeed = entry.Rotation.Speed.Value; + if (entry.Rotation.Sequence != null) light.RotationSequenceRaw = entry.Rotation.Sequence; + if (entry.Rotation.Multiples != null) light.RotationMultiples = entry.Rotation.Multiples.Value; + if (entry.Rotation.Direction != null) light.RotationDirection = entry.Rotation.Direction.Value; + if (entry.Rotation.SyncToBPM != null) light.RotationSynchronizeToBpm = entry.Rotation.SyncToBPM.Value; // Flash settings - light.FlashinessDelta = entry.Flashiness.DeltaDeg ?? light.FlashinessDelta; - light.FlashinessStart = entry.Flashiness.StartDeg ?? light.FlashinessStart; - light.FlashinessSpeed = entry.Flashiness.Speed?.Value ?? light.FlashinessSpeed; - light.FlashinessSequenceRaw = entry.Flashiness.Sequence ?? light.FlashinessSequenceRaw; - light.FlashinessMultiples = entry.Flashiness.Multiples?.Value ?? light.FlashinessMultiples; - light.FlashinessDirection = entry.Flashiness.Direction?.Value ?? light.FlashinessDirection; - light.FlashinessSynchronizeToBpm = entry.Flashiness.SyncToBPM?.Value ?? light.FlashinessSynchronizeToBpm; + if (entry.Flashiness.DeltaDeg.HasValue) light.FlashinessDelta = entry.Flashiness.DeltaDeg.Value; + if (entry.Flashiness.StartDeg.HasValue) light.FlashinessStart = entry.Flashiness.StartDeg.Value; + if (entry.Flashiness.Speed != null) light.FlashinessSpeed = entry.Flashiness.Speed.Value; + if (entry.Flashiness.Sequence != null) light.FlashinessSequenceRaw = entry.Flashiness.Sequence; + if (entry.Flashiness.Multiples != null) light.FlashinessMultiples = entry.Flashiness.Multiples.Value; + if (entry.Flashiness.Direction != null) light.FlashinessDirection = entry.Flashiness.Direction.Value; + if (entry.Flashiness.SyncToBPM != null) light.FlashinessSynchronizeToBpm = entry.Flashiness.SyncToBPM.Value; } } } diff --git a/DLSv2/Core/SirenSetting.cs b/DLSv2/Core/SirenSetting.cs index c695d3e..1efb507 100644 --- a/DLSv2/Core/SirenSetting.cs +++ b/DLSv2/Core/SirenSetting.cs @@ -28,15 +28,30 @@ public class SirenSetting [XmlElement("lightOffset", IsNullable = true)] public ValueItem LightOffset { get; set; } - [XmlElement("textureName", IsNullable = true)] - public string TextureName { get; set; } + private string textureName; + private uint? textureHash; + [XmlElement("textureName", IsNullable = true)] + public string TextureName + { + get => textureName; + set + { + textureName = value; + textureHash = Core.TextureHash.StringToHash(value); + } + } + [XmlIgnore] public uint? TextureHash { - get => TextureName != null ? Core.TextureHash.StringToHash(TextureName) : (uint?)null; + get => textureHash; - set => TextureName = value.HasValue ? Core.TextureHash.HashToString(value.Value) : null; + set + { + TextureName = value.HasValue ? Core.TextureHash.HashToString(value.Value) : null; + textureHash = value; + } } [XmlElement("sequencerBpm", IsNullable = true)] @@ -190,7 +205,7 @@ public class LightDetailEntry public float? DeltaDeg { get => DeltaRad == null ? (float?)null : Rage.MathHelper.ConvertRadiansToDegrees(DeltaRad); - set => DeltaRad = value.HasValue ? (float?)null : Rage.MathHelper.ConvertDegreesToRadians(value.Value); + set => DeltaRad = value.HasValue ? Rage.MathHelper.ConvertDegreesToRadians(value.Value) : (float?)null; } [XmlElement("start", IsNullable = true)] @@ -200,7 +215,7 @@ public float? DeltaDeg public float? StartDeg { get => StartRad == null ? (float?) null : Rage.MathHelper.ConvertRadiansToDegrees(StartRad); - set => StartRad = value.HasValue ? (float?)null : Rage.MathHelper.ConvertDegreesToRadians(value.Value); + set => StartRad = value.HasValue ? Rage.MathHelper.ConvertDegreesToRadians(value.Value) : (float?)null; } [XmlElement("speed", IsNullable = true)] @@ -248,6 +263,7 @@ public string Sequence } } +// Only used to embed taillight and headlight sequencers directly into raw carcols format [DebuggerDisplay("{Sequencer.Sequence} = {Sequencer.SequenceRaw}")] public class SequencerWrapper { diff --git a/DLSv2/Core/Wrappers.cs b/DLSv2/Core/Wrappers.cs index 5151c98..6c8da41 100644 --- a/DLSv2/Core/Wrappers.cs +++ b/DLSv2/Core/Wrappers.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; namespace DLSv2.Core; @@ -23,6 +24,7 @@ public abstract class BaseControlGroupInstance public bool Enabled => ActiveIndexes.Count > 0; public List ActiveIndexes = new(); + private int[] InactiveIndexes = new int[] { }; public BaseControlGroupInstance(T cg) { @@ -32,14 +34,21 @@ public BaseControlGroupInstance(T cg) public void Toggle(int newIndex = 0) { if (Enabled) - ActiveIndexes = new(); + { + InactiveIndexes = ActiveIndexes.ToArray(); + ActiveIndexes.Clear(); + } else - ActiveIndexes = new() { newIndex }; + { + if (InactiveIndexes.Length > 0) ActiveIndexes = InactiveIndexes.ToList(); + else ActiveIndexes = new() { newIndex }; + } } public virtual void Disable() { - ActiveIndexes = new(); + InactiveIndexes = ActiveIndexes.ToArray(); + ActiveIndexes.Clear(); } public void MoveToNext(bool cycleOnly = false) diff --git a/DLSv2/DLSv2.csproj b/DLSv2/DLSv2.csproj index d049c9c..c705ce4 100644 --- a/DLSv2/DLSv2.csproj +++ b/DLSv2/DLSv2.csproj @@ -54,10 +54,12 @@ + + @@ -86,10 +88,8 @@ - - diff --git a/DLSv2/EntryPoint.cs b/DLSv2/EntryPoint.cs index 47ffb7b..1e72925 100644 --- a/DLSv2/EntryPoint.cs +++ b/DLSv2/EntryPoint.cs @@ -48,11 +48,6 @@ public static void Main() GameFiber.StartNew(CachedGameTime.Process, "DLS - GameTime Cache"); "Loaded: DLS - GameTime Cache Thread".ToLog(); - // Creates Triggers manager - "Loading: DLS - Triggers Manager".ToLog(); - GameFiber.StartNew(TriggersManager.Process, "DLS - Triggers Manager"); - "Loaded: DLS - Triggers Manager".ToLog(); - // Loads DLS Models DLSModels = Loaders.ParseVCFs(); @@ -66,17 +61,17 @@ public static void Main() GameFiber.StartNew(PlayerManager.MainLoop, "DLS - Player Controller"); "Loaded: DLS - Player Controller".ToLog(); - // Creates cleanup manager - "Loading: DLS - Cleanup Manager".ToLog(); - GameFiber.StartNew(Threads.CleanupManager.Process, "DLS - Cleanup Manager"); - "Loaded: DLS - Cleanup Manager".ToLog(); - // Creates AI manager "Loading: DLS - AI Manager".ToLog(); GameFiber.StartNew(AiManager.ScanProcess, "DLS - AI Manager Scan"); - GameFiber.StartNew(AiManager.MonitorProcess, "DLS - AI Manager Monitor"); "Loaded: DLS - AI Manager".ToLog(); + // Creates Managed Vehicle process + "Loading: DLS - Vehicle Manager".ToLog(); + GameFiber.StartNew(VehicleManager.Process, "DLS - Vehicle Manager"); + "Loaded: DLS - Vehicle Manager".ToLog(); + + //If extra patch is enabled if (Settings.EXTRAPATCH) { diff --git a/DLSv2/Properties/AssemblyInfo.cs b/DLSv2/Properties/AssemblyInfo.cs index 8b3daa7..cf13276 100644 --- a/DLSv2/Properties/AssemblyInfo.cs +++ b/DLSv2/Properties/AssemblyInfo.cs @@ -14,5 +14,5 @@ [assembly: Guid("64295692-3277-4187-bccf-0af4cb1f9350")] -[assembly: AssemblyVersion("2.0.0.2")] -[assembly: AssemblyFileVersion("2.0.0.2")] \ No newline at end of file +[assembly: AssemblyVersion("2.1.0.0")] +[assembly: AssemblyFileVersion("2.1.0.0")] \ No newline at end of file diff --git a/DLSv2/Threads/AIManager.cs b/DLSv2/Threads/AIManager.cs index ef295e4..04b36c9 100644 --- a/DLSv2/Threads/AIManager.cs +++ b/DLSv2/Threads/AIManager.cs @@ -20,6 +20,11 @@ public static void ScanProcess() { while (true) { + GameFiber.Sleep((int)Math.Max(timeBetweenScans, CachedGameTime.GameTime - lastScanTime)); + lastScanTime = CachedGameTime.GameTime; + + if (Game.IsPaused || Game.Console.IsOpen) continue; + var checksDone = 0; var allVeh = new HashSet(World.GetAllVehicles()); @@ -36,25 +41,6 @@ public static void ScanProcess() if (checksDone % yieldAfterScan == 0) GameFiber.Yield(); } - GameFiber.Sleep((int)Math.Max(timeBetweenScans, CachedGameTime.GameTime - lastScanTime)); - lastScanTime = CachedGameTime.GameTime; - } - } - - public static void MonitorProcess() - { - while (true) - { - foreach (var mv in Entrypoint.ManagedVehicles) - { - if (!mv.Key) continue; - if (mv.Value.LightsOn == mv.Key.IsSirenOn) continue; - - mv.Value.LightsOn = mv.Key.IsSirenOn; - mv.Value.UpdateLights(); - } - - GameFiber.Sleep(timeBetweenMonitor); } } } \ No newline at end of file diff --git a/DLSv2/Threads/CleanupManager.cs b/DLSv2/Threads/CleanupManager.cs deleted file mode 100644 index 2686740..0000000 --- a/DLSv2/Threads/CleanupManager.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Rage; -using System; -using System.Linq; - -namespace DLSv2.Threads; - -using Core; -using Utils; - -internal class CleanupManager -{ - static uint lastProcessTime = CachedGameTime.GameTime; - static int timeBetweenChecks = 10000; - static int yieldAfterChecks = 10; - public static void Process() - { - while (true) - { - int checksDone = 0; - - foreach (ManagedVehicle managedVehicle in Entrypoint.ManagedVehicles.Values.ToList()) - { - if (!managedVehicle.Vehicle) - { - // Removes from Managed Vehicles - Entrypoint.ManagedVehicles.Remove(managedVehicle.Vehicle); - - // Adds EL to available pool, if used - if (Entrypoint.ELUsedPool.ContainsKey(managedVehicle.VehicleHandle)) - { - ("Moving " + managedVehicle.VehicleHandle + " to Available Pool").ToLog(); - Entrypoint.ELAvailablePool.Add(Entrypoint.ELUsedPool[managedVehicle.VehicleHandle]); - Entrypoint.ELUsedPool.Remove(managedVehicle.VehicleHandle); - } - - // Clears all sound IDs - foreach (var soundId in managedVehicle.SoundIds.ToList()) - managedVehicle.StopMode(soundId.Key); - } - - checksDone++; - if (checksDone % yieldAfterChecks == 0) - GameFiber.Yield(); - } - GameFiber.Sleep((int)Math.Max(timeBetweenChecks, CachedGameTime.GameTime - lastProcessTime)); - lastProcessTime = CachedGameTime.GameTime; - } - } -} \ No newline at end of file diff --git a/DLSv2/Threads/ControlsManager.cs b/DLSv2/Threads/ControlsManager.cs index c5c4d23..368397f 100644 --- a/DLSv2/Threads/ControlsManager.cs +++ b/DLSv2/Threads/ControlsManager.cs @@ -54,6 +54,9 @@ public override string ToString() // was the key combo held down on the last tick private bool wasHeld; + // is a controller available + private bool isControllerAvailable; + public ControlsInput(string name) { Name = name; @@ -81,6 +84,7 @@ public void Process() { bool isHeld = IsHeldDown; bool isPressed = IsJustPressed; + isControllerAvailable = Game.IsControllerConnected; if (!ControlsManager.KeysLocked || Name == "LOCKALL") { @@ -115,6 +119,7 @@ private bool IsKeyAvailable() private bool IsButtonAvailable() { return + isControllerAvailable && Button != ControllerButtons.None && !isAnyOtherModifierButtonPressed() && (ButtonModifier == ControllerButtons.None || Game.IsControllerButtonDownRightNow(ButtonModifier)); @@ -123,7 +128,7 @@ private bool IsButtonAvailable() public bool IsJustPressed => IsKeyJustPressed() || IsButtonJustPressed(); - private bool IsKeyJustPressed() => IsKeyAvailable() && Game.IsKeyDown(Key); + private bool IsKeyJustPressed() => IsKeyAvailable() && IsKeyPressedNow(Key) && Game.IsKeyDown(Key); private bool IsButtonJustPressed() => IsButtonAvailable() && Game.IsControllerButtonDown(Button); @@ -213,9 +218,11 @@ public static void Process() { GameFiber.Yield(); + if (!PlayerManager.registeredKeys) continue; + var keyboardState = Game.GetKeyboardState(); - if (Game.IsPaused || keyboardState == null) continue; + if (Game.IsPaused || Game.Console.IsOpen || keyboardState == null) continue; PressedKeys = keyboardState.PressedKeys; IsTextboxOpen = NativeFunction.Natives.UPDATE_ONSCREEN_KEYBOARD() == 0; diff --git a/DLSv2/Threads/PlayerManager.cs b/DLSv2/Threads/PlayerManager.cs index 65adbbe..4b19ec5 100644 --- a/DLSv2/Threads/PlayerManager.cs +++ b/DLSv2/Threads/PlayerManager.cs @@ -19,6 +19,10 @@ internal static void MainLoop() { while (true) { + GameFiber.Yield(); + + if (Game.IsPaused || Game.Console.IsOpen) continue; + VehicleOwner.Process(); Ped playerPed = Game.LocalPlayer.Character; @@ -105,8 +109,6 @@ internal static void MainLoop() ControlsManager.ClearInputs(); registeredKeys = false; } - - GameFiber.Yield(); } } diff --git a/DLSv2/Threads/TriggersManager.cs b/DLSv2/Threads/TriggersManager.cs deleted file mode 100644 index 109f8b8..0000000 --- a/DLSv2/Threads/TriggersManager.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Rage; - -namespace DLSv2.Threads; - -using Core; - -internal class TriggersManager -{ - public static void Process() - { - while (true) - { - foreach (ManagedVehicle mv in Entrypoint.ManagedVehicles.Values) - { - if (mv.Vehicle) foreach (BaseCondition condition in mv.Conditions) condition.Update(mv); - } - GameFiber.Yield(); - } - } -} \ No newline at end of file diff --git a/DLSv2/Threads/VehicleManager.cs b/DLSv2/Threads/VehicleManager.cs new file mode 100644 index 0000000..7a0142e --- /dev/null +++ b/DLSv2/Threads/VehicleManager.cs @@ -0,0 +1,65 @@ +using Rage; +using System.Linq; + +namespace DLSv2.Threads; +using Core; +using Utils; + +internal static class VehicleManager +{ + internal static void Process() + { + while(true) + { + GameFiber.Yield(); + + if (Game.IsPaused || Game.Console.IsOpen) continue; + + foreach (ManagedVehicle mv in Entrypoint.ManagedVehicles.Values.ToArray()) + { + // Check if the vehicle is still valid + if (mv.Vehicle) + { + // Update triggers and conditions + foreach (BaseCondition condition in mv.Conditions) condition.Update(mv); + + // Check if DLS light status matches vehicle siren status + if (mv.LightsOn != mv.Vehicle.IsSirenOn) + { + // If status does not match, force update lights now + // TODO: Update this to smartly toggle lights for player-controlled vehicles + mv.LightsOn = mv.Vehicle.IsSirenOn; + mv.UpdateLights(); + } + else if (mv.lightsNeedUpdate) + { + // Process updates if required + // UpdateLights calls ProcessExtendedSequences already + mv.UpdateLights(); + } + else if (mv.LightsOn) + { + // Process extended sequences if lights are enabled and no updates required + mv.ProcessExtendedSequences(false); + } + } else + { + // Vehicle is no longer valid. Do cleanup. + Entrypoint.ManagedVehicles.Remove(mv.Vehicle); + + // Adds EL to available pool, if used + if (Entrypoint.ELUsedPool.ContainsKey(mv.VehicleHandle)) + { + ("Moving " + mv.VehicleHandle + " to Available Pool").ToLog(); + Entrypoint.ELAvailablePool.Add(Entrypoint.ELUsedPool[mv.VehicleHandle]); + Entrypoint.ELUsedPool.Remove(mv.VehicleHandle); + } + + // Clears all sound IDs + foreach (var soundId in mv.SoundIds.ToArray()) + mv.StopMode(soundId.Key); + } + } + } + } +} diff --git a/DLSv2/Utils/DLSExtensions.cs b/DLSv2/Utils/DLSExtensions.cs index b1a2fb0..af17338 100644 --- a/DLSv2/Utils/DLSExtensions.cs +++ b/DLSv2/Utils/DLSExtensions.cs @@ -107,7 +107,7 @@ internal static SirenSetting GetDefaultSirenSetting(this Vehicle veh) }, // Flashiness Settings - Flashiness = new LightDetailEntry + Flashiness = new LightDetailEntry() { DeltaDeg = light.FlashinessDelta, StartDeg = light.FlashinessStart, @@ -131,59 +131,98 @@ internal static SirenSetting GetDefaultSirenSetting(this Vehicle veh) }; } - public static void ApplyLightModes(this ManagedVehicle managedVehicle, List modes) + public static EmergencyLighting GetDLSEmergencyLighting(this Vehicle vehicle) { // Safety checks - if (managedVehicle == null) return; - var vehicle = managedVehicle.Vehicle; - if (!vehicle) return; + if (!vehicle) return null; EmergencyLighting eL; - var key = vehicle.Handle; + uint key = vehicle.Handle.Value; + string name = "DLS_" + key.ToString("X"); if (Entrypoint.ELUsedPool.TryGetValue(key, out var elFromPool)) + { eL = elFromPool; + } else if (Entrypoint.ELAvailablePool.Count > 0) { eL = Entrypoint.ELAvailablePool[0]; Entrypoint.ELAvailablePool.Remove(eL); - eL.Name = "DLS_" + key; + eL.Name = name; ("Allocated \"" + eL.Name + "\" (now \"" + key + "\") EL from Available Pool").ToLog(LogLevel.DEBUG); } + else if (EmergencyLighting.GetByName(name) != null) + { + eL = EmergencyLighting.GetByName(name); + ("Allocated \"" + eL.Name + "\" EL from Game Memory").ToLog(LogLevel.DEBUG); + } else { - if (EmergencyLighting.GetByName("DLS_" + key) != null) - { - eL = EmergencyLighting.GetByName("DLS_" + key); - ("Allocated \"" + eL.Name + "\" EL from Game Memory").ToLog(LogLevel.DEBUG); - } - else - { - eL = new EmergencyLighting(); - eL.Name = "DLS_" + key; - ("Created \"" + eL.Name + "\" EL").ToLog(LogLevel.DEBUG); - } + eL = new EmergencyLighting(); + eL.Name = name; + ("Created \"" + eL.Name + "\" EL").ToLog(LogLevel.DEBUG); } + if (!Entrypoint.ELUsedPool.ContainsKey(key)) + Entrypoint.ELUsedPool.Add(key, eL); + + return eL; + } + + public static void ApplyLightModes(this ManagedVehicle managedVehicle, List modes) + { + // Safety checks + if (managedVehicle == null) return; + var vehicle = managedVehicle.Vehicle; + if (!vehicle) return; + + managedVehicle.extendedSequences.Clear(); + + EmergencyLighting eL = managedVehicle.eL; + SirenApply.ApplySirenSettingsToEmergencyLighting(managedVehicle.EmptyMode.SirenSettings, eL); var shouldYield = false; var extras = new Dictionary(); Animation anim = null; var paints = new Dictionary(); + var sequences = new Dictionary(); foreach (var mode in modes) { + // apply siren settings including regular sequences if (mode.ApplyDefaultSirenSettings && vehicle.DefaultEmergencyLighting != null) eL.Copy(vehicle.DefaultEmergencyLighting); - SirenApply.ApplySirenSettingsToEmergencyLighting(mode.SirenSettings, eL); + + if (mode.SirenSettings != null) + { + // remove any overridden sequences from extended sequence list + foreach (var siren in mode.SirenSettings.Sirens) + if (siren.Flashiness?.Sequence?.Sequence != null) + foreach (int sirenID in siren.sirenIDs) + managedVehicle.extendedSequences.Remove(sirenID); + + // apply siren settings if specified + SirenApply.ApplySirenSettingsToEmergencyLighting(mode.SirenSettings, eL); + } + + // add regular sequences + foreach (var seq in mode.StandardSequences) + { + int i = seq.Key - 1; + if (i < eL.Lights.Length) eL.Lights[i].FlashinessSequence = seq.Value; + managedVehicle.extendedSequences.Remove(seq.Key); + } + + // add extended sequences + foreach (var seq in mode.ExtendedSequences) + managedVehicle.extendedSequences[seq.Key] = seq.Value; // Sets the extras for the specific mode foreach (var extra in mode.Extra) extras[extra.ID] = extra.Enabled; - // Sets modkits for the specific mode foreach (var kit in mode.ModKits) if (vehicle.HasModkitMod(kit.Type) && vehicle.GetModkitModCount(kit.Type) > kit.Index) @@ -277,10 +316,14 @@ public static void ApplyLightModes(this ManagedVehicle managedVehicle, List 0) + { + // Game.LogTrivialDebug("force update extended sequence"); + managedVehicle.ProcessExtendedSequences(true); + } - if (!Entrypoint.ELUsedPool.ContainsKey(key)) - Entrypoint.ELUsedPool.Add(key, eL); + vehicle.ShouldVehiclesYieldToThisVehicle = shouldYield; managedVehicle.Vehicle.EmergencyLightingOverride = eL; } @@ -328,6 +371,10 @@ internal static void DebugCurrentModes(this Vehicle vehicle, bool showConditions ($" {boolToCheck(vehicle.IsSirenSilent)} IsSirenSilent").ToLog(LogLevel.DEVMODE); ($" {boolToCheck(vehicle.ShouldVehiclesYieldToThisVehicle)} ShouldYield").ToLog(LogLevel.DEVMODE); + ("DLS:").ToLog(LogLevel.DEVMODE); + ($" {boolToCheck(managedVehicle.LightsOn)} DLS LightsOn").ToLog(LogLevel.DEVMODE); + ($" {boolToCheck(managedVehicle.SirenOn)} DLS SirenOn").ToLog(LogLevel.DEVMODE); + ("").ToLog(LogLevel.DEVMODE); ("").ToLog(LogLevel.DEVMODE); ("Light Modes:").ToLog(LogLevel.DEVMODE); diff --git a/DLSv2/Utils/Game/SirenInstance.cs b/DLSv2/Utils/Game/SirenInstance.cs index fbb4864..f4deb61 100644 --- a/DLSv2/Utils/Game/SirenInstance.cs +++ b/DLSv2/Utils/Game/SirenInstance.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Runtime.InteropServices; using Rage; using Rage.Attributes; @@ -118,6 +119,25 @@ private static void Command_SetSirenOnTime(uint time) s.SetSirenOnTime(time); Game.DisplayNotification($"Reset siren on time to {time}"); } + + [ConsoleCommand(Name = "DebugActiveSirens")] + private static void Command_DebugActiveSirens() + { + Vehicle v = Game.LocalPlayer.Character.CurrentVehicle; + if (!v) return; + + var s = new SirenInstance(v); + while(v) + { + if (v.IsSirenOn && s.CurrentSirenBeat >= 0) + { + int i = s.CurrentSirenBeat; + string info = string.Concat(v.EmergencyLighting.Lights.Select(l => l.FlashinessSequence[i] == '1' ? "~r~x~w~" : "~c~x~w~")); + Game.DisplaySubtitle(info, 100); + } + GameFiber.Yield(); + } + } #endif } } diff --git a/DLSv2/Utils/Game/SirenSounds.cs b/DLSv2/Utils/Game/SirenSounds.cs index 8626516..0be04b7 100644 --- a/DLSv2/Utils/Game/SirenSounds.cs +++ b/DLSv2/Utils/Game/SirenSounds.cs @@ -47,6 +47,7 @@ static SirenSounds() Marshal.StructureToPtr(soundSet, ptr, false); EmptySoundSet = ptr; + $"EmptySoundSet = {ptr}".ToLog(LogLevel.DEBUG); } private static unsafe audSoundSet* GetAudioSoundSetPtr(this Vehicle vehicle) @@ -62,9 +63,25 @@ static SirenSounds() public static unsafe void DisableSirenSounds(this Vehicle vehicle) { if (DefaultSoundSets.ContainsKey(vehicle)) return; - - DefaultSoundSets[vehicle] = (IntPtr)vehicle.GetAudioSoundSetPtr()->Data; - $"Setting {vehicle.MemoryAddress}'s SirenSounds to Empty".ToLog(LogLevel.DEBUG); + IntPtr soundSet = (IntPtr)vehicle.GetAudioSoundSetPtr()->Data; + // Default soundset doesn't get assigned if vehicle is spawned while game is paused + // It takes two ticks for it to get assigned, hence calling yield twice + // Vehicles with no default siren sounds always have soundset = 0, so we + // cannot just check while(soundset==0) or it will never exit the loop + + if (soundSet == IntPtr.Zero) + { + do + { + GameFiber.Yield(); + GameFiber.Yield(); + if (!vehicle) return; + soundSet = (IntPtr)vehicle.GetAudioSoundSetPtr()->Data; + } while (Game.IsPaused || Game.Console.IsOpen); + } + + DefaultSoundSets[vehicle] = soundSet; + $"Setting {vehicle.MemoryAddress}'s SirenSounds to Empty ({EmptySoundSet}), existing SoundSet is {DefaultSoundSets[vehicle]}".ToLog(LogLevel.DEBUG); vehicle.GetAudioSoundSetPtr()->Data = (SoundSet*)EmptySoundSet; }