Browse Source

Merge branch 'develop' into spell

Laserlicht 1 year ago
parent
commit
a826b88641
100 changed files with 1800 additions and 860 deletions
  1. 1 1
      AI/BattleAI/BattleEvaluator.cpp
  2. 1 0
      AI/VCAI/MapObjectsEvaluator.cpp
  3. 1 5
      CMakeLists.txt
  4. 3 1
      Mods/vcmi/config/vcmi/chinese.json
  5. 4 1
      Mods/vcmi/config/vcmi/english.json
  6. 3 1
      Mods/vcmi/config/vcmi/german.json
  7. 1 1
      Mods/vcmi/config/vcmi/portuguese.json
  8. 672 0
      Mods/vcmi/config/vcmi/swedish.json
  9. 21 10
      Mods/vcmi/mod.json
  10. 2 0
      client/CPlayerInterface.cpp
  11. 1 1
      client/Client.h
  12. 1 1
      client/battle/BattleWindow.cpp
  13. 2 2
      client/lobby/OptionsTab.cpp
  14. 59 18
      client/media/CVideoHandler.cpp
  15. 14 9
      client/render/AssetGenerator.cpp
  16. 7 0
      client/render/ColorFilter.cpp
  17. 1 0
      client/render/ColorFilter.h
  18. 6 0
      client/renderSDL/CBitmapFont.cpp
  19. 8 0
      client/renderSDL/CTrueTypeFont.cpp
  20. 20 2
      client/renderSDL/FontChain.cpp
  21. 1 1
      client/renderSDL/FontChain.h
  22. 2 0
      client/renderSDL/RenderHandler.cpp
  23. 3 3
      client/widgets/CArtifactsOfHeroBase.cpp
  24. 1 1
      client/widgets/CArtifactsOfHeroBase.h
  25. 3 6
      client/windows/CWindowWithArtifacts.cpp
  26. 2 1
      client/windows/GUIClasses.cpp
  27. 0 32
      config/creatures/conflux.json
  28. 4 4
      config/fonts.json
  29. 16 0
      config/gameConfig.json
  30. 7 0
      config/schemas/gameSettings.json
  31. 40 40
      config/schemas/mod.json
  32. 1 1
      config/schemas/settings.json
  33. 4 0
      config/schemas/spell.json
  34. 46 0
      config/schemas/template.json
  35. 3 0
      docs/modders/Entities_Format/Spell_Format.md
  36. 3 2
      docs/modders/Mod_File_Format.md
  37. 28 3
      docs/modders/Random_Map_Template.md
  38. 1 0
      include/vcmi/spells/Spell.h
  39. 53 53
      launcher/translation/portuguese.ts
  40. 1 1
      lib/ArtifactUtils.cpp
  41. 62 106
      lib/CArtHandler.cpp
  42. 13 24
      lib/CArtHandler.h
  43. 3 25
      lib/CArtifactInstance.cpp
  44. 2 5
      lib/CArtifactInstance.h
  45. 5 4
      lib/CBonusTypeHandler.cpp
  46. 3 3
      lib/CCreatureHandler.cpp
  47. 3 3
      lib/CCreatureSet.cpp
  48. 2 2
      lib/CCreatureSet.h
  49. 1 1
      lib/CGameInfoCallback.cpp
  50. 5 5
      lib/CHeroHandler.cpp
  51. 5 0
      lib/CMakeLists.txt
  52. 2 2
      lib/CSkillHandler.cpp
  53. 1 0
      lib/GameSettings.cpp
  54. 1 1
      lib/IGameCallback.h
  55. 1 0
      lib/IGameSettings.h
  56. 1 1
      lib/RiverHandler.cpp
  57. 1 1
      lib/RoadHandler.cpp
  58. 1 1
      lib/TerrainHandler.cpp
  59. 5 5
      lib/entities/faction/CTownHandler.cpp
  60. 3 1
      lib/gameState/CGameState.cpp
  61. 4 4
      lib/gameState/CGameStateCampaign.cpp
  62. 21 0
      lib/json/JsonUtils.cpp
  63. 2 0
      lib/json/JsonUtils.h
  64. 1 1
      lib/mapObjectConstructors/CBankInstanceConstructor.cpp
  65. 71 1
      lib/mapObjectConstructors/CObjectClassesHandler.cpp
  66. 8 23
      lib/mapObjectConstructors/CObjectClassesHandler.h
  67. 4 3
      lib/mapObjectConstructors/CRewardableConstructor.cpp
  68. 1 1
      lib/mapObjectConstructors/CRewardableConstructor.h
  69. 1 1
      lib/mapObjectConstructors/DwellingInstanceConstructor.cpp
  70. 3 0
      lib/mapObjectConstructors/IObjectInfo.h
  71. 20 7
      lib/mapObjects/CGCreature.cpp
  72. 1 1
      lib/mapObjects/CGCreature.h
  73. 3 3
      lib/mapObjects/CGHeroInstance.cpp
  74. 2 2
      lib/mapObjects/CGHeroInstance.h
  75. 1 1
      lib/mapObjects/CQuest.cpp
  76. 14 165
      lib/mapObjects/CRewardableObject.cpp
  77. 7 15
      lib/mapObjects/CRewardableObject.h
  78. 37 0
      lib/mapObjects/CompoundMapObjectID.h
  79. 3 4
      lib/mapObjects/MiscObjects.cpp
  80. 5 0
      lib/mapObjects/ObjectTemplate.cpp
  81. 4 0
      lib/mapObjects/ObjectTemplate.h
  82. 33 90
      lib/mapObjects/TownBuildingInstance.cpp
  83. 6 2
      lib/mapObjects/TownBuildingInstance.h
  84. 28 0
      lib/mapping/CMap.cpp
  85. 3 0
      lib/mapping/CMap.h
  86. 2 2
      lib/mapping/CMapHeader.cpp
  87. 2 2
      lib/mapping/MapFormatH3M.cpp
  88. 5 41
      lib/modding/CModHandler.cpp
  89. 0 2
      lib/modding/CModHandler.h
  90. 2 2
      lib/modding/ContentTypeHandler.cpp
  91. 1 1
      lib/modding/ContentTypeHandler.h
  92. 18 26
      lib/networkPacks/NetPacksLib.cpp
  93. 4 4
      lib/networkPacks/PacksForClient.h
  94. 6 1
      lib/rewardable/Info.cpp
  95. 2 0
      lib/rewardable/Info.h
  96. 152 2
      lib/rewardable/Interface.cpp
  97. 16 3
      lib/rewardable/Interface.h
  98. 21 3
      lib/rewardable/Limiter.cpp
  99. 81 51
      lib/rmg/CRmgTemplate.cpp
  100. 38 5
      lib/rmg/CRmgTemplate.h

+ 1 - 1
AI/BattleAI/BattleEvaluator.cpp

