فهرست منبع

Merge pull request #5484 from vcmi/beta

Merge beta -> master
Ivan Savenko 7 ماه پیش
والد
کامیت
dfda6d2626
54فایلهای تغییر یافته به همراه867 افزوده شده و 388 حذف شده
  1. 1 0
      AI/Nullkiller/AIGateway.cpp
  2. 3 0
      CI/conan/base/android
  3. 22 0
      ChangeLog.md
  4. 2 1
      Global.h
  5. 1 1
      Mods/vcmi/Content/config/chinese.json
  6. 3 1
      Mods/vcmi/Content/config/czech.json
  7. 217 212
      Mods/vcmi/Content/config/english.json
  8. 6 1
      Mods/vcmi/Content/config/german.json
  9. 1 1
      Mods/vcmi/Content/config/hungarian.json
  10. 1 1
      Mods/vcmi/Content/config/italian.json
  11. 1 1
      Mods/vcmi/Content/config/polish.json
  12. 1 1
      Mods/vcmi/Content/config/portuguese.json
  13. 1 1
      Mods/vcmi/Content/config/swedish.json
  14. 6 1
      Mods/vcmi/Content/config/ukrainian.json
  15. 1 1
      Mods/vcmi/Content/config/vietnamese.json
  16. 2 2
      android/vcmi-app/build.gradle
  17. 0 1
      client/CServerHandler.cpp
  18. 14 1
      client/ServerRunner.cpp
  19. 1 0
      client/ServerRunner.h
  20. 19 6
      client/eventsSDL/InputSourceTouch.cpp
  21. 4 0
      client/eventsSDL/InputSourceTouch.h
  22. 1 1
      client/globalLobby/GlobalLobbyClient.cpp
  23. 1 0
      client/globalLobby/GlobalLobbyInviteWindow.cpp
  24. 1 1
      client/globalLobby/GlobalLobbyServerSetup.cpp
  25. 1 1
      client/globalLobby/GlobalLobbyWidget.cpp
  26. 31 24
      client/lobby/SelectionTab.cpp
  27. 50 19
      client/mainmenu/CStatisticScreen.cpp
  28. 6 0
      client/mainmenu/CStatisticScreen.h
  29. 6 1
      client/mapView/MapView.cpp
  30. 13 4
      client/mapView/MapViewController.cpp
  31. 1 1
      client/mapView/MapViewController.h
  32. 42 13
      client/renderSDL/SDLImage.cpp
  33. 10 2
      client/renderSDL/SDLImageScaler.cpp
  34. 4 9
      client/renderSDL/ScalableImage.cpp
  35. 5 0
      client/widgets/Buttons.cpp
  36. 1 0
      client/widgets/Buttons.h
  37. 2 1
      client/widgets/Images.cpp
  38. 45 25
      client/widgets/Slider.cpp
  39. 3 0
      client/widgets/Slider.h
  40. 15 11
      client/windows/CCastleInterface.cpp
  41. 2 2
      client/windows/CKingdomInterface.cpp
  42. 8 0
      client/windows/settings/GeneralOptionsTab.cpp
  43. 1 1
      cmake_modules/VersionDefinition.cmake
  44. 22 3
      config/schemas/settings.json
  45. 23 22
      config/widgets/lobbyWindow.json
  46. 212 0
      config/widgets/lobbyWindowWide.json
  47. 9 0
      config/widgets/settings/generalOptionsTab.json
  48. 6 0
      debian/changelog
  49. 1 1
      docs/Readme.md
  50. 1 0
      launcher/eu.vcmi.VCMI.metainfo.xml
  51. 11 11
      launcher/translation/swedish.ts
  52. 11 0
      lib/filesystem/CZipLoader.cpp
  53. 12 2
      lib/mapObjectConstructors/CObjectClassesHandler.cpp
  54. 3 0
      lib/modding/ModManager.cpp

+ 1 - 0
AI/Nullkiller/AIGateway.cpp

@@ -1608,6 +1608,7 @@ void AIGateway::requestActionASAP(std::function<void()> whatToDo)
 void AIGateway::lostHero(HeroPtr h)
 {
 	logAi->debug("I lost my hero %s. It's best to forget and move on.", h.name());
+	nullkiller->invalidatePathfinderData();
 }
 
 void AIGateway::answerQuery(QueryID queryID, int selection)

+ 3 - 0
CI/conan/base/android

@@ -4,3 +4,6 @@ compiler=clang
 compiler.libcxx=c++_shared
 compiler.version=14
 os=Android
+
+[buildenv]
+LD=ld # fixes shared libiconv build

+ 22 - 0
ChangeLog.md

@@ -1,5 +1,27 @@
 # VCMI Project Changelog
 
+## 1.6.6 -> 1.6.7
+
+### Stability
+
+* Fixed regression causing crash when trying to create lobby room
+* Fixed regression causing crash when upscaling image in background thread on some systems
+* Fixed possible crash on opening Custom Campaigns window while having campaign with unsupported format in maps directory
+* Fixed possible crash on misconfigured `compatibilityIdentifiers` field in mods
+* Fixed rare crash on AI turn that could sometimes happen after AI dismissed a hero
+
+### General
+
+* Added alternative layout for global lobby window that supports H3-like 4:3 screen ratio
+* Added option in launcher to disable in-game overlay available with Alt or two-finger touch.
+* Game will now save and restore map zoom level between sessions.
+* Fixed regression that caused Brotherhood of the Sword to open the Thieves' Guild window instead of the Tavern window when clicked.
+* Fixed regression causing black pixels on some city building sprites from mods when played without upscaling filter
+* Improved handling of very slow taps on mobile systems
+* Added snapping of marker when mouse cursor is next to data point for easy selection in game statistics window
+* Fixed some graphical artifacts in the game statistics window.
+* Fixed client not checking if submod is compatible with current VCMI version
+
 ## 1.6.5 -> 1.6.6
 
 ### General

+ 2 - 1
Global.h

@@ -116,11 +116,12 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size.");
 #include <atomic>
 #include <bitset>
 #include <cassert>
+#include <chrono>
 #include <climits>
 #include <cmath>
 #include <codecvt>
-#include <cstdlib>
 #include <cstdio>
+#include <cstdlib>
 #include <fstream>
 #include <functional>
 #include <iomanip>

+ 1 - 1
Mods/vcmi/Content/config/chinese.json

@@ -175,7 +175,7 @@
 	"vcmi.lobby.match.solo" : "单人游戏",
 	"vcmi.lobby.match.duel" : "与 %s 的游戏", // %s -> nickname of another player
 	"vcmi.lobby.match.multi" : "%d 个玩家",
-	"vcmi.lobby.room.create" : "创建房间",
+	"vcmi.lobby.room.create.hover" : "创建房间",
 	"vcmi.lobby.room.players.limit" : "玩家限制",
 	"vcmi.lobby.room.description.public" : "任何玩家都可以加入公开房间。",
 	"vcmi.lobby.room.description.private" : "只有被邀请的玩家能加入私有房间。",

+ 3 - 1
Mods/vcmi/Content/config/czech.json

@@ -178,7 +178,7 @@
 	"vcmi.lobby.match.solo" : "Hra jednoho hráče",
 	"vcmi.lobby.match.duel" : "Hra s %s", // %s -> nickname of another player
 	"vcmi.lobby.match.multi" : "%d hráčů",
-	"vcmi.lobby.room.create" : "Vytvořit novou místnost",
+	"vcmi.lobby.room.create.hover" : "Vytvořit novou místnost",
 	"vcmi.lobby.room.players.limit" : "Omezení počtu hráčů",
 	"vcmi.lobby.room.description.public" : "Jakýkoliv hráč se může připojit do veřejné místnosti.",
 	"vcmi.lobby.room.description.private" : "Pouze pozvaní hráči se mohou připojit do soukromé místnosti.",
@@ -307,6 +307,8 @@
 	"vcmi.systemOptions.enableLargeSpellbookButton.help"   : "{Velká kniha kouzel}\n\nPovolí větší knihu kouzel, do které se vejde více kouzel na jednu stranu. Animace změny stránek s tímto nastavením nefunguje.",
 	"vcmi.systemOptions.audioMuteFocus.hover"  : "Ztlumit při neaktivitě",
 	"vcmi.systemOptions.audioMuteFocus.help"   : "{Ztlumit při neaktivitě}\n\nZtlumit zvuk, pokud je okno hry v pozadí. Výjimkou jsou zprávy ve hře a zvuk nového tahu.",
+	"vcmi.systemOptions.enableOverlayButton.hover" : "Povolit zobrazení informací",
+	"vcmi.systemOptions.enableOverlayButton.help" : "{Povolit zobrazení informací}\n\nZapne vrstvu zobrazující dodatečné informace, například názvy budov, pomocí klávesy ALT nebo gesta dvěma prsty.",
 
 	"vcmi.adventureOptions.infoBarPick.hover" : "Zobrazit zprávy v panelu informací",
 	"vcmi.adventureOptions.infoBarPick.help" : "{Zobrazit zprávy v panelu informací}\n\nKdyž bude možné, herní zprávy z návštěv míst na mapě budou zobrazeny v panelu informací místo ve zvláštním okně.",

+ 217 - 212
Mods/vcmi/Content/config/english.json

@@ -178,7 +178,8 @@
 	"vcmi.lobby.match.solo" : "Singleplayer Game",
 	"vcmi.lobby.match.duel" : "Game with %s", // %s -> nickname of another player
 	"vcmi.lobby.match.multi" : "%d players",
-	"vcmi.lobby.room.create" : "Create New Room",
+	"vcmi.lobby.room.create.hover" : "Create New Room",
+	"vcmi.lobby.room.create.help" : "Create a new room in the online lobby that other players can join.",
 	"vcmi.lobby.room.players.limit" : "Players Limit",
 	"vcmi.lobby.room.description.public" : "Any player can join public room.",
 	"vcmi.lobby.room.description.private" : "Only invited players can join private room.",
@@ -201,6 +202,8 @@
 	"vcmi.lobby.preview.error.mods" : "You are using different set of mods.",
 	"vcmi.lobby.preview.error.version" : "You are using different version of VCMI.",
 	"vcmi.lobby.channel.add" : "Add Channel",
+	"vcmi.lobby.channel.sendMessage.hover" : "Send message",
+	"vcmi.lobby.channel.sendMessage.help" : "Send message",
 	"vcmi.lobby.room.new" : "New Game",
 	"vcmi.lobby.room.load" : "Load Game",
 	"vcmi.lobby.room.type" : "Room Type",
@@ -307,6 +310,8 @@
 	"vcmi.systemOptions.enableLargeSpellbookButton.help"   : "{Large Spell Book}\n\nEnables larger spell book that fits more spells per page. Spell book page change animation does not work with this setting enabled.",
 	"vcmi.systemOptions.audioMuteFocus.hover"  : "Mute on inactivity",
 	"vcmi.systemOptions.audioMuteFocus.help"   : "{Mute on inactivity}\n\nMute audio on inactive window focus. Exceptions are ingame messages and new turn sound.",
+	"vcmi.systemOptions.enableOverlayButton.hover"  : "Enable Overlay",
+	"vcmi.systemOptions.enableOverlayButton.help"   : "{Enable Overlay}\n\nEnable overlays for showing additional infos such as building names using the ALT key or the two finger gesture.",
 
 	"vcmi.adventureOptions.infoBarPick.hover" : "Show Messages in Info Panel",
 	"vcmi.adventureOptions.infoBarPick.help" : "{Show Messages in Info Panel}\n\nWhenever possible, game messages from visiting map objects will be shown in the info panel, instead of popping up in a separate window.",
@@ -328,44 +333,44 @@
 	"vcmi.adventureOptions.smoothDragging.help" : "{Smooth Map Dragging}\n\nWhen enabled, map dragging has a modern run out effect.",
 	"vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Skip fading effects",
 	"vcmi.adventureOptions.skipAdventureMapAnimations.help" : "{Skip fading effects}\n\nWhen enabled, Skips object fadeout and similar effects (resource collection, ship embark etc). Makes UI more reactive in some cases at the expense of aesthetics. Especially useful in PvP games. For maximum movement speed skipping is active regardless of this setting.",
-	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
-	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
-	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",
-	"vcmi.adventureOptions.mapScrollSpeed1.help": "Set the map scrolling speed to very slow.",
-	"vcmi.adventureOptions.mapScrollSpeed5.help": "Set the map scrolling speed to very fast.",
-	"vcmi.adventureOptions.mapScrollSpeed6.help": "Set the map scrolling speed to instantaneous.",
+	"vcmi.adventureOptions.mapScrollSpeed1.hover" : "",
+	"vcmi.adventureOptions.mapScrollSpeed5.hover" : "",
+	"vcmi.adventureOptions.mapScrollSpeed6.hover" : "",
+	"vcmi.adventureOptions.mapScrollSpeed1.help" : "Set the map scrolling speed to very slow.",
+	"vcmi.adventureOptions.mapScrollSpeed5.help" : "Set the map scrolling speed to very fast.",
+	"vcmi.adventureOptions.mapScrollSpeed6.help" : "Set the map scrolling speed to instantaneous.",
 	"vcmi.adventureOptions.hideBackground.hover" : "Hide Background",
 	"vcmi.adventureOptions.hideBackground.help" : "{Hide Background}\n\nHide the adventuremap in the background and show a texture instead.",
 
