Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,11 @@ jobs:
scons -j$(nproc) release=1 server=0 resource-fetch-target-test
python3 test/run-savegame-safety-tests.py --check-preferences build/src/ResourceFetchTargetHarness .

- name: Build and run the round-trip hunger gate regression
run: |
scons -j$(nproc) release=1 server=0 round-trip-hunger-gate-test
python3 test/run-savegame-safety-tests.py --check-preferences build/src/RoundTripHungerGateHarness .

- name: Test savegame loading and atomic autosaves
run: |
scons -j$(nproc) release=1 server=0 savegame-safety-test buffered-file-test
Expand Down
3 changes: 3 additions & 0 deletions src/FileFormatVersions.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ static constexpr int FILE_FORMAT_VERSION_PENDING_CONSTRUCTION = 90;
//! Saved-game live RNG, occupancy, fog buffers and cached routing fields.
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;

// === Save-file section signatures (4-byte ASCII tags) ===
// Embedded as four chars at the start of each save section so a corrupted
// stream fails fast. NEVER change these values — old saves on disk depend
Expand Down
2 changes: 1 addition & 1 deletion src/Game_io.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ bool Game::load(GAGCore::InputStream *stream)
std::istringstream input(state.str());
input.imbue(std::locale::classic());
if (!(input >> savedRandom)) return false;
map.loadRuntimeState(stream);
map.loadRuntimeState(stream, versionMinor);
}
gameSection.commit();

Expand Down
8 changes: 4 additions & 4 deletions src/ReplayReader.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ 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 and 94 changed the simulation (weighted pathfinding and diagonal timing,
//! hiring-bucket iteration, trapped-colony elimination, fetch-job apportionment),
//! so earlier replays would diverge from what happened.
static constexpr Uint16 REPLAY_MINIMUM_VERSION_MINOR = 94;
//! 93, 94 and 95 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;

/// This class is used for reading replays.
/// The replay stream is kept open and read every time you do retrieveOrder.
Expand Down
7 changes: 7 additions & 0 deletions src/SConscript
Original file line number Diff line number Diff line change
Expand Up @@ -662,3 +662,10 @@ if not env['server']:
regression_sources += local.Object('BuildingExpelHarness.o', '#test/BuildingExpelHarness.cpp')
regression_test = local.Program('BuildingExpelHarness', regression_sources)
local.Alias('building-expel-test', regression_test)

# Real engine regression for the resource hunger gate, built only by its explicit target.
if not env['server']:
regression_sources = [source for source in source_files if source != 'Glob2.cpp']
regression_sources += local.Object('RoundTripHungerGateHarness.o', '#test/RoundTripHungerGateHarness.cpp')
regression_test = local.Program('RoundTripHungerGateHarness', regression_sources)
local.Alias('round-trip-hunger-gate-test', regression_test)
4 changes: 3 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 94
#define VERSION_MINOR 95
// 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 @@ -100,6 +100,8 @@
// version 94 apportions fetch jobs across the resources a building wants instead of
// letting the nearest one take every slot, and prices a loaded candidate
// 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

//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
16 changes: 16 additions & 0 deletions src/building/Building.h
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,10 @@ class Building : public BuildingUtils
Unit* choosen;
};

/// Lets test/RoundTripHungerGateHarness.cpp reach considerUnitForResource
/// without exposing it to game callers, as GameGUI does for its own harness.
friend class RoundTripHungerGateHarness;

/// Whether a unit is a possible hire at all: harvest-capable, idle, healthy,
/// high enough level, and close enough to reach this building before going
/// hungry. Fills *distBuilding on success; on failure tallies the rejection
Expand Down Expand Up @@ -586,6 +590,18 @@ class Building : public BuildingUtils
Uint32 lastGlobalGradientUpdateStepCounter[SWIM_CLASS_COUNT];
// These flags track physical access (cannot swim / can swim), not travel cost.
// All swimming classes share passability, but keep separate weighted fields.
//! Last step a unit asked for the gradient; freeIdleGradients drops it when that is long ago.
Uint32 globalGradientUsedStep[SWIM_CLASS_COUNT];
//! Round-trip gradients per resource type and swim class (see Map::roundTripGradient),
//! NULL until a unit fetching that resource for this building asks for one, freed again
//! by freeIdleGradients when unused for a while. Their last rebuild and last
//! use, in steps.
Uint16 *roundTripGradient[MAX_NB_RESOURCES][SWIM_CLASS_COUNT];
Uint32 roundTripGradientStep[MAX_NB_RESOURCES][SWIM_CLASS_COUNT];
Uint32 roundTripGradientUsedStep[MAX_NB_RESOURCES][SWIM_CLASS_COUNT];
//! Drop the building's and the round-trip gradients nobody asked for lately. Only
//! buildings with fetchers need one, and each is a full map of Uint16.
void freeIdleGradients();
bool locked[SWIM_VARIANT_COUNT]; //True if the building is not reachable.

