Index: source/graphics/MiniMapTexture.cpp =================================================================== --- source/graphics/MiniMapTexture.cpp +++ source/graphics/MiniMapTexture.cpp @@ -52,6 +52,7 @@ #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpMinimap.h" #include "simulation2/components/ICmpRangeManager.h" +#include "simulation2/helpers/Los.h" #include "simulation2/system/ParamNode.h" #include Index: source/graphics/tests/test_LOSTexture.h =================================================================== --- source/graphics/tests/test_LOSTexture.h +++ source/graphics/tests/test_LOSTexture.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 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 @@ -31,27 +31,26 @@ CSimulation2 sim(NULL, g_ScriptContext, NULL); CLOSTexture tex(sim); + LosMask H = LosMask(LosStateFlags::VISIBLE, 1); + LosMask O = LosMask(LosStateFlags::UNEXPLORED, 1); const ssize_t size = 8; - u32 inputData[size*size] = { - 2, 2, 2, 0, 0, 0, 0, 0, - 2, 2, 2, 0, 0, 0, 0, 0, - 2, 2, 2, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 2 + LosMask inputData[size*size] = { + H, H, H, O, O, O, O, O, + H, H, H, O, O, O, O, O, + H, H, H, O, O, O, O, O, + O, O, O, O, O, O, O, O, + O, O, O, O, O, O, O, O, + O, O, O, O, O, O, O, O, + O, O, O, O, O, O, O, O, + O, O, O, O, O, O, O, H }; - Grid inputDataVec(size, size); + Grid inputDataVec(size, size); for (u8 i = 0; i < size; ++i) for (u8 j = 0; j < size; ++j) inputDataVec.set(i, j, inputData[i + j * size]); - // LosState::MASK should be cmpRanageManager->GetSharedLosMask(1), - // but that would mean adding a huge mock component for this and it - // should always be LosState::MASK for player 1 (as the other players are bit-shifted). - CLosQuerier los((u32)LosState::MASK, inputDataVec, size); + CLosQuerier los(LosMask(LosStateFlags::EXPLORED | LosStateFlags::VISIBLE, 1), inputDataVec, size); std::vector losData; size_t pitch; @@ -71,12 +70,9 @@ CLOSTexture tex(sim); const ssize_t size = 257; - Grid inputDataVec(size, size); + Grid inputDataVec(size, size); - // LosState::MASK should be cmpRanageManager->GetSharedLosMask(1), - // but that would mean adding a huge mock component for this and it - // should always be LosState::MASK for player 1 (as the other players are bit-shifted). - CLosQuerier los((u32)LosState::MASK, inputDataVec, size); + CLosQuerier los(LosMask(LosStateFlags::EXPLORED | LosStateFlags::VISIBLE, 1), inputDataVec, size); size_t reps = 128; double t = timer_Time(); 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/EnumArray.h" #include "simulation2/helpers/Los.h" #include "simulation2/helpers/MapEdgeTiles.h" #include "simulation2/helpers/Render.h" @@ -60,95 +61,33 @@ */ const fixed PARABOLIC_RANGE_TOLERANCE = fixed::FromInt(1)/2; +using LosRegion = std::pair; + /** * Convert an owner ID (-1 = unowned, 0 = gaia, 1..30 = players) - * into a 32-bit mask for quick set-membership tests. + * into a bit-mask for quick set-membership tests. */ -u32 CalcOwnerMask(player_id_t owner) +constexpr EnumArray CalcOwnerMask(player_id_t owner) { - if (owner >= -1 && owner < 31) - return 1 << (1+owner); + if (owner == INVALID_PLAYER) + return EnumArray(true, MAX_PLAYER_ID+1); + else if (owner >= 0 && owner <= MAX_PLAYER_ID) + return EnumArray(true, 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; + return EnumArray(); // Invalid owner, default to 0 everywhere. } /** * Returns shared LOS mask for given list of players. */ -u32 CalcSharedLosMask(std::vector players) +LosMask CalcSharedLosMask(std::vector players) { - u32 playerMask = 0; + LosMask playerMask; for (size_t i = 0; i < players.size(); i++) - playerMask |= CalcPlayerLosMask(players[i]); - + playerMask |= LosMask::mask(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 +98,7 @@ entity_pos_t minRange; entity_pos_t maxRange; entity_pos_t yOrigin; // Used for parabolas only. - u32 ownersMask; + EnumArray ownersMask; // Range queries can check against unowned entities. i32 interface; u8 flagsMask; bool enabled; @@ -176,7 +115,7 @@ * * Avoids sqrting and overflowing. */ -static bool InParabolicRange(CFixedVector3D v, fixed range) +bool InParabolicRange(CFixedVector3D v, fixed range) { u64 xx = SQUARE_U64_FIXED(v.X); // xx <= 2^62 u64 zz = SQUARE_U64_FIXED(v.Z); @@ -225,14 +164,14 @@ struct EntityData { EntityData() : - visibilities(0), size(0), visionSharing(0), + visibilities(), size(0), visionSharing(), owner(-1), flags(FlagMasks::Normal) { } entity_pos_t x, z; entity_pos_t visionRange; - u32 visibilities; // 2-bit visibility, per player + EnumArray visibilities; u32 size; - u16 visionSharing; // 1-bit per player + EnumArray visionSharing; i8 owner; u8 flags; // See the FlagMasks enum @@ -291,7 +230,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 +271,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,20 +330,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; + // The last value is a special value that means "for all". + 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 +350,18 @@ // 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_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 (including GAIA and invalid) + std::array m_SharedLosMasks; + // Shared dirty visibility masks, one per player (including GAIA and invalid) + std::array, MAX_PLAYERS> m_SharedDirtyVisibilityMasks; // Cache explored vertices per player (not serialized) u32 m_TotalInworldVertices; @@ -453,8 +389,13 @@ m_SubdivisionResults.reserve(4096); // The whole map should be visible to Gaia by default, else e.g. animals - // will get confused when trying to run from enemies - m_LosRevealAll[0] = true; + // will get confused when trying to run from enemies. + // TODO: this particular setting should be made easily moddable. + // If you do make it moddable, remember to change all loops starting at FIRST_NONGAIA_PLAYER_ID. + for (player_id_t gaia_player = 0; gaia_player < FIRST_NONGAIA_PLAYER_ID; ++gaia_player) + m_LosRevealAll[gaia_player] = true; + for (player_id_t player = FIRST_NONGAIA_PLAYER_ID; player <= MAX_PLAYER_ID; ++player) + m_LosRevealAll[player] = false; m_GlobalVisibilityUpdate = true; @@ -737,8 +678,8 @@ 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 > FIRST_NONGAIA_PLAYER_ID && msgData.player <= MAX_PLAYER_ID); + EnumArray visionChanged(true, msgData.player); if (!it->second.HasFlag()) { @@ -803,8 +744,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_PLAYERS> oldPlayerCounts = m_LosPlayerCounts; + Grid oldStateRevealed = m_LosStateRevealed; FastSpatialSubdivision oldSubdivision = m_Subdivision; Grid > oldLosRegions = m_LosRegions; @@ -861,7 +802,7 @@ m_LosPlayerCounts[player_id].clear(); m_ExploredVertices.clear(); - m_ExploredVertices.resize(MAX_LOS_PLAYER_ID+1, 0); + m_ExploredVertices.resize(MAX_PLAYERS, 0); if (m_Deserializing) { @@ -869,8 +810,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 (player_id_t player = FIRST_NONGAIA_PLAYER_ID; player <= MAX_PLAYER_ID; ++player) + m_ExploredVertices.at(player) += static_cast(m_LosState.get(i, j).at(player) & LosStateFlags::EXPLORED); } else m_LosState.resize(m_LosVerticesPerSide, m_LosVerticesPerSide); @@ -903,10 +844,10 @@ for (i32 i = 0; i < m_LosVerticesPerSide; ++i) { if (LosIsOffWorld(i,j)) - m_LosStateRevealed.get(i, j) = 0; + m_LosStateRevealed.get(i, j) = LosMask::all(LosStateFlags::UNEXPLORED); else { - m_LosStateRevealed.get(i, j) = 0xFFFFFFFFu; + m_LosStateRevealed.get(i, j) = LosMask::all(LosStateFlags::EXPLORED | LosStateFlags::VISIBLE); m_TotalInworldVertices++; } } @@ -1073,15 +1014,18 @@ std::vector GetNonGaiaEntities() const override { - return GetEntitiesByMask(~3u); // bit 0 for owner=-1 and bit 1 for gaia + EnumArray mask(true, MAX_PLAYERS+1); + for (player_id_t gaia_players = 0; gaia_players < FIRST_NONGAIA_PLAYER_ID; ++gaia_players) + mask |= EnumArray(true, gaia_players); + return GetEntitiesByMask(~mask); } std::vector GetGaiaAndNonGaiaEntities() const override { - return GetEntitiesByMask(~1u); // bit 0 for owner=-1 + return GetEntitiesByMask(~EnumArray(true, MAX_PLAYERS+1)); } - std::vector GetEntitiesByMask(u32 ownerMask) const + std::vector GetEntitiesByMask(EnumArray ownerMask) const { std::vector entities; @@ -1445,11 +1389,11 @@ q.maxRange += fixed::FromInt(size); } - q.ownersMask = 0; + q.ownersMask = EnumArray(); for (size_t i = 0; i < owners.size(); ++i) q.ownersMask |= CalcOwnerMask(owners[i]); - if (q.ownersMask == 0) + if (!q.ownersMask) LOGWARNING("CCmpRangeManager: No owners in query for entity %u", source); q.interface = requiredInterface; @@ -1638,7 +1582,7 @@ CLosQuerier GetLosQuerier(player_id_t player) const override { if (GetLosRevealAll(player)) - return CLosQuerier(0xFFFFFFFFu, m_LosStateRevealed, m_LosVerticesPerSide); + return CLosQuerier(LosMask::all(LosStateFlags::EXPLORED | LosStateFlags::VISIBLE), m_LosStateRevealed, m_LosVerticesPerSide); else return CLosQuerier(GetSharedLosMask(player), m_LosState, m_LosVerticesPerSide); } @@ -1765,13 +1709,12 @@ if (!cmpPosition || !cmpPosition->IsInWorld()) return LosVisibility::HIDDEN; - // Gaia and observers do not have a visibility cache + // Observers do not have a visibility cache if (player <= 0) return ComputeLosVisibility(ent, player); CFixedVector2D pos = cmpPosition->GetPosition2D(); - - if (IsVisibilityDirty(m_DirtyVisibility[PosToLosRegionsHelper(pos.X, pos.Y)], player)) + if (m_DirtyVisibility[PosToLosRegionsHelper(pos.X, pos.Y)].at(player)) return ComputeLosVisibility(ent, player); if (std::find(m_ModifiedEntities.begin(), m_ModifiedEntities.end(), entId) != m_ModifiedEntities.end()) @@ -1781,7 +1724,7 @@ if (it == m_EntityData.end()) return ComputeLosVisibility(ent, player); - return static_cast(GetPlayerVisibility(it->second.visibilities, player)); + return static_cast(it->second.visibilities.at(player)); } LosVisibility GetLosVisibility(entity_id_t ent, player_id_t player) const override @@ -1860,12 +1803,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 (player_id_t player = FIRST_NONGAIA_PLAYER_ID; player <= MAX_PLAYER_ID; ++player) for (const entity_id_t& ent : m_LosRegions[pos]) + if (m_DirtyVisibility[pos].at(player) || m_GlobalPlayerVisibilityUpdate[player] == 1 || m_GlobalVisibilityUpdate) UpdateVisibility(ent, player); - m_DirtyVisibility[pos] = 0; + m_DirtyVisibility[pos] = m_DirtyVisibility[pos].all(false); } std::fill(m_GlobalPlayerVisibilityUpdate.begin(), m_GlobalPlayerVisibilityUpdate.end(), false); @@ -1898,13 +1841,13 @@ if (itEnts == m_EntityData.end()) return; - LosVisibility oldVis = GetPlayerVisibility(itEnts->second.visibilities, player); + LosVisibility oldVis = itEnts->second.visibilities.at(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 +1855,17 @@ void UpdateVisibility(entity_id_t ent) { - for (player_id_t player = 1; player < MAX_LOS_PLAYER_ID + 1; ++player) + for (player_id_t player = FIRST_NONGAIA_PLAYER_ID; player <= MAX_PLAYER_ID; ++player) UpdateVisibility(ent, player); } void SetLosRevealAll(player_id_t player, bool enabled) override { - if (player == -1) - m_LosRevealAll[MAX_LOS_PLAYER_ID+1] = enabled; + if (player == INVALID_PLAYER) + m_LosRevealAll[MAX_PLAYER_ID+1] = enabled; else { - ENSURE(player >= 0 && player <= MAX_LOS_PLAYER_ID); + ENSURE(player >= 0 && player <= MAX_PLAYER_ID); m_LosRevealAll[player] = enabled; } @@ -1933,9 +1876,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_PLAYER_ID+1] || player == INVALID_PLAYER) return true; - ENSURE(player >= 0 && player <= MAX_LOS_PLAYER_ID+1); + ENSURE(player >= 0 && player <= MAX_PLAYER_ID); // Otherwise check the player-specific flag if (m_LosRevealAll[player]) return true; @@ -1965,11 +1908,11 @@ // '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 (player_id_t p = FIRST_NONGAIA_PLAYER_ID; p <= MAX_PLAYER_ID; ++p) { bool inList = std::find(players.begin(), players.end(), p) != players.end(); - if (SetPlayerSharedDirtyVisibilityBit(m_SharedDirtyVisibilityMasks[p], player, inList)) + if ((m_SharedDirtyVisibilityMasks[p][player] = inList)) modified = true; } @@ -1977,7 +1920,7 @@ m_GlobalPlayerVisibilityUpdate[player-1] = 1; } - u32 GetSharedLosMask(player_id_t player) const override + LosMask GetSharedLosMask(player_id_t player) const { return m_SharedLosMasks[player]; } @@ -1990,8 +1933,8 @@ 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).at(p) & LosStateFlags::EXPLORED); + m_LosState.get(i, j)[p] |= LosStateFlags::EXPLORED; } SeeExploredEntities(p); @@ -2016,26 +1959,24 @@ for (i32 j = 0; j < m_LosVerticesPerSide; ++j) for (i32 i = 0; i < m_LosVerticesPerSide; ++i) { - // 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) + u8 p = grid.get(i, j) & ICmpTerritoryManager::TERRITORY_PLAYER_MASK; + if (p >= FIRST_NONGAIA_PLAYER_ID && p <= MAX_PLAYER_ID) { 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))))) + LosMask& losState = m_LosState.get(i, j); + if (!(losState.at(p) & LosStateFlags::EXPLORED)) { ++explored; - losState |= ((u32)LosState::EXPLORED << (2*(p-1))); + losState[p] |= LosStateFlags::EXPLORED; } } } - for (player_id_t p = 1; p < MAX_LOS_PLAYER_ID+1; ++p) + for (player_id_t p = FIRST_NONGAIA_PLAYER_ID; p <= MAX_PLAYER_ID; ++p) SeeExploredEntities(p); } @@ -2082,7 +2023,7 @@ void RevealShore(player_id_t p, bool enable) override { - if (p <= 0 || p > MAX_LOS_PLAYER_ID) + if (p == INVALID_PLAYER || p < FIRST_NONGAIA_PLAYER_ID || p > MAX_PLAYER_ID) return; // Maximum distance to the shore @@ -2142,7 +2083,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(player_id_t owner, i32 i0, i32 i1, i32 j, Grid& counts) { if (i1 < i0) return; @@ -2155,8 +2096,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).at(owner) & LosStateFlags::EXPLORED); + m_LosState.get(i, j)[owner] |= (LosStateFlags::VISIBLE | LosStateFlags::EXPLORED); } MarkVisibilityDirtyAroundTile(owner, i, j); @@ -2170,7 +2111,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(player_id_t owner, i32 i0, i32 i1, i32 j, Grid& counts) { if (i1 < i0) return; @@ -2184,14 +2125,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].unset_bit(LosStateFlags::VISIBLE); MarkVisibilityDirtyAroundTile(owner, i, j); } } } - inline void MarkVisibilityDirtyAroundTile(u8 owner, i32 i, i32 j) + inline void MarkVisibilityDirtyAroundTile(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,7 +2145,7 @@ LosRegion n3 = LosVertexToLosRegionsHelper(i, j-1); LosRegion n4 = LosVertexToLosRegionsHelper(i, j); - u16 sharedDirtyVisibilityMask = m_SharedDirtyVisibilityMasks[owner]; + EnumArray sharedDirtyVisibilityMask = m_SharedDirtyVisibilityMasks[owner]; if (j > 0 && i > 0) m_DirtyVisibility[n1] |= sharedDirtyVisibilityMask; @@ -2222,7 +2163,7 @@ * Assumes owner is in the valid range. */ template - void LosUpdateHelper(u8 owner, entity_pos_t visionRange, CFixedVector2D pos) + void LosUpdateHelper(player_id_t owner, entity_pos_t visionRange, CFixedVector2D pos) { if (m_LosVerticesPerSide == 0) // do nothing if not initialised yet return; @@ -2309,7 +2250,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(player_id_t owner, entity_pos_t visionRange, CFixedVector2D from, CFixedVector2D to) { if (m_LosVerticesPerSide == 0) // do nothing if not initialised yet return; @@ -2434,64 +2375,64 @@ 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 == INVALID_PLAYER || owner < FIRST_NONGAIA_PLAYER_ID || owner > MAX_PLAYER_ID) return; - LosUpdateHelper((u8)owner, visionRange, pos); + LosUpdateHelper(owner, visionRange, pos); } - void SharingLosAdd(u16 visionSharing, entity_pos_t visionRange, CFixedVector2D pos) + void SharingLosAdd(EnumArray 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)) - LosAdd(i, visionRange, pos); + for (player_id_t player = FIRST_NONGAIA_PLAYER_ID; player <= MAX_PLAYER_ID; ++player) + if (visionSharing.at(player)) + LosAdd(player, 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 == INVALID_PLAYER || owner < FIRST_NONGAIA_PLAYER_ID || owner > MAX_PLAYER_ID) return; LosUpdateHelper((u8)owner, visionRange, pos); } - void SharingLosRemove(u16 visionSharing, entity_pos_t visionRange, CFixedVector2D pos) + void SharingLosRemove(EnumArray 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)) - LosRemove(i, visionRange, pos); + for (player_id_t player = FIRST_NONGAIA_PLAYER_ID; player <= MAX_PLAYER_ID; ++player) + if (visionSharing.at(player)) + LosRemove(player, 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 == INVALID_PLAYER || owner < FIRST_NONGAIA_PLAYER_ID || owner > MAX_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(owner, visionRange, from); + LosUpdateHelper(owner, visionRange, to); } else // Otherwise use the version optimised for mostly-overlapping circles - LosUpdateHelperIncremental((u8)owner, visionRange, from, to); + LosUpdateHelperIncremental(owner, visionRange, from, to); } - void SharingLosMove(u16 visionSharing, entity_pos_t visionRange, CFixedVector2D from, CFixedVector2D to) + void SharingLosMove(EnumArray 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)) - LosMove(i, visionRange, from, to); + for (player_id_t player = FIRST_NONGAIA_PLAYER_ID; player <= MAX_PLAYER_ID; ++player) + if (visionSharing.at(player)) + LosMove(player, visionRange, from, to); } u8 GetPercentMapExplored(player_id_t player) const override @@ -2511,7 +2452,7 @@ continue; for (playerIt = players.begin(); playerIt != players.end(); ++playerIt) - if (m_LosState.get(i, j) & ((u32)LosState::EXPLORED << (2*((*playerIt)-1)))) + if (m_LosState.get(i, j).at(*playerIt) & LosStateFlags::EXPLORED) { exploredVertices += 1; break; Index: source/simulation2/components/CCmpSoundManager.cpp =================================================================== --- source/simulation2/components/CCmpSoundManager.cpp +++ source/simulation2/components/CCmpSoundManager.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 @@ -21,9 +21,10 @@ #include "ICmpSoundManager.h" #include "simulation2/MessageTypes.h" +#include "simulation2/components/ICmpOwnership.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpRangeManager.h" -#include "simulation2/components/ICmpOwnership.h" +#include "simulation2/helpers/Los.h" #include "soundmanager/ISoundManager.h" Index: source/simulation2/components/CCmpUnitRenderer.cpp =================================================================== --- source/simulation2/components/CCmpUnitRenderer.cpp +++ source/simulation2/components/CCmpUnitRenderer.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 @@ -18,8 +18,10 @@ #include "precompiled.h" #include "simulation2/system/Component.h" + #include "ICmpUnitRenderer.h" +#include "simulation2/helpers/Los.h" #include "simulation2/MessageTypes.h" #include "ICmpPosition.h" 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 @@ -28,6 +28,7 @@ #include class FastSpatialSubdivision; +enum class LosVisibility : u8; /** * Value assigned to a range we will always be in (caused by out of world or "too high" in parabolic ranges). @@ -42,23 +43,8 @@ const entity_pos_t NEVER_IN_RANGE = entity_pos_t::FromInt(-2); /** - * Since GetVisibility queries are run by the range manager - * other code using these must include ICmpRangeManager.h anyways, - * so define this enum here (Ideally, it'd be in its own header file, - * but adding header file does incur its own compilation time increase). - */ -enum class LosVisibility : u8 -{ - HIDDEN = 0, - FOGGED = 1, - VISIBLE = 2 -}; - -/** - * The same principle applies to CLosQuerier, but to avoid recompiling TUs (a fortiori headers) - * dependent on RangeManager but not CLosQuerier when CLosQuerier changes, - * we define it in another file. Code using LOS will then explicitly include the LOS header - * which makes sense anyways. + * To avoid recompiling TUs (a fortiori headers) dependent on RangeManager but not CLosQuerier when CLosQuerier changes, + * we define it in another file. Code using LOS will then explicitly include the LOS header which makes sense anyways. */ class CLosQuerier; @@ -362,11 +348,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/components/ICmpRangeManager.cpp =================================================================== --- source/simulation2/components/ICmpRangeManager.cpp +++ source/simulation2/components/ICmpRangeManager.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 @@ -19,6 +19,7 @@ #include "ICmpRangeManager.h" +#include "simulation2/helpers/Los.h" #include "simulation2/system/InterfaceScripted.h" namespace { Index: source/simulation2/components/ICmpVisibility.cpp =================================================================== --- source/simulation2/components/ICmpVisibility.cpp +++ source/simulation2/components/ICmpVisibility.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 @@ -19,6 +19,7 @@ #include "ICmpVisibility.h" +#include "simulation2/helpers/Los.h" #include "simulation2/scripting/ScriptComponent.h" #include "simulation2/system/InterfaceScripted.h" Index: source/simulation2/helpers/EnumArray.h =================================================================== --- /dev/null +++ source/simulation2/helpers/EnumArray.h @@ -0,0 +1,219 @@ +/* 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_ENUMARRAY +#define INCLUDED_ENUMARRAY + +#include "Player.h" + +#include "simulation2/serialization/IDeserializer.h" +#include "simulation2/serialization/ISerializer.h" + +#include "simulation2/serialization/SerializeTemplates.h" + +/** + * This class keeps a packed representation of flag/bit values (from Enum) for SIZE members. + * A typical use-case is LOS storing a boolean or a 2-bit flag per player. + * A large part of the interface of this class only makes sense if Enum is bitfield-like. + */ +template +class EnumArray +{ + friend class TestEnumArray; + friend struct storage_wrapper; + friend struct SerializeHelper>; + +////////////////////////////////////////////////////////////// +//// Size requirements & data storage +////////////////////////////////////////////////////////////// +public: + + static constexpr u8 bits_per_enum = std::numeric_limits::digits; + static_assert(bits_per_enum <= 8, "Data types bigger than 8 bits are not supported"); + static_assert(bits_per_enum > 0, "0-bit data need not be stored"); + static constexpr u16 total_bits = bits_per_enum * SIZE; + static constexpr u16 bytes = total_bits / CHAR_BIT + ((total_bits % CHAR_BIT) > 0); + + // Determine storage type based on type. + static_assert(bytes <= 8, "Cannot store more data than 64 bits for now"); + using storage = \ + typename std::conditional::type>::type>::type; + +protected: + storage m_Storage; + +////////////////////////////////////////////////////////////// +//// Convenience bit functions +////////////////////////////////////////////////////////////// +protected: + + // Returns 1 for the first 'bits_per_enum' bits, then 0. + static constexpr storage first_bits_mask() + { + static_assert(std::is_scalar::value, "Not implemented for non-scalar types"); + u8 r = 0; + for (u8 i = 0; i < bits_per_enum; i++) + r |= 1 << i; + return r; + } + // Returns val SIZE times. + static constexpr storage bitmask_all(Enum val) + { + static_assert(std::is_scalar::value, "Not implemented for non-scalar types"); + storage r = 0; + for (u8 i = 0; i < SIZE; i++) + r |= static_cast(val) << (i * bits_per_enum); + return r; + } + +////////////////////////////////////////////////////////////// +//// Returned by operator[] to provide write semantics. +////////////////////////////////////////////////////////////// +protected: + class storage_wrapper + { + friend class EnumArray; + protected: + EnumArray& data; + u8 pos; + constexpr storage_wrapper(EnumArray& d, u8 p) : data(d), pos(p) {}; + public: + void constexpr operator |= (Enum val) { data.or_at(val, pos); } + void constexpr operator &= (Enum val) { data.and_at(val, pos); } + void constexpr unset_bit(Enum val) { data.and_at(Enum(~(u8)val), pos); } + bool constexpr operator=(Enum val) { return data.replace(val, pos); }; + }; + + +////////////////////////////////////////////////////////////// +//// Constructors & Conversions +////////////////////////////////////////////////////////////// +public: + constexpr EnumArray() : m_Storage(0) {} + constexpr EnumArray(Enum val, u8 pos) : m_Storage(0) { or_at(val, pos); } + +protected: + explicit constexpr EnumArray(storage val) : m_Storage(val) {} + explicit constexpr operator storage() { return m_Storage; } + +////////////////////////////////////////////////////////////// +//// Storage-wise operations +////////////////////////////////////////////////////////////// +public: + EnumArray constexpr operator ~() const { return EnumArray(~m_Storage); } + EnumArray constexpr operator |(const EnumArray& o) const { return EnumArray(m_Storage | o.m_Storage); } + EnumArray constexpr operator &(const EnumArray& o) const { return EnumArray(m_Storage & o.m_Storage); } + + void constexpr operator &= (const EnumArray& o) { m_Storage &= o.m_Storage; } + void constexpr operator |= (const EnumArray& o) { m_Storage |= o.m_Storage; } + + bool constexpr operator==(const EnumArray& o) const { return m_Storage == o.m_Storage; }; + bool constexpr operator!=(const EnumArray& o) const { return m_Storage != o.m_Storage; }; + + bool constexpr any(Enum val) { return m_Storage & bitmask_all(val); } + +////////////////////////////////////////////////////////////// +//// Member-wise operations +////////////////////////////////////////////////////////////// +public: + void constexpr or_at(Enum val, u8 pos) + { + static_assert(std::is_scalar::value, "Not implemented for non-scalar types"); + if (pos >= SIZE) + return; + + // We need to set all non-pos bits to 0 to avoid accidentally settings bits for other members. + // (the shift defaults to 0 so it's fine to do it in the left-op). + m_Storage |= ((u8)val & first_bits_mask()) << (pos * bits_per_enum); + } + + void constexpr and_at(Enum val, u8 pos) + { + static_assert(std::is_scalar::value, "Not implemented for non-scalar types"); + if (pos >= SIZE) + return; + + // We need to be sure all non-pos bits are 1 so that &= doesn't reset bits for other members. + m_Storage &= ((u8)val << (pos * bits_per_enum)) | ~(first_bits_mask() << (pos * bits_per_enum)); + } + + // Returns true if storage was modified. + bool constexpr replace(Enum val, u8 pos) + { + static_assert(std::is_scalar::value, "Not implemented for non-scalar types"); + + if (pos >= SIZE) + return false; + + Enum oldval = at(pos); + // First clear whatever bits we had there + m_Storage &= ~(first_bits_mask() << (pos * bits_per_enum)); + // Then set whatever we have now. + m_Storage |= (u8)val << pos * bits_per_enum; + + return oldval != at(pos); + } + + Enum constexpr at(u8 pos) const + { + static_assert(std::is_scalar::value, "Not implemented for non-scalar types"); + return Enum((m_Storage >> (pos * bits_per_enum)) & first_bits_mask()); + } + + Enum constexpr operator[](u8 pos) const + { + return at(pos); + } + + storage_wrapper constexpr operator[](u8 pos) + { + return { *this, pos }; + } + +////////////////////////////////////////////////////////////// +//// Other +////////////////////////////////////////////////////////////// +public: + + // NB -> not per member, so an EnumArray is truthy if any bit of any member is non-zero. + explicit constexpr operator bool() const { return m_Storage > 0; } + + static constexpr EnumArray mask(u8 n) { return EnumArray(first_bits_mask() << (n * bits_per_enum)); } + static constexpr EnumArray all(Enum val) { return EnumArray(bitmask_all(val)); } + static constexpr size_t size() { return SIZE; } +}; + + +template +struct SerializeHelper> +{ + void operator()(ISerializer& serialize, const char* name, EnumArray& value) + { + Serializer(serialize, name, value.m_Storage); + } + + void operator()(IDeserializer& deserialize, const char* name, EnumArray& value) + { + Serializer(deserialize, name, value.m_Storage); + } +}; + +#endif // INCLUDED_ENUMARRAY 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 @@ -20,7 +20,9 @@ // It doesn't seem worth moving the implementation to c++ and early-declaring Grid // since files must include "Los.h" explicitly, and that's only done in .cpp files. +#include "EnumArray.h" #include "Grid.h" +#include "Player.h" /** * Computing LOS data at a very high resolution is not necessary and quite slow. @@ -29,14 +31,38 @@ */ static constexpr i32 LOS_TILE_SIZE = 4; -enum class LosState : u8 +enum class LosVisibility : u8 { - UNEXPLORED = 0, - EXPLORED = 1, - VISIBLE = 2, - MASK = 3 + HIDDEN = 0, + FOGGED = 1, + VISIBLE = 2 }; +enum class LosStateFlags : u8 +{ + UNEXPLORED = 0, // Provided for convenience + EXPLORED = 0x1, + VISIBLE = 0x2, +}; + +/** + * LosStateFlags is an enum-class to prevent implicit conversion to int, + * but we do want it to have bitfield-like behaviour. Since enum class can't have members + * the simplest solution is to provide these free function operators. + * operator& returns bool for explicitness. + * operator~ is not available on purpose as EnumArray provides the same behaviour with more explicit semantics. + */ +inline constexpr LosStateFlags operator| (LosStateFlags a, LosStateFlags b) { return LosStateFlags((u8)a | (u8)b); } +inline constexpr bool operator& (LosStateFlags a, LosStateFlags b) { return ((u8)a & (u8)b) != 0; } + +namespace std +{ + template<> class numeric_limits { public: static constexpr int digits = 2; }; + template<> class numeric_limits { public: static constexpr int digits = 2; }; +} + +using LosMask = EnumArray; + /** * Object providing efficient abstracted access to the LOS state. * This depends on some implementation details of CCmpRangeManager. @@ -49,7 +75,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) { } @@ -64,12 +90,7 @@ { if (!(i >= 0 && j >= 0 && i < m_VerticesPerSide && j < m_VerticesPerSide)) return false; - - // Check high bit of each bit-pair - if ((m_Data.get(i, j) & m_PlayerMask) & 0xAAAAAAAAu) - return true; - else - return false; + return (m_Data.get(i, j) & m_PlayerMask).any(LosStateFlags::VISIBLE); } /** @@ -79,12 +100,7 @@ { if (!(i >= 0 && j >= 0 && i < m_VerticesPerSide && j < m_VerticesPerSide)) return false; - - // Check low bit of each bit-pair - if ((m_Data.get(i, j) & m_PlayerMask) & 0x55555555u) - return true; - else - return false; + return (m_Data.get(i, j) & m_PlayerMask).any(LosStateFlags::EXPLORED); } /** @@ -96,11 +112,7 @@ #ifndef NDEBUG 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) - return true; - else - return false; + return (m_Data.get(i, j) & m_PlayerMask).any(LosStateFlags::VISIBLE); } /** @@ -112,16 +124,12 @@ #ifndef NDEBUG 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) - return true; - else - return false; + return (m_Data.get(i, j) & m_PlayerMask).any(LosStateFlags::EXPLORED); } private: - u32 m_PlayerMask; - const Grid& m_Data; + LosMask m_PlayerMask; + const Grid& m_Data; ssize_t m_VerticesPerSide; }; 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,26 @@ */ typedef int32_t player_id_t; -static const player_id_t INVALID_PLAYER = -1; +static constexpr 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. + */ +static constexpr player_id_t MAX_PLAYERS = 15; + +/** + * Provided for convenience. + */ +static constexpr player_id_t MAX_PLAYER_ID = MAX_PLAYERS-1; + +/** + * This is used to explicit "0" as gaia where relevant. + * Currently only used in RangeManager to set the "reveal-all" flag for gaia players. + * This number doesn't have to be hardcoded as much as the others, but it would likely + * incur a performance penalty since the compiler wouldn't be able to optimise as well. + */ +static constexpr player_id_t FIRST_NONGAIA_PLAYER_ID = 1; + #endif // INCLUDED_PLAYER Index: source/simulation2/helpers/Selection.cpp =================================================================== --- source/simulation2/helpers/Selection.cpp +++ source/simulation2/helpers/Selection.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 @@ -29,6 +29,7 @@ #include "simulation2/components/ICmpSelectable.h" #include "simulation2/components/ICmpVisual.h" #include "simulation2/components/ICmpUnitRenderer.h" +#include "simulation2/helpers/Los.h" #include "simulation2/system/ComponentManager.h" #include Index: source/simulation2/tests/test_EnumArray.h =================================================================== --- /dev/null +++ source/simulation2/tests/test_EnumArray.h @@ -0,0 +1,154 @@ +/* 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 "lib/timer.h" + +#include "simulation2/serialization/ISerializer.h" +#include "simulation2/serialization/IDeserializer.h" + +#include "simulation2/helpers/EnumArray.h" + +enum SomeEnum : u8 +{ + BIT_0_TOGGLE = 1, + BIT_1_TOGGLE = 2, + BIT_2_TOGGLE = 4, +}; + +namespace std +{ + template<> class numeric_limits + { + public: + static constexpr int digits = 3; // Number of bits. + }; +} + +class TestEnumArray : public CxxTest::TestSuite +{ +public: + void setUp() + { + } + + void tearDown() + { + } + + void test_sizes() + { + static_assert(EnumArray::bits_per_enum == 1, ""); + static_assert(EnumArray::bits_per_enum == 3, ""); + + static_assert(EnumArray::bytes == 1, ""); + static_assert(EnumArray::bytes == 1, ""); + static_assert(EnumArray::bytes == 2, ""); + static_assert(EnumArray::bytes == 2, ""); + static_assert(EnumArray::bytes == 3, ""); + + static_assert(EnumArray::bytes == 2, ""); + static_assert(EnumArray::bytes == 3, ""); + static_assert(EnumArray::bytes == 4, ""); + static_assert(EnumArray::bytes == 6, ""); + static_assert(EnumArray::bytes == 7, ""); + } + + void test_bitmasks() + { + EnumArray test_bool; + EnumArray test_enum; + + // Would love to use C++14 binary literals here. + // Also would love to constexpr. + TS_ASSERT(test_bool.bitmask_all(true) == 15); + TS_ASSERT(test_bool.bitmask_all(false) == 0); + + TS_ASSERT(test_enum.bitmask_all(BIT_0_TOGGLE) == 0x9249249); + TS_ASSERT(test_enum.bitmask_all(BIT_1_TOGGLE) == 0x12492492); + TS_ASSERT(test_enum.bitmask_all(BIT_2_TOGGLE) == 0x24924924); + + TS_ASSERT(test_bool.first_bits_mask() == 1); + TS_ASSERT(test_enum.first_bits_mask() == 7); + + test_enum = test_enum.all(static_cast(0)); + test_enum[2] |= BIT_1_TOGGLE; + test_enum[2] = BIT_2_TOGGLE; + TS_ASSERT(test_enum.at(2) == BIT_2_TOGGLE); + TS_ASSERT_EQUALS(test_enum.m_Storage, 256); + + test_enum = test_enum.all(static_cast(0)); + test_enum[0] |= BIT_2_TOGGLE; + TS_ASSERT_EQUALS(test_enum.m_Storage, 4); + test_enum[0] |= BIT_1_TOGGLE; + TS_ASSERT_EQUALS(test_enum.m_Storage, 6); + + test_enum = test_enum.all(static_cast(0)); + test_enum[1] |= BIT_1_TOGGLE; + TS_ASSERT(test_enum.any(BIT_1_TOGGLE)); + TS_ASSERT(test_enum.at(1) & BIT_1_TOGGLE); + TS_ASSERT(!(test_enum.at(3) & BIT_1_TOGGLE)); + } + + void test_boolean() + { + EnumArray test_enum; + TS_ASSERT(!test_enum); + + test_enum[0] |= true; + test_enum[1] |= true; + test_enum[2] |= false; + + TS_ASSERT(test_enum.at(0) == true); + TS_ASSERT(test_enum.at(1) == true); + TS_ASSERT(test_enum.at(1) & true); + TS_ASSERT(test_enum.at(2) == false); + + test_enum[1] &= false; + + TS_ASSERT(test_enum.at(0) == true); + TS_ASSERT(test_enum.at(1) == false); + TS_ASSERT(test_enum.at(2) == false); + } + + void test_complex() + { + EnumArray test_enum; + TS_ASSERT(!test_enum); + + test_enum[0] |= BIT_0_TOGGLE; + test_enum[1] |= BIT_1_TOGGLE; + test_enum[1] |= BIT_0_TOGGLE; + test_enum[2] |= BIT_2_TOGGLE; + + TS_ASSERT(test_enum.at(0) == BIT_0_TOGGLE); + TS_ASSERT(test_enum.at(0) & BIT_0_TOGGLE); + TS_ASSERT(test_enum.at(1) == (BIT_1_TOGGLE | BIT_0_TOGGLE)); + TS_ASSERT(test_enum.at(1) & BIT_1_TOGGLE); + TS_ASSERT(test_enum.at(2) == BIT_2_TOGGLE); + TS_ASSERT(test_enum.at(2) & BIT_2_TOGGLE); + + test_enum[1].unset_bit(BIT_1_TOGGLE); + + TS_ASSERT(test_enum.at(0) == BIT_0_TOGGLE); + TS_ASSERT(test_enum.at(0) & BIT_0_TOGGLE); + TS_ASSERT(test_enum.at(1) == BIT_0_TOGGLE); + TS_ASSERT(test_enum.at(1) & BIT_0_TOGGLE); + TS_ASSERT(test_enum.at(2) == BIT_2_TOGGLE); + TS_ASSERT(test_enum.at(2) & BIT_2_TOGGLE); + } +};