Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Economy mode #1314

Merged
merged 15 commits into from
Dec 21, 2020
Merged
Show file tree
Hide file tree
Changes from 14 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
30 changes: 30 additions & 0 deletions libs/common/include/helpers/make_array.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) 2018 - 2020 Settlers Freaks (sf-team at siedler25.org)
//
// This file is part of Return To The Roots.
//
// Return To The Roots is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.
//
// Return To The Roots is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Return To The Roots. If not, see <http://www.gnu.org/licenses/>.

#pragma once

#include <array>
namespace helpers {

template<class D = void, class... Types>
constexpr auto make_array(Types&&... t)
JonathanSteinbuch marked this conversation as resolved.
Show resolved Hide resolved
{
using ResultType = std::conditional_t<std::is_same<D, void>::value, std::common_type_t<Types...>, D>;
return std::array<ResultType, sizeof...(Types)>{std::forward<Types>(t)...};
}

} // namespace helpers
300 changes: 300 additions & 0 deletions libs/s25main/EconomyModeHandler.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
// Copyright (c) 2005 - 2020 Settlers Freaks (sf-team at siedler25.org)
//
// This file is part of Return To The Roots.
//
// Return To The Roots is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.
//
// Return To The Roots is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Return To The Roots. If not, see <http://www.gnu.org/licenses/>.

#include "EconomyModeHandler.h"

#include "EventManager.h"
#include "GameInterface.h"
#include "GamePlayer.h"
#include "GlobalGameSettings.h"
#include "SerializedGameData.h"
#include "helpers/containerUtils.h"
#include "helpers/make_array.h"
#include "random/Random.h"
#include "world/GameWorld.h"
#include "world/GameWorldGame.h"
#include "gameTypes/JobTypes.h"
#include "gameData/JobConsts.h"
#include <boost/optional.hpp>
#include <array>

EconomyModeHandler::EconomyModeHandler(unsigned endFrame) : endFrame(endFrame), gfLastUpdated(0)
{
constexpr auto specialGoodPool =
helpers::make_array(GD_TONGS, GD_HAMMER, GD_AXE, GD_SAW, GD_PICKAXE, GD_SHOVEL, GD_CRUCIBLE, GD_RODANDLINE,
GD_SCYTHE, GD_CLEAVER, GD_ROLLINGPIN, GD_BOW);

constexpr auto commonGoodPool =
helpers::make_array(GD_BEER, GD_WATER, GD_BOAT, GD_SWORD, GD_IRON, GD_FLOUR, GD_FISH, GD_BREAD, GD_WOOD,
GD_BOARDS, GD_STONES, GD_GRAIN, GD_COINS, GD_GOLD, GD_IRONORE, GD_COAL, GD_MEAT, GD_HAM);

constexpr unsigned numGoodTypesToCollect = 7;

// Randomly determine *numGoodTypesToCollect* many good types, one of which is a special good (=tool)

static_assert(numGoodTypesToCollect > 0, "There have to be goods to be collected");
static_assert(commonGoodPool.size() >= numGoodTypesToCollect - 1, "There have to be enough commond goods");
static_assert(!specialGoodPool.empty(), "There have to be enough special goods");
goodsToCollect.clear();
goodsToCollect.resize(numGoodTypesToCollect);
Flamefire marked this conversation as resolved.
Show resolved Hide resolved
auto nextSlot = begin(goodsToCollect);

while(nextSlot != end(goodsToCollect) - 1)
{
GoodType nextGoodType = commonGoodPool[RANDOM.Rand(__FILE__, __LINE__, GetObjId(), commonGoodPool.size())];
// No duplicates should be in goodsToCollect, so only add a good if it isn't one of the already found goods
if(std::find(begin(goodsToCollect), nextSlot, nextGoodType) == nextSlot)
{
*nextSlot = nextGoodType;
nextSlot++;
}
}
*nextSlot = specialGoodPool[RANDOM.Rand(__FILE__, __LINE__, GetObjId(), specialGoodPool.size())];

// Schedule end game event and trust the event manager to keep track of it
if(!isInfinite())
GetEvMgr().AddEvent(this, endFrame);

// Find and set up Teams and trackers
DetermineTeams();
amountsThePlayersCollected.resize(goodsToCollect.size());
maxAmountsATeamCollected.resize(goodsToCollect.size());

// Send Mission Goal
for(unsigned p = 0; p < gwg->GetNumPlayers(); ++p)
{
std::string goalText = _("Economy Mode: Collect as much as you can of the following good types: ");
for(unsigned i = 0; i < numGoodTypesToCollect; i++)
{
if(i > 0)
goalText += ", ";
goalText += _(WARE_NAMES[goodsToCollect[i]]);
}
goalText += ". ";
goalText += _("Tools in the hands of workers are also counted. So are weapons, and beer, that soldiers have in "
"use. For an updating tally of the collected goods see the economic progress window.");

gwg->GetPostMgr().SetMissionGoal(p, goalText);
}
}

