From 249c9507e7a658f04b6f23dd6d9e68c948140094 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Zaremba?= Date: Thu, 18 Sep 2025 10:56:53 +0200 Subject: [PATCH 01/20] Fix remove artifacts during map load --- lib/mapping/MapFormatH3M.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 52fbef41f..29940945f 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -925,8 +925,7 @@ void CMapLoaderH3M::loadArtifactsOfHero(CGHeroInstance * hero) logGlobal->debug("Hero %d at %s has set artifacts twice (in map properties and on adventure map instance). Using the latter set...", hero->getHeroTypeID().getNum(), hero->anchorPos().toString()); hero->artifactsInBackpack.clear(); - while(!hero->artifactsWorn.empty()) - hero->removeArtifact(hero->artifactsWorn.begin()->first); + hero->artifactsWorn.clear(); } for(int i = 0; i < features.artifactSlotsCount; i++) From 949b34a3cb07b8c35fe6c64709793e39a1256387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Zaremba?= Date: Thu, 18 Sep 2025 10:57:56 +0200 Subject: [PATCH 02/20] Fix: deal with null map objects (reserved for heroes not on map) --- mapeditor/mainwindow.cpp | 3 +++ mapeditor/mapcontroller.cpp | 2 ++ mapeditor/mapsettings/translations.cpp | 7 +++++-- mapeditor/validator.cpp | 2 ++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index cdbda068a..856440fd7 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -618,6 +618,9 @@ void MainWindow::saveMap() for(auto obj : controller.map()->objects) { + if(!obj) + continue; + if(obj->ID == Obj::HERO_PLACEHOLDER) { auto hero = dynamic_cast(obj.get()); diff --git a/mapeditor/mapcontroller.cpp b/mapeditor/mapcontroller.cpp index 40fe6497e..9a1fc18e0 100644 --- a/mapeditor/mapcontroller.cpp +++ b/mapeditor/mapcontroller.cpp @@ -742,6 +742,8 @@ ModCompatibilityInfo MapController::modAssessmentMap(const CMap & map) for(auto obj : map.objects) { + if(!obj) + continue; modAssessmentObject(obj.get(), result); } return result; diff --git a/mapeditor/mapsettings/translations.cpp b/mapeditor/mapsettings/translations.cpp index 7f5fb2bbc..c6715d3c5 100644 --- a/mapeditor/mapsettings/translations.cpp +++ b/mapeditor/mapsettings/translations.cpp @@ -20,8 +20,11 @@ void Translations::cleanupRemovedItems(CMap & map) { std::set existingObjects{"map", "header"}; for(auto object : map.objects) - existingObjects.insert(object->instanceName); - + { + if(object) + existingObjects.insert(object->instanceName); + } + for(auto & translations : map.translations.Struct()) { JsonNode updateTranslations; diff --git a/mapeditor/validator.cpp b/mapeditor/validator.cpp index ff1794610..3163cef02 100644 --- a/mapeditor/validator.cpp +++ b/mapeditor/validator.cpp @@ -79,6 +79,8 @@ std::set Validator::validate(const CMap * map) //checking all objects in the map for(auto o : map->objects) { + if(!o) + continue; //owners for objects if(o->getOwner() == PlayerColor::UNFLAGGABLE) { From 55cdbb931cba4a0ba3781ee7512edd5f6a30d835 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 20 Sep 2025 20:50:29 +0200 Subject: [PATCH 03/20] generate windows for 8/9 resources --- client/render/AssetGenerator.cpp | 75 ++++++++++++++++++++++++++++++++ client/render/AssetGenerator.h | 2 + 2 files changed, 77 insertions(+) diff --git a/client/render/AssetGenerator.cpp b/client/render/AssetGenerator.cpp index 0f8119911..4b2e5b3e8 100644 --- a/client/render/AssetGenerator.cpp +++ b/client/render/AssetGenerator.cpp @@ -52,6 +52,22 @@ void AssetGenerator::initialize() imageFiles[ImagePath::builtin("SpelTabNone.png")] = [this](){ return createSpellTabNone();}; + for (PlayerColor color(-1); color < PlayerColor::PLAYER_LIMIT; ++color) + { + for(int amount : { 8, 9 }) + { + auto addResWindow = [this, amount, color](std::string baseName, CreateResourceWindowType type){ + std::string name = baseName + "-R" + std::to_string(amount) + (color == -1 ? "" : "-" + color.toString()); + imageFiles[ImagePath::builtin(name)] = [this, color, amount, type](){ return createResourceWindow(type, amount, std::max(PlayerColor(0), color)); }; + }; + addResWindow("TPMRKABS", CreateResourceWindowType::ARTIFACTS_BUYING); + addResWindow("TPMRKASS", CreateResourceWindowType::ARTIFACTS_SELLING); + addResWindow("TPMRKRES", CreateResourceWindowType::MARKET_RESOURCES); + addResWindow("TPMRKCRS", CreateResourceWindowType::FREELANCERS_GUILD); + addResWindow("TPMRKPTS", CreateResourceWindowType::TRANSFER_RESOURCES); + } + } + imageFiles[ImagePath::builtin("stackWindow/info-panel-0.png")] = [this](){ return createCreatureInfoPanel(2);}; imageFiles[ImagePath::builtin("stackWindow/info-panel-1.png")] = [this](){ return createCreatureInfoPanel(3);}; imageFiles[ImagePath::builtin("stackWindow/info-panel-2.png")] = [this](){ return createCreatureInfoPanel(4);}; @@ -610,6 +626,65 @@ AssetGenerator::CanvasPtr AssetGenerator::createCreatureInfoPanel(int boxesAmoun return image; } +AssetGenerator::CanvasPtr AssetGenerator::createResourceWindow(CreateResourceWindowType type, int count, PlayerColor color) const +{ + assert(count >= 8 && count <= 9); + + const std::map files = { + { ARTIFACTS_BUYING, ImagePath::builtin("TPMRKABS") }, + { ARTIFACTS_SELLING, ImagePath::builtin("TPMRKASS") }, + { MARKET_RESOURCES, ImagePath::builtin("TPMRKRES") }, + { FREELANCERS_GUILD, ImagePath::builtin("TPMRKCRS") }, + { TRANSFER_RESOURCES, ImagePath::builtin("TPMRKPTS") } + }; + + auto file = files.at(type); + auto locator = ImageLocator(file, EImageBlitMode::COLORKEY); + std::shared_ptr baseImg = ENGINE->renderHandler().loadImage(locator); + baseImg->playerColored(color); + + auto image = ENGINE->renderHandler().createImage(baseImg->dimensions(), CanvasScalingPolicy::IGNORE); + Canvas canvas = image->getCanvas(); + canvas.draw(baseImg, Point(0, 0)); + + auto drawBox = [&canvas, &baseImg](bool left, bool one){ + if(left) + { + canvas.draw(baseImg, Point(38, 339), Rect(121, 339, 71, 69)); + if(!one) + canvas.draw(baseImg, Point(204, 339), Rect(121, 339, 71, 69)); + } + else + { + canvas.draw(baseImg, Point(325, 339), Rect(408, 339, 71, 69)); + if(!one) + canvas.draw(baseImg, Point(491, 339), Rect(408, 339, 71, 69)); + } + }; + + switch (type) + { + case ARTIFACTS_BUYING: + drawBox(true, count == 8); + break; + case ARTIFACTS_SELLING: + drawBox(false, count == 8); + break; + case MARKET_RESOURCES: + drawBox(true, count == 8); + drawBox(false, count == 8); + break; + case FREELANCERS_GUILD: + drawBox(false, count == 8); + break; + case TRANSFER_RESOURCES: + drawBox(true, count == 8); + break; + } + + return image; +} + AssetGenerator::CanvasPtr AssetGenerator::createCreatureInfoPanelElement(CreatureInfoPanelElement element) const { std::map size { diff --git a/client/render/AssetGenerator.h b/client/render/AssetGenerator.h index 0259db88f..218c35054 100644 --- a/client/render/AssetGenerator.h +++ b/client/render/AssetGenerator.h @@ -55,6 +55,8 @@ private: CanvasPtr createPaletteShiftedImage(const AnimationPath & source, const std::vector & animation, int frameIndex, int paletteShiftCounter) const; CanvasPtr createAdventureMapButtonClear(const PlayerColor & player) const; CanvasPtr createCreatureInfoPanel(int boxesAmount) const; + enum CreateResourceWindowType{ ARTIFACTS_BUYING, ARTIFACTS_SELLING, MARKET_RESOURCES, FREELANCERS_GUILD, TRANSFER_RESOURCES }; + CanvasPtr createResourceWindow(CreateResourceWindowType type, int count, PlayerColor color) const; enum CreatureInfoPanelElement{ BONUS_EFFECTS, SPELL_EFFECTS, BUTTON_PANEL, COMMANDER_BACKGROUND, COMMANDER_ABILITIES }; CanvasPtr createCreatureInfoPanelElement(CreatureInfoPanelElement element) const; CanvasPtr createQuestWindow() const; From 8f00a1221e2fe836b74121464bb35322995e6f3b Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 21 Sep 2025 16:28:19 +0200 Subject: [PATCH 04/20] add scrolling for resources --- client/widgets/markets/CMarketBase.cpp | 41 ++++++++++------ client/widgets/markets/CMarketBase.h | 2 +- client/widgets/markets/CMarketResources.cpp | 6 ++- client/widgets/markets/TradePanels.cpp | 52 ++++++++++++++++++--- client/widgets/markets/TradePanels.h | 20 ++++---- client/windows/CMarketWindow.cpp | 30 ++++++++++-- client/windows/CMarketWindow.h | 2 + 7 files changed, 117 insertions(+), 36 deletions(-) diff --git a/client/widgets/markets/CMarketBase.cpp b/client/widgets/markets/CMarketBase.cpp index f318ed566..b76edb0b9 100644 --- a/client/widgets/markets/CMarketBase.cpp +++ b/client/widgets/markets/CMarketBase.cpp @@ -46,7 +46,8 @@ void CMarketBase::deselect() offerTradePanel->highlightedSlot->selectSlot(false); offerTradePanel->highlightedSlot.reset(); } - deal->block(true); + if(deal) + deal->block(true); bidQty = 0; offerQty = 0; updateShowcases(); @@ -54,7 +55,12 @@ void CMarketBase::deselect() void CMarketBase::onSlotClickPressed(const std::shared_ptr & newSlot, std::shared_ptr & curPanel) { - assert(newSlot); + if(!newSlot) + { + deselect(); + return; + } + assert(curPanel); if(newSlot == curPanel->highlightedSlot) return; @@ -81,13 +87,14 @@ void CMarketBase::updateSubtitlesForBid(EMarketMode marketMode, int bidId) } else { - for(const auto & slot : offerTradePanel->slots) - { - int slotBidQty = 0; - int slotOfferQty = 0; - market->getOffer(bidId, slot->id, slotBidQty, slotOfferQty, marketMode); - offerTradePanel->updateOffer(*slot, slotBidQty, slotOfferQty); - } + if(offerTradePanel) + for(const auto & slot : offerTradePanel->slots) + { + int slotBidQty = 0; + int slotOfferQty = 0; + market->getOffer(bidId, slot->id, slotBidQty, slotOfferQty, marketMode); + offerTradePanel->updateOffer(*slot, slotBidQty, slotOfferQty); + } } }; @@ -120,6 +127,11 @@ void CMarketBase::highlightingChanged() updateShowcases(); } +CMarketBase::MarketShowcasesParams CMarketBase::getShowcasesParams() const +{ + return {}; +} + CExperienceAltar::CExperienceAltar() { OBJECT_CONSTRUCTION; @@ -164,8 +176,9 @@ bool CCreaturesSelling::slotDeletingCheck(const std::shared_ptr void CCreaturesSelling::updateSubtitles() const { - for(const auto & heroSlot : bidTradePanel->slots) - heroSlot->subtitle->setText(std::to_string(this->hero->getStackCount(SlotID(heroSlot->serial)))); + if(bidTradePanel) + for(const auto & heroSlot : bidTradePanel->slots) + heroSlot->subtitle->setText(std::to_string(this->hero->getStackCount(SlotID(heroSlot->serial)))); } CResourcesBuying::CResourcesBuying(const CTradeableItem::ClickPressedFunctor & clickPressedCallback, @@ -188,8 +201,9 @@ CResourcesSelling::CResourcesSelling(const CTradeableItem::ClickPressedFunctor & void CResourcesSelling::updateSubtitles() const { - for(const auto & slot : bidTradePanel->slots) - slot->subtitle->setText(std::to_string(GAME->interface()->cb->getResourceAmount(static_cast(slot->serial)))); + if(bidTradePanel) + for(const auto & slot : bidTradePanel->slots) + slot->subtitle->setText(std::to_string(GAME->interface()->cb->getResourceAmount(static_cast(slot->serial)))); } CMarketSlider::CMarketSlider(const CSlider::SliderMovingFunctor & movingCallback) @@ -197,6 +211,7 @@ CMarketSlider::CMarketSlider(const CSlider::SliderMovingFunctor & movingCallback OBJECT_CONSTRUCTION; offerSlider = std::make_shared(Point(230, 489), 137, movingCallback, 0, 0, 0, Orientation::HORIZONTAL); + offerSlider->setScrollBounds(Rect()); maxAmount = std::make_shared(Point(228, 520), AnimationPath::builtin("IRCBTNS.DEF"), LIBRARY->generaltexth->zelp[596], [this]() { diff --git a/client/widgets/markets/CMarketBase.h b/client/widgets/markets/CMarketBase.h index 7b3e7ede3..1a1019eaf 100644 --- a/client/widgets/markets/CMarketBase.h +++ b/client/widgets/markets/CMarketBase.h @@ -58,7 +58,7 @@ protected: virtual void onSlotClickPressed(const std::shared_ptr & newSlot, std::shared_ptr & curPanel); virtual void updateSubtitlesForBid(EMarketMode marketMode, int bidId); virtual void updateShowcases(); - virtual MarketShowcasesParams getShowcasesParams() const = 0; + virtual MarketShowcasesParams getShowcasesParams() const; virtual void highlightingChanged(); }; diff --git a/client/widgets/markets/CMarketResources.cpp b/client/widgets/markets/CMarketResources.cpp index 275ef31ca..f949685ea 100644 --- a/client/widgets/markets/CMarketResources.cpp +++ b/client/widgets/markets/CMarketResources.cpp @@ -98,8 +98,10 @@ void CMarketResources::highlightingChanged() void CMarketResources::updateSubtitles() { CMarketBase::updateSubtitlesForBid(EMarketMode::RESOURCE_RESOURCE, bidTradePanel->getHighlightedItemId()); - if(bidTradePanel->highlightedSlot) - offerTradePanel->slots[bidTradePanel->highlightedSlot->serial]->subtitle->setText(LIBRARY->generaltexth->allTexts[164]); // n/a + if(bidTradePanel && bidTradePanel->highlightedSlot) + for(auto & slot : offerTradePanel->slots) + if(slot->id == bidTradePanel->highlightedSlot->id) + slot->subtitle->setText(LIBRARY->generaltexth->allTexts[164]); // n/a } std::string CMarketResources::getTraderText() diff --git a/client/widgets/markets/TradePanels.cpp b/client/widgets/markets/TradePanels.cpp index c8dfc71b5..42bcc7ab9 100644 --- a/client/widgets/markets/TradePanels.cpp +++ b/client/widgets/markets/TradePanels.cpp @@ -13,6 +13,7 @@ #include "../../GameEngine.h" #include "../../GameInstance.h" #include "../../render/Canvas.h" +#include "../../widgets/Slider.h" #include "../../widgets/TextControls.h" #include "../../windows/InfoWindows.h" @@ -261,20 +262,59 @@ bool TradePanelBase::isHighlighted() const ResourcesPanel::ResourcesPanel(const CTradeableItem::ClickPressedFunctor & clickPressedCallback, const UpdateSlotsFunctor & updateSubtitles) + : clickPressedCallback(std::move(clickPressedCallback)) { - assert(resourcesForTrade.size() == slotsPos.size()); OBJECT_CONSTRUCTION; - for(const auto & res : resourcesForTrade) + // TODO: replace with LIBRARY->resourceTypeHandler->getAllObjects() -> also check order after doing this + resourcesForTrade = { - auto slot = slots.emplace_back(std::make_shared(Rect(slotsPos[res.num], slotDimension), EType::RESOURCE, res.num, res.num)); - slot->clickPressedCallback = clickPressedCallback; - slot->setSelectionWidth(selectionWidth); + GameResID::WOOD, GameResID::MERCURY, GameResID::ORE, + GameResID::SULFUR, GameResID::CRYSTAL, GameResID::GEMS, + GameResID::GOLD, + GameResID::GOLD, // TODO: remove + GameResID::GOLD, // TODO: remove + GameResID::GOLD // TODO: remove + }; + + int lines = vstd::divideAndCeil(resourcesForTrade.size(), 3); + if(lines > 3) + { + slider = std::make_shared(Point(240, 0), 224, [this](int to){ updateSlots(to); setRedrawParent(true); redraw(); }, 3, lines, 0, Orientation::VERTICAL, CSlider::BROWN); + slider->setPanningStep(72); + slider->setScrollBounds(Rect(-240, 0, 240, 224)); } - updateSlotsCallback = updateSubtitles; + + updateSlotsCallback = std::move(updateSubtitles); + + updateSlots(0); showcaseSlot = std::make_shared(Rect(selectedPos, slotDimension), EType::RESOURCE, 0, 0); } +void ResourcesPanel::updateSlots(int line) +{ + OBJECT_CONSTRUCTION; + + clickPressedCallback(nullptr); + + int offset = line * 3; + slots.clear(); + for (int i = 0; i < std::min(static_cast(resourcesForTrade.size() - offset), 9); i++) + { + const auto& res = resourcesForTrade[i + offset]; + auto slotPos = slotsPos[i]; + if(resourcesForTrade.size() == 7 && i == 6) // for 7 ressources place gold in the middle + slotPos = slotsPos[i + 1]; + + auto slotPtr = std::make_shared(Rect(slotPos, slotDimension), EType::RESOURCE, res.num, res.num); + slotPtr->clickPressedCallback = clickPressedCallback; + slotPtr->setSelectionWidth(selectionWidth); + slots.push_back(slotPtr); + } + + updateSlotsCallback(); +} + ArtifactsPanel::ArtifactsPanel(const CTradeableItem::ClickPressedFunctor & clickPressedCallback, const UpdateSlotsFunctor & updateSubtitles, const std::vector & arts) { diff --git a/client/widgets/markets/TradePanels.h b/client/widgets/markets/TradePanels.h index 3e2610106..007f88d3f 100644 --- a/client/widgets/markets/TradePanels.h +++ b/client/widgets/markets/TradePanels.h @@ -14,6 +14,8 @@ #include "../../../lib/networkPacks/TradeItem.h" +class CSlider; + enum class EType { RESOURCE, PLAYER, ARTIFACT_TYPE, CREATURE, ARTIFACT @@ -68,21 +70,21 @@ public: class ResourcesPanel : public TradePanelBase { - const std::vector resourcesForTrade = - { - GameResID::WOOD, GameResID::MERCURY, GameResID::ORE, - GameResID::SULFUR, GameResID::CRYSTAL, GameResID::GEMS, - GameResID::GOLD - }; const std::vector slotsPos = { - Point(0, 0), Point(83, 0), Point(166, 0), - Point(0, 79), Point(83, 79), Point(166, 79), - Point(83, 158) + Point(0, 0), Point(83, 0), Point(166, 0), + Point(0, 79), Point(83, 79), Point(166, 79), + Point(0, 158), Point(83, 158), Point(166, 158) }; const Point slotDimension = Point(69, 66); const Point selectedPos = Point(83, 267); + CTradeableItem::ClickPressedFunctor clickPressedCallback; + + std::vector resourcesForTrade; + std::shared_ptr slider; + + void updateSlots(int line); public: ResourcesPanel(const CTradeableItem::ClickPressedFunctor & clickPressedCallback, const UpdateSlotsFunctor & updateSubtitles); }; diff --git a/client/windows/CMarketWindow.cpp b/client/windows/CMarketWindow.cpp index 95b567905..65aa26b62 100644 --- a/client/windows/CMarketWindow.cpp +++ b/client/windows/CMarketWindow.cpp @@ -200,11 +200,23 @@ std::string CMarketWindow::getMarketTitle(const ObjectInstanceID marketId, const return GAME->interface()->cb->getObj(marketId)->getObjectName(); } +ImagePath CMarketWindow::getImagePathBasedOnResources(std::string name) +{ + int res = 9; //TODO: replace with LIBRARY->resourceTypeHandler->getAllObjects(); + if(res == 8) + name += "-R8"; + else if(res > 8) + name += "-R9"; + return ImagePath::builtin(name); +} + void CMarketWindow::createArtifactsBuying(const IMarket * market, const CGHeroInstance * hero) { OBJECT_CONSTRUCTION; - background = createBg(ImagePath::builtin("TPMRKABS.bmp"), PLAYER_COLORED); + auto image = getImagePathBasedOnResources("TPMRKABS"); + + background = createBg(image, PLAYER_COLORED); marketWidget = std::make_shared(market, hero, getMarketTitle(market->getObjInstanceID(), EMarketMode::RESOURCE_ARTIFACT)); initWidgetInternals(EMarketMode::RESOURCE_ARTIFACT, LIBRARY->generaltexth->zelp[600]); } @@ -213,7 +225,9 @@ void CMarketWindow::createArtifactsSelling(const IMarket * market, const CGHeroI { OBJECT_CONSTRUCTION; - background = createBg(ImagePath::builtin("TPMRKASS.bmp"), PLAYER_COLORED); + auto image = getImagePathBasedOnResources("TPMRKASS"); + + background = createBg(image, PLAYER_COLORED); // Create image that copies part of background containing slot MISC_1 into position of slot MISC_5 artSlotBack = std::make_shared(background->getSurface(), Rect(20, 187, 47, 47), 0, 0); artSlotBack->moveTo(pos.topLeft() + Point(18, 339)); @@ -229,7 +243,9 @@ void CMarketWindow::createMarketResources(const IMarket * market, const CGHeroIn { OBJECT_CONSTRUCTION; - background = createBg(ImagePath::builtin("TPMRKRES.bmp"), PLAYER_COLORED); + auto image = getImagePathBasedOnResources("TPMRKRES"); + + background = createBg(image, PLAYER_COLORED); marketWidget = std::make_shared(market, hero); initWidgetInternals(EMarketMode::RESOURCE_RESOURCE, LIBRARY->generaltexth->zelp[600]); } @@ -238,7 +254,9 @@ void CMarketWindow::createFreelancersGuild(const IMarket * market, const CGHeroI { OBJECT_CONSTRUCTION; - background = createBg(ImagePath::builtin("TPMRKCRS.bmp"), PLAYER_COLORED); + auto image = getImagePathBasedOnResources("TPMRKCRS"); + + background = createBg(image, PLAYER_COLORED); marketWidget = std::make_shared(market, hero); initWidgetInternals(EMarketMode::CREATURE_RESOURCE, LIBRARY->generaltexth->zelp[600]); } @@ -247,7 +265,9 @@ void CMarketWindow::createTransferResources(const IMarket * market, const CGHero { OBJECT_CONSTRUCTION; - background = createBg(ImagePath::builtin("TPMRKPTS.bmp"), PLAYER_COLORED); + auto image = getImagePathBasedOnResources("TPMRKPTS"); + + background = createBg(image, PLAYER_COLORED); marketWidget = std::make_shared(market, hero); initWidgetInternals(EMarketMode::RESOURCE_PLAYER, LIBRARY->generaltexth->zelp[600]); } diff --git a/client/windows/CMarketWindow.h b/client/windows/CMarketWindow.h index f2874918e..6707aacdd 100644 --- a/client/windows/CMarketWindow.h +++ b/client/windows/CMarketWindow.h @@ -38,6 +38,8 @@ private: void createAltarArtifacts(const IMarket * market, const CGHeroInstance * hero); void createAltarCreatures(const IMarket * market, const CGHeroInstance * hero); + ImagePath getImagePathBasedOnResources(std::string name); + const int buttonHeightWithMargin = 32 + 3; std::vector> changeModeButtons; std::shared_ptr quitButton; From 1d09700f6d8a577431d07e0a6ed04c0b8d410ff8 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 21 Sep 2025 16:35:49 +0200 Subject: [PATCH 05/20] sett scroll bound --- client/widgets/markets/CMarketBase.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/widgets/markets/CMarketBase.cpp b/client/widgets/markets/CMarketBase.cpp index b76edb0b9..afe95f0e0 100644 --- a/client/widgets/markets/CMarketBase.cpp +++ b/client/widgets/markets/CMarketBase.cpp @@ -211,7 +211,7 @@ CMarketSlider::CMarketSlider(const CSlider::SliderMovingFunctor & movingCallback OBJECT_CONSTRUCTION; offerSlider = std::make_shared(Point(230, 489), 137, movingCallback, 0, 0, 0, Orientation::HORIZONTAL); - offerSlider->setScrollBounds(Rect()); + offerSlider->setScrollBounds(Rect(-215, -50, 575, 120)); maxAmount = std::make_shared(Point(228, 520), AnimationPath::builtin("IRCBTNS.DEF"), LIBRARY->generaltexth->zelp[596], [this]() { From d7ba132fc1dfad6b5f616fb4a809ee6b9365d9a8 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 28 Sep 2025 21:36:46 +0200 Subject: [PATCH 06/20] remove todo --- client/widgets/markets/TradePanels.cpp | 11 +---------- client/windows/CMarketWindow.cpp | 3 ++- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/client/widgets/markets/TradePanels.cpp b/client/widgets/markets/TradePanels.cpp index 4a7637afb..eeb87b80d 100644 --- a/client/widgets/markets/TradePanels.cpp +++ b/client/widgets/markets/TradePanels.cpp @@ -267,16 +267,7 @@ ResourcesPanel::ResourcesPanel(const CTradeableItem::ClickPressedFunctor & click { OBJECT_CONSTRUCTION; - // TODO: replace with LIBRARY->resourceTypeHandler->getAllObjects() -> also check order after doing this - resourcesForTrade = - { - GameResID::WOOD, GameResID::MERCURY, GameResID::ORE, - GameResID::SULFUR, GameResID::CRYSTAL, GameResID::GEMS, - GameResID::GOLD, - GameResID::GOLD, // TODO: remove - GameResID::GOLD, // TODO: remove - GameResID::GOLD // TODO: remove - }; + resourcesForTrade = LIBRARY->resourceTypeHandler->getAllObjects(); int lines = vstd::divideAndCeil(resourcesForTrade.size(), 3); if(lines > 3) diff --git a/client/windows/CMarketWindow.cpp b/client/windows/CMarketWindow.cpp index 65aa26b62..cf4b7264d 100644 --- a/client/windows/CMarketWindow.cpp +++ b/client/windows/CMarketWindow.cpp @@ -30,6 +30,7 @@ #include "../../lib/GameLibrary.h" #include "../../lib/callback/CCallback.h" #include "../../lib/entities/building/CBuilding.h" +#include "../../lib/entities/ResourceTypeHandler.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/CGMarket.h" #include "../../lib/mapObjects/CGTownInstance.h" @@ -202,7 +203,7 @@ std::string CMarketWindow::getMarketTitle(const ObjectInstanceID marketId, const ImagePath CMarketWindow::getImagePathBasedOnResources(std::string name) { - int res = 9; //TODO: replace with LIBRARY->resourceTypeHandler->getAllObjects(); + int res = LIBRARY->resourceTypeHandler->getAllObjects().size(); if(res == 8) name += "-R8"; else if(res > 8) From ea261ae48faa53d7559812027191a35f7f5727e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Zaremba?= Date: Tue, 9 Sep 2025 15:28:59 +0200 Subject: [PATCH 07/20] Rework handling some errors and uncought exceptions --- Mods/vcmi/Content/config/chinese.json | 2 +- Mods/vcmi/Content/config/czech.json | 2 +- Mods/vcmi/Content/config/english.json | 3 +- Mods/vcmi/Content/config/german.json | 2 +- Mods/vcmi/Content/config/hungarian.json | 2 +- Mods/vcmi/Content/config/italian.json | 2 +- Mods/vcmi/Content/config/polish.json | 3 +- Mods/vcmi/Content/config/portuguese.json | 2 +- Mods/vcmi/Content/config/russian.json | 2 +- Mods/vcmi/Content/config/spanish.json | 2 +- Mods/vcmi/Content/config/swedish.json | 2 +- Mods/vcmi/Content/config/ukrainian.json | 2 +- Mods/vcmi/Content/config/vietnamese.json | 2 +- client/CServerHandler.cpp | 16 +-- client/NetPacksLobbyClient.cpp | 2 +- .../CObjectClassesHandler.cpp | 6 +- lib/modding/ModIncompatibility.h | 19 +++ mapeditor/mainwindow.cpp | 12 +- server/CVCMIServer.cpp | 115 ++++++++++-------- server/CVCMIServer.h | 2 +- 20 files changed, 117 insertions(+), 83 deletions(-) diff --git a/Mods/vcmi/Content/config/chinese.json b/Mods/vcmi/Content/config/chinese.json index 943ab8ead..30d03b445 100644 --- a/Mods/vcmi/Content/config/chinese.json +++ b/Mods/vcmi/Content/config/chinese.json @@ -249,7 +249,7 @@ "vcmi.server.errors.existingProcess" : "一个VCMI进程已经在运行,启动新进程前请结束它。", "vcmi.server.errors.modsToEnable" : "{需要启用的mod列表}", "vcmi.server.errors.modsToDisable" : "{需要禁用的mod列表}", - "vcmi.server.errors.unknownEntity" : "加载保存失败! 在保存的游戏中发现未知实体'%s'! 保存可能与当前安装的mod版本不兼容!", + "vcmi.server.errors.saveFile.unknownEntity" : "加载保存失败! 在保存的游戏中发现未知实体'%s'! 保存可能与当前安装的mod版本不兼容!", "vcmi.server.errors.wrongIdentified" : "你被识别为玩家%s,但预期是玩家%s。", "vcmi.server.errors.notAllowed" : "你无权执行此操作!", diff --git a/Mods/vcmi/Content/config/czech.json b/Mods/vcmi/Content/config/czech.json index f269c3188..80cf61ddc 100644 --- a/Mods/vcmi/Content/config/czech.json +++ b/Mods/vcmi/Content/config/czech.json @@ -249,7 +249,7 @@ "vcmi.server.errors.existingProcess" : "Již běží jiný server VCMI. Prosím, ukončete ho před startem nové hry.", "vcmi.server.errors.modsToEnable" : "{Následující modifikace jsou nutné pro načtení hry}", "vcmi.server.errors.modsToDisable" : "{Následující modifikace musí být zakázány}", - "vcmi.server.errors.unknownEntity" : "Nelze načíst uloženou pozici! Neznámá entita '%s' nalezena v uložené pozici! Uložná pozice nemusí být kompatibilní s aktuálními verzemi modifikací!", + "vcmi.server.errors.saveFile.unknownEntity" : "Nelze načíst uloženou pozici! Neznámá entita '%s' nalezena v uložené pozici! Uložná pozice nemusí být kompatibilní s aktuálními verzemi modifikací!", "vcmi.server.errors.wrongIdentified" : "Byli jste identifikováni jako hráč %s, zatímco byl očekáván hráč %s.", "vcmi.server.errors.notAllowed" : "Nemáte oprávnění provést tuto akci!", diff --git a/Mods/vcmi/Content/config/english.json b/Mods/vcmi/Content/config/english.json index d926c7940..28c1bec88 100644 --- a/Mods/vcmi/Content/config/english.json +++ b/Mods/vcmi/Content/config/english.json @@ -249,7 +249,8 @@ "vcmi.server.errors.existingProcess" : "Another VCMI server process is running. Please terminate it before starting a new game.", "vcmi.server.errors.modsToEnable" : "{Following mods are required}", "vcmi.server.errors.modsToDisable" : "{Following mods must be disabled}", - "vcmi.server.errors.unknownEntity" : "Failed to load save! Unknown entity '%s' found in saved game! Save may not be compatible with currently installed version of mods!", + "vcmi.server.errors.saveFile.unknownEntity" : "Failed to load save! Unknown entity '%s' found in saved game! Save may not be compatible with currently installed version of mods!", + "vcmi.server.errors.campOrMapFile.unknownEntity" : "Failed to load file! Unknown entity '%s' found! File may not be compatible with currently installed version of mods!", "vcmi.server.errors.wrongIdentified" : "You were identified as player %s while expecting %s", "vcmi.server.errors.notAllowed" : "You are not allowed to perform this action!", diff --git a/Mods/vcmi/Content/config/german.json b/Mods/vcmi/Content/config/german.json index 6bd8535d4..a2a956ef5 100644 --- a/Mods/vcmi/Content/config/german.json +++ b/Mods/vcmi/Content/config/german.json @@ -249,7 +249,7 @@ "vcmi.server.errors.existingProcess" : "Es läuft ein weiterer vcmiserver-Prozess, bitte beendet diesen zuerst", "vcmi.server.errors.modsToEnable" : "{Erforderliche Mods um das Spiel zu laden}", "vcmi.server.errors.modsToDisable" : "{Folgende Mods müssen deaktiviert werden}", - "vcmi.server.errors.unknownEntity" : "Spielstand konnte nicht geladen werden! Unbekannte Entität '%s' im gespeicherten Spiel gefunden! Der Spielstand ist möglicherweise nicht mit der aktuell installierten Version der Mods kompatibel!", + "vcmi.server.errors.saveFile.unknownEntity" : "Spielstand konnte nicht geladen werden! Unbekannte Entität '%s' im gespeicherten Spiel gefunden! Der Spielstand ist möglicherweise nicht mit der aktuell installierten Version der Mods kompatibel!", "vcmi.server.errors.wrongIdentified" : "Ihr wurdet als Spieler %s identifiziert, während %s erwartet wurde", "vcmi.server.errors.notAllowed" : "Ihr dürft diese Aktion nicht durchführen!", diff --git a/Mods/vcmi/Content/config/hungarian.json b/Mods/vcmi/Content/config/hungarian.json index 49c254ad4..312a5089b 100644 --- a/Mods/vcmi/Content/config/hungarian.json +++ b/Mods/vcmi/Content/config/hungarian.json @@ -225,7 +225,7 @@ "vcmi.server.errors.existingProcess" : "Egy másik VCMI szerver folyamat fut. Kérjük, zárja be, mielőtt új játékot indítana.", "vcmi.server.errors.modsToEnable" : "{Az alábbi modok szükségesek}", "vcmi.server.errors.modsToDisable" : "{Az alábbi modokat le kell tiltani}", - "vcmi.server.errors.unknownEntity" : "Nem sikerült betölteni a mentést! Ismeretlen entitás '%s' található a mentett játékban! A mentés nem kompatibilis a jelenleg telepített modverziókkal!", + "vcmi.server.errors.saveFile.unknownEntity" : "Nem sikerült betölteni a mentést! Ismeretlen entitás '%s' található a mentett játékban! A mentés nem kompatibilis a jelenleg telepített modverziókkal!", "vcmi.server.errors.wrongIdentified" : "Önt %s játékosként azonosították, miközben %s játékosra számítottak", "vcmi.server.errors.notAllowed" : "Nem engedélyezett művelet!", diff --git a/Mods/vcmi/Content/config/italian.json b/Mods/vcmi/Content/config/italian.json index 9e1a5a621..0b581b723 100644 --- a/Mods/vcmi/Content/config/italian.json +++ b/Mods/vcmi/Content/config/italian.json @@ -227,7 +227,7 @@ "vcmi.server.errors.existingProcess" : "Un altro processo del server VCMI è in esecuzione. Terminarlo prima di avviare una nuova partita.", "vcmi.server.errors.modsToEnable" : "{Le seguenti mod sono richieste}", "vcmi.server.errors.modsToDisable" : "{Le seguenti mod devono essere disattivate}", - "vcmi.server.errors.unknownEntity" : "Impossibile caricare il salvataggio! Entità sconosciuta '%s' trovata nel salvataggio! Il salvataggio potrebbe non essere compatibile con la versione attualmente installata delle mod!", + "vcmi.server.errors.saveFile.unknownEntity" : "Impossibile caricare il salvataggio! Entità sconosciuta '%s' trovata nel salvataggio! Il salvataggio potrebbe non essere compatibile con la versione attualmente installata delle mod!", "vcmi.server.errors.wrongIdentified" : "Sei stato identificato come giocatore %s mentre ci si aspettava %s", "vcmi.server.errors.notAllowed" : "Non ti è permesso eseguire questa azione!", diff --git a/Mods/vcmi/Content/config/polish.json b/Mods/vcmi/Content/config/polish.json index 7b0fe3d68..4997ba2df 100644 --- a/Mods/vcmi/Content/config/polish.json +++ b/Mods/vcmi/Content/config/polish.json @@ -250,7 +250,8 @@ "vcmi.server.errors.modsToDisable" : "{Następujące mody muszą zostać wyłączone}", "vcmi.server.errors.modDependencyLoop" : "Nie udało się wczytać moda {'%s'}!\n Być może znajduje się w pętli zależności", "vcmi.server.errors.notAllowed" : "To działanie nie jest dozwolone!", - "vcmi.server.errors.unknownEntity" : "Nie udało się wczytać zapisu! Nieznany element '%s' znaleziony w pliku zapisu! Zapis może nie być zgodny z aktualnie zainstalowaną wersją modów!", + "vcmi.server.errors.saveFile.unknownEntity" : "Nie udało się wczytać zapisu! Nieznany element '%s' znaleziony w pliku zapisu! Zapis może nie być zgodny z aktualnie zainstalowaną wersją modów!", + "vcmi.server.errors.campOrMapFile.unknownEntity" : "Nie udało się wczytać pliku! Nieznany element '%s' znaleziony w pliku! Plik może nie być zgodny z aktualnie zainstalowaną wersją modów!", "vcmi.server.errors.wrongIdentified" : "Zostałeś zidentyfikowany jako gracz %s natomiast powinieneś być %s", "vcmi.dimensionDoor.seaToLandError" : "Nie jest możliwa teleportacja przez drzwi wymiarów z wód na ląd i na odwrót.", diff --git a/Mods/vcmi/Content/config/portuguese.json b/Mods/vcmi/Content/config/portuguese.json index 949819736..37772b37e 100644 --- a/Mods/vcmi/Content/config/portuguese.json +++ b/Mods/vcmi/Content/config/portuguese.json @@ -244,7 +244,7 @@ "vcmi.server.errors.existingProcess" : "Outro processo do servidor VCMI está em execução. Por favor, termine-o antes de iniciar um novo jogo.", "vcmi.server.errors.modsToEnable" : "{Os seguintes mods são necessários}", "vcmi.server.errors.modsToDisable" : "{Os seguintes mods devem ser desativados}", - "vcmi.server.errors.unknownEntity" : "Falha ao carregar o jogo salvo! Entidade desconhecida '%s' encontrada no jogo salvo! O jogo salvo pode não ser compatível com a versão atualmente instalada dos mods!", + "vcmi.server.errors.saveFile.unknownEntity" : "Falha ao carregar o jogo salvo! Entidade desconhecida '%s' encontrada no jogo salvo! O jogo salvo pode não ser compatível com a versão atualmente instalada dos mods!", "vcmi.server.errors.wrongIdentified" : "Você foi identificado como o jogador %s, mas esperava-se %s", "vcmi.server.errors.notAllowed" : "Você não tem permissão para realizar esta ação!", diff --git a/Mods/vcmi/Content/config/russian.json b/Mods/vcmi/Content/config/russian.json index 1d74878f5..07dcfc1d4 100644 --- a/Mods/vcmi/Content/config/russian.json +++ b/Mods/vcmi/Content/config/russian.json @@ -225,7 +225,7 @@ "vcmi.server.errors.existingProcess" : "Запущен другой процесс сервера VCMI. Пожалуйста, завершите его перед запуском новой игры.", "vcmi.server.errors.modsToEnable" : "{Требуемые моды для загрузки игры}", "vcmi.server.errors.modsToDisable" : "{Необходимо отключить следующие моды}", - "vcmi.server.errors.unknownEntity" : "Не удалось загрузить сохранение! В сохраненной игре обнаружен неизвестный объект '%s'! Возможно, сохранение несовместимо с текущей версией модов!", + "vcmi.server.errors.saveFile.unknownEntity" : "Не удалось загрузить сохранение! В сохраненной игре обнаружен неизвестный объект '%s'! Возможно, сохранение несовместимо с текущей версией модов!", "vcmi.server.errors.wrongIdentified" : "Вы были идентифицированы как игрок %s, ожидалось %s", "vcmi.server.errors.notAllowed" : "Вам запрещено выполнять данное действие!", "vcmi.dimensionDoor.seaToLandError" : "Невозможно телепортироваться с моря на сушу или наоборот с помощью Двери Измерений.", diff --git a/Mods/vcmi/Content/config/spanish.json b/Mods/vcmi/Content/config/spanish.json index 32c6bdfad..cfd4b9730 100644 --- a/Mods/vcmi/Content/config/spanish.json +++ b/Mods/vcmi/Content/config/spanish.json @@ -76,7 +76,7 @@ "vcmi.server.errors.modsToEnable" : "{Se requieren los siguientes mods}", "vcmi.server.errors.modsToDisable" : "{Deben desactivarse los siguientes mods}", "vcmi.server.confirmReconnect" : "¿Quieres reconectar a la última sesión?", - "vcmi.server.errors.unknownEntity" : "Error al cargar la partida guardada. ¡Se encontró una entidad desconocida '%s' en la partida guardada! Es posible que la partida no sea compatible con la versión actualmente instalada de los mods.", + "vcmi.server.errors.saveFile.unknownEntity" : "Error al cargar la partida guardada. ¡Se encontró una entidad desconocida '%s' en la partida guardada! Es posible que la partida no sea compatible con la versión actualmente instalada de los mods.", "vcmi.settingsMainWindow.generalTab.hover" : "General", "vcmi.settingsMainWindow.generalTab.help" : "Cambiar a la pestaña de opciones generales, que contiene ajustes relacionados con el comportamiento general del juego", diff --git a/Mods/vcmi/Content/config/swedish.json b/Mods/vcmi/Content/config/swedish.json index bbecd006d..1cabde601 100644 --- a/Mods/vcmi/Content/config/swedish.json +++ b/Mods/vcmi/Content/config/swedish.json @@ -249,7 +249,7 @@ "vcmi.server.errors.existingProcess" : "En annan VCMI-serverprocess är igång. Vänligen avsluta den innan du startar ett nytt spel.", "vcmi.server.errors.modsToEnable" : "{Följande modd(ar) krävs}", "vcmi.server.errors.modsToDisable" : "{Följande modd(ar) måste inaktiveras}", - "vcmi.server.errors.unknownEntity" : "Misslyckades med att ladda sparat spel! Okänd enhet '%s' hittades i sparat spel! Sparningen kanske inte är kompatibel med den aktuella versionen av moddarna!", + "vcmi.server.errors.saveFile.unknownEntity" : "Misslyckades med att ladda sparat spel! Okänd enhet '%s' hittades i sparat spel! Sparningen kanske inte är kompatibel med den aktuella versionen av moddarna!", "vcmi.server.errors.wrongIdentified" : "Du identifierades som spelare %s när du förväntade dig %s", "vcmi.server.errors.notAllowed" : "Du får inte utföra denna åtgärd!", diff --git a/Mods/vcmi/Content/config/ukrainian.json b/Mods/vcmi/Content/config/ukrainian.json index abe421a74..829f9c04c 100644 --- a/Mods/vcmi/Content/config/ukrainian.json +++ b/Mods/vcmi/Content/config/ukrainian.json @@ -241,7 +241,7 @@ "vcmi.server.errors.existingProcess" : "Працює інший процес vcmiserver, будь ласка, спочатку завершіть його", "vcmi.server.errors.modsToEnable" : "{Потрібні модифікації для завантаження гри}", "vcmi.server.errors.modsToDisable" : "{Модифікації що мають бути вимкнені}", - "vcmi.server.errors.unknownEntity" : "Не вдалося завантажити гру! У збереженій грі знайдено невідомий об'єкт '%s'! Це збереження може бути несумісним зі встановленою версією модифікацій!", + "vcmi.server.errors.saveFile.unknownEntity" : "Не вдалося завантажити гру! У збереженій грі знайдено невідомий об'єкт '%s'! Це збереження може бути несумісним зі встановленою версією модифікацій!", "vcmi.server.errors.wrongIdentified" : "Ви були ідентифіковані як гравець %s, хоча очікували %s", "vcmi.server.errors.notAllowed" : "Ви не можете виконати цю дію!", diff --git a/Mods/vcmi/Content/config/vietnamese.json b/Mods/vcmi/Content/config/vietnamese.json index 7757815e6..915dfaebf 100644 --- a/Mods/vcmi/Content/config/vietnamese.json +++ b/Mods/vcmi/Content/config/vietnamese.json @@ -225,7 +225,7 @@ "vcmi.server.errors.existingProcess" : "Một chương trình máy chủ VCMI khác đang chạy. Hãy đóng nó trước khi bắt đầu một trò chơi mới.", "vcmi.server.errors.modsToEnable" : "{Các mod sau đây là bắt buộc}", "vcmi.server.errors.modsToDisable" : "{Bạn phải tắt các mod sau đây}", - "vcmi.server.errors.unknownEntity" : "Không tải được tệp tin đã lưu! Có lỗi chưa xác định trong tệp tin đã lưu '%s'! Tệp tin có thể không tương thích với phiên bản mod hiện đang cài đặt!", + "vcmi.server.errors.saveFile.unknownEntity" : "Không tải được tệp tin đã lưu! Có lỗi chưa xác định trong tệp tin đã lưu '%s'! Tệp tin có thể không tương thích với phiên bản mod hiện đang cài đặt!", "vcmi.server.errors.wrongIdentified" : "Bạn được chỉ định là người chơi %s trong khi bạn muốn %s", "vcmi.server.errors.notAllowed" : "Bạn không được phép thực hiện hành động này!", diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 37cca84fd..cb80f843d 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -569,24 +569,14 @@ bool CServerHandler::validateGameStart(bool allowOnlyAI) const catch(ModIncompatibility & e) { logGlobal->warn("Incompatibility exception during start scenario: %s", e.what()); - std::string errorMsg; - if(!e.whatMissing().empty()) - { - errorMsg += LIBRARY->generaltexth->translate("vcmi.server.errors.modsToEnable") + '\n'; - errorMsg += e.whatMissing(); - } - if(!e.whatExcessive().empty()) - { - errorMsg += LIBRARY->generaltexth->translate("vcmi.server.errors.modsToDisable") + '\n'; - errorMsg += e.whatExcessive(); - } - showServerError(errorMsg); + + showServerError(e.getFullErrorMsg()); return false; } catch(std::exception & e) { logGlobal->error("Exception during startScenario: %s", e.what()); - showServerError( std::string("Unable to start map! Reason: ") + e.what()); + showServerError(std::string("Unable to start map!\nReason: ") + e.what()); return false; } diff --git a/client/NetPacksLobbyClient.cpp b/client/NetPacksLobbyClient.cpp index a419d60ca..f81006125 100644 --- a/client/NetPacksLobbyClient.cpp +++ b/client/NetPacksLobbyClient.cpp @@ -146,7 +146,6 @@ void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyRestartGame(LobbyRestartGame & void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyPrepareStartGame(LobbyPrepareStartGame & pack) { - handler.client = std::make_unique(); handler.logicConnection->enterLobbyConnectionMode(); } @@ -159,6 +158,7 @@ void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyStartGame(LobbyStartGame & pac handler.si = pack.initializedStartInfo; handler.si->mode = modeBackup; } + handler.client = std::make_unique(); handler.startGameplay(pack.initializedGameState); } diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.cpp b/lib/mapObjectConstructors/CObjectClassesHandler.cpp index 839ed55bf..503ddd2be 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.cpp +++ b/lib/mapObjectConstructors/CObjectClassesHandler.cpp @@ -408,9 +408,9 @@ TObjectTypeHandler CObjectClassesHandler::getHandlerFor(const std::string & scop return object->objectTypeHandlers.at(subID.value()); } - std::string errorString = "Failed to find object of type " + type + "::" + subtype; - logGlobal->error(errorString); - throw std::runtime_error(errorString); + std::string objectType = type + "::" + subtype; + logGlobal->error("Failed to find object of type %s", objectType); + throw IdentifierResolutionException(objectType); } TObjectTypeHandler CObjectClassesHandler::getHandlerFor(CompoundMapObjectID compoundIdentifier) const diff --git a/lib/modding/ModIncompatibility.h b/lib/modding/ModIncompatibility.h index e21ad8bff..86bab7c90 100644 --- a/lib/modding/ModIncompatibility.h +++ b/lib/modding/ModIncompatibility.h @@ -9,6 +9,9 @@ */ #pragma once +#include "../lib/texts/CGeneralTextHandler.h" +#include "GameLibrary.h" + VCMI_LIB_NAMESPACE_BEGIN class DLL_LINKAGE ModIncompatibility: public std::exception @@ -49,6 +52,22 @@ public: return messageExcessiveMods; } + std::string getFullErrorMsg() const noexcept + { + std::string errorMsg; + if(!messageMissingMods.empty()) + { + errorMsg += LIBRARY->generaltexth->translate("vcmi.server.errors.modsToEnable") + '\n'; + errorMsg += messageMissingMods; + } + if(!messageExcessiveMods.empty()) + { + errorMsg += LIBRARY->generaltexth->translate("vcmi.server.errors.modsToDisable") + '\n'; + errorMsg += messageExcessiveMods; + } + return errorMsg; + } + private: std::string messageMissingMods; std::string messageExcessiveMods; diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index cdbda068a..a76a1181e 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -467,9 +467,19 @@ bool MainWindow::openMap(const QString & filenameSelect) catch(const ModIncompatibility & e) { assert(e.whatExcessive().empty()); - QMessageBox::warning(this, tr("Mods are required"), QString::fromStdString(e.whatMissing())); + auto qstrError = QString::fromStdString(e.getFullErrorMsg()).remove('{').remove('}'); + QMessageBox::warning(this, tr("Mods are required"), qstrError); return false; } + catch(const IdentifierResolutionException & e) + { + MetaString errorMsg; + errorMsg.appendTextID("vcmi.server.errors.campOrMapFile.unknownEntity"); + errorMsg.replaceRawString(e.identifierName); + QMessageBox::critical(this, tr("Failed to open map"), QString::fromStdString(errorMsg.toString())); + return false; + } + catch(const std::exception & e) { QMessageBox::critical(this, tr("Failed to open map"), tr(e.what())); diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index 4dc44cb64..72e4f1ccd 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -230,9 +230,6 @@ bool CVCMIServer::prepareToStartGame() Load::Progress current(1); progressTracking.include(current); - if (lobbyProcessor) - lobbyProcessor->sendGameStarted(); - auto progressTrackingThread = std::thread([this, &progressTracking]() { setThreadName("progressTrackingThread"); @@ -252,43 +249,72 @@ bool CVCMIServer::prepareToStartGame() } }); - gh = std::make_shared(*this); - switch(si->mode) + auto newGH = std::make_shared(*this); + bool started = false; + try { - case EStartMode::CAMPAIGN: - logNetwork->info("Preparing to start new campaign"); - si->startTime = std::time(nullptr); - si->fileURI = mi->fileURI; - si->campState->setCurrentMap(campaignMap); - si->campState->setCurrentMapBonus(campaignBonus); - gh->init(si.get(), progressTracking); - break; - - case EStartMode::NEW_GAME: - logNetwork->info("Preparing to start new game"); - si->startTime = std::time(nullptr); - si->fileURI = mi->fileURI; - gh->init(si.get(), progressTracking); - break; - - case EStartMode::LOAD_GAME: - logNetwork->info("Preparing to start loaded game"); - if(!loadSavedGame(*si)) + switch(si->mode) { - current.finish(); - progressTrackingThread.join(); - return false; - } - break; - default: - logNetwork->error("Wrong mode in StartInfo!"); - assert(0); - break; - } + case EStartMode::CAMPAIGN: + logNetwork->info("Preparing to start new campaign"); + si->startTime = std::time(nullptr); + si->fileURI = mi->fileURI; + si->campState->setCurrentMap(campaignMap); + si->campState->setCurrentMapBonus(campaignBonus); + newGH->init(si.get(), progressTracking); // may throw + started = true; + break; + case EStartMode::NEW_GAME: + logNetwork->info("Preparing to start new game"); + si->startTime = std::time(nullptr); + si->fileURI = mi->fileURI; + newGH->init(si.get(), progressTracking); // may throw + started = true; + break; + case EStartMode::LOAD_GAME: + logNetwork->info("Preparing to start loaded game"); + if(loadSavedGame(*newGH, *si)) + started = true; + break; + default: + logNetwork->error("Wrong mode in StartInfo!"); + assert(0); + break; + } + } + catch(const ModIncompatibility & e) + { + logGlobal->error("Failed to launch game: %s", e.what()); + announceMessage(e.getFullErrorMsg()); + } + catch(const IdentifierResolutionException & e) + { + logGlobal->error("Failed to launch game: %s", e.what()); + MetaString errorMsg; + errorMsg.appendTextID("vcmi.server.errors.campOrMapFile.unknownEntity"); + errorMsg.replaceRawString(e.identifierName); + announceMessage(errorMsg); + } + catch(const std::exception & e) + { + logGlobal->error("Failed to launch game: %s", e.what()); + auto str = MetaString::createFromTextID("vcmi.broadcast.failedLoadGame"); + str.appendRawString(":\n"); + str.appendRawString(e.what()); + announceMessage(str); + } current.finish(); progressTrackingThread.join(); + if (!started) + return false; + + gh = std::move(newGH); + + if(lobbyProcessor) + lobbyProcessor->sendGameStarted(); + return true; } @@ -1066,36 +1092,23 @@ INetworkServer & CVCMIServer::getNetworkServer() return *networkServer; } -bool CVCMIServer::loadSavedGame(const StartInfo &info) +bool CVCMIServer::loadSavedGame(CGameHandler & handler, const StartInfo & info) { try { - gh->load(info); + handler.load(info); } catch(const ModIncompatibility & e) { logGlobal->error("Failed to load game: %s", e.what()); - MetaString errorMsg; - if(!e.whatMissing().empty()) - { - errorMsg.appendTextID("vcmi.server.errors.modsToEnable"); - errorMsg.appendRawString("\n"); - errorMsg.appendRawString(e.whatMissing()); - } - if(!e.whatExcessive().empty()) - { - errorMsg.appendTextID("vcmi.server.errors.modsToDisable"); - errorMsg.appendRawString("\n"); - errorMsg.appendRawString(e.whatExcessive()); - } - announceMessage(errorMsg); + announceMessage(e.getFullErrorMsg()); return false; } catch(const IdentifierResolutionException & e) { logGlobal->error("Failed to load game: %s", e.what()); MetaString errorMsg; - errorMsg.appendTextID("vcmi.server.errors.unknownEntity"); + errorMsg.appendTextID("vcmi.server.errors.saveFile.unknownEntity"); errorMsg.replaceRawString(e.identifierName); announceMessage(errorMsg); return false; diff --git a/server/CVCMIServer.h b/server/CVCMIServer.h index 41b392e90..dbabf63a1 100644 --- a/server/CVCMIServer.h +++ b/server/CVCMIServer.h @@ -55,7 +55,7 @@ class CVCMIServer : public LobbyInfo, public INetworkServerListener, public INet bool runByClient; - bool loadSavedGame(const StartInfo &info); + bool loadSavedGame(CGameHandler & handler, const StartInfo & info); public: // IGameServer impl From 56db9e5f34ecbe8111bdc09d79bcc9c046770e40 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 4 Oct 2025 15:22:05 +0200 Subject: [PATCH 08/20] adjusted calendar icon to h3 look --- .../Sprites/lobby/selectionTabSortDate.png | Bin 994 -> 1275 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Mods/vcmi/Content/Sprites/lobby/selectionTabSortDate.png b/Mods/vcmi/Content/Sprites/lobby/selectionTabSortDate.png index 227841c56e129817e23b5f87955dc45d0a2d43d0..61f60ce0cdbf479b5bb42cf480261346c12f35db 100644 GIT binary patch delta 1256 zcmVZ~cl7 zd-VO{$tS=1fg!Ix|Lp%jZ(qBPF)gH6Bi1D`_sF7!vw^}ysu;+lK^6k%JSwyxH%M0j zAn${UPM5fFWCf55kO`RrnGjUtIl0V`aSfb8CP(lU&Ec5fSOwLtL}eNC#gr5R!X|vV zfaqxTyC@>w4S(@X4QdUtM6sS@({vT2gCQva^@NbMknI*)b+Ppn?<2OXaRNdUh-E-W zNn-;dnpIFx38@Wg{S1VF6q33cBT%GNq1pvTZjujjq6-AskQWhwI-$raJcrmg;mXt7 zy#AAiS)aem!QH=7k4FTTxY&7-t1oNNm_5NItB#;k(0Y4i9ao0!&sti8oLkfO&m6HfIP|F;%(6ZquWHa~xDxC#LnCt_vnC!3A^YK5i)tKqs>y(E(q+pRIJ7ND^p|W*)AA1xbHA)w3o`2$a zgwfMG{RrO9NHNeqdx;ntqHhrD#0yz;JNRb8;jKR*RffqoIJ)x=F*NkgJj!tQ4N~-M zo_~VpfBZcf8+iJgUw?mT-%s{peh2R!RR4c*?u)QdUAh6h)H@g$!i6bK~2HUwLe zbVjZX?fe0%zA+!q1ReaH>mC=)${A`#|C}Kt1o>18*y3L2mVA{zI>UF;`|4zG;}%= SdEr_B0000Q)OH&cvA(ELquQ~`s5Zmewsm~(c6ZpVF5h1HUp@Pq z%ripkCQ`Hs#f*?SWTS~So`w$O!IK4DWdds*$~Pg?NSgy7`G2Dt-5z0XNgF^WKt^Qr zq(YDnR>@?F3~jIq87xsd%)YD{(s1HZuz6=4m5`VAAR~AAAI>E z0NFYl)kVTDI`G;DL(2QEVwOABuIir`s{GwgCyUGteyWS?^amCl3dc+D4p^=b<8n^A z(yxn@ZnMkQJxUw+zm;i8`O>s5lIeP!aMI=2)h__hoh=ufv^K@fzp3YcaQ4I}_2Z9n zL~wIPAAdbf_0$+x&KF#ahxHkG?-4SkyT#rBOPnm{3r<&B^%>#*_^f_S^$>$ui{0Pm z0A?^*I)3Gd!H^Gs1faDU3vO>gJzuaPX)m3cnFwUkqQA|d05t}d^95@a>Y_%!|NG{B z`!WDYd$Ye~)!dLf?zjtpb5B2pL~dFa6~z$Plz*})0qDSjyE0vH%gt~;01&;w`dAlf z*}%*v^tU}4fMAOS$4JG;{K1NSciezTM0U2v6yMcFG0r&hN{fh1{5U750U=D0?JlXF z;H@F%3A)p++Kwkx3FQM*R9rg#;57Mg3Rq+dLNuhUjfkT$Wl<1IRplwgcu1&tI{FDS z9DftFU7C@c)5zAvPJw2VBvT;AB>nY~sYVG+kxz)^IQKJ>k~Hfebgw=mLIiDzeu|0( z!sN(g1yf83MS(Y-=p8~NRl5YhS8U5kU8IwBk+Mm;?uK|9QE3x1F31`Ud~QG)l2)6j zv+CP|kP Date: Sat, 4 Oct 2025 16:21:07 +0200 Subject: [PATCH 09/20] add slider for new mines --- client/windows/CKingdomInterface.cpp | 21 +++++++++++++++++++-- client/windows/CKingdomInterface.h | 3 ++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/client/windows/CKingdomInterface.cpp b/client/windows/CKingdomInterface.cpp index cb9925e40..db0e0af77 100644 --- a/client/windows/CKingdomInterface.cpp +++ b/client/windows/CKingdomInterface.cpp @@ -38,6 +38,7 @@ #include "../../lib/StartInfo.h" #include "../../lib/callback/CCallback.h" #include "../../lib/entities/hero/CHeroHandler.h" +#include "../../lib/entities/ResourceTypeHandler.h" #include "../../lib/texts/TextOperations.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/CGTownInstance.h" @@ -476,7 +477,7 @@ CKingdomInterface::CKingdomInterface() std::vector ownedObjects = GAME->interface()->cb->getMyObjects(); generateObjectsList(ownedObjects); - generateMinesList(ownedObjects); + generateMinesList(ownedObjects, 0); generateButtons(); statusbar = CGStatusBar::create(std::make_shared(ImagePath::builtin("KSTATBAR"), 10,pos.h - 45)); @@ -594,12 +595,17 @@ std::shared_ptr CKingdomInterface::createMainTab(size_t index) } } -void CKingdomInterface::generateMinesList(const std::vector & ownedObjects) +void CKingdomInterface::generateMinesList(const std::vector & ownedObjects, int line) { + OBJECT_CONSTRUCTION; + ui32 footerPos = OVERVIEW_SIZE * 116; ResourceSet minesCount = ResourceSet(); int totalIncome=0; + for(auto & ptr : minesBox) + ptr.reset(); + for(const CGObjectInstance * object : ownedObjects) { //Mines @@ -626,6 +632,15 @@ void CKingdomInterface::generateMinesList(const std::vector(Point(20+i*80, 31+footerPos), InfoBox::POS_INSIDE, InfoBox::SIZE_SMALL, data); minesBox[i]->removeUsedEvents(LCLICK|SHOW_POPUP); //fixes #890 - mines boxes ignore clicks } + + if(LIBRARY->resourceTypeHandler->getAllObjects().size() > GameConstants::RESOURCE_QUANTITY) + { + int lines = vstd::divideAndCeil(LIBRARY->resourceTypeHandler->getAllObjects().size(), GameConstants::RESOURCE_QUANTITY); + minesSlider = std::make_shared(Point(723, 495), 57, [this, ownedObjects](int to){ generateMinesList(ownedObjects, to); setRedrawParent(true); redraw(); }, 1, lines, line, Orientation::VERTICAL, CSlider::BROWN); + minesSlider->setPanningStep(57); + minesSlider->setScrollBounds(Rect(-735, 0, 735, 57)); + } + incomeArea = std::make_shared(); incomeArea->pos = Rect(pos.x+580, pos.y+31+footerPos, 136, 68); incomeArea->hoverText = LIBRARY->generaltexth->allTexts[255]; @@ -722,6 +737,7 @@ CKingdHeroList::CKingdHeroList(size_t maxSize, const CreateHeroItemFunctor & onC return std::make_shared(AnimationPath::builtin("OVSLOT"), (idx - 2) % GameConstants::KINGDOM_WINDOW_HEROES_SLOTS); } }, Point(19,21), Point(0,116), maxSize, townCount, 0, 1, Rect(-19, -21, size, size)); + heroes->getSlider()->setScrollBounds(Rect(0, 0, 725, 483)); } void CKingdHeroList::updateGarrisons() @@ -755,6 +771,7 @@ CKingdTownList::CKingdTownList(size_t maxSize) ui32 size = OVERVIEW_SIZE*116 + 19; towns = std::make_shared(std::bind(&CKingdTownList::createTownItem, this, _1), Point(19,21), Point(0,116), maxSize, townCount, 0, 1, Rect(-19, -21, size, size)); + towns->getSlider()->setScrollBounds(Rect(0, 0, 725, 483)); } void CKingdTownList::townChanged(const CGTownInstance * town) diff --git a/client/windows/CKingdomInterface.h b/client/windows/CKingdomInterface.h index e45fb9315..2a1f5e167 100644 --- a/client/windows/CKingdomInterface.h +++ b/client/windows/CKingdomInterface.h @@ -232,6 +232,7 @@ private: std::shared_ptr dwellBottom; std::array, 7> minesBox; + std::shared_ptr minesSlider; std::shared_ptr incomeArea; std::shared_ptr incomeAmount; @@ -244,7 +245,7 @@ private: //Internal functions used during construction void generateButtons(); void generateObjectsList(const std::vector &ownedObjects); - void generateMinesList(const std::vector &ownedObjects); + void generateMinesList(const std::vector &ownedObjects, int line); std::shared_ptr createOwnedObject(size_t index); std::shared_ptr createMainTab(size_t index); From 46619f35fe6a7c527609d6df1c0c881e6915c4bb Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 4 Oct 2025 17:14:24 +0200 Subject: [PATCH 10/20] load mine image in kingdom overview --- client/windows/CKingdomInterface.cpp | 44 +++++++++++++++++-- client/windows/CKingdomInterface.h | 4 ++ .../CommonConstructors.cpp | 7 +++ .../CommonConstructors.h | 2 + 4 files changed, 54 insertions(+), 3 deletions(-) diff --git a/client/windows/CKingdomInterface.cpp b/client/windows/CKingdomInterface.cpp index db0e0af77..d2abecf6a 100644 --- a/client/windows/CKingdomInterface.cpp +++ b/client/windows/CKingdomInterface.cpp @@ -43,6 +43,8 @@ #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/mapObjects/MiscObjects.h" +#include "../../lib/mapObjectConstructors/CommonConstructors.h" +#include "../../lib/mapObjectConstructors/CObjectClassesHandler.h" #include "texts/CGeneralTextHandler.h" #include "../../lib/GameSettings.h" @@ -595,6 +597,25 @@ std::shared_ptr CKingdomInterface::createMainTab(size_t index) } } +std::shared_ptr CKingdomInterface::getMineHandler(const GameResID & res) +{ + std::shared_ptr mineHandler; + for(auto & subObjID : LIBRARY->objtypeh->knownSubObjects(Obj::MINE)) + { + auto handler = std::dynamic_pointer_cast(LIBRARY->objtypeh->getHandlerFor(Obj::MINE, subObjID)); + if(handler->getResourceType() == res) + mineHandler = handler; + } + + if(!mineHandler) + { + logGlobal->error("No mine for resource %s found!", res.toResource()->getJsonKey()); + return nullptr; + } + + return mineHandler; +} + void CKingdomInterface::generateMinesList(const std::vector & ownedObjects, int line) { OBJECT_CONSTRUCTION; @@ -625,10 +646,22 @@ void CKingdomInterface::generateMinesList(const std::vectorinterface()->cb->getPlayerState(GAME->interface()->playerID)->valOfBonuses(BonusType::RESOURCES_CONSTANT_BOOST, BonusSubtypeID(GameResID(EGameResID::GOLD))) * playerSettings->handicap.percentIncome / 100; totalIncome += GAME->interface()->cb->getPlayerState(GAME->interface()->playerID)->valOfBonuses(BonusType::RESOURCES_TOWN_MULTIPLYING_BOOST, BonusSubtypeID(GameResID(EGameResID::GOLD))) * towns.size() * playerSettings->handicap.percentIncome / 100; - for(int i=0; i(value, "", AnimationPath::builtin("OVMINES"), i, LIBRARY->generaltexth->translate("core.minename", i)); + std::shared_ptr data; + if(line == 0) + data = std::make_shared(value, "", AnimationPath::builtin("OVMINES"), i, LIBRARY->generaltexth->translate("core.minename", i)); + else + { + int resID = line * GameConstants::RESOURCE_QUANTITY + i; + if(resID >= LIBRARY->resourceTypeHandler->getAllObjects().size()) + break; + auto mine = getMineHandler(GameResID(resID)); + if(!mine || mine->getKingdomOverviewImage().empty()) + continue; + data = std::make_shared(value, "", mine->getKingdomOverviewImage(), 0, mine->getNameTranslated()); + } minesBox[i] = std::make_shared(Point(20+i*80, 31+footerPos), InfoBox::POS_INSIDE, InfoBox::SIZE_SMALL, data); minesBox[i]->removeUsedEvents(LCLICK|SHOW_POPUP); //fixes #890 - mines boxes ignore clicks } @@ -636,7 +669,12 @@ void CKingdomInterface::generateMinesList(const std::vectorresourceTypeHandler->getAllObjects().size() > GameConstants::RESOURCE_QUANTITY) { int lines = vstd::divideAndCeil(LIBRARY->resourceTypeHandler->getAllObjects().size(), GameConstants::RESOURCE_QUANTITY); - minesSlider = std::make_shared(Point(723, 495), 57, [this, ownedObjects](int to){ generateMinesList(ownedObjects, to); setRedrawParent(true); redraw(); }, 1, lines, line, Orientation::VERTICAL, CSlider::BROWN); + minesSlider = std::make_shared(Point(723, 495), 57, [this, ownedObjects](int to){ + generateMinesList(ownedObjects, to); + statusbar->clear(); + setRedrawParent(true); + redraw(); + }, 1, lines, line, Orientation::VERTICAL, CSlider::BROWN); minesSlider->setPanningStep(57); minesSlider->setScrollBounds(Rect(-735, 0, 735, 57)); } diff --git a/client/windows/CKingdomInterface.h b/client/windows/CKingdomInterface.h index 2a1f5e167..7b2f231f0 100644 --- a/client/windows/CKingdomInterface.h +++ b/client/windows/CKingdomInterface.h @@ -11,6 +11,8 @@ #include "CWindowWithArtifacts.h" +#include "../../lib/mapObjectConstructors/CommonConstructors.h" + VCMI_LIB_NAMESPACE_BEGIN class CGObjectInstance; VCMI_LIB_NAMESPACE_END @@ -242,6 +244,8 @@ private: void activateTab(size_t which); + std::shared_ptr getMineHandler(const GameResID & res); + //Internal functions used during construction void generateButtons(); void generateObjectsList(const std::vector &ownedObjects); diff --git a/lib/mapObjectConstructors/CommonConstructors.cpp b/lib/mapObjectConstructors/CommonConstructors.cpp index 74929af7e..9f946e5ca 100644 --- a/lib/mapObjectConstructors/CommonConstructors.cpp +++ b/lib/mapObjectConstructors/CommonConstructors.cpp @@ -107,6 +107,8 @@ void MineInstanceConstructor::initTypeData(const JsonNode & input) if (!config["description"].isNull()) LIBRARY->generaltexth->registerString(config.getModScope(), getDescriptionTextID(), config["description"]); + + kingdomOverviewImage = AnimationPath::fromJson(config["kingdomOverviewImage"]); } GameResID MineInstanceConstructor::getResourceType() const @@ -129,6 +131,11 @@ std::string MineInstanceConstructor::getDescriptionTranslated() const return LIBRARY->generaltexth->translate(getDescriptionTextID()); } +AnimationPath MineInstanceConstructor::getKingdomOverviewImage() const +{ + return kingdomOverviewImage; +} + void CTownInstanceConstructor::initTypeData(const JsonNode & input) { LIBRARY->identifiers()->requestIdentifier("faction", input["faction"], [&](si32 index) diff --git a/lib/mapObjectConstructors/CommonConstructors.h b/lib/mapObjectConstructors/CommonConstructors.h index 38851f897..40f409eb9 100644 --- a/lib/mapObjectConstructors/CommonConstructors.h +++ b/lib/mapObjectConstructors/CommonConstructors.h @@ -67,6 +67,7 @@ class DLL_LINKAGE MineInstanceConstructor : public CDefaultObjectTypeHandler From e1eba7c231170884081516a629b1cadc83053eee Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 4 Oct 2025 17:35:59 +0200 Subject: [PATCH 11/20] docs --- docs/modders/Animation_Format.md | 2 +- docs/modders/Map_Object_Format.md | 2 +- docs/modders/Map_Objects/Mine.md | 24 ++++++++++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 docs/modders/Map_Objects/Mine.md diff --git a/docs/modders/Animation_Format.md b/docs/modders/Animation_Format.md index 4db5addb3..c81d5d541 100644 --- a/docs/modders/Animation_Format.md +++ b/docs/modders/Animation_Format.md @@ -47,7 +47,7 @@ VCMI allows overriding HoMM3 .def files with .json replacement. Compared to .def // Group of this image. Optional, default = 0 "group" : 0, - // Imdex of the image in group + // Index of the image in group "frame" : 0, // Filename for this frame diff --git a/docs/modders/Map_Object_Format.md b/docs/modders/Map_Object_Format.md index f487c60f4..d9f07bf4a 100644 --- a/docs/modders/Map_Object_Format.md +++ b/docs/modders/Map_Object_Format.md @@ -49,6 +49,7 @@ These are object types that are available for modding and have configurable prop - `bank` - see [Creature Bank](Map_Objects/Creature_Bank.md). Object that grants award on defeating guardians. Deprectated in favor of [Rewardable](Map_Objects/Rewardable.md) - `dwelling` - see [Dwelling](Map_Objects/Dwelling.md). Object that allows recruitments of units outside of towns - `market` - see [Market](Map_Objects/Market.md). Trading resources, artifacts, creatures and such +- `mine` - see [Mine](Map_Objects/Mine.md). for Mines - `boat` - see [Boat](Map_Objects/Boat.md). Object to move across different terrains, such as water - `flaggable` - see [Flaggable](Map_Objects/Flaggable.md). Object that can be flagged by a player to provide [Bonus](Bonus_Format.md) or resources - `hillFort` - TODO: documentation. See config files in vcmi installation for reference @@ -64,7 +65,6 @@ These are types that don't have configurable properties, however it is possible - `borderGate` - `borderGuard` - `magi` -- `mine` - `obelisk` - `subterraneanGate` - `whirlpool` diff --git a/docs/modders/Map_Objects/Mine.md b/docs/modders/Map_Objects/Mine.md new file mode 100644 index 000000000..e8bdd854c --- /dev/null +++ b/docs/modders/Map_Objects/Mine.md @@ -0,0 +1,24 @@ +# Mine + +Currently only one mine per resource allowed. + +Beside the common parameters from [Map Object Format](../Map_Object_Format.md) there are some additional parameters: + +```json +{ + /// produced resource + "resource" : "mithril", + + /// amount of resources produced each day + "defaultQuantity" : 1, + + /// displayed name of mine + "name" : "name text", + + /// displayed description of mine (for popup) + "description" : "description text", + + /// Image showed on kingdom overview (animation; only frame 0 displayed) + "kingdomOverviewImage" : "image.def" +} +``` From 7c74f34ddc5a8d7c7b38307986d21efb40b38b60 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 4 Oct 2025 17:43:37 +0200 Subject: [PATCH 12/20] fix --- client/windows/CKingdomInterface.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/client/windows/CKingdomInterface.cpp b/client/windows/CKingdomInterface.cpp index d2abecf6a..21b20cda5 100644 --- a/client/windows/CKingdomInterface.cpp +++ b/client/windows/CKingdomInterface.cpp @@ -648,15 +648,16 @@ void CKingdomInterface::generateMinesList(const std::vector= LIBRARY->resourceTypeHandler->getAllObjects().size()) + break; + + std::string value = std::to_string(minesCount[resID]); std::shared_ptr data; if(line == 0) data = std::make_shared(value, "", AnimationPath::builtin("OVMINES"), i, LIBRARY->generaltexth->translate("core.minename", i)); else { - int resID = line * GameConstants::RESOURCE_QUANTITY + i; - if(resID >= LIBRARY->resourceTypeHandler->getAllObjects().size()) - break; auto mine = getMineHandler(GameResID(resID)); if(!mine || mine->getKingdomOverviewImage().empty()) continue; From 91101d26faad4fb3bd972a282523575d013a45b4 Mon Sep 17 00:00:00 2001 From: Simeon Manolov Date: Sun, 28 Sep 2025 00:48:42 +0300 Subject: [PATCH 13/20] Remove std::vector inheritance --- lib/battle/CBattleInfoCallback.cpp | 6 +-- lib/bonuses/Bonus.cpp | 38 ++--------------- lib/bonuses/Bonus.h | 66 +++++++++++++++++++++++++++--- 3 files changed, 68 insertions(+), 42 deletions(-) diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp index 687c5653c..f33c98207 100644 --- a/lib/battle/CBattleInfoCallback.cpp +++ b/lib/battle/CBattleInfoCallback.cpp @@ -1456,13 +1456,13 @@ AttackableTiles CBattleInfoCallback::getPotentiallyAttackableHexes( const auto multihexAnimation = attacker->getBonusesOfType(BonusType::MULTIHEX_ANIMATION); for (const auto & bonus : *multihexUnit) - at.friendlyCreaturePositions.insert(processTargets(bonus->additionalInfo)); + at.friendlyCreaturePositions.insert(processTargets(bonus->additionalInfo.data())); for (const auto & bonus : *multihexEnemy) - at.hostileCreaturePositions.insert(processTargets(bonus->additionalInfo)); + at.hostileCreaturePositions.insert(processTargets(bonus->additionalInfo.data())); for (const auto & bonus : *multihexAnimation) - at.overrideAnimationPositions.insert(processTargets(bonus->additionalInfo)); + at.overrideAnimationPositions.insert(processTargets(bonus->additionalInfo.data())); if(attacker->hasBonusOfType(BonusType::THREE_HEADED_ATTACK)) at.hostileCreaturePositions.insert(processTargets({2,6})); diff --git a/lib/bonuses/Bonus.cpp b/lib/bonuses/Bonus.cpp index b2f07394a..87bb78d15 100644 --- a/lib/bonuses/Bonus.cpp +++ b/lib/bonuses/Bonus.cpp @@ -36,38 +36,8 @@ CAddInfo::CAddInfo() = default; CAddInfo::CAddInfo(si32 value) { - if(value != CAddInfo::NONE) - push_back(value); -} - -bool CAddInfo::operator==(si32 value) const -{ - switch(size()) - { - case 0: - return value == CAddInfo::NONE; - case 1: - return operator[](0) == value; - default: - return false; - } -} - -bool CAddInfo::operator!=(si32 value) const -{ - return !operator==(value); -} - -si32 & CAddInfo::operator[](size_type pos) -{ - if(pos >= size()) - resize(pos + 1, CAddInfo::NONE); - return vector::operator[](pos); -} - -si32 CAddInfo::operator[](size_type pos) const -{ - return pos < size() ? vector::operator[](pos) : CAddInfo::NONE; + if (value != CAddInfo::NONE) + data_.push_back(value); } std::string CAddInfo::toString() const @@ -79,12 +49,12 @@ JsonNode CAddInfo::toJsonNode() const { if(size() < 2) { - return JsonNode(operator[](0)); + return JsonNode((*this)[0]); } else { JsonNode node; - for(si32 value : *this) + for(si32 value : data_) node.Vector().emplace_back(value); return node; } diff --git a/lib/bonuses/Bonus.h b/lib/bonuses/Bonus.h index ac0c3ea9a..22de0219f 100644 --- a/lib/bonuses/Bonus.h +++ b/lib/bonuses/Bonus.h @@ -29,22 +29,78 @@ using TConstBonusListPtr = std::shared_ptr; using TPropagatorPtr = std::shared_ptr; using TUpdaterPtr = std::shared_ptr; -class DLL_LINKAGE CAddInfo : public std::vector +class DLL_LINKAGE CAddInfo final { public: + using container = std::vector; + using size_type = container::size_type; enum { NONE = -1 }; CAddInfo(); CAddInfo(si32 value); - bool operator==(si32 value) const; - bool operator!=(si32 value) const; + // Inline definitions in the header to avoid missing symbols across TUs + bool operator==(const CAddInfo& other) const noexcept { + return data_ == other.data_; + } - si32 & operator[](size_type pos); - si32 operator[](size_type pos) const; + bool operator!=(const CAddInfo& other) const noexcept { + return !(*this == other); + } + + bool operator==(si32 value) const + { + switch(data_.size()) + { + case 0: + return value == CAddInfo::NONE; + case 1: + return data_[0] == value; + default: + return false; + } + } + + bool operator!=(si32 value) const + { + return !(*this == value); + } + + + si32 & operator[](size_type pos) + { + if(pos >= data_.size()) + data_.resize(pos + 1, CAddInfo::NONE); + return data_[pos]; + } + + si32 operator[](size_type pos) const + { + return pos < data_.size() ? data_[pos] : CAddInfo::NONE; + } std::string toString() const; JsonNode toJsonNode() const; + + // Minimal vector-like facade + size_type size() const noexcept { return data_.size(); } + bool empty() const noexcept { return data_.empty(); } + void push_back(si32 v) { data_.push_back(v); } + void resize(size_type n, si32 fill = CAddInfo::NONE) { data_.resize(n, fill); } + + container::iterator begin() noexcept { return data_.begin(); } + container::iterator end() noexcept { return data_.end(); } + container::const_iterator begin() const noexcept { return data_.begin(); } + container::const_iterator end() const noexcept { return data_.end(); } + + // expose const view for free operators + const container& data() const noexcept { return data_; } + + template + void serialize(H& h) { h & data_; } + +private: + container data_; }; /// Struct for handling bonuses of several types. Can be transferred to any hero From 4fcca634eab618b49e62745646bd62439f827de5 Mon Sep 17 00:00:00 2001 From: George King <98261225+GeorgeK1ng@users.noreply.github.com> Date: Sun, 5 Oct 2025 11:21:20 +0200 Subject: [PATCH 14/20] Use performNativeCopy instead of copy (#6185) --- launcher/helper.cpp | 38 ++++++++++++--------- launcher/helper.h | 2 +- launcher/mainwindow_moc.cpp | 2 +- launcher/modManager/chroniclesextractor.cpp | 4 +-- launcher/modManager/cmodlistview_moc.cpp | 2 +- 5 files changed, 27 insertions(+), 21 deletions(-) diff --git a/launcher/helper.cpp b/launcher/helper.cpp index 22bedbc79..bc5cbf2f5 100644 --- a/launcher/helper.cpp +++ b/launcher/helper.cpp @@ -42,6 +42,19 @@ static QScrollerProperties generateScrollerProperties() } #endif +#ifdef VCMI_ANDROID +static QString safeEncode(QString uri) +{ + // %-encode unencoded parts of string. + // This is needed because Qt returns a mixed content url with %-encoded and unencoded parts. On Android >= 13 this causes problems reading these files, when using spaces and unicode characters in folder or filename. + // Only these should be encoded (other typically %-encoded chars should not be encoded because this leads to errors). + // Related, but seems not completly fixed (at least in our setup): https://bugreports.qt.io/browse/QTBUG-114435 + if (!uri.startsWith("content://", Qt::CaseInsensitive)) + return uri; + return QString::fromUtf8(QUrl::toPercentEncoding(uri, "!#$&'()*+,/:;=?@[]<>{}\"`^~%")); +} +#endif + namespace Helper { void loadSettings() @@ -77,35 +90,28 @@ QString getRealPath(QString path) #ifdef VCMI_ANDROID if(path.contains("content://", Qt::CaseInsensitive)) { - auto str = QAndroidJniObject::fromString(path); + auto str = QAndroidJniObject::fromString(safeEncode(path)); return QAndroidJniObject::callStaticObjectMethod("eu/vcmi/vcmi/util/FileUtil", "getFilenameFromUri", "(Ljava/lang/String;Landroid/content/Context;)Ljava/lang/String;", str.object(), QtAndroid::androidContext().object()).toString(); } - else - return path; + return path; #else return path; #endif } -void performNativeCopy(QString src, QString dst) +bool performNativeCopy(QString src, QString dst) { #ifdef VCMI_ANDROID - // %-encode unencoded parts of string. - // This is needed because Qt returns a mixed content url with %-encoded and unencoded parts. On Android >= 13 this causes problems reading these files, when using spaces and unicode characters in folder or filename. - // Only these should be encoded (other typically %-encoded chars should not be encoded because this leads to errors). - // Related, but seems not completly fixed (at least in our setup): https://bugreports.qt.io/browse/QTBUG-114435 - auto safeEncode = [&](QString uri) -> QString - { - if(!uri.startsWith("content://", Qt::CaseInsensitive)) - return uri; - return QString::fromUtf8(QUrl::toPercentEncoding(uri, "!#$&'()*+,/:;=?@[]<>{}\"`^~%")); - }; - auto srcStr = QAndroidJniObject::fromString(safeEncode(src)); auto dstStr = QAndroidJniObject::fromString(safeEncode(dst)); QAndroidJniObject::callStaticObjectMethod("eu/vcmi/vcmi/util/FileUtil", "copyFileFromUri", "(Ljava/lang/String;Ljava/lang/String;Landroid/content/Context;)V", srcStr.object(), dstStr.object(), QtAndroid::androidContext().object()); + + if(QFileInfo(dst).exists()) + return true; + else + return false; #else - QFile::copy(src, dst); + return QFile::copy(src, dst); #endif } diff --git a/launcher/helper.h b/launcher/helper.h index 7b8dc16bf..bb515d08e 100644 --- a/launcher/helper.h +++ b/launcher/helper.h @@ -20,7 +20,7 @@ void loadSettings(); void reLoadSettings(); void enableScrollBySwiping(QObject * scrollTarget); QString getRealPath(QString path); -void performNativeCopy(QString src, QString dst); +bool performNativeCopy(QString src, QString dst); void revealDirectoryInFileBrowser(QString path); MainWindow * getMainWindow(); void keepScreenOn(bool isEnabled); diff --git a/launcher/mainwindow_moc.cpp b/launcher/mainwindow_moc.cpp index 71b2bb0a7..f07c0982f 100644 --- a/launcher/mainwindow_moc.cpp +++ b/launcher/mainwindow_moc.cpp @@ -295,7 +295,7 @@ void MainWindow::manualInstallFile(QString filePath) { const auto configFilePath = configDir.filePath(configFile[0]); QFile::remove(configFilePath); - QFile::copy(filePath, configFilePath); + Helper::performNativeCopy(filePath, configFilePath); Helper::reLoadSettings(); } diff --git a/launcher/modManager/chroniclesextractor.cpp b/launcher/modManager/chroniclesextractor.cpp index f2a55effa..39d078ee7 100644 --- a/launcher/modManager/chroniclesextractor.cpp +++ b/launcher/modManager/chroniclesextractor.cpp @@ -133,7 +133,7 @@ void ChroniclesExtractor::createBaseMod() const { QDir().mkpath(pathToQString(destFolder)); QFile::remove(destFile); - QFile::copy(file, destFile); + Helper::performNativeCopy(file, destFile); } } } @@ -237,7 +237,7 @@ void ChroniclesExtractor::extractFiles(int no) const continue; auto srcName = vstd::reverseMap(mapping).at(no); auto dstName = (no == 7 || no == 8) ? srcName : "Intro"; - QFile::copy(tmpDir.filePath(QString::fromStdString(srcName + ending)), outDirVideo.filePath(QString::fromStdString(dstName + ending))); + Helper::performNativeCopy(tmpDir.filePath(QString::fromStdString(srcName + ending)), outDirVideo.filePath(QString::fromStdString(dstName + ending))); } } diff --git a/launcher/modManager/cmodlistview_moc.cpp b/launcher/modManager/cmodlistview_moc.cpp index f111a82f8..d3c1ab6a8 100644 --- a/launcher/modManager/cmodlistview_moc.cpp +++ b/launcher/modManager/cmodlistview_moc.cpp @@ -1212,7 +1212,7 @@ void CModListView::installMaps(QStringList maps) QFile::remove(destFile); } - if (QFile::copy(map, destFile)) + if (Helper::performNativeCopy(map, destFile)) successCount++; else { From 870e90b4d56f54648e45c8abed0350341c519013 Mon Sep 17 00:00:00 2001 From: George King <98261225+GeorgeK1ng@users.noreply.github.com> Date: Sun, 5 Oct 2025 13:45:30 +0200 Subject: [PATCH 15/20] Add missing presets --- CMakePresets.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CMakePresets.json b/CMakePresets.json index 2d5788ab0..c160e6ccc 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -537,6 +537,16 @@ "configurePreset": "linux-gcc-test", "inherits": "default-release" }, + { + "name": "linux-gcc-test", + "configurePreset": "linux-gcc-test", + "inherits": "default-release" + }, + { + "name": "linux-clang-test", + "configurePreset": "linux-clang-test", + "inherits": "default-release" + }, { "name": "macos-xcode-release", "configurePreset": "macos-xcode-release", From 0fad70cac701d5a78990b57f9d8baff767e47796 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 5 Oct 2025 20:31:48 +0200 Subject: [PATCH 16/20] puzzlemap fine tuning --- client/windows/CPuzzleWindow.cpp | 9 +++++++-- client/windows/CPuzzleWindow.h | 2 ++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/client/windows/CPuzzleWindow.cpp b/client/windows/CPuzzleWindow.cpp index a1a87c266..a9dd08e44 100644 --- a/client/windows/CPuzzleWindow.cpp +++ b/client/windows/CPuzzleWindow.cpp @@ -21,6 +21,7 @@ #include "../widgets/Buttons.h" #include "../widgets/Images.h" #include "../widgets/TextControls.h" +#include "../widgets/GraphicalPrimitiveCanvas.h" #include "../../lib/callback/CCallback.h" #include "../../lib/entities/faction/CFaction.h" @@ -42,7 +43,7 @@ CPuzzleWindow::CPuzzleWindow(const int3 & GrailPos, double discoveredRatio) quitb = std::make_shared(Point(670, 538), AnimationPath::builtin("IOK6432.DEF"), CButton::tooltip(LIBRARY->generaltexth->allTexts[599]), std::bind(&CPuzzleWindow::close, this), EShortcut::GLOBAL_RETURN); quitb->setBorderColor(Colors::METALLIC_GOLD); - mapView = std::make_shared(Point(8,9), Point(591, 544), grailPos); + mapView = std::make_shared(Point(8,8), Point(592, 544), grailPos); mapView->needFullUpdate = true; logo = std::make_shared(ImagePath::builtin("PUZZLOGO"), 607, 3); @@ -57,7 +58,7 @@ CPuzzleWindow::CPuzzleWindow(const int3 & GrailPos, double discoveredRatio) { const SPuzzleInfo & info = elem; - auto piece = std::make_shared(info.filename, info.position.x, info.position.y); + auto piece = std::make_shared(info.filename, info.position.x + 1, info.position.y); piece->needRefresh = true; //piece that will slowly disappear @@ -71,6 +72,10 @@ CPuzzleWindow::CPuzzleWindow(const int3 & GrailPos, double discoveredRatio) visiblePieces.push_back(piece); } } + + border = std::make_shared(Rect(Point(6,6), Point(596, 548))); + for(int i = 0; i < 3; i++) + border->addRectangle(Point(i, i), Point(border->pos.w - i * 2, border->pos.h - i * 2), Colors::BLACK); } void CPuzzleWindow::showAll(Canvas & to) diff --git a/client/windows/CPuzzleWindow.h b/client/windows/CPuzzleWindow.h index 18f502d52..d15c508d7 100644 --- a/client/windows/CPuzzleWindow.h +++ b/client/windows/CPuzzleWindow.h @@ -16,6 +16,7 @@ class CLabel; class CButton; class CResDataBar; class PuzzleMapView; +class GraphicalPrimitiveCanvas; /// Puzzle screen which gets uncovered when you visit obilisks class CPuzzleWindow : public CWindowObject @@ -24,6 +25,7 @@ private: int3 grailPos; std::shared_ptr mapView; std::shared_ptr logo; + std::shared_ptr border; std::shared_ptr title; std::shared_ptr quitb; std::shared_ptr resDataBar; From faad200aed30e742e4aa5856155e68186ec8c738 Mon Sep 17 00:00:00 2001 From: Opuszek Date: Mon, 6 Oct 2025 20:33:09 +0200 Subject: [PATCH 17/20] Improve editor performance --- mapeditor/mainwindow.cpp | 9 +- mapeditor/mapcontroller.cpp | 100 +++-- mapeditor/mapcontroller.h | 2 +- mapeditor/maphandler.cpp | 27 +- mapeditor/maphandler.h | 10 +- mapeditor/mapview.cpp | 292 ++++++++------ mapeditor/mapview.h | 29 +- mapeditor/scenelayer.cpp | 779 ++++++++++++++++++++++-------------- mapeditor/scenelayer.h | 163 +++++--- 9 files changed, 831 insertions(+), 580 deletions(-) diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index cdbda068a..e501ef001 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -295,6 +295,7 @@ MainWindow::MainWindow(QWidget* parent) : mapLevel = combo->currentIndex(); ui->mapView->setScene(controller.scene(mapLevel)); + ui->mapView->setViewports(); ui->minimapView->setScene(controller.miniScene(mapLevel)); }); layout->addWidget(combo, c == ui->menuView ? 1 : 0); @@ -1323,9 +1324,10 @@ void MainWindow::on_actionUpdate_appearance_triggered() controller.scene(mapLevel)->selectionObjectsView.deselectObject(obj); continue; } - + std::vector selectedTiles; for(auto & offset : obj->appearance->getBlockedOffsets()) - controller.scene(mapLevel)->selectionTerrainView.select(obj->pos + offset); + selectedTiles.push_back(obj->pos + offset); + controller.scene(mapLevel)->selectionTerrainView.select(selectedTiles); } else { @@ -1484,8 +1486,6 @@ void MainWindow::on_actionLock_triggered() } controller.scene(mapLevel)->selectionObjectsView.clear(); } - controller.scene(mapLevel)->objectsView.update(); - controller.scene(mapLevel)->selectionObjectsView.update(); } } @@ -1497,7 +1497,6 @@ void MainWindow::on_actionUnlock_triggered() controller.scene(mapLevel)->selectionObjectsView.unlockAll(); controller.scene(mapLevel)->objectsView.unlockAll(); } - controller.scene(mapLevel)->objectsView.update(); } diff --git a/mapeditor/mapcontroller.cpp b/mapeditor/mapcontroller.cpp index 40fe6497e..a1814d20c 100644 --- a/mapeditor/mapcontroller.cpp +++ b/mapeditor/mapcontroller.cpp @@ -229,7 +229,7 @@ void MapController::setMap(std::unique_ptr cmap) _miniscenes[i].reset(new MinimapScene(i)); } resetMapHandler(); - sceneForceUpdate(); + initializeMap(); connectScenes(); @@ -256,21 +256,24 @@ void MapController::initObstaclePainters(CMap * map) } } +void MapController::initializeMap() +{ + for(int i = 0; i < _map->mapLevels; i++) + { + _scenes[i]->createMap(); + _miniscenes[i]->createMap(); + } +} + void MapController::sceneForceUpdate() { for(int i = 0; i < _map->mapLevels; i++) { - _scenes[i]->updateViews(); - _miniscenes[i]->updateViews(); + _scenes[i]->updateMap(); + _miniscenes[i]->updateMap(); } } -void MapController::sceneForceUpdate(int level) -{ - _scenes[level]->updateViews(); - _miniscenes[level]->updateViews(); -} - void MapController::resetMapHandler() { if(!_mapHandler) @@ -293,16 +296,13 @@ void MapController::commitTerrainChange(int level, const TerrainId & terrain) return; _scenes[level]->selectionTerrainView.clear(); - _scenes[level]->selectionTerrainView.draw(); _map->getEditManager()->getTerrainSelection().setSelection(v); _map->getEditManager()->drawTerrain(terrain, terrainDecorationPercentageLevel, &CRandomGenerator::getDefault()); - for(auto & t : v) - _scenes[level]->terrainView.setDirty(t); - _scenes[level]->terrainView.draw(); + _scenes[level]->terrainView.redrawTerrain(v); - _miniscenes[level]->updateViews(); + _miniscenes[level]->updateMap(); main->mapChanged(); } @@ -314,19 +314,16 @@ void MapController::commitRoadOrRiverChange(int level, ui8 type, bool isRoad) return; _scenes[level]->selectionTerrainView.clear(); - _scenes[level]->selectionTerrainView.draw(); _map->getEditManager()->getTerrainSelection().setSelection(v); if(isRoad) _map->getEditManager()->drawRoad(RoadId(type), &CRandomGenerator::getDefault()); else _map->getEditManager()->drawRiver(RiverId(type), &CRandomGenerator::getDefault()); + + _scenes[level]->terrainView.redrawTerrain(v); - for(auto & t : v) - _scenes[level]->terrainView.setDirty(t); - _scenes[level]->terrainView.draw(); - - _miniscenes[level]->updateViews(); + _miniscenes[level]->updateMap(); main->mapChanged(); } @@ -351,15 +348,13 @@ void MapController::commitObjectErase(int level) { //invalidate tiles under objects _mapHandler->removeObject(obj); - _scenes[level]->objectsView.setDirty(obj); } + _scenes[level]->objectsView.redrawObjects(selectedObjects); _scenes[level]->selectionObjectsView.clear(); - _scenes[level]->objectsView.draw(); - _scenes[level]->selectionObjectsView.draw(); - _scenes[level]->passabilityView.update(); + _scenes[level]->passabilityView.redraw(); - _miniscenes[level]->updateViews(); + _miniscenes[level]->updateMap(); main->mapChanged(); } @@ -407,11 +402,11 @@ void MapController::pasteFromClipboard(int level) if(!errors.isEmpty()) QMessageBox::warning(main, QObject::tr("Can't place object"), errors.join('\n')); - _scenes[level]->objectsView.draw(); - _scenes[level]->passabilityView.update(); - _scenes[level]->selectionObjectsView.draw(); + _scenes[level]->objectsView.redraw(); + _scenes[level]->passabilityView.redraw(); + _scenes[level]->selectionObjectsView.redraw(); - _miniscenes[level]->updateViews(); + _miniscenes[level]->updateMap(); main->mapChanged(); } @@ -421,9 +416,8 @@ bool MapController::discardObject(int level) const if(_scenes[level]->selectionObjectsView.newObject) { _scenes[level]->selectionObjectsView.newObject.reset(); - _scenes[level]->selectionObjectsView.shift = QPoint(0, 0); + _scenes[level]->selectionObjectsView.setShift(0, 0); _scenes[level]->selectionObjectsView.selectionMode = SelectionObjectsLayer::NOTHING; - _scenes[level]->selectionObjectsView.draw(); return true; } return false; @@ -433,7 +427,7 @@ void MapController::createObject(int level, std::shared_ptr ob { _scenes[level]->selectionObjectsView.newObject = obj; _scenes[level]->selectionObjectsView.selectionMode = SelectionObjectsLayer::MOVEMENT; - _scenes[level]->selectionObjectsView.draw(); + _scenes[level]->selectionObjectsView.redraw(); } void MapController::commitObstacleFill(int level) @@ -463,29 +457,26 @@ void MapController::commitObstacleFill(int level) for(auto o : sel.second->placeObstacles(CRandomGenerator::getDefault())) { _mapHandler->invalidate(o.get()); - _scenes[level]->objectsView.setDirty(o.get()); + _scenes[level]->objectsView.redrawObjects({o.get()}); } } _scenes[level]->selectionTerrainView.clear(); - _scenes[level]->selectionTerrainView.draw(); - _scenes[level]->objectsView.draw(); + _scenes[level]->objectsView.update(); _scenes[level]->passabilityView.update(); - _miniscenes[level]->updateViews(); + _miniscenes[level]->updateMap(); main->mapChanged(); } void MapController::commitObjectChange(int level) { - for( auto * o : _scenes[level]->selectionObjectsView.getSelection()) - _scenes[level]->objectsView.setDirty(o); + _scenes[level]->objectsView.redrawObjects(_scenes[level]->selectionObjectsView.getSelection()); - _scenes[level]->objectsView.draw(); - _scenes[level]->selectionObjectsView.draw(); - _scenes[level]->passabilityView.update(); + _scenes[level]->selectionObjectsView.redraw(); + _scenes[level]->passabilityView.redraw(); - _miniscenes[level]->updateViews(); + _miniscenes[level]->updateMap(); main->mapChanged(); } @@ -502,29 +493,30 @@ void MapController::commitObjectShift(int level) bool makeShift = !shift.isNull(); if(makeShift) { - for(auto * obj : _scenes[level]->selectionObjectsView.getSelection()) + std::set movedObjects = _scenes[level]->selectionObjectsView.getSelection(); + for(auto * obj : movedObjects) { int3 pos = obj->pos; pos.z = level; pos.x += shift.x(); pos.y += shift.y(); - _scenes[level]->objectsView.setDirty(obj); //set dirty before movement _map->getEditManager()->moveObject(obj, pos); _mapHandler->invalidate(obj); } + _scenes[level]->objectsView.redrawObjects(movedObjects); } _scenes[level]->selectionObjectsView.newObject = nullptr; - _scenes[level]->selectionObjectsView.shift = QPoint(0, 0); + _scenes[level]->selectionObjectsView.setShift(0, 0); _scenes[level]->selectionObjectsView.selectionMode = SelectionObjectsLayer::NOTHING; if(makeShift) { - _scenes[level]->objectsView.draw(); - _scenes[level]->selectionObjectsView.draw(); - _scenes[level]->passabilityView.update(); + _scenes[level]->objectsView.redraw(); + _scenes[level]->passabilityView.redraw(); + _scenes[level]->selectionObjectsView.redraw(); - _miniscenes[level]->updateViews(); + _miniscenes[level]->updateMap(); main->mapChanged(); } } @@ -547,16 +539,14 @@ void MapController::commitObjectCreate(int level) _map->getEditManager()->insertObject(newObj); _mapHandler->invalidate(newObj.get()); - _scenes[level]->objectsView.setDirty(newObj.get()); + _scenes[level]->objectsView.redrawObjects({newObj.get()}); _scenes[level]->selectionObjectsView.newObject = nullptr; - _scenes[level]->selectionObjectsView.shift = QPoint(0, 0); + _scenes[level]->selectionObjectsView.setShift(0, 0); _scenes[level]->selectionObjectsView.selectionMode = SelectionObjectsLayer::NOTHING; - _scenes[level]->objectsView.draw(); - _scenes[level]->selectionObjectsView.draw(); - _scenes[level]->passabilityView.update(); + _scenes[level]->passabilityView.redraw(); - _miniscenes[level]->updateViews(); + _miniscenes[level]->updateMap(); main->mapChanged(); } diff --git a/mapeditor/mapcontroller.h b/mapeditor/mapcontroller.h index 38c92ab65..5edb97b1c 100644 --- a/mapeditor/mapcontroller.h +++ b/mapeditor/mapcontroller.h @@ -48,8 +48,8 @@ public: void resetMapHandler(); + void initializeMap(); void sceneForceUpdate(); - void sceneForceUpdate(int level); void commitTerrainChange(int level, const TerrainId & terrain); void commitRoadOrRiverChange(int level, ui8 type, bool isRoad); diff --git a/mapeditor/maphandler.cpp b/mapeditor/maphandler.cpp index 7ae93122c..7bbd16af7 100644 --- a/mapeditor/maphandler.cpp +++ b/mapeditor/maphandler.cpp @@ -109,18 +109,17 @@ void MapHandler::initTerrainGraphics() loadFlipped(roadAnimations, roadImages, roadFiles); } -void MapHandler::drawTerrainTile(QPainter & painter, int x, int y, int z) +void MapHandler::drawTerrainTile(QPainter & painter, int x, int y, int z, QPointF offset) { const auto & tinfo = map->getTile(int3(x, y, z)); auto terrainName = tinfo.getTerrain()->getJsonKey(); if(terrainImages.at(terrainName).size() <= tinfo.terView) return; - - painter.drawImage(x * tileSize, y * tileSize, flippedImage(terrainImages.at(terrainName)[tinfo.terView], tinfo.extTileFlags)); + painter.drawImage(x * tileSize - offset.x(), y * tileSize - offset.y(), flippedImage(terrainImages.at(terrainName)[tinfo.terView], tinfo.extTileFlags)); } -void MapHandler::drawRoad(QPainter & painter, int x, int y, int z) +void MapHandler::drawRoad(QPainter & painter, int x, int y, int z, QPointF offset) { const auto & tinfo = map->getTile(int3(x, y, z)); auto * tinfoUpper = map->isInTheMap(int3(x, y - 1, z)) ? &map->getTile(int3(x, y - 1, z)) : nullptr; @@ -132,7 +131,7 @@ void MapHandler::drawRoad(QPainter & painter, int x, int y, int z) { const QRect source{0, tileSize / 2, tileSize, tileSize / 2}; const ui8 rotationFlags = tinfoUpper->extTileFlags >> 4; - painter.drawImage(QPoint(x * tileSize, y * tileSize), flippedImage(roadImages.at(roadName)[tinfoUpper->roadDir], rotationFlags), source); + painter.drawImage(QPoint(x * tileSize - offset.x(), y * tileSize - offset.y()), flippedImage(roadImages.at(roadName)[tinfoUpper->roadDir], rotationFlags), source); } } @@ -143,12 +142,12 @@ void MapHandler::drawRoad(QPainter & painter, int x, int y, int z) { const QRect source{0, 0, tileSize, tileSize / 2}; const ui8 rotationFlags = tinfo.extTileFlags >> 4; - painter.drawImage(QPoint(x * tileSize, y * tileSize + tileSize / 2), flippedImage(roadImages.at(roadName)[tinfo.roadDir], rotationFlags), source); + painter.drawImage(QPoint(x * tileSize - offset.x(), y * tileSize + tileSize / 2 - offset.y()), flippedImage(roadImages.at(roadName)[tinfo.roadDir], rotationFlags), source); } } } -void MapHandler::drawRiver(QPainter & painter, int x, int y, int z) +void MapHandler::drawRiver(QPainter & painter, int x, int y, int z, QPointF offset) { const auto & tinfo = map->getTile(int3(x, y, z)); @@ -162,7 +161,7 @@ void MapHandler::drawRiver(QPainter & painter, int x, int y, int z) return; const ui8 rotationFlags = tinfo.extTileFlags >> 2; - painter.drawImage(x * tileSize, y * tileSize, flippedImage(riverImages.at(riverName)[tinfo.riverDir], rotationFlags)); + painter.drawImage(x * tileSize - offset.x(), y * tileSize - offset.y(), flippedImage(riverImages.at(riverName)[tinfo.riverDir], rotationFlags)); } void setPlayerColor(QImage * sur, PlayerColor player) @@ -366,7 +365,7 @@ std::vector & MapHandler::getObjects(int x, int y, int z) return tileObjects[index(x, y, z)]; } -void MapHandler::drawObjects(QPainter & painter, int x, int y, int z, const std::set & locked) +void MapHandler::drawObjects(QPainter & painter, int x, int y, int z, QPointF offset, const std::set & locked) { painter.setRenderHint(QPainter::Antialiasing, false); painter.setRenderHint(QPainter::SmoothPixmapTransform, false); @@ -390,7 +389,7 @@ void MapHandler::drawObjects(QPainter & painter, int x, int y, int z, const std: { auto pos = obj->anchorPos(); - painter.drawImage(QPoint(x * tileSize, y * tileSize), *objData.objBitmap, object.rect, Qt::AutoColor | Qt::NoOpaqueDetection); + painter.drawImage(QPoint(x * tileSize - offset.x(), y * tileSize - offset.y()), *objData.objBitmap, object.rect, Qt::AutoColor | Qt::NoOpaqueDetection); if(locked.count(obj)) { @@ -402,13 +401,13 @@ void MapHandler::drawObjects(QPainter & painter, int x, int y, int z, const std: if(objData.flagBitmap) { if(x == pos.x && y == pos.y) - painter.drawImage(QPoint((x - 2) * tileSize, (y - 1) * tileSize), *objData.flagBitmap); + painter.drawImage(QPoint((x - 2) * tileSize - offset.x(), (y - 1) * tileSize - offset.y()), *objData.flagBitmap); } } } } -void MapHandler::drawObjectAt(QPainter & painter, const CGObjectInstance * obj, int x, int y) +void MapHandler::drawObjectAt(QPainter & painter, const CGObjectInstance * obj, int x, int y, QPointF offset) { if (!obj) { @@ -424,10 +423,10 @@ void MapHandler::drawObjectAt(QPainter & painter, const CGObjectInstance * obj, if (objData.objBitmap) { - painter.drawImage(QPoint((x + 1) * tileSize - objData.objBitmap->width(), (y + 1) * tileSize - objData.objBitmap->height()), *objData.objBitmap); + painter.drawImage(QPoint((x + 1) * tileSize - objData.objBitmap->width() - offset.x(), (y + 1) * tileSize - objData.objBitmap->height() - offset.y()), *objData.objBitmap); if (objData.flagBitmap) - painter.drawImage(QPoint((x + 1) * tileSize - objData.objBitmap->width(), (y + 1) * tileSize - objData.objBitmap->height()), *objData.flagBitmap); + painter.drawImage(QPoint((x + 1) * tileSize - objData.objBitmap->width() - offset.x(), (y + 1) * tileSize - objData.objBitmap->height() - offset.y()), *objData.flagBitmap); } } diff --git a/mapeditor/maphandler.h b/mapeditor/maphandler.h index 3f8cb5a26..e4bfbf948 100644 --- a/mapeditor/maphandler.h +++ b/mapeditor/maphandler.h @@ -92,11 +92,11 @@ public: void reset(const CMap * Map); - void drawTerrainTile(QPainter & painter, int x, int y, int z); + void drawTerrainTile(QPainter & painter, int x, int y, int z, QPointF offset); /// draws a river segment on current tile - void drawRiver(QPainter & painter, int x, int y, int z); + void drawRiver(QPainter & painter, int x, int y, int z, QPointF offset); /// draws a road segment on current tile - void drawRoad(QPainter & painter, int x, int y, int z); + void drawRoad(QPainter & painter, int x, int y, int z, QPointF offset); std::set invalidate(const CGObjectInstance *); //invalidates object rects void invalidateObjects(); //invalidates all objects on the map @@ -111,8 +111,8 @@ public: std::set addObject(const CGObjectInstance * object); /// draws all objects on current tile (higher-level logic, unlike other draw*** methods) - void drawObjects(QPainter & painter, int x, int y, int z, const std::set & locked); - void drawObjectAt(QPainter & painter, const CGObjectInstance * object, int x, int y); + void drawObjects(QPainter & painter, int x, int y, int z, QPointF offset, const std::set & locked); + void drawObjectAt(QPainter & painter, const CGObjectInstance * object, int x, int y, QPointF offset); void drawMinimapTile(QPainter & painter, int x, int y, int z); diff --git a/mapeditor/mapview.cpp b/mapeditor/mapview.cpp index 1d478040d..bde5906e2 100644 --- a/mapeditor/mapview.cpp +++ b/mapeditor/mapview.cpp @@ -59,6 +59,8 @@ MapView::MapView(QWidget * parent): QGraphicsView(parent), selectionTool(MapView::SelectionTool::None) { + connect(horizontalScrollBar(), &QScrollBar::valueChanged, this, &MapView::setViewports); + connect(verticalScrollBar(), &QScrollBar::valueChanged, this, &MapView::setViewports); } void MapView::cameraChanged(const QPointF & pos) @@ -71,6 +73,11 @@ void MapView::setController(MapController * ctrl) controller = ctrl; } +void MapView::resizeEvent(QResizeEvent * event) +{ + setViewports(); +} + void MapView::mouseMoveEvent(QMouseEvent *mouseEvent) { this->update(); @@ -96,24 +103,24 @@ void MapView::mouseMoveEvent(QMouseEvent *mouseEvent) { case MapView::SelectionTool::Brush: if(mouseEvent->buttons() & Qt::RightButton) - sc->selectionTerrainView.erase(tile); + sc->selectionTerrainView.erase({tile}); else if(mouseEvent->buttons() == Qt::LeftButton) - sc->selectionTerrainView.select(tile); - sc->selectionTerrainView.draw(); + sc->selectionTerrainView.select({tile}); break; case MapView::SelectionTool::Brush2: { std::array extra{ int3{0, 0, 0}, int3{1, 0, 0}, int3{0, 1, 0}, int3{1, 1, 0} }; + std::vector tiles; for(auto & e : extra) { - if(mouseEvent->buttons() & Qt::RightButton) - sc->selectionTerrainView.erase(tile + e); - else if(mouseEvent->buttons() == Qt::LeftButton) - sc->selectionTerrainView.select(tile + e); + tiles.push_back(tile + e); } + if(mouseEvent->buttons() & Qt::RightButton) + sc->selectionTerrainView.erase(tiles); + else if(mouseEvent->buttons() == Qt::LeftButton) + sc->selectionTerrainView.select(tiles); } - sc->selectionTerrainView.draw(); break; case MapView::SelectionTool::Brush4: @@ -124,33 +131,37 @@ void MapView::mouseMoveEvent(QMouseEvent *mouseEvent) int3{-1, 1, 0}, int3{0, 1, 0}, int3{1, 1, 0}, int3{2, 1, 0}, int3{-1, 2, 0}, int3{0, 2, 0}, int3{1, 2, 0}, int3{2, 2, 0} }; + std::vector tiles; for(auto & e : extra) { - if(mouseEvent->buttons() & Qt::RightButton) - sc->selectionTerrainView.erase(tile + e); - else if(mouseEvent->buttons() == Qt::LeftButton) - sc->selectionTerrainView.select(tile + e); + tiles.push_back(tile + e); } + if(mouseEvent->buttons() & Qt::RightButton) + sc->selectionTerrainView.erase(tiles); + else if(mouseEvent->buttons() == Qt::LeftButton) + sc->selectionTerrainView.select(tiles); } - sc->selectionTerrainView.draw(); break; case MapView::SelectionTool::Area: + { if(mouseEvent->buttons() & Qt::RightButton || !(mouseEvent->buttons() & Qt::LeftButton)) break; sc->selectionTerrainView.clear(); + std::vector selectedTiles; for(int j = std::min(tile.y, tileStart.y); j < std::max(tile.y, tileStart.y); ++j) { for(int i = std::min(tile.x, tileStart.x); i < std::max(tile.x, tileStart.x); ++i) { - sc->selectionTerrainView.select(int3(i, j, sc->level)); + selectedTiles.emplace_back(i, j, sc->level); } } - sc->selectionTerrainView.draw(); + sc->selectionTerrainView.select(selectedTiles); break; - + } case MapView::SelectionTool::Line: + { { assert(tile.z == tileStart.z); const auto diff = tile - tileStart; @@ -174,39 +185,45 @@ void MapView::mouseMoveEvent(QMouseEvent *mouseEvent) if(mouseEvent->buttons() == Qt::LeftButton) { - for(auto & ts : temporaryTiles) - sc->selectionTerrainView.erase(ts); - + std::vectorerasedTiles(temporaryTiles.begin(), temporaryTiles.end()); + sc->selectionTerrainView.erase(erasedTiles); + + std::vectorselectedTiles; for(auto ts = tileStart; ts.dist2d(tileStart) < edge; ts += dir) { if(!controller->map()->isInTheMap(ts)) break; if(!sc->selectionTerrainView.selection().count(ts)) temporaryTiles.insert(ts); - sc->selectionTerrainView.select(ts); + selectedTiles.push_back(ts); } + sc->selectionTerrainView.select(selectedTiles); } if(mouseEvent->buttons() == Qt::RightButton) { - for(auto & ts : temporaryTiles) - sc->selectionTerrainView.select(ts); - + std::vectorselectedTiles(temporaryTiles.begin(), temporaryTiles.end()); + sc->selectionTerrainView.select(selectedTiles); + + std::vectorerasedTiles; for(auto ts = tileStart; ts.dist2d(tileStart) < edge; ts += dir) { if(!controller->map()->isInTheMap(ts)) break; if(sc->selectionTerrainView.selection().count(ts)) temporaryTiles.insert(ts); - sc->selectionTerrainView.erase(ts); + erasedTiles.push_back(ts); } + sc->selectionTerrainView.erase(selectedTiles); } - sc->selectionTerrainView.draw(); break; } - + } + case MapView::SelectionTool::Lasso: + { if(mouseEvent->buttons() == Qt::LeftButton) { + std::vectortiles; for(auto i = tilePrev; i != tile;) { int length = std::numeric_limits::max(); @@ -220,18 +237,19 @@ void MapView::mouseMoveEvent(QMouseEvent *mouseEvent) } } i += dir; - sc->selectionTerrainView.select(i); + tiles.push_back(i); } - sc->selectionTerrainView.draw(); + sc->selectionTerrainView.select(tiles); } break; + } case MapView::SelectionTool::None: if(mouseEvent->buttons() & Qt::RightButton) break; auto sh = tile - tileStart; - sc->selectionObjectsView.shift = QPoint(sh.x, sh.y); + sc->selectionObjectsView.setShift(sh.x, sh.y); if(sh.x || sh.y) { @@ -244,8 +262,6 @@ void MapView::mouseMoveEvent(QMouseEvent *mouseEvent) } } } - - sc->selectionObjectsView.draw(); break; } @@ -276,34 +292,31 @@ void MapView::mousePressEvent(QMouseEvent *event) case MapView::SelectionTool::Brush: case MapView::SelectionTool::Line: sc->selectionObjectsView.clear(); - sc->selectionObjectsView.draw(); if(event->button() == Qt::RightButton) - sc->selectionTerrainView.erase(tileStart); + sc->selectionTerrainView.erase({tileStart}); else if(event->button() == Qt::LeftButton) - sc->selectionTerrainView.select(tileStart); - sc->selectionTerrainView.draw(); + sc->selectionTerrainView.select({tileStart}); break; case MapView::SelectionTool::Brush2: sc->selectionObjectsView.clear(); - sc->selectionObjectsView.draw(); { std::array extra{ int3{0, 0, 0}, int3{1, 0, 0}, int3{0, 1, 0}, int3{1, 1, 0} }; + std::vector tiles; for(auto & e : extra) { - if(event->button() == Qt::RightButton) - sc->selectionTerrainView.erase(tileStart + e); - else if(event->button() == Qt::LeftButton) - sc->selectionTerrainView.select(tileStart + e); + tiles.push_back(tileStart + e); } + if(event->buttons() & Qt::RightButton) + sc->selectionTerrainView.erase(tiles); + else if(event->buttons() == Qt::LeftButton) + sc->selectionTerrainView.select(tiles); } - sc->selectionTerrainView.draw(); break; case MapView::SelectionTool::Brush4: sc->selectionObjectsView.clear(); - sc->selectionObjectsView.draw(); { std::array extra{ int3{-1, -1, 0}, int3{0, -1, 0}, int3{1, -1, 0}, int3{2, -1, 0}, @@ -311,15 +324,16 @@ void MapView::mousePressEvent(QMouseEvent *event) int3{-1, 1, 0}, int3{0, 1, 0}, int3{1, 1, 0}, int3{2, 1, 0}, int3{-1, 2, 0}, int3{0, 2, 0}, int3{1, 2, 0}, int3{2, 2, 0} }; + std::vector tiles; for(auto & e : extra) { - if(event->button() == Qt::RightButton) - sc->selectionTerrainView.erase(tileStart + e); - else if(event->button() == Qt::LeftButton) - sc->selectionTerrainView.select(tileStart + e); + tiles.push_back(tileStart + e); } + if(event->buttons() & Qt::RightButton) + sc->selectionTerrainView.erase(tiles); + else if(event->buttons() == Qt::LeftButton) + sc->selectionTerrainView.select(tiles); } - sc->selectionTerrainView.draw(); break; case MapView::SelectionTool::Area: @@ -328,63 +342,63 @@ void MapView::mousePressEvent(QMouseEvent *event) break; sc->selectionTerrainView.clear(); - sc->selectionTerrainView.draw(); sc->selectionObjectsView.clear(); - sc->selectionObjectsView.draw(); break; - + case MapView::SelectionTool::Fill: - { - if(event->button() != Qt::RightButton && event->button() != Qt::LeftButton) - break; - - std::vector queue; - queue.push_back(tileStart); - - const std::array dirs{ int3{1, 0, 0}, int3{-1, 0, 0}, int3{0, 1, 0}, int3{0, -1, 0} }; - - while(!queue.empty()) - { - auto tile = queue.back(); - queue.pop_back(); - if(event->button() == Qt::LeftButton) - sc->selectionTerrainView.select(tile); - else - sc->selectionTerrainView.erase(tile); - for(auto & d : dirs) - { - auto tilen = tile + d; - if(!controller->map()->isInTheMap(tilen)) - continue; - if(event->button() == Qt::LeftButton) - { - if(controller->map()->getTile(tile).roadType - && controller->map()->getTile(tile).roadType != controller->map()->getTile(tilen).roadType) - continue; - else if(controller->map()->getTile(tile).riverType - && controller->map()->getTile(tile).riverType != controller->map()->getTile(tilen).riverType) - continue; - else if(controller->map()->getTile(tile).terrainType != controller->map()->getTile(tilen).terrainType) - continue; - } - if(event->button() == Qt::LeftButton && sc->selectionTerrainView.selection().count(tilen)) - continue; - if(event->button() == Qt::RightButton && !sc->selectionTerrainView.selection().count(tilen)) - continue; - queue.push_back(tilen); - } - } - - - sc->selectionTerrainView.draw(); - sc->selectionObjectsView.clear(); - sc->selectionObjectsView.draw(); + { + if(event->button() != Qt::RightButton && event->button() != Qt::LeftButton) break; + + std::vector queue; + std::set tilesToFill; + queue.push_back(tileStart); + + const std::array dirs{ int3{1, 0, 0}, int3{-1, 0, 0}, int3{0, 1, 0}, int3{0, -1, 0} }; + + while(!queue.empty()) + { + auto tile = queue.back(); + queue.pop_back(); + tilesToFill.insert(tile); + for(auto & d : dirs) + { + auto tilen = tile + d; + if (tilesToFill.count(tilen)) + continue; + if(!controller->map()->isInTheMap(tilen)) + continue; + if(event->button() == Qt::LeftButton) + { + if(controller->map()->getTile(tile).roadType + && controller->map()->getTile(tile).roadType != controller->map()->getTile(tilen).roadType) + continue; + else if(controller->map()->getTile(tile).riverType + && controller->map()->getTile(tile).riverType != controller->map()->getTile(tilen).riverType) + continue; + else if(controller->map()->getTile(tile).terrainType != controller->map()->getTile(tilen).terrainType) + continue; + } + if(event->button() == Qt::LeftButton && sc->selectionTerrainView.selection().count(tilen)) + continue; + if(event->button() == Qt::RightButton && !sc->selectionTerrainView.selection().count(tilen)) + continue; + queue.push_back(tilen); + } } + std::vector result(tilesToFill.begin(), tilesToFill.end()); + + if(event->button() == Qt::LeftButton) + sc->selectionTerrainView.select(result); + else + sc->selectionTerrainView.erase(result); + + sc->selectionObjectsView.clear(); + break; + } case MapView::SelectionTool::None: sc->selectionTerrainView.clear(); - sc->selectionTerrainView.draw(); if(sc->selectionObjectsView.newObject && sc->selectionObjectsView.isSelected(sc->selectionObjectsView.newObject.get())) { @@ -426,15 +440,14 @@ void MapView::mousePressEvent(QMouseEvent *event) { sc->selectionObjectsView.clear(); sc->selectionObjectsView.selectionMode = SelectionObjectsLayer::SELECTION; - + if(!rubberBand) rubberBand = new QRubberBand(QRubberBand::Rectangle, this); rubberBand->setGeometry(QRect(mapFromScene(mouseStart), QSize())); rubberBand->show(); } } - sc->selectionObjectsView.shift = QPoint(0, 0); - sc->selectionObjectsView.draw(); + sc->selectionObjectsView.setShift(0, 0); } break; } @@ -474,7 +487,7 @@ void MapView::mouseReleaseEvent(QMouseEvent *event) case MapView::SelectionTool::Lasso: { if(event->button() == Qt::RightButton) break; - + std::vectorinitialTiles; //connect with initial tile for(auto i = tilePrev; i != tileStart;) { @@ -489,9 +502,10 @@ void MapView::mouseReleaseEvent(QMouseEvent *event) } } i += dir; - sc->selectionTerrainView.select(i); + initialTiles.push_back(i); } - + sc->selectionTerrainView.select(initialTiles); + //key: y position of tile //value.first: x position of left tile //value.second: x postiion of right tile @@ -532,10 +546,8 @@ void MapView::mouseReleaseEvent(QMouseEvent *event) selectionByY.insert(int3(selectionRange.first, i, sc->level)); } std::set_intersection(selectionByX.begin(), selectionByX.end(), selectionByY.begin(), selectionByY.end(), std::back_inserter(finalSelection)); - for(auto & lassoTile : finalSelection) - sc->selectionTerrainView.select(lassoTile); - - sc->selectionTerrainView.draw(); + sc->selectionTerrainView.select(finalSelection); + break; } @@ -556,8 +568,7 @@ void MapView::mouseReleaseEvent(QMouseEvent *event) else { sc->selectionObjectsView.selectionMode = SelectionObjectsLayer::NOTHING; - sc->selectionObjectsView.shift = QPoint(0, 0); - sc->selectionObjectsView.draw(); + sc->selectionObjectsView.setShift(0, 0); tab = true; } auto selection = sc->selectionObjectsView.getSelection(); @@ -642,10 +653,9 @@ void MapView::dragMoveEvent(QDragMoveEvent * event) if(sc->selectionObjectsView.newObject) { - sc->selectionObjectsView.shift = QPoint(tile.x, tile.y); - sc->selectionObjectsView.selectObject(sc->selectionObjectsView.newObject.get()); sc->selectionObjectsView.selectionMode = SelectionObjectsLayer::MOVEMENT; - sc->selectionObjectsView.draw(); + sc->selectionObjectsView.selectObject(sc->selectionObjectsView.newObject.get()); + sc->selectionObjectsView.setShift(tile.x, tile.y); } event->acceptProposedAction(); @@ -655,23 +665,25 @@ void MapView::dragLeaveEvent(QDragLeaveEvent * event) { if(!controller || !controller->map()) return; - + auto * sc = static_cast(scene()); if(!sc) return; - + controller->discardObject(sc->level); } - -bool MapView::viewportEvent(QEvent *event) +void MapView::setViewports() { - if(auto * sc = static_cast(scene())) + if(auto * sc = dynamic_cast(scene())) { auto rect = mapToScene(viewport()->geometry()).boundingRect(); controller->miniScene(sc->level)->viewport.setViewport(rect.x() / 32, rect.y() / 32, rect.width() / 32, rect.height() / 32); + for (auto * layer : sc->getDynamicLayers()) + { + layer->setViewport(rect); + } } - return QGraphicsView::viewportEvent(event); } MapSceneBase::MapSceneBase(int lvl): @@ -682,18 +694,31 @@ MapSceneBase::MapSceneBase(int lvl): void MapSceneBase::initialize(MapController & controller) { - for(auto * layer : getAbstractLayers()) + for(auto * layer : getStaticLayers()) + layer->initialize(controller); + for(auto * layer : getDynamicLayers()) layer->initialize(controller); } -void MapSceneBase::updateViews() +void MapSceneBase::createMap() { - for(auto * layer : getAbstractLayers()) + for(auto * layer : getStaticLayers()) layer->update(); + for(auto * layer : getDynamicLayers()) + layer->createLayer(); +} + +void MapSceneBase::updateMap() +{ + for(auto * layer : getStaticLayers()) + layer->update(); + for(auto * layer : getDynamicLayers()) + layer->redraw(); } MapScene::MapScene(int lvl): MapSceneBase(lvl), + emptyLayer(this), gridView(this), passabilityView(this), selectionTerrainView(this), @@ -708,7 +733,14 @@ MapScene::MapScene(int lvl): connect(&selectionObjectsView, &SelectionObjectsLayer::selectionMade, this, &MapScene::objectSelected); } -std::list MapScene::getAbstractLayers() +std::list MapScene::getStaticLayers() +{ + return { + &emptyLayer + }; +} + +std::list MapScene::getDynamicLayers() { //sequence is important because it defines rendering order return { @@ -722,9 +754,9 @@ std::list MapScene::getAbstractLayers() }; } -void MapScene::updateViews() +void MapScene::createMap() { - MapSceneBase::updateViews(); + MapSceneBase::createMap(); terrainView.show(true); objectsView.show(true); @@ -752,7 +784,7 @@ MinimapScene::MinimapScene(int lvl): { } -std::list MinimapScene::getAbstractLayers() +std::list MinimapScene::getStaticLayers() { //sequence is important because it defines rendering order return { @@ -761,10 +793,16 @@ std::list MinimapScene::getAbstractLayers() }; } -void MinimapScene::updateViews() +std::list MinimapScene::getDynamicLayers() { - MapSceneBase::updateViews(); - + //Nothing here + return {}; +} + +void MinimapScene::createMap() +{ + MapSceneBase::createMap(); + minimapView.show(true); viewport.show(true); } diff --git a/mapeditor/mapview.h b/mapeditor/mapview.h index 6c9c7b021..744932ce9 100644 --- a/mapeditor/mapview.h +++ b/mapeditor/mapview.h @@ -31,12 +31,11 @@ public: MapSceneBase(int lvl); const int level; - - virtual void updateViews(); + virtual void createMap(); + virtual void updateMap(); virtual void initialize(MapController &); - -protected: - virtual std::list getAbstractLayers() = 0; + virtual std::list getStaticLayers() = 0; + virtual std::list getDynamicLayers() = 0; }; class MinimapScene : public MapSceneBase @@ -44,13 +43,12 @@ class MinimapScene : public MapSceneBase public: MinimapScene(int lvl); - void updateViews() override; + void createMap() override; MinimapLayer minimapView; MinimapViewLayer viewport; - -protected: - std::list getAbstractLayers() override; + std::list getStaticLayers() override; + std::list getDynamicLayers() override; }; class MapScene : public MapSceneBase @@ -59,8 +57,11 @@ class MapScene : public MapSceneBase public: MapScene(int lvl); - void updateViews() override; + void createMap() override; + std::list getStaticLayers() override; + std::list getDynamicLayers() override; + EmptyLayer emptyLayer; GridLayer gridView; PassabilityLayer passabilityView; SelectionTerrainLayer selectionTerrainView; @@ -75,10 +76,8 @@ signals: public slots: void terrainSelected(bool anything); void objectSelected(bool anything); - -protected: - std::list getAbstractLayers() override; +protected: bool isTerrainSelected; bool isObjectSelected; @@ -100,6 +99,7 @@ public: SelectionTool selectionTool; public slots: + void resizeEvent (QResizeEvent * event) override; void mouseMoveEvent(QMouseEvent * mouseEvent) override; void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; @@ -107,8 +107,8 @@ public slots: void dragMoveEvent(QDragMoveEvent *event) override; void dragLeaveEvent(QDragLeaveEvent *event) override; void dropEvent(QDropEvent * event) override; - void cameraChanged(const QPointF & pos); + void setViewports(); signals: void openObjectProperties(CGObjectInstance *, bool switchTab); @@ -116,7 +116,6 @@ signals: //void viewportChanged(const QRectF & rect); protected: - bool viewportEvent(QEvent *event) override; private: MapController * controller = nullptr; diff --git a/mapeditor/scenelayer.cpp b/mapeditor/scenelayer.cpp index 7f34be7ff..706c0e2a7 100644 --- a/mapeditor/scenelayer.cpp +++ b/mapeditor/scenelayer.cpp @@ -29,15 +29,32 @@ void AbstractLayer::initialize(MapController & controller) void AbstractLayer::show(bool show) { - if(isShown == show) - return; - isShown = show; - + redraw(); } -void AbstractLayer::redraw() + +int AbstractLayer::mapWidthPx() const +{ + return map ? map->width * tileSize : 0; +} + +int AbstractLayer::mapHeightPx() const +{ + return map ? map->height * tileSize : 0; +} + +int AbstractLayer::toInt(double value) const +{ + return static_cast(std::round(value)); // is rounded explicitly in order to avoid rounding down unprecise double values +} + +AbstractFixedLayer::AbstractFixedLayer(MapSceneBase * s): AbstractLayer(s) +{ +} + +void AbstractFixedLayer::redraw() { if(item) { @@ -55,65 +72,284 @@ void AbstractLayer::redraw() } } -GridLayer::GridLayer(MapSceneBase * s): AbstractLayer(s) +AbstractViewportLayer::AbstractViewportLayer(MapSceneBase * s): AbstractLayer(s) { } -void GridLayer::update() +void AbstractViewportLayer::createLayer() +{ + QListemptyList; + items.reset(scene->createItemGroup(emptyList)); +} + +void AbstractViewportLayer::setViewport(const QRectF & viewPort) { if(!map) return; - - pixmap.reset(new QPixmap(map->width * 32, map->height * 32)); - pixmap->fill(Qt::transparent); - QPainter painter(pixmap.get()); - painter.setPen(QColor(0, 0, 0, 190)); - - for(int j = 0; j < map->height; ++j) + if (items->boundingRect().contains(viewPort)) + return; + + std::vector outOfScreenSectors; + for (QGraphicsItem * sector : getAllSectors()) { - painter.drawLine(0, j * 32, map->width * 32 - 1, j * 32); + if (!viewPort.intersects(sector->sceneBoundingRect())) + outOfScreenSectors.push_back(sector); } - for(int i = 0; i < map->width; ++i) + for (QGraphicsItem * sector : outOfScreenSectors) { - painter.drawLine(i * 32, 0, i * 32, map->height * 32 - 1); + removeSector(sector); } - + + std::vector newAreas; + + int left = toInt(viewPort.left()); + int right = toInt(viewPort.right()); + int top = toInt(viewPort.top()); + int bottom = toInt(viewPort.bottom()); + int startX = left - (left % sectorSize); + int limitX = std::min(right + (sectorSize - right % sectorSize), mapWidthPx()); + int startY = top - (top % sectorSize); + int limitY = std::min(bottom + (sectorSize - bottom % sectorSize), mapHeightPx()); + + for (int x = startX; x < limitX; x += sectorSize) + { + for (int y = startY; y < limitY; y += sectorSize) + { + int width = x + sectorSize < limitX ? sectorSize : limitX - x; + int height = y + sectorSize < limitY ? sectorSize : limitY - y; + QRectF area(x, y, width, height); + if (!items->boundingRect().intersects(area)) + newAreas.emplace_back(area); + } + } + + for(QRectF newSection : newAreas) + { + QGraphicsItem * sector = draw(newSection); + if (sector) + addSector(sector); + } +} + +void AbstractViewportLayer::update() +{ redraw(); } -PassabilityLayer::PassabilityLayer(MapSceneBase * s): AbstractLayer(s) +void AbstractViewportLayer::redraw() { + std::set allSectors; + for (auto * sector : getAllSectors()) + allSectors.insert(sector); + redrawSectors(allSectors); } -void PassabilityLayer::update() +void AbstractViewportLayer::redraw(const std::vector & tiles) { - if(!map) - return; - - pixmap.reset(new QPixmap(map->width * 32, map->height * 32)); - pixmap->fill(Qt::transparent); - - QPainter painter(pixmap.get()); - for(int j = 0; j < map->height; ++j) + std::set sectorsToRedraw = getContainingSectors(tiles); + redrawSectors(sectorsToRedraw); +} + +void AbstractViewportLayer::redrawWithSurroundingTiles(const std::vector & tiles) +{ + int maxX = 0; + int maxY = 0; + int minX = INT_MAX; + int minY = INT_MAX; + for (const int3 tile : tiles) { - for(int i = 0; i < map->width; ++i) + maxX = std::max(tile.x, maxX); + maxY = std::max(tile.y, maxY); + minX = std::min(tile.x, minX); + minY = std::min(tile.y, minY); + } + + QRectF bounds((minX - 2) * tileSize, (minY - 2) * tileSize, (maxX - minX + 4) * tileSize, (maxY - minY + 4) * tileSize); //tiles start with 1, QRectF from 0 + redraw({bounds}); +} + +void AbstractViewportLayer::redraw(const std::set & objects) +{ + std::vector areas(objects.size()); + for (const CGObjectInstance * object : objects) + { + areas.push_back(getObjectArea(object)); + } + redraw(areas); +} + +void AbstractViewportLayer::redraw(const std::vector & areas) +{ + std::set intersectingSectors; + for (QGraphicsItem * existingSector : getAllSectors()) + { + for (auto area : areas) { - auto tl = map->getTile(int3(i, j, scene->level)); - if(tl.blocked() || tl.visitable()) + if (existingSector->sceneBoundingRect().intersects(area)) { - painter.fillRect(i * 32, j * 32, 31, 31, tl.visitable() ? QColor(200, 200, 0, 64) : QColor(255, 0, 0, 64)); + intersectingSectors.insert(existingSector); } } } - + redrawSectors(intersectingSectors); +} + +QRectF AbstractViewportLayer::getObjectArea(const CGObjectInstance * object) +{ + auto pos = object->pos; + int x = ((pos.x + 1) * tileSize) - (object->getWidth() * tileSize); //Qt set 0,0 point on the top right corner, CGObjectInstance on the bottom left + int y = ((pos.y + 1) * tileSize) - (object->getHeight() * tileSize); + QRectF objectArea(x, y, object->getWidth() * tileSize, object->getHeight() * tileSize); + return objectArea; +} + +void AbstractViewportLayer::addSector(QGraphicsItem * sector) +{ + items->addToGroup(sector); +} + +void AbstractViewportLayer::removeSector(QGraphicsItem * sector) +{ + items->removeFromGroup(sector); + delete sector; +} + +void AbstractViewportLayer::redrawSectors(std::set & sectors) +{ + std::set sectorsToRemove; + + for (QGraphicsItem * existingSectors : getAllSectors()) + { + for (QGraphicsItem * sector : sectors) + { + if (existingSectors->sceneBoundingRect().contains(sector->sceneBoundingRect())) + sectorsToRemove.insert(existingSectors); + } + } + for (QGraphicsItem * sectorToRemove : sectorsToRemove) + { + addSector(draw(sectorToRemove->sceneBoundingRect())); + removeSector(sectorToRemove); + } +} + +const QList AbstractViewportLayer::getAllSectors() //returning const is necessary to avoid "range-loop might detach Qt container" problem +{ + QList emptyList; + return items ? items->childItems() : emptyList; +} + +std::set AbstractViewportLayer::getContainingSectors(const std::vector & tiles) +{ + std::set result; + for (QGraphicsItem * existingSector : getAllSectors()) { + for (const int3 tile : tiles) + { + if (existingSector->sceneBoundingRect().contains(QPointF(tile.x * tileSize, tile.y * tileSize))) + { + result.insert(existingSector); + break; + } + } + } + return result; +} + +std::set AbstractViewportLayer::getIntersectingSectors(const std::vector & areas) +{ + std::set result; + for (QGraphicsItem * existingSector : getAllSectors()) { + for (QRectF area : areas) + { + if (existingSector->sceneBoundingRect().intersects(area)) + { + result.insert(existingSector); + } + } + } + return result; +} + +EmptyLayer::EmptyLayer(MapSceneBase * s): AbstractFixedLayer(s) +{ + isShown = true; +} + +void EmptyLayer::update() +{ + if(!map) + return; + + pixmap = std::make_unique(map->width * 32, map->height * 32); redraw(); } -ObjectPickerLayer::ObjectPickerLayer(MapSceneBase * s): AbstractLayer(s) +GridLayer::GridLayer(MapSceneBase * s): AbstractViewportLayer(s) { } -void ObjectPickerLayer::highlight(std::function predicate) +QGraphicsItem * GridLayer::draw(const QRectF & section) +{ + QPixmap pixmap(toInt(section.width()), toInt(section.height())); + pixmap.fill(Qt::transparent); + if (isShown) + { + QPainter painter(&pixmap); + painter.setPen(QColor(0, 0, 0, 190)); + + for(int j = 0; j <= pixmap.height(); j += tileSize) + { + painter.drawLine(0, j, pixmap.width(), j); + } + for(int i = 0; i <= pixmap.width(); i += tileSize) + { + painter.drawLine(i, 0, i, pixmap.height()); + } + } + + QGraphicsItem * result = scene->addPixmap(pixmap); + result->setPos(section.x(), section.y()); + + return result; +} + +PassabilityLayer::PassabilityLayer(MapSceneBase * s): AbstractViewportLayer(s) +{ +} + +QGraphicsItem * PassabilityLayer::draw(const QRectF & section) +{ + + QPixmap pixmap(toInt(section.width()), toInt(section.height())); + pixmap.fill(Qt::transparent); + + if(isShown) + { + QPainter painter(&pixmap); + for(int j = 0; j <= pixmap.height(); j += tileSize) + { + for(int i = 0; i < pixmap.width(); i += tileSize) + { + auto tl = map->getTile(int3(toInt(section.x())/tileSize + i/tileSize, toInt(section.y())/tileSize + j/tileSize, scene->level)); + if(tl.blocked() || tl.visitable()) + { + painter.fillRect(i, j, 31, 31, tl.visitable() ? QColor(200, 200, 0, 64) : QColor(255, 0, 0, 64)); + } + } + } + } + + QGraphicsItem * result = scene->addPixmap(pixmap); + result->setPos(section.x(), section.y()); + + return result; +} + +ObjectPickerLayer::ObjectPickerLayer(MapSceneBase * s): AbstractViewportLayer(s) +{ +} + +void ObjectPickerLayer::highlight(const std::function & predicate) { if(!map) return; @@ -151,29 +387,33 @@ void ObjectPickerLayer::clear() isActive = false; } -void ObjectPickerLayer::update() +QGraphicsItem * ObjectPickerLayer::draw(const QRectF & section) { - if(!map) - return; - - pixmap.reset(new QPixmap(map->width * 32, map->height * 32)); - pixmap->fill(Qt::transparent); - if(isActive) - pixmap->fill(QColor(255, 255, 255, 128)); - - - QPainter painter(pixmap.get()); + + int offsetX = toInt(section.x()); + int offsetY = toInt(section.y()); + QPixmap pixmap(toInt(section.width()), toInt(section.height())); + pixmap.fill(Qt::transparent); + + if(isVisible()) + pixmap.fill(QColor(255, 255, 255, 128)); + + + QPainter painter(&pixmap); painter.setCompositionMode(QPainter::CompositionMode_Source); - for(auto * obj : possibleObjects) + for(const auto * obj : possibleObjects) { if(obj->pos.z != scene->level) continue; - - for(auto & pos : obj->getBlockedPos()) - painter.fillRect(pos.x * 32, pos.y * 32, 32, 32, QColor(255, 211, 0, 64)); + + for(const auto & pos : obj->getBlockedPos()) + painter.fillRect(pos.x * tileSize - offsetX, pos.y * tileSize - offsetY, tileSize, tileSize, QColor(255, 211, 0, 64)); } - painter.setCompositionMode(QPainter::CompositionMode_SourceOver); - redraw(); + + QGraphicsItem * result = scene->addPixmap(pixmap); + result->setPos(section.x(), section.y()); + + return result; } void ObjectPickerLayer::select(const CGObjectInstance * obj) @@ -181,92 +421,73 @@ void ObjectPickerLayer::select(const CGObjectInstance * obj) if(obj && possibleObjects.count(obj)) { clear(); - selectionMade(obj); + Q_EMIT selectionMade(obj); } } void ObjectPickerLayer::discard() { clear(); - selectionMade(nullptr); + Q_EMIT selectionMade(nullptr); } -SelectionTerrainLayer::SelectionTerrainLayer(MapSceneBase * s): AbstractLayer(s) +SelectionTerrainLayer::SelectionTerrainLayer(MapSceneBase * s): AbstractViewportLayer(s) { } -void SelectionTerrainLayer::update() +QGraphicsItem * SelectionTerrainLayer::draw(const QRectF & section) { - if(!map) - return; - - area.clear(); - areaAdd.clear(); - areaErase.clear(); - onSelection(); - - pixmap.reset(new QPixmap(map->width * 32, map->height * 32)); - pixmap->fill(Qt::transparent); - - redraw(); -} + int offsetX = toInt(section.x()); + int offsetY = toInt(section.y()); + QPixmap pixmap(toInt(section.width()), toInt(section.height())); + pixmap.fill(Qt::transparent); + + QPainter painter(&pixmap); -void SelectionTerrainLayer::draw() -{ - if(!pixmap) - return; - - QPainter painter(pixmap.get()); painter.setCompositionMode(QPainter::CompositionMode_Source); - for(auto & t : areaAdd) + for(const auto & t : area) { - painter.fillRect(t.x * 32, t.y * 32, 31, 31, QColor(128, 128, 128, 96)); + if(section.contains(t.x * tileSize, t.y * tileSize)) + painter.fillRect(t.x * tileSize - offsetX, t.y * tileSize - offsetY, 31, 31, QColor(128, 128, 128, 96)); } - for(auto & t : areaErase) - { - painter.fillRect(t.x * 32, t.y * 32, 31, 31, QColor(0, 0, 0, 0)); - } - - areaAdd.clear(); - areaErase.clear(); - - redraw(); + + QGraphicsPixmapItem * result = scene->addPixmap(pixmap); + result->setPos(section.x(), section.y()); + + return result; } -void SelectionTerrainLayer::select(const int3 & tile) +void SelectionTerrainLayer::select(const std::vector & tiles) { - if(!map || !map->isInTheMap(tile)) - return; - - if(!area.count(tile)) + for (int3 tile : tiles) { - area.insert(tile); - areaAdd.insert(tile); - areaErase.erase(tile); + if(!area.count(tile)) + { + area.insert(tile); + } } + redraw(tiles); onSelection(); } -void SelectionTerrainLayer::erase(const int3 & tile) +void SelectionTerrainLayer::erase(const std::vector & tiles) { - if(!map || !map->isInTheMap(tile)) - return; - - if(area.count(tile)) + for (int3 tile : tiles) { - area.erase(tile); - areaErase.insert(tile); - areaAdd.erase(tile); + if(area.count(tile)) + { + area.erase(tile); + } } + redraw(tiles); onSelection(); } void SelectionTerrainLayer::clear() { - areaErase = area; - areaAdd.clear(); area.clear(); onSelection(); + redraw(); } const std::set & SelectionTerrainLayer::selection() const @@ -276,157 +497,87 @@ const std::set & SelectionTerrainLayer::selection() const void SelectionTerrainLayer::onSelection() { - selectionMade(!area.empty()); + Q_EMIT selectionMade(!area.empty()); } -TerrainLayer::TerrainLayer(MapSceneBase * s): AbstractLayer(s) +TerrainLayer::TerrainLayer(MapSceneBase * s): AbstractViewportLayer(s) { } -void TerrainLayer::update() +void TerrainLayer::redrawTerrain(const std::vector & tiles) { - if(!map) - return; - - pixmap.reset(new QPixmap(map->width * 32, map->height * 32)); - draw(false); + redrawWithSurroundingTiles(tiles); } -void TerrainLayer::setDirty(const int3 & tile) +QGraphicsItem * TerrainLayer::draw(const QRectF & section) { - dirty.insert(tile); -} + int left = toInt(section.left()); + int right = toInt(section.right()); + int top = toInt(section.top()); + int bottom = toInt(section.bottom()); + QPixmap pixmap(toInt(section.width()), toInt(section.height())); + pixmap.fill(Qt::transparent); -void TerrainLayer::draw(bool onlyDirty) -{ - if(!pixmap) - return; - - if(!map) - return; - - QPainter painter(pixmap.get()); - //painter.setCompositionMode(QPainter::CompositionMode_Source); - - if(onlyDirty) + QPainter painter(&pixmap); + + QPointF offset = section.topLeft(); + + for(int x = left/tileSize; x < right/tileSize; ++x) { - std::set forRedrawing(dirty); - std::set neighbours; - for(auto & t : dirty) + for(int y = top/tileSize; y < bottom/tileSize; ++y) { - for(auto & tt : int3::getDirs()) - { - if(map->isInTheMap(t + tt)) - neighbours.insert(t + tt); - } - } - for(auto & t : neighbours) - { - for(auto & tt : int3::getDirs()) - { - forRedrawing.insert(t); - if(map->isInTheMap(t + tt)) - forRedrawing.insert(t + tt); - } - } - for(auto & t : forRedrawing) - { - handler->drawTerrainTile(painter, t.x, t.y, scene->level); - handler->drawRiver(painter, t.x, t.y, scene->level); - handler->drawRoad(painter, t.x, t.y, scene->level); + handler->drawTerrainTile(painter, x, y, scene->level, offset); + handler->drawRiver(painter, x, y, scene->level, offset); + handler->drawRoad(painter, x, y, scene->level, offset); } } - else + + QGraphicsPixmapItem * result = scene->addPixmap(pixmap); + result->setPos(section.x(), section.y()); + + return result; +} + +ObjectsLayer::ObjectsLayer(MapSceneBase * s): AbstractViewportLayer(s) +{ +} + +QGraphicsItem * ObjectsLayer::draw(const QRectF & section) +{ + int left = toInt(section.left()); + int right = toInt(section.right()); + int top = toInt(section.top()); + int bottom = toInt(section.bottom()); + QPixmap pixmap(toInt(section.width()), toInt(section.height())); + pixmap.fill(Qt::transparent); + + if (isShown) { - for(int j = 0; j < map->height; ++j) + QPainter painter(&pixmap); + + QPointF offset = section.topLeft(); + + int margin = 2; // margin is necessary to properly display flags on heroes on a border between two sections + + for(int x = (left - margin)/tileSize; x < (right + margin)/tileSize; ++x) { - for(int i = 0; i < map->width; ++i) + for(int y = (top - margin)/tileSize; y < (bottom + margin)/tileSize; ++y) { - handler->drawTerrainTile(painter, i, j, scene->level); - handler->drawRiver(painter, i, j, scene->level); - handler->drawRoad(painter, i, j, scene->level); + handler->drawObjects(painter, x, y, scene->level, offset, lockedObjects); } } } - - dirty.clear(); - redraw(); + + QGraphicsPixmapItem * result = scene->addPixmap(pixmap); + result->setPos(section.x(), section.y()); + + return result; } -ObjectsLayer::ObjectsLayer(MapSceneBase * s): AbstractLayer(s) +void ObjectsLayer::redrawObjects(const std::set & objects) { -} - -void ObjectsLayer::update() -{ - if(!map) - return; - - pixmap.reset(new QPixmap(map->width * 32, map->height * 32)); - pixmap->fill(Qt::transparent); - draw(false); -} - -void ObjectsLayer::draw(bool onlyDirty) -{ - if(!pixmap) - return; - - if(!map) - return; - - QPainter painter(pixmap.get()); - - if(onlyDirty) - { - //objects could be modified - for(auto * obj : objDirty) - setDirty(obj); - - //clear tiles which will be redrawn. It's needed because some object could be replaced - painter.setCompositionMode(QPainter::CompositionMode_Source); - for(auto & p : dirty) - painter.fillRect(p.x * 32, p.y * 32, 32, 32, Qt::transparent); - painter.setCompositionMode(QPainter::CompositionMode_SourceOver); - - for(auto & p : dirty) - handler->drawObjects(painter, p.x, p.y, p.z, lockedObjects); - } - else - { - pixmap->fill(Qt::transparent); - for(int j = 0; j < map->height; ++j) - { - for(int i = 0; i < map->width; ++i) - { - handler->drawObjects(painter, i, j, scene->level, lockedObjects); - } - } - } - - dirty.clear(); - redraw(); -} - -void ObjectsLayer::setDirty(int x, int y) -{ - int3 pos(x, y, scene->level); - if(map->isInTheMap(pos)) - dirty.insert(pos); -} - -void ObjectsLayer::setDirty(const CGObjectInstance * object) -{ - objDirty.insert(object); - //mark tiles under object as dirty - for(int j = 0; j < object->getHeight(); ++j) - { - for(int i = 0; i < object->getWidth(); ++i) - { - setDirty(object->anchorPos().x - i, object->anchorPos().y - j); - } - } + redraw(objects); } void ObjectsLayer::setLockObject(const CGObjectInstance * object, bool lock) @@ -442,64 +593,63 @@ void ObjectsLayer::unlockAll() lockedObjects.clear(); } -SelectionObjectsLayer::SelectionObjectsLayer(MapSceneBase * s): AbstractLayer(s), newObject(nullptr) +SelectionObjectsLayer::SelectionObjectsLayer(MapSceneBase * s): AbstractViewportLayer(s), newObject(nullptr) { } -void SelectionObjectsLayer::update() -{ - if(!map) - return; - - selectedObjects.clear(); - onSelection(); - shift = QPoint(); - newObject.reset(); - - pixmap.reset(new QPixmap(map->width * 32, map->height * 32)); - //pixmap->fill(QColor(0, 0, 0, 0)); - - draw(); -} -void SelectionObjectsLayer::draw() +QGraphicsItem * SelectionObjectsLayer::draw(const QRectF & section) { - if(!pixmap) - return; - - pixmap->fill(Qt::transparent); - - QPainter painter(pixmap.get()); - painter.setCompositionMode(QPainter::CompositionMode_Source); - painter.setPen(Qt::white); - - for(auto * obj : selectedObjects) + QPixmap pixmap(toInt(section.width()), toInt(section.height())); + pixmap.fill(Qt::transparent); + + if (isShown) { - if(obj != newObject.get()) + QPainter painter(&pixmap); + painter.setCompositionMode(QPainter::CompositionMode_Source); + painter.setPen(Qt::white); + + QPointF offset = section.topLeft(); + + for(auto * obj : selectedObjects) { - QRect bbox(obj->anchorPos().x, obj->anchorPos().y, 1, 1); - for(auto & t : obj->getBlockedPos()) + auto objectArea = getObjectArea(obj); + if(obj != newObject.get() && section.intersects(objectArea)) { - QPoint topLeft(std::min(t.x, bbox.topLeft().x()), std::min(t.y, bbox.topLeft().y())); - bbox.setTopLeft(topLeft); - QPoint bottomRight(std::max(t.x, bbox.bottomRight().x()), std::max(t.y, bbox.bottomRight().y())); - bbox.setBottomRight(bottomRight); + auto pos = obj->anchorPos(); + QRectF bbox(pos.x, pos.y, 1, 1); + for(const auto & t : obj->getBlockedPos()) + { + QPointF topLeft(std::min(t.x * 1.0, bbox.topLeft().x()), std::min(t.y * 1.0, bbox.topLeft().y())); + bbox.setTopLeft(topLeft); + QPointF bottomRight(std::max(t.x * 1.0, bbox.bottomRight().x()), std::max(t.y * 1.0, bbox.bottomRight().y())); + bbox.setBottomRight(bottomRight); + } + //selection box's size was decreased by 1 px to get rid of a persistent bug + //with displaying a box on a border of two sectors. Bite me. + + painter.setOpacity(1.0); + QRectF rect((bbox.x() * tileSize + 1) - offset.x(), (bbox.y() * tileSize + 1) - offset.y(), (bbox.width() * tileSize) - 2, (bbox.height() * tileSize) - 2); + painter.drawRect(rect); + } + + if(selectionMode == SelectionMode::MOVEMENT && (shift.x() || shift.y())) + { + objectArea.moveTo(objectArea.topLeft() + (shift * tileSize)); + if (section.intersects(objectArea)) + { + painter.setOpacity(0.7); + auto newPos = QPoint(obj->anchorPos().x, obj->anchorPos().y) + shift; + handler->drawObjectAt(painter, obj, newPos.x(), newPos.y(), offset); + } } - - painter.setOpacity(1.0); - painter.drawRect(bbox.x() * 32, bbox.y() * 32, bbox.width() * 32, bbox.height() * 32); - } - - //show translation - if(selectionMode == SelectionMode::MOVEMENT && (shift.x() || shift.y())) - { - painter.setOpacity(0.7); - auto newPos = QPoint(obj->anchorPos().x, obj->anchorPos().y) + shift; - handler->drawObjectAt(painter, obj, newPos.x(), newPos.y()); } } - - redraw(); + + QGraphicsPixmapItem * result = scene->addPixmap(pixmap); + result->setPos(section.x(), section.y()); + + return result; } CGObjectInstance * SelectionObjectsLayer::selectObjectAt(int x, int y, const CGObjectInstance * ignore) const @@ -558,7 +708,8 @@ void SelectionObjectsLayer::selectObjects(int x1, int y1, int x2, int y2) if(y1 > y2) std::swap(y1, y2); - + + std::set selectedObjects; for(int j = y1; j < y2; ++j) { for(int i = x1; i < x2; ++i) @@ -567,25 +718,36 @@ void SelectionObjectsLayer::selectObjects(int x1, int y1, int x2, int y2) { for(auto & o : handler->getObjects(i, j, scene->level)) if(!lockedObjects.count(o.obj)) - selectObject(const_cast(o.obj), false); //do not inform about each object added + { + selectedObjects.insert(const_cast(o.obj)); + } } } } - onSelection(); + selectObjects(selectedObjects); } -void SelectionObjectsLayer::selectObject(CGObjectInstance * obj, bool inform /* = true */) +void SelectionObjectsLayer::selectObject(CGObjectInstance * obj) { selectedObjects.insert(obj); - if (inform) + onSelection(); + redraw({obj}); +} + +void SelectionObjectsLayer::selectObjects(const std::set & objs) +{ + for (CGObjectInstance * obj : objs) { - onSelection(); + selectedObjects.insert(obj); } + onSelection(); + redraw(objs); } void SelectionObjectsLayer::deselectObject(CGObjectInstance * obj) { selectedObjects.erase(obj); + redraw({obj}); } bool SelectionObjectsLayer::isSelected(const CGObjectInstance * obj) const @@ -601,22 +763,47 @@ std::set SelectionObjectsLayer::getSelection() const void SelectionObjectsLayer::clear() { selectedObjects.clear(); - onSelection(); shift.setX(0); shift.setY(0); + redraw(); } void SelectionObjectsLayer::onSelection() { - selectionMade(!selectedObjects.empty()); + Q_EMIT selectionMade(!selectedObjects.empty()); } -void SelectionObjectsLayer::setLockObject(const CGObjectInstance * object, bool lock) +void SelectionObjectsLayer::setShift(int x, int y) +{ + std::vectorareas; + + if(shift.x() || shift.y()) + { + for (auto * selectedObject : selectedObjects) + { + QRectF formerArea = getObjectArea(selectedObject); + formerArea.moveTo(formerArea.topLeft() + (shift * tileSize)); + areas.emplace_back(formerArea); + } + } + + shift = QPoint(x, y); + for (auto * selectedObject : selectedObjects) + { + QRectF area = getObjectArea(selectedObject); + area.moveTo(area.topLeft() + (shift * tileSize)); + areas.emplace_back(area); + } + redraw(areas); +} + +void SelectionObjectsLayer::setLockObject(CGObjectInstance * object, bool lock) { if(lock) lockedObjects.insert(object); else lockedObjects.erase(object); + redraw({object}); } void SelectionObjectsLayer::unlockAll() @@ -624,18 +811,18 @@ void SelectionObjectsLayer::unlockAll() lockedObjects.clear(); } -MinimapLayer::MinimapLayer(MapSceneBase * s): AbstractLayer(s) +MinimapLayer::MinimapLayer(MapSceneBase * s): AbstractFixedLayer(s) { - + } void MinimapLayer::update() { if(!map) return; - - pixmap.reset(new QPixmap(map->width, map->height)); - + + pixmap = std::make_unique(map->width, map->height); + QPainter painter(pixmap.get()); //coordinate transformation for(int j = 0; j < map->height; ++j) @@ -649,7 +836,7 @@ void MinimapLayer::update() redraw(); } -MinimapViewLayer::MinimapViewLayer(MapSceneBase * s): AbstractLayer(s) +MinimapViewLayer::MinimapViewLayer(MapSceneBase * s): AbstractFixedLayer(s) { } @@ -657,9 +844,9 @@ void MinimapViewLayer::update() { if(!map) return; - - pixmap.reset(new QPixmap(map->width, map->height)); - + + pixmap = std::make_unique(map->width, map->height); + draw(); } diff --git a/mapeditor/scenelayer.h b/mapeditor/scenelayer.h index cccc130a4..c832f23da 100644 --- a/mapeditor/scenelayer.h +++ b/mapeditor/scenelayer.h @@ -28,60 +28,111 @@ class AbstractLayer : public QObject Q_OBJECT public: AbstractLayer(MapSceneBase * s); - - virtual void update() = 0; - - void show(bool show); - void redraw(); void initialize(MapController & controller); - + void show(bool show); + virtual void update() = 0; + virtual void redraw() = 0; + protected: + int mapWidthPx() const; + int mapHeightPx() const; + int toInt(double value) const; + MapSceneBase * scene; CMap * map = nullptr; MapHandler * handler = nullptr; bool isShown = false; - + const int tileSize = 32; +}; + +class AbstractFixedLayer : public AbstractLayer +{ + Q_OBJECT +public: + AbstractFixedLayer(MapSceneBase * s); + void redraw(); + +protected: std::unique_ptr pixmap; QPixmap emptyPixmap; - + private: std::unique_ptr item; }; +class AbstractViewportLayer : public AbstractLayer +{ +public: + AbstractViewportLayer(MapSceneBase * s); + void createLayer(); + void setViewport(const QRectF & _viewPort); -class GridLayer: public AbstractLayer + void update(); + void redraw(); +protected: + virtual QGraphicsItem * draw(const QRectF & area) = 0; + void redraw(const std::vector & tiles); + void redrawWithSurroundingTiles(const std::vector & tiles); + void redraw(const std::set & objects); + void redraw(const std::vector & areas); + QRectF getObjectArea(const CGObjectInstance * object); +private: + void addSector(QGraphicsItem * item); + void removeSector(QGraphicsItem * item); + void redrawSectors(std::set & items); + const QList getAllSectors(); + + std::set getContainingSectors(const std::vector & tiles); + std::set getIntersectingSectors(const std::vector & areas); + std::unique_ptr items; + const int sectorSizeInTiles = 10; + const int sectorSize = sectorSizeInTiles * tileSize; +}; + +class EmptyLayer: public AbstractFixedLayer +{ + Q_OBJECT +public: + EmptyLayer(MapSceneBase * s); + + void update() override; +}; + +class GridLayer: public AbstractViewportLayer { Q_OBJECT public: GridLayer(MapSceneBase * s); - - void update() override; + +protected: + QGraphicsItem * draw(const QRectF & section) override; }; -class PassabilityLayer: public AbstractLayer +class PassabilityLayer: public AbstractViewportLayer { Q_OBJECT public: PassabilityLayer(MapSceneBase * s); - - void update() override; + +protected: + QGraphicsItem * draw(const QRectF & section) override; }; -class SelectionTerrainLayer: public AbstractLayer +class SelectionTerrainLayer: public AbstractViewportLayer { Q_OBJECT public: SelectionTerrainLayer(MapSceneBase* s); - - void update() override; - - void draw(); - void select(const int3 & tile); - void erase(const int3 & tile); + + void select(const std::vector & tiles); + void erase(const std::vector & tiles); void clear(); - + const std::set & selection() const; +protected: + QGraphicsItem * draw(const QRectF & section) override; + signals: void selectionMade(bool anythingSelected); @@ -94,52 +145,39 @@ private: }; -class TerrainLayer: public AbstractLayer +class TerrainLayer: public AbstractViewportLayer { Q_OBJECT public: TerrainLayer(MapSceneBase * s); - - void update() override; - - void draw(bool onlyDirty = true); - void setDirty(const int3 & tile); - -private: - std::set dirty; + void redrawTerrain(const std::vector & tiles); + +protected: + QGraphicsItem * draw(const QRectF & section) override; }; -class ObjectsLayer: public AbstractLayer +class ObjectsLayer: public AbstractViewportLayer { Q_OBJECT public: ObjectsLayer(MapSceneBase * s); - - void update() override; - - void draw(bool onlyDirty = true); - - void setDirty(int x, int y); - void setDirty(const CGObjectInstance * object); + void redrawObjects(const std::set & objects); void setLockObject(const CGObjectInstance * object, bool lock); void unlockAll(); - +protected: + QGraphicsItem * draw(const QRectF & section) override; private: - std::set objDirty; std::set lockedObjects; - std::set dirty; }; -class ObjectPickerLayer: public AbstractLayer +class ObjectPickerLayer: public AbstractViewportLayer { Q_OBJECT public: ObjectPickerLayer(MapSceneBase * s); - - void update() override; bool isVisible() const; template @@ -147,14 +185,16 @@ public: { highlight([](const CGObjectInstance * o){ return dynamic_cast(o); }); } - - void highlight(std::function predicate); - + + void highlight(const std::function & predicate); + void clear(); void select(const CGObjectInstance *); void discard(); - +protected: + QGraphicsItem * draw(const QRectF & section) override; + signals: void selectionMade(const CGObjectInstance *); @@ -164,7 +204,7 @@ private: }; -class SelectionObjectsLayer: public AbstractLayer +class SelectionObjectsLayer: public AbstractViewportLayer { Q_OBJECT public: @@ -174,38 +214,37 @@ public: }; SelectionObjectsLayer(MapSceneBase* s); - - void update() override; - - void draw(); - + CGObjectInstance * selectObjectAt(int x, int y, const CGObjectInstance * ignore = nullptr) const; void selectObjects(int x1, int y1, int x2, int y2); - void selectObject(CGObjectInstance *, bool inform = true); + void selectObject(CGObjectInstance *); void deselectObject(CGObjectInstance *); bool isSelected(const CGObjectInstance *) const; std::set getSelection() const; void clear(); - void setLockObject(const CGObjectInstance * object, bool lock); + void setShift(int x, int y); + void setLockObject(CGObjectInstance * object, bool lock); void unlockAll(); - - QPoint shift; std::shared_ptr newObject; - //FIXME: magic number + QPoint shift; SelectionMode selectionMode = SelectionMode::NOTHING; +protected: + QGraphicsItem * draw(const QRectF & section) override; + signals: void selectionMade(bool anythingSelected); private: + void selectObjects(const std::set & objs); std::set selectedObjects; std::set lockedObjects; void onSelection(); }; -class MinimapLayer: public AbstractLayer +class MinimapLayer: public AbstractFixedLayer { public: MinimapLayer(MapSceneBase * s); @@ -213,7 +252,7 @@ public: void update() override; }; -class MinimapViewLayer: public AbstractLayer +class MinimapViewLayer: public AbstractFixedLayer { public: MinimapViewLayer(MapSceneBase * s); From 0c929b8fd874ab1a82c0df2c6b2fc0cbd68aee8a Mon Sep 17 00:00:00 2001 From: Opuszek Date: Mon, 6 Oct 2025 20:52:25 +0200 Subject: [PATCH 18/20] Code review fix --- mapeditor/scenelayer.cpp | 8 ++++---- mapeditor/scenelayer.h | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/mapeditor/scenelayer.cpp b/mapeditor/scenelayer.cpp index 706c0e2a7..49a25f452 100644 --- a/mapeditor/scenelayer.cpp +++ b/mapeditor/scenelayer.cpp @@ -194,7 +194,7 @@ void AbstractViewportLayer::redraw(const std::vector & areas) redrawSectors(intersectingSectors); } -QRectF AbstractViewportLayer::getObjectArea(const CGObjectInstance * object) +QRectF AbstractViewportLayer::getObjectArea(const CGObjectInstance * object) const { auto pos = object->pos; int x = ((pos.x + 1) * tileSize) - (object->getWidth() * tileSize); //Qt set 0,0 point on the top right corner, CGObjectInstance on the bottom left @@ -233,13 +233,13 @@ void AbstractViewportLayer::redrawSectors(std::set & sectors) } } -const QList AbstractViewportLayer::getAllSectors() //returning const is necessary to avoid "range-loop might detach Qt container" problem +const QList AbstractViewportLayer::getAllSectors() const //returning const is necessary to avoid "range-loop might detach Qt container" problem { QList emptyList; return items ? items->childItems() : emptyList; } -std::set AbstractViewportLayer::getContainingSectors(const std::vector & tiles) +std::set AbstractViewportLayer::getContainingSectors(const std::vector & tiles) const { std::set result; for (QGraphicsItem * existingSector : getAllSectors()) { @@ -255,7 +255,7 @@ std::set AbstractViewportLayer::getContainingSectors(const std: return result; } -std::set AbstractViewportLayer::getIntersectingSectors(const std::vector & areas) +std::set AbstractViewportLayer::getIntersectingSectors(const std::vector & areas) const { std::set result; for (QGraphicsItem * existingSector : getAllSectors()) { diff --git a/mapeditor/scenelayer.h b/mapeditor/scenelayer.h index c832f23da..ec55682a3 100644 --- a/mapeditor/scenelayer.h +++ b/mapeditor/scenelayer.h @@ -75,15 +75,15 @@ protected: void redrawWithSurroundingTiles(const std::vector & tiles); void redraw(const std::set & objects); void redraw(const std::vector & areas); - QRectF getObjectArea(const CGObjectInstance * object); + QRectF getObjectArea(const CGObjectInstance * object) const; private: void addSector(QGraphicsItem * item); void removeSector(QGraphicsItem * item); void redrawSectors(std::set & items); - const QList getAllSectors(); + const QList getAllSectors() const; - std::set getContainingSectors(const std::vector & tiles); - std::set getIntersectingSectors(const std::vector & areas); + std::set getContainingSectors(const std::vector & tiles) const; + std::set getIntersectingSectors(const std::vector & areas) const; std::unique_ptr items; const int sectorSizeInTiles = 10; const int sectorSize = sectorSizeInTiles * tileSize; From b7aefc198c0b05f41046be7296afc5d4b2e45390 Mon Sep 17 00:00:00 2001 From: Andrey Filipenkov Date: Tue, 7 Oct 2025 15:00:32 +0300 Subject: [PATCH 19/20] [iOS] ensure that all RPATH entries of dependencies point to real files AltStore has issues with symlinks to dylibs --- ios/rpath_remove_symlinks.sh | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/ios/rpath_remove_symlinks.sh b/ios/rpath_remove_symlinks.sh index acdcf4407..4c4bedcbd 100755 --- a/ios/rpath_remove_symlinks.sh +++ b/ios/rpath_remove_symlinks.sh @@ -1,7 +1,27 @@ #!/usr/bin/env bash -cd "$CODESIGNING_FOLDER_PATH/Frameworks" -tbbFilename=$(otool -L libNullkiller.dylib | egrep --only-matching 'libtbb\S+') -if [[ -L "$tbbFilename" ]]; then - mv -f "$(readlink "$tbbFilename")" "$tbbFilename" -fi +# AltStore has issues with symlinks to dylibs + +cd "$CODESIGNING_FOLDER_PATH/$BUNDLE_FRAMEWORKS_FOLDER_PATH" + +# find rpath entries that point to symlinks +rpathSymlinks=() +for binary in "../$EXECUTABLE_NAME" $(find . -type f -iname '*.dylib'); do + echo "checking $binary" + # dyld_info sample output: @rpath/libogg.0.dylib + for lib in $(dyld_info -linked_dylibs "$binary" | awk -F / '/@rpath/ {print $2}'); do + if [ -L "$lib" ]; then + echo "- symlink: $lib" + rpathSymlinks+=("$lib") + fi + done +done + +# move real files to symlinks location +echo +for symlink in "${rpathSymlinks[@]}"; do + mv -fv "$(readlink "$symlink")" "$symlink" +done + +# remove the rest of the useless symlinks +find . -type l -delete From 90924a74ef27e982775323d6c5566409103c7467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Zaremba?= Date: Wed, 8 Oct 2025 12:10:24 +0200 Subject: [PATCH 20/20] Fix null tile being processed in CGTownInstance::updateAppearance --- lib/mapObjects/CGTownInstance.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 9f99f74b3..f07daa261 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -698,7 +698,15 @@ ObjectInstanceID CGTownInstance::getObjInstanceID() const void CGTownInstance::updateAppearance() { - auto terrain = cb->getTile(visitablePos())->getTerrainID(); + const auto tile = cb->getTile(visitablePos()); + if(!tile) + { + logGlobal->warn("Town is misplaced at (%d, %d, %d)", visitablePos().x, + visitablePos().y, visitablePos().z); + return; + } + + auto terrain = tile->getTerrainID(); //FIXME: not the best way to do this auto app = getObjectHandler()->getOverride(terrain, this); if (app)