Index: ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js (revision 24216) +++ ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js (revision 24217) @@ -1,388 +1,410 @@ // Number of rounds of firing per 2 seconds. const roundCount = 10; const attackType = "Ranged"; function BuildingAI() {} BuildingAI.prototype.Schema = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; BuildingAI.prototype.MAX_PREFERENCE_BONUS = 2; BuildingAI.prototype.Init = function() { this.currentRound = 0; this.archersGarrisoned = 0; this.arrowsLeft = 0; this.targetUnits = []; }; BuildingAI.prototype.OnGarrisonedUnitsChanged = function() { this.RecalculateProjectileCount(); }; BuildingAI.prototype.OnTurretsChanged = function() { this.RecalculateProjectileCount(); }; BuildingAI.prototype.RecalculateProjectileCount = function() { this.archersGarrisoned = 0; let classes = this.template.GarrisonArrowClasses; let cmpTurretHolder = Engine.QueryInterface(this.entity, IID_TurretHolder); let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); for (let ent of cmpGarrisonHolder.GetEntities()) { // Only count non-visible garrisoned entities towards extra arrows. if (cmpTurretHolder && cmpTurretHolder.OccupiesTurret(ent)) continue; let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (!cmpIdentity) continue; if (MatchesClassList(cmpIdentity.GetClassesList(), classes)) ++this.archersGarrisoned; } }; BuildingAI.prototype.OnOwnershipChanged = function(msg) { this.targetUnits = []; this.SetupRangeQuery(); this.SetupGaiaRangeQuery(); }; BuildingAI.prototype.OnDiplomacyChanged = function(msg) { if (!IsOwnedByPlayer(msg.player, this.entity)) return; // Remove maybe now allied/neutral units this.targetUnits = []; this.SetupRangeQuery(); this.SetupGaiaRangeQuery(); }; BuildingAI.prototype.OnDestroy = function() { if (this.timer) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); this.timer = undefined; } // Clean up range queries let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.enemyUnitsQuery) cmpRangeManager.DestroyActiveQuery(this.enemyUnitsQuery); if (this.gaiaUnitsQuery) cmpRangeManager.DestroyActiveQuery(this.gaiaUnitsQuery); }; /** * React on Attack value modifications, as it might influence the range */ BuildingAI.prototype.OnValueModification = function(msg) { if (msg.component != "Attack") return; this.targetUnits = []; this.SetupRangeQuery(); this.SetupGaiaRangeQuery(); }; /** * Setup the Range Query to detect units coming in & out of range */ BuildingAI.prototype.SetupRangeQuery = function() { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return; var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.enemyUnitsQuery) { cmpRangeManager.DestroyActiveQuery(this.enemyUnitsQuery); this.enemyUnitsQuery = undefined; } var cmpPlayer = QueryOwnerInterface(this.entity); if (!cmpPlayer) return; var enemies = cmpPlayer.GetEnemies(); if (enemies.length && enemies[0] == 0) enemies.shift(); // remove gaia if (!enemies.length) return; var range = cmpAttack.GetRange(attackType); this.enemyUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery( this.entity, range.min, range.max, range.elevationBonus, enemies, IID_Resistance, cmpRangeManager.GetEntityFlagMask("normal")); cmpRangeManager.EnableActiveQuery(this.enemyUnitsQuery); }; // Set up a range query for Gaia units within LOS range which can be attacked. // This should be called whenever our ownership changes. BuildingAI.prototype.SetupGaiaRangeQuery = function() { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return; var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.gaiaUnitsQuery) { cmpRangeManager.DestroyActiveQuery(this.gaiaUnitsQuery); this.gaiaUnitsQuery = undefined; } var cmpPlayer = QueryOwnerInterface(this.entity); if (!cmpPlayer || !cmpPlayer.IsEnemy(0)) return; var range = cmpAttack.GetRange(attackType); // This query is only interested in Gaia entities that can attack. this.gaiaUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery( this.entity, range.min, range.max, range.elevationBonus, [0], IID_Attack, cmpRangeManager.GetEntityFlagMask("normal")); cmpRangeManager.EnableActiveQuery(this.gaiaUnitsQuery); }; /** * Called when units enter or leave range */ BuildingAI.prototype.OnRangeUpdate = function(msg) { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return; // Target enemy units except non-dangerous animals if (msg.tag == this.gaiaUnitsQuery) { msg.added = msg.added.filter(e => { let cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI); return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal()); }); } else if (msg.tag != this.enemyUnitsQuery) return; // Add new targets for (let entity of msg.added) if (cmpAttack.CanAttack(entity)) this.targetUnits.push(entity); // Remove targets outside of vision-range for (let entity of msg.removed) { let index = this.targetUnits.indexOf(entity); if (index > -1) this.targetUnits.splice(index, 1); } if (this.targetUnits.length) this.StartTimer(); }; BuildingAI.prototype.StartTimer = function() { if (this.timer) return; var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return; var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); var attackTimers = cmpAttack.GetTimers(attackType); this.timer = cmpTimer.SetInterval(this.entity, IID_BuildingAI, "FireArrows", attackTimers.prepare, attackTimers.repeat / roundCount, null); }; BuildingAI.prototype.GetDefaultArrowCount = function() { var arrowCount = +this.template.DefaultArrowCount; return Math.round(ApplyValueModificationsToEntity("BuildingAI/DefaultArrowCount", arrowCount, this.entity)); }; BuildingAI.prototype.GetMaxArrowCount = function() { if (!this.template.MaxArrowCount) return Infinity; let maxArrowCount = +this.template.MaxArrowCount; return Math.round(ApplyValueModificationsToEntity("BuildingAI/MaxArrowCount", maxArrowCount, this.entity)); }; BuildingAI.prototype.GetGarrisonArrowMultiplier = function() { var arrowMult = +this.template.GarrisonArrowMultiplier; return ApplyValueModificationsToEntity("BuildingAI/GarrisonArrowMultiplier", arrowMult, this.entity); }; BuildingAI.prototype.GetGarrisonArrowClasses = function() { var string = this.template.GarrisonArrowClasses; if (string) return string.split(/\s+/); return []; }; /** * Returns the number of arrows which needs to be fired. * DefaultArrowCount + Garrisoned Archers(ie., any unit capable * of shooting arrows from inside buildings) */ BuildingAI.prototype.GetArrowCount = function() { let count = this.GetDefaultArrowCount() + Math.round(this.archersGarrisoned * this.GetGarrisonArrowMultiplier()); return Math.min(count, this.GetMaxArrowCount()); }; BuildingAI.prototype.SetUnitAITarget = function(ent) { this.unitAITarget = ent; if (ent) this.StartTimer(); }; /** * Fire arrows with random temporal distribution on prefered targets. * Called 'roundCount' times every 'RepeatTime' seconds when there are units in the range. */ BuildingAI.prototype.FireArrows = function() { if (!this.targetUnits.length && !this.unitAITarget) { if (!this.timer) return; let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); this.timer = undefined; return; } let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return; if (this.currentRound > roundCount - 1) this.currentRound = 0; if (this.currentRound == 0) this.arrowsLeft = this.GetArrowCount(); let arrowsToFire = 0; if (this.currentRound == roundCount - 1) arrowsToFire = this.arrowsLeft; else arrowsToFire = Math.min( randIntInclusive(0, 2 * this.GetArrowCount() / roundCount), this.arrowsLeft ); if (arrowsToFire <= 0) { ++this.currentRound; return; } // Add targets to a weighted list, to allow preferences let targets = new WeightedList(); let maxPreference = this.MAX_PREFERENCE_BONUS; let addTarget = function(target) { let preference = cmpAttack.GetPreference(target); let weight = 1; if (preference !== null && preference !== undefined) weight += maxPreference / (1 + preference); targets.push(target, weight); }; // Add the UnitAI target separately, as the UnitMotion and RangeManager implementations differ if (this.unitAITarget && this.targetUnits.indexOf(this.unitAITarget) == -1) addTarget(this.unitAITarget); for (let target of this.targetUnits) addTarget(target); + // The obstruction manager performs approximate range checks + // so we need to verify them here. + // TODO: perhaps an optional 'precise' mode to range queries would be more performant. + let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); + let range = cmpAttack.GetRange(attackType); + + let thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position); + if (!thisCmpPosition.IsInWorld()) + return; + let s = thisCmpPosition.GetPosition(); + for (let i = 0; i < arrowsToFire; ++i) { let selectedIndex = targets.randomIndex(); let selectedTarget = targets.itemAt(selectedIndex); - if (selectedTarget && this.CheckTargetVisible(selectedTarget)) + // Copied from UnitAI's MoveToTargetAttackRange. + let targetCmpPosition = Engine.QueryInterface(selectedTarget, IID_Position); + if (!targetCmpPosition.IsInWorld()) + continue; + + let t = targetCmpPosition.GetPosition(); + // h is positive when I'm higher than the target + let h = s.y - t.y + range.elevationBonus; + let parabolicMaxRange = Math.sqrt(Math.square(range.max) + 2 * range.max * h); + if (selectedTarget && this.CheckTargetVisible(selectedTarget) && + h > -range.max / 2 && cmpObstructionManager.IsInTargetRange( + this.entity, selectedTarget, range.min, parabolicMaxRange, false)) { cmpAttack.PerformAttack(attackType, selectedTarget); PlaySound("attack_" + attackType.toLowerCase(), this.entity); continue; } // Could not attack target, retry targets.removeAt(selectedIndex); --i; if (!targets.length()) { this.arrowsLeft += arrowsToFire; break; } } this.arrowsLeft -= arrowsToFire; this.currentRound++; }; /** * Returns true if the target entity is visible through the FoW/SoD. */ BuildingAI.prototype.CheckTargetVisible = function(target) { var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership) return false; // Entities that are hidden and miraged are considered visible var cmpFogging = Engine.QueryInterface(target, IID_Fogging); if (cmpFogging && cmpFogging.IsMiraged(cmpOwnership.GetOwner())) return true; // Either visible directly, or visible in fog let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); return cmpRangeManager.GetLosVisibility(target, cmpOwnership.GetOwner()) != "hidden"; }; Engine.RegisterComponentType(IID_BuildingAI, "BuildingAI", BuildingAI); Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber/monument.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber/monument.xml (revision 24216) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber/monument.xml (revision 24217) @@ -1,57 +1,57 @@ structures/iber_monument Monument Monument - 150 + 135 120 100 100 8.0 1200 decay|rubble/rubble_stone_2x2 iber Revered Monument Gur Oroigarri Monument structures/iberian_bull.png 20 20 interface/complete/building/complete_iber_monument.xml structures/iberians/sb_1.xml structures/fndn_2x2.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/pillar_ashoka.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/pillar_ashoka.xml (revision 24216) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur/pillar_ashoka.xml (revision 24217) @@ -1,57 +1,57 @@ structures/maur_pillar Pillar Pillar - 75 + 70 80 100 100 5.0 1000 decay|rubble/rubble_stone_2x2 maur Edict Pillar of Ashoka Śāsana Stambha Aśokā Pillar structures/ashoka_pillar.png 20 20 interface/complete/building/complete_iber_monument.xml props/structures/mauryas/ashoka_pillar.xml structures/fndn_2x2.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/merc_camp_egyptian.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/merc_camp_egyptian.xml (revision 24216) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/merc_camp_egyptian.xml (revision 24217) @@ -1,65 +1,65 @@ own neutral MercenaryCamp - 100 + 70 300 100 0 100 12.0 1200 decay|rubble/rubble_stone_5x5 ptol Egyptian Mercenary Camp Stratopedeia Misthophorōn MercenaryCamp Capture this structure to train mercenaries from Hellenistic Egypt. structures/military_settlement.png phase_town 20 0 20 units/{civ}/infantry_spearman_merc_b units/{civ}/infantry_swordsman_merc_b units/{civ}/cavalry_spearman_merc_b units/{civ}/cavalry_javelineer_merc_b interface/complete/building/complete_gymnasium.xml 1 structures/mercenaries/camp_egyptian.xml structures/fndn_7x7.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_gate.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_gate.xml (revision 24216) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/palisades_gate.xml (revision 24217) @@ -1,56 +1,56 @@ 10 10 7.0 - 20 + 2 0.5 Palisade Gate structures/palisades_gate Allow units access through Palisades. Can be locked to prevent access. Gate structures/wooden_gate.png 2 interface/complete/building/complete_gate.xml actor/gate/stonegate_close.xml actor/gate/stonegate_open.xml interface/select/building/sel_gate.xml interface/select/building/sel_gate.xml 8.0 structures/fndn_3x1.xml props/special/palisade_rocks_gate.xml 14 Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/mercenary_camp.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/mercenary_camp.xml (revision 24216) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol/mercenary_camp.xml (revision 24217) @@ -1,64 +1,64 @@ own neutral MercenaryCamp - 100 + 70 300 100 100 12.0 1200 decay|rubble/rubble_stone_5x5 ptol Mercenary Camp Stratopedeia Misthophorōn MercenaryCamp Cheap Barracks-like structure that is buildable in neutral territory, but casts no territory influence. Train Mercenaries. structures/mercenary_camp.png phase_town 20 0 20 units/{civ}/infantry_spearman_merc_b units/{civ}/infantry_swordsman_merc_b units/{civ}/cavalry_spearman_merc_b units/{civ}/cavalry_javelineer_merc_b interface/complete/building/complete_gymnasium.xml 1 structures/ptolemies/settlement.xml structures/fndn_7x7.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/army_camp.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/army_camp.xml (revision 24216) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome/army_camp.xml (revision 24217) @@ -1,139 +1,139 @@ Bow 0.0 25.0 0.0 60.0 0.0 1200 2000 0 75.0 1.5 9.81 false outline_border.png outline_border_mask.png 0.175 1 15 1 Soldier neutral enemy ArmyCamp ArmyCamp - 80 + 45 1500 10.0 3.0 5 250 500 12.0 40 Support Infantry Cavalry Siege 1 6 2500 decay|rubble/rubble_rome_sb rome Army Camp Castra Build in neutral or enemy territory. Train Citizen-Soldiers and construct Siege Engines. Garrison Soldiers for additional arrows. ConquestCritical ArmyCamp structures/roman_camp.png 100 100 0.7 units/{civ}/infantry_swordsman_b units/{civ}/infantry_spearman_a units/{civ}/infantry_javelineer_b units/{civ}/cavalry_spearman_b units/{civ}/siege_ballista_packed units/{civ}/siege_scorpio_packed units/{civ}/siege_oxybeles_packed units/{civ}/siege_lithobolos_packed units/{civ}/siege_ram units/{civ}/siege_tower 15 25 2 1 5 1 interface/complete/building/complete_broch.xml attack/weapon/bow_attack.xml attack/impact/arrow_impact.xml 37.5 60 structures/romans/camp.xml structures/fndn_8x8.xml 29.5 8 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 24216) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml (revision 24217) @@ -1,148 +1,148 @@ FemaleCitizen 140 190 100 Bow 0.0 12.0 0.0 72.0 0.0 1200 2000 0 75.0 1.5 9.81 false Human outline_border.png outline_border_mask.png 0.175 3 1 Soldier own neutral CivilCentre CivilCentre - 200 + 160 2500 5.0 20 500 500 500 500 8.0 20 0.1 Unit Support Infantry Cavalry 1 1 3000 decay|rubble/rubble_stone_6x6 Civic Center template_structure_civic_civil_centre Build in own or neutral territory. Acquire large tracts of territory. Territory root. Train Citizens and research technologies. Garrison Soldiers for additional arrows. CivCentre Defensive CivilCentre structures/civic_centre.png 200 100 100 100 0.8 units/{civ}/support_female_citizen phase_town_{civ} phase_city_{civ} unlock_spies spy_counter 5 5 5 15 3 food wood stone metal true interface/complete/building/complete_civ_center.xml interface/alarm/alarm_alert_0.xml interface/alarm/alarm_alert_1.xml attack/weapon/bow_attack.xml attack/impact/arrow_impact.xml true 140 10000 90 structures/fndn_8x8.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre_military_colony.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre_military_colony.xml (revision 24216) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre_military_colony.xml (revision 24217) @@ -1,54 +1,54 @@ 1 own neutral Colony CivilCentre - 120 + 80 300 200 200 200 2000 decay|rubble/rubble_stone_5x5 Military Colony template_structure_civic_civil_centre_military_colony Colony structures/military_settlement.png phase_town 40 40 40 -phase_town_{civ} -phase_city_{civ} upgrade_rank_advanced_mercenary interface/complete/building/complete_gymnasium.xml 80 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_outpost.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_outpost.xml (revision 24216) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_outpost.xml (revision 24217) @@ -1,82 +1,82 @@ structures/wall_garrisoned own neutral Outpost - 50 + 45 30 60 13.0 1 0.1 Unit Infantry 0 2 400 decay|rubble/rubble_stone_2x2 Outpost template_structure_defensive_outpost Build in own or neutral territory. Slowly converts to Gaia while in neutral territory. Outpost structures/outpost.png 12 10 20 1 interface/complete/building/complete_tower.xml 14.0 1 0 8 0 structures/fndn_2x2.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower.xml (revision 24216) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower.xml (revision 24217) @@ -1,73 +1,73 @@ Bow 0 0 0 1200 2000 0 75.0 1.5 39.81 false Human outline_border.png outline_border_mask.png 0.175 1 1 Infantry Tower Tower - 60 + 55 0.1 Unit Support Infantry 0 2 1000 decay|rubble/rubble_stone_2x2 Tower Tower interface/complete/building/complete_tower.xml attack/weapon/bow_attack.xml attack/impact/arrow_impact.xml 18.0 80 structures/fndn_3x3.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall_gate.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall_gate.xml (revision 24216) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall_gate.xml (revision 24217) @@ -1,52 +1,52 @@ structures/wall_garrisoned 30 12 18 - 20 + 2 2500 decay|rubble/rubble_stone_wall_long Gate template_structure_defensive_wall_gate Allow units access through Walls. Can be locked to prevent access. Gate structures/gate.png 2 4 interface/complete/building/complete_gate.xml interface/complete/building/complete_gate.xml actor/gate/stonegate_close.xml actor/gate/stonegate_open.xml interface/select/building/sel_gate.xml interface/select/building/sel_gate.xml structures/fndn_9x3_wall.xml 36 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_fortress.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_fortress.xml (revision 24216) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_fortress.xml (revision 24217) @@ -1,116 +1,116 @@ Bow 0.0 16.0 0.0 72.0 0.0 1200 2000 0 75.0 1.5 9.81 false Human outline_border.png outline_border_mask.png 0.175 3 1 Soldier Fortress Fortress - 80 + 55 4000 10.0 10 500 1000 8.0 20 0.075 Support Infantry Cavalry Siege 6 4200 decay|rubble/rubble_stone_6x6 Fortress template_structure_military_fortress Train Champions and Heroes and research technologies. Garrison Soldiers for additional arrows. GarrisonFortress Defensive City Fortress structures/fortress.png phase_city 100 200 0.8 attack_soldiers_will 5 5 3 interface/complete/building/complete_fortress.xml attack/weapon/bow_attack.xml attack/impact/arrow_impact.xml 100 80 structures/fndn_8x8.xml Index: ps/trunk/source/simulation2/components/CCmpRangeManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpRangeManager.cpp (revision 24216) +++ ps/trunk/source/simulation2/components/CCmpRangeManager.cpp (revision 24217) @@ -1,2459 +1,2468 @@ /* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "simulation2/system/Component.h" #include "ICmpRangeManager.h" #include "ICmpTerrain.h" #include "simulation2/system/EntityMap.h" #include "simulation2/MessageTypes.h" #include "simulation2/components/ICmpFogging.h" #include "simulation2/components/ICmpMirage.h" #include "simulation2/components/ICmpOwnership.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpObstructionManager.h" #include "simulation2/components/ICmpTerritoryManager.h" #include "simulation2/components/ICmpVisibility.h" #include "simulation2/components/ICmpVision.h" #include "simulation2/components/ICmpWaterManager.h" #include "simulation2/helpers/Los.h" #include "simulation2/helpers/MapEdgeTiles.h" #include "simulation2/helpers/Render.h" #include "simulation2/helpers/Spatial.h" #include "graphics/Overlay.h" #include "graphics/Terrain.h" #include "lib/timer.h" #include "ps/CLogger.h" #include "ps/Profile.h" #include "renderer/Scene.h" #define LOS_TILES_RATIO 8 #define DEBUG_RANGE_MANAGER_BOUNDS 0 /** * Convert an owner ID (-1 = unowned, 0 = gaia, 1..30 = players) * into a 32-bit mask for quick set-membership tests. */ static inline u32 CalcOwnerMask(player_id_t owner) { if (owner >= -1 && owner < 31) return 1 << (1+owner); else return 0; // owner was invalid } /** * Returns LOS mask for given player. */ static inline u32 CalcPlayerLosMask(player_id_t player) { if (player > 0 && player <= 16) return (u32)LosState::MASK << (2*(player-1)); return 0; } /** * Returns shared LOS mask for given list of players. */ static u32 CalcSharedLosMask(std::vector players) { u32 playerMask = 0; for (size_t i = 0; i < players.size(); i++) playerMask |= CalcPlayerLosMask(players[i]); return playerMask; } /** * Add/remove a player to/from mask, which is a 1-bit mask representing a list of players. * Returns true if the mask is modified. */ static bool SetPlayerSharedDirtyVisibilityBit(u16& mask, player_id_t player, bool enable) { if (player <= 0 || player > 16) return false; u16 oldMask = mask; if (enable) mask |= (0x1 << (player - 1)); else mask &= ~(0x1 << (player - 1)); return oldMask != mask; } /** * Computes the 2-bit visibility for one player, given the total 32-bit visibilities */ static inline LosVisibility GetPlayerVisibility(u32 visibilities, player_id_t player) { if (player > 0 && player <= 16) return static_cast( (visibilities >> (2 *(player-1))) & 0x3 ); return LosVisibility::HIDDEN; } /** * Test whether the visibility is dirty for a given LoS tile and a given player */ static inline bool IsVisibilityDirty(u16 dirty, player_id_t player) { if (player > 0 && player <= 16) return (dirty >> (player - 1)) & 0x1; return false; } /** * Test whether a player share this vision */ static inline bool HasVisionSharing(u16 visionSharing, player_id_t player) { return visionSharing & 1 << (player-1); } /** * Computes the shared vision mask for the player */ static inline u16 CalcVisionSharingMask(player_id_t player) { return 1 << (player-1); } /** * Representation of a range query. */ struct Query { - bool enabled; - bool parabolic; + std::vector lastMatch; CEntityHandle source; // TODO: this could crash if an entity is destroyed while a Query is still referencing it entity_pos_t minRange; entity_pos_t maxRange; - entity_pos_t elevationBonus; + entity_pos_t elevationBonus; // Used for parabolas only. u32 ownersMask; i32 interface; - std::vector lastMatch; u8 flagsMask; + bool enabled; + bool parabolic; }; /** * Checks whether v is in a parabolic range of (0,0,0) * The highest point of the paraboloid is (0,range/2,0) * and the circle of distance 'range' around (0,0,0) on height y=0 is part of the paraboloid + * This equates to computing f(x, z) = y = -(xx + zz)/(2*range) + range/2 > 0, + * or alternatively √(xx+zz) <= √(range^2 - 2range*y). * * Avoids sqrting and overflowing. */ static bool InParabolicRange(CFixedVector3D v, fixed range) { u64 xx = SQUARE_U64_FIXED(v.X); // xx <= 2^62 u64 zz = SQUARE_U64_FIXED(v.Z); i64 d2 = (xx + zz) >> 1; // d2 <= 2^62 (no overflow) i32 y = v.Y.GetInternalValue(); i32 c = range.GetInternalValue(); i32 c_2 = c >> 1; i64 c2 = MUL_I64_I32_I32(c_2 - y, c); return d2 <= c2; } struct EntityParabolicRangeOutline { entity_id_t source; CFixedVector3D position; entity_pos_t range; std::vector outline; }; static std::map ParabolicRangesOutlines; /** * Representation of an entity, with the data needed for queries. */ enum FlagMasks { // flags used for queries None = 0x00, Normal = 0x01, Injured = 0x02, AllQuery = Normal | Injured, // 0x04 reserved for future use // general flags InWorld = 0x08, RetainInFog = 0x10, RevealShore = 0x20, ScriptedVisibility = 0x40, SharedVision = 0x80 }; struct EntityData { EntityData() : visibilities(0), size(0), visionSharing(0), owner(-1), flags(FlagMasks::Normal) { } entity_pos_t x, z; entity_pos_t visionRange; u32 visibilities; // 2-bit visibility, per player u32 size; u16 visionSharing; // 1-bit per player i8 owner; u8 flags; // See the FlagMasks enum template inline bool HasFlag() const { return (flags & mask) != 0; } template inline void SetFlag(bool val) { flags = val ? (flags | mask) : (flags & ~mask); } inline void SetFlag(u8 mask, bool val) { flags = val ? (flags | mask) : (flags & ~mask); } }; cassert(sizeof(EntityData) == 24); /** * Serialization helper template for Query */ struct SerializeQuery { template void Common(S& serialize, const char* UNUSED(name), Query& value) { serialize.Bool("enabled", value.enabled); serialize.Bool("parabolic",value.parabolic); serialize.NumberFixed_Unbounded("min range", value.minRange); serialize.NumberFixed_Unbounded("max range", value.maxRange); serialize.NumberFixed_Unbounded("elevation bonus", value.elevationBonus); serialize.NumberU32_Unbounded("owners mask", value.ownersMask); serialize.NumberI32_Unbounded("interface", value.interface); SerializeVector()(serialize, "last match", value.lastMatch); serialize.NumberU8_Unbounded("flagsMask", value.flagsMask); } void operator()(ISerializer& serialize, const char* name, Query& value, const CSimContext& UNUSED(context)) { Common(serialize, name, value); uint32_t id = value.source.GetId(); serialize.NumberU32_Unbounded("source", id); } void operator()(IDeserializer& deserialize, const char* name, Query& value, const CSimContext& context) { Common(deserialize, name, value); uint32_t id; deserialize.NumberU32_Unbounded("source", id); value.source = context.GetComponentManager().LookupEntityHandle(id, true); // the referenced entity might not have been deserialized yet, // so tell LookupEntityHandle to allocate the handle if necessary } }; /** * Serialization helper template for EntityData */ struct SerializeEntityData { template void operator()(S& serialize, const char* UNUSED(name), EntityData& value) { serialize.NumberFixed_Unbounded("x", value.x); serialize.NumberFixed_Unbounded("z", value.z); serialize.NumberFixed_Unbounded("vision", value.visionRange); serialize.NumberU32_Unbounded("visibilities", value.visibilities); serialize.NumberU32_Unbounded("size", value.size); serialize.NumberU16_Unbounded("vision sharing", value.visionSharing); serialize.NumberI8_Unbounded("owner", value.owner); serialize.NumberU8_Unbounded("flags", value.flags); } }; /** * Functor for sorting entities by distance from a source point. * It must only be passed entities that are in 'entities' * and are currently in the world. */ struct EntityDistanceOrdering { EntityDistanceOrdering(const EntityMap& entities, const CFixedVector2D& source) : m_EntityData(entities), m_Source(source) { } bool operator()(entity_id_t a, entity_id_t b) const { const EntityData& da = m_EntityData.find(a)->second; const EntityData& db = m_EntityData.find(b)->second; CFixedVector2D vecA = CFixedVector2D(da.x, da.z) - m_Source; CFixedVector2D vecB = CFixedVector2D(db.x, db.z) - m_Source; return (vecA.CompareLength(vecB) < 0); } const EntityMap& m_EntityData; CFixedVector2D m_Source; private: EntityDistanceOrdering& operator=(const EntityDistanceOrdering&); }; /** * Range manager implementation. * Maintains a list of all entities (and their positions and owners), which is used for * queries. * * LOS implementation is based on the model described in GPG2. * (TODO: would be nice to make it cleverer, so e.g. mountains and walls * can block vision) */ class CCmpRangeManager : public ICmpRangeManager { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeGloballyToMessageType(MT_Create); componentManager.SubscribeGloballyToMessageType(MT_PositionChanged); componentManager.SubscribeGloballyToMessageType(MT_OwnershipChanged); componentManager.SubscribeGloballyToMessageType(MT_Destroy); componentManager.SubscribeGloballyToMessageType(MT_VisionRangeChanged); componentManager.SubscribeGloballyToMessageType(MT_VisionSharingChanged); componentManager.SubscribeToMessageType(MT_Deserialized); componentManager.SubscribeToMessageType(MT_Update); componentManager.SubscribeToMessageType(MT_RenderSubmit); // for debug overlays } DEFAULT_COMPONENT_ALLOCATOR(RangeManager) bool m_DebugOverlayEnabled; bool m_DebugOverlayDirty; std::vector m_DebugOverlayLines; // Deserialization flag. A lot of different functions are called by Deserialize() // and we don't want to pass isDeserializing bool arguments to all of them... bool m_Deserializing; // World bounds (entities are expected to be within this range) entity_pos_t m_WorldX0; entity_pos_t m_WorldZ0; entity_pos_t m_WorldX1; entity_pos_t m_WorldZ1; // Range query state: tag_t m_QueryNext; // next allocated id std::map m_Queries; EntityMap m_EntityData; FastSpatialSubdivision m_Subdivision; // spatial index of m_EntityData std::vector m_SubdivisionResults; // LOS state: static const player_id_t MAX_LOS_PLAYER_ID = 16; using LosTile = std::pair; std::array m_LosRevealAll; bool m_LosCircular; i32 m_TerrainVerticesPerSide; // Cache for visibility tracking i32 m_LosTilesPerSide; bool m_GlobalVisibilityUpdate; std::array m_GlobalPlayerVisibilityUpdate; Grid m_DirtyVisibility; Grid> m_LosTiles; // List of entities that must be updated, regardless of the status of their tile std::vector m_ModifiedEntities; // Counts of units seeing vertex, per vertex, per player (starting with player 0). // Use u16 to avoid overflows when we have very large (but not infeasibly large) numbers // of units in a very small area. // (Note we use vertexes, not tiles, to better match the renderer.) // Lazily constructed when it's needed, to save memory in smaller games. std::array, MAX_LOS_PLAYER_ID> m_LosPlayerCounts; // 2-bit LosState per player, starting with player 1 (not 0!) up to player MAX_LOS_PLAYER_ID (inclusive) Grid m_LosState; // Special static visibility data for the "reveal whole map" mode // (TODO: this is usually a waste of memory) Grid m_LosStateRevealed; // Shared LOS masks, one per player. std::array m_SharedLosMasks; // Shared dirty visibility masks, one per player. std::array m_SharedDirtyVisibilityMasks; // Cache explored vertices per player (not serialized) u32 m_TotalInworldVertices; std::vector m_ExploredVertices; static std::string GetSchema() { return ""; } virtual void Init(const CParamNode& UNUSED(paramNode)) { m_QueryNext = 1; m_DebugOverlayEnabled = false; m_DebugOverlayDirty = true; m_Deserializing = false; m_WorldX0 = m_WorldZ0 = m_WorldX1 = m_WorldZ1 = entity_pos_t::Zero(); // Initialise with bogus values (these will get replaced when // SetBounds is called) ResetSubdivisions(entity_pos_t::FromInt(1024), entity_pos_t::FromInt(1024)); m_SubdivisionResults.reserve(4096); // The whole map should be visible to Gaia by default, else e.g. animals // will get confused when trying to run from enemies m_LosRevealAll[0] = true; m_GlobalVisibilityUpdate = true; m_LosCircular = false; m_TerrainVerticesPerSide = 0; } virtual void Deinit() { } template void SerializeCommon(S& serialize) { serialize.NumberFixed_Unbounded("world x0", m_WorldX0); serialize.NumberFixed_Unbounded("world z0", m_WorldZ0); serialize.NumberFixed_Unbounded("world x1", m_WorldX1); serialize.NumberFixed_Unbounded("world z1", m_WorldZ1); serialize.NumberU32_Unbounded("query next", m_QueryNext); SerializeMap()(serialize, "queries", m_Queries, GetSimContext()); SerializeEntityMap()(serialize, "entity data", m_EntityData); SerializeArray()(serialize, "los reveal all", m_LosRevealAll); serialize.Bool("los circular", m_LosCircular); serialize.NumberI32_Unbounded("terrain verts per side", m_TerrainVerticesPerSide); serialize.Bool("global visibility update", m_GlobalVisibilityUpdate); SerializeArray()(serialize, "global player visibility update", m_GlobalPlayerVisibilityUpdate); SerializedGridCompressed()(serialize, "dirty visibility", m_DirtyVisibility); SerializeVector()(serialize, "modified entities", m_ModifiedEntities); // We don't serialize m_Subdivision, m_LosPlayerCounts or m_LosTiles // since they can be recomputed from the entity data when deserializing; // m_LosState must be serialized since it depends on the history of exploration SerializedGridCompressed()(serialize, "los state", m_LosState); SerializeArray()(serialize, "shared los masks", m_SharedLosMasks); SerializeArray()(serialize, "shared dirty visibility masks", m_SharedDirtyVisibilityMasks); } 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_Deserialized: { // Reinitialize subdivisions and LOS data after all // other components have been deserialized. m_Deserializing = true; ResetDerivedData(); m_Deserializing = false; break; } case MT_Create: { const CMessageCreate& msgData = static_cast (msg); entity_id_t ent = msgData.entity; // Ignore local entities - we shouldn't let them influence anything if (ENTITY_IS_LOCAL(ent)) break; // Ignore non-positional entities CmpPtr cmpPosition(GetSimContext(), ent); if (!cmpPosition) break; // The newly-created entity will have owner -1 and position out-of-world // (any initialisation of those values will happen later), so we can just // use the default-constructed EntityData here EntityData entdata; // Store the LOS data, if any CmpPtr cmpVision(GetSimContext(), ent); if (cmpVision) { entdata.visionRange = cmpVision->GetRange(); entdata.SetFlag(cmpVision->GetRevealShore()); } CmpPtr cmpVisibility(GetSimContext(), ent); if (cmpVisibility) entdata.SetFlag(cmpVisibility->GetRetainInFog()); // Store the size CmpPtr cmpObstruction(GetSimContext(), ent); if (cmpObstruction) entdata.size = cmpObstruction->GetSize().ToInt_RoundToInfinity(); // Remember this entity m_EntityData.insert(ent, entdata); break; } case MT_PositionChanged: { const CMessagePositionChanged& msgData = static_cast (msg); entity_id_t ent = msgData.entity; EntityMap::iterator it = m_EntityData.find(ent); // Ignore if we're not already tracking this entity if (it == m_EntityData.end()) break; if (msgData.inWorld) { if (it->second.HasFlag()) { CFixedVector2D from(it->second.x, it->second.z); CFixedVector2D to(msgData.x, msgData.z); m_Subdivision.Move(ent, from, to, it->second.size); if (it->second.HasFlag()) SharingLosMove(it->second.visionSharing, it->second.visionRange, from, to); else LosMove(it->second.owner, it->second.visionRange, from, to); LosTile oldLosTile = PosToLosTilesHelper(it->second.x, it->second.z); LosTile newLosTile = PosToLosTilesHelper(msgData.x, msgData.z); if (oldLosTile != newLosTile) { RemoveFromTile(oldLosTile, ent); AddToTile(newLosTile, ent); } } else { CFixedVector2D to(msgData.x, msgData.z); m_Subdivision.Add(ent, to, it->second.size); if (it->second.HasFlag()) SharingLosAdd(it->second.visionSharing, it->second.visionRange, to); else LosAdd(it->second.owner, it->second.visionRange, to); AddToTile(PosToLosTilesHelper(msgData.x, msgData.z), ent); } it->second.SetFlag(true); it->second.x = msgData.x; it->second.z = msgData.z; } else { if (it->second.HasFlag()) { CFixedVector2D from(it->second.x, it->second.z); m_Subdivision.Remove(ent, from, it->second.size); if (it->second.HasFlag()) SharingLosRemove(it->second.visionSharing, it->second.visionRange, from); else LosRemove(it->second.owner, it->second.visionRange, from); RemoveFromTile(PosToLosTilesHelper(it->second.x, it->second.z), ent); } it->second.SetFlag(false); it->second.x = entity_pos_t::Zero(); it->second.z = entity_pos_t::Zero(); } RequestVisibilityUpdate(ent); break; } case MT_OwnershipChanged: { const CMessageOwnershipChanged& msgData = static_cast (msg); entity_id_t ent = msgData.entity; EntityMap::iterator it = m_EntityData.find(ent); // Ignore if we're not already tracking this entity if (it == m_EntityData.end()) break; if (it->second.HasFlag()) { // Entity vision is taken into account in VisionSharingChanged // when sharing component activated if (!it->second.HasFlag()) { CFixedVector2D pos(it->second.x, it->second.z); LosRemove(it->second.owner, it->second.visionRange, pos); LosAdd(msgData.to, it->second.visionRange, pos); } if (it->second.HasFlag()) { RevealShore(it->second.owner, false); RevealShore(msgData.to, true); } } ENSURE(-128 <= msgData.to && msgData.to <= 127); it->second.owner = (i8)msgData.to; break; } case MT_Destroy: { const CMessageDestroy& msgData = static_cast (msg); entity_id_t ent = msgData.entity; EntityMap::iterator it = m_EntityData.find(ent); // Ignore if we're not already tracking this entity if (it == m_EntityData.end()) break; if (it->second.HasFlag()) { m_Subdivision.Remove(ent, CFixedVector2D(it->second.x, it->second.z), it->second.size); RemoveFromTile(PosToLosTilesHelper(it->second.x, it->second.z), ent); } // This will be called after Ownership's OnDestroy, so ownership will be set // to -1 already and we don't have to do a LosRemove here ENSURE(it->second.owner == -1); m_EntityData.erase(it); break; } case MT_VisionRangeChanged: { const CMessageVisionRangeChanged& msgData = static_cast (msg); entity_id_t ent = msgData.entity; EntityMap::iterator it = m_EntityData.find(ent); // Ignore if we're not already tracking this entity if (it == m_EntityData.end()) break; CmpPtr cmpVision(GetSimContext(), ent); if (!cmpVision) break; entity_pos_t oldRange = it->second.visionRange; entity_pos_t newRange = msgData.newRange; // If the range changed and the entity's in-world, we need to manually adjust it // but if it's not in-world, we only need to set the new vision range it->second.visionRange = newRange; if (it->second.HasFlag()) { CFixedVector2D pos(it->second.x, it->second.z); if (it->second.HasFlag()) { SharingLosRemove(it->second.visionSharing, oldRange, pos); SharingLosAdd(it->second.visionSharing, newRange, pos); } else { LosRemove(it->second.owner, oldRange, pos); LosAdd(it->second.owner, newRange, pos); } } break; } case MT_VisionSharingChanged: { const CMessageVisionSharingChanged& msgData = static_cast (msg); entity_id_t ent = msgData.entity; EntityMap::iterator it = m_EntityData.find(ent); // Ignore if we're not already tracking this entity if (it == m_EntityData.end()) break; ENSURE(msgData.player > 0 && msgData.player < MAX_LOS_PLAYER_ID+1); u16 visionChanged = CalcVisionSharingMask(msgData.player); if (!it->second.HasFlag()) { // Activation of the Vision Sharing ENSURE(it->second.owner == (i8)msgData.player); it->second.visionSharing = visionChanged; it->second.SetFlag(true); break; } if (it->second.HasFlag()) { entity_pos_t range = it->second.visionRange; CFixedVector2D pos(it->second.x, it->second.z); if (msgData.add) LosAdd(msgData.player, range, pos); else LosRemove(msgData.player, range, pos); } if (msgData.add) it->second.visionSharing |= visionChanged; else it->second.visionSharing &= ~visionChanged; break; } case MT_Update: { m_DebugOverlayDirty = true; ExecuteActiveQueries(); UpdateVisibilityData(); break; } case MT_RenderSubmit: { const CMessageRenderSubmit& msgData = static_cast (msg); RenderSubmit(msgData.collector); break; } } } virtual void SetBounds(entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, ssize_t vertices) { m_WorldX0 = x0; m_WorldZ0 = z0; m_WorldX1 = x1; m_WorldZ1 = z1; m_TerrainVerticesPerSide = (i32)vertices; ResetDerivedData(); } virtual void Verify() { // Ignore if map not initialised yet if (m_WorldX1.IsZero()) return; // Check that calling ResetDerivedData (i.e. recomputing all the state from scratch) // does not affect the incrementally-computed state std::array, MAX_LOS_PLAYER_ID> oldPlayerCounts = m_LosPlayerCounts; Grid oldStateRevealed = m_LosStateRevealed; FastSpatialSubdivision oldSubdivision = m_Subdivision; Grid > oldLosTiles = m_LosTiles; m_Deserializing = true; ResetDerivedData(); m_Deserializing = false; if (oldPlayerCounts != m_LosPlayerCounts) { for (size_t id = 0; id < m_LosPlayerCounts.size(); ++id) { debug_printf("player %li\n", id); for (size_t i = 0; i < oldPlayerCounts[id].width(); ++i) { for (size_t j = 0; j < oldPlayerCounts[id].height(); ++j) debug_printf("%i ", oldPlayerCounts[id].get(i,j)); debug_printf("\n"); } } for (size_t id = 0; id < m_LosPlayerCounts.size(); ++id) { debug_printf("player %li\n", id); for (size_t i = 0; i < m_LosPlayerCounts[id].width(); ++i) { for (size_t j = 0; j < m_LosPlayerCounts[id].height(); ++j) debug_printf("%i ", m_LosPlayerCounts[id].get(i,j)); debug_printf("\n"); } } debug_warn(L"inconsistent player counts"); } if (oldStateRevealed != m_LosStateRevealed) debug_warn(L"inconsistent revealed"); if (oldSubdivision != m_Subdivision) debug_warn(L"inconsistent subdivs"); if (oldLosTiles != m_LosTiles) debug_warn(L"inconsistent los tiles"); } FastSpatialSubdivision* GetSubdivision() { return &m_Subdivision; } // Reinitialise subdivisions and LOS data, based on entity data void ResetDerivedData() { ENSURE(m_WorldX0.IsZero() && m_WorldZ0.IsZero()); // don't bother implementing non-zero offsets yet ResetSubdivisions(m_WorldX1, m_WorldZ1); m_LosTilesPerSide = (m_TerrainVerticesPerSide - 1)/LOS_TILES_RATIO; for (size_t player_id = 0; player_id < m_LosPlayerCounts.size(); ++player_id) m_LosPlayerCounts[player_id].clear(); m_ExploredVertices.clear(); m_ExploredVertices.resize(MAX_LOS_PLAYER_ID+1, 0); if (m_Deserializing) { // recalc current exploration stats. for (i32 j = 0; j < m_TerrainVerticesPerSide; j++) for (i32 i = 0; i < m_TerrainVerticesPerSide; i++) if (!LosIsOffWorld(i, j)) for (u8 k = 1; k < MAX_LOS_PLAYER_ID+1; ++k) m_ExploredVertices.at(k) += ((m_LosState.get(i, j) & ((u32)LosState::EXPLORED << (2*(k-1)))) > 0); } else m_LosState.resize(m_TerrainVerticesPerSide, m_TerrainVerticesPerSide); m_LosStateRevealed.resize(m_TerrainVerticesPerSide, m_TerrainVerticesPerSide); if (!m_Deserializing) { m_DirtyVisibility.resize(m_LosTilesPerSide, m_LosTilesPerSide); } ENSURE(m_DirtyVisibility.width() == m_LosTilesPerSide); ENSURE(m_DirtyVisibility.height() == m_LosTilesPerSide); m_LosTiles.resize(m_LosTilesPerSide, m_LosTilesPerSide); for (EntityMap::const_iterator it = m_EntityData.begin(); it != m_EntityData.end(); ++it) if (it->second.HasFlag()) { if (it->second.HasFlag()) SharingLosAdd(it->second.visionSharing, it->second.visionRange, CFixedVector2D(it->second.x, it->second.z)); else LosAdd(it->second.owner, it->second.visionRange, CFixedVector2D(it->second.x, it->second.z)); AddToTile(PosToLosTilesHelper(it->second.x, it->second.z), it->first); if (it->second.HasFlag()) RevealShore(it->second.owner, true); } m_TotalInworldVertices = 0; for (ssize_t j = 0; j < m_TerrainVerticesPerSide; ++j) for (ssize_t i = 0; i < m_TerrainVerticesPerSide; ++i) { if (LosIsOffWorld(i,j)) m_LosStateRevealed.get(i, j) = 0; else { m_LosStateRevealed.get(i, j) = 0xFFFFFFFFu; m_TotalInworldVertices++; } } } void ResetSubdivisions(entity_pos_t x1, entity_pos_t z1) { m_Subdivision.Reset(x1, z1); for (EntityMap::const_iterator it = m_EntityData.begin(); it != m_EntityData.end(); ++it) if (it->second.HasFlag()) m_Subdivision.Add(it->first, CFixedVector2D(it->second.x, it->second.z), it->second.size); } virtual tag_t CreateActiveQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, const std::vector& owners, int requiredInterface, u8 flags) { tag_t id = m_QueryNext++; m_Queries[id] = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, flags); return id; } virtual tag_t CreateActiveParabolicQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t elevationBonus, const std::vector& owners, int requiredInterface, u8 flags) { tag_t id = m_QueryNext++; m_Queries[id] = ConstructParabolicQuery(source, minRange, maxRange, elevationBonus, owners, requiredInterface, flags); return id; } virtual void DestroyActiveQuery(tag_t tag) { if (m_Queries.find(tag) == m_Queries.end()) { LOGERROR("CCmpRangeManager: DestroyActiveQuery called with invalid tag %u", tag); return; } m_Queries.erase(tag); } virtual void EnableActiveQuery(tag_t tag) { std::map::iterator it = m_Queries.find(tag); if (it == m_Queries.end()) { LOGERROR("CCmpRangeManager: EnableActiveQuery called with invalid tag %u", tag); return; } Query& q = it->second; q.enabled = true; } virtual void DisableActiveQuery(tag_t tag) { std::map::iterator it = m_Queries.find(tag); if (it == m_Queries.end()) { LOGERROR("CCmpRangeManager: DisableActiveQuery called with invalid tag %u", tag); return; } Query& q = it->second; q.enabled = false; } virtual bool IsActiveQueryEnabled(tag_t tag) const { std::map::const_iterator it = m_Queries.find(tag); if (it == m_Queries.end()) { LOGERROR("CCmpRangeManager: IsActiveQueryEnabled called with invalid tag %u", tag); return false; } const Query& q = it->second; return q.enabled; } virtual std::vector ExecuteQueryAroundPos(const CFixedVector2D& pos, entity_pos_t minRange, entity_pos_t maxRange, const std::vector& owners, int requiredInterface) { Query q = ConstructQuery(INVALID_ENTITY, minRange, maxRange, owners, requiredInterface, GetEntityFlagMask("normal")); std::vector r; PerformQuery(q, r, pos); // Return the list sorted by distance from the entity std::stable_sort(r.begin(), r.end(), EntityDistanceOrdering(m_EntityData, pos)); return r; } virtual std::vector ExecuteQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, const std::vector& owners, int requiredInterface) { PROFILE("ExecuteQuery"); Query q = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, GetEntityFlagMask("normal")); std::vector r; CmpPtr cmpSourcePosition(q.source); if (!cmpSourcePosition || !cmpSourcePosition->IsInWorld()) { // If the source doesn't have a position, then the result is just the empty list return r; } CFixedVector2D pos = cmpSourcePosition->GetPosition2D(); PerformQuery(q, r, pos); // Return the list sorted by distance from the entity std::stable_sort(r.begin(), r.end(), EntityDistanceOrdering(m_EntityData, pos)); return r; } virtual std::vector ResetActiveQuery(tag_t tag) { PROFILE("ResetActiveQuery"); std::vector r; std::map::iterator it = m_Queries.find(tag); if (it == m_Queries.end()) { LOGERROR("CCmpRangeManager: ResetActiveQuery called with invalid tag %u", tag); return r; } Query& q = it->second; q.enabled = true; CmpPtr cmpSourcePosition(q.source); if (!cmpSourcePosition || !cmpSourcePosition->IsInWorld()) { // If the source doesn't have a position, then the result is just the empty list q.lastMatch = r; return r; } CFixedVector2D pos = cmpSourcePosition->GetPosition2D(); PerformQuery(q, r, pos); q.lastMatch = r; // Return the list sorted by distance from the entity std::stable_sort(r.begin(), r.end(), EntityDistanceOrdering(m_EntityData, pos)); return r; } virtual std::vector GetEntitiesByPlayer(player_id_t player) const { return GetEntitiesByMask(CalcOwnerMask(player)); } virtual std::vector GetNonGaiaEntities() const { return GetEntitiesByMask(~3); // bit 0 for owner=-1 and bit 1 for gaia } virtual std::vector GetGaiaAndNonGaiaEntities() const { return GetEntitiesByMask(~1); // bit 0 for owner=-1 } std::vector GetEntitiesByMask(u32 ownerMask) const { std::vector entities; for (EntityMap::const_iterator it = m_EntityData.begin(); it != m_EntityData.end(); ++it) { // Check owner and add to list if it matches if (CalcOwnerMask(it->second.owner) & ownerMask) entities.push_back(it->first); } return entities; } virtual void SetDebugOverlay(bool enabled) { m_DebugOverlayEnabled = enabled; m_DebugOverlayDirty = true; if (!enabled) m_DebugOverlayLines.clear(); } /** * Update all currently-enabled active queries. */ void ExecuteActiveQueries() { PROFILE3("ExecuteActiveQueries"); // Store a queue of all messages before sending any, so we can assume // no entities will move until we've finished checking all the ranges std::vector > messages; std::vector results; std::vector added; std::vector removed; for (std::map::iterator it = m_Queries.begin(); it != m_Queries.end(); ++it) { Query& query = it->second; if (!query.enabled) continue; results.clear(); CmpPtr cmpSourcePosition(query.source); if (cmpSourcePosition && cmpSourcePosition->IsInWorld()) { results.reserve(query.lastMatch.size()); PerformQuery(query, results, cmpSourcePosition->GetPosition2D()); } // Compute the changes vs the last match added.clear(); removed.clear(); // Return the 'added' list sorted by distance from the entity // (Don't bother sorting 'removed' because they might not even have positions or exist any more) std::set_difference(results.begin(), results.end(), query.lastMatch.begin(), query.lastMatch.end(), std::back_inserter(added)); std::set_difference(query.lastMatch.begin(), query.lastMatch.end(), results.begin(), results.end(), std::back_inserter(removed)); if (added.empty() && removed.empty()) continue; if (cmpSourcePosition && cmpSourcePosition->IsInWorld()) std::stable_sort(added.begin(), added.end(), EntityDistanceOrdering(m_EntityData, cmpSourcePosition->GetPosition2D())); messages.resize(messages.size() + 1); std::pair& back = messages.back(); back.first = query.source.GetId(); back.second.tag = it->first; back.second.added.swap(added); back.second.removed.swap(removed); query.lastMatch.swap(results); } CComponentManager& cmpMgr = GetSimContext().GetComponentManager(); for (size_t i = 0; i < messages.size(); ++i) cmpMgr.PostMessage(messages[i].first, messages[i].second); } /** * Returns whether the given entity matches the given query (ignoring maxRange) */ bool TestEntityQuery(const Query& q, entity_id_t id, const EntityData& entity) const { // Quick filter to ignore entities with the wrong owner if (!(CalcOwnerMask(entity.owner) & q.ownersMask)) return false; // Ignore entities not present in the world if (!entity.HasFlag()) return false; // Ignore entities that don't match the current flags if (!((entity.flags & FlagMasks::AllQuery) & q.flagsMask)) return false; // Ignore self if (id == q.source.GetId()) return false; // Ignore if it's missing the required interface if (q.interface && !GetSimContext().GetComponentManager().QueryInterface(id, q.interface)) return false; return true; } /** * Returns a list of distinct entity IDs that match the given query, sorted by ID. */ void PerformQuery(const Query& q, std::vector& r, CFixedVector2D pos) { // Special case: range -1.0 means check all entities ignoring distance if (q.maxRange == entity_pos_t::FromInt(-1)) { for (EntityMap::const_iterator it = m_EntityData.begin(); it != m_EntityData.end(); ++it) { if (!TestEntityQuery(q, it->first, it->second)) continue; r.push_back(it->first); } } // Not the entire world, so check a parabolic range, or a regular range else if (q.parabolic) { // elevationBonus is part of the 3D position, as the source is really that much heigher CmpPtr cmpSourcePosition(q.source); CFixedVector3D pos3d = cmpSourcePosition->GetPosition()+ CFixedVector3D(entity_pos_t::Zero(), q.elevationBonus, entity_pos_t::Zero()) ; // Get a quick list of entities that are potentially in range, with a cutoff of 2*maxRange m_SubdivisionResults.clear(); m_Subdivision.GetNear(m_SubdivisionResults, pos, q.maxRange * 2); for (size_t i = 0; i < m_SubdivisionResults.size(); ++i) { EntityMap::const_iterator it = m_EntityData.find(m_SubdivisionResults[i]); ENSURE(it != m_EntityData.end()); if (!TestEntityQuery(q, it->first, it->second)) continue; CmpPtr cmpSecondPosition(GetSimContext(), m_SubdivisionResults[i]); if (!cmpSecondPosition || !cmpSecondPosition->IsInWorld()) continue; CFixedVector3D secondPosition = cmpSecondPosition->GetPosition(); - // Restrict based on precise distance - if (!InParabolicRange( - CFixedVector3D(it->second.x, secondPosition.Y, it->second.z) - - pos3d, - q.maxRange)) + // Doing an exact check for parabolas with obstruction sizes is not really possible. + // However, we can prove that InParabolicRange(d, range + size) > InParabolicRange(d, range) + // in the sense that it always returns true when the latter would, which is enough. + // To do so, compute the derivative with respect to distance, and notice that + // they have an intersection after which the former grows slower, and then use that to prove the above. + // Note that this is only true because we do not account for vertical size here, + // if we did, we would also need to artificially 'raise' the source over the target. + if (!InParabolicRange(CFixedVector3D(it->second.x, secondPosition.Y, it->second.z) - pos3d, + q.maxRange + fixed::FromInt(it->second.size))) continue; if (!q.minRange.IsZero()) - { - int distVsMin = (CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.minRange); - if (distVsMin < 0) + if ((CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.minRange) < 0) continue; - } r.push_back(it->first); } std::sort(r.begin(), r.end()); } // check a regular range (i.e. not the entire world, and not parabolic) else { // Get a quick list of entities that are potentially in range m_SubdivisionResults.clear(); m_Subdivision.GetNear(m_SubdivisionResults, pos, q.maxRange); for (size_t i = 0; i < m_SubdivisionResults.size(); ++i) { EntityMap::const_iterator it = m_EntityData.find(m_SubdivisionResults[i]); ENSURE(it != m_EntityData.end()); if (!TestEntityQuery(q, it->first, it->second)) continue; - // Restrict based on precise distance - int distVsMax = (CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.maxRange); - if (distVsMax > 0) + // Restrict based on approximate circle-circle distance. + if ((CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.maxRange + fixed::FromInt(it->second.size)) > 0) continue; if (!q.minRange.IsZero()) - { - int distVsMin = (CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.minRange); - if (distVsMin < 0) + if ((CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.minRange) < 0) continue; - } r.push_back(it->first); } std::sort(r.begin(), r.end()); } } virtual entity_pos_t GetElevationAdaptedRange(const CFixedVector3D& pos1, const CFixedVector3D& rot, entity_pos_t range, entity_pos_t elevationBonus, entity_pos_t angle) const { entity_pos_t r = entity_pos_t::Zero(); CFixedVector3D pos(pos1); pos.Y += elevationBonus; entity_pos_t orientation = rot.Y; entity_pos_t maxAngle = orientation + angle/2; entity_pos_t minAngle = orientation - angle/2; int numberOfSteps = 16; if (angle == entity_pos_t::Zero()) numberOfSteps = 1; std::vector coords = getParabolicRangeForm(pos, range, range*2, minAngle, maxAngle, numberOfSteps); entity_pos_t part = entity_pos_t::FromInt(numberOfSteps); for (int i = 0; i < numberOfSteps; ++i) r = r + CFixedVector2D(coords[2*i],coords[2*i+1]).Length() / part; return r; } virtual std::vector getParabolicRangeForm(CFixedVector3D pos, entity_pos_t maxRange, entity_pos_t cutoff, entity_pos_t minAngle, entity_pos_t maxAngle, int numberOfSteps) const { std::vector r; CmpPtr cmpTerrain(GetSystemEntity()); if (!cmpTerrain) return r; // angle = 0 goes in the positive Z direction u64 precisionSquared = SQUARE_U64_FIXED(entity_pos_t::FromInt(static_cast(TERRAIN_TILE_SIZE)) / 8); CmpPtr cmpWaterManager(GetSystemEntity()); entity_pos_t waterLevel = cmpWaterManager ? cmpWaterManager->GetWaterLevel(pos.X, pos.Z) : entity_pos_t::Zero(); entity_pos_t thisHeight = pos.Y > waterLevel ? pos.Y : waterLevel; for (int i = 0; i < numberOfSteps; ++i) { entity_pos_t angle = minAngle + (maxAngle - minAngle) / numberOfSteps * i; entity_pos_t sin; entity_pos_t cos; entity_pos_t minDistance = entity_pos_t::Zero(); entity_pos_t maxDistance = cutoff; sincos_approx(angle, sin, cos); CFixedVector2D minVector = CFixedVector2D(entity_pos_t::Zero(), entity_pos_t::Zero()); CFixedVector2D maxVector = CFixedVector2D(sin, cos).Multiply(cutoff); entity_pos_t targetHeight = cmpTerrain->GetGroundLevel(pos.X+maxVector.X, pos.Z+maxVector.Y); // use water level to display range on water targetHeight = targetHeight > waterLevel ? targetHeight : waterLevel; if (InParabolicRange(CFixedVector3D(maxVector.X, targetHeight-thisHeight, maxVector.Y), maxRange)) { r.push_back(maxVector.X); r.push_back(maxVector.Y); continue; } // Loop until vectors come close enough while ((maxVector - minVector).CompareLengthSquared(precisionSquared) > 0) { // difference still bigger than precision, bisect to get smaller difference entity_pos_t newDistance = (minDistance+maxDistance)/entity_pos_t::FromInt(2); CFixedVector2D newVector = CFixedVector2D(sin, cos).Multiply(newDistance); // get the height of the ground targetHeight = cmpTerrain->GetGroundLevel(pos.X+newVector.X, pos.Z+newVector.Y); targetHeight = targetHeight > waterLevel ? targetHeight : waterLevel; if (InParabolicRange(CFixedVector3D(newVector.X, targetHeight-thisHeight, newVector.Y), maxRange)) { // new vector is in parabolic range, so this is a new minVector minVector = newVector; minDistance = newDistance; } else { // new vector is out parabolic range, so this is a new maxVector maxVector = newVector; maxDistance = newDistance; } } r.push_back(maxVector.X); r.push_back(maxVector.Y); } r.push_back(r[0]); r.push_back(r[1]); return r; } Query ConstructQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, const std::vector& owners, int requiredInterface, u8 flagsMask) const { // Min range must be non-negative if (minRange < entity_pos_t::Zero()) LOGWARNING("CCmpRangeManager: Invalid min range %f in query for entity %u", minRange.ToDouble(), source); // Max range must be non-negative, or else -1 if (maxRange < entity_pos_t::Zero() && maxRange != entity_pos_t::FromInt(-1)) LOGWARNING("CCmpRangeManager: Invalid max range %f in query for entity %u", maxRange.ToDouble(), source); Query q; q.enabled = false; q.parabolic = false; q.source = GetSimContext().GetComponentManager().LookupEntityHandle(source); q.minRange = minRange; q.maxRange = maxRange; q.elevationBonus = entity_pos_t::Zero(); + if (q.source.GetId() != INVALID_ENTITY && q.maxRange != entity_pos_t::FromInt(-1)) + { + EntityMap::const_iterator it = m_EntityData.find(q.source.GetId()); + ENSURE(it != m_EntityData.end()); + // Adjust the range query based on the querier's obstruction radius. + // The smallest side of the obstruction isn't known here, so we can't safely adjust the min-range, only the max. + // 'size' is the diagonal size rounded up so this will cover all possible rotations of the querier. + q.maxRange += fixed::FromInt(it->second.size); + } + q.ownersMask = 0; for (size_t i = 0; i < owners.size(); ++i) q.ownersMask |= CalcOwnerMask(owners[i]); if (q.ownersMask == 0) LOGWARNING("CCmpRangeManager: No owners in query for entity %u", source); q.interface = requiredInterface; q.flagsMask = flagsMask; return q; } Query ConstructParabolicQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t elevationBonus, const std::vector& owners, int requiredInterface, u8 flagsMask) const { Query q = ConstructQuery(source,minRange,maxRange,owners,requiredInterface,flagsMask); q.parabolic = true; q.elevationBonus = elevationBonus; return q; } void RenderSubmit(SceneCollector& collector) { if (!m_DebugOverlayEnabled) return; static CColor disabledRingColor(1, 0, 0, 1); // red static CColor enabledRingColor(0, 1, 0, 1); // green static CColor subdivColor(0, 0, 1, 1); // blue static CColor rayColor(1, 1, 0, 0.2f); if (m_DebugOverlayDirty) { m_DebugOverlayLines.clear(); for (std::map::iterator it = m_Queries.begin(); it != m_Queries.end(); ++it) { Query& q = it->second; CmpPtr cmpSourcePosition(q.source); if (!cmpSourcePosition || !cmpSourcePosition->IsInWorld()) continue; CFixedVector2D pos = cmpSourcePosition->GetPosition2D(); // Draw the max range circle if (!q.parabolic) { m_DebugOverlayLines.push_back(SOverlayLine()); m_DebugOverlayLines.back().m_Color = (q.enabled ? enabledRingColor : disabledRingColor); SimRender::ConstructCircleOnGround(GetSimContext(), pos.X.ToFloat(), pos.Y.ToFloat(), q.maxRange.ToFloat(), m_DebugOverlayLines.back(), true); } else { // elevation bonus is part of the 3D position. As if the unit is really that much higher CFixedVector3D pos = cmpSourcePosition->GetPosition(); pos.Y += q.elevationBonus; std::vector coords; // Get the outline from cache if possible if (ParabolicRangesOutlines.find(q.source.GetId()) != ParabolicRangesOutlines.end()) { EntityParabolicRangeOutline e = ParabolicRangesOutlines[q.source.GetId()]; if (e.position == pos && e.range == q.maxRange) { // outline is cached correctly, use it coords = e.outline; } else { // outline was cached, but important parameters changed // (position, elevation, range) // update it coords = getParabolicRangeForm(pos,q.maxRange,q.maxRange*2, entity_pos_t::Zero(), entity_pos_t::FromFloat(2.0f*3.14f),70); e.outline = coords; e.range = q.maxRange; e.position = pos; ParabolicRangesOutlines[q.source.GetId()] = e; } } else { // outline wasn't cached (first time you enable the range overlay // or you created a new entiy) // cache a new outline coords = getParabolicRangeForm(pos,q.maxRange,q.maxRange*2, entity_pos_t::Zero(), entity_pos_t::FromFloat(2.0f*3.14f),70); EntityParabolicRangeOutline e; e.source = q.source.GetId(); e.range = q.maxRange; e.position = pos; e.outline = coords; ParabolicRangesOutlines[q.source.GetId()] = e; } CColor thiscolor = q.enabled ? enabledRingColor : disabledRingColor; // draw the outline (piece by piece) for (size_t i = 3; i < coords.size(); i += 2) { std::vector c; c.push_back((coords[i-3]+pos.X).ToFloat()); c.push_back((coords[i-2]+pos.Z).ToFloat()); c.push_back((coords[i-1]+pos.X).ToFloat()); c.push_back((coords[i]+pos.Z).ToFloat()); m_DebugOverlayLines.push_back(SOverlayLine()); m_DebugOverlayLines.back().m_Color = thiscolor; SimRender::ConstructLineOnGround(GetSimContext(), c, m_DebugOverlayLines.back(), true); } } // Draw the min range circle if (!q.minRange.IsZero()) SimRender::ConstructCircleOnGround(GetSimContext(), pos.X.ToFloat(), pos.Y.ToFloat(), q.minRange.ToFloat(), m_DebugOverlayLines.back(), true); // Draw a ray from the source to each matched entity for (size_t i = 0; i < q.lastMatch.size(); ++i) { CmpPtr cmpTargetPosition(GetSimContext(), q.lastMatch[i]); if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld()) continue; CFixedVector2D targetPos = cmpTargetPosition->GetPosition2D(); std::vector coords; coords.push_back(pos.X.ToFloat()); coords.push_back(pos.Y.ToFloat()); coords.push_back(targetPos.X.ToFloat()); coords.push_back(targetPos.Y.ToFloat()); m_DebugOverlayLines.push_back(SOverlayLine()); m_DebugOverlayLines.back().m_Color = rayColor; SimRender::ConstructLineOnGround(GetSimContext(), coords, m_DebugOverlayLines.back(), true); } } // render subdivision grid float divSize = m_Subdivision.GetDivisionSize(); int size = m_Subdivision.GetWidth(); for (int x = 0; x < size; ++x) { for (int y = 0; y < size; ++y) { m_DebugOverlayLines.push_back(SOverlayLine()); m_DebugOverlayLines.back().m_Color = subdivColor; float xpos = x*divSize + divSize/2; float zpos = y*divSize + divSize/2; SimRender::ConstructSquareOnGround(GetSimContext(), xpos, zpos, divSize, divSize, 0.0f, m_DebugOverlayLines.back(), false, 1.0f); } } m_DebugOverlayDirty = false; } for (size_t i = 0; i < m_DebugOverlayLines.size(); ++i) collector.Submit(&m_DebugOverlayLines[i]); } virtual u8 GetEntityFlagMask(const std::string& identifier) const { if (identifier == "normal") return FlagMasks::Normal; if (identifier == "injured") return FlagMasks::Injured; LOGWARNING("CCmpRangeManager: Invalid flag identifier %s", identifier.c_str()); return FlagMasks::None; } virtual void SetEntityFlag(entity_id_t ent, const std::string& identifier, bool value) { EntityMap::iterator it = m_EntityData.find(ent); // We don't have this entity if (it == m_EntityData.end()) return; u8 flag = GetEntityFlagMask(identifier); if (flag == FlagMasks::None) LOGWARNING("CCmpRangeManager: Invalid flag identifier %s for entity %u", identifier.c_str(), ent); else it->second.SetFlag(flag, value); } // **************************************************************** // LOS implementation: virtual CLosQuerier GetLosQuerier(player_id_t player) const { if (GetLosRevealAll(player)) return CLosQuerier(0xFFFFFFFFu, m_LosStateRevealed, m_TerrainVerticesPerSide); else return CLosQuerier(GetSharedLosMask(player), m_LosState, m_TerrainVerticesPerSide); } virtual void ActivateScriptedVisibility(entity_id_t ent, bool status) { EntityMap::iterator it = m_EntityData.find(ent); if (it != m_EntityData.end()) it->second.SetFlag(status); } LosVisibility ComputeLosVisibility(CEntityHandle ent, player_id_t player) const { // Entities not with positions in the world are never visible if (ent.GetId() == INVALID_ENTITY) return LosVisibility::HIDDEN; CmpPtr cmpPosition(ent); if (!cmpPosition || !cmpPosition->IsInWorld()) return LosVisibility::HIDDEN; // Mirage entities, whatever the situation, are visible for one specific player CmpPtr cmpMirage(ent); if (cmpMirage && cmpMirage->GetPlayer() != player) return LosVisibility::HIDDEN; CFixedVector2D pos = cmpPosition->GetPosition2D(); int i = (pos.X / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); int j = (pos.Y / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); // Reveal flag makes all positioned entities visible and all mirages useless if (GetLosRevealAll(player)) { if (LosIsOffWorld(i, j) || cmpMirage) return LosVisibility::HIDDEN; return LosVisibility::VISIBLE; } // Get visible regions CLosQuerier los(GetSharedLosMask(player), m_LosState, m_TerrainVerticesPerSide); CmpPtr cmpVisibility(ent); // Possibly ask the scripted Visibility component EntityMap::const_iterator it = m_EntityData.find(ent.GetId()); if (it != m_EntityData.end()) { if (it->second.HasFlag() && cmpVisibility) return cmpVisibility->GetVisibility(player, los.IsVisible(i, j), los.IsExplored(i, j)); } else { if (cmpVisibility && cmpVisibility->IsActivated()) return cmpVisibility->GetVisibility(player, los.IsVisible(i, j), los.IsExplored(i, j)); } // Else, default behavior if (los.IsVisible(i, j)) { if (cmpMirage) return LosVisibility::HIDDEN; return LosVisibility::VISIBLE; } if (!los.IsExplored(i, j)) return LosVisibility::HIDDEN; // Invisible if the 'retain in fog' flag is not set, and in a non-visible explored region // Try using the 'retainInFog' flag in m_EntityData to save a script call if (it != m_EntityData.end()) { if (!it->second.HasFlag()) return LosVisibility::HIDDEN; } else { if (!(cmpVisibility && cmpVisibility->GetRetainInFog())) return LosVisibility::HIDDEN; } if (cmpMirage) return LosVisibility::FOGGED; CmpPtr cmpOwnership(ent); if (!cmpOwnership) return LosVisibility::FOGGED; if (cmpOwnership->GetOwner() == player) { CmpPtr cmpFogging(ent); if (!(cmpFogging && cmpFogging->IsMiraged(player))) return LosVisibility::FOGGED; return LosVisibility::HIDDEN; } // Fogged entities are hidden in two cases: // - They were not scouted // - A mirage replaces them CmpPtr cmpFogging(ent); if (cmpFogging && cmpFogging->IsActivated() && (!cmpFogging->WasSeen(player) || cmpFogging->IsMiraged(player))) return LosVisibility::HIDDEN; return LosVisibility::FOGGED; } LosVisibility ComputeLosVisibility(entity_id_t ent, player_id_t player) const { CEntityHandle handle = GetSimContext().GetComponentManager().LookupEntityHandle(ent); return ComputeLosVisibility(handle, player); } virtual LosVisibility GetLosVisibility(CEntityHandle ent, player_id_t player) const { entity_id_t entId = ent.GetId(); // Entities not with positions in the world are never visible if (entId == INVALID_ENTITY) return LosVisibility::HIDDEN; CmpPtr cmpPosition(ent); if (!cmpPosition || !cmpPosition->IsInWorld()) return LosVisibility::HIDDEN; // Gaia and observers do not have a visibility cache if (player <= 0) return ComputeLosVisibility(ent, player); CFixedVector2D pos = cmpPosition->GetPosition2D(); if (IsVisibilityDirty(m_DirtyVisibility[PosToLosTilesHelper(pos.X, pos.Y)], player)) return ComputeLosVisibility(ent, player); if (std::find(m_ModifiedEntities.begin(), m_ModifiedEntities.end(), entId) != m_ModifiedEntities.end()) return ComputeLosVisibility(ent, player); EntityMap::const_iterator it = m_EntityData.find(entId); if (it == m_EntityData.end()) return ComputeLosVisibility(ent, player); return static_cast(GetPlayerVisibility(it->second.visibilities, player)); } virtual LosVisibility GetLosVisibility(entity_id_t ent, player_id_t player) const { CEntityHandle handle = GetSimContext().GetComponentManager().LookupEntityHandle(ent); return GetLosVisibility(handle, player); } virtual LosVisibility GetLosVisibilityPosition(entity_pos_t x, entity_pos_t z, player_id_t player) const { int i = (x / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); int j = (z / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); // Reveal flag makes all positioned entities visible and all mirages useless if (GetLosRevealAll(player)) { if (LosIsOffWorld(i, j)) return LosVisibility::HIDDEN; else return LosVisibility::VISIBLE; } // Get visible regions CLosQuerier los(GetSharedLosMask(player), m_LosState, m_TerrainVerticesPerSide); if (los.IsVisible(i,j)) return LosVisibility::VISIBLE; if (los.IsExplored(i,j)) return LosVisibility::FOGGED; return LosVisibility::HIDDEN; } LosTile PosToLosTilesHelper(u16 x, u16 z) const { return LosTile{ Clamp(x/LOS_TILES_RATIO, 0, m_LosTilesPerSide - 1), Clamp(z/LOS_TILES_RATIO, 0, m_LosTilesPerSide - 1) }; } LosTile PosToLosTilesHelper(entity_pos_t x, entity_pos_t z) const { i32 i = Clamp( (x/(entity_pos_t::FromInt(TERRAIN_TILE_SIZE * LOS_TILES_RATIO))).ToInt_RoundToZero(), 0, m_LosTilesPerSide - 1); i32 j = Clamp( (z/(entity_pos_t::FromInt(TERRAIN_TILE_SIZE * LOS_TILES_RATIO))).ToInt_RoundToZero(), 0, m_LosTilesPerSide - 1); return std::make_pair(i, j); } void AddToTile(LosTile tile, entity_id_t ent) { m_LosTiles[tile].insert(ent); } void RemoveFromTile(LosTile tile, entity_id_t ent) { std::set::const_iterator tileIt = m_LosTiles[tile].find(ent); if (tileIt != m_LosTiles[tile].end()) m_LosTiles[tile].erase(tileIt); } void UpdateVisibilityData() { PROFILE("UpdateVisibilityData"); for (u16 i = 0; i < m_LosTilesPerSide; ++i) for (u16 j = 0; j < m_LosTilesPerSide; ++j) { LosTile pos{i, j}; for (player_id_t player = 1; player < MAX_LOS_PLAYER_ID + 1; ++player) if (IsVisibilityDirty(m_DirtyVisibility[pos], player) || m_GlobalPlayerVisibilityUpdate[player-1] == 1 || m_GlobalVisibilityUpdate) for (const entity_id_t& ent : m_LosTiles[pos]) UpdateVisibility(ent, player); m_DirtyVisibility[pos] = 0; } std::fill(m_GlobalPlayerVisibilityUpdate.begin(), m_GlobalPlayerVisibilityUpdate.end(), false); m_GlobalVisibilityUpdate = false; // Calling UpdateVisibility can modify m_ModifiedEntities, so be careful: // infinite loops could be triggered by feedback between entities and their mirages. std::map attempts; while (!m_ModifiedEntities.empty()) { entity_id_t ent = m_ModifiedEntities.back(); m_ModifiedEntities.pop_back(); ++attempts[ent]; ENSURE(attempts[ent] < 100 && "Infinite loop in UpdateVisibilityData"); UpdateVisibility(ent); } } virtual void RequestVisibilityUpdate(entity_id_t ent) { if (std::find(m_ModifiedEntities.begin(), m_ModifiedEntities.end(), ent) == m_ModifiedEntities.end()) m_ModifiedEntities.push_back(ent); } void UpdateVisibility(entity_id_t ent, player_id_t player) { EntityMap::iterator itEnts = m_EntityData.find(ent); if (itEnts == m_EntityData.end()) return; LosVisibility oldVis = GetPlayerVisibility(itEnts->second.visibilities, player); LosVisibility newVis = ComputeLosVisibility(itEnts->first, player); if (oldVis == newVis) return; itEnts->second.visibilities = (itEnts->second.visibilities & ~(0x3 << 2 * (player - 1))) | ((u8)newVis << 2 * (player - 1)); CMessageVisibilityChanged msg(player, ent, static_cast(oldVis), static_cast(newVis)); GetSimContext().GetComponentManager().PostMessage(ent, msg); } void UpdateVisibility(entity_id_t ent) { for (player_id_t player = 1; player < MAX_LOS_PLAYER_ID + 1; ++player) UpdateVisibility(ent, player); } virtual void SetLosRevealAll(player_id_t player, bool enabled) { if (player == -1) m_LosRevealAll[MAX_LOS_PLAYER_ID+1] = enabled; else { ENSURE(player >= 0 && player <= MAX_LOS_PLAYER_ID); m_LosRevealAll[player] = enabled; } // On next update, update the visibility of every entity in the world m_GlobalVisibilityUpdate = true; } virtual bool GetLosRevealAll(player_id_t player) const { // Special player value can force reveal-all for every player if (m_LosRevealAll[MAX_LOS_PLAYER_ID+1] || player == -1) return true; ENSURE(player >= 0 && player <= MAX_LOS_PLAYER_ID+1); // Otherwise check the player-specific flag if (m_LosRevealAll[player]) return true; return false; } virtual void SetLosCircular(bool enabled) { m_LosCircular = enabled; ResetDerivedData(); } virtual bool GetLosCircular() const { return m_LosCircular; } virtual void SetSharedLos(player_id_t player, const std::vector& players) { m_SharedLosMasks[player] = CalcSharedLosMask(players); // Units belonging to any of 'players' can now trigger visibility updates for 'player'. // If shared LOS partners have been removed, we disable visibility updates from them // in order to improve performance. That also allows us to properly determine whether // 'player' needs a global visibility update for this turn. bool modified = false; for (player_id_t p = 1; p < MAX_LOS_PLAYER_ID+1; ++p) { bool inList = std::find(players.begin(), players.end(), p) != players.end(); if (SetPlayerSharedDirtyVisibilityBit(m_SharedDirtyVisibilityMasks[p], player, inList)) modified = true; } if (modified && (size_t)player <= m_GlobalPlayerVisibilityUpdate.size()) m_GlobalPlayerVisibilityUpdate[player-1] = 1; } virtual u32 GetSharedLosMask(player_id_t player) const { return m_SharedLosMasks[player]; } void ExploreAllTiles(player_id_t p) { for (u16 j = 0; j < m_TerrainVerticesPerSide; ++j) for (u16 i = 0; i < m_TerrainVerticesPerSide; ++i) { if (LosIsOffWorld(i,j)) continue; u32 &explored = m_ExploredVertices.at(p); explored += !(m_LosState.get(i, j) & ((u32)LosState::EXPLORED << (2*(p-1)))); m_LosState.get(i, j) |= ((u32)LosState::EXPLORED << (2*(p-1))); } SeeExploredEntities(p); } virtual void ExploreTerritories() { PROFILE3("ExploreTerritories"); CmpPtr cmpTerritoryManager(GetSystemEntity()); const Grid& grid = cmpTerritoryManager->GetTerritoryGrid(); // Territory data is stored per territory-tile (typically a multiple of terrain-tiles). // LOS data is stored per terrain-tile vertex. // For each territory-tile, if it is owned by a valid player then update the LOS // for every vertex inside/around that tile, to mark them as explored. // Currently this code doesn't support territory-tiles smaller than terrain-tiles // (it will get scale==0 and break), or a non-integer multiple, so check that first cassert(ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE >= Pathfinding::NAVCELLS_PER_TILE); cassert(ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE % Pathfinding::NAVCELLS_PER_TILE == 0); int scale = ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE / Pathfinding::NAVCELLS_PER_TILE; ENSURE(grid.m_W*scale == m_TerrainVerticesPerSide-1 && grid.m_H*scale == m_TerrainVerticesPerSide-1); for (u16 j = 0; j < grid.m_H; ++j) for (u16 i = 0; i < grid.m_W; ++i) { u8 p = grid.get(i, j) & ICmpTerritoryManager::TERRITORY_PLAYER_MASK; if (p > 0 && p <= MAX_LOS_PLAYER_ID) { u32& explored = m_ExploredVertices.at(p); for (int tj = j * scale; tj <= (j+1) * scale; ++tj) for (int ti = i * scale; ti <= (i+1) * scale; ++ti) { if (LosIsOffWorld(ti, tj)) continue; u32& losState = m_LosState.get(ti, tj); if (!(losState & ((u32)LosState::EXPLORED << (2*(p-1))))) { ++explored; losState |= ((u32)LosState::EXPLORED << (2*(p-1))); } } } } for (player_id_t p = 1; p < MAX_LOS_PLAYER_ID+1; ++p) SeeExploredEntities(p); } /** * Force any entity in explored territory to appear for player p. * This is useful for miraging entities inside the territory borders at the beginning of a game, * or if the "Explore Map" option has been set. */ void SeeExploredEntities(player_id_t p) const { // Warning: Code related to fogging (like ForceMiraging) shouldn't be // invoked while iterating through m_EntityData. // Otherwise, by deleting mirage entities and so on, that code will // change the indexes in the map, leading to segfaults. // So we just remember what entities to mirage and do that later. std::vector miragableEntities; for (EntityMap::const_iterator it = m_EntityData.begin(); it != m_EntityData.end(); ++it) { CmpPtr cmpPosition(GetSimContext(), it->first); if (!cmpPosition || !cmpPosition->IsInWorld()) continue; CFixedVector2D pos = cmpPosition->GetPosition2D(); int i = (pos.X / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); int j = (pos.Y / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); CLosQuerier los(GetSharedLosMask(p), m_LosState, m_TerrainVerticesPerSide); if (!los.IsExplored(i,j) || los.IsVisible(i,j)) continue; CmpPtr cmpFogging(GetSimContext(), it->first); if (cmpFogging) miragableEntities.push_back(it->first); } for (std::vector::iterator it = miragableEntities.begin(); it != miragableEntities.end(); ++it) { CmpPtr cmpFogging(GetSimContext(), *it); ENSURE(cmpFogging && "Impossible to retrieve Fogging component, previously achieved"); cmpFogging->ForceMiraging(p); } } virtual void RevealShore(player_id_t p, bool enable) { if (p <= 0 || p > MAX_LOS_PLAYER_ID) return; // Maximum distance to the shore const u16 maxdist = 10; CmpPtr cmpPathfinder(GetSystemEntity()); const Grid& shoreGrid = cmpPathfinder->ComputeShoreGrid(true); ENSURE(shoreGrid.m_W == m_TerrainVerticesPerSide-1 && shoreGrid.m_H == m_TerrainVerticesPerSide-1); Grid& counts = m_LosPlayerCounts.at(p); ENSURE(!counts.blank()); for (u16 j = 0; j < shoreGrid.m_H; ++j) for (u16 i = 0; i < shoreGrid.m_W; ++i) { u16 shoredist = shoreGrid.get(i, j); if (shoredist > maxdist) continue; // Maybe we could be more clever and don't add dummy strips of one tile if (enable) LosAddStripHelper(p, i, i, j, counts); else LosRemoveStripHelper(p, i, i, j, counts); } } /** * Returns whether the given vertex is outside the normal bounds of the world * (i.e. outside the range of a circular map) */ inline bool LosIsOffWorld(ssize_t i, ssize_t j) const { if (m_LosCircular) { // With a circular map, vertex is off-world if hypot(i - size/2, j - size/2) >= size/2: ssize_t dist2 = (i - m_TerrainVerticesPerSide/2)*(i - m_TerrainVerticesPerSide/2) + (j - m_TerrainVerticesPerSide/2)*(j - m_TerrainVerticesPerSide/2); ssize_t r = m_TerrainVerticesPerSide / 2 - MAP_EDGE_TILES + 1; // subtract a bit from the radius to ensure nice // SoD blurring around the edges of the map return (dist2 >= r*r); } else { // With a square map, the outermost edge of the map should be off-world, // so the SoD texture blends out nicely return i < MAP_EDGE_TILES || j < MAP_EDGE_TILES || i >= m_TerrainVerticesPerSide - MAP_EDGE_TILES || j >= m_TerrainVerticesPerSide - MAP_EDGE_TILES; } } /** * Update the LOS state of tiles within a given horizontal strip (i0,j) to (i1,j) (inclusive). */ inline void LosAddStripHelper(u8 owner, i32 i0, i32 i1, i32 j, Grid& counts) { if (i1 < i0) return; u32 &explored = m_ExploredVertices.at(owner); for (i32 i = i0; i <= i1; ++i) { // Increasing from zero to non-zero - move from unexplored/explored to visible+explored if (counts.get(i, j) == 0) { if (!LosIsOffWorld(i, j)) { explored += !(m_LosState.get(i, j) & ((u32)LosState::EXPLORED << (2*(owner-1)))); m_LosState.get(i, j) |= (((int)LosState::VISIBLE | (u32)LosState::EXPLORED) << (2*(owner-1))); } MarkVisibilityDirtyAroundTile(owner, i, j); } ENSURE(counts.get(i, j) < std::numeric_limits::max()); counts.get(i, j) = (u16)(counts.get(i, j) + 1); // ignore overflow; the player should never have 64K units } } /** * Update the LOS state of tiles within a given horizontal strip (i0,j) to (i1,j) (inclusive). */ inline void LosRemoveStripHelper(u8 owner, i32 i0, i32 i1, i32 j, Grid& counts) { if (i1 < i0) return; for (i32 i = i0; i <= i1; ++i) { ASSERT(counts.get(i, j) > 0); counts.get(i, j) = (u16)(counts.get(i, j) - 1); // Decreasing from non-zero to zero - move from visible+explored to explored if (counts.get(i, j) == 0) { // (If LosIsOffWorld then this is a no-op, so don't bother doing the check) m_LosState.get(i, j) &= ~((int)LosState::VISIBLE << (2*(owner-1))); MarkVisibilityDirtyAroundTile(owner, i, j); } } } inline void MarkVisibilityDirtyAroundTile(u8 owner, i32 i, i32 j) { // If we're still in the deserializing process, we must not modify m_DirtyVisibility if (m_Deserializing) return; // Mark the LoS tiles around the updated vertex // 1: left-up, 2: right-up, 3: left-down, 4: right-down LosTile n1 = PosToLosTilesHelper(i-1, j-1); LosTile n2 = PosToLosTilesHelper(i-1, j); LosTile n3 = PosToLosTilesHelper(i, j-1); LosTile n4 = PosToLosTilesHelper(i, j); u16 sharedDirtyVisibilityMask = m_SharedDirtyVisibilityMasks[owner]; if (j > 0 && i > 0) m_DirtyVisibility[n1] |= sharedDirtyVisibilityMask; if (n2 != n1 && j > 0 && i < m_TerrainVerticesPerSide) m_DirtyVisibility[n2] |= sharedDirtyVisibilityMask; if (n3 != n1 && j < m_TerrainVerticesPerSide && i > 0) m_DirtyVisibility[n3] |= sharedDirtyVisibilityMask; if (n4 != n1 && j < m_TerrainVerticesPerSide && i < m_TerrainVerticesPerSide) m_DirtyVisibility[n4] |= sharedDirtyVisibilityMask; } /** * Update the LOS state of tiles within a given circular range, * either adding or removing visibility depending on the template parameter. * Assumes owner is in the valid range. */ template void LosUpdateHelper(u8 owner, entity_pos_t visionRange, CFixedVector2D pos) { if (m_TerrainVerticesPerSide == 0) // do nothing if not initialised yet return; PROFILE("LosUpdateHelper"); Grid& counts = m_LosPlayerCounts.at(owner); // Lazy initialisation of counts: if (counts.blank()) counts.resize(m_TerrainVerticesPerSide, m_TerrainVerticesPerSide); // Compute the circular region as a series of strips. // Rather than quantise pos to vertexes, we do more precise sub-tile computations // to get smoother behaviour as a unit moves rather than jumping a whole tile // at once. // To avoid the cost of sqrt when computing the outline of the circle, // we loop from the bottom to the top and estimate the width of the current // strip based on the previous strip, then adjust each end of the strip // inwards or outwards until it's the widest that still falls within the circle. // Compute top/bottom coordinates, and clamp to exclude the 1-tile border around the map // (so that we never render the sharp edge of the map) i32 j0 = ((pos.Y - visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToInfinity(); i32 j1 = ((pos.Y + visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToNegInfinity(); i32 j0clamp = std::max(j0, 1); i32 j1clamp = std::min(j1, m_TerrainVerticesPerSide-2); // Translate world coordinates into fractional tile-space coordinates entity_pos_t x = pos.X / (int)TERRAIN_TILE_SIZE; entity_pos_t y = pos.Y / (int)TERRAIN_TILE_SIZE; entity_pos_t r = visionRange / (int)TERRAIN_TILE_SIZE; entity_pos_t r2 = r.Square(); // Compute the integers on either side of x i32 xfloor = (x - entity_pos_t::Epsilon()).ToInt_RoundToNegInfinity(); i32 xceil = (x + entity_pos_t::Epsilon()).ToInt_RoundToInfinity(); // Initialise the strip (i0, i1) to a rough guess i32 i0 = xfloor; i32 i1 = xceil; for (i32 j = j0clamp; j <= j1clamp; ++j) { // Adjust i0 and i1 to be the outermost values that don't exceed // the circle's radius (i.e. require dy^2 + dx^2 <= r^2). // When moving the points inwards, clamp them to xceil+1 or xfloor-1 // so they don't accidentally shoot off in the wrong direction forever. entity_pos_t dy = entity_pos_t::FromInt(j) - y; entity_pos_t dy2 = dy.Square(); while (dy2 + (entity_pos_t::FromInt(i0-1) - x).Square() <= r2) --i0; while (i0 < xceil && dy2 + (entity_pos_t::FromInt(i0) - x).Square() > r2) ++i0; while (dy2 + (entity_pos_t::FromInt(i1+1) - x).Square() <= r2) ++i1; while (i1 > xfloor && dy2 + (entity_pos_t::FromInt(i1) - x).Square() > r2) --i1; #if DEBUG_RANGE_MANAGER_BOUNDS if (i0 <= i1) { ENSURE(dy2 + (entity_pos_t::FromInt(i0) - x).Square() <= r2); ENSURE(dy2 + (entity_pos_t::FromInt(i1) - x).Square() <= r2); } ENSURE(dy2 + (entity_pos_t::FromInt(i0 - 1) - x).Square() > r2); ENSURE(dy2 + (entity_pos_t::FromInt(i1 + 1) - x).Square() > r2); #endif // Clamp the strip to exclude the 1-tile border, // then add or remove the strip as requested i32 i0clamp = std::max(i0, 1); i32 i1clamp = std::min(i1, m_TerrainVerticesPerSide-2); if (adding) LosAddStripHelper(owner, i0clamp, i1clamp, j, counts); else LosRemoveStripHelper(owner, i0clamp, i1clamp, j, counts); } } /** * Update the LOS state of tiles within a given circular range, * by removing visibility around the 'from' position * and then adding visibility around the 'to' position. */ void LosUpdateHelperIncremental(u8 owner, entity_pos_t visionRange, CFixedVector2D from, CFixedVector2D to) { if (m_TerrainVerticesPerSide == 0) // do nothing if not initialised yet return; PROFILE("LosUpdateHelperIncremental"); Grid& counts = m_LosPlayerCounts.at(owner); // Lazy initialisation of counts: if (counts.blank()) counts.resize(m_TerrainVerticesPerSide, m_TerrainVerticesPerSide); // See comments in LosUpdateHelper. // This does exactly the same, except computing the strips for // both circles simultaneously. // (The idea is that the circles will be heavily overlapping, // so we can compute the difference between the removed/added strips // and only have to touch tiles that have a net change.) i32 j0_from = ((from.Y - visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToInfinity(); i32 j1_from = ((from.Y + visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToNegInfinity(); i32 j0_to = ((to.Y - visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToInfinity(); i32 j1_to = ((to.Y + visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToNegInfinity(); i32 j0clamp = std::max(std::min(j0_from, j0_to), 1); i32 j1clamp = std::min(std::max(j1_from, j1_to), m_TerrainVerticesPerSide-2); entity_pos_t x_from = from.X / (int)TERRAIN_TILE_SIZE; entity_pos_t y_from = from.Y / (int)TERRAIN_TILE_SIZE; entity_pos_t x_to = to.X / (int)TERRAIN_TILE_SIZE; entity_pos_t y_to = to.Y / (int)TERRAIN_TILE_SIZE; entity_pos_t r = visionRange / (int)TERRAIN_TILE_SIZE; entity_pos_t r2 = r.Square(); i32 xfloor_from = (x_from - entity_pos_t::Epsilon()).ToInt_RoundToNegInfinity(); i32 xceil_from = (x_from + entity_pos_t::Epsilon()).ToInt_RoundToInfinity(); i32 xfloor_to = (x_to - entity_pos_t::Epsilon()).ToInt_RoundToNegInfinity(); i32 xceil_to = (x_to + entity_pos_t::Epsilon()).ToInt_RoundToInfinity(); i32 i0_from = xfloor_from; i32 i1_from = xceil_from; i32 i0_to = xfloor_to; i32 i1_to = xceil_to; for (i32 j = j0clamp; j <= j1clamp; ++j) { entity_pos_t dy_from = entity_pos_t::FromInt(j) - y_from; entity_pos_t dy2_from = dy_from.Square(); while (dy2_from + (entity_pos_t::FromInt(i0_from-1) - x_from).Square() <= r2) --i0_from; while (i0_from < xceil_from && dy2_from + (entity_pos_t::FromInt(i0_from) - x_from).Square() > r2) ++i0_from; while (dy2_from + (entity_pos_t::FromInt(i1_from+1) - x_from).Square() <= r2) ++i1_from; while (i1_from > xfloor_from && dy2_from + (entity_pos_t::FromInt(i1_from) - x_from).Square() > r2) --i1_from; entity_pos_t dy_to = entity_pos_t::FromInt(j) - y_to; entity_pos_t dy2_to = dy_to.Square(); while (dy2_to + (entity_pos_t::FromInt(i0_to-1) - x_to).Square() <= r2) --i0_to; while (i0_to < xceil_to && dy2_to + (entity_pos_t::FromInt(i0_to) - x_to).Square() > r2) ++i0_to; while (dy2_to + (entity_pos_t::FromInt(i1_to+1) - x_to).Square() <= r2) ++i1_to; while (i1_to > xfloor_to && dy2_to + (entity_pos_t::FromInt(i1_to) - x_to).Square() > r2) --i1_to; #if DEBUG_RANGE_MANAGER_BOUNDS if (i0_from <= i1_from) { ENSURE(dy2_from + (entity_pos_t::FromInt(i0_from) - x_from).Square() <= r2); ENSURE(dy2_from + (entity_pos_t::FromInt(i1_from) - x_from).Square() <= r2); } ENSURE(dy2_from + (entity_pos_t::FromInt(i0_from - 1) - x_from).Square() > r2); ENSURE(dy2_from + (entity_pos_t::FromInt(i1_from + 1) - x_from).Square() > r2); if (i0_to <= i1_to) { ENSURE(dy2_to + (entity_pos_t::FromInt(i0_to) - x_to).Square() <= r2); ENSURE(dy2_to + (entity_pos_t::FromInt(i1_to) - x_to).Square() <= r2); } ENSURE(dy2_to + (entity_pos_t::FromInt(i0_to - 1) - x_to).Square() > r2); ENSURE(dy2_to + (entity_pos_t::FromInt(i1_to + 1) - x_to).Square() > r2); #endif // Check whether this strip moved at all if (!(i0_to == i0_from && i1_to == i1_from)) { i32 i0clamp_from = std::max(i0_from, 1); i32 i1clamp_from = std::min(i1_from, m_TerrainVerticesPerSide-2); i32 i0clamp_to = std::max(i0_to, 1); i32 i1clamp_to = std::min(i1_to, m_TerrainVerticesPerSide-2); // Check whether one strip is negative width, // and we can just add/remove the entire other strip if (i1clamp_from < i0clamp_from) { LosAddStripHelper(owner, i0clamp_to, i1clamp_to, j, counts); } else if (i1clamp_to < i0clamp_to) { LosRemoveStripHelper(owner, i0clamp_from, i1clamp_from, j, counts); } else { // There are four possible regions of overlap between the two strips // (remove before add, remove after add, add before remove, add after remove). // Process each of the regions as its own strip. // (If this produces negative-width strips then they'll just get ignored // which is fine.) // (If the strips don't actually overlap (which is very rare with normal unit // movement speeds), the region between them will be both added and removed, // so we have to do the add first to avoid overflowing to -1 and triggering // assertion failures.) LosAddStripHelper(owner, i0clamp_to, i0clamp_from-1, j, counts); LosAddStripHelper(owner, i1clamp_from+1, i1clamp_to, j, counts); LosRemoveStripHelper(owner, i0clamp_from, i0clamp_to-1, j, counts); LosRemoveStripHelper(owner, i1clamp_to+1, i1clamp_from, j, counts); } } } } void LosAdd(player_id_t owner, entity_pos_t visionRange, CFixedVector2D pos) { if (visionRange.IsZero() || owner <= 0 || owner > MAX_LOS_PLAYER_ID) return; LosUpdateHelper((u8)owner, visionRange, pos); } void SharingLosAdd(u16 visionSharing, entity_pos_t visionRange, CFixedVector2D pos) { if (visionRange.IsZero()) return; for (player_id_t i = 1; i < MAX_LOS_PLAYER_ID+1; ++i) if (HasVisionSharing(visionSharing, i)) LosAdd(i, visionRange, pos); } void LosRemove(player_id_t owner, entity_pos_t visionRange, CFixedVector2D pos) { if (visionRange.IsZero() || owner <= 0 || owner > MAX_LOS_PLAYER_ID) return; LosUpdateHelper((u8)owner, visionRange, pos); } void SharingLosRemove(u16 visionSharing, entity_pos_t visionRange, CFixedVector2D pos) { if (visionRange.IsZero()) return; for (player_id_t i = 1; i < MAX_LOS_PLAYER_ID+1; ++i) if (HasVisionSharing(visionSharing, i)) LosRemove(i, visionRange, pos); } void LosMove(player_id_t owner, entity_pos_t visionRange, CFixedVector2D from, CFixedVector2D to) { if (visionRange.IsZero() || owner <= 0 || owner > MAX_LOS_PLAYER_ID) return; if ((from - to).CompareLength(visionRange) > 0) { // If it's a very large move, then simply remove and add to the new position LosUpdateHelper((u8)owner, visionRange, from); LosUpdateHelper((u8)owner, visionRange, to); } else // Otherwise use the version optimised for mostly-overlapping circles LosUpdateHelperIncremental((u8)owner, visionRange, from, to); } void SharingLosMove(u16 visionSharing, entity_pos_t visionRange, CFixedVector2D from, CFixedVector2D to) { if (visionRange.IsZero()) return; for (player_id_t i = 1; i < MAX_LOS_PLAYER_ID+1; ++i) if (HasVisionSharing(visionSharing, i)) LosMove(i, visionRange, from, to); } virtual u8 GetPercentMapExplored(player_id_t player) const { return m_ExploredVertices.at((u8)player) * 100 / m_TotalInworldVertices; } virtual u8 GetUnionPercentMapExplored(const std::vector& players) const { u32 exploredVertices = 0; std::vector::const_iterator playerIt; for (i32 j = 0; j < m_TerrainVerticesPerSide; j++) for (i32 i = 0; i < m_TerrainVerticesPerSide; i++) { if (LosIsOffWorld(i, j)) continue; for (playerIt = players.begin(); playerIt != players.end(); ++playerIt) if (m_LosState.get(i, j) & ((u32)LosState::EXPLORED << (2*((*playerIt)-1)))) { exploredVertices += 1; break; } } return exploredVertices * 100 / m_TotalInworldVertices; } }; REGISTER_COMPONENT_TYPE(RangeManager) #undef LOS_TILES_RATIO #undef DEBUG_RANGE_MANAGER_BOUNDS Index: ps/trunk/source/simulation2/components/ICmpRangeManager.h =================================================================== --- ps/trunk/source/simulation2/components/ICmpRangeManager.h (revision 24216) +++ ps/trunk/source/simulation2/components/ICmpRangeManager.h (revision 24217) @@ -1,364 +1,364 @@ /* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_ICMPRANGEMANAGER #define INCLUDED_ICMPRANGEMANAGER #include "maths/FixedVector3D.h" #include "maths/FixedVector2D.h" #include "simulation2/system/Interface.h" #include "simulation2/helpers/Position.h" #include "simulation2/helpers/Player.h" #include "graphics/Terrain.h" // for TERRAIN_TILE_SIZE class FastSpatialSubdivision; /** * Since GetVisibility queries are run by the range manager * other code using these must include ICmpRangeManager.h anyways, * so define this enum here (Ideally, it'd be in its own header file, * but adding header file does incur its own compilation time increase). */ enum class LosVisibility : u8 { HIDDEN = 0, FOGGED = 1, VISIBLE = 2 }; /** * The same principle applies to CLosQuerier, but to avoid recompiling TUs (a fortiori headers) * dependent on RangeManager but not CLosQuerier when CLosQuerier changes, * we define it in another file. Code using LOS will then explicitly include the LOS header * which makes sense anyways. */ class CLosQuerier; /** * Provides efficient range-based queries of the game world, * and also LOS-based effects (fog of war). * * (These are somewhat distinct concepts but they share a lot of the implementation, * so for efficiency they're combined into this class.) * * Possible use cases: * - combat units need to detect targetable enemies entering LOS, so they can choose * to auto-attack. * - auras let a unit have some effect on all units (or those of the same player, or of enemies) * within a certain range. * - capturable animals need to detect when a player-owned unit is nearby and no units of other * players are in range. * - scenario triggers may want to detect when units enter a given area. * - units gathering from a resource that is exhausted need to find a new resource of the * same type, near the old one and reachable. * - projectile weapons with splash damage need to find all units within some distance * of the target point. * - ... * * In most cases the users are event-based and want notifications when something * has entered or left the range, and the query can be set up once and rarely changed. - * These queries have to be fast. It's fine to approximate an entity as a point. + * These queries have to be fast. Entities are approximated as circles. * * Current design: * * This class handles just the most common parts of range queries: * distance, target interface, and player ownership. * The caller can then apply any more complex filtering that it needs. * * There are two types of query: * Passive queries are performed by ExecuteQuery and immediately return the matching entities. * Active queries are set up by CreateActiveQuery, and then a CMessageRangeUpdate message will be * sent to the entity once per turn if anybody has entered or left the range since the last RangeUpdate. * Queries can be disabled, in which case no message will be sent. */ class ICmpRangeManager : public IComponent { public: /** * External identifiers for active queries. */ typedef u32 tag_t; /** * Access the spatial subdivision kept by the range manager. * @return pointer to spatial subdivision structure. */ virtual FastSpatialSubdivision* GetSubdivision() = 0; /** * Set the bounds of the world. * Entities should not be outside the bounds (else efficiency will suffer). * @param x0,z0,x1,z1 Coordinates of the corners of the world * @param vertices Number of terrain vertices per side */ virtual void SetBounds(entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, ssize_t vertices) = 0; /** * Execute a passive query. * @param source the entity around which the range will be computed. * @param minRange non-negative minimum distance in metres (inclusive). * @param maxRange non-negative maximum distance in metres (inclusive); or -1.0 to ignore distance. * @param owners list of player IDs that matching entities may have; -1 matches entities with no owner. * @param requiredInterface if non-zero, an interface ID that matching entities must implement. * @return list of entities matching the query, ordered by increasing distance from the source entity. */ virtual std::vector ExecuteQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, const std::vector& owners, int requiredInterface) = 0; /** * Execute a passive query. * @param pos the position around which the range will be computed. * @param minRange non-negative minimum distance in metres (inclusive). * @param maxRange non-negative maximum distance in metres (inclusive); or -1.0 to ignore distance. * @param owners list of player IDs that matching entities may have; -1 matches entities with no owner. * @param requiredInterface if non-zero, an interface ID that matching entities must implement. * @return list of entities matching the query, ordered by increasing distance from the source entity. */ virtual std::vector ExecuteQueryAroundPos(const CFixedVector2D& pos, entity_pos_t minRange, entity_pos_t maxRange, const std::vector& owners, int requiredInterface) = 0; /** * Construct an active query. The query will be disabled by default. * @param source the entity around which the range will be computed. * @param minRange non-negative minimum distance in metres (inclusive). * @param maxRange non-negative maximum distance in metres (inclusive); or -1.0 to ignore distance. * @param owners list of player IDs that matching entities may have; -1 matches entities with no owner. * @param requiredInterface if non-zero, an interface ID that matching entities must implement. * @param flags if a entity in range has one of the flags set it will show up. * @return unique non-zero identifier of query. */ virtual tag_t CreateActiveQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, const std::vector& owners, int requiredInterface, u8 flags) = 0; /** * Construct an active query of a paraboloic form around the unit. * The query will be disabled by default. * @param source the entity around which the range will be computed. * @param minRange non-negative minimum horizontal distance in metres (inclusive). MinRange doesn't do parabolic checks. * @param maxRange non-negative maximum distance in metres (inclusive) for units on the same elevation; * or -1.0 to ignore distance. * For units on a different elevation, a physical correct paraboloid with height=maxRange/2 above the unit is used to query them * @param elevationBonus extra bonus so the source can be placed higher and shoot further * @param owners list of player IDs that matching entities may have; -1 matches entities with no owner. * @param requiredInterface if non-zero, an interface ID that matching entities must implement. * @param flags if a entity in range has one of the flags set it will show up. * @return unique non-zero identifier of query. */ virtual tag_t CreateActiveParabolicQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t elevationBonus, const std::vector& owners, int requiredInterface, u8 flags) = 0; /** * Get the average elevation over 8 points on distance range around the entity * @param id the entity id to look around * @param range the distance to compare terrain height with * @return a fixed number representing the average difference. It's positive when the entity is on average higher than the terrain surrounding it. */ virtual entity_pos_t GetElevationAdaptedRange(const CFixedVector3D& pos, const CFixedVector3D& rot, entity_pos_t range, entity_pos_t elevationBonus, entity_pos_t angle) const = 0; /** * Destroy a query and clean up resources. This must be called when an entity no longer needs its * query (e.g. when the entity is destroyed). * @param tag identifier of query. */ virtual void DestroyActiveQuery(tag_t tag) = 0; /** * Re-enable the processing of a query. * @param tag identifier of query. */ virtual void EnableActiveQuery(tag_t tag) = 0; /** * Disable the processing of a query (no RangeUpdate messages will be sent). * @param tag identifier of query. */ virtual void DisableActiveQuery(tag_t tag) = 0; /** * Check if the processing of a query is enabled. * @param tag identifier of a query. */ virtual bool IsActiveQueryEnabled(tag_t tag) const = 0; /** * Immediately execute a query, and re-enable it if disabled. * The next RangeUpdate message will say who has entered/left since this call, * so you won't miss any notifications. * @param tag identifier of query. * @return list of entities matching the query, ordered by increasing distance from the source entity. */ virtual std::vector ResetActiveQuery(tag_t tag) = 0; /** * Returns a list of all entities for a specific player. * (This is on this interface because it shares a lot of the implementation. * Maybe it should be extended to be more like ExecuteQuery without * the range parameter.) */ virtual std::vector GetEntitiesByPlayer(player_id_t player) const = 0; /** * Returns a list of all entities of all players except gaia. */ virtual std::vector GetNonGaiaEntities() const = 0; /** * Returns a list of all entities owned by a player or gaia. */ virtual std::vector GetGaiaAndNonGaiaEntities() const = 0; /** * Toggle the rendering of debug info. */ virtual void SetDebugOverlay(bool enabled) = 0; /** * Returns the mask for the specified identifier. */ virtual u8 GetEntityFlagMask(const std::string& identifier) const = 0; /** * Set the flag specified by the identifier to the supplied value for the entity * @param ent the entity whose flags will be modified. * @param identifier the flag to be modified. * @param value to which the flag will be set. */ virtual void SetEntityFlag(entity_id_t ent, const std::string& identifier, bool value) = 0; ////////////////////////////////////////////////////////////////// //// LOS interface below this line //// ////////////////////////////////////////////////////////////////// /** * Returns a CLosQuerier for checking whether vertex positions are visible to the given player * (or other players it shares LOS with). */ virtual CLosQuerier GetLosQuerier(player_id_t player) const = 0; /** * Toggle the scripted Visibility component activation for entity ent. */ virtual void ActivateScriptedVisibility(entity_id_t ent, bool status) = 0; /** * Returns the visibility status of the given entity, with respect to the given player. * Returns LosVisibility::HIDDEN if the entity doesn't exist or is not in the world. * This respects the GetLosRevealAll flag. */ virtual LosVisibility GetLosVisibility(CEntityHandle ent, player_id_t player) const = 0; virtual LosVisibility GetLosVisibility(entity_id_t ent, player_id_t player) const = 0; /** * Returns the visibility status of the given position, with respect to the given player. * This respects the GetLosRevealAll flag. */ virtual LosVisibility GetLosVisibilityPosition(entity_pos_t x, entity_pos_t z, player_id_t player) const = 0; /** * Request the update of the visibility cache of ent at next turn. * Typically used for fogging. */ virtual void RequestVisibilityUpdate(entity_id_t ent) = 0; /** * GetLosVisibility wrapped for script calls. * Returns "hidden", "fogged" or "visible". */ std::string GetLosVisibility_wrapper(entity_id_t ent, player_id_t player) const; /** * GetLosVisibilityPosition wrapped for script calls. * Returns "hidden", "fogged" or "visible". */ std::string GetLosVisibilityPosition_wrapper(entity_pos_t x, entity_pos_t z, player_id_t player) const; /** * Explore all tiles (but leave them in the FoW) for player p */ virtual void ExploreAllTiles(player_id_t p) = 0; /** * Explore the tiles inside each player's territory. * This is done only at the beginning of the game. */ virtual void ExploreTerritories() = 0; /** * Reveal the shore for specified player p. * This works like for entities: if RevealShore is called multiple times with enabled, it * will be necessary to call it the same number of times with !enabled to make the shore * fall back into the FoW. */ virtual void RevealShore(player_id_t p, bool enable) = 0; /** * Set whether the whole map should be made visible to the given player. * If player is -1, the map will be made visible to all players. */ virtual void SetLosRevealAll(player_id_t player, bool enabled) = 0; /** * Returns whether the whole map has been made visible to the given player. */ virtual bool GetLosRevealAll(player_id_t player) const = 0; /** * Set the LOS to be restricted to a circular map. */ virtual void SetLosCircular(bool enabled) = 0; /** * Returns whether the LOS is restricted to a circular map. */ virtual bool GetLosCircular() const = 0; /** * Sets shared LOS data for player to the given list of players. */ virtual void SetSharedLos(player_id_t player, const std::vector& players) = 0; /** * Returns shared LOS mask for player. */ virtual u32 GetSharedLosMask(player_id_t player) const = 0; /** * Get percent map explored statistics for specified player. */ virtual u8 GetPercentMapExplored(player_id_t player) const = 0; /** * Get percent map explored statistics for specified set of players. * Note: this function computes statistics from scratch and should not be called too often. */ virtual u8 GetUnionPercentMapExplored(const std::vector& players) const = 0; /** * Perform some internal consistency checks for testing/debugging. */ virtual void Verify() = 0; DECLARE_INTERFACE_TYPE(RangeManager) }; #endif // INCLUDED_ICMPRANGEMANAGER Index: ps/trunk/source/simulation2/components/tests/test_RangeManager.h =================================================================== --- ps/trunk/source/simulation2/components/tests/test_RangeManager.h (revision 24216) +++ ps/trunk/source/simulation2/components/tests/test_RangeManager.h (revision 24217) @@ -1,156 +1,268 @@ /* Copyright (C) 2019 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 "simulation2/system/ComponentTest.h" #include "simulation2/components/ICmpRangeManager.h" +#include "simulation2/components/ICmpObstruction.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpVision.h" #include #include -class MockVision : public ICmpVision +class MockVisionRgm : public ICmpVision { public: DEFAULT_MOCK_COMPONENT() virtual entity_pos_t GetRange() const { return entity_pos_t::FromInt(66); } virtual bool GetRevealShore() const { return false; } }; -class MockPosition : public ICmpPosition +class MockPositionRgm : public ICmpPosition { public: DEFAULT_MOCK_COMPONENT() virtual void SetTurretParent(entity_id_t UNUSED(id), const CFixedVector3D& UNUSED(pos)) {} virtual entity_id_t GetTurretParent() const {return INVALID_ENTITY;} virtual void UpdateTurretPosition() {} virtual std::set* GetTurrets() { return NULL; } virtual bool IsInWorld() const { return true; } virtual void MoveOutOfWorld() { } virtual void MoveTo(entity_pos_t UNUSED(x), entity_pos_t UNUSED(z)) { } virtual void MoveAndTurnTo(entity_pos_t UNUSED(x), entity_pos_t UNUSED(z), entity_angle_t UNUSED(a)) { } virtual void JumpTo(entity_pos_t UNUSED(x), entity_pos_t UNUSED(z)) { } virtual void SetHeightOffset(entity_pos_t UNUSED(dy)) { } virtual entity_pos_t GetHeightOffset() const { return entity_pos_t::Zero(); } virtual void SetHeightFixed(entity_pos_t UNUSED(y)) { } virtual entity_pos_t GetHeightFixed() const { return entity_pos_t::Zero(); } virtual bool IsHeightRelative() const { return true; } virtual void SetHeightRelative(bool UNUSED(relative)) { } virtual bool CanFloat() const { return false; } virtual void SetFloating(bool UNUSED(flag)) { } virtual void SetActorFloating(bool UNUSED(flag)) { } virtual void SetConstructionProgress(fixed UNUSED(progress)) { } - virtual CFixedVector3D GetPosition() const { return CFixedVector3D(); } - virtual CFixedVector2D GetPosition2D() const { return CFixedVector2D(); } + virtual CFixedVector3D GetPosition() const { return m_Pos; } + virtual CFixedVector2D GetPosition2D() const { return CFixedVector2D(m_Pos.X, m_Pos.Z); } virtual CFixedVector3D GetPreviousPosition() const { return CFixedVector3D(); } virtual CFixedVector2D GetPreviousPosition2D() const { return CFixedVector2D(); } virtual void TurnTo(entity_angle_t UNUSED(y)) { } virtual void SetYRotation(entity_angle_t UNUSED(y)) { } virtual void SetXZRotation(entity_angle_t UNUSED(x), entity_angle_t UNUSED(z)) { } virtual CFixedVector3D GetRotation() const { return CFixedVector3D(); } virtual fixed GetDistanceTravelled() const { return fixed::Zero(); } virtual void GetInterpolatedPosition2D(float UNUSED(frameOffset), float& x, float& z, float& rotY) const { x = z = rotY = 0; } virtual CMatrix3D GetInterpolatedTransform(float UNUSED(frameOffset)) const { return CMatrix3D(); } + + CFixedVector3D m_Pos; +}; + +class MockObstructionRgm : public ICmpObstruction +{ +public: + DEFAULT_MOCK_COMPONENT(); + + MockObstructionRgm(entity_pos_t s) : m_Size(s) {}; + + virtual ICmpObstructionManager::tag_t GetObstruction() const { return {}; }; + virtual bool GetObstructionSquare(ICmpObstructionManager::ObstructionSquare&) const { return false; }; + virtual bool GetPreviousObstructionSquare(ICmpObstructionManager::ObstructionSquare&) const { return false; }; + virtual entity_pos_t GetSize() const { return m_Size; }; + virtual CFixedVector2D GetStaticSize() const { return {}; }; + virtual EObstructionType GetObstructionType() const { return {}; }; + virtual void SetUnitClearance(const entity_pos_t&) {}; + virtual bool IsControlPersistent() const { return {}; }; + virtual bool CheckShorePlacement() const { return {}; }; + virtual EFoundationCheck CheckFoundation(const std::string&) const { return {}; }; + virtual EFoundationCheck CheckFoundation(const std::string& , bool) const { return {}; }; + virtual std::string CheckFoundation_wrapper(const std::string&, bool) const { return {}; }; + virtual bool CheckDuplicateFoundation() const { return {}; }; + virtual std::vector GetEntitiesByFlags(ICmpObstructionManager::flags_t) const { return {}; }; + virtual std::vector GetEntitiesBlockingMovement() const { return {}; }; + virtual std::vector GetEntitiesBlockingConstruction() const { return {}; }; + virtual std::vector GetEntitiesDeletedUponConstruction() const { return {}; }; + virtual void ResolveFoundationCollisions() const {}; + virtual void SetActive(bool) {}; + virtual void SetMovingFlag(bool) {}; + virtual void SetDisableBlockMovementPathfinding(bool, bool, int32_t) {}; + virtual bool GetBlockMovementFlag() const { return {}; }; + virtual void SetControlGroup(entity_id_t) {}; + virtual entity_id_t GetControlGroup() const { return {}; }; + virtual void SetControlGroup2(entity_id_t) {}; + virtual entity_id_t GetControlGroup2() const { return {}; }; +private: + entity_pos_t m_Size; }; class TestCmpRangeManager : public CxxTest::TestSuite { public: void setUp() { CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); } // TODO It would be nice to call Verify() with the shore revealing system // but that means testing on an actual map, with water and land. void test_basic() { ComponentTestHelper test(g_ScriptContext); ICmpRangeManager* cmp = test.Add(CID_RangeManager, "", SYSTEM_ENTITY); - MockVision vision; + MockVisionRgm vision; test.AddMock(100, IID_Vision, vision); - MockPosition position; + MockPositionRgm position; test.AddMock(100, IID_Position, position); // This tests that the incremental computation produces the correct result // in various edge cases cmp->SetBounds(entity_pos_t::FromInt(0), entity_pos_t::FromInt(0), entity_pos_t::FromInt(512), entity_pos_t::FromInt(512), 512/TERRAIN_TILE_SIZE + 1); cmp->Verify(); { CMessageCreate msg(100); cmp->HandleMessage(msg, false); } cmp->Verify(); { CMessageOwnershipChanged msg(100, -1, 1); cmp->HandleMessage(msg, false); } cmp->Verify(); { CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(247), entity_pos_t::FromDouble(257.95), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } cmp->Verify(); { CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(247), entity_pos_t::FromInt(253), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } cmp->Verify(); { CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256), entity_pos_t::FromInt(256), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } cmp->Verify(); { CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256)+entity_pos_t::Epsilon(), entity_pos_t::FromInt(256), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } cmp->Verify(); { CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256)-entity_pos_t::Epsilon(), entity_pos_t::FromInt(256), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } cmp->Verify(); { CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256), entity_pos_t::FromInt(256)+entity_pos_t::Epsilon(), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } cmp->Verify(); { CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256), entity_pos_t::FromInt(256)-entity_pos_t::Epsilon(), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } cmp->Verify(); { CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(383), entity_pos_t::FromInt(84), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } cmp->Verify(); { CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(348), entity_pos_t::FromInt(83), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } cmp->Verify(); boost::mt19937 rng; for (size_t i = 0; i < 1024; ++i) { double x = boost::random::uniform_real_distribution(0.0, 512.0)(rng); double z = boost::random::uniform_real_distribution(0.0, 512.0)(rng); { CMessagePositionChanged msg(100, true, entity_pos_t::FromDouble(x), entity_pos_t::FromDouble(z), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } cmp->Verify(); } // Test OwnershipChange, GetEntitiesByPlayer, GetNonGaiaEntities { player_id_t previousOwner = -1; for (player_id_t newOwner = 0; newOwner < 8; ++newOwner) { CMessageOwnershipChanged msg(100, previousOwner, newOwner); cmp->HandleMessage(msg, false); for (player_id_t i = 0; i < 8; ++i) TS_ASSERT_EQUALS(cmp->GetEntitiesByPlayer(i).size(), i == newOwner ? 1 : 0); TS_ASSERT_EQUALS(cmp->GetNonGaiaEntities().size(), newOwner > 0 ? 1 : 0); previousOwner = newOwner; } } } + + void test_queries() + { + ComponentTestHelper test(g_ScriptContext); + + ICmpRangeManager* cmp = test.Add(CID_RangeManager, "", SYSTEM_ENTITY); + + MockVisionRgm vision, vision2; + MockPositionRgm position, position2; + MockObstructionRgm obs(fixed::FromInt(2)), obs2(fixed::Zero()); + test.AddMock(100, IID_Vision, vision); + test.AddMock(100, IID_Position, position); + test.AddMock(100, IID_Obstruction, obs); + + test.AddMock(101, IID_Vision, vision2); + test.AddMock(101, IID_Position, position2); + test.AddMock(101, IID_Obstruction, obs2); + + cmp->SetBounds(entity_pos_t::FromInt(0), entity_pos_t::FromInt(0), entity_pos_t::FromInt(512), entity_pos_t::FromInt(512), 512/TERRAIN_TILE_SIZE + 1); + cmp->Verify(); + { CMessageCreate msg(100); cmp->HandleMessage(msg, false); } + { CMessageCreate msg(101); cmp->HandleMessage(msg, false); } + + { CMessageOwnershipChanged msg(100, -1, 1); cmp->HandleMessage(msg, false); } + { CMessageOwnershipChanged msg(101, -1, 1); cmp->HandleMessage(msg, false); } + + auto move = [&cmp](entity_id_t ent, MockPositionRgm& pos, fixed x, fixed z) { + pos.m_Pos = CFixedVector3D(x, fixed::Zero(), z); + { CMessagePositionChanged msg(ent, true, x, z, entity_angle_t::Zero()); cmp->HandleMessage(msg, false); } + }; + + move(100, position, fixed::FromInt(10), fixed::FromInt(10)); + move(101, position2, fixed::FromInt(10), fixed::FromInt(20)); + + std::vector nearby = cmp->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {1}, 0); + TS_ASSERT_EQUALS(nearby, std::vector{}); + nearby = cmp->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {1}, 0); + TS_ASSERT_EQUALS(nearby, std::vector{101}); + + move(101, position2, fixed::FromInt(10), fixed::FromInt(10)); + nearby = cmp->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {1}, 0); + TS_ASSERT_EQUALS(nearby, std::vector{101}); + nearby = cmp->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {1}, 0); + TS_ASSERT_EQUALS(nearby, std::vector{}); + + move(101, position2, fixed::FromInt(10), fixed::FromInt(13)); + nearby = cmp->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {1}, 0); + TS_ASSERT_EQUALS(nearby, std::vector{101}); + nearby = cmp->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {1}, 0); + TS_ASSERT_EQUALS(nearby, std::vector{}); + + move(101, position2, fixed::FromInt(10), fixed::FromInt(15)); + // In range thanks to self obstruction size. + nearby = cmp->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {1}, 0); + TS_ASSERT_EQUALS(nearby, std::vector{101}); + // In range thanks to target obstruction size. + nearby = cmp->ExecuteQuery(101, fixed::FromInt(0), fixed::FromInt(4), {1}, 0); + TS_ASSERT_EQUALS(nearby, std::vector{100}); + + // Trickier: min-range is closest-to-closest, but rotation may change the real distance. + nearby = cmp->ExecuteQuery(100, fixed::FromInt(2), fixed::FromInt(50), {1}, 0); + TS_ASSERT_EQUALS(nearby, std::vector{101}); + nearby = cmp->ExecuteQuery(100, fixed::FromInt(5), fixed::FromInt(50), {1}, 0); + TS_ASSERT_EQUALS(nearby, std::vector{101}); + nearby = cmp->ExecuteQuery(100, fixed::FromInt(6), fixed::FromInt(50), {1}, 0); + TS_ASSERT_EQUALS(nearby, std::vector{}); + nearby = cmp->ExecuteQuery(101, fixed::FromInt(5), fixed::FromInt(50), {1}, 0); + TS_ASSERT_EQUALS(nearby, std::vector{100}); + nearby = cmp->ExecuteQuery(101, fixed::FromInt(6), fixed::FromInt(50), {1}, 0); + TS_ASSERT_EQUALS(nearby, std::vector{}); + + } };