EconomyModeHandler::EconomyModeHandler(SerializedGameData& sgd, unsigned objId)
: GameObject(sgd, objId), endFrame(sgd.PopUnsignedInt()), gfLastUpdated(0)
{
sgd.PopContainer(goodsToCollect);

std::vector<unsigned> teamBitMasks;
sgd.PopContainer(teamBitMasks);
for(auto& teamMask : teamBitMasks)
{
economyModeTeams.emplace_back(teamMask, goodsToCollect.size());
}

amountsThePlayersCollected.resize(goodsToCollect.size());
maxAmountsATeamCollected.resize(goodsToCollect.size());
}

void EconomyModeHandler::Destroy() {}

/// Serialisierungsfunktion
void EconomyModeHandler::Serialize(SerializedGameData& sgd) const
{
sgd.PushUnsignedInt(endFrame);
sgd.PushContainer(goodsToCollect);
std::vector<unsigned> teamBitMasks;
for(const EconomyModeHandler::EconTeam& curTeam : economyModeTeams)
{
teamBitMasks.push_back(curTeam.playersInTeam.to_ulong());
}
sgd.PushContainer(teamBitMasks);
}

void EconomyModeHandler::DetermineTeams()
{
RTTR_Assert(economyModeTeams.empty());
for(unsigned i = 0; i < gwg->GetNumPlayers(); ++i)
{
if(gwg->GetPlayer(i).isUsed())
{
bool foundTeam = false;
for(const auto& team : economyModeTeams)
{
if(team.containsPlayer(i))
{
foundTeam = true;
break;
}
}
if(!foundTeam)
{
const GamePlayer& player = gwg->GetPlayer(i);
std::bitset<MAX_PLAYERS> newTeam;
newTeam.set(i);
for(unsigned j = i + 1; j < gwg->GetNumPlayers(); ++j)
{
if(gwg->GetPlayer(j).isUsed() && player.IsAlly(j))
{
newTeam.set(j);
}
}
economyModeTeams.emplace_back(newTeam, goodsToCollect.size());
}
}
}
}

unsigned EconomyModeHandler::SumUpGood(GoodType good, const Inventory& Inventory)
{
unsigned retVal = Inventory.goods[good];

// Add the tools used by workers to the good totals
for(unsigned j = 0; j < NUM_JOB_TYPES; j++)
{
boost::optional<GoodType> tool = JOB_CONSTS[(Job)j].tool;
if(tool == good)
{
retVal += Inventory.people[j];
}
}
// Add the weapons and beer used by soldiers to the good totals
if(good == GD_BEER || good == GD_SWORD || good == GD_SHIELDROMANS)
{
for(const auto& it : SOLDIER_JOBS)
{
retVal += Inventory.people[it];
}
}

return retVal;
}

