diff --git a/SharedClasses/ConfigManager.cs b/SharedClasses/ConfigManager.cs index 26a0693d7..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, @@ -38,6 +37,11 @@ public enum Setting // Vehicle Chameleon Colours vmenu_using_chameleon_colours, + // Prevent Extras Abuse + vmenu_prevent_extras_when_damaged, + 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/SharedClasses/PermissionsManager.cs b/SharedClasses/PermissionsManager.cs index a076bd751..2420e41da 100644 --- a/SharedClasses/PermissionsManager.cs +++ b/SharedClasses/PermissionsManager.cs @@ -28,6 +28,7 @@ public enum Permission OPTeleport, OPWaypoint, OPSpectate, + OPSendMessage, OPIdentifiers, OPSummon, OPKill, @@ -105,6 +106,7 @@ public enum Permission VOInfiniteFuel, VOFlares, VOPlaneBombs, + VOBypassExtraDamage, #endregion // Vehicle Spawner @@ -384,9 +386,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 @@ -455,11 +464,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/vMenu/CommonFunctions.cs b/vMenu/CommonFunctions.cs index c4d6d6d0c..8c940f6ff 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; } @@ -961,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 @@ -1529,7 +1540,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); } @@ -1569,17 +1580,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); @@ -2024,9 +2050,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. @@ -2041,8 +2070,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; @@ -2054,8 +2082,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 @@ -3265,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; } @@ -3456,5 +3486,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/EventManager.cs b/vMenu/EventManager.cs index c0cc95f50..909516b6c 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); /// @@ -40,16 +40,14 @@ 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)); 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)); EventHandlers.Add("vMenu:PrivateMessage", new Action(PrivateMessage)); EventHandlers.Add("vMenu:UpdateTeleportLocations", new Action(UpdateTeleportLocations)); @@ -284,11 +282,6 @@ private async Task UpdateWeatherParticles() /// private async Task WeatherSync() { - if (MainMenu.WeatherOptionsMenu == null) - { - return; - } - await UpdateWeatherParticles(); SetArtificialLightsState(IsBlackoutEnabled); SetArtificialLightsStateAffectsVehicles(!IsVehicleLightsEnabled); @@ -358,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. @@ -383,67 +358,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); - } - - /// - /// 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); - } - } - } - } - } - } + private void ClearAreaNearPos(Vector3 position) => ClearAreaOfEverything(position.X, position.Y, position.Z, 100f, false, false, false, false); /// /// Updates ped decorators for the clothing animation when players have joined. diff --git a/vMenu/MainMenu.cs b/vMenu/MainMenu.cs index fcc5d037d..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,55 +326,59 @@ 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) { 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; } + } + + private static Dictionary rpcQueue = new Dictionary(); + private static long rpcIdCounter = 0; - // TODO: replace with client<->server RPC once implemented in CitizenFX! - Func CallbackFunction = (data) => + [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 @@ -520,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"); } @@ -900,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/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/MiscSettings.cs b/vMenu/menus/MiscSettings.cs index ec9629214..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. /// @@ -469,8 +484,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/vMenu/menus/MpPedCustomization.cs b/vMenu/menus/MpPedCustomization.cs index 3396a33ec..61e55e77e 100644 --- a/vMenu/menus/MpPedCustomization.cs +++ b/vMenu/menus/MpPedCustomization.cs @@ -121,7 +121,8 @@ public class MpPedCustomization private float _shapeMixValue; private float _skinMixValue; private readonly Dictionary shapeFaceValues = []; - private readonly Dictionary> apperanceValues = []; + // 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; private int _hairHighlightColorSelection; @@ -151,8 +152,34 @@ 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(); } + else + { + PedHeadBlendData headBlendData = currentCharacter.PedHeadBlendData; + + _dadSelection = headBlendData.FirstFaceShape; + _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>(); currentCharacter.PropVariations.props ??= new Dictionary>(); @@ -179,6 +206,43 @@ private void MakeCreateCharacterMenu(bool male, bool editPed = false) propsMenu.ClearMenuItems(); #region appearance menu. + 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++) + { + appearanceValues[i] = new Tuple(0, 0, 0f); + } + } + else + { + 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%" }; var maxHairStyles = GetNumberOfPedDrawableVariations(Game.PlayerPed.Handle, 2); @@ -704,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(); @@ -1009,8 +1084,8 @@ 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.", 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); @@ -1056,8 +1131,20 @@ int UnclampMix(float value) inheritanceMenu.OnSliderPositionChange += (sender, item, oldPosition, newPosition, itemIndex) => { - _shapeMixValue = inheritanceShapeMix.Position; - _skinMixValue = 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(); }; @@ -1761,8 +1848,8 @@ void ApplySavedTattoos() { _dadSelection = _random.Next(parents.Count); _mumSelection = _random.Next(parents.Count); - _skinMixValue = _random.Next(2, 8); - _shapeMixValue = _random.Next(2, 8); + _skinMixValue = (float)_random.NextDouble(); + _shapeMixValue = (float)_random.NextDouble(); SetHeadBlend(); @@ -1805,6 +1892,7 @@ void ApplySavedTattoos() case 1: if (!currentCharacter.IsMale) { + appearanceValues[i] = new Tuple(0, 0, 0f); continue; } @@ -1837,6 +1925,7 @@ void ApplySavedTattoos() case 8: if (currentCharacter.IsMale) { + appearanceValues[i] = new Tuple(0, 0, 0f); continue; } @@ -1859,6 +1948,7 @@ void ApplySavedTattoos() case 10: if (!currentCharacter.IsMale) { + appearanceValues[i] = new Tuple(0, 0, 0f); continue; } @@ -1879,16 +1969,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); } } @@ -1966,8 +2056,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) @@ -1986,56 +2076,59 @@ 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; - ((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 = apperanceValues[0].Item1; - ((MenuListItem)items[4]).ListIndex = (int)(apperanceValues[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 = apperanceValues[1].Item1; - ((MenuListItem)items[6]).ListIndex = (int)(apperanceValues[1].Item3 * 10); - ((MenuListItem)items[7]).ListIndex = apperanceValues[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 = apperanceValues[2].Item1; - ((MenuListItem)items[9]).ListIndex = (int)(apperanceValues[2].Item3 * 10); - ((MenuListItem)items[10]).ListIndex = apperanceValues[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 = apperanceValues[3].Item1; - ((MenuListItem)items[12]).ListIndex = (int)(apperanceValues[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 = apperanceValues[4].Item1; - ((MenuListItem)items[14]).ListIndex = (int)(apperanceValues[4].Item3 * 10); - ((MenuListItem)items[15]).ListIndex = apperanceValues[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 = apperanceValues[5].Item1; - ((MenuListItem)items[17]).ListIndex = (int)(apperanceValues[5].Item3 * 10); - ((MenuListItem)items[18]).ListIndex = apperanceValues[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 = apperanceValues[6].Item1; - ((MenuListItem)items[20]).ListIndex = (int)(apperanceValues[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 = apperanceValues[7].Item1; - ((MenuListItem)items[22]).ListIndex = (int)(apperanceValues[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 = apperanceValues[8].Item1; - ((MenuListItem)items[24]).ListIndex = (int)(apperanceValues[8].Item3 * 10); - ((MenuListItem)items[25]).ListIndex = apperanceValues[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 = apperanceValues[9].Item1; - ((MenuListItem)items[27]).ListIndex = (int)(apperanceValues[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 = apperanceValues[10].Item1; - ((MenuListItem)items[29]).ListIndex = (int)(apperanceValues[10].Item3 * 10); - ((MenuListItem)items[30]).ListIndex = apperanceValues[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 = apperanceValues[11].Item1; - ((MenuListItem)items[32]).ListIndex = (int)(apperanceValues[11].Item3 * 10); + menuListItems.First(i => i.Text == "Eye Colors").ListIndex = _eyeColorSelection; appearanceMenu.RefreshIndex(); + + SetHeadBlend(); } }; @@ -2072,9 +2165,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); } @@ -2108,9 +2200,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); } @@ -3093,6 +3184,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. /// diff --git a/vMenu/menus/OnlinePlayers.cs b/vMenu/menus/OnlinePlayers.cs index 048029dd8..b6cefae2b 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); @@ -245,6 +252,7 @@ private void CreateMenu() } else if (item == printIdentifiers) { + // TODO: Replace callback function Func CallbackFunction = (data) => { Debug.WriteLine(data); 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/vMenu/menus/SavedVehicles.cs b/vMenu/menus/SavedVehicles.cs index ad951a3fc..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() + ")"; }; @@ -499,7 +527,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."); } } 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/vMenu/menus/VehicleOptions.cs b/vMenu/menus/VehicleOptions.cs index 19f29b6e0..2d35de4e1 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,21 +1028,35 @@ private void CreateMenu() #endregion #region Vehicle Colors Submenu Stuff + // color customization menu + var customizeColorMenu = new Menu("Vehicle Colors", "Customize Colors"); + MenuController.AddSubmenu(VehicleColorsMenu, customizeColorMenu); + + 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(); @@ -1108,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(); @@ -1309,7 +1306,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 +1350,66 @@ async void HandleItemSelect(Menu menu, MenuItem menuItem, int itemIndex) secondaryColorsMenu.OnItemSelect += HandleItemSelect; } } + + customizeColorMenu.OnMenuOpen += (_) => + { + int numVehColors = GetNumberOfVehicleColours(GetVehicle().Handle); + + if (numVehColors == 0) + { + presetColorsBtn.Enabled = false; + presetColorsBtn.ListItems = ["No Preset Colors"]; + presetColorsBtn.ListIndex = 0; + return; + } + + 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) + { + 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."); + } + }; + + 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) + { + 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 @@ -1694,6 +1751,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) && !IsAllowed(Permission.VOBypassExtraDamage); + + 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,6 +1805,30 @@ async void HandleItemSelect(Menu menu, MenuItem menuItem, int itemIndex) if (vehicleExtras.TryGetValue(item, out var extra)) { var veh = GetVehicle(); + + if (!Entity.Exists(veh)) + { + Notify.Error(CommonErrors.NoVehicle); + return; + } + + bool checkDamageBeforeChangingExtras = GetSettingsBool(Setting.vmenu_prevent_extras_when_damaged) && !IsAllowed(Permission.VOBypassExtraDamage); + + 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); + + // Send to previous menu + VehicleComponentsMenu.GoBack(); + return; + } + } + veh.ToggleExtra(extra, _checked); } }; @@ -1946,7 +2073,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) @@ -2426,5 +2553,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/vMenu/menus/WeatherOptions.cs b/vMenu/menus/WeatherOptions.cs index 53598b4b3..b742ec5e6 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,28 @@ 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) { + 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.IsBlackoutEnabled, EventManager.DynamicWeatherEnabled, _checked); + UpdateServerWeather(EventManager.GetServerWeather, EventManager.DynamicWeatherEnabled, _checked); } }; } diff --git a/vMenuServer/MainServer.cs b/vMenuServer/MainServer.cs index d2bbde98a..284192509 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; @@ -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); } @@ -217,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") ?? "{}"; @@ -262,6 +265,8 @@ public MainServer() { Tick += TimeLoop; } + + GlobalState.Set("vmenu_onesync", GetConvar("onesync", "off") == "on", true); } } #endregion @@ -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 @@ -520,17 +525,46 @@ 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)) + { + 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++) { - if (vMenuShared.PermissionsManager.GetPermissionAndParentPermissions(vMenuShared.PermissionsManager.Permission.PVKickPassengers).Any(perm => vMenuShared.PermissionsManager.IsAllowed(perm, source))) + int pedHandle = GetPedInVehicleSeat(vehicleHandle, i); + + if (pedHandle == 0 || !IsPedAPlayer(pedHandle)) { - 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 playerHandle = NetworkGetEntityOwner(pedHandle); + Player player = GetPlayerFromServerId(playerHandle); + + if (player is null || player == source) + { + continue; + } + + int warpOutFlag = 16; + + TaskLeaveVehicle(pedHandle, vehicleHandle, warpOutFlag); + + player.TriggerEvent("vMenu:Notify", "The owner of the vehicle has kicked you out."); } } #endregion @@ -539,13 +573,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 @@ -669,11 +703,15 @@ private void RefreshWeather() /// Update the weather for all clients. /// /// - /// /// [EventHandler("vMenu:UpdateServerWeather")] - internal void UpdateWeather(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)) + { + 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") @@ -683,7 +721,6 @@ internal void UpdateWeather(string newWeather, bool blackoutNew, bool dynamicWea // Update the new weather related variables. CurrentWeather = newWeather; - BlackoutEnabled = blackoutNew; DynamicWeatherEnabled = dynamicWeatherNew; ManualSnowEnabled = enableSnow; @@ -691,19 +728,57 @@ internal void UpdateWeather(string newWeather, bool blackoutNew, bool dynamicWea 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. /// /// [EventHandler("vMenu:UpdateServerWeatherCloudsType")] - internal void UpdateWeatherCloudsType(bool removeClouds) + internal void UpdateWeatherCloudsType([FromSource] Player source, bool removeClouds) { + 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); @@ -715,13 +790,34 @@ 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) { + if (!PermissionsManager.IsAllowed(PermissionsManager.Permission.TOSetTime, source) && !PermissionsManager.IsAllowed(PermissionsManager.Permission.TOAll, source)) + { + BanManager.BanCheater(source); + return; + } + CurrentHours = newHours; CurrentMinutes = newMinutes; - FreezeTime = freezeTimeNew; + } + + /// + /// 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)) + { + BanManager.BanCheater(source); + return; + } + + FreezeTime = freezeTime; } #endregion @@ -735,34 +831,31 @@ internal void UpdateTime(int newHours, int newMinutes, bool freezeTimeNew) [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.OPAll, 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); } /// @@ -773,22 +866,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); } /// @@ -797,52 +888,129 @@ 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; + } + + bool lastVehicle = false; + Ped sourcePed = source.Character; + int sourcePedHandle = sourcePed.Handle; + 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; + } + + 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; + + 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; } - TriggerClientEvent(player: source, eventName: "vMenu:Notify", args: "An unknown error occurred. Report it here: vespura.com/vmenu"); + + SetPedIntoVehicle(targetPedHandle, sourcePedVehicle, i); + break; } - else + + if (!seatFound) { - BanManager.BanCheater(source); + source.TriggerEvent("vMenu:Notify", "No free seats in your vehicle for summoned player."); } } [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)) + { + BanManager.BanCheater(source); + return; + } + + bool sourcePmsDisabled = source.State.Get("vmenu_pms_disabled") ?? false; + + if (sourcePmsDisabled) { - targetPlayer.TriggerEvent("vMenu:PrivateMessage", source.Handle, message); + 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; + } - foreach (var p in Players) + 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; + } + + 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 @@ -874,16 +1042,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); @@ -893,14 +1078,7 @@ 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" - }); - } - + // TODO: Replace this logic and all child logic with statebags (server set, client read) [EventHandler("vMenu:RequestPlayerList")] internal void RequestPlayerListFromPlayer([FromSource] Player player) { @@ -912,35 +1090,67 @@ 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) { - if (IsPlayerAceAllowed(source.Handle, "vMenu.OnlinePlayers.Teleport") || IsPlayerAceAllowed(source.Handle, "vMenu.Everything") || - IsPlayerAceAllowed(source.Handle, "vMenu.OnlinePlayers.All")) + var coords = Vector3.Zero; + + if (PermissionsManager.IsAllowed(PermissionsManager.Permission.OPTeleport, source) || PermissionsManager.IsAllowed(PermissionsManager.Permission.OPAll, source)) { - var coords = Players[playerId]?.Character?.Position ?? Vector3.Zero; + Player targetPlayer = GetPlayerFromServerId(playerId); - _ = callback(coords); + if (targetPlayer is not null) + { + Ped targetPed = targetPlayer.Character; - return; + if (targetPed is not null && DoesEntityExist(targetPed.Handle)) + { + coords = targetPed.Position; + } + } } - _ = callback(Vector3.Zero); + source.TriggerEvent("vMenu:GetPlayerCoords:reply", rpcId, coords); } #endregion #region Player join/quit private readonly HashSet joinedPlayers = new(); - private Task PlayersFirstTick() + 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(3_000); + foreach (var player in Players) { joinedPlayers.Add(player.Handle); - } - return Task.FromResult(0); + PermissionsManager.SetPermissionsForPlayer(player); + } } [EventHandler("playerJoining")] @@ -948,13 +1158,13 @@ internal void OnPlayerJoining([FromSource] Player sourcePlayer) { joinedPlayers.Add(sourcePlayer.Handle); - foreach (var player in Players) + PermissionsManager.SetPermissionsForPlayer(sourcePlayer); + + string sourcePlayerName = sourcePlayer.Name; + + 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", sourcePlayerName, null); } } @@ -968,15 +1178,37 @@ internal void OnPlayerDropped([FromSource] Player sourcePlayer, string reason) joinedPlayers.Remove(sourcePlayer.Handle); - foreach (var player in Players) + string sourcePlayerName = sourcePlayer.Name; + + 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", sourcePlayerName, 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 } } diff --git a/vMenuServer/config/permissions.cfg b/vMenuServer/config/permissions.cfg index 960eb4ae4..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 @@ -133,6 +130,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_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_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. @@ -199,6 +202,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 @@ -285,6 +289,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 # 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