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));
}
///