Browse Source

Merge remote-tracking branch 'upstream/develop' into develop

Xilmi 1 year ago
parent
commit
a7240041ec

+ 1 - 1
.gitmodules

@@ -1,7 +1,7 @@
 [submodule "test/googletest"]
 	path = test/googletest
 	url = https://github.com/google/googletest
-	branch = v1.13.x
+	branch = v1.15.x
 [submodule "AI/FuzzyLite"]
 	path = AI/FuzzyLite
 	url = https://github.com/fuzzylite/fuzzylite.git

+ 33 - 0
Global.h

@@ -102,6 +102,12 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size.");
 #  define STRONG_INLINE inline
 #endif
 
+// Required for building boost::stacktrace on macOS.
+// See https://github.com/boostorg/stacktrace/issues/88
+#if defined(VCMI_APPLE)
+#define _GNU_SOURCE
+#endif
+
 #define _USE_MATH_DEFINES
 
 #include <algorithm>
@@ -700,6 +706,33 @@ namespace vstd
 		return a + (b - a) * f;
 	}
 
+	/// Divides dividend by divisor and rounds result up
+	/// For use with integer-only arithmetic
+	template<typename Integer1, typename Integer2>
+	Integer1 divideAndCeil(const Integer1 & dividend, const Integer2 & divisor)
+	{
+		static_assert(std::is_integral_v<Integer1> && std::is_integral_v<Integer2>, "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<typename Integer1, typename Integer2>
+	Integer1 divideAndRound(const Integer1 & dividend, const Integer2 & divisor)
+	{
+		static_assert(std::is_integral_v<Integer1> && std::is_integral_v<Integer2>, "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<typename Integer1, typename Integer2>
+	Integer1 divideAndFloor(const Integer1 & dividend, const Integer2 & divisor)
+	{
+		static_assert(std::is_integral_v<Integer1> && std::is_integral_v<Integer2>, "This function should only be used with integral types");
+		return dividend / divisor;
+	}
+
 	template<typename Floating>
 	bool isAlmostZero(const Floating & value)
 	{

+ 5 - 0
Mods/vcmi/config/vcmi/portuguese.json

@@ -72,6 +72,11 @@
 	"vcmi.lobby.noUnderground" : "sem subterrâneo",
 	"vcmi.lobby.sortDate" : "Classifica mapas por data de alteração",
 	"vcmi.lobby.backToLobby" : "Voltar para a sala de espera",
+	"vcmi.lobby.author" : "Autor",
+	"vcmi.lobby.handicap" : "Desvant.",
+	"vcmi.lobby.handicap.resource" : "Fornece aos jogadores recursos apropriados para começar, além dos recursos iniciais normais. Valores negativos são permitidos, mas são limitados a 0 no total (o jogador nunca começa com recursos negativos).",
+	"vcmi.lobby.handicap.income" : "Altera as várias rendas do jogador em porcentagem. Arredondado para cima.",
+	"vcmi.lobby.handicap.growth" : "Altera a taxa de produção das criaturas nas cidades possuídas pelo jogador. Arredondado para cima.",
 	
 	"vcmi.lobby.login.title" : "Sala de Espera Online do VCMI",
 	"vcmi.lobby.login.username" : "Nome de usuário:",

+ 53 - 13
client/battle/BattleInterfaceClasses.cpp

@@ -433,6 +433,50 @@ QuickSpellPanel::QuickSpellPanel(BattleInterface & owner)
 	create();
 }
 
+std::vector<std::tuple<SpellID, bool>> QuickSpellPanel::getSpells()
+{
+	std::vector<SpellID> spellIds;
+	std::vector<bool> spellIdsFromSetting;
+	for(int i = 0; i < QUICKSPELL_SLOTS; i++)
+	{
+		std::string spellIdentifier = persistentStorage["quickSpell"][std::to_string(i)].String();
+		SpellID id;
+		try
+		{
+			id = SpellID::decode(spellIdentifier);
+		}
+		catch(const IdentifierResolutionException& e)
+		{
+			id = SpellID::NONE;
+		}	
+		spellIds.push_back(id);	
+		spellIdsFromSetting.push_back(id != SpellID::NONE);	
+	}
+
+	// autofill empty slots with spells if possible
+	auto hero = owner.getBattle()->battleGetMyHero();
+	for(int i = 0; i < QUICKSPELL_SLOTS; i++)
+	{
+		if(spellIds[i] != SpellID::NONE)
+			continue;
+
+		for(const auto & availableSpellID : CGI->spellh->getDefaultAllowed())
+		{
+			const auto * availableSpell = availableSpellID.toSpell();
+			if(!availableSpell->isAdventure() && !availableSpell->isCreatureAbility() && hero->canCastThisSpell(availableSpell) && !vstd::contains(spellIds, availableSpell->getId()))
+			{
+				spellIds[i] = availableSpell->getId();
+				break;
+			}	
+		}
+	}
+
+	std::vector<std::tuple<SpellID, bool>> ret;
+	for(int i = 0; i < QUICKSPELL_SLOTS; i++)
+		ret.push_back(std::make_tuple(spellIds[i], spellIdsFromSetting[i]));
+	return ret;
+}
+
 void QuickSpellPanel::create()
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
@@ -447,18 +491,11 @@ void QuickSpellPanel::create()
 	if(!hero)
 		return;
 
-	for(int i = 0; i < 12; i++) {
-		std::string spellIdentifier = persistentStorage["quickSpell"][std::to_string(i)].String();
-
+	auto spells = getSpells();
+	for(int i = 0; i < QUICKSPELL_SLOTS; i++) {
 		SpellID id;
-		try
-		{
-			id = SpellID::decode(spellIdentifier);
-		}
-		catch(const IdentifierResolutionException& e)
-		{
-			id = SpellID::NONE;
-		}
+		bool fromSettings;
+		std::tie(id, fromSettings) = spells[i];
 
 		auto button = std::make_shared<CButton>(Point(2, 7 + 50 * i), AnimationPath::builtin("spellint"), CButton::tooltip(), [this, id, hero](){
 			if(id.hasValue() && id.toSpell()->canBeCast(owner.getBattle().get(), spells::Mode::HERO, hero))
@@ -466,16 +503,19 @@ void QuickSpellPanel::create()
 				owner.castThisSpell(id);
 			}
 		});
-		button->setOverlay(std::make_shared<CAnimImage>(AnimationPath::builtin("spellint"), !spellIdentifier.empty() ? id.num + 1 : 0));
+		button->setOverlay(std::make_shared<CAnimImage>(AnimationPath::builtin("spellint"), id != SpellID::NONE ? id.num + 1 : 0));
 		button->addPopupCallback([this, i, hero](){
 			GH.input().hapticFeedback();
 			GH.windows().createAndPushWindow<CSpellWindow>(hero, owner.curInt.get(), true, [this, i](SpellID spell){
 				Settings configID = persistentStorage.write["quickSpell"][std::to_string(i)];
-				configID->String() = spell.toSpell()->identifier;
+				configID->String() = spell == SpellID::NONE ? "" : spell.toSpell()->identifier;
 				create();
 			});
 		});
 
+		if(fromSettings)
+			buttonsIsAutoGenerated.push_back(std::make_shared<TransparentFilledRectangle>(Rect(45, 37 + 50 * i, 5, 5), Colors::ORANGE));
+
 		if(!id.hasValue() || !id.toSpell()->canBeCast(owner.getBattle().get(), spells::Mode::HERO, hero))
 		{
 			buttonsDisabled.push_back(std::make_shared<TransparentFilledRectangle>(Rect(2, 7 + 50 * i, 48, 36), ColorRGBA(0, 0, 0, 172)));

+ 5 - 0
client/battle/BattleInterfaceClasses.h

@@ -155,17 +155,22 @@ private:
 	std::shared_ptr<CFilledTexture> background;
 	std::shared_ptr<TransparentFilledRectangle> rect;
 	std::vector<std::shared_ptr<CButton>> buttons;
+	std::vector<std::shared_ptr<TransparentFilledRectangle>> buttonsIsAutoGenerated;
 	std::vector<std::shared_ptr<TransparentFilledRectangle>> buttonsDisabled;
 	std::vector<std::shared_ptr<CLabel>> labels;
 
 	BattleInterface & owner;
 public:
+	int QUICKSPELL_SLOTS = 12;
+
 	bool isEnabled; // isActive() is not working on multiple conditions, because of this we need a seperate flag
 
 	QuickSpellPanel(BattleInterface & owner);
 
 	void create();
 
+	std::vector<std::tuple<SpellID, bool>> getSpells();
+
 	void show(Canvas & to) override;
 	void inputModeChanged(InputMode modi) override;
 };

+ 8 - 9
client/battle/BattleWindow.cpp

@@ -218,7 +218,9 @@ void BattleWindow::showStickyQuickSpellWindow()
 	Settings showStickyQuickSpellWindow = settings.write["battle"]["enableQuickSpellPanel"];
 	showStickyQuickSpellWindow->Bool() = true;
 
-	if(GH.screenDimensions().x >= 1050 && owner.getBattle()->battleGetMyHero()->hasSpellbook())
+	auto hero = owner.getBattle()->battleGetMyHero();
+
+	if(GH.screenDimensions().x >= 1050 && hero != nullptr && hero->hasSpellbook())
 	{
 		quickSpellWindow->enable();
 		quickSpellWindow->isEnabled = true;
@@ -273,16 +275,13 @@ std::shared_ptr<BattleConsole> BattleWindow::buildBattleConsole(const JsonNode &
 
 void BattleWindow::useSpellIfPossible(int slot)
 {
-	std::string spellIdentifier = persistentStorage["quickSpell"][std::to_string(slot)].String();
 	SpellID id;
-	try
-	{
-		id = SpellID::decode(spellIdentifier);
-	}
-	catch(const IdentifierResolutionException& e)
-	{
+	bool fromSettings;
+	std::tie(id, fromSettings) = quickSpellWindow->getSpells()[slot];
+
+	if(id == SpellID::NONE)
 		return;
-	}
+
 	if(id.hasValue() && owner.getBattle()->battleGetMyHero() && id.toSpell()->canBeCast(owner.getBattle().get(), spells::Mode::HERO, owner.getBattle()->battleGetMyHero()))
 	{
 		owner.castThisSpell(id);

+ 9 - 0
client/gui/CIntObject.cpp

@@ -258,6 +258,15 @@ void CIntObject::redraw()
 	}
 }
 
+void CIntObject::moveChildForeground(const CIntObject * childToMove)
+{
+	for(auto child = children.begin(); child != children.end(); child++)
+		if(*child == childToMove && child != children.end())
+		{
+			std::rotate(child, child + 1, children.end());
+		}
+}
+
 bool CIntObject::receiveEvent(const Point & position, int eventType) const
 {
 	return pos.isInside(position);

+ 2 - 0
client/gui/CIntObject.h

@@ -102,6 +102,8 @@ public:
 	void showAll(Canvas & to) override;
 	//request complete redraw of this object
 	void redraw() override;
+	// Move child object to foreground
+	void moveChildForeground(const CIntObject * childToMove);
 
 	/// returns true if this element is a popup window
 	/// called only for windows

+ 1 - 0
client/widgets/CArtPlace.cpp

@@ -90,6 +90,7 @@ CArtPlace::CArtPlace(Point position, const CArtifactInstance * art)
 
 	image = std::make_shared<CAnimImage>(AnimationPath::builtin("artifact"), imageIndex);
 	image->disable();
+	moveSelectionForeground();
 }
 
 const CArtifactInstance * CArtPlace::getArt() const

+ 5 - 0
client/widgets/MiscWidgets.cpp

@@ -714,3 +714,8 @@ void SelectableSlot::setSelectionWidth(int width)
 	selection = std::make_shared<TransparentFilledRectangle>( selection->pos - pos.topLeft(), Colors::TRANSPARENCY, Colors::YELLOW, width);
 	selectSlot(selected);
 }
+
+void SelectableSlot::moveSelectionForeground()
+{
+	moveChildForeground(selection.get());
+}

+ 1 - 0
client/widgets/MiscWidgets.h

@@ -261,4 +261,5 @@ public:
 	void selectSlot(bool on);
 	bool isSelected() const;
 	void setSelectionWidth(int width);
+	void moveSelectionForeground();
 };

+ 14 - 3
client/windows/CSpellWindow.cpp

@@ -29,6 +29,7 @@
 #include "../widgets/CComponent.h"
 #include "../widgets/CTextInput.h"
 #include "../widgets/TextControls.h"
+#include "../widgets/Buttons.h"
 #include "../adventureMap/AdventureMapInterface.h"
 #include "../render/IRenderHandler.h"
 #include "../render/IImage.h"
@@ -130,9 +131,9 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m
 
 	pos = background->center(Point(pos.w/2 + pos.x, pos.h/2 + pos.y));
 
+	Rect r(90, isBigSpellbook ? 480 : 420, isBigSpellbook ? 160 : 110, 16);
 	if(settings["general"]["enableUiEnhancements"].Bool())
 	{
-		Rect r(90, isBigSpellbook ? 480 : 420, isBigSpellbook ? 160 : 110, 16);
 		const ColorRGBA rectangleColor = ColorRGBA(0, 0, 0, 75);
 		const ColorRGBA borderColor = ColorRGBA(128, 100, 75);
 		const ColorRGBA grayedColor = ColorRGBA(158, 130, 105);
@@ -143,6 +144,13 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m
 		searchBox->setCallback(std::bind(&CSpellWindow::searchInput, this));
 	}
 
+	if(onSpellSelect)
+	{
+		Point boxPos = r.bottomLeft() + Point(-2, 5);
+		showAllSpells = std::make_shared<CToggleButton>(boxPos, AnimationPath::builtin("sysopchk.def"), CButton::tooltip(CGI->generaltexth->translate("core.help.458.hover"), CGI->generaltexth->translate("core.help.458.hover")), [this](bool state){ searchInput(); });
+		showAllSpellsDescription = std::make_shared<CLabel>(boxPos.x + 40, boxPos.y + 12, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, CGI->generaltexth->translate("core.help.458.hover"));
+	}
+
 	processSpells();
 
 	//numbers of spell pages computed
@@ -288,7 +296,7 @@ void CSpellWindow::processSpells()
 
 		if(onSpellSelect)
 		{
-			if(spell->isCombat() == openOnBattleSpells && !spell->isSpecial() && !spell->isCreatureAbility() && searchTextFound)
+			if(spell->isCombat() == openOnBattleSpells && !spell->isSpecial() && !spell->isCreatureAbility() && searchTextFound && (showAllSpells->isSelected() || myHero->canCastThisSpell(spell.get())))
 				mySpells.push_back(spell.get());
 			continue;
 		}
@@ -359,6 +367,9 @@ void CSpellWindow::fexitb()
 	(myInt->battleInt ? myInt->localState->spellbookSettings.spellbookLastTabBattle : myInt->localState->spellbookSettings.spellbookLastTabAdvmap) = selectedTab;
 	(myInt->battleInt ? myInt->localState->spellbookSettings.spellbookLastPageBattle : myInt->localState->spellbookSettings.spellbookLastPageAdvmap) = currentPage;
 
+	if(onSpellSelect)
+		onSpellSelect(SpellID::NONE);
+
 	close();
 }
 
@@ -605,7 +616,7 @@ void CSpellWindow::SpellArea::clickPressed(const Point & cursorPosition)
 		if(owner->onSpellSelect)
 		{
 			owner->onSpellSelect(mySpell->id);
-			owner->fexitb();
+			owner->close();
 			return;
 		}
 

+ 4 - 0
client/windows/CSpellWindow.h

@@ -27,6 +27,7 @@ class CPlayerInterface;
 class CSpellWindow;
 class CTextInput;
 class TransparentFilledRectangle;
+class CToggleButton;
 
 /// The spell window
 class CSpellWindow : public CWindowObject
@@ -82,6 +83,9 @@ class CSpellWindow : public CWindowObject
 	std::shared_ptr<TransparentFilledRectangle> searchBoxRectangle;
 	std::shared_ptr<CLabel> searchBoxDescription;
 
+	std::shared_ptr<CToggleButton> showAllSpells;
+	std::shared_ptr<CLabel> showAllSpellsDescription;
+
 	bool isBigSpellbook;
 	int spellsPerPage;
 	int offL;

+ 1 - 1
config/schemas/template.json

@@ -99,7 +99,7 @@
 				"type":
 				{
 					"type" : "string",
-					"enum" : ["wide", "fictive", "repulsive"]
+					"enum" : ["wide", "fictive", "repulsive", "forcePortal"]
 				}
 			}
 		},

+ 1 - 1
docs/modders/Random_Map_Template.md

@@ -38,7 +38,7 @@
 		{ "a" : "zoneA", "b" : "zoneB", "guard" : 5000, "road" : "false" },
 		{ "a" : "zoneA", "b" : "zoneC", "guard" : 5000, "road" : "random" },
 		{ "a" : "zoneB", "b" : "zoneC", "type" : "wide" }
-		//"type" can be "guarded" (default), "wide", "fictive" or "repulsive"
+		//"type" can be "guarded" (default), "wide", "fictive", "repulsive" or "forcePortal"
 		//"wide" connections have no border, or guard. "fictive" and "repulsive" connections are virtual -
 		//they do not create actual path, but only attract or repulse zones, respectively
 	]

+ 37 - 25
lib/CConsoleHandler.cpp

@@ -13,6 +13,8 @@
 
 #include "CThreadHelper.h"
 
+#include <boost/stacktrace.hpp>
+
 VCMI_LIB_NAMESPACE_BEGIN
 
 std::mutex CConsoleHandler::smx;
@@ -142,6 +144,30 @@ static void createMemoryDump(MINIDUMP_EXCEPTION_INFORMATION * meinfo)
 	MessageBoxA(0, "VCMI has crashed. We are sorry. File with information about encountered problem has been created.", "VCMI Crashhandler", MB_OK | MB_ICONERROR);
 }
 