// Per-swim-variant tri-state cache of whether a clearing flag has any
Expand Down
39 changes: 39 additions & 0 deletions src/building/Lifecycle.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@
Building::Building(GAGCore::InputStream *stream, BuildingsTypes *types, Team *owner, Sint32 versionMinor)
{
for (int i=0; i<SWIM_CLASS_COUNT; i++)
{
globalGradient[i]=NULL;
for (int r=0; r<MAX_NB_RESOURCES; r++)
roundTripGradient[r][i]=NULL;
}
freeGradients();
load(stream, types, owner, versionMinor);
}
Expand Down Expand Up @@ -112,7 +116,11 @@ Building::Building(int x, int y, Uint16 gid, Sint32 typeNum, Team *team, Buildin
inUpgrade[i]=LS_UNKNOWN;

for (int i=0; i<SWIM_CLASS_COUNT; i++)
{
globalGradient[i]=NULL;
for (int r=0; r<MAX_NB_RESOURCES; r++)
roundTripGradient[r][i]=NULL;
}
freeGradients();

verbose=false;
Expand Down Expand Up @@ -148,14 +156,45 @@ void Building::resetPathfindGradients()
{
delete[] globalGradient[i];
globalGradient[i] = NULL;
for (int r=0; r<MAX_NB_RESOURCES; r++)
{
delete[] roundTripGradient[r][i];
roundTripGradient[r][i] = NULL;
roundTripGradientStep[r][i] = 0;
roundTripGradientUsedStep[r][i] = 0;
}
}
}

void Building::freeIdleGradients()
{
// Units keep a gradient alive by reading it; 500 ticks after the last one, it goes.
constexpr Uint32 IDLE_TICKS = 500;
Uint32 now = owner->game->stepCounter;
for (int c=0; c<SWIM_CLASS_COUNT; c++)
{
if (globalGradient[c] && globalGradientUsedStep[c]+IDLE_TICKS<now)
{
delete[] globalGradient[c];
globalGradient[c] = NULL;
}
for (int r=0; r<MAX_NB_RESOURCES; r++)
if (roundTripGradient[r][c] && roundTripGradientUsedStep[r][c]+IDLE_TICKS<now)
{
delete[] roundTripGradient[r][c];
roundTripGradient[r][c] = NULL;
}
}
}

void Building::freeGradients()
{
resetPathfindGradients();
for (int i=0; i<SWIM_CLASS_COUNT; i++)
{
lastGlobalGradientUpdateStepCounter[i] = 0;
globalGradientUsedStep[i] = 0;
}
for (int i=0; i<SWIM_VARIANT_COUNT; i++)
anyResourceToClear[i] = 0;
}
Expand Down
14 changes: 13 additions & 1 deletion src/building/Step.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <list>
#include <math.h>
#include <stdlib.h>
#include <algorithm>
#include <climits>

#include "Building.h"
Expand Down Expand Up @@ -39,6 +40,8 @@ namespace
void Building::step(void)
{
computeWishedResources(wishedResources);
if (((owner->game->stepCounter + gid) & 255) == 0)
freeIdleGradients();

updateCallLists();
if(underAttackTimer>0)
Expand Down Expand Up @@ -103,7 +106,16 @@ bool Building::considerUnitForResource(Unit* unit, int wantedResource, int* dist
return false;
}

*dist = (distBuilding + distResource)<<Q8_FIXED_POINT_SHIFT;
// Score by the whole job: the round-trip field when a fetcher has already
// built one. Without one, estimate the carry leg rather than reach for the
// building distance alone: a unit standing at the building carries as far
// as it walked out, and one standing at the resource carries the building
// distance. Building a field here instead would cost one per resource of
// every hiring building, nearly all of them never fetched.
int roundTrip = 0;
if(!owner->map->roundTripDistance(this, wantedResource, unit->swimClass(), unit->posX, unit->posY, &roundTrip))
roundTrip = distResource + std::max(distBuilding, distResource);
*dist = roundTrip<<Q8_FIXED_POINT_SHIFT;
return true;
}

