From 35a47bb5e4e49b1044d951435bdede94e6c5dba3 Mon Sep 17 00:00:00 2001 From: David-Lor <17401854+david-lor@users.noreply.github.com> Date: Sun, 17 Oct 2021 15:45:27 +0200 Subject: [PATCH] Initial functional script --- README.md | 37 ++++ SimpleGangWar.cs | 553 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 590 insertions(+) create mode 100644 SimpleGangWar.cs diff --git a/README.md b/README.md index d46f099..4f1dae4 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,40 @@ Grand Theft Auto IV script to create a basic battle between two teams. ## Background This is a port of [GTA V SimpleGangWar script](https://github.com/David-Lor/GTAV-SimpleGangWar) and [RDR2 SimpleGangWar script](https://github.com/David-Lor/RDR2-SimpleGangWar). Since most functions and methods are the same between GTA5, RDR2 and GTA4, and the language used is the same (C#), almost all functionalities were ported to GTA IV. + +## Installing + +**Prerrequisites**: ScriptHookDotNet and its requirements. I recommend downloading [LSPD:FR](https://www.lcpdfr.com/downloads/gta4mods/g17media/4607-lcpd-first-response-legacy-edition/), which bundles all the requirements. Tested on GTA IV version 1.2.0.43 (Steam+Rockstar version) on October 2021. + +**Installing**: put SimpleGangWar.cs and SimpleGangWar.ini into the `Grand Theft Auto IV/scripts` folder + +## Usage + +The key `F9` ("Hotkey") is used to navigate through all the steps of the script. In-game help popups will describe what to do, but these are the different stages you will find: + +1. The script will ask you to move to where the enemies will spawn +2. After pressing the hotkey, you must do the same to define where the allies will spawn +3. Right after defining both spawnpoints, peds from both teams will spawn on their respective spawnpoints, and fight each other +4. Press the hotkey once to enter the "exit mode" (it will ask for confirmation to stop the battle) +5. Pressing the hotkey again will inmediately stop the battle and remove all alive & dead peds from the map + +An additional hotkey `Z` ("SpawnHotkey") is used to pause/resume the ped spawning in both teams. + +## Settings + +At this moment, no .ini config file is supported. Configuration must be changed directly on the script. The settings will be documented here when the config file support is included. + +## Known bugs + +- First spawned peds may stand still and not fight. A possible workaround is to bump into them, or kill them. + +## TODO + +- Support .ini config file +- Make spawnpoint blips blink (or change in any other way) when spawning is paused. +- Implement other missing configuration features... + +## Changelog + +- 0.0.1 + - Initial ported functional release; missing some features and support for config file diff --git a/SimpleGangWar.cs b/SimpleGangWar.cs new file mode 100644 index 0000000..0f905d2 --- /dev/null +++ b/SimpleGangWar.cs @@ -0,0 +1,553 @@ +using GTA; +using System; +using System.Windows.Forms; +using System.Collections.Generic; + +namespace SimpleGangWar +{ + public class SimpleGangWarScript : Script + { + // Settings defined on script variables serve as fallback for settings not defined (or invalid) on .ini config file + + // https://gtamods.com/wiki/List_of_models_hashes + // https://gtamods.com/wiki/List_of_Weapons_(GTA4) + + private static Model[] pedsAllies = {"M_O_GRUS_HI_01", "M_Y_GRUS_LO_01", "M_Y_GRUS_LO_02", "M_Y_GRUS_HI_02", "M_M_GRU2_HI_01", "M_M_GRU2_HI_02", "M_M_GRU2_LO_02", "M_Y_GRU2_LO_01"}; + private static Weapon[] weaponsAllies = {Weapon.Rifle_AK47, Weapon.Handgun_DesertEagle, Weapon.Handgun_Glock, Weapon.SMG_Uzi}; + private static Model[] pedsEnemies = {"M_M_GJAM_HI_01", "M_M_GJAM_HI_02", "M_M_GJAM_HI_03", "M_Y_GJAM_LO_01", "M_Y_GJAM_LO_02"}; + private static Weapon[] weaponsEnemies = {Weapon.Rifle_AK47, Weapon.Rifle_M4, Weapon.Handgun_Glock, Weapon.Handgun_DesertEagle, Weapon.SMG_MP5}; + + private static readonly char[] StringSeparators = {',', ';'}; + + private static int healthAllies = 120; + private static int armorAllies = 0; + private static int healthEnemies = 120; + private static int armorEnemies = 0; + private static int accuracyAllies = 5; + private static int accuracyEnemies = 5; + + private static int maxPedsPerTeam = 10; + private static Keys hotkey = Keys.B; + private static Keys spawnHotkey = Keys.N; + private static bool noWantedLevel = true; + private static bool showBlipsOnPeds = true; + private static bool dropWeaponOnDead = false; + private static bool removeDeadPeds = true; + private static bool runToSpawnpoint = false; + private static bool processOtherRelationshipGroups = false; + private static bool neutralPlayer = false; + private static int spawnpointFloodLimitPeds = -1; + private static float spawnpointFloodLimitDistance = 8.0f; + private static int idleInterval = 500; + private static int battleInterval = 100; + private static int maxPedsAllies = 10; + private static int maxPedsEnemies = 10; + private static int maxSpawnPedsAllies = 5; + private static int maxSpawnPedsEnemies = 5; + + // From here, internal script variables - do not change! + + private RelationshipGroup relationshipGroupEnemies = RelationshipGroup.NetworkPlayer_32; + + private int originalWantedLevel; + + private int spawnedAlliesCounter; + private int spawnedEnemiesCounter; + + private List spawnedAllies = new List(); + private List spawnedEnemies = new List(); + private List deadPeds = new List(); + private List pedsRemove = new List(); + private List processedRelationshipGroups = new List(); + private Dictionary pedsBlips = new Dictionary(); + + private bool spawnEnabled = true; + private Stage stage = Stage.Initial; + + private Vector3 spawnpointAllies; + private Vector3 spawnpointEnemies; + private float spawnpointsDistance; + + private Blip spawnpointBlipAllies; + private Blip spawnpointBlipEnemies; + + private static Relationship[] allyRelationships = + {Relationship.Companion, Relationship.Like, Relationship.Respect}; + + private static Relationship[] enemyRelationships = {Relationship.Hate, Relationship.Dislike}; + + private int relationshipGroupPlayer; + private static Random random; + + private enum Stage + { + Initial = 0, + DefiningEnemySpawnpoint = 1, + EnemySpawnpointDefined = 2, + Running = 3, + StopKeyPressed = 4 + } + + private class SettingsHeader + { + public static readonly string Allies = "ALLIED_TEAM"; + public static readonly string Enemies = "ENEMY_TEAM"; + public static readonly string General = "SETTINGS"; + } + + + public SimpleGangWarScript() + { + Tick += MainLoop; + KeyUp += OnKeyUp; + Interval = idleInterval; + + /*ScriptSettings config = ScriptSettings.Load("scripts\\SimpleGangWar.ini"); + string configString; + + healthAllies = config.GetValue(SettingsHeader.Allies, "Health", healthAllies); + healthEnemies = config.GetValue(SettingsHeader.Enemies, "Health", healthEnemies); + + armorAllies = config.GetValue(SettingsHeader.Allies, "Armor", armorAllies); + armorEnemies = config.GetValue(SettingsHeader.Enemies, "Armor", armorEnemies); + + accuracyAllies = config.GetValue(SettingsHeader.Allies, "Accuracy", accuracyAllies); + accuracyEnemies = config.GetValue(SettingsHeader.Enemies, "Accuracy", accuracyEnemies); + + configString = config.GetValue(SettingsHeader.Allies, "CombatMovement", ""); + combatMovementAllies = EnumParse(configString, combatMovementAllies); + configString = config.GetValue(SettingsHeader.Enemies, "CombatMovement", ""); + combatMovementEnemies = EnumParse(configString, combatMovementEnemies); + + configString = config.GetValue(SettingsHeader.Allies, "CombatRange", ""); + combatRangeAllies = EnumParse(configString, combatRangeAllies); + configString = config.GetValue(SettingsHeader.Enemies, "CombatRange", ""); + combatRangeEnemies = EnumParse(configString, combatRangeEnemies); + + configString = config.GetValue(SettingsHeader.Allies, "Weapons", ""); + weaponsAllies = ArrayParse(configString, weaponsAllies); + configString = config.GetValue(SettingsHeader.Enemies, "Weapons", ""); + weaponsEnemies = ArrayParse(configString, weaponsEnemies); + + configString = config.GetValue(SettingsHeader.Allies, "Models", ""); + pedsAllies = ArrayParse(configString, pedsAllies); + configString = config.GetValue(SettingsHeader.Enemies, "Models", ""); + pedsEnemies = ArrayParse(configString, pedsEnemies); + + configString = config.GetValue(SettingsHeader.General, "Hotkey", ""); + hotkey = EnumParse(configString, hotkey); + configString = config.GetValue(SettingsHeader.General, "SpawnHotkey", ""); + spawnHotkey = EnumParse(configString, spawnHotkey); + + maxPedsPerTeam = config.GetValue(SettingsHeader.General, "MaxPedsPerTeam", maxPedsPerTeam); + noWantedLevel = config.GetValue(SettingsHeader.General, "NoWantedLevel", noWantedLevel); + showBlipsOnPeds = config.GetValue(SettingsHeader.General, "ShowBlipsOnPeds", showBlipsOnPeds); + dropWeaponOnDead = config.GetValue(SettingsHeader.General, "DropWeaponOnDead", dropWeaponOnDead); + removeDeadPeds = config.GetValue(SettingsHeader.General, "RemoveDeadPeds", removeDeadPeds); + runToSpawnpoint = config.GetValue(SettingsHeader.General, "RunToSpawnpoint", runToSpawnpoint); + processOtherRelationshipGroups = config.GetValue(SettingsHeader.General, "ProcessOtherRelationshipGroups", + processOtherRelationshipGroups); + neutralPlayer = config.GetValue(SettingsHeader.General, "NeutralPlayer", neutralPlayer); + spawnpointFloodLimitPeds = config.GetValue(SettingsHeader.General, "SpawnpointFloodLimitPeds", + spawnpointFloodLimitPeds); + spawnpointFloodLimitDistance = config.GetValue(SettingsHeader.General, "SpawnpointFloodLimitDistance", + spawnpointFloodLimitDistance); + idleInterval = config.GetValue(SettingsHeader.General, "IdleInterval", idleInterval); + battleInterval = config.GetValue(SettingsHeader.General, "BattleInterval", battleInterval); + + maxPedsAllies = config.GetValue(SettingsHeader.Allies, "MaxPeds", maxPedsPerTeam); + maxPedsEnemies = config.GetValue(SettingsHeader.Enemies, "MaxPeds", maxPedsPerTeam); + maxSpawnPedsAllies = config.GetValue(SettingsHeader.Allies, "MaxSpawnPeds", maxSpawnPedsAllies); + maxSpawnPedsEnemies = config.GetValue(SettingsHeader.Enemies, "MaxSpawnPeds", maxSpawnPedsEnemies); + */ + + World.SetGroupRelationship(RelationshipGroup.Player, Relationship.Respect, RelationshipGroup.Player); + World.SetGroupRelationship(RelationshipGroup.Player, Relationship.Hate, relationshipGroupEnemies); + + random = new Random(); + + Game.DisplayText("SimpleGangWar loaded"); + } + + + /// + /// The main script loop runs at the frequency delimited by the Interval, which varies depending if the battle is running or not. + /// The loop only spawn peds and processes them as the battle is running. Any other actions that happen outside a battle are processed by Key event handlers. + /// + private void MainLoop(object sender, EventArgs e) + { + if (stage >= Stage.Running) + { + try + { + SpawnPeds(true); + SpawnPeds(false); + + SetUnmanagedPedsInRelationshipGroups(); + ProcessSpawnedPeds(true); + ProcessSpawnedPeds(false); + } + catch (FormatException exception) + { + Game.DisplayText("(SimpleGangWar) Error! " + exception.Message); + } + } + } + + + /// + /// Key event handler for key releases. + /// + private void OnKeyUp(object sender, GTA.KeyEventArgs e) + { + if (e.Key == hotkey) + { + switch (stage) + { + case Stage.Initial: + Game.DisplayText( + "Welcome to SimpleGangWar!\nGo to the enemy spawnpoint and press the hotkey again to define it.", + 180000); + stage = Stage.DefiningEnemySpawnpoint; + break; + case Stage.DefiningEnemySpawnpoint: + DefineSpawnpoint(false); + Game.DisplayText( + "Enemy spawnpoint defined! Now go to the allied spawnpoint and press the hotkey again to define it.", + 180000); + stage = Stage.EnemySpawnpointDefined; + break; + case Stage.EnemySpawnpointDefined: + DefineSpawnpoint(true); + SetupBattle(); + Game.DisplayText("The battle begins NOW!", 5000); + stage = Stage.Running; + break; + case Stage.Running: + Game.DisplayText("Do you really want to stop the battle? Press the hotkey again to confirm.", + 7000); + stage = Stage.StopKeyPressed; + break; + case Stage.StopKeyPressed: + Game.DisplayText("The battle has ended!", 5000); + stage = Stage.Initial; + Teardown(); + break; + } + } + else if (e.Key == spawnHotkey) + { + spawnEnabled = !spawnEnabled; + BlinkSpawnpoint(true); + BlinkSpawnpoint(false); + } + } + + + /// + /// After the spawnpoints are defined, some tweaks are required just before the battle begins. + /// + private void SetupBattle() + { + Interval = battleInterval; + spawnpointsDistance = spawnpointEnemies.DistanceTo(spawnpointAllies); + spawnedAlliesCounter = 0; + spawnedEnemiesCounter = 0; + } + + /// + /// Spawn peds on the given team, until the ped limit for that team is reached. + /// + /// true=ally team / false=enemy team + private void SpawnPeds(bool alliedTeam) + { + while (spawnEnabled && CanPedsSpawn(alliedTeam)) + { + SpawnRandomPed(alliedTeam); + } + } + + /// + /// Determine if peds on the given team should spawn or not. + /// + /// true=ally team / false=enemy team + private bool CanPedsSpawn(bool alliedTeam) + { + List spawnedPedsList = alliedTeam ? spawnedAllies : spawnedEnemies; + int maxPeds = alliedTeam ? maxPedsAllies : maxPedsEnemies; + int maxSpawnPeds = alliedTeam ? maxSpawnPedsAllies : maxSpawnPedsEnemies; + int totalSpawnedPeds = alliedTeam ? spawnedAlliesCounter : spawnedEnemiesCounter; + + // by MaxPeds in the team + if (spawnedPedsList.Count >= maxPeds) return false; + return true; + + // TODO continue validations + + // by MaxSpawnPeds limit + if (maxSpawnPeds >= 0 && totalSpawnedPeds > maxSpawnPeds) return false; + + // by SpawnpointFlood limit + if (spawnpointFloodLimitPeds < 1) return true; + + Vector3 spawnpointPosition = alliedTeam ? spawnpointAllies : spawnpointEnemies; + Ped[] pedsNearSpawnpoint = World.GetPeds(spawnpointPosition, spawnpointFloodLimitDistance); + + int pedsNearSpawnpointCount = 0; + foreach (Ped ped in pedsNearSpawnpoint) + { + if (ped.isAlive && spawnedPedsList.Contains(ped)) pedsNearSpawnpointCount++; + } + + return pedsNearSpawnpointCount < spawnpointFloodLimitPeds; + } + + /// + /// Spawns a ped on the given team, ready to fight. + /// + /// true=ally team / false=enemy team + /// The spawned ped + private Ped SpawnRandomPed(bool alliedTeam) + { + Vector3 pedPosition = alliedTeam ? spawnpointAllies : spawnpointEnemies; + Model pedModel = RandomChoice(alliedTeam ? pedsAllies : pedsEnemies); + Weapon pedWeapon = RandomChoice(alliedTeam ? weaponsAllies : weaponsEnemies); + + Ped ped = World.CreatePed(pedModel, pedPosition); + ped.Weapons.Select(pedWeapon); + ped.Weapons.Current.Ammo = Int32.MaxValue; + + ped.Health = ped.MaxHealth = alliedTeam ? healthAllies : healthEnemies; + ped.Armor = alliedTeam ? armorAllies : armorEnemies; + ped.Money = 0; + ped.Accuracy = alliedTeam ? accuracyAllies : accuracyEnemies; + ped.RelationshipGroup = alliedTeam ? RelationshipGroup.Player : relationshipGroupEnemies; + + if (showBlipsOnPeds) + { + Blip blip = ped.AttachBlip(); + blip.Color = alliedTeam ? BlipColor.Cyan : BlipColor.Orange; + blip.Name = alliedTeam ? "Ally team member" : "Enemy team member"; + blip.Display = BlipDisplay.MapOnly; + blip.Scale = 0.5f; + pedsBlips.Add(ped, blip); + } + + ped.Task.ClearAllImmediately(); + ped.Task.AlwaysKeepTask = true; + if (runToSpawnpoint) ped.Task.RunTo(alliedTeam ? spawnpointEnemies : spawnpointAllies); + else ped.Task.FightAgainstHatedTargets(spawnpointsDistance); + + if (alliedTeam) + { + spawnedAllies.Add(ped); + spawnedAlliesCounter++; + } + else + { + spawnedEnemies.Add(ped); + spawnedEnemiesCounter++; + } + + return ped; + } + + /// + /// Processes the spawned peds of the given team. This includes making sure they fight and process their removal as they are killed in action. + /// + /// true=ally team / false=enemy team + private void ProcessSpawnedPeds(bool alliedTeam) + { + List pedList = alliedTeam ? spawnedAllies : spawnedEnemies; + + foreach (Ped ped in pedList) + { + if (ped.isDead) + { + Blip pedBlip; + if (pedsBlips.TryGetValue(ped, out pedBlip)) { + pedBlip.Delete(); + } + + pedsRemove.Add(ped); + deadPeds.Add(ped); + if (removeDeadPeds) ped.NoLongerNeeded(); + } + // TODO this check can make peds stutter forever: + else if (ped.isIdle) + { + if (runToSpawnpoint) ped.Task.RunTo(alliedTeam ? spawnpointEnemies : spawnpointAllies); + else ped.Task.FightAgainstHatedTargets(spawnpointsDistance); + } + } + + foreach (Ped ped in pedsRemove) + { + pedList.Remove(ped); + } + + pedsRemove.Clear(); + pedsBlips.Clear(); + } + + /// + /// Set the spawnpoint for the given team on the position where the player is at. + /// + /// true=ally team / false=enemy team + private void DefineSpawnpoint(bool alliedTeam) + { + Vector3 position = Player.Character.Position; + Blip blip = Blip.AddBlip(position); + + if (alliedTeam) + { + spawnpointAllies = position; + spawnpointBlipAllies = blip; + blip.Icon = BlipIcon.Building_Garage; + blip.Color = BlipColor.Cyan; + blip.Display = BlipDisplay.ArrowAndMap; + blip.Name = "Ally spawnpoint"; + } + else + { + spawnpointEnemies = position; + spawnpointBlipEnemies = blip; + blip.Icon = BlipIcon.Activity_Darts; + blip.Color = BlipColor.Orange; + blip.Display = BlipDisplay.ArrowAndMap; + blip.Name = "Enemy spawnpoint"; + } + + BlinkSpawnpoint(alliedTeam); + } + + /// + /// Blink or stop blinking the spawnpoint blip of the given team, depending on if the spawn is disabled (blink) or not (stop blinking). + /// + /// true=ally team / false=enemy team + private void BlinkSpawnpoint(bool alliedTeam) + { + Blip blip = alliedTeam ? spawnpointBlipAllies : spawnpointBlipEnemies; + if (blip == null) return; + + /*if (spawnEnabled) + { + blip.Transparency = 0.5f; + } + else + { + blip.Transparency = 1.0f; + }*/ + } + + /// + /// Get all the relationship groups from foreign peds (those that are not part of SimpleGangWar), and set the relationship between these groups and the SimpleGangWar groups. + /// + private void SetUnmanagedPedsInRelationshipGroups() + { + // TODO + /*if (processOtherRelationshipGroups) + { + foreach (Ped ped in World.GetAllPeds()) + { + if (ped.IsHuman && !ped.IsPlayer) + { + Relationship pedRelationshipWithPlayer = ped.GetRelationshipWithPed(Game.Player.Character); + int relationshipGroup = ped.RelationshipGroup; + + if (relationshipGroup != relationshipGroupAllies && + relationshipGroup != relationshipGroupEnemies && + relationshipGroup != relationshipGroupPlayer) + { + if (allyRelationships.Contains(pedRelationshipWithPlayer)) + { + SetRelationshipBetweenGroups(Relationship.Respect, relationshipGroup, + relationshipGroupAllies); + SetRelationshipBetweenGroups(Relationship.Hate, relationshipGroup, + relationshipGroupEnemies); + } + else if (enemyRelationships.Contains(pedRelationshipWithPlayer)) + { + SetRelationshipBetweenGroups(Relationship.Respect, relationshipGroup, + relationshipGroupEnemies); + SetRelationshipBetweenGroups(Relationship.Hate, relationshipGroup, + relationshipGroupAllies); + } + } + } + } + }*/ + } + + /// + /// Physically delete the peds from the given list from the game world. + /// + /// List of peds to teardown + private void TeardownPeds(List pedList) + { + foreach (Ped ped in pedList) + { + if (ped.Exists()) ped.Delete(); + } + } + + /// + /// Manage the battle teardown on user requests. This brings the game to an initial state, before battle start and spawnpoint definition. + /// + private void Teardown() + { + Interval = idleInterval; + spawnpointBlipAllies.Delete(); + spawnpointBlipEnemies.Delete(); + + TeardownPeds(spawnedAllies); + TeardownPeds(spawnedEnemies); + TeardownPeds(deadPeds); + + spawnedAllies.Clear(); + spawnedEnemies.Clear(); + deadPeds.Clear(); + pedsRemove.Clear(); + processedRelationshipGroups.Clear(); + } + + /// + /// Choose a random item from a given array, containing objects of type T + /// + /// Type of objects in the array + /// Array to choose from + /// A random item from the array + private T RandomChoice(T[] array) + { + return array[random.Next(0, array.Length)]; + } + + /// + /// Given a string key from an enum, return the referenced enum object. + /// + /// The whole enum object, to choose an option from + /// The enum key as string + /// What enum option to return if the referenced enum key does not exist in the enum + /// The chosen enum option + /*private EnumType EnumParse(string enumKey, EnumType defaultValue) where EnumType : struct + { + EnumType returnValue; + if (!Enum.TryParse(enumKey, true, out returnValue)) returnValue = defaultValue; + return returnValue; + }*/ + + /// + /// Given a string of words to be split, split them and return a string array. + /// + /// Input string + /// Array to return if the input string contains no items + /// A string array + /*private string[] ArrayParse(string stringInput, string[] defaultArray) + { + string[] resultArray = stringInput.Replace(" ", string.Empty) + .Split(StringSeparators, StringSplitOptions.RemoveEmptyEntries); + if (resultArray.Length == 0) resultArray = defaultArray; + return resultArray; + }*/ + } +}