From 81e6207df06d33c660a487598c44fcfb6e95c63f Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 1 Aug 2024 20:48:55 +0000 Subject: [PATCH] Add helper functions for integer division rounding Added set of functions that perform integer division with different rounding modes: - divideAndCeil - rounds up to next integer - divideAndRound - rounds to nearest integer - divideAndFloor - rounds to previous integer (equivalent to default division) Intended for use in library, where usage of floating point might lead to desync in multiplayer games. Replaced some cases that I knew of, including recent handicap PR --- Global.h | 27 ++++++++++++++++++++++++ lib/battle/DamageCalculator.cpp | 3 ++- lib/mapObjects/CGTownInstance.cpp | 4 ++-- lib/mapObjects/MiscObjects.cpp | 2 +- server/battles/BattleActionProcessor.cpp | 2 +- 5 files changed, 33 insertions(+), 5 deletions(-) diff --git a/Global.h b/Global.h index 57c4466ef..1f9274e90 100644 --- a/Global.h +++ b/Global.h @@ -700,6 +700,33 @@ namespace vstd return a + (b - a) * f; } + /// Divides dividend by divisor and rounds result up + /// For use with integer-only arithmetic + template + Integer1 divideAndCeil(const Integer1 & dividend, const Integer2 & divisor) + { + static_assert(std::is_integral_v && std::is_integral_v, "This function should only be used with integral types"); + return (dividend + divisor - 1) / divisor; + } + + /// Divides dividend by divisor and rounds result to nearest + /// For use with integer-only arithmetic + template + Integer1 divideAndRound(const Integer1 & dividend, const Integer2 & divisor) + { + static_assert(std::is_integral_v && std::is_integral_v, "This function should only be used with integral types"); + return (dividend + divisor / 2 - 1) / divisor; + } + + /// Divides dividend by divisor and rounds result down + /// For use with integer-only arithmetic + template + Integer1 divideAndFloor(const Integer1 & dividend, const Integer2 & divisor) + { + static_assert(std::is_integral_v && std::is_integral_v, "This function should only be used with integral types"); + return dividend / divisor; + } + template bool isAlmostZero(const Floating & value) { diff --git a/lib/battle/DamageCalculator.cpp b/lib/battle/DamageCalculator.cpp index 99bce45e6..0bcc88ab0 100644 --- a/lib/battle/DamageCalculator.cpp +++ b/lib/battle/DamageCalculator.cpp @@ -145,7 +145,8 @@ int DamageCalculator::getActorAttackIgnored() const if(multAttackReductionPercent > 0) { - int reduction = (getActorAttackBase() * multAttackReductionPercent + 49) / 100; //using ints so 1.5 for 5 attack is rounded down as in HotA / h3assist etc. (keep in mind h3assist 1.2 shows wrong value for 15 attack points and unupg. nix) + //using ints so 1.5 for 5 attack is rounded down as in HotA / h3assist etc. (keep in mind h3assist 1.2 shows wrong value for 15 attack points and unupg. nix) + int reduction = vstd::divideAndRound( getActorAttackBase() * multAttackReductionPercent, 100); return -std::min(reduction, getActorAttackBase()); } return 0; diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index bec0aebc7..6f646bf87 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -227,7 +227,7 @@ TResources CGTownInstance::dailyIncome() const auto playerSettings = cb->gameState()->scenarioOps->getIthPlayersSettings(getOwner()); for(TResources::nziterator it(ret); it.valid(); it++) // always round up income - we don't want to always produce zero if handicap in use - ret[it->resType] = (ret[it->resType] * playerSettings.handicap.percentIncome + 99) / 100; + ret[it->resType] = vstd::divideAndCeil(ret[it->resType] * playerSettings.handicap.percentIncome, 100); return ret; } @@ -1271,7 +1271,7 @@ int GrowthInfo::totalGrowth() const ret += entry.count; // always round up income - we don't want buildings to always produce zero if handicap in use - return (ret * handicapPercentage + 99) / 100; + return vstd::divideAndCeil(ret * handicapPercentage, 100); } void CGTownInstance::fillUpgradeInfo(UpgradeInfo & info, const CStackInstance &stack) const diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index b0eff7592..33e96cbd7 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -200,7 +200,7 @@ ui32 CGMine::getProducedQuantity() const { auto * playerSettings = cb->getPlayerSettings(getOwner()); // always round up income - we don't want mines to always produce zero if handicap in use - return (producedQuantity * playerSettings->handicap.percentIncome + 99) / 100; + return vstd::divideAndCeil(producedQuantity * playerSettings->handicap.percentIncome, 100); } void CGMine::battleFinished(const CGHeroInstance *hero, const BattleResult &result) const diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index 4a8e9b4f0..c8978a1ea 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -1267,7 +1267,7 @@ void BattleActionProcessor::handleDeathStare(const CBattleInfoCallback & battle, vstd::amin(chanceToKill, 1); //cap at 100% int killedCreatures = gameHandler->getRandomGenerator().nextBinomialInt(attacker->getCount(), chanceToKill); - int maxToKill = (attacker->getCount() * singleCreatureKillChancePercent + 99) / 100; + int maxToKill = vstd::divideAndCeil(attacker->getCount() * singleCreatureKillChancePercent, 100); vstd::amin(killedCreatures, maxToKill); killedCreatures += (attacker->level() * attacker->valOfBonuses(BonusType::DEATH_STARE, BonusCustomSubtype::deathStareCommander)) / defender->level();