@@ -394,7 +394,7 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector
 	{
 		std::set<BattleHex> obstacleHexes;
 
-		auto insertAffected = [](const CObstacleInstance & spellObst, std::set<BattleHex> obstacleHexes) {
+		auto insertAffected = [](const CObstacleInstance & spellObst, std::set<BattleHex> & obstacleHexes) {
 			auto affectedHexes = spellObst.getAffectedTiles();
 			obstacleHexes.insert(affectedHexes.cbegin(), affectedHexes.cend());
 		};

+ 1 - 0
AI/VCAI/MapObjectsEvaluator.cpp

@@ -13,6 +13,7 @@
 #include "../../lib/VCMI_Lib.h"
 #include "../../lib/CCreatureHandler.h"
 #include "../../lib/CHeroHandler.h"
+#include "../../lib/mapObjects/CompoundMapObjectID.h"
 #include "../../lib/mapObjectConstructors/AObjectTypeHandler.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
 #include "../../lib/mapObjects/CGTownInstance.h"

+ 1 - 5
CMakeLists.txt

@@ -486,11 +486,7 @@ if(NOT FORCE_BUNDLED_MINIZIP)
 endif()
 
 if (ENABLE_CLIENT)
-	set(FFMPEG_COMPONENTS avutil swscale avformat avcodec)
-	if(APPLE_IOS AND NOT USING_CONAN)
-		list(APPEND FFMPEG_COMPONENTS swresample)
-	endif()
-	find_package(ffmpeg COMPONENTS ${FFMPEG_COMPONENTS})
+	find_package(ffmpeg COMPONENTS avutil swscale avformat avcodec swresample)
 
 	find_package(SDL2 REQUIRED)
 	find_package(SDL2_image REQUIRED)

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

@@ -12,7 +12,9 @@
 	"vcmi.adventureMap.monsterThreat.levels.9"  : "压倒性的",
 	"vcmi.adventureMap.monsterThreat.levels.10" : "致命的",
 	"vcmi.adventureMap.monsterThreat.levels.11" : "无法取胜",
-	"vcmi.adventureMap.monsterLevel"            : "\n\n%TOWN%LEVEL级生物",
+	"vcmi.adventureMap.monsterLevel"            : "\n\n%TOWN%LEVEL级%ATTACK_TYPE生物",
+	"vcmi.adventureMap.monsterMeleeType"        : "近战",
+	"vcmi.adventureMap.monsterRangedType"       : "远程",
 
 	"vcmi.adventureMap.confirmRestartGame"     : "你想要重新开始游戏吗?",
 	"vcmi.adventureMap.noTownWithMarket"       : "没有足够的市场。",

+ 4 - 1
Mods/vcmi/config/vcmi/english.json

@@ -12,7 +12,9 @@
 	"vcmi.adventureMap.monsterThreat.levels.9"  : "Overpowering",
 	"vcmi.adventureMap.monsterThreat.levels.10" : "Deadly",
 	"vcmi.adventureMap.monsterThreat.levels.11" : "Impossible",
-	"vcmi.adventureMap.monsterLevel"            : "\n\nLevel %LEVEL %TOWN unit",
+	"vcmi.adventureMap.monsterLevel"            : "\n\nLevel %LEVEL %TOWN %ATTACK_TYPE unit",
+	"vcmi.adventureMap.monsterMeleeType"        : "melee",
+	"vcmi.adventureMap.monsterRangedType"       : "ranged",
 
 	"vcmi.adventureMap.confirmRestartGame"               : "Are you sure you want to restart the game?",
 	"vcmi.adventureMap.noTownWithMarket"                 : "There are no available marketplaces!",
@@ -150,6 +152,7 @@
 	"vcmi.client.errors.invalidMap" : "{Invalid map or campaign}\n\nFailed to start game! Selected map or campaign might be invalid or corrupted. Reason:\n%s",
 	"vcmi.client.errors.missingCampaigns" : "{Missing data files}\n\nCampaigns data files were not found! You may be using incomplete or corrupted Heroes 3 data files. Please reinstall game data.",
 	"vcmi.server.errors.disconnected" : "{Network Error}\n\nConnection to game server has been lost!",
+	"vcmi.server.errors.playerLeft" : "{Player Left}\n\n%s player have disconnected from the game!", //%s -> player color
 	"vcmi.server.errors.existingProcess" : "Another VCMI server process is running. Please terminate it before starting a new game.",
 	"vcmi.server.errors.modsToEnable"    : "{Following mods are required}",
 	"vcmi.server.errors.modsToDisable"   : "{Following mods must be disabled}",

+ 3 - 1
Mods/vcmi/config/vcmi/german.json

@@ -12,7 +12,9 @@
 	"vcmi.adventureMap.monsterThreat.levels.9"  : "Überwältigend",
 	"vcmi.adventureMap.monsterThreat.levels.10" : "Tödlich",
 	"vcmi.adventureMap.monsterThreat.levels.11" : "Unmöglich",
-	"vcmi.adventureMap.monsterLevel"            : "\n\nStufe %LEVEL %TOWN-Einheit",
+	"vcmi.adventureMap.monsterLevel"            : "\n\nStufe %LEVEL %TOWN-Einheit (%ATTACK_TYPE)",
+	"vcmi.adventureMap.monsterMeleeType"        : "Nahkampf",
+	"vcmi.adventureMap.monsterRangedType"       : "Fernkampf",
 
 	"vcmi.adventureMap.confirmRestartGame"               : "Seid Ihr sicher, dass Ihr das Spiel neu starten wollt?",
 	"vcmi.adventureMap.noTownWithMarket"                 : "Kein Marktplatz verfügbar!",

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

@@ -79,7 +79,7 @@
 	"vcmi.lobby.handicap.resource" : "Fornece aos jogadores recursos apropriados para começar, além dos recursos iniciais normais. Valores negativos são permitidos, mas são limitados a 0 no total (o jogador nunca começa com recursos negativos).",
 	"vcmi.lobby.handicap.income" : "Altera as várias rendas do jogador em porcentagem. Arredondado para cima.",
 	"vcmi.lobby.handicap.growth" : "Altera a taxa de produção das criaturas nas cidades possuídas pelo jogador. Arredondado para cima.",
-	
+		
 	"vcmi.lobby.login.title" : "Sala de Espera Online do VCMI",
 	"vcmi.lobby.login.username" : "Nome de usuário:",
 	"vcmi.lobby.login.connecting" : "Conectando...",

+ 672 - 0
Mods/vcmi/config/vcmi/swedish.json

@@ -0,0 +1,672 @@
+{
+	"vcmi.adventureMap.monsterThreat.title"     : "\n\nHotnivå: ",
+	"vcmi.adventureMap.monsterThreat.levels.0"  : "Utan ansträngning",
+	"vcmi.adventureMap.monsterThreat.levels.1"  : "Väldigt svag",
+	"vcmi.adventureMap.monsterThreat.levels.2"  : "Svag",
+	"vcmi.adventureMap.monsterThreat.levels.3"  : "Lite svagare",
+	"vcmi.adventureMap.monsterThreat.levels.4"  : "Jämbördig",
+	"vcmi.adventureMap.monsterThreat.levels.5"  : "Lite starkare",
+	"vcmi.adventureMap.monsterThreat.levels.6"  : "Stark",
+	"vcmi.adventureMap.monsterThreat.levels.7"  : "Väldigt stark",
+	"vcmi.adventureMap.monsterThreat.levels.8"  : "Utmanande",
+	"vcmi.adventureMap.monsterThreat.levels.9"  : "Överväldigande",
+	"vcmi.adventureMap.monsterThreat.levels.10" : "Dödlig",
+	"vcmi.adventureMap.monsterThreat.levels.11" : "Omöjlig",
+	"vcmi.adventureMap.monsterLevel"            : "\n\nNivå: %LEVEL - Faktion: %TOWN",
+
+	"vcmi.adventureMap.confirmRestartGame"               : "Är du säker på att du vill starta om spelet?",
+	"vcmi.adventureMap.noTownWithMarket"                 : "Det finns inga tillgängliga marknadsplatser!",
+	"vcmi.adventureMap.noTownWithTavern"                 : "Det finns inga tillgängliga städer med värdshus!",
+	"vcmi.adventureMap.spellUnknownProblem"              : "Det finns ett okänt problem med den här formeln! Ingen mer information är tillgänglig.",
+	"vcmi.adventureMap.playerAttacked"                   : "Spelare har blivit attackerad: %s",
+	"vcmi.adventureMap.moveCostDetails"                  : "Förflyttningspoängs-kostnad: %TURNS tur(er) + %POINTS poäng - Återstående poäng: %REMAINING",
+	"vcmi.adventureMap.moveCostDetailsNoTurns"           : "Förflyttningspoängs-kostnad: %POINTS poäng - Återstående poäng: %REMAINING",
+	"vcmi.adventureMap.movementPointsHeroInfo" 			 : "(Förflyttningspoäng: %REMAINING / %POINTS)",
+	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Tyvärr, att spela om motståndarens tur är inte implementerat ännu!",
+
+	"vcmi.capitalColors.0" : "Röd",
+	"vcmi.capitalColors.1" : "Blå",
+	"vcmi.capitalColors.2" : "Ljusbrun",
+	"vcmi.capitalColors.3" : "Grön",
+	"vcmi.capitalColors.4" : "Orange",
+	"vcmi.capitalColors.5" : "Lila",
+	"vcmi.capitalColors.6" : "Grönblå",
+	"vcmi.capitalColors.7" : "Rosa",
+
+	"vcmi.heroOverview.startingArmy"    : "Startarmé",
+	"vcmi.heroOverview.warMachine"      : "Krigsmaskiner",
+	"vcmi.heroOverview.secondarySkills" : "Sekundärförmågor",
+	"vcmi.heroOverview.spells"          : "Trollformler",
+
+	"vcmi.radialWheel.mergeSameUnit"    : "Slå samman samma varelser",
+	"vcmi.radialWheel.fillSingleUnit"   : "Fyll på med enstaka varelser",
+	"vcmi.radialWheel.splitSingleUnit"  : "Dela av en enda varelse",
+	"vcmi.radialWheel.splitUnitEqually" : "Dela upp varelser lika",
+	"vcmi.radialWheel.moveUnit"         : "Flytta varelser till en annan armé",
+	"vcmi.radialWheel.splitUnit"        : "Dela upp varelse till en annan ruta",
+
+	"vcmi.radialWheel.heroGetArmy"       : "Hämta armé från annan hjälte",
+	"vcmi.radialWheel.heroSwapArmy"      : "Byt armé med annan hjälte",
+	"vcmi.radialWheel.heroExchange"      : "Öppna hjälteutbyte",
+	"vcmi.radialWheel.heroGetArtifacts"  : "Hämta artefakter från annan hjälte",
+	"vcmi.radialWheel.heroSwapArtifacts" : "Byt artefakter med annan hjälte",
+	"vcmi.radialWheel.heroDismiss"       : "Avfärda hjälten",
+
+	"vcmi.radialWheel.moveTop"    : "Flytta till toppen",
+	"vcmi.radialWheel.moveUp"     : "Flytta upp",
+	"vcmi.radialWheel.moveDown"   : "Flytta nedåt",
+	"vcmi.radialWheel.moveBottom" : "Flytta till botten",
+
+	"vcmi.spellBook.search" : "sök...",
+
+	"vcmi.mainMenu.serverConnecting"       : "Ansluter...",
+	"vcmi.mainMenu.serverAddressEnter"     : "Ange adress:",
+	"vcmi.mainMenu.serverConnectionFailed" : "Misslyckades med att ansluta",
+	"vcmi.mainMenu.serverClosing"          : "Avslutar...",
+	"vcmi.mainMenu.hostTCP"                : "Spela som värd (TCP/IP)",
+	"vcmi.mainMenu.joinTCP"                : "Anslut till värd (TCP/IP)",
+
+	"vcmi.lobby.filepath"          : "Filsökväg",
+	"vcmi.lobby.creationDate"      : "Skapelsedatum",
+	"vcmi.lobby.scenarioName"      : "Namn på scenariot",
+	"vcmi.lobby.mapPreview"        : "Förhandsgranskning av karta",
+	"vcmi.lobby.noPreview"         : "ingen förhandsgranskning",
+	"vcmi.lobby.noUnderground"     : "ingen underjord",
+	"vcmi.lobby.sortDate"          : "Sorterar kartor efter ändringsdatum",
+	"vcmi.lobby.backToLobby"       : "Återgå till lobbyn",
+	"vcmi.lobby.author"            : "Skaparen av lobbyn",
+	"vcmi.lobby.handicap"          : "Handikapp",
+	"vcmi.lobby.handicap.resource" : "Ger spelarna lämpliga resurser att börja med utöver de normala startresurserna. Negativa värden är tillåtna men är begränsade till 0 totalt (spelaren börjar aldrig med negativa resurser).",
+	"vcmi.lobby.handicap.income"   : "Ändrar spelarens olika inkomster i procent (resultaten avrundas uppåt).",
+	"vcmi.lobby.handicap.growth"   : "Ändrar tillväxttakten för varelser i de städer som ägs av spelaren (resultaten avrundas uppåt).",
+
+	"vcmi.lobby.login.title"              : "VCMI Online Lobby",
+	"vcmi.lobby.login.username"           : "Användarnamn:",
+	"vcmi.lobby.login.connecting"         : "Ansluter...",
+	"vcmi.lobby.login.error"              : "Anslutningsfel: %s",
+	"vcmi.lobby.login.create"             : "Nytt konto",
+	"vcmi.lobby.login.login"              : "Logga in",
+	"vcmi.lobby.login.as"                 : "Logga in som %s",
+	"vcmi.lobby.header.rooms"             : "Spelrum - %d",
+	"vcmi.lobby.header.channels"          : "Chattkanaler",
+	"vcmi.lobby.header.chat.global"       : "Global spelchatt - %s", // %s -> språknamn
+	"vcmi.lobby.header.chat.match"        : "Chatt från föregående spel på %s", // %s -> datum och tid för spelstart
+	"vcmi.lobby.header.chat.player"       : "Privat chatt med %s", // %s -> smeknamn på en annan spelare
+	"vcmi.lobby.header.history"           : "Dina tidigare spel",
+	"vcmi.lobby.header.players"           : "Spelare online - %d",
+	"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.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.",
+	"vcmi.lobby.room.description.new"     : "För att starta spelet, välj ett scenario eller skapa en slumpmässig karta.",
+	"vcmi.lobby.room.description.load"    : "Använd ett av dina sparade spel för att starta spelet.",
+	"vcmi.lobby.room.description.limit"   : "Upp till %d spelare kan komma in i ditt rum (dig inkluderad).",
+	"vcmi.lobby.invite.header"            : "Bjud in spelare",
+	"vcmi.lobby.invite.notification"      : "Spelaren har bjudit in dig till sitt spelrum. Du kan nu gå med i deras privata rum.",
+	"vcmi.lobby.preview.title"            : "Gå med i spelrummet",
+	"vcmi.lobby.preview.subtitle"         : "Spel på karta/RMG-mall: %s - Värdens smeknamn: %s", //TL Notering: 1) namn på karta eller RMG-mall 2) smeknamn på spelvärden
+	"vcmi.lobby.preview.version"          : "Spelversion:",
+	"vcmi.lobby.preview.players"          : "Spelare:",
+	"vcmi.lobby.preview.mods"             : "Moddar som används:",
+	"vcmi.lobby.preview.allowed"          : "Gå med i spelrummet?",
+	"vcmi.lobby.preview.error.header"     : "Det går inte att gå med i det här rummet.",
+	"vcmi.lobby.preview.error.playing"    : "Du måste lämna ditt nuvarande spel först.",
+	"vcmi.lobby.preview.error.full"       : "Rummet är redan fullt.",
+	"vcmi.lobby.preview.error.busy"       : "Rummet tar inte längre emot nya spelare.",
+	"vcmi.lobby.preview.error.invite"     : "Du blev inte inbjuden till det här rummet.",
+	"vcmi.lobby.preview.error.mods"       : "Du använder en annan uppsättning moddar.",
+	"vcmi.lobby.preview.error.version"    : "Du använder en annan version av VCMI.",
+	"vcmi.lobby.room.new"                 : "Nytt spel",
+	"vcmi.lobby.room.load"                : "Ladda spel",
+	"vcmi.lobby.room.type"                : "Rumstyp",
+	"vcmi.lobby.room.mode"                : "Spelläge",
+	"vcmi.lobby.room.state.public"        : "Offentligt",
+	"vcmi.lobby.room.state.private"       : "Privat",
+	"vcmi.lobby.room.state.busy"          : "I spel",
+	"vcmi.lobby.room.state.invited"       : "Inbjuden",
+	"vcmi.lobby.mod.state.compatible"     : "Kompatibel",
+	"vcmi.lobby.mod.state.disabled"       : "Måste vara aktiverat",
+	"vcmi.lobby.mod.state.version"        : "Versioner matchar inte",
+	"vcmi.lobby.mod.state.excessive"      : "Måste vara inaktiverat",
+	"vcmi.lobby.mod.state.missing"        : "Ej installerad",
+	"vcmi.lobby.pvp.coin.hover"           : "Mynt",
+	"vcmi.lobby.pvp.coin.help"            : "Singla slant",
+	"vcmi.lobby.pvp.randomTown.hover"     : "Slumpmässig stad",
+	"vcmi.lobby.pvp.randomTown.help"      : "Skriv en slumpmässig stad i chatten",
+	"vcmi.lobby.pvp.randomTownVs.hover"   : "Slumpmässig stad vs.",
+	"vcmi.lobby.pvp.randomTownVs.help"    : "Skriv två slumpmässiga städer i chatten",
+	"vcmi.lobby.pvp.versus"               : "vs.",
+
+	"vcmi.client.errors.invalidMap"       : "{Ogiltig karta eller kampanj}\n\nStartade inte spelet! Vald karta eller kampanj kan vara ogiltig eller skadad. Orsak:\n%s",
+	"vcmi.client.errors.missingCampaigns" : "{Saknade datafiler}\n\nKampanjernas datafiler hittades inte! Du kanske använder ofullständiga eller skadade Heroes 3-datafiler. Vänligen installera om speldata.",
+	"vcmi.server.errors.disconnected"     : "{Nätverksfel}\n\nAnslutningen till spelservern har förlorats!",
+	"vcmi.server.errors.existingProcess"  : "En annan VCMI-serverprocess är igång. Vänligen avsluta den innan du startar ett nytt spel.",
+	"vcmi.server.errors.modsToEnable"     : "{Följande modd(ar) krävs}",
+	"vcmi.server.errors.modsToDisable"    : "{Följande modd(ar) måste inaktiveras}",
+	"vcmi.server.errors.modNoDependency"  : "Misslyckades med att ladda modd {'%s'}!\n Den är beroende av modd {'%s'} som inte är aktiverad!\n",
+	"vcmi.server.errors.modConflict"      : "Misslyckades med att ladda modd {'%s'}!\n Konflikter med aktiverad modd {'%s'}!\n",
+	"vcmi.server.errors.unknownEntity"    : "Misslyckades med att ladda sparat spel! Okänd enhet '%s' hittades i sparat spel! Sparningen kanske inte är kompatibel med den aktuella versionen av moddarna!",
+
+	"vcmi.dimensionDoor.seaToLandError" : "Det går inte att teleportera sig från hav till land eller tvärtom med trollformeln 'Dimensionsdörr'.",
+
+	"vcmi.settingsMainWindow.generalTab.hover"   : "Allmänt",
+	"vcmi.settingsMainWindow.generalTab.help"    : "Växlar till fliken/menyn med allmänna spelklients-inställningar relaterade till allmänt beteende för spelklienten.",
+	"vcmi.settingsMainWindow.battleTab.hover"    : "Strid",
+	"vcmi.settingsMainWindow.battleTab.help"     : "Växlar till fliken/menyn med strids-inställningar där man kan konfigurera spelets beteende under strider.",
+	"vcmi.settingsMainWindow.adventureTab.hover" : "Äventyrskarta",
+	"vcmi.settingsMainWindow.adventureTab.help"  : "Växlar till fliken/menyn med inställningar som har med äventyrskartan att göra (äventyrskartan är den del av spelet där spelarna kan styra sina hjältars förflyttning på land, vatten och nere i underjorden).",
+
+	"vcmi.systemOptions.videoGroup" : "Bild-inställningar",
+	"vcmi.systemOptions.audioGroup" : "Ljud-inställningar",
+	"vcmi.systemOptions.otherGroup" : "Andra inställningar", // unused right now
+	"vcmi.systemOptions.townsGroup" : "By-/Stads-skärm",
+
+	"vcmi.statisticWindow.statistics"                    : "Statistik",
+	"vcmi.statisticWindow.tsvCopy"                       : "Statistik-data till urklipp",
+	"vcmi.statisticWindow.selectView"                    : "Välj vy",
+	"vcmi.statisticWindow.value"                         : "Värde",
+	"vcmi.statisticWindow.title.overview"                : "Översikt",
+	"vcmi.statisticWindow.title.resources"               : "Resurser",
+	"vcmi.statisticWindow.title.income"                  : "Inkomst",
+	"vcmi.statisticWindow.title.numberOfHeroes"          : "Antal hjältar",
+	"vcmi.statisticWindow.title.numberOfTowns"           : "Antal städer/byar",
+	"vcmi.statisticWindow.title.numberOfArtifacts"       : "Antal artefakter",
+	"vcmi.statisticWindow.title.numberOfDwellings"       : "Antal varelse-bon",
+	"vcmi.statisticWindow.title.numberOfMines"           : "Antal gruvor",
+	"vcmi.statisticWindow.title.armyStrength"            : "Arméns styrka",
+	"vcmi.statisticWindow.title.experience"              : "Erfarenhet",
+	"vcmi.statisticWindow.title.resourcesSpentArmy"      : "Armé-kostnader",
+	"vcmi.statisticWindow.title.resourcesSpentBuildings" : "Byggnadskostnader",
+	"vcmi.statisticWindow.title.mapExplored"             : "Kart-utforskningsratio",
+	"vcmi.statisticWindow.param.playerName"              : "Spelarens namn",
+	"vcmi.statisticWindow.param.daysSurvived"            : "Överlevda dagar",
+	"vcmi.statisticWindow.param.maxHeroLevel"            : "Den högsta hjältenivån",
+	"vcmi.statisticWindow.param.battleWinRatioHero"      : "Vinstkvot (gentemot hjältar)",
+	"vcmi.statisticWindow.param.battleWinRatioNeutral"   : "Vinstkvot (gentemot neutrala)",
+	"vcmi.statisticWindow.param.battlesHero"             : "Strider (gentemot hjältar)",
+	"vcmi.statisticWindow.param.battlesNeutral"          : "Strider (gentemot neutrala)",
+	"vcmi.statisticWindow.param.maxArmyStrength"         : "Största totala arméstyrkan",
+	"vcmi.statisticWindow.param.tradeVolume"             : "Handelsvolym",
+	"vcmi.statisticWindow.param.obeliskVisited"          : "Obelisker besökta",
+	"vcmi.statisticWindow.icon.townCaptured"             : "Städer/byar erövrade",
+	"vcmi.statisticWindow.icon.strongestHeroDefeated"    : "Den starkaste motståndarhjälten som blivit besegrad",
+	"vcmi.statisticWindow.icon.grailFound"               : "Graal funnen",
+	"vcmi.statisticWindow.icon.defeated"                 : "Besegrad",
+
+	"vcmi.systemOptions.fullscreenBorderless.hover"        : "Helskärm (kantlös)",
+	"vcmi.systemOptions.fullscreenBorderless.help"         : "{Kantlös helskärm}\n\nI kantlöst helskärmsläge kommer spelet alltid att använda samma bildskärmsupplösning som valts i operativsystemet (bildskärmsupplösningen som valts i VCMI kommer ignoreras).",
+	"vcmi.systemOptions.fullscreenExclusive.hover"         : "Exklusivt helskärmsläge",
+	"vcmi.systemOptions.fullscreenExclusive.help"          : "{Helskärm}\n\nI exklusivt helskärmsläge kommer spelet att ändra bildskärmsupplösningen till det som valts i VCMI.",
+	"vcmi.systemOptions.resolutionButton.hover"            : "Bildskärmsupplösning: %wx%h",
+	"vcmi.systemOptions.resolutionButton.help"             : "{Bildskärmsupplösning}\n\nÄndrar bildskärmens upplösning i spelet.",
+	"vcmi.systemOptions.resolutionMenu.hover"              : "Välj bildskärmsupplösningen i spelet",
+	"vcmi.systemOptions.resolutionMenu.help"               : "Ändrar bildskärmsupplösning i spelet.",
+	"vcmi.systemOptions.scalingButton.hover"               : "Gränssnittsskalning: %p%",
+	"vcmi.systemOptions.scalingButton.help"                : "{Gränssnittsskalning}\n\nÄndrar storleken av de olika gränssnitten som finns i spelet.",
+	"vcmi.systemOptions.scalingMenu.hover"                 : "Välj gränssnittsskalning",
+	"vcmi.systemOptions.scalingMenu.help"                  : "Förstorar eller förminskar olika gränssnitt i spelet.",
+	"vcmi.systemOptions.longTouchButton.hover"             : "Fördröjt tryckintervall: %d ms", // Översättningsnot: ’ms’ = "millisekunder"
+	"vcmi.systemOptions.longTouchButton.help"              : "{Fördröjt tryckintervall}\n\nNär du använder dig av en pekskärm och vill komma åt spelalternativ behöver du göra en fördröjd pekskärmsberöring under en specifikt angiven tid (i millisekunder) för att en popup-meny skall synas.",
+	"vcmi.systemOptions.longTouchMenu.hover"               : "Välj tidsintervall för fördröjd pekskärmsberöringsmeny",
+	"vcmi.systemOptions.longTouchMenu.help"                : "Ändra varaktighetsintervallet för fördröjd beröring.",
+	"vcmi.systemOptions.longTouchMenu.entry"               : "%d millisekunder",
+	"vcmi.systemOptions.framerateButton.hover"             : "Visar FPS (skärmbilder per sekund)",
+	"vcmi.systemOptions.framerateButton.help"              : "{Visa FPS}\n\nVisar räknaren för bildrutor per sekund i hörnet av spelfönstret.",
+	"vcmi.systemOptions.hapticFeedbackButton.hover"        : "Haptisk återkoppling",
+	"vcmi.systemOptions.hapticFeedbackButton.help"         : "{Haptisk feedback}\n\nÄndrar den haptiska feedbacken för berörings-input.",
+	"vcmi.systemOptions.enableUiEnhancementsButton.hover"  : "Förbättringar av användargränssnittet",
+	"vcmi.systemOptions.enableUiEnhancementsButton.help"   : "{Gränssnittsförbättringar}\n\nVälj mellan olika förbättringar av användargränssnittet. Till exempel en lättåtkomlig ryggsäcksknapp med mera. Avaktivera för att få en mer klassisk spelupplevelse.",
+	"vcmi.systemOptions.enableLargeSpellbookButton.hover"  : "Stor trollformelsbok",
+	"vcmi.systemOptions.enableLargeSpellbookButton.help"   : "{Stor trollformelsbok}\n\nAktiverar en större trollformelsbok som rymmer fler trollformler per sida (animeringen av sidbyte i den större trollformelsboken fungerar inte).",
+	"vcmi.systemOptions.audioMuteFocus.hover"              : "Stänger av ljudet vid inaktivitet",
+	"vcmi.systemOptions.audioMuteFocus.help"               : "{Stäng av ljud vid inaktivitet}\n\nStänger av ljudet i spelet vid inaktivitet. Undantag är meddelanden i spelet och ljudet för ny tur/omgång.",
+
+	"vcmi.adventureOptions.infoBarPick.hover"                : "Visar textmeddelanden i infopanelen",
+	"vcmi.adventureOptions.infoBarPick.help"                 : "{Infopanelsmeddelanden}\n\nNär det är möjligt kommer spelmeddelanden från besökande kartobjekt att visas i infopanelen istället för att dyka upp i ett separat fönster.",
+	"vcmi.adventureOptions.numericQuantities.hover"          : "Numeriska antal varelser",
+	"vcmi.adventureOptions.numericQuantities.help"           : "{Numerisk varelsemängd}\n\nVisa ungefärliga mängder av fiendevarelser i det numeriska A-B-formatet.",
+	"vcmi.adventureOptions.forceMovementInfo.hover"          : "Visa alltid förflyttningskostnad",
+	"vcmi.adventureOptions.forceMovementInfo.help"           : "{Visa alltid förflyttningskostnad}\n\nVisar alltid förflyttningspoäng i statusfältet (istället för att bara visa dem när du håller ned ALT-tangenten).",
+	"vcmi.adventureOptions.showGrid.hover"                   : "Visa rutnät",
+	"vcmi.adventureOptions.showGrid.help"                    : "{Visa rutnät}\n\nVisa rutnätsöverlägget som markerar gränserna mellan äventyrskartans brickor/rutor.",
+	"vcmi.adventureOptions.borderScroll.hover"               : "Kantrullning",
+	"vcmi.adventureOptions.borderScroll.help"                : "{Kantrullning}\n\nRullar äventyrskartan när markören är angränsande till fönsterkanten. Kan inaktiveras genom att hålla ned CTRL-tangenten.",
+	"vcmi.adventureOptions.infoBarCreatureManagement.hover"  : "Hantering av varelser i infopanelen i nedre högra hörnet",
+	"vcmi.adventureOptions.infoBarCreatureManagement.help"   : "{Varelsehantering i infopanelen}\n\nTillåter omarrangering av varelser i infopanelen längst ner till höger på äventyrskartan istället för att bläddra mellan olika infopaneler.",
+	"vcmi.adventureOptions.leftButtonDrag.hover"             : "Dra kartan med vänster musknapp",
+	"vcmi.adventureOptions.leftButtonDrag.help"              : "{Vänsterklicksdragning}\n\nVid aktivering kan äventyrskartans kartvy dras genom att flytta musen med vänster musknapp nedtryckt.",
+	"vcmi.adventureOptions.rightButtonDrag.hover"            : "Dra kartan med höger musknapp",
+	"vcmi.adventureOptions.rightButtonDrag.help"             : "{Högerklicksdragning}\n\nVid aktivering kan äventyrskartans kartvy dras genom att flytta musen med höger musknapp nedtryckt.",
+	"vcmi.adventureOptions.smoothDragging.hover"             : "Mjuk kartdragning",
+	"vcmi.adventureOptions.smoothDragging.help"              : "{Mjuk kartdragning}\n\nVid aktivering så har kartdragningen en modern rullningseffekt.",
+	"vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Skippar övertoningseffekter",
+	"vcmi.adventureOptions.skipAdventureMapAnimations.help"  : "{Skippa övertoningseffekter}\n\nHoppar över ut- och intoningseffekten och liknande effekter av kartobjekt (resursinsamling, ombordning och avbordning av skepp osv.). Gör användargränssnittet mer reaktivt i vissa fall på bekostnad av estetiken. Speciellt användbart i PvP-spel. När maximal förflyttningshastighet är valt så är skippning av effekter aktiverat oavsett vad du har valt här.",
+	"vcmi.adventureOptions.mapScrollSpeed1.hover"            : "",
+	"vcmi.adventureOptions.mapScrollSpeed5.hover"            : "",
+	"vcmi.adventureOptions.mapScrollSpeed6.hover"            : "",
+	"vcmi.adventureOptions.mapScrollSpeed1.help"             : "Ställ in kartans rullningshastighet på 'Mycket långsam'.",
+	"vcmi.adventureOptions.mapScrollSpeed5.help"             : "Ställ in kartans rullningshastighet på 'Mycket snabb'.",
+	"vcmi.adventureOptions.mapScrollSpeed6.help"             : "Ställ in kartans rullningshastighet till 'Omedelbar'.",
+	"vcmi.adventureOptions.hideBackground.hover"             : "Dölj bakgrund",
+	"vcmi.adventureOptions.hideBackground.help"              : "{Dölj bakgrund}\n\nDöljer äventyrskartan i bakgrunden och visar en textur istället.",
+
+	"vcmi.battleOptions.queueSizeLabel.hover"            : "Visa turordningskö",
+	"vcmi.battleOptions.queueSizeNoneButton.hover"       : "AV",
+	"vcmi.battleOptions.queueSizeAutoButton.hover"       : "AUTO",
+	"vcmi.battleOptions.queueSizeSmallButton.hover"      : "LITEN",
+	"vcmi.battleOptions.queueSizeBigButton.hover"        : "STOR",
+	"vcmi.battleOptions.queueSizeNoneButton.help"        : "Visa inte turordningskö.",
+	"vcmi.battleOptions.queueSizeAutoButton.help"        : "Justera automatiskt storleken på turordningskön baserat på spelets skärmbildsupplösning ('LITEN' används när det är mindre än 700 pixlar i höjd, 'STOR' används annars).",
+	"vcmi.battleOptions.queueSizeSmallButton.help"       : "Ställer in storleksinställningen på turordningskön till 'LITEN'.",
+	"vcmi.battleOptions.queueSizeBigButton.help"         : "Ställer in storleksinställningen på turordningskön till 'STOR' (bildskärmsupplösningen måste överstiga 700 pixlar i höjd).",
+	"vcmi.battleOptions.animationsSpeed1.hover"          : "",
+	"vcmi.battleOptions.animationsSpeed5.hover"          : "",
+	"vcmi.battleOptions.animationsSpeed6.hover"          : "",
+	"vcmi.battleOptions.animationsSpeed1.help"           : "Ställ in animationshastigheten till mycket långsam.",
+	"vcmi.battleOptions.animationsSpeed5.help"           : "Ställ in animationshastigheten till mycket snabb.",
+	"vcmi.battleOptions.animationsSpeed6.help"           : "Ställ in animationshastigheten till omedelbar.",
+	"vcmi.battleOptions.movementHighlightOnHover.hover"  : "Muspeka (hovra) för att avslöja förflyttningsräckvidd",
+	"vcmi.battleOptions.movementHighlightOnHover.help"   : "{Muspeka för att avslöja förflyttningsräckvidd}\n\nVisar enheters potentiella förflyttningsräckvidd över slagfältet när du håller muspekaren över dem.",
+	"vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Avslöja skyttars räckvidd",
+	"vcmi.battleOptions.rangeLimitHighlightOnHover.help" : "{Muspeka för att avslöja skyttars räckvidd}\n\nVisar hur långt en enhets distansattack sträcker sig över slagfältet när du håller muspekaren över dem.",
+	"vcmi.battleOptions.showStickyHeroInfoWindows.hover" : "Visa fönster med hjältars primärförmågor",
+	"vcmi.battleOptions.showStickyHeroInfoWindows.help"  : "{Visa fönster med hjältars primärförmågor}\n\nKommer alltid att visa ett fönster där du kan se dina hjältars primärförmågor (anfall, försvar, trollkonst, kunskap och trollformelpoäng).",
+	"vcmi.battleOptions.skipBattleIntroMusic.hover"      : "Hoppa över intromusik",
+	"vcmi.battleOptions.skipBattleIntroMusic.help"       : "{Hoppa över intromusik}\n\nTillåt åtgärder under intromusiken som spelas i början av varje strid.",
+	"vcmi.battleOptions.endWithAutocombat.hover"         : "Slutför striden så fort som möjligt",
+	"vcmi.battleOptions.endWithAutocombat.help"          : "{Slutför strid}\n\nAuto-strid spelar striden åt dig för att striden ska slutföras så fort som möjligt.",
+	"vcmi.battleOptions.showQuickSpell.hover"            : "Snabb åtkomst till dina trollformler",
+	"vcmi.battleOptions.showQuickSpell.help"             : "{Visa snabbtrollformels-panelen}\n\nVisar en snabbvalspanel vid sidan av stridsfönstret där du har snabb åtkomst till några av dina trollformler",
+
+	"vcmi.adventureMap.revisitObject.hover" : "Gör ett återbesök",
+	"vcmi.adventureMap.revisitObject.help"  : "{Återbesök kartobjekt}\n\nEn hjälte som för närvarande står på ett kartobjekt kan göra ett återbesök (utan att först behöva avlägsna sig från kartobjektet för att sedan göra ett återbesök).",
+
+	"vcmi.battleWindow.pressKeyToSkipIntro"          : "Tryck på valfri tangent för att starta striden omedelbart",
+	"vcmi.battleWindow.damageEstimation.melee"       : "Attackera %CREATURE (%DAMAGE).",
+	"vcmi.battleWindow.damageEstimation.meleeKills"  : "Attackera %CREATURE (%DAMAGE, %KILLS).",
+	"vcmi.battleWindow.damageEstimation.ranged"      : "Skjut %CREATURE (%SHOTS, %DAMAGE).",
+	"vcmi.battleWindow.damageEstimation.rangedKills" : "Skjut %CREATURE (%SHOTS, %DAMAGE, %KILLS).",
+	"vcmi.battleWindow.damageEstimation.shots"       : "%d skott kvar",
+	"vcmi.battleWindow.damageEstimation.shots.1"     : "%d skott kvar",
+	"vcmi.battleWindow.damageEstimation.damage"      : "%d skada",
+	"vcmi.battleWindow.damageEstimation.damage.1"    : "%d skada",
+	"vcmi.battleWindow.damageEstimation.kills"       : "%d kommer att förgås",
+	"vcmi.battleWindow.damageEstimation.kills.1"     : "%d kommer att förgås",
+
+	"vcmi.battleWindow.damageRetaliation.will"        : "Kommer att retaliera ",
+	"vcmi.battleWindow.damageRetaliation.may"         : "Kommer kanske att retaliera ",
+	"vcmi.battleWindow.damageRetaliation.never"       : "Kommer inte att retaliera.",
+	"vcmi.battleWindow.damageRetaliation.damage"      : "(%DAMAGE).",
+	"vcmi.battleWindow.damageRetaliation.damageKills" : "(%DAMAGE, %KILLS).",
+
+	"vcmi.battleWindow.killed" : "Dödad",
+	"vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s dödades av träffsäkra skott!",
+	"vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s dödades med ett träffsäkert skott!",
+	"vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s dödades av träffsäkra skott!",
+	"vcmi.battleWindow.endWithAutocombat" : "Är du säker på att du vill slutföra striden med auto-strid?",
+
+	"vcmi.battleResultsWindow.applyResultsLabel" : "Acceptera stridsresultat?",
+
+	"vcmi.tutorialWindow.title"                           : "Pekskärmsintroduktion",
+	"vcmi.tutorialWindow.decription.RightClick"           : "Rör vid och håll kvar det element som du vill högerklicka på. Tryck på det fria området för att stänga.",
+	"vcmi.tutorialWindow.decription.MapPanning"           : "Tryck och dra med ett finger för att flytta kartan.",
+	"vcmi.tutorialWindow.decription.MapZooming"           : "Nyp med två fingrar för att ändra kartans zoom.",
+	"vcmi.tutorialWindow.decription.RadialWheel"          : "Genom att svepa öppnas ett radiellt hjul för olika åtgärder t.ex. hantering av varelser/hjältar och stadsåtgärder.",
+	"vcmi.tutorialWindow.decription.BattleDirection"      : "För att attackera från en viss riktning sveper du i den riktning från vilken attacken ska göras.",
+	"vcmi.tutorialWindow.decription.BattleDirectionAbort" : "Gesten för attackriktning kan avbrytas om du sveper tillräckligt långt bort.",
+	"vcmi.tutorialWindow.decription.AbortSpell"           : "Tryck och håll för att avbryta en vald trollformel.",
+
+	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Visar tillgängliga varelser att rekrytera",
+	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.help"  : "{Visa tillgängliga varelser}\n\nVisa antalet varelser som finns tillgängliga att rekrytera istället för deras veckovisa förökning i stadsöversikten (nedre vänstra hörnet av stadsskärmen).",
+	"vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover"     : "Visar den veckovisa varelseförökningen",
+	"vcmi.otherOptions.creatureGrowthAsDwellingLabel.help"      : "{Visa veckovis varelseförökning}\n\nVisa varelsers veckovisa förökning istället för antalet tillgängliga varelser i stadsöversikten (nedre vänstra hörnet av stadsskärmen).",
+	"vcmi.otherOptions.compactTownCreatureInfo.hover"           : "Visa mindre varelse-info",
+	"vcmi.otherOptions.compactTownCreatureInfo.help"            : "{Kompakt varelse-info}\n\nVisa mindre information för stadens varelser i stadsöversikten (nedre vänstra hörnet av stadsskärmen).",
+
+	"vcmi.townHall.missingBase"             : "Basbyggnaden '%s' måste byggas först",
+	"vcmi.townHall.noCreaturesToRecruit"    : "Det finns inga varelser att rekrytera!",
+
+	"vcmi.townStructure.bank.borrow"  : "Du går in i banken. En bankman ser dig och säger: \"Vi har gjort ett specialerbjudande till dig. Du kan ta ett lån på 2500 guld från oss i 5 dagar. Du måste återbetala 500 guld varje dag.\"",
+	"vcmi.townStructure.bank.payBack" : "Du går in i banken. En bankman ser dig och säger: \"Du har redan fått ditt lån. Betala tillbaka det innan du tar ett nytt.\"",
+
+	"vcmi.logicalExpressions.anyOf"  : "Något av följande:",
+	"vcmi.logicalExpressions.allOf"  : "Alla följande:",
+	"vcmi.logicalExpressions.noneOf" : "Inget av följande:",
+
+	"vcmi.heroWindow.openCommander.hover" : "Öppna befälhavarens informationsfönster",
+	"vcmi.heroWindow.openCommander.help"  : "Visar detaljer om befälhavaren för den här hjälten.",
+	"vcmi.heroWindow.openBackpack.hover"  : "Öppna artefaktryggsäcksfönster",
+	"vcmi.heroWindow.openBackpack.help"   : "Öppnar fönster som gör det enklare att hantera artefaktryggsäcken.",
+
+	"vcmi.tavernWindow.inviteHero"  : "Bjud in hjälte",
+
+	"vcmi.commanderWindow.artifactMessage" : "Vill du återlämna denna artefakt till hjälten?",
+
+	"vcmi.creatureWindow.showBonuses.hover"    : "Byt till bonusvy",
+	"vcmi.creatureWindow.showBonuses.help"     : "Visa befälhavarens aktiva bonusar.",
+	"vcmi.creatureWindow.showSkills.hover"     : "Byt till färdighetsvy",
+	"vcmi.creatureWindow.showSkills.help"      : "Visa befälhavarens inlärda färdigheter.",
+	"vcmi.creatureWindow.returnArtifact.hover" : "Återlämna artefakt",
+	"vcmi.creatureWindow.returnArtifact.help"  : "Klicka på den här knappen för att lägga tillbaka artefakten i hjältens ryggsäck.",
+
+	"vcmi.questLog.hideComplete.hover" : "Gömmer alla slutförda uppdrag",
+	"vcmi.questLog.hideComplete.help"  : "Dölj alla slutförda uppdrag.",
+
+	"vcmi.randomMapTab.widgets.randomTemplate"       : "(Slumpmässig)",
+	"vcmi.randomMapTab.widgets.templateLabel"        : "Mall",
+	"vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Ställ in...",
+	"vcmi.randomMapTab.widgets.teamAlignmentsLabel"  : "Laggruppering",
+	"vcmi.randomMapTab.widgets.roadTypesLabel"       : "Vägtyper",
+
+	"vcmi.optionsTab.turnOptions.hover" : "Turomgångsalternativ",
+	"vcmi.optionsTab.turnOptions.help"  : "Välj alternativ för turomgångs-timer och simultana turer",
+
+	"vcmi.optionsTab.chessFieldBase.hover"          : "Bas-timern",
+	"vcmi.optionsTab.chessFieldTurn.hover"          : "Turomgångs-timern",
+	"vcmi.optionsTab.chessFieldBattle.hover"        : "Strids-timern",
+	"vcmi.optionsTab.chessFieldUnit.hover"          : "Enhets-timern",
+	"vcmi.optionsTab.chessFieldBase.help"           : "Används när {Turomgångs-timern} når '0'. Ställs in en gång i början av spelet. När den når '0' avslutas den aktuella turomgången (pågående strid avslutas med förlust).",
+	"vcmi.optionsTab.chessFieldTurnAccumulate.help" : "Används utanför strid eller när {Strids-timern} tar slut. Återställs varje turomgång. Outnyttjad tid läggs till i {Bas-timern} till nästa turomgång.",
+	"vcmi.optionsTab.chessFieldTurnDiscard.help"    : "Används utanför strid eller när {Strids-timern} tar slut. Återställs varje turomgång. Outnyttjad tid går förlorad.",
+	"vcmi.optionsTab.chessFieldBattle.help"         : "Används i strider med AI eller i PVP-strid när {Enhets-timern} tar slut. Återställs i början av varje strid.",
+	"vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Används när du styr din enhet i PVP-strid. Outnyttjad tid läggs till i {Strids-timern} när enheten har avslutat sin turomgång.",
+	"vcmi.optionsTab.chessFieldUnitDiscard.help"    : "Används när du styr din enhet i PVP-strid. Återställs i början av varje enhets turomgång. Outnyttjad tid går förlorad.",
+
+	"vcmi.optionsTab.accumulate" : "Ackumulera",
+
+	"vcmi.optionsTab.simturnsTitle"     : "Simultana turomgångar",
+	"vcmi.optionsTab.simturnsMin.hover" : "Åtminstone i",
+	"vcmi.optionsTab.simturnsMax.hover" : "Som mest i",
+	"vcmi.optionsTab.simturnsAI.hover"  : "(Experimentell) Simultana AI-turomgångar",
+	"vcmi.optionsTab.simturnsMin.help"  : "Spela samtidigt som andra spelare under ett angivet antal dagar. Kontakter mellan spelare under denna period är blockerade",
+	"vcmi.optionsTab.simturnsMax.help"  : "Spela samtidigt som andra spelare under ett angivet antal dagar eller tills en tillräckligt nära kontakt inträffar med en annan spelare",
+	"vcmi.optionsTab.simturnsAI.help"   : "{Simultana AI-turomgångar}\nExperimentellt alternativ. Tillåter AI-spelare att agera samtidigt som den mänskliga spelaren när simultana turomgångar är aktiverade.",
+
+	"vcmi.optionsTab.turnTime.select"     : "Turtids-förinställningar",
+	"vcmi.optionsTab.turnTime.unlimited"  : "Obegränsat med tid",
+	"vcmi.optionsTab.turnTime.classic.1"  : "Klassisk timer: 1 minut",
+	"vcmi.optionsTab.turnTime.classic.2"  : "Klassisk timer: 2 minuter",
+	"vcmi.optionsTab.turnTime.classic.5"  : "Klassisk timer: 5 minuter",
+	"vcmi.optionsTab.turnTime.classic.10" : "Klassisk timer: 10 minuter",
+	"vcmi.optionsTab.turnTime.classic.20" : "Klassisk timer: 20 minuter",
+	"vcmi.optionsTab.turnTime.classic.30" : "Klassisk timer: 30 minuter",
+	"vcmi.optionsTab.turnTime.chess.20"   : "Schack-timer: 20:00 + 10:00 + 02:00 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.16"   : "Schack-timer: 16:00 + 08:00 + 01:30 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.8"    : "Schack-timer: 08:00 + 04:00 + 01:00 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.4"    : "Schack-timer: 04:00 + 02:00 + 00:30 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.2"    : "Schack-timer: 02:00 + 01:00 + 00:15 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.1"    : "Schack-timer: 01:00 + 01:00 + 00:00 + 00:00",
+
+	"vcmi.optionsTab.simturns.select"         : "Välj förinställning för simultana/samtidiga turer",
+	"vcmi.optionsTab.simturns.none"           : "Inga simultana/samtidiga turer",
+	"vcmi.optionsTab.simturns.tillContactMax" : "Simultantur: Fram till kontakt",
+	"vcmi.optionsTab.simturns.tillContact1"   : "Simultantur: 1 vecka, bryt vid kontakt",
+	"vcmi.optionsTab.simturns.tillContact2"   : "Simultantur: 2 veckor, bryt vid kontakt",
+	"vcmi.optionsTab.simturns.tillContact4"   : "Simultantur: 1 månad, bryt vid kontakt",
+	"vcmi.optionsTab.simturns.blocked1"       : "Simultantur: 1 vecka, kontakter blockerade",
+	"vcmi.optionsTab.simturns.blocked2"       : "Simultantur: 2 veckor, kontakter blockerade",
+	"vcmi.optionsTab.simturns.blocked4"       : "Simultantur: 1 månad, kontakter blockerade",
+
+	// Translation note: translate strings below using form that is correct for "0 days", "1 day" and "2 days" in your language
+	// Using this information, VCMI will automatically select correct plural form for every possible amount
+	"vcmi.optionsTab.simturns.days.0"   : " %d dagar",
+	"vcmi.optionsTab.simturns.days.1"   : " %d dag",
+	"vcmi.optionsTab.simturns.days.2"   : " %d dagar",
+	"vcmi.optionsTab.simturns.weeks.0"  : " %d veckor",
+	"vcmi.optionsTab.simturns.weeks.1"  : " %d vecka",
+	"vcmi.optionsTab.simturns.weeks.2"  : " %d veckor",
+	"vcmi.optionsTab.simturns.months.0" : " %d månader",
+	"vcmi.optionsTab.simturns.months.1" : " %d månad",
+	"vcmi.optionsTab.simturns.months.2" : " %d månader",
+
+	"vcmi.optionsTab.extraOptions.hover" : "Extra-inställningar",
+	"vcmi.optionsTab.extraOptions.help"  : "Ytterligare spelinställningar",
+
+	"vcmi.optionsTab.cheatAllowed.hover"    : "Tillåter fusk i spelet",
+	"vcmi.optionsTab.unlimitedReplay.hover" : "Obegränsade omspelningar av strider",
+	"vcmi.optionsTab.cheatAllowed.help"     : "{Tillåt fusk}\nTillåter inmatning av fuskkoder under spelets gång.",
+	"vcmi.optionsTab.unlimitedReplay.help"  : "{Obegränsade stridsomspelningar}\nIngen begränsning för hur många gånger man kan spela om sina strider.",
+
+	// Custom victory conditions for H3 campaigns and HotA maps
+	"vcmi.map.victoryCondition.daysPassed.toOthers"            : "Fienden har lyckats överleva fram till denna dag. Segern är deras!",
+	"vcmi.map.victoryCondition.daysPassed.toSelf"              : "Gratulerar! Du har lyckats överleva. Segern är er!",
+	"vcmi.map.victoryCondition.eliminateMonsters.toOthers"     : "Fienden har besegrat alla monster som plågade detta land och utropar seger!",
+	"vcmi.map.victoryCondition.eliminateMonsters.toSelf"       : "Gratulerar! Du har besegrat alla monster som plågade detta land och kan utropa seger!",
+	"vcmi.map.victoryCondition.collectArtifacts.message"       : "Förvärva tre artefakter",
+	"vcmi.map.victoryCondition.angelicAlliance.toSelf"         : "Gratulerar! Alla dina fiender har besegrats och du har 'Änglaalliansen'! Segern är din!",
+	"vcmi.map.victoryCondition.angelicAlliance.message"        : "Besegra alla fiender och sätt ihop 'Änglaalliansen'",
+	"vcmi.map.victoryCondition.angelicAlliancePartLost.toSelf" : "Ack, du har förlorat en del av 'Änglaalliansen'. Allt är förlorat.",
+
+	// few strings from WoG used by vcmi
+	"vcmi.stackExperience.description" : "» V a r e l s e f ö r b a n d s d e t a l j e r «\n\nVarelsetyp ................... : %s\nErfarenhetsrank ................. : %s (%i)\nErfarenhetspoäng ............... : %i\nErfarenhetspoäng till nästa rank .. : %i\nMaximal erfarenhet per strid ... : %i%% (%i)\nAntal varelser i förbandet .... : %i\nMaximalt antal nya rekryter\n utan att förlora nuvarande rank .... : %i\nErfarenhetsmultiplikator ........... : %.2f\nUppgradera erfarenhetsmultiplikator .............. : %.2f\nErfarenhet efter rank 10 ........ : %i\nMaximalt antal nyrekryteringar för att stanna kvar på\n rank 10 vid maximal erfarenhet : %i",
+	"vcmi.stackExperience.rank.0"  : "Grundläggande",
+	"vcmi.stackExperience.rank.1"  : "Novis",
+	"vcmi.stackExperience.rank.2"  : "Tränad",
+	"vcmi.stackExperience.rank.3"  : "Erfaren",
+	"vcmi.stackExperience.rank.4"  : "Beprövad",
+	"vcmi.stackExperience.rank.5"  : "Veteran",
+	"vcmi.stackExperience.rank.6"  : "Adept",
+	"vcmi.stackExperience.rank.7"  : "Expert",
+	"vcmi.stackExperience.rank.8"  : "Elit",
+	"vcmi.stackExperience.rank.9"  : "Mästare",
+	"vcmi.stackExperience.rank.10" : "Äss",
+
+	// Strings for HotA Seer Hut / Quest Guards
+	"core.seerhut.quest.heroClass.complete.0"    : "Ah, du är %s.  Här är en gåva till dig.  Tar du emot den?",
+	"core.seerhut.quest.heroClass.complete.1"    : "Ah, du är %s.  Här är en gåva till dig.  Tar du emot den?",
+	"core.seerhut.quest.heroClass.complete.2"    : "Ah, du är %s.  Här är en gåva till dig.  Tar du emot den?",
+	"core.seerhut.quest.heroClass.complete.3"    : "Vakterna ser att du är %s och erbjuder att låta dig passera.  Accepterar du?",
+	"core.seerhut.quest.heroClass.complete.4"    : "Vakterna ser att du är %s och erbjuder att låta dig passera.  Accepterar du?",
+	"core.seerhut.quest.heroClass.complete.5"    : "Vakterna ser att du är %s och erbjuder att låta dig passera.  Accepterar du?",
+	"core.seerhut.quest.heroClass.description.0" : "Skicka %s till %s",
+	"core.seerhut.quest.heroClass.description.1" : "Skicka %s till %s",
+	"core.seerhut.quest.heroClass.description.2" : "Skicka %s till %s",
+	"core.seerhut.quest.heroClass.description.3" : "Skicka %s för att öppna grinden",
+	"core.seerhut.quest.heroClass.description.4" : "Skicka %s till öppen grind",
+	"core.seerhut.quest.heroClass.description.5" : "Skicka %s till öppen grind",
+	"core.seerhut.quest.heroClass.hover.0"       : "(söker hjälte i %s klass)",
+	"core.seerhut.quest.heroClass.hover.1"       : "(söker hjälte i %s klass)",
+	"core.seerhut.quest.heroClass.hover.2"       : "(söker hjälte i %s klass)",
+	"core.seerhut.quest.heroClass.hover.3"       : "(söker hjälte i %s klass)",
+	"core.seerhut.quest.heroClass.hover.4"       : "(söker hjälte i %s klass)",
+	"core.seerhut.quest.heroClass.hover.5"       : "(söker hjälte i %s klass)",
+	"core.seerhut.quest.heroClass.receive.0"     : "Jag har en gåva till %s.",
+	"core.seerhut.quest.heroClass.receive.1"     : "Jag har en gåva till %s.",
+	"core.seerhut.quest.heroClass.receive.2"     : "Jag har en gåva till %s.",
+	"core.seerhut.quest.heroClass.receive.3"     : "Vakterna här säger att de bara kommer att låta %s passera.",
+	"core.seerhut.quest.heroClass.receive.4"     : "Vakterna här säger att de bara kommer att låta %s passera.",
+	"core.seerhut.quest.heroClass.receive.5"     : "Vakterna här säger att de bara kommer att låta %s passera.",
+	"core.seerhut.quest.heroClass.visit.0"       : "Du är inte %s.  Det finns inget här för dig att hämta. Försvinn!",
+	"core.seerhut.quest.heroClass.visit.1"       : "Du är inte %s.  Det finns inget här för dig att hämta. Försvinn!",
+	"core.seerhut.quest.heroClass.visit.2"       : "Du är inte %s.  Det finns inget här för dig att hämta. Försvinn!",
+	"core.seerhut.quest.heroClass.visit.3"       : "Vakterna här kommer bara att låta %s passera.",
+	"core.seerhut.quest.heroClass.visit.4"       : "Vakterna här kommer bara att låta %s passera.",
+	"core.seerhut.quest.heroClass.visit.5"       : "Vakterna här kommer bara att låta %s passera.",
+
+	"core.seerhut.quest.reachDate.complete.0"    : "Jag är fri nu.  Det här är vad jag har att erbjuda dig.  Accepterar du?",
+	"core.seerhut.quest.reachDate.complete.1"    : "Jag är fri nu.  Det här är vad jag har att erbjuda dig.  Accepterar du?",
+	"core.seerhut.quest.reachDate.complete.2"    : "Jag är fri nu.  Det här är vad jag har att erbjuda dig.  Accepterar du?",
+	"core.seerhut.quest.reachDate.complete.3"    : "Du är fri att gå igenom nu.  Önskar ni att passera?",
+	"core.seerhut.quest.reachDate.complete.4"    : "Du är fri att gå igenom nu.  Önskar ni att passera?",
+	"core.seerhut.quest.reachDate.complete.5"    : "Du är fri att gå igenom nu.  Önskar ni att passera?",
+	"core.seerhut.quest.reachDate.description.0" : "Vänta tills %s för %s",
+	"core.seerhut.quest.reachDate.description.1" : "Vänta tills %s för %s",
+	"core.seerhut.quest.reachDate.description.2" : "Vänta tills %s för %s",
+	"core.seerhut.quest.reachDate.description.3" : "Vänta tills %s öppnar grinden",
+	"core.seerhut.quest.reachDate.description.4" : "Vänta tills %s öppnar grinden",
+	"core.seerhut.quest.reachDate.description.5" : "Vänta tills %s öppnar grinden",
+	"core.seerhut.quest.reachDate.hover.0"       : "(Återvänd inte före %s)",
+	"core.seerhut.quest.reachDate.hover.1"       : "(Återvänd inte före %s)",
+	"core.seerhut.quest.reachDate.hover.2"       : "(Återvänd inte före %s)",
+	"core.seerhut.quest.reachDate.hover.3"       : "(Återvänd inte före %s)",
+	"core.seerhut.quest.reachDate.hover.4"       : "(Återvänd inte före %s)",
+	"core.seerhut.quest.reachDate.hover.5"       : "(Återvänd inte före %s)",
+	"core.seerhut.quest.reachDate.receive.0"     : "Jag är upptagen.  Kom inte tillbaka före %s",
+	"core.seerhut.quest.reachDate.receive.1"     : "Jag är upptagen.  Kom inte tillbaka före %s",
+	"core.seerhut.quest.reachDate.receive.2"     : "Jag är upptagen.  Kom inte tillbaka före %s",
+	"core.seerhut.quest.reachDate.receive.3"     : "Stängt fram till %s.",
+	"core.seerhut.quest.reachDate.receive.4"     : "Stängt fram till %s.",
+	"core.seerhut.quest.reachDate.receive.5"     : "Stängt fram till %s.",
+	"core.seerhut.quest.reachDate.visit.0"       : "Jag är upptagen.  Kom inte tillbaka före %s.",
+	"core.seerhut.quest.reachDate.visit.1"       : "Jag är upptagen.  Kom inte tillbaka före %s.",
+	"core.seerhut.quest.reachDate.visit.2"       : "Jag är upptagen.  Kom inte tillbaka före %s.",
+	"core.seerhut.quest.reachDate.visit.3"       : "Stängt fram till %s.",
+	"core.seerhut.quest.reachDate.visit.4"       : "Stängt fram till %s.",
+	"core.seerhut.quest.reachDate.visit.5"       : "Stängt fram till %s.",
+
+	"core.bonus.ADDITIONAL_ATTACK.name"                  : "Dubbelslag",
+	"core.bonus.ADDITIONAL_ATTACK.description"           : "Attackerar två gånger",
+	"core.bonus.ADDITIONAL_RETALIATION.name"             : "Ytterligare motattacker",
+	"core.bonus.ADDITIONAL_RETALIATION.description"      : "Kan slå tillbaka ${val} extra gånger",
+	"core.bonus.AIR_IMMUNITY.name"                       : "Luft-immunitet",
+	"core.bonus.AIR_IMMUNITY.description"                : "Immun mot alla trollformler från skolan för luftmagi",
+	"core.bonus.ATTACKS_ALL_ADJACENT.name"               : "Attackera runtomkring",
+	"core.bonus.ATTACKS_ALL_ADJACENT.description"        : "Attackerar alla angränsande fiender",
+	"core.bonus.BLOCKS_RETALIATION.name"                 : "Ingen motattack",
+	"core.bonus.BLOCKS_RETALIATION.description"          : "Fienden kan inte slå tillbaka/retaliera",
+	"core.bonus.BLOCKS_RANGED_RETALIATION.name"          : "Ingen motattack på avstånd",
+	"core.bonus.BLOCKS_RANGED_RETALIATION.description"   : "Fienden kan inte göra en motattack/retaliering på avstånd genom att använda en distansattack",
+	"core.bonus.CATAPULT.name"                           : "Katapult",
+	"core.bonus.CATAPULT.description"                    : "Attackerar belägringsmurar",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name"        : "Minska trollformelkostnaden (${val})",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description" : "Minskar trollformelkostnaden för hjälten med ${val}",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name"       : "Magisk dämpare (${val})",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Ökar trollformelkostnaden för fiendens trollformler med ${val}",
+	"core.bonus.CHARGE_IMMUNITY.name"                    : "Galoppanfalls-immunitet",
+	"core.bonus.CHARGE_IMMUNITY.description"             : "Immun mot ryttares och tornerares galopperande ridanfall",
+	"core.bonus.DARKNESS.name"                           : "I skydd av mörkret",
+	"core.bonus.DARKNESS.description"                    : "Skapar ett hölje av mörker med en ${val}-rutorsradie",
+	"core.bonus.DEATH_STARE.name"                        : "Dödsblick (${val}%)",
+	"core.bonus.DEATH_STARE.description"                 : "Varje förbandsenhet med 'Dödsblick' har ${val}% chans att döda den översta enheten i ett fiendeförband",
+	"core.bonus.DEFENSIVE_STANCE.name"                   : "Försvarshållning",
+	"core.bonus.DEFENSIVE_STANCE.description"            : "Ger ytterligare +${val} till enhetens försvarsförmåga när du väljer att försvarar dig",
+	"core.bonus.DESTRUCTION.name"                        : "Förintelse",
+	"core.bonus.DESTRUCTION.description"                 : "Har ${val}% chans att döda extra enheter efter attack",
+	"core.bonus.DOUBLE_DAMAGE_CHANCE.name"               : "Dödsstöt",
+	"core.bonus.DOUBLE_DAMAGE_CHANCE.description"        : "Har ${val}% chans att ge dubbel basskada vid attack",
+	"core.bonus.DRAGON_NATURE.name"                      : "Drake",
+	"core.bonus.DRAGON_NATURE.description"               : "Varelsen har en draknatur",
+	"core.bonus.EARTH_IMMUNITY.name"                     : "Jord-immunitet",
+	"core.bonus.EARTH_IMMUNITY.description"              : "Immun mot alla trollformler från skolan för jordmagi",
+	"core.bonus.ENCHANTER.name"                          : "Förtrollare",
+	"core.bonus.ENCHANTER.description"                   : "Kan kasta ${subtyp.spell} på alla varje tur/omgång",
+	"core.bonus.ENCHANTED.name"                          : "Förtrollad",
+	"core.bonus.ENCHANTED.description"                   : "Påverkas av permanent ${subtype.spell}",
+	"core.bonus.ENEMY_ATTACK_REDUCTION.name"             : "Avfärda attack (${val}%)",
+	"core.bonus.ENEMY_ATTACK_REDUCTION.description"      : "När du blir attackerad ignoreras ${val}% av angriparens attack",
+	"core.bonus.ENEMY_DEFENCE_REDUCTION.name"            : "Förbigå försvar (${val}%)",
+	"core.bonus.ENEMY_DEFENCE_REDUCTION.description"     : "När du attackerar ignoreras ${val}% av försvararens försvar",
+	"core.bonus.FIRE_IMMUNITY.name"                      : "Eld-immunitet",
+	"core.bonus.FIRE_IMMUNITY.description"               : "Immun mot alla trollformler från skolan för eldmagi",
+	"core.bonus.FIRE_SHIELD.name"                        : "Eldsköld (${val}%)",
+	"core.bonus.FIRE_SHIELD.description"                 : "Reflekterar en del av närstridsskadorna",
+	"core.bonus.FIRST_STRIKE.name"                       : "Första slaget",
+	"core.bonus.FIRST_STRIKE.description"                : "Denna varelse gör en motattack innan den blir attackerad",
+	"core.bonus.FEAR.name"                               : "Rädsla",
+	"core.bonus.FEAR.description"                        : "Orsakar rädsla på ett fiendeförband",
+	"core.bonus.FEARLESS.name"                           : "Orädd",
+	"core.bonus.FEARLESS.description"                    : "Immun mot rädsla",
+	"core.bonus.FEROCITY.name"                           : "Vildsint",
+	"core.bonus.FEROCITY.description"                    : "Attackerar ${val} extra gång(er) om någon dödas",
+	"core.bonus.FLYING.name"                             : "Flygande",
+	"core.bonus.FLYING.description"                      : "Flyger vid förflyttning (ignorerar hinder)",
+	"core.bonus.FREE_SHOOTING.name"                      : "Skjut på nära håll",
+	"core.bonus.FREE_SHOOTING.description"               : "Kan använda distansattacker på närstridsavstånd",
+	"core.bonus.GARGOYLE.name"                           : "Stenfigur",
+	"core.bonus.GARGOYLE.description"                    : "Kan varken upplivas eller läkas",
+	"core.bonus.GENERAL_DAMAGE_REDUCTION.name"           : "Minska skada (${val}%)",
+	"core.bonus.GENERAL_DAMAGE_REDUCTION.description"    : "Reducerar fysisk skada från både distans- och närstridsattacker",
+	"core.bonus.HATE.name"                               : "Hatar ${subtyp.varelse}",
+	"core.bonus.HATE.description"                        : "Gör ${val}% mer skada mot ${subtyp.varelse}",
+	"core.bonus.HEALER.name"                             : "Helare",
+	"core.bonus.HEALER.description"                      : "Helar/läker allierade enheter",
+	"core.bonus.HP_REGENERATION.name"                    : "Självläkande",
+	"core.bonus.HP_REGENERATION.description"             : "Får tillbaka ${val} träffpoäng (hälsa) varje runda",
+	"core.bonus.JOUSTING.name"                           : "Galopperande ridanfall",
+	"core.bonus.JOUSTING.description"                    : "Orsakar +${val}% extra skada för varje ruta som enheten förflyttas innan attack",
+	"core.bonus.KING.name"                               : "Kung",
+	"core.bonus.KING.description"                        : "Sårbar för 'Dräpar'-nivå ${val} eller högre",
+	"core.bonus.LEVEL_SPELL_IMMUNITY.name"               : "Förtrollningsimmunitet 1-${val}",
+	"core.bonus.LEVEL_SPELL_IMMUNITY.description"        : "Immun mot trollformler på nivå 1-${val}",
+	"core.bonus.LIMITED_SHOOTING_RANGE.name"             : "Begränsad räckvidd för skjutning",
+	"core.bonus.LIMITED_SHOOTING_RANGE.description"      : "Kan inte sikta på enheter längre bort än ${val} rutor",
+	"core.bonus.LIFE_DRAIN.name"                         : "Dränerar livskraft (${val}%)",
+	"core.bonus.LIFE_DRAIN.description"                  : "Dränerar ${val}% träffpoäng (hälsa) av utdelad skada",
+	"core.bonus.MANA_CHANNELING.name"                    : "Kanalisera trollformelspoäng ${val}%",
+	"core.bonus.MANA_CHANNELING.description"             : "Ger din hjälte ${val}% av den mängd trollformelspoäng som fienden spenderar per trollformel i strid",
+	"core.bonus.MANA_DRAIN.name"                         : "Dränera trollformelspoäng",
+	"core.bonus.MANA_DRAIN.description"                  : "Dränerar ${val} trollformelspoäng varje tur",
+	"core.bonus.MAGIC_MIRROR.name"                       : "Magisk spegel (${val}%)",
+	"core.bonus.MAGIC_MIRROR.description"                : "Har ${val}% chans att reflektera (omdirigera) en offensiv trollformel på en fiendeenhet",
+	"core.bonus.MAGIC_RESISTANCE.name"                   : "Magiskt motstånd (${val}%)",
+	"core.bonus.MAGIC_RESISTANCE.description"            : "Har en ${val}% chans att motstå en skadlig trollformel",
+	"core.bonus.MIND_IMMUNITY.name"                      : "Immunitet mot sinnesförtrollningar",
+	"core.bonus.MIND_IMMUNITY.description"               : "Immun mot förtrollningar som påverkar dina sinnen",
+	"core.bonus.NO_DISTANCE_PENALTY.name"                : "Ingen avståndsbestraffning",
+	"core.bonus.NO_DISTANCE_PENALTY.description"         : "Gör full skada på vilket avstånd som helst i strid",
+	"core.bonus.NO_MELEE_PENALTY.name"                   : "Ingen närstridsbestraffning",
+	"core.bonus.NO_MELEE_PENALTY.description"            : "Varelsen har ingen närstridsbestraffning",
+	"core.bonus.NO_MORALE.name"                          : "Ingen Moralpåverkan",
+	"core.bonus.NO_MORALE.description"                   : "Varelsen är immun mot moraliska effekter och har alltid neutral moral",
+	"core.bonus.NO_WALL_PENALTY.name"                    : "Ingen murbestraffning",
+	"core.bonus.NO_WALL_PENALTY.description"             : "Orsakar full skada mot fiender bakom en mur",
+	"core.bonus.NON_LIVING.name"                         : "Icke levande",
+	"core.bonus.NON_LIVING.description"                  : "Immunitet mot många effekter som annars bara påverkar levande och odöda varelser",
+	"core.bonus.RANDOM_SPELLCASTER.name"                 : "Slumpmässig besvärjare",
+	"core.bonus.RANDOM_SPELLCASTER.description"          : "Kan kasta trollformler som väljs slumpmässigt",
+	"core.bonus.RANGED_RETALIATION.name"                 : "Motattacker på avstånd",
+	"core.bonus.RANGED_RETALIATION.description"          : "Kan retaliera/motattackera på avstånd",
+	"core.bonus.RECEPTIVE.name"                          : "Mottaglig",
+	"core.bonus.RECEPTIVE.description"                   : "Ingen immunitet mot vänliga besvärjelser",
+	"core.bonus.REBIRTH.name"                            : "Återfödelse (${val}%)",
+	"core.bonus.REBIRTH.description"                     : "${val}% av enheterna kommer att återuppväckas efter döden",
+	"core.bonus.RETURN_AFTER_STRIKE.name"                : "Återvänder efter närstrid",
+	"core.bonus.RETURN_AFTER_STRIKE.description"         : "Efter att ha attackerat en fiendeenhet i närstrid återvänder enheten till rutan som den var placerad på innan den utförde sin närstridsattack",
+	"core.bonus.REVENGE.name"                            : "Hämnd",
+	"core.bonus.REVENGE.description"                     : "Orsakar extra skada baserat på angriparens förlorade träffpoäng (hälsa) i strid",
+	"core.bonus.SHOOTER.name"                            : "Distans-attack",
+	"core.bonus.SHOOTER.description"                     : "Varelsen kan skjuta/attackera på avstånd",
+	"core.bonus.SHOOTS_ALL_ADJACENT.name"                : "Skjuter alla i närheten",
+	"core.bonus.SHOOTS_ALL_ADJACENT.description"         : "Denna varelses distans-attacker drabbar alla mål i ett litet område",
+	"core.bonus.SOUL_STEAL.name"                         : "Själtjuv",
+	"core.bonus.SOUL_STEAL.description"                  : "Återuppväcker ${val} av sina egna enheter för varje dödad fiendeenhet",
+	"core.bonus.SPELLCASTER.name"                        : "Besvärjare",
+	"core.bonus.SPELLCASTER.description"                 : "Kan kasta ${subtype.spell}",
+	"core.bonus.SPELL_AFTER_ATTACK.name"                 : "Besvärja efter attack",
+	"core.bonus.SPELL_AFTER_ATTACK.description"          : "Har en ${val}% chans att kasta ${subtype.spell} efter att den har attackerat",
+	"core.bonus.SPELL_BEFORE_ATTACK.name"                : "Besvärja före attack",
+	"core.bonus.SPELL_BEFORE_ATTACK.description"         : "Har en ${val}% chans att kasta ${subtype.spell} innan den attackerar",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name"             : "Trolldoms-resistens",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description"      : "Skadan från trollformler är reducet med ${val}%.",
+	"core.bonus.SPELL_IMMUNITY.name"                     : "Trolldoms-immunitet",
+	"core.bonus.SPELL_IMMUNITY.description"              : "Immun mot ${subtype.spell}",
+	"core.bonus.SPELL_LIKE_ATTACK.name"                  : "Trolldomsliknande attack",
+	"core.bonus.SPELL_LIKE_ATTACK.description"           : "Attackerar med ${subtype.spell}",
+	"core.bonus.SPELL_RESISTANCE_AURA.name"              : "Motståndsaura",
+	"core.bonus.SPELL_RESISTANCE_AURA.description"       : "Närbelägna förband får ${val}% magi-resistens",
+	"core.bonus.SUMMON_GUARDIANS.name"                   : "Åkalla väktare",
+	"core.bonus.SUMMON_GUARDIANS.description"            : "I början av striden åkallas ${subtype.creature} (${val}%)",
+	"core.bonus.SYNERGY_TARGET.name"                     : "Synergibar",
+	"core.bonus.SYNERGY_TARGET.description"              : "Denna varelse är sårbar för synergieffekt",
+	"core.bonus.TWO_HEX_ATTACK_BREATH.name"              : "Dödlig andedräkt",
+	"core.bonus.TWO_HEX_ATTACK_BREATH.description"       : "Andningsattack (2 rutors räckvidd)",
+	"core.bonus.THREE_HEADED_ATTACK.name"                : "Trehövdad attack",
+	"core.bonus.THREE_HEADED_ATTACK.description"         : "Attackerar tre angränsande enheter",
+	"core.bonus.TRANSMUTATION.name"                      : "Transmutation",
+	"core.bonus.TRANSMUTATION.description"               : "${val}% chans att förvandla angripen enhet till en annan typ",
+	"core.bonus.UNDEAD.name"                             : "Odöd",
+	"core.bonus.UNDEAD.description"                      : "Varelsen är odöd",
+	"core.bonus.UNLIMITED_RETALIATIONS.name"             : "Obegränsat antal motattacker",
+	"core.bonus.UNLIMITED_RETALIATIONS.description"      : "Kan slå tillbaka mot ett obegränsat antal attacker varje omgång",
+	"core.bonus.WATER_IMMUNITY.name"                     : "Vatten-immunitet",
+	"core.bonus.WATER_IMMUNITY.description"              : "Immun mot alla trollformler från vattenmagi-skolan",
+	"core.bonus.WIDE_BREATH.name"                        : "Bred dödlig andedräkt",
+	"core.bonus.WIDE_BREATH.description"                 : "Bred andningsattack (flera rutor)",
+	"core.bonus.DISINTEGRATE.name"                       : "Desintegrerar",
+	"core.bonus.DISINTEGRATE.description"                : "Ingen fysisk kropp finns kvar efter att enheten blivit besegrad i strid",
+	"core.bonus.INVINCIBLE.name"                         : "Oövervinnerlig",
+	"core.bonus.INVINCIBLE.description"                  : "Kan inte påverkas av någonting"
+}

+ 21 - 10
Mods/vcmi/mod.json

@@ -77,25 +77,36 @@
 		]
 	},
 
