Index: binaries/data/mods/public/maps/scenarios/los_perf_test.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/maps/scenarios/los_perf_test.xml
@@ -0,0 +1,94 @@
+
+
+
+
+
+ default
+
+
+
+
+
+ 0
+ 0.5
+
+
+
+
+ ocean
+
+
+ 5
+ 4
+ 0.45
+ 0
+
+
+
+ 0
+ 1
+ 0.99
+ 0.1999
+ default
+
+
+
+
+
+
+
+
+
+
+
+
Index: binaries/data/mods/public/maps/scenarios/los_perf_test_triggers.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/maps/scenarios/los_perf_test_triggers.js
@@ -0,0 +1,71 @@
+const UNIT_TEMPLATE = "units/athen/support_female_citizen";
+
+var QuickSpawn = function(x, z, template, owner = 1)
+{
+ let ent = Engine.AddEntity(template);
+
+ let cmpEntOwnership = Engine.QueryInterface(ent, IID_Ownership);
+ if (cmpEntOwnership)
+ cmpEntOwnership.SetOwner(owner);
+
+ let cmpEntPosition = Engine.QueryInterface(ent, IID_Position);
+ cmpEntPosition.JumpTo(x, z);
+ return ent;
+};
+
+var WalkTo = function(x, z, ent, owner = 1)
+{
+ ProcessCommand(owner, {
+ "type": "walk",
+ "entities": Array.isArray(ent) ? ent : [ent],
+ "x": x,
+ "z": z,
+ "queued": true
+ });
+ return ent;
+};
+
+var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
+
+var entities = [];
+
+Trigger.prototype.SetupUnitsEverywhere = function()
+{
+ for (let x = 0; x < 20*16*4; x += 50)
+ for (let z = 0; z < 20*16*4; z += 50)
+ for (let owner = 1; owner <= 8; owner++)
+ entities.push(QuickSpawn(x+owner, z, UNIT_TEMPLATE, owner));
+};
+
+
+Trigger.prototype.SetupUnitsSomeAndMove = function()
+{
+ for (let x = 0; x < 20*16*4; x += 50*2)
+ for (let z = 0; z < 20*16*4; z += 50*2)
+ for (let owner = 1; owner <= 8; owner++)
+ {
+ let ent = QuickSpawn(x+owner, z, UNIT_TEMPLATE, owner);
+ WalkTo(x + 50 + owner, z+owner, ent, owner);
+ WalkTo(x + 0 + owner, z+owner, ent, owner);
+ WalkTo(x + 50 + owner, z+owner, ent, owner);
+ WalkTo(x + 0 + owner, z+owner, ent, owner);
+ WalkTo(x + 50 + owner, z+owner, ent, owner);
+ WalkTo(x + 0 + owner, z+owner, ent, owner);
+ WalkTo(x + 50 + owner, z+owner, ent, owner);
+ WalkTo(x + 0 + owner, z+owner, ent, owner);
+ WalkTo(x + 50 + owner, z+owner, ent, owner);
+ entities.push(ent);
+ }
+};
+
+Trigger.prototype.KillAll = function()
+{
+ for (let ent of entities)
+ Engine.DestroyEntity(ent);
+ entities = [];
+};
+
+cmpTrigger.DoAfterDelay(400, "SetupUnitsEverywhere", {});
+cmpTrigger.DoAfterDelay(800, "KillAll", {});
+cmpTrigger.DoAfterDelay(1000, "SetupUnitsSomeAndMove", {});
+cmpTrigger.DoAfterDelay(50000, "KillAll", {});
Index: source/simulation2/components/CCmpRangeManager.cpp
===================================================================
--- source/simulation2/components/CCmpRangeManager.cpp
+++ source/simulation2/components/CCmpRangeManager.cpp
@@ -1,4 +1,4 @@
-/* Copyright (C) 2022 Wildfire Games.
+/* Copyright (C) 2023 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -32,6 +32,7 @@
#include "simulation2/components/ICmpVisibility.h"
#include "simulation2/components/ICmpVision.h"
#include "simulation2/components/ICmpWaterManager.h"
+#include "simulation2/helpers/PackedArray.h"
#include "simulation2/helpers/Los.h"
#include "simulation2/helpers/MapEdgeTiles.h"
#include "simulation2/helpers/Render.h"
@@ -46,8 +47,40 @@
#define DEBUG_RANGE_MANAGER_BOUNDS 0
+namespace std
+{
+ template<> class numeric_limits { public: static constexpr int digits = 2; };
+}
+
namespace
{
+/**
+ * Use this type to store something for each player, including GAIA player(s).
+ */
+template
+using PerPlayer = PackedArray;
+
+/**
+ * Use this type to store something for each player, including INVALID_PLAYER (at -1) and GAIA player(s).
+ */
+template
+using PerPlayerAndInvalid = PackedArray;
+
+/**
+ * 'Declaration of intent' typedef for a valid, non-GAIA player (range 1..MAX_NB_OF_PLAYERS).
+ */
+using nongaia_player_id_t = std::make_unsigned_t;
+
+constexpr std::array GetNonGaiaPlayers()
+{
+ std::array ret{};
+ for (nongaia_player_id_t i = 0; i < MAX_NB_OF_PLAYERS - 1; ++i)
+ ret[i] = i + 1;
+ return ret;
+}
+
+constexpr std::array nongaia_players = GetNonGaiaPlayers();
+
/**
* How many LOS vertices to have per region.
* LOS regions are used to keep track of units.
@@ -60,95 +93,6 @@
*/
const fixed PARABOLIC_RANGE_TOLERANCE = fixed::FromInt(1)/2;
-/**
- * Convert an owner ID (-1 = unowned, 0 = gaia, 1..30 = players)
- * into a 32-bit mask for quick set-membership tests.
- */
-u32 CalcOwnerMask(player_id_t owner)
-{
- if (owner >= -1 && owner < 31)
- return 1 << (1+owner);
- else
- return 0; // owner was invalid
-}
-
-/**
- * Returns LOS mask for given player.
- */
-u32 CalcPlayerLosMask(player_id_t player)
-{
- if (player > 0 && player <= 16)
- return (u32)LosState::MASK << (2*(player-1));
- return 0;
-}
-
-/**
- * Returns shared LOS mask for given list of players.
- */
-u32 CalcSharedLosMask(std::vector players)
-{
- u32 playerMask = 0;
- for (size_t i = 0; i < players.size(); i++)
- playerMask |= CalcPlayerLosMask(players[i]);
-
- return playerMask;
-}
-
-/**
- * Add/remove a player to/from mask, which is a 1-bit mask representing a list of players.
- * Returns true if the mask is modified.
- */
-bool SetPlayerSharedDirtyVisibilityBit(u16& mask, player_id_t player, bool enable)
-{
- if (player <= 0 || player > 16)
- return false;
-
- u16 oldMask = mask;
-
- if (enable)
- mask |= (0x1 << (player - 1));
- else
- mask &= ~(0x1 << (player - 1));
-
- return oldMask != mask;
-}
-
-/**
- * Computes the 2-bit visibility for one player, given the total 32-bit visibilities
- */
-LosVisibility GetPlayerVisibility(u32 visibilities, player_id_t player)
-{
- if (player > 0 && player <= 16)
- return static_cast( (visibilities >> (2 *(player-1))) & 0x3 );
- return LosVisibility::HIDDEN;
-}
-
-/**
- * Test whether the visibility is dirty for a given LoS region and a given player
- */
-bool IsVisibilityDirty(u16 dirty, player_id_t player)
-{
- if (player > 0 && player <= 16)
- return (dirty >> (player - 1)) & 0x1;
- return false;
-}
-
-/**
- * Test whether a player share this vision
- */
-bool HasVisionSharing(u16 visionSharing, player_id_t player)
-{
- return (visionSharing & (1 << (player - 1))) != 0;
-}
-
-/**
- * Computes the shared vision mask for the player
- */
-u16 CalcVisionSharingMask(player_id_t player)
-{
- return 1 << (player-1);
-}
-
/**
* Representation of a range query.
*/
@@ -159,7 +103,7 @@
entity_pos_t minRange;
entity_pos_t maxRange;
entity_pos_t yOrigin; // Used for parabolas only.
- u32 ownersMask;
+ PerPlayerAndInvalid ownersMask; // Flag used for 'no owner', i.e. owner == -1.
i32 interface;
u8 flagsMask;
bool enabled;
@@ -225,14 +169,14 @@
struct EntityData
{
EntityData() :
- visibilities(0), size(0), visionSharing(0),
+ visibilities(LosVisibility::HIDDEN), size(0), visionSharing(0),
owner(-1), flags(FlagMasks::Normal)
{ }
entity_pos_t x, z;
entity_pos_t visionRange;
- u32 visibilities; // 2-bit visibility, per player
+ PerPlayer visibilities;
u32 size;
- u16 visionSharing; // 1-bit per player
+ PerPlayer visionSharing;
i8 owner;
u8 flags; // See the FlagMasks enum
@@ -291,7 +235,7 @@
serialize.NumberFixed_Unbounded("min range", value.minRange);
serialize.NumberFixed_Unbounded("max range", value.maxRange);
serialize.NumberFixed_Unbounded("yOrigin", value.yOrigin);
- serialize.NumberU32_Unbounded("owners mask", value.ownersMask);
+ Serializer(serialize, "owners mask", value.ownersMask);
serialize.NumberI32_Unbounded("interface", value.interface);
Serializer(serialize, "last match", value.lastMatch);
serialize.NumberU8_Unbounded("flagsMask", value.flagsMask);
@@ -332,9 +276,9 @@
serialize.NumberFixed_Unbounded("x", value.x);
serialize.NumberFixed_Unbounded("z", value.z);
serialize.NumberFixed_Unbounded("vision", value.visionRange);
- serialize.NumberU32_Unbounded("visibilities", value.visibilities);
+ Serializer(serialize, "visibilities", value.visibilities);
serialize.NumberU32_Unbounded("size", value.size);
- serialize.NumberU16_Unbounded("vision sharing", value.visionSharing);
+ Serializer(serialize, "vision sharing", value.visionSharing);
serialize.NumberI8_Unbounded("owner", value.owner);
serialize.NumberU8_Unbounded("flags", value.flags);
}
@@ -391,19 +335,18 @@
std::vector m_SubdivisionResults;
// LOS state:
- static const player_id_t MAX_LOS_PLAYER_ID = 16;
-
using LosRegion = std::pair;
- std::array m_LosRevealAll;
+ // One per player + one for 'revealed map'
+ std::array m_LosRevealAll;
bool m_LosCircular;
i32 m_LosVerticesPerSide;
// Cache for visibility tracking
i32 m_LosRegionsPerSide;
bool m_GlobalVisibilityUpdate;
- std::array m_GlobalPlayerVisibilityUpdate;
- Grid m_DirtyVisibility;
+ std::array m_GlobalPlayerVisibilityUpdate;
+ Grid> m_DirtyVisibility;
Grid> m_LosRegions;
// List of entities that must be updated, regardless of the status of their tile
std::vector m_ModifiedEntities;
@@ -413,19 +356,19 @@
// of units in a very small area.
// (Note we use vertexes, not tiles, to better match the renderer.)
// Lazily constructed when it's needed, to save memory in smaller games.
- std::array, MAX_LOS_PLAYER_ID> m_LosPlayerCounts;
+ std::array, MAX_NB_OF_PLAYERS> m_LosPlayerCounts;
// 2-bit LosState per player, starting with player 1 (not 0!) up to player MAX_LOS_PLAYER_ID (inclusive)
- Grid m_LosState;
+ Grid m_LosState;
// Special static visibility data for the "reveal whole map" mode
// (TODO: this is usually a waste of memory)
- Grid m_LosStateRevealed;
+ Grid m_LosStateRevealed;
- // Shared LOS masks, one per player.
- std::array m_SharedLosMasks;
- // Shared dirty visibility masks, one per player.
- std::array m_SharedDirtyVisibilityMasks;
+ // Shared LOS masks, one per player (GAIA doesn't have one, but we keep it for easier indexing).
+ std::array m_SharedLosMasks;
+ // Shared dirty visibility masks, one per player (GAIA doesn't have one, but we keep it for easier indexing).
+ std::array, MAX_NB_OF_PLAYERS> m_SharedDirtyVisibilityMasks;
// Cache explored vertices per player (not serialized)
u32 m_TotalInworldVertices;
@@ -737,14 +680,14 @@
if (it == m_EntityData.end())
break;
- ENSURE(msgData.player > 0 && msgData.player < MAX_LOS_PLAYER_ID+1);
- u16 visionChanged = CalcVisionSharingMask(msgData.player);
+ ENSURE(msgData.player > 0 && msgData.player <= LAST_PLAYER_ID);
+ nongaia_player_id_t player = msgData.player;
if (!it->second.HasFlag())
{
// Activation of the Vision Sharing
- ENSURE(it->second.owner == (i8)msgData.player);
- it->second.visionSharing = visionChanged;
+ ENSURE(nongaia_player_id_t(it->second.owner) == player);
+ it->second.visionSharing[player] = true;
it->second.SetFlag(true);
break;
}
@@ -754,15 +697,12 @@
entity_pos_t range = it->second.visionRange;
CFixedVector2D pos(it->second.x, it->second.z);
if (msgData.add)
- LosAdd(msgData.player, range, pos);
+ LosAdd(player, range, pos);
else
- LosRemove(msgData.player, range, pos);
+ LosRemove(player, range, pos);
}
-
- if (msgData.add)
- it->second.visionSharing |= visionChanged;
- else
- it->second.visionSharing &= ~visionChanged;
+ // Add / remove the player from the mask.
+ it->second.visionSharing[player] = msgData.add;
break;
}
case MT_Update:
@@ -803,8 +743,8 @@
// Check that calling ResetDerivedData (i.e. recomputing all the state from scratch)
// does not affect the incrementally-computed state
- std::array, MAX_LOS_PLAYER_ID> oldPlayerCounts = m_LosPlayerCounts;
- Grid oldStateRevealed = m_LosStateRevealed;
+ std::array, MAX_NB_OF_PLAYERS> oldPlayerCounts = m_LosPlayerCounts;
+ Grid oldStateRevealed = m_LosStateRevealed;
FastSpatialSubdivision oldSubdivision = m_Subdivision;
Grid > oldLosRegions = m_LosRegions;
@@ -861,7 +801,7 @@
m_LosPlayerCounts[player_id].clear();
m_ExploredVertices.clear();
- m_ExploredVertices.resize(MAX_LOS_PLAYER_ID+1, 0);
+ m_ExploredVertices.resize(MAX_NB_OF_PLAYERS, 0);
if (m_Deserializing)
{
@@ -869,8 +809,8 @@
for (i32 j = 0; j < m_LosVerticesPerSide; j++)
for (i32 i = 0; i < m_LosVerticesPerSide; i++)
if (!LosIsOffWorld(i, j))
- for (u8 k = 1; k < MAX_LOS_PLAYER_ID+1; ++k)
- m_ExploredVertices.at(k) += ((m_LosState.get(i, j) & ((u32)LosState::EXPLORED << (2*(k-1)))) > 0);
+ for (nongaia_player_id_t k : nongaia_players)
+ m_ExploredVertices.at(k) += m_LosState.get(i, j)[k - 1] && LosState::EXPLORED;
} else
m_LosState.resize(m_LosVerticesPerSide, m_LosVerticesPerSide);
@@ -903,10 +843,10 @@
for (i32 i = 0; i < m_LosVerticesPerSide; ++i)
{
if (LosIsOffWorld(i,j))
- m_LosStateRevealed.get(i, j) = 0;
+ m_LosStateRevealed.get(i, j) = LosStatePerNonGaiaPlayer(LosState::UNEXPLORED);
else
{
- m_LosStateRevealed.get(i, j) = 0xFFFFFFFFu;
+ m_LosStateRevealed.get(i, j) = LosStatePerNonGaiaPlayer(LosState::VISIBLE_AND_EXPLORED);
m_TotalInworldVertices++;
}
}
@@ -1068,27 +1008,38 @@
std::vector GetEntitiesByPlayer(player_id_t player) const override
{
- return GetEntitiesByMask(CalcOwnerMask(player));
+ std::vector entities;
+
+ for (EntityMap::const_iterator it = m_EntityData.begin(); it != m_EntityData.end(); ++it)
+ if (it->second.owner == player)
+ entities.push_back(it->first);
+
+ return entities;
}
std::vector GetNonGaiaEntities() const override
{
- return GetEntitiesByMask(~3u); // bit 0 for owner=-1 and bit 1 for gaia
+ PerPlayerAndInvalid mask(true);
+ mask[-1] = false;
+ mask[0] = false;
+ return GetEntitiesByMask(mask);
}
std::vector GetGaiaAndNonGaiaEntities() const override
{
- return GetEntitiesByMask(~1u); // bit 0 for owner=-1
+ PerPlayerAndInvalid mask(true);
+ mask[-1] = false;
+ return GetEntitiesByMask(mask);
}
- std::vector GetEntitiesByMask(u32 ownerMask) const
+ std::vector GetEntitiesByMask(PerPlayerAndInvalid mask) const
{
std::vector entities;
for (EntityMap::const_iterator it = m_EntityData.begin(); it != m_EntityData.end(); ++it)
{
// Check owner and add to list if it matches
- if (CalcOwnerMask(it->second.owner) & ownerMask)
+ if (mask[it->second.owner])
entities.push_back(it->first);
}
@@ -1167,7 +1118,7 @@
bool TestEntityQuery(const Query& q, entity_id_t id, const EntityData& entity) const
{
// Quick filter to ignore entities with the wrong owner
- if (!(CalcOwnerMask(entity.owner) & q.ownersMask))
+ if (!q.ownersMask[entity.owner])
return false;
// Ignore entities not present in the world
@@ -1445,13 +1396,13 @@
q.maxRange += fixed::FromInt(size);
}
- q.ownersMask = 0;
- for (size_t i = 0; i < owners.size(); ++i)
- q.ownersMask |= CalcOwnerMask(owners[i]);
-
- if (q.ownersMask == 0)
+ if (owners.empty())
LOGWARNING("CCmpRangeManager: No owners in query for entity %u", source);
+ q.ownersMask = PerPlayerAndInvalid(false);
+ for (int player : owners)
+ q.ownersMask[player] = true;
+
q.interface = requiredInterface;
q.flagsMask = flagsMask;
@@ -1638,7 +1589,7 @@
CLosQuerier GetLosQuerier(player_id_t player) const override
{
if (GetLosRevealAll(player))
- return CLosQuerier(0xFFFFFFFFu, m_LosStateRevealed, m_LosVerticesPerSide);
+ return CLosQuerier(LosStatePerNonGaiaPlayer(LosState::VISIBLE_AND_EXPLORED), m_LosStateRevealed, m_LosVerticesPerSide);
else
return CLosQuerier(GetSharedLosMask(player), m_LosState, m_LosVerticesPerSide);
}
@@ -1771,7 +1722,8 @@
CFixedVector2D pos = cmpPosition->GetPosition2D();
- if (IsVisibilityDirty(m_DirtyVisibility[PosToLosRegionsHelper(pos.X, pos.Y)], player))
+ nongaia_player_id_t validPlayer = player;
+ if (m_DirtyVisibility[PosToLosRegionsHelper(pos.X, pos.Y)][validPlayer])
return ComputeLosVisibility(ent, player);
if (std::find(m_ModifiedEntities.begin(), m_ModifiedEntities.end(), entId) != m_ModifiedEntities.end())
@@ -1781,7 +1733,7 @@
if (it == m_EntityData.end())
return ComputeLosVisibility(ent, player);
- return static_cast(GetPlayerVisibility(it->second.visibilities, player));
+ return it->second.visibilities[validPlayer];
}
LosVisibility GetLosVisibility(entity_id_t ent, player_id_t player) const override
@@ -1860,12 +1812,12 @@
for (u16 j = 0; j < m_LosRegionsPerSide; ++j)
{
LosRegion pos{i, j};
- for (player_id_t player = 1; player < MAX_LOS_PLAYER_ID + 1; ++player)
- if (IsVisibilityDirty(m_DirtyVisibility[pos], player) || m_GlobalPlayerVisibilityUpdate[player-1] == 1 || m_GlobalVisibilityUpdate)
+ for (nongaia_player_id_t player : nongaia_players)
+ if (m_DirtyVisibility[pos][player] || m_GlobalPlayerVisibilityUpdate[player] || m_GlobalVisibilityUpdate)
for (const entity_id_t& ent : m_LosRegions[pos])
UpdateVisibility(ent, player);
- m_DirtyVisibility[pos] = 0;
+ m_DirtyVisibility[pos] = PerPlayer(false);
}
std::fill(m_GlobalPlayerVisibilityUpdate.begin(), m_GlobalPlayerVisibilityUpdate.end(), false);
@@ -1892,19 +1844,19 @@
m_ModifiedEntities.push_back(ent);
}
- void UpdateVisibility(entity_id_t ent, player_id_t player)
+ void UpdateVisibility(entity_id_t ent, nongaia_player_id_t player)
{
EntityMap::iterator itEnts = m_EntityData.find(ent);
if (itEnts == m_EntityData.end())
return;
- LosVisibility oldVis = GetPlayerVisibility(itEnts->second.visibilities, player);
+ LosVisibility oldVis = itEnts->second.visibilities[player];
LosVisibility newVis = ComputeLosVisibility(itEnts->first, player);
if (oldVis == newVis)
return;
- itEnts->second.visibilities = (itEnts->second.visibilities & ~(0x3 << 2 * (player - 1))) | ((u8)newVis << 2 * (player - 1));
+ itEnts->second.visibilities[player] = newVis;
CMessageVisibilityChanged msg(player, ent, static_cast(oldVis), static_cast(newVis));
GetSimContext().GetComponentManager().PostMessage(ent, msg);
@@ -1912,17 +1864,17 @@
void UpdateVisibility(entity_id_t ent)
{
- for (player_id_t player = 1; player < MAX_LOS_PLAYER_ID + 1; ++player)
+ for (nongaia_player_id_t player : nongaia_players)
UpdateVisibility(ent, player);
}
void SetLosRevealAll(player_id_t player, bool enabled) override
{
if (player == -1)
- m_LosRevealAll[MAX_LOS_PLAYER_ID+1] = enabled;
+ m_LosRevealAll[MAX_NB_OF_PLAYERS] = enabled;
else
{
- ENSURE(player >= 0 && player <= MAX_LOS_PLAYER_ID);
+ ENSURE(player >= 0 && player <= LAST_PLAYER_ID);
m_LosRevealAll[player] = enabled;
}
@@ -1933,9 +1885,9 @@
bool GetLosRevealAll(player_id_t player) const override
{
// Special player value can force reveal-all for every player
- if (m_LosRevealAll[MAX_LOS_PLAYER_ID+1] || player == -1)
+ if (m_LosRevealAll[MAX_NB_OF_PLAYERS] || player == -1)
return true;
- ENSURE(player >= 0 && player <= MAX_LOS_PLAYER_ID+1);
+ ENSURE(player >= 0 && player <= LAST_PLAYER_ID);
// Otherwise check the player-specific flag
if (m_LosRevealAll[player])
return true;
@@ -1957,7 +1909,23 @@
void SetSharedLos(player_id_t player, const std::vector& players) override
{
- m_SharedLosMasks[player] = CalcSharedLosMask(players);
+ if (player <= 0)
+ {
+ LOGERROR("Cannot set shared LOS for player %i (GAIA or invalid)", player);
+ return;
+ }
+
+ nongaia_player_id_t validPlayer = player;
+ m_SharedLosMasks[validPlayer] = LosStatePerNonGaiaPlayer(LosState::UNEXPLORED);
+ for (player_id_t p : players)
+ {
+ if (p <= 0)
+ {
+ LOGERROR("Cannot share LOS of player %i (GAIA or invalid)", p);
+ return;
+ }
+ m_SharedLosMasks[validPlayer][nongaia_player_id_t(p) - 1] = LosState::VISIBLE_AND_EXPLORED;
+ }
// Units belonging to any of 'players' can now trigger visibility updates for 'player'.
// If shared LOS partners have been removed, we disable visibility updates from them
@@ -1965,33 +1933,37 @@
// 'player' needs a global visibility update for this turn.
bool modified = false;
- for (player_id_t p = 1; p < MAX_LOS_PLAYER_ID+1; ++p)
+ for (nongaia_player_id_t p : nongaia_players)
{
bool inList = std::find(players.begin(), players.end(), p) != players.end();
-
- if (SetPlayerSharedDirtyVisibilityBit(m_SharedDirtyVisibilityMasks[p], player, inList))
+ auto&& mask = m_SharedDirtyVisibilityMasks[p][validPlayer];
+ if (mask != inList)
modified = true;
+ mask = inList;
}
- if (modified && (size_t)player <= m_GlobalPlayerVisibilityUpdate.size())
- m_GlobalPlayerVisibilityUpdate[player-1] = 1;
+ if (modified)
+ m_GlobalPlayerVisibilityUpdate[validPlayer] = true;
}
- u32 GetSharedLosMask(player_id_t player) const override
+ LosStatePerNonGaiaPlayer GetSharedLosMask(nongaia_player_id_t player) const
{
return m_SharedLosMasks[player];
}
void ExploreMap(player_id_t p) override
{
+ if (p <= 0 || p > LAST_PLAYER_ID)
+ return;
+ nongaia_player_id_t player = p;
for (i32 j = 0; j < m_LosVerticesPerSide; ++j)
for (i32 i = 0; i < m_LosVerticesPerSide; ++i)
{
if (LosIsOffWorld(i,j))
continue;
u32 &explored = m_ExploredVertices.at(p);
- explored += !(m_LosState.get(i, j) & ((u32)LosState::EXPLORED << (2*(p-1))));
- m_LosState.get(i, j) |= ((u32)LosState::EXPLORED << (2*(p-1)));
+ explored += !(m_LosState.get(i, j)[player - 1] && LosState::EXPLORED);
+ m_LosState.get(i, j)[player - 1] = m_LosState.get(i, j)[player - 1] | LosState::EXPLORED;
}
SeeExploredEntities(p);
@@ -2018,24 +1990,22 @@
{
// TODO: This fetches data redundantly if the los grid is smaller than the territory grid
// (but it's unlikely to matter much).
- u8 p = grid.get(scale(i, grid.width() - 1), scale(j, grid.height() - 1)) & ICmpTerritoryManager::TERRITORY_PLAYER_MASK;
- if (p > 0 && p <= MAX_LOS_PLAYER_ID)
+ player_id_t p = grid.get(scale(i, grid.width() - 1), scale(j, grid.height() - 1)) & ICmpTerritoryManager::TERRITORY_PLAYER_MASK;
+ if (p <= 0 || p > LAST_PLAYER_ID)
+ continue;
+ if (LosIsOffWorld(i, j))
+ continue;
+ nongaia_player_id_t player = p;
+ u32& explored = m_ExploredVertices.at(player);
+ LosStatePerNonGaiaPlayer& losState = m_LosState.get(i, j);
+ if (!(losState[player - 1] && LosState::EXPLORED))
{
- u32& explored = m_ExploredVertices.at(p);
-
- if (LosIsOffWorld(i, j))
- continue;
-
- u32& losState = m_LosState.get(i, j);
- if (!(losState & ((u32)LosState::EXPLORED << (2*(p-1)))))
- {
- ++explored;
- losState |= ((u32)LosState::EXPLORED << (2*(p-1)));
- }
+ ++explored;
+ losState[player - 1] = losState[player - 1] | LosState::EXPLORED;
}
}
- for (player_id_t p = 1; p < MAX_LOS_PLAYER_ID+1; ++p)
+ for (nongaia_player_id_t p : nongaia_players)
SeeExploredEntities(p);
}
@@ -2082,7 +2052,7 @@
void RevealShore(player_id_t p, bool enable) override
{
- if (p <= 0 || p > MAX_LOS_PLAYER_ID)
+ if (p <= 0 || p > LAST_PLAYER_ID)
return;
// Maximum distance to the shore
@@ -2142,7 +2112,7 @@
/**
* Update the LOS state of tiles within a given horizontal strip (i0,j) to (i1,j) (inclusive).
*/
- inline void LosAddStripHelper(u8 owner, i32 i0, i32 i1, i32 j, Grid& counts)
+ inline void LosAddStripHelper(nongaia_player_id_t owner, i32 i0, i32 i1, i32 j, Grid& counts)
{
if (i1 < i0)
return;
@@ -2155,8 +2125,8 @@
{
if (!LosIsOffWorld(i, j))
{
- explored += !(m_LosState.get(i, j) & ((u32)LosState::EXPLORED << (2*(owner-1))));
- m_LosState.get(i, j) |= (((int)LosState::VISIBLE | (u32)LosState::EXPLORED) << (2*(owner-1)));
+ explored += !(m_LosState.get(i, j)[owner - 1] && LosState::EXPLORED);
+ m_LosState.get(i, j)[owner - 1] = m_LosState.get(i, j)[owner - 1] | LosState::VISIBLE_AND_EXPLORED;
}
MarkVisibilityDirtyAroundTile(owner, i, j);
@@ -2170,7 +2140,7 @@
/**
* Update the LOS state of tiles within a given horizontal strip (i0,j) to (i1,j) (inclusive).
*/
- inline void LosRemoveStripHelper(u8 owner, i32 i0, i32 i1, i32 j, Grid& counts)
+ inline void LosRemoveStripHelper(nongaia_player_id_t owner, i32 i0, i32 i1, i32 j, Grid& counts)
{
if (i1 < i0)
return;
@@ -2184,14 +2154,14 @@
if (counts.get(i, j) == 0)
{
// (If LosIsOffWorld then this is a no-op, so don't bother doing the check)
- m_LosState.get(i, j) &= ~((int)LosState::VISIBLE << (2*(owner-1)));
+ m_LosState.get(i, j)[owner - 1] = LosState::EXPLORED;
MarkVisibilityDirtyAroundTile(owner, i, j);
}
}
}
- inline void MarkVisibilityDirtyAroundTile(u8 owner, i32 i, i32 j)
+ inline void MarkVisibilityDirtyAroundTile(nongaia_player_id_t owner, i32 i, i32 j)
{
// If we're still in the deserializing process, we must not modify m_DirtyVisibility
if (m_Deserializing)
@@ -2204,16 +2174,20 @@
LosRegion n3 = LosVertexToLosRegionsHelper(i, j-1);
LosRegion n4 = LosVertexToLosRegionsHelper(i, j);
- u16 sharedDirtyVisibilityMask = m_SharedDirtyVisibilityMasks[owner];
+ PerPlayer sharedDirtyVisibilityMask = m_SharedDirtyVisibilityMasks[owner];
if (j > 0 && i > 0)
- m_DirtyVisibility[n1] |= sharedDirtyVisibilityMask;
+ for (nongaia_player_id_t player : nongaia_players)
+ m_DirtyVisibility[n1][player] |= sharedDirtyVisibilityMask[player];
if (n2 != n1 && j > 0 && i < m_LosVerticesPerSide)
- m_DirtyVisibility[n2] |= sharedDirtyVisibilityMask;
+ for (nongaia_player_id_t player : nongaia_players)
+ m_DirtyVisibility[n2][player] |= sharedDirtyVisibilityMask[player];
if (n3 != n1 && j < m_LosVerticesPerSide && i > 0)
- m_DirtyVisibility[n3] |= sharedDirtyVisibilityMask;
+ for (nongaia_player_id_t player : nongaia_players)
+ m_DirtyVisibility[n3][player] |= sharedDirtyVisibilityMask[player];
if (n4 != n1 && j < m_LosVerticesPerSide && i < m_LosVerticesPerSide)
- m_DirtyVisibility[n4] |= sharedDirtyVisibilityMask;
+ for (nongaia_player_id_t player : nongaia_players)
+ m_DirtyVisibility[n4][player] |= sharedDirtyVisibilityMask[player];
}
/**
@@ -2222,7 +2196,7 @@
* Assumes owner is in the valid range.
*/
template
- void LosUpdateHelper(u8 owner, entity_pos_t visionRange, CFixedVector2D pos)
+ void LosUpdateHelper(nongaia_player_id_t owner, entity_pos_t visionRange, CFixedVector2D pos)
{
if (m_LosVerticesPerSide == 0) // do nothing if not initialised yet
return;
@@ -2309,7 +2283,7 @@
* by removing visibility around the 'from' position
* and then adding visibility around the 'to' position.
*/
- void LosUpdateHelperIncremental(u8 owner, entity_pos_t visionRange, CFixedVector2D from, CFixedVector2D to)
+ void LosUpdateHelperIncremental(nongaia_player_id_t owner, entity_pos_t visionRange, CFixedVector2D from, CFixedVector2D to)
{
if (m_LosVerticesPerSide == 0) // do nothing if not initialised yet
return;
@@ -2434,75 +2408,76 @@
void LosAdd(player_id_t owner, entity_pos_t visionRange, CFixedVector2D pos)
{
- if (visionRange.IsZero() || owner <= 0 || owner > MAX_LOS_PLAYER_ID)
+ if (visionRange.IsZero() || owner <= 0 || owner > LAST_PLAYER_ID)
return;
- LosUpdateHelper((u8)owner, visionRange, pos);
+ LosUpdateHelper(nongaia_player_id_t(owner), visionRange, pos);
}
- void SharingLosAdd(u16 visionSharing, entity_pos_t visionRange, CFixedVector2D pos)
+ void SharingLosAdd(PerPlayer visionSharing, entity_pos_t visionRange, CFixedVector2D pos)
{
if (visionRange.IsZero())
return;
- for (player_id_t i = 1; i < MAX_LOS_PLAYER_ID+1; ++i)
- if (HasVisionSharing(visionSharing, i))
+ for (nongaia_player_id_t i : nongaia_players)
+ if (visionSharing[i])
LosAdd(i, visionRange, pos);
}
void LosRemove(player_id_t owner, entity_pos_t visionRange, CFixedVector2D pos)
{
- if (visionRange.IsZero() || owner <= 0 || owner > MAX_LOS_PLAYER_ID)
+ if (visionRange.IsZero() || owner <= 0 || owner > LAST_PLAYER_ID)
return;
- LosUpdateHelper((u8)owner, visionRange, pos);
+ LosUpdateHelper(nongaia_player_id_t(owner), visionRange, pos);
}
- void SharingLosRemove(u16 visionSharing, entity_pos_t visionRange, CFixedVector2D pos)
+ void SharingLosRemove(PerPlayer visionSharing, entity_pos_t visionRange, CFixedVector2D pos)
{
if (visionRange.IsZero())
return;
- for (player_id_t i = 1; i < MAX_LOS_PLAYER_ID+1; ++i)
- if (HasVisionSharing(visionSharing, i))
+ for (nongaia_player_id_t i : nongaia_players)
+ if (visionSharing[i])
LosRemove(i, visionRange, pos);
}
void LosMove(player_id_t owner, entity_pos_t visionRange, CFixedVector2D from, CFixedVector2D to)
{
- if (visionRange.IsZero() || owner <= 0 || owner > MAX_LOS_PLAYER_ID)
+ if (visionRange.IsZero() || owner <= 0 || owner > LAST_PLAYER_ID)
return;
if ((from - to).CompareLength(visionRange) > 0)
{
// If it's a very large move, then simply remove and add to the new position
- LosUpdateHelper((u8)owner, visionRange, from);
- LosUpdateHelper((u8)owner, visionRange, to);
+ LosUpdateHelper(nongaia_player_id_t(owner), visionRange, from);
+ LosUpdateHelper(nongaia_player_id_t(owner), visionRange, to);
}
else
// Otherwise use the version optimised for mostly-overlapping circles
- LosUpdateHelperIncremental((u8)owner, visionRange, from, to);
+ LosUpdateHelperIncremental(nongaia_player_id_t(owner), visionRange, from, to);
}
- void SharingLosMove(u16 visionSharing, entity_pos_t visionRange, CFixedVector2D from, CFixedVector2D to)
+ void SharingLosMove(PerPlayer visionSharing, entity_pos_t visionRange, CFixedVector2D from, CFixedVector2D to)
{
if (visionRange.IsZero())
return;
- for (player_id_t i = 1; i < MAX_LOS_PLAYER_ID+1; ++i)
- if (HasVisionSharing(visionSharing, i))
+ for (nongaia_player_id_t i : nongaia_players)
+ if (visionSharing[i])
LosMove(i, visionRange, from, to);
}
u8 GetPercentMapExplored(player_id_t player) const override
{
- return m_ExploredVertices.at((u8)player) * 100 / m_TotalInworldVertices;
+ if (player <= 0 || player > LAST_PLAYER_ID)
+ return 100;
+ return m_ExploredVertices.at(nongaia_player_id_t(player)) * 100 / m_TotalInworldVertices;
}
u8 GetUnionPercentMapExplored(const std::vector& players) const override
{
u32 exploredVertices = 0;
- std::vector::const_iterator playerIt;
for (i32 j = 0; j < m_LosVerticesPerSide; j++)
for (i32 i = 0; i < m_LosVerticesPerSide; i++)
@@ -2510,12 +2485,16 @@
if (LosIsOffWorld(i, j))
continue;
- for (playerIt = players.begin(); playerIt != players.end(); ++playerIt)
- if (m_LosState.get(i, j) & ((u32)LosState::EXPLORED << (2*((*playerIt)-1))))
+ for (player_id_t p : players)
+ {
+ if (p <= 0 || p > LAST_PLAYER_ID)
+ continue;
+ if (m_LosState.get(i, j)[nongaia_player_id_t(p) - 1] && LosState::EXPLORED)
{
exploredVertices += 1;
break;
}
+ }
}
return exploredVertices * 100 / m_TotalInworldVertices;
Index: source/simulation2/components/ICmpRangeManager.h
===================================================================
--- source/simulation2/components/ICmpRangeManager.h
+++ source/simulation2/components/ICmpRangeManager.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2022 Wildfire Games.
+/* Copyright (C) 2023 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -362,11 +362,6 @@
*/
virtual void SetSharedLos(player_id_t player, const std::vector& players) = 0;
- /**
- * Returns shared LOS mask for player.
- */
- virtual u32 GetSharedLosMask(player_id_t player) const = 0;
-
/**
* Get percent map explored statistics for specified player.
*/
Index: source/simulation2/helpers/Los.h
===================================================================
--- source/simulation2/helpers/Los.h
+++ source/simulation2/helpers/Los.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2020 Wildfire Games.
+/* Copyright (C) 2023 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -18,9 +18,11 @@
#ifndef INCLUDED_LOS
#define INCLUDED_LOS
-// It doesn't seem worth moving the implementation to c++ and early-declaring Grid
+// It doesn't seem worth moving the implementation to c++ and early-declaring these
// since files must include "Los.h" explicitly, and that's only done in .cpp files.
#include "Grid.h"
+#include "PackedArray.h"
+#include "Player.h"
/**
* Computing LOS data at a very high resolution is not necessary and quite slow.
@@ -29,14 +31,48 @@
*/
static constexpr i32 LOS_TILE_SIZE = 4;
+/**
+ * NB: this is a bit-flag class, so you can be any combinations of VISIBLE | EXPLORED.
+ */
enum class LosState : u8
{
UNEXPLORED = 0,
EXPLORED = 1,
VISIBLE = 2,
- MASK = 3
+ VISIBLE_AND_EXPLORED = 3
};
+// Invalid operations, fail at compile-time with an "ambiguous" error.
+bool operator==(const LosState&, const LosState&);
+bool operator!=(const LosState&, const LosState&);
+
+// Bit-wise comparison.
+constexpr inline bool operator&&(const LosState& a, const LosState& b)
+{
+ return static_cast(a) & static_cast(b);
+}
+
+constexpr inline LosState operator|(const LosState& a, const LosState& b)
+{
+ return static_cast(static_cast(a) | static_cast(b));
+}
+
+
+namespace std
+{
+ template<> class numeric_limits { public: static constexpr int digits = 2; };
+}
+
+/**
+ * LOS State is only stored for non-GAIA players.
+ */
+using LosStatePerNonGaiaPlayer = PackedArray;
+
+/**
+ * For clarity.
+ */
+using LosMask = LosStatePerNonGaiaPlayer;
+
/**
* Object providing efficient abstracted access to the LOS state.
* This depends on some implementation details of CCmpRangeManager.
@@ -49,7 +85,7 @@
friend class CCmpRangeManager;
friend class TestLOSTexture;
- CLosQuerier(u32 playerMask, const Grid& data, ssize_t verticesPerSide) :
+ CLosQuerier(LosMask playerMask, const Grid& data, ssize_t verticesPerSide) :
m_Data(data), m_PlayerMask(playerMask), m_VerticesPerSide(verticesPerSide)
{
}
@@ -66,7 +102,7 @@
return false;
// Check high bit of each bit-pair
- if ((m_Data.get(i, j) & m_PlayerMask) & 0xAAAAAAAAu)
+ if ((*m_Data.get(i, j).data() & *m_PlayerMask.data()) & *LosStatePerNonGaiaPlayer(LosState::VISIBLE).data())
return true;
else
return false;
@@ -81,7 +117,7 @@
return false;
// Check low bit of each bit-pair
- if ((m_Data.get(i, j) & m_PlayerMask) & 0x55555555u)
+ if ((*m_Data.get(i, j).data() & *m_PlayerMask.data()) & *LosStatePerNonGaiaPlayer(LosState::EXPLORED).data())
return true;
else
return false;
@@ -97,7 +133,7 @@
ENSURE(i >= 0 && j >= 0 && i < m_VerticesPerSide && j < m_VerticesPerSide);
#endif
// Check high bit of each bit-pair
- if ((m_Data.get(i, j) & m_PlayerMask) & 0xAAAAAAAAu)
+ if ((*m_Data.get(i, j).data() & *m_PlayerMask.data()) & *LosStatePerNonGaiaPlayer(LosState::VISIBLE).data())
return true;
else
return false;
@@ -113,15 +149,15 @@
ENSURE(i >= 0 && j >= 0 && i < m_VerticesPerSide && j < m_VerticesPerSide);
#endif
// Check low bit of each bit-pair
- if ((m_Data.get(i, j) & m_PlayerMask) & 0x55555555u)
+ if ((*m_Data.get(i, j).data() & *m_PlayerMask.data()) & *LosStatePerNonGaiaPlayer(LosState::EXPLORED).data())
return true;
else
return false;
}
private:
- u32 m_PlayerMask;
- const Grid& m_Data;
+ LosMask m_PlayerMask;
+ const Grid& m_Data;
ssize_t m_VerticesPerSide;
};
Index: source/simulation2/helpers/PackedArray.h
===================================================================
--- /dev/null
+++ source/simulation2/helpers/PackedArray.h
@@ -0,0 +1,207 @@
+/* Copyright (C) 2023 Wildfire Games.
+ * This file is part of 0 A.D.
+ *
+ * 0 A.D. is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 0 A.D. is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with 0 A.D. If not, see .
+ */
+
+#ifndef INCLUDED_PACKEDARRAY
+#define INCLUDED_PACKEDARRAY
+
+#include "simulation2/serialization/SerializeTemplates.h"
+
+#include
+
+/**
+ * This class keeps a packed representation of T for SIZE members (where N = std::numeric_limits<>::digits).
+ * Essentially std::vector but fixed-size and for bigger types.
+ * It can conveniently replace std::array for very small structs, such as booleans or small enums.
+ * Note that there is probably a low size (4? 5?) where a regular std::array performs better.
+ * @a MINUS_ONE_MEANS_LAST makes indexing at -1 index at SIZE-1 (the last value). Note that other values are not supported.
+ */
+template
+class PackedArray
+{
+ friend struct SerializeHelper>;
+ friend class EditableReference;
+ friend class TestPackedArray;
+
+public:
+
+ static constexpr u8 NumberOfBits = std::numeric_limits::digits;
+ static_assert(NumberOfBits > 0, "T has 0 digits - numeric_limits::digits may need to be specialised.");
+ static constexpr u16 TotalBits = NumberOfBits * SIZE;
+ static constexpr u16 Bytes = TotalBits / CHAR_BIT + ((TotalBits % CHAR_BIT) > 0);
+
+ static_assert(Bytes <= 8, "PackedArray not implemented above 8 bytes of data.");
+
+ // Determine storage type based on type.
+ using Storage =
+ std::conditional_t>>>;
+
+ static constexpr bool StorageIsScalar = std::is_arithmetic_v;
+protected:
+ static constexpr Storage GetMaskBits()
+ {
+ Storage ret = 0;
+ for (size_t i = 0; i < NumberOfBits; ++i)
+ ret |= 1 << i;
+ return ret;
+ }
+ static constexpr Storage MaskBits = GetMaskBits();
+public:
+
+ /**
+ * Proxy to provide a clean interface when using operator[] in a mutable fashion.
+ */
+ class EditableReference
+ {
+ friend class PackedArray;
+ public:
+ constexpr T operator*() const
+ {
+ return static_cast(*this);
+ }
+ constexpr void operator=(const T& o)
+ {
+ m_Array.Set(m_Offset, o);
+ }
+ constexpr void operator|=(const T& o)
+ {
+ m_Array.Set(m_Offset, static_cast(o | static_cast(*this)));
+ }
+ constexpr operator T() const
+ {
+ return static_cast(m_Array)[m_Offset];
+ }
+ constexpr bool operator==(const T& o) const
+ {
+ return static_cast(*this) == o;
+ }
+ constexpr bool operator!=(const T& o) const
+ {
+ return static_cast(*this) != o;
+ }
+
+ protected:
+ constexpr EditableReference(PackedArray& a, size_t o) : m_Array(a), m_Offset(o) {};
+ PackedArray& m_Array;
+ size_t m_Offset;
+ };
+
+ /**
+ * As a means of providing some compile-time refactoring safety, and since this class
+ * generally replaces unsigned integers, ensure that indexing signed numbers (e.g. -1) will trigger
+ * compile time failures unless explicitly handled.
+ */
+ class IndexingWrapper
+ {
+ public:
+ // Unsigned integers can be implicitly converted.
+ template, U> = 0>
+ constexpr IndexingWrapper(U o) : index(o) {}
+ // With MINUS_ONE_MEANS_LAST, -1 becomes the last index. Note that other negative values are not supported.
+ template, U> = 0>
+ constexpr IndexingWrapper(U o) {
+ if (o == -1)
+ index = SIZE - 1;
+ else
+ index = o;
+ }
+
+ constexpr operator unsigned int() { return index; }
+ protected:
+ static_assert(std::numeric_limits::max() > SIZE, "Only up to max(uint) items are supported");
+ unsigned int index;
+ };
+
+ constexpr PackedArray() : m_Storage(static_cast(T{})) {}
+ explicit constexpr PackedArray(T val) : m_Storage(static_cast(T{}))
+ {
+ SetEverywhere(static_cast(val));
+ }
+ constexpr PackedArray(const PackedArray&) = default;
+ constexpr PackedArray& operator=(const PackedArray&) = default;
+ constexpr PackedArray(PackedArray&&) = default;
+ constexpr PackedArray& operator=(PackedArray&&) = default;
+
+ constexpr T operator[](IndexingWrapper offset) const
+ {
+ return static_cast(Mask(m_Storage >> (offset * NumberOfBits)));
+ }
+
+ constexpr EditableReference operator[](IndexingWrapper offset)
+ {
+ return { *this, offset };
+ }
+
+ constexpr bool operator==(const PackedArray& o) const
+ {
+ return m_Storage == o.m_Storage;
+ }
+ /**
+ * Temporary stop-gap.
+ */
+ constexpr const Storage* data() const
+ {
+ return &m_Storage;
+ }
+
+protected:
+ constexpr Storage Mask(Storage value) const
+ {
+ return value & MaskBits;
+ }
+
+ constexpr void Set(size_t offset, T o)
+ {
+ Set(offset, static_cast(o));
+ }
+
+ constexpr void Set(size_t offset, Storage o)
+ {
+ // First clear whatever bits we had there
+ m_Storage &= ~(MaskBits << (offset * NumberOfBits));
+ // Then set whatever we have now.
+ m_Storage |= o << (offset * NumberOfBits);
+ }
+
+ constexpr void SetEverywhere(Storage o)
+ {
+ for (size_t i = 0; i < SIZE; ++i)
+ Set(i, o);
+ }
+
+ Storage m_Storage;
+};
+
+template
+struct SerializeHelper>
+{
+ void operator()(ISerializer& serialize, const char* UNUSED(name), const PackedArray& value)
+ {
+ Serializer(serialize, "value", value.m_Storage);
+ }
+
+ void operator()(IDeserializer& deserialize, const char* UNUSED(name), PackedArray& value)
+ {
+ Serializer(deserialize, "value", value.m_Storage);
+ }
+};
+
+#endif // INCLUDED_PACKEDARRAY
Index: source/simulation2/helpers/Player.h
===================================================================
--- source/simulation2/helpers/Player.h
+++ source/simulation2/helpers/Player.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2010 Wildfire Games.
+/* Copyright (C) 2023 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -23,6 +23,18 @@
*/
typedef int32_t player_id_t;
-static const player_id_t INVALID_PLAYER = -1;
+constexpr inline player_id_t INVALID_PLAYER = -1;
+
+/**
+ * Maximum # of players supported by the engine, including gaia players (but not the invalid player).
+ * This is hardcoded because specific data structures are used for performance that don't necessarily scale.
+ * Since some structures add an 'invalid player' or a 'global player', this should generally be one less than a power of two.
+ */
+constexpr inline player_id_t MAX_NB_OF_PLAYERS = 15;
+
+/**
+ * Provided for convenience.
+ */
+constexpr inline player_id_t LAST_PLAYER_ID = MAX_NB_OF_PLAYERS-1;
#endif // INCLUDED_PLAYER
Index: source/simulation2/tests/test_PackedArray.h
===================================================================
--- /dev/null
+++ source/simulation2/tests/test_PackedArray.h
@@ -0,0 +1,85 @@
+/* Copyright (C) 2023 Wildfire Games.
+ * This file is part of 0 A.D.
+ *
+ * 0 A.D. is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 0 A.D. is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with 0 A.D. If not, see .
+ */
+
+#include "lib/self_test.h"
+
+#include "simulation2/helpers/PackedArray.h"
+
+enum class SmallEnum
+{
+ ZERO = 0,
+ ONE = 1,
+ TWO = 2
+};
+
+namespace std
+{
+ template<> class numeric_limits { public: static constexpr int digits = 2; };
+}
+
+class TestPackedArray : public CxxTest::TestSuite
+{
+public:
+ void setUp()
+ {
+ }
+
+ void tearDown()
+ {
+ }
+
+ void test_compiletime()
+ {
+ static_assert(std::is_same_v::Storage, u16>);
+ static_assert(std::is_same_v::Storage, u8>);
+ static_assert(std::is_same_v::Storage, u64>);
+ static_assert(sizeof(PackedArray::Storage) == 2);
+
+ static_assert(PackedArray::MaskBits == 0b11);
+ {
+ constexpr PackedArray test(SmallEnum::ONE);
+ static_assert(test.m_Storage == 0b0101010101010101);
+ }
+ {
+ constexpr PackedArray test(SmallEnum::TWO);
+ static_assert(test.m_Storage == 0b1010101010101010);
+ }
+ }
+
+ void test_basic()
+ {
+ PackedArray packed;
+ packed[4u] = SmallEnum::TWO;
+ packed[7u] = SmallEnum::ONE;
+ TS_ASSERT_EQUALS(packed[0u], SmallEnum::ZERO)
+ TS_ASSERT_EQUALS(packed[4u], SmallEnum::TWO)
+ TS_ASSERT_EQUALS(packed[7u], SmallEnum::ONE)
+ TS_ASSERT_EQUALS(packed.m_Storage, 0b0100001000000000);
+ }
+
+ void test_wraparound()
+ {
+ PackedArray packed;
+ packed[2] = SmallEnum::TWO;
+ packed[5] = SmallEnum::ONE;
+ TS_ASSERT_EQUALS(packed[0], SmallEnum::ZERO)
+ TS_ASSERT_EQUALS(packed[2], SmallEnum::TWO)
+ TS_ASSERT_EQUALS(packed[5], SmallEnum::ONE)
+ TS_ASSERT_EQUALS(packed[-1], SmallEnum::ONE)
+ TS_ASSERT_EQUALS(packed.m_Storage, 0b010000100000);
+ }
+};