-	"vcmi.battleOptions.queueSizeLabel.hover": "Show Turn Order Queue",
-	"vcmi.battleOptions.queueSizeNoneButton.hover": "OFF",
-	"vcmi.battleOptions.queueSizeAutoButton.hover": "AUTO",
-	"vcmi.battleOptions.queueSizeSmallButton.hover": "SMALL",
-	"vcmi.battleOptions.queueSizeBigButton.hover": "BIG",
-	"vcmi.battleOptions.queueSizeNoneButton.help": "Do not display Turn Order Queue.",
-	"vcmi.battleOptions.queueSizeAutoButton.help": "Automatically adjust the size of the turn order queue based on the game's resolution(SMALL size is used when playing the game on a resolution with a height lower than 700 pixels, BIG size is used otherwise).",
-	"vcmi.battleOptions.queueSizeSmallButton.help": "Sets turn order queue size to SMALL.",
-	"vcmi.battleOptions.queueSizeBigButton.help": "Sets turn order queue size to BIG (not supported if game resolution height is less than 700 pixels).",
-	"vcmi.battleOptions.animationsSpeed1.hover": "",
-	"vcmi.battleOptions.animationsSpeed5.hover": "",
-	"vcmi.battleOptions.animationsSpeed6.hover": "",
-	"vcmi.battleOptions.animationsSpeed1.help": "Set animation speed to very slow.",
-	"vcmi.battleOptions.animationsSpeed5.help": "Set animation speed to very fast.",
-	"vcmi.battleOptions.animationsSpeed6.help": "Set animation speed to instantaneous.",
-	"vcmi.battleOptions.movementHighlightOnHover.hover": "Movement Highlight on Hover",
-	"vcmi.battleOptions.movementHighlightOnHover.help": "{Movement Highlight on Hover}\n\nHighlight unit's movement range when you hover over it.",
-	"vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Show range limits for shooters",
-	"vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Show range limits for shooters on Hover}\n\nShow shooter's range limits when you hover over it.",
-	"vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Show heroes statistics windows",
-	"vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Show heroes statistics windows}\n\nPermanently toggle on heroes statistics windows that show primary stats and spell points.",
-	"vcmi.battleOptions.skipBattleIntroMusic.hover": "Skip Intro Music",
-	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Skip Intro Music}\n\nAllow actions during the intro music that plays at the beginning of each battle.",	
-	"vcmi.battleOptions.endWithAutocombat.hover": "Ends battle",
-	"vcmi.battleOptions.endWithAutocombat.help": "{Ends battle}\n\nAuto-Combat plays battle to end instant",
-	"vcmi.battleOptions.showQuickSpell.hover": "Show Quickspell panel",
-	"vcmi.battleOptions.showQuickSpell.help": "{Show Quickspell panel}\n\nShow panel for quick selecting spells",
-	"vcmi.battleOptions.showHealthBar.hover": "Show health bar",
-	"vcmi.battleOptions.showHealthBar.help": "{Show health bar}\n\nShow health bar indicating remaining health before one unit dies.",	
+	"vcmi.battleOptions.queueSizeLabel.hover" : "Show Turn Order Queue",
+	"vcmi.battleOptions.queueSizeNoneButton.hover" : "OFF",
+	"vcmi.battleOptions.queueSizeAutoButton.hover" : "AUTO",
+	"vcmi.battleOptions.queueSizeSmallButton.hover" : "SMALL",
+	"vcmi.battleOptions.queueSizeBigButton.hover" : "BIG",
+	"vcmi.battleOptions.queueSizeNoneButton.help" : "Do not display Turn Order Queue.",
+	"vcmi.battleOptions.queueSizeAutoButton.help" : "Automatically adjust the size of the turn order queue based on the game's resolution(SMALL size is used when playing the game on a resolution with a height lower than 700 pixels, BIG size is used otherwise).",
+	"vcmi.battleOptions.queueSizeSmallButton.help" : "Sets turn order queue size to SMALL.",
+	"vcmi.battleOptions.queueSizeBigButton.help" : "Sets turn order queue size to BIG (not supported if game resolution height is less than 700 pixels).",
+	"vcmi.battleOptions.animationsSpeed1.hover" : "",
+	"vcmi.battleOptions.animationsSpeed5.hover" : "",
+	"vcmi.battleOptions.animationsSpeed6.hover" : "",
+	"vcmi.battleOptions.animationsSpeed1.help" : "Set animation speed to very slow.",
+	"vcmi.battleOptions.animationsSpeed5.help" : "Set animation speed to very fast.",
+	"vcmi.battleOptions.animationsSpeed6.help" : "Set animation speed to instantaneous.",
+	"vcmi.battleOptions.movementHighlightOnHover.hover" : "Movement Highlight on Hover",
+	"vcmi.battleOptions.movementHighlightOnHover.help" : "{Movement Highlight on Hover}\n\nHighlight unit's movement range when you hover over it.",
+	"vcmi.battleOptions.rangeLimitHighlightOnHover.hover" : "Show range limits for shooters",
+	"vcmi.battleOptions.rangeLimitHighlightOnHover.help" : "{Show range limits for shooters on Hover}\n\nShow shooter's range limits when you hover over it.",
+	"vcmi.battleOptions.showStickyHeroInfoWindows.hover" : "Show heroes statistics windows",
+	"vcmi.battleOptions.showStickyHeroInfoWindows.help" : "{Show heroes statistics windows}\n\nPermanently toggle on heroes statistics windows that show primary stats and spell points.",
+	"vcmi.battleOptions.skipBattleIntroMusic.hover" : "Skip Intro Music",
+	"vcmi.battleOptions.skipBattleIntroMusic.help" : "{Skip Intro Music}\n\nAllow actions during the intro music that plays at the beginning of each battle.",	
+	"vcmi.battleOptions.endWithAutocombat.hover" : "Ends battle",
+	"vcmi.battleOptions.endWithAutocombat.help" : "{Ends battle}\n\nAuto-Combat plays battle to end instant",
+	"vcmi.battleOptions.showQuickSpell.hover" : "Show Quickspell panel",
+	"vcmi.battleOptions.showQuickSpell.help" : "{Show Quickspell panel}\n\nShow panel for quick selecting spells",
+	"vcmi.battleOptions.showHealthBar.hover" : "Show health bar",
+	"vcmi.battleOptions.showHealthBar.help" : "{Show health bar}\n\nShow health bar indicating remaining health before one unit dies.",	
 
 	"vcmi.adventureMap.revisitObject.hover" : "Revisit Object",
 	"vcmi.adventureMap.revisitObject.help" : "{Revisit Object}\n\nIf a hero currently stands on a Map Object, he can revisit the location.",
@@ -409,8 +414,8 @@
 	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Show Available Creatures}\n\nShow the number of creatures available to purchase instead of their growth in town summary (bottom-left corner of town screen).",
 	"vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Show Weekly Growth of Creatures",
 	"vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{Show Weekly Growth of Creatures}\n\nShow creatures' weekly growth instead of available amount in town summary (bottom-left corner of town screen).",
-	"vcmi.otherOptions.compactTownCreatureInfo.hover": "Compact Creature Info",
-	"vcmi.otherOptions.compactTownCreatureInfo.help": "{Compact Creature Info}\n\nShow smaller information for town creatures in town summary (bottom-left corner of town screen).",
+	"vcmi.otherOptions.compactTownCreatureInfo.hover" : "Compact Creature Info",
+	"vcmi.otherOptions.compactTownCreatureInfo.help" : "{Compact Creature Info}\n\nShow smaller information for town creatures in town summary (bottom-left corner of town screen).",
 
 	"vcmi.townHall.missingBase"             : "Base building %s must be built first",
 	"vcmi.townHall.noCreaturesToRecruit"    : "There are no creatures to recruit!",
@@ -615,182 +620,182 @@
 	
 	"mapObject.core.hillFort.object.description" : "Upgrades creatures. Levels 1 - 4 are less expensive than in associated town.",
 	
-	"core.bonus.ADDITIONAL_ATTACK.name": "Double Strike",
-	"core.bonus.ADDITIONAL_ATTACK.description": "Attacks twice",
-	"core.bonus.ADDITIONAL_RETALIATION.name": "Additional retaliations",
-	"core.bonus.ADDITIONAL_RETALIATION.description": "May retaliate ${val} extra times",
-	"core.bonus.AIR_IMMUNITY.name": "Air immunity",
-	"core.bonus.AIR_IMMUNITY.description": "Immune to all spells from the school of Air magic",
-	"core.bonus.ATTACKS_ALL_ADJACENT.name": "Attack all around",
-	"core.bonus.ATTACKS_ALL_ADJACENT.description": "Attacks all adjacent enemies",
-	"core.bonus.BLOCKS_RETALIATION.name": "No retaliation",
-	"core.bonus.BLOCKS_RETALIATION.description": "Enemy cannot retaliate",
-	"core.bonus.BLOCKS_RANGED_RETALIATION.name": "No ranged retaliation",
-	"core.bonus.BLOCKS_RANGED_RETALIATION.description": "Enemy cannot retaliate by using a ranged attack",
-	"core.bonus.CATAPULT.name": "Catapult",
-	"core.bonus.CATAPULT.description": "Attacks siege walls",
-	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "Reduce Casting Cost (${val})",
-	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "Reduces the spellcasting cost for the hero by ${val}",
-	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "Magic Damper (${val})",
-	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Increases spellcasting cost of enemy spells by ${val}",
-	"core.bonus.CHARGE_IMMUNITY.name": "Immune to Charge",
-	"core.bonus.CHARGE_IMMUNITY.description": "Immune to Cavalier's and Champion's Charge",
-	"core.bonus.DARKNESS.name": "Darkness cover",
-	"core.bonus.DARKNESS.description": "Creates a shroud of darkness with a ${val} radius",
-	"core.bonus.DEATH_STARE.name": "Death Stare (${val}%)",
-	"core.bonus.DEATH_STARE.description": "Has a ${val}% chance to kill a single creature",
-	"core.bonus.DEFENSIVE_STANCE.name": "Defense Bonus",
-	"core.bonus.DEFENSIVE_STANCE.description": "+${val} Defense when defending",
-	"core.bonus.DESTRUCTION.name": "Destruction",
-	"core.bonus.DESTRUCTION.description": "Has ${val}% chance to kill extra units after attack",
-	"core.bonus.DOUBLE_DAMAGE_CHANCE.name": "Death Blow",
-	"core.bonus.DOUBLE_DAMAGE_CHANCE.description": "Has a ${val}% chance of dealing double base damage when attacking",
-	"core.bonus.DRAGON_NATURE.name": "Dragon",
-	"core.bonus.DRAGON_NATURE.description": "Creature has a Dragon Nature",
-	"core.bonus.EARTH_IMMUNITY.name": "Earth immunity",
-	"core.bonus.EARTH_IMMUNITY.description": "Immune to all spells from the school of Earth magic",
-	"core.bonus.ENCHANTER.name": "Enchanter",
-	"core.bonus.ENCHANTER.description": "Can cast mass ${subtype.spell} every turn",
-	"core.bonus.ENCHANTED.name": "Enchanted",
-	"core.bonus.ENCHANTED.description": "Affected by permanent ${subtype.spell}",
-	"core.bonus.ENEMY_ATTACK_REDUCTION.name": "Ignore Attack (${val}%)",
-	"core.bonus.ENEMY_ATTACK_REDUCTION.description": "When being attacked, ${val}% of the attacker's attack is ignored",
-	"core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Ignore Defense (${val}%)",
-	"core.bonus.ENEMY_DEFENCE_REDUCTION.description": "When attacking, ${val}% of the defender's defense is ignored",
-	"core.bonus.FIRE_IMMUNITY.name": "Fire immunity",
-	"core.bonus.FIRE_IMMUNITY.description": "Immune to all spells from the school of Fire magic",
-	"core.bonus.FIRE_SHIELD.name": "Fire Shield (${val}%)",
-	"core.bonus.FIRE_SHIELD.description": "Reflects part of melee damage",
-	"core.bonus.FIRST_STRIKE.name": "First Strike",
-	"core.bonus.FIRST_STRIKE.description": "This creature retaliates before being attacked",
-	"core.bonus.FEAR.name": "Fear",
-	"core.bonus.FEAR.description": "Causes Fear on an enemy stack",
-	"core.bonus.FEARLESS.name": "Fearless",
-	"core.bonus.FEARLESS.description": "Immune to Fear ability",
-	"core.bonus.FEROCITY.name": "Ferocity",
-	"core.bonus.FEROCITY.description": "Attacks ${val} additional times if killed anybody",
-	"core.bonus.FLYING.name": "Fly",
-	"core.bonus.FLYING.description": "Flies when moving (ignores obstacles)",
-	"core.bonus.FREE_SHOOTING.name": "Shoot Close",
-	"core.bonus.FREE_SHOOTING.description": "Can use ranged attacks at melee range",
-	"core.bonus.GARGOYLE.name": "Gargoyle",
-	"core.bonus.GARGOYLE.description": "Cannot be raised or healed",
-	"core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Reduce Damage (${val}%)",
-	"core.bonus.GENERAL_DAMAGE_REDUCTION.description": "Reduces physical damage from ranged or melee attacks",
-	"core.bonus.HATE.name": "Hates ${subtype.creature}",
-	"core.bonus.HATE.description": "Does ${val}% more damage to ${subtype.creature}",
-	"core.bonus.HEALER.name": "Healer",
-	"core.bonus.HEALER.description": "Heals allied units",
-	"core.bonus.HP_REGENERATION.name": "Regeneration",
-	"core.bonus.HP_REGENERATION.description": "Heals ${val} hit points every round",
-	"core.bonus.JOUSTING.name": "Champion charge",
-	"core.bonus.JOUSTING.description": "+${val}% damage for each hex travelled",
-	"core.bonus.KING.name": "King",
-	"core.bonus.KING.description": "Vulnerable to SLAYER level ${val} or higher",
-	"core.bonus.LEVEL_SPELL_IMMUNITY.name": "Spell immunity 1-${val}",
-	"core.bonus.LEVEL_SPELL_IMMUNITY.description": "Immune to spells of levels 1-${val}",
+	"core.bonus.ADDITIONAL_ATTACK.name" : "Double Strike",
+	"core.bonus.ADDITIONAL_ATTACK.description" : "Attacks twice",
+	"core.bonus.ADDITIONAL_RETALIATION.name" : "Additional retaliations",
+	"core.bonus.ADDITIONAL_RETALIATION.description" : "May retaliate ${val} extra times",
+	"core.bonus.AIR_IMMUNITY.name" : "Air immunity",
+	"core.bonus.AIR_IMMUNITY.description" : "Immune to all spells from the school of Air magic",
+	"core.bonus.ATTACKS_ALL_ADJACENT.name" : "Attack all around",
+	"core.bonus.ATTACKS_ALL_ADJACENT.description" : "Attacks all adjacent enemies",
+	"core.bonus.BLOCKS_RETALIATION.name" : "No retaliation",
+	"core.bonus.BLOCKS_RETALIATION.description" : "Enemy cannot retaliate",
+	"core.bonus.BLOCKS_RANGED_RETALIATION.name" : "No ranged retaliation",
+	"core.bonus.BLOCKS_RANGED_RETALIATION.description" : "Enemy cannot retaliate by using a ranged attack",
+	"core.bonus.CATAPULT.name" : "Catapult",
+	"core.bonus.CATAPULT.description" : "Attacks siege walls",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name" : "Reduce Casting Cost (${val})",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description" : "Reduces the spellcasting cost for the hero by ${val}",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name" : "Magic Damper (${val})",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description" : "Increases spellcasting cost of enemy spells by ${val}",
+	"core.bonus.CHARGE_IMMUNITY.name" : "Immune to Charge",
+	"core.bonus.CHARGE_IMMUNITY.description" : "Immune to Cavalier's and Champion's Charge",
+	"core.bonus.DARKNESS.name" : "Darkness cover",
+	"core.bonus.DARKNESS.description" : "Creates a shroud of darkness with a ${val} radius",
+	"core.bonus.DEATH_STARE.name" : "Death Stare (${val}%)",
+	"core.bonus.DEATH_STARE.description" : "Has a ${val}% chance to kill a single creature",
+	"core.bonus.DEFENSIVE_STANCE.name" : "Defense Bonus",
+	"core.bonus.DEFENSIVE_STANCE.description" : "+${val} Defense when defending",
+	"core.bonus.DESTRUCTION.name" : "Destruction",
+	"core.bonus.DESTRUCTION.description" : "Has ${val}% chance to kill extra units after attack",
+	"core.bonus.DOUBLE_DAMAGE_CHANCE.name" : "Death Blow",
+	"core.bonus.DOUBLE_DAMAGE_CHANCE.description" : "Has a ${val}% chance of dealing double base damage when attacking",
+	"core.bonus.DRAGON_NATURE.name" : "Dragon",
+	"core.bonus.DRAGON_NATURE.description" : "Creature has a Dragon Nature",
+	"core.bonus.EARTH_IMMUNITY.name" : "Earth immunity",
+	"core.bonus.EARTH_IMMUNITY.description" : "Immune to all spells from the school of Earth magic",
+	"core.bonus.ENCHANTER.name" : "Enchanter",
+	"core.bonus.ENCHANTER.description" : "Can cast mass ${subtype.spell} every turn",
+	"core.bonus.ENCHANTED.name" : "Enchanted",
+	"core.bonus.ENCHANTED.description" : "Affected by permanent ${subtype.spell}",
+	"core.bonus.ENEMY_ATTACK_REDUCTION.name" : "Ignore Attack (${val}%)",
+	"core.bonus.ENEMY_ATTACK_REDUCTION.description" : "When being attacked, ${val}% of the attacker's attack is ignored",
+	"core.bonus.ENEMY_DEFENCE_REDUCTION.name" : "Ignore Defense (${val}%)",
+	"core.bonus.ENEMY_DEFENCE_REDUCTION.description" : "When attacking, ${val}% of the defender's defense is ignored",
+	"core.bonus.FIRE_IMMUNITY.name" : "Fire immunity",
+	"core.bonus.FIRE_IMMUNITY.description" : "Immune to all spells from the school of Fire magic",
+	"core.bonus.FIRE_SHIELD.name" : "Fire Shield (${val}%)",
+	"core.bonus.FIRE_SHIELD.description" : "Reflects part of melee damage",
+	"core.bonus.FIRST_STRIKE.name" : "First Strike",
+	"core.bonus.FIRST_STRIKE.description" : "This creature retaliates before being attacked",
+	"core.bonus.FEAR.name" : "Fear",
+	"core.bonus.FEAR.description" : "Causes Fear on an enemy stack",
+	"core.bonus.FEARLESS.name" : "Fearless",
+	"core.bonus.FEARLESS.description" : "Immune to Fear ability",
+	"core.bonus.FEROCITY.name" : "Ferocity",
+	"core.bonus.FEROCITY.description" : "Attacks ${val} additional times if killed anybody",
+	"core.bonus.FLYING.name" : "Fly",
+	"core.bonus.FLYING.description" : "Flies when moving (ignores obstacles)",
+	"core.bonus.FREE_SHOOTING.name" : "Shoot Close",
+	"core.bonus.FREE_SHOOTING.description" : "Can use ranged attacks at melee range",
+	"core.bonus.GARGOYLE.name" : "Gargoyle",
+	"core.bonus.GARGOYLE.description" : "Cannot be raised or healed",
+	"core.bonus.GENERAL_DAMAGE_REDUCTION.name" : "Reduce Damage (${val}%)",
+	"core.bonus.GENERAL_DAMAGE_REDUCTION.description" : "Reduces physical damage from ranged or melee attacks",
+	"core.bonus.HATE.name" : "Hates ${subtype.creature}",
+	"core.bonus.HATE.description" : "Does ${val}% more damage to ${subtype.creature}",
+	"core.bonus.HEALER.name" : "Healer",
+	"core.bonus.HEALER.description" : "Heals allied units",
+	"core.bonus.HP_REGENERATION.name" : "Regeneration",
+	"core.bonus.HP_REGENERATION.description" : "Heals ${val} hit points every round",
+	"core.bonus.JOUSTING.name" : "Champion charge",
+	"core.bonus.JOUSTING.description" : "+${val}% damage for each hex travelled",
+	"core.bonus.KING.name" : "King",
+	"core.bonus.KING.description" : "Vulnerable to SLAYER level ${val} or higher",
+	"core.bonus.LEVEL_SPELL_IMMUNITY.name" : "Spell immunity 1-${val}",
+	"core.bonus.LEVEL_SPELL_IMMUNITY.description" : "Immune to spells of levels 1-${val}",
 	"core.bonus.LIMITED_SHOOTING_RANGE.name" : "Limited shooting range",
 	"core.bonus.LIMITED_SHOOTING_RANGE.description" : "Unable to target units farther than ${val} hexes",
