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/LiveLights.csproj b/LiveLights/LiveLights.csproj index d889fbd..b953639 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.98.0\lib\net472\RagePluginHook.dll False @@ -84,6 +84,7 @@ + diff --git a/LiveLights/Menu/CommonSelectionItems.cs b/LiveLights/Menu/CommonSelectionItems.cs index 49175a4..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, 20).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 5b08d92..a6ff179 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 @@ -24,12 +25,16 @@ public EmergencyLightingMenu(EmergencyLighting els) Menu.ControlDisablingEnabled = true; Menu.MouseControlsEnabled = false; Menu.AllowCameraMovement = true; + Menu.MaxItemsOnScreen = 15; // Main siren settings 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); @@ -70,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 @@ -90,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 @@ -103,11 +112,10 @@ 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; - SirenMenus = new List(); - + SirensMenuItem.Activated += OnSirenSubmenuActivated; + // Create each siren menu - for (int i = 0; i < 20; i++) + for (int i = 0; i < EmergencyLighting.MaxLights; i++) { EmergencyLightMenu sirenMenu = new EmergencyLightMenu(ELS, i); sirenMenu.Menu.ParentItem = SirensMenuItem; @@ -115,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 @@ -135,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); @@ -144,25 +157,41 @@ 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; - - 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); + ExportCarcolsItem.Activated += OnExportClicked; MenuController.Pool.AddAfterYield(Menu, HeadlightsMenu, TaillightsMenu, SequenceQuickEdit.Menu, CopyMenu.Menu); 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 @@ -170,53 +199,100 @@ 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) + if(selectedItem == ExportCarcolsItem) { - ImportExportMenu.OnImportCarcols(this); - } else if(selectedItem == ExportCarcolsItem) - { - ImportExportMenu.ExportCarcols(this.ELS, ExportAllowOverwriteItem.Checked); + ImportExportMenu.ExportSelectSettingsMenu.SelectItems(ELS); + Menu.Visible = false; + ImportExportMenu.ExportMenu.Visible = true; } } - private void onSirenSubmenuActivated(UIMenu sender, UIMenuItem selectedItem) + private void OnSirenSubmenuActivated(UIMenu sender, UIMenuItem selectedItem) { sender.Visible = false; 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; } - for (int i = 0; i < SequenceQuickEdit.SirenSequenceItems.Length; i++) + var currentMenu = SirenMenus[currentSirenId - 1]; + if (v && currentMenu.Menu.Visible || currentMenu.Menu.Children.Values.Any(c => c.Visible)) { - int sirenId = i + 1; - UIMenuStringSelector item = SequenceQuickEdit.SirenSequenceItems[i]; - if(SequenceQuickEdit.Menu.Visible && item.MenuItem.Selected) + v.ShowSirenMarker(currentSirenId); + } + + if (SequenceQuickEdit.Menu.Visible) + { + 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; + } } } + + if (CopyMenu.Menu.Visible) CopyMenu.ProcessShowSirens(v); + } - 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; } @@ -247,20 +323,20 @@ public void ShowSirenPositions(Vehicle v, bool selectedOnly) // 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[EmergencyLighting.MaxLights]; // Quick edit menu public UIMenuItem SequenceQuickEditItem { get; } public SequenceQuickEditMenu SequenceQuickEdit { get; } + // Import + public UIMenuItem SequenceImportItem { get; } + // Copy menu public CopyMenu CopyMenu { get; } public UIMenuItem CopyMenuItem { get; } // 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 fe706e4..6a7acf1 100644 --- a/LiveLights/Menu/ImportExportMenu.cs +++ b/LiveLights/Menu/ImportExportMenu.cs @@ -12,8 +12,64 @@ namespace LiveLights.Menu using RAGENativeUI.Elements; using Utils; + internal static class ImportExportMenu { + static ImportExportMenu() + { + ExportMenu = new UIMenu("Export Siren Settings", ""); + + 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 {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); + } + + 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 selected siren settings 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 UIMenuListItemSelector MaxExportSirensItem { 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() @@ -24,51 +80,115 @@ public static void CreateExportFolder() } } - public static void OnImportCarcols(EmergencyLightingMenu menu) + public static (string filename, string filepath) GetFilepath() { - Game.DisplayNotification("~y~Export not implemented yet"); + CreateExportFolder(); + 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 + // 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 bool ExportCarcols(EmergencyLighting els, bool allowOverwrite = false) + private static void ImportCarcols() { - 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 (!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) + { + Game.LogTrivial($"Importing {setting.Name} from {filename}"); + var els = new EmergencyLighting(); + 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}"); + } + + } + + 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)) { 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)); } 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 desired siren ID", "", 3); - if (byte.TryParse(sirenIdStr, out byte sirenId)) - { - setting.ID = sirenId; - } else + foreach (var els in settings) { - 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(MaxExportSirensItem?.ItemValue); + 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/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..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 < 20; 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 == 20) + } 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[20]; + private UIMenuCheckboxItem[] checkboxes = new UIMenuCheckboxItem[EmergencyLighting.MaxLights]; public IEnumerable Checkboxes => checkboxes; internal int GetHighlightedSirenId() diff --git a/LiveLights/Menu/SirenSettingsSelectionMenu.cs b/LiveLights/Menu/SirenSettingsSelectionMenu.cs index 6c17a93..3463d47 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,72 +112,159 @@ 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) { - if(selectedSetting?.Item1?.Exists() == true) + this.RefreshSirenSettingList(); + } + + protected abstract T CreateNewSirenSettingMenuItem(EmergencyLighting els); + + public virtual void RefreshSirenSettingList(bool forceUpdateAll = false) + { + 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.None; + 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.Clone(); + selectedEls = selectedEls.CloneWithID(); RefreshSirenSettingList(); item = elsEntries[selectedEls]; } @@ -156,105 +273,133 @@ 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 + public override void RefreshSirenSettingList(bool forceUpdateAll = false) + { + base.RefreshSirenSettingList(forceUpdateAll); + SelectSelectedItem(); + } + + public override void UpdateBoundMenuLabel(UIMenuItem item) + { + if (item == null) return; + + if (selectedSetting?.Item1?.Exists() == true) { - SirenSettingMenuItem item = null; - if(value != null) - { - elsEntries.TryGetValue(value, out item); - } - SetSelectedSetting(item); + item.RightLabel = selectedSetting.Item1.Name + " →"; + } + else + { + item.RightLabel = "~c~none~w~"; } } - public void RefreshSirenSettingList(bool forceUpdateAll = false) + private void OnMenuItemSelected(UIMenu sender, UIMenuItem selectedItem, int index) { - IEnumerable elsToShow = EmergencyLighting.Get(IncludeBuiltInSettings, IncludeCustomSettings).Where(e => e.Exists()); - - // Remove any lighting entries which are no longer valid - foreach (EmergencyLighting els in elsEntries.Keys.ToArray()) + SirenSettingMenuItem selectedSetting = selectedItem as SirenSettingMenuItem; + if (selectedSetting != null) { - 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 + SetSelectedSetting(selectedSetting); + if (CloseOnSelection) { - elsEntries[els].Text = els.Name; + Menu.GoBack(); } } + } - // Add any new lighting entries - foreach (EmergencyLighting els in elsToShow) + protected override void RemoveEntry(EmergencyLighting els) + { + base.RemoveEntry(els); + if (SelectedEmergencyLighting == els) { - if(forceUpdateAll || !elsEntries.ContainsKey(els)) - { - bool isCustom = els.IsCustomSetting(); - if(!elsEntries.TryGetValue(els, out SirenSettingMenuItem menuEntry)) - { - menuEntry = new SirenSettingMenuItem(els); - elsEntries.Add(els, menuEntry); - Menu.AddItem(menuEntry); - } - - if(isCustom) - { - menuEntry.LeftBadge = UIMenuItem.BadgeStyle.Car; - menuEntry.Description = "~g~Editable~w~ siren setting entry"; - } else - { - menuEntry.LeftBadge = UIMenuItem.BadgeStyle.Lock; - menuEntry.Description = "~y~Built-in~w~ siren setting entry"; - if(AlwaysReturnEditableSetting) - { - menuEntry.Description += ". An ~g~editable~w~ copy will be created if you select this setting."; - } - } - - Game.LogTrivialDebug("Added EmergencyLighting entry " + els.Name); - } + SetSelectedSetting(null); } + } + + protected override SirenSettingMenuItem CreateNewSirenSettingMenuItem(EmergencyLighting els) => new SirenSettingMenuItem(els); + } + + + internal class SirenSettingsSelectionMenuMulti : BaseSirenSettingsSelectionMenu + { + public UIMenuItem AcceptMenuItem { get; } + public bool ShowAcceptButton { get; set; } - SelectedSelectedItem(); + 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); } - internal class SirenSettingMenuItem : UIMenuItem + 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) { - public EmergencyLighting ELS { get; } + this.ShowAcceptButton = showAcceptButton; + AcceptMenuItem = new UIMenuItem("Accept Selection"); + AcceptMenuItem.HighlightedForeColor = Color.Green; + AcceptMenuItem.RightLabel = "→"; - public SirenSettingMenuItem(EmergencyLighting els) : base(els.Name) + if (initialSelected != null) { - this.ELS = els; + SelectItems(initialSelected); } } - private void SelectedSelectedItem() + protected override MultiSirenSettingMenuItem CreateNewSirenSettingMenuItem(EmergencyLighting els) => new MultiSirenSettingMenuItem(els); + + public override void UpdateBoundMenuLabel(UIMenuItem item) { - Menu.RefreshIndex(); - if(selectedSetting != null) + if (item == null) return; + + int numSelected = SelectedItems.Length; + + if (numSelected == 0) { - Menu.CurrentSelection = Menu.MenuItems.IndexOf(selectedSetting.Item2); + item.RightLabel = "~c~None selected~s~ →"; + } else if (numSelected == 1) + { + item.RightLabel = SelectedItems[0].Name; + } + else + { + item.RightLabel = $"{numSelected} selected →"; } } + + public override void RefreshSirenSettingList(bool forceUpdateAll = false) + { + base.RefreshSirenSettingList(forceUpdateAll); + Menu.MenuItems.Remove(AcceptMenuItem); + if (ShowAcceptButton) Menu.AddItem(AcceptMenuItem); + Menu.RefreshIndex(); + 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 dc1e090..2d282b3 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 { @@ -23,10 +25,12 @@ 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(); BannerItem.LeftBadge = UIMenuItem.BadgeStyle.Heart; + BannerItem.LeftBadgeInfo.Color = Color.LightSkyBlue; BannerItem.BackColor = Color.Black; BannerItem.ForeColor = Color.LightSkyBlue; BannerItem.HighlightedBackColor = Color.LightSkyBlue; @@ -35,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; @@ -44,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~{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; + } + } + SSLAStatusItem.Activated += OnSSLAClicked; + Menu.AddItem(SSLAStatusItem); + SirenSettingMenu = new SirenSettingsSelectionMenu(null, true, true, true, false); SirenSettingSelectorItem = SirenSettingMenu.CreateAndBindToSubmenuItem(Menu); @@ -59,6 +94,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(); @@ -66,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/"); @@ -88,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; } @@ -111,7 +162,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) @@ -162,7 +213,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(); @@ -172,13 +224,16 @@ 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; } + public static UIMenuItem ImportSelectorItem { get; } public static EmergencyLightingMenu SirenConfigMenu { get; private set; } public static UIMenuItem SirenConfigItem { get; } public static UIMenuRefreshableCheckboxItem EmergencyLightsOnItem { get; } public static UIMenuRefreshableCheckboxItem SirenAudioOnItem { get; } - public static UIMenuItem BannerItem { get; } } } 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")] 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/SirenApply.cs b/LiveLights/SirenApply.cs index 1d97b18..e7fbe61 100644 --- a/LiveLights/SirenApply.cs +++ b/LiveLights/SirenApply.cs @@ -5,22 +5,22 @@ using System.Threading.Tasks; using System.Reflection; using System.Linq.Expressions; +using System.Drawing; namespace LiveLights { using Rage; + using Utils; 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) { - 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; @@ -33,59 +33,100 @@ 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++) + 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.RotationSequence = 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.FlashinessSequence = 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; @@ -97,17 +138,23 @@ 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++) + // 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]; @@ -133,7 +180,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; @@ -142,7 +189,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; @@ -151,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; } @@ -165,7 +212,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/SirenSetting.cs b/LiveLights/SirenSetting.cs index e647cee..3ac2c66 100644 --- a/LiveLights/SirenSetting.cs +++ b/LiveLights/SirenSetting.cs @@ -33,14 +33,16 @@ 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; @@ -107,6 +109,10 @@ public uint TextureHash [XmlElement("useRealLights")] public ValueItem UseRealLights { get; set; } = true; + // 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")] @@ -123,28 +129,44 @@ public SirenEntry[] Sirens } } + [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(); 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); } } + [XmlType(TypeName = "Item")] public class SirenEntry { [XmlIgnore] internal string SirenIdCommentText { get; set; } - + + [XmlIgnore] [XmlAnyElement("SirenIdComment")] public XmlComment SirenIdComment { @@ -262,7 +284,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)) { } diff --git a/LiveLights/Utils/MenuItems.cs b/LiveLights/Utils/MenuItems.cs index 5e72c23..bfe80f8 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) @@ -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) { @@ -640,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); } } diff --git a/LiveLights/Utils/SirenExtensions.cs b/LiveLights/Utils/SirenExtensions.cs index bedc2d7..eba0d63 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 { @@ -20,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); @@ -31,10 +32,16 @@ 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); + } + 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 +51,7 @@ public static EmergencyLighting GetCustomOrClone(this EmergencyLighting els) return els; } else { - return els.Clone(); + return els.CloneWithID(); } } @@ -52,7 +59,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..1b3635a --- /dev/null +++ b/LiveLights/Utils/SirenSource.cs @@ -0,0 +1,95 @@ +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 + { + public static EmergencyLighting CloneWithID(this EmergencyLighting source) + { + uint srcId = source.SirenSettingID(); + var clone = source.Clone(); + 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..fa5222c 100644 --- a/LiveLights/Utils/UserInput.cs +++ b/LiveLights/Utils/UserInput.cs @@ -5,6 +5,8 @@ using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; +using RAGENativeUI; +using RAGENativeUI.Elements; namespace LiveLights.Utils { @@ -13,79 +15,53 @@ namespace LiveLights.Utils internal static 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(); - } } } diff --git a/LiveLights/packages.config b/LiveLights/packages.config index cab86fd..c7606bc 100644 --- a/LiveLights/packages.config +++ b/LiveLights/packages.config @@ -1,6 +1,9 @@  + + + \ No newline at end of file diff --git a/README.md b/README.md index 573e214..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. @@ -41,36 +43,41 @@ 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.   -# 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