Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add celltrigger support to ProximityCapturable #21123

Merged
merged 4 commits into from
Dec 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 24 additions & 0 deletions OpenRA.Game/FieldLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont
{ typeof(WAngle), ParseWAngle },
{ typeof(WRot), ParseWRot },
{ typeof(CPos), ParseCPos },
{ typeof(CPos[]), ParseCPosArray },
{ typeof(CVec), ParseCVec },
{ typeof(CVec[]), ParseCVecArray },
{ typeof(BooleanExpression), ParseBooleanExpression },
Expand Down Expand Up @@ -287,6 +288,29 @@ static object ParseCPos(string fieldName, Type fieldType, string value, MemberIn
return InvalidValueAction(value, fieldType, fieldName);
}

static object ParseCPosArray(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (value != null)
{
var parts = value.Split(SplitComma);

if (parts.Length % 2 != 0)
return InvalidValueAction(value, fieldType, fieldName);

var vecs = new CPos[parts.Length / 2];
for (var i = 0; i < vecs.Length; i++)
{
if (int.TryParse(parts[2 * i], out var rx)
&& int.TryParse(parts[2 * i + 1], out var ry))
vecs[i] = new CPos(rx, ry);
}

return vecs;
}

return InvalidValueAction(value, fieldType, fieldName);
}

