From 81343926b674796b8bb7c26f08c20b9d23ed32dd Mon Sep 17 00:00:00 2001 From: reaperrr Date: Wed, 14 Mar 2018 20:51:16 +0100 Subject: [PATCH] Split Locomotor trait from Mobile Add GrantConditionOn*Layer traits This allows to - drop some booleans from Locomotor - drop a good part of the subterranean- and jumpjet-specific code/hacks from Mobile - grant more than 1 condition per layer type (via multiple traits) - easily add more traits of this kind for other layers --- OpenRA.Mods.Cnc/Traits/Mine.cs | 4 +- OpenRA.Mods.Common/AI/AIHarvesterManager.cs | 8 +- OpenRA.Mods.Common/AI/States/NavyStates.cs | 6 +- .../Activities/FindResources.cs | 10 +- OpenRA.Mods.Common/Activities/Move/Move.cs | 6 +- .../Activities/Move/MoveAdjacentTo.cs | 8 +- OpenRA.Mods.Common/OpenRA.Mods.Common.csproj | 7 + OpenRA.Mods.Common/Pathfinder/PathGraph.cs | 18 +- OpenRA.Mods.Common/Pathfinder/PathSearch.cs | 12 +- .../Scripting/Global/ReinforcementsGlobal.cs | 11 +- .../GrantConditionOnJumpjetLayer.cs | 59 +++ .../Conditions/GrantConditionOnLayer.cs | 73 +++ .../GrantConditionOnSubterraneanLayer.cs | 94 ++++ .../Conditions/GrantConditionOnTunnelLayer.cs | 26 + OpenRA.Mods.Common/Traits/Crates/Crate.cs | 2 +- OpenRA.Mods.Common/Traits/Crushable.cs | 2 +- OpenRA.Mods.Common/Traits/Harvester.cs | 4 +- OpenRA.Mods.Common/Traits/Mobile.cs | 449 +++--------------- .../Traits/ProductionFromMapEdge.cs | 6 +- .../Traits/World/DomainIndex.cs | 9 +- .../Traits/World/ElevatedBridgeLayer.cs | 6 +- .../Traits/World/JumpjetActorLayer.cs | 19 +- .../Traits/World/JumpjetLocomotor.cs | 36 ++ OpenRA.Mods.Common/Traits/World/Locomotor.cs | 302 ++++++++++++ OpenRA.Mods.Common/Traits/World/PathFinder.cs | 22 +- .../Traits/World/SubterraneanActorLayer.cs | 19 +- .../Traits/World/SubterraneanLocomotor.cs | 40 ++ .../Traits/World/TerrainTunnelLayer.cs | 6 +- OpenRA.Mods.Common/TraitsInterfaces.cs | 24 +- 29 files changed, 813 insertions(+), 475 deletions(-) create mode 100644 OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnJumpjetLayer.cs create mode 100644 OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnLayer.cs create mode 100644 OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnSubterraneanLayer.cs create mode 100644 OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnTunnelLayer.cs create mode 100644 OpenRA.Mods.Common/Traits/World/JumpjetLocomotor.cs create mode 100644 OpenRA.Mods.Common/Traits/World/Locomotor.cs create mode 100644 OpenRA.Mods.Common/Traits/World/SubterraneanLocomotor.cs diff --git a/OpenRA.Mods.Cnc/Traits/Mine.cs b/OpenRA.Mods.Cnc/Traits/Mine.cs index 07dd13761e..e24b488381 100644 --- a/OpenRA.Mods.Cnc/Traits/Mine.cs +++ b/OpenRA.Mods.Cnc/Traits/Mine.cs @@ -45,10 +45,10 @@ namespace OpenRA.Mods.Cnc.Traits return; var mobile = crusher.TraitOrDefault(); - if (mobile != null && !info.DetonateClasses.Overlaps(mobile.Info.Crushes)) + if (mobile != null && !info.DetonateClasses.Overlaps(mobile.Info.LocomotorInfo.Crushes)) return; - self.Kill(crusher, mobile != null ? mobile.Info.CrushDamageTypes : new HashSet()); + self.Kill(crusher, mobile != null ? mobile.Info.LocomotorInfo.CrushDamageTypes : new HashSet()); } bool ICrushable.CrushableBy(Actor self, Actor crusher, HashSet crushClasses) diff --git a/OpenRA.Mods.Common/AI/AIHarvesterManager.cs b/OpenRA.Mods.Common/AI/AIHarvesterManager.cs index 986ef65402..c7fd65d877 100644 --- a/OpenRA.Mods.Common/AI/AIHarvesterManager.cs +++ b/OpenRA.Mods.Common/AI/AIHarvesterManager.cs @@ -41,16 +41,16 @@ namespace OpenRA.Mods.Common.AI CPos FindNextResource(Actor actor, Harvester harv) { - var mobileInfo = actor.Info.TraitInfo(); - var passable = (uint)mobileInfo.GetMovementClass(world.Map.Rules.TileSet); + var locomotorInfo = actor.Info.TraitInfo().LocomotorInfo; + var passable = (uint)locomotorInfo.GetMovementClass(World.Map.Rules.TileSet); Func isValidResource = cell => - domainIndex.IsPassable(actor.Location, cell, mobileInfo, passable) && + domainIndex.IsPassable(actor.Location, cell, locomotorInfo, passable) && harv.CanHarvestCell(actor, cell) && claimLayer.CanClaimCell(actor, cell); var path = pathfinder.FindPath( - PathSearch.Search(world, mobileInfo, actor, true, isValidResource) + PathSearch.Search(world, locomotorInfo, actor, true, isValidResource) .WithCustomCost(loc => world.FindActorsInCircle(world.Map.CenterOfCell(loc), ai.Info.HarvesterEnemyAvoidanceRadius) .Where(u => !u.IsDead && actor.Owner.Stances[u.Owner] == Stance.Enemy) .Sum(u => Math.Max(WDist.Zero.Length, ai.Info.HarvesterEnemyAvoidanceRadius.Length - (world.Map.CenterOfCell(loc) - u.CenterPosition).Length))) diff --git a/OpenRA.Mods.Common/AI/States/NavyStates.cs b/OpenRA.Mods.Common/AI/States/NavyStates.cs index fa57a2cc92..da14dbc750 100644 --- a/OpenRA.Mods.Common/AI/States/NavyStates.cs +++ b/OpenRA.Mods.Common/AI/States/NavyStates.cs @@ -30,12 +30,12 @@ namespace OpenRA.Mods.Common.AI // (Way better than finding a nearest target which is likely to be on Ground) // You might be tempted to move these lookups into Activate() but that causes null reference exception. var domainIndex = first.World.WorldActor.Trait(); - var mobileInfo = first.Info.TraitInfo(); - var passable = (uint)mobileInfo.GetMovementClass(first.World.Map.Rules.TileSet); + var locomotorInfo = first.Info.TraitInfo().LocomotorInfo; + var passable = (uint)locomotorInfo.GetMovementClass(first.World.Map.Rules.TileSet); var navalProductions = owner.World.ActorsHavingTrait().Where(a => owner.Bot.Info.BuildingCommonNames.NavalProduction.Contains(a.Info.Name) - && domainIndex.IsPassable(first.Location, a.Location, mobileInfo, passable) + && domainIndex.IsPassable(first.Location, a.Location, locomotorInfo, passable) && a.AppearsHostileTo(first)); if (navalProductions.Any()) diff --git a/OpenRA.Mods.Common/Activities/FindResources.cs b/OpenRA.Mods.Common/Activities/FindResources.cs index 5d755433da..97870cd214 100644 --- a/OpenRA.Mods.Common/Activities/FindResources.cs +++ b/OpenRA.Mods.Common/Activities/FindResources.cs @@ -23,7 +23,7 @@ namespace OpenRA.Mods.Common.Activities readonly Harvester harv; readonly HarvesterInfo harvInfo; readonly Mobile mobile; - readonly MobileInfo mobileInfo; + readonly LocomotorInfo locomotorInfo; readonly ResourceClaimLayer claimLayer; readonly IPathFinder pathFinder; readonly DomainIndex domainIndex; @@ -35,7 +35,7 @@ namespace OpenRA.Mods.Common.Activities harv = self.Trait(); harvInfo = self.Info.TraitInfo(); mobile = self.Trait(); - mobileInfo = self.Info.TraitInfo(); + locomotorInfo = mobile.Info.LocomotorInfo; claimLayer = self.World.WorldActor.Trait(); pathFinder = self.World.WorldActor.Trait(); domainIndex = self.World.WorldActor.Trait(); @@ -126,10 +126,10 @@ namespace OpenRA.Mods.Common.Activities var searchRadiusSquared = searchRadius * searchRadius; // Find any harvestable resources: - var passable = (uint)mobileInfo.GetMovementClass(self.World.Map.Rules.TileSet); + var passable = (uint)locomotorInfo.GetMovementClass(self.World.Map.Rules.TileSet); List path; - using (var search = PathSearch.Search(self.World, mobileInfo, self, true, loc => - domainIndex.IsPassable(self.Location, loc, mobileInfo, passable) && harv.CanHarvestCell(self, loc) && claimLayer.CanClaimCell(self, loc)) + using (var search = PathSearch.Search(self.World, locomotorInfo, self, true, loc => + domainIndex.IsPassable(self.Location, loc, locomotorInfo, passable) && harv.CanHarvestCell(self, loc) && claimLayer.CanClaimCell(self, loc)) .WithCustomCost(loc => { if ((avoidCell.HasValue && loc == avoidCell.Value) || diff --git a/OpenRA.Mods.Common/Activities/Move/Move.cs b/OpenRA.Mods.Common/Activities/Move/Move.cs index 1480b33d13..1ebdea5b71 100644 --- a/OpenRA.Mods.Common/Activities/Move/Move.cs +++ b/OpenRA.Mods.Common/Activities/Move/Move.cs @@ -51,7 +51,7 @@ namespace OpenRA.Mods.Common.Activities { List path; using (var search = - PathSearch.FromPoint(self.World, mobile.Info, self, mobile.ToCell, destination, false) + PathSearch.FromPoint(self.World, mobile.Info.LocomotorInfo, self, mobile.ToCell, destination, false) .WithoutLaneBias()) path = self.World.WorldActor.Trait().FindPath(search); return path; @@ -293,7 +293,9 @@ namespace OpenRA.Mods.Common.Activities // Wait a bit to see if they leave if (!hasWaited) { - waitTicksRemaining = mobile.Info.WaitAverage + self.World.SharedRandom.Next(-mobile.Info.WaitSpread, mobile.Info.WaitSpread); + waitTicksRemaining = mobile.Info.LocomotorInfo.WaitAverage + + self.World.SharedRandom.Next(-mobile.Info.LocomotorInfo.WaitSpread, mobile.Info.LocomotorInfo.WaitSpread); + hasWaited = true; } diff --git a/OpenRA.Mods.Common/Activities/Move/MoveAdjacentTo.cs b/OpenRA.Mods.Common/Activities/Move/MoveAdjacentTo.cs index 478e22f283..db32440d72 100644 --- a/OpenRA.Mods.Common/Activities/Move/MoveAdjacentTo.cs +++ b/OpenRA.Mods.Common/Activities/Move/MoveAdjacentTo.cs @@ -56,7 +56,7 @@ namespace OpenRA.Mods.Common.Activities Mobile = self.Trait(); pathFinder = self.World.WorldActor.Trait(); domainIndex = self.World.WorldActor.Trait(); - movementClass = (uint)Mobile.Info.GetMovementClass(self.World.Map.Rules.TileSet); + movementClass = (uint)Mobile.Info.LocomotorInfo.GetMovementClass(self.World.Map.Rules.TileSet); if (target.IsValidFor(self)) targetPosition = self.World.Map.CellContaining(target.CenterPosition); @@ -142,14 +142,14 @@ namespace OpenRA.Mods.Common.Activities var loc = self.Location; foreach (var cell in targetCells) - if (domainIndex.IsPassable(loc, cell, Mobile.Info, movementClass) && Mobile.CanEnterCell(cell)) + if (domainIndex.IsPassable(loc, cell, Mobile.Info.LocomotorInfo, movementClass) && Mobile.CanEnterCell(cell)) searchCells.Add(cell); if (!searchCells.Any()) return NoPath; - using (var fromSrc = PathSearch.FromPoints(self.World, Mobile.Info, self, searchCells, loc, true)) - using (var fromDest = PathSearch.FromPoint(self.World, Mobile.Info, self, loc, targetPosition, true).Reverse()) + using (var fromSrc = PathSearch.FromPoints(self.World, Mobile.Info.LocomotorInfo, self, searchCells, loc, true)) + using (var fromDest = PathSearch.FromPoint(self.World, Mobile.Info.LocomotorInfo, self, loc, targetPosition, true).Reverse()) return pathFinder.FindBidiPath(fromSrc, fromDest); } diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj index 56ca4e5174..1e481b9144 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -524,6 +524,10 @@ + + + + @@ -534,6 +538,9 @@ + + + diff --git a/OpenRA.Mods.Common/Pathfinder/PathGraph.cs b/OpenRA.Mods.Common/Pathfinder/PathGraph.cs index e7e0bf10f4..e38a0cd935 100644 --- a/OpenRA.Mods.Common/Pathfinder/PathGraph.cs +++ b/OpenRA.Mods.Common/Pathfinder/PathGraph.cs @@ -84,8 +84,8 @@ namespace OpenRA.Mods.Common.Pathfinder public Actor IgnoreActor { get; set; } readonly CellConditions checkConditions; - readonly MobileInfo mobileInfo; - readonly MobileInfo.WorldMovementInfo worldMovementInfo; + readonly LocomotorInfo locomotorInfo; + readonly LocomotorInfo.WorldMovementInfo worldMovementInfo; readonly CellInfoLayerPool.PooledCellInfoLayer pooledLayer; readonly bool checkTerrainHeight; CellLayer groundInfo; @@ -93,19 +93,19 @@ namespace OpenRA.Mods.Common.Pathfinder readonly Dictionary>> customLayerInfo = new Dictionary>>(); - public PathGraph(CellInfoLayerPool layerPool, MobileInfo mobileInfo, Actor actor, World world, bool checkForBlocked) + public PathGraph(CellInfoLayerPool layerPool, LocomotorInfo li, Actor actor, World world, bool checkForBlocked) { pooledLayer = layerPool.Get(); groundInfo = pooledLayer.GetLayer(); + locomotorInfo = li; var layers = world.GetCustomMovementLayers().Values - .Where(cml => cml.EnabledForActor(actor.Info, mobileInfo)); + .Where(cml => cml.EnabledForActor(actor.Info, locomotorInfo)); foreach (var cml in layers) customLayerInfo[cml.Index] = Pair.New(cml, pooledLayer.GetLayer()); World = world; - this.mobileInfo = mobileInfo; - worldMovementInfo = mobileInfo.GetWorldMovementInfo(world); + worldMovementInfo = locomotorInfo.GetWorldMovementInfo(world); Actor = actor; LaneBias = 1; checkConditions = checkForBlocked ? CellConditions.TransientActors : CellConditions.None; @@ -153,7 +153,7 @@ namespace OpenRA.Mods.Common.Pathfinder foreach (var cli in customLayerInfo.Values) { var layerPosition = new CPos(position.X, position.Y, cli.First.Index); - var entryCost = cli.First.EntryMovementCost(Actor.Info, mobileInfo, layerPosition); + var entryCost = cli.First.EntryMovementCost(Actor.Info, locomotorInfo, layerPosition); if (entryCost != Constants.InvalidNode) validNeighbors.Add(new GraphConnection(layerPosition, entryCost)); } @@ -161,7 +161,7 @@ namespace OpenRA.Mods.Common.Pathfinder else { var layerPosition = new CPos(position.X, position.Y, 0); - var exitCost = customLayerInfo[position.Layer].First.ExitMovementCost(Actor.Info, mobileInfo, layerPosition); + var exitCost = customLayerInfo[position.Layer].First.ExitMovementCost(Actor.Info, locomotorInfo, layerPosition); if (exitCost != Constants.InvalidNode) validNeighbors.Add(new GraphConnection(layerPosition, exitCost)); } @@ -171,7 +171,7 @@ namespace OpenRA.Mods.Common.Pathfinder int GetCostToNode(CPos destNode, CVec direction) { - var movementCost = mobileInfo.MovementCostToEnterCell(worldMovementInfo, Actor, destNode, IgnoreActor, checkConditions); + var movementCost = locomotorInfo.MovementCostToEnterCell(worldMovementInfo, Actor, destNode, IgnoreActor, checkConditions); if (movementCost != int.MaxValue && !(CustomBlock != null && CustomBlock(destNode))) return CalculateCellCost(destNode, direction, movementCost); diff --git a/OpenRA.Mods.Common/Pathfinder/PathSearch.cs b/OpenRA.Mods.Common/Pathfinder/PathSearch.cs index ca7c75a5cd..268a4ece83 100644 --- a/OpenRA.Mods.Common/Pathfinder/PathSearch.cs +++ b/OpenRA.Mods.Common/Pathfinder/PathSearch.cs @@ -45,18 +45,18 @@ namespace OpenRA.Mods.Common.Pathfinder considered = new LinkedList>(); } - public static IPathSearch Search(World world, MobileInfo mi, Actor self, bool checkForBlocked, Func goalCondition) + public static IPathSearch Search(World world, LocomotorInfo li, Actor self, bool checkForBlocked, Func goalCondition) { - var graph = new PathGraph(LayerPoolForWorld(world), mi, self, world, checkForBlocked); + var graph = new PathGraph(LayerPoolForWorld(world), li, self, world, checkForBlocked); var search = new PathSearch(graph); search.isGoal = goalCondition; search.heuristic = loc => 0; return search; } - public static IPathSearch FromPoint(World world, MobileInfo mi, Actor self, CPos from, CPos target, bool checkForBlocked) + public static IPathSearch FromPoint(World world, LocomotorInfo li, Actor self, CPos from, CPos target, bool checkForBlocked) { - var graph = new PathGraph(LayerPoolForWorld(world), mi, self, world, checkForBlocked); + var graph = new PathGraph(LayerPoolForWorld(world), li, self, world, checkForBlocked); var search = new PathSearch(graph) { heuristic = DefaultEstimator(target) @@ -74,9 +74,9 @@ namespace OpenRA.Mods.Common.Pathfinder return search; } - public static IPathSearch FromPoints(World world, MobileInfo mi, Actor self, IEnumerable froms, CPos target, bool checkForBlocked) + public static IPathSearch FromPoints(World world, LocomotorInfo li, Actor self, IEnumerable froms, CPos target, bool checkForBlocked) { - var graph = new PathGraph(LayerPoolForWorld(world), mi, self, world, checkForBlocked); + var graph = new PathGraph(LayerPoolForWorld(world), li, self, world, checkForBlocked); var search = new PathSearch(graph) { heuristic = DefaultEstimator(target) diff --git a/OpenRA.Mods.Common/Scripting/Global/ReinforcementsGlobal.cs b/OpenRA.Mods.Common/Scripting/Global/ReinforcementsGlobal.cs index 55db7e11f5..63c86a2f57 100644 --- a/OpenRA.Mods.Common/Scripting/Global/ReinforcementsGlobal.cs +++ b/OpenRA.Mods.Common/Scripting/Global/ReinforcementsGlobal.cs @@ -161,12 +161,13 @@ namespace OpenRA.Mods.Common.Scripting { var mobiles = cargo != null ? cargo.Passengers.Select(a => { - var mobileInfo = a.Info.TraitInfoOrDefault(); - if (mobileInfo == null) - return new Pair(null, 0); + var mobile = a.TraitOrDefault(); + if (mobile == null) + return new Pair(null, 0); - return new Pair(mobileInfo, (uint)mobileInfo.GetMovementClass(a.World.Map.Rules.TileSet)); - }) : new Pair[0]; + var locomotorInfo = mobile.Info.LocomotorInfo; + return new Pair(locomotorInfo, (uint)locomotorInfo.GetMovementClass(a.World.Map.Rules.TileSet)); + }) : new Pair[0]; foreach (var c in transport.World.Map.FindTilesInCircle(destination, dropRange)) { diff --git a/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnJumpjetLayer.cs b/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnJumpjetLayer.cs new file mode 100644 index 0000000000..92678a84ab --- /dev/null +++ b/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnJumpjetLayer.cs @@ -0,0 +1,59 @@ +#region Copyright & License Information +/* + * Copyright 2007-2018 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 OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + public class GrantConditionOnJumpjetLayerInfo : GrantConditionOnLayerInfo + { + public override object Create(ActorInitializer init) { return new GrantConditionOnJumpjetLayer(init.Self, this); } + + public override void RulesetLoaded(Ruleset rules, ActorInfo ai) + { + var mobileInfo = ai.TraitInfoOrDefault(); + if (mobileInfo == null || !(mobileInfo.LocomotorInfo is JumpjetLocomotorInfo)) + throw new YamlException("GrantConditionOnJumpjetLayer requires Mobile to be linked to a JumpjetLocomotor!"); + + base.RulesetLoaded(rules, ai); + } + } + + public class GrantConditionOnJumpjetLayer : GrantConditionOnLayer, INotifyFinishedMoving + { + bool jumpjetInAir; + + public GrantConditionOnJumpjetLayer(Actor self, GrantConditionOnJumpjetLayerInfo info) + : base(self, info, CustomMovementLayerType.Jumpjet) { } + + void INotifyFinishedMoving.FinishedMoving(Actor self, byte oldLayer, byte newLayer) + { + if (jumpjetInAir && oldLayer != ValidLayerType && newLayer != ValidLayerType) + UpdateConditions(self, oldLayer, newLayer); + } + + protected override void UpdateConditions(Actor self, byte oldLayer, byte newLayer) + { + if (!jumpjetInAir && newLayer == ValidLayerType && oldLayer != ValidLayerType && conditionToken == ConditionManager.InvalidConditionToken) + { + conditionToken = conditionManager.GrantCondition(self, Info.Condition); + jumpjetInAir = true; + } + + // By the time the condition is meant to be revoked, the 'oldLayer' is already no longer the Jumpjet layer, either + if (jumpjetInAir && newLayer != ValidLayerType && oldLayer != ValidLayerType && conditionToken != ConditionManager.InvalidConditionToken) + { + conditionToken = conditionManager.RevokeCondition(self, conditionToken); + jumpjetInAir = false; + } + } + } +} diff --git a/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnLayer.cs b/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnLayer.cs new file mode 100644 index 0000000000..69c451416e --- /dev/null +++ b/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnLayer.cs @@ -0,0 +1,73 @@ +#region Copyright & License Information +/* + * Copyright 2007-2018 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.Linq; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + public abstract class GrantConditionOnLayerInfo : ConditionalTraitInfo + { + [FieldLoader.Require] + [GrantedConditionReference] + [Desc("The condition to grant to self when changing to specific custom layer.")] + public readonly string Condition = null; + } + + public abstract class GrantConditionOnLayer : ConditionalTrait, INotifyCustomLayerChanged where InfoType : GrantConditionOnLayerInfo + { + protected readonly byte ValidLayerType; + protected ConditionManager conditionManager; + protected int conditionToken = ConditionManager.InvalidConditionToken; + + public GrantConditionOnLayer(Actor self, InfoType info, byte validLayer) + : base(info) + { + ValidLayerType = validLayer; + } + + protected override void Created(Actor self) + { + conditionManager = self.TraitOrDefault(); + base.Created(self); + } + + void INotifyCustomLayerChanged.CustomLayerChanged(Actor self, byte oldLayer, byte newLayer) + { + if (conditionManager == null) + return; + + UpdateConditions(self, oldLayer, newLayer); + } + + protected virtual void UpdateConditions(Actor self, byte oldLayer, byte newLayer) + { + if (newLayer == ValidLayerType && oldLayer != ValidLayerType && conditionToken == ConditionManager.InvalidConditionToken) + conditionToken = conditionManager.GrantCondition(self, Info.Condition); + else if (newLayer != ValidLayerType && oldLayer == ValidLayerType && conditionToken != ConditionManager.InvalidConditionToken) + conditionToken = conditionManager.RevokeCondition(self, conditionToken); + } + + protected override void TraitEnabled(Actor self) + { + if (self.Location.Layer == ValidLayerType && conditionToken == ConditionManager.InvalidConditionToken) + conditionToken = conditionManager.GrantCondition(self, Info.Condition); + } + + protected override void TraitDisabled(Actor self) + { + if (conditionToken == ConditionManager.InvalidConditionToken) + return; + + conditionToken = conditionManager.RevokeCondition(self, conditionToken); + } + } +} diff --git a/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnSubterraneanLayer.cs b/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnSubterraneanLayer.cs new file mode 100644 index 0000000000..e60ff8f665 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnSubterraneanLayer.cs @@ -0,0 +1,94 @@ +#region Copyright & License Information +/* + * Copyright 2007-2018 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 OpenRA.Mods.Common.Effects; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [Desc("Grants Condition on subterranean layer. Also plays transition audio-visuals.")] + public class GrantConditionOnSubterraneanLayerInfo : GrantConditionOnLayerInfo + { + [Desc("Dig animation image to play when transitioning.")] + public readonly string SubterraneanTransitionImage = null; + + [SequenceReference("SubterraneanTransitionImage")] + [Desc("Dig animation sequence to play when transitioning.")] + public readonly string SubterraneanTransitionSequence = null; + + [PaletteReference] + public readonly string SubterraneanTransitionPalette = "effect"; + + [Desc("Dig sound to play when transitioning.")] + public readonly string SubterraneanTransitionSound = null; + + public override object Create(ActorInitializer init) { return new GrantConditionOnSubterraneanLayer(init.Self, this); } + + public override void RulesetLoaded(Ruleset rules, ActorInfo ai) + { + var mobileInfo = ai.TraitInfoOrDefault(); + if (mobileInfo == null || !(mobileInfo.LocomotorInfo is SubterraneanLocomotorInfo)) + throw new YamlException("GrantConditionOnSubterraneanLayer requires Mobile to be linked to a SubterraneanLocomotor!"); + + base.RulesetLoaded(rules, ai); + } + } + + public class GrantConditionOnSubterraneanLayer : GrantConditionOnLayer, INotifyVisualPositionChanged + { + WDist transitionDepth; + + public GrantConditionOnSubterraneanLayer(Actor self, GrantConditionOnSubterraneanLayerInfo info) + : base(self, info, CustomMovementLayerType.Subterranean) { } + + protected override void Created(Actor self) + { + var mobileInfo = self.Info.TraitInfo(); + var li = (SubterraneanLocomotorInfo)mobileInfo.LocomotorInfo; + transitionDepth = li.SubterraneanTransitionDepth; + base.Created(self); + } + + void PlayTransitionAudioVisuals(Actor self, CPos fromCell) + { + if (!string.IsNullOrEmpty(Info.SubterraneanTransitionSequence)) + self.World.AddFrameEndTask(w => w.Add(new SpriteEffect(self.World.Map.CenterOfCell(fromCell), self.World, + Info.SubterraneanTransitionImage, + Info.SubterraneanTransitionSequence, Info.SubterraneanTransitionPalette))); + + if (!string.IsNullOrEmpty(Info.SubterraneanTransitionSound)) + Game.Sound.Play(SoundType.World, Info.SubterraneanTransitionSound); + } + + void INotifyVisualPositionChanged.VisualPositionChanged(Actor self, byte oldLayer, byte newLayer) + { + var depth = self.World.Map.DistanceAboveTerrain(self.CenterPosition); + + // Grant condition when new layer is Subterranean and depth is lower than transition depth, + // revoke condition when new layer is not Subterranean and depth is at or higher than transition depth. + if (newLayer == ValidLayerType && depth < transitionDepth && conditionToken == ConditionManager.InvalidConditionToken) + conditionToken = conditionManager.GrantCondition(self, Info.Condition); + else if (newLayer != ValidLayerType && depth > transitionDepth && conditionToken != ConditionManager.InvalidConditionToken) + { + conditionToken = conditionManager.RevokeCondition(self, conditionToken); + PlayTransitionAudioVisuals(self, self.Location); + } + } + + protected override void UpdateConditions(Actor self, byte oldLayer, byte newLayer) + { + // Special case, only audio-visuals are played at the time the Layer changes from normal to Subterranean + if (newLayer == ValidLayerType && oldLayer != ValidLayerType) + PlayTransitionAudioVisuals(self, self.Location); + } + } +} diff --git a/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnTunnelLayer.cs b/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnTunnelLayer.cs new file mode 100644 index 0000000000..a42884c78c --- /dev/null +++ b/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnTunnelLayer.cs @@ -0,0 +1,26 @@ +#region Copyright & License Information +/* + * Copyright 2007-2018 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 OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + public class GrantConditionOnTunnelLayerInfo : GrantConditionOnLayerInfo + { + public override object Create(ActorInitializer init) { return new GrantConditionOnTunnelLayer(init.Self, this); } + } + + public class GrantConditionOnTunnelLayer : GrantConditionOnLayer + { + public GrantConditionOnTunnelLayer(Actor self, GrantConditionOnTunnelLayerInfo info) + : base(self, info, CustomMovementLayerType.Tunnel) { } + } +} diff --git a/OpenRA.Mods.Common/Traits/Crates/Crate.cs b/OpenRA.Mods.Common/Traits/Crates/Crate.cs index 7ce2a3ca89..ccbbca3656 100644 --- a/OpenRA.Mods.Common/Traits/Crates/Crate.cs +++ b/OpenRA.Mods.Common/Traits/Crates/Crate.cs @@ -121,7 +121,7 @@ namespace OpenRA.Mods.Common.Traits // Make sure that the actor can collect this crate type // Crate can only be crushed if it is not in the air. - return self.IsAtGroundLevel() && mi.Crushes.Contains(info.CrushClass); + return self.IsAtGroundLevel() && mi.LocomotorInfo.Crushes.Contains(info.CrushClass); }); // Destroy the crate if none of the units in the cell are valid collectors diff --git a/OpenRA.Mods.Common/Traits/Crushable.cs b/OpenRA.Mods.Common/Traits/Crushable.cs index 49cf688d2d..1c06f0f715 100644 --- a/OpenRA.Mods.Common/Traits/Crushable.cs +++ b/OpenRA.Mods.Common/Traits/Crushable.cs @@ -58,7 +58,7 @@ namespace OpenRA.Mods.Common.Traits Game.Sound.Play(SoundType.World, Info.CrushSound, crusher.CenterPosition); var crusherMobile = crusher.TraitOrDefault(); - self.Kill(crusher, crusherMobile != null ? crusherMobile.Info.CrushDamageTypes : new HashSet()); + self.Kill(crusher, crusherMobile != null ? crusherMobile.Info.LocomotorInfo.CrushDamageTypes : new HashSet()); } bool ICrushable.CrushableBy(Actor self, Actor crusher, HashSet crushClasses) diff --git a/OpenRA.Mods.Common/Traits/Harvester.cs b/OpenRA.Mods.Common/Traits/Harvester.cs index 199aa95234..00773edc87 100644 --- a/OpenRA.Mods.Common/Traits/Harvester.cs +++ b/OpenRA.Mods.Common/Traits/Harvester.cs @@ -185,8 +185,8 @@ namespace OpenRA.Mods.Common.Traits // Start a search from each refinery's delivery location: List path; - var mi = self.Info.TraitInfo(); - using (var search = PathSearch.FromPoints(self.World, mi, self, refs.Values.Select(r => r.Location), self.Location, false) + var li = self.Info.TraitInfo().LocomotorInfo; + using (var search = PathSearch.FromPoints(self.World, li, self, refs.Values.Select(r => r.Location), self.Location, false) .WithCustomCost(loc => { if (!refs.ContainsKey(loc)) diff --git a/OpenRA.Mods.Common/Traits/Mobile.cs b/OpenRA.Mods.Common/Traits/Mobile.cs index eff3103a4b..017a8bdd54 100644 --- a/OpenRA.Mods.Common/Traits/Mobile.cs +++ b/OpenRA.Mods.Common/Traits/Mobile.cs @@ -22,49 +22,13 @@ using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { - [Flags] - public enum CellConditions - { - None = 0, - TransientActors, - BlockedByMovers, - All = TransientActors | BlockedByMovers - } - - public static class CellConditionsExts - { - public static bool HasCellCondition(this CellConditions c, CellConditions cellCondition) - { - // PERF: Enum.HasFlag is slower and requires allocations. - return (c & cellCondition) == cellCondition; - } - } - - public static class CustomMovementLayerType - { - public const byte Tunnel = 1; - public const byte Subterranean = 2; - public const byte Jumpjet = 3; - public const byte ElevatedBridge = 4; - } - [Desc("Unit is able to move.")] public class MobileInfo : ConditionalTraitInfo, IMoveInfo, IPositionableInfo, IFacingInfo, UsesInit, UsesInit, UsesInit, IActorPreviewInitInfo { - [FieldLoader.LoadUsing("LoadSpeeds", true)] - [Desc("Set Water: 0 for ground units and lower the value on rough terrain.")] - public readonly Dictionary TerrainSpeeds; - - [Desc("e.g. crate, wall, infantry")] - public readonly HashSet Crushes = new HashSet(); - - [Desc("Types of damage that are caused while crushing. Leave empty for no damage types.")] - public readonly HashSet CrushDamageTypes = new HashSet(); - - public readonly int WaitAverage = 5; - - public readonly int WaitSpread = 2; + [Desc("Which Locomotor does this trait use. Must be defined on the World actor.")] + [FieldLoader.Require] + public readonly string Locomotor = null; public readonly int InitialFacing = 0; @@ -73,68 +37,11 @@ namespace OpenRA.Mods.Common.Traits public readonly int Speed = 1; - [Desc("Allow multiple (infantry) units in one cell.")] - public readonly bool SharesCell = false; - - [Desc("Can the actor be ordered to move in to shroud?")] - public readonly bool MoveIntoShroud = true; - public readonly string Cursor = "move"; public readonly string BlockedCursor = "move-blocked"; [VoiceReference] public readonly string Voice = "Action"; - [GrantedConditionReference] - [Desc("The condition to grant to self while inside a tunnel.")] - public readonly string TunnelCondition = null; - - [Desc("Can this unit move underground?")] - public readonly bool Subterranean = false; - - [GrantedConditionReference] - [Desc("The condition to grant to self while underground.")] - public readonly string SubterraneanCondition = null; - - [Desc("Pathfinding cost for submerging or reemerging.")] - public readonly int SubterraneanTransitionCost = 0; - - [Desc("The terrain types that this actor can transition on. Leave empty to allow any.")] - public readonly HashSet SubterraneanTransitionTerrainTypes = new HashSet(); - - [Desc("Can this actor transition on slopes?")] - public readonly bool SubterraneanTransitionOnRamps = false; - - [Desc("Depth at which the subterranian condition is applied.")] - public readonly WDist SubterraneanTransitionDepth = new WDist(-1024); - - [Desc("Dig animation image to play when transitioning.")] - public readonly string SubterraneanTransitionImage = null; - - [SequenceReference("SubterraneanTransitionImage")] - [Desc("Dig animation image to play when transitioning.")] - public readonly string SubterraneanTransitionSequence = null; - - [PaletteReference] - public readonly string SubterraneanTransitionPalette = "effect"; - - public readonly string SubterraneanTransitionSound = null; - - [Desc("Can this unit fly over obstacles?")] - public readonly bool Jumpjet = false; - - [GrantedConditionReference] - [Desc("The condition to grant to self while flying.")] - public readonly string JumpjetCondition = null; - - [Desc("Pathfinding cost for taking off or landing.")] - public readonly int JumpjetTransitionCost = 0; - - [Desc("The terrain types that this actor can transition on. Leave empty to allow any.")] - public readonly HashSet JumpjetTransitionTerrainTypes = new HashSet(); - - [Desc("Can this actor transition on slopes?")] - public readonly bool JumpjetTransitionOnRamps = true; - [Desc("Facing to use for actor previews (map editor, color picker, etc)")] public readonly int PreviewFacing = 92; @@ -145,240 +52,37 @@ namespace OpenRA.Mods.Common.Traits public override object Create(ActorInitializer init) { return new Mobile(init, this); } - static object LoadSpeeds(MiniYaml y) + public LocomotorInfo LocomotorInfo { get; private set; } + + public override void RulesetLoaded(Ruleset rules, ActorInfo ai) { - var ret = new Dictionary(); - foreach (var t in y.ToDictionary()["TerrainSpeeds"].Nodes) - { - var speed = FieldLoader.GetValue("speed", t.Value.Value); - var nodesDict = t.Value.ToDictionary(); - var cost = nodesDict.ContainsKey("PathingCost") - ? FieldLoader.GetValue("cost", nodesDict["PathingCost"].Value) - : 10000 / speed; - ret.Add(t.Key, new TerrainInfo(speed, cost)); - } + var locomotorInfos = rules.Actors["world"].TraitInfos(); + LocomotorInfo = locomotorInfos.FirstOrDefault(li => li.Name == Locomotor); + if (LocomotorInfo == null) + throw new YamlException("A locomotor named '{0}' doesn't exist.".F(Locomotor)); + else if (locomotorInfos.Count(li => li.Name == Locomotor) > 1) + throw new YamlException("There is more than one locomotor named '{0}'.".F(Locomotor)); - return ret; - } - - TerrainInfo[] LoadTilesetSpeeds(TileSet tileSet) - { - var info = new TerrainInfo[tileSet.TerrainInfo.Length]; - for (var i = 0; i < info.Length; i++) - info[i] = TerrainInfo.Impassable; - - foreach (var kvp in TerrainSpeeds) - { - byte index; - if (tileSet.TryGetTerrainIndex(kvp.Key, out index)) - info[index] = kvp.Value; - } - - return info; - } - - public class TerrainInfo - { - public static readonly TerrainInfo Impassable = new TerrainInfo(); - - public readonly int Cost; - public readonly int Speed; - - public TerrainInfo() - { - Cost = int.MaxValue; - Speed = 0; - } - - public TerrainInfo(int speed, int cost) - { - Speed = speed; - Cost = cost; - } - } - - public struct WorldMovementInfo - { - internal readonly World World; - internal readonly TerrainInfo[] TerrainInfos; - internal WorldMovementInfo(World world, MobileInfo info) - { - // PERF: This struct allows us to cache the terrain info for the tileset used by the world. - // This allows us to speed up some performance-sensitive pathfinding calculations. - World = world; - TerrainInfos = info.TilesetTerrainInfo[world.Map.Rules.TileSet]; - } - } - - public readonly Cache TilesetTerrainInfo; - public readonly Cache TilesetMovementClass; - - public MobileInfo() - { - TilesetTerrainInfo = new Cache(LoadTilesetSpeeds); - TilesetMovementClass = new Cache(CalculateTilesetMovementClass); - } - - public int MovementCostForCell(World world, CPos cell) - { - return MovementCostForCell(world, TilesetTerrainInfo[world.Map.Rules.TileSet], cell); - } - - int MovementCostForCell(World world, TerrainInfo[] terrainInfos, CPos cell) - { - if (!world.Map.Contains(cell)) - return int.MaxValue; - - var index = cell.Layer == 0 ? world.Map.GetTerrainIndex(cell) : - world.GetCustomMovementLayers()[cell.Layer].GetTerrainIndex(cell); - - if (index == byte.MaxValue) - return int.MaxValue; - - return terrainInfos[index].Cost; - } - - public int CalculateTilesetMovementClass(TileSet tileset) - { - // collect our ability to cross *all* terraintypes, in a bitvector - return TilesetTerrainInfo[tileset].Select(ti => ti.Cost < int.MaxValue).ToBits(); - } - - public int GetMovementClass(TileSet tileset) - { - return TilesetMovementClass[tileset]; - } - - static bool IsMovingInMyDirection(Actor self, Actor other) - { - var otherMobile = other.TraitOrDefault(); - if (otherMobile == null || !otherMobile.IsMoving) - return false; - - var selfMobile = self.TraitOrDefault(); - if (selfMobile == null) - return false; - - // Moving in the same direction if the facing delta is between +/- 90 degrees - var delta = Util.NormalizeFacing(otherMobile.Facing - selfMobile.Facing); - return delta < 64 || delta > 192; - } - - public int TileSetMovementHash(TileSet tileSet) - { - var terrainInfos = TilesetTerrainInfo[tileSet]; - - // Compute and return the hash using aggregate - return terrainInfos.Aggregate(terrainInfos.Length, - (current, terrainInfo) => unchecked(current * 31 + terrainInfo.Cost)); - } - - public bool CanEnterCell(World world, Actor self, CPos cell, Actor ignoreActor = null, bool checkTransientActors = true) - { - if (MovementCostForCell(world, cell) == int.MaxValue) - return false; - - var check = checkTransientActors ? CellConditions.All : CellConditions.BlockedByMovers; - return CanMoveFreelyInto(world, self, cell, ignoreActor, check); - } - - // Determines whether the actor is blocked by other Actors - public bool CanMoveFreelyInto(World world, Actor self, CPos cell, Actor ignoreActor, CellConditions check) - { - if (!check.HasCellCondition(CellConditions.TransientActors)) - return true; - - if (SharesCell && world.ActorMap.HasFreeSubCell(cell)) - return true; - - // PERF: Avoid LINQ. - foreach (var otherActor in world.ActorMap.GetActorsAt(cell)) - if (IsBlockedBy(self, otherActor, ignoreActor, check)) - return false; - - return true; - } - - bool IsBlockedBy(Actor self, Actor otherActor, Actor ignoreActor, CellConditions check) - { - // We are not blocked by the actor we are ignoring. - if (otherActor == ignoreActor) - return false; - - // If self is null, we don't have a real actor - we're just checking what would happen theoretically. - // In such a scenario - we'll just assume any other actor in the cell will block us by default. - // If we have a real actor, we can then perform the extra checks that allow us to avoid being blocked. - if (self == null) - return true; - - // If the check allows: we are not blocked by allied units moving in our direction. - if (!check.HasCellCondition(CellConditions.BlockedByMovers) && - self.Owner.Stances[otherActor.Owner] == Stance.Ally && - IsMovingInMyDirection(self, otherActor)) - return false; - - // If there is a temporary blocker in our path, but we can remove it, we are not blocked. - var temporaryBlocker = otherActor.TraitOrDefault(); - if (temporaryBlocker != null && temporaryBlocker.CanRemoveBlockage(otherActor, self)) - return false; - - // If we cannot crush the other actor in our way, we are blocked. - if (Crushes == null || Crushes.Count == 0) - return true; - - // If the other actor in our way cannot be crushed, we are blocked. - // PERF: Avoid LINQ. - var crushables = otherActor.TraitsImplementing(); - foreach (var crushable in crushables) - if (crushable.CrushableBy(otherActor, self, Crushes)) - return false; - - return true; - } - - public WorldMovementInfo GetWorldMovementInfo(World world) - { - return new WorldMovementInfo(world, this); - } - - public int MovementCostToEnterCell(WorldMovementInfo worldMovementInfo, Actor self, CPos cell, Actor ignoreActor = null, CellConditions check = CellConditions.All) - { - var cost = MovementCostForCell(worldMovementInfo.World, worldMovementInfo.TerrainInfos, cell); - if (cost == int.MaxValue || !CanMoveFreelyInto(worldMovementInfo.World, self, cell, ignoreActor, check)) - return int.MaxValue; - return cost; - } - - public SubCell GetAvailableSubCell( - World world, Actor self, CPos cell, SubCell preferredSubCell = SubCell.Any, Actor ignoreActor = null, CellConditions check = CellConditions.All) - { - if (MovementCostForCell(world, cell) == int.MaxValue) - return SubCell.Invalid; - - if (check.HasCellCondition(CellConditions.TransientActors)) - { - Func checkTransient = otherActor => IsBlockedBy(self, otherActor, ignoreActor, check); - - if (!SharesCell) - return world.ActorMap.AnyActorsAt(cell, SubCell.FullCell, checkTransient) ? SubCell.Invalid : SubCell.FullCell; - - return world.ActorMap.FreeSubCell(cell, preferredSubCell, checkTransient); - } - - if (!SharesCell) - return world.ActorMap.AnyActorsAt(cell, SubCell.FullCell) ? SubCell.Invalid : SubCell.FullCell; - - return world.ActorMap.FreeSubCell(cell, preferredSubCell); + base.RulesetLoaded(rules, ai); } public int GetInitialFacing() { return InitialFacing; } + public bool CanEnterCell(World world, Actor self, CPos cell, Actor ignoreActor = null, bool checkTransientActors = true) + { + if (LocomotorInfo.MovementCostForCell(world, cell) == int.MaxValue) + return false; + + var check = checkTransientActors ? CellConditions.All : CellConditions.BlockedByMovers; + return LocomotorInfo.CanMoveFreelyInto(world, self, cell, ignoreActor, check); + } + public IReadOnlyDictionary OccupiedCells(ActorInfo info, CPos location, SubCell subCell = SubCell.Any) { return new ReadOnlyDictionary(new Dictionary() { { location, subCell } }); } - bool IOccupySpaceInfo.SharesCell { get { return SharesCell; } } + bool IOccupySpaceInfo.SharesCell { get { return LocomotorInfo.SharesCell; } } } public class Mobile : ConditionalTrait, INotifyCreated, IIssueOrder, IResolveOrder, IOrderVoice, IPositionable, IMove, @@ -396,10 +100,9 @@ namespace OpenRA.Mods.Common.Traits int facing; CPos fromCell, toCell; public SubCell FromSubCell, ToSubCell; - int tunnelToken = ConditionManager.InvalidConditionToken; - int subterraneanToken = ConditionManager.InvalidConditionToken; - int jumpjetToken = ConditionManager.InvalidConditionToken; - ConditionManager conditionManager; + INotifyCustomLayerChanged[] notifyCustomLayerChanged; + INotifyVisualPositionChanged[] notifyVisualPositionChanged; + INotifyFinishedMoving[] notifyFinishedMoving; [Sync] public int Facing { @@ -428,29 +131,10 @@ namespace OpenRA.Mods.Common.Traits ToSubCell = toSub; AddInfluence(); - // Tunnel condition is added/removed when starting the transition between layers - if (toCell.Layer == CustomMovementLayerType.Tunnel && conditionManager != null && - !string.IsNullOrEmpty(Info.TunnelCondition) && tunnelToken == ConditionManager.InvalidConditionToken) - tunnelToken = conditionManager.GrantCondition(self, Info.TunnelCondition); - else if (toCell.Layer != CustomMovementLayerType.Tunnel && tunnelToken != ConditionManager.InvalidConditionToken) - tunnelToken = conditionManager.RevokeCondition(self, tunnelToken); - - // Play submerging animation as soon as it starts to submerge (before applying the condition) - if (toCell.Layer == CustomMovementLayerType.Subterranean && fromCell.Layer != CustomMovementLayerType.Subterranean) - { - if (!string.IsNullOrEmpty(Info.SubterraneanTransitionSequence)) - self.World.AddFrameEndTask(w => w.Add(new SpriteEffect(self.World.Map.CenterOfCell(fromCell), self.World, Info.SubterraneanTransitionImage, - Info.SubterraneanTransitionSequence, Info.SubterraneanTransitionPalette))); - - if (!string.IsNullOrEmpty(Info.SubterraneanTransitionSound)) - Game.Sound.Play(SoundType.World, Info.SubterraneanTransitionSound); - } - - // Grant the jumpjet condition as soon as the actor starts leaving the ground layer - // The condition is revoked from FinishedMoving - if (toCell.Layer == CustomMovementLayerType.Jumpjet && conditionManager != null && - !string.IsNullOrEmpty(Info.JumpjetCondition) && jumpjetToken == ConditionManager.InvalidConditionToken) - jumpjetToken = conditionManager.GrantCondition(self, Info.JumpjetCondition); + // Most custom layer conditions are added/removed when starting the transition between layers. + if (toCell.Layer != fromCell.Layer) + foreach (var n in notifyCustomLayerChanged) + n.CustomLayerChanged(self, fromCell.Layer, toCell.Layer); } public Mobile(ActorInitializer init, MobileInfo info) @@ -460,7 +144,7 @@ namespace OpenRA.Mods.Common.Traits speedModifiers = Exts.Lazy(() => self.TraitsImplementing().ToArray().Select(x => x.GetSpeedModifier())); - ToSubCell = FromSubCell = info.SharesCell ? init.World.Map.Grid.DefaultSubCell : SubCell.FullCell; + ToSubCell = FromSubCell = info.LocomotorInfo.SharesCell ? init.World.Map.Grid.DefaultSubCell : SubCell.FullCell; if (init.Contains()) FromSubCell = ToSubCell = init.Get(); @@ -480,7 +164,9 @@ namespace OpenRA.Mods.Common.Traits protected override void Created(Actor self) { - conditionManager = self.TraitOrDefault(); + notifyCustomLayerChanged = self.TraitsImplementing().ToArray(); + notifyVisualPositionChanged = self.TraitsImplementing().ToArray(); + notifyFinishedMoving = self.TraitsImplementing().ToArray(); base.Created(self); } @@ -493,7 +179,7 @@ namespace OpenRA.Mods.Common.Traits preferred = FromSubCell; // Fix sub-cell assignment - if (Info.SharesCell) + if (Info.LocomotorInfo.SharesCell) { if (preferred <= SubCell.FullCell) return self.World.Map.Grid.DefaultSubCell; @@ -536,31 +222,12 @@ namespace OpenRA.Mods.Common.Traits CenterPosition = pos; self.World.UpdateMaps(self, this); - // HACK: The submerging conditions must be applied part way through a move, and this is the only method that gets called - // at the right times to detect this - if (toCell.Layer == CustomMovementLayerType.Subterranean) - { - var depth = self.World.Map.DistanceAboveTerrain(self.CenterPosition); - if (subterraneanToken == ConditionManager.InvalidConditionToken && depth < Info.SubterraneanTransitionDepth && conditionManager != null - && !string.IsNullOrEmpty(Info.SubterraneanCondition)) - subterraneanToken = conditionManager.GrantCondition(self, Info.SubterraneanCondition); - } - else if (subterraneanToken != ConditionManager.InvalidConditionToken) - { - var depth = self.World.Map.DistanceAboveTerrain(self.CenterPosition); - if (depth > Info.SubterraneanTransitionDepth) - { - subterraneanToken = conditionManager.RevokeCondition(self, subterraneanToken); + // The first time SetVisualPosition is called is in the constructor before creation, so we need a null check here as well + if (notifyVisualPositionChanged == null) + return; - // HACK: the submerging animation and sound won't play if a condition isn't defined - if (!string.IsNullOrEmpty(Info.SubterraneanTransitionSound)) - Game.Sound.Play(SoundType.World, Info.SubterraneanTransitionSound); - - if (!string.IsNullOrEmpty(Info.SubterraneanTransitionSequence)) - self.World.AddFrameEndTask(w => w.Add(new SpriteEffect(self.World.Map.CenterOfCell(fromCell), self.World, Info.SubterraneanTransitionImage, - Info.SubterraneanTransitionSequence, Info.SubterraneanTransitionPalette))); - } - } + foreach (var n in notifyVisualPositionChanged) + n.VisualPositionChanged(self, fromCell.Layer, toCell.Layer); } void INotifyAddedToWorld.AddedToWorld(Actor self) @@ -630,7 +297,7 @@ namespace OpenRA.Mods.Common.Traits { var loc = self.World.Map.Clamp(order.TargetLocation); - if (!Info.MoveIntoShroud && !self.Owner.Shroud.IsExplored(loc)) + if (!Info.LocomotorInfo.MoveIntoShroud && !self.Owner.Shroud.IsExplored(loc)) return; if (!order.Queued) @@ -651,7 +318,7 @@ namespace OpenRA.Mods.Common.Traits public string VoicePhraseForOrder(Actor self, Order order) { - if (!Info.MoveIntoShroud && !self.Owner.Shroud.IsExplored(order.TargetLocation)) + if (!Info.LocomotorInfo.MoveIntoShroud && !self.Owner.Shroud.IsExplored(order.TargetLocation)) return null; switch (order.OrderString) @@ -673,6 +340,7 @@ namespace OpenRA.Mods.Common.Traits return new[] { Pair.New(FromCell, FromSubCell) }; if (CanEnterCell(ToCell)) return new[] { Pair.New(ToCell, ToSubCell) }; + return new[] { Pair.New(FromCell, FromSubCell), Pair.New(ToCell, ToSubCell) }; } @@ -684,12 +352,13 @@ namespace OpenRA.Mods.Common.Traits public SubCell GetAvailableSubCell(CPos a, SubCell preferredSubCell = SubCell.Any, Actor ignoreActor = null, bool checkTransientActors = true) { - return Info.GetAvailableSubCell(self.World, self, a, preferredSubCell, ignoreActor, checkTransientActors ? CellConditions.All : CellConditions.None); + var cellConditions = checkTransientActors ? CellConditions.All : CellConditions.None; + return Info.LocomotorInfo.GetAvailableSubCell(self.World, self, a, preferredSubCell, ignoreActor, cellConditions); } public bool CanExistInCell(CPos cell) { - return Info.MovementCostForCell(self.World, cell) != int.MaxValue; + return Info.LocomotorInfo.MovementCostForCell(self.World, cell) != int.MaxValue; } public bool CanEnterCell(CPos cell, Actor ignoreActor = null, bool checkTransientActors = true) @@ -699,7 +368,7 @@ namespace OpenRA.Mods.Common.Traits public bool CanMoveFreelyInto(CPos cell, Actor ignoreActor = null, bool checkTransientActors = true) { - return Info.CanMoveFreelyInto(self.World, self, cell, ignoreActor, checkTransientActors ? CellConditions.All : CellConditions.BlockedByMovers); + return Info.LocomotorInfo.CanMoveFreelyInto(self.World, self, cell, ignoreActor, checkTransientActors ? CellConditions.All : CellConditions.BlockedByMovers); } public void EnteringCell(Actor self) @@ -714,15 +383,15 @@ namespace OpenRA.Mods.Common.Traits var notifiers = actors.SelectMany(a => a.TraitsImplementing().Select(t => new TraitPair(a, t))); foreach (var notifyCrushed in notifiers) - notifyCrushed.Trait.WarnCrush(notifyCrushed.Actor, self, Info.Crushes); + notifyCrushed.Trait.WarnCrush(notifyCrushed.Actor, self, Info.LocomotorInfo.Crushes); } public void FinishedMoving(Actor self) { // Need to check both fromCell and toCell because FinishedMoving is called multiple times during the move - // and that condition guarantees that this only runs when the unit has finished landing. - if (fromCell.Layer != CustomMovementLayerType.Jumpjet && toCell.Layer != CustomMovementLayerType.Jumpjet && jumpjetToken != ConditionManager.InvalidConditionToken) - jumpjetToken = conditionManager.RevokeCondition(self, jumpjetToken); + if (fromCell.Layer == toCell.Layer) + foreach (var n in notifyFinishedMoving) + n.FinishedMoving(self, fromCell.Layer, toCell.Layer); // Only make actor crush if it is on the ground if (!self.IsAtGroundLevel()) @@ -734,7 +403,7 @@ namespace OpenRA.Mods.Common.Traits var notifiers = actors.SelectMany(a => a.TraitsImplementing().Select(t => new TraitPair(a, t))); foreach (var notifyCrushed in notifiers) - notifyCrushed.Trait.OnCrush(notifyCrushed.Actor, self, Info.Crushes); + notifyCrushed.Trait.OnCrush(notifyCrushed.Actor, self, Info.LocomotorInfo.Crushes); } bool AnyCrushables(List actors) @@ -744,7 +413,7 @@ namespace OpenRA.Mods.Common.Traits return false; foreach (var crushes in crushables) - if (crushes.Trait.CrushableBy(crushes.Actor, self, Info.Crushes)) + if (crushes.Trait.CrushableBy(crushes.Actor, self, Info.LocomotorInfo.Crushes)) return true; return false; @@ -758,7 +427,7 @@ namespace OpenRA.Mods.Common.Traits if (index == byte.MaxValue) return 0; - var terrainSpeed = Info.TilesetTerrainInfo[self.World.Map.Rules.TileSet][index].Speed; + var terrainSpeed = Info.LocomotorInfo.TilesetTerrainInfo[self.World.Map.Rules.TileSet][index].Speed; if (terrainSpeed == 0) return 0; @@ -868,6 +537,7 @@ namespace OpenRA.Mods.Common.Traits class MoveOrderTargeter : IOrderTargeter { readonly Mobile mobile; + readonly LocomotorInfo locomotorInfo; readonly bool rejectMove; public bool TargetOverridesSelection(TargetModifiers modifiers) { @@ -877,6 +547,7 @@ namespace OpenRA.Mods.Common.Traits public MoveOrderTargeter(Actor self, Mobile unit) { mobile = unit; + locomotorInfo = mobile.Info.LocomotorInfo; rejectMove = !self.AcceptsOrder("Move"); } @@ -897,8 +568,8 @@ namespace OpenRA.Mods.Common.Traits (self.World.Map.GetTerrainInfo(location).CustomCursor ?? mobile.Info.Cursor) : mobile.Info.BlockedCursor; if (mobile.IsTraitDisabled - || (!explored && !mobile.Info.MoveIntoShroud) - || (explored && mobile.Info.MovementCostForCell(self.World, location) == int.MaxValue)) + || (!explored && !locomotorInfo.MoveIntoShroud) + || (explored && locomotorInfo.MovementCostForCell(self.World, location) == int.MaxValue)) cursor = mobile.Info.BlockedCursor; return true; @@ -924,7 +595,7 @@ namespace OpenRA.Mods.Common.Traits var pos = self.CenterPosition; if (subCell == SubCell.Any) - subCell = Info.SharesCell ? self.World.ActorMap.FreeSubCell(cell, subCell) : SubCell.FullCell; + subCell = Info.LocomotorInfo.SharesCell ? self.World.ActorMap.FreeSubCell(cell, subCell) : SubCell.FullCell; // TODO: solve/reduce cell is full problem if (subCell == SubCell.Invalid) @@ -990,7 +661,7 @@ namespace OpenRA.Mods.Common.Traits var pathFinder = self.World.WorldActor.Trait(); List path; - using (var search = PathSearch.Search(self.World, Info, self, true, + using (var search = PathSearch.Search(self.World, Info.LocomotorInfo, self, true, loc => loc.Layer == 0 && CanEnterCell(loc)) .FromPoint(self.Location)) path = pathFinder.FindPath(search); diff --git a/OpenRA.Mods.Common/Traits/ProductionFromMapEdge.cs b/OpenRA.Mods.Common/Traits/ProductionFromMapEdge.cs index 8e443453ad..812711a802 100644 --- a/OpenRA.Mods.Common/Traits/ProductionFromMapEdge.cs +++ b/OpenRA.Mods.Common/Traits/ProductionFromMapEdge.cs @@ -10,6 +10,7 @@ #endregion using System.Drawing; +using System.Linq; using OpenRA.Primitives; using OpenRA.Traits; @@ -47,8 +48,9 @@ namespace OpenRA.Mods.Common.Traits var aircraftInfo = producee.TraitInfoOrDefault(); var mobileInfo = producee.TraitInfoOrDefault(); + var locomotorInfo = mobileInfo.LocomotorInfo; - var passable = mobileInfo != null ? (uint)mobileInfo.GetMovementClass(self.World.Map.Rules.TileSet) : 0; + var passable = mobileInfo != null ? (uint)locomotorInfo.GetMovementClass(self.World.Map.Rules.TileSet) : 0; var destination = rp != null ? rp.Location : self.Location; var location = spawnLocation; @@ -59,7 +61,7 @@ namespace OpenRA.Mods.Common.Traits if (mobileInfo != null) location = self.World.Map.ChooseClosestMatchingEdgeCell(self.Location, - c => mobileInfo.CanEnterCell(self.World, null, c) && domainIndex.IsPassable(c, destination, mobileInfo, passable)); + c => mobileInfo.CanEnterCell(self.World, null, c) && domainIndex.IsPassable(c, destination, locomotorInfo, passable)); } // No suitable spawn location could be found, so production has failed. diff --git a/OpenRA.Mods.Common/Traits/World/DomainIndex.cs b/OpenRA.Mods.Common/Traits/World/DomainIndex.cs index 8daa82b147..0364a4d0e9 100644 --- a/OpenRA.Mods.Common/Traits/World/DomainIndex.cs +++ b/OpenRA.Mods.Common/Traits/World/DomainIndex.cs @@ -29,15 +29,14 @@ namespace OpenRA.Mods.Common.Traits { domainIndexes = new Dictionary(); var tileSet = world.Map.Rules.TileSet; - var movementClasses = - world.Map.Rules.Actors.Where(ai => ai.Value.HasTraitInfo()) - .Select(ai => (uint)ai.Value.TraitInfo().GetMovementClass(tileSet)).Distinct(); + var locomotors = world.WorldActor.TraitsImplementing().Where(l => !string.IsNullOrEmpty(l.Info.Name)); + var movementClasses = locomotors.Select(t => (uint)t.Info.GetMovementClass(tileSet)).Distinct(); foreach (var mc in movementClasses) domainIndexes[mc] = new MovementClassDomainIndex(world, mc); } - public bool IsPassable(CPos p1, CPos p2, MobileInfo mi, uint movementClass) + public bool IsPassable(CPos p1, CPos p2, LocomotorInfo loco, uint movementClass) { // HACK: Work around units in other movement layers from being blocked // when the point in the main layer is not pathable @@ -45,7 +44,7 @@ namespace OpenRA.Mods.Common.Traits return true; // HACK: Workaround until we can generalize movement classes - if (mi.Subterranean || mi.Jumpjet) + if (loco is SubterraneanLocomotorInfo || loco is JumpjetLocomotorInfo) return true; return domainIndexes[movementClass].IsPassable(p1, p2); diff --git a/OpenRA.Mods.Common/Traits/World/ElevatedBridgeLayer.cs b/OpenRA.Mods.Common/Traits/World/ElevatedBridgeLayer.cs index 3fe87d89c9..ffb26f1cb8 100644 --- a/OpenRA.Mods.Common/Traits/World/ElevatedBridgeLayer.cs +++ b/OpenRA.Mods.Common/Traits/World/ElevatedBridgeLayer.cs @@ -69,7 +69,7 @@ namespace OpenRA.Mods.Common.Traits } } - bool ICustomMovementLayer.EnabledForActor(ActorInfo a, MobileInfo mi) { return enabled; } + bool ICustomMovementLayer.EnabledForActor(ActorInfo a, LocomotorInfo li) { return enabled; } byte ICustomMovementLayer.Index { get { return CustomMovementLayerType.ElevatedBridge; } } bool ICustomMovementLayer.InteractsWithDefaultLayer { get { return true; } } @@ -78,12 +78,12 @@ namespace OpenRA.Mods.Common.Traits return cellCenters[cell]; } - int ICustomMovementLayer.EntryMovementCost(ActorInfo a, MobileInfo mi, CPos cell) + int ICustomMovementLayer.EntryMovementCost(ActorInfo a, LocomotorInfo li, CPos cell) { return ends.Contains(cell) ? 0 : int.MaxValue; } - int ICustomMovementLayer.ExitMovementCost(ActorInfo a, MobileInfo mi, CPos cell) + int ICustomMovementLayer.ExitMovementCost(ActorInfo a, LocomotorInfo li, CPos cell) { return ends.Contains(cell) ? 0 : int.MaxValue; } diff --git a/OpenRA.Mods.Common/Traits/World/JumpjetActorLayer.cs b/OpenRA.Mods.Common/Traits/World/JumpjetActorLayer.cs index b3b179953f..adeed12258 100644 --- a/OpenRA.Mods.Common/Traits/World/JumpjetActorLayer.cs +++ b/OpenRA.Mods.Common/Traits/World/JumpjetActorLayer.cs @@ -62,7 +62,7 @@ namespace OpenRA.Mods.Common.Traits } } - bool ICustomMovementLayer.EnabledForActor(ActorInfo a, MobileInfo mi) { return mi.Jumpjet; } + bool ICustomMovementLayer.EnabledForActor(ActorInfo a, LocomotorInfo li) { return li is JumpjetLocomotorInfo; } byte ICustomMovementLayer.Index { get { return CustomMovementLayerType.Jumpjet; } } bool ICustomMovementLayer.InteractsWithDefaultLayer { get { return true; } } @@ -72,13 +72,14 @@ namespace OpenRA.Mods.Common.Traits return pos + new WVec(0, 0, height[cell] - pos.Z); } - bool ValidTransitionCell(CPos cell, MobileInfo mi) + bool ValidTransitionCell(CPos cell, LocomotorInfo li) { var terrainType = map.GetTerrainInfo(cell).Type; - if (!mi.JumpjetTransitionTerrainTypes.Contains(terrainType) && mi.JumpjetTransitionTerrainTypes.Any()) + var jli = (JumpjetLocomotorInfo)li; + if (!jli.JumpjetTransitionTerrainTypes.Contains(terrainType) && jli.JumpjetTransitionTerrainTypes.Any()) return false; - if (mi.JumpjetTransitionOnRamps) + if (jli.JumpjetTransitionOnRamps) return true; var tile = map.Tiles[cell]; @@ -86,14 +87,16 @@ namespace OpenRA.Mods.Common.Traits return ti == null || ti.RampType == 0; } - int ICustomMovementLayer.EntryMovementCost(ActorInfo a, MobileInfo mi, CPos cell) + int ICustomMovementLayer.EntryMovementCost(ActorInfo a, LocomotorInfo li, CPos cell) { - return ValidTransitionCell(cell, mi) ? mi.JumpjetTransitionCost : int.MaxValue; + var jli = (JumpjetLocomotorInfo)li; + return ValidTransitionCell(cell, jli) ? jli.JumpjetTransitionCost : int.MaxValue; } - int ICustomMovementLayer.ExitMovementCost(ActorInfo a, MobileInfo mi, CPos cell) + int ICustomMovementLayer.ExitMovementCost(ActorInfo a, LocomotorInfo li, CPos cell) { - return ValidTransitionCell(cell, mi) ? mi.JumpjetTransitionCost : int.MaxValue; + var jli = (JumpjetLocomotorInfo)li; + return ValidTransitionCell(cell, jli) ? jli.JumpjetTransitionCost : int.MaxValue; } byte ICustomMovementLayer.GetTerrainIndex(CPos cell) diff --git a/OpenRA.Mods.Common/Traits/World/JumpjetLocomotor.cs b/OpenRA.Mods.Common/Traits/World/JumpjetLocomotor.cs new file mode 100644 index 0000000000..4f1932737d --- /dev/null +++ b/OpenRA.Mods.Common/Traits/World/JumpjetLocomotor.cs @@ -0,0 +1,36 @@ +#region Copyright & License Information +/* + * Copyright 2007-2018 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; + +namespace OpenRA.Mods.Common.Traits +{ + [Desc("Used by Mobile. Required for jumpjet actors. Attach these to the world actor. You can have multiple variants by adding @suffixes.")] + public class JumpjetLocomotorInfo : LocomotorInfo + { + [Desc("Pathfinding cost for taking off or landing.")] + public readonly int JumpjetTransitionCost = 0; + + [Desc("The terrain types that this actor can transition on. Leave empty to allow any.")] + public readonly HashSet JumpjetTransitionTerrainTypes = new HashSet(); + + [Desc("Can this actor transition on slopes?")] + public readonly bool JumpjetTransitionOnRamps = true; + + public override object Create(ActorInitializer init) { return new JumpjetLocomotor(init.Self, this); } + } + + public class JumpjetLocomotor : Locomotor + { + public JumpjetLocomotor(Actor self, JumpjetLocomotorInfo info) + : base(self, info) { } + } +} diff --git a/OpenRA.Mods.Common/Traits/World/Locomotor.cs b/OpenRA.Mods.Common/Traits/World/Locomotor.cs new file mode 100644 index 0000000000..09c606a190 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/World/Locomotor.cs @@ -0,0 +1,302 @@ +#region Copyright & License Information +/* + * Copyright 2007-2018 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.Collections.Generic; +using System.Linq; +using OpenRA.Mods.Common.Pathfinder; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [Flags] + public enum CellConditions + { + None = 0, + TransientActors, + BlockedByMovers, + All = TransientActors | BlockedByMovers + } + + public static class CellConditionsExts + { + public static bool HasCellCondition(this CellConditions c, CellConditions cellCondition) + { + // PERF: Enum.HasFlag is slower and requires allocations. + return (c & cellCondition) == cellCondition; + } + } + + public static class CustomMovementLayerType + { + public const byte Tunnel = 1; + public const byte Subterranean = 2; + public const byte Jumpjet = 3; + public const byte ElevatedBridge = 4; + } + + [Desc("Used by Mobile. Attach these to the world actor. You can have multiple variants by adding @suffixes.")] + public class LocomotorInfo : ITraitInfo + { + [Desc("Locomotor ID.")] + public readonly string Name = "default"; + + public readonly int WaitAverage = 5; + + public readonly int WaitSpread = 2; + + [Desc("Allow multiple (infantry) units in one cell.")] + public readonly bool SharesCell = false; + + [Desc("Can the actor be ordered to move in to shroud?")] + public readonly bool MoveIntoShroud = true; + + [Desc("e.g. crate, wall, infantry")] + public readonly HashSet Crushes = new HashSet(); + + [Desc("Types of damage that are caused while crushing. Leave empty for no damage types.")] + public readonly HashSet CrushDamageTypes = new HashSet(); + + [FieldLoader.LoadUsing("LoadSpeeds", true)] + [Desc("Set Water: 0 for ground units and lower the value on rough terrain.")] + public readonly Dictionary TerrainSpeeds; + + protected static object LoadSpeeds(MiniYaml y) + { + var ret = new Dictionary(); + foreach (var t in y.ToDictionary()["TerrainSpeeds"].Nodes) + { + var speed = FieldLoader.GetValue("speed", t.Value.Value); + var nodesDict = t.Value.ToDictionary(); + var cost = nodesDict.ContainsKey("PathingCost") + ? FieldLoader.GetValue("cost", nodesDict["PathingCost"].Value) + : 10000 / speed; + ret.Add(t.Key, new TerrainInfo(speed, cost)); + } + + return ret; + } + + TerrainInfo[] LoadTilesetSpeeds(TileSet tileSet) + { + var info = new TerrainInfo[tileSet.TerrainInfo.Length]; + for (var i = 0; i < info.Length; i++) + info[i] = TerrainInfo.Impassable; + + foreach (var kvp in TerrainSpeeds) + { + byte index; + if (tileSet.TryGetTerrainIndex(kvp.Key, out index)) + info[index] = kvp.Value; + } + + return info; + } + + public class TerrainInfo + { + public static readonly TerrainInfo Impassable = new TerrainInfo(); + + public readonly int Cost; + public readonly int Speed; + + public TerrainInfo() + { + Cost = int.MaxValue; + Speed = 0; + } + + public TerrainInfo(int speed, int cost) + { + Speed = speed; + Cost = cost; + } + } + + public struct WorldMovementInfo + { + internal readonly World World; + internal readonly TerrainInfo[] TerrainInfos; + internal WorldMovementInfo(World world, LocomotorInfo info) + { + // PERF: This struct allows us to cache the terrain info for the tileset used by the world. + // This allows us to speed up some performance-sensitive pathfinding calculations. + World = world; + TerrainInfos = info.TilesetTerrainInfo[world.Map.Rules.TileSet]; + } + } + + public readonly Cache TilesetTerrainInfo; + public readonly Cache TilesetMovementClass; + + public LocomotorInfo() + { + TilesetTerrainInfo = new Cache(LoadTilesetSpeeds); + TilesetMovementClass = new Cache(CalculateTilesetMovementClass); + } + + public int MovementCostForCell(World world, CPos cell) + { + return MovementCostForCell(world, TilesetTerrainInfo[world.Map.Rules.TileSet], cell); + } + + int MovementCostForCell(World world, TerrainInfo[] terrainInfos, CPos cell) + { + if (!world.Map.Contains(cell)) + return int.MaxValue; + + var index = cell.Layer == 0 ? world.Map.GetTerrainIndex(cell) : + world.GetCustomMovementLayers()[cell.Layer].GetTerrainIndex(cell); + + if (index == byte.MaxValue) + return int.MaxValue; + + return terrainInfos[index].Cost; + } + + public int CalculateTilesetMovementClass(TileSet tileset) + { + // collect our ability to cross *all* terraintypes, in a bitvector + return TilesetTerrainInfo[tileset].Select(ti => ti.Cost < int.MaxValue).ToBits(); + } + + public int GetMovementClass(TileSet tileset) + { + return TilesetMovementClass[tileset]; + } + + public int TileSetMovementHash(TileSet tileSet) + { + var terrainInfos = TilesetTerrainInfo[tileSet]; + + // Compute and return the hash using aggregate + return terrainInfos.Aggregate(terrainInfos.Length, + (current, terrainInfo) => unchecked(current * 31 + terrainInfo.Cost)); + } + + public WorldMovementInfo GetWorldMovementInfo(World world) + { + return new WorldMovementInfo(world, this); + } + + public int MovementCostToEnterCell(WorldMovementInfo worldMovementInfo, Actor self, CPos cell, Actor ignoreActor = null, CellConditions check = CellConditions.All) + { + var cost = MovementCostForCell(worldMovementInfo.World, worldMovementInfo.TerrainInfos, cell); + if (cost == int.MaxValue || !CanMoveFreelyInto(worldMovementInfo.World, self, cell, ignoreActor, check)) + return int.MaxValue; + return cost; + } + + public SubCell GetAvailableSubCell( + World world, Actor self, CPos cell, SubCell preferredSubCell = SubCell.Any, Actor ignoreActor = null, CellConditions check = CellConditions.All) + { + if (MovementCostForCell(world, cell) == int.MaxValue) + return SubCell.Invalid; + + if (check.HasCellCondition(CellConditions.TransientActors)) + { + Func checkTransient = otherActor => IsBlockedBy(self, otherActor, ignoreActor, check); + + if (!SharesCell) + return world.ActorMap.AnyActorsAt(cell, SubCell.FullCell, checkTransient) ? SubCell.Invalid : SubCell.FullCell; + + return world.ActorMap.FreeSubCell(cell, preferredSubCell, checkTransient); + } + + if (!SharesCell) + return world.ActorMap.AnyActorsAt(cell, SubCell.FullCell) ? SubCell.Invalid : SubCell.FullCell; + + return world.ActorMap.FreeSubCell(cell, preferredSubCell); + } + + static bool IsMovingInMyDirection(Actor self, Actor other) + { + var otherMobile = other.TraitOrDefault(); + if (otherMobile == null || !otherMobile.IsMoving) + return false; + + var selfMobile = self.TraitOrDefault(); + if (selfMobile == null) + return false; + + // Moving in the same direction if the facing delta is between +/- 90 degrees + var delta = Util.NormalizeFacing(otherMobile.Facing - selfMobile.Facing); + return delta < 64 || delta > 192; + } + + // Determines whether the actor is blocked by other Actors + public bool CanMoveFreelyInto(World world, Actor self, CPos cell, Actor ignoreActor, CellConditions check) + { + if (!check.HasCellCondition(CellConditions.TransientActors)) + return true; + + if (SharesCell && world.ActorMap.HasFreeSubCell(cell)) + return true; + + // PERF: Avoid LINQ. + foreach (var otherActor in world.ActorMap.GetActorsAt(cell)) + if (IsBlockedBy(self, otherActor, ignoreActor, check)) + return false; + + return true; + } + + bool IsBlockedBy(Actor self, Actor otherActor, Actor ignoreActor, CellConditions check) + { + // We are not blocked by the actor we are ignoring. + if (otherActor == ignoreActor) + return false; + + // If self is null, we don't have a real actor - we're just checking what would happen theoretically. + // In such a scenario - we'll just assume any other actor in the cell will block us by default. + // If we have a real actor, we can then perform the extra checks that allow us to avoid being blocked. + if (self == null) + return true; + + // If the check allows: we are not blocked by allied units moving in our direction. + if (!check.HasCellCondition(CellConditions.BlockedByMovers) && + self.Owner.Stances[otherActor.Owner] == Stance.Ally && + IsMovingInMyDirection(self, otherActor)) + return false; + + // If there is a temporary blocker in our path, but we can remove it, we are not blocked. + var temporaryBlocker = otherActor.TraitOrDefault(); + if (temporaryBlocker != null && temporaryBlocker.CanRemoveBlockage(otherActor, self)) + return false; + + // If we cannot crush the other actor in our way, we are blocked. + if (Crushes == null || Crushes.Count == 0) + return true; + + // If the other actor in our way cannot be crushed, we are blocked. + // PERF: Avoid LINQ. + var crushables = otherActor.TraitsImplementing(); + foreach (var crushable in crushables) + if (crushable.CrushableBy(otherActor, self, Crushes)) + return false; + + return true; + } + + public virtual object Create(ActorInitializer init) { return new Locomotor(init.Self, this); } + } + + public class Locomotor + { + public readonly LocomotorInfo Info; + + public Locomotor(Actor self, LocomotorInfo info) + { + Info = info; + } + } +} diff --git a/OpenRA.Mods.Common/Traits/World/PathFinder.cs b/OpenRA.Mods.Common/Traits/World/PathFinder.cs index 8b0951389b..51906d339c 100644 --- a/OpenRA.Mods.Common/Traits/World/PathFinder.cs +++ b/OpenRA.Mods.Common/Traits/World/PathFinder.cs @@ -13,13 +13,14 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using OpenRA.Graphics; using OpenRA.Mods.Common.Pathfinder; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { [Desc("Calculates routes for mobile units based on the A* search algorithm.", " Attach this to the world actor.")] - public class PathFinderInfo : ITraitInfo + public class PathFinderInfo : ITraitInfo, Requires { public object Create(ActorInitializer init) { @@ -62,20 +63,20 @@ namespace OpenRA.Mods.Common.Traits public List FindUnitPath(CPos source, CPos target, Actor self, Actor ignoreActor) { - var mi = self.Info.TraitInfo(); + var li = self.Info.TraitInfo().LocomotorInfo; // If a water-land transition is required, bail early var domainIndex = world.WorldActor.TraitOrDefault(); if (domainIndex != null) { - var passable = mi.GetMovementClass(world.Map.Rules.TileSet); - if (!domainIndex.IsPassable(source, target, mi, (uint)passable)) + var passable = li.GetMovementClass(world.Map.Rules.TileSet); + if (!domainIndex.IsPassable(source, target, li, (uint)passable)) return EmptyPath; } List pb; - using (var fromSrc = PathSearch.FromPoint(world, mi, self, target, source, true).WithIgnoredActor(ignoreActor)) - using (var fromDest = PathSearch.FromPoint(world, mi, self, source, target, true).WithIgnoredActor(ignoreActor).Reverse()) + using (var fromSrc = PathSearch.FromPoint(world, li, self, target, source, true).WithIgnoredActor(ignoreActor)) + using (var fromDest = PathSearch.FromPoint(world, li, self, source, target, true).WithIgnoredActor(ignoreActor).Reverse()) pb = FindBidiPath(fromSrc, fromDest); CheckSanePath2(pb, source, target); @@ -86,6 +87,7 @@ namespace OpenRA.Mods.Common.Traits public List FindUnitPathToRange(CPos source, SubCell srcSub, WPos target, WDist range, Actor self) { var mi = self.Info.TraitInfo(); + var li = mi.LocomotorInfo; var targetCell = world.Map.CellContaining(target); // Correct for SubCell offset @@ -102,14 +104,14 @@ namespace OpenRA.Mods.Common.Traits var domainIndex = world.WorldActor.TraitOrDefault(); if (domainIndex != null) { - var passable = mi.GetMovementClass(world.Map.Rules.TileSet); - tilesInRange = new List(tilesInRange.Where(t => domainIndex.IsPassable(source, t, mi, (uint)passable))); + var passable = li.GetMovementClass(world.Map.Rules.TileSet); + tilesInRange = new List(tilesInRange.Where(t => domainIndex.IsPassable(source, t, li, (uint)passable))); if (!tilesInRange.Any()) return EmptyPath; } - using (var fromSrc = PathSearch.FromPoints(world, mi, self, tilesInRange, source, true)) - using (var fromDest = PathSearch.FromPoint(world, mi, self, source, targetCell, true).Reverse()) + using (var fromSrc = PathSearch.FromPoints(world, li, self, tilesInRange, source, true)) + using (var fromDest = PathSearch.FromPoint(world, li, self, source, targetCell, true).Reverse()) return FindBidiPath(fromSrc, fromDest); } diff --git a/OpenRA.Mods.Common/Traits/World/SubterraneanActorLayer.cs b/OpenRA.Mods.Common/Traits/World/SubterraneanActorLayer.cs index b3d72a09b6..d20dae19d1 100644 --- a/OpenRA.Mods.Common/Traits/World/SubterraneanActorLayer.cs +++ b/OpenRA.Mods.Common/Traits/World/SubterraneanActorLayer.cs @@ -61,7 +61,7 @@ namespace OpenRA.Mods.Common.Traits } } - bool ICustomMovementLayer.EnabledForActor(ActorInfo a, MobileInfo mi) { return mi.Subterranean; } + bool ICustomMovementLayer.EnabledForActor(ActorInfo a, LocomotorInfo li) { return li is SubterraneanLocomotorInfo; } byte ICustomMovementLayer.Index { get { return CustomMovementLayerType.Subterranean; } } bool ICustomMovementLayer.InteractsWithDefaultLayer { get { return false; } } @@ -71,13 +71,14 @@ namespace OpenRA.Mods.Common.Traits return pos + new WVec(0, 0, height[cell] - pos.Z); } - bool ValidTransitionCell(CPos cell, MobileInfo mi) + bool ValidTransitionCell(CPos cell, LocomotorInfo li) { var terrainType = map.GetTerrainInfo(cell).Type; - if (!mi.SubterraneanTransitionTerrainTypes.Contains(terrainType) && mi.SubterraneanTransitionTerrainTypes.Any()) + var sli = (SubterraneanLocomotorInfo)li; + if (!sli.SubterraneanTransitionTerrainTypes.Contains(terrainType) && sli.SubterraneanTransitionTerrainTypes.Any()) return false; - if (mi.SubterraneanTransitionOnRamps) + if (sli.SubterraneanTransitionOnRamps) return true; var tile = map.Tiles[cell]; @@ -85,14 +86,16 @@ namespace OpenRA.Mods.Common.Traits return ti == null || ti.RampType == 0; } - int ICustomMovementLayer.EntryMovementCost(ActorInfo a, MobileInfo mi, CPos cell) + int ICustomMovementLayer.EntryMovementCost(ActorInfo a, LocomotorInfo li, CPos cell) { - return ValidTransitionCell(cell, mi) ? mi.SubterraneanTransitionCost : int.MaxValue; + var sli = (SubterraneanLocomotorInfo)li; + return ValidTransitionCell(cell, sli) ? sli.SubterraneanTransitionCost : int.MaxValue; } - int ICustomMovementLayer.ExitMovementCost(ActorInfo a, MobileInfo mi, CPos cell) + int ICustomMovementLayer.ExitMovementCost(ActorInfo a, LocomotorInfo li, CPos cell) { - return ValidTransitionCell(cell, mi) ? mi.SubterraneanTransitionCost : int.MaxValue; + var sli = (SubterraneanLocomotorInfo)li; + return ValidTransitionCell(cell, sli) ? sli.SubterraneanTransitionCost : int.MaxValue; } byte ICustomMovementLayer.GetTerrainIndex(CPos cell) diff --git a/OpenRA.Mods.Common/Traits/World/SubterraneanLocomotor.cs b/OpenRA.Mods.Common/Traits/World/SubterraneanLocomotor.cs new file mode 100644 index 0000000000..5bd69f3faf --- /dev/null +++ b/OpenRA.Mods.Common/Traits/World/SubterraneanLocomotor.cs @@ -0,0 +1,40 @@ +#region Copyright & License Information +/* + * Copyright 2007-2018 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 OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [Desc("Used by Mobile. Required for subterranean actors. Attach these to the world actor. You can have multiple variants by adding @suffixes.")] + public class SubterraneanLocomotorInfo : LocomotorInfo + { + [Desc("Pathfinding cost for submerging or reemerging.")] + public readonly int SubterraneanTransitionCost = 0; + + [Desc("The terrain types that this actor can transition on. Leave empty to allow any.")] + public readonly HashSet SubterraneanTransitionTerrainTypes = new HashSet(); + + [Desc("Can this actor transition on slopes?")] + public readonly bool SubterraneanTransitionOnRamps = false; + + [Desc("Depth at which the subterranian condition is applied.")] + public readonly WDist SubterraneanTransitionDepth = new WDist(-1024); + + public override object Create(ActorInitializer init) { return new SubterraneanLocomotor(init.Self, this); } + } + + public class SubterraneanLocomotor : Locomotor + { + public SubterraneanLocomotor(Actor self, SubterraneanLocomotorInfo info) + : base(self, info) { } + } +} diff --git a/OpenRA.Mods.Common/Traits/World/TerrainTunnelLayer.cs b/OpenRA.Mods.Common/Traits/World/TerrainTunnelLayer.cs index 7a71f7a76c..3bad9bd540 100644 --- a/OpenRA.Mods.Common/Traits/World/TerrainTunnelLayer.cs +++ b/OpenRA.Mods.Common/Traits/World/TerrainTunnelLayer.cs @@ -68,7 +68,7 @@ namespace OpenRA.Mods.Common.Traits } } - bool ICustomMovementLayer.EnabledForActor(ActorInfo a, MobileInfo mi) { return enabled; } + bool ICustomMovementLayer.EnabledForActor(ActorInfo a, LocomotorInfo li) { return enabled; } byte ICustomMovementLayer.Index { get { return CustomMovementLayerType.Tunnel; } } bool ICustomMovementLayer.InteractsWithDefaultLayer { get { return false; } } @@ -77,12 +77,12 @@ namespace OpenRA.Mods.Common.Traits return cellCenters[cell]; } - int ICustomMovementLayer.EntryMovementCost(ActorInfo a, MobileInfo mi, CPos cell) + int ICustomMovementLayer.EntryMovementCost(ActorInfo a, LocomotorInfo li, CPos cell) { return portals.Contains(cell) ? 0 : int.MaxValue; } - int ICustomMovementLayer.ExitMovementCost(ActorInfo a, MobileInfo mi, CPos cell) + int ICustomMovementLayer.ExitMovementCost(ActorInfo a, LocomotorInfo li, CPos cell) { return portals.Contains(cell) ? 0 : int.MaxValue; } diff --git a/OpenRA.Mods.Common/TraitsInterfaces.cs b/OpenRA.Mods.Common/TraitsInterfaces.cs index 309db1a746..efae316a8d 100644 --- a/OpenRA.Mods.Common/TraitsInterfaces.cs +++ b/OpenRA.Mods.Common/TraitsInterfaces.cs @@ -50,6 +50,24 @@ namespace OpenRA.Mods.Common.Traits void Sold(Actor self); } + [RequireExplicitImplementation] + public interface INotifyCustomLayerChanged + { + void CustomLayerChanged(Actor self, byte oldLayer, byte newLayer); + } + + [RequireExplicitImplementation] + public interface INotifyVisualPositionChanged + { + void VisualPositionChanged(Actor self, byte oldLayer, byte newLayer); + } + + [RequireExplicitImplementation] + public interface INotifyFinishedMoving + { + void FinishedMoving(Actor self, byte oldLayer, byte newLayer); + } + public interface IDemolishableInfo : ITraitInfoInterface { bool IsValidTarget(ActorInfo actorInfo, Actor saboteur); } public interface IDemolishable { @@ -313,9 +331,9 @@ namespace OpenRA.Mods.Common.Traits byte Index { get; } bool InteractsWithDefaultLayer { get; } - bool EnabledForActor(ActorInfo a, MobileInfo mi); - int EntryMovementCost(ActorInfo a, MobileInfo mi, CPos cell); - int ExitMovementCost(ActorInfo a, MobileInfo mi, CPos cell); + bool EnabledForActor(ActorInfo a, LocomotorInfo li); + int EntryMovementCost(ActorInfo a, LocomotorInfo li, CPos cell); + int ExitMovementCost(ActorInfo a, LocomotorInfo li, CPos cell); byte GetTerrainIndex(CPos cell); WPos CenterOfCell(CPos cell);