Expand Down
32 changes: 25 additions & 7 deletions src/map/Map.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ class Map
{
public:
void saveRuntimeState(GAGCore::OutputStream *stream) const;
void loadRuntimeState(GAGCore::InputStream *stream);
void loadRuntimeState(GAGCore::InputStream *stream, Sint32 versionMinor);
//! Type of terrain (used for undermap)

// === Tile geometry (cross-slice) ===
Expand Down Expand Up @@ -598,6 +598,8 @@ class Map
static constexpr int SWIM_CLASS_EVEN = 3;
//! Cheapest possible step for a class, the A* heuristic unit.
static int minStepCost(int swimClass);
//! Highest cost a gradient can hold (see MapInternal.h).
static constexpr int GRADIENT_COST_LIMIT = 0xFFFF - 1 - 1 - 42;
//! Cost of stepping (dx, dy) into the cell at targetIndex, in gradient units.
int stepCost(int dx, int dy, size_t targetIndex, int swimClass) const;

Expand Down Expand Up @@ -629,22 +631,38 @@ class Map
// the pathfinding gradients are built by propagateGradient. Defined in
// MapGradientGlobal.cpp.
void updateGlobalGradient(Uint8 *gradient);
//! Dijkstra on a freshly seeded field (see MapInternal.h). Seed costs must be
//! between 0 and the largest terrain step (currently 42); do not pass a completed
//! field. Uses shared scratch storage: calls across all Maps must be serial and
//! non-reentrant. swimClass must be in [0, SWIM_CLASS_COUNT).
void propagateGradient(Uint16 *gradient, int swimClass);
//! Dijkstra from every seeded cell of a pathfinding gradient (see MapInternal.h).
//! Seeds may carry any cost up to GRADIENT_COST_LIMIT (0 for GRADIENT_AT_GOAL; e.g. a
//! resource tile seeded with its distance to a building); do not pass a completed
//! field. With maxCost, cells that would cost more stay unreachable. Uses shared
//! scratch storage: calls across all Maps must be serial and non-reentrant.
//! swimClass must be in [0, SWIM_CLASS_COUNT).
void propagateGradient(Uint16 *gradient, int swimClass, int maxCost = GRADIENT_COST_LIMIT);
//! 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);
bool pathfindResource(int teamNumber, Uint8 resourceType, int swimClass, int x, int y, int *dx, int *dy, bool *stopWork);
//! 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);
#ifndef YOG_SERVER_ONLY
void pathfindRandom(Unit *unit);
#endif // !YOG_SERVER_ONLY

//! Rebuild the building's full-map gradient for a swim class.
void updateGlobalGradient(Building *building, int swimClass);
//! Rebuild the building's round-trip gradient for a resource type and swim class:
//! every tile of that resource is seeded with its distance to the building, so a
//! cell's value is the cheapest fetch-and-carry trip from there.
void updateRoundTripGradient(Building *building, int resourceType, int swimClass);
//! The building's round-trip gradient, built or refreshed on demand. NULL when the
//! building cannot be reached.
const Uint16 *roundTripGradient(Building *building, int resourceType, int swimClass);
//! Tiles of the cheapest trip from (x, y) to a resource of resourceType and on to the
//! building, read from a round-trip gradient a fetcher's walk has already built. False
//! when there is none or no such trip; the caller then scores by the plain distances.
bool roundTripDistance(Building *building, int resourceType, int swimClass, int x, int y, int *dist);
//! The building's gradient for a swim class, built or refreshed as needed; NULL if the building is unreachable.
const Uint16 *buildingGradient(Building *building, int swimClass);
bool buildingAvailable(Building *building, int swimClass, int x, int y, int *dist);
Expand Down
41 changes: 41 additions & 0 deletions src/map/gradient/MapGradientBuilding.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

// updateGlobalGradient(Building*): the full-map gradient toward a building, a
// flag's zone or, for a clearing flag, the clearable resources in its range.
// updateRoundTripGradient: the gradient of the trip to a resource and on to
// the building.

void Map::updateGlobalGradient(Building *building, int swimClass)
{
Expand Down Expand Up @@ -126,3 +128,42 @@ void Map::updateGlobalGradient(Building *building, int swimClass)

propagateGradient(gradient, swimClass);
}


void Map::updateRoundTripGradient(Building *building, int resourceType, int swimClass)
{
Uint16 *gradient=building->roundTripGradient[resourceType][swimClass];
assert(gradient);
building->roundTripGradientStep[resourceType][swimClass]=game->stepCounter;
const Uint16 *toBuilding=building->globalGradient[swimClass];
const Uint16 *toResource=getResourceGradient(building->owner->teamNumber, resourceType, swimClass);
// Same obstacles as the resource gradient. A resource tile is seeded with
// the cost of carrying from the cheapest free cell next to it, where the
// unit harvests, to the building.
Uint16 bestSeed=GRADIENT_UNREACHABLE;
for (size_t i=0; i<size; i++)
{
if (toResource[i]!=GRADIENT_AT_GOAL)
{
gradient[i]=toResource[i]==GRADIENT_FORBIDDEN ? GRADIENT_FORBIDDEN : GRADIENT_UNREACHABLE;
continue;
}
size_t x=i&wMask;
size_t y=i>>wDec;
Uint16 best=GRADIENT_UNREACHABLE;
for (int d=0; d<8; d++)
{
size_t n=coordToIndex(x+tabClose[d][0], y+tabClose[d][1]);
if (toResource[n]>GRADIENT_UNREACHABLE && toBuilding[n]>best)
best=toBuilding[n];
}
gradient[i]=best;
if (best>bestSeed)
bestSeed=best;
}
// Units farther than this from the cheapest fetch are scored by the plain
// distances instead (the callers fall back when a cell is unreachable
// here), which keeps the build small on big maps.
constexpr int ROUND_TRIP_RANGE=128*GRADIENT_STEP;
propagateGradient(gradient, swimClass, GRADIENT_AT_GOAL-bestSeed+ROUND_TRIP_RANGE);
}
Loading
Loading