-	"core.bonus.LIFE_DRAIN.name": "Drain life (${val}%)",
-	"core.bonus.LIFE_DRAIN.description": "Drains ${val}% of damage dealt",
-	"core.bonus.MANA_CHANNELING.name": "Magic Channel ${val}%",
-	"core.bonus.MANA_CHANNELING.description": "Gives your hero ${val}% of the mana spent by the enemy",
-	"core.bonus.MANA_DRAIN.name": "Mana Drain",
-	"core.bonus.MANA_DRAIN.description": "Drains ${val} mana every turn",
-	"core.bonus.MAGIC_MIRROR.name": "Magic Mirror (${val}%)",
-	"core.bonus.MAGIC_MIRROR.description": "Has a ${val}% chance to redirect an offensive spell to an enemy unit",
-	"core.bonus.MAGIC_RESISTANCE.name": "Magic Resistance (${val}%)",
-	"core.bonus.MAGIC_RESISTANCE.description": "Has a ${val}% chance to resist an enemy spell",
-	"core.bonus.MIND_IMMUNITY.name": "Mind Spell Immunity",
-	"core.bonus.MIND_IMMUNITY.description": "Immune to Mind-type spells",
-	"core.bonus.NO_DISTANCE_PENALTY.name": "No distance penalty",
-	"core.bonus.NO_DISTANCE_PENALTY.description": "Does full damage at any distance",
-	"core.bonus.NO_MELEE_PENALTY.name": "No melee penalty",
-	"core.bonus.NO_MELEE_PENALTY.description": "Creature has no Melee Penalty",
-	"core.bonus.NO_MORALE.name": "Neutral Morale",
-	"core.bonus.NO_MORALE.description": "Creature is immune to morale effects",
-	"core.bonus.NO_WALL_PENALTY.name": "No wall penalty",
-	"core.bonus.NO_WALL_PENALTY.description": "Full damage during siege",
-	"core.bonus.NON_LIVING.name": "Non living",
-	"core.bonus.NON_LIVING.description": "Immunity to many effects",
-	"core.bonus.RANDOM_SPELLCASTER.name": "Random spellcaster",
-	"core.bonus.RANDOM_SPELLCASTER.description": "Can cast random spell",
-	"core.bonus.RANGED_RETALIATION.name": "Ranged retaliation",
-	"core.bonus.RANGED_RETALIATION.description": "Can perform ranged counterattack",
-	"core.bonus.RECEPTIVE.name": "Receptive",
-	"core.bonus.RECEPTIVE.description": "No Immunity to Friendly Spells",
-	"core.bonus.REBIRTH.name": "Rebirth (${val}%)",
-	"core.bonus.REBIRTH.description": "${val}% of stack will rise after death",
-	"core.bonus.RETURN_AFTER_STRIKE.name": "Attack and Return",
-	"core.bonus.RETURN_AFTER_STRIKE.description": "Returns after melee attack",
-	"core.bonus.REVENGE.name": "Revenge",
-	"core.bonus.REVENGE.description": "Deals extra damage based on attacker's lost health in battle",
-	"core.bonus.SHOOTER.name": "Ranged",
-	"core.bonus.SHOOTER.description": "Creature can shoot",
-	"core.bonus.SHOOTS_ALL_ADJACENT.name": "Shoot all around",
-	"core.bonus.SHOOTS_ALL_ADJACENT.description": "This creature's ranged attacks strike all targets in a small area",
-	"core.bonus.SOUL_STEAL.name": "Soul Steal",
-	"core.bonus.SOUL_STEAL.description": "Gains ${val} new creatures for each enemy killed",
-	"core.bonus.SPELLCASTER.name": "Spellcaster",
-	"core.bonus.SPELLCASTER.description": "Can cast ${subtype.spell}",
-	"core.bonus.SPELL_AFTER_ATTACK.name": "Cast After Attack",
-	"core.bonus.SPELL_AFTER_ATTACK.description": "Has a ${val}% chance to cast ${subtype.spell} after it attacks",
-	"core.bonus.SPELL_BEFORE_ATTACK.name": "Cast Before Attack",
-	"core.bonus.SPELL_BEFORE_ATTACK.description": "Has a ${val}% chance to cast ${subtype.spell} before it attacks",
-	"core.bonus.SPELL_IMMUNITY.name": "Spell immunity",
-	"core.bonus.SPELL_IMMUNITY.description": "Immune to ${subtype.spell}",
-	"core.bonus.SPELL_LIKE_ATTACK.name": "Spell-like attack",
-	"core.bonus.SPELL_LIKE_ATTACK.description": "Attacks with ${subtype.spell}",
-	"core.bonus.SPELL_RESISTANCE_AURA.name": "Aura of Resistance",
-	"core.bonus.SPELL_RESISTANCE_AURA.description": "Nearby stacks get ${val}% magic resistance",
-	"core.bonus.SUMMON_GUARDIANS.name": "Summon guardians",
-	"core.bonus.SUMMON_GUARDIANS.description": "At the start of battle summons ${subtype.creature} (${val}%)",
-	"core.bonus.SYNERGY_TARGET.name": "Synergizable",
-	"core.bonus.SYNERGY_TARGET.description": "This creature is vulnerable to synergy effect",
-	"core.bonus.TWO_HEX_ATTACK_BREATH.name": "Breath",
-	"core.bonus.TWO_HEX_ATTACK_BREATH.description": "Breath Attack (2-hex range)",
-	"core.bonus.THREE_HEADED_ATTACK.name": "Three-headed attack",
-	"core.bonus.THREE_HEADED_ATTACK.description": "Attacks three adjacent units",
-	"core.bonus.TRANSMUTATION.name": "Transmutation",
-	"core.bonus.TRANSMUTATION.description": "${val}% chance to transform attacked unit to a different type",
-	"core.bonus.UNDEAD.name": "Undead",
-	"core.bonus.UNDEAD.description": "Creature is Undead",
-	"core.bonus.UNLIMITED_RETALIATIONS.name": "Unlimited retaliations",
-	"core.bonus.UNLIMITED_RETALIATIONS.description": "Can retaliate against an unlimited number of attacks",
-	"core.bonus.WATER_IMMUNITY.name": "Water immunity",
-	"core.bonus.WATER_IMMUNITY.description": "Immune to all spells from the school of Water magic",
-	"core.bonus.WIDE_BREATH.name": "Wide breath",
-	"core.bonus.WIDE_BREATH.description": "Wide breath attack (multiple hexes)",
-	"core.bonus.DISINTEGRATE.name": "Disintegrate",
-	"core.bonus.DISINTEGRATE.description": "No corpse remains after death",
-	"core.bonus.INVINCIBLE.name": "Invincible",
-	"core.bonus.INVINCIBLE.description": "Cannot be affected by anything",
-	"core.bonus.MECHANICAL.name": "Mechanical",
-	"core.bonus.MECHANICAL.description": "Immunity to many effects, repairable",
-	"core.bonus.PRISM_HEX_ATTACK_BREATH.name": "Prism Breath",
-	"core.bonus.PRISM_HEX_ATTACK_BREATH.description": "Prism Breath Attack (three directions)",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.name": "Spell Resistance",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.name.air": "Air Spells Resistance",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.name.fire": "Fire Spells Resistance",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.name.water": "Water Spells Resistance",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.name.earth": "Earth Spells Resistance",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.description": "Damage from all spells reduced by ${val}%.",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.description.air": "Damage from all Air spells reduced by ${val}%.",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.description.fire": "Damage from all Fire spells reduced by ${val}%.",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.description.water": "Damage from all Water spells reduced by ${val}%.",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.description.earth": "Damage from all Earth spells reduced by ${val}%.",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.name": "Spell immunity",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.air": "Air immunity",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.fire": "Fire immunity",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.water": "Water immunity",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.earth": "Earth immunity",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.description": "This unit is immune to all spells",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.air": "This unit is immune to all Air school spells",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.fire": "This unit is immune to all Fire school spells",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.water": "This unit is immune to all Water school spells",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.earth": "This unit is immune to all Earth school spells",
-	"core.bonus.OPENING_BATTLE_SPELL.name": "Starts with spell",
-	"core.bonus.OPENING_BATTLE_SPELL.description": "Casts ${subtype.spell} on battle start",
+	"core.bonus.LIFE_DRAIN.name" : "Drain life (${val}%)",
+	"core.bonus.LIFE_DRAIN.description" : "Drains ${val}% of damage dealt",
+	"core.bonus.MANA_CHANNELING.name" : "Magic Channel ${val}%",
+	"core.bonus.MANA_CHANNELING.description" : "Gives your hero ${val}% of the mana spent by the enemy",
+	"core.bonus.MANA_DRAIN.name" : "Mana Drain",
+	"core.bonus.MANA_DRAIN.description" : "Drains ${val} mana every turn",
+	"core.bonus.MAGIC_MIRROR.name" : "Magic Mirror (${val}%)",
+	"core.bonus.MAGIC_MIRROR.description" : "Has a ${val}% chance to redirect an offensive spell to an enemy unit",
+	"core.bonus.MAGIC_RESISTANCE.name" : "Magic Resistance (${val}%)",
+	"core.bonus.MAGIC_RESISTANCE.description" : "Has a ${val}% chance to resist an enemy spell",
+	"core.bonus.MIND_IMMUNITY.name" : "Mind Spell Immunity",
+	"core.bonus.MIND_IMMUNITY.description" : "Immune to Mind-type spells",
+	"core.bonus.NO_DISTANCE_PENALTY.name" : "No distance penalty",
+	"core.bonus.NO_DISTANCE_PENALTY.description" : "Does full damage at any distance",
+	"core.bonus.NO_MELEE_PENALTY.name" : "No melee penalty",
+	"core.bonus.NO_MELEE_PENALTY.description" : "Creature has no Melee Penalty",
+	"core.bonus.NO_MORALE.name" : "Neutral Morale",
+	"core.bonus.NO_MORALE.description" : "Creature is immune to morale effects",
+	"core.bonus.NO_WALL_PENALTY.name" : "No wall penalty",
+	"core.bonus.NO_WALL_PENALTY.description" : "Full damage during siege",
+	"core.bonus.NON_LIVING.name" : "Non living",
+	"core.bonus.NON_LIVING.description" : "Immunity to many effects",
+	"core.bonus.RANDOM_SPELLCASTER.name" : "Random spellcaster",
+	"core.bonus.RANDOM_SPELLCASTER.description" : "Can cast random spell",
+	"core.bonus.RANGED_RETALIATION.name" : "Ranged retaliation",
+	"core.bonus.RANGED_RETALIATION.description" : "Can perform ranged counterattack",
+	"core.bonus.RECEPTIVE.name" : "Receptive",
+	"core.bonus.RECEPTIVE.description" : "No Immunity to Friendly Spells",
+	"core.bonus.REBIRTH.name" : "Rebirth (${val}%)",
+	"core.bonus.REBIRTH.description" : "${val}% of stack will rise after death",
+	"core.bonus.RETURN_AFTER_STRIKE.name" : "Attack and Return",
+	"core.bonus.RETURN_AFTER_STRIKE.description" : "Returns after melee attack",
+	"core.bonus.REVENGE.name" : "Revenge",
+	"core.bonus.REVENGE.description" : "Deals extra damage based on attacker's lost health in battle",
+	"core.bonus.SHOOTER.name" : "Ranged",
+	"core.bonus.SHOOTER.description" : "Creature can shoot",
+	"core.bonus.SHOOTS_ALL_ADJACENT.name" : "Shoot all around",
+	"core.bonus.SHOOTS_ALL_ADJACENT.description" : "This creature's ranged attacks strike all targets in a small area",
+	"core.bonus.SOUL_STEAL.name" : "Soul Steal",
+	"core.bonus.SOUL_STEAL.description" : "Gains ${val} new creatures for each enemy killed",
+	"core.bonus.SPELLCASTER.name" : "Spellcaster",
+	"core.bonus.SPELLCASTER.description" : "Can cast ${subtype.spell}",
+	"core.bonus.SPELL_AFTER_ATTACK.name" : "Cast After Attack",
+	"core.bonus.SPELL_AFTER_ATTACK.description" : "Has a ${val}% chance to cast ${subtype.spell} after it attacks",
+	"core.bonus.SPELL_BEFORE_ATTACK.name" : "Cast Before Attack",
+	"core.bonus.SPELL_BEFORE_ATTACK.description" : "Has a ${val}% chance to cast ${subtype.spell} before it attacks",
+	"core.bonus.SPELL_IMMUNITY.name" : "Spell immunity",
+	"core.bonus.SPELL_IMMUNITY.description" : "Immune to ${subtype.spell}",
+	"core.bonus.SPELL_LIKE_ATTACK.name" : "Spell-like attack",
+	"core.bonus.SPELL_LIKE_ATTACK.description" : "Attacks with ${subtype.spell}",
+	"core.bonus.SPELL_RESISTANCE_AURA.name" : "Aura of Resistance",
+	"core.bonus.SPELL_RESISTANCE_AURA.description" : "Nearby stacks get ${val}% magic resistance",
+	"core.bonus.SUMMON_GUARDIANS.name" : "Summon guardians",
+	"core.bonus.SUMMON_GUARDIANS.description" : "At the start of battle summons ${subtype.creature} (${val}%)",
+	"core.bonus.SYNERGY_TARGET.name" : "Synergizable",
+	"core.bonus.SYNERGY_TARGET.description" : "This creature is vulnerable to synergy effect",
+	"core.bonus.TWO_HEX_ATTACK_BREATH.name" : "Breath",
+	"core.bonus.TWO_HEX_ATTACK_BREATH.description" : "Breath Attack (2-hex range)",
+	"core.bonus.THREE_HEADED_ATTACK.name" : "Three-headed attack",
+	"core.bonus.THREE_HEADED_ATTACK.description" : "Attacks three adjacent units",
+	"core.bonus.TRANSMUTATION.name" : "Transmutation",
+	"core.bonus.TRANSMUTATION.description" : "${val}% chance to transform attacked unit to a different type",
+	"core.bonus.UNDEAD.name" : "Undead",
+	"core.bonus.UNDEAD.description" : "Creature is Undead",
+	"core.bonus.UNLIMITED_RETALIATIONS.name" : "Unlimited retaliations",
+	"core.bonus.UNLIMITED_RETALIATIONS.description" : "Can retaliate against an unlimited number of attacks",
+	"core.bonus.WATER_IMMUNITY.name" : "Water immunity",
+	"core.bonus.WATER_IMMUNITY.description" : "Immune to all spells from the school of Water magic",
+	"core.bonus.WIDE_BREATH.name" : "Wide breath",
+	"core.bonus.WIDE_BREATH.description" : "Wide breath attack (multiple hexes)",
+	"core.bonus.DISINTEGRATE.name" : "Disintegrate",
+	"core.bonus.DISINTEGRATE.description" : "No corpse remains after death",
+	"core.bonus.INVINCIBLE.name" : "Invincible",
+	"core.bonus.INVINCIBLE.description" : "Cannot be affected by anything",
+	"core.bonus.MECHANICAL.name" : "Mechanical",
+	"core.bonus.MECHANICAL.description" : "Immunity to many effects, repairable",
+	"core.bonus.PRISM_HEX_ATTACK_BREATH.name" : "Prism Breath",
+	"core.bonus.PRISM_HEX_ATTACK_BREATH.description" : "Prism Breath Attack (three directions)",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name" : "Spell Resistance",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.air" : "Air Spells Resistance",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.fire" : "Fire Spells Resistance",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.water" : "Water Spells Resistance",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.earth" : "Earth Spells Resistance",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description" : "Damage from all spells reduced by ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.air" : "Damage from all Air spells reduced by ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.fire" : "Damage from all Fire spells reduced by ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.water" : "Damage from all Water spells reduced by ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.earth" : "Damage from all Earth spells reduced by ${val}%.",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name" : "Spell immunity",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.air" : "Air immunity",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.fire" : "Fire immunity",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.water" : "Water immunity",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.earth" : "Earth immunity",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description" : "This unit is immune to all spells",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.air" : "This unit is immune to all Air school spells",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.fire" : "This unit is immune to all Fire school spells",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.water" : "This unit is immune to all Water school spells",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.earth" : "This unit is immune to all Earth school spells",
+	"core.bonus.OPENING_BATTLE_SPELL.name" : "Starts with spell",
+	"core.bonus.OPENING_BATTLE_SPELL.description" : "Casts ${subtype.spell} on battle start",
 	
 	"spell.core.castleMoat.name" : "Moat",
 	"spell.core.castleMoatTrigger.name" : "Moat",

