Files
OpenRA/OpenRA.Mods.Common/Orders/UnitOrderGenerator.cs
Dominic Renaud 032f20b622 Fix selection cursor behaviour
Fix erroneous minimap selection cursor when not in classic mode

Fix selection cursor not showing up with units selected when not in classic mouse style
2026-01-04 12:35:35 +00:00

225 lines
8.0 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.Collections.Generic;
using System.Linq;
using OpenRA.Graphics;
using OpenRA.Orders;
using OpenRA.Traits;
using OpenRA.Widgets;
namespace OpenRA.Mods.Common.Orders
{
public class UnitOrderGenerator : IOrderGenerator
{
readonly string worldSelectCursor = ChromeMetrics.Get<string>("WorldSelectCursor");
readonly string worldDefaultCursor = ChromeMetrics.Get<string>("WorldDefaultCursor");
readonly GameSettings gameSettings;
protected virtual MouseActionType ActionType => MouseActionType.Contextual;
public MouseButton ActionButton => gameSettings.ResolveActionButton(ActionType);
public MouseButton CancelButton => gameSettings.ResolveCancelButton(ActionType);
public UnitOrderGenerator(World world)
{
gameSettings = Game.Settings.Game;
}
protected static Target TargetForInput(World world, CPos cell, int2 worldPixel, MouseInput mi)
{
var actor = world.ScreenMap.ActorsAtMouse(mi)
.Where(a => !a.Actor.IsDead && a.Actor.Info.HasTraitInfo<ITargetableInfo>() && !world.FogObscures(a.Actor))
.WithHighestSelectionPriority(worldPixel, mi.Modifiers);
if (actor != null)
return Target.FromActor(actor);
var frozen = world.ScreenMap.FrozenActorsAtMouse(world.RenderPlayer, mi)
.Where(a => a.Info.HasTraitInfo<ITargetableInfo>() && a.Visible && a.HasRenderables)
.WithHighestSelectionPriority(worldPixel, mi.Modifiers);
if (frozen != null)
return Target.FromFrozenActor(frozen);
return Target.FromCell(world, cell);
}
public virtual IEnumerable<Order> Order(World world, CPos cell, int2 worldPixel, MouseInput mi)
{
if (mi.Button == ActionButton)
return OrderInner(world, cell, worldPixel, mi);
if (mi.Button == CancelButton)
world.CancelInputMode();
return [];
}
protected virtual IEnumerable<Order> OrderInner(World world, CPos cell, int2 worldPixel, MouseInput mi)
{
var target = TargetForInput(world, cell, worldPixel, mi);
var orders = world.Selection.Actors
.Select(a => OrderForUnit(a, target, cell, mi))
.Where(o => o != null)
.ToList();
var actorsInvolved = orders.Select(o => o.Actor).Distinct().ToArray();
if (actorsInvolved.Length == 0)
yield break;
// HACK: This is required by the hacky player actions-per-minute calculation
// TODO: Reimplement APM properly and then remove this
yield return new Order("CreateGroup", actorsInvolved[0].Owner.PlayerActor, false, actorsInvolved);
foreach (var o in orders)
yield return CheckSameOrder(o.Order, o.Trait.IssueOrder(o.Actor, o.Order, o.Target, mi.Modifiers.HasModifier(Modifiers.Shift)));
}
public virtual void Tick(World world) { }
public virtual IEnumerable<IRenderable> Render(WorldRenderer wr, World world) { yield break; }
public virtual IEnumerable<IRenderable> RenderAboveShroud(WorldRenderer wr, World world) { yield break; }
public virtual IEnumerable<IRenderable> RenderAnnotations(WorldRenderer wr, World world) { yield break; }
public virtual string GetCursor(World world, CPos cell, int2 worldPixel, MouseInput mi)
{
var target = TargetForInput(world, cell, worldPixel, mi);
bool useSelect;
if (gameSettings.MouseControlStyle == MouseControlStyle.Classic && !InputOverridesSelection(world, worldPixel, mi))
useSelect = target.Type == TargetType.Actor && target.Actor.Info.HasTraitInfo<ISelectableInfo>();
else
{
var ordersWithCursor = world.Selection.Actors
.Select(a => OrderForUnit(a, target, cell, mi))
.Where(o => o != null && o.Cursor != null);
var cursorOrder = ordersWithCursor.MaxByOrDefault(o => o.Order.OrderPriority);
useSelect = target.Type == TargetType.Actor && target.Actor.Info.HasTraitInfo<ISelectableInfo>() &&
(cursorOrder == null || world.Selection.Actors.Count == 0 || !InputOverridesSelection(world, worldPixel, mi));
if (!useSelect && cursorOrder != null)
return cursorOrder.Cursor;
}
return useSelect ? worldSelectCursor : worldDefaultCursor;
}
public void Deactivate() { }
bool IOrderGenerator.HandleKeyPress(KeyInput e) { return false; }
// Used for classic mouse orders, determines whether or not action at xy is move or select
public virtual bool InputOverridesSelection(World world, int2 xy, MouseInput mi)
{
var actor = world.ScreenMap.ActorsAtMouse(xy)
.Where(a =>
!a.Actor.IsDead &&
a.Actor.Info.HasTraitInfo<ISelectableInfo>() &&
(a.Actor.Owner.IsAlliedWith(world.RenderPlayer) || !world.FogObscures(a.Actor)))
.WithHighestSelectionPriority(xy, mi.Modifiers);
if (actor == null)
return true;
var target = Target.FromActor(actor);
var cell = world.Map.CellContaining(target.CenterPosition);
var actorsAt = world.ActorMap.GetActorsAt(cell).ToList();
var modifiers = TargetModifiers.None;
if (mi.Modifiers.HasModifier(Modifiers.Ctrl))
modifiers |= TargetModifiers.ForceAttack;
if (mi.Modifiers.HasModifier(Modifiers.Shift))
modifiers |= TargetModifiers.ForceQueue;
if (mi.Modifiers.HasModifier(Modifiers.Alt))
modifiers |= TargetModifiers.ForceMove;
foreach (var a in world.Selection.Actors)
{
var o = OrderForUnit(a, target, cell, mi);
if (o != null && o.Order.TargetOverridesSelection(a, target, actorsAt, cell, modifiers))
return true;
}
return false;
}
public virtual void SelectionChanged(World world, IEnumerable<Actor> selected) { }
/// <summary>
/// Returns the most appropriate order for a given actor and target.
/// First priority is given to orders that interact with the given actors.
/// Second priority is given to actors in the given cell.
/// </summary>
protected UnitOrderResult OrderForUnit(Actor self, Target target, CPos xy, MouseInput mi)
{
if (self.Owner != self.World.LocalPlayer)
return null;
if (self.World.IsGameOver)
return null;
if (self.Disposed || !target.IsValidFor(self))
return null;
var modifiers = TargetModifiers.None;
if (mi.Modifiers.HasModifier(Modifiers.Ctrl))
modifiers |= TargetModifiers.ForceAttack;
if (mi.Modifiers.HasModifier(Modifiers.Shift))
modifiers |= TargetModifiers.ForceQueue;
if (mi.Modifiers.HasModifier(Modifiers.Alt))
modifiers |= TargetModifiers.ForceMove;
var orders = self.TraitsImplementing<IIssueOrder>()
.SelectMany(trait => trait.Orders.Select(x => new { Trait = trait, Order = x }))
.OrderByDescending(x => x.Order.OrderPriority)
.ToList();
for (var i = 0; i < 2; i++)
{
foreach (var o in orders)
{
var localModifiers = modifiers;
string cursor = null;
if (o.Order.CanTarget(self, target, ref localModifiers, ref cursor))
return new UnitOrderResult(self, o.Order, o.Trait, cursor, target);
}
// No valid orders, so check for orders against the cell
target = Target.FromCell(self.World, xy);
}
return null;
}
static Order CheckSameOrder(IOrderTargeter iot, Order order)
{
if (order == null && iot.OrderID != null)
TextNotificationsManager.Debug("BUG: in order targeter - decided on {0} but then didn't order", iot.OrderID);
else if (order != null && iot.OrderID != order.OrderString)
TextNotificationsManager.Debug("BUG: in order targeter - decided on {0} but ordered {1}", iot.OrderID, order.OrderString);
return order;
}
protected sealed class UnitOrderResult(Actor actor, IOrderTargeter order, IIssueOrder trait, string cursor, in Target target)
{
public readonly Actor Actor = actor;
public readonly IOrderTargeter Order = order;
public readonly IIssueOrder Trait = trait;
public readonly string Cursor = cursor;
public ref readonly Target Target => ref target;
readonly Target target = target;
}
public virtual bool ClearSelectionOnLeftClick => true;
}
}