Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ jobs:
scons -j$(nproc) release=1 server=0 hiring-bucket-test
python3 test/run-savegame-safety-tests.py --check-preferences build/src/HiringBucketHarness .

- name: Build and run the market-fetch regression
run: |
scons -j$(nproc) release=1 server=0 market-fetch-test
python3 test/run-savegame-safety-tests.py --check-preferences build/src/MarketFetchHarness .
- name: Build and run the resource-fetch target regression
run: |
scons -j$(nproc) release=1 server=0 resource-fetch-target-test
Expand Down
1 change: 1 addition & 0 deletions .local/state/gh/device-id
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
9f877a11-305a-44a7-ab94-ee15a2cc948c
3 changes: 3 additions & 0 deletions src/FileFormatVersions.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ static constexpr int FILE_FORMAT_VERSION_CONTINUATION_STATE = 91;

//! A building's round-trip fields and gradient use stamps join the cached routing fields.
static constexpr int FILE_FORMAT_VERSION_ROUND_TRIP_FIELDS = 95;
/// The "with markets" twin of each resource gradient travels with the map
/// runtime state, like the plain one.
static constexpr int FILE_FORMAT_VERSION_MARKET_GRADIENTS = 98;

// === Save-file section signatures (4-byte ASCII tags) ===
// Embedded as four chars at the start of each save section so a corrupted
Expand Down
8 changes: 5 additions & 3 deletions src/ReplayReader.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ static constexpr Uint32 REPLAY_MIN_VALID_ORDERS = 5;

//! Oldest replay format (the VERSION_MINOR the replay was written with) that
//! the reader still accepts. Replays older than this are rejected: versions 90, 92,
//! 93, 94 and 95 changed the simulation (weighted pathfinding and diagonal timing,
//! 93, 94, 95, 96, 97 and 98 changed the simulation (weighted pathfinding and diagonal timing,
//! hiring-bucket iteration, trapped-colony elimination, fetch-job apportionment,
//! round-trip routing and hiring), so earlier replays would diverge from what happened.
static constexpr Uint16 REPLAY_MINIMUM_VERSION_MINOR = 95;
//! round-trip routing and hiring, Echo building-order ids surviving a load,
//! per-delivery hiring, markets on the fetch gradients), so earlier replays
//! would diverge from what happened.
static constexpr Uint16 REPLAY_MINIMUM_VERSION_MINOR = 98;

/// This class is used for reading replays.
/// The replay stream is kept open and read every time you do retrieveOrder.
Expand Down
6 changes: 6 additions & 0 deletions src/SConscript
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,12 @@ if not env['server'] and 'hiring-bucket-test' in COMMAND_LINE_TARGETS:
hiring_sources += local.Object('HiringBucketHarness.o', '#test/HiringBucketHarness.cpp')
local.Alias('hiring-bucket-test', local.Program('HiringBucketHarness', hiring_sources))

# A building's fetch is led to a stocked market of its team when that is the shorter trip.
if not env['server'] and 'market-fetch-test' in COMMAND_LINE_TARGETS:
market_sources = [source for source in source_files if source != 'Glob2.cpp']
market_sources += local.Object('MarketFetchHarness.o', '#test/MarketFetchHarness.cpp')
local.Alias('market-fetch-test', local.Program('MarketFetchHarness', market_sources))

if not env['server'] and 'custom-setup-test' in COMMAND_LINE_TARGETS:
custom_sources = [source for source in source_files if source != 'Glob2.cpp']
custom_sources += local.Object('CustomGameSetupHarness.o', '#test/CustomGameSetupHarness.cpp')
Expand Down
11 changes: 10 additions & 1 deletion src/Version.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// This is the version of map and savegame format, and all of the recorded data on the server
#define VERSION_MAJOR 0
#define MINIMUM_VERSION_MINOR 58
#define VERSION_MINOR 95
#define VERSION_MINOR 98
// version 91 saves the live RNG and routing state for deterministic continuation.
// version 10 adds script saved in game
// version 11 the gamesfiles do saves which building has been seen under fog of war.
Expand Down Expand Up @@ -102,6 +102,15 @@
// rather than refusing it: the simulation changed again
// version 95 routes and hires by round trip (fetch plus carry) and saves the
// round-trip fields with the map runtime state: the simulation changed again
// version 96 saves AIEcho::Construction::BuildingOrder::id, which was assigned at
// runtime and never serialised, so every pending building order restored
// from a save carried an uninitialised heap value as its register key
// version 97 hires a fetcher for one delivery at a time: a unit that has just
// dropped off goes back to the free pool instead of re-hiring itself
// for its building's next trip, so every trip is auctioned
// version 98 puts the team's stocked markets into the resource gradients a
// building's fetchers walk and are hired by, so the market path
// survives per-delivery hiring: the simulation changed again

