From 5b85b3d534c12d2e4cfd22b1d4e4b4aa0e888ea9 Mon Sep 17 00:00:00 2001 From: PNWParksFan Date: Tue, 14 Jul 2020 02:22:11 -0700 Subject: [PATCH 01/21] Initial working implementation of comments above siren list --- LiveLights/Serializer.cs | 13 +++++++++++++ LiveLights/SirenSetting.cs | 39 +++++++++++++++++++++++++++++++++++--- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/LiveLights/Serializer.cs b/LiveLights/Serializer.cs index 723e0a3..6f35000 100644 --- a/LiveLights/Serializer.cs +++ b/LiveLights/Serializer.cs @@ -158,5 +158,18 @@ private static bool ValidatePath(string path) return File.Exists(path); } + + public static XmlElement SerializeToXMLElement(T obj) + { + XmlDocument doc = new XmlDocument(); + using(XmlWriter writer = doc.CreateNavigator().AppendChild()) + { + var xns = new XmlSerializerNamespaces(); + xns.Add(string.Empty, string.Empty); + _getOrCreateSerializer().Serialize(writer, obj, xns); + } + + return doc.DocumentElement; + } } } diff --git a/LiveLights/SirenSetting.cs b/LiveLights/SirenSetting.cs index e647cee..7ddaa32 100644 --- a/LiveLights/SirenSetting.cs +++ b/LiveLights/SirenSetting.cs @@ -33,14 +33,17 @@ public XmlComment LinkComment set { } } - + // [XmlIgnore] [XmlArray("Sirens")] [XmlArrayItem("Item")] public List SirenSettings { get; set; } = new List(); + } // [XmlType(TypeName="Item")] - public class SirenSetting // : IList + [XmlInclude(typeof(XmlComment))] + [XmlInclude(typeof(SirenEntry))] + public class SirenSetting { [XmlElement("id")] public ValueItem ID { get; set; } = 0; @@ -110,6 +113,7 @@ public uint TextureHash [XmlArray("sirens")] [XmlArrayItem("Item")] + [XmlIgnore] public SirenEntry[] Sirens { get => sirenList.ToArray(); @@ -123,6 +127,33 @@ public SirenEntry[] Sirens } } + + // [XmlArray("sirens2")] + // [XmlArrayItem("Item")] + // [XmlAnyElement("sirens2")] + [XmlAnyElement()] + public XmlNode CommentedSirenSettings + { + set { } + get + { + var export = new XmlDocument(); + var root = export.CreateElement("sirens"); + export.AppendChild(root); + + for (int i = 0; i < sirenList.Count; i++) + { + root.AppendChild(export.ImportNode(sirenList[i].SirenIdComment, true)); + var element = export.ImportNode(Serializer.SerializeToXMLElement(sirenList[i]), true); + root.AppendChild(element); + } + + return root; + } + } + + + [XmlIgnore] private List sirenList = new List(); @@ -140,11 +171,13 @@ public void AddSiren(SirenEntry item) } } + [XmlType(TypeName = "Item")] public class SirenEntry { [XmlIgnore] internal string SirenIdCommentText { get; set; } - + + [XmlIgnore] [XmlAnyElement("SirenIdComment")] public XmlComment SirenIdComment { From a41de821cead905f24e7cb0d3937c1e72dd0c93e Mon Sep 17 00:00:00 2001 From: PNWParksFan Date: Sat, 8 Oct 2022 19:06:35 -0700 Subject: [PATCH 02/21] Cleaned up SirenSettings XML config --- LiveLights/SirenSetting.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/LiveLights/SirenSetting.cs b/LiveLights/SirenSetting.cs index 7ddaa32..201e299 100644 --- a/LiveLights/SirenSetting.cs +++ b/LiveLights/SirenSetting.cs @@ -40,7 +40,6 @@ public XmlComment LinkComment } - // [XmlType(TypeName="Item")] [XmlInclude(typeof(XmlComment))] [XmlInclude(typeof(SirenEntry))] public class SirenSetting @@ -110,9 +109,6 @@ public uint TextureHash [XmlElement("useRealLights")] public ValueItem UseRealLights { get; set; } = true; - - [XmlArray("sirens")] - [XmlArrayItem("Item")] [XmlIgnore] public SirenEntry[] Sirens { @@ -127,10 +123,6 @@ public SirenEntry[] Sirens } } - - // [XmlArray("sirens2")] - // [XmlArrayItem("Item")] - // [XmlAnyElement("sirens2")] [XmlAnyElement()] public XmlNode CommentedSirenSettings { @@ -152,8 +144,6 @@ public XmlNode CommentedSirenSettings } } - - [XmlIgnore] private List sirenList = new List(); From 2630749316281406c705eadcc05ddff7263f025e Mon Sep 17 00:00:00 2001 From: PNWParksFan Date: Sat, 8 Oct 2022 19:47:14 -0700 Subject: [PATCH 03/21] Fixed bug in SirenApply that resulted in wrong binary sequence padding --- LiveLights/SirenApply.cs | 4 ++-- LiveLights/SirenSetting.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LiveLights/SirenApply.cs b/LiveLights/SirenApply.cs index 1d97b18..e378ed7 100644 --- a/LiveLights/SirenApply.cs +++ b/LiveLights/SirenApply.cs @@ -69,7 +69,7 @@ public static void ApplySirenSettingsToEmergencyLighting(this SirenSetting setti light.RotationDelta = entry.Rotation.DeltaDeg; light.RotationStart = entry.Rotation.StartDeg; light.RotationSpeed = entry.Rotation.Speed; - light.RotationSequence = entry.Rotation.Sequence; + light.RotationSequenceRaw = entry.Rotation.Sequence.Value; light.RotationMultiples = entry.Rotation.Multiples; light.RotationDirection = entry.Rotation.Direction; light.RotationSynchronizeToBpm = entry.Rotation.SyncToBPM; @@ -78,7 +78,7 @@ public static void ApplySirenSettingsToEmergencyLighting(this SirenSetting setti light.FlashinessDelta = entry.Flashiness.DeltaDeg; light.FlashinessStart = entry.Flashiness.StartDeg; light.FlashinessSpeed = entry.Flashiness.Speed; - light.FlashinessSequence = entry.Flashiness.Sequence; + light.FlashinessSequenceRaw = entry.Flashiness.Sequence.Value; light.FlashinessMultiples = entry.Flashiness.Multiples; light.FlashinessDirection = entry.Flashiness.Direction; light.FlashinessSynchronizeToBpm = entry.Flashiness.SyncToBPM; diff --git a/LiveLights/SirenSetting.cs b/LiveLights/SirenSetting.cs index e647cee..b6db5e1 100644 --- a/LiveLights/SirenSetting.cs +++ b/LiveLights/SirenSetting.cs @@ -262,7 +262,7 @@ public class Sequencer : ValueItem public static implicit operator Sequencer(uint value) => new Sequencer(value); public static implicit operator Sequencer(string value) => new Sequencer(value); public static implicit operator uint(Sequencer item) => item.Value; - public static implicit operator string(Sequencer item) => Convert.ToString(item.Value, 2); + public static implicit operator string(Sequencer item) => Convert.ToString(item.Value, 2).PadLeft(32, '0'); public Sequencer(uint value) : base(value) { } public Sequencer(string value) : base(Convert.ToUInt32(value, 2)) { } From e16427db612fa29808ce291a2cb70e5e1f88c83e Mon Sep 17 00:00:00 2001 From: PNWParksFan Date: Sat, 8 Oct 2022 19:47:31 -0700 Subject: [PATCH 04/21] Initial implementation of carcols import menu --- LiveLights/Menu/EmergencyLightingMenu.cs | 4 +- LiveLights/Menu/ImportExportMenu.cs | 57 +++++++++++++++++++----- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/LiveLights/Menu/EmergencyLightingMenu.cs b/LiveLights/Menu/EmergencyLightingMenu.cs index 5b08d92..3652d80 100644 --- a/LiveLights/Menu/EmergencyLightingMenu.cs +++ b/LiveLights/Menu/EmergencyLightingMenu.cs @@ -145,12 +145,10 @@ public EmergencyLightingMenu(EmergencyLighting els) Menu.BindMenuAndCopyProperties(CopyMenu.Menu, CopyMenuItem); Menu.AddItem(CopyMenuItem); - /* ImportCarcolsItem = new UIMenuItem("Import carcols.meta file", "Imports all siren settings in selected carcols.meta file"); Menu.AddItem(ImportCarcolsItem); ImportCarcolsItem.Activated += OnImportExportClicked; - */ - + ExportCarcolsItem = new UIMenuItem("Export carcols.meta file", "Exports the siren setting currently being modified to a carcols.meta file"); Menu.AddItem(ExportCarcolsItem); ExportCarcolsItem.Activated += OnImportExportClicked; diff --git a/LiveLights/Menu/ImportExportMenu.cs b/LiveLights/Menu/ImportExportMenu.cs index fe706e4..de37b7c 100644 --- a/LiveLights/Menu/ImportExportMenu.cs +++ b/LiveLights/Menu/ImportExportMenu.cs @@ -24,26 +24,63 @@ public static void CreateExportFolder() } } + public static (string filename, string filepath) GetFilepath() + { + CreateExportFolder(); + string filename = UserInput.GetUserInput("Type or paste an export filename (e.g. ~c~~h~carcols-police.meta~h~~w~) or absolute path (e.g. ~c~~h~C:\\mods\\police\\carcols.meta~h~~w~)", "Enter a filename", 1000); + if (!string.IsNullOrWhiteSpace(filename)) + { + // If the user pasted (or manually typed) an absolute path or + // a valid path relative to the GTA root folder, use that + // location. Otherwise, use the export folder + string filepath = filename; + if (!Directory.Exists(Path.GetDirectoryName(filename))) + { + filepath = Path.Combine(exportFolder, filename); + } + + return (filename, filepath); + } + + return (null, null); + } + public static void OnImportCarcols(EmergencyLightingMenu menu) { - Game.DisplayNotification("~y~Export not implemented yet"); + (string filename, string filepath) = GetFilepath(); + + if (!File.Exists(filepath)) + { + Game.DisplayNotification($"~y~Unable to import~w~ {filename}~y~: File does not exist."); + } + + try + { + CarcolsFile carcols = Serializer.LoadItemFromXML(filepath); + foreach (var setting in carcols.SirenSettings) + { + Game.LogTrivial($"Importing {setting.Name} from {filename}"); + var els = new EmergencyLighting(); + setting.ApplySirenSettingsToEmergencyLighting(els); + Game.LogTrivial($"\tImported as {els.Name}"); + } + Game.DisplayNotification($"Imported ~b~{carcols.SirenSettings.Count}~w~ siren settings from ~b~{filename}"); + } catch (Exception e) + { + Game.DisplayNotification($"~y~Error importing~w~ {filename}~y~: {e.Message}"); + } + } public static bool ExportCarcols(EmergencyLighting els, bool allowOverwrite = false) { - CreateExportFolder(); - string filename = UserInput.GetUserInput("Type or paste an export filename (e.g. ~c~~h~carcols-police.meta~h~~w~) or absolute path (e.g. ~c~~h~C:\\mods\\police\\carcols.meta~h~~w~)", "Enter a filename", 1000); - if(!string.IsNullOrWhiteSpace(filename)) + (string filename, string filepath) = GetFilepath(); + if(!string.IsNullOrWhiteSpace(filepath)) { try { - // If the user pasted (or manually typed) an absolute path or - // a valid path relative to the GTA root folder, use that - // location. Otherwise, create file in the export folder - string filepath = filename; - if(!Directory.Exists(Path.GetDirectoryName(filename))) + if(!Directory.Exists(Path.GetDirectoryName(filepath))) { - filepath = Path.Combine(exportFolder, filename); Directory.CreateDirectory(Path.GetDirectoryName(filepath)); } From 8ee0f7fa21a173e8c15deb807d6cc3ff0c96ff48 Mon Sep 17 00:00:00 2001 From: PNWParksFan Date: Sat, 8 Oct 2022 20:00:41 -0700 Subject: [PATCH 05/21] Fixed SirenSetting deserialization with commented siren setting IDs --- LiveLights/SirenSetting.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/LiveLights/SirenSetting.cs b/LiveLights/SirenSetting.cs index d6bb50f..be96e23 100644 --- a/LiveLights/SirenSetting.cs +++ b/LiveLights/SirenSetting.cs @@ -109,7 +109,13 @@ public uint TextureHash [XmlElement("useRealLights")] public ValueItem UseRealLights { get; set; } = true; - [XmlIgnore] + // Sirens property should always be *deserialized* to read in the sirens array, + // but should never be *serialized* because this is written by the CommentedSirenSettings + // below with siren number comments inline + public bool ShouldSerializeSirens() => false; + + [XmlArray("sirens")] + [XmlArrayItem("Item")] public SirenEntry[] Sirens { get => sirenList.ToArray(); From 6ffb51672e4a5ad89e4c0351054ba5c03453d3c4 Mon Sep 17 00:00:00 2001 From: PNWParksFan Date: Sun, 9 Oct 2022 11:27:05 -0700 Subject: [PATCH 06/21] Added RNUI and RPH as NuGet dependencies --- LiveLights/LiveLights.csproj | 8 ++++---- LiveLights/packages.config | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/LiveLights/LiveLights.csproj b/LiveLights/LiveLights.csproj index d889fbd..f50aaca 100644 --- a/LiveLights/LiveLights.csproj +++ b/LiveLights/LiveLights.csproj @@ -41,12 +41,12 @@ ..\packages\Octokit.0.46.0\lib\net46\Octokit.dll - - ..\..\..\references\RAGENativeUI.dll + + ..\packages\RAGENativeUI.1.9.0\lib\net472\RAGENativeUI.dll False - - ..\..\..\references\RagePluginHookSDK.dll + + ..\packages\RagePluginHook.1.86.1\lib\net472\RagePluginHook.dll False diff --git a/LiveLights/packages.config b/LiveLights/packages.config index cab86fd..742380d 100644 --- a/LiveLights/packages.config +++ b/LiveLights/packages.config @@ -1,6 +1,9 @@  + + + \ No newline at end of file From d3cd79b5af4a9ed59cc90dc0d1b1492f14961c73 Mon Sep 17 00:00:00 2001 From: PNWParksFan Date: Sun, 9 Oct 2022 14:15:23 -0700 Subject: [PATCH 07/21] Track siren setting ID source, updated user input --- LiveLights/LiveLights.csproj | 1 + LiveLights/Menu/ImportExportMenu.cs | 9 +- LiveLights/Menu/SirenSettingsSelectionMenu.cs | 20 +++- LiveLights/Menu/VehicleMenu.cs | 3 +- LiveLights/SirenApply.cs | 3 +- LiveLights/Utils/MenuItems.cs | 2 +- LiveLights/Utils/SirenExtensions.cs | 13 ++- LiveLights/Utils/SirenSource.cs | 101 ++++++++++++++++++ LiveLights/Utils/UserInput.cs | 82 +++++--------- 9 files changed, 167 insertions(+), 67 deletions(-) create mode 100644 LiveLights/Utils/SirenSource.cs diff --git a/LiveLights/LiveLights.csproj b/LiveLights/LiveLights.csproj index f50aaca..d8d0fab 100644 --- a/LiveLights/LiveLights.csproj +++ b/LiveLights/LiveLights.csproj @@ -84,6 +84,7 @@ + diff --git a/LiveLights/Menu/ImportExportMenu.cs b/LiveLights/Menu/ImportExportMenu.cs index de37b7c..186d05d 100644 --- a/LiveLights/Menu/ImportExportMenu.cs +++ b/LiveLights/Menu/ImportExportMenu.cs @@ -27,7 +27,9 @@ public static void CreateExportFolder() public static (string filename, string filepath) GetFilepath() { CreateExportFolder(); - string filename = UserInput.GetUserInput("Type or paste an export filename (e.g. ~c~~h~carcols-police.meta~h~~w~) or absolute path (e.g. ~c~~h~C:\\mods\\police\\carcols.meta~h~~w~)", "Enter a filename", 1000); + Game.DisplaySubtitle(@"Type or paste an export filename (e.g. ~c~~h~carcols-police.meta~h~~w~) or absolute path (e.g. ~c~~h~C:\mods\police\carcols.meta~h~~w~)", 10000); + string filename = UserInput.GetUserInput("Export filename", "", 1000); + Game.DisplaySubtitle("", 1); if (!string.IsNullOrWhiteSpace(filename)) { // If the user pasted (or manually typed) an absolute path or @@ -60,8 +62,9 @@ public static void OnImportCarcols(EmergencyLightingMenu menu) foreach (var setting in carcols.SirenSettings) { Game.LogTrivial($"Importing {setting.Name} from {filename}"); - var els = new EmergencyLighting(); + var els = new EmergencyLighting().GetSafeInstance(); setting.ApplySirenSettingsToEmergencyLighting(els); + els.SetSource(setting.ID, EmergencyLightingSource.Imported); Game.LogTrivial($"\tImported as {els.Name}"); } Game.DisplayNotification($"Imported ~b~{carcols.SirenSettings.Count}~w~ siren settings from ~b~{filename}"); @@ -93,7 +96,7 @@ public static bool ExportCarcols(EmergencyLighting els, bool allowOverwrite = fa CarcolsFile carcols = new CarcolsFile(); SirenSetting setting = els.ExportEmergencyLightingToSirenSettings(); - string sirenIdStr = UserInput.GetUserInput("Enter desired siren ID", "", 3); + string sirenIdStr = UserInput.GetUserInput("Enter a siren ID to export", "", 3); if (byte.TryParse(sirenIdStr, out byte sirenId)) { setting.ID = sirenId; diff --git a/LiveLights/Menu/SirenSettingsSelectionMenu.cs b/LiveLights/Menu/SirenSettingsSelectionMenu.cs index 6c17a93..4f6fcf8 100644 --- a/LiveLights/Menu/SirenSettingsSelectionMenu.cs +++ b/LiveLights/Menu/SirenSettingsSelectionMenu.cs @@ -135,7 +135,7 @@ private void SetSelectedSetting(SirenSettingMenuItem item) { if(selectedSetting?.Item2 != null) { - selectedSetting.Item2.RightBadge = UIMenuItem.BadgeStyle.None; + selectedSetting.Item2.RightBadge = UIMenuItem.BadgeStyle.Blank; selectedSetting.Item2.BackColor = Color.Empty; } @@ -147,7 +147,7 @@ private void SetSelectedSetting(SirenSettingMenuItem item) EmergencyLighting selectedEls = item.ELS; if(AlwaysReturnEditableSetting && !selectedEls.IsCustomSetting()) { - selectedEls = selectedEls.Clone(); + selectedEls = selectedEls.CloneWithID(); RefreshSirenSettingList(); item = elsEntries[selectedEls]; } @@ -210,7 +210,9 @@ public void RefreshSirenSettingList(bool forceUpdateAll = false) if(forceUpdateAll || !elsEntries.ContainsKey(els)) { bool isCustom = els.IsCustomSetting(); - if(!elsEntries.TryGetValue(els, out SirenSettingMenuItem menuEntry)) + SirenSource src = els.GetSource(); + + if (!elsEntries.TryGetValue(els, out SirenSettingMenuItem menuEntry)) { menuEntry = new SirenSettingMenuItem(els); elsEntries.Add(els, menuEntry); @@ -221,10 +223,11 @@ public void RefreshSirenSettingList(bool forceUpdateAll = false) { menuEntry.LeftBadge = UIMenuItem.BadgeStyle.Car; menuEntry.Description = "~g~Editable~w~ siren setting entry"; + if (src != null) menuEntry.Description += $" {src.SourceDescription.ToLower()} from siren setting ID ~b~{src.SourceId}"; } else { menuEntry.LeftBadge = UIMenuItem.BadgeStyle.Lock; - menuEntry.Description = "~y~Built-in~w~ siren setting entry"; + menuEntry.Description = $"~y~Built-in~w~ siren setting entry, siren setting ID ~b~{els.SirenSettingID()}"; if(AlwaysReturnEditableSetting) { menuEntry.Description += ". An ~g~editable~w~ copy will be created if you select this setting."; @@ -245,6 +248,15 @@ internal class SirenSettingMenuItem : UIMenuItem public SirenSettingMenuItem(EmergencyLighting els) : base(els.Name) { this.ELS = els; + this.RightBadge = BadgeStyle.Blank; + SirenSource src = els.GetSource(); + if (els.IsCustomSetting() && src != null) + { + this.RightLabel = $"~c~[{src.SourceId}*]"; + } else if (src != null) + { + this.RightLabel = $"~c~[{els.SirenSettingID()}]"; + } } } diff --git a/LiveLights/Menu/VehicleMenu.cs b/LiveLights/Menu/VehicleMenu.cs index dc1e090..5faa838 100644 --- a/LiveLights/Menu/VehicleMenu.cs +++ b/LiveLights/Menu/VehicleMenu.cs @@ -162,7 +162,8 @@ private static void OnNonEditableConfigSelected(UIMenu sender, UIMenuItem select { if(Vehicle && Vehicle.EmergencyLighting.Exists()) { - Vehicle.EmergencyLightingOverride = Vehicle.EmergencyLighting.Clone(); + var clone = Vehicle.EmergencyLighting.CloneWithID(); + Vehicle.EmergencyLightingOverride = clone; SirenSettingMenu.RefreshSirenSettingList(); SirenSettingMenu.SelectedEmergencyLighting = Vehicle.EmergencyLighting; ResetConfigMenu(); diff --git a/LiveLights/SirenApply.cs b/LiveLights/SirenApply.cs index e378ed7..936f427 100644 --- a/LiveLights/SirenApply.cs +++ b/LiveLights/SirenApply.cs @@ -9,6 +9,7 @@ namespace LiveLights { using Rage; + using Utils; internal static class SirenApply { @@ -165,7 +166,7 @@ public static EmergencyLighting GetELSForVehicle(this Vehicle v) EmergencyLighting els = v.EmergencyLightingOverride; if (!els.Exists()) { - v.EmergencyLightingOverride = v.DefaultEmergencyLighting.Clone(); + v.EmergencyLightingOverride = v.DefaultEmergencyLighting.CloneWithID(); els = v.EmergencyLightingOverride; Game.LogTrivial("Cloned default ELS"); } diff --git a/LiveLights/Utils/MenuItems.cs b/LiveLights/Utils/MenuItems.cs index 5e72c23..bd2683b 100644 --- a/LiveLights/Utils/MenuItems.cs +++ b/LiveLights/Utils/MenuItems.cs @@ -108,7 +108,7 @@ protected virtual void UpdateMenuDisplay() protected virtual int MaxInputLength { get; } = 1000; protected virtual string DisplayMenu => ItemValue?.ToString() ?? "(empty)"; protected virtual string DisplayInputBox => ItemValue.ToString(); - protected virtual string DisplayInputPrompt => $"Enter a value for ~b~{this.MenuItem.Text}~w~ ~c~({typeof(T).Name}, max length {MaxInputLength})"; + protected virtual string DisplayInputPrompt => $"Enter a value for \"{this.MenuItem.Text}\" ({typeof(T).Name}, max length {MaxInputLength})"; public virtual string CustomInputPrompt { get; set; } = null; protected virtual void ActivatedHandler(UIMenu sender, UIMenuItem selectedItem) diff --git a/LiveLights/Utils/SirenExtensions.cs b/LiveLights/Utils/SirenExtensions.cs index bedc2d7..7f5cec0 100644 --- a/LiveLights/Utils/SirenExtensions.cs +++ b/LiveLights/Utils/SirenExtensions.cs @@ -4,6 +4,7 @@ using System.Text; using System.Threading.Tasks; using System.Drawing; +using System.Reflection; namespace LiveLights.Utils { @@ -31,10 +32,14 @@ public static void ShowSirenMarker(this Vehicle vehicle, int siren, Vector3 scal } } + public static uint SirenSettingID(this EmergencyLighting els) + { + return (uint)typeof(EmergencyLighting).GetProperty("Id", BindingFlags.GetProperty | BindingFlags.NonPublic | BindingFlags.Instance).GetValue(els); + } + public static bool IsCustomSetting(this EmergencyLighting els) { - return EmergencyLighting.Get(false, true).Contains(els); - // return EmergencyLighting.Get(false, true).Any(l => l.Name == els.Name); + return els.SirenSettingID() == uint.MaxValue; } public static EmergencyLighting GetCustomOrClone(this EmergencyLighting els) @@ -44,7 +49,7 @@ public static EmergencyLighting GetCustomOrClone(this EmergencyLighting els) return els; } else { - return els.Clone(); + return els.CloneWithID(); } } @@ -52,7 +57,7 @@ public static EmergencyLighting GetOrCreateOverrideEmergencyLighting(this Vehicl { if (!vehicle.EmergencyLightingOverride.Exists()) { - vehicle.EmergencyLightingOverride = vehicle.DefaultEmergencyLighting.Clone(); + vehicle.EmergencyLightingOverride = vehicle.DefaultEmergencyLighting.CloneWithID(); } return vehicle.EmergencyLightingOverride; diff --git a/LiveLights/Utils/SirenSource.cs b/LiveLights/Utils/SirenSource.cs new file mode 100644 index 0000000..6bfc007 --- /dev/null +++ b/LiveLights/Utils/SirenSource.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Rage; + +namespace LiveLights.Utils +{ + internal enum EmergencyLightingSource + { + BuiltIn, + Cloned, + Imported, + Manual + } + + internal class SirenSource + { + public uint SourceId { get; set; } + public EmergencyLightingSource Source { get; set; } + + public string SourceDescription { + get + { + switch (Source) + { + case EmergencyLightingSource.BuiltIn: + return "Built In"; + case EmergencyLightingSource.Cloned: + return "Cloned"; + case EmergencyLightingSource.Imported: + return "Imported"; + case EmergencyLightingSource.Manual: + return "Manaully Set"; + default: + return "Copied"; + } + } + } + + private SirenSource(uint sourceId, EmergencyLightingSource source) + { + SourceId = sourceId; + Source = source; + } + + private static Dictionary sources = new Dictionary(); + + internal static void SetSource(EmergencyLighting els, uint srcId, EmergencyLightingSource src) + { + if (srcId == uint.MaxValue) return; + + if (!sources.TryGetValue(els, out SirenSource source)) + { + source = new SirenSource(srcId, src); + sources.Add(els, source); + } + else + { + source.SourceId = srcId; + source.Source = src; + } + } + + internal static SirenSource GetSource(EmergencyLighting els) + { + if (sources.TryGetValue(els, out SirenSource source)) + { + return source; + } + else if (!els.IsCustomSetting()) + { + return new SirenSource(els.SirenSettingID(), EmergencyLightingSource.BuiltIn); + } + + return null; + } + } + + internal static class SirenSourceExtensions + { + + // this abomination is required because the first instance returned has a different hashcode from + // any subsequent instances returned by Get[ByName], Vehicle.EmergencyLighting, etc. + // hopefully will be fixed soon + public static EmergencyLighting GetSafeInstance(this EmergencyLighting els) => EmergencyLighting.GetByName(els.Name); + + public static EmergencyLighting CloneWithID(this EmergencyLighting source) + { + uint srcId = source.SirenSettingID(); + var clone = source.Clone().GetSafeInstance(); + SirenSource.SetSource(clone, srcId, EmergencyLightingSource.Cloned); + return clone; + } + + public static void SetSource(this EmergencyLighting els, uint srcId, EmergencyLightingSource src) => SirenSource.SetSource(els, srcId, src); + + public static SirenSource GetSource(this EmergencyLighting els) => SirenSource.GetSource(els); + } +} diff --git a/LiveLights/Utils/UserInput.cs b/LiveLights/Utils/UserInput.cs index 4884333..d9db2fa 100644 --- a/LiveLights/Utils/UserInput.cs +++ b/LiveLights/Utils/UserInput.cs @@ -5,87 +5,63 @@ using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; +using RAGENativeUI; +using RAGENativeUI.Elements; namespace LiveLights.Utils { using Rage; using Rage.Native; - internal static class UserInput + internal class UserInput { - public static string GetUserInput(string windowTitle, string defaultText, int maxLength) + public static string GetUserInput(string windowTitle, string defaultText, int maxLength, bool canCopy = true, bool canPaste = true) { - NativeFunction.Natives.DISABLE_ALL_CONTROL_ACTIONS(2); + string help = ""; + if (canPaste) help += $"~{Keys.LControlKey.GetInstructionalId()}~ ~{Keys.V.GetInstructionalId()}~ overwrite input with clipboard\n"; + if (canCopy) help += $"~{Keys.LControlKey.GetInstructionalId()}~ ~{Keys.C.GetInstructionalId()}~ copy initial input to clipboard\n"; + help += "~INPUT_FRONTEND_ACCEPT~ commit changes\n~INPUT_FRONTEND_CANCEL~ discard changes"; + + if (Game.IsPaused) + { + Game.IsPaused = false; + Game.DisplayHelp(help, true); + GameFiber.Yield(); + Game.IsPaused = true; + } + else + { + Game.DisplayHelp(help, true); + } - NativeFunction.Natives.DISPLAY_ONSCREEN_KEYBOARD(true, windowTitle, 0, defaultText, 0, 0, 0, maxLength); - Game.DisplaySubtitle(windowTitle, 100000); - Game.DisplayHelp("~b~Ctrl~w~ + ~b~V~w~: overwrite input with clipboard\n~b~Ctrl~w~ + ~b~C~w~: copy initial input to clipboard\n~INPUT_FRONTEND_ACCEPT~ commit changes\n~INPUT_FRONTEND_CANCEL~ discard changes", true); + Localization.SetText("TEXTBOX_TMP_LABEL", windowTitle); + NativeFunction.Natives.DISABLE_ALL_CONTROL_ACTIONS(2); + NativeFunction.Natives.DISPLAY_ONSCREEN_KEYBOARD(0, "TEXTBOX_TMP_LABEL", 0, defaultText ?? "", 0, 0, 0, maxLength); while (NativeFunction.Natives.UPDATE_ONSCREEN_KEYBOARD() == 0) { - if(Game.IsControlKeyDownRightNow && Game.IsKeyDown(Keys.V)) + if (canPaste && Game.IsControlKeyDownRightNow && Game.IsKeyDown(Keys.V)) { - string text = GetClipboardText(); - if(!string.IsNullOrEmpty(text)) + string text = Game.GetClipboardText(); + if (!string.IsNullOrEmpty(text)) { - NativeFunction.Natives.DISPLAY_ONSCREEN_KEYBOARD(true, windowTitle, 0, text, 0, 0, 0, maxLength); + NativeFunction.Natives.DISPLAY_ONSCREEN_KEYBOARD(0, "TEXTBOX_TMP_LABEL", 0, text, 0, 0, 0, maxLength); Game.DisplayNotification($"Pasted \"~b~{text}~w~\" from clipboard. Note: Ctrl+V overwrites any data currently in text box."); } } - if(Game.IsControlKeyDownRightNow && Game.IsKeyDown(Keys.C)) + if (canCopy && Game.IsControlKeyDownRightNow && Game.IsKeyDown(Keys.C)) { - SetClipboardText(defaultText); + Game.SetClipboardText(defaultText); Game.DisplayNotification($"Copied \"~b~{defaultText}~w~\" to clipboard. Note: Ctrl+C can only copy the starting value, not any uncommitted changes."); } GameFiber.Yield(); } NativeFunction.Natives.ENABLE_ALL_CONTROL_ACTIONS(2); - Game.DisplaySubtitle("", 5); Game.HideHelp(); return NativeFunction.Natives.GET_ONSCREEN_KEYBOARD_RESULT(); } - - // STA thread required to use clipboard: https://stackoverflow.com/a/518724 - public static string GetClipboardText() - { - string text = null; - Thread staThread = new Thread( - delegate() - { - try - { - text = Clipboard.GetText(); - } catch(Exception e) - { - Game.LogTrivialDebug("Could not access clipboard: " + e.Message); - } - }); - staThread.SetApartmentState(ApartmentState.STA); - staThread.Start(); - staThread.Join(); - return text; - } - - public static void SetClipboardText(string text) - { - Thread staThread = new Thread( - delegate () - { - try - { - Clipboard.SetText(text); - } - catch (Exception e) - { - Game.LogTrivialDebug("Could not access clipboard: " + e.Message); - } - }); - staThread.SetApartmentState(ApartmentState.STA); - staThread.Start(); - staThread.Join(); - } } } From 374668bdded49777adc866dba525229f9648c90b Mon Sep 17 00:00:00 2001 From: PNWParksFan Date: Sun, 9 Oct 2022 14:37:06 -0700 Subject: [PATCH 08/21] Added siren setting ID to edit menu --- LiveLights/Menu/EmergencyLightingMenu.cs | 17 ++++- LiveLights/Menu/SirenSettingsSelectionMenu.cs | 69 ++++++++++--------- 2 files changed, 50 insertions(+), 36 deletions(-) diff --git a/LiveLights/Menu/EmergencyLightingMenu.cs b/LiveLights/Menu/EmergencyLightingMenu.cs index 3652d80..22e3e57 100644 --- a/LiveLights/Menu/EmergencyLightingMenu.cs +++ b/LiveLights/Menu/EmergencyLightingMenu.cs @@ -30,6 +30,9 @@ public EmergencyLightingMenu(EmergencyLighting els) NameItem = new UIMenuStringSelector("Name", ELS.Name, "Siren setting name as shown in carcols.meta"); Menu.AddMenuDataBinding(NameItem, (x) => ELS.Name = x, () => ELS.Name); + IdItem = new UIMenuUIntSelector("Siren Setting ID", GetSourceID(), "Siren setting ID which will be exported to carcols.meta"); + Menu.AddMenuDataBinding(IdItem, SetSourceID, GetSourceID); + BpmItem = new UIMenuUIntSelector("BPM", ELS.SequencerBpm, "Beats per minute"); Menu.AddMenuDataBinding(BpmItem, (x) => ELS.SequencerBpm = x, () => ELS.SequencerBpm); @@ -103,7 +106,7 @@ public EmergencyLightingMenu(EmergencyLighting els) SirensMenuItem = new UIMenuItem("Sirens", "Edit sequences and other settings for individual sirens"); SirensMenuItem.RightLabel = "→"; Menu.AddItem(SirensMenuItem, 3); - SirensMenuItem.Activated += onSirenSubmenuActivated; + SirensMenuItem.Activated += OnSirenSubmenuActivated; SirenMenus = new List(); // Create each siren menu @@ -179,7 +182,7 @@ private void OnImportExportClicked(UIMenu sender, UIMenuItem selectedItem) } } - private void onSirenSubmenuActivated(UIMenu sender, UIMenuItem selectedItem) + private void OnSirenSubmenuActivated(UIMenu sender, UIMenuItem selectedItem) { sender.Visible = false; SirenSwitcherItem.SwitchMenuItem.CurrentMenu.Visible = true; @@ -210,11 +213,21 @@ public void ShowSirenPositions(Vehicle v, bool selectedOnly) CopyMenu.ProcessShowSirens(v); } + private uint GetSourceID() + { + SirenSource source = ELS.GetSource(); + if (source != null) return source.SourceId; + else return 0; + } + + private void SetSourceID(uint id) => ELS.SetSource(id, EmergencyLightingSource.Manual); + public EmergencyLighting ELS { get; } // Core lighting settings public UIMenuRefreshable Menu { get; } public UIMenuStringSelector NameItem { get; } + public UIMenuUIntSelector IdItem { get; } public UIMenuUIntSelector BpmItem { get; } public UIMenuListItemSelector TextureHashItem { get; } public UIMenuListItemSelector TimeMultiplierItem { get; } diff --git a/LiveLights/Menu/SirenSettingsSelectionMenu.cs b/LiveLights/Menu/SirenSettingsSelectionMenu.cs index 4f6fcf8..1442fa9 100644 --- a/LiveLights/Menu/SirenSettingsSelectionMenu.cs +++ b/LiveLights/Menu/SirenSettingsSelectionMenu.cs @@ -183,7 +183,24 @@ public void RefreshSirenSettingList(bool forceUpdateAll = false) { IEnumerable elsToShow = EmergencyLighting.Get(IncludeBuiltInSettings, IncludeCustomSettings).Where(e => e.Exists()); - // Remove any lighting entries which are no longer valid + + // Add any new lighting entries + foreach (EmergencyLighting els in elsToShow) + { + if (forceUpdateAll || !elsEntries.ContainsKey(els)) + { + if (!elsEntries.TryGetValue(els, out SirenSettingMenuItem menuEntry)) + { + menuEntry = new SirenSettingMenuItem(els); + elsEntries.Add(els, menuEntry); + Menu.AddItem(menuEntry); + } + + Game.LogTrivialDebug("Added EmergencyLighting entry " + els.Name); + } + } + + // Remove any lighting entries which are no longer valid and update labels for valid items foreach (EmergencyLighting els in elsEntries.Keys.ToArray()) { if(!els.IsValid() || !elsToShow.Contains(els)) @@ -200,41 +217,33 @@ public void RefreshSirenSettingList(bool forceUpdateAll = false) Game.LogTrivialDebug("Removed EmergencyLighting entry " + (els.IsValid() ? " of undesired type" : "for being invalid")); } else { - elsEntries[els].Text = els.Name; - } - } + var menu = elsEntries[els]; + menu.Text = els.Name; - // Add any new lighting entries - foreach (EmergencyLighting els in elsToShow) - { - if(forceUpdateAll || !elsEntries.ContainsKey(els)) - { bool isCustom = els.IsCustomSetting(); SirenSource src = els.GetSource(); - if (!elsEntries.TryGetValue(els, out SirenSettingMenuItem menuEntry)) + if (isCustom) { - menuEntry = new SirenSettingMenuItem(els); - elsEntries.Add(els, menuEntry); - Menu.AddItem(menuEntry); + menu.LeftBadge = UIMenuItem.BadgeStyle.Car; + menu.Description = "~g~Editable~w~ siren setting entry"; + if (src != null) + { + menu.RightLabel = $"~c~[{src.SourceId}*]"; + menu.Description += $" {src.SourceDescription.ToLower()} from siren setting ID ~b~{src.SourceId}"; + } } - - if(isCustom) + else { - menuEntry.LeftBadge = UIMenuItem.BadgeStyle.Car; - menuEntry.Description = "~g~Editable~w~ siren setting entry"; - if (src != null) menuEntry.Description += $" {src.SourceDescription.ToLower()} from siren setting ID ~b~{src.SourceId}"; - } else - { - menuEntry.LeftBadge = UIMenuItem.BadgeStyle.Lock; - menuEntry.Description = $"~y~Built-in~w~ siren setting entry, siren setting ID ~b~{els.SirenSettingID()}"; - if(AlwaysReturnEditableSetting) + menu.LeftBadge = UIMenuItem.BadgeStyle.Lock; + menu.Description = $"~y~Built-in~w~ siren setting entry, siren setting ID ~b~{els.SirenSettingID()}"; + menu.RightLabel = $"~c~[{els.SirenSettingID()}]"; + + if (AlwaysReturnEditableSetting) { - menuEntry.Description += ". An ~g~editable~w~ copy will be created if you select this setting."; + menu.Description += ". An ~g~editable~w~ copy will be created if you select this setting."; } } - - Game.LogTrivialDebug("Added EmergencyLighting entry " + els.Name); } } @@ -249,14 +258,6 @@ public SirenSettingMenuItem(EmergencyLighting els) : base(els.Name) { this.ELS = els; this.RightBadge = BadgeStyle.Blank; - SirenSource src = els.GetSource(); - if (els.IsCustomSetting() && src != null) - { - this.RightLabel = $"~c~[{src.SourceId}*]"; - } else if (src != null) - { - this.RightLabel = $"~c~[{els.SirenSettingID()}]"; - } } } From d5177f74beb4eb920446e072d70e11e006580e4b Mon Sep 17 00:00:00 2001 From: PNWParksFan Date: Mon, 10 Oct 2022 01:25:34 -0700 Subject: [PATCH 09/21] Updated import/export menu with bulk export and import support --- LiveLights/Menu/EmergencyLightingMenu.cs | 18 +- LiveLights/Menu/ImportExportMenu.cs | 97 ++++- LiveLights/Menu/SirenSettingsSelectionMenu.cs | 349 ++++++++++++------ LiveLights/Menu/VehicleMenu.cs | 15 +- 4 files changed, 336 insertions(+), 143 deletions(-) diff --git a/LiveLights/Menu/EmergencyLightingMenu.cs b/LiveLights/Menu/EmergencyLightingMenu.cs index 22e3e57..b67980a 100644 --- a/LiveLights/Menu/EmergencyLightingMenu.cs +++ b/LiveLights/Menu/EmergencyLightingMenu.cs @@ -147,14 +147,10 @@ public EmergencyLightingMenu(EmergencyLighting els) CopyMenuItem.RightLabel = "→"; Menu.BindMenuAndCopyProperties(CopyMenu.Menu, CopyMenuItem); Menu.AddItem(CopyMenuItem); - - ImportCarcolsItem = new UIMenuItem("Import carcols.meta file", "Imports all siren settings in selected carcols.meta file"); - Menu.AddItem(ImportCarcolsItem); - ImportCarcolsItem.Activated += OnImportExportClicked; - ExportCarcolsItem = new UIMenuItem("Export carcols.meta file", "Exports the siren setting currently being modified to a carcols.meta file"); + ExportCarcolsItem = new UIMenuItem("Export carcols.meta file", "Exports the single siren setting currently being modified to a carcols.meta file. To export multiple settings into a single file, use the bulk export tool from the main menu."); Menu.AddItem(ExportCarcolsItem); - ExportCarcolsItem.Activated += OnImportExportClicked; + ExportCarcolsItem.Activated += OnExportClicked; ExportAllowOverwriteItem = new UIMenuCheckboxItem("Allow overwrite on export", Settings.DefaultOverwrite, "Allow exported carcols.meta files to overwrite existing files with the same name"); Menu.AddItem(ExportAllowOverwriteItem); @@ -171,14 +167,11 @@ private void OnQuickEditMenuOpened(UIMenu sender, UIMenuItem selectedItem) SequenceQuickEdit.Menu.RefreshIndex(); } - private void OnImportExportClicked(UIMenu sender, UIMenuItem selectedItem) + private void OnExportClicked(UIMenu sender, UIMenuItem selectedItem) { - if(selectedItem == ImportCarcolsItem) - { - ImportExportMenu.OnImportCarcols(this); - } else if(selectedItem == ExportCarcolsItem) + if(selectedItem == ExportCarcolsItem) { - ImportExportMenu.ExportCarcols(this.ELS, ExportAllowOverwriteItem.Checked); + ImportExportMenu.ExportCarcols(ExportAllowOverwriteItem.Checked, this.ELS); } } @@ -272,6 +265,5 @@ private uint GetSourceID() // Import/export public UIMenuItem ExportCarcolsItem { get; } public UIMenuCheckboxItem ExportAllowOverwriteItem { get; } - public UIMenuItem ImportCarcolsItem { get; } } } diff --git a/LiveLights/Menu/ImportExportMenu.cs b/LiveLights/Menu/ImportExportMenu.cs index 186d05d..dec7633 100644 --- a/LiveLights/Menu/ImportExportMenu.cs +++ b/LiveLights/Menu/ImportExportMenu.cs @@ -12,8 +12,55 @@ namespace LiveLights.Menu using RAGENativeUI.Elements; using Utils; + internal static class ImportExportMenu { + static ImportExportMenu() + { + ExportMenu = new UIMenu("Export Siren Settings", ""); + ExportAllowOverwriteItem = new UIMenuCheckboxItem("Allow overwrite on export", Settings.DefaultOverwrite, "Allow exported carcols.meta files to overwrite existing files with the same name"); + ExportSelectSettingsMenu = new SirenSettingsSelectionMenuMulti(returnEditable: false); + ExportSelectSettingsMenu.CreateAndBindToSubmenuItem(ExportMenu, "Select settings to export", "Select one or more siren settings to be exported in a single file"); + ExportItem = new UIMenuItem("Export carcols.meta file", "Exports the siren setting currently being modified to a carcols.meta file"); + ExportMenu.AddItems(ExportAllowOverwriteItem, ExportItem); + ExportItem.Activated += OnExportActivated; + + ImportActiveSettingMenu = new SirenSettingsSelectionMenu(null, custom: true, builtIn: false, returnEditable: false); + ImportActiveSettingMenu.Menu.ParentMenu = VehicleMenu.Menu; + ImportActiveSettingMenu.Menu.ParentItem = VehicleMenu.ImportSelectorItem; + ImportActiveSettingMenu.Menu.SubtitleText = "Select an imported setting to activate"; + ImportActiveSettingMenu.OnSirenSettingSelected += OnImportedSettingSelected; + + MenuController.Pool.AddAfterYield(ExportMenu); + } + + private static void OnImportedSettingSelected(SirenSettingsSelectionMenu sender, UIMenu menu, SirenSettingMenuItem item, EmergencyLighting setting) + { + if (VehicleMenu.Vehicle && VehicleMenu.Vehicle.HasSiren && setting != null && setting.IsValid()) + { + Game.DisplayNotification($"Activated imported siren setting ~b~{setting.Name}~w~ on current vehicle"); + VehicleMenu.Vehicle.EmergencyLightingOverride = setting; + VehicleMenu.Refresh(); + } + } + + public static void OnImportActivated(UIMenu sender, UIMenuItem selectedItem) + { + ImportCarcols(); + } + + private static void OnExportActivated(UIMenu sender, UIMenuItem selectedItem) + { + ExportCarcols(ExportAllowOverwriteItem.Checked, ExportSelectSettingsMenu.SelectedItems); + } + + public static UIMenu ExportMenu { get; } + public static UIMenuCheckboxItem ExportAllowOverwriteItem { get; } + public static SirenSettingsSelectionMenuMulti ExportSelectSettingsMenu { get; } + public static UIMenuItem ExportItem { get; } + + public static SirenSettingsSelectionMenu ImportActiveSettingMenu { get; } + public static string exportFolder = @"Plugins\LiveLights\carcols\"; public static void CreateExportFolder() @@ -47,17 +94,19 @@ public static (string filename, string filepath) GetFilepath() return (null, null); } - public static void OnImportCarcols(EmergencyLightingMenu menu) + private static void ImportCarcols() { (string filename, string filepath) = GetFilepath(); if (!File.Exists(filepath)) { Game.DisplayNotification($"~y~Unable to import~w~ {filename}~y~: File does not exist."); + return; } try { + List newItems = new List(); CarcolsFile carcols = Serializer.LoadItemFromXML(filepath); foreach (var setting in carcols.SirenSettings) { @@ -65,9 +114,21 @@ public static void OnImportCarcols(EmergencyLightingMenu menu) var els = new EmergencyLighting().GetSafeInstance(); setting.ApplySirenSettingsToEmergencyLighting(els); els.SetSource(setting.ID, EmergencyLightingSource.Imported); + newItems.Add(els); Game.LogTrivial($"\tImported as {els.Name}"); } Game.DisplayNotification($"Imported ~b~{carcols.SirenSettings.Count}~w~ siren settings from ~b~{filename}"); + + VehicleMenu.SirenSettingMenu.RefreshSirenSettingList(true); + + if (VehicleMenu.Vehicle && VehicleMenu.Vehicle.HasSiren) + { + ImportActiveSettingMenu.CustomEntries = newItems; + + VehicleMenu.Menu.Visible = false; + ImportActiveSettingMenu.Menu.Visible = true; + } + } catch (Exception e) { Game.DisplayNotification($"~y~Error importing~w~ {filename}~y~: {e.Message}"); @@ -75,8 +136,18 @@ public static void OnImportCarcols(EmergencyLightingMenu menu) } - public static bool ExportCarcols(EmergencyLighting els, bool allowOverwrite = false) + public static bool ExportCarcols(bool allowOverwrite, IEnumerable settings) => ExportCarcols(allowOverwrite, settings.ToArray()); + + public static bool ExportCarcols(bool allowOverwrite, params EmergencyLighting[] settings) { + int count = settings.Length; + if (count == 0) + { + Game.DisplayNotification("~y~Unable to export~w~ because no siren settings were selected"); + Game.LogTrivial("Unable to export because no siren settings were selected"); + return false; + } + (string filename, string filepath) = GetFilepath(); if(!string.IsNullOrWhiteSpace(filepath)) { @@ -90,25 +161,25 @@ public static bool ExportCarcols(EmergencyLighting els, bool allowOverwrite = fa if(!allowOverwrite && File.Exists(filepath)) { Game.DisplayNotification($"~y~Unable to export~w~ {filename}~y~: File already exists."); + Game.LogTrivial($"Unable to export to \"{filename}\" because file already exists and overwrite is not enabled."); return false; } CarcolsFile carcols = new CarcolsFile(); - SirenSetting setting = els.ExportEmergencyLightingToSirenSettings(); - - string sirenIdStr = UserInput.GetUserInput("Enter a siren ID to export", "", 3); - if (byte.TryParse(sirenIdStr, out byte sirenId)) + foreach (var els in settings) { - setting.ID = sirenId; - } else - { - Game.DisplayNotification("Unable to parse a valid siren ID, defaulting to ~y~0~w~. Make sure to update the siren ID when using the exported file."); + Game.LogTrivial($" Serializing \"{els.Name}\""); + SirenSetting setting = els.ExportEmergencyLightingToSirenSettings(); + var src = els.GetSource(); + if (src != null) setting.ID = src.SourceId; + else setting.ID = 0; + + carcols.SirenSettings.Add(setting); } - carcols.SirenSettings.Add(setting); Serializer.SaveItemToXML(carcols, filepath); - Game.DisplayNotification($"~g~Successfully exported~w~ \"{els.Name}\" ~g~to~w~ \"{Path.GetFullPath(filepath)}\""); - Game.LogTrivial($"Exported {els.Name} to \"{Path.GetFullPath(filepath)}\""); + Game.DisplayNotification($"~g~Successfully exported~w~ {count} siren settings ~g~to~w~ \"{Path.GetFullPath(filepath)}\""); + Game.LogTrivial($"Exported {count} siren settings to \"{Path.GetFullPath(filepath)}\""); return true; } catch (Exception e) { diff --git a/LiveLights/Menu/SirenSettingsSelectionMenu.cs b/LiveLights/Menu/SirenSettingsSelectionMenu.cs index 1442fa9..d20e57e 100644 --- a/LiveLights/Menu/SirenSettingsSelectionMenu.cs +++ b/LiveLights/Menu/SirenSettingsSelectionMenu.cs @@ -12,12 +12,47 @@ namespace LiveLights.Menu using RAGENativeUI.Elements; using Utils; - internal class SirenSettingsSelectionMenu + internal interface ISirenSettingMenuItem { - public UIMenu Menu { get; } - private Dictionary elsEntries = new Dictionary(); + EmergencyLighting ELS { get; } + } - public bool CloseOnSelection { get; set; } + internal class SirenSettingMenuItem : UIMenuItem, ISirenSettingMenuItem + { + public EmergencyLighting ELS { get; } + + public SirenSettingMenuItem(EmergencyLighting els) : base(els.Name) + { + this.ELS = els; + this.RightBadge = BadgeStyle.Blank; + } + } + + internal class MultiSirenSettingMenuItem : UIMenuCheckboxItem, ISirenSettingMenuItem + { + public EmergencyLighting ELS { get; } + + public MultiSirenSettingMenuItem(EmergencyLighting els) : base(els.Name, false) + { + this.ELS = els; + } + } + + internal abstract class BaseSirenSettingsSelectionMenu where T: UIMenuItem, ISirenSettingMenuItem + { + public UIMenu Menu { get; } + protected Dictionary elsEntries = new Dictionary(); + + protected IEnumerable customEntries; + public IEnumerable CustomEntries + { + get => customEntries; + set + { + customEntries = value; + RefreshSirenSettingList(true); + } + } private bool includeBuiltIn; public bool IncludeBuiltInSettings @@ -55,15 +90,13 @@ public bool AlwaysReturnEditableSetting get => alwaysReturnEditable; } - public delegate void SirenSettingSelectedEvent(SirenSettingsSelectionMenu sender, UIMenu menu, SirenSettingMenuItem item, EmergencyLighting setting); - public event SirenSettingSelectedEvent OnSirenSettingSelected; - - public SirenSettingsSelectionMenu(EmergencyLighting initialSelected, bool closeOnSelect = true, bool builtIn = true, bool custom = true, bool returnEditable = true) + public BaseSirenSettingsSelectionMenu(bool builtIn = true, bool custom = true, bool returnEditable = true, IEnumerable customList = null) { - this.CloseOnSelection = closeOnSelect; this.includeBuiltIn = builtIn; this.includeCustom = custom; this.alwaysReturnEditable = (returnEditable && custom); + this.customEntries = customList; + if(returnEditable && !custom) { Game.LogTrivialDebug("Warning: Attempted to create siren setting selection menu without custom entries but with editable required"); @@ -72,9 +105,6 @@ public SirenSettingsSelectionMenu(EmergencyLighting initialSelected, bool closeO Menu = new UIMenu("Siren Selection", "~b~Select a siren setting to use"); MenuController.Pool.AddAfterYield(Menu); RefreshSirenSettingList(); - Menu.OnItemSelect += OnMenuItemSelected; - - SelectedEmergencyLighting = initialSelected; } public UIMenuItem CreateAndBindToSubmenuItem(UIMenu parentMenu) => CreateAndBindToSubmenuItem(parentMenu, "Select Siren Setting", ""); @@ -82,70 +112,157 @@ public SirenSettingsSelectionMenu(EmergencyLighting initialSelected, bool closeO public UIMenuItem CreateAndBindToSubmenuItem(UIMenu parentMenu, string text, string description, bool addItem = true) { UIMenuItem item = new UIMenuItem(text, description); - if(addItem) + if (addItem) { parentMenu.AddItem(item); } parentMenu.BindMenuAndCopyProperties(Menu, item, false); UpdateBoundMenuLabel(item); - this.OnSirenSettingSelected += OnBoundMenuUpdated; item.Activated += OnBoundMenuItemActivated; return item; } - public void UpdateBoundMenuLabel(UIMenuItem item) + public abstract void UpdateBoundMenuLabel(UIMenuItem item); + + protected void OnBoundMenuItemActivated(UIMenu sender, UIMenuItem selectedItem) + { + this.RefreshSirenSettingList(); + } + + protected abstract T CreateNewSirenSettingMenuItem(EmergencyLighting els); + + public virtual void RefreshSirenSettingList(bool forceUpdateAll = false) { - if(selectedSetting?.Item1?.Exists() == true) + IEnumerable elsToShow; + if (customEntries != null) { - item.RightLabel = selectedSetting.Item1.Name + " →"; + elsToShow = customEntries.Where(e => e.Exists() && ((IncludeBuiltInSettings && !e.IsCustomSetting()) || (IncludeCustomSettings && e.IsCustomSetting()))); } else { - item.RightLabel = "~c~none~w~"; + elsToShow = EmergencyLighting.Get(IncludeBuiltInSettings, IncludeCustomSettings).Where(e => e.Exists()); } - } - private void OnBoundMenuItemActivated(UIMenu sender, UIMenuItem selectedItem) - { - this.RefreshSirenSettingList(); + // Add any new lighting entries + foreach (EmergencyLighting els in elsToShow) + { + if (forceUpdateAll || !elsEntries.ContainsKey(els)) + { + if (!elsEntries.TryGetValue(els, out T menuEntry)) + { + menuEntry = CreateNewSirenSettingMenuItem(els); + elsEntries.Add(els, menuEntry); + Menu.AddItem(menuEntry); + } + + Game.LogTrivialDebug("Added EmergencyLighting entry " + els.Name); + } + } + + // Remove any lighting entries which are no longer valid and update labels for valid items + foreach (EmergencyLighting els in elsEntries.Keys.ToArray()) + { + if(!els.IsValid() || !elsToShow.Contains(els)) + { + RemoveEntry(els); + } else + { + var item = elsEntries[els]; + item.Text = els.Name; + bool isCheckbox = (item is UIMenuCheckboxItem); + bool isCustom = els.IsCustomSetting(); + SirenSource src = els.GetSource(); + + if (isCustom) + { + item.LeftBadge = UIMenuItem.BadgeStyle.Car; + item.Description = "~g~Editable~w~ siren setting entry"; + if (src != null && (src.SourceId > 0 || src.Source == EmergencyLightingSource.Manual)) + { + if (!isCheckbox) item.RightLabel = $"~c~[{src.SourceId}*]"; + item.Description += $" {src.SourceDescription.ToLower()} from siren setting ID ~b~{src.SourceId}~s~"; + } + } + else + { + item.LeftBadge = UIMenuItem.BadgeStyle.Lock; + item.Description = $"~y~Built-in~w~ siren setting entry, siren setting ID ~b~{els.SirenSettingID()}~s~"; + + if (!isCheckbox) item.RightLabel = $"~c~[{els.SirenSettingID()}]"; + + if (AlwaysReturnEditableSetting) + { + item.Description += ". An ~g~editable~w~ copy will be created if you select this setting."; + } + } + } + } + + Menu.RefreshIndex(); } - private void OnBoundMenuUpdated(SirenSettingsSelectionMenu sender, UIMenu menu, SirenSettingMenuItem item, EmergencyLighting setting) + protected virtual void RemoveEntry(EmergencyLighting els) { - UpdateBoundMenuLabel(menu.ParentItem); - // menu.ParentItem.SetRightLabel(setting.Name); + if (elsEntries.ContainsKey(els)) + { + Menu.RemoveItemAt(Menu.MenuItems.IndexOf(elsEntries[els])); + } + elsEntries.Remove(els); + Game.LogTrivialDebug("Removed EmergencyLighting entry " + (els.IsValid() ? " of undesired type" : "for being invalid")); } + } - private void OnMenuItemSelected(UIMenu sender, UIMenuItem selectedItem, int index) + internal class SirenSettingsSelectionMenu : BaseSirenSettingsSelectionMenu + { + public delegate void SirenSettingSelectedEvent(SirenSettingsSelectionMenu sender, UIMenu menu, SirenSettingMenuItem item, EmergencyLighting setting); + public event SirenSettingSelectedEvent OnSirenSettingSelected; + + private Tuple selectedSetting = null; + public EmergencyLighting SelectedEmergencyLighting { - SirenSettingMenuItem selectedSetting = selectedItem as SirenSettingMenuItem; - if(selectedSetting != null) + get { - SetSelectedSetting(selectedSetting); - if (CloseOnSelection) + return selectedSetting?.Item1; + } + + set + { + SirenSettingMenuItem item = null; + if (value != null) { - Menu.GoBack(); + elsEntries.TryGetValue(value, out item); } + SetSelectedSetting(item); } } + public bool CloseOnSelection { get; set; } + + public SirenSettingsSelectionMenu(EmergencyLighting initialSelected, bool closeOnSelect = true, bool builtIn = true, bool custom = true, bool returnEditable = true, IEnumerable customList = null) : base(builtIn, custom, returnEditable, customList) + { + this.CloseOnSelection = closeOnSelect; + SelectedEmergencyLighting = initialSelected; + Menu.OnItemSelect += OnMenuItemSelected; + } + private void SetSelectedSetting(SirenSettingMenuItem item) { - if(item != selectedSetting?.Item2) + if (item != selectedSetting?.Item2) { - if(selectedSetting?.Item2 != null) + if (selectedSetting?.Item2 != null) { selectedSetting.Item2.RightBadge = UIMenuItem.BadgeStyle.Blank; selectedSetting.Item2.BackColor = Color.Empty; } - - if(item == null) + + if (item == null) { selectedSetting = null; - } else + } + else { EmergencyLighting selectedEls = item.ELS; - if(AlwaysReturnEditableSetting && !selectedEls.IsCustomSetting()) + if (AlwaysReturnEditableSetting && !selectedEls.IsCustomSetting()) { selectedEls = selectedEls.CloneWithID(); RefreshSirenSettingList(); @@ -156,118 +273,118 @@ private void SetSelectedSetting(SirenSettingMenuItem item) item.BackColor = Color.DarkGray; } OnSirenSettingSelected?.Invoke(this, Menu, item, item?.ELS); - SelectedSelectedItem(); + SelectSelectedItem(); + UpdateBoundMenuLabel(Menu.ParentItem); } } - private Tuple selectedSetting = null; - public EmergencyLighting SelectedEmergencyLighting + private void SelectSelectedItem() { - get + Menu.RefreshIndex(); + if (selectedSetting != null) { - return selectedSetting?.Item1; + Menu.CurrentSelection = Menu.MenuItems.IndexOf(selectedSetting.Item2); } + } - set - { - SirenSettingMenuItem item = null; - if(value != null) - { - elsEntries.TryGetValue(value, out item); - } - SetSelectedSetting(item); - } + public override void RefreshSirenSettingList(bool forceUpdateAll = false) + { + base.RefreshSirenSettingList(forceUpdateAll); + SelectSelectedItem(); } - public void RefreshSirenSettingList(bool forceUpdateAll = false) + public override void UpdateBoundMenuLabel(UIMenuItem item) { - IEnumerable elsToShow = EmergencyLighting.Get(IncludeBuiltInSettings, IncludeCustomSettings).Where(e => e.Exists()); + if (item == null) return; + if (selectedSetting?.Item1?.Exists() == true) + { + item.RightLabel = selectedSetting.Item1.Name + " →"; + } + else + { + item.RightLabel = "~c~none~w~"; + } + } - // Add any new lighting entries - foreach (EmergencyLighting els in elsToShow) + private void OnMenuItemSelected(UIMenu sender, UIMenuItem selectedItem, int index) + { + SirenSettingMenuItem selectedSetting = selectedItem as SirenSettingMenuItem; + if (selectedSetting != null) { - if (forceUpdateAll || !elsEntries.ContainsKey(els)) + SetSelectedSetting(selectedSetting); + if (CloseOnSelection) { - if (!elsEntries.TryGetValue(els, out SirenSettingMenuItem menuEntry)) - { - menuEntry = new SirenSettingMenuItem(els); - elsEntries.Add(els, menuEntry); - Menu.AddItem(menuEntry); - } - - Game.LogTrivialDebug("Added EmergencyLighting entry " + els.Name); + Menu.GoBack(); } } + } - // Remove any lighting entries which are no longer valid and update labels for valid items - foreach (EmergencyLighting els in elsEntries.Keys.ToArray()) + protected override void RemoveEntry(EmergencyLighting els) + { + base.RemoveEntry(els); + if (SelectedEmergencyLighting == els) { - if(!els.IsValid() || !elsToShow.Contains(els)) - { - if(elsEntries.ContainsKey(els)) - { - Menu.RemoveItemAt(Menu.MenuItems.IndexOf(elsEntries[els])); - if(SelectedEmergencyLighting == els) - { - SetSelectedSetting(null); - } - } - elsEntries.Remove(els); - Game.LogTrivialDebug("Removed EmergencyLighting entry " + (els.IsValid() ? " of undesired type" : "for being invalid")); - } else - { - var menu = elsEntries[els]; - menu.Text = els.Name; + SetSelectedSetting(null); + } + } - bool isCustom = els.IsCustomSetting(); - SirenSource src = els.GetSource(); + protected override SirenSettingMenuItem CreateNewSirenSettingMenuItem(EmergencyLighting els) => new SirenSettingMenuItem(els); + } - if (isCustom) - { - menu.LeftBadge = UIMenuItem.BadgeStyle.Car; - menu.Description = "~g~Editable~w~ siren setting entry"; - if (src != null) - { - menu.RightLabel = $"~c~[{src.SourceId}*]"; - menu.Description += $" {src.SourceDescription.ToLower()} from siren setting ID ~b~{src.SourceId}"; - } - } - else - { - menu.LeftBadge = UIMenuItem.BadgeStyle.Lock; - menu.Description = $"~y~Built-in~w~ siren setting entry, siren setting ID ~b~{els.SirenSettingID()}"; - menu.RightLabel = $"~c~[{els.SirenSettingID()}]"; + + internal class SirenSettingsSelectionMenuMulti : BaseSirenSettingsSelectionMenu + { + public UIMenuItem AcceptMenuItem { get; } + public bool ShowAcceptButton { get; set; } - if (AlwaysReturnEditableSetting) - { - menu.Description += ". An ~g~editable~w~ copy will be created if you select this setting."; - } - } + public EmergencyLighting[] SelectedItems => elsEntries.Where(e => e.Value.Checked).Select(e => e.Key).ToArray(); + + public SirenSettingsSelectionMenuMulti(bool showAcceptButton = false, bool builtIn = true, bool custom = true, bool returnEditable = true, IEnumerable customList = null, IEnumerable initialSelected = null) : base(builtIn, custom, returnEditable, customList) + { + this.ShowAcceptButton = showAcceptButton; + AcceptMenuItem = new UIMenuItem("Accept Selection"); + AcceptMenuItem.HighlightedForeColor = Color.Green; + AcceptMenuItem.RightLabel = "→"; + + if (initialSelected != null) + { + foreach (var item in elsEntries) + { + item.Value.Checked = initialSelected.Contains(item.Key); } } - - SelectedSelectedItem(); } - internal class SirenSettingMenuItem : UIMenuItem + protected override MultiSirenSettingMenuItem CreateNewSirenSettingMenuItem(EmergencyLighting els) => new MultiSirenSettingMenuItem(els); + + public override void UpdateBoundMenuLabel(UIMenuItem item) { - public EmergencyLighting ELS { get; } + int numSelected = SelectedItems.Length; - public SirenSettingMenuItem(EmergencyLighting els) : base(els.Name) + if (numSelected == 0) + { + item.RightLabel = "~c~None selected~s~ →"; + } + else { - this.ELS = els; - this.RightBadge = BadgeStyle.Blank; + item.RightLabel = $"{numSelected} selected →"; } } - private void SelectedSelectedItem() + public override void RefreshSirenSettingList(bool forceUpdateAll = false) { + base.RefreshSirenSettingList(forceUpdateAll); + Menu.MenuItems.Remove(AcceptMenuItem); + if (ShowAcceptButton) Menu.AddItem(AcceptMenuItem); Menu.RefreshIndex(); - if(selectedSetting != null) - { - Menu.CurrentSelection = Menu.MenuItems.IndexOf(selectedSetting.Item2); - } + Menu.OnCheckboxChange += OnCheckboxItemChanged; + } + + private void OnCheckboxItemChanged(UIMenu sender, UIMenuCheckboxItem checkboxItem, bool Checked) + { + UpdateBoundMenuLabel(Menu.ParentItem); } } + } diff --git a/LiveLights/Menu/VehicleMenu.cs b/LiveLights/Menu/VehicleMenu.cs index 5faa838..b6744fb 100644 --- a/LiveLights/Menu/VehicleMenu.cs +++ b/LiveLights/Menu/VehicleMenu.cs @@ -59,6 +59,17 @@ static VehicleMenu() SirenAudioOnItem = new UIMenuRefreshableCheckboxItem("Siren Audio Enabled", false, "Toggle siren audio on this vehicle"); Menu.AddMenuDataBinding(SirenAudioOnItem, (x) => Vehicle.IsSirenSilent = !x, () => !Vehicle.IsSirenSilent); + ExportSelectorItem = new UIMenuItem("Export", "Export siren settings to carcols.meta files"); + ExportSelectorItem.RightLabel = "→"; + Menu.AddItem(ExportSelectorItem); + Menu.BindMenuAndCopyProperties(ImportExportMenu.ExportMenu, ExportSelectorItem); + + ImportSelectorItem = new UIMenuItem("Import", "Import siren settings from carcols.meta files"); + ImportSelectorItem.RightLabel = "→"; + ImportSelectorItem.Activated += ImportExportMenu.OnImportActivated; + Menu.CopyMenuProperties(ImportExportMenu.ImportActiveSettingMenu.Menu); + Menu.AddItem(ImportSelectorItem); + SirenSettingMenu.OnSirenSettingSelected += OnSirenSelectionChanged; Refresh(); @@ -111,7 +122,7 @@ public static void Refresh() } } - private static void OnSirenSelectionChanged(SirenSettingsSelectionMenu sender, UIMenu menu, SirenSettingsSelectionMenu.SirenSettingMenuItem item, EmergencyLighting setting) + private static void OnSirenSelectionChanged(SirenSettingsSelectionMenu sender, UIMenu menu, UIMenuItem item, EmergencyLighting setting) { // EmergencyLighting els = setting.GetCustomOrClone(); if (Vehicle) @@ -176,6 +187,8 @@ private static void OnNonEditableConfigSelected(UIMenu sender, UIMenuItem select public static UIMenuItem UpdateItem { get; } public static SirenSettingsSelectionMenu SirenSettingMenu { get; } public static UIMenuItem SirenSettingSelectorItem { get; } + public static UIMenuItem ExportSelectorItem { get; } + public static UIMenuItem ImportSelectorItem { get; } public static EmergencyLightingMenu SirenConfigMenu { get; private set; } public static UIMenuItem SirenConfigItem { get; } public static UIMenuRefreshableCheckboxItem EmergencyLightsOnItem { get; } From d016ac2936ce6e77e243b926aae1a93bc4876d2a Mon Sep 17 00:00:00 2001 From: PNWParksFan Date: Thu, 13 Oct 2022 23:19:49 -0700 Subject: [PATCH 10/21] Import/export menu improvements --- LiveLights/Menu/EmergencyLightingMenu.cs | 9 ++++---- LiveLights/Menu/ImportExportMenu.cs | 2 +- LiveLights/Menu/SirenSettingsSelectionMenu.cs | 23 +++++++++++++++---- LiveLights/Menu/VehicleMenu.cs | 1 + LiveLights/SirenApply.cs | 6 ++--- LiveLights/Utils/MenuItems.cs | 1 + 6 files changed, 28 insertions(+), 14 deletions(-) diff --git a/LiveLights/Menu/EmergencyLightingMenu.cs b/LiveLights/Menu/EmergencyLightingMenu.cs index b67980a..315709f 100644 --- a/LiveLights/Menu/EmergencyLightingMenu.cs +++ b/LiveLights/Menu/EmergencyLightingMenu.cs @@ -24,6 +24,7 @@ public EmergencyLightingMenu(EmergencyLighting els) Menu.ControlDisablingEnabled = true; Menu.MouseControlsEnabled = false; Menu.AllowCameraMovement = true; + Menu.MaxItemsOnScreen = 15; // Main siren settings @@ -152,9 +153,6 @@ public EmergencyLightingMenu(EmergencyLighting els) Menu.AddItem(ExportCarcolsItem); ExportCarcolsItem.Activated += OnExportClicked; - ExportAllowOverwriteItem = new UIMenuCheckboxItem("Allow overwrite on export", Settings.DefaultOverwrite, "Allow exported carcols.meta files to overwrite existing files with the same name"); - Menu.AddItem(ExportAllowOverwriteItem); - MenuController.Pool.AddAfterYield(Menu, HeadlightsMenu, TaillightsMenu, SequenceQuickEdit.Menu, CopyMenu.Menu); Menu.RefreshIndex(); @@ -171,7 +169,9 @@ private void OnExportClicked(UIMenu sender, UIMenuItem selectedItem) { if(selectedItem == ExportCarcolsItem) { - ImportExportMenu.ExportCarcols(ExportAllowOverwriteItem.Checked, this.ELS); + ImportExportMenu.ExportSelectSettingsMenu.SelectItems(ELS); + Menu.Visible = false; + ImportExportMenu.ExportMenu.Visible = true; } } @@ -264,6 +264,5 @@ private uint GetSourceID() // Import/export public UIMenuItem ExportCarcolsItem { get; } - public UIMenuCheckboxItem ExportAllowOverwriteItem { get; } } } diff --git a/LiveLights/Menu/ImportExportMenu.cs b/LiveLights/Menu/ImportExportMenu.cs index dec7633..6dda48b 100644 --- a/LiveLights/Menu/ImportExportMenu.cs +++ b/LiveLights/Menu/ImportExportMenu.cs @@ -21,7 +21,7 @@ static ImportExportMenu() ExportAllowOverwriteItem = new UIMenuCheckboxItem("Allow overwrite on export", Settings.DefaultOverwrite, "Allow exported carcols.meta files to overwrite existing files with the same name"); ExportSelectSettingsMenu = new SirenSettingsSelectionMenuMulti(returnEditable: false); ExportSelectSettingsMenu.CreateAndBindToSubmenuItem(ExportMenu, "Select settings to export", "Select one or more siren settings to be exported in a single file"); - ExportItem = new UIMenuItem("Export carcols.meta file", "Exports the siren setting currently being modified to a carcols.meta file"); + ExportItem = new UIMenuItem("Export carcols.meta file", "Exports the selected siren settings to a carcols.meta file"); ExportMenu.AddItems(ExportAllowOverwriteItem, ExportItem); ExportItem.Activated += OnExportActivated; diff --git a/LiveLights/Menu/SirenSettingsSelectionMenu.cs b/LiveLights/Menu/SirenSettingsSelectionMenu.cs index d20e57e..3463d47 100644 --- a/LiveLights/Menu/SirenSettingsSelectionMenu.cs +++ b/LiveLights/Menu/SirenSettingsSelectionMenu.cs @@ -340,6 +340,19 @@ internal class SirenSettingsSelectionMenuMulti : BaseSirenSettingsSelectionMenu< public EmergencyLighting[] SelectedItems => elsEntries.Where(e => e.Value.Checked).Select(e => e.Key).ToArray(); + public void SelectItems(bool clearOthers, params EmergencyLighting[] items) + { + foreach (var entry in elsEntries) + { + entry.Value.Checked = items.Contains(entry.Key) || (entry.Value.Checked && !clearOthers); + } + UpdateBoundMenuLabel(Menu.ParentItem); + } + + public void SelectItems(bool clearOthers, IEnumerable items) => SelectItems(clearOthers, items.ToArray()); + public void SelectItems(IEnumerable items) => SelectItems(true, items); + public void SelectItems(params EmergencyLighting[] items) => SelectItems(true, items); + public SirenSettingsSelectionMenuMulti(bool showAcceptButton = false, bool builtIn = true, bool custom = true, bool returnEditable = true, IEnumerable customList = null, IEnumerable initialSelected = null) : base(builtIn, custom, returnEditable, customList) { this.ShowAcceptButton = showAcceptButton; @@ -349,10 +362,7 @@ public SirenSettingsSelectionMenuMulti(bool showAcceptButton = false, bool built if (initialSelected != null) { - foreach (var item in elsEntries) - { - item.Value.Checked = initialSelected.Contains(item.Key); - } + SelectItems(initialSelected); } } @@ -360,11 +370,16 @@ public SirenSettingsSelectionMenuMulti(bool showAcceptButton = false, bool built public override void UpdateBoundMenuLabel(UIMenuItem item) { + if (item == null) return; + int numSelected = SelectedItems.Length; if (numSelected == 0) { item.RightLabel = "~c~None selected~s~ →"; + } else if (numSelected == 1) + { + item.RightLabel = SelectedItems[0].Name; } else { diff --git a/LiveLights/Menu/VehicleMenu.cs b/LiveLights/Menu/VehicleMenu.cs index b6744fb..a6ec3a0 100644 --- a/LiveLights/Menu/VehicleMenu.cs +++ b/LiveLights/Menu/VehicleMenu.cs @@ -23,6 +23,7 @@ static VehicleMenu() Menu.ControlDisablingEnabled = true; Menu.MouseControlsEnabled = false; Menu.AllowCameraMovement = true; + Menu.MaxItemsOnScreen = 15; BannerItem = new UIMenuItem("LiveLights by PNWParksFan", $"LiveLights was created by ~g~PNWParksFan~w~ using the RPH emergency lighting SDK. If you found this plugin useful and made something cool with it, ~y~please mention it in your credits/readme~w~. If you'd like to say thanks, you can donate to support my various modding projects at ~b~parksmods.com/donate~w~ and get member-exclusive perks. Press Enter to learn more!"); BannerItem.RightLabel = "v" + EntryPoint.CurrentFileVersion.ToString(); diff --git a/LiveLights/SirenApply.cs b/LiveLights/SirenApply.cs index 936f427..66810cb 100644 --- a/LiveLights/SirenApply.cs +++ b/LiveLights/SirenApply.cs @@ -15,13 +15,11 @@ internal static class SirenApply { public static void ApplySirenSettingsToEmergencyLighting(this SirenSetting setting, EmergencyLighting els) { - int iName = 1; string name = setting.Name; - do + for (int iName = 1; EmergencyLighting.GetByName(name).Exists(); iName++) { name = $"{setting.Name} ({iName})"; - iName++; - } while (EmergencyLighting.GetByName(name).Exists()); + } els.Name = name; diff --git a/LiveLights/Utils/MenuItems.cs b/LiveLights/Utils/MenuItems.cs index bd2683b..c7caa2b 100644 --- a/LiveLights/Utils/MenuItems.cs +++ b/LiveLights/Utils/MenuItems.cs @@ -591,6 +591,7 @@ public static void CopyMenuProperties(this UIMenu parentMenu, UIMenu newMenu, bo newMenu.MouseControlsEnabled = parentMenu.MouseControlsEnabled; newMenu.MouseEdgeEnabled = parentMenu.MouseEdgeEnabled; newMenu.AllowCameraMovement = parentMenu.AllowCameraMovement; + newMenu.MaxItemsOnScreen = parentMenu.MaxItemsOnScreen; if(recursive) { From baf4ae9ddc539d574622f8b6ee796d670315734b Mon Sep 17 00:00:00 2001 From: PNWParksFan Date: Thu, 13 Oct 2022 23:27:13 -0700 Subject: [PATCH 11/21] Updated readme to reflect new features --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 573e214..fbfcfdd 100644 --- a/README.md +++ b/README.md @@ -41,15 +41,17 @@ If you encounter any bugs, please [submit an issue](https://github.com/pnwparksf - Open the LiveLights menu (`-` key on the main keyboard by default) - The menu will show the default siren setting name for the current vehicle. You can switch to a different siren setting from the menu if you want. - Any siren setting defined in any carcols.ymt/carcols.meta file is not editable in game. You can only edit cloned copies which are created temporarily in memory. Clicking into the Edit Siren Settings menu will automatically create an editable clone of the currently selected siren setting. Once an editable clone has been created, any changes you make to it will only apply to that cloned setting; they will not affect the original version. You can set any individual vehicle to use the cloned copy by selecting it through the menu, but any newly spawned vehicle will use its default, unedited siren setting when spawned. - - Any changes you make in the Edit Siren Settings menu will immediately be applied to all spawned vehicles which have been set to use the clonsed siren setting. + - Any changes you make in the Edit Siren Settings menu will immediately be applied to all spawned vehicles which have been set to use that clonsed siren setting. - Within the main menu you can change all settings which apply to the overall siren setting entry (e.g. BPM, falloff settings, etc.) - There are submenus to edit settings for each individual siren (1-20). The siren submenus siren-specific settings, and have further submenus for corona, flashiness, and rotation settings. - There are separate submenus for headlight and taillight settings. - There is a sequence quick-edit menu which allows you to change the flashiness sequence for all sirens, plus headlights and taillights, from a single menu without having to switch between siren submenus. - - When you are finished configuring your siren setting and are satisfied with the results, you can click the Export item on the main Edit Siren Settings menu. + - You can use the Copy menu to copy settings between sirens within one settings instance, or to copy between different settings instances. Select dynamically whether to copy everything or only certain properties. + - When you are finished configuring your siren setting and are satisfied with the results, you can click the Export item on the Edit Siren Settings menu to export that individual setting, or you can use the Export menu from the main menu to select multiple settings to export to a single file. - You will be prompted for a file location. If you just enter a filename, the setting entry currently being edited will be saved to `GTA V\Plugins\LiveLights\carcols\`. If you enter a path relative to the GTA root folder, your file will be saved to that path, e.g. `LML\police-pack\data\new-carcols.meta`. You can also enter an absolute path and the file will be saved to that exact location even if it is not within the GTA V folder, e.g. `C:\GTA V\mods\police\carcols-2.meta`. - - Exported files will always contain siren ID `0` by default. You will need to edit the siren ID to the value you wish to use in your carvariations.meta. See info below regarding important notes on siren setting ID limitations. - - The exported file will be a fully valid carcols.meta file (except for the ID), but you may want to copy the specific `` entry out of the exported file and add it to another file with multiple entries. + - You can set the siren ID on each edited siren setting as you make changes. This only affects the siren ID that will be exported; it does not do anything in-game, and nothing prevents you from exporting multiple settings with the same ID (which you should probably avoid). + - If you do not specify a siren ID it will be exported as `0` by default. You will need to edit the siren ID to the value you wish to use in your carcols.meta/carvariations.meta. See info below regarding important notes on siren setting ID limitations. + - The exported file will be a fully valid carcols.meta file and can be used directly in a DLC, LML package, or FiveM resource. - All changes will be lost when you exit the game. Make sure to export any savings you want to keep, and add those exported savings to a carcols.meta file which will get loaded by the game. From 4f101a04a8c8b730acfee9487dbd7116ea8f4f68 Mon Sep 17 00:00:00 2001 From: PNWParksFan Date: Thu, 13 Oct 2022 23:33:43 -0700 Subject: [PATCH 12/21] Further tweaks to applying sequences --- LiveLights/SirenApply.cs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/LiveLights/SirenApply.cs b/LiveLights/SirenApply.cs index 66810cb..ba02d46 100644 --- a/LiveLights/SirenApply.cs +++ b/LiveLights/SirenApply.cs @@ -32,13 +32,13 @@ public static void ApplySirenSettingsToEmergencyLighting(this SirenSetting setti els.TextureHash = setting.TextureHash; els.SequencerBpm = setting.SequencerBPM; els.UseRealLights = setting.UseRealLights; - els.LeftHeadLightSequence = setting.LeftHeadLightSequencer; + els.LeftHeadLightSequenceRaw = setting.LeftHeadLightSequencer; els.LeftHeadLightMultiples = setting.LeftHeadLightMultiples; - els.RightHeadLightSequence = setting.RightHeadLightSequencer; + els.RightHeadLightSequenceRaw = setting.RightHeadLightSequencer; els.RightHeadLightMultiples = setting.RightHeadLightMultiples; - els.LeftTailLightSequence = setting.LeftTailLightSequencer; + els.LeftTailLightSequenceRaw = setting.LeftTailLightSequencer; els.LeftTailLightMultiples = setting.LeftTailLightMultiples; - els.RightTailLightSequence = setting.RightTailLightSequencer; + els.RightTailLightSequenceRaw = setting.RightTailLightSequencer; els.RightTailLightMultiples = setting.RightTailLightMultiples; for (int i = 0; i < setting.Sirens.Length; i++) @@ -68,7 +68,7 @@ public static void ApplySirenSettingsToEmergencyLighting(this SirenSetting setti light.RotationDelta = entry.Rotation.DeltaDeg; light.RotationStart = entry.Rotation.StartDeg; light.RotationSpeed = entry.Rotation.Speed; - light.RotationSequenceRaw = entry.Rotation.Sequence.Value; + light.RotationSequenceRaw = entry.Rotation.Sequence; light.RotationMultiples = entry.Rotation.Multiples; light.RotationDirection = entry.Rotation.Direction; light.RotationSynchronizeToBpm = entry.Rotation.SyncToBPM; @@ -77,7 +77,7 @@ public static void ApplySirenSettingsToEmergencyLighting(this SirenSetting setti light.FlashinessDelta = entry.Flashiness.DeltaDeg; light.FlashinessStart = entry.Flashiness.StartDeg; light.FlashinessSpeed = entry.Flashiness.Speed; - light.FlashinessSequenceRaw = entry.Flashiness.Sequence.Value; + light.FlashinessSequenceRaw = entry.Flashiness.Sequence; light.FlashinessMultiples = entry.Flashiness.Multiples; light.FlashinessDirection = entry.Flashiness.Direction; light.FlashinessSynchronizeToBpm = entry.Flashiness.SyncToBPM; @@ -96,13 +96,13 @@ public static void ExportEmergencyLightingToSirenSettings(this EmergencyLighting setting.TextureHash = els.TextureHash; setting.SequencerBPM = els.SequencerBpm; setting.UseRealLights = els.UseRealLights; - setting.LeftHeadLightSequencer = els.LeftHeadLightSequence; + setting.LeftHeadLightSequencer = els.LeftHeadLightSequenceRaw; setting.LeftHeadLightMultiples = els.LeftHeadLightMultiples; - setting.RightHeadLightSequencer = els.RightHeadLightSequence; + setting.RightHeadLightSequencer = els.RightHeadLightSequenceRaw; setting.RightHeadLightMultiples = els.RightHeadLightMultiples; - setting.LeftTailLightSequencer = els.LeftTailLightSequence; + setting.LeftTailLightSequencer = els.LeftTailLightSequenceRaw; setting.LeftTailLightMultiples = els.LeftTailLightMultiples; - setting.RightTailLightSequencer = els.RightTailLightSequence; + setting.RightTailLightSequencer = els.RightTailLightSequenceRaw; setting.RightTailLightMultiples = els.RightTailLightMultiples; for (int i = 0; i < els.Lights.Length; i++) @@ -132,7 +132,7 @@ public static void ExportEmergencyLightingToSirenSettings(this EmergencyLighting entry.Rotation.DeltaDeg = light.RotationDelta; entry.Rotation.StartDeg = light.RotationStart; entry.Rotation.Speed = light.RotationSpeed; - entry.Rotation.Sequence = light.RotationSequence; + entry.Rotation.Sequence = light.RotationSequenceRaw; entry.Rotation.Multiples = light.RotationMultiples; entry.Rotation.Direction = light.RotationDirection; entry.Rotation.SyncToBPM = light.RotationSynchronizeToBpm; @@ -141,7 +141,7 @@ public static void ExportEmergencyLightingToSirenSettings(this EmergencyLighting entry.Flashiness.DeltaDeg = light.FlashinessDelta; entry.Flashiness.StartDeg = light.FlashinessStart; entry.Flashiness.Speed = light.FlashinessSpeed; - entry.Flashiness.Sequence = light.FlashinessSequence; + entry.Flashiness.Sequence = light.FlashinessSequenceRaw; entry.Flashiness.Multiples = light.FlashinessMultiples; entry.Flashiness.Direction = light.FlashinessDirection; entry.Flashiness.SyncToBPM = light.FlashinessSynchronizeToBpm; From 87c1d1cc5a633250c2254d4a5e60d2d9b0838102 Mon Sep 17 00:00:00 2001 From: PNWParksFan Date: Thu, 13 Oct 2022 23:36:26 -0700 Subject: [PATCH 13/21] oops --- LiveLights/Utils/UserInput.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LiveLights/Utils/UserInput.cs b/LiveLights/Utils/UserInput.cs index d9db2fa..fa5222c 100644 --- a/LiveLights/Utils/UserInput.cs +++ b/LiveLights/Utils/UserInput.cs @@ -13,7 +13,7 @@ namespace LiveLights.Utils using Rage; using Rage.Native; - internal class UserInput + internal static class UserInput { public static string GetUserInput(string windowTitle, string defaultText, int maxLength, bool canCopy = true, bool canPaste = true) { From 3aa274f1e79a0b1c7bfa195b8f0d7cc52e599940 Mon Sep 17 00:00:00 2001 From: PNWParksFan Date: Thu, 13 Oct 2022 23:42:24 -0700 Subject: [PATCH 14/21] =?UTF-8?q?1.0=20=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LiveLights/Properties/AssemblyInfo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LiveLights/Properties/AssemblyInfo.cs b/LiveLights/Properties/AssemblyInfo.cs index 9aa5404..ad79a86 100644 --- a/LiveLights/Properties/AssemblyInfo.cs +++ b/LiveLights/Properties/AssemblyInfo.cs @@ -31,8 +31,8 @@ // // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: -[assembly: AssemblyVersion("0.7.*")] +[assembly: AssemblyVersion("1.0.*")] // This is required for Octokit to check the version against github -[assembly: AssemblyInformationalVersion("0.7 RC")] +[assembly: AssemblyInformationalVersion("1.0")] // [assembly: AssemblyVersion("1.0.0.0")] // [assembly: AssemblyFileVersion("1.0.0.0")] From 5e1470a180585bb5e47de3246ea02b1465ed14ef Mon Sep 17 00:00:00 2001 From: PNWParksFan Date: Fri, 14 Oct 2022 01:51:51 -0700 Subject: [PATCH 15/21] Iniital support for >20 sirens --- LiveLights/Menu/CommonSelectionItems.cs | 2 +- LiveLights/Menu/EmergencyLightingMenu.cs | 81 ++++++++++---- LiveLights/Menu/MenuController.cs | 2 +- LiveLights/Menu/SequenceQuickEditMenu.cs | 1 + LiveLights/Menu/SirenIdMultiselectMenu.cs | 6 +- LiveLights/Settings.cs | 3 + LiveLights/SirenApply.cs | 128 +++++++++++++++------- LiveLights/SirenSetting.cs | 11 +- LiveLights/Utils/SirenExtensions.cs | 4 +- 9 files changed, 163 insertions(+), 75 deletions(-) diff --git a/LiveLights/Menu/CommonSelectionItems.cs b/LiveLights/Menu/CommonSelectionItems.cs index 49175a4..5b996fc 100644 --- a/LiveLights/Menu/CommonSelectionItems.cs +++ b/LiveLights/Menu/CommonSelectionItems.cs @@ -17,6 +17,6 @@ internal static class CommonSelectionItems public static float[] IntensityFloat => new float[] { 0.5f, 1.0f, 2.0f, 4.0f }; public static IEnumerable LightGroupByte => Enumerable.Range(0, 4).Select(x => (byte)x); public static byte[] ScaleFactorByte => new byte[] { 0, 2, 4, 10, 20 }; - public static IEnumerable SirensOrAll => Enumerable.Range(1, 20).Select(s => new DisplayItem(s, $"Siren {s}")).Concat(new IDisplayItem[] { new DisplayItem(-1, "1-to-1") }); + public static IEnumerable SirensOrAll => Enumerable.Range(1, Settings.MaxSirens).Select(s => new DisplayItem(s, $"Siren {s}")).Concat(new IDisplayItem[] { new DisplayItem(-1, "1-to-1") }); } } diff --git a/LiveLights/Menu/EmergencyLightingMenu.cs b/LiveLights/Menu/EmergencyLightingMenu.cs index 315709f..860b036 100644 --- a/LiveLights/Menu/EmergencyLightingMenu.cs +++ b/LiveLights/Menu/EmergencyLightingMenu.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using System.Drawing; namespace LiveLights.Menu @@ -74,12 +75,14 @@ public EmergencyLightingMenu(EmergencyLighting els) HeadlightsMenu.AddMenuDataBinding(LeftHeadlightMultiplesItem, (x) => ELS.LeftHeadLightMultiples = x, () => ELS.LeftHeadLightMultiples); LeftHeadlightSequenceItem = new UIMenuSequenceItemSelector("Front Left Sequence", ELS.LeftHeadLightSequence, "Left headlight flash pattern sequence"); + LeftHeadlightSequenceItem.MenuItem.RightBadge = UIMenuItem.BadgeStyle.Blank; HeadlightsMenu.AddMenuDataBinding(LeftHeadlightSequenceItem, (x) => ELS.LeftHeadLightSequence = x, () => ELS.LeftHeadLightSequence); RightHeadlightMultiplesItem = new UIMenuListItemSelector("Front Right Multiples", "Right headlight multiples per flash", ELS.RightHeadLightMultiples, CommonSelectionItems.MultiplesBytes); HeadlightsMenu.AddMenuDataBinding(RightHeadlightMultiplesItem, (x) => ELS.RightHeadLightMultiples = x, () => ELS.RightHeadLightMultiples); RightHeadlightSequenceItem = new UIMenuSequenceItemSelector("Front Right Sequence", ELS.RightHeadLightSequence, "Right headlight flash pattern sequence"); + RightHeadlightSequenceItem.MenuItem.RightBadge = UIMenuItem.BadgeStyle.Blank; HeadlightsMenu.AddMenuDataBinding(RightHeadlightSequenceItem, (x) => ELS.RightHeadLightSequence = x, () => ELS.RightHeadLightSequence); // Taillights @@ -94,12 +97,14 @@ public EmergencyLightingMenu(EmergencyLighting els) TaillightsMenu.AddMenuDataBinding(LeftTaillightMultiplesItem, (x) => ELS.LeftTailLightMultiples = x, () => ELS.LeftTailLightMultiples); LeftTaillightSequenceItem = new UIMenuSequenceItemSelector("Left Rear Sequence", ELS.LeftTailLightSequence, "Left Taillight flash pattern sequence"); + LeftTaillightSequenceItem.MenuItem.RightBadge = UIMenuItem.BadgeStyle.Blank; TaillightsMenu.AddMenuDataBinding(LeftTaillightSequenceItem, (x) => ELS.LeftTailLightSequence = x, () => ELS.LeftTailLightSequence); RightTaillightMultiplesItem = new UIMenuListItemSelector("Right Rear Multiples", "Right Taillight multiples per flash", ELS.RightTailLightMultiples, CommonSelectionItems.MultiplesBytes); TaillightsMenu.AddMenuDataBinding(RightTaillightMultiplesItem, (x) => ELS.RightTailLightMultiples = x, () => ELS.RightTailLightMultiples); RightTaillightSequenceItem = new UIMenuSequenceItemSelector("Right Rear Sequence", ELS.RightTailLightSequence, "Right Taillight flash pattern sequence"); + RightTaillightSequenceItem.MenuItem.RightBadge = UIMenuItem.BadgeStyle.Blank; TaillightsMenu.AddMenuDataBinding(RightTaillightSequenceItem, (x) => ELS.RightTailLightSequence = x, () => ELS.RightTailLightSequence); // Sirens @@ -108,10 +113,9 @@ public EmergencyLightingMenu(EmergencyLighting els) SirensMenuItem.RightLabel = "→"; Menu.AddItem(SirensMenuItem, 3); SirensMenuItem.Activated += OnSirenSubmenuActivated; - SirenMenus = new List(); - + // Create each siren menu - for (int i = 0; i < 20; i++) + for (int i = 0; i < Settings.MaxSirens; i++) { EmergencyLightMenu sirenMenu = new EmergencyLightMenu(ELS, i); sirenMenu.Menu.ParentItem = SirensMenuItem; @@ -119,7 +123,7 @@ public EmergencyLightingMenu(EmergencyLighting els) Menu.AddSubMenuBinding(sirenMenu.Menu); Menu.CopyMenuProperties(sirenMenu.Menu, true); MenuController.Pool.AddMenuAndSubMenusToPool(sirenMenu.Menu, true); - SirenMenus.Add(sirenMenu); + SirenMenus[i] = sirenMenu; } // Create switcher and add to menus @@ -181,29 +185,67 @@ private void OnSirenSubmenuActivated(UIMenu sender, UIMenuItem selectedItem) SirenSwitcherItem.SwitchMenuItem.CurrentMenu.Visible = true; } - public void ShowSirenPositions(Vehicle v, bool selectedOnly) + public void ShowSirenInfo(Vehicle v) { - if (!v) return; - - foreach (EmergencyLightMenu sirenMenu in SirenSubMenus) - { - if (!selectedOnly || sirenMenu.Menu.Visible || sirenMenu.Menu.Children.Values.Any(c => c.Visible)) + int currentSirenId = SirenSwitcherItem.ItemValue; + var switcher = SirenSwitcherItem.MenuItem; + switcher.Description = "Select the siren to edit. Press ~b~ENTER~w~ to type a siren ID.\n"; + + if (v) + { + switcher.Description += $"The current vehicle ({v.Model.Name}) "; + if (v.HasSiren(currentSirenId)) + { + switcher.Description += $"~g~has~w~ Siren {currentSirenId}"; + switcher.RightBadge = UIMenuItem.BadgeStyle.Tick; + switcher.RightBadgeInfo.Color = Color.Green; + } + else { - v.ShowSirenMarker(sirenMenu.SirenID); + switcher.Description += $"~r~does not have~s~ Siren {currentSirenId}, but other vehicle models using this siren setting might"; + switcher.RightBadge = UIMenuItem.BadgeStyle.Alert; + switcher.RightBadgeInfo.Color = Color.Yellow; } + } else + { + switcher.Description += "~c~No vehicle is currently selected"; + switcher.RightBadge = UIMenuItem.BadgeStyle.Alert; + switcher.RightBadgeInfo.Color = Color.DarkGray; + } + + var currentMenu = SirenMenus[currentSirenId - 1]; + if (v && currentMenu.Menu.Visible || currentMenu.Menu.Children.Values.Any(c => c.Visible)) + { + v.ShowSirenMarker(currentSirenId); } - for (int i = 0; i < SequenceQuickEdit.SirenSequenceItems.Length; i++) + if (SequenceQuickEdit.Menu.Visible) { - int sirenId = i + 1; - UIMenuStringSelector item = SequenceQuickEdit.SirenSequenceItems[i]; - if(SequenceQuickEdit.Menu.Visible && item.MenuItem.Selected) + for (int i = 0; i < SequenceQuickEdit.SirenSequenceItems.Length - 4; i++) { - v.ShowSirenMarker(i+1); + int sirenId = i + 1; + var item = SequenceQuickEdit.SirenSequenceItems[i]; + if (v) + { + if (item.MenuItem.Selected) v.ShowSirenMarker(i + 1); + + if (v.HasSiren(sirenId)) + { + item.MenuItem.RightBadge = UIMenuItem.BadgeStyle.Car; + item.MenuItem.RightBadgeInfo.Color = Color.DarkGray; + } else + { + item.MenuItem.RightBadge = UIMenuItem.BadgeStyle.Alert; + item.MenuItem.RightBadgeInfo.Color = Color.DarkGray; + } + } else + { + item.MenuItem.RightBadge = UIMenuItem.BadgeStyle.Blank; + } } } - - CopyMenu.ProcessShowSirens(v); + + if (CopyMenu.Menu.Visible) CopyMenu.ProcessShowSirens(v); } private uint GetSourceID() @@ -251,8 +293,7 @@ private uint GetSourceID() // Sirens menu public UIMenuItem SirensMenuItem { get; } public UIMenuSwitchSelectable SirenSwitcherItem { get; } - private List SirenMenus { get; } - public EmergencyLightMenu[] SirenSubMenus => SirenMenus.ToArray(); + public EmergencyLightMenu[] SirenMenus { get; } = new EmergencyLightMenu[Settings.MaxSirens]; // Quick edit menu public UIMenuItem SequenceQuickEditItem { get; } diff --git a/LiveLights/Menu/MenuController.cs b/LiveLights/Menu/MenuController.cs index 84ae7fa..18309a9 100644 --- a/LiveLights/Menu/MenuController.cs +++ b/LiveLights/Menu/MenuController.cs @@ -30,7 +30,7 @@ internal static void Process() VehicleMenu.Menu.Visible = !VehicleMenu.Menu.Visible; } - VehicleMenu.SirenConfigMenu?.ShowSirenPositions(VehicleMenu.Vehicle, true); + VehicleMenu.SirenConfigMenu?.ShowSirenInfo(VehicleMenu.Vehicle); VehicleMenu.SirenConfigMenu?.SequenceQuickEdit?.Process(); diff --git a/LiveLights/Menu/SequenceQuickEditMenu.cs b/LiveLights/Menu/SequenceQuickEditMenu.cs index fca1752..0daecd5 100644 --- a/LiveLights/Menu/SequenceQuickEditMenu.cs +++ b/LiveLights/Menu/SequenceQuickEditMenu.cs @@ -28,6 +28,7 @@ public SequenceQuickEditMenu(EmergencyLighting els, EmergencyLightingMenu parent EmergencyLight siren = ELS.Lights[i]; string sirenId = $"Siren {i + 1}"; UIMenuSequenceItemSelector item = new UIMenuSequenceItemSelector($"{sirenId} Sequence", siren.FlashinessSequence, $"Edit 32-bit sequence for {sirenId}"); + item.MenuItem.RightBadge = UIMenuItem.BadgeStyle.Blank; Menu.AddMenuDataBinding(item, (x) => siren.FlashinessSequence = x, () => siren.FlashinessSequence); sirenSequenceItems.Add(item); } diff --git a/LiveLights/Menu/SirenIdMultiselectMenu.cs b/LiveLights/Menu/SirenIdMultiselectMenu.cs index b3b2ee4..609622b 100644 --- a/LiveLights/Menu/SirenIdMultiselectMenu.cs +++ b/LiveLights/Menu/SirenIdMultiselectMenu.cs @@ -18,7 +18,7 @@ public SirenIdMultiselectMenu(string desc = "Select {siren}") { Menu.AddItem(SelectAllItem); - for (int i = 0; i < 20; i++) + for (int i = 0; i < Settings.MaxSirens; i++) { string sirenId = $"Siren {i + 1}"; UIMenuCheckboxItem checkbox = new UIMenuCheckboxItem(sirenId, false, desc.Replace("{siren}", sirenId)); @@ -51,7 +51,7 @@ private void OnCheckboxChanged(UIMenu sender, UIMenuCheckboxItem checkboxItem, b if(sirens.Length == 0) { label = "None selected"; - } else if (sirens.Length == 20) + } else if (sirens.Length == Settings.MaxSirens) { label = "All"; } else @@ -66,7 +66,7 @@ private void OnCheckboxChanged(UIMenu sender, UIMenuCheckboxItem checkboxItem, b public UIMenu Menu { get; } = new UIMenu("Select Siren IDs", "~b~"); public UIMenuCheckboxItem SelectAllItem { get; } = new UIMenuCheckboxItem("Select All", false, "Select or deselect all siren IDs"); // 0-indexed array - private UIMenuCheckboxItem[] checkboxes = new UIMenuCheckboxItem[20]; + private UIMenuCheckboxItem[] checkboxes = new UIMenuCheckboxItem[Settings.MaxSirens]; public IEnumerable Checkboxes => checkboxes; internal int GetHighlightedSirenId() diff --git a/LiveLights/Settings.cs b/LiveLights/Settings.cs index 9838ba0..a07a37d 100644 --- a/LiveLights/Settings.cs +++ b/LiveLights/Settings.cs @@ -56,5 +56,8 @@ static Settings() } + + // will be replaced with RPH static property + internal const int MaxSirens = 32; } } diff --git a/LiveLights/SirenApply.cs b/LiveLights/SirenApply.cs index ba02d46..4982775 100644 --- a/LiveLights/SirenApply.cs +++ b/LiveLights/SirenApply.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using System.Reflection; using System.Linq.Expressions; +using System.Drawing; namespace LiveLights { @@ -13,7 +14,7 @@ namespace LiveLights internal static class SirenApply { - public static void ApplySirenSettingsToEmergencyLighting(this SirenSetting setting, EmergencyLighting els) + public static void ApplySirenSettingsToEmergencyLighting(this SirenSetting setting, EmergencyLighting els, bool clearExcessSirens = true) { string name = setting.Name; for (int iName = 1; EmergencyLighting.GetByName(name).Exists(); iName++) @@ -41,50 +42,91 @@ public static void ApplySirenSettingsToEmergencyLighting(this SirenSetting setti els.RightTailLightSequenceRaw = setting.RightTailLightSequencer; els.RightTailLightMultiples = setting.RightTailLightMultiples; - for (int i = 0; i < setting.Sirens.Length; i++) + for (int i = 0; i < els.Lights.Length; i++) { - SirenEntry entry = setting.Sirens[i]; EmergencyLight light = els.Lights[i]; - // Main light settings - light.Color = entry.LightColor; - light.Intensity = entry.Intensity; - light.LightGroup = entry.LightGroup; - light.Rotate = entry.Rotate; - light.Scale = entry.Scale; - light.ScaleFactor = entry.ScaleFactor; - light.Flash = entry.Flash; - light.SpotLight = entry.SpotLight; - light.CastShadows = entry.CastShadows; - light.Light = entry.Light; - - // Corona settings - light.CoronaIntensity = entry.Corona.CoronaIntensity; - light.CoronaSize = entry.Corona.CoronaSize; - light.CoronaPull = entry.Corona.CoronaPull; - light.CoronaFaceCamera = entry.Corona.CoronaFaceCamera; - - // Rotation settings - light.RotationDelta = entry.Rotation.DeltaDeg; - light.RotationStart = entry.Rotation.StartDeg; - light.RotationSpeed = entry.Rotation.Speed; - light.RotationSequenceRaw = entry.Rotation.Sequence; - light.RotationMultiples = entry.Rotation.Multiples; - light.RotationDirection = entry.Rotation.Direction; - light.RotationSynchronizeToBpm = entry.Rotation.SyncToBPM; - - // Flash settings - light.FlashinessDelta = entry.Flashiness.DeltaDeg; - light.FlashinessStart = entry.Flashiness.StartDeg; - light.FlashinessSpeed = entry.Flashiness.Speed; - light.FlashinessSequenceRaw = entry.Flashiness.Sequence; - light.FlashinessMultiples = entry.Flashiness.Multiples; - light.FlashinessDirection = entry.Flashiness.Direction; - light.FlashinessSynchronizeToBpm = entry.Flashiness.SyncToBPM; + if (i < setting.Sirens.Length) + { + SirenEntry entry = setting.Sirens[i]; + + // Main light settings + light.Color = entry.LightColor; + light.Intensity = entry.Intensity; + light.LightGroup = entry.LightGroup; + light.Rotate = entry.Rotate; + light.Scale = entry.Scale; + light.ScaleFactor = entry.ScaleFactor; + light.Flash = entry.Flash; + light.SpotLight = entry.SpotLight; + light.CastShadows = entry.CastShadows; + light.Light = entry.Light; + + // Corona settings + light.CoronaIntensity = entry.Corona.CoronaIntensity; + light.CoronaSize = entry.Corona.CoronaSize; + light.CoronaPull = entry.Corona.CoronaPull; + light.CoronaFaceCamera = entry.Corona.CoronaFaceCamera; + + // Rotation settings + light.RotationDelta = entry.Rotation.DeltaDeg; + light.RotationStart = entry.Rotation.StartDeg; + light.RotationSpeed = entry.Rotation.Speed; + light.RotationSequenceRaw = entry.Rotation.Sequence; + light.RotationMultiples = entry.Rotation.Multiples; + light.RotationDirection = entry.Rotation.Direction; + light.RotationSynchronizeToBpm = entry.Rotation.SyncToBPM; + + // Flash settings + light.FlashinessDelta = entry.Flashiness.DeltaDeg; + light.FlashinessStart = entry.Flashiness.StartDeg; + light.FlashinessSpeed = entry.Flashiness.Speed; + light.FlashinessSequenceRaw = entry.Flashiness.Sequence; + light.FlashinessMultiples = entry.Flashiness.Multiples; + light.FlashinessDirection = entry.Flashiness.Direction; + light.FlashinessSynchronizeToBpm = entry.Flashiness.SyncToBPM; + } else if (clearExcessSirens) + { + // Main light settings + light.Color = Color.Black; + light.Intensity = 0; + light.LightGroup = 0; + light.Rotate = false; + light.Scale = false; + light.ScaleFactor = 0; + light.Flash = false; + light.SpotLight = false; + light.CastShadows = false; + light.Light = false; + + // Corona settings + light.CoronaIntensity = 0; + light.CoronaSize = 0; + light.CoronaPull = 0; + light.CoronaFaceCamera = false; + + // Rotation settings + light.RotationDelta = 0; + light.RotationStart = 0; + light.RotationSpeed = 0; + light.RotationSequenceRaw = 0; + light.RotationMultiples = 0; + light.RotationDirection = false; + light.RotationSynchronizeToBpm = false; + + // Flash settings + light.FlashinessDelta = 0; + light.FlashinessStart = 0; + light.FlashinessSpeed = 0; + light.FlashinessSequenceRaw = 0; + light.FlashinessMultiples = 0; + light.FlashinessDirection = false; + light.FlashinessSynchronizeToBpm = false; + } } } - public static void ExportEmergencyLightingToSirenSettings(this EmergencyLighting els, ref SirenSetting setting) + public static void ExportEmergencyLightingToSirenSettings(this EmergencyLighting els, ref SirenSetting setting, int? maxToExport = null) { setting.Name = els.Name; setting.TimeMultiplier = els.TimeMultiplier; @@ -105,8 +147,14 @@ public static void ExportEmergencyLightingToSirenSettings(this EmergencyLighting setting.RightTailLightSequencer = els.RightTailLightSequenceRaw; setting.RightTailLightMultiples = els.RightTailLightMultiples; - for (int i = 0; i < els.Lights.Length; i++) + // if a max is defined, export up to the max or the total available lights + // if a max is not defined, export all available lights + maxToExport = Math.Min(maxToExport ?? els.Lights.Length, els.Lights.Length); + + for (int i = 0; i < maxToExport; i++) { + if (maxToExport.HasValue && i >= maxToExport.Value) break; + SirenEntry entry = new SirenEntry(); EmergencyLight light = els.Lights[i]; diff --git a/LiveLights/SirenSetting.cs b/LiveLights/SirenSetting.cs index be96e23..3ac2c66 100644 --- a/LiveLights/SirenSetting.cs +++ b/LiveLights/SirenSetting.cs @@ -155,15 +155,8 @@ public XmlNode CommentedSirenSettings public void AddSiren(SirenEntry item) { - if (sirenList.Count < 20) - { - item.SirenIdCommentText = "Siren " + (sirenList.Count + 1); - sirenList.Add(item); - } - else - { - throw new IndexOutOfRangeException("A SirenSetting cannot contain more than 20 sirens"); - } + item.SirenIdCommentText = "Siren " + (sirenList.Count + 1); + sirenList.Add(item); } } diff --git a/LiveLights/Utils/SirenExtensions.cs b/LiveLights/Utils/SirenExtensions.cs index 7f5cec0..eba0d63 100644 --- a/LiveLights/Utils/SirenExtensions.cs +++ b/LiveLights/Utils/SirenExtensions.cs @@ -21,7 +21,7 @@ public static void ShowSirenMarker(this Vehicle vehicle, int siren, float size = public static void ShowSirenMarker(this Vehicle vehicle, int siren, Vector3 scale, MarkerStyle style = MarkerStyle.MarkerTypeUpsideDownCone, float verticalOffset = 0.6f) { string boneName = $"siren{siren}"; - if (vehicle && vehicle.HasBone(boneName) && vehicle.EmergencyLighting.Exists()) + if (vehicle && vehicle.HasBone(boneName) && vehicle.EmergencyLighting.Exists() && siren <= vehicle.EmergencyLighting.Lights.Length) { EmergencyLight light = vehicle.EmergencyLighting.Lights[siren - 1]; Vector3 bonePosition = vehicle.GetBonePosition(boneName); @@ -32,6 +32,8 @@ public static void ShowSirenMarker(this Vehicle vehicle, int siren, Vector3 scal } } + public static bool HasSiren(this Vehicle vehicle, int sirenNum) => vehicle.HasBone($"siren{sirenNum}"); + public static uint SirenSettingID(this EmergencyLighting els) { return (uint)typeof(EmergencyLighting).GetProperty("Id", BindingFlags.GetProperty | BindingFlags.NonPublic | BindingFlags.Instance).GetValue(els); From c93480ab76cbb8bf54e9f4293d12a12b5c8f5926 Mon Sep 17 00:00:00 2001 From: PNWParksFan Date: Fri, 14 Oct 2022 23:23:31 -0700 Subject: [PATCH 16/21] Added quick pattern clipboard import --- LiveLights/Menu/EmergencyLightingMenu.cs | 35 +++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/LiveLights/Menu/EmergencyLightingMenu.cs b/LiveLights/Menu/EmergencyLightingMenu.cs index 860b036..959dc94 100644 --- a/LiveLights/Menu/EmergencyLightingMenu.cs +++ b/LiveLights/Menu/EmergencyLightingMenu.cs @@ -143,7 +143,12 @@ public EmergencyLightingMenu(EmergencyLighting els) SequenceQuickEditItem.Activated += OnQuickEditMenuOpened; Menu.AddItem(SequenceQuickEditItem, 4); SequenceQuickEditItem.RightLabel = "→"; - + + SequenceImportItem = new UIMenuItem("Quick Sequence Import", "Import multiple sequences from the clipboard (one per line) to quickly apply them to multiple sirens. ~y~CAUTION!~s~ Immediately overwrites all siren sequences when selected!"); + Menu.AddItem(SequenceImportItem, 5); + SequenceImportItem.RightLabel = "→"; + SequenceImportItem.Activated += OnSequenceQuickImport; + RefreshItem = new UIMenuItem("Refresh Siren Setting Data", "Refreshes the menu with the siren setting data for the current vehicle. Use this if the data may have been changed outside the menu."); Menu.AddRefreshItem(RefreshItem); @@ -162,6 +167,31 @@ public EmergencyLightingMenu(EmergencyLighting els) Menu.RefreshIndex(); } + private void OnSequenceQuickImport(UIMenu sender, UIMenuItem selectedItem) + { + string clipboard = Game.GetClipboardText(); + if (string.IsNullOrWhiteSpace(clipboard)) + { + Game.DisplayNotification("~y~Clipboard is empty"); + } else + { + int siren = 0; + foreach (string line in clipboard.Trim().Split('\n')) + { + string clean = string.Concat(line.Trim().Where(c => c == '1' || c == '0').Take(32)); + if (clean.Length == 32) + { + SirenMenus[siren].FlashSequenceItem.ItemValue = clean; + siren++; + } + + if (siren >= SirenMenus.Length) break; + } + + Game.DisplayNotification($"Imported sequences for ~b~{siren}~s~ sirens"); + } + } + private void OnQuickEditMenuOpened(UIMenu sender, UIMenuItem selectedItem) { // Sequences may have been changed by other menus, need to refresh before showing @@ -299,6 +329,9 @@ private uint GetSourceID() public UIMenuItem SequenceQuickEditItem { get; } public SequenceQuickEditMenu SequenceQuickEdit { get; } + // Import + public UIMenuItem SequenceImportItem { get; } + // Copy menu public CopyMenu CopyMenu { get; } public UIMenuItem CopyMenuItem { get; } From 3b4c57b21dfa849eb36ee6af1e299871204b200e Mon Sep 17 00:00:00 2001 From: PNWParksFan Date: Sat, 15 Oct 2022 00:56:05 -0700 Subject: [PATCH 17/21] Made maxsirens dynamic with a temporary workaround --- LiveLights/Settings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LiveLights/Settings.cs b/LiveLights/Settings.cs index a07a37d..93f73f2 100644 --- a/LiveLights/Settings.cs +++ b/LiveLights/Settings.cs @@ -58,6 +58,6 @@ static Settings() } // will be replaced with RPH static property - internal const int MaxSirens = 32; + internal static int MaxSirens = new EmergencyLighting().Lights.Length; } } From d092b36cfb7fb123c794512c269516ba47f53a1f Mon Sep 17 00:00:00 2001 From: PNWParksFan Date: Sat, 15 Oct 2022 00:57:07 -0700 Subject: [PATCH 18/21] Menu improvements, including max siren export limit and SSLA status check, --- LiveLights/Menu/ImportExportMenu.cs | 11 ++++++- LiveLights/Menu/VehicleMenu.cs | 50 ++++++++++++++++++++++++++--- LiveLights/SirenApply.cs | 4 +-- LiveLights/Utils/MenuItems.cs | 1 + 4 files changed, 58 insertions(+), 8 deletions(-) diff --git a/LiveLights/Menu/ImportExportMenu.cs b/LiveLights/Menu/ImportExportMenu.cs index 6dda48b..782124c 100644 --- a/LiveLights/Menu/ImportExportMenu.cs +++ b/LiveLights/Menu/ImportExportMenu.cs @@ -18,6 +18,14 @@ internal static class ImportExportMenu static ImportExportMenu() { ExportMenu = new UIMenu("Export Siren Settings", ""); + + if (Settings.MaxSirens > 20) + { + MaxExportSirensItem = new UIMenuListItemSelector("Export # siren items", $"Choose the number of siren items to be exported to the carcols.meta file. If you are only using sirens 1-20, export 20. If you are using SSLA to enable >20 sirens, export up to {Settings.MaxSirens}.", Settings.MaxSirens, 20, Settings.MaxSirens); + MaxExportSirensItem.MenuUpdateBinding = (x) => { if (x < 20 || x > Settings.MaxSirens) throw new Exception($"Must export between 20 and {Settings.MaxSirens} sirens"); }; + ExportMenu.AddItem(MaxExportSirensItem); + } + ExportAllowOverwriteItem = new UIMenuCheckboxItem("Allow overwrite on export", Settings.DefaultOverwrite, "Allow exported carcols.meta files to overwrite existing files with the same name"); ExportSelectSettingsMenu = new SirenSettingsSelectionMenuMulti(returnEditable: false); ExportSelectSettingsMenu.CreateAndBindToSubmenuItem(ExportMenu, "Select settings to export", "Select one or more siren settings to be exported in a single file"); @@ -55,6 +63,7 @@ private static void OnExportActivated(UIMenu sender, UIMenuItem selectedItem) } public static UIMenu ExportMenu { get; } + public static UIMenuListItemSelector MaxExportSirensItem { get; } public static UIMenuCheckboxItem ExportAllowOverwriteItem { get; } public static SirenSettingsSelectionMenuMulti ExportSelectSettingsMenu { get; } public static UIMenuItem ExportItem { get; } @@ -169,7 +178,7 @@ public static bool ExportCarcols(bool allowOverwrite, params EmergencyLighting[] foreach (var els in settings) { Game.LogTrivial($" Serializing \"{els.Name}\""); - SirenSetting setting = els.ExportEmergencyLightingToSirenSettings(); + SirenSetting setting = els.ExportEmergencyLightingToSirenSettings(MaxExportSirensItem?.ItemValue); var src = els.GetSource(); if (src != null) setting.ID = src.SourceId; else setting.ID = 0; diff --git a/LiveLights/Menu/VehicleMenu.cs b/LiveLights/Menu/VehicleMenu.cs index a6ec3a0..802af79 100644 --- a/LiveLights/Menu/VehicleMenu.cs +++ b/LiveLights/Menu/VehicleMenu.cs @@ -4,6 +4,8 @@ using System.Text; using System.Threading.Tasks; using System.Drawing; +using System.IO; +using System.Diagnostics; namespace LiveLights.Menu { @@ -28,6 +30,7 @@ static VehicleMenu() BannerItem = new UIMenuItem("LiveLights by PNWParksFan", $"LiveLights was created by ~g~PNWParksFan~w~ using the RPH emergency lighting SDK. If you found this plugin useful and made something cool with it, ~y~please mention it in your credits/readme~w~. If you'd like to say thanks, you can donate to support my various modding projects at ~b~parksmods.com/donate~w~ and get member-exclusive perks. Press Enter to learn more!"); BannerItem.RightLabel = "v" + EntryPoint.CurrentFileVersion.ToString(); BannerItem.LeftBadge = UIMenuItem.BadgeStyle.Heart; + BannerItem.LeftBadgeInfo.Color = Color.LightSkyBlue; BannerItem.BackColor = Color.Black; BannerItem.ForeColor = Color.LightSkyBlue; BannerItem.HighlightedBackColor = Color.LightSkyBlue; @@ -36,7 +39,7 @@ static VehicleMenu() if(EntryPoint.VersionCheck?.IsUpdateAvailable() == true) { - UpdateItem = new UIMenuItem("Update Available", $"Version ~y~{EntryPoint.VersionCheck.LatestRelease.TagName}~w~ is available for download. Press ~b~Enter~w~ to download ~y~{EntryPoint.VersionCheck.LatestRelease.Name}~w~."); + UpdateItem = new UIMenuItem("LiveLights Update Available", $"Version ~y~{EntryPoint.VersionCheck.LatestRelease.TagName}~w~ is available for download. Press ~b~Enter~w~ to download ~y~{EntryPoint.VersionCheck.LatestRelease.Name}~w~."); UpdateItem.RightLabel = "~o~" + EntryPoint.VersionCheck.LatestRelease.TagName; UpdateItem.LeftBadge = UIMenuItem.BadgeStyle.Alert; UpdateItem.BackColor = Color.Black; @@ -45,6 +48,37 @@ static VehicleMenu() Menu.AddItem(UpdateItem); UpdateItem.Activated += OnUpdateClicked; } + + SSLAStatusItem = new UIMenuItem("Siren Setting Limit Adjuster", "Download and install the latest version of ~b~Siren Setting Limit Adjuster~w~ to enable >20 sirens on vehicles and unlimited siren setting IDs. Press ~b~ENTER~w~ to download."); + SSLAStatusItem.BackColor = Color.Black; + SSLAStatusItem.ForeColor = Color.LightSkyBlue; + SSLAStatusItem.HighlightedBackColor = Color.LightSkyBlue; + string sslaFilename = "SirenSetting_Limit_Adjuster.asi"; + if (!File.Exists(sslaFilename)) + { + SSLAStatusItem.Text += " not installed"; + SSLAStatusItem.LeftBadge = UIMenuItem.BadgeStyle.Alert; + SSLAStatusItem.LeftBadgeInfo.Color = Color.Yellow; + } else + { + FileVersionInfo sslaVersion = FileVersionInfo.GetVersionInfo(sslaFilename); + if (sslaVersion.FileMajorPart < 2) + { + SSLAStatusItem.Text = "Update " + SSLAStatusItem.Text; + SSLAStatusItem.LeftBadge = UIMenuItem.BadgeStyle.Alert; + SSLAStatusItem.LeftBadgeInfo.Color = Color.Yellow; + } else + { + SSLAStatusItem.Text = "SSLA Installed"; + SSLAStatusItem.Description = $"~b~Siren Setting Limit Adjuster~w~ is installed and supports up to ~b~{Settings.MaxSirens}~w~ sirens per vehicle. Press ~b~ENTER~w~ to check for SSLA updates."; + SSLAStatusItem.RightLabel = $"{Settings.MaxSirens} sirens supported"; + SSLAStatusItem.LeftBadge = UIMenuItem.BadgeStyle.Tick; + SSLAStatusItem.LeftBadgeInfo.Color = Color.Green; + } + } + SSLAStatusItem.Activated += OnSSLAClicked; + Menu.AddItem(SSLAStatusItem); + SirenSettingMenu = new SirenSettingsSelectionMenu(null, true, true, true, false); SirenSettingSelectorItem = SirenSettingMenu.CreateAndBindToSubmenuItem(Menu); @@ -78,13 +112,18 @@ static VehicleMenu() if(UpdateItem != null) { - Menu.CurrentSelection = 2; + Menu.CurrentSelection = 3; } else { - Menu.CurrentSelection = 1; + Menu.CurrentSelection = 2; } } + private static void OnSSLAClicked(UIMenu sender, UIMenuItem selectedItem) + { + selectedItem.OpenUrl("https://www.gta5-mods.com/scripts/sirensetting-limit-adjuster"); + } + private static void OnBannerClicked(UIMenu sender, UIMenuItem selectedItem) { selectedItem.OpenUrl("https://parksmods.com/donate/"); @@ -100,7 +139,7 @@ public static void Refresh() bool validVehicle = Vehicle.Exists(); foreach (UIMenuItem menuItem in Menu.MenuItems) { - if(menuItem != UpdateItem && menuItem != BannerItem) + if(menuItem != UpdateItem && menuItem != BannerItem && menuItem != SSLAStatusItem) { menuItem.Enabled = validVehicle; } @@ -185,7 +224,9 @@ private static void OnNonEditableConfigSelected(UIMenu sender, UIMenuItem select public static Vehicle Vehicle => Game.LocalPlayer.Character.LastVehicle; public static UIMenuRefreshable Menu { get; } + public static UIMenuItem BannerItem { get; } public static UIMenuItem UpdateItem { get; } + public static UIMenuItem SSLAStatusItem { get; } public static SirenSettingsSelectionMenu SirenSettingMenu { get; } public static UIMenuItem SirenSettingSelectorItem { get; } public static UIMenuItem ExportSelectorItem { get; } @@ -194,6 +235,5 @@ private static void OnNonEditableConfigSelected(UIMenu sender, UIMenuItem select public static UIMenuItem SirenConfigItem { get; } public static UIMenuRefreshableCheckboxItem EmergencyLightsOnItem { get; } public static UIMenuRefreshableCheckboxItem SirenAudioOnItem { get; } - public static UIMenuItem BannerItem { get; } } } diff --git a/LiveLights/SirenApply.cs b/LiveLights/SirenApply.cs index 4982775..e7fbe61 100644 --- a/LiveLights/SirenApply.cs +++ b/LiveLights/SirenApply.cs @@ -198,10 +198,10 @@ public static void ExportEmergencyLightingToSirenSettings(this EmergencyLighting } } - public static SirenSetting ExportEmergencyLightingToSirenSettings(this EmergencyLighting els) + public static SirenSetting ExportEmergencyLightingToSirenSettings(this EmergencyLighting els, int? maxToExport = null) { SirenSetting s = new SirenSetting(); - els.ExportEmergencyLightingToSirenSettings(ref s); + els.ExportEmergencyLightingToSirenSettings(ref s, maxToExport); return s; } diff --git a/LiveLights/Utils/MenuItems.cs b/LiveLights/Utils/MenuItems.cs index c7caa2b..bfe80f8 100644 --- a/LiveLights/Utils/MenuItems.cs +++ b/LiveLights/Utils/MenuItems.cs @@ -641,6 +641,7 @@ public static void OpenUrl(this UIMenuItem item, string url) { System.Diagnostics.Process.Start(url); item.Parent.Visible = false; + GameFiber.Yield(); NativeFunction.Natives.SET_FRONTEND_ACTIVE(true); } } From 4190c35afc30e6b5c289e70ccfd81698fde26332 Mon Sep 17 00:00:00 2001 From: PNWParksFan Date: Tue, 18 Oct 2022 23:56:55 -0700 Subject: [PATCH 19/21] Updated to SDK with fixed EmergencyLighting.GetHashCode and added MaxLights property --- LiveLights/EntryPoint.cs | 4 ++-- LiveLights/Menu/CommonSelectionItems.cs | 3 ++- LiveLights/Menu/EmergencyLightingMenu.cs | 4 ++-- LiveLights/Menu/ImportExportMenu.cs | 8 ++++---- LiveLights/Menu/SirenIdMultiselectMenu.cs | 6 +++--- LiveLights/Menu/VehicleMenu.cs | 4 ++-- LiveLights/Settings.cs | 3 --- LiveLights/Utils/SirenSource.cs | 8 +------- 8 files changed, 16 insertions(+), 24 deletions(-) diff --git a/LiveLights/EntryPoint.cs b/LiveLights/EntryPoint.cs index 42a3c18..21c262e 100644 --- a/LiveLights/EntryPoint.cs +++ b/LiveLights/EntryPoint.cs @@ -27,9 +27,9 @@ private static void Main() // Older versions of RPH do not support the EmergencyLighting API properly FileVersionInfo rphVer = FileVersionInfo.GetVersionInfo("ragepluginhook.exe"); Game.LogTrivial("Detected RPH " + rphVer.FileVersion); - if(rphVer.FileMinorPart < 81) + if(rphVer.FileMinorPart < 98) { - Game.LogTrivial("RPH 81+ is required to use this version of LiveLights"); + Game.LogTrivial("RPH 1.98+ is required to use this version of LiveLights"); Game.DisplayNotification($"~y~Unable to load LiveLights~w~\nRagePluginHook version ~b~81~w~ or later is required, you are on version ~b~{rphVer.FileMinorPart}"); return; } diff --git a/LiveLights/Menu/CommonSelectionItems.cs b/LiveLights/Menu/CommonSelectionItems.cs index 5b996fc..1d8ef3a 100644 --- a/LiveLights/Menu/CommonSelectionItems.cs +++ b/LiveLights/Menu/CommonSelectionItems.cs @@ -8,6 +8,7 @@ namespace LiveLights.Menu { using RAGENativeUI; + using Rage; internal static class CommonSelectionItems { @@ -17,6 +18,6 @@ internal static class CommonSelectionItems public static float[] IntensityFloat => new float[] { 0.5f, 1.0f, 2.0f, 4.0f }; public static IEnumerable LightGroupByte => Enumerable.Range(0, 4).Select(x => (byte)x); public static byte[] ScaleFactorByte => new byte[] { 0, 2, 4, 10, 20 }; - public static IEnumerable SirensOrAll => Enumerable.Range(1, Settings.MaxSirens).Select(s => new DisplayItem(s, $"Siren {s}")).Concat(new IDisplayItem[] { new DisplayItem(-1, "1-to-1") }); + public static IEnumerable SirensOrAll => Enumerable.Range(1, EmergencyLighting.MaxLights).Select(s => new DisplayItem(s, $"Siren {s}")).Concat(new IDisplayItem[] { new DisplayItem(-1, "1-to-1") }); } } diff --git a/LiveLights/Menu/EmergencyLightingMenu.cs b/LiveLights/Menu/EmergencyLightingMenu.cs index 959dc94..a6ff179 100644 --- a/LiveLights/Menu/EmergencyLightingMenu.cs +++ b/LiveLights/Menu/EmergencyLightingMenu.cs @@ -115,7 +115,7 @@ public EmergencyLightingMenu(EmergencyLighting els) SirensMenuItem.Activated += OnSirenSubmenuActivated; // Create each siren menu - for (int i = 0; i < Settings.MaxSirens; i++) + for (int i = 0; i < EmergencyLighting.MaxLights; i++) { EmergencyLightMenu sirenMenu = new EmergencyLightMenu(ELS, i); sirenMenu.Menu.ParentItem = SirensMenuItem; @@ -323,7 +323,7 @@ private uint GetSourceID() // Sirens menu public UIMenuItem SirensMenuItem { get; } public UIMenuSwitchSelectable SirenSwitcherItem { get; } - public EmergencyLightMenu[] SirenMenus { get; } = new EmergencyLightMenu[Settings.MaxSirens]; + public EmergencyLightMenu[] SirenMenus { get; } = new EmergencyLightMenu[EmergencyLighting.MaxLights]; // Quick edit menu public UIMenuItem SequenceQuickEditItem { get; } diff --git a/LiveLights/Menu/ImportExportMenu.cs b/LiveLights/Menu/ImportExportMenu.cs index 782124c..6a7acf1 100644 --- a/LiveLights/Menu/ImportExportMenu.cs +++ b/LiveLights/Menu/ImportExportMenu.cs @@ -19,10 +19,10 @@ static ImportExportMenu() { ExportMenu = new UIMenu("Export Siren Settings", ""); - if (Settings.MaxSirens > 20) + if (EmergencyLighting.MaxLights > 20) { - MaxExportSirensItem = new UIMenuListItemSelector("Export # siren items", $"Choose the number of siren items to be exported to the carcols.meta file. If you are only using sirens 1-20, export 20. If you are using SSLA to enable >20 sirens, export up to {Settings.MaxSirens}.", Settings.MaxSirens, 20, Settings.MaxSirens); - MaxExportSirensItem.MenuUpdateBinding = (x) => { if (x < 20 || x > Settings.MaxSirens) throw new Exception($"Must export between 20 and {Settings.MaxSirens} sirens"); }; + MaxExportSirensItem = new UIMenuListItemSelector("Export # siren items", $"Choose the number of siren items to be exported to the carcols.meta file. If you are only using sirens 1-20, export 20. If you are using SSLA to enable >20 sirens, export up to {EmergencyLighting.MaxLights}.", EmergencyLighting.MaxLights, 20, EmergencyLighting.MaxLights); + MaxExportSirensItem.MenuUpdateBinding = (x) => { if (x < 20 || x > EmergencyLighting.MaxLights) throw new Exception($"Must export between 20 and {EmergencyLighting.MaxLights} sirens"); }; ExportMenu.AddItem(MaxExportSirensItem); } @@ -120,7 +120,7 @@ private static void ImportCarcols() foreach (var setting in carcols.SirenSettings) { Game.LogTrivial($"Importing {setting.Name} from {filename}"); - var els = new EmergencyLighting().GetSafeInstance(); + var els = new EmergencyLighting(); setting.ApplySirenSettingsToEmergencyLighting(els); els.SetSource(setting.ID, EmergencyLightingSource.Imported); newItems.Add(els); diff --git a/LiveLights/Menu/SirenIdMultiselectMenu.cs b/LiveLights/Menu/SirenIdMultiselectMenu.cs index 609622b..edf65db 100644 --- a/LiveLights/Menu/SirenIdMultiselectMenu.cs +++ b/LiveLights/Menu/SirenIdMultiselectMenu.cs @@ -18,7 +18,7 @@ public SirenIdMultiselectMenu(string desc = "Select {siren}") { Menu.AddItem(SelectAllItem); - for (int i = 0; i < Settings.MaxSirens; i++) + for (int i = 0; i < EmergencyLighting.MaxLights; i++) { string sirenId = $"Siren {i + 1}"; UIMenuCheckboxItem checkbox = new UIMenuCheckboxItem(sirenId, false, desc.Replace("{siren}", sirenId)); @@ -51,7 +51,7 @@ private void OnCheckboxChanged(UIMenu sender, UIMenuCheckboxItem checkboxItem, b if(sirens.Length == 0) { label = "None selected"; - } else if (sirens.Length == Settings.MaxSirens) + } else if (sirens.Length == EmergencyLighting.MaxLights) { label = "All"; } else @@ -66,7 +66,7 @@ private void OnCheckboxChanged(UIMenu sender, UIMenuCheckboxItem checkboxItem, b public UIMenu Menu { get; } = new UIMenu("Select Siren IDs", "~b~"); public UIMenuCheckboxItem SelectAllItem { get; } = new UIMenuCheckboxItem("Select All", false, "Select or deselect all siren IDs"); // 0-indexed array - private UIMenuCheckboxItem[] checkboxes = new UIMenuCheckboxItem[Settings.MaxSirens]; + private UIMenuCheckboxItem[] checkboxes = new UIMenuCheckboxItem[EmergencyLighting.MaxLights]; public IEnumerable Checkboxes => checkboxes; internal int GetHighlightedSirenId() diff --git a/LiveLights/Menu/VehicleMenu.cs b/LiveLights/Menu/VehicleMenu.cs index 802af79..2d282b3 100644 --- a/LiveLights/Menu/VehicleMenu.cs +++ b/LiveLights/Menu/VehicleMenu.cs @@ -70,8 +70,8 @@ static VehicleMenu() } else { SSLAStatusItem.Text = "SSLA Installed"; - SSLAStatusItem.Description = $"~b~Siren Setting Limit Adjuster~w~ is installed and supports up to ~b~{Settings.MaxSirens}~w~ sirens per vehicle. Press ~b~ENTER~w~ to check for SSLA updates."; - SSLAStatusItem.RightLabel = $"{Settings.MaxSirens} sirens supported"; + SSLAStatusItem.Description = $"~b~Siren Setting Limit Adjuster~w~ is installed and supports up to ~b~{EmergencyLighting.MaxLights}~w~ sirens per vehicle. Press ~b~ENTER~w~ to check for SSLA updates."; + SSLAStatusItem.RightLabel = $"{EmergencyLighting.MaxLights} sirens supported"; SSLAStatusItem.LeftBadge = UIMenuItem.BadgeStyle.Tick; SSLAStatusItem.LeftBadgeInfo.Color = Color.Green; } diff --git a/LiveLights/Settings.cs b/LiveLights/Settings.cs index 93f73f2..9838ba0 100644 --- a/LiveLights/Settings.cs +++ b/LiveLights/Settings.cs @@ -56,8 +56,5 @@ static Settings() } - - // will be replaced with RPH static property - internal static int MaxSirens = new EmergencyLighting().Lights.Length; } } diff --git a/LiveLights/Utils/SirenSource.cs b/LiveLights/Utils/SirenSource.cs index 6bfc007..1b3635a 100644 --- a/LiveLights/Utils/SirenSource.cs +++ b/LiveLights/Utils/SirenSource.cs @@ -80,16 +80,10 @@ internal static SirenSource GetSource(EmergencyLighting els) internal static class SirenSourceExtensions { - - // this abomination is required because the first instance returned has a different hashcode from - // any subsequent instances returned by Get[ByName], Vehicle.EmergencyLighting, etc. - // hopefully will be fixed soon - public static EmergencyLighting GetSafeInstance(this EmergencyLighting els) => EmergencyLighting.GetByName(els.Name); - public static EmergencyLighting CloneWithID(this EmergencyLighting source) { uint srcId = source.SirenSettingID(); - var clone = source.Clone().GetSafeInstance(); + var clone = source.Clone(); SirenSource.SetSource(clone, srcId, EmergencyLightingSource.Cloned); return clone; } From d5a99abf91742fe4ed8661882c6e57ca1d9e184d Mon Sep 17 00:00:00 2001 From: PNWParksFan Date: Tue, 25 Oct 2022 00:24:43 -0700 Subject: [PATCH 20/21] Updated RPH to 1.98 SDK --- LiveLights/LiveLights.csproj | 2 +- LiveLights/packages.config | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LiveLights/LiveLights.csproj b/LiveLights/LiveLights.csproj index d8d0fab..b953639 100644 --- a/LiveLights/LiveLights.csproj +++ b/LiveLights/LiveLights.csproj @@ -46,7 +46,7 @@ False - ..\packages\RagePluginHook.1.86.1\lib\net472\RagePluginHook.dll + ..\packages\RagePluginHook.1.98.0\lib\net472\RagePluginHook.dll False diff --git a/LiveLights/packages.config b/LiveLights/packages.config index 742380d..c7606bc 100644 --- a/LiveLights/packages.config +++ b/LiveLights/packages.config @@ -5,5 +5,5 @@ - + \ No newline at end of file From 01d3888de18e4a08cbf6cebfeaa5cc14d59e62ac Mon Sep 17 00:00:00 2001 From: PNWParksFan Date: Tue, 25 Oct 2022 00:48:56 -0700 Subject: [PATCH 21/21] Updated readme --- README.md | 47 ++++++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index fbfcfdd..7eadac9 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ This is a RagePluginHook plugin for single-player GTA V. It enables users to view and modify all siren setting parameters live in game and import/export carcols.meta files. -You must have RagePluginHook version 78 or later. +You must have [RagePluginHook](http://ragepluginhook.net/) version 98 or later. +You can download RPH [bundled with LSPDFR](https://www.lcpdfr.com/downloads/gta5mods/g17media/7792-lspd-first-response/) +or from the [RPH Discord server](https://discord.gg/0v9TP1BOmfwZms7y). This plugin is primarily intended as a tool for vehicle developers to create and modify siren settings. Users are welcome to use it to @@ -21,7 +23,7 @@ development projects and get member-exclusive benefits. [Download the latest version from the releases tab](https://github.com/pnwparksfan/rph-live-lights/releases) -[![Latest Version](https://img.shields.io/github/release/pnwparksfan/rph-live-lights?include_prereleases)](https://github.com/pnwparksfan/rph-live-lights/releases) +[![Latest Version](https://img.shields.io/github/release/pnwparksfan/rph-live-lights)](https://github.com/pnwparksfan/rph-live-lights/releases) [![Download Count](https://img.shields.io/github/downloads/pnwparksfan/rph-live-lights/total)](https://github.com/pnwparksfan/rph-live-lights/releases) If you encounter any bugs, please [submit an issue](https://github.com/pnwparksfan/rph-live-lights/issues) or contact me on Discord if we share a mutual server. @@ -57,22 +59,25 @@ If you encounter any bugs, please [submit an issue](https://github.com/pnwparksf   -# Siren Setting ID limit and registry - -As of GTA V build 1868, there is a hard-coded limit of 255 siren setting IDs -in the game. Siren setting IDs over 255 can be entered in carcols.meta, but -will overflow and be converted to a number between 0-255. You can calculate -the "real" siren ID from an ID over 255 using the modulo operator -(`MOD(id, 256)` or `id % 256`) in any programming language or spreadsheet -software. For example, `MOD(1355, 256) = 75`, so if your siren setting is -saved as ID 1355, it will actually be interpreted by the game as 75, and will -conflict with any other siren IDs which are also interpreted as 75 -(331, 587, 1099...). All carcols.meta entries should use an ID between 0-255. - -To avoid conflicts between mods, I have started a public tracker/registry of -siren IDs. Feel free to list your own mods in this tracker. If you already -use your own tracker or are in a modding group which uses a shared tracker, -you can PM me and I can set up the public tracker to include info from -your tracker. - -## [GTA V Siren Setting ID Registry](https://docs.google.com/spreadsheets/d/1MG2BDdboYbfAGGIG3HluLg34Ne3K7kN4FXUtTc4ebtw/edit?usp=sharing) +# Siren Setting ID limit + +GTA V has a hard-coded limit of 255 siren setting IDs in the game. +Siren setting IDs over 255 can be entered in carcols.meta, but +will overflow and be converted to a number between 0-255, which may +conflict with other mods. + +## SirenSetting Limit Adjuster + +It is strongly recommended to install the [SirenSetting Limit Adjuster](https://www.lcpdfr.com/downloads/gta5mods/scripts/28560-sirensetting-limit-adjuster/) +(SSLA). This raises the siren setting ID limit to 65535, and increases the per-vehicle siren +limit from 20 sirens to 32 sirens. LiveLights automatically detects if SSLA is installed and +how many sirens are supported by your game. If SSLA is installed, you can choose to export +carcols.meta files with only 20 sirens, or up to 32 sirens. Select this in the export menu. + + +# Credits + +Special thanks to... + - LMS and MulleDK: for implementing and maintaining support for the EmergencyLighting SDK in RagePluginHook. + - alexguirre: for siren settings research and RageNativeUI + - cpast: for creating the SirenSetting Limit Adjuster