+LONG WINAPI onUnhandledException(EXCEPTION_POINTERS* exception)
+{
+	logGlobal->error("Disaster happened.");
+
+	PEXCEPTION_RECORD einfo = exception->ExceptionRecord;
+	logGlobal->error("Reason: 0x%x - %s at %04x:%x", einfo->ExceptionCode, exceptionName(einfo->ExceptionCode), exception->ContextRecord->SegCs, (void*)einfo->ExceptionAddress);
+
+	if (einfo->ExceptionCode == EXCEPTION_ACCESS_VIOLATION)
+	{
+		logGlobal->error("Attempt to %s 0x%8x", (einfo->ExceptionInformation[0] == 1 ? "write to" : "read from"), (void*)einfo->ExceptionInformation[1]);
+	}
+	const DWORD threadId = ::GetCurrentThreadId();
+	logGlobal->error("Thread ID: %d", threadId);
+
+	//exception info to be placed in the dump
+	MINIDUMP_EXCEPTION_INFORMATION meinfo = {threadId, exception, TRUE};
+
+	createMemoryDump(&meinfo);
+
+	return EXCEPTION_EXECUTE_HANDLER;
+}
+
+#endif
+
 [[noreturn]] static void onTerminate()
 {
 	logGlobal->error("Disaster happened.");
@@ -166,37 +192,20 @@ static void createMemoryDump(MINIDUMP_EXCEPTION_INFORMATION * meinfo)
 		logGlobal->error("Reason: unknown exception!");
 	}
 
