Files
OpenRA/OpenRA.Mods.Common/Activities/Air/Fly.cs
Paul Chote eb09870a10 Fix landing altitude checks.
Both GetPosition and LandPosition were trying to solve the problem
of dynamic altitude offsets for carryalls with different-sized cargo.

However, because they both apply the offsets, when the two are used
together the two sets of offsets cancel to 0, producing incorrect behaviour.

GetPosition is removed in favor of using CenterPosition with the dynamic
LandAltitude. Fly.Tick and Carryall ignore the z coordinate so their
behaviour is unchanged. Land and Aircraft.AtLandAltitude now return
the correct result.
2025-12-29 16:19:57 +02:00

284 lines
11 KiB
C#

#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* 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 OpenRA.Activities;
using OpenRA.Mods.Common.Traits;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Activities
{
public class Fly : Activity
{
readonly Aircraft aircraft;
readonly WDist maxRange;
readonly WDist minRange;
readonly Color? targetLineColor;
readonly WDist nearEnough;
Target target;
Target lastVisibleTarget;
bool useLastVisibleTarget;
readonly RingBuffer<WPos> previousPositions = new(5);
public Fly(Actor self, in Target t, WDist nearEnough, WPos? initialTargetPosition = null, Color? targetLineColor = null)
: this(self, t, initialTargetPosition, targetLineColor)
{
this.nearEnough = nearEnough;
}
public Fly(Actor self, in Target t, WPos? initialTargetPosition = null, Color? targetLineColor = null)
{
aircraft = self.Trait<Aircraft>();
target = t;
this.targetLineColor = targetLineColor;
// The target may become hidden between the initial order request and the first tick (e.g. if queued)
// Moving to any position (even if quite stale) is still better than immediately giving up
if ((target.Type == TargetType.Actor && target.Actor.CanBeViewedByPlayer(self.Owner))
|| target.Type == TargetType.FrozenActor || target.Type == TargetType.Terrain)
lastVisibleTarget = Target.FromPos(target.CenterPosition);
else if (initialTargetPosition.HasValue)
lastVisibleTarget = Target.FromPos(initialTargetPosition.Value);
}
public Fly(Actor self, in Target t, WDist minRange, WDist maxRange,
WPos? initialTargetPosition = null, Color? targetLineColor = null)
: this(self, t, initialTargetPosition, targetLineColor)
{
this.maxRange = maxRange;
this.minRange = minRange;
}
public static void FlyTick(Actor self, Aircraft aircraft, WAngle desiredFacing, WDist desiredAltitude, in WVec moveOverride, bool idleTurn = false)
{
var dat = self.World.Map.DistanceAboveTerrain(aircraft.CenterPosition);
var move = moveOverride != WVec.Zero ? moveOverride : (aircraft.Info.CanSlide ? aircraft.FlyStep(desiredFacing) : aircraft.FlyStep(aircraft.Facing));
var oldFacing = aircraft.Facing;
aircraft.Facing = Util.TickFacing(aircraft.Facing, desiredFacing, aircraft.GetTurnSpeed(idleTurn));
var roll = idleTurn ? aircraft.Info.IdleRoll ?? aircraft.Info.Roll : aircraft.Info.Roll;
if (roll != WAngle.Zero)
{
var desiredRoll = aircraft.Facing == desiredFacing ? WAngle.Zero :
new WAngle(roll.Angle * Util.GetTurnDirection(aircraft.Facing, oldFacing));
aircraft.Roll = Util.TickFacing(aircraft.Roll, desiredRoll, aircraft.Info.RollSpeed);
}
if (aircraft.Info.Pitch != WAngle.Zero)
aircraft.Pitch = Util.TickFacing(aircraft.Pitch, aircraft.Info.Pitch, aircraft.Info.PitchSpeed);
// Note: we assume that if move.Z is not zero, it's intentional and we want to move in that vertical direction instead of towards desiredAltitude.
// If that is not desired, the place that calls this should make sure moveOverride.Z is zero.
if (dat != desiredAltitude || move.Z != 0)
{
var maxDelta = move.HorizontalLength * aircraft.Info.MaximumPitch.Tan() / 1024;
var moveZ = move.Z != 0 ? move.Z : (desiredAltitude.Length - dat.Length);
var deltaZ = moveZ.Clamp(-maxDelta, maxDelta);
move = new WVec(move.X, move.Y, deltaZ);
}
aircraft.SetPosition(self, aircraft.CenterPosition + move);
}
public static void FlyTick(Actor self, Aircraft aircraft, WAngle desiredFacing, WDist desiredAltitude, bool idleTurn = false)
{
FlyTick(self, aircraft, desiredFacing, desiredAltitude, WVec.Zero, idleTurn);
}
// Should only be used for vertical-only movement, usually VTOL take-off or land. Terrain-induced altitude changes should always be handled by FlyTick.
public static bool VerticalTakeOffOrLandTick(Actor self, Aircraft aircraft, WAngle desiredFacing, WDist desiredAltitude, bool idleTurn = false)
{
var turnSpeed = idleTurn ? aircraft.IdleTurnSpeed ?? aircraft.TurnSpeed : aircraft.TurnSpeed;
aircraft.Facing = Util.TickFacing(aircraft.Facing, desiredFacing, turnSpeed);
var dat = self.World.Map.DistanceAboveTerrain(aircraft.CenterPosition);
if (dat == desiredAltitude)
return false;
var maxDelta = aircraft.Info.AltitudeVelocity.Length;
var deltaZ = (desiredAltitude.Length - dat.Length).Clamp(-maxDelta, maxDelta);
aircraft.SetPosition(self, aircraft.CenterPosition + new WVec(0, 0, deltaZ));
return true;
}
public override bool Tick(Actor self)
{
// Refuse to take off if it would land immediately again.
if (aircraft.ForceLanding)
Cancel(self);
var dat = self.World.Map.DistanceAboveTerrain(aircraft.CenterPosition);
var isLanded = dat <= aircraft.LandAltitude;
// HACK: Prevent paused (for example, EMP'd) aircraft from taking off.
// This is necessary until the TODOs in the IsCanceling block below are addressed.
if (isLanded && aircraft.IsTraitPaused)
return false;
if (IsCanceling)
{
// We must return the actor to a sensible height before continuing.
// If the aircraft is on the ground we queue TakeOff to manage the influence reservation and takeoff sounds etc.
// TODO: It would be better to not take off at all, but we lack the plumbing to detect current airborne/landed state.
// If the aircraft lands when idle and is idle, we let the default idle handler manage this.
// TODO: Remove this after fixing all activities to work properly with arbitrary starting altitudes.
var landWhenIdle = aircraft.Info.IdleBehavior == IdleBehaviorType.Land;
var skipHeightAdjustment = landWhenIdle && self.CurrentActivity.IsCanceling && self.CurrentActivity.NextActivity == null;
if (aircraft.Info.CanHover && !skipHeightAdjustment && dat != aircraft.Info.CruiseAltitude)
{
if (isLanded)
QueueChild(new TakeOff(self));
else
VerticalTakeOffOrLandTick(self, aircraft, aircraft.Facing, aircraft.Info.CruiseAltitude);
return false;
}
return true;
}
else if (isLanded)
{
QueueChild(new TakeOff(self));
return false;
}
target = target.Recalculate(self.Owner, out var targetIsHiddenActor);
if (!targetIsHiddenActor && target.Type == TargetType.Actor)
lastVisibleTarget = Target.FromTargetPositions(target);
useLastVisibleTarget = targetIsHiddenActor || !target.IsValidFor(self);
// Target is hidden or dead, and we don't have a fallback position to move towards
if (useLastVisibleTarget && !lastVisibleTarget.IsValidFor(self))
return true;
var checkTarget = useLastVisibleTarget ? lastVisibleTarget : target;
var pos = self.CenterPosition;
var delta = checkTarget.CenterPosition - pos;
// Inside the target annulus, so we're done
var insideMaxRange = maxRange.Length > 0 && checkTarget.IsInRange(pos, maxRange);
var insideMinRange = minRange.Length > 0 && checkTarget.IsInRange(pos, minRange);
if (insideMaxRange && !insideMinRange)
return true;
var isSlider = aircraft.Info.CanSlide;
var desiredFacing = aircraft.Facing;
if (delta.HorizontalLengthSquared != 0)
{
var facing = delta.Yaw;
// Prevent jittering.
var diff = Math.Abs(facing.Angle - desiredFacing.Angle);
var deadzone = aircraft.Info.TurnDeadzone.Angle;
if (diff > deadzone && diff < 1024 - deadzone)
desiredFacing = facing;
}
var move = isSlider ? aircraft.FlyStep(desiredFacing) : aircraft.FlyStep(aircraft.Facing);
// Inside the minimum range, so reverse if we CanSlide, otherwise face away from the target.
if (insideMinRange)
{
if (isSlider)
FlyTick(self, aircraft, desiredFacing, aircraft.Info.CruiseAltitude, -move);
else
FlyTick(self, aircraft, desiredFacing + new WAngle(512), aircraft.Info.CruiseAltitude, move);
return false;
}
// HACK: Consider ourselves blocked if we have moved by less than 64 WDist in the last five ticks
// Stop if we are blocked and close enough
if (previousPositions.Count == previousPositions.Capacity &&
(previousPositions.First() - previousPositions.Last()).LengthSquared < 4096 &&
delta.HorizontalLengthSquared <= nearEnough.LengthSquared)
return true;
// The next move would overshoot, so consider it close enough or set final position if we CanSlide
if (delta.HorizontalLengthSquared < move.HorizontalLengthSquared)
{
// For VTOL landing to succeed, it must reach the exact target position,
// so for the final move it needs to behave as if it had CanSlide.
if (isSlider || aircraft.Info.VTOL)
{
// Set final (horizontal) position
if (delta.HorizontalLengthSquared != 0)
{
// Ensure we don't include a non-zero vertical component here that would move us away from CruiseAltitude
var deltaMove = new WVec(delta.X, delta.Y, 0);
FlyTick(self, aircraft, desiredFacing, dat, deltaMove);
}
// Move to CruiseAltitude, if not already there
if (dat != aircraft.Info.CruiseAltitude)
{
VerticalTakeOffOrLandTick(self, aircraft, aircraft.Facing, aircraft.Info.CruiseAltitude);
return false;
}
}
return true;
}
if (!isSlider)
{
// Using the turn rate, compute a hypothetical circle traced by a continuous turn.
// If it contains the destination point, it's unreachable without more complex maneuvering.
var turnRadius = CalculateTurnRadius(aircraft.MovementSpeed, aircraft.TurnSpeed);
// The current facing is a tangent of the minimal turn circle.
// Make a perpendicular vector, and use it to locate the turn's center.
var turnCenterFacing = aircraft.Facing + new WAngle(Util.GetTurnDirection(aircraft.Facing, desiredFacing) * 256);
var turnCenterDir = new WVec(0, -1024, 0).Rotate(WRot.FromYaw(turnCenterFacing));
turnCenterDir *= turnRadius;
turnCenterDir /= 1024;
// Compare with the target point, and keep flying away if it's inside the circle.
var turnCenter = aircraft.CenterPosition + turnCenterDir;
if ((checkTarget.CenterPosition - turnCenter).HorizontalLengthSquared < turnRadius * turnRadius)
desiredFacing = aircraft.Facing;
}
previousPositions.Add(self.CenterPosition);
FlyTick(self, aircraft, desiredFacing, aircraft.Info.CruiseAltitude, WVec.Zero);
return false;
}
public override IEnumerable<Target> GetTargets(Actor self)
{
yield return target;
}
public override IEnumerable<TargetLineNode> TargetLineNodes(Actor self)
{
if (targetLineColor.HasValue)
yield return new TargetLineNode(useLastVisibleTarget ? lastVisibleTarget : target, targetLineColor.Value);
}
public static int CalculateTurnRadius(int speed, WAngle turnSpeed)
{
// turnSpeed -> divide into 256 to get the number of ticks per complete rotation
// speed -> multiply to get distance travelled per rotation (circumference)
// 180 -> divide by 2*pi to get the turn radius: 180==1024/(2*pi), with some extra leeway
return turnSpeed.Angle > 0 ? 180 * speed / turnSpeed.Angle : 0;
}
}
}