-	"ukrainian" : {
-		"name" : "VCMI - ключові файли",
-		"description" : "Ключові файли необхідні для повноцінної роботи VCMI",
-		"author" : "Команда VCMI",
+	"spanish" : {
+		"name" : "VCMI - ficheros necesarios",
+		"description" : "Ficheros necesarios para ejecutar VCMI correctamente",
+		"author" : "Abel Rivas",
 
 		"skipValidation" : true,
 		"translations" : [
-			"config/vcmi/ukrainian.json"
+			"config/vcmi/spanish.json"
 		]
 	},
 
-	"spanish" : {
-		"name" : "VCMI - ficheros necesarios",
-		"description" : "Ficheros necesarios para ejecutar VCMI correctamente",
-		"author" : "Abel Rivas",
+	"swedish" : {
+		"name" : "Nödvändiga VCMI-filer",
+		"description" : "Filer som behövs för att köra VCMI korrekt",
+		"author" : "Maurycy (XCOM-HUB on GitHub)",
 
 		"skipValidation" : true,
 		"translations" : [
-			"config/vcmi/spanish.json"
+			"config/vcmi/swedish.json"
+		]
+	},
+
+	"ukrainian" : {
+		"name" : "VCMI - ключові файли",
+		"description" : "Ключові файли необхідні для повноцінної роботи VCMI",
+		"author" : "Команда VCMI",
+
+		"skipValidation" : true,
+		"translations" : [
+			"config/vcmi/ukrainian.json"
 		]
 	},
 

+ 2 - 0
client/CPlayerInterface.cpp

@@ -420,6 +420,8 @@ void CPlayerInterface::heroCreated(const CGHeroInstance * hero)
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	localState->addWanderingHero(hero);
 	adventureInt->onHeroChanged(hero);
+	if(castleInt)
+		CCS->soundh->playSound(soundBase::newBuilding);
 }
 void CPlayerInterface::openTownWindow(const CGTownInstance * town)
 {

+ 1 - 1
client/Client.h

@@ -189,7 +189,7 @@ public:
 	bool swapGarrisonOnSiege(ObjectInstanceID tid) override {return false;};
 	bool giveHeroNewArtifact(const CGHeroInstance * h, const ArtifactID & artId, const ArtifactPosition & pos) override {return false;};
 	bool giveHeroNewScroll(const CGHeroInstance * h, const SpellID & spellId, const ArtifactPosition & pos) override {return false;};
-	bool putArtifact(const ArtifactLocation & al, const CArtifactInstance * art, std::optional<bool> askAssemble) override {return false;};
+	bool putArtifact(const ArtifactLocation & al, const ArtifactInstanceID & id, std::optional<bool> askAssemble) override {return false;};
 	void removeArtifact(const ArtifactLocation & al) override {};
 	bool moveArtifact(const PlayerColor & player, const ArtifactLocation & al1, const ArtifactLocation & al2) override {return false;};
 

+ 1 - 1
client/battle/BattleWindow.cpp

@@ -743,7 +743,7 @@ void BattleWindow::bSpellf()
 			const auto artID = blockingBonus->sid.as<ArtifactID>();
 			//If we have artifact, put name of our hero. Otherwise assume it's the enemy.
 			//TODO check who *really* is source of bonus
-			std::string heroName = myHero->hasArt(artID) ? myHero->getNameTranslated() : owner.enemyHero().name;
+			std::string heroName = myHero->hasArt(artID, true) ? myHero->getNameTranslated() : owner.enemyHero().name;
 
 			//%s wields the %s, an ancient artifact which creates a p dead to all magic.
 			LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[683])

+ 2 - 2
client/lobby/OptionsTab.cpp

@@ -1037,11 +1037,11 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con
 	}
 	const auto & font = GH.renderHandler().loadFont(FONT_SMALL);
 
-	labelWhoCanPlay = std::make_shared<CMultiLineLabel>(Rect(6, 23, 45, font->getLineHeight()*2), EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->arraytxt[206 + whoCanPlay]);
+	labelWhoCanPlay = std::make_shared<CMultiLineLabel>(Rect(6, 23, 45, font->getLineHeight()*2), EFonts::FONT_TINY, ETextAlignment::TOPCENTER, Colors::WHITE, CGI->generaltexth->arraytxt[206 + whoCanPlay]);
 
 	auto hasHandicap = [this](){ return s->handicap.startBonus.empty() && s->handicap.percentIncome == 100 && s->handicap.percentGrowth == 100; };
 	std::string labelHandicapText = hasHandicap() ? CGI->generaltexth->arraytxt[210] : MetaString::createFromTextID("vcmi.lobby.handicap").toString();
-	labelHandicap = std::make_shared<CMultiLineLabel>(Rect(57, 24, 47, font->getLineHeight()*2), EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, labelHandicapText);
+	labelHandicap = std::make_shared<CMultiLineLabel>(Rect(57, 24, 47, font->getLineHeight()*2), EFonts::FONT_TINY, ETextAlignment::TOPCENTER, Colors::WHITE, labelHandicapText);
 	handicap = std::make_shared<LRClickableArea>(Rect(56, 24, 49, font->getLineHeight()*2), [](){
 		if(!CSH->isHost())
 			return;

+ 59 - 18
client/media/CVideoHandler.cpp

@@ -33,7 +33,9 @@ extern "C" {
 #include <libavformat/avformat.h>
 #include <libavcodec/avcodec.h>
 #include <libavutil/imgutils.h>
+#include <libavutil/opt.h>
 #include <libswscale/swscale.h>
+#include <libswresample/swresample.h>
 }
 
 // Define a set of functions to read data
@@ -501,32 +503,71 @@ std::pair<std::unique_ptr<ui8 []>, si64> CAudioInstance::extractAudio(const Vide
 	int numChannels = codecpar->ch_layout.nb_channels;
 #endif
 
-	samples.reserve(44100 * 5); // arbitrary 5-second buffer
+	samples.reserve(44100 * 5); // arbitrary 5-second buffer to reduce reallocations
 
-	for (;;)
+	if (formatProperties.isPlanar && numChannels > 1)
 	{
-		decodeNextFrame();
-		const AVFrame * frame = getCurrentFrame();
+		// Format is 'planar', which is not supported by wav / SDL
+		// Use swresample part of ffmpeg to deplanarize audio into format supported by wav / SDL
 
-		if (!frame)
-			break;
+		auto sourceFormat = static_cast<AVSampleFormat>(codecpar->format);
+		auto targetFormat = av_get_alt_sample_fmt(sourceFormat, false);
 
-		int samplesToRead = frame->nb_samples * numChannels;
-		int bytesToRead = samplesToRead * formatProperties.sampleSizeBytes;
+		SwrContext * swr_ctx = swr_alloc();
 
-		if (formatProperties.isPlanar && numChannels > 1)
+#if (LIBAVUTIL_VERSION_MAJOR < 58)
+		av_opt_set_channel_layout(swr_ctx, "in_chlayout", codecpar->channel_layout, 0);
+		av_opt_set_channel_layout(swr_ctx, "out_chlayout", codecpar->channel_layout, 0);
+#else
+		av_opt_set_chlayout(swr_ctx, "in_chlayout", &codecpar->ch_layout, 0);
+		av_opt_set_chlayout(swr_ctx, "out_chlayout", &codecpar->ch_layout, 0);
+#endif
+		av_opt_set_int(swr_ctx, "in_sample_rate", codecpar->sample_rate, 0);
+		av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", sourceFormat, 0);
+		av_opt_set_int(swr_ctx, "out_sample_rate", codecpar->sample_rate, 0);
+		av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", targetFormat, 0);
+
+		int initResult = swr_init(swr_ctx);
+		if (initResult < 0)
+			throwFFmpegError(initResult);
+
+		std::vector<uint8_t> frameSamplesBuffer;
+		for (;;)
 		{
-			// Workaround for lack of resampler
-			// Currently, ffmpeg on conan systems is built without sws resampler
-			// Because of that, and because wav format does not supports 'planar' formats from ffmpeg
-			// we need to de-planarize it and convert to "normal" (non-planar / interleaved) stream
-			samples.reserve(samples.size() + bytesToRead);
-			for (int sm = 0; sm < frame->nb_samples; ++sm)
-				for (int ch = 0; ch < numChannels; ++ch)
-					samples.insert(samples.end(), frame->data[ch] + sm * formatProperties.sampleSizeBytes, frame->data[ch] + (sm+1) * formatProperties.sampleSizeBytes );
+			decodeNextFrame();
+			const AVFrame * frame = getCurrentFrame();
+
+			if (!frame)
+				break;
+
+			size_t samplesToRead = frame->nb_samples * numChannels;
+			size_t bytesToRead = samplesToRead * formatProperties.sampleSizeBytes;
+			frameSamplesBuffer.resize(std::max(frameSamplesBuffer.size(), bytesToRead));
+			uint8_t * frameSamplesPtr = frameSamplesBuffer.data();
+
+			int result = swr_convert(swr_ctx, &frameSamplesPtr, frame->nb_samples, (const uint8_t **)frame->data, frame->nb_samples);
+
+			if (result < 0)
+				throwFFmpegError(result);
+
+			size_t samplesToCopy = result * numChannels;
+			size_t bytesToCopy = samplesToCopy * formatProperties.sampleSizeBytes;
+			samples.insert(samples.end(), frameSamplesBuffer.begin(), frameSamplesBuffer.begin() + bytesToCopy);
 		}
-		else
+		swr_free(&swr_ctx);
+	}
+	else
+	{
+		for (;;)
 		{
+			decodeNextFrame();
+			const AVFrame * frame = getCurrentFrame();
+
+			if (!frame)
+				break;
+
+			size_t samplesToRead = frame->nb_samples * numChannels;
+			size_t bytesToRead = samplesToRead * formatProperties.sampleSizeBytes;
 			samples.insert(samples.end(), frame->data[0], frame->data[0] + bytesToRead);
 		}
 	}

+ 14 - 9
client/render/AssetGenerator.cpp

@@ -18,6 +18,10 @@
 #include "../render/IRenderHandler.h"
 
 #include "../lib/filesystem/Filesystem.h"
+#include "../lib/GameSettings.h"
+#include "../lib/IGameSettings.h"
+#include "../lib/json/JsonNode.h"
+#include "../lib/VCMI_Lib.h"
 
 void AssetGenerator::generateAll()
 {
@@ -138,16 +142,17 @@ void AssetGenerator::createPlayerColoredBackground(const PlayerColor & player)
 
 	std::shared_ptr<IImage> texture = GH.renderHandler().loadImage(locator, EImageBlitMode::OPAQUE);
 
-	// Color transform to make color of brown DIBOX.PCX texture match color of specified player
+	// transform to make color of brown DIBOX.PCX texture match color of specified player
+	auto filterSettings = VLC->settingsHandler->getFullConfig()["interface"]["playerColoredBackground"];
 	static const std::array<ColorFilter, PlayerColor::PLAYER_LIMIT_I> filters = {
-		ColorFilter::genRangeShifter(  0.25,  0,     0,     1.25, 0.00, 0.00 ), // red
-		ColorFilter::genRangeShifter(  0,     0,     0,     0.45, 1.20, 4.50 ), // blue
-		ColorFilter::genRangeShifter(  0.40,  0.27,  0.23,  1.10, 1.20, 1.15 ), // tan
-		ColorFilter::genRangeShifter( -0.27,  0.10, -0.27,  0.70, 1.70, 0.70 ), // green
-		ColorFilter::genRangeShifter(  0.47,  0.17, -0.27,  1.60, 1.20, 0.70 ), // orange
-		ColorFilter::genRangeShifter(  0.12, -0.1,   0.25,  1.15, 1.20, 2.20 ), // purple
-		ColorFilter::genRangeShifter( -0.13,  0.23,  0.23,  0.90, 1.20, 2.20 ), // teal
-		ColorFilter::genRangeShifter(  0.44,  0.15,  0.25,  1.00, 1.00, 1.75 )  // pink
+		ColorFilter::genRangeShifter( filterSettings["red"   ].convertTo<std::vector<float>>() ),
+		ColorFilter::genRangeShifter( filterSettings["blue"  ].convertTo<std::vector<float>>() ),
+		ColorFilter::genRangeShifter( filterSettings["tan"   ].convertTo<std::vector<float>>() ),
+		ColorFilter::genRangeShifter( filterSettings["green" ].convertTo<std::vector<float>>() ),
+		ColorFilter::genRangeShifter( filterSettings["orange"].convertTo<std::vector<float>>() ),
+		ColorFilter::genRangeShifter( filterSettings["purple"].convertTo<std::vector<float>>() ),
+		ColorFilter::genRangeShifter( filterSettings["teal"  ].convertTo<std::vector<float>>() ),
+		ColorFilter::genRangeShifter( filterSettings["pink"  ].convertTo<std::vector<float>>() )
 	};
 
 	assert(player.isValidPlayer());

+ 7 - 0
client/render/ColorFilter.cpp

@@ -70,6 +70,13 @@ ColorFilter ColorFilter::genRangeShifter( float minR, float minG, float minB, fl
 				  1.f);
 }
 
+ColorFilter ColorFilter::genRangeShifter( std::vector<float> parameters )
+{
+	assert(std::size(parameters) == 6);
+
+	return genRangeShifter(parameters[0], parameters[1], parameters[2], parameters[3], parameters[4], parameters[5]);
+}
+
 ColorFilter ColorFilter::genMuxerShifter( ChannelMuxer r, ChannelMuxer g, ChannelMuxer b, float a )
 {
 	return ColorFilter(r, g, b, a);

+ 1 - 0
client/render/ColorFilter.h

@@ -44,6 +44,7 @@ public:
 
 	/// Generates object that transforms each channel independently
 	static ColorFilter genRangeShifter( float minR, float minG, float minB, float maxR, float maxG, float maxB );
+	static ColorFilter genRangeShifter( std::vector<float> parameters );
 
 	/// Generates object that performs arbitrary mixing between any channels
 	static ColorFilter genMuxerShifter( ChannelMuxer r, ChannelMuxer g, ChannelMuxer b, float a );

+ 6 - 0
client/renderSDL/CBitmapFont.cpp

@@ -212,6 +212,12 @@ CBitmapFont::CBitmapFont(const std::string & filename):
 		SDL_FreeSurface(atlasImage);
 		atlasImage = scaledSurface;
 	}
+
+	logGlobal->debug("Loaded BMP font: '%s', height %d, ascent %d",
+					 filename,
+					 getLineHeightScaled(),
+					 getFontAscentScaled()
+					 );
 }
 
 CBitmapFont::~CBitmapFont()

+ 8 - 0
client/renderSDL/CTrueTypeFont.cpp

@@ -73,6 +73,14 @@ CTrueTypeFont::CTrueTypeFont(const JsonNode & fontConfig):
 	TTF_SetFontStyle(font.get(), getFontStyle(fontConfig));
 	TTF_SetFontHinting(font.get(),TTF_HINTING_MONO);
 
+	logGlobal->debug("Loaded TTF font: '%s', point size %d, height %d, ascent %d, descent %d, line skip %d",
+					 fontConfig["file"].String(),
+					 getPointSize(fontConfig["size"]),
+					 TTF_FontHeight(font.get()),
+					 TTF_FontAscent(font.get()),
+					 TTF_FontDescent(font.get()),
+					 TTF_FontLineSkip(font.get())
+	);
 }
 
 CTrueTypeFont::~CTrueTypeFont() = default;

+ 20 - 2
client/renderSDL/FontChain.cpp

@@ -13,8 +13,13 @@
 #include "CTrueTypeFont.h"
 #include "CBitmapFont.h"
 
+#include "../CGameInfo.h"
+
 #include "../../lib/CConfigHandler.h"
+#include "../../lib/modding/CModHandler.h"
 #include "../../lib/texts/TextOperations.h"
+#include "../../lib/texts/CGeneralTextHandler.h"
+#include "../../lib/texts/Languages.h"
 
 void FontChain::renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const
 {
@@ -39,7 +44,7 @@ size_t FontChain::getFontAscentScaled() const
 	return maxHeight;
 }
 
-bool FontChain::bitmapFontsPrioritized() const
+bool FontChain::bitmapFontsPrioritized(const std::string & bitmapFontName) const
 {
 	const std::string & fontType = settings["video"]["fontsType"].String();
 	if (fontType == "original")
@@ -55,6 +60,19 @@ bool FontChain::bitmapFontsPrioritized() const
 	if (!vstd::isAlmostEqual(1.0, settings["video"]["fontScalingFactor"].Float()))
 		return false; // If player requested non-100% scaling - use scalable fonts
 
+	std::string modName = CGI->modh->findResourceOrigin(ResourcePath("data/" + bitmapFontName, EResType::BMP_FONT));
+	std::string fontLanguage = CGI->modh->getModLanguage(modName);
+	std::string gameLanguage = CGI->generaltexth->getPreferredLanguage();
+	std::string fontEncoding = Languages::getLanguageOptions(fontLanguage).encoding;
+	std::string gameEncoding = Languages::getLanguageOptions(gameLanguage).encoding;
+
+	// player uses language with different encoding than his bitmap fonts
+	// for example, Polish language with English fonts or Chinese language which can't use H3 fonts at all
+	// this may result in unintended mixing of ttf and bitmap fonts, which may have a bit different look
+	// so in this case prefer ttf fonts that are likely to cover target language better than H3 fonts
+	if (fontEncoding != gameEncoding)
+		return false;
+
 	return true; // else - use original bitmap fonts
 }
 
@@ -65,7 +83,7 @@ void FontChain::addTrueTypeFont(const JsonNode & trueTypeConfig)
 
 void FontChain::addBitmapFont(const std::string & bitmapFilename)
 {
-	if (bitmapFontsPrioritized())
+	if (bitmapFontsPrioritized(bitmapFilename))
 		chain.insert(chain.begin(), std::make_unique<CBitmapFont>(bitmapFilename));
 	else
 		chain.push_back(std::make_unique<CBitmapFont>(bitmapFilename));

+ 1 - 1
client/renderSDL/FontChain.h

@@ -29,7 +29,7 @@ class FontChain final : public IFont
 
 	void renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const override;
 	size_t getFontAscentScaled() const override;
-	bool bitmapFontsPrioritized() const;
+	bool bitmapFontsPrioritized(const std::string & bitmapFontName) const;
 public:
 	FontChain() = default;
 

+ 2 - 0
client/renderSDL/RenderHandler.cpp

@@ -342,6 +342,8 @@ std::shared_ptr<const IFont> RenderHandler::loadFont(EFonts font)
 		return fonts.at(font);
 
 	const int8_t index = static_cast<int8_t>(font);
+	logGlobal->debug("Loading font %d", static_cast<int>(index));
+
 	auto configList = CResourceHandler::get()->getResourcesWithName(JsonPath::builtin("config/fonts.json"));
 	std::shared_ptr<FontChain> loadedFont = std::make_shared<FontChain>();
 	std::string bitmapPath;

+ 3 - 3
client/widgets/CArtifactsOfHeroBase.cpp

@@ -137,9 +137,9 @@ void CArtifactsOfHeroBase::scrollBackpack(bool left)
 	LOCPLINT->cb->scrollBackpackArtifacts(curHero->id, left);
 }
 
-void CArtifactsOfHeroBase::markPossibleSlots(const CArtifactInstance * art, bool assumeDestRemoved)
+void CArtifactsOfHeroBase::markPossibleSlots(const CArtifact * art, bool assumeDestRemoved)
 {
-	for(auto artPlace : artWorn)
+	for(const auto & artPlace : artWorn)
 		artPlace.second->selectSlot(art->canBePutAt(curHero, artPlace.second->slot, assumeDestRemoved));
 }
 
@@ -271,7 +271,7 @@ void CArtifactsOfHeroBase::setSlotData(ArtPlacePtr artPlace, const ArtifactPosit
 			arts.try_emplace(combinedArt->getId(), std::vector<ArtifactID>{});
 			for(const auto part : combinedArt->getConstituents())
 			{
-				if(curHero->hasArt(part->getId(), false, false, false))
+				if(curHero->hasArt(part->getId(), false, false))
 					arts.at(combinedArt->getId()).emplace_back(part->getId());
 			}
 		}

+ 1 - 1
client/widgets/CArtifactsOfHeroBase.h

@@ -39,7 +39,7 @@ public:
 	virtual void setHero(const CGHeroInstance * hero);
 	virtual const CGHeroInstance * getHero() const;
 	virtual void scrollBackpack(bool left);
-	virtual void markPossibleSlots(const CArtifactInstance * art, bool assumeDestRemoved = true);
+	virtual void markPossibleSlots(const CArtifact * art, bool assumeDestRemoved = true);
 	virtual void unmarkSlots();
 	virtual ArtPlacePtr getArtPlace(const ArtifactPosition & slot);
 	virtual ArtPlacePtr getArtPlace(const Point & cursorPosition);

+ 3 - 6
client/windows/CWindowWithArtifacts.cpp

@@ -62,15 +62,12 @@ const CGHeroInstance * CWindowWithArtifacts::getHeroPickedArtifact() const
 
 const CArtifactInstance * CWindowWithArtifacts::getPickedArtifact() const
 {
-	const CArtifactInstance * art = nullptr;
-
 	for(const auto & artSet : artSets)
 		if(const auto pickedArt = artSet->getHero()->getArt(ArtifactPosition::TRANSITION_POS))
 		{
-			art = pickedArt;
-			break;
+			return pickedArt;
 		}
-	return art;
+	return nullptr;
 }
 
 void CWindowWithArtifacts::clickPressedOnArtPlace(const CGHeroInstance * hero, const ArtifactPosition & slot,
@@ -207,7 +204,7 @@ void CWindowWithArtifacts::markPossibleSlots() const
 				continue;
 
 			if(getHeroPickedArtifact() == hero || !std::dynamic_pointer_cast<CArtifactsOfHeroKingdom>(artSet))
-				artSet->markPossibleSlots(pickedArtInst, hero->tempOwner == LOCPLINT->playerID);
+				artSet->markPossibleSlots(pickedArtInst->artType, hero->tempOwner == LOCPLINT->playerID);
 		}
 	}
 }