+	logGlobal->error("Call stack information:");
+	std::stringstream stream;
+	stream << boost::stacktrace::stacktrace();
+	logGlobal->error("%s", stream.str());
+
+#ifdef VCMI_WINDOWS
 	const DWORD threadId = ::GetCurrentThreadId();
 	logGlobal->error("Thread ID: %d", threadId);
 
 	createMemoryDump(nullptr);
+#endif
 	std::abort();
 }
 
-LONG WINAPI onUnhandledException(EXCEPTION_POINTERS* exception)
-{
-	logGlobal->error("Disaster happened.");
-
-	PEXCEPTION_RECORD einfo = exception->ExceptionRecord;
-	logGlobal->error("Reason: 0x%x - %s at %04x:%x", einfo->ExceptionCode, exceptionName(einfo->ExceptionCode), exception->ContextRecord->SegCs, (void*)einfo->ExceptionAddress);
-
-	if (einfo->ExceptionCode == EXCEPTION_ACCESS_VIOLATION)
-	{
-		logGlobal->error("Attempt to %s 0x%8x", (einfo->ExceptionInformation[0] == 1 ? "write to" : "read from"), (void*)einfo->ExceptionInformation[1]);
-	}
-	const DWORD threadId = ::GetCurrentThreadId();
-	logGlobal->error("Thread ID: %d", threadId);
-
-	//exception info to be placed in the dump
-	MINIDUMP_EXCEPTION_INFORMATION meinfo = {threadId, exception, TRUE};
-
-	createMemoryDump(&meinfo);
-
-	return EXCEPTION_EXECUTE_HANDLER;
-}
-#endif
-
-
 void CConsoleHandler::setColor(EConsoleTextColor::EConsoleTextColor color)
 {
 	TColor colorCode;
@@ -289,11 +298,14 @@ CConsoleHandler::CConsoleHandler():
 	defErrColor = csbi.wAttributes;
 #ifndef _DEBUG
 	SetUnhandledExceptionFilter(onUnhandledException);
-	std::set_terminate(onTerminate);
 #endif
 #else
 	defColor = "\x1b[0m";
 #endif
+
+#ifndef _DEBUG
+	std::set_terminate(onTerminate);
+#endif
 }
 CConsoleHandler::~CConsoleHandler()
 {

+ 2 - 1
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;

+ 2 - 2
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

+ 1 - 1
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

+ 2 - 1
lib/rmg/CRmgTemplate.cpp

@@ -463,7 +463,8 @@ void ZoneConnection::serializeJson(JsonSerializeFormat & handler)
 		"guarded",
 		"fictive",
 		"repulsive",
-		"wide"
+		"wide",
+		"forcePortal"
 	};
 
 	static const std::vector<std::string> roadOptions =

