Skip to content
Draft
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 gig-release regression
run: |
scons -j$(nproc) release=1 server=0 gig-release-test
python3 test/run-savegame-safety-tests.py --check-preferences build/src/GigReleaseHarness .
- name: Build and run the resource-fetch target regression
run: |
scons -j$(nproc) release=1 server=0 resource-fetch-target-test
Expand Down
8 changes: 4 additions & 4 deletions src/ReplayReader.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ 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, 95 and 96 changed the simulation (weighted pathfinding and diagonal timing,
//! 93, 94, 95, 96 and 97 changed the simulation (weighted pathfinding and diagonal timing,
//! hiring-bucket iteration, trapped-colony elimination, fetch-job apportionment,
//! round-trip routing and hiring, Echo building-order ids surviving a load), so
//! earlier replays would diverge from what happened.
static constexpr Uint16 REPLAY_MINIMUM_VERSION_MINOR = 96;
//! round-trip routing and hiring, Echo building-order ids surviving a load,
//! per-delivery hiring), so earlier replays would diverge from what happened.
static constexpr Uint16 REPLAY_MINIMUM_VERSION_MINOR = 97;

/// 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))

# After a delivery the worker is released only while a fifth of the team is idle.
if not env['server'] and 'gig-release-test' in COMMAND_LINE_TARGETS:
gig_sources = [source for source in source_files if source != 'Glob2.cpp']
gig_sources += local.Object('GigReleaseHarness.o', '#test/GigReleaseHarness.cpp')
local.Alias('gig-release-test', local.Program('GigReleaseHarness', gig_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
5 changes: 4 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 96
#define VERSION_MINOR 97
// 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 @@ -105,6 +105,9 @@
// 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

//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/team/Team.h
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ class Team:public BaseTeam
//! Give `unit`'s fetching job to a team mate and take the mate's job, when that
//! shortens the two trips together by more than a few tiles (see TeamStep.cpp).
void swapTask(Unit *unit);
/// Whether at least `percent` of the team's living workers are idle
/// (ACT_RANDOM). Read after a delivery to decide whether to release the
/// worker for the hiring auction.
bool idleWorkerShareAtLeast(int percent) const;
//! Give `unit` a team mate's inn and the mate `unit`'s, when that shortens the
//! two walks together by more than a few tiles; called as `unit` books its place.
void swapInn(Unit *unit);
Expand Down
15 changes: 15 additions & 0 deletions src/team/TeamStep.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,21 @@ namespace
}
}

bool Team::idleWorkerShareAtLeast(int percent) const
{
int workers=0, idle=0;
for (int i=0; i<Unit::MAX_COUNT; i++)
{
const Unit *u=myUnits[i];
if (!u || u->typeNum!=WORKER || u->isDead)
continue;
workers++;
if (u->activity==Unit::ACT_RANDOM)
idle++;
}
return workers>0 && idle*100>=percent*workers;
}

void Team::swapTask(Unit *unit)
{
if (!isFetching(unit))
Expand Down
3 changes: 3 additions & 0 deletions src/unit/UnitConsts.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ const int NB_UNIT_LEVELS=4;
//! when the counter is treated as "fully arrived" (Unit.cpp:296, 304;
//! UnitMovement.cpp; MapQuery.cpp; TypeSteps.cpp turret bullet timing).
static constexpr int UNIT_DELTA_MAX = 255;
/// Share of a team's workers that must be idle for a worker to be released
/// after a delivery instead of keeping its building (Unit::handleDisplacement).
static constexpr int RELEASE_IDLE_WORKER_PERCENT = 20;
//! Modular quantum that wraps a unit's `delta` counter, equal to
//! UNIT_DELTA_MAX + 1. Used in expressions like (256 - delta) / speed.
static constexpr int UNIT_DELTA_QUANTUM = 256;
Expand Down
14 changes: 14 additions & 0 deletions src/unit/UnitDisplacement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,20 @@ void Unit::handleDisplacement(void)
validTarget=false;
assert(needToRecheckMedical);
}
else if (owner->idleWorkerShareAtLeast(RELEASE_IDLE_WORKER_PERCENT))
{
// One delivery is one gig while there are idle hands: give the trip
// back to the pool and let Team::updateAllBuildingTasks, which runs
// later in this same tick after every unit has stepped, auction it
// among every free worker and every building that wants one. An
// idle worker may by chance stand closer to the next job than the
// one that just delivered. With nobody idle the auction could only
// hand the job back to this unit a tile later, so it keeps its
// building and picks its next trip itself, below.
if (verbose)
printf("guid=(%d) delivered, released for the auction.\n", gid);
stopAttachedForBuilding(false);
}
else
{
///Find a resource that the building wants and a location to get it from
Expand Down
136 changes: 136 additions & 0 deletions test/GigReleaseHarness.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// Real-engine regression: after a delivery a worker is released for the hiring
// auction only when at least RELEASE_IDLE_WORKER_PERCENT of the team's workers
// are idle; otherwise it keeps its building and picks its next trip itself.
#define SDL_MAIN_HANDLED
#ifdef main
#undef main
#endif
#include "GlobalContainer.h"
#include "FileManager.h"
#include <SDL.h>
#include <string>
#include "Game.h"
#include "GameGUI.h"
#include "Unit.h"
#include "UnitConsts.h"
#include "Team.h"
#include "MapInternal.h"
#include "Race.h"
#include "Ressource.h"
#include "IntBuildingType.h"
#include <cstdio>
#include <cstdlib>

GlobalContainer* globalContainer = nullptr;

static void require(bool ok, const char* message)
{
if (!ok) { std::fprintf(stderr, "FAIL: %s\n", message); std::exit(1); }
}

struct TestUnit : Unit
{
using Unit::Unit;
void deliver() { needToRecheckMedical = true; handleDisplacement(); }
};

// A worker standing at the door of an inn that wants wheat, wheat on its back,
// about to deposit; `others` more workers of the team, idle or busy.
struct Bed
{
GameGUI gui;
Game& game;
Team* team;
Building* inn;
TestUnit* carrier;
Bed(int others, bool othersIdle) : game(gui.game)
{
game.map.setSize(5, 5, GRASS);
game.map.setGame(&game);
for (int y = 0; y < game.map.getH(); ++y)
for (int x = 0; x < game.map.getW(); ++x)
game.map.clearImmobileUnit(x, y);
game.addTeam(0);
team = game.teams[0];
team->race.loadDefault();
int innType = globalContainer->buildingsTypes.getTypeNum("inn", 0, false);
require(innType >= 0, "inn type exists");
inn = game.addBuilding(8, 8, innType, 0);
require(inn != nullptr, "inn placed");
game.map.setBuilding(8, 8, inn->type->width, inn->type->height, inn->gid);
inn->maxUnitWorking = 2;
inn->resources[CORN] = 0;
inn->updateCallLists();
require(game.map.incResource(14, 8, CORN, 0), "wheat within reach for the next trip");
// The carrier: attached, at the door, wheat on its back, depositing.
carrier = new TestUnit(7, 8, Unit::GIDfrom(0, 0), WORKER, team, 0);
team->myUnits[0] = carrier;
game.map.setGroundUnit(7, 8, carrier->gid);
carrier->activity = Unit::ACT_FILLING;
carrier->medical = Unit::MED_FREE;
carrier->attachedBuilding = inn;
inn->unitsWorking.push_back(carrier);
carrier->setTargetBuilding(inn);
carrier->destinationPurpose = CORN;
carrier->carriedResource = CORN;
carrier->displacement = Unit::DIS_FILLING_BUILDING;
carrier->dx = 1; carrier->dy = 0;
carrier->validTarget = false;
for (int i = 0; i < others; ++i)
{
Unit* u = game.addUnit(2 + i, 2, 0, WORKER, 0, 0, 0, 0);
require(u != nullptr, "place another worker");
u->medical = Unit::MED_FREE;
u->activity = othersIdle ? Unit::ACT_RANDOM : Unit::ACT_FILLING;
}
}
};

int main(int argc, char** argv)
{
SDL_SetMainReady();
require(argc == 3, "usage: harness PROFILE ROOT");
require(std::string(argv[1]).find("glob2-save-test-") == 0, "disposable profile required");
GlobalContainer globals(argv[1]);
globals.fileManager->addDir(argv[2]);
globalContainer = &globals;
globals.runNoX = true;
globals.settings.rememberUnit = false;
globals.buildingsTypes.init();
IntBuildingType::init();
Race::loadDefault();
require(RELEASE_IDLE_WORKER_PERCENT == 20, "this harness assumes the one-in-five threshold");

{
// Four idle workers of five: released.
Bed bed(4, true);
require(bed.team->idleWorkerShareAtLeast(RELEASE_IDLE_WORKER_PERCENT), "four idle of five is above the threshold");
bed.carrier->deliver();
require(bed.inn->resources[CORN] == 1, "the wheat was deposited");
require(bed.carrier->activity == Unit::ACT_RANDOM && bed.carrier->attachedBuilding == NULL, "released for the auction");
require(bed.inn->unitsWorking.empty(), "no longer working for the inn");
std::puts("gig release: with idle hands the deliverer goes back to the pool");
}
{
// Four busy workers of five: keeps its building and heads out again.
Bed bed(4, false);
require(!bed.team->idleWorkerShareAtLeast(RELEASE_IDLE_WORKER_PERCENT), "nobody idle is below the threshold");
bed.carrier->deliver();
require(bed.inn->resources[CORN] == 1, "the wheat was deposited");
require(bed.carrier->activity == Unit::ACT_FILLING && bed.carrier->attachedBuilding == bed.inn, "keeps its building");
require(bed.carrier->displacement == Unit::DIS_GOING_TO_RESOURCE && bed.carrier->destinationPurpose == CORN, "already heading for the next wheat");
std::puts("gig release: with nobody idle the deliverer keeps its job");
}
{
// Exactly one idle of five: the threshold is inclusive.
Bed bed(4, false);
bed.team->myUnits[1]->activity = Unit::ACT_RANDOM;
require(bed.team->idleWorkerShareAtLeast(RELEASE_IDLE_WORKER_PERCENT), "one in five counts");
bed.carrier->deliver();
require(bed.carrier->activity == Unit::ACT_RANDOM, "released at exactly one in five");
std::puts("gig release: one idle worker in five is enough");
}
std::puts("PASS a delivery releases the worker only while a fifth of the team is idle");
return 0;
}
Loading