diff --git a/OpenRA.Game/OpenRA.Game.csproj b/OpenRA.Game/OpenRA.Game.csproj
index 024f9c9287..93fa12e8f2 100644
--- a/OpenRA.Game/OpenRA.Game.csproj
+++ b/OpenRA.Game/OpenRA.Game.csproj
@@ -153,6 +153,7 @@
+
diff --git a/OpenRA.Game/Primitives/PlayerDictionary.cs b/OpenRA.Game/Primitives/PlayerDictionary.cs
new file mode 100644
index 0000000000..2d2d317aa9
--- /dev/null
+++ b/OpenRA.Game/Primitives/PlayerDictionary.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+
+namespace OpenRA.Primitives
+{
+ ///
+ /// Provides a mapping of players to values, as well as fast lookup by player index.
+ ///
+ public struct PlayerDictionary : IReadOnlyList, IReadOnlyDictionary where T : class
+ {
+ readonly T[] valueByPlayerIndex;
+ readonly Dictionary valueByPlayer;
+
+ public PlayerDictionary(World world, Func valueFactory)
+ {
+ var players = world.Players;
+ if (players.Length == 0)
+ throw new InvalidOperationException("The players in the world have not yet been set.");
+
+ // The world players never change, so we can safely maintain an array of values.
+ // We need to enforce T is a class, so if it changes that change is available in both collections.
+ valueByPlayerIndex = new T[players.Length];
+ valueByPlayer = new Dictionary(players.Length);
+ for (var i = 0; i < players.Length; i++)
+ {
+ var player = players[i];
+ var state = valueFactory(player);
+ valueByPlayerIndex[i] = state;
+ valueByPlayer[player] = state;
+ }
+ }
+
+ /// Gets the value for the specified player. This is slower than specifying a player index.
+ public T this[Player player] { get { return valueByPlayer[player]; } }
+
+ /// Gets the value for the specified player index.
+ public T this[int playerIndex] { get { return valueByPlayerIndex[playerIndex]; } }
+
+ public int Count { get { return valueByPlayerIndex.Length; } }
+ public IEnumerator GetEnumerator() { return ((IEnumerable)valueByPlayerIndex).GetEnumerator(); }
+
+ ICollection IReadOnlyDictionary.Keys { get { return valueByPlayer.Keys; } }
+ ICollection IReadOnlyDictionary.Values { get { return valueByPlayer.Values; } }
+ bool IReadOnlyDictionary.ContainsKey(Player key) { return valueByPlayer.ContainsKey(key); }
+ bool IReadOnlyDictionary.TryGetValue(Player key, out T value) { return valueByPlayer.TryGetValue(key, out value); }
+ IEnumerator> IEnumerable>.GetEnumerator() { return valueByPlayer.GetEnumerator(); }
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); }
+ }
+}
diff --git a/OpenRA.Game/World.cs b/OpenRA.Game/World.cs
index 4cacd3392d..708e253c40 100644
--- a/OpenRA.Game/World.cs
+++ b/OpenRA.Game/World.cs
@@ -42,6 +42,8 @@ namespace OpenRA
public void SetPlayers(IEnumerable players, Player localPlayer)
{
+ if (Players.Length > 0)
+ throw new InvalidOperationException("Players are fixed once they have been set.");
Players = players.ToArray();
SetLocalPlayer(localPlayer);
}
diff --git a/OpenRA.Mods.Common/Traits/Modifiers/FrozenUnderFog.cs b/OpenRA.Mods.Common/Traits/Modifiers/FrozenUnderFog.cs
index 1ef4d701d6..830bb60c6d 100644
--- a/OpenRA.Mods.Common/Traits/Modifiers/FrozenUnderFog.cs
+++ b/OpenRA.Mods.Common/Traits/Modifiers/FrozenUnderFog.cs
@@ -12,6 +12,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using OpenRA.Graphics;
+using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits
@@ -35,9 +36,7 @@ namespace OpenRA.Mods.Common.Traits
readonly bool startsRevealed;
readonly PPos[] footprint;
- readonly Dictionary stateByPlayer = new Dictionary();
-
- FrozenState[] stateByPlayerIndex;
+ PlayerDictionary frozenStates;
ITooltip tooltip;
Health health;
bool initialized;
@@ -71,7 +70,7 @@ namespace OpenRA.Mods.Common.Traits
if (!byPlayer.Shroud.FogEnabled)
return byPlayer.Shroud.AnyExplored(self.OccupiesSpace.OccupiedCells());
- return initialized && stateByPlayer[byPlayer].IsVisible;
+ return initialized && frozenStates[byPlayer].IsVisible;
}
public bool IsVisible(Actor self, Player byPlayer)
@@ -89,41 +88,36 @@ namespace OpenRA.Mods.Common.Traits
return;
VisibilityHash = 0;
- var players = self.World.Players;
if (!initialized)
{
- // The world players never change, so we can safely index this collection.
- stateByPlayerIndex = new FrozenState[players.Length];
+ frozenStates = new PlayerDictionary(self.World, player =>
+ {
+ var frozenActor = new FrozenActor(self, footprint, player.Shroud, startsRevealed);
+ player.PlayerActor.Trait().Add(frozenActor);
+ return new FrozenState(frozenActor) { IsVisible = startsRevealed };
+ });
tooltip = self.TraitsImplementing().FirstOrDefault();
health = self.TraitOrDefault();
}
- for (var i = 0; i < players.Length; i++)
+ for (var playerIndex = 0; playerIndex < frozenStates.Count; playerIndex++)
{
- FrozenActor frozenActor;
+ var state = frozenStates[playerIndex];
+ var frozenActor = state.FrozenActor;
bool isVisible;
if (!initialized)
{
- var player = players[i];
- frozenActor = new FrozenActor(self, footprint, player.Shroud, startsRevealed);
- isVisible = startsRevealed;
- var state = new FrozenState(frozenActor) { IsVisible = isVisible };
- stateByPlayer.Add(player, state);
- stateByPlayerIndex[i] = state;
- player.PlayerActor.Trait().Add(frozenActor);
+ isVisible = state.IsVisible;
}
else
{
- // PERF: Minimize lookup cost by combining all state into one, and using an array rather than a dictionary.
- var state = stateByPlayerIndex[i];
- frozenActor = state.FrozenActor;
isVisible = !frozenActor.Visible;
state.IsVisible = isVisible;
}
if (isVisible)
- VisibilityHash |= 1 << (i % 32);
+ VisibilityHash |= 1 << (playerIndex % 32);
else
continue;
@@ -151,9 +145,9 @@ namespace OpenRA.Mods.Common.Traits
return;
IRenderable[] renderables = null;
- foreach (var player in self.World.Players)
+ for (var playerIndex = 0; playerIndex < frozenStates.Count; playerIndex++)
{
- var frozen = stateByPlayer[player].FrozenActor;
+ var frozen = frozenStates[playerIndex].FrozenActor;
if (!frozen.NeedRenderables)
continue;
diff --git a/OpenRA.Mods.RA/Effects/GpsDot.cs b/OpenRA.Mods.RA/Effects/GpsDot.cs
index 1ff8497ef9..562d68380e 100644
--- a/OpenRA.Mods.RA/Effects/GpsDot.cs
+++ b/OpenRA.Mods.RA/Effects/GpsDot.cs
@@ -37,7 +37,7 @@ namespace OpenRA.Mods.RA.Effects
readonly GpsDotInfo info;
readonly Animation anim;
- readonly Dictionary stateByPlayer = new Dictionary();
+ readonly PlayerDictionary dotStates;
readonly Lazy huf;
readonly Lazy fuf;
readonly Lazy disguise;
@@ -70,13 +70,12 @@ namespace OpenRA.Mods.RA.Effects
cloak = Exts.Lazy(() => self.TraitOrDefault());
frozen = new Cache(p => p.PlayerActor.Trait());
-
- stateByPlayer = self.World.Players.ToDictionary(p => p, p => new DotState(p.PlayerActor.Trait()));
+ dotStates = new PlayerDictionary(self.World, player => new DotState(player.PlayerActor.Trait()));
}
public bool IsDotVisible(Player toPlayer)
{
- return stateByPlayer[toPlayer].IsTargetable;
+ return dotStates[toPlayer].IsTargetable;
}
bool IsTargetableBy(Player toPlayer, out bool shouldRenderIndicator)
@@ -122,11 +121,11 @@ namespace OpenRA.Mods.RA.Effects
if (!self.IsInWorld || self.IsDead)
return;
- foreach (var player in self.World.Players)
+ for (var playerIndex = 0; playerIndex < dotStates.Count; playerIndex++)
{
- var state = stateByPlayer[player];
+ var state = dotStates[playerIndex];
var shouldRender = false;
- var targetable = (state.Gps.Granted || state.Gps.GrantedAllies) && IsTargetableBy(player, out shouldRender);
+ var targetable = (state.Gps.Granted || state.Gps.GrantedAllies) && IsTargetableBy(world.Players[playerIndex], out shouldRender);
state.IsTargetable = targetable;
state.ShouldRender = targetable && shouldRender;
}
@@ -134,7 +133,7 @@ namespace OpenRA.Mods.RA.Effects
public IEnumerable Render(WorldRenderer wr)
{
- if (self.World.RenderPlayer == null || !stateByPlayer[self.World.RenderPlayer].ShouldRender || self.Disposed)
+ if (self.World.RenderPlayer == null || !dotStates[self.World.RenderPlayer].ShouldRender || self.Disposed)
return SpriteRenderable.None;
var palette = wr.Palette(info.IndicatorPalettePrefix + self.Owner.InternalName);