← Back to team overview

widelands-dev team mailing list archive

[Merge] lp:~widelands-dev/widelands/campaignselect_box into lp:widelands

 

GunChleoc has proposed merging lp:~widelands-dev/widelands/campaignselect_box into lp:widelands.

Commit message:
Redesigned the campaign/scenario selection screens to use Box layout

- Tables entries can now be greyed out
- Converted campaigns definition to Lua
- Mark scenarios as solved rather than as unlocked
- Automatic conversion of legacy campvis file contents if the new file
  doesn't exist yet so that players will not lose their progress

Requested reviews:
  Widelands Developers (widelands-dev)
Related bugs:
  Bug #627361 in widelands: "Show available campaigns and missions greyed out"
  https://bugs.launchpad.net/widelands/+bug/627361
  Bug #1377660 in widelands: "Fullscreen Menu overhaul"
  https://bugs.launchpad.net/widelands/+bug/1377660
  Bug #1398733 in widelands: "Fullscreen Menus cannot relayout themselves"
  https://bugs.launchpad.net/widelands/+bug/1398733
  Bug #1634750 in widelands: "Convert campaigns.conf and tutorials.conf to Lua"
  https://bugs.launchpad.net/widelands/+bug/1634750
  Bug #1799809 in widelands: "Resize UI when toggling between fullscreen and windows mode"
  https://bugs.launchpad.net/widelands/+bug/1799809

For more details, see:
https://code.launchpad.net/~widelands-dev/widelands/campaignselect_box/+merge/360901
-- 
Your team Widelands Developers is requested to review the proposed merge of lp:~widelands-dev/widelands/campaignselect_box into lp:widelands.
=== modified file 'data/campaigns/atl01.wmf/scripting/mission_thread.lua'
--- data/campaigns/atl01.wmf/scripting/mission_thread.lua	2018-11-16 06:41:28 +0000
+++ data/campaigns/atl01.wmf/scripting/mission_thread.lua	2018-12-13 23:38:35 +0000
@@ -233,8 +233,7 @@
 
    -- Success
    msg_boxes(scenario_won)
-   p1:reveal_scenario("atlanteans01")
-   p1:reveal_campaign("campsect3")
+   p1:mark_scenario_as_solved("atl01.wmf")
 end
 
 

=== modified file 'data/campaigns/bar01.wmf/scripting/mission_thread.lua'
--- data/campaigns/bar01.wmf/scripting/mission_thread.lua	2018-11-19 08:09:41 +0000
+++ data/campaigns/bar01.wmf/scripting/mission_thread.lua	2018-12-13 23:38:35 +0000
@@ -327,8 +327,7 @@
    end
 
    message_box_objective(plr, msg_mission_complete)
-   plr:reveal_scenario("barbariantut01")
-   p1:reveal_campaign("campsect1")
+   p1:mark_scenario_as_solved("bar01.wmf")
 end
 
 

=== modified file 'data/campaigns/bar02.wmf/scripting/mission_thread.lua'
--- data/campaigns/bar02.wmf/scripting/mission_thread.lua	2018-01-26 09:00:04 +0000
+++ data/campaigns/bar02.wmf/scripting/mission_thread.lua	2018-12-13 23:38:35 +0000
@@ -376,8 +376,7 @@
 
    campaign_message_box(story_msg_7)
 
-   p1:reveal_scenario("barbariantut02")
-   p1:reveal_campaign("campsect1")
+   p1:mark_scenario_as_solved("bar02.wmf")
 end
 
 run(initial_message_and_small_food_economy)

=== renamed file 'data/campaigns/campaigns.conf' => 'data/campaigns/campaigns.lua'
--- data/campaigns/campaigns.conf	2018-08-13 16:44:58 +0000
+++ data/campaigns/campaigns.lua	2018-12-13 23:38:35 +0000
@@ -1,141 +1,127 @@
-##########################################
-#      Campaign configuration - file     #
-##########################################
-
-
-
-#####
-# Section "global"
-#
-# version      = Version-number of this file - used to check, whether campvis-
-#               file needs to be updated. Higher the value, if you added a map
-#               or campaign or if you changed a visibility-value.
-# campname??   = Name of the Campaign with the number ??.
-# campsect??   = Name of section, to be loaded, if this campaign is selected.
-# campdiff??   = Level of difficulty 1-3 (easy to hard) (this is only a visua-
-#               lisation, it has no influences on the real difficulty).
-# campdesc??   = Description of the campaign
-# campvisi??   = Is campaign visible by default? (1=yes, 0=no)
-# * cnewvisi?? = the name of a campaign or a scenario that must be visible, to show
-#                this entry as well - this string unhides the map, if the player
-#                completed the requirements with an older cconfig version.
-#####
-
-[global]
-version = 8
-# Barbarians Introduction
-campname0=_"The Second Empire"
-campsect0=barbariantut
-camptribe0=_"Barbarians"
-campdiff0=1
-campdiffdescr0=_"Easy. Introduces the Barbarians"
-campdesc0=_"When Chat’Karuth died, he was an old man, father to three strong and ambitious sons, and warlord to an army that could match any enemy willing to rise against the ancient forests. Though at the end of his glorious reign, Chat’Karuth chose his eldest son, Thron, to succeed him as the tribe’s warlord – a decision that left his two brothers unsatisfied. The old warlord knew that. As his father instructed him, Thron left the capital of Al’thunran, the home of the Throne Among the Trees, and withdrew his forces to the high hills where he buried the corpse of his father. There he swore to the gods and his father’s spirit that he’d return to re-established order. While his brothers have raged blind war against Thron and the few forces he left to secure the borders of Al’thunran, the young warlord seeks to reunite his ambitious brothers and force the tribes to march once again under a common banner."
-campvisi0=1
-# Empire Introduction
-campname1=_"The Months of Exile"
-campsect1=empiretut
-camptribe1=_"Empire"
-campdiff1=1
-campdiffdescr1=_"Easy. Introduces the Empire"
-campdesc1=_"Six months ago, Lutius – a young general of the Empire – was sent with 150 soldiers to the frontier beyond the northern forests where Barbarian tribes were crossing onto land held by the Empire. His task was to defend the Empire’s land. At first, everything was calm. He even talked to a few Barbarian children and thought about a peaceful life – side by side with this archaic folk. He began to feel safer and his army began to drop their attention off the potential enemy. That was their undoing. One night in March his unprepared army was attacked by 100 Barbarian footmen and was completely scattered. Only with his bare life he and a handful of his soldiers survived."
-campvisi1=0
-# Atlantean Introduction
-campname2=_"The Run for the Fire"
-campsect2=atlanteans
-camptribe2=_"Atlanteans"
-campdiff2=2
-campdiffdescr2=_"Challenging. Introduces the Atlanteans"
-campdesc2=_"When their God lost faith in the Atlanteans and drowned their island, one woman’s struggle for justice and a second chance for her people would become the stuff of legends. Leading the remaining Atlanteans into a new future in a new part of the World, Jundlina became the most powerful human of her time, but at a high cost: her humanity and soul."
-campvisi2=0
-cnewvisi2=empiretut01
-# Frisian Introduction
-campname3=_"From Water to Ice"
-campsect3=frisians
-camptribe3=_"Frisians"
-campdiff3=3
-campdiffdescr3=_"For advanced players. Introduces the Frisians"
-campdesc3=_"Living off the ocean is a constant struggle, and even more so for the inhabitants of the Frisian North Sea shore. Was the last storm flood, the most devastating one in human memory, really nothing more than yet another example for the hardships all Frisians have to face – or a sign from the gods that a tribe that only just settled here must seek out an entirely new home?"
-campvisi3=0
-cnewvisi3=atlanteans01
-
-
-
-#####
-# Sections of the campaign - maps
-# Naming MUST be the name of the campaign-section + "??" where ?? is an increasing number.
-#
-# name      = name of the map.
-# * newvisi = the name of a campaign or a scenario that must be visible, to show
-#             this entry as well - this string unhides the map, if the player
-#             completed the requirements with an older cconfig version.
-# visible   = is this map visible(1), or does it need another map to be played first(0).
-# path      = path to the map.
-#####
-
-[barbariantut00]
-name=_"A Place to Call Home"
-visible=1
-path="campaigns/bar01.wmf"
-
-[barbariantut01]
-name=_"This Land is Our Land"
-visible=0
-path="campaigns/bar02.wmf"
-
-[barbariantut02]
-name=_"Not yet implemented"
-visible=0
-path="campaigns/dummy.wmf"
-
-[empiretut00]
-name=_"The Strands of Malac’ Mor"
-visible=1
-path="campaigns/emp01.wmf"
-
-[empiretut01]
-name=_"An Outpost for Exile"
-visible=0
-path="campaigns/emp02.wmf"
-
-[empiretut02]
-name=_"Neptune’s Revenge"
-visible=0
-path="campaigns/emp03.wmf"
-
-[empiretut03]
-name=_"Surprise, Surprise!"
-visible=0
-path="campaigns/emp04.wmf"
-
-[empiretut04]
-name=_"Not yet implemented"
-newvisi="campsect2"
-visible=0
-path="campaigns/dummy.wmf"
-
-
-[atlanteans00]
-name=_"From Nemesis to Genesis"
-visible=1
-path="campaigns/atl01.wmf"
-
-[atlanteans01]
-name=_"Not yet implemented"
-visible=0
-path="campaigns/dummy.wmf"
-
-
-[frisians00]
-name=_"The Great Stormflood"
-visible=1
-path="campaigns/fri01.wmf"
-
-[frisians01]
-name=_"Colder than Ice"
-visible=0
-path="campaigns/fri02.wmf"
-
-[frisians02]
-name=_"Not yet implemented"
-visible=0
-path="campaigns/dummy.wmf"
+--##########################################
+--#      Campaign configuration - file     #
+--##########################################
+
+return {
+   --##########################################
+   --#   Descriptions of difficulty levels    #
+   --##########################################
+   difficulties = {
+      {
+         -- This will be prefixed to any text that you might add in each
+         -- campaign's difficulty description.
+         -- TRANSLATORS: The difficulty level of a campign
+         descname = _"Easy.",
+         -- An image to represent the difficulty level
+         image = "images/ui_fsmenu/easy.png",
+      },
+      {
+         -- TRANSLATORS: The difficulty level of a campign
+         descname = _"Medium.",
+         image = "images/ui_fsmenu/medium.png",
+      },
+      {
+         -- TRANSLATORS: The difficulty level of a campign
+         descname = _"Hard.",
+         image = "images/ui_fsmenu/hard.png",
+      },
+      {
+         -- TRANSLATORS: The difficulty level of a campign
+         descname = _"Challenging.",
+         image = "images/ui_fsmenu/challenging.png",
+      },
+   },
+
+   --##########################################
+   --#        The campaigns themselves        #
+   --##########################################
+   campaigns = {
+      {
+         -- **** Barbarians Introduction ****
+         -- The name the user sees on screen
+         -- TRANSLATORS: The name of a Barbarian campign
+         descname = _"The Second Empire",
+         -- The internal name of the tribe that the user will be playing
+         tribe = "barbarians",
+         -- The difficulty of this campaign.
+         -- Start counting at 1 in the "difficulties" table above
+         -- TRANSLATORS: A short description of a campign
+         difficulty = { level=1, description=_"Introduces the Barbarians." },
+         -- An introduction story
+         -- TRANSLATORS: A long description of a campign
+         description = _"When Chat’Karuth died, he was an old man, father to three strong and ambitious sons, and warlord to an army that could match any enemy willing to rise against the ancient forests. Though at the end of his glorious reign, Chat’Karuth chose his eldest son, Thron, to succeed him as the tribe’s warlord – a decision that left his two brothers unsatisfied. The old warlord knew that. As his father instructed him, Thron left the capital of Al’thunran, the home of the Throne Among the Trees, and withdrew his forces to the high hills where he buried the corpse of his father. There he swore to the gods and his father’s spirit that he’d return to re-established order. While his brothers have raged blind war against Thron and the few forces he left to secure the borders of Al’thunran, the young warlord seeks to reunite his ambitious brothers and force the tribes to march once again under a common banner.",
+         -- The campaign's scenarios. The first scenario is always visible if
+         -- the campaign itself is visible.
+         -- Paths to the scenarios are relative to data/campaigns
+         -- Once a scenario has been marked as solved by calling
+         -- `player:mark_scenario_as_solved`, the next scenario in the list will
+         -- become visible.
+         -- Also, campaigns that have a prerequisite scenarios will be unlocked
+         -- when any of the referenced scenarios has been solved.
+         scenarios = {
+            "bar01.wmf",
+            "bar02.wmf",
+            "dummy.wmf"
+         }
+      },
+      {
+         -- **** Empire Introduction ****
+         -- TRANSLATORS: The name of an Empire campign
+         descname = _"The Months of Exile",
+         tribe = "empire",
+         -- TRANSLATORS: A short description of a campign
+         difficulty = { level=2, description=_"Introduces the Empire." },
+         -- TRANSLATORS: A long description of a campign
+         description = _"Six months ago, Lutius – a young general of the Empire – was sent with 150 soldiers to the frontier beyond the northern forests where Barbarian tribes were crossing onto land held by the Empire. His task was to defend the Empire’s land. At first, everything was calm. He even talked to a few Barbarian children and thought about a peaceful life – side by side with this archaic folk. He began to feel safer and his army began to drop their attention off the potential enemy. That was their undoing. One night in March his unprepared army was attacked by 100 Barbarian footmen and was completely scattered. Only with his bare life he and a handful of his soldiers survived.",
+         -- If `prerequisites` is present, the campaign is greyed out by default.
+         -- The campaign will become unlocked when any of the referenced scenarios
+         -- have been solved.
+         prerequisites = {
+            "bar01.wmf",
+         },
+         scenarios = {
+            "emp01.wmf",
+            "emp02.wmf",
+            "emp03.wmf",
+            "emp04.wmf",
+            "dummy.wmf"
+         }
+      },
+      {
+         -- **** Atlantean Introduction ****
+         -- TRANSLATORS: The name of an Atlantean campign
+         descname = _"The Run for the Fire",
+         tribe = "atlanteans",
+         -- TRANSLATORS: A short description of a campign
+         difficulty = { level=3, description=_"Introduces the Atlanteans." },
+         -- TRANSLATORS: A long description of a campign
+         description = _"When their God lost faith in the Atlanteans and drowned their island, one woman’s struggle for justice and a second chance for her people would become the stuff of legends. Leading the remaining Atlanteans into a new future in a new part of the World, Jundlina became the most powerful human of her time, but at a high cost: her humanity and soul.",
+         prerequisites = {
+            "emp02.wmf",
+         },
+         scenarios = {
+            "atl01.wmf",
+            "dummy.wmf"
+         }
+      },
+      {
+         -- **** Frisian Introduction ****
+         -- TRANSLATORS: The name of a Frisian campign
+         descname = _"From Water to Ice",
+         tribe = "frisians",
+         -- TRANSLATORS: A short description of a campign
+         difficulty = { level=4, description=_"Introduces the Frisians." },
+         -- TRANSLATORS: A long description of a campign
+         description = _"Living off the ocean is a constant struggle, and even more so for the inhabitants of the Frisian North Sea shore. Was the last storm flood, the most devastating one in human memory, really nothing more than yet another example for the hardships all Frisians have to face – or a sign from the gods that a tribe that only just settled here must seek out an entirely new home?",
+         prerequisites = {
+            "emp04.wmf",
+            "atl01.wmf",
+         },
+         scenarios = {
+            "fri01.wmf",
+            "fri02.wmf",
+            "dummy.wmf"
+         }
+      }
+   }
+}

