Index: ps/trunk/source/simulation2/components/ICmpRangeManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/ICmpRangeManager.cpp (revision 8233) +++ ps/trunk/source/simulation2/components/ICmpRangeManager.cpp (revision 8234) @@ -1,33 +1,34 @@ /* Copyright (C) 2010 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 "precompiled.h" #include "ICmpRangeManager.h" #include "simulation2/system/InterfaceScripted.h" BEGIN_INTERFACE_WRAPPER(RangeManager) DEFINE_INTERFACE_METHOD_4("ExecuteQuery", std::vector, ICmpRangeManager, ExecuteQuery, entity_id_t, entity_pos_t, std::vector, int) DEFINE_INTERFACE_METHOD_4("CreateActiveQuery", ICmpRangeManager::tag_t, ICmpRangeManager, CreateActiveQuery, entity_id_t, entity_pos_t, std::vector, int) DEFINE_INTERFACE_METHOD_1("DestroyActiveQuery", void, ICmpRangeManager, DestroyActiveQuery, ICmpRangeManager::tag_t) DEFINE_INTERFACE_METHOD_1("EnableActiveQuery", void, ICmpRangeManager, EnableActiveQuery, ICmpRangeManager::tag_t) DEFINE_INTERFACE_METHOD_1("DisableActiveQuery", void, ICmpRangeManager, DisableActiveQuery, ICmpRangeManager::tag_t) DEFINE_INTERFACE_METHOD_1("ResetActiveQuery", std::vector, ICmpRangeManager, ResetActiveQuery, ICmpRangeManager::tag_t) +DEFINE_INTERFACE_METHOD_1("GetEntitiesByPlayer", std::vector, ICmpRangeManager, GetEntitiesByPlayer, int) DEFINE_INTERFACE_METHOD_1("SetDebugOverlay", void, ICmpRangeManager, SetDebugOverlay, bool) DEFINE_INTERFACE_METHOD_1("SetLosRevealAll", void, ICmpRangeManager, SetLosRevealAll, bool) END_INTERFACE_WRAPPER(RangeManager) Index: ps/trunk/source/simulation2/components/ICmpRangeManager.h =================================================================== --- ps/trunk/source/simulation2/components/ICmpRangeManager.h (revision 8233) +++ ps/trunk/source/simulation2/components/ICmpRangeManager.h (revision 8234) @@ -1,236 +1,244 @@ /* Copyright (C) 2010 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_ICMPRANGEMANAGER #define INCLUDED_ICMPRANGEMANAGER #include "simulation2/system/Interface.h" #include "simulation2/helpers/Position.h" #include "graphics/Terrain.h" // for CELL_SIZE /** * Provides efficient range-based queries of the game world, * and also LOS-based effects (fog of war). * * (These are somewhat distinct concepts but they share a lot of the implementation, * so for efficiency they're combined into this class.) * * Possible use cases: * - combat units need to detect targetable enemies entering LOS, so they can choose * to auto-attack. * - auras let a unit have some effect on all units (or those of the same player, or of enemies) * within a certain range. * - capturable animals need to detect when a player-owned unit is nearby and no units of other * players are in range. * - scenario triggers may want to detect when units enter a given area. * - units gathering from a resource that is exhausted need to find a new resource of the * same type, near the old one and reachable. * - projectile weapons with splash damage need to find all units within some distance * of the target point. * - ... * * In most cases the users are event-based and want notifications when something * has entered or left the range, and the query can be set up once and rarely changed. * These queries have to be fast. It's fine to approximate an entity as a point. * * Current design: * * This class handles just the most common parts of range queries: * distance, target interface, and player ownership. * The caller can then apply any more complex filtering that it needs. * * There are two types of query: * Passive queries are performed by ExecuteQuery and immediately return the matching entities. * Active queries are set up by CreateActiveQuery, and then a CMessageRangeUpdate message will be * sent to the entity once per turn if anybody has entered or left the range since the last RangeUpdate. * Queries can be disabled, in which case no message will be sent. */ class ICmpRangeManager : public IComponent { public: /** * External identifiers for active queries. */ typedef u32 tag_t; /** * Set the bounds of the world. * Entities should not be outside the bounds (else efficiency will suffer). * @param x0,z0,x1,z1 Coordinates of the corners of the world * @param vertices Number of terrain vertices per side */ virtual void SetBounds(entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, ssize_t vertices) = 0; /** * Execute a passive query. * @param source the entity around which the range will be computed. * @param maxRange maximum distance in metres (inclusive). * @param owners list of player IDs that matching entities may have; -1 matches entities with no owner. * @param requiredInterface if non-zero, an interface ID that matching entities must implement. * @return list of entities matching the query, ordered by increasing distance from the source entity. */ virtual std::vector ExecuteQuery(entity_id_t source, entity_pos_t maxRange, std::vector owners, int requiredInterface) = 0; /** * Construct an active query. The query will be disabled by default. * @param source the entity around which the range will be computed. * @param maxRange maximum distance in metres (inclusive). * @param owners list of player IDs that matching entities may have; -1 matches entities with no owner. * @param requiredInterface if non-zero, an interface ID that matching entities must implement. * @return unique non-zero identifier of query. */ virtual tag_t CreateActiveQuery(entity_id_t source, entity_pos_t maxRange, std::vector owners, int requiredInterface) = 0; /** * Destroy a query and clean up resources. This must be called when an entity no longer needs its * query (e.g. when the entity is destroyed). * @param tag identifier of query. */ virtual void DestroyActiveQuery(tag_t tag) = 0; /** * Re-enable the processing of a query. * @param tag identifier of query. */ virtual void EnableActiveQuery(tag_t tag) = 0; /** * Disable the processing of a query (no RangeUpdate messages will be sent). * @param tag identifier of query. */ virtual void DisableActiveQuery(tag_t tag) = 0; /** * Immediately execute a query, and re-enable it if disabled. * The next RangeUpdate message will say who has entered/left since this call, * so you won't miss any notifications. * @param tag identifier of query. * @return list of entities matching the query, ordered by increasing distance from the source entity. */ virtual std::vector ResetActiveQuery(tag_t tag) = 0; /** + * Returns list of all entities for specific player. + * (This is on this interface because it shares a lot of the implementation. + * Maybe it should be extended to be more like ExecuteQuery without + * the range parameter.) + */ + virtual std::vector GetEntitiesByPlayer(int playerId) = 0; + + /** * Toggle the rendering of debug info. */ virtual void SetDebugOverlay(bool enabled) = 0; // LOS interface: enum ELosState { LOS_UNEXPLORED = 0, LOS_EXPLORED = 1, LOS_VISIBLE = 2, LOS_MASK = 3 }; enum ELosVisibility { VIS_HIDDEN, VIS_FOGGED, VIS_VISIBLE }; /** * Object providing efficient abstracted access to the LOS state. * This depends on some implementation details of CCmpRangeManager. * * This *ignores* the GetLosRevealAll flag - callers should check that explicitly. */ class CLosQuerier { private: friend class CCmpRangeManager; CLosQuerier(int player, const std::vector& data, ssize_t verticesPerSide) : m_Data(data), m_VerticesPerSide(verticesPerSide) { if (player > 0 && player <= 16) m_PlayerMask = LOS_MASK << (2*(player-1)); else m_PlayerMask = 0; } const CLosQuerier& operator=(const CLosQuerier&); // not implemented public: /** * Returns whether the given vertex is visible (i.e. is within a unit's LOS). * i and j must be in the range [0, verticesPerSide). */ inline bool IsVisible(ssize_t i, ssize_t j) { #ifndef NDEBUG debug_assert(i >= 0 && j >= 0 && i < m_VerticesPerSide && j < m_VerticesPerSide); #endif // Check high bit of each bit-pair if ((m_Data.at(j*m_VerticesPerSide + i) & m_PlayerMask) & 0xAAAAAAAAu) return true; else return false; } /** * Returns whether the given vertex is explored (i.e. was (or still is) within a unit's LOS). * i and j must be in the range [0, verticesPerSide). */ inline bool IsExplored(ssize_t i, ssize_t j) { #ifndef NDEBUG debug_assert(i >= 0 && j >= 0 && i < m_VerticesPerSide && j < m_VerticesPerSide); #endif // Check low bit of each bit-pair if ((m_Data.at(j*m_VerticesPerSide + i) & m_PlayerMask) & 0x55555555u) return true; else return false; } private: u32 m_PlayerMask; const std::vector& m_Data; ssize_t m_VerticesPerSide; }; /** * Returns a CLosQuerier for checking whether vertex positions are visible to the given player. */ virtual CLosQuerier GetLosQuerier(int player) = 0; /** * Returns the visibility status of the given entity, with respect to the given player. * Returns VIS_HIDDEN if the entity doesn't exist or is not in the world. * This respects the GetLosRevealAll flag. */ virtual ELosVisibility GetLosVisibility(entity_id_t ent, int player) = 0; /** * Set globally whether the whole map should be made visible. */ virtual void SetLosRevealAll(bool enabled) = 0; /** * Returns whether the whole map has been made visible to the given player. */ virtual bool GetLosRevealAll(int player) = 0; DECLARE_INTERFACE_TYPE(RangeManager) }; #endif // INCLUDED_ICMPRANGEMANAGER Index: ps/trunk/source/simulation2/components/CCmpRangeManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpRangeManager.cpp (revision 8233) +++ ps/trunk/source/simulation2/components/CCmpRangeManager.cpp (revision 8234) @@ -1,791 +1,807 @@ /* Copyright (C) 2010 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 "precompiled.h" #include "simulation2/system/Component.h" #include "ICmpRangeManager.h" #include "ICmpPosition.h" #include "ICmpVision.h" #include "simulation2/MessageTypes.h" #include "simulation2/helpers/Render.h" #include "simulation2/helpers/Spatial.h" #include "graphics/Overlay.h" #include "graphics/Terrain.h" #include "lib/timer.h" #include "maths/FixedVector2D.h" #include "ps/CLogger.h" #include "ps/Overlay.h" #include "ps/Profile.h" #include "renderer/Scene.h" /** * Representation of a range query. */ struct Query { bool enabled; entity_id_t source; entity_pos_t maxRange; u32 ownersMask; int interface; std::vector lastMatch; }; /** * Convert an owner ID (-1 = unowned, 0 = gaia, 1..30 = players) * into a 32-bit mask for quick set-membership tests. */ static u32 CalcOwnerMask(i32 owner) { if (owner >= -1 && owner < 31) return 1 << (1+owner); else return 0; // owner was invalid } /** * Representation of an entity, with the data needed for queries. */ struct EntityData { EntityData() : retainInFog(0), owner(-1), inWorld(0) { } entity_pos_t x, z; entity_pos_t visionRange; u8 retainInFog; // boolean i8 owner; u8 inWorld; // boolean }; cassert(sizeof(EntityData) == 16); /** * Functor for sorting entities by distance from a source point. * It must only be passed entities that are in 'entities' * and are currently in the world. */ struct EntityDistanceOrdering { EntityDistanceOrdering(const std::map& entities, const CFixedVector2D& source) : m_EntityData(entities), m_Source(source) { } bool operator()(entity_id_t a, entity_id_t b) { const EntityData& da = m_EntityData.find(a)->second; const EntityData& db = m_EntityData.find(b)->second; CFixedVector2D vecA = CFixedVector2D(da.x, da.z) - m_Source; CFixedVector2D vecB = CFixedVector2D(db.x, db.z) - m_Source; return (vecA.CompareLength(vecB) < 0); } const std::map& m_EntityData; CFixedVector2D m_Source; private: EntityDistanceOrdering& operator=(const EntityDistanceOrdering&); }; /** * Range manager implementation. * Maintains a list of all entities (and their positions and owners), which is used for * queries. * * LOS implementation is based on the model described in GPG2. * (TODO: would be nice to make it cleverer, so e.g. mountains and walls * can block vision) */ class CCmpRangeManager : public ICmpRangeManager { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeGloballyToMessageType(MT_Create); componentManager.SubscribeGloballyToMessageType(MT_PositionChanged); componentManager.SubscribeGloballyToMessageType(MT_OwnershipChanged); componentManager.SubscribeGloballyToMessageType(MT_Destroy); componentManager.SubscribeToMessageType(MT_Update); componentManager.SubscribeToMessageType(MT_RenderSubmit); // for debug overlays } DEFAULT_COMPONENT_ALLOCATOR(RangeManager) bool m_DebugOverlayEnabled; bool m_DebugOverlayDirty; std::vector m_DebugOverlayLines; SpatialSubdivision m_Subdivision; // Range query state: tag_t m_QueryNext; // next allocated id std::map m_Queries; std::map m_EntityData; // LOS state: bool m_LosRevealAll; ssize_t m_TerrainVerticesPerSide; // Counts of units seeing vertex, per vertex, per player (starting with player 0). // Use u16 to avoid overflows when we have very large (but not infeasibly large) numbers // 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::vector > m_LosPlayerCounts; // 2-bit ELosState per player, starting with player 1 (not 0!) up to player MAX_LOS_PLAYER_ID (inclusive) std::vector m_LosState; static const int MAX_LOS_PLAYER_ID = 16; static std::string GetSchema() { return ""; } virtual void Init(const CSimContext& UNUSED(context), const CParamNode& UNUSED(paramNode)) { m_QueryNext = 1; m_DebugOverlayEnabled = false; m_DebugOverlayDirty = true; // Initialise with bogus values (these will get replaced when // SetBounds is called) ResetSubdivisions(entity_pos_t::FromInt(1), entity_pos_t::FromInt(1)); m_LosRevealAll = false; m_TerrainVerticesPerSide = 0; } virtual void Deinit(const CSimContext& UNUSED(context)) { } virtual void Serialize(ISerializer& UNUSED(serialize)) { // TODO } virtual void Deserialize(const CSimContext& context, const CParamNode& paramNode, IDeserializer& UNUSED(deserialize)) { Init(context, paramNode); } virtual void HandleMessage(const CSimContext& UNUSED(context), const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_Create: { const CMessageCreate& msgData = static_cast (msg); entity_id_t ent = msgData.entity; // Ignore local entities - we shouldn't let them influence anything if (ENTITY_IS_LOCAL(ent)) break; // Ignore non-positional entities CmpPtr cmpPosition(GetSimContext(), ent); if (cmpPosition.null()) break; // The newly-created entity will have owner -1 and position out-of-world // (any initialisation of those values will happen later), so we can just // use the default-constructed EntityData here EntityData entdata; // Store the LOS data, if any CmpPtr cmpVision(GetSimContext(), ent); if (!cmpVision.null()) { entdata.visionRange = cmpVision->GetRange(); entdata.retainInFog = (cmpVision->GetRetainInFog() ? 1 : 0); } // Remember this entity m_EntityData.insert(std::make_pair(ent, entdata)); break; } case MT_PositionChanged: { const CMessagePositionChanged& msgData = static_cast (msg); entity_id_t ent = msgData.entity; std::map::iterator it = m_EntityData.find(ent); // Ignore if we're not already tracking this entity if (it == m_EntityData.end()) break; if (msgData.inWorld) { if (it->second.inWorld) { CFixedVector2D from(it->second.x, it->second.z); CFixedVector2D to(msgData.x, msgData.z); m_Subdivision.Move(ent, from, to); LosMove(it->second.owner, it->second.visionRange, from, to); } else { CFixedVector2D to(msgData.x, msgData.z); m_Subdivision.Add(ent, to); LosAdd(it->second.owner, it->second.visionRange, to); } it->second.inWorld = 1; it->second.x = msgData.x; it->second.z = msgData.z; } else { if (it->second.inWorld) { CFixedVector2D from(it->second.x, it->second.z); m_Subdivision.Remove(ent, from); LosRemove(it->second.owner, it->second.visionRange, from); } it->second.inWorld = 0; it->second.x = entity_pos_t::Zero(); it->second.z = entity_pos_t::Zero(); } break; } case MT_OwnershipChanged: { const CMessageOwnershipChanged& msgData = static_cast (msg); entity_id_t ent = msgData.entity; std::map::iterator it = m_EntityData.find(ent); // Ignore if we're not already tracking this entity if (it == m_EntityData.end()) break; if (it->second.inWorld) { CFixedVector2D pos(it->second.x, it->second.z); LosRemove(it->second.owner, it->second.visionRange, pos); LosAdd(msgData.to, it->second.visionRange, pos); } it->second.owner = msgData.to; break; } case MT_Destroy: { const CMessageDestroy& msgData = static_cast (msg); entity_id_t ent = msgData.entity; std::map::iterator it = m_EntityData.find(ent); // Ignore if we're not already tracking this entity if (it == m_EntityData.end()) break; if (it->second.inWorld) m_Subdivision.Remove(ent, CFixedVector2D(it->second.x, it->second.z)); m_EntityData.erase(it); break; } case MT_Update: { m_DebugOverlayDirty = true; ExecuteActiveQueries(); break; } case MT_RenderSubmit: { const CMessageRenderSubmit& msgData = static_cast (msg); RenderSubmit(msgData.collector); break; } } } virtual void SetBounds(entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, ssize_t vertices) { debug_assert(x0.IsZero() && z0.IsZero()); // don't bother implementing non-zero offsets yet ResetSubdivisions(x1, z1); m_TerrainVerticesPerSide = vertices; m_LosPlayerCounts.clear(); m_LosPlayerCounts.resize(MAX_LOS_PLAYER_ID+1); m_LosState.clear(); m_LosState.resize(vertices*vertices); for (std::map::iterator it = m_EntityData.begin(); it != m_EntityData.end(); ++it) LosAdd(it->second.owner, it->second.visionRange, CFixedVector2D(it->second.x, it->second.z)); } void ResetSubdivisions(entity_pos_t x1, entity_pos_t z1) { // Use 8x8 tile subdivisions // (TODO: find the optimal number instead of blindly guessing) m_Subdivision.Reset(x1, z1, entity_pos_t::FromInt(8*CELL_SIZE)); for (std::map::const_iterator it = m_EntityData.begin(); it != m_EntityData.end(); ++it) { if (it->second.inWorld) m_Subdivision.Add(it->first, CFixedVector2D(it->second.x, it->second.z)); } } virtual tag_t CreateActiveQuery(entity_id_t source, entity_pos_t maxRange, std::vector owners, int requiredInterface) { size_t id = m_QueryNext++; m_Queries[id] = ConstructQuery(source, maxRange, owners, requiredInterface); return (tag_t)id; } virtual void DestroyActiveQuery(tag_t tag) { if (m_Queries.find(tag) == m_Queries.end()) { LOGERROR(L"CCmpRangeManager: DestroyActiveQuery called with invalid tag %d", tag); return; } m_Queries.erase(tag); } virtual void EnableActiveQuery(tag_t tag) { std::map::iterator it = m_Queries.find(tag); if (it == m_Queries.end()) { LOGERROR(L"CCmpRangeManager: EnableActiveQuery called with invalid tag %d", tag); return; } Query& q = it->second; q.enabled = true; } virtual void DisableActiveQuery(tag_t tag) { std::map::iterator it = m_Queries.find(tag); if (it == m_Queries.end()) { LOGERROR(L"CCmpRangeManager: DisableActiveQuery called with invalid tag %d", tag); return; } Query& q = it->second; q.enabled = false; } virtual std::vector ExecuteQuery(entity_id_t source, entity_pos_t maxRange, std::vector owners, int requiredInterface) { PROFILE("ExecuteQuery"); Query q = ConstructQuery(source, maxRange, owners, requiredInterface); std::vector r; CmpPtr cmpSourcePosition(GetSimContext(), q.source); if (cmpSourcePosition.null() || !cmpSourcePosition->IsInWorld()) { // If the source doesn't have a position, then the result is just the empty list return r; } PerformQuery(q, r); // Return the list sorted by distance from the entity CFixedVector2D pos = cmpSourcePosition->GetPosition2D(); std::stable_sort(r.begin(), r.end(), EntityDistanceOrdering(m_EntityData, pos)); return r; } virtual std::vector ResetActiveQuery(tag_t tag) { PROFILE("ResetActiveQuery"); std::vector r; std::map::iterator it = m_Queries.find(tag); if (it == m_Queries.end()) { LOGERROR(L"CCmpRangeManager: ResetActiveQuery called with invalid tag %d", tag); return r; } Query& q = it->second; q.enabled = true; CmpPtr cmpSourcePosition(GetSimContext(), q.source); if (cmpSourcePosition.null() || !cmpSourcePosition->IsInWorld()) { // If the source doesn't have a position, then the result is just the empty list q.lastMatch = r; return r; } PerformQuery(q, r); q.lastMatch = r; // Return the list sorted by distance from the entity CFixedVector2D pos = cmpSourcePosition->GetPosition2D(); std::stable_sort(r.begin(), r.end(), EntityDistanceOrdering(m_EntityData, pos)); return r; } + virtual std::vector GetEntitiesByPlayer(int playerId) + { + std::vector entities; + + u32 ownerMask = CalcOwnerMask(playerId); + + for (std::map::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) + entities.push_back(it->first); + } + + return entities; + } + virtual void SetDebugOverlay(bool enabled) { m_DebugOverlayEnabled = enabled; m_DebugOverlayDirty = true; if (!enabled) m_DebugOverlayLines.clear(); } private: /** * Update all currently-enabled active queries. */ void ExecuteActiveQueries() { PROFILE("ExecuteActiveQueries"); // Store a queue of all messages before sending any, so we can assume // no entities will move until we've finished checking all the ranges std::vector > messages; for (std::map::iterator it = m_Queries.begin(); it != m_Queries.end(); ++it) { Query& q = it->second; if (!q.enabled) continue; CmpPtr cmpSourcePosition(GetSimContext(), q.source); if (cmpSourcePosition.null() || !cmpSourcePosition->IsInWorld()) continue; std::vector r; r.reserve(q.lastMatch.size()); PerformQuery(q, r); // Compute the changes vs the last match std::vector added; std::vector removed; std::set_difference(r.begin(), r.end(), q.lastMatch.begin(), q.lastMatch.end(), std::back_inserter(added)); std::set_difference(q.lastMatch.begin(), q.lastMatch.end(), r.begin(), r.end(), std::back_inserter(removed)); if (added.empty() && removed.empty()) continue; // Return the 'added' list sorted by distance from the entity // (Don't bother sorting 'removed' because they might not even have positions or exist any more) CFixedVector2D pos = cmpSourcePosition->GetPosition2D(); std::stable_sort(added.begin(), added.end(), EntityDistanceOrdering(m_EntityData, pos)); messages.push_back(std::make_pair(q.source, CMessageRangeUpdate(it->first))); messages.back().second.added.swap(added); messages.back().second.removed.swap(removed); it->second.lastMatch.swap(r); } for (size_t i = 0; i < messages.size(); ++i) GetSimContext().GetComponentManager().PostMessage(messages[i].first, messages[i].second); } /** * Returns a list of distinct entity IDs that match the given query, sorted by ID. */ void PerformQuery(const Query& q, std::vector& r) { CmpPtr cmpSourcePosition(GetSimContext(), q.source); if (cmpSourcePosition.null() || !cmpSourcePosition->IsInWorld()) return; CFixedVector2D pos = cmpSourcePosition->GetPosition2D(); // Get a quick list of entities that are potentially in range std::vector ents = m_Subdivision.GetNear(pos, q.maxRange); for (size_t i = 0; i < ents.size(); ++i) { std::map::const_iterator it = m_EntityData.find(ents[i]); debug_assert(it != m_EntityData.end()); // Quick filter to ignore entities with the wrong owner if (!(CalcOwnerMask(it->second.owner) & q.ownersMask)) continue; // Restrict based on precise location if (!it->second.inWorld) continue; int distVsMax = (CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.maxRange); if (distVsMax > 0) continue; // Ignore self if (it->first == q.source) continue; // Ignore if it's missing the required interface if (q.interface && !GetSimContext().GetComponentManager().QueryInterface(it->first, q.interface)) continue; r.push_back(it->first); } } Query ConstructQuery(entity_id_t source, entity_pos_t maxRange, std::vector owners, int requiredInterface) { Query q; q.enabled = false; q.source = source; q.maxRange = maxRange; q.ownersMask = 0; for (size_t i = 0; i < owners.size(); ++i) q.ownersMask |= CalcOwnerMask(owners[i]); q.interface = requiredInterface; return q; } void RenderSubmit(SceneCollector& collector) { if (!m_DebugOverlayEnabled) return; CColor enabledRingColour(0, 1, 0, 1); CColor disabledRingColour(1, 0, 0, 1); CColor rayColour(1, 1, 0, 0.2f); if (m_DebugOverlayDirty) { m_DebugOverlayLines.clear(); for (std::map::iterator it = m_Queries.begin(); it != m_Queries.end(); ++it) { Query& q = it->second; CmpPtr cmpSourcePosition(GetSimContext(), q.source); if (cmpSourcePosition.null() || !cmpSourcePosition->IsInWorld()) continue; CFixedVector2D pos = cmpSourcePosition->GetPosition2D(); // Draw the range circle m_DebugOverlayLines.push_back(SOverlayLine()); m_DebugOverlayLines.back().m_Color = (q.enabled ? enabledRingColour : disabledRingColour); SimRender::ConstructCircleOnGround(GetSimContext(), pos.X.ToFloat(), pos.Y.ToDouble(), q.maxRange.ToFloat(), m_DebugOverlayLines.back(), true); // Draw a ray from the source to each matched entity for (size_t i = 0; i < q.lastMatch.size(); ++i) { CmpPtr cmpTargetPosition(GetSimContext(), q.lastMatch[i]); if (cmpTargetPosition.null() || !cmpTargetPosition->IsInWorld()) continue; CFixedVector2D targetPos = cmpTargetPosition->GetPosition2D(); std::vector coords; coords.push_back(pos.X.ToFloat()); coords.push_back(pos.Y.ToFloat()); coords.push_back(targetPos.X.ToFloat()); coords.push_back(targetPos.Y.ToFloat()); m_DebugOverlayLines.push_back(SOverlayLine()); m_DebugOverlayLines.back().m_Color = rayColour; SimRender::ConstructLineOnGround(GetSimContext(), coords, m_DebugOverlayLines.back(), true); } } m_DebugOverlayDirty = false; } for (size_t i = 0; i < m_DebugOverlayLines.size(); ++i) collector.Submit(&m_DebugOverlayLines[i]); } // LOS implementation: virtual CLosQuerier GetLosQuerier(int player) { return CLosQuerier(player, m_LosState, m_TerrainVerticesPerSide); } virtual ELosVisibility GetLosVisibility(entity_id_t ent, int player) { // (We can't use m_EntityData since this needs to handle LOCAL entities too) CmpPtr cmpPosition(GetSimContext(), ent); if (cmpPosition.null() || !cmpPosition->IsInWorld()) return VIS_HIDDEN; if (m_LosRevealAll) return VIS_VISIBLE; CFixedVector2D pos = cmpPosition->GetPosition2D(); CLosQuerier los(player, m_LosState, m_TerrainVerticesPerSide); int i = (pos.X / (int)CELL_SIZE).ToInt_RoundToNearest(); int j = (pos.Y / (int)CELL_SIZE).ToInt_RoundToNearest(); if (los.IsVisible(i, j)) return VIS_VISIBLE; if (los.IsExplored(i, j)) { CmpPtr cmpVision(GetSimContext(), ent); if (!cmpVision.null() && cmpVision->GetRetainInFog()) return VIS_FOGGED; } return VIS_HIDDEN; } virtual void SetLosRevealAll(bool enabled) { // Eventually we might want this to be a per-player flag (which is why // GetLosRevealAll takes a player argument), but currently it's just a // global setting since I can't quite work out where per-player would be useful m_LosRevealAll = enabled; } virtual bool GetLosRevealAll(int UNUSED(player)) { return m_LosRevealAll; } /** * Update the LOS state of tiles within a given horizontal strip (i0,j) to (i1,j) (inclusive). * amount is +1 or -1. */ inline void LosUpdateStripHelper(u8 owner, ssize_t i0, ssize_t i1, ssize_t j, int amount, std::vector& counts) { for (ssize_t i = i0; i <= i1; ++i) { ssize_t idx = j*m_TerrainVerticesPerSide + i; // Increasing from zero to non-zero - move from unexplored/explored to visible+explored if (counts[idx] == 0 && amount > 0) { m_LosState[idx] |= ((LOS_VISIBLE | LOS_EXPLORED) << (2*(owner-1))); } counts[idx] += amount; // Decreasing from non-zero to zero - move from visible+explored to explored if (counts[idx] == 0 && amount < 0) { m_LosState[idx] &= ~(LOS_VISIBLE << (2*(owner-1))); } } } /** * Update the LOS state of tiles within a given circular range. * Assumes owner is in the valid range. */ inline void LosUpdateHelper(u8 owner, entity_pos_t visionRange, CFixedVector2D pos, int amount) { if (m_TerrainVerticesPerSide == 0) // do nothing if not initialised yet return; PROFILE("LosUpdateHelper"); std::vector& counts = m_LosPlayerCounts.at(owner); // Lazy initialisation of counts: if (counts.empty()) counts.resize(m_TerrainVerticesPerSide*m_TerrainVerticesPerSide); // Compute the circular region as a series of strips. // Rather than quantise pos to vertexes, we do more precise sub-tile computations // to get smoother behaviour as a unit moves rather than jumping a whole tile // at once. // Compute top/bottom coordinates, and clamp to exclude the 1-tile border around the map // (so that we never render the sharp edge of the map) ssize_t j0 = ((pos.Y - visionRange)/(int)CELL_SIZE).ToInt_RoundToInfinity(); ssize_t j1 = ((pos.Y + visionRange)/(int)CELL_SIZE).ToInt_RoundToNegInfinity(); ssize_t j0clamp = std::max(j0, (ssize_t)1); ssize_t j1clamp = std::min(j1, m_TerrainVerticesPerSide-2); entity_pos_t xscale = pos.X / (int)CELL_SIZE; entity_pos_t yscale = pos.Y / (int)CELL_SIZE; entity_pos_t rsquared = (visionRange / (int)CELL_SIZE).Square(); for (ssize_t j = j0clamp; j <= j1clamp; ++j) { // Compute values such that (i - x)^2 + (j - y)^2 <= r^2 // (TODO: is this sqrt slow? can we optimise it?) entity_pos_t di = (rsquared - (entity_pos_t::FromInt(j) - yscale).Square()).Sqrt(); ssize_t i0 = (xscale - di).ToInt_RoundToInfinity(); ssize_t i1 = (xscale + di).ToInt_RoundToNegInfinity(); ssize_t i0clamp = std::max(i0, (ssize_t)1); ssize_t i1clamp = std::min(i1, m_TerrainVerticesPerSide-2); LosUpdateStripHelper(owner, i0clamp, i1clamp, j, amount, counts); } } void LosAdd(i8 owner, entity_pos_t visionRange, CFixedVector2D pos) { if (visionRange.IsZero() || owner <= 0 || owner > MAX_LOS_PLAYER_ID) return; LosUpdateHelper(owner, visionRange, pos, 1); } void LosRemove(i8 owner, entity_pos_t visionRange, CFixedVector2D pos) { if (visionRange.IsZero() || owner <= 0 || owner > MAX_LOS_PLAYER_ID) return; LosUpdateHelper(owner, visionRange, pos, -1); } void LosMove(i8 owner, entity_pos_t visionRange, CFixedVector2D from, CFixedVector2D to) { if (visionRange.IsZero() || owner <= 0 || owner > MAX_LOS_PLAYER_ID) return; // TODO: we could optimise this by only modifying tiles that changed LosRemove(owner, visionRange, from); LosAdd(owner, visionRange, to); } }; REGISTER_COMPONENT_TYPE(RangeManager) Index: ps/trunk/source/simulation2/Simulation2.cpp =================================================================== --- ps/trunk/source/simulation2/Simulation2.cpp (revision 8233) +++ ps/trunk/source/simulation2/Simulation2.cpp (revision 8234) @@ -1,456 +1,457 @@ /* Copyright (C) 2010 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 "precompiled.h" #include "Simulation2.h" #include "simulation2/MessageTypes.h" #include "simulation2/system/ComponentManager.h" #include "simulation2/system/ParamNode.h" #include "simulation2/system/SimContext.h" #include "simulation2/components/ICmpTemplateManager.h" #include "simulation2/components/ICmpCommandQueue.h" #include "lib/file/file_system_util.h" #include "lib/utf8.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Profile.h" #include "ps/Pyrogenesis.h" #include "ps/XML/Xeromyces.h" #include #if MSC_VERSION #include #define getpid _getpid // use the non-deprecated function name #endif class CSimulation2Impl { public: CSimulation2Impl(CUnitManager* unitManager, CTerrain* terrain) : m_SimContext(), m_ComponentManager(m_SimContext), m_EnableOOSLog(false) { m_SimContext.m_UnitManager = unitManager; m_SimContext.m_Terrain = terrain; m_ComponentManager.LoadComponentTypes(); RegisterFileReloadFunc(ReloadChangedFileCB, this); // m_EnableOOSLog = true; // TODO: this should be a command-line flag or similar // (can't call ResetState here since the scripts haven't been loaded yet) } ~CSimulation2Impl() { UnregisterFileReloadFunc(ReloadChangedFileCB, this); } CParamNode LoadXML(const std::wstring& name) { CParamNode ret; VfsPath path = VfsPath(L"simulation/templates/") / name; CXeromyces xero; PSRETURN ok = xero.Load(g_VFS, path); if (ok != PSRETURN_OK) return ret; // (Xeromyces already logged an error) CParamNode::LoadXML(ret, xero); return ret; } void ResetState(bool skipScriptedComponents) { m_ComponentManager.ResetState(); m_DeltaTime = 0.0; m_LastFrameOffset = 0.0f; m_TurnNumber = 0; CParamNode noParam; CComponentManager::ComponentTypeId cid; // Add native system components: m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_TemplateManager, noParam); m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_CommandQueue, noParam); m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_ObstructionManager, noParam); m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_Pathfinder, LoadXML(L"special/pathfinder.xml").GetChild("Pathfinder")); m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_ProjectileManager, noParam); m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_RangeManager, noParam); m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_SoundManager, noParam); m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_Terrain, noParam); m_ComponentManager.AddComponent(SYSTEM_ENTITY, CID_WaterManager, noParam); // Add scripted system components: if (!skipScriptedComponents) { #define LOAD_SCRIPTED_COMPONENT(name) \ cid = m_ComponentManager.LookupCID(name); \ if (cid == CID__Invalid) \ LOGERROR(L"Can't find component type " L##name); \ m_ComponentManager.AddComponent(SYSTEM_ENTITY, cid, noParam) LOAD_SCRIPTED_COMPONENT("GuiInterface"); LOAD_SCRIPTED_COMPONENT("PlayerManager"); LOAD_SCRIPTED_COMPONENT("Timer"); + LOAD_SCRIPTED_COMPONENT("EndGameManager"); #undef LOAD_SCRIPTED_COMPONENT } } bool LoadScripts(const VfsPath& path); LibError ReloadChangedFile(const VfsPath& path); static LibError ReloadChangedFileCB(void* param, const VfsPath& path) { return static_cast(param)->ReloadChangedFile(path); } bool Update(int turnLength, const std::vector& commands); void Interpolate(float frameLength, float frameOffset); void DumpState(); CSimContext m_SimContext; CComponentManager m_ComponentManager; double m_DeltaTime; float m_LastFrameOffset; std::wstring m_StartupScript; CScriptValRooted m_MapSettings; std::set m_LoadedScripts; uint32_t m_TurnNumber; bool m_EnableOOSLog; }; bool CSimulation2Impl::LoadScripts(const VfsPath& path) { VfsPaths pathnames; if (fs_util::GetPathnames(g_VFS, path, L"*.js", pathnames) < 0) return false; bool ok = true; for (VfsPaths::iterator it = pathnames.begin(); it != pathnames.end(); ++it) { std::wstring filename = it->string(); m_LoadedScripts.insert(filename); LOGMESSAGE(L"Loading simulation script '%ls'", filename.c_str()); if (! m_ComponentManager.LoadScript(filename)) ok = false; } return ok; } LibError CSimulation2Impl::ReloadChangedFile(const VfsPath& path) { const std::wstring& filename = path.string(); // Ignore if this file wasn't loaded as a script // (TODO: Maybe we ought to load in any new .js files that are created in the right directories) if (m_LoadedScripts.find(filename) == m_LoadedScripts.end()) return INFO::OK; // If the file doesn't exist (e.g. it was deleted), don't bother loading it since that'll give an error message. // (Also don't bother trying to 'unload' it from the component manager, because that's not possible) if (!FileExists(path)) return INFO::OK; LOGMESSAGE(L"Reloading simulation script '%ls'", filename.c_str()); if (!m_ComponentManager.LoadScript(filename, true)) return ERR::FAIL; return INFO::OK; } bool CSimulation2Impl::Update(int turnLength, const std::vector& commands) { fixed turnLengthFixed = fixed::FromInt(turnLength) / 1000; // TODO: the update process is pretty ugly, with lots of messages and dependencies // between different components. Ought to work out a nicer way to do this. CMessageTurnStart msgTurnStart; m_ComponentManager.BroadcastMessage(msgTurnStart); if (m_TurnNumber == 0) { ScriptInterface& scriptInterface = m_ComponentManager.GetScriptInterface(); CScriptVal ret; scriptInterface.CallFunction(scriptInterface.GetGlobalObject(), "LoadMapSettings", m_MapSettings, ret); if (!m_StartupScript.empty()) m_ComponentManager.GetScriptInterface().LoadScript(L"map startup script", m_StartupScript); } CmpPtr cmpPathfinder(m_SimContext, SYSTEM_ENTITY); if (!cmpPathfinder.null()) cmpPathfinder->FinishAsyncRequests(); CmpPtr cmpCommandQueue(m_SimContext, SYSTEM_ENTITY); if (!cmpCommandQueue.null()) cmpCommandQueue->FlushTurn(commands); // Send all the update phases { CMessageUpdate msgUpdate(turnLengthFixed); m_ComponentManager.BroadcastMessage(msgUpdate); } { CMessageUpdate_MotionFormation msgUpdate(turnLengthFixed); m_ComponentManager.BroadcastMessage(msgUpdate); } { CMessageUpdate_MotionUnit msgUpdate(turnLengthFixed); m_ComponentManager.BroadcastMessage(msgUpdate); } { CMessageUpdate_Final msgUpdate(turnLengthFixed); m_ComponentManager.BroadcastMessage(msgUpdate); } // Clean up any entities destroyed during the simulation update m_ComponentManager.FlushDestroyedComponents(); // if (m_TurnNumber == 0) // m_ComponentManager.GetScriptInterface().DumpHeap(); if (m_EnableOOSLog) DumpState(); ++m_TurnNumber; return true; // TODO: don't bother with bool return } void CSimulation2Impl::Interpolate(float frameLength, float frameOffset) { m_LastFrameOffset = frameOffset; CMessageInterpolate msg(frameLength, frameOffset); m_ComponentManager.BroadcastMessage(msg); } void CSimulation2Impl::DumpState() { PROFILE("DumpState"); std::wstringstream name; name << L"sim_log/" << getpid() << L"/" << std::setw(5) << std::setfill(L'0') << m_TurnNumber << L".txt"; fs::wpath path (psLogDir()/name.str()); CreateDirectories(path.branch_path(), 0700); std::ofstream file (path.external_file_string().c_str(), std::ofstream::out | std::ofstream::trunc); file << "State hash: " << std::hex; std::string hashRaw; m_ComponentManager.ComputeStateHash(hashRaw); for (size_t i = 0; i < hashRaw.size(); ++i) file << std::setfill('0') << std::setw(2) << (int)(unsigned char)hashRaw[i]; file << std::dec << "\n"; file << "\n"; m_ComponentManager.DumpDebugState(file); std::ofstream binfile (change_extension(path, L".dat").external_file_string().c_str(), std::ofstream::out | std::ofstream::trunc | std::ofstream::binary); m_ComponentManager.SerializeState(binfile); } //////////////////////////////////////////////////////////////// CSimulation2::CSimulation2(CUnitManager* unitManager, CTerrain* terrain) : m(new CSimulation2Impl(unitManager, terrain)) { } CSimulation2::~CSimulation2() { delete m; } // Forward all method calls to the appropriate CSimulation2Impl/CComponentManager methods: void CSimulation2::EnableOOSLog() { m->m_EnableOOSLog = true; } entity_id_t CSimulation2::AddEntity(const std::wstring& templateName) { return m->m_ComponentManager.AddEntity(templateName, m->m_ComponentManager.AllocateNewEntity()); } entity_id_t CSimulation2::AddEntity(const std::wstring& templateName, entity_id_t preferredId) { return m->m_ComponentManager.AddEntity(templateName, m->m_ComponentManager.AllocateNewEntity(preferredId)); } entity_id_t CSimulation2::AddLocalEntity(const std::wstring& templateName) { return m->m_ComponentManager.AddEntity(templateName, m->m_ComponentManager.AllocateNewLocalEntity()); } void CSimulation2::DestroyEntity(entity_id_t ent) { m->m_ComponentManager.DestroyComponentsSoon(ent); } void CSimulation2::FlushDestroyedEntities() { m->m_ComponentManager.FlushDestroyedComponents(); } IComponent* CSimulation2::QueryInterface(entity_id_t ent, int iid) const { return m->m_ComponentManager.QueryInterface(ent, iid); } void CSimulation2::PostMessage(entity_id_t ent, const CMessage& msg) const { m->m_ComponentManager.PostMessage(ent, msg); } void CSimulation2::BroadcastMessage(const CMessage& msg) const { m->m_ComponentManager.BroadcastMessage(msg); } const CSimulation2::InterfaceList& CSimulation2::GetEntitiesWithInterface(int iid) { return m->m_ComponentManager.GetEntitiesWithInterface(iid); } const CSimContext& CSimulation2::GetSimContext() const { return m->m_SimContext; } ScriptInterface& CSimulation2::GetScriptInterface() const { return m->m_ComponentManager.GetScriptInterface(); } void CSimulation2::InitGame(const CScriptVal& data) { CScriptVal ret; // ignored GetScriptInterface().CallFunction(GetScriptInterface().GetGlobalObject(), "InitGame", data, ret); } bool CSimulation2::Update(int turnLength) { std::vector commands; return m->Update(turnLength, commands); } bool CSimulation2::Update(int turnLength, const std::vector& commands) { return m->Update(turnLength, commands); } void CSimulation2::Interpolate(float frameLength, float frameOffset) { m->Interpolate(frameLength, frameOffset); } void CSimulation2::RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling) { CMessageRenderSubmit msg(collector, frustum, culling); m->m_ComponentManager.BroadcastMessage(msg); } float CSimulation2::GetLastFrameOffset() const { return m->m_LastFrameOffset; } bool CSimulation2::LoadScripts(const VfsPath& path) { return m->LoadScripts(path); } bool CSimulation2::LoadDefaultScripts() { return ( m->LoadScripts(L"simulation/components/interfaces/") && m->LoadScripts(L"simulation/helpers/") && m->LoadScripts(L"simulation/components/") ); } void CSimulation2::SetStartupScript(const std::wstring& code) { m->m_StartupScript = code; } const std::wstring& CSimulation2::GetStartupScript() { return m->m_StartupScript; } void CSimulation2::SetMapSettings(const utf16string& settings) { m->m_MapSettings = m->m_ComponentManager.GetScriptInterface().ParseJSON(settings); } std::string CSimulation2::GetMapSettings() { return m->m_ComponentManager.GetScriptInterface().StringifyJSON(m->m_MapSettings.get()); } LibError CSimulation2::ReloadChangedFile(const VfsPath& path) { return m->ReloadChangedFile(path); } void CSimulation2::ResetState(bool skipGui) { m->ResetState(skipGui); } bool CSimulation2::ComputeStateHash(std::string& outHash) { return m->m_ComponentManager.ComputeStateHash(outHash); } bool CSimulation2::DumpDebugState(std::ostream& stream) { return m->m_ComponentManager.DumpDebugState(stream); } bool CSimulation2::SerializeState(std::ostream& stream) { return m->m_ComponentManager.SerializeState(stream); } bool CSimulation2::DeserializeState(std::istream& stream) { // TODO: need to make sure the required SYSTEM_ENTITY components get constructed return m->m_ComponentManager.DeserializeState(stream); } std::string CSimulation2::GenerateSchema() { return m->m_ComponentManager.GenerateSchema(); } Index: ps/trunk/binaries/data/mods/public/maps/scenarios/Pathfinding_terrain_demo.xml =================================================================== --- ps/trunk/binaries/data/mods/public/maps/scenarios/Pathfinding_terrain_demo.xml (revision 8233) +++ ps/trunk/binaries/data/mods/public/maps/scenarios/Pathfinding_terrain_demo.xml (revision 8234) @@ -1,262 +1,263 @@ default default 19.3049 150 8 0.45 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 \ No newline at end of file Index: ps/trunk/binaries/data/mods/public/maps/scenarios/Pathfinding_demo.xml =================================================================== --- ps/trunk/binaries/data/mods/public/maps/scenarios/Pathfinding_demo.xml (revision 8233) +++ ps/trunk/binaries/data/mods/public/maps/scenarios/Pathfinding_demo.xml (revision 8234) @@ -1,635 +1,636 @@ default default 5 150 8 0.45 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 \ No newline at end of file Index: ps/trunk/binaries/data/mods/public/maps/scenarios/Units_demo.xml =================================================================== --- ps/trunk/binaries/data/mods/public/maps/scenarios/Units_demo.xml (revision 8233) +++ ps/trunk/binaries/data/mods/public/maps/scenarios/Units_demo.xml (revision 8234) @@ -1,94 +1,95 @@ default default 5 150 8 0.45 0 Index: ps/trunk/binaries/data/mods/public/gui/page_summary.xml =================================================================== --- ps/trunk/binaries/data/mods/public/gui/page_summary.xml (nonexistent) +++ ps/trunk/binaries/data/mods/public/gui/page_summary.xml (revision 8234) @@ -0,0 +1,9 @@ + + + common/setup.xml + common/styles.xml + common/sprite1.xml + common/init.xml + summary/summary.xml + common/global.xml + Index: ps/trunk/binaries/data/mods/public/gui/common/functions_global_object.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/common/functions_global_object.js (revision 8233) +++ ps/trunk/binaries/data/mods/public/gui/common/functions_global_object.js (revision 8234) @@ -1,35 +1,35 @@ /* DESCRIPTION : Contains global GUI functions, which will later be accessible from every GUI script/file. NOTES : So far, only the message box-related functions are implemented. */ // ******************************************* // messageBox // ******************************************* -// @params: int mbWidth, int mbHeight, string mbMessage, string mbTitle, int mbMode, arr mbButtonCaptions +// @params: int mbWidth, int mbHeight, string mbMessage, string mbTitle, int mbMode, arr mbButtonCaptions, arr mbButtonsCode // @return: void // @desc: Displays a new modal message box. // ******************************************* function messageBox (mbWidth, mbHeight, mbMessage, mbTitle, mbMode, mbButtonCaptions, mbButtonsCode) { Engine.PushGuiPage("page_msgbox.xml", { width: mbWidth, height: mbHeight, message: mbMessage, title: mbTitle, mode: mbMode, buttonCaptions: mbButtonCaptions, buttonCode: mbButtonsCode }); } // ==================================================================== function updateFPS() { getGUIObjectByName("fpsCounter").caption = "FPS: " + getFPS(); } // ==================================================================== Index: ps/trunk/binaries/data/mods/public/gui/session_new/session.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session_new/session.js (revision 8233) +++ ps/trunk/binaries/data/mods/public/gui/session_new/session.js (revision 8234) @@ -1,186 +1,240 @@ // Network Mode var g_IsNetworked = false; // Cache the basic player data (name, civ, color) var g_Players = []; var g_PlayerAssignments = { "local": { "name": "You", "player": 1 } }; // Cache dev-mode settings that are frequently or widely used var g_DevSettings = { controlAll: false }; // Indicate when one of the current player's training queues is blocked // (this is used to support population counter blinking) var g_IsTrainingQueueBlocked = false; // Cache EntityStates var g_EntityStates = {}; // {id:entState} +// Whether the player has lost/won and reached the end of their game +var g_GameEnded = false; + function GetEntityState(entId) { if (!(entId in g_EntityStates)) { var entState = Engine.GuiInterfaceCall("GetEntityState", entId); g_EntityStates[entId] = entState; } return g_EntityStates[entId]; } // Cache TemplateData var g_TemplateData = {}; // {id:template} function GetTemplateData(templateName) { if (!(templateName in g_TemplateData)) { var template = Engine.GuiInterfaceCall("GetTemplateData", templateName); g_TemplateData[templateName] = template; } return g_TemplateData[templateName]; } // Init function init(initData, hotloadData) { if (hotloadData) { g_Selection.selected = hotloadData.selection; } else { // Starting for the first time: startMusic(); } if (initData) { g_IsNetworked = initData.isNetworked; // Set network mode g_PlayerAssignments = initData.playerAssignments; g_Players = getPlayerData(initData.playerAssignments); // Cache the player data } else // Needed for autostart loading option { g_Players = getPlayerData(null); } getGUIObjectByName("civIcon").sprite = g_Players[Engine.GetPlayerID()].civ+"Icon"; cacheMenuObjects(); onSimulationUpdate(); } function leaveGame() { + var simState = Engine.GuiInterfaceCall("GetSimulationState"); + var playerState = simState.players[Engine.GetPlayerID()]; + + var gameResult; + if (playerState.state == "won") + { + gameResult = "You have won the battle!"; + } + else if (playerState.state == "defeated") + { + gameResult = "You have been defeated..."; + } + else // "active" + { + gameResult = "You have abandoned the game."; + + // Tell other players that we have given up and + // been defeated + Engine.PostNetworkCommand({ + "type": "defeat-player", + "playerId": Engine.GetPlayerID() + }); + + } + stopMusic(); endGame(); - Engine.SwitchGuiPage("page_pregame.xml"); + + Engine.SwitchGuiPage("page_summary.xml", { "gameResult" : gameResult }); + } // Return some data that we'll use when hotloading this file after changes function getHotloadData() { return { selection: g_Selection.selected }; } function onTick() { + checkPlayerState(); + while (true) { var message = Engine.PollNetworkClient(); if (!message) break; handleNetMessage(message); } g_DevSettings.controlAll = getGUIObjectByName("devControlAll").checked; // TODO: at some point this controlAll needs to disable the simulation code's // player checks (once it has some player checks) updateCursor(); // If the selection changed, we need to regenerate the sim display if (g_Selection.dirty) { onSimulationUpdate(); // Display rally points for selected buildings Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": g_Selection.toList() }); } // Run timers updateTimers(); // When training is blocked, flash population (alternates colour every 500msec) if (g_IsTrainingQueueBlocked && (Date.now() % 1000) < 500) getGUIObjectByName("resourcePop").textcolor = "255 165 0"; else getGUIObjectByName("resourcePop").textcolor = "white"; } +function checkPlayerState() +{ + var simState = Engine.GuiInterfaceCall("GetSimulationState"); + var playerState = simState.players[Engine.GetPlayerID()]; + + if (!g_GameEnded) + { + if (playerState.state == "defeated") + { + g_GameEnded = true; + messageBox(400, 200, "You have been defeated... Do you want to leave the game now?", + "Defeat", 0, ["Yes", "No!"], [leaveGame, null]); + } + else if (playerState.state == "won") + { + g_GameEnded = true; + messageBox(400, 200, "You have won the battle! Do you want to leave the game now?", + "Victory", 0, ["Yes", "No!"], [leaveGame, null]); + } + } +} + function onSimulationUpdate() { g_Selection.dirty = false; g_EntityStates = {}; g_TemplateData = {}; var simState = Engine.GuiInterfaceCall("GetSimulationState"); // If we're called during init when the game is first loading, there will be no simulation yet, so do nothing if (!simState) return; handleNotifications(); updateDebug(simState); updatePlayerDisplay(simState); updateSelectionDetails(); } function updateDebug(simState) { var debug = getGUIObjectByName("debug"); if (getGUIObjectByName("devDisplayState").checked) { debug.hidden = false; } else { debug.hidden = true; return; } var text = uneval(simState); var selection = g_Selection.toList(); if (selection.length) { var entState = GetEntityState(selection[0]); if (entState) { var template = GetTemplateData(entState.template); text += "\n\n" + uneval(entState) + "\n\n" + uneval(template); } } debug.caption = text; } function updatePlayerDisplay(simState) { var playerState = simState.players[Engine.GetPlayerID()]; if (!playerState) return; getGUIObjectByName("resourceFood").caption = playerState.resourceCounts.food; getGUIObjectByName("resourceWood").caption = playerState.resourceCounts.wood; getGUIObjectByName("resourceStone").caption = playerState.resourceCounts.stone; getGUIObjectByName("resourceMetal").caption = playerState.resourceCounts.metal; getGUIObjectByName("resourcePop").caption = playerState.popCount + "/" + playerState.popLimit; g_IsTrainingQueueBlocked = playerState.trainingQueueBlocked; } Index: ps/trunk/binaries/data/mods/public/gui/summary/summary.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/summary/summary.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/gui/summary/summary.js (revision 8234) @@ -0,0 +1,4 @@ +function init(data) +{ + getGUIObjectByName("summaryText").caption = data.gameResult; +} Index: ps/trunk/binaries/data/mods/public/gui/summary/summary.xml =================================================================== --- ps/trunk/binaries/data/mods/public/gui/summary/summary.xml (nonexistent) +++ ps/trunk/binaries/data/mods/public/gui/summary/summary.xml (revision 8234) @@ -0,0 +1,38 @@ + + + + + +