diff --git a/.github/workflows/aab-from-build.yml b/.github/workflows/aab-from-build.yml index 57ce6b12a..6392388ed 100644 --- a/.github/workflows/aab-from-build.yml +++ b/.github/workflows/aab-from-build.yml @@ -42,7 +42,7 @@ jobs: echo "ANDROID_AAB_PATH=$ANDROID_AAB_PATH" >> $GITHUB_ENV - name: Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: aab compression-level: 0 diff --git a/.github/workflows/github.yml b/.github/workflows/github.yml index 214987da1..c72c6e206 100644 --- a/.github/workflows/github.yml +++ b/.github/workflows/github.yml @@ -332,7 +332,7 @@ jobs: - name: Upload Artifact id: upload_artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} compression-level: 9 @@ -342,7 +342,7 @@ jobs: - name: Upload AAB Artifact id: upload_aab if: ${{ startsWith(matrix.platform, 'android') && github.ref == 'refs/heads/master' }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} - aab compression-level: 9 @@ -352,7 +352,7 @@ jobs: - name: Upload debug symbols id: upload_symbols if: ${{ startsWith(matrix.platform, 'msvc') }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} - symbols compression-level: 9 @@ -381,7 +381,7 @@ jobs: python3 CI/emit_partial.py - name: Upload partial JSON with build informations - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: partial-json-${{ matrix.platform }} path: .summary/${{ matrix.platform }}.json @@ -412,7 +412,7 @@ jobs: - name: Upload source code archive id: upload_source - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ${{ env.VCMI_PACKAGE_FILE_NAME }} compression-level: 9 @@ -428,7 +428,7 @@ jobs: JSON - name: Upload partial JSON with source informations - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: partial-json-source path: .summary/source.json @@ -615,7 +615,7 @@ jobs: PULL_REQUEST: ${{ github.event.pull_request.number }} - name: Download Artifact - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 with: name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} path: ${{github.workspace}}/artifact @@ -645,7 +645,7 @@ jobs: - name: Upload VCMI Installer Artifacts id: upload_installer - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} - installer compression-level: 9 @@ -673,7 +673,7 @@ jobs: JSON - name: Upload partial JSON with installer informations - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: partial-json-${{ matrix.platform }}-installer path: .summary/installer-${{ matrix.platform }}.json @@ -720,7 +720,7 @@ jobs: - name: Download all partial JSON artifacts continue-on-error: true - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 with: pattern: partial-json-* merge-multiple: true diff --git a/CI/wininstaller/installer.iss b/CI/wininstaller/installer.iss index f74af9881..1b9fba702 100644 --- a/CI/wininstaller/installer.iss +++ b/CI/wininstaller/installer.iss @@ -166,12 +166,12 @@ Source: "{#UCRTFilesPath}\{#InstallerArch}\*"; DestDir: "{app}"; Flags: ignoreve [Icons] -Name: "{group}\{cm:ShortcutLauncher}"; Filename: "{app}\VCMI_launcher.exe"; Comment: "{cm:ShortcutLauncherComment}"; Tasks: startmenu -Name: "{group}\{cm:ShortcutMapEditor}"; Filename: "{app}\VCMI_mapeditor.exe"; Comment: "{cm:ShortcutMapEditorComment}"; Tasks: startmenu +Name: "{group}\{cm:ShortcutLauncher}{code:GetBranchSuffix}"; Filename: "{app}\VCMI_launcher.exe"; Comment: "{cm:ShortcutLauncherComment}{code:GetBranchSuffix}"; Tasks: startmenu +Name: "{group}\{cm:ShortcutMapEditor}{code:GetBranchSuffix}"; Filename: "{app}\VCMI_mapeditor.exe"; Comment: "{cm:ShortcutMapEditorComment}{code:GetBranchSuffix}"; Tasks: startmenu Name: "{group}\{cm:ShortcutWebPage}"; Filename: "{#VCMIHome}"; Comment: "{cm:ShortcutWebPageComment}"; Tasks: startmenu Name: "{group}\{cm:ShortcutDiscord}"; Filename: "{#VCMIContact}"; Comment: "{cm:ShortcutDiscordComment}"; Tasks: startmenu -Name: "{code:GetUserDesktopFolder}\{cm:ShortcutLauncher}"; Filename: "{app}\VCMI_launcher.exe"; Comment: "{cm:ShortcutLauncherComment}"; Tasks: desktop +Name: "{code:GetUserDesktopFolder}\{cm:ShortcutLauncher}{code:GetBranchSuffix}"; Filename: "{app}\VCMI_launcher.exe"; Comment: "{cm:ShortcutLauncherComment}{code:GetBranchSuffix}"; Tasks: desktop [Tasks] @@ -371,6 +371,22 @@ begin end; +function GetBranchSuffix(Param: string): string; +var + Branch: string; +begin + Branch := UpperCase(ExpandConstant('{#VCMIFolder}')); + + if Pos('(BRANCH BETA)', Branch) > 0 then + Result := ' (Beta)' + else + if Pos('(BRANCH DEVELOP)', Branch) > 0 then + Result := ' (Develop)' + else + Result := ''; +end; + + function GetCommonProgramFilesDir: String; begin if IsARM64 then diff --git a/CMakePresets.json b/CMakePresets.json index c160e6ccc..489613626 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -462,19 +462,19 @@ "inherits": "default-release" }, { - "name": "windows-msvc-ninja-release", - "configurePreset": "windows-msvc-ninja-release", - "inherits": "default-release" + "name": "windows-msvc-ninja-release", + "configurePreset": "windows-msvc-ninja-release", + "inherits": "default-release" }, { - "name": "windows-msvc-ninja-release-x86", - "configurePreset": "windows-msvc-ninja-release-x86", - "inherits": "default-release" + "name": "windows-msvc-ninja-release-x86", + "configurePreset": "windows-msvc-ninja-release-x86", + "inherits": "default-release" }, { - "name": "windows-msvc-ninja-release-arm64", - "configurePreset": "windows-msvc-ninja-release-arm64", - "inherits": "default-release" + "name": "windows-msvc-ninja-release-arm64", + "configurePreset": "windows-msvc-ninja-release-arm64", + "inherits": "default-release" }, { "name": "ios-release-conan", @@ -518,9 +518,24 @@ } }, { - "name": "linux-clang-release", - "configurePreset": "linux-clang-release", - "inherits": "default-release" + "name": "linux-clang-debug", + "configurePreset": "linux-clang-debug", + "inherits": "default-release" + }, + { + "name": "linux-clang-release", + "configurePreset": "linux-clang-release", + "inherits": "default-release" + }, + { + "name": "linux-clang-test", + "configurePreset": "linux-clang-test", + "inherits": "default-release" + }, + { + "name": "linux-gcc-debug", + "configurePreset": "linux-gcc-debug", + "inherits": "default-release" }, { "name": "linux-gcc-release", @@ -528,25 +543,10 @@ "inherits": "default-release" }, { - "name": "linux-clang-debug", - "configurePreset": "linux-clang-test", - "inherits": "default-release" - }, - { - "name": "linux-gcc-debug", + "name": "linux-gcc-test", "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", diff --git a/Mods/vcmi/Content/config/english.json b/Mods/vcmi/Content/config/english.json index 28c1bec88..82fb3b677 100644 --- a/Mods/vcmi/Content/config/english.json +++ b/Mods/vcmi/Content/config/english.json @@ -138,6 +138,8 @@ "vcmi.lobby.deleteFile" : "Do you want to delete following file?", "vcmi.lobby.deleteFolder" : "Do you want to delete following folder?", "vcmi.lobby.deleteMode" : "Switch to delete mode and back", + "vcmi.lobby.templatesSelect.hover" : "Templates", + "vcmi.lobby.templatesSelect.help" : "Search and select template", "vcmi.broadcast.failedLoadGame" : "Failed to load game", "vcmi.broadcast.command" : "Use '!help' to list available commands", diff --git a/Mods/vcmi/Content/config/german.json b/Mods/vcmi/Content/config/german.json index a2a956ef5..ed0a74cf3 100644 --- a/Mods/vcmi/Content/config/german.json +++ b/Mods/vcmi/Content/config/german.json @@ -138,6 +138,8 @@ "vcmi.lobby.deleteFile" : "Möchtet Ihr folgende Datei löschen?", "vcmi.lobby.deleteFolder" : "Möchtet Ihr folgenden Ordner löschen?", "vcmi.lobby.deleteMode" : "In den Löschmodus wechseln und zurück", + "vcmi.lobby.templatesSelect.hover" : "Templates", + "vcmi.lobby.templatesSelect.help" : "Suche und wähle Template aus", "vcmi.broadcast.failedLoadGame" : "Spiel konnte nicht geladen werden", "vcmi.broadcast.command" : "Benutze '!help' um alle verfügbaren Befehle aufzulisten", diff --git a/client/ClientCommandManager.cpp b/client/ClientCommandManager.cpp index a28c55f63..e1c361a05 100644 --- a/client/ClientCommandManager.cpp +++ b/client/ClientCommandManager.cpp @@ -239,7 +239,7 @@ void ClientCommandManager::handleTranslateMapsCommand() try { // load and drop loaded map - we only need loader to run over all maps - loadedMaps.push_back(mapService.loadMap(mapName, nullptr)); + loadedMaps.push_back(mapService.loadMap(mapName, GAME->interface()->cb.get())); } catch(std::exception & e) { @@ -260,7 +260,7 @@ void ClientCommandManager::handleTranslateMapsCommand() { loadedCampaigns.push_back(CampaignHandler::getCampaign(campaignName.getName())); for (auto const & part : loadedCampaigns.back()->allScenarios()) - loadedCampaigns.back()->getMap(part, nullptr); + loadedCampaigns.back()->getMap(part, GAME->interface()->cb.get()); } catch(std::exception & e) { diff --git a/client/ClientNetPackVisitors.h b/client/ClientNetPackVisitors.h index fc7714b5f..1a2813a02 100644 --- a/client/ClientNetPackVisitors.h +++ b/client/ClientNetPackVisitors.h @@ -104,6 +104,7 @@ public: void visitSetAvailableArtifacts(SetAvailableArtifacts & pack) override; void visitEntitiesChanged(EntitiesChanged & pack) override; void visitPlayerCheated(PlayerCheated & pack) override; + void visitChangeTownName(ChangeTownName & pack) override; }; class ApplyFirstClientNetPackVisitor : public VCMI_LIB_WRAP_NAMESPACE(ICPackVisitor) diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index eb53538c8..8ab47529e 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -1074,3 +1074,16 @@ void ApplyClientNetPackVisitor::visitPlayerCheated(PlayerCheated & pack) if(pack.colorScheme != ColorScheme::KEEP && vstd::contains(cl.playerint, pack.player)) cl.playerint[pack.player]->setColorScheme(pack.colorScheme); } + +void ApplyClientNetPackVisitor::visitChangeTownName(ChangeTownName & pack) +{ + if(!adventureInt) + return; + + const CGTownInstance *town = gs.getTown(pack.tid); + if(town) + { + adventureInt->onTownChanged(town); + ENGINE->windows().totalRedraw(); + } +} diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 5ed1ff996..edf3c7728 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -1035,10 +1035,7 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con if(s->isControlledByAI() || GAME->server().isGuest()) labelPlayerName = std::make_shared(55, 10, EFonts::FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, name, 95); else - { - labelPlayerNameEdit = std::make_shared(Rect(6, 3, 95, 15), EFonts::FONT_SMALL, ETextAlignment::CENTER, false); - labelPlayerNameEdit->setText(name); - } + labelPlayerNameEdit = std::make_shared(Rect(6, 3, 95, 15), EFonts::FONT_SMALL, ETextAlignment::CENTER, name, false, [this](){ updateName(); }); labelWhoCanPlay = std::make_shared(Rect(6, 21, 45, 26), EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, LIBRARY->generaltexth->arraytxt[206 + whoCanPlay]); @@ -1114,28 +1111,6 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con bonus = std::make_shared(Point(271, 2), *s, BONUS); } -bool OptionsTab::PlayerOptionsEntry::captureThisKey(EShortcut key) -{ - return labelPlayerNameEdit && labelPlayerNameEdit->hasFocus() && key == EShortcut::GLOBAL_ACCEPT; -} - -void OptionsTab::PlayerOptionsEntry::keyPressed(EShortcut key) -{ - if(labelPlayerNameEdit && key == EShortcut::GLOBAL_ACCEPT) - updateName(); -} - -bool OptionsTab::PlayerOptionsEntry::receiveEvent(const Point & position, int eventType) const -{ - return eventType == AEventsReceiver::LCLICK; // capture all left clicks (not only within control) -} - -void OptionsTab::PlayerOptionsEntry::clickReleased(const Point & cursorPosition) -{ - if(labelPlayerNameEdit && !labelPlayerNameEdit->pos.isInside(cursorPosition)) - updateName(); -} - void OptionsTab::PlayerOptionsEntry::updateName() { if(labelPlayerNameEdit->getText() != name) { @@ -1146,8 +1121,6 @@ void OptionsTab::PlayerOptionsEntry::updateName() { set->String() = labelPlayerNameEdit->getText(); } } - - labelPlayerNameEdit->removeFocus(); name = labelPlayerNameEdit->getText(); } diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index e74666e6d..3acaffa22 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -28,6 +28,7 @@ class CTextBox; class CButton; class CSlider; class LRClickableArea; +class CTextInputWithConfirm; class FilledTexturePlayerColored; class TransparentFilledRectangle; @@ -196,7 +197,7 @@ private: std::unique_ptr pi; std::unique_ptr s; std::shared_ptr labelPlayerName; - std::shared_ptr labelPlayerNameEdit; + std::shared_ptr labelPlayerNameEdit; std::shared_ptr labelWhoCanPlay; std::shared_ptr background; std::shared_ptr buttonTownLeft; @@ -215,10 +216,6 @@ private: PlayerOptionsEntry(const PlayerSettings & S, const OptionsTab & parentTab); void hideUnavailableButtons(); - bool captureThisKey(EShortcut key) override; - void keyPressed(EShortcut key) override; - void clickReleased(const Point & cursorPosition) override; - bool receiveEvent(const Point & position, int eventType) const override; private: const OptionsTab & parentTab; diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index 4144bfcaa..8e9f2d45c 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/client/lobby/RandomMapTab.cpp @@ -43,7 +43,8 @@ #include "../../lib/serializer/JsonDeserializer.h" RandomMapTab::RandomMapTab(): - InterfaceObjectConfigurable() + InterfaceObjectConfigurable(), + templateIndex(0) { recActions = 0; mapGenOptions = std::make_shared(); @@ -140,16 +141,18 @@ RandomMapTab::RandomMapTab(): //set combo box callbacks if(auto w = widget("templateList")) { - w->onConstructItems = [](std::vector & curItems){ + auto getTemplates = [](){ auto templates = LIBRARY->tplh->getTemplates(); - boost::range::sort(templates, [](const CRmgTemplate * a, const CRmgTemplate * b){ return a->getName() < b->getName(); }); + return templates; + }; + w->onConstructItems = [getTemplates](std::vector & curItems){ curItems.push_back(nullptr); //default template - for(auto & t : templates) + for(auto & t : getTemplates()) curItems.push_back(t); }; @@ -164,6 +167,20 @@ RandomMapTab::RandomMapTab(): return readText(variables["randomTemplate"]); return std::string(""); }; + + w->addCallback([this, getTemplates]() // no real dropdown... - instead open dialog + { + std::vector texts; + texts.push_back(readText(variables["randomTemplate"])); + for(auto & t : getTemplates()) + texts.push_back(t->getName()); + + ENGINE->windows().popWindows(1); + ENGINE->windows().createAndPushWindow(texts, nullptr, LIBRARY->generaltexth->translate("vcmi.lobby.templatesSelect.hover"), LIBRARY->generaltexth->translate("vcmi.lobby.templatesSelect.help"), [this](int index){ + widget("templateList")->setItem(index); + templateIndex = index; + }, templateIndex, std::vector>(), true); + }); } loadOptions(); diff --git a/client/lobby/RandomMapTab.h b/client/lobby/RandomMapTab.h index fa62e151d..4e199e410 100644 --- a/client/lobby/RandomMapTab.h +++ b/client/lobby/RandomMapTab.h @@ -55,6 +55,8 @@ private: std::set playerTeamsAllowed; std::set compCountAllowed; std::set compTeamsAllowed; + + int templateIndex; }; class TeamAlignmentsWidget: public InterfaceObjectConfigurable diff --git a/client/mapView/IMapRendererContext.h b/client/mapView/IMapRendererContext.h index c1ff9bfc6..52ee6af51 100644 --- a/client/mapView/IMapRendererContext.h +++ b/client/mapView/IMapRendererContext.h @@ -99,6 +99,7 @@ public: virtual bool showGrid() const = 0; virtual bool showVisitable() const = 0; virtual bool showBlocked() const = 0; + virtual bool showInvisible() const = 0; /// if true, spell range for teleport / scuttle boat will be visible virtual bool showSpellRange(const int3 & position) const = 0; diff --git a/client/mapView/MapRenderer.cpp b/client/mapView/MapRenderer.cpp index cddaadec7..bcbd7cf5d 100644 --- a/client/mapView/MapRenderer.cpp +++ b/client/mapView/MapRenderer.cpp @@ -14,6 +14,10 @@ #include "IMapRendererContext.h" #include "mapHandler.h" +#include "../CPlayerInterface.h" +#include "../CServerHandler.h" +#include "../GameInstance.h" +#include "../Client.h" #include "../GameEngine.h" #include "../render/CAnimation.h" #include "../render/Canvas.h" @@ -23,12 +27,15 @@ #include "../render/Graphics.h" #include "../../lib/CConfigHandler.h" +#include "../../lib/callback/CCallback.h" +#include "../../lib/gameState/CGameState.h" #include "../../lib/RiverHandler.h" #include "../../lib/RoadHandler.h" #include "../../lib/TerrainHandler.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/MiscObjects.h" #include "../../lib/mapObjects/ObjectTemplate.h" +#include "../../lib/mapping/CMap.h" #include "../../lib/mapping/TerrainTile.h" #include "../../lib/pathfinder/CGPathNode.h" @@ -592,8 +599,10 @@ MapRendererOverlay::MapRendererOverlay() , imageBlocked(ENGINE->renderHandler().loadImage(ImagePath::builtin("debug/blocked"), EImageBlitMode::COLORKEY)) , imageVisitable(ENGINE->renderHandler().loadImage(ImagePath::builtin("debug/visitable"), EImageBlitMode::COLORKEY)) , imageSpellRange(ENGINE->renderHandler().loadImage(ImagePath::builtin("debug/spellRange"), EImageBlitMode::COLORKEY)) + , imageEvent(ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("AVZevnt0"), EImageBlitMode::COLORKEY)->getImage(0)) + , imageGrail(ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("AVZgrail"), EImageBlitMode::COLORKEY)->getImage(0)) + , grailPos(GAME->server().client->gameState().getMap().grailPos) { - } void MapRendererOverlay::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) @@ -601,7 +610,7 @@ void MapRendererOverlay::renderTile(IMapRendererContext & context, Canvas & targ if(context.showGrid()) target.draw(imageGrid, Point(0,0)); - if(context.showVisitable() || context.showBlocked()) + if(GAME->interface()->cb->getStartInfo()->extraOptionsInfo.cheatsAllowed && (context.showVisitable() || context.showBlocked() || context.showInvisible())) { bool blocking = false; bool visitable = false; @@ -610,6 +619,12 @@ void MapRendererOverlay::renderTile(IMapRendererContext & context, Canvas & targ { const auto * object = context.getObject(objectID); + if(object->ID == Obj::EVENT && context.showInvisible()) + target.draw(imageEvent, Point(0,0)); + + if(grailPos == coordinates && context.showInvisible()) + target.draw(imageGrail, Point(0,0)); + if(context.objectTransparency(objectID, coordinates) > 0 && !context.isActiveHero(object)) { visitable |= object->visitableAt(coordinates); @@ -643,6 +658,9 @@ uint8_t MapRendererOverlay::checksum(IMapRendererContext & context, const int3 & if (context.showSpellRange(coordinates)) result += 8; + if (context.showInvisible()) + result += 16; + return result; } diff --git a/client/mapView/MapRenderer.h b/client/mapView/MapRenderer.h index e72160a1c..880524c48 100644 --- a/client/mapView/MapRenderer.h +++ b/client/mapView/MapRenderer.h @@ -9,11 +9,11 @@ */ #pragma once +#include "../../lib/int3.h" #include "../../lib/filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN -class int3; class ObjectInstanceID; class CGObjectInstance; @@ -139,6 +139,10 @@ class MapRendererOverlay std::shared_ptr imageVisitable; std::shared_ptr imageBlocked; std::shared_ptr imageSpellRange; + std::shared_ptr imageEvent; + std::shared_ptr imageGrail; + + int3 grailPos; public: MapRendererOverlay(); diff --git a/client/mapView/MapRendererContext.cpp b/client/mapView/MapRendererContext.cpp index 5da7cd5c6..7c248a22d 100644 --- a/client/mapView/MapRendererContext.cpp +++ b/client/mapView/MapRendererContext.cpp @@ -232,6 +232,11 @@ bool MapRendererBaseContext::showBlocked() const return false; } +bool MapRendererBaseContext::showInvisible() const +{ + return false; +} + bool MapRendererBaseContext::showSpellRange(const int3 & position) const { return false; @@ -362,6 +367,11 @@ bool MapRendererAdventureContext::showBlocked() const return settingShowBlocked; } +bool MapRendererAdventureContext::showInvisible() const +{ + return settingShowInvisible; +} + bool MapRendererAdventureContext::showTextOverlay() const { return settingTextOverlay; diff --git a/client/mapView/MapRendererContext.h b/client/mapView/MapRendererContext.h index ece349d22..f9054ad42 100644 --- a/client/mapView/MapRendererContext.h +++ b/client/mapView/MapRendererContext.h @@ -62,6 +62,7 @@ public: bool showGrid() const override; bool showVisitable() const override; bool showBlocked() const override; + bool showInvisible() const override; bool showSpellRange(const int3 & position) const override; }; @@ -72,6 +73,7 @@ public: bool settingShowGrid = false; bool settingShowVisitable = false; bool settingShowBlocked = false; + bool settingShowInvisible = false; bool settingTextOverlay = false; bool settingsAdventureObjectAnimation = true; bool settingsAdventureTerrainAnimation = true; @@ -88,6 +90,7 @@ public: bool showGrid() const override; bool showVisitable() const override; bool showBlocked() const override; + bool showInvisible() const override; bool showTextOverlay() const override; bool showSpellRange(const int3 & position) const override; diff --git a/client/mapView/MapViewController.cpp b/client/mapView/MapViewController.cpp index f52ca4e46..468992038 100644 --- a/client/mapView/MapViewController.cpp +++ b/client/mapView/MapViewController.cpp @@ -233,6 +233,7 @@ void MapViewController::updateState() adventureContext->settingShowGrid = settings["gameTweaks"]["showGrid"].Bool(); adventureContext->settingShowVisitable = settings["session"]["showVisitable"].Bool(); adventureContext->settingShowBlocked = settings["session"]["showBlocked"].Bool(); + adventureContext->settingShowInvisible = settings["session"]["showInvisible"].Bool(); adventureContext->settingTextOverlay = (ENGINE->isKeyboardAltDown() || ENGINE->input().getNumTouchFingers() == 2) && settings["general"]["enableOverlay"].Bool(); } } diff --git a/client/netlag/PackRollbackGeneratorVisitor.h b/client/netlag/PackRollbackGeneratorVisitor.h index affb415f2..a26877591 100644 --- a/client/netlag/PackRollbackGeneratorVisitor.h +++ b/client/netlag/PackRollbackGeneratorVisitor.h @@ -85,6 +85,7 @@ private: //void visitSetCommanderProperty(SetCommanderProperty & pack) override; //void visitAddQuest(AddQuest & pack) override; //void visitChangeFormation(ChangeFormation & pack) override; + //void visitChangeTownName(ChangeTownName & pack) override; //void visitChangeSpells(ChangeSpells & pack) override; //void visitSetAvailableHero(SetAvailableHero & pack) override; //void visitChangeObjectVisitors(ChangeObjectVisitors & pack) override; diff --git a/client/widgets/CTextInput.cpp b/client/widgets/CTextInput.cpp index 4d1a8d729..70ab3c343 100644 --- a/client/widgets/CTextInput.cpp +++ b/client/widgets/CTextInput.cpp @@ -27,6 +27,85 @@ std::list CFocusable::focusables; CFocusable * CFocusable::inputWithFocus; +CTextInputWithConfirm::CTextInputWithConfirm(const Rect & Pos, EFonts font, ETextAlignment alignment, std::string text, bool limitToRect, std::function confirmCallback) + : CTextInput(Pos, font, alignment, false), confirmCb(confirmCallback), limitToRect(limitToRect), initialText(text) +{ + setText(text); +} + +bool CTextInputWithConfirm::captureThisKey(EShortcut key) +{ + return hasFocus() && (key == EShortcut::GLOBAL_ACCEPT || key == EShortcut::GLOBAL_CANCEL || key == EShortcut::GLOBAL_BACKSPACE); +} + +void CTextInputWithConfirm::keyPressed(EShortcut key) +{ + if(!hasFocus()) + return; + + if(key == EShortcut::GLOBAL_ACCEPT) + confirm(); + else if(key == EShortcut::GLOBAL_CANCEL) + { + setText(initialText); + removeFocus(); + } + + CTextInput::keyPressed(key); +} + +bool CTextInputWithConfirm::receiveEvent(const Point & position, int eventType) const +{ + return eventType == AEventsReceiver::LCLICK; // capture all left clicks (not only within control) +} + +void CTextInputWithConfirm::clickReleased(const Point & cursorPosition) +{ + if(!pos.isInside(cursorPosition)) // clicked outside + confirm(); +} + +void CTextInputWithConfirm::clickPressed(const Point & cursorPosition) +{ + if(pos.isInside(cursorPosition)) // clickPressed should respect control area (receiveEvent also affects this) + CTextInput::clickPressed(cursorPosition); +} + +void CTextInputWithConfirm::onFocusGot() +{ + initialText = getText(); + + CTextInput::onFocusGot(); +} + +void CTextInputWithConfirm::textInputted(const std::string & enteredText) +{ + if(!hasFocus()) + return; + + std::string visibleText = getVisibleText() + enteredText; + const auto & font = ENGINE->renderHandler().loadFont(label->font); + if(!limitToRect || (font->getStringWidth(visibleText) - CLabel::getDelimitersWidth(label->font, visibleText)) < pos.w) + CTextInput::textInputted(enteredText); +} + +void CTextInputWithConfirm::deactivate() +{ + removeUsedEvents(LCLICK); + + CTextInput::deactivate(); +} + +void CTextInputWithConfirm::confirm() +{ + if(getText().empty()) + setText(initialText); + + if(confirmCb && initialText != getText()) + confirmCb(); + removeFocus(); +} + CTextInput::CTextInput(const Rect & Pos) :originalAlignment(ETextAlignment::CENTERLEFT) { @@ -196,7 +275,7 @@ void CTextInput::updateLabel() label->alignment = originalAlignment; const auto & font = ENGINE->renderHandler().loadFont(label->font); - while (font->getStringWidth(visibleText) > pos.w) + while ((font->getStringWidth(visibleText) - CLabel::getDelimitersWidth(label->font, visibleText)) > pos.w) { label->alignment = ETextAlignment::CENTERRIGHT; visibleText = visibleText.substr(TextOperations::getUnicodeCharacterSize(visibleText[0])); diff --git a/client/widgets/CTextInput.h b/client/widgets/CTextInput.h index 67639d098..9edbdde3d 100644 --- a/client/widgets/CTextInput.h +++ b/client/widgets/CTextInput.h @@ -45,8 +45,9 @@ public: }; /// Text input box where players can enter text -class CTextInput final : public CFocusable +class CTextInput : public CFocusable { +protected: using TextEditedCallback = std::function; using TextFilterCallback = std::function; @@ -71,12 +72,12 @@ class CTextInput final : public CFocusable void createLabel(bool giveFocusToInput); void updateLabel(); - void clickPressed(const Point & cursorPosition) final; - void textInputted(const std::string & enteredText) final; - void textEdited(const std::string & enteredText) final; - void onFocusGot() final; - void onFocusLost() final; - void showPopupWindow(const Point & cursorPosition) final; + void clickPressed(const Point & cursorPosition) override; + void textInputted(const std::string & enteredText) override; + void textEdited(const std::string & enteredText) override; + void onFocusGot() override; + void onFocusLost() override; + void showPopupWindow(const Point & cursorPosition) override; CTextInput(const Rect & Pos); public: @@ -105,7 +106,27 @@ public: void setAlignment(ETextAlignment alignment); // CIntObject interface impl - void keyPressed(EShortcut key) final; - void activate() final; - void deactivate() final; + void keyPressed(EShortcut key) override; + void activate() override; + void deactivate() override; +}; + +class CTextInputWithConfirm final : public CTextInput +{ + std::string initialText; + std::function confirmCb; + bool limitToRect; + + void confirm(); +public: + CTextInputWithConfirm(const Rect & Pos, EFonts font, ETextAlignment alignment, std::string text, bool limitToRect, std::function confirmCallback); + + bool captureThisKey(EShortcut key) override; + void keyPressed(EShortcut key) override; + void clickReleased(const Point & cursorPosition) override; + void clickPressed(const Point & cursorPosition) override; + bool receiveEvent(const Point & position, int eventType) const override; + void onFocusGot() override; + void textInputted(const std::string & enteredText) override; + void deactivate() override; }; diff --git a/client/widgets/TextControls.cpp b/client/widgets/TextControls.cpp index 2cb8a4442..245e02a1c 100644 --- a/client/widgets/TextControls.cpp +++ b/client/widgets/TextControls.cpp @@ -182,29 +182,39 @@ std::vector CMultiLineLabel::getLines() return lines; } -void CTextContainer::blitLine(Canvas & to, Rect destRect, std::string what) +const std::string delimiters = "{}"; + +int CTextContainer::getDelimitersWidth(EFonts font, std::string text) { const auto f = ENGINE->renderHandler().loadFont(font); - Point where = destRect.topLeft(); - const std::string delimiters = "{}"; - auto delimitersCount = std::count_if(what.cbegin(), what.cend(), [&delimiters](char c) + auto delimitersWidth = std::count_if(text.cbegin(), text.cend(), [](char c) { return delimiters.find(c) != std::string::npos; }); //We should count delimiters length from string to correct centering later. - delimitersCount *= f->getStringWidth(delimiters)/2; + delimitersWidth *= f->getStringWidth(delimiters)/2; std::smatch match; std::regex expr("\\{(.*?)\\|"); - std::string::const_iterator searchStart( what.cbegin() ); - while(std::regex_search(searchStart, what.cend(), match, expr)) + std::string::const_iterator searchStart( text.cbegin() ); + while(std::regex_search(searchStart, text.cend(), match, expr)) { std::string colorText = match[1].str(); if(auto c = Colors::parseColor(colorText)) - delimitersCount += f->getStringWidth(colorText + "|"); + delimitersWidth += f->getStringWidth(colorText + "|"); searchStart = match.suffix().first; } + return delimitersWidth; +} + +void CTextContainer::blitLine(Canvas & to, Rect destRect, std::string what) +{ + const auto f = ENGINE->renderHandler().loadFont(font); + Point where = destRect.topLeft(); + + int delimitersWidth = getDelimitersWidth(font, what); + // input is rect in which given text should be placed // calculate proper position for top-left corner of the text @@ -212,10 +222,10 @@ void CTextContainer::blitLine(Canvas & to, Rect destRect, std::string what) where.x += getBorderSize().x; if(alignment == ETextAlignment::CENTER || alignment == ETextAlignment::TOPCENTER || alignment == ETextAlignment::BOTTOMCENTER) - where.x += (destRect.w - (static_cast(f->getStringWidth(what)) - delimitersCount)) / 2; + where.x += (destRect.w - (static_cast(f->getStringWidth(what)) - delimitersWidth)) / 2; if(alignment == ETextAlignment::TOPRIGHT || alignment == ETextAlignment::BOTTOMRIGHT || alignment == ETextAlignment::CENTERRIGHT) - where.x += getBorderSize().x + destRect.w - (static_cast(f->getStringWidth(what)) - delimitersCount); + where.x += getBorderSize().x + destRect.w - (static_cast(f->getStringWidth(what)) - delimitersWidth); if(alignment == ETextAlignment::TOPLEFT || alignment == ETextAlignment::TOPCENTER || alignment == ETextAlignment::TOPRIGHT) where.y += getBorderSize().y; diff --git a/client/widgets/TextControls.h b/client/widgets/TextControls.h index 0c99c193b..b190279c2 100644 --- a/client/widgets/TextControls.h +++ b/client/widgets/TextControls.h @@ -31,6 +31,8 @@ protected: CTextContainer(ETextAlignment alignment, EFonts font, ColorRGBA color); public: + static int getDelimitersWidth(EFonts font, std::string text); + ETextAlignment alignment; EFonts font; ColorRGBA color; // default font color. Can be overridden by placing "{}" into the string diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index 9317d1c12..542085d95 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -30,6 +30,7 @@ #include "../widgets/MiscWidgets.h" #include "../widgets/CComponent.h" #include "../widgets/CGarrisonInt.h" +#include "../widgets/CTextInput.h" #include "../widgets/Buttons.h" #include "../widgets/TextControls.h" #include "../widgets/RadialMenu.h" @@ -1435,7 +1436,15 @@ CCastleInterface::CCastleInterface(const CGTownInstance * Town, const CGTownInst garr->setRedrawParent(true); heroes = std::make_shared(town, Point(241, 387), Point(241, 483), garr, true); - title = std::make_shared(85, 387, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, town->getNameTranslated()); + title = std::make_shared(Rect(83, 386, 140, 20), FONT_MEDIUM, ETextAlignment::TOPLEFT, town->getNameTranslated(), true, [this](){ + std::string name = title->getText(); + std::string originalName = LIBRARY->generaltexth->translate(town->getNameTextID()); + if(name == originalName) + name = ""; // use textID again + GAME->interface()->cb->setTownName(town, name); + }); + if(town->tempOwner != GAME->interface()->playerID) // disable changing for allied towns + title->deactivate(); income = std::make_shared(195, 443, FONT_SMALL, ETextAlignment::CENTER); icon = std::make_shared(AnimationPath::builtin("ITPT"), 0, 0, 15, 387); diff --git a/client/windows/CCastleInterface.h b/client/windows/CCastleInterface.h index 46a546b5e..3ff010091 100644 --- a/client/windows/CCastleInterface.h +++ b/client/windows/CCastleInterface.h @@ -37,6 +37,7 @@ class CGarrisonInt; class CComponent; class CComponentBox; class LRClickableArea; +class CTextInputWithConfirm; /// Building "button" class CBuildingRect : public CShowableAnim @@ -225,7 +226,7 @@ public: /// Class which manages the castle window class CCastleInterface final : public CStatusbarWindow, public IGarrisonHolder, public IArtifactsHolder { - std::shared_ptr title; + std::shared_ptr title; std::shared_ptr income; std::shared_ptr icon; diff --git a/client/windows/CWindowObject.cpp b/client/windows/CWindowObject.cpp index 9c0c72c6b..e0665ed28 100644 --- a/client/windows/CWindowObject.cpp +++ b/client/windows/CWindowObject.cpp @@ -86,7 +86,7 @@ std::shared_ptr CWindowObject::createBg(const ImagePath & imageName, b return nullptr; auto image = std::make_shared(imageName, Point(0,0), EImageBlitMode::OPAQUE); - if(playerColored) + if(playerColored && GAME->interface()) image->setPlayerColor(GAME->interface()->playerID); return image; } diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 1d0f4de49..6c6275b0e 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -1596,7 +1596,7 @@ CObjectListWindow::CObjectListWindow(const std::vector & _items, std::share for(int id : _items) { std::string objectName = GAME->interface()->cb->getObjInstance(ObjectInstanceID(id))->getObjectName(); - trimTextIfTooWide(objectName); + trimTextIfTooWide(objectName, false); items.emplace_back(id, objectName); } itemsVisible = items; @@ -1620,7 +1620,7 @@ CObjectListWindow::CObjectListWindow(const std::vector & _items, st for(size_t i = 0; i < _items.size(); i++) { std::string objectName = _items[i]; - trimTextIfTooWide(objectName); + trimTextIfTooWide(objectName, true); items.emplace_back(static_cast(i), objectName); } itemsVisible = items; @@ -1664,16 +1664,24 @@ void CObjectListWindow::init(std::shared_ptr titleWidget_, std::stri searchBox->setCallback(std::bind(&CObjectListWindow::itemsSearchCallback, this, std::placeholders::_1)); } -void CObjectListWindow::trimTextIfTooWide(std::string & text) const +void CObjectListWindow::trimTextIfTooWide(std::string & text, bool preserveCountSuffix) const { + std::string suffix = "..."; int maxWidth = pos.w - 60; // 60 px for scrollbar and borders - auto posBrace = text.find('('); - auto posClosing = text.find(')'); - std::string objCount = text.substr(posBrace, posClosing - posBrace) + ')'; + if(text[0] == '{') - objCount = '}' + objCount; + suffix += "}"; + + if (preserveCountSuffix) + { + auto posBrace = text.find_last_of("("); + auto posClosing = text.find_last_of(")"); + std::string objCount = text.substr(posBrace, posClosing - posBrace) + ')'; + suffix += " "; + suffix += objCount; + } + const auto & font = ENGINE->renderHandler().loadFont(FONT_SMALL); - std::string suffix = "... " + objCount; if(font->getStringWidth(text) >= maxWidth) { diff --git a/client/windows/GUIClasses.h b/client/windows/GUIClasses.h index 6eae917e0..6a59a8c1b 100644 --- a/client/windows/GUIClasses.h +++ b/client/windows/GUIClasses.h @@ -206,7 +206,7 @@ class CObjectListWindow : public CWindowObject std::vector< std::pair > itemsVisible; //visible items present in list void init(std::shared_ptr titleWidget_, std::string _title, std::string _descr, bool searchBoxEnabled); - void trimTextIfTooWide(std::string & text) const; // trim item's text to fit within window's width + void trimTextIfTooWide(std::string & text, bool preserveCountSuffix) const; // trim item's text to fit within window's width void itemsSearchCallback(const std::string & text); void exitPressed(); public: diff --git a/docs/modders/Entities_Format/Hero_Type_Format.md b/docs/modders/Entities_Format/Hero_Type_Format.md index c8dc305f9..e96dd93b6 100644 --- a/docs/modders/Entities_Format/Hero_Type_Format.md +++ b/docs/modders/Entities_Format/Hero_Type_Format.md @@ -130,10 +130,21 @@ In order to make functional hero you also need: "anotherOne" : {Bonus Format} }, // Shortcut for defining creature specialty, using standard H3 rules + // Can be combined with bonuses-based specialty if desired "creature" : "griffin", // Shortcut for defining specialty in secondary skill, using standard H3 rules - "secondary" : "offence" + // Can be combined with bonuses-based specialty if desired + "secondary" : "offence", + + // Optional, only applicable to creature specialties + // Overrides creature level to specific value for purposes of computing growth of h3-like creature specialty + "creatureLevel" : 5 + + // Optional, only applicable to creature and secondary skill specialties + // Overrides default (5% for vanilla H3 specialties) growth of specialties per level to a specified value + // Default value can be modified globally using specialtySecondarySkillGrowth and specialtyCreatureGrowth game settings + "stepSize" : 5 } } ``` diff --git a/docs/players/Cheat_Codes.md b/docs/players/Cheat_Codes.md index ba65eae71..42dd2887b 100644 --- a/docs/players/Cheat_Codes.md +++ b/docs/players/Cheat_Codes.md @@ -162,16 +162,19 @@ Below a list of supported commands, with their arguments wrapped in `<>` #### Settings -- `set ` - sets special temporary settings that reset on game quit. Below some of the most notable commands: -- `autoskip` - identical to `autoskip` option -- `onlyAI` - run without human player, all players will be *default AI* -- `headless` - run without GUI, implies `onlyAI` is set -- `showGrid` - display a square grid overlay on top of adventure map -- `showBlocked` - show blocked tiles on map -- `showVisitable` - show visitable tiles on map -- `hideSystemMessages` - suppress server messages in chat +- `set ` - sets special temporary settings that reset on game quit. Below some of the most notable commands: +- `autoskip` - identical to `autoskip` option +- `onlyAI` - run without human player, all players will be *default AI* +- `headless` - run without GUI, implies `onlyAI` is set +- `showGrid` - display a square grid overlay on top of adventure map +- `showBlocked` - show blocked tiles on map +- `showVisitable` - show visitable tiles on map +- `showInvisible` - show invisible tiles (events, grail) on map +- `hideSystemMessages` - suppress server messages in chat - `antilag` - toggles network lag compensation in multiplayer on or off +`showBlocked`, `showVisitable` and `showInvisible` only works if cheats are enabled. + #### Developer Commands - `crash` - force a game crash. It is sometimes useful to generate memory dump file in certain situations, for example game freeze diff --git a/lib/callback/CCallback.cpp b/lib/callback/CCallback.cpp index a826d2950..f94c8cdf1 100644 --- a/lib/callback/CCallback.cpp +++ b/lib/callback/CCallback.cpp @@ -286,6 +286,12 @@ void CCallback::setFormation(const CGHeroInstance * hero, EArmyFormation mode) sendRequest(pack); } +void CCallback::setTownName(const CGTownInstance * town, std::string & name) +{ + SetTownName pack(town->id, name); + sendRequest(pack); +} + void CCallback::recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero, const HeroTypeID & nextHero) { assert(townOrTavern); diff --git a/lib/callback/CCallback.h b/lib/callback/CCallback.h index cf8484bb9..7eb6a42c0 100644 --- a/lib/callback/CCallback.h +++ b/lib/callback/CCallback.h @@ -76,6 +76,7 @@ public: void trade(const ObjectInstanceID marketId, EMarketMode mode, TradeItemSell id1, TradeItemBuy id2, ui32 val1, const CGHeroInstance * hero = nullptr) override; void trade(const ObjectInstanceID marketId, EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero = nullptr) override; void setFormation(const CGHeroInstance * hero, EArmyFormation mode) override; + void setTownName(const CGTownInstance * town, std::string & name) override; void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero, const HeroTypeID & nextHero=HeroTypeID::NONE) override; void save(const std::string &fname) override; void sendMessage(const std::string &mess, const CGObjectInstance * currentObject = nullptr) override; diff --git a/lib/callback/IGameActionCallback.h b/lib/callback/IGameActionCallback.h index 946eee67d..8f5a9bcd9 100644 --- a/lib/callback/IGameActionCallback.h +++ b/lib/callback/IGameActionCallback.h @@ -70,6 +70,7 @@ public: virtual void endTurn()=0; virtual void buyArtifact(const CGHeroInstance *hero, ArtifactID aid)=0; //used to buy artifacts in towns (including spell book in the guild and war machines in blacksmith) virtual void setFormation(const CGHeroInstance * hero, EArmyFormation mode)=0; + virtual void setTownName(const CGTownInstance * town, std::string & name)=0; virtual void save(const std::string &fname) = 0; virtual void sendMessage(const std::string &mess, const CGObjectInstance * currentObject = nullptr) = 0; diff --git a/lib/gameState/GameStatePackVisitor.cpp b/lib/gameState/GameStatePackVisitor.cpp index d527d7555..4fc72af4c 100644 --- a/lib/gameState/GameStatePackVisitor.cpp +++ b/lib/gameState/GameStatePackVisitor.cpp @@ -123,6 +123,11 @@ void GameStatePackVisitor::visitChangeFormation(ChangeFormation & pack) gs.getHero(pack.hid)->setFormation(pack.formation); } +void GameStatePackVisitor::visitChangeTownName(ChangeTownName & pack) +{ + gs.getTown(pack.tid)->setCustomName(pack.name); +} + void GameStatePackVisitor::visitHeroVisitCastle(HeroVisitCastle & pack) { CGHeroInstance *h = gs.getHero(pack.hid); diff --git a/lib/gameState/GameStatePackVisitor.h b/lib/gameState/GameStatePackVisitor.h index fa4421e4e..8cb0642a7 100644 --- a/lib/gameState/GameStatePackVisitor.h +++ b/lib/gameState/GameStatePackVisitor.h @@ -87,6 +87,7 @@ public: void visitSetCommanderProperty(SetCommanderProperty & pack) override; void visitAddQuest(AddQuest & pack) override; void visitChangeFormation(ChangeFormation & pack) override; + void visitChangeTownName(ChangeTownName & pack) override; void visitChangeSpells(ChangeSpells & pack) override; void visitSetAvailableHero(SetAvailableHero & pack) override; void visitChangeObjectVisitors(ChangeObjectVisitors & pack) override; diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index f07daa261..085674e74 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -860,7 +860,7 @@ CBonusSystemNode & CGTownInstance::whatShouldBeAttached() std::string CGTownInstance::getNameTranslated() const { - return LIBRARY->generaltexth->translate(nameTextId); + return customName.empty() ? LIBRARY->generaltexth->translate(nameTextId) : customName; } std::string CGTownInstance::getNameTextID() const @@ -873,6 +873,11 @@ void CGTownInstance::setNameTextId( const std::string & newName ) nameTextId = newName; } +void CGTownInstance::setCustomName( const std::string & newName ) +{ + customName = newName; +} + const CArmedInstance * CGTownInstance::getUpperArmy() const { if(getGarrisonHero()) diff --git a/lib/mapObjects/CGTownInstance.h b/lib/mapObjects/CGTownInstance.h index 132a5f358..4b1c9811b 100644 --- a/lib/mapObjects/CGTownInstance.h +++ b/lib/mapObjects/CGTownInstance.h @@ -46,6 +46,7 @@ class DLL_LINKAGE CGTownInstance : public CGDwelling, public IShipyard, public I { friend class CTownInstanceConstructor; std::string nameTextId; // name of town + std::string customName; std::map convertOldBuildings(std::vector oldVector); std::set builtBuildings; @@ -75,6 +76,8 @@ public: { h & static_cast(*this); h & nameTextId; + if (h.version >= Handler::Version::CUSTOM_NAMES) + h & customName; h & built; h & destroyed; h & identifier; @@ -128,6 +131,7 @@ public: std::string getNameTranslated() const; std::string getNameTextID() const; void setNameTextId(const std::string & newName); + void setCustomName(const std::string & newName); ////////////////////////////////////////////////////////////////////////// diff --git a/lib/mapObjects/ObjectTemplate.cpp b/lib/mapObjects/ObjectTemplate.cpp index a14481420..bf15d1ff3 100644 --- a/lib/mapObjects/ObjectTemplate.cpp +++ b/lib/mapObjects/ObjectTemplate.cpp @@ -66,7 +66,7 @@ void ObjectTemplate::afterLoadFixup() if(id == Obj::EVENT) { setSize(1,1); - usedTiles[0][0] = VISITABLE; + usedTiles[0][0] = VISITABLE | VISIBLE; visitDir = 0xFF; } } diff --git a/lib/mapping/CMapOperation.cpp b/lib/mapping/CMapOperation.cpp index 92d985bd6..2cf46f200 100644 --- a/lib/mapping/CMapOperation.cpp +++ b/lib/mapping/CMapOperation.cpp @@ -311,7 +311,7 @@ void CDrawTerrainOperation::updateTerrainViews() if(!pattern.diffImages) { tile.terView = gen->nextInt(mapping.first, mapping.second); - tile.extTileFlags = valRslt.flip; + tile.extTileFlags = (tile.extTileFlags & 0b11111100) | valRslt.flip; } else { diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 29940945f..79d1e427e 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1100,6 +1100,8 @@ void CMapLoaderH3M::readBoxContent(CGPandoraBox * object, const int3 & mapPositi SpellID scrollSpell = reader->readSpell16(); if (grantedArtifact == ArtifactID::SPELL_SCROLL) reward.grantedScrolls.push_back(scrollSpell); + else + reward.grantedArtifacts.push_back(grantedArtifact); } else reward.grantedArtifacts.push_back(grantedArtifact); @@ -2306,6 +2308,8 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, con SpellID scrollSpell = reader->readSpell16(); if (grantedArtifact == ArtifactID::SPELL_SCROLL) reward.grantedScrolls.push_back(scrollSpell); + else + reward.grantedArtifacts.push_back(grantedArtifact); } else reward.grantedArtifacts.push_back(grantedArtifact); @@ -2381,6 +2385,8 @@ EQuestMission CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & positi SpellID scrollSpell = reader->readSpell16(); if (requiredArtifact == ArtifactID::SPELL_SCROLL) guard->getQuest().mission.scrolls.push_back(scrollSpell); + else + guard->getQuest().mission.artifacts.push_back(requiredArtifact); } else guard->getQuest().mission.artifacts.push_back(requiredArtifact); diff --git a/lib/networkPacks/NetPackVisitor.h b/lib/networkPacks/NetPackVisitor.h index eef61f439..d1221172f 100644 --- a/lib/networkPacks/NetPackVisitor.h +++ b/lib/networkPacks/NetPackVisitor.h @@ -58,6 +58,7 @@ public: virtual void visitSetCommanderProperty(SetCommanderProperty & pack) {} virtual void visitAddQuest(AddQuest & pack) {} virtual void visitChangeFormation(ChangeFormation & pack) {} + virtual void visitChangeTownName(ChangeTownName & pack) {} virtual void visitRemoveObject(RemoveObject & pack) {} virtual void visitTryMoveHero(TryMoveHero & pack) {} virtual void visitNewStructures(NewStructures & pack) {} @@ -144,6 +145,7 @@ public: virtual void visitBuyArtifact(BuyArtifact & pack) {} virtual void visitTradeOnMarketplace(TradeOnMarketplace & pack) {} virtual void visitSetFormation(SetFormation & pack) {} + virtual void visitSetTownName(SetTownName & pack) {} virtual void visitHireHero(HireHero & pack) {} virtual void visitBuildBoat(BuildBoat & pack) {} virtual void visitQueryReply(QueryReply & pack) {} diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index 5d7c08477..c7df14a35 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -213,6 +213,11 @@ void ChangeFormation::visitTyped(ICPackVisitor & visitor) visitor.visitChangeFormation(*this); } +void ChangeTownName::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitChangeTownName(*this); +} + void RemoveObject::visitTyped(ICPackVisitor & visitor) { visitor.visitRemoveObject(*this); @@ -643,6 +648,11 @@ void SetFormation::visitTyped(ICPackVisitor & visitor) visitor.visitSetFormation(*this); } +void SetTownName::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetTownName(*this); +} + void HireHero::visitTyped(ICPackVisitor & visitor) { visitor.visitHireHero(*this); diff --git a/lib/networkPacks/PacksForClient.h b/lib/networkPacks/PacksForClient.h index d2d0740fa..ae059867d 100644 --- a/lib/networkPacks/PacksForClient.h +++ b/lib/networkPacks/PacksForClient.h @@ -614,6 +614,20 @@ struct DLL_LINKAGE ChangeFormation : public CPackForClient } }; +struct DLL_LINKAGE ChangeTownName : public CPackForClient +{ + ObjectInstanceID tid; + std::string name; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h) + { + h & tid; + h & name; + } +}; + struct DLL_LINKAGE RemoveObject : public CPackForClient { RemoveObject() = default; diff --git a/lib/networkPacks/PacksForServer.h b/lib/networkPacks/PacksForServer.h index 9924d3b4b..f28d64c5f 100644 --- a/lib/networkPacks/PacksForServer.h +++ b/lib/networkPacks/PacksForServer.h @@ -609,6 +609,28 @@ struct DLL_LINKAGE SetFormation : public CPackForServer } }; +struct DLL_LINKAGE SetTownName : public CPackForServer +{ + SetTownName() = default; + ; + SetTownName(const ObjectInstanceID & TID, std::string Name) + : tid(TID) + , name(Name) + { + } + ObjectInstanceID tid; + std::string name; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h) + { + h & static_cast(*this); + h & tid; + h & name; + } +}; + struct DLL_LINKAGE HireHero : public CPackForServer { HireHero() = default; diff --git a/lib/serializer/ESerializationVersion.h b/lib/serializer/ESerializationVersion.h index 45b11eb20..e3f1520db 100644 --- a/lib/serializer/ESerializationVersion.h +++ b/lib/serializer/ESerializationVersion.h @@ -50,8 +50,9 @@ enum class ESerializationVersion : int32_t BONUS_HIDDEN, // hidden bonus MORE_MAP_LAYERS, // more map layers CONFIGURABLE_RESOURCES, // configurable resources + CUSTOM_NAMES, // custom names - CURRENT = CONFIGURABLE_RESOURCES, + CURRENT = CUSTOM_NAMES, }; static_assert(ESerializationVersion::MINIMAL <= ESerializationVersion::CURRENT, "Invalid serialization version definition!"); diff --git a/lib/serializer/RegisterTypes.h b/lib/serializer/RegisterTypes.h index 22a4c7508..9116a902d 100644 --- a/lib/serializer/RegisterTypes.h +++ b/lib/serializer/RegisterTypes.h @@ -291,6 +291,8 @@ void registerTypes(Serializer &s) s.template registerType(249); s.template registerType(250); s.template registerType(251); + s.template registerType(252); + s.template registerType(253); } VCMI_LIB_NAMESPACE_END diff --git a/lib/spells/adventure/TownPortalEffect.cpp b/lib/spells/adventure/TownPortalEffect.cpp index f4f985a74..49861c5f6 100644 --- a/lib/spells/adventure/TownPortalEffect.cpp +++ b/lib/spells/adventure/TownPortalEffect.cpp @@ -140,7 +140,7 @@ void TownPortalEffect::endCast(SpellCastEnvironment * env, const AdventureSpellC { const CGTownInstance * destination = nullptr; - if(parameters.caster->getSpellSchoolLevel(owner) < 2) + if(!allowTownSelection) { std::vector pool = getPossibleTowns(env, parameters); destination = findNearestTown(env, parameters, pool); @@ -194,7 +194,7 @@ ESpellCastResult TownPortalEffect::beginCast(SpellCastEnvironment * env, const A return ESpellCastResult::CANCEL; } - if(!parameters.pos.isValid() && parameters.caster->getSpellSchoolLevel(owner) >= 2) + if(!parameters.pos.isValid() && allowTownSelection) { auto queryCallback = [&mechanics, env, parameters](std::optional reply) -> void { diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 3dc4a56b4..6a437611a 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -3221,6 +3221,23 @@ bool CGameHandler::setFormation(ObjectInstanceID hid, EArmyFormation formation) return true; } +bool CGameHandler::setTownName(ObjectInstanceID tid, std::string & name) +{ + const CGTownInstance *t = gameInfo().getTown(tid); + if (!t) + { + logGlobal->error("Town doesn't exist!"); + return false; + } + + ChangeTownName ctn; + ctn.tid = tid; + ctn.name = name; + sendAndApply(ctn); + + return true; +} + bool CGameHandler::queryReply(QueryID qid, std::optional answer, PlayerColor player) { logGlobal->trace("Player %s attempts answering query %d with answer:", player, qid); @@ -3883,8 +3900,14 @@ void CGameHandler::castSpell(const spells::Caster * caster, SpellID spellID, con const CSpell * s = spellID.toSpell(); s->adventureCast(spellEnv.get(), p); - if(const auto * hero = caster->getHeroCaster()) - useChargeBasedSpell(hero->id, spellID); + // FIXME: hack to avoid attempts to use charges when spell is casted externally + // For example, town gates map object in hota/wog + // Proper fix would be to instead spend charges similar to existing caster::spendMana call + if (dynamic_cast(caster) == nullptr) + { + if(const auto * hero = caster->getHeroCaster()) + useChargeBasedSpell(hero->id, spellID); + } } bool CGameHandler::swapStacks(const StackLocation & sl1, const StackLocation & sl2) diff --git a/server/CGameHandler.h b/server/CGameHandler.h index f1661b294..e0e7c791e 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -212,6 +212,7 @@ public: bool queryReply( QueryID qid, std::optional reply, PlayerColor player ); bool buildBoat( ObjectInstanceID objid, PlayerColor player ); bool setFormation( ObjectInstanceID hid, EArmyFormation formation ); + bool setTownName( ObjectInstanceID tid, std::string & name ); bool tradeResources(const IMarket *market, ui32 amountToSell, PlayerColor player, GameResID toSell, GameResID toBuy); bool sacrificeCreatures(const IMarket * market, const CGHeroInstance * hero, const std::vector & slot, const std::vector & count); bool sendResources(ui32 val, PlayerColor player, GameResID r1, PlayerColor r2); diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index 47b9c52f0..e7cde39c3 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -362,6 +362,13 @@ void ApplyGhNetPackVisitor::visitSetFormation(SetFormation & pack) result = gh.setFormation(pack.hid, pack.formation); } +void ApplyGhNetPackVisitor::visitSetTownName(SetTownName & pack) +{ + gh.throwIfWrongOwner(connection, &pack, pack.tid); + + result = gh.setTownName(pack.tid, pack.name); +} + void ApplyGhNetPackVisitor::visitHireHero(HireHero & pack) { gh.throwIfWrongPlayer(connection, &pack); diff --git a/server/ServerNetPackVisitors.h b/server/ServerNetPackVisitors.h index a6ffffb63..3b971fa7d 100644 --- a/server/ServerNetPackVisitors.h +++ b/server/ServerNetPackVisitors.h @@ -58,6 +58,7 @@ public: void visitBuyArtifact(BuyArtifact & pack) override; void visitTradeOnMarketplace(TradeOnMarketplace & pack) override; void visitSetFormation(SetFormation & pack) override; + void visitSetTownName(SetTownName & pack) override; void visitHireHero(HireHero & pack) override; void visitBuildBoat(BuildBoat & pack) override; void visitQueryReply(QueryReply & pack) override; diff --git a/test/bonus/BonusSystemTest.cpp b/test/bonus/BonusSystemTest.cpp index ed327fffb..8468763cf 100644 --- a/test/bonus/BonusSystemTest.cpp +++ b/test/bonus/BonusSystemTest.cpp @@ -189,6 +189,8 @@ TEST_F(BonusSystemTest, battlewidePropagationToAll) EXPECT_TRUE(heroAine.hasBonusOfType(BonusType::BLOCK_ALL_MAGIC)); EXPECT_TRUE(heroBron.hasBonusOfType(BonusType::BLOCK_ALL_MAGIC)); + + heroAine.detachFromSource(orb); } TEST_F(BonusSystemTest, battlewidePropagationToEnemies) @@ -297,6 +299,8 @@ TEST_F(BonusSystemTest, legionPieces) heroAine.detachFrom(townAndVisitor); EXPECT_EQ(town.valOfBonuses(BonusType::CREATURE_GROWTH, BonusCustomSubtype::creatureLevel(3)), 0); + + heroAine.detachFromSource(legion); } } diff --git a/test/events/ApplyDamageTest.cpp b/test/events/ApplyDamageTest.cpp index b396ccd44..1773133ee 100644 --- a/test/events/ApplyDamageTest.cpp +++ b/test/events/ApplyDamageTest.cpp @@ -22,9 +22,11 @@ namespace test using namespace ::testing; using namespace ::events; -class ListenerMock +class ApplyDamageListenerMock { public: + virtual ~ApplyDamageListenerMock() = default; + MOCK_METHOD1(beforeEvent, void(ApplyDamage &)); MOCK_METHOD1(afterEvent, void(const ApplyDamage &)); }; @@ -33,7 +35,7 @@ class ApplyDamageTest : public Test { public: EventBus eventBus; - ListenerMock listener; + ApplyDamageListenerMock listener; StrictMock environmentMock; std::shared_ptr> targetMock; @@ -47,8 +49,8 @@ protected: //this should be the only subscription test for events, just in case cross-binary subscription breaks TEST_F(ApplyDamageTest, Subscription) { - auto subscription1 = eventBus.subscribeBefore(std::bind(&ListenerMock::beforeEvent, &listener, _1)); - auto subscription2 = eventBus.subscribeAfter(std::bind(&ListenerMock::afterEvent, &listener, _1)); + auto subscription1 = eventBus.subscribeBefore(std::bind(&ApplyDamageListenerMock::beforeEvent, &listener, _1)); + auto subscription2 = eventBus.subscribeAfter(std::bind(&ApplyDamageListenerMock::afterEvent, &listener, _1)); EXPECT_CALL(listener, beforeEvent(_)).Times(1); EXPECT_CALL(listener, afterEvent(_)).Times(1); diff --git a/test/events/EventBusTest.cpp b/test/events/EventBusTest.cpp index 150731960..f8c225be1 100644 --- a/test/events/EventBusTest.cpp +++ b/test/events/EventBusTest.cpp @@ -35,9 +35,11 @@ public: friend class SubscriptionRegistry; }; -class ListenerMock +class EventBusListenerMock { public: + virtual ~EventBusListenerMock() = default; + MOCK_METHOD1(beforeEvent, void(EventExample &)); MOCK_METHOD1(onEvent, void(EventExample &)); MOCK_METHOD1(afterEvent, void(const EventExample &)); @@ -51,9 +53,9 @@ public: EventBus subject1; EventBus subject2; - StrictMock listener; - StrictMock listener1; - StrictMock listener2; + StrictMock listener; + StrictMock listener1; + StrictMock listener2; }; TEST_F(EventBusTest, ExecuteNoListeners) @@ -67,15 +69,15 @@ TEST_F(EventBusTest, ExecuteNoListenersWithHandler) EXPECT_CALL(event1, isEnabled()).WillRepeatedly(Return(true)); EXPECT_CALL(listener, onEvent(Ref(event1))).Times(1); - subject1.executeEvent(event1, std::bind(&ListenerMock::onEvent, &listener, _1)); + subject1.executeEvent(event1, std::bind(&EventBusListenerMock::onEvent, &listener, _1)); } TEST_F(EventBusTest, ExecuteIgnoredSubscription) { EXPECT_CALL(event1, isEnabled()).WillRepeatedly(Return(true)); - subject1.subscribeBefore(std::bind(&ListenerMock::beforeEvent, &listener, _1)); - subject1.subscribeAfter(std::bind(&ListenerMock::afterEvent, &listener, _1)); + subject1.subscribeBefore(std::bind(&EventBusListenerMock::beforeEvent, &listener, _1)); + subject1.subscribeAfter(std::bind(&EventBusListenerMock::afterEvent, &listener, _1)); EXPECT_CALL(listener, beforeEvent(_)).Times(0); EXPECT_CALL(listener, afterEvent(_)).Times(0); @@ -87,10 +89,10 @@ TEST_F(EventBusTest, ExecuteSequence) { EXPECT_CALL(event1, isEnabled()).WillRepeatedly(Return(true)); - auto subscription1 = subject1.subscribeBefore(std::bind(&ListenerMock::beforeEvent, &listener1, _1)); - auto subscription2 = subject1.subscribeAfter(std::bind(&ListenerMock::afterEvent, &listener1, _1)); - auto subscription3 = subject1.subscribeBefore(std::bind(&ListenerMock::beforeEvent, &listener2, _1)); - auto subscription4 = subject1.subscribeAfter(std::bind(&ListenerMock::afterEvent, &listener2, _1)); + auto subscription1 = subject1.subscribeBefore(std::bind(&EventBusListenerMock::beforeEvent, &listener1, _1)); + auto subscription2 = subject1.subscribeAfter(std::bind(&EventBusListenerMock::afterEvent, &listener1, _1)); + auto subscription3 = subject1.subscribeBefore(std::bind(&EventBusListenerMock::beforeEvent, &listener2, _1)); + auto subscription4 = subject1.subscribeAfter(std::bind(&EventBusListenerMock::afterEvent, &listener2, _1)); { InSequence sequence; @@ -101,17 +103,17 @@ TEST_F(EventBusTest, ExecuteSequence) EXPECT_CALL(listener2, afterEvent(Ref(event1))).Times(1); } - subject1.executeEvent(event1, std::bind(&ListenerMock::onEvent, &listener, _1)); + subject1.executeEvent(event1, std::bind(&EventBusListenerMock::onEvent, &listener, _1)); } TEST_F(EventBusTest, BusesAreIndependent) { EXPECT_CALL(event1, isEnabled()).WillRepeatedly(Return(true)); - auto subscription1 = subject1.subscribeBefore(std::bind(&ListenerMock::beforeEvent, &listener1, _1)); - auto subscription2 = subject1.subscribeAfter(std::bind(&ListenerMock::afterEvent, &listener1, _1)); - auto subscription3 = subject2.subscribeBefore(std::bind(&ListenerMock::beforeEvent, &listener2, _1)); - auto subscription4 = subject2.subscribeAfter(std::bind(&ListenerMock::afterEvent, &listener2, _1)); + auto subscription1 = subject1.subscribeBefore(std::bind(&EventBusListenerMock::beforeEvent, &listener1, _1)); + auto subscription2 = subject1.subscribeAfter(std::bind(&EventBusListenerMock::afterEvent, &listener1, _1)); + auto subscription3 = subject2.subscribeBefore(std::bind(&EventBusListenerMock::beforeEvent, &listener2, _1)); + auto subscription4 = subject2.subscribeAfter(std::bind(&EventBusListenerMock::afterEvent, &listener2, _1)); EXPECT_CALL(listener1, beforeEvent(_)).Times(1); EXPECT_CALL(listener2, beforeEvent(_)).Times(0); @@ -125,7 +127,7 @@ TEST_F(EventBusTest, DisabledTestDontExecute) { EXPECT_CALL(event1, isEnabled()).Times(AtLeast(1)).WillRepeatedly(Return(false)); EXPECT_CALL(listener, onEvent(Ref(event1))).Times(0); - subject1.executeEvent(event1, std::bind(&ListenerMock::onEvent, &listener, _1)); + subject1.executeEvent(event1, std::bind(&EventBusListenerMock::onEvent, &listener, _1)); } TEST_F(EventBusTest, DisabledTestDontExecutePostHandler) @@ -134,10 +136,10 @@ TEST_F(EventBusTest, DisabledTestDontExecutePostHandler) EXPECT_CALL(listener, onEvent(Ref(event1))).WillRepeatedly(Return()); EXPECT_CALL(listener1, afterEvent(Ref(event1))).Times(0); - auto subscription1 = subject1.subscribeAfter(std::bind(&ListenerMock::afterEvent, &listener1, _1)); + auto subscription1 = subject1.subscribeAfter(std::bind(&EventBusListenerMock::afterEvent, &listener1, _1)); - subject1.executeEvent(event1, std::bind(&ListenerMock::onEvent, &listener, _1)); + subject1.executeEvent(event1, std::bind(&EventBusListenerMock::onEvent, &listener, _1)); } TEST_F(EventBusTest, DisabledTestExecutePreHandler) @@ -146,9 +148,9 @@ TEST_F(EventBusTest, DisabledTestExecutePreHandler) EXPECT_CALL(listener, onEvent(Ref(event1))).WillRepeatedly(Return()); EXPECT_CALL(listener1, beforeEvent(Ref(event1))).Times(1); - auto subscription1 = subject1.subscribeBefore(std::bind(&ListenerMock::beforeEvent, &listener1, _1)); + auto subscription1 = subject1.subscribeBefore(std::bind(&EventBusListenerMock::beforeEvent, &listener1, _1)); - subject1.executeEvent(event1, std::bind(&ListenerMock::onEvent, &listener, _1)); + subject1.executeEvent(event1, std::bind(&EventBusListenerMock::onEvent, &listener, _1)); }