diff --git a/UncreatedWarfare/Commands/WarfareDev/DebugEffectAttachCommand.cs b/UncreatedWarfare/Commands/WarfareDev/DebugEffectAttachCommand.cs index a4b06ab7..03d45510 100644 --- a/UncreatedWarfare/Commands/WarfareDev/DebugEffectAttachCommand.cs +++ b/UncreatedWarfare/Commands/WarfareDev/DebugEffectAttachCommand.cs @@ -32,6 +32,8 @@ public UniTask ExecuteAsync(CancellationToken token) Context.TryGet(0, out float lifetime); if (!Context.TryGet(1, out float tickSpeed)) tickSpeed = WorldIconManager.DefaultTickSpeed; + if (!Context.TryGet(2, out float distance)) + distance = float.MaxValue; WorldIconInfo info; if (raycast.transform.gameObject.layer == (int)ELayerMask.GROUND) @@ -44,6 +46,11 @@ public UniTask ExecuteAsync(CancellationToken token) } info.TickSpeed = tickSpeed; + if (distance < 0) + info.RelevanceRegions = (byte)Math.Round(-distance); + else + info.RelevanceDistance = distance; + _iconManager.CreateIcon(info); return UniTask.CompletedTask; diff --git a/UncreatedWarfare/Interaction/Commands/CommandParser.cs b/UncreatedWarfare/Interaction/Commands/CommandParser.cs index 6ae322e1..440ebc1a 100644 --- a/UncreatedWarfare/Interaction/Commands/CommandParser.cs +++ b/UncreatedWarfare/Interaction/Commands/CommandParser.cs @@ -139,7 +139,7 @@ private static ReadOnlySpan GetNextArg(ref ReadOnlySpan args, ReadOn while (firstNonFlag < args.Length && flagPrefixes[flagPrefix] == args[firstNonFlag]) ++firstNonFlag; - if (firstNonFlag is 1 or 2) + if (firstNonFlag is 1 or 2 && firstNonFlag < args.Length && !char.IsDigit(args[firstNonFlag]) && args[firstNonFlag] != '.' && args[firstNonFlag] != ',') { args = args[firstNonFlag..]; ReadOnlySpan span = GetNextArg(ref args, startArgChars, endArgChars, flagPrefixes, out isEmpty, out flagDashCt); @@ -334,8 +334,11 @@ private static bool IsFlag(string str) return false; ReadOnlySpan flagPrefixes = [ '-', '–', '—', '−' ]; - if (flagPrefixes.IndexOf(str[0]) < 0) - return false; + + + + if (flagPrefixes.IndexOf(str[0]) < 0 || char.IsDigit(str[1]) || str[1] == '.' || str[1] == ',') + return false; // prevents negative numbers ^ counting as flags return str.Length < 2 && flagPrefixes.IndexOf(str[1]) >= 0 || flagPrefixes.IndexOf(str[1]) < 0 || flagPrefixes.IndexOf(str[2]) < 0; } diff --git a/UncreatedWarfare/Interaction/Icons/WorldIconInfo.cs b/UncreatedWarfare/Interaction/Icons/WorldIconInfo.cs index d3a09e4d..5ee33302 100644 --- a/UncreatedWarfare/Interaction/Icons/WorldIconInfo.cs +++ b/UncreatedWarfare/Interaction/Icons/WorldIconInfo.cs @@ -14,12 +14,15 @@ namespace Uncreated.Warfare.Interaction.Icons; /// public class WorldIconInfo : ITransformObject, IDisposable { - private Vector3 _prevSpawnPosition; private bool _needsRespawn = true; + internal Vector3 LastSpawnPosition; internal float LastSpawnRealtime; internal float LastPositionUpdateRealtime; internal float FirstSpawnRealtime; + // used when distance is specified to clear from players who have left the area + private List? _previousPlayers; + private Vector3 _position; private Color32 _color = new Color32(255, 255, 255, 0); @@ -143,6 +146,18 @@ public Color32 Color public bool Alive { get; internal set; } + /// + /// Radius in regions this effect should be shown. Takes the minimum of this . + /// + public byte RelevanceRegions { get; set; } = byte.MaxValue; + + /// + /// Radius in distance this effect should be shown. Takes the minimum of this . + /// + public float RelevanceDistance { get; set; } = float.MaxValue; + + public bool IsDistanceLimited => RelevanceDistance <= 32768f || RelevanceRegions != byte.MaxValue; + public WorldIconInfo(Transform transform, IAssetLink effect, Team? targetTeam = null, WarfarePlayer? targetPlayer = null, Func? playerSelector = null, float lifetimeSec = 0) : this(effect, targetTeam, targetPlayer, playerSelector, lifetimeSec) { @@ -185,24 +200,34 @@ 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) + internal void UpdateRelevantPlayers(IPlayerService playerService, ref PooledTransportConnectionList? list, ref ITransportConnection? single, in Vector3 spawnPosition, HashSet workingHashSetCache) { + bool distanceLimited = IsDistanceLimited; + Vector3 pos = default; if (TargetPlayer != null) { - Add(TargetPlayer.Connection, ref list, ref single, workingHashSetCache); + if (distanceLimited) + pos = TargetPlayer.Position; + if (!distanceLimited || CheckPositionRelevant(in pos, in spawnPosition)) + Add(TargetPlayer.Connection, ref list, ref single, workingHashSetCache); } else if (TargetTeam is not null) { foreach (WarfarePlayer player in playerService.OnlinePlayersOnTeam(TargetTeam)) { - Add(player.Connection, ref list, ref single, workingHashSetCache); + if (distanceLimited) + pos = player.Position; + if (!distanceLimited || CheckPositionRelevant(in pos, in spawnPosition)) + Add(player.Connection, ref list, ref single, workingHashSetCache); } } else if (PlayerSelector != null) { foreach (WarfarePlayer player in playerService.OnlinePlayers) { - if (PlayerSelector(player)) + if (distanceLimited) + pos = player.Position; + if ((!distanceLimited || CheckPositionRelevant(in pos, in spawnPosition)) && PlayerSelector(player)) Add(player.Connection, ref list, ref single, workingHashSetCache); } } @@ -210,7 +235,19 @@ internal void UpdateRelevantPlayers(IPlayerService playerService, ref PooledTran { foreach (WarfarePlayer player in playerService.OnlinePlayers) { - Add(player.Connection, ref list, ref single, workingHashSetCache); + if (distanceLimited) + pos = player.Position; + if (!distanceLimited || CheckPositionRelevant(in pos, in spawnPosition)) + Add(player.Connection, ref list, ref single, workingHashSetCache); + } + } + + if ((distanceLimited || PlayerSelector != null) && _previousPlayers != null) + { + foreach (WarfarePlayer player in _previousPlayers) + { + if (player.IsOnline) + Add(player.Connection, ref list, ref single, workingHashSetCache); } } @@ -236,7 +273,7 @@ static void Add(ITransportConnection connection, ref PooledTransportConnectionLi } } - internal bool ShouldPlayerSeeIcon(WarfarePlayer player) + internal bool ShouldPlayerSeeIcon(WarfarePlayer player, in Vector3 spawnPosition) { if (TargetPlayer != null && !TargetPlayer.Equals(player)) return false; @@ -247,7 +284,31 @@ internal bool ShouldPlayerSeeIcon(WarfarePlayer player) if (PlayerSelector != null && !PlayerSelector(player)) return false; - return true; + return CheckPositionRelevant(player.Position, in spawnPosition); + } + + private bool CheckPositionRelevant(in Vector3 sendPos, in Vector3 spawnPos) + { + float dist = RelevanceDistance; + int area = RelevanceRegions; + if (dist > 32768f && area == byte.MaxValue) + return true; + + byte regionSize = Regions.REGION_SIZE; + if ((area + 0.5f) * regionSize > dist) + { + return MathUtility.SquaredDistance(in sendPos, in spawnPos, true) <= dist * dist; + } + + if (area == byte.MaxValue) + return true; + + int sendX = (int)Math.Floor((sendPos.x + 4096f) / regionSize); + int sendY = (int)Math.Floor((sendPos.z + 4096f) / regionSize); + int spawnX = (int)Math.Floor((spawnPos.x + 4096f) / regionSize); + int spawnY = (int)Math.Floor((spawnPos.z + 4096f) / regionSize); + return sendX >= spawnX - area && sendY >= spawnY - area && + sendX <= spawnX + area && sendY <= spawnY + area; } private static void GetColoredEffectForward(Color32 c, out Vector3 forward, out float scale) @@ -270,7 +331,7 @@ internal bool TryGetSpawnPosition(out Vector3 position) Vector3 v3; if (TransformableObject != null) { - v3 = TransformableObject.Position + Offset; + v3 = TransformableObject.Position; } else if (UnityObject is null) { @@ -278,7 +339,7 @@ internal bool TryGetSpawnPosition(out Vector3 position) } else if (UnityObject != null) { - v3 = UnityObject.position + Offset; + v3 = UnityObject.position; } else { @@ -321,6 +382,9 @@ internal void SpawnEffect(IPlayerService playerService, float rt, bool updatePos return; } + bool distanceLimited = IsDistanceLimited; + bool hasMutablePlayerSelector = distanceLimited || PlayerSelector != null; + Vector3 pos; if (updatePosition) { @@ -329,7 +393,7 @@ internal void SpawnEffect(IPlayerService playerService, float rt, bool updatePos return; } - if (_prevSpawnPosition.IsNearlyEqual(pos) && !_needsRespawn && (!_canTrackLifetime || rt - LastSpawnRealtime < _minimumLifetime)) + if (LastSpawnPosition.IsNearlyEqual(pos) && !(_needsRespawn || hasMutablePlayerSelector) && (!_canTrackLifetime || rt - LastSpawnRealtime < _minimumLifetime)) { return; } @@ -338,7 +402,7 @@ internal void SpawnEffect(IPlayerService playerService, float rt, bool updatePos } else { - pos = _prevSpawnPosition; + pos = LastSpawnPosition; } TriggerEffectParameters parameters = new TriggerEffectParameters(effect); @@ -355,18 +419,49 @@ internal void SpawnEffect(IPlayerService playerService, float rt, bool updatePos parameters.SetUniformScale(_colorScale); } - _prevSpawnPosition = pos; + LastSpawnPosition = pos; _needsRespawn = false; - + + if (hasMutablePlayerSelector) + { + if (_previousPlayers == null) + _previousPlayers = new List(4); + else + _previousPlayers.Clear(); + } + + Vector3 sendPos = default; if (forPlayer != null) { - parameters.SetRelevantPlayer(forPlayer.Connection); + if (distanceLimited) + sendPos = forPlayer.Position; + if (!distanceLimited || CheckPositionRelevant(in sendPos, in pos)) + { + parameters.SetRelevantPlayer(forPlayer.Connection); + if (hasMutablePlayerSelector) + _previousPlayers!.Add(forPlayer); + } + else + { + return; + } } else if (TargetPlayer != null) { - if (TargetPlayer.IsOnline && (TargetTeam is null || TargetPlayer.Team == TargetTeam) && (PlayerSelector == null || PlayerSelector(TargetPlayer))) + if (TargetPlayer.IsOnline) { - parameters.SetRelevantPlayer(TargetPlayer.Connection); + if (distanceLimited) + sendPos = TargetPlayer.Position; + if ((TargetTeam is null || TargetPlayer.Team == TargetTeam) && (!distanceLimited || CheckPositionRelevant(in sendPos, in pos)) && (PlayerSelector == null || PlayerSelector(TargetPlayer))) + { + parameters.SetRelevantPlayer(TargetPlayer.Connection); + if (hasMutablePlayerSelector) + _previousPlayers!.Add(TargetPlayer); + } + else + { + return; + } } else { @@ -379,8 +474,17 @@ internal void SpawnEffect(IPlayerService playerService, float rt, bool updatePos foreach (WarfarePlayer player in playerService.OnlinePlayers) { - if (player.Team == TargetTeam && (PlayerSelector == null || PlayerSelector(player))) + if (distanceLimited) + sendPos = player.Position; + + if (player.Team == TargetTeam + && (!distanceLimited || CheckPositionRelevant(in sendPos, in pos)) + && (PlayerSelector == null || PlayerSelector(player))) + { list.Add(player.Connection); + if (hasMutablePlayerSelector) + _previousPlayers!.Add(player); + } } if (list.Count == 0) @@ -397,14 +501,29 @@ internal void SpawnEffect(IPlayerService playerService, float rt, bool updatePos if (list.Capacity < playerService.OnlinePlayers.Count) list.Capacity = playerService.OnlinePlayers.Count; foreach (WarfarePlayer player in playerService.OnlinePlayers) - list.Add(player.Connection); + { + if (distanceLimited) + sendPos = player.Position; + if (!distanceLimited || CheckPositionRelevant(in sendPos, in pos)) + { + list.Add(player.Connection); + if (hasMutablePlayerSelector) + _previousPlayers!.Add(player); + } + } } else { foreach (WarfarePlayer player in playerService.OnlinePlayers) { - if (PlayerSelector(player)) + if (distanceLimited) + sendPos = player.Position; + if ((!distanceLimited || CheckPositionRelevant(in sendPos, in pos)) && PlayerSelector(player)) + { list.Add(player.Connection); + if (hasMutablePlayerSelector) + _previousPlayers!.Add(player); + } } } @@ -430,10 +549,7 @@ Vector3 ITransformObject.Position TryGetSpawnPosition(out Vector3 position); return position; } - set - { - EffectPosition = value - Offset; - } + set => EffectPosition = value - Offset; } Quaternion ITransformObject.Rotation diff --git a/UncreatedWarfare/Interaction/Icons/WorldIconManager.cs b/UncreatedWarfare/Interaction/Icons/WorldIconManager.cs index 06b5c687..2dc5a516 100644 --- a/UncreatedWarfare/Interaction/Icons/WorldIconManager.cs +++ b/UncreatedWarfare/Interaction/Icons/WorldIconManager.cs @@ -1,5 +1,5 @@ #if DEBUG -//#define ICONS_DEBUG_LOGGING +#define ICONS_DEBUG_LOGGING #endif using SDG.Framework.Utilities; using SDG.NetTransport; @@ -160,9 +160,17 @@ public void RemoveAllIcons() public void RemoveIcon(WorldIconInfo icon) #nullable restore { - if (RemoveIconIntl(icon)) - { + if (icon == null) + return; + + bool recheck = RemoveIconIntl(icon); + + if (recheck) RecheckTickSpeed(); + + if (_iconsByGuid.ContainsKey(icon.Effect.Guid)) + { + UpdateIcon(icon.Effect.Guid); } } @@ -191,7 +199,7 @@ private bool RemoveIconIntl(WorldIconInfo icon) ITransportConnection? singlePlayer = null; PooledTransportConnectionList? pooledList = null; - icon.UpdateRelevantPlayers(_playerService, ref pooledList, ref singlePlayer, WorkingConnectionHashSet); + icon.UpdateRelevantPlayers(_playerService, ref pooledList, ref singlePlayer, in icon.LastSpawnPosition, WorkingConnectionHashSet); WorkingConnectionHashSet.Clear(); if (pooledList != null) @@ -344,7 +352,7 @@ private void ClearEffectsByGuid(Guid guid, List list) PooledTransportConnectionList? pooledList = null; foreach (WorldIconInfo info in list) { - info.UpdateRelevantPlayers(_playerService, ref pooledList, ref singlePlayer, WorkingConnectionHashSet); + info.UpdateRelevantPlayers(_playerService, ref pooledList, ref singlePlayer, in info.LastSpawnPosition, WorkingConnectionHashSet); if (!anyNeedToBeCleared && info.NeedsToBeCleared(rt)) anyNeedToBeCleared = true; } @@ -412,7 +420,10 @@ private void UpdateForPlayer(WarfarePlayer player) bool hasCleared = false; foreach (WorldIconInfo icon in iconSets) { - if (!icon.ShouldPlayerSeeIcon(player)) + Vector3 pos = icon.LastSpawnPosition; + if (icon.IsDistanceLimited) + icon.TryGetSpawnPosition(out pos); + if (!icon.ShouldPlayerSeeIcon(player, in pos)) continue; if (!hasCleared) @@ -457,6 +468,7 @@ private void RemovePlayerSpecificIcons(WarfarePlayer player, Team? team) List? toRemove = null; List? toUpdate = null; + // ReSharper disable once RedundantAssignment float rt = Time.realtimeSinceStartup; foreach (List list in _iconsByGuid.Values) { diff --git a/UncreatedWarfare/Patches/SendBarricadeRegionPatch.cs b/UncreatedWarfare/Patches/SendBarricadeRegionPatch.cs index 03f4c936..1f5b5bcd 100644 --- a/UncreatedWarfare/Patches/SendBarricadeRegionPatch.cs +++ b/UncreatedWarfare/Patches/SendBarricadeRegionPatch.cs @@ -102,15 +102,16 @@ private static bool Prefix(SteamPlayer client, BarricadeRegion region, byte x, b } byte pkt = packet; + int ct = count; SendMultipleBarricades.Invoke(ENetReliability.Reliable, client.transportConnection, writer => { writer.WriteUInt8(x); writer.WriteUInt8(y); writer.WriteNetId(parentNetId); writer.WriteUInt8(pkt); - writer.WriteUInt16((ushort)(count - index)); + writer.WriteUInt16((ushort)(ct - index)); writer.WriteFloat(sortOrder); - for (; index < count; ++index) + for (; index < ct; ++index) { BarricadeDrop drop = region.drops[index]; BarricadeData serversideData = drop.GetServersideData(); diff --git a/UncreatedWarfare/Squads/Spotted/SpottableObjectComponent.cs b/UncreatedWarfare/Squads/Spotted/SpottableObjectComponent.cs index fc5bd348..1a3fa07f 100644 --- a/UncreatedWarfare/Squads/Spotted/SpottableObjectComponent.cs +++ b/UncreatedWarfare/Squads/Spotted/SpottableObjectComponent.cs @@ -63,8 +63,8 @@ private struct MultipleTeamIconPair // 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 + new SpotterTypeStats(SpottedType.FOB, VehicleType.None, 240f, 1.0f, "Effects:Spotted:FOB", Vector3.zero) +]; // null if never spotted private List? _spotters;