=== modified file 'data/campaigns/dummy.wmf/elemental'
--- data/campaigns/dummy.wmf/elemental	2017-12-17 19:06:02 +0000
+++ data/campaigns/dummy.wmf/elemental	2018-12-13 23:38:35 +0000
@@ -8,6 +8,7 @@
 name=_"Not yet implemented"
 # TRANSLATORS: Author for dummy map
 author=_"Nobody"
+# TRANSLATORS: Description for greyed out dummy scenario displayed in campaign screen
 descr=_"Sorry, this map is not yet implemented."
 hint=
 tags=

=== modified file 'data/campaigns/emp01.wmf/scripting/mission_thread.lua'
--- data/campaigns/emp01.wmf/scripting/mission_thread.lua	2017-06-25 12:53:48 +0000
+++ data/campaigns/emp01.wmf/scripting/mission_thread.lua	2018-12-13 23:38:35 +0000
@@ -89,7 +89,7 @@
    sleep(25000) -- Sleep a while
 
    campaign_message_box(diary_page_4)
-   p1:reveal_scenario("empiretut01")
+   p1:mark_scenario_as_solved("emp01.wmf")
 end
 
 -- Show a funny message when the player has build 10 blockhouses

=== modified file 'data/campaigns/emp02.wmf/scripting/mission_thread.lua'
--- data/campaigns/emp02.wmf/scripting/mission_thread.lua	2018-11-19 08:09:41 +0000
+++ data/campaigns/emp02.wmf/scripting/mission_thread.lua	2018-12-13 23:38:35 +0000
@@ -265,8 +265,7 @@
    campaign_message_box(seven_days_later)
    campaign_message_box(diary_page_11)
 
-   p1:reveal_scenario("empiretut02")
-   p1:reveal_campaign("campsect2")
+   p1:mark_scenario_as_solved("emp02.wmf")
 end
 
 run(building_materials)

=== modified file 'data/campaigns/emp03.wmf/scripting/mission_thread.lua'
--- data/campaigns/emp03.wmf/scripting/mission_thread.lua	2018-09-13 21:00:11 +0000
+++ data/campaigns/emp03.wmf/scripting/mission_thread.lua	2018-12-13 23:38:35 +0000
@@ -265,7 +265,7 @@
    sleep(25000)
    campaign_message_box(diary_page_5)
 
-   p1:reveal_scenario("empiretut03")
+   p1:mark_scenario_as_solved("emp03.wmf")
 end
 
 -- After discovery of Barbarian ruins, we should hurry to build a full training capability

=== modified file 'data/campaigns/emp04.wmf/scripting/mission_thread.lua'
--- data/campaigns/emp04.wmf/scripting/mission_thread.lua	2018-11-16 06:41:28 +0000
+++ data/campaigns/emp04.wmf/scripting/mission_thread.lua	2018-12-13 23:38:35 +0000
@@ -389,9 +389,7 @@
    sleep(25000)
    campaign_message_box(diary_page_4)
 
-   p1:reveal_campaign("campsect2")
-   p1:reveal_campaign("campsect3")
-   p1:reveal_scenario("empiretut04")
+   p1:mark_scenario_as_solved("emp04.wmf")
 end
 
 -- another production chain that is ineffective and needs to be corrected

=== renamed file 'data/campaigns/tutorials.conf' => 'data/campaigns/tutorials.lua'
--- data/campaigns/tutorials.conf	2018-08-13 16:44:58 +0000
+++ data/campaigns/tutorials.lua	2018-12-13 23:38:35 +0000
@@ -1,28 +1,12 @@
-##########################################
-#     Tutorials configuration - file     #
-##########################################
-
-
-#####
-# Sections of the tutorial - maps
-# Naming MUST be the name of the tutorial-section + "??" where ?? is an increasing number.
-#
-# name      = name of the map.
-# path      = path to the map.
-#####
-
-[tutorials00]
-name=_"Basic Control"
-path="campaigns/tutorial01_basic_control.wmf"
-
-[tutorials01]
-name=_"Warfare"
-path="campaigns/tutorial02_warfare.wmf"
-
-[tutorials02]
-name=_"Seafaring"
-path="campaigns/tutorial03_seafaring.wmf"
-
-[tutorials03]
-name=_"Economy"
-path="campaigns/tutorial04_economy.wmf"
+--##########################################
+--#     Tutorials configuration - file     #
+--##########################################
+
+return {
+   -- The tutorial scenarios in the order that they will appear on screen
+   -- The paths are relative to data/campaigns
+   "tutorial01_basic_control.wmf",
+   "tutorial02_warfare.wmf",
+   "tutorial03_seafaring.wmf",
+   "tutorial04_economy.wmf"
+}

=== renamed file 'data/images/ui_fsmenu/hard.png' => 'data/images/ui_fsmenu/challenging.png'
=== added file 'data/images/ui_fsmenu/easy.png'
Binary files data/images/ui_fsmenu/easy.png	1970-01-01 00:00:00 +0000 and data/images/ui_fsmenu/easy.png	2018-12-13 23:38:35 +0000 differ
=== renamed file 'data/images/ui_fsmenu/challenging.png' => 'data/images/ui_fsmenu/hard.png'
=== renamed file 'data/images/ui_fsmenu/easy.png' => 'data/images/ui_fsmenu/medium.png'
=== modified file 'src/base/i18n.cc'
--- src/base/i18n.cc	2018-12-13 07:24:01 +0000
+++ src/base/i18n.cc	2018-12-13 23:38:35 +0000
@@ -335,6 +335,7 @@
 }
 
 std::string localize_list(const std::vector<std::string>& items, ConcatenateWith listtype) {
+	i18n::Textdomain td("widelands");
 	std::string result;
 	for (std::vector<std::string>::const_iterator it = items.begin(); it != items.end(); ++it) {
 		if (it == items.begin()) {
@@ -365,4 +366,12 @@
 	}
 	return result;
 }
+
+std::string join_sentences(const std::string& sentence1, const std::string& sentence2) {
+	i18n::Textdomain td("widelands");
+	/** TRANSLATORS: Put 2 sentences one after the other. Languages using Chinese script probably
+	 * want to lose the blank space here. */
+	return (boost::format(pgettext("sentence_separator", "%1% %2%")) % sentence1 % sentence2).str();
+}
+
 }  // namespace i18n

=== modified file 'src/base/i18n.h'
--- src/base/i18n.h	2018-12-13 07:24:01 +0000
+++ src/base/i18n.h	2018-12-13 23:38:35 +0000
@@ -58,10 +58,19 @@
 void set_localedir(const std::string&);
 const std::string& get_localedir();
 
-// Localize a list of 'items'. The last 2 items are concatenated with "and" or
-// "or", depending on 'concatenate_with'.
 enum class ConcatenateWith { AND, OR, AMPERSAND, COMMA };
+/**
+  * Localize a list of 'items'. The last 2 items are concatenated with "and" or
+  * "or" etc, depending on 'concatenate_with'.
+  */
 std::string localize_list(const std::vector<std::string>& items, ConcatenateWith concatenate_with);
+
+/**
+ * Joins 2 sentences together. Use this rather than manually concatenating
+ * a blank space, because some languages don't use blank spaces.
+ */
+std::string join_sentences(const std::string& sentence1, const std::string& sentence2);
+
 }  // namespace i18n
 
 #endif  // end of include guard: WL_BASE_I18N_H

=== modified file 'src/graphic/text_layout.cc'
--- src/graphic/text_layout.cc	2018-12-13 07:24:01 +0000
+++ src/graphic/text_layout.cc	2018-12-13 23:38:35 +0000
@@ -222,6 +222,7 @@
 	}
 	NEVER_HERE();
 }