+ 2 - 1
client/windows/GUIClasses.cpp

@@ -548,11 +548,12 @@ void CTavernWindow::addInvite()
 
 	if(!inviteableHeroes.empty())
 	{
+		int imageIndex = heroToInvite ? (*CGI->heroh)[heroToInvite->getHeroType()]->imageIndex : 156; // 156 => special id for random
 		if(!heroToInvite)
 			heroToInvite = (*RandomGeneratorUtil::nextItem(inviteableHeroes, CRandomGenerator::getDefault())).second;
 
 		inviteHero = std::make_shared<CLabel>(170, 444, EFonts::FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("vcmi.tavernWindow.inviteHero"));
-		inviteHeroImage = std::make_shared<CAnimImage>(AnimationPath::builtin("PortraitsSmall"), (*CGI->heroh)[heroToInvite->getHeroType()]->imageIndex, 0, 245, 428);
+		inviteHeroImage = std::make_shared<CAnimImage>(AnimationPath::builtin("PortraitsSmall"), imageIndex, 0, 245, 428);
 		inviteHeroImageArea = std::make_shared<LRClickableArea>(Rect(245, 428, 48, 32), [this](){ GH.windows().createAndPushWindow<HeroSelector>(inviteableHeroes, [this](CGHeroInstance* h){ heroToInvite = h; addInvite(); }); }, [this](){ GH.windows().createAndPushWindow<CRClickPopupInt>(std::make_shared<CHeroWindow>(heroToInvite)); });
 	}
 }

+ 0 - 32
config/creatures/conflux.json

@@ -55,10 +55,6 @@
 		"upgrades": ["stormElemental"],
 		"graphics" :
 		{
-			"animationTime" :
-			{
-				"idle" : 0
-			},
 			"animation": "CAELEM.DEF"
 		},
 		"sound" :
@@ -122,10 +118,6 @@
 		"upgrades": ["magmaElemental"],
 		"graphics" :
 		{
-			"animationTime" :
-			{
-				"idle" : 0
-			},
 			"animation": "CEELEM.DEF"
 		},
 		"sound" :
@@ -185,10 +177,6 @@
 		"upgrades": ["energyElemental"],
 		"graphics" :
 		{
-			"animationTime" :
-			{
-				"idle" : 0
-			},
 			"animation": "CFELEM.DEF"
 		},
 		"sound" :
@@ -273,10 +261,6 @@
 		"upgrades": ["iceElemental"],
 		"graphics" :
 		{
-			"animationTime" :
-			{
-				"idle" : 0
-			},
 			"animation": "CWELEM.DEF"
 		},
 		"sound" :
@@ -472,10 +456,6 @@
 		"graphics" :
 		{
 			"animation": "CICEE.DEF",
-			"animationTime" :
-			{
-				"idle" : 0
-			},
 			"missile" :
 			{
 				"projectile": "PICEE.DEF"
@@ -554,10 +534,6 @@
 		},
 		"graphics" :
 		{
-			"animationTime" :
-			{
-				"idle" : 0
-			},
 			"animation": "CSTONE.DEF"
 		},
 		"sound" :
@@ -635,10 +611,6 @@
 		"graphics" :
 		{
 			"animation": "CSTORM.DEF",
-			"animationTime" :
-			{
-				"idle" : 0
-			},
 			"missile" :
 			{
 				"projectile": "CPRGTIX.DEF"
@@ -717,10 +689,6 @@
 		},
 		"graphics" :
 		{
-			"animationTime" :
-			{
-				"idle" : 0
-			},
 			"animation": "CNRG.DEF"
 		},
 		"sound" :

+ 4 - 4
config/fonts.json

@@ -29,14 +29,14 @@
 	// "noShadow" - if set, this font will not drop any shadow
 	"trueType":
 	{
-		"BIGFONT"  : { "file" : "NotoSerif-Bold.ttf",   "size" : [ 19, 39, 58, 78] },
+		"BIGFONT"  : { "file" : "NotoSerif-Bold.ttf",   "size" : [ 18, 38, 57, 76] },
 		"CALLI10R" : { "file" : "NotoSerif-Bold.ttf",   "size" : [ 12, 24, 36, 48] }, // TODO: find better matching font? This is likely non-free 'Callisto MT' font
 		"CREDITS"  : { "file" : "NotoSerif-Black.ttf",  "size" : [ 22, 44, 66, 88], "outline" : true },
 		"HISCORE"  : { "file" : "NotoSerif-Black.ttf",  "size" : [ 18, 36, 54, 72], "outline" : true },
-		"MEDFONT"  : { "file" : "NotoSerif-Bold.ttf",   "size" : [ 15, 31, 46, 62] },
-		"SMALFONT" : { "file" : "NotoSerif-Medium.ttf", "size" : [ 12, 24, 36, 48] },
+		"MEDFONT"  : { "file" : "NotoSerif-Bold.ttf",   "size" : [ 13, 26, 39, 52] },
+		"SMALFONT" : { "file" : "NotoSerif-Medium.ttf", "size" : [ 11, 22, 33, 44] },
 		"TIMES08R" : { "file" : "NotoSerif-Medium.ttf", "size" : [  8, 16, 24, 32] },
-		"TINY"     : { "file" : "NotoSans-Medium.ttf",  "size" : [  9, 19, 28, 38], "noShadow" : true }, // The only H3 font without shadow
+		"TINY"     : { "file" : "NotoSans-Medium.ttf",  "size" : [  9, 18, 28, 38], "noShadow" : true }, // The only H3 font without shadow
 		"VERD10B"  : { "file" : "NotoSans-Medium.ttf",  "size" : [ 13, 26, 39, 52] }
 	}
 }

+ 16 - 0
config/gameConfig.json

@@ -569,6 +569,22 @@
 					"valueType" : "BASE_NUMBER"
 				}
 			}
+		},
+
+		"interface" :
+		{
+			// Color transform to make color of brown DIBOX.PCX texture match color of specified player
+			"playerColoredBackground" :
+			{
+				"red" :    [  0.25,  0,     0,     1.25, 0.00, 0.00 ],
+				"blue" :   [  0,     0,     0,     0.45, 1.20, 4.50 ],
+				"tan" :    [  0.40,  0.27,  0.23,  1.10, 1.20, 1.15 ],
+				"green" :  [ -0.27,  0.10, -0.27,  0.70, 1.70, 0.70 ],
+				"orange" : [  0.47,  0.17, -0.27,  1.60, 1.20, 0.70 ],
+				"purple" : [  0.12, -0.1,   0.25,  1.15, 1.20, 2.20 ],
+				"teal" :   [ -0.13,  0.23,  0.23,  0.90, 1.20, 2.20 ],
+				"pink" :   [  0.44,  0.15,  0.25,  1.00, 1.00, 1.75 ]
+			}
 		}
 	}
 }

+ 7 - 0
config/schemas/gameSettings.json

@@ -152,5 +152,12 @@
 				"perHero" : { "type" : "object" }
 			}
 		},
+		"interface": {
+			"type" : "object",
+			"additionalProperties" : false,
+			"properties" : {
+				"playerColoredBackground" : { "type" : "object" }
+			}
+		}
 	}
 }

+ 40 - 40
config/schemas/mod.json

@@ -5,6 +5,17 @@
 	"description" : "Format used to define main mod file (mod.json) in VCMI",
 	"required" : [ "name", "description", "modType", "version", "author", "contact" ],
 	"definitions" : {
+		"fileListOrObject" : {
+			"oneOf" : [
+				{
+					"type" : "array",
+					"items" : { "type" : "string", "format" : "textFile" }
+				},
+				{
+					"type" : "object"
+				}
+			]
+		},
 		"localizable" : {
 			"type" : "object",
 			"additionalProperties" : false,
@@ -35,9 +46,8 @@
 					"description" : "If set to true, vcmi will skip validation of current translation json files"
 				},
 				"translations" : {
-					"type" : "array",
 					"description" : "List of files with translations for this language",
-					"items" : { "type" : "string", "format" : "textFile" }
+					"$ref" : "#/definitions/fileListOrObject"
 				}
 			}
 		}
