From f9dd588e6a05c179b8ddd758f3f33f7c780d3fcc Mon Sep 17 00:00:00 2001 From: poco <107696090+poco8537@users.noreply.github.com> Date: Tue, 10 Sep 2024 17:52:14 +0900 Subject: [PATCH 01/54] Add weather/cloud/time change security checks --- vMenuServer/MainServer.cs | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/vMenuServer/MainServer.cs b/vMenuServer/MainServer.cs index 9523c5992..d6069be13 100644 --- a/vMenuServer/MainServer.cs +++ b/vMenuServer/MainServer.cs @@ -660,8 +660,13 @@ private void RefreshWeather() /// /// [EventHandler("vMenu:UpdateServerWeather")] - internal void UpdateWeather(string newWeather, bool blackoutNew, bool dynamicWeatherNew, bool enableSnow) + internal void UpdateWeather([FromSource] Player source, string newWeather, bool blackoutNew, bool dynamicWeatherNew, bool enableSnow) { + if (source != null && !IsPlayerAceAllowed(source.Handle, "vMenu.WeatherOptions.Menu") && !IsPlayerAceAllowed(source.Handle, "vMenu.WeatherOptions.All")) + { + BanManager.BanCheater(source); + return; + } // Automatically enable snow effects whenever one of the snow weather types is selected. if (newWeather is "XMAS" or "SNOWLIGHT" or "SNOW" or "BLIZZARD") @@ -684,8 +689,14 @@ internal void UpdateWeather(string newWeather, bool blackoutNew, bool dynamicWea /// /// [EventHandler("vMenu:UpdateServerWeatherCloudsType")] - internal void UpdateWeatherCloudsType(bool removeClouds) + internal void UpdateWeatherCloudsType([FromSource] Player source, bool removeClouds) { + if (source != null && !IsPlayerAceAllowed(source.Handle, "vMenu.WeatherOptions.RemoveClouds") && !IsPlayerAceAllowed(source.Handle, "vMenu.WeatherOptions.RandomizeClouds")) + { + BanManager.BanCheater(source); + return; + } + if (removeClouds) { TriggerClientEvent("vMenu:SetClouds", 0f, "removed"); @@ -705,8 +716,14 @@ internal void UpdateWeatherCloudsType(bool removeClouds) /// /// [EventHandler("vMenu:UpdateServerTime")] - internal void UpdateTime(int newHours, int newMinutes, bool freezeTimeNew) + internal void UpdateTime([FromSource] Player source, int newHours, int newMinutes, bool freezeTimeNew) { + if (source != null && !IsPlayerAceAllowed(source.Handle, "vMenu.TimeOptions.Menu") && !IsPlayerAceAllowed(source.Handle, "vMenu.TimeOptions.All")) + { + BanManager.BanCheater(source); + return; + } + CurrentHours = newHours; CurrentMinutes = newMinutes; FreezeTime = freezeTimeNew; From ba004978a535adee92378a396adcbef5da4afdd9 Mon Sep 17 00:00:00 2001 From: FRSTR-5M Date: Fri, 11 Jul 2025 04:29:42 +0700 Subject: [PATCH 02/54] Added vehicle color preset menu Implementation of a dynamic vehicle color preset selection submenu within the main color menu. Also adding the use of the native prologue label for North Yankton license plate naming. Could be useful if the server owner wants to change the license plate names and overwrite the existing label. Small fix to the Custom RGB color submenu arrows label to better match the rest of the menu style. --- vMenu/menus/VehicleOptions.cs | 50 +++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/vMenu/menus/VehicleOptions.cs b/vMenu/menus/VehicleOptions.cs index 19f29b6e0..d10a1a7a9 100644 --- a/vMenu/menus/VehicleOptions.cs +++ b/vMenu/menus/VehicleOptions.cs @@ -185,7 +185,7 @@ private void CreateMenu() GetLabelText("CMOD_PLA_2"), // Plate Index 2 // BlueOnWhite3 GetLabelText("CMOD_PLA_3"), // Plate Index 3 // YellowOnBlue GetLabelText("CMOD_PLA_4"), // Plate Index 4 // YellowOnBlack - "North Yankton", // Plate Index 5 // NorthYankton + GetLabelText("PROL"), // Plate Index 5 // NorthYankton GetLabelText("CMOD_PLA_6"), // Plate Index 6 // ECola GetLabelText("CMOD_PLA_7"), // Plate Index 7 // LasVenturas GetLabelText("CMOD_PLA_8"), // Plate Index 8 // LibertyCity @@ -1028,6 +1028,14 @@ private void CreateMenu() #endregion #region Vehicle Colors Submenu Stuff + // presets menu + var presetColorsMenu = new Menu("Vehicle Colors", "Preset Colors"); + MenuController.AddSubmenu(VehicleColorsMenu, presetColorsMenu); + + var presetColorsBtn = new MenuItem("Preset Colors") { Label = "→→→" }; + VehicleColorsMenu.AddMenuItem(presetColorsBtn); + MenuController.BindMenuItem(VehicleColorsMenu, presetColorsMenu, presetColorsBtn); + // primary menu var primaryColorsMenu = new Menu("Vehicle Colors", "Primary Colors"); MenuController.AddSubmenu(VehicleColorsMenu, primaryColorsMenu); @@ -1309,7 +1317,7 @@ async void HandleItemSelect(Menu menu, MenuItem menuItem, int itemIndex) for (var i = 0; i < 2; i++) { - var customColour = new MenuItem("Custom RGB") { Label = ">>>" }; + var customColour = new MenuItem("Custom RGB") { Label = "→→→" }; var pearlescentList = new MenuListItem("Pearlescent", classic, 0); var classicList = new MenuListItem("Classic", classic, 0); var metallicList = new MenuListItem("Metallic", classic, 0); @@ -1353,6 +1361,44 @@ async void HandleItemSelect(Menu menu, MenuItem menuItem, int itemIndex) secondaryColorsMenu.OnItemSelect += HandleItemSelect; } } + + VehicleColorsMenu.OnItemSelect += (sender, item, index) => + { + // When the color presets submenu is openend, update preset color items. + if (item == presetColorsBtn) + { + if (Game.PlayerPed.IsInVehicle()) + { + presetColorsMenu.ClearMenuItems(); + var veh = GetVehicle(); + var VehicleColorCombinationsCount = GetNumberOfVehicleColours(veh.Handle); + + for (int i = 0; i < VehicleColorCombinationsCount; i++) + { + var presetColor = new MenuItem($"Preset {i + 1}"); + presetColorsMenu.AddMenuItem(presetColor); + } + } + else + { + VehicleColorsMenu.CloseMenu(); + menu.OpenMenu(); + } + } + }; + + presetColorsMenu.OnItemSelect += (sender, item, index) => + { + var veh = GetVehicle(); + if (veh != null && veh.Exists() && !veh.IsDead && veh.Driver == Game.PlayerPed) + { + SetVehicleColourCombination(veh.Handle, index); + } + else + { + Notify.Error("You need to be the driver of a driveable vehicle to change this."); + } + }; #endregion #region Vehicle Doors Submenu Stuff From 80daf9452ee5ed76a9109418e4344ed53649a10a Mon Sep 17 00:00:00 2001 From: Tylerg9822-sudo Date: Tue, 2 Sep 2025 17:09:20 -0500 Subject: [PATCH 03/54] Update permissions.cfg --- vMenuServer/config/permissions.cfg | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/vMenuServer/config/permissions.cfg b/vMenuServer/config/permissions.cfg index 960eb4ae4..2b1f696ac 100644 --- a/vMenuServer/config/permissions.cfg +++ b/vMenuServer/config/permissions.cfg @@ -134,6 +134,17 @@ setr vmenu_sync_to_machine_time false # You must be streaming the chameleon colours in order for this to function properly. setr vmenu_using_chameleon_colours false +# Setting this to true will prevent players from modifying vehicle extras to repair their vehicle when it is damaged. +setr vmenu_prevent_extras_when_damaged false +# This is the amount of engine damage before the extras will be blocked. +setr vmenu_prevent_extras_engine_damage 800 +# This is the amount of body damage before the extras will be blocked. +setr vmenu_prevent_extras_body_damage 800 +# Enabling this will show a notification when the player attempts to modify extras. +setr vmenu_prevent_extras_notification_enabled true +# This is the message that will be displayed when a player attempts to modify extras when the vehicle is damaged. +setr vmenu_prevent_extras_notification_text "Vehicle is too damaged. Please visit a mechanic." + ### MP Ped options ### # Setting this to true will enable a 3D ped preview when viewing saved MP Peds. setr vmenu_mp_ped_preview true From bf3d449ec24be5bea66bfc972c596f08aaab57be Mon Sep 17 00:00:00 2001 From: Tylerg9822-sudo Date: Tue, 2 Sep 2025 17:10:42 -0500 Subject: [PATCH 04/54] Update ConfigManager.cs --- SharedClasses/ConfigManager.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/SharedClasses/ConfigManager.cs b/SharedClasses/ConfigManager.cs index 26a0693d7..4ddba6894 100644 --- a/SharedClasses/ConfigManager.cs +++ b/SharedClasses/ConfigManager.cs @@ -38,6 +38,13 @@ public enum Setting // Vehicle Chameleon Colours vmenu_using_chameleon_colours, + // Vehicle Prevent Extras When Damaged + vmenu_prevent_extras_when_damaged, + vmenu_prevent_extras_engine_damage, + vmenu_prevent_extras_body_damage, + vmenu_prevent_extras_notification_enabled, + vmenu_prevent_extras_notification_text, + // MP Ped preview setting, vmenu_mp_ped_preview, From 3aae4fc999343a22bd3b25d4fdb8836d78c31ad2 Mon Sep 17 00:00:00 2001 From: Tylerg9822-sudo Date: Tue, 2 Sep 2025 17:14:20 -0500 Subject: [PATCH 05/54] Update VehicleOptions.cs --- vMenu/menus/VehicleOptions.cs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/vMenu/menus/VehicleOptions.cs b/vMenu/menus/VehicleOptions.cs index 19f29b6e0..58ab56506 100644 --- a/vMenu/menus/VehicleOptions.cs +++ b/vMenu/menus/VehicleOptions.cs @@ -1702,7 +1702,25 @@ async void HandleItemSelect(Menu menu, MenuItem menuItem, int itemIndex) if (vehicleExtras.TryGetValue(item, out var extra)) { var veh = GetVehicle(); - veh.ToggleExtra(extra, _checked); + if (veh != null && veh.Exists()) + { + // If vmenu_prevent_extras_when_damaged is enabled, check vehicle engine and body health + if (GetSettingsBool(Setting.vmenu_prevent_extras_when_damaged) && + (API.GetVehicleBodyHealth(veh.Handle) < GetSettingsInt(Setting.vmenu_prevent_extras_engine_damage) || + API.GetVehicleEngineHealth(veh.Handle) < GetSettingsInt(Setting.vmenu_prevent_extras_body_damage))) + { + if (GetSettingsBool(Setting.vmenu_prevent_extras_notification_enabled)) + { + Screen.ShowNotification(GetSettingsString(Setting.vmenu_prevent_extras_notification_text)); + } + + // Revert checkbox back to original state + ((MenuCheckboxItem)item).Checked = veh.IsExtraOn(extra); + return; + } + + veh.ToggleExtra(extra, _checked); + } } }; #endregion From 2af9319c05cc583ba1511043d3c6bf5e355c9083 Mon Sep 17 00:00:00 2001 From: Tylerg9822-sudo Date: Tue, 2 Sep 2025 17:34:48 -0500 Subject: [PATCH 06/54] Update VehicleOptions.cs --- vMenu/menus/VehicleOptions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vMenu/menus/VehicleOptions.cs b/vMenu/menus/VehicleOptions.cs index 58ab56506..4248c6bc1 100644 --- a/vMenu/menus/VehicleOptions.cs +++ b/vMenu/menus/VehicleOptions.cs @@ -3,6 +3,8 @@ using System.Linq; using CitizenFX.Core; +using CitizenFX.Core.Native; +using CitizenFX.Core.UI; using MenuAPI; From 952d49bbeef828f76ab1981c0de35afbaeacfef8 Mon Sep 17 00:00:00 2001 From: Tylerg9822-sudo Date: Sat, 6 Sep 2025 18:02:31 -0500 Subject: [PATCH 07/54] Update EventManager.cs to Fix Weather Syncing Fixed Weather Syncing --- vMenu/EventManager.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/vMenu/EventManager.cs b/vMenu/EventManager.cs index c0cc95f50..e4db2383b 100644 --- a/vMenu/EventManager.cs +++ b/vMenu/EventManager.cs @@ -284,10 +284,6 @@ private async Task UpdateWeatherParticles() /// private async Task WeatherSync() { - if (MainMenu.WeatherOptionsMenu == null) - { - return; - } await UpdateWeatherParticles(); SetArtificialLightsState(IsBlackoutEnabled); From 49d3646ecce8d17853597c25cb26711f4c2acb95 Mon Sep 17 00:00:00 2001 From: Tylerg9822-sudo Date: Sat, 6 Sep 2025 18:07:23 -0500 Subject: [PATCH 08/54] Update EventManager.cs fixed useless line --- vMenu/EventManager.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/vMenu/EventManager.cs b/vMenu/EventManager.cs index e4db2383b..7eb113a03 100644 --- a/vMenu/EventManager.cs +++ b/vMenu/EventManager.cs @@ -284,7 +284,6 @@ private async Task UpdateWeatherParticles() /// private async Task WeatherSync() { - await UpdateWeatherParticles(); SetArtificialLightsState(IsBlackoutEnabled); SetArtificialLightsStateAffectsVehicles(!IsVehicleLightsEnabled); From 1514ebca7685985b23cfbed990b3e82ae0b298c4 Mon Sep 17 00:00:00 2001 From: Tom <31419184+TomGrobbe@users.noreply.github.com> Date: Sun, 7 Sep 2025 20:44:27 +0200 Subject: [PATCH 09/54] Update EventManager.cs Fix Weather sync when players don't have access to WeatherOptionsMenu --- vMenu/EventManager.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/vMenu/EventManager.cs b/vMenu/EventManager.cs index c0cc95f50..7eb113a03 100644 --- a/vMenu/EventManager.cs +++ b/vMenu/EventManager.cs @@ -284,11 +284,6 @@ private async Task UpdateWeatherParticles() /// private async Task WeatherSync() { - if (MainMenu.WeatherOptionsMenu == null) - { - return; - } - await UpdateWeatherParticles(); SetArtificialLightsState(IsBlackoutEnabled); SetArtificialLightsStateAffectsVehicles(!IsVehicleLightsEnabled); From 67600d707800398d2a56044c0346d2d42d9b3e1a Mon Sep 17 00:00:00 2001 From: Yauhen Pahrabniak Date: Wed, 2 Jul 2025 11:26:07 +0200 Subject: [PATCH 10/54] Fix teleport to Player --- vMenu/CommonFunctions.cs | 5 +++++ vMenu/MainMenu.cs | 42 +++++++++++++++++++++++++++++---------- vMenuServer/MainServer.cs | 12 ++++------- 3 files changed, 40 insertions(+), 19 deletions(-) diff --git a/vMenu/CommonFunctions.cs b/vMenu/CommonFunctions.cs index c4d6d6d0c..25dd04af8 100644 --- a/vMenu/CommonFunctions.cs +++ b/vMenu/CommonFunctions.cs @@ -423,6 +423,11 @@ public static async Task TeleportToPlayer(IPlayer player, bool inVehicle = false else { playerPos = await MainMenu.RequestPlayerCoordinates(player.ServerId); + if (playerPos == Vector3.Zero) + { + Notify.Error("Could not retrieve the coordinates of the specified player. Teleport cancelled."); + return; + } wasActive = false; } diff --git a/vMenu/MainMenu.cs b/vMenu/MainMenu.cs index fcc5d037d..45f0d7086 100644 --- a/vMenu/MainMenu.cs +++ b/vMenu/MainMenu.cs @@ -369,26 +369,46 @@ public void ReceivedPlayerList(IList players) PlayersList?.ReceivedPlayerList(players); } - public static async Task RequestPlayerCoordinates(int serverId) + struct RPCData { - var coords = Vector3.Zero; - var completed = false; + public bool IsCompleted { get; set; } + public Vector3 Coords { get; set; } + } - // TODO: replace with client<->server RPC once implemented in CitizenFX! - Func CallbackFunction = (data) => + private static Dictionary rpcQueue = new Dictionary(); + private static long rpcIdCounter = 0; + + [EventHandler("vMenu:GetPlayerCoords:reply")] + public static void PlayerCoordinatesReceived(long rpcId, Vector3 coords) + { + if (rpcQueue.ContainsKey(rpcId)) { - coords = data; - completed = true; - return true; - }; + var rpcItem = rpcQueue[rpcId]; + rpcItem.IsCompleted = true; + rpcItem.Coords = coords; + rpcQueue[rpcId] = rpcItem; + } + else + { + Debug.WriteLine($"[vMenu] Warning: Received player coordinates for unknown RPC ID: {rpcId}"); + } + } - TriggerServerEvent("vMenu:GetPlayerCoords", serverId, CallbackFunction); + public static async Task RequestPlayerCoordinates(int serverId) + { + long rpcId = rpcIdCounter++; + rpcQueue.Add(rpcId, new RPCData { IsCompleted = false, Coords = Vector3.Zero }); + + TriggerServerEvent("vMenu:GetPlayerCoords", rpcId, serverId); - while (!completed) + while (!rpcQueue[rpcId].IsCompleted) { await Delay(0); } + Vector3 coords = rpcQueue[rpcId].Coords; + rpcQueue.Remove(rpcId); + return coords; } #endregion diff --git a/vMenuServer/MainServer.cs b/vMenuServer/MainServer.cs index d2bbde98a..a17e36c0a 100644 --- a/vMenuServer/MainServer.cs +++ b/vMenuServer/MainServer.cs @@ -912,19 +912,15 @@ internal void RequestPlayerListFromPlayer([FromSource] Player player) } [EventHandler("vMenu:GetPlayerCoords")] - internal void GetPlayerCoords([FromSource] Player source, int playerId, NetworkCallbackDelegate callback) + internal void GetPlayerCoords([FromSource] Player source, long rpcId, int playerId, NetworkCallbackDelegate callback) { + var coords = Vector3.Zero; if (IsPlayerAceAllowed(source.Handle, "vMenu.OnlinePlayers.Teleport") || IsPlayerAceAllowed(source.Handle, "vMenu.Everything") || IsPlayerAceAllowed(source.Handle, "vMenu.OnlinePlayers.All")) { - var coords = Players[playerId]?.Character?.Position ?? Vector3.Zero; - - _ = callback(coords); - - return; + coords = Players[playerId]?.Character?.Position ?? Vector3.Zero; } - - _ = callback(Vector3.Zero); + source.TriggerEvent("vMenu:GetPlayerCoords:reply", rpcId, coords); } #endregion From 555bea81309a63df4ae8cf14fe8b66f2f9f57211 Mon Sep 17 00:00:00 2001 From: "Christopher M." Date: Sat, 20 Sep 2025 15:20:08 +1000 Subject: [PATCH 11/54] Refactors weather & time perms check to use PermissionsManager.IsAllowed What was here and written by poco8537 was bang on the money, but could be simplified by using the existing `PermissionsManager` class and it's existing funcs and enums - which is what this commit refactors. Also adds a missing permission check for randomizing clouds, and an explicit check for setting time (versus simply having access to the time menu). --- vMenuServer/MainServer.cs | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/vMenuServer/MainServer.cs b/vMenuServer/MainServer.cs index d6069be13..91d657eb0 100644 --- a/vMenuServer/MainServer.cs +++ b/vMenuServer/MainServer.cs @@ -662,7 +662,7 @@ private void RefreshWeather() [EventHandler("vMenu:UpdateServerWeather")] internal void UpdateWeather([FromSource] Player source, string newWeather, bool blackoutNew, bool dynamicWeatherNew, bool enableSnow) { - if (source != null && !IsPlayerAceAllowed(source.Handle, "vMenu.WeatherOptions.Menu") && !IsPlayerAceAllowed(source.Handle, "vMenu.WeatherOptions.All")) + if (!PermissionsManager.IsAllowed(PermissionsManager.Permission.WOSetWeather, source) && !PermissionsManager.IsAllowed(PermissionsManager.Permission.WOAll, source)) { BanManager.BanCheater(source); return; @@ -691,18 +691,26 @@ internal void UpdateWeather([FromSource] Player source, string newWeather, bool [EventHandler("vMenu:UpdateServerWeatherCloudsType")] internal void UpdateWeatherCloudsType([FromSource] Player source, bool removeClouds) { - if (source != null && !IsPlayerAceAllowed(source.Handle, "vMenu.WeatherOptions.RemoveClouds") && !IsPlayerAceAllowed(source.Handle, "vMenu.WeatherOptions.RandomizeClouds")) - { - BanManager.BanCheater(source); - return; - } + bool allWOPermissions = PermissionsManager.IsAllowed(PermissionsManager.Permission.WOAll, source); if (removeClouds) { + if (!PermissionsManager.IsAllowed(PermissionsManager.Permission.WORemoveClouds, source) && !allWOPermissions) + { + BanManager.BanCheater(source); + return; + } + TriggerClientEvent("vMenu:SetClouds", 0f, "removed"); } else { + if (!PermissionsManager.IsAllowed(PermissionsManager.Permission.WORandomizeClouds, source) && !allWOPermissions) + { + BanManager.BanCheater(source); + return; + } + var opacity = float.Parse(new Random().NextDouble().ToString()); var type = CloudTypes[new Random().Next(0, CloudTypes.Count)]; TriggerClientEvent("vMenu:SetClouds", opacity, type); @@ -718,12 +726,25 @@ internal void UpdateWeatherCloudsType([FromSource] Player source, bool removeClo [EventHandler("vMenu:UpdateServerTime")] internal void UpdateTime([FromSource] Player source, int newHours, int newMinutes, bool freezeTimeNew) { - if (source != null && !IsPlayerAceAllowed(source.Handle, "vMenu.TimeOptions.Menu") && !IsPlayerAceAllowed(source.Handle, "vMenu.TimeOptions.All")) + bool allTOPermissions = PermissionsManager.IsAllowed(PermissionsManager.Permission.TOSetTime, source); + + if (!PermissionsManager.IsAllowed(PermissionsManager.Permission.TOSetTime, source) && !allTOPermissions) { BanManager.BanCheater(source); return; } + // Chris: This logic is inherently problematic, as players WITHOUT `TOFreezeTime` can still *un*freeze time, even if they can't freeze time + // TODO: Move time freezing to separate event, so `TOFreezeTime` can be checked regardless of the boolean value? + if (freezeTimeNew) + { + if (!PermissionsManager.IsAllowed(PermissionsManager.Permission.TOFreezeTime, source) && !allTOPermissions) + { + BanManager.BanCheater(source); + return; + } + } + CurrentHours = newHours; CurrentMinutes = newMinutes; FreezeTime = freezeTimeNew; From 0cab6433520b5ab08a198119093abc393d6910a9 Mon Sep 17 00:00:00 2001 From: "Christopher M." Date: Sat, 20 Sep 2025 15:49:18 +1000 Subject: [PATCH 12/54] Refactors time freezing logic to properly check for required permission Addresses an issue identified in #430 where malicious clients without `TOFreezeTime` could still *un*freeze time. Also fixes a typo @cm8263 introduced in #430 where `TOFreezeTime` was checked instead of `TOAll`. --- vMenu/CommonFunctions.cs | 11 ++++++++--- vMenu/menus/TimeOptions.cs | 6 +++--- vMenuServer/MainServer.cs | 34 ++++++++++++++++++---------------- 3 files changed, 29 insertions(+), 22 deletions(-) diff --git a/vMenu/CommonFunctions.cs b/vMenu/CommonFunctions.cs index 25dd04af8..9475f7842 100644 --- a/vMenu/CommonFunctions.cs +++ b/vMenu/CommonFunctions.cs @@ -2046,8 +2046,7 @@ public static Dictionary JsonToDictionary(string json) /// /// Hours (0-23) /// Minutes (0-59) - /// Should the time be frozen? - public static void UpdateServerTime(int hours, int minutes, bool freezeTime) + public static void UpdateServerTime(int hours, int minutes) { var realHours = hours; var realMinutes = minutes; @@ -2059,8 +2058,14 @@ public static void UpdateServerTime(int hours, int minutes, bool freezeTime) { realMinutes = 0; } - TriggerServerEvent("vMenu:UpdateServerTime", realHours, realMinutes, freezeTime); + TriggerServerEvent("vMenu:UpdateServerTime", realHours, realMinutes); } + + /// + /// Updates the server on if time should be frozen or not. + /// + /// `true` to freeze time, `false` to unfreeze time + public static void FreezeServerTime(bool freezeTime) => TriggerServerEvent("vMenu:FreezeServerTime", freezeTime); #endregion #region StringToStringArray diff --git a/vMenu/menus/TimeOptions.cs b/vMenu/menus/TimeOptions.cs index ca80452a7..a76b16a28 100644 --- a/vMenu/menus/TimeOptions.cs +++ b/vMenu/menus/TimeOptions.cs @@ -97,7 +97,7 @@ private void CreateMenu() if (item == freezeTimeToggle) { Subtitle.Info($"Time will now {(EventManager.IsServerTimeFrozen ? "~y~continue" : "~o~freeze")}~s~.", prefix: "Info:"); - UpdateServerTime(EventManager.GetServerHours, EventManager.GetServerMinutes, !EventManager.IsServerTimeFrozen); + FreezeServerTime(!EventManager.IsServerTimeFrozen); } else { @@ -117,7 +117,7 @@ private void CreateMenu() var newMinute = 0; Subtitle.Info($"Time set to ~y~{(newHour < 10 ? $"0{newHour}" : newHour.ToString())}~s~:~y~" + $"{(newMinute < 10 ? $"0{newMinute}" : newMinute.ToString())}~s~.", prefix: "Info:"); - UpdateServerTime(newHour, newMinute, EventManager.IsServerTimeFrozen); + UpdateServerTime(newHour, newMinute); } }; @@ -137,7 +137,7 @@ private void CreateMenu() Subtitle.Info($"Time set to ~y~{(newHour < 10 ? $"0{newHour}" : newHour.ToString())}~s~:~y~" + $"{(newMinute < 10 ? $"0{newMinute}" : newMinute.ToString())}~s~.", prefix: "Info:"); - UpdateServerTime(newHour, newMinute, EventManager.IsServerTimeFrozen); + UpdateServerTime(newHour, newMinute); }; } diff --git a/vMenuServer/MainServer.cs b/vMenuServer/MainServer.cs index c19156a71..f7b3e8da5 100644 --- a/vMenuServer/MainServer.cs +++ b/vMenuServer/MainServer.cs @@ -734,32 +734,34 @@ internal void UpdateWeatherCloudsType([FromSource] Player source, bool removeClo /// /// /// - /// [EventHandler("vMenu:UpdateServerTime")] - internal void UpdateTime([FromSource] Player source, int newHours, int newMinutes, bool freezeTimeNew) + internal void UpdateTime([FromSource] Player source, int newHours, int newMinutes) { - bool allTOPermissions = PermissionsManager.IsAllowed(PermissionsManager.Permission.TOSetTime, source); - - if (!PermissionsManager.IsAllowed(PermissionsManager.Permission.TOSetTime, source) && !allTOPermissions) + if (!PermissionsManager.IsAllowed(PermissionsManager.Permission.TOSetTime, source) && !PermissionsManager.IsAllowed(PermissionsManager.Permission.TOAll, source)) { BanManager.BanCheater(source); return; } - // Chris: This logic is inherently problematic, as players WITHOUT `TOFreezeTime` can still *un*freeze time, even if they can't freeze time - // TODO: Move time freezing to separate event, so `TOFreezeTime` can be checked regardless of the boolean value? - if (freezeTimeNew) + CurrentHours = newHours; + CurrentMinutes = newMinutes; + } + + /// + /// Set and sync if time is frozen for all clients. + /// + /// + /// + [EventHandler("vMenu:FreezeServerTime")] + internal void FreezeServerTime([FromSource] Player source, bool freezeTime) + { + if (!PermissionsManager.IsAllowed(PermissionsManager.Permission.TOFreezeTime, source) && !PermissionsManager.IsAllowed(PermissionsManager.Permission.TOAll, source)) { - if (!PermissionsManager.IsAllowed(PermissionsManager.Permission.TOFreezeTime, source) && !allTOPermissions) - { - BanManager.BanCheater(source); - return; - } + BanManager.BanCheater(source); + return; } - CurrentHours = newHours; - CurrentMinutes = newMinutes; - FreezeTime = freezeTimeNew; + FreezeTime = freezeTime; } #endregion From 58005a9a7b086448eeee102596633435bb3b2dc6 Mon Sep 17 00:00:00 2001 From: "Christopher M." Date: Sat, 20 Sep 2025 17:36:06 +1000 Subject: [PATCH 13/54] Refactors vehicle mod logic to use updated mod type list Calling `.GetAllMods()` checked against `VehicleModType` internally, which was missing values 47 and 49 (as well as most values being named incorrectly). This commit provides an up-to-date mod type list and gets vehicle mods based on said list. --- vMenu/CommonFunctions.cs | 23 +++++++++++++- vMenu/data/VehicleData.cs | 57 +++++++++++++++++++++++++++++++++++ vMenu/menus/VehicleOptions.cs | 2 +- 3 files changed, 80 insertions(+), 2 deletions(-) diff --git a/vMenu/CommonFunctions.cs b/vMenu/CommonFunctions.cs index 9475f7842..4a2d4ff4b 100644 --- a/vMenu/CommonFunctions.cs +++ b/vMenu/CommonFunctions.cs @@ -1534,7 +1534,7 @@ public static async void SaveVehicle(string updateExistingSavedVehicleName = nul #region new saving method var mods = new Dictionary(); - foreach (var mod in veh.Mods.GetAllMods()) + foreach (var mod in GetAllVehicleMods(veh)) { mods.Add((int)mod.ModType, mod.Index); } @@ -3466,5 +3466,26 @@ public static async void SavePlayerLocationToLocationsFile() Notify.Success("The location was successfully saved."); } #endregion + + #region Get all vehicle mods + public static VehicleMod[] GetAllVehicleMods(Vehicle vehicle) + { + int vehicleHandle = vehicle.Handle; + + bool HasVehicleMod(VehicleData.ModType modType) + { + return GetNumVehicleMods(vehicleHandle, (int)modType) > 0; + } + + return + [ + .. Enum.GetValues(typeof(VehicleData.ModType)) + .Cast() + .Where(HasVehicleMod) + // The cast to `VehicleModType` is fine here because `VehicleMod` casts `VehicleModType` to `int` + .Select(modType => vehicle.Mods[(VehicleModType)modType]) + ]; + } + #endregion } } diff --git a/vMenu/data/VehicleData.cs b/vMenu/data/VehicleData.cs index 71b6a555c..470681180 100644 --- a/vMenu/data/VehicleData.cs +++ b/vMenu/data/VehicleData.cs @@ -1446,5 +1446,62 @@ public static string[] GetAllVehicles() return vehs.ToArray(); } } + + /// + /// Values taken from `SET_VEHICLE_MOD` native + /// + public enum ModType + { + VMT_SPOILER = 0, + VMT_BUMPER_F = 1, + VMT_BUMPER_R = 2, + VMT_SKIRT = 3, + VMT_EXHAUST = 4, + VMT_CHASSIS = 5, + VMT_GRILL = 6, + VMT_BONNET = 7, + VMT_WING_L = 8, + VMT_WING_R = 9, + VMT_ROOF = 10, + VMT_ENGINE = 11, + VMT_BRAKES = 12, + VMT_GEARBOX = 13, + VMT_HORN = 14, + VMT_SUSPENSION = 15, + VMT_ARMOUR = 16, + VMT_NITROUS = 17, + VMT_TURBO = 18, + VMT_SUBWOOFER = 19, + VMT_TYRE_SMOKE = 20, + VMT_HYDRAULICS = 21, + VMT_XENON_LIGHTS = 22, + VMT_WHEELS = 23, + VMT_WHEELS_REAR_OR_HYDRAULICS = 24, + VMT_PLTHOLDER = 25, + VMT_PLTVANITY = 26, + VMT_INTERIOR1 = 27, + VMT_INTERIOR2 = 28, + VMT_INTERIOR3 = 29, + VMT_INTERIOR4 = 30, + VMT_INTERIOR5 = 31, + VMT_SEATS = 32, + VMT_STEERING = 33, + VMT_KNOB = 34, + VMT_PLAQUE = 35, + VMT_ICE = 36, + VMT_TRUNK = 37, + VMT_HYDRO = 38, + VMT_ENGINEBAY1 = 39, + VMT_ENGINEBAY2 = 40, + VMT_ENGINEBAY3 = 41, + VMT_CHASSIS2 = 42, + VMT_CHASSIS3 = 43, + VMT_CHASSIS4 = 44, + VMT_CHASSIS5 = 45, + VMT_DOOR_L = 46, + VMT_DOOR_R = 47, + VMT_LIVERY_MOD = 48, + VMT_LIGHTBAR = 49, + } } } diff --git a/vMenu/menus/VehicleOptions.cs b/vMenu/menus/VehicleOptions.cs index 19f29b6e0..25905979a 100644 --- a/vMenu/menus/VehicleOptions.cs +++ b/vMenu/menus/VehicleOptions.cs @@ -1946,7 +1946,7 @@ public void UpdateMods(int selectedIndex = 0) SetVehicleModKit(veh.Handle, 0); // Get all mods available on this vehicle. - var mods = veh.Mods.GetAllMods(); + var mods = GetAllVehicleMods(veh); // Loop through all the mods. foreach (var mod in mods) From f8c4da7b53ef42ccda7972bafb834af01745a0e0 Mon Sep 17 00:00:00 2001 From: Tylerg9822-sudo Date: Sat, 20 Sep 2025 10:39:51 -0500 Subject: [PATCH 14/54] Update ConfigManager.cs removed per request --- SharedClasses/ConfigManager.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/SharedClasses/ConfigManager.cs b/SharedClasses/ConfigManager.cs index 4ddba6894..26a0693d7 100644 --- a/SharedClasses/ConfigManager.cs +++ b/SharedClasses/ConfigManager.cs @@ -38,13 +38,6 @@ public enum Setting // Vehicle Chameleon Colours vmenu_using_chameleon_colours, - // Vehicle Prevent Extras When Damaged - vmenu_prevent_extras_when_damaged, - vmenu_prevent_extras_engine_damage, - vmenu_prevent_extras_body_damage, - vmenu_prevent_extras_notification_enabled, - vmenu_prevent_extras_notification_text, - // MP Ped preview setting, vmenu_mp_ped_preview, From 6dca010d773ef79f3eddd5840d4db67d40b844d1 Mon Sep 17 00:00:00 2001 From: Tylerg9822-sudo Date: Sat, 20 Sep 2025 10:51:49 -0500 Subject: [PATCH 15/54] Update permissions.cfg hardcoded message and removed its config string --- vMenuServer/config/permissions.cfg | 2 -- 1 file changed, 2 deletions(-) diff --git a/vMenuServer/config/permissions.cfg b/vMenuServer/config/permissions.cfg index 2b1f696ac..6bbd8c132 100644 --- a/vMenuServer/config/permissions.cfg +++ b/vMenuServer/config/permissions.cfg @@ -142,8 +142,6 @@ setr vmenu_prevent_extras_engine_damage 800 setr vmenu_prevent_extras_body_damage 800 # Enabling this will show a notification when the player attempts to modify extras. setr vmenu_prevent_extras_notification_enabled true -# This is the message that will be displayed when a player attempts to modify extras when the vehicle is damaged. -setr vmenu_prevent_extras_notification_text "Vehicle is too damaged. Please visit a mechanic." ### MP Ped options ### # Setting this to true will enable a 3D ped preview when viewing saved MP Peds. From 47065737130893692327031346a5b80815c686d7 Mon Sep 17 00:00:00 2001 From: Tylerg9822-sudo Date: Sat, 20 Sep 2025 10:53:14 -0500 Subject: [PATCH 16/54] Update VehicleOptions.cs modify per request --- vMenu/menus/VehicleOptions.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/vMenu/menus/VehicleOptions.cs b/vMenu/menus/VehicleOptions.cs index 4248c6bc1..09b653c64 100644 --- a/vMenu/menus/VehicleOptions.cs +++ b/vMenu/menus/VehicleOptions.cs @@ -3,8 +3,6 @@ using System.Linq; using CitizenFX.Core; -using CitizenFX.Core.Native; -using CitizenFX.Core.UI; using MenuAPI; @@ -1704,7 +1702,7 @@ async void HandleItemSelect(Menu menu, MenuItem menuItem, int itemIndex) if (vehicleExtras.TryGetValue(item, out var extra)) { var veh = GetVehicle(); - if (veh != null && veh.Exists()) + if (Entity.Exists(veh)) { // If vmenu_prevent_extras_when_damaged is enabled, check vehicle engine and body health if (GetSettingsBool(Setting.vmenu_prevent_extras_when_damaged) && @@ -1713,15 +1711,13 @@ async void HandleItemSelect(Menu menu, MenuItem menuItem, int itemIndex) { if (GetSettingsBool(Setting.vmenu_prevent_extras_notification_enabled)) { - Screen.ShowNotification(GetSettingsString(Setting.vmenu_prevent_extras_notification_text)); + Notify.Alert("Vehicle is too damaged to change extra, repair it first!", true, false); } // Revert checkbox back to original state ((MenuCheckboxItem)item).Checked = veh.IsExtraOn(extra); return; } - - veh.ToggleExtra(extra, _checked); } } }; From 80e95ae2fcdceb04cda59f12a70c506746936bd5 Mon Sep 17 00:00:00 2001 From: Tylerg9822-sudo Date: Sat, 20 Sep 2025 10:49:07 -0700 Subject: [PATCH 17/54] Update VehicleOptions.cs --- vMenu/menus/VehicleOptions.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/vMenu/menus/VehicleOptions.cs b/vMenu/menus/VehicleOptions.cs index 09b653c64..1850045e1 100644 --- a/vMenu/menus/VehicleOptions.cs +++ b/vMenu/menus/VehicleOptions.cs @@ -1706,8 +1706,8 @@ async void HandleItemSelect(Menu menu, MenuItem menuItem, int itemIndex) { // If vmenu_prevent_extras_when_damaged is enabled, check vehicle engine and body health if (GetSettingsBool(Setting.vmenu_prevent_extras_when_damaged) && - (API.GetVehicleBodyHealth(veh.Handle) < GetSettingsInt(Setting.vmenu_prevent_extras_engine_damage) || - API.GetVehicleEngineHealth(veh.Handle) < GetSettingsInt(Setting.vmenu_prevent_extras_body_damage))) + (GetVehicleBodyHealth(veh.Handle) < GetSettingsInt(Setting.vmenu_prevent_extras_engine_damage) || + GetVehicleEngineHealth(veh.Handle) < GetSettingsInt(Setting.vmenu_prevent_extras_body_damage))) { if (GetSettingsBool(Setting.vmenu_prevent_extras_notification_enabled)) { @@ -1718,6 +1718,7 @@ async void HandleItemSelect(Menu menu, MenuItem menuItem, int itemIndex) ((MenuCheckboxItem)item).Checked = veh.IsExtraOn(extra); return; } + veh.ToggleExtra(extra, _checked); } } }; From 288f08ec3ece93765d86b87c52afcbba025ca8fb Mon Sep 17 00:00:00 2001 From: Tylerg9822-sudo Date: Sat, 20 Sep 2025 10:50:39 -0700 Subject: [PATCH 18/54] Update permissions.cfg --- vMenuServer/config/permissions.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vMenuServer/config/permissions.cfg b/vMenuServer/config/permissions.cfg index 6bbd8c132..9299f754a 100644 --- a/vMenuServer/config/permissions.cfg +++ b/vMenuServer/config/permissions.cfg @@ -136,9 +136,9 @@ setr vmenu_using_chameleon_colours false # Setting this to true will prevent players from modifying vehicle extras to repair their vehicle when it is damaged. setr vmenu_prevent_extras_when_damaged false -# This is the amount of engine damage before the extras will be blocked. +# This is the amount of engine damage before the extras will be blocked. Value must be between 0 and 1000 (inclusive) setr vmenu_prevent_extras_engine_damage 800 -# This is the amount of body damage before the extras will be blocked. +# This is the amount of body damage before the extras will be blocked. Value must be between 0 and 1000 (inclusive) setr vmenu_prevent_extras_body_damage 800 # Enabling this will show a notification when the player attempts to modify extras. setr vmenu_prevent_extras_notification_enabled true From 87d1596c71f667f4ba90696383ff7e1b31b4eb79 Mon Sep 17 00:00:00 2001 From: Tylerg9822-sudo Date: Sat, 20 Sep 2025 10:51:31 -0700 Subject: [PATCH 19/54] Update ConfigManager.cs --- SharedClasses/ConfigManager.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/SharedClasses/ConfigManager.cs b/SharedClasses/ConfigManager.cs index 26a0693d7..853016542 100644 --- a/SharedClasses/ConfigManager.cs +++ b/SharedClasses/ConfigManager.cs @@ -38,6 +38,12 @@ public enum Setting // Vehicle Chameleon Colours vmenu_using_chameleon_colours, + // Prevent Extras Abuse + vmenu_prevent_extras_when_damaged, + vmenu_prevent_extras_engine_damage, + vmenu_prevent_extras_body_damage, + vmenu_prevent_extras_notification_enabled, + // MP Ped preview setting, vmenu_mp_ped_preview, From 8c3e09d3b7eadb7e37751fe53e3d2e3e4ccdad85 Mon Sep 17 00:00:00 2001 From: "Christopher M." Date: Mon, 22 Sep 2025 15:07:16 +1000 Subject: [PATCH 20/54] Adds missing required entries in `appearanceValues` for randomized peds --- vMenu/menus/MpPedCustomization.cs | 81 ++++++++++++++++++------------- 1 file changed, 46 insertions(+), 35 deletions(-) diff --git a/vMenu/menus/MpPedCustomization.cs b/vMenu/menus/MpPedCustomization.cs index 3396a33ec..af2e782ca 100644 --- a/vMenu/menus/MpPedCustomization.cs +++ b/vMenu/menus/MpPedCustomization.cs @@ -121,7 +121,7 @@ public class MpPedCustomization private float _shapeMixValue; private float _skinMixValue; private readonly Dictionary shapeFaceValues = []; - private readonly Dictionary> apperanceValues = []; + private readonly Dictionary> appearanceValues = []; private int _hairSelection; private int _hairColorSelection; private int _hairHighlightColorSelection; @@ -839,6 +839,11 @@ private void CreateMenu() bodyBlemishesList.Add($"Style #{i + 1}"); } + for (int i = 0; i < 12; i++) + { + appearanceValues[i] = new Tuple(0, 0, 0f); + } + // Create the menu. menu = new Menu(Game.Player.Name, "MP Ped Customization"); @@ -1805,6 +1810,7 @@ void ApplySavedTattoos() case 1: if (!currentCharacter.IsMale) { + appearanceValues[i] = new Tuple(0, 0, 0f); continue; } @@ -1837,6 +1843,7 @@ void ApplySavedTattoos() case 8: if (currentCharacter.IsMale) { + appearanceValues[i] = new Tuple(0, 0, 0f); continue; } @@ -1859,6 +1866,7 @@ void ApplySavedTattoos() case 10: if (!currentCharacter.IsMale) { + appearanceValues[i] = new Tuple(0, 0, 0f); continue; } @@ -1879,16 +1887,16 @@ void ApplySavedTattoos() break; default: - apperanceValues[i] = new Tuple(0, 0, 0); + appearanceValues[i] = new Tuple(0, 0, 0); continue; } - apperanceValues[i] = new Tuple(value, color, opacity); - SetPedHeadOverlay(Game.PlayerPed.Handle, i, apperanceValues[i].Item1, apperanceValues[i].Item3); + appearanceValues[i] = new Tuple(value, color, opacity); + SetPedHeadOverlay(Game.PlayerPed.Handle, i, appearanceValues[i].Item1, appearanceValues[i].Item3); if (colorRequired) { - SetPedHeadOverlayColor(Game.PlayerPed.Handle, i, colorIndex, apperanceValues[i].Item2, apperanceValues[i].Item2); + SetPedHeadOverlayColor(Game.PlayerPed.Handle, i, colorIndex, appearanceValues[i].Item2, appearanceValues[i].Item2); } } @@ -1988,52 +1996,55 @@ void ApplySavedTattoos() { List items = appearanceMenu.GetMenuItems(); + // Chris: This is so, so terrible... (and I wrote it) + // This needs to be re-done at some point. + // TODO: Make not trash ((MenuListItem)items[0]).ListIndex = _hairSelection; ((MenuListItem)items[1]).ListIndex = _hairColorSelection; ((MenuListItem)items[2]).ListIndex = _hairHighlightColorSelection; ((MenuListItem)items[33]).ListIndex = _eyeColorSelection; - ((MenuListItem)items[3]).ListIndex = apperanceValues[0].Item1; - ((MenuListItem)items[4]).ListIndex = (int)(apperanceValues[0].Item3 * 10); + ((MenuListItem)items[3]).ListIndex = appearanceValues[0].Item1; + ((MenuListItem)items[4]).ListIndex = (int)(appearanceValues[0].Item3 * 10); - ((MenuListItem)items[5]).ListIndex = apperanceValues[1].Item1; - ((MenuListItem)items[6]).ListIndex = (int)(apperanceValues[1].Item3 * 10); - ((MenuListItem)items[7]).ListIndex = apperanceValues[1].Item1; + ((MenuListItem)items[5]).ListIndex = appearanceValues[1].Item1; + ((MenuListItem)items[6]).ListIndex = (int)(appearanceValues[1].Item3 * 10); + ((MenuListItem)items[7]).ListIndex = appearanceValues[1].Item1; - ((MenuListItem)items[8]).ListIndex = apperanceValues[2].Item1; - ((MenuListItem)items[9]).ListIndex = (int)(apperanceValues[2].Item3 * 10); - ((MenuListItem)items[10]).ListIndex = apperanceValues[2].Item1; + ((MenuListItem)items[8]).ListIndex = appearanceValues[2].Item1; + ((MenuListItem)items[9]).ListIndex = (int)(appearanceValues[2].Item3 * 10); + ((MenuListItem)items[10]).ListIndex = appearanceValues[2].Item1; - ((MenuListItem)items[11]).ListIndex = apperanceValues[3].Item1; - ((MenuListItem)items[12]).ListIndex = (int)(apperanceValues[3].Item3 * 10); + ((MenuListItem)items[11]).ListIndex = appearanceValues[3].Item1; + ((MenuListItem)items[12]).ListIndex = (int)(appearanceValues[3].Item3 * 10); - ((MenuListItem)items[13]).ListIndex = apperanceValues[4].Item1; - ((MenuListItem)items[14]).ListIndex = (int)(apperanceValues[4].Item3 * 10); - ((MenuListItem)items[15]).ListIndex = apperanceValues[4].Item1; + ((MenuListItem)items[13]).ListIndex = appearanceValues[4].Item1; + ((MenuListItem)items[14]).ListIndex = (int)(appearanceValues[4].Item3 * 10); + ((MenuListItem)items[15]).ListIndex = appearanceValues[4].Item1; - ((MenuListItem)items[16]).ListIndex = apperanceValues[5].Item1; - ((MenuListItem)items[17]).ListIndex = (int)(apperanceValues[5].Item3 * 10); - ((MenuListItem)items[18]).ListIndex = apperanceValues[5].Item1; + ((MenuListItem)items[16]).ListIndex = appearanceValues[5].Item1; + ((MenuListItem)items[17]).ListIndex = (int)(appearanceValues[5].Item3 * 10); + ((MenuListItem)items[18]).ListIndex = appearanceValues[5].Item1; - ((MenuListItem)items[19]).ListIndex = apperanceValues[6].Item1; - ((MenuListItem)items[20]).ListIndex = (int)(apperanceValues[6].Item3 * 10); + ((MenuListItem)items[19]).ListIndex = appearanceValues[6].Item1; + ((MenuListItem)items[20]).ListIndex = (int)(appearanceValues[6].Item3 * 10); - ((MenuListItem)items[21]).ListIndex = apperanceValues[7].Item1; - ((MenuListItem)items[22]).ListIndex = (int)(apperanceValues[7].Item3 * 10); + ((MenuListItem)items[21]).ListIndex = appearanceValues[7].Item1; + ((MenuListItem)items[22]).ListIndex = (int)(appearanceValues[7].Item3 * 10); - ((MenuListItem)items[23]).ListIndex = apperanceValues[8].Item1; - ((MenuListItem)items[24]).ListIndex = (int)(apperanceValues[8].Item3 * 10); - ((MenuListItem)items[25]).ListIndex = apperanceValues[8].Item1; + ((MenuListItem)items[23]).ListIndex = appearanceValues[8].Item1; + ((MenuListItem)items[24]).ListIndex = (int)(appearanceValues[8].Item3 * 10); + ((MenuListItem)items[25]).ListIndex = appearanceValues[8].Item1; - ((MenuListItem)items[26]).ListIndex = apperanceValues[9].Item1; - ((MenuListItem)items[27]).ListIndex = (int)(apperanceValues[9].Item3 * 10); + ((MenuListItem)items[26]).ListIndex = appearanceValues[9].Item1; + ((MenuListItem)items[27]).ListIndex = (int)(appearanceValues[9].Item3 * 10); - ((MenuListItem)items[28]).ListIndex = apperanceValues[10].Item1; - ((MenuListItem)items[29]).ListIndex = (int)(apperanceValues[10].Item3 * 10); - ((MenuListItem)items[30]).ListIndex = apperanceValues[10].Item1; + ((MenuListItem)items[28]).ListIndex = appearanceValues[10].Item1; + ((MenuListItem)items[29]).ListIndex = (int)(appearanceValues[10].Item3 * 10); + ((MenuListItem)items[30]).ListIndex = appearanceValues[10].Item1; - ((MenuListItem)items[31]).ListIndex = apperanceValues[11].Item1; - ((MenuListItem)items[32]).ListIndex = (int)(apperanceValues[11].Item3 * 10); + ((MenuListItem)items[31]).ListIndex = appearanceValues[11].Item1; + ((MenuListItem)items[32]).ListIndex = (int)(appearanceValues[11].Item3 * 10); appearanceMenu.RefreshIndex(); } From dcb5893f4958091dba223860d17c0e447a403b89 Mon Sep 17 00:00:00 2001 From: "Christopher M." Date: Mon, 22 Sep 2025 16:20:54 +1000 Subject: [PATCH 21/54] Fixes default ped overlay color being green Also fixes prior appearance menu values carrying over to newly created peds. --- vMenu/menus/MpPedCustomization.cs | 68 +++++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 9 deletions(-) diff --git a/vMenu/menus/MpPedCustomization.cs b/vMenu/menus/MpPedCustomization.cs index af2e782ca..630f7b4a2 100644 --- a/vMenu/menus/MpPedCustomization.cs +++ b/vMenu/menus/MpPedCustomization.cs @@ -179,6 +179,17 @@ private void MakeCreateCharacterMenu(bool male, bool editPed = false) propsMenu.ClearMenuItems(); #region appearance menu. + // Clears any saved appearance values from prior peds + _hairSelection = 0; + _hairColorSelection = 0; + _hairHighlightColorSelection = 0; + _eyeColorSelection = 0; + + for (int i = 0; i < 12; i++) + { + appearanceValues[i] = new Tuple(0, 0, 0f); + } + var opacity = new List() { "0%", "10%", "20%", "30%", "40%", "50%", "60%", "70%", "80%", "90%", "100%" }; var maxHairStyles = GetNumberOfPedDrawableVariations(Game.PlayerPed.Handle, 2); @@ -839,11 +850,6 @@ private void CreateMenu() bodyBlemishesList.Add($"Style #{i + 1}"); } - for (int i = 0; i < 12; i++) - { - appearanceValues[i] = new Tuple(0, 0, 0f); - } - // Create the menu. menu = new Menu(Game.Player.Name, "MP Ped Customization"); @@ -2047,6 +2053,8 @@ void ApplySavedTattoos() ((MenuListItem)items[32]).ListIndex = (int)(appearanceValues[11].Item3 * 10); appearanceMenu.RefreshIndex(); + + SetHeadBlend(); } }; @@ -2083,9 +2091,8 @@ void ApplySavedTattoos() ClearPedDecorations(Game.PlayerPed.Handle); ClearPedFacialDecorations(Game.PlayerPed.Handle); SetPedDefaultComponentVariation(Game.PlayerPed.Handle); - SetPedHairColor(Game.PlayerPed.Handle, 0, 0); - SetPedEyeColor(Game.PlayerPed.Handle, 0); ClearAllPedProps(Game.PlayerPed.Handle); + DefaultPlayerColors(); MakeCreateCharacterMenu(male: true); } @@ -2119,9 +2126,8 @@ void ApplySavedTattoos() ClearPedDecorations(Game.PlayerPed.Handle); ClearPedFacialDecorations(Game.PlayerPed.Handle); SetPedDefaultComponentVariation(Game.PlayerPed.Handle); - SetPedHairColor(Game.PlayerPed.Handle, 0, 0); - SetPedEyeColor(Game.PlayerPed.Handle, 0); ClearAllPedProps(Game.PlayerPed.Handle); + DefaultPlayerColors(); MakeCreateCharacterMenu(male: false); } @@ -3104,6 +3110,50 @@ internal void SetPlayerClothing() } } + /// + /// Sets all the ped's overlay colors to their default (0) entry. + /// When called, prevents default color being bright green. + /// + internal void DefaultPlayerColors() + { + SetHeadBlend(); + + for (int i = 0; i < 12; i++) + { + int color = 0; + int colorIndex = 0; + + switch (i) + { + case 1: + colorIndex = 1; + break; + + case 2: + colorIndex = 1; + break; + + case 8: + colorIndex = 2; + break; + + case 10: + colorIndex = 1; + break; + + default: + continue; + } + + SetPedHeadOverlay(Game.PlayerPed.Handle, i, 0, 0f); + + if (colorIndex > 0) + { + SetPedHeadOverlayColor(Game.PlayerPed.Handle, i, colorIndex, color, color); + } + } + } + /// /// Create the menu if it doesn't exist, and then returns it. /// From 8abf1b3d37f6517e47be277ffa83884fd02a2f28 Mon Sep 17 00:00:00 2001 From: "Christopher M." Date: Mon, 22 Sep 2025 16:42:00 +1000 Subject: [PATCH 22/54] Changes csproj to always output all files in the `config` directory --- vMenuServer/vMenuServer.csproj | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/vMenuServer/vMenuServer.csproj b/vMenuServer/vMenuServer.csproj index dec00d96b..1d9c44a3c 100644 --- a/vMenuServer/vMenuServer.csproj +++ b/vMenuServer/vMenuServer.csproj @@ -32,13 +32,7 @@ - - Always - - - Always - - + Always From a045820c683d22597bb8f52993180e5e142c81a6 Mon Sep 17 00:00:00 2001 From: Sky Vincent Date: Thu, 25 Sep 2025 13:15:36 -0500 Subject: [PATCH 23/54] convert `int` values to `float` as stated native --- vMenu/menus/MpPedCustomization.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/vMenu/menus/MpPedCustomization.cs b/vMenu/menus/MpPedCustomization.cs index 630f7b4a2..a355509c1 100644 --- a/vMenu/menus/MpPedCustomization.cs +++ b/vMenu/menus/MpPedCustomization.cs @@ -1020,14 +1020,15 @@ void AddMoms() var inheritanceDads = new MenuListItem("Father", dads.Keys.ToList(), 0, "Select a father."); var inheritanceMoms = new MenuListItem("Mother", moms.Keys.ToList(), 0, "Select a mother."); - var inheritanceShapeMix = new MenuSliderItem("Head Shape Mix", "Select how much of your head shape should be inherited from your father or mother. All the way on the left is your dad, all the way on the right is your mom.", 0, 10, 5, true) { SliderLeftIcon = MenuItem.Icon.MALE, SliderRightIcon = MenuItem.Icon.FEMALE }; - var inheritanceSkinMix = new MenuSliderItem("Body Skin Mix", "Select how much of your body skin tone should be inherited from your father or mother. All the way on the left is your dad, all the way on the right is your mom.", 0, 10, 5, true) { SliderLeftIcon = MenuItem.Icon.MALE, SliderRightIcon = MenuItem.Icon.FEMALE }; + var inheritanceShapeMix = new MenuSliderItem("Head Shape Mix", "Select how much of your head shape should be inherited from your father or mother. All the way on the left is your dad, all the way on the right is your mom.", -10, 10, 0, true) { SliderLeftIcon = MenuItem.Icon.MALE, SliderRightIcon = MenuItem.Icon.FEMALE }; + var inheritanceSkinMix = new MenuSliderItem("Body Skin Mix", "Select how much of your body skin tone should be inherited from your father or mother. All the way on the left is your dad, all the way on the right is your mom.", -10, 10, 0, true) { SliderLeftIcon = MenuItem.Icon.MALE, SliderRightIcon = MenuItem.Icon.FEMALE }; inheritanceMenu.AddMenuItem(inheritanceDads); inheritanceMenu.AddMenuItem(inheritanceMoms); inheritanceMenu.AddMenuItem(inheritanceShapeMix); inheritanceMenu.AddMenuItem(inheritanceSkinMix); + // formula from maintransition.#sc float GetMinimum() { @@ -1067,9 +1068,8 @@ int UnclampMix(float value) inheritanceMenu.OnSliderPositionChange += (sender, item, oldPosition, newPosition, itemIndex) => { - _shapeMixValue = inheritanceShapeMix.Position; - _skinMixValue = inheritanceSkinMix.Position; - + _shapeMixValue = ((float)inheritanceShapeMix.Position) / 10.0f ; + _skinMixValue = ((float)inheritanceSkinMix.Position) / 10.0f; SetHeadBlend(); }; #endregion @@ -1772,8 +1772,9 @@ void ApplySavedTattoos() { _dadSelection = _random.Next(parents.Count); _mumSelection = _random.Next(parents.Count); - _skinMixValue = _random.Next(2, 8); - _shapeMixValue = _random.Next(2, 8); + // These equations are needed to get random float values + _skinMixValue = (float)(_random.NextDouble() * 2 -1) * 10.0f; + _shapeMixValue = (float)(_random.NextDouble() * 2 - 1) * 10.0f; SetHeadBlend(); From 02b271f3fa3ed2c3d80886b743d667fdf1114107 Mon Sep 17 00:00:00 2001 From: Sky Vincent Date: Thu, 25 Sep 2025 13:59:12 -0500 Subject: [PATCH 24/54] refactor math done during random ped generation --- vMenu/menus/MpPedCustomization.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vMenu/menus/MpPedCustomization.cs b/vMenu/menus/MpPedCustomization.cs index a355509c1..69d7dd7e6 100644 --- a/vMenu/menus/MpPedCustomization.cs +++ b/vMenu/menus/MpPedCustomization.cs @@ -1,4 +1,4 @@ -using System; +_using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -1773,8 +1773,8 @@ void ApplySavedTattoos() _dadSelection = _random.Next(parents.Count); _mumSelection = _random.Next(parents.Count); // These equations are needed to get random float values - _skinMixValue = (float)(_random.NextDouble() * 2 -1) * 10.0f; - _shapeMixValue = (float)(_random.NextDouble() * 2 - 1) * 10.0f; + _skinMixValue = (float)_random.NextDouble() * 2 -1 * 10.0f; + _shapeMixValue = (float)_random.NextDouble() * 2 - 1 * 10.0f; SetHeadBlend(); From faad5b630d77635436064a9fa291a005d246b4bc Mon Sep 17 00:00:00 2001 From: Sky Vincent Date: Thu, 25 Sep 2025 14:03:25 -0500 Subject: [PATCH 25/54] Whoops wrong idea --- vMenu/menus/MpPedCustomization.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vMenu/menus/MpPedCustomization.cs b/vMenu/menus/MpPedCustomization.cs index 69d7dd7e6..a41febaea 100644 --- a/vMenu/menus/MpPedCustomization.cs +++ b/vMenu/menus/MpPedCustomization.cs @@ -1773,8 +1773,8 @@ void ApplySavedTattoos() _dadSelection = _random.Next(parents.Count); _mumSelection = _random.Next(parents.Count); // These equations are needed to get random float values - _skinMixValue = (float)_random.NextDouble() * 2 -1 * 10.0f; - _shapeMixValue = (float)_random.NextDouble() * 2 - 1 * 10.0f; + _skinMixValue = ((float)_random.NextDouble() * 2 -1) * 10.0f; + _shapeMixValue = ((float)_random.NextDouble() * 2 - 1) * 10.0f; SetHeadBlend(); From 21cb115f7bbfd3f4f4b91b2dbdab747a10463bff Mon Sep 17 00:00:00 2001 From: Tylerg9822-sudo Date: Thu, 25 Sep 2025 18:11:45 -0500 Subject: [PATCH 26/54] Update ConfigManager.cs remove option to disable notification per request --- SharedClasses/ConfigManager.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/SharedClasses/ConfigManager.cs b/SharedClasses/ConfigManager.cs index 853016542..3ef8d6946 100644 --- a/SharedClasses/ConfigManager.cs +++ b/SharedClasses/ConfigManager.cs @@ -42,7 +42,6 @@ public enum Setting vmenu_prevent_extras_when_damaged, vmenu_prevent_extras_engine_damage, vmenu_prevent_extras_body_damage, - vmenu_prevent_extras_notification_enabled, // MP Ped preview setting, vmenu_mp_ped_preview, From 087c4e49df66b87f136566d8fe4406cc6a4aa3fd Mon Sep 17 00:00:00 2001 From: Tylerg9822-sudo Date: Thu, 25 Sep 2025 18:14:57 -0500 Subject: [PATCH 27/54] Update VehicleOptions.cs remove option to disable notification per request --- vMenu/menus/VehicleOptions.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/vMenu/menus/VehicleOptions.cs b/vMenu/menus/VehicleOptions.cs index 1850045e1..a0efb9799 100644 --- a/vMenu/menus/VehicleOptions.cs +++ b/vMenu/menus/VehicleOptions.cs @@ -1709,10 +1709,8 @@ async void HandleItemSelect(Menu menu, MenuItem menuItem, int itemIndex) (GetVehicleBodyHealth(veh.Handle) < GetSettingsInt(Setting.vmenu_prevent_extras_engine_damage) || GetVehicleEngineHealth(veh.Handle) < GetSettingsInt(Setting.vmenu_prevent_extras_body_damage))) { - if (GetSettingsBool(Setting.vmenu_prevent_extras_notification_enabled)) - { - Notify.Alert("Vehicle is too damaged to change extra, repair it first!", true, false); - } + // Send message to player when extra change is denied + Notify.Alert("Vehicle is too damaged to change extra, repair it first!", true, false); // Revert checkbox back to original state ((MenuCheckboxItem)item).Checked = veh.IsExtraOn(extra); From 353dab1f80e35f13d68132e309f9351717769f81 Mon Sep 17 00:00:00 2001 From: Tylerg9822-sudo Date: Thu, 25 Sep 2025 18:15:28 -0500 Subject: [PATCH 28/54] Update permissions.cfg remove option to disable notification per request --- vMenuServer/config/permissions.cfg | 2 -- 1 file changed, 2 deletions(-) diff --git a/vMenuServer/config/permissions.cfg b/vMenuServer/config/permissions.cfg index 9299f754a..d1d3f6221 100644 --- a/vMenuServer/config/permissions.cfg +++ b/vMenuServer/config/permissions.cfg @@ -140,8 +140,6 @@ setr vmenu_prevent_extras_when_damaged false setr vmenu_prevent_extras_engine_damage 800 # This is the amount of body damage before the extras will be blocked. Value must be between 0 and 1000 (inclusive) setr vmenu_prevent_extras_body_damage 800 -# Enabling this will show a notification when the player attempts to modify extras. -setr vmenu_prevent_extras_notification_enabled true ### MP Ped options ### # Setting this to true will enable a 3D ped preview when viewing saved MP Peds. From 644114ed50a6b0dfe53c4ecbc694eaa20f505890 Mon Sep 17 00:00:00 2001 From: Sky Vincent Date: Thu, 25 Sep 2025 21:14:43 -0500 Subject: [PATCH 29/54] Clamp Values to `0.0f` and `1.0f` and fix slider During the conversion I forgot about when you return to the sub-menu but now it works just as needed. I also changed the code flow to help with readability. --- vMenu/menus/MpPedCustomization.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/vMenu/menus/MpPedCustomization.cs b/vMenu/menus/MpPedCustomization.cs index a41febaea..69cb59b45 100644 --- a/vMenu/menus/MpPedCustomization.cs +++ b/vMenu/menus/MpPedCustomization.cs @@ -1,4 +1,4 @@ -_using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -1068,8 +1068,9 @@ int UnclampMix(float value) inheritanceMenu.OnSliderPositionChange += (sender, item, oldPosition, newPosition, itemIndex) => { - _shapeMixValue = ((float)inheritanceShapeMix.Position) / 10.0f ; - _skinMixValue = ((float)inheritanceSkinMix.Position) / 10.0f; + // Converts the position to a float which is required by the Native + _shapeMixValue = (float)inheritanceShapeMix.Position; + _skinMixValue = (float)inheritanceSkinMix.Position; SetHeadBlend(); }; #endregion @@ -1773,8 +1774,8 @@ void ApplySavedTattoos() _dadSelection = _random.Next(parents.Count); _mumSelection = _random.Next(parents.Count); // These equations are needed to get random float values - _skinMixValue = ((float)_random.NextDouble() * 2 -1) * 10.0f; - _shapeMixValue = ((float)_random.NextDouble() * 2 - 1) * 10.0f; + _skinMixValue = (float)_random.NextDouble() * 10.0f; + _shapeMixValue = (float)_random.NextDouble() * 10.0f; SetHeadBlend(); @@ -3015,7 +3016,10 @@ private MultiplayerPedData ReplacePedDataClothing(MultiplayerPedData character) internal void SetHeadBlend() { - SetPedHeadBlendData(Game.PlayerPed.Handle, _dadSelection, _mumSelection, 0, _dadSelection, _mumSelection, 0, _shapeMixValue, _skinMixValue, 0f, false); + // Scales down from 10 then clamps the value between 0.0f - 1.0f and assigns to _shape/skinValue to prevent menu from breaking + float _shapeValue = (((_shapeMixValue) / 10.0f) + 1) / 2; + float _skinValue = (((_skinMixValue) / 10.0f) + 1) / 2; + SetPedHeadBlendData(Game.PlayerPed.Handle, _dadSelection, _mumSelection, 0, _dadSelection, _mumSelection, 0, _shapeValue, _skinValue, 0f, false); } internal void ChangePlayerHair(int newHairIndex) From 15a7addfb5a61e7f625c010668ef66aa06646bc5 Mon Sep 17 00:00:00 2001 From: "Christopher M." Date: Fri, 26 Sep 2025 14:18:45 +1000 Subject: [PATCH 30/54] Refactors slider ranges and fixes slider change value issue --- vMenu/menus/MpPedCustomization.cs | 41 ++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/vMenu/menus/MpPedCustomization.cs b/vMenu/menus/MpPedCustomization.cs index 69cb59b45..558cd3587 100644 --- a/vMenu/menus/MpPedCustomization.cs +++ b/vMenu/menus/MpPedCustomization.cs @@ -151,6 +151,10 @@ private void MakeCreateCharacterMenu(bool male, bool editPed = false) currentCharacter.ModelHash = male ? (uint)GetHashKey("mp_m_freemode_01") : (uint)GetHashKey("mp_f_freemode_01"); currentCharacter.IsMale = male; + // Places the sliders in the middle by default + _shapeMixValue = 0.5f; + _skinMixValue = 0.5f; + SetPlayerClothing(); } currentCharacter.DrawableVariations.clothes ??= new Dictionary>(); @@ -1020,15 +1024,14 @@ void AddMoms() var inheritanceDads = new MenuListItem("Father", dads.Keys.ToList(), 0, "Select a father."); var inheritanceMoms = new MenuListItem("Mother", moms.Keys.ToList(), 0, "Select a mother."); - var inheritanceShapeMix = new MenuSliderItem("Head Shape Mix", "Select how much of your head shape should be inherited from your father or mother. All the way on the left is your dad, all the way on the right is your mom.", -10, 10, 0, true) { SliderLeftIcon = MenuItem.Icon.MALE, SliderRightIcon = MenuItem.Icon.FEMALE }; - var inheritanceSkinMix = new MenuSliderItem("Body Skin Mix", "Select how much of your body skin tone should be inherited from your father or mother. All the way on the left is your dad, all the way on the right is your mom.", -10, 10, 0, true) { SliderLeftIcon = MenuItem.Icon.MALE, SliderRightIcon = MenuItem.Icon.FEMALE }; + var inheritanceShapeMix = new MenuSliderItem("Head Shape Mix", "Select how much of your head shape should be inherited from your father or mother. All the way on the left is your dad, all the way on the right is your mom.", 0, 10, 5, true) { SliderLeftIcon = MenuItem.Icon.MALE, SliderRightIcon = MenuItem.Icon.FEMALE, ItemData = "shape_mix" }; + var inheritanceSkinMix = new MenuSliderItem("Body Skin Mix", "Select how much of your body skin tone should be inherited from your father or mother. All the way on the left is your dad, all the way on the right is your mom.", 0, 10, 5, true) { SliderLeftIcon = MenuItem.Icon.MALE, SliderRightIcon = MenuItem.Icon.FEMALE, ItemData = "skin_mix" }; inheritanceMenu.AddMenuItem(inheritanceDads); inheritanceMenu.AddMenuItem(inheritanceMoms); inheritanceMenu.AddMenuItem(inheritanceShapeMix); inheritanceMenu.AddMenuItem(inheritanceSkinMix); - // formula from maintransition.#sc float GetMinimum() { @@ -1068,9 +1071,21 @@ int UnclampMix(float value) inheritanceMenu.OnSliderPositionChange += (sender, item, oldPosition, newPosition, itemIndex) => { - // Converts the position to a float which is required by the Native - _shapeMixValue = (float)inheritanceShapeMix.Position; - _skinMixValue = (float)inheritanceSkinMix.Position; + // Chris: We can't call `.Position` on the slider items here because it returns the value *prior* to the change + switch (item.ItemData) + { + case "shape_mix": + _shapeMixValue = newPosition / 10f; + break; + + case "skin_mix": + _skinMixValue = newPosition / 10f; + break; + + default: + break; + } + SetHeadBlend(); }; #endregion @@ -1773,9 +1788,8 @@ void ApplySavedTattoos() { _dadSelection = _random.Next(parents.Count); _mumSelection = _random.Next(parents.Count); - // These equations are needed to get random float values - _skinMixValue = (float)_random.NextDouble() * 10.0f; - _shapeMixValue = (float)_random.NextDouble() * 10.0f; + _skinMixValue = (float)_random.NextDouble(); + _shapeMixValue = (float)_random.NextDouble(); SetHeadBlend(); @@ -1982,8 +1996,8 @@ void ApplySavedTattoos() { inheritanceDads.ListIndex = _dadSelection; inheritanceMoms.ListIndex = _mumSelection; - inheritanceShapeMix.Position = (int)_shapeMixValue; - inheritanceSkinMix.Position = (int)_skinMixValue; + inheritanceShapeMix.Position = (int)(_shapeMixValue * 10f); + inheritanceSkinMix.Position = (int)(_skinMixValue * 10f); inheritanceMenu.RefreshIndex(); } else if (item == faceButton) @@ -3016,10 +3030,7 @@ private MultiplayerPedData ReplacePedDataClothing(MultiplayerPedData character) internal void SetHeadBlend() { - // Scales down from 10 then clamps the value between 0.0f - 1.0f and assigns to _shape/skinValue to prevent menu from breaking - float _shapeValue = (((_shapeMixValue) / 10.0f) + 1) / 2; - float _skinValue = (((_skinMixValue) / 10.0f) + 1) / 2; - SetPedHeadBlendData(Game.PlayerPed.Handle, _dadSelection, _mumSelection, 0, _dadSelection, _mumSelection, 0, _shapeValue, _skinValue, 0f, false); + SetPedHeadBlendData(Game.PlayerPed.Handle, _dadSelection, _mumSelection, 0, _dadSelection, _mumSelection, 0, _shapeMixValue, _skinMixValue, 0f, false); } internal void ChangePlayerHair(int newHairIndex) From 5b386bb9aa392583c78b458112634d0d01ed58de Mon Sep 17 00:00:00 2001 From: "Christopher M." Date: Fri, 26 Sep 2025 15:10:01 +1000 Subject: [PATCH 31/54] Enables menu's vehicle extra menu items based on vehicle damage --- SharedClasses/ConfigManager.cs | 4 +- vMenu/menus/VehicleOptions.cs | 83 ++++++++++++++++++++++++++---- vMenuServer/config/permissions.cfg | 5 +- 3 files changed, 78 insertions(+), 14 deletions(-) diff --git a/SharedClasses/ConfigManager.cs b/SharedClasses/ConfigManager.cs index 3ef8d6946..2d5ee3f71 100644 --- a/SharedClasses/ConfigManager.cs +++ b/SharedClasses/ConfigManager.cs @@ -40,8 +40,8 @@ public enum Setting // Prevent Extras Abuse vmenu_prevent_extras_when_damaged, - vmenu_prevent_extras_engine_damage, - vmenu_prevent_extras_body_damage, + vmenu_allowed_engine_damage_for_extra_change, + vmenu_allowed_body_damage_for_extra_change, // MP Ped preview setting, vmenu_mp_ped_preview, diff --git a/vMenu/menus/VehicleOptions.cs b/vMenu/menus/VehicleOptions.cs index 71591c481..163d78b31 100644 --- a/vMenu/menus/VehicleOptions.cs +++ b/vMenu/menus/VehicleOptions.cs @@ -1694,6 +1694,52 @@ async void HandleItemSelect(Menu menu, MenuItem menuItem, int itemIndex) } } }; + + // Disable all extra options if vehicle is too damaged + VehicleComponentsMenu.OnMenuOpen += (menu) => + { + Vehicle vehicle; + bool checkDamageBeforeChangingExtras = GetSettingsBool(Setting.vmenu_prevent_extras_when_damaged); + + if (!checkDamageBeforeChangingExtras || !Entity.Exists(vehicle = GetVehicle())) + { + return; + } + + List menuItems = menu.GetMenuItems(); + bool isTooDamaged = IsVehicleTooDamagedToChangeExtras(vehicle); + + menu.ClearMenuItems(); + + if (isTooDamaged && !menuItems.Exists(i => i.Text.Contains("too damaged"))) + { + MenuItem spacer = GetSpacerMenuItem("Vehicle too damaged!", "Vehicle is too damaged to change extras, repair it first!"); + + // Place at the start of the menu + menuItems.Insert(0, spacer); + } + + foreach (MenuItem item in menuItems) + { + // Check for spacer + if (item.Text.Contains("too damaged")) + { + if (!isTooDamaged) + { + continue; + } + } + else if (item.Text != "Go Back") + { + item.Enabled = !isTooDamaged; + } + + menu.AddMenuItem(item); + } + + menu.RefreshIndex(); + }; + // when a checkbox in the components menu changes VehicleComponentsMenu.OnCheckboxChange += (sender, item, index, _checked) => { @@ -1702,22 +1748,31 @@ async void HandleItemSelect(Menu menu, MenuItem menuItem, int itemIndex) if (vehicleExtras.TryGetValue(item, out var extra)) { var veh = GetVehicle(); - if (Entity.Exists(veh)) + + if (!Entity.Exists(veh)) { - // If vmenu_prevent_extras_when_damaged is enabled, check vehicle engine and body health - if (GetSettingsBool(Setting.vmenu_prevent_extras_when_damaged) && - (GetVehicleBodyHealth(veh.Handle) < GetSettingsInt(Setting.vmenu_prevent_extras_engine_damage) || - GetVehicleEngineHealth(veh.Handle) < GetSettingsInt(Setting.vmenu_prevent_extras_body_damage))) + Notify.Error(CommonErrors.NoVehicle); + return; + } + + bool checkDamageBeforeChangingExtras = GetSettingsBool(Setting.vmenu_prevent_extras_when_damaged); + + if (checkDamageBeforeChangingExtras) + { + bool isTooDamaged = IsVehicleTooDamagedToChangeExtras(veh); + + if (isTooDamaged) { // Send message to player when extra change is denied Notify.Alert("Vehicle is too damaged to change extra, repair it first!", true, false); - - // Revert checkbox back to original state - ((MenuCheckboxItem)item).Checked = veh.IsExtraOn(extra); + + // Send to previous menu + VehicleComponentsMenu.GoBack(); return; } - veh.ToggleExtra(extra, _checked); } + + veh.ToggleExtra(extra, _checked); } }; #endregion @@ -2441,5 +2496,15 @@ private int GetIndexFromColor() return 0; } #endregion + + private bool IsVehicleTooDamagedToChangeExtras(Vehicle vehicle) + { + float bodyHealth = vehicle.BodyHealth; + float engineHealth = vehicle.EngineHealth; + float allowedBodyHealth = GetSettingsInt(Setting.vmenu_allowed_body_damage_for_extra_change); + float allowedEngineHealth = GetSettingsInt(Setting.vmenu_allowed_engine_damage_for_extra_change); + + return bodyHealth < allowedBodyHealth || engineHealth < allowedEngineHealth; + } } } diff --git a/vMenuServer/config/permissions.cfg b/vMenuServer/config/permissions.cfg index d1d3f6221..405839a52 100644 --- a/vMenuServer/config/permissions.cfg +++ b/vMenuServer/config/permissions.cfg @@ -133,13 +133,12 @@ setr vmenu_sync_to_machine_time false # Setting this to true will enable the chameleon colour vehicle paints category in the primary colours menu. # You must be streaming the chameleon colours in order for this to function properly. setr vmenu_using_chameleon_colours false - # Setting this to true will prevent players from modifying vehicle extras to repair their vehicle when it is damaged. setr vmenu_prevent_extras_when_damaged false # This is the amount of engine damage before the extras will be blocked. Value must be between 0 and 1000 (inclusive) -setr vmenu_prevent_extras_engine_damage 800 +setr vmenu_allowed_engine_damage_for_extra_change 800 # This is the amount of body damage before the extras will be blocked. Value must be between 0 and 1000 (inclusive) -setr vmenu_prevent_extras_body_damage 800 +setr vmenu_allowed_body_damage_for_extra_change 800 ### MP Ped options ### # Setting this to true will enable a 3D ped preview when viewing saved MP Peds. From e5a3383c54d13bfe72f33028bd77cd88577ca279 Mon Sep 17 00:00:00 2001 From: "Christopher M." Date: Wed, 1 Oct 2025 16:30:29 +1000 Subject: [PATCH 32/54] Fixes custom vehicle RGB colors being saved erroneously Also kicks players out of the saved vehicle menu after they replace a vehicle, since the saved vehicle data is stored in the item data of each menu item, so if the player spawns a saved vehicle right after replacing it, it actually spawns the prior version - kicking them out of the menu forces the menu to be recreated, and pull the latest version of the saved vehicle. --- vMenu/CommonFunctions.cs | 19 +++++++++++++++++-- vMenu/menus/SavedVehicles.cs | 2 +- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/vMenu/CommonFunctions.cs b/vMenu/CommonFunctions.cs index 4a2d4ff4b..768de159a 100644 --- a/vMenu/CommonFunctions.cs +++ b/vMenu/CommonFunctions.cs @@ -1574,17 +1574,32 @@ public static async void SaveVehicle(string updateExistingSavedVehicleName = nul colors.Add("tyresmokeR", tyresmokeR); colors.Add("tyresmokeG", tyresmokeG); colors.Add("tyresmokeB", tyresmokeB); + int customPrimaryR = -1; int customPrimaryG = -1; int customPrimaryB = -1; - GetVehicleCustomPrimaryColour(veh.Handle, ref customPrimaryR, ref customPrimaryG, ref customPrimaryB); + bool primaryColorIsCustom = GetIsVehiclePrimaryColourCustom(veh.Handle); + + if (primaryColorIsCustom) + { + GetVehicleCustomPrimaryColour(veh.Handle, ref customPrimaryR, ref customPrimaryG, ref customPrimaryB); + } + colors.Add("customPrimaryR", customPrimaryR); colors.Add("customPrimaryG", customPrimaryG); colors.Add("customPrimaryB", customPrimaryB); + int customSecondaryR = -1; int customSecondaryG = -1; int customSecondaryB = -1; - GetVehicleCustomSecondaryColour(veh.Handle, ref customSecondaryR, ref customSecondaryG, ref customSecondaryB); + + bool secondaryColorIsCustom = GetIsVehicleSecondaryColourCustom(veh.Handle); + + if (secondaryColorIsCustom) + { + GetVehicleCustomSecondaryColour(veh.Handle, ref customSecondaryR, ref customSecondaryG, ref customSecondaryB); + } + colors.Add("customSecondaryR", customSecondaryR); colors.Add("customSecondaryG", customSecondaryG); colors.Add("customSecondaryB", customSecondaryB); diff --git a/vMenu/menus/SavedVehicles.cs b/vMenu/menus/SavedVehicles.cs index ad951a3fc..15935d658 100644 --- a/vMenu/menus/SavedVehicles.cs +++ b/vMenu/menus/SavedVehicles.cs @@ -499,7 +499,7 @@ string ChangeCallback(MenuDynamicListItem item, bool left) replaceButtonPressedCount = 0; item.Label = ""; SaveVehicle(currentlySelectedVehicle.Key.Substring(4), currentlySelectedVehicle.Value.Category); - selectedVehicleMenu.GoBack(); + selectedVehicleMenu.CloseMenu(); Notify.Success("Your saved vehicle has been replaced with your current vehicle."); } } From 26b05400bb46986fe064afc323f45a9a485f93b6 Mon Sep 17 00:00:00 2001 From: "Christopher M." Date: Wed, 1 Oct 2025 20:56:32 +1000 Subject: [PATCH 33/54] Tweaks saved vehs menu to allow all actions on missing vehs except spawn --- vMenu/menus/SavedVehicles.cs | 38 +++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/vMenu/menus/SavedVehicles.cs b/vMenu/menus/SavedVehicles.cs index 15935d658..393e52bc1 100644 --- a/vMenu/menus/SavedVehicles.cs +++ b/vMenu/menus/SavedVehicles.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; @@ -187,6 +187,9 @@ string ChangeCallback(MenuDynamicListItem item, bool left) if (savedVehicles.Count > 0) { + List spawnableVehicles = []; + List unspawnableVehicles = []; + foreach (var kvp in savedVehicles) { string name = kvp.Key; @@ -207,17 +210,37 @@ string ChangeCallback(MenuDynamicListItem item, bool left) } } + string buttonName = name.Substring(4); bool canUse = IsModelInCdimage(vehicle.model); + string buttonDescription = "Manage this saved vehicle."; + + if (!canUse) + { + buttonName = $"~italic~{buttonName}~italic~"; + buttonDescription += "\n\n~r~NOTE~w~~s~: This model could not be found, and so cannot be spawned."; + } - var btn = new MenuItem(name.Substring(4), canUse ? "Manage this saved vehicle." : "This model could not be found in the game files. Most likely because this is an addon vehicle and it's currently not streamed by the server.") + var btn = new MenuItem(buttonName, buttonDescription) { Label = $"({vehicle.name}) →→→", - Enabled = canUse, LeftIcon = canUse ? MenuItem.Icon.NONE : MenuItem.Icon.LOCK, ItemData = kvp, }; - savedVehiclesCategoryMenu.AddMenuItem(btn); + if (canUse) + { + spawnableVehicles.Add(btn); + } + else + { + unspawnableVehicles.Add(btn); + } + } + + // Menu order: Category buttons -> Spawnable vehs -> Unspawnable vehs + foreach (MenuItem menuItem in spawnableVehicles.Concat(unspawnableVehicles)) + { + savedVehiclesCategoryMenu.AddMenuItem(menuItem); } } }; @@ -420,7 +443,7 @@ string ChangeCallback(MenuDynamicListItem item, bool left) } }; - var spawnVehicle = new MenuItem("Spawn Vehicle", "Spawn this saved vehicle."); + var spawnVehicle = new MenuItem("Spawn Vehicle"); var renameVehicle = new MenuItem("Rename Vehicle", "Rename your saved vehicle."); var replaceVehicle = new MenuItem("~r~Replace Vehicle", "Your saved vehicle will be replaced with the vehicle you are currently sitting in. ~r~Warning: this can NOT be undone!"); var deleteVehicle = new MenuItem("~r~Delete Vehicle", "~r~This will delete your saved vehicle. Warning: this can NOT be undone!"); @@ -432,6 +455,11 @@ string ChangeCallback(MenuDynamicListItem item, bool left) selectedVehicleMenu.OnMenuOpen += (sender) => { + bool vehicleModelExists = IsModelInCdimage(currentlySelectedVehicle.Value.model); + + spawnVehicle.Enabled = vehicleModelExists; + spawnVehicle.Description = vehicleModelExists ? "Spawn this saved vehicle." : "This model could not be found in the game files. Most likely because this is an addon vehicle and it's currently not streamed by the server."; + spawnVehicle.Label = "(" + GetDisplayNameFromVehicleModel(currentlySelectedVehicle.Value.model).ToLower() + ")"; }; From 10fd6026f982e21b1e66b4747b96b627026eeecb Mon Sep 17 00:00:00 2001 From: "Christopher M." Date: Fri, 3 Oct 2025 17:00:38 +1000 Subject: [PATCH 34/54] Fixes blackout for vehicle lights not syncing between clients Also separates both blackout options into their own event for permissions checking purposes --- vMenu/CommonFunctions.cs | 7 +++++-- vMenu/EventManager.cs | 2 +- vMenu/menus/WeatherOptions.cs | 14 ++++++------- vMenuServer/MainServer.cs | 39 +++++++++++++++++++++++++++++------ 4 files changed, 46 insertions(+), 16 deletions(-) diff --git a/vMenu/CommonFunctions.cs b/vMenu/CommonFunctions.cs index 768de159a..ce2bf08b8 100644 --- a/vMenu/CommonFunctions.cs +++ b/vMenu/CommonFunctions.cs @@ -2044,9 +2044,12 @@ public static Dictionary JsonToDictionary(string json) /// Update the server with the new weather type, blackout status and dynamic weather changes enabled status. /// /// The new weather type. - /// Manual blackout mode enabled/disabled. /// Dynamic weather changes enabled/disabled. - public static void UpdateServerWeather(string newWeather, bool blackout, bool dynamicChanges, bool isSnowEnabled) => TriggerServerEvent("vMenu:UpdateServerWeather", newWeather, blackout, dynamicChanges, isSnowEnabled); + public static void UpdateServerWeather(string newWeather, bool dynamicChanges, bool isSnowEnabled) => TriggerServerEvent("vMenu:UpdateServerWeather", newWeather, dynamicChanges, isSnowEnabled); + + public static void UpdateServerBlackout(bool value) => TriggerServerEvent("vMenu:UpdateServerBlackout", value); + + public static void UpdateServerVehicleBlackout(bool value) => TriggerServerEvent("vMenu:UpdateServerVehicleBlackout", value); /// /// Modify the clouds for everyone. If removeClouds is true, then remove all clouds. If it's false, then randomize the clouds. diff --git a/vMenu/EventManager.cs b/vMenu/EventManager.cs index 7eb113a03..fbfd8e720 100644 --- a/vMenu/EventManager.cs +++ b/vMenu/EventManager.cs @@ -28,7 +28,7 @@ public class EventManager : BaseScript public static string GetServerWeather => GetSettingsString(Setting.vmenu_current_weather, "CLEAR"); public static bool DynamicWeatherEnabled => GetSettingsBool(Setting.vmenu_enable_dynamic_weather); public static bool IsBlackoutEnabled => GetSettingsBool(Setting.vmenu_blackout_enabled); - public static bool IsVehicleLightsEnabled { get; set; } = GetSettingsBool(Setting.vmenu_vehicle_blackout_enabled); + public static bool IsVehicleLightsEnabled => GetSettingsBool(Setting.vmenu_vehicle_blackout_enabled); public static int WeatherChangeTime => MathUtil.Clamp(GetSettingsInt(Setting.vmenu_weather_change_duration), 0, 45); /// diff --git a/vMenu/menus/WeatherOptions.cs b/vMenu/menus/WeatherOptions.cs index 53598b4b3..520fb4c12 100644 --- a/vMenu/menus/WeatherOptions.cs +++ b/vMenu/menus/WeatherOptions.cs @@ -45,7 +45,7 @@ private void CreateMenu() dynamicWeatherEnabled = new MenuCheckboxItem("Toggle Dynamic Weather", "Enable or disable dynamic weather changes.", EventManager.DynamicWeatherEnabled); blackout = new MenuCheckboxItem("Toggle Blackout", "This disables or enables all lights across the map.", EventManager.IsBlackoutEnabled); - vehicleBlackout = new MenuCheckboxItem("Toggle Vehicle Lights Blackout", "This disables or enables all vehicle lights across the map.", EventManager.IsVehicleLightsEnabled); + vehicleBlackout = new MenuCheckboxItem("Toggle Vehicle Lights Blackout", "This disables or enables all vehicle lights across the map.", !EventManager.IsVehicleLightsEnabled); snowEnabled = new MenuCheckboxItem("Enable Snow Effects", "This will force snow to appear on the ground and enable snow particle effects for peds and vehicles. Combine with X-MAS or Light Snow weather for best results.", ConfigManager.GetSettingsBool(ConfigManager.Setting.vmenu_enable_snow)); var extrasunny = new MenuItem("Extra Sunny", "Set the weather to ~y~extra sunny~s~!") { ItemData = "EXTRASUNNY" }; @@ -120,7 +120,7 @@ private void CreateMenu() else if (item.ItemData is string weatherType) { Notify.Custom($"The weather will be changed to ~y~{item.Text}~s~. This will take {EventManager.WeatherChangeTime} seconds."); - UpdateServerWeather(weatherType, EventManager.IsBlackoutEnabled, EventManager.DynamicWeatherEnabled, EventManager.IsSnowEnabled); + UpdateServerWeather(weatherType, EventManager.DynamicWeatherEnabled, EventManager.IsSnowEnabled); } }; @@ -129,22 +129,22 @@ private void CreateMenu() if (item == dynamicWeatherEnabled) { Notify.Custom($"Dynamic weather changes are now {(_checked ? "~g~enabled" : "~r~disabled")}~s~."); - UpdateServerWeather(EventManager.GetServerWeather, EventManager.IsBlackoutEnabled, _checked, EventManager.IsSnowEnabled); + UpdateServerWeather(EventManager.GetServerWeather, _checked, EventManager.IsSnowEnabled); } else if (item == blackout) { Notify.Custom($"Blackout mode is now {(_checked ? "~g~enabled" : "~r~disabled")}~s~."); - UpdateServerWeather(EventManager.GetServerWeather, _checked , EventManager.DynamicWeatherEnabled, EventManager.IsSnowEnabled); + UpdateServerBlackout(_checked); } else if (item == vehicleBlackout) { - Notify.Custom($"Vehicle lights mode is now {(_checked ? "~g~enabled" : "~r~disabled")}~s~."); - EventManager.IsVehicleLightsEnabled = _checked; + Notify.Custom($"Vehicle light blackout mode is now {(_checked ? "~g~enabled" : "~r~disabled")}~s~."); + UpdateServerVehicleBlackout(!_checked); } else if (item == snowEnabled) { Notify.Custom($"Snow effects will now be forced {(_checked ? "~g~enabled" : "~r~disabled")}~s~."); - UpdateServerWeather(EventManager.GetServerWeather, EventManager.IsBlackoutEnabled, EventManager.DynamicWeatherEnabled, _checked); + UpdateServerWeather(EventManager.GetServerWeather, EventManager.DynamicWeatherEnabled, _checked); } }; } diff --git a/vMenuServer/MainServer.cs b/vMenuServer/MainServer.cs index f7b3e8da5..85515970f 100644 --- a/vMenuServer/MainServer.cs +++ b/vMenuServer/MainServer.cs @@ -133,6 +133,11 @@ private bool BlackoutEnabled get { return GetSettingsBool(Setting.vmenu_blackout_enabled); } set { SetConvarReplicated(Setting.vmenu_blackout_enabled.ToString(), value.ToString().ToLower()); } } + private bool VehicleBlackoutEnabled + { + get { return GetSettingsBool(Setting.vmenu_vehicle_blackout_enabled); } + set { SetConvarReplicated(Setting.vmenu_vehicle_blackout_enabled.ToString(), value.ToString().ToLower()); } + } private int DynamicWeatherMinutes { get { return Math.Max(GetSettingsInt(Setting.vmenu_dynamic_weather_timer), 1); } @@ -321,7 +326,7 @@ internal void ServerCommandHandler(int source, List args, string _) var wtype = args[1].ToString().ToUpper(); if (WeatherTypes.Contains(wtype)) { - TriggerEvent("vMenu:UpdateServerWeather", wtype, BlackoutEnabled, DynamicWeatherEnabled, ManualSnowEnabled); + TriggerEvent("vMenu:UpdateServerWeather", wtype, DynamicWeatherEnabled, ManualSnowEnabled); Debug.WriteLine($"[vMenu] Weather is now set to: {wtype}"); } else if (wtype.ToLower() == "dynamic") @@ -330,12 +335,12 @@ internal void ServerCommandHandler(int source, List args, string _) { if ((args[2].ToString().ToLower() ?? $"{DynamicWeatherEnabled}") == "true") { - TriggerEvent("vMenu:UpdateServerWeather", CurrentWeather, BlackoutEnabled, true, ManualSnowEnabled); + TriggerEvent("vMenu:UpdateServerWeather", CurrentWeather, true, ManualSnowEnabled); Debug.WriteLine("[vMenu] Dynamic weather is now turned on."); } else if ((args[2].ToString().ToLower() ?? $"{DynamicWeatherEnabled}") == "false") { - TriggerEvent("vMenu:UpdateServerWeather", CurrentWeather, BlackoutEnabled, false, ManualSnowEnabled); + TriggerEvent("vMenu:UpdateServerWeather", CurrentWeather, false, ManualSnowEnabled); Debug.WriteLine("[vMenu] Dynamic weather is now turned off."); } else @@ -669,10 +674,9 @@ private void RefreshWeather() /// Update the weather for all clients. /// /// - /// /// [EventHandler("vMenu:UpdateServerWeather")] - internal void UpdateWeather([FromSource] Player source, string newWeather, bool blackoutNew, bool dynamicWeatherNew, bool enableSnow) + internal void UpdateWeather([FromSource] Player source, string newWeather, bool dynamicWeatherNew, bool enableSnow) { if (!PermissionsManager.IsAllowed(PermissionsManager.Permission.WOSetWeather, source) && !PermissionsManager.IsAllowed(PermissionsManager.Permission.WOAll, source)) { @@ -688,7 +692,6 @@ internal void UpdateWeather([FromSource] Player source, string newWeather, bool // Update the new weather related variables. CurrentWeather = newWeather; - BlackoutEnabled = blackoutNew; DynamicWeatherEnabled = dynamicWeatherNew; ManualSnowEnabled = enableSnow; @@ -696,6 +699,30 @@ internal void UpdateWeather([FromSource] Player source, string newWeather, bool lastWeatherChange = GetGameTimer(); } + [EventHandler("vMenu:UpdateServerBlackout")] + internal void UpdateBlackout([FromSource] Player source, bool value) + { + if (!PermissionsManager.IsAllowed(PermissionsManager.Permission.WOBlackout, source) && !PermissionsManager.IsAllowed(PermissionsManager.Permission.WOAll, source)) + { + BanManager.BanCheater(source); + return; + } + + BlackoutEnabled = value; + } + + [EventHandler("vMenu:UpdateServerVehicleBlackout")] + internal void UpdateVehicleBlackout([FromSource] Player source, bool value) + { + if (!PermissionsManager.IsAllowed(PermissionsManager.Permission.WOVehBlackout, source) && !PermissionsManager.IsAllowed(PermissionsManager.Permission.WOAll, source)) + { + BanManager.BanCheater(source); + return; + } + + VehicleBlackoutEnabled = value; + } + /// /// Set a new random clouds type and opacity for all clients. /// From 79e21f69adea2deea9b8b4cd85210df323e2e0ca Mon Sep 17 00:00:00 2001 From: "Christopher M." Date: Sat, 11 Oct 2025 12:17:39 +1100 Subject: [PATCH 35/54] Adds bypass permissions for damage check before extra change --- SharedClasses/PermissionsManager.cs | 1 + vMenu/menus/VehicleOptions.cs | 4 ++-- vMenuServer/config/permissions.cfg | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/SharedClasses/PermissionsManager.cs b/SharedClasses/PermissionsManager.cs index a076bd751..a078c8740 100644 --- a/SharedClasses/PermissionsManager.cs +++ b/SharedClasses/PermissionsManager.cs @@ -105,6 +105,7 @@ public enum Permission VOInfiniteFuel, VOFlares, VOPlaneBombs, + VOBypassExtraDamage, #endregion // Vehicle Spawner diff --git a/vMenu/menus/VehicleOptions.cs b/vMenu/menus/VehicleOptions.cs index 163d78b31..e4f85c06e 100644 --- a/vMenu/menus/VehicleOptions.cs +++ b/vMenu/menus/VehicleOptions.cs @@ -1699,7 +1699,7 @@ async void HandleItemSelect(Menu menu, MenuItem menuItem, int itemIndex) VehicleComponentsMenu.OnMenuOpen += (menu) => { Vehicle vehicle; - bool checkDamageBeforeChangingExtras = GetSettingsBool(Setting.vmenu_prevent_extras_when_damaged); + bool checkDamageBeforeChangingExtras = GetSettingsBool(Setting.vmenu_prevent_extras_when_damaged) && !IsAllowed(Permission.VOBypassExtraDamage); if (!checkDamageBeforeChangingExtras || !Entity.Exists(vehicle = GetVehicle())) { @@ -1755,7 +1755,7 @@ async void HandleItemSelect(Menu menu, MenuItem menuItem, int itemIndex) return; } - bool checkDamageBeforeChangingExtras = GetSettingsBool(Setting.vmenu_prevent_extras_when_damaged); + bool checkDamageBeforeChangingExtras = GetSettingsBool(Setting.vmenu_prevent_extras_when_damaged) && !IsAllowed(Permission.VOBypassExtraDamage); if (checkDamageBeforeChangingExtras) { diff --git a/vMenuServer/config/permissions.cfg b/vMenuServer/config/permissions.cfg index 405839a52..f651a3c6c 100644 --- a/vMenuServer/config/permissions.cfg +++ b/vMenuServer/config/permissions.cfg @@ -291,6 +291,7 @@ add_ace builtin.everyone "vMenu.VehicleOptions.All" allow #add_ace builtin.everyone "vMenu.VehicleOptions.InfiniteFuel" allow #add_ace builtin.everyone "vMenu.VehicleOptions.VOFlares" allow #add_ace builtin.everyone "vMenu.VehicleOptions.VOPlaneBombs" allow +#add_ace group.moderator "vMenu.VehicleOptions.BypassExtraDamage" allow #################################### # VEHICLE SPAWNER MENU # From 9ee7adfa749f4091ed484f92f545d0a3400bee5e Mon Sep 17 00:00:00 2001 From: "Christopher M." Date: Sat, 11 Oct 2025 12:47:48 +1100 Subject: [PATCH 36/54] Fixes head blend data resetting when editing saved peds --- vMenu/menus/MpPedCustomization.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/vMenu/menus/MpPedCustomization.cs b/vMenu/menus/MpPedCustomization.cs index 558cd3587..6c78dcdf6 100644 --- a/vMenu/menus/MpPedCustomization.cs +++ b/vMenu/menus/MpPedCustomization.cs @@ -157,6 +157,16 @@ private void MakeCreateCharacterMenu(bool male, bool editPed = false) SetPlayerClothing(); } + else + { + PedHeadBlendData headBlendData = currentCharacter.PedHeadBlendData; + + _dadSelection = headBlendData.FirstFaceShape; + _mumSelection = headBlendData.SecondFaceShape; + _shapeMixValue = headBlendData.ParentFaceShapePercent; + _skinMixValue = headBlendData.ParentSkinTonePercent; + } + currentCharacter.DrawableVariations.clothes ??= new Dictionary>(); currentCharacter.PropVariations.props ??= new Dictionary>(); From 0a7e49ee3df7d67d0f8a3c60a958f4993186ac02 Mon Sep 17 00:00:00 2001 From: "Christopher M." Date: Sat, 11 Oct 2025 12:59:04 +1100 Subject: [PATCH 37/54] Check for invalid saved values and correct accordingly --- vMenu/menus/MpPedCustomization.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/vMenu/menus/MpPedCustomization.cs b/vMenu/menus/MpPedCustomization.cs index 6c78dcdf6..78f094410 100644 --- a/vMenu/menus/MpPedCustomization.cs +++ b/vMenu/menus/MpPedCustomization.cs @@ -165,6 +165,18 @@ private void MakeCreateCharacterMenu(bool male, bool editPed = false) _mumSelection = headBlendData.SecondFaceShape; _shapeMixValue = headBlendData.ParentFaceShapePercent; _skinMixValue = headBlendData.ParentSkinTonePercent; + + if (_shapeMixValue > 1f) + { + Log("Shape mix value was incorrectly saved with a value higher than the possible maximum. Resetting to max value"); + _shapeMixValue = 1f; + } + + if (_skinMixValue > 1f) + { + Log("Skin mix value was incorrectly saved with a value higher than the possible maximum. Resetting to max value"); + _skinMixValue = 1f; + } } currentCharacter.DrawableVariations.clothes ??= new Dictionary>(); From 08bc4eec6d5de0629765e0a144797caced55fca1 Mon Sep 17 00:00:00 2001 From: "Christopher M." Date: Sat, 11 Oct 2025 13:56:15 +1100 Subject: [PATCH 38/54] Properly gets and sets appearance values when editing peds --- vMenu/menus/MpPedCustomization.cs | 115 ++++++++++++++++++------------ 1 file changed, 70 insertions(+), 45 deletions(-) diff --git a/vMenu/menus/MpPedCustomization.cs b/vMenu/menus/MpPedCustomization.cs index 78f094410..806dbd40f 100644 --- a/vMenu/menus/MpPedCustomization.cs +++ b/vMenu/menus/MpPedCustomization.cs @@ -121,6 +121,7 @@ public class MpPedCustomization private float _shapeMixValue; private float _skinMixValue; private readonly Dictionary shapeFaceValues = []; + // TODO: Chris: Replace with enums or something more sane - updating with index/magic numbers is nuts private readonly Dictionary> appearanceValues = []; private int _hairSelection; private int _hairColorSelection; @@ -205,15 +206,41 @@ private void MakeCreateCharacterMenu(bool male, bool editPed = false) propsMenu.ClearMenuItems(); #region appearance menu. - // Clears any saved appearance values from prior peds - _hairSelection = 0; - _hairColorSelection = 0; - _hairHighlightColorSelection = 0; - _eyeColorSelection = 0; + if (!editPed) + { + // Clears any saved appearance values from prior peds + _hairSelection = 0; + _hairColorSelection = 0; + _hairHighlightColorSelection = 0; + _eyeColorSelection = 0; - for (int i = 0; i < 12; i++) + for (int i = 0; i < 12; i++) + { + appearanceValues[i] = new Tuple(0, 0, 0f); + } + } + else { - appearanceValues[i] = new Tuple(0, 0, 0f); + PedAppearance appearanceData = currentCharacter.PedAppearance; + + _hairSelection = appearanceData.hairStyle; + _hairColorSelection = appearanceData.hairColor; + _hairHighlightColorSelection = appearanceData.hairHighlightColor; + + appearanceValues[0] = new(appearanceData.blemishesStyle, 0, appearanceData.blemishesOpacity); + appearanceValues[1] = new(appearanceData.beardStyle, appearanceData.beardColor, appearanceData.beardOpacity); + appearanceValues[2] = new(appearanceData.eyebrowsStyle, appearanceData.eyebrowsColor, appearanceData.eyebrowsOpacity); + appearanceValues[3] = new(appearanceData.ageingStyle, 0, appearanceData.ageingOpacity); + appearanceValues[4] = new(appearanceData.makeupStyle, appearanceData.makeupColor, appearanceData.makeupOpacity); + appearanceValues[5] = new(appearanceData.blushStyle, appearanceData.blushColor, appearanceData.blushOpacity); + appearanceValues[6] = new(appearanceData.complexionStyle, 0, appearanceData.complexionOpacity); + appearanceValues[7] = new(appearanceData.sunDamageStyle, 0, appearanceData.sunDamageOpacity); + appearanceValues[8] = new(appearanceData.lipstickStyle, appearanceData.lipstickColor, appearanceData.lipstickOpacity); + appearanceValues[9] = new(appearanceData.molesFrecklesStyle, 0, appearanceData.molesFrecklesOpacity); + appearanceValues[10] = new(appearanceData.chestHairStyle, appearanceData.chestHairColor, appearanceData.chestHairOpacity); + appearanceValues[11] = new(appearanceData.bodyBlemishesStyle, 0, appearanceData.bodyBlemishesOpacity); + + _eyeColorSelection = appearanceData.eyeColor; } var opacity = new List() { "0%", "10%", "20%", "30%", "40%", "50%", "60%", "70%", "80%", "90%", "100%" }; @@ -2038,57 +2065,55 @@ void ApplySavedTattoos() } else if (item == appearanceButton) { - List items = appearanceMenu.GetMenuItems(); + List menuListItems = [.. appearanceMenu.GetMenuItems().OfType()]; + + menuListItems.First(i => i.Text == "Hair Style").ListIndex = _hairSelection; + menuListItems.First(i => i.Text == "Hair Color").ListIndex = _hairColorSelection; + menuListItems.First(i => i.Text == "Hair Highlight Color").ListIndex = _hairHighlightColorSelection; - // Chris: This is so, so terrible... (and I wrote it) - // This needs to be re-done at some point. - // TODO: Make not trash - ((MenuListItem)items[0]).ListIndex = _hairSelection; - ((MenuListItem)items[1]).ListIndex = _hairColorSelection; - ((MenuListItem)items[2]).ListIndex = _hairHighlightColorSelection; - ((MenuListItem)items[33]).ListIndex = _eyeColorSelection; + menuListItems.First(i => i.Text == "Blemishes Style").ListIndex = appearanceValues[0].Item1; + menuListItems.First(i => i.Text == "Blemishes Opacity").ListIndex = (int)(appearanceValues[0].Item3 * 10); - ((MenuListItem)items[3]).ListIndex = appearanceValues[0].Item1; - ((MenuListItem)items[4]).ListIndex = (int)(appearanceValues[0].Item3 * 10); + menuListItems.First(i => i.Text == "Beard Style").ListIndex = appearanceValues[1].Item1; + menuListItems.First(i => i.Text == "Beard Opacity").ListIndex = (int)(appearanceValues[1].Item3 * 10); + menuListItems.First(i => i.Text == "Beard Color").ListIndex = appearanceValues[1].Item2; - ((MenuListItem)items[5]).ListIndex = appearanceValues[1].Item1; - ((MenuListItem)items[6]).ListIndex = (int)(appearanceValues[1].Item3 * 10); - ((MenuListItem)items[7]).ListIndex = appearanceValues[1].Item1; + menuListItems.First(i => i.Text == "Eyebrows Style").ListIndex = appearanceValues[2].Item1; + menuListItems.First(i => i.Text == "Eyebrows Opacity").ListIndex = (int)(appearanceValues[2].Item3 * 10); + menuListItems.First(i => i.Text == "Eyebrows Color").ListIndex = appearanceValues[2].Item2; - ((MenuListItem)items[8]).ListIndex = appearanceValues[2].Item1; - ((MenuListItem)items[9]).ListIndex = (int)(appearanceValues[2].Item3 * 10); - ((MenuListItem)items[10]).ListIndex = appearanceValues[2].Item1; + menuListItems.First(i => i.Text == "Ageing Style").ListIndex = appearanceValues[3].Item1; + menuListItems.First(i => i.Text == "Ageing Opacity").ListIndex = (int)(appearanceValues[3].Item3 * 10); - ((MenuListItem)items[11]).ListIndex = appearanceValues[3].Item1; - ((MenuListItem)items[12]).ListIndex = (int)(appearanceValues[3].Item3 * 10); + menuListItems.First(i => i.Text == "Makeup Style").ListIndex = appearanceValues[4].Item1; + menuListItems.First(i => i.Text == "Makeup Opacity").ListIndex = (int)(appearanceValues[4].Item3 * 10); + menuListItems.First(i => i.Text == "Makeup Color").ListIndex = appearanceValues[4].Item2; - ((MenuListItem)items[13]).ListIndex = appearanceValues[4].Item1; - ((MenuListItem)items[14]).ListIndex = (int)(appearanceValues[4].Item3 * 10); - ((MenuListItem)items[15]).ListIndex = appearanceValues[4].Item1; + menuListItems.First(i => i.Text == "Blush Style").ListIndex = appearanceValues[5].Item1; + menuListItems.First(i => i.Text == "Blush Opacity").ListIndex = (int)(appearanceValues[5].Item3 * 10); + menuListItems.First(i => i.Text == "Blush Color").ListIndex = appearanceValues[5].Item2; - ((MenuListItem)items[16]).ListIndex = appearanceValues[5].Item1; - ((MenuListItem)items[17]).ListIndex = (int)(appearanceValues[5].Item3 * 10); - ((MenuListItem)items[18]).ListIndex = appearanceValues[5].Item1; + menuListItems.First(i => i.Text == "Complexion Style").ListIndex = appearanceValues[6].Item1; + menuListItems.First(i => i.Text == "Complexion Opacity").ListIndex = (int)(appearanceValues[6].Item3 * 10); - ((MenuListItem)items[19]).ListIndex = appearanceValues[6].Item1; - ((MenuListItem)items[20]).ListIndex = (int)(appearanceValues[6].Item3 * 10); + menuListItems.First(i => i.Text == "Sun Damage Style").ListIndex = appearanceValues[7].Item1; + menuListItems.First(i => i.Text == "Sun Damage Opacity").ListIndex = (int)(appearanceValues[7].Item3 * 10); - ((MenuListItem)items[21]).ListIndex = appearanceValues[7].Item1; - ((MenuListItem)items[22]).ListIndex = (int)(appearanceValues[7].Item3 * 10); + menuListItems.First(i => i.Text == "Lipstick Style").ListIndex = appearanceValues[8].Item1; + menuListItems.First(i => i.Text == "Lipstick Opacity").ListIndex = (int)(appearanceValues[8].Item3 * 10); + menuListItems.First(i => i.Text == "Lipstick Color").ListIndex = appearanceValues[8].Item2; - ((MenuListItem)items[23]).ListIndex = appearanceValues[8].Item1; - ((MenuListItem)items[24]).ListIndex = (int)(appearanceValues[8].Item3 * 10); - ((MenuListItem)items[25]).ListIndex = appearanceValues[8].Item1; + menuListItems.First(i => i.Text == "Moles and Freckles Style").ListIndex = appearanceValues[9].Item1; + menuListItems.First(i => i.Text == "Moles and Freckles Opacity").ListIndex = (int)(appearanceValues[9].Item3 * 10); - ((MenuListItem)items[26]).ListIndex = appearanceValues[9].Item1; - ((MenuListItem)items[27]).ListIndex = (int)(appearanceValues[9].Item3 * 10); + menuListItems.First(i => i.Text == "Chest Hair Style").ListIndex = appearanceValues[10].Item1; + menuListItems.First(i => i.Text == "Chest Hair Opacity").ListIndex = (int)(appearanceValues[10].Item3 * 10); + menuListItems.First(i => i.Text == "Chest Hair Color").ListIndex = appearanceValues[10].Item2; - ((MenuListItem)items[28]).ListIndex = appearanceValues[10].Item1; - ((MenuListItem)items[29]).ListIndex = (int)(appearanceValues[10].Item3 * 10); - ((MenuListItem)items[30]).ListIndex = appearanceValues[10].Item1; + menuListItems.First(i => i.Text == "Body Blemishes Style").ListIndex = appearanceValues[11].Item1; + menuListItems.First(i => i.Text == "Body Blemishes Opacity").ListIndex = (int)(appearanceValues[11].Item3 * 10); - ((MenuListItem)items[31]).ListIndex = appearanceValues[11].Item1; - ((MenuListItem)items[32]).ListIndex = (int)(appearanceValues[11].Item3 * 10); + menuListItems.First(i => i.Text == "Eye Colors").ListIndex = _eyeColorSelection; appearanceMenu.RefreshIndex(); From b7697324e791762cc1433e699e895465347d5a0a Mon Sep 17 00:00:00 2001 From: "Christopher M." Date: Sat, 11 Oct 2025 14:06:27 +1100 Subject: [PATCH 39/54] Fixes saved mp ped category not updating in edit menu --- vMenu/menus/MpPedCustomization.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/vMenu/menus/MpPedCustomization.cs b/vMenu/menus/MpPedCustomization.cs index 806dbd40f..61e55e77e 100644 --- a/vMenu/menus/MpPedCustomization.cs +++ b/vMenu/menus/MpPedCustomization.cs @@ -768,8 +768,19 @@ private void MakeCreateCharacterMenu(bool male, bool editPed = false) List categoryIcons = GetCategoryIcons(categoryNames); categoryBtn.ItemData = new Tuple, List>(categoryNames, categoryIcons); - categoryBtn.ListItems = categoryNames; - categoryBtn.ListIndex = 0; + categoryBtn.ListItems = categoryNames; + + if (editPed) + { + int characterCategoryIndex = categoryNames.IndexOf(currentCharacter.Category); + + categoryBtn.ListIndex = characterCategoryIndex; + } + else + { + categoryBtn.ListIndex = 0; + } + categoryBtn.RightIcon = categoryIcons[categoryBtn.ListIndex]; createCharacterMenu.RefreshIndex(); From d8daaa4d327a6f45407318c306e40f0a006d19a3 Mon Sep 17 00:00:00 2001 From: "Christopher M." Date: Wed, 15 Oct 2025 22:03:09 +1100 Subject: [PATCH 40/54] Refactors client onesync & permissions check Also moves MenuToggle keymapping from constructor to PostPermissions function since the menu might not exist when the constructor runs --- vMenu/MainMenu.cs | 57 ++++++++++++++++----------------------- vMenuServer/MainServer.cs | 25 ++++++++--------- 2 files changed, 34 insertions(+), 48 deletions(-) diff --git a/vMenu/MainMenu.cs b/vMenu/MainMenu.cs index 45f0d7086..4f936d07a 100644 --- a/vMenu/MainMenu.cs +++ b/vMenu/MainMenu.cs @@ -120,7 +120,7 @@ public MainMenu() } #endregion #region keymapping - string KeyMappingID = String.IsNullOrWhiteSpace(GetSettingsString(Setting.vmenu_keymapping_id)) ? "Default" : GetSettingsString(Setting.vmenu_keymapping_id); + string KeyMappingID = GetKeyMappingId(); RegisterCommand($"vMenu:{KeyMappingID}:NoClip", new Action, string>((dynamic source, List args, string rawCommand) => { if (IsAllowed(Permission.NoClip)) @@ -144,20 +144,6 @@ public MainMenu() } } }), false); - RegisterCommand($"vMenu:{KeyMappingID}:MenuToggle", new Action, string>((dynamic source, List args, string rawCommand) => - { - if (MenuEnabled) - { - if (!MenuController.IsAnyMenuOpen()) - { - Menu.OpenMenu(); - } - else - { - MenuController.CloseAllMenus(); - } - } - }), false); if (!(GetSettingsString(Setting.vmenu_noclip_toggle_key) == null)) { @@ -340,29 +326,13 @@ public MainMenu() // Clear all previous pause menu info/brief messages on resource start. ClearBrief(); - // Request the permissions data from the server. - TriggerServerEvent("vMenu:RequestPermissions"); - - // Request server state from the server. - TriggerServerEvent("vMenu:RequestServerState"); - } - - #region Infinity bits - [EventHandler("vMenu:SetServerState")] - public void SetServerState(IDictionary data) - { - if (data.TryGetValue("IsInfinity", out var isInfinity)) + if (GlobalState.Get("vmenu_onesync") ?? false) { - if (isInfinity is bool isInfinityBool) - { - if (isInfinityBool) - { - PlayersList = new InfinityPlayerList(Players); - } - } + PlayersList = new InfinityPlayerList(Players); } } + #region Infinity bits [EventHandler("vMenu:ReceivePlayerList")] public void ReceivedPlayerList(IList players) { @@ -540,6 +510,21 @@ static bool canUseMenu() StatSetFloat((uint)GetHashKey("MP0_PLAYER_MENTAL_STATE"), 0f, true); // Mental State } + RegisterCommand($"vMenu:{GetKeyMappingId()}:MenuToggle", new Action, string>((dynamic source, List args, string rawCommand) => + { + if (MenuEnabled) + { + if (!MenuController.IsAnyMenuOpen()) + { + Menu.OpenMenu(); + } + else + { + MenuController.CloseAllMenus(); + } + } + }), false); + TriggerEvent("vMenu:SetupTickFunctions"); } @@ -920,5 +905,9 @@ private static void CreateSubmenus() } } #endregion + + #region Utilities + private static string GetKeyMappingId() => string.IsNullOrWhiteSpace(GetSettingsString(Setting.vmenu_keymapping_id)) ? "Default" : GetSettingsString(Setting.vmenu_keymapping_id); + #endregion } } diff --git a/vMenuServer/MainServer.cs b/vMenuServer/MainServer.cs index 85515970f..8382eb1b5 100644 --- a/vMenuServer/MainServer.cs +++ b/vMenuServer/MainServer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -222,8 +222,6 @@ public MainServer() }); CallbackFunction(JsonConvert.SerializeObject(data)); })); - EventHandlers.Add("vMenu:RequestPermissions", new Action(PermissionsManager.SetPermissionsForPlayer)); - EventHandlers.Add("vMenu:RequestServerState", new Action(RequestServerStateFromPlayer)); // check addons file for errors var addons = LoadResourceFile(GetCurrentResourceName(), "config/addons.json") ?? "{}"; @@ -267,6 +265,8 @@ public MainServer() { Tick += TimeLoop; } + + GlobalState.Set("vmenu_onesync", GetConvar("onesync", "off") == "on", true); } } #endregion @@ -960,14 +960,6 @@ internal void AddTeleportLocation([FromSource] Player _, string locationJson) #endregion #region Infinity bits - private void RequestServerStateFromPlayer([FromSource] Player player) - { - player.TriggerEvent("vMenu:SetServerState", new - { - IsInfinity = GetConvar("onesync_enableInfinity", "false") == "true" - }); - } - [EventHandler("vMenu:RequestPlayerList")] internal void RequestPlayerListFromPlayer([FromSource] Player player) { @@ -994,16 +986,19 @@ internal void GetPlayerCoords([FromSource] Player source, long rpcId, int player #region Player join/quit private readonly HashSet joinedPlayers = new(); - private Task PlayersFirstTick() + private async Task PlayersFirstTick() { Tick -= PlayersFirstTick; + // Allow plenty of time for the connected clients to restart their client scripts + await Delay(5_000); + foreach (var player in Players) { joinedPlayers.Add(player.Handle); - } - return Task.FromResult(0); + PermissionsManager.SetPermissionsForPlayer(player); + } } [EventHandler("playerJoining")] @@ -1011,6 +1006,8 @@ internal void OnPlayerJoining([FromSource] Player sourcePlayer) { joinedPlayers.Add(sourcePlayer.Handle); + PermissionsManager.SetPermissionsForPlayer(sourcePlayer); + foreach (var player in Players) { if (IsPlayerAceAllowed(player.Handle, "vMenu.MiscSettings.JoinQuitNotifs") || From 33a8d66bc1c26571d80b3a60e91068ff685c268e Mon Sep 17 00:00:00 2001 From: "Christopher M." Date: Thu, 16 Oct 2025 20:17:34 +1100 Subject: [PATCH 41/54] Refactors Clear Area to use ped position from server --- vMenu/EventManager.cs | 7 ++----- vMenu/menus/MiscSettings.cs | 3 +-- vMenuServer/MainServer.cs | 10 +++++----- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/vMenu/EventManager.cs b/vMenu/EventManager.cs index fbfd8e720..288428b37 100644 --- a/vMenu/EventManager.cs +++ b/vMenu/EventManager.cs @@ -46,7 +46,7 @@ public EventManager() EventHandlers.Add("vMenu:SetClouds", new Action(SetClouds)); EventHandlers.Add("vMenu:GoodBye", new Action(GoodBye)); EventHandlers.Add("vMenu:SetBanList", new Action(UpdateBanList)); - EventHandlers.Add("vMenu:ClearArea", new Action(ClearAreaNearPos)); + EventHandlers.Add("vMenu:ClearArea", new Action(ClearAreaNearPos)); EventHandlers.Add("vMenu:updatePedDecors", new Action(UpdatePedDecors)); EventHandlers.Add("playerSpawned", new Action(SetAppearanceOnFirstSpawn)); EventHandlers.Add("vMenu:GetOutOfCar", new Action(GetOutOfCar)); @@ -378,10 +378,7 @@ private async void SummonPlayer(string targetPlayer) /// /// /// - private void ClearAreaNearPos(float x, float y, float z) - { - ClearAreaOfEverything(x, y, z, 100f, false, false, false, false); - } + private void ClearAreaNearPos(Vector3 position) => ClearAreaOfEverything(position.X, position.Y, position.Z, 100f, false, false, false, false); /// /// Kicks the current player from the specified vehicle if they're inside and don't own the vehicle themselves. diff --git a/vMenu/menus/MiscSettings.cs b/vMenu/menus/MiscSettings.cs index ec9629214..66d9bebc1 100644 --- a/vMenu/menus/MiscSettings.cs +++ b/vMenu/menus/MiscSettings.cs @@ -469,8 +469,7 @@ private void CreateMenu() { if (item == clearArea) { - var pos = Game.PlayerPed.Position; - BaseScript.TriggerServerEvent("vMenu:ClearArea", pos.X, pos.Y, pos.Z); + BaseScript.TriggerServerEvent("vMenu:ClearArea"); } }; diff --git a/vMenuServer/MainServer.cs b/vMenuServer/MainServer.cs index 8382eb1b5..5bdf51948 100644 --- a/vMenuServer/MainServer.cs +++ b/vMenuServer/MainServer.cs @@ -544,13 +544,13 @@ internal void GetOutOfCar([FromSource] Player source, int vehicleNetId, int play /// /// Clear the area near this point for all players. /// - /// - /// - /// [EventHandler("vMenu:ClearArea")] - internal void ClearAreaNearPos(float x, float y, float z) + internal void ClearAreaNearPos([FromSource] Player source) { - TriggerClientEvent("vMenu:ClearArea", x, y, z); + Ped ped = source.Character; + Vector3 position = ped.Position; + + TriggerClientEvent("vMenu:ClearArea", position); } #endregion From bcfc67e67c7b0189819eb3f1609a48e7e62572d2 Mon Sep 17 00:00:00 2001 From: "Christopher M." Date: Thu, 16 Oct 2025 20:22:54 +1100 Subject: [PATCH 42/54] Improves feedback when trying to disable snow effects with snow weather --- vMenu/menus/WeatherOptions.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vMenu/menus/WeatherOptions.cs b/vMenu/menus/WeatherOptions.cs index 520fb4c12..b742ec5e6 100644 --- a/vMenu/menus/WeatherOptions.cs +++ b/vMenu/menus/WeatherOptions.cs @@ -143,6 +143,12 @@ private void CreateMenu() } else if (item == snowEnabled) { + if (EventManager.GetServerWeather is "XMAS" or "SNOWLIGHT" or "SNOW" or "BLIZZARD") + { + Notify.Custom($"Snow effects cannot be disabled when weather is ~y~{EventManager.GetServerWeather}~s~."); + return; + } + Notify.Custom($"Snow effects will now be forced {(_checked ? "~g~enabled" : "~r~disabled")}~s~."); UpdateServerWeather(EventManager.GetServerWeather, EventManager.DynamicWeatherEnabled, _checked); } From 649adff8f373064f5362f94cb23d49436c547ed5 Mon Sep 17 00:00:00 2001 From: "Christopher M." Date: Thu, 16 Oct 2025 21:01:06 +1100 Subject: [PATCH 43/54] Refactors Personal Vehicle Kick out logic to be server-sided --- vMenu/EventManager.cs | 58 ------------------------------ vMenu/menus/PersonalVehicle.cs | 7 ++-- vMenuServer/MainServer.cs | 66 ++++++++++++++++++++++++++++++---- 3 files changed, 64 insertions(+), 67 deletions(-) diff --git a/vMenu/EventManager.cs b/vMenu/EventManager.cs index 288428b37..19394f537 100644 --- a/vMenu/EventManager.cs +++ b/vMenu/EventManager.cs @@ -49,7 +49,6 @@ public EventManager() EventHandlers.Add("vMenu:ClearArea", new Action(ClearAreaNearPos)); EventHandlers.Add("vMenu:updatePedDecors", new Action(UpdatePedDecors)); EventHandlers.Add("playerSpawned", new Action(SetAppearanceOnFirstSpawn)); - EventHandlers.Add("vMenu:GetOutOfCar", new Action(GetOutOfCar)); EventHandlers.Add("vMenu:PrivateMessage", new Action(PrivateMessage)); EventHandlers.Add("vMenu:UpdateTeleportLocations", new Action(UpdateTeleportLocations)); @@ -380,63 +379,6 @@ private async void SummonPlayer(string targetPlayer) /// private void ClearAreaNearPos(Vector3 position) => ClearAreaOfEverything(position.X, position.Y, position.Z, 100f, false, false, false, false); - /// - /// Kicks the current player from the specified vehicle if they're inside and don't own the vehicle themselves. - /// - /// - /// - private async void GetOutOfCar(int vehNetId, int vehicleOwnedBy) - { - if (NetworkDoesNetworkIdExist(vehNetId)) - { - var veh = NetToVeh(vehNetId); - if (DoesEntityExist(veh)) - { - var vehicle = new Vehicle(veh); - - if (vehicle == null || !vehicle.Exists()) - { - return; - } - - if (Game.PlayerPed.IsInVehicle(vehicle) && vehicleOwnedBy != Game.Player.ServerId) - { - if (!vehicle.IsStopped) - { - Notify.Alert("The owner of this vehicle is reclaiming their personal vehicle. You will be kicked from this vehicle in about 10 seconds. Stop the vehicle now to avoid taking damage.", false, true); - } - - // Wait for the vehicle to come to a stop, or 10 seconds, whichever is faster. - var timer = GetGameTimer(); - while (vehicle != null && vehicle.Exists() && !vehicle.IsStopped) - { - await Delay(0); - if (GetGameTimer() - timer > (10 * 1000)) // 10 second timeout - { - break; - } - } - - // just to make sure they're actually still inside the vehicle and the vehicle still exists. - if (vehicle != null && vehicle.Exists() && Game.PlayerPed.IsInVehicle(vehicle)) - { - // Make the ped jump out because the car isn't stopped yet. - if (!vehicle.IsStopped) - { - Notify.Info("You were warned, now you'll have to suffer the consequences!"); - TaskLeaveVehicle(Game.PlayerPed.Handle, vehicle.Handle, 4160); - } - // Make the ped exit gently. - else - { - TaskLeaveVehicle(Game.PlayerPed.Handle, vehicle.Handle, 0); - } - } - } - } - } - } - /// /// Updates ped decorators for the clothing animation when players have joined. /// diff --git a/vMenu/menus/PersonalVehicle.cs b/vMenu/menus/PersonalVehicle.cs index 667b41324..5fd840d02 100644 --- a/vMenu/menus/PersonalVehicle.cs +++ b/vMenu/menus/PersonalVehicle.cs @@ -280,10 +280,11 @@ private void CreateMenu() { if (item == kickAllPassengers) { - if (CurrentPersonalVehicle.Occupants.Count() > 0 && CurrentPersonalVehicle.Occupants.Any(p => p != Game.PlayerPed)) + Ped[] occupants = CurrentPersonalVehicle.Occupants; + + if (occupants.Count() > 0 && occupants.Any(p => p != Game.PlayerPed && p.IsPlayer)) { - var netId = VehToNet(CurrentPersonalVehicle.Handle); - TriggerServerEvent("vMenu:GetOutOfCar", netId, Game.Player.ServerId); + TriggerServerEvent("vMenu:GetOutOfCar", CurrentPersonalVehicle.NetworkId); } else { diff --git a/vMenuServer/MainServer.cs b/vMenuServer/MainServer.cs index 5bdf51948..482643b7c 100644 --- a/vMenuServer/MainServer.cs +++ b/vMenuServer/MainServer.cs @@ -525,17 +525,47 @@ internal void ServerCommandHandler(int source, List args, string _) /// /// /// - /// [EventHandler("vMenu:GetOutOfCar")] - internal void GetOutOfCar([FromSource] Player source, int vehicleNetId, int playerOwner) + internal void GetOutOfCar([FromSource] Player source, int vehicleNetId) { - if (source != null) + if (!PermissionsManager.IsAllowed(PermissionsManager.Permission.PVKickPassengers, source) && !PermissionsManager.IsAllowed(PermissionsManager.Permission.PVAll, source)) { - if (vMenuShared.PermissionsManager.GetPermissionAndParentPermissions(vMenuShared.PermissionsManager.Permission.PVKickPassengers).Any(perm => vMenuShared.PermissionsManager.IsAllowed(perm, source))) + BanManager.BanCheater(source); + return; + } + + Entity vehicle = Entity.FromNetworkId(vehicleNetId); + + if (vehicle is null) + { + return; + } + + int vehicleHandle = vehicle.Handle; + + for (int i = -1; i < 15; i++) + { + int pedHandle = GetPedInVehicleSeat(vehicleHandle, i); + + if (pedHandle == 0 || !IsPedAPlayer(pedHandle)) + { + continue; + } + + int playerHandle = NetworkGetEntityOwner(pedHandle); + + Player player = GetPlayerFromServerId(playerHandle); + + if (player is null || player == source) { - TriggerClientEvent("vMenu:GetOutOfCar", vehicleNetId, playerOwner); - source.TriggerEvent("vMenu:Notify", "All passengers will be kicked out as soon as the vehicle stops moving, or after 10 seconds if they refuse to stop the vehicle."); + continue; } + + int warpOutFlag = 16; + + TaskLeaveVehicle(pedHandle, vehicleHandle, warpOutFlag); + + player.TriggerEvent("vMenu:Notify", "The owner of the vehicle has kicked you out."); } } #endregion @@ -1038,5 +1068,29 @@ internal void OnPlayerDropped([FromSource] Player sourcePlayer, string reason) } } #endregion + + #region Utilities + private Player GetPlayerFromServerId(string serverId) + { + if (!int.TryParse(serverId, out int serverIdInt)) + { + return null; + } + + return GetPlayerFromServerId(serverIdInt); + } + + private Player GetPlayerFromServerId(int serverId) + { + string serverIdString = serverId.ToString(); + + if (serverId <= 0 || !DoesPlayerExist(serverIdString)) + { + return null; + } + + return Players[serverId]; + } + #endregion } } From b7a0244d156f1efedfa2f7fa2842d202cd3754db Mon Sep 17 00:00:00 2001 From: "Christopher M." Date: Thu, 16 Oct 2025 21:09:28 +1100 Subject: [PATCH 44/54] Refactor Kick Player server event --- vMenuServer/MainServer.cs | 41 ++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/vMenuServer/MainServer.cs b/vMenuServer/MainServer.cs index 482643b7c..776572b37 100644 --- a/vMenuServer/MainServer.cs +++ b/vMenuServer/MainServer.cs @@ -832,34 +832,31 @@ internal void FreezeServerTime([FromSource] Player source, bool freezeTime) [EventHandler("vMenu:KickPlayer")] internal void KickPlayer([FromSource] Player source, int target, string kickReason = "You have been kicked from the server.") { - if (IsPlayerAceAllowed(source.Handle, "vMenu.OnlinePlayers.Kick") || IsPlayerAceAllowed(source.Handle, "vMenu.Everything") || - IsPlayerAceAllowed(source.Handle, "vMenu.OnlinePlayers.All")) + if (!PermissionsManager.IsAllowed(PermissionsManager.Permission.OPKick, source) && !PermissionsManager.IsAllowed(PermissionsManager.Permission.POAll, source)) { - // If the player is allowed to be kicked. - var targetPlayer = Players[target]; - if (targetPlayer != null) - { - if (!IsPlayerAceAllowed(targetPlayer.Handle, "vMenu.DontKickMe")) - { - TriggerEvent("vMenu:KickSuccessful", source.Name, kickReason, targetPlayer.Name); + BanManager.BanCheater(source); + return; + } - KickLog($"Player: {source.Name} has kicked: {targetPlayer.Name} for: {kickReason}."); - TriggerClientEvent(player: source, eventName: "vMenu:Notify", args: $"The target player ({targetPlayer.Name}) has been kicked."); + Player targetPlayer = GetPlayerFromServerId(target); - // Kick the player from the server using the specified reason. - DropPlayer(targetPlayer.Handle, kickReason); - return; - } - // Trigger the client event on the source player to let them know that kicking this player is not allowed. - TriggerClientEvent(player: source, eventName: "vMenu:Notify", args: "Sorry, this player can ~r~not ~w~be kicked."); - return; - } - TriggerClientEvent(player: source, eventName: "vMenu:Notify", args: "An unknown error occurred. Report it here: vespura.com/vmenu"); + if (targetPlayer is null) + { + source.TriggerEvent("vMenu:Notify", "Failed to kick target, because the target could not be found. Did they already leave?"); + return; } - else + + if (PermissionsManager.IsAllowed(PermissionsManager.Permission.DontKickMe, targetPlayer)) { - BanManager.BanCheater(source); + source.TriggerEvent("vMenu:Notify", "Sorry, this player can ~r~not ~w~be kicked."); + return; } + + KickLog($"Player: {source.Name} has kicked: {targetPlayer.Name} for: {kickReason}."); + + source.TriggerEvent("vMenu:Notify", $"The target player ({targetPlayer.Name}) has been kicked."); + + targetPlayer.Drop(kickReason); } /// From 3bd21f33ecb3a9ca2ba1109cc2df95cf047a2519 Mon Sep 17 00:00:00 2001 From: "Christopher M." Date: Fri, 17 Oct 2025 20:33:24 +1100 Subject: [PATCH 45/54] Refactor Kill Player server event --- vMenuServer/MainServer.cs | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/vMenuServer/MainServer.cs b/vMenuServer/MainServer.cs index 776572b37..e06a5b11c 100644 --- a/vMenuServer/MainServer.cs +++ b/vMenuServer/MainServer.cs @@ -867,22 +867,20 @@ internal void KickPlayer([FromSource] Player source, int target, string kickReas [EventHandler("vMenu:KillPlayer")] internal void KillPlayer([FromSource] Player source, int target) { - if (IsPlayerAceAllowed(source.Handle, "vMenu.OnlinePlayers.Kill") || IsPlayerAceAllowed(source.Handle, "vMenu.Everything") || - IsPlayerAceAllowed(source.Handle, "vMenu.OnlinePlayers.All")) + if (!PermissionsManager.IsAllowed(PermissionsManager.Permission.OPKill, source) && !PermissionsManager.IsAllowed(PermissionsManager.Permission.OPAll, source)) { - var targetPlayer = Players[target]; - if (targetPlayer != null) - { - // Trigger the client event on the target player to make them kill themselves. R.I.P. - TriggerClientEvent(player: targetPlayer, eventName: "vMenu:KillMe", args: source.Name); - return; - } - TriggerClientEvent(player: source, eventName: "vMenu:Notify", args: "An unknown error occurred. Report it here: vespura.com/vmenu"); + BanManager.BanCheater(source); + return; } - else + + Player targetPlayer = GetPlayerFromServerId(target); + + if (targetPlayer is null) { - BanManager.BanCheater(source); + return; } + + targetPlayer.TriggerEvent("vMenu:KillMe", source.Name); } /// From 13f42dce73d7dbecd6e108a05517280c8f60b0df Mon Sep 17 00:00:00 2001 From: "Christopher M." Date: Sun, 19 Oct 2025 12:07:55 +1100 Subject: [PATCH 46/54] Refactor Summon Player logic to be server-sided --- vMenu/CommonFunctions.cs | 8 +++- vMenu/EventManager.cs | 19 --------- vMenuServer/MainServer.cs | 81 +++++++++++++++++++++++++++++++++------ 3 files changed, 77 insertions(+), 31 deletions(-) diff --git a/vMenu/CommonFunctions.cs b/vMenu/CommonFunctions.cs index ce2bf08b8..c60d24dd8 100644 --- a/vMenu/CommonFunctions.cs +++ b/vMenu/CommonFunctions.cs @@ -966,7 +966,13 @@ public static async void CommitSuicide() /// Summon player. /// /// - public static void SummonPlayer(IPlayer player) => TriggerServerEvent("vMenu:SummonPlayer", player.ServerId); + public static void SummonPlayer(IPlayer player) + { + Vehicle currentVehicle = GetVehicle(); + int numberOfSeats = currentVehicle is not null ? GetVehicleModelNumberOfSeats(currentVehicle.Model) : 0; + + TriggerServerEvent("vMenu:SummonPlayer", player.ServerId, numberOfSeats); + } #endregion #region Spectate function diff --git a/vMenu/EventManager.cs b/vMenu/EventManager.cs index 19394f537..909516b6c 100644 --- a/vMenu/EventManager.cs +++ b/vMenu/EventManager.cs @@ -40,7 +40,6 @@ public EventManager() EventHandlers.Add("vMenu:SetAddons", new Action(SetConfigOptions)); // DEPRECATED: Backwards-compatible event handler; use 'vMenu:SetConfigOptions' instead EventHandlers.Add("vMenu:SetConfigOptions", new Action(SetConfigOptions)); EventHandlers.Add("vMenu:SetPermissions", new Action(MainMenu.SetPermissions)); - EventHandlers.Add("vMenu:GoToPlayer", new Action(SummonPlayer)); EventHandlers.Add("vMenu:KillMe", new Action(KillMe)); EventHandlers.Add("vMenu:Notify", new Action(NotifyPlayer)); EventHandlers.Add("vMenu:SetClouds", new Action(SetClouds)); @@ -352,24 +351,6 @@ private void KillMe(string sourceName) SetEntityHealth(Game.PlayerPed.Handle, 0); } - /// - /// Teleport to the specified player. - /// - /// - private async void SummonPlayer(string targetPlayer) - { - // ensure the player list is requested in case of Infinity - MainMenu.PlayersList.RequestPlayerList(); - await MainMenu.PlayersList.WaitRequested(); - - var player = MainMenu.PlayersList.FirstOrDefault(a => a.ServerId == int.Parse(targetPlayer)); - - if (player != null) - { - _ = TeleportToPlayer(player); - } - } - /// /// Clear the area around the provided x, y, z coordinates. Clears everything like (destroyed) objects, peds, (ai) vehicles, etc. /// Also restores broken streetlights, etc. diff --git a/vMenuServer/MainServer.cs b/vMenuServer/MainServer.cs index e06a5b11c..36bc257ee 100644 --- a/vMenuServer/MainServer.cs +++ b/vMenuServer/MainServer.cs @@ -889,23 +889,82 @@ internal void KillPlayer([FromSource] Player source, int target) /// /// [EventHandler("vMenu:SummonPlayer")] - internal void SummonPlayer([FromSource] Player source, int target) + internal async void SummonPlayer([FromSource] Player source, int target, int numberOfSeats) { - if (IsPlayerAceAllowed(source.Handle, "vMenu.OnlinePlayers.Summon") || IsPlayerAceAllowed(source.Handle, "vMenu.Everything") || - IsPlayerAceAllowed(source.Handle, "vMenu.OnlinePlayers.All")) + if (!PermissionsManager.IsAllowed(PermissionsManager.Permission.OPSummon, source) && !PermissionsManager.IsAllowed(PermissionsManager.Permission.OPAll, source)) + { + BanManager.BanCheater(source); + return; + } + + Player targetPlayer = GetPlayerFromServerId(target); + + if (targetPlayer is null) + { + return; + } + + int targetPedHandle; + Ped targetPed = targetPlayer.Character; + + if (targetPed is null || !DoesEntityExist(targetPedHandle = targetPed.Handle)) + { + return; + } + + Ped sourcePed = source.Character; + int sourcePedHandle = sourcePed.Handle; + + bool lastVehicle = false; + int sourcePedVehicle = GetVehiclePedIsIn(sourcePedHandle, lastVehicle); + + if (sourcePedVehicle == 0) + { + targetPed.Position = sourcePed.Position; + return; + } + + bool seatFound = false; + + // Seat indices start at `-1` + numberOfSeats -= 1; + + for (int i = -1; i < numberOfSeats; i++) { - // Trigger the client event on the target player to make them teleport to the source player. - var targetPlayer = Players[target]; - if (targetPlayer != null) + bool seatFree = GetPedInVehicleSeat(sourcePedVehicle, i) == 0; + + if (!seatFree) { - TriggerClientEvent(player: targetPlayer, eventName: "vMenu:GoToPlayer", args: source.Handle); - return; + continue; } - TriggerClientEvent(player: source, eventName: "vMenu:Notify", args: "An unknown error occurred. Report it here: vespura.com/vmenu"); + + Vector3 priorPosition = targetPed.Position; + Vector3 newPosition = sourcePed.Position + new Vector3(0f, 0f, 5f); + + seatFound = true; + targetPed.Position = newPosition; + + Vector3 checkPosition; + long timeout = GetGameTimer() + 1_500; + + while (timeout > GetGameTimer() && priorPosition.DistanceToSquared(checkPosition = targetPed.Position) < newPosition.DistanceToSquared(checkPosition)) + { + await Delay(100); + } + + if (timeout < GetGameTimer()) + { + source.TriggerEvent("vMenu:Notify", "Failed to teleport player."); + break; + } + + SetPedIntoVehicle(targetPedHandle, sourcePedVehicle, i); + break; } - else + + if (!seatFound) { - BanManager.BanCheater(source); + source.TriggerEvent("vMenu:Notify", "No free seats in your vehicle for summoned player."); } } From 1bf4606c3bca6665e32c79e64e81c79b444a85c6 Mon Sep 17 00:00:00 2001 From: "Christopher M." Date: Sun, 19 Oct 2025 17:05:11 +1100 Subject: [PATCH 47/54] Refactor Join & Leave server events --- SharedClasses/PermissionsManager.cs | 27 ++++++++++++++++--- vMenuServer/MainServer.cs | 40 +++++++++++++++++++---------- 2 files changed, 50 insertions(+), 17 deletions(-) diff --git a/SharedClasses/PermissionsManager.cs b/SharedClasses/PermissionsManager.cs index a078c8740..7c5efe7a5 100644 --- a/SharedClasses/PermissionsManager.cs +++ b/SharedClasses/PermissionsManager.cs @@ -385,9 +385,16 @@ public enum Permission /// /// /// - /// if true, then the permissions will be checked even if they aren't setup yet. /// public static bool IsAllowed(Permission permission, Player source) => IsAllowedServer(permission, source); + + /// + /// Public function to check if a permission is allowed. + /// + /// + /// + /// + public static bool IsAllowed(Permission permission, string playerHandle) => IsAllowedServer(permission, playerHandle); #endif #if CLIENT @@ -456,11 +463,23 @@ private static bool IsAllowedServer(Permission permission, Player source) return false; } - if (IsPlayerAceAllowed(source.Handle, GetAceName(permission))) + return IsAllowedServer(permission, source.Handle); + } + + /// + /// Checks if the player is allowed that specific permission. + /// + /// + /// + /// + private static bool IsAllowedServer(Permission permission, string playerHandle) + { + if (!DoesPlayerExist(playerHandle)) { - return true; + return false; } - return false; + + return IsPlayerAceAllowed(playerHandle, GetAceName(permission)); } #endif diff --git a/vMenuServer/MainServer.cs b/vMenuServer/MainServer.cs index 36bc257ee..7630342d8 100644 --- a/vMenuServer/MainServer.cs +++ b/vMenuServer/MainServer.cs @@ -1070,12 +1070,34 @@ internal void GetPlayerCoords([FromSource] Player source, long rpcId, int player #region Player join/quit private readonly HashSet joinedPlayers = new(); + private IEnumerable GetJoinQuitNotifPlayers() + { + List players = []; + + foreach (string playerHandle in joinedPlayers) + { + if (!PermissionsManager.IsAllowed(PermissionsManager.Permission.MSJoinQuitNotifs, playerHandle) && !PermissionsManager.IsAllowed(PermissionsManager.Permission.MSAll, playerHandle)) + { + continue; + } + + Player player = GetPlayerFromServerId(playerHandle); + + if (player is not null) + { + players.Add(player); + } + } + + return players; + } + private async Task PlayersFirstTick() { Tick -= PlayersFirstTick; // Allow plenty of time for the connected clients to restart their client scripts - await Delay(5_000); + await Delay(3_000); foreach (var player in Players) { @@ -1092,13 +1114,9 @@ internal void OnPlayerJoining([FromSource] Player sourcePlayer) PermissionsManager.SetPermissionsForPlayer(sourcePlayer); - foreach (var player in Players) + foreach (Player notifPlayer in GetJoinQuitNotifPlayers()) { - if (IsPlayerAceAllowed(player.Handle, "vMenu.MiscSettings.JoinQuitNotifs") || - IsPlayerAceAllowed(player.Handle, "vMenu.MiscSettings.All")) - { - player.TriggerEvent("vMenu:PlayerJoinQuit", sourcePlayer.Name, null); - } + notifPlayer.TriggerEvent("vMenu:PlayerJoinQuit", sourcePlayer.Name, null); } } @@ -1112,13 +1130,9 @@ internal void OnPlayerDropped([FromSource] Player sourcePlayer, string reason) joinedPlayers.Remove(sourcePlayer.Handle); - foreach (var player in Players) + foreach (Player notifPlayer in GetJoinQuitNotifPlayers()) { - if (IsPlayerAceAllowed(player.Handle, "vMenu.MiscSettings.JoinQuitNotifs") || - IsPlayerAceAllowed(player.Handle, "vMenu.MiscSettings.All")) - { - player.TriggerEvent("vMenu:PlayerJoinQuit", sourcePlayer.Name, reason); - } + notifPlayer.TriggerEvent("vMenu:PlayerJoinQuit", sourcePlayer.Name, reason); } } #endregion From 83410ab62cbc19cc639fd3dfc6f4a9f8e2486603 Mon Sep 17 00:00:00 2001 From: "Christopher M." Date: Sun, 19 Oct 2025 17:12:35 +1100 Subject: [PATCH 48/54] Refactor Save TP Locs server event --- vMenuServer/MainServer.cs | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/vMenuServer/MainServer.cs b/vMenuServer/MainServer.cs index 7630342d8..82052e3dd 100644 --- a/vMenuServer/MainServer.cs +++ b/vMenuServer/MainServer.cs @@ -1025,16 +1025,33 @@ private static void KickLog(string kickLogMesage) #region Add teleport location [EventHandler("vMenu:SaveTeleportLocation")] - internal void AddTeleportLocation([FromSource] Player _, string locationJson) + internal void AddTeleportLocation([FromSource] Player source, string locationJson) { - var location = JsonConvert.DeserializeObject(locationJson); - if (GetTeleportLocationsData().Any(loc => loc.name == location.name)) + if (!PermissionsManager.IsAllowed(PermissionsManager.Permission.MSTeleportSaveLocation, source) && !PermissionsManager.IsAllowed(PermissionsManager.Permission.MSAll, source)) + { + BanManager.BanCheater(source); + return; + } + + TeleportLocation teleportLocation; + + try + { + teleportLocation = JsonConvert.DeserializeObject(locationJson); + } + catch + { + Log("Teleport location could not be deserialized, location was not saved.", LogLevel.error); + return; + } + + if (GetTeleportLocationsData().Exists(loc => loc.name == teleportLocation.name)) { Log("A teleport location with this name already exists, location was not saved.", LogLevel.error); return; } var locs = GetLocations(); - locs.teleports.Add(location); + locs.teleports.Add(teleportLocation); if (!SaveResourceFile(GetCurrentResourceName(), "config/locations.json", JsonConvert.SerializeObject(locs, Formatting.Indented), -1)) { Log("Could not save locations.json file, reason unknown.", LogLevel.error); From 3f50f33edd4eeeb2f10d107555708bb6192ec99e Mon Sep 17 00:00:00 2001 From: "Christopher M." Date: Sun, 19 Oct 2025 20:38:16 +1100 Subject: [PATCH 49/54] Refactors Private Messages to need Permission & uses statebags --- SharedClasses/PermissionsManager.cs | 1 + vMenu/CommonFunctions.cs | 4 -- vMenu/menus/MiscSettings.cs | 17 ++++++++- vMenu/menus/OnlinePlayers.cs | 13 +++++-- vMenuServer/MainServer.cs | 58 +++++++++++++++++++---------- vMenuServer/config/permissions.cfg | 1 + 6 files changed, 67 insertions(+), 27 deletions(-) diff --git a/SharedClasses/PermissionsManager.cs b/SharedClasses/PermissionsManager.cs index 7c5efe7a5..2420e41da 100644 --- a/SharedClasses/PermissionsManager.cs +++ b/SharedClasses/PermissionsManager.cs @@ -28,6 +28,7 @@ public enum Permission OPTeleport, OPWaypoint, OPSpectate, + OPSendMessage, OPIdentifiers, OPSummon, OPKill, diff --git a/vMenu/CommonFunctions.cs b/vMenu/CommonFunctions.cs index c60d24dd8..8c940f6ff 100644 --- a/vMenu/CommonFunctions.cs +++ b/vMenu/CommonFunctions.cs @@ -3299,10 +3299,6 @@ public static async void PrivateMessage(string source, string message, bool sent if (MainMenu.MiscSettingsMenu == null || MainMenu.MiscSettingsMenu.MiscDisablePrivateMessages) { - if (!(sent && source == Game.Player.ServerId.ToString())) - { - TriggerServerEvent("vMenu:PmsDisabled", source); - } return; } diff --git a/vMenu/menus/MiscSettings.cs b/vMenu/menus/MiscSettings.cs index 66d9bebc1..953fe4d6e 100644 --- a/vMenu/menus/MiscSettings.cs +++ b/vMenu/menus/MiscSettings.cs @@ -50,7 +50,16 @@ public class MiscSettings public bool RestorePlayerWeapons { get; private set; } = UserDefaults.MiscRestorePlayerWeapons; public bool DrawTimeOnScreen { get; internal set; } = UserDefaults.MiscShowTime; public bool MiscRightAlignMenu { get; private set; } = UserDefaults.MiscRightAlignMenu; - public bool MiscDisablePrivateMessages { get; private set; } = UserDefaults.MiscDisablePrivateMessages; + private bool _disablePrivateMessages; + public bool MiscDisablePrivateMessages + { + get => _disablePrivateMessages; + set + { + _disablePrivateMessages = value; + Game.Player.State.Set("vmenu_pms_disabled", value, true); + } + } public bool MiscDisableControllerSupport { get; private set; } = UserDefaults.MiscDisableControllerSupport; internal bool TimecycleEnabled { get; private set; } = false; @@ -70,6 +79,12 @@ public class MiscSettings internal static List TpLocations = new(); + public MiscSettings() + { + // Sets statebag when resource starts + MiscDisablePrivateMessages = UserDefaults.MiscDisablePrivateMessages; + } + /// /// Creates the menu. /// diff --git a/vMenu/menus/OnlinePlayers.cs b/vMenu/menus/OnlinePlayers.cs index 048029dd8..37629b440 100644 --- a/vMenu/menus/OnlinePlayers.cs +++ b/vMenu/menus/OnlinePlayers.cs @@ -52,9 +52,10 @@ private void CreateMenu() var ban = new MenuItem("~r~Ban Player Permanently", "Ban this player permanently from the server. Are you sure you want to do this? You can specify the ban reason after clicking this button."); var tempban = new MenuItem("~r~Ban Player Temporarily", "Give this player a tempban of up to 30 days (max). You can specify duration and ban reason after clicking this button."); - // always allowed - playerMenu.AddMenuItem(sendMessage); - // permissions specific + if (IsAllowed(Permission.OPSendMessage)) + { + playerMenu.AddMenuItem(sendMessage); + } if (IsAllowed(Permission.OPTeleport)) { playerMenu.AddMenuItem(teleport); @@ -111,6 +112,12 @@ private void CreateMenu() // send message if (item == sendMessage) { + if (currentPlayer.Handle == Game.Player.Handle) + { + Notify.Error("You cannot message yourself!"); + return; + } + if (MainMenu.MiscSettingsMenu != null && !MainMenu.MiscSettingsMenu.MiscDisablePrivateMessages) { var message = await GetUserInput($"Private Message To {currentPlayer.Name}", 200); diff --git a/vMenuServer/MainServer.cs b/vMenuServer/MainServer.cs index 82052e3dd..baa8e9db0 100644 --- a/vMenuServer/MainServer.cs +++ b/vMenuServer/MainServer.cs @@ -969,31 +969,51 @@ internal async void SummonPlayer([FromSource] Player source, int target, int num } [EventHandler("vMenu:SendMessageToPlayer")] - internal void SendPrivateMessage([FromSource] Player source, int targetServerId, string message) + internal void SendPrivateMessage([FromSource] Player source, int target, string message) { - var targetPlayer = Players[targetServerId]; - if (targetPlayer != null) + if (!PermissionsManager.IsAllowed(PermissionsManager.Permission.OPSendMessage, source) && !PermissionsManager.IsAllowed(PermissionsManager.Permission.OPAll, source)) { - targetPlayer.TriggerEvent("vMenu:PrivateMessage", source.Handle, message); + BanManager.BanCheater(source); + return; + } + + bool sourcePmsDisabled = source.State.Get("vmenu_pms_disabled") ?? false; + + if (sourcePmsDisabled) + { + source.TriggerEvent("vMenu:Notify", "You can't send a private message if you have private messages disabled yourself. Enable them in the Misc Settings menu and try again."); + return; + } + + Player targetPlayer = GetPlayerFromServerId(target); + + if (targetPlayer is null) + { + source.TriggerEvent("vMenu:Notify", "Failed to send message because the target could not be found. Did they disconnect?"); + return; + } + + bool targetPmsDisabled = targetPlayer.State.Get("vmenu_pms_disabled") ?? false; + + if (targetPmsDisabled) + { + source.TriggerEvent("vMenu:Notify", $"Sorry, your private message to {source.Name}~s~ could not be delivered because they have private messages disabled."); + return; + } - foreach (var p in Players) + targetPlayer.TriggerEvent("vMenu:PrivateMessage", source.Handle, message); + + foreach (string playerHandle in joinedPlayers) + { + if (!PermissionsManager.IsAllowed(PermissionsManager.Permission.OPSeePrivateMessages, playerHandle) && !PermissionsManager.IsAllowed(PermissionsManager.Permission.OPAll, playerHandle)) { - if (p != source && p != targetPlayer) - { - if (vMenuShared.PermissionsManager.IsAllowed(vMenuShared.PermissionsManager.Permission.OPSeePrivateMessages, p)) - { - p.TriggerEvent("vMenu:Notify", $"[vMenu Staff Log] {source.Name}~s~ sent a PM to {targetPlayer.Name}~s~: {message}"); - } - } + continue; } - } - } - [EventHandler("vMenu:PmsDisabled")] - internal void NotifySenderThatDmsAreDisabled([FromSource] Player source, string senderServerId) - { - var p = Players[int.Parse(senderServerId)]; - p?.TriggerEvent("vMenu:Notify", $"Sorry, your private message to {source.Name}~s~ could not be delivered because they disabled private messages."); + Player player = GetPlayerFromServerId(playerHandle); + + player?.TriggerEvent("vMenu:Notify", $"[vMenu Staff Log] {source.Name}~s~ sent a PM to {targetPlayer.Name}~s~: {message}"); + } } #endregion diff --git a/vMenuServer/config/permissions.cfg b/vMenuServer/config/permissions.cfg index f651a3c6c..9fd8c6e1f 100644 --- a/vMenuServer/config/permissions.cfg +++ b/vMenuServer/config/permissions.cfg @@ -205,6 +205,7 @@ add_ace builtin.everyone "vMenu.OnlinePlayers.Menu" allow add_ace builtin.everyone "vMenu.OnlinePlayers.Teleport" allow add_ace builtin.everyone "vMenu.OnlinePlayers.Waypoint" allow add_ace builtin.everyone "vMenu.OnlinePlayers.Spectate" allow +add_ace builtin.everyone "vMenu.OnlinePlayers.SendMessage" allow # Moderators & admins only: add_ace group.moderator "vMenu.OnlinePlayers.Summon" allow From d8afc29ef5d1f3e79af0ff9f28cb2eb4fab5fa94 Mon Sep 17 00:00:00 2001 From: "Christopher M." Date: Sun, 19 Oct 2025 20:43:32 +1100 Subject: [PATCH 50/54] Refactors Get Player Coords server event --- vMenu/menus/OnlinePlayers.cs | 1 + vMenuServer/MainServer.cs | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/vMenu/menus/OnlinePlayers.cs b/vMenu/menus/OnlinePlayers.cs index 37629b440..b6cefae2b 100644 --- a/vMenu/menus/OnlinePlayers.cs +++ b/vMenu/menus/OnlinePlayers.cs @@ -252,6 +252,7 @@ private void CreateMenu() } else if (item == printIdentifiers) { + // TODO: Replace callback function Func CallbackFunction = (data) => { Debug.WriteLine(data); diff --git a/vMenuServer/MainServer.cs b/vMenuServer/MainServer.cs index baa8e9db0..cffafdced 100644 --- a/vMenuServer/MainServer.cs +++ b/vMenuServer/MainServer.cs @@ -1081,6 +1081,7 @@ internal void AddTeleportLocation([FromSource] Player source, string locationJso #endregion #region Infinity bits + // TODO: Replace this logic and all child logic with statebags (server set, client read) [EventHandler("vMenu:RequestPlayerList")] internal void RequestPlayerListFromPlayer([FromSource] Player player) { @@ -1095,11 +1096,22 @@ internal void RequestPlayerListFromPlayer([FromSource] Player player) internal void GetPlayerCoords([FromSource] Player source, long rpcId, int playerId, NetworkCallbackDelegate callback) { var coords = Vector3.Zero; - if (IsPlayerAceAllowed(source.Handle, "vMenu.OnlinePlayers.Teleport") || IsPlayerAceAllowed(source.Handle, "vMenu.Everything") || - IsPlayerAceAllowed(source.Handle, "vMenu.OnlinePlayers.All")) + + if (PermissionsManager.IsAllowed(PermissionsManager.Permission.OPTeleport, source) || PermissionsManager.IsAllowed(PermissionsManager.Permission.OPAll, source)) { - coords = Players[playerId]?.Character?.Position ?? Vector3.Zero; + Player targetPlayer = GetPlayerFromServerId(playerId); + + if (targetPlayer is not null) + { + Ped targetPed = targetPlayer.Character; + + if (targetPed is not null && DoesEntityExist(targetPed.Handle)) + { + coords = targetPed.Position; + } + } } + source.TriggerEvent("vMenu:GetPlayerCoords:reply", rpcId, coords); } #endregion From 1f98aad1d75caea581a8f7d53f2944866b4fc9e6 Mon Sep 17 00:00:00 2001 From: Christopher <10535902+cm8263@users.noreply.github.com> Date: Sat, 1 Nov 2025 16:01:20 +1100 Subject: [PATCH 51/54] Apply suggestions from code review --- vMenuServer/MainServer.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/vMenuServer/MainServer.cs b/vMenuServer/MainServer.cs index cffafdced..adbc08c5a 100644 --- a/vMenuServer/MainServer.cs +++ b/vMenuServer/MainServer.cs @@ -553,7 +553,6 @@ internal void GetOutOfCar([FromSource] Player source, int vehicleNetId) } int playerHandle = NetworkGetEntityOwner(pedHandle); - Player player = GetPlayerFromServerId(playerHandle); if (player is null || player == source) @@ -912,10 +911,9 @@ internal async void SummonPlayer([FromSource] Player source, int target, int num return; } + bool lastVehicle = false; Ped sourcePed = source.Character; int sourcePedHandle = sourcePed.Handle; - - bool lastVehicle = false; int sourcePedVehicle = GetVehiclePedIsIn(sourcePedHandle, lastVehicle); if (sourcePedVehicle == 0) @@ -938,15 +936,14 @@ internal async void SummonPlayer([FromSource] Player source, int target, int num continue; } + Vector3 checkPosition; + long timeout = GetGameTimer() + 1_500; Vector3 priorPosition = targetPed.Position; Vector3 newPosition = sourcePed.Position + new Vector3(0f, 0f, 5f); seatFound = true; targetPed.Position = newPosition; - Vector3 checkPosition; - long timeout = GetGameTimer() + 1_500; - while (timeout > GetGameTimer() && priorPosition.DistanceToSquared(checkPosition = targetPed.Position) < newPosition.DistanceToSquared(checkPosition)) { await Delay(100); @@ -1163,9 +1160,11 @@ internal void OnPlayerJoining([FromSource] Player sourcePlayer) PermissionsManager.SetPermissionsForPlayer(sourcePlayer); + string sourcePlayerName = sourcePlayer.Name; + foreach (Player notifPlayer in GetJoinQuitNotifPlayers()) { - notifPlayer.TriggerEvent("vMenu:PlayerJoinQuit", sourcePlayer.Name, null); + notifPlayer.TriggerEvent("vMenu:PlayerJoinQuit", sourcePlayerName, null); } } @@ -1179,9 +1178,11 @@ internal void OnPlayerDropped([FromSource] Player sourcePlayer, string reason) joinedPlayers.Remove(sourcePlayer.Handle); + string sourcePlayerName = sourcePlayer.Name; + foreach (Player notifPlayer in GetJoinQuitNotifPlayers()) { - notifPlayer.TriggerEvent("vMenu:PlayerJoinQuit", sourcePlayer.Name, reason); + notifPlayer.TriggerEvent("vMenu:PlayerJoinQuit", sourcePlayerName, reason); } } #endregion From af0e3661d4084767cfce65c4d9df7ab0f9e18f56 Mon Sep 17 00:00:00 2001 From: TheIndra55 Date: Mon, 3 Nov 2025 22:48:33 +0100 Subject: [PATCH 52/54] Remove unused server information convars setting --- SharedClasses/ConfigManager.cs | 1 - vMenuServer/config/permissions.cfg | 3 --- 2 files changed, 4 deletions(-) diff --git a/SharedClasses/ConfigManager.cs b/SharedClasses/ConfigManager.cs index 2d5ee3f71..c8b07fe7a 100644 --- a/SharedClasses/ConfigManager.cs +++ b/SharedClasses/ConfigManager.cs @@ -30,7 +30,6 @@ public enum Setting vmenu_enable_animals_spawn_menu, vmenu_pvp_mode, keep_player_head_props, - vmenu_disable_server_info_convars, vmenu_player_names_distance, vmenu_disable_entity_outlines_tool, vmenu_disable_player_stats_setup, diff --git a/vMenuServer/config/permissions.cfg b/vMenuServer/config/permissions.cfg index 9fd8c6e1f..303848d28 100644 --- a/vMenuServer/config/permissions.cfg +++ b/vMenuServer/config/permissions.cfg @@ -80,9 +80,6 @@ setr vmenu_pvp_mode 0 # false = vMenu will not touch this feature, by default this means that head props will fall off when the player is hit, which is the default behavior for GTA V Single Player peds. setr keep_player_head_props true -# Set this to true if you don't want vMenu to use any server information convars. -setr vmenu_disable_server_info_convars false - # Distance for playerblips to showup. This is using "game units" as measurement. It's unknown # what this is in relation to meters or something similar, but 500.0 seems fine in most cases. setr vmenu_player_names_distance 500.0 From 74a748bb6ebd16604fb259728a0e62f89e78b738 Mon Sep 17 00:00:00 2001 From: "Christopher M." Date: Sun, 9 Nov 2025 11:09:12 +1100 Subject: [PATCH 53/54] Adds "Customize Colors" menu w/ primary, secondary, preset, and chrome --- vMenu/menus/VehicleOptions.cs | 107 +++++++++++++++++++--------------- 1 file changed, 59 insertions(+), 48 deletions(-) diff --git a/vMenu/menus/VehicleOptions.cs b/vMenu/menus/VehicleOptions.cs index d10a1a7a9..f4c28ac56 100644 --- a/vMenu/menus/VehicleOptions.cs +++ b/vMenu/menus/VehicleOptions.cs @@ -1028,29 +1028,35 @@ private void CreateMenu() #endregion #region Vehicle Colors Submenu Stuff - // presets menu - var presetColorsMenu = new Menu("Vehicle Colors", "Preset Colors"); - MenuController.AddSubmenu(VehicleColorsMenu, presetColorsMenu); + // color customization menu + var customizeColorMenu = new Menu("Vehicle Colors", "Customize Colors"); + MenuController.AddSubmenu(VehicleColorsMenu, customizeColorMenu); - var presetColorsBtn = new MenuItem("Preset Colors") { Label = "→→→" }; - VehicleColorsMenu.AddMenuItem(presetColorsBtn); - MenuController.BindMenuItem(VehicleColorsMenu, presetColorsMenu, presetColorsBtn); + var colorsCustomizationBtn = new MenuItem("Customize Colors") { Label = "→→→" }; + VehicleColorsMenu.AddMenuItem(colorsCustomizationBtn); + MenuController.BindMenuItem(VehicleColorsMenu, customizeColorMenu, colorsCustomizationBtn); // primary menu var primaryColorsMenu = new Menu("Vehicle Colors", "Primary Colors"); - MenuController.AddSubmenu(VehicleColorsMenu, primaryColorsMenu); + MenuController.AddSubmenu(customizeColorMenu, primaryColorsMenu); var primaryColorsBtn = new MenuItem("Primary Color") { Label = "→→→" }; - VehicleColorsMenu.AddMenuItem(primaryColorsBtn); - MenuController.BindMenuItem(VehicleColorsMenu, primaryColorsMenu, primaryColorsBtn); + customizeColorMenu.AddMenuItem(primaryColorsBtn); + MenuController.BindMenuItem(customizeColorMenu, primaryColorsMenu, primaryColorsBtn); // secondary menu var secondaryColorsMenu = new Menu("Vehicle Colors", "Secondary Colors"); - MenuController.AddSubmenu(VehicleColorsMenu, secondaryColorsMenu); + MenuController.AddSubmenu(customizeColorMenu, secondaryColorsMenu); var secondaryColorsBtn = new MenuItem("Secondary Color") { Label = "→→→" }; - VehicleColorsMenu.AddMenuItem(secondaryColorsBtn); - MenuController.BindMenuItem(VehicleColorsMenu, secondaryColorsMenu, secondaryColorsBtn); + customizeColorMenu.AddMenuItem(secondaryColorsBtn); + MenuController.BindMenuItem(customizeColorMenu, secondaryColorsMenu, secondaryColorsBtn); + + var presetColorsBtn = new MenuListItem("Preset Colors", [], 0); + customizeColorMenu.AddMenuItem(presetColorsBtn); + + var chrome = new MenuItem("Chrome"); + customizeColorMenu.AddMenuItem(chrome); // color lists var classic = new List(); @@ -1116,25 +1122,8 @@ private void CreateMenu() var intColorList = new MenuListItem("Interior / Trim Color", classic, 0); var vehicleEnveffScale = new MenuSliderItem("Vehicle Enveff Scale", "This works on certain vehicles only, like the besra for example. It 'fades' certain paint layers.", 0, 20, 10, true); - var chrome = new MenuItem("Chrome"); - VehicleColorsMenu.AddMenuItem(chrome); VehicleColorsMenu.AddMenuItem(vehicleEnveffScale); - VehicleColorsMenu.OnItemSelect += (sender, item, index) => - { - var veh = GetVehicle(); - if (veh != null && veh.Exists() && !veh.IsDead && veh.Driver == Game.PlayerPed) - { - if (item == chrome) - { - SetVehicleColours(veh.Handle, 120, 120); // chrome is index 120 - } - } - else - { - Notify.Error("You need to be the driver of a driveable vehicle to change this."); - } - }; VehicleColorsMenu.OnSliderPositionChange += (m, sliderItem, oldPosition, newPosition, itemIndex) => { var veh = GetVehicle(); @@ -1362,32 +1351,54 @@ async void HandleItemSelect(Menu menu, MenuItem menuItem, int itemIndex) } } - VehicleColorsMenu.OnItemSelect += (sender, item, index) => + customizeColorMenu.OnMenuOpen += (_) => { - // When the color presets submenu is openend, update preset color items. - if (item == presetColorsBtn) + int numVehColors = GetNumberOfVehicleColours(GetVehicle().Handle); + + if (numVehColors == 0) { - if (Game.PlayerPed.IsInVehicle()) - { - presetColorsMenu.ClearMenuItems(); - var veh = GetVehicle(); - var VehicleColorCombinationsCount = GetNumberOfVehicleColours(veh.Handle); + presetColorsBtn.Enabled = false; + presetColorsBtn.ListItems = ["No Preset Colors"]; + presetColorsBtn.ListIndex = 0; + return; + } - for (int i = 0; i < VehicleColorCombinationsCount; i++) - { - var presetColor = new MenuItem($"Preset {i + 1}"); - presetColorsMenu.AddMenuItem(presetColor); - } - } - else + List colorOptions = []; + + presetColorsBtn.Enabled = true; + + for (int i = 0; i < numVehColors; i++) + { + colorOptions.Add($"Preset Color #{i + 1}"); + } + + int currentColor = GetVehicleColourCombination(GetVehicle().Handle); + + presetColorsBtn.ListItems = colorOptions; + presetColorsBtn.ListIndex = currentColor < 0 ? 0 : currentColor; + }; + + customizeColorMenu.OnItemSelect += (_, item, _) => + { + var veh = GetVehicle(); + if (veh != null && veh.Exists() && !veh.IsDead && veh.Driver == Game.PlayerPed) + { + if (item == chrome) { - VehicleColorsMenu.CloseMenu(); - menu.OpenMenu(); + SetVehicleColours(veh.Handle, 120, 120); // chrome is index 120 } } + else + { + Notify.Error("You need to be the driver of a driveable vehicle to change this."); + } }; - presetColorsMenu.OnItemSelect += (sender, item, index) => + customizeColorMenu.OnListItemSelect += (_, _, index, _) => ChangeVehiclePresetColor(index); + + customizeColorMenu.OnListIndexChange += (_, _, _, index, _) => ChangeVehiclePresetColor(index); + + void ChangeVehiclePresetColor(int index) { var veh = GetVehicle(); if (veh != null && veh.Exists() && !veh.IsDead && veh.Driver == Game.PlayerPed) @@ -1398,7 +1409,7 @@ async void HandleItemSelect(Menu menu, MenuItem menuItem, int itemIndex) { Notify.Error("You need to be the driver of a driveable vehicle to change this."); } - }; + } #endregion #region Vehicle Doors Submenu Stuff From 430d47c4d98dd719418dc3897d9c06e207477d57 Mon Sep 17 00:00:00 2001 From: "Christopher M." Date: Sun, 9 Nov 2025 11:15:23 +1100 Subject: [PATCH 54/54] Fixes tiny typo causing not so tiny issues... --- vMenuServer/MainServer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vMenuServer/MainServer.cs b/vMenuServer/MainServer.cs index adbc08c5a..284192509 100644 --- a/vMenuServer/MainServer.cs +++ b/vMenuServer/MainServer.cs @@ -831,7 +831,7 @@ internal void FreezeServerTime([FromSource] Player source, bool freezeTime) [EventHandler("vMenu:KickPlayer")] internal void KickPlayer([FromSource] Player source, int target, string kickReason = "You have been kicked from the server.") { - if (!PermissionsManager.IsAllowed(PermissionsManager.Permission.OPKick, source) && !PermissionsManager.IsAllowed(PermissionsManager.Permission.POAll, source)) + if (!PermissionsManager.IsAllowed(PermissionsManager.Permission.OPKick, source) && !PermissionsManager.IsAllowed(PermissionsManager.Permission.OPAll, source)) { BanManager.BanCheater(source); return;