+
 std::string as_content(const std::string& txt, UI::PanelStyle style) {
 	switch (style) {
 	case UI::PanelStyle::kFsMenu:

=== modified file 'src/graphic/text_layout.h'
--- src/graphic/text_layout.h	2018-12-13 07:24:01 +0000
+++ src/graphic/text_layout.h	2018-12-13 23:38:35 +0000
@@ -105,9 +105,11 @@
                                     bool noescape = false);
 
 /**
+ * Heading in menu info texts
  * 'is_first' omits the vertical gap before the line.
  */
 std::string as_heading(const std::string& txt, UI::PanelStyle style, bool is_first = false);
+/// Paragraph in menu info texts
 std::string as_content(const std::string& txt, UI::PanelStyle style);
 
 /**

=== modified file 'src/logic/CMakeLists.txt'
--- src/logic/CMakeLists.txt	2018-08-24 07:12:20 +0000
+++ src/logic/CMakeLists.txt	2018-12-13 23:38:35 +0000
@@ -57,17 +57,6 @@
     wui
 )
 
-wl_library(logic_campaign_visibility
-  SRCS
-    campaign_visibility.cc
-    campaign_visibility.h
-  DEPENDS
-    base_exceptions
-    io_filesystem
-    logic_filesystem_constants
-    profile
-)
-
 wl_library(logic_constants
   SRCS
     widelands.cc

=== modified file 'src/logic/filesystem_constants.h'
--- src/logic/filesystem_constants.h	2018-11-09 08:00:35 +0000
+++ src/logic/filesystem_constants.h	2018-12-13 23:38:35 +0000
@@ -34,6 +34,7 @@
 
 /// Filesystem names for maps
 const std::string kMapsDir = "maps";
+const std::string kCampaignsDir = "campaigns";
 const std::string kWidelandsMapExtension = ".wmf";
 const std::string kS2MapExtension1 = ".swd";
 const std::string kS2MapExtension2 = ".wld";
@@ -55,7 +56,7 @@
 
 /// Filesystem names and intervals for savegames
 const std::string kSaveDir = "save";
-const std::string kCampVisFile = "save/campvis";
+const std::string kCampVisFile = "save/campaigns.conf";
 const std::string kSavegameExtension = ".wgf";
 const std::string kAutosavePrefix = "wl_autosave";
 // Default autosave interval in minutes

=== modified file 'src/scripting/CMakeLists.txt'
--- src/scripting/CMakeLists.txt	2018-09-14 08:44:42 +0000
+++ src/scripting/CMakeLists.txt	2018-12-13 23:38:35 +0000
@@ -109,7 +109,6 @@
     io_fileread
     io_filesystem
     logic
-    logic_campaign_visibility
     logic_constants
     logic_filesystem_constants
     logic_game_controller

=== modified file 'src/scripting/lua_game.cc'
--- src/scripting/lua_game.cc	2018-12-13 07:24:01 +0000
+++ src/scripting/lua_game.cc	2018-12-13 23:38:35 +0000
@@ -25,7 +25,7 @@
 
 #include "economy/economy.h"
 #include "economy/flag.h"
-#include "logic/campaign_visibility.h"
+#include "logic/filesystem_constants.h"
 #include "logic/game_controller.h"
 #include "logic/map_objects/tribes/tribe_descr.h"
 #include "logic/message.h"
@@ -93,8 +93,7 @@
    METHOD(LuaPlayer, add_objective),
    METHOD(LuaPlayer, reveal_fields),
    METHOD(LuaPlayer, hide_fields),
-   METHOD(LuaPlayer, reveal_scenario),
-   METHOD(LuaPlayer, reveal_campaign),
+   METHOD(LuaPlayer, mark_scenario_as_solved),
    METHOD(LuaPlayer, get_ships),
    METHOD(LuaPlayer, get_buildings),
    METHOD(LuaPlayer, get_suitability),
@@ -633,42 +632,23 @@
 }
 
 /* RST
-   .. method:: reveal_scenario(name)
+   .. method:: mark_scenario_as_solved(name)
 
-      This reveals a scenario inside a campaign. This only works for the
+      Marks a campaign scenario as solved. Reads the scenario definition in data/campaigns/campaigns.lua
+      to check which scenario and/or campaign should be revealed as a result. This only works for the
       interactive player and most likely also only in single player games.
 
-      :arg name: name of the scenario to reveal
+      :arg name: name of the scenario to be marked as solved
       :type name: :class:`string`
 */
 // UNTESTED
-int LuaPlayer::reveal_scenario(lua_State* L) {
+int LuaPlayer::mark_scenario_as_solved(lua_State* L) {
 	if (get_game(L).get_ipl()->player_number() != player_number())
 		report_error(L, "Can only be called for interactive player!");
 
-	CampaignVisibilitySave cvs;
-	cvs.set_map_visibility(luaL_checkstring(L, 2), true);
-
-	return 0;
-}
-
-/* RST
-   .. method:: reveal_campaign(name)
-
-      This reveals a campaign. This only works for the
-      interactive player and most likely also only in single player games.
-
-      :arg name: name of the campaign to reveal
-      :type name: :class:`string`
-*/
-// UNTESTED
-int LuaPlayer::reveal_campaign(lua_State* L) {
-	if (get_game(L).get_ipl()->player_number() != player_number()) {
-		report_error(L, "Can only be called for interactive player!");
-	}
-
-	CampaignVisibilitySave cvs;
-	cvs.set_campaign_visibility(luaL_checkstring(L, 2), true);
+	Profile campvis(kCampVisFile.c_str());
+	campvis.pull_section("scenarios").set_bool(luaL_checkstring(L, 2), true);
+	campvis.write(kCampVisFile.c_str(), false);
 
 	return 0;
 }

=== modified file 'src/scripting/lua_game.h'
--- src/scripting/lua_game.h	2018-12-13 07:24:01 +0000
+++ src/scripting/lua_game.h	2018-12-13 23:38:35 +0000
@@ -89,8 +89,7 @@
 	int add_objective(lua_State* L);
 	int reveal_fields(lua_State* L);
 	int hide_fields(lua_State* L);
-	int reveal_scenario(lua_State* L);
-	int reveal_campaign(lua_State* L);
+	int mark_scenario_as_solved(lua_State* L);
 	int get_ships(lua_State* L);
 	int get_buildings(lua_State* L);
 	int get_suitability(lua_State* L);

=== modified file 'src/ui_basic/table.cc'
--- src/ui_basic/table.cc	2018-12-13 07:24:01 +0000
+++ src/ui_basic/table.cc	2018-12-13 23:38:35 +0000
@@ -300,8 +300,8 @@
 				curx += curw;
 				continue;
 			}
-			std::shared_ptr<const UI::RenderedText> rendered_text =
-			   UI::g_fh->render(as_uifont(richtext_escape(entry_string)));
+			std::shared_ptr<const UI::RenderedText> rendered_text = UI::g_fh->render(
+			   as_uifont(richtext_escape(entry_string), UI_FONT_SIZE_SMALL, er.get_color()));
 
 			// Fix text alignment for BiDi languages if the entry contains an RTL character. We want
 			// this always on, e.g. for mixed language savegame filenames.
@@ -718,7 +718,7 @@
 	return ea.get_string(column) < eb.get_string(column);
 }
 