void EconomyModeHandler::UpdateAmounts()
{
// Return if the game is over or we already updated the amounts this game frame
if(isOver() || gfLastUpdated == GetEvMgr().GetCurrentGF())
Copy link
Member

@Flow86 Flow86 Dec 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to update each GF? or should we also say its calculated every "now and then"? specially for lower end computers these calculations are a heavy load at the moment?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

besides this question everything looks good!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you go for the "now and then" approach: I remember that there is a "statistics step". I guess that would be a good occasion.

@Flow86 Is the "heavy load" measured? If so, maybe we can improve that?
BTW: Do our toolchains support OpenMP? That would be a simple way to get some more speed here.

Copy link
Contributor Author

@JonathanSteinbuch JonathanSteinbuch Dec 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked at this. Indeed, if I open the Economic progress window the framerate (without frame limiting) drops significantly. So I analysed and optimized a bit and even changed it to only update the iwEconomicProgress content at most once every second, because that's certainly enough (the statistics step seemed a bit too far apart).

But it doesn't make a significant difference in the frame rate. Indeed, when you do some perfomance analysis, I think you will see that the only thing for the ingame windows that takes any significant amount of time at all is drawing. And this seems to depend on the number and type of control elements displayed in the window, for example the tool window with tool ordering enabled is even much worse than the economic progress window. So I recommend that if the goal is to reduce the load while displaying ingame windows the Draw routine should be optimized, for example by prerendering/baking the whole windows except elements that change into textures or if possible by using vector buffers instead of directly drawing quads or whatever takes time there. But this is vastly beyond the scope of this pull request.

So tldr: I checked, and no, in comparison with the whole situation of drawing an ingame window these calculations are not a heavy load at all.

PS: But now I see that I broke my own test by not updating everything... oops, brb

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be all good now again

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about this again, and to be honest, I don't like these last three "performance optimization" commits I made very much, because of stylistic and clarity reasons, and as I wrote it's probably unnecessary to have them. I regret them and think I will revert them tomorrow. Should also make it easier to review as @Flow86 already said before that it basically looks good.

{
return;
}

// Sum up goods
for(unsigned i = 0; i < gwg->GetNumPlayers(); ++i)
{
const GamePlayer& player = gwg->GetPlayer(i);
Inventory playerInventory = player.GetInventory();
for(unsigned g = 0; g < goodsToCollect.size(); g++)
{
amountsThePlayersCollected[g][i] = SumUpGood(goodsToCollect[g], playerInventory);
}
}

// Compute the amounts for the teams
std::fill(maxAmountsATeamCollected.begin(), maxAmountsATeamCollected.end(), 0);
for(auto& team : economyModeTeams)
{
std::fill(team.amountsTheTeamCollected.begin(), team.amountsTheTeamCollected.end(), 0);
for(unsigned i = 0; i < gwg->GetNumPlayers(); ++i)
{
if(team.containsPlayer(i))
{
for(unsigned g = 0; g < goodsToCollect.size(); g++)
{
team.amountsTheTeamCollected[g] += GetAmount(g, i);
if(team.amountsTheTeamCollected[g] > maxAmountsATeamCollected[g])
{
maxAmountsATeamCollected[g] = team.amountsTheTeamCollected[g];
}
}
}
}
}
// Determine the leading teams for each good type and determine how many good type wins is the maximum.
mostGoodTypeWins = 0;
for(auto& team : economyModeTeams)
{
team.goodTypeWins = 0;
for(unsigned g = 0; g < goodsToCollect.size(); g++)
{
if(team.amountsTheTeamCollected[g] >= maxAmountsATeamCollected[g])
{
team.goodTypeWins++;
if(team.goodTypeWins > mostGoodTypeWins)
{
mostGoodTypeWins = team.goodTypeWins;
}
}
}
}

gfLastUpdated = GetEvMgr().GetCurrentGF();
}

void EconomyModeHandler::HandleEvent(const unsigned)
{
if(isOver())
{
return;
}

// Handle game end event

// Update one last time
UpdateAmounts();

// Determine bitmask of all players in teams with the most good type wins
std::bitset<MAX_PLAYERS> bestMask;
for(auto& team : economyModeTeams)
{
if(team.goodTypeWins == mostGoodTypeWins)
{
bestMask |= team.playersInTeam;
}
}

// Let players know who won
if(bestMask.count() > 1)
gwg->GetGameInterface()->GI_TeamWinner(bestMask.to_ulong());
else
for(unsigned i = 0; i < gwg->GetNumPlayers(); ++i)
{
if(bestMask[i])
{
gwg->GetGameInterface()->GI_Winner(i);
}
}

gwg->MakeWholeMapVisibleForAllPlayers();
gwg->GetGameInterface()->GI_UpdateMapVisibility();
}

bool EconomyModeHandler::isOver() const
{
return gwg->GetGGS().objective == GO_ECONOMYMODE && !isInfinite() && endFrame < GetEvMgr().GetCurrentGF();
}

EconomyModeHandler::EconTeam::EconTeam(SerializedGameData& sgd, unsigned numGoodTypesToCollect)
: playersInTeam(sgd.PopSignedInt()), amountsTheTeamCollected(numGoodTypesToCollect, 0), goodTypeWins(0)
{}

void EconomyModeHandler::EconTeam::Serialize(SerializedGameData& sgd) const
{
sgd.PushUnsignedInt(playersInTeam.to_ulong());
}

bool EconomyModeHandler::EconTeam::containsPlayer(unsigned playerId) const
{
return playersInTeam[playerId];
}
Loading