When handling the Nodes collection in MiniYaml, individual nodes are located via one of two methods:
// Lookup a single key with linear search.
var node = yaml.Nodes.FirstOrDefault(n => n.Key == "SomeKey");
// Convert to dictionary, expecting many key lookups.
var dict = nodes.ToDictionary();
// Lookup a single key in the dictionary.
var node = dict["SomeKey"];
To simplify lookup of individual keys via linear search, provide helper methods NodeWithKeyOrDefault and NodeWithKey. These helpers do the equivalent of Single{OrDefault} searches. Whilst this requires checking the whole list, it provides a useful correctness check. Two duplicated keys in TS yaml are fixed as a result. We can also optimize the helpers to not use LINQ, avoiding allocation of the delegate to search for a key.
Adjust existing code to use either lnear searches or dictionary lookups based on whether it will be resolving many keys. Resolving few keys can be done with linear searches to avoid building a dictionary. Resolving many keys should be done with a dictionary to avoid quaradtic runtime from repeated linear searches.
200 lines
5.4 KiB
C#
200 lines
5.4 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.Traits;
|
|
|
|
namespace OpenRA.Mods.Common.Traits
|
|
{
|
|
public class SelectionInfo : TraitInfo
|
|
{
|
|
public override object Create(ActorInitializer init) { return new Selection(); }
|
|
}
|
|
|
|
[TraitLocation(SystemActors.World | SystemActors.EditorWorld)]
|
|
public class Selection : ISelection, INotifyCreated, INotifyOwnerChanged, ITick, IGameSaveTraitData
|
|
{
|
|
public int Hash { get; private set; }
|
|
public IReadOnlyCollection<Actor> Actors => actors;
|
|
|
|
readonly HashSet<Actor> actors = new();
|
|
readonly List<Actor> rolloverActors = new();
|
|
World world;
|
|
|
|
INotifySelection[] worldNotifySelection;
|
|
|
|
void INotifyCreated.Created(Actor self)
|
|
{
|
|
worldNotifySelection = self.TraitsImplementing<INotifySelection>().ToArray();
|
|
world = self.World;
|
|
}
|
|
|
|
void UpdateHash()
|
|
{
|
|
// Not a real hash, but things checking this only care about checking when the selection has changed
|
|
// For this purpose, having a false positive (forcing a refresh when nothing changed) is much better
|
|
// than a false negative (selection state mismatch)
|
|
Hash += 1;
|
|
}
|
|
|
|
public virtual void Add(Actor a)
|
|
{
|
|
actors.Add(a);
|
|
UpdateHash();
|
|
|
|
foreach (var sel in a.TraitsImplementing<INotifySelected>())
|
|
sel.Selected(a);
|
|
|
|
Sync.RunUnsynced(world, () => world.OrderGenerator.SelectionChanged(world, actors));
|
|
foreach (var ns in worldNotifySelection)
|
|
ns.SelectionChanged();
|
|
}
|
|
|
|
public virtual void Remove(Actor a)
|
|
{
|
|
if (actors.Remove(a))
|
|
{
|
|
UpdateHash();
|
|
Sync.RunUnsynced(world, () => world.OrderGenerator.SelectionChanged(world, actors));
|
|
foreach (var ns in worldNotifySelection)
|
|
ns.SelectionChanged();
|
|
}
|
|
}
|
|
|
|
void INotifyOwnerChanged.OnOwnerChanged(Actor a, Player oldOwner, Player newOwner)
|
|
{
|
|
if (!actors.Contains(a))
|
|
return;
|
|
|
|
// Remove the actor from the original owners selection
|
|
// Call UpdateHash directly for everyone else so watchers can account for the owner change if needed
|
|
if (oldOwner == world.LocalPlayer)
|
|
Remove(a);
|
|
else
|
|
UpdateHash();
|
|
}
|
|
|
|
public bool Contains(Actor a)
|
|
{
|
|
return actors.Contains(a);
|
|
}
|
|
|
|
public virtual void Combine(World world, IEnumerable<Actor> newSelection, bool isCombine, bool isClick)
|
|
{
|
|
var newSelectionCollection = newSelection as IReadOnlyCollection<Actor>;
|
|
newSelectionCollection ??= newSelection.ToList();
|
|
|
|
if (isClick)
|
|
{
|
|
// TODO: select BEST, not FIRST
|
|
var adjNewSelection = newSelectionCollection.Take(1);
|
|
if (isCombine)
|
|
actors.SymmetricExceptWith(adjNewSelection);
|
|
else
|
|
{
|
|
actors.Clear();
|
|
actors.UnionWith(adjNewSelection);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (isCombine)
|
|
actors.UnionWith(newSelectionCollection);
|
|
else
|
|
{
|
|
actors.Clear();
|
|
actors.UnionWith(newSelectionCollection);
|
|
}
|
|
}
|
|
|
|
UpdateHash();
|
|
|
|
foreach (var a in newSelectionCollection)
|
|
foreach (var sel in a.TraitsImplementing<INotifySelected>())
|
|
sel.Selected(a);
|
|
|
|
Sync.RunUnsynced(world, () => world.OrderGenerator.SelectionChanged(world, actors));
|
|
foreach (var ns in worldNotifySelection)
|
|
ns.SelectionChanged();
|
|
|
|
if (world.IsGameOver)
|
|
return;
|
|
|
|
// Play the selection voice from one of the selected actors
|
|
// TODO: This probably should only be considering the newly selected actors
|
|
foreach (var actor in actors)
|
|
{
|
|
if (actor.Owner != world.LocalPlayer || !actor.IsInWorld)
|
|
continue;
|
|
|
|
var selectable = actor.Info.TraitInfoOrDefault<ISelectableInfo>();
|
|
if (selectable == null || !actor.HasVoice(selectable.Voice))
|
|
continue;
|
|
|
|
actor.PlayVoice(selectable.Voice);
|
|
break;
|
|
}
|
|
}
|
|
|
|
public void Clear()
|
|
{
|
|
actors.Clear();
|
|
UpdateHash();
|
|
Sync.RunUnsynced(world, () => world.OrderGenerator.SelectionChanged(world, actors));
|
|
foreach (var ns in worldNotifySelection)
|
|
ns.SelectionChanged();
|
|
}
|
|
|
|
public void SetRollover(IEnumerable<Actor> rollover)
|
|
{
|
|
rolloverActors.Clear();
|
|
rolloverActors.AddRange(rollover);
|
|
}
|
|
|
|
public bool RolloverContains(Actor a)
|
|
{
|
|
return rolloverActors.Contains(a);
|
|
}
|
|
|
|
void ITick.Tick(Actor self)
|
|
{
|
|
var removed = actors.RemoveWhere(a => !a.IsInWorld || (!a.Owner.IsAlliedWith(world.RenderPlayer) && world.FogObscures(a)));
|
|
if (removed > 0)
|
|
{
|
|
UpdateHash();
|
|
Sync.RunUnsynced(world, () => world.OrderGenerator.SelectionChanged(world, actors));
|
|
foreach (var ns in worldNotifySelection)
|
|
ns.SelectionChanged();
|
|
}
|
|
}
|
|
|
|
List<MiniYamlNode> IGameSaveTraitData.IssueTraitData(Actor self)
|
|
{
|
|
return new List<MiniYamlNode>()
|
|
{
|
|
new MiniYamlNode("Selection", FieldSaver.FormatValue(Actors.Select(a => a.ActorID).ToArray()))
|
|
};
|
|
}
|
|
|
|
void IGameSaveTraitData.ResolveTraitData(Actor self, MiniYaml data)
|
|
{
|
|
var selectionNode = data.NodeWithKeyOrDefault("Selection");
|
|
if (selectionNode != null)
|
|
{
|
|
var selected = FieldLoader.GetValue<uint[]>("Selection", selectionNode.Value.Value)
|
|
.Select(a => self.World.GetActorById(a)).Where(a => a != null);
|
|
Combine(self.World, selected, false, false);
|
|
}
|
|
}
|
|
}
|
|
}
|