+ 6 - 1
Mods/vcmi/Content/config/german.json

@@ -178,7 +178,8 @@
 	"vcmi.lobby.match.solo" : "Einzelspieler-Spiel",
 	"vcmi.lobby.match.duel" : "Spiel mit %s", // %s -> nickname of another player
 	"vcmi.lobby.match.multi" : "%d Spieler",
-	"vcmi.lobby.room.create" : "Neuen Spiel-Raum erstellen",
+	"vcmi.lobby.room.create.hover" : "Neuen Spiel-Raum erstellen",
+	"vcmi.lobby.room.create.help" : "Erstelle einen neuen Raum in der Online-Lobby, dem andere Spieler beitreten können.",
 	"vcmi.lobby.room.players.limit" : "Spieler Limit",
 	"vcmi.lobby.room.description.public" : "Jeder Spieler kann dem öffentlichen Raum beitreten.",
 	"vcmi.lobby.room.description.private" : "Nur eingeladene Spieler können den privaten Raum betreten.",
@@ -201,6 +202,8 @@
 	"vcmi.lobby.preview.error.mods" : "Ihr verwendet andere Mods.",
 	"vcmi.lobby.preview.error.version" : "Ihr verwendet eine andere Version von VCMI.",
 	"vcmi.lobby.channel.add" : "Kanal hinzufügen",
+	"vcmi.lobby.channel.sendMessage.hover" : "Nachricht senden",
+	"vcmi.lobby.channel.sendMessage.help" : "Nachricht senden",
 	"vcmi.lobby.room.new" : "Neues Spiel",
 	"vcmi.lobby.room.load" : "Spiel laden",
 	"vcmi.lobby.room.type" : "Raumtyp",
@@ -307,6 +310,8 @@
 	"vcmi.systemOptions.enableLargeSpellbookButton.help"   : "{Großes Zauberbuch}\n\nErmöglicht ein größeres Zauberbuch, in das mehr Zaubersprüche pro Seite passen. Die Animation des Seitenwechsels im Zauberbuch funktioniert nicht, wenn diese Einstellung aktiviert ist.",
 	"vcmi.systemOptions.audioMuteFocus.hover"  : "Stumm bei Inaktivität",
 	"vcmi.systemOptions.audioMuteFocus.help"   : "{Stumm bei Inaktivität}\n\nSchaltet Audio bei inaktiven Fenster-Fokus stumm. Ausnahmen sind Ingame-Nachrichten und der Neuer-Zug-Sound.",
+	"vcmi.systemOptions.enableOverlayButton.hover"  : "Overlay aktivieren",
+	"vcmi.systemOptions.enableOverlayButton.help"   : "{Overlay aktivieren}\n\nAktiviere Overlays, die zusätzliche Infos, wie Gebäudenamen anzeigen, wenn die ALT-Taste gedrückt oder die Zwei-Finger-Geste genutzt wird.",
 
 	"vcmi.adventureOptions.infoBarPick.hover" : "Meldungen im Infobereich anzeigen",
 	"vcmi.adventureOptions.infoBarPick.help" : "{Meldungen im Infobereich anzeigen}\n\nWann immer möglich, werden Spielnachrichten von besuchten Kartenobjekten in der Infoleiste angezeigt, anstatt als Popup-Fenster zu erscheinen",

+ 1 - 1
Mods/vcmi/Content/config/hungarian.json

@@ -175,7 +175,7 @@
 	"vcmi.lobby.match.solo" : "Egyszemélyes játék",
 	"vcmi.lobby.match.duel" : "Játék %s-szel", // %s -> nickname of another player
 	"vcmi.lobby.match.multi" : "%d játékos",
-	"vcmi.lobby.room.create" : "Új szoba létrehozása",
+	"vcmi.lobby.room.create.hover" : "Új szoba létrehozása",
 	"vcmi.lobby.room.players.limit" : "Játékosok száma",
 	"vcmi.lobby.room.description.public" : "Bárki csatlakozhat a nyilvános szobához.",
 	"vcmi.lobby.room.description.private" : "Csak meghívott játékosok csatlakozhatnak a privát szobához.",

+ 1 - 1
Mods/vcmi/Content/config/italian.json

@@ -177,7 +177,7 @@
 	"vcmi.lobby.match.solo" : "Partita in singolo",
 	"vcmi.lobby.match.duel" : "Partita con %s", // %s -> nickname of another player
 	"vcmi.lobby.match.multi" : "%d giocatori",
-	"vcmi.lobby.room.create" : "Crea nuova stanza",
+	"vcmi.lobby.room.create.hover" : "Crea nuova stanza",
 	"vcmi.lobby.room.players.limit" : "Limite giocatori",
 	"vcmi.lobby.room.description.public" : "Qualsiasi giocatore può entrare in una stanza pubblica.",
 	"vcmi.lobby.room.description.private" : "Solo i giocatori invitati possono entrare in una stanza privata.",

+ 1 - 1
Mods/vcmi/Content/config/polish.json

@@ -171,7 +171,7 @@
 	"vcmi.lobby.match.solo" : "Gra jednoosobowa",
 	"vcmi.lobby.match.duel" : "vs. %s", // %s -> nickname of another player
 	"vcmi.lobby.match.multi" : "%d graczy",
-	"vcmi.lobby.room.create" : "Stwórz nowy pokój",
+	"vcmi.lobby.room.create.hover" : "Stwórz nowy pokój",
 	"vcmi.lobby.room.players.limit" : "Limit graczy",
 	"vcmi.lobby.room.description.public" : "Każdy może dołączyć do pokoju.",
 	"vcmi.lobby.room.description.private" : "Tylko zaproszeni gracze mogą dołączyć do pokoju.",

+ 1 - 1
Mods/vcmi/Content/config/portuguese.json

@@ -175,7 +175,7 @@
 	"vcmi.lobby.match.solo" : "Jogo para um Jogador",
 	"vcmi.lobby.match.duel" : "Jogo com %s", // %s -> apelido de outro jogador
 	"vcmi.lobby.match.multi" : "%d jogadores",
-	"vcmi.lobby.room.create" : "Criar Nova Sala",
+	"vcmi.lobby.room.create.hover" : "Criar Nova Sala",
 	"vcmi.lobby.room.players.limit" : "Limite de Jogadores",
 	"vcmi.lobby.room.description.public" : "Qualquer jogador pode entrar na sala pública.",
 	"vcmi.lobby.room.description.private" : "Apenas jogadores convidados podem entrar na sala privada.",

+ 1 - 1
Mods/vcmi/Content/config/swedish.json

@@ -178,7 +178,7 @@
 	"vcmi.lobby.match.solo"              : "Spel för en spelare",
 	"vcmi.lobby.match.duel"              : "Spel med %s", // %s -> smeknamn på en annan spelare
 	"vcmi.lobby.match.multi"             : "%d spelare",
-	"vcmi.lobby.room.create"             : "Skapa nytt rum",
+	"vcmi.lobby.room.create.hover"             : "Skapa nytt rum",
 	"vcmi.lobby.room.players.limit"      : "Begränsning av spelare",
 	"vcmi.lobby.room.description.public" : "Alla spelare kan gå med i det offentliga rummet.",
 	"vcmi.lobby.room.description.private": "Endast inbjudna spelare kan gå med i ett privat rum.",

+ 6 - 1
Mods/vcmi/Content/config/ukrainian.json

@@ -178,7 +178,8 @@
 	"vcmi.lobby.match.solo" : "Одиночна гра",
 	"vcmi.lobby.match.duel" : "Гра з %s", // %s -> nickname of another player
 	"vcmi.lobby.match.multi" : "%d гравців",
-	"vcmi.lobby.room.create" : "Створити нову кімнату",
+	"vcmi.lobby.room.create.hover" : "Створити нову кімнату",
+	"vcmi.lobby.room.create.help" : "Створити нову кімнату в онлайн-лобі, до якої зможуть приєднатися інші гравці",
 	"vcmi.lobby.room.players.limit" : "Максимум гравців",
 	"vcmi.lobby.room.description.public" : "Будь-хто з гравців може приєднатися до публічної кімнати.",
 	"vcmi.lobby.room.description.private" : "Тільки запрошені гравці можуть приєднатися до приватної кімнати.",
@@ -201,6 +202,8 @@
 	"vcmi.lobby.preview.error.mods" : "Ви використовуєте інший набір модифікацій.",
 	"vcmi.lobby.preview.error.version" : "Ви використовуєте іншу версію VCMI.",
 	"vcmi.lobby.channel.add" : "Додати канал чату",
+	"vcmi.lobby.channel.sendMessage.hover" : "Надіслати повідомлення",
+	"vcmi.lobby.channel.sendMessage.help" : "Надіслати повідомлення",
 	"vcmi.lobby.room.new" : "Нова гра",
 	"vcmi.lobby.room.load" : "Завантажити гру",
 	"vcmi.lobby.room.type" : "Тип кімнати",
@@ -307,6 +310,8 @@
 	"vcmi.systemOptions.enableLargeSpellbookButton.help"   : "{Велика книга заклять}\n\nВмикає більшу книгу заклять, яка вміщує більше заклять на сторінці. Якщо цей параметр увімкнено, анімація зміни сторінок книги заклять не буде відображатися.",
 	"vcmi.systemOptions.audioMuteFocus.hover"  : "Тиша при втраті фокусу",
 	"vcmi.systemOptions.audioMuteFocus.help"   : "{Тиша при втраті фокусу}\n\nВимкнути звук коли вікно не у фокусі. Виняток становлять ігрові сповіщення та звук нового ходу.",
+	"vcmi.systemOptions.enableOverlayButton.hover"  : "Дозволити оверлей",
+	"vcmi.systemOptions.enableOverlayButton.help"   : "{Дозволити оверлей}\n\nДозволяє показувати додаткову інформацію, наприклад, назви будівель, за допомогою клавіші ALT або дотиком двох пальців",
 
 	"vcmi.adventureOptions.infoBarPick.help" : "{Повідомлення у панелі статусу}\n\nЗа можливості, повідомлення про відвідування об'єктів карти пригод будуть відображені у панелі статусу замість окремого вікна",
 	"vcmi.adventureOptions.infoBarPick.hover" : "Повідомлення у панелі статусу",

+ 1 - 1
Mods/vcmi/Content/config/vietnamese.json

@@ -175,7 +175,7 @@
 	"vcmi.lobby.match.solo" : "Chơi Đơn",
 	"vcmi.lobby.match.duel" : "Chơi với %s", // %s -> nickname of another player
 	"vcmi.lobby.match.multi" : "người chơi %d",
-	"vcmi.lobby.room.create" : "Tạo Phòng Mới",
+	"vcmi.lobby.room.create.hover" : "Tạo Phòng Mới",
 	"vcmi.lobby.room.players.limit" : "Số Người Chơi",
 	"vcmi.lobby.room.description.public" : "Người chơi có thể vào phòng công khai.",
 	"vcmi.lobby.room.description.private" : "Người chơi được mời mới có thể vào phòng riêng tư.",

+ 2 - 2
android/vcmi-app/build.gradle

@@ -26,8 +26,8 @@ android {
 		minSdk = qtMinSdkVersion as Integer
 		targetSdk = qtTargetSdkVersion as Integer // ANDROID_TARGET_SDK_VERSION in the CMake project
 
-		versionCode 1660
-		versionName "1.6.6"
+		versionCode 1670
+		versionName "1.6.7"
 
 		setProperty("archivesBaseName", "vcmi")
 	}

+ 0 - 1
client/CServerHandler.cpp

