diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
index ce4c980fff..73c779a779 100644
--- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
+++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
@@ -802,6 +802,8 @@
+
+
diff --git a/OpenRA.Mods.Common/Traits/EntersTunnels.cs b/OpenRA.Mods.Common/Traits/EntersTunnels.cs
new file mode 100644
index 0000000000..36480467b3
--- /dev/null
+++ b/OpenRA.Mods.Common/Traits/EntersTunnels.cs
@@ -0,0 +1,132 @@
+#region Copyright & License Information
+/*
+ * Copyright 2007-2017 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, either version 3 of
+ * the License, or (at your option) any later version. For more
+ * information, see COPYING.
+ */
+#endregion
+
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using OpenRA.Mods.Common.Orders;
+using OpenRA.Traits;
+
+namespace OpenRA.Mods.Common.Traits
+{
+ [Desc("This actor can interact with TunnelEntrances to move through TerrainTunnels.")]
+ public class EntersTunnelsInfo : ITraitInfo, Requires
+ {
+ public readonly string EnterCursor = "enter";
+ public readonly string EnterBlockedCursor = "enter-blocked";
+
+ [VoiceReference] public readonly string Voice = "Action";
+
+ public object Create(ActorInitializer init) { return new EntersTunnels(init.Self, this); }
+ }
+
+ public class EntersTunnels : IIssueOrder, IResolveOrder, IOrderVoice
+ {
+ readonly EntersTunnelsInfo info;
+ readonly IMove move;
+
+ public EntersTunnels(Actor self, EntersTunnelsInfo info)
+ {
+ this.info = info;
+ move = self.Trait();
+ }
+
+ public IEnumerable Orders
+ {
+ get
+ {
+ yield return new EnterTunnelOrderTargeter(info);
+ }
+ }
+
+ public Order IssueOrder(Actor self, IOrderTargeter order, Target target, bool queued)
+ {
+ if (order.OrderID != "EnterTunnel")
+ return null;
+
+ if (target.Type == TargetType.FrozenActor)
+ return new Order(order.OrderID, self, queued) { ExtraData = target.FrozenActor.ID, SuppressVisualFeedback = true };
+
+ return new Order(order.OrderID, self, queued) { TargetActor = target.Actor, SuppressVisualFeedback = true };
+ }
+
+ public string VoicePhraseForOrder(Actor self, Order order)
+ {
+ return order.OrderString == "EnterTunnel" ? info.Voice : null;
+ }
+
+ public void ResolveOrder(Actor self, Order order)
+ {
+ if (order.OrderString != "EnterTunnel")
+ return;
+
+ var target = self.ResolveFrozenActorOrder(order, Color.Red);
+ if (target.Type != TargetType.Actor)
+ return;
+
+ var tunnel = target.Actor.TraitOrDefault();
+ if (!tunnel.Exit.HasValue)
+ return;
+
+ if (!order.Queued)
+ self.CancelActivity();
+
+ self.SetTargetLine(Target.FromCell(self.World, tunnel.Exit.Value), Color.Green);
+ self.QueueActivity(move.MoveTo(tunnel.Entrance, tunnel.NearEnough));
+ self.QueueActivity(move.MoveTo(tunnel.Exit.Value, tunnel.NearEnough));
+ }
+
+ class EnterTunnelOrderTargeter : UnitOrderTargeter
+ {
+ readonly EntersTunnelsInfo info;
+
+ public EnterTunnelOrderTargeter(EntersTunnelsInfo info)
+ : base("EnterTunnel", 6, info.EnterCursor, true, true)
+ {
+ this.info = info;
+ }
+
+ public override bool CanTargetActor(Actor self, Actor target, TargetModifiers modifiers, ref string cursor)
+ {
+ if (target.IsDead)
+ return false;
+
+ var tunnel = target.TraitOrDefault();
+ if (tunnel == null)
+ return false;
+
+ // HACK: The engine does not support HiddenUnderFog combined with buildings that use the "_" footprint
+ // We therefore have to use AlwaysVisible and then force-disable interacting with the entrance under shroud
+ var buildingInfo = target.Info.TraitInfoOrDefault();
+ if (buildingInfo != null)
+ {
+ var footprint = FootprintUtils.PathableTiles(target.Info.Name, buildingInfo, target.Location);
+ if (footprint.All(c => self.World.ShroudObscures(c)))
+ return false;
+ }
+
+ if (!tunnel.Exit.HasValue)
+ {
+ cursor = info.EnterBlockedCursor;
+ return false;
+ }
+
+ cursor = info.EnterCursor;
+ return true;
+ }
+
+ public override bool CanTargetFrozenActor(Actor self, FrozenActor target, TargetModifiers modifiers, ref string cursor)
+ {
+ return CanTargetActor(self, target.Actor, modifiers, ref cursor);
+ }
+ }
+ }
+}
diff --git a/OpenRA.Mods.Common/Traits/TunnelEntrance.cs b/OpenRA.Mods.Common/Traits/TunnelEntrance.cs
new file mode 100644
index 0000000000..8e4afcef64
--- /dev/null
+++ b/OpenRA.Mods.Common/Traits/TunnelEntrance.cs
@@ -0,0 +1,74 @@
+#region Copyright & License Information
+/*
+ * Copyright 2007-2017 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, either version 3 of
+ * the License, or (at your option) any later version. For more
+ * information, see COPYING.
+ */
+#endregion
+
+using System;
+using System.Linq;
+using OpenRA.Traits;
+
+namespace OpenRA.Mods.Common.Traits
+{
+ [Desc("Provides a target for players to issue orders for units to move through a TerrainTunnel.",
+ "The host actor should be placed so that the Sensor position overlaps one of the TerrainTunnel portal cells.")]
+ public class TunnelEntranceInfo : ITraitInfo
+ {
+ [FieldLoader.Require]
+ [Desc("Offset to use as a staging point for actors entering or exiting the tunnel.",
+ "Should be at least Margin cells away from the actual entrance.")]
+ public readonly CVec RallyPoint = CVec.Zero;
+
+ [Desc("Cell radius to use as a staging area around the RallyPoint.")]
+ public readonly int Margin = 2;
+
+ [Desc("Offset to check for the corresponding TerrainTunnel portal cell(s).")]
+ public readonly CVec Sensor = CVec.Zero;
+
+ public object Create(ActorInitializer init) { return new TunnelEntrance(init.Self, this); }
+ }
+
+ public class TunnelEntrance : INotifyCreated
+ {
+ readonly TunnelEntranceInfo info;
+
+ public readonly CPos Entrance;
+ public CPos? Exit { get; private set; }
+ public int NearEnough { get { return info.Margin; } }
+
+ public TunnelEntrance(Actor self, TunnelEntranceInfo info)
+ {
+ this.info = info;
+
+ Entrance = self.Location + info.RallyPoint;
+ }
+
+ void INotifyCreated.Created(Actor self)
+ {
+ // Find the map tunnel associated with this entrance
+ var sensor = self.Location + info.Sensor;
+ var tunnel = self.World.WorldActor.Info.TraitInfos()
+ .FirstOrDefault(tti => tti.PortalCells().Contains(sensor));
+
+ if (tunnel != null)
+ {
+ // Find the matching entrance at the other end of the tunnel
+ // Run at the end of the tick to make sure that all the entrances exist in the world
+ self.World.AddFrameEndTask(w =>
+ {
+ var portalCells = tunnel.PortalCells().ToList();
+ var other = self.World.ActorsWithTrait()
+ .FirstOrDefault(x => x.Actor != self && portalCells.Contains(x.Actor.Location + x.Trait.info.Sensor));
+
+ if (other.Trait != null)
+ Exit = other.Trait.Entrance;
+ });
+ }
+ }
+ }
+}
diff --git a/mods/ts/rules/defaults.yaml b/mods/ts/rules/defaults.yaml
index a9c635d3ba..ffafb54be1 100644
--- a/mods/ts/rules/defaults.yaml
+++ b/mods/ts/rules/defaults.yaml
@@ -381,6 +381,8 @@
ReferencePoint: Bottom, Right
RequiresCondition: hospitalheal
RevealOnFire:
+ EntersTunnels:
+ Voice: Move
^RegularInfantryDeath:
WithDeathAnimation@normal:
@@ -565,6 +567,8 @@
Carryable:
RequiresCondition: !inside-tunnel
RevealOnFire:
+ EntersTunnels:
+ Voice: Move
^Tank:
Inherits: ^Vehicle
@@ -710,6 +714,8 @@
Guardable:
WithFacingSpriteBody:
RevealOnFire:
+ EntersTunnels:
+ Voice: Move
^BlossomTree:
Inherits@1: ^SpriteActor
@@ -883,6 +889,8 @@
CustomBounds: 144, 144
Targetable:
AlwaysVisible:
+ TunnelEntrance:
+ Dimensions: 3, 3
^Gate:
Inherits: ^Building
diff --git a/mods/ts/rules/misc.yaml b/mods/ts/rules/misc.yaml
index 45d14be609..3d72f4e558 100644
--- a/mods/ts/rules/misc.yaml
+++ b/mods/ts/rules/misc.yaml
@@ -213,12 +213,24 @@ TRACKS16:
TUNTOP01:
Inherits: ^Tunnel
+ TunnelEntrance:
+ RallyPoint: -3, 1
+ Sensor: 0, 1
TUNTOP02:
Inherits: ^Tunnel
+ TunnelEntrance:
+ RallyPoint: 1, -3
+ Sensor: 1, 0
TUNTOP03:
Inherits: ^Tunnel
+ TunnelEntrance:
+ RallyPoint: 3, 1
+ Sensor: 0, 1
TUNTOP04:
Inherits: ^Tunnel
+ TunnelEntrance:
+ RallyPoint: 1, 3
+ Sensor: 1, 0