소스 검색

Merge branch 'vcmi:develop' into cheats

Laserlicht 5 달 전
부모
커밋
043075a719
99개의 변경된 파일1979개의 추가작업 그리고 1336개의 파일을 삭제
  1. 30 30
      Mods/vcmi/Content/config/english.json
  2. 38 38
      Mods/vcmi/Content/config/ukrainian.json
  3. 0 1
      client/CServerHandler.cpp
  4. 6 8
      client/Client.cpp
  5. 4 7
      client/Client.h
  6. 3 3
      client/ClientCommandManager.cpp
  7. 4 4
      client/ClientNetPackVisitors.h
  8. 64 64
      client/NetPacksClient.cpp
  9. 0 1
      client/NetPacksLobbyClient.cpp
  10. 73 21
      client/windows/CCreatureWindow.cpp
  11. 1 1
      client/windows/CMessage.cpp
  12. 193 193
      config/artifacts.json
  13. 7 7
      config/battlefields.json
  14. 4 8
      config/creatures/dungeon.json
  15. 11 2
      config/schemas/artifact.json
  16. 5 0
      config/schemas/bonusInstance.json
  17. 0 5
      config/spells/timed.json
  18. 32 4
      docs/images/Bonus_System_Nodes.gv
  19. 373 308
      docs/images/Bonus_System_Nodes.svg
  20. 11 8
      docs/modders/Bonus/Bonus_Types.md
  21. 6 2
      docs/modders/Bonus_Format.md
  22. 9 0
      docs/modders/Entities_Format/Artifact_Format.md
  23. 1 0
      launcher/firstLaunch/firstlaunch_moc.cpp
  24. 5 0
      launcher/modManager/modstatecontroller.cpp
  25. 6 1
      lib/CCreatureHandler.cpp
  26. 1 0
      lib/CCreatureHandler.h
  27. 9 0
      lib/CCreatureSet.cpp
  28. 5 2
      lib/CSkillHandler.cpp
  29. 4 4
      lib/battle/BattleLayout.cpp
  30. 2 2
      lib/battle/BattleLayout.h
  31. 4 0
      lib/bonuses/Bonus.h
  32. 0 2
      lib/bonuses/BonusList.h
  33. 2 2
      lib/callback/CGameInfoCallback.cpp
  34. 2 2
      lib/callback/CGameInfoCallback.h
  35. 1 1
      lib/callback/IGameEventCallback.h
  36. 1 0
      lib/constants/EntityIdentifiers.h
  37. 28 3
      lib/entities/artifact/CArtHandler.cpp
  38. 5 2
      lib/entities/artifact/CArtifact.cpp
  39. 3 0
      lib/entities/artifact/CArtifact.h
  40. 2 1
      lib/entities/faction/CTownHandler.cpp
  41. 10 13
      lib/gameState/CGameState.cpp
  42. 10 7
      lib/gameState/CGameState.h
  43. 1 1
      lib/gameState/CGameStateCampaign.cpp
  44. 1 1
      lib/gameState/GameStatePackVisitor.cpp
  45. 16 16
      lib/gameState/GameStatistics.cpp
  46. 4 4
      lib/gameState/GameStatistics.h
  47. 19 5
      lib/json/JsonBonus.cpp
  48. 3 3
      lib/json/JsonBonus.h
  49. 1 1
      lib/mapObjects/CGTownInstance.cpp
  50. 1 1
      lib/mapObjects/CRewardableObject.cpp
  51. 18 2
      lib/mapping/CMap.cpp
  52. 3 2
      lib/mapping/CMap.h
  53. 7 0
      lib/serializer/BinaryDeserializer.h
  54. 2 7
      lib/serializer/Connection.cpp
  55. 1 2
      lib/serializer/Connection.h
  56. 4 2
      lib/serializer/ESerializationVersion.h
  57. 1 0
      lib/serializer/SerializerReflection.h
  58. 1 0
      mapeditor/Animation.h
  59. 2 0
      mapeditor/CMakeLists.txt
  60. 104 0
      mapeditor/PlayerSelectionDialog.cpp
  61. 45 0
      mapeditor/PlayerSelectionDialog.h
  62. 1 1
      mapeditor/campaigneditor/campaigneditor.cpp
  63. 2 2
      mapeditor/campaigneditor/campaigneditor.h
  64. 2 2
      mapeditor/campaigneditor/campaigneditor.ui
  65. 20 9
      mapeditor/mainwindow.cpp
  66. 13 8
      mapeditor/mainwindow.h
  67. 125 50
      mapeditor/mapcontroller.cpp
  68. 24 3
      mapeditor/mapcontroller.h
  69. 7 2
      mapeditor/mapsettings/mapsettings.cpp
  70. 4 0
      mapeditor/mapsettings/mapsettings.h
  71. 14 2
      mapeditor/mapsettings/modsettings.cpp
  72. 2 3
      mapeditor/mapsettings/modsettings.h
  73. 1 1
      mapeditor/mapview.cpp
  74. 1 0
      mapeditor/mapview.h
  75. 127 15
      mapeditor/validator.cpp
  76. 29 0
      mapeditor/validator.h
  77. 1 1
      mapeditor/windownewmap.cpp
  78. 146 143
      server/CGameHandler.cpp
  79. 18 9
      server/CGameHandler.h
  80. 1 1
      server/CVCMIServer.cpp
  81. 3 2
      server/NetPacksLobbyServer.cpp
  82. 14 14
      server/NetPacksServer.cpp
  83. 1 1
      server/ServerSpellCastEnvironment.cpp
  84. 12 12
      server/TurnTimerHandler.cpp
  85. 2 2
      server/battles/BattleActionProcessor.cpp
  86. 6 6
      server/battles/BattleProcessor.cpp
  87. 14 14
      server/battles/BattleResultProcessor.cpp
  88. 12 12
      server/processors/HeroPoolProcessor.cpp
  89. 16 16
      server/processors/NewTurnProcessor.cpp
  90. 6 6
      server/processors/PlayerMessageProcessor.cpp
  91. 21 21
      server/processors/TurnOrderProcessor.cpp
  92. 3 2
      server/queries/MapQueries.cpp
  93. 2 2
      test/CMakeLists.txt
  94. 13 14
      test/game/CGameStateTest.cpp
  95. 0 38
      test/mock/mock_IGameCallback.cpp
  96. 0 114
      test/mock/mock_IGameCallback.h
  97. 30 0
      test/mock/mock_IGameEventCallback.cpp
  98. 76 3
      test/mock/mock_IGameEventCallback.h
  99. 1 1
      test/netpacks/NetPackFixture.cpp

+ 30 - 30
Mods/vcmi/Content/config/english.json

@@ -649,6 +649,8 @@
 	"core.bonus.DEFENSIVE_STANCE.description" : "+${val} Defense when defending",
 	"core.bonus.DESTRUCTION.name" : "Destruction",
 	"core.bonus.DESTRUCTION.description" : "Has ${val}% chance to kill extra units after attack",
+	"core.bonus.DISINTEGRATE.name" : "Disintegrate",
+	"core.bonus.DISINTEGRATE.description" : "No corpse remains after death",
 	"core.bonus.DOUBLE_DAMAGE_CHANCE.name" : "Death Blow",
 	"core.bonus.DOUBLE_DAMAGE_CHANCE.description" : "Has a ${val}% chance of dealing double base damage when attacking",
 	"core.bonus.DRAGON_NATURE.name" : "Dragon",
@@ -689,6 +691,8 @@
 	"core.bonus.HEALER.description" : "Heals allied units",
 	"core.bonus.HP_REGENERATION.name" : "Regeneration",
 	"core.bonus.HP_REGENERATION.description" : "Heals ${val} hit points every round",
+	"core.bonus.INVINCIBLE.name" : "Invincible",
+	"core.bonus.INVINCIBLE.description" : "Cannot be affected by anything",
 	"core.bonus.JOUSTING.name" : "Champion charge",
 	"core.bonus.JOUSTING.description" : "+${val}% damage for each hex travelled",
 	"core.bonus.KING.name" : "King",
@@ -707,6 +711,8 @@
 	"core.bonus.MAGIC_MIRROR.description" : "Has a ${val}% chance to redirect an offensive spell to an enemy unit",
 	"core.bonus.MAGIC_RESISTANCE.name" : "Magic Resistance (${val}%)",
 	"core.bonus.MAGIC_RESISTANCE.description" : "Has a ${val}% chance to resist an enemy spell",
+	"core.bonus.MECHANICAL.name" : "Mechanical",
+	"core.bonus.MECHANICAL.description" : "Immunity to many effects, repairable",
 	"core.bonus.MIND_IMMUNITY.name" : "Mind Spell Immunity",
 	"core.bonus.MIND_IMMUNITY.description" : "Immune to Mind-type spells",
 	"core.bonus.NO_DISTANCE_PENALTY.name" : "No distance penalty",
@@ -719,6 +725,10 @@
 	"core.bonus.NO_WALL_PENALTY.description" : "Full damage during siege",
 	"core.bonus.NON_LIVING.name" : "Non living",
 	"core.bonus.NON_LIVING.description" : "Immunity to many effects",
+	"core.bonus.OPENING_BATTLE_SPELL.name" : "Starts with spell",
+	"core.bonus.OPENING_BATTLE_SPELL.description" : "Casts ${subtype.spell} on battle start",
+	"core.bonus.PRISM_HEX_ATTACK_BREATH.name" : "Prism Breath",
+	"core.bonus.PRISM_HEX_ATTACK_BREATH.description" : "Prism Breath Attack (three directions)",
 	"core.bonus.RANDOM_SPELLCASTER.name" : "Random spellcaster",
 	"core.bonus.RANDOM_SPELLCASTER.description" : "Can cast random spell",
 	"core.bonus.RANGED_RETALIATION.name" : "Ranged retaliation",
@@ -743,10 +753,30 @@
 	"core.bonus.SPELL_AFTER_ATTACK.description" : "Has a ${val}% chance to cast ${subtype.spell} after it attacks",
 	"core.bonus.SPELL_BEFORE_ATTACK.name" : "Cast Before Attack",
 	"core.bonus.SPELL_BEFORE_ATTACK.description" : "Has a ${val}% chance to cast ${subtype.spell} before it attacks",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name" : "Spell Resistance",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.air" : "Air Spells Resistance",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.fire" : "Fire Spells Resistance",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.water" : "Water Spells Resistance",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.earth" : "Earth Spells Resistance",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description" : "Damage from all spells reduced by ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.air" : "Damage from all Air spells reduced by ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.fire" : "Damage from all Fire spells reduced by ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.water" : "Damage from all Water spells reduced by ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.earth" : "Damage from all Earth spells reduced by ${val}%.",
 	"core.bonus.SPELL_IMMUNITY.name" : "Spell immunity",
 	"core.bonus.SPELL_IMMUNITY.description" : "Immune to ${subtype.spell}",
 	"core.bonus.SPELL_LIKE_ATTACK.name" : "Spell-like attack",
 	"core.bonus.SPELL_LIKE_ATTACK.description" : "Attacks with ${subtype.spell}",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name" : "Spell immunity",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.air" : "Air immunity",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.fire" : "Fire immunity",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.water" : "Water immunity",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.earth" : "Earth immunity",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description" : "This unit is immune to all spells",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.air" : "This unit is immune to all Air school spells",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.fire" : "This unit is immune to all Fire school spells",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.water" : "This unit is immune to all Water school spells",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.earth" : "This unit is immune to all Earth school spells",
 	"core.bonus.SPELL_RESISTANCE_AURA.name" : "Aura of Resistance",
 	"core.bonus.SPELL_RESISTANCE_AURA.description" : "Nearby stacks get ${val}% magic resistance",
 	"core.bonus.SUMMON_GUARDIANS.name" : "Summon guardians",
@@ -767,36 +797,6 @@
 	"core.bonus.WATER_IMMUNITY.description" : "Immune to all spells from the school of Water magic",
 	"core.bonus.WIDE_BREATH.name" : "Wide breath",
 	"core.bonus.WIDE_BREATH.description" : "Wide breath attack (multiple hexes)",
-	"core.bonus.DISINTEGRATE.name" : "Disintegrate",
-	"core.bonus.DISINTEGRATE.description" : "No corpse remains after death",
-	"core.bonus.INVINCIBLE.name" : "Invincible",
-	"core.bonus.INVINCIBLE.description" : "Cannot be affected by anything",
-	"core.bonus.MECHANICAL.name" : "Mechanical",
-	"core.bonus.MECHANICAL.description" : "Immunity to many effects, repairable",
-	"core.bonus.PRISM_HEX_ATTACK_BREATH.name" : "Prism Breath",
-	"core.bonus.PRISM_HEX_ATTACK_BREATH.description" : "Prism Breath Attack (three directions)",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.name" : "Spell Resistance",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.name.air" : "Air Spells Resistance",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.name.fire" : "Fire Spells Resistance",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.name.water" : "Water Spells Resistance",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.name.earth" : "Earth Spells Resistance",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.description" : "Damage from all spells reduced by ${val}%.",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.description.air" : "Damage from all Air spells reduced by ${val}%.",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.description.fire" : "Damage from all Fire spells reduced by ${val}%.",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.description.water" : "Damage from all Water spells reduced by ${val}%.",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.description.earth" : "Damage from all Earth spells reduced by ${val}%.",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.name" : "Spell immunity",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.air" : "Air immunity",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.fire" : "Fire immunity",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.water" : "Water immunity",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.earth" : "Earth immunity",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.description" : "This unit is immune to all spells",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.air" : "This unit is immune to all Air school spells",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.fire" : "This unit is immune to all Fire school spells",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.water" : "This unit is immune to all Water school spells",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.earth" : "This unit is immune to all Earth school spells",
-	"core.bonus.OPENING_BATTLE_SPELL.name" : "Starts with spell",
-	"core.bonus.OPENING_BATTLE_SPELL.description" : "Casts ${subtype.spell} on battle start",
 	
 	"spell.core.castleMoat.name" : "Moat",
 	"spell.core.castleMoatTrigger.name" : "Moat",

+ 38 - 38
Mods/vcmi/Content/config/ukrainian.json

@@ -647,6 +647,8 @@
 	"core.bonus.DEFENSIVE_STANCE.description" : "+${val} Захист при обороні",
 	"core.bonus.DESTRUCTION.name" : "Знищення",
 	"core.bonus.DESTRUCTION.description" : "Має ${val}% шанс вбити додаткових юнітів після атаки",
+	"core.bonus.DISINTEGRATE.description" : "Після смерті не залишається трупа",
+	"core.bonus.DISINTEGRATE.name" : "Розпад",
 	"core.bonus.DOUBLE_DAMAGE_CHANCE.name" : "Смертельний удар",
 	"core.bonus.DOUBLE_DAMAGE_CHANCE.description" : "${val}% шанс нанести подвійної шкоди",
 	"core.bonus.DRAGON_NATURE.name" : "Дракон",
@@ -657,6 +659,8 @@
 	"core.bonus.ENCHANTER.description" : "Може використовувати масове закляття ${subtype.spell} кожного ходу",
 	"core.bonus.ENCHANTED.name" : "Зачарований",
 	"core.bonus.ENCHANTED.description" : "Піддається впливу постійних закляття ${subtype.spell}",
+	"core.bonus.ENEMY_ATTACK_REDUCTION.name" : "Ігнорування атаки (${val}%)",
+	"core.bonus.ENEMY_ATTACK_REDUCTION.description" : "При атаці ігнорується ${val}% атаки нападника",
 	"core.bonus.ENEMY_DEFENCE_REDUCTION.name" : "Ігнорує ${val}% захисту",
 	"core.bonus.ENEMY_DEFENCE_REDUCTION.description" : "Ігнорує частину захисту для атаки",
 	"core.bonus.FIRE_IMMUNITY.name" : "Імунітет до вогню",
@@ -669,6 +673,8 @@
 	"core.bonus.FEAR.description" : "Спричиняє страх у загоні ворога",
 	"core.bonus.FEARLESS.name" : "Безстрашний",
 	"core.bonus.FEARLESS.description" : "Імунітет до страху",
+	"core.bonus.FEROCITY.name" : "Лютість",
+	"core.bonus.FEROCITY.description" : "Атакує ${val} більше разів, якщо вбиває когось",
 	"core.bonus.FLYING.name" : "Літає",
 	"core.bonus.FLYING.description" : "Може літати (ігнорує перешкоди)",
 	"core.bonus.FREE_SHOOTING.name" : "Стріляє впритул",
@@ -677,6 +683,8 @@
 	"core.bonus.GARGOYLE.description" : "Не може бути відроджена або зцілена",
 	"core.bonus.GENERAL_DAMAGE_REDUCTION.name" : "Зменшує шкоду (${val}%)",
 	"core.bonus.GENERAL_DAMAGE_REDUCTION.description" : "Зменшує фізичну шкоду від ударів з дальньої та ближньої дистанції",
+	"core.bonus.INVINCIBLE.description" : "На нього ніщо не може вплинути",
+	"core.bonus.INVINCIBLE.name" : "Невразливий",
 	"core.bonus.HATE.name" : "Ненавидить ${subtype.creature}",
 	"core.bonus.HATE.description" : "Завдає на ${val}% більше шкоди",
 	"core.bonus.HEALER.name" : "Цілитель",
@@ -691,6 +699,8 @@
 	"core.bonus.LEVEL_SPELL_IMMUNITY.description" : "Імунітет до заклять рівнів 1-${val}",
 	"core.bonus.LIFE_DRAIN.name" : "Висмоктує життя (${val}%)",
 	"core.bonus.LIFE_DRAIN.description" : "Висмоктує ${val}% від завданої шкоди",
+	"core.bonus.LIMITED_SHOOTING_RANGE.name" : "Обмежена дальність стрільби",
+	"core.bonus.LIMITED_SHOOTING_RANGE.description" : "Не може стріляти по цілях на відстані більше ${val} гексів",
 	"core.bonus.MANA_CHANNELING.name" : "Магічний канал ${val}%",
 	"core.bonus.MANA_CHANNELING.description" : "Повертає вашому герою ману, витрачену ворогом",
 	"core.bonus.MANA_DRAIN.name" : "Викрадання мани",
@@ -699,6 +709,8 @@
 	"core.bonus.MAGIC_MIRROR.description" : "Відбиває ворожі заклинання до випадкової істоти ворога з силою в ${val}%",
 	"core.bonus.MAGIC_RESISTANCE.name" : "Опір магії (${val}%)",
 	"core.bonus.MAGIC_RESISTANCE.description" : "${val}% шанс протистояти ворожим закляттям",
+	"core.bonus.MECHANICAL.description" : "Імунітет до багатьох ефектів, можна ремонтувати",
+	"core.bonus.MECHANICAL.name" : "Механічний",
 	"core.bonus.MIND_IMMUNITY.name" : "Імунітет до заклять розуму",
 	"core.bonus.MIND_IMMUNITY.description" : "Імунітет до заклять типу ",
 	"core.bonus.NO_DISTANCE_PENALTY.name" : "Немає штрафу за відстань",
@@ -711,6 +723,8 @@
 	"core.bonus.NO_WALL_PENALTY.description" : "Повна шкода при пострілах через стіни",
 	"core.bonus.NON_LIVING.name" : "Не жива",
 	"core.bonus.NON_LIVING.description" : "Імунітет до багатьох ефектів",
+	"core.bonus.PRISM_HEX_ATTACK_BREATH.description" : "Атака подихом у трьох напрямах",
+	"core.bonus.PRISM_HEX_ATTACK_BREATH.name" : "Призматична атака",
 	"core.bonus.RANDOM_SPELLCASTER.name" : "Випадковий заклинатель",
 	"core.bonus.RANDOM_SPELLCASTER.description" : "Може накласти випадкове закляття",
 	"core.bonus.RANGED_RETALIATION.name" : "Дистанційна відплата",
@@ -721,6 +735,8 @@
 	"core.bonus.REBIRTH.description" : "${val}% загону відродиться після смерті",
 	"core.bonus.RETURN_AFTER_STRIKE.name" : "Атакує і повертається",
 	"core.bonus.RETURN_AFTER_STRIKE.description" : "Повертається після атаки ближнього бою",
+	"core.bonus.REVENGE.name" : "Помста",
+	"core.bonus.REVENGE.description" : "Завдає додаткової шкоди залежно від втраченого здоров'я в бою",
 	"core.bonus.SHOOTER.name" : "Стрілок",
 	"core.bonus.SHOOTER.description" : "Істота може стріляти",
 	"core.bonus.SHOOTS_ALL_ADJACENT.name" : "Стріляйте по площі",
@@ -729,16 +745,36 @@
 	"core.bonus.SOUL_STEAL.description" : "Отримує ${val} нових істот за кожного вбитого ворога",
 	"core.bonus.SPELLCASTER.name" : "Заклинатель",
 	"core.bonus.SPELLCASTER.description" : "Може використовувати закляття ${subtype.spell}",
-	"core.bonus.SPELL_AFTER_ATTACK.name" : "Після атаки",
+	"core.bonus.SPELL_AFTER_ATTACK.name" : "Закляття після атаки",
 	"core.bonus.SPELL_AFTER_ATTACK.description" : "Застосовує ${subtype.spell} з вірогідністю ${val}% після атаки",
-	"core.bonus.SPELL_BEFORE_ATTACK.name" : "закляття перед атакою",
+	"core.bonus.SPELL_BEFORE_ATTACK.name" : "Закляття перед атакою",
 	"core.bonus.SPELL_BEFORE_ATTACK.description" : "Застосовує ${subtype.spell} з вірогідністю ${val}% перед атакою",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name": "Стійкість до заклять",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.air": "Стійкість до Повітря",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.fire": "Стійкість до Вогню",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.water": "Стійкість до Води",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.earth": "Стійкість до Землі",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description": "Шкода від усіх заклять зменшується на ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.air": "Шкода від усіх заклять школи Повітря зменшується на ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.fire": "Шкода від усіх заклять школи Вогню зменшується на ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.water": "Шкода від усіх заклять школи Води зменшується на ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.earth": "Шкода від усіх заклять школи Землі зменшується на ${val}%.",
 	"core.bonus.SPELL_IMMUNITY.name" : "Імунітет до заклять",
 	"core.bonus.SPELL_IMMUNITY.description" : "Імунітет до ${subtype.spell}",
 	"core.bonus.SPELL_LIKE_ATTACK.name" : "Атака, схожа на закляття",
 	"core.bonus.SPELL_LIKE_ATTACK.description" : "Атакує за допомогою ${subtype.spell}",
 	"core.bonus.SPELL_RESISTANCE_AURA.name" : "Аура стійкості",
 	"core.bonus.SPELL_RESISTANCE_AURA.description" : "Поруч розташовані стеки отримують ${val}% опору",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name": "Імунітет до усіх заклять",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.air": "Імунітет до Повітря",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.fire": "Імунітет до Вогню",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.water": "Імунітет до Води",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.earth": "Імунітет до Землі",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description": "На цей загін не діють жодні заклинання",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.air": "На цей загін не діють жодні закляття школи Повітря",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.fire": "На цей загін не діють жодні закляття школи Вогню",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.water": "На цей загін не діють жодні закляття школи Води",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.earth": "На цей загін не діють жодні закляття школи Землі",
 	"core.bonus.SUMMON_GUARDIANS.name" : "Закликати охоронців",
 	"core.bonus.SUMMON_GUARDIANS.description" : "На початку бою викликає ${subtype.creature} (${val}%)",
 	"core.bonus.SYNERGY_TARGET.name" : "Синергізм",
@@ -757,42 +793,6 @@
 	"core.bonus.WATER_IMMUNITY.description" : "Імунітет до всіх заклять школи Води",
 	"core.bonus.WIDE_BREATH.name" : "Широкий подих",
 	"core.bonus.WIDE_BREATH.description" : "Атака широким подихом",
-	"core.bonus.LIMITED_SHOOTING_RANGE.name" : "Обмежена дальність стрільби",
-	"core.bonus.LIMITED_SHOOTING_RANGE.description" : "Не може стріляти по цілях на відстані більше ${val} гексів",
-	"core.bonus.DISINTEGRATE.description" : "Після смерті не залишається трупа",
-	"core.bonus.DISINTEGRATE.name" : "Розпад",
-	"core.bonus.ENEMY_ATTACK_REDUCTION.description" : "При атаці ігнорується ${val}% атаки нападника",
-	"core.bonus.ENEMY_ATTACK_REDUCTION.name" : "Ігнорування атаки (${val}%)",
-	"core.bonus.FEROCITY.description" : "Атакує ${val} більше разів, якщо вбиває когось",
-	"core.bonus.FEROCITY.name" : "Лютість",
-	"core.bonus.INVINCIBLE.description" : "На нього ніщо не може вплинути",
-	"core.bonus.INVINCIBLE.name" : "Невразливий",
-	"core.bonus.MECHANICAL.description" : "Імунітет до багатьох ефектів, можна ремонтувати",
-	"core.bonus.MECHANICAL.name" : "Механічний",
-	"core.bonus.PRISM_HEX_ATTACK_BREATH.description" : "Атака подихом у трьох напрямах",
-	"core.bonus.PRISM_HEX_ATTACK_BREATH.name" : "Призматична атака",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.name": "Стійкість до заклять",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.name.air": "Стійкість до Повітря",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.name.fire": "Стійкість до Вогню",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.name.water": "Стійкість до Води",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.name.earth": "Стійкість до Землі",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.description": "Шкода від усіх заклять зменшується на ${val}%.",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.description.air": "Шкода від усіх заклять школи Повітря зменшується на ${val}%.",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.description.fire": "Шкода від усіх заклять школи Вогню зменшується на ${val}%.",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.description.water": "Шкода від усіх заклять школи Води зменшується на ${val}%.",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.description.earth": "Шкода від усіх заклять школи Землі зменшується на ${val}%.",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.name": "Імунітет до усіх заклять",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.air": "Імунітет до Повітря",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.fire": "Імунітет до Вогню",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.water": "Імунітет до Води",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.earth": "Імунітет до Землі",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.description": "На цей загін не діють жодні заклинання",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.air": "На цей загін не діють жодні закляття школи Повітря",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.fire": "На цей загін не діють жодні закляття школи Вогню",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.water": "На цей загін не діють жодні закляття школи Води",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.earth": "На цей загін не діють жодні закляття школи Землі",
-	"core.bonus.REVENGE.description" : "Завдає додаткової шкоди залежно від втраченого здоров'я в бою",
-	"core.bonus.REVENGE.name" : "Помста",
 	
 	"spell.core.castleMoat.name" : "Рів",
 	"spell.core.castleMoatTrigger.name" : "Рів",

+ 0 - 1
client/CServerHandler.cpp

@@ -633,7 +633,6 @@ void CServerHandler::startGameplay(std::shared_ptr<CGameState> gameState)
 		throw std::runtime_error("Invalid mode");
 	}
 	// After everything initialized we can accept CPackToClient netpacks
-	logicConnection->enterGameplayConnectionMode(client->gameState());
 	setState(EClientState::GAMEPLAY);
 }
 

+ 6 - 8
client/Client.cpp

@@ -91,6 +91,11 @@ CClient::CClient()
 
 CClient::~CClient() = default;
 
+IGameInfoCallback & CClient::gameInfo()
+{
+	return *gamestate;
+}
+
 const Services * CClient::services() const
 {
 	return LIBRARY; //todo: this should be LIBRARY
@@ -103,7 +108,7 @@ const CClient::BattleCb * CClient::battle(const BattleID & battleID) const
 
 const CClient::GameCb * CClient::game() const
 {
-	return this;
+	return gamestate.get();
 }
 
 vstd::CLoggerBase * CClient::logger() const
@@ -494,13 +499,6 @@ void CClient::startPlayerBattleAction(const BattleID & battleID, PlayerColor col
 	}
 }
 
-#if SCRIPTING_ENABLED
-scripting::Pool * CClient::getGlobalContextPool() const
-{
-	return clientScripts.get();
-}
-#endif
-
 void CClient::reinitScripting()
 {
 	clientEventBus = std::make_unique<events::EventBus>();

+ 4 - 7
client/Client.h

@@ -122,7 +122,7 @@ public:
 };
 
 /// Class which handles client - server logic
-class CClient : public CGameInfoCallback, public Environment, public IClient
+class CClient : public Environment, public IClient
 {
 	std::shared_ptr<CGameState> gamestate;
 public:
@@ -142,8 +142,9 @@ public:
 	vstd::CLoggerBase * logger() const override;
 	events::EventBus * eventBus() const override;
 
-	CGameState & gameState() final { return *gamestate; }
-	const CGameState & gameState() const final { return *gamestate; }
+	CGameState & gameState() { return *gamestate; }
+	const CGameState & gameState() const { return *gamestate; }
+	IGameInfoCallback & gameInfo();
 
 	void newGame(std::shared_ptr<CGameState> gameState);
 	void loadGame(std::shared_ptr<CGameState> gameState);
@@ -179,10 +180,6 @@ public:
 
 	void removeGUI() const;
 
-#if SCRIPTING_ENABLED
-	scripting::Pool * getGlobalContextPool() const override;
-#endif
-
 private:
 	std::map<PlayerColor, std::shared_ptr<CBattleCallback>> battleCallbacks; //callbacks given to player interfaces
 	std::map<PlayerColor, std::shared_ptr<CPlayerEnvironment>> playerEnvironments;

+ 3 - 3
client/ClientCommandManager.cpp

@@ -90,7 +90,7 @@ void ClientCommandManager::handleGoSoloCommand()
 		// unlikely it will work but just in case to be consistent
 		for(auto & color : GAME->server().getAllClientPlayers(GAME->server().logicConnection->connectionID))
 		{
-			if(color.isValidPlayer() && GAME->server().client->getStartInfo()->playerInfos.at(color).isControlledByHuman())
+			if(color.isValidPlayer() && GAME->server().client->gameInfo().getStartInfo()->playerInfos.at(color).isControlledByHuman())
 			{
 				GAME->server().client->installNewPlayerInterface(std::make_shared<CPlayerInterface>(color), color);
 			}
@@ -103,9 +103,9 @@ void ClientCommandManager::handleGoSoloCommand()
 		
 		for(auto & color : GAME->server().getAllClientPlayers(GAME->server().logicConnection->connectionID))
 		{
-			if(color.isValidPlayer() && GAME->server().client->getStartInfo()->playerInfos.at(color).isControlledByHuman())
+			if(color.isValidPlayer() && GAME->server().client->gameInfo().getStartInfo()->playerInfos.at(color).isControlledByHuman())
 			{
-				auto AiToGive = GAME->server().client->aiNameForPlayer(*GAME->server().client->getPlayerSettings(color), false, false);
+				auto AiToGive = GAME->server().client->aiNameForPlayer(*GAME->server().client->gameInfo().getPlayerSettings(color), false, false);
 				printCommandMessage("Player " + color.toString() + " will be lead by " + AiToGive, ELogLevel::INFO);
 				GAME->server().client->installNewPlayerInterface(CDynLibHandler::getNewAI(AiToGive), color);
 			}

+ 4 - 4
client/ClientNetPackVisitors.h

@@ -23,10 +23,10 @@ class ApplyClientNetPackVisitor : public VCMI_LIB_WRAP_NAMESPACE(ICPackVisitor)
 {
 private:
 	CClient & cl;
-	CGameState & gs;
+	const CGameState & gs;
 
 public:
-	ApplyClientNetPackVisitor(CClient & cl, CGameState & gs)
+	ApplyClientNetPackVisitor(CClient & cl, const CGameState & gs)
 		:cl(cl), gs(gs)
 	{
 	}
@@ -110,10 +110,10 @@ class ApplyFirstClientNetPackVisitor : public VCMI_LIB_WRAP_NAMESPACE(ICPackVisi
 {
 private:
 	CClient & cl;
-	CGameState & gs;
+	const CGameState & gs;
 
 public:
-	ApplyFirstClientNetPackVisitor(CClient & cl, CGameState & gs)
+	ApplyFirstClientNetPackVisitor(CClient & cl, const CGameState & gs)
 		:cl(cl), gs(gs)
 	{
 	}

+ 64 - 64
client/NetPacksClient.cpp

@@ -124,7 +124,7 @@ void ApplyClientNetPackVisitor::visitSetResources(SetResources & pack)
 
 void ApplyClientNetPackVisitor::visitSetPrimSkill(SetPrimSkill & pack)
 {
-	const CGHeroInstance * h = cl.getHero(pack.id);
+	const CGHeroInstance * h = cl.gameInfo().getHero(pack.id);
 	if(!h)
 	{
 		logNetwork->error("Cannot find hero with pack.id %d", pack.id.getNum());
@@ -135,7 +135,7 @@ void ApplyClientNetPackVisitor::visitSetPrimSkill(SetPrimSkill & pack)
 
 void ApplyClientNetPackVisitor::visitSetSecSkill(SetSecSkill & pack)
 {
-	const CGHeroInstance *h = cl.getHero(pack.id);
+	const CGHeroInstance *h = cl.gameInfo().getHero(pack.id);
 	if(!h)
 	{
 		logNetwork->error("Cannot find hero with pack.id %d", pack.id.getNum());
@@ -146,7 +146,7 @@ void ApplyClientNetPackVisitor::visitSetSecSkill(SetSecSkill & pack)
 
 void ApplyClientNetPackVisitor::visitHeroVisitCastle(HeroVisitCastle & pack)
 {
-	const CGHeroInstance *h = cl.getHero(pack.hid);
+	const CGHeroInstance *h = cl.gameInfo().getHero(pack.hid);
 	
 	if(pack.start())
 	{
@@ -156,7 +156,7 @@ void ApplyClientNetPackVisitor::visitHeroVisitCastle(HeroVisitCastle & pack)
 
 void ApplyClientNetPackVisitor::visitSetMana(SetMana & pack)
 {
-	const CGHeroInstance *h = cl.getHero(pack.hid);
+	const CGHeroInstance *h = cl.gameInfo().getHero(pack.hid);
 	callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroManaPointsChanged, h);
 
 	if(settings["session"]["headless"].Bool())
@@ -168,7 +168,7 @@ void ApplyClientNetPackVisitor::visitSetMana(SetMana & pack)
 
 void ApplyClientNetPackVisitor::visitSetMovePoints(SetMovePoints & pack)
 {
-	const CGHeroInstance *h = cl.getHero(pack.hid);
+	const CGHeroInstance *h = cl.gameInfo().getHero(pack.hid);
 	callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroMovePointsChanged, h);
 }
 
@@ -182,11 +182,11 @@ void ApplyClientNetPackVisitor::visitFoWChange(FoWChange & pack)
 {
 	for(auto &i : cl.playerint)
 	{
-		if(cl.getPlayerRelations(i.first, pack.player) == PlayerRelations::SAME_PLAYER && pack.waitForDialogs && GAME->interface() == i.second.get())
+		if(cl.gameInfo().getPlayerRelations(i.first, pack.player) == PlayerRelations::SAME_PLAYER && pack.waitForDialogs && GAME->interface() == i.second.get())
 		{
 			GAME->interface()->waitWhileDialog();
 		}
-		if(cl.getPlayerRelations(i.first, pack.player) != PlayerRelations::ENEMIES)
+		if(cl.gameInfo().getPlayerRelations(i.first, pack.player) != PlayerRelations::ENEMIES)
 		{
 			if(pack.mode == ETileVisibility::REVEALED)
 				i.second->tileRevealed(pack.tiles);
@@ -199,7 +199,7 @@ void ApplyClientNetPackVisitor::visitFoWChange(FoWChange & pack)
 
 static void dispatchGarrisonChange(CClient & cl, ObjectInstanceID army1, ObjectInstanceID army2)
 {
-	auto obj1 = cl.getObj(army1);
+	auto obj1 = cl.gameInfo().getObj(army1);
 	if(!obj1)
 	{
 		logNetwork->error("Cannot find army with pack.id %d", army1.getNum());
@@ -210,7 +210,7 @@ static void dispatchGarrisonChange(CClient & cl, ObjectInstanceID army1, ObjectI
 
 	if(army2 != ObjectInstanceID() && army2 != army1)
 	{
-		auto obj2 = cl.getObj(army2);
+		auto obj2 = cl.gameInfo().getObj(army2);
 		if(!obj2)
 		{
 			logNetwork->error("Cannot find army with pack.id %d", army2.getNum());
@@ -265,20 +265,20 @@ void ApplyClientNetPackVisitor::visitBulkRebalanceStacks(BulkRebalanceStacks & p
 
 void ApplyClientNetPackVisitor::visitPutArtifact(PutArtifact & pack)
 {
-	callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactPut, pack.al);
+	callInterfaceIfPresent(cl, cl.gameState().getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactPut, pack.al);
 	if(pack.askAssemble)
-		callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::askToAssembleArtifact, pack.al);
+		callInterfaceIfPresent(cl, cl.gameState().getOwner(pack.al.artHolder), &IGameEventsReceiver::askToAssembleArtifact, pack.al);
 }
 
 void ApplyClientNetPackVisitor::visitBulkEraseArtifacts(BulkEraseArtifacts & pack)
 {
 	for(const auto & slotErase : pack.posPack)
-		callInterfaceIfPresent(cl, cl.getOwner(pack.artHolder), &IGameEventsReceiver::artifactRemoved, ArtifactLocation(pack.artHolder, slotErase));
+		callInterfaceIfPresent(cl, cl.gameState().getOwner(pack.artHolder), &IGameEventsReceiver::artifactRemoved, ArtifactLocation(pack.artHolder, slotErase));
 }
 
 void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack)
 {
-	const auto dstOwner = cl.getOwner(pack.dstArtHolder);
+	const auto dstOwner = cl.gameState().getOwner(pack.dstArtHolder);
 	const auto applyMove = [this, &pack, dstOwner](const std::vector<MoveArtifactInfo> & artsPack)
 	{
 		for(const auto & slotToMove : artsPack)
@@ -318,18 +318,18 @@ void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack)
 
 void ApplyClientNetPackVisitor::visitAssembledArtifact(AssembledArtifact & pack)
 {
-	callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactAssembled, pack.al);
+	callInterfaceIfPresent(cl, cl.gameState().getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactAssembled, pack.al);
 }
 
 void ApplyClientNetPackVisitor::visitDisassembledArtifact(DisassembledArtifact & pack)
 {
-	callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactDisassembled, pack.al);
+	callInterfaceIfPresent(cl, cl.gameState().getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactDisassembled, pack.al);
 }
 
 void ApplyClientNetPackVisitor::visitHeroVisit(HeroVisit & pack)
 {
-	auto hero = cl.getHero(pack.heroId);
-	auto obj = cl.getObj(pack.objId, false);
+	auto hero = cl.gameInfo().getHero(pack.heroId);
+	auto obj = cl.gameInfo().getObj(pack.objId, false);
 	callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::heroVisit, hero, obj, pack.starting);
 }
 
@@ -369,14 +369,14 @@ void ApplyClientNetPackVisitor::visitGiveBonus(GiveBonus & pack)
 
 void ApplyFirstClientNetPackVisitor::visitChangeObjPos(ChangeObjPos & pack)
 {
-	CGObjectInstance *obj = gs.getObjInstance(pack.objid);
+	const CGObjectInstance *obj = gs.getObjInstance(pack.objid);
 	GAME->map().onObjectFadeOut(obj, pack.initiator);
 	GAME->map().waitForOngoingAnimations();
 }
 
 void ApplyClientNetPackVisitor::visitChangeObjPos(ChangeObjPos & pack)
 {
-	CGObjectInstance *obj = gs.getObjInstance(pack.objid);
+	const CGObjectInstance *obj = gs.getObjInstance(pack.objid);
 	GAME->map().onObjectFadeIn(obj, pack.initiator);
 	GAME->map().waitForOngoingAnimations();
 	callAllInterfaces(cl, &CGameInterface::invalidatePaths);
@@ -386,8 +386,8 @@ void ApplyClientNetPackVisitor::visitPlayerEndsGame(PlayerEndsGame & pack)
 {
 	callAllInterfaces(cl, &IGameEventsReceiver::gameOver, pack.player, pack.victoryLossCheckResult);
 
-	bool localHumanWinsGame = vstd::contains(cl.playerint, pack.player) && cl.getPlayerState(pack.player)->human && pack.victoryLossCheckResult.victory();
-	bool lastHumanEndsGame = GAME->server().howManyPlayerInterfaces() == 1 && vstd::contains(cl.playerint, pack.player) && cl.getPlayerState(pack.player)->human && !settings["session"]["spectate"].Bool();
+	bool localHumanWinsGame = vstd::contains(cl.playerint, pack.player) && cl.gameInfo().getPlayerState(pack.player)->human && pack.victoryLossCheckResult.victory();
+	bool lastHumanEndsGame = GAME->server().howManyPlayerInterfaces() == 1 && vstd::contains(cl.playerint, pack.player) && cl.gameInfo().getPlayerState(pack.player)->human && !settings["session"]["spectate"].Bool();
 
 	if(lastHumanEndsGame || localHumanWinsGame)
 	{
@@ -466,7 +466,7 @@ void ApplyClientNetPackVisitor::visitRemoveBonus(RemoveBonus & pack)
 
 void ApplyFirstClientNetPackVisitor::visitRemoveObject(RemoveObject & pack)
 {
-	const CGObjectInstance *o = cl.getObj(pack.objectID);
+	const CGObjectInstance *o = cl.gameInfo().getObj(pack.objectID);
 
 	GAME->map().onObjectFadeOut(o, pack.initiator);
 
@@ -475,7 +475,7 @@ void ApplyFirstClientNetPackVisitor::visitRemoveObject(RemoveObject & pack)
 	{
 		//below line contains little cheat for AI so it will be aware of deletion of enemy heroes that moved or got re-covered by FoW
 		//TODO: loose requirements as next AI related crashes appear, for example another pack.player collects object that got re-covered by FoW, unsure if AI code workarounds this
-		if(gs.isVisibleFor(o, i->first) || (!cl.getPlayerState(i->first)->human && o->ID == Obj::HERO && o->tempOwner != i->first))
+		if(gs.isVisibleFor(o, i->first) || (!cl.gameInfo().getPlayerState(i->first)->human && o->ID == Obj::HERO && o->tempOwner != i->first))
 			i->second->objectRemoved(o, pack.initiator);
 	}
 
@@ -492,7 +492,7 @@ void ApplyClientNetPackVisitor::visitRemoveObject(RemoveObject & pack)
 
 void ApplyFirstClientNetPackVisitor::visitTryMoveHero(TryMoveHero & pack)
 {
-	CGHeroInstance *h = gs.getHero(pack.id);
+	const CGHeroInstance *h = gs.getHero(pack.id);
 
 	switch (pack.result)
 	{
@@ -510,7 +510,7 @@ void ApplyFirstClientNetPackVisitor::visitTryMoveHero(TryMoveHero & pack)
 
 void ApplyClientNetPackVisitor::visitTryMoveHero(TryMoveHero & pack)
 {
-	const CGHeroInstance *h = cl.getHero(pack.id);
+	const CGHeroInstance *h = cl.gameInfo().getHero(pack.id);
 	callAllInterfaces(cl, &CGameInterface::invalidatePaths);
 
 	switch(pack.result)
@@ -532,7 +532,7 @@ void ApplyClientNetPackVisitor::visitTryMoveHero(TryMoveHero & pack)
 	PlayerColor player = h->tempOwner;
 
 	for(auto &i : cl.playerint)
-		if(cl.getPlayerRelations(i.first, player) != PlayerRelations::ENEMIES)
+		if(cl.gameInfo().getPlayerRelations(i.first, player) != PlayerRelations::ENEMIES)
 			i.second->tileRevealed(pack.fowRevealed);
 
 	for(auto i=cl.playerint.begin(); i!=cl.playerint.end(); i++)
@@ -544,7 +544,7 @@ void ApplyClientNetPackVisitor::visitTryMoveHero(TryMoveHero & pack)
 			|| gs.isVisibleFor(h->convertToVisitablePos(pack.end), i->first))
 		{
 			// pack.src and pack.dst of enemy hero move may be not visible => 'verbose' should be false
-			const bool verbose = cl.getPlayerRelations(i->first, player) != PlayerRelations::ENEMIES;
+			const bool verbose = cl.gameInfo().getPlayerRelations(i->first, player) != PlayerRelations::ENEMIES;
 			i->second->heroMoved(pack, verbose);
 		}
 	}
@@ -552,7 +552,7 @@ void ApplyClientNetPackVisitor::visitTryMoveHero(TryMoveHero & pack)
 
 void ApplyClientNetPackVisitor::visitNewStructures(NewStructures & pack)
 {
-	CGTownInstance *town = gs.getTown(pack.tid);
+	const CGTownInstance *town = gs.getTown(pack.tid);
 	for(const auto & id : pack.bid)
 	{
 		callInterfaceIfPresent(cl, town->getOwner(), &IGameEventsReceiver::buildChanged, town, id, 1);
@@ -565,7 +565,7 @@ void ApplyClientNetPackVisitor::visitNewStructures(NewStructures & pack)
 
 void ApplyClientNetPackVisitor::visitRazeStructures(RazeStructures & pack)
 {
-	CGTownInstance * town = gs.getTown(pack.tid);
+	const CGTownInstance * town = gs.getTown(pack.tid);
 	for(const auto & id : pack.bid)
 	{
 		callInterfaceIfPresent(cl, town->getOwner(), &IGameEventsReceiver::buildChanged, town, id, 2);
@@ -578,11 +578,11 @@ void ApplyClientNetPackVisitor::visitRazeStructures(RazeStructures & pack)
 
 void ApplyClientNetPackVisitor::visitSetAvailableCreatures(SetAvailableCreatures & pack)
 {
-	const CGDwelling * dw = static_cast<const CGDwelling*>(cl.getObj(pack.tid));
+	const CGDwelling * dw = static_cast<const CGDwelling*>(cl.gameInfo().getObj(pack.tid));
 
 	PlayerColor p;
 	if(dw->ID == Obj::WAR_MACHINE_FACTORY) //War Machines Factory is not flaggable, it's "owned" by visitor
-		p = cl.getObjInstance(cl.getTile(dw->visitablePos())->visitableObjects.back())->getOwner();
+		p = cl.gameInfo().getObjInstance(cl.gameInfo().getTile(dw->visitablePos())->visitableObjects.back())->getOwner();
 	else
 		p = dw->tempOwner;
 
@@ -591,9 +591,9 @@ void ApplyClientNetPackVisitor::visitSetAvailableCreatures(SetAvailableCreatures
 
 void ApplyClientNetPackVisitor::visitSetHeroesInTown(SetHeroesInTown & pack)
 {
-	CGTownInstance * t = gs.getTown(pack.tid);
-	CGHeroInstance * hGarr  = gs.getHero(pack.garrison);
-	CGHeroInstance * hVisit = gs.getHero(pack.visiting);
+	const CGTownInstance * t = gs.getTown(pack.tid);
+	const CGHeroInstance * hGarr  = gs.getHero(pack.garrison);
+	const CGHeroInstance * hVisit = gs.getHero(pack.visiting);
 
 	//inform all players that see this object
 	for(auto i = cl.playerint.cbegin(); i != cl.playerint.cend(); ++i)
@@ -612,7 +612,7 @@ void ApplyClientNetPackVisitor::visitSetHeroesInTown(SetHeroesInTown & pack)
 
 void ApplyClientNetPackVisitor::visitHeroRecruited(HeroRecruited & pack)
 {
-	auto * h = gs.getMap().getHero(pack.hid);
+	const auto * h = gs.getMap().getHero(pack.hid);
 	if(h->getHeroTypeID() != pack.hid)
 	{
 		logNetwork->error("Something wrong with hero recruited!");
@@ -628,7 +628,7 @@ void ApplyClientNetPackVisitor::visitHeroRecruited(HeroRecruited & pack)
 
 void ApplyClientNetPackVisitor::visitGiveHero(GiveHero & pack)
 {
-	CGHeroInstance *h = gs.getHero(pack.id);
+	const CGHeroInstance *h = gs.getHero(pack.id);
 	GAME->map().onObjectInstantAdd(h, h->getOwner());
 	callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroCreated, h);
 }
@@ -681,14 +681,14 @@ void ApplyClientNetPackVisitor::visitSetObjectProperty(SetObjectProperty & pack)
 
 void ApplyClientNetPackVisitor::visitHeroLevelUp(HeroLevelUp & pack)
 {
-	const CGHeroInstance * hero = cl.getHero(pack.heroId);
+	const CGHeroInstance * hero = cl.gameInfo().getHero(pack.heroId);
 	assert(hero);
 	callOnlyThatInterface(cl, pack.player, &CGameInterface::heroGotLevel, hero, pack.primskill, pack.skills, pack.queryID);
 }
 
 void ApplyClientNetPackVisitor::visitCommanderLevelUp(CommanderLevelUp & pack)
 {
-	const CGHeroInstance * hero = cl.getHero(pack.heroId);
+	const CGHeroInstance * hero = cl.gameInfo().getHero(pack.heroId);
 	assert(hero);
 	const auto & commander = hero->getCommander();
 	assert(commander);
@@ -706,8 +706,8 @@ void ApplyClientNetPackVisitor::visitBlockingDialog(BlockingDialog & pack)
 
 void ApplyClientNetPackVisitor::visitGarrisonDialog(GarrisonDialog & pack)
 {
-	const CGHeroInstance *h = cl.getHero(pack.hid);
-	const CArmedInstance *obj = static_cast<const CArmedInstance*>(cl.getObj(pack.objid));
+	const CGHeroInstance *h = cl.gameInfo().getHero(pack.hid);
+	const CArmedInstance *obj = static_cast<const CArmedInstance*>(cl.gameInfo().getObj(pack.objid));
 
 	callOnlyThatInterface(cl, h->getOwner(), &CGameInterface::showGarrisonDialog, obj, h, pack.removableUnits, pack.queryID);
 }
@@ -719,7 +719,7 @@ void ApplyClientNetPackVisitor::visitExchangeDialog(ExchangeDialog & pack)
 
 void ApplyClientNetPackVisitor::visitTeleportDialog(TeleportDialog & pack)
 {
-	const CGHeroInstance *h = cl.getHero(pack.hero);
+	const CGHeroInstance *h = cl.gameInfo().getHero(pack.hero);
 	callOnlyThatInterface(cl, h->getOwner(), &CGameInterface::showTeleportDialog, h, pack.channel, pack.exits, pack.impassable, pack.queryID);
 }
 
@@ -948,7 +948,7 @@ void ApplyClientNetPackVisitor::visitAdvmapSpellCast(AdvmapSpellCast & pack)
 {
 	callAllInterfaces(cl, &CGameInterface::invalidatePaths);
 
-	auto caster = cl.getHero(pack.casterID);
+	auto caster = cl.gameInfo().getHero(pack.casterID);
 	if(caster)
 		//consider notifying other interfaces that see hero?
 		callInterfaceIfPresent(cl, caster->getOwner(), &IGameEventsReceiver::advmapSpellCast, caster, pack.spellID);
@@ -968,15 +968,15 @@ void ApplyClientNetPackVisitor::visitOpenWindow(OpenWindow & pack)
 	case EOpenWindowMode::RECRUITMENT_FIRST:
 	case EOpenWindowMode::RECRUITMENT_ALL:
 		{
-			const CGDwelling *dw = dynamic_cast<const CGDwelling*>(cl.getObj(ObjectInstanceID(pack.object)));
-			const CArmedInstance *dst = dynamic_cast<const CArmedInstance*>(cl.getObj(ObjectInstanceID(pack.visitor)));
+			const CGDwelling *dw = dynamic_cast<const CGDwelling*>(cl.gameInfo().getObj(ObjectInstanceID(pack.object)));
+			const CArmedInstance *dst = dynamic_cast<const CArmedInstance*>(cl.gameInfo().getObj(ObjectInstanceID(pack.visitor)));
 			callInterfaceIfPresent(cl, dst->tempOwner, &IGameEventsReceiver::showRecruitmentDialog, dw, dst, pack.window == EOpenWindowMode::RECRUITMENT_FIRST ? 0 : -1, pack.queryID);
 		}
 		break;
 	case EOpenWindowMode::SHIPYARD_WINDOW:
 		{
 			assert(pack.queryID == QueryID::NONE);
-			const auto * sy = dynamic_cast<const IShipyard *>(cl.getObj(ObjectInstanceID(pack.object)));
+			const auto * sy = dynamic_cast<const IShipyard *>(cl.gameInfo().getObj(ObjectInstanceID(pack.object)));
 			callInterfaceIfPresent(cl, sy->getObject()->getOwner(), &IGameEventsReceiver::showShipyardDialog, sy);
 		}
 		break;
@@ -984,27 +984,27 @@ void ApplyClientNetPackVisitor::visitOpenWindow(OpenWindow & pack)
 		{
 			assert(pack.queryID == QueryID::NONE);
 			//displays Thieves' Guild window (when hero enters Den of Thieves)
-			const CGObjectInstance *obj = cl.getObj(ObjectInstanceID(pack.object));
-			const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor));
+			const CGObjectInstance *obj = cl.gameInfo().getObj(ObjectInstanceID(pack.object));
+			const CGHeroInstance *hero = cl.gameInfo().getHero(ObjectInstanceID(pack.visitor));
 			callInterfaceIfPresent(cl, hero->getOwner(), &IGameEventsReceiver::showThievesGuildWindow, obj);
 		}
 		break;
 	case EOpenWindowMode::UNIVERSITY_WINDOW:
 		{
 			//displays University window (when hero enters University on adventure map)
-			const auto * market = cl.getMarket(ObjectInstanceID(pack.object));
-			const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor));
+			const auto * market = cl.gameState().getMarket(ObjectInstanceID(pack.object));
+			const CGHeroInstance *hero = cl.gameInfo().getHero(ObjectInstanceID(pack.visitor));
 			callInterfaceIfPresent(cl, hero->tempOwner, &IGameEventsReceiver::showUniversityWindow, market, hero, pack.queryID);
 		}
 		break;
 	case EOpenWindowMode::MARKET_WINDOW:
 		{
 			//displays Thieves' Guild window (when hero enters Den of Thieves)
-			const CGObjectInstance *obj = cl.getObj(ObjectInstanceID(pack.object));
-			const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor));
-			const auto market = cl.getMarket(pack.object);
-			const auto * tile = cl.getTile(obj->visitablePos());
-			const auto * topObject = cl.getObjInstance(tile->visitableObjects.back());
+			const CGObjectInstance *obj = cl.gameInfo().getObj(ObjectInstanceID(pack.object));
+			const CGHeroInstance *hero = cl.gameInfo().getHero(ObjectInstanceID(pack.visitor));
+			const auto market = cl.gameState().getMarket(pack.object);
+			const auto * tile = cl.gameInfo().getTile(obj->visitablePos());
+			const auto * topObject = cl.gameInfo().getObjInstance(tile->visitableObjects.back());
 			callInterfaceIfPresent(cl, topObject->getOwner(), &IGameEventsReceiver::showMarketWindow, market, hero, pack.queryID);
 		}
 		break;
@@ -1012,24 +1012,24 @@ void ApplyClientNetPackVisitor::visitOpenWindow(OpenWindow & pack)
 		{
 			assert(pack.queryID == QueryID::NONE);
 			//displays Hill fort window
-			const CGObjectInstance *obj = cl.getObj(ObjectInstanceID(pack.object));
-			const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor));
-			const auto * tile = cl.getTile(obj->visitablePos());
-			const auto * topObject = cl.getObjInstance(tile->visitableObjects.back());
+			const CGObjectInstance *obj = cl.gameInfo().getObj(ObjectInstanceID(pack.object));
+			const CGHeroInstance *hero = cl.gameInfo().getHero(ObjectInstanceID(pack.visitor));
+			const auto * tile = cl.gameInfo().getTile(obj->visitablePos());
+			const auto * topObject = cl.gameInfo().getObjInstance(tile->visitableObjects.back());
 			callInterfaceIfPresent(cl, topObject->getOwner(), &IGameEventsReceiver::showHillFortWindow, obj, hero);
 		}
 		break;
 	case EOpenWindowMode::PUZZLE_MAP:
 		{
 			assert(pack.queryID == QueryID::NONE);
-			const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor));
+			const CGHeroInstance *hero = cl.gameInfo().getHero(ObjectInstanceID(pack.visitor));
 			callInterfaceIfPresent(cl, hero->getOwner(), &IGameEventsReceiver::showPuzzleMap);
 		}
 		break;
 	case EOpenWindowMode::TAVERN_WINDOW:
 		{
-			const CGObjectInstance *obj1 = cl.getObj(ObjectInstanceID(pack.object));
-			const CGHeroInstance * hero = cl.getHero(ObjectInstanceID(pack.visitor));
+			const CGObjectInstance *obj1 = cl.gameInfo().getObj(ObjectInstanceID(pack.object));
+			const CGHeroInstance * hero = cl.gameInfo().getHero(ObjectInstanceID(pack.visitor));
 			callInterfaceIfPresent(cl, hero->tempOwner, &IGameEventsReceiver::showTavernWindow, obj1, hero, pack.queryID);
 		}
 		break;
@@ -1065,10 +1065,10 @@ void ApplyClientNetPackVisitor::visitSetAvailableArtifacts(SetAvailableArtifacts
 	}
 	else
 	{
-		const CGBlackMarket *bm = dynamic_cast<const CGBlackMarket *>(cl.getObj(ObjectInstanceID(pack.id)));
+		const CGBlackMarket *bm = dynamic_cast<const CGBlackMarket *>(cl.gameInfo().getObj(ObjectInstanceID(pack.id)));
 		assert(bm);
-		const auto * tile = cl.getTile(bm->visitablePos());
-		const auto * topObject = cl.getObjInstance(tile->visitableObjects.back());
+		const auto * tile = cl.gameInfo().getTile(bm->visitablePos());
+		const auto * topObject = cl.gameInfo().getObjInstance(tile->visitableObjects.back());
 
 		callInterfaceIfPresent(cl, topObject->getOwner(), &IGameEventsReceiver::availableArtifactsChanged, bm);
 	}

+ 0 - 1
client/NetPacksLobbyClient.cpp

@@ -148,7 +148,6 @@ void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyPrepareStartGame(LobbyPrepareS
 {
 	handler.client = std::make_unique<CClient>();
 	handler.logicConnection->enterLobbyConnectionMode();
-	handler.logicConnection->setCallback(handler.client.get());
 }
 
 void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyStartGame(LobbyStartGame & pack)

+ 73 - 21
client/windows/CCreatureWindow.cpp

@@ -258,8 +258,8 @@ CStackWindow::BonusLineSection::BonusLineSection(CStackWindow * owner, size_t li
 
 	static const std::array<Point, 2> offset =
 	{
-		Point(6, 4),
-		Point(214, 4)
+		Point(6, 2),
+		Point(214, 2)
 	};
 
 	auto drawBonusSource = [this](int leftRight, Point p, BonusInfo & bi)
@@ -274,7 +274,7 @@ CStackWindow::BonusLineSection::BonusLineSection(CStackWindow * owner, size_t li
 			{BonusSource::STACK_EXPERIENCE,  Colors::CYAN},
 			{BonusSource::COMMANDER,         Colors::CYAN},
 		};
-		
+
 		std::map<BonusSource, std::string> bonusNames = {
 			{BonusSource::ARTIFACT,          LIBRARY->generaltexth->translate("vcmi.bonusSource.artifact")},
 			{BonusSource::ARTIFACT_INSTANCE, LIBRARY->generaltexth->translate("vcmi.bonusSource.artifact")},
@@ -313,8 +313,14 @@ CStackWindow::BonusLineSection::BonusLineSection(CStackWindow * owner, size_t li
 			BonusInfo & bi = parent->activeBonuses[bonusIndex];
 			if (!bi.imagePath.empty())
 				icon[leftRight] = std::make_shared<CPicture>(bi.imagePath, position.x, position.y);
-			name[leftRight] = std::make_shared<CLabel>(position.x + 60, position.y + 2, FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, bi.name, 137);
-			description[leftRight] = std::make_shared<CMultiLineLabel>(Rect(position.x + 60, position.y + 20, 137, 26), FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, bi.description);
+
+			if (!bi.name.empty())
+			{
+				name[leftRight] = std::make_shared<CLabel>(position.x + 60, position.y + 2, FONT_TINY, ETextAlignment::TOPLEFT, Colors::YELLOW, bi.name, 137);
+				description[leftRight] = std::make_shared<CMultiLineLabel>(Rect(position.x + 60, position.y + 20, 137, 26), FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, bi.description);
+			}
+			else
+				description[leftRight] = std::make_shared<CMultiLineLabel>(Rect(position.x + 60, position.y + 2, 137, 50), FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, bi.description);
 			drawBonusSource(leftRight, Point(position.x - 1, position.y - 1), bi);
 		}
 	}
@@ -846,12 +852,10 @@ void CStackWindow::init()
 
 void CStackWindow::initBonusesList()
 {
-	auto inputPtr = info->stackNode->getBonuses(CSelector(Bonus::Permanent), Selector::all);
-
-	BonusList output;
-	BonusList input = *inputPtr;
+	BonusList receivedBonuses = *info->stackNode->getBonuses(CSelector(Bonus::Permanent), Selector::all);
+	BonusList abilities = info->creature->getExportedBonusList();
 
-	std::sort(input.begin(), input.end(), [this](std::shared_ptr<Bonus> v1, std::shared_ptr<Bonus> & v2){
+	const auto & bonusSortingPredicate = [this](const std::shared_ptr<Bonus> & v1, const std::shared_ptr<Bonus> & v2){
 		if (v1->source != v2->source)
 		{
 			int priorityV1 = v1->source == BonusSource::CREATURE_ABILITY ? -1 : static_cast<int>(v1->source);
@@ -860,30 +864,78 @@ void CStackWindow::initBonusesList()
 		}
 		else
 			return  info->stackNode->bonusToString(v1, false) < info->stackNode->bonusToString(v2, false);
-	});
+	};
+
+	// these bonuses require special handling. For example they come with own descriptions, for use in morale/luck description
+	// also, this information is already available in creature window
+	receivedBonuses.remove_if(Selector::type()(BonusType::MORALE));
+	receivedBonuses.remove_if(Selector::type()(BonusType::LUCK));
+
+	std::vector<BonusList> groupedBonuses;
+	while (!receivedBonuses.empty())
+	{
+		auto currentBonus = receivedBonuses.front();
 
-	while(!input.empty())
+		const auto & sameBonusPredicate = [currentBonus](const std::shared_ptr<Bonus> & b)
+		{
+			return currentBonus->type == b->type && currentBonus->subtype == b->subtype;
+		};
+
+		groupedBonuses.emplace_back();
+
+		std::copy_if(receivedBonuses.begin(), receivedBonuses.end(), std::back_inserter(groupedBonuses.back()), sameBonusPredicate);
+		receivedBonuses.remove_if(Selector::typeSubtype(currentBonus->type, currentBonus->subtype));
+		// FIXME: potential edge case: unit has ability that is propagated away (and needs to be displayed), but also receives same bonus from someplace else
+		abilities.remove_if(Selector::typeSubtype(currentBonus->type, currentBonus->subtype));
+	}
+
+	// Add any remaining abilities of this unit that don't affect it at the moment, such as abilities that are propagated away, e.g. to other side in combat
+	BonusList visibleBonuses = abilities;
+
+	for (auto & group : groupedBonuses)
 	{
-		auto b = input.front();
-		output.push_back(std::make_shared<Bonus>(*b));
-		output.back()->val = input.valOfBonuses(Selector::typeSubtype(b->type, b->subtype)); //merge multiple bonuses into one
-		input.remove_if (Selector::typeSubtype(b->type, b->subtype)); //remove used bonuses
+		// Try to find the bonus in the group that represents the final effect in the best way.
+		std::sort(group.begin(), group.end(), bonusSortingPredicate);
+
+		BonusList groupIndepMin = group;
+		BonusList groupIndepMax = group;
+		BonusList groupNoMinMax = group;
+		groupIndepMin.remove_if([](const Bonus * b) { return b->valType != BonusValueType::INDEPENDENT_MIN; });
+		groupIndepMax.remove_if([](const Bonus * b) { return b->valType != BonusValueType::INDEPENDENT_MAX; });
+		groupNoMinMax.remove_if([](const Bonus * b) { return b->valType == BonusValueType::INDEPENDENT_MAX || b->valType == BonusValueType::INDEPENDENT_MIN; });
+
+		int valIndepMin = groupIndepMin.totalValue();
+		int valIndepMax = groupIndepMax.totalValue();
+		int valNoMinMax = group.totalValue();
+
+		BonusList usedGroup;
+
+		if (!groupIndepMin.empty() && valNoMinMax != valIndepMin)
+			usedGroup = groupIndepMin; // bonus value was limited due to INDEPENDENT_MIN bonus -> show this bonus
+		else if (!groupIndepMax.empty() && valNoMinMax != valIndepMax)
+			usedGroup = groupIndepMax; // bonus value was limited due to INDEPENDENT_MAX bonus -> show this bonus
+		else
+			usedGroup = groupNoMinMax; // bonus value is not limited - show first non-independent bonus
+
+		// It is possible that empty group was selected. For example, there is only INDEPENDENT effect with value of 0, which does not actually has any effect on this unit
+		// For example, orb of vulnerability on unit without any resistances
+		if (!usedGroup.empty())
+			visibleBonuses.push_back(usedGroup.front());
 	}
 
+	std::sort(visibleBonuses.begin(), visibleBonuses.end(), bonusSortingPredicate);
+
 	BonusInfo bonusInfo;
-	for(auto b : output)
+	for(auto b : visibleBonuses)
 	{
 		bonusInfo.name = info->stackNode->bonusToString(b, false);
 		bonusInfo.description = info->stackNode->bonusToString(b, true);
 		bonusInfo.imagePath = info->stackNode->bonusToGraphics(b);
 		bonusInfo.bonusSource = b->source;
 
-		if(b->sid.as<CreatureID>() != info->stackNode->getId() && b->propagator && b->propagator->getPropagatorType() == CBonusSystemNode::HERO) // Shows bonus with "propagator":"HERO" only at creature with bonus
-			continue;
-
 		//if it's possible to give any description or image for this kind of bonus
 		//TODO: figure out why half of bonuses don't have proper description
-		if(!bonusInfo.name.empty() || !bonusInfo.imagePath.empty())
+		if(!bonusInfo.name.empty() || !bonusInfo.description.empty())
 			activeBonuses.push_back(bonusInfo);
 	}
 }

+ 1 - 1
client/windows/CMessage.cpp

@@ -124,7 +124,7 @@ std::vector<std::string> CMessage::breakText(std::string text, size_t maxLineWid
 
 		// not all line has been processed - it turned out to be too long, so erase everything after last word break
 		// if string consists from a single word (or this is Chinese/Korean) - erase only last symbol to bring line back to allowed length
-		if(currPos < text.length() && (text[currPos] != 0x0a))
+		if(fontPtr->getStringWidth(printableString) > maxLineWidth && (text[currPos] != 0x0a))
 		{
 			if(wordBreak != ui32(-1))
 			{

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 193 - 193
config/artifacts.json


+ 7 - 7
config/battlefields.json

@@ -79,14 +79,14 @@
 				"type" : "MORALE",
 				"val" : 1,
 				"valueType" : "BASE_NUMBER",
-				"description" : "core.arraytxt.123",
+				"description" : "@core.arraytxt.123",
 				"limiters": [{ "type" : "CREATURE_ALIGNMENT_LIMITER", "parameters" : ["good"] }]
 			},
 			{
 				"type" : "MORALE",
 				"val" : -1,
 				"valueType" : "BASE_NUMBER",
-				"description" : "core.arraytxt.124",
+				"description" : "@core.arraytxt.124",
 				"limiters": [{ "type" : "CREATURE_ALIGNMENT_LIMITER", "parameters" : ["evil"] }]
 			}
 		]
@@ -99,7 +99,7 @@
 				"type" : "LUCK",
 				"val" : 2,
 				"valueType" : "BASE_NUMBER",
-				"description" : "core.arraytxt.83",
+				"description" : "@core.arraytxt.83",
 				"limiters": [{ "type" : "CREATURE_ALIGNMENT_LIMITER", "parameters" : ["neutral"] }]
 			}
 		]
@@ -112,14 +112,14 @@
 				"type" : "MORALE",
 				"val" : -1,
 				"valueType" : "BASE_NUMBER",
-				"description" : "core.arraytxt.126",
+				"description" : "@core.arraytxt.126",
 				"limiters": [{ "type" : "CREATURE_ALIGNMENT_LIMITER", "parameters" : ["good"] }]
 			},
 			{
 				"type" : "MORALE",
 				"val" : 1,
 				"valueType" : "BASE_NUMBER",
-				"description" : "core.arraytxt.125",
+				"description" : "@core.arraytxt.125",
 				"limiters": [{ "type" : "CREATURE_ALIGNMENT_LIMITER", "parameters" : ["evil"] }]
 			}
 		]
@@ -132,13 +132,13 @@
 				"type" : "NO_MORALE",
 				"val" : 0,
 				"valueType" : "INDEPENDENT_MIN",
-				"description" : "core.arraytxt.112"
+				"description" : "@core.arraytxt.112"
 			},
 			{
 				"type" : "NO_LUCK",
 				"val" : 0,
 				"valueType" : "INDEPENDENT_MIN",
-				"description" : "core.arraytxt.81"
+				"description" : "@core.arraytxt.81"
 			},
 			{
 				"type" : "BLOCK_MAGIC_ABOVE",

+ 4 - 8
config/creatures/dungeon.json

@@ -9,14 +9,12 @@
 			"blindImmunity" : 
 			{
 				"type" : "SPELL_IMMUNITY",
-				"subtype" : "spell.blind",
-				"addInfo" : 1
+				"subtype" : "spell.blind"
 			},
 			"petrifyImmunity" : 
 			{
 				"type" : "SPELL_IMMUNITY",
-				"subtype" : "spell.stoneGaze",
-				"addInfo" : 1
+				"subtype" : "spell.stoneGaze"
 			}
 		},
 		"upgrades": ["infernalTroglodyte"],
@@ -44,14 +42,12 @@
 			"blindImmunity" : 
 			{
 				"type" : "SPELL_IMMUNITY",
-				"subtype" : "spell.blind",
-				"addInfo" : 1
+				"subtype" : "spell.blind"
 			},
 			"petrifyImmunity" : 
 			{
 				"type" : "SPELL_IMMUNITY",
-				"subtype" : "spell.stoneGaze",
-				"addInfo" : 1
+				"subtype" : "spell.stoneGaze"
 			}
 		},
 		"graphics" :

+ 11 - 2
config/schemas/artifact.json

@@ -66,9 +66,18 @@
 			"description" : "Used together with components fild. Marks the artifact as fused. Cannot be disassembled."
 		},
 		"bonuses" : {
-			"type" : "array",
 			"description" : "Bonuses provided by this artifact using bonus system",
-			"items" : { "$ref" : "bonusInstance.json" }
+			"type" : "object",
+			"additionalProperties" : {
+				"$ref" : "bonusInstance.json"
+			}
+		},
+		"instanceBonuses" : {
+			"description" : "Bonuses provided by every instance of this artifact using bonus system",
+			"type" : "object",
+			"additionalProperties" : {
+				"$ref" : "bonusInstance.json"
+			}
 		},
 		"growing" : {
 			"type" : "object",

+ 5 - 0
config/schemas/bonusInstance.json

@@ -178,6 +178,11 @@
 			"type" : "string",
 			"description" : "stacking"
 		},
+		"icon" : {
+			"type" : "string",
+			"description" : "Optional, custom icons to show in creature window",
+			"format" : "imageFile"
+		},
 		"description" : {
 			"anyOf" : [
 				{ "type" : "string" },

+ 0 - 5
config/spells/timed.json

@@ -278,7 +278,6 @@
 						"type":"core:timed",
 						"bonus":{
 							"levelSpellImmunity":{
-								"addInfo" : 1, //absolute
 								"val" : 3,
 								"type" : "LEVEL_SPELL_IMMUNITY",
 								"valueType" : "INDEPENDENT_MAX",
@@ -417,7 +416,6 @@
 				"targetModifier":{"smart":true},
 				"effects" : {
 					"alwaysMinimumDamage" : {
-						"addInfo" : 0,
 						"val" : 0,
 						"type" : "ALWAYS_MINIMUM_DAMAGE",
 						"valueType" : "INDEPENDENT_MAX",
@@ -719,7 +717,6 @@
 						"duration" : "N_TURNS"
 					},
 					"stacksSpeed" : {
-						"addInfo" : 0,
 						"type" : "STACKS_SPEED",
 						"val" : 2,
 						"duration" : "N_TURNS"
@@ -972,7 +969,6 @@
 				"targetModifier":{"smart":true},
 				"effects" : {
 					"stacksSpeed" : {
-						"addInfo" : 0,
 						"type" : "STACKS_SPEED",
 						"val" : 3,
 						"duration" : "N_TURNS"
@@ -1023,7 +1019,6 @@
 				"targetModifier":{"smart":true},
 				"effects" : {
 					"stacksSpeed" : {
-						"addInfo" : 0,
 						"type" : "STACKS_SPEED",
 						"val" : -25,
 						"valueType" : "PERCENT_TO_ALL",

+ 32 - 4
docs/images/Bonus_System_Nodes.gv

@@ -17,6 +17,7 @@ digraph mygraph {
 					<tr><td>C++ Class: <font face="monospace"><b>CGameState</b></font></td></tr>
 					<tr><td>Global node to which<br/>all map entities are connected</td></tr>
 					<tr><td>Note: Not recruited heroes (such as in tavern)<br/>are not attached to any node</td></tr>
+					<tr><td>Contains global bonuses, global stack experience and difficulty bonuses</td></tr>
 				</table>>
 		]
 		"Team" [
@@ -46,6 +47,7 @@ digraph mygraph {
 					<tr><td>Propagator: <font face="monospace"><b>HERO</b></font></td></tr>
 					<tr><td>C++ Class: <font face="monospace"><b>CGHeroInstance</b></font></td></tr>
 					<tr><td>Represents a hero, either owned by player or in prison.<br/>Bonuses from specialty and secondary skills<br/>are attached directly to this node</td></tr>
+					<tr><td>Contains per-hero global bonuses, specialty bonuses, <br/>primary and secondary skill bonuses, campaign primary skill bonus</td></tr>
 				</table>>
 		]
 		"Combat" [
@@ -94,6 +96,13 @@ digraph mygraph {
 					<tr><td>Army owned by a player.<br/>Mines, Garrisons, Dwellings</td></tr>
 				</table>>
 		]
+		"Owned Object" [
+			label =<<table>
+					<tr><td><b>Owned Object</b></td></tr>
+					<tr><td>Other objects owned by a player, like Lighthouse</td></tr>
+					<tr><td>Contains Flaggable Objects bonuses</td></tr>
+				</table>>
+		]
 	};
 
 	subgraph rankedTopHero {
@@ -103,15 +112,17 @@ digraph mygraph {
 			label =<<table>
 					<tr><td><b>Town</b></td></tr>
 					<tr><td>C++ Class: <font face="monospace"><b>CGTownInstance</b></font></td></tr>
-					<tr><td>Represents a town on map.<br/>Town buildings can provide bonuses to this node<br/>(or propagate them upward)</td></tr>
+					<tr><td>Represents a town on map.</td></tr>
+					<tr><td>Contains town building bonuses</td></tr>
 				</table>>
 		]
 		"Artifact Instance" [
 			fillcolor="#00FFFF80"
 			label =<<table>
 					<tr><td><b>Artifact Instance</b></td></tr>
-					<tr><td>C++ Class: <font face="monospace"><b>CArtifact</b></font></td></tr>
+					<tr><td>C++ Class: <font face="monospace"><b>CArtifactInstance</b></font></td></tr>
 					<tr><td>Represents a particular instance of an artifact<br/> that hero can equip or trade</td></tr>
+					<tr><td>Contains bonuses of spell scrolls and growing artifacts</td></tr>
 				</table>>
 		]
 		"Boat" [
@@ -119,7 +130,8 @@ digraph mygraph {
 			label =<<table>
 					<tr><td><b>Boat</b></td></tr>
 					<tr><td>C++ Class: <font face="monospace"><b>CGBoat</b></font></td></tr>
-					<tr><td>Represents a boat or other type of transport.<br/>Can provide bonuses to boarded hero</td></tr>
+					<tr><td>Represents a boat or other type of transport.</td></tr>
+					<tr><td>Contains bonuses provided to boarded hero</td></tr>
 				</table>>
 		]
 	};
@@ -142,6 +154,7 @@ digraph mygraph {
 					<tr><td>Propagator: <font face="monospace"><b>BATTLE_WIDE</b></font></td></tr>
 					<tr><td>C++ Class: <font face="monospace"><b>BattleInfo</b></font></td></tr>
 					<tr><td>Node that contains both sides of a combat<br/>Anything propagated to this node will affect both sides in combat</td></tr>
+					<tr><td>Contains battlefield and native terrain bonuses</td></tr>
 				</table>>
 		]
 		
@@ -151,6 +164,7 @@ digraph mygraph {
 					<tr><td><b>Creature Type</b></td></tr>
 					<tr><td>C++ Class: <font face="monospace"><b>CCreature</b></font></td></tr>
 					<tr><td>Represents a creature type, such as Pikeman or Archer</td></tr>
+					<tr><td>Contains creature abilities bonuses, stack experience bonuses</td></tr>
 				</table>>
 		]
 		
@@ -160,6 +174,7 @@ digraph mygraph {
 					<tr><td><b>Artifact Type</b></td></tr>
 					<tr><td>C++ Class: <font face="monospace"><b>CArtifact</b></font></td></tr>
 					<tr><td>Represents an artifact type, for example Ring of Life</td></tr>
+					<tr><td>Contains fixed bonuses of artifacts</td></tr>
 				</table>>
 		]
 		
@@ -167,7 +182,7 @@ digraph mygraph {
 			fillcolor="#80808080"
 			label =<<table>
 					<tr><td><b>Artifact Component</b></td></tr>
-					<tr><td>C++ Class: <font face="monospace"><b>CArtifact</b></font></td></tr>
+					<tr><td>C++ Class: <font face="monospace"><b>CArtifactInstance</b></font></td></tr>
 					<tr><td>For combined, non-fused artifacts,<br/>instances of components are attached to instance of combined artifact</td></tr>
 				</table>>
 		]
@@ -177,6 +192,7 @@ digraph mygraph {
 					<tr><td><b>Army</b></td></tr>
 					<tr><td>C++ Class: <font face="monospace"><b>CArmedInstance</b></font></td></tr>
 					<tr><td>Represents any object that can hold army,<br/>such as town, hero, mines, garrisons, wandering monsters</td></tr>
+					<tr><td>Contain anti-magic garrison bonus, faction mixing morale bonus</td></tr>
 				</table>>
 		]
 
@@ -188,6 +204,14 @@ digraph mygraph {
 				</table>>
 		]
 		
+		"Commander" [
+			label =<<table>
+					<tr><td><b>Commander</b></td></tr>
+					<tr><td>C++ Class: <font face="monospace"><b>CCommanderInstance</b></font></td></tr>
+					<tr><td>Represents a hero commander, WoG feature</td></tr>
+				</table>>
+		]
+		
 		"Unit in Combat" [
 			label =<<table>
 					<tr><td><b>Unit in Combat</b></td></tr>
@@ -211,17 +235,21 @@ digraph mygraph {
 	"Player" -> "Town and visiting hero"
 	"Player" -> "Wandering Hero"
 	"Player" -> "Owned Army"
+	"Player" -> "Owned Object"
 	"Town and visiting hero" -> "Town"
 	"Town and visiting hero" -> "Visiting Hero"
 	"Boat" -> "Hero"
 	"Combat" -> "Army"
+	"Army" -> "Commander"
 	"Army" -> "Unit in Army"
 	"Army" -> "Summon in Combat"
 	"Unit in Army" -> "Unit in Combat" 
+	"Commander" -> "Unit in Combat" 
 	"Artifact Type" -> "Artifact Instance"
 	"Artifact Component" -> "Artifact Instance"
 	"Artifact Instance" -> "Hero"
 
+	"Creature Type" -> "Commander" 
 	"Creature Type" -> "Summon in Combat"
 	"Creature Type" -> "Unit in Army"
 

+ 373 - 308
docs/images/Bonus_System_Nodes.svg

@@ -4,471 +4,536 @@
 <!-- Generated by graphviz version 2.43.0 (0)
  -->
 <!-- Title: mygraph Pages: 1 -->
-<svg width="1625pt" height="1440pt"
- viewBox="0.00 0.00 1624.50 1440.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
-<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1436)">
+<svg width="1970pt" height="1540pt"
+ viewBox="0.00 0.00 1970.00 1540.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1536)">
 <title>mygraph</title>
-<polygon fill="white" stroke="transparent" points="-4,4 -4,-1436 1620.5,-1436 1620.5,4 -4,4"/>
+<polygon fill="white" stroke="transparent" points="-4,4 -4,-1536 1966,-1536 1966,4 -4,4"/>
 <!-- Global -->
 <g id="node1" class="node">
 <title>Global</title>
-<polygon fill="#602000" fill-opacity="0.501961" stroke="transparent" points="1365.5,-1432 1047.5,-1432 1047.5,-1283 1365.5,-1283 1365.5,-1432"/>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1050.5,-1407.5 1050.5,-1428.5 1362.5,-1428.5 1362.5,-1407.5 1050.5,-1407.5"/>
-<text text-anchor="start" x="1183" y="-1415.3" font-family="Noto Serif" font-weight="bold" font-size="14.00">Global</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1050.5,-1384.5 1050.5,-1405.5 1362.5,-1405.5 1362.5,-1384.5 1050.5,-1384.5"/>
-<text text-anchor="start" x="1110.5" y="-1392.3" font-family="Noto Serif" font-size="14.00">Propagator: </text>
-<text text-anchor="start" x="1194.5" y="-1392.3" font-family="monospace" font-weight="bold" font-size="14.00">GLOBAL_EFFECT</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1050.5,-1361.5 1050.5,-1382.5 1362.5,-1382.5 1362.5,-1361.5 1050.5,-1361.5"/>
-<text text-anchor="start" x="1131" y="-1369.3" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
-<text text-anchor="start" x="1199" y="-1369.3" font-family="monospace" font-weight="bold" font-size="14.00">CGameState</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1050.5,-1323.5 1050.5,-1359.5 1362.5,-1359.5 1362.5,-1323.5 1050.5,-1323.5"/>
-<text text-anchor="start" x="1135" y="-1345.3" font-family="Noto Serif" font-size="14.00">Global node to which</text>
-<text text-anchor="start" x="1105" y="-1330.3" font-family="Noto Serif" font-size="14.00">all map entities are connected</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1050.5,-1285.5 1050.5,-1321.5 1362.5,-1321.5 1362.5,-1285.5 1050.5,-1285.5"/>
-<text text-anchor="start" x="1053.5" y="-1307.3" font-family="Noto Serif" font-size="14.00">Note: Not recruited heroes (such as in tavern)</text>
-<text text-anchor="start" x="1109" y="-1292.3" font-family="Noto Serif" font-size="14.00">are not attached to any node</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1047.5,-1283 1047.5,-1432 1365.5,-1432 1365.5,-1283 1047.5,-1283"/>
+<polygon fill="#602000" fill-opacity="0.501961" stroke="transparent" points="1495,-1532 1005,-1532 1005,-1360 1495,-1360 1495,-1532"/>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1008,-1508 1008,-1529 1492,-1529 1492,-1508 1008,-1508"/>
+<text text-anchor="start" x="1226.5" y="-1515.8" font-family="Noto Serif" font-weight="bold" font-size="14.00">Global</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1008,-1485 1008,-1506 1492,-1506 1492,-1485 1008,-1485"/>
+<text text-anchor="start" x="1154" y="-1492.8" font-family="Noto Serif" font-size="14.00">Propagator: </text>
+<text text-anchor="start" x="1238" y="-1492.8" font-family="monospace" font-weight="bold" font-size="14.00">GLOBAL_EFFECT</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1008,-1462 1008,-1483 1492,-1483 1492,-1462 1008,-1462"/>
+<text text-anchor="start" x="1174.5" y="-1469.8" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
+<text text-anchor="start" x="1242.5" y="-1469.8" font-family="monospace" font-weight="bold" font-size="14.00">CGameState</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1008,-1424 1008,-1460 1492,-1460 1492,-1424 1008,-1424"/>
+<text text-anchor="start" x="1178.5" y="-1445.8" font-family="Noto Serif" font-size="14.00">Global node to which</text>
+<text text-anchor="start" x="1148.5" y="-1430.8" font-family="Noto Serif" font-size="14.00">all map entities are connected</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1008,-1386 1008,-1422 1492,-1422 1492,-1386 1008,-1386"/>
+<text text-anchor="start" x="1097" y="-1407.8" font-family="Noto Serif" font-size="14.00">Note: Not recruited heroes (such as in tavern)</text>
+<text text-anchor="start" x="1152.5" y="-1392.8" font-family="Noto Serif" font-size="14.00">are not attached to any node</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1008,-1363 1008,-1384 1492,-1384 1492,-1363 1008,-1363"/>
+<text text-anchor="start" x="1011" y="-1369.8" font-family="Noto Serif" font-size="14.00">Contains global bonuses, global stack experience and difficulty bonuses</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1005,-1360 1005,-1532 1495,-1532 1495,-1360 1005,-1360"/>
 </g>
 <!-- Team -->
 <g id="node2" class="node">
 <title>Team</title>
-<polygon fill="#602000" fill-opacity="0.501961" stroke="transparent" points="1256,-1247 1001,-1247 1001,-1121 1256,-1121 1256,-1247"/>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1004.5,-1223 1004.5,-1244 1253.5,-1244 1253.5,-1223 1004.5,-1223"/>
-<text text-anchor="start" x="1109" y="-1230.8" font-family="Noto Serif" font-weight="bold" font-size="14.00">Team</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1004.5,-1200 1004.5,-1221 1253.5,-1221 1253.5,-1200 1004.5,-1200"/>
-<text text-anchor="start" x="1025" y="-1207.8" font-family="Noto Serif" font-size="14.00">Propagator: </text>
-<text text-anchor="start" x="1109" y="-1207.8" font-family="monospace" font-weight="bold" font-size="14.00">TEAM_PROPAGATOR</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1004.5,-1177 1004.5,-1198 1253.5,-1198 1253.5,-1177 1004.5,-1177"/>
-<text text-anchor="start" x="1057.5" y="-1184.8" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
-<text text-anchor="start" x="1125.5" y="-1184.8" font-family="monospace" font-weight="bold" font-size="14.00">TeamState</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1004.5,-1124 1004.5,-1175 1253.5,-1175 1253.5,-1124 1004.5,-1124"/>
-<text text-anchor="start" x="1077.5" y="-1160.8" font-family="Noto Serif" font-size="14.00">Per&#45;team node.</text>
-<text text-anchor="start" x="1007.5" y="-1145.8" font-family="Noto Serif" font-size="14.00">Game will put players without team</text>
-<text text-anchor="start" x="1022" y="-1130.8" font-family="Noto Serif" font-size="14.00">into a team with a single player</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1001,-1121 1001,-1247 1256,-1247 1256,-1121 1001,-1121"/>
+<polygon fill="#602000" fill-opacity="0.501961" stroke="transparent" points="1299.5,-1324 1044.5,-1324 1044.5,-1198 1299.5,-1198 1299.5,-1324"/>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1048,-1300 1048,-1321 1297,-1321 1297,-1300 1048,-1300"/>
+<text text-anchor="start" x="1152.5" y="-1307.8" font-family="Noto Serif" font-weight="bold" font-size="14.00">Team</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1048,-1277 1048,-1298 1297,-1298 1297,-1277 1048,-1277"/>
+<text text-anchor="start" x="1068.5" y="-1284.8" font-family="Noto Serif" font-size="14.00">Propagator: </text>
+<text text-anchor="start" x="1152.5" y="-1284.8" font-family="monospace" font-weight="bold" font-size="14.00">TEAM_PROPAGATOR</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1048,-1254 1048,-1275 1297,-1275 1297,-1254 1048,-1254"/>
+<text text-anchor="start" x="1101" y="-1261.8" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
+<text text-anchor="start" x="1169" y="-1261.8" font-family="monospace" font-weight="bold" font-size="14.00">TeamState</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1048,-1201 1048,-1252 1297,-1252 1297,-1201 1048,-1201"/>
+<text text-anchor="start" x="1121" y="-1237.8" font-family="Noto Serif" font-size="14.00">Per&#45;team node.</text>
+<text text-anchor="start" x="1051" y="-1222.8" font-family="Noto Serif" font-size="14.00">Game will put players without team</text>
+<text text-anchor="start" x="1065.5" y="-1207.8" font-family="Noto Serif" font-size="14.00">into a team with a single player</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1044.5,-1198 1044.5,-1324 1299.5,-1324 1299.5,-1198 1044.5,-1198"/>
 </g>
 <!-- Global&#45;&gt;Team -->
 <g id="edge1" class="edge">
 <title>Global&#45;&gt;Team</title>
-<path fill="none" stroke="black" d="M1172.97,-1282.77C1169,-1274.04 1164.95,-1265.14 1160.99,-1256.43"/>
-<polygon fill="black" stroke="black" points="1164.07,-1254.74 1156.74,-1247.09 1157.69,-1257.64 1164.07,-1254.74"/>
+<path fill="none" stroke="black" d="M1213.74,-1359.93C1209.97,-1351.09 1206.17,-1342.16 1202.47,-1333.49"/>
+<polygon fill="black" stroke="black" points="1205.65,-1332.03 1198.51,-1324.2 1199.21,-1334.77 1205.65,-1332.03"/>
 </g>
 <!-- Neutral Army -->
 <g id="node9" class="node">
 <title>Neutral Army</title>
-<polygon fill="#808080" fill-opacity="0.501961" stroke="transparent" points="1616.5,-614 1336.5,-614 1336.5,-549 1616.5,-549 1616.5,-614"/>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1339.5,-589.5 1339.5,-610.5 1613.5,-610.5 1613.5,-589.5 1339.5,-589.5"/>
-<text text-anchor="start" x="1427" y="-597.3" font-family="Noto Serif" font-weight="bold" font-size="14.00">Neutral Army</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1339.5,-551.5 1339.5,-587.5 1613.5,-587.5 1613.5,-551.5 1339.5,-551.5"/>
-<text text-anchor="start" x="1343" y="-573.3" font-family="Noto Serif" font-size="14.00">Any army that is not owned by a player</text>
-<text text-anchor="start" x="1342.5" y="-558.3" font-family="Noto Serif" font-size="14.00">Wandering monsters, Banks, Events, etc</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1336.5,-549 1336.5,-614 1616.5,-614 1616.5,-549 1336.5,-549"/>
+<polygon fill="#808080" fill-opacity="0.501961" stroke="transparent" points="1962,-679 1682,-679 1682,-614 1962,-614 1962,-679"/>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1685,-654.5 1685,-675.5 1959,-675.5 1959,-654.5 1685,-654.5"/>
+<text text-anchor="start" x="1772.5" y="-662.3" font-family="Noto Serif" font-weight="bold" font-size="14.00">Neutral Army</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1685,-616.5 1685,-652.5 1959,-652.5 1959,-616.5 1685,-616.5"/>
+<text text-anchor="start" x="1688.5" y="-638.3" font-family="Noto Serif" font-size="14.00">Any army that is not owned by a player</text>
+<text text-anchor="start" x="1688" y="-623.3" font-family="Noto Serif" font-size="14.00">Wandering monsters, Banks, Events, etc</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1682,-614 1682,-679 1962,-679 1962,-614 1682,-614"/>
 </g>
 <!-- Global&#45;&gt;Neutral Army -->
 <g id="edge2" class="edge">
 <title>Global&#45;&gt;Neutral Army</title>
-<path fill="none" stroke="black" d="M1292.33,-1282.85C1354.8,-1220.68 1428.5,-1125.59 1428.5,-1023 1428.5,-1023 1428.5,-1023 1428.5,-855 1428.5,-771.96 1450.39,-676.82 1464.6,-623.78"/>
-<polygon fill="black" stroke="black" points="1468,-624.63 1467.25,-614.07 1461.24,-622.8 1468,-624.63"/>
+<path fill="none" stroke="black" d="M1495.07,-1396.94C1650.32,-1350.63 1822,-1262.65 1822,-1100 1822,-1100 1822,-1100 1822,-932 1822,-845.69 1822,-744.66 1822,-689.21"/>
+<polygon fill="black" stroke="black" points="1825.5,-689.07 1822,-679.07 1818.5,-689.07 1825.5,-689.07"/>
 </g>
 <!-- Player -->
 <g id="node3" class="node">
 <title>Player</title>
-<polygon fill="#602000" fill-opacity="0.501961" stroke="transparent" points="1173,-1085 936,-1085 936,-959 1173,-959 1173,-1085"/>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="939.5,-1061 939.5,-1082 1170.5,-1082 1170.5,-1061 939.5,-1061"/>
-<text text-anchor="start" x="1031.5" y="-1068.8" font-family="Noto Serif" font-weight="bold" font-size="14.00">Player</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="939.5,-1038 939.5,-1059 1170.5,-1059 1170.5,-1038 939.5,-1038"/>
-<text text-anchor="start" x="942.5" y="-1045.8" font-family="Noto Serif" font-size="14.00">Propagator: </text>
-<text text-anchor="start" x="1026.5" y="-1045.8" font-family="monospace" font-weight="bold" font-size="14.00">PLAYER_PROPAGATOR</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="939.5,-1015 939.5,-1036 1170.5,-1036 1170.5,-1015 939.5,-1015"/>
-<text text-anchor="start" x="971" y="-1022.8" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
-<text text-anchor="start" x="1039" y="-1022.8" font-family="monospace" font-weight="bold" font-size="14.00">CPlayerState</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="939.5,-962 939.5,-1013 1170.5,-1013 1170.5,-962 939.5,-962"/>
-<text text-anchor="start" x="999" y="-998.8" font-family="Noto Serif" font-size="14.00">Per&#45;player team.</text>
-<text text-anchor="start" x="956.5" y="-983.8" font-family="Noto Serif" font-size="14.00">All objects owned by a player</text>
-<text text-anchor="start" x="988" y="-968.8" font-family="Noto Serif" font-size="14.00">belong to such node</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="936,-959 936,-1085 1173,-1085 1173,-959 936,-959"/>
+<polygon fill="#602000" fill-opacity="0.501961" stroke="transparent" points="796.5,-1162 559.5,-1162 559.5,-1036 796.5,-1036 796.5,-1162"/>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="563,-1138 563,-1159 794,-1159 794,-1138 563,-1138"/>
+<text text-anchor="start" x="655" y="-1145.8" font-family="Noto Serif" font-weight="bold" font-size="14.00">Player</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="563,-1115 563,-1136 794,-1136 794,-1115 563,-1115"/>
+<text text-anchor="start" x="566" y="-1122.8" font-family="Noto Serif" font-size="14.00">Propagator: </text>
+<text text-anchor="start" x="650" y="-1122.8" font-family="monospace" font-weight="bold" font-size="14.00">PLAYER_PROPAGATOR</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="563,-1092 563,-1113 794,-1113 794,-1092 563,-1092"/>
+<text text-anchor="start" x="594.5" y="-1099.8" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
+<text text-anchor="start" x="662.5" y="-1099.8" font-family="monospace" font-weight="bold" font-size="14.00">CPlayerState</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="563,-1039 563,-1090 794,-1090 794,-1039 563,-1039"/>
+<text text-anchor="start" x="622.5" y="-1075.8" font-family="Noto Serif" font-size="14.00">Per&#45;player team.</text>
+<text text-anchor="start" x="580" y="-1060.8" font-family="Noto Serif" font-size="14.00">All objects owned by a player</text>
+<text text-anchor="start" x="611.5" y="-1045.8" font-family="Noto Serif" font-size="14.00">belong to such node</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="559.5,-1036 559.5,-1162 796.5,-1162 796.5,-1036 559.5,-1036"/>
 </g>
 <!-- Team&#45;&gt;Player -->
 <g id="edge3" class="edge">
 <title>Team&#45;&gt;Player</title>
-<path fill="none" stroke="black" d="M1099.68,-1120.68C1095.67,-1112.02 1091.53,-1103.07 1087.46,-1094.26"/>
-<polygon fill="black" stroke="black" points="1090.62,-1092.76 1083.24,-1085.15 1084.26,-1095.69 1090.62,-1092.76"/>
+<path fill="none" stroke="black" d="M1044.46,-1218.69C971.28,-1194.99 879.66,-1165.31 806.34,-1141.57"/>
+<polygon fill="black" stroke="black" points="807.17,-1138.16 796.58,-1138.4 805.01,-1144.82 807.17,-1138.16"/>
 </g>
 <!-- Wandering Hero -->
 <g id="node8" class="node">
 <title>Wandering Hero</title>
-<polygon fill="#808080" fill-opacity="0.501961" stroke="transparent" points="784.5,-614 548.5,-614 548.5,-549 784.5,-549 784.5,-614"/>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="551.5,-589.5 551.5,-610.5 781.5,-610.5 781.5,-589.5 551.5,-589.5"/>
-<text text-anchor="start" x="607.5" y="-597.3" font-family="Noto Serif" font-weight="bold" font-size="14.00">Wandering Hero</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="551.5,-551.5 551.5,-587.5 781.5,-587.5 781.5,-551.5 551.5,-551.5"/>
-<text text-anchor="start" x="592.5" y="-573.3" font-family="Noto Serif" font-size="14.00">Hero that is currently</text>
-<text text-anchor="start" x="554.5" y="-558.3" font-family="Noto Serif" font-size="14.00">moving on map, outside of towns</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="548.5,-549 548.5,-614 784.5,-614 784.5,-549 548.5,-549"/>
+<polygon fill="#808080" fill-opacity="0.501961" stroke="transparent" points="1664,-679 1428,-679 1428,-614 1664,-614 1664,-679"/>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1431,-654.5 1431,-675.5 1661,-675.5 1661,-654.5 1431,-654.5"/>
+<text text-anchor="start" x="1487" y="-662.3" font-family="Noto Serif" font-weight="bold" font-size="14.00">Wandering Hero</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1431,-616.5 1431,-652.5 1661,-652.5 1661,-616.5 1431,-616.5"/>
+<text text-anchor="start" x="1472" y="-638.3" font-family="Noto Serif" font-size="14.00">Hero that is currently</text>
+<text text-anchor="start" x="1434" y="-623.3" font-family="Noto Serif" font-size="14.00">moving on map, outside of towns</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1428,-614 1428,-679 1664,-679 1664,-614 1428,-614"/>
 </g>
 <!-- Player&#45;&gt;Wandering Hero -->
 <g id="edge5" class="edge">
 <title>Player&#45;&gt;Wandering Hero</title>
-<path fill="none" stroke="black" d="M1019.12,-958.91C1013.14,-947.18 1007.32,-934.87 1002.5,-923 953.37,-801.94 993.71,-733.83 893.5,-650 884.76,-642.69 841.02,-629.05 794.23,-615.94"/>
-<polygon fill="black" stroke="black" points="795.1,-612.55 784.53,-613.24 793.22,-619.29 795.1,-612.55"/>
+<path fill="none" stroke="black" d="M796.56,-1096.14C1064.93,-1090.69 1700.65,-1070.42 1765,-1000 1851.75,-905.07 1786.66,-810.2 1696,-719 1682.06,-704.97 1664.83,-693.33 1647.07,-683.81"/>
+<polygon fill="black" stroke="black" points="1648.24,-680.47 1637.75,-679.01 1645.04,-686.7 1648.24,-680.47"/>
 </g>
 <!-- Owned Army -->
 <g id="node10" class="node">
 <title>Owned Army</title>
-<polygon fill="#808080" fill-opacity="0.501961" stroke="transparent" points="199,-614 0,-614 0,-549 199,-549 199,-614"/>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="3.5,-589.5 3.5,-610.5 196.5,-610.5 196.5,-589.5 3.5,-589.5"/>
-<text text-anchor="start" x="53" y="-597.3" font-family="Noto Serif" font-weight="bold" font-size="14.00">Owned Army</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="3.5,-551.5 3.5,-587.5 196.5,-587.5 196.5,-551.5 3.5,-551.5"/>
-<text text-anchor="start" x="16.5" y="-573.3" font-family="Noto Serif" font-size="14.00">Army owned by a player.</text>
-<text text-anchor="start" x="6.5" y="-558.3" font-family="Noto Serif" font-size="14.00">Mines, Garrisons, Dwellings</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="0,-549 0,-614 199,-614 199,-549 0,-549"/>
+<polygon fill="#808080" fill-opacity="0.501961" stroke="transparent" points="555.5,-679 356.5,-679 356.5,-614 555.5,-614 555.5,-679"/>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="360,-654.5 360,-675.5 553,-675.5 553,-654.5 360,-654.5"/>
+<text text-anchor="start" x="409.5" y="-662.3" font-family="Noto Serif" font-weight="bold" font-size="14.00">Owned Army</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="360,-616.5 360,-652.5 553,-652.5 553,-616.5 360,-616.5"/>
+<text text-anchor="start" x="373" y="-638.3" font-family="Noto Serif" font-size="14.00">Army owned by a player.</text>
+<text text-anchor="start" x="363" y="-623.3" font-family="Noto Serif" font-size="14.00">Mines, Garrisons, Dwellings</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="356.5,-614 356.5,-679 555.5,-679 555.5,-614 356.5,-614"/>
 </g>
 <!-- Player&#45;&gt;Owned Army -->
 <g id="edge6" class="edge">
 <title>Player&#45;&gt;Owned Army</title>
-<path fill="none" stroke="black" d="M935.73,-1017.81C688.35,-1009.92 135.99,-985.45 79.5,-923 4.57,-840.16 50.92,-694.54 80.5,-623.62"/>
-<polygon fill="black" stroke="black" points="83.85,-624.7 84.55,-614.13 77.41,-621.95 83.85,-624.7"/>
+<path fill="none" stroke="black" d="M559.38,-1065.77C526.78,-1051.05 494.95,-1029.97 475,-1000 411.22,-904.22 430.78,-759.31 445.74,-688.86"/>
+<polygon fill="black" stroke="black" points="449.17,-689.54 447.9,-679.03 442.33,-688.04 449.17,-689.54"/>
+</g>
+<!-- Owned Object -->
+<g id="node11" class="node">
+<title>Owned Object</title>
+<polygon fill="#602000" fill-opacity="0.501961" stroke="transparent" points="338,-683 0,-683 0,-610 338,-610 338,-683"/>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="3,-658.5 3,-679.5 335,-679.5 335,-658.5 3,-658.5"/>
+<text text-anchor="start" x="119" y="-666.3" font-family="Noto Serif" font-weight="bold" font-size="14.00">Owned Object</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="3,-635.5 3,-656.5 335,-656.5 335,-635.5 3,-635.5"/>
+<text text-anchor="start" x="6" y="-642.3" font-family="Noto Serif" font-size="14.00">Other objects owned by a player, like Lighthouse</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="3,-612.5 3,-633.5 335,-633.5 335,-612.5 3,-612.5"/>
+<text text-anchor="start" x="50" y="-619.3" font-family="Noto Serif" font-size="14.00">Contains Flaggable Objects bonuses</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="0,-610 0,-683 338,-683 338,-610 0,-610"/>
+</g>
+<!-- Player&#45;&gt;Owned Object -->
+<g id="edge7" class="edge">
+<title>Player&#45;&gt;Owned Object</title>
+<path fill="none" stroke="black" d="M559.41,-1066.51C515.59,-1051.13 467.31,-1029.44 429,-1000 316.53,-913.58 229.92,-765.96 191.2,-692.2"/>
+<polygon fill="black" stroke="black" points="194.22,-690.41 186.5,-683.15 188.01,-693.64 194.22,-690.41"/>
 </g>
 <!-- Town and visiting hero -->
-<g id="node14" class="node">
+<g id="node15" class="node">
 <title>Town and visiting hero</title>
-<polygon fill="#602000" fill-opacity="0.501961" stroke="transparent" points="1398,-923 1011,-923 1011,-789 1398,-789 1398,-923"/>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1014.5,-899 1014.5,-920 1395.5,-920 1395.5,-899 1014.5,-899"/>
-<text text-anchor="start" x="1122" y="-906.8" font-family="Noto Serif" font-weight="bold" font-size="14.00">Town and Visiting Hero</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1014.5,-876 1014.5,-897 1395.5,-897 1395.5,-876 1014.5,-876"/>
-<text text-anchor="start" x="1063.5" y="-883.8" font-family="Noto Serif" font-size="14.00">Propagator: </text>
-<text text-anchor="start" x="1147.5" y="-883.8" font-family="monospace" font-weight="bold" font-size="14.00">VISITED_TOWN_AND_VISITOR</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1014.5,-853 1014.5,-874 1395.5,-874 1395.5,-853 1014.5,-853"/>
-<text text-anchor="start" x="1088" y="-860.8" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
-<text text-anchor="start" x="1156" y="-860.8" font-family="monospace" font-weight="bold" font-size="14.00">CTownAndVisitingHero</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1014.5,-815 1014.5,-851 1395.5,-851 1395.5,-815 1014.5,-815"/>
-<text text-anchor="start" x="1105.5" y="-836.8" font-family="Noto Serif" font-size="14.00">Helper node that exists solely</text>
-<text text-anchor="start" x="1029" y="-821.8" font-family="Noto Serif" font-size="14.00">to propagate bonuses to both town and visiting hero</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1014.5,-792 1014.5,-813 1395.5,-813 1395.5,-792 1014.5,-792"/>
-<text text-anchor="start" x="1017.5" y="-798.8" font-family="Noto Serif" font-size="14.00">Note: Neutral towns are attached to global node instead</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1011,-789 1011,-923 1398,-923 1398,-789 1011,-789"/>
+<polygon fill="#602000" fill-opacity="0.501961" stroke="transparent" points="871.5,-1000 484.5,-1000 484.5,-866 871.5,-866 871.5,-1000"/>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="488,-976 488,-997 869,-997 869,-976 488,-976"/>
+<text text-anchor="start" x="595.5" y="-983.8" font-family="Noto Serif" font-weight="bold" font-size="14.00">Town and Visiting Hero</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="488,-953 488,-974 869,-974 869,-953 488,-953"/>
+<text text-anchor="start" x="537" y="-960.8" font-family="Noto Serif" font-size="14.00">Propagator: </text>
+<text text-anchor="start" x="621" y="-960.8" font-family="monospace" font-weight="bold" font-size="14.00">VISITED_TOWN_AND_VISITOR</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="488,-930 488,-951 869,-951 869,-930 488,-930"/>
+<text text-anchor="start" x="561.5" y="-937.8" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
+<text text-anchor="start" x="629.5" y="-937.8" font-family="monospace" font-weight="bold" font-size="14.00">CTownAndVisitingHero</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="488,-892 488,-928 869,-928 869,-892 488,-892"/>
+<text text-anchor="start" x="579" y="-913.8" font-family="Noto Serif" font-size="14.00">Helper node that exists solely</text>
+<text text-anchor="start" x="502.5" y="-898.8" font-family="Noto Serif" font-size="14.00">to propagate bonuses to both town and visiting hero</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="488,-869 488,-890 869,-890 869,-869 488,-869"/>
+<text text-anchor="start" x="491" y="-875.8" font-family="Noto Serif" font-size="14.00">Note: Neutral towns are attached to global node instead</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="484.5,-866 484.5,-1000 871.5,-1000 871.5,-866 484.5,-866"/>
 </g>
 <!-- Player&#45;&gt;Town and visiting hero -->
 <g id="edge4" class="edge">
 <title>Player&#45;&gt;Town and visiting hero</title>
-<path fill="none" stroke="black" d="M1111.21,-959C1119.65,-949.77 1128.43,-940.17 1137.07,-930.73"/>
-<polygon fill="black" stroke="black" points="1139.79,-932.93 1143.96,-923.19 1134.63,-928.21 1139.79,-932.93"/>
+<path fill="none" stroke="black" d="M678,-1036C678,-1027.59 678,-1018.88 678,-1010.26"/>
+<polygon fill="black" stroke="black" points="681.5,-1010.19 678,-1000.19 674.5,-1010.19 681.5,-1010.19"/>
 </g>
 <!-- Hero -->
 <g id="node4" class="node">
 <title>Hero</title>
-<polygon fill="#808080" fill-opacity="0.501961" stroke="transparent" points="1060.5,-513 678.5,-513 678.5,-387 1060.5,-387 1060.5,-513"/>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="681.5,-489 681.5,-510 1057.5,-510 1057.5,-489 681.5,-489"/>
-<text text-anchor="start" x="851.5" y="-496.8" font-family="Noto Serif" font-weight="bold" font-size="14.00">Hero</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="681.5,-466 681.5,-487 1057.5,-487 1057.5,-466 681.5,-466"/>
-<text text-anchor="start" x="810.5" y="-473.8" font-family="Noto Serif" font-size="14.00">Propagator: </text>
-<text text-anchor="start" x="894.5" y="-473.8" font-family="monospace" font-weight="bold" font-size="14.00">HERO</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="681.5,-443 681.5,-464 1057.5,-464 1057.5,-443 681.5,-443"/>
-<text text-anchor="start" x="777.5" y="-450.8" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
-<text text-anchor="start" x="845.5" y="-450.8" font-family="monospace" font-weight="bold" font-size="14.00">CGHeroInstance</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="681.5,-390 681.5,-441 1057.5,-441 1057.5,-390 681.5,-390"/>
-<text text-anchor="start" x="684.5" y="-426.8" font-family="Noto Serif" font-size="14.00">Represents a hero, either owned by player or in prison.</text>
-<text text-anchor="start" x="722" y="-411.8" font-family="Noto Serif" font-size="14.00">Bonuses from specialty and secondary skills</text>
-<text text-anchor="start" x="758" y="-396.8" font-family="Noto Serif" font-size="14.00">are attached directly to this node</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="678.5,-387 678.5,-513 1060.5,-513 1060.5,-387 678.5,-387"/>
+<polygon fill="#808080" fill-opacity="0.501961" stroke="transparent" points="1435.5,-574 966.5,-574 966.5,-410 1435.5,-410 1435.5,-574"/>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="970,-550 970,-571 1433,-571 1433,-550 970,-550"/>
+<text text-anchor="start" x="1183.5" y="-557.8" font-family="Noto Serif" font-weight="bold" font-size="14.00">Hero</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="970,-527 970,-548 1433,-548 1433,-527 970,-527"/>
+<text text-anchor="start" x="1142.5" y="-534.8" font-family="Noto Serif" font-size="14.00">Propagator: </text>
+<text text-anchor="start" x="1226.5" y="-534.8" font-family="monospace" font-weight="bold" font-size="14.00">HERO</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="970,-504 970,-525 1433,-525 1433,-504 970,-504"/>
+<text text-anchor="start" x="1109.5" y="-511.8" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
+<text text-anchor="start" x="1177.5" y="-511.8" font-family="monospace" font-weight="bold" font-size="14.00">CGHeroInstance</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="970,-451 970,-502 1433,-502 1433,-451 970,-451"/>
+<text text-anchor="start" x="1016.5" y="-487.8" font-family="Noto Serif" font-size="14.00">Represents a hero, either owned by player or in prison.</text>
+<text text-anchor="start" x="1054" y="-472.8" font-family="Noto Serif" font-size="14.00">Bonuses from specialty and secondary skills</text>
+<text text-anchor="start" x="1090" y="-457.8" font-family="Noto Serif" font-size="14.00">are attached directly to this node</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="970,-413 970,-449 1433,-449 1433,-413 970,-413"/>
+<text text-anchor="start" x="1023.5" y="-434.8" font-family="Noto Serif" font-size="14.00">Contains per&#45;hero global bonuses, specialty bonuses, </text>
+<text text-anchor="start" x="973" y="-419.8" font-family="Noto Serif" font-size="14.00">primary and secondary skill bonuses, campaign primary skill bonus</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="966.5,-410 966.5,-574 1435.5,-574 1435.5,-410 966.5,-410"/>
 </g>
 <!-- Army -->
-<g id="node18" class="node">
+<g id="node19" class="node">
 <title>Army</title>
-<polygon fill="#602000" fill-opacity="0.501961" stroke="transparent" points="1071,-351 668,-351 668,-263 1071,-263 1071,-351"/>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="671.5,-327 671.5,-348 1068.5,-348 1068.5,-327 671.5,-327"/>
-<text text-anchor="start" x="850" y="-334.8" font-family="Noto Serif" font-weight="bold" font-size="14.00">Army</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="671.5,-304 671.5,-325 1068.5,-325 1068.5,-304 671.5,-304"/>
-<text text-anchor="start" x="778" y="-311.8" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
-<text text-anchor="start" x="846" y="-311.8" font-family="monospace" font-weight="bold" font-size="14.00">CArmedInstance</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="671.5,-266 671.5,-302 1068.5,-302 1068.5,-266 671.5,-266"/>
-<text text-anchor="start" x="729" y="-287.8" font-family="Noto Serif" font-size="14.00">Represents any object that can hold army,</text>
-<text text-anchor="start" x="674.5" y="-272.8" font-family="Noto Serif" font-size="14.00">such as town, hero, mines, garrisons, wandering monsters</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="668,-263 668,-351 1071,-351 1071,-263 668,-263"/>
+<polygon fill="#602000" fill-opacity="0.501961" stroke="transparent" points="1052,-374 604,-374 604,-263 1052,-263 1052,-374"/>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="607,-349.5 607,-370.5 1049,-370.5 1049,-349.5 607,-349.5"/>
+<text text-anchor="start" x="808" y="-357.3" font-family="Noto Serif" font-weight="bold" font-size="14.00">Army</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="607,-326.5 607,-347.5 1049,-347.5 1049,-326.5 607,-326.5"/>
+<text text-anchor="start" x="736" y="-334.3" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
+<text text-anchor="start" x="804" y="-334.3" font-family="monospace" font-weight="bold" font-size="14.00">CArmedInstance</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="607,-288.5 607,-324.5 1049,-324.5 1049,-288.5 607,-288.5"/>
+<text text-anchor="start" x="687" y="-310.3" font-family="Noto Serif" font-size="14.00">Represents any object that can hold army,</text>
+<text text-anchor="start" x="632.5" y="-295.3" font-family="Noto Serif" font-size="14.00">such as town, hero, mines, garrisons, wandering monsters</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="607,-265.5 607,-286.5 1049,-286.5 1049,-265.5 607,-265.5"/>
+<text text-anchor="start" x="610" y="-272.3" font-family="Noto Serif" font-size="14.00">Contain anti&#45;magic garrison bonus, faction mixing morale bonus</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="604,-263 604,-374 1052,-374 1052,-263 604,-263"/>
 </g>
 <!-- Hero&#45;&gt;Army -->
-<g id="edge26" class="edge">
+<g id="edge30" class="edge">
 <title>Hero&#45;&gt;Army</title>
-<path fill="none" stroke="black" d="M869.5,-386.74C869.5,-378.34 869.5,-369.78 869.5,-361.55"/>
-<polygon fill="black" stroke="black" points="873,-361.33 869.5,-351.33 866,-361.33 873,-361.33"/>
+<path fill="none" stroke="black" d="M1024.6,-409.9C1001.41,-399.23 978.02,-388.47 955.85,-378.28"/>
+<polygon fill="black" stroke="black" points="957.31,-375.1 946.76,-374.1 954.38,-381.46 957.31,-375.1"/>
 </g>
 <!-- Combat -->
 <g id="node5" class="node">
 <title>Combat</title>
-<polygon fill="#602000" fill-opacity="0.501961" stroke="transparent" points="660.5,-505.5 210.5,-505.5 210.5,-394.5 660.5,-394.5 660.5,-505.5"/>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="213.5,-481 213.5,-502 657.5,-502 657.5,-481 213.5,-481"/>
-<text text-anchor="start" x="408" y="-488.8" font-family="Noto Serif" font-weight="bold" font-size="14.00">Combat</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="213.5,-458 213.5,-479 657.5,-479 657.5,-458 213.5,-458"/>
-<text text-anchor="start" x="348" y="-465.8" font-family="Noto Serif" font-size="14.00">Propagator: </text>
-<text text-anchor="start" x="432" y="-465.8" font-family="monospace" font-weight="bold" font-size="14.00">BATTLE_WIDE</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="213.5,-435 213.5,-456 657.5,-456 657.5,-435 213.5,-435"/>
-<text text-anchor="start" x="360" y="-442.8" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
-<text text-anchor="start" x="428" y="-442.8" font-family="monospace" font-weight="bold" font-size="14.00">BattleInfo</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="213.5,-397 213.5,-433 657.5,-433 657.5,-397 213.5,-397"/>
-<text text-anchor="start" x="295" y="-418.8" font-family="Noto Serif" font-size="14.00">Node that contains both sides of a combat</text>
-<text text-anchor="start" x="216.5" y="-403.8" font-family="Noto Serif" font-size="14.00">Anything propagated to this node will affect both sides in combat</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="210.5,-394.5 210.5,-505.5 660.5,-505.5 660.5,-394.5 210.5,-394.5"/>
+<polygon fill="#602000" fill-opacity="0.501961" stroke="transparent" points="800,-559 350,-559 350,-425 800,-425 800,-559"/>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="353,-535 353,-556 797,-556 797,-535 353,-535"/>
+<text text-anchor="start" x="547.5" y="-542.8" font-family="Noto Serif" font-weight="bold" font-size="14.00">Combat</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="353,-512 353,-533 797,-533 797,-512 353,-512"/>
+<text text-anchor="start" x="487.5" y="-519.8" font-family="Noto Serif" font-size="14.00">Propagator: </text>
+<text text-anchor="start" x="571.5" y="-519.8" font-family="monospace" font-weight="bold" font-size="14.00">BATTLE_WIDE</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="353,-489 353,-510 797,-510 797,-489 353,-489"/>
+<text text-anchor="start" x="499.5" y="-496.8" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
+<text text-anchor="start" x="567.5" y="-496.8" font-family="monospace" font-weight="bold" font-size="14.00">BattleInfo</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="353,-451 353,-487 797,-487 797,-451 353,-451"/>
+<text text-anchor="start" x="434.5" y="-472.8" font-family="Noto Serif" font-size="14.00">Node that contains both sides of a combat</text>
+<text text-anchor="start" x="356" y="-457.8" font-family="Noto Serif" font-size="14.00">Anything propagated to this node will affect both sides in combat</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="353,-428 353,-449 797,-449 797,-428 353,-428"/>
+<text text-anchor="start" x="417" y="-434.8" font-family="Noto Serif" font-size="14.00">Contains battlefield and native terrain bonuses</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="350,-425 350,-559 800,-559 800,-425 350,-425"/>
 </g>
 <!-- Combat&#45;&gt;Army -->
-<g id="edge10" class="edge">
+<g id="edge11" class="edge">
 <title>Combat&#45;&gt;Army</title>
-<path fill="none" stroke="black" d="M603.3,-394.49C644.28,-381.17 687.74,-367.05 727.4,-354.17"/>
-<polygon fill="black" stroke="black" points="728.7,-357.42 737.13,-351.01 726.53,-350.77 728.7,-357.42"/>
+<path fill="none" stroke="black" d="M672.45,-424.94C694.33,-410.11 717.43,-394.45 738.94,-379.87"/>
+<polygon fill="black" stroke="black" points="741.1,-382.63 747.42,-374.13 737.17,-376.84 741.1,-382.63"/>
 </g>
 <!-- Visiting Hero -->
 <g id="node6" class="node">
 <title>Visiting Hero</title>
-<polygon fill="#808080" fill-opacity="0.501961" stroke="transparent" points="1012,-614 803,-614 803,-549 1012,-549 1012,-614"/>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="806.5,-589.5 806.5,-610.5 1009.5,-610.5 1009.5,-589.5 806.5,-589.5"/>
-<text text-anchor="start" x="861" y="-597.3" font-family="Noto Serif" font-weight="bold" font-size="14.00">Visiting Hero</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="806.5,-551.5 806.5,-587.5 1009.5,-587.5 1009.5,-551.5 806.5,-551.5"/>
-<text text-anchor="start" x="834" y="-573.3" font-family="Noto Serif" font-size="14.00">Hero that is currently</text>
-<text text-anchor="start" x="809.5" y="-558.3" font-family="Noto Serif" font-size="14.00">visiting owned or allied town</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="803,-549 803,-614 1012,-614 1012,-549 803,-549"/>
+<polygon fill="#808080" fill-opacity="0.501961" stroke="transparent" points="1371.5,-679 1162.5,-679 1162.5,-614 1371.5,-614 1371.5,-679"/>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1166,-654.5 1166,-675.5 1369,-675.5 1369,-654.5 1166,-654.5"/>
+<text text-anchor="start" x="1220.5" y="-662.3" font-family="Noto Serif" font-weight="bold" font-size="14.00">Visiting Hero</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1166,-616.5 1166,-652.5 1369,-652.5 1369,-616.5 1166,-616.5"/>
+<text text-anchor="start" x="1193.5" y="-638.3" font-family="Noto Serif" font-size="14.00">Hero that is currently</text>
+<text text-anchor="start" x="1169" y="-623.3" font-family="Noto Serif" font-size="14.00">visiting owned or allied town</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1162.5,-614 1162.5,-679 1371.5,-679 1371.5,-614 1162.5,-614"/>
 </g>
 <!-- Visiting Hero&#45;&gt;Hero -->
-<g id="edge23" class="edge">
+<g id="edge27" class="edge">
 <title>Visiting Hero&#45;&gt;Hero</title>
-<path fill="none" stroke="black" d="M898.2,-548.82C895.88,-540.9 893.29,-532.08 890.65,-523.07"/>
-<polygon fill="black" stroke="black" points="893.94,-521.86 887.77,-513.25 887.22,-523.83 893.94,-521.86"/>
+<path fill="none" stroke="black" d="M1253.32,-613.9C1249.37,-604.77 1244.85,-594.32 1240.15,-583.46"/>
+<polygon fill="black" stroke="black" points="1243.36,-582.06 1236.18,-574.28 1236.94,-584.84 1243.36,-582.06"/>
 </g>
 <!-- Garrisoned Hero -->
 <g id="node7" class="node">
 <title>Garrisoned Hero</title>
-<polygon fill="#808080" fill-opacity="0.501961" stroke="transparent" points="1280.5,-614 1030.5,-614 1030.5,-549 1280.5,-549 1280.5,-614"/>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1033.5,-589.5 1033.5,-610.5 1277.5,-610.5 1277.5,-589.5 1033.5,-589.5"/>
-<text text-anchor="start" x="1095.5" y="-597.3" font-family="Noto Serif" font-weight="bold" font-size="14.00">Garrisoned Hero</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1033.5,-551.5 1033.5,-587.5 1277.5,-587.5 1277.5,-551.5 1033.5,-551.5"/>
-<text text-anchor="start" x="1081.5" y="-573.3" font-family="Noto Serif" font-size="14.00">Hero that is currently</text>
-<text text-anchor="start" x="1036.5" y="-558.3" font-family="Noto Serif" font-size="14.00">placed in a garrison of owned town</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1030.5,-549 1030.5,-614 1280.5,-614 1280.5,-549 1030.5,-549"/>
+<polygon fill="#808080" fill-opacity="0.501961" stroke="transparent" points="1106,-679 856,-679 856,-614 1106,-614 1106,-679"/>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="859,-654.5 859,-675.5 1103,-675.5 1103,-654.5 859,-654.5"/>
+<text text-anchor="start" x="921" y="-662.3" font-family="Noto Serif" font-weight="bold" font-size="14.00">Garrisoned Hero</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="859,-616.5 859,-652.5 1103,-652.5 1103,-616.5 859,-616.5"/>
+<text text-anchor="start" x="907" y="-638.3" font-family="Noto Serif" font-size="14.00">Hero that is currently</text>
+<text text-anchor="start" x="862" y="-623.3" font-family="Noto Serif" font-size="14.00">placed in a garrison of owned town</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="856,-614 856,-679 1106,-679 1106,-614 856,-614"/>
 </g>
 <!-- Garrisoned Hero&#45;&gt;Hero -->
-<g id="edge24" class="edge">
+<g id="edge28" class="edge">
 <title>Garrisoned Hero&#45;&gt;Hero</title>
-<path fill="none" stroke="black" d="M1085.9,-548.99C1064.64,-539.36 1040.41,-528.39 1016.05,-517.36"/>
-<polygon fill="black" stroke="black" points="1017.26,-514.06 1006.71,-513.13 1014.37,-520.44 1017.26,-514.06"/>
+<path fill="none" stroke="black" d="M1026.58,-613.9C1041.08,-603.86 1057.87,-592.21 1075.21,-580.2"/>
+<polygon fill="black" stroke="black" points="1077.52,-582.85 1083.75,-574.28 1073.54,-577.1 1077.52,-582.85"/>
 </g>
 <!-- Wandering Hero&#45;&gt;Hero -->
-<g id="edge25" class="edge">
+<g id="edge29" class="edge">
 <title>Wandering Hero&#45;&gt;Hero</title>
-<path fill="none" stroke="black" d="M715.9,-548.99C730.39,-539.74 746.83,-529.26 763.42,-518.67"/>
-<polygon fill="black" stroke="black" points="765.56,-521.46 772.11,-513.13 761.8,-515.56 765.56,-521.46"/>
+<path fill="none" stroke="black" d="M1474.52,-613.9C1450.56,-603.31 1422.6,-590.95 1393.87,-578.25"/>
+<polygon fill="black" stroke="black" points="1395.08,-574.96 1384.51,-574.12 1392.25,-581.36 1395.08,-574.96"/>
 </g>
 <!-- Neutral Army&#45;&gt;Army -->
-<g id="edge21" class="edge">
+<g id="edge25" class="edge">
 <title>Neutral Army&#45;&gt;Army</title>
-<path fill="none" stroke="black" d="M1461.15,-548.93C1437.84,-504.7 1389.07,-425.4 1322.5,-387 1231.06,-334.27 1190.02,-367.76 1081.09,-350.98"/>
-<polygon fill="black" stroke="black" points="1081.48,-347.49 1071.04,-349.32 1080.34,-354.4 1081.48,-347.49"/>
+<path fill="none" stroke="black" d="M1784.91,-613.89C1720.6,-561.13 1582.08,-456.33 1445,-410 1285.7,-356.16 1232.22,-399.85 1062.07,-374.08"/>
+<polygon fill="black" stroke="black" points="1062.45,-370.6 1052.03,-372.5 1061.36,-377.51 1062.45,-370.6"/>
 </g>
 <!-- Owned Army&#45;&gt;Army -->
-<g id="edge22" class="edge">
+<g id="edge26" class="edge">
 <title>Owned Army&#45;&gt;Army</title>
-<path fill="none" stroke="black" d="M106.42,-548.96C117.92,-504.75 145.46,-425.48 201.5,-387 273.67,-337.44 490.45,-318.84 657.69,-311.93"/>
-<polygon fill="black" stroke="black" points="658.02,-315.42 667.87,-311.52 657.74,-308.42 658.02,-315.42"/>
+<path fill="none" stroke="black" d="M380.63,-613.89C364.79,-603.59 350.14,-590.44 341,-574 305.59,-510.29 294.41,-466.05 341,-410 374.42,-369.8 485.94,-347.37 593.84,-334.9"/>
+<polygon fill="black" stroke="black" points="594.38,-338.36 603.93,-333.76 593.6,-331.41 594.38,-338.36"/>
 </g>
 <!-- Town -->
-<g id="node11" class="node">
+<g id="node12" class="node">
 <title>Town</title>
-<polygon fill="#808080" fill-opacity="0.501961" stroke="transparent" points="1388,-753 1049,-753 1049,-650 1388,-650 1388,-753"/>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1052.5,-728.5 1052.5,-749.5 1385.5,-749.5 1385.5,-728.5 1052.5,-728.5"/>
-<text text-anchor="start" x="1199.5" y="-736.3" font-family="Noto Serif" font-weight="bold" font-size="14.00">Town</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1052.5,-705.5 1052.5,-726.5 1385.5,-726.5 1385.5,-705.5 1052.5,-705.5"/>
-<text text-anchor="start" x="1127" y="-713.3" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
-<text text-anchor="start" x="1195" y="-713.3" font-family="monospace" font-weight="bold" font-size="14.00">CGTownInstance</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1052.5,-652.5 1052.5,-703.5 1385.5,-703.5 1385.5,-652.5 1052.5,-652.5"/>
-<text text-anchor="start" x="1128" y="-689.3" font-family="Noto Serif" font-size="14.00">Represents a town on map.</text>
-<text text-anchor="start" x="1055.5" y="-674.3" font-family="Noto Serif" font-size="14.00">Town buildings can provide bonuses to this node</text>
-<text text-anchor="start" x="1123" y="-659.3" font-family="Noto Serif" font-size="14.00">(or propagate them upward)</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1049,-650 1049,-753 1388,-753 1388,-650 1049,-650"/>
+<polygon fill="#808080" fill-opacity="0.501961" stroke="transparent" points="925,-822.5 699,-822.5 699,-726.5 925,-726.5 925,-822.5"/>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="702,-798.5 702,-819.5 922,-819.5 922,-798.5 702,-798.5"/>
+<text text-anchor="start" x="792.5" y="-806.3" font-family="Noto Serif" font-weight="bold" font-size="14.00">Town</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="702,-775.5 702,-796.5 922,-796.5 922,-775.5 702,-775.5"/>
+<text text-anchor="start" x="720" y="-783.3" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
+<text text-anchor="start" x="788" y="-783.3" font-family="monospace" font-weight="bold" font-size="14.00">CGTownInstance</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="702,-752.5 702,-773.5 922,-773.5 922,-752.5 702,-752.5"/>
+<text text-anchor="start" x="721" y="-759.3" font-family="Noto Serif" font-size="14.00">Represents a town on map.</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="702,-729.5 702,-750.5 922,-750.5 922,-729.5 702,-729.5"/>
+<text text-anchor="start" x="705" y="-736.3" font-family="Noto Serif" font-size="14.00">Contains town building bonuses</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="699,-726.5 699,-822.5 925,-822.5 925,-726.5 699,-726.5"/>
 </g>
 <!-- Town&#45;&gt;Garrisoned Hero -->
-<g id="edge19" class="edge">
+<g id="edge23" class="edge">
 <title>Town&#45;&gt;Garrisoned Hero</title>
-<path fill="none" stroke="black" d="M1191.42,-649.77C1186.67,-640.87 1181.78,-631.72 1177.2,-623.14"/>
-<polygon fill="black" stroke="black" points="1180.16,-621.27 1172.37,-614.09 1173.99,-624.56 1180.16,-621.27"/>
+<path fill="none" stroke="black" d="M875.41,-726.22C893.52,-712.72 913.01,-698.19 930.26,-685.33"/>
+<polygon fill="black" stroke="black" points="932.6,-687.95 938.53,-679.17 928.42,-682.34 932.6,-687.95"/>
 </g>
 <!-- Town&#45;&gt;Army -->
-<g id="edge20" class="edge">
+<g id="edge24" class="edge">
 <title>Town&#45;&gt;Army</title>
-<path fill="none" stroke="black" d="M1269.58,-649.67C1277.74,-638.72 1284.98,-626.62 1289.5,-614 1299.23,-586.8 1302.53,-574.78 1289.5,-549 1240.14,-451.35 1132.4,-390.7 1038.41,-354.66"/>
-<polygon fill="black" stroke="black" points="1039.37,-351.29 1028.78,-351.04 1036.9,-357.84 1039.37,-351.29"/>
+<path fill="none" stroke="black" d="M813.67,-726.2C816.55,-644.44 822.46,-476.58 825.72,-384.13"/>
+<polygon fill="black" stroke="black" points="829.22,-384.18 826.08,-374.06 822.23,-383.93 829.22,-384.18"/>
 </g>
 <!-- Artifact Instance -->
-<g id="node12" class="node">
+<g id="node13" class="node">
 <title>Artifact Instance</title>
-<polygon fill="#00ffff" fill-opacity="0.501961" stroke="transparent" points="884.5,-745.5 562.5,-745.5 562.5,-657.5 884.5,-657.5 884.5,-745.5"/>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="565.5,-721.5 565.5,-742.5 881.5,-742.5 881.5,-721.5 565.5,-721.5"/>
-<text text-anchor="start" x="663" y="-729.3" font-family="Noto Serif" font-weight="bold" font-size="14.00">Artifact Instance</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="565.5,-698.5 565.5,-719.5 881.5,-719.5 881.5,-698.5 565.5,-698.5"/>
-<text text-anchor="start" x="652" y="-706.3" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
-<text text-anchor="start" x="720" y="-706.3" font-family="monospace" font-weight="bold" font-size="14.00">CArtifact</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="565.5,-660.5 565.5,-696.5 881.5,-696.5 881.5,-660.5 565.5,-660.5"/>
-<text text-anchor="start" x="568.5" y="-682.3" font-family="Noto Serif" font-size="14.00">Represents a particular instance of an artifact</text>
-<text text-anchor="start" x="626.5" y="-667.3" font-family="Noto Serif" font-size="14.00"> that hero can equip or trade</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="562.5,-657.5 562.5,-745.5 884.5,-745.5 884.5,-657.5 562.5,-657.5"/>
+<polygon fill="#00ffff" fill-opacity="0.501961" stroke="transparent" points="1687,-830 1311,-830 1311,-719 1687,-719 1687,-830"/>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1314,-805.5 1314,-826.5 1684,-826.5 1684,-805.5 1314,-805.5"/>
+<text text-anchor="start" x="1438.5" y="-813.3" font-family="Noto Serif" font-weight="bold" font-size="14.00">Artifact Instance</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1314,-782.5 1314,-803.5 1684,-803.5 1684,-782.5 1314,-782.5"/>
+<text text-anchor="start" x="1394.5" y="-790.3" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
+<text text-anchor="start" x="1462.5" y="-790.3" font-family="monospace" font-weight="bold" font-size="14.00">CArtifactInstance</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1314,-744.5 1314,-780.5 1684,-780.5 1684,-744.5 1314,-744.5"/>
+<text text-anchor="start" x="1344" y="-766.3" font-family="Noto Serif" font-size="14.00">Represents a particular instance of an artifact</text>
+<text text-anchor="start" x="1402" y="-751.3" font-family="Noto Serif" font-size="14.00"> that hero can equip or trade</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1314,-721.5 1314,-742.5 1684,-742.5 1684,-721.5 1314,-721.5"/>
+<text text-anchor="start" x="1317" y="-728.3" font-family="Noto Serif" font-size="14.00">Contains bonuses of spell scrolls and growing artifacts</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1311,-719 1311,-830 1687,-830 1687,-719 1311,-719"/>
 </g>
 <!-- Artifact Instance&#45;&gt;Hero -->
-<g id="edge16" class="edge">
+<g id="edge19" class="edge">
 <title>Artifact Instance&#45;&gt;Hero</title>
-<path fill="none" stroke="black" d="M585.05,-657.47C566.99,-646.13 550.85,-631.88 539.5,-614 524.02,-589.61 522.01,-571.99 539.5,-549 573.95,-503.71 608.3,-527.17 668.56,-512.98"/>
-<polygon fill="black" stroke="black" points="669.69,-516.3 678.46,-510.36 667.9,-509.53 669.69,-516.3"/>
+<path fill="none" stroke="black" d="M1446.84,-718.76C1437.06,-707.35 1427.31,-695.1 1419,-683 1398.29,-652.85 1405.59,-637.08 1381,-610 1371.53,-599.57 1360.94,-589.68 1349.76,-580.38"/>
+<polygon fill="black" stroke="black" points="1351.91,-577.62 1341.93,-574.04 1347.5,-583.06 1351.91,-577.62"/>
 </g>
 <!-- Boat -->
-<g id="node13" class="node">
+<g id="node14" class="node">
 <title>Boat</title>
-<polygon fill="#00ffff" fill-opacity="0.501961" stroke="transparent" points="544,-745.5 233,-745.5 233,-657.5 544,-657.5 544,-745.5"/>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="236.5,-721.5 236.5,-742.5 541.5,-742.5 541.5,-721.5 236.5,-721.5"/>
-<text text-anchor="start" x="372.5" y="-729.3" font-family="Noto Serif" font-weight="bold" font-size="14.00">Boat</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="236.5,-698.5 236.5,-719.5 541.5,-719.5 541.5,-698.5 236.5,-698.5"/>
-<text text-anchor="start" x="330" y="-706.3" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
-<text text-anchor="start" x="398" y="-706.3" font-family="monospace" font-weight="bold" font-size="14.00">CGBoat</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="236.5,-660.5 236.5,-696.5 541.5,-696.5 541.5,-660.5 236.5,-660.5"/>
-<text text-anchor="start" x="239.5" y="-682.3" font-family="Noto Serif" font-size="14.00">Represents a boat or other type of transport.</text>
-<text text-anchor="start" x="263.5" y="-667.3" font-family="Noto Serif" font-size="14.00">Can provide bonuses to boarded hero</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="233,-657.5 233,-745.5 544,-745.5 544,-657.5 233,-657.5"/>
+<polygon fill="#00ffff" fill-opacity="0.501961" stroke="transparent" points="1254.5,-822.5 943.5,-822.5 943.5,-726.5 1254.5,-726.5 1254.5,-822.5"/>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="947,-798.5 947,-819.5 1252,-819.5 1252,-798.5 947,-798.5"/>
+<text text-anchor="start" x="1083" y="-806.3" font-family="Noto Serif" font-weight="bold" font-size="14.00">Boat</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="947,-775.5 947,-796.5 1252,-796.5 1252,-775.5 947,-775.5"/>
+<text text-anchor="start" x="1040.5" y="-783.3" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
+<text text-anchor="start" x="1108.5" y="-783.3" font-family="monospace" font-weight="bold" font-size="14.00">CGBoat</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="947,-752.5 947,-773.5 1252,-773.5 1252,-752.5 947,-752.5"/>
+<text text-anchor="start" x="950" y="-759.3" font-family="Noto Serif" font-size="14.00">Represents a boat or other type of transport.</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="947,-729.5 947,-750.5 1252,-750.5 1252,-729.5 947,-729.5"/>
+<text text-anchor="start" x="953.5" y="-736.3" font-family="Noto Serif" font-size="14.00">Contains bonuses provided to boarded hero</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="943.5,-726.5 943.5,-822.5 1254.5,-822.5 1254.5,-726.5 943.5,-726.5"/>
 </g>
 <!-- Boat&#45;&gt;Hero -->
-<g id="edge9" class="edge">
+<g id="edge10" class="edge">
 <title>Boat&#45;&gt;Hero</title>
-<path fill="none" stroke="black" d="M415.69,-657.4C439.67,-622.94 477.72,-576.05 522.5,-549 577.38,-515.85 602.3,-529.51 668.43,-513.13"/>
-<polygon fill="black" stroke="black" points="669.71,-516.41 678.5,-510.49 667.94,-509.64 669.71,-516.41"/>
+<path fill="none" stroke="black" d="M1114.06,-726.38C1124.73,-693.58 1139.65,-648.92 1154,-610 1157.14,-601.48 1160.5,-592.65 1163.92,-583.83"/>
+<polygon fill="black" stroke="black" points="1167.27,-584.86 1167.65,-574.28 1160.75,-582.32 1167.27,-584.86"/>
 </g>
 <!-- Town and visiting hero&#45;&gt;Visiting Hero -->
-<g id="edge8" class="edge">
+<g id="edge9" class="edge">
 <title>Town and visiting hero&#45;&gt;Visiting Hero</title>
-<path fill="none" stroke="black" d="M1079.27,-788.81C1065.1,-778.07 1051.74,-766.11 1040.5,-753 1008.09,-715.22 1027.13,-689.24 996.5,-650 988.05,-639.18 977.46,-629.17 966.57,-620.37"/>
-<polygon fill="black" stroke="black" points="968.65,-617.55 958.61,-614.18 964.36,-623.08 968.65,-617.55"/>
+<path fill="none" stroke="black" d="M871.69,-868C874.81,-867.31 877.92,-866.64 881,-866 964.71,-848.68 1205.93,-892.74 1264,-830 1298.6,-792.62 1290.93,-730.23 1280.43,-688.97"/>
+<polygon fill="black" stroke="black" points="1283.79,-687.96 1277.8,-679.22 1277.03,-689.79 1283.79,-687.96"/>
 </g>
 <!-- Town and visiting hero&#45;&gt;Town -->
-<g id="edge7" class="edge">
+<g id="edge8" class="edge">
 <title>Town and visiting hero&#45;&gt;Town</title>
-<path fill="none" stroke="black" d="M1210.56,-789C1211.34,-780.53 1212.13,-771.89 1212.9,-763.5"/>
-<polygon fill="black" stroke="black" points="1216.4,-763.71 1213.82,-753.43 1209.42,-763.07 1216.4,-763.71"/>
+<path fill="none" stroke="black" d="M734.83,-865.63C744.91,-853.85 755.31,-841.71 765.08,-830.29"/>
+<polygon fill="black" stroke="black" points="767.89,-832.4 771.74,-822.52 762.57,-827.84 767.89,-832.4"/>
 </g>
 <!-- Creature Type -->
-<g id="node15" class="node">
+<g id="node16" class="node">
 <title>Creature Type</title>
-<polygon fill="#00ffff" fill-opacity="0.501961" stroke="transparent" points="1469.5,-343.5 1089.5,-343.5 1089.5,-270.5 1469.5,-270.5 1469.5,-343.5"/>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1092.5,-319 1092.5,-340 1466.5,-340 1466.5,-319 1092.5,-319"/>
-<text text-anchor="start" x="1228" y="-326.8" font-family="Noto Serif" font-weight="bold" font-size="14.00">Creature Type</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1092.5,-296 1092.5,-317 1466.5,-317 1466.5,-296 1092.5,-296"/>
-<text text-anchor="start" x="1208" y="-303.8" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
-<text text-anchor="start" x="1276" y="-303.8" font-family="monospace" font-weight="bold" font-size="14.00">CCreature</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1092.5,-273 1092.5,-294 1466.5,-294 1466.5,-273 1092.5,-273"/>
-<text text-anchor="start" x="1095.5" y="-279.8" font-family="Noto Serif" font-size="14.00">Represents a creature type, such as Pikeman or Archer</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1089.5,-270.5 1089.5,-343.5 1469.5,-343.5 1469.5,-270.5 1089.5,-270.5"/>
+<polygon fill="#00ffff" fill-opacity="0.501961" stroke="transparent" points="1494,-366.5 1070,-366.5 1070,-270.5 1494,-270.5 1494,-366.5"/>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1073,-342.5 1073,-363.5 1491,-363.5 1491,-342.5 1073,-342.5"/>
+<text text-anchor="start" x="1230.5" y="-350.3" font-family="Noto Serif" font-weight="bold" font-size="14.00">Creature Type</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1073,-319.5 1073,-340.5 1491,-340.5 1491,-319.5 1073,-319.5"/>
+<text text-anchor="start" x="1210.5" y="-327.3" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
+<text text-anchor="start" x="1278.5" y="-327.3" font-family="monospace" font-weight="bold" font-size="14.00">CCreature</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1073,-296.5 1073,-317.5 1491,-317.5 1491,-296.5 1073,-296.5"/>
+<text text-anchor="start" x="1098" y="-303.3" font-family="Noto Serif" font-size="14.00">Represents a creature type, such as Pikeman or Archer</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1073,-273.5 1073,-294.5 1491,-294.5 1491,-273.5 1073,-273.5"/>
+<text text-anchor="start" x="1076" y="-280.3" font-family="Noto Serif" font-size="14.00">Contains creature abilities bonuses, stack experience bonuses</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1070,-270.5 1070,-366.5 1494,-366.5 1494,-270.5 1070,-270.5"/>
 </g>
 <!-- Unit in Army -->
-<g id="node19" class="node">
+<g id="node20" class="node">
 <title>Unit in Army</title>
-<polygon fill="#602000" fill-opacity="0.501961" stroke="transparent" points="1394.5,-227 1118.5,-227 1118.5,-124 1394.5,-124 1394.5,-227"/>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1121.5,-202.5 1121.5,-223.5 1391.5,-223.5 1391.5,-202.5 1121.5,-202.5"/>
-<text text-anchor="start" x="1210" y="-210.3" font-family="Noto Serif" font-weight="bold" font-size="14.00">Unit in Army</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1121.5,-179.5 1121.5,-200.5 1391.5,-200.5 1391.5,-179.5 1121.5,-179.5"/>
-<text text-anchor="start" x="1164.5" y="-187.3" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
-<text text-anchor="start" x="1232.5" y="-187.3" font-family="monospace" font-weight="bold" font-size="14.00">CStackInstance</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1121.5,-126.5 1121.5,-177.5 1391.5,-177.5 1391.5,-126.5 1121.5,-126.5"/>
-<text text-anchor="start" x="1124.5" y="-163.3" font-family="Noto Serif" font-size="14.00">Represents a unit that is part of a army</text>
-<text text-anchor="start" x="1142" y="-148.3" font-family="Noto Serif" font-size="14.00">A unit always has a creature type,</text>
-<text text-anchor="start" x="1130" y="-133.3" font-family="Noto Serif" font-size="14.00">belongs to an army and has stack size</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1118.5,-124 1118.5,-227 1394.5,-227 1394.5,-124 1118.5,-124"/>
+<polygon fill="#602000" fill-opacity="0.501961" stroke="transparent" points="1198,-227 922,-227 922,-124 1198,-124 1198,-227"/>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="925,-202.5 925,-223.5 1195,-223.5 1195,-202.5 925,-202.5"/>
+<text text-anchor="start" x="1013.5" y="-210.3" font-family="Noto Serif" font-weight="bold" font-size="14.00">Unit in Army</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="925,-179.5 925,-200.5 1195,-200.5 1195,-179.5 925,-179.5"/>
+<text text-anchor="start" x="968" y="-187.3" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
+<text text-anchor="start" x="1036" y="-187.3" font-family="monospace" font-weight="bold" font-size="14.00">CStackInstance</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="925,-126.5 925,-177.5 1195,-177.5 1195,-126.5 925,-126.5"/>
+<text text-anchor="start" x="928" y="-163.3" font-family="Noto Serif" font-size="14.00">Represents a unit that is part of a army</text>
+<text text-anchor="start" x="945.5" y="-148.3" font-family="Noto Serif" font-size="14.00">A unit always has a creature type,</text>
+<text text-anchor="start" x="933.5" y="-133.3" font-family="Noto Serif" font-size="14.00">belongs to an army and has stack size</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="922,-124 922,-227 1198,-227 1198,-124 922,-124"/>
 </g>
 <!-- Creature Type&#45;&gt;Unit in Army -->
-<g id="edge18" class="edge">
+<g id="edge22" class="edge">
 <title>Creature Type&#45;&gt;Unit in Army</title>
-<path fill="none" stroke="black" d="M1273.15,-270.27C1271.32,-259.96 1269.27,-248.42 1267.25,-237.05"/>
-<polygon fill="black" stroke="black" points="1270.69,-236.37 1265.49,-227.14 1263.8,-237.6 1270.69,-236.37"/>
+<path fill="none" stroke="black" d="M1207.77,-270.35C1188.77,-258.29 1168.16,-245.2 1148.52,-232.72"/>
+<polygon fill="black" stroke="black" points="1150.2,-229.64 1139.88,-227.23 1146.44,-235.55 1150.2,-229.64"/>
 </g>
-<!-- Summon in Combat -->
+<!-- Commander -->
 <g id="node21" class="node">
+<title>Commander</title>
+<polygon fill="#602000" fill-opacity="0.501961" stroke="transparent" points="1524,-212 1216,-212 1216,-139 1524,-139 1524,-212"/>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1219,-187.5 1219,-208.5 1521,-208.5 1521,-187.5 1219,-187.5"/>
+<text text-anchor="start" x="1326.5" y="-195.3" font-family="Noto Serif" font-weight="bold" font-size="14.00">Commander</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1219,-164.5 1219,-185.5 1521,-185.5 1521,-164.5 1219,-164.5"/>
+<text text-anchor="start" x="1261.5" y="-172.3" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
+<text text-anchor="start" x="1329.5" y="-172.3" font-family="monospace" font-weight="bold" font-size="14.00">CCommanderInstance</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1219,-141.5 1219,-162.5 1521,-162.5 1521,-141.5 1219,-141.5"/>
+<text text-anchor="start" x="1222" y="-148.3" font-family="Noto Serif" font-size="14.00">Represents a hero commander, WoG feature</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1216,-139 1216,-212 1524,-212 1524,-139 1216,-139"/>
+</g>
+<!-- Creature Type&#45;&gt;Commander -->
+<g id="edge20" class="edge">
+<title>Creature Type&#45;&gt;Commander</title>
+<path fill="none" stroke="black" d="M1311.55,-270.16C1321.49,-254.23 1332.54,-236.52 1342.33,-220.83"/>
+<polygon fill="black" stroke="black" points="1345.3,-222.68 1347.63,-212.35 1339.36,-218.98 1345.3,-222.68"/>
+</g>
+<!-- Summon in Combat -->
+<g id="node23" class="node">
 <title>Summon in Combat</title>
-<polygon fill="#602000" fill-opacity="0.501961" stroke="transparent" points="1078,-219.5 751,-219.5 751,-131.5 1078,-131.5 1078,-219.5"/>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="754.5,-195.5 754.5,-216.5 1075.5,-216.5 1075.5,-195.5 754.5,-195.5"/>
-<text text-anchor="start" x="846" y="-203.3" font-family="Noto Serif" font-weight="bold" font-size="14.00">Summon in Combat</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="754.5,-172.5 754.5,-193.5 1075.5,-193.5 1075.5,-172.5 754.5,-172.5"/>
-<text text-anchor="start" x="856" y="-180.3" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
-<text text-anchor="start" x="924" y="-180.3" font-family="monospace" font-weight="bold" font-size="14.00">CStack</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="754.5,-134.5 754.5,-170.5 1075.5,-170.5 1075.5,-134.5 754.5,-134.5"/>
-<text text-anchor="start" x="757.5" y="-156.3" font-family="Noto Serif" font-size="14.00">Represents any unit that was added in combat,</text>
-<text text-anchor="start" x="800.5" y="-141.3" font-family="Noto Serif" font-size="14.00">and may not remain after combat</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="751,-131.5 751,-219.5 1078,-219.5 1078,-131.5 751,-131.5"/>
+<polygon fill="#602000" fill-opacity="0.501961" stroke="transparent" points="903.5,-219.5 576.5,-219.5 576.5,-131.5 903.5,-131.5 903.5,-219.5"/>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="580,-195.5 580,-216.5 901,-216.5 901,-195.5 580,-195.5"/>
+<text text-anchor="start" x="671.5" y="-203.3" font-family="Noto Serif" font-weight="bold" font-size="14.00">Summon in Combat</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="580,-172.5 580,-193.5 901,-193.5 901,-172.5 580,-172.5"/>
+<text text-anchor="start" x="681.5" y="-180.3" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
+<text text-anchor="start" x="749.5" y="-180.3" font-family="monospace" font-weight="bold" font-size="14.00">CStack</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="580,-134.5 580,-170.5 901,-170.5 901,-134.5 580,-134.5"/>
+<text text-anchor="start" x="583" y="-156.3" font-family="Noto Serif" font-size="14.00">Represents any unit that was added in combat,</text>
+<text text-anchor="start" x="626" y="-141.3" font-family="Noto Serif" font-size="14.00">and may not remain after combat</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="576.5,-131.5 576.5,-219.5 903.5,-219.5 903.5,-131.5 576.5,-131.5"/>
 </g>
 <!-- Creature Type&#45;&gt;Summon in Combat -->
-<g id="edge17" class="edge">
+<g id="edge21" class="edge">
 <title>Creature Type&#45;&gt;Summon in Combat</title>
-<path fill="none" stroke="black" d="M1179.28,-270.44C1137.99,-255.79 1089.59,-238.62 1045.55,-223"/>
-<polygon fill="black" stroke="black" points="1046.53,-219.63 1035.93,-219.58 1044.18,-226.23 1046.53,-219.63"/>
+<path fill="none" stroke="black" d="M1091.54,-270.49C1081.21,-267.95 1070.98,-265.44 1061,-263 995.25,-246.9 978.4,-244.49 913,-227 907.29,-225.47 901.48,-223.9 895.61,-222.28"/>
+<polygon fill="black" stroke="black" points="896.27,-218.83 885.7,-219.54 894.4,-225.58 896.27,-218.83"/>
 </g>
 <!-- Artifact Type -->
-<g id="node16" class="node">
+<g id="node17" class="node">
 <title>Artifact Type</title>
-<polygon fill="#00ffff" fill-opacity="0.501961" stroke="transparent" points="954.5,-892.5 590.5,-892.5 590.5,-819.5 954.5,-819.5 954.5,-892.5"/>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="593.5,-868 593.5,-889 951.5,-889 951.5,-868 593.5,-868"/>
-<text text-anchor="start" x="724.5" y="-875.8" font-family="Noto Serif" font-weight="bold" font-size="14.00">Artifact Type</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="593.5,-845 593.5,-866 951.5,-866 951.5,-845 593.5,-845"/>
-<text text-anchor="start" x="701" y="-852.8" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
-<text text-anchor="start" x="769" y="-852.8" font-family="monospace" font-weight="bold" font-size="14.00">CArtifact</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="593.5,-822 593.5,-843 951.5,-843 951.5,-822 593.5,-822"/>
-<text text-anchor="start" x="596.5" y="-828.8" font-family="Noto Serif" font-size="14.00">Represents an artifact type, for example Ring of Life</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="590.5,-819.5 590.5,-892.5 954.5,-892.5 954.5,-819.5 590.5,-819.5"/>
+<polygon fill="#00ffff" fill-opacity="0.501961" stroke="transparent" points="1254,-981 890,-981 890,-885 1254,-885 1254,-981"/>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="893,-957 893,-978 1251,-978 1251,-957 893,-957"/>
+<text text-anchor="start" x="1024" y="-964.8" font-family="Noto Serif" font-weight="bold" font-size="14.00">Artifact Type</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="893,-934 893,-955 1251,-955 1251,-934 893,-934"/>
+<text text-anchor="start" x="1000.5" y="-941.8" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
+<text text-anchor="start" x="1068.5" y="-941.8" font-family="monospace" font-weight="bold" font-size="14.00">CArtifact</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="893,-911 893,-932 1251,-932 1251,-911 893,-911"/>
+<text text-anchor="start" x="896" y="-917.8" font-family="Noto Serif" font-size="14.00">Represents an artifact type, for example Ring of Life</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="893,-888 893,-909 1251,-909 1251,-888 893,-888"/>
+<text text-anchor="start" x="956.5" y="-894.8" font-family="Noto Serif" font-size="14.00">Contains fixed bonuses of artifacts</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="890,-885 890,-981 1254,-981 1254,-885 890,-885"/>
 </g>
 <!-- Artifact Type&#45;&gt;Artifact Instance -->
-<g id="edge14" class="edge">
+<g id="edge17" class="edge">
 <title>Artifact Type&#45;&gt;Artifact Instance</title>
-<path fill="none" stroke="black" d="M761.01,-819.25C754.91,-800.25 747.28,-776.52 740.51,-755.43"/>
-<polygon fill="black" stroke="black" points="743.75,-754.08 737.36,-745.63 737.09,-756.23 743.75,-754.08"/>
+<path fill="none" stroke="black" d="M1200.4,-884.94C1244.52,-868.77 1294.43,-850.48 1340.65,-833.54"/>
+<polygon fill="black" stroke="black" points="1341.96,-836.79 1350.14,-830.06 1339.55,-830.21 1341.96,-836.79"/>
 </g>
 <!-- Artifact Component -->
-<g id="node17" class="node">
+<g id="node18" class="node">
 <title>Artifact Component</title>
-<polygon fill="#808080" fill-opacity="0.501961" stroke="transparent" points="572.5,-900 88.5,-900 88.5,-812 572.5,-812 572.5,-900"/>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="91.5,-876 91.5,-897 569.5,-897 569.5,-876 91.5,-876"/>
-<text text-anchor="start" x="259.5" y="-883.8" font-family="Noto Serif" font-weight="bold" font-size="14.00">Artifact Component</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="91.5,-853 91.5,-874 569.5,-874 569.5,-853 91.5,-853"/>
-<text text-anchor="start" x="259" y="-860.8" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
-<text text-anchor="start" x="327" y="-860.8" font-family="monospace" font-weight="bold" font-size="14.00">CArtifact</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="91.5,-815 91.5,-851 569.5,-851 569.5,-815 91.5,-815"/>
-<text text-anchor="start" x="215" y="-836.8" font-family="Noto Serif" font-size="14.00">For combined, non&#45;fused artifacts,</text>
-<text text-anchor="start" x="94.5" y="-821.8" font-family="Noto Serif" font-size="14.00">instances of components are attached to instance of combined artifact</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="88.5,-812 88.5,-900 572.5,-900 572.5,-812 88.5,-812"/>
+<polygon fill="#808080" fill-opacity="0.501961" stroke="transparent" points="1756,-977 1272,-977 1272,-889 1756,-889 1756,-977"/>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1275,-953 1275,-974 1753,-974 1753,-953 1275,-953"/>
+<text text-anchor="start" x="1443" y="-960.8" font-family="Noto Serif" font-weight="bold" font-size="14.00">Artifact Component</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1275,-930 1275,-951 1753,-951 1753,-930 1275,-930"/>
+<text text-anchor="start" x="1409.5" y="-937.8" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
+<text text-anchor="start" x="1477.5" y="-937.8" font-family="monospace" font-weight="bold" font-size="14.00">CArtifactInstance</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1275,-892 1275,-928 1753,-928 1753,-892 1275,-892"/>
+<text text-anchor="start" x="1398.5" y="-913.8" font-family="Noto Serif" font-size="14.00">For combined, non&#45;fused artifacts,</text>
+<text text-anchor="start" x="1278" y="-898.8" font-family="Noto Serif" font-size="14.00">instances of components are attached to instance of combined artifact</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1272,-889 1272,-977 1756,-977 1756,-889 1272,-889"/>
 </g>
 <!-- Artifact Component&#45;&gt;Artifact Instance -->
-<g id="edge15" class="edge">
+<g id="edge18" class="edge">
 <title>Artifact Component&#45;&gt;Artifact Instance</title>
-<path fill="none" stroke="black" d="M441.54,-811.91C491.86,-792.38 551.47,-769.25 602.89,-749.3"/>
-<polygon fill="black" stroke="black" points="604.34,-752.5 612.39,-745.61 601.8,-745.97 604.34,-752.5"/>
+<path fill="none" stroke="black" d="M1509.86,-888.82C1508.42,-873.76 1506.77,-856.52 1505.19,-840.1"/>
+<polygon fill="black" stroke="black" points="1508.66,-839.63 1504.22,-830.01 1501.69,-840.3 1508.66,-839.63"/>
 </g>
 <!-- Army&#45;&gt;Unit in Army -->
-<g id="edge11" class="edge">
+<g id="edge13" class="edge">
 <title>Army&#45;&gt;Unit in Army</title>
-<path fill="none" stroke="black" d="M998.1,-262.97C1033.53,-251.11 1072.25,-238.15 1108.68,-225.96"/>
-<polygon fill="black" stroke="black" points="1110.01,-229.21 1118.39,-222.72 1107.79,-222.57 1110.01,-229.21"/>
+<path fill="none" stroke="black" d="M917.7,-262.99C934.2,-252.95 951.46,-242.47 968.03,-232.4"/>
+<polygon fill="black" stroke="black" points="969.89,-235.36 976.62,-227.17 966.26,-229.38 969.89,-235.36"/>
 </g>
-<!-- Army&#45;&gt;Summon in Combat -->
+<!-- Army&#45;&gt;Commander -->
 <g id="edge12" class="edge">
+<title>Army&#45;&gt;Commander</title>
+<path fill="none" stroke="black" d="M1052.23,-264.59C1124.31,-247.46 1190.04,-231.65 1207,-227 1220.58,-223.28 1234.74,-219.2 1248.79,-215.02"/>
+<polygon fill="black" stroke="black" points="1249.91,-218.34 1258.49,-212.12 1247.91,-211.63 1249.91,-218.34"/>
+</g>
+<!-- Army&#45;&gt;Summon in Combat -->
+<g id="edge14" class="edge">
 <title>Army&#45;&gt;Summon in Combat</title>
-<path fill="none" stroke="black" d="M884.48,-262.88C888.2,-252.18 892.23,-240.59 896.09,-229.48"/>
-<polygon fill="black" stroke="black" points="899.45,-230.47 899.43,-219.87 892.84,-228.17 899.45,-230.47"/>
+<path fill="none" stroke="black" d="M793.98,-262.99C786.81,-251.5 779.27,-239.42 772.17,-228.04"/>
+<polygon fill="black" stroke="black" points="775.1,-226.14 766.84,-219.51 769.17,-229.84 775.1,-226.14"/>
 </g>
 <!-- Unit in Combat -->
-<g id="node20" class="node">
+<g id="node22" class="node">
 <title>Unit in Combat</title>
-<polygon fill="#602000" fill-opacity="0.501961" stroke="transparent" points="1428,-88 1085,-88 1085,0 1428,0 1428,-88"/>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1088.5,-64 1088.5,-85 1425.5,-85 1425.5,-64 1088.5,-64"/>
-<text text-anchor="start" x="1203.5" y="-71.8" font-family="Noto Serif" font-weight="bold" font-size="14.00">Unit in Combat</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1088.5,-41 1088.5,-62 1425.5,-62 1425.5,-41 1088.5,-41"/>
-<text text-anchor="start" x="1198" y="-48.8" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
-<text text-anchor="start" x="1266" y="-48.8" font-family="monospace" font-weight="bold" font-size="14.00">CStack</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1088.5,-3 1088.5,-39 1425.5,-39 1425.5,-3 1088.5,-3"/>
-<text text-anchor="start" x="1091.5" y="-24.8" font-family="Noto Serif" font-size="14.00">Represents current state of a unit during combat,</text>
-<text text-anchor="start" x="1112" y="-9.8" font-family="Noto Serif" font-size="14.00">can be affected by spells or receive damage</text>
-<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1085,0 1085,-88 1428,-88 1428,0 1085,0"/>
+<polygon fill="#602000" fill-opacity="0.501961" stroke="transparent" points="1386.5,-88 1043.5,-88 1043.5,0 1386.5,0 1386.5,-88"/>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1047,-64 1047,-85 1384,-85 1384,-64 1047,-64"/>
+<text text-anchor="start" x="1162" y="-71.8" font-family="Noto Serif" font-weight="bold" font-size="14.00">Unit in Combat</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1047,-41 1047,-62 1384,-62 1384,-41 1047,-41"/>
+<text text-anchor="start" x="1156.5" y="-48.8" font-family="Noto Serif" font-size="14.00">C++ Class: </text>
+<text text-anchor="start" x="1224.5" y="-48.8" font-family="monospace" font-weight="bold" font-size="14.00">CStack</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1047,-3 1047,-39 1384,-39 1384,-3 1047,-3"/>
+<text text-anchor="start" x="1050" y="-24.8" font-family="Noto Serif" font-size="14.00">Represents current state of a unit during combat,</text>
+<text text-anchor="start" x="1070.5" y="-9.8" font-family="Noto Serif" font-size="14.00">can be affected by spells or receive damage</text>
+<polygon fill="none" stroke="#000000" stroke-opacity="0.501961" points="1043.5,0 1043.5,-88 1386.5,-88 1386.5,0 1043.5,0"/>
 </g>
 <!-- Unit in Army&#45;&gt;Unit in Combat -->
-<g id="edge13" class="edge">
+<g id="edge15" class="edge">
 <title>Unit in Army&#45;&gt;Unit in Combat</title>
-<path fill="none" stroke="black" d="M1256.5,-123.69C1256.5,-115.33 1256.5,-106.64 1256.5,-98.22"/>
-<polygon fill="black" stroke="black" points="1260,-98.12 1256.5,-88.12 1253,-98.12 1260,-98.12"/>
+<path fill="none" stroke="black" d="M1120.82,-123.69C1132.25,-114.13 1144.2,-104.15 1155.6,-94.63"/>
+<polygon fill="black" stroke="black" points="1157.96,-97.22 1163.39,-88.12 1153.47,-91.85 1157.96,-97.22"/>
+</g>
+<!-- Commander&#45;&gt;Unit in Combat -->
+<g id="edge16" class="edge">
+<title>Commander&#45;&gt;Unit in Combat</title>
+<path fill="none" stroke="black" d="M1327.24,-138.77C1310.99,-125.2 1292.19,-109.49 1274.67,-94.85"/>
+<polygon fill="black" stroke="black" points="1276.64,-91.94 1266.72,-88.21 1272.15,-97.31 1276.64,-91.94"/>
 </g>
 </g>
 </svg>

+ 11 - 8
docs/modders/Bonus/Bonus_Types.md

@@ -257,6 +257,7 @@ Gives additional bonus to effect of specific spell
 Gives creature under effect of this spell additional bonus, which is hardcoded and depends on the creature tier.
 
 - subtype: affected spell identifier, ie. `spell.haste`
+- addInfo: must be set to 0, or 1 for Slayer specialty
 
 ### SPECIAL_ADD_VALUE_ENCHANT
 
@@ -334,7 +335,7 @@ Affected heroes will add specified resources amounts to player treasure on new d
 
 Increases weekly growth of creatures in affected towns (Legion artifacts)
 
-- value: number of additional weekly creatures
+- val: number of additional weekly creatures
 - subtype: dwelling level, in form `creatureLevelX` where X is desired level (1-7)
 
 ### CREATURE_GROWTH_PERCENT
@@ -478,7 +479,7 @@ Affected unit will always retaliate if able (Royal Griffin)
 
 Affected unit can retaliate multiple times per turn (basic Griffin)
 
-- value: number of additional retaliations
+- val: number of additional retaliations
 
 ### JOUSTING
 
@@ -497,8 +498,8 @@ Affected unit will deal more damage when attacking specific creature
 
 Affected unit ranged attack will use animation and range of specified spell (Magog, Lich)
 
-- subtype - spell identifier
-- value - spell mastery level
+- subtype: spell identifier
+- val: spell mastery level
 
 ### ATTACKS_ALL_ADJACENT
 
@@ -831,7 +832,7 @@ Affected unit will not use spellcast as default attack option
 Affected units can cast a spell as targeted action (Archangel, Faerie Dragon). Use CASTS bonus to specify how many times per combat creature can use spellcasting. Use SPECIFIC_SPELL_POWER, CREATURE_SPELL_POWER or CREATURE_ENCHANT_POWER bonuses to set spell power.
 
 - subtype: spell identifier
-- value: spell mastery level
+- val: spell mastery level
 - addInfo: weighted chance to select this spell. Can be omitted for always available spells
 
 ### ENCHANTER
@@ -840,7 +841,7 @@ Affected unit will cast specified spell before his turn (Enchanter)
 
 - val - spell mastery level
 - subtype - spell identifier
-- additionalInfo - cooldown before next cast, in number of turns
+- addInfo - cooldown before next cast, in number of turns
 
 ### RANDOM_SPELLCASTER
 
@@ -857,7 +858,7 @@ Determines how many times per combat affected creature can cast its targeted spe
 ### SPELL_AFTER_ATTACK
 
 - subtype - spell id, eg. spell.iceBolt
-- value - chance (percent)
+- val - chance (percent)
 - addInfo - \[X, Y, Z\]
   - X - spell mastery level (1 - Basic, 3 - Expert)
   - Y = 0 - all attacks, 1 - shot only, 2 - melee only
@@ -867,7 +868,7 @@ Determines how many times per combat affected creature can cast its targeted spe
 ### SPELL_BEFORE_ATTACK
 
 - subtype - spell id
-- value - chance %
+- val - chance %
 - addInfo - \[X, Y, Z\]
   - X - spell mastery level (1 - Basic, 3 - Expert)
   - Y = 0 - all attacks, 1 - shot only, 2 - melee only
@@ -1057,6 +1058,8 @@ Blocks casting spells of the level below specified one in battles affected by th
 
 Dummy bonus that acts as marker for Dendroid's Bind ability
 
+- addInfo: ID of stack that have bound the unit
+
 ### SYNERGY_TARGET
 
 Dummy skill for alternative upgrades mod

+ 6 - 2
docs/modders/Bonus_Format.md

@@ -65,8 +65,12 @@ All parameters but type are optional.
 	// using its propagator. It has no effect on bonuses without propagator
 	"propagationUpdater" :	{Bonus Updater, but works during propagation},
 	
-	// TODO
-	"description" : "",
+	// Optional custom description, at the moment limited to creature abilities
+	// Visible only in creature window
+	"description" : "{Ability Name}\nLong description text",
+	
+	// Optional, path to custom icon that will be visible in creature window
+	"icon" : "",
 	
 	// Stacking string allows to block stacking of bonuses from different entities
 	// For example, devils and archdevils (different entities) both have battle-wide debuff to luck

+ 9 - 0
docs/modders/Entities_Format/Artifact_Format.md

@@ -54,11 +54,20 @@ In order to make functional artifact you also need:
 	},
 
 	// Bonuses provided by this artifact using bonus system
+	// If hero equips multiple instances of the same artifact, their effect will not stack
 	"bonuses":
 	{
 		Bonus_1,
 		Bonus_2
 	},
+	
+	// Bonuses provided by every instance of this artifact using bonus system
+	// These bonuses will stack if hero equips multiple instances of this artifact
+	"instanceBonuses":
+	{
+		Bonus_1,
+		Bonus_2
+	},
 
 	// Optional, list of components for combinational artifacts
 	"components": 

+ 1 - 0
launcher/firstLaunch/firstlaunch_moc.cpp

@@ -349,6 +349,7 @@ void FirstLaunchView::extractGogData()
 		ui->progressBarGog->setVisible(false);
 		ui->pushButtonGogInstall->setVisible(true);
 		setEnabled(true);
+		heroesDataUpdate();
 	});
 #endif
 }

+ 5 - 0
launcher/modManager/modstatecontroller.cpp

@@ -232,6 +232,11 @@ bool ModStateController::doInstallMod(QString modname, QString archivePath)
 	auto rc = QFile::rename(destDir + modDirName, destDir + modname);
 	if (rc)
 		extractedDir.setPath(destDir + modname);
+
+	// Remove .github folder from installed mod
+	QDir githubDir(extractedDir.filePath(".github"));
+	if (githubDir.exists())
+	    githubDir.removeRecursively();
 	
 	//there are possible excessive files - remove them
 	QString upperLevel = modDirName.section('/', 0, 0);

+ 6 - 1
lib/CCreatureHandler.cpp

@@ -228,6 +228,11 @@ std::string CCreature::getDescriptionTextID() const
 	return TextIdentifier("creatures", modScope, identifier, "description").get();
 }
 
+std::string CCreature::getBonusTextID(const std::string & bonusID) const
+{
+	return TextIdentifier("creatures", modScope, identifier, "bonus", bonusID).get();
+}
+
 CCreature::CreatureQuantityId CCreature::getQuantityID(const int & quantity)
 {
 	if (quantity<5)
@@ -904,7 +909,7 @@ void CCreatureHandler::loadCreatureJson(CCreature * creature, const JsonNode & c
 		{
 			if (!ability.second.isNull())
 			{
-				auto b = JsonUtils::parseBonus(ability.second);
+				auto b = JsonUtils::parseBonus(ability.second, creature->getBonusTextID(ability.first));
 				b->source = BonusSource::CREATURE_ABILITY;
 				b->sid = BonusSourceID(creature->getId());
 				b->duration = BonusDuration::PERMANENT;

+ 1 - 0
lib/CCreatureHandler.h

@@ -56,6 +56,7 @@ class DLL_LINKAGE CCreature : public Creature, public CBonusSystemNode
 public:
 	std::string getDescriptionTranslated() const;
 	std::string getDescriptionTextID() const;
+	std::string getBonusTextID(const std::string & bonusID) const;
 
 	ui32 ammMin; // initial size of stack of these creatures on adventure map (if not set in editor)
 	ui32 ammMax;

+ 9 - 0
lib/CCreatureSet.cpp

@@ -837,11 +837,20 @@ void CStackInstance::setCount(TQuantity newCount)
 
 std::string CStackInstance::bonusToString(const std::shared_ptr<Bonus>& bonus, bool description) const
 {
+	if (!bonus->description.empty())
+	{
+		if (description)
+			return bonus->description.toString();
+		else
+			return {};
+	}
 	return LIBRARY->getBth()->bonusToString(bonus, this, description);
 }
 
 ImagePath CStackInstance::bonusToGraphics(const std::shared_ptr<Bonus> & bonus) const
 {
+	if (!bonus->customIconPath.empty())
+		return bonus->customIconPath;
 	return LIBRARY->getBth()->bonusToGraphics(bonus);
 }
 

+ 5 - 2
lib/CSkillHandler.cpp

@@ -109,8 +109,11 @@ void CSkill::addNewBonus(const std::shared_ptr<Bonus> & b, int level)
 	b->source = BonusSource::SECONDARY_SKILL;
 	b->sid = BonusSourceID(id);
 	b->duration = BonusDuration::PERMANENT;
-	b->description.appendTextID(getNameTextID());
-	b->description.appendRawString(" %+d");
+	if (b->description.empty() && (b->type == BonusType::LUCK || b->type == BonusType::MORALE))
+	{
+		b->description.appendTextID(getNameTextID());
+		b->description.appendRawString(" %+d");
+	}
 	levels[level-1].effects.push_back(b);
 }
 

+ 4 - 4
lib/battle/BattleLayout.cpp

@@ -18,12 +18,12 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-BattleLayout BattleLayout::createDefaultLayout(IGameInfoCallback * cb, const CArmedInstance * attacker, const CArmedInstance * defender)
+BattleLayout BattleLayout::createDefaultLayout(const IGameInfoCallback & gameInfo, const CArmedInstance * attacker, const CArmedInstance * defender)
 {
-	return createLayout(cb, "default", attacker, defender);
+	return createLayout(gameInfo, "default", attacker, defender);
 }
 
-BattleLayout BattleLayout::createLayout(IGameInfoCallback * cb, const std::string & layoutName, const CArmedInstance * attacker, const CArmedInstance * defender)
+BattleLayout BattleLayout::createLayout(const IGameInfoCallback & gameInfo, const std::string & layoutName, const CArmedInstance * attacker, const CArmedInstance * defender)
 {
 	const auto & loadHex = [](const JsonNode & node)
 	{
@@ -44,7 +44,7 @@ BattleLayout BattleLayout::createLayout(IGameInfoCallback * cb, const std::strin
 		return result;
 	};
 
-	const JsonNode & configRoot = cb->getSettings().getValue(EGameSettings::COMBAT_LAYOUTS);
+	const JsonNode & configRoot = gameInfo.getSettings().getValue(EGameSettings::COMBAT_LAYOUTS);
 	const JsonNode & config = configRoot[layoutName];
 
 	BattleLayout result;

+ 2 - 2
lib/battle/BattleLayout.h

@@ -32,8 +32,8 @@ struct DLL_EXPORT BattleLayout
 	bool tacticsAllowed = false;
 	bool obstaclesAllowed = false;
 
-	static BattleLayout createDefaultLayout(IGameInfoCallback * cb, const CArmedInstance * attacker, const CArmedInstance * defender);
-	static BattleLayout createLayout(IGameInfoCallback * cb, const std::string & layoutName, const CArmedInstance * attacker, const CArmedInstance * defender);
+	static BattleLayout createDefaultLayout(const IGameInfoCallback & gameInfo, const CArmedInstance * attacker, const CArmedInstance * defender);
+	static BattleLayout createLayout(const IGameInfoCallback & gameInfo, const std::string & layoutName, const CArmedInstance * attacker, const CArmedInstance * defender);
 };
 
 VCMI_LIB_NAMESPACE_END

+ 4 - 0
lib/bonuses/Bonus.h

@@ -15,6 +15,7 @@
 #include "../constants/EntityIdentifiers.h"
 #include "../serializer/Serializeable.h"
 #include "../texts/MetaString.h"
+#include "../filesystem/ResourcePath.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -79,6 +80,7 @@ struct DLL_LINKAGE Bonus : public std::enable_shared_from_this<Bonus>, public Se
 	TUpdaterPtr updater;
 	TUpdaterPtr propagationUpdater;
 
+	ImagePath customIconPath;
 	MetaString description;
 
 	Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, BonusSourceID sourceID);
@@ -95,6 +97,8 @@ struct DLL_LINKAGE Bonus : public std::enable_shared_from_this<Bonus>, public Se
 		h & val;
 		h & sid;
 		h & description;
+		if (h.hasFeature(Handler::Version::CUSTOM_BONUS_ICONS))
+			h & customIconPath;
 		h & additionalInfo;
 		h & turnsRemain;
 		h & valType;

+ 0 - 2
lib/bonuses/BonusList.h

@@ -80,8 +80,6 @@ public:
 		std::copy(newList.begin(), newList.end(), bonuses.begin());
 	}
 
-	template <class InputIterator>
-	void insert(const int position, InputIterator first, InputIterator last);
 	void insert(TInternalContainer::iterator position, TInternalContainer::size_type n, const std::shared_ptr<Bonus> & x);
 
 	template <typename Handler>

+ 2 - 2
lib/callback/CGameInfoCallback.cpp

@@ -574,7 +574,7 @@ EDiggingStatus CGameInfoCallback::getTileDigStatus(int3 tile, bool verbose) cons
 	return getTile(tile)->getDiggingStatus();
 }
 
-EBuildingState CGameInfoCallback::canBuildStructure( const CGTownInstance *t, BuildingID ID )
+EBuildingState CGameInfoCallback::canBuildStructure( const CGTownInstance *t, BuildingID ID ) const
 {
 	ERROR_RET_VAL_IF(!canGetFullInfo(t), "Town is not owned!", EBuildingState::TOWN_NOT_OWNED);
 
@@ -947,7 +947,7 @@ void CGameInfoCallback::getAllTiles(std::unordered_set<int3> & tiles, std::optio
 	}
 }
 
-void CGameInfoCallback::getAllowedSpells(std::vector<SpellID> & out, std::optional<ui16> level)
+void CGameInfoCallback::getAllowedSpells(std::vector<SpellID> & out, std::optional<ui16> level) const
 {
 	for (auto const & spellID : gameState().getMap().allowedSpells)
 	{

+ 2 - 2
lib/callback/CGameInfoCallback.h

@@ -93,7 +93,7 @@ public:
 	int howManyTowns(PlayerColor Player) const;
 	std::vector<const CGHeroInstance *> getAvailableHeroes(const CGObjectInstance * townOrTavern) const;
 	std::string getTavernRumor(const CGObjectInstance * townOrTavern) const;
-	EBuildingState canBuildStructure(const CGTownInstance *t, BuildingID ID);
+	EBuildingState canBuildStructure(const CGTownInstance *t, BuildingID ID) const;
 	bool getTownInfo(const CGObjectInstance * town, InfoAboutTown & dest, const CGObjectInstance * selectedObject = nullptr) const;
 
 	//from gs
@@ -115,7 +115,7 @@ public:
 	void getTilesInRange(std::unordered_set<int3> & tiles, const int3 & pos, int radius, ETileVisibility mode, std::optional<PlayerColor> player = std::optional<PlayerColor>(), int3::EDistanceFormula formula = int3::DIST_2D) const override;
 	void getAllTiles(std::unordered_set<int3> &tiles, std::optional<PlayerColor> player, int level, std::function<bool(const TerrainTile *)> filter) const override;
 
-	void getAllowedSpells(std::vector<SpellID> &out, std::optional<ui16> level = std::nullopt);
+	void getAllowedSpells(std::vector<SpellID> &out, std::optional<ui16> level = std::nullopt) const;
 
 #if SCRIPTING_ENABLED
 	virtual scripting::Pool * getGlobalContextPool() const override;

+ 1 - 1
lib/callback/IGameEventCallback.h

@@ -74,7 +74,7 @@ public:
 	virtual void showTeleportDialog(TeleportDialog *iw) =0;
 	virtual void showObjectWindow(const CGObjectInstance * object, EOpenWindowMode window, const CGHeroInstance * visitor, bool addQuery) = 0;
 	virtual void giveResource(PlayerColor player, GameResID which, int val)=0;
-	virtual void giveResources(PlayerColor player, ResourceSet resources)=0;
+	virtual void giveResources(PlayerColor player, const ResourceSet & resources)=0;
 
 	virtual void giveCreatures(const CGHeroInstance * h, const CCreatureSet &creatures) =0;
 	virtual void giveCreatures(const CArmedInstance *objid, const CGHeroInstance * h, const CCreatureSet &creatures, bool remove) =0;

+ 1 - 0
lib/constants/EntityIdentifiers.h

@@ -9,6 +9,7 @@
  */
 #pragma once
 
+#include "Global.h"
 #include "NumericConstants.h"
 #include "IdentifierBase.h"
 

+ 28 - 3
lib/entities/artifact/CArtHandler.cpp

@@ -181,10 +181,35 @@ std::shared_ptr<CArtifact> CArtHandler::loadFromJson(const std::string & scope,
 	loadType(art.get(), node);
 	loadComponents(art.get(), node);
 
-	for(const auto & b : node["bonuses"].Vector())
+	if (node["bonuses"].isVector())
 	{
-		auto bonus = JsonUtils::parseBonus(b);
-		art->addNewBonus(bonus);
+		for(const auto & b : node["bonuses"].Vector())
+		{
+			auto bonus = JsonUtils::parseBonus(b);
+			art->addNewBonus(bonus);
+		}
+	}
+	else
+	{
+		for(const auto & b : node["bonuses"].Struct())
+		{
+			if (b.second.isNull())
+				continue;
+			auto bonus = JsonUtils::parseBonus(b.second);
+			art->addNewBonus(bonus);
+		}
+	}
+
+	for(const auto & b : node["instanceBonuses"].Struct())
+	{
+		if (b.second.isNull())
+			continue;
+		auto bonus = JsonUtils::parseBonus(b.second);
+		bonus->source = BonusSource::ARTIFACT;
+		bonus->duration = BonusDuration::PERMANENT;
+		bonus->description.appendTextID(art->getNameTextID());
+		bonus->description.appendRawString(" %+d");
+		art->instanceBonuses.push_back(bonus);
 	}
 
 	const JsonNode & warMachine = node["warMachine"];

+ 5 - 2
lib/entities/artifact/CArtifact.cpp

@@ -332,8 +332,11 @@ void CArtifact::addNewBonus(const std::shared_ptr<Bonus>& b)
 {
 	b->source = BonusSource::ARTIFACT;
 	b->duration = BonusDuration::PERMANENT;
-	b->description.appendTextID(getNameTextID());
-	b->description.appendRawString(" %+d");
+	if (b->description.empty() && (b->type == BonusType::LUCK || b->type == BonusType::MORALE))
+	{
+		b->description.appendTextID(getNameTextID());
+		b->description.appendRawString(" %+d");
+	}
 	CBonusSystemNode::addNewBonus(b);
 }
 

+ 3 - 0
lib/entities/artifact/CArtifact.h

@@ -102,6 +102,9 @@ class DLL_LINKAGE CArtifact final : public Artifact, public CBonusSystemNode,
 	std::map<ArtBearer, std::vector<ArtifactPosition>> possibleSlots;
 
 public:
+	/// Bonuses that are created for each instance of artifact
+	std::vector<std::shared_ptr<Bonus>> instanceBonuses;
+
 	EArtifactClass aClass = EArtifactClass::ART_SPECIAL;
 	bool onlyOnWaterMap;
 

+ 2 - 1
lib/entities/faction/CTownHandler.cpp

@@ -247,7 +247,8 @@ void CTownHandler::loadBuildingBonuses(const JsonNode & source, BonusList & bonu
 		if(!JsonUtils::parseBonus(b, bonus.get()))
 			continue;
 
-		bonus->description.appendTextID(building->getNameTextID());
+		if (bonus->description.empty() && (bonus->type == BonusType::MORALE || bonus->type == BonusType::LUCK))
+			bonus->description.appendTextID(building->getNameTextID());
 
 		//JsonUtils::parseBuildingBonus produces UNKNOWN type propagator instead of empty.
 		assert(bonus->propagator == nullptr || bonus->propagator->getPropagatorType() != CBonusSystemNode::ENodeTypes::UNKNOWN);

+ 10 - 13
lib/gameState/CGameState.cpp

@@ -151,8 +151,7 @@ int CGameState::getDate(Date mode) const
 	return getDate(day, mode);
 }
 
-CGameState::CGameState(IGameInfoCallback * callback)
-	: GameCallbackHolder(callback)
+CGameState::CGameState()
 {
 	heroesPool = std::make_unique<TavernHeroesPool>(this);
 	globalEffects.setNodeType(CBonusSystemNode::GLOBAL_EFFECTS);
@@ -177,7 +176,6 @@ void CGameState::preInit(Services * newServices)
 void CGameState::init(const IMapService * mapService, StartInfo * si, IGameRandomizer & gameRandomizer, Load::ProgressAccumulator & progressTracking, bool allowSavingRandomMap)
 {
 	assert(services);
-	assert(cb);
 	scenarioOps = CMemorySerializer::deepCopy(*si);
 	initialOpts = CMemorySerializer::deepCopy(*si);
 	si = nullptr;
@@ -284,7 +282,6 @@ void CGameState::updateEntity(Metatype metatype, int32_t index, const JsonNode &
 void CGameState::updateOnLoad(StartInfo * si)
 {
 	assert(services);
-	assert(cb);
 	scenarioOps->playerInfos = si->playerInfos;
 	for(auto & i : si->playerInfos)
 		players.at(i.first).human = i.second.isControlledByHuman();
@@ -301,7 +298,7 @@ void CGameState::initNewGame(const IMapService * mapService, vstd::RNG & randomG
 		CStopWatch sw;
 
 		// Gen map
-		CMapGenerator mapGenerator(*scenarioOps->mapGenOptions, cb, randomGenerator.nextInt());
+		CMapGenerator mapGenerator(*scenarioOps->mapGenOptions, this, randomGenerator.nextInt());
 		progressTracking.include(mapGenerator);
 
 		map = mapGenerator.generate();
@@ -363,7 +360,7 @@ void CGameState::initNewGame(const IMapService * mapService, vstd::RNG & randomG
 	{
 		logGlobal->info("Open map file: %s", scenarioOps->mapname);
 		const ResourcePath mapURI(scenarioOps->mapname, EResType::MAP);
-		map = mapService->loadMap(mapURI, cb);
+		map = mapService->loadMap(mapURI, this);
 	}
 }
 
@@ -526,7 +523,7 @@ void CGameState::initPlayerStates()
 	logGlobal->debug("\tCreating player entries in gs");
 	for(auto & elem : scenarioOps->playerInfos)
 	{
-		players.try_emplace(elem.first, cb);
+		players.try_emplace(elem.first, this);
 		PlayerState & p = players.at(elem.first);
 		p.color=elem.first;
 		p.human = elem.second.isControlledByHuman();
@@ -553,7 +550,7 @@ void CGameState::placeStartingHero(const PlayerColor & playerColor, const HeroTy
 	if (!hero)
 	{
 		auto handler = LIBRARY->objtypeh->getHandlerFor(Obj::HERO, heroTypeId.toHeroType()->heroClass->getIndex());
-		auto object = handler->create(cb, handler->getTemplates().front());
+		auto object = handler->create(this, handler->getTemplates().front());
 		hero = std::dynamic_pointer_cast<CGHeroInstance>(object);
 		hero->ID = Obj::HERO;
 		hero->setHeroType(heroTypeId);
@@ -623,7 +620,7 @@ void CGameState::initHeroes(IGameRandomizer & gameRandomizer)
 		if (tile.isWater())
 		{
 			auto handler = LIBRARY->objtypeh->getHandlerFor(Obj::BOAT, hero->getBoatType().getNum());
-			auto boat = std::dynamic_pointer_cast<CGBoat>(handler->create(cb, nullptr));
+			auto boat = std::dynamic_pointer_cast<CGBoat>(handler->create(this, nullptr));
 			handler->configureObject(boat.get(), gameRandomizer);
 
 			boat->setAnchorPos(hero->anchorPos());
@@ -644,7 +641,7 @@ void CGameState::initHeroes(IGameRandomizer & gameRandomizer)
 		// instances for h3 heroes from roe/ab h3m maps and heroes from mods at this point don't exist -> create them
 		if (!heroInPool)
 		{
-			auto newHeroPtr = std::make_shared<CGHeroInstance>(cb);
+			auto newHeroPtr = std::make_shared<CGHeroInstance>(this);
 			newHeroPtr->subID = htype.getNum();
 			map->addToHeroPool(newHeroPtr);
 			heroInPool = newHeroPtr.get();
@@ -938,7 +935,7 @@ void CGameState::initMapObjects(IGameRandomizer & gameRandomizer)
 		if (q->ID ==Obj::QUEST_GUARD || q->ID ==Obj::SEER_HUT)
 			q->setObjToKill();
 	}
-	CGSubterraneanGate::postInit(cb); //pairing subterranean gates
+	CGSubterraneanGate::postInit(this); //pairing subterranean gates
 
 	map->calculateGuardingGreaturePositions(); //calculate once again when all the guards are placed and initialized
 }
@@ -1031,7 +1028,7 @@ BattleInfo * CGameState::getBattle(const BattleID & battle)
 	return nullptr;
 }
 
-BattleField CGameState::battleGetBattlefieldType(int3 tile, vstd::RNG & randomGenerator)
+BattleField CGameState::battleGetBattlefieldType(int3 tile, vstd::RNG & randomGenerator) const
 {
 	assert(tile.isValid());
 
@@ -1397,7 +1394,7 @@ bool CGameState::checkForStandardLoss(const PlayerColor & player) const
 	return pState.checkVanquished();
 }
 
-void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level)
+void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level) const
 {
 	auto playerInactive = [&](const PlayerColor & color) 
 	{

+ 10 - 7
lib/gameState/CGameState.h

@@ -42,7 +42,7 @@ class UpgradeInfo;
 
 DLL_LINKAGE std::ostream & operator<<(std::ostream & os, const EVictoryLossCheckResult & victoryLossCheckResult);
 
-class DLL_LINKAGE CGameState : public CNonConstInfoCallback, public Serializeable, public GameCallbackHolder
+class DLL_LINKAGE CGameState : public CNonConstInfoCallback, public Serializeable
 {
 	friend class CGameStateCampaign;
 
@@ -66,7 +66,7 @@ public:
 	/// list of players currently making turn. Usually - just one, except for simturns
 	std::set<PlayerColor> actingPlayers;
 
-	CGameState(IGameInfoCallback * callback);
+	CGameState();
 	virtual ~CGameState();
 
 	CGameState & gameState() final { return *this; }
@@ -83,8 +83,6 @@ public:
 	CBonusSystemNode globalEffects;
 	RumorState currentRumor;
 
-	StatisticDataSet statistic;
-
 	// NOTE: effectively AI mutex, only used by adventure map AI
 	static std::shared_mutex mutex;
 
@@ -95,7 +93,7 @@ public:
 	HeroTypeID pickNextHeroType(vstd::RNG & randomGenerator, const PlayerColor & owner);
 
 	void apply(CPackForClient & pack);
-	BattleField battleGetBattlefieldType(int3 tile, vstd::RNG & randomGenerator);
+	BattleField battleGetBattlefieldType(int3 tile, vstd::RNG & randomGenerator) const;
 
 	PlayerRelations getPlayerRelations(PlayerColor color1, PlayerColor color2) const override;
 	void calculatePaths(const std::shared_ptr<PathfinderConfig> & config) const override;
@@ -123,7 +121,8 @@ public:
 	PlayerColor checkForStandardWin() const; //returns color of player that accomplished standard victory conditions or 255 (NEUTRAL) if no winner
 	bool checkForStandardLoss(const PlayerColor & player) const; //checks if given player lost the game
 
-	void obtainPlayersStats(SThievesGuildInfo & tgi, int level); //fills tgi with info about other players that is available at given level of thieves' guild
+	//fills tgi with info about other players that is available at given level of thieves' guild
+	void obtainPlayersStats(SThievesGuildInfo & tgi, int level) const;
 	const IGameSettings & getSettings() const override;
 
 	StartInfo * getStartInfo()
@@ -184,7 +183,11 @@ public:
 			std::map<ArtifactID, int> allocatedArtifactsUnused;
 			h & allocatedArtifactsUnused;
 		}
-		h & statistic;
+		if (!h.hasFeature(Handler::Version::SERVER_STATISTICS))
+		{
+			StatisticDataSet statistic;
+			h & statistic;
+		}
 
 		if(!h.saving && h.loadingGamestate)
 			restoreBonusSystemTree();

+ 1 - 1
lib/gameState/CGameStateCampaign.cpp

@@ -694,7 +694,7 @@ bool CGameStateCampaign::playerHasStartingHero(PlayerColor playerColor) const
 
 std::unique_ptr<CMap> CGameStateCampaign::getCurrentMap()
 {
-	return gameState->scenarioOps->campState->getMap(CampaignScenarioID::NONE, gameState->cb);
+	return gameState->scenarioOps->campState->getMap(CampaignScenarioID::NONE, gameState);
 }
 
 VCMI_LIB_NAMESPACE_END

+ 1 - 1
lib/gameState/GameStatePackVisitor.cpp

@@ -716,7 +716,7 @@ void GameStatePackVisitor::visitSwapStacks(SwapStacks & pack)
 void GameStatePackVisitor::visitInsertNewStack(InsertNewStack & pack)
 {
 	if(auto * obj = gs.getArmyInstance(pack.army))
-		obj->putStack(pack.slot, std::make_unique<CStackInstance>(gs.cb, pack.type, pack.count));
+		obj->putStack(pack.slot, std::make_unique<CStackInstance>(&gs, pack.type, pack.count));
 	else
 		throw std::runtime_error("InsertNewStack: invalid army object " + std::to_string(pack.army.getNum()) + ", possible game state corruption.");
 }

+ 16 - 16
lib/gameState/GameStatistics.cpp

@@ -31,7 +31,7 @@ void StatisticDataSet::add(StatisticDataSetEntry entry)
 	data.push_back(entry);
 }
 
-StatisticDataSetEntry StatisticDataSet::createEntry(const PlayerState * ps, const CGameState * gs)
+StatisticDataSetEntry StatisticDataSet::createEntry(const PlayerState * ps, const CGameState * gs, const StatisticDataSet & accumulatedData)
 {
 	StatisticDataSetEntry data;
 
@@ -63,23 +63,23 @@ StatisticDataSetEntry StatisticDataSet::createEntry(const PlayerState * ps, cons
 	data.numMines = Statistic::getNumMines(gs, ps);
 	data.score = scenarioHighScores.calculate().total;
 	data.maxHeroLevel = Statistic::findBestHero(gs, ps->color) ? Statistic::findBestHero(gs, ps->color)->level : 0;
-	data.numBattlesNeutral = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).numBattlesNeutral : 0;
-	data.numBattlesPlayer = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).numBattlesPlayer : 0;
-	data.numWinBattlesNeutral = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).numWinBattlesNeutral : 0;
-	data.numWinBattlesPlayer = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).numWinBattlesPlayer : 0;
-	data.numHeroSurrendered = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).numHeroSurrendered : 0;
-	data.numHeroEscaped = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).numHeroEscaped : 0;
-	data.spentResourcesForArmy = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).spentResourcesForArmy : TResources();
-	data.spentResourcesForBuildings = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).spentResourcesForBuildings : TResources();
-	data.tradeVolume = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).tradeVolume : TResources();
-	data.eventCapturedTown = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).lastCapturedTownDay == gs->getDate(Date::DAY) : false;
-	data.eventDefeatedStrongestHero = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).lastDefeatedStrongestHeroDay == gs->getDate(Date::DAY) : false;
-	data.movementPointsUsed = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).movementPointsUsed : 0;
+	data.numBattlesNeutral = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).numBattlesNeutral : 0;
+	data.numBattlesPlayer = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).numBattlesPlayer : 0;
+	data.numWinBattlesNeutral = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).numWinBattlesNeutral : 0;
+	data.numWinBattlesPlayer = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).numWinBattlesPlayer : 0;
+	data.numHeroSurrendered = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).numHeroSurrendered : 0;
+	data.numHeroEscaped = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).numHeroEscaped : 0;
+	data.spentResourcesForArmy = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).spentResourcesForArmy : TResources();
+	data.spentResourcesForBuildings = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).spentResourcesForBuildings : TResources();
+	data.tradeVolume = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).tradeVolume : TResources();
+	data.eventCapturedTown = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).lastCapturedTownDay == gs->getDate(Date::DAY) : false;
+	data.eventDefeatedStrongestHero = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).lastDefeatedStrongestHeroDay == gs->getDate(Date::DAY) : false;
+	data.movementPointsUsed = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).movementPointsUsed : 0;
 
 	return data;
 }
 
-std::string StatisticDataSet::toCsv(std::string sep)
+std::string StatisticDataSet::toCsv(std::string sep) const
 {
 	std::stringstream ss;
 
@@ -162,7 +162,7 @@ std::string StatisticDataSet::toCsv(std::string sep)
 		for(auto & resource : resources)
 			ss << sep << entry.resources[resource];
 		for(auto & resource : resources)
-			ss << sep << entry.numMines[resource];
+			ss << sep << entry.numMines.at(resource);
 		for(auto & resource : resources)
 			ss << sep << entry.spentResourcesForArmy[resource];
 		for(auto & resource : resources)
@@ -175,7 +175,7 @@ std::string StatisticDataSet::toCsv(std::string sep)
 	return ss.str();
 }
 
-std::string StatisticDataSet::writeCsv()
+std::string StatisticDataSet::writeCsv() const
 {
 	const boost::filesystem::path outPath = VCMIDirs::get().userCachePath() / "statistic";
 	boost::filesystem::create_directories(outPath);

+ 4 - 4
lib/gameState/GameStatistics.h

@@ -100,10 +100,10 @@ struct DLL_LINKAGE StatisticDataSetEntry
 class DLL_LINKAGE StatisticDataSet
 {
 public:
-    void add(StatisticDataSetEntry entry);
-	static StatisticDataSetEntry createEntry(const PlayerState * ps, const CGameState * gs);
-    std::string toCsv(std::string sep);
-    std::string writeCsv();
+	void add(StatisticDataSetEntry entry);
+	static StatisticDataSetEntry createEntry(const PlayerState * ps, const CGameState * gs, const StatisticDataSet & accumulatedData);
+	std::string toCsv(std::string sep) const;
+	std::string writeCsv() const;
 
 	struct PlayerAccumulatedValueStorage // holds some actual values needed for stats
 	{

+ 19 - 5
lib/json/JsonBonus.cpp

@@ -214,9 +214,12 @@ static void loadBonusAddInfo(CAddInfo & var, BonusType type, const JsonNode & no
 		case BonusType::DESTRUCTION:
 		case BonusType::LIMITED_SHOOTING_RANGE:
 		case BonusType::ACID_BREATH:
+		case BonusType::BIND_EFFECT:
 		case BonusType::SPELLCASTER:
 		case BonusType::FEROCITY:
 		case BonusType::PRIMARY_SKILL:
+		case BonusType::ENCHANTER:
+		case BonusType::SPECIAL_PECULIAR_ENCHANT:
 			// 1 number
 			var = getFirstValue(value).Integer();
 			break;
@@ -635,10 +638,10 @@ std::shared_ptr<ILimiter> JsonUtils::parseLimiter(const JsonNode & limiter)
 	return nullptr;
 }
 
-std::shared_ptr<Bonus> JsonUtils::parseBonus(const JsonNode &ability)
+std::shared_ptr<Bonus> JsonUtils::parseBonus(const JsonNode &ability, const TextIdentifier & descriptionID)
 {
 	auto b = std::make_shared<Bonus>();
-	if (!parseBonus(ability, b.get()))
+	if (!parseBonus(ability, b.get(), descriptionID))
 	{
 		// caller code can not handle this case and presumes that returned bonus is always valid
 		logGlobal->error("Failed to parse bonus! Json config was %S ", ability.toString());
@@ -648,7 +651,7 @@ std::shared_ptr<Bonus> JsonUtils::parseBonus(const JsonNode &ability)
 	return b;
 }
 
-bool JsonUtils::parseBonus(const JsonNode &ability, Bonus *b)
+bool JsonUtils::parseBonus(const JsonNode &ability, Bonus *b, const TextIdentifier & descriptionID)
 {
 	const JsonNode * value = nullptr;
 
@@ -691,12 +694,23 @@ bool JsonUtils::parseBonus(const JsonNode &ability, Bonus *b)
 
 	if(!ability["description"].isNull())
 	{
-		if (ability["description"].isString())
-			b->description.appendTextID(ability["description"].String());
+		if (ability["description"].isString() && !ability["description"].String().empty())
+		{
+			if (ability["description"].String()[0] == '@')
+				b->description.appendTextID(ability["description"].String());
+			else if (!descriptionID.get().empty())
+			{
+				LIBRARY->generaltexth->registerString(ability.getModScope(), descriptionID, ability["description"]);
+				b->description.appendTextID(descriptionID.get());
+			}
+		}
 		if (ability["description"].isNumber())
 			b->description.appendTextID("core.arraytxt." + std::to_string(ability["description"].Integer()));
 	}
 
+	if(!ability["icon"].isNull())
+		b->customIconPath = ImagePath::fromJson(ability["icon"]);
+
 	value = &ability["effectRange"];
 	if (!value->isNull())
 		b->effectRange = static_cast<BonusLimitEffect>(parseByMapN(bonusLimitEffect, value, "effect range "));

+ 3 - 3
lib/json/JsonBonus.h

@@ -11,6 +11,7 @@
 
 #include "JsonNode.h"
 #include "../GameConstants.h"
+#include "../texts/TextIdentifier.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -18,12 +19,11 @@ struct Bonus;
 class ILimiter;
 class CSelector;
 class CAddInfo;
-
 namespace JsonUtils
 {
 	std::shared_ptr<Bonus> parseBonus(const JsonVector & ability_vec);
-	std::shared_ptr<Bonus> parseBonus(const JsonNode & ability);
-	bool parseBonus(const JsonNode & ability, Bonus * placement);
+	std::shared_ptr<Bonus> parseBonus(const JsonNode & ability, const TextIdentifier & descriptionID = "");
+	bool parseBonus(const JsonNode & ability, Bonus * placement, const TextIdentifier & descriptionID = "");
 	std::shared_ptr<ILimiter> parseLimiter(const JsonNode & limiter);
 	CSelector parseSelector(const JsonNode &ability);
 }

+ 1 - 1
lib/mapObjects/CGTownInstance.cpp

@@ -325,7 +325,7 @@ void CGTownInstance::onHeroVisit(IGameEventCallback & gameEvents, const CGHeroIn
 
 				const_cast<CGHeroInstance *>(defendingHero)->setVisitedTown(this, false); //hack to return visitor from garrison after battle
 			}
-			gameEvents.startBattle(h, defendingArmy, getSightCenter(), h, defendingHero, BattleLayout::createDefaultLayout(cb, h, defendingArmy), (isBattleOutside ? nullptr : this));
+			gameEvents.startBattle(h, defendingArmy, getSightCenter(), h, defendingHero, BattleLayout::createDefaultLayout(*cb, h, defendingArmy), (isBattleOutside ? nullptr : this));
 		}
 		else
 		{

+ 1 - 1
lib/mapObjects/CRewardableObject.cpp

@@ -99,7 +99,7 @@ void CRewardableObject::garrisonDialogClosed(IGameEventCallback & gameEvents, co
 
 void CRewardableObject::doStartBattle(IGameEventCallback & gameEvents, const CGHeroInstance * hero) const
 {
-	auto layout = BattleLayout::createLayout(cb, configuration.guardsLayout, hero, this);
+	auto layout = BattleLayout::createLayout(*cb, configuration.guardsLayout, hero, this);
 	gameEvents.startBattle(hero, this, visitablePos(), hero, nullptr, layout, nullptr);
 }
 

+ 18 - 2
lib/mapping/CMap.cpp

@@ -262,6 +262,18 @@ CGHeroInstance * CMap::getHero(HeroTypeID heroID)
 	return nullptr;
 }
 
+const CGHeroInstance * CMap::getHero(HeroTypeID heroID) const
+{
+	for (const auto & objectID : heroesOnMap)
+	{
+		const auto hero = std::dynamic_pointer_cast<CGHeroInstance>(objects.at(objectID.getNum()));
+
+		if (hero->getHeroTypeID() == heroID)
+			return hero.get();
+	}
+	return nullptr;
+}
+
 bool CMap::isCoastalTile(const int3 & pos) const
 {
 	//todo: refactoring: extract neighbour tile iterator and use it in GameState
@@ -855,6 +867,10 @@ CArtifactInstance * CMap::createArtifact(const ArtifactID & artID, const SpellID
 		artInst->addNewBonus(bonus);
 		artInst->addCharges(art->getDefaultStartCharges());
 	}
+
+	for (const auto & bonus : art->instanceBonuses)
+		artInst->addNewBonus(std::make_shared<Bonus>(*bonus));
+
 	return artInst;
 }
 
@@ -868,12 +884,12 @@ const CArtifactInstance * CMap::getArtifactInstance(const ArtifactInstanceID & a
 	return artInstances.at(artifactID.getNum()).get();
 }
 
-const std::vector<ObjectInstanceID> & CMap::getAllTowns()
+const std::vector<ObjectInstanceID> & CMap::getAllTowns() const
 {
 	return towns;
 }
 
-const std::vector<ObjectInstanceID> & CMap::getHeroesOnMap()
+const std::vector<ObjectInstanceID> & CMap::getHeroesOnMap() const
 {
 	return heroesOnMap;
 }

+ 3 - 2
lib/mapping/CMap.h

@@ -233,13 +233,14 @@ public:
 
 	/// Returns pointer to hero of specified type if hero is present on map
 	CGHeroInstance * getHero(HeroTypeID heroId);
+	const CGHeroInstance * getHero(HeroTypeID heroId) const;
 
 	/// Returns ID's of all heroes that are currently present on map
 	/// Includes all garrisoned and imprisoned heroes
-	const std::vector<ObjectInstanceID> & getHeroesOnMap();
+	const std::vector<ObjectInstanceID> & getHeroesOnMap() const;
 
 	/// Returns ID's of all towns present on map
-	const std::vector<ObjectInstanceID> & getAllTowns();
+	const std::vector<ObjectInstanceID> & getAllTowns() const;
 
 	/// Sets the victory/loss condition objectives ??
 	void checkForObjectives();

+ 7 - 0
lib/serializer/BinaryDeserializer.h

@@ -135,6 +135,13 @@ private:
 		////that const cast is evil because it allows to implicitly overwrite const objects when deserializing
 		typedef typename std::remove_const_t<T> nonConstT;
 		auto & hlp = const_cast<nonConstT &>(data);
+
+		if constexpr(std::is_base_of_v<IGameInfoCallback, std::remove_pointer_t<nonConstT>>)
+		{
+			assert(cb == nullptr);
+			cb = &data;
+		}
+
 		hlp.serialize(*this);
 	}
 	template<typename T, typename std::enable_if_t<std::is_array_v<T>, int> = 0>

+ 2 - 7
lib/serializer/Connection.cpp

@@ -124,14 +124,9 @@ void CConnection::enterLobbyConnectionMode()
 	serializer->clear();
 }
 
-void CConnection::setCallback(IGameInfoCallback * cb)
+void CConnection::setCallback(IGameInfoCallback & cb)
 {
-	deserializer->cb = cb;
-}
-
-void CConnection::enterGameplayConnectionMode(CGameState & gs)
-{
-	setCallback(gs.cb);
+	deserializer->cb = &cb;
 }
 
 void CConnection::setSerializationVersion(ESerializationVersion version)

+ 1 - 2
lib/serializer/Connection.h

@@ -50,8 +50,7 @@ public:
 	std::unique_ptr<CPack> retrievePack(const std::vector<std::byte> & data);
 
 	void enterLobbyConnectionMode();
-	void setCallback(IGameInfoCallback * cb);
-	void enterGameplayConnectionMode(CGameState & gs);
+	void setCallback(IGameInfoCallback & cb);
 	void setSerializationVersion(ESerializationVersion version);
 };
 

+ 4 - 2
lib/serializer/ESerializationVersion.h

@@ -23,7 +23,7 @@
 /// - change 'CURRENT' to 'CURRENT = NEW_TEST_KEY'.
 ///
 /// To check for version in serialize() call use form
-/// if (h.version >= Handler::Version::NEW_TEST_KEY)
+/// if (h.hasFeature(Handler::Version::NEW_TEST_KEY))
 ///     h & newKey; // loading/saving save of a new version
 /// else
 ///     newKey = saneDefaultValue; // loading of old save
@@ -42,8 +42,10 @@ enum class ESerializationVersion : int32_t
 	REWARDABLE_EXTENSIONS, // new functionality for rewardable objects
 	FLAGGABLE_BONUS_SYSTEM_NODE, // flaggable objects now contain bonus system node
 	RANDOMIZATION_REWORK, // random rolls logic has been moved to server
+	CUSTOM_BONUS_ICONS, // support for custom icons in bonuses
+	SERVER_STATISTICS, // statistics now only saved on server
 
-	CURRENT = RANDOMIZATION_REWORK,
+	CURRENT = SERVER_STATISTICS,
 };
 
 static_assert(ESerializationVersion::MINIMAL <= ESerializationVersion::CURRENT, "Invalid serialization version definition!");

+ 1 - 0
lib/serializer/SerializerReflection.h

@@ -35,6 +35,7 @@ struct ClassObjectCreator<T, typename std::enable_if_t<std::is_base_of_v<GameCal
 	static T *invoke(IGameInfoCallback *cb)
 	{
 		static_assert(!std::is_abstract_v<T>, "Cannot call new upon abstract classes!");
+		assert(cb != nullptr);
 		return new T(cb);
 	}
 };

+ 1 - 0
mapeditor/Animation.h

@@ -13,6 +13,7 @@
 #include "../lib/GameConstants.h"
 #include <QRgb>
 #include <QImage>
+#include <QDir>
 
 VCMI_LIB_NAMESPACE_BEGIN
 class JsonNode;

+ 2 - 0
mapeditor/CMakeLists.txt

@@ -20,6 +20,7 @@ set(editor_SRCS
 		mapsettings/eventsettings.cpp
 		mapsettings/rumorsettings.cpp
 		mapsettings/translations.cpp
+		PlayerSelectionDialog.cpp
 		playersettings.cpp
 		playerparams.cpp
 		scenelayer.cpp
@@ -70,6 +71,7 @@ set(editor_HEADERS
 		mapsettings/eventsettings.h
 		mapsettings/rumorsettings.h
 		mapsettings/translations.h
+		PlayerSelectionDialog.h
 		playersettings.h
 		playerparams.h
 		scenelayer.h

+ 104 - 0
mapeditor/PlayerSelectionDialog.cpp

@@ -0,0 +1,104 @@
+/*
+ * PlayerSelectionDialog.cpp, 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
+ *
+ */
+
+#include "PlayerSelectionDialog.h"
+#include "../lib/mapping/CMap.h"
+#include "mainwindow.h"
+
+#include <QRadioButton>
+#include <QButtonGroup>
+#include <QDialogButtonBox>
+#include <QAction>
+#include <QLabel>
+
+
+PlayerSelectionDialog::PlayerSelectionDialog(MainWindow * mainWindow)
+	: QDialog(mainWindow), selectedPlayer(PlayerColor::NEUTRAL)
+{
+	setupDialogComponents();
+
+	int maxPlayers = 0;
+	if(mainWindow && mainWindow->controller.map())
+		maxPlayers = mainWindow->controller.map()->players.size();
+
+	for(int i = 0; i < maxPlayers; ++i)
+	{
+		PlayerColor player(i);
+		addRadioButton(mainWindow->getActionPlayer(player), player);
+	}
+}
+
+PlayerColor PlayerSelectionDialog::getSelectedPlayer() const
+{
+	return selectedPlayer;
+}
+
+void PlayerSelectionDialog::setupDialogComponents()
+{
+	setWindowTitle(tr("Select Player"));
+	setFixedWidth(dialogWidth);
+	setWindowFlags(Qt::Dialog | Qt::MSWindowsFixedSizeDialogHint);
+	font.setPointSize(10);
+	setFont(font);
+
+	buttonGroup = new QButtonGroup(this);
+	buttonGroup->setExclusive(true);
+
+	QLabel * errorLabel = new QLabel(tr("Hero cannot be created as NEUTRAL"), this);
+	font.setBold(true);
+	errorLabel->setFont(font);
+	errorLabel->setWordWrap(true);
+	mainLayout.addWidget(errorLabel);
+
+	QLabel * instructionLabel = new QLabel(tr("Switch to one of the available players:"), this);
+	font.setBold(false);
+	instructionLabel->setFont(font);
+	instructionLabel->setWordWrap(true);
+	mainLayout.addWidget(instructionLabel);
+
+	QWidget * radioContainer = new QWidget(this);
+	radioContainer->setLayout(& radioButtonsLayout);
+	mainLayout.addWidget(radioContainer);
+
+	QDialogButtonBox * box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
+	connect(box, &QDialogButtonBox::accepted, this, &QDialog::accept);
+	connect(box, &QDialogButtonBox::rejected, this, &QDialog::reject);
+	mainLayout.addWidget(box);
+
+	setLayout(& mainLayout);
+}
+
+void PlayerSelectionDialog::addRadioButton(QAction * action, PlayerColor player)
+{
+	auto * radioButton = new QRadioButton(action->text(), this);
+	radioButton->setEnabled(action->isEnabled());
+	// Select first available player by default
+	if(buttonGroup->buttons().isEmpty() && radioButton->isEnabled())
+	{
+		radioButton->setChecked(true);
+		selectedPlayer = player;
+	}
+
+	radioButton->setToolTip(tr("Shortcut: %1").arg(action->shortcut().toString()));
+	buttonGroup->addButton(radioButton, player.getNum());
+	radioButtonsLayout.addWidget(radioButton);
+
+	connect(radioButton, &QRadioButton::clicked, this, [this, player]()
+		{
+			selectedPlayer = player;
+		});
+
+	addAction(action);
+	connect(action, &QAction::triggered, this, [radioButton]()
+		{
+			if(radioButton->isEnabled())
+				radioButton->click();
+		});
+}

+ 45 - 0
mapeditor/PlayerSelectionDialog.h

@@ -0,0 +1,45 @@
+/*
+ * PlayerSelectionDialog.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 <QDialog>
+#include <QVBoxLayout>
+#include "../lib/constants/EntityIdentifiers.h"
+
+class QRadioButton;
+class QButtonGroup;
+class MainWindow;
+
+/// Dialog shown when a hero cannot be placed as NEUTRAL.
+/// Allows the user to select a valid player via checkboxes,
+/// or using the existing keyboard shortcuts from MainWindow's player QActions.
+class PlayerSelectionDialog : public QDialog
+{
+	Q_OBJECT
+
+public:
+	explicit PlayerSelectionDialog(MainWindow * mainWindow);
+	PlayerColor getSelectedPlayer() const;
+
+private:
+	const int dialogWidth = 320;
+
+	QButtonGroup * buttonGroup;
+	PlayerColor selectedPlayer;
+
+	QFont font;
+	QVBoxLayout mainLayout;
+	QVBoxLayout radioButtonsLayout;
+
+	void setupDialogComponents();
+	void addRadioButton(QAction * action, PlayerColor player);
+
+};

+ 1 - 1
mapeditor/campaigneditor/campaigneditor.cpp

@@ -249,7 +249,7 @@ void CampaignEditor::on_actionScenarioProperties_triggered()
 void CampaignEditor::closeEvent(QCloseEvent *event)
 {
 	if(getAnswerAboutUnsavedChanges())
-		QDialog::closeEvent(event);
+		QWidget::closeEvent(event);
 	else
 		event->ignore();
 }

+ 2 - 2
mapeditor/campaigneditor/campaigneditor.h

@@ -8,7 +8,7 @@
  *
  */
 #pragma once
-#include <QDialog>
+#include <QWidget>
 
 #include "campaignview.h"
 
@@ -21,7 +21,7 @@ namespace Ui {
 class CampaignEditor;
 }
 
-class CampaignEditor : public QDialog
+class CampaignEditor : public QWidget
 {
 	Q_OBJECT
 

+ 2 - 2
mapeditor/campaigneditor/campaigneditor.ui

@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <ui version="4.0">
  <class>CampaignEditor</class>
- <widget class="QDialog" name="CampaignEditor">
+ <widget class="QWidget" name="CampaignEditor">
   <property name="geometry">
    <rect>
     <x>0</x>
@@ -26,7 +26,7 @@
    <property name="bottomMargin">
     <number>0</number>
    </property>
-   <property name="Spacing" stdset="0">
+   <property name="spacing">
     <number>0</number>
    </property>
    <item>

+ 20 - 9
mapeditor/mainwindow.cpp

@@ -45,6 +45,7 @@
 #include "inspector/inspector.h"
 #include "mapsettings/mapsettings.h"
 #include "mapsettings/translations.h"
+#include "mapsettings/modsettings.h"
 #include "playersettings.h"
 #include "validator.h"
 #include "helper.h"
@@ -371,6 +372,10 @@ void MainWindow::initializeMap(bool isNew)
 	initialScale = ui->mapView->mapToScene(ui->mapView->viewport()->geometry()).boundingRect();
 	
 	//enable settings
+	mapSettings = new MapSettings(controller, this);
+	connect(&controller, &MapController::requestModsUpdate,
+		mapSettings->getModSettings(), &ModSettings::updateModWidgetBasedOnMods);
+
 	ui->actionMapSettings->setEnabled(true);
 	ui->actionPlayers_settings->setEnabled(true);
 	ui->actionTranslations->setEnabled(true);
@@ -1124,9 +1129,15 @@ void MainWindow::on_inspectorWidget_itemChanged(QTableWidgetItem *item)
 
 void MainWindow::on_actionMapSettings_triggered()
 {
-	auto settingsDialog = new MapSettings(controller, this);
-	settingsDialog->setWindowModality(Qt::WindowModal);
-	settingsDialog->setModal(true);
+	if(mapSettings->isVisible())
+	{
+		mapSettings->raise();
+		mapSettings->activateWindow();
+	}
+	else
+	{
+		mapSettings->show();
+	}
 }
 
 
@@ -1155,15 +1166,15 @@ void MainWindow::switchDefaultPlayer(const PlayerColor & player)
 {
 	if(controller.defaultPlayer == player)
 		return;
-	
-	ui->actionNeutral->blockSignals(true);
+
+	QSignalBlocker blockerNeutral(ui->actionNeutral);
 	ui->actionNeutral->setChecked(PlayerColor::NEUTRAL == player);
-	ui->actionNeutral->blockSignals(false);
+
 	for(int i = 0; i < PlayerColor::PLAYER_LIMIT.getNum(); ++i)
 	{
-		getActionPlayer(PlayerColor(i))->blockSignals(true);
-		getActionPlayer(PlayerColor(i))->setChecked(PlayerColor(i) == player);
-		getActionPlayer(PlayerColor(i))->blockSignals(false);
+		QAction * playerEntry = getActionPlayer(PlayerColor(i));
+		QSignalBlocker blocker(playerEntry); 
+		playerEntry->setChecked(PlayerColor(i) == player);
 	}
 	controller.defaultPlayer = player;
 }

+ 13 - 8
mapeditor/mainwindow.h

@@ -3,11 +3,14 @@
 #include <QMainWindow>
 #include <QGraphicsScene>
 #include <QStandardItemModel>
+#include <QTranslator>
+#include <QTableWidgetItem>
 #include "mapcontroller.h"
 #include "resourceExtractor/ResourceConverter.h"
 
 class ObjectBrowser;
 class ObjectBrowserProxyModel;
+class MapSettings;
 
 VCMI_LIB_NAMESPACE_BEGIN
 class CConsoleHandler;
@@ -24,7 +27,7 @@ namespace Ui
 
 class MainWindow : public QMainWindow
 {
-    Q_OBJECT
+	Q_OBJECT
 
 	const QString mainWindowSizeSetting = "MainWindow/Size";
 	const QString mainWindowPositionSetting = "MainWindow/Position";
@@ -41,8 +44,8 @@ class MainWindow : public QMainWindow
 	std::unique_ptr<CBasicLogConfigurator> logConfig;
 
 public:
-    explicit MainWindow(QWidget *parent = nullptr);
-    ~MainWindow();
+	explicit MainWindow(QWidget *parent = nullptr);
+	~MainWindow();
 
 	void initializeMap(bool isNew);
 
@@ -61,6 +64,11 @@ public:
 
 	void loadTranslation();
 
+	QAction * getActionPlayer(const PlayerColor &);
+
+public slots:
+	void switchDefaultPlayer(const PlayerColor &);
+
 private slots:
 	void on_actionOpen_triggered();
 	
@@ -109,8 +117,6 @@ private slots:
 	void on_actionUpdate_appearance_triggered();
 
 	void on_actionRecreate_obstacles_triggered();
-	
-	void switchDefaultPlayer(const PlayerColor &);
 
 	void on_actionCut_triggered();
 
@@ -168,8 +174,6 @@ private:
 	void preparePreview(const QModelIndex & index);
 	void addGroupIntoCatalog(const QString & groupName, bool staticOnly);
 	void addGroupIntoCatalog(const QString & groupName, bool useCustomName, bool staticOnly, int ID);
-	
-	QAction * getActionPlayer(const PlayerColor &);
 
 	void changeBrushState(int idx);
 	void setTitle();
@@ -186,9 +190,10 @@ private:
 	void updateRecentMenu(const QString & filenameSelect);
 
 private:
-    Ui::MainWindow * ui;
+	Ui::MainWindow * ui;
 	ObjectBrowserProxyModel * objectBrowser = nullptr;
 	QGraphicsScene * scenePreview;
+	MapSettings * mapSettings = nullptr;
 	
 	QString filename;
 	QString lastSavingDir;

+ 125 - 50
mapeditor/mapcontroller.cpp

@@ -29,12 +29,19 @@
 #include "../lib/spells/CSpellHandler.h"
 #include "../lib/CRandomGenerator.h"
 #include "../lib/serializer/CMemorySerializer.h"
+#include "mapsettings/modsettings.h"
 #include "mapview.h"
 #include "scenelayer.h"
 #include "maphandler.h"
 #include "mainwindow.h"
 #include "inspector/inspector.h"
 #include "GameLibrary.h"
+#include "PlayerSelectionDialog.h"
+
+MapController::MapController(QObject * parent)
+	: QObject(parent)
+{
+}
 
 MapController::MapController(MainWindow * m): main(m)
 {
@@ -365,7 +372,7 @@ void MapController::pasteFromClipboard(int level)
 	{
 		auto obj = CMemorySerializer::deepCopyShared(*objUniquePtr);
 		QString errorMsg;
-		if (!canPlaceObject(level, obj.get(), errorMsg))
+		if(!canPlaceObject(obj.get(), errorMsg))
 		{
 			errors.push_back(std::move(errorMsg));
 			continue;
@@ -536,33 +543,96 @@ void MapController::commitObjectCreate(int level)
 	main->mapChanged();
 }
 
-bool MapController::canPlaceObject(int level, CGObjectInstance * newObj, QString & error) const
+bool MapController::canPlaceObject(const CGObjectInstance * newObj, QString & error) const
+{	
+	if(newObj->ID == Obj::GRAIL) //special case for grail
+		return canPlaceGrail(newObj, error);
+	
+	if(defaultPlayer == PlayerColor::NEUTRAL && (newObj->ID == Obj::HERO || newObj->ID == Obj::RANDOM_HERO))
+		return canPlaceHero(newObj, error);
+	
+	return checkRequiredMods(newObj, error);
+}
+
+bool MapController::canPlaceGrail(const CGObjectInstance * grailObj, QString & error) const
 {
+	assert(grailObj->ID == Obj::GRAIL);
+
 	//find all objects of such type
 	int objCounter = 0;
 	for(auto o : _map->objects)
 	{
-		if(o->ID == newObj->ID && o->subID == newObj->subID)
+		if(o->ID == grailObj->ID && o->subID == grailObj->subID)
 		{
 			++objCounter;
 		}
 	}
-	
-	if(newObj->ID == Obj::GRAIL && objCounter >= 1) //special case for grail
+
+	if(objCounter >= 1)
 	{
 		error = QObject::tr("There can only be one grail object on the map.");
 		return false; //maplimit reached
 	}
 	
-	if(defaultPlayer == PlayerColor::NEUTRAL && (newObj->ID == Obj::HERO || newObj->ID == Obj::RANDOM_HERO))
+	return true;
+}
+
+bool MapController::canPlaceHero(const CGObjectInstance * heroObj, QString & error) const
+{
+	assert(heroObj->ID == Obj::HERO || heroObj->ID == Obj::RANDOM_HERO);
+
+	PlayerSelectionDialog dialog(main);
+	if(dialog.exec() == QDialog::Accepted)
 	{
-		error = QObject::tr("Hero %1 cannot be created as NEUTRAL.").arg(QString::fromStdString(newObj->instanceName));
-		return false;
+		main->switchDefaultPlayer(dialog.getSelectedPlayer());
+		return true;
 	}
 	
+	error = tr("Hero %1 cannot be created as NEUTRAL.").arg(QString::fromStdString(heroObj->instanceName));
+	return false;
+}
+
+bool MapController::checkRequiredMods(const CGObjectInstance * obj, QString & error) const
+{
+	ModCompatibilityInfo modsInfo;
+	modAssessmentObject(obj, modsInfo);
+
+	for(auto & mod : modsInfo)
+	{
+		if(!_map->mods.count(mod.first))
+		{
+			auto reply = QMessageBox::question(main,
+				tr("Missing Required Mod"), modMissingMessage(mod.second) + tr("\n\nDo you want to do that now ?"),
+				QMessageBox::Yes | QMessageBox::No);
+
+			if(reply == QMessageBox::Yes)
+			{
+				_map->mods[mod.first] = LIBRARY->modh->getModInfo(mod.first).getVerificationInfo();
+				Q_EMIT requestModsUpdate(modsInfo, true); // signal for MapSettings
+			}
+			else
+			{
+				error = tr("This object's mod is mandatory for map to remain valid.");
+				return false;
+			}
+		}
+	}
 	return true;
 }
 
+QString MapController::modMissingMessage(const ModVerificationInfo & info)
+{
+	QString modName = QString::fromStdString(info.name);
+	QString submod;
+	if(!info.parent.empty())
+		submod = QObject::tr(" (submod of %1)").arg(QString::fromStdString(info.parent));
+
+	return QObject::tr("The mod '%1'%2, is required by an object on the map.\n"
+		"Add it to the map's required mods in Map->General settings.",
+		"should be consistent with Map->General menu entry translation")
+		.arg(modName, submod);
+}
+
 void MapController::undo()
 {
 	_map->getEditManager()->getUndoManager().undo();
@@ -587,70 +657,75 @@ ModCompatibilityInfo MapController::modAssessmentAll()
 		for(auto secondaryID : LIBRARY->objtypeh->knownSubObjects(primaryID))
 		{
 			auto handler = LIBRARY->objtypeh->getHandlerFor(primaryID, secondaryID);
-			auto modName = QString::fromStdString(handler->getJsonKey()).split(":").at(0).toStdString();
-			if(modName != "core")
-				result[modName] = LIBRARY->modh->getModInfo(modName).getVerificationInfo();
+			auto modScope = handler->getModScope();
+			if(modScope != "core")
+				result[modScope] = LIBRARY->modh->getModInfo(modScope).getVerificationInfo();
 		}
 	}
 	return result;
 }
 
-ModCompatibilityInfo MapController::modAssessmentMap(const CMap & map)
+void MapController::modAssessmentObject(const CGObjectInstance * obj, ModCompatibilityInfo & result)
 {
-	ModCompatibilityInfo result;
-
-	auto extractEntityMod = [&result](const auto & entity) 
+	auto extractEntityMod = [&result](const auto & entity)
 	{
 		auto modScope = entity->getModScope();
 		if(modScope != "core")
 			result[modScope] = LIBRARY->modh->getModInfo(modScope).getVerificationInfo();
 	};
 
-	for(auto obj : map.objects)
-	{
-		auto handler = obj->getObjectHandler();
-		auto modScope = handler->getModScope();
-		if(modScope != "core")
-			result[modScope] = LIBRARY->modh->getModInfo(modScope).getVerificationInfo();
+	auto handler = obj->getObjectHandler();
+	auto modScope = handler->getModScope();
+	if(modScope != "core")
+		result[modScope] = LIBRARY->modh->getModInfo(modScope).getVerificationInfo();
 
-		if(obj->ID == Obj::TOWN || obj->ID == Obj::RANDOM_TOWN)
+	if(obj->ID == Obj::TOWN || obj->ID == Obj::RANDOM_TOWN)
+	{
+		auto town = dynamic_cast<const CGTownInstance *>(obj);
+		for(const auto & spellID : town->possibleSpells)
 		{
-			auto town = dynamic_cast<CGTownInstance *>(obj.get());
-			for(const auto & spellID : town->possibleSpells)
-			{
-				if(spellID == SpellID::PRESET)
-					continue;
-				extractEntityMod(spellID.toEntity(LIBRARY));
-			}
+			if(spellID == SpellID::PRESET)
+				continue;
+			extractEntityMod(spellID.toEntity(LIBRARY));
+		}
 
-			for(const auto & spellID : town->obligatorySpells)
-			{
-				extractEntityMod(spellID.toEntity(LIBRARY));
-			}
+		for(const auto & spellID : town->obligatorySpells)
+		{
+			extractEntityMod(spellID.toEntity(LIBRARY));
 		}
+	}
 
-		if(obj->ID == Obj::HERO || obj->ID == Obj::RANDOM_HERO)
+	if(obj->ID == Obj::HERO || obj->ID == Obj::RANDOM_HERO)
+	{
+		auto hero = dynamic_cast<const CGHeroInstance *>(obj);
+		for(const auto & spellID : hero->getSpellsInSpellbook())
 		{
-			auto hero = dynamic_cast<CGHeroInstance *>(obj.get());
-			for(const auto & spellID : hero->getSpellsInSpellbook())
-			{
-				if(spellID == SpellID::PRESET || spellID == SpellID::SPELLBOOK_PRESET)
-					continue;
-				extractEntityMod(spellID.toEntity(LIBRARY));
-			}
+			if(spellID == SpellID::PRESET || spellID == SpellID::SPELLBOOK_PRESET)
+				continue;
+			extractEntityMod(spellID.toEntity(LIBRARY));
+		}
 
-			for(const auto & [_, slotInfo] : hero->artifactsWorn)
-			{
-				extractEntityMod(slotInfo.getArt()->getTypeId().toEntity(LIBRARY));
-			}
+		for(const auto & [_, slotInfo] : hero->artifactsWorn)
+		{
+			extractEntityMod(slotInfo.getArt()->getTypeId().toEntity(LIBRARY));
+		}
 
-			for(const auto & art : hero->artifactsInBackpack)
-			{
-				extractEntityMod(art.getArt()->getTypeId().toEntity(LIBRARY));
-			}
+		for(const auto & art : hero->artifactsInBackpack)
+		{
+			extractEntityMod(art.getArt()->getTypeId().toEntity(LIBRARY));
 		}
 	}
 
-	//TODO: terrains?
+//TODO: terrains?
+}
+
+ModCompatibilityInfo MapController::modAssessmentMap(const CMap & map)
+{
+	ModCompatibilityInfo result;
+
+	for(auto obj : map.objects)
+	{
+		modAssessmentObject(obj.get(), result);
+	}
 	return result;
 }

+ 24 - 3
mapeditor/mapcontroller.h

@@ -12,17 +12,20 @@
 
 #include "maphandler.h"
 #include "mapview.h"
+#include "lib/modding/ModVerificationInfo.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
-struct ModVerificationInfo;
 using ModCompatibilityInfo = std::map<std::string, ModVerificationInfo>;
 class EditorObstaclePlacer;
 VCMI_LIB_NAMESPACE_END
 
 class MainWindow;
-class MapController
+class MapController : public QObject
 {
+	Q_OBJECT
+
 public:
+	explicit MapController(QObject * parent = nullptr);
 	MapController(MainWindow *);
 	MapController(const MapController &) = delete;
 	MapController(const MapController &&) = delete;
@@ -60,16 +63,34 @@ public:
 	
 	bool discardObject(int level) const;
 	void createObject(int level, std::shared_ptr<CGObjectInstance> obj) const;
-	bool canPlaceObject(int level, CGObjectInstance * obj, QString & error) const;
+	bool canPlaceObject(const CGObjectInstance * obj, QString & error) const;
+	bool canPlaceGrail(const CGObjectInstance * grailObj, QString & error) const;
+	bool canPlaceHero(const CGObjectInstance * heroObj, QString & error) const;
 	
+	/// Ensures that the object's mod is listed in the map's required mods.
+	/// If the mod is missing, prompts the user to add it. Returns false if the user declines,
+	/// making the object invalid for placement.
+	bool checkRequiredMods(const CGObjectInstance * obj, QString & error) const;
+
+	/// These functions collect mod verification data for gameplay objects by scanning map objects
+	/// and their nested elements (like spells and artifacts). The gathered information
+	/// is used to assess compatibility and integrity of mods used in a given map or game state
+	static void modAssessmentObject(const CGObjectInstance * obj, ModCompatibilityInfo & result);
 	static ModCompatibilityInfo modAssessmentAll();
 	static ModCompatibilityInfo modAssessmentMap(const CMap & map);
 
+	/// Returns formatted message string describing a missing mod requirement for the map.
+	/// Used in both warnings and confirmations related to required mod dependencies.
+	static QString modMissingMessage(const ModVerificationInfo & info);
+
 	void undo();
 	void redo();
 	
 	PlayerColor defaultPlayer;
 	QDialog * settingsDialog = nullptr;
+
+signals:
+	void requestModsUpdate(const ModCompatibilityInfo & mods, bool leaveCheckedUnchanged) const;
 	
 private:
 	std::unique_ptr<CMap> _map;

+ 7 - 2
mapeditor/mapsettings/mapsettings.cpp

@@ -25,12 +25,12 @@ MapSettings::MapSettings(MapController & ctrl, QWidget *parent) :
 	controller(ctrl)
 {
 	ui->setupUi(this);
-	
+
+	setWindowModality(Qt::WindowModal);
 	setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowCloseButtonHint);
 	
 	assert(controller.map());
 	controller.settingsDialog = this;
-	show();
 
 	for(auto const & objectPtr : LIBRARY->skillh->objects)
 	{
@@ -79,6 +79,11 @@ MapSettings::~MapSettings()
 	delete ui;
 }
 
+ModSettings * MapSettings::getModSettings()
+{
+	return ui->mods;
+}
+
 void MapSettings::on_pushButton_clicked()
 {	
 	auto updateMapArray = [](const QListWidget * widget, auto & arr)

+ 4 - 0
mapeditor/mapsettings/mapsettings.h

@@ -18,6 +18,8 @@ namespace Ui {
 class MapSettings;
 }
 
+class ModSettings;
+
 class MapSettings : public QDialog
 {
 	Q_OBJECT
@@ -26,6 +28,8 @@ public:
 	explicit MapSettings(MapController & controller, QWidget *parent = nullptr);
 	~MapSettings();
 
+	ModSettings * getModSettings();
+
 private slots:
 	void on_pushButton_clicked();
 

+ 14 - 2
mapeditor/mapsettings/modsettings.cpp

@@ -44,6 +44,8 @@ void ModSettings::initialize(MapController & c)
 	QMap<QString, QTreeWidgetItem*> addedMods;
 	QSet<QString> modsToProcess;
 	ui->treeMods->blockSignals(true);
+	ui->treeMods->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
+	ui->treeMods->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
 
 	auto createModTreeWidgetItem = [&](QTreeWidgetItem * parent, const ModDescription & modInfo)
 	{
@@ -59,6 +61,8 @@ void ModSettings::initialize(MapController & c)
 
 	for(const auto & modName : LIBRARY->modh->getActiveMods())
 	{
+		if(modName == "core")
+			continue;
 		QString qmodName = QString::fromStdString(modName);
 		if(qmodName.split(".").size() == 1)
 		{
@@ -115,13 +119,21 @@ void ModSettings::update()
 	}
 }
 
-void ModSettings::updateModWidgetBasedOnMods(const ModCompatibilityInfo & mods)
+void ModSettings::updateModWidgetBasedOnMods(const ModCompatibilityInfo & mods, bool leaveCheckedUnchanged)
 {
 	//Mod management
 	auto widgetAction = [&](QTreeWidgetItem * item)
 	{
 		auto modName = item->data(0, Qt::UserRole).toString().toStdString();
-		item->setCheckState(0, mods.count(modName) ? Qt::Checked : Qt::Unchecked);
+		if(leaveCheckedUnchanged)
+		{
+			if(mods.count(modName))
+				item->setCheckState(0, Qt::Checked);
+		}
+		else
+		{
+			item->setCheckState(0, mods.count(modName) ? Qt::Checked : Qt::Unchecked);
+		}
 	};
 
 	for (int i = 0; i < ui->treeMods->topLevelItemCount(); ++i)

+ 2 - 3
mapeditor/mapsettings/modsettings.h

@@ -26,6 +26,8 @@ public:
 	void initialize(MapController & map) override;
 	void update() override;
 
+	void updateModWidgetBasedOnMods(const ModCompatibilityInfo & mods, bool leaveCheckedUnchanged = false);
+
 private slots:
 	void on_modResolution_map_clicked();
 
@@ -33,9 +35,6 @@ private slots:
 
 	void on_treeMods_itemChanged(QTreeWidgetItem *item, int column);
 
-private:
-	void updateModWidgetBasedOnMods(const ModCompatibilityInfo & mods);
-
 private:
 	Ui::ModSettings *ui;
 };

+ 1 - 1
mapeditor/mapview.cpp

@@ -614,7 +614,7 @@ void MapView::dropEvent(QDropEvent * event)
 	if(sc->selectionObjectsView.newObject)
 	{
 		QString errorMsg;
-		if(controller->canPlaceObject(sc->level, sc->selectionObjectsView.newObject.get(), errorMsg))
+		if(controller->canPlaceObject(sc->selectionObjectsView.newObject.get(), errorMsg))
 		{
 			auto obj = sc->selectionObjectsView.newObject;
 			controller->commitObjectCreate(sc->level);

+ 1 - 0
mapeditor/mapview.h

@@ -12,6 +12,7 @@
 
 #include <QGraphicsScene>
 #include <QGraphicsView>
+#include <QRubberBand>
 #include "scenelayer.h"
 #include "../lib/int3.h"
 

+ 127 - 15
mapeditor/validator.cpp

@@ -24,20 +24,12 @@ Validator::Validator(const CMap * map, QWidget *parent) :
 	ui(new Ui::Validator)
 {
 	ui->setupUi(this);
+
+	screenGeometry = QApplication::primaryScreen()->availableGeometry();
 	
 	setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowCloseButtonHint);
 	
-	show();
-	
-	setAttribute(Qt::WA_DeleteOnClose);
-
-	std::array<QString, 2> icons{":/icons/mod-update.png", ":/icons/mod-delete.png"};
-
-	for(auto & issue : Validator::validate(map))
-	{
-		auto * item = new QListWidgetItem(QIcon(icons[issue.critical ? 1 : 0]), issue.message);
-		ui->listWidget->addItem(item);
-	}
+	showValidationResults(map);
 }
 
 Validator::~Validator()
@@ -175,13 +167,11 @@ std::set<Validator::Issue> Validator::validate(const CMap * map)
 		if(map->description.empty())
 			issues.insert({ tr("Map description is not specified"), false });
 		
-		//verificationfor mods
+		//verification for mods
 		for(auto & mod : MapController::modAssessmentMap(*map))
 		{
 			if(!map->mods.count(mod.first))
-			{
-				issues.insert({ tr("Map contains object from mod \"%1\", but doesn't require it").arg(QString::fromStdString(LIBRARY->modh->getModInfo(mod.first).getVerificationInfo().name)), true });
-			}
+				issues.insert({ MapController::modMissingMessage(mod.second), true });
 		}
 	}
 	catch(const std::exception & e)
@@ -195,3 +185,125 @@ std::set<Validator::Issue> Validator::validate(const CMap * map)
 	
 	return issues;
 }
+
+void Validator::showValidationResults(const CMap * map)
+{
+	show();
+	setAttribute(Qt::WA_DeleteOnClose);
+	ui->listWidget->setItemDelegate(new ValidatorItemDelegate(ui->listWidget));
+
+	for(auto const & issue : Validator::validate(map))
+	{
+		auto * item = new QListWidgetItem(QIcon(issue.critical ? ":/icons/mod-delete.png" : ":/icons/mod-update.png"),
+			issue.message, ui->listWidget);
+
+		ui->listWidget->addItem(item);
+	}
+
+	if(ui->listWidget->count() == 0)
+	{
+		QPixmap greenTick(":/icons/mod-enabled.png");
+		QString validMessage = tr("The map is valid and has no issues.");
+		auto * item = new QListWidgetItem(QIcon(greenTick), validMessage, ui->listWidget);
+		ui->listWidget->addItem(item);
+	}
+
+	ui->listWidget->updateGeometry();
+
+	adjustWindowSize();
+}
+
+void Validator::adjustWindowSize()
+{
+	const int minWidth = 350;
+	const int minHeight = 50;
+	const int padding = 30;		// reserved space for eventual scrollbars
+	const int screenMarginVertical = 300;
+	const int screenMarginHorizontal = 350;
+
+	int contentHeight = minHeight;
+	int contentWidth = minWidth;
+
+	QStyleOptionViewItem option;
+	option.initFrom(ui->listWidget);
+
+	int listWidgetWidth = ui->listWidget->viewport()->width();
+
+	for(int i = 0; i < ui->listWidget->count(); ++i)
+	{
+		option.rect = QRect(0, 0, listWidgetWidth, 0);
+		auto itemSize = ui->listWidget->itemDelegate()->sizeHint(option, ui->listWidget->model()->index(i, 0));
+		contentHeight += itemSize.height();
+		contentWidth = qMax(contentWidth, itemSize.width());
+	}
+
+	int screenWidth = screenGeometry.width();
+	int screenHeight = screenGeometry.height();
+
+	int finalWidth = qMin(contentWidth + padding, screenWidth - screenMarginHorizontal);
+	int finalHeight = qMin(contentHeight + padding, screenHeight - screenMarginVertical);
+
+	QWidget * parentWidget = ui->listWidget->parentWidget();
+	if(parentWidget)
+	{
+		parentWidget->setMinimumWidth(finalWidth + padding);
+		parentWidget->setMinimumHeight(finalHeight + padding);
+	}
+
+	ui->listWidget->resize(finalWidth, finalHeight);
+
+	move((screenWidth - finalWidth) / 2, (screenHeight - finalHeight) / 2);
+}
+
+void ValidatorItemDelegate::paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const
+{
+	painter->save();
+
+	QStyleOptionViewItem opt(option);
+	QFontMetrics metrics(option.fontMetrics);
+	initStyleOption(&opt, index);
+
+	const QRect iconRect = option.rect.adjusted(iconPadding, iconPadding, 0, 0);
+
+	const QRect textRect = option.rect.adjusted(offsetForIcon, 0, -textPaddingRight, 0);
+
+	if(!opt.icon.isNull())
+	{
+		opt.icon.paint(painter, iconRect, Qt::AlignTop | Qt::AlignLeft);
+	}
+
+	QTextOption textOption;
+
+	int textWidth = metrics.horizontalAdvance(opt.text);
+	if(textWidth + offsetForIcon + textPaddingRight > screenGeometry.width() - screenMargin)
+	{
+		textOption.setWrapMode(QTextOption::WordWrap);
+	}
+
+	painter->drawText(textRect, opt.text, textOption);
+
+	painter->restore();
+}
+
+QSize ValidatorItemDelegate::sizeHint(const QStyleOptionViewItem & option, const QModelIndex & index) const
+{
+	QFontMetrics metrics(option.fontMetrics);
+	QString text = index.data(Qt::DisplayRole).toString();
+	QStringList lines = text.split('\n');
+	int textWidth = minItemWidth;
+	int requiredHeight = 0;
+	for(auto line : lines)
+		textWidth = std::max(metrics.horizontalAdvance(line), textWidth);
+
+	requiredHeight = qMax(requiredHeight, lines.size() * metrics.height());
+
+	int finalWidth = qMax(textWidth + offsetForIcon, minItemWidth);
+	finalWidth = qMin(finalWidth, screenGeometry.width() - screenMargin - offsetForIcon);
+
+	QRect textBoundingRect = metrics.boundingRect(QRect(0, 0, finalWidth, 0),
+		Qt::TextWordWrap, text);
+
+	int finalHeight = qMax(textBoundingRect.height() + itemPaddingBottom, requiredHeight);
+
+	return QSize(finalWidth, finalHeight);
+}

+ 29 - 0
mapeditor/validator.h

@@ -46,4 +46,33 @@ public:
 
 private:
 	Ui::Validator *ui;
+
+	QRect screenGeometry;
+
+	void showValidationResults(const CMap * map);
+	void adjustWindowSize();
+};
+
+class ValidatorItemDelegate : public QStyledItemDelegate
+{
+public:
+	explicit ValidatorItemDelegate(QObject * parent = nullptr) : QStyledItemDelegate(parent) 
+	{
+		screenGeometry = QApplication::primaryScreen()->availableGeometry();
+	}
+
+	int itemPaddingBottom = 14;
+	int iconPadding = 4;
+	int textOffsetForIcon = 30;
+	int textPaddingRight = 10;
+	int minItemWidth = 350;
+	int screenMargin = 350;     // some reserved space from screenWidth; used if text.width > screenWidth - screenMargin 
+	
+	const int offsetForIcon = iconPadding + textOffsetForIcon;
+
+	void paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const override;
+	QSize sizeHint(const QStyleOptionViewItem & option, const QModelIndex & index) const override;
+
+private:
+	QRect screenGeometry;
 };

+ 1 - 1
mapeditor/windownewmap.cpp

@@ -311,7 +311,7 @@ void WindowNewMap::on_okButton_clicked()
 		nmap = f.get();
 	}
 	
-	nmap->mods = MapController::modAssessmentAll();
+	nmap->mods = MapController::modAssessmentMap(*nmap);
 	static_cast<MainWindow*>(parent())->controller.setMap(std::move(nmap));
 	static_cast<MainWindow*>(parent())->initializeMap(true);
 	close();

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 146 - 143
server/CGameHandler.cpp


+ 18 - 9
server/CGameHandler.h

@@ -11,7 +11,6 @@
 
 #include <vcmi/Environment.h>
 
-#include "../lib/callback/CGameInfoCallback.h"
 #include "../lib/callback/IGameEventCallback.h"
 #include "../lib/LoadProgress.h"
 #include "../lib/ScriptHandler.h"
@@ -28,7 +27,10 @@ class CCommanderInstance;
 class EVictoryLossCheckResult;
 class CRandomGenerator;
 class GameRandomizer;
+class StatisticDataSet;
 
+struct StartInfo;
+struct TerrainTile;
 struct CPackForServer;
 struct NewTurn;
 struct CGarrisonOperationPack;
@@ -55,7 +57,7 @@ class QueriesProcessor;
 class CObjectVisitQuery;
 class NewTurnProcessor;
 
-class CGameHandler : public CGameInfoCallback, public Environment, public IGameEventCallback
+class CGameHandler : public Environment, public IGameEventCallback
 {
 	CVCMIServer * lobby;
 
@@ -67,6 +69,7 @@ public:
 	std::unique_ptr<TurnTimerHandler> turnTimerHandler;
 	std::unique_ptr<NewTurnProcessor> newTurnProcessor;
 	std::unique_ptr<GameRandomizer> randomizer;
+	std::unique_ptr<StatisticDataSet> statistics;
 	std::shared_ptr<CGameState> gs;
 
 	//use enums as parameters, because doMove(sth, true, false, true) is not readable
@@ -94,8 +97,8 @@ public:
 	bool isAllowedExchange(ObjectInstanceID id1, ObjectInstanceID id2);
 	void giveSpells(const CGTownInstance *t, const CGHeroInstance *h);
 
-	CGameState & gameState() final { return *gs; }
-	const CGameState & gameState() const final { return *gs; }
+	IGameInfoCallback & gameInfo();
+	const CGameState & gameState() const { return *gs; }
 
 	// Helpers to create new object of specified type
 
@@ -124,7 +127,7 @@ public:
 	void showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID hid, bool removableUnits) override;
 	void showObjectWindow(const CGObjectInstance * object, EOpenWindowMode window, const CGHeroInstance * visitor, bool addQuery) override;
 	void giveResource(PlayerColor player, GameResID which, int val) override;
-	void giveResources(PlayerColor player, TResources resources) override;
+	void giveResources(PlayerColor player, const ResourceSet & resources) override;
 
 	void giveCreatures(const CGHeroInstance * h, const CCreatureSet &creatures) override;
 	void giveCreatures(const CArmedInstance *objid, const CGHeroInstance * h, const CCreatureSet &creatures, bool remove) override;
@@ -220,7 +223,7 @@ public:
 	bool garrisonSwap(ObjectInstanceID tid);
 	bool swapGarrisonOnSiege(ObjectInstanceID tid) override;
 	bool upgradeCreature( ObjectInstanceID objid, SlotID pos, CreatureID upgID );
-	bool recruitCreatures(ObjectInstanceID objid, ObjectInstanceID dst, CreatureID crid, ui32 cram, si32 level, PlayerColor player);
+	bool recruitCreatures(ObjectInstanceID objid, ObjectInstanceID dst, CreatureID crid, int32_t cram, int32_t level, PlayerColor player);
 	bool buildStructure(ObjectInstanceID tid, BuildingID bid, bool force=false);//force - for events: no cost, no checkings
 	bool visitTownBuilding(ObjectInstanceID tid, BuildingID bid);
 	bool razeStructure(ObjectInstanceID tid, BuildingID bid);
@@ -255,6 +258,12 @@ public:
 		h & *turnOrder;
 		h & *turnTimerHandler;
 
+		if (h.hasFeature(Handler::Version::SERVER_STATISTICS))
+		{
+			h & *statistics;
+		}
+
+
 #if SCRIPTING_ENABLED
 		JsonNode scriptsState;
 		if(h.saving)
@@ -297,10 +306,10 @@ public:
 
 	vstd::RNG & getRandomGenerator() override;
 
-#if SCRIPTING_ENABLED
-	scripting::Pool * getGlobalContextPool() const override;
+//#if SCRIPTING_ENABLED
+//	scripting::Pool * getGlobalContextPool() const override;
 //	scripting::Pool * getContextPool() const override;
-#endif
+//#endif
 
 	friend class CVCMIServer;
 private:

+ 1 - 1
server/CVCMIServer.cpp

@@ -294,7 +294,7 @@ bool CVCMIServer::prepareToStartGame()
 void CVCMIServer::startGameImmediately()
 {
 	for(auto activeConnection : activeConnections)
-		activeConnection->enterGameplayConnectionMode(*gh->gs);
+		activeConnection->setCallback(gh->gameInfo());
 
 	gh->start(si->mode == EStartMode::LOAD_GAME);
 	setState(EServerState::GAMEPLAY);

+ 3 - 2
server/NetPacksLobbyServer.cpp

@@ -19,6 +19,7 @@
 #include "../lib/campaign/CampaignState.h"
 #include "../lib/entities/faction/CTownHandler.h"
 #include "../lib/entities/faction/CFaction.h"
+#include "../lib/gameState/CGameState.h"
 #include "../lib/serializer/Connection.h"
 #include "../lib/mapping/CMapInfo.h"
 #include "../lib/mapping/CMapHeader.h"
@@ -225,7 +226,7 @@ void ApplyOnServerNetPackVisitor::visitLobbyStartGame(LobbyStartGame & pack)
 		return;
 	}
 	
-	pack.initializedStartInfo = std::make_shared<StartInfo>(*srv.gh->getInitialStartInfo());
+	pack.initializedStartInfo = std::make_shared<StartInfo>(*srv.gh->gameState().getInitialStartInfo());
 	pack.initializedGameState = srv.gh->gs;
 	result = true;
 }
@@ -240,7 +241,7 @@ void ApplyOnServerAfterAnnounceNetPackVisitor::visitLobbyStartGame(LobbyStartGam
 		{
 			if(connection->connectionID == pack.clientId)
 			{
-				connection->enterGameplayConnectionMode(srv.gh->gameState());
+				connection->setCallback(srv.gh->gameInfo());
 				srv.reconnectPlayer(pack.clientId);
 			}
 		}

+ 14 - 14
server/NetPacksServer.cpp

@@ -54,7 +54,7 @@ void ApplyGhNetPackVisitor::visitDismissHero(DismissHero & pack)
 {
 	gh.throwIfWrongOwner(connection, &pack, pack.hid);
 	gh.throwIfPlayerNotActive(connection, &pack);
-	result = gh.removeObject(gh.getObj(pack.hid), pack.player);
+	result = gh.removeObject(gh.gameInfo().getObj(pack.hid), pack.player);
 }
 
 void ApplyGhNetPackVisitor::visitMoveHero(MoveHero & pack)
@@ -181,7 +181,7 @@ void ApplyGhNetPackVisitor::visitUpgradeCreature(UpgradeCreature & pack)
 
 void ApplyGhNetPackVisitor::visitGarrisonHeroSwap(GarrisonHeroSwap & pack)
 {
-	const CGTownInstance * town = gh.getTown(pack.tid);
+	const CGTownInstance * town = gh.gameInfo().getTown(pack.tid);
 	if(!gh.isPlayerOwns(connection, &pack, pack.tid) && !(town->getGarrisonHero() && gh.isPlayerOwns(connection, &pack, town->getGarrisonHero()->id)))
 		gh.throwNotAllowedAction(connection); //neither town nor garrisoned hero (if present) is ours
 	gh.throwIfPlayerNotActive(connection, &pack);
@@ -191,8 +191,8 @@ void ApplyGhNetPackVisitor::visitGarrisonHeroSwap(GarrisonHeroSwap & pack)
 
 void ApplyGhNetPackVisitor::visitExchangeArtifacts(ExchangeArtifacts & pack)
 {
-	if(gh.getHero(pack.src.artHolder))
-		gh.throwIfWrongPlayer(connection, &pack, gh.getOwner(pack.src.artHolder)); //second hero can be ally
+	if(gh.gameInfo().getHero(pack.src.artHolder))
+		gh.throwIfWrongPlayer(connection, &pack, gh.gameState().getOwner(pack.src.artHolder)); //second hero can be ally
 	gh.throwIfPlayerNotActive(connection, &pack);
 
 	result = gh.moveArtifact(pack.player, pack.src, pack.dst);
@@ -200,7 +200,7 @@ void ApplyGhNetPackVisitor::visitExchangeArtifacts(ExchangeArtifacts & pack)
 
 void ApplyGhNetPackVisitor::visitBulkExchangeArtifacts(BulkExchangeArtifacts & pack)
 {
-	if(gh.getMarket(pack.srcHero) == nullptr)
+	if(gh.gameState().getMarket(pack.srcHero) == nullptr)
 		gh.throwIfWrongOwner(connection, &pack, pack.srcHero);
 	if(pack.swap)
 		gh.throwIfWrongOwner(connection, &pack, pack.dstHero);
@@ -213,7 +213,7 @@ void ApplyGhNetPackVisitor::visitManageBackpackArtifacts(ManageBackpackArtifacts
 {
 	gh.throwIfPlayerNotActive(connection, &pack);
 
-	if(gh.getPlayerRelations(pack.player, gh.getOwner(pack.artHolder)) != PlayerRelations::ENEMIES)
+	if(gh.gameInfo().getPlayerRelations(pack.player, gh.gameState().getOwner(pack.artHolder)) != PlayerRelations::ENEMIES)
 		result = gh.manageBackpackArtifacts(pack.player, pack.artHolder, pack.cmd);
 }
 
@@ -235,7 +235,7 @@ void ApplyGhNetPackVisitor::visitAssembleArtifacts(AssembleArtifacts & pack)
 
 void ApplyGhNetPackVisitor::visitEraseArtifactByClient(EraseArtifactByClient & pack)
 {
-	gh.throwIfWrongPlayer(connection, &pack, gh.getOwner(pack.al.artHolder));
+	gh.throwIfWrongPlayer(connection, &pack, gh.gameState().getOwner(pack.al.artHolder));
 	gh.throwIfPlayerNotActive(connection, &pack);
 	result = gh.eraseArtifactByClient(pack.al);
 }
@@ -249,9 +249,9 @@ void ApplyGhNetPackVisitor::visitBuyArtifact(BuyArtifact & pack)
 
 void ApplyGhNetPackVisitor::visitTradeOnMarketplace(TradeOnMarketplace & pack)
 {
-	const CGObjectInstance * object = gh.getObj(pack.marketId);
-	const CGHeroInstance * hero = gh.getHero(pack.heroId);
-	const auto * market = gh.getMarket(pack.marketId);
+	const CGObjectInstance * object = gh.gameInfo().getObj(pack.marketId);
+	const CGHeroInstance * hero = gh.gameInfo().getHero(pack.heroId);
+	const auto * market = gh.gameState().getMarket(pack.marketId);
 
 	gh.throwIfWrongPlayer(connection, &pack);
 	gh.throwIfPlayerNotActive(connection, &pack);
@@ -295,7 +295,7 @@ void ApplyGhNetPackVisitor::visitTradeOnMarketplace(TradeOnMarketplace & pack)
 		if (!object->visitableAt(hero->visitablePos()))
 			gh.throwAndComplain(connection, "Can not trade - object not visited!");
 
-		if (object->getOwner().isValidPlayer() && gh.getPlayerRelations(object->getOwner(), hero->getOwner()) == PlayerRelations::ENEMIES)
+		if (object->getOwner().isValidPlayer() && gh.gameInfo().getPlayerRelations(object->getOwner(), hero->getOwner()) == PlayerRelations::ENEMIES)
 			gh.throwAndComplain(connection, "Can not trade - market not owned!");
 	}
 
@@ -377,7 +377,7 @@ void ApplyGhNetPackVisitor::visitBuildBoat(BuildBoat & pack)
 	gh.throwIfWrongPlayer(connection, &pack);
 	gh.throwIfPlayerNotActive(connection, &pack);
 
-	if(gh.getPlayerRelations(gh.getOwner(pack.objid), pack.player) == PlayerRelations::ENEMIES)
+	if(gh.gameInfo().getPlayerRelations(gh.gameState().getOwner(pack.objid), pack.player) == PlayerRelations::ENEMIES)
 		gh.throwAndComplain(connection, "Can't build boat at enemy shipyard");
 
 	result = gh.buildBoat(pack.objid, pack.player);
@@ -413,7 +413,7 @@ void ApplyGhNetPackVisitor::visitDigWithHero(DigWithHero & pack)
 	gh.throwIfWrongOwner(connection, &pack, pack.id);
 	gh.throwIfPlayerNotActive(connection, &pack);
 
-	result = gh.dig(gh.getHero(pack.id));
+	result = gh.dig(gh.gameInfo().getHero(pack.id));
 }
 
 void ApplyGhNetPackVisitor::visitCastAdvSpell(CastAdvSpell & pack)
@@ -424,7 +424,7 @@ void ApplyGhNetPackVisitor::visitCastAdvSpell(CastAdvSpell & pack)
 	if (!pack.sid.hasValue())
 		gh.throwNotAllowedAction(connection);
 
-	const CGHeroInstance * h = gh.getHero(pack.hid);
+	const CGHeroInstance * h = gh.gameInfo().getHero(pack.hid);
 	if(!h)
 		gh.throwNotAllowedAction(connection);
 

+ 1 - 1
server/ServerSpellCastEnvironment.cpp

@@ -81,7 +81,7 @@ void ServerSpellCastEnvironment::apply(CatapultAttack & pack)
 
 const IGameInfoCallback * ServerSpellCastEnvironment::getCb() const
 {
-	return gh;
+	return &gh->gameInfo();
 }
 
 const CMap * ServerSpellCastEnvironment::getMap() const

+ 12 - 12
server/TurnTimerHandler.cpp

@@ -29,7 +29,7 @@ TurnTimerHandler::TurnTimerHandler(CGameHandler & gh):
 
 void TurnTimerHandler::onGameplayStart(PlayerColor player)
 {
-	if(const auto * si = gameHandler.getStartInfo())
+	if(const auto * si = gameHandler.gameInfo().getStartInfo())
 	{
 		timers[player] = si->turnTimerInfo;
 		timers[player].turnTimer = 0;
@@ -66,7 +66,7 @@ void TurnTimerHandler::sendTimerUpdate(PlayerColor player)
 
 void TurnTimerHandler::onPlayerGetTurn(PlayerColor player)
 {
-	if(const auto * si = gameHandler.getStartInfo())
+	if(const auto * si = gameHandler.gameInfo().getStartInfo())
 	{
 		if(si->turnTimerInfo.isEnabled())
 		{
@@ -89,7 +89,7 @@ void TurnTimerHandler::prolongTimers(int durationMs)
 
 void TurnTimerHandler::update(int waitTimeMs)
 {
-	if(!gameHandler.getStartInfo()->turnTimerInfo.isEnabled())
+	if(!gameHandler.gameInfo().getStartInfo()->turnTimerInfo.isEnabled())
 		return;
 
 	for(PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player)
@@ -123,12 +123,12 @@ bool TurnTimerHandler::timerCountDown(int & timer, int initialTimer, PlayerColor
 
 void TurnTimerHandler::onPlayerMakingTurn(PlayerColor player, int waitTime)
 {
-	const auto * si = gameHandler.getStartInfo();
+	const auto * si = gameHandler.gameInfo().getStartInfo();
 	if(!si || !si->turnTimerInfo.isEnabled())
 		return;
 	
 	auto & timer = timers[player];
-	const auto * state = gameHandler.getPlayerState(player);
+	const auto * state = gameHandler.gameInfo().getPlayerState(player);
 	if(state && state->human && timer.isActive && !timer.isBattle && state->status == EPlayerStatus::INGAME)
 	{
 		// turn timers are only used if turn timer is non-zero
@@ -153,8 +153,8 @@ bool TurnTimerHandler::isPvpBattle(const BattleID & battleID) const
 	auto defender = gs.getBattle(battleID)->getSidePlayer(BattleSide::DEFENDER);
 	if(attacker.isValidPlayer() && defender.isValidPlayer())
 	{
-		const auto * attackerState = gameHandler.getPlayerState(attacker);
-		const auto * defenderState = gameHandler.getPlayerState(defender);
+		const auto * attackerState = gameHandler.gameInfo().getPlayerState(attacker);
+		const auto * defenderState = gameHandler.gameInfo().getPlayerState(defender);
 		if(attackerState && defenderState && attackerState->human && defenderState->human)
 			return true;
 	}
@@ -164,7 +164,7 @@ bool TurnTimerHandler::isPvpBattle(const BattleID & battleID) const
 void TurnTimerHandler::onBattleStart(const BattleID & battleID)
 {
 	const auto & gs = gameHandler.gameState();
-	const auto * si = gameHandler.getStartInfo();
+	const auto * si = gameHandler.gameInfo().getStartInfo();
 	if(!si)
 		return;
 
@@ -191,7 +191,7 @@ void TurnTimerHandler::onBattleStart(const BattleID & battleID)
 void TurnTimerHandler::onBattleEnd(const BattleID & battleID)
 {
 	const auto & gs = gameHandler.gameState();
-	const auto * si = gameHandler.getStartInfo();
+	const auto * si = gameHandler.gameInfo().getStartInfo();
 	if(!si)
 	{
 		assert(0);
@@ -219,7 +219,7 @@ void TurnTimerHandler::onBattleEnd(const BattleID & battleID)
 void TurnTimerHandler::onBattleNextStack(const BattleID & battleID, const CStack & stack)
 {
 	const auto & gs = gameHandler.gameState();
-	const auto * si = gameHandler.getStartInfo();
+	const auto * si = gameHandler.gameInfo().getStartInfo();
 	if(!si || !gs.getBattle(battleID))
 	{
 		assert(0);
@@ -245,7 +245,7 @@ void TurnTimerHandler::onBattleNextStack(const BattleID & battleID, const CStack
 void TurnTimerHandler::onBattleLoop(const BattleID & battleID, int waitTime)
 {
 	const auto & gs = gameHandler.gameState();
-	const auto * si = gameHandler.getStartInfo();
+	const auto * si = gameHandler.gameInfo().getStartInfo();
 	if(!si)
 	{
 		assert(0);
@@ -273,7 +273,7 @@ void TurnTimerHandler::onBattleLoop(const BattleID & battleID, int waitTime)
 	if(!player.isValidPlayer())
 		return;
 	
-	const auto * state = gameHandler.getPlayerState(player);
+	const auto * state = gameHandler.gameInfo().getPlayerState(player);
 	assert(state && state->status == EPlayerStatus::INGAME);
 	if(!state || state->status != EPlayerStatus::INGAME || !state->human)
 		return;

+ 2 - 2
server/battles/BattleActionProcessor.cpp

@@ -81,7 +81,7 @@ bool BattleActionProcessor::doSurrenderAction(const CBattleInfoCallback & battle
 		return false;
 	}
 
-	if (gameHandler->getResource(player, EGameResID::GOLD) < cost)
+	if (gameHandler->gameInfo().getResource(player, EGameResID::GOLD) < cost)
 	{
 		gameHandler->complain("Not enough gold to surrender!");
 		return false;
@@ -899,7 +899,7 @@ int BattleActionProcessor::moveStack(const CBattleInfoCallback & battle, int sta
 		}
 	}
 	//handle last hex separately for deviation
-	if (gameHandler->getSettings().getBoolean(EGameSettings::COMBAT_ONE_HEX_TRIGGERS_OBSTACLES))
+	if (gameHandler->gameInfo().getSettings().getBoolean(EGameSettings::COMBAT_ONE_HEX_TRIGGERS_OBSTACLES))
 	{
 		if (dest == battle::Unit::occupiedHex(start, curStack->doubleWide(), curStack->unitSide())
 			|| start == battle::Unit::occupiedHex(dest, curStack->doubleWide(), curStack->unitSide()))

+ 6 - 6
server/battles/BattleProcessor.cpp

@@ -110,7 +110,7 @@ void BattleProcessor::startBattle(const CArmedInstance *army1, const CArmedInsta
 	assert(battle);
 	
 	//add battle bonuses based from player state only when attacks neutral creatures
-	const auto * attackerInfo = gameHandler->getPlayerState(army1->getOwner(), false);
+	const auto * attackerInfo = gameHandler->gameInfo().getPlayerState(army1->getOwner(), false);
 	if(attackerInfo && !army2->getOwner().isValidPlayer())
 	{
 		for(auto bonus : attackerInfo->battleBonuses)
@@ -150,13 +150,13 @@ void BattleProcessor::startBattle(const CArmedInstance *army1, const CArmedInsta
 	startBattle(army1, army2, army2->visitablePos(),
 		army1->ID == Obj::HERO ? dynamic_cast<const CGHeroInstance*>(army1) : nullptr,
 		army2->ID == Obj::HERO ? dynamic_cast<const CGHeroInstance*>(army2) : nullptr,
-		BattleLayout::createDefaultLayout(gameHandler, army1, army2),
+		BattleLayout::createDefaultLayout(gameHandler->gameInfo(), army1, army2),
 		nullptr);
 }
 
 BattleID BattleProcessor::setupBattle(int3 tile, BattleSideArray<const CArmedInstance *> armies, BattleSideArray<const CGHeroInstance *> heroes, const BattleLayout & layout, const CGTownInstance *town)
 {
-	const auto & t = *gameHandler->getTile(tile);
+	const auto & t = *gameHandler->gameInfo().getTile(tile);
 	TerrainId terrain = t.getTerrainID();
 	if (town)
 		terrain = town->getNativeTerrain();
@@ -175,7 +175,7 @@ BattleID BattleProcessor::setupBattle(int3 tile, BattleSideArray<const CArmedIns
 
 	//send info about battles
 	BattleStart bs;
-	bs.info = BattleInfo::setupBattle(gameHandler->gameState().cb, tile, terrain, battlefieldType, armies, heroes, layout, town);
+	bs.info = BattleInfo::setupBattle(&gameHandler->gameInfo(), tile, terrain, battlefieldType, armies, heroes, layout, town);
 	bs.battleID = gameHandler->gameState().nextBattleID;
 
 	engageIntoBattle(bs.info->getSide(BattleSide::ATTACKER).color);
@@ -184,8 +184,8 @@ BattleID BattleProcessor::setupBattle(int3 tile, BattleSideArray<const CArmedIns
 	auto lastBattleQuery = std::dynamic_pointer_cast<CBattleQuery>(gameHandler->queries->topQuery(bs.info->getSide(BattleSide::ATTACKER).color));
 	if(!lastBattleQuery)
 		lastBattleQuery = std::dynamic_pointer_cast<CBattleQuery>(gameHandler->queries->topQuery(bs.info->getSide(BattleSide::DEFENDER).color));
-	bool isDefenderHuman = bs.info->getSide(BattleSide::DEFENDER).color.isValidPlayer() && gameHandler->getPlayerState(bs.info->getSide(BattleSide::DEFENDER).color)->isHuman();
-	bool isAttackerHuman = gameHandler->getPlayerState(bs.info->getSide(BattleSide::ATTACKER).color)->isHuman();
+	bool isDefenderHuman = bs.info->getSide(BattleSide::DEFENDER).color.isValidPlayer() && gameHandler->gameInfo().getPlayerState(bs.info->getSide(BattleSide::DEFENDER).color)->isHuman();
+	bool isAttackerHuman = gameHandler->gameInfo().getPlayerState(bs.info->getSide(BattleSide::ATTACKER).color)->isHuman();
 
 	bool onlyOnePlayerHuman = isDefenderHuman != isAttackerHuman;
 	bs.info->replayAllowed = lastBattleQuery == nullptr && onlyOnePlayerHuman;

+ 14 - 14
server/battles/BattleResultProcessor.cpp

@@ -143,7 +143,7 @@ CasualtiesAfterBattle::CasualtiesAfterBattle(const CBattleInfoCallback & battle,
 
 void CasualtiesAfterBattle::updateArmy(CGameHandler *gh)
 {
-	if (gh->getObjInstance(army->id) == nullptr)
+	if (gh->gameInfo().getObjInstance(army->id) == nullptr)
 		throw std::runtime_error("Object " + army->getObjectName() + " is not on the map!");
 
 	for (TStackAndItsNewCount &ncount : newStackCounts)
@@ -269,8 +269,8 @@ void BattleResultProcessor::endBattle(const CBattleInfoCallback & battle)
 	finishingBattles[battle.getBattle()->getBattleID()] = std::make_unique<FinishingBattleHelper>(battle, *battleResult, queriedPlayers);
 
 	// in battles against neutrals, 1st player can ask to replay battle manually
-	const auto * attackerPlayer = gameHandler->getPlayerState(battle.getBattle()->getSidePlayer(BattleSide::ATTACKER));
-	const auto * defenderPlayer = gameHandler->getPlayerState(battle.getBattle()->getSidePlayer(BattleSide::DEFENDER));
+	const auto * attackerPlayer = gameHandler->gameInfo().getPlayerState(battle.getBattle()->getSidePlayer(BattleSide::ATTACKER));
+	const auto * defenderPlayer = gameHandler->gameInfo().getPlayerState(battle.getBattle()->getSidePlayer(BattleSide::DEFENDER));
 	bool isAttackerHuman = attackerPlayer && attackerPlayer->isHuman();
 	bool isDefenderHuman = defenderPlayer && defenderPlayer->isHuman();
 	bool onlyOnePlayerHuman = isAttackerHuman != isDefenderHuman;
@@ -343,21 +343,21 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle)
 			if(!strongestHero || hero->exp > strongestHero->exp)
 				strongestHero = hero;
 		if(strongestHero->id == finishingBattle->loserId && strongestHero->level > 5)
-			gameHandler->gameState().statistic.accumulatedValues[finishingBattle->victor].lastDefeatedStrongestHeroDay = gameHandler->gameState().getDate(Date::DAY);
+			gameHandler->statistics->accumulatedValues[finishingBattle->victor].lastDefeatedStrongestHeroDay = gameHandler->gameState().getDate(Date::DAY);
 	}
 	if(battle.sideToPlayer(BattleSide::ATTACKER) == PlayerColor::NEUTRAL || battle.sideToPlayer(BattleSide::DEFENDER) == PlayerColor::NEUTRAL)
 	{
-		gameHandler->gameState().statistic.accumulatedValues[battle.sideToPlayer(BattleSide::ATTACKER)].numBattlesNeutral++;
-		gameHandler->gameState().statistic.accumulatedValues[battle.sideToPlayer(BattleSide::DEFENDER)].numBattlesNeutral++;
+		gameHandler->statistics->accumulatedValues[battle.sideToPlayer(BattleSide::ATTACKER)].numBattlesNeutral++;
+		gameHandler->statistics->accumulatedValues[battle.sideToPlayer(BattleSide::DEFENDER)].numBattlesNeutral++;
 		if(!finishingBattle->isDraw())
-			gameHandler->gameState().statistic.accumulatedValues[battle.sideToPlayer(finishingBattle->winnerSide)].numWinBattlesNeutral++;
+			gameHandler->statistics->accumulatedValues[battle.sideToPlayer(finishingBattle->winnerSide)].numWinBattlesNeutral++;
 	}
 	else
 	{
-		gameHandler->gameState().statistic.accumulatedValues[battle.sideToPlayer(BattleSide::ATTACKER)].numBattlesPlayer++;
-		gameHandler->gameState().statistic.accumulatedValues[battle.sideToPlayer(BattleSide::DEFENDER)].numBattlesPlayer++;
+		gameHandler->statistics->accumulatedValues[battle.sideToPlayer(BattleSide::ATTACKER)].numBattlesPlayer++;
+		gameHandler->statistics->accumulatedValues[battle.sideToPlayer(BattleSide::DEFENDER)].numBattlesPlayer++;
 		if(!finishingBattle->isDraw())
-			gameHandler->gameState().statistic.accumulatedValues[battle.sideToPlayer(finishingBattle->winnerSide)].numWinBattlesPlayer++;
+			gameHandler->statistics->accumulatedValues[battle.sideToPlayer(finishingBattle->winnerSide)].numWinBattlesPlayer++;
 	}
 
 	BattleResultAccepted raccepted;
@@ -469,7 +469,7 @@ void BattleResultProcessor::battleFinalize(const BattleID & battleID, const Batt
 				for(const auto & artSlot : loserHero->getCommander()->artifactsWorn)
 					addArtifactToTransfer(packCommander, artSlot.first, artSlot.second.getArt());
 			}
-			auto armyObj = dynamic_cast<const CArmedInstance*>(gameHandler->getObj(finishingBattle->loserId));
+			auto armyObj = dynamic_cast<const CArmedInstance*>(gameHandler->gameInfo().getObj(finishingBattle->loserId));
 			for(const auto & armySlot : armyObj->stacks)
 			{
 				auto & packsArmy = resultsApplied.movingArtifacts.emplace_back(finishingBattle->victor, finishingBattle->loserId, finishingBattle->winnerId, false);
@@ -557,19 +557,19 @@ void BattleResultProcessor::battleFinalize(const BattleID & battleID, const Batt
 	{
 		RemoveObject ro(winnerHero->id, finishingBattle->loser);
 		gameHandler->sendAndApply(ro);
-		if(gameHandler->getSettings().getBoolean(EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS))
+		if(gameHandler->gameInfo().getSettings().getBoolean(EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS))
 			gameHandler->heroPool->onHeroEscaped(finishingBattle->victor, winnerHero);
 	}
 
 	if (result.result == EBattleResult::SURRENDER)
 	{
-		gameHandler->gameState().statistic.accumulatedValues[finishingBattle->loser].numHeroSurrendered++;
+		gameHandler->statistics->accumulatedValues[finishingBattle->loser].numHeroSurrendered++;
 		gameHandler->heroPool->onHeroSurrendered(finishingBattle->loser, loserHero);
 	}
 
 	if (result.result == EBattleResult::ESCAPE)
 	{
-		gameHandler->gameState().statistic.accumulatedValues[finishingBattle->loser].numHeroEscaped++;
+		gameHandler->statistics->accumulatedValues[finishingBattle->loser].numHeroEscaped++;
 		gameHandler->heroPool->onHeroEscaped(finishingBattle->loser, loserHero);
 	}
 

+ 12 - 12
server/processors/HeroPoolProcessor.cpp

@@ -147,9 +147,9 @@ void HeroPoolProcessor::onNewWeek(const PlayerColor & color)
 
 bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTypeID & heroToRecruit, const PlayerColor & player, const HeroTypeID & nextHero)
 {
-	const PlayerState * playerState = gameHandler->getPlayerState(player);
-	const CGObjectInstance * mapObject = gameHandler->getObj(objectID);
-	const CGTownInstance * town = gameHandler->getTown(objectID);
+	const PlayerState * playerState = gameHandler->gameInfo().getPlayerState(player);
+	const CGObjectInstance * mapObject = gameHandler->gameInfo().getObj(objectID);
+	const CGTownInstance * town = gameHandler->gameInfo().getTown(objectID);
 	const auto & heroesPool = gameHandler->gameState().heroesPool;
 
 	if (!mapObject && gameHandler->complain("Invalid map object!"))
@@ -161,15 +161,15 @@ bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTy
 	if (playerState->resources[EGameResID::GOLD] < GameConstants::HERO_GOLD_COST && gameHandler->complain("Not enough gold for buying hero!"))
 		return false;
 
-	if (gameHandler->getHeroCount(player, false) >= gameHandler->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP) && gameHandler->complain("Cannot hire hero, too many wandering heroes already!"))
+	if (gameHandler->gameInfo().getHeroCount(player, false) >= gameHandler->gameInfo().getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP) && gameHandler->complain("Cannot hire hero, too many wandering heroes already!"))
 		return false;
 
-	if (gameHandler->getHeroCount(player, true) >= gameHandler->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP) && gameHandler->complain("Cannot hire hero, too many heroes garrisoned and wandering heroes present!"))
+	if (gameHandler->gameInfo().getHeroCount(player, true) >= gameHandler->gameInfo().getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP) && gameHandler->complain("Cannot hire hero, too many heroes garrisoned and wandering heroes present!"))
 		return false;
 
 	if (nextHero != HeroTypeID::NONE) // player attempts to invite next hero
 	{
-		if(!gameHandler->getSettings().getBoolean(EGameSettings::HEROES_TAVERN_INVITE) && gameHandler->complain("Inviting heroes not allowed!"))
+		if(!gameHandler->gameInfo().getSettings().getBoolean(EGameSettings::HEROES_TAVERN_INVITE) && gameHandler->complain("Inviting heroes not allowed!"))
 			return false;
 
 		if(!heroesPool->unusedHeroesFromPool().count(nextHero) && gameHandler->complain("Cannot invite specified hero!"))
@@ -181,7 +181,7 @@ bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTy
 
 	if(town) //tavern in town
 	{
-		if(gameHandler->getPlayerRelations(mapObject->tempOwner, player) == PlayerRelations::ENEMIES && gameHandler->complain("Can't buy hero in enemy town!"))
+		if(gameHandler->gameInfo().getPlayerRelations(mapObject->tempOwner, player) == PlayerRelations::ENEMIES && gameHandler->complain("Can't buy hero in enemy town!"))
 			return false;
 
 		if(!town->hasBuilt(BuildingID::TAVERN) && gameHandler->complain("No tavern!"))
@@ -201,7 +201,7 @@ bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTy
 			return false;
 		}
 
-		if(gameHandler->getTile(mapObject->visitablePos())->visitableObjects.back() != mapObject->id && gameHandler->complain("Tavern entry must be unoccupied!"))
+		if(gameHandler->gameInfo().getTile(mapObject->visitablePos())->visitableObjects.back() != mapObject->id && gameHandler->complain("Tavern entry must be unoccupied!"))
 			return false;
 	}
 
@@ -227,11 +227,11 @@ bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTy
 	hr.hid = recruitedHero->getHeroTypeID();
 	hr.player = player;
 	hr.tile = recruitedHero->convertFromVisitablePos(targetPos );
-	if(gameHandler->getTile(targetPos)->isWater() && !recruitedHero->inBoat())
+	if(gameHandler->gameInfo().getTile(targetPos)->isWater() && !recruitedHero->inBoat())
 	{
 		//Create a new boat for hero
 		gameHandler->createBoat(targetPos, recruitedHero->getBoatType(), player);
-		hr.boatId = gameHandler->getTopObj(targetPos)->id;
+		hr.boatId = gameHandler->gameInfo().getTopObj(targetPos)->id;
 	}
 
 	// apply netpack -> this will remove hired hero from pool
@@ -257,7 +257,7 @@ std::vector<const CHeroClass *> HeroPoolProcessor::findAvailableClassesFor(const
 	std::vector<const CHeroClass *> result;
 
 	const auto & heroesPool = gameHandler->gameState().heroesPool;
-	FactionID factionID = gameHandler->getPlayerSettings(player)->castle;
+	FactionID factionID = gameHandler->gameInfo().getPlayerSettings(player)->castle;
 
 	for(const auto & elem : heroesPool->unusedHeroesFromPool())
 	{
@@ -302,7 +302,7 @@ const CHeroClass * HeroPoolProcessor::pickClassFor(bool isNative, const PlayerCo
 		return nullptr;
 	}
 
-	FactionID factionID = gameHandler->getPlayerSettings(player)->castle;
+	FactionID factionID = gameHandler->gameInfo().getPlayerSettings(player)->castle;
 	const auto & heroesPool = gameHandler->gameState().heroesPool;
 	const auto & currentTavern = heroesPool->getHeroesFor(player);
 

+ 16 - 16
server/processors/NewTurnProcessor.cpp

@@ -47,7 +47,7 @@ void NewTurnProcessor::handleTimeEvents(PlayerColor color)
 		if (!event.occursToday(gameHandler->gameState().day))
 			continue;
 
-		if (!event.affectsPlayer(color, gameHandler->getPlayerState(color)->isHuman()))
+		if (!event.affectsPlayer(color, gameHandler->gameInfo().getPlayerState(color)->isHuman()))
 			continue;
 
 		InfoWindow iw;
@@ -66,7 +66,7 @@ void NewTurnProcessor::handleTimeEvents(PlayerColor color)
 		//remove objects specified by event
 		for(const ObjectInstanceID objectIdToRemove : event.deletedObjectsInstances)
 		{
-			auto objectInstance = gameHandler->getObj(objectIdToRemove, false);
+			auto objectInstance = gameHandler->gameInfo().getObj(objectIdToRemove, false);
 			if(objectInstance != nullptr)
 				gameHandler->removeObject(objectInstance, PlayerColor::NEUTRAL);
 		}
@@ -82,7 +82,7 @@ void NewTurnProcessor::handleTownEvents(const CGTownInstance * town)
 			continue;
 
 		PlayerColor player = town->getOwner();
-		if (!event.affectsPlayer(player, gameHandler->getPlayerState(player)->isHuman()))
+		if (!event.affectsPlayer(player, gameHandler->gameInfo().getPlayerState(player)->isHuman()))
 			continue;
 
 		// dialog
@@ -178,7 +178,7 @@ void NewTurnProcessor::onPlayerTurnEnded(PlayerColor which)
 	// check for 7 days without castle
 	gameHandler->checkVictoryLossConditionsForPlayer(which);
 
-	bool newWeek = gameHandler->getDate(Date::DAY_OF_WEEK) == 7; // end of 7th day
+	bool newWeek = gameHandler->gameInfo().getDate(Date::DAY_OF_WEEK) == 7; // end of 7th day
 
 	if (newWeek) //new heroes in tavern
 		gameHandler->heroPool->onNewWeek(which);
@@ -186,7 +186,7 @@ void NewTurnProcessor::onPlayerTurnEnded(PlayerColor which)
 
 ResourceSet NewTurnProcessor::generatePlayerIncome(PlayerColor playerID, bool newWeek)
 {
-	const auto & playerSettings = gameHandler->getPlayerSettings(playerID);
+	const auto & playerSettings = gameHandler->gameInfo().getPlayerSettings(playerID);
 	const PlayerState & state = gameHandler->gameState().players.at(playerID);
 	ResourceSet income;
 
@@ -515,7 +515,7 @@ std::tuple<EWeekType, CreatureID> NewTurnProcessor::pickWeekType(bool newMonth)
 			return { EWeekType::DEITYOFFIRE, CreatureID::IMP };
 	}
 
-	if(!gameHandler->getSettings().getBoolean(EGameSettings::CREATURES_ALLOW_RANDOM_SPECIAL_WEEKS))
+	if(!gameHandler->gameInfo().getSettings().getBoolean(EGameSettings::CREATURES_ALLOW_RANDOM_SPECIAL_WEEKS))
 		return { EWeekType::NORMAL, CreatureID::NONE};
 
 	int monthType = gameHandler->getRandomGenerator().nextInt(99);
@@ -523,7 +523,7 @@ std::tuple<EWeekType, CreatureID> NewTurnProcessor::pickWeekType(bool newMonth)
 	{
 		if (monthType < 40) //double growth
 		{
-			if (gameHandler->getSettings().getBoolean(EGameSettings::CREATURES_ALLOW_ALL_FOR_DOUBLE_MONTH))
+			if (gameHandler->gameInfo().getSettings().getBoolean(EGameSettings::CREATURES_ALLOW_ALL_FOR_DOUBLE_MONTH))
 			{
 				CreatureID creatureID = gameHandler->randomizer->rollCreature();
 				return { EWeekType::DOUBLE_GROWTH, creatureID};
@@ -567,7 +567,7 @@ std::vector<SetMana> NewTurnProcessor::updateHeroesManaPoints()
 
 	for (auto & elem : gameHandler->gameState().players)
 	{
-		for (CGHeroInstance *h : elem.second.getHeroes())
+		for (const CGHeroInstance *h : elem.second.getHeroes())
 		{
 			int32_t newMana = h->getManaNewTurn();
 
@@ -584,7 +584,7 @@ std::vector<SetMovePoints> NewTurnProcessor::updateHeroesMovementPoints()
 
 	for (auto & elem : gameHandler->gameState().players)
 	{
-		for (CGHeroInstance *h : elem.second.getHeroes())
+		for (const CGHeroInstance *h : elem.second.getHeroes())
 		{
 			auto ti = h->getTurnInfo(1);
 			// NOTE: this code executed when bonuses of previous day not yet updated (this happen in NewTurn::applyGs). See issue 2356
@@ -645,9 +645,9 @@ NewTurn NewTurnProcessor::generateNewTurnPack()
 	n.creatureid = CreatureID::NONE;
 	n.day = gameHandler->gameState().day + 1;
 
-	bool firstTurn = !gameHandler->getDate(Date::DAY);
-	bool newWeek = gameHandler->getDate(Date::DAY_OF_WEEK) == 7; //day numbers are confusing, as day was not yet switched
-	bool newMonth = gameHandler->getDate(Date::DAY_OF_MONTH) == 28;
+	bool firstTurn = !gameHandler->gameInfo().getDate(Date::DAY);
+	bool newWeek = gameHandler->gameInfo().getDate(Date::DAY_OF_WEEK) == 7; //day numbers are confusing, as day was not yet switched
+	bool newMonth = gameHandler->gameInfo().getDate(Date::DAY_OF_MONTH) == 28;
 
 	if (!firstTurn)
 	{
@@ -691,9 +691,9 @@ void NewTurnProcessor::onNewTurn()
 {
 	NewTurn n = generateNewTurnPack();
 
-	bool firstTurn = !gameHandler->getDate(Date::DAY);
-	bool newWeek = gameHandler->getDate(Date::DAY_OF_WEEK) == 7; //day numbers are confusing, as day was not yet switched
-	bool newMonth = gameHandler->getDate(Date::DAY_OF_MONTH) == 28;
+	bool firstTurn = !gameHandler->gameInfo().getDate(Date::DAY);
+	bool newWeek = gameHandler->gameInfo().getDate(Date::DAY_OF_WEEK) == 7; //day numbers are confusing, as day was not yet switched
+	bool newMonth = gameHandler->gameInfo().getDate(Date::DAY_OF_MONTH) == 28;
 
 	gameHandler->sendAndApply(n);
 
@@ -713,7 +713,7 @@ void NewTurnProcessor::onNewTurn()
 		{
 			auto t = gameHandler->gameState().getTown(townID);
 			if (!t->getOwner().isValidPlayer())
-				updateNeutralTownGarrison(t, 1 + gameHandler->getDate(Date::DAY) / 7);
+				updateNeutralTownGarrison(t, 1 + gameHandler->gameInfo().getDate(Date::DAY) / 7);
 		}
 	}
 

+ 6 - 6
server/processors/PlayerMessageProcessor.cpp

@@ -50,7 +50,7 @@ void PlayerMessageProcessor::playerMessage(PlayerColor player, const std::string
 
 	if(handleCheatCode(message, player, currObj))
 	{
-		if(!gameHandler->getPlayerSettings(player)->isControlledByAI())
+		if(!gameHandler->gameInfo().getPlayerSettings(player)->isControlledByAI())
 		{
 			MetaString txt;
 			txt.appendLocalString(EMetaText::GENERAL_TXT, 260);
@@ -149,7 +149,7 @@ void PlayerMessageProcessor::commandStatistic(PlayerColor player, const std::vec
 	if(!isHost)
 		return;
 
-	std::string path = gameHandler->gameState().statistic.writeCsv();
+	std::string path = gameHandler->statistics->writeCsv();
 
 	auto str = MetaString::createFromTextID("vcmi.broadcast.statisticFile");
 	str.replaceRawString(path);
@@ -330,7 +330,7 @@ void PlayerMessageProcessor::startVoting(PlayerColor initiator, ECurrentChatVote
 
 	for(PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player)
 	{
-		auto state = gameHandler->getPlayerState(player, false);
+		auto state = gameHandler->gameInfo().getPlayerState(player, false);
 		if(state && state->isHuman() && initiator != player)
 			awaitingPlayers.insert(player);
 	}
@@ -699,7 +699,7 @@ bool PlayerMessageProcessor::handleCheatCode(const std::string & cheat, PlayerCo
 	std::vector<std::string> words;
 	boost::split(words, boost::trim_copy(cheat), boost::is_any_of("\t\r\n "));
 
-	if (words.empty() || !gameHandler->getStartInfo()->extraOptionsInfo.cheatsAllowed)
+	if (words.empty() || !gameHandler->gameInfo().getStartInfo()->extraOptionsInfo.cheatsAllowed)
 		return false;
 
 	//Make cheat name case-insensitive, but keep words/parameters (e.g. creature name) as it
@@ -797,8 +797,8 @@ bool PlayerMessageProcessor::handleCheatCode(const std::string & cheat, PlayerCo
 
 void PlayerMessageProcessor::executeCheatCode(const std::string & cheatName, PlayerColor player, ObjectInstanceID currObj, const std::vector<std::string> & words)
 {
-	const CGHeroInstance * hero = gameHandler->getHero(currObj);
-	const CGTownInstance * town = gameHandler->getTown(currObj);
+	const CGHeroInstance * hero = gameHandler->gameInfo().getHero(currObj);
+	const CGTownInstance * town = gameHandler->gameInfo().getTown(currObj);
 	if (!town && hero)
 		town = hero->getVisitedTown();
 

+ 21 - 21
server/processors/TurnOrderProcessor.cpp

@@ -33,14 +33,14 @@ int TurnOrderProcessor::simturnsTurnsMaxLimit() const
 {
 	if (simturnsMaxDurationDays)
 		return *simturnsMaxDurationDays;
-	return gameHandler->getStartInfo()->simturnsInfo.optionalTurns;
+	return gameHandler->gameInfo().getStartInfo()->simturnsInfo.optionalTurns;
 }
 
 int TurnOrderProcessor::simturnsTurnsMinLimit() const
 {
 	if (simturnsMinDurationDays)
 		return *simturnsMinDurationDays;
-	return gameHandler->getStartInfo()->simturnsInfo.requiredTurns;
+	return gameHandler->gameInfo().getStartInfo()->simturnsInfo.requiredTurns;
 }
 
 std::vector<TurnOrderProcessor::PlayerPair> TurnOrderProcessor::computeContactStatus() const
@@ -101,13 +101,13 @@ bool TurnOrderProcessor::playersInContact(PlayerColor left, PlayerColor right) c
 	boost::multi_array<bool, 3> leftReachability;
 	boost::multi_array<bool, 3> rightReachability;
 
-	int3 mapSize = gameHandler->getMapSize();
+	int3 mapSize = gameHandler->gameInfo().getMapSize();
 
 	leftReachability.resize(boost::extents[mapSize.z][mapSize.x][mapSize.y]);
 	rightReachability.resize(boost::extents[mapSize.z][mapSize.x][mapSize.y]);
 
-	const auto * leftInfo = gameHandler->getPlayerState(left, false);
-	const auto * rightInfo = gameHandler->getPlayerState(right, false);
+	const auto * leftInfo = gameHandler->gameInfo().getPlayerState(left, false);
+	const auto * rightInfo = gameHandler->gameInfo().getPlayerState(right, false);
 
 	for (auto obj : gameHandler->gameState().getMap().getObjects())
 	{
@@ -170,8 +170,8 @@ bool TurnOrderProcessor::isContactAllowed(PlayerColor active, PlayerColor waitin
 
 bool TurnOrderProcessor::computeCanActSimultaneously(PlayerColor active, PlayerColor waiting) const
 {
-	const auto * activeInfo = gameHandler->getPlayerState(active, false);
-	const auto * waitingInfo = gameHandler->getPlayerState(waiting, false);
+	const auto * activeInfo = gameHandler->gameInfo().getPlayerState(active, false);
+	const auto * waitingInfo = gameHandler->gameInfo().getPlayerState(waiting, false);
 
 	assert(active != waiting);
 	assert(activeInfo);
@@ -180,7 +180,7 @@ bool TurnOrderProcessor::computeCanActSimultaneously(PlayerColor active, PlayerC
 	if (activeInfo->human != waitingInfo->human)
 	{
 		// only one AI and one human can play simultaneously from single connection
-		if (!gameHandler->getStartInfo()->simturnsInfo.allowHumanWithAI)
+		if (!gameHandler->gameInfo().getStartInfo()->simturnsInfo.allowHumanWithAI)
 			return false;
 	}
 	else
@@ -190,13 +190,13 @@ bool TurnOrderProcessor::computeCanActSimultaneously(PlayerColor active, PlayerC
 			return false;
 	}
 
-	if (gameHandler->getDate(Date::DAY) < simturnsTurnsMinLimit())
+	if (gameHandler->gameInfo().getDate(Date::DAY) < simturnsTurnsMinLimit())
 		return true;
 
-	if (gameHandler->getDate(Date::DAY) > simturnsTurnsMaxLimit())
+	if (gameHandler->gameInfo().getDate(Date::DAY) > simturnsTurnsMaxLimit())
 		return false;
 
-	if (gameHandler->getStartInfo()->simturnsInfo.ignoreAlliedContacts && activeInfo->team == waitingInfo->team)
+	if (gameHandler->gameInfo().getStartInfo()->simturnsInfo.ignoreAlliedContacts && activeInfo->team == waitingInfo->team)
 		return true;
 
 	if (playersInContact(active, waiting))
@@ -207,8 +207,8 @@ bool TurnOrderProcessor::computeCanActSimultaneously(PlayerColor active, PlayerC
 
 bool TurnOrderProcessor::mustActBefore(PlayerColor left, PlayerColor right) const
 {
-	const auto * leftInfo = gameHandler->getPlayerState(left, false);
-	const auto * rightInfo = gameHandler->getPlayerState(right, false);
+	const auto * leftInfo = gameHandler->gameInfo().getPlayerState(left, false);
+	const auto * rightInfo = gameHandler->gameInfo().getPlayerState(right, false);
 
 	assert(left != right);
 	assert(leftInfo && rightInfo);
@@ -254,7 +254,7 @@ void TurnOrderProcessor::doStartNewDay()
 	bool activePlayer = false;
 	for (auto player : actedPlayers)
 	{
-		if (gameHandler->getPlayerState(player)->status == EPlayerStatus::INGAME)
+		if (gameHandler->gameInfo().getPlayerState(player)->status == EPlayerStatus::INGAME)
 			activePlayer = true;
 	}
 
@@ -272,8 +272,8 @@ void TurnOrderProcessor::doStartNewDay()
 
 void TurnOrderProcessor::doStartPlayerTurn(PlayerColor which)
 {
-	assert(gameHandler->getPlayerState(which));
-	assert(gameHandler->getPlayerState(which)->status == EPlayerStatus::INGAME);
+	assert(gameHandler->gameInfo().getPlayerState(which));
+	assert(gameHandler->gameInfo().getPlayerState(which)->status == EPlayerStatus::INGAME);
 
 	// Only if player is actually starting his turn (and not loading from save)
 	if (!actingPlayers.count(which))
@@ -296,7 +296,7 @@ void TurnOrderProcessor::doStartPlayerTurn(PlayerColor which)
 void TurnOrderProcessor::doEndPlayerTurn(PlayerColor which)
 {
 	assert(isPlayerMakingTurn(which));
-	assert(gameHandler->getPlayerStatus(which) == EPlayerStatus::INGAME);
+	assert(gameHandler->gameInfo().getPlayerStatus(which) == EPlayerStatus::INGAME);
 
 	actingPlayers.erase(which);
 	actedPlayers.insert(which);
@@ -340,7 +340,7 @@ bool TurnOrderProcessor::onPlayerEndsTurn(PlayerColor which)
 		return false;
 	}
 
-	if(gameHandler->getPlayerStatus(which) != EPlayerStatus::INGAME)
+	if(gameHandler->gameInfo().getPlayerStatus(which) != EPlayerStatus::INGAME)
 	{
 		gameHandler->complain("Can not end turn for player that is not in game!");
 		return false;
@@ -356,7 +356,7 @@ bool TurnOrderProcessor::onPlayerEndsTurn(PlayerColor which)
 
 	// it is possible that player have lost - e.g. spent 7 days without town
 	// in this case - don't call doEndPlayerTurn - turn transfer was already handled by onPlayerEndsGame
-	if(gameHandler->getPlayerStatus(which) == EPlayerStatus::INGAME)
+	if(gameHandler->gameInfo().getPlayerStatus(which) == EPlayerStatus::INGAME)
 		doEndPlayerTurn(which);
 
 	return true;
@@ -402,10 +402,10 @@ bool TurnOrderProcessor::isPlayerAwaitsNewDay(PlayerColor which) const
 
 void TurnOrderProcessor::setMinSimturnsDuration(int days)
 {
-	simturnsMinDurationDays = gameHandler->getDate(Date::DAY) + days;
+	simturnsMinDurationDays = gameHandler->gameInfo().getDate(Date::DAY) + days;
 }
 
 void TurnOrderProcessor::setMaxSimturnsDuration(int days)
 {
-	simturnsMaxDurationDays = gameHandler->getDate(Date::DAY) + days;
+	simturnsMaxDurationDays = gameHandler->gameInfo().getDate(Date::DAY) + days;
 }

+ 3 - 2
server/queries/MapQueries.cpp

@@ -13,6 +13,7 @@
 #include "QueriesProcessor.h"
 #include "../CGameHandler.h"
 #include "../TurnTimerHandler.h"
+#include "../../lib/callback/IGameInfoCallback.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
 #include "../../lib/mapObjects/MiscObjects.h"
 #include "../../lib/networkPacks/PacksForServer.h"
@@ -209,7 +210,7 @@ CTeleportDialogQuery::CTeleportDialogQuery(CGameHandler * owner, const TeleportD
 	CDialogQuery(owner)
 {
 	this->td = td;
-	addPlayer(gh->getHero(td.hero)->getOwner());
+	addPlayer(gh->gameInfo().getHero(td.hero)->getOwner());
 }
 
 CHeroLevelUpDialogQuery::CHeroLevelUpDialogQuery(CGameHandler * owner, const HeroLevelUp & Hlu, const CGHeroInstance * Hero):
@@ -266,7 +267,7 @@ void CHeroMovementQuery::onExposure(QueryPtr topQuery)
 		logGlobal->trace("Hero %s after victory over guard finishes visit to %s", hero->getNameTranslated(), tmh.end.toString());
 		//finish movement
 		visitDestAfterVictory = false;
-		gh->visitObjectOnTile(*gh->getTile(hero->convertToVisitablePos(tmh.end)), hero);
+		gh->visitObjectOnTile(*gh->gameInfo().getTile(hero->convertToVisitablePos(tmh.end)), hero);
 	}
 
 	owner->popIfTop(*this);

+ 2 - 2
test/CMakeLists.txt

@@ -72,7 +72,7 @@ set(test_SRCS
  		spells/targetConditions/TargetConditionItemFixture.cpp
 		
 		mock/BattleFake.cpp
-		mock/mock_IGameCallback.cpp
+		mock/mock_IGameEventCallback.cpp
  		mock/mock_MapService.cpp
  		mock/mock_BonusBearer.cpp
 		mock/mock_CPSICallback.cpp
@@ -94,7 +94,7 @@ set(test_HEADERS
 
 		mock/BattleFake.h
 		mock/mock_BonusBearer.h
-		mock/mock_IGameCallback.h
+		mock/mock_IGameEventCallback.h
  		mock/mock_MapService.h
 		mock/mock_BonusBearer.h
 

+ 13 - 14
test/game/CGameStateTest.cpp

@@ -11,7 +11,7 @@
 
 #include "mock/mock_Services.h"
 #include "mock/mock_MapService.h"
-#include "mock/mock_IGameCallback.h"
+#include "mock/mock_IGameEventCallback.h"
 #include "mock/mock_spells_Problem.h"
 
 #include "../../lib/VCMIDirs.h"
@@ -41,7 +41,7 @@ class CGameStateTest : public ::testing::Test, public SpellCastEnvironment, publ
 {
 public:
 	CGameStateTest()
-		: gameCallback(new GameCallbackMock(this)),
+		: gameEventCallback(std::make_shared<GameEventCallbackMock>(this)),
 		mapService("test/MiniTest/", this),
 		map(nullptr)
 	{
@@ -50,8 +50,7 @@ public:
 
 	void SetUp() override
 	{
-		gameState = std::make_shared<CGameState>(gameCallback.get());
-		gameCallback->setGameState(gameState);
+		gameState = std::make_shared<CGameState>();
 		gameState->preInit(&services);
 	}
 
@@ -195,26 +194,26 @@ public:
 
 		int3 tile(4,4,0);
 
-		const auto & t = *gameCallback->getTile(tile);
+		const auto & t = *gameState->getTile(tile);
 
 		auto terrain = t.getTerrainID();
 		BattleField terType(0);
-		BattleLayout layout = BattleLayout::createDefaultLayout(gameState->cb, attacker, defender);
+		BattleLayout layout = BattleLayout::createDefaultLayout(*gameState, attacker, defender);
 
 		//send info about battles
 
-		auto battle = BattleInfo::setupBattle(gameState->cb, tile, terrain, terType, armedInstancies, heroes, layout, nullptr);
+		auto battle = BattleInfo::setupBattle(gameState.get(), tile, terrain, terType, armedInstancies, heroes, layout, nullptr);
 
 		BattleStart bs;
 		bs.info = std::move(battle);
 		ASSERT_EQ(gameState->currentBattles.size(), 0);
-		gameCallback->sendAndApply(bs);
+		gameEventCallback->sendAndApply(bs);
 		ASSERT_EQ(gameState->currentBattles.size(), 1);
 	}
 
 	std::shared_ptr<CGameState> gameState;
 
-	std::shared_ptr<GameCallbackMock> gameCallback;
+	std::shared_ptr<GameEventCallbackMock> gameEventCallback;
 
 	MapServiceMock mapService;
 	ServicesMock services;
@@ -241,7 +240,7 @@ TEST_F(CGameStateTest, DISABLED_issue2765)
 		na.artHolder = defender->id;
 		na.artId = ArtifactID::BALLISTA;
 		na.pos = ArtifactPosition::MACH1;
-		gameCallback->sendAndApply(na);
+		gameEventCallback->sendAndApply(na);
 	}
 
 	startTestBattle(attacker, defender);
@@ -258,7 +257,7 @@ TEST_F(CGameStateTest, DISABLED_issue2765)
 		BattleUnitsChanged pack;
 		pack.changedStacks.emplace_back(info.id, UnitChanges::EOperation::ADD);
 		info.save(pack.changedStacks.back().data);
-		gameCallback->sendAndApply(pack);
+		gameEventCallback->sendAndApply(pack);
 	}
 
 	const CStack * att = nullptr;
@@ -332,7 +331,7 @@ TEST_F(CGameStateTest, DISABLED_battleResurrection)
 		na.artHolder = attacker->id;
 		na.artId = ArtifactID::SPELLBOOK;
 		na.pos = ArtifactPosition::SPELLBOOK;
-		gameCallback->sendAndApply(na);
+		gameEventCallback->sendAndApply(na);
 	}
 
 	startTestBattle(attacker, defender);
@@ -351,7 +350,7 @@ TEST_F(CGameStateTest, DISABLED_battleResurrection)
 		BattleUnitsChanged pack;
 		pack.changedStacks.emplace_back(info.id, UnitChanges::EOperation::ADD);
 		info.save(pack.changedStacks.back().data);
-		gameCallback->sendAndApply(pack);
+		gameEventCallback->sendAndApply(pack);
 	}
 
 	{
@@ -366,7 +365,7 @@ TEST_F(CGameStateTest, DISABLED_battleResurrection)
 		BattleUnitsChanged pack;
 		pack.changedStacks.emplace_back(info.id, UnitChanges::EOperation::ADD);
 		info.save(pack.changedStacks.back().data);
-		gameCallback->sendAndApply(pack);
+		gameEventCallback->sendAndApply(pack);
 	}
 
 	CStack * unit = gameState->currentBattles.front()->getStack(unitId);

+ 0 - 38
test/mock/mock_IGameCallback.cpp

@@ -1,38 +0,0 @@
-/*
- * mock_IGameCallback.cpp, 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
- *
- */
-#include "StdInc.h"
-
-#include "mock_IGameCallback.h"
-
-GameCallbackMock::GameCallbackMock(UpperCallback * upperCallback_)
-	: upperCallback(upperCallback_)
-{
-
-}
-
-GameCallbackMock::~GameCallbackMock()
-{
-
-}
-
-void GameCallbackMock::setGameState(std::shared_ptr<CGameState> newGameState)
-{
-	gamestate = newGameState;
-}
-
-void GameCallbackMock::sendAndApply(CPackForClient & pack)
-{
-	upperCallback->apply(pack);
-}
-
-vstd::RNG & GameCallbackMock::getRandomGenerator()
-{
-	throw std::runtime_error("Not implemented!");
-}

+ 0 - 114
test/mock/mock_IGameCallback.h

@@ -1,114 +0,0 @@
-/*
- * mock_IGameCallback.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 <vcmi/ServerCallback.h>
-
-#include "../../lib/callback/CGameInfoCallback.h"
-#include "../../lib/callback/IGameEventCallback.h"
-#include "../../lib/int3.h"
-#include "../../lib/ResourceSet.h"
-
-class GameCallbackMock : public CGameInfoCallback, public IGameEventCallback
-{
-	std::shared_ptr<CGameState> gamestate;
-public:
-	using UpperCallback = ::ServerCallback;
-
-	GameCallbackMock(UpperCallback * upperCallback_);
-	virtual ~GameCallbackMock();
-
-	void setGameState(std::shared_ptr<CGameState> gameState);
-	CGameState & gameState() final { return *gamestate; }
-	const CGameState & gameState() const final { return *gamestate; }
-
-
-	///STUBS, to be removed as long as same methods moved from GameHandler
-
-	//all calls to such methods should be replaced with other object calls or actual netpacks
-	//currently they are declared in callbacks, overridden in GameHandler and stubbed in client
-
-	//TODO: fail all stub calls
-
-	void setObjPropertyValue(ObjectInstanceID objid, ObjProperty prop, int32_t value = 0) override {}
-	void setObjPropertyID(ObjectInstanceID objid, ObjProperty prop, ObjPropertyID identifier) override {}
-	void setRewardableObjectConfiguration(ObjectInstanceID mapObjectID, const Rewardable::Configuration & configuration) override {}
-	void setRewardableObjectConfiguration(ObjectInstanceID townInstanceID, BuildingID buildingID, const Rewardable::Configuration & configuration) override {}
-
-	void showInfoDialog(InfoWindow * iw) override {}
-
-	void changeSpells(const CGHeroInstance * hero, bool give, const std::set<SpellID> &spells) override {}
-	void setResearchedSpells(const CGTownInstance * town, int level, const std::vector<SpellID> & spells, bool accepted) override {}
-	bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) override {return false;}
-	void createBoat(const int3 & visitablePosition, BoatId type, PlayerColor initiator) override {}
-	void setOwner(const CGObjectInstance * objid, PlayerColor owner) override {}
-	void giveExperience(const CGHeroInstance * hero, TExpType val) override {}
-	void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, ChangeValueMode mode) override {}
-	void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, ChangeValueMode mode) override {}
-	void showBlockingDialog(const IObjectInterface * caller, BlockingDialog *iw) override {}
-	void showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID hid, bool removableUnits) override {} //cb will be called when player closes garrison window
-	void showTeleportDialog(TeleportDialog *iw) override {}
-	void showObjectWindow(const CGObjectInstance * object, EOpenWindowMode window, const CGHeroInstance * visitor, bool addQuery) override {};
-	void giveResource(PlayerColor player, GameResID which, int val) override {}
-	void giveResources(PlayerColor player, ResourceSet resources) override {}
-
-	void giveCreatures(const CGHeroInstance * h, const CCreatureSet &creatures) override{}
-	void giveCreatures(const CArmedInstance *objid, const CGHeroInstance * h, const CCreatureSet &creatures, bool remove) override {}
-	void takeCreatures(ObjectInstanceID objid, const std::vector<CStackBasicDescriptor> &creatures, bool forceRemoval) override {}
-	bool changeStackCount(const StackLocation &sl, TQuantity count, ChangeValueMode mode) override {return false;}
-	bool changeStackType(const StackLocation &sl, const CCreature *c) override {return false;}
-	bool insertNewStack(const StackLocation &sl, const CCreature *c, TQuantity count = -1) override {return false;} //count -1 => moves whole stack
-	bool eraseStack(const StackLocation &sl, bool forceRemoval = false) override {return false;}
-	bool swapStacks(const StackLocation &sl1, const StackLocation &sl2) override {return false;}
-	bool addToSlot(const StackLocation &sl, const CCreature *c, TQuantity count) override {return false;} //makes new stack or increases count of already existing
-	void tryJoiningArmy(const CArmedInstance *src, const CArmedInstance *dst, bool removeObjWhenFinished, bool allowMerging) override {} //merges army from src do dst or opens a garrison window
-	bool moveStack(const StackLocation &src, const StackLocation &dst, TQuantity count) override {return false;}
-
-	void removeAfterVisit(const ObjectInstanceID & id) override {} //object will be destroyed when interaction is over. Do not call when interaction is not ongoing!
-
-	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 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;}
-
-	void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {}
-	void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {}
-	void visitCastleObjects(const CGTownInstance * obj, const CGHeroInstance * hero) override {}
-	void startBattle(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const BattleLayout & layout, const CGTownInstance *town) override {} //use hero=nullptr for no hero
-	void startBattle(const CArmedInstance *army1, const CArmedInstance *army2) override {}
-	bool moveHero(ObjectInstanceID hid, int3 dst, EMovementMode movementMode, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override {return false;}
-	bool swapGarrisonOnSiege(ObjectInstanceID tid) override {return false;}
-	void giveHeroBonus(GiveBonus * bonus) override {}
-	void setMovePoints(SetMovePoints * smp) override {}
-	void setMovePoints(ObjectInstanceID hid, int val, ChangeValueMode mode) override {};
-	void setManaPoints(ObjectInstanceID hid, int val) override {}
-	void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) override {}
-	void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override {}
-	void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override {} //when two heroes meet on adventure map
-	void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) override {}
-	void changeFogOfWar(const std::unordered_set<int3> &tiles, PlayerColor player, ETileVisibility mode) override {}
-	void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override {}
-
-	///useful callback methods
-	void sendAndApply(CPackForClient & pack) override;
-
-	bool isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero) override {return false;}
-
-	vstd::RNG & getRandomGenerator() override;
-
-#if SCRIPTING_ENABLED
-	MOCK_CONST_METHOD0(getGlobalContextPool, scripting::Pool *());
-#endif
-
-private:
-	UpperCallback * upperCallback;
-};

+ 30 - 0
test/mock/mock_IGameEventCallback.cpp

@@ -0,0 +1,30 @@
+/*
+ * mock_IGameEventCallback.cpp, 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
+ *
+ */
+#include "StdInc.h"
+
+#include "mock_IGameEventCallback.h"
+
+GameEventCallbackMock::GameEventCallbackMock(UpperCallback * upperCallback_)
+	: upperCallback(upperCallback_)
+{
+
+}
+
+GameEventCallbackMock::~GameEventCallbackMock() = default;
+
+void GameEventCallbackMock::sendAndApply(CPackForClient & pack)
+{
+	upperCallback->apply(pack);
+}
+
+vstd::RNG & GameEventCallbackMock::getRandomGenerator()
+{
+	throw std::runtime_error("Not implemented!");
+}

+ 76 - 3
test/mock/mock_IGameEventCallback.h

@@ -10,14 +10,87 @@
 
 #pragma once
 
-#include "../../lib/IGameCallback.h"
+#include <vcmi/ServerCallback.h>
 
-class IGameEventCallbackMock : public IGameEventCallback
+#include "../../lib/callback/IGameEventCallback.h"
+#include "../../lib/int3.h"
+#include "../../lib/ResourceSet.h"
+
+class GameEventCallbackMock : public IGameEventCallback
 {
 public:
+	using UpperCallback = ::ServerCallback;
 
-};
+	GameEventCallbackMock(UpperCallback * upperCallback_);
+	virtual ~GameEventCallbackMock();
+
+	void setObjPropertyValue(ObjectInstanceID objid, ObjProperty prop, int32_t value) override {}
+	void setObjPropertyID(ObjectInstanceID objid, ObjProperty prop, ObjPropertyID identifier) override {}
+	void setRewardableObjectConfiguration(ObjectInstanceID mapObjectID, const Rewardable::Configuration & configuration) override {}
+	void setRewardableObjectConfiguration(ObjectInstanceID townInstanceID, BuildingID buildingID, const Rewardable::Configuration & configuration) override {}
+
+	void showInfoDialog(InfoWindow * iw) override {}
+
+	void changeSpells(const CGHeroInstance * hero, bool give, const std::set<SpellID> &spells) override {}
+	void setResearchedSpells(const CGTownInstance * town, int level, const std::vector<SpellID> & spells, bool accepted) override {}
+	bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) override {return false;}
+	void createBoat(const int3 & visitablePosition, BoatId type, PlayerColor initiator) override {}
+	void setOwner(const CGObjectInstance * objid, PlayerColor owner) override {}
+	void giveExperience(const CGHeroInstance * hero, TExpType val) override {}
+	void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, ChangeValueMode mode) override {}
+	void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, ChangeValueMode mode) override {}
+	void showBlockingDialog(const IObjectInterface * caller, BlockingDialog *iw) override {}
+	void showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID hid, bool removableUnits) override {} //cb will be called when player closes garrison window
+	void showTeleportDialog(TeleportDialog *iw) override {}
+	void showObjectWindow(const CGObjectInstance * object, EOpenWindowMode window, const CGHeroInstance * visitor, bool addQuery) override {};
+	void giveResource(PlayerColor player, GameResID which, int val) override {}
+	void giveResources(PlayerColor player, const ResourceSet & resources) override {}
 
+	void giveCreatures(const CGHeroInstance * h, const CCreatureSet &creatures) override{}
+	void giveCreatures(const CArmedInstance *objid, const CGHeroInstance * h, const CCreatureSet &creatures, bool remove) override {}
+	void takeCreatures(ObjectInstanceID objid, const std::vector<CStackBasicDescriptor> &creatures, bool forceRemoval) override {}
+	bool changeStackCount(const StackLocation &sl, TQuantity count, ChangeValueMode mode) override {return false;}
+	bool changeStackType(const StackLocation &sl, const CCreature *c) override {return false;}
+	bool insertNewStack(const StackLocation &sl, const CCreature *c, TQuantity count) override {return false;} //count -1 => moves whole stack
+	bool eraseStack(const StackLocation &sl, bool forceRemoval) override {return false;}
+	bool swapStacks(const StackLocation &sl1, const StackLocation &sl2) override {return false;}
+	bool addToSlot(const StackLocation &sl, const CCreature *c, TQuantity count) override {return false;} //makes new stack or increases count of already existing
+	void tryJoiningArmy(const CArmedInstance *src, const CArmedInstance *dst, bool removeObjWhenFinished, bool allowMerging) override {} //merges army from src do dst or opens a garrison window
+	bool moveStack(const StackLocation &src, const StackLocation &dst, TQuantity count) override {return false;}
 
+	void removeAfterVisit(const ObjectInstanceID & id) override {} //object will be destroyed when interaction is over. Do not call when interaction is not ongoing!
 
+	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 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;}
 
+	void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {}
+	void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {}
+	void visitCastleObjects(const CGTownInstance * obj, const CGHeroInstance * hero) override {}
+	void startBattle(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const BattleLayout & layout, const CGTownInstance *town) override {} //use hero=nullptr for no hero
+	void startBattle(const CArmedInstance *army1, const CArmedInstance *army2) override {}
+	bool moveHero(ObjectInstanceID hid, int3 dst, EMovementMode movementMode, bool transit, PlayerColor asker) override {return false;}
+	bool swapGarrisonOnSiege(ObjectInstanceID tid) override {return false;}
+	void giveHeroBonus(GiveBonus * bonus) override {}
+	void setMovePoints(SetMovePoints * smp) override {}
+	void setMovePoints(ObjectInstanceID hid, int val, ChangeValueMode mode) override {};
+	void setManaPoints(ObjectInstanceID hid, int val) override {}
+	void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId) override {}
+	void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override {}
+	void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override {} //when two heroes meet on adventure map
+	void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) override {}
+	void changeFogOfWar(const std::unordered_set<int3> &tiles, PlayerColor player, ETileVisibility mode) override {}
+	void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override {}
+
+	///useful callback methods
+	void sendAndApply(CPackForClient & pack) override;
+
+	bool isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero) override {return false;}
+
+	vstd::RNG & getRandomGenerator() override;
+
+private:
+	UpperCallback * upperCallback;
+};

+ 1 - 1
test/netpacks/NetPackFixture.cpp

@@ -24,7 +24,7 @@ NetPackFixture::~NetPackFixture() = default;
 
 void NetPackFixture::setUp()
 {
-	gameState = std::make_shared<GameStateFake>(nullptr);
+    gameState = std::make_shared<GameStateFake>();
 }
 
 }

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.