diff --git a/UncreatedWarfare.sln.DotSettings b/UncreatedWarfare.sln.DotSettings new file mode 100644 index 00000000..9c50f929 --- /dev/null +++ b/UncreatedWarfare.sln.DotSettings @@ -0,0 +1,3 @@ + + FOB + UAV \ No newline at end of file diff --git a/UncreatedWarfare/Commands/Group/GroupJoinCommand.cs b/UncreatedWarfare/Commands/Group/GroupJoinCommand.cs index 768c5ead..da15d9b3 100644 --- a/UncreatedWarfare/Commands/Group/GroupJoinCommand.cs +++ b/UncreatedWarfare/Commands/Group/GroupJoinCommand.cs @@ -30,12 +30,12 @@ public async UniTask ExecuteAsync(CancellationToken token) { Context.AssertRanByPlayer(); - Context.AssertArgs(2); + Context.AssertArgs(1); Team? newTeam; - if (!Context.TryGet(1, out ulong groupId)) + if (!Context.TryGet(0, out ulong groupId)) { - string groupInput = Context.Get(1)!; + string groupInput = Context.Get(0)!; newTeam = _teamManager.FindTeam(groupInput); if (newTeam == null) diff --git a/UncreatedWarfare/Components/LaserGuidedMissileComponent.cs b/UncreatedWarfare/Components/LaserGuidedMissileComponent.cs index 03eade07..3352c031 100644 --- a/UncreatedWarfare/Components/LaserGuidedMissileComponent.cs +++ b/UncreatedWarfare/Components/LaserGuidedMissileComponent.cs @@ -1,22 +1,28 @@ using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Uncreated.Warfare.Configuration; +using Uncreated.Warfare.Layouts.Teams; +using Uncreated.Warfare.Players; +using Uncreated.Warfare.Squads.Spotted; namespace Uncreated.Warfare.Components; internal class LaserGuidedMissileComponent : MonoBehaviour { #nullable disable - private Player _firer; + private WarfarePlayer _firer; + private Team _team; private GameObject _projectile; private Transform _aim; private Rigidbody _rigidbody; private List _colliders; #nullable restore - private SpottedComponent? _laserTarget; + private SpottableObjectComponent? _laserTarget; + private SpottedService? _spottedService; private float _guiderDistance; private float _aquisitionRange; private float _projectileSpeed; @@ -34,10 +40,11 @@ internal class LaserGuidedMissileComponent : MonoBehaviour public float InitializationTime { get; private set; } public bool LockedOn => _laserTarget != null; - public void Initialize(GameObject projectile, Player firer, IServiceProvider serviceProvider, float projectileSpeed, float responsiveness, float aquisitionRange, float armingDistance, float fullGuidanceDelay) + public void Initialize(GameObject projectile, WarfarePlayer firer, IServiceProvider serviceProvider, float projectileSpeed, float responsiveness, float aquisitionRange, float armingDistance, float fullGuidanceDelay) { _projectile = projectile; _firer = firer; + _team = firer.Team; _maxTurnDegrees = responsiveness; _projectileSpeed = projectileSpeed; _aquisitionRange = aquisitionRange; @@ -46,6 +53,8 @@ public void Initialize(GameObject projectile, Player firer, IServiceProvider ser _guiderDistance = 30; _turnMultiplier = 0; + _spottedService = serviceProvider.GetRequiredService(); + AssetConfiguration assetConfig = serviceProvider.GetRequiredService(); _fxSilent = assetConfig.GetAssetLink("Effects:Projectiles:GuidedMissileSilent"); _fxSound = assetConfig.GetAssetLink("Effects:Projectiles:GuidedMissileSound"); @@ -65,12 +74,12 @@ public void Initialize(GameObject projectile, Player firer, IServiceProvider ser return; } - InteractableVehicle? vehicle = firer.movement.getVehicle(); + InteractableVehicle? vehicle = firer.UnturnedPlayer.movement.getVehicle(); if (vehicle != null) { foreach (Passenger turret in vehicle.turrets) { - if (turret.player == null || turret.player.player != firer) + if (turret.player == null || !firer.Equals(turret.player.player)) continue; _aim = turret.turretAim; @@ -90,7 +99,7 @@ public void Initialize(GameObject projectile, Player firer, IServiceProvider ser } else { - _aim = firer.look.aim.transform; + _aim = firer.UnturnedPlayer.look.aim.transform; _isActive = true; projectile.transform.forward = _aim.forward; _rigidbody.velocity = projectile.transform.forward * projectileSpeed; @@ -103,29 +112,34 @@ private bool TryAcquireTarget() { if (_laserTarget != null) { - if (_laserTarget.IsActive) + if (_laserTarget.IsLaserTarget(_team)) return true; + _laserTarget = null; } float minAngle = 45; - if (_firer is null) + if (_firer is null || _spottedService == null) return false; - foreach (SpottedComponent spotted in SpottedComponent.ActiveMarkers) + foreach (SpottableObjectComponent spotted in _spottedService.AliveSpottableObjects) { - if (spotted.SpottingTeam == _firer.quests.groupID.m_SteamID && spotted.IsLaserTarget) + if (!spotted.IsLaserTarget(_team)) { - if ((spotted.transform.position - _projectile.transform.position).sqrMagnitude < _aquisitionRange * _aquisitionRange) - { - float angleBetween = Vector3.Angle(spotted.transform.position - _projectile.transform.position, _projectile.transform.forward); - if (angleBetween < minAngle) - { - minAngle = angleBetween; - _laserTarget = spotted; - } - } + continue; + } + + if ((spotted.transform.position - _projectile.transform.position).sqrMagnitude >= _aquisitionRange * _aquisitionRange) + { + continue; + } + + float angleBetween = Vector3.Angle(spotted.transform.position - _projectile.transform.position, _projectile.transform.forward); + if (angleBetween < minAngle) + { + minAngle = angleBetween; + _laserTarget = spotted; } } @@ -136,6 +150,7 @@ private bool TryAcquireTarget() private float _lastSent; [UsedImplicitly] + [SuppressMessage("CodeQuality", "IDE0051")] private void FixedUpdate() { if (!_isActive) diff --git a/UncreatedWarfare/Components/SpottedComponent.cs b/UncreatedWarfare/Components/SpottedComponent.cs deleted file mode 100644 index 5e42f291..00000000 --- a/UncreatedWarfare/Components/SpottedComponent.cs +++ /dev/null @@ -1,431 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Collections.Generic; -using Uncreated.Warfare.Configuration; -using Uncreated.Warfare.Layouts.Teams; -using Uncreated.Warfare.Players; -using Uncreated.Warfare.Players.Management; -using Uncreated.Warfare.Players.UI; -using Uncreated.Warfare.Translations; -using Uncreated.Warfare.Util; -using Uncreated.Warfare.Vehicles.Info; - -namespace Uncreated.Warfare.Components; - -public class SpottedComponent : MonoBehaviour -{ -#nullable disable - private ILogger _logger; - private IServiceProvider _serviceProvider; - private IPlayerService _playerService; - private Team _team; - private Team _ownerTeam; -#nullable restore - - public EffectAsset? Effect { get; private set; } - public Spotted? Type { get; private set; } - public VehicleType? VehicleType { get; private set; } - - /// - /// Player who spotted the object. - /// - /// May not always be online or have a value at all. - public WarfarePlayer? CurrentSpotter { get; private set; } - public Team SpottingTeam => _team; - public Team OwnerTeam { get => _vehicle is not null ? Team.NoTeam /* _vehicle.lockedGroup.m_SteamID todo */ : _ownerTeam; set => _ownerTeam = value; } - public bool IsActive => _coroutine != null; - public bool IsLaserTarget { get; private set; } - private float _frequency; - private float _defaultTimer; - private Coroutine? _coroutine; - public float ToBeUnspottedNonUAV; - public float EndTime; - public bool UAVMode; - public WarfarePlayer? LastNonUAVSpotter = null; - private InteractableVehicle? _vehicle; - public Vector3 UAVLastKnown { get; internal set; } - - public static readonly HashSet ActiveMarkers = new HashSet(); - public static readonly List AllMarkers = new List(128); - - public void Initialize(Spotted type, Team ownerTeam, IServiceProvider serviceProvider) - { - _ownerTeam = ownerTeam; - _vehicle = null; - VehicleType = null; - - Type = type; - CurrentSpotter = null; - IsLaserTarget = type == Spotted.FOB; - - InitializeCommon(serviceProvider); - - AssetConfiguration assetConfig = serviceProvider.GetRequiredService(); - - IAssetLink? effect; - switch (type) - { - case Spotted.Infantry: - effect = assetConfig.GetAssetLink("Effects:Spotted:Infantry"); - _defaultTimer = 12; - _frequency = 0.5f; - break; - - case Spotted.FOB: - effect = assetConfig.GetAssetLink("Effects:Spotted:FOB"); - _defaultTimer = 240; - _frequency = 1f; - break; - - default: - _vehicle = null; - _logger.LogWarning("Unknown spotted type: {0} in SpottedComponent.", type); - Destroy(this); - return; - } - - if (effect.TryGetAsset(out EffectAsset? asset)) - { - Effect = asset; - } - else - { - _logger.LogWarning("SpottedComponent could not initialize: Effect asset not found: {0}.", type); - } - - if (!AllMarkers.Contains(this)) - AllMarkers.Add(this); - - _logger.LogConditional("Spotter initialized: {0}.", this); - } - public void Initialize(VehicleType type, InteractableVehicle vehicle, IServiceProvider serviceProvider) - { - - CurrentSpotter = null; - IsLaserTarget = type.IsGroundVehicle(); - _vehicle = vehicle; - VehicleType = type; - - InitializeCommon(serviceProvider); - - AssetConfiguration assetConfig = serviceProvider.GetRequiredService(); - IAssetLink? effect; - switch (type) - { - case Vehicles.Info.VehicleType.AA: - effect = assetConfig.GetAssetLink("Effects:Spotted:AA"); - _defaultTimer = 240; - _frequency = 1f; - Type = Spotted.Emplacement; - break; - - case Vehicles.Info.VehicleType.APC: - effect = assetConfig.GetAssetLink("Effects:Spotted:APC"); - _defaultTimer = 30; - _frequency = 0.5f; - Type = Spotted.Emplacement; - break; - - case Vehicles.Info.VehicleType.ATGM: - effect = assetConfig.GetAssetLink("Effects:Spotted:ATGM"); - _defaultTimer = 240; - _frequency = 1f; - Type = Spotted.Emplacement; - break; - - case Vehicles.Info.VehicleType.AttackHeli: - effect = assetConfig.GetAssetLink("Effects:Spotted:AttackHeli"); - _defaultTimer = 15; - _frequency = 0.5f; - Type = Spotted.Aircraft; - break; - - case Vehicles.Info.VehicleType.HMG: - effect = assetConfig.GetAssetLink("Effects:Spotted:HMG"); - _defaultTimer = 240; - _frequency = 1f; - Type = Spotted.Emplacement; - break; - - case Vehicles.Info.VehicleType.Humvee: - effect = assetConfig.GetAssetLink("Effects:Spotted:Humvee"); - _defaultTimer = 30; - _frequency = 0.5f; - Type = Spotted.LightVehicle; - break; - - case Vehicles.Info.VehicleType.IFV: - effect = assetConfig.GetAssetLink("Effects:Spotted:IFV"); - _defaultTimer = 30; - _frequency = 0.5f; - Type = Spotted.Armor; - break; - - case Vehicles.Info.VehicleType.Jet: - effect = assetConfig.GetAssetLink("Effects:Spotted:Jet"); - _defaultTimer = 10; - _frequency = 0.5f; - Type = Spotted.Aircraft; - break; - - case Vehicles.Info.VehicleType.MBT: - effect = assetConfig.GetAssetLink("Effects:Spotted:MBT"); - _defaultTimer = 30; - _frequency = 0.5f; - Type = Spotted.Armor; - break; - - case Vehicles.Info.VehicleType.Mortar: - effect = assetConfig.GetAssetLink("Effects:Spotted:Mortar"); - _defaultTimer = 240; - _frequency = 1f; - Type = Spotted.Emplacement; - break; - - case Vehicles.Info.VehicleType.ScoutCar: - effect = assetConfig.GetAssetLink("Effects:Spotted:ScoutCar"); - _defaultTimer = 30; - _frequency = 0.5f; - Type = Spotted.LightVehicle; - break; - - case Vehicles.Info.VehicleType.TransportAir: - effect = assetConfig.GetAssetLink("Effects:Spotted:TransportHeli"); - _defaultTimer = 15; - _frequency = 0.5f; - Type = Spotted.Aircraft; - break; - - case Vehicles.Info.VehicleType.LogisticsGround: - effect = assetConfig.GetAssetLink("Effects:Spotted:Truck"); - _defaultTimer = 30; - _frequency = 0.5f; - Type = Spotted.LightVehicle; - break; - - case Vehicles.Info.VehicleType.TransportGround: - effect = assetConfig.GetAssetLink("Effects:Spotted:Truck"); - _defaultTimer = 30; - _frequency = 0.5f; - Type = Spotted.LightVehicle; - break; - - default: - VehicleType = null; - _vehicle = null; - Type = null; - _logger.LogWarning("Unknown vehicle type: {0} in SpottedComponent.", type); - Destroy(this); - return; - } - - if (effect.TryGetAsset(out EffectAsset? asset)) - { - Effect = asset; - } - else - { - _logger.LogWarning("SpottedComponent could not initialize: Effect asset not found: {0}.", type); - } - - if (!AllMarkers.Contains(this)) - AllMarkers.Add(this); - - _logger.LogConditional("Spotter initialized: {0}.", this); - } - - private void InitializeCommon(IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - _playerService = serviceProvider.GetRequiredService(); - - _logger = serviceProvider.GetRequiredService>(); - } - - public void MarkTarget(Transform transform, WarfarePlayer spotter, IServiceProvider serviceProvider, bool isUav = false) - { - if (!transform.gameObject.TryGetComponent(out SpottedComponent spotted)) - return; - - ITranslationValueFormatter formatter = serviceProvider.GetRequiredService(); - IPlayerService playerService = serviceProvider.GetRequiredService(); - WarfarePlayer warfarePlayer; - - if (transform.TryGetComponent(out InteractableVehicle vehicle) && vehicle.lockedGroup.m_SteamID != spotter.Team) - { - if (vehicle.transform.TryGetComponent(out VehicleComponent vc) && vc.VehicleData != null) - spotted.TryAnnounce(spotter, formatter.FormatEnum(vc.VehicleData.Type, formatter.LanguageService.GetDefaultLanguage())); - else - spotted.TryAnnounce(spotter, vehicle.asset.vehicleName); - - _logger.LogConditional("Spotting vehicle {0}.", vehicle.asset.vehicleName); - spotted.Activate(spotter, isUav); - } - else if (transform.TryGetComponent(out Player player) && (warfarePlayer = playerService.GetOnlinePlayer(player)).Team != spotter.Team /* todo && !Ghost.IsHidden(warfarePlayer) */) - { - spotted.TryAnnounce(spotter, T.SpottedTargetPlayer.Translate(formatter.LanguageService.GetDefaultLanguage())); - _logger.LogConditional("Spotting player {0}", player.name); - - spotted.Activate(spotter, isUav); - } - //else if (transform.TryGetComponent(out Cache cache) && cache.Team != spotter.GetTeam()) - //{ - // spotted.TryAnnounce(spotter, T.SpottedTargetCache.Translate(formatter.LanguageService.GetDefaultLanguage())); - // _logger.LogConditional("Spotting cache {0}.", cache.Name); - // - // spotted.Activate(spotter, isUav); - //} - else - { - BarricadeDrop drop = BarricadeManager.FindBarricadeByRootTransform(transform); - if (drop == null || drop.GetServersideData().group == spotter.Team) - return; - - spotted.TryAnnounce(spotter, T.SpottedTargetFOB.Translate(formatter.LanguageService.GetDefaultLanguage())); - _logger.LogConditional("Spotting barricade {0}.", drop.asset.itemName); - spotted.Activate(spotter, isUav); - } - } - - public void OnTargetKilled(int assistXP, int assistRep) - { - if (CurrentSpotter == null) - return; - - // todo Points.AwardXP(new XPParameters(CurrentSpotter.Steam64, _team, assistXP) - // { - // OverrideReputationAmount = assistRep, - // Multiplier = 1f, - // Message = PointsConfig.GetDefaultTranslation(CurrentSpotter.Locale.LanguageInfo, CurrentSpotter.Locale.CultureInfo, XPReward.KillAssist), - // Reward = XPReward.KillAssist - // }); - } - - public void Activate(WarfarePlayer spotter, bool isUav) => Activate(spotter, _defaultTimer, isUav); - public void Activate(WarfarePlayer spotter, float seconds, bool isUav) - { - if (this == null) - { - _coroutine = null; - return; - } - EndTime = Time.realtimeSinceStartup + seconds; - if (!isUav) - ToBeUnspottedNonUAV = EndTime; - else - UAVLastKnown = transform.position; - UAVMode = isUav; - if (_coroutine != null) - StopCoroutine(_coroutine); - - CurrentSpotter = spotter; - _team = spotter.Team; - - _coroutine = StartCoroutine(MarkerLoop()); - - // todo if (!isUav) - // todo spotter.ActivateMarker(this); - - _logger.LogConditional("New Spotter activated: " + this); - } - - internal void OnUAVLeft() - { - if (IsActive) - { - if (LastNonUAVSpotter != null && LastNonUAVSpotter.IsOnline && LastNonUAVSpotter.Team != OwnerTeam) - { - EndTime = ToBeUnspottedNonUAV; - CurrentSpotter = LastNonUAVSpotter; - UAVMode = false; - } - else Deactivate(); - } - } - - public void Deactivate() - { - _logger.LogDebug("New Spotter deactivated: {0}.", this); - // todo if (CurrentSpotter != null && CurrentSpotter.IsOnline) - // CurrentSpotter.DeactivateMarker(this); - - if (_coroutine != null) - StopCoroutine(_coroutine); - - _coroutine = null; - CurrentSpotter = null; - ActiveMarkers.Remove(this); - UAVMode = false; - } - internal void SendMarkers() - { - Vector3 pos = UAVMode ? UAVLastKnown : transform.position; - foreach (WarfarePlayer player in _playerService.OnlinePlayers) - { - if (player.Team == _team && (player.Position - pos).sqrMagnitude < 650 * 650) - { - if (Effect != null) - EffectUtility.TriggerEffect(Effect, player.Connection, pos, false); - } - } - } - private void TryAnnounce(WarfarePlayer spotter, string targetName) - { - if (IsActive) - return; - - spotter.SendToast(new ToastMessage(ToastMessageStyle.Mini, T.SpottedToast.Translate(spotter))); - - Team t = spotter.Team; - Color t1 = t.Faction.Color; - // todo targetName = targetName.Colorize(Teams.TeamManager.GetTeamHexColor(Teams.TeamManager.Other(t))); - - foreach (LanguageSet set in _serviceProvider.GetRequiredService().SetOf.PlayersOnTeam(t)) - { - string t2 = T.SpottedMessage.Translate(t1, targetName, in set); - while (set.MoveNext()) - ChatManager.serverSendMessage(t2, Palette.AMBIENT, spotter.SteamPlayer, set.Next.SteamPlayer, EChatMode.SAY, null, true); - } - } - private IEnumerator MarkerLoop() - { - ActiveMarkers.Add(this); - - while (UAVMode || Time.realtimeSinceStartup < EndTime) - { - SendMarkers(); - - yield return new WaitForSeconds(_frequency); - } - - Deactivate(); - } - - [UsedImplicitly] - private void OnDestroy() - { - Deactivate(); - AllMarkers.Remove(this); - // if (!_statInit) - // return; - - //EventDispatcher.EnterVehicle -= OnEnterVehicle; - //EventDispatcher.ExitVehicle -= OnExitVehicle; - //_statInit = false; - } - - public override string ToString() - { - return $"Spotter ({GetInstanceID()}) for {Type}: {(IsActive ? "Spotted" : "Not Spotted")}, CurrentSpotter: {(CurrentSpotter == null ? "null" : CurrentSpotter.Names.CharacterName)}. Under UAV: {(UAVMode ? "Yes" : "No")}, Spotting team: {SpottingTeam}, Owner Team: {OwnerTeam}"; - } - public enum Spotted - { - Infantry, - FOB, - LightVehicle, - Armor, - Aircraft, - Emplacement, - UAV // todo - } -} diff --git a/UncreatedWarfare/Components/VehicleComponent.cs b/UncreatedWarfare/Components/VehicleComponent.cs index c1cc31d3..378a5657 100644 --- a/UncreatedWarfare/Components/VehicleComponent.cs +++ b/UncreatedWarfare/Components/VehicleComponent.cs @@ -9,7 +9,6 @@ using Uncreated.Warfare.Players.UI; using Uncreated.Warfare.Util; using Uncreated.Warfare.Util.List; -using Uncreated.Warfare.Vehicles; using Uncreated.Warfare.Vehicles.Info; using Uncreated.Warfare.Vehicles.Spawners; using Uncreated.Warfare.Vehicles.UI; @@ -139,11 +138,8 @@ public void Initialize(InteractableVehicle vehicle, IServiceProvider serviceProv if (vehicleInfoStore != null) { VehicleData = vehicleInfoStore.GetVehicleInfo(Vehicle.asset); - if (VehicleData != null) - { - vehicle.transform.gameObject.GetOrAddComponent().Initialize(VehicleData.Type, vehicle, serviceProvider); - } } + _lastPosInterval = transform.position; #if false // todo diff --git a/UncreatedWarfare/Components/WarfareTimeComponent.cs b/UncreatedWarfare/Components/WarfareTimeComponent.cs index 59d30572..8ccabede 100644 --- a/UncreatedWarfare/Components/WarfareTimeComponent.cs +++ b/UncreatedWarfare/Components/WarfareTimeComponent.cs @@ -1,5 +1,5 @@ namespace Uncreated.Warfare.Components; -internal sealed class WarfareTimeComponent : MonoBehaviour +public sealed class WarfareTimeComponent : MonoBehaviour { public uint Updates { get; private set; } public uint Ticks { get; private set; } diff --git a/UncreatedWarfare/EventFunctions.cs b/UncreatedWarfare/EventFunctions.cs index 5f4e2eab..d3f0c570 100644 --- a/UncreatedWarfare/EventFunctions.cs +++ b/UncreatedWarfare/EventFunctions.cs @@ -142,50 +142,6 @@ internal static void OnBarricadePlaced(BarricadeRegion region, BarricadeDrop dro // if (firer is null) // return; // - // if (gun.isAiming && Gamemode.Config.ItemLaserDesignator.MatchGuid(gun.equippedGunAsset.GUID)) - // { - // float grndDist = float.NaN; - // if (Physics.Raycast(projectile.transform.position, projectile.transform.up, out RaycastHit hit, length, - // rayMask)) - // { - // if (hit.transform != null) - // { - // if ((ELayerMask)hit.transform.gameObject.layer is ELayerMask.GROUND or ELayerMask.GROUND2 - // or ELayerMask.LARGE or ELayerMask.MEDIUM or ELayerMask.SMALL) - // grndDist = (projectile.transform.position - hit.transform.position).sqrMagnitude; - // else - // { - // SpottedComponent.MarkTarget(hit.transform, firer); - // return; - // } - // } - // } - // - // List hits = new List(Physics.SphereCastAll(projectile.transform.position, radius, - // projectile.transform.up, length, rayMaskBackup)); - // Vector3 strtPos = projectile.transform.position; - // hits.RemoveAll( - // x => - // { - // if (x.transform == null || !x.transform.gameObject.TryGetComponent(out _)) - // return true; - // float dist = (x.transform.position - strtPos).sqrMagnitude; - // return dist < radius * radius + 1 || dist > grndDist; - // }); - // if (hits.Count == 0) return; - // if (hits.Count == 1) - // { - // SpottedComponent.MarkTarget(hits[0].transform, firer); - // return; - // } - // hits.Sort((a, b) => (strtPos - b.point).sqrMagnitude.CompareTo((strtPos - a.point).sqrMagnitude)); - // hits.Sort((a, _) => (ELayerMask)a.transform.gameObject.layer is ELayerMask.PLAYER ? -1 : 1); - // - // SpottedComponent.MarkTarget(hits[0].transform, firer); - // UnityEngine.Object.Destroy(projectile); - // return; - // } - // // Rocket[] rockets = projectile.GetComponentsInChildren(true); // foreach (Rocket rocket in rockets) // { @@ -205,7 +161,6 @@ internal static void OnBarricadePlaced(BarricadeRegion region, BarricadeDrop dro // projectile.AddComponent().Initialize(projectile, firer, 150, 1.15f, 150, 15, 0.6f); // } // - // Patches.DeathsPatches.lastProjected = projectile; // if (!gun.player.TryGetPlayerData(out WarfarePlayerData c)) // return; // @@ -239,34 +194,6 @@ internal static void OnBarricadePlaced(BarricadeRegion region, BarricadeDrop dro // c.LastGunShot = gun.equippedGunAsset.GUID; // } // } - // internal static void ReloadCommand_onTranslationsReloaded() - // { - // foreach (WarfarePlayer player in PlayerManager.OnlinePlayers) - // UCWarfare.I.UpdateLangs(player, false); - // } - // - // internal static void OnLandmineExploding(TriggerTrapRequested e) - // { - // if (!e.IsExplosive) - // return; - // - // if (e.TriggeringPlayer is { VanishMode: true }) - // { - // e.Cancel(); - // return; - // } - // - // if (UCWarfare.Config.BlockLandmineFriendlyFire && e.TriggeringTeam == e.ServersideData.group.GetTeam()) - // { - // // allow players to trigger their own landmines with throwables - // if (e.TriggeringPlayer == null || e.TriggeringPlayer.Steam64 != e.ServersideData.owner || e.TriggeringThrowable == null) - // e.Cancel(); - // } - // else if (!CheckLandminePosition(e.ServersideData.point)) - // { - // e.Cancel(); - // } - // } // public static void OnPickedUpItemRequested(Player player, byte x, byte y, uint instanceId, byte toX, byte toY, byte toRotation, byte toPage, ItemData itemData, ref bool shouldAllow) // { // WarfarePlayer? ucplayer = WarfarePlayer.FromPlayer(player); diff --git a/UncreatedWarfare/Events/EventDispatcher.cs b/UncreatedWarfare/Events/EventDispatcher.cs index cd691ce8..8b4c426a 100644 --- a/UncreatedWarfare/Events/EventDispatcher.cs +++ b/UncreatedWarfare/Events/EventDispatcher.cs @@ -605,7 +605,9 @@ private void FindEventListenerProviders(ILifetimeScope scope, List(ILifetimeScope scope, List _logger; private readonly TrackingList _entities; private readonly TrackingList _fobs; + private readonly ChatService _chatService; public readonly FobConfiguration Configuration; @@ -44,6 +46,7 @@ public FobManager(IServiceProvider serviceProvider, ILogger logger) Configuration = serviceProvider.GetRequiredService(); _translations = serviceProvider.GetRequiredService>().Value; _assetConfiguration = serviceProvider.GetRequiredService(); + _chatService = serviceProvider.GetRequiredService(); _serviceProvider = serviceProvider; _logger = logger; _fobs = new TrackingList(24); diff --git a/UncreatedWarfare/FOBs/FobEvents.cs b/UncreatedWarfare/FOBs/FobEvents.cs index 374ca7ac..71c2c65f 100644 --- a/UncreatedWarfare/FOBs/FobEvents.cs +++ b/UncreatedWarfare/FOBs/FobEvents.cs @@ -1,38 +1,66 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using SDG.Framework.Water; using System; using System.Collections.Generic; using System.Linq; using Uncreated.Warfare.Buildables; -using Uncreated.Warfare.Commands; using Uncreated.Warfare.Configuration; using Uncreated.Warfare.Events.Models; using Uncreated.Warfare.Events.Models.Barricades; using Uncreated.Warfare.Events.Models.Fobs; using Uncreated.Warfare.Events.Models.Items; -using Uncreated.Warfare.Events.Models.Players; using Uncreated.Warfare.Events.Models.Vehicles; using Uncreated.Warfare.FOBs; using Uncreated.Warfare.FOBs.Construction; using Uncreated.Warfare.FOBs.Entities; using Uncreated.Warfare.FOBs.SupplyCrates; -using Uncreated.Warfare.Interaction; -using Uncreated.Warfare.Kits; using Uncreated.Warfare.Layouts.Teams; -using Uncreated.Warfare.Translations; using Uncreated.Warfare.Util; -using Uncreated.Warfare.Util.Containers; using Uncreated.Warfare.Util.Timing; -using Uncreated.Warfare.Zones; namespace Uncreated.Warfare.Fobs; public partial class FobManager : IAsyncEventListener, + IEventListener, IEventListener, IEventListener, - IEventListener + IEventListener, + IEventListener { + private bool IsTrapTooNearFobSpawn(in Vector3 pos) + { + const float maxDistance = 10; + + foreach (BuildableFob fob in Fobs.OfType()) + { + if (MathUtility.WithinRange2D(in pos, fob.SpawnPosition, maxDistance)) + { + return true; + } + } + + return false; + } + + void IEventListener.HandleEvent(PlaceBarricadeRequested e, IServiceProvider serviceProvider) + { + // dont allow placing traps near spawner + if (e.Asset is not ItemTrapAsset || !IsTrapTooNearFobSpawn(e.Position)) + return; + + if (e.OriginalPlacer != null) + _chatService.Send(e.OriginalPlacer, _translations.BuildableNotAllowed); + + e.Cancel(); + } + + void IEventListener.HandleEvent(TriggerTrapRequested e, IServiceProvider serviceProvider) + { + Vector3 pos = e.Barricade.GetServersideData().point; + if (IsTrapTooNearFobSpawn(in pos)) + e.Cancel(); + } + async UniTask IAsyncEventListener.HandleEventAsync(BarricadePlaced e, IServiceProvider serviceProvider, CancellationToken token) { await UniTask.NextFrame(); @@ -195,4 +223,4 @@ public void HandleEvent(VehicleDespawned e, IServiceProvider serviceProvider) if (emplacement != null) DeregisterFobEntity(emplacement); } -} +} \ No newline at end of file diff --git a/UncreatedWarfare/Interaction/Icons/WorldIconInfo.cs b/UncreatedWarfare/Interaction/Icons/WorldIconInfo.cs index 1ca54519..d3a09e4d 100644 --- a/UncreatedWarfare/Interaction/Icons/WorldIconInfo.cs +++ b/UncreatedWarfare/Interaction/Icons/WorldIconInfo.cs @@ -54,12 +54,38 @@ public class WorldIconInfo : ITransformObject, IDisposable /// /// The source of the effect's position. Interchangable with or . /// - public ITransformObject? TransformableObject { get; private set; } + /// This can be changed at any time. + public ITransformObject? TransformableObject + { + get; + set + { + field = value; + if (value is not null) + { + UnityObject = null; + _position = default; + } + } + } /// /// The source of the effect's position. Interchangable with or . /// - public Transform? UnityObject { get; private set; } + /// This can be changed at any time. + public Transform? UnityObject + { + get; + set + { + field = value; + if (value is not null) + { + TransformableObject = null; + _position = default; + } + } + } /// /// The source of the effect's position. Interchangable with or . @@ -107,7 +133,7 @@ public Color32 Color /// /// Number of seconds this effect will be alive. /// - public float LifetimeSeconds { get; } + public float LifetimeSeconds { get; private set; } /// /// Number of seconds between updates. @@ -117,19 +143,19 @@ public Color32 Color public bool Alive { get; internal set; } - public WorldIconInfo(Transform transform, IAssetLink effect, Team? targetTeam = null, WarfarePlayer? targetPlayer = null, Func? playerSelector = null, float lifetimeSec = default) + public WorldIconInfo(Transform transform, IAssetLink effect, Team? targetTeam = null, WarfarePlayer? targetPlayer = null, Func? playerSelector = null, float lifetimeSec = 0) : this(effect, targetTeam, targetPlayer, playerSelector, lifetimeSec) { UnityObject = transform; } - public WorldIconInfo(ITransformObject transform, IAssetLink effect, Team? targetTeam = null, WarfarePlayer? targetPlayer = null, Func? playerSelector = null, float lifetimeSec = default) + public WorldIconInfo(ITransformObject transform, IAssetLink effect, Team? targetTeam = null, WarfarePlayer? targetPlayer = null, Func? playerSelector = null, float lifetimeSec = 0) : this(effect, targetTeam, targetPlayer, playerSelector, lifetimeSec) { TransformableObject = transform; } - public WorldIconInfo(Vector3 startPosition, IAssetLink effect, Team? targetTeam = null, WarfarePlayer? targetPlayer = null, Func? playerSelector = null, float lifetimeSec = default) + public WorldIconInfo(Vector3 startPosition, IAssetLink effect, Team? targetTeam = null, WarfarePlayer? targetPlayer = null, Func? playerSelector = null, float lifetimeSec = 0) : this(effect, targetTeam, targetPlayer, playerSelector, lifetimeSec) { _position = startPosition; @@ -141,7 +167,7 @@ private WorldIconInfo(IAssetLink effect, Team? targetTeam, WarfareP TargetTeam = targetTeam; TargetPlayer = targetPlayer; PlayerSelector = playerSelector; - LifetimeSeconds = lifetimeSec == 0 ? float.MaxValue : 0; + LifetimeSeconds = lifetimeSec == 0 || !float.IsFinite(lifetimeSec) ? float.MaxValue : lifetimeSec; if (!Effect.TryGetAsset(out EffectAsset? asset) || asset.lifetime <= 0) return; @@ -151,6 +177,14 @@ private WorldIconInfo(IAssetLink effect, Team? targetTeam, WarfareP _minimumLifetime = asset.lifetime - asset.lifetimeSpread; } + /// + /// Raise such that from now the effect will despawn in a given amount of seconds. + /// + public void KeepAliveFor(float seconds) + { + LifetimeSeconds = FirstSpawnRealtime == 0 ? seconds : Time.realtimeSinceStartup - FirstSpawnRealtime + seconds; + } + internal void UpdateRelevantPlayers(IPlayerService playerService, ref PooledTransportConnectionList? list, ref ITransportConnection? single, HashSet workingHashSetCache) { if (TargetPlayer != null) diff --git a/UncreatedWarfare/Interaction/Icons/WorldIconManager.cs b/UncreatedWarfare/Interaction/Icons/WorldIconManager.cs index 0477ddb9..06b5c687 100644 --- a/UncreatedWarfare/Interaction/Icons/WorldIconManager.cs +++ b/UncreatedWarfare/Interaction/Icons/WorldIconManager.cs @@ -156,7 +156,9 @@ public void RemoveAllIcons() /// /// Removes a single icon. /// + #nullable disable public void RemoveIcon(WorldIconInfo icon) + #nullable restore { if (RemoveIconIntl(icon)) { @@ -167,7 +169,7 @@ public void RemoveIcon(WorldIconInfo icon) /// If tick speed needs to be rechecked. private bool RemoveIconIntl(WorldIconInfo icon) { - if (!_allIcons.Remove(icon)) + if (icon == null || !_allIcons.Remove(icon)) return false; Guid guid = icon.Effect.Guid; @@ -183,6 +185,7 @@ private bool RemoveIconIntl(WorldIconInfo icon) bool needsRecheck = _greatestTickSpeed <= icon.TickSpeed || icon.TickSpeed >= _lowestTickSpeed; if (!icon.NeedsToBeCleared(rt)) { + icon.Dispose(); return needsRecheck; } @@ -196,6 +199,7 @@ private bool RemoveIconIntl(WorldIconInfo icon) else if (singlePlayer != null) EffectManager.ClearEffectByGuid(guid, singlePlayer); + icon.Dispose(); return needsRecheck; } @@ -248,18 +252,33 @@ public void UpdateIcon(Guid guid) } } - private static bool IsInactive(WorldIconInfo info, float rt) + private bool IsInactive(WorldIconInfo info, float rt) { if (!info.Alive) + { + Log(" | Inactive - not alive: {0}", info); return true; + } if (info.TransformableObject is { Alive: false }) + { + Log(" | Inactive - transformable not alive: {0}", info); return true; + } if (info.UnityObject is not null && info.UnityObject == null) + { + Log(" | Inactive - unity object not alive: {0}", info); return true; + } - return info.FirstSpawnRealtime > 0 && info.FirstSpawnRealtime + info.LifetimeSeconds < rt; + if (info.FirstSpawnRealtime > 0 && info.FirstSpawnRealtime + info.LifetimeSeconds < rt) + { + Log(" | Inactive - lifetime expired: {0} ({1} + {2})", info, info.FirstSpawnRealtime, info.LifetimeSeconds); + return true; + } + + return false; } /// @@ -437,6 +456,7 @@ private void RemovePlayerSpecificIcons(WarfarePlayer player, Team? team) List? toRemove = null; List? toUpdate = null; + float rt = Time.realtimeSinceStartup; foreach (List list in _iconsByGuid.Values) { diff --git a/UncreatedWarfare/Moderation/Reports/ReportService.cs b/UncreatedWarfare/Moderation/Reports/ReportService.cs index 27cb0d37..5c7fd69f 100644 --- a/UncreatedWarfare/Moderation/Reports/ReportService.cs +++ b/UncreatedWarfare/Moderation/Reports/ReportService.cs @@ -21,6 +21,7 @@ namespace Uncreated.Warfare.Moderation.Reports; [RpcClass(DefaultTypeName = "Uncreated.Web.Bot.Services.DiscordReportService, uncreated-web")] +[Priority(-100)] public class ReportService : IDisposable, IHostedService, IEventListener, IEventListener, IEventListener { private delegate float GetBulletDamageMultiplierHandler(ref BulletInfo bullet); @@ -90,7 +91,6 @@ private void OnProjectileSpawned(UseableGun gun, GameObject projectile) { PlayerData playerData = GetOrAddPlayerData(_playerService.GetOnlinePlayer(gun)); CheckExpiredBullets(playerData, gun); - } private void OnBulletSpawned(UseableGun gun, BulletInfo bullet) @@ -118,6 +118,13 @@ private void OnBulletHit(UseableGun gun, BulletInfo bullet, InputInfo hit, ref b if (pendingBullet.Bullet != bullet) continue; + playerData.PendingBullets.RemoveAt(i); + if (!shouldallow) + { + _logger.LogInformation("Bullet cancelled: {0} {1}", bullet.magazineAsset, bullet.pellet); + return; + } + ItemGunAsset gunAsset = gun.equippedGunAsset; IModerationActor? actor = hit.player != null ? Actors.GetActor(hit.player.channel.owner.playerID.steamID) : null; @@ -125,7 +132,6 @@ private void OnBulletHit(UseableGun gun, BulletInfo bullet, InputInfo hit, ref b InteractableObjectRubble? rubbleObject = hit.type == ERaycastInfoType.OBJECT ? hit.transform.GetComponentInParent() : null; - Asset? hitAsset = hit.type switch { ERaycastInfoType.ANIMAL => hit.animal.asset, @@ -230,7 +236,6 @@ private void OnBulletHit(UseableGun gun, BulletInfo bullet, InputInfo hit, ref b _logger.LogInformation("Bullet hit: {0} {1} dmg: {2} asset: {3}", bullet.magazineAsset, bullet.pellet, damage, hitAsset); - playerData.PendingBullets.RemoveAt(i); playerData.AddShot(new ShotRecord(gunAsset.GUID, bullet.magazineAsset.GUID, gunAsset.itemName, bullet.magazineAsset.itemName, diff --git a/UncreatedWarfare/Patches/DeathsPatches.cs b/UncreatedWarfare/Patches/DeathsPatches.cs index 038decdc..25cb3a38 100644 --- a/UncreatedWarfare/Patches/DeathsPatches.cs +++ b/UncreatedWarfare/Patches/DeathsPatches.cs @@ -1,11 +1,4 @@ using HarmonyLib; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using System.Reflection.Emit; -using Uncreated.Warfare.Components; -using Uncreated.Warfare.Fobs; -using Uncreated.Warfare.Traits.Buffs; // ReSharper disable InconsistentNaming // ReSharper disable UnusedMember.Local @@ -19,48 +12,8 @@ public static partial class Patches public static class DeathsPatches { internal static GameObject? lastProjected; - // SDG.Unturned.UseableGun #if false - /// - /// Postfix of to predict mortar hits. - /// - [SuppressMessage(Data.SuppressCategory, Data.SuppressID)] - [HarmonyPatch(typeof(UseableGun), "project")] - [HarmonyPostfix] - private static void OnPostProjected(Vector3 origin, Vector3 direction, ItemBarrelAsset barrelAsset, ItemMagazineAsset magazineAsset, UseableGun __instance) - { - if (lastProjected != null && lastProjected.activeInHierarchy && __instance.equippedGunAsset.isTurret && FOBManager.Loaded) - { - BuildableData? data = FOBManager.Config.Buildables.Find(x => - x.Emplacement is not null && (x.Emplacement.ShouldWarnFriendlies || x.Emplacement.ShouldWarnEnemies) && - x.Emplacement.EmplacementVehicle.Exists && - x.Emplacement.EmplacementVehicle.Asset!.turrets.Any(y => - y.itemID == __instance.equippedGunAsset.id)); - if (data != null) - { - UCWarfare.I.Solver.GetLandingPoint(lastProjected, origin, direction, __instance, OnMortarLandingPointFound); - } - } - lastProjected = null; - } - - private static void OnMortarLandingPointFound(Player? owner, Vector3 position, float impactTime, ItemGunAsset gun, ItemMagazineAsset? ammoType) - { - if (owner == null || ammoType == null) - return; - BuildableData? data = !FOBManager.Loaded ? null : FOBManager.Config.Buildables.Find(x => - x.Emplacement is not null && (x.Emplacement.ShouldWarnFriendlies || x.Emplacement.ShouldWarnEnemies) && - x.Emplacement.EmplacementVehicle.Exists && - x.Emplacement.EmplacementVehicle.Asset!.turrets.Any(y => - y.itemID == gun.id)); - - if (data == null) return; - - UCPlayer? player = UCPlayer.FromPlayer(owner); - if (player != null) - BadOmen.TryWarn(player, position, impactTime, gun, ammoType, data.Emplacement!.ShouldWarnFriendlies, data.Emplacement!.ShouldWarnEnemies); - } // SDG.Unturned.Bumper /// Adds the id of the vehicle that hit the player to their pt component. diff --git a/UncreatedWarfare/Patches/FobEmplacementWarningPatch.cs b/UncreatedWarfare/Patches/FobEmplacementWarningPatch.cs new file mode 100644 index 00000000..19121c6b --- /dev/null +++ b/UncreatedWarfare/Patches/FobEmplacementWarningPatch.cs @@ -0,0 +1,200 @@ +using DanielWillett.ReflectionTools; +using DanielWillett.ReflectionTools.Formatting; +using Microsoft.Extensions.Configuration; +using SDG.Framework.Utilities; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using Uncreated.Warfare.Fobs; +using Uncreated.Warfare.FOBs.Construction; +using Uncreated.Warfare.Layouts.Teams; +using Uncreated.Warfare.Players; +using Uncreated.Warfare.Players.Management; +using Uncreated.Warfare.Players.UI; +using Uncreated.Warfare.Projectiles; +using Uncreated.Warfare.Proximity; +using Uncreated.Warfare.Translations; + +namespace Uncreated.Warfare.Patches; + +[UsedImplicitly] +internal sealed class FobEmplacementWarningPatch : IHarmonyPatch +{ + private static MethodInfo? _target; + void IHarmonyPatch.Patch(ILogger logger, HarmonyLib.Harmony patcher) + { + _target = typeof(UseableGun).GetMethod("project", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + if (_target != null) + { + patcher.Patch(_target, postfix: Accessor.GetMethod(Postfix)); + logger.LogDebug("Patched {0} for adding warning.", _target); + UseableGun.onProjectileSpawned += UseableGunOnProjectileSpawned; + return; + } + + logger.LogError("Failed to find method: {0}.", + new MethodDefinition("project") + .DeclaredIn(isStatic: false) + .WithNoParameters() + .ReturningVoid() + ); + } + + void IHarmonyPatch.Unpatch(ILogger logger, HarmonyLib.Harmony patcher) + { + if (_target == null) + return; + + patcher.Unpatch(_target, Accessor.GetMethod(Postfix)); + logger.LogDebug("Unpatched {0} for cancelling vanilla reputation.", _target); + UseableGun.onProjectileSpawned -= UseableGunOnProjectileSpawned; + _target = null; + } + + private static void UseableGunOnProjectileSpawned(UseableGun sender, GameObject projectile) + { + LastProjectileObject = projectile; + } + + internal static GameObject? LastProjectileObject; + + // SDG.Unturned.UseableGun + /// + /// Postfix of to predict mortar hits. + /// + [SuppressMessage(Data.SuppressCategory, Data.SuppressID)] + private static void Postfix(Vector3 origin, Vector3 direction, ItemBarrelAsset barrelAsset, ItemMagazineAsset magazineAsset, UseableGun __instance) + { + if (!WarfareModule.Singleton.IsLayoutActive()) + { + LastProjectileObject = null; + return; + } + + ILifetimeScope serviceProvider = WarfareModule.Singleton.GetActiveLayout().ServiceProvider; + + if (serviceProvider.TryResolve(out FobManager? fobManager) + && LastProjectileObject is { activeInHierarchy: true } + && __instance.equippedGunAsset.isTurret) + { + // find emplacement from turret item ID + ShovelableInfo? shovelableInfo = + (fobManager.Configuration.GetRequiredSection("Shovelables").Get>() + ?? Array.Empty()) + .FirstOrDefault(s => s.Emplacement != null + && (s.Emplacement.ShouldWarnEnemies || s.Emplacement.ShouldWarnFriendlies) + && s.Emplacement.Vehicle.GetAsset() is { } vehicle + && vehicle.turrets.Any(y => y.itemID == __instance.equippedGunAsset.id) + ); + + if (shovelableInfo != null) + { + serviceProvider.Resolve().BeginSolvingProjectile(LastProjectileObject, origin, direction, __instance, + (owner, position, time, gun, type) => OnMortarLandingPointFound(shovelableInfo, owner, position, time, gun, type) + ); + } + } + + LastProjectileObject = null; + } + + private static void OnMortarLandingPointFound( + ShovelableInfo info, + WarfarePlayer? owner, + Vector3 position, + float impactTime, + ItemGunAsset gun, + ItemMagazineAsset? ammoType) + { + float warnRadius = gun.range; + if (ammoType != null) + { + warnRadius *= ammoType.projectileBlastRadiusMultiplier; + } + + warnRadius += 5; + + GameObject hitPoint = new GameObject("hit_proj_overlap"); + ColliderProximity proximity = hitPoint.AddComponent(); + + Team team = owner?.Team ?? Team.NoTeam; + + LandingZone lz = new LandingZone(info.Emplacement!, team); + + proximity.Initialize( + new SphereProximity(in position, warnRadius), + WarfareModule.Singleton.ServiceProvider.Resolve(), + false, + validationCheck: lz.ValidationCheck + ); + + proximity.OnObjectEntered += lz.PlayerEntered; + proximity.OnObjectExited += lz.PlayerExited; + + lz.Proximity = proximity; + + TimeUtility.InvokeAfterDelay(lz.Destroy, impactTime - Time.realtimeSinceStartup); + } + + [PlayerComponent] + private class LandingZonesComponent : IPlayerComponent + { + public int NumLandingZones; + public required WarfarePlayer Player { get; set; } + public void Init(IServiceProvider serviceProvider, bool isOnJoin) { } + } + + private class LandingZone + { + public ColliderProximity? Proximity; + + private readonly EmplacementInfo _emplacement; + private readonly Team _friendlyTeam; + + public LandingZone(EmplacementInfo emplacement, Team friendlyTeam) + { + _emplacement = emplacement; + _friendlyTeam = friendlyTeam; + } + + public void PlayerEntered(WarfarePlayer player) + { + LandingZonesComponent zones = player.Component(); + + // can be in multiple at once + if (Interlocked.Increment(ref zones.NumLandingZones) == 1) + { + player.SendToast(new ToastMessage(ToastMessageStyle.FlashingWarning, + WarfareModule.Singleton.ServiceProvider + .Resolve>().Value.MortarWarning.Translate(player)) + ); + } + } + + public void PlayerExited(WarfarePlayer player) + { + LandingZonesComponent zones = player.Component(); + + // can be in multiple at once + if (Interlocked.Decrement(ref zones.NumLandingZones) <= 0) + { + player.Component().SkipExpiration(ToastMessageStyle.FlashingWarning); + } + } + + public bool ValidationCheck(WarfarePlayer player) + { + bool friendly = player.Team.IsFriendly(_friendlyTeam); + return !friendly && _emplacement.ShouldWarnEnemies || + friendly && _emplacement.ShouldWarnFriendlies; + } + + public void Destroy() + { + Proximity?.Dispose(); + } + } +} \ No newline at end of file diff --git a/UncreatedWarfare/Players/PlayersTranslations.cs b/UncreatedWarfare/Players/PlayersTranslations.cs new file mode 100644 index 00000000..3472838b --- /dev/null +++ b/UncreatedWarfare/Players/PlayersTranslations.cs @@ -0,0 +1,86 @@ +using Uncreated.Warfare.FOBs.Deployment; +using Uncreated.Warfare.Kits; +using Uncreated.Warfare.Translations; +using Uncreated.Warfare.Translations.Addons; +using Uncreated.Warfare.Translations.Util; +using Uncreated.Warfare.Zones; + +namespace Uncreated.Warfare.Players; +public sealed class PlayersTranslations : PropertiesTranslationCollection +{ + // todo: some of these are unused + + protected override string FileName => "Players"; + + [TranslationData("Gets broadcasted when a player connects.", "Connecting player")] + public readonly Translation PlayerConnected = new Translation("<#e6e3d5>{0} joined the server."); + + [TranslationData("Gets broadcasted when a player disconnects.", "Disconnecting player")] + public readonly Translation PlayerDisconnected = new Translation("<#e6e3d5>{0} left the server."); + + [TranslationData("Kick message for a player that suffers from a rare bug which will cause GameObject.get_transform() to throw a NullReferenceException (not return null). They are kicked if this happens.", "Discord Join Code")] + public readonly Translation NullTransformKickMessage = new Translation("Your character is bugged, which messes up our zone plugin. Rejoin or contact a Director if this continues. (discord.gg/{0})."); + + [TranslationData("Gets sent to a player who is attempting to main camp the other team.")] + public readonly Translation AntiMainCampWarning = new Translation("<#fa9e9e>Stop <#ff3300>main-camping! Damage is reversed back on you."); + + [TranslationData("Gets sent to a player who is trying to place a non-whitelisted barricade on a vehicle.", "Barricade being placed")] + public readonly Translation NoPlacementOnVehicle = new Translation("<#fa9e9e>You can't place {0} on a vehicle!", arg0Fmt: new ArgumentFormat(PluralAddon.Always(), RarityColorAddon.Instance)); + + [TranslationData("Generic message sent when a player is placing something in a place they shouldn't.", "Item being placed")] + public readonly Translation ProhibitedPlacement = new Translation("<#fa9e9e>You're not allowed to place {0} here.", arg0Fmt: new ArgumentFormat(PluralAddon.Always(), RarityColorAddon.Instance)); + + [TranslationData("Generic message sent when a player is dropping an item where they shouldn't.", "Item being dropped", "Zone or flag the player is dropping their item in.")] + public readonly Translation ProhibitedDropZone = new Translation("<#fa9e9e>You're not allowed to drop {0} in {1}.", arg0Fmt: new ArgumentFormat(PluralAddon.Always(), RarityColorAddon.Instance), arg1Fmt: Flags.ColorNameDiscoverFormat); + + [TranslationData("Generic message sent when a player is picking up an item where they shouldn't.", "Item being picked up", "Zone or flag the player is picking up their item in.")] + public readonly Translation ProhibitedPickupZone = new Translation("<#fa9e9e>You're not allowed to pick up {0} in {1}.", arg0Fmt: new ArgumentFormat(PluralAddon.Always(), RarityColorAddon.Instance), arg1Fmt: Flags.ColorNameDiscoverFormat); + + [TranslationData("Generic message sent when a player is placing something in a zone they shouldn't be.", "Item being placed", "Zone or flag the player is placing their item in.")] + public readonly Translation ProhibitedPlacementZone = new Translation("<#fa9e9e>You're not allowed to place {0} in {1}.", arg0Fmt: new ArgumentFormat(PluralAddon.Always(), RarityColorAddon.Instance), arg1Fmt: Flags.ColorNameDiscoverFormat); + + [TranslationData("Sent when a player tries to steal a battery.")] + public readonly Translation NoStealingBatteries = new Translation("<#fa9e9e>Stealing batteries is not allowed."); + + [TranslationData("Sent when a player tries to manually leave their group.")] + public readonly Translation NoLeavingGroup = new Translation("<#fa9e9e>You are not allowed to manually change groups, use <#cedcde>/teams instead."); + + [TranslationData("Message sent when a player tries to place a non-whitelisted item in a storage inventory.", "Item being stored")] + public readonly Translation ProhibitedStoring = new Translation("<#fa9e9e>You are not allowed to store {0}.", arg0Fmt: new ArgumentFormat(PluralAddon.Always(), RarityColorAddon.Instance)); + + [TranslationData("Sent when a player tries to point or mark while not a squad leader.")] + public readonly Translation MarkerNotInSquad = new Translation("<#fa9e9e>Only your squad can see markers. Create a squad with <#cedcde>/squad create to use this feature."); + + [TranslationData("Sent on a SEVERE toast when the player enters enemy territory.", "Seconds until death")] + public readonly Translation EnteredEnemyTerritory = new Translation("ENEMY HQ PROXIMITY\nLEAVE IMMEDIATELY\nDEAD IN {0}", TranslationOptions.UnityUI); + + [TranslationData("Sent 2 times before a player is kicked for inactivity.", "Time code")] + public readonly Translation InactivityWarning = new Translation("<#fa9e9e>You will be AFK-Kicked in <#cedcde>{0} if you don't move."); + + [TranslationData("Broadcasted when a player is removed from the game by BattlEye.", "Player being kicked.")] + public readonly Translation BattlEyeKickBroadcast = new Translation("<#00ffff><#d8addb>{0} was kicked by <#feed00>BattlEye.", arg0Fmt: WarfarePlayer.FormatPlayerName); + + [TranslationData("Sent when an unauthorized player attempts to edit a sign.")] + public readonly Translation ProhibitedSignEditing = new Translation("<#ff8c69>You are not allowed to edit that sign."); + + [TranslationData("Sent when a player tries to craft a blacklisted blueprint.")] + public readonly Translation NoCraftingBlueprint = new Translation("<#b3a6a2>Crafting is disabled for this item."); + + [TranslationData("Shows above the XP UI when divisions are enabled.", "Branch (Division) the player is a part of.")] + public readonly Translation XPUIDivision = new Translation("{0} Division"); + + [TranslationData("Tells the player that the game detected they have started nitro boosting.")] + public readonly Translation StartedNitroBoosting = new Translation("<#e00ec9>Thank you for nitro boosting! In-game perks have been activated."); + + [TranslationData("Tells the player that the game detected they have stopped nitro boosting.")] + public readonly Translation StoppedNitroBoosting = new Translation("<#9b59b6>Your nitro boost(s) have expired. In-game perks have been deactivated."); + + [TranslationData("Tells the player that they can't remove clothes which have item storage.")] + public readonly Translation NoRemovingClothing = new Translation("<#b3a6a2>You can not remove clothes with storage from your kit."); + + [TranslationData("Tells the player that they can't unlock vehicles from the vehicle bay.")] + public readonly Translation UnlockVehicleNotAllowed = new Translation("<#b3a6a2>You can not unlock a requested vehicle."); + + [TranslationData("Goes on the warning UI.")] + public readonly Translation MortarWarning = new Translation("FRIENDLY MORTAR\nINCOMING", TranslationOptions.TMProUI); +} \ No newline at end of file diff --git a/UncreatedWarfare/Players/WarfarePlayer.cs b/UncreatedWarfare/Players/WarfarePlayer.cs index 6fb8c464..3df31215 100644 --- a/UncreatedWarfare/Players/WarfarePlayer.cs +++ b/UncreatedWarfare/Players/WarfarePlayer.cs @@ -10,6 +10,7 @@ using Uncreated.Warfare.Moderation; using Uncreated.Warfare.Players.Management; using Uncreated.Warfare.Players.Saves; +using Uncreated.Warfare.Squads.Spotted; using Uncreated.Warfare.Stats; using Uncreated.Warfare.Steam.Models; using Uncreated.Warfare.Translations; @@ -30,7 +31,15 @@ public interface IPlayer : ITranslationArgument } [CannotApplyEqualityOperator] -public class WarfarePlayer : IPlayer, ICommandUser, IModerationActor, IComponentContainer, IEquatable, IEquatable, ITransformObject +public class WarfarePlayer : + IPlayer, + ICommandUser, + IModerationActor, + IComponentContainer, + IEquatable, + IEquatable, + ITransformObject, + ISpotter { private readonly CancellationTokenSource _disconnectTokenSource; private readonly ILogger _logger; @@ -182,6 +191,7 @@ public void ApplyOfflineState() _disconnectTokenSource.Cancel(); IsDisconnecting = false; IsOnline = false; + OnDestroyed?.Invoke(this); } finally { @@ -334,4 +344,13 @@ ValueTask IModerationActor.GetDisplayName(DatabaseInterface database, Ca { throw new NotImplementedException(); } + + bool ISpotter.IsTrackable => true; + private event Action? OnDestroyed; + + event Action? ISpotter.OnDestroyed + { + add => OnDestroyed += value; + remove => OnDestroyed -= value; + } } \ No newline at end of file diff --git a/UncreatedWarfare/Projectiles/ProjectileSolver.cs b/UncreatedWarfare/Projectiles/ProjectileSolver.cs index 4bab8fc7..81a48097 100644 --- a/UncreatedWarfare/Projectiles/ProjectileSolver.cs +++ b/UncreatedWarfare/Projectiles/ProjectileSolver.cs @@ -1,188 +1,271 @@ -using DanielWillett.ReflectionTools; +#if DEBUG + +#define PROJECTILE_TRACERS + +#endif + +using DanielWillett.ReflectionTools; using SDG.Framework.Landscapes; +using SDG.Framework.Utilities; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Reflection; using Uncreated.Warfare.Components; +using Uncreated.Warfare.Events.Models; +using Uncreated.Warfare.Events.Models.Players; +using Uncreated.Warfare.Players; +using Uncreated.Warfare.Players.Management; +using Uncreated.Warfare.Services; using Uncreated.Warfare.Util; +using Uncreated.Warfare.Util.List; using UnityEngine.SceneManagement; namespace Uncreated.Warfare.Projectiles; -public class ProjectileSolver : MonoBehaviour +public class ProjectileSolver : ILevelHostedService, IDisposable, IEventListener { + private readonly ILogger _logger; + private readonly WarfareTimeComponent _warfareTimeComponent; + private readonly IPlayerService _playerService; + + private static readonly InstanceGetter? GetIsExploded + = Accessor.GenerateInstanceGetter("isExploded", throwOnError: false); + + private readonly PlayerDictionary _lastProjectileShot = new PlayerDictionary(); + + // note this is in simulated time, not actual time + private const float MaximumSimulationTime = 30f; + private Scene _simScene; private Scene _mainScene; private PhysicsScene _mainPhysxScene; private PhysicsScene _physxScene; private readonly Queue _queue = new Queue(3); private ProjectileData _current; + private int _isSetUp; - [UsedImplicitly] - [SuppressMessage(Data.SuppressCategory, Data.SuppressID)] - private void Start() + public ProjectileSolver(ILogger logger, WarfareTimeComponent warfareTimeComponent, IPlayerService playerService) + { + _logger = logger; + _warfareTimeComponent = warfareTimeComponent; + _playerService = playerService; + } + + internal void RegisterLastMagazineShot(CSteamID player, ItemMagazineAsset magazine) + { + _lastProjectileShot[player] = magazine; + } + + public void BeginSolvingProjectile(GameObject projectileObject, Vector3 origin, Vector3 direction, UseableGun gun, ProjectileSolved? callback) + { + GameThread.AssertCurrent(); + + if (GetIsExploded == null) + return; + + ProjectileData data = default; + data.Player = _playerService.GetOnlinePlayer(gun.player); + data.ProjectileObject = projectileObject; + data.Origin = origin; + data.Direction = direction; + data.Gun = gun; + data.GunAsset = gun.equippedGunAsset; + data.LaunchTime = Time.realtimeSinceStartup; + data.Callback = callback; + _lastProjectileShot.TryGetValue(gun.player, out data.AmmunitionType); + data.MagazineForceMultiplier = data.AmmunitionType?.projectileLaunchForceMultiplier ?? 1f; + + if (_current.Gun is null) + { + _current = data; + _warfareTimeComponent.StartCoroutine(Simulate()); + } + else + _queue.Enqueue(data); + } + + UniTask ILevelHostedService.LoadLevelAsync(CancellationToken token) { _mainScene = SceneManager.GetActiveScene(); _simScene = SceneManager.CreateScene("SimulationScene", new CreateSceneParameters(LocalPhysicsMode.Physics3D)); _physxScene = _simScene.GetPhysicsScene(); _mainPhysxScene = _mainScene.GetPhysicsScene(); - Physics.autoSimulation = false; - if (typeof(Landscape).GetField("tiles", BindingFlags.Static | BindingFlags.NonPublic)?.GetValue(null) is Dictionary d) + + if (Interlocked.Exchange(ref _isSetUp, 1) == 0) + { + Physics.autoSimulation = false; + TimeUtility.physicsUpdated += FixedUpdate; + } + + // creates a new physics scene containing all the major colliders from the main world + // this includes landscape and collision-important objects + FieldInfo? tileField = typeof(Landscape).GetField("tiles", BindingFlags.Static | BindingFlags.NonPublic); + + if (tileField?.GetValue(null) is Dictionary d) { - //L.LogDebug("Found " + d.Count + " tiles to add to scene."); + _logger.LogDebug("Found {0} tiles to add to scene.", d.Count); foreach (LandscapeTile tile in d.Values) { - GameObject obj = Instantiate(tile.gameObject, tile.gameObject.transform.position, tile.gameObject.transform.rotation); + GameObject obj = Object.Instantiate(tile.gameObject, tile.gameObject.transform.position, tile.gameObject.transform.rotation); SceneManager.MoveGameObjectToScene(obj, _simScene); - //L.LogDebug("Adding (" + tile.coord.x + ", " + tile.coord.y + ") tile to scene."); + _logger.LogConditional("Adding ({0}, {1}) tile to scene.", tile.coord.x, tile.coord.y); } } - //else - // L.LogWarning("Failed to add tiles to sim scene."); + else + { + _logger.LogWarning("Failed to add tiles to sim scene."); + } + for (int i = 0; i < Level.level.childCount; ++i) { Transform t = Level.level.GetChild(i); - GameObject obj = Instantiate(t.gameObject, t.position, t.rotation); + GameObject obj = Object.Instantiate(t.gameObject, t.position, t.rotation); SceneManager.MoveGameObjectToScene(obj, _simScene); - //L.LogDebug("Adding " + t.name + " clip to scene."); + _logger.LogDebug("Adding {0} clip to scene.", t.name); } -#if DEBUG - List bcs = new List(8); -#endif foreach (ObjectInfo objInfo in LevelObjectUtility.EnumerateObjects()) { + LevelObject obj = objInfo.Object; + Transform? transform = obj.transform; + if (transform == null || obj.asset is not { isCollisionImportant: true }) + continue; + GameObject? model = objInfo.Object.asset.GetOrLoadModel(); if (model == null) continue; - GameObject newObject = Instantiate(model, objInfo.Object.transform.position, objInfo.Object.transform.rotation); + GameObject newObject = Object.Instantiate(model, transform.position, transform.rotation); if (objInfo.Object.asset.useScale) { - newObject.transform.localScale = objInfo.Object.transform.localScale; + newObject.transform.localScale = transform.localScale; } - Rigidbody rigidbody = transform.GetComponent(); + Rigidbody rigidbody = newObject.transform.GetComponent(); if (rigidbody != null) - Destroy(rigidbody); + Object.Destroy(rigidbody); SceneManager.MoveGameObjectToScene(newObject, _simScene); -#if DEBUG - //newObject.GetComponentsInChildren(bcs); - //for (int i = 0; i < bcs.Count; ++i) - //{ - // BoxCollider c = bcs[i]; - // if (c.transform.localScale.x < 0 || c.transform.localScale.y < 0 || c.transform.localScale.z < 0 || - // c.size.x < 0 || c.size.y < 0 || c.size.z < 0) - // { - // L.LogWarning(objInfo.Object.asset.objectName + " (" + objInfo.Object.asset.id + ", " + - // objInfo.Object.asset.GUID.ToString("N") + - // "): Negative scale or size detected, recommended to fix."); - // } - //} - // - //bcs.Clear(); -#endif } + + _logger.LogInformation("Created projectile scene."); + return UniTask.CompletedTask; } - [UsedImplicitly] - [SuppressMessage(Data.SuppressCategory, Data.SuppressID)] - private void Update() + private void FixedUpdate() { - if (_queue.Count > 0 && _current.Gun is null) - { - _current = _queue.Dequeue(); - StartCoroutine(Simulate()); - } + // because Physics.autoSimulation is turned off, this continues to simulate the main physics scene. + _mainPhysxScene.Simulate(Time.fixedDeltaTime); } - private static readonly InstanceGetter GetIsExploded = Accessor.GenerateInstanceGetter("isExploded", throwOnError: true)!; + public void Dispose() + { + if (Interlocked.Exchange(ref _isSetUp, 0) == 0) + return; + + Physics.autoSimulation = true; + TimeUtility.physicsUpdated -= FixedUpdate; + } private IEnumerator Simulate() { ProjectileData data = _current; - if (data.Gun?.equippedGunAsset?.projectile == null) - yield break; - Transform transform = Instantiate(data.Gun.equippedGunAsset.projectile, data.Origin, Quaternion.LookRotation(data.Direction) * Quaternion.Euler(90f, 0.0f, 0.0f)).transform; - SceneManager.MoveGameObjectToScene(transform.gameObject, _simScene); - - transform.name = "Projectile_SimClone"; - Destroy(transform.gameObject, data.Gun.equippedGunAsset.projectileLifespan); - if (transform.TryGetComponent(out Rigidbody body)) + + if (data.Gun?.equippedGunAsset?.projectile != null) { - body.AddForce(data.Direction * data.Gun.equippedGunAsset.ballisticForce * data.MagazineForceMultiplier); - body.collisionDetectionMode = CollisionDetectionMode.Continuous; - } - DetectComponent c = transform.gameObject.AddComponent(); + Transform transform = Object.Instantiate( + data.Gun.equippedGunAsset.projectile, + data.Origin, + Quaternion.LookRotation(data.Direction) * Quaternion.Euler(90f, 0.0f, 0.0f) + ).transform; - if (!data.Obj.TryGetComponent(out Rocket rocket)) - yield break; + SceneManager.MoveGameObjectToScene(transform.gameObject, _simScene); - c.IgnoreTransform = rocket.ignoreTransform; - c.OriginalRocketData = rocket; + transform.name = "Projectile_Simulation"; + Object.Destroy(transform.gameObject, data.Gun.equippedGunAsset.projectileLifespan); - int i = 0; - int iter = Mathf.CeilToInt(MAX_TIME / Time.fixedDeltaTime); - int skip = Mathf.CeilToInt(1f / (Time.fixedDeltaTime * 1.5f)); -#if false && DEBUG - float seconds; - float lastSent = 0f; -#endif - for (; !c.IsExploded && i < iter; ++i) - { -#if false && DEBUG - seconds = i * Time.fixedDeltaTime; - if (seconds - lastSent > 0.25f) + if (transform.TryGetComponent(out Rigidbody body)) { - if (Gamemode.Config.EffectActionSuppliesAmmo.TryGetAsset(out EffectAsset? asset)) - EffectUtility.TriggerEffect(asset, Level.size * 2, transform.gameObject.transform.position, true); - lastSent = seconds; + body.AddForce(data.Direction * data.Gun.equippedGunAsset.ballisticForce * data.MagazineForceMultiplier); + body.collisionDetectionMode = CollisionDetectionMode.Continuous; } + + DetectComponent c = transform.gameObject.AddComponent(); + + if (!data.ProjectileObject.TryGetComponent(out Rocket rocket)) + yield break; + + c.Logger = _logger; + c.IgnoreTransform = rocket.ignoreTransform; + + float fixedDeltaTime = Time.fixedDeltaTime; + + int iter = Mathf.CeilToInt(MaximumSimulationTime / fixedDeltaTime); + int skip = Mathf.CeilToInt(1f / (fixedDeltaTime * 1.5f)); + + int i = 0; + float seconds; + +#if PROJECTILE_TRACERS + float lastTracerSent = 0f; + EffectAsset? tracerAsset = Assets.find(new Guid("50dbb9c23ae647b8adb829a771742d4c")); #endif - _physxScene.Simulate(Time.fixedDeltaTime); - if (i % skip == 0) + for (; !c.IsExploded && i < iter; ++i) { - yield return null; - if (GetIsExploded(rocket)) - yield break; - } - } -#if !false || !DEBUG - float + +#if PROJECTILE_TRACERS + seconds = i * fixedDeltaTime; + if (seconds - lastTracerSent > 0.25f) + { + if (tracerAsset != null) + EffectUtility.TriggerEffect(tracerAsset, Level.size * 2, transform.gameObject.transform.position, true); + lastTracerSent = seconds; + } #endif - seconds = i * Time.fixedDeltaTime; - Vector3 pos = transform.gameObject.transform.position; - float landTime = data.LaunchTime + seconds; - if (data.Obj != null && data.Obj.TryGetComponent(out ProjectileComponent comp)) - { - comp.PredictedLandingPosition = pos; - comp.PredictedImpactTime = landTime; + _physxScene.Simulate(fixedDeltaTime); + + if (i % skip == 0) + { + yield return null; + if (GetIsExploded!(rocket)) + yield break; + } + } + + seconds = i * fixedDeltaTime; + + Vector3 pos = transform.gameObject.transform.position; + float landTime = data.LaunchTime + seconds; + if (data.ProjectileObject != null && data.ProjectileObject.TryGetComponent(out ProjectileComponent comp)) + { + comp.PredictedLandingPosition = pos; + comp.PredictedImpactTime = landTime; + } + + Object.Destroy(transform.gameObject); + + if (data.Callback != null) + { + try + { + data.Callback.Invoke(data.Player, pos, landTime, data.GunAsset, data.AmmunitionType); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error invocing ProjectileSolver callback: {0}.", data.Callback.Method); + } + } } - Destroy(transform.gameObject); - _current = default; - data.Callback?.Invoke(!data.Gun.player.isActiveAndEnabled ? null : data.Gun.player, pos, landTime, data.GunAsset, data.AmmunitionType); - } - [UsedImplicitly] - [SuppressMessage(Data.SuppressCategory, Data.SuppressID)] - private void FixedUpdate() - { - _mainPhysxScene.Simulate(Time.fixedDeltaTime); - } - private const float MAX_TIME = 30f; - internal void GetLandingPoint(GameObject obj, Vector3 origin, Vector3 direction, UseableGun gun, ProjectileLandingPointCalculated callback) - { - ItemMagazineAsset? ammo = gun.player.TryGetPlayerData(out UCPlayerData data) ? data.LastProjectedAmmoType : null; - if (_current.Gun is null) + _current = default; + if (_queue.TryDequeue(out _current)) { - _current = new ProjectileData(obj, origin, direction, gun, ammo, Time.realtimeSinceStartup, callback); - StartCoroutine(Simulate()); + _warfareTimeComponent.StartCoroutine(Simulate()); } - else - _queue.Enqueue(new ProjectileData(obj, origin, direction, gun, ammo, Time.realtimeSinceStartup, callback)); } + [UsedImplicitly] [SuppressMessage(Data.SuppressCategory, Data.SuppressID)] private void OnDestroy() @@ -192,47 +275,43 @@ private void OnDestroy() _physxScene = default; Physics.autoSimulation = true; } - private readonly struct ProjectileData + private struct ProjectileData { - public readonly GameObject Obj; - public readonly Vector3 Origin; - public readonly Vector3 Direction; - public readonly UseableGun Gun; - public readonly ProjectileLandingPointCalculated Callback; - public readonly ItemMagazineAsset? AmmunitionType; - public readonly ItemGunAsset GunAsset; - public readonly float MagazineForceMultiplier; - public readonly float LaunchTime; - public ProjectileData(GameObject obj, Vector3 origin, Vector3 direction, UseableGun gun, ItemMagazineAsset? ammunitionType, float launchTime, ProjectileLandingPointCalculated callback) - { - Obj = obj; - Origin = origin; - Direction = direction; - Gun = gun; - Callback = callback; - LaunchTime = launchTime; - GunAsset = gun.equippedGunAsset; - AmmunitionType = ammunitionType; - MagazineForceMultiplier = ammunitionType != null ? ammunitionType.projectileLaunchForceMultiplier : 1f; - } + public WarfarePlayer Player; + public GameObject ProjectileObject; + public Vector3 Origin; + public Vector3 Direction; + public UseableGun? Gun; + public ProjectileSolved? Callback; + public ItemMagazineAsset? AmmunitionType; + public ItemGunAsset GunAsset; + public float MagazineForceMultiplier; + public float LaunchTime; } private class DetectComponent : MonoBehaviour { + public ILogger? Logger; + public bool IsExploded; - public Transform IgnoreTransform; - public Rocket OriginalRocketData; + public Transform? IgnoreTransform; + [UsedImplicitly] [SuppressMessage(Data.SuppressCategory, Data.SuppressID)] private void Start() { - //L.LogDebug("Added component to " + gameObject.name + "."); +#if PROJECTILE_TRACERS + Logger?.LogConditional("Added component to {0}.", gameObject.name); +#endif } + [UsedImplicitly] [SuppressMessage(Data.SuppressCategory, Data.SuppressID)] private void OnTriggerEnter(Collider other) { - //L.LogDebug("hit " + other.name); +#if PROJECTILE_TRACERS + Logger?.LogDebug("hit {0}.", other.name); +#endif if (IsExploded || other.isTrigger || IgnoreTransform != null && (other.transform == IgnoreTransform || other.transform.IsChildOf(IgnoreTransform))) return; @@ -247,6 +326,7 @@ private void OnTriggerEnter(Collider other) body.Sleep(); } } + [UsedImplicitly] [SuppressMessage(Data.SuppressCategory, Data.SuppressID)] private void OnDestroy() @@ -254,6 +334,11 @@ private void OnDestroy() IsExploded = true; } } + + void IEventListener.HandleEvent(PlayerLeft e, IServiceProvider serviceProvider) + { + _lastProjectileShot.Remove(e.Steam64); + } } -public delegate void ProjectileLandingPointCalculated(Player? owner, Vector3 position, float impactTime, ItemGunAsset gun, ItemMagazineAsset? ammoType); \ No newline at end of file +public delegate void ProjectileSolved(WarfarePlayer? owner, Vector3 position, float impactTime, ItemGunAsset gun, ItemMagazineAsset? ammoType); \ No newline at end of file diff --git a/UncreatedWarfare/Squads/Spotted/ISpotter.cs b/UncreatedWarfare/Squads/Spotted/ISpotter.cs new file mode 100644 index 00000000..7e88ca62 --- /dev/null +++ b/UncreatedWarfare/Squads/Spotted/ISpotter.cs @@ -0,0 +1,27 @@ +using System; +using Uncreated.Warfare.Layouts.Teams; +using Uncreated.Warfare.Players; +using Uncreated.Warfare.Util; + +namespace Uncreated.Warfare.Squads.Spotted; + +/// +/// Object that can spot other objects. Right now this is either a or UAV. +/// +public interface ISpotter : ITransformObject +{ + /// + /// If the spotted object's position continues to update until the duration has ran out. + /// + bool IsTrackable { get; } + + /// + /// The team to show spotted effects to. + /// + Team Team { get; } + + /// + /// Runs when this spotter is no longer viable. + /// + event Action? OnDestroyed; +} \ No newline at end of file diff --git a/UncreatedWarfare/Squads/Spotted/SpottableObjectComponent.cs b/UncreatedWarfare/Squads/Spotted/SpottableObjectComponent.cs new file mode 100644 index 00000000..fc5bd348 --- /dev/null +++ b/UncreatedWarfare/Squads/Spotted/SpottableObjectComponent.cs @@ -0,0 +1,579 @@ +#if DEBUG +#define SPOTTER_DEBUG_LOG +#endif +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Uncreated.Warfare.Buildables; +using Uncreated.Warfare.Configuration; +using Uncreated.Warfare.Interaction.Icons; +using Uncreated.Warfare.Layouts.Teams; +using Uncreated.Warfare.Players; +using Uncreated.Warfare.Players.Management; +using Uncreated.Warfare.Util; +using Uncreated.Warfare.Vehicles.Info; +using Uncreated.Warfare.Vehicles.Vehicle; + +namespace Uncreated.Warfare.Squads.Spotted; + +public class SpottableObjectComponent : MonoBehaviour, IManualOnDestroy +{ + private record struct SpotterTypeStats( + SpottedType Type, + VehicleType VehicleType, + float DefaultTimer, + float UpdateFrequency, + string EffectName, + Vector3 Offset); + + private struct SpotterInfo + { + public float TimeExpired; + public bool IsTickable; + public Team Team; + public ISpotter Spotter; + } + + private struct MultipleTeamIconPair + { + public WorldIconInfo? Icon; + public Team Team; + public bool AnySpotterIsTickable; + } + + private static readonly SpotterTypeStats[] TypeStats = + [ + // vehicles (try to keep in order of VehicleType) + new SpotterTypeStats(SpottedType.LightVehicle, VehicleType.Humvee, 30f, 0.5f, "Effects:Spotted:Humvee", Vector3.zero), + new SpotterTypeStats(SpottedType.LightVehicle, VehicleType.TransportGround, 30f, 0.5f, "Effects:Spotted:Truck", Vector3.zero), + new SpotterTypeStats(SpottedType.LightVehicle, VehicleType.ScoutCar, 30f, 0.5f, "Effects:Spotted:ScoutCar", Vector3.zero), + new SpotterTypeStats(SpottedType.LightVehicle, VehicleType.LogisticsGround, 30f, 0.5f, "Effects:Spotted:Truck", Vector3.zero), + new SpotterTypeStats(SpottedType.Armor, VehicleType.APC, 30f, 0.5f, "Effects:Spotted:APC", Vector3.zero), + new SpotterTypeStats(SpottedType.Armor, VehicleType.IFV, 30f, 0.5f, "Effects:Spotted:IFV", Vector3.zero), + new SpotterTypeStats(SpottedType.Armor, VehicleType.MBT, 30f, 0.5f, "Effects:Spotted:MBT", Vector3.zero), + new SpotterTypeStats(SpottedType.Aircraft, VehicleType.TransportAir, 15f, 0.5f, "Effects:Spotted:TransportHeli", Vector3.zero), + new SpotterTypeStats(SpottedType.Aircraft, VehicleType.AttackHeli, 15f, 0.5f, "Effects:Spotted:AttackHeli", Vector3.zero), + new SpotterTypeStats(SpottedType.Aircraft, VehicleType.Jet, 10f, 0.5f, "Effects:Spotted:Jet", Vector3.zero), + new SpotterTypeStats(SpottedType.Emplacement, VehicleType.AA, 240f, 1.0f, "Effects:Spotted:AA", Vector3.zero), + new SpotterTypeStats(SpottedType.Emplacement, VehicleType.HMG, 240f, 1.0f, "Effects:Spotted:HMG", Vector3.zero), + new SpotterTypeStats(SpottedType.Emplacement, VehicleType.ATGM, 240f, 1.0f, "Effects:Spotted:ATGM", Vector3.zero), + new SpotterTypeStats(SpottedType.Emplacement, VehicleType.Mortar, 240f, 1.0f, "Effects:Spotted:Mortar", Vector3.zero), + + // other + new SpotterTypeStats(SpottedType.Infantry, VehicleType.None, 12f, 0.5f, "Effects:Spotted:Infantry", new Vector3(0f, 1.5f, 0f)), + new SpotterTypeStats(SpottedType.FOB, VehicleType.None, 10f, 1.0f, "Effects:Spotted:FOB", Vector3.zero) +]; // todo: ^ change back to 240 + + // null if never spotted + private List? _spotters; + + private IAssetLink _asset = null!; + private float _defaultDuration; + private float _updateFrequency; + private Vector3 _offset; + private List? _multipleTeamIcons; + + private SpottedService? _spottedService; + private WorldIconManager? _worldIconManager; + private WorldIconInfo? _singleTeamActiveIcon; + + // time at which the next expire check will be done. + // this is calculated by the lowest expiring spotter in the list, then updated after removing them + private float _nextExpireCheck; + private Coroutine? _expireCoroutine; + + // WarfarePlayer, InteractableVehicle, or IBuildable + private object _owner = null!; + + /// + /// If a player or UAV is spotting this object. + /// + public bool IsSpotted => _spotters is { Count: > 0 }; + + /// + /// The category of object being spotted. + /// + public SpottedType Type { get; private set; } + + /// + /// The category of vehicle being spotted, or if not a vehicle. + /// + public VehicleType VehicleType { get; private set; } + + public InteractableVehicle? Vehicle => _owner as InteractableVehicle; + public IBuildable? Buildable => _owner as IBuildable; + public WarfarePlayer? Player => _owner as WarfarePlayer; + + [UsedImplicitly] + [SuppressMessage("CodeQuality", "IDE0051")] + private void Awake() + { + ref SpotterTypeStats stats = ref Unsafe.NullRef(); + + ILifetimeScope serviceProvider = WarfareModule.Singleton.ServiceProvider; + + if (gameObject.CompareTag("Player")) + { + Player? player = GetComponent(); + if (player == null) + { + Destroy(this); + return; + } + + stats = ref FindTypeStats(SpottedType.Infantry); + _owner = serviceProvider.Resolve().GetOnlinePlayer(player); + } + else if (gameObject.CompareTag("Vehicle")) + { + InteractableVehicle? vehicle = gameObject.GetComponent(); + if (vehicle == null + || !vehicle.gameObject.TryGetComponent(out WarfareVehicleComponent vehicleComponent) + || vehicleComponent.WarfareVehicle.Info.Type == VehicleType.None) + { + Destroy(this); + return; + } + + stats = ref FindTypeStats(vehicleComponent.WarfareVehicle.Info.Type); + _owner = vehicle; + } + else if (gameObject.CompareTag("Barricade")) + { + BarricadeDrop? barricade = BarricadeManager.FindBarricadeByRootTransform(gameObject.transform); + if (barricade == null) + { + Destroy(this); + return; + } + + // todo: is UAV check, want to be able to spot UAVs if we add them + // SpottedType type = IsUav(barricade) ? SpottedType.UAV : SpottedType.FOB; + stats = ref FindTypeStats(SpottedType.FOB); + _owner = new BuildableBarricade(barricade); + } + else if (gameObject.CompareTag("Structure")) + { + StructureDrop? structure = StructureManager.FindStructureByRootTransform(gameObject.transform); + if (structure == null) + { + Destroy(this); + return; + } + + stats = ref FindTypeStats(SpottedType.FOB); + _owner = new BuildableStructure(structure); + } + else + { + Destroy(this); + return; + } + + _defaultDuration = stats.DefaultTimer; + _updateFrequency = stats.UpdateFrequency; + _offset = stats.Offset; + + _asset = serviceProvider.Resolve().GetAssetLink(stats.EffectName); + + Type = stats.Type; + VehicleType = stats.VehicleType; + + _worldIconManager = serviceProvider.Resolve(); + + _spottedService = serviceProvider.Resolve(); + _spottedService.AddSpottableObject(this); + } + + public static SpottableObjectComponent? GetOrAddIfValid(InteractableVehicle vehicle) + { + SpottableObjectComponent comp = vehicle.transform.GetOrAddComponent(); + return comp._owner != null ? comp : null; + } + + public static SpottableObjectComponent? GetOrAddIfValid(WarfarePlayer player) + { + SpottableObjectComponent comp = player.Transform.GetOrAddComponent(); + return comp._owner != null ? comp : null; + } + + public static SpottableObjectComponent? GetOrAddIfValid(IBuildable buildable) + { + SpottableObjectComponent comp = buildable.Model.GetOrAddComponent(); + return comp._owner != null ? comp : null; + } + + /// + /// If this object is an active target for laser guided missiles. + /// + public bool IsLaserTarget(Team team) + { + if (_spotters is not { Count: > 0 }) + return false; + + if (_singleTeamActiveIcon is { Alive: true } && _singleTeamActiveIcon.TargetTeam == team) + return true; + + if (_multipleTeamIcons != null) + { + for (int i = 0; i < _multipleTeamIcons.Count; i++) + { + MultipleTeamIconPair pair = _multipleTeamIcons[i]; + if (pair.Team == team) + { + return pair.Icon is { Alive: true }; + } + } + } + + return false; + } + + private static ref SpotterTypeStats FindTypeStats(SpottedType type) + { + for (int i = TypeStats.Length - 1; i >= 0; --i) + { + ref SpotterTypeStats stats = ref TypeStats[i]; + if (stats.Type == type) + return ref stats; + } + + throw new InvalidOperationException($"Invalid spotted type: {type}."); + } + + private static ref SpotterTypeStats FindTypeStats(VehicleType vehicle) + { + // try by index first + if ((int)vehicle <= TypeStats.Length) + { + ref SpotterTypeStats stats = ref TypeStats[(int)vehicle - 1]; + if (stats.VehicleType == vehicle) + return ref stats; + } + + for (int i = 0; i < TypeStats.Length; ++i) + { + ref SpotterTypeStats stats = ref TypeStats[i]; + if (stats.VehicleType == vehicle) + return ref stats; + } + + throw new InvalidOperationException($"Invalid vehicle type: {vehicle}."); + } + + public bool TryAddSpotter(ISpotter spotter, float duration = float.NaN) + { + GameThread.AssertCurrent(); + + if (spotter.Team is null || !spotter.Team.IsValid) + throw new InvalidOperationException("Spotter must have a valid team."); + + if (_spotters != null && _spotters.Exists(x => ReferenceEquals(x.Spotter, spotter))) + { + return false; + } + + bool isTickable = spotter.IsTrackable; + spotter.OnDestroyed += OnSpotterDestroyed; + + SpotterInfo spotterInfo = default; + + spotterInfo.Team = spotter.Team; + spotterInfo.Spotter = spotter; + spotterInfo.IsTickable = isTickable; + spotterInfo.TimeExpired = Time.realtimeSinceStartup + (float.IsFinite(duration) ? duration : _defaultDuration); + + (_spotters ??= new List(2)).Add(spotterInfo); + + if (_spotters.Count == 1 || spotterInfo.TimeExpired < _nextExpireCheck) + { + if (_expireCoroutine != null) + StopCoroutine(_expireCoroutine); + _nextExpireCheck = spotterInfo.TimeExpired; + _expireCoroutine = StartCoroutine(ExpireCheckCoroutine()); + } + else + { + Log($"Expire check not updated: {_nextExpireCheck} (in {_nextExpireCheck - Time.realtimeSinceStartup} sec)."); + } + + UpdateIcons(); + return true; + } + + private IEnumerator ExpireCheckCoroutine() + { + while (_spotters is { Count: > 0 }) + { + Log($"Expire check at {_nextExpireCheck} (in {_nextExpireCheck - Time.realtimeSinceStartup} sec)."); + yield return new WaitForSecondsRealtime(_nextExpireCheck - Time.realtimeSinceStartup); + + if (_spotters is not { Count: > 0 }) + yield break; + + bool anyRemoved = false; + float lowestExpireTime = float.MaxValue; + + float rt = Time.realtimeSinceStartup; + + for (int i = _spotters.Count - 1; i >= 0; --i) + { + SpotterInfo info = _spotters[i]; + if (rt <= info.TimeExpired) + { + if (lowestExpireTime > info.TimeExpired) + lowestExpireTime = info.TimeExpired; + continue; + } + + Log($"Spotter expired: {i} ({info.Spotter})"); + RemoveSpotterIntl(i); + anyRemoved = true; + } + + _nextExpireCheck = lowestExpireTime; + if (anyRemoved) + UpdateIcons(); + } + + _expireCoroutine = null; + } + + public void RemoveAllSpotters() + { + if (_spotters == null) + return; + + foreach (SpotterInfo spotter in _spotters) + { + spotter.Spotter.OnDestroyed -= OnSpotterDestroyed; + } + + _spotters.Clear(); + UpdateIcons(); + } + + public bool RemoveSpotter(ISpotter spotter) + { + if (_spotters == null) + return false; + + for (int i = 0; i < _spotters.Count; ++i) + { + SpotterInfo spotterInfo = _spotters[i]; + if (!ReferenceEquals(spotterInfo.Spotter, spotter)) + { + continue; + } + + RemoveSpotterIntl(i); + UpdateIcons(); + return true; + } + + return false; + } + + private void OnSpotterDestroyed(ISpotter spotter) + { + RemoveSpotter(spotter); + } + + private void RemoveSpotterIntl(int index) + { + SpotterInfo spotterInfo = _spotters![index]; + spotterInfo.Spotter.OnDestroyed -= OnSpotterDestroyed; + _spotters.RemoveAt(index); + } + + private void UpdateIcons() + { + if (_worldIconManager == null) + { + _singleTeamActiveIcon = null; + _multipleTeamIcons?.Clear(); + return; + } + + // no spotters + if (_spotters is not { Count: > 0 }) + { + if (_singleTeamActiveIcon is { Alive: true }) + { + _worldIconManager.RemoveIcon(_singleTeamActiveIcon); + _singleTeamActiveIcon = null; + } + + if (_multipleTeamIcons is { Count: > 0 }) + { + _multipleTeamIcons.ForEach(x => _worldIconManager.RemoveIcon(x.Icon)); + _multipleTeamIcons.Clear(); + } + + return; + } + + // _multipleTeamIcons is used to keep separate icons for each team, + // if there's only one team active then _singleTeamActiveIcon is used instead. + + Team? allTeam = null; + bool hasMultipleTeams = false; + float latestExpiry = 0; + bool anySpotterIsTickable = false; + Log($"Spotters: {_spotters.Count} (now: {Time.realtimeSinceStartup})"); + foreach (SpotterInfo spotter in _spotters) + { + Log($" - spotter: {spotter.Spotter} tick: {spotter.IsTickable} team: {spotter.Team} expire: {spotter.TimeExpired}"); + if (spotter.TimeExpired > latestExpiry) + latestExpiry = spotter.TimeExpired; + + anySpotterIsTickable |= spotter.IsTickable; + + if (hasMultipleTeams) + { + int index = _multipleTeamIcons!.FindIndex(x => x.Team == spotter.Team); + + if (index < 0) + { + _multipleTeamIcons.Add(new MultipleTeamIconPair + { + Team = spotter.Team, + AnySpotterIsTickable = spotter.IsTickable + }); + } + else if (spotter.IsTickable) + { + MultipleTeamIconPair pair = _multipleTeamIcons[index]; + if (!pair.AnySpotterIsTickable) + { + pair.AnySpotterIsTickable = true; + _multipleTeamIcons[index] = pair; + } + } + } + else if (allTeam == null) + { + allTeam = spotter.Team; + } + else if (allTeam != spotter.Team) + { + hasMultipleTeams = true; + if (_multipleTeamIcons != null) + _multipleTeamIcons.Clear(); + else + _multipleTeamIcons = new List(2); + _multipleTeamIcons.Add(new MultipleTeamIconPair + { + Team = allTeam, + AnySpotterIsTickable = anySpotterIsTickable + }); + allTeam = null; + } + } + Log($" Latest expiry: {latestExpiry}, anySpotterIsTickable: {anySpotterIsTickable}."); + + float duration = latestExpiry - Time.realtimeSinceStartup; + if (!hasMultipleTeams) + { + // spotters only on one team + Log($"Updating icon 1 team ({allTeam})."); + CheckIcon(ref _singleTeamActiveIcon, duration, anySpotterIsTickable, allTeam!); + } + else + { + Log($"Updating icon many teams team ({_multipleTeamIcons!.Count})."); + // spotters across multiple teams + if (_singleTeamActiveIcon is { Alive: true }) + { + _worldIconManager.RemoveIcon(_singleTeamActiveIcon); + } + + _singleTeamActiveIcon = null; + + for (int i = 0; i < _multipleTeamIcons!.Count; i++) + { + MultipleTeamIconPair team = _multipleTeamIcons![i]; + WorldIconInfo? oldIcon = team.Icon; + WorldIconInfo? newIcon = oldIcon; + CheckIcon(ref newIcon, duration, team.AnySpotterIsTickable, team.Team); + if (newIcon != oldIcon) + _multipleTeamIcons[i] = team with { Icon = newIcon }; + } + } + } + + [Conditional("SPOTTER_DEBUG_LOG")] + private void Log(string msg) + { + if (_asset.TryGetAsset(out EffectAsset? asset)) + WarfareModule.Singleton.ServiceProvider.Resolve().CreateLogger(asset.name).LogDebug(msg); + } + + private void CheckIcon(ref WorldIconInfo? icon, float duration, bool anySpotterIsTickable, Team team) + { + if (icon is not { Alive: true } || icon.TargetTeam != team) + { + _worldIconManager!.RemoveIcon(icon); + icon = anySpotterIsTickable + ? new WorldIconInfo(transform, _asset, team, lifetimeSec: duration) + : new WorldIconInfo(transform.position, _asset, team, lifetimeSec: duration); + + icon.TickSpeed = _updateFrequency; + icon.Offset = _offset; + + Log($" Icon created team: {team} duration: {duration}."); + _worldIconManager.CreateIcon(icon); + _multipleTeamIcons = null; + } + else + { + if (anySpotterIsTickable && icon.UnityObject is null) + icon.UnityObject = transform; + else if (!anySpotterIsTickable && icon.UnityObject is not null) + icon.EffectPosition = transform.position; + + icon.KeepAliveFor(duration); + Log($" Icon kept alive {duration} t: {team}."); + } + } + + // non-tickable spotters (like a UAV) can call this to notify of a known position update. + public void OnPositionUpdated() + { + if (_singleTeamActiveIcon is { UnityObject: null }) + { + _singleTeamActiveIcon.EffectPosition = transform.position; + } + + if (_multipleTeamIcons == null) + return; + + Vector3 position = transform.position; + foreach (MultipleTeamIconPair pair in _multipleTeamIcons) + { + if (pair.Icon is { UnityObject: null }) + pair.Icon.EffectPosition = position; + } + } + + [UsedImplicitly] + [SuppressMessage("CodeQuality", "IDE0051")] + private void OnDestroy() + { + if (_expireCoroutine != null) + { + StopCoroutine(_expireCoroutine); + _expireCoroutine = null; + } + + _spottedService?.RemoveSpottableObject(this); + RemoveAllSpotters(); + } + + void IManualOnDestroy.ManualOnDestroy() + { + Destroy(this); + } +} \ No newline at end of file diff --git a/UncreatedWarfare/Squads/Spotted/SpottedService.cs b/UncreatedWarfare/Squads/Spotted/SpottedService.cs new file mode 100644 index 00000000..f47e4c06 --- /dev/null +++ b/UncreatedWarfare/Squads/Spotted/SpottedService.cs @@ -0,0 +1,206 @@ +using DanielWillett.ReflectionTools; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Uncreated.Warfare.Buildables; +using Uncreated.Warfare.Configuration; +using Uncreated.Warfare.Layouts.Teams; +using Uncreated.Warfare.Players; +using Uncreated.Warfare.Players.Management; +using Uncreated.Warfare.Players.UI; +using Uncreated.Warfare.Services; +using Uncreated.Warfare.Translations; +using Uncreated.Warfare.Translations.Util; +using Uncreated.Warfare.Vehicles.Vehicle; + +namespace Uncreated.Warfare.Squads.Spotted; + +[Priority(1)] +internal sealed class SpottedService : ILayoutHostedService +{ + private readonly List _allSpottableObjects; + + private readonly ILogger _logger; + + + private readonly WarfareModule _module; + private readonly IPlayerService _playerService; + private readonly IAssetLink _laserDesignator; + private readonly ITranslationValueFormatter _formatter; + private readonly SpottedTranslations _translations; + private ITeamManager? _teamManager; + + public IReadOnlyList AliveSpottableObjects { get; } + + public SpottedService(IServiceProvider serviceProvider, ILogger logger) + { + _logger = logger; + + _module = serviceProvider.GetRequiredService(); + _formatter = serviceProvider.GetRequiredService(); + _translations = serviceProvider.GetRequiredService>().Value; + _playerService = serviceProvider.GetRequiredService(); + _laserDesignator = serviceProvider.GetRequiredService() + .GetAssetLink("Items:LaserDesignator"); + + _allSpottableObjects = new List(64); + AliveSpottableObjects = new ReadOnlyCollection(_allSpottableObjects); + } + + UniTask ILayoutHostedService.StartAsync(CancellationToken token) + { + UseableGun.onBulletHit += UseableGunOnBulletHit; + return UniTask.CompletedTask; + } + + UniTask ILayoutHostedService.StopAsync(CancellationToken token) + { + UseableGun.onBulletHit -= UseableGunOnBulletHit; + + foreach (SpottableObjectComponent comp in _allSpottableObjects) + { + comp.RemoveAllSpotters(); + } + + _teamManager = null; + return UniTask.CompletedTask; + } + + private void UseableGunOnBulletHit(UseableGun gun, BulletInfo bullet, InputInfo hit, ref bool shouldAllow) + { + _logger.LogDebug("received shot from {0}.", hit.type); + if (!_laserDesignator.MatchAsset(gun.equippedGunAsset) || !shouldAllow || hit.transform == null) + { + return; + } + + WarfarePlayer spotter = _playerService.GetOnlinePlayer(gun.player); + + IBuildable? buildable = null; + switch (hit.type) + { + // infantry + case ERaycastInfoType.PLAYER: + WarfarePlayer? player = _playerService.GetOnlinePlayerOrNull(hit.player); + if (player == null || !spotter.Team.IsOpponent(player.Team)) + { + _logger.LogDebug("Invalid spot: no player team: {0}.", player); + break; + } + + SpottableObjectComponent? spotted = SpottableObjectComponent.GetOrAddIfValid(player); + if (spotted == null) + break; + + _logger.LogConditional("Spotting player {0}", player); + Spot(spotter, player.Team, spotted, _translations.SpottedTargetPlayer.Translate()); + break; + + // vehicles + case ERaycastInfoType.VEHICLE: + InteractableVehicle? vehicle = hit.vehicle; + if (vehicle == null || !vehicle.TryGetComponent(out WarfareVehicleComponent vehComp)) + { + _logger.LogDebug("Invalid spot: no vehicle found."); + break; + } + + Team? team = vehComp.WarfareVehicle.Spawn?.Team; + if (team == null || !spotter.Team.IsOpponent(team)) + { + _logger.LogDebug("Invalid spot: no vehicle team: {0}.", vehicle.asset); + break; + } + + spotted = SpottableObjectComponent.GetOrAddIfValid(vehicle); + if (spotted == null) + break; + + _logger.LogConditional("Spotting vehicle {0}.", vehicle.asset.vehicleName); + Spot(spotter, team, spotted, vehicle.transform.TryGetComponent(out WarfareVehicleComponent vc) + ? _formatter.FormatEnum(vc.WarfareVehicle.Info.Type, null) + : vehicle.asset.vehicleName + ); + break; + + // buildables + case ERaycastInfoType.STRUCTURE: + StructureDrop? structure = StructureManager.FindStructureByRootTransform(hit.transform); + if (structure == null) + { + _logger.LogDebug("Invalid spot: no structure found."); + break; + } + + buildable = new BuildableStructure(structure); + goto case ERaycastInfoType.BARRICADE; + + case ERaycastInfoType.BARRICADE: + if (buildable == null) + { + BarricadeDrop? barricade = BarricadeManager.FindBarricadeByRootTransform(hit.transform); + if (barricade == null) + { + _logger.LogDebug("Invalid spot: no barricade found."); + break; + } + + buildable = new BuildableBarricade(barricade); + } + + if (_teamManager == null && _module.IsLayoutActive()) + _teamManager = _module.GetActiveLayout().TeamManager; + + team = _teamManager?.GetTeam(buildable.Group); + if (team is null || !team.IsValid || !team.IsOpponent(spotter.Team)) + { + _logger.LogDebug("Invalid spot: no team on buildable: {0}.", buildable); + break; + } + + spotted = SpottableObjectComponent.GetOrAddIfValid(buildable); + if (spotted == null) + break; + + _logger.LogConditional("Spotting buildable {0}.", buildable.Asset.itemName); + Spot(spotter, team, spotted, _translations.SpottedTargetFOB.Translate()); + break; + } + + shouldAllow = false; + } + + private void Spot(WarfarePlayer spotter, Team targetTeam, SpottableObjectComponent spottedComp, string targetName) + { + if (!spottedComp.TryAddSpotter(spotter)) + { + _logger.LogConditional(" - already spotted."); + return; + } + + spotter.SendToast(new ToastMessage(ToastMessageStyle.Mini, _translations.SpottedToast.Translate(spotter))); + + Team t = spotter.Team; + Color t1 = t.Faction.Color; + + targetName = TranslationFormattingUtility.Colorize(targetName, targetTeam.Faction.Color); + + foreach (LanguageSet set in _formatter.TranslationService.SetOf.PlayersOnTeam(t)) + { + string t2 = _translations.SpottedMessage.Translate(t1, targetName, in set); + while (set.MoveNext()) + ChatManager.serverSendMessage(t2, Palette.AMBIENT, spotter.SteamPlayer, set.Next.SteamPlayer, EChatMode.SAY, null, true); + } + } + + internal void AddSpottableObject(SpottableObjectComponent comp) + { + _allSpottableObjects.Add(comp); + } + + internal void RemoveSpottableObject(SpottableObjectComponent comp) + { + _allSpottableObjects.Remove(comp); + } +} \ No newline at end of file diff --git a/UncreatedWarfare/Squads/Spotted/SpottedTranslations.cs b/UncreatedWarfare/Squads/Spotted/SpottedTranslations.cs new file mode 100644 index 00000000..d5e9e734 --- /dev/null +++ b/UncreatedWarfare/Squads/Spotted/SpottedTranslations.cs @@ -0,0 +1,22 @@ +using Uncreated.Warfare.Translations; + +namespace Uncreated.Warfare.Squads.Spotted; +internal sealed class SpottedTranslations : PropertiesTranslationCollection +{ + protected override string FileName => "Spotted"; + + [TranslationData] + public readonly Translation SpottedToast = new Translation("<#b9ffaa>SPOTTED", TranslationOptions.TMProUI); + + [TranslationData(Parameters = ["Team color of the speaker.", "Target"])] + public readonly Translation SpottedMessage = new Translation("[T] %SPEAKER%: Enemy {1} spotted!", TranslationOptions.UnityUINoReplace); + + [TranslationData] + public readonly Translation SpottedTargetPlayer = new Translation("contact", TranslationOptions.UnityUINoReplace); + + [TranslationData] + public readonly Translation SpottedTargetFOB = new Translation("FOB", TranslationOptions.UnityUINoReplace); + + [TranslationData] + public readonly Translation SpottedTargetCache = new Translation("Cache", TranslationOptions.UnityUINoReplace); +} diff --git a/UncreatedWarfare/Squads/Spotted/SpottedType.cs b/UncreatedWarfare/Squads/Spotted/SpottedType.cs new file mode 100644 index 00000000..26acce31 --- /dev/null +++ b/UncreatedWarfare/Squads/Spotted/SpottedType.cs @@ -0,0 +1,25 @@ +namespace Uncreated.Warfare.Squads.Spotted; + + +public enum SpottedType +{ + /// + /// A player + /// + Infantry, + + /// + /// Any barricade or structure. + /// + FOB, + + LightVehicle, + + Armor, + + Aircraft, + + Emplacement, + + UAV +} \ No newline at end of file diff --git a/UncreatedWarfare/Stats/PointsUI.cs b/UncreatedWarfare/Stats/PointsUI.cs index 1e3f3b9d..c17b0590 100644 --- a/UncreatedWarfare/Stats/PointsUI.cs +++ b/UncreatedWarfare/Stats/PointsUI.cs @@ -183,7 +183,7 @@ public void UpdatePointsUI(WarfarePlayer player, PointsService pointsService) } int expectedPosition = GetPositionLogicIndex(player); - GetLogger().LogInformation("POSITION: {0}", expectedPosition); + if (data.Position != expectedPosition) { data.Position = expectedPosition; diff --git a/UncreatedWarfare/Translations/DefaultTranslations.cs b/UncreatedWarfare/Translations/DefaultTranslations.cs index ada3a697..e0899694 100644 --- a/UncreatedWarfare/Translations/DefaultTranslations.cs +++ b/UncreatedWarfare/Translations/DefaultTranslations.cs @@ -154,82 +154,6 @@ internal static class T public static readonly Translation CachesHeader = new Translation("Caches", TranslationOptions.UnityUI); #endregion - #region Players - private const string SectionPlayers = "Players"; - - [TranslationData(SectionPlayers, "Gets broadcasted when a player connects.", "Connecting player")] - public static readonly Translation PlayerConnected = new Translation("<#e6e3d5>{0} joined the server."); - - [TranslationData(SectionPlayers, "Gets broadcasted when a player disconnects.", "Disconnecting player")] - public static readonly Translation PlayerDisconnected = new Translation("<#e6e3d5>{0} left the server."); - - [TranslationData(SectionPlayers, "Kick message for a player that suffers from a rare bug which will cause GameObject.get_transform() to throw a NullReferenceException (not return null). They are kicked if this happens.", "Discord Join Code")] - public static readonly Translation NullTransformKickMessage = new Translation("Your character is bugged, which messes up our zone plugin. Rejoin or contact a Director if this continues. (discord.gg/{0})."); - - [TranslationData(SectionPlayers, "Gets sent to a player who is attempting to main camp the other team.")] - public static readonly Translation AntiMainCampWarning = new Translation("<#fa9e9e>Stop <#ff3300>main-camping! Damage is reversed back on you."); - - [TranslationData(SectionPlayers, "Gets sent to a player who is trying to place a non-whitelisted barricade on a vehicle.", "Barricade being placed")] - public static readonly Translation NoPlacementOnVehicle = new Translation("<#fa9e9e>You can't place {0} on a vehicle!", arg0Fmt: new ArgumentFormat(PluralAddon.Always(), RarityColorAddon.Instance)); - - [TranslationData(SectionPlayers, "Generic message sent when a player is placing something in a place they shouldn't.", "Item being placed")] - public static readonly Translation ProhibitedPlacement = new Translation("<#fa9e9e>You're not allowed to place {0} here.", arg0Fmt: new ArgumentFormat(PluralAddon.Always(), RarityColorAddon.Instance)); - - [TranslationData(SectionPlayers, "Generic message sent when a player is dropping an item where they shouldn't.", "Item being dropped", "Zone or flag the player is dropping their item in.")] - public static readonly Translation ProhibitedDropZone = new Translation("<#fa9e9e>You're not allowed to drop {0} in {1}.", arg0Fmt: new ArgumentFormat(PluralAddon.Always(), RarityColorAddon.Instance), arg1Fmt: Flags.ColorNameDiscoverFormat); - - [TranslationData(SectionPlayers, "Generic message sent when a player is picking up an item where they shouldn't.", "Item being picked up", "Zone or flag the player is picking up their item in.")] - public static readonly Translation ProhibitedPickupZone = new Translation("<#fa9e9e>You're not allowed to pick up {0} in {1}.", arg0Fmt: new ArgumentFormat(PluralAddon.Always(), RarityColorAddon.Instance), arg1Fmt: Flags.ColorNameDiscoverFormat); - - [TranslationData(SectionPlayers, "Generic message sent when a player is placing something in a zone they shouldn't be.", "Item being placed", "Zone or flag the player is placing their item in.")] - public static readonly Translation ProhibitedPlacementZone = new Translation("<#fa9e9e>You're not allowed to place {0} in {1}.", arg0Fmt: new ArgumentFormat(PluralAddon.Always(), RarityColorAddon.Instance), arg1Fmt: Flags.ColorNameDiscoverFormat); - - [TranslationData(SectionPlayers, "Sent when a player tries to steal a battery.")] - public static readonly Translation NoStealingBatteries = new Translation("<#fa9e9e>Stealing batteries is not allowed."); - - [TranslationData(SectionPlayers, "Sent when a player tries to manually leave their group.")] - public static readonly Translation NoLeavingGroup = new Translation("<#fa9e9e>You are not allowed to manually change groups, use <#cedcde>/teams instead."); - - [TranslationData(SectionPlayers, "Message sent when a player tries to place a non-whitelisted item in a storage inventory.", "Item being stored")] - public static readonly Translation ProhibitedStoring = new Translation("<#fa9e9e>You are not allowed to store {0}.", arg0Fmt: new ArgumentFormat(PluralAddon.Always(), RarityColorAddon.Instance)); - - [TranslationData(SectionPlayers, "Sent when a player tries to point or mark while not a squad leader.")] - public static readonly Translation MarkerNotInSquad = new Translation("<#fa9e9e>Only your squad can see markers. Create a squad with <#cedcde>/squad create to use this feature."); - - [TranslationData(SectionPlayers, "Sent on a SEVERE toast when the player enters enemy territory.", "Seconds until death")] - public static readonly Translation EnteredEnemyTerritory = new Translation("ENEMY HQ PROXIMITY\nLEAVE IMMEDIATELY\nDEAD IN {0}", TranslationOptions.UnityUI); - - [TranslationData(SectionPlayers, "Sent 2 times before a player is kicked for inactivity.", "Time code")] - public static readonly Translation InactivityWarning = new Translation("<#fa9e9e>You will be AFK-Kicked in <#cedcde>{0} if you don't move."); - - [TranslationData(SectionPlayers, "Broadcasted when a player is removed from the game by BattlEye.", "Player being kicked.")] - public static readonly Translation BattlEyeKickBroadcast = new Translation("<#00ffff><#d8addb>{0} was kicked by <#feed00>BattlEye.", arg0Fmt: WarfarePlayer.FormatPlayerName); - - [TranslationData(SectionPlayers, "Sent when an unauthorized player attempts to edit a sign.")] - public static readonly Translation ProhibitedSignEditing = new Translation("<#ff8c69>You are not allowed to edit that sign."); - - [TranslationData(SectionPlayers, "Sent when a player tries to craft a blacklisted blueprint.")] - public static readonly Translation NoCraftingBlueprint = new Translation("<#b3a6a2>Crafting is disabled for this item."); - - [TranslationData(SectionPlayers, "Shows above the XP UI when divisions are enabled.", "Branch (Division) the player is a part of.")] - public static readonly Translation XPUIDivision = new Translation("{0} Division"); - - [TranslationData(SectionPlayers, "Tells the player that the game detected they have started nitro boosting.")] - public static readonly Translation StartedNitroBoosting = new Translation("<#e00ec9>Thank you for nitro boosting! In-game perks have been activated."); - - [TranslationData(SectionPlayers, "Tells the player that the game detected they have stopped nitro boosting.")] - public static readonly Translation StoppedNitroBoosting = new Translation("<#9b59b6>Your nitro boost(s) have expired. In-game perks have been deactivated."); - - [TranslationData(SectionPlayers, "Tells the player that they can't remove clothes which have item storage.")] - public static readonly Translation NoRemovingClothing = new Translation("<#b3a6a2>You can not remove clothes with storage from your kit."); - - [TranslationData(SectionPlayers, "Tells the player that they can't unlock vehicles from the vehicle bay.")] - public static readonly Translation UnlockVehicleNotAllowed = new Translation("<#b3a6a2>You can not unlock a requested vehicle."); - - [TranslationData(SectionPlayers, "Goes on the warning UI.")] - public static readonly Translation MortarWarning = new Translation("FRIENDLY MORTAR\nINCOMING", TranslationOptions.TMProUI); - #endregion - #region Leaderboards private const string SectionLeaderboard = "Leaderboard"; @@ -1241,20 +1165,6 @@ internal static class T public static readonly Translation TeamsShuffleQueued = new Translation("Teams will be SHUFFLED next game."); #endregion - #region Spotting - private const string SectionSpotting = "Spotting"; - [TranslationData(SectionSpotting)] - public static readonly Translation SpottedToast = new Translation("<#b9ffaa>SPOTTED", TranslationOptions.TMProUI); - [TranslationData(SectionSpotting, Parameters = [ "Team color of the speaker.", "Target" ])] - public static readonly Translation SpottedMessage = new Translation("[T] <#{0}>%SPEAKER%: Enemy {1} spotted!"); - [TranslationData(SectionSpotting)] - public static readonly Translation SpottedTargetPlayer = new Translation("contact"); - [TranslationData(SectionSpotting)] - public static readonly Translation SpottedTargetFOB = new Translation("FOB"); - [TranslationData(SectionSpotting)] - public static readonly Translation SpottedTargetCache = new Translation("Cache"); - #endregion - #region Actions private const string SectionActions = "Actions"; [TranslationData(SectionActions)] diff --git a/UncreatedWarfare/Tweaks/LandmineExplosionRestrictions.cs b/UncreatedWarfare/Tweaks/LandmineExplosionRestrictions.cs new file mode 100644 index 00000000..56fc62d5 --- /dev/null +++ b/UncreatedWarfare/Tweaks/LandmineExplosionRestrictions.cs @@ -0,0 +1,58 @@ +using System; +using System.Runtime.CompilerServices; +using Uncreated.Warfare.Events; +using Uncreated.Warfare.Events.Models; +using Uncreated.Warfare.Events.Models.Barricades; +using Uncreated.Warfare.Layouts.Teams; +using Uncreated.Warfare.Zones; + +namespace Uncreated.Warfare.Tweaks; + +internal sealed class LandmineExplosionRestrictions(ITeamManager teamManager, ZoneStore zoneStore) + : IEventListener, IEventListener +{ + [EventListener(Priority = 1)] + void IEventListener.HandleEvent(TriggerTrapRequested e, IServiceProvider serviceProvider) + { + if (e.Barricade.asset is not ItemTrapAsset { isExplosive: true }) + return; + + if (e.TriggeringPlayer != null && e.TriggeringPlayer.ComponentOrNull() is { IsActive: true }) + { + e.Cancel(); + return; + } + + Team placedTeam = teamManager.GetTeam(Unsafe.As(ref e.Barricade.GetServersideData().group)); + + if (e.TriggeringTeam != null && e.TriggeringTeam.IsFriendly(placedTeam)) + { + // allow players to trigger their own landmines with throwables + if (e.TriggeringPlayer == null || !e.TriggeringPlayer.Equals(e.ServersideData.owner) || e.TriggeringThrowable == null) + e.Cancel(); + } + else if (!CheckLandminePosition(e.ServersideData.point)) + { + e.Cancel(); + } + } + + [EventListener(Priority = 1)] + void IEventListener.HandleEvent(PlaceBarricadeRequested e, IServiceProvider serviceProvider) + { + if (e.Barricade.asset is not ItemTrapAsset) + { + return; + } + + if (!CheckLandminePosition(e.Position)) + { + e.Cancel(); + } + } + + private bool CheckLandminePosition(Vector3 position) + { + return !(zoneStore.IsInsideZone(position, ZoneType.MainBase, null) || zoneStore.IsInsideZone(position, ZoneType.AntiMainCampArea, null)); + } +} diff --git a/UncreatedWarfare/Util/MathUtility.cs b/UncreatedWarfare/Util/MathUtility.cs index 0656466e..36c7d81c 100644 --- a/UncreatedWarfare/Util/MathUtility.cs +++ b/UncreatedWarfare/Util/MathUtility.cs @@ -148,7 +148,13 @@ public static float SquaredDistance(in Vector3 pos1, in Vector2 pos2) /// Returns true if and are less than units away from each other, otherwise false. Compares using square magnitude for speed. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool WithinRange(in Vector3 pos1, in Vector3 pos2, float range) => (pos1 - pos2).sqrMagnitude <= Math.Pow(range, 2); + public static bool WithinRange(in Vector3 pos1, in Vector3 pos2, float range) => SquaredDistance(in pos1, in pos2, false) <= range * range; + + /// + /// Returns true if and are less than units away from each other, otherwise false. Compares using square magnitude for speed. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool WithinRange2D(in Vector3 pos1, in Vector3 pos2, float range) => SquaredDistance(in pos1, in pos2, true) <= range * range; /// /// Counts the number of digits in a number, not counting the negative sign. diff --git a/UncreatedWarfare/Vehicles/Events/Tweaks/GuidedMissileLaunchTweaks.cs b/UncreatedWarfare/Vehicles/Events/Tweaks/GuidedMissileLaunchTweaks.cs index d6334b74..7c527481 100644 --- a/UncreatedWarfare/Vehicles/Events/Tweaks/GuidedMissileLaunchTweaks.cs +++ b/UncreatedWarfare/Vehicles/Events/Tweaks/GuidedMissileLaunchTweaks.cs @@ -58,7 +58,7 @@ public void HandleEvent(ProjectileSpawned e, IServiceProvider serviceProvider) } else if (_laserGuidedMissiles.Any(a => a.MatchAsset(e.Asset))) { - e.Object.GetOrAddComponent().Initialize(e.Object, e.Player.UnturnedPlayer, serviceProvider, 150, 1.15f, 150, 15, 0.6f); + e.Object.GetOrAddComponent().Initialize(e.Object, e.Player, serviceProvider, 150, 1.15f, 150, 15, 0.6f); } } diff --git a/UncreatedWarfare/Vehicles/WarfareVehicle/WarfareVehicle.cs b/UncreatedWarfare/Vehicles/WarfareVehicle/WarfareVehicle.cs index 11755e59..068e625a 100644 --- a/UncreatedWarfare/Vehicles/WarfareVehicle/WarfareVehicle.cs +++ b/UncreatedWarfare/Vehicles/WarfareVehicle/WarfareVehicle.cs @@ -25,7 +25,7 @@ public WarfareVehicle(InteractableVehicle interactableVehicle, WarfareVehicleInf public void Dispose() { - GameObject.Destroy(Component); + Object.Destroy(Component); } internal void UnlinkFromSpawn(VehicleSpawner spawn) diff --git a/UncreatedWarfare/WarfareModule.cs b/UncreatedWarfare/WarfareModule.cs index 099d871f..b0adc286 100644 --- a/UncreatedWarfare/WarfareModule.cs +++ b/UncreatedWarfare/WarfareModule.cs @@ -57,6 +57,7 @@ using Uncreated.Warfare.Sessions; using Uncreated.Warfare.Signs; using Uncreated.Warfare.Squads; +using Uncreated.Warfare.Squads.Spotted; using Uncreated.Warfare.Squads.UI; using Uncreated.Warfare.Stats; using Uncreated.Warfare.Stats.EventHandlers; @@ -684,6 +685,10 @@ private void ConfigureServices(ContainerBuilder bldr) bldr.RegisterType() .SingleInstance(); + bldr.RegisterType() + .AsSelf().AsImplementedInterfaces() + .SingleInstance(); + // Active ITeamManager bldr.Register(_ => GetActiveLayout().TeamManager) .InstancePerMatchingLifetimeScope(LifetimeScopeTags.Session); @@ -705,8 +710,12 @@ private void ConfigureServices(ContainerBuilder bldr) .AsSelf().AsImplementedInterfaces() .InstancePerMatchingLifetimeScope(LifetimeScopeTags.Session); - bldr.RegisterType().AsImplementedInterfaces(); - bldr.RegisterType().AsImplementedInterfaces(); + bldr.RegisterType().AsImplementedInterfaces() + .SingleInstance(); + bldr.RegisterType().AsImplementedInterfaces() + .InstancePerMatchingLifetimeScope(LifetimeScopeTags.Session); + bldr.RegisterType().AsImplementedInterfaces() + .InstancePerMatchingLifetimeScope(LifetimeScopeTags.Session); // Localization bldr.RegisterType() diff --git a/UncreatedWarfare/Zones/ElectricalGridService.cs b/UncreatedWarfare/Zones/ElectricalGridService.cs index a03dc62b..417b214a 100644 --- a/UncreatedWarfare/Zones/ElectricalGridService.cs +++ b/UncreatedWarfare/Zones/ElectricalGridService.cs @@ -1,7 +1,12 @@ -using System.Collections.Generic; +using DanielWillett.ReflectionTools; +using System; +using System.Collections.Generic; +using System.Linq; using Uncreated.Warfare.Events.Models; using Uncreated.Warfare.Events.Models.Flags; +using Uncreated.Warfare.Layouts; using Uncreated.Warfare.Layouts.Flags; +using Uncreated.Warfare.Layouts.Teams; using Uncreated.Warfare.Patches; using Uncreated.Warfare.Services; using Uncreated.Warfare.Util; @@ -14,15 +19,22 @@ namespace Uncreated.Warfare.Zones; /// For example, lights, gates in a parking lot, vending machines, etc may be enabled when the zone is objective or in rotation. /// /// -public class ElectricalGridService : ILevelHostedService +public class ElectricalGridService : ILevelHostedService, IEventListener, ILayoutHostedService { private readonly ILogger _logger; + private readonly WarfareModule _module; + private readonly ZoneStore _zoneStore; + + private static readonly Action? RefreshIsConnectedToPower = + Accessor.GenerateInstanceCaller>("RefreshIsConnectedToPower"); public bool Enabled { get; internal set; } - public ElectricalGridService(ILogger logger) + public ElectricalGridService(ILogger logger, WarfareModule module, ZoneStore zoneStore) { _logger = logger; + _module = module; + _zoneStore = zoneStore; } /// @@ -100,4 +112,106 @@ private static bool IsPointInRotation(IFlagRotationService flagRotation, Vector3 return false; } + + void IEventListener.HandleEvent(FlagObjectiveChanged e, IServiceProvider serviceProvider) + { + if (!Enabled || !_module.IsLayoutActive()) + return; + + IFlagRotationService? rotationService = _module.GetActiveLayout().ServiceProvider.ResolveOptional(); + + if (rotationService is not { GridBehaivor: ElectricalGridBehaivor.EnabledWhenObjective }) + { + return; + } + + if (e.OldObjective != null) + { + SetPowerForAllGrid(e.OldObjective.Region.Primary.Zone, false); + } + if (e.NewObjective != null) + { + SetPowerForAllGrid(e.NewObjective.Region.Primary.Zone, true); + } + + CheckPowerForAllBarricades(); + } + + private static void CheckPowerForAllBarricades() + { + if (RefreshIsConnectedToPower == null) + return; + + foreach (BarricadeInfo barricade in BarricadeUtility.EnumerateBarricades()) + { + if (barricade.Drop.interactable is InteractablePower power) + RefreshIsConnectedToPower(power); + } + } + + private static void SetPowerForAllGrid(Zone zone, bool state) + { + Vector3 c = zone.Center; + foreach (uint gridObject in zone.GridObjects) + { + ObjectInfo obj = LevelObjectUtility.FindObject(gridObject, c); + + if (!obj.HasValue) + { + continue; + } + + InteractableObject intx = obj.Object.interactable; + if (intx is null) + continue; + + if (intx.objectAsset.interactabilityHint is EObjectInteractabilityHint.FIRE or EObjectInteractabilityHint.GENERATOR or EObjectInteractabilityHint.SWITCH) + { + ObjectManager.forceObjectBinaryState(obj.Object.transform, state); + } + + RefreshIsConnectedToPower?.Invoke(intx); + } + } + + UniTask ILayoutHostedService.StartAsync(CancellationToken token) + { + Layout layout = _module.GetActiveLayout(); + + IFlagRotationService? rotationService = layout.ServiceProvider.ResolveOptional(); + + foreach (Team team in layout.TeamManager.AllTeams) + { + Zone? zone = _zoneStore.SearchZone(ZoneType.MainBase, team.Faction); + if (zone == null) + continue; + + SetPowerForAllGrid(zone, true); + } + + foreach (Zone zone in _zoneStore.Zones.Where(x => x.Type == ZoneType.Flag)) + { + if (!zone.IsPrimary) + continue; + + bool isEnabled = false; + if (rotationService != null) + { + isEnabled = rotationService.GridBehaivor == ElectricalGridBehaivor.AllEnabled; + if (!isEnabled && rotationService.GridBehaivor == ElectricalGridBehaivor.EnabledWhenInRotation) + { + isEnabled = rotationService.ActiveFlags.Any(x => x.Region.Primary.Zone == zone); + } + } + + SetPowerForAllGrid(zone, isEnabled); + } + + return UniTask.CompletedTask; + } + + UniTask ILayoutHostedService.StopAsync(CancellationToken token) + { + return UniTask.CompletedTask; + } } \ No newline at end of file diff --git a/UncreatedWarfare/Zones/ZoneStore.cs b/UncreatedWarfare/Zones/ZoneStore.cs index 13754823..04cfa50a 100644 --- a/UncreatedWarfare/Zones/ZoneStore.cs +++ b/UncreatedWarfare/Zones/ZoneStore.cs @@ -293,8 +293,8 @@ public IEnumerable EnumerateInsideZones(Vector2 point, ZoneType? type = nu public Zone? SearchZone(ZoneType type, FactionInfo? faction = null) { return faction == null - ? Zones.FirstOrDefault(zone => zone.Type == type) - : Zones.FirstOrDefault(zone => zone.Type == type && string.Equals(zone.Faction, faction.FactionId, StringComparison.Ordinal)); + ? Zones.FirstOrDefault(zone => zone.IsPrimary && zone.Type == type) + : Zones.FirstOrDefault(zone => zone.IsPrimary && zone.Type == type && string.Equals(zone.Faction, faction.FactionId, StringComparison.Ordinal)); } ///