diff --git a/binaries/data/mods/public/simulation/components/UnitAI.js b/binaries/data/mods/public/simulation/components/UnitAI.js
index fe0768a5a1..b740c86ea6 100644
--- a/binaries/data/mods/public/simulation/components/UnitAI.js
+++ b/binaries/data/mods/public/simulation/components/UnitAI.js
@@ -42,6 +42,11 @@ UnitAI.prototype.Schema =
"" +
"" +
"" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
"" +
"" +
"" +
@@ -3462,6 +3467,7 @@ UnitAI.prototype.Init = function()
this.formationAnimationVariant = undefined;
this.cheeringTime = +(this.template.CheeringTime || 0);
+ this.rangeError = +(this.template.RangeError || 0);
this.SetStance(this.template.DefaultStance);
};
@@ -3776,7 +3782,7 @@ UnitAI.prototype.SetupLOSRangeQuery = function(enable = true)
// Do not compensate for entity sizes: LOS doesn't, and UnitAI relies on that.
this.losRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity,
range.min, range.max, players, IID_Identity,
- cmpRangeManager.GetEntityFlagMask("normal"), false);
+ cmpRangeManager.GetEntityFlagMask("normal"), false, this.rangeError);
if (enable)
cmpRangeManager.EnableActiveQuery(this.losRangeQuery);
@@ -3841,7 +3847,7 @@ UnitAI.prototype.SetupAttackRangeQuery = function(enable = true)
// Do not compensate for entity sizes: LOS doesn't, and UnitAI relies on that.
this.losAttackRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity,
range.min, range.max, players, IID_Resistance,
- cmpRangeManager.GetEntityFlagMask("normal"), false);
+ cmpRangeManager.GetEntityFlagMask("normal"), false, this.rangeError);
if (enable)
cmpRangeManager.EnableActiveQuery(this.losAttackRangeQuery);
diff --git a/binaries/data/mods/public/simulation/templates/template_unit.xml b/binaries/data/mods/public/simulation/templates/template_unit.xml
index e819518061..0fb884a89c 100644
--- a/binaries/data/mods/public/simulation/templates/template_unit.xml
+++ b/binaries/data/mods/public/simulation/templates/template_unit.xml
@@ -109,6 +109,7 @@
true
1
2800
+ 10
special/formations/null
special/formations/box
diff --git a/binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_crossbowman.xml b/binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_crossbowman.xml
index 05f3626771..5710abfe2a 100644
--- a/binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_crossbowman.xml
+++ b/binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_crossbowman.xml
@@ -42,6 +42,7 @@
+ 15
special/formations/skirmish
diff --git a/binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_crossbowman.xml b/binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_crossbowman.xml
index 5fcbb13751..c5d203b8c0 100644
--- a/binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_crossbowman.xml
+++ b/binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged_crossbowman.xml
@@ -45,6 +45,9 @@
attack/weapon/bow_attack.xml
+
+ 20
+
0.6
1.2
diff --git a/binaries/data/mods/public/simulation/templates/template_unit_siege_boltshooter.xml b/binaries/data/mods/public/simulation/templates/template_unit_siege_boltshooter.xml
index f547eb8491..55e679a6be 100644
--- a/binaries/data/mods/public/simulation/templates/template_unit_siege_boltshooter.xml
+++ b/binaries/data/mods/public/simulation/templates/template_unit_siege_boltshooter.xml
@@ -67,6 +67,7 @@
+ 3
standground
diff --git a/source/maths/FixedVector2D.h b/source/maths/FixedVector2D.h
index 100c42597e..0ca06232d1 100644
--- a/source/maths/FixedVector2D.h
+++ b/source/maths/FixedVector2D.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2020 Wildfire Games.
+/* Copyright (C) 2024 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -169,6 +169,29 @@ public:
return 0;
}
+ /**
+ * Returns -1, 0, +1 depending on whether length estimate is less/equal/greater
+ * than the argument's length.
+ * Uses a percent error parameter to compare lengths with that percent error.
+ */
+ int CompareLengthRough(const CFixedVector2D& other, u8 rangeError) const
+ {
+ u64 d2 = SQUARE_U64_FIXED(X) + SQUARE_U64_FIXED(Y);
+ u64 od2 = SQUARE_U64_FIXED(other.X) + SQUARE_U64_FIXED(other.Y);
+
+ //overflow risk with long ranges (designed for unit ranges)
+ CheckMultiplicationOverflow(u64, d2, 100+rangeError, "Overflow in CFixedVector2D::CompareLengthRough()","Underflow in CFixedVector2D::CompareLengthRough()")
+ d2 = d2 * (100-rangeError + d2 % (1+(rangeError*2)));
+ CheckMultiplicationOverflow(u64, od2, 100, "Overflow in CFixedVector2D::CompareLengthRough()", "Underflow in CFixedVector2D::CompareLengthRough()")
+ od2 = od2 * 100;
+
+ if (d2 < od2)
+ return -1;
+ if (d2 > od2)
+ return +1;
+ return 0;
+ }
+
bool IsZero() const
{
return X.IsZero() && Y.IsZero();
diff --git a/source/simulation2/components/CCmpRangeManager.cpp b/source/simulation2/components/CCmpRangeManager.cpp
index d54318caef..31c639d636 100644
--- a/source/simulation2/components/CCmpRangeManager.cpp
+++ b/source/simulation2/components/CCmpRangeManager.cpp
@@ -1,4 +1,4 @@
-/* Copyright (C) 2022 Wildfire Games.
+/* Copyright (C) 2024 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -162,6 +162,7 @@ struct Query
u32 ownersMask;
i32 interface;
u8 flagsMask;
+ u8 rangeError;
bool enabled;
bool parabolic;
bool accountForSize; // If true, the query accounts for unit sizes, otherwise it treats all entities as points.
@@ -255,8 +256,8 @@ static_assert(sizeof(EntityData) == 24);
class EntityDistanceOrdering
{
public:
- EntityDistanceOrdering(const EntityMap& entities, const CFixedVector2D& source) :
- m_EntityData(entities), m_Source(source)
+ EntityDistanceOrdering(const EntityMap& entities, const CFixedVector2D& source, u8 rangeError = 0) :
+ m_EntityData(entities), m_Source(source), m_RangeError(rangeError)
{
}
@@ -268,11 +269,12 @@ public:
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);
+ return m_RangeError > 0 ? vecA.CompareLengthRough(vecB, m_RangeError) < 0 : vecA.CompareLength(vecB) < 0;
}
const EntityMap& m_EntityData;
CFixedVector2D m_Source;
+ u8 m_RangeError;
private:
EntityDistanceOrdering& operator=(const EntityDistanceOrdering&);
@@ -294,6 +296,7 @@ struct SerializeHelper
serialize.NumberU32_Unbounded("owners mask", value.ownersMask);
serialize.NumberI32_Unbounded("interface", value.interface);
Serializer(serialize, "last match", value.lastMatch);
+ serialize.NumberU8_Unbounded("range percent error", value.rangeError);
serialize.NumberU8_Unbounded("flagsMask", value.flagsMask);
serialize.Bool("enabled", value.enabled);
serialize.Bool("parabolic",value.parabolic);
@@ -923,20 +926,20 @@ public:
tag_t CreateActiveQuery(entity_id_t source,
entity_pos_t minRange, entity_pos_t maxRange,
- const std::vector& owners, int requiredInterface, u8 flags, bool accountForSize) override
+ const std::vector& owners, int requiredInterface, u8 flags, bool accountForSize, u8 rangeError) override
{
tag_t id = m_QueryNext++;
- m_Queries[id] = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, flags, accountForSize);
+ m_Queries[id] = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, flags, accountForSize, rangeError);
return id;
}
tag_t CreateActiveParabolicQuery(entity_id_t source,
entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t yOrigin,
- const std::vector& owners, int requiredInterface, u8 flags) override
+ const std::vector& owners, int requiredInterface, u8 flags, u8 rangeError) override
{
tag_t id = m_QueryNext++;
- m_Queries[id] = ConstructParabolicQuery(source, minRange, maxRange, yOrigin, owners, requiredInterface, flags, true);
+ m_Queries[id] = ConstructParabolicQuery(source, minRange, maxRange, yOrigin, owners, requiredInterface, flags, true, rangeError);
return id;
}
@@ -993,25 +996,25 @@ public:
std::vector ExecuteQueryAroundPos(const CFixedVector2D& pos,
entity_pos_t minRange, entity_pos_t maxRange,
- const std::vector& owners, int requiredInterface, bool accountForSize) override
+ const std::vector& owners, int requiredInterface, bool accountForSize, u8 rangeError) override
{
- Query q = ConstructQuery(INVALID_ENTITY, minRange, maxRange, owners, requiredInterface, GetEntityFlagMask("normal"), accountForSize);
+ Query q = ConstructQuery(INVALID_ENTITY, minRange, maxRange, owners, requiredInterface, GetEntityFlagMask("normal"), accountForSize, rangeError);
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));
+ std::stable_sort(r.begin(), r.end(), EntityDistanceOrdering(m_EntityData, pos, q.rangeError));
return r;
}
std::vector ExecuteQuery(entity_id_t source,
entity_pos_t minRange, entity_pos_t maxRange,
- const std::vector& owners, int requiredInterface, bool accountForSize) override
+ const std::vector& owners, int requiredInterface, bool accountForSize, u8 rangeError) override
{
PROFILE("ExecuteQuery");
- Query q = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, GetEntityFlagMask("normal"), accountForSize);
+ Query q = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, GetEntityFlagMask("normal"), accountForSize, rangeError);
std::vector r;
@@ -1026,7 +1029,7 @@ public:
PerformQuery(q, r, pos);
// Return the list sorted by distance from the entity
- std::stable_sort(r.begin(), r.end(), EntityDistanceOrdering(m_EntityData, pos));
+ std::stable_sort(r.begin(), r.end(), EntityDistanceOrdering(m_EntityData, pos, q.rangeError));
return r;
}
@@ -1061,7 +1064,7 @@ public:
q.lastMatch = r;
// Return the list sorted by distance from the entity
- std::stable_sort(r.begin(), r.end(), EntityDistanceOrdering(m_EntityData, pos));
+ std::stable_sort(r.begin(), r.end(), EntityDistanceOrdering(m_EntityData, pos, q.rangeError));
return r;
}
@@ -1145,7 +1148,7 @@ public:
continue;
if (cmpSourcePosition && cmpSourcePosition->IsInWorld())
- std::stable_sort(added.begin(), added.end(), EntityDistanceOrdering(m_EntityData, cmpSourcePosition->GetPosition2D()));
+ std::stable_sort(added.begin(), added.end(), EntityDistanceOrdering(m_EntityData, cmpSourcePosition->GetPosition2D(),query.rangeError));
messages.resize(messages.size() + 1);
std::pair& back = messages.back();
@@ -1404,7 +1407,7 @@ public:
Query ConstructQuery(entity_id_t source,
entity_pos_t minRange, entity_pos_t maxRange,
- const std::vector& owners, int requiredInterface, u8 flagsMask, bool accountForSize) const
+ const std::vector& owners, int requiredInterface, u8 flagsMask, bool accountForSize, u8 rangeError = 0) const
{
// Min range must be non-negative.
if (minRange < entity_pos_t::Zero())
@@ -1453,6 +1456,7 @@ public:
LOGWARNING("CCmpRangeManager: No owners in query for entity %u", source);
q.interface = requiredInterface;
+ q.rangeError = rangeError;
q.flagsMask = flagsMask;
return q;
@@ -1460,9 +1464,9 @@ public:
Query ConstructParabolicQuery(entity_id_t source,
entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t yOrigin,
- const std::vector& owners, int requiredInterface, u8 flagsMask, bool accountForSize) const
+ const std::vector& owners, int requiredInterface, u8 flagsMask, bool accountForSize, u8 rangeError = 0) const
{
- Query q = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, flagsMask, accountForSize);
+ Query q = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, flagsMask, accountForSize, rangeError);
q.parabolic = true;
q.yOrigin = yOrigin;
return q;
diff --git a/source/simulation2/components/ICmpRangeManager.h b/source/simulation2/components/ICmpRangeManager.h
index 5200d01aa3..b5c9e535c3 100644
--- a/source/simulation2/components/ICmpRangeManager.h
+++ b/source/simulation2/components/ICmpRangeManager.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2022 Wildfire Games.
+/* Copyright (C) 2024 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -132,7 +132,7 @@ public:
* @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, bool accountForSize) = 0;
+ const std::vector& owners, int requiredInterface, bool accountForSize, u8 rangeError = 0) = 0;
/**
* Execute a passive query.
@@ -145,7 +145,7 @@ public:
* @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, bool accountForSize) = 0;
+ const std::vector& owners, int requiredInterface, bool accountForSize, u8 rangeError = 0) = 0;
/**
* Construct an active query. The query will be disabled by default.
@@ -159,7 +159,7 @@ public:
* @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, bool accountForSize) = 0;
+ const std::vector& owners, int requiredInterface, u8 flags, bool accountForSize, u8 rangeError = 0) = 0;
/**
* Construct an active query of a paraboloic form around the unit.
@@ -177,7 +177,7 @@ public:
* @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 yOrigin,
- const std::vector& owners, int requiredInterface, u8 flags) = 0;
+ const std::vector& owners, int requiredInterface, u8 flags, u8 rangeError = 0) = 0;
/**