static object ParseCVec(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (value != null)
Expand Down
98 changes: 98 additions & 0 deletions OpenRA.Mods.Common/Graphics/BorderedRegionRenderable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#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.Collections.Generic;
using System.Linq;
using OpenRA.Graphics;
using OpenRA.Primitives;

namespace OpenRA.Mods.Common.Graphics
{
public readonly struct BorderedRegionRenderable : IRenderable, IFinalizedRenderable
{
enum Corner { TopLeft, TopRight, BottomRight, BottomLeft }

// Maps a cell offset to the index of the corner (in the 'Corner' arrays in the MapGrid.CellRamp structs)
// from which a border should be drawn. The index of the end corner will be (cornerIndex + 1) % 4.
static readonly Dictionary<CVec, int> Offset2CornerIndex = new()
{
{ new CVec(0, -1), (int)Corner.TopLeft },
{ new CVec(1, 0), (int)Corner.TopRight },
{ new CVec(0, 1), (int)Corner.BottomRight },
{ new CVec(-1, 0), (int)Corner.BottomLeft },
};

readonly CPos[] region;
readonly Color color, contrastColor;
readonly float width, contrastWidth;

public BorderedRegionRenderable(CPos[] region, Color color, float width, Color contrastColor, float contrastWidth)
{
this.region = region;
this.color = color;
this.contrastColor = contrastColor;
this.width = width;
this.contrastWidth = contrastWidth;
}

readonly WPos IRenderable.Pos { get { return WPos.Zero; } }
readonly int IRenderable.ZOffset { get { return 0; } }
readonly bool IRenderable.IsDecoration { get { return true; } }

IRenderable IRenderable.WithZOffset(int newOffset) { return new BorderedRegionRenderable(region, color, width, contrastColor, contrastWidth); }
IRenderable IRenderable.OffsetBy(in WVec offset) { return new BorderedRegionRenderable(region, color, width, contrastColor, contrastWidth); }
IRenderable IRenderable.AsDecoration() { return this; }

IFinalizedRenderable IRenderable.PrepareRender(WorldRenderer wr) { return this; }
void IFinalizedRenderable.Render(WorldRenderer wr) { Draw(wr, region, color, width, contrastColor, contrastWidth); }
void IFinalizedRenderable.RenderDebugGeometry(WorldRenderer wr) { }
Rectangle IFinalizedRenderable.ScreenBounds(WorldRenderer wr) { return Rectangle.Empty; }

public static void Draw(WorldRenderer wr, CPos[] region, Color color, float width, Color constrastColor, float constrastWidth)
{
if (width == 0 && constrastWidth == 0)
return;

var map = wr.World.Map;
var cr = Game.Renderer.RgbaColorRenderer;

foreach (var c in region)
{
var mpos = c.ToMPos(map);
if (!map.Height.Contains(mpos) || wr.World.ShroudObscures(c))
continue;

var tile = map.Tiles[mpos];
var ti = map.Rules.TerrainInfo.GetTerrainInfo(tile);
var ramp = ti?.RampType ?? 0;

var corners = map.Grid.Ramps[ramp].Corners;
var pos = map.CenterOfCell(c) - new WVec(0, 0, map.Grid.Ramps[ramp].CenterHeightOffset);

foreach (var o in Offset2CornerIndex)
{
// If the neighboring cell is part of the region, don't draw a border between the cells.
if (region.Contains(c + o.Key))
continue;

var start = wr.Viewport.WorldToViewPx(wr.Screen3DPosition(pos + corners[o.Value]));
var end = wr.Viewport.WorldToViewPx(wr.Screen3DPosition(pos + corners[(o.Value + 1) % 4]));

if (constrastWidth > 0)
cr.DrawLine(start, end, constrastWidth, constrastColor);

if (width > 0)
cr.DrawLine(start, end, width, color);
}
}
}
}
}
174 changes: 18 additions & 156 deletions OpenRA.Mods.Common/Traits/ProximityCapturable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,187 +9,49 @@
*/
#endregion

using System.Collections.Generic;
using System.Linq;
using OpenRA.Mods.Common.Effects;
using OpenRA.Graphics;
using OpenRA.Mods.Common.Graphics;
using OpenRA.Primitives;
using OpenRA.Traits;

namespace OpenRA.Mods.Common.Traits
{
[Desc("Actor can be captured by units in a specified proximity.")]
public class ProximityCapturableInfo : TraitInfo, IRulesetLoaded
[Desc("Actor can be captured by units within a certain range.")]
public class ProximityCapturableInfo : ProximityCapturableBaseInfo
{
[Desc("Maximum range at which a ProximityCaptor actor can initiate the capture.")]
[Desc("Maximum range at which a " + nameof(ProximityCaptor) + " actor can initiate the capture.")]
public readonly WDist Range = WDist.FromCells(5);

[Desc("Allowed ProximityCaptor actors to capture this actor.")]
public readonly BitSet<CaptureType> CaptorTypes = new("Player", "Vehicle", "Tank", "Infantry");

[Desc("If set, the capturing process stops immediately after another player comes into Range.")]
public readonly bool MustBeClear = false;

[Desc("If set, the ownership will not revert back when the captor leaves the area.")]
public readonly bool Sticky = false;

[Desc("If set, the actor can only be captured via this logic once.",
"This option implies the `Sticky` behaviour as well.")]
public readonly bool Permanent = false;

public void RulesetLoaded(Ruleset rules, ActorInfo info)
{
var pci = rules.Actors[SystemActors.Player].TraitInfoOrDefault<ProximityCaptorInfo>();
if (pci == null)
throw new YamlException("ProximityCapturable requires the `Player` actor to have the ProximityCaptor trait.");
}

public override object Create(ActorInitializer init) { return new ProximityCapturable(init.Self, this); }
public override object Create(ActorInitializer init) { return new ProximityCapturable(init, this); }
}

public class ProximityCapturable : ITick, INotifyAddedToWorld, INotifyRemovedFromWorld, INotifyOwnerChanged
public class ProximityCapturable : ProximityCapturableBase
{
public readonly Player OriginalOwner;
public bool Captured => Self.Owner != OriginalOwner;

public ProximityCapturableInfo Info;
public Actor Self;
public new readonly ProximityCapturableInfo Info;

readonly List<Actor> actorsInRange = new();
int proximityTrigger;
WPos prevPosition;
bool skipTriggerUpdate;

public ProximityCapturable(Actor self, ProximityCapturableInfo info)
public ProximityCapturable(ActorInitializer init, ProximityCapturableInfo info)
: base(init, info)
{
Info = info;
Self = self;
OriginalOwner = self.Owner;
}

void INotifyAddedToWorld.AddedToWorld(Actor self)
protected override int CreateTrigger(Actor self)
{
if (skipTriggerUpdate)
return;

// TODO: Eventually support CellTriggers as well
proximityTrigger = self.World.ActorMap.AddProximityTrigger(self.CenterPosition, Info.Range, WDist.Zero, ActorEntered, ActorLeft);
return self.World.ActorMap.AddProximityTrigger(self.CenterPosition, Info.Range, WDist.Zero, ActorEntered, ActorLeft);
}

void INotifyRemovedFromWorld.RemovedFromWorld(Actor self)
protected override void RemoveTrigger(Actor self, int trigger)
{
if (skipTriggerUpdate)
return;

self.World.ActorMap.RemoveProximityTrigger(proximityTrigger);
actorsInRange.Clear();
}

void ITick.Tick(Actor self)
{
if (!self.IsInWorld || self.CenterPosition == prevPosition)
return;

self.World.ActorMap.UpdateProximityTrigger(proximityTrigger, self.CenterPosition, Info.Range, WDist.Zero);
prevPosition = self.CenterPosition;
}

void ActorEntered(Actor other)
{
if (skipTriggerUpdate || !CanBeCapturedBy(other))
return;

actorsInRange.Add(other);
UpdateOwnership();
self.World.ActorMap.RemoveProximityTrigger(trigger);
}

void ActorLeft(Actor other)
protected override void TickInner(Actor self)
{
if (skipTriggerUpdate || !CanBeCapturedBy(other))
return;

actorsInRange.Remove(other);
UpdateOwnership();
}

bool CanBeCapturedBy(Actor a)
{
if (a == Self)
return false;

var pc = a.Info.TraitInfoOrDefault<ProximityCaptorInfo>();
return pc != null && pc.Types.Overlaps(Info.CaptorTypes);
}

void UpdateOwnership()
{
if (Captured && Info.Permanent)
{
// This area has been captured and cannot ever be re-captured, so we get rid of the
// ProximityTrigger and ensure that it won't be recreated in AddedToWorld.
skipTriggerUpdate = true;
Self.World.ActorMap.RemoveProximityTrigger(proximityTrigger);
return;
}

// The actor that has been in the area the longest will be the captor.
// The previous implementation used the closest one, but that doesn't work with
// ProximityTriggers since they only generate events when actors enter or leave.
var captor = actorsInRange.FirstOrDefault();

// The last unit left the area
if (captor == null)
{
// Unless the Sticky option is set, we revert to the original owner.
if (Captured && !Info.Sticky)
ChangeOwnership(Self, OriginalOwner.PlayerActor);
}
else
{
if (Info.MustBeClear)
{
var isClear = actorsInRange.All(a => captor.Owner.RelationshipWith(a.Owner) == PlayerRelationship.Ally);

// An enemy unit has wandered into the area, so we've lost control of it.
if (Captured && !isClear)
ChangeOwnership(Self, OriginalOwner.PlayerActor);

// We don't own the area yet, but it is clear from enemy units, so we take possession of it.
else if (Self.Owner != captor.Owner && isClear)
ChangeOwnership(Self, captor);
}
else
{
// In all other cases, we just take over.
if (Self.Owner != captor.Owner)
ChangeOwnership(Self, captor);
}
}
}

void ChangeOwnership(Actor self, Actor captor)
{
self.World.AddFrameEndTask(w =>
{
if (self.Disposed || captor.Disposed)
return;

// prevent (Added|Removed)FromWorld from firing during Actor.ChangeOwner
skipTriggerUpdate = true;
var previousOwner = self.Owner;
self.ChangeOwner(captor.Owner);

if (self.Owner == self.World.LocalPlayer)
w.Add(new FlashTarget(self, Color.White));

var pc = captor.Info.TraitInfoOrDefault<ProximityCaptorInfo>();
foreach (var t in self.TraitsImplementing<INotifyCapture>())
t.OnCapture(self, captor, previousOwner, captor.Owner, pc.Types);
});
self.World.ActorMap.UpdateProximityTrigger(trigger, self.CenterPosition, Info.Range, WDist.Zero);
}

void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner)
protected override IRenderable GetRenderable(Actor self, WorldRenderer wr)
{
Game.RunAfterTick(() => skipTriggerUpdate = false);
return new RangeCircleAnnotationRenderable(self.CenterPosition, Info.Range, 0, self.Owner.Color, 1, Color.Black, 3);
}
}
}