Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure.xml (revision 9969) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure.xml (revision 9970) @@ -1,78 +1,78 @@ 10.0 42.0 10.0 0 0 - standard - allied + land + own 0 0 10 0 0 0 0 0.0 4.0 9.8 corpse 0 false true Structure Structure ConquestCritical structure true true true true true false false interface/complete/building/complete_universal.xml attack/destruction/building_collapse_large.xml 6.0 0.6 12.0 40 true false false true Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_dock.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_dock.xml (revision 9969) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_dock.xml (revision 9970) @@ -1,59 +1,61 @@ 15.0 40.0 20.0 + own ally neutral + shore Dock 5 250 300 8.0 2500 Dock Build upon a shoreline to construct naval vessels and to open sea trade. Town structures/dock.png true food wood stone metal interface/complete/building/complete_dock.xml attack/destruction/building_collapse_large.xml units/{civ}_ship_fishing units/{civ}_ship_merchant 40 structures/fndn_4x4.xml Index: ps/trunk/source/simulation2/components/CCmpPathfinder_Common.h =================================================================== --- ps/trunk/source/simulation2/components/CCmpPathfinder_Common.h (revision 9969) +++ ps/trunk/source/simulation2/components/CCmpPathfinder_Common.h (revision 9970) @@ -1,286 +1,303 @@ /* Copyright (C) 2011 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_CCMPPATHFINDER_COMMON #define INCLUDED_CCMPPATHFINDER_COMMON /** * @file * Declares CCmpPathfinder, whose implementation is split into multiple source files, * and provides common code needed for more than one of those files. * CCmpPathfinder includes two pathfinding algorithms (one tile-based, one vertex-based) * with some shared state and functionality, so the code is split into * CCmpPathfinder_Vertex.cpp, CCmpPathfinder_Tile.cpp and CCmpPathfinder.cpp */ #include "simulation2/system/Component.h" #include "ICmpPathfinder.h" #include "graphics/Overlay.h" #include "graphics/Terrain.h" #include "maths/MathUtil.h" #include "simulation2/helpers/Geometry.h" #include "simulation2/helpers/Grid.h" class PathfinderOverlay; class SceneCollector; struct PathfindTile; #ifdef NDEBUG #define PATHFIND_DEBUG 0 #else #define PATHFIND_DEBUG 1 #endif /* * For efficient pathfinding we want to try hard to minimise the per-tile search cost, * so we precompute the tile passability flags and movement costs for the various different * types of unit. * We also want to minimise memory usage (there can easily be 100K tiles so we don't want * to store many bytes for each). * * To handle passability efficiently, we have a small number of passability classes * (e.g. "infantry", "ship"). Each unit belongs to a single passability class, and * uses that for all its pathfinding. * Passability is determined by water depth, terrain slope, forestness, buildingness. * We need at least one bit per class per tile to represent passability. * * We use a separate bit to indicate building obstructions (instead of folding it into * the class passabilities) so that it can be ignored when doing the accurate short paths. * We use another bit to indicate tiles near obstructions that block construction, * for the AI to plan safe building spots. * * To handle movement costs, we have an arbitrary number of unit cost classes (e.g. "infantry", "camel"), * and a small number of terrain cost classes (e.g. "grass", "steep grass", "road", "sand"), * and a cost mapping table between the classes (e.g. camels are fast on sand). * We need log2(|terrain cost classes|) bits per tile to represent costs. * * We could have one passability bitmap per class, and another array for cost classes, * but instead (for no particular reason) we'll pack them all into a single u16 array. * * We handle dynamic updates currently by recomputing the entire array, which is stupid; * it should only bother updating the region that has changed. */ class PathfinderPassability { public: PathfinderPassability(ICmpPathfinder::pass_class_t mask, const CParamNode& node) : m_Mask(mask) { if (node.GetChild("MinWaterDepth").IsOk()) m_MinDepth = node.GetChild("MinWaterDepth").ToFixed(); else m_MinDepth = std::numeric_limits::min(); if (node.GetChild("MaxWaterDepth").IsOk()) m_MaxDepth = node.GetChild("MaxWaterDepth").ToFixed(); else m_MaxDepth = std::numeric_limits::max(); if (node.GetChild("MaxTerrainSlope").IsOk()) m_MaxSlope = node.GetChild("MaxTerrainSlope").ToFixed(); else m_MaxSlope = std::numeric_limits::max(); + + if (node.GetChild("MinShoreDistance").IsOk()) + m_MinShore = node.GetChild("MinShoreDistance").ToFixed(); + else + m_MinShore = std::numeric_limits::min(); + + if (node.GetChild("MaxShoreDistance").IsOk()) + m_MaxShore = node.GetChild("MaxShoreDistance").ToFixed(); + else + m_MaxShore = std::numeric_limits::max(); + } - bool IsPassable(fixed waterdepth, fixed steepness) + bool IsPassable(fixed waterdepth, fixed steepness, fixed shoredist) { - return ((m_MinDepth <= waterdepth && waterdepth <= m_MaxDepth) && (steepness < m_MaxSlope)); + return ((m_MinDepth <= waterdepth && waterdepth <= m_MaxDepth) && (steepness < m_MaxSlope) && (m_MinShore <= shoredist && shoredist <= m_MaxShore)); } ICmpPathfinder::pass_class_t m_Mask; private: fixed m_MinDepth; fixed m_MaxDepth; fixed m_MaxSlope; + fixed m_MinShore; + fixed m_MaxShore; }; typedef u16 TerrainTile; // 1 bit for pathfinding obstructions, // 1 bit for construction obstructions (used by AI), // PASS_CLASS_BITS for terrain passability (allowing PASS_CLASS_BITS classes), // COST_CLASS_BITS for movement costs (allowing 2^COST_CLASS_BITS classes) const int PASS_CLASS_BITS = 10; const int COST_CLASS_BITS = 16 - (PASS_CLASS_BITS + 2); #define IS_TERRAIN_PASSABLE(item, classmask) (((item) & (classmask)) == 0) #define IS_PASSABLE(item, classmask) (((item) & ((classmask) | 1)) == 0) #define GET_COST_CLASS(item) ((item) >> (PASS_CLASS_BITS + 2)) #define COST_CLASS_MASK(id) ( (TerrainTile) ((id) << (PASS_CLASS_BITS + 2)) ) typedef SparseGrid PathfindTileGrid; struct AsyncLongPathRequest { u32 ticket; entity_pos_t x0; entity_pos_t z0; ICmpPathfinder::Goal goal; ICmpPathfinder::pass_class_t passClass; ICmpPathfinder::cost_class_t costClass; entity_id_t notify; }; struct AsyncShortPathRequest { u32 ticket; entity_pos_t x0; entity_pos_t z0; entity_pos_t r; entity_pos_t range; ICmpPathfinder::Goal goal; ICmpPathfinder::pass_class_t passClass; bool avoidMovingUnits; entity_id_t group; entity_id_t notify; }; /** * Implementation of ICmpPathfinder */ class CCmpPathfinder : public ICmpPathfinder { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeToMessageType(MT_Update); componentManager.SubscribeToMessageType(MT_RenderSubmit); // for debug overlays componentManager.SubscribeToMessageType(MT_TerrainChanged); componentManager.SubscribeToMessageType(MT_TurnStart); } DEFAULT_COMPONENT_ALLOCATOR(Pathfinder) // Template state: std::map m_PassClassMasks; std::vector m_PassClasses; std::map m_TerrainCostClassTags; std::map m_UnitCostClassTags; std::vector > m_MoveCosts; // costs[unitClass][terrainClass] std::vector > m_MoveSpeeds; // speeds[unitClass][terrainClass] // Dynamic state: std::vector m_AsyncLongPathRequests; std::vector m_AsyncShortPathRequests; u32 m_NextAsyncTicket; // unique IDs for asynchronous path requests u16 m_SameTurnMovesCount; // current number of same turn moves we have processed this turn // Lazily-constructed dynamic state (not serialized): u16 m_MapSize; // tiles per side Grid* m_Grid; // terrain/passability information Grid* m_ObstructionGrid; // cached obstruction information (TODO: we shouldn't bother storing this, it's redundant with LSBs of m_Grid) bool m_TerrainDirty; // indicates if m_Grid has been updated since terrain changed // For responsiveness we will process some moves in the same turn they were generated in u16 m_MaxSameTurnMoves; // max number of moves that can be created and processed in the same turn // Debugging - output from last pathfind operation: PathfindTileGrid* m_DebugGrid; u32 m_DebugSteps; Path* m_DebugPath; PathfinderOverlay* m_DebugOverlay; pass_class_t m_DebugPassClass; std::vector m_DebugOverlayShortPathLines; static std::string GetSchema() { return ""; } virtual void Init(const CParamNode& paramNode); virtual void Deinit(); virtual void Serialize(ISerializer& serialize); virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize); virtual void HandleMessage(const CMessage& msg, bool global); virtual pass_class_t GetPassabilityClass(const std::string& name); virtual std::map GetPassabilityClasses(); virtual cost_class_t GetCostClass(const std::string& name); virtual const Grid& GetPassabilityGrid(); virtual void ComputePath(entity_pos_t x0, entity_pos_t z0, const Goal& goal, pass_class_t passClass, cost_class_t costClass, Path& ret); virtual u32 ComputePathAsync(entity_pos_t x0, entity_pos_t z0, const Goal& goal, pass_class_t passClass, cost_class_t costClass, entity_id_t notify); virtual void ComputeShortPath(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t r, entity_pos_t range, const Goal& goal, pass_class_t passClass, Path& ret); virtual u32 ComputeShortPathAsync(entity_pos_t x0, entity_pos_t z0, entity_pos_t r, entity_pos_t range, const Goal& goal, pass_class_t passClass, bool avoidMovingUnits, entity_id_t controller, entity_id_t notify); virtual void SetDebugPath(entity_pos_t x0, entity_pos_t z0, const Goal& goal, pass_class_t passClass, cost_class_t costClass); virtual void ResetDebugPath(); virtual void SetDebugOverlay(bool enabled); virtual fixed GetMovementSpeed(entity_pos_t x0, entity_pos_t z0, cost_class_t costClass); virtual CFixedVector2D GetNearestPointOnGoal(CFixedVector2D pos, const Goal& goal); virtual bool CheckMovement(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, pass_class_t passClass); + virtual bool CheckUnitPlacement(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r, pass_class_t passClass); + + virtual bool CheckBuildingPlacement(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t a, entity_pos_t w, entity_pos_t h, entity_id_t id, pass_class_t passClass); + virtual void FinishAsyncRequests(); void ProcessLongRequests(const std::vector& longRequests); void ProcessShortRequests(const std::vector& shortRequests); virtual void ProcessSameTurnMoves(); /** * Returns the tile containing the given position */ void NearestTile(entity_pos_t x, entity_pos_t z, u16& i, u16& j) { i = clamp((x / (int)CELL_SIZE).ToInt_RoundToZero(), 0, m_MapSize-1); j = clamp((z / (int)CELL_SIZE).ToInt_RoundToZero(), 0, m_MapSize-1); } /** * Returns the position of the center of the given tile */ static void TileCenter(u16 i, u16 j, entity_pos_t& x, entity_pos_t& z) { x = entity_pos_t::FromInt(i*(int)CELL_SIZE + CELL_SIZE/2); z = entity_pos_t::FromInt(j*(int)CELL_SIZE + CELL_SIZE/2); } static fixed DistanceToGoal(CFixedVector2D pos, const CCmpPathfinder::Goal& goal); /** * Regenerates the grid based on the current obstruction list, if necessary */ void UpdateGrid(); void RenderSubmit(SceneCollector& collector); }; #endif // INCLUDED_CCMPPATHFINDER_COMMON Index: ps/trunk/source/simulation2/components/CCmpFootprint.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpFootprint.cpp (revision 9969) +++ ps/trunk/source/simulation2/components/CCmpFootprint.cpp (revision 9970) @@ -1,229 +1,255 @@ -/* Copyright (C) 2010 Wildfire Games. +/* Copyright (C) 2011 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 "ICmpFootprint.h" -#include "ICmpObstruction.h" -#include "ICmpObstructionManager.h" -#include "ICmpPosition.h" +#include "simulation2/components/ICmpObstruction.h" +#include "simulation2/components/ICmpObstructionManager.h" +#include "simulation2/components/ICmpPathfinder.h" +#include "simulation2/components/ICmpPosition.h" +#include "simulation2/components/ICmpUnitMotion.h" #include "simulation2/MessageTypes.h" +#include "graphics/Terrain.h" // For CELL_SIZE #include "maths/FixedVector2D.h" class CCmpFootprint : public ICmpFootprint { public: static void ClassInit(CComponentManager& UNUSED(componentManager)) { } DEFAULT_COMPONENT_ALLOCATOR(Footprint) EShape m_Shape; entity_pos_t m_Size0; // width/radius entity_pos_t m_Size1; // height/radius entity_pos_t m_Height; static std::string GetSchema() { return "Approximation of the entity's shape, for collision detection and outline rendering. " "Shapes are flat horizontal squares or circles, extended vertically to a given height." "" "" "0.0" "" "" "" "0.0" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" ""; } virtual void Init(const CParamNode& paramNode) { if (paramNode.GetChild("Square").IsOk()) { m_Shape = SQUARE; m_Size0 = paramNode.GetChild("Square").GetChild("@width").ToFixed(); m_Size1 = paramNode.GetChild("Square").GetChild("@depth").ToFixed(); } else if (paramNode.GetChild("Circle").IsOk()) { m_Shape = CIRCLE; m_Size0 = m_Size1 = paramNode.GetChild("Circle").GetChild("@radius").ToFixed(); } else { // Error - pick some default m_Shape = CIRCLE; m_Size0 = m_Size1 = entity_pos_t::FromInt(1); } m_Height = paramNode.GetChild("Height").ToFixed(); } virtual void Deinit() { } virtual void Serialize(ISerializer& UNUSED(serialize)) { // No dynamic state to serialize } virtual void Deserialize(const CParamNode& paramNode, IDeserializer& UNUSED(deserialize)) { Init(paramNode); } virtual void GetShape(EShape& shape, entity_pos_t& size0, entity_pos_t& size1, entity_pos_t& height) { shape = m_Shape; size0 = m_Size0; size1 = m_Size1; height = m_Height; } virtual CFixedVector3D PickSpawnPoint(entity_id_t spawned) { // Try to find a free space around the building's footprint. // (Note that we use the footprint, not the obstruction shape - this might be a bit dodgy // because the footprint might be inside the obstruction, but it hopefully gives us a nicer // shape.) - CFixedVector3D error(fixed::FromInt(-1), fixed::FromInt(-1), fixed::FromInt(-1)); + const CFixedVector3D error(fixed::FromInt(-1), fixed::FromInt(-1), fixed::FromInt(-1)); CmpPtr cmpPosition(GetSimContext(), GetEntityId()); if (cmpPosition.null() || !cmpPosition->IsInWorld()) return error; CmpPtr cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY); if (cmpObstructionManager.null()) return error; entity_pos_t spawnedRadius; ICmpObstructionManager::tag_t spawnedTag; CmpPtr cmpSpawnedObstruction(GetSimContext(), spawned); if (!cmpSpawnedObstruction.null()) { spawnedRadius = cmpSpawnedObstruction->GetUnitRadius(); spawnedTag = cmpSpawnedObstruction->GetObstruction(); } // else use zero radius - // The spawn point should be far enough from this footprint to fit the unit, plus a little gap - entity_pos_t clearance = spawnedRadius + entity_pos_t::FromInt(2); + // Get passability class from UnitMotion + CmpPtr cmpUnitMotion(GetSimContext(), spawned); + if (cmpUnitMotion.null()) + return error; + ICmpPathfinder::pass_class_t spawnedPass = cmpUnitMotion->GetPassabilityClass(); + CmpPtr cmpPathfinder(GetSimContext(), SYSTEM_ENTITY); + if (cmpPathfinder.null()) + return error; + CFixedVector2D initialPos = cmpPosition->GetPosition2D(); entity_angle_t initialAngle = cmpPosition->GetRotation().Y; + // Max spawning distance in tiles + const size_t maxSpawningDistance = 4; + if (m_Shape == CIRCLE) { - entity_pos_t radius = m_Size0 + clearance; - - // Try equally-spaced points around the circle, starting from the front and expanding outwards in alternating directions - const ssize_t numPoints = 31; - for (ssize_t i = 0; i < (numPoints+1)/2; i = (i > 0 ? -i : 1-i)) // [0, +1, -1, +2, -2, ... (np-1)/2, -(np-1)/2] + // Expand outwards from foundation + for (size_t dist = 0; dist <= maxSpawningDistance; ++dist) { - entity_angle_t angle = initialAngle + (entity_angle_t::Pi()*2).Multiply(entity_angle_t::FromInt(i)/(int)numPoints); + // The spawn point should be far enough from this footprint to fit the unit, plus a little gap + entity_pos_t clearance = spawnedRadius + entity_pos_t::FromInt(2 + CELL_SIZE*dist); + entity_pos_t radius = m_Size0 + clearance; + + // Try equally-spaced points around the circle in alternating directions, starting from the front + const ssize_t numPoints = 31 + 2*dist; + for (ssize_t i = 0; i < (numPoints+1)/2; i = (i > 0 ? -i : 1-i)) // [0, +1, -1, +2, -2, ... (np-1)/2, -(np-1)/2] + { + entity_angle_t angle = initialAngle + (entity_angle_t::Pi()*2).Multiply(entity_angle_t::FromInt(i)/(int)numPoints); - fixed s, c; - sincos_approx(angle, s, c); + fixed s, c; + sincos_approx(angle, s, c); - CFixedVector3D pos (initialPos.X + s.Multiply(radius), fixed::Zero(), initialPos.Y + c.Multiply(radius)); + CFixedVector3D pos (initialPos.X + s.Multiply(radius), fixed::Zero(), initialPos.Y + c.Multiply(radius)); - SkipTagObstructionFilter filter(spawnedTag); // ignore collisions with the spawned entity - if (!cmpObstructionManager->TestUnitShape(filter, pos.X, pos.Z, spawnedRadius, NULL)) - return pos; // this position is okay, so return it + SkipTagObstructionFilter filter(spawnedTag); // ignore collisions with the spawned entity + if (cmpPathfinder->CheckUnitPlacement(filter, pos.X, pos.Z, spawnedRadius, spawnedPass)) + return pos; // this position is okay, so return it + } } } else { fixed s, c; sincos_approx(initialAngle, s, c); - for (size_t edge = 0; edge < 4; ++edge) + // Expand outwards from foundation + for (size_t dist = 0; dist <= maxSpawningDistance; ++dist) { - // Try equally-spaced points along the edge, starting from the middle and expanding outwards in alternating directions - const ssize_t numPoints = 9; + // The spawn point should be far enough from this footprint to fit the unit, plus a little gap + entity_pos_t clearance = spawnedRadius + entity_pos_t::FromInt(2 + CELL_SIZE*dist); - // Compute the direction and length of the current edge - CFixedVector2D dir; - fixed sx, sy; - switch (edge) + for (size_t edge = 0; edge < 4; ++edge) { - case 0: - dir = CFixedVector2D(c, -s); - sx = m_Size0; - sy = m_Size1; - break; - case 1: - dir = CFixedVector2D(-s, -c); - sx = m_Size1; - sy = m_Size0; - break; - case 2: - dir = CFixedVector2D(s, c); - sx = m_Size1; - sy = m_Size0; - break; - case 3: - dir = CFixedVector2D(-c, s); - sx = m_Size0; - sy = m_Size1; - break; - } - CFixedVector2D center = initialPos - dir.Perpendicular().Multiply(sy/2 + clearance); - dir = dir.Multiply((sx + clearance*2) / (int)(numPoints-1)); + // Try equally-spaced points along the edge in alternating directions, starting from the middle + const ssize_t numPoints = 9 + 2*dist; - for (ssize_t i = 0; i < (numPoints+1)/2; i = (i > 0 ? -i : 1-i)) // [0, +1, -1, +2, -2, ... (np-1)/2, -(np-1)/2] - { - CFixedVector2D pos (center + dir*i); - - SkipTagObstructionFilter filter(spawnedTag); // ignore collisions with the spawned entity - if (!cmpObstructionManager->TestUnitShape(filter, pos.X, pos.Y, spawnedRadius, NULL)) - return CFixedVector3D(pos.X, fixed::Zero(), pos.Y); // this position is okay, so return it + // Compute the direction and length of the current edge + CFixedVector2D dir; + fixed sx, sy; + switch (edge) + { + case 0: + dir = CFixedVector2D(c, -s); + sx = m_Size0; + sy = m_Size1; + break; + case 1: + dir = CFixedVector2D(-s, -c); + sx = m_Size1; + sy = m_Size0; + break; + case 2: + dir = CFixedVector2D(s, c); + sx = m_Size1; + sy = m_Size0; + break; + case 3: + dir = CFixedVector2D(-c, s); + sx = m_Size0; + sy = m_Size1; + break; + } + CFixedVector2D center = initialPos - dir.Perpendicular().Multiply(sy/2 + clearance); + dir = dir.Multiply((sx + clearance*2) / (int)(numPoints-1)); + + for (ssize_t i = 0; i < (numPoints+1)/2; i = (i > 0 ? -i : 1-i)) // [0, +1, -1, +2, -2, ... (np-1)/2, -(np-1)/2] + { + CFixedVector2D pos (center + dir*i); + + SkipTagObstructionFilter filter(spawnedTag); // ignore collisions with the spawned entity + if (cmpPathfinder->CheckUnitPlacement(filter, pos.X, pos.Y, spawnedRadius, spawnedPass)) + return CFixedVector3D(pos.X, fixed::Zero(), pos.Y); // this position is okay, so return it + } } } } return error; } }; REGISTER_COMPONENT_TYPE(Footprint) Index: ps/trunk/source/simulation2/components/CCmpTerritoryManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpTerritoryManager.cpp (revision 9969) +++ ps/trunk/source/simulation2/components/CCmpTerritoryManager.cpp (revision 9970) @@ -1,711 +1,720 @@ /* Copyright (C) 2011 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 "ICmpTerritoryManager.h" #include "graphics/Overlay.h" #include "graphics/Terrain.h" #include "graphics/TextureManager.h" #include "maths/MathUtil.h" #include "maths/Vector2D.h" #include "ps/Overlay.h" #include "renderer/Renderer.h" #include "renderer/Scene.h" #include "renderer/TerrainOverlay.h" #include "simulation2/MessageTypes.h" #include "simulation2/components/ICmpObstruction.h" #include "simulation2/components/ICmpObstructionManager.h" #include "simulation2/components/ICmpOwnership.h" #include "simulation2/components/ICmpPathfinder.h" #include "simulation2/components/ICmpPlayer.h" #include "simulation2/components/ICmpPlayerManager.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpSettlement.h" #include "simulation2/components/ICmpTerrain.h" #include "simulation2/components/ICmpTerritoryInfluence.h" #include "simulation2/helpers/Geometry.h" #include "simulation2/helpers/Grid.h" #include "simulation2/helpers/PriorityQueue.h" #include "simulation2/helpers/Render.h" class CCmpTerritoryManager; class TerritoryOverlay : public TerrainOverlay { NONCOPYABLE(TerritoryOverlay); public: CCmpTerritoryManager& m_TerritoryManager; TerritoryOverlay(CCmpTerritoryManager& manager) : m_TerritoryManager(manager) { } virtual void StartRender(); virtual void ProcessTile(ssize_t i, ssize_t j); }; class CCmpTerritoryManager : public ICmpTerritoryManager { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeGloballyToMessageType(MT_OwnershipChanged); componentManager.SubscribeGloballyToMessageType(MT_PositionChanged); componentManager.SubscribeToMessageType(MT_TerrainChanged); componentManager.SubscribeToMessageType(MT_RenderSubmit); } DEFAULT_COMPONENT_ALLOCATOR(TerritoryManager) static std::string GetSchema() { return ""; } u8 m_ImpassableCost; float m_BorderThickness; float m_BorderSeparation; Grid* m_Territories; TerritoryOverlay* m_DebugOverlay; std::vector m_BoundaryLines; bool m_BoundaryLinesDirty; virtual void Init(const CParamNode& UNUSED(paramNode)) { m_Territories = NULL; m_DebugOverlay = NULL; // m_DebugOverlay = new TerritoryOverlay(*this); m_BoundaryLinesDirty = true; m_DirtyID = 1; CParamNode externalParamNode; CParamNode::LoadXML(externalParamNode, L"simulation/data/territorymanager.xml"); m_ImpassableCost = externalParamNode.GetChild("TerritoryManager").GetChild("ImpassableCost").ToInt(); m_BorderThickness = externalParamNode.GetChild("TerritoryManager").GetChild("BorderThickness").ToFixed().ToFloat(); m_BorderSeparation = externalParamNode.GetChild("TerritoryManager").GetChild("BorderSeparation").ToFixed().ToFloat(); } virtual void Deinit() { SAFE_DELETE(m_Territories); SAFE_DELETE(m_DebugOverlay); } virtual void Serialize(ISerializer& serialize) { // TODO } virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) { Init(paramNode); } virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_OwnershipChanged: { const CMessageOwnershipChanged& msgData = static_cast (msg); MakeDirtyIfRelevantEntity(msgData.entity); break; } case MT_PositionChanged: { const CMessagePositionChanged& msgData = static_cast (msg); MakeDirtyIfRelevantEntity(msgData.entity); break; } case MT_TerrainChanged: { MakeDirty(); break; } case MT_RenderSubmit: { const CMessageRenderSubmit& msgData = static_cast (msg); RenderSubmit(msgData.collector); break; } } } // Check whether the entity is either a settlement or territory influence; // ignore any others void MakeDirtyIfRelevantEntity(entity_id_t ent) { CmpPtr cmpSettlement(GetSimContext(), ent); if (!cmpSettlement.null()) MakeDirty(); CmpPtr cmpTerritoryInfluence(GetSimContext(), ent); if (!cmpTerritoryInfluence.null()) MakeDirty(); } virtual const Grid& GetTerritoryGrid() { CalculateTerritories(); return *m_Territories; } + virtual int32_t GetOwner(entity_pos_t x, entity_pos_t z); + // To support lazy updates of territory render data, // we maintain a DirtyID here and increment it whenever territories change; // if a caller has a lower DirtyID then it needs to be updated. size_t m_DirtyID; void MakeDirty() { SAFE_DELETE(m_Territories); ++m_DirtyID; m_BoundaryLinesDirty = true; } virtual bool NeedUpdate(size_t* dirtyID) { if (*dirtyID != m_DirtyID) { *dirtyID = m_DirtyID; return true; } return false; } void CalculateTerritories(); /** * Updates @p grid based on the obstruction shapes of all entities with * a TerritoryInfluence component. Grid cells are 0 if no influence, * or 1+c if the influence have cost c (assumed between 0 and 254). */ void RasteriseInfluences(CComponentManager::InterfaceList& infls, Grid& grid); struct TerritoryBoundary { player_id_t owner; std::vector points; }; std::vector ComputeBoundaries(); void UpdateBoundaryLines(); void RenderSubmit(SceneCollector& collector); }; REGISTER_COMPONENT_TYPE(TerritoryManager) /* We compute the territory influence of an entity with a kind of best-first search, storing an 'open' list of tiles that have not yet been processed, then taking the highest-weight tile (closest to origin) and updating the weight of extending to each neighbour (based on radius-determining 'falloff' value, adjusted by terrain movement cost), and repeating until all tiles are processed. */ typedef PriorityQueueHeap, u32, std::greater > OpenQueue; static void ProcessNeighbour(u32 falloff, u16 i, u16 j, u32 pg, bool diagonal, Grid& grid, OpenQueue& queue, const Grid& costGrid) { u32 dg = falloff * costGrid.get(i, j); if (diagonal) dg = (dg * 362) / 256; // Stop if new cost g=pg-dg is not better than previous value for that tile // (arranged to avoid underflow if pg < dg) if (pg <= grid.get(i, j) + dg) return; u32 g = pg - dg; // cost to this tile = cost to predecessor - falloff from predecessor grid.set(i, j, g); OpenQueue::Item tile = { std::make_pair(i, j), g }; queue.push(tile); } static void FloodFill(Grid& grid, Grid& costGrid, OpenQueue& openTiles, u32 falloff) { u32 tilesW = grid.m_W; u32 tilesH = grid.m_H; while (!openTiles.empty()) { OpenQueue::Item tile = openTiles.pop(); // Process neighbours (if they're not off the edge of the map) u16 x = tile.id.first; u16 z = tile.id.second; if (x > 0) ProcessNeighbour(falloff, x-1, z, tile.rank, false, grid, openTiles, costGrid); if (x < tilesW-1) ProcessNeighbour(falloff, x+1, z, tile.rank, false, grid, openTiles, costGrid); if (z > 0) ProcessNeighbour(falloff, x, z-1, tile.rank, false, grid, openTiles, costGrid); if (z < tilesH-1) ProcessNeighbour(falloff, x, z+1, tile.rank, false, grid, openTiles, costGrid); if (x > 0 && z > 0) ProcessNeighbour(falloff, x-1, z-1, tile.rank, true, grid, openTiles, costGrid); if (x > 0 && z < tilesH-1) ProcessNeighbour(falloff, x-1, z+1, tile.rank, true, grid, openTiles, costGrid); if (x < tilesW-1 && z > 0) ProcessNeighbour(falloff, x+1, z-1, tile.rank, true, grid, openTiles, costGrid); if (x < tilesW-1 && z < tilesH-1) ProcessNeighbour(falloff, x+1, z+1, tile.rank, true, grid, openTiles, costGrid); } } void CCmpTerritoryManager::CalculateTerritories() { PROFILE("CalculateTerritories"); if (m_Territories) return; CmpPtr cmpTerrain(GetSimContext(), SYSTEM_ENTITY); uint32_t tilesW = cmpTerrain->GetVerticesPerSide() - 1; uint32_t tilesH = cmpTerrain->GetVerticesPerSide() - 1; SAFE_DELETE(m_Territories); m_Territories = new Grid(tilesW, tilesH); // Compute terrain-passability-dependent costs per tile Grid influenceGrid(tilesW, tilesH); CmpPtr cmpPathfinder(GetSimContext(), SYSTEM_ENTITY); ICmpPathfinder::pass_class_t passClassUnrestricted = cmpPathfinder->GetPassabilityClass("unrestricted"); ICmpPathfinder::pass_class_t passClassDefault = cmpPathfinder->GetPassabilityClass("default"); const Grid& passGrid = cmpPathfinder->GetPassabilityGrid(); for (u32 j = 0; j < tilesH; ++j) { for (u32 i = 0; i < tilesW; ++i) { u16 g = passGrid.get(i, j); u8 cost; if (g & passClassUnrestricted) cost = 255; // off the world; use maximum cost else if (g & passClassDefault) cost = m_ImpassableCost; else cost = 1; influenceGrid.set(i, j, cost); } } // Find all territory influence entities CComponentManager::InterfaceList influences = GetSimContext().GetComponentManager().GetEntitiesWithInterface(IID_TerritoryInfluence); // Allow influence entities to override the terrain costs RasteriseInfluences(influences, influenceGrid); // Split influence entities into per-player lists, ignoring any with invalid properties std::map > influenceEntities; for (CComponentManager::InterfaceList::iterator it = influences.begin(); it != influences.end(); ++it) { // Ignore any with no weight or radius (to avoid divide-by-zero later) ICmpTerritoryInfluence* cmpTerritoryInfluence = static_cast(it->second); if (cmpTerritoryInfluence->GetWeight() == 0 || cmpTerritoryInfluence->GetRadius() == 0) continue; CmpPtr cmpOwnership(GetSimContext(), it->first); if (cmpOwnership.null()) continue; // Ignore Gaia and unassigned player_id_t owner = cmpOwnership->GetOwner(); if (owner <= 0) continue; // Ignore if invalid position CmpPtr cmpPosition(GetSimContext(), it->first); if (cmpPosition.null() || !cmpPosition->IsInWorld()) continue; influenceEntities[owner].push_back(it->first); } // For each player, store the sum of influences on each tile std::vector > > playerGrids; // TODO: this is a large waste of memory; we don't really need to store // all the intermediate grids for (std::map >::iterator it = influenceEntities.begin(); it != influenceEntities.end(); ++it) { Grid playerGrid(tilesW, tilesH); std::vector& ents = it->second; for (std::vector::iterator eit = ents.begin(); eit != ents.end(); ++eit) { // Compute the influence map of the current entity, then add it to the player grid Grid entityGrid(tilesW, tilesH); CmpPtr cmpPosition(GetSimContext(), *eit); CFixedVector2D pos = cmpPosition->GetPosition2D(); int i = clamp((pos.X / (int)CELL_SIZE).ToInt_RoundToNegInfinity(), 0, (int)tilesW-1); int j = clamp((pos.Y / (int)CELL_SIZE).ToInt_RoundToNegInfinity(), 0, (int)tilesH-1); CmpPtr cmpTerritoryInfluence(GetSimContext(), *eit); u32 weight = cmpTerritoryInfluence->GetWeight(); u32 radius = cmpTerritoryInfluence->GetRadius() / CELL_SIZE; u32 falloff = weight / radius; // earlier check for GetRadius() == 0 prevents divide-by-zero // TODO: we should have some maximum value on weight, to avoid overflow // when doing all the sums // Initialise the tile under the entity entityGrid.set(i, j, weight); OpenQueue openTiles; OpenQueue::Item tile = { std::make_pair((u16)i, (i16)j), weight }; openTiles.push(tile); // Expand influences outwards FloodFill(entityGrid, influenceGrid, openTiles, falloff); // TODO: we should do a sparse grid and only add the non-zero regions, for performance for (u16 j = 0; j < entityGrid.m_H; ++j) for (u16 i = 0; i < entityGrid.m_W; ++i) playerGrid.set(i, j, playerGrid.get(i, j) + entityGrid.get(i, j)); } playerGrids.push_back(std::make_pair(it->first, playerGrid)); } // Set m_Territories to the player ID with the highest influence for each tile for (u16 j = 0; j < tilesH; ++j) { for (u16 i = 0; i < tilesW; ++i) { u32 bestWeight = 0; for (size_t k = 0; k < playerGrids.size(); ++k) { u32 w = playerGrids[k].second.get(i, j); if (w > bestWeight) { player_id_t id = playerGrids[k].first; m_Territories->set(i, j, (u8)id); bestWeight = w; } } } } } /** * Compute the tile indexes on the grid nearest to a given point */ static void NearestTile(entity_pos_t x, entity_pos_t z, u16& i, u16& j, u16 w, u16 h) { i = clamp((x / (int)CELL_SIZE).ToInt_RoundToZero(), 0, w-1); j = clamp((z / (int)CELL_SIZE).ToInt_RoundToZero(), 0, h-1); } /** * Returns the position of the center of the given tile */ static void TileCenter(u16 i, u16 j, entity_pos_t& x, entity_pos_t& z) { x = entity_pos_t::FromInt(i*(int)CELL_SIZE + CELL_SIZE/2); z = entity_pos_t::FromInt(j*(int)CELL_SIZE + CELL_SIZE/2); } // TODO: would be nice not to duplicate those two functions from CCmpObstructionManager.cpp void CCmpTerritoryManager::RasteriseInfluences(CComponentManager::InterfaceList& infls, Grid& grid) { for (CComponentManager::InterfaceList::iterator it = infls.begin(); it != infls.end(); ++it) { ICmpTerritoryInfluence* cmpTerritoryInfluence = static_cast(it->second); int cost = cmpTerritoryInfluence->GetCost(); if (cost == -1) continue; CmpPtr cmpObstruction(GetSimContext(), it->first); if (cmpObstruction.null()) continue; ICmpObstructionManager::ObstructionSquare square; if (!cmpObstruction->GetObstructionSquare(square)) continue; CFixedVector2D halfSize(square.hw, square.hh); CFixedVector2D halfBound = Geometry::GetHalfBoundingBox(square.u, square.v, halfSize); u16 i0, j0, i1, j1; NearestTile(square.x - halfBound.X, square.z - halfBound.Y, i0, j0, grid.m_W, grid.m_H); NearestTile(square.x + halfBound.X, square.z + halfBound.Y, i1, j1, grid.m_W, grid.m_H); for (u16 j = j0; j <= j1; ++j) { for (u16 i = i0; i <= i1; ++i) { entity_pos_t x, z; TileCenter(i, j, x, z); if (Geometry::PointIsInSquare(CFixedVector2D(x - square.x, z - square.z), square.u, square.v, halfSize)) grid.set(i, j, cost); } } } } std::vector CCmpTerritoryManager::ComputeBoundaries() { PROFILE("ComputeBoundaries"); std::vector boundaries; CalculateTerritories(); // Copy the territories grid so we can mess with it Grid grid (*m_Territories); // Some constants for the border walk CVector2D edgeOffsets[] = { CVector2D(0.5f, 0.0f), CVector2D(1.0f, 0.5f), CVector2D(0.5f, 1.0f), CVector2D(0.0f, 0.5f) }; // Try to find an assigned tile for (int j = 0; j < grid.m_H; ++j) { for (int i = 0; i < grid.m_W; ++i) { u8 owner = grid.get(i, j); if (owner) { // Found the first tile (which must be the lowest j value of any non-zero tile); // start at the bottom edge of it and chase anticlockwise around the border until // we reach the starting point again boundaries.push_back(TerritoryBoundary()); boundaries.back().owner = owner; std::vector& points = boundaries.back().points; int dir = 0; // 0 == bottom edge of tile, 1 == right, 2 == top, 3 == left int cdir = dir; int ci = i, cj = j; while (true) { points.push_back((CVector2D(ci, cj) + edgeOffsets[cdir]) * CELL_SIZE); // Given that we're on an edge on a continuous boundary and aiming anticlockwise, // we can either carry on straight or turn left or turn right, so examine each // of the three possible cases (depending on initial direction): switch (cdir) { case 0: if (ci < grid.m_W-1 && cj > 0 && grid.get(ci+1, cj-1) == owner) { ++ci; --cj; cdir = 3; } else if (ci < grid.m_W-1 && grid.get(ci+1, cj) == owner) ++ci; else cdir = 1; break; case 1: if (ci < grid.m_W-1 && cj < grid.m_H-1 && grid.get(ci+1, cj+1) == owner) { ++ci; ++cj; cdir = 0; } else if (cj < grid.m_H-1 && grid.get(ci, cj+1) == owner) ++cj; else cdir = 2; break; case 2: if (ci > 0 && cj < grid.m_H-1 && grid.get(ci-1, cj+1) == owner) { --ci; ++cj; cdir = 1; } else if (ci > 0 && grid.get(ci-1, cj) == owner) --ci; else cdir = 3; break; case 3: if (ci > 0 && cj > 0 && grid.get(ci-1, cj-1) == owner) { --ci; --cj; cdir = 2; } else if (cj > 0 && grid.get(ci, cj-1) == owner) --cj; else cdir = 0; break; } // Stop when we've reached the starting point again if (ci == i && cj == j && cdir == dir) break; } // Zero out this whole territory with a simple flood fill, so we don't // process it a second time std::vector > tileStack; #define ZERO_AND_PUSH(i, j) STMT(grid.set(i, j, 0); tileStack.push_back(std::make_pair(i, j)); ) ZERO_AND_PUSH(i, j); while (!tileStack.empty()) { int ti = tileStack.back().first; int tj = tileStack.back().second; tileStack.pop_back(); if (ti > 0 && grid.get(ti-1, tj) == owner) ZERO_AND_PUSH(ti-1, tj); if (ti < grid.m_W-1 && grid.get(ti+1, tj) == owner) ZERO_AND_PUSH(ti+1, tj); if (tj > 0 && grid.get(ti, tj-1) == owner) ZERO_AND_PUSH(ti, tj-1); if (tj < grid.m_H-1 && grid.get(ti, tj+1) == owner) ZERO_AND_PUSH(ti, tj+1); if (ti > 0 && tj > 0 && grid.get(ti-1, tj-1) == owner) ZERO_AND_PUSH(ti-1, tj-1); if (ti > 0 && tj < grid.m_H-1 && grid.get(ti-1, tj+1) == owner) ZERO_AND_PUSH(ti-1, tj+1); if (ti < grid.m_W-1 && tj > 0 && grid.get(ti+1, tj-1) == owner) ZERO_AND_PUSH(ti+1, tj-1); if (ti < grid.m_W-1 && tj < grid.m_H-1 && grid.get(ti+1, tj+1) == owner) ZERO_AND_PUSH(ti+1, tj+1); } #undef ZERO_AND_PUSH } } } return boundaries; } void CCmpTerritoryManager::UpdateBoundaryLines() { PROFILE("update boundary lines"); m_BoundaryLines.clear(); std::vector boundaries = ComputeBoundaries(); CTextureProperties texturePropsBase("art/textures/misc/territory_border.png"); texturePropsBase.SetWrap(GL_CLAMP_TO_BORDER, GL_CLAMP_TO_EDGE); texturePropsBase.SetMaxAnisotropy(2.f); CTexturePtr textureBase = g_Renderer.GetTextureManager().CreateTexture(texturePropsBase); CTextureProperties texturePropsMask("art/textures/misc/territory_border_mask.png"); texturePropsMask.SetWrap(GL_CLAMP_TO_BORDER, GL_CLAMP_TO_EDGE); texturePropsMask.SetMaxAnisotropy(2.f); CTexturePtr textureMask = g_Renderer.GetTextureManager().CreateTexture(texturePropsMask); CmpPtr cmpTerrain(GetSimContext(), SYSTEM_ENTITY); if (cmpTerrain.null()) return; CTerrain* terrain = cmpTerrain->GetCTerrain(); CmpPtr cmpPlayerManager(GetSimContext(), SYSTEM_ENTITY); if (cmpPlayerManager.null()) return; for (size_t i = 0; i < boundaries.size(); ++i) { if (boundaries[i].points.empty()) continue; CColor color(1, 0, 1, 1); CmpPtr cmpPlayer(GetSimContext(), cmpPlayerManager->GetPlayerByID(boundaries[i].owner)); if (!cmpPlayer.null()) color = cmpPlayer->GetColour(); m_BoundaryLines.push_back(SOverlayTexturedLine()); m_BoundaryLines.back().m_Terrain = terrain; m_BoundaryLines.back().m_TextureBase = textureBase; m_BoundaryLines.back().m_TextureMask = textureMask; m_BoundaryLines.back().m_Color = color; m_BoundaryLines.back().m_Thickness = m_BorderThickness; SimRender::SmoothPointsAverage(boundaries[i].points, true); SimRender::InterpolatePointsRNS(boundaries[i].points, true, m_BorderSeparation); std::vector& points = m_BoundaryLines.back().m_Coords; for (size_t j = 0; j < boundaries[i].points.size(); ++j) { points.push_back(boundaries[i].points[j].X); points.push_back(boundaries[i].points[j].Y); } } } void CCmpTerritoryManager::RenderSubmit(SceneCollector& collector) { if (m_BoundaryLinesDirty) { UpdateBoundaryLines(); m_BoundaryLinesDirty = false; } for (size_t i = 0; i < m_BoundaryLines.size(); ++i) collector.Submit(&m_BoundaryLines[i]); } +int32_t CCmpTerritoryManager::GetOwner(entity_pos_t x, entity_pos_t z) +{ + u16 i, j; + CalculateTerritories(); + NearestTile(x, z, i, j, m_Territories->m_W, m_Territories->m_H); + return m_Territories->get(i, j); +} void TerritoryOverlay::StartRender() { m_TerritoryManager.CalculateTerritories(); } void TerritoryOverlay::ProcessTile(ssize_t i, ssize_t j) { if (!m_TerritoryManager.m_Territories) return; u8 id = m_TerritoryManager.m_Territories->get(i, j); float a = 0.2f; switch (id) { case 0: break; case 1: RenderTile(CColor(1, 0, 0, a), false); break; case 2: RenderTile(CColor(0, 1, 0, a), false); break; case 3: RenderTile(CColor(0, 0, 1, a), false); break; case 4: RenderTile(CColor(1, 1, 0, a), false); break; case 5: RenderTile(CColor(0, 1, 1, a), false); break; case 6: RenderTile(CColor(1, 0, 1, a), false); break; default: RenderTile(CColor(1, 1, 1, a), false); break; } } Index: ps/trunk/source/simulation2/components/CCmpTemplateManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpTemplateManager.cpp (revision 9969) +++ ps/trunk/source/simulation2/components/CCmpTemplateManager.cpp (revision 9970) @@ -1,538 +1,539 @@ /* Copyright (C) 2011 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 "ICmpTemplateManager.h" #include "simulation2/MessageTypes.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/XML/RelaxNG.h" #include "ps/XML/Xeromyces.h" static const wchar_t TEMPLATE_ROOT[] = L"simulation/templates/"; static const wchar_t ACTOR_ROOT[] = L"art/actors/"; class CCmpTemplateManager : public ICmpTemplateManager { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeGloballyToMessageType(MT_Destroy); } DEFAULT_COMPONENT_ALLOCATOR(TemplateManager) static std::string GetSchema() { return ""; } virtual void Init(const CParamNode& UNUSED(paramNode)) { m_DisableValidation = false; m_Validator.LoadGrammar(GetSimContext().GetComponentManager().GenerateSchema()); // TODO: handle errors loading the grammar here? // TODO: support hotloading changes to the grammar } virtual void Deinit() { } virtual void Serialize(ISerializer& serialize) { size_t count = 0; for (std::map::const_iterator it = m_LatestTemplates.begin(); it != m_LatestTemplates.end(); ++it) { if (ENTITY_IS_LOCAL(it->first)) continue; ++count; } serialize.NumberU32_Unbounded("num entities", (u32)count); for (std::map::const_iterator it = m_LatestTemplates.begin(); it != m_LatestTemplates.end(); ++it) { if (ENTITY_IS_LOCAL(it->first)) continue; serialize.NumberU32_Unbounded("id", it->first); serialize.StringASCII("template", it->second, 0, 256); } // TODO: maybe we should do some kind of interning thing instead of printing so many strings? // TODO: will need to serialize techs too, because we need to be giving out // template data before other components (like the tech components) have been deserialized } virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) { Init(paramNode); u32 numEntities; deserialize.NumberU32_Unbounded("num entities", numEntities); for (u32 i = 0; i < numEntities; ++i) { entity_id_t ent; std::string templateName; deserialize.NumberU32_Unbounded("id", ent); deserialize.StringASCII("template", templateName, 0, 256); m_LatestTemplates[ent] = templateName; } } virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_Destroy: { const CMessageDestroy& msgData = static_cast (msg); // Clean up m_LatestTemplates so it doesn't record any data for destroyed entities m_LatestTemplates.erase(msgData.entity); break; } } } virtual void DisableValidation() { m_DisableValidation = true; } virtual const CParamNode* LoadTemplate(entity_id_t ent, const std::string& templateName, int playerID); virtual const CParamNode* GetTemplate(std::string templateName); virtual const CParamNode* GetTemplateWithoutValidation(std::string templateName); virtual const CParamNode* LoadLatestTemplate(entity_id_t ent); virtual std::string GetCurrentTemplateName(entity_id_t ent); virtual std::vector FindAllTemplates(bool includeActors); virtual std::vector GetEntitiesUsingTemplate(std::string templateName); private: // Entity template XML validator RelaxNGValidator m_Validator; // Disable validation, for test cases bool m_DisableValidation; // Map from template name (XML filename or special |-separated string) to the most recently // loaded non-broken template data. This includes files that will fail schema validation. // (Failed loads won't remove existing entries under the same name, so we behave more nicely // when hotloading broken files) std::map m_TemplateFileData; // Map from template name to schema validation status. // (Some files, e.g. inherited parent templates, may not be valid themselves but we still need to load // them and use them; we only reject invalid templates that were requested directly by GetTemplate/etc) std::map m_TemplateSchemaValidity; // Remember the template used by each entity, so we can return them // again for deserialization. // TODO: should store player ID etc. std::map m_LatestTemplates; // (Re)loads the given template, regardless of whether it exists already, // and saves into m_TemplateFileData. Also loads any parents that are not yet // loaded. Returns false on error. // @param templateName XML filename to load (not a |-separated string) bool LoadTemplateFile(const std::string& templateName, int depth); // Constructs a standard static-decorative-object template for the given actor void ConstructTemplateActor(const std::string& actorName, CParamNode& out); // Copy the non-interactive components of an entity template (position, actor, etc) into // a new entity template void CopyPreviewSubset(CParamNode& out, const CParamNode& in, bool corpse); // Copy the components of an entity necessary for a construction foundation // (position, actor, armour, health, etc) into a new entity template void CopyFoundationSubset(CParamNode& out, const CParamNode& in); }; REGISTER_COMPONENT_TYPE(TemplateManager) const CParamNode* CCmpTemplateManager::LoadTemplate(entity_id_t ent, const std::string& templateName, int UNUSED(playerID)) { m_LatestTemplates[ent] = templateName; const CParamNode* templateRoot = GetTemplate(templateName); if (!templateRoot) return NULL; // TODO: Eventually we need to support techs in here, and return a different template per playerID return templateRoot; } const CParamNode* CCmpTemplateManager::GetTemplate(std::string templateName) { // Load the template if necessary if (!LoadTemplateFile(templateName, 0)) { LOGERROR(L"Failed to load entity template '%hs'", templateName.c_str()); return NULL; } if (!m_DisableValidation) { // Compute validity, if it's not computed before if (m_TemplateSchemaValidity.find(templateName) == m_TemplateSchemaValidity.end()) m_TemplateSchemaValidity[templateName] = m_Validator.Validate(wstring_from_utf8(templateName), m_TemplateFileData[templateName].ToXML()); // Refuse to return invalid templates if (!m_TemplateSchemaValidity[templateName]) return NULL; } const CParamNode& templateRoot = m_TemplateFileData[templateName].GetChild("Entity"); if (!templateRoot.IsOk()) { // The validator should never let this happen LOGERROR(L"Invalid root element in entity template '%hs'", templateName.c_str()); return NULL; } return &templateRoot; } const CParamNode* CCmpTemplateManager::GetTemplateWithoutValidation(std::string templateName) { // Load the template if necessary if (!LoadTemplateFile(templateName, 0)) { LOGERROR(L"Failed to load entity template '%hs'", templateName.c_str()); return NULL; } const CParamNode& templateRoot = m_TemplateFileData[templateName].GetChild("Entity"); if (!templateRoot.IsOk()) return NULL; return &templateRoot; } const CParamNode* CCmpTemplateManager::LoadLatestTemplate(entity_id_t ent) { std::map::const_iterator it = m_LatestTemplates.find(ent); if (it == m_LatestTemplates.end()) return NULL; return LoadTemplate(ent, it->second, -1); } std::string CCmpTemplateManager::GetCurrentTemplateName(entity_id_t ent) { std::map::const_iterator it = m_LatestTemplates.find(ent); if (it == m_LatestTemplates.end()) return ""; return it->second; } bool CCmpTemplateManager::LoadTemplateFile(const std::string& templateName, int depth) { // If this file was already loaded, we don't need to do anything if (m_TemplateFileData.find(templateName) != m_TemplateFileData.end()) return true; // Handle infinite loops more gracefully than running out of stack space and crashing if (depth > 100) { LOGERROR(L"Probable infinite inheritance loop in entity template '%hs'", templateName.c_str()); return false; } // Handle special case "actor|foo" if (templateName.find("actor|") == 0) { ConstructTemplateActor(templateName.substr(6), m_TemplateFileData[templateName]); return true; } // Handle special case "preview|foo" if (templateName.find("preview|") == 0) { // Load the base entity template, if it wasn't already loaded std::string baseName = templateName.substr(8); if (!LoadTemplateFile(baseName, depth+1)) { LOGERROR(L"Failed to load entity template '%hs'", baseName.c_str()); return false; } // Copy a subset to the requested template CopyPreviewSubset(m_TemplateFileData[templateName], m_TemplateFileData[baseName], false); return true; } // Handle special case "corpse|foo" if (templateName.find("corpse|") == 0) { // Load the base entity template, if it wasn't already loaded std::string baseName = templateName.substr(7); if (!LoadTemplateFile(baseName, depth+1)) { LOGERROR(L"Failed to load entity template '%hs'", baseName.c_str()); return false; } // Copy a subset to the requested template CopyPreviewSubset(m_TemplateFileData[templateName], m_TemplateFileData[baseName], true); return true; } // Handle special case "foundation|foo" if (templateName.find("foundation|") == 0) { // Load the base entity template, if it wasn't already loaded std::string baseName = templateName.substr(11); if (!LoadTemplateFile(baseName, depth+1)) { LOGERROR(L"Failed to load entity template '%hs'", baseName.c_str()); return false; } // Copy a subset to the requested template CopyFoundationSubset(m_TemplateFileData[templateName], m_TemplateFileData[baseName]); return true; } // Normal case: templateName is an XML file: VfsPath path = VfsPath(TEMPLATE_ROOT) / wstring_from_utf8(templateName + ".xml"); CXeromyces xero; PSRETURN ok = xero.Load(g_VFS, path); if (ok != PSRETURN_OK) return false; // (Xeromyces already logged an error with the full filename) int attr_parent = xero.GetAttributeID("parent"); CStr parentName = xero.GetRoot().GetAttributes().GetNamedItem(attr_parent); if (!parentName.empty()) { // To prevent needless complexity in template design, we don't allow |-separated strings as parents if (parentName.find('|') != parentName.npos) { LOGERROR(L"Invalid parent '%hs' in entity template '%hs'", parentName.c_str(), templateName.c_str()); return false; } // Ensure the parent is loaded if (!LoadTemplateFile(parentName, depth+1)) { LOGERROR(L"Failed to load parent '%hs' of entity template '%hs'", parentName.c_str(), templateName.c_str()); return false; } CParamNode& parentData = m_TemplateFileData[parentName]; // Initialise this template with its parent m_TemplateFileData[templateName] = parentData; } // Load the new file into the template data (overriding parent values) CParamNode::LoadXML(m_TemplateFileData[templateName], xero); return true; } void CCmpTemplateManager::ConstructTemplateActor(const std::string& actorName, CParamNode& out) { // Load the base actor template if necessary const char* templateName = "special/actor"; if (!LoadTemplateFile(templateName, 0)) { LOGERROR(L"Failed to load entity template '%hs'", templateName); return; } // Copy the actor template out = m_TemplateFileData[templateName]; // Initialise the actor's name std::string name = utf8_from_wstring(CParamNode::EscapeXMLString(wstring_from_utf8(actorName))); std::string xml = "" + name + ""; CParamNode::LoadXMLString(out, xml.c_str()); } static Status AddToTemplates(const VfsPath& pathname, const FileInfo& UNUSED(fileInfo), const uintptr_t cbData) { std::vector& templates = *(std::vector*)cbData; // Strip the .xml extension VfsPath pathstem = pathname.ChangeExtension(L""); // Strip the root from the path std::wstring name = pathstem.string().substr(ARRAY_SIZE(TEMPLATE_ROOT)-1); // We want to ignore template_*.xml templates, since they should never be built in the editor if (name.substr(0, 9) == L"template_") return INFO::OK; templates.push_back(std::string(name.begin(), name.end())); return INFO::OK; } static Status AddActorToTemplates(const VfsPath& pathname, const FileInfo& UNUSED(fileInfo), const uintptr_t cbData) { std::vector& templates = *(std::vector*)cbData; // Strip the root from the path std::wstring name = pathname.string().substr(ARRAY_SIZE(ACTOR_ROOT)-1); templates.push_back("actor|" + std::string(name.begin(), name.end())); return INFO::OK; } std::vector CCmpTemplateManager::FindAllTemplates(bool includeActors) { // TODO: eventually this should probably read all the template files and look for flags to // determine which should be displayed in the editor (and in what categories etc); for now we'll // just return all the files std::vector templates; Status ok; // Find all the normal entity templates first ok = vfs::ForEachFile(g_VFS, TEMPLATE_ROOT, AddToTemplates, (uintptr_t)&templates, L"*.xml", vfs::DIR_RECURSIVE); WARN_IF_ERR(ok); if (includeActors) { // Add all the actors too ok = vfs::ForEachFile(g_VFS, ACTOR_ROOT, AddActorToTemplates, (uintptr_t)&templates, L"*.xml", vfs::DIR_RECURSIVE); WARN_IF_ERR(ok); } return templates; } /** * Get the list of entities using the specified template */ std::vector CCmpTemplateManager::GetEntitiesUsingTemplate(std::string templateName) { std::vector entities; for (std::map::const_iterator it = m_LatestTemplates.begin(); it != m_LatestTemplates.end(); ++it) { if(it->second == templateName) entities.push_back(it->first); } return entities; } void CCmpTemplateManager::CopyPreviewSubset(CParamNode& out, const CParamNode& in, bool corpse) { // We only want to include components which are necessary (for the visual previewing of an entity) // and safe (i.e. won't do anything that affects the synchronised simulation state), so additions // to this list should be carefully considered std::set permittedComponentTypes; permittedComponentTypes.insert("Ownership"); permittedComponentTypes.insert("Position"); permittedComponentTypes.insert("VisualActor"); permittedComponentTypes.insert("Footprint"); permittedComponentTypes.insert("Obstruction"); permittedComponentTypes.insert("Decay"); + permittedComponentTypes.insert("BuildRestrictions"); // Need these for the Actor Viewer: permittedComponentTypes.insert("Attack"); permittedComponentTypes.insert("UnitMotion"); permittedComponentTypes.insert("Sound"); // (This set could be initialised once and reused, but it's not worth the effort) CParamNode::LoadXMLString(out, ""); out.CopyFilteredChildrenOfChild(in, "Entity", permittedComponentTypes); // Disable the Obstruction component (if there is one) so it doesn't affect pathfinding // (but can still be used for testing this entity for collisions against others) if (out.GetChild("Entity").GetChild("Obstruction").IsOk()) CParamNode::LoadXMLString(out, "false"); if (!corpse) { // Previews should always be visible in fog-of-war/etc CParamNode::LoadXMLString(out, "0falsetrue"); } if (corpse) { // Corpses should include decay components and un-inactivate them if (out.GetChild("Entity").GetChild("Decay").IsOk()) CParamNode::LoadXMLString(out, ""); // Corpses shouldn't display silhouettes (especially since they're often half underground) if (out.GetChild("Entity").GetChild("VisualActor").IsOk()) CParamNode::LoadXMLString(out, "false"); } } void CCmpTemplateManager::CopyFoundationSubset(CParamNode& out, const CParamNode& in) { // TODO: this is all kind of yucky and hard-coded; it'd be nice to have a more generic // extensible scriptable way to define these subsets std::set permittedComponentTypes; permittedComponentTypes.insert("Ownership"); permittedComponentTypes.insert("Position"); permittedComponentTypes.insert("VisualActor"); permittedComponentTypes.insert("Identity"); permittedComponentTypes.insert("BuildRestrictions"); permittedComponentTypes.insert("Obstruction"); permittedComponentTypes.insert("Selectable"); permittedComponentTypes.insert("Footprint"); permittedComponentTypes.insert("Armour"); permittedComponentTypes.insert("Health"); permittedComponentTypes.insert("StatusBars"); permittedComponentTypes.insert("OverlayRenderer"); permittedComponentTypes.insert("Decay"); permittedComponentTypes.insert("Cost"); permittedComponentTypes.insert("Sound"); permittedComponentTypes.insert("Vision"); permittedComponentTypes.insert("AIProxy"); CParamNode::LoadXMLString(out, ""); out.CopyFilteredChildrenOfChild(in, "Entity", permittedComponentTypes); // Switch the actor to foundation mode CParamNode::LoadXMLString(out, ""); // Add the Foundation component, to deal with the construction process CParamNode::LoadXMLString(out, ""); // Initialise health to 1 CParamNode::LoadXMLString(out, "1"); // Foundations shouldn't initially block unit movement if (out.GetChild("Entity").GetChild("Obstruction").IsOk()) CParamNode::LoadXMLString(out, "truetrue"); // Don't provide population bonuses yet (but still do take up population cost) if (out.GetChild("Entity").GetChild("Cost").IsOk()) CParamNode::LoadXMLString(out, "0"); // Foundations should be visible themselves in fog-of-war if their base template is, // but shouldn't have any vision range if (out.GetChild("Entity").GetChild("Vision").IsOk()) CParamNode::LoadXMLString(out, "0"); } Index: ps/trunk/source/simulation2/components/ICmpUnitMotion.cpp =================================================================== --- ps/trunk/source/simulation2/components/ICmpUnitMotion.cpp (revision 9969) +++ ps/trunk/source/simulation2/components/ICmpUnitMotion.cpp (revision 9970) @@ -1,101 +1,106 @@ /* Copyright (C) 2011 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 "ICmpUnitMotion.h" #include "simulation2/system/InterfaceScripted.h" #include "simulation2/scripting/ScriptComponent.h" BEGIN_INTERFACE_WRAPPER(UnitMotion) DEFINE_INTERFACE_METHOD_4("MoveToPointRange", bool, ICmpUnitMotion, MoveToPointRange, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t) DEFINE_INTERFACE_METHOD_3("IsInTargetRange", bool, ICmpUnitMotion, IsInTargetRange, entity_id_t, entity_pos_t, entity_pos_t) DEFINE_INTERFACE_METHOD_3("MoveToTargetRange", bool, ICmpUnitMotion, MoveToTargetRange, entity_id_t, entity_pos_t, entity_pos_t) DEFINE_INTERFACE_METHOD_3("MoveToFormationOffset", void, ICmpUnitMotion, MoveToFormationOffset, entity_id_t, entity_pos_t, entity_pos_t) DEFINE_INTERFACE_METHOD_2("FaceTowardsPoint", void, ICmpUnitMotion, FaceTowardsPoint, entity_pos_t, entity_pos_t) DEFINE_INTERFACE_METHOD_0("StopMoving", void, ICmpUnitMotion, StopMoving) DEFINE_INTERFACE_METHOD_1("SetSpeed", void, ICmpUnitMotion, SetSpeed, fixed) DEFINE_INTERFACE_METHOD_0("GetWalkSpeed", fixed, ICmpUnitMotion, GetWalkSpeed) DEFINE_INTERFACE_METHOD_0("GetRunSpeed", fixed, ICmpUnitMotion, GetRunSpeed) DEFINE_INTERFACE_METHOD_1("SetUnitRadius", void, ICmpUnitMotion, SetUnitRadius, fixed) DEFINE_INTERFACE_METHOD_1("SetDebugOverlay", void, ICmpUnitMotion, SetDebugOverlay, bool) END_INTERFACE_WRAPPER(UnitMotion) class CCmpUnitMotionScripted : public ICmpUnitMotion { public: DEFAULT_SCRIPT_WRAPPER(UnitMotionScripted) virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) { return m_Script.Call("MoveToPointRange", x, z, minRange, maxRange); } virtual bool IsInTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) { return m_Script.Call("IsInTargetRange", target, minRange, maxRange); } virtual bool MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) { return m_Script.Call("MoveToTargetRange", target, minRange, maxRange); } virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) { m_Script.CallVoid("MoveToFormationOffset", target, x, z); } virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z) { m_Script.CallVoid("FaceTowardsPoint", x, z); } virtual void StopMoving() { m_Script.CallVoid("StopMoving"); } virtual void SetSpeed(fixed speed) { m_Script.CallVoid("SetSpeed", speed); } virtual fixed GetWalkSpeed() { return m_Script.Call("GetWalkSpeed"); } virtual fixed GetRunSpeed() { return m_Script.Call("GetRunSpeed"); } + virtual ICmpPathfinder::pass_class_t GetPassabilityClass() + { + return m_Script.Call("GetPassabilityClass"); + } + virtual void SetUnitRadius(fixed radius) { m_Script.CallVoid("SetUnitRadius", radius); } virtual void SetDebugOverlay(bool enabled) { m_Script.CallVoid("SetDebugOverlay", enabled); } }; REGISTER_COMPONENT_SCRIPT_WRAPPER(UnitMotionScripted) Index: ps/trunk/source/simulation2/components/CCmpPathfinder.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpPathfinder.cpp (revision 9969) +++ ps/trunk/source/simulation2/components/CCmpPathfinder.cpp (revision 9970) @@ -1,552 +1,723 @@ /* Copyright (C) 2011 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 . */ /** * @file * Common code and setup code for CCmpPathfinder. */ #include "precompiled.h" #include "CCmpPathfinder_Common.h" #include "ps/CLogger.h" #include "ps/CStr.h" #include "ps/Profile.h" #include "renderer/Scene.h" #include "simulation2/MessageTypes.h" +#include "simulation2/components/ICmpObstruction.h" #include "simulation2/components/ICmpObstructionManager.h" #include "simulation2/components/ICmpWaterManager.h" #include "simulation2/serialization/SerializeTemplates.h" // Default cost to move a single tile is a fairly arbitrary number, which should be big // enough to be precise when multiplied/divided and small enough to never overflow when // summing the cost of a whole path. const int DEFAULT_MOVE_COST = 256; REGISTER_COMPONENT_TYPE(Pathfinder) void CCmpPathfinder::Init(const CParamNode& UNUSED(paramNode)) { m_MapSize = 0; m_Grid = NULL; m_ObstructionGrid = NULL; m_TerrainDirty = true; m_NextAsyncTicket = 1; m_DebugOverlay = NULL; m_DebugGrid = NULL; m_DebugPath = NULL; m_SameTurnMovesCount = 0; // Since this is used as a system component (not loaded from an entity template), // we can't use the real paramNode (it won't get handled properly when deserializing), // so load the data from a special XML file. CParamNode externalParamNode; CParamNode::LoadXML(externalParamNode, L"simulation/data/pathfinder.xml"); // Previously all move commands during a turn were // queued up and processed asynchronously at the start // of the next turn. Now we are processing queued up // events several times duing the turn. This improves // responsiveness and units move more smoothly especially. // when in formation. There is still a call at the // beginning of a turn to process all outstanding moves - // this will handle any moves above the MaxSameTurnMoves // threshold. // // TODO - The moves processed at the beginning of the // turn do not count against the maximum moves per turn // currently. The thinking is that this will eventually // happen in another thread. Either way this probably // will require some adjustment and rethinking. const CParamNode pathingSettings = externalParamNode.GetChild("Pathfinder"); m_MaxSameTurnMoves = pathingSettings.GetChild("MaxSameTurnMoves").ToInt(); const CParamNode::ChildrenMap& passClasses = externalParamNode.GetChild("Pathfinder").GetChild("PassabilityClasses").GetChildren(); for (CParamNode::ChildrenMap::const_iterator it = passClasses.begin(); it != passClasses.end(); ++it) { std::string name = it->first; ENSURE((int)m_PassClasses.size() <= PASS_CLASS_BITS); pass_class_t mask = (pass_class_t)(1u << (m_PassClasses.size() + 2)); m_PassClasses.push_back(PathfinderPassability(mask, it->second)); m_PassClassMasks[name] = mask; } const CParamNode::ChildrenMap& moveClasses = externalParamNode.GetChild("Pathfinder").GetChild("MovementClasses").GetChildren(); // First find the set of unit classes used by any terrain classes, // and assign unique tags to terrain classes std::set unitClassNames; unitClassNames.insert("default"); // must always have costs for default { size_t i = 0; for (CParamNode::ChildrenMap::const_iterator it = moveClasses.begin(); it != moveClasses.end(); ++it) { std::string terrainClassName = it->first; m_TerrainCostClassTags[terrainClassName] = (cost_class_t)i; ++i; const CParamNode::ChildrenMap& unitClasses = it->second.GetChild("UnitClasses").GetChildren(); for (CParamNode::ChildrenMap::const_iterator uit = unitClasses.begin(); uit != unitClasses.end(); ++uit) unitClassNames.insert(uit->first); } } // For each terrain class, set the costs for every unit class, // and assign unique tags to unit classes { size_t i = 0; for (std::set::const_iterator nit = unitClassNames.begin(); nit != unitClassNames.end(); ++nit) { m_UnitCostClassTags[*nit] = (cost_class_t)i; ++i; std::vector costs; std::vector speeds; for (CParamNode::ChildrenMap::const_iterator it = moveClasses.begin(); it != moveClasses.end(); ++it) { // Default to the general costs for this terrain class fixed cost = it->second.GetChild("@Cost").ToFixed(); fixed speed = it->second.GetChild("@Speed").ToFixed(); // Check for specific cost overrides for this unit class const CParamNode& unitClass = it->second.GetChild("UnitClasses").GetChild(nit->c_str()); if (unitClass.IsOk()) { cost = unitClass.GetChild("@Cost").ToFixed(); speed = unitClass.GetChild("@Speed").ToFixed(); } costs.push_back((cost * DEFAULT_MOVE_COST).ToInt_RoundToZero()); speeds.push_back(speed); } m_MoveCosts.push_back(costs); m_MoveSpeeds.push_back(speeds); } } } void CCmpPathfinder::Deinit() { SetDebugOverlay(false); // cleans up memory ResetDebugPath(); delete m_Grid; delete m_ObstructionGrid; } struct SerializeLongRequest { template void operator()(S& serialize, const char* UNUSED(name), AsyncLongPathRequest& value) { serialize.NumberU32_Unbounded("ticket", value.ticket); serialize.NumberFixed_Unbounded("x0", value.x0); serialize.NumberFixed_Unbounded("z0", value.z0); SerializeGoal()(serialize, "goal", value.goal); serialize.NumberU16_Unbounded("pass class", value.passClass); serialize.NumberU8_Unbounded("cost class", value.costClass); serialize.NumberU32_Unbounded("notify", value.notify); } }; struct SerializeShortRequest { template void operator()(S& serialize, const char* UNUSED(name), AsyncShortPathRequest& value) { serialize.NumberU32_Unbounded("ticket", value.ticket); serialize.NumberFixed_Unbounded("x0", value.x0); serialize.NumberFixed_Unbounded("z0", value.z0); serialize.NumberFixed_Unbounded("r", value.r); serialize.NumberFixed_Unbounded("range", value.range); SerializeGoal()(serialize, "goal", value.goal); serialize.NumberU16_Unbounded("pass class", value.passClass); serialize.Bool("avoid moving units", value.avoidMovingUnits); serialize.NumberU32_Unbounded("group", value.group); serialize.NumberU32_Unbounded("notify", value.notify); } }; void CCmpPathfinder::Serialize(ISerializer& serialize) { SerializeVector()(serialize, "long requests", m_AsyncLongPathRequests); SerializeVector()(serialize, "short requests", m_AsyncShortPathRequests); serialize.NumberU32_Unbounded("next ticket", m_NextAsyncTicket); serialize.NumberU16_Unbounded("same turn moves count", m_SameTurnMovesCount); } void CCmpPathfinder::Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) { Init(paramNode); SerializeVector()(deserialize, "long requests", m_AsyncLongPathRequests); SerializeVector()(deserialize, "short requests", m_AsyncShortPathRequests); deserialize.NumberU32_Unbounded("next ticket", m_NextAsyncTicket); deserialize.NumberU16_Unbounded("same turn moves count", m_SameTurnMovesCount); } void CCmpPathfinder::HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_RenderSubmit: { const CMessageRenderSubmit& msgData = static_cast (msg); RenderSubmit(msgData.collector); break; } case MT_TerrainChanged: { // TODO: we ought to only bother updating the dirtied region m_TerrainDirty = true; break; } case MT_TurnStart: { m_SameTurnMovesCount = 0; break; } } } void CCmpPathfinder::RenderSubmit(SceneCollector& collector) { for (size_t i = 0; i < m_DebugOverlayShortPathLines.size(); ++i) collector.Submit(&m_DebugOverlayShortPathLines[i]); } fixed CCmpPathfinder::GetMovementSpeed(entity_pos_t x0, entity_pos_t z0, u8 costClass) { UpdateGrid(); u16 i, j; NearestTile(x0, z0, i, j); TerrainTile tileTag = m_Grid->get(i, j); return m_MoveSpeeds.at(costClass).at(GET_COST_CLASS(tileTag)); } ICmpPathfinder::pass_class_t CCmpPathfinder::GetPassabilityClass(const std::string& name) { if (m_PassClassMasks.find(name) == m_PassClassMasks.end()) { LOGERROR(L"Invalid passability class name '%hs'", name.c_str()); return 0; } return m_PassClassMasks[name]; } std::map CCmpPathfinder::GetPassabilityClasses() { return m_PassClassMasks; } ICmpPathfinder::cost_class_t CCmpPathfinder::GetCostClass(const std::string& name) { if (m_UnitCostClassTags.find(name) == m_UnitCostClassTags.end()) { LOGERROR(L"Invalid unit cost class name '%hs'", name.c_str()); return m_UnitCostClassTags["default"]; } return m_UnitCostClassTags[name]; } fixed CCmpPathfinder::DistanceToGoal(CFixedVector2D pos, const CCmpPathfinder::Goal& goal) { switch (goal.type) { case CCmpPathfinder::Goal::POINT: return (pos - CFixedVector2D(goal.x, goal.z)).Length(); case CCmpPathfinder::Goal::CIRCLE: return ((pos - CFixedVector2D(goal.x, goal.z)).Length() - goal.hw).Absolute(); case CCmpPathfinder::Goal::SQUARE: { CFixedVector2D halfSize(goal.hw, goal.hh); CFixedVector2D d(pos.X - goal.x, pos.Y - goal.z); return Geometry::DistanceToSquare(d, goal.u, goal.v, halfSize); } default: debug_warn(L"invalid type"); return fixed::Zero(); } } const Grid& CCmpPathfinder::GetPassabilityGrid() { UpdateGrid(); return *m_Grid; } void CCmpPathfinder::UpdateGrid() { // If the terrain was resized then delete the old grid data if (m_Grid && m_MapSize != GetSimContext().GetTerrain().GetTilesPerSide()) { SAFE_DELETE(m_Grid); SAFE_DELETE(m_ObstructionGrid); m_TerrainDirty = true; } // Initialise the terrain data when first needed if (!m_Grid) { // TOOD: these bits should come from ICmpTerrain ssize_t size = GetSimContext().GetTerrain().GetTilesPerSide(); ENSURE(size >= 1 && size <= 0xffff); // must fit in 16 bits m_MapSize = size; m_Grid = new Grid(m_MapSize, m_MapSize); m_ObstructionGrid = new Grid(m_MapSize, m_MapSize); } CmpPtr cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY); bool obstructionsDirty = cmpObstructionManager->Rasterise(*m_ObstructionGrid); if (obstructionsDirty && !m_TerrainDirty) { PROFILE("UpdateGrid obstructions"); // Obstructions changed - we need to recompute passability // Since terrain hasn't changed we only need to update the obstruction bits // and can skip the rest of the data // TODO: if ObstructionManager::SetPassabilityCircular was called at runtime // (which should probably never happen, but that's not guaranteed), // then TILE_OUTOFBOUNDS will change and we can't use this fast path, but // currently it'll just set obstructionsDirty and we won't notice for (u16 j = 0; j < m_MapSize; ++j) { for (u16 i = 0; i < m_MapSize; ++i) { TerrainTile& t = m_Grid->get(i, j); u8 obstruct = m_ObstructionGrid->get(i, j); if (obstruct & ICmpObstructionManager::TILE_OBSTRUCTED_PATHFINDING) t |= 1; else t &= ~1; if (obstruct & ICmpObstructionManager::TILE_OBSTRUCTED_FOUNDATION) t |= 2; else t &= ~2; } } ++m_Grid->m_DirtyID; } else if (obstructionsDirty || m_TerrainDirty) { PROFILE("UpdateGrid full"); // Obstructions or terrain changed - we need to recompute passability // TODO: only bother recomputing the region that has actually changed CmpPtr cmpWaterMan(GetSimContext(), SYSTEM_ENTITY); + // TOOD: these bits should come from ICmpTerrain CTerrain& terrain = GetSimContext().GetTerrain(); + // avoid integer overflow in intermediate calculation + const u16 shoreMax = 32767; + + // First pass - find underwater tiles + Grid waterGrid(m_MapSize, m_MapSize); + for (u16 j = 0; j < m_MapSize; ++j) + { + for (u16 i = 0; i < m_MapSize; ++i) + { + fixed x, z; + TileCenter(i, j, x, z); + + bool underWater = !cmpWaterMan.null() && (cmpWaterMan->GetWaterLevel(x, z) > terrain.GetExactGroundLevelFixed(x, z)); + waterGrid.set(i, j, underWater); + } + } + // Second pass - find shore tiles + Grid shoreGrid(m_MapSize, m_MapSize); + for (u16 j = 0; j < m_MapSize; ++j) + { + for (u16 i = 0; i < m_MapSize; ++i) + { + // Find a land tile + if (!waterGrid.get(i, j) && ( + (i > 0 && waterGrid.get(i-1, j)) || (i > 0 && j < m_MapSize-1 && waterGrid.get(i-1, j+1)) || (i > 0 && j > 0 && waterGrid.get(i-1, j-1)) || + (i < m_MapSize-1 && waterGrid.get(i+1, j)) || (i < m_MapSize-1 && j < m_MapSize-1 && waterGrid.get(i+1, j+1)) || (i < m_MapSize-1 && j > 0 && waterGrid.get(i+1, j-1)) || + (j > 0 && waterGrid.get(i, j-1)) || (j < m_MapSize-1 && waterGrid.get(i, j+1)) + )) + { + shoreGrid.set(i, j, 0); + } + else + { + shoreGrid.set(i, j, shoreMax); + } + } + } + + // Expand influences to find shore distance + for (size_t y = 0; y < m_MapSize; ++y) + { + u16 min = shoreMax; + for (size_t x = 0; x < m_MapSize; ++x) + { + u16 g = shoreGrid.get(x, y); + if (g > min) + shoreGrid.set(x, y, min); + else if (g < min) + min = g; + + ++min; + } + for (size_t x = m_MapSize; x > 0; --x) + { + u16 g = shoreGrid.get(x-1, y); + if (g > min) + shoreGrid.set(x-1, y, min); + else if (g < min) + min = g; + + ++min; + } + } + for (size_t x = 0; x < m_MapSize; ++x) + { + u16 min = shoreMax; + for (size_t y = 0; y < m_MapSize; ++y) + { + u16 g = shoreGrid.get(x, y); + if (g > min) + shoreGrid.set(x, y, min); + else if (g < min) + min = g; + + ++min; + } + for (size_t y = m_MapSize; y > 0; --y) + { + u16 g = shoreGrid.get(x, y-1); + if (g > min) + shoreGrid.set(x, y-1, min); + else if (g < min) + min = g; + + ++min; + } + } + + // Apply passability classes to terrain for (u16 j = 0; j < m_MapSize; ++j) { for (u16 i = 0; i < m_MapSize; ++i) { fixed x, z; TileCenter(i, j, x, z); TerrainTile t = 0; u8 obstruct = m_ObstructionGrid->get(i, j); - fixed height = terrain.GetVertexGroundLevelFixed(i, j); // TODO: should use tile centre + fixed height = terrain.GetExactGroundLevelFixed(x, z); fixed water; if (!cmpWaterMan.null()) water = cmpWaterMan->GetWaterLevel(x, z); fixed depth = water - height; fixed slope = terrain.GetSlopeFixed(i, j); + fixed shoredist = fixed::FromInt(shoreGrid.get(i, j)); + if (obstruct & ICmpObstructionManager::TILE_OBSTRUCTED_PATHFINDING) t |= 1; if (obstruct & ICmpObstructionManager::TILE_OBSTRUCTED_FOUNDATION) t |= 2; if (obstruct & ICmpObstructionManager::TILE_OUTOFBOUNDS) { // If out of bounds, nobody is allowed to pass for (size_t n = 0; n < m_PassClasses.size(); ++n) t |= m_PassClasses[n].m_Mask; } else { for (size_t n = 0; n < m_PassClasses.size(); ++n) { - if (!m_PassClasses[n].IsPassable(depth, slope)) + if (!m_PassClasses[n].IsPassable(depth, slope, shoredist)) t |= m_PassClasses[n].m_Mask; } } std::string moveClass = terrain.GetMovementClass(i, j); if (m_TerrainCostClassTags.find(moveClass) != m_TerrainCostClassTags.end()) t |= COST_CLASS_MASK(m_TerrainCostClassTags[moveClass]); m_Grid->set(i, j, t); } } m_TerrainDirty = false; ++m_Grid->m_DirtyID; } } ////////////////////////////////////////////////////////// // Async path requests: u32 CCmpPathfinder::ComputePathAsync(entity_pos_t x0, entity_pos_t z0, const Goal& goal, pass_class_t passClass, cost_class_t costClass, entity_id_t notify) { AsyncLongPathRequest req = { m_NextAsyncTicket++, x0, z0, goal, passClass, costClass, notify }; m_AsyncLongPathRequests.push_back(req); return req.ticket; } u32 CCmpPathfinder::ComputeShortPathAsync(entity_pos_t x0, entity_pos_t z0, entity_pos_t r, entity_pos_t range, const Goal& goal, pass_class_t passClass, bool avoidMovingUnits, entity_id_t group, entity_id_t notify) { AsyncShortPathRequest req = { m_NextAsyncTicket++, x0, z0, r, range, goal, passClass, avoidMovingUnits, group, notify }; m_AsyncShortPathRequests.push_back(req); return req.ticket; } void CCmpPathfinder::FinishAsyncRequests() { // Save the request queue in case it gets modified while iterating std::vector longRequests; m_AsyncLongPathRequests.swap(longRequests); std::vector shortRequests; m_AsyncShortPathRequests.swap(shortRequests); // TODO: we should only compute one path per entity per turn // TODO: this computation should be done incrementally, spread // across multiple frames (or even multiple turns) ProcessLongRequests(longRequests); ProcessShortRequests(shortRequests); } void CCmpPathfinder::ProcessLongRequests(const std::vector& longRequests) { for (size_t i = 0; i < longRequests.size(); ++i) { const AsyncLongPathRequest& req = longRequests[i]; Path path; ComputePath(req.x0, req.z0, req.goal, req.passClass, req.costClass, path); CMessagePathResult msg(req.ticket, path); GetSimContext().GetComponentManager().PostMessage(req.notify, msg); } } void CCmpPathfinder::ProcessShortRequests(const std::vector& shortRequests) { for (size_t i = 0; i < shortRequests.size(); ++i) { const AsyncShortPathRequest& req = shortRequests[i]; Path path; ControlGroupMovementObstructionFilter filter(req.avoidMovingUnits, req.group); ComputeShortPath(filter, req.x0, req.z0, req.r, req.range, req.goal, req.passClass, path); CMessagePathResult msg(req.ticket, path); GetSimContext().GetComponentManager().PostMessage(req.notify, msg); } } void CCmpPathfinder::ProcessSameTurnMoves() { u32 moveCount; if (!m_AsyncLongPathRequests.empty()) { // Figure out how many moves we can do this time moveCount = m_MaxSameTurnMoves - m_SameTurnMovesCount; if (moveCount <= 0) return; // Copy the long request elements we are going to process into a new array std::vector longRequests; if (m_AsyncLongPathRequests.size() <= moveCount) { m_AsyncLongPathRequests.swap(longRequests); moveCount = longRequests.size(); } else { longRequests.resize(moveCount); copy(m_AsyncLongPathRequests.begin(), m_AsyncLongPathRequests.begin() + moveCount, longRequests.begin()); m_AsyncLongPathRequests.erase(m_AsyncLongPathRequests.begin(), m_AsyncLongPathRequests.begin() + moveCount); } ProcessLongRequests(longRequests); m_SameTurnMovesCount += moveCount; } if (!m_AsyncShortPathRequests.empty()) { // Figure out how many moves we can do now moveCount = m_MaxSameTurnMoves - m_SameTurnMovesCount; if (moveCount <= 0) return; // Copy the short request elements we are going to process into a new array std::vector shortRequests; if (m_AsyncShortPathRequests.size() <= moveCount) { m_AsyncShortPathRequests.swap(shortRequests); moveCount = shortRequests.size(); } else { shortRequests.resize(moveCount); copy(m_AsyncShortPathRequests.begin(), m_AsyncShortPathRequests.begin() + moveCount, shortRequests.begin()); m_AsyncShortPathRequests.erase(m_AsyncShortPathRequests.begin(), m_AsyncShortPathRequests.begin() + moveCount); } ProcessShortRequests(shortRequests); m_SameTurnMovesCount += moveCount; } } +bool CCmpPathfinder::CheckUnitPlacement(const IObstructionTestFilter& filter, + entity_pos_t x, entity_pos_t z, entity_pos_t r, pass_class_t passClass) +{ + // Check unit obstruction + CmpPtr cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY); + if (cmpObstructionManager.null()) + return false; + + if (cmpObstructionManager->TestUnitShape(filter, x, z, r, NULL)) + return false; + + // Test against terrain: + + UpdateGrid(); + + u16 i0, j0, i1, j1; + NearestTile(x - r, z - r, i0, j0); + NearestTile(x + r, z + r, i1, j1); + for (u16 j = j0; j <= j1; ++j) + { + for (u16 i = i0; i <= i1; ++i) + { + if (!IS_TERRAIN_PASSABLE(m_Grid->get(i,j), passClass)) + { + return false; + } + } + } + return true; +} + +bool CCmpPathfinder::CheckBuildingPlacement(const IObstructionTestFilter& filter, + entity_pos_t x, entity_pos_t z, entity_pos_t a, entity_pos_t w, + entity_pos_t h, entity_id_t id, pass_class_t passClass) +{ + // Check unit obstruction + CmpPtr cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY); + if (cmpObstructionManager.null()) + return false; + + if (cmpObstructionManager->TestStaticShape(filter, x, z, a, w, h, NULL)) + return false; + + // Test against terrain: + + UpdateGrid(); + + CmpPtr cmpObstruction(GetSimContext(), id); + if (cmpObstruction.null()) + return false; + + ICmpObstructionManager::ObstructionSquare square; + if (!cmpObstruction->GetObstructionSquare(square)) + return false; + + CFixedVector2D halfSize(square.hw, square.hh); + halfSize = halfSize * 1.41421f; + CFixedVector2D halfBound = Geometry::GetHalfBoundingBox(square.u, square.v, halfSize); + + u16 i0, j0, i1, j1; + NearestTile(square.x - halfBound.X, square.z - halfBound.Y, i0, j0); + NearestTile(square.x + halfBound.X, square.z + halfBound.Y, i1, j1); + for (u16 j = j0; j <= j1; ++j) + { + for (u16 i = i0; i <= i1; ++i) + { + entity_pos_t x, z; + TileCenter(i, j, x, z); + if (Geometry::PointIsInSquare(CFixedVector2D(x - square.x, z - square.z), square.u, square.v, halfSize) + && !IS_TERRAIN_PASSABLE(m_Grid->get(i,j), passClass)) + { + return false; + } + } + } + + return true; +} Index: ps/trunk/source/simulation2/components/ICmpTerritoryManager.h =================================================================== --- ps/trunk/source/simulation2/components/ICmpTerritoryManager.h (revision 9969) +++ ps/trunk/source/simulation2/components/ICmpTerritoryManager.h (revision 9970) @@ -1,35 +1,42 @@ /* Copyright (C) 2011 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_ICMPTERRITORYMANAGER #define INCLUDED_ICMPTERRITORYMANAGER -#include "simulation2/system/Interface.h" - #include "simulation2/helpers/Grid.h" +#include "simulation2/system/Interface.h" +#include "simulation2/components/ICmpPosition.h" class ICmpTerritoryManager : public IComponent { public: virtual bool NeedUpdate(size_t* dirtyID) = 0; virtual const Grid& GetTerritoryGrid() = 0; + /** + * Get owner of territory at given position + * + * @return player ID of owner; 0 if neutral territory + */ + virtual int32_t GetOwner(entity_pos_t x, entity_pos_t z) = 0; + DECLARE_INTERFACE_TYPE(TerritoryManager) }; #endif // INCLUDED_ICMPTERRITORYMANAGER Index: ps/trunk/source/simulation2/components/CCmpObstruction.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpObstruction.cpp (revision 9969) +++ ps/trunk/source/simulation2/components/CCmpObstruction.cpp (revision 9970) @@ -1,409 +1,412 @@ /* Copyright (C) 2011 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 "ICmpObstruction.h" -#include "ICmpObstructionManager.h" -#include "ICmpPosition.h" +#include "simulation2/components/ICmpObstructionManager.h" +#include "simulation2/components/ICmpPosition.h" #include "simulation2/MessageTypes.h" /** * Obstruction implementation. This keeps the ICmpPathfinder's model of the world updated when the * entities move and die, with shapes derived from ICmpFootprint. */ class CCmpObstruction : public ICmpObstruction { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeToMessageType(MT_PositionChanged); componentManager.SubscribeToMessageType(MT_Destroy); } DEFAULT_COMPONENT_ALLOCATOR(Obstruction) // Template state: enum { STATIC, UNIT } m_Type; entity_pos_t m_Size0; // radius or width entity_pos_t m_Size1; // radius or depth u8 m_TemplateFlags; // Dynamic state: bool m_Active; // whether the obstruction is obstructing or just an inactive placeholder bool m_Moving; entity_id_t m_ControlGroup; ICmpObstructionManager::tag_t m_Tag; u8 m_Flags; static std::string GetSchema() { return "" "Causes this entity to obstruct the motion of other units." "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" ""; } virtual void Init(const CParamNode& paramNode) { if (paramNode.GetChild("Unit").IsOk()) { m_Type = UNIT; m_Size0 = m_Size1 = paramNode.GetChild("Unit").GetChild("@radius").ToFixed(); } else { m_Type = STATIC; m_Size0 = paramNode.GetChild("Static").GetChild("@width").ToFixed(); m_Size1 = paramNode.GetChild("Static").GetChild("@depth").ToFixed(); } m_TemplateFlags = 0; if (paramNode.GetChild("BlockMovement").ToBool()) m_TemplateFlags |= ICmpObstructionManager::FLAG_BLOCK_MOVEMENT; if (paramNode.GetChild("BlockPathfinding").ToBool()) m_TemplateFlags |= ICmpObstructionManager::FLAG_BLOCK_PATHFINDING; if (paramNode.GetChild("BlockFoundation").ToBool()) m_TemplateFlags |= ICmpObstructionManager::FLAG_BLOCK_FOUNDATION; if (paramNode.GetChild("BlockConstruction").ToBool()) m_TemplateFlags |= ICmpObstructionManager::FLAG_BLOCK_CONSTRUCTION; m_Flags = m_TemplateFlags; if (paramNode.GetChild("DisableBlockMovement").ToBool()) m_Flags &= ~ICmpObstructionManager::FLAG_BLOCK_MOVEMENT; if (paramNode.GetChild("DisableBlockPathfinding").ToBool()) m_Flags &= ~ICmpObstructionManager::FLAG_BLOCK_PATHFINDING; m_Active = paramNode.GetChild("Active").ToBool(); m_Tag = ICmpObstructionManager::tag_t(); m_Moving = false; m_ControlGroup = GetEntityId(); } virtual void Deinit() { } template void SerializeCommon(S& serialize) { serialize.Bool("active", m_Active); serialize.Bool("moving", m_Moving); serialize.NumberU32_Unbounded("control group", m_ControlGroup); serialize.NumberU32_Unbounded("tag", m_Tag.n); serialize.NumberU8_Unbounded("flags", m_Flags); } virtual void Serialize(ISerializer& serialize) { SerializeCommon(serialize); } virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) { Init(paramNode); SerializeCommon(deserialize); } virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_PositionChanged: { if (!m_Active) break; const CMessagePositionChanged& data = static_cast (msg); if (!data.inWorld && !m_Tag.valid()) break; // nothing needs to change CmpPtr cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY); if (cmpObstructionManager.null()) break; // error if (data.inWorld && m_Tag.valid()) { cmpObstructionManager->MoveShape(m_Tag, data.x, data.z, data.a); } else if (data.inWorld && !m_Tag.valid()) { // Need to create a new pathfinder shape: if (m_Type == STATIC) m_Tag = cmpObstructionManager->AddStaticShape(GetEntityId(), data.x, data.z, data.a, m_Size0, m_Size1, m_Flags); else m_Tag = cmpObstructionManager->AddUnitShape(GetEntityId(), data.x, data.z, m_Size0, m_Flags | (m_Moving ? ICmpObstructionManager::FLAG_MOVING : 0), m_ControlGroup); } else if (!data.inWorld && m_Tag.valid()) { cmpObstructionManager->RemoveShape(m_Tag); m_Tag = ICmpObstructionManager::tag_t(); } break; } case MT_Destroy: { if (m_Tag.valid()) { CmpPtr cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY); if (cmpObstructionManager.null()) break; // error cmpObstructionManager->RemoveShape(m_Tag); m_Tag = ICmpObstructionManager::tag_t(); } break; } } } virtual void SetActive(bool active) { if (active && !m_Active) { m_Active = true; // Construct the obstruction shape CmpPtr cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY); if (cmpObstructionManager.null()) return; // error CmpPtr cmpPosition(GetSimContext(), GetEntityId()); if (cmpPosition.null()) return; // error if (!cmpPosition->IsInWorld()) return; // don't need an obstruction CFixedVector2D pos = cmpPosition->GetPosition2D(); if (m_Type == STATIC) m_Tag = cmpObstructionManager->AddStaticShape(GetEntityId(), pos.X, pos.Y, cmpPosition->GetRotation().Y, m_Size0, m_Size1, m_Flags); else m_Tag = cmpObstructionManager->AddUnitShape(GetEntityId(), pos.X, pos.Y, m_Size0, m_Flags | (m_Moving ? ICmpObstructionManager::FLAG_MOVING : 0), m_ControlGroup); } else if (!active && m_Active) { m_Active = false; // Delete the obstruction shape if (m_Tag.valid()) { CmpPtr cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY); if (cmpObstructionManager.null()) return; // error cmpObstructionManager->RemoveShape(m_Tag); m_Tag = ICmpObstructionManager::tag_t(); } } // else we didn't change the active status } virtual void SetDisableBlockMovementPathfinding(bool disabled) { if (disabled) { // Remove the blocking flags m_Flags &= ~ICmpObstructionManager::FLAG_BLOCK_MOVEMENT; m_Flags &= ~ICmpObstructionManager::FLAG_BLOCK_PATHFINDING; } else { // Add the blocking flags if the template had enabled them m_Flags |= (m_TemplateFlags & ICmpObstructionManager::FLAG_BLOCK_MOVEMENT); m_Flags |= (m_TemplateFlags & ICmpObstructionManager::FLAG_BLOCK_PATHFINDING); } // Reset the shape with the new flags (kind of inefficiently - we // should have a ICmpObstructionManager::SetFlags function or something) if (m_Active) { SetActive(false); SetActive(true); } } virtual bool GetBlockMovementFlag() { return (m_TemplateFlags & ICmpObstructionManager::FLAG_BLOCK_MOVEMENT) != 0; } virtual ICmpObstructionManager::tag_t GetObstruction() { return m_Tag; } virtual bool GetObstructionSquare(ICmpObstructionManager::ObstructionSquare& out) { CmpPtr cmpPosition(GetSimContext(), GetEntityId()); if (cmpPosition.null()) return false; // error CmpPtr cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY); if (cmpObstructionManager.null()) return false; // error if (!cmpPosition->IsInWorld()) return false; // no obstruction square CFixedVector2D pos = cmpPosition->GetPosition2D(); if (m_Type == STATIC) out = cmpObstructionManager->GetStaticShapeObstruction(pos.X, pos.Y, cmpPosition->GetRotation().Y, m_Size0, m_Size1); else out = cmpObstructionManager->GetUnitShapeObstruction(pos.X, pos.Y, m_Size0); return true; } virtual entity_pos_t GetUnitRadius() { if (m_Type == UNIT) return m_Size0; else return entity_pos_t::Zero(); } - virtual bool CheckFoundationCollisions() + virtual bool CheckFoundation(std::string className) { CmpPtr cmpPosition(GetSimContext(), GetEntityId()); if (cmpPosition.null()) return false; // error if (!cmpPosition->IsInWorld()) return false; // no obstruction CFixedVector2D pos = cmpPosition->GetPosition2D(); - CmpPtr cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY); - if (cmpObstructionManager.null()) + CmpPtr cmpPathfinder(GetSimContext(), SYSTEM_ENTITY); + if (cmpPathfinder.null()) return false; // error + // Get passability class + ICmpPathfinder::pass_class_t passClass = cmpPathfinder->GetPassabilityClass(className); + // Ignore collisions with self, or with non-foundation-blocking obstructions SkipTagFlagsObstructionFilter filter(m_Tag, ICmpObstructionManager::FLAG_BLOCK_FOUNDATION); if (m_Type == STATIC) - return cmpObstructionManager->TestStaticShape(filter, pos.X, pos.Y, cmpPosition->GetRotation().Y, m_Size0, m_Size1, NULL); + return cmpPathfinder->CheckBuildingPlacement(filter, pos.X, pos.Y, cmpPosition->GetRotation().Y, m_Size0, m_Size1, GetEntityId(), passClass); else - return cmpObstructionManager->TestUnitShape(filter, pos.X, pos.Y, m_Size0, NULL); + return cmpPathfinder->CheckUnitPlacement(filter, pos.X, pos.Y, m_Size0, passClass); } virtual std::vector GetConstructionCollisions() { std::vector ret; CmpPtr cmpPosition(GetSimContext(), GetEntityId()); if (cmpPosition.null()) return ret; // error if (!cmpPosition->IsInWorld()) return ret; // no obstruction CFixedVector2D pos = cmpPosition->GetPosition2D(); CmpPtr cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY); if (cmpObstructionManager.null()) return ret; // error // Ignore collisions with self, or with non-construction-blocking obstructions SkipTagFlagsObstructionFilter filter(m_Tag, ICmpObstructionManager::FLAG_BLOCK_CONSTRUCTION); if (m_Type == STATIC) cmpObstructionManager->TestStaticShape(filter, pos.X, pos.Y, cmpPosition->GetRotation().Y, m_Size0, m_Size1, &ret); else cmpObstructionManager->TestUnitShape(filter, pos.X, pos.Y, m_Size0, &ret); return ret; } virtual void SetMovingFlag(bool enabled) { m_Moving = enabled; if (m_Tag.valid() && m_Type == UNIT) { CmpPtr cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY); if (!cmpObstructionManager.null()) cmpObstructionManager->SetUnitMovingFlag(m_Tag, m_Moving); } } virtual void SetControlGroup(entity_id_t group) { m_ControlGroup = group; if (m_Tag.valid() && m_Type == UNIT) { CmpPtr cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY); if (!cmpObstructionManager.null()) cmpObstructionManager->SetUnitControlGroup(m_Tag, m_ControlGroup); } } }; REGISTER_COMPONENT_TYPE(Obstruction) Index: ps/trunk/source/simulation2/components/ICmpUnitMotion.h =================================================================== --- ps/trunk/source/simulation2/components/ICmpUnitMotion.h (revision 9969) +++ ps/trunk/source/simulation2/components/ICmpUnitMotion.h (revision 9970) @@ -1,110 +1,116 @@ -/* Copyright (C) 2010 Wildfire Games. +/* Copyright (C) 2011 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_ICMPUNITMOTION #define INCLUDED_ICMPUNITMOTION #include "simulation2/system/Interface.h" -#include "ICmpPosition.h" // for entity_pos_t +#include "simulation2/components/ICmpPathfinder.h" // for pass_class_t +#include "simulation2/components/ICmpPosition.h" // for entity_pos_t /** * Motion interface for entities with complex movement capabilities. * (Simpler motion is handled by ICmpMotion instead.) * * Currently this is limited to telling the entity to walk to a point. * Eventually it should support different movement speeds, moving to areas * instead of points, moving as part of a group, moving as part of a formation, * etc. */ class ICmpUnitMotion : public IComponent { public: /** * Attempt to walk into range of a to a given point, or as close as possible. * If the unit is already in range, or cannot move anywhere at all, or if there is * some other error, then returns false. * Otherwise, returns true and sends a MotionChanged message after starting to move, * and sends another MotionChanged after finishing moving. * If maxRange is negative, then the maximum range is treated as infinity. */ virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) = 0; /** * Determine whether the target is within the given range, using the same measurement * as MoveToTargetRange. */ virtual bool IsInTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) = 0; /** * Attempt to walk into range of a given target entity, or as close as possible. * If the unit is already in range, or cannot move anywhere at all, or if there is * some other error, then returns false. * Otherwise, returns true and sends a MotionChanged message after starting to move, * and sends another MotionChanged after finishing moving. * If maxRange is negative, then the maximum range is treated as infinity. */ virtual bool MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) = 0; /** * Join a formation, and move towards a given offset relative to the formation controller entity. * Continues following the formation until given a different command. */ virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) = 0; /** * Turn to look towards the given point. */ virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z) = 0; /** * Stop moving immediately. */ virtual void StopMoving() = 0; /** * Set the current movement speed. */ virtual void SetSpeed(fixed speed) = 0; /** * Get the default speed that this unit will have when walking, in metres per second. */ virtual fixed GetWalkSpeed() = 0; /** * Get the default speed that this unit will have when running, in metres per second. */ virtual fixed GetRunSpeed() = 0; /** + * Get the unit's passability class. + */ + virtual ICmpPathfinder::pass_class_t GetPassabilityClass() = 0; + + /** * Override the default obstruction radius, used for planning paths and checking for collisions. * Bad things may happen if this entity has an active Obstruction component with a larger * radius. (This is intended primarily for formation controllers.) */ virtual void SetUnitRadius(fixed radius) = 0; /** * Toggle the rendering of debug info. */ virtual void SetDebugOverlay(bool enabled) = 0; DECLARE_INTERFACE_TYPE(UnitMotion) }; #endif // INCLUDED_ICMPUNITMOTION Index: ps/trunk/source/simulation2/components/ICmpObstruction.cpp =================================================================== --- ps/trunk/source/simulation2/components/ICmpObstruction.cpp (revision 9969) +++ ps/trunk/source/simulation2/components/ICmpObstruction.cpp (revision 9970) @@ -1,32 +1,32 @@ /* Copyright (C) 2011 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 "ICmpObstruction.h" #include "simulation2/system/InterfaceScripted.h" BEGIN_INTERFACE_WRAPPER(Obstruction) DEFINE_INTERFACE_METHOD_0("GetUnitRadius", entity_pos_t, ICmpObstruction, GetUnitRadius) -DEFINE_INTERFACE_METHOD_0("CheckFoundationCollisions", bool, ICmpObstruction, CheckFoundationCollisions) +DEFINE_INTERFACE_METHOD_1("CheckFoundation", bool, ICmpObstruction, CheckFoundation, std::string) DEFINE_INTERFACE_METHOD_0("GetConstructionCollisions", std::vector, ICmpObstruction, GetConstructionCollisions) DEFINE_INTERFACE_METHOD_1("SetActive", void, ICmpObstruction, SetActive, bool) DEFINE_INTERFACE_METHOD_1("SetDisableBlockMovementPathfinding", void, ICmpObstruction, SetDisableBlockMovementPathfinding, bool) DEFINE_INTERFACE_METHOD_0("GetBlockMovementFlag", bool, ICmpObstruction, GetBlockMovementFlag) DEFINE_INTERFACE_METHOD_1("SetControlGroup", void, ICmpObstruction, SetControlGroup, entity_id_t) END_INTERFACE_WRAPPER(Obstruction) Index: ps/trunk/source/simulation2/components/ICmpPathfinder.h =================================================================== --- ps/trunk/source/simulation2/components/ICmpPathfinder.h (revision 9969) +++ ps/trunk/source/simulation2/components/ICmpPathfinder.h (revision 9970) @@ -1,170 +1,184 @@ /* Copyright (C) 2011 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_ICMPPATHFINDER #define INCLUDED_ICMPPATHFINDER #include "simulation2/system/Interface.h" #include "simulation2/helpers/Position.h" #include "maths/FixedVector2D.h" #include #include class IObstructionTestFilter; template class Grid; /** * Pathfinder algorithms. * * There are two different modes: a tile-based pathfinder that works over long distances and * accounts for terrain costs but ignore units, and a 'short' vertex-based pathfinder that * provides precise paths and avoids other units. * * Both use the same concept of a Goal: either a point, circle or square. * (If the starting point is inside the goal shape then the path will move outwards * to reach the shape's outline.) * * The output is a list of waypoints. */ class ICmpPathfinder : public IComponent { public: typedef u16 pass_class_t; typedef u8 cost_class_t; struct Goal { enum Type { POINT, CIRCLE, SQUARE } type; entity_pos_t x, z; // position of center CFixedVector2D u, v; // if SQUARE, then orthogonal unit axes entity_pos_t hw, hh; // if SQUARE, then half width & height; if CIRCLE, then hw is radius }; struct Waypoint { entity_pos_t x, z; }; /** * Returned path. * Waypoints are in *reverse* order (the earliest is at the back of the list) */ struct Path { std::vector m_Waypoints; }; /** * Get the list of all known passability classes. */ virtual std::map GetPassabilityClasses() = 0; /** * Get the tag for a given passability class name. * Logs an error and returns something acceptable if the name is unrecognised. */ virtual pass_class_t GetPassabilityClass(const std::string& name) = 0; /** * Get the tag for a given movement cost class name. * Logs an error and returns something acceptable if the name is unrecognised. */ virtual cost_class_t GetCostClass(const std::string& name) = 0; virtual const Grid& GetPassabilityGrid() = 0; /** * Compute a tile-based path from the given point to the goal, and return the set of waypoints. * The waypoints correspond to the centers of horizontally/vertically adjacent tiles * along the path. */ virtual void ComputePath(entity_pos_t x0, entity_pos_t z0, const Goal& goal, pass_class_t passClass, cost_class_t costClass, Path& ret) = 0; /** * Asynchronous version of ComputePath. * The result will be sent as CMessagePathResult to 'notify'. * Returns a unique non-zero number, which will match the 'ticket' in the result, * so callers can recognise each individual request they make. */ virtual u32 ComputePathAsync(entity_pos_t x0, entity_pos_t z0, const Goal& goal, pass_class_t passClass, cost_class_t costClass, entity_id_t notify) = 0; /** * If the debug overlay is enabled, render the path that will computed by ComputePath. */ virtual void SetDebugPath(entity_pos_t x0, entity_pos_t z0, const Goal& goal, pass_class_t passClass, cost_class_t costClass) = 0; /** * Compute a precise path from the given point to the goal, and return the set of waypoints. * The path is based on the full set of obstructions that pass the filter, such that * a unit of radius 'r' will be able to follow the path with no collisions. * The path is restricted to a box of radius 'range' from the starting point. */ virtual void ComputeShortPath(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t r, entity_pos_t range, const Goal& goal, pass_class_t passClass, Path& ret) = 0; /** * Asynchronous version of ComputeShortPath (using ControlGroupObstructionFilter). * The result will be sent as CMessagePathResult to 'notify'. * Returns a unique non-zero number, which will match the 'ticket' in the result, * so callers can recognise each individual request they make. */ virtual u32 ComputeShortPathAsync(entity_pos_t x0, entity_pos_t z0, entity_pos_t r, entity_pos_t range, const Goal& goal, pass_class_t passClass, bool avoidMovingUnits, entity_id_t group, entity_id_t notify) = 0; /** * Find the speed factor (typically around 1.0) for a unit of the given cost class * at the given position. */ virtual fixed GetMovementSpeed(entity_pos_t x0, entity_pos_t z0, cost_class_t costClass) = 0; /** * Returns the coordinates of the point on the goal that is closest to pos in a straight line. */ virtual CFixedVector2D GetNearestPointOnGoal(CFixedVector2D pos, const Goal& goal) = 0; /** * Check whether the given movement line is valid and doesn't hit any obstructions * or impassable terrain. * Returns true if the movement is okay. */ virtual bool CheckMovement(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, pass_class_t passClass) = 0; /** + * Check whether a unit placed here is valid and doesn't hit any obstructions + * or impassable terrain. + * Returns true if the placement is okay. + */ + virtual bool CheckUnitPlacement(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r, pass_class_t passClass) = 0; + + /** + * Check whether a building placed here is valid and doesn't hit any obstructions + * or impassable terrain. + * Returns true if the placement is okay. + */ + virtual bool CheckBuildingPlacement(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t a, entity_pos_t w, entity_pos_t h, entity_id_t id, pass_class_t passClass) = 0; + + /** * Toggle the storage and rendering of debug info. */ virtual void SetDebugOverlay(bool enabled) = 0; /** * Finish computing asynchronous path requests and send the CMessagePathResult messages. */ virtual void FinishAsyncRequests() = 0; /** * Process moves during the same turn they were created in to improve responsiveness. */ virtual void ProcessSameTurnMoves() = 0; DECLARE_INTERFACE_TYPE(Pathfinder) }; #endif // INCLUDED_ICMPPATHFINDER Index: ps/trunk/source/simulation2/components/ICmpTerritoryManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/ICmpTerritoryManager.cpp (revision 9969) +++ ps/trunk/source/simulation2/components/ICmpTerritoryManager.cpp (revision 9970) @@ -1,25 +1,26 @@ /* Copyright (C) 2011 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 "ICmpTerritoryManager.h" #include "simulation2/system/InterfaceScripted.h" BEGIN_INTERFACE_WRAPPER(TerritoryManager) +DEFINE_INTERFACE_METHOD_2("GetOwner", int32_t, ICmpTerritoryManager, GetOwner, entity_pos_t, entity_pos_t) END_INTERFACE_WRAPPER(TerritoryManager) Index: ps/trunk/source/simulation2/components/CCmpUnitMotion.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpUnitMotion.cpp (revision 9969) +++ ps/trunk/source/simulation2/components/CCmpUnitMotion.cpp (revision 9970) @@ -1,1573 +1,1578 @@ /* Copyright (C) 2011 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 "ICmpUnitMotion.h" -#include "ICmpObstruction.h" -#include "ICmpObstructionManager.h" -#include "ICmpOwnership.h" -#include "ICmpPosition.h" -#include "ICmpPathfinder.h" -#include "ICmpRangeManager.h" -#include "simulation2/MessageTypes.h" +#include "simulation2/components/ICmpObstruction.h" +#include "simulation2/components/ICmpObstructionManager.h" +#include "simulation2/components/ICmpOwnership.h" +#include "simulation2/components/ICmpPosition.h" +#include "simulation2/components/ICmpPathfinder.h" +#include "simulation2/components/ICmpRangeManager.h" #include "simulation2/helpers/Geometry.h" #include "simulation2/helpers/Render.h" +#include "simulation2/MessageTypes.h" #include "simulation2/serialization/SerializeTemplates.h" #include "graphics/Overlay.h" #include "graphics/Terrain.h" #include "maths/FixedVector2D.h" #include "ps/CLogger.h" #include "ps/Profile.h" #include "renderer/Scene.h" /** * When advancing along the long path, and picking a new waypoint to move * towards, we'll pick one that's up to this far from the unit's current * position (to minimise the effects of grid-constrained movement) */ static const entity_pos_t WAYPOINT_ADVANCE_MAX = entity_pos_t::FromInt(CELL_SIZE*8); /** * When advancing along the long path, we'll pick a new waypoint to move * towards if we expect to reach the end of our current short path within * this many turns (assuming constant speed and turn length). * (This could typically be 1, but we need some tolerance in case speeds * or turn lengths change.) */ static const int WAYPOINT_ADVANCE_LOOKAHEAD_TURNS = 4; /** * Maximum range to restrict short path queries to. (Larger ranges are slower, * smaller ranges might miss some legitimate routes around large obstacles.) */ static const entity_pos_t SHORT_PATH_SEARCH_RANGE = entity_pos_t::FromInt(CELL_SIZE*10); /** * When short-pathing to an intermediate waypoint, we aim for a circle of this radius * around the waypoint rather than expecting to reach precisely the waypoint itself * (since it might be inside an obstacle). */ static const entity_pos_t SHORT_PATH_GOAL_RADIUS = entity_pos_t::FromInt(CELL_SIZE*3/2); /** * If we are this close to our target entity/point, then think about heading * for it in a straight line instead of pathfinding. */ static const entity_pos_t DIRECT_PATH_RANGE = entity_pos_t::FromInt(CELL_SIZE*4); /** * If we're following a target entity, * we will recompute our path if the target has moved * more than this distance from where we last pathed to. */ static const entity_pos_t CHECK_TARGET_MOVEMENT_MIN_DELTA = entity_pos_t::FromInt(CELL_SIZE*4); /** * If we're following as part of a formation, * but can't move to our assigned target point in a straight line, * we will recompute our path if the target has moved * more than this distance from where we last pathed to. */ static const entity_pos_t CHECK_TARGET_MOVEMENT_MIN_DELTA_FORMATION = entity_pos_t::FromInt(CELL_SIZE*1); /** * If we're following something but it's more than this distance away along * our path, then don't bother trying to repath regardless of how much it has * moved, until we get this close to the end of our old path. */ static const entity_pos_t CHECK_TARGET_MOVEMENT_AT_MAX_DIST = entity_pos_t::FromInt(CELL_SIZE*16); static const CColor OVERLAY_COLOUR_LONG_PATH(1, 1, 1, 1); static const CColor OVERLAY_COLOUR_SHORT_PATH(1, 0, 0, 1); static const entity_pos_t g_GoalDelta = entity_pos_t::FromInt(CELL_SIZE)/4; // for extending the goal outwards/inwards a little bit class CCmpUnitMotion : public ICmpUnitMotion { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeToMessageType(MT_Update_MotionFormation); componentManager.SubscribeToMessageType(MT_Update_MotionUnit); componentManager.SubscribeToMessageType(MT_RenderSubmit); // for debug overlays componentManager.SubscribeToMessageType(MT_PathResult); } DEFAULT_COMPONENT_ALLOCATOR(UnitMotion) bool m_DebugOverlayEnabled; std::vector m_DebugOverlayLongPathLines; std::vector m_DebugOverlayShortPathLines; // Template state: bool m_FormationController; fixed m_WalkSpeed; // in metres per second fixed m_RunSpeed; - u8 m_PassClass; - u8 m_CostClass; + ICmpPathfinder::pass_class_t m_PassClass; + ICmpPathfinder::cost_class_t m_CostClass; // Dynamic state: entity_pos_t m_Radius; enum State { /* * Not moving at all. */ STATE_IDLE, /* * Not moving at all. Will go to IDLE next turn. * (This one-turn delay is a hack to fix animation timings.) */ STATE_STOPPING, /* * Member of a formation. * Pathing to the target (depending on m_PathState). * Target is m_TargetEntity plus m_TargetOffset. */ STATE_FORMATIONMEMBER_PATH, /* * Individual unit or formation controller. * Pathing to the target (depending on m_PathState). * Target is m_TargetPos, m_TargetMinRange, m_TargetMaxRange; * if m_TargetEntity is not INVALID_ENTITY then m_TargetPos is tracking it. */ STATE_INDIVIDUAL_PATH, STATE_MAX }; u8 m_State; enum PathState { /* * There is no path. * (This should only happen in IDLE and STOPPING.) */ PATHSTATE_NONE, /* * We have an outstanding long path request. * No paths are usable yet, so we can't move anywhere. */ PATHSTATE_WAITING_REQUESTING_LONG, /* * We have an outstanding short path request. * m_LongPath is valid. * m_ShortPath is not yet valid, so we can't move anywhere. */ PATHSTATE_WAITING_REQUESTING_SHORT, /* * We are following our path, and have no path requests. * m_LongPath and m_ShortPath are valid. */ PATHSTATE_FOLLOWING, /* * We are following our path, and have an outstanding long path request. * (This is because our target moved a long way and we need to recompute * the whole path). * m_LongPath and m_ShortPath are valid. */ PATHSTATE_FOLLOWING_REQUESTING_LONG, /* * We are following our path, and have an outstanding short path request. * (This is because our target moved and we've got a new long path * which we need to follow). * m_LongPath is valid; m_ShortPath is valid but obsolete. */ PATHSTATE_FOLLOWING_REQUESTING_SHORT, /* * We are following our path, and have an outstanding short path request * to append to our current path. * (This is because we got near the end of our short path and need * to extend it to continue along the long path). * m_LongPath and m_ShortPath are valid. */ PATHSTATE_FOLLOWING_REQUESTING_SHORT_APPEND, PATHSTATE_MAX }; u8 m_PathState; u32 m_ExpectedPathTicket; // asynchronous request ID we're waiting for, or 0 if none entity_id_t m_TargetEntity; CFixedVector2D m_TargetPos; CFixedVector2D m_TargetOffset; entity_pos_t m_TargetMinRange; entity_pos_t m_TargetMaxRange; fixed m_Speed; // Currently active paths (storing waypoints in reverse order). // The last item in each path is the point we're currently heading towards. ICmpPathfinder::Path m_LongPath; ICmpPathfinder::Path m_ShortPath; ICmpPathfinder::Goal m_FinalGoal; static std::string GetSchema() { return "Provides the unit with the ability to move around the world by itself." "" "7.0" "default" "infantry" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" ""; } /* * TODO: the running/charging thing needs to be designed and implemented */ virtual void Init(const CParamNode& paramNode) { m_FormationController = paramNode.GetChild("FormationController").ToBool(); m_WalkSpeed = paramNode.GetChild("WalkSpeed").ToFixed(); m_Speed = m_WalkSpeed; if (paramNode.GetChild("Run").IsOk()) { m_RunSpeed = paramNode.GetChild("Run").GetChild("Speed").ToFixed(); } else { m_RunSpeed = m_WalkSpeed; } CmpPtr cmpPathfinder(GetSimContext(), SYSTEM_ENTITY); if (!cmpPathfinder.null()) { m_PassClass = cmpPathfinder->GetPassabilityClass(paramNode.GetChild("PassabilityClass").ToUTF8()); m_CostClass = cmpPathfinder->GetCostClass(paramNode.GetChild("CostClass").ToUTF8()); } CmpPtr cmpObstruction(GetSimContext(), GetEntityId()); if (!cmpObstruction.null()) m_Radius = cmpObstruction->GetUnitRadius(); m_State = STATE_IDLE; m_PathState = PATHSTATE_NONE; m_ExpectedPathTicket = 0; m_TargetEntity = INVALID_ENTITY; m_FinalGoal.type = ICmpPathfinder::Goal::POINT; m_DebugOverlayEnabled = false; } virtual void Deinit() { } template void SerializeCommon(S& serialize) { serialize.NumberFixed_Unbounded("radius", m_Radius); serialize.NumberU8("state", m_State, 0, STATE_MAX-1); serialize.NumberU8("path state", m_PathState, 0, PATHSTATE_MAX-1); serialize.NumberU32_Unbounded("ticket", m_ExpectedPathTicket); serialize.NumberU32_Unbounded("target entity", m_TargetEntity); serialize.NumberFixed_Unbounded("target pos x", m_TargetPos.X); serialize.NumberFixed_Unbounded("target pos y", m_TargetPos.Y); serialize.NumberFixed_Unbounded("target offset x", m_TargetOffset.X); serialize.NumberFixed_Unbounded("target offset y", m_TargetOffset.Y); serialize.NumberFixed_Unbounded("target min range", m_TargetMinRange); serialize.NumberFixed_Unbounded("target max range", m_TargetMaxRange); serialize.NumberFixed_Unbounded("speed", m_Speed); SerializeVector()(serialize, "long path", m_LongPath.m_Waypoints); SerializeVector()(serialize, "short path", m_ShortPath.m_Waypoints); SerializeGoal()(serialize, "goal", m_FinalGoal); } virtual void Serialize(ISerializer& serialize) { SerializeCommon(serialize); } virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) { Init(paramNode); SerializeCommon(deserialize); } virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_Update_MotionFormation: { if (m_FormationController) { fixed dt = static_cast (msg).turnLength; Move(dt); } break; } case MT_Update_MotionUnit: { if (!m_FormationController) { fixed dt = static_cast (msg).turnLength; Move(dt); } break; } case MT_RenderSubmit: { const CMessageRenderSubmit& msgData = static_cast (msg); RenderSubmit(msgData.collector); break; } case MT_PathResult: { const CMessagePathResult& msgData = static_cast (msg); PathResult(msgData.ticket, msgData.path); break; } } } virtual fixed GetWalkSpeed() { return m_WalkSpeed; } virtual fixed GetRunSpeed() { return m_RunSpeed; } + virtual ICmpPathfinder::pass_class_t GetPassabilityClass() + { + return m_PassClass; + } + virtual void SetSpeed(fixed speed) { m_Speed = speed; } virtual void SetDebugOverlay(bool enabled) { m_DebugOverlayEnabled = enabled; } virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange); virtual bool MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange); virtual bool IsInTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange); virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z); virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z); virtual void StopMoving() { m_ExpectedPathTicket = 0; m_State = STATE_STOPPING; m_PathState = PATHSTATE_NONE; m_LongPath.m_Waypoints.clear(); m_ShortPath.m_Waypoints.clear(); } virtual void SetUnitRadius(fixed radius) { m_Radius = radius; } private: bool ShouldAvoidMovingUnits() { return !m_FormationController; } bool IsFormationMember() { return m_State == STATE_FORMATIONMEMBER_PATH; } void StartFailed() { StopMoving(); m_State = STATE_IDLE; // don't go through the STOPPING state since we never even started CmpPtr cmpObstruction(GetSimContext(), GetEntityId()); if (!cmpObstruction.null()) cmpObstruction->SetMovingFlag(false); CMessageMotionChanged msg(true, true); GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } void MoveFailed() { StopMoving(); CmpPtr cmpObstruction(GetSimContext(), GetEntityId()); if (!cmpObstruction.null()) cmpObstruction->SetMovingFlag(false); CMessageMotionChanged msg(false, true); GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } void StartSucceeded() { CMessageMotionChanged msg(true, false); GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } void MoveSucceeded() { CmpPtr cmpObstruction(GetSimContext(), GetEntityId()); if (!cmpObstruction.null()) cmpObstruction->SetMovingFlag(false); CMessageMotionChanged msg(false, false); GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } /** * Handle the result of an asynchronous path query. */ void PathResult(u32 ticket, const ICmpPathfinder::Path& path); /** * Do the per-turn movement and other updates. */ void Move(fixed dt); /** * Decide whether to approximate the given range from a square target as a circle, * rather than as a square. */ bool ShouldTreatTargetAsCircle(entity_pos_t range, entity_pos_t hw, entity_pos_t hh, entity_pos_t circleRadius); /** * Computes the current location of our target entity (plus offset). * Returns false if no target entity or no valid position. */ bool ComputeTargetPosition(CFixedVector2D& out); /** * Attempts to replace the current path with a straight line to the target * entity, if it's close enough and the route is not obstructed. */ bool TryGoingStraightToTargetEntity(CFixedVector2D from); /** * Returns whether the target entity has moved more than minDelta since our * last path computations, and we're close enough to it to care. */ bool CheckTargetMovement(CFixedVector2D from, entity_pos_t minDelta); /** * Returns whether the length of the given path, plus the distance from * 'from' to the first waypoints, it shorter than minDistance. */ bool PathIsShort(const ICmpPathfinder::Path& path, CFixedVector2D from, entity_pos_t minDistance); /** * Rotate to face towards the target point, given the current pos */ void FaceTowardsPointFromPos(CFixedVector2D pos, entity_pos_t x, entity_pos_t z); /** * Returns an appropriate obstruction filter for use with path requests. */ ControlGroupMovementObstructionFilter GetObstructionFilter(bool forceAvoidMovingUnits = false); /** * Start moving to the given goal, from our current position 'from'. * Might go in a straight line immediately, or might start an asynchronous * path request. */ void BeginPathing(CFixedVector2D from, const ICmpPathfinder::Goal& goal); /** * Start an asynchronous long path query. */ void RequestLongPath(CFixedVector2D from, const ICmpPathfinder::Goal& goal); /** * Start an asynchronous short path query. */ void RequestShortPath(CFixedVector2D from, const ICmpPathfinder::Goal& goal, bool avoidMovingUnits); /** * Select a next long waypoint, given the current unit position. * Also recomputes the short path to use that waypoint. * Returns false on error, or if there is no waypoint to pick. */ bool PickNextLongWaypoint(const CFixedVector2D& pos, bool avoidMovingUnits); /** * Convert a path into a renderable list of lines */ void RenderPath(const ICmpPathfinder::Path& path, std::vector& lines, CColor color); void RenderSubmit(SceneCollector& collector); }; REGISTER_COMPONENT_TYPE(UnitMotion) void CCmpUnitMotion::PathResult(u32 ticket, const ICmpPathfinder::Path& path) { // Ignore obsolete path requests if (ticket != m_ExpectedPathTicket) return; m_ExpectedPathTicket = 0; // we don't expect to get this result again if (m_PathState == PATHSTATE_WAITING_REQUESTING_LONG) { m_LongPath = path; m_ShortPath.m_Waypoints.clear(); // If there's no waypoints then we couldn't get near the target. // Sort of hack: Just try going directly to the goal point instead // (via the short pathfinder), so if we're stuck and the user clicks // close enough to the unit then we can probably get unstuck if (m_LongPath.m_Waypoints.empty()) { ICmpPathfinder::Waypoint wp = { m_FinalGoal.x, m_FinalGoal.z }; m_LongPath.m_Waypoints.push_back(wp); } CmpPtr cmpPosition(GetSimContext(), GetEntityId()); if (cmpPosition.null() || !cmpPosition->IsInWorld()) { StartFailed(); return; } CFixedVector2D pos = cmpPosition->GetPosition2D(); if (!PickNextLongWaypoint(pos, ShouldAvoidMovingUnits())) { StartFailed(); return; } // We started a short path request to the next long path waypoint m_PathState = PATHSTATE_WAITING_REQUESTING_SHORT; } else if (m_PathState == PATHSTATE_WAITING_REQUESTING_SHORT) { m_ShortPath = path; // If there's no waypoints then we couldn't get near the target if (m_ShortPath.m_Waypoints.empty()) { StartFailed(); return; } CmpPtr cmpPosition(GetSimContext(), GetEntityId()); if (cmpPosition.null() || !cmpPosition->IsInWorld()) { StartFailed(); return; } // Now we've got a short path that we can follow m_PathState = PATHSTATE_FOLLOWING; StartSucceeded(); } else if (m_PathState == PATHSTATE_FOLLOWING_REQUESTING_LONG) { m_LongPath = path; // Leave the old m_ShortPath - we'll carry on following it until the // new short path has been computed // If there's no waypoints then we couldn't get near the target. // Sort of hack: Just try going directly to the goal point instead // (via the short pathfinder), so if we're stuck and the user clicks // close enough to the unit then we can probably get unstuck if (m_LongPath.m_Waypoints.empty()) { ICmpPathfinder::Waypoint wp = { m_FinalGoal.x, m_FinalGoal.z }; m_LongPath.m_Waypoints.push_back(wp); } CmpPtr cmpPosition(GetSimContext(), GetEntityId()); if (cmpPosition.null() || !cmpPosition->IsInWorld()) { StopMoving(); return; } CFixedVector2D pos = cmpPosition->GetPosition2D(); if (!PickNextLongWaypoint(pos, ShouldAvoidMovingUnits())) { StopMoving(); return; } // We started a short path request to the next long path waypoint m_PathState = PATHSTATE_FOLLOWING_REQUESTING_SHORT; // (TODO: is this entirely safe? We might continue moving along our // old path while this request is active, so it'll be slightly incorrect // by the time the request has completed) } else if (m_PathState == PATHSTATE_FOLLOWING_REQUESTING_SHORT) { // Replace the current path with the new one m_ShortPath = path; // If there's no waypoints then we couldn't get near the target if (m_ShortPath.m_Waypoints.empty()) { // We should stop moving (unless we're in a formation, in which // case we should continue following it) if (!IsFormationMember()) { MoveFailed(); return; } } m_PathState = PATHSTATE_FOLLOWING; } else if (m_PathState == PATHSTATE_FOLLOWING_REQUESTING_SHORT_APPEND) { // Append the new path onto our current one m_ShortPath.m_Waypoints.insert(m_ShortPath.m_Waypoints.begin(), path.m_Waypoints.begin(), path.m_Waypoints.end()); // If there's no waypoints then we couldn't get near the target // from the last intermediate long-path waypoint. But we can still // continue using the remainder of our current short path. So just // discard the now-useless long path. if (path.m_Waypoints.empty()) m_LongPath.m_Waypoints.clear(); m_PathState = PATHSTATE_FOLLOWING; } else { LOGWARNING(L"unexpected PathResult (%d %d %d)", GetEntityId(), m_State, m_PathState); } } void CCmpUnitMotion::Move(fixed dt) { PROFILE("Move"); if (m_State == STATE_STOPPING) { m_State = STATE_IDLE; MoveSucceeded(); return; } if (m_State == STATE_IDLE) { return; } switch (m_PathState) { case PATHSTATE_NONE: { // If we're not pathing, do nothing return; } case PATHSTATE_WAITING_REQUESTING_LONG: case PATHSTATE_WAITING_REQUESTING_SHORT: { // If we're waiting for a path and don't have one yet, do nothing return; } case PATHSTATE_FOLLOWING: case PATHSTATE_FOLLOWING_REQUESTING_SHORT: case PATHSTATE_FOLLOWING_REQUESTING_SHORT_APPEND: case PATHSTATE_FOLLOWING_REQUESTING_LONG: { // TODO: there's some asymmetry here when units look at other // units' positions - the result will depend on the order of execution. // Maybe we should split the updates into multiple phases to minimise // that problem. CmpPtr cmpPathfinder (GetSimContext(), SYSTEM_ENTITY); if (cmpPathfinder.null()) return; CmpPtr cmpPosition(GetSimContext(), GetEntityId()); if (cmpPosition.null() || !cmpPosition->IsInWorld()) return; CFixedVector2D initialPos = cmpPosition->GetPosition2D(); // If we're chasing a potentially-moving unit and are currently close // enough to its current position, and we can head in a straight line // to it, then throw away our current path and go straight to it if (m_PathState == PATHSTATE_FOLLOWING) TryGoingStraightToTargetEntity(initialPos); // Keep track of the current unit's position during the update CFixedVector2D pos = initialPos; // If in formation, run to keep up; otherwise just walk // (TODO: support stamina, charging, etc) fixed basicSpeed; if (IsFormationMember()) basicSpeed = GetRunSpeed(); else basicSpeed = m_Speed; // (typically but not always WalkSpeed) // Find the speed factor of the underlying terrain // (We only care about the tile we start on - it doesn't matter if we're moving // partially onto a much slower/faster tile) fixed terrainSpeed = cmpPathfinder->GetMovementSpeed(pos.X, pos.Y, m_CostClass); fixed maxSpeed = basicSpeed.Multiply(terrainSpeed); bool wasObstructed = false; // We want to move (at most) maxSpeed*dt units from pos towards the next waypoint fixed timeLeft = dt; while (timeLeft > fixed::Zero()) { // If we ran out of short path, we have to stop if (m_ShortPath.m_Waypoints.empty()) break; CFixedVector2D target(m_ShortPath.m_Waypoints.back().x, m_ShortPath.m_Waypoints.back().z); CFixedVector2D offset = target - pos; // Face towards the target if (!offset.IsZero()) { entity_angle_t angle = atan2_approx(offset.X, offset.Y); cmpPosition->TurnTo(angle); } // Work out how far we can travel in timeLeft fixed maxdist = maxSpeed.Multiply(timeLeft); // If the target is close, we can move there directly fixed offsetLength = offset.Length(); if (offsetLength <= maxdist) { if (cmpPathfinder->CheckMovement(GetObstructionFilter(), pos.X, pos.Y, target.X, target.Y, m_Radius, m_PassClass)) { pos = target; // Spend the rest of the time heading towards the next waypoint timeLeft = timeLeft - (offsetLength / maxSpeed); m_ShortPath.m_Waypoints.pop_back(); continue; } else { // Error - path was obstructed wasObstructed = true; break; } } else { // Not close enough, so just move in the right direction offset.Normalize(maxdist); target = pos + offset; if (cmpPathfinder->CheckMovement(GetObstructionFilter(), pos.X, pos.Y, target.X, target.Y, m_Radius, m_PassClass)) { pos = target; break; } else { // Error - path was obstructed wasObstructed = true; break; } } } // Update the Position component after our movement (if we actually moved anywhere) if (pos != initialPos) cmpPosition->MoveTo(pos.X, pos.Y); if (wasObstructed) { // Oops, we hit something (very likely another unit). // Stop, and recompute the whole path. // TODO: if the target has UnitMotion and is higher priority, // we should wait a little bit. RequestLongPath(pos, m_FinalGoal); m_PathState = PATHSTATE_WAITING_REQUESTING_LONG; return; } // We successfully moved along our path, until running out of // waypoints or time. if (m_PathState == PATHSTATE_FOLLOWING) { // If we're not currently computing any new paths: // If we are close to reaching the end of the short path // (or have reached it already), try to extend it entity_pos_t minDistance = basicSpeed.Multiply(dt) * WAYPOINT_ADVANCE_LOOKAHEAD_TURNS; if (PathIsShort(m_ShortPath, pos, minDistance)) { // Start the path extension from the end of this short path // (or our current position if no short path) CFixedVector2D from = pos; if (!m_ShortPath.m_Waypoints.empty()) from = CFixedVector2D(m_ShortPath.m_Waypoints[0].x, m_ShortPath.m_Waypoints[0].z); if (PickNextLongWaypoint(from, ShouldAvoidMovingUnits())) { m_PathState = PATHSTATE_FOLLOWING_REQUESTING_SHORT_APPEND; } else { // Failed (there were no long waypoints left). // If there's still some short path then continue following // it, else we've finished moving. if (m_ShortPath.m_Waypoints.empty()) { if (IsFormationMember()) { // If we're in formation, we've reached our assigned position. // so wait here. // (We'll try to continue following the formation next turn.) } else { // Not in formation, so just finish moving StopMoving(); FaceTowardsPointFromPos(pos, m_FinalGoal.x, m_FinalGoal.z); // TODO: if the goal was a square building, we ought to point towards the // nearest point on the square, not towards its center } } } } } // If we have a target entity, and we're not miles away from the end of // our current path, and the target moved enough, then recompute our // whole path if (m_PathState == PATHSTATE_FOLLOWING) { if (IsFormationMember()) CheckTargetMovement(pos, CHECK_TARGET_MOVEMENT_MIN_DELTA_FORMATION); else CheckTargetMovement(pos, CHECK_TARGET_MOVEMENT_MIN_DELTA); } } } } bool CCmpUnitMotion::ComputeTargetPosition(CFixedVector2D& out) { if (m_TargetEntity == INVALID_ENTITY) return false; CmpPtr cmpPosition(GetSimContext(), m_TargetEntity); if (cmpPosition.null() || !cmpPosition->IsInWorld()) return false; if (m_TargetOffset.IsZero()) { // No offset, just return the position directly out = cmpPosition->GetPosition2D(); } else { // There is an offset, so compute it relative to orientation entity_angle_t angle = cmpPosition->GetRotation().Y; CFixedVector2D offset = m_TargetOffset.Rotate(angle); out = cmpPosition->GetPosition2D() + offset; } return true; } bool CCmpUnitMotion::TryGoingStraightToTargetEntity(CFixedVector2D from) { CFixedVector2D targetPos; if (!ComputeTargetPosition(targetPos)) return false; // Fail if the target is too far away if ((targetPos - from).CompareLength(DIRECT_PATH_RANGE) > 0) return false; CmpPtr cmpPathfinder (GetSimContext(), SYSTEM_ENTITY); if (cmpPathfinder.null()) return false; // Move the goal to match the target entity's new position ICmpPathfinder::Goal goal = m_FinalGoal; goal.x = targetPos.X; goal.z = targetPos.Y; // (we ignore changes to the target's rotation, since only buildings are // square and buildings don't move) // Find the point on the goal shape that we should head towards CFixedVector2D goalPos = cmpPathfinder->GetNearestPointOnGoal(from, goal); // Check if there's any collisions on that route if (!cmpPathfinder->CheckMovement(GetObstructionFilter(), from.X, from.Y, goalPos.X, goalPos.Y, m_Radius, m_PassClass)) return false; // That route is okay, so update our path m_FinalGoal = goal; m_LongPath.m_Waypoints.clear(); m_ShortPath.m_Waypoints.clear(); ICmpPathfinder::Waypoint wp = { goalPos.X, goalPos.Y }; m_ShortPath.m_Waypoints.push_back(wp); return true; } bool CCmpUnitMotion::CheckTargetMovement(CFixedVector2D from, entity_pos_t minDelta) { CFixedVector2D targetPos; if (!ComputeTargetPosition(targetPos)) return false; // Fail unless the target has moved enough CFixedVector2D oldTargetPos(m_FinalGoal.x, m_FinalGoal.z); if ((targetPos - oldTargetPos).CompareLength(minDelta) < 0) return false; // Fail unless we're close enough to the target to care about its movement if (!PathIsShort(m_LongPath, from, CHECK_TARGET_MOVEMENT_AT_MAX_DIST)) return false; // Fail if the target is no longer visible to this entity's owner // (in which case we'll continue moving to its last known location, // unless it comes back into view before we reach that location) CmpPtr cmpOwnership(GetSimContext(), GetEntityId()); if (!cmpOwnership.null()) { CmpPtr cmpRangeManager(GetSimContext(), SYSTEM_ENTITY); if (!cmpRangeManager.null()) { if (cmpRangeManager->GetLosVisibility(m_TargetEntity, cmpOwnership->GetOwner()) == ICmpRangeManager::VIS_HIDDEN) return false; } } // The target moved and we need to update our current path; // change the goal here and expect our caller to start the path request m_FinalGoal.x = targetPos.X; m_FinalGoal.z = targetPos.Y; RequestLongPath(from, m_FinalGoal); m_PathState = PATHSTATE_FOLLOWING_REQUESTING_LONG; return true; } bool CCmpUnitMotion::PathIsShort(const ICmpPathfinder::Path& path, CFixedVector2D from, entity_pos_t minDistance) { CFixedVector2D pos = from; entity_pos_t distLeft = minDistance; for (ssize_t i = path.m_Waypoints.size()-1; i >= 0; --i) { // Check if the next path segment is longer than the requested minimum CFixedVector2D waypoint(path.m_Waypoints[i].x, path.m_Waypoints[i].z); CFixedVector2D delta = waypoint - pos; if (delta.CompareLength(distLeft) > 0) return false; // Still short enough - prepare to check the next segment distLeft -= delta.Length(); pos = waypoint; } // Reached the end of the path before exceeding minDistance return true; } void CCmpUnitMotion::FaceTowardsPoint(entity_pos_t x, entity_pos_t z) { CmpPtr cmpPosition(GetSimContext(), GetEntityId()); if (cmpPosition.null() || !cmpPosition->IsInWorld()) return; CFixedVector2D pos = cmpPosition->GetPosition2D(); FaceTowardsPointFromPos(pos, x, z); } void CCmpUnitMotion::FaceTowardsPointFromPos(CFixedVector2D pos, entity_pos_t x, entity_pos_t z) { CFixedVector2D target(x, z); CFixedVector2D offset = target - pos; if (!offset.IsZero()) { entity_angle_t angle = atan2_approx(offset.X, offset.Y); CmpPtr cmpPosition(GetSimContext(), GetEntityId()); if (cmpPosition.null()) return; cmpPosition->TurnTo(angle); } } ControlGroupMovementObstructionFilter CCmpUnitMotion::GetObstructionFilter(bool forceAvoidMovingUnits) { entity_id_t group; if (IsFormationMember()) group = m_TargetEntity; else group = GetEntityId(); return ControlGroupMovementObstructionFilter(forceAvoidMovingUnits || ShouldAvoidMovingUnits(), group); } void CCmpUnitMotion::BeginPathing(CFixedVector2D from, const ICmpPathfinder::Goal& goal) { // Cancel any pending path requests m_ExpectedPathTicket = 0; // Set our 'moving' flag, so other units pathfinding now will ignore us CmpPtr cmpObstruction(GetSimContext(), GetEntityId()); if (!cmpObstruction.null()) cmpObstruction->SetMovingFlag(true); // If we're aiming at a target entity and it's close and we can reach // it in a straight line, then we'll just go along the straight line // instead of computing a path. if (TryGoingStraightToTargetEntity(from)) { m_PathState = PATHSTATE_FOLLOWING; return; } // TODO: should go straight to non-entity points too // Otherwise we need to compute a path. // TODO: if it's close then just do a short path, not a long path // (But if it's close on the opposite side of a river then we really // need a long path, so we can't simply check linear distance) m_PathState = PATHSTATE_WAITING_REQUESTING_LONG; RequestLongPath(from, goal); } void CCmpUnitMotion::RequestLongPath(CFixedVector2D from, const ICmpPathfinder::Goal& goal) { CmpPtr cmpPathfinder(GetSimContext(), SYSTEM_ENTITY); if (cmpPathfinder.null()) return; cmpPathfinder->SetDebugPath(from.X, from.Y, goal, m_PassClass, m_CostClass); m_ExpectedPathTicket = cmpPathfinder->ComputePathAsync(from.X, from.Y, goal, m_PassClass, m_CostClass, GetEntityId()); } void CCmpUnitMotion::RequestShortPath(CFixedVector2D from, const ICmpPathfinder::Goal& goal, bool avoidMovingUnits) { CmpPtr cmpPathfinder(GetSimContext(), SYSTEM_ENTITY); if (cmpPathfinder.null()) return; m_ExpectedPathTicket = cmpPathfinder->ComputeShortPathAsync(from.X, from.Y, m_Radius, SHORT_PATH_SEARCH_RANGE, goal, m_PassClass, avoidMovingUnits, m_TargetEntity, GetEntityId()); } bool CCmpUnitMotion::PickNextLongWaypoint(const CFixedVector2D& pos, bool avoidMovingUnits) { // If there's no long path, we can't pick the next waypoint from it if (m_LongPath.m_Waypoints.empty()) return false; // First try to get the immediate next waypoint entity_pos_t targetX = m_LongPath.m_Waypoints.back().x; entity_pos_t targetZ = m_LongPath.m_Waypoints.back().z; m_LongPath.m_Waypoints.pop_back(); // To smooth the motion and avoid grid-constrained movement and allow dynamic obstacle avoidance, // try skipping some more waypoints if they're close enough while (!m_LongPath.m_Waypoints.empty()) { CFixedVector2D w(m_LongPath.m_Waypoints.back().x, m_LongPath.m_Waypoints.back().z); if ((w - pos).CompareLength(WAYPOINT_ADVANCE_MAX) > 0) break; targetX = m_LongPath.m_Waypoints.back().x; targetZ = m_LongPath.m_Waypoints.back().z; m_LongPath.m_Waypoints.pop_back(); } // Now we need to recompute a short path to the waypoint ICmpPathfinder::Goal goal; if (m_LongPath.m_Waypoints.empty()) { // This was the last waypoint - head for the exact goal goal = m_FinalGoal; } else { // Head for somewhere near the waypoint (but allow some leeway in case it's obstructed) goal.type = ICmpPathfinder::Goal::CIRCLE; goal.hw = SHORT_PATH_GOAL_RADIUS; goal.x = targetX; goal.z = targetZ; } CmpPtr cmpPathfinder(GetSimContext(), SYSTEM_ENTITY); if (cmpPathfinder.null()) return false; m_ExpectedPathTicket = cmpPathfinder->ComputeShortPathAsync(pos.X, pos.Y, m_Radius, SHORT_PATH_SEARCH_RANGE, goal, m_PassClass, avoidMovingUnits, GetEntityId(), GetEntityId()); return true; } bool CCmpUnitMotion::MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) { PROFILE("MoveToPointRange"); CmpPtr cmpPosition(GetSimContext(), GetEntityId()); if (cmpPosition.null() || !cmpPosition->IsInWorld()) return false; CFixedVector2D pos = cmpPosition->GetPosition2D(); ICmpPathfinder::Goal goal; if (minRange.IsZero() && maxRange.IsZero()) { // Handle the non-ranged mode: // Check whether this point is in an obstruction CmpPtr cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY); if (cmpObstructionManager.null()) return false; ICmpObstructionManager::ObstructionSquare obstruction; if (cmpObstructionManager->FindMostImportantObstruction(GetObstructionFilter(true), x, z, m_Radius, obstruction)) { // If we're aiming inside a building, then aim for the outline of the building instead // TODO: if we're aiming at a unit then maybe a circle would look nicer? goal.type = ICmpPathfinder::Goal::SQUARE; goal.x = obstruction.x; goal.z = obstruction.z; goal.u = obstruction.u; goal.v = obstruction.v; goal.hw = obstruction.hw + m_Radius + g_GoalDelta; // nudge the goal outwards so it doesn't intersect the building itself goal.hh = obstruction.hh + m_Radius + g_GoalDelta; } else { // Unobstructed - head directly for the goal goal.type = ICmpPathfinder::Goal::POINT; goal.x = x; goal.z = z; } } else { entity_pos_t distance = (pos - CFixedVector2D(x, z)).Length(); entity_pos_t goalDistance; if (distance < minRange) { goalDistance = minRange + g_GoalDelta; } else if (maxRange >= entity_pos_t::Zero() && distance > maxRange) { goalDistance = maxRange - g_GoalDelta; } else { // We're already in range - no need to move anywhere FaceTowardsPointFromPos(pos, x, z); return false; } // TODO: what happens if goalDistance < 0? (i.e. we probably can never get close enough to the target) goal.type = ICmpPathfinder::Goal::CIRCLE; goal.x = x; goal.z = z; goal.hw = m_Radius + goalDistance; } m_State = STATE_INDIVIDUAL_PATH; m_TargetEntity = INVALID_ENTITY; m_TargetOffset = CFixedVector2D(); m_TargetMinRange = minRange; m_TargetMaxRange = maxRange; m_FinalGoal = goal; BeginPathing(pos, goal); return true; } bool CCmpUnitMotion::ShouldTreatTargetAsCircle(entity_pos_t range, entity_pos_t hw, entity_pos_t hh, entity_pos_t circleRadius) { // Given a square, plus a target range we should reach, the shape at that distance // is a round-cornered square which we can approximate as either a circle or as a square. // Choose the shape that will minimise the worst-case error: // For a square, error is (sqrt(2)-1) * range at the corners entity_pos_t errSquare = (entity_pos_t::FromInt(4142)/10000).Multiply(range); // For a circle, error is radius-hw at the sides and radius-hh at the top/bottom entity_pos_t errCircle = circleRadius - std::min(hw, hh); return (errCircle < errSquare); } bool CCmpUnitMotion::MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) { PROFILE("MoveToTargetRange"); CmpPtr cmpPosition(GetSimContext(), GetEntityId()); if (cmpPosition.null() || !cmpPosition->IsInWorld()) return false; CFixedVector2D pos = cmpPosition->GetPosition2D(); CmpPtr cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY); if (cmpObstructionManager.null()) return false; bool hasObstruction = false; ICmpObstructionManager::ObstructionSquare obstruction; CmpPtr cmpObstruction(GetSimContext(), target); if (!cmpObstruction.null()) hasObstruction = cmpObstruction->GetObstructionSquare(obstruction); /* * If we're starting outside the maxRange, we need to move closer in. * If we're starting inside the minRange, we need to move further out. * These ranges are measured from the center of this entity to the edge of the target; * we add the goal range onto the size of the target shape to get the goal shape. * (Then we extend it outwards/inwards by a little bit to be sure we'll end up * within the right range, in case of minor numerical inaccuracies.) * * There's a bit of a problem with large square targets: * the pathfinder only lets us move to goals that are squares, but the points an equal * distance from the target make a rounded square shape instead. * * When moving closer, we could shrink the goal radius to 1/sqrt(2) so the goal shape fits entirely * within the desired rounded square, but that gives an unfair advantage to attackers who approach * the target diagonally. * * If the target is small relative to the range (e.g. archers attacking anything), * then we cheat and pretend the target is actually a circle. * (TODO: that probably looks rubbish for things like walls?) * * If the target is large relative to the range (e.g. melee units attacking buildings), * then we multiply maxRange by approx 1/sqrt(2) to guarantee they'll always aim close enough. * (Those units should set minRange to 0 so they'll never be considered *too* close.) */ if (hasObstruction) { CFixedVector2D halfSize(obstruction.hw, obstruction.hh); ICmpPathfinder::Goal goal; goal.x = obstruction.x; goal.z = obstruction.z; entity_pos_t distance = Geometry::DistanceToSquare(pos - CFixedVector2D(obstruction.x, obstruction.z), obstruction.u, obstruction.v, halfSize); if (distance < minRange) { // Too close to the square - need to move away // TODO: maybe we should do the ShouldTreatTargetAsCircle thing here? entity_pos_t goalDistance = minRange + g_GoalDelta; goal.type = ICmpPathfinder::Goal::SQUARE; goal.u = obstruction.u; goal.v = obstruction.v; entity_pos_t delta = std::max(goalDistance, m_Radius + entity_pos_t::FromInt(CELL_SIZE)/16); // ensure it's far enough to not intersect the building itself goal.hw = obstruction.hw + delta; goal.hh = obstruction.hh + delta; } else if (maxRange < entity_pos_t::Zero() || distance < maxRange) { // We're already in range - no need to move anywhere FaceTowardsPointFromPos(pos, goal.x, goal.z); return false; } else { // We might need to move closer: // Circumscribe the square entity_pos_t circleRadius = halfSize.Length(); if (ShouldTreatTargetAsCircle(maxRange, obstruction.hw, obstruction.hh, circleRadius)) { // The target is small relative to our range, so pretend it's a circle // Note that the distance to the circle will always be less than // the distance to the square, so the previous "distance < maxRange" // check is still valid (though not sufficient) entity_pos_t circleDistance = (pos - CFixedVector2D(obstruction.x, obstruction.z)).Length() - circleRadius; if (circleDistance < maxRange) { // We're already in range - no need to move anywhere FaceTowardsPointFromPos(pos, goal.x, goal.z); return false; } entity_pos_t goalDistance = maxRange - g_GoalDelta; goal.type = ICmpPathfinder::Goal::CIRCLE; goal.hw = circleRadius + goalDistance; } else { // The target is large relative to our range, so treat it as a square and // get close enough that the diagonals come within range entity_pos_t goalDistance = (maxRange - g_GoalDelta)*2 / 3; // multiply by slightly less than 1/sqrt(2) goal.type = ICmpPathfinder::Goal::SQUARE; goal.u = obstruction.u; goal.v = obstruction.v; entity_pos_t delta = std::max(goalDistance, m_Radius + entity_pos_t::FromInt(CELL_SIZE)/16); // ensure it's far enough to not intersect the building itself goal.hw = obstruction.hw + delta; goal.hh = obstruction.hh + delta; } } m_State = STATE_INDIVIDUAL_PATH; m_TargetEntity = target; m_TargetOffset = CFixedVector2D(); m_TargetMinRange = minRange; m_TargetMaxRange = maxRange; m_FinalGoal = goal; BeginPathing(pos, goal); return true; } else { // The target didn't have an obstruction or obstruction shape, so treat it as a point instead CmpPtr cmpTargetPosition(GetSimContext(), target); if (cmpTargetPosition.null() || !cmpTargetPosition->IsInWorld()) return false; CFixedVector2D targetPos = cmpTargetPosition->GetPosition2D(); return MoveToPointRange(targetPos.X, targetPos.Y, minRange, maxRange); } } bool CCmpUnitMotion::IsInTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) { // This function closely mirrors MoveToTargetRange - it needs to return true // after that Move has completed CmpPtr cmpPosition(GetSimContext(), GetEntityId()); if (cmpPosition.null() || !cmpPosition->IsInWorld()) return false; CFixedVector2D pos = cmpPosition->GetPosition2D(); CmpPtr cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY); if (cmpObstructionManager.null()) return false; bool hasObstruction = false; ICmpObstructionManager::ObstructionSquare obstruction; CmpPtr cmpObstruction(GetSimContext(), target); if (!cmpObstruction.null()) hasObstruction = cmpObstruction->GetObstructionSquare(obstruction); entity_pos_t distance; if (hasObstruction) { CFixedVector2D halfSize(obstruction.hw, obstruction.hh); entity_pos_t distance = Geometry::DistanceToSquare(pos - CFixedVector2D(obstruction.x, obstruction.z), obstruction.u, obstruction.v, halfSize); // See if we're too close to the target square if (distance < minRange) return false; // See if we're close enough to the target square if (maxRange < entity_pos_t::Zero() || distance <= maxRange) return true; entity_pos_t circleRadius = halfSize.Length(); if (ShouldTreatTargetAsCircle(maxRange, obstruction.hw, obstruction.hh, circleRadius)) { // The target is small relative to our range, so pretend it's a circle // and see if we're close enough to that entity_pos_t circleDistance = (pos - CFixedVector2D(obstruction.x, obstruction.z)).Length() - circleRadius; if (circleDistance <= maxRange) return true; } return false; } else { CmpPtr cmpTargetPosition(GetSimContext(), target); if (cmpTargetPosition.null() || !cmpTargetPosition->IsInWorld()) return false; CFixedVector2D targetPos = cmpTargetPosition->GetPosition2D(); entity_pos_t distance = (pos - targetPos).Length(); if (minRange <= distance && (maxRange < entity_pos_t::Zero() || distance <= maxRange)) return true; return false; } } void CCmpUnitMotion::MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) { CmpPtr cmpPosition(GetSimContext(), GetEntityId()); if (cmpPosition.null() || !cmpPosition->IsInWorld()) return; CFixedVector2D pos = cmpPosition->GetPosition2D(); ICmpPathfinder::Goal goal; goal.type = ICmpPathfinder::Goal::POINT; goal.x = pos.X; goal.z = pos.Y; m_State = STATE_FORMATIONMEMBER_PATH; m_TargetEntity = target; m_TargetOffset = CFixedVector2D(x, z); m_TargetMinRange = entity_pos_t::Zero(); m_TargetMaxRange = entity_pos_t::Zero(); m_FinalGoal = goal; BeginPathing(pos, goal); } void CCmpUnitMotion::RenderPath(const ICmpPathfinder::Path& path, std::vector& lines, CColor color) { bool floating = false; CmpPtr cmpPosition(GetSimContext(), GetEntityId()); if (!cmpPosition.null()) floating = cmpPosition->IsFloating(); lines.clear(); std::vector waypointCoords; for (size_t i = 0; i < path.m_Waypoints.size(); ++i) { float x = path.m_Waypoints[i].x.ToFloat(); float z = path.m_Waypoints[i].z.ToFloat(); waypointCoords.push_back(x); waypointCoords.push_back(z); lines.push_back(SOverlayLine()); lines.back().m_Color = color; SimRender::ConstructSquareOnGround(GetSimContext(), x, z, 1.0f, 1.0f, 0.0f, lines.back(), floating); } lines.push_back(SOverlayLine()); lines.back().m_Color = color; SimRender::ConstructLineOnGround(GetSimContext(), waypointCoords, lines.back(), floating); } void CCmpUnitMotion::RenderSubmit(SceneCollector& collector) { if (!m_DebugOverlayEnabled) return; RenderPath(m_LongPath, m_DebugOverlayLongPathLines, OVERLAY_COLOUR_LONG_PATH); RenderPath(m_ShortPath, m_DebugOverlayShortPathLines, OVERLAY_COLOUR_SHORT_PATH); for (size_t i = 0; i < m_DebugOverlayLongPathLines.size(); ++i) collector.Submit(&m_DebugOverlayLongPathLines[i]); for (size_t i = 0; i < m_DebugOverlayShortPathLines.size(); ++i) collector.Submit(&m_DebugOverlayShortPathLines[i]); } Index: ps/trunk/source/simulation2/components/ICmpObstruction.h =================================================================== --- ps/trunk/source/simulation2/components/ICmpObstruction.h (revision 9969) +++ ps/trunk/source/simulation2/components/ICmpObstruction.h (revision 9970) @@ -1,76 +1,76 @@ /* Copyright (C) 2011 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_ICMPOBSTRUCTION #define INCLUDED_ICMPOBSTRUCTION #include "simulation2/system/Interface.h" #include "simulation2/components/ICmpObstructionManager.h" /** * Flags an entity as obstructing movement for other units, * and handles the processing of collision queries. */ class ICmpObstruction : public IComponent { public: virtual ICmpObstructionManager::tag_t GetObstruction() = 0; /** * Gets the square corresponding to this obstruction shape. * @return true and updates @p out on success; * false on failure (e.g. object not in the world). */ virtual bool GetObstructionSquare(ICmpObstructionManager::ObstructionSquare& out) = 0; virtual entity_pos_t GetUnitRadius() = 0; /** * Test whether this entity is colliding with any obstruction that are set to * block the creation of foundations. - * @return true if there is a collision + * @return true if foundation is valid (not obstructed) */ - virtual bool CheckFoundationCollisions() = 0; + virtual bool CheckFoundation(std::string className) = 0; /** * Returns a list of entities that are colliding with this entity, and that * are set to block construction. - * @return true if there is a collision + * @return vector of blocking entities */ virtual std::vector GetConstructionCollisions() = 0; virtual void SetActive(bool active) = 0; virtual void SetMovingFlag(bool enabled) = 0; virtual void SetDisableBlockMovementPathfinding(bool disabled) = 0; virtual bool GetBlockMovementFlag() = 0; /** * Change the control group that the entity belongs to. * Control groups are used to let units ignore collisions with other units from * the same group. Default is the entity's own ID. */ virtual void SetControlGroup(entity_id_t group) = 0; DECLARE_INTERFACE_TYPE(Obstruction) }; #endif // INCLUDED_ICMPOBSTRUCTION Index: ps/trunk/binaries/data/mods/public/gui/session/input.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 9969) +++ ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 9970) @@ -1,1245 +1,1236 @@ const SDL_BUTTON_LEFT = 1; const SDL_BUTTON_MIDDLE = 2; const SDL_BUTTON_RIGHT = 3; const SDLK_LEFTBRACKET = 91; const SDLK_RIGHTBRACKET = 93; const SDLK_RSHIFT = 303; const SDLK_LSHIFT = 304; const SDLK_RCTRL = 305; const SDLK_LCTRL = 306; const SDLK_RALT = 307; const SDLK_LALT = 308; // TODO: these constants should be defined somewhere else instead, in // case any other code wants to use them too const ACTION_NONE = 0; const ACTION_GARRISON = 1; const ACTION_REPAIR = 2; var preSelectedAction = ACTION_NONE; var INPUT_NORMAL = 0; var INPUT_SELECTING = 1; var INPUT_BANDBOXING = 2; var INPUT_BUILDING_PLACEMENT = 3; var INPUT_BUILDING_CLICK = 4; var INPUT_BUILDING_DRAG = 5; var INPUT_BATCHTRAINING = 6; var INPUT_PRESELECTEDACTION = 7; var inputState = INPUT_NORMAL; var defaultPlacementAngle = Math.PI*3/4; var placementAngle = undefined; var placementPosition = undefined; var placementEntity = undefined; var mouseX = 0; var mouseY = 0; var mouseIsOverObject = false; // Number of pixels the mouse can move before the action is considered a drag var maxDragDelta = 4; // Time in milliseconds in which a double click is recognized const doubleClickTime = 500; var doubleClickTimer = 0; var doubleClicked = false; // Store the previously clicked entity - ensure a double/triple click happens on the same entity var prevClickedEntity = 0; // Same double-click behaviour for hotkey presses const doublePressTime = 500; var doublePressTimer = 0; var prevHotkey = 0; function updateCursor() { if (!mouseIsOverObject) { var action = determineAction(mouseX, mouseY); if (inputState == INPUT_NORMAL || inputState == INPUT_PRESELECTEDACTION) { if (action) { if (action.cursor) { Engine.SetCursor(action.cursor); return; } } } } Engine.SetCursor("arrow-default"); } function updateBuildingPlacementPreview() { // The preview should be recomputed every turn, so that it responds // to obstructions/fog/etc moving underneath it if (placementEntity && placementPosition) { - Engine.GuiInterfaceCall("SetBuildingPlacementPreview", { + return Engine.GuiInterfaceCall("SetBuildingPlacementPreview", { "template": placementEntity, "x": placementPosition.x, "z": placementPosition.z, "angle": placementAngle }); } + + return false; } function resetPlacementEntity() { Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {"template": ""}); placementEntity = undefined; placementPosition = undefined; placementAngle = undefined; } function findGatherType(gatherer, supply) { if (!gatherer || !supply) return undefined; if (gatherer[supply.type.generic+"."+supply.type.specific]) return supply.type.specific; if (gatherer[supply.type.generic]) return supply.type.generic; return undefined; } function getActionInfo(action, target) { var selection = g_Selection.toList(); // If the selection doesn't exist, no action var entState = GetEntityState(selection[0]); if (!entState) return {"possible": false}; // If the selection isn't friendly units, no action var playerID = Engine.GetPlayerID(); var allOwnedByPlayer = selection.every(function(ent) { var entState = GetEntityState(ent); return entState && entState.player == playerID; }); if (!g_DevSettings.controlAll && !allOwnedByPlayer) return {"possible": false}; // Work out whether the selection can have rally points var haveRallyPoints = selection.every(function(ent) { var entState = GetEntityState(ent); return entState && entState.rallyPoint; }); if (!target) { if (action == "set-rallypoint" && haveRallyPoints) return {"possible": true}; else if (action == "move") return {"possible": true}; else return {"possible": false}; } if (haveRallyPoints && selection.indexOf(target) != -1 && action == "unset-rallypoint") return {"possible": true}; // Look at the first targeted entity // (TODO: maybe we eventually want to look at more, and be more context-sensitive? // e.g. prefer to attack an enemy unit, even if some friendly units are closer to the mouse) var targetState = GetEntityState(target); // If we selected buildings with rally points, and then click on one of those selected // buildings, we should remove the rally point //if (haveRallyPoints && selection.indexOf(target) != -1) // return {"type": "unset-rallypoint"}; // Check if the target entity is a resource, dropsite, foundation, or enemy unit. // Check if any entities in the selection can gather the requested resource, // can return to the dropsite, can build the foundation, or can attack the enemy var simState = Engine.GuiInterfaceCall("GetSimulationState"); for each (var entityID in selection) { var entState = GetEntityState(entityID); if (!entState) continue; var playerState = simState.players[entState.player]; var playerOwned = (targetState.player == entState.player); var allyOwned = playerState.isAlly[targetState.player]; var enemyOwned = playerState.isEnemy[targetState.player]; var gaiaOwned = (targetState.player == 0); // Find the resource type we're carrying, if any var carriedType = undefined; if (entState.resourceCarrying && entState.resourceCarrying.length) carriedType = entState.resourceCarrying[0].type; switch (action) { case "garrison": if (isUnit(entState) && targetState.garrisonHolder && playerOwned) { var allowedClasses = targetState.garrisonHolder.allowedClasses; for each (var unitClass in entState.identity.classes) { if (allowedClasses.indexOf(unitClass) != -1) { return {"possible": true}; } } } break; case "gather": if (targetState.resourceSupply && (playerOwned || gaiaOwned)) { var resource = findGatherType(entState.resourceGatherRates, targetState.resourceSupply); if (resource) return {"possible": true, "cursor": "action-gather-" + resource}; } break; case "returnresource": if (targetState.resourceDropsite && playerOwned && carriedType && targetState.resourceDropsite.types.indexOf(carriedType) != -1) return {"possible": true, "cursor": "action-return-" + carriedType}; break; case "build": if (targetState.foundation && entState.buildEntities && playerOwned) return {"possible": true}; break; case "repair": if (entState.buildEntities && targetState.needsRepair && allyOwned) return {"possible": true}; break; case "attack": if (entState.attack && targetState.hitpoints && enemyOwned) return {"possible": true}; } } if (action == "move") return {"possible": true}; else return {"possible": false}; } /** * Determine the context-sensitive action that should be performed when the mouse is at (x,y) */ function determineAction(x, y, fromMinimap) { var selection = g_Selection.toList(); // No action if there's no selection if (!selection.length) { preSelectedAction = ACTION_NONE; return undefined; } // If the selection doesn't exist, no action var entState = GetEntityState(selection[0]); if (!entState) return undefined; // If the selection isn't friendly units, no action var playerID = Engine.GetPlayerID(); var allOwnedByPlayer = selection.every(function(ent) { var entState = GetEntityState(ent); return entState && entState.player == playerID; }); if (!g_DevSettings.controlAll && !allOwnedByPlayer) return undefined; // Work out whether the selection can have rally points var haveRallyPoints = selection.every(function(ent) { var entState = GetEntityState(ent); return entState && entState.rallyPoint; }); var targets = []; var target = undefined; var type = "none"; var cursor = ""; var targetState = undefined; if (!fromMinimap) targets = Engine.PickEntitiesAtPoint(x, y); if (targets.length) { target = targets[0]; } if (preSelectedAction != ACTION_NONE) { switch (preSelectedAction) { case ACTION_GARRISON: if (getActionInfo("garrison", target).possible) return {"type": "garrison", "cursor": "action-garrison", "target": target}; else return {"type": "none", "cursor": "action-garrison-disabled", "target": undefined}; break; case ACTION_REPAIR: if (getActionInfo("repair", target).possible) return {"type": "repair", "cursor": "action-repair", "target": target}; else return {"type": "none", "cursor": "action-repair-disabled", "target": undefined}; break; } } else if (Engine.HotkeyIsPressed("session.garrison")) { if (getActionInfo("garrison", target).possible) return {"type": "garrison", "cursor": "action-garrison", "target": target}; else return {"type": "none", "cursor": "action-garrison-disabled", "target": undefined}; } else { var actionInfo = undefined; if ((actionInfo = getActionInfo("gather", target)).possible) return {"type": "gather", "cursor": actionInfo.cursor, "target": target}; else if ((actionInfo = getActionInfo("returnresource", target)).possible) return {"type": "returnresource", "cursor": actionInfo.cursor, "target": target}; else if (getActionInfo("build", target).possible) return {"type": "build", "cursor": "action-build", "target": target}; else if (getActionInfo("repair", target).possible) return {"type": "build", "cursor": "action-repair", "target": target}; else if (getActionInfo("attack", target).possible) return {"type": "attack", "cursor": "action-attack", "target": target}; else if(getActionInfo("set-rallypoint", target).possible) return {"type": "set-rallypoint"}; else if(getActionInfo("unset-rallypoint", target).possible) return {"type": "unset-rallypoint"}; else if (getActionInfo("move", target).possible) return {"type": "move"}; } return {"type": type, "cursor": cursor, "target": target}; } var dragStart; // used for remembering mouse coordinates at start of drag operations function tryPlaceBuilding(queued) { var selection = g_Selection.toList(); // Use the preview to check it's a valid build location - var ok = Engine.GuiInterfaceCall("SetBuildingPlacementPreview", { - "template": placementEntity, - "x": placementPosition.x, - "z": placementPosition.z, - "angle": placementAngle - }); - if (!ok) + if (!updateBuildingPlacementPreview()) { // invalid location - don't build it // TODO: play a sound? return false; } // Start the construction Engine.PostNetworkCommand({ "type": "construct", "template": placementEntity, "x": placementPosition.x, "z": placementPosition.z, "angle": placementAngle, "entities": selection, "autorepair": true, "autocontinue": true, "queued": queued }); Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] }); if (!queued) resetPlacementEntity(); return true; } // Limits bandboxed selections to certain types of entities based on priority function getPreferredEntities(ents) { var entStateList = []; var preferredEnts = []; // Check if there are units in the selection and get a list of entity states for each (var ent in ents) { var entState = GetEntityState(ent); if (!entState) continue; if (isUnit(entState)) preferredEnts.push(ent); entStateList.push(entState); } // If there are no units, check if there are defensive entities in the selection if (!preferredEnts.length) for (var i = 0; i < ents.length; i++) if (isDefensive(entStateList[i])) preferredEnts.push(ents[i]); return preferredEnts; } function handleInputBeforeGui(ev, hoveredObject) { // Capture mouse position so we can use it for displaying cursors, // and key states switch (ev.type) { case "mousebuttonup": case "mousebuttondown": case "mousemotion": mouseX = ev.x; mouseY = ev.y; break; } // Remember whether the mouse is over a GUI object or not mouseIsOverObject = (hoveredObject != null); // State-machine processing: // // (This is for states which should override the normal GUI processing - events will // be processed here before being passed on, and propagation will stop if this function // returns true) // // TODO: it'd probably be nice to have a better state-machine system, with guaranteed // entry/exit functions, since this is a bit broken now switch (inputState) { case INPUT_BANDBOXING: switch (ev.type) { case "mousemotion": var x0 = dragStart[0]; var y0 = dragStart[1]; var x1 = ev.x; var y1 = ev.y; if (x0 > x1) { var t = x0; x0 = x1; x1 = t; } if (y0 > y1) { var t = y0; y0 = y1; y1 = t; } var bandbox = getGUIObjectByName("bandbox"); bandbox.size = [x0, y0, x1, y1].join(" "); bandbox.hidden = false; // TODO: Should we handle "control all units" here as well? var ents = Engine.PickFriendlyEntitiesInRect(x0, y0, x1, y1, Engine.GetPlayerID()); g_Selection.setHighlightList(ents); return false; case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { var x0 = dragStart[0]; var y0 = dragStart[1]; var x1 = ev.x; var y1 = ev.y; if (x0 > x1) { var t = x0; x0 = x1; x1 = t; } if (y0 > y1) { var t = y0; y0 = y1; y1 = t; } var bandbox = getGUIObjectByName("bandbox"); bandbox.hidden = true; // Get list of entities limited to preferred entities // TODO: Should we handle "control all units" here as well? var ents = Engine.PickFriendlyEntitiesInRect(x0, y0, x1, y1, Engine.GetPlayerID()); var preferredEntities = getPreferredEntities(ents) if (preferredEntities.length) ents = preferredEntities; // Remove the bandbox hover highlighting g_Selection.setHighlightList([]); // Update the list of selected units if (Engine.HotkeyIsPressed("selection.add")) { g_Selection.addList(ents); } else if (Engine.HotkeyIsPressed("selection.remove")) { g_Selection.removeList(ents); } else { g_Selection.reset(); g_Selection.addList(ents); } inputState = INPUT_NORMAL; return true; } else if (ev.button == SDL_BUTTON_RIGHT) { // Cancel selection var bandbox = getGUIObjectByName("bandbox"); bandbox.hidden = true; g_Selection.setHighlightList([]); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BUILDING_CLICK: switch (ev.type) { case "mousemotion": // If the mouse moved far enough from the original click location, // then switch to drag-orientation mode var dragDeltaX = ev.x - dragStart[0]; var dragDeltaY = ev.y - dragStart[1]; var maxDragDelta = 16; if (Math.abs(dragDeltaX) >= maxDragDelta || Math.abs(dragDeltaY) >= maxDragDelta) { inputState = INPUT_BUILDING_DRAG; return false; } break; case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { // If shift is down, let the player continue placing another of the same building var queued = Engine.HotkeyIsPressed("session.queue"); if (tryPlaceBuilding(queued)) { if (queued) inputState = INPUT_BUILDING_PLACEMENT; else inputState = INPUT_NORMAL; } else { inputState = INPUT_BUILDING_PLACEMENT; } return true; } break; case "mousebuttondown": if (ev.button == SDL_BUTTON_RIGHT) { // Cancel building resetPlacementEntity(); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BUILDING_DRAG: switch (ev.type) { case "mousemotion": var dragDeltaX = ev.x - dragStart[0]; var dragDeltaY = ev.y - dragStart[1]; var maxDragDelta = 16; if (Math.abs(dragDeltaX) >= maxDragDelta || Math.abs(dragDeltaY) >= maxDragDelta) { // Rotate in the direction of the mouse var target = Engine.GetTerrainAtPoint(ev.x, ev.y); placementAngle = Math.atan2(target.x - placementPosition.x, target.z - placementPosition.z); } else { // If the mouse is near the center, snap back to the default orientation placementAngle = defaultPlacementAngle; } - Engine.GuiInterfaceCall("SetBuildingPlacementPreview", { + var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { "template": placementEntity, "x": placementPosition.x, - "z": placementPosition.z, - "angle": placementAngle + "z": placementPosition.z }); - + if (snapData.snapped) + placementAngle = snapData.angle; + + updateBuildingPlacementPreview(); break; case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { // If shift is down, let the player continue placing another of the same building var queued = Engine.HotkeyIsPressed("session.queue"); if (tryPlaceBuilding(queued)) { if (queued) inputState = INPUT_BUILDING_PLACEMENT; else inputState = INPUT_NORMAL; } else { inputState = INPUT_BUILDING_PLACEMENT; } return true; } break; case "mousebuttondown": if (ev.button == SDL_BUTTON_RIGHT) { // Cancel building resetPlacementEntity(); inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BATCHTRAINING: switch (ev.type) { case "hotkeyup": if (ev.hotkey == "session.batchtrain") { flushTrainingQueueBatch(); inputState = INPUT_NORMAL; } break; } } return false; } function handleInputAfterGui(ev) { // Handle the time-warp testing features, restricted to single-player if (!g_IsNetworked && getGUIObjectByName("devTimeWarp").checked) { if (ev.type == "hotkeydown" && ev.hotkey == "timewarp.fastforward") Engine.SetSimRate(20.0); else if (ev.type == "hotkeyup" && ev.hotkey == "timewarp.fastforward") Engine.SetSimRate(1.0); else if (ev.type == "hotkeyup" && ev.hotkey == "timewarp.rewind") Engine.RewindTimeWarp(); } // State-machine processing: switch (inputState) { case INPUT_NORMAL: switch (ev.type) { case "mousemotion": // Highlight the first hovered entity (if any) var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y); if (ents.length) g_Selection.setHighlightList([ents[0]]); else g_Selection.setHighlightList([]); return false; case "mousebuttondown": if (ev.button == SDL_BUTTON_LEFT) { dragStart = [ ev.x, ev.y ]; inputState = INPUT_SELECTING; return true; } else if (ev.button == SDL_BUTTON_RIGHT) { var action = determineAction(ev.x, ev.y); if (!action) break; return doAction(action, ev); } break; case "hotkeydown": if (ev.hotkey.indexOf("selection.group.") == 0) { var now = new Date(); if ((now.getTime() - doublePressTimer < doublePressTime) && (ev.hotkey == prevHotkey)) { if (ev.hotkey.indexOf("selection.group.select.") == 0) { var sptr = ev.hotkey.split("."); performGroup("snap", sptr[3]); } } else { var sptr = ev.hotkey.split("."); performGroup(sptr[2], sptr[3]); doublePressTimer = now.getTime(); prevHotkey = ev.hotkey; } } break; } break; case INPUT_PRESELECTEDACTION: switch (ev.type) { case "mousebuttondown": if (ev.button == SDL_BUTTON_LEFT && preSelectedAction != ACTION_NONE) { var action = determineAction(ev.x, ev.y); if (!action) break; preSelectedAction = ACTION_NONE; inputState = INPUT_NORMAL; return doAction(action, ev); } else if (ev.button == SDL_BUTTON_RIGHT && preSelectedAction != ACTION_NONE) { preSelectedAction = ACTION_NONE; inputState = INPUT_NORMAL; break; } } break; case INPUT_SELECTING: switch (ev.type) { case "mousemotion": // If the mouse moved further than a limit, switch to bandbox mode var dragDeltaX = ev.x - dragStart[0]; var dragDeltaY = ev.y - dragStart[1]; if (Math.abs(dragDeltaX) >= maxDragDelta || Math.abs(dragDeltaY) >= maxDragDelta) { inputState = INPUT_BANDBOXING; return false; } var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y); g_Selection.setHighlightList(ents); return false; case "mousebuttonup": if (ev.button == SDL_BUTTON_LEFT) { var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y); if (!ents.length) { g_Selection.reset(); resetIdleUnit(); inputState = INPUT_NORMAL; return true; } var selectedEntity = ents[0]; var now = new Date(); // If camera following and we select different unit, stop if (Engine.GetFollowedEntity() != selectedEntity) { Engine.CameraFollow(0); } if ((now.getTime() - doubleClickTimer < doubleClickTime) && (selectedEntity == prevClickedEntity)) { // Double click or triple click has occurred var showOffscreen = Engine.HotkeyIsPressed("selection.offscreen"); var matchRank = true; var templateToMatch; // Check for double click or triple click if (!doubleClicked) { // If double click hasn't already occurred, this is a double click. // Select units matching exact template name (same rank) templateToMatch = Engine.GuiInterfaceCall("GetEntityState", selectedEntity).template; doubleClicked = true; // Reset the timer so the user has an extra period 'doubleClickTimer' to do a triple-click doubleClickTimer = now.getTime(); } else { // Double click has already occurred, so this is a triple click. // Select similar units regardless of rank templateToMatch = Engine.GuiInterfaceCall("GetEntityState", selectedEntity).identity.selectionGroupName; if (templateToMatch) { matchRank = false; } else { // No selection group name defined, so fall back to exact match templateToMatch = Engine.GuiInterfaceCall("GetEntityState", selectedEntity).template; } } // TODO: Should we handle "control all units" here as well? ents = Engine.PickSimilarFriendlyEntities(templateToMatch, showOffscreen, matchRank); } else { // It's single click right now but it may become double or triple click doubleClicked = false; doubleClickTimer = now.getTime(); prevClickedEntity = selectedEntity; // We only want to include the first picked unit in the selection ents = [ents[0]]; } // Update the list of selected units if (Engine.HotkeyIsPressed("selection.add")) { g_Selection.addList(ents); } else if (Engine.HotkeyIsPressed("selection.remove")) { g_Selection.removeList(ents); } else { g_Selection.reset(); g_Selection.addList(ents); } inputState = INPUT_NORMAL; return true; } break; } break; case INPUT_BUILDING_PLACEMENT: switch (ev.type) { case "mousemotion": placementPosition = Engine.GetTerrainAtPoint(ev.x, ev.y); - Engine.GuiInterfaceCall("SetBuildingPlacementPreview", { + var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", { "template": placementEntity, "x": placementPosition.x, - "z": placementPosition.z, - "angle": placementAngle + "z": placementPosition.z }); + if (snapData.snapped) + placementAngle = snapData.angle; + + updateBuildingPlacementPreview(); return false; // continue processing mouse motion case "mousebuttondown": if (ev.button == SDL_BUTTON_LEFT) { placementPosition = Engine.GetTerrainAtPoint(ev.x, ev.y); dragStart = [ ev.x, ev.y ]; inputState = INPUT_BUILDING_CLICK; return true; } else if (ev.button == SDL_BUTTON_RIGHT) { // Cancel building resetPlacementEntity(); inputState = INPUT_NORMAL; return true; } break; case "hotkeydown": var rotation_step = Math.PI / 12; // 24 clicks make a full rotation switch (ev.hotkey) { case "session.rotate.cw": placementAngle += rotation_step; - Engine.GuiInterfaceCall("SetBuildingPlacementPreview", { - "template": placementEntity, - "x": placementPosition.x, - "z": placementPosition.z, - "angle": placementAngle - }); + updateBuildingPlacementPreview(); break; case "session.rotate.ccw": placementAngle -= rotation_step; - Engine.GuiInterfaceCall("SetBuildingPlacementPreview", { - "template": placementEntity, - "x": placementPosition.x, - "z": placementPosition.z, - "angle": placementAngle - }); + updateBuildingPlacementPreview(); break; } break; } break; } return false; } function doAction(action, ev) { var selection = g_Selection.toList(); // If shift is down, add the order to the unit's order queue instead // of running it immediately var queued = Engine.HotkeyIsPressed("session.queue"); switch (action.type) { case "move": var target = Engine.GetTerrainAtPoint(ev.x, ev.y); Engine.PostNetworkCommand({"type": "walk", "entities": selection, "x": target.x, "z": target.z, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] }); return true; case "attack": Engine.PostNetworkCommand({"type": "attack", "entities": selection, "target": action.target, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] }); return true; case "build": // (same command as repair) case "repair": Engine.PostNetworkCommand({"type": "repair", "entities": selection, "target": action.target, "autocontinue": true, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] }); return true; case "gather": Engine.PostNetworkCommand({"type": "gather", "entities": selection, "target": action.target, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_gather", "entity": selection[0] }); return true; case "returnresource": Engine.PostNetworkCommand({"type": "returnresource", "entities": selection, "target": action.target, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_gather", "entity": selection[0] }); return true; case "garrison": Engine.PostNetworkCommand({"type": "garrison", "entities": selection, "target": action.target, "queued": queued}); // TODO: Play a sound? return true; case "set-rallypoint": var target = Engine.GetTerrainAtPoint(ev.x, ev.y); Engine.PostNetworkCommand({"type": "set-rallypoint", "entities": selection, "x": target.x, "z": target.z}); // Display rally point at the new coordinates, to avoid display lag Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": selection, "x": target.x, "z": target.z }); return true; case "unset-rallypoint": var target = Engine.GetTerrainAtPoint(ev.x, ev.y); Engine.PostNetworkCommand({"type": "unset-rallypoint", "entities": selection}); // Remove displayed rally point Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": [] }); return true; case "none": return true; default: error("Invalid action.type "+action.type); return false; } } function handleMinimapEvent(target) { // Partly duplicated from handleInputAfterGui(), but with the input being // world coordinates instead of screen coordinates. if (inputState == INPUT_NORMAL) { var fromMinimap = true; var action = determineAction(undefined, undefined, fromMinimap); if (!action) return false; var selection = g_Selection.toList(); var queued = Engine.HotkeyIsPressed("session.queue"); switch (action.type) { case "move": Engine.PostNetworkCommand({"type": "walk", "entities": selection, "x": target.x, "z": target.z, "queued": queued}); Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] }); return true; case "set-rallypoint": Engine.PostNetworkCommand({"type": "set-rallypoint", "entities": selection, "x": target.x, "z": target.z}); // Display rally point at the new coordinates, to avoid display lag Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": selection, "x": target.x, "z": target.z }); return true; default: error("Invalid action.type "+action.type); } } return false; } // Called by GUI when user clicks construction button function startBuildingPlacement(buildEntType) { placementEntity = buildEntType; placementAngle = defaultPlacementAngle; inputState = INPUT_BUILDING_PLACEMENT; } // Batch training: // When the user shift-clicks, we set these variables and switch to INPUT_BATCHTRAINING // When the user releases shift, or clicks on a different training button, we create the batched units var batchTrainingEntity; var batchTrainingType; var batchTrainingCount; const batchIncrementSize = 5; function flushTrainingQueueBatch() { Engine.PostNetworkCommand({"type": "train", "entity": batchTrainingEntity, "template": batchTrainingType, "count": batchTrainingCount}); } // Called by GUI when user clicks training button function addToTrainingQueue(entity, trainEntType) { if (Engine.HotkeyIsPressed("session.batchtrain")) { if (inputState == INPUT_BATCHTRAINING) { // If we're already creating a batch of this unit, then just extend it if (batchTrainingEntity == entity && batchTrainingType == trainEntType) { batchTrainingCount += batchIncrementSize; return; } // Otherwise start a new one else { flushTrainingQueueBatch(); // fall through to create the new batch } } inputState = INPUT_BATCHTRAINING; batchTrainingEntity = entity; batchTrainingType = trainEntType; batchTrainingCount = batchIncrementSize; } else { // Non-batched - just create a single entity Engine.PostNetworkCommand({"type": "train", "entity": entity, "template": trainEntType, "count": 1}); } } // Returns the number of units that will be present in a batch if the user clicks // the training button with shift down function getTrainingQueueBatchStatus(entity, trainEntType) { if (inputState == INPUT_BATCHTRAINING && batchTrainingEntity == entity && batchTrainingType == trainEntType) return [batchTrainingCount, batchIncrementSize]; else return [0, batchIncrementSize]; } // Called by GUI when user clicks production queue item function removeFromTrainingQueue(entity, id) { Engine.PostNetworkCommand({"type": "stop-train", "entity": entity, "id": id}); } // Called by unit selection buttons function changePrimarySelectionGroup(templateName) { if (Engine.HotkeyIsPressed("session.deselectgroup")) g_Selection.makePrimarySelection(templateName, true); else g_Selection.makePrimarySelection(templateName, false); } // Performs the specified command (delete, town bell, repair, etc.) function performCommand(entity, commandName) { if (entity) { var entState = GetEntityState(entity); var template = GetTemplateData(entState.template); var unitName = getEntityName(template); var playerID = Engine.GetPlayerID(); if (entState.player == playerID || g_DevSettings.controlAll) { switch (commandName) { case "delete": var selection = g_Selection.toList(); if (selection.length > 0) { var message = "Are you sure you want to\ndelete the selected units?"; var deleteFunction = function () { Engine.PostNetworkCommand({"type": "delete-entities", "entities": selection}); }; g_SessionDialog.open("Delete", message, null, 340, 160, deleteFunction); } break; case "garrison": inputState = INPUT_PRESELECTEDACTION; preSelectedAction = ACTION_GARRISON; break; case "repair": inputState = INPUT_PRESELECTEDACTION; preSelectedAction = ACTION_REPAIR; break; case "unload-all": unloadAll(entity); break; default: break; } } } } // Performs the specified formation function performFormation(entity, formationName) { if (entity) { var selection = g_Selection.toList(); Engine.PostNetworkCommand({ "type": "formation", "entities": selection, "name": formationName }); } } // Performs the specified group function performGroup(action, groupId) { switch (action) { case "snap": case "select": var toSelect = []; g_Groups.update(); for (var ent in g_Groups.groups[groupId].ents) toSelect.push(+ent); g_Selection.reset(); g_Selection.addList(toSelect); if (action == "snap" && toSelect.length) Engine.CameraFollow(toSelect[0]); break; case "add": var selection = g_Selection.toList(); g_Groups.addEntities(groupId, selection); updateGroups(); break; case "save": var selection = g_Selection.toList(); g_Groups.groups[groupId].reset(); g_Groups.addEntities(groupId, selection); updateGroups(); break; } } // Performs the specified stance function performStance(entity, stanceName) { if (entity) { var selection = g_Selection.toList(); Engine.PostNetworkCommand({ "type": "stance", "entities": selection, "name": stanceName }); } } // Set the camera to follow the given unit function setCameraFollow(entity) { // Follow the given entity if it's a unit if (entity) { var entState = GetEntityState(entity); if (entState && isUnit(entState)) { Engine.CameraFollow(entity); return; } } // Otherwise stop following Engine.CameraFollow(0); } var lastIdleUnit = 0; var currIdleClass = 0; function resetIdleUnit() { lastIdleUnit = 0; currIdleClass = 0; } function findIdleUnit(classes) { // Cycle through idling classes before giving up for (var i = 0; i <= classes.length; ++i) { var data = { prevUnit: lastIdleUnit, idleClass: classes[currIdleClass] }; var newIdleUnit = Engine.GuiInterfaceCall("FindIdleUnit", data); // Check if we have new valid entity if (newIdleUnit && newIdleUnit != lastIdleUnit) { lastIdleUnit = newIdleUnit; g_Selection.reset() g_Selection.addList([lastIdleUnit]); Engine.CameraFollow(lastIdleUnit); return; } lastIdleUnit = 0; currIdleClass = (currIdleClass + 1) % classes.length; } // TODO: display a message or play a sound to indicate no more idle units, or something // Reset for next cycle resetIdleUnit(); } function unload(garrisonHolder, entity) { Engine.PostNetworkCommand({"type": "unload", "entity": entity, "garrisonHolder": garrisonHolder}); } function unloadAll(garrisonHolder) { Engine.PostNetworkCommand({"type": "unload-all", "garrisonHolder": garrisonHolder}); } Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Player.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Player.js (revision 9969) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/Player.js (revision 9970) @@ -1,282 +1,292 @@ /** * Used to create player entities prior to reading the rest of a map, * all other initialization must be done after loading map (terrain/entities). * DO NOT use other components here, as they may fail unpredictably. */ function LoadPlayerSettings(settings) { // Default settings if (!settings) { settings = {}; } // Get default player data var rawData = Engine.ReadJSONFile("player_defaults.json"); if (!(rawData && rawData.PlayerData)) { throw("Player.js: Error reading player default data (player_defaults.json)"); } var playerDefaults = rawData.PlayerData; // default number of players var numPlayers = 8; if (settings.PlayerData) { //Get number of players including gaia numPlayers = settings.PlayerData.length + 1; } else { warn("Player.js: Setup has no player data - using defaults"); } // Get player manager var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var teams = []; var diplomacy = []; // Build team + diplomacy data for (var i = 0; i < numPlayers; ++i) { diplomacy[i] = cmpPlayerMan.Diplomacy.ENEMY; // Skip gaia if (i > 0) { var pData = settings.PlayerData ? settings.PlayerData[i-1] : {}; var pDefs = playerDefaults ? playerDefaults[i] : {}; var team = getSetting(pData, pDefs, "Team"); // If team defined, add player to the team if (team !== undefined && team != -1) { if (!teams[team]) { teams[team] = [i]; } else { teams[team].push(i); } } } } for (var i = 0; i < numPlayers; ++i) { // Add player entity to engine var entID = Engine.AddEntity("special/player"); // Retrieve entity var player = Engine.QueryInterface(entID, IID_Player); if (!player) { throw("Player.js: Error creating player entity "+i); } player.SetPlayerID(i); var pDefs = playerDefaults ? playerDefaults[i] : {}; // Skip gaia if (i > 0) { var pData = settings.PlayerData ? settings.PlayerData[i-1] : {}; // Copy player data player.SetName(getSetting(pData, pDefs, "Name")); player.SetCiv(getSetting(pData, pDefs, "Civ")); var colour = getSetting(pData, pDefs, "Colour"); player.SetColour(colour.r, colour.g, colour.b); if (getSetting(pData, pDefs, "PopulationLimit") !== undefined) { player.SetMaxPopulation(getSetting(pData, pDefs, "PopulationLimit")); } if (getSetting(pData, pDefs, "Resources") !== undefined) { player.SetResourceCounts(getSetting(pData, pDefs, "Resources")); } var team = getSetting(pData, pDefs, "Team"); //If diplomacy array exists use that, otherwise use team data or default diplomacy if (getSetting(pData, pDefs, "Diplomacy") !== undefined) { player.SetDiplomacy(getSetting(pData, pDefs, "Diplomacy")); } else if (team !== undefined && team != -1) { //Team exists, copy default diplomacy var teamDiplomacy = []; for (var p in diplomacy) { teamDiplomacy[p] = diplomacy[p]; } // Set teammates to allies var myTeam = teams[team]; for (var n in myTeam) { teamDiplomacy[myTeam[n]] = cmpPlayerMan.Diplomacy.ALLY; //Set ally } player.SetDiplomacy(teamDiplomacy); } else { //Set default player.SetDiplomacy(diplomacy); } var startCam = getSetting(pData, pDefs, "StartingCamera"); if (startCam !== undefined) { player.SetStartingCamera(startCam.Position, startCam.Rotation); } } else { // Copy gaia data from defaults player.SetName(pDefs.Name); player.SetCiv(pDefs.Civ); player.SetColour(pDefs.Colour.r, pDefs.Colour.g, pDefs.Colour.b); player.SetDiplomacy(diplomacy); } // Add player to player manager cmpPlayerMan.AddPlayer(entID); } } // Get a setting if it exists or return default function getSetting(settings, defaults, property) { if (settings && (property in settings)) return settings[property]; // Use defaults if (defaults && (property in defaults)) return defaults[property]; return undefined; } /** * Similar to Engine.QueryInterface but applies to the player entity * that owns the given entity. * iid is typically IID_Player. */ function QueryOwnerInterface(ent, iid) { var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (!cmpOwnership) return null; var playerEnt = cmpPlayerMan.GetPlayerByID(cmpOwnership.GetOwner()); if (!playerEnt) return null; return Engine.QueryInterface(playerEnt, iid); } /** * Similar to Engine.QueryInterface but applies to the player entity * with the given ID number. * iid is typically IID_Player. */ function QueryPlayerIDInterface(id, iid) { var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var playerEnt = cmpPlayerMan.GetPlayerByID(id); if (!playerEnt) return null; return Engine.QueryInterface(playerEnt, iid); } /** * Returns true if the entity 'target' is owned by an ally of * the owner of 'entity'. */ function IsOwnedByAllyOfEntity(entity, target) { // Figure out which player controls us var owner = 0; var cmpOwnership = Engine.QueryInterface(entity, IID_Ownership); if (cmpOwnership) owner = cmpOwnership.GetOwner(); // Figure out which player controls the foundation being built var targetOwner = 0; var cmpOwnershipTarget = Engine.QueryInterface(target, IID_Ownership); if (cmpOwnershipTarget) targetOwner = cmpOwnershipTarget.GetOwner(); // Get our diplomacy array var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(owner), IID_Player); // Check for allied diplomacy status if (cmpPlayer.IsAlly(targetOwner)) return true; return false; } /** + * Returns true if the entity 'target' is owned by player + */ +function IsOwnedByPlayer(player, target) +{ + var cmpOwnershipTarget = Engine.QueryInterface(target, IID_Ownership); + return (cmpOwnershipTarget && player == cmpOwnershipTarget.GetOwner()); +} + +/** * Returns true if the entity 'target' is owned by an ally of player */ function IsOwnedByAllyOfPlayer(player, target) { // Figure out which player controls the foundation being built var targetOwner = 0; var cmpOwnershipTarget = Engine.QueryInterface(target, IID_Ownership); if (cmpOwnershipTarget) targetOwner = cmpOwnershipTarget.GetOwner(); // Get our diplomacy array var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(player), IID_Player); // Check for allied diplomacy status if (cmpPlayer.IsAlly(targetOwner)) return true; return false; } /** * Returns true if the entity 'target' is owned by an enemy of player */ function IsOwnedByEnemyOfPlayer(player, target) { // Figure out which player controls the foundation being built var targetOwner = 0; var cmpOwnershipTarget = Engine.QueryInterface(target, IID_Ownership); if (cmpOwnershipTarget) targetOwner = cmpOwnershipTarget.GetOwner(); // Get our diplomacy array var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(player), IID_Player); // Check for allied diplomacy status if (cmpPlayer.IsEnemy(targetOwner)) return true; return false; } Engine.RegisterGlobal("LoadPlayerSettings", LoadPlayerSettings); Engine.RegisterGlobal("QueryOwnerInterface", QueryOwnerInterface); Engine.RegisterGlobal("QueryPlayerIDInterface", QueryPlayerIDInterface); Engine.RegisterGlobal("IsOwnedByAllyOfEntity", IsOwnedByAllyOfEntity); +Engine.RegisterGlobal("IsOwnedByPlayer", IsOwnedByPlayer); Engine.RegisterGlobal("IsOwnedByAllyOfPlayer", IsOwnedByAllyOfPlayer); Engine.RegisterGlobal("IsOwnedByEnemyOfPlayer", IsOwnedByEnemyOfPlayer); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 9969) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 9970) @@ -1,595 +1,592 @@ function ProcessCommand(player, cmd) { // Do some basic checks here that commanding player is valid var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (!cmpPlayerMan || player < 0) return; var playerEnt = cmpPlayerMan.GetPlayerByID(player); if (playerEnt == INVALID_ENTITY) return; var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player); if (!cmpPlayer) return; var controlAllUnits = cmpPlayer.CanControlAllUnits(); // Now handle various commands switch (cmd.type) { case "debug-print": print(cmd.message); break; case "chat": var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({"type": "chat", "player": player, "message": cmd.message}); break; case "control-all": cmpPlayer.SetControlAllUnits(cmd.flag); break; case "reveal-map": // Reveal the map for all players, not just the current player, // primarily to make it obvious to everyone that the player is cheating var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); cmpRangeManager.SetLosRevealAll(-1, cmd.enable); break; case "walk": var entities = FilterEntityList(cmd.entities, player, controlAllUnits); GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) { cmpUnitAI.Walk(cmd.x, cmd.z, cmd.queued); }); break; case "attack": // Check if target is owned by player's enemy if (IsOwnedByEnemyOfPlayer(player, cmd.target)) { var entities = FilterEntityList(cmd.entities, player, controlAllUnits); GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) { cmpUnitAI.Attack(cmd.target, cmd.queued); }); } break; case "repair": // This covers both repairing damaged buildings, and constructing unfinished foundations // Check if target building is owned by player or an ally if (IsOwnedByAllyOfPlayer(player, cmd.target)) { var entities = FilterEntityList(cmd.entities, player, controlAllUnits); GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) { cmpUnitAI.Repair(cmd.target, cmd.autocontinue, cmd.queued); }); } break; case "gather": var entities = FilterEntityList(cmd.entities, player, controlAllUnits); GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) { cmpUnitAI.Gather(cmd.target, cmd.queued); }); break; case "returnresource": // Check dropsite is owned by player - if (IsOwnedByPlayer(cmd.target, player)) + if (IsOwnedByPlayer(player, cmd.target)) { var entities = FilterEntityList(cmd.entities, player, controlAllUnits); GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) { cmpUnitAI.ReturnResource(cmd.target, cmd.queued); }); } break; case "train": if (CanControlUnit(cmd.entity, player, controlAllUnits)) { var queue = Engine.QueryInterface(cmd.entity, IID_TrainingQueue); if (queue) queue.AddBatch(cmd.template, +cmd.count, cmd.metadata); } break; case "stop-train": if (CanControlUnit(cmd.entity, player, controlAllUnits)) { var queue = Engine.QueryInterface(cmd.entity, IID_TrainingQueue); if (queue) queue.RemoveBatch(cmd.id); } break; case "construct": // Message structure: // { // "type": "construct", // "entities": [...], // "template": "...", // "x": ..., // "z": ..., // "angle": ..., // "autorepair": true, // whether to automatically start constructing/repairing the new foundation // "autocontinue": true, // whether to automatically gather/build/etc after finishing this // "queued": true, // } /* * Construction process: * . Take resources away immediately. * . Create a foundation entity with 1hp, 0% build progress. * . Increase hp and build progress up to 100% when people work on it. * . If it's destroyed, an appropriate fraction of the resource cost is refunded. * . If it's completed, it gets replaced with the real building. */ // Check that we can control these units var entities = FilterEntityList(cmd.entities, player, controlAllUnits); if (!entities.length) break; // Tentatively create the foundation (we might find later that it's a invalid build command) var ent = Engine.AddEntity("foundation|" + cmd.template); - // TODO: report errors (e.g. invalid template names) + if (ent == INVALID_ENTITY) + { + // Error (e.g. invalid template names) + error("Error creating foundation for '" + cmd.template + "'"); + break; + } // Move the foundation to the right place var cmpPosition = Engine.QueryInterface(ent, IID_Position); cmpPosition.JumpTo(cmd.x, cmd.z); cmpPosition.SetYRotation(cmd.angle); - // Check whether it's obstructed by other entities - var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction); - if (cmpObstruction && cmpObstruction.CheckFoundationCollisions()) + // Check whether it's obstructed by other entities or invalid terrain + var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); + if (!cmpBuildRestrictions || !cmpBuildRestrictions.CheckPlacement(player)) { - // TODO: report error to player (the building site was obstructed) - print("Building site was obstructed\n"); + var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); + cmpGuiInterface.PushNotification({ "player": player, "message": "Building site was obstructed" }); // Remove the foundation because the construction was aborted Engine.DestroyEntity(ent); - + break; + } + + // Check build limits + var cmpBuildLimits = QueryPlayerIDInterface(player, IID_BuildLimits); + if (!cmpBuildLimits || !cmpBuildLimits.AllowedToBuild(cmpBuildRestrictions.GetCategory())) + { + // TODO: The UI should tell the user they can't build this (but we still need this check) + + // Remove the foundation because the construction was aborted + Engine.DestroyEntity(ent); break; } /* TODO: the AI isn't smart enough to explore before building, so we'll * just disable the requirement that the location is visible. Should we * fix that, or let players build in fog too, or something? // Check whether it's in a visible region var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var visible = (cmpRangeManager.GetLosVisibility(ent, player) == "visible"); if (!visible) { // TODO: report error to player (the building site was not visible) print("Building site was not visible\n"); Engine.DestroyEntity(ent); break; } */ - - var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); - var cmpBuildLimits = Engine.QueryInterface(playerEnt, IID_BuildLimits); - if (!cmpBuildLimits.AllowedToBuild(cmpBuildRestrictions.GetCategory())) - { - Engine.DestroyEntity(ent); - break; - } var cmpCost = Engine.QueryInterface(ent, IID_Cost); if (!cmpPlayer.TrySubtractResources(cmpCost.GetResourceCosts())) { - // TODO: report error to player (they ran out of resources) Engine.DestroyEntity(ent); break; } // Make it owned by the current player var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); cmpOwnership.SetOwner(player); // Initialise the foundation var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation); cmpFoundation.InitialiseConstruction(player, cmd.template); // Tell the units to start building this new entity if (cmd.autorepair) { ProcessCommand(player, { "type": "repair", "entities": entities, "target": ent, "autocontinue": cmd.autocontinue, "queued": cmd.queued }); } break; case "delete-entities": var entities = FilterEntityList(cmd.entities, player, controlAllUnits); for each (var ent in entities) { var cmpHealth = Engine.QueryInterface(ent, IID_Health); if (cmpHealth) cmpHealth.Kill(); else Engine.DestroyEntity(ent); } break; case "set-rallypoint": var entities = FilterEntityList(cmd.entities, player, controlAllUnits); for each (var ent in entities) { var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (cmpRallyPoint) cmpRallyPoint.SetPosition(cmd.x, cmd.z); } break; case "unset-rallypoint": var entities = FilterEntityList(cmd.entities, player, controlAllUnits); for each (var ent in entities) { var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (cmpRallyPoint) cmpRallyPoint.Unset(); } break; case "defeat-player": // Send "OnPlayerDefeated" message to player Engine.PostMessage(playerEnt, MT_PlayerDefeated, null); break; case "garrison": if (CanControlUnit(cmd.target, player, controlAllUnits)) { var entities = FilterEntityList(cmd.entities, player, controlAllUnits); GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) { cmpUnitAI.Garrison(cmd.target); }); } break; case "unload": if (CanControlUnit(cmd.garrisonHolder, player, controlAllUnits)) { var cmpGarrisonHolder = Engine.QueryInterface(cmd.garrisonHolder, IID_GarrisonHolder); if (cmpGarrisonHolder) cmpGarrisonHolder.Unload(cmd.entity); } break; case "unload-all": if (CanControlUnit(cmd.garrisonHolder, player, controlAllUnits)) { var cmpGarrisonHolder = Engine.QueryInterface(cmd.garrisonHolder, IID_GarrisonHolder); cmpGarrisonHolder.UnloadAll(); } break; case "formation": var entities = FilterEntityList(cmd.entities, player, controlAllUnits); GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) { var cmpFormation = Engine.QueryInterface(cmpUnitAI.entity, IID_Formation); if (!cmpFormation) return; cmpFormation.LoadFormation(cmd.name); cmpFormation.MoveMembersIntoFormation(true); }); break; case "promote": var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({"type": "chat", "player": player, "message": "(Cheat - promoted units)"}); for each (var ent in cmd.entities) { var cmpPromotion = Engine.QueryInterface(ent, IID_Promotion); if (cmpPromotion) cmpPromotion.IncreaseXp(cmpPromotion.GetRequiredXp() - cmpPromotion.GetCurrentXp()); } break; case "stance": var entities = FilterEntityList(cmd.entities, player, controlAllUnits); for each (var ent in entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) cmpUnitAI.SwitchToStance(cmd.name); } break; default: error("Ignoring unrecognised command type '" + cmd.type + "'"); } } /** * Get some information about the formations used by entities. * The entities must have a UnitAI component. */ function ExtractFormations(ents) { var entities = []; // subset of ents that have UnitAI var members = {}; // { formationentity: [ent, ent, ...], ... } for each (var ent in ents) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); var fid = cmpUnitAI.GetFormationController(); if (fid != INVALID_ENTITY) { if (!members[fid]) members[fid] = []; members[fid].push(ent); } entities.push(ent); } var ids = [ id for (id in members) ]; return { "entities": entities, "members": members, "ids": ids }; } /** * Remove the given list of entities from their current formations. */ function RemoveFromFormation(ents) { var formation = ExtractFormations(ents); for (var fid in formation.members) { var cmpFormation = Engine.QueryInterface(+fid, IID_Formation); if (cmpFormation) cmpFormation.RemoveMembers(formation.members[fid]); } } /** * Returns a list of UnitAI components, each belonging either to a * selected unit or to a formation entity for groups of the selected units. */ function GetFormationUnitAIs(ents) { // If an individual was selected, remove it from any formation // and command it individually if (ents.length == 1) { // Skip unit if it has no UnitAI var cmpUnitAI = Engine.QueryInterface(ents[0], IID_UnitAI); if (!cmpUnitAI) return []; RemoveFromFormation(ents); return [ cmpUnitAI ]; } // Separate out the units that don't support the chosen formation var formedEnts = []; var nonformedUnitAIs = []; for each (var ent in ents) { // Skip units with no UnitAI var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (!cmpUnitAI) continue; var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); // TODO: Currently we use LineClosed as effectively a boolean flag // to determine whether formations are allowed at all. Instead we // should check specific formation names and do something sensible // (like what?) when some units don't support them. // TODO: We'll also need to fix other formation code to use // "LineClosed" instead of "Line Closed" etc consistently. if (cmpIdentity && cmpIdentity.CanUseFormation("LineClosed")) formedEnts.push(ent); else nonformedUnitAIs.push(cmpUnitAI); } if (formedEnts.length == 0) { // No units support the foundation - return all the others return nonformedUnitAIs; } // Find what formations the formationable selected entities are currently in var formation = ExtractFormations(formedEnts); var formationEnt = undefined; if (formation.ids.length == 1) { // Selected units all belong to the same formation. // Check that it doesn't have any other members var fid = formation.ids[0]; var cmpFormation = Engine.QueryInterface(+fid, IID_Formation); if (cmpFormation && cmpFormation.GetMemberCount() == formation.entities.length) { // The whole formation was selected, so reuse its controller for this command formationEnt = +fid; } } if (!formationEnt) { // We need to give the selected units a new formation controller // Remove selected units from their current formation for (var fid in formation.members) { var cmpFormation = Engine.QueryInterface(+fid, IID_Formation); if (cmpFormation) cmpFormation.RemoveMembers(formation.members[fid]); } // Create the new controller formationEnt = Engine.AddEntity("special/formation"); var cmpFormation = Engine.QueryInterface(formationEnt, IID_Formation); cmpFormation.SetMembers(formation.entities); // If all the selected units were previously in formations of the same shape, // then set this new formation to that shape too; otherwise use the default shape var lastFormationName = undefined; for each (var ent in formation.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) { var name = cmpUnitAI.GetLastFormationName(); if (lastFormationName === undefined) { lastFormationName = name; } else if (lastFormationName != name) { lastFormationName = undefined; break; } } } var formationName; if (lastFormationName) formationName = lastFormationName; else formationName = "Line Closed"; if (CanMoveEntsIntoFormation(formation.entities, formationName)) { cmpFormation.LoadFormation(formationName); } else { cmpFormation.LoadFormation("Loose"); } } return nonformedUnitAIs.concat(Engine.QueryInterface(formationEnt, IID_UnitAI)); } function CanMoveEntsIntoFormation(ents, formationName) { var count = ents.length; var classesRequired; // TODO: should check the player's civ is allowed to use this formation if (formationName == "Loose") { return true; } else if (formationName == "Box") { if (count < 4) return false; } else if (formationName == "Column Closed") { } else if (formationName == "Line Closed") { } else if (formationName == "Column Open") { } else if (formationName == "Line Open") { } else if (formationName == "Flank") { if (count < 8) return false; } else if (formationName == "Skirmish") { classesRequired = ["Ranged"]; } else if (formationName == "Wedge") { if (count < 3) return false; classesRequired = ["Cavalry"]; } else if (formationName == "Formation12") { } else if (formationName == "Phalanx") { if (count < 10) return false; classesRequired = ["Melee", "Infantry"]; } else if (formationName == "Syntagma") { if (count < 9) return false; classesRequired = ["Melee", "Infantry"]; // TODO: pike only } else if (formationName == "Testudo") { if (count < 9) return false; classesRequired = ["Melee", "Infantry"]; } else { return false; } var looseOnlyUnits = true; for each (var ent in ents) { var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (cmpIdentity) { var classes = cmpIdentity.GetClassesList(); if (looseOnlyUnits && (classes.indexOf("Worker") == -1 || classes.indexOf("Support") == -1)) looseOnlyUnits = false; for each (var classRequired in classesRequired) { if (classes.indexOf(classRequired) == -1) { return false; } } } } if (looseOnlyUnits) return false; return true; } /** - * Check if entity is owned by player - */ -function IsOwnedByPlayer(entity, player) -{ - var cmpOwnership = Engine.QueryInterface(entity, IID_Ownership); - return (cmpOwnership && cmpOwnership.GetOwner() == player); -} - -/** * Check if player can control this entity * returns: true if the entity is valid and owned by the player if * or control all units is activated for the player, else false */ function CanControlUnit(entity, player, controlAll) { - return (IsOwnedByPlayer(entity, player) || controlAll); + return (IsOwnedByPlayer(player, entity) || controlAll); } /** * Filter entities which the player can control */ function FilterEntityList(entities, player, controlAll) { return entities.filter(function(ent) { return CanControlUnit(ent, player, controlAll);} ); } Engine.RegisterGlobal("CanMoveEntsIntoFormation", CanMoveEntsIntoFormation); Engine.RegisterGlobal("ProcessCommand", ProcessCommand); Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 9969) +++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 9970) @@ -1,603 +1,675 @@ function GuiInterface() {} GuiInterface.prototype.Schema = ""; GuiInterface.prototype.Serialize = function() { // This component isn't network-synchronised so we mustn't serialise // its non-deterministic data. Instead just return an empty object. return {}; }; GuiInterface.prototype.Deserialize = function(obj) { this.Init(); }; GuiInterface.prototype.Init = function() { this.placementEntity = undefined; // = undefined or [templateName, entityID] this.rallyPoints = undefined; this.notifications = []; this.renamedEntities = []; }; /* * All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg) * from GUI scripts, and executed here with arguments (player, arg). */ /** * Returns global information about the current game state. * This is used by the GUI and also by AI scripts. */ GuiInterface.prototype.GetSimulationState = function(player) { var ret = { "players": [] }; var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var n = cmpPlayerMan.GetNumPlayers(); for (var i = 0; i < n; ++i) { var playerEnt = cmpPlayerMan.GetPlayerByID(i); var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player); var cmpPlayerStatisticsTracker = Engine.QueryInterface(playerEnt, IID_StatisticsTracker); // store player ally/enemy data as arrays var allies = []; var enemies = []; for (var j = 0; j <= n; ++j) { allies[j] = cmpPlayer.IsAlly(j); enemies[j] = cmpPlayer.IsEnemy(j); } var playerData = { "name": cmpPlayer.GetName(), "civ": cmpPlayer.GetCiv(), "colour": cmpPlayer.GetColour(), "popCount": cmpPlayer.GetPopulationCount(), "popLimit": cmpPlayer.GetPopulationLimit(), "resourceCounts": cmpPlayer.GetResourceCounts(), "trainingQueueBlocked": cmpPlayer.IsTrainingQueueBlocked(), "state": cmpPlayer.GetState(), "team": cmpPlayer.GetTeam(), "phase": cmpPlayer.GetPhase(), "isAlly": allies, "isEnemy": enemies }; ret.players.push(playerData); } var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (cmpRangeManager) { ret.circularMap = cmpRangeManager.GetLosCircular(); } // Add timeElapsed var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); ret.timeElapsed = cmpTimer.GetTime(); return ret; }; GuiInterface.prototype.GetExtendedSimulationState = function(player) { // Get basic simulation info var ret = this.GetSimulationState(); // Add statistics to each player var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var n = cmpPlayerMan.GetNumPlayers(); for (var i = 0; i < n; ++i) { var playerEnt = cmpPlayerMan.GetPlayerByID(i); var cmpPlayerStatisticsTracker = Engine.QueryInterface(playerEnt, IID_StatisticsTracker); ret.players[i].statistics = cmpPlayerStatisticsTracker.GetStatistics(); } return ret; }; GuiInterface.prototype.GetRenamedEntities = function(player, clearList) { var result = this.renamedEntities; if (clearList) this.renamedEntities = []; return result; }; GuiInterface.prototype.GetEntityState = function(player, ent) { var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); // All units must have a template; if not then it's a nonexistent entity id var template = cmpTempMan.GetCurrentTemplateName(ent); if (!template) return null; var ret = { "id": ent, "template": template } var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (cmpIdentity) { ret.identity = { "rank": cmpIdentity.GetRank(), "classes": cmpIdentity.GetClassesList(), "selectionGroupName": cmpIdentity.GetSelectionGroupName() }; } var cmpPosition = Engine.QueryInterface(ent, IID_Position); if (cmpPosition && cmpPosition.IsInWorld()) { ret.position = cmpPosition.GetPosition(); } var cmpHealth = Engine.QueryInterface(ent, IID_Health); if (cmpHealth) { ret.hitpoints = cmpHealth.GetHitpoints(); ret.maxHitpoints = cmpHealth.GetMaxHitpoints(); ret.needsRepair = cmpHealth.IsRepairable() && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints()); } var cmpAttack = Engine.QueryInterface(ent, IID_Attack); if (cmpAttack) { var type = cmpAttack.GetBestAttack(); // TODO: how should we decide which attack to show? ret.attack = cmpAttack.GetAttackStrengths(type); } var cmpArmour = Engine.QueryInterface(ent, IID_DamageReceiver); if (cmpArmour) { ret.armour = cmpArmour.GetArmourStrengths(); } var cmpBuilder = Engine.QueryInterface(ent, IID_Builder); if (cmpBuilder) { ret.buildEntities = cmpBuilder.GetEntitiesList(); } var cmpTrainingQueue = Engine.QueryInterface(ent, IID_TrainingQueue); if (cmpTrainingQueue) { ret.training = { "entities": cmpTrainingQueue.GetEntitiesList(), "queue": cmpTrainingQueue.GetQueue(), }; } var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation); if (cmpFoundation) { ret.foundation = { "progress": cmpFoundation.GetBuildPercentage() }; } var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) { ret.player = cmpOwnership.GetOwner(); } var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply); if (cmpResourceSupply) { ret.resourceSupply = { "max": cmpResourceSupply.GetMaxAmount(), "amount": cmpResourceSupply.GetCurrentAmount(), "type": cmpResourceSupply.GetType() }; } var cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer); if (cmpResourceGatherer) { ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates(); ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus(); } var cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite); if (cmpResourceDropsite) { ret.resourceDropsite = { "types": cmpResourceDropsite.GetTypes() }; } var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (cmpRallyPoint) { ret.rallyPoint = { }; } var cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder); if (cmpGarrisonHolder) { ret.garrisonHolder = { "entities": cmpGarrisonHolder.GetEntities(), "allowedClasses": cmpGarrisonHolder.GetAllowedClassesList() }; } var cmpPromotion = Engine.QueryInterface(ent, IID_Promotion); if (cmpPromotion) { ret.promotion = { "curr": cmpPromotion.GetCurrentXp(), "req": cmpPromotion.GetRequiredXp() }; } var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) { ret.unitAI = { // TODO: reading properties directly is kind of violating abstraction "state": cmpUnitAI.fsmStateName, "orders": cmpUnitAI.orderQueue, }; } var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); ret.visibility = cmpRangeManager.GetLosVisibility(ent, player); return ret; }; GuiInterface.prototype.GetTemplateData = function(player, name) { var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTempMan.GetTemplate(name); if (!template) return null; var ret = {}; if (template.Identity) { ret.selectionGroupName = template.Identity.SelectionGroupName; ret.name = { "specific": (template.Identity.SpecificName || template.Identity.GenericName), "generic": template.Identity.GenericName }; ret.icon = template.Identity.Icon; ret.tooltip = template.Identity.Tooltip; } if (template.Cost) { ret.cost = {}; if (template.Cost.Resources.food) ret.cost.food = +template.Cost.Resources.food; if (template.Cost.Resources.wood) ret.cost.wood = +template.Cost.Resources.wood; if (template.Cost.Resources.stone) ret.cost.stone = +template.Cost.Resources.stone; if (template.Cost.Resources.metal) ret.cost.metal = +template.Cost.Resources.metal; if (template.Cost.Population) ret.cost.population = +template.Cost.Population; if (template.Cost.PopulationBonus) ret.cost.populationBonus = +template.Cost.PopulationBonus; } return ret; }; GuiInterface.prototype.PushNotification = function(notification) { this.notifications.push(notification); }; GuiInterface.prototype.GetNextNotification = function() { if (this.notifications.length) return this.notifications.pop(); else return ""; }; GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data) { return CanMoveEntsIntoFormation(data.ents, data.formationName); }; GuiInterface.prototype.IsStanceSelected = function(player, data) { for each (var ent in data.ents) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) { if (cmpUnitAI.GetStanceName() == data.stance) return true; } } return false; }; GuiInterface.prototype.SetSelectionHighlight = function(player, cmd) { var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var playerColours = {}; // cache of owner -> colour map for each (var ent in cmd.entities) { var cmpSelectable = Engine.QueryInterface(ent, IID_Selectable); if (!cmpSelectable) continue; if (cmd.alpha == 0) { cmpSelectable.SetSelectionHighlight({"r":0, "g":0, "b":0, "a":0}); continue; } // Find the entity's owner's colour: var owner = -1; var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) owner = cmpOwnership.GetOwner(); var colour = playerColours[owner]; if (!colour) { colour = {"r":1, "g":1, "b":1}; var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(owner), IID_Player); if (cmpPlayer) colour = cmpPlayer.GetColour(); playerColours[owner] = colour; } cmpSelectable.SetSelectionHighlight({"r":colour.r, "g":colour.g, "b":colour.b, "a":cmd.alpha}); } }; GuiInterface.prototype.SetStatusBars = function(player, cmd) { for each (var ent in cmd.entities) { var cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars); if (cmpStatusBars) cmpStatusBars.SetEnabled(cmd.enabled); } }; /** * Displays the rally point of a building */ GuiInterface.prototype.DisplayRallyPoint = function(player, cmd) { // If there are rally points already displayed, destroy them for each (var ent in this.rallyPoints) { // Hide it first (the destruction won't be instantaneous) var cmpPosition = Engine.QueryInterface(ent, IID_Position); cmpPosition.MoveOutOfWorld(); Engine.DestroyEntity(ent); } this.rallyPoints = []; var positions = []; // DisplayRallyPoints is called passing a list of entities for which // rally points must be displayed for each (var ent in cmd.entities) { var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (!cmpRallyPoint) continue; // Verify the owner var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (!cmpOwnership || cmpOwnership.GetOwner() != player) continue; // If the command was passed an explicit position, use that and // override the real rally point position; otherwise use the real position var pos; if (cmd.x && cmd.z) pos = {"x": cmd.x, "z": cmd.z}; else pos = cmpRallyPoint.GetPosition(); if (pos) { // TODO: it'd probably be nice if we could draw some kind of line // between the building and pos, to make the marker easy to find even // if it's a long way from the building positions.push(pos); } } // Add rally point entity for each building for each (var pos in positions) { var rallyPoint = Engine.AddLocalEntity("actor|props/special/common/waypoint_flag.xml"); var cmpPosition = Engine.QueryInterface(rallyPoint, IID_Position); cmpPosition.JumpTo(pos.x, pos.z); this.rallyPoints.push(rallyPoint); } }; /** * Display the building placement preview. * cmd.template is the name of the entity template, or "" to disable the preview. * cmd.x, cmd.z, cmd.angle give the location. * Returns true if the placement is okay (everything is valid and the entity is not obstructed by others). */ GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd) { // See if we're changing template if (!this.placementEntity || this.placementEntity[0] != cmd.template) { // Destroy the old preview if there was one if (this.placementEntity) Engine.DestroyEntity(this.placementEntity[1]); // Load the new template if (cmd.template == "") { this.placementEntity = undefined; } else { this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)]; } } if (this.placementEntity) { var ent = this.placementEntity[1]; // Move the preview into the right location var pos = Engine.QueryInterface(ent, IID_Position); if (pos) { pos.JumpTo(cmd.x, cmd.z); pos.SetYRotation(cmd.angle); } - // Check whether it's obstructed by other entities - var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction); - var colliding = (cmpObstruction && cmpObstruction.CheckFoundationCollisions()); - // Check whether it's in a visible region var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); - var visible = (cmpRangeManager.GetLosVisibility(ent, player) == "visible"); - - var ok = (!colliding && visible); + var visible = (cmpRangeManager && cmpRangeManager.GetLosVisibility(ent, player) == "visible"); + var validPlacement = false; + + if (visible) + { // Check whether it's obstructed by other entities or invalid terrain + var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); + if (!cmpBuildRestrictions) + error("cmpBuildRestrictions not defined"); + + validPlacement = (cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement(player)); + } + var ok = (visible && validPlacement); + // Set it to a red shade if this is an invalid location var cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (cmpVisual) { if (!ok) cmpVisual.SetShadingColour(1.4, 0.4, 0.4, 1); else cmpVisual.SetShadingColour(1, 1, 1, 1); } return ok; } return false; }; +GuiInterface.prototype.GetFoundationSnapData = function(player, data) +{ + var cmpTemplateMgr = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); + var template = cmpTemplateMgr.GetTemplate(data.template); + + if (template.BuildRestrictions.Category == "Dock") + { + var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); + var cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager); + + // Find direction of most open water, algorithm: + // 1. Pick points in a circle around dock + // 2. If point is in water, add to array + // 3. Scan array looking for consective points + // 4. Find longest sequence of conseuctive points + // 5. Calculate angle using average of sequence + const numPoints = 16; + const dist = 20.0; + var waterPoints = []; + for (var i = 0; i < numPoints; ++i) + { + var angle = (i/numPoints)*2*Math.PI; + var nx = data.x - dist*Math.sin(angle); + var nz = data.z + dist*Math.cos(angle); + + if (cmpTerrain.GetGroundLevel(nx, nz) < cmpWaterManager.GetWaterLevel(nx, nz)) + { + waterPoints.push(i); + } + } + var consec = []; + var length = waterPoints.length; + for (var i = 0; i < length; ++i) + { + var count = 0; + for (var j = 0; j < (length-1); ++j) + { + if (((waterPoints[(i + j) % length]+1) % numPoints) == waterPoints[(i + j + 1) % length]) + { + ++count; + } + else + { + break; + } + } + consec[i] = count; + } + var start = 0; + var count = 0; + for (var c in consec) + { + if (consec[c] > count) + { + start = c; + count = consec[c]; + } + } + + return { "snapped": true, "x": data.x, "z": data.z, "angle": -(((waterPoints[start] + consec[start]/2) % numPoints)/numPoints*2*Math.PI) }; + } + + return {"snapped": false}; +}; + GuiInterface.prototype.PlaySound = function(player, data) { // Ignore if no entity was passed if (!data.entity) return; PlaySound(data.name, data.entity); }; function isIdleUnit(ent, idleClass) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); // TODO: Do something with garrisoned idle units return (cmpUnitAI && cmpIdentity && cmpUnitAI.IsIdle() && !cmpUnitAI.IsGarrisoned() && idleClass && cmpIdentity.HasClass(idleClass)); } GuiInterface.prototype.FindIdleUnit = function(player, data) { var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var playerEntities = rangeMan.GetEntitiesByPlayer(player); // Find the first matching entity that is after the previous selection, // so that we cycle around in a predictable order for each (var ent in playerEntities) { if (ent > data.prevUnit && isIdleUnit(ent, data.idleClass)) return ent; } // No idle entities left in the class return 0; }; GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled) { var cmpPathfinder = Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder); cmpPathfinder.SetDebugOverlay(enabled); }; GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled) { var cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); cmpObstructionManager.SetDebugOverlay(enabled); }; GuiInterface.prototype.SetMotionDebugOverlay = function(player, data) { for each (var ent in data.entities) { var cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); if (cmpUnitMotion) cmpUnitMotion.SetDebugOverlay(data.enabled); } }; GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); cmpRangeManager.SetDebugOverlay(enabled); }; GuiInterface.prototype.OnGlobalEntityRenamed = function(msg) { this.renamedEntities.push(msg); } // List the GuiInterface functions that can be safely called by GUI scripts. // (GUI scripts are non-deterministic and untrusted, so these functions must be // appropriately careful. They are called with a first argument "player", which is // trusted and indicates the player associated with the current client; no data should // be returned unless this player is meant to be able to see it.) var exposedFunctions = { "GetSimulationState": 1, "GetExtendedSimulationState": 1, "GetRenamedEntities": 1, "GetEntityState": 1, "GetTemplateData": 1, "GetNextNotification": 1, "CanMoveEntsIntoFormation": 1, "IsStanceSelected": 1, "SetSelectionHighlight": 1, "SetStatusBars": 1, "DisplayRallyPoint": 1, "SetBuildingPlacementPreview": 1, + "GetFoundationSnapData": 1, "PlaySound": 1, "FindIdleUnit": 1, "SetPathfinderDebugOverlay": 1, "SetObstructionDebugOverlay": 1, "SetMotionDebugOverlay": 1, "SetRangeDebugOverlay": 1, }; GuiInterface.prototype.ScriptCall = function(player, name, args) { if (exposedFunctions[name]) return this[name](player, args); else throw new Error("Invalid GuiInterface Call name \""+name+"\""); }; Engine.RegisterComponentType(IID_GuiInterface, "GuiInterface", GuiInterface); Index: ps/trunk/binaries/data/mods/public/simulation/components/TrainingQueue.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/TrainingQueue.js (revision 9969) +++ ps/trunk/binaries/data/mods/public/simulation/components/TrainingQueue.js (revision 9970) @@ -1,326 +1,330 @@ var g_ProgressInterval = 1000; const MAX_QUEUE_SIZE = 16; function TrainingQueue() {} TrainingQueue.prototype.Schema = "Allows the building to train new units." + "" + "" + "\n units/{civ}_support_female_citizen\n units/{civ}_support_trader\n units/celt_infantry_spearman_b\n " + "" + "" + "" + "" + "tokens" + "" + "" + ""; TrainingQueue.prototype.Init = function() { this.nextID = 1; this.queue = []; // Queue items are: // { // "id": 1, // "player": 1, // who paid for this batch; we need this to cope with refunds cleanly // "template": "units/example", // "count": 10, // "resources": { "wood": 100, ... }, // "population": 10, // "trainingStarted": false, // true iff we have reserved population // "timeTotal": 15000, // msecs // "timeRemaining": 10000, // msecs // } this.timer = undefined; // g_ProgressInterval msec timer, active while the queue is non-empty }; TrainingQueue.prototype.GetEntitiesList = function() { var string = this.template.Entities._string; // Replace the "{civ}" codes with this entity's civ ID var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); if (cmpIdentity) string = string.replace(/\{civ\}/g, cmpIdentity.GetCiv()); return string.split(/\s+/); }; TrainingQueue.prototype.AddBatch = function(templateName, count, metadata) { // TODO: there should probably be a limit on the number of queued batches // TODO: there should be a way for the GUI to determine whether it's going // to be possible to add a batch (based on resource costs and length limits) var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); if (this.queue.length < MAX_QUEUE_SIZE) { // Find the template data so we can determine the build costs var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTempMan.GetTemplate(templateName); if (!template) return; var costMult = count; // Apply a time discount to larger batches. // TODO: work out what equation we should use here. var timeMult = Math.pow(count, 0.7); var time = timeMult * template.Cost.BuildTime; var costs = {}; for each (var r in ["food", "wood", "stone", "metal"]) costs[r] = Math.floor(costMult * template.Cost.Resources[r]); var population = template.Cost.Population * count; // TrySubtractResources should report error to player (they ran out of resources) if (!cmpPlayer.TrySubtractResources(costs)) return; this.queue.push({ "id": this.nextID++, "player": cmpPlayer.GetPlayerID(), "template": templateName, "count": count, "metadata": metadata, "resources": costs, "population": population, "trainingStarted": false, "timeTotal": time*1000, "timeRemaining": time*1000, }); Engine.PostMessage(this.entity, MT_TrainingQueueChanged, { }); // If this is the first item in the queue, start the timer if (!this.timer) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetTimeout(this.entity, IID_TrainingQueue, "ProgressTimeout", g_ProgressInterval, {}); } } else { var notification = {"player": cmpPlayer.GetPlayerID(), "message": "The training queue is full."}; var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification(notification); } }; TrainingQueue.prototype.RemoveBatch = function(id) { for (var i = 0; i < this.queue.length; ++i) { var item = this.queue[i]; if (item.id != id) continue; // Now we've found the item to remove var cmpPlayer = QueryPlayerIDInterface(item.player, IID_Player); // Refund the resource cost for this batch cmpPlayer.AddResources(item.resources); // Remove reserved population slots if necessary if (item.trainingStarted) cmpPlayer.UnReservePopulationSlots(item.population); // Remove from the queue // (We don't need to remove the timer - it'll expire if it discovers the queue is empty) this.queue.splice(i, 1); Engine.PostMessage(this.entity, MT_TrainingQueueChanged, { }); return; } }; TrainingQueue.prototype.GetQueue = function() { var out = []; for each (var item in this.queue) { out.push({ "id": item.id, "template": item.template, "count": item.count, "progress": 1-(item.timeRemaining/item.timeTotal), "metadata": item.metadata, }); } return out; }; TrainingQueue.prototype.ResetQueue = function() { // Empty the training queue and refund all the resource costs // to the player. (This is to avoid players having to micromanage their // buildings' queues when they're about to be destroyed or captured.) while (this.queue.length) this.RemoveBatch(this.queue[0].id); }; TrainingQueue.prototype.OnOwnershipChanged = function(msg) { if (msg.from != -1) { // Unset flag that previous owner's training queue may be blocked var cmpPlayer = QueryPlayerIDInterface(msg.from, IID_Player); if (cmpPlayer && this.queue.length > 0) cmpPlayer.UnBlockTrainingQueue(); } // Reset the training queue whenever the owner changes. // (This should prevent players getting surprised when they capture // an enemy building, and then loads of the enemy's civ's soldiers get // created from it. Also it means we don't have to worry about // updating the reserved pop slots.) this.ResetQueue(); }; TrainingQueue.prototype.OnDestroy = function() { // Reset the queue to refund any resources this.ResetQueue(); if (this.timer) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); } }; TrainingQueue.prototype.SpawnUnits = function(templateName, count, metadata) { var cmpFootprint = Engine.QueryInterface(this.entity, IID_Footprint); var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); var cmpRallyPoint = Engine.QueryInterface(this.entity, IID_RallyPoint); var ents = []; for (var i = 0; i < count; ++i) { var ent = Engine.AddEntity(templateName); var pos = cmpFootprint.PickSpawnPoint(ent); if (pos.y < 0) { // Whoops, something went wrong (maybe there wasn't any space to spawn the unit). // What should we do here? // For now, just move the unit into the middle of the building where it'll probably get stuck pos = cmpPosition.GetPosition(); - warn("Can't find free space to spawn trained unit"); + + var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); + var notification = {"player": cmpPlayer.GetPlayerID(), "message": "Can't find free space to spawn trained unit"}; + var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); + cmpGUIInterface.PushNotification(notification); } var cmpNewPosition = Engine.QueryInterface(ent, IID_Position); cmpNewPosition.JumpTo(pos.x, pos.z); // TODO: what direction should they face in? var cmpNewOwnership = Engine.QueryInterface(ent, IID_Ownership); cmpNewOwnership.SetOwner(cmpOwnership.GetOwner()); var cmpPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker); cmpPlayerStatisticsTracker.IncreaseTrainedUnitsCounter(); ents.push(ent); // Play a sound, but only for the first in the batch (to avoid nasty phasing effects) if (i == 0) PlaySound("trained", ent); } // If a rally point is set, walk towards it (in formation) if (cmpRallyPoint) { var rallyPos = cmpRallyPoint.GetPosition(); if (rallyPos) { ProcessCommand(cmpOwnership.GetOwner(), { "type": "walk", "entities": ents, "x": rallyPos.x, "z": rallyPos.z, "queued": false }); } } Engine.PostMessage(this.entity, MT_TrainingFinished, { "entities": ents, "owner": cmpOwnership.GetOwner(), "metadata": metadata, }); }; TrainingQueue.prototype.ProgressTimeout = function(data) { // Allocate the 1000msecs to as many queue items as it takes // until we've used up all the time (so that we work accurately // with items that take fractions of a second) var time = g_ProgressInterval; var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); while (time > 0 && this.queue.length) { var item = this.queue[0]; if (!item.trainingStarted) { // Batch's training hasn't started yet. // Try to reserve the necessary population slots if (!cmpPlayer.TryReservePopulationSlots(item.population)) { // No slots available - don't train this batch now // (we'll try again on the next timeout) // Set flag that training queue is blocked cmpPlayer.BlockTrainingQueue(); break; } // Unset flag that training queue is blocked cmpPlayer.UnBlockTrainingQueue(); item.trainingStarted = true; } // If we won't finish the batch now, just update its timer if (item.timeRemaining > time) { item.timeRemaining -= time; break; } // This item is finished now time -= item.timeRemaining; cmpPlayer.UnReservePopulationSlots(item.population); this.SpawnUnits(item.template, item.count, item.metadata); this.queue.shift(); Engine.PostMessage(this.entity, MT_TrainingQueueChanged, { }); } // If the queue's empty, delete the timer, else repeat it if (this.queue.length == 0) { this.timer = undefined; // Unset flag that training queue is blocked // (This might happen when the player unqueues all batches) cmpPlayer.UnBlockTrainingQueue(); } else { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetTimeout(this.entity, IID_TrainingQueue, "ProgressTimeout", g_ProgressInterval, data); } } Engine.RegisterComponentType(IID_TrainingQueue, "TrainingQueue", TrainingQueue); Index: ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js (revision 9969) +++ ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js (revision 9970) @@ -1,329 +1,333 @@ function GarrisonHolder() {} GarrisonHolder.prototype.Schema = "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + "" + "" + "" + "" + "" + ""; /** * Initialize GarrisonHolder Component */ GarrisonHolder.prototype.Init = function() { // Garrisoned Units this.entities = []; this.spaceOccupied = 0; this.timer = undefined; this.healRate = this.template.BuffHeal; }; /** * Return the list of entities garrisoned inside */ GarrisonHolder.prototype.GetEntities = function() { return this.entities; }; /** * Returns an array of unit classes which can be garrisoned inside this * particualar entity. Obtained from the entity's template */ GarrisonHolder.prototype.GetAllowedClassesList = function() { var string = this.template.List._string; return string.split(/\s+/); }; /** * Get Maximum pop which can be garrisoned */ GarrisonHolder.prototype.GetCapacity = function() { return this.template.Max; }; /** * Get number of garrisoned units capable of shooting arrows * Not necessarily archers */ GarrisonHolder.prototype.GetGarrisonedArcherCount = function() { var count = 0; for each (var entity in this.entities) { var cmpIdentity = Engine.QueryInterface(entity, IID_Identity); var classes = cmpIdentity.GetClassesList(); if (classes.indexOf("Infantry") != -1 || classes.indexOf("Ranged") != -1) count++; } return count; }; /** * Checks if an entity can be allowed to garrison in the building * based on its class */ GarrisonHolder.prototype.AllowedToGarrison = function(entity) { var allowedClasses = this.GetAllowedClassesList(); var entityClasses = (Engine.QueryInterface(entity, IID_Identity)).GetClassesList(); // Check if the unit is allowed to be garrisoned inside the building for each (var allowedClass in allowedClasses) { if (entityClasses.indexOf(allowedClass) != -1) { return true; } } return false; }; /** * Garrison a unit inside. * Returns true if successful, false if not * The timer for AutoHeal is started here */ GarrisonHolder.prototype.Garrison = function(entity) { var entityPopCost = (Engine.QueryInterface(entity, IID_Cost)).GetPopCost(); var entityClasses = (Engine.QueryInterface(entity, IID_Identity)).GetClassesList(); if (!this.HasEnoughHealth()) return false; // Check if the unit is allowed to be garrisoned inside the building if(!this.AllowedToGarrison(entity)) return false; if (this.GetCapacity() < this.spaceOccupied + 1) return false; if (!this.timer) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetTimeout(this.entity, IID_GarrisonHolder, "HealTimeout", 1000, {}); } var cmpPosition = Engine.QueryInterface(entity, IID_Position); if (cmpPosition) { // Actual garrisoning happens here this.entities.push(entity); this.spaceOccupied += 1; cmpPosition.MoveOutOfWorld(); this.UpdateGarrisonFlag(); return true; } return false; }; /** * Simply eject the unit from the garrisoning entity without * moving it */ GarrisonHolder.prototype.Eject = function(entity) { var entityIndex = this.entities.indexOf(entity); this.spaceOccupied -= 1; this.entities.splice(entityIndex, 1); var cmpUnitAI = Engine.QueryInterface(entity, IID_UnitAI); if (cmpUnitAI) { cmpUnitAI.Ungarrison(); } var cmpFootprint = Engine.QueryInterface(this.entity, IID_Footprint); var pos = cmpFootprint.PickSpawnPoint(entity); if (pos.y < 0) { // Whoops, something went wrong (maybe there wasn't any space to place the unit). // What should we do here? // For now, just move the unit into the middle of the building where it'll probably get stuck var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); pos = cmpPosition.GetPosition(); - warn("Can't find free space to ungarrison unit"); + + var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); + var notification = {"player": cmpPlayer.GetPlayerID(), "message": "Can't find free space to ungarrison unit"}; + var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); + cmpGUIInterface.PushNotification(notification); } var cmpNewPosition = Engine.QueryInterface(entity, IID_Position); cmpNewPosition.JumpTo(pos.x, pos.z); // TODO: what direction should they face in? }; /** * Order entities to walk to the Rally Point */ GarrisonHolder.prototype.OrderWalkToRallyPoint = function(entities) { var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); var cmpRallyPoint = Engine.QueryInterface(this.entity, IID_RallyPoint); if (cmpRallyPoint) { var rallyPos = cmpRallyPoint.GetPosition(); if (rallyPos) { ProcessCommand(cmpOwnership.GetOwner(), { "type": "walk", "entities": entities, "x": rallyPos.x, "z": rallyPos.z, "queued": false }); } } }; /** * Unload units from the garrisoning entity and order them * to move to the Rally Point */ GarrisonHolder.prototype.Unload = function(entity) { this.Eject(entity); this.OrderWalkToRallyPoint([entity]); this.UpdateGarrisonFlag(); }; /** * Unload all units from the entity */ GarrisonHolder.prototype.UnloadAll = function() { // The entities list is saved to a temporary variable // because during each loop an element is removed // from the list var entities = this.entities.splice(0); for each (var entity in entities) { this.Eject(entity); } this.OrderWalkToRallyPoint(entities); this.UpdateGarrisonFlag(); }; /** * Used to check if the garrisoning entity's health has fallen below * a certain limit after which all garrisoned units are unloaded */ GarrisonHolder.prototype.OnHealthChanged = function(msg) { if (!this.HasEnoughHealth()) { this.UnloadAll(); } }; /** * Check if this entity has enough health to garrison units inside it */ GarrisonHolder.prototype.HasEnoughHealth = function() { var cmpHealth = Engine.QueryInterface(this.entity, IID_Health) var hitpoints = cmpHealth.GetHitpoints(); var maxHitpoints = cmpHealth.GetMaxHitpoints(); var ejectHitpoints = parseInt(parseFloat(this.template.EjectHealth) * maxHitpoints); return hitpoints > ejectHitpoints; }; /** * Called every second. Heals garrisoned units */ GarrisonHolder.prototype.HealTimeout = function(data) { if (this.entities.length == 0) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); this.timer = undefined; } else { for each (var entity in this.entities) { var cmpHealth = Engine.QueryInterface(entity, IID_Health); if (cmpHealth) { if (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints()) cmpHealth.Increase(this.healRate); } } var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetTimeout(this.entity, IID_GarrisonHolder, "HealTimeout", 1000, {}); } }; GarrisonHolder.prototype.UpdateGarrisonFlag = function() { var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (!cmpVisual) return; cmpVisual.SelectAnimation("garrisoned", true, 0, ""); // TODO: ought to extend ICmpVisual to let us just select variant // keywords without changing the animation too if (this.entities.length) cmpVisual.SelectAnimation("garrisoned", false, 1.0, ""); else cmpVisual.SelectAnimation("idle", false, 1.0, ""); }; /** * Cancel timer when destroyed */ GarrisonHolder.prototype.OnDestroy = function() { if (this.timer) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); } }; /** * If a garrisoned entity is captured, or about to be killed (so its owner * changes to '-1'), remove it from the building so we only ever contain valid * entities */ GarrisonHolder.prototype.OnGlobalOwnershipChanged = function(msg) { if (this.entities.indexOf(msg.entity) != -1) { // If the entity is dead, remove it directly instead of ejecting the corpse var cmpHealth = Engine.QueryInterface(msg.entity, IID_Health); if (cmpHealth && cmpHealth.GetHitpoints() == 0) { this.entities.splice(this.entities.indexOf(msg.entity), 1); } else { // Otherwise the unit probably got captured somehow and we don't want it // any more, so eject it this.Eject(msg.entity); } } }; /** * Update list of garrisoned entities if one gets renamed (e.g. by promotion) */ GarrisonHolder.prototype.OnGlobalEntityRenamed = function(msg) { if (this.entities.indexOf(msg.entity) != -1) { this.entities[this.entities.indexOf(msg.entity)] = msg.newentity; } }; Engine.RegisterComponentType(IID_GarrisonHolder, "GarrisonHolder", GarrisonHolder); Index: ps/trunk/binaries/data/mods/public/simulation/components/BuildRestrictions.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/BuildRestrictions.js (revision 9969) +++ ps/trunk/binaries/data/mods/public/simulation/components/BuildRestrictions.js (revision 9970) @@ -1,74 +1,232 @@ function BuildRestrictions() {} BuildRestrictions.prototype.Schema = - "" + + "Specifies building placement restrictions as they relate to terrain, territories, and distance." + + "" + + "" + + "land" + + "own" + + "Special" + + "" + + "CivilCentre" + + "40" + + "" + + "" + + "" + + "" + "" + - "standard" + - "settlement" + - "" + // TODO: add special types for fields, docks, walls - "" + - "" + - "" + - "all" + - "allied" + + "land" + + "shore" + "" + "" + - "" + + "" + + "" + + "" + + "" + + "own" + + "ally" + + "neutral" + + "enemy" + + "" + + "" + + "" + + "" + + "" + "" + "CivilCentre" + "House" + "ScoutTower" + "Farmstead" + "Market" + "Barracks" + "Dock" + "Fortress" + "Field" + "Temple" + "Wall" + "Fence" + "Mill" + "Stoa" + "Resource" + "Special" + "" + - ""; + "" + + "" + + "" + + "" + + "" + + "" + + "CivilCentre" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; // TODO: add phases, prerequisites, etc -/* - * TODO: the vague plan for Category is to add some BuildLimitManager which - * specifies the limit per category, and which can determine whether you're - * allowed to build more (based on the number in total / per territory / per - * civ center as appropriate) - */ - -/* - * TODO: the vague plan for PlacementType is that it may restrict the locations - * and orientations of new construction work (e.g. civ centers must be on settlements, - * docks must be on shores), which affects the UI and the build permissions - */ +BuildRestrictions.prototype.Init = function() +{ + this.territories = this.template.Territory.split(/\s+/); +}; BuildRestrictions.prototype.OnOwnershipChanged = function(msg) { + // This automatically updates building counts if (this.template.Category) { - var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); if (msg.from != -1) { - var fromPlayerBuildLimits = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(msg.from), IID_BuildLimits); + var fromPlayerBuildLimits = QueryPlayerIDInterface(msg.from, IID_BuildLimits); fromPlayerBuildLimits.DecrementCount(this.template.Category); } if (msg.to != -1) { - var toPlayerBuildLimits = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(msg.to), IID_BuildLimits); + var toPlayerBuildLimits = QueryPlayerIDInterface(msg.to, IID_BuildLimits); toPlayerBuildLimits.IncrementCount(this.template.Category); } } }; +BuildRestrictions.prototype.CheckPlacement = function(player) +{ + // TODO: Return error code for invalid placement, which can be handled by the UI + + // Check obstructions and terrain passability + var passClassName = ""; + switch (this.template.PlacementType) + { + case "shore": + passClassName = "building-shore"; + break; + + case "land": + default: + passClassName = "building-land"; + } + + var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); + if (!cmpObstruction || !cmpObstruction.CheckFoundation(passClassName)) + { + return false; // Fail + } + + // Check territory restrictions + var cmpTerritoryManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager); + var cmpPlayer = QueryPlayerIDInterface(player, IID_Player); + var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); + if (!(cmpTerritoryManager && cmpPlayer && cmpPosition && cmpPosition.IsInWorld())) + { + return false; // Fail + } + + var pos = cmpPosition.GetPosition2D(); + var tileOwner = cmpTerritoryManager.GetOwner(pos.x, pos.y); + var isOwn = (tileOwner == player); + var isNeutral = (tileOwner == 0); + var isAlly = !isOwn && cmpPlayer.IsAlly(tileOwner); + var isEnemy = !isNeutral && cmpPlayer.IsEnemy(tileOwner); + + if ((isAlly && !this.HasTerritory("ally")) + || (isOwn && !this.HasTerritory("own")) + || (isNeutral && !this.HasTerritory("neutral")) + || (isEnemy && !this.HasTerritory("enemy"))) + { + return false; // Fail + } + + // Check special requirements + if (this.template.Category == "Dock") + { + // Dock must be oriented from land facing into water + var cmpFootprint = Engine.QueryInterface(this.entity, IID_Footprint); + if (!cmpFootprint) + { + return false; // Fail + } + + // Get building's footprint + var shape = cmpFootprint.GetShape(); + var halfSize = 0; + if (shape.type == "square") + { + halfSize = shape.depth/2; + } + else if (shape.type == "circle") + { + halfSize = shape.radius; + } + + var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); + var cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager); + if (!cmpTerrain || !cmpWaterManager) + { + return false; // Fail + } + + var ang = cmpPosition.GetRotation().y; + var sz = halfSize * Math.sin(ang); + var cz = halfSize * Math.cos(ang); + if ((cmpTerrain.GetGroundLevel(pos.x + sz, pos.y + cz) > cmpWaterManager.GetWaterLevel(pos.x + sz, pos.y + cz)) || // front + (cmpTerrain.GetGroundLevel(pos.x - sz, pos.y - cz) <= cmpWaterManager.GetWaterLevel(pos.x - sz, pos.y - cz))) // back + { + return false; // Fail + } + } + + // Check distance restriction + if (this.template.Distance) + { + var minDist = 65535; + var maxDist = 0; + var ents = Engine.GetEntitiesWithInterface(IID_BuildRestrictions); + for each (var ent in ents) + { + var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); + if (cmpBuildRestrictions.GetCategory() == this.template.Distance.FromCategory && IsOwnedByPlayer(player, ent)) + { + var cmpEntPosition = Engine.QueryInterface(ent, IID_Position); + if (cmpEntPosition && cmpEntPosition.IsInWorld()) + { + var entPos = cmpEntPosition.GetPosition2D(); + var dist = Math.sqrt((pos.x-entPos.x)*(pos.x-entPos.x) + (pos.y-entPos.y)*(pos.y-entPos.y)); + if (dist < minDist) + { + minDist = dist; + } + if (dist > maxDist) + { + maxDist = dist; + } + } + } + } + + if (this.template.Distance.MinDistance !== undefined && minDist < this.template.Distance.MinDistance + || this.template.Distance.MaxDistance !== undefined && maxDist > this.template.Distance.MaxDistance) + { + return false; // Fail + } + } + + // Success + return true; +}; + BuildRestrictions.prototype.GetCategory = function() { return this.template.Category; }; +BuildRestrictions.prototype.GetTerritories = function() +{ + return this.territories; +}; + +BuildRestrictions.prototype.HasTerritory = function(territory) +{ + return (this.territories.indexOf(territory) != -1); +}; + Engine.RegisterComponentType(IID_BuildRestrictions, "BuildRestrictions", BuildRestrictions); Index: ps/trunk/binaries/data/mods/public/simulation/components/BuildLimits.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/BuildLimits.js (revision 9969) +++ ps/trunk/binaries/data/mods/public/simulation/components/BuildLimits.js (revision 9970) @@ -1,63 +1,102 @@ function BuildLimits() {} BuildLimits.prototype.Schema = - "" + + "Specifies per category limits on number of buildings that can be constructed for each player." + + "" + + "" + + "" + + "20" + + "5" + + "" + + "1" + + "" + + "" + + "" + "" + "" + "" + "" + "" + - "" + + "" + "" + - "" + + "" + + "" + + "" + + "" + + "" + + "" + "" + "" + ""; +/* + * TODO: Use an inheriting player_{civ}.xml template for civ-specific limits + */ + BuildLimits.prototype.Init = function() { - this.limits = []; - this.unitCount = []; + this.limit = []; + this.count = []; for (var category in this.template.Limits) { - this.limits[category] = this.template.Limits[category]; - this.unitCount[category] = 0; + this.limit[category] = this.template.Limits[category]; + this.count[category] = 0; } }; BuildLimits.prototype.IncrementCount = function(category) { - if (this.unitCount[category] !== undefined) - this.unitCount[category]++; + if (this.count[category] !== undefined) + { + this.count[category]++; + } }; BuildLimits.prototype.DecrementCount = function(category) { - if (this.unitCount[category] !== undefined) - this.unitCount[category]--; + if (this.count[category] !== undefined) + { + this.count[category]--; + } }; BuildLimits.prototype.AllowedToBuild = function(category) { - if (this.unitCount[category]) + // TODO: The UI should reflect this before the user tries to place the building, + // since the limits are independent of placement location + + // Allow unspecified categories and those with no limit + if (this.count[category] === undefined || this.limit[category] === undefined) { - if (this.unitCount[category] >= this.limits[category]) + return true; + } + + // Rather than complicating the schema unecessarily, just handle special cases here + if (this.limit[category].LimitPerCivCentre !== undefined) + { + if (this.count[category] >= this.count["CivilCentre"] * this.limit[category].LimitPerCivCentre) { var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); - var notification = {"player": cmpPlayer.GetPlayerID(), "message": "Build limit reached for this building"}; + var notification = {"player": cmpPlayer.GetPlayerID(), "message": category+" build limit of "+this.limit[category].LimitPerCivCentre+" per civil centre reached"}; var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification(notification); + return false; } - else - return true; } - else + else if (this.count[category] >= this.limit[category]) { - //Check here for terrain - return true; + var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); + var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); + var notification = {"player": cmpPlayer.GetPlayerID(), "message": category+" build limit of "+this.limit[category]+ " reached"}; + var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); + cmpGUIInterface.PushNotification(notification); + + return false; } + + return true; }; -Engine.RegisterComponentType(IID_BuildLimits, "BuildLimits", BuildLimits); \ No newline at end of file +Engine.RegisterComponentType(IID_BuildLimits, "BuildLimits", BuildLimits); Index: ps/trunk/binaries/data/mods/public/simulation/data/pathfinder.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/pathfinder.xml (revision 9969) +++ ps/trunk/binaries/data/mods/public/simulation/data/pathfinder.xml (revision 9970) @@ -1,41 +1,51 @@ 64 2 1.0 1 - + 0 + 2.0 1.0 + + 3.0 + 1.5 + Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_mill.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_mill.xml (revision 9969) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_mill.xml (revision 9970) @@ -1,55 +1,54 @@ 10.0 40.0 15.0 Mill - all 80 100 8.0 800 Mill Dropsite for Wood, Stone, and Metal resources. Village structures/mill.png wood stone metal interface/complete/building/complete_mill.xml attack/destruction/building_collapse_large.xml 20 65536 20 structures/fndn_3x3.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/special/player.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/special/player.xml (revision 9969) +++ ps/trunk/binaries/data/mods/public/simulation/templates/special/player.xml (revision 9970) @@ -1,10 +1,13 @@ 1.0 + + 20 + 5 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml (revision 9969) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml (revision 9970) @@ -1,74 +1,77 @@ 20.0 40 10.0 - settlement - all + own neutral CivilCentre + + CivilCentre + 150 + 20 300 0 300 300 300 8.0 20 0.1 Support Infantry Cavalry 1 3000 Civic Centre Build to acquire large tracts of territory. Train citizens. Village Defensive CivCentre structures/civic_centre.png food wood stone metal interface/complete/building/complete_civ_center.xml attack/destruction/building_collapse_large.xml 180 65536 units/{civ}_support_female_citizen 90 structures/fndn_6x6.xml