Avoid copying fire-free maps for water crossing (#8775)

This commit is contained in:
2026-07-24 10:12:00 -07:00
committed by GitHub
parent f672126a00
commit 78e5d25b89
2 changed files with 53 additions and 18 deletions
@@ -5,6 +5,7 @@
#include "AIWaterCrossingCalculator.hpp"
#include <algorithm>
#include <array>
#include "AIMinimumDistanceAndTarget.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/HexMapHelpers.hpp"
@@ -22,6 +23,19 @@ auto RecomputeModifierHash(HexMap *hexMap) -> MapId {
return ActionPointDistancesCache::GetMapId(hexMap);
}
auto HasNonWaterFire(const HexMap *hexMap) -> bool {
return std::ranges::any_of(*hexMap->terrain(), [](const auto *terrain) {
return !IsWater(terrain->type()) && terrain->modifier().fire().present();
});
}
auto CanReachWithDistances(
const Coords &origin,
const Coords &destination,
const ActionPointDistances *distances) -> bool {
return distances->Distance(origin, destination) != ActionPointDistances::IMPOSSIBLE;
}
auto CanCreateWaterCrossing(
const net::eagle0::shardok::storage::fb::Unit *unit,
const BattalionTypeGetter &battalionTypeGetter) -> bool {
@@ -43,19 +57,36 @@ auto UnitIdsRequiringWaterCrossing(
const CoordsSet &destinations,
const APDCache &apdCache,
const BattalionTypeGetter &battalionTypeGetter) -> vector<UnitId> {
// Put out all the fires, except on bridges
fb::HexMapW mapCopy = fb::CopyHexMap(gameState->hex_map());
for (uint32_t index = 0; index < mapCopy->terrain()->size(); ++index) {
if (IsWater(mapCopy->terrain()->Get(index)->type())) continue;
// const_cast is safe because we own the mutable buffer (mapCopy)
const_cast<net::eagle0::shardok::storage::fb::Terrain *>(
mapCopy->mutable_terrain()->GetMutableObject(index))
->mutable_modifier()
.mutable_fire()
.mutate_present(false);
const HexMap *map = gameState->hex_map();
MapId mapId = ActionPointDistancesCache::GetMapId(map);
// Most states have no relevant fire, so avoid copying the entire map unless it needs mutation.
fb::HexMapW fireClearedMap;
if (HasNonWaterFire(map)) {
fireClearedMap = fb::CopyHexMap(map);
for (uint32_t index = 0; index < fireClearedMap->terrain()->size(); ++index) {
if (IsWater(fireClearedMap->terrain()->Get(index)->type())) continue;
// const_cast is safe because we own the mutable buffer (fireClearedMap)
const_cast<net::eagle0::shardok::storage::fb::Terrain *>(
fireClearedMap->mutable_terrain()->GetMutableObject(index))
->mutable_modifier()
.mutable_fire()
.mutate_present(false);
}
map = fireClearedMap.Get();
mapId = RecomputeModifierHash(fireClearedMap.Get());
}
const auto mapId = RecomputeModifierHash(mapCopy.Get());
std::array<const ActionPointDistances *, BattalionTypeId::BattalionTypeId_MAX + 1>
distancesByBattalionType{};
const auto distancesFor = [&](const BattalionTypeSPtr &battalionType) {
auto &distances = distancesByBattalionType[battalionType->typeId];
if (distances == nullptr) {
distances = apdCache->GetRaw(map, mapId, battalionType, false);
}
return distances;
};
vector<UnitId> unitIdsToReturn{};
@@ -67,8 +98,9 @@ auto UnitIdsRequiringWaterCrossing(
const auto &battType = battalionTypeGetter(unit->battalion().type());
if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
const auto *distances = distancesFor(battType);
for (const Coords &destination : destinations) {
if (!CanReach(unit->location(), destination, mapCopy, mapId, apdCache, battType)) {
if (!CanReachWithDistances(unit->location(), destination, distances)) {
unitIdsToReturn.push_back(unit->unit_id());
break;
}
@@ -76,6 +108,7 @@ auto UnitIdsRequiringWaterCrossing(
} else if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT) {
if (startingPositionIndices[unit->starting_position_index()] == -1) {
startingPositionIndices[unit->starting_position_index()] = 0;
const auto *distances = distancesFor(battType);
const auto *startingPositions = gameState->hex_map()
->attacker_starting_positions()
->Get(unit->starting_position_index())
@@ -83,7 +116,7 @@ auto UnitIdsRequiringWaterCrossing(
for (const Coords *pos : *startingPositions) {
for (const Coords &destination : destinations) {
if (!CanReach(*pos, destination, mapCopy, mapId, apdCache, battType)) {
if (!CanReachWithDistances(*pos, destination, distances)) {
startingPositionIndices[unit->starting_position_index()] = 1;
break;
}
@@ -128,10 +161,10 @@ auto CanReach(
const MapId &mapId,
const APDCache &apdCache,
const BattalionTypeSPtr &battalionType) -> bool {
const DIST_T startingDistance =
apdCache->GetRaw(hexMap, mapId, battalionType, false)->Distance(origin, destination);
return startingDistance != ActionPointDistances::IMPOSSIBLE;
return CanReachWithDistances(
origin,
destination,
apdCache->GetRaw(hexMap, mapId, battalionType, false));
}
// Returns the *non*-water tiles from which you could bridge/freeze water to allow a unit to cross
@@ -165,7 +165,8 @@ TEST_F(AIWaterCrossingCalculatorTest,
battalionTypeGetter);
EXPECT_EQ(1, ActionPointDistancesCache::GetThreadLocalCacheSize());
MutableTerrain(hexMap, Coords(0, 0))->mutable_modifier().mutable_fire().mutate_present(true);
auto* fireTerrain = MutableTerrain(hexMap, Coords(0, 0));
fireTerrain->mutable_modifier().mutable_fire().mutate_present(true);
hexMap->mutate_modifier_hash(0);
hexMap->mutate_modifier_hash(ActionPointDistancesCache::GetMapId(hexMap).modifierId);
@@ -177,6 +178,7 @@ TEST_F(AIWaterCrossingCalculatorTest,
battalionTypeGetter);
EXPECT_EQ(beforeFire, afterFire);
EXPECT_EQ(1, ActionPointDistancesCache::GetThreadLocalCacheSize());
EXPECT_TRUE(fireTerrain->modifier().fire().present());
}
TEST_F(AIWaterCrossingCalculatorTest, CanReach_sameSide_returnsTrue) {