//This must be updated when there are changes to YOG, MapHeader, GameHeader, BasePlayer, BaseTeam,
//NetMessage, and the likes, in parallel to change of the VERSION_MINOR above
Expand Down
4 changes: 4 additions & 0 deletions src/ai/echo/BuildingOrder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ bool BuildingOrder::load(GAGCore::InputStream *stream, Player *player, Sint32 ve

building_type=stream->readUint32("building_type");
number_of_workers=stream->readUint32("number_of_workers");
// Saves older than 96 do not carry it; Echo::load registers a fresh one.
if (versionMinor>=96)
id=static_cast<int>(stream->readUint32("id"));

stream->readEnterSection("constraints");
Uint32 size = stream->readUint32("size");
Expand Down Expand Up @@ -62,6 +65,7 @@ void BuildingOrder::save(GAGCore::OutputStream *stream)

stream->writeUint32(building_type, "building_type");
stream->writeUint32(number_of_workers, "number_of_workers");
stream->writeUint32(static_cast<Uint32>(id), "id");

stream->writeEnterSection("constraints");
stream->writeUint32(constraints.size(), "size");
Expand Down
6 changes: 5 additions & 1 deletion src/ai/echo/Construction.h
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,11 @@ namespace AIEcho
int get_number_of_workers() const { return number_of_workers; }
int building_type;
int number_of_workers;
int id;
/// Assigned by Echo::add_building_order from BuildingRegister, and the key
/// this order is known by in BuildingRegister::pending_buildings. Defaulted
/// so an order that is constructed and never registered is still readable;
/// load() leaves it at -1 for saves written before it was serialised.
int id = -1;
std::vector<std::shared_ptr<Constraint> > constraints;
std::vector<std::shared_ptr<Conditions::Condition> > conditions;
};
Expand Down
6 changes: 6 additions & 0 deletions src/ai/echo/EchoSerialization.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ bool Echo::load(GAGCore::InputStream *stream, Player *player, Sint32 versionMino
stream->readEnterSection(buildingIndex);
building_orders[buildingIndex]=std::shared_ptr<BuildingOrder>(new BuildingOrder);
building_orders[buildingIndex]->load(stream, player, versionMinor);
// A save from before the id was serialised leaves it at -1. Hand out a
// fresh registration rather than a sentinel: the id is used as a
// BuildingRegister map key and passed to AssignWorkers, so it has to be
// a real one. br is already loaded at this point.
if (building_orders[buildingIndex]->id < 0)
building_orders[buildingIndex]->id = static_cast<int>(br.register_building());
stream->readLeaveSection();
}
stream->readLeaveSection();
Expand Down
4 changes: 4 additions & 0 deletions src/building/Building.h
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,10 @@ class Building : public BuildingUtils
bool subscribeToBringResourcesStep(void);
//! Whether the unit's type and level qualify it to work for this building.
bool canUnitWorkHere(Unit* unit);
/// Whether fetches for this building may take from the team's stocked
/// markets. Markets fetch for themselves from the map only, so stock never
/// circulates between markets.
bool fetchesFromMarkets() const;
///This function subscribes any flag that needs units.
///It is considered greedy, hiring as many units as it needs in order of its preference
///Returns true if a unit was hired
Expand Down
13 changes: 13 additions & 0 deletions src/building/Misc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ void Building::kill(void)
}

buildingState=DEAD;
if (type->canExchange)
for (int r=0; r<MAX_NB_RESOURCES; r++)
if (resources[r]>0)
owner->map->dirtyMarketGradients(owner->teamNumber, r);

updateUnitsHarvesting();

Expand All @@ -112,6 +116,11 @@ void Building::kill(void)
}


bool Building::fetchesFromMarkets() const
{
return !type->canExchange;
}

