diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
index 3414364aad..1795252c1f 100644
--- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
+++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
@@ -256,6 +256,7 @@
+
@@ -328,6 +329,7 @@
+
diff --git a/OpenRA.Mods.Common/Traits/Player/EnemyWatcher.cs b/OpenRA.Mods.Common/Traits/Player/EnemyWatcher.cs
new file mode 100644
index 0000000000..e703837a9e
--- /dev/null
+++ b/OpenRA.Mods.Common/Traits/Player/EnemyWatcher.cs
@@ -0,0 +1,115 @@
+#region Copyright & License Information
+/*
+ * Copyright 2007-2014 The OpenRA Developers (see AUTHORS)
+ * This file is part of OpenRA, which is free software. It is made
+ * available to you under the terms of the GNU General Public License
+ * as published by the Free Software Foundation. For more information,
+ * see COPYING.
+ */
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using OpenRA.Traits;
+
+namespace OpenRA.Mods.Common.Traits
+{
+ [Desc("Tracks neutral and enemy actors' visibility and notifies the player.",
+ "Attach this to the player actor.")]
+ class EnemyWatcherInfo : ITraitInfo
+ {
+ [Desc("Interval in ticks between scanning for enemies.")]
+ public readonly int ScanInterval = 25;
+
+ [Desc("Minimal interval in ticks between notifications.")]
+ public readonly int NotificationInterval = 200;
+
+ public object Create(ActorInitializer init) { return new EnemyWatcher(init.Self, this); }
+ }
+
+ class EnemyWatcher : ITick
+ {
+ readonly EnemyWatcherInfo info;
+ readonly Lazy radarPings;
+
+ bool announcedAny;
+ int rescanInterval;
+ int ticksBeforeNextNotification;
+ HashSet lastKnownActorIds;
+ HashSet visibleActorIds;
+ HashSet playedNotifications;
+
+ public EnemyWatcher(Actor self, EnemyWatcherInfo info)
+ {
+ lastKnownActorIds = new HashSet();
+ this.info = info;
+ rescanInterval = info.ScanInterval;
+ ticksBeforeNextNotification = info.NotificationInterval;
+ radarPings = Exts.Lazy(() => self.World.WorldActor.Trait());
+ }
+
+ public void Tick(Actor self)
+ {
+ // TODO: Make the AI handle such notifications and remove Owner.IsBot from this check
+ // Disable notifications for AI and neutral players (creeps) and for spectators
+ if (self.Owner.Shroud.Disabled || self.Owner.IsBot || !self.Owner.Playable || self.Owner.PlayerReference.Spectating)
+ return;
+
+ rescanInterval--;
+ ticksBeforeNextNotification--;
+
+ if (rescanInterval > 0 || ticksBeforeNextNotification > 0)
+ return;
+
+ rescanInterval = info.ScanInterval;
+
+ announcedAny = false;
+ visibleActorIds = new HashSet();
+ playedNotifications = new HashSet();
+
+ foreach (var actor in self.World.ActorsWithTrait())
+ {
+ // We only care about enemy actors (creeps should be enemies)
+ if ((actor.Actor.EffectiveOwner != null && self.Owner.Stances[actor.Actor.EffectiveOwner.Owner] != Stance.Enemy)
+ || self.Owner.Stances[actor.Actor.Owner] != Stance.Enemy)
+ continue;
+
+ // The actor is not currently visible
+ if (!self.Owner.Shroud.IsVisible(actor.Actor))
+ continue;
+
+ visibleActorIds.Add(actor.Actor.ActorID);
+
+ // We already know about this actor
+ if (lastKnownActorIds.Contains(actor.Actor.ActorID))
+ continue;
+
+ // We have already played this type of notification
+ if (playedNotifications.Contains(actor.Trait.Info.Notification))
+ continue;
+
+ Announce(self, actor);
+ }
+
+ if (announcedAny)
+ ticksBeforeNextNotification = info.NotificationInterval;
+
+ lastKnownActorIds = visibleActorIds;
+ }
+
+ void Announce(Actor self, TraitPair announce)
+ {
+ // Audio notification
+ if (self.World.LocalPlayer != null)
+ Sound.PlayNotification(self.World.Map.Rules, self.World.LocalPlayer, "Speech", announce.Trait.Info.Notification, self.Owner.Country.Race);
+
+ // Radar notificaion
+ if (announce.Trait.Info.PingRadar && radarPings.Value != null)
+ radarPings.Value.Add(() => true, announce.Actor.CenterPosition, Color.Red, 50);
+
+ playedNotifications.Add(announce.Trait.Info.Notification);
+ announcedAny = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/OpenRA.Mods.Common/Traits/Sound/AnnounceOnSeen.cs b/OpenRA.Mods.Common/Traits/Sound/AnnounceOnSeen.cs
new file mode 100644
index 0000000000..f7277d645a
--- /dev/null
+++ b/OpenRA.Mods.Common/Traits/Sound/AnnounceOnSeen.cs
@@ -0,0 +1,35 @@
+#region Copyright & License Information
+/*
+ * Copyright 2007-2014 The OpenRA Developers (see AUTHORS)
+ * This file is part of OpenRA, which is free software. It is made
+ * available to you under the terms of the GNU General Public License
+ * as published by the Free Software Foundation. For more information,
+ * see COPYING.
+ */
+#endregion
+
+using OpenRA.Traits;
+
+namespace OpenRA.Mods.Common.Traits
+{
+ [Desc("Players will be notified when this actor becomes visible to them.")]
+ public class AnnounceOnSeenInfo : ITraitInfo
+ {
+ [Desc("Should there be a radar ping on enemies' radar at the actor's location when they see him")]
+ public readonly bool PingRadar = false;
+
+ public readonly string Notification = "EnemyUnitSighted";
+
+ public object Create(ActorInitializer init) { return new AnnounceOnSeen(this); }
+ }
+
+ public class AnnounceOnSeen
+ {
+ public readonly AnnounceOnSeenInfo Info;
+
+ public AnnounceOnSeen(AnnounceOnSeenInfo info)
+ {
+ Info = info;
+ }
+ }
+}
\ No newline at end of file