Skip to content

Commit

Permalink
research.json: Add support for new "calculationMode" option
Browse files Browse the repository at this point in the history
The default `"calculationMode"` is `"compat"`, and functions as many previous versions for many years have. This mode can accumulate noticeable error - especially if repeatedly upgrading small values by smaller percentages (commonly impacted: armour, thermal).

The new opt-in `"calculationMode"` is `"improved"`. This handles calculating upgrades in a way that significantly reduces accumulated errors from multiple applied research upgrades to a component's value(s).

To opt-in to the new mode, add a new "_config_" dict to the top-level research.json object, specifying the desired "calculationMode".

Example:
```json
{
  "_config_": {
    "calculationMode": "improved"
  },
  ...
}
```
  • Loading branch information
past-due committed May 7, 2024
1 parent 2f00d6d commit 43e8fb0
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 2 deletions.
111 changes: 109 additions & 2 deletions src/research.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,13 @@

// The stores for the research stats
std::vector<RESEARCH> asResearch;
optional<ResearchUpgradeCalculationMode> researchUpgradeCalcMode;
nlohmann::json cachedStatsObject = nlohmann::json(nullptr);
std::vector<wzapi::PerPlayerUpgrades> cachedPerPlayerUpgrades;

typedef std::unordered_map<std::string, std::unordered_map<std::string, int64_t>> RawResearchUpgradeChangeValues;
std::array<RawResearchUpgradeChangeValues, MAX_PLAYERS> cachedPerPlayerRawUpgradeChange;

//used for Callbacks to say which topic was last researched
RESEARCH *psCBLastResearch;
STRUCTURE *psCBLastResStructure;
Expand Down Expand Up @@ -105,6 +109,7 @@ bool researchInitVars()
psCBLastResStructure = nullptr;
CBResFacilityOwner = -1;
asResearch.clear();
researchUpgradeCalcMode = nullopt;
cachedStatsObject = nlohmann::json(nullptr);
cachedPerPlayerUpgrades.clear();
playerUpgradeCounts = std::vector<PlayerUpgradeCounts>(MAX_PLAYERS);
Expand All @@ -120,6 +125,12 @@ bool researchInitVars()
return true;
}

ResearchUpgradeCalculationMode getResearchUpgradeCalcMode()
{
// Default to ResearchUpgradeCalculationMode::Compat, unless otherwise specified
return researchUpgradeCalcMode.value_or(ResearchUpgradeCalculationMode::Compat);
}

uint32_t PlayerUpgradeCounts::getNumWeaponImpactClassUpgrades(WEAPON_SUBCLASS subClass)
{
auto subClassStr = getWeaponSubClass(subClass);
Expand Down Expand Up @@ -240,17 +251,80 @@ class CycleDetection
}
};

static optional<ResearchUpgradeCalculationMode> resCalcModeStringToValue(const WzString& calcModeStr)
{
if (calcModeStr.compare("compat") == 0)
{
return ResearchUpgradeCalculationMode::Compat;
}
else if (calcModeStr.compare("improved") == 0)
{
return ResearchUpgradeCalculationMode::Improved;
}
else
{
return nullopt;
}
}

static const char* resCalcModeToString(ResearchUpgradeCalculationMode mode)
{
switch (mode)
{
case ResearchUpgradeCalculationMode::Compat:
return "compat";
case ResearchUpgradeCalculationMode::Improved:
return "improved";
}
return "invalid";
}

#define RESEARCH_JSON_CONFIG_DICT_KEY "_config_"

