diff --git a/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj b/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj
index be236ea26a..ce55cb81f7 100644
--- a/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj
+++ b/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj
@@ -550,6 +550,7 @@
+
diff --git a/OpenRA.Mods.RA/UpgradeActorsNear.cs b/OpenRA.Mods.RA/UpgradeActorsNear.cs
new file mode 100644
index 0000000000..070805eb57
--- /dev/null
+++ b/OpenRA.Mods.RA/UpgradeActorsNear.cs
@@ -0,0 +1,117 @@
+#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 OpenRA.GameRules;
+using OpenRA.Traits;
+
+namespace OpenRA.Mods.RA
+{
+ [Desc("Applies an upgrade to actors within a specified range.")]
+ public class UpgradeActorsNearInfo : ITraitInfo
+ {
+ [Desc("The upgrades to grant.")]
+ public readonly string[] Upgrades = { };
+
+ [Desc("The range to search for actors to upgrade.")]
+ public readonly WRange Range = WRange.FromCells(3);
+
+ [Desc("What diplomatic stances are affected.")]
+ public readonly Stance ValidStances = Stance.Ally;
+
+ [Desc("Grant the upgrades apply to this actor.")]
+ public readonly bool AffectsParent = false;
+
+ public readonly string EnableSound = null;
+ public readonly string DisableSound = null;
+
+ public object Create(ActorInitializer init) { return new UpgradeActorsNear(init.self, this); }
+ }
+
+ public class UpgradeActorsNear : ITick, INotifyAddedToWorld, INotifyRemovedFromWorld
+ {
+ readonly UpgradeActorsNearInfo info;
+ readonly Actor self;
+
+ int proximityTrigger;
+ WPos cachedPosition;
+ WRange cachedRange;
+ WRange desiredRange;
+
+ bool cachedDisabled = true;
+
+ public UpgradeActorsNear(Actor self, UpgradeActorsNearInfo info)
+ {
+ this.info = info;
+ this.self = self;
+ cachedRange = info.Range;
+ }
+
+ public void AddedToWorld(Actor self)
+ {
+ cachedPosition = self.CenterPosition;
+ proximityTrigger = self.World.ActorMap.AddProximityTrigger(cachedPosition, cachedRange, ActorEntered, ActorExited);
+ }
+
+ public void RemovedFromWorld(Actor self)
+ {
+ self.World.ActorMap.RemoveProximityTrigger(proximityTrigger);
+ }
+
+ public void Tick(Actor self)
+ {
+ var disabled = self.IsDisabled();
+
+ if (cachedDisabled != disabled)
+ {
+ Sound.Play(disabled ? info.DisableSound : info.EnableSound, self.CenterPosition);
+ desiredRange = disabled ? WRange.Zero : info.Range;
+ cachedDisabled = disabled;
+ }
+
+ if (self.CenterPosition != cachedPosition || desiredRange != cachedRange)
+ {
+ cachedPosition = self.CenterPosition;
+ cachedRange = desiredRange;
+ self.World.ActorMap.UpdateProximityTrigger(proximityTrigger, cachedPosition, cachedRange);
+ }
+ }
+
+ void ActorEntered(Actor a)
+ {
+ if (a.Destroyed)
+ return;
+
+ if (a == self && !info.AffectsParent)
+ return;
+
+ var stance = self.Owner.Stances[a.Owner];
+ if (!info.ValidStances.HasFlag(stance))
+ return;
+
+ var um = a.TraitOrDefault();
+ if (um != null)
+ foreach (var u in info.Upgrades)
+ um.GrantUpgrade(a, u, this);
+ }
+
+ void ActorExited(Actor a)
+ {
+ if (a == self || a.Destroyed)
+ return;
+
+ var um = a.TraitOrDefault();
+ if (um != null)
+ foreach (var u in info.Upgrades)
+ um.RevokeUpgrade(a, u, this);
+ }
+ }
+}