-Table<void*>::EntryRecord::EntryRecord(void* const e) : entry_(e) {
+Table<void*>::EntryRecord::EntryRecord(void* const e) : entry_(e), clr(UI_FONT_CLR_FG) {
 }
 
 void Table<void*>::EntryRecord::set_picture(uint8_t const col,

=== modified file 'src/ui_basic/table.h'
--- src/ui_basic/table.h	2018-12-13 07:24:01 +0000
+++ src/ui_basic/table.h	2018-12-13 23:38:35 +0000
@@ -88,7 +88,7 @@
 	void remove(uint32_t);
 	void remove_entry(Entry);
 
-	EntryRecord& add(void* const entry, const bool select_this = false);
+	EntryRecord& add(void* const entry, bool const select_this = false);
 
 	uint32_t size() const;
 	bool empty() const;
@@ -142,13 +142,8 @@
 			return entry_;
 		}
 		void set_color(const RGBColor& c) {
-			use_clr = true;
 			clr = c;
 		}
-
-		bool use_color() const {
-			return use_clr;
-		}
 		RGBColor get_color() const {
 			return clr;
 		}
@@ -156,7 +151,6 @@
 	private:
 		friend class Table<void*>;
 		void* entry_;
-		bool use_clr;
 		RGBColor clr;
 		struct Data {
 			const Image* d_picture;
@@ -214,7 +208,7 @@
 	void remove(uint32_t);
 	void remove_entry(const void* const entry);
 
-	EntryRecord& add(void* entry = nullptr, bool select = false);
+	EntryRecord& add(void* entry = nullptr, bool const select_this = false);
 
 	uint32_t size() const {
 		return entry_records_.size();

=== modified file 'src/ui_fsmenu/CMakeLists.txt'
--- src/ui_fsmenu/CMakeLists.txt	2018-02-13 16:52:12 +0000
+++ src/ui_fsmenu/CMakeLists.txt	2018-12-13 23:38:35 +0000
@@ -126,10 +126,18 @@
 
 wl_library(ui_fsmenu_maploading
   SRCS
+    campaigndetails.cc
+    campaigndetails.h
+    campaigns.cc
+    campaigns.h
     campaign_select.cc
     campaign_select.h
     mapselect.cc
     mapselect.h
+    scenariodetails.cc
+    scenariodetails.h
+    scenario_select.cc
+    scenario_select.h
   DEPENDS
     base_exceptions
     base_i18n
@@ -137,13 +145,16 @@
     graphic
     graphic_fonthandler
     graphic_text_constants
+    graphic_surface
     io_filesystem
-    logic_campaign_visibility
     logic_filesystem_constants
     logic_game_controller
     logic_game_settings
+    logic_tribe_basic_info
     map_io_map_loader
     profile
+    scripting_lua_interface
+    scripting_lua_table
     ui_basic
     ui_fsmenu_base
     ui_fsmenu_loading_common

=== modified file 'src/ui_fsmenu/campaign_select.cc'
--- src/ui_fsmenu/campaign_select.cc	2018-04-27 06:11:05 +0000
+++ src/ui_fsmenu/campaign_select.cc	2018-12-13 23:38:35 +0000
@@ -27,67 +27,28 @@
 #include "base/wexception.h"
 #include "graphic/graphic.h"
 #include "graphic/text_constants.h"
-#include "logic/campaign_visibility.h"
-#include "map_io/widelands_map_loader.h"
+#include "logic/filesystem_constants.h"
 #include "profile/profile.h"
-
-/*
- * UI 1 - Selection of Campaign
- *
- */
+#include "scripting/lua_interface.h"
+#include "scripting/lua_table.h"
 
 /**
  * CampaignSelect UI
  * Loads a list of all visible campaigns
  */
-FullscreenMenuCampaignSelect::FullscreenMenuCampaignSelect()
+FullscreenMenuCampaignSelect::FullscreenMenuCampaignSelect(Campaigns* campvis)
    : FullscreenMenuLoadMapOrGame(),
-     table_(this, tablex_, tabley_, tablew_, tableh_, UI::PanelStyle::kFsMenu),
+     table_(this, 0, 0, 0, 0, UI::PanelStyle::kFsMenu),
 
      // Main Title
-     title_(this, get_w() / 2, tabley_ / 3, _("Choose a campaign"), UI::Align::kCenter),
+     title_(this, 0, 0, _("Choose a campaign"), UI::Align::kCenter),
 
      // Campaign description
-     label_campname_(this, right_column_x_, tabley_),
-     ta_campname_(this,
-                  right_column_x_ + indent_,
-                  get_y_from_preceding(label_campname_) + padding_,
-                  get_right_column_w(right_column_x_) - indent_,
-                  label_height_,
-                  UI::PanelStyle::kFsMenu),
-
-     label_tribename_(this, right_column_x_, get_y_from_preceding(ta_campname_) + 2 * padding_),
-     ta_tribename_(this,
-                   right_column_x_ + indent_,
-                   get_y_from_preceding(label_tribename_) + padding_,
-                   get_right_column_w(right_column_x_ + indent_),
-                   label_height_,
-                   UI::PanelStyle::kFsMenu),
-
-     label_difficulty_(this, right_column_x_, get_y_from_preceding(ta_tribename_) + 2 * padding_),
-     ta_difficulty_(this,
-                    right_column_x_ + indent_,
-                    get_y_from_preceding(label_difficulty_) + padding_,
-                    get_right_column_w(right_column_x_ + indent_),
-                    2 * label_height_ - padding_,
-                    UI::PanelStyle::kFsMenu),
-
-     label_description_(this,
-                        right_column_x_,
-                        get_y_from_preceding(ta_difficulty_) + 2 * padding_,
-                        _("Description:")),
-     ta_description_(this,
-                     right_column_x_ + indent_,
-                     get_y_from_preceding(label_description_) + padding_,
-                     get_right_column_w(right_column_x_ + indent_),
-                     buty_ - get_y_from_preceding(label_description_) - 4 * padding_,
-                     UI::PanelStyle::kFsMenu) {
+     campaign_details_(this),
+     campaigns_(campvis) {
 	title_.set_fontsize(UI_FONT_SIZE_BIG);
 	back_.set_tooltip(_("Return to the main menu"));
 	ok_.set_tooltip(_("Play this campaign"));
-	ta_campname_.set_tooltip(_("The name of this campaign"));
-	ta_tribename_.set_tooltip(_("The tribe you will be playing"));
-	ta_difficulty_.set_tooltip(_("The difficulty of this campaign"));
 
 	ok_.sigclicked.connect(
 	   boost::bind(&FullscreenMenuCampaignSelect::clicked_ok, boost::ref(*this)));
@@ -99,7 +60,7 @@
 
 	/** TRANSLATORS: Campaign difficulty table header */
 	table_.add_column(45, _("Diff."), _("Difficulty"));
-	table_.add_column(100, _("Tribe"), _("Tribe Name"));
+	table_.add_column(130, _("Tribe"), _("Tribe Name"));
 	table_.add_column(
 	   0, _("Campaign Name"), _("Campaign Name"), UI::Align::kLeft, UI::TableColumnType::kFlexible);
 	table_.set_column_compare(
@@ -107,10 +68,19 @@
 	table_.set_sort_column(0);
 	table_.focus();
 	fill_table();
+	layout();
 }
 
 void FullscreenMenuCampaignSelect::layout() {
-	// TODO(GunChleoc): Implement when we have box layout for the details.
+	FullscreenMenuLoadMapOrGame::layout();
+	title_.set_pos(Vector2i(0, tabley_ / 3));
+	title_.set_size(get_w(), title_.get_h());
+	table_.set_size(tablew_, tableh_);
+	table_.set_pos(Vector2i(tablex_, tabley_));
+	campaign_details_.set_size(get_right_column_w(right_column_x_), tableh_ - buth_ - 4 * padding_);
+	campaign_details_.set_desired_size(
+	   get_right_column_w(right_column_x_), tableh_ - buth_ - 4 * padding_);
+	campaign_details_.set_pos(Vector2i(right_column_x_, tabley_));
 }
 
 /**
@@ -120,385 +90,62 @@
 	if (!table_.has_selection()) {
 		return;
 	}
-	get_campaign();
+	const CampaignData& campaign_data = *campaigns_->get_campaign(table_.get_selected());
+	if (!campaign_data.visible) {
+		return;
+	}
 	end_modal<FullscreenMenuBase::MenuTarget>(FullscreenMenuBase::MenuTarget::kOk);
 }
 
-int32_t FullscreenMenuCampaignSelect::get_campaign() {
-	return campaign;
+size_t FullscreenMenuCampaignSelect::get_campaign_index() const {
+	return table_.get_selected();
 }
 
-/// Pictorial descriptions of difficulty levels.
-static char const* const difficulty_picture_filenames[] = {
-   "images/novalue.png", "images/ui_fsmenu/easy.png", "images/ui_fsmenu/challenging.png",
-   "images/ui_fsmenu/hard.png"};
-
 bool FullscreenMenuCampaignSelect::set_has_selection() {
-	bool has_selection = table_.has_selection();
+	const bool has_selection = table_.has_selection();
 	ok_.set_enabled(has_selection);
-
-	if (!has_selection) {
-		label_campname_.set_text(std::string());
-		label_tribename_.set_text(std::string());
-		label_difficulty_.set_text(std::string());
-		label_description_.set_text(std::string());
-
-		ta_campname_.set_text(std::string());
-		ta_tribename_.set_text(std::string());
-		ta_difficulty_.set_text(std::string());
-		ta_description_.set_text(std::string());
-
-	} else {
-		label_campname_.set_text(_("Campaign Name:"));
-		label_tribename_.set_text(_("Tribe:"));
-		label_difficulty_.set_text(_("Difficulty:"));
-		label_description_.set_text(_("Description:"));
-	}
 	return has_selection;
 }
 
 void FullscreenMenuCampaignSelect::entry_selected() {
 	if (set_has_selection()) {
-		const CampaignListData& campaign_data = campaigns_data_[table_.get_selected()];
-		campaign = campaign_data.index;
-
-		ta_campname_.set_text(campaign_data.name);
-		ta_tribename_.set_text(campaign_data.tribename);
-		ta_difficulty_.set_text(campaign_data.difficulty_description);
-		ta_description_.set_text(campaign_data.description);
+		const CampaignData& campaign_data = *campaigns_->get_campaign(table_.get_selected());
+		ok_.set_enabled(campaign_data.visible);
+		campaign_details_.update(campaign_data);
 	}
-	ta_description_.scroll_to_top();
 }
 
 /**
  * fill the campaign list
  */
 void FullscreenMenuCampaignSelect::fill_table() {
-	campaigns_data_.clear();
 	table_.clear();
 
-	// Read in the campaign config
-	Profile prof("campaigns/campaigns.conf", nullptr, "maps");
-	Section& s = prof.get_safe_section("global");
-
-	// Read in campvis-file
-	CampaignVisibilitySave cvs;
-	Profile campvis(cvs.get_path().c_str());
-	Section& c = campvis.get_safe_section("campaigns");
-
-	// Predefine variables, used in while-loop
-	uint32_t i = 0;
-	std::string csection = (boost::format("campsect%u") % i).str();
-	std::string cname;
-	std::string ctribename;
-	std::string cdifficulty;
-	std::string cdiff_descr;
-	std::string cdescription;
-
-	while (s.get_string(csection.c_str())) {
-
-		cname = (boost::format("campname%u") % i).str();
-		ctribename = (boost::format("camptribe%u") % i).str();
-		cdifficulty = (boost::format("campdiff%u") % i).str();
-		cdiff_descr = (boost::format("campdiffdescr%u") % i).str();
-		cdescription = (boost::format("campdesc%u") % i).str();
-
-		// Only list visible campaigns
-		if (c.get_bool(csection.c_str())) {
-
-			uint32_t difficulty = s.get_int(cdifficulty.c_str());
-			if (sizeof(difficulty_picture_filenames) / sizeof(*difficulty_picture_filenames) <=
-			    difficulty) {
-				difficulty = 0;
-			}
-
-			CampaignListData campaign_data;
-
-			campaign_data.index = i;
-
-			{
-				i18n::Textdomain td("maps");
-				campaign_data.name = _(s.get_string(cname.c_str(), ""));
-				campaign_data.tribename = _(s.get_string(ctribename.c_str(), ""));
-				campaign_data.difficulty = difficulty;
-				campaign_data.difficulty_description = _(s.get_string(cdiff_descr.c_str(), ""));
-				campaign_data.description = _(s.get_string(cdescription.c_str(), ""));
-			}
-
-			campaigns_data_.push_back(campaign_data);
-
-			UI::Table<uintptr_t>::EntryRecord& tableEntry = table_.add(i);
-			tableEntry.set_picture(0, g_gr->images().get(difficulty_picture_filenames[difficulty]));
-			tableEntry.set_string(1, campaign_data.tribename);
-			tableEntry.set_string(2, campaign_data.name);
+	for (size_t i = 0; i < campaigns_->no_of_campaigns(); ++i) {
+		const CampaignData& campaign_data = *campaigns_->get_campaign(i);
+
+		UI::Table<uintptr_t const>::EntryRecord& tableEntry = table_.add(i);
+		tableEntry.set_picture(0, campaign_data.difficulty_image);
+		tableEntry.set_string(1, campaign_data.tribename);
+		tableEntry.set_string(2, campaign_data.descname);
+		if (!campaign_data.visible) {
+			tableEntry.set_color(UI_FONT_CLR_DISABLED);
 		}
-
-		// Increase counter & csection
-		++i;
-		csection = (boost::format("campsect%u") % i).str();
-
-	}  // while (s.get_string(csection.c_str()))
-	table_.sort();
+	}
 
 	if (table_.size()) {
+		table_.sort();
 		table_.select(0);
 	}
 	set_has_selection();
 }
 
 bool FullscreenMenuCampaignSelect::compare_difficulty(uint32_t rowa, uint32_t rowb) {
-	const CampaignListData& r1 = campaigns_data_[table_[rowa]];
-	const CampaignListData& r2 = campaigns_data_[table_[rowb]];
+	const CampaignData& r1 = *campaigns_->get_campaign(table_[rowa]);
+	const CampaignData& r2 = *campaigns_->get_campaign(table_[rowb]);
 
-	if (r1.difficulty < r2.difficulty) {
+	if (r1.difficulty_level < r2.difficulty_level) {
 		return true;
 	}
-	return r1.index < r2.index;
-}
-
-/*
- * UI 2 - Selection of a map
- *
- */
-
-/**
- * CampaignMapSelect UI.
- *
- * Loads a list of all visible maps of selected campaign and let's the user
- * choose one.
- */
-FullscreenMenuCampaignMapSelect::FullscreenMenuCampaignMapSelect(bool is_tutorial)
-   : FullscreenMenuLoadMapOrGame(),
-     table_(this, tablex_, tabley_, tablew_, tableh_, UI::PanelStyle::kFsMenu),
-
-     // Main title
-     title_(this,
-            get_w() / 2,
-            tabley_ / 3,
-            is_tutorial ? _("Choose a tutorial") : _("Choose a scenario"),
-            UI::Align::kCenter),
-     subtitle_(this,
-               get_w() / 6,
-               get_y_from_preceding(title_) + 6 * padding_,
-               get_w() * 2 / 3,
-               4 * label_height_,
-               UI::PanelStyle::kFsMenu,
-               "",
-               UI::Align::kCenter),
-
-     // Map description
-     label_mapname_(this, right_column_x_, tabley_),
-     ta_mapname_(this,
-                 right_column_x_ + indent_,
-                 get_y_from_preceding(label_mapname_) + padding_,
-                 get_right_column_w(right_column_x_ + indent_),
-                 label_height_,
-                 UI::PanelStyle::kFsMenu),
-
-     label_author_(this, right_column_x_, get_y_from_preceding(ta_mapname_) + 2 * padding_),
-     ta_author_(this,
-                right_column_x_ + indent_,
-                get_y_from_preceding(label_author_) + padding_,
-                get_right_column_w(right_column_x_ + indent_),
-                2 * label_height_,
-                UI::PanelStyle::kFsMenu),
-
-     label_description_(this, right_column_x_, get_y_from_preceding(ta_author_) + padding_),
-     ta_description_(this,
-                     right_column_x_ + indent_,
-                     get_y_from_preceding(label_description_) + padding_,
-                     get_right_column_w(right_column_x_ + indent_),
-                     buty_ - get_y_from_preceding(label_description_) - 4 * padding_,
-                     UI::PanelStyle::kFsMenu),
-
-     is_tutorial_(is_tutorial) {
-	title_.set_fontsize(UI_FONT_SIZE_BIG);
-	back_.set_tooltip(_("Return to the main menu"));
-	if (is_tutorial_) {
-		ok_.set_tooltip(_("Play this tutorial"));
-		ta_mapname_.set_tooltip(_("The name of this tutorial"));
-		ta_description_.set_tooltip(_("What you will learn in this tutorial"));
-	} else {
-		ok_.set_tooltip(_("Play this scenario"));
-		ta_mapname_.set_tooltip(_("The name of this scenario"));
-	}
-
-	ok_.sigclicked.connect(
-	   boost::bind(&FullscreenMenuCampaignMapSelect::clicked_ok, boost::ref(*this)));
-	back_.sigclicked.connect(
-	   boost::bind(&FullscreenMenuCampaignMapSelect::clicked_back, boost::ref(*this)));
-	table_.selected.connect(boost::bind(&FullscreenMenuCampaignMapSelect::entry_selected, this));
-	table_.double_clicked.connect(
-	   boost::bind(&FullscreenMenuCampaignMapSelect::clicked_ok, boost::ref(*this)));
-
-	std::string number_tooltip;
-	std::string name_tooltip;
-	if (is_tutorial_) {
-		number_tooltip = _("The order in which the tutorials should be played");
-		name_tooltip = _("Tutorial Name");
-	} else {
-		number_tooltip = _("The number of this scenario in the campaign");
-		name_tooltip = _("Scenario Name");
-	}
-
-	/** TRANSLATORS: Campaign scenario number table header */
-	table_.add_column(35, _("#"), number_tooltip);
-	table_.add_column(
-	   0, name_tooltip, name_tooltip, UI::Align::kLeft, UI::TableColumnType::kFlexible);
-	table_.set_sort_column(0);
-
-	table_.focus();
-}
-
-void FullscreenMenuCampaignMapSelect::layout() {
-	// TODO(GunChleoc): Implement when we have box layout for the details.
-	table_.layout();
-}
-
-std::string FullscreenMenuCampaignMapSelect::get_map() {
-	return campmapfile;
-}
-
-// Telling this class what campaign we have and since we know what campaign we have, fill it.
-void FullscreenMenuCampaignMapSelect::set_campaign(uint32_t const i) {
-	campaign = i;
-	fill_table();
-}
-
-bool FullscreenMenuCampaignMapSelect::set_has_selection() {
-	bool has_selection = table_.has_selection();
-	ok_.set_enabled(has_selection);
-
-	if (!has_selection) {
-		label_mapname_.set_text(std::string());
-		label_author_.set_text(std::string());
-		label_description_.set_text(std::string());
-
-		ta_mapname_.set_text(std::string());
-		ta_author_.set_text(std::string());
-		ta_description_.set_text(std::string());
-
-	} else {
-		is_tutorial_ ? label_mapname_.set_text(_("Tutorial:")) :
-		               label_mapname_.set_text(_("Scenario:"));
-		label_description_.set_text(_("Description:"));
-	}
-	return has_selection;
-}
-
-void FullscreenMenuCampaignMapSelect::entry_selected() {
-	if (set_has_selection()) {
-		const CampaignScenarioData& scenario_data = scenarios_data_[table_.get_selected()];
-		campmapfile = scenario_data.path;
-		Widelands::Map map;
-
-		std::unique_ptr<Widelands::MapLoader> ml(map.get_correct_loader(campmapfile));
-		if (!ml) {
-			throw wexception(_("Invalid path to file in campaigns.conf: %s"), campmapfile.c_str());
-		}
-
-		map.set_filename(campmapfile);
-		ml->preload_map(true);
-
-		// Localizing this, because some author fields now have "edited by" text.
-		MapAuthorData authors(_(map.get_author()));
-
-		ta_author_.set_text(authors.get_names());
-		if (is_tutorial_) {
-			ta_author_.set_tooltip(ngettext("The designer of this tutorial",
-			                                "The designers of this tutorial", authors.get_number()));
-		} else {
-			ta_author_.set_tooltip(ngettext("The designer of this scenario",
-			                                "The designers of this scenario", authors.get_number()));
-		}
-		label_author_.set_text(ngettext("Author:", "Authors:", authors.get_number()));
-
-		{
-			i18n::Textdomain td("maps");
-			ta_mapname_.set_text(_(map.get_name()));
-			ta_description_.set_text(_(map.get_description()));
-		}
-		ta_description_.scroll_to_top();
-
-		// The dummy scenario can't be played, so we disable the OK button.
-		if (campmapfile == "campaigns/dummy.wmf") {
-			ok_.set_enabled(false);
-		}
-	}
-}
-
-/**
- * fill the campaign-map list
- */
-void FullscreenMenuCampaignMapSelect::fill_table() {
-	// read in the campaign config
-	std::unique_ptr<Profile> prof;
-	std::string campsection;
-	if (is_tutorial_) {
-		prof.reset(new Profile("campaigns/tutorials.conf", nullptr, "maps"));
-
-		// Set subtitle of the page
-		const std::string subtitle1 = _("Pick a tutorial from the list, then hit \"OK\".");
-		const std::string subtitle2 =
-		   _("You can see a description of the currently selected tutorial on the right.");
-		subtitle_.set_text((boost::format("%s\n%s") % subtitle1 % subtitle2).str());
-
-		// Get section of campaign-maps
-		campsection = "tutorials";
-
-	} else {
-		prof.reset(new Profile("campaigns/campaigns.conf", nullptr, "maps"));
-
-		Section& global_s = prof->get_safe_section("global");
-
-		// Set subtitle of the page
-		const char* campaign_tribe =
-		   _(global_s.get_string((boost::format("camptribe%u") % campaign).str().c_str()));
-		const char* campaign_name;
-		{
-			i18n::Textdomain td("maps");
-			campaign_name =
-			   _(global_s.get_string((boost::format("campname%u") % campaign).str().c_str()));
-		}
-		subtitle_.set_text((boost::format("%s — %s") % campaign_tribe % campaign_name).str());
-
-		// Get section of campaign-maps
-		campsection = global_s.get_string((boost::format("campsect%u") % campaign).str().c_str());
-	}
-
-	// Create the entry we use to load the section of the map
-	uint32_t i = 0;
-	std::string mapsection = campsection + (boost::format("%02i") % i).str();
-
-	// Read in campvis-file
-	CampaignVisibilitySave cvs;
-	Profile campvis(cvs.get_path().c_str());
-	Section& c = campvis.get_safe_section("campmaps");
-
-	// Add all visible entries to the list.
-	while (Section* const s = prof->get_section(mapsection)) {
-		if (is_tutorial_ || c.get_bool(mapsection.c_str())) {
-
-			CampaignScenarioData scenario_data;
-			scenario_data.index = i + 1;
-			scenario_data.name = s->get_string("name", "");
-			scenario_data.path = s->get_string("path");
-			scenarios_data_.push_back(scenario_data);
-
-			UI::Table<uintptr_t>::EntryRecord& tableEntry = table_.add(i);
-			tableEntry.set_string(0, (boost::format("%u") % scenario_data.index).str());
-			tableEntry.set_picture(
-			   1, g_gr->images().get("images/ui_basic/ls_wlmap.png"), scenario_data.name);
-		}
-
-		// Increase counter & mapsection
-		++i;
-		mapsection = campsection + (boost::format("%02i") % i).str();
-	}
-	table_.sort();
-
-	if (table_.size()) {
-		table_.select(0);
-	}
-	set_has_selection();
+	return table_[rowa] < table_[rowb];
 }

=== modified file 'src/ui_fsmenu/campaign_select.h'
--- src/ui_fsmenu/campaign_select.h	2018-04-07 16:59:00 +0000
+++ src/ui_fsmenu/campaign_select.h	2018-12-13 23:38:35 +0000
@@ -20,26 +20,22 @@
 #ifndef WL_UI_FSMENU_CAMPAIGN_SELECT_H
 #define WL_UI_FSMENU_CAMPAIGN_SELECT_H
 
-#include "ui_basic/button.h"
-#include "ui_basic/multilinetextarea.h"
+#include <vector>
+
 #include "ui_basic/table.h"
 #include "ui_basic/textarea.h"
-#include "ui_fsmenu/base.h"
+#include "ui_fsmenu/campaigndetails.h"
+#include "ui_fsmenu/campaigns.h"
 #include "ui_fsmenu/load_map_or_game.h"
 
 /*
- * Fullscreen Menu for all Campaigns
- */
-
-/*
- * UI 1 - Selection of Campaign
- *
+ * Fullscreen Menu for selecting a campaign
  */
 class FullscreenMenuCampaignSelect : public FullscreenMenuLoadMapOrGame {
 public:
-	FullscreenMenuCampaignSelect();
+	FullscreenMenuCampaignSelect(Campaigns* campvis);
 
-	int32_t get_campaign();
+	size_t get_campaign_index() const;
 
 protected:
 	void clicked_ok() override;
@@ -52,90 +48,14 @@
 	/// Updates buttons and text labels and returns whether a table entry is selected.
 	bool set_has_selection();
 
-	/**
-	 * Data about a campaign that we're interested in.
-	 */
-	struct CampaignListData {
-		uint32_t index;
-		std::string name;
-		std::string tribename;
-		uint32_t difficulty;
-		std::string difficulty_description;
-		std::string description;
-
-		CampaignListData() : index(0), difficulty(0) {
-		}
-	};
-
 	bool compare_difficulty(uint32_t, uint32_t);
 
 	UI::Table<uintptr_t const> table_;
 
 	UI::Textarea title_;
-	UI::Textarea label_campname_;
-	UI::MultilineTextarea ta_campname_;
-	UI::Textarea label_tribename_;
-	UI::MultilineTextarea ta_tribename_;
-	UI::Textarea label_difficulty_;
-	UI::MultilineTextarea ta_difficulty_;
-	UI::Textarea label_description_;
-	UI::MultilineTextarea ta_description_;
-
-	std::vector<CampaignListData> campaigns_data_;
-
-	/// Variables used for exchange between the two Campaign UIs and
-	/// Game::run_campaign
-	int32_t campaign;
-};
-/*
- * UI 2 - Selection of a map
- *
- */
-class FullscreenMenuCampaignMapSelect : public FullscreenMenuLoadMapOrGame {
-public:
-	explicit FullscreenMenuCampaignMapSelect(bool is_tutorial = false);
-
-	std::string get_map();
-	void set_campaign(uint32_t);
-
-protected:
-	void entry_selected() override;
-	void fill_table() override;
-
-private:
-	void layout() override;
-
-	/// Updates buttons and text labels and returns whether a table entry is selected.
-	bool set_has_selection();
-	/**
-	 * Data about a campaign scenario that we're interested in.
-	 */
-	struct CampaignScenarioData {
-		uint32_t index;
-		std::string name;
-		std::string path;
-
-		CampaignScenarioData() : index(0) {
-		}
-	};
-
-	UI::Table<uintptr_t const> table_;
-
-	UI::Textarea title_;
-	UI::MultilineTextarea subtitle_;
-	UI::Textarea label_mapname_;
-	UI::MultilineTextarea ta_mapname_;
-	UI::Textarea label_author_;
-	UI::MultilineTextarea ta_author_;
-	UI::Textarea label_description_;
-	UI::MultilineTextarea ta_description_;
-
-	uint32_t campaign;
-	std::string campmapfile;
-
-	std::vector<CampaignScenarioData> scenarios_data_;
-
-	bool is_tutorial_;
+	CampaignDetails campaign_details_;
+
+	Campaigns* campaigns_;
 };
 
 #endif  // end of include guard: WL_UI_FSMENU_CAMPAIGN_SELECT_H

=== added file 'src/ui_fsmenu/campaigndetails.cc'
--- src/ui_fsmenu/campaigndetails.cc	1970-01-01 00:00:00 +0000
+++ src/ui_fsmenu/campaigndetails.cc	2018-12-13 23:38:35 +0000
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2017 by the Widelands Development Team
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+#include "ui_fsmenu/campaigndetails.h"
+
+#include <boost/format.hpp>
+
+#include "base/i18n.h"
+#include "graphic/text_constants.h"
+#include "ui_basic/scrollbar.h"
+
+CampaignDetails::CampaignDetails(Panel* parent)
+   : UI::Box(parent, 0, 0, UI::Box::Vertical),
+     name_label_(this,
+                 0,
+                 0,
+                 UI::Scrollbar::kSize,
+                 0,
+				 UI::PanelStyle::kFsMenu,
+                 "",
+                 UI::Align::kLeft,
+                 UI::MultilineTextarea::ScrollMode::kNoScrolling),
+     descr_(this, 0, 0, UI::Scrollbar::kSize, 0, UI::PanelStyle::kFsMenu) {
+
+	constexpr int kPadding = 4;
+	add(&name_label_, UI::Box::Resizing::kFullSize);
+	add_space(kPadding);
+	add(&descr_, UI::Box::Resizing::kExpandBoth);
+}
+
+void CampaignDetails::update(const CampaignData& campaigndata) {
+	name_label_.set_text((boost::format("<rt>%s%s</rt>") %
+	                      /** TRANSLATORS: Header for campaign name */
+	                      as_heading(_("Campaign"), UI::PanelStyle::kFsMenu, true) %
+	                      as_content(campaigndata.descname, UI::PanelStyle::kFsMenu))
+	                        .str());
+
+	std::string description = "";
+
+	if (campaigndata.visible) {
+		/** TRANSLATORS: Header for campaign tribe */
+		description = (boost::format("%s%s") % as_heading(_("Tribe"), UI::PanelStyle::kFsMenu) %
+		               as_content(campaigndata.tribename, UI::PanelStyle::kFsMenu))
+		                 .str();
+		description =
+		   /** TRANSLATORS: Header for campaign difficulty */
+		   (boost::format("%s%s") % description % as_heading(_("Difficulty"), UI::PanelStyle::kFsMenu)).str();
+		description = (boost::format("%s%s") % description %
+		               as_content(campaigndata.difficulty_description, UI::PanelStyle::kFsMenu))
+		                 .str();
+
+		description =
+		   /** TRANSLATORS: Header for campaign description */
+		   (boost::format("%s%s") % description % as_heading(_("Description"), UI::PanelStyle::kFsMenu))
+		      .str();
+		description = (boost::format("%s%s") % description %
+		               as_content(campaigndata.description, UI::PanelStyle::kFsMenu))
+		                 .str();
+	}
+	description =
+	   (boost::format("%s%s") % description % as_content(campaigndata.description, UI::PanelStyle::kFsMenu))
+	      .str();
+
+	description = (boost::format("<rt>%s</rt>") % description).str();
+	descr_.set_text(description);
+	descr_.scroll_to_top();
+}

=== added file 'src/ui_fsmenu/campaigndetails.h'
--- src/ui_fsmenu/campaigndetails.h	1970-01-01 00:00:00 +0000
+++ src/ui_fsmenu/campaigndetails.h	2018-12-13 23:38:35 +0000
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2017 by the Widelands Development Team
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ *
+ */
+
+#ifndef WL_UI_FSMENU_CAMPAIGNDETAILS_H
+#define WL_UI_FSMENU_CAMPAIGNDETAILS_H
+
+#include "ui_basic/box.h"
+#include "ui_basic/multilinetextarea.h"
+#include "ui_fsmenu/campaigns.h"
+
+/**
+ * Show a Box with information about a campaign.
+ */
+class CampaignDetails : public UI::Box {
+public:
+	explicit CampaignDetails(Panel* parent);
+
+	void update(const CampaignData& campaigndata);
+
+private:
+	UI::MultilineTextarea name_label_;
+	UI::MultilineTextarea descr_;
+};
+
+#endif  // end of include guard: WL_UI_FSMENU_CAMPAIGNDETAILS_H

=== renamed file 'src/logic/campaign_visibility.cc' => 'src/ui_fsmenu/campaigns.cc'
--- src/logic/campaign_visibility.cc	2018-04-28 09:51:16 +0000
+++ src/ui_fsmenu/campaigns.cc	2018-12-13 23:38:35 +0000
@@ -17,166 +17,206 @@
  *
  */
 
-#include "logic/campaign_visibility.h"
-
-#include <cstdio>
-#include <cstdlib>
-
-#include <sys/stat.h>
-
-#include "base/wexception.h"
+#include "ui_fsmenu/campaigns.h"
+
+#include <map>
+#include <memory>
+
+#include "base/log.h"
+#include "graphic/graphic.h"
 #include "io/filesystem/filesystem.h"
 #include "logic/filesystem_constants.h"
+#include "logic/map_objects/tribes/tribe_basic_info.h"
 #include "profile/profile.h"
-
-/**
- * Get the path of campaign visibility save-file
- */
-std::string CampaignVisibilitySave::get_path() {
-	g_fs->ensure_directory_exists(kSaveDir);  // Make sure save directory exists
-
-	// check if campaigns visibility-save is available
+#include "scripting/lua_interface.h"
+
+namespace {
+const std::string kCampVisFileLegacy = "save/campvis";
+}
+
+Campaigns::Campaigns() {
+	// Load solved scenarios
+	std::unique_ptr<Profile> campvis;
 	if (!(g_fs->file_exists(kCampVisFile))) {
-		make_campvis(kCampVisFile);
-	}
-
-	// check if campaigns visibility-save is up to date
-	Profile ca(kCampVisFile.c_str());
-
-	//  1st version of campvis had no global section
-	if (!ca.get_section("global"))
-		update_campvis(kCampVisFile);
-	else {
-		Section& ca_s = ca.get_safe_section("global");
-		Profile cc("campaigns/campaigns.conf");
-		Section& cc_s = cc.get_safe_section("global");
-		if (cc_s.get_int("version") > ca_s.get_int("version"))
-			update_campvis(kCampVisFile);
-	}
-
-	return kCampVisFile;
-}
-
-/**
- * Create the campaign visibility save-file of the user
- */
-void CampaignVisibilitySave::make_campvis(const std::string& savepath) {
-	// Only prepare campvis-file -> data will be written via update_campvis
-	Profile campvis(savepath.c_str());
-	campvis.pull_section("global");
-	campvis.pull_section("campaigns");
-	campvis.pull_section("campmaps");
-	campvis.write(savepath.c_str(), true);
-
-	update_campvis(savepath);
-}
-
-/**
- * Update the campaign visibility save-file of the user
- */
-void CampaignVisibilitySave::update_campvis(const std::string& savepath) {
-	// Variable declaration
-	int32_t i = 0;
-	int32_t imap = 0;
-	char csection[24];
-	char number[12];
-	std::string mapsection;
-	std::string cms;
-
-	// Prepare campaigns.conf and campvis
-	Profile cconfig("campaigns/campaigns.conf");
-	Section& cconf_s = cconfig.get_safe_section("global");
-	Profile campvisr(savepath.c_str());
-	Profile campvisw(savepath.c_str());
-
-	// Write down global section
-	campvisw.pull_section("global").set_int("version", cconf_s.get_int("version", 1));
-
-	// Write down visibility of campaigns
-	Section& campv_c = campvisr.get_safe_section("campaigns");
-	Section& campv_m = campvisr.get_safe_section("campmaps");
-	{
-		Section& vis = campvisw.pull_section("campaigns");
-		sprintf(csection, "campsect%i", i);
-		char cvisible[24];
-		char cnewvisi[24];
-		while (cconf_s.get_string(csection)) {
-			sprintf(cvisible, "campvisi%i", i);
-			sprintf(cnewvisi, "cnewvisi%i", i);
-			bool visible = cconf_s.get_bool(cvisible) || campv_c.get_bool(csection);
-			if (!visible) {
-				const char* newvisi = cconf_s.get_string(cnewvisi, "");
-				if (sizeof(newvisi) > 1) {
-					visible = campv_m.get_bool(newvisi, false) || campv_c.get_bool(newvisi, false);
-				}
-			}
-			vis.set_bool(csection, visible);
-			++i;
-			sprintf(csection, "campsect%i", i);
-		}
-	}
-
-	// Write down visibility of campaign maps
-	Section& vis = campvisw.pull_section("campmaps");
-	i = 0;
-
-	sprintf(csection, "campsect%i", i);
-	while (cconf_s.get_string(csection)) {
-		mapsection = cconf_s.get_string(csection);
-
-		cms = mapsection;
-		sprintf(number, "%02i", imap);
-		cms += number;
-
-		while (Section* const s = cconfig.get_section(cms.c_str())) {
-			bool visible = s->get_bool("visible") || campv_m.get_bool(cms.c_str());
-			if (!visible) {
-				const char* newvisi = s->get_string("newvisi", "");
-				if (sizeof(newvisi) > 1) {
-					visible = campv_m.get_bool(newvisi, false) || campv_c.get_bool(newvisi, false);
-				}
-			}
-			vis.set_bool(cms.c_str(), visible);
-
-			++imap;
-			cms = mapsection;
-			sprintf(number, "%02i", imap);
-			cms += number;
-		}
-
-		++i;
-		sprintf(csection, "campsect%i", i);
-		imap = 0;
-	}
-	campvisw.write(savepath.c_str(), true);
-}
-
-/**
- * Set an campaign entry in campvis visible or invisible.
- * If it doesn't exist, create it.
- * \param entry entry to be changed
- * \param visible should the map be visible?
- */
-void CampaignVisibilitySave::set_campaign_visibility(const std::string& entry, bool visible) {
-	std::string savepath = get_path();
-	Profile campvis(savepath.c_str());
-
-	campvis.pull_section("campaigns").set_bool(entry.c_str(), visible);
-
-	campvis.write(savepath.c_str(), false);
-}
-
-/**
- * Set an campaignmap entry in campvis visible or invisible.
- * If it doesn't exist, create it.
- * \param entry entry to be changed
- * \param visible should the map be visible?
- */
-void CampaignVisibilitySave::set_map_visibility(const std::string& entry, bool visible) {
-	std::string savepath = get_path();
-	Profile campvis(savepath.c_str());
-
-	campvis.pull_section("campmaps").set_bool(entry.c_str(), visible);
-
-	campvis.write(savepath.c_str(), false);
+		// There is no campaigns.conf file - create one.
+		campvis.reset(new Profile(kCampVisFile.c_str()));
+		campvis->pull_section("scenarios");
+		campvis->write(kCampVisFile.c_str(), true);
+		if (g_fs->file_exists(kCampVisFileLegacy)) {
+			update_legacy_campvis();
+		}
+	}
+	campvis.reset(new Profile(kCampVisFile.c_str()));
+	Section& campvis_scenarios = campvis->get_safe_section("scenarios");
+
+	// Now load the campaign info
+	LuaInterface lua;
+	std::unique_ptr<LuaTable> table(lua.run_script("campaigns/campaigns.lua"));
+
+	// Read difficulty images
+	std::unique_ptr<LuaTable> difficulties_table(table->get_table("difficulties"));
+	std::vector<std::pair<const std::string, const Image*>> difficulty_levels;
+	for (const auto& difficulty_level_table :
+	     difficulties_table->array_entries<std::unique_ptr<LuaTable>>()) {
+		difficulty_levels.push_back(
+		   std::make_pair(_(difficulty_level_table->get_string("descname")),
+		                  g_gr->images().get(difficulty_level_table->get_string("image"))));
+	}
+
+	// Read the campaigns themselves
+	std::unique_ptr<LuaTable> campaigns_table(table->get_table("campaigns"));
+	i18n::Textdomain td("maps");
+
+	for (const auto& campaign_table : campaigns_table->array_entries<std::unique_ptr<LuaTable>>()) {
+		CampaignData* campaign_data = new CampaignData();
+		campaign_data->descname = _(campaign_table->get_string("descname"));
+		campaign_data->tribename =
+		   Widelands::get_tribeinfo(campaign_table->get_string("tribe")).descname;
+		campaign_data->description = _(campaign_table->get_string("description"));
+		if (campaign_table->has_key("prerequisites")) {
+			for (const std::string& prerequisite :
+				 campaign_table->get_table("prerequisites")->array_entries<std::string>()) {
+				campaign_data->prerequisites.insert(prerequisite);
+			}
+		}
+
+		campaign_data->visible = false;
+
+		// Collect difficulty information
+		std::unique_ptr<LuaTable> difficulty_table(campaign_table->get_table("difficulty"));
+		campaign_data->difficulty_level = difficulty_table->get_int("level");
+		campaign_data->difficulty_image =
+		   difficulty_levels.at(campaign_data->difficulty_level - 1).second;
+		campaign_data->difficulty_description =
+		   difficulty_levels.at(campaign_data->difficulty_level - 1).first;
+		const std::string difficulty_description = _(difficulty_table->get_string("description"));
+		if (!difficulty_description.empty()) {
+			campaign_data->difficulty_description =
+			   i18n::join_sentences(campaign_data->difficulty_description, difficulty_description);
+		}
+
+		// Scenarios
+		std::unique_ptr<LuaTable> scenarios_table(campaign_table->get_table("scenarios"));
+		for (const std::string& path : scenarios_table->array_entries<std::string>()) {
+			ScenarioData* scenario_data = new ScenarioData();
+			scenario_data->path = path;
+			if (campvis_scenarios.get_bool(scenario_data->path.c_str(), false)) {
+				solved_scenarios_.insert(scenario_data->path);
+			}
+
+			scenario_data->is_tutorial = false;
+			scenario_data->playable = scenario_data->path != "dummy.wmf";
+			scenario_data->visible = false;
+			campaign_data->scenarios.push_back(
+			   std::unique_ptr<ScenarioData>(std::move(scenario_data)));
+		}
+
+		campaigns_.push_back(std::unique_ptr<CampaignData>(std::move(campaign_data)));
+	}
+
+	// Finally, calculate the visibility
+	update_visibility_info();
+}
+
+void Campaigns::update_visibility_info() {
+	for (auto& campaign : campaigns_) {
+		if (campaign->prerequisites.empty()) {
+			// A campaign is visible if it has no prerequisites
+			campaign->visible = true;
+		} else {
+			// A campaign is visible if one of its prerequisites has been fulfilled
+			for (const std::string prerequisite : campaign->prerequisites) {
+				if (solved_scenarios_.count(prerequisite) == 1) {
+					campaign->visible = true;
+					break;
+				}
+			}
+		}
+		if (!campaign->visible) {
+			// A campaign is also visible if one of its scenarios has been solved
+			for (size_t i = 0; i < campaign->scenarios.size(); ++i) {
+				auto& scenario = campaign->scenarios.at(i);
+				if (solved_scenarios_.count(scenario->path) == 1) {
+					campaign->visible = true;
+					break;
+				}
+			}
+		}
+		// Now set scenario visibility
+		if (campaign->visible) {
+			for (size_t i = 0; i < campaign->scenarios.size(); ++i) {
+				auto& scenario = campaign->scenarios.at(i);
+				if (i == 0) {
+					// The first scenario in a visible campaign is always visible
+					scenario->visible = true;
+				} else {
+					// A scenario is visible if its predecessor was solved
+					scenario->visible =
+					   solved_scenarios_.count(campaign->scenarios.at(i - 1)->path) == 1;
+				}
+				if (!scenario->visible) {
+					// If a scenario is invisible, subsequent scenarios are also invisible
+					break;
+				}
+			}
+		}
+	}
+}
+
+/**
+ * Handle legacy campvis file
+ */
+// TODO(GunChleoc): Remove after Build 22
+void Campaigns::update_legacy_campvis() {
+	Profile legacy_campvis(kCampVisFileLegacy.c_str());
+	if (legacy_campvis.get_section("campmaps") == nullptr) {
+		return;
+	}
+
+	log("Converting legacy campvis\n");
+
+	using LegacyList = std::vector<std::pair<std::string, std::string>>;
+
+	std::vector<LegacyList> legacy_scenarios;
+
+	legacy_scenarios.push_back(
+	   {{"fri02.wmf", "frisians01"}, {"fri01.wmf", "frisians00"}, {"atl01.wmf", "atlanteans00"}});
+
+	legacy_scenarios.push_back(
+	   {{"fri02.wmf", "frisians01"}, {"fri01.wmf", "frisians00"}, {"emp04.wmf", "empiretut03"}});
+
+	legacy_scenarios.push_back({{"atl02.wmf", "atlanteans01"},
+	                            {"atl01.wmf", "atlanteans00"},
+	                            {"emp02.wmf", "empiretut01"},
+	                            {"emp01.wmf", "empiretut00"}});
+
+	legacy_scenarios.push_back({
+	   {"emp04.wmf", "empiretut03"},
+	   {"emp03.wmf", "empiretut02"},
+	   {"emp02.wmf", "empiretut01"},
+	   {"emp01.wmf", "empiretut00"},
+	   {"bar02.wmf", "barbariantut01"},
+	   {"bar01.wmf", "barbariantut00"},
+	});
+
+	Section& campvis_scenarios = legacy_campvis.get_safe_section("campmaps");
+	std::set<std::string> solved_legacy_scenarios;
+	for (const auto& legacy_list : legacy_scenarios) {
+		bool set_solved = false;
+		for (const auto& legacy_scenario : legacy_list) {
+			if (set_solved) {
+				solved_legacy_scenarios.insert(legacy_scenario.first);
+			}
+			set_solved = campvis_scenarios.get_bool(legacy_scenario.second.c_str(), false);
+		}
+	}
+
+	// Now write everything
+	Profile write_campvis(kCampVisFile.c_str());
+	Section& write_scenarios = write_campvis.pull_section("scenarios");
+	for (const auto& scenario : solved_legacy_scenarios) {
+		write_scenarios.set_bool(scenario.c_str(), true);
+	}
+
+	write_campvis.write(kCampVisFile.c_str(), true);
 }

=== renamed file 'src/logic/campaign_visibility.h' => 'src/ui_fsmenu/campaigns.h'
--- src/logic/campaign_visibility.h	2018-04-07 16:59:00 +0000
+++ src/ui_fsmenu/campaigns.h	2018-12-13 23:38:35 +0000
@@ -17,22 +17,68 @@
  *
  */
 
-#ifndef WL_LOGIC_CAMPAIGN_VISIBILITY_H
-#define WL_LOGIC_CAMPAIGN_VISIBILITY_H
+#ifndef WL_UI_FSMENU_CAMPAIGNS_H
+#define WL_UI_FSMENU_CAMPAIGNS_H
 
-#include <cstring>
+#include <memory>
 #include <string>
-
-#include <stdint.h>
-
-struct CampaignVisibilitySave {
-	std::string get_path();
-	void set_campaign_visibility(const std::string&, bool);
-	void set_map_visibility(const std::string&, bool);
+#include <unordered_set>
+#include <vector>
+
+#include "graphic/image.h"
+#include "scripting/lua_table.h"
+#include "wui/mapauthordata.h"
+
+/**
+ * Data about a campaign or tutorial scenario that we're interested in.
+ */
+struct ScenarioData {
+	std::string path;
+	std::string descname;
+	std::string description;
+	MapAuthorData authors;
+	bool is_tutorial;
+	bool playable;
+	bool visible;
+
+	ScenarioData() = default;
+};
+
+/**
+ * Data about a campaign that we're interested in.
+ */
+struct CampaignData {
+	std::string descname;
+	std::string tribename;
+	uint32_t difficulty_level;
+	const Image* difficulty_image;
+	std::string difficulty_description;
+	std::string description;
+	std::set<std::string> prerequisites;
+	bool visible;
+	std::vector<std::unique_ptr<ScenarioData>> scenarios;
+
+	CampaignData() = default;
+};
+
+struct Campaigns {
+	Campaigns();
+
+	size_t no_of_campaigns() const {
+		return campaigns_.size();
+	}
+
+	CampaignData* get_campaign(size_t campaign_index) const {
+		assert(campaign_index < campaigns_.size());
+		return campaigns_.at(campaign_index).get();
+	}
 
 private:
-	void make_campvis(const std::string&);
-	void update_campvis(const std::string&);
+	void update_visibility_info();
+	static void update_legacy_campvis();
+
+	std::vector<std::unique_ptr<CampaignData>> campaigns_;
+	std::unordered_set<std::string> solved_scenarios_;
 };
 
-#endif  // end of include guard: WL_LOGIC_CAMPAIGN_VISIBILITY_H
+#endif  // end of include guard: WL_UI_FSMENU_CAMPAIGNS_H

=== added file 'src/ui_fsmenu/scenario_select.cc'
--- src/ui_fsmenu/scenario_select.cc	1970-01-01 00:00:00 +0000
+++ src/ui_fsmenu/scenario_select.cc	2018-12-13 23:38:35 +0000
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2002-2017 by the Widelands Development Team
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ *
+ */
+
+#include "ui_fsmenu/scenario_select.h"
+
+#include <memory>
+
+#include <boost/format.hpp>
+
+#include "base/i18n.h"
+#include "base/wexception.h"
+#include "graphic/graphic.h"
+#include "graphic/text_constants.h"
+#include "logic/filesystem_constants.h"
+#include "map_io/widelands_map_loader.h"
+#include "profile/profile.h"
+#include "scripting/lua_interface.h"
+#include "scripting/lua_table.h"
+#include "ui_basic/scrollbar.h"
+#include "ui_fsmenu/campaigns.h"
+
+/**
+ * FullscreenMenuScenarioSelect UI.
+ *
+ * Loads a list of all visible maps of selected campaign or all tutorials and
+ * lets the user choose one.
+ */
+FullscreenMenuScenarioSelect::FullscreenMenuScenarioSelect(CampaignData* camp)
+   : FullscreenMenuLoadMapOrGame(),
+     is_tutorial_(camp == nullptr),
+     table_(this, tablex_, tabley_, tablew_, tableh_, UI::PanelStyle::kFsMenu),
+     header_box_(this, 0, 0, UI::Box::Vertical),
+
+     // Main title
+     title_(&header_box_,
+            0,
+            0,
+            is_tutorial_ ? _("Choose a tutorial") : _("Choose a scenario"),
+            UI::Align::kCenter),
+     subtitle_(&header_box_,
+               0,
+               0,
+               UI::Scrollbar::kSize,
+               0,
+			   UI::PanelStyle::kFsMenu,
+               "",
+               UI::Align::kCenter,
+               UI::MultilineTextarea::ScrollMode::kNoScrolling),
+     scenario_details_(this),
+     campaign_(camp) {
+	title_.set_fontsize(UI_FONT_SIZE_BIG);
+
+	// Set subtitle of the page
+	if (campaign_ == nullptr) {
+		const std::string subtitle1 = _("Pick a tutorial from the list, then hit “OK”.");
+		const std::string subtitle2 =
+		   _("You can see a description of the currently selected tutorial on the right.");
+		subtitle_.set_text((boost::format("%s\n%s") % subtitle1 % subtitle2).str());
+	} else {
+		subtitle_.set_text(
+		   (boost::format("%s — %s") % campaign_->tribename % campaign_->descname).str());
+	}
+
+	header_box_.add_inf_space();
+	header_box_.add_inf_space();
+	header_box_.add_inf_space();
+	header_box_.add(&title_, UI::Box::Resizing::kFullSize);
+	header_box_.add_inf_space();
+	header_box_.add(&subtitle_, UI::Box::Resizing::kFullSize);
+	header_box_.add_inf_space();
+	header_box_.add_inf_space();
+	header_box_.add_inf_space();
+
+	back_.set_tooltip(is_tutorial_ ? _("Return to the main menu") :
+	                                 _("Return to campaign selection"));
+	ok_.set_tooltip(is_tutorial_ ? _("Play this tutorial") : _("Play this scenario"));
+
+	ok_.sigclicked.connect(
+	   boost::bind(&FullscreenMenuScenarioSelect::clicked_ok, boost::ref(*this)));
+	back_.sigclicked.connect(
+	   boost::bind(&FullscreenMenuScenarioSelect::clicked_back, boost::ref(*this)));
+	table_.selected.connect(boost::bind(&FullscreenMenuScenarioSelect::entry_selected, this));
+	table_.double_clicked.connect(
+	   boost::bind(&FullscreenMenuScenarioSelect::clicked_ok, boost::ref(*this)));
+
+	std::string number_tooltip;
+	std::string name_tooltip;
+	if (is_tutorial_) {
+		number_tooltip = _("The order in which the tutorials should be played");
+		name_tooltip = _("Tutorial Name");
+	} else {
+		number_tooltip = _("The number of this scenario in the campaign");
+		name_tooltip = _("Scenario Name");
+	}
+
+	/** TRANSLATORS: Campaign scenario number table header */
+	table_.add_column(35, _("#"), number_tooltip);
+	table_.add_column(
+	   0, name_tooltip, name_tooltip, UI::Align::kLeft, UI::TableColumnType::kFlexible);
+	table_.set_sort_column(0);
+	table_.focus();
+	fill_table();
+	layout();
+}
+
+void FullscreenMenuScenarioSelect::layout() {
+	FullscreenMenuLoadMapOrGame::layout();
+	header_box_.set_size(get_w(), tabley_);
+	table_.set_size(tablew_, tableh_);
+	table_.set_pos(Vector2i(tablex_, tabley_));
+	scenario_details_.set_size(get_right_column_w(right_column_x_), tableh_ - buth_ - 4 * padding_);
+	scenario_details_.set_pos(Vector2i(right_column_x_, tabley_));
+}
+
+std::string FullscreenMenuScenarioSelect::get_map() {
+	if (set_has_selection()) {
+		return g_fs->FileSystem::fix_cross_file(kCampaignsDir + "/" +
+		                                        scenarios_data_.at(table_.get_selected()).path);
+	}
+	return "";
+}
+
+bool FullscreenMenuScenarioSelect::set_has_selection() {
+	bool has_selection = table_.has_selection();
+	ok_.set_enabled(has_selection);
+	return has_selection;
+}
+
+void FullscreenMenuScenarioSelect::clicked_ok() {
+	if (!table_.has_selection()) {
+		return;
+	}
+	const ScenarioData& scenario_data = scenarios_data_[table_.get_selected()];
+	if (!scenario_data.playable) {
+		return;
+	}
+	end_modal<FullscreenMenuBase::MenuTarget>(FullscreenMenuBase::MenuTarget::kOk);
+}
+
+void FullscreenMenuScenarioSelect::entry_selected() {
+	if (set_has_selection()) {
+		const ScenarioData& scenario_data = scenarios_data_[table_.get_selected()];
+		scenario_details_.update(scenario_data);
+
+		// The dummy scenario can't be played, so we disable the OK button.
+		ok_.set_enabled(scenario_data.playable);
+	}
+}
+
+/**
+ * fill the campaign-map list
+ */
+void FullscreenMenuScenarioSelect::fill_table() {
+	if (campaign_ == nullptr) {
+		// Load the tutorials
+		LuaInterface lua;
+		std::unique_ptr<LuaTable> table(lua.run_script("campaigns/tutorials.lua"));
+		for (const std::string& path : table->array_entries<std::string>()) {
+			ScenarioData scenario_data;
+			scenario_data.path = path;
+			scenario_data.playable = true;
+			scenario_data.is_tutorial = true;
+			scenarios_data_.push_back(scenario_data);
+		}
+	} else {
+		// Load the current campaign
+		for (auto& scenario_data : campaign_->scenarios) {
+			if (scenario_data->visible) {
+				scenario_data->is_tutorial = false;
+				scenario_data->playable = scenario_data->path != "dummy.wmf";
+				scenarios_data_.push_back(*scenario_data.get());
+			} else {
+				break;
+			}
+		}
+	}
+
+	for (size_t i = 0; i < scenarios_data_.size(); ++i) {
+		// Get details info from maps
+		ScenarioData* scenario_data = &scenarios_data_.at(i);
+		const std::string full_path =
+		   g_fs->FileSystem::fix_cross_file(kCampaignsDir + "/" + scenario_data->path);
+		Widelands::Map map;
+		std::unique_ptr<Widelands::MapLoader> ml(map.get_correct_loader(full_path));
+		if (!ml) {
+			throw wexception(
+			   _("Invalid path to file in campaigns.lua of tutorials.lua: %s"), full_path.c_str());
+		}
+
+		map.set_filename(full_path);
+		ml->preload_map(true);
+
+		{
+			i18n::Textdomain td("maps");
+			scenario_data->authors.set_authors(map.get_author());
+			scenario_data->descname = _(map.get_name());
+			scenario_data->description = _(map.get_description());
+		}
+
+		// Now add to table
+		UI::Table<uintptr_t>::EntryRecord& te = table_.add(i);
+		te.set_string(0, (boost::format("%d") % (i + 1)).str());
+		te.set_picture(
+		   1, g_gr->images().get("images/ui_basic/ls_wlmap.png"), scenario_data->descname);
+		if (!scenario_data->playable) {
+			te.set_color(UI_FONT_CLR_DISABLED);
+		}
+	}
+
+	if (!table_.empty()) {
+		table_.select(0);
+	}
+	entry_selected();
+}

=== added file 'src/ui_fsmenu/scenario_select.h'
--- src/ui_fsmenu/scenario_select.h	1970-01-01 00:00:00 +0000
+++ src/ui_fsmenu/scenario_select.h	2018-12-13 23:38:35 +0000
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2002-2017 by the Widelands Development Team
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ *
+ */
+
+#ifndef WL_UI_FSMENU_SCENARIO_SELECT_H
+#define WL_UI_FSMENU_SCENARIO_SELECT_H
+
+#include "ui_basic/box.h"
+#include "ui_basic/multilinetextarea.h"
+#include "ui_basic/table.h"
+#include "ui_basic/textarea.h"
+#include "ui_fsmenu/load_map_or_game.h"
+#include "ui_fsmenu/scenariodetails.h"
+
+/*
+ * Fullscreen Menu for selecting a campaign or tutorial scenario
+ */
+class FullscreenMenuScenarioSelect : public FullscreenMenuLoadMapOrGame {
+public:
+	// If camp is not set, we'll be loading the tutorials
+	explicit FullscreenMenuScenarioSelect(CampaignData* camp = nullptr);
+
+	std::string get_map();
+
+protected:
+	void clicked_ok() override;
+	void entry_selected() override;
+	void fill_table() override;
+
+private:
+	void layout() override;
+
+	/// Updates buttons and text labels and returns whether a table entry is selected.
+	bool set_has_selection();
+
+	bool is_tutorial_;
+	UI::Table<uintptr_t const> table_;
+
+	UI::Box header_box_;
+
+	UI::Textarea title_;
+	UI::MultilineTextarea subtitle_;
+	ScenarioDetails scenario_details_;
+
+	CampaignData* campaign_;
+
+	std::vector<ScenarioData> scenarios_data_;
+};
+
+#endif  // end of include guard: WL_UI_FSMENU_SCENARIO_SELECT_H

=== added file 'src/ui_fsmenu/scenariodetails.cc'
--- src/ui_fsmenu/scenariodetails.cc	1970-01-01 00:00:00 +0000
+++ src/ui_fsmenu/scenariodetails.cc	2018-12-13 23:38:35 +0000
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2017 by the Widelands Development Team
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+#include "ui_fsmenu/scenariodetails.h"
+
+#include <boost/format.hpp>
+
+#include "base/i18n.h"
+#include "graphic/text_constants.h"
+#include "ui_basic/scrollbar.h"
+
+ScenarioDetails::ScenarioDetails(Panel* parent)
+   : UI::Box(parent, 0, 0, UI::Box::Vertical),
+     name_label_(this,
+                 0,
+                 0,
+                 UI::Scrollbar::kSize,
+                 0,
+				 UI::PanelStyle::kFsMenu,
+                 "",
+                 UI::Align::kLeft,
+                 UI::MultilineTextarea::ScrollMode::kNoScrolling),
+     descr_(this, 0, 0, UI::Scrollbar::kSize, 0, UI::PanelStyle::kFsMenu) {
+
+	constexpr int kPadding = 4;
+	add(&name_label_, UI::Box::Resizing::kFullSize);
+	add_space(kPadding);
+	add(&descr_, UI::Box::Resizing::kExpandBoth);
+}
+
+void ScenarioDetails::update(const ScenarioData& scenariodata) {
+	name_label_.set_text(
+	   (boost::format("<rt>%s%s</rt>") %
+	    as_heading(scenariodata.is_tutorial ? _("Tutorial") : _("Scenario"), UI::PanelStyle::kFsMenu, true) %
+	    as_content(scenariodata.descname, UI::PanelStyle::kFsMenu))
+	      .str());
+
+	if (scenariodata.playable) {
+		std::string description =
+		   (boost::format("%s%s") %
+		    as_heading(
+		       ngettext("Author", "Authors", scenariodata.authors.get_number()), UI::PanelStyle::kFsMenu) %
+		    as_content(scenariodata.authors.get_names(), UI::PanelStyle::kFsMenu))
+		      .str();
+
+		description =
+		   (boost::format("%s%s") % description % as_heading(_("Description"), UI::PanelStyle::kFsMenu))
+		      .str();
+		description = (boost::format("%s%s") % description %
+		               as_content(scenariodata.description, UI::PanelStyle::kFsMenu))
+		                 .str();
+
+		description = (boost::format("<rt>%s</rt>") % description).str();
+		descr_.set_text(description);
+	} else {
+		descr_.set_text("");
+	}
+	descr_.scroll_to_top();
+}

=== added file 'src/ui_fsmenu/scenariodetails.h'
--- src/ui_fsmenu/scenariodetails.h	1970-01-01 00:00:00 +0000
+++ src/ui_fsmenu/scenariodetails.h	2018-12-13 23:38:35 +0000
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2017 by the Widelands Development Team
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ *
+ */
+
+#ifndef WL_UI_FSMENU_SCENARIODETAILS_H
+#define WL_UI_FSMENU_SCENARIODETAILS_H
+
+#include "ui_basic/box.h"
+#include "ui_basic/multilinetextarea.h"
+#include "ui_fsmenu/campaigns.h"
+
+/**
+ * Show a Box with information about a campaign or tutorial scenario.
+ */
+class ScenarioDetails : public UI::Box {
+public:
+	explicit ScenarioDetails(Panel* parent);
+
+	void update(const ScenarioData& scenariodata);
+
+private:
+	UI::MultilineTextarea name_label_;
+	UI::MultilineTextarea descr_;
+};
+
+#endif  // end of include guard: WL_UI_FSMENU_SCENARIODETAILS_H

=== modified file 'src/wlapplication.cc'
--- src/wlapplication.cc	2018-12-13 07:24:01 +0000
+++ src/wlapplication.cc	2018-12-13 23:38:35 +0000
@@ -81,6 +81,7 @@
 #include "ui_basic/progresswindow.h"
 #include "ui_fsmenu/about.h"
 #include "ui_fsmenu/campaign_select.h"
+#include "ui_fsmenu/campaigns.h"
 #include "ui_fsmenu/internet_lobby.h"
 #include "ui_fsmenu/intro.h"
 #include "ui_fsmenu/launch_spg.h"
@@ -90,6 +91,7 @@
 #include "ui_fsmenu/multiplayer.h"
 #include "ui_fsmenu/netsetup_lan.h"
 #include "ui_fsmenu/options.h"
+#include "ui_fsmenu/scenario_select.h"
 #include "ui_fsmenu/singleplayer.h"
 #include "wlapplication_messages.h"
 #include "wui/game_tips.h"
@@ -1139,8 +1141,7 @@
 	Widelands::Game game;
 	std::string filename;
 	//  Start UI for the tutorials.
-	FullscreenMenuCampaignMapSelect select_campaignmap(true);
-	select_campaignmap.set_campaign(0);
+	FullscreenMenuScenarioSelect select_campaignmap;
 	if (select_campaignmap.run<FullscreenMenuBase::MenuTarget>() ==
 	    FullscreenMenuBase::MenuTarget::kOk) {
 		filename = select_campaignmap.get_map();
@@ -1367,20 +1368,22 @@
 	Widelands::Game game;
 	std::string filename;
 	for (;;) {  // Campaign UI - Loop
-		int32_t campaign;
+		std::unique_ptr<Campaigns> campaign_visibility(new Campaigns());
+
+		size_t campaign_index;
 		{  //  First start UI for selecting the campaign.
-			FullscreenMenuCampaignSelect select_campaign;
+			FullscreenMenuCampaignSelect select_campaign(campaign_visibility.get());
 			if (select_campaign.run<FullscreenMenuBase::MenuTarget>() ==
-			    FullscreenMenuBase::MenuTarget::kOk)
-				campaign = select_campaign.get_campaign();
-			else {  //  back was pressed
+				 FullscreenMenuBase::MenuTarget::kOk) {
+				campaign_index = select_campaign.get_campaign_index();
+			} else {  //  back was pressed
 				filename = "";
 				break;
 			}
 		}
 		//  Then start UI for the selected campaign.
-		FullscreenMenuCampaignMapSelect select_campaignmap;
-		select_campaignmap.set_campaign(campaign);
+		CampaignData* campaign_data = campaign_visibility->get_campaign(campaign_index);
+		FullscreenMenuScenarioSelect select_campaignmap(campaign_data);
 		if (select_campaignmap.run<FullscreenMenuBase::MenuTarget>() ==
 		    FullscreenMenuBase::MenuTarget::kOk) {
 			filename = select_campaignmap.get_map();

=== modified file 'src/wui/CMakeLists.txt'
--- src/wui/CMakeLists.txt	2018-09-12 21:24:39 +0000
+++ src/wui/CMakeLists.txt	2018-12-13 23:38:35 +0000
@@ -87,6 +87,7 @@
 
 wl_library(wui_common_mapdetails
   SRCS
+    mapauthordata.h
     mapdetails.cc
     mapdetails.h
     mapdata.cc
@@ -102,9 +103,9 @@
     graphic
     graphic_fonthandler
     graphic_text_constants
+    graphic_text_layout
     io_filesystem
     logic
-    logic_constants
     logic_game_controller
     logic_game_settings
     map_io_map_loader

=== modified file 'src/wui/load_or_save_game.cc'
--- src/wui/load_or_save_game.cc	2018-12-13 07:24:01 +0000
+++ src/wui/load_or_save_game.cc	2018-12-13 23:38:35 +0000
@@ -366,22 +366,20 @@
 
 	if (filetype_ == FileType::kReplay) {
 		gamefiles = filter(g_fs->list_directory(kReplayDir), [](const std::string& fn) {
-			return boost::ends_with(fn, kReplayExtension);
+			return boost::algorithm::ends_with(fn, kReplayExtension);
 		});
 		// Update description column title for replays
 		table_.set_column_tooltip(2, show_filenames_ ? _("Filename: Map name (start of replay)") :
 		                                               _("Map name (start of replay)"));
 	} else {
-		gamefiles = g_fs->list_directory(kSaveDir);
+		gamefiles = filter(g_fs->list_directory(kSaveDir), [](const std::string& fn) {
+			return boost::algorithm::ends_with(fn, kSavegameExtension);
+		});
 	}
 
 	Widelands::GamePreloadPacket gpdp;
 
 	for (const std::string& gamefilename : gamefiles) {
-		if (gamefilename == kCampVisFile || gamefilename == g_fs->fix_cross_file(kCampVisFile)) {
-			continue;
-		}
-
 		SavegameData gamedata;
 
 		std::string savename = gamefilename;

=== added file 'src/wui/mapauthordata.h'
--- src/wui/mapauthordata.h	1970-01-01 00:00:00 +0000
+++ src/wui/mapauthordata.h	2018-12-13 23:38:35 +0000
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2002-2017 by the Widelands Development Team
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ *
+ */
+
+#ifndef WL_WUI_MAPAUTHORDATA_H
+#define WL_WUI_MAPAUTHORDATA_H
+
+#include <set>
+#include <string>
+#include <vector>
+
+#include <boost/algorithm/string.hpp>
+#include <boost/format.hpp>
+
+#include "base/i18n.h"
+#include "io/filesystem/filesystem.h"
+#include "logic/map.h"
+
+/**
+ * Author data for a map or scenario.
+ */
+struct MapAuthorData {
+	const std::string& get_names() const {
+		return names_;
+	}
+	size_t get_number() const {
+		return number_;
+	}
+
+	void set_authors(const std::string& author_list) {
+		std::vector<std::string> authors;
+		{
+			i18n::Textdomain td("maps");
+			const std::string loc_author_list = _(author_list);
+			boost::split(authors, loc_author_list, boost::is_any_of(","));
+		}
+		names_ = i18n::localize_list(authors, i18n::ConcatenateWith::AMPERSAND);
+		number_ = authors.size();
+	}
+
+	// We allow empty authors, because those will often be loaded
+	// later from the maps
+	MapAuthorData() = default;
+
+	// Parses author list string into localized contatenated list
+	// string. Use , as list separator and no whitespaces between
+	// author names.
+	explicit MapAuthorData(const std::string& author_list) {
+		set_authors(author_list);
+	}
+
+private:
+	std::string names_;
+	size_t number_;
+};
+
+#endif  // end of include guard: WL_WUI_MAPAUTHORDATA_H

=== modified file 'src/wui/mapdata.cc'
--- src/wui/mapdata.cc	2018-04-07 16:59:00 +0000
+++ src/wui/mapdata.cc	2018-12-13 23:38:35 +0000
@@ -41,16 +41,16 @@
                  const std::string& init_filename,
                  const MapData::MapType& init_maptype,
                  const MapData::DisplayType& init_displaytype)
-   : MapData(init_filename, _("No Name"), _("No Author"), init_maptype, init_displaytype) {
+   : MapData(init_filename,
+             _("No Name"),
+             map.get_author().empty() ? _("No Author") : map.get_author(),
+             init_maptype,
+             init_displaytype) {
 
 	i18n::Textdomain td("maps");
 	if (!map.get_name().empty()) {
 		name = map.get_name();
-	}
-	localized_name = _(name);
-	// Localizing this, because some author fields now have "edited by" text.
-	if (!map.get_author().empty()) {
-		authors = map.get_author();
+		localized_name = _(name);
 	}
 	description = map.get_description().empty() ? "" : _(map.get_description());
 	hint = map.get_hint().empty() ? "" : _(map.get_hint());
@@ -68,7 +68,7 @@
 MapData::MapData(const std::string& init_filename, const std::string& init_localized_name)
    : MapData(init_filename,
              init_localized_name,
-             "",
+             _("No Author"),
              MapData::MapType::kDirectory,
              MapData::DisplayType::kMapnamesLocalized) {
 }

=== modified file 'src/wui/mapdata.h'
--- src/wui/mapdata.h	2018-04-07 16:59:00 +0000
+++ src/wui/mapdata.h	2018-12-13 23:38:35 +0000
@@ -30,33 +30,7 @@
 #include "base/i18n.h"
 #include "io/filesystem/filesystem.h"
 #include "logic/map.h"
-#include "logic/widelands.h"
-
-/**
- * Author data for a map or scenario.
- */
-struct MapAuthorData {
-	const std::string& get_names() const {
-		return names_;
-	}
-	size_t get_number() const {
-		return number_;
-	}
-
-	// Parses author list string into localized contatenated list
-	// string. Use , as list separator and no whitespaces between
-	// author names.
-	MapAuthorData(const std::string& author_list) {
-		std::vector<std::string> authors;
-		boost::split(authors, author_list, boost::is_any_of(","));
-		names_ = i18n::localize_list(authors, i18n::ConcatenateWith::AMPERSAND);
-		number_ = authors.size();
-	}
-
-private:
-	std::string names_;
-	size_t number_;
-};
+#include "wui/mapauthordata.h"
 
 /**
  * Data about a map that we're interested in.

=== modified file 'src/wui/mapdetails.h'
--- src/wui/mapdetails.h	2018-04-27 06:11:05 +0000
+++ src/wui/mapdetails.h	2018-12-13 23:38:35 +0000
@@ -20,6 +20,7 @@
 #ifndef WL_WUI_MAPDETAILS_H
 #define WL_WUI_MAPDETAILS_H
 
+#include "graphic/text_layout.h"
 #include "ui_basic/box.h"
 #include "ui_basic/multilinetextarea.h"
 #include "ui_basic/panel.h"


Follow ups