@@ -194,7 +194,6 @@ void CServerHandler::startLocalServerAndConnect(bool connectToLobby)
 
 void CServerHandler::connectToServer(const std::string & addr, const ui16 port)
 {
-	logNetwork->info("Establishing connection to %s:%d...", addr, port);
 	setState(EClientState::CONNECTING);
 	serverHostname = addr;
 	serverPort = port;

+ 14 - 1
client/ServerRunner.cpp

@@ -40,6 +40,7 @@ void ServerThreadRunner::start(bool listenForConnections, bool connectToLobby, s
 	// cfgport may be 0 -- the real port is returned after calling prepare()
 	uint16_t port = settings["server"]["localPort"].Integer();
 	server = std::make_unique<CVCMIServer>(port, true);
+	lobbyMode = connectToLobby;
 
 	if (startingInfo)
 	{
@@ -77,7 +78,18 @@ int ServerThreadRunner::exitCode()
 
 void ServerThreadRunner::connect(INetworkHandler & network, INetworkClientListener & listener)
 {
-	network.createInternalConnection(listener, server->getNetworkServer());
+	if (lobbyMode)
+	{
+		std::string host = settings["server"]["localHostname"].String();
+		uint16_t port = settings["server"]["localPort"].Integer();
+		logNetwork->info("Establishing connection to %s:%d...", host, port);
+
+		network.connectToRemote(listener, host, port);
+	}
+	else
+	{
+		network.createInternalConnection(listener, server->getNetworkServer());
+	}
 }
 
 #ifdef ENABLE_SERVER_PROCESS
@@ -122,6 +134,7 @@ void ServerProcessRunner::connect(INetworkHandler & network, INetworkClientListe
 {
 	std::string host = settings["server"]["localHostname"].String();
 	uint16_t port = settings["server"]["localPort"].Integer();
+	logNetwork->info("Establishing connection to %s:%d...", host, port);
 
 	network.connectToRemote(listener, host, port);
 }

+ 1 - 0
client/ServerRunner.h

@@ -38,6 +38,7 @@ class ServerThreadRunner final : public IServerRunner, boost::noncopyable
 	std::unique_ptr<CVCMIServer> server;
 	boost::thread threadRunLocalServer;
 	uint16_t serverPort = 0;
+	bool lobbyMode = false;
 
 public:
 	void start(bool listenForConnections, bool connectToLobby, std::shared_ptr<StartInfo> startingInfo) override;

+ 19 - 6
client/eventsSDL/InputSourceTouch.cpp

@@ -58,6 +58,15 @@ InputSourceTouch::InputSourceTouch()
 
 void InputSourceTouch::handleEventFingerMotion(const SDL_TouchFingerEvent & tfinger)
 {
+	Point screenSize = GH.screenDimensions();
+
+	motionAccumulatedX[tfinger.fingerId] += tfinger.dx;
+	motionAccumulatedY[tfinger.fingerId] += tfinger.dy;
+
+	float motionThreshold = 1.0 / std::min(screenSize.x, screenSize.y);
+	if(std::abs(motionAccumulatedX[tfinger.fingerId]) < motionThreshold && std::abs(motionAccumulatedY[tfinger.fingerId]) < motionThreshold)
+		return;
+
 	if (CCS && CCS->curh && settings["video"]["cursor"].String() == "software" && state != TouchState::RELATIVE_MODE)
 		CCS->curh->cursorMove(GH.getCursorPosition().x, GH.getCursorPosition().y);
 
@@ -65,12 +74,11 @@ void InputSourceTouch::handleEventFingerMotion(const SDL_TouchFingerEvent & tfin
 	{
 		case TouchState::RELATIVE_MODE:
 		{
-			Point screenSize = GH.screenDimensions();
 			int scalingFactor = GH.screenHandler().getScalingFactor();
 
 			Point moveDistance {
-				static_cast<int>(screenSize.x * params.relativeModeSpeedFactor * tfinger.dx),
-				static_cast<int>(screenSize.y * params.relativeModeSpeedFactor * tfinger.dy)
+				static_cast<int>(screenSize.x * params.relativeModeSpeedFactor * motionAccumulatedX[tfinger.fingerId]),
+				static_cast<int>(screenSize.y * params.relativeModeSpeedFactor * motionAccumulatedY[tfinger.fingerId])
 			};
 
 			GH.input().moveCursorPosition(moveDistance);
@@ -112,6 +120,11 @@ void InputSourceTouch::handleEventFingerMotion(const SDL_TouchFingerEvent & tfin
 			break;
 		}
 	}
+
+	if(std::abs(motionAccumulatedX[tfinger.fingerId]) >= motionThreshold)
+		motionAccumulatedX[tfinger.fingerId] = 0;
+	if(std::abs(motionAccumulatedY[tfinger.fingerId]) >= motionThreshold)
+		motionAccumulatedY[tfinger.fingerId] = 0;
 }
 
 void InputSourceTouch::handleEventFingerDown(const SDL_TouchFingerEvent & tfinger)
@@ -293,7 +306,7 @@ int InputSourceTouch::getNumTouchFingers() const
 
 void InputSourceTouch::emitPanningEvent(const SDL_TouchFingerEvent & tfinger)
 {
-	Point distance = convertTouchToMouse(-tfinger.dx, -tfinger.dy);
+	Point distance = convertTouchToMouse(-motionAccumulatedX[tfinger.fingerId], -motionAccumulatedY[tfinger.fingerId]);
 
 	GH.events().dispatchGesturePanning(lastTapPosition, convertTouchToMouse(tfinger), distance);
 }
@@ -327,8 +340,8 @@ void InputSourceTouch::emitPinchEvent(const SDL_TouchFingerEvent & tfinger)
 
 	float thisX = tfinger.x * GH.screenDimensions().x;
 	float thisY = tfinger.y * GH.screenDimensions().y;
-	float deltaX = tfinger.dx * GH.screenDimensions().x;
-	float deltaY = tfinger.dy * GH.screenDimensions().y;
+	float deltaX = motionAccumulatedX[tfinger.fingerId] * GH.screenDimensions().x;
+	float deltaY = motionAccumulatedY[tfinger.fingerId] * GH.screenDimensions().y;
 
 	float oldX = thisX - deltaX - otherX;
 	float oldY = thisY - deltaY - otherY;

+ 4 - 0
client/eventsSDL/InputSourceTouch.h

@@ -10,6 +10,7 @@
 
 #pragma once
 
+#include "SDL_touch.h"
 #include "../../lib/Point.h"
 
 // Debug option. If defined, mouse events will instead generate touch events, allowing testing of touch input on desktop
@@ -110,6 +111,9 @@ class InputSourceTouch
 	Point lastLeftClickPosition;
 	int numTouchFingers;
 
+	std::map<SDL_FingerID, float> motionAccumulatedX;
+	std::map<SDL_FingerID, float> motionAccumulatedY;
+
 	Point convertTouchToMouse(const SDL_TouchFingerEvent & current);
 	Point convertTouchToMouse(float x, float y);
 

+ 1 - 1
client/globalLobby/GlobalLobbyClient.cpp

@@ -477,7 +477,7 @@ std::shared_ptr<GlobalLobbyLoginWindow> GlobalLobbyClient::createLoginWindow()
 std::shared_ptr<GlobalLobbyWindow> GlobalLobbyClient::createLobbyWindow()
 {
 	auto lobbyWindowPtr = lobbyWindow.lock();
-	if(lobbyWindowPtr)
+	if(lobbyWindowPtr && GH.screenDimensions().x >= lobbyWindowPtr->pos.w) // if wide window doesn't fit anymore after ingame screen resolution change
 		return lobbyWindowPtr;
 
 	lobbyWindowPtr = std::make_shared<GlobalLobbyWindow>();

+ 1 - 0
client/globalLobby/GlobalLobbyInviteWindow.cpp

@@ -95,6 +95,7 @@ GlobalLobbyInviteWindow::GlobalLobbyInviteWindow()
 
 	listBackground = std::make_shared<TransparentFilledRectangle>(Rect(8, 48, 220, 324), ColorRGBA(0, 0, 0, 64), ColorRGBA(64, 80, 128, 255), 1);
 	accountList = std::make_shared<CListBox>(createAccountCardCallback, Point(10, 50), Point(0, 40), 8, CSH->getGlobalLobby().getActiveAccounts().size(), 0, 1 | 4, Rect(200, 0, 320, 320));
+	accountList->setRedrawParent(true);
 
 	buttonClose = std::make_shared<CButton>(Point(86, 384), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this]() { close(); }, EShortcut::GLOBAL_RETURN );
 

+ 1 - 1
client/globalLobby/GlobalLobbyServerSetup.cpp

@@ -35,7 +35,7 @@ GlobalLobbyServerSetup::GlobalLobbyServerSetup()
 	pos.h = 340;
 
 	filledBackground = std::make_shared<FilledTexturePlayerColored>(Rect(0, 0, pos.w, pos.h));
-	labelTitle = std::make_shared<CLabel>( pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.room.create"));
+	labelTitle = std::make_shared<CLabel>( pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.room.create.hover"));
 	labelPlayerLimit = std::make_shared<CLabel>( pos.w / 2, 48, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.room.players.limit"));
 	labelRoomType = std::make_shared<CLabel>( pos.w / 2, 108, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.room.type"));
 	labelGameMode = std::make_shared<CLabel>( pos.w / 2, 158, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.room.mode"));

+ 1 - 1
client/globalLobby/GlobalLobbyWidget.cpp

@@ -44,7 +44,7 @@ GlobalLobbyWidget::GlobalLobbyWidget(GlobalLobbyWindow * window)
 
 	REGISTER_BUILDER("lobbyItemList", &GlobalLobbyWidget::buildItemList);
 
-	const JsonNode config(JsonPath::builtin("config/widgets/lobbyWindow.json"));
+	const JsonNode config(JsonPath::builtin(GH.screenDimensions().x >= 1024 ? "config/widgets/lobbyWindowWide.json" : "config/widgets/lobbyWindow.json"));
 	build(config);
 }
 

+ 31 - 24
client/lobby/SelectionTab.cpp

@@ -961,31 +961,38 @@ void SelectionTab::parseCampaigns(const std::unordered_set<ResourcePath> & files
 	allItems.reserve(files.size());
 	for(auto & file : files)
 	{
-		auto info = std::make_shared<ElementInfo>();
-		info->fileURI = file.getOriginalName();
-		info->campaignInit();
-		info->name = info->getNameForList();
-				
-		if(info->campaign)
+		try
+		{
+			auto info = std::make_shared<ElementInfo>();
+			info->fileURI = file.getOriginalName();
+			info->campaignInit();
+			info->name = info->getNameForList();
+
+			if(info->campaign)
+			{
+				// skip campaigns organized in sets
+				std::string foundInSet = "";
+				for (auto const & set : campaignSets.Struct())
+					for (auto const & item : set.second["items"].Vector())
+						if(file.getName() == ResourcePath(item["file"].String()).getName())
+							foundInSet = set.first;
+
+				// set has to be used in main menu
+				bool setInMainmenu = false;
+				if(!foundInSet.empty())
+					for (auto const & item : mainmenu["window"]["items"].Vector())
+						if(item["name"].String() == "campaign")
+							for (auto const & button : item["buttons"].Vector())
+								if(boost::algorithm::ends_with(boost::algorithm::to_lower_copy(button["command"].String()), boost::algorithm::to_lower_copy(foundInSet)))
+									setInMainmenu = true;
+
+				if(!setInMainmenu)
+					allItems.push_back(info);
+			}
+		}
+		catch(const std::exception & e)
 		{
-			// skip campaigns organized in sets
-			std::string foundInSet = "";
-			for (auto const & set : campaignSets.Struct())
-				for (auto const & item : set.second["items"].Vector())
-					if(file.getName() == ResourcePath(item["file"].String()).getName())
-						foundInSet = set.first;
-			
-			// set has to be used in main menu
-			bool setInMainmenu = false;
-			if(!foundInSet.empty())
-				for (auto const & item : mainmenu["window"]["items"].Vector())
-					if(item["name"].String() == "campaign")
-						for (auto const & button : item["buttons"].Vector())
-							if(boost::algorithm::ends_with(boost::algorithm::to_lower_copy(button["command"].String()), boost::algorithm::to_lower_copy(foundInSet)))
-								setInMainmenu = true;
-
-			if(!setInMainmenu)
-				allItems.push_back(info);
+			logGlobal->error("Error: Failed to process campaign %s: %s", file.getName(), e.what());
 		}
 	}
 }

+ 50 - 19
client/mainmenu/CStatisticScreen.cpp

@@ -438,7 +438,7 @@ int computeGridStep(int maxAmount, int linesLimit)
 }
 
 LineChart::LineChart(Rect position, std::string title, TData data, TIcons icons, float maxY)
-	: CIntObject(), maxVal(0), maxDay(0)
+	: CIntObject(), maxVal(0), maxDay(0), data(data)
 {
 	OBJECT_CONSTRUCTION;
 
@@ -474,21 +474,17 @@ LineChart::LineChart(Rect position, std::string title, TData data, TIcons icons,
 	niceMaxVal = gridStep * std::ceil(maxVal / gridStep);
 	niceMaxVal = std::max(1, niceMaxVal); // avoid zero size Y axis (if all values are 0)
 
-	// calculate points in chart
-	auto getPoint = [this](int i, std::vector<float> data){
-		float x = (static_cast<float>(chartArea.w) / static_cast<float>(maxDay - 1)) * static_cast<float>(i);
-		float y = static_cast<float>(chartArea.h) - (static_cast<float>(chartArea.h) / niceMaxVal) * data[i];
-		return Point(x, y);
-	};
-
 	// draw grid (vertical lines)
 	int dayGridInterval = maxDay < 700 ? 7 : 28;
-	for(const auto & line : data)
+	if(maxDay > 1)
 	{
-		for(int i = 0; i < line.second.size(); i += dayGridInterval)
+		for(const auto & line : data)
 		{
-			Point p = getPoint(i, line.second) + chartArea.topLeft();
-			canvas->addLine(Point(p.x, chartArea.topLeft().y), Point(p.x, chartArea.topLeft().y + chartArea.h), ColorRGBA(70, 70, 70));
+			for(int i = 0; i < line.second.size(); i += dayGridInterval)
+			{
+				Point p = getPoint(i, line.second) + chartArea.topLeft();
+				canvas->addLine(Point(p.x, chartArea.topLeft().y), Point(p.x, chartArea.topLeft().y + chartArea.h), ColorRGBA(70, 70, 70));
+			}
 		}
 	}
 
@@ -518,7 +514,9 @@ LineChart::LineChart(Rect position, std::string title, TData data, TIcons icons,
 			for(auto & icon : icons)
 				if(std::get<0>(icon) == line.first && std::get<1>(icon) == i + 1) // color && day
 				{
-					pictures.emplace_back(std::make_shared<CPicture>(std::get<2>(icon), Point(p.x - (std::get<2>(icon)->width() / 2), p.y - (std::get<2>(icon)->height() / 2))));
+					auto img = std::get<2>(icon);
+					Point imgPos(p.x - (img->contentRect().w / 2) - img->contentRect().x, p.y - (img->contentRect().h / 2) - img->contentRect().y);
+					pictures.emplace_back(std::make_shared<CPicture>(img, imgPos));
 					pictures.back()->addRClickCallback([icon](){ CRClickPopup::createAndPush(std::get<3>(icon)); });
 				}
 
@@ -536,18 +534,51 @@ LineChart::LineChart(Rect position, std::string title, TData data, TIcons icons,
 	layout.emplace_back(std::make_shared<CLabel>(p.x, p.y, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.64")));
 }
 
+Point LineChart::getPoint(int i, std::vector<float> data)
+{
+	float x = (static_cast<float>(chartArea.w) / static_cast<float>(maxDay - 1)) * static_cast<float>(i);
+	float y = static_cast<float>(chartArea.h) - (static_cast<float>(chartArea.h) / niceMaxVal) * data[i];
+	return Point(x, y);
+};
+
 void LineChart::updateStatusBar(const Point & cursorPosition)
 {
-	statusBar->moveTo(cursorPosition + Point(-statusBar->pos.w / 2, 20));
-	statusBar->fitToRect(pos, 10);
+	OBJECT_CONSTRUCTION;
+
 	Rect r(pos.x + chartArea.x, pos.y + chartArea.y, chartArea.w, chartArea.h);
-	statusBar->setEnabled(r.isInside(cursorPosition));
-	if(r.isInside(cursorPosition))
+	Point curPos = cursorPosition;
+	if(r.isInside(curPos))
 	{
-		float x = (static_cast<float>(maxDay - 1) / static_cast<float>(chartArea.w)) * (static_cast<float>(cursorPosition.x) - static_cast<float>(r.x)) + 1.0f;
-		float y = niceMaxVal - (niceMaxVal / static_cast<float>(chartArea.h)) * (static_cast<float>(cursorPosition.y) - static_cast<float>(r.y));
+		std::vector<std::pair<int, Point>> points;
+		for(const auto & line : data)
+		{
+			for(int i = 0; i < line.second.size(); i++)
+			{
+				Point p = getPoint(i, line.second) + chartArea.topLeft();
+				int len = Point(curPos.x - p.x - pos.x, curPos.y - p.y - pos.y).length();
+				points.push_back(std::make_pair(len, p));
+			}
+		}
+		std::sort(points.begin(), points.end(), [](const auto &a, const auto &b) { return a.first < b.first; });
+		if(points.size() && points[0].first < 15)
+		{
+			// Snap in with marker for nearest point
+			hoverMarker = std::make_shared<TransparentFilledRectangle>(Rect(points[0].second - Point(3, 3), Point(6, 6)), Colors::ORANGE);
+			curPos = points[0].second + pos;
+		}
+		else
+			hoverMarker.reset();
+
+		float x = (static_cast<float>(maxDay - 1) / static_cast<float>(chartArea.w)) * (static_cast<float>(curPos.x) - static_cast<float>(r.x)) + 1.0f;
+		float y = niceMaxVal - (niceMaxVal / static_cast<float>(chartArea.h)) * (static_cast<float>(curPos.y) - static_cast<float>(r.y));
+
 		statusBar->write(CGI->generaltexth->translate("core.genrltxt.64") + ": " + CStatisticScreen::getDay(x) + "   " + CGI->generaltexth->translate("vcmi.statisticWindow.value") + ": " + (static_cast<int>(y) > 0 ? std::to_string(static_cast<int>(y)) : std::to_string(y)));
 	}
+
+	statusBar->setEnabled(r.resize(1).isInside(curPos));
+	statusBar->moveTo(curPos + Point(-statusBar->pos.w / 2, 20));
+	statusBar->fitToRect(pos, 10);
+
 	setRedrawParent(true);
 	redraw();
 }

+ 6 - 0
client/mainmenu/CStatisticScreen.h

@@ -20,6 +20,7 @@ class ComboBox;
 class CSlider;
 class IImage;
 class CPicture;
+class TransparentFilledRectangle;
 
 using TData = std::vector<std::pair<ColorRGBA, std::vector<float>>>;
 using TIcons = std::vector<std::tuple<ColorRGBA, int, std::shared_ptr<IImage>, std::string>>; // Color, Day, Image, Helptext
@@ -118,13 +119,18 @@ class LineChart : public CIntObject
 	std::vector<std::shared_ptr<CIntObject>> layout;
 	std::shared_ptr<CGStatusBar> statusBar;
 	std::vector<std::shared_ptr<CPicture>> pictures;
+	std::shared_ptr<TransparentFilledRectangle> hoverMarker;
 
 	Rect chartArea;
 	float maxVal;
 	int niceMaxVal;
 	int maxDay;
 
+	TData data;
+
 	void updateStatusBar(const Point & cursorPosition);
+	// calculate points in chart
+	Point getPoint(int i, std::vector<float> data);
 public:
 	LineChart(Rect position, std::string title, TData data, TIcons icons, float maxY);
 

+ 6 - 1
client/mapView/MapView.cpp

@@ -210,7 +210,12 @@ void MapView::onMapZoomLevelChanged(int stepsChange, bool useDeadZone)
 void MapView::onViewMapActivated()
 {
 	controller->activateAdventureContext();
-	controller->setTileSize(Point(32, 32));
+
+	int zoom = settings["adventure"]["tileZoom"].Integer();
+	if(zoom)
+		controller->setTileSize(Point(zoom, zoom));
+	else
+		controller->setTileSize(Point(32, 32));
 }
 
 PuzzleMapView::PuzzleMapView(const Point & offset, const Point & dimensions, const int3 & tileToCenter)

+ 13 - 4
client/mapView/MapViewController.cpp

@@ -72,7 +72,7 @@ void MapViewController::setViewCenter(const Point & position, int level)
 		adventureInt->onMapViewMoved(model->getTilesTotalRect(), model->getLevel());
 }
 
-void MapViewController::setTileSize(const Point & tileSize)
+void MapViewController::setTileSize(const Point & tileSize, bool setTarget)
 {
 	Point oldSize = model->getSingleTileSize();
 	model->setTileSize(tileSize);
@@ -87,6 +87,9 @@ void MapViewController::setTileSize(const Point & tileSize)
 
 	// force update of view center since changing tile size may invalidated it
 	setViewCenter(newViewCenter, model->getLevel());
+	
+	if(setTarget)
+		targetTileSize = tileSize;
 }
 
 void MapViewController::modifyTileSize(int stepsChange, bool useDeadZone)
@@ -125,7 +128,7 @@ void MapViewController::modifyTileSize(int stepsChange, bool useDeadZone)
 			if(actualZoom.y >= defaultTileSize - zoomTileDeadArea && actualZoom.y <= defaultTileSize + zoomTileDeadArea)
 				actualZoom.y = defaultTileSize;
 		}
-		
+
 		bool isInDeadZone = targetTileSize != actualZoom || actualZoom == Point(defaultTileSize, defaultTileSize);
 
 		if(!wasInDeadZone && isInDeadZone)
@@ -133,7 +136,13 @@ void MapViewController::modifyTileSize(int stepsChange, bool useDeadZone)
 
 		wasInDeadZone = isInDeadZone;
 
-		setTileSize(actualZoom);
+		setTileSize(actualZoom, false);
+
+		if (adventureContext)
+		{
+			Settings tileZoom = settings.write["adventure"]["tileZoom"];
+			tileZoom->Integer() = actualZoom.x;
+		}
 	}
 }
 
@@ -224,7 +233,7 @@ void MapViewController::updateState()
 		adventureContext->settingShowVisitable = settings["session"]["showVisitable"].Bool();
 		adventureContext->settingShowBlocked = settings["session"]["showBlocked"].Bool();
 		adventureContext->settingSpellRange = settings["session"]["showSpellRange"].Bool();
-		adventureContext->settingTextOverlay = GH.isKeyboardAltDown() || GH.input().getNumTouchFingers() == 2;
+		adventureContext->settingTextOverlay = (GH.isKeyboardAltDown() || GH.input().getNumTouchFingers() == 2) && settings["general"]["enableOverlay"].Bool();
 	}
 }
 

+ 1 - 1
client/mapView/MapViewController.h

@@ -96,7 +96,7 @@ public:
 
 	void setViewCenter(const int3 & position);
 	void setViewCenter(const Point & position, int level);
-	void setTileSize(const Point & tileSize);
+	void setTileSize(const Point & tileSize, bool setTarget = true);
 	void modifyTileSize(int stepsChange, bool useDeadZone);
 	void tick(uint32_t timePassed);
 	void afterRender();

+ 42 - 13
client/renderSDL/SDLImage.cpp

@@ -20,6 +20,8 @@
 #include "../gui/CGuiHandler.h"
 #include "../render/IScreenHandler.h"
 
+#include "../../lib/CConfigHandler.h"
+
 #include <tbb/parallel_for.h>
 #include <tbb/task_arena.h>
 
@@ -93,7 +95,9 @@ SDLImageShared::SDLImageShared(const ImagePath & filename)
 
 void SDLImageShared::scaledDraw(SDL_Surface * where, SDL_Palette * palette, const Point & scaleTo, const Point & dest, const Rect * src, const ColorRGBA & colorMultiplier, uint8_t alpha, EImageBlitMode mode) const
 {
-	assert(upscalingInProgress == false);
+	if(upscalingInProgress)
+		throw std::runtime_error("Attempt to access images that is still being loaded!");
+
 	if (!surf)
 		return;
 
@@ -154,7 +158,9 @@ void SDLImageShared::scaledDraw(SDL_Surface * where, SDL_Palette * palette, cons
 
 void SDLImageShared::draw(SDL_Surface * where, SDL_Palette * palette, const Point & dest, const Rect * src, const ColorRGBA & colorMultiplier, uint8_t alpha, EImageBlitMode mode) const
 {
-	assert(upscalingInProgress == false);
+	if(upscalingInProgress)
+		throw std::runtime_error("Attempt to access images that is still being loaded!");
+
 	if (!surf)
 		return;
 
@@ -221,7 +227,9 @@ void SDLImageShared::optimizeSurface()
 
 std::shared_ptr<const ISharedImage> SDLImageShared::scaleInteger(int factor, SDL_Palette * palette, EImageBlitMode mode) const
 {
-	assert(upscalingInProgress == false);
+	if(upscalingInProgress)
+		throw std::runtime_error("Attempt to access images that is still being loaded!");
+
 	if (factor <= 0)
 		throw std::runtime_error("Unable to scale by integer value of " + std::to_string(factor));
 
@@ -264,7 +272,10 @@ SDLImageShared::SDLImageShared(const SDLImageShared * from, int integerScaleFact
 		upscalingInProgress = false;
 	};
 
-	upscalingArena.enqueue(scalingTask);
+	if(settings["video"]["asyncUpscaling"].Bool())
+		upscalingArena.enqueue(scalingTask);
+	else
+		scalingTask();
 }
 
 bool SDLImageShared::isLoading() const
@@ -274,7 +285,9 @@ bool SDLImageShared::isLoading() const
 
 std::shared_ptr<const ISharedImage> SDLImageShared::scaleTo(const Point & size, SDL_Palette * palette) const
 {
-	assert(upscalingInProgress == false);
+	if(upscalingInProgress)
+		throw std::runtime_error("Attempt to access images that is still being loaded!");
+
 	if (palette && surf->format->palette)
 		SDL_SetSurfacePalette(surf, palette);
 
@@ -310,7 +323,9 @@ void SDLImageShared::exportBitmap(const boost::filesystem::path& path, SDL_Palet
 	directory.remove_filename();
 	boost::filesystem::create_directories(directory);
 
-	assert(upscalingInProgress == false);
+	if(upscalingInProgress)
+		throw std::runtime_error("Attempt to access images that is still being loaded!");
+
 	if (!surf)
 		return;
 
@@ -323,7 +338,9 @@ void SDLImageShared::exportBitmap(const boost::filesystem::path& path, SDL_Palet
 
 bool SDLImageShared::isTransparent(const Point & coords) const
 {
-	assert(upscalingInProgress == false);
+	if(upscalingInProgress)
+		throw std::runtime_error("Attempt to access images that is still being loaded!");
+
 	if (surf)
 		return CSDL_Ext::isTransparent(surf, coords.x - margins.x, coords.y	- margins.y);
 	else
@@ -332,7 +349,9 @@ bool SDLImageShared::isTransparent(const Point & coords) const
 
 Rect SDLImageShared::contentRect() const
 {
-	assert(upscalingInProgress == false);
+	if(upscalingInProgress)
+		throw std::runtime_error("Attempt to access images that is still being loaded!");
+
 	auto tmpMargins = margins;
 	auto tmpSize = Point(surf->w, surf->h);
 	return Rect(tmpMargins, tmpSize);
@@ -340,7 +359,9 @@ Rect SDLImageShared::contentRect() const
 
 const SDL_Palette * SDLImageShared::getPalette() const
 {
-	assert(upscalingInProgress == false);
+	if(upscalingInProgress)
+		throw std::runtime_error("Attempt to access images that is still being loaded!");
+
 	if (!surf)
 		return nullptr;
 	return surf->format->palette;
@@ -348,13 +369,17 @@ const SDL_Palette * SDLImageShared::getPalette() const
 
 Point SDLImageShared::dimensions() const
 {
-	assert(upscalingInProgress == false);
+	if(upscalingInProgress)
+		throw std::runtime_error("Attempt to access images that is still being loaded!");
+
 	return fullSize;
 }
 
 std::shared_ptr<const ISharedImage> SDLImageShared::horizontalFlip() const
 {
-	assert(upscalingInProgress == false);
+	if(upscalingInProgress)
+		throw std::runtime_error("Attempt to access images that is still being loaded!");
+
 	if (!surf)
 		return shared_from_this();
 
@@ -370,7 +395,9 @@ std::shared_ptr<const ISharedImage> SDLImageShared::horizontalFlip() const
 
 std::shared_ptr<const ISharedImage> SDLImageShared::verticalFlip() const
 {
-	assert(upscalingInProgress == false);
+	if(upscalingInProgress)
+		throw std::runtime_error("Attempt to access images that is still being loaded!");
+
 	if (!surf)
 		return shared_from_this();
 
@@ -387,7 +414,9 @@ std::shared_ptr<const ISharedImage> SDLImageShared::verticalFlip() const
 // Keep the original palette, in order to do color switching operation
 void SDLImageShared::savePalette()
 {
-	assert(upscalingInProgress == false);
+	if(upscalingInProgress)
+		throw std::runtime_error("Attempt to access images that is still being loaded!");
+
 	// For some images that don't have palette, skip this
 	if(surf->format->palette == nullptr)
 		return;

+ 10 - 2
client/renderSDL/SDLImageScaler.cpp

@@ -12,6 +12,7 @@
 
 #include "SDL_Extensions.h"
 
+#include "../gui/CGuiHandler.h"
 #include "../CMT.h"
 #include "../xBRZ/xbrz.h"
 
@@ -227,12 +228,19 @@ SDLImageScaler::SDLImageScaler(SDL_Surface * surf, const Rect & virtualDimension
 		SDL_FreeSurface(intermediate);
 		intermediate = SDL_ConvertSurfaceFormat(surf, SDL_PIXELFORMAT_ARGB8888, 0);
 	}
+
+	if (intermediate == surf)
+		throw std::runtime_error("Scaler uses same surface as input!");
 }
 
 SDLImageScaler::~SDLImageScaler()
 {
-	SDL_FreeSurface(intermediate);
-	SDL_FreeSurface(ret);
+	GH.dispatchMainThread([surface = intermediate]()
+	{
+		// potentially SDL bug, execute SDL_FreeSurface in main thread to avoid thread races to its internal state
+		// may be fixed somewhere between 2.26.5 - 2.30
+		SDL_FreeSurface(surface);
+	});
 }
 
 SDL_Surface * SDLImageScaler::acquireResultSurface()

+ 4 - 9
client/renderSDL/ScalableImage.cpp

@@ -271,11 +271,6 @@ void ScalableImageShared::draw(SDL_Surface * where, const Point & dest, const Re
 		return images[index];
 	};
 
-	const auto & flipAndDraw = [&](FlippedImages & images, const ColorRGBA & colorMultiplier, uint8_t alphaValue){
-
-		getFlippedImage(images)->draw(where, parameters.palette, dest, src, colorMultiplier, alphaValue, locator.layer);
-	};
-
 	bool shadowLoading = scaled.at(scalingFactor).shadow.at(0) && scaled.at(scalingFactor).shadow.at(0)->isLoading();
 	bool bodyLoading = scaled.at(scalingFactor).body.at(0) && scaled.at(scalingFactor).body.at(0)->isLoading();
 	bool overlayLoading = scaled.at(scalingFactor).overlay.at(0) && scaled.at(scalingFactor).overlay.at(0)->isLoading();
@@ -292,7 +287,7 @@ void ScalableImageShared::draw(SDL_Surface * where, const Point & dest, const Re
 	}
 
 	if (scaled.at(scalingFactor).shadow.at(0))
-		flipAndDraw(scaled.at(scalingFactor).shadow, Colors::WHITE_TRUE, parameters.alphaValue);
+		getFlippedImage(scaled.at(scalingFactor).shadow)->draw(where, parameters.palette, dest, src, Colors::WHITE_TRUE, parameters.alphaValue, locator.layer);
 
 	if (parameters.player != PlayerColor::CANNOT_DETERMINE && scaled.at(scalingFactor).playerColored.at(1+parameters.player.getNum()))
 	{
@@ -301,14 +296,14 @@ void ScalableImageShared::draw(SDL_Surface * where, const Point & dest, const Re
 	else
 	{
 		if (scaled.at(scalingFactor).body.at(0))
-			flipAndDraw(scaled.at(scalingFactor).body, parameters.colorMultiplier, parameters.alphaValue);
+			getFlippedImage(scaled.at(scalingFactor).body)->draw(where, parameters.palette, dest, src, parameters.colorMultiplier, parameters.alphaValue, locator.layer);
 
 		if (scaled.at(scalingFactor).bodyGrayscale.at(0) && parameters.effectColorMultiplier.a != ColorRGBA::ALPHA_TRANSPARENT)
-			flipAndDraw(scaled.at(scalingFactor).bodyGrayscale, parameters.effectColorMultiplier, parameters.alphaValue);
+			getFlippedImage(scaled.at(scalingFactor).bodyGrayscale)->draw(where, parameters.palette, dest, src, parameters.effectColorMultiplier, parameters.alphaValue, locator.layer);
 	}
 
 	if (scaled.at(scalingFactor).overlay.at(0))
-		flipAndDraw(scaled.at(scalingFactor).overlay, parameters.ovelayColorMultiplier, static_cast<int>(parameters.alphaValue) * parameters.ovelayColorMultiplier.a / 255);
+		getFlippedImage(scaled.at(scalingFactor).overlay)->draw(where, parameters.palette, dest, src, parameters.ovelayColorMultiplier, static_cast<int>(parameters.alphaValue) * parameters.ovelayColorMultiplier.a / 255, locator.layer);
 }
 
 const SDL_Palette * ScalableImageShared::getPalette() const

+ 5 - 0
client/widgets/Buttons.cpp

@@ -206,6 +206,11 @@ bool CButton::isHighlighted()
 	return getState() == EButtonState::HIGHLIGHTED;
 }
 
+bool CButton::isPressed()
+{
+	return getState() == EButtonState::PRESSED;
+}
+
 void CButton::setHoverable(bool on)
 {
 	hoverable = on;

+ 1 - 0
client/widgets/Buttons.h

@@ -105,6 +105,7 @@ public:
 	/// State modifiers
 	bool isBlocked();
 	bool isHighlighted();
+	bool isPressed();
 
 	/// Constructor
 	CButton(Point position, const AnimationPath & defName, const std::pair<std::string, std::string> & help,

+ 2 - 1
client/widgets/Images.cpp

@@ -442,7 +442,8 @@ void CShowableAnim::blitImage(size_t frame, size_t group, Canvas & to)
 	if(img)
 	{
 		img->setAlpha(alpha);
-		img->setOverlayColor(Colors::TRANSPARENCY);
+		if (getModeForFlags(flags) == EImageBlitMode::WITH_SHADOW_AND_SELECTION)
+			img->setOverlayColor(Colors::TRANSPARENCY);
 		to.draw(img, pos.topLeft(), src);
 	}
 }

+ 45 - 25
client/widgets/Slider.cpp

@@ -21,6 +21,13 @@
 
 void CSlider::mouseDragged(const Point & cursorPosition, const Point & lastUpdateDistance)
 {
+	bool onControl = pos.isInside(cursorPosition) && !left->pos.isInside(cursorPosition) && !right->pos.isInside(cursorPosition);
+	if(!onControl && !slider->isPressed())
+		return;
+
+	if(onControl && !slider->isPressed())
+		slider->clickPressed(cursorPosition);
+
 	double newPosition = 0;
 	if(getOrientation() == Orientation::HORIZONTAL)
 	{
@@ -129,44 +136,57 @@ void CSlider::scrollTo(int to, bool callCallbacks)
 		moved(getValue());
 }
 
-void CSlider::clickPressed(const Point & cursorPosition)
+double CSlider::getClickPos(const Point & cursorPosition)
 {
-	if(!slider->isBlocked())
+	double pw = 0;
+	double rw = 0;
+	if(getOrientation() == Orientation::HORIZONTAL)
 	{
-		double pw = 0;
-		double rw = 0;
-		if(getOrientation() == Orientation::HORIZONTAL)
-		{
-			pw = cursorPosition.x-pos.x-25;
-			rw = pw / static_cast<double>(pos.w - 48);
-		}
-		else
-		{
-			pw = cursorPosition.y-pos.y-24;
-			rw = pw / (pos.h-48);
-		}
+		pw = cursorPosition.x-pos.x-25;
+		rw = pw / static_cast<double>(pos.w - 48);
+	}
+	else
+	{
+		pw = cursorPosition.y-pos.y-24;
+		rw = pw / (pos.h-48);
+	}
 
-		// click on area covered by buttons -> ignore, will be handled by left/right buttons
-		if (!vstd::iswithin(rw, 0, 1))
-			return;
+	return rw;
+}
 
-		slider->clickPressed(cursorPosition);
-		scrollTo((int)(rw * positions  +  0.5));
+void CSlider::clickPressed(const Point & cursorPosition)
+{
+	bool onControl = pos.isInside(cursorPosition) && !left->pos.isInside(cursorPosition) && !right->pos.isInside(cursorPosition);
+	if(!onControl)
 		return;
-	}
+
+	if(slider->isBlocked())
+		return;
+
+	// click on area covered by buttons -> ignore, will be handled by left/right buttons
+	auto rw = getClickPos(cursorPosition);
+	if (!vstd::iswithin(rw, 0, 1))
+		return;
+
+	slider->clickPressed(cursorPosition);
+	scrollTo((int)(rw * positions + 0.5));
+}
+
+void CSlider::clickReleased(const Point & cursorPosition)
+{
+	if(slider->isBlocked())
+		return;
+
+	slider->clickReleased(cursorPosition);
 }
 
 bool CSlider::receiveEvent(const Point &position, int eventType) const
 {
 	if (eventType == LCLICK)
-	{
-		return pos.isInside(position) && !left->pos.isInside(position) && !right->pos.isInside(position);
-	}
+		return true; //capture "clickReleased" also outside of control
 
 	if(eventType != WHEEL && eventType != GESTURE)
-	{
 		return CIntObject::receiveEvent(position, eventType);
-	}
 
 	if (!scrollBounds)
 		return true;

+ 3 - 0
client/widgets/Slider.h

@@ -38,6 +38,8 @@ class CSlider : public Scrollable
 
 	void updateSliderPos();
 
+	double getClickPos(const Point & cursorPosition);
+
 public:
 	enum EStyle
 	{
@@ -71,6 +73,7 @@ public:
 	bool receiveEvent(const Point & position, int eventType) const override;
 	void keyPressed(EShortcut key) override;
 	void clickPressed(const Point & cursorPosition) override;
+	void clickReleased(const Point & cursorPosition) override;
 	void mouseDragged(const Point & cursorPosition, const Point & lastUpdateDistance) override;
 	void gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) override;
 	void showAll(Canvas & to) override;

+ 15 - 11
client/windows/CCastleInterface.cpp

@@ -177,7 +177,7 @@ void CBuildingRect::show(Canvas & to)
 {
 	uint32_t stageDelay = BUILDING_APPEAR_TIMEPOINT;
 
-	bool showTextOverlay = GH.isKeyboardAltDown() || GH.input().getNumTouchFingers() == 2;
+	bool showTextOverlay = (GH.isKeyboardAltDown() || GH.input().getNumTouchFingers() == 2) && settings["general"]["enableOverlay"].Bool();
 
 	if(stateTimeCounter < BUILDING_APPEAR_TIMEPOINT)
 	{
@@ -770,7 +770,7 @@ void CCastleBuildings::show(Canvas & to)
 {
 	CIntObject::show(to);
 
-	bool showTextOverlay = GH.isKeyboardAltDown() || GH.input().getNumTouchFingers() == 2;
+	bool showTextOverlay = (GH.isKeyboardAltDown() || GH.input().getNumTouchFingers() == 2) && settings["general"]["enableOverlay"].Bool();
 	if(showTextOverlay)
 		drawOverlays(to, buildings);
 }
@@ -814,22 +814,26 @@ const CGHeroInstance * CCastleBuildings::getHero()
 
 void CCastleBuildings::buildingClicked(BuildingID building)
 {
-	BuildingID buildingToEnter = building;
-	for(;;)
+	std::vector<BuildingID> buildingsToTest;
+
+	for(BuildingID buildingToEnter = building;;)
 	{
 		const CBuilding *b = town->getTown()->buildings.find(buildingToEnter)->second;
 
-		if (buildingTryActivateCustomUI(buildingToEnter, building))
-			return;
-
+		buildingsToTest.push_back(buildingToEnter);
 		if (!b->upgrade.hasValue())
-		{
-			enterBuilding(building);
-			return;
-		}
+			break;
 
 		buildingToEnter = b->upgrade;
 	}
+
+	for(BuildingID buildingToEnter : boost::adaptors::reverse(buildingsToTest))
+	{
+		if (buildingTryActivateCustomUI(buildingToEnter, building))
+			return;
+	}
+
+	enterBuilding(building);
 }
 
 bool CCastleBuildings::buildingTryActivateCustomUI(BuildingID buildingToTest, BuildingID buildingTarget)

+ 2 - 2
client/windows/CKingdomInterface.cpp

@@ -475,7 +475,7 @@ CKingdomInterface::CKingdomInterface()
 	statusbar = CGStatusBar::create(std::make_shared<CPicture>(ImagePath::builtin("KSTATBAR"), 10,pos.h - 45));
 	resdatabar = std::make_shared<CResDataBar>(ImagePath::builtin("KRESBAR"), 7, 111+footerPos, 29, 3, 76, 81);
 
-	activateTab(persistentStorage["gui"]["lastKindomInterface"].Integer());
+	activateTab(settings["general"]["lastKindomInterface"].Integer());
 }
 
 void CKingdomInterface::generateObjectsList(const std::vector<const CGObjectInstance * > &ownedObjects)
@@ -640,7 +640,7 @@ void CKingdomInterface::generateButtons()
 
 void CKingdomInterface::activateTab(size_t which)
 {
-	Settings s = persistentStorage.write["gui"]["lastKindomInterface"];
+	Settings s = settings.write["general"]["lastKindomInterface"];
 	s->Integer() = which;
 
 	btnHeroes->block(which == 0);

+ 8 - 0
client/windows/settings/GeneralOptionsTab.cpp

@@ -160,6 +160,10 @@ GeneralOptionsTab::GeneralOptionsTab()
 	{
 		setBoolSetting("general", "hapticFeedback", value);
 	});
+	addCallback("enableOverlayChanged", [](bool value)
+	{
+		setBoolSetting("general", "enableOverlay", value);
+	});
 	addCallback("enableUiEnhancementsChanged", [](bool value)
 	{
 		setBoolSetting("general", "enableUiEnhancements", value);
@@ -223,6 +227,10 @@ GeneralOptionsTab::GeneralOptionsTab()
 	if (hapticFeedbackCheckbox)
 		hapticFeedbackCheckbox->setSelected(settings["general"]["hapticFeedback"].Bool());
 
+	std::shared_ptr<CToggleButton> enableOverlayCheckbox = widget<CToggleButton>("enableOverlayCheckbox");
+	if (enableOverlayCheckbox)
+		enableOverlayCheckbox->setSelected(settings["general"]["enableOverlay"].Bool());
+
 	std::shared_ptr<CToggleButton> enableUiEnhancementsCheckbox = widget<CToggleButton>("enableUiEnhancementsCheckbox");
 	if (enableUiEnhancementsCheckbox)
 		enableUiEnhancementsCheckbox->setSelected(settings["general"]["enableUiEnhancements"].Bool());

+ 1 - 1
cmake_modules/VersionDefinition.cmake

@@ -1,6 +1,6 @@
 set(VCMI_VERSION_MAJOR 1)
 set(VCMI_VERSION_MINOR 6)
-set(VCMI_VERSION_PATCH 6)
+set(VCMI_VERSION_PATCH 7)
 add_definitions(
 	-DVCMI_VERSION_MAJOR=${VCMI_VERSION_MAJOR}
 	-DVCMI_VERSION_MINOR=${VCMI_VERSION_MINOR}

+ 22 - 3
config/schemas/settings.json

@@ -42,7 +42,9 @@
 				"savePrefix",
 				"startTurnAutosave",
 				"enableUiEnhancements",
-				"audioMuteFocus"
+				"audioMuteFocus",
+				"enableOverlay",
+				"lastKindomInterface"
 			],
 			"properties" : {
 				"playerName" : {
@@ -146,6 +148,14 @@
 				"audioMuteFocus" : {
 					"type": "boolean",
 					"default": false
+				},
+				"enableOverlay" : {
+					"type": "boolean",
+					"default": true
+				},
+				"lastKindomInterface" : {
+					"type" : "number",
+					"default" : 0
 				}
 			}
 		},
@@ -189,7 +199,8 @@
 				"upscalingFilter",
 				"fontUpscalingFilter",
 				"downscalingFilter",
-				"allowPortrait"
+				"allowPortrait",
+				"asyncUpscaling"
 			],
 			"properties" : {
 				"resolution" : {
@@ -281,6 +292,10 @@
 				"allowPortrait" : {
 					"type" : "boolean",
 					"default" : false
+				},
+				"asyncUpscaling" : {
+					"type" : "boolean",
+					"default" : true
 				}
 			}
 		},
@@ -362,7 +377,7 @@
 			"type" : "object",
 			"additionalProperties" : false,
 			"default" : {},
-			"required" : [ "heroMoveTime", "enemyMoveTime", "scrollSpeedPixels", "heroReminder", "quickCombat", "objectAnimation", "terrainAnimation", "forceQuickCombat", "borderScroll", "leftButtonDrag", "rightButtonDrag", "smoothDragging", "backgroundDimLevel", "hideBackground", "backgroundDimSmallWindows" ],
+			"required" : [ "heroMoveTime", "enemyMoveTime", "scrollSpeedPixels", "heroReminder", "quickCombat", "objectAnimation", "terrainAnimation", "forceQuickCombat", "borderScroll", "leftButtonDrag", "rightButtonDrag", "smoothDragging", "backgroundDimLevel", "hideBackground", "backgroundDimSmallWindows", "tileZoom" ],
 			"properties" : {
 				"heroMoveTime" : {
 					"type" : "number",
@@ -426,6 +441,10 @@
 				"backgroundDimSmallWindows" : {
 					"type" : "boolean",
 					"default" : false
+				},
+				"tileZoom" : {
+					"type" : "number",
+					"default" : 32
 				}
 			}
 		},

+ 23 - 22
config/widgets/lobbyWindow.json

@@ -26,14 +26,14 @@
 	},
 	
 	
-	"width": 1024,
+	"width": 800,
 	"height": 600,
 	
 	"items":
 	[
 		{
 			"type": "backgroundTexture",
-			"rect": {"w": 1024, "h": 600}
+			"rect": {"w": 800, "h": 600}
 		},
 		
 		{
@@ -69,36 +69,36 @@
 		
 		{
 			"type": "areaFilled",
-			"rect": {"x": 270, "y": 50, "w": 150, "h": 180}
+			"rect": {"x": 258, "y": 50, "w": 150, "h": 180}
 		},
 		{
 			"name" : "headerChannelList",
 			"type": "labelTitle",
-			"position": {"x": 280, "y": 53}
+			"position": {"x": 268, "y": 53}
 		},
 		{
 			"type" : "lobbyItemList",
 			"name" : "channelList",
 			"itemType" : "channel",
-			"position" : { "x" : 272, "y" : 68 },
+			"position" : { "x" : 260, "y" : 68 },
 			"itemOffset" : { "x" : 0, "y" : 40 },
 			"visibleAmount" : 4
 		},
 		
 		{
 			"type": "areaFilled",
-			"rect": {"x": 270, "y": 250, "w": 150, "h": 340}
+			"rect": {"x": 258, "y": 250, "w": 150, "h": 340}
 		},
 		{
 			"name" : "headerMatchList",
 			"type": "labelTitle",
-			"position": {"x": 280, "y": 253}
+			"position": {"x": 268, "y": 253}
 		},
 		{
 			"type" : "lobbyItemList",
 			"name" : "matchList",
 			"itemType" : "match",
-			"position" : { "x" : 272, "y" : 268 },
+			"position" : { "x" : 260, "y" : 268 },
 			"itemOffset" : { "x" : 0, "y" : 40 },
 			"sliderPosition" : { "x" : 130, "y" : 0 },
 			"sliderSize" : { "x" : 320, "y" : 320 },
@@ -107,12 +107,13 @@
 
 		{
 			"type": "areaFilled",
-			"rect": {"x": 430, "y": 50, "w": 430, "h": 515}
+			"rect": {"x": 411, "y": 50, "w": 232, "h": 515}
 		},
 		{
 			"name" : "headerGameChat",
 			"type": "labelTitle",
-			"position": {"x": 440, "y": 53}
+			"position": {"x": 421, "y": 53},
+			"maxWidth": 210
 		},
 		{
 			"type": "textBox",
@@ -121,33 +122,33 @@
 			"alignment": "left",
 			"color": "white",
 			"blueTheme" : true,
-			"rect": {"x": 440, "y": 68, "w": 418, "h": 495}
+			"rect": {"x": 421, "y": 68, "w": 210, "h": 495}
 		},
 		
 		{
 			"type": "areaFilled",
-			"rect": {"x": 430, "y": 565, "w": 397, "h": 25}
+			"rect": {"x": 411, "y": 565, "w": 196, "h": 25}
 		},
 		{
 			"name" : "messageInput",
 			"type": "textInput",
-			"rect": {"x": 440, "y": 568, "w": 377, "h": 20}
+			"rect": {"x": 421, "y": 568, "w": 176, "h": 20}
 		},
 		
 		{
 			"type": "areaFilled",
-			"rect": {"x": 870, "y": 50, "w": 150, "h": 540}
+			"rect": {"x": 646, "y": 50, "w": 150, "h": 540}
 		},
 		{
 			"name": "headerAccountList",
 			"type": "labelTitle",
-			"position": {"x": 880, "y": 53}
+			"position": {"x": 656, "y": 53}
 		},
 		{
 			"type" : "lobbyItemList",
 			"name" : "accountList",
 			"itemType" : "account",
-			"position" : { "x" : 872, "y" : 68 },
+			"position" : { "x" : 648, "y" : 68 },
 			"itemOffset" : { "x" : 0, "y" : 40 },
 			"sliderPosition" : { "x" : 130, "y" : 0 },
 			"sliderSize" : { "x" : 520, "y" : 520 },
@@ -156,7 +157,7 @@
 
 		{
 			"type": "button",
-			"position": {"x": 870, "y": 10},
+			"position": {"x": 646, "y": 10},
 			"image": "lobbyHideWindow",
 			"help": "core.help.288",
 			"callback": "closeWindow",
@@ -175,9 +176,9 @@
 		
 		{
 			"type": "button",
-			"position": {"x": 828, "y": 565},
+			"position": {"x": 610, "y": 565},
 			"image": "lobbySendMessage",
-			"help": "core.help.288",
+			"help": "vcmi.lobby.channel.sendMessage",
 			"callback": "sendMessage",
 			"hotkey": "globalAccept",
 			"items":
@@ -191,9 +192,9 @@
 		
 		{
 			"type": "button",
-			"position": {"x": 10, "y": 555},
+			"position": {"x": 5, "y": 556},
 			"image": "lobbyCreateRoom",
-			"help": "core.help.288",
+			"help": "vcmi.lobby.room.create",
 			"callback": "createGameRoom",
 			"items":
 			[
@@ -202,7 +203,7 @@
 					"font": "medium",
 					"alignment": "center",
 					"color": "yellow",
-					"text": "vcmi.lobby.room.create"
+					"text": "vcmi.lobby.room.create.hover"
 				}
 			]
 		},

+ 212 - 0
config/widgets/lobbyWindowWide.json

@@ -0,0 +1,212 @@
+{
+	"customTypes" : {
+		"labelTitleMain" : {
+			"type": "label",
+			"font": "big",
+			"alignment": "left",
+			"color": "yellow"
+		},
+		"labelTitle" : {
+			"type": "label",
+			"font": "small",
+			"alignment": "left",
+			"color": "yellow"
+		},
+		"backgroundTexture" : {
+			"type": "texture",
+			"font": "tiny",
+			"color" : "blue",
+			"image": "DIBOXBCK"
+		},
+		"areaFilled":{
+			"type": "transparentFilledRectangle",
+			"color": [0, 0, 0, 75],
+			"colorLine": [64, 80, 128, 255]
+		}
+	},
+	
+	
+	"width": 1024,
+	"height": 600,
+	
+	"items":
+	[
+		{
+			"type": "backgroundTexture",
+			"rect": {"w": 1024, "h": 600}
+		},
+		
+		{
+			"type": "areaFilled",
+			"rect": {"x": 5, "y": 5, "w": 250, "h": 40}
+		},
+		{
+			"name" : "accountNameLabel",
+			"type": "labelTitleMain",
+			"position": {"x": 15, "y": 10},
+			"maxWidth": 230
+		},
+
+		{
+			"type": "areaFilled",
+			"rect": {"x": 5, "y": 50, "w": 250, "h": 500}
+		},
+		{
+			"name" : "headerRoomList",
+			"type": "labelTitle",
+			"position": {"x": 15, "y": 53}
+		},
+		{
+			"type" : "lobbyItemList",
+			"name" : "roomList",
+			"itemType" : "room",
+			"position" : { "x" : 7, "y" : 68 },
+			"itemOffset" : { "x" : 0, "y" : 40 },
+			"sliderPosition" : { "x" : 230, "y" : 0 },
+			"sliderSize" : { "x" : 480, "y" : 480 },
+			"visibleAmount" : 12
+		},
+		
+		{
+			"type": "areaFilled",
+			"rect": {"x": 270, "y": 50, "w": 150, "h": 180}
+		},
+		{
+			"name" : "headerChannelList",
+			"type": "labelTitle",
+			"position": {"x": 280, "y": 53}
+		},
+		{
+			"type" : "lobbyItemList",
+			"name" : "channelList",
+			"itemType" : "channel",
+			"position" : { "x" : 272, "y" : 68 },
+			"itemOffset" : { "x" : 0, "y" : 40 },
+			"visibleAmount" : 4
+		},
+		
+		{
+			"type": "areaFilled",
+			"rect": {"x": 270, "y": 250, "w": 150, "h": 340}
+		},
+		{
+			"name" : "headerMatchList",
+			"type": "labelTitle",
+			"position": {"x": 280, "y": 253}
+		},
+		{
+			"type" : "lobbyItemList",
+			"name" : "matchList",
+			"itemType" : "match",
+			"position" : { "x" : 272, "y" : 268 },
+			"itemOffset" : { "x" : 0, "y" : 40 },
+			"sliderPosition" : { "x" : 130, "y" : 0 },
+			"sliderSize" : { "x" : 320, "y" : 320 },
+			"visibleAmount" : 8
+		},
+
+		{
+			"type": "areaFilled",
+			"rect": {"x": 430, "y": 50, "w": 430, "h": 515}
+		},
+		{
+			"name" : "headerGameChat",
+			"type": "labelTitle",
+			"position": {"x": 440, "y": 53},
+			"maxWidth": 418
+		},
+		{
+			"type": "textBox",
+			"name": "gameChat",
+			"font": "small",
+			"alignment": "left",
+			"color": "white",
+			"blueTheme" : true,
+			"rect": {"x": 440, "y": 68, "w": 418, "h": 495}
+		},
+		
+		{
+			"type": "areaFilled",
+			"rect": {"x": 430, "y": 565, "w": 397, "h": 25}
+		},
+		{
+			"name" : "messageInput",
+			"type": "textInput",
+			"rect": {"x": 440, "y": 568, "w": 377, "h": 20}
+		},
+		
+		{
+			"type": "areaFilled",
+			"rect": {"x": 870, "y": 50, "w": 150, "h": 540}
+		},
+		{
+			"name": "headerAccountList",
+			"type": "labelTitle",
+			"position": {"x": 880, "y": 53}
+		},
+		{
+			"type" : "lobbyItemList",
+			"name" : "accountList",
+			"itemType" : "account",
+			"position" : { "x" : 872, "y" : 68 },
+			"itemOffset" : { "x" : 0, "y" : 40 },
+			"sliderPosition" : { "x" : 130, "y" : 0 },
+			"sliderSize" : { "x" : 520, "y" : 520 },
+			"visibleAmount" : 13
+		},
+
+		{
+			"type": "button",
+			"position": {"x": 870, "y": 10},
+			"image": "lobbyHideWindow",
+			"help": "core.help.288",
+			"callback": "closeWindow",
+			"hotkey": "globalCancel",
+			"items":
+			[
+				{
+					"type": "label",
+					"font": "medium",
+					"alignment": "center",
+					"color": "yellow",
+					"text": "core.help.561.hover" // Back
+				}
+			]
+		},
+		
+		{
+			"type": "button",
+			"position": {"x": 827, "y": 565},
+			"image": "lobbySendMessage",
+			"help": "vcmi.lobby.channel.sendMessage",
+			"callback": "sendMessage",
+			"hotkey": "globalAccept",
+			"items":
+			[
+				{
+					"type": "picture",
+					"image": "lobby/iconSend"
+				}
+			]
+		},
+		
+		{
+			"type": "button",
+			"position": {"x": 5, "y": 555},
+			"image": "lobbyCreateRoom",
+			"help": "vcmi.lobby.room.create",
+			"callback": "createGameRoom",
+			"items":
+			[
+				{
+					"type": "label",
+					"font": "medium",
+					"alignment": "center",
+					"color": "yellow",
+					"text": "vcmi.lobby.room.create.hover"
+				}
+			]
+		},
+
+	]
+}

+ 9 - 0
config/widgets/settings/generalOptionsTab.json

@@ -67,6 +67,9 @@
 					"text": "vcmi.systemOptions.hapticFeedbackButton.hover",
 					"created" : "mobile"
 				},
+				{
+					"text": "vcmi.systemOptions.enableOverlayButton.hover"
+				},
 				{
 					"text": "vcmi.systemOptions.enableUiEnhancementsButton.hover"
 				}
@@ -98,6 +101,7 @@
 				{
 					"created" : "mobile"
 				},
+				{},
 				{}
 			]
 		},
@@ -159,6 +163,11 @@
 					"callback": "hapticFeedbackChanged",
 					"created" : "mobile"
 				},
+				{
+					"name": "enableOverlayCheckbox",
+					"help": "vcmi.systemOptions.enableOverlayButton",
+					"callback": "enableOverlayChanged"
+				},
 				{
 					"name": "enableUiEnhancementsCheckbox",
 					"help": "vcmi.systemOptions.enableUiEnhancementsButton",

+ 6 - 0
debian/changelog

@@ -1,3 +1,9 @@
+vcmi (1.6.7) jammy; urgency=medium
+
+  * New upstream release
+
+ -- Ivan Savenko <[email protected]>  Fri, 28 Feb 2025 12:00:00 +0200
+
 vcmi (1.6.6) jammy; urgency=medium
 
   * New upstream release

+ 1 - 1
docs/Readme.md

@@ -1,9 +1,9 @@
 # VCMI Project
 
 [![VCMI](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg?branch=develop&event=push)](https://github.com/vcmi/vcmi/actions/workflows/github.yml?query=branch%3Adevelop+event%3Apush)
-[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.6.4/total)](https://github.com/vcmi/vcmi/releases/tag/1.6.4)
 [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.6.5/total)](https://github.com/vcmi/vcmi/releases/tag/1.6.5)
 [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.6.6/total)](https://github.com/vcmi/vcmi/releases/tag/1.6.6)
+[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.6.7/total)](https://github.com/vcmi/vcmi/releases/tag/1.6.7)
 [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases)
 
 VCMI is an open-source recreation of Heroes of Might & Magic III engine, giving it new and extended possibilities.

+ 1 - 0
launcher/eu.vcmi.VCMI.metainfo.xml

@@ -90,6 +90,7 @@
 	</screenshots>
 	<launchable type="desktop-id">vcmilauncher.desktop</launchable>
 	<releases>
+		<release version="1.6.7" date="2025-02-28" type="stable"/>
 		<release version="1.6.6" date="2025-02-21" type="stable"/>
 		<release version="1.6.5" date="2025-02-03" type="stable"/>
 		<release version="1.6.4" date="2025-01-31" type="stable"/>

+ 11 - 11
launcher/translation/swedish.ts

@@ -307,17 +307,17 @@
     <message>
         <location filename="../modManager/cmodlistview_moc.cpp" line="482"/>
         <source>Context menu</source>
-        <translation type="unfinished"></translation>
+        <translation>Kontextmeny</translation>
     </message>
     <message>
         <location filename="../modManager/cmodlistview_moc.cpp" line="525"/>
         <source>Open directory</source>
-        <translation type="unfinished"></translation>
+        <translation>Öppna mapp</translation>
     </message>
     <message>
         <location filename="../modManager/cmodlistview_moc.cpp" line="530"/>
         <source>Open repository</source>
-        <translation type="unfinished"></translation>
+        <translation>Öppna repositorie</translation>
     </message>
     <message>
         <location filename="../modManager/cmodlistview_moc.cpp" line="783"/>
@@ -497,7 +497,7 @@ Installation framgångsrikt nedladdad?</translation>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="533"/>
         <source>Allow portrait mode</source>
-        <translation type="unfinished"></translation>
+        <translation>Tillåt porträttläge</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="811"/>
@@ -1175,7 +1175,7 @@ Orsak till fel: </translation>
     <message>
         <location filename="../innoextract.cpp" line="55"/>
         <source>Not a supported Inno Setup installer!</source>
-        <translation>Inno Setup-installationsprogrammet stöds inte!</translation>
+        <translation>Inte en stödd Inno Setup-installationsfil!</translation>
     </message>
     <message>
         <location filename="../innoextract.cpp" line="58"/>
@@ -1258,7 +1258,7 @@ Bin (%n byte):
         <source>Unknown files! Maybe files are corrupted? Please download again.
 
 %1</source>
-        <translation>Okända filer! Filerna kanske är skadade? Prova med att ladda ner filen/filerna igen och försök igen.
+        <translation>Okända filer! Filerna kanske är skadade? Prova med att ladda ner filen/filerna på nytt och försök igen.
 
 %1</translation>
     </message>
@@ -1705,7 +1705,7 @@ Orsak: %2</translation>
     <message>
         <location filename="../startGame/StartGameTab.ui" line="657"/>
         <source>You are using the latest version</source>
-        <translation>Du har den senaste versionen</translation>
+        <translation>Du använder den senaste versionen</translation>
     </message>
     <message>
         <location filename="../startGame/StartGameTab.ui" line="37"/>
@@ -1794,7 +1794,7 @@ Orsak: %2</translation>
     <message>
         <location filename="../startGame/StartGameTab.cpp" line="251"/>
         <source>Select files (configs, mods, maps, campaigns, gog files) to install...</source>
-        <translation>Välj filer (konfigurations-, modd-, kart-, kampanj-och GOG-filer) som ska installeras...</translation>
+        <translation>Välj filer (konfigurations-, modd-, kart-, kampanj-och GOG-filer) som du vill installera...</translation>
     </message>
     <message>
         <location filename="../startGame/StartGameTab.cpp" line="294"/>
@@ -1823,7 +1823,7 @@ Orsak: %2</translation>
     <message>
         <location filename="../startGame/StartGameTab.cpp" line="319"/>
         <source>Translation of Heroes III into your language is installed, but has been turned off. Use this option to enable it.</source>
-        <translation>Översättning av Heroes III till ditt språk är installerat men har stängts av. Använd det här alternativet för att aktivera det.</translation>
+        <translation>Översättning av Heroes III till ditt språk är installerat men har stängts av. Använd det här alternativet för att aktivera översättningen.</translation>
     </message>
     <message>
         <location filename="../startGame/StartGameTab.cpp" line="329"/>
@@ -1838,7 +1838,7 @@ VARNING: I vissa fall kanske uppdaterade versioner av moddar inte är kompatibla
         <location filename="../startGame/StartGameTab.cpp" line="341"/>
         <source>If you own Heroes Chronicles on gog.com, you can use offline backup installers provided by gog to import Heroes Chronicles data into VCMI as custom campaigns.
 To import Heroes Chronicles, download offline backup installer of each chronicle that you wish to install, select &apos;Import files&apos; option and select downloaded file. This will generate and install mod for VCMI that contains imported chronicles</source>
-        <translation>Om du äger Hjältarnas krönikor (Heroes Chronicles) på gog.com kan du använda &quot;offline backup game installers&quot; som tillhandahålls av GOG för att importera Heroes Chronicles data till VCMI så att man kan spela dem i VCMI.
+        <translation>Om du äger Hjältarnas krönikor (Heroes Chronicles) på gog.com kan du använda &quot;offline backup game installers&quot; som tillhandahålls av GOG för att importera Heroes Chronicles data till VCMI så att man kan spela dem i VCMI.
 För att importera Hjältarnas krönikor (Heroes Chronicles) ska du först ladda ner &quot;offline backup game installers&quot; av varje krönika som du vill installera. Välj alternativet &apos;Importera filer&apos; och välj nedladdad fil. Detta kommer att generera och installera modden för VCMI som innehåller importerade krönikor</translation>
     </message>
     <message>
@@ -1893,7 +1893,7 @@ För att lösa problemet måste du kopiera de saknade datafilerna från Heroes I
     <message>
         <location filename="../updatedialog_moc.ui" line="71"/>
         <source>You have the latest version</source>
-        <translation>Du har den senaste versionen</translation>
+        <translation>Du har senaste versionen</translation>
     </message>
     <message>
         <location filename="../updatedialog_moc.ui" line="94"/>

+ 11 - 0
lib/filesystem/CZipLoader.cpp

@@ -224,7 +224,18 @@ bool ZipArchive::extract(const boost::filesystem::path & where, const std::strin
 
 	std::fstream destFile(fullName.c_str(), std::ios::out | std::ios::binary);
 	if (!destFile.good())
+	{
+#ifdef VCMI_WINDOWS
+		if (fullName.size() < 260)
+			logGlobal->error("Failed to open file '%s'", fullName.c_str());
+		else
+			logGlobal->error("Failed to open file with long path '%s' (%d characters)", fullName.c_str(), fullName.size());
+#else
+		logGlobal->error("Failed to open file '%s'", fullName.c_str());
+#endif
+
 		return false;
+	}
 
 	if (!extractCurrent(archive, destFile))
 		return false;

+ 12 - 2
lib/mapObjectConstructors/CObjectClassesHandler.cpp

@@ -177,7 +177,12 @@ void CObjectClassesHandler::loadSubObject(const std::string & scope, const std::
 
 	registerObject(scope, baseObject->getJsonKey(), subObject->getSubTypeName(), subObject->subtype);
 	for(const auto & compatID : entry["compatibilityIdentifiers"].Vector())
-		registerObject(scope, baseObject->getJsonKey(), compatID.String(), subObject->subtype);
+	{
+		if (identifier != compatID.String())
+			registerObject(scope, baseObject->getJsonKey(), compatID.String(), subObject->subtype);
+		else
+			logMod->warn("Mod '%s' map object '%s': compatibility identifier has same name as object itself!", scope, identifier);
+	}
 }
 
 void CObjectClassesHandler::loadSubObject(const std::string & scope, const std::string & identifier, const JsonNode & entry, ObjectClass * baseObject, size_t index)
@@ -192,7 +197,12 @@ void CObjectClassesHandler::loadSubObject(const std::string & scope, const std::
 
 	registerObject(scope, baseObject->getJsonKey(), subObject->getSubTypeName(), subObject->subtype);
 	for(const auto & compatID : entry["compatibilityIdentifiers"].Vector())
-		registerObject(scope, baseObject->getJsonKey(), compatID.String(), subObject->subtype);
+	{
+		if (identifier != compatID.String())
+			registerObject(scope, baseObject->getJsonKey(), compatID.String(), subObject->subtype);
+		else
+			logMod->warn("Mod '%s' map object '%s': compatibility identifier has same name as object itself!");
+	}
 }
 
 TObjectTypeHandler CObjectClassesHandler::loadSubObjectFromJson(const std::string & scope, const std::string & identifier, const JsonNode & entry, ObjectClass * baseObject, size_t index)

+ 3 - 0
lib/modding/ModManager.cpp

@@ -763,6 +763,9 @@ void ModDependenciesResolver::tryAddMods(TModList modsToResolve, const ModsStora
 		if (mod.isTranslation() && CGeneralTextHandler::getPreferredLanguage() != mod.getBaseLanguage())
 			return false;
 
+		if(!mod.isCompatible())
+			return false;
+
 		if(mod.getDependencies().size() > resolvedModIDs.size())
 			return false;