From 0ad4a16f0a55b8aa8e340f502d7ecc8b244940f2 Mon Sep 17 00:00:00 2001 From: Explooosion-code <61145047+Explooosion-code@users.noreply.github.com> Date: Thu, 25 Mar 2021 10:48:26 +0100 Subject: [PATCH 1/3] Added EntitySpawner to developer tools I really missed a feature like this to set up scenes, or spawn vehicles/peds for specific needs. Its now possible with this feature. --- SharedClasses/PermissionsManager.cs | 1 + vMenu/EntitySpawner.cs | 276 ++++++++++++++++++++++++++++ vMenu/menus/MiscSettings.cs | 52 +++++- vMenuServer/config/permissions.cfg | 1 + 4 files changed, 329 insertions(+), 1 deletion(-) create mode 100644 vMenu/EntitySpawner.cs diff --git a/SharedClasses/PermissionsManager.cs b/SharedClasses/PermissionsManager.cs index edf760d2..c27de37a 100644 --- a/SharedClasses/PermissionsManager.cs +++ b/SharedClasses/PermissionsManager.cs @@ -325,6 +325,7 @@ public enum Permission MSRestoreAppearance, MSRestoreWeapons, MSDriftMode, + MSEntitySpawner, #endregion // Voice Chat diff --git a/vMenu/EntitySpawner.cs b/vMenu/EntitySpawner.cs new file mode 100644 index 00000000..a928b0eb --- /dev/null +++ b/vMenu/EntitySpawner.cs @@ -0,0 +1,276 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Threading.Tasks; +using CitizenFX.Core; +using static CitizenFX.Core.Native.API; + +namespace vMenuClient +{ + public class EntitySpawner : BaseScript + { + public static bool Active { get; private set; } = false; + public static Entity CurrentEntity { get; private set; } = null; + private int scaleform = 0; + private float rotateSpeed = 20f; + + private const float RayDistance = 25f; + + /// + /// Constructor. + /// + + public EntitySpawner() + { + #if DEBUG + RegisterCommand("testEntity", new Action>((source, args) => + { + string prop = (string)args[0]; + SpawnEntity(prop, Game.PlayerPed.Position); + }), false); + + RegisterCommand("endTest", new Action(() => + { + FinishPlacement(); + }), false); + #endif + } + + #region PublicMethods + + /// + /// Method for spawning entity with EntitySpawner. After entity is spawned you will be able to change + /// position of entity with your mouse. + /// + /// model of entity as string + /// initial coords for the entity + /// true spawn was succesful + + public static void SpawnEntity(string model, Vector3 coords) + { + SpawnEntity((uint) GetHashKey(model), coords); + } + + /// + /// Method for spawning entity with EntitySpawner. After entity is spawned you will be able to change + /// position of entity with your mouse. + /// + /// model of entity as hash + /// initial coords for the entity + /// true spawn was succesful + public static async void SpawnEntity(uint model, Vector3 coords) + { + if (!IsModelValid(model)) + { + Notify.Error(CommonErrors.InvalidInput); + return; + } + + if (CurrentEntity != null) + { + Notify.Error("One entity is currently beeing processed"); + return; + } + + int handle = 0; + RequestModel(model); + while (!HasModelLoaded(model)) + { + await Delay(1); + } + if (IsModelAPed(model)) + { + handle = CreatePed(4, model, coords.X, coords.Y, coords.Z, Game.PlayerPed.Heading, true, true); + } else if (IsModelAVehicle(model)) + { + int modelClass = GetVehicleClassFromName(model); + if (!VehicleSpawner.allowedCategories[modelClass]) + { + Notify.Alert("You are not allowed to spawn this vehicle, because it belongs to a category which is restricted by the server owner."); + return; + } + handle = CreateVehicle(model, coords.X, coords.Y, coords.Z, Game.PlayerPed.Heading, true, true); + } + else + { + handle = CreateObject((int)model, coords.X, coords.Y, coords.Z, true, true, true); + } + + SetEntityAsMissionEntity(handle, true, true); // Set As mission to prevent despawning + + CurrentEntity = Entity.FromHandle(handle); + + if (!CurrentEntity.Exists()) + throw new Exception("Failed to create entity"); + + + if (!Active) + Active = true; + } + + /// + /// Method used to confirm location of prop and finish placement + /// + + public static void FinishPlacement() + { + Active = false; + CurrentEntity = null; + } + + #endregion + + #region InternalMethods + + /// + /// Used internally for drawing of help text + /// + private void DrawButtons() //TODO: Right keys + { + BeginScaleformMovieMethod(scaleform, "CLEAR_ALL"); + EndScaleformMovieMethod(); + + BeginScaleformMovieMethod(scaleform, "SET_DATA_SLOT"); + ScaleformMovieMethodAddParamInt(0); + PushScaleformMovieMethodParameterString("~INPUT_VEH_FLY_ROLL_LR~"); + PushScaleformMovieMethodParameterString("Rotate Object"); + EndScaleformMovieMethod(); + + BeginScaleformMovieMethod(scaleform, "DRAW_INSTRUCTIONAL_BUTTONS"); + ScaleformMovieMethodAddParamInt(0); + EndScaleformMovieMethod(); + + DrawScaleformMovieFullscreen(scaleform, 255, 255, 255, 255, 0); + } + + /// + /// Used internally for getting direction vector from rotation vector + /// + /// Input rotation vector + /// Output direction vector + + private Vector3 RotationToDirection(Vector3 rotation) + { + Vector3 adj = new Vector3( + (float)Math.PI / 180f * rotation.X, + (float)Math.PI / 180f * rotation.Y, + (float)Math.PI / 180f * rotation.Z + ); + + return new Vector3( + (float)(-Math.Sin(adj.Z) * Math.Abs(Math.Cos(adj.X))), + (float)(Math.Cos(adj.Z) * Math.Abs(Math.Cos(adj.X))), + (float)Math.Sin(adj.X) + ); + } + + /// + /// Used to get coords of reycast from player camera; + /// + /// destination if no hit was found and coords of hit if there was one + + private Vector3 GetCoordsPlayerIsLookingAt() + { + Vector3 camRotation = GetGameplayCamRot(0); + Vector3 camCoords = GetGameplayCamCoord(); + Vector3 camDirection = RotationToDirection(camRotation); + + Vector3 dest = new Vector3( + camCoords.X + camDirection.X * RayDistance, + camCoords.Y + camDirection.Y * RayDistance, + camCoords.Z + camDirection.Z * RayDistance + ); + + RaycastResult res = World.Raycast(camCoords, dest, IntersectOptions.Everything, Game.PlayerPed); + +#if DEBUG + DrawLine(Game.PlayerPed.Position.X, Game.PlayerPed.Position.Y,Game.PlayerPed.Position.Z, dest.X, dest.Y, dest.Z, 255, 0, 0, 255); +#endif + + return res.DitHit ? res.HitPosition : dest; + } + + #endregion + + /// + /// Main tick method for class + /// + [Tick] + private async Task MoveHandler() + { + if (Active) + { + scaleform = RequestScaleformMovie("INSTRUCTIONAL_BUTTONS"); + while (!HasScaleformMovieLoaded(scaleform)) + { + await Delay(0); + } + } + else + { + if (scaleform != 0) + { + SetScaleformMovieAsNoLongerNeeded(ref scaleform); //Unload scaleform if there is no need to draw it + scaleform = 0; + } + } + + float headingOffset = 0f; + while (Active) + { + int handle = CurrentEntity.Handle; + if (CurrentEntity == null || !CurrentEntity.Exists()) + { + Active = false; + CurrentEntity = null; + break; + } + + DrawButtons(); + + FreezeEntityPosition(handle, true); + SetEntityInvincible(handle, true); + SetEntityCollision(handle, false, false); + SetEntityAlpha(handle, (int)(255 * 0.4), 0); + CurrentEntity.Heading = (GetGameplayCamRot(0).Z + headingOffset) % 360f; + + Vector3 newPosition = GetCoordsPlayerIsLookingAt(); + + CurrentEntity.Position = newPosition; + if (CurrentEntity.HeightAboveGround < 3.0f) + { + if (CurrentEntity.Model.IsVehicle) + { + SetVehicleOnGroundProperly(CurrentEntity.Handle); + } + else + { + PlaceObjectOnGroundProperly(CurrentEntity.Handle); + } + } + + // Controls + + if (Game.IsControlPressed(0, Control.VehicleFlyRollLeftOnly)) + { + headingOffset += rotateSpeed * Game.LastFrameTime; + } else if (Game.IsControlPressed(0, Control.VehicleFlyRollRightOnly)) + { + headingOffset -= rotateSpeed * Game.LastFrameTime; + } + + + await Delay(0); + + FreezeEntityPosition(handle, false); + SetEntityInvincible(handle, false); + SetEntityCollision(handle, true, true); + ResetEntityAlpha(handle); + } + + await Task.FromResult(0); + } + } +} \ No newline at end of file diff --git a/vMenu/menus/MiscSettings.cs b/vMenu/menus/MiscSettings.cs index e6133625..fd2d6ac1 100644 --- a/vMenu/menus/MiscSettings.cs +++ b/vMenu/menus/MiscSettings.cs @@ -16,6 +16,7 @@ public class MiscSettings private Menu menu; private Menu teleportOptionsMenu; private Menu developerToolsMenu; + private Menu entitySpawnerMenu; public bool ShowSpeedoKmh { get; private set; } = UserDefaults.MiscSpeedKmh; public bool ShowSpeedoMph { get; private set; } = UserDefaults.MiscSpeedMph; @@ -81,6 +82,7 @@ private void CreateMenu() menu = new Menu(Game.Player.Name, "Misc Settings"); teleportOptionsMenu = new Menu(Game.Player.Name, "Teleport Options"); developerToolsMenu = new Menu(Game.Player.Name, "Development Tools"); + entitySpawnerMenu = new Menu(Game.Player.Name, "Entity Spawner"); // teleport menu Menu teleportMenu = new Menu(Game.Player.Name, "Teleport Locations"); @@ -134,7 +136,10 @@ private void CreateMenu() MenuCheckboxItem lockCamX = new MenuCheckboxItem("Lock Camera Horizontal Rotation", "Locks your camera horizontal rotation. Could be useful in helicopters I guess.", false); MenuCheckboxItem lockCamY = new MenuCheckboxItem("Lock Camera Vertical Rotation", "Locks your camera vertical rotation. Could be useful in helicopters I guess.", false); - + // Entity spawner + MenuItem spawnNewEntity = new MenuItem("Spawn New Entity", "Spawns entity into the world and lets you set its position and rotation"); + MenuItem confirmEntityPosition = new MenuItem("Confirm Entity Position", "Stops placing entity and sets it at it current location."); + MenuItem cancelEntity = new MenuItem("Cancel", "Deletes current entity and cancels its placement"); Menu connectionSubmenu = new Menu(Game.Player.Name, "Connection Options"); MenuItem connectionSubmenuBtn = new MenuItem("Connection Options", "Server connection/game quit options."); @@ -503,6 +508,51 @@ private void CreateMenu() } }; + if (IsAllowed(Permission.MSEntitySpawner)) + { + MenuItem entSpawnerMenuBtn = new MenuItem("Entity Spawner", "Spawn and move entities") { Label = "→→→" }; + developerToolsMenu.AddMenuItem(entSpawnerMenuBtn); + MenuController.BindMenuItem(developerToolsMenu, entitySpawnerMenu, entSpawnerMenuBtn); + + entitySpawnerMenu.AddMenuItem(spawnNewEntity); + entitySpawnerMenu.AddMenuItem(confirmEntityPosition); + entitySpawnerMenu.AddMenuItem(cancelEntity); + + entitySpawnerMenu.OnItemSelect += async (sender, item, index) => + { + if (item == spawnNewEntity) + { + if (EntitySpawner.CurrentEntity != null || EntitySpawner.Active) + { + Notify.Error("You are already placing one entity, set its location or cancel and try again!"); + return; + } + + string result = await GetUserInput(windowTitle: "Enter model name"); + + if (String.IsNullOrEmpty(result)) + { + Notify.Error(CommonErrors.InvalidInput); + } + + EntitySpawner.SpawnEntity(result, Game.PlayerPed.Position); + } else if (item == confirmEntityPosition) + { + if (EntitySpawner.CurrentEntity != null) + { + EntitySpawner.FinishPlacement(); + } + else + { + Notify.Error("No entity to confirm position for!"); + } + } else if (item == cancelEntity) + { + EntitySpawner.CurrentEntity.Delete(); + } + }; + } + #endregion diff --git a/vMenuServer/config/permissions.cfg b/vMenuServer/config/permissions.cfg index 8b168b67..bcb5f2d0 100644 --- a/vMenuServer/config/permissions.cfg +++ b/vMenuServer/config/permissions.cfg @@ -485,6 +485,7 @@ add_ace builtin.everyone "vMenu.MiscSettings.ConnectionMenu" allow add_ace builtin.everyone "vMenu.MiscSettings.RestoreAppearance" allow add_ace builtin.everyone "vMenu.MiscSettings.RestoreWeapons" allow add_ace builtin.everyone "vMenu.MiscSettings.DriftMode" allow +add_ace group.moderator "vMenu.MiscSettings.EntitySpawner" allow # Probably not the best idea to give this feature for everyone #################################### # VOICE CHAT OPTIONS MENU # From 5bdf2e1c0edcef67f76242736a33c2dc23bc5f70 Mon Sep 17 00:00:00 2001 From: Explooosion-code <61145047+Explooosion-code@users.noreply.github.com> Date: Thu, 25 Mar 2021 12:04:58 +0100 Subject: [PATCH 2/3] Some refactoring to pass all checks --- vMenu/EntitySpawner.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/vMenu/EntitySpawner.cs b/vMenu/EntitySpawner.cs index a928b0eb..ab90d123 100644 --- a/vMenu/EntitySpawner.cs +++ b/vMenu/EntitySpawner.cs @@ -21,7 +21,6 @@ public class EntitySpawner : BaseScript /// /// Constructor. /// - public EntitySpawner() { #if DEBUG @@ -47,7 +46,6 @@ public EntitySpawner() /// model of entity as string /// initial coords for the entity /// true spawn was succesful - public static void SpawnEntity(string model, Vector3 coords) { SpawnEntity((uint) GetHashKey(model), coords); @@ -113,7 +111,6 @@ public static async void SpawnEntity(uint model, Vector3 coords) /// /// Method used to confirm location of prop and finish placement /// - public static void FinishPlacement() { Active = false; @@ -150,7 +147,6 @@ private void DrawButtons() //TODO: Right keys /// /// Input rotation vector /// Output direction vector - private Vector3 RotationToDirection(Vector3 rotation) { Vector3 adj = new Vector3( @@ -170,7 +166,6 @@ private Vector3 RotationToDirection(Vector3 rotation) /// Used to get coords of reycast from player camera; /// /// destination if no hit was found and coords of hit if there was one - private Vector3 GetCoordsPlayerIsLookingAt() { Vector3 camRotation = GetGameplayCamRot(0); @@ -178,9 +173,9 @@ private Vector3 GetCoordsPlayerIsLookingAt() Vector3 camDirection = RotationToDirection(camRotation); Vector3 dest = new Vector3( - camCoords.X + camDirection.X * RayDistance, - camCoords.Y + camDirection.Y * RayDistance, - camCoords.Z + camDirection.Z * RayDistance + camCoords.X + (camDirection.X * RayDistance), + camCoords.Y + (camDirection.Y * RayDistance), + camCoords.Z + (camDirection.Z * RayDistance) ); RaycastResult res = World.Raycast(camCoords, dest, IntersectOptions.Everything, Game.PlayerPed); @@ -261,7 +256,6 @@ private async Task MoveHandler() headingOffset -= rotateSpeed * Game.LastFrameTime; } - await Delay(0); FreezeEntityPosition(handle, false); From 7c6b2bd5f99b6924d257b1d871b2371ff982c850 Mon Sep 17 00:00:00 2001 From: Explooosion-code <61145047+Explooosion-code@users.noreply.github.com> Date: Thu, 25 Mar 2021 21:44:17 +0100 Subject: [PATCH 3/3] Added new menu options, fixes. Added option to "confirm and duplicate" which creates new entity after placing last one. It makes it easier to set up scenes. Added an if statement to prevent null reference errors. --- vMenu/EntitySpawner.cs | 21 ++++++++++++++++----- vMenu/menus/MiscSettings.cs | 18 ++++++++++++++---- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/vMenu/EntitySpawner.cs b/vMenu/EntitySpawner.cs index ab90d123..7f481245 100644 --- a/vMenu/EntitySpawner.cs +++ b/vMenu/EntitySpawner.cs @@ -107,14 +107,25 @@ public static async void SpawnEntity(uint model, Vector3 coords) if (!Active) Active = true; } - + /// /// Method used to confirm location of prop and finish placement /// - public static void FinishPlacement() + public static async void FinishPlacement(bool duplicate = false) { - Active = false; - CurrentEntity = null; + if (duplicate) + { + int hash = CurrentEntity.Model.Hash; + Vector3 position = CurrentEntity.Position; + CurrentEntity = null; + await Delay(1); // Mandatory + SpawnEntity((uint) hash, position); + } + else + { + Active = false; + CurrentEntity = null; + } } #endregion @@ -215,13 +226,13 @@ private async Task MoveHandler() float headingOffset = 0f; while (Active) { - int handle = CurrentEntity.Handle; if (CurrentEntity == null || !CurrentEntity.Exists()) { Active = false; CurrentEntity = null; break; } + int handle = CurrentEntity.Handle; DrawButtons(); diff --git a/vMenu/menus/MiscSettings.cs b/vMenu/menus/MiscSettings.cs index fd2d6ac1..80d38943 100644 --- a/vMenu/menus/MiscSettings.cs +++ b/vMenu/menus/MiscSettings.cs @@ -140,6 +140,8 @@ private void CreateMenu() MenuItem spawnNewEntity = new MenuItem("Spawn New Entity", "Spawns entity into the world and lets you set its position and rotation"); MenuItem confirmEntityPosition = new MenuItem("Confirm Entity Position", "Stops placing entity and sets it at it current location."); MenuItem cancelEntity = new MenuItem("Cancel", "Deletes current entity and cancels its placement"); + MenuItem confirmAndDuplicate = new MenuItem("Confirm Entity Position And Duplicate", "Stops placing entity and sets it at it current location and creates new one to place."); + Menu connectionSubmenu = new Menu(Game.Player.Name, "Connection Options"); MenuItem connectionSubmenuBtn = new MenuItem("Connection Options", "Server connection/game quit options."); @@ -516,8 +518,9 @@ private void CreateMenu() entitySpawnerMenu.AddMenuItem(spawnNewEntity); entitySpawnerMenu.AddMenuItem(confirmEntityPosition); + entitySpawnerMenu.AddMenuItem(confirmAndDuplicate); entitySpawnerMenu.AddMenuItem(cancelEntity); - + entitySpawnerMenu.OnItemSelect += async (sender, item, index) => { if (item == spawnNewEntity) @@ -536,11 +539,11 @@ private void CreateMenu() } EntitySpawner.SpawnEntity(result, Game.PlayerPed.Position); - } else if (item == confirmEntityPosition) + } else if (item == confirmEntityPosition || item == confirmAndDuplicate) { if (EntitySpawner.CurrentEntity != null) { - EntitySpawner.FinishPlacement(); + EntitySpawner.FinishPlacement(item == confirmAndDuplicate); } else { @@ -548,7 +551,14 @@ private void CreateMenu() } } else if (item == cancelEntity) { - EntitySpawner.CurrentEntity.Delete(); + if (EntitySpawner.CurrentEntity != null) + { + EntitySpawner.CurrentEntity.Delete(); + } + else + { + Notify.Error("No entity to cancel!"); + } } }; }