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] 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 #