bool Building::canUnitWorkHere(Unit* unit)
{
if(type->isVirtual)
Expand Down Expand Up @@ -192,6 +201,8 @@ void Building::updateResourcesPointer()

void Building::addResourceIntoBuilding(int resourceType)
{
if (type->canExchange && resources[resourceType]<=0)
owner->map->dirtyMarketGradients(owner->teamNumber, resourceType);
resources[resourceType]+=type->multiplierResource[resourceType];
//You can not exceed the maximum amount
resources[resourceType] = std::min(resources[resourceType], type->maxResource[resourceType]);
Expand Down Expand Up @@ -232,6 +243,8 @@ void Building::removeResourceFromBuilding(int resourceType)
{
resources[resourceType]-=type->multiplierResource[resourceType];
resources[resourceType]= std::max(resources[resourceType], 0);
if (type->canExchange && resources[resourceType]<=0)
owner->map->dirtyMarketGradients(owner->teamNumber, resourceType);
updateCallLists();
}

Expand Down
2 changes: 1 addition & 1 deletion src/building/Step.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ bool Building::considerUnitForResource(Unit* unit, int wantedResource, int* dist
int timeLeft=(unit->hungry-unit->trigHungry)/unit->race->hungriness;
int distResource = 0;
if(!owner->map->resourceAvailable(owner->teamNumber, wantedResource, unit->swimClass(),
unit->posX, unit->posY, &distResource))
unit->posX, unit->posY, &distResource, fetchesFromMarkets()))
{
if(wantedResource<BASIC_COUNT)
unitsFailingRequirements[UnitCantAccessResource] += 1;
Expand Down
5 changes: 5 additions & 0 deletions src/map/Map.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ Map::Map()
{
resourcesGradient[t][r][s] = NULL;
gradientUpdated[t][r][s] = false;
marketResourcesGradient[t][r][s] = NULL;
marketGradientDirty[t][r][s] = false;
}
for (int t=0; t<Team::MAX_COUNT; t++)
for (int s=0; s<SWIM_CLASS_COUNT; s++)
Expand Down Expand Up @@ -102,6 +104,9 @@ void Map::clear()
delete[] resourcesGradient[t][r][swim];
resourcesGradient[t][r][swim] = NULL;
gradientUpdated[t][r][swim] = false;
delete[] marketResourcesGradient[t][r][swim];
marketResourcesGradient[t][r][swim] = NULL;
marketGradientDirty[t][r][swim] = false;
}
for (int swim=0; swim<SWIM_CLASS_COUNT; ++swim)
{
Expand Down
31 changes: 23 additions & 8 deletions src/map/Map.h
Original file line number Diff line number Diff line change
Expand Up @@ -605,14 +605,22 @@ class Map

// Gradients are built per team and swim class the first time a unit of that
// class asks for one, so classes nobody uses cost nothing.
Uint16 *getResourceGradient(int teamNumber, int resourceType, int swimClass);
//! withMarkets: the variant where the team's stocked markets are goals too,
//! priced MARKET_DETOUR_TILES beyond a tile of the resource. Used by every
//! fetch for a building that is not itself a market (Building::fetchesFromMarkets).
Uint16 *getResourceGradient(int teamNumber, int resourceType, int swimClass, bool withMarkets = false);
Uint16 *getForbiddenGradient(int teamNumber, int swimClass);
Uint16 *getGuardAreasGradient(int teamNumber, int swimClass);
Uint16 *getClearAreasGradient(int teamNumber, int swimClass);

bool resourceAvailable(int teamNumber, int resourceType, int swimClass, int x, int y);
bool resourceAvailable(int teamNumber, int resourceType, int swimClass, int x, int y, int *dist);
bool resourceAvailableUpdate(int teamNumber, int resourceType, int swimClass, int x, int y, Sint32 *targetX, Sint32 *targetY, int *dist);
bool resourceAvailable(int teamNumber, int resourceType, int swimClass, int x, int y, bool withMarkets = false);
bool resourceAvailable(int teamNumber, int resourceType, int swimClass, int x, int y, int *dist, bool withMarkets = false);
bool resourceAvailableUpdate(int teamNumber, int resourceType, int swimClass, int x, int y, Sint32 *targetX, Sint32 *targetY, int *dist, bool withMarkets = false);
//! The team's own, alive market next to the unit that holds resourceType, or NULL.
Building *touchedStockedMarket(Unit *unit, int resourceType) const;
//! A stock of resourceType in one of the team's markets appeared or ran out:
//! rebuild the "with markets" gradients for it at the next step.
void dirtyMarketGradients(int teamNumber, int resourceType);

//! Follow the gradient uphill from (x, y). Returns whether a goal cell was reached; the
//! last position is in (targetX, targetY). Works on the Uint16 pathfinding gradients and
Expand All @@ -625,9 +633,9 @@ class Map
template<typename T>
bool isGradientPeak(const T *gradient, int x, int y) const;

Uint16 getGradient(int teamNumber, Uint8 resourceType, int swimClass, int x, int y)
Uint16 getGradient(int teamNumber, Uint8 resourceType, int swimClass, int x, int y, bool withMarkets = false)
{
return getResourceGradient(teamNumber, resourceType, swimClass)[coordToIndex(x, y)];
return getResourceGradient(teamNumber, resourceType, swimClass, withMarkets)[coordToIndex(x, y)];
}

// Chamfer distance transform on a pre-seeded Uint8 buffer. Caller fills the
Expand All @@ -646,11 +654,11 @@ class Map
//! Step toward the neighbour with the highest value minus step cost. strict requires
//! real progress; otherwise a random sidestep to an equal cell is accepted when blocked.
bool directionByGradient(Uint32 teamMask, int swimClass, int x, int y, const Uint16 *gradient, int *dx, int *dy, bool strict) const;
void updateResourcesGradient(int teamNumber, Uint8 resourceType, int swimClass);
void updateResourcesGradient(int teamNumber, Uint8 resourceType, int swimClass, bool withMarkets = false);
//! Direction toward a resource of resourceType. With a target building the round-trip
//! gradient is descended, so the unit heads for the resource that is nearest for
//! fetching and carrying it there; without one, for the resource nearest to itself.
bool pathfindResource(int teamNumber, Uint8 resourceType, int swimClass, int x, int y, int *dx, int *dy, bool *stopWork, Building *target);
bool pathfindResource(int teamNumber, Uint8 resourceType, int swimClass, int x, int y, int *dx, int *dy, bool *stopWork, Building *target, bool withMarkets = false);
#ifndef YOG_SERVER_ONLY
void pathfindRandom(Unit *unit);
#endif // !YOG_SERVER_ONLY
Expand Down Expand Up @@ -746,6 +754,8 @@ class Map
// Used to go to resources
//[int team][int resourceNumber][int swimClass]
Uint16 *resourcesGradient[Team::MAX_COUNT][MAX_NB_RESOURCES][SWIM_CLASS_COUNT];
//! Same, with the team's stocked markets as goals (see getResourceGradient).
Uint16 *marketResourcesGradient[Team::MAX_COUNT][MAX_NB_RESOURCES][SWIM_CLASS_COUNT];

// Used to go out of forbidden areas
Uint16 *forbiddenGradient[Team::MAX_COUNT][SWIM_CLASS_COUNT];
Expand Down Expand Up @@ -776,6 +786,11 @@ class Map
protected:
//Used for scheduling computation time.
bool gradientUpdated[Team::MAX_COUNT][MAX_NB_RESOURCES][SWIM_CLASS_COUNT];
//! A market's stock of the resource switched: rebuild the twin before its plain
//! gradient's next turn in the round robin.
bool marketGradientDirty[Team::MAX_COUNT][MAX_NB_RESOURCES][SWIM_CLASS_COUNT];
//! Whether tile gid is one of teamNumber's alive markets holding resourceType.
bool isStockedMarketTile(Uint16 gid, int teamNumber, int resourceType) const;
//Used for scheduling computation time on the guard area gradients
bool guardGradientUpdated[Team::MAX_COUNT][SWIM_CLASS_COUNT];
//Used for scheduling computation time on the clear area gradients
Expand Down
7 changes: 7 additions & 0 deletions src/map/MapInternal.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ constexpr std::uint16_t GRADIENT_FORBIDDEN = 0;
constexpr std::uint16_t GRADIENT_UNREACHABLE = 1;
constexpr std::uint16_t GRADIENT_AT_GOAL = 0xFFFF;
constexpr std::uint16_t GRADIENT_FORBIDDEN_BORDER = GRADIENT_AT_GOAL - GRADIENT_STEP;
/// Tiles a fetch out of a market is charged on top of the walk, for the
/// resource having been carried there once already. Markets sit next to what
/// they teleport, so the detour is small.
constexpr int MARKET_DETOUR_TILES = 5;
/// Seed of a stocked market's tiles in a "with markets" resource gradient: a
/// goal that costs the detour more than a tile of the resource itself.
constexpr std::uint16_t GRADIENT_MARKET_SEED = GRADIENT_AT_GOAL - MARKET_DETOUR_TILES * GRADIENT_STEP;

// Weighted cost rounded to whole land-step equivalents, for a reachable value.
// This is not a geometric tile count: water and diagonal steps change the cost.
Expand Down
23 changes: 23 additions & 0 deletions src/map/MapQuery.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
#include "Utilities.h"
#include "BuildingType.h"
#include "Unit.h"
#include "Building.h"
#include "Team.h"
#include "MapInternal.h"


Expand Down Expand Up @@ -144,6 +146,27 @@ std::optional<Offset> Map::doesUnitTouchResource(Unit *unit, int resourceType) c
return std::nullopt;
}

bool Map::isStockedMarketTile(Uint16 gid, int teamNumber, int resourceType) const
{
if (gid == NOGBID || Building::GIDtoTeam(gid) != teamNumber)
return false;
const Building *b = game->teams[teamNumber]->myBuildings[Building::GIDtoID(gid)];
return b && b->type->canExchange && b->buildingState == Building::ALIVE && b->resources[resourceType] > 0;
}

Building *Map::touchedStockedMarket(Unit *unit, int resourceType) const
{
const int teamNumber=unit->owner->teamNumber;
for (int tdx=-1; tdx<=1; tdx++)
for (int tdy=-1; tdy<=1; tdy++)
{
Uint16 gid=getBuilding(unit->posX+tdx, unit->posY+tdy);
if (isStockedMarketTile(gid, teamNumber, resourceType))
return game->teams[teamNumber]->myBuildings[Building::GIDtoID(gid)];
}
return NULL;
}

std::optional<Offset> Map::doesPosTouchResource(int x, int y, int resourceType) const
{
for (int tdx=-1; tdx<=1; tdx++)
Expand Down
16 changes: 8 additions & 8 deletions src/map/MapResources.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -161,15 +161,15 @@ void Map::setAreaName(int n, std::string name)
}


bool Map::resourceAvailable(int teamNumber, int resourceType, int swimClass, int x, int y)
bool Map::resourceAvailable(int teamNumber, int resourceType, int swimClass, int x, int y, bool withMarkets)
{
Uint16 g = getGradient(teamNumber, resourceType, swimClass, x, y);
Uint16 g = getGradient(teamNumber, resourceType, swimClass, x, y, withMarkets);
return g>GRADIENT_UNREACHABLE; //Because 0==obstacle, 1==no obstacle, but you don't know if there is anything around.
}

bool Map::resourceAvailable(int teamNumber, int resourceType, int swimClass, int x, int y, int *dist)
bool Map::resourceAvailable(int teamNumber, int resourceType, int swimClass, int x, int y, int *dist, bool withMarkets)
{
Uint16 g = getGradient(teamNumber, resourceType, swimClass, x, y);
Uint16 g = getGradient(teamNumber, resourceType, swimClass, x, y, withMarkets);
if (g>GRADIENT_UNREACHABLE)
{
*dist = gradientTiles(g);
Expand All @@ -179,17 +179,17 @@ bool Map::resourceAvailable(int teamNumber, int resourceType, int swimClass, int
return false;
}

bool Map::resourceAvailableUpdate(int teamNumber, int resourceType, int swimClass, int x, int y, Sint32 *targetX, Sint32 *targetY, int *dist)
bool Map::resourceAvailableUpdate(int teamNumber, int resourceType, int swimClass, int x, int y, Sint32 *targetX, Sint32 *targetY, int *dist, bool withMarkets)
{
// distance and availability
bool result;
if (dist)
result = resourceAvailable(teamNumber, resourceType, swimClass, x, y, dist);
result = resourceAvailable(teamNumber, resourceType, swimClass, x, y, dist, withMarkets);
else
result = resourceAvailable(teamNumber, resourceType, swimClass, x, y);
result = resourceAvailable(teamNumber, resourceType, swimClass, x, y, withMarkets);

// target position
const Uint16 *gradient = getResourceGradient(teamNumber, resourceType, swimClass);
const Uint16 *gradient = getResourceGradient(teamNumber, resourceType, swimClass, withMarkets);
getGlobalGradientDestination(gradient, x, y, targetX, targetY);

return result;
Expand Down
10 changes: 10 additions & 0 deletions src/map/MapStep.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,16 @@ void Map::syncStep(Uint32 stepCounter)
gradientUpdated[t][r][s]=true;
return;
}
// The "with markets" twins are rebuilt together with their plain gradient;
// on their own only when a market's stock switched since.
for (int t=0; t<numberOfTeam; t++)
for (int r=0; r<MAX_RESOURCES; r++)
for (int s=0; s<SWIM_CLASS_COUNT; s++)
if (marketResourcesGradient[t][r][s] && marketGradientDirty[t][r][s])
{
updateResourcesGradient(t, r, s, true);
return;
}
for (int t=0; t<numberOfTeam; t++)
for(int s=0; s<SWIM_CLASS_COUNT; s++)
if(guardAreasGradient[t][s] && !guardGradientUpdated[t][s])
Expand Down
Loading
Loading