+ 2 - 1
lib/rmg/CRmgTemplate.h

@@ -75,7 +75,8 @@ enum class EConnectionType
 	GUARDED = 0, //default
 	FICTIVE,
 	REPULSIVE,
-	WIDE
+	WIDE,
+	FORCE_PORTAL
 };
 
 enum class ERoadOption

+ 31 - 6
lib/rmg/CZonePlacer.cpp

@@ -80,12 +80,21 @@ void CZonePlacer::findPathsBetweenZones()
 
 			for (auto & connection : connectedZoneIds)
 			{
-				if (connection.getConnectionType() == rmg::EConnectionType::REPULSIVE)
+				switch (connection.getConnectionType())
 				{
 					//Do not consider virtual connections for graph distance
-					continue;
+					case rmg::EConnectionType::REPULSIVE:
+					case rmg::EConnectionType::FORCE_PORTAL:
+						continue;
 				}
 				auto neighbor = connection.getOtherZoneId(current);
+
+				if (current == neighbor)
+				{
+					//Do not consider self-connections
+					continue;
+				}
+
 				if (!visited[neighbor])
 				{
 					visited[neighbor] = true;
@@ -552,8 +561,16 @@ void CZonePlacer::attractConnectedZones(TZoneMap & zones, TForceVector & forces,
 
 		for (const auto & connection : zone.second->getConnections())
 		{
-			if (connection.getConnectionType() == rmg::EConnectionType::REPULSIVE)
+			switch (connection.getConnectionType())
 			{
+				//Do not consider virtual connections for graph distance
+				case rmg::EConnectionType::REPULSIVE:
+				case rmg::EConnectionType::FORCE_PORTAL:
+					continue;
+			}
+			if (connection.getZoneA() == connection.getZoneB())
+			{
+				//Do not consider self-connections
 				continue;
 			}
 
@@ -710,11 +727,19 @@ void CZonePlacer::moveOneZone(TZoneMap& zones, TForceVector& totalForces, TDista
 		std::set<TRmgTemplateZoneId> connectedZones;
 		for (const auto& connection : firstZone->getConnections())
 		{
-			//FIXME: Should we also exclude fictive connections?
-			if (connection.getConnectionType() != rmg::EConnectionType::REPULSIVE)
+			switch (connection.getConnectionType())
 			{
-				connectedZones.insert(connection.getOtherZoneId(firstZone->getId()));
+				//Do not consider virtual connections for graph distance
+				case rmg::EConnectionType::REPULSIVE:
+				case rmg::EConnectionType::FORCE_PORTAL:
+					continue;
+			}
+			if (connection.getZoneA() == connection.getZoneB())
+			{
+				//Do not consider self-connections
+				continue;
 			}
+			connectedZones.insert(connection.getOtherZoneId(firstZone->getId()));
 		}
 
 		auto level = firstZone->getCenter().z;

+ 47 - 15
lib/rmg/modificators/ConnectionsPlacer.cpp

@@ -55,6 +55,17 @@ void ConnectionsPlacer::process()
 	{
 		for (auto& c : dConnections)
 		{
+			if (c.getZoneA() == c.getZoneB())
+			{
+				// Zone can always be connected to itself, but only by monolith pair
+				RecursiveLock lock(externalAccessMutex);
+				if (!vstd::contains(dCompleted, c))
+				{
+					placeMonolithConnection(c);
+					continue;
+				}
+			}
+
 			auto otherZone = map.getZones().at(c.getZoneB());
 			auto* cp = otherZone->getModificator<ConnectionsPlacer>();
 
@@ -74,6 +85,11 @@ void ConnectionsPlacer::process()
 		}
 	};
 
+	diningPhilosophers([this](const rmg::ZoneConnection& c)
+	{
+		forcePortalConnection(c);
+	});
+
 	diningPhilosophers([this](const rmg::ZoneConnection& c)
 	{
 		selfSideDirectConnection(c);
@@ -115,6 +131,15 @@ void ConnectionsPlacer::otherSideConnection(const rmg::ZoneConnection & connecti
 	dCompleted.push_back(connection);
 }
 
+void ConnectionsPlacer::forcePortalConnection(const rmg::ZoneConnection & connection)
+{
+	// This should always succeed
+	if (connection.getConnectionType() == rmg::EConnectionType::FORCE_PORTAL)
+	{
+		placeMonolithConnection(connection);
+	}
+}
+
 void ConnectionsPlacer::selfSideDirectConnection(const rmg::ZoneConnection & connection)
 {
 	bool success = false;
@@ -410,23 +435,30 @@ void ConnectionsPlacer::selfSideIndirectConnection(const rmg::ZoneConnection & c
 	//4. place monoliths/portals
 	if(!success)
 	{
-		auto factory = VLC->objtypeh->getHandlerFor(Obj::MONOLITH_TWO_WAY, generator.getNextMonlithIndex());
-		auto * teleport1 = factory->create(map.mapInstance->cb, nullptr);
-		auto * teleport2 = factory->create(map.mapInstance->cb, nullptr);
-
-		RequiredObjectInfo obj1(teleport1, connection.getGuardStrength(), allowRoad);
-		RequiredObjectInfo obj2(teleport2, connection.getGuardStrength(), allowRoad);
-		zone.getModificator<ObjectManager>()->addRequiredObject(obj1);
-		otherZone->getModificator<ObjectManager>()->addRequiredObject(obj2);
-		
-		assert(otherZone->getModificator<ConnectionsPlacer>());
-		otherZone->getModificator<ConnectionsPlacer>()->otherSideConnection(connection);
-		
-		success = true;
+		placeMonolithConnection(connection);
 	}
+}
+
+void ConnectionsPlacer::placeMonolithConnection(const rmg::ZoneConnection & connection)
+{
+	auto otherZoneId = (connection.getZoneA() == zone.getId() ? connection.getZoneB() : connection.getZoneA());
+	auto & otherZone = map.getZones().at(otherZoneId);
+
+	bool allowRoad = shouldGenerateRoad(connection);
+
+	auto factory = VLC->objtypeh->getHandlerFor(Obj::MONOLITH_TWO_WAY, generator.getNextMonlithIndex());
+	auto * teleport1 = factory->create(map.mapInstance->cb, nullptr);
+	auto * teleport2 = factory->create(map.mapInstance->cb, nullptr);
+
+	RequiredObjectInfo obj1(teleport1, connection.getGuardStrength(), allowRoad);
+	RequiredObjectInfo obj2(teleport2, connection.getGuardStrength(), allowRoad);
+	zone.getModificator<ObjectManager>()->addRequiredObject(obj1);
+	otherZone->getModificator<ObjectManager>()->addRequiredObject(obj2);
+
+	dCompleted.push_back(connection);
 	
-	if(success)
-		dCompleted.push_back(connection);
+	assert(otherZone->getModificator<ConnectionsPlacer>());
+	otherZone->getModificator<ConnectionsPlacer>()->otherSideConnection(connection);
 }
 
 void ConnectionsPlacer::collectNeighbourZones()

+ 2 - 1
lib/rmg/modificators/ConnectionsPlacer.h

@@ -23,7 +23,8 @@ public:
 	void init() override;
 	
 	void addConnection(const rmg::ZoneConnection& connection);
-	
+	void placeMonolithConnection(const rmg::ZoneConnection& connection);
+	void forcePortalConnection(const rmg::ZoneConnection & connection);
 	void selfSideDirectConnection(const rmg::ZoneConnection & connection);
 	void selfSideIndirectConnection(const rmg::ZoneConnection & connection);
 	void otherSideConnection(const rmg::ZoneConnection & connection);

+ 1 - 1
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();

+ 1 - 1
test/googletest

@@ -1 +1 @@
-Subproject commit b796f7d44681514f58a683a3a71ff17c94edb0c1
+Subproject commit b514bdc898e2951020cbdca1304b75f5950d1f59