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
MonumentMonument
- 150
+ 1351201001008.01200decay|rubble/rubble_stone_2x2iberRevered MonumentGur OroigarriMonumentstructures/iberian_bull.png2020interface/complete/building/complete_iber_monument.xmlstructures/iberians/sb_1.xmlstructures/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
PillarPillar
- 75
+ 70801001005.01000decay|rubble/rubble_stone_2x2maurEdict Pillar of AshokaŚāsana Stambha AśokāPillarstructures/ashoka_pillar.png2020interface/complete/building/complete_iber_monument.xmlprops/structures/mauryas/ashoka_pillar.xmlstructures/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 neutralMercenaryCamp
- 100
+ 70300100010012.01200decay|rubble/rubble_stone_5x5ptolEgyptian Mercenary CampStratopedeia MisthophorōnMercenaryCampCapture this structure to train mercenaries from Hellenistic Egypt.structures/military_settlement.pngphase_town20020
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.xml1structures/mercenaries/camp_egyptian.xmlstructures/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 @@
10107.0
- 20
+ 20.5Palisade Gatestructures/palisades_gateAllow units access through Palisades. Can be locked to prevent access.Gatestructures/wooden_gate.png2interface/complete/building/complete_gate.xmlactor/gate/stonegate_close.xmlactor/gate/stonegate_open.xmlinterface/select/building/sel_gate.xmlinterface/select/building/sel_gate.xml8.0structures/fndn_3x1.xmlprops/special/palisade_rocks_gate.xml14
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 neutralMercenaryCamp
- 100
+ 7030010010012.01200decay|rubble/rubble_stone_5x5ptolMercenary CampStratopedeia MisthophorōnMercenaryCampCheap Barracks-like structure that is buildable in neutral territory, but casts no territory influence. Train Mercenaries.structures/mercenary_camp.pngphase_town20020
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.xml1structures/ptolemies/settlement.xmlstructures/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 @@
Bow0.025.00.060.00.012002000075.01.59.81falseoutline_border.pngoutline_border_mask.png0.1751151Soldierneutral enemyArmyCampArmyCamp
- 80
+ 45150010.03.0525050012.040Support Infantry Cavalry Siege162500decay|rubble/rubble_rome_sbromeArmy CampCastraBuild in neutral or enemy territory. Train Citizen-Soldiers and construct Siege Engines. Garrison Soldiers for additional arrows.ConquestCriticalArmyCampstructures/roman_camp.png1001000.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
15252151interface/complete/building/complete_broch.xmlattack/weapon/bow_attack.xmlattack/impact/arrow_impact.xml37.560structures/romans/camp.xmlstructures/fndn_8x8.xml29.58
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 @@
FemaleCitizen140190100Bow0.012.00.072.00.012002000075.01.59.81falseHumanoutline_border.pngoutline_border_mask.png0.17531Soldierown neutralCivilCentreCivilCentre
- 200
+ 16025005.0205005005005008.0200.1UnitSupport Infantry Cavalry113000decay|rubble/rubble_stone_6x6Civic Centertemplate_structure_civic_civil_centreBuild in own or neutral territory. Acquire large tracts of territory. Territory root. Train Citizens and research technologies. Garrison Soldiers for additional arrows.CivCentreDefensive CivilCentrestructures/civic_centre.png2001001001000.8
units/{civ}/support_female_citizen
phase_town_{civ}
phase_city_{civ}
unlock_spies
spy_counter
555153food wood stone metaltrueinterface/complete/building/complete_civ_center.xmlinterface/alarm/alarm_alert_0.xmlinterface/alarm/alarm_alert_1.xmlattack/weapon/bow_attack.xmlattack/impact/arrow_impact.xmltrue1401000090structures/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 @@
1own neutralColonyCivilCentre
- 120
+ 803002002002002000decay|rubble/rubble_stone_5x5Military Colonytemplate_structure_civic_civil_centre_military_colonyColonystructures/military_settlement.pngphase_town404040
-phase_town_{civ}
-phase_city_{civ}
upgrade_rank_advanced_mercenary
interface/complete/building/complete_gymnasium.xml80
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 neutralOutpost
- 50
+ 45306013.010.1UnitInfantry02400decay|rubble/rubble_stone_2x2Outposttemplate_structure_defensive_outpostBuild in own or neutral territory. Slowly converts to Gaia while in neutral territory.Outpoststructures/outpost.png1210201interface/complete/building/complete_tower.xml14.01080structures/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 @@
Bow00012002000075.01.539.81falseHumanoutline_border.pngoutline_border_mask.png0.17511InfantryTowerTower
- 60
+ 550.1UnitSupport Infantry021000decay|rubble/rubble_stone_2x2TowerTowerinterface/complete/building/complete_tower.xmlattack/weapon/bow_attack.xmlattack/impact/arrow_impact.xml18.080structures/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
301218
- 20
+ 22500decay|rubble/rubble_stone_wall_longGatetemplate_structure_defensive_wall_gateAllow units access through Walls. Can be locked to prevent access.Gatestructures/gate.png24interface/complete/building/complete_gate.xmlinterface/complete/building/complete_gate.xmlactor/gate/stonegate_close.xmlactor/gate/stonegate_open.xmlinterface/select/building/sel_gate.xmlinterface/select/building/sel_gate.xmlstructures/fndn_9x3_wall.xml36
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 @@
Bow0.016.00.072.00.012002000075.01.59.81falseHumanoutline_border.pngoutline_border_mask.png0.17531SoldierFortressFortress
- 80
+ 55400010.01050010008.0200.075Support Infantry Cavalry Siege64200decay|rubble/rubble_stone_6x6Fortresstemplate_structure_military_fortressTrain Champions and Heroes and research technologies. Garrison Soldiers for additional arrows.GarrisonFortressDefensive City Fortressstructures/fortress.pngphase_city1002000.8
attack_soldiers_will
553interface/complete/building/complete_fortress.xmlattack/weapon/bow_attack.xmlattack/impact/arrow_impact.xml10080structures/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{});
+
+ }
};