diff --git a/project/SPT.SinglePlayer/Models/Progression/LighthouseProgressionClass.cs b/project/SPT.SinglePlayer/Models/Progression/LighthouseProgressionClass.cs index 6021931..d0a84b8 100644 --- a/project/SPT.SinglePlayer/Models/Progression/LighthouseProgressionClass.cs +++ b/project/SPT.SinglePlayer/Models/Progression/LighthouseProgressionClass.cs @@ -1,5 +1,8 @@ -using Comfort.Common; +using BepInEx.Logging; +using Comfort.Common; using EFT; +using SPT.Reflection.Patching; +using System; using System.Collections.Generic; using System.Linq; using UnityEngine; @@ -8,123 +11,236 @@ namespace SPT.SinglePlayer.Models.Progression { public class LighthouseProgressionClass : MonoBehaviour { + /// + /// Flag to disable mines and AI data that instructs Zryachiy to attack you if the main player is authorized to enter Lightkeeper Island + /// + public static bool MainPlayerControlsIslandAccessForEveryone { get; set; } = true; + + /// + /// Flag indicating if the Lightkeeper-Island bridge mines and AI data that instructs Zryachiy to attack you have been disabled for everyone + /// + public bool IsIslandOpenForEveryone { get; private set; } = false; + + private static readonly string _transmitterId = "62e910aaf957f2915e0a5e36"; + private static readonly string _lightKeeperTid = "638f541a29ffd1183d187f57"; + private GameWorld _gameWorld; - private Player _player; - private float _timer; - private List _bridgeMines; - private RecodableItemClass _transmitter; - private readonly List _zryachiyAndFollowers = new List(); - private bool _aggressor; - private bool _isDoorDisabled; - private readonly string _transmitterId = "62e910aaf957f2915e0a5e36"; - private readonly string _lightKeeperTid = "638f541a29ffd1183d187f57"; + private ManualLogSource _logger; + private List lightkeeperFriendlyPlayers = new List(); + private List playersOnIsland = new List(); + + /// + /// PMC's that have been reported to be Lightkeeper-friendly + /// + public IReadOnlyList LightkeeperFriendlyPlayers => lightkeeperFriendlyPlayers.AsReadOnly(); + + /// + /// PMC's that have been reported to be on Lightkeeper Island + /// + public IReadOnlyList LightkeeperFriendlyPlayersOnIsland => playersOnIsland.AsReadOnly(); public void Start() { _gameWorld = Singleton.Instance; - _player = _gameWorld?.MainPlayer; - if (_gameWorld == null || _player == null) + if (_gameWorld == null || _gameWorld.MainPlayer == null) { - Destroy(this); - return; } + _logger = BepInEx.Logging.Logger.CreateLogSource(nameof(ModulePatch)); - // Get transmitter from players inventory - _transmitter = GetTransmitterFromInventory(); + // Watch for Zryachiy and his followers to spawn + Singleton.Instance.BotsController.BotSpawner.OnBotCreated += botCreated; // Exit if transmitter does not exist and isnt green - if (!PlayerHasActiveTransmitterInInventory()) + if (CheckAndAddLightkeeperFriendlyPlayer(_gameWorld.MainPlayer) && MainPlayerControlsIslandAccessForEveryone) { - Destroy(this); - - return; + AllowEveryoneAccessToLightkeeperIsland(); } - - var places = Singleton.Instance.BotsController.CoversData.AIPlaceInfoHolder.Places; - - places.First(x => x.name == "Attack").gameObject.SetActive(false); - - // Zone was added in a newer version and the gameObject actually has a \ - places.First(y => y.name == "CloseZone\\").gameObject.SetActive(false); - - // Give access to Lightkeepers door - _gameWorld.BufferZoneController.SetPlayerAccessStatus(_player.ProfileId, true); - - _bridgeMines = _gameWorld.MineManager.Mines; - - // Set mines to be non-active - SetBridgeMinesStatus(false); } - public void Update() + /// + /// Check if the player has been added to the Lightkeeper-friendly PMC list + /// + public bool IsALightkeeperFriendlyPlayer(IPlayer player) { - IncrementLastUpdateTimer(); + return player != null && lightkeeperFriendlyPlayers.Contains(player); + } - // Exit early if last update() run time was < 10 secs ago - if (_timer < 10f) + /// + /// Check if the player has been added to the list of Lightkeeper-friendly PMC's on Lightkeeper Island + /// + public bool IsLightkeeperFriendlyPlayerOnIsland(IPlayer player) + { + return player != null && playersOnIsland.Contains(player); + } + + /// + /// Checks if the player has an active transmitter in its inventory, and if so add it to the Lightkeeper-friendly PMC list + /// + /// True if the player was added to the Lightkeeper-friendly PMC list + public bool CheckAndAddLightkeeperFriendlyPlayer(IPlayer player) + { + if (PlayerHasActiveTransmitterInInventory(player)) + { + return AddLightkeeperFriendlyPlayer(player); + } + + return false; + } + + /// + /// Add the player to the Lightkeeper-friendly PMC list + /// + /// True if the player was added to the Lightkeeper-friendly PMC list + public bool AddLightkeeperFriendlyPlayer(IPlayer player) + { + if (player == null) + { + return false; + } + + if (lightkeeperFriendlyPlayers.Contains(player)) + { + _logger.LogWarning($"{player.Profile.Nickname} is already a registered Lightkeeper-friendly player"); + return false; + } + + lightkeeperFriendlyPlayers.Add(player); + + // Give access to Lightkeepers door + _gameWorld.BufferZoneController.SetPlayerAccessStatus(player.ProfileId, true); + + return true; + } + + /// + /// Remove the player from the Lightkeeper-friendly PMC list + /// + /// True if the player was removed from the Lightkeeper-friendly PMC list + public bool RemoveLightkeeperFriendlyPlayer(IPlayer player) + { + if (player == null) + { + return false; + } + + if (!lightkeeperFriendlyPlayers.Contains(player)) + { + _logger.LogWarning($"{player.Profile.Nickname} is not a registered Lightkeeper-friendly player"); + return false; + } + + lightkeeperFriendlyPlayers.Remove(player); + + // Revoke access to Lightkeepers door + _gameWorld.BufferZoneController.SetPlayerAccessStatus(player.ProfileId, false); + + return true; + } + + /// + /// Add the player to the list of PMC's that are on Lightkeeper Island + /// + public void LightkeeperFriendlyPlayerEnteredIsland(Player player) + { + if (playersOnIsland.Contains(player)) + { + _logger.LogWarning($"{player.name} is already a registered player on Lightkeeper Island"); + return; + } + + playersOnIsland.Add(player); + player.OnPlayerDead += OnLightkeeperFriendlyPlayerDead; + } + + /// + /// Remove the player from the list of PMC's that are on Lightkeeper Island + /// + public void LightkeeperFriendlyPlayerLeftIsland(Player player) + { + if (!playersOnIsland.Contains(player)) + { + _logger.LogWarning($"{player.name} is not a registered player on Lightkeeper Island"); + return; + } + + playersOnIsland.Remove(player); + player.OnPlayerDead -= OnLightkeeperFriendlyPlayerDead; + } + + /// + /// Disables brige mines, disables AI data to instruct Zryachiy to attack you, and watch for Zryachiy and his followers to spawn + /// + public void AllowEveryoneAccessToLightkeeperIsland() + { + if (IsIslandOpenForEveryone) { return; } - // Skip if: - // GameWorld missing - // Player not an enemy to Zryachiy - // Lk door not accessible - // Player has no transmitter on thier person - if (_gameWorld == null || _isDoorDisabled || _transmitter == null) - { - return; - } + DisableAIPlaceInfoForZryachiy(); - // Find Zryachiy and prep him - if (_zryachiyAndFollowers.Count == 0) - { - SetupZryachiyAndFollowerHostility(); - } + // Set mines to be non-active + SetBridgeMinesStatus(false); - // If player becomes aggressor, block access to LK - if (_aggressor) - { - DisableAccessToLightKeeper(); - } + IsIslandOpenForEveryone = true; + } + + /// + /// Disable the "Attack" and "CloseZone" AIPlaceInfo objects that instruct Zryachiy and his followers to attack you + /// + public void DisableAIPlaceInfoForZryachiy() + { + var places = Singleton.Instance.BotsController.CoversData.AIPlaceInfoHolder.Places; + + places.First(x => x.name == "Attack").gameObject.SetActive(false); + + // Zone was added in a newer version and the gameObject actually has a \ + places.First(y => y.name == "CloseZone\\").gameObject.SetActive(false); } /// /// Gets transmitter from players inventory /// - private RecodableItemClass GetTransmitterFromInventory() + public RecodableItemClass GetTransmitterFromInventory(IPlayer player) { - return (RecodableItemClass) _player.Profile.Inventory.AllRealPlayerItems.FirstOrDefault(x => x.TemplateId == _transmitterId); + if (player == null) + { + return null; + } + + return (RecodableItemClass)player.Profile.Inventory.AllRealPlayerItems.FirstOrDefault(x => x.TemplateId == _transmitterId); } /// /// Checks for transmitter status and exists in players inventory /// - private bool PlayerHasActiveTransmitterInInventory() + public bool PlayerHasActiveTransmitterInInventory(IPlayer player) { - return _transmitter != null && - _transmitter?.RecodableComponent?.Status == RadioTransmitterStatus.Green; + RecodableItemClass transmitter = GetTransmitterFromInventory(player); + return IsTransmitterActive(transmitter); } /// - /// Update _time to diff from last run of update() + /// Check if the transmitter allows access to the island /// - private void IncrementLastUpdateTimer() + public bool IsTransmitterActive(RecodableItemClass transmitter) { - _timer += Time.deltaTime; + return transmitter != null && transmitter?.RecodableComponent?.Status == RadioTransmitterStatus.Green; } /// /// Set all brdige mines to desire state /// /// What state should bridge mines be set to - private void SetBridgeMinesStatus(bool desiredMineState) + public void SetBridgeMinesStatus(bool desiredMineState) { - // Find mines with opposite state of what we want - var mines = _bridgeMines.Where(mine => mine.gameObject.activeSelf == !desiredMineState && mine.transform.parent.gameObject.name == "Directional_mines_LHZONE"); + // Find mines with opposite state of what we want + var mines = _gameWorld.MineManager.Mines + .Where(mine => IsLighthouseBridgeMine(mine) && mine.gameObject.activeSelf == !desiredMineState); + foreach (var mine in mines) { mine.gameObject.SetActive(desiredMineState); @@ -132,61 +248,98 @@ namespace SPT.SinglePlayer.Models.Progression } /// - /// Put Zryachiy and followers into a list and sub to their death event - /// Make player agressor if player kills them. + /// Check if the mine is on the Lightkeeper Island bridge /// - private void SetupZryachiyAndFollowerHostility() + /// True if the mine is on the Lightkeeper Island bridge + public static bool IsLighthouseBridgeMine(MineDirectional mine) { - // Only process non-players (ai) - foreach (var aiBot in _gameWorld.AllAlivePlayersList.Where(x => !x.IsYourPlayer)) + if (mine == null) { - // Bots that die on mounted guns get stuck in AllAlivePlayersList, need to check health - if (!aiBot.HealthController.IsAlive) + return false; + } + + return mine.transform.parent.gameObject.name == "Directional_mines_LHZONE"; + } + + /// + /// Set aggression + standing loss when Zryachiy/follower or a Lightkeeper-friendly PMC is killed by the main player + /// + /// The player that was killed + public void OnLightkeeperFriendlyPlayerDead(Player player, IPlayer lastAggressor, DamageInfo damageInfo, EBodyPart part) + { + foreach (Player lightkeeperFriendlyPlayer in lightkeeperFriendlyPlayers) + { + // Check if a Lightkeeper-friendly player was the killer + if ((lightkeeperFriendlyPlayer == null) || (lightkeeperFriendlyPlayer.ProfileId != player?.KillerId)) { continue; } - // Edge case of bossZryachiy not being hostile to player - if (aiBot.AIData.BotOwner.IsRole(WildSpawnType.bossZryachiy) || aiBot.AIData.BotOwner.IsRole(WildSpawnType.followerZryachiy)) + // A Lightkeeper-friendly player killed Zryachiy or one of his followers + if (isZryachiyOrFollower(player)) { - // Subscribe to bots OnDeath event - aiBot.OnPlayerDeadOrUnspawn += OnZryachiyOrFollowerDeath; + playerKilledLightkeeperFriendlyPlayer(lastAggressor); + break; + } - // Save bot to list for later access - if (!_zryachiyAndFollowers.Contains(aiBot)) - { - _zryachiyAndFollowers.Add(aiBot); - } + // A Lightkeeper-friendly player killed another Lightkeeper-friendly player when they were both on the island + if (playersOnIsland.Any(x => x?.Id == player?.Id) && playersOnIsland.Any(x => x?.Id == lastAggressor?.Id)) + { + playerKilledLightkeeperFriendlyPlayer(lastAggressor); + break; } } } - /// - /// Set aggression + standing loss when Zryachiy/follower is killed by player - /// - /// The player who killed Zryachiy/follower. - private void OnZryachiyOrFollowerDeath(Player player) + private void playerKilledLightkeeperFriendlyPlayer(IPlayer player) { - // Check if zryachiy/follower was killed by player - if (player?.KillerId == _player?.ProfileId) + if (player == null) { - // If player kills zryachiy or follower, force aggressor state - // Also set players Lk standing to negative (allows access to quest chain (Making Amends)) - _aggressor = true; - _player?.Profile.TradersInfo[_lightKeeperTid].SetStanding(-0.01); + return; + } + + // Set players Lk standing to negative (allows access to quest chain (Making Amends)) + player.Profile.TradersInfo[_lightKeeperTid].SetStanding(-0.01); + + // Disable access to Lightkeepers door for the player + _gameWorld.BufferZoneController.SetPlayerAccessStatus(player.ProfileId, false); + + RecodableItemClass transmitter = GetTransmitterFromInventory(player); + if ((transmitter != null) && IsTransmitterActive(transmitter)) + { + transmitter.RecodableComponent.SetStatus(RadioTransmitterStatus.Yellow); + transmitter.RecodableComponent.SetEncoded(false); + } + + RemoveLightkeeperFriendlyPlayer(player); + + _logger.LogInfo($"Removed Lightkeeper access for {player.Profile.Nickname}"); + } + + private void botCreated(BotOwner bot) + { + // Make sure the bot is Zryachiy or one of his followers + if (bot.Side != EPlayerSide.Savage) + { + return; + } + + // Check if the bot is Zryachiy or one of his followers + if (isZryachiyOrFollower(bot)) + { + // Subscribe to bots OnDeath event + bot.GetPlayer.OnPlayerDead += OnLightkeeperFriendlyPlayerDead; } } - /// - /// Disable door + set transmitter to 'red' - /// - private void DisableAccessToLightKeeper() + private static bool isZryachiyOrFollower(IPlayer player) { - // Disable access to Lightkeepers door for the player - _gameWorld.BufferZoneController.SetPlayerAccessStatus(_gameWorld.MainPlayer.ProfileId, false); - _transmitter?.RecodableComponent?.SetStatus(RadioTransmitterStatus.Yellow); - _transmitter?.RecodableComponent?.SetEncoded(false); - _isDoorDisabled = true; + if (player == null || !player.IsAI) + { + return false; + } + + return player.AIData.BotOwner.IsRole(WildSpawnType.bossZryachiy) || player.AIData.BotOwner.IsRole(WildSpawnType.followerZryachiy); } } } diff --git a/project/SPT.SinglePlayer/Patches/Progression/LighthouseBridgePatch.cs b/project/SPT.SinglePlayer/Patches/Progression/LighthouseBridgePatch.cs index 4db3ebc..85b6b82 100644 --- a/project/SPT.SinglePlayer/Patches/Progression/LighthouseBridgePatch.cs +++ b/project/SPT.SinglePlayer/Patches/Progression/LighthouseBridgePatch.cs @@ -24,7 +24,7 @@ namespace SPT.SinglePlayer.Patches.Progression return; } - if (gameWorld.MainPlayer.Location.ToLower() != "lighthouse" || gameWorld.MainPlayer.Side == EPlayerSide.Savage) + if (gameWorld.MainPlayer.Location.ToLower() != "lighthouse") { return; }