@@ -122,9 +132,17 @@
 			"description" : "If set to true, mod will not be enabled automatically on install"
 		},
 		"settings" : {
-			"type" : "object",
 			"description" : "List of changed game settings by mod",
-			"$ref" : "gameSettings.json"
+			"oneOf" : [
+				{
+					"type" : "object",
+					"$ref" : "gameSettings.json"
+				},
+				{
+					"type" : "array",
+					"items" : { "type" : "string", "format" : "textFile" }
+				},
+			]
 		},
 		"filesystem" : {
 			"type" : "object",
@@ -206,94 +224,76 @@
 			"$ref" : "#/definitions/localizable"
 		},
 		"translations" : {
-			"type" : "array",
 			"description" : "List of files with translations for this language",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"factions" : {
-			"type" : "array",
 			"description" : "List of configuration files for towns/factions",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"heroClasses" : {
-			"type" : "array",
 			"description" : "List of configuration files for hero classes",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"heroes" : {
-			"type" : "array",
 			"description" : "List of configuration files for heroes",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"skills" : {
-			"type" : "array",
 			"description" : "List of configuration files for skills",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"creatures" : {
-			"type" : "array",
 			"description" : "List of configuration files for creatures",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"artifacts" : {
-			"type" : "array",
 			"description" : "List of configuration files for artifacts",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"spells" : {
-			"type" : "array",
 			"description" : "List of configuration files for spells",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"objects" : {
-			"type" : "array",
 			"description" : "List of configuration files for objects",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"biomes" : {
-			"type" : "array",
 			"description" : "List of configuration files for biomes",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"bonuses" : {
-			"type" : "array",
 			"description" : "List of configuration files for bonuses",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"terrains" : {
-			"type" : "array",
 			"description" : "List of configuration files for terrains",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"roads" : {
-			"type" : "array",
 			"description" : "List of configuration files for roads",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"rivers" : {
-			"type" : "array",
 			"description" : "List of configuration files for rivers",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"battlefields" : {
-			"type" : "array",
 			"description" : "List of configuration files for battlefields",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"obstacles" : {
-			"type" : "array",
 			"description" : "List of configuration files for obstacles",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"templates" : {
-			"type" : "array",
 			"description" : "List of configuration files for RMG templates",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		},
 		"scripts" : {
-			"type" : "array",
 			"description" : "List of configuration files for scripts",
-			"items" : { "type" : "string", "format" : "textFile" }
+			"$ref" : "#/definitions/fileListOrObject"
 		}
 	}
 }

+ 1 - 1
config/schemas/settings.json

@@ -509,7 +509,7 @@
 				},
 				"neutralAI" : {
 					"type" : "string",
-					"default" : "StupidAI"
+					"default" : "BattleAI"
 				},
 				"enemyAI" : {
 					"type" : "string",

+ 4 - 0
config/schemas/spell.json

@@ -171,6 +171,10 @@
 			"type" : "boolean",
 			"description" : "If used as creature spell, unit can cast this spell on itself"
 		},
+		"canCastOnlyOnSelf" : {
+			"type" : "boolean",
+			"description" : "If used as creature spell, unit can cast this spell only on itself"
+		},
 		"canCastWithoutSkip" : {
 			"type" : "boolean",
 			"description" : "If used the creature will not skip the turn after casting a spell."

+ 46 - 0
config/schemas/template.json

@@ -22,6 +22,7 @@
 				"minesLikeZone" : { "type" : "number" },
 				"terrainTypeLikeZone" : { "type" : "number" },
 				"treasureLikeZone" : { "type" : "number" },
+				"customObjectsLikeZone" : { "type" : "number" },
 				
 				"terrainTypes": {"$ref" : "#/definitions/stringArray"},
 				"bannedTerrains": {"$ref" : "#/definitions/stringArray"},
@@ -49,6 +50,51 @@
 						},
 						"additionalProperties" : false
 					}
+				},
+				"customObjects" : {
+					"type" : "object",
+					"properties": {
+						"bannedCategories": {
+							"type": "array",
+							"items": {
+								"type": "string",
+								"enum": ["all", "dwelling", "creatureBank", "randomArtifact", "bonus", "resource", "resourceGenerator", "spellScroll", "pandorasBox", "questArtifact", "seerHut"]
+							}
+						},
+						"bannedObjects": {
+							"type": "array",
+							"items": {
+								"type": "string"
+							}
+						},
+						"commonObjects": {
+							"type": "array",
+							"items": {
+								"type": "object",
+								"properties": {
+									"id": {
+										"type": "string"
+									},
+									"rmg": {
+										"type": "object",
+										"properties": {
+											"value": {
+												"type": "integer"
+											},
+											"rarity": {
+												"type": "integer"
+											},
+											"zoneLimit": {
+												"type": "integer"
+											}
+										},
+										"required": ["value", "rarity"]
+									}
+								},
+								"required": ["id", "rmg"]
+							}
+						}
+					}
 				}
 			}
 		},

+ 3 - 0
docs/modders/Entities_Format/Spell_Format.md

@@ -64,6 +64,9 @@
 		// If true, then creature capable of casting this spell can cast this spell on itself
 		// If false, then creature can only cast this spell on other units
 		"canCastOnSelf" : false,
+		
+		// If true, then creature capable of casting this spell can cast this spell only on itself
+		"canCastOnlyOnSelf" : false,
 
 		// If true the creature will not skip the turn after casting a spell
 		"canCastWithoutSkip": false,

+ 3 - 2
docs/modders/Mod_File_Format.md

@@ -90,8 +90,9 @@ These are fields that are present only in local mod.json file
 {
 	// Following section describes configuration files with content added by mod
 	// It can be split into several files in any way you want but recommended organization is
-	// to keep one file per object (creature/hero/etc) and, if applicable, add separate file
-	// with translatable strings for each type of content
+	// to keep one file per object (creature/hero/etc)
+	// Alternatively, for small changes you can embed changes to content directly in here, e.g.
+	// "creatures" : { "core:imp" : { "health" : 5 }}
 
 	// list of factions/towns configuration files
 	"factions" :

+ 28 - 3
docs/modders/Random_Map_Template.md

@@ -99,10 +99,13 @@
 	"minesLikeZone" : 1,
 	
 	// Treasures will have same configuration as in linked zone
-	"treasureLikeZone" : 1
+	"treasureLikeZone" : 1,
 	
 	// Terrain type will have same configuration as in linked zone
-	"terrainTypeLikeZone" : 3
+	"terrainTypeLikeZone" : 3,
+
+	// Custom objects will have same configuration as in linked zone
+	"customObjectsLikeZone" : 1,
 
 	// factions of monsters allowed on this zone
 	"allowedMonsters" : ["inferno", "necropolis"] 
@@ -130,6 +133,28 @@
 			"density" : 5
 		}
 		  ...
-	]
+	],
+
+	// Objects with different configuration than default / set by mods
+	"customObjects" :
+	{
+		// All of objects of this kind will be removed from zone
+		// Possible values: "all", "none", "creatureBank", "bonus", "dwelling", "resource", "resourceGenerator", "spellScroll", "randomArtifact", "pandorasBox", "questArtifact", "seerHut", "other
+		"bannedCategories" : ["all", "dwelling", "creatureBank", "other"],
+		// Specify object types and subtypes
+		"bannedObjects" :["core:object.randomArtifactRelic"],
+		// Configure individual common objects - overrides banned objects
+		"commonObjects":
+		[
+			{
+				"id" : "core:object.creatureBank.dragonFlyHive",
+				"rmg" : {
+					"value"		: 9000,
+					"rarity"	: 500,
+					"zoneLimit" : 2
+				}
+			}
+		]
+	}
 }
 ```

+ 1 - 0
include/vcmi/spells/Spell.h

@@ -45,6 +45,7 @@ public:
 
 	virtual bool hasSchool(SpellSchool school) const = 0;
 	virtual bool canCastOnSelf() const = 0;
+	virtual bool canCastOnlyOnSelf() const = 0;
 	virtual bool canCastWithoutSkip() const = 0;
 	virtual void forEachSchool(const SchoolCallback & cb) const = 0;
 	virtual int32_t getCost(const int32_t skillLevel) const = 0;

+ 53 - 53
launcher/translation/portuguese.ts

@@ -442,17 +442,17 @@
     <message>
         <location filename="../modManager/cmodlistview_moc.cpp" line="648"/>
         <source>Gog files</source>
-        <translation type="unfinished"></translation>
+        <translation>Arquivos GOG</translation>
     </message>
     <message>
         <location filename="../modManager/cmodlistview_moc.cpp" line="650"/>
         <source>All files (*.*)</source>
-        <translation type="unfinished"></translation>
+        <translation>Todos os arquivos (*.*)</translation>
     </message>
     <message>
         <location filename="../modManager/cmodlistview_moc.cpp" line="652"/>
         <source>Select files (configs, mods, maps, campaigns, gog files) to install...</source>
-        <translation type="unfinished"></translation>
+        <translation>Selecione arquivos (configurações, mods, mapas, campanhas, arquivos gog) para instalar...</translation>
     </message>
     <message>
         <location filename="../modManager/cmodlistview_moc.cpp" line="677"/>
@@ -483,7 +483,7 @@ Encountered errors:
 </source>
         <translation>Não foi possível baixar todos os arquivos.
 
-Encontrados os seguintes erros:
+Erros encontrados:
 
 </translation>
     </message>
@@ -494,12 +494,12 @@ Encontrados os seguintes erros:
 Install successfully downloaded?</source>
         <translation>
 
-Instalar o download realizado com sucesso?</translation>
+O download da instalação foi bem-sucedido?</translation>
     </message>
     <message>
         <location filename="../modManager/cmodlistview_moc.cpp" line="852"/>
         <source>Installing chronicles</source>
-        <translation type="unfinished"></translation>
+        <translation>Instalando crônicas</translation>
     </message>
     <message>
         <location filename="../modManager/cmodlistview_moc.cpp" line="925"/>
@@ -641,12 +641,12 @@ Instalar o download realizado com sucesso?</translation>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="307"/>
         <source>Artificial Intelligence</source>
-        <translation>Inteligência Artificial</translation>
+        <translation>Inteligência artificial</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="1072"/>
         <source>Interface Scaling</source>
-        <translation>Escala da Interface</translation>
+        <translation>Escala da interface</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="924"/>
@@ -666,12 +666,12 @@ Instalar o download realizado com sucesso?</translation>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="931"/>
         <source>Adventure Map Allies</source>
-        <translation>Aliados do Mapa de Aventura</translation>
+        <translation>Aliados do mapa de aventura</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="490"/>
         <source>Online Lobby port</source>
-        <translation>Porta da Sala de Espera On-line</translation>
+        <translation>Porta da sala de espera on-line</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="331"/>
@@ -681,22 +681,22 @@ Instalar o download realizado com sucesso?</translation>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="352"/>
         <source>Sticks Sensitivity</source>
-        <translation>Sensibilidade dos Analógicos</translation>
+        <translation>Sensibilidade dos analógicos</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="618"/>
         <source>Automatic (Linear)</source>
-        <translation>Automático (Linear)</translation>
+        <translation>Automático (linear)</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="798"/>
         <source>Haptic Feedback</source>
-        <translation>Resposta Tátil</translation>
+        <translation>Resposta tátil</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="835"/>
         <source>Software Cursor</source>
-        <translation>Cursor por Software</translation>
+        <translation>Cursor por software</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="1166"/>
@@ -723,25 +723,30 @@ Instalar o download realizado com sucesso?</translation>
         <source>xBRZ x4</source>
         <translation>xBRZ x4</translation>
     </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="1194"/>
+        <source>Use scalable fonts</source>
+    <translation>Usar fontes escaláveis</translation>
+    </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="138"/>
         <source>Online Lobby address</source>
-        <translation>Endereço da Sala de Espera On-line</translation>
+        <translation>Endereço da sala de espera on-line</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="1158"/>
         <source>Upscaling Filter</source>
-        <translation>Filtro de Aumento de Escala</translation>
+        <translation>Filtro de aumento de escala</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="317"/>
         <source>Use Relative Pointer Mode</source>
-        <translation>Usar Modo de Ponteiro Relativo</translation>
+        <translation>Usar modo de ponteiro relativo</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="608"/>
         <source>Nearest</source>
-        <translation>Mais Próximo</translation>
+        <translation>Mais próximo</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="613"/>
@@ -751,28 +756,23 @@ Instalar o download realizado com sucesso?</translation>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="750"/>
         <source>Input - Touchscreen</source>
-        <translation>Entrada - Tela de Toque</translation>
+        <translation>Entrada - tela de toque</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="900"/>
         <source>Adventure Map Enemies</source>
-        <translation>Inimigos do Mapa de Aventura</translation>
+        <translation>Inimigos do mapa de aventura</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="1144"/>
         <source>Show Tutorial again</source>
-        <translation>Mostrar o Tutorial novamente</translation>
+        <translation>Mostrar o tutorial novamente</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="1151"/>
         <source>Reset</source>
         <translation>Redefinir</translation>
     </message>
-    <message>
-        <location filename="../settingsView/csettingsview_moc.ui" line="1194"/>
-        <source>Use scalable fonts</source>
-        <translation>Usar Fontes Escaláveis</translation>
-    </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="854"/>
         <source>Network</source>
@@ -786,12 +786,12 @@ Instalar o download realizado com sucesso?</translation>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="842"/>
         <source>Relative Pointer Speed</source>
-        <translation>Velocidade do Ponteiro Relativo</translation>
+        <translation>Velocidade do ponteiro relativo</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="1137"/>
         <source>Music Volume</source>
-        <translation>Volume da Música</translation>
+        <translation>Volume da música</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="767"/>
@@ -801,12 +801,12 @@ Instalar o download realizado com sucesso?</translation>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="943"/>
         <source>Input - Mouse</source>
-        <translation>Entrada - Mouse</translation>
+        <translation>Entrada - mouse</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="345"/>
         <source>Long Touch Duration</source>
-        <translation>Duração do Toque Longo</translation>
+        <translation>Duração do toque longo</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="115"/>
@@ -816,22 +816,22 @@ Instalar o download realizado com sucesso?</translation>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="1037"/>
         <source>Controller Click Tolerance</source>
-        <translation>Tolerância de Clique do Controle</translation>
+        <translation>Tolerância de clique do controle</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="359"/>
         <source>Touch Tap Tolerance</source>
-        <translation>Tolerância de Toque Tátil</translation>
+        <translation>Tolerância de toque tátil</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="1020"/>
         <source>Input - Controller</source>
-        <translation>Entrada - Controle</translation>
+        <translation>Entrada - controle</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="1086"/>
         <source>Sound Volume</source>
-        <translation>Volume do Som</translation>
+        <translation>Volume do som</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="402"/>
@@ -856,12 +856,12 @@ Instalar o download realizado com sucesso?</translation>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="893"/>
         <source>Downscaling Filter</source>
-        <translation>Filtro de Redução de Escala</translation>
+        <translation>Filtro de redução de escala</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="1030"/>
         <source>Framerate Limit</source>
-        <translation>Limite de Taxa de Quadros</translation>
+        <translation>Limite de taxa de quadros</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="760"/>
@@ -871,12 +871,12 @@ Instalar o download realizado com sucesso?</translation>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="828"/>
         <source>Mouse Click Tolerance</source>
-        <translation>Tolerância de Clique do Mouse</translation>
+        <translation>Tolerância de clique do mouse</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="94"/>
         <source>Sticks Acceleration</source>
-        <translation>Aceleração dos Analógicos</translation>
+        <translation>Aceleração dos analógicos</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="1008"/>
@@ -1003,7 +1003,7 @@ Modo de tela cheia exclusivo - o jogo cobrirá toda a sua tela e usará a resolu
     <message>
         <location filename="../settingsView/csettingsview_moc.cpp" line="536"/>
         <source>Not Installed</source>
-        <translation>Não Instalado</translation>
+        <translation>Não instalado</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.cpp" line="537"/>
@@ -1016,35 +1016,35 @@ Modo de tela cheia exclusivo - o jogo cobrirá toda a sua tela e usará a resolu
     <message>
         <location filename="../modManager/chroniclesextractor.cpp" line="48"/>
         <source>File cannot opened</source>
-        <translation type="unfinished"></translation>
+        <translation>Não foi possível abrir o arquivo</translation>
     </message>
     <message>
         <location filename="../modManager/chroniclesextractor.cpp" line="56"/>
         <location filename="../modManager/chroniclesextractor.cpp" line="71"/>
         <source>Invalid file selected</source>
-        <translation type="unfinished">Arquivo selecionado inválido</translation>
+        <translation>Arquivo selecionado inválido</translation>
     </message>
     <message>
         <location filename="../modManager/chroniclesextractor.cpp" line="56"/>
         <source>You have to select an gog installer file!</source>
-        <translation type="unfinished"></translation>
+        <translation>Você precisa selecionar um arquivo de instalação do GOG!</translation>
     </message>
     <message>
         <location filename="../modManager/chroniclesextractor.cpp" line="71"/>
         <source>You have to select an chronicle installer file!</source>
-        <translation type="unfinished"></translation>
+        <translation>Você precisa selecionar um arquivo de instalação do Chronicles!</translation>
     </message>
     <message>
         <location filename="../modManager/chroniclesextractor.cpp" line="87"/>
         <source>Extracting error!</source>
-        <translation type="unfinished">Erro ao extrair!</translation>
+        <translation>Erro ao extrair!</translation>
     </message>
     <message>
         <location filename="../modManager/chroniclesextractor.cpp" line="104"/>
         <location filename="../modManager/chroniclesextractor.cpp" line="105"/>
         <location filename="../modManager/chroniclesextractor.cpp" line="141"/>
         <source>Heroes Chronicles</source>
-        <translation type="unfinished"></translation>
+        <translation>Heroes Chronicles</translation>
     </message>
 </context>
 <context>
@@ -1090,7 +1090,7 @@ Modo de tela cheia exclusivo - o jogo cobrirá toda a sua tela e usará a resolu
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="78"/>
         <source>Mods Preset</source>
-        <translation>Predefinição de Mod</translation>
+        <translation>Predefinição de mod</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="126"/>
@@ -1220,7 +1220,7 @@ O instalador offline consiste em duas partes, .exe e .bin. Certifique-se de baix
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="347"/>
         <source>Manual Installation</source>
-        <translation>Instalação Manual</translation>
+        <translation>Instalação manual</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="360"/>
@@ -1246,7 +1246,7 @@ O instalador offline consiste em duas partes, .exe e .bin. Certifique-se de baix
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="594"/>
         <source>Install VCMI Mod Preset</source>
-        <translation>Instalar Predefinição de Mod do VCMI</translation>
+        <translation>Instalar predefinição de mod do VCMI</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="710"/>
@@ -1370,7 +1370,7 @@ Por favor, selecione o diretório com Heroes III: Complete Edition ou Heroes III
     <message>
         <location filename="../modManager/imageviewer_moc.ui" line="20"/>
         <source>Image Viewer</source>
-        <translation>Visualizador de Imagens</translation>
+        <translation>Visualizador de imagens</translation>
     </message>
 </context>
 <context>
@@ -1379,18 +1379,18 @@ Por favor, selecione o diretório com Heroes III: Complete Edition ou Heroes III
         <location filename="../innoextract.cpp" line="42"/>
         <source>Stream error while extracting files!
 error reason: </source>
-        <translation type="unfinished">Erro de fluxo ao extrair arquivos!
+        <translation>Erro de fluxo ao extrair arquivos!
 Motivo do erro: </translation>
     </message>
     <message>
         <location filename="../innoextract.cpp" line="55"/>
         <source>Not a supported Inno Setup installer!</source>
-        <translation type="unfinished">Instalador Inno Setup não suportado!</translation>
+        <translation>Instalador Inno Setup não suportado!</translation>
     </message>
     <message>
         <location filename="../innoextract.cpp" line="58"/>
         <source>VCMI was compiled without innoextract support, which is needed to extract exe files!</source>
-        <translation type="unfinished"></translation>
+        <translation>O VCMI foi compilado sem suporte ao innoextract, que é necessário para extrair arquivos EXE!</translation>
     </message>
 </context>
 <context>
@@ -1506,7 +1506,7 @@ Motivo do erro: </translation>
     <message>
         <location filename="../mainwindow_moc.ui" line="210"/>
         <source>Map Editor</source>
-        <translation>Editor de Mapas</translation>
+        <translation>Editor de mapas</translation>
     </message>
     <message>
         <location filename="../mainwindow_moc.ui" line="259"/>

+ 1 - 1
lib/ArtifactUtils.cpp

@@ -209,7 +209,7 @@ DLL_LINKAGE std::vector<const CArtifact*> ArtifactUtils::assemblyPossibilities(
 
 		for(const auto constituent : artifact->getConstituents()) //check if all constituents are available
 		{
-			if(!artSet->hasArt(constituent->getId(), onlyEquiped, false, false))
+			if(!artSet->hasArt(constituent->getId(), onlyEquiped, false))
 			{
 				possible = false;
 				break;

+ 62 - 106
lib/CArtHandler.cpp

@@ -220,7 +220,7 @@ bool CArtifact::canBePutAt(const CArtifactSet * artSet, ArtifactPosition slot, b
 				auto possibleSlot = ArtifactUtils::getArtAnyPosition(&fittingSet, art->getId());
 				if(ArtifactUtils::isSlotEquipment(possibleSlot))
 				{
-					fittingSet.setNewArtSlot(possibleSlot, nullptr, true);
+					fittingSet.lockSlot(possibleSlot);
 				}
 				else
 				{
@@ -431,9 +431,9 @@ std::shared_ptr<CArtifact> CArtHandler::loadFromJson(const std::string & scope,
 
 	const JsonNode & text = node["text"];
 
-	VLC->generaltexth->registerString(scope, art->getNameTextID(), text["name"].String());
-	VLC->generaltexth->registerString(scope, art->getDescriptionTextID(), text["description"].String());
-	VLC->generaltexth->registerString(scope, art->getEventTextID(), text["event"].String());
+	VLC->generaltexth->registerString(scope, art->getNameTextID(), text["name"]);
+	VLC->generaltexth->registerString(scope, art->getDescriptionTextID(), text["description"]);
+	VLC->generaltexth->registerString(scope, art->getEventTextID(), text["event"]);
 
 	const JsonNode & graphics = node["graphics"];
 	art->image = graphics["image"].String();
@@ -691,9 +691,7 @@ void CArtHandler::afterLoadFinalization()
 	CBonusSystemNode::treeHasChanged();
 }
 
-CArtifactSet::~CArtifactSet() = default;
-
-const CArtifactInstance * CArtifactSet::getArt(const ArtifactPosition & pos, bool excludeLocked) const
+CArtifactInstance * CArtifactSet::getArt(const ArtifactPosition & pos, bool excludeLocked) const
 {
 	if(const ArtSlotInfo * si = getSlot(pos))
 	{
@@ -704,56 +702,34 @@ const CArtifactInstance * CArtifactSet::getArt(const ArtifactPosition & pos, boo
 	return nullptr;
 }
 
-CArtifactInstance * CArtifactSet::getArt(const ArtifactPosition & pos, bool excludeLocked)
-{
-	return const_cast<CArtifactInstance*>((const_cast<const CArtifactSet*>(this))->getArt(pos, excludeLocked));
-}
-
 ArtifactPosition CArtifactSet::getArtPos(const ArtifactID & aid, bool onlyWorn, bool allowLocked) const
 {
-	const auto result = getAllArtPositions(aid, onlyWorn, allowLocked, false);
-	return result.empty() ? ArtifactPosition{ArtifactPosition::PRE_FIRST} : result[0];
-}
-
-std::vector<ArtifactPosition> CArtifactSet::getAllArtPositions(const ArtifactID & aid, bool onlyWorn, bool allowLocked, bool getAll) const
-{
-	std::vector<ArtifactPosition> result;
-	for(const auto & slotInfo : artifactsWorn)
-		if(slotInfo.second.artifact->getTypeId() == aid && (allowLocked || !slotInfo.second.locked))
-			result.push_back(slotInfo.first);
-
-	if(onlyWorn)
-		return result;
-	if(!getAll && !result.empty())
-		return result;
-
-	auto backpackPositions = getBackpackArtPositions(aid);
-	result.insert(result.end(), backpackPositions.begin(), backpackPositions.end());
-	return result;
-}
-
-std::vector<ArtifactPosition> CArtifactSet::getBackpackArtPositions(const ArtifactID & aid) const
-{
-	std::vector<ArtifactPosition> result;
-
-	si32 backpackPosition = ArtifactPosition::BACKPACK_START;
-	for(const auto & artInfo : artifactsInBackpack)
+	for(const auto & [slot, slotInfo] : artifactsWorn)
 	{
-		const auto * art = artInfo.getArt();
-		if(art && art->artType->getId() == aid)
-			result.emplace_back(backpackPosition);
-		backpackPosition++;
+		if(slotInfo.artifact->getTypeId() == aid && (allowLocked || !slotInfo.locked))
+			return slot;
+	}
+	if(!onlyWorn)
+	{
+		size_t backpackPositionIdx = ArtifactPosition::BACKPACK_START;
+		for(const auto & artInfo : artifactsInBackpack)
+		{
+			const auto art = artInfo.getArt();
+			if(art && art->artType->getId() == aid)
+				return ArtifactPosition(backpackPositionIdx);
+			backpackPositionIdx++;
+		}
 	}
-	return result;
+	return ArtifactPosition::PRE_FIRST;
 }
 
 const CArtifactInstance * CArtifactSet::getArtByInstanceId(const ArtifactInstanceID & artInstId) const
 {
-	for(auto i : artifactsWorn)
+	for(const auto & i : artifactsWorn)
 		if(i.second.artifact->getId() == artInstId)
 			return i.second.artifact;
 
-	for(auto i : artifactsInBackpack)
+	for(const auto & i : artifactsInBackpack)
 		if(i.artifact->getId() == artInstId)
 			return i.artifact;
 
@@ -779,29 +755,16 @@ ArtifactPosition CArtifactSet::getArtPos(const CArtifactInstance * artInst) cons
 	return ArtifactPosition::PRE_FIRST;
 }
 
-bool CArtifactSet::hasArt(const ArtifactID & aid, bool onlyWorn, bool searchBackpackAssemblies, bool allowLocked) const
-{
-	return getArtPosCount(aid, onlyWorn, searchBackpackAssemblies, allowLocked) > 0;
-}
-
-bool CArtifactSet::hasArtBackpack(const ArtifactID & aid) const
+bool CArtifactSet::hasArt(const ArtifactID & aid, bool onlyWorn, bool searchCombinedParts) const
 {
-	return !getBackpackArtPositions(aid).empty();
-}
-
-unsigned CArtifactSet::getArtPosCount(const ArtifactID & aid, bool onlyWorn, bool searchBackpackAssemblies, bool allowLocked) const
-{
-	const auto allPositions = getAllArtPositions(aid, onlyWorn, allowLocked, true);
-	if(!allPositions.empty())
-		return allPositions.size();
-
-	if(searchBackpackAssemblies && getHiddenArt(aid))
-		return 1;
-
-	return 0;
+	if(searchCombinedParts && getCombinedArtWithPart(aid))
+		return true;
+	if(getArtPos(aid, onlyWorn, searchCombinedParts) != ArtifactPosition::PRE_FIRST)
+		return true;
+	return false;
 }
 
-CArtifactSet::ArtPlacementMap CArtifactSet::putArtifact(ArtifactPosition slot, CArtifactInstance * art)
+CArtifactSet::ArtPlacementMap CArtifactSet::putArtifact(const ArtifactPosition & slot, CArtifactInstance * art)
 {
 	ArtPlacementMap resArtPlacement;
 
@@ -827,19 +790,38 @@ CArtifactSet::ArtPlacementMap CArtifactSet::putArtifact(ArtifactPosition slot, C
 
 				assert(ArtifactUtils::isSlotEquipment(partSlot));
 				setNewArtSlot(partSlot, part.art, true);
-				resArtPlacement.emplace(std::make_pair(part.art, partSlot));
+				resArtPlacement.emplace(part.art, partSlot);
 			}
 			else
 			{
-				resArtPlacement.emplace(std::make_pair(part.art, part.slot));
+				resArtPlacement.emplace(part.art, part.slot);
 			}
 		}
 	}
 	return resArtPlacement;
 }
 
-void CArtifactSet::removeArtifact(ArtifactPosition slot)
+void CArtifactSet::removeArtifact(const ArtifactPosition & slot)
 {
+	const auto eraseArtSlot = [this](const ArtifactPosition & slotForErase)
+	{
+		if(slotForErase == ArtifactPosition::TRANSITION_POS)
+		{
+			artifactsTransitionPos.artifact = nullptr;
+		}
+		else if(ArtifactUtils::isSlotBackpack(slotForErase))
+		{
+			auto backpackSlot = ArtifactPosition(slotForErase - ArtifactPosition::BACKPACK_START);
+
+			assert(artifactsInBackpack.begin() + backpackSlot < artifactsInBackpack.end());
+			artifactsInBackpack.erase(artifactsInBackpack.begin() + backpackSlot);
+		}
+		else
+		{
+			artifactsWorn.erase(slotForErase);
+		}
+	};
+
 	if(const auto art = getArt(slot, false))
 	{
 		if(art->isCombined())
@@ -858,7 +840,7 @@ void CArtifactSet::removeArtifact(ArtifactPosition slot)
 	}
 }
 
-std::pair<const CArtifactInstance *, const CArtifactInstance *> CArtifactSet::searchForConstituent(const ArtifactID & aid) const
+const CArtifactInstance * CArtifactSet::getCombinedArtWithPart(const ArtifactID & partId) const
 {
 	for(const auto & slot : artifactsInBackpack)
 	{
@@ -867,24 +849,12 @@ std::pair<const CArtifactInstance *, const CArtifactInstance *> CArtifactSet::se
 		{
 			for(auto & ci : art->getPartsInfo())
 			{
-				if(ci.art->getTypeId() == aid)
-				{
-					return {art, ci.art};
-				}
+				if(ci.art->getTypeId() == partId)
+					return art;
 			}
 		}
 	}
-	return {nullptr, nullptr};
-}
-
-const CArtifactInstance * CArtifactSet::getHiddenArt(const ArtifactID & aid) const
-{
-	return searchForConstituent(aid).second;
-}
-
-const CArtifactInstance * CArtifactSet::getAssemblyByConstituent(const ArtifactID & aid) const
-{
-	return searchForConstituent(aid).first;
+	return nullptr;
 }
 
 const ArtSlotInfo * CArtifactSet::getSlot(const ArtifactPosition & pos) const
@@ -905,6 +875,11 @@ const ArtSlotInfo * CArtifactSet::getSlot(const ArtifactPosition & pos) const
 	return nullptr;
 }
 
+void CArtifactSet::lockSlot(const ArtifactPosition & pos)
+{
+	setNewArtSlot(pos, nullptr, true);
+}
+
 bool CArtifactSet::isPositionFree(const ArtifactPosition & pos, bool onlyLockCheck) const
 {
 	if(bearerType() == ArtBearer::ALTAR)
@@ -916,7 +891,7 @@ bool CArtifactSet::isPositionFree(const ArtifactPosition & pos, bool onlyLockChe
 	return true; //no slot means not used
 }
 
-void CArtifactSet::setNewArtSlot(const ArtifactPosition & slot, ConstTransitivePtr<CArtifactInstance> art, bool locked)
+void CArtifactSet::setNewArtSlot(const ArtifactPosition & slot, CArtifactInstance * art, bool locked)
 {
 	assert(!vstd::contains(artifactsWorn, slot));
 
@@ -932,31 +907,12 @@ void CArtifactSet::setNewArtSlot(const ArtifactPosition & slot, ConstTransitiveP
 	else
 	{
 		auto position = artifactsInBackpack.begin() + slot - ArtifactPosition::BACKPACK_START;
-		slotInfo = &(*artifactsInBackpack.emplace(position, ArtSlotInfo()));
+		slotInfo = &(*artifactsInBackpack.emplace(position));
 	}
 	slotInfo->artifact = art;
 	slotInfo->locked = locked;
 }
 
-void CArtifactSet::eraseArtSlot(const ArtifactPosition & slot)
-{
-	if(slot == ArtifactPosition::TRANSITION_POS)
-	{
-		artifactsTransitionPos.artifact = nullptr;
-	}
-	else if(ArtifactUtils::isSlotBackpack(slot))
-	{
-		auto backpackSlot = ArtifactPosition(slot - ArtifactPosition::BACKPACK_START);
-
-		assert(artifactsInBackpack.begin() + backpackSlot < artifactsInBackpack.end());
-		artifactsInBackpack.erase(artifactsInBackpack.begin() + backpackSlot);
-	}
-	else
-	{
-		artifactsWorn.erase(slot);
-	}
-}
-
 void CArtifactSet::artDeserializationFix(CBonusSystemNode *node)
 {
 	for(auto & elem : artifactsWorn)

+ 13 - 24
lib/CArtHandler.h

@@ -175,10 +175,10 @@ private:
 
 struct DLL_LINKAGE ArtSlotInfo
 {
-	ConstTransitivePtr<CArtifactInstance> artifact;
-	ui8 locked; //if locked, then artifact points to the combined artifact
+	CArtifactInstance * artifact;
+	bool locked; //if locked, then artifact points to the combined artifact
 
-	ArtSlotInfo() : locked(false) {}
+	ArtSlotInfo() : artifact(nullptr), locked(false) {}
 	const CArtifactInstance * getArt() const;
 
 	template <typename Handler> void serialize(Handler & h)
@@ -197,32 +197,20 @@ public:
 	std::map<ArtifactPosition, ArtSlotInfo> artifactsWorn; //map<position,artifact_id>; positions: 0 - head; 1 - shoulders; 2 - neck; 3 - right hand; 4 - left hand; 5 - torso; 6 - right ring; 7 - left ring; 8 - feet; 9 - misc1; 10 - misc2; 11 - misc3; 12 - misc4; 13 - mach1; 14 - mach2; 15 - mach3; 16 - mach4; 17 - spellbook; 18 - misc5
 	ArtSlotInfo artifactsTransitionPos; // Used as transition position for dragAndDrop artifact exchange
 
-	void setNewArtSlot(const ArtifactPosition & slot, ConstTransitivePtr<CArtifactInstance> art, bool locked);
-	void eraseArtSlot(const ArtifactPosition & slot);
-
 	const ArtSlotInfo * getSlot(const ArtifactPosition & pos) const;
-	const CArtifactInstance * getArt(const ArtifactPosition & pos, bool excludeLocked = true) const; //nullptr - no artifact
-	CArtifactInstance * getArt(const ArtifactPosition & pos, bool excludeLocked = true); //nullptr - no artifact
-	/// Looks for equipped artifact with given ID and returns its slot ID or -1 if none
-	/// (if more than one such artifact lower ID is returned)
+	void lockSlot(const ArtifactPosition & pos);
+	CArtifactInstance * getArt(const ArtifactPosition & pos, bool excludeLocked = true) const;
+	/// Looks for first artifact with given ID
 	ArtifactPosition getArtPos(const ArtifactID & aid, bool onlyWorn = true, bool allowLocked = true) const;
 	ArtifactPosition getArtPos(const CArtifactInstance * art) const;
-	std::vector<ArtifactPosition> getAllArtPositions(const ArtifactID & aid, bool onlyWorn, bool allowLocked, bool getAll) const;
-	std::vector<ArtifactPosition> getBackpackArtPositions(const ArtifactID & aid) const;
 	const CArtifactInstance * getArtByInstanceId(const ArtifactInstanceID & artInstId) const;
-	/// Search for constituents of assemblies in backpack which do not have an ArtifactPosition
-	const CArtifactInstance * getHiddenArt(const ArtifactID & aid) const;
-	const CArtifactInstance * getAssemblyByConstituent(const ArtifactID & aid) const;
-	/// Checks if hero possess artifact of given id (either in backack or worn)
-	bool hasArt(const ArtifactID & aid, bool onlyWorn = false, bool searchBackpackAssemblies = false, bool allowLocked = true) const;
-	bool hasArtBackpack(const ArtifactID & aid) const;
+	bool hasArt(const ArtifactID & aid, bool onlyWorn = false, bool searchCombinedParts = false) const;
 	bool isPositionFree(const ArtifactPosition & pos, bool onlyLockCheck = false) const;
-	unsigned getArtPosCount(const ArtifactID & aid, bool onlyWorn = true, bool searchBackpackAssemblies = true, bool allowLocked = true) const;
 
 	virtual ArtBearer::ArtBearer bearerType() const = 0;
-	virtual ArtPlacementMap putArtifact(ArtifactPosition slot, CArtifactInstance * art);
-	virtual void removeArtifact(ArtifactPosition slot);
-	virtual ~CArtifactSet();
+	virtual ArtPlacementMap putArtifact(const ArtifactPosition & slot, CArtifactInstance * art);
+	virtual void removeArtifact(const ArtifactPosition & slot);
+	virtual ~CArtifactSet() = default;
 
 	template <typename Handler> void serialize(Handler &h)
 	{
@@ -233,10 +221,11 @@ public:
 	void artDeserializationFix(CBonusSystemNode *node);
 
 	void serializeJsonArtifacts(JsonSerializeFormat & handler, const std::string & fieldName);
-protected:
-	std::pair<const CArtifactInstance *, const CArtifactInstance *> searchForConstituent(const ArtifactID & aid) const;
+	const CArtifactInstance * getCombinedArtWithPart(const ArtifactID & partId) const;
 
 private:
+	void setNewArtSlot(const ArtifactPosition & slot, CArtifactInstance * art, bool locked);
+
 	void serializeJsonHero(JsonSerializeFormat & handler);
 	void serializeJsonCreature(JsonSerializeFormat & handler);
 	void serializeJsonCommander(JsonSerializeFormat & handler);

+ 3 - 25
lib/CArtifactInstance.cpp

@@ -49,13 +49,13 @@ const std::vector<CCombinedArtifactInstance::PartInfo> & CCombinedArtifactInstan
 	return partsInfo;
 }
 
-void CCombinedArtifactInstance::addPlacementMap(CArtifactSet::ArtPlacementMap & placementMap)
+void CCombinedArtifactInstance::addPlacementMap(const CArtifactSet::ArtPlacementMap & placementMap)
 {
 	if(!placementMap.empty())
 		for(auto & part : partsInfo)
 		{
-			assert(placementMap.find(part.art) != placementMap.end());
-			part.slot = placementMap.at(part.art);
+			if(placementMap.find(part.art) != placementMap.end())
+				part.slot = placementMap.at(part.art);
 		}
 }
 
@@ -167,28 +167,6 @@ bool CArtifactInstance::isScroll() const
 	return artType->isScroll();
 }
 
-void CArtifactInstance::putAt(CArtifactSet & set, const ArtifactPosition slot)
-{
-	auto placementMap = set.putArtifact(slot, this);
-	addPlacementMap(placementMap);
-}
-
-void CArtifactInstance::removeFrom(CArtifactSet & set, const ArtifactPosition slot)
-{
-	set.removeArtifact(slot);
-	for(auto & part : partsInfo)
-	{
-		if(part.slot != ArtifactPosition::PRE_FIRST)
-			part.slot = ArtifactPosition::PRE_FIRST;
-	}
-}
-
-void CArtifactInstance::move(CArtifactSet & srcSet, const ArtifactPosition srcSlot, CArtifactSet & dstSet, const ArtifactPosition dstSlot)
-{
-	removeFrom(srcSet, srcSlot);
-	putAt(dstSet, dstSlot);
-}
-
 void CArtifactInstance::deserializationFix()
 {
 	setType(artType);

+ 2 - 5
lib/CArtifactInstance.h

@@ -25,7 +25,7 @@ protected:
 public:
 	struct PartInfo
 	{
-		ConstTransitivePtr<CArtifactInstance> art;
+		CArtifactInstance * art;
 		ArtifactPosition slot;
 		template <typename Handler> void serialize(Handler & h)
 		{
@@ -39,7 +39,7 @@ public:
 	// Checks if supposed part inst is part of this combined art inst
 	bool isPart(const CArtifactInstance * supposedPart) const;
 	const std::vector<PartInfo> & getPartsInfo() const;
-	void addPlacementMap(CArtifactSet::ArtPlacementMap & placementMap);
+	void addPlacementMap(const CArtifactSet::ArtPlacementMap & placementMap);
 
 	template <typename Handler> void serialize(Handler & h)
 	{
@@ -88,9 +88,6 @@ public:
 		bool assumeDestRemoved = false) const;
 	bool isCombined() const;
 	bool isScroll() const;
-	void putAt(CArtifactSet & set, const ArtifactPosition slot);
-	void removeFrom(CArtifactSet & set, const ArtifactPosition slot);
-	void move(CArtifactSet & srcSet, const ArtifactPosition srcSlot, CArtifactSet & dstSet, const ArtifactPosition dstSlot);
 	
 	void deserializationFix();
 	template <typename Handler> void serialize(Handler & h)

+ 5 - 4
lib/CBonusTypeHandler.cpp

@@ -200,8 +200,9 @@ ImagePath CBonusTypeHandler::bonusToGraphics(const std::shared_ptr<Bonus> & bonu
 
 void CBonusTypeHandler::load()
 {
-	const JsonNode gameConf(JsonPath::builtin("config/gameConfig.json"));
-	const JsonNode config(JsonUtils::assembleFromFiles(gameConf["bonuses"].convertTo<std::vector<std::string>>()));
+	JsonNode gameConf(JsonPath::builtin("config/gameConfig.json"));
+	JsonNode config(JsonUtils::assembleFromFiles(gameConf["bonuses"].convertTo<std::vector<std::string>>()));
+	config.setModScope("vcmi");
 	load(config);
 }
 
@@ -240,8 +241,8 @@ void CBonusTypeHandler::loadItem(const JsonNode & source, CBonusType & dest, con
 
 	if (!dest.hidden)
 	{
-		VLC->generaltexth->registerString( "vcmi", dest.getNameTextID(), source["name"].String());
-		VLC->generaltexth->registerString( "vcmi", dest.getDescriptionTextID(), source["description"].String());
+		VLC->generaltexth->registerString( "vcmi", dest.getNameTextID(), source["name"]);
+		VLC->generaltexth->registerString( "vcmi", dest.getDescriptionTextID(), source["description"]);
 	}
 
 	const JsonNode & graphics = source["graphics"];

+ 3 - 3
lib/CCreatureHandler.cpp

@@ -617,9 +617,9 @@ std::shared_ptr<CCreature> CCreatureHandler::loadFromJson(const std::string & sc
 
 	cre->cost = ResourceSet(node["cost"]);
 
-	VLC->generaltexth->registerString(scope, cre->getNameSingularTextID(), node["name"]["singular"].String());
-	VLC->generaltexth->registerString(scope, cre->getNamePluralTextID(), node["name"]["plural"].String());
-	VLC->generaltexth->registerString(scope, cre->getDescriptionTextID(), node["description"].String());
+	VLC->generaltexth->registerString(scope, cre->getNameSingularTextID(), node["name"]["singular"]);
+	VLC->generaltexth->registerString(scope, cre->getNamePluralTextID(), node["name"]["plural"]);
+	VLC->generaltexth->registerString(scope, cre->getDescriptionTextID(), node["description"]);
 
 	cre->addBonus(node["hitPoints"].Integer(), BonusType::STACK_HEALTH);
 	cre->addBonus(node["speed"].Integer(), BonusType::STACKS_SPEED);

+ 3 - 3
lib/CCreatureSet.cpp

@@ -855,7 +855,7 @@ std::string CStackInstance::getName() const
 ui64 CStackInstance::getPower() const
 {
 	assert(type);
-	return type->getAIValue() * count;
+	return static_cast<ui64>(type->getAIValue()) * count;
 }
 
 ArtBearer::ArtBearer CStackInstance::bearerType() const
@@ -863,7 +863,7 @@ ArtBearer::ArtBearer CStackInstance::bearerType() const
 	return ArtBearer::CREATURE;
 }
 
-CStackInstance::ArtPlacementMap CStackInstance::putArtifact(ArtifactPosition pos, CArtifactInstance * art)
+CStackInstance::ArtPlacementMap CStackInstance::putArtifact(const ArtifactPosition & pos, CArtifactInstance * art)
 {
 	assert(!getArt(pos));
 	assert(art->canBePutAt(this, pos));
@@ -872,7 +872,7 @@ CStackInstance::ArtPlacementMap CStackInstance::putArtifact(ArtifactPosition pos
 	return CArtifactSet::putArtifact(pos, art);
 }
 
-void CStackInstance::removeArtifact(ArtifactPosition pos)
+void CStackInstance::removeArtifact(const ArtifactPosition & pos)
 {
 	assert(getArt(pos));
 

+ 2 - 2
lib/CCreatureSet.h

@@ -126,8 +126,8 @@ public:
 	void setArmyObj(const CArmedInstance *ArmyObj);
 	virtual void giveStackExp(TExpType exp);
 	bool valid(bool allowUnrandomized) const;
-	ArtPlacementMap putArtifact(ArtifactPosition pos, CArtifactInstance * art) override;//from CArtifactSet
-	void removeArtifact(ArtifactPosition pos) override;
+	ArtPlacementMap putArtifact(const ArtifactPosition & pos, CArtifactInstance * art) override;//from CArtifactSet
+	void removeArtifact(const ArtifactPosition & pos) override;
 	ArtBearer::ArtBearer bearerType() const override; //from CArtifactSet
 	std::string nodeName() const override; //from CBonusSystemnode
 	void deserializationFix();

+ 1 - 1
lib/CGameInfoCallback.cpp

@@ -964,7 +964,7 @@ std::vector<ObjectInstanceID> CGameInfoCallback::getVisibleTeleportObjects(std::
 	vstd::erase_if(ids, [&](const ObjectInstanceID & id) -> bool
 	{
 		const auto * obj = getObj(id, false);
-		return player != PlayerColor::UNFLAGGABLE && (!obj || !isVisible(obj->pos, player));
+		return player != PlayerColor::UNFLAGGABLE && (!obj || !isVisible(obj->visitablePos(), player));
 	});
 	return ids;
 }

+ 5 - 5
lib/CHeroHandler.cpp

@@ -459,11 +459,11 @@ std::shared_ptr<CHero> CHeroHandler::loadFromJson(const std::string & scope, con
 	hero->onlyOnWaterMap = node["onlyOnWaterMap"].Bool();
 	hero->onlyOnMapWithoutWater = node["onlyOnMapWithoutWater"].Bool();
 
-	VLC->generaltexth->registerString(scope, hero->getNameTextID(), node["texts"]["name"].String());
-	VLC->generaltexth->registerString(scope, hero->getBiographyTextID(), node["texts"]["biography"].String());
-	VLC->generaltexth->registerString(scope, hero->getSpecialtyNameTextID(), node["texts"]["specialty"]["name"].String());
-	VLC->generaltexth->registerString(scope, hero->getSpecialtyTooltipTextID(), node["texts"]["specialty"]["tooltip"].String());
-	VLC->generaltexth->registerString(scope, hero->getSpecialtyDescriptionTextID(), node["texts"]["specialty"]["description"].String());
+	VLC->generaltexth->registerString(scope, hero->getNameTextID(), node["texts"]["name"]);
+	VLC->generaltexth->registerString(scope, hero->getBiographyTextID(), node["texts"]["biography"]);
+	VLC->generaltexth->registerString(scope, hero->getSpecialtyNameTextID(), node["texts"]["specialty"]["name"]);
+	VLC->generaltexth->registerString(scope, hero->getSpecialtyTooltipTextID(), node["texts"]["specialty"]["tooltip"]);
+	VLC->generaltexth->registerString(scope, hero->getSpecialtyDescriptionTextID(), node["texts"]["specialty"]["description"]);
 
 	hero->iconSpecSmall = node["images"]["specialtySmall"].String();
 	hero->iconSpecLarge = node["images"]["specialtyLarge"].String();

+ 5 - 0
lib/CMakeLists.txt

@@ -184,6 +184,8 @@ set(lib_MAIN_SRCS
 	rmg/TileInfo.cpp
 	rmg/Zone.cpp
 	rmg/Functions.cpp
+	rmg/ObjectInfo.cpp
+	rmg/ObjectConfig.cpp
 	rmg/RmgMap.cpp
 	rmg/PenroseTiling.cpp
 	rmg/modificators/Modificator.cpp
@@ -510,6 +512,7 @@ set(lib_MAIN_HEADERS
 	mapObjects/IOwnableObject.h
 	mapObjects/MapObjects.h
 	mapObjects/MiscObjects.h
+	mapObjects/CompoundMapObjectID.h
 	mapObjects/ObjectTemplate.h
 	mapObjects/ObstacleSetHandler.h
 
@@ -587,6 +590,8 @@ set(lib_MAIN_HEADERS
 	rmg/RmgMap.h
 	rmg/float3.h
 	rmg/Functions.h
+	rmg/ObjectInfo.h
+	rmg/ObjectConfig.h
 	rmg/PenroseTiling.h
 	rmg/modificators/Modificator.h
 	rmg/modificators/ObjectManager.h

+ 2 - 2
lib/CSkillHandler.cpp

@@ -212,7 +212,7 @@ std::shared_ptr<CSkill> CSkillHandler::loadFromJson(const std::string & scope, c
 
 	skill->onlyOnWaterMap = json["onlyOnWaterMap"].Bool();
 
-	VLC->generaltexth->registerString(scope, skill->getNameTextID(), json["name"].String());
+	VLC->generaltexth->registerString(scope, skill->getNameTextID(), json["name"]);
 	switch(json["gainChance"].getType())
 	{
 	case JsonNode::JsonType::DATA_INTEGER:
@@ -237,7 +237,7 @@ std::shared_ptr<CSkill> CSkillHandler::loadFromJson(const std::string & scope, c
 			skill->addNewBonus(bonus, level);
 		}
 		CSkill::LevelInfo & skillAtLevel = skill->at(level);
-		VLC->generaltexth->registerString(scope, skill->getDescriptionTextID(level), levelNode["description"].String());
+		VLC->generaltexth->registerString(scope, skill->getDescriptionTextID(level), levelNode["description"]);
 		skillAtLevel.iconSmall = levelNode["images"]["small"].String();
 		skillAtLevel.iconMedium = levelNode["images"]["medium"].String();
 		skillAtLevel.iconLarge = levelNode["images"]["large"].String();

+ 1 - 0
lib/GameSettings.cpp

@@ -105,6 +105,7 @@ const std::vector<GameSettings::SettingOption> GameSettings::settingProperties =
 		{EGameSettings::TOWNS_SPELL_RESEARCH_COST,                        "towns",     "spellResearchCost"                    },
 		{EGameSettings::TOWNS_SPELL_RESEARCH_PER_DAY,                     "towns",     "spellResearchPerDay"                  },
 		{EGameSettings::TOWNS_SPELL_RESEARCH_COST_EXPONENT_PER_RESEARCH,  "towns",     "spellResearchCostExponentPerResearch" },
+		{EGameSettings::INTERFACE_PLAYER_COLORED_BACKGROUND,              "interface", "playerColoredBackground"              },
 	};
 
 void GameSettings::loadBase(const JsonNode & input)

+ 1 - 1
lib/IGameCallback.h

@@ -123,7 +123,7 @@ public:
 
 	virtual bool giveHeroNewArtifact(const CGHeroInstance * h, const ArtifactID & artId, const ArtifactPosition & pos) = 0;
 	virtual bool giveHeroNewScroll(const CGHeroInstance * h, const SpellID & spellId, const ArtifactPosition & pos) = 0;
-	virtual bool putArtifact(const ArtifactLocation & al, const CArtifactInstance * art, std::optional<bool> askAssemble = std::nullopt) = 0;
+	virtual bool putArtifact(const ArtifactLocation & al, const ArtifactInstanceID & id, std::optional<bool> askAssemble = std::nullopt) = 0;
 	virtual void removeArtifact(const ArtifactLocation& al) = 0;
 	virtual bool moveArtifact(const PlayerColor & player, const ArtifactLocation & al1, const ArtifactLocation & al2) = 0;
 

+ 1 - 0
lib/IGameSettings.h

@@ -79,6 +79,7 @@ enum class EGameSettings
 	TEXTS_TERRAIN,
 	TOWNS_BUILDINGS_PER_TURN_CAP,
 	TOWNS_STARTING_DWELLING_CHANCES,
+	INTERFACE_PLAYER_COLORED_BACKGROUND,
 	TOWNS_SPELL_RESEARCH,
 	TOWNS_SPELL_RESEARCH_COST,
 	TOWNS_SPELL_RESEARCH_PER_DAY,

+ 1 - 1
lib/RiverHandler.cpp

@@ -50,7 +50,7 @@ std::shared_ptr<RiverType> RiverTypeHandler::loadFromJson(
 		info->paletteAnimation.push_back(element);
 	}
 
-	VLC->generaltexth->registerString(scope, info->getNameTextID(), json["text"].String());
+	VLC->generaltexth->registerString(scope, info->getNameTextID(), json["text"]);
 
 	return info;
 }

+ 1 - 1
lib/RoadHandler.cpp

@@ -41,7 +41,7 @@ std::shared_ptr<RoadType> RoadTypeHandler::loadFromJson(
 	info->shortIdentifier = json["shortIdentifier"].String();
 	info->movementCost    = json["moveCost"].Integer();
 
-	VLC->generaltexth->registerString(scope,info->getNameTextID(), json["text"].String());
+	VLC->generaltexth->registerString(scope,info->getNameTextID(), json["text"]);
 
 	return info;
 }

+ 1 - 1
lib/TerrainHandler.cpp

@@ -45,7 +45,7 @@ std::shared_ptr<TerrainType> TerrainTypeHandler::loadFromJson( const std::string
 	info->transitionRequired = json["transitionRequired"].Bool();
 	info->terrainViewPatterns = json["terrainViewPatterns"].String();
 
-	VLC->generaltexth->registerString(scope, info->getNameTextID(), json["text"].String());
+	VLC->generaltexth->registerString(scope, info->getNameTextID(), json["text"]);
 
 	const JsonVector & unblockedVec = json["minimapUnblocked"].Vector();
 	info->minimapUnblocked =

+ 5 - 5
lib/entities/faction/CTownHandler.cpp

@@ -292,8 +292,8 @@ void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, cons
 	ret->modScope = source.getModScope();
 	ret->town = town;
 
-	VLC->generaltexth->registerString(source.getModScope(), ret->getNameTextID(), source["name"].String());
-	VLC->generaltexth->registerString(source.getModScope(), ret->getDescriptionTextID(), source["description"].String());
+	VLC->generaltexth->registerString(source.getModScope(), ret->getNameTextID(), source["name"]);
+	VLC->generaltexth->registerString(source.getModScope(), ret->getDescriptionTextID(), source["description"]);
 
 	ret->subId = vstd::find_or(MappedKeys::SPECIAL_BUILDINGS, source["type"].String(), BuildingSubID::NONE);
 	ret->resources = TResources(source["cost"]);
@@ -603,7 +603,7 @@ void CTownHandler::loadTown(CTown * town, const JsonNode & source)
 	town->namesCount = 0;
 	for(const auto & name : source["names"].Vector())
 	{
-		VLC->generaltexth->registerString(town->faction->modScope, town->getRandomNameTextID(town->namesCount), name.String());
+		VLC->generaltexth->registerString(town->faction->modScope, town->getRandomNameTextID(town->namesCount), name);
 		town->namesCount += 1;
 	}
 
@@ -718,8 +718,8 @@ std::shared_ptr<CFaction> CTownHandler::loadFromJson(const std::string & scope,
 	faction->modScope = scope;
 	faction->identifier = identifier;
 
-	VLC->generaltexth->registerString(scope, faction->getNameTextID(), source["name"].String());
-	VLC->generaltexth->registerString(scope, faction->getDescriptionTextID(), source["description"].String());
+	VLC->generaltexth->registerString(scope, faction->getNameTextID(), source["name"]);
+	VLC->generaltexth->registerString(scope, faction->getDescriptionTextID(), source["description"]);
 
 	faction->creatureBg120 = ImagePath::fromJson(source["creatureBackground"]["120px"]);
 	faction->creatureBg130 = ImagePath::fromJson(source["creatureBackground"]["130px"]);

+ 3 - 1
lib/gameState/CGameState.cpp

@@ -267,6 +267,8 @@ void CGameState::updateOnLoad(StartInfo * si)
 	for(auto & i : si->playerInfos)
 		gs->players[i.first].human = i.second.isControlledByHuman();
 	scenarioOps->extraOptionsInfo = si->extraOptionsInfo;
+	scenarioOps->turnTimerInfo = si->turnTimerInfo;
+	scenarioOps->simturnsInfo = si->simturnsInfo;
 }
 
 void CGameState::initNewGame(const IMapService * mapService, bool allowSavingRandomMap, Load::ProgressAccumulator & progressTracking)
@@ -1638,7 +1640,7 @@ bool CGameState::giveHeroArtifact(CGHeroInstance * h, const ArtifactID & aid)
 	 auto slot = ArtifactUtils::getArtAnyPosition(h, aid);
 	 if(ArtifactUtils::isSlotEquipment(slot) || ArtifactUtils::isSlotBackpack(slot))
 	 {
-		 ai->putAt(*h, slot);
+		 map->putArtifactInstance(*h, ai, slot);
 		 return true;
 	 }
 	 else

+ 4 - 4
lib/gameState/CGameStateCampaign.cpp

@@ -147,7 +147,7 @@ void CGameStateCampaign::trimCrossoverHeroesParameters(const CampaignTravel & tr
 				if (!locked && !takeable)
 				{
 					logGlobal->debug("Removing artifact %s from slot %d of hero %s", art->artType->getJsonKey(), al.slot.getNum(), hero.hero->getHeroTypeName());
-					hero.hero->getArt(al.slot)->removeFrom(*hero.hero, al.slot);
+					gameState->map->removeArtifactInstance(*hero.hero, al.slot);
 					return true;
 				}
 				return false;
@@ -327,7 +327,7 @@ void CGameStateCampaign::giveCampaignBonusToHero(CGHeroInstance * hero)
 			CArtifactInstance * scroll = ArtifactUtils::createScroll(SpellID(curBonus->info2));
 			const auto slot = ArtifactUtils::getArtAnyPosition(hero, scroll->getTypeId());
 			if(ArtifactUtils::isSlotEquipment(slot) || ArtifactUtils::isSlotBackpack(slot))
-				scroll->putAt(*hero, slot);
+				gameState->map->putArtifactInstance(*hero, scroll, slot);
 			else
 				logGlobal->error("Cannot give starting scroll - no free slots!");
 			break;
@@ -423,7 +423,7 @@ void CGameStateCampaign::transferMissingArtifacts(const CampaignTravel & travelO
 			auto * artifact = donorHero->getArt(artLocation);
 
 			logGlobal->debug("Removing artifact %s from slot %d of hero %s for transfer", artifact->artType->getJsonKey(), artLocation.getNum(), donorHero->getHeroTypeName());
-			artifact->removeFrom(*donorHero, artLocation);
+			gameState->map->removeArtifactInstance(*donorHero, artLocation);
 
 			if (receiver)
 			{
@@ -431,7 +431,7 @@ void CGameStateCampaign::transferMissingArtifacts(const CampaignTravel & travelO
 
 				const auto slot = ArtifactUtils::getArtAnyPosition(receiver, artifact->getTypeId());
 				if(ArtifactUtils::isSlotEquipment(slot) || ArtifactUtils::isSlotBackpack(slot))
-					artifact->putAt(*receiver, slot);
+					gameState->map->putArtifactInstance(*receiver, artifact, slot);
 				else
 					logGlobal->error("Cannot transfer artifact - no free slots!");
 			}

+ 21 - 0
lib/json/JsonUtils.cpp

@@ -230,6 +230,27 @@ void JsonUtils::inherit(JsonNode & descendant, const JsonNode & base)
 	std::swap(descendant, inheritedNode);
 }
 
+JsonNode JsonUtils::assembleFromFiles(const JsonNode & files, bool & isValid)
+{
+	if (files.isVector())
+	{
+		auto configList = files.convertTo<std::vector<std::string> >();
+		JsonNode result = JsonUtils::assembleFromFiles(configList, isValid);
+
+		return result;
+	}
+	else
+	{
+		return files;
+	}
+}
+
+JsonNode JsonUtils::assembleFromFiles(const JsonNode & files)
+{
+	bool isValid = false;
+	return assembleFromFiles(files, isValid);
+}
+
 JsonNode JsonUtils::assembleFromFiles(const std::vector<std::string> & files)
 {
 	bool isValid = false;

+ 2 - 0
lib/json/JsonUtils.h

@@ -44,6 +44,8 @@ namespace JsonUtils
 	 * @brief generate one Json structure from multiple files
 	 * @param files - list of filenames with parts of json structure
 	 */
+	DLL_LINKAGE JsonNode assembleFromFiles(const JsonNode & files);
+	DLL_LINKAGE JsonNode assembleFromFiles(const JsonNode & files, bool & isValid);
 	DLL_LINKAGE JsonNode assembleFromFiles(const std::vector<std::string> & files);
 	DLL_LINKAGE JsonNode assembleFromFiles(const std::vector<std::string> & files, bool & isValid);
 

+ 1 - 1
lib/mapObjectConstructors/CBankInstanceConstructor.cpp

@@ -28,7 +28,7 @@ void CBankInstanceConstructor::initTypeData(const JsonNode & input)
 	if (input.Struct().count("name") == 0)
 		logMod->warn("Bank %s missing name!", getJsonKey());
 
-	VLC->generaltexth->registerString(input.getModScope(), getNameTextID(), input["name"].String());
+	VLC->generaltexth->registerString(input.getModScope(), getNameTextID(), input["name"]);
 
 	levels = input["levels"].Vector();
 	bankResetDuration = static_cast<si32>(input["resetDuration"].Float());

+ 71 - 1
lib/mapObjectConstructors/CObjectClassesHandler.cpp

@@ -40,6 +40,8 @@
 #include "../texts/CGeneralTextHandler.h"
 #include "../texts/CLegacyConfigParser.h"
 
+#include <vstd/StringUtils.h>
+
 VCMI_LIB_NAMESPACE_BEGIN
 
 CObjectClassesHandler::CObjectClassesHandler()
@@ -276,7 +278,7 @@ std::unique_ptr<ObjectClass> CObjectClassesHandler::loadFromJson(const std::stri
 	newObject->base = json["base"];
 	newObject->id = index;
 
-	VLC->generaltexth->registerString(scope, newObject->getNameTextID(), json["name"].String());
+	VLC->generaltexth->registerString(scope, newObject->getNameTextID(), json["name"]);
 
 	newObject->objectTypeHandlers.resize(json["lastReservedIndex"].Float() + 1);
 
@@ -390,6 +392,62 @@ TObjectTypeHandler CObjectClassesHandler::getHandlerFor(CompoundMapObjectID comp
 	return getHandlerFor(compoundIdentifier.primaryID, compoundIdentifier.secondaryID);
 }
 
+CompoundMapObjectID CObjectClassesHandler::getCompoundIdentifier(const std::string & scope, const std::string & type, const std::string & subtype) const
+{
+	std::optional<si32> id;
+	if (scope.empty())
+	{
+		id = VLC->identifiers()->getIdentifier("object", type);
+	}
+	else
+	{
+		id = VLC->identifiers()->getIdentifier(scope, "object", type);
+	}
+
+	if(id)
+	{
+		if (subtype.empty())
+			return CompoundMapObjectID(id.value(), 0);
+
+		const auto & object = mapObjectTypes.at(id.value());
+		std::optional<si32> subID = VLC->identifiers()->getIdentifier(scope, object->getJsonKey(), subtype);
+
+		if (subID)
+			return CompoundMapObjectID(id.value(), subID.value());
+	}
+
+	std::string errorString = "Failed to get id for object of type " + type + "." + subtype;
+	logGlobal->error(errorString);
+	throw std::runtime_error(errorString);
+}
+
+CompoundMapObjectID CObjectClassesHandler::getCompoundIdentifier(const std::string & objectName) const
+{
+	std::string subtype = "object"; //Default for objects with no subIds
+	std::string type;
+
+	auto scopeAndFullName = vstd::splitStringToPair(objectName, ':');
+	logGlobal->debug("scopeAndFullName: %s, %s", scopeAndFullName.first, scopeAndFullName.second);
+	
+	auto typeAndName = vstd::splitStringToPair(scopeAndFullName.second, '.');
+	logGlobal->debug("typeAndName: %s, %s", typeAndName.first, typeAndName.second);
+	
+	auto nameAndSubtype = vstd::splitStringToPair(typeAndName.second, '.');
+	logGlobal->debug("nameAndSubtype: %s, %s", nameAndSubtype.first, nameAndSubtype.second);
+
+	if (!nameAndSubtype.first.empty())
+	{
+		type = nameAndSubtype.first;
+		subtype = nameAndSubtype.second;
+	}
+	else
+	{
+		type = typeAndName.second;
+	}
+	
+	return getCompoundIdentifier(boost::to_lower_copy(scopeAndFullName.first), type, subtype);
+}
+
 std::set<MapObjectID> CObjectClassesHandler::knownObjects() const
 {
 	std::set<MapObjectID> ret;
@@ -459,6 +517,18 @@ void CObjectClassesHandler::afterLoadFinalization()
 				logGlobal->warn("No templates found for %s:%s", entry->getJsonKey(), obj->getJsonKey());
 		}
 	}
+
+	for(auto & entry : objectIdHandlers)
+	{
+		// Call function for each object id
+		entry.second(entry.first);
+	}
+}
+
+void CObjectClassesHandler::resolveObjectCompoundId(const std::string & id, std::function<void(CompoundMapObjectID)> callback)
+{
+	auto compoundId = getCompoundIdentifier(id);
+	objectIdHandlers.push_back(std::make_pair(compoundId, callback));
 }
 
 void CObjectClassesHandler::generateExtraMonolithsForRMG(ObjectClass * container)

+ 8 - 23
lib/mapObjectConstructors/CObjectClassesHandler.h

@@ -9,7 +9,7 @@
  */
 #pragma once
 
-#include "../constants/EntityIdentifiers.h"
+#include "../mapObjects/CompoundMapObjectID.h"
 #include "../IHandlerBase.h"
 #include "../json/JsonNode.h"
 
@@ -19,27 +19,6 @@ class AObjectTypeHandler;
 class ObjectTemplate;
 struct SObjectSounds;
 
-struct DLL_LINKAGE CompoundMapObjectID
-{
-	si32 primaryID;
-	si32 secondaryID;
-
-	CompoundMapObjectID(si32 primID, si32 secID) : primaryID(primID), secondaryID(secID) {};
-
-	bool operator<(const CompoundMapObjectID& other) const
-	{
-		if(this->primaryID != other.primaryID)
-			return this->primaryID < other.primaryID;
-		else
-			return this->secondaryID < other.secondaryID;
-	}
-
-	bool operator==(const CompoundMapObjectID& other) const
-	{
-		return (this->primaryID == other.primaryID) && (this->secondaryID == other.secondaryID);
-	}
-};
-
 class CGObjectInstance;
 
 using TObjectTypeHandler = std::shared_ptr<AObjectTypeHandler>;
@@ -74,6 +53,8 @@ class DLL_LINKAGE CObjectClassesHandler : public IHandlerBase, boost::noncopyabl
 	/// map that is filled during construction with all known handlers. Not serializeable due to usage of std::function
 	std::map<std::string, std::function<TObjectTypeHandler()> > handlerConstructors;
 
+	std::vector<std::pair<CompoundMapObjectID, std::function<void(CompoundMapObjectID)>>> objectIdHandlers;
+
 	/// container with H3 templates, used only during loading, no need to serialize it
 	using TTemplatesContainer = std::multimap<std::pair<MapObjectID, MapObjectSubID>, std::shared_ptr<const ObjectTemplate>>;
 	TTemplatesContainer legacyTemplates;
@@ -110,15 +91,19 @@ public:
 	TObjectTypeHandler getHandlerFor(MapObjectID type, MapObjectSubID subtype) const;
 	TObjectTypeHandler getHandlerFor(const std::string & scope, const std::string & type, const std::string & subtype) const;
 	TObjectTypeHandler getHandlerFor(CompoundMapObjectID compoundIdentifier) const;
+	CompoundMapObjectID getCompoundIdentifier(const std::string & scope, const std::string & type, const std::string & subtype) const;
+	CompoundMapObjectID getCompoundIdentifier(const std::string & objectName) const;
 
 	std::string getObjectName(MapObjectID type, MapObjectSubID subtype) const;
 
 	SObjectSounds getObjectSounds(MapObjectID type, MapObjectSubID subtype) const;
 
+	void resolveObjectCompoundId(const std::string & id, std::function<void(CompoundMapObjectID)> callback);
+
 	/// Returns handler string describing the handler (for use in client)
 	std::string getObjectHandlerName(MapObjectID type) const;
 
 	std::string getJsonKey(MapObjectID type) const;
 };
 
-VCMI_LIB_NAMESPACE_END
+VCMI_LIB_NAMESPACE_END

+ 4 - 3
lib/mapObjectConstructors/CRewardableConstructor.cpp

@@ -23,7 +23,7 @@ void CRewardableConstructor::initTypeData(const JsonNode & config)
 	blockVisit = config["blockedVisitable"].Bool();
 
 	if (!config["name"].isNull())
-		VLC->generaltexth->registerString( config.getModScope(), getNameTextID(), config["name"].String());
+		VLC->generaltexth->registerString( config.getModScope(), getNameTextID(), config["name"]);
 
 	JsonUtils::validate(config, "vcmi:rewardable", getJsonKey());
 	
@@ -43,9 +43,10 @@ CGObjectInstance * CRewardableConstructor::create(IGameCallback * cb, std::share
 	return ret;
 }
 
-Rewardable::Configuration CRewardableConstructor::generateConfiguration(IGameCallback * cb, vstd::RNG & rand, MapObjectID objectID) const
+Rewardable::Configuration CRewardableConstructor::generateConfiguration(IGameCallback * cb, vstd::RNG & rand, MapObjectID objectID, const std::map<std::string, JsonNode> & presetVariables) const
 {
 	Rewardable::Configuration result;
+	result.variables.preset = presetVariables;
 	objectInfo.configureObject(result, rand, cb);
 
 	for(auto & rewardInfo : result.info)
@@ -67,7 +68,7 @@ void CRewardableConstructor::configureObject(CGObjectInstance * object, vstd::RN
 	if (!rewardableObject)
 		throw std::runtime_error("Object " + std::to_string(object->getObjGroupIndex()) + ", " + std::to_string(object->getObjTypeIndex()) + " is not a rewardable object!" );
 
-	rewardableObject->configuration = generateConfiguration(object->cb, rng, object->ID);
+	rewardableObject->configuration = generateConfiguration(object->cb, rng, object->ID, rewardableObject->configuration.variables.preset);
 	rewardableObject->initializeGuards();
 
 	if (rewardableObject->configuration.info.empty())

+ 1 - 1
lib/mapObjectConstructors/CRewardableConstructor.h

@@ -31,7 +31,7 @@ public:
 
 	std::unique_ptr<IObjectInfo> getObjectInfo(std::shared_ptr<const ObjectTemplate> tmpl) const override;
 
-	Rewardable::Configuration generateConfiguration(IGameCallback * cb, vstd::RNG & rand, MapObjectID objectID) const;
+	Rewardable::Configuration generateConfiguration(IGameCallback * cb, vstd::RNG & rand, MapObjectID objectID, const std::map<std::string, JsonNode> & presetVariables) const;
 };
 
 VCMI_LIB_NAMESPACE_END

+ 1 - 1
lib/mapObjectConstructors/DwellingInstanceConstructor.cpp

@@ -29,7 +29,7 @@ void DwellingInstanceConstructor::initTypeData(const JsonNode & input)
 	if (input.Struct().count("name") == 0)
 		logMod->warn("Dwelling %s missing name!", getJsonKey());
 
-	VLC->generaltexth->registerString( input.getModScope(), getNameTextID(), input["name"].String());
+	VLC->generaltexth->registerString( input.getModScope(), getNameTextID(), input["name"]);
 
 	const JsonVector & levels = input["creatures"].Vector();
 	const auto totalLevels = levels.size();

+ 3 - 0
lib/mapObjectConstructors/IObjectInfo.h

@@ -49,7 +49,10 @@ public:
 
 	virtual bool givesBonuses() const { return false; }
 
+	virtual bool hasGuards() const { return false; }
+
 	virtual ~IObjectInfo() = default;
+
 };
 
 VCMI_LIB_NAMESPACE_END

+ 20 - 7
lib/mapObjects/CGCreature.cpp

@@ -66,6 +66,18 @@ std::string CGCreature::getHoverText(const CGHeroInstance * hero) const
 	}
 }
 
+std::string CGCreature::getMonsterLevelText() const
+{
+	std::string monsterLevel = VLC->generaltexth->translate("vcmi.adventureMap.monsterLevel");
+	bool isRanged = VLC->creatures()->getById(getCreature())->getBonusBearer()->hasBonusOfType(BonusType::SHOOTER);
+	std::string attackTypeKey = isRanged ? "vcmi.adventureMap.monsterRangedType" : "vcmi.adventureMap.monsterMeleeType";
+	std::string attackType = VLC->generaltexth->translate(attackTypeKey);
+	boost::replace_first(monsterLevel, "%TOWN", (*VLC->townh)[VLC->creatures()->getById(getCreature())->getFaction()]->getNameTranslated());
+	boost::replace_first(monsterLevel, "%LEVEL", std::to_string(VLC->creatures()->getById(getCreature())->getLevel()));
+	boost::replace_first(monsterLevel, "%ATTACK_TYPE", attackType);
+	return monsterLevel;
+}
+
 std::string CGCreature::getPopupText(const CGHeroInstance * hero) const
 {
 	std::string hoverName;
@@ -102,15 +114,13 @@ std::string CGCreature::getPopupText(const CGHeroInstance * hero) const
 
 	if (settings["general"]["enableUiEnhancements"].Bool())
 	{
-		std::string monsterLevel = VLC->generaltexth->translate("vcmi.adventureMap.monsterLevel");
-		boost::replace_first(monsterLevel, "%TOWN", (*VLC->townh)[VLC->creatures()->getById(getCreature())->getFaction()]->getNameTranslated());
-		boost::replace_first(monsterLevel, "%LEVEL", std::to_string(VLC->creatures()->getById(getCreature())->getLevel()));
-		hoverName += monsterLevel;
-
+		hoverName += getMonsterLevelText();
 		hoverName += VLC->generaltexth->translate("vcmi.adventureMap.monsterThreat.title");
 
 		int choice;
-		double ratio = (static_cast<double>(getArmyStrength()) / hero->getTotalStrength());
+		uint64_t armyStrength = getArmyStrength();
+		uint64_t heroStrength = hero->getTotalStrength();
+		double ratio = static_cast<double>(armyStrength) / heroStrength;
 		if (ratio < 0.1)  choice = 0;
 		else if (ratio < 0.25) choice = 1;
 		else if (ratio < 0.6)  choice = 2;
@@ -131,7 +141,10 @@ std::string CGCreature::getPopupText(const CGHeroInstance * hero) const
 
 std::string CGCreature::getPopupText(PlayerColor player) const
 {
-	return getHoverText(player);
+	std::string hoverName = getHoverText(player);
+	if (settings["general"]["enableUiEnhancements"].Bool())
+		hoverName += getMonsterLevelText();
+	return hoverName;
 }
 
 std::vector<Component> CGCreature::getPopupComponents(PlayerColor player) const

+ 1 - 1
lib/mapObjects/CGCreature.h

@@ -81,7 +81,7 @@ private:
 
 	int takenAction(const CGHeroInstance *h, bool allowJoin=true) const; //action on confrontation: -2 - fight, -1 - flee, >=0 - will join for given value of gold (may be 0)
 	void giveReward(const CGHeroInstance * h) const;
-
+	std::string getMonsterLevelText() const;
 };
 
 VCMI_LIB_NAMESPACE_END

+ 3 - 3
lib/mapObjects/CGHeroInstance.cpp

@@ -1156,7 +1156,7 @@ std::string CGHeroInstance::getBiographyTextID() const
 	return ""; //for random hero
 }
 
-CGHeroInstance::ArtPlacementMap CGHeroInstance::putArtifact(ArtifactPosition pos, CArtifactInstance * art)
+CGHeroInstance::ArtPlacementMap CGHeroInstance::putArtifact(const ArtifactPosition & pos, CArtifactInstance * art)
 {
 	assert(art->canBePutAt(this, pos));
 
@@ -1165,7 +1165,7 @@ CGHeroInstance::ArtPlacementMap CGHeroInstance::putArtifact(ArtifactPosition pos
 	return CArtifactSet::putArtifact(pos, art);
 }
 
-void CGHeroInstance::removeArtifact(ArtifactPosition pos)
+void CGHeroInstance::removeArtifact(const ArtifactPosition & pos)
 {
 	auto art = getArt(pos);
 	assert(art);
@@ -1201,7 +1201,7 @@ void CGHeroInstance::removeSpellbook()
 
 	if(hasSpellbook())
 	{
-		getArt(ArtifactPosition::SPELLBOOK)->removeFrom(*this, ArtifactPosition::SPELLBOOK);
+		cb->removeArtifact(ArtifactLocation(this->id, ArtifactPosition::SPELLBOOK));
 	}
 }
 

+ 2 - 2
lib/mapObjects/CGHeroInstance.h

@@ -241,8 +241,8 @@ public:
 	void initHero(vstd::RNG & rand);
 	void initHero(vstd::RNG & rand, const HeroTypeID & SUBID);
 
-	ArtPlacementMap putArtifact(ArtifactPosition pos, CArtifactInstance * art) override;
-	void removeArtifact(ArtifactPosition pos) override;
+	ArtPlacementMap putArtifact(const ArtifactPosition & pos, CArtifactInstance * art) override;
+	void removeArtifact(const ArtifactPosition & pos) override;
 	void initExp(vstd::RNG & rand);
 	void initArmy(vstd::RNG & rand, IArmyDescriptor *dst = nullptr);
 	void pushPrimSkill(PrimarySkill which, int val);

+ 1 - 1
lib/mapObjects/CQuest.cpp

@@ -152,7 +152,7 @@ void CQuest::completeQuest(IGameCallback * cb, const CGHeroInstance *h) const
 		}
 		else
 		{
-			const auto * assembly = h->getAssemblyByConstituent(elem);
+			const auto * assembly = h->getCombinedArtWithPart(elem);
 			assert(assembly);
 			auto parts = assembly->getPartsInfo();
 

+ 14 - 165
lib/mapObjects/CRewardableObject.cpp

@@ -12,99 +12,33 @@
 #include "CRewardableObject.h"
 
 #include "../CPlayerState.h"
-#include "../GameSettings.h"
 #include "../IGameCallback.h"
+#include "../IGameSettings.h"
 #include "../battle/BattleLayout.h"
 #include "../gameState/CGameState.h"
 #include "../mapObjectConstructors/AObjectTypeHandler.h"
-#include "../mapObjectConstructors/CObjectClassesHandler.h"
 #include "../mapObjectConstructors/CRewardableConstructor.h"
 #include "../mapObjects/CGHeroInstance.h"
 #include "../networkPacks/PacksForClient.h"
 #include "../networkPacks/PacksForClientBattle.h"
 #include "../serializer/JsonSerializeFormat.h"
-#include "../texts/CGeneralTextHandler.h"
 
 #include <vstd/RNG.h>
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-void CRewardableObject::grantRewardWithMessage(const CGHeroInstance * contextHero, int index, bool markAsVisit) const
+const IObjectInterface * CRewardableObject::getObject() const
 {
-	auto vi = configuration.info.at(index);
-	logGlobal->debug("Granting reward %d. Message says: %s", index, vi.message.toString());
-	// show message only if it is not empty or in infobox
-	if (configuration.infoWindowType != EInfoWindowMode::MODAL || !vi.message.toString().empty())
-	{
-		InfoWindow iw;
-		iw.player = contextHero->tempOwner;
-		iw.text = vi.message;
-		vi.reward.loadComponents(iw.components, contextHero);
-		iw.type = configuration.infoWindowType;
-		if(!iw.components.empty() || !iw.text.toString().empty())
-			cb->showInfoDialog(&iw);
-	}
-	// grant reward afterwards. Note that it may remove object
-	if(markAsVisit)
-		markAsVisited(contextHero);
-	grantReward(index, contextHero);
+	return this;
 }
 
-void CRewardableObject::selectRewardWithMessage(const CGHeroInstance * contextHero, const std::vector<ui32> & rewardIndices, const MetaString & dialog) const
+void CRewardableObject::markAsScouted(const CGHeroInstance * hero) const
 {
-	BlockingDialog sd(configuration.canRefuse, rewardIndices.size() > 1);
-	sd.player = contextHero->tempOwner;
-	sd.text = dialog;
-	sd.components = loadComponents(contextHero, rewardIndices);
-	cb->showBlockingDialog(this, &sd);
-
-}
-
-void CRewardableObject::grantAllRewardsWithMessage(const CGHeroInstance * contextHero, const std::vector<ui32> & rewardIndices, bool markAsVisit) const
-{
-	if (rewardIndices.empty())
-		return;
-		
-	for (auto index : rewardIndices)
-	{
-		// TODO: Merge all rewards of same type, with single message?
-		grantRewardWithMessage(contextHero, index, false);
-	}
-	// Mark visited only after all rewards were processed
-	if(markAsVisit)
-		markAsVisited(contextHero);
-}
-
-std::vector<Component> CRewardableObject::loadComponents(const CGHeroInstance * contextHero, const std::vector<ui32> & rewardIndices) const
-{
-	std::vector<Component> result;
-
-	if (rewardIndices.empty())
-		return result;
-
-	if (configuration.selectMode != Rewardable::SELECT_FIRST && rewardIndices.size() > 1)
-	{
-		for (auto index : rewardIndices)
-			result.push_back(configuration.info.at(index).reward.getDisplayedComponent(contextHero));
-	}
-	else
-	{
-		configuration.info.at(rewardIndices.front()).reward.loadComponents(result, contextHero);
-	}
-
-	return result;
-}
-
-bool CRewardableObject::guardedPotentially() const
-{
-	for (auto const & visitInfo : configuration.info)
-		if (!visitInfo.reward.guards.empty())
-			return true;
-
-	return false;
+	ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_PLAYER, id, hero->id);
+	cb->sendAndApply(&cov);
 }
 
-bool CRewardableObject::guardedPresently() const
+bool CRewardableObject::isGuarded() const
 {
 	return stacksCount() > 0;
 }
@@ -117,7 +51,7 @@ void CRewardableObject::onHeroVisit(const CGHeroInstance *hero) const
 		cb->sendAndApply(&cov);
 	}
 
-	if (guardedPresently())
+	if (isGuarded())
 	{
 		auto guardedIndexes = getAvailableRewards(hero, Rewardable::EEventType::EVENT_GUARDED);
 		auto guardedReward = configuration.info.at(guardedIndexes.at(0));
@@ -136,94 +70,9 @@ void CRewardableObject::onHeroVisit(const CGHeroInstance *hero) const
 	}
 }
 
-void CRewardableObject::doHeroVisit(const CGHeroInstance *h) const
-{
-	if(!wasVisitedBefore(h))
-	{
-		auto rewards = getAvailableRewards(h, Rewardable::EEventType::EVENT_FIRST_VISIT);
-		bool objectRemovalPossible = false;
-		for(auto index : rewards)
-		{
-			if(configuration.info.at(index).reward.removeObject)
-				objectRemovalPossible = true;
-		}
-
-		logGlobal->debug("Visiting object with %d possible rewards", rewards.size());
-		switch (rewards.size())
-		{
-			case 0: // no available rewards, e.g. visiting School of War without gold
-			{
-				auto emptyRewards = getAvailableRewards(h, Rewardable::EEventType::EVENT_NOT_AVAILABLE);
-				if (!emptyRewards.empty())
-					grantRewardWithMessage(h, emptyRewards[0], false);
-				else
-					logMod->warn("No applicable message for visiting empty object!");
-				break;
-			}
-			case 1: // one reward. Just give it with message
-			{
-				if (configuration.canRefuse)
-					selectRewardWithMessage(h, rewards, configuration.info.at(rewards.front()).message);
-				else
-					grantRewardWithMessage(h, rewards.front(), true);
-				break;
-			}
-			default: // multiple rewards. Act according to select mode
-			{
-				switch (configuration.selectMode) {
-					case Rewardable::SELECT_PLAYER: // player must select
-						selectRewardWithMessage(h, rewards, configuration.onSelect);
-						break;
-					case Rewardable::SELECT_FIRST: // give first available
-						if (configuration.canRefuse)
-							selectRewardWithMessage(h, { rewards.front() }, configuration.info.at(rewards.front()).message);
-						else
-							grantRewardWithMessage(h, rewards.front(), true);
-						break;
-					case Rewardable::SELECT_RANDOM: // give random
-					{
-						ui32 rewardIndex = *RandomGeneratorUtil::nextItem(rewards, cb->gameState()->getRandomGenerator());
-						if (configuration.canRefuse)
-							selectRewardWithMessage(h, { rewardIndex }, configuration.info.at(rewardIndex).message);
-						else
-							grantRewardWithMessage(h, rewardIndex, true);
-						break;
-					}
-					case Rewardable::SELECT_ALL: // grant all possible
-						grantAllRewardsWithMessage(h, rewards, true);
-						break;
-				}
-				break;
-			}
-		}
-
-		if(!objectRemovalPossible && getAvailableRewards(h, Rewardable::EEventType::EVENT_FIRST_VISIT).empty())
-		{
-			ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_PLAYER, id, h->id);
-			cb->sendAndApply(&cov);
-		}
-	}
-	else
-	{
-		logGlobal->debug("Revisiting already visited object");
-
-		if (!wasVisited(h->getOwner()))
-		{
-			ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_PLAYER, id, h->id);
-			cb->sendAndApply(&cov);
-		}
-
-		auto visitedRewards = getAvailableRewards(h, Rewardable::EEventType::EVENT_ALREADY_VISITED);
-		if (!visitedRewards.empty())
-			grantRewardWithMessage(h, visitedRewards[0], false);
-		else
-			logMod->warn("No applicable message for visiting already visited object!");
-	}
-}
-
 void CRewardableObject::heroLevelUpDone(const CGHeroInstance *hero) const
 {
-	grantRewardAfterLevelup(cb, configuration.info.at(selectedReward), this, hero);
+	grantRewardAfterLevelup(configuration.info.at(selectedReward), this, hero);
 }
 
 void CRewardableObject::battleFinished(const CGHeroInstance *hero, const BattleResult &result) const
@@ -236,7 +85,7 @@ void CRewardableObject::battleFinished(const CGHeroInstance *hero, const BattleR
 
 void CRewardableObject::blockingDialogAnswered(const CGHeroInstance * hero, int32_t answer) const
 {
-	if(guardedPresently())
+	if(isGuarded())
 	{
 		if (answer)
 		{
@@ -273,12 +122,12 @@ void CRewardableObject::markAsVisited(const CGHeroInstance * hero) const
 void CRewardableObject::grantReward(ui32 rewardID, const CGHeroInstance * hero) const
 {
 	cb->setObjPropertyValue(id, ObjProperty::REWARD_SELECT, rewardID);
-	grantRewardBeforeLevelup(cb, configuration.info.at(rewardID), hero);
+	grantRewardBeforeLevelup(configuration.info.at(rewardID), hero);
 	
 	// hero is not blocked by levelup dialog - grant remainder immediately
 	if(!cb->isVisitCoveredByAnotherQuery(this, hero))
 	{
-		grantRewardAfterLevelup(cb, configuration.info.at(rewardID), this, hero);
+		grantRewardAfterLevelup(configuration.info.at(rewardID), this, hero);
 	}
 }
 
@@ -410,7 +259,7 @@ std::vector<Component> CRewardableObject::getPopupComponentsImpl(PlayerColor pla
 	if (!wasScouted(player))
 		return {};
 
-	if (guardedPresently())
+	if (isGuarded())
 	{
 		if (!cb->getSettings().getBoolean(EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION))
 			return {};
@@ -480,7 +329,7 @@ void CRewardableObject::newTurn(vstd::RNG & rand) const
 		if (configuration.resetParameters.rewards)
 		{
 			auto handler = std::dynamic_pointer_cast<const CRewardableConstructor>(getObjectHandler());
-			auto newConfiguration = handler->generateConfiguration(cb, rand, ID);
+			auto newConfiguration = handler->generateConfiguration(cb, rand, ID, configuration.variables.preset);
 			cb->setRewardableObjectConfiguration(id, newConfiguration);
 		}
 		if (configuration.resetParameters.visitors)

+ 7 - 15
lib/mapObjects/CRewardableObject.h

@@ -25,32 +25,24 @@ protected:
 	/// reward selected by player, no serialize
 	ui16 selectedReward = 0;
 	
-	void grantReward(ui32 rewardID, const CGHeroInstance * hero) const;
-	void markAsVisited(const CGHeroInstance * hero) const;
+	void grantReward(ui32 rewardID, const CGHeroInstance * hero) const override;
+	void markAsVisited(const CGHeroInstance * hero) const override;
+
+	const IObjectInterface * getObject() const override;
+	void markAsScouted(const CGHeroInstance * hero) const override;
 	
 	/// return true if this object was "cleared" before and no longer has rewards applicable to selected hero
 	/// unlike wasVisited, this method uses information not available to player owner, for example, if object was cleared by another player before
-	bool wasVisitedBefore(const CGHeroInstance * contextHero) const;
+	bool wasVisitedBefore(const CGHeroInstance * contextHero) const override;
 	
 	void serializeJsonOptions(JsonSerializeFormat & handler) override;
 	
-	virtual void grantRewardWithMessage(const CGHeroInstance * contextHero, int rewardIndex, bool markAsVisit) const;
-	virtual void selectRewardWithMessage(const CGHeroInstance * contextHero, const std::vector<ui32> & rewardIndices, const MetaString & dialog) const;
-
-	virtual void grantAllRewardsWithMessage(const CGHeroInstance * contextHero, const std::vector<ui32>& rewardIndices, bool markAsVisit) const;
-
-	std::vector<Component> loadComponents(const CGHeroInstance * contextHero, const std::vector<ui32> & rewardIndices) const;
-
 	std::string getDisplayTextImpl(PlayerColor player, const CGHeroInstance * hero, bool includeDescription) const;
 	std::string getDescriptionMessage(PlayerColor player, const CGHeroInstance * hero) const;
 	std::vector<Component> getPopupComponentsImpl(PlayerColor player, const CGHeroInstance * hero) const;
 
-	void doHeroVisit(const CGHeroInstance *h) const;
-
-	/// Returns true if this object might have guards present, whether they were cleared or not
-	bool guardedPotentially() const;
 	/// Returns true if this object is currently guarded
-	bool guardedPresently() const;
+	bool isGuarded() const;
 public:
 
 	/// Visitability checks. Note that hero check includes check for hero owner (returns true if object was visited by player)

+ 37 - 0
lib/mapObjects/CompoundMapObjectID.h

@@ -0,0 +1,37 @@
+/*
+ * CompoundMapObjectID.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "../constants/EntityIdentifiers.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+struct DLL_LINKAGE CompoundMapObjectID
+{
+	si32 primaryID;
+	si32 secondaryID;
+
+	CompoundMapObjectID(si32 primID, si32 secID) : primaryID(primID), secondaryID(secID) {};
+
+	bool operator<(const CompoundMapObjectID& other) const
+	{
+		if(this->primaryID != other.primaryID)
+			return this->primaryID < other.primaryID;
+		else
+			return this->secondaryID < other.secondaryID;
+	}
+
+	bool operator==(const CompoundMapObjectID& other) const
+	{
+		return (this->primaryID == other.primaryID) && (this->secondaryID == other.secondaryID);
+	}
+};
+
+VCMI_LIB_NAMESPACE_END

+ 3 - 4
lib/mapObjects/MiscObjects.cpp

@@ -772,9 +772,8 @@ void CGArtifact::initObj(vstd::RNG & rand)
 	{
 		if (!storedArtifact)
 		{
-			auto * a = new CArtifactInstance();
-			cb->gameState()->map->addNewArtifactInstance(a);
-			storedArtifact = a;
+			storedArtifact = ArtifactUtils::createArtifact(ArtifactID());
+			cb->gameState()->map->addNewArtifactInstance(storedArtifact);
 		}
 		if(!storedArtifact->artType)
 			storedArtifact->setType(getArtifact().toArtifact());
@@ -901,7 +900,7 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const
 
 void CGArtifact::pick(const CGHeroInstance * h) const
 {
-	if(cb->putArtifact(ArtifactLocation(h->id, ArtifactPosition::FIRST_AVAILABLE), storedArtifact))
+	if(cb->putArtifact(ArtifactLocation(h->id, ArtifactPosition::FIRST_AVAILABLE), storedArtifact->getId()))
 		cb->removeObject(this, h->getOwner());
 }
 

+ 5 - 0
lib/mapObjects/ObjectTemplate.cpp

@@ -508,6 +508,11 @@ bool ObjectTemplate::canBePlacedAt(TerrainId terrainID) const
 	return vstd::contains(allowedTerrains, terrainID);
 }
 
+CompoundMapObjectID ObjectTemplate::getCompoundID() const
+{
+	return CompoundMapObjectID(id, subid);
+}
+
 void ObjectTemplate::recalculate()
 {
 	calculateWidth();

+ 4 - 0
lib/mapObjects/ObjectTemplate.h

@@ -13,6 +13,7 @@
 #include "../int3.h"
 #include "../filesystem/ResourcePath.h"
 #include "../serializer/Serializeable.h"
+#include "../mapObjects/CompoundMapObjectID.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -46,6 +47,7 @@ public:
 	/// H3 ID/subID of this object
 	MapObjectID id;
 	MapObjectSubID subid;
+
 	/// print priority, objects with higher priority will be print first, below everything else
 	si32 printPriority;
 	/// animation file that should be used to display object
@@ -122,6 +124,8 @@ public:
 	// Checks if object can be placed on specific terrain
 	bool canBePlacedAt(TerrainId terrain) const;
 
+	CompoundMapObjectID getCompoundID() const;
+
 	ObjectTemplate();
 
 	void readTxt(CLegacyConfigParser & parser);

+ 33 - 90
lib/mapObjects/TownBuildingInstance.cpp

@@ -12,14 +12,10 @@
 #include "TownBuildingInstance.h"
 
 #include "CGTownInstance.h"
-#include "../texts/CGeneralTextHandler.h"
 #include "../IGameCallback.h"
-#include "../gameState/CGameState.h"
 #include "../mapObjects/CGHeroInstance.h"
-#include "../networkPacks/PacksForClient.h"
 #include "../entities/building/CBuilding.h"
 
-
 #include <vstd/RNG.h>
 
 VCMI_LIB_NAMESPACE_BEGIN
@@ -130,7 +126,7 @@ void TownRewardableBuildingInstance::setProperty(ObjProperty what, ObjPropertyID
 
 void TownRewardableBuildingInstance::heroLevelUpDone(const CGHeroInstance *hero) const
 {
-	grantRewardAfterLevelup(cb, configuration.info.at(selectedReward), town, hero);
+	grantRewardAfterLevelup(configuration.info.at(selectedReward), town, hero);
 }
 
 void TownRewardableBuildingInstance::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
@@ -154,14 +150,12 @@ void TownRewardableBuildingInstance::blockingDialogAnswered(const CGHeroInstance
 
 void TownRewardableBuildingInstance::grantReward(ui32 rewardID, const CGHeroInstance * hero) const
 {
-	town->addHeroToStructureVisitors(hero, getBuildingType());
-	
-	grantRewardBeforeLevelup(cb, configuration.info.at(rewardID), hero);
+	grantRewardBeforeLevelup(configuration.info.at(rewardID), hero);
 	
 	// hero is not blocked by levelup dialog - grant remainder immediately
 	if(!cb->isVisitCoveredByAnotherQuery(town, hero))
 	{
-		grantRewardAfterLevelup(cb, configuration.info.at(rewardID), town, hero);
+		grantRewardAfterLevelup(configuration.info.at(rewardID), town, hero);
 	}
 }
 
@@ -196,93 +190,42 @@ bool TownRewardableBuildingInstance::wasVisitedBefore(const CGHeroInstance * con
 
 void TownRewardableBuildingInstance::onHeroVisit(const CGHeroInstance *h) const
 {
-	auto grantRewardWithMessage = [&](int index) -> void
-	{
-		auto vi = configuration.info.at(index);
-		logGlobal->debug("Granting reward %d. Message says: %s", index, vi.message.toString());
-		
-		town->addHeroToStructureVisitors(h, getBuildingType()); //adding to visitors
-
-		InfoWindow iw;
-		iw.player = h->tempOwner;
-		iw.text = vi.message;
-		vi.reward.loadComponents(iw.components, h);
-		iw.type = EInfoWindowMode::MODAL;
-		if(!iw.components.empty() || !iw.text.toString().empty())
-			cb->showInfoDialog(&iw);
-		
-		grantReward(index, h);
-	};
-	auto selectRewardsMessage = [&](const std::vector<ui32> & rewards, const MetaString & dialog) -> void
-	{
-		BlockingDialog sd(configuration.canRefuse, rewards.size() > 1);
-		sd.player = h->tempOwner;
-		sd.text = dialog;
+	assert(town->hasBuilt(getBuildingType()));
 
-		if (rewards.size() > 1)
-			for (auto index : rewards)
-				sd.components.push_back(configuration.info.at(index).reward.getDisplayedComponent(h));
-
-		if (rewards.size() == 1)
-			configuration.info.at(rewards.front()).reward.loadComponents(sd.components, h);
+	if(town->hasBuilt(getBuildingType()))
+		doHeroVisit(h);
+}
 
-		cb->showBlockingDialog(this, &sd);
-	};
-	
-	if(!town->hasBuilt(getBuildingType()))
-		return;
+const IObjectInterface * TownRewardableBuildingInstance::getObject() const
+{
+	return this;
+}
 
-	if(!wasVisitedBefore(h))
+bool TownRewardableBuildingInstance::wasVisited(PlayerColor player) const
+{
+	switch (configuration.visitMode)
 	{
-		auto rewards = getAvailableRewards(h, Rewardable::EEventType::EVENT_FIRST_VISIT);
-
-		logGlobal->debug("Visiting object with %d possible rewards", rewards.size());
-		switch (rewards.size())
-		{
-			case 0: // no available rewards, e.g. visiting School of War without gold
-			{
-				auto emptyRewards = getAvailableRewards(h, Rewardable::EEventType::EVENT_NOT_AVAILABLE);
-				if (!emptyRewards.empty())
-					grantRewardWithMessage(emptyRewards[0]);
-				else
-					logMod->warn("No applicable message for visiting empty object!");
-				break;
-			}
-			case 1: // one reward. Just give it with message
-			{
-				if (configuration.canRefuse)
-					selectRewardsMessage(rewards, configuration.info.at(rewards.front()).message);
-				else
-					grantRewardWithMessage(rewards.front());
-				break;
-			}
-			default: // multiple rewards. Act according to select mode
-			{
-				switch (configuration.selectMode) {
-					case Rewardable::SELECT_PLAYER: // player must select
-						selectRewardsMessage(rewards, configuration.onSelect);
-						break;
-					case Rewardable::SELECT_FIRST: // give first available
-						grantRewardWithMessage(rewards.front());
-						break;
-					case Rewardable::SELECT_RANDOM: // give random
-						grantRewardWithMessage(*RandomGeneratorUtil::nextItem(rewards, cb->gameState()->getRandomGenerator()));
-						break;
-				}
-				break;
-			}
-		}
+		case Rewardable::VISIT_UNLIMITED:
+		case Rewardable::VISIT_BONUS:
+		case Rewardable::VISIT_HERO:
+		case Rewardable::VISIT_LIMITER:
+			return false;
+		case Rewardable::VISIT_ONCE:
+		case Rewardable::VISIT_PLAYER:
+			return !visitors.empty();
+		default:
+			return false;
 	}
-	else
-	{
-		logGlobal->debug("Revisiting already visited object");
+}
 
-		auto visitedRewards = getAvailableRewards(h, Rewardable::EEventType::EVENT_ALREADY_VISITED);
-		if (!visitedRewards.empty())
-			grantRewardWithMessage(visitedRewards[0]);
-		else
-			logMod->debug("No applicable message for visiting already visited object!");
-	}
+void TownRewardableBuildingInstance::markAsVisited(const CGHeroInstance * hero) const
+{
+	town->addHeroToStructureVisitors(hero, getBuildingType());
+}
+
+void TownRewardableBuildingInstance::markAsScouted(const CGHeroInstance * hero) const
+{
+	// no-op - town building is always 'scouted' by owner
 }
 
 

+ 6 - 2
lib/mapObjects/TownBuildingInstance.h

@@ -63,10 +63,14 @@ class DLL_LINKAGE TownRewardableBuildingInstance : public TownBuildingInstance,
 	ui16 selectedReward = 0;
 	std::set<ObjectInstanceID> visitors;
 
-	bool wasVisitedBefore(const CGHeroInstance * contextHero) const;
-	void grantReward(ui32 rewardID, const CGHeroInstance * hero) const;
+	bool wasVisitedBefore(const CGHeroInstance * contextHero) const override;
+	void grantReward(ui32 rewardID, const CGHeroInstance * hero) const override;
 	Rewardable::Configuration generateConfiguration(vstd::RNG & rand) const;
 
+	const IObjectInterface * getObject() const override;
+	bool wasVisited(PlayerColor player) const override;
+	void markAsVisited(const CGHeroInstance * hero) const override;
+	void markAsScouted(const CGHeroInstance * hero) const override;
 public:
 	void setProperty(ObjProperty what, ObjPropertyID identifier) override;
 	void onHeroVisit(const CGHeroInstance * h) const override;

+ 28 - 0
lib/mapping/CMap.cpp

@@ -552,6 +552,34 @@ void CMap::eraseArtifactInstance(CArtifactInstance * art)
 	artInstances[art->getId().getNum()].dellNull();
 }
 
+void CMap::moveArtifactInstance(
+	CArtifactSet & srcSet, const ArtifactPosition & srcSlot,
+	CArtifactSet & dstSet, const ArtifactPosition & dstSlot)
+{
+	auto art = srcSet.getArt(srcSlot);
+	removeArtifactInstance(srcSet, srcSlot);
+	putArtifactInstance(dstSet, art, dstSlot);
+}
+
+void CMap::putArtifactInstance(CArtifactSet & set, CArtifactInstance * art, const ArtifactPosition & slot)
+{
+	art->addPlacementMap(set.putArtifact(slot, art));
+}
+
+void CMap::removeArtifactInstance(CArtifactSet & set, const ArtifactPosition & slot)
+{
+	auto art = set.getArt(slot);
+	assert(art);
+	set.removeArtifact(slot);
+	CArtifactSet::ArtPlacementMap partsMap;
+	for(auto & part : art->getPartsInfo())
+	{
+		if(part.slot != ArtifactPosition::PRE_FIRST)
+			partsMap.try_emplace(part.art, ArtifactPosition::PRE_FIRST);
+	}
+	art->addPlacementMap(partsMap);
+}
+
 void CMap::addNewQuestInstance(CQuest* quest)
 {
 	quest->qid = static_cast<si32>(quests.size());

+ 3 - 0
lib/mapping/CMap.h

@@ -110,6 +110,9 @@ public:
 	void addNewArtifactInstance(CArtifactSet & artSet);
 	void addNewArtifactInstance(ConstTransitivePtr<CArtifactInstance> art);
 	void eraseArtifactInstance(CArtifactInstance * art);
+	void moveArtifactInstance(CArtifactSet & srcSet, const ArtifactPosition & srcSlot, CArtifactSet & dstSet, const ArtifactPosition & dstSlot);
+	void putArtifactInstance(CArtifactSet & set, CArtifactInstance * art, const ArtifactPosition & slot);
+	void removeArtifactInstance(CArtifactSet & set, const ArtifactPosition & slot);
 
 	void addNewQuestInstance(CQuest * quest);
 	void removeQuestInstance(CQuest * quest);

+ 2 - 2
lib/mapping/CMapHeader.cpp

@@ -189,7 +189,7 @@ void CMapHeader::registerMapStrings()
 		JsonUtils::mergeCopy(data, translations[language]);
 	
 	for(auto & s : data.Struct())
-		texts.registerString("map", TextIdentifier(s.first), s.second.String(), language);
+		texts.registerString("map", TextIdentifier(s.first), s.second.String());
 }
 
 std::string mapRegisterLocalizedString(const std::string & modContext, CMapHeader & mapHeader, const TextIdentifier & UID, const std::string & localized)
@@ -199,7 +199,7 @@ std::string mapRegisterLocalizedString(const std::string & modContext, CMapHeade
 
 std::string mapRegisterLocalizedString(const std::string & modContext, CMapHeader & mapHeader, const TextIdentifier & UID, const std::string & localized, const std::string & language)
 {
-	mapHeader.texts.registerString(modContext, UID, localized, language);
+	mapHeader.texts.registerString(modContext, UID, localized);
 	mapHeader.translations.Struct()[language].Struct()[UID.get()].String() = localized;
 	return UID.get();
 }

+ 2 - 2
lib/mapping/MapFormatH3M.cpp

@@ -917,7 +917,7 @@ void CMapLoaderH3M::loadArtifactsOfHero(CGHeroInstance * hero)
 
 		hero->artifactsInBackpack.clear();
 		while(!hero->artifactsWorn.empty())
-			hero->eraseArtSlot(hero->artifactsWorn.begin()->first);
+			hero->removeArtifact(hero->artifactsWorn.begin()->first);
 	}
 
 	for(int i = 0; i < features.artifactSlotsCount; i++)
@@ -959,7 +959,7 @@ bool CMapLoaderH3M::loadArtifactToSlot(CGHeroInstance * hero, int slot)
 	if(ArtifactID(artifactID).toArtifact()->canBePutAt(hero, ArtifactPosition(slot)))
 	{
 		auto * artifact = ArtifactUtils::createArtifact(artifactID);
-		artifact->putAt(*hero, ArtifactPosition(slot));
+		map->putArtifactInstance(*hero, artifact, slot);
 		map->addNewArtifactInstance(artifact);
 	}
 	else

+ 5 - 41
lib/modding/CModHandler.cpp

@@ -384,7 +384,7 @@ std::set<TModID> CModHandler::getModDependencies(const TModID & modId, bool & is
 
 void CModHandler::initializeConfig()
 {
-	VLC->settingsHandler->loadBase(coreMod->config["settings"]);
+	VLC->settingsHandler->loadBase(JsonUtils::assembleFromFiles(coreMod->config["settings"]));
 
 	for(const TModID & modName : activeMods)
 	{
@@ -401,33 +401,6 @@ CModVersion CModHandler::getModVersion(TModID modName) const
 	return {};
 }
 
-bool CModHandler::validateTranslations(TModID modName) const
-{
-	bool result = true;
-	const auto & mod = allMods.at(modName);
-
-	{
-		auto fileList = mod.config["translations"].convertTo<std::vector<std::string> >();
-		JsonNode json = JsonUtils::assembleFromFiles(fileList);
-		result |= VLC->generaltexth->validateTranslation(mod.baseLanguage, modName, json);
-	}
-
-	for(const auto & language : Languages::getLanguageList())
-	{
-		if (mod.config[language.identifier].isNull())
-			continue;
-
-		if (mod.config[language.identifier]["skipValidation"].Bool())
-			continue;
-
-		auto fileList = mod.config[language.identifier]["translations"].convertTo<std::vector<std::string> >();
-		JsonNode json = JsonUtils::assembleFromFiles(fileList);
-		result |= VLC->generaltexth->validateTranslation(language.identifier, modName, json);
-	}
-
-	return result;
-}
-
 void CModHandler::loadTranslation(const TModID & modName)
 {
 	const auto & mod = allMods[modName];
@@ -435,14 +408,11 @@ void CModHandler::loadTranslation(const TModID & modName)
 	std::string preferredLanguage = VLC->generaltexth->getPreferredLanguage();
 	std::string modBaseLanguage = allMods[modName].baseLanguage;
 
-	auto baseTranslationList = mod.config["translations"].convertTo<std::vector<std::string> >();
-	auto extraTranslationList = mod.config[preferredLanguage]["translations"].convertTo<std::vector<std::string> >();
+	JsonNode baseTranslation = JsonUtils::assembleFromFiles(mod.config["translations"]);
+	JsonNode extraTranslation = JsonUtils::assembleFromFiles(mod.config[preferredLanguage]["translations"]);
 
-	JsonNode baseTranslation = JsonUtils::assembleFromFiles(baseTranslationList);
-	JsonNode extraTranslation = JsonUtils::assembleFromFiles(extraTranslationList);
-
-	VLC->generaltexth->loadTranslationOverrides(modBaseLanguage, modName, baseTranslation);
-	VLC->generaltexth->loadTranslationOverrides(preferredLanguage, modName, extraTranslation);
+	VLC->generaltexth->loadTranslationOverrides(modName, baseTranslation);
+	VLC->generaltexth->loadTranslationOverrides(modName, extraTranslation);
 }
 
 void CModHandler::load()
@@ -480,12 +450,6 @@ void CModHandler::load()
 	for(const TModID & modName : activeMods)
 		loadTranslation(modName);
 
-#if 0
-	for(const TModID & modName : activeMods)
-		if (!validateTranslations(modName))
-			allMods[modName].validation = CModInfo::FAILED;
-#endif
-
 	logMod->info("\tLoading mod data: %d ms", timer.getDiff());
 	VLC->creh->loadCrExpMod();
 	VLC->identifiersHandler->finalize();

+ 0 - 2
lib/modding/CModHandler.h

@@ -49,8 +49,6 @@ class DLL_LINKAGE CModHandler final : boost::noncopyable
 	void loadOneMod(std::string modName, const std::string & parent, const JsonNode & modSettings, bool enableMods);
 	void loadTranslation(const TModID & modName);
 
-	bool validateTranslations(TModID modName) const;
-
 	CModVersion getModVersion(TModID modName) const;
 
 public:

+ 2 - 2
lib/modding/ContentTypeHandler.cpp

@@ -50,7 +50,7 @@ ContentTypeHandler::ContentTypeHandler(IHandlerBase * handler, const std::string
 	}
 }
 
-bool ContentTypeHandler::preloadModData(const std::string & modName, const std::vector<std::string> & fileList, bool validate)
+bool ContentTypeHandler::preloadModData(const std::string & modName, const JsonNode & fileList, bool validate)
 {
 	bool result = false;
 	JsonNode data = JsonUtils::assembleFromFiles(fileList, result);
@@ -216,7 +216,7 @@ bool CContentHandler::preloadModData(const std::string & modName, JsonNode modCo
 	bool result = true;
 	for(auto & handler : handlers)
 	{
-		result &= handler.second.preloadModData(modName, modConfig[handler.first].convertTo<std::vector<std::string> >(), validate);
+		result &= handler.second.preloadModData(modName, modConfig[handler.first], validate);
 	}
 	return result;
 }

+ 1 - 1
lib/modding/ContentTypeHandler.h

@@ -39,7 +39,7 @@ public:
 
 	/// local version of methods in ContentHandler
 	/// returns true if loading was successful
-	bool preloadModData(const std::string & modName, const std::vector<std::string> & fileList, bool validate);
+	bool preloadModData(const std::string & modName, const JsonNode & fileList, bool validate);
 	bool loadMod(const std::string & modName, bool validate);
 	void loadCustom();
 	void afterLoadFinalization();

+ 18 - 26
lib/networkPacks/NetPacksLib.cpp

@@ -1490,8 +1490,7 @@ void NewArtifact::applyGs(CGameState *gs)
 {
 	auto art = ArtifactUtils::createArtifact(artId, spellId);
 	gs->map->addNewArtifactInstance(art);
-	PutArtifact pa(ArtifactLocation(artHolder, pos), false);
-	pa.art = art;
+	PutArtifact pa(art->getId(), ArtifactLocation(artHolder, pos), false);
 	pa.applyGs(gs);
 }
 
@@ -1610,14 +1609,14 @@ void RebalanceStacks::applyGs(CGameState *gs)
 			const auto dstHero = dynamic_cast<CGHeroInstance*>(dst.army.get());
 			auto srcStack = const_cast<CStackInstance*>(src.getStack());
 			auto dstStack = const_cast<CStackInstance*>(dst.getStack());
-			if(auto srcArt = srcStack->getArt(ArtifactPosition::CREATURE_SLOT))
+			if(srcStack->getArt(ArtifactPosition::CREATURE_SLOT))
 			{
 				if(auto dstArt = dstStack->getArt(ArtifactPosition::CREATURE_SLOT))
 				{
 					auto dstSlot = ArtifactUtils::getArtBackpackPosition(srcHero, dstArt->getTypeId());
 					if(srcHero && dstSlot != ArtifactPosition::PRE_FIRST)
 					{
-						dstArt->move(*dstStack, ArtifactPosition::CREATURE_SLOT, *srcHero, dstSlot);
+						gs->map->moveArtifactInstance(*dstStack, ArtifactPosition::CREATURE_SLOT, *srcHero, dstSlot);
 					}
 					//else - artifact can be lost :/
 					else
@@ -1629,12 +1628,12 @@ void RebalanceStacks::applyGs(CGameState *gs)
 						ea.applyGs(gs);
 						logNetwork->warn("Cannot move artifact! No free slots");
 					}
-					srcArt->move(*srcStack, ArtifactPosition::CREATURE_SLOT, *dstStack, ArtifactPosition::CREATURE_SLOT);
+					gs->map->moveArtifactInstance(*srcStack, ArtifactPosition::CREATURE_SLOT, *dstStack, ArtifactPosition::CREATURE_SLOT);
 					//TODO: choose from dialog
 				}
 				else //just move to the other slot before stack gets erased
 				{
-					srcArt->move(*srcStack, ArtifactPosition::CREATURE_SLOT, *dstStack, ArtifactPosition::CREATURE_SLOT);
+					gs->map->moveArtifactInstance(*srcStack, ArtifactPosition::CREATURE_SLOT, *dstStack, ArtifactPosition::CREATURE_SLOT);
 				}
 			}
 			if (stackExp)
@@ -1704,14 +1703,13 @@ void BulkSmartRebalanceStacks::applyGs(CGameState *gs)
 
 void PutArtifact::applyGs(CGameState *gs)
 {
-	// Ensure that artifact has been correctly added via NewArtifact pack
-	assert(vstd::contains(gs->map->artInstances, art));
+	auto art = gs->getArtInstance(id);
 	assert(!art->getParentNodes().empty());
 	auto hero = gs->getHero(al.artHolder);
 	assert(hero);
 	assert(art && art->canBePutAt(hero, al.slot));
 	assert(ArtifactUtils::checkIfSlotValid(*hero, al.slot));
-	art->putAt(*hero, al.slot);
+	gs->map->putArtifactInstance(*hero, art, al.slot);
 }
 
 void BulkEraseArtifacts::applyGs(CGameState *gs)
@@ -1736,7 +1734,7 @@ void BulkEraseArtifacts::applyGs(CGameState *gs)
 			for(auto & slotInfoWorn : artSet->artifactsWorn)
 			{
 				auto art = slotInfoWorn.second.artifact;
-				if(art->isCombined() && art->isPart(slotInfo->getArt()))
+				if(art->isCombined() && art->isPart(slotInfo->artifact))
 				{
 					dis.al.slot = artSet->getArtPos(art);
 					break;
@@ -1750,15 +1748,13 @@ void BulkEraseArtifacts::applyGs(CGameState *gs)
 		{
 			logGlobal->debug("Erasing artifact %s", slotInfo->artifact->artType->getNameTranslated());
 		}
-		auto art = artSet->getArt(slot);
-		assert(art);
-		art->removeFrom(*artSet, slot);
+		gs->map->removeArtifactInstance(*artSet, slot);
 	}
 }
 
 void BulkMoveArtifacts::applyGs(CGameState *gs)
 {
-	const auto bulkArtsRemove = [](std::vector<LinkedSlots> & artsPack, CArtifactSet & artSet)
+	const auto bulkArtsRemove = [gs](std::vector<LinkedSlots> & artsPack, CArtifactSet & artSet)
 	{
 		std::vector<ArtifactPosition> packToRemove;
 		for(const auto & slotsPair : artsPack)
@@ -1769,20 +1765,16 @@ void BulkMoveArtifacts::applyGs(CGameState *gs)
 			});
 
 		for(const auto & slot : packToRemove)
-		{
-			auto * art = artSet.getArt(slot);
-			assert(art);
-			art->removeFrom(artSet, slot);
-		}
+			gs->map->removeArtifactInstance(artSet, slot);
 	};
 
-	const auto bulkArtsPut = [](std::vector<LinkedSlots> & artsPack, CArtifactSet & initArtSet, CArtifactSet & dstArtSet)
+	const auto bulkArtsPut = [gs](std::vector<LinkedSlots> & artsPack, CArtifactSet & initArtSet, CArtifactSet & dstArtSet)
 	{
 		for(const auto & slotsPair : artsPack)
 		{
 			auto * art = initArtSet.getArt(slotsPair.srcPos);
 			assert(art);
-			art->putAt(dstArtSet, slotsPair.dstPos);
+			gs->map->putArtifactInstance(dstArtSet, art, slotsPair.dstPos);
 		}
 	};
 	
@@ -1859,7 +1851,7 @@ void AssembledArtifact::applyGs(CGameState *gs)
 	for(const auto slot : slotsInvolved)
 	{
 		const auto constituentInstance = hero->getArt(slot);
-		constituentInstance->removeFrom(*hero, slot);
+		gs->map->removeArtifactInstance(*hero, slot);
 
 		if(ArtifactUtils::isSlotEquipment(al.slot) && slot != al.slot)
 			combinedArt->addPart(constituentInstance, slot);
@@ -1868,7 +1860,7 @@ void AssembledArtifact::applyGs(CGameState *gs)
 	}
 
 	// Put new combined artifacts
-	combinedArt->putAt(*hero, al.slot);
+	gs->map->putArtifactInstance(*hero, combinedArt, al.slot);
 }
 
 void DisassembledArtifact::applyGs(CGameState *gs)
@@ -1878,14 +1870,14 @@ void DisassembledArtifact::applyGs(CGameState *gs)
 	auto disassembledArt = hero->getArt(al.slot);
 	assert(disassembledArt);
 
-	auto parts = disassembledArt->getPartsInfo();
-	disassembledArt->removeFrom(*hero, al.slot);
+	const auto parts = disassembledArt->getPartsInfo();
+	gs->map->removeArtifactInstance(*hero, al.slot);
 	for(auto & part : parts)
 	{
 		// ArtifactPosition::PRE_FIRST is value of main part slot -> it'll replace combined artifact in its pos
 		auto slot = (ArtifactUtils::isSlotEquipment(part.slot) ? part.slot : al.slot);
 		disassembledArt->detachFrom(*part.art);
-		part.art->putAt(*hero, slot);
+		gs->map->putArtifactInstance(*hero, part.art, slot);
 	}
 	gs->map->eraseArtifactInstance(disassembledArt);
 }

+ 4 - 4
lib/networkPacks/PacksForClient.h

@@ -985,14 +985,14 @@ struct DLL_LINKAGE CArtifactOperationPack : CPackForClient
 struct DLL_LINKAGE PutArtifact : CArtifactOperationPack
 {
 	PutArtifact() = default;
-	explicit PutArtifact(const ArtifactLocation & dst, bool askAssemble = true)
-		: al(dst), askAssemble(askAssemble)
+	explicit PutArtifact(const ArtifactInstanceID & id, const ArtifactLocation & dst, bool askAssemble = true)
+		: al(dst), askAssemble(askAssemble), id(id)
 	{
 	}
 
 	ArtifactLocation al;
 	bool askAssemble;
-	ConstTransitivePtr<CArtifactInstance> art;
+	ArtifactInstanceID id;
 
 	void applyGs(CGameState * gs) override;
 	void visitTyped(ICPackVisitor & visitor) override;
@@ -1001,7 +1001,7 @@ struct DLL_LINKAGE PutArtifact : CArtifactOperationPack
 	{
 		h & al;
 		h & askAssemble;
-		h & art;
+		h & id;
 	}
 };
 

+ 6 - 1
lib/rewardable/Info.cpp

@@ -76,7 +76,7 @@ void Rewardable::Info::init(const JsonNode & objectConfig, const std::string & o
 
 	auto loadString = [&](const JsonNode & entry, const TextIdentifier & textID){
 		if (entry.isString() && !entry.String().empty() && entry.String()[0] != '@')
-			VLC->generaltexth->registerString(entry.getModScope(), textID, entry.String());
+			VLC->generaltexth->registerString(entry.getModScope(), textID, entry);
 	};
 
 	parameters = objectConfig;
@@ -526,6 +526,11 @@ bool Rewardable::Info::givesBonuses() const
 	return testForKey(parameters, "bonuses");
 }
 
+bool Rewardable::Info::hasGuards() const
+{
+	return testForKey(parameters, "guards");
+}
+
 const JsonNode & Rewardable::Info::getParameters() const
 {
 	return parameters;

+ 2 - 0
lib/rewardable/Info.h

@@ -68,6 +68,8 @@ public:
 
 	bool givesBonuses() const override;
 
+	bool hasGuards() const override;
+
 	void configureObject(Rewardable::Configuration & object, vstd::RNG & rng, IGameCallback * cb) const;
 
 	void init(const JsonNode & objectConfig, const std::string & objectTextID);

+ 152 - 2
lib/rewardable/Interface.cpp

@@ -25,6 +25,8 @@
 #include "../networkPacks/PacksForClient.h"
 #include "../IGameCallback.h"
 
+#include <vstd/RNG.h>
+
 VCMI_LIB_NAMESPACE_BEGIN
 
 std::vector<ui32> Rewardable::Interface::getAvailableRewards(const CGHeroInstance * hero, Rewardable::EEventType event) const
@@ -44,8 +46,10 @@ std::vector<ui32> Rewardable::Interface::getAvailableRewards(const CGHeroInstanc
 	return ret;
 }
 
-void Rewardable::Interface::grantRewardBeforeLevelup(IGameCallback * cb, const Rewardable::VisitInfo & info, const CGHeroInstance * hero) const
+void Rewardable::Interface::grantRewardBeforeLevelup(const Rewardable::VisitInfo & info, const CGHeroInstance * hero) const
 {
+	auto cb = getObject()->cb;
+
 	assert(hero);
 	assert(hero->tempOwner.isValidPlayer());
 	assert(info.reward.creatures.size() <= GameConstants::ARMY_SIZE);
@@ -129,8 +133,10 @@ void Rewardable::Interface::grantRewardBeforeLevelup(IGameCallback * cb, const R
 		cb->giveExperience(hero, expToGive);
 }
 
-void Rewardable::Interface::grantRewardAfterLevelup(IGameCallback * cb, const Rewardable::VisitInfo & info, const CArmedInstance * army, const CGHeroInstance * hero) const
+void Rewardable::Interface::grantRewardAfterLevelup(const Rewardable::VisitInfo & info, const CArmedInstance * army, const CGHeroInstance * hero) const
 {
+	auto cb = getObject()->cb;
+
 	if(info.reward.manaDiff || info.reward.manaPercentage >= 0)
 		cb->setManaPoints(hero->id, info.reward.calculateManaPoints(hero));
 
@@ -216,4 +222,148 @@ void Rewardable::Interface::serializeJson(JsonSerializeFormat & handler)
 	configuration.serializeJson(handler);
 }
 
+void Rewardable::Interface::grantRewardWithMessage(const CGHeroInstance * contextHero, int index, bool markAsVisit) const
+{
+	auto vi = configuration.info.at(index);
+	logGlobal->debug("Granting reward %d. Message says: %s", index, vi.message.toString());
+	// show message only if it is not empty or in infobox
+	if (configuration.infoWindowType != EInfoWindowMode::MODAL || !vi.message.toString().empty())
+	{
+		InfoWindow iw;
+		iw.player = contextHero->tempOwner;
+		iw.text = vi.message;
+		vi.reward.loadComponents(iw.components, contextHero);
+		iw.type = configuration.infoWindowType;
+		if(!iw.components.empty() || !iw.text.toString().empty())
+			getObject()->cb->showInfoDialog(&iw);
+	}
+	// grant reward afterwards. Note that it may remove object
+	if(markAsVisit)
+		markAsVisited(contextHero);
+	grantReward(index, contextHero);
+}
+
+void Rewardable::Interface::selectRewardWithMessage(const CGHeroInstance * contextHero, const std::vector<ui32> & rewardIndices, const MetaString & dialog) const
+{
+	BlockingDialog sd(configuration.canRefuse, rewardIndices.size() > 1);
+	sd.player = contextHero->tempOwner;
+	sd.text = dialog;
+	sd.components = loadComponents(contextHero, rewardIndices);
+	getObject()->cb->showBlockingDialog(getObject(), &sd);
+}
+
+std::vector<Component> Rewardable::Interface::loadComponents(const CGHeroInstance * contextHero, const std::vector<ui32> & rewardIndices) const
+{
+	std::vector<Component> result;
+
+	if (rewardIndices.empty())
+		return result;
+
+	if (configuration.selectMode != Rewardable::SELECT_FIRST && rewardIndices.size() > 1)
+	{
+		for (auto index : rewardIndices)
+			result.push_back(configuration.info.at(index).reward.getDisplayedComponent(contextHero));
+	}
+	else
+	{
+		configuration.info.at(rewardIndices.front()).reward.loadComponents(result, contextHero);
+	}
+
+	return result;
+}
+
+void Rewardable::Interface::grantAllRewardsWithMessage(const CGHeroInstance * contextHero, const std::vector<ui32> & rewardIndices, bool markAsVisit) const
+{
+	if (rewardIndices.empty())
+		return;
+
+	for (auto index : rewardIndices)
+	{
+		// TODO: Merge all rewards of same type, with single message?
+		grantRewardWithMessage(contextHero, index, false);
+	}
+	// Mark visited only after all rewards were processed
+	if(markAsVisit)
+		markAsVisited(contextHero);
+}
+
+void Rewardable::Interface::doHeroVisit(const CGHeroInstance *h) const
+{
+	if(!wasVisitedBefore(h))
+	{
+		auto rewards = getAvailableRewards(h, Rewardable::EEventType::EVENT_FIRST_VISIT);
+		bool objectRemovalPossible = false;
+		for(auto index : rewards)
+		{
+			if(configuration.info.at(index).reward.removeObject)
+				objectRemovalPossible = true;
+		}
+
+		logGlobal->debug("Visiting object with %d possible rewards", rewards.size());
+		switch (rewards.size())
+		{
+			case 0: // no available rewards, e.g. visiting School of War without gold
+			{
+				auto emptyRewards = getAvailableRewards(h, Rewardable::EEventType::EVENT_NOT_AVAILABLE);
+				if (!emptyRewards.empty())
+					grantRewardWithMessage(h, emptyRewards[0], false);
+				else
+					logMod->warn("No applicable message for visiting empty object!");
+				break;
+			}
+			case 1: // one reward. Just give it with message
+			{
+				if (configuration.canRefuse)
+					selectRewardWithMessage(h, rewards, configuration.info.at(rewards.front()).message);
+				else
+					grantRewardWithMessage(h, rewards.front(), true);
+				break;
+			}
+			default: // multiple rewards. Act according to select mode
+			{
+				switch (configuration.selectMode) {
+					case Rewardable::SELECT_PLAYER: // player must select
+						selectRewardWithMessage(h, rewards, configuration.onSelect);
+						break;
+					case Rewardable::SELECT_FIRST: // give first available
+						if (configuration.canRefuse)
+							selectRewardWithMessage(h, { rewards.front() }, configuration.info.at(rewards.front()).message);
+						else
+							grantRewardWithMessage(h, rewards.front(), true);
+						break;
+					case Rewardable::SELECT_RANDOM: // give random
+					{
+						ui32 rewardIndex = *RandomGeneratorUtil::nextItem(rewards, getObject()->cb->getRandomGenerator());
+						if (configuration.canRefuse)
+							selectRewardWithMessage(h, { rewardIndex }, configuration.info.at(rewardIndex).message);
+						else
+							grantRewardWithMessage(h, rewardIndex, true);
+						break;
+					}
+					case Rewardable::SELECT_ALL: // grant all possible
+						grantAllRewardsWithMessage(h, rewards, true);
+						break;
+				}
+				break;
+			}
+		}
+
+		if(!objectRemovalPossible && getAvailableRewards(h, Rewardable::EEventType::EVENT_FIRST_VISIT).empty())
+			markAsScouted(h);
+	}
+	else
+	{
+		logGlobal->debug("Revisiting already visited object");
+
+		if (!wasVisited(h->getOwner()))
+			markAsScouted(h);
+
+		auto visitedRewards = getAvailableRewards(h, Rewardable::EEventType::EVENT_ALREADY_VISITED);
+		if (!visitedRewards.empty())
+			grantRewardWithMessage(h, visitedRewards[0], false);
+		else
+			logMod->warn("No applicable message for visiting already visited object!");
+	}
+}
+
 VCMI_LIB_NAMESPACE_END

+ 16 - 3
lib/rewardable/Interface.h

@@ -15,7 +15,7 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-class IGameCallback;
+class IObjectInterface;
 
 namespace Rewardable
 {
@@ -30,11 +30,24 @@ private:
 protected:
 	
 	/// function that must be called if hero got level-up during grantReward call
-	virtual void grantRewardAfterLevelup(IGameCallback * cb, const Rewardable::VisitInfo & reward, const CArmedInstance * army, const CGHeroInstance * hero) const;
+	void grantRewardAfterLevelup(const Rewardable::VisitInfo & reward, const CArmedInstance * army, const CGHeroInstance * hero) const;
 
 	/// grants reward to hero
-	virtual void grantRewardBeforeLevelup(IGameCallback * cb, const Rewardable::VisitInfo & reward, const CGHeroInstance * hero) const;
+	void grantRewardBeforeLevelup(const Rewardable::VisitInfo & reward, const CGHeroInstance * hero) const;
 	
+	virtual void grantRewardWithMessage(const CGHeroInstance * contextHero, int rewardIndex, bool markAsVisit) const;
+	void selectRewardWithMessage(const CGHeroInstance * contextHero, const std::vector<ui32> & rewardIndices, const MetaString & dialog) const;
+	void grantAllRewardsWithMessage(const CGHeroInstance * contextHero, const std::vector<ui32>& rewardIndices, bool markAsVisit) const;
+	std::vector<Component> loadComponents(const CGHeroInstance * contextHero, const std::vector<ui32> & rewardIndices) const;
+
+	void doHeroVisit(const CGHeroInstance *h) const;
+
+	virtual const IObjectInterface * getObject() const = 0;
+	virtual bool wasVisitedBefore(const CGHeroInstance * hero) const = 0;
+	virtual bool wasVisited(PlayerColor player) const = 0;
+	virtual void markAsVisited(const CGHeroInstance * hero) const = 0;
+	virtual void markAsScouted(const CGHeroInstance * hero) const = 0;
+	virtual void grantReward(ui32 rewardID, const CGHeroInstance * hero) const = 0;
 public:
 
 	/// filters list of visit info and returns rewards that can be granted to current hero

+ 21 - 3
lib/rewardable/Limiter.cpp

@@ -143,10 +143,28 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const
 		for(const auto & elem : artifactsRequirements)
 		{
 			// check required amount of artifacts
-			if(hero->getArtPosCount(elem.first, false, true, true) < elem.second)
+			size_t artCnt = 0;
+			for(const auto & [slot, slotInfo] : hero->artifactsWorn)
+				if(slotInfo.artifact->getTypeId() == elem.first)
+					artCnt++;
+
+			for(auto & slotInfo : hero->artifactsInBackpack)
+				if(slotInfo.artifact->getTypeId() == elem.first)
+				{
+					artCnt++;
+				}
+				else if(slotInfo.artifact->isCombined())
+				{
+					for(const auto & partInfo : slotInfo.artifact->getPartsInfo())
+						if(partInfo.art->getTypeId() == elem.first)
+							artCnt++;
+				}
+
+			if(artCnt < elem.second)
 				return false;
-			if(!hero->hasArt(elem.first))
-				reqSlots += hero->getAssemblyByConstituent(elem.first)->getPartsInfo().size() - 2;
+			// Check if art has no own slot. (As part of combined in backpack)
+			if(hero->getArtPos(elem.first, false) == ArtifactPosition::PRE_FIRST)
+				reqSlots += hero->getCombinedArtWithPart(elem.first)->getPartsInfo().size() - 2;
 		}
 		if(!ArtifactUtils::isBackpackFreeSlots(hero, reqSlots))
 			return false;

+ 81 - 51
lib/rmg/CRmgTemplate.cpp

@@ -102,6 +102,7 @@ void ZoneOptions::CTownInfo::serializeJson(JsonSerializeFormat & handler)
 	handler.serializeInt("castles", castleCount, 0);
 	handler.serializeInt("townDensity", townDensity, 0);
 	handler.serializeInt("castleDensity", castleDensity, 0);
+	handler.serializeInt("sourceZone", sourceZone, NO_ZONE);
 }
 
 ZoneOptions::ZoneOptions():
@@ -156,7 +157,7 @@ std::optional<int> ZoneOptions::getOwner() const
 	return owner;
 }
 
-const std::set<TerrainId> ZoneOptions::getTerrainTypes() const
+std::set<TerrainId> ZoneOptions::getTerrainTypes() const
 {
 	if (terrainTypes.empty())
 	{
@@ -191,7 +192,7 @@ std::set<FactionID> ZoneOptions::getDefaultTownTypes() const
 	return VLC->townh->getDefaultAllowed();
 }
 
-const std::set<FactionID> ZoneOptions::getTownTypes() const
+std::set<FactionID> ZoneOptions::getTownTypes() const
 {
 	if (townTypes.empty())
 	{
@@ -214,7 +215,7 @@ void ZoneOptions::setMonsterTypes(const std::set<FactionID> & value)
 	monsterTypes = value;
 }
 
-const std::set<FactionID> ZoneOptions::getMonsterTypes() const
+std::set<FactionID> ZoneOptions::getMonsterTypes() const
 {
 	return vstd::difference(monsterTypes, bannedMonsters);
 }
@@ -250,7 +251,7 @@ void ZoneOptions::addTreasureInfo(const CTreasureInfo & value)
 	vstd::amax(maxTreasureValue, value.max);
 }
 
-const std::vector<CTreasureInfo> & ZoneOptions::getTreasureInfo() const
+std::vector<CTreasureInfo> ZoneOptions::getTreasureInfo() const
 {
 	return treasureInfo;
 }
@@ -272,7 +273,22 @@ TRmgTemplateZoneId ZoneOptions::getTerrainTypeLikeZone() const
 
 TRmgTemplateZoneId ZoneOptions::getTreasureLikeZone() const
 {
-    return treasureLikeZone;
+	return treasureLikeZone;
+}
+
+ObjectConfig ZoneOptions::getCustomObjects() const
+{
+	return objectConfig;
+}
+
+void ZoneOptions::setCustomObjects(const ObjectConfig & value)
+{
+	objectConfig = value;
+}
+
+TRmgTemplateZoneId ZoneOptions::getCustomObjectsLikeZone() const
+{
+	return customObjectsLikeZone;
 }
 
 void ZoneOptions::addConnection(const ZoneConnection & connection)
@@ -334,6 +350,7 @@ void ZoneOptions::serializeJson(JsonSerializeFormat & handler)
 	SERIALIZE_ZONE_LINK(minesLikeZone);
 	SERIALIZE_ZONE_LINK(terrainTypeLikeZone);
 	SERIALIZE_ZONE_LINK(treasureLikeZone);
+	SERIALIZE_ZONE_LINK(customObjectsLikeZone);
 
 	#undef SERIALIZE_ZONE_LINK
 
@@ -398,6 +415,8 @@ void ZoneOptions::serializeJson(JsonSerializeFormat & handler)
 			handler.serializeInt(GameConstants::RESOURCE_NAMES[idx], mines[idx], 0);
 		}
 	}
+
+	handler.serializeStruct("customObjects", objectConfig);
 }
 
 ZoneConnection::ZoneConnection():
@@ -759,53 +778,29 @@ const JsonNode & CRmgTemplate::getMapSettings() const
 	return *mapSettings;
 }
 
-std::set<TerrainId> CRmgTemplate::inheritTerrainType(std::shared_ptr<ZoneOptions> zone, uint32_t iteration /* = 0 */)
+template<typename T>
+T CRmgTemplate::inheritZoneProperty(std::shared_ptr<rmg::ZoneOptions> zone, 
+									T (rmg::ZoneOptions::*getter)() const,
+									void (rmg::ZoneOptions::*setter)(const T&),
+									TRmgTemplateZoneId (rmg::ZoneOptions::*inheritFrom)() const,
+									const std::string& propertyString,
+									uint32_t iteration)
 {
 	if (iteration >= 50)
 	{
-		logGlobal->error("Infinite recursion for terrain types detected in template %s", name);
-		return std::set<TerrainId>();
+		logGlobal->error("Infinite recursion for %s detected in template %s", propertyString, name);
+		return T();
 	}
-	if (zone->getTerrainTypeLikeZone() != ZoneOptions::NO_ZONE)
-	{
-		iteration++;
-		const auto otherZone = zones.at(zone->getTerrainTypeLikeZone());
-		zone->setTerrainTypes(inheritTerrainType(otherZone, iteration));
-	}
-	//This implicitly excludes banned terrains
-	return zone->getTerrainTypes();
-}
-
-std::map<TResource, ui16> CRmgTemplate::inheritMineTypes(std::shared_ptr<ZoneOptions> zone, uint32_t iteration /* = 0 */)
-{
-	if (iteration >= 50)
-	{
-		logGlobal->error("Infinite recursion for mine types detected in template %s", name);
-		return std::map<TResource, ui16>();
-	}
-	if (zone->getMinesLikeZone() != ZoneOptions::NO_ZONE)
-	{
-		iteration++;
-		const auto otherZone = zones.at(zone->getMinesLikeZone());
-		zone->setMinesInfo(inheritMineTypes(otherZone, iteration));
-	}
-	return zone->getMinesInfo();
-}
-
-std::vector<CTreasureInfo> CRmgTemplate::inheritTreasureInfo(std::shared_ptr<ZoneOptions> zone, uint32_t iteration /* = 0 */)
-{
-	if (iteration >= 50)
-	{
-		logGlobal->error("Infinite recursion for treasures detected in template %s", name);
-		return std::vector<CTreasureInfo>();
-	}
-	if (zone->getTreasureLikeZone() != ZoneOptions::NO_ZONE)
+	
+	if (((*zone).*inheritFrom)() != rmg::ZoneOptions::NO_ZONE)
 	{
 		iteration++;
-		const auto otherZone = zones.at(zone->getTreasureLikeZone());
-		zone->setTreasureInfo(inheritTreasureInfo(otherZone, iteration));
+		const auto otherZone = zones.at(((*zone).*inheritFrom)());
+		T inheritedValue = inheritZoneProperty(otherZone, getter, setter, inheritFrom, propertyString, iteration);
+		((*zone).*setter)(inheritedValue);
 	}
-	return zone->getTreasureInfo();
+	
+	return ((*zone).*getter)();
 }
 
 void CRmgTemplate::afterLoad()
@@ -814,12 +809,32 @@ void CRmgTemplate::afterLoad()
 	{
 		auto zone = idAndZone.second;
 
-		//Inherit properties recursively.
-		inheritTerrainType(zone);
-		inheritMineTypes(zone);
-		inheritTreasureInfo(zone);
-
-		//TODO: Inherit monster types as well
+		// Inherit properties recursively
+		inheritZoneProperty(zone, 
+							&rmg::ZoneOptions::getTerrainTypes, 
+							&rmg::ZoneOptions::setTerrainTypes, 
+							&rmg::ZoneOptions::getTerrainTypeLikeZone,
+							"terrain types");
+		
+		inheritZoneProperty(zone, 
+							&rmg::ZoneOptions::getMinesInfo, 
+							&rmg::ZoneOptions::setMinesInfo, 
+							&rmg::ZoneOptions::getMinesLikeZone,
+							"mine types");
+		
+		inheritZoneProperty(zone, 
+							&rmg::ZoneOptions::getTreasureInfo, 
+							&rmg::ZoneOptions::setTreasureInfo, 
+							&rmg::ZoneOptions::getTreasureLikeZone,
+							"treasure info");
+
+		inheritZoneProperty(zone, 
+							&rmg::ZoneOptions::getCustomObjects, 
+							&rmg::ZoneOptions::setCustomObjects, 
+							&rmg::ZoneOptions::getCustomObjectsLikeZone,
+							"custom objects");
+
+				//TODO: Inherit monster types as well
 		auto monsterTypes = zone->getMonsterTypes();
 		if (monsterTypes.empty())
 		{
@@ -848,6 +863,7 @@ void CRmgTemplate::afterLoad()
 	allowedWaterContent.erase(EWaterContent::RANDOM);
 }
 
+// TODO: Allow any integer size which does not match enum, as well
 void CRmgTemplate::serializeSize(JsonSerializeFormat & handler, int3 & value, const std::string & fieldName)
 {
 	static const std::map<std::string, int3> sizeMapping =
@@ -916,5 +932,19 @@ void CRmgTemplate::serializePlayers(JsonSerializeFormat & handler, CPlayerCountR
 		value.fromString(encodedValue);
 }
 
+const std::vector<CompoundMapObjectID> & ZoneOptions::getBannedObjects() const
+{
+	return objectConfig.getBannedObjects();
+}
+
+const std::vector<ObjectConfig::EObjectCategory> & ZoneOptions::getBannedObjectCategories() const
+{
+	return objectConfig.getBannedObjectCategories();
+}
+
+const std::vector<ObjectInfo> & ZoneOptions::getConfiguredObjects() const
+{
+	return objectConfig.getConfiguredObjects();
+}
 
 VCMI_LIB_NAMESPACE_END

+ 38 - 5
lib/rmg/CRmgTemplate.h

@@ -13,10 +13,14 @@
 #include "../int3.h"
 #include "../GameConstants.h"
 #include "../ResourceSet.h"
+#include "ObjectInfo.h"
+#include "ObjectConfig.h"
+#include "../mapObjectConstructors/CObjectClassesHandler.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
 class JsonSerializeFormat;
+struct CompoundMapObjectID;
 
 enum class ETemplateZoneType
 {
@@ -132,6 +136,9 @@ public:
 		int castleCount;
 		int townDensity;
 		int castleDensity;
+
+		// TODO: Copy from another zone once its randomized
+		TRmgTemplateZoneId sourceZone = NO_ZONE;
 	};
 
 	ZoneOptions();
@@ -146,15 +153,15 @@ public:
 	void setSize(int value);
 	std::optional<int> getOwner() const;
 
-	const std::set<TerrainId> getTerrainTypes() const;
+	std::set<TerrainId> getTerrainTypes() const;
 	void setTerrainTypes(const std::set<TerrainId> & value);
 	std::set<TerrainId> getDefaultTerrainTypes() const;
 
 	const CTownInfo & getPlayerTowns() const;
 	const CTownInfo & getNeutralTowns() const;
 	std::set<FactionID> getDefaultTownTypes() const;
-	const std::set<FactionID> getTownTypes() const;
-	const std::set<FactionID> getMonsterTypes() const;
+	std::set<FactionID> getTownTypes() const;
+	std::set<FactionID> getMonsterTypes() const;
 
 	void setTownTypes(const std::set<FactionID> & value);
 	void setMonsterTypes(const std::set<FactionID> & value);
@@ -164,7 +171,7 @@ public:
 
 	void setTreasureInfo(const std::vector<CTreasureInfo> & value);
 	void addTreasureInfo(const CTreasureInfo & value);
-	const std::vector<CTreasureInfo> & getTreasureInfo() const;
+	std::vector<CTreasureInfo> getTreasureInfo() const;
 	ui32 getMaxTreasureValue() const;
 	void recalculateMaxTreasureValue();
 
@@ -183,12 +190,24 @@ public:
 	bool areTownsSameType() const;
 	bool isMatchTerrainToTown() const;
 
+	// Get a group of configured objects
+	const std::vector<CompoundMapObjectID> & getBannedObjects() const;
+	const std::vector<ObjectConfig::EObjectCategory> & getBannedObjectCategories() const;
+	const std::vector<ObjectInfo> & getConfiguredObjects() const;
+
+	// Copy whole custom object config from another zone
+	ObjectConfig getCustomObjects() const;
+	void setCustomObjects(const ObjectConfig & value);
+	TRmgTemplateZoneId	getCustomObjectsLikeZone() const;
+
 protected:
 	TRmgTemplateZoneId id;
 	ETemplateZoneType type;
 	int size;
 	ui32 maxTreasureValue;
 	std::optional<int> owner;
+
+	ObjectConfig objectConfig;
 	CTownInfo playerTowns;
 	CTownInfo neutralTowns;
 	bool matchTerrainToTown;
@@ -211,6 +230,7 @@ protected:
 	TRmgTemplateZoneId minesLikeZone;
 	TRmgTemplateZoneId terrainTypeLikeZone;
 	TRmgTemplateZoneId treasureLikeZone;
+	TRmgTemplateZoneId customObjectsLikeZone;
 };
 
 }
@@ -280,8 +300,21 @@ private:
 	std::set<TerrainId> inheritTerrainType(std::shared_ptr<rmg::ZoneOptions> zone, uint32_t iteration = 0);
 	std::map<TResource, ui16> inheritMineTypes(std::shared_ptr<rmg::ZoneOptions> zone, uint32_t iteration = 0);
 	std::vector<CTreasureInfo> inheritTreasureInfo(std::shared_ptr<rmg::ZoneOptions> zone, uint32_t iteration = 0);
+
+	// TODO: Copy custom object settings
+	// TODO: Copy town type after source town is actually randomized
+
 	void serializeSize(JsonSerializeFormat & handler, int3 & value, const std::string & fieldName);
 	void serializePlayers(JsonSerializeFormat & handler, CPlayerCountRange & value, const std::string & fieldName);
+
+	template<typename T>
+	T inheritZoneProperty(std::shared_ptr<rmg::ZoneOptions> zone, 
+						  T (rmg::ZoneOptions::*getter)() const,
+						  void (rmg::ZoneOptions::*setter)(const T&),
+						  TRmgTemplateZoneId (rmg::ZoneOptions::*inheritFrom)() const,
+						  const std::string& propertyString,
+						  uint32_t iteration = 0);
+
 };
 
-VCMI_LIB_NAMESPACE_END
+VCMI_LIB_NAMESPACE_END

Some files were not shown because too many files changed in this diff