/** Load the research stats */
bool loadResearch(WzConfig &ini)
{
ASSERT(ini.isAtDocumentRoot(), "WzConfig instance is in the middle of traversal");
const WzString CONFIG_DICT_KEY_STR = RESEARCH_JSON_CONFIG_DICT_KEY;
std::vector<WzString> list = ini.childGroups();
PLAYER_RESEARCH dummy;
memset(&dummy, 0, sizeof(dummy));
std::vector<std::vector<WzString>> preResearch;
preResearch.resize(list.size());
for (size_t inc = 0; inc < list.size(); ++inc)
{
if (list[inc] == CONFIG_DICT_KEY_STR)
{
// handle the special config dict
ini.beginGroup(list[inc]);

// calculationMode
auto calcModeStr = ini.value("calculationMode", resCalcModeToString(ResearchUpgradeCalculationMode::Compat)).toWzString();
auto calcModeParsed = resCalcModeStringToValue(calcModeStr);
if (calcModeParsed.has_value())
{
if (!researchUpgradeCalcMode.has_value())
{
researchUpgradeCalcMode = calcModeParsed.value();
}
else
{
if (researchUpgradeCalcMode.value() != calcModeParsed.value())
{
debug(LOG_ERROR, "Non-matching research JSON calculationModes");
debug(LOG_INFO, "Research JSON file \"%s\" has specified a calculationMode (\"%s\") that does not match the first loaded research JSON's calculationMode (\"%s\")", ini.fileName().toUtf8().c_str(), calcModeStr.toUtf8().c_str(), resCalcModeToString(researchUpgradeCalcMode.value()));
}
}
}
else
{
ASSERT_OR_RETURN(false, false, "Invalid _config_ \"calculationMode\" value: \"%s\"", calcModeStr.toUtf8().c_str());
}

ini.endGroup();
continue;
}

// HACK FIXME: the code assumes we have empty PLAYER_RESEARCH entries to throw around
for (auto &j : asPlayerResList)
{
Expand Down Expand Up @@ -501,6 +575,12 @@ bool loadResearch(WzConfig &ini)
return false;
}

// If the first research json file does not explicitly set calculationMode, default to compat
if (!researchUpgradeCalcMode.has_value())
{
researchUpgradeCalcMode = ResearchUpgradeCalculationMode::Compat;
}

return true;
}

Expand Down Expand Up @@ -680,6 +760,8 @@ static void eventResearchedHandleUpgrades(const RESEARCH *psResearch, const STRU
debug(LOG_RESEARCH, "RESEARCH : %s(%s) for %d", psResearch->name.toUtf8().c_str(), psResearch->id.toUtf8().c_str(), player);

ASSERT_OR_RETURN(, player >= 0 && player < cachedPerPlayerUpgrades.size(), "Player %d does not exist in per-player upgrades?", player);
auto &playerRawUpgradeChangeTotals = cachedPerPlayerRawUpgradeChange[player];
const auto upgradeCalcMode = getResearchUpgradeCalcMode();

PlayerUpgradeCounts tempStats;

Expand Down Expand Up @@ -830,8 +912,28 @@ static void eventResearchedHandleUpgrades(const RESEARCH *psResearch, const STRU
}
int64_t currentUpgradesValue = currentUpgradesValue_json.get<int64_t>();
int64_t scaledChange = (statsOriginalValue * value);
int64_t newUpgradesChange = (value < 0) ? iDivFloor(scaledChange, 100) : iDivCeil(scaledChange, 100);
int64_t newUpgradesValue = (currentUpgradesValue + newUpgradesChange);
int64_t newUpgradesChange = 0;
int64_t newUpgradesValue = 0;
switch (upgradeCalcMode)
{
case ResearchUpgradeCalculationMode::Compat:
// Default / compat cumulative upgrade handling (the only option for many years - from at least 3.x/(3.2+?)-4.4.2?)
// This can accumulate noticeable error, especially if repeatedly upgrading small values by small percentages (commonly impacted: armour, thermal)
// However, research.json created and tested during this long period may be expecting this outcome / behavior
newUpgradesChange = (value < 0) ? iDivFloor(scaledChange, 100) : iDivCeil(scaledChange, 100);
newUpgradesValue = (currentUpgradesValue + newUpgradesChange);
break;
case ResearchUpgradeCalculationMode::Improved:
{
// "Improved" cumulative upgrade handling (significantly reduces accumulated errors)
auto& compUpgradeTotals = playerRawUpgradeChangeTotals[cname.first];
auto& cumulativeUpgradeScaledChange = compUpgradeTotals[parameter];
cumulativeUpgradeScaledChange += scaledChange;
newUpgradesValue = statsOriginalValue + ((cumulativeUpgradeScaledChange < 0) ? iDivFloor(cumulativeUpgradeScaledChange, 100) : iDivCeil(cumulativeUpgradeScaledChange, 100));
newUpgradesChange = newUpgradesValue - currentUpgradesValue;
break;
}
}
if (currentUpgradesValue_json.is_number_unsigned())
{
// original was unsigned integer - round anything less than 0 up to 0
Expand Down Expand Up @@ -1075,12 +1177,17 @@ bool ResearchShutDown()
void ResearchRelease()
{
asResearch.clear();
researchUpgradeCalcMode = nullopt;
for (auto &i : asPlayerResList)
{
i.clear();
}
cachedStatsObject = nlohmann::json(nullptr);
cachedPerPlayerUpgrades.clear();
for (auto &p : cachedPerPlayerRawUpgradeChange)
{
p.clear();
}
playerUpgradeCounts = std::vector<PlayerUpgradeCounts>(MAX_PLAYERS);
}

Expand Down
13 changes: 13 additions & 0 deletions src/research.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,17 @@ enum
RID_MAXRID
};

enum class ResearchUpgradeCalculationMode
{
// Default / compat cumulative upgrade handling (the only option for many years - from at least 3.x/(3.2+?)-4.4.2?)
// This can accumulate noticeable error, especially if repeatedly upgrading small values by small percentages (commonly impacted: armour, thermal)
// However, research.json created and tested during this long period may be expecting this outcome / behavior
Compat,

// "Improved" cumulative upgrade handling (significantly reduces accumulated errors)
Improved
};

/* The store for the research stats */
extern std::vector<RESEARCH> asResearch;

Expand All @@ -85,6 +96,8 @@ extern UDWORD aDefaultSensor[MAX_PLAYERS];
extern UDWORD aDefaultECM[MAX_PLAYERS];
extern UDWORD aDefaultRepair[MAX_PLAYERS];

ResearchUpgradeCalculationMode getResearchUpgradeCalcMode();

bool loadResearch(WzConfig &ini);

/*function to check what can be researched for a particular player at any one
Expand Down

0 comments on commit 43e8fb0

Please sign in to comment.