← Back to team overview

widelands-dev team mailing list archive

[Merge] lp:~widelands-dev/widelands/unify-program-parsers into lp:widelands

 

GunChleoc has proposed merging lp:~widelands-dev/widelands/unify-program-parsers into lp:widelands with lp:~widelands-dev/widelands/list-directories-in-cpp as a prerequisite.

Commit message:
Refactor program parsers
- Pull out common code into new class MapObjectProgram
- Fix ware demand checks so that they only affect the correct tribes
- Get rid of extraneous calls to EditorGameBase::postload()
- Get rid of now empty helper library

Requested reviews:
  Widelands Developers (widelands-dev)

For more details, see:
https://code.launchpad.net/~widelands-dev/widelands/unify-program-parsers/+merge/367936

The goal of this branch is to make the code easier to read, and to get rid of code duplication. This also fixes a bug with the ware demand checks.
-- 
Your team Widelands Developers is requested to review the proposed merge of lp:~widelands-dev/widelands/unify-program-parsers into lp:widelands.
=== modified file 'data/tribes/immovables/shipconstruction_atlanteans/init.lua'
--- data/tribes/immovables/shipconstruction_atlanteans/init.lua	2019-04-20 08:40:37 +0000
+++ data/tribes/immovables/shipconstruction_atlanteans/init.lua	2019-05-26 08:37:46 +0000
@@ -11,7 +11,7 @@
    programs = {
       program = {
          "construct=idle 5000 210000",
-         "transform=bob tribe:atlanteans_ship",
+         "transform=bob atlanteans_ship",
       }
    },
    buildcost = {

=== modified file 'data/tribes/immovables/shipconstruction_barbarians/init.lua'
--- data/tribes/immovables/shipconstruction_barbarians/init.lua	2019-04-20 08:40:37 +0000
+++ data/tribes/immovables/shipconstruction_barbarians/init.lua	2019-05-26 08:37:46 +0000
@@ -11,7 +11,7 @@
    programs = {
       program = {
          "construct=idle 5000 210000",
-         "transform=bob tribe:barbarians_ship",
+         "transform=bob barbarians_ship",
       }
    },
    buildcost = {

=== modified file 'data/tribes/immovables/shipconstruction_empire/init.lua'
--- data/tribes/immovables/shipconstruction_empire/init.lua	2019-04-20 08:40:37 +0000
+++ data/tribes/immovables/shipconstruction_empire/init.lua	2019-05-26 08:37:46 +0000
@@ -11,7 +11,7 @@
    programs = {
       program = {
          "construct=idle 5000 210000",
-         "transform=bob tribe:empire_ship",
+         "transform=bob empire_ship",
       }
    },
    buildcost = {

=== modified file 'data/tribes/immovables/shipconstruction_frisians/init.lua'
--- data/tribes/immovables/shipconstruction_frisians/init.lua	2019-04-26 05:32:54 +0000
+++ data/tribes/immovables/shipconstruction_frisians/init.lua	2019-05-26 08:37:46 +0000
@@ -11,7 +11,7 @@
    programs = {
       program = {
          "construct=idle 5000 210000",
-         "transform=bob tribe:frisians_ship",
+         "transform=bob frisians_ship",
       }
    },
    buildcost = {

=== modified file 'data/tribes/workers/empire/stonemason/init.lua'
--- data/tribes/workers/empire/stonemason/init.lua	2019-05-26 08:37:42 +0000
+++ data/tribes/workers/empire/stonemason/init.lua	2019-05-26 08:37:46 +0000
@@ -41,7 +41,7 @@
          "return"
       },
       cut_marble = {
-         "findobject= attrib:rocks radius:6",
+         "findobject=attrib:rocks radius:6",
          "walk=object",
          "playsound=sound/stonecutting/stonecutter 220",
          "animate=hacking 10000",

=== modified file 'src/CMakeLists.txt'
--- src/CMakeLists.txt	2019-05-26 08:37:42 +0000
+++ src/CMakeLists.txt	2019-05-26 08:37:46 +0000
@@ -132,16 +132,6 @@
     wui
 )
 
-# TODO(sirver): Split into libs with useful names.
-wl_library(helper
-  SRCS
-    helper.cc
-    helper.h
-  USES_SDL2
-  DEPENDS
-    base_exceptions
-)
-
 if (CMAKE_SYSTEM_NAME MATCHES "FreeBSD" OR CMAKE_SYSTEM_NAME MATCHES "OpenBSD")
   target_link_libraries(widelands_ball_of_mud ${EXECINFO_LIBRARY})
 endif (CMAKE_SYSTEM_NAME MATCHES "FreeBSD" OR CMAKE_SYSTEM_NAME MATCHES "OpenBSD")

=== modified file 'src/editor/editorinteractive.cc'
--- src/editor/editorinteractive.cc	2019-04-26 05:52:49 +0000
+++ src/editor/editorinteractive.cc	2019-05-26 08:37:46 +0000
@@ -190,8 +190,7 @@
 	}
 
 	ml->load_map_complete(egbase(), Widelands::MapLoader::LoadType::kEditor);
-	egbase().postload();
-	egbase().load_graphics(loader_ui);
+	egbase().create_tempfile_and_save_mapdata(FileSystem::ZIP);
 	map_changed(MapWas::kReplaced);
 }
 

=== modified file 'src/editor/ui_menus/main_menu_new_map.cc'
--- src/editor/ui_menus/main_menu_new_map.cc	2019-04-26 05:52:49 +0000
+++ src/editor/ui_menus/main_menu_new_map.cc	2019-05-26 08:37:46 +0000
@@ -137,8 +137,7 @@
 	   list_.get_selected(), _("No Name"),
 	   g_options.pull_section("global").get_string("realname", pgettext("author_name", "Unknown")));
 
-	egbase.postload();
-	egbase.load_graphics(loader_ui);
+	egbase.create_tempfile_and_save_mapdata(FileSystem::ZIP);
 
 	map->recalc_whole_map(egbase.world());
 	parent.map_changed(EditorInteractive::MapWas::kReplaced);

=== modified file 'src/editor/ui_menus/main_menu_random_map.cc'
--- src/editor/ui_menus/main_menu_random_map.cc	2019-04-24 07:09:29 +0000
+++ src/editor/ui_menus/main_menu_random_map.cc	2019-05-26 08:37:46 +0000
@@ -559,8 +559,7 @@
 
 	gen.create_random_map();
 
-	egbase.postload();
-	egbase.load_graphics(loader_ui);
+	egbase.create_tempfile_and_save_mapdata(FileSystem::ZIP);
 
 	map->recalc_whole_map(egbase.world());
 	eia.map_changed(EditorInteractive::MapWas::kReplaced);

=== removed file 'src/helper.cc'
--- src/helper.cc	2019-02-23 11:00:49 +0000
+++ src/helper.cc	1970-01-01 00:00:00 +0000
@@ -1,55 +0,0 @@
-/*
- * Copyright (C) 2002-2019 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 "helper.h"
-
-#include <cstdarg>
-#include <memory>
-#include <string>
-
-#include <boost/algorithm/string/replace.hpp>
-#include <boost/format.hpp>
-#include <boost/lexical_cast.hpp>
-
-std::vector<std::string> split_string(const std::string& s, const char* const separators) {
-	std::vector<std::string> result;
-	for (std::string::size_type pos = 0, endpos;
-	     (pos = s.find_first_not_of(separators, pos)) != std::string::npos; pos = endpos) {
-		endpos = s.find_first_of(separators, pos);
-		result.push_back(s.substr(pos, endpos - pos));
-	}
-	return result;
-}
-
-char* next_word(char*& p, bool& reached_end, char const terminator) {
-	assert(terminator);
-	char* const result = p;
-	for (; *p != terminator; ++p)
-		if (*p == '\0') {
-			reached_end = true;
-			goto end;
-		}
-	reached_end = false;
-	*p = '\0';  //  terminate the word
-	++p;        //  move past the terminator
-end:
-	if (result < p)
-		return result;
-	throw wexception("expected word");
-}

=== removed file 'src/helper.h'
--- src/helper.h	2019-05-26 08:37:42 +0000
+++ src/helper.h	1970-01-01 00:00:00 +0000
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2006-2019 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_HELPER_H
-#define WL_HELPER_H
-
-#include <cassert>
-#include <cstring>
-#include <string>
-#include <vector>
-
-#include <SDL_keyboard.h>
-#include <boost/utility.hpp>
-
-#include "base/wexception.h"
-
-/// Returns the word starting at the character that p points to and ending
-/// before the first terminator character. Replaces the terminator with null.
-// TODO(sirver): move into a logic/strings lib or so.
-char* next_word(char*& p, bool& reached_end, char terminator = ' ');
-
-/// Split a string by separators.
-/// \note This ignores empty elements, so do not use this for example to split
-/// a string with newline characters into lines, because it would ignore empty
-/// lines.
-std::vector<std::string> split_string(const std::string&, char const* separators);
-
-#endif  // end of include guard: WL_HELPER_H

=== modified file 'src/logic/editor_game_base.cc'
--- src/logic/editor_game_base.cc	2019-04-26 05:52:49 +0000
+++ src/logic/editor_game_base.cc	2019-05-26 08:37:46 +0000
@@ -113,50 +113,58 @@
  * throws an exception if something goes wrong
  */
 void EditorGameBase::create_tempfile_and_save_mapdata(FileSystem::Type const type) {
-	// should only be called when a map was already loaded
-	assert(map_.filesystem());
-
-	g_fs->ensure_directory_exists(kTempFileDir);
-
-	std::string filename = kTempFileDir + g_fs->file_separator() + timestring() + "_mapdata";
-	std::string complete_filename = filename + kTempFileExtension;
-
-	// if a file with that name already exists, then try a few name modifications
-	if (g_fs->file_exists(complete_filename)) {
-		int suffix;
-		for (suffix = 0; suffix <= 9; suffix++) {
-			complete_filename = filename + "-" + std::to_string(suffix) + kTempFileExtension;
-			if (!g_fs->file_exists(complete_filename))
-				break;
-		}
-		if (suffix > 9) {
-			throw wexception("EditorGameBase::create_tempfile_and_save_mapdata(): for all considered "
-			                 "filenames a file already existed");
-		}
-	}
-
-	// create tmp_fs_
-	tmp_fs_.reset(g_fs->create_sub_file_system(complete_filename, type));
-
-	// save necessary map data (we actually save the whole map)
-	std::unique_ptr<Widelands::MapSaver> wms(new Widelands::MapSaver(*tmp_fs_, *this));
-	wms->save();
-
-	// swap map fs
-	std::unique_ptr<FileSystem> mapfs(tmp_fs_->make_sub_file_system("."));
-	map_.swap_filesystem(mapfs);
-	mapfs.reset();
-
-	// This is just a convenience hack:
-	// If tmp_fs_ is a zip filesystem then - because of the way zip filesystems are currently
-	// implemented -
-	// the file is still in zip mode right now, which means that the file isn't finalized yet, i.e.,
-	// not even a valid zip file until zip mode ends. To force ending the zip mode (thus finalizing
-	// the file)
-	// we simply perform a (otherwise useless) filesystem request.
-	// It's not strictly necessary, but this way we get a valid zip file immediately istead of
-	// at some unkown later point (when an unzip operation happens or a filesystem object destructs).
-	tmp_fs_->file_exists("binary");
+	if (!map_.filesystem()) {
+		return;
+	}
+
+	// save map data to temporary file and reassign map fs
+	try {
+
+		g_fs->ensure_directory_exists(kTempFileDir);
+
+		std::string filename = kTempFileDir + g_fs->file_separator() + timestring() + "_mapdata";
+		std::string complete_filename = filename + kTempFileExtension;
+
+		// if a file with that name already exists, then try a few name modifications
+		if (g_fs->file_exists(complete_filename)) {
+			int suffix;
+			for (suffix = 0; suffix <= 9; suffix++) {
+				complete_filename = filename + "-" + std::to_string(suffix) + kTempFileExtension;
+				if (!g_fs->file_exists(complete_filename))
+					break;
+			}
+			if (suffix > 9) {
+				throw wexception("EditorGameBase::create_tempfile_and_save_mapdata(): for all considered "
+								 "filenames a file already existed");
+			}
+		}
+
+		// create tmp_fs_
+		tmp_fs_.reset(g_fs->create_sub_file_system(complete_filename, type));
+
+		// save necessary map data (we actually save the whole map)
+		std::unique_ptr<Widelands::MapSaver> wms(new Widelands::MapSaver(*tmp_fs_, *this));
+		wms->save();
+
+		// swap map fs
+		std::unique_ptr<FileSystem> mapfs(tmp_fs_->make_sub_file_system("."));
+		map_.swap_filesystem(mapfs);
+		mapfs.reset();
+
+		// This is just a convenience hack:
+		// If tmp_fs_ is a zip filesystem then - because of the way zip filesystems are currently
+		// implemented -
+		// the file is still in zip mode right now, which means that the file isn't finalized yet, i.e.,
+		// not even a valid zip file until zip mode ends. To force ending the zip mode (thus finalizing
+		// the file)
+		// we simply perform a (otherwise useless) filesystem request.
+		// It's not strictly necessary, but this way we get a valid zip file immediately istead of
+		// at some unkown later point (when an unzip operation happens or a filesystem object destructs).
+		tmp_fs_->file_exists("binary");
+	} catch (const WException& e) {
+		log("EditorGameBase: saving map to temporary file failed: %s", e.what());
+		throw;
+	}
 }
 
 void EditorGameBase::think() {
@@ -275,20 +283,11 @@
 }
 
 /**
- * Load and prepare detailed game data.
- * This happens once just after the host has started the game and before the
- * graphics are loaded.
+ * Load and prepare detailed game and map data.
+ * This happens once just after the host has started the game / the editor has started and before the graphics are loaded.
  */
 void EditorGameBase::postload() {
-	if (map_.filesystem()) {
-		// save map data to temporary file and reassign map fs
-		try {
-			create_tempfile_and_save_mapdata(FileSystem::ZIP);
-		} catch (const WException& e) {
-			log("EditorGameBase::postload: saving map to temporary file failed: %s", e.what());
-			throw;
-		}
-	}
+	create_tempfile_and_save_mapdata(FileSystem::ZIP);
 
 	// Postload tribes
 	assert(tribes_);

=== modified file 'src/logic/editor_game_base.h'
--- src/logic/editor_game_base.h	2019-02-27 19:00:36 +0000
+++ src/logic/editor_game_base.h	2019-05-26 08:37:46 +0000
@@ -198,6 +198,8 @@
 	// Returns the mutable tribes. Prefer tribes() whenever possible.
 	Tribes* mutable_tribes();
 
+	void create_tempfile_and_save_mapdata(FileSystem::Type type);
+
 private:
 	/// Common function for create_critter and create_ship.
 	Bob& create_bob(Coords, const BobDescr&, Player* owner = nullptr);
@@ -262,7 +264,6 @@
 	/// a temporary file (in a special dir) is created for such data.
 	std::unique_ptr<FileSystem> tmp_fs_;
 	void delete_tempfile();
-	void create_tempfile_and_save_mapdata(FileSystem::Type type);
 
 	DISALLOW_COPY_AND_ASSIGN(EditorGameBase);
 };

=== modified file 'src/logic/map_objects/CMakeLists.txt'
--- src/logic/map_objects/CMakeLists.txt	2019-05-05 18:53:14 +0000
+++ src/logic/map_objects/CMakeLists.txt	2019-05-26 08:37:46 +0000
@@ -39,8 +39,11 @@
     immovable.cc
     immovable.h
     immovable_program.h
+    immovable_program.cc
     map_object.cc
     map_object.h
+    map_object_program.cc
+    map_object_program.h
     terrain_affinity.cc
     terrain_affinity.h
     tribes/attack_target.h
@@ -127,7 +130,6 @@
     graphic_surface
     graphic_text_constants
     graphic_text_layout
-    helper
     io_fileread
     io_filesystem
     logic # TODO(GunChleoc): Circular dependency

=== modified file 'src/logic/map_objects/bob.cc'
--- src/logic/map_objects/bob.cc	2019-05-11 13:48:12 +0000
+++ src/logic/map_objects/bob.cc	2019-05-26 08:37:46 +0000
@@ -1092,8 +1092,8 @@
 	throw GameDataError("unknown bob task '%s'", name.c_str());
 }
 
-const BobProgramBase* Bob::Loader::get_program(const std::string& name) {
-	throw GameDataError("unknown bob program '%s'", name.c_str());
+const MapObjectProgram* Bob::Loader::get_program(const std::string& name) {
+	throw GameDataError("unknown map object program '%s'", name.c_str());
 }
 
 void Bob::save(EditorGameBase& eg, MapObjectSaver& mos, FileWrite& fw) {
@@ -1156,7 +1156,7 @@
 			fw.unsigned_8(0);
 		}
 
-		fw.c_string(state.program ? state.program->get_name() : "");
+		fw.c_string(state.program ? state.program->name() : "");
 	}
 }
 }  // namespace Widelands

=== modified file 'src/logic/map_objects/bob.h'
--- src/logic/map_objects/bob.h	2019-04-24 06:01:37 +0000
+++ src/logic/map_objects/bob.h	2019-05-26 08:37:46 +0000
@@ -27,29 +27,19 @@
 #include "graphic/diranimations.h"
 #include "logic/map_objects/draw_text.h"
 #include "logic/map_objects/map_object.h"
+#include "logic/map_objects/map_object_program.h"
 #include "logic/map_objects/walkingdir.h"
 #include "logic/widelands_geometry.h"
 
 namespace Widelands {
+
+class Bob;
 class Map;
 struct Route;
 struct Transfer;
 class TribeDescr;
 
 /**
- * BobProgramBase is only used that
- * get_name always works
- */
-
-struct BobProgramBase {
-	virtual ~BobProgramBase() {
-	}
-	virtual std::string get_name() const = 0;
-};
-
-class Bob;
-
-/**
  * Implement MapObjectDescr for the following \ref Bob class.
  */
 class BobDescr : public MapObjectDescr {
@@ -223,7 +213,7 @@
 		DirAnimations diranims;
 		Path* path;
 		Route* route;
-		const BobProgramBase* program;  ///< pointer to current program
+		const MapObjectProgram* program;  ///< pointer to current program
 	};
 
 	MO_DESCR(BobDescr)
@@ -419,7 +409,7 @@
 
 	protected:
 		virtual const Task* get_task(const std::string& name);
-		virtual const BobProgramBase* get_program(const std::string& name);
+		virtual const MapObjectProgram* get_program(const std::string& name);
 
 	private:
 		struct LoadState {

=== modified file 'src/logic/map_objects/immovable.cc'
--- src/logic/map_objects/immovable.cc	2019-05-19 12:25:24 +0000
+++ src/logic/map_objects/immovable.cc	2019-05-26 08:37:46 +0000
@@ -19,42 +19,18 @@
 
 #include "logic/map_objects/immovable.h"
 
-#include <cstdio>
-#include <cstring>
 #include <memory>
 
-#include <boost/algorithm/string.hpp>
-#include <boost/format.hpp>
-
-#include "base/log.h"
-#include "base/macros.h"
-#include "base/wexception.h"
-#include "config.h"
-#include "graphic/graphic.h"
-#include "graphic/rendertarget.h"
 #include "graphic/text_constants.h"
-#include "helper.h"
 #include "io/fileread.h"
 #include "io/filewrite.h"
-#include "logic/editor_game_base.h"
-#include "logic/field.h"
-#include "logic/game.h"
 #include "logic/game_data_error.h"
-#include "logic/map.h"
 #include "logic/map_objects/immovable_program.h"
 #include "logic/map_objects/terrain_affinity.h"
-#include "logic/map_objects/tribes/tribe_descr.h"
-#include "logic/map_objects/tribes/worker.h"
 #include "logic/map_objects/world/world.h"
-#include "logic/mapfringeregion.h"
 #include "logic/player.h"
 #include "logic/widelands_geometry_io.h"
-#include "map_io/tribes_legacy_lookup_table.h"
 #include "map_io/world_legacy_lookup_table.h"
-#include "notifications/notifications.h"
-#include "scripting/lua_table.h"
-#include "sound/note_sound.h"
-#include "sound/sound_handler.h"
 
 namespace Widelands {
 
@@ -141,52 +117,6 @@
 /*
 ==============================================================================
 
-ImmovableProgram IMPLEMENTATION
-
-==============================================================================
-*/
-
-ImmovableProgram::ImmovableProgram(const std::string& init_name,
-                                   const std::vector<std::string>& lines,
-                                   ImmovableDescr* immovable)
-   : name_(init_name) {
-	for (const std::string& line : lines) {
-		std::vector<std::string> parts;
-		boost::split(parts, line, boost::is_any_of("="));
-		if (parts.size() != 2) {
-			throw GameDataError("invalid line: %s.", line.c_str());
-		}
-		std::unique_ptr<char[]> arguments(new char[parts[1].size() + 1]);
-		strncpy(arguments.get(), parts[1].c_str(), parts[1].size() + 1);
-
-		Action* action;
-		if (parts[0] == "animate") {
-			action = new ActAnimate(arguments.get(), *immovable);
-		} else if (parts[0] == "transform") {
-			action = new ActTransform(arguments.get(), *immovable);
-		} else if (parts[0] == "grow") {
-			action = new ActGrow(arguments.get(), *immovable);
-		} else if (parts[0] == "remove") {
-			action = new ActRemove(arguments.get(), *immovable);
-		} else if (parts[0] == "seed") {
-			action = new ActSeed(arguments.get(), *immovable);
-		} else if (parts[0] == "playsound") {
-			action = new ActPlaySound(arguments.get(), *immovable);
-		} else if (parts[0] == "construct") {
-			action = new ActConstruct(arguments.get(), *immovable);
-		} else {
-			throw GameDataError("unknown command type \"%s\" in immovable \"%s\"", parts[0].c_str(),
-			                    immovable->name().c_str());
-		}
-		actions_.push_back(action);
-	}
-	if (actions_.empty())
-		throw GameDataError("no actions");
-}
-
-/*
-==============================================================================
-
 ImmovableDescr IMPLEMENTATION
 
 ==============================================================================
@@ -250,12 +180,16 @@
 	}
 
 	std::unique_ptr<LuaTable> programs = table.get_table("programs");
-	for (const std::string& program_name : programs->keys<std::string>()) {
+	for (std::string program_name : programs->keys<std::string>()) {
+		std::transform(program_name.begin(), program_name.end(), program_name.begin(), tolower);
+		if (programs_.count(program_name)) {
+			throw GameDataError("Program '%s' has already been declared for immovable '%s'", program_name.c_str(), name().c_str());
+		}
 		try {
 			programs_[program_name] = new ImmovableProgram(
-			   program_name, programs->get_table(program_name)->array_entries<std::string>(), this);
+			   program_name, programs->get_table(program_name)->array_entries<std::string>(), *this);
 		} catch (const std::exception& e) {
-			throw wexception("Error in program %s: %s", program_name.c_str(), e.what());
+			throw GameDataError("%s: Error in immovable program %s: %s", name().c_str(), program_name.c_str(), e.what());
 		}
 	}
 
@@ -309,9 +243,9 @@
 void ImmovableDescr::make_sure_default_program_is_there() {
 	if (!programs_.count("program")) {  //  default program
 		assert(is_animation_known("idle"));
-		char parameters[] = "idle";
+		std::vector<std::string> arguments{"idle"};
 		programs_["program"] =
-		   new ImmovableProgram("program", new ImmovableProgram::ActAnimate(parameters, *this));
+		   new ImmovableProgram("program", std::unique_ptr<ImmovableProgram::Action>(new ImmovableProgram::ActAnimate(arguments, *this)));
 	}
 }
 
@@ -419,10 +353,12 @@
 	ImmovableProgram const* prog = program_;
 	if (!prog) {
 		prog = descr().get_program("program");
-		assert(prog != nullptr);
-	}
-	if (upcast(ImmovableProgram::ActAnimate const, act_animate, &(*prog)[program_ptr_]))
-		start_animation(egbase, descr().get_animation(act_animate->animation(), this));
+	}
+	assert(prog != nullptr);
+
+	if (upcast(ImmovableProgram::ActAnimate const, act_animate, &(*prog)[program_ptr_])) {
+		start_animation(egbase, act_animate->animation());
+	}
 
 	if (upcast(Game, game, &egbase)) {
 		switch_program(*game, "program");
@@ -762,391 +698,6 @@
 	return loader.release();
 }
 
-ImmovableProgram::Action::~Action() {
-}
-
-ImmovableProgram::ActAnimate::ActAnimate(char* parameters, ImmovableDescr& descr) {
-	try {
-		bool reached_end;
-		animation_name_ = std::string(next_word(parameters, reached_end));
-		if (!descr.is_animation_known(animation_name_)) {
-			throw GameDataError("Unknown animation: %s.", animation_name_.c_str());
-		}
-
-		if (!reached_end) {  //  The next parameter is the duration.
-			char* endp;
-			long int const value = strtol(parameters, &endp, 0);
-			if (*endp || value <= 0)
-				throw GameDataError("expected %s but found \"%s\"", "duration in ms", parameters);
-			duration_ = value;
-		} else {
-			duration_ = 0;  //  forever
-		}
-	} catch (const WException& e) {
-		throw GameDataError("animate: %s", e.what());
-	}
-}
-
-/// Use convolutuion to make the animation time a random variable with binomial
-/// distribution and the configured time as the expected value.
-void ImmovableProgram::ActAnimate::execute(Game& game, Immovable& immovable) const {
-	immovable.start_animation(game, immovable.descr().get_animation(animation_name_, &immovable));
-	immovable.program_step(
-	   game, duration_ ? 1 + game.logic_rand() % duration_ + game.logic_rand() % duration_ : 0);
-}
-
-ImmovableProgram::ActPlaySound::ActPlaySound(char* parameters, const ImmovableDescr&) {
-	try {
-		bool reached_end;
-		std::string name = next_word(parameters, reached_end);
-
-		if (!reached_end) {
-			char* endp;
-			unsigned long long int const value = strtoull(parameters, &endp, 0);
-			priority = value;
-			if (*endp || priority != value)
-				throw GameDataError("expected %s but found \"%s\"", "priority", parameters);
-		} else
-			priority = 127;
-
-		fx = g_sh->register_fx(SoundType::kAmbient, name);
-	} catch (const WException& e) {
-		throw GameDataError("playsound: %s", e.what());
-	}
-}
-
-/** Demand from the g_sound_handler to play a certain sound effect.
- * Whether the effect actually gets played
- * is decided only by the sound server*/
-void ImmovableProgram::ActPlaySound::execute(Game& game, Immovable& immovable) const {
-	Notifications::publish(NoteSound(SoundType::kAmbient, fx, immovable.get_position(), priority));
-	immovable.program_step(game);
-}
-
-ImmovableProgram::ActTransform::ActTransform(char* parameters, ImmovableDescr& descr) {
-	try {
-		tribe = true;
-		bob = false;
-		probability = 0;
-
-		std::vector<std::string> params = split_string(parameters, " ");
-		for (uint32_t i = 0; i < params.size(); ++i) {
-			if (params[i] == "bob")
-				bob = true;
-			else if (params[i] == "immovable")
-				bob = false;
-			else if (params[i][0] >= '0' && params[i][0] <= '9') {
-				long int const value = atoi(params[i].c_str());
-				if (value < 1 || 254 < value)
-					throw GameDataError("expected %s but found \"%s\"", "probability in range [1, 254]",
-					                    params[i].c_str());
-				probability = value;
-			} else {
-				std::vector<std::string> segments = split_string(params[i], ":");
-
-				if (segments.size() > 2)
-					throw GameDataError("object type has more than 2 segments");
-				if (segments.size() == 2) {
-					if (segments[0] == "world")
-						tribe = false;
-					else if (segments[0] == "tribe") {
-						if (descr.owner_type() != MapObjectDescr::OwnerType::kTribe)
-							throw GameDataError("scope \"tribe\" does not match the immovable type");
-						tribe = true;
-					} else
-						throw GameDataError("unknown scope \"%s\" given for target type (must be "
-						                    "\"world\" or \"tribe\")",
-						                    parameters);
-
-					type_name = segments[1];
-				} else {
-					type_name = segments[0];
-				}
-			}
-		}
-		if (type_name == descr.name())
-			throw GameDataError("illegal transformation to the same type");
-	} catch (const WException& e) {
-		throw GameDataError("transform: %s", e.what());
-	}
-}
-
-void ImmovableProgram::ActTransform::execute(Game& game, Immovable& immovable) const {
-	if (probability == 0 || game.logic_rand() % 256 < probability) {
-		Player* player = immovable.get_owner();
-		Coords const c = immovable.get_position();
-		MapObjectDescr::OwnerType owner_type = immovable.descr().owner_type();
-		immovable.remove(game);  //  Now immovable is a dangling reference!
-
-		if (bob) {
-			game.create_ship(c, type_name, player);
-		} else {
-			game.create_immovable_with_name(
-			   c, type_name, owner_type, player, nullptr /* former_building_descr */);
-		}
-	} else
-		immovable.program_step(game);
-}
-
-ImmovableProgram::ActGrow::ActGrow(char* parameters, ImmovableDescr& descr) {
-	if (!descr.has_terrain_affinity()) {
-		throw GameDataError(
-		   "Immovable %s can 'grow', but has no terrain_affinity entry.", descr.name().c_str());
-	}
-
-	try {
-		tribe = true;
-		for (char* p = parameters;;)
-			switch (*p) {
-			case ':': {
-				*p = '\0';
-				++p;
-				if (descr.owner_type() != MapObjectDescr::OwnerType::kTribe)
-					throw GameDataError("immovable type not in tribes but target type has scope "
-					                    "(\"%s\")",
-					                    parameters);
-				else if (strcmp(parameters, "world"))
-					throw GameDataError("scope \"%s\" given for target type (must be "
-					                    "\"world\")",
-					                    parameters);
-				tribe = false;
-				parameters = p;
-				break;
-			}
-			case '\0':
-				goto end;
-			default:
-				++p;
-				break;
-			}
-	end:
-		type_name = parameters;
-	} catch (const WException& e) {
-		throw GameDataError("grow: %s", e.what());
-	}
-}
-
-void ImmovableProgram::ActGrow::execute(Game& game, Immovable& immovable) const {
-	const Map& map = game.map();
-	FCoords const f = map.get_fcoords(immovable.get_position());
-	const ImmovableDescr& descr = immovable.descr();
-
-	if ((game.logic_rand() % TerrainAffinity::kPrecisionFactor) <
-	    probability_to_grow(descr.terrain_affinity(), f, map, game.world().terrains())) {
-		MapObjectDescr::OwnerType owner_type = descr.owner_type();
-		Player* owner = immovable.get_owner();
-		immovable.remove(game);  //  Now immovable is a dangling reference!
-		game.create_immovable_with_name(
-		   f, type_name, owner_type, owner, nullptr /* former_building_descr */);
-	} else {
-		immovable.program_step(game);
-	}
-}
-
-/**
- * remove
- */
-ImmovableProgram::ActRemove::ActRemove(char* parameters, ImmovableDescr&) {
-	try {
-		if (*parameters) {
-			char* endp;
-			long int const value = strtol(parameters, &endp, 0);
-			if (*endp || value < 1 || 254 < value)
-				throw GameDataError(
-				   "expected %s but found \"%s\"", "probability in range [1, 254]", parameters);
-			probability = value;
-		} else
-			probability = 0;
-	} catch (const WException& e) {
-		throw GameDataError("remove: %s", e.what());
-	}
-}
-
-void ImmovableProgram::ActRemove::execute(Game& game, Immovable& immovable) const {
-	if (probability == 0 || game.logic_rand() % 256 < probability)
-		immovable.remove(game);  //  Now immovable is a dangling reference!
-	else
-		immovable.program_step(game);
-}
-
-ImmovableProgram::ActSeed::ActSeed(char* parameters, ImmovableDescr& descr) {
-	try {
-		probability = 0;
-		for (char* p = parameters;;)
-			switch (*p) {
-			case ':': {
-				*p = '\0';
-				++p;
-				if (descr.owner_type() != MapObjectDescr::OwnerType::kTribe)
-					throw GameDataError("immovable type not in tribes but target type has scope "
-					                    "(\"%s\")",
-					                    parameters);
-				else if (strcmp(parameters, "world"))
-					throw GameDataError("scope \"%s\" given for target type (must be "
-					                    "\"world\")",
-					                    parameters);
-				parameters = p;
-				break;
-			}
-			case ' ': {
-				*p = '\0';
-				++p;
-				char* endp;
-				long int const value = strtol(p, &endp, 0);
-				if (*endp || value < 1 || 254 < value)
-					throw GameDataError(
-					   "expected %s but found \"%s\"", "probability in range [1, 254]", p);
-				probability = value;
-				//  fallthrough
-			}
-				FALLS_THROUGH;
-			case '\0':
-				goto end;
-			default:
-				++p;
-				break;
-			}
-	end:
-		type_name = parameters;
-	} catch (const WException& e) {
-		throw GameDataError("seed: %s", e.what());
-	}
-}
-
-void ImmovableProgram::ActSeed::execute(Game& game, Immovable& immovable) const {
-	const Map& map = game.map();
-	FCoords const f = map.get_fcoords(immovable.get_position());
-	const ImmovableDescr& descr = immovable.descr();
-
-	if ((game.logic_rand() % TerrainAffinity::kPrecisionFactor) <
-	    probability_to_grow(descr.terrain_affinity(), f, map, game.world().terrains())) {
-		// Seed a new tree.
-		MapFringeRegion<> mr(map, Area<>(f, 0));
-		uint32_t fringe_size = 0;
-		do {
-			mr.extend(map);
-			fringe_size += 6;
-		} while (game.logic_rand() % std::numeric_limits<uint8_t>::max() < probability);
-
-		for (uint32_t n = game.logic_rand() % fringe_size; n; --n) {
-			mr.advance(map);
-		}
-
-		const FCoords new_location = map.get_fcoords(mr.location());
-		if (!new_location.field->get_immovable() &&
-		    (new_location.field->nodecaps() & MOVECAPS_WALK) &&
-		    (game.logic_rand() % TerrainAffinity::kPrecisionFactor) <
-		       probability_to_grow(
-		          descr.terrain_affinity(), new_location, map, game.world().terrains())) {
-			game.create_immovable_with_name(mr.location(), type_name, descr.owner_type(),
-			                                nullptr /* owner */, nullptr /* former_building_descr */);
-		}
-	}
-
-	immovable.program_step(game);
-}
-
-ImmovableProgram::ActConstruct::ActConstruct(char* parameters, ImmovableDescr& descr) {
-	try {
-		if (descr.owner_type() != MapObjectDescr::OwnerType::kTribe)
-			throw GameDataError("only usable for tribe immovable");
-
-		std::vector<std::string> params = split_string(parameters, " ");
-
-		if (params.size() != 3)
-			throw GameDataError("usage: animation-name buildtime decaytime");
-
-		buildtime_ = atoi(params[1].c_str());
-		decaytime_ = atoi(params[2].c_str());
-
-		animation_name_ = params[0];
-		if (!descr.is_animation_known(animation_name_)) {
-			throw GameDataError("unknown animation \"%s\" in immovable program for immovable \"%s\"",
-			                    animation_name_.c_str(), descr.name().c_str());
-		}
-	} catch (const WException& e) {
-		throw GameDataError("construct: %s", e.what());
-	}
-}
-
-constexpr uint8_t kCurrentPacketVersionConstructionData = 1;
-
-struct ActConstructData : ImmovableActionData {
-	const char* name() const override {
-		return "construct";
-	}
-	void save(FileWrite& fw, Immovable& imm) override {
-		fw.unsigned_8(kCurrentPacketVersionConstructionData);
-		delivered.save(fw, imm.get_owner()->tribe());
-	}
-
-	static ActConstructData* load(FileRead& fr, Immovable& imm) {
-		ActConstructData* d = new ActConstructData;
-
-		try {
-			uint8_t packet_version = fr.unsigned_8();
-			if (packet_version == kCurrentPacketVersionConstructionData) {
-				d->delivered.load(fr, imm.get_owner()->tribe());
-			} else {
-				throw UnhandledVersionError(
-				   "ActConstructData", packet_version, kCurrentPacketVersionConstructionData);
-			}
-		} catch (const WException& e) {
-			delete d;
-			d = nullptr;
-			throw GameDataError("ActConstructData: %s", e.what());
-		}
-
-		return d;
-	}
-
-	Buildcost delivered;
-};
-
-void ImmovableProgram::ActConstruct::execute(Game& g, Immovable& imm) const {
-	ActConstructData* d = imm.get_action_data<ActConstructData>();
-	if (!d) {
-		// First execution
-		d = new ActConstructData;
-		imm.set_action_data(d);
-
-		imm.start_animation(g, imm.descr().get_animation(animation_name_, &imm));
-		imm.anim_construction_total_ = imm.descr().buildcost().total();
-	} else {
-		// Perhaps we are called due to the construction timeout of the last construction step
-		Buildcost remaining;
-		imm.construct_remaining_buildcost(g, &remaining);
-		if (remaining.empty()) {
-			imm.program_step(g);
-			return;
-		}
-
-		// Otherwise, this is a decay timeout
-		uint32_t totaldelivered = 0;
-		for (Buildcost::const_iterator it = d->delivered.begin(); it != d->delivered.end(); ++it)
-			totaldelivered += it->second;
-
-		if (!totaldelivered) {
-			imm.remove(g);
-			return;
-		}
-
-		uint32_t randdecay = g.logic_rand() % totaldelivered;
-		for (Buildcost::iterator it = d->delivered.begin(); it != d->delivered.end(); ++it) {
-			if (randdecay < it->second) {
-				it->second--;
-				break;
-			}
-
-			randdecay -= it->second;
-		}
-
-		imm.anim_construction_done_ = d->delivered.total();
-	}
-
-	imm.program_step_ = imm.schedule_act(g, decaytime_);
-}
-
 /**
  * For an immovable that is currently in construction mode, return \c true and
  * compute the remaining buildcost.
@@ -1209,16 +760,6 @@
 	return true;
 }
 
-ImmovableActionData*
-ImmovableActionData::load(FileRead& fr, Immovable& imm, const std::string& name) {
-	// TODO(GunChleoc): Use "construct" only after Build 20
-	if (name == "construction" || name == "construct")
-		return ActConstructData::load(fr, imm);
-	else {
-		log("ImmovableActionData::load: type %s not known", name.c_str());
-		return nullptr;
-	}
-}
 
 /*
 ==============================================================================

=== added file 'src/logic/map_objects/immovable_program.cc'
--- src/logic/map_objects/immovable_program.cc	1970-01-01 00:00:00 +0000
+++ src/logic/map_objects/immovable_program.cc	2019-05-26 08:37:46 +0000
@@ -0,0 +1,342 @@
+/*
+ * Copyright (C) 2002-2019 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 "logic/map_objects/immovable_program.h"
+
+#include <memory>
+
+#include "logic/game.h"
+#include "logic/game_data_error.h"
+#include "logic/map_objects/terrain_affinity.h"
+#include "logic/map_objects/world/world.h"
+#include "logic/mapfringeregion.h"
+#include "logic/player.h"
+#include "sound/note_sound.h"
+
+namespace Widelands {
+
+ImmovableProgram::ImmovableProgram(const std::string& init_name, std::unique_ptr<Action> action) : MapObjectProgram(init_name) {
+	actions_.push_back(std::move(action));
+}
+
+ImmovableProgram::ImmovableProgram(const std::string& init_name,
+                                   const std::vector<std::string>& lines,
+                                   const ImmovableDescr& immovable)
+   : MapObjectProgram(init_name) {
+	for (const std::string& line : lines) {
+		if (line.empty()) {
+			throw GameDataError("Empty line");
+		}
+		try {
+			ProgramParseInput parseinput = parse_program_string(line);
+
+			if (parseinput.name == "animate") {
+				actions_.push_back(std::unique_ptr<Action>(new ActAnimate(parseinput.arguments, immovable)));
+			} else if (parseinput.name == "transform") {
+				actions_.push_back(std::unique_ptr<Action>(new ActTransform(parseinput.arguments, immovable)));
+			} else if (parseinput.name == "grow") {
+				actions_.push_back(std::unique_ptr<Action>(new ActGrow(parseinput.arguments, immovable)));
+			} else if (parseinput.name == "remove") {
+				actions_.push_back(std::unique_ptr<Action>(new ActRemove(parseinput.arguments)));
+			} else if (parseinput.name == "seed") {
+				actions_.push_back(std::unique_ptr<Action>(new ActSeed(parseinput.arguments, immovable)));
+			} else if (parseinput.name == "playsound") {
+				actions_.push_back(std::unique_ptr<Action>(new ActPlaySound(parseinput.arguments)));
+			} else if (parseinput.name == "construct") {
+				actions_.push_back(std::unique_ptr<Action>(new ActConstruct(parseinput.arguments, immovable)));
+			} else {
+				throw GameDataError("Unknown command '%s' in line '%s'", parseinput.name.c_str(), line.c_str());
+			}
+		} catch (const GameDataError& e) {
+			throw GameDataError("Error reading line '%s': %s", line.c_str(), e.what());
+		}
+	}
+	if (actions_.empty()) {
+		throw GameDataError("No actions found");
+	}
+}
+
+ImmovableProgram::Action::~Action() {
+}
+
+ImmovableProgram::ActAnimate::ActAnimate(const std::vector<std::string>& arguments, const ImmovableDescr& descr) {
+	parameters = MapObjectProgram::parse_act_animate(arguments, descr, true);
+}
+
+/// Use convolutuion to make the animation time a random variable with binomial
+/// distribution and the configured time as the expected value.
+void ImmovableProgram::ActAnimate::execute(Game& game, Immovable& immovable) const {
+	immovable.start_animation(game, parameters.animation);
+	immovable.program_step(
+	   game, parameters.duration ? 1 + game.logic_rand() % parameters.duration + game.logic_rand() % parameters.duration : 0);
+}
+
+ImmovableProgram::ActPlaySound::ActPlaySound(const std::vector<std::string>& arguments) {
+	parameters = MapObjectProgram::parse_act_play_sound(arguments, kFxPriorityAllowMultiple - 1);
+}
+
+/** Demand from the g_sound_handler to play a certain sound effect.
+ * Whether the effect actually gets played
+ * is decided only by the sound server*/
+void ImmovableProgram::ActPlaySound::execute(Game& game, Immovable& immovable) const {
+	Notifications::publish(NoteSound(SoundType::kAmbient, parameters.fx, immovable.get_position(), parameters.priority));
+	immovable.program_step(game);
+}
+
+ImmovableProgram::ActTransform::ActTransform(std::vector<std::string>& arguments, const ImmovableDescr& descr) {
+	if (arguments.empty()) {
+		throw GameDataError("Usage: transform=[bob] <name> [<probability>]");
+	}
+	try {
+		bob = false;
+		probability = 0;
+
+		for (uint32_t i = 0; i < arguments.size(); ++i) {
+			if (arguments[i] == "bob") {
+				bob = true;
+			} else if (arguments[i][0] >= '0' && arguments[i][0] <= '9') {
+				probability = read_positive(arguments[i], 254);
+			} else {
+				// TODO(GunChleoc): If would be nice to check if target exists, but we can't guarantee the load order. Maybe in postload() one day.
+				type_name = arguments[i];
+			}
+		}
+		if (type_name == descr.name()) {
+			throw GameDataError("illegal transformation to the same type");
+		}
+	} catch (const WException& e) {
+		throw GameDataError("transform: %s", e.what());
+	}
+}
+
+void ImmovableProgram::ActTransform::execute(Game& game, Immovable& immovable) const {
+	if (probability == 0 || game.logic_rand() % 256 < probability) {
+		Player* player = immovable.get_owner();
+		Coords const c = immovable.get_position();
+		MapObjectDescr::OwnerType owner_type = immovable.descr().owner_type();
+		immovable.remove(game);  //  Now immovable is a dangling reference!
+
+		if (bob) {
+			game.create_ship(c, type_name, player);
+		} else {
+			game.create_immovable_with_name(
+			   c, type_name, owner_type, player, nullptr /* former_building_descr */);
+		}
+	} else
+		immovable.program_step(game);
+}
+
+ImmovableProgram::ActGrow::ActGrow(std::vector<std::string>& arguments, const ImmovableDescr& descr) {
+	if (arguments.size() != 1) {
+		throw GameDataError("Usage: grow=<immovable name>");
+	}
+	if (!descr.has_terrain_affinity()) {
+		throw GameDataError(
+		   "Immovable %s can 'grow', but has no terrain_affinity entry.", descr.name().c_str());
+	}
+
+	// TODO(GunChleoc): If would be nice to check if target exists, but we can't guarantee the load order. Maybe in postload() one day.
+	type_name = arguments.front();
+}
+
+void ImmovableProgram::ActGrow::execute(Game& game, Immovable& immovable) const {
+	const Map& map = game.map();
+	FCoords const f = map.get_fcoords(immovable.get_position());
+	const ImmovableDescr& descr = immovable.descr();
+
+	if ((game.logic_rand() % TerrainAffinity::kPrecisionFactor) <
+	    probability_to_grow(descr.terrain_affinity(), f, map, game.world().terrains())) {
+		MapObjectDescr::OwnerType owner_type = descr.owner_type();
+		Player* owner = immovable.get_owner();
+		immovable.remove(game);  //  Now immovable is a dangling reference!
+		game.create_immovable_with_name(
+		   f, type_name, owner_type, owner, nullptr /* former_building_descr */);
+	} else {
+		immovable.program_step(game);
+	}
+}
+
+/**
+ * remove
+ */
+ImmovableProgram::ActRemove::ActRemove(std::vector<std::string>& arguments) {
+	if (arguments.size() > 1) {
+		throw GameDataError("Usage: remove=[<probability>]");
+	}
+	probability = arguments.empty() ? 0 : read_positive(arguments.front(), 254);
+}
+
+void ImmovableProgram::ActRemove::execute(Game& game, Immovable& immovable) const {
+	if (probability == 0 || game.logic_rand() % 256 < probability)
+		immovable.remove(game);  //  Now immovable is a dangling reference!
+	else
+		immovable.program_step(game);
+}
+
+ImmovableProgram::ActSeed::ActSeed(std::vector<std::string>& arguments, const ImmovableDescr& descr) {
+	if (arguments.size() != 1) {
+		throw GameDataError("Usage: seed=<immovable name>");
+	}
+	if (!descr.has_terrain_affinity()) {
+		throw GameDataError(
+		   "Immovable %s can 'seed', but has no terrain_affinity entry.", descr.name().c_str());
+	}
+
+	// TODO(GunChleoc): If would be nice to check if target exists, but we can't guarantee the load order. Maybe in postload() one day.
+	type_name = arguments.front();
+}
+
+void ImmovableProgram::ActSeed::execute(Game& game, Immovable& immovable) const {
+	const Map& map = game.map();
+	FCoords const f = map.get_fcoords(immovable.get_position());
+	const ImmovableDescr& descr = immovable.descr();
+
+	if ((game.logic_rand() % TerrainAffinity::kPrecisionFactor) <
+	    probability_to_grow(descr.terrain_affinity(), f, map, game.world().terrains())) {
+		// Seed a new tree.
+		MapFringeRegion<> mr(map, Area<>(f, 0));
+		uint32_t fringe_size = 0;
+		do {
+			mr.extend(map);
+			fringe_size += 6;
+		} while (game.logic_rand() % std::numeric_limits<uint8_t>::max() < probability);
+
+		for (uint32_t n = game.logic_rand() % fringe_size; n; --n) {
+			mr.advance(map);
+		}
+
+		const FCoords new_location = map.get_fcoords(mr.location());
+		if (!new_location.field->get_immovable() &&
+		    (new_location.field->nodecaps() & MOVECAPS_WALK) &&
+		    (game.logic_rand() % TerrainAffinity::kPrecisionFactor) <
+		       probability_to_grow(
+		          descr.terrain_affinity(), new_location, map, game.world().terrains())) {
+			game.create_immovable_with_name(mr.location(), type_name, descr.owner_type(),
+			                                nullptr /* owner */, nullptr /* former_building_descr */);
+		}
+	}
+
+	immovable.program_step(game);
+}
+
+ImmovableProgram::ActConstruct::ActConstruct(std::vector<std::string>& arguments, const ImmovableDescr& descr) {
+	if (arguments.size() != 3) {
+		throw GameDataError("Usage: construct=<animation> <build duration> <decay duration>");
+	}
+	try {
+		animation_name_ = arguments[0];
+		if (!descr.is_animation_known(animation_name_)) {
+			throw GameDataError("Unknown animation '%s' in immovable program for immovable '%s'",
+			                    animation_name_.c_str(), descr.name().c_str());
+		}
+
+		buildtime_ = read_positive(arguments[1]);
+		decaytime_ = read_positive(arguments[2]);
+	} catch (const WException& e) {
+		throw GameDataError("construct: %s", e.what());
+	}
+}
+
+constexpr uint8_t kCurrentPacketVersionConstructionData = 1;
+
+
+const char* ActConstructData::name() const {
+	return "construct";
+}
+void ActConstructData::save(FileWrite& fw, Immovable& imm) const {
+	fw.unsigned_8(kCurrentPacketVersionConstructionData);
+	delivered.save(fw, imm.get_owner()->tribe());
+}
+
+ActConstructData* ActConstructData::load(FileRead& fr, Immovable& imm) {
+	ActConstructData* d = new ActConstructData;
+
+	try {
+		uint8_t packet_version = fr.unsigned_8();
+		if (packet_version == kCurrentPacketVersionConstructionData) {
+			d->delivered.load(fr, imm.get_owner()->tribe());
+		} else {
+			throw UnhandledVersionError(
+			   "ActConstructData", packet_version, kCurrentPacketVersionConstructionData);
+		}
+	} catch (const WException& e) {
+		delete d;
+		d = nullptr;
+		throw GameDataError("ActConstructData: %s", e.what());
+	}
+
+	return d;
+}
+
+
+void ImmovableProgram::ActConstruct::execute(Game& g, Immovable& imm) const {
+	ActConstructData* d = imm.get_action_data<ActConstructData>();
+	if (!d) {
+		// First execution
+		d = new ActConstructData;
+		imm.set_action_data(d);
+
+		imm.start_animation(g, imm.descr().get_animation(animation_name_, &imm));
+		imm.anim_construction_total_ = imm.descr().buildcost().total();
+	} else {
+		// Perhaps we are called due to the construction timeout of the last construction step
+		Buildcost remaining;
+		imm.construct_remaining_buildcost(g, &remaining);
+		if (remaining.empty()) {
+			imm.program_step(g);
+			return;
+		}
+
+		// Otherwise, this is a decay timeout
+		uint32_t totaldelivered = 0;
+		for (Buildcost::const_iterator it = d->delivered.begin(); it != d->delivered.end(); ++it)
+			totaldelivered += it->second;
+
+		if (!totaldelivered) {
+			imm.remove(g);
+			return;
+		}
+
+		uint32_t randdecay = g.logic_rand() % totaldelivered;
+		for (Buildcost::iterator it = d->delivered.begin(); it != d->delivered.end(); ++it) {
+			if (randdecay < it->second) {
+				it->second--;
+				break;
+			}
+
+			randdecay -= it->second;
+		}
+
+		imm.anim_construction_done_ = d->delivered.total();
+	}
+
+	imm.program_step_ = imm.schedule_act(g, decaytime_);
+}
+
+ImmovableActionData*
+ImmovableActionData::load(FileRead& fr, Immovable& imm, const std::string& name) {
+	// TODO(GunChleoc): Use "construct" only after Build 20
+	if (name == "construction" || name == "construct")
+		return ActConstructData::load(fr, imm);
+	else {
+		log("ImmovableActionData::load: type %s not known", name.c_str());
+		return nullptr;
+	}
+}
+}  // namespace Widelands

=== modified file 'src/logic/map_objects/immovable_program.h'
--- src/logic/map_objects/immovable_program.h	2019-04-26 12:46:40 +0000
+++ src/logic/map_objects/immovable_program.h	2019-05-26 08:37:46 +0000
@@ -21,21 +21,19 @@
 #define WL_LOGIC_MAP_OBJECTS_IMMOVABLE_PROGRAM_H
 
 #include <cstring>
+#include <memory>
 #include <string>
 
 #include "base/macros.h"
 
-/*
- * Implementation is in immovable.cc
- */
-
 #include "logic/map_objects/buildcost.h"
 #include "logic/map_objects/immovable.h"
+#include "logic/map_objects/map_object_program.h"
 
 namespace Widelands {
 
 /// Ordered sequence of actions (at least 1). Has a name.
-struct ImmovableProgram {
+struct ImmovableProgram : public MapObjectProgram {
 
 	/// Can be executed on an Immovable.
 	struct Action {
@@ -63,15 +61,14 @@
 	/// will not be stopped by this command. It will run until another animation
 	/// is started.)
 	struct ActAnimate : public Action {
-		ActAnimate(char* parameters, ImmovableDescr&);
+		ActAnimate(const std::vector<std::string>& arguments, const ImmovableDescr&);
 		void execute(Game&, Immovable&) const override;
-		const std::string& animation() const {
-			return animation_name_;
+		uint32_t animation() const {
+			return parameters.animation;
 		}
 
 	private:
-		std::string animation_name_;
-		Duration duration_;
+		AnimationParameters parameters;
 	};
 
 	/// Transforms the immovable into another immovable or into a bob
@@ -91,19 +88,18 @@
 	///    name:
 	///       name of the replacement object
 	struct ActTransform : public Action {
-		ActTransform(char* parameters, ImmovableDescr&);
+		ActTransform(std::vector<std::string>& arguments, const ImmovableDescr&);
 		void execute(Game&, Immovable&) const override;
 
 	private:
 		std::string type_name;
 		bool bob;
-		bool tribe;
 		uint8_t probability;
 	};
 
 	/// Like ActTransform but the probability is determined by the suitability.
 	struct ActGrow : public Action {
-		ActGrow(char* parameters, ImmovableDescr&);
+		ActGrow(std::vector<std::string>& arguments, const ImmovableDescr&);
 		void execute(Game&, Immovable&) const override;
 
 	private:
@@ -112,7 +108,7 @@
 	};
 
 	struct ActRemove : public Action {
-		ActRemove(char* parameters, ImmovableDescr&);
+		ActRemove(std::vector<std::string>& arguments);
 		void execute(Game&, Immovable&) const override;
 
 	private:
@@ -120,7 +116,7 @@
 	};
 
 	struct ActSeed : public Action {
-		ActSeed(char* parameters, ImmovableDescr&);
+		ActSeed(std::vector<std::string>& arguments, const ImmovableDescr&);
 		void execute(Game&, Immovable&) const override;
 
 	private:
@@ -142,12 +138,11 @@
 	/// Plays the specified sound effect with the specified priority. Whether the
 	/// sound effect is actually played is determined by the sound handler.
 	struct ActPlaySound : public Action {
-		ActPlaySound(char* parameters, const ImmovableDescr&);
+		ActPlaySound(const std::vector<std::string>& arguments);
 		void execute(Game&, Immovable&) const override;
 
 	private:
-		FxId fx;
-		uint8_t priority;
+		PlaySoundParameters parameters;
 	};
 
 	/**
@@ -164,7 +159,7 @@
 	 *       Time until construction decays one step if no progress has been made.
 	 */
 	struct ActConstruct : public Action {
-		ActConstruct(char* parameters, ImmovableDescr&);
+		ActConstruct(std::vector<std::string>& arguments, const ImmovableDescr&);
 		void execute(Game&, Immovable&) const override;
 
 		Duration buildtime() const {
@@ -181,24 +176,16 @@
 	};
 
 	/// Create a program with a single action.
-	ImmovableProgram(char const* const init_name, Action* const action) : name_(init_name) {
-		actions_.push_back(action);
-	}
+	ImmovableProgram(const std::string& init_name, std::unique_ptr<Action> action);
 
-	// Create an immovable program from a number of lines.
+	/// Create an immovable program from a number of lines.
 	ImmovableProgram(const std::string& init_name,
 	                 const std::vector<std::string>& lines,
-	                 ImmovableDescr* immovable);
+	                 const ImmovableDescr& immovable);
 
 	~ImmovableProgram() {
-		for (Action* action : actions_) {
-			delete action;
-		}
 	}
 
-	const std::string& name() const {
-		return name_;
-	}
 	size_t size() const {
 		return actions_.size();
 	}
@@ -207,14 +194,8 @@
 		return *actions_[idx];
 	}
 
-	using Actions = std::vector<Action*>;
-	const Actions& actions() const {
-		return actions_;
-	}
-
 private:
-	std::string name_;
-	Actions actions_;
+	std::vector<std::unique_ptr<Action>> actions_;
 };
 
 struct ImmovableActionData {
@@ -224,10 +205,18 @@
 	}
 
 	virtual const char* name() const = 0;
-	virtual void save(FileWrite& fw, Immovable& imm) = 0;
+	virtual void save(FileWrite& fw, Immovable& imm) const = 0;
 
 	static ImmovableActionData* load(FileRead& fr, Immovable& imm, const std::string& name);
 };
+
+struct ActConstructData : ImmovableActionData {
+	const char* name() const override;
+	void save(FileWrite& fw, Immovable& imm) const override;
+	static ActConstructData* load(FileRead& fr, Immovable& imm);
+
+	Buildcost delivered;
+};
 }  // namespace Widelands
 
 #endif  // end of include guard: WL_LOGIC_MAP_OBJECTS_IMMOVABLE_PROGRAM_H

=== modified file 'src/logic/map_objects/map_object.h'
--- src/logic/map_objects/map_object.h	2019-05-26 08:37:42 +0000
+++ src/logic/map_objects/map_object.h	2019-05-26 08:37:46 +0000
@@ -125,6 +125,7 @@
 	}
 
 	virtual uint32_t get_animation(const std::string& animname, const MapObject* mo) const;
+
 	uint32_t main_animation() const;
 	std::string get_animation_name(uint32_t) const;  ///< needed for save, debug
 

=== added file 'src/logic/map_objects/map_object_program.cc'
--- src/logic/map_objects/map_object_program.cc	1970-01-01 00:00:00 +0000
+++ src/logic/map_objects/map_object_program.cc	2019-05-26 08:37:46 +0000
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2002-2019 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 "logic/map_objects/map_object_program.h"
+
+#include "io/filesystem/layered_filesystem.h"
+#include "logic/game_data_error.h"
+#include "logic/map_objects/map_object.h"
+#include "sound/sound_handler.h"
+
+namespace Widelands {
+
+MapObjectProgram::MapObjectProgram(const std::string& init_name) : name_ (init_name) {}
+
+const std::string& MapObjectProgram::name() const {
+	return name_;
+}
+
+std::vector<std::string> MapObjectProgram::split_string(const std::string& s, const char* const separators) {
+	std::vector<std::string> result;
+	for (std::string::size_type pos = 0, endpos;
+	     (pos = s.find_first_not_of(separators, pos)) != std::string::npos; pos = endpos) {
+		endpos = s.find_first_of(separators, pos);
+		result.push_back(s.substr(pos, endpos - pos));
+	}
+	return result;
+}
+
+
+unsigned int MapObjectProgram::read_int(const std::string& input, int min_value, int max_value) {
+	unsigned int  result = 0U;
+	char* endp;
+	long int const value = strtol(input.c_str(), &endp, 0);
+	result = value;
+	if (*endp || result != value) {
+		throw GameDataError("Expected a number but found \"%s\"", input.c_str());
+	}
+	if (value < min_value) {
+		throw GameDataError("Expected a number >= %d but found \"%s\"", min_value, input.c_str());
+	}
+	if (value > max_value) {
+		throw GameDataError("Expected a number <= %d but found \"%s\"", max_value, input.c_str());
+	}
+	return result;
+}
+
+unsigned int MapObjectProgram::read_positive(const std::string& input, int max_value) {
+	return read_int(input, 1, max_value);
+}
+
+MapObjectProgram::ProgramParseInput MapObjectProgram::parse_program_string(const std::string& line) {
+	const std::pair<std::string, std::string> key_values = MapObjectProgram::read_key_value_pair(line, '=');
+	return ProgramParseInput{key_values.first, split_string(key_values.second, " \t\n")};
+}
+
+const std::pair<std::string, std::string> MapObjectProgram::read_key_value_pair(const std::string& input, const char separator, const std::string& default_value, const std::string& expected_key) {
+	const size_t idx = input.find(separator);
+	const std::string key = input.substr(0, idx);
+
+	if (!expected_key.empty()) {
+		if (idx == input.npos) {
+			throw GameDataError("Empty value in '%s' for separator '%c'\n", input.c_str(), separator);
+		}
+		if (key != expected_key) {
+			throw GameDataError("Expected key '%s' but found '%s' in '%s'\n", expected_key.c_str(), key.c_str(), input.c_str());
+		}
+	}
+
+	return std::make_pair(key, idx == input.npos ? default_value : input.substr(idx + 1));
+}
+
+MapObjectProgram::AnimationParameters MapObjectProgram::parse_act_animate(const std::vector<std::string>& arguments, const MapObjectDescr& descr, bool is_idle_allowed) {
+	if (arguments.size() < 1 || arguments.size() > 2) {
+		throw GameDataError("Usage: animate=<name> <duration>");
+	}
+
+	AnimationParameters result;
+	const std::string animation_name = arguments.at(0);
+
+	if (!is_idle_allowed && animation_name == "idle") {
+		throw GameDataError("'idle' animation is default; calling is not allowed");
+	}
+	if (!descr.is_animation_known(animation_name)) {
+		throw GameDataError("Unknown animation '%s'", animation_name.c_str());
+	}
+	result.animation = descr.get_animation(animation_name, nullptr);
+
+	if (arguments.size() == 2) {
+		result.duration = read_positive(arguments.at(1));
+	}
+	return result;
+}
+
+MapObjectProgram::PlaySoundParameters MapObjectProgram::parse_act_play_sound(const std::vector<std::string>& arguments, uint8_t default_priority) {
+	if (arguments.size() < 2 || arguments.size() > 3) {
+		throw GameDataError("Usage: playsound <sound_dir> <sound_name> [priority]");
+	}
+	PlaySoundParameters result;
+	result.fx = SoundHandler::register_fx(SoundType::kAmbient, arguments.at(0));
+
+	result.priority = arguments.size() == 2 ? read_positive(arguments.at(1)) : default_priority;
+	if (result.priority < kFxPriorityLowest) {
+		throw GameDataError("Minmum priority for sounds is %d, but only %d was specified for %s",
+		                    kFxPriorityLowest, result.priority, arguments.at(0).c_str());
+	}
+	return result;
+}
+
+}  // namespace Widelands

=== added file 'src/logic/map_objects/map_object_program.h'
--- src/logic/map_objects/map_object_program.h	1970-01-01 00:00:00 +0000
+++ src/logic/map_objects/map_object_program.h	2019-05-26 08:37:46 +0000
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2002-2019 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_LOGIC_MAP_OBJECTS_MAP_OBJECT_PROGRAM_H
+#define WL_LOGIC_MAP_OBJECTS_MAP_OBJECT_PROGRAM_H
+
+#include <limits>
+#include <string>
+#include <vector>
+
+#include "logic/widelands.h"
+#include "sound/constants.h"
+
+namespace Widelands {
+
+struct MapObjectDescr;
+
+/// Superclass for Worker, immovable and productionsite programs. Includes a program name and diverse parsing convenience functions. The creation and execution of program actions is left to the sub-classes.
+struct MapObjectProgram {
+	const std::string& name() const;
+
+	explicit MapObjectProgram(const std::string& init_name);
+	virtual ~MapObjectProgram() = default;
+
+protected:
+	/// Splits a string by separators.
+	/// \note This ignores empty elements, so do not use this for example to split
+	/// a string with newline characters into lines, because it would ignore empty
+	/// lines.
+	static std::vector<std::string> split_string(const std::string&, char const* separators);
+
+	/// Reads an int value from a string. Throws a GameDataError if 'min_value' or 'max_value' are exceeded
+	static unsigned int read_int(const std::string& input, int min_value, int max_value = std::numeric_limits<int32_t>::max());
+	/// Same as 'read_int', with 'min_value' == 1
+	static unsigned int read_positive(const std::string& input, int max_value = std::numeric_limits<int32_t>::max());
+
+	/**
+	 * @brief Reads a key-value pair from a string using the given separator, e.g. "attrib:tree", "meat:2", "return=skipped unless economy needs meal"
+	 * @param input The string to parse
+	 * @param separator The separator for splitting the string, e.g. ':' or '='
+	 * @param default_value A default to assign to the right-hand value if the separator is not found
+	 * @param expected_key If this is not empty, the left-hand key must match this string
+	 * @return A key, value pair
+	 */
+	static const std::pair<std::string, std::string> read_key_value_pair(const std::string& input, const char separator, const std::string& default_value = "", const std::string& expected_key = "");
+
+	/// Left-hand and right-hand elements of a line in a program, e.g. parsed from "return=skipped unless economy needs meal"
+	struct ProgramParseInput {
+		/// Program name, e.g. "return"
+		std::string name;
+		/// Program arguments, e.g. { "skipped", "unless", "economy", "needs", "meal" }
+		std::vector<std::string> arguments;
+	};
+	/// Reads the program name and arguments from a string
+	static ProgramParseInput parse_program_string(const std::string& line);
+
+	/// Animation information
+	struct AnimationParameters {
+		/// Animation ID
+		uint32_t animation = 0;
+		/// Animation duration. 0 will play the animation forever.
+		Duration duration = 0;
+	};
+	/// Parses the arguments for an animation action, e.g. { "working", "24000" }. If 'is_idle_allowed' == false, throws a GameDataError if the animation is called "idle".
+	static AnimationParameters parse_act_animate(const std::vector<std::string>& arguments, const MapObjectDescr& descr, bool is_idle_allowed);
+
+	/// Sound effect information
+	struct PlaySoundParameters {
+		/// Sound effect ID
+		FxId fx;
+		/// Sound effect priority
+		uint8_t priority = 0;
+	};
+	/// Parses the arguments for a play_sound action, e.g. { "sound/smiths", "sharpening", "120" }
+	static PlaySoundParameters parse_act_play_sound(const std::vector<std::string>& arguments, uint8_t default_priority);
+
+private:
+	const std::string name_;
+};
+}  // namespace Widelands
+
+#endif  // end of include guard: WL_LOGIC_MAP_OBJECTS_MAP_OBJECT_PROGRAM_H

=== modified file 'src/logic/map_objects/tribes/production_program.cc'
--- src/logic/map_objects/tribes/production_program.cc	2019-05-18 11:58:43 +0000
+++ src/logic/map_objects/tribes/production_program.cc	2019-05-26 08:37:46 +0000
@@ -20,10 +20,6 @@
 #include "logic/map_objects/tribes/production_program.h"
 
 #include <memory>
-#include <sstream>
-
-#include <boost/algorithm/string.hpp>
-#include <boost/format.hpp>
 
 #include "base/i18n.h"
 #include "base/macros.h"
@@ -32,7 +28,6 @@
 #include "economy/economy.h"
 #include "economy/flag.h"
 #include "economy/input_queue.h"
-#include "helper.h"
 #include "io/filesystem/layered_filesystem.h"
 #include "logic/game.h"
 #include "logic/game_data_error.h"
@@ -56,135 +51,28 @@
 namespace Widelands {
 
 namespace {
-
-/// Matches the string that candidate points to against the string that
-/// template points to. Stops at when reaching a null character or the
-/// character terminator. If a match is found, candidate is moved beyond the
-/// matched part.
-///
-/// example:
-///    char const * candidate = "return   75";
-///    bool const result = match(candidate, "return");
-/// now candidate points to "   75" and result is true
-bool match(char*& candidate, const char* pattern) {
-	for (char* p = candidate;; ++p, ++pattern)
-		if (!*pattern) {
-			candidate = p;
-			return true;
-		} else if (*p != *pattern)
-			break;
-	return false;
-}
-
-/// Skips a sequence of consecutive characters with the value c, starting at p.
-/// Throws WException if no characters were skipped.
-void force_skip(char*& p, char const c = ' ') {
-	char* t = p;
-	while (*t == c)
-		++t;
-	if (p < t)
-		p = t;
-	else
-		throw wexception("expected '%c' but found \"%s\"", c, p);
-}
-
-/// Skips a sequence of consecutive characters with the value c, starting at p.
-/// Returns whether any characters were skipped.
-bool skip(char*& p, char const c = ' ') {
-	char* t = p;
-	while (*t == c)
-		++t;
-	if (p < t) {
-		p = t;
-		return true;
-	} else
-		return false;
-}
-
-/// Combines match and force_skip.
-///
-/// example:
-///    char const * candidate = "return   75";
-///    bool const result = match_force_skip(candidate, "return");
-/// now candidate points to "75" and result is true
-///
-/// example:
-///   char const * candidate = "return75";
-///    bool const result = match_force_skip(candidate, "return");
-/// throws WException
-bool match_force_skip(char*& candidate, const char* pattern) {
-	for (char* p = candidate;; ++p, ++pattern)
-		if (!*pattern) {
-			force_skip(p);
-			candidate = p;
-			return true;
-		} else if (*p != *pattern)
-			return false;
-
-	NEVER_HERE();
-}
-
-ProductionProgram::ActReturn::Condition* create_economy_condition(char*& parameters,
-                                                                  const Tribes& tribes) {
-	try {
-		if (match_force_skip(parameters, "needs"))
-			try {
-				bool reached_end;
-				char const* const type_name = next_word(parameters, reached_end);
-				const DescriptionIndex& wareindex = tribes.ware_index(type_name);
-				if (tribes.ware_exists(wareindex)) {
-					for (size_t i = 0; i < tribes.nrtribes(); ++i) {
-						const TribeDescr& tribe_descr = *tribes.get_tribe_descr(i);
-						if (tribe_descr.has_ware(wareindex)) {
-							tribes.set_ware_type_has_demand_check(wareindex, tribe_descr.name());
-						}
-					}
-					return new ProductionProgram::ActReturn::EconomyNeedsWare(wareindex);
-				} else if (tribes.worker_exists(tribes.worker_index(type_name))) {
-					const DescriptionIndex& workerindex = tribes.worker_index(type_name);
-					for (size_t i = 0; i < tribes.nrtribes(); ++i) {
-						const TribeDescr* tribe_descr = tribes.get_tribe_descr(i);
-						if (tribe_descr->has_worker(workerindex)) {
-							tribes.set_worker_type_has_demand_check(workerindex);
-						}
-					}
-					return new ProductionProgram::ActReturn::EconomyNeedsWorker(workerindex);
-				} else
-					throw GameDataError(
-					   "expected %s but found \"%s\"", "ware type or worker type", type_name);
-			} catch (const WException& e) {
-				throw GameDataError("needs: %s", e.what());
-			}
-		else
-			throw GameDataError("expected %s but found \"%s\"", "\"needs\"", parameters);
-	} catch (const WException& e) {
-		throw GameDataError("economy: %s", e.what());
-	}
-}
-
-ProductionProgram::ActReturn::Condition*
-create_site_condition(char*& parameters, const ProductionSiteDescr& descr, const Tribes& tribes) {
-	try {
-		if (match_force_skip(parameters, "has"))
-			return new ProductionProgram::ActReturn::SiteHas(parameters, descr, tribes);
-		else
-			throw GameDataError("expected %s but found \"%s\"", "\"has\"", parameters);
-	} catch (const WException& e) {
-		throw GameDataError("site: %s", e.what());
-	}
-}
-
-ProductionProgram::ActReturn::Condition* create_workers_condition(char*& parameters) {
-	try {
-		if (match(parameters, "need experience"))
-			return new ProductionProgram::ActReturn::WorkersNeedExperience;
-		else
-			throw GameDataError("expected %s but found \"%s\"", "\"need experience\"", parameters);
-	} catch (const WException& e) {
-		throw GameDataError("workers: %s", e.what());
-	}
-}
-
+/// If the iterator contents match the string, increment the iterator. Returns whether it matched.
+bool match_and_skip(std::vector<std::string>::const_iterator& it, const std::string& matchme) {
+	bool result = *it == matchme;
+	if (result) {
+		++it;
+	}
+	return result;
+}
+
+ProductionProgram::ActReturn::Condition* create_economy_condition(const std::string& item, const ProductionSiteDescr& descr, const Tribes& tribes) {
+	DescriptionIndex index = tribes.ware_index(item);
+	if (tribes.ware_exists(index)) {
+		descr.ware_demand_checks()->insert(index);
+		return new ProductionProgram::ActReturn::EconomyNeedsWare(index);
+	} else if (tribes.worker_exists(tribes.worker_index(item))) {
+		index = tribes.worker_index(item);
+		descr.worker_demand_checks()->insert(index);
+		return new ProductionProgram::ActReturn::EconomyNeedsWorker(index);
+	} else {
+		throw GameDataError("Expected ware or worker type but found '%s'", item.c_str());
+	}
+}
 }  // namespace
 
 ProductionProgram::Action::~Action() {
@@ -197,90 +85,101 @@
 void ProductionProgram::Action::building_work_failed(Game&, ProductionSite&, Worker&) const {
 }
 
-void ProductionProgram::parse_ware_type_group(char*& parameters,
-                                              WareTypeGroup& group,
-                                              const Tribes& tribes,
-                                              const BillOfMaterials& input_wares,
-                                              const BillOfMaterials& input_workers) {
-	std::set<std::pair<DescriptionIndex, WareWorker>>::iterator last_insert_pos = group.first.end();
-	uint8_t count = 1;
-	uint8_t count_max = 0;
-	for (;;) {
-		char const* ware = parameters;
-		while (*parameters && *parameters != ',' && *parameters != ':' && *parameters != ' ')
-			++parameters;
-		char const terminator = *parameters;
-		*parameters = '\0';
-
-		// Try as ware
-		WareWorker type = wwWARE;
-		const BillOfMaterials* input_list = &input_wares;
-		DescriptionIndex ware_index = tribes.ware_index(ware);
-		if (!tribes.ware_exists(ware_index)) {
-			ware_index = tribes.worker_index(ware);
-			if (tribes.worker_exists(ware_index)) {
-				// It is a worker
-				type = wwWORKER;
-				input_list = &input_workers;
-			} else {
-				throw GameDataError("Unknown ware or worker type \"%s\"", ware);
-			}
-		}
-
-		bool found = false;
-		for (const WareAmount& input : *input_list) {
-			if (input.first == ware_index) {
-				count_max += input.second;
-				found = true;
-				break;
-			}
-		}
-		if (!found) {
-			throw GameDataError("%s is not declared as an input (\"%s=<count>\" was not "
-			                    "found in the [inputs] section)",
-			                    ware, ware);
-		}
-
-		if (group.first.size() && ware_index <= group.first.begin()->first)
-			throw GameDataError("wrong order of ware types within group: ware type %s appears "
-			                    "after ware type %s (fix order!)",
-			                    ware,
-			                    tribes.get_ware_descr(group.first.begin()->first)->name().c_str());
-		last_insert_pos = group.first.insert(last_insert_pos, std::make_pair(ware_index, type));
-		*parameters = terminator;
-		switch (terminator) {
-		case ':': {
-			++parameters;
-			char* endp;
-			unsigned long long int const value = strtoull(parameters, &endp, 0);
-			count = value;
-			if ((*endp && *endp != ' ') || value < 1 || count != value)
-				throw GameDataError("expected %s but found \"%s\"", "count", parameters);
-			parameters = endp;
-			if (count_max < count)
-				throw GameDataError("group count is %u but (total) input storage capacity of "
-				                    "the specified ware type(s) is only %u, so the group can "
+ProductionProgram::Groups ProductionProgram::parse_ware_type_groups(std::vector<std::string>::const_iterator begin, std::vector<std::string>::const_iterator end, const ProductionSiteDescr& descr, const Tribes& tribes) {
+	ProductionProgram::Groups result;
+
+	for (auto& it = begin; it != end; ++it) {
+		const std::pair<std::string, std::string> names_to_amount = read_key_value_pair(*it, ':', "1");
+		const uint8_t amount = read_positive(names_to_amount.second);
+		uint8_t max_amount = 0;
+		std::set<std::pair<DescriptionIndex, WareWorker>> ware_worker_names;
+		for (const std::string& item_name : split_string(names_to_amount.first, ",")) {
+			// Try as ware
+			WareWorker type = wwWARE;
+			DescriptionIndex item_index = tribes.ware_index(item_name);
+			if (!tribes.ware_exists(item_index)) {
+				item_index = tribes.worker_index(item_name);
+				if (tribes.worker_exists(item_index)) {
+					// It is a worker
+					type = wwWORKER;
+				} else {
+					throw GameDataError("Expected ware or worker type but found '%s'", item_name.c_str());
+				}
+			}
+
+			// Sanity checks
+			bool found = false;
+			const BillOfMaterials& inputs = (type == wwWARE) ? descr.input_wares() : descr.input_workers();
+			for (const WareAmount& input : inputs) {
+				if (input.first == item_index) {
+					max_amount += input.second;
+					found = true;
+					break;
+				}
+			}
+			if (!found) {
+				throw GameDataError("%s was not declared in the building's 'inputs' table", item_name.c_str());
+			}
+
+			if (max_amount < amount) {
+				throw GameDataError("Ware/worker count is %u but (total) input storage capacity of "
+				                    "the specified ware type(s) is only %u, so the ware/worker requirement can "
 				                    "never be fulfilled by the site",
-				                    count, count_max);
+				                    static_cast<unsigned int>(amount), static_cast<unsigned int>(max_amount));
+			}
+			// Add item
+			ware_worker_names.insert(std::make_pair(item_index, type));
 		}
-			FALLS_THROUGH;
-		case '\0':
-		case ' ':
-			group.second = count;
-			return;
-		case ',':
-			++parameters;
-			break;
+		// Add set
+		result.push_back(std::make_pair(ware_worker_names, amount));
+	}
+	if (result.empty()) {
+		throw GameDataError("No wares or workers found");
+	}
+	return result;
+}
+
+
+BillOfMaterials ProductionProgram::parse_bill_of_materials(const std::vector<std::string>& arguments,
+																  WareWorker ww,
+																  const ProductionSiteDescr& descr,
+																  const Tribes& tribes) {
+	BillOfMaterials result;
+	for (const std::string& argument : arguments) {
+		const std::pair<std::string, std::string> produceme = read_key_value_pair(argument, ':', "1");
+
+		const DescriptionIndex index = ww == WareWorker::wwWARE ?
+										   tribes.safe_ware_index(produceme.first) :
+										   tribes.safe_worker_index(produceme.first);
+
+		// Verify the building outputs
+		switch (ww) {
+		case WareWorker::wwWARE:
+			if (!descr.is_output_ware_type(index)) {
+				throw GameDataError("Ware '%s' is not listed in the building's 'outputs' table",
+									produceme.first.c_str());
+			} break;
+		case WareWorker::wwWORKER:
+			if (!descr.is_output_worker_type(index)) {
+				throw GameDataError("Worker '%s' is not listed in the building's 'outputs' table",
+									produceme.first.c_str());
+			} break;
 		default:
-			// scan for terminator should ensure that this cannot happen
 			NEVER_HERE();
 		}
+
+		result.push_back(std::make_pair(index, read_positive(produceme.second)));
 	}
+	return result;
 }
 
 ProductionProgram::ActReturn::Condition::~Condition() {
 }
 
+ProductionProgram::ActReturn::Negation::Negation(std::vector<std::string>::const_iterator& begin, std::vector<std::string>::const_iterator& end, const ProductionSiteDescr& descr, const Tribes& tribes)
+   : operand(create_condition(begin, end, descr, tribes)) {
+}
+
 ProductionProgram::ActReturn::Negation::~Negation() {
 	delete operand;
 }
@@ -343,13 +242,14 @@
 	return result;
 }
 
-ProductionProgram::ActReturn::SiteHas::SiteHas(char*& parameters,
+ProductionProgram::ActReturn::SiteHas::SiteHas(std::vector<std::string>::const_iterator begin,
+                                               std::vector<std::string>::const_iterator end,
                                                const ProductionSiteDescr& descr,
                                                const Tribes& tribes) {
 	try {
-		parse_ware_type_group(parameters, group, tribes, descr.input_wares(), descr.input_workers());
-	} catch (const WException& e) {
-		throw GameDataError("has ware_type1[,ware_type2[,...]][:N]: %s", e.what());
+		group = parse_ware_type_groups(begin, end, descr, tribes).front();
+	} catch (const GameDataError& e) {
+		throw GameDataError("Expected <ware or worker>[,<ware or worker>[,...]][:<amount>] after 'site has' but got %s", e.what());
 	}
 }
 bool ProductionProgram::ActReturn::SiteHas::evaluate(const ProductionSite& ps) const {
@@ -439,75 +339,87 @@
 }
 
 ProductionProgram::ActReturn::Condition* ProductionProgram::ActReturn::create_condition(
-   char*& parameters, const ProductionSiteDescr& descr, const Tribes& tribes) {
+   std::vector<std::string>::const_iterator& begin, std::vector<std::string>::const_iterator& end, const ProductionSiteDescr& descr, const Tribes& tribes) {
+	if (begin == end) {
+		throw GameDataError("Expected a condition after '%s'", (begin - 1)->c_str());
+	}
 	try {
-		if (match_force_skip(parameters, "not"))
-			return new ActReturn::Negation(parameters, descr, tribes);
-		else if (match_force_skip(parameters, "economy"))
-			return create_economy_condition(parameters, tribes);
-		else if (match_force_skip(parameters, "site"))
-			return create_site_condition(parameters, descr, tribes);
-		else if (match_force_skip(parameters, "workers"))
-			return create_workers_condition(parameters);
-		else
+		if (match_and_skip(begin, "not")) {
+			return new ActReturn::Negation(begin, end, descr, tribes);
+		} else if (match_and_skip(begin, "economy")) {
+			if (!match_and_skip(begin, "needs")) {
+				throw GameDataError("Expected 'needs' after 'economy' but found '%s'", begin->c_str());
+			}
+			return create_economy_condition(*begin, descr, tribes);
+		} else if (match_and_skip(begin, "site")) {
+			if (!match_and_skip(begin, "has")) {
+				throw GameDataError("Expected 'has' after 'site' but found '%s'", begin->c_str());
+			}
+			return new ProductionProgram::ActReturn::SiteHas(begin, end, descr, tribes);
+		} else if (match_and_skip(begin, "workers")) {
+			if (!match_and_skip(begin, "need")) {
+				throw GameDataError("Expected 'need experience' after 'workers' but found '%s'", begin->c_str());
+			}
+			if (!match_and_skip(begin, "experience")) {
+				throw GameDataError("Expected 'experience' after 'workers need' but found '%s'", begin->c_str());
+			}
+			return new ProductionProgram::ActReturn::WorkersNeedExperience();
+		} else {
 			throw GameDataError(
-			   "expected %s but found \"%s\"", "{\"not\"|\"economy\"|\"workers\"}", parameters);
+			   "Expected not|economy|site|workers after '%s' but found '%s'", (begin - 1)->c_str(), begin->c_str());
+		}
 	} catch (const WException& e) {
-		throw GameDataError("invalid condition: %s", e.what());
+		throw GameDataError("Invalid condition. %s", e.what());
 	}
 }
 
-ProductionProgram::ActReturn::ActReturn(char* parameters,
+ProductionProgram::ActReturn::ActReturn(const std::vector<std::string>& arguments,
                                         const ProductionSiteDescr& descr,
                                         const Tribes& tribes) {
-	try {
-		if (match(parameters, "failed"))
-			result_ = ProgramResult::kFailed;
-		else if (match(parameters, "completed"))
-			result_ = ProgramResult::kCompleted;
-		else if (match(parameters, "skipped"))
-			result_ = ProgramResult::kSkipped;
-		else if (match(parameters, "no_stats"))
-			result_ = ProgramResult::kNone;
-		else
-			throw GameDataError("expected %s but found \"%s\"",
-			                    "{\"failed\"|\"completed\"|\"skipped\"|\"no_stats\"}", parameters);
-
-		if (skip(parameters)) {
-			if (match_force_skip(parameters, "when")) {
-				is_when_ = true;
-				for (;;) {
-					conditions_.push_back(create_condition(parameters, descr, tribes));
-					if (*parameters) {
-						skip(parameters);
-						if (!match_force_skip(parameters, "and"))
-							throw GameDataError("expected \"%s\" or end of input", "and");
-					} else
-						break;
-				}
-			} else if (match_force_skip(parameters, "unless")) {
-				is_when_ = false;
-				for (;;) {
-					if (!*parameters)
-						throw GameDataError("expected condition at end of input");
-					conditions_.push_back(create_condition(parameters, descr, tribes));
-					if (*parameters) {
-						skip(parameters);
-						if (!match_force_skip(parameters, "or"))
-							throw GameDataError("expected \"%s\" or end of input", "or");
-					} else
-						break;
-				}
-			} else
-				throw GameDataError(
-				   "expected %s but found \"%s\"", "{\"when\"|\"unless\"}", parameters);
-		} else if (*parameters)
-			throw GameDataError("expected %s but found \"%s\"", ("space or end of input"), parameters);
-		else
-			is_when_ = true;
-
-	} catch (const WException& e) {
-		throw GameDataError("return: %s", e.what());
+	if (arguments.empty()) {
+		throw GameDataError("Usage: return=failed|completed|skipped|no_stats [when|unless <conditions>]");
+	}
+	auto begin = arguments.begin();
+
+	if (match_and_skip(begin, "failed")) {
+		result_ = ProgramResult::kFailed;
+	} else if (match_and_skip(begin, "completed")) {
+		result_ = ProgramResult::kCompleted;
+	} else if (match_and_skip(begin, "skipped")) {
+		result_ = ProgramResult::kSkipped;
+	} else if (match_and_skip(begin, "no_stats")) {
+		result_ = ProgramResult::kNone;
+	} else {
+		throw GameDataError("Usage: return=failed|completed|skipped|no_stats [when|unless <conditions>]");
+	}
+
+
+	// Parse all arguments starting from the given iterator into our 'conditions_', splitting individual conditions by the given 'separator'
+	auto parse_conditions = [this, &descr, &tribes] (const std::vector<std::string>& args, std::vector<std::string>::const_iterator it, const std::string& separator) {
+		while (it != args.end()) {
+			auto end = it + 1;
+			while (end != args.end() && *end != separator) {
+				++end;
+			}
+			if (it == end) {
+				throw GameDataError("Expected: [%s] <condition> after '%s'", separator.c_str(), (it - 1)->c_str());
+			}
+			conditions_.push_back(create_condition(it, end, descr, tribes));
+			match_and_skip(end, separator);
+			it = end;
+		}
+	};
+
+	is_when_ = true;
+	if (begin != arguments.end()) {
+		if (match_and_skip(begin, "when")) {
+			parse_conditions(arguments, begin, "and");
+		} else if (match_and_skip(begin, "unless")) {
+			is_when_ = false;
+			parse_conditions(arguments, begin, "or");
+		} else {
+			throw GameDataError("Expected when|unless but found '%s'", begin->c_str());
+		}
 	}
 }
 
@@ -573,7 +485,11 @@
 	return ps.program_end(game, result_);
 }
 
-ProductionProgram::ActCall::ActCall(char* parameters, const ProductionSiteDescr& descr) {
+ProductionProgram::ActCall::ActCall(const std::vector<std::string>& arguments, const ProductionSiteDescr& descr) {
+	if (arguments.size() < 1 || arguments.size() > 4) {
+		throw GameDataError("Usage: call=<program name> [on failure|completion|skip fail|complete|skip|repeat]");
+	}
+
 	//  Initialize with default handling methods.
 	handling_methods_[program_result_index(ProgramResult::kFailed)] =
 	   ProgramResultHandlingMethod::kContinue;
@@ -582,61 +498,60 @@
 	handling_methods_[program_result_index(ProgramResult::kSkipped)] =
 	   ProgramResultHandlingMethod::kContinue;
 
-	try {
-		bool reached_end;
-		{
-			char const* const program_name = next_word(parameters, reached_end);
-			const ProductionSiteDescr::Programs& programs = descr.programs();
-			ProductionSiteDescr::Programs::const_iterator const it = programs.find(program_name);
-			if (it == programs.end())
-				throw GameDataError("the program \"%s\" has not (yet) been declared in %s "
-				                    "(wrong declaration order?)",
-				                    program_name, descr.name().c_str());
-			program_ = it->second.get();
-		}
-
-		//  Override with specified handling methods.
-		while (!reached_end) {
-			skip(parameters);
-			match_force_skip(parameters, "on");
-
-			ProgramResult result_to_set_method_for;
-			if (match_force_skip(parameters, "failure")) {
-				if (handling_methods_[program_result_index(ProgramResult::kFailed)] !=
-				    ProgramResultHandlingMethod::kContinue)
-					throw GameDataError("%s handling method already defined", "failure");
-				result_to_set_method_for = ProgramResult::kFailed;
-			} else if (match_force_skip(parameters, "completion")) {
-				if (handling_methods_[program_result_index(ProgramResult::kCompleted)] !=
-				    ProgramResultHandlingMethod::kContinue)
-					throw GameDataError("%s handling method already defined", "completion");
-				result_to_set_method_for = ProgramResult::kCompleted;
-			} else if (match_force_skip(parameters, "skip")) {
-				if (handling_methods_[program_result_index(ProgramResult::kSkipped)] !=
-				    ProgramResultHandlingMethod::kContinue)
-					throw GameDataError("%s handling method already defined", "skip");
-				result_to_set_method_for = ProgramResult::kSkipped;
-			} else
-				throw GameDataError(
-				   "expected %s but found \"%s\"", "{\"failure\"|\"completion\"|\"skip\"}", parameters);
-
-			ProgramResultHandlingMethod handling_method;
-			if (match(parameters, "fail"))
-				handling_method = ProgramResultHandlingMethod::kFail;
-			else if (match(parameters, "complete"))
-				handling_method = ProgramResultHandlingMethod::kComplete;
-			else if (match(parameters, "skip"))
-				handling_method = ProgramResultHandlingMethod::kSkip;
-			else if (match(parameters, "repeat"))
-				handling_method = ProgramResultHandlingMethod::kRepeat;
-			else
-				throw GameDataError("expected %s but found \"%s\"",
-				                    "{\"fail\"|\"complete\"|\"skip\"|\"repeat\"}", parameters);
-			handling_methods_[program_result_index(result_to_set_method_for)] = handling_method;
-			reached_end = !*parameters;
-		}
-	} catch (const WException& e) {
-		throw GameDataError("call: %s", e.what());
+	// Fetch program to call
+	const std::string& program_name = arguments.front();
+	const ProductionSiteDescr::Programs& programs = descr.programs();
+	ProductionSiteDescr::Programs::const_iterator const it = programs.find(program_name);
+	if (it == programs.end()) {
+		throw GameDataError("The program '%s' has not (yet) been declared in %s "
+							"(wrong declaration order?)",
+							program_name.c_str(), descr.name().c_str());
+	}
+	program_ = it->second.get();
+
+	//  Override with specified handling methods.
+	if (arguments.size() > 1) {
+		if (arguments.at(1) != "on") {
+			throw GameDataError("Expected 'on' keyword in second position");
+		}
+
+		ProgramResult result_to_set_method_for;
+		if (arguments.at(2) == "failure") {
+			if (handling_methods_[program_result_index(ProgramResult::kFailed)] !=
+				ProgramResultHandlingMethod::kContinue) {
+				throw GameDataError("%s handling method already defined", "failure");
+			}
+			result_to_set_method_for = ProgramResult::kFailed;
+		} else if (arguments.at(2) == "completion") {
+			if (handling_methods_[program_result_index(ProgramResult::kCompleted)] !=
+				ProgramResultHandlingMethod::kContinue) {
+				throw GameDataError("%s handling method already defined", "completion");
+			}
+			result_to_set_method_for = ProgramResult::kCompleted;
+		} else if (arguments.at(2) == "skip") {
+			if (handling_methods_[program_result_index(ProgramResult::kSkipped)] !=
+				ProgramResultHandlingMethod::kContinue) {
+				throw GameDataError("%s handling method already defined", "skip");
+			}
+			result_to_set_method_for = ProgramResult::kSkipped;
+		} else {
+			throw GameDataError(
+			   "Expected failure|completion|skip after 'on' but found '%s'", arguments.at(2).c_str());
+		}
+
+		ProgramResultHandlingMethod handling_method;
+		if (arguments.at(3) == "fail") {
+			handling_method = ProgramResultHandlingMethod::kFail;
+		} else if (arguments.at(3) == "complete") {
+			handling_method = ProgramResultHandlingMethod::kComplete;
+		} else if (arguments.at(3) == "skip") {
+			handling_method = ProgramResultHandlingMethod::kSkip;
+		} else if (arguments.at(3) == "repeat") {
+			handling_method = ProgramResultHandlingMethod::kRepeat;
+		} else {
+			throw GameDataError("Expected fail|complete|skip|repeat in final position but found '%s'", arguments.at(3).c_str());
+		}
+		handling_methods_[program_result_index(result_to_set_method_for)] = handling_method;
 	}
 }
 
@@ -662,38 +577,38 @@
 	}
 }
 
-ProductionProgram::ActCallWorker::ActCallWorker(char* parameters,
+ProductionProgram::ActCallWorker::ActCallWorker(const std::vector<std::string>& arguments,
                                                 const std::string& production_program_name,
                                                 ProductionSiteDescr* descr,
                                                 const Tribes& tribes) {
-	try {
-		program_ = parameters;
-
-		//  Quote form "void ProductionSite::program_act(Game &)":
-		//  "Always main worker is doing stuff"
-		const WorkerDescr& main_worker_descr =
-		   *tribes.get_worker_descr(descr->working_positions()[0].first);
-
-		//  This will fail unless the main worker has a program with the given
-		//  name, so it also validates the parameter.
-		const WorkareaInfo& worker_workarea_info =
-		   main_worker_descr.get_program(program_)->get_workarea_info();
-
-		for (const auto& area_info : worker_workarea_info) {
-			std::set<std::string>& building_radius_infos = descr->workarea_info_[area_info.first];
-
-			for (const std::string& worker_name : area_info.second) {
-				std::string description = descr->name();
-				description += ' ';
-				description += production_program_name;
-				description += " worker ";
-				description += main_worker_descr.name();
-				description += worker_name;
-				building_radius_infos.insert(description);
-			}
+	if (arguments.size() != 1) {
+		throw GameDataError("Usage: callworker=<worker program name>");
+	}
+
+	program_ = arguments.front();
+
+	//  Quote from "void ProductionSite::program_act(Game &)":
+	//  "Always main worker is doing stuff"
+	const WorkerDescr& main_worker_descr =
+	   *tribes.get_worker_descr(descr->working_positions().front().first);
+
+	//  This will fail unless the main worker has a program with the given
+	//  name, so it also validates the parameter.
+	const WorkareaInfo& worker_workarea_info =
+	   main_worker_descr.get_program(program_)->get_workarea_info();
+
+	for (const auto& area_info : worker_workarea_info) {
+		std::set<std::string>& building_radius_infos = descr->workarea_info_[area_info.first];
+
+		for (const std::string& worker_name : area_info.second) {
+			std::string description = descr->name();
+			description += ' ';
+			description += production_program_name;
+			description += " worker ";
+			description += main_worker_descr.name();
+			description += worker_name;
+			building_radius_infos.insert(description);
 		}
-	} catch (const WException& e) {
-		throw GameDataError("worker: %s", e.what());
 	}
 }
 
@@ -722,37 +637,22 @@
 	psite.program_end(game, ProgramResult::kFailed);
 }
 
-ProductionProgram::ActSleep::ActSleep(char* parameters) {
-	try {
-		if (*parameters) {
-			char* endp;
-			long long int const value = strtoll(parameters, &endp, 0);
-			duration_ = value;
-			if (*endp || value <= 0 || duration_ != value)
-				throw GameDataError("expected %s but found \"%s\"", "duration in ms", parameters);
-		} else
-			duration_ = 0;  //  Get duration from the result of a previous action.
-	} catch (const WException& e) {
-		throw GameDataError("sleep: %s", e.what());
+ProductionProgram::ActSleep::ActSleep(const std::vector<std::string>& arguments) {
+	if (arguments.size() != 1) {
+		throw GameDataError("Usage: sleep=<duration>");
 	}
+	duration_ = read_positive(arguments.front());
 }
 
 void ProductionProgram::ActSleep::execute(Game& game, ProductionSite& ps) const {
 	return ps.program_step(game, duration_ ? duration_ : 0, ps.top_state().phase);
 }
 
-ProductionProgram::ActCheckMap::ActCheckMap(char* parameters) {
-	try {
-		if (*parameters) {
-			if (!strcmp(parameters, "seafaring"))
-				feature_ = SEAFARING;
-			else
-				throw GameDataError("Unknown parameter \"%s\"", parameters);
-		} else
-			throw GameDataError("No parameter given!");
-	} catch (const WException& e) {
-		throw GameDataError("sleep: %s", e.what());
+ProductionProgram::ActCheckMap::ActCheckMap(const std::vector<std::string>& arguments) {
+	if (arguments.size() != 1 || arguments.front() != "seafaring") {
+		throw GameDataError("Usage: checkmap=seafaring");
 	}
+	feature_ = SEAFARING;
 }
 
 void ProductionProgram::ActCheckMap::execute(Game& game, ProductionSite& ps) const {
@@ -770,52 +670,22 @@
 	}
 }
 
-ProductionProgram::ActAnimate::ActAnimate(char* parameters, ProductionSiteDescr* descr) {
-	try {
-		bool reached_end;
-		animation_name_ = std::string(next_word(parameters, reached_end));
-		if (animation_name_ == "idle") {
-			throw GameDataError("idle animation is default; calling is not allowed");
-		}
-		if (!descr->is_animation_known(animation_name_)) {
-			throw GameDataError("Unknown animation '%s'", animation_name_.c_str());
-		}
-		if (!reached_end) {  //  The next parameter is the duration.
-			char* endp;
-			long long int const value = strtoll(parameters, &endp, 0);
-			duration_ = value;
-			if (*endp || value <= 0 || duration_ != value)
-				throw GameDataError("expected %s but found \"%s\"", "duration in ms", parameters);
-		} else
-			duration_ = 0;  //  Get duration from the result of a previous action.
-	} catch (const WException& e) {
-		throw GameDataError("animate: %s", e.what());
-	}
+ProductionProgram::ActAnimate::ActAnimate(const std::vector<std::string>& arguments, ProductionSiteDescr* descr) {
+	parameters = MapObjectProgram::parse_act_animate(arguments, *descr, false);
 }
 
 void ProductionProgram::ActAnimate::execute(Game& game, ProductionSite& ps) const {
-	ps.start_animation(game, ps.descr().get_animation(animation_name_, &ps));
-	return ps.program_step(game, duration_ ? duration_ : 0, ps.top_state().phase);
+	ps.start_animation(game, parameters.animation);
+	return ps.program_step(game, parameters.duration ? parameters.duration : 0, ps.top_state().phase);
 }
 
-ProductionProgram::ActConsume::ActConsume(char* parameters,
+ProductionProgram::ActConsume::ActConsume(const std::vector<std::string>& arguments,
                                           const ProductionSiteDescr& descr,
                                           const Tribes& tribes) {
-	try {
-		for (;;) {
-			consumed_wares_workers_.resize(consumed_wares_workers_.size() + 1);
-			parse_ware_type_group(parameters, *consumed_wares_workers_.rbegin(), tribes,
-			                      descr.input_wares(), descr.input_workers());
-			if (!*parameters)
-				break;
-			force_skip(parameters);
-		}
-		if (consumed_wares_workers_.empty()) {
-			throw GameDataError("expected ware_type1[,ware_type2[,...]][:N] ...");
-		}
-	} catch (const WException& e) {
-		throw GameDataError("consume: %s", e.what());
+	if (arguments.empty()) {
+		throw GameDataError("Usage: consume=<ware or worker>[,<ware or worker>[,...]][:<amount>] ...");
 	}
+	consumed_wares_workers_ = parse_ware_type_groups(arguments.begin(), arguments.end(), descr, tribes);
 }
 
 void ProductionProgram::ActConsume::execute(Game& game, ProductionSite& ps) const {
@@ -938,47 +808,13 @@
 	}
 }
 
-ProductionProgram::ActProduce::ActProduce(char* parameters,
+ProductionProgram::ActProduce::ActProduce(const std::vector<std::string>& arguments,
                                           const ProductionSiteDescr& descr,
                                           const Tribes& tribes) {
-	try {
-		for (bool more = true; more; ++parameters) {
-			produced_wares_.resize(produced_wares_.size() + 1);
-			WareAmount& item = *produced_wares_.rbegin();
-			skip(parameters);
-			char const* ware = parameters;
-			for (;; ++parameters) {
-				switch (*parameters) {
-				default:
-					break;
-				case '\0':
-				case ' ':
-					item.second = 1;
-					goto item_end;
-				case ':': {
-					*parameters = '\0';
-					++parameters;
-					char* endp;
-					unsigned long long int const value = strtoull(parameters, &endp, 0);
-					item.second = value;
-					if ((*endp && *endp != ' ') || value < 1 || item.second != value)
-						throw GameDataError("expected %s but found \"%s\"", "count", parameters);
-					parameters = endp;
-					goto item_end;
-				}
-				}
-			}
-		item_end:
-			more = *parameters != '\0';
-			*parameters = '\0';
-			if (!descr.is_output_ware_type(item.first = tribes.safe_ware_index(ware)))
-				throw GameDataError("%s is not declared as an output (\"%s\" was not "
-				                    "found in the \"outputs\" table)",
-				                    ware, ware);
-		}
-	} catch (const WException& e) {
-		throw GameDataError("produce: %s", e.what());
+	if (arguments.empty()) {
+		throw GameDataError("Usage: produce=<ware name>[:<amount>] [<ware name>[:<amount>]...]");
 	}
+	produced_wares_ = parse_bill_of_materials(arguments, WareWorker::wwWARE, descr, tribes);
 }
 
 void ProductionProgram::ActProduce::execute(Game& game, ProductionSite& ps) const {
@@ -1024,47 +860,13 @@
 	return false;
 }
 
-ProductionProgram::ActRecruit::ActRecruit(char* parameters,
+ProductionProgram::ActRecruit::ActRecruit(const std::vector<std::string>& arguments,
                                           const ProductionSiteDescr& descr,
                                           const Tribes& tribes) {
-	try {
-		for (bool more = true; more; ++parameters) {
-			recruited_workers_.resize(recruited_workers_.size() + 1);
-			WareAmount& item = *recruited_workers_.rbegin();
-			skip(parameters);
-			char const* worker = parameters;
-			for (;; ++parameters) {
-				switch (*parameters) {
-				default:
-					break;
-				case '\0':
-				case ' ':
-					item.second = 1;
-					goto item_end;
-				case ':': {
-					*parameters = '\0';
-					++parameters;
-					char* endp;
-					unsigned long long int const value = strtoull(parameters, &endp, 0);
-					item.second = value;
-					if ((*endp && *endp != ' ') || value < 1 || item.second != value)
-						throw GameDataError("expected %s but found \"%s\"", "count", parameters);
-					parameters = endp;
-					goto item_end;
-				}
-				}
-			}
-		item_end:
-			more = *parameters != '\0';
-			*parameters = '\0';
-			if (!descr.is_output_worker_type(item.first = tribes.safe_worker_index(worker)))
-				throw GameDataError("%s is not declared as an output (\"%s\" was not "
-				                    "found in the \"outputs\" table)",
-				                    worker, worker);
-		}
-	} catch (const WException& e) {
-		throw GameDataError("recruit: %s", e.what());
+	if (arguments.empty()) {
+		throw GameDataError("Usage: recruit=<worker name>[:<amount>] [<worker name>[:<amount>]...]");
 	}
+	recruited_workers_ = parse_bill_of_materials(arguments, WareWorker::wwWORKER, descr, tribes);
 }
 
 void ProductionProgram::ActRecruit::execute(Game& game, ProductionSite& ps) const {
@@ -1106,55 +908,24 @@
 	return false;
 }
 
-ProductionProgram::ActMine::ActMine(char* parameters,
+ProductionProgram::ActMine::ActMine(const std::vector<std::string>& arguments,
                                     const World& world,
                                     const std::string& production_program_name,
                                     ProductionSiteDescr* descr) {
-	try {
-		bool reached_end;
-		resource_ = world.safe_resource_index(next_word(parameters, reached_end));
-
-		{
-			char* endp;
-			unsigned long long int const value = strtoull(parameters, &endp, 0);
-			distance_ = value;
-			if (*endp != ' ' || distance_ != value)
-				throw GameDataError("expected %s but found \"%s\"", "distance", parameters);
-			parameters = endp;
-		}
-
-		{
-			char* endp;
-			unsigned long long int const value = strtoull(parameters, &endp, 0);
-			max_ = value;
-			if (*endp != ' ' || value < 1 || 100 < value)
-				throw GameDataError("expected %s but found \"%s\"", "percentage", parameters);
-			parameters = endp;
-		}
-
-		{
-			char* endp;
-			unsigned long long int const value = strtoull(parameters, &endp, 0);
-			chance_ = value;
-			if (*endp != ' ' || value < 1 || 100 < value)
-				throw GameDataError("expected %s but found \"%s\"", "percentage", parameters);
-			parameters = endp;
-		}
-		{
-			char* endp;
-			unsigned long long int const value = strtoull(parameters, &endp, 0);
-			training_ = value;
-			if (*endp || value < 1 || 100 < value)
-				throw GameDataError("expected %s but found \"%s\"", "percentage", parameters);
-		}
-		std::string description = (boost::format("%1$s %2$s mine %3$s") % descr->name() %
-		                           production_program_name % world.get_resource(resource_)->name())
-		                             .str();
-
-		descr->workarea_info_[distance_].insert(description);
-	} catch (const WException& e) {
-		throw GameDataError("mine: %s", e.what());
+	if (arguments.size() != 5) {
+		throw GameDataError("Usage: mine=resource <workarea radius> <max> <chance> <worker experience gained>");
 	}
+
+	resource_ = world.safe_resource_index(arguments.front().c_str());
+	distance_ = read_positive(arguments.at(1));
+	max_ = read_positive(arguments.at(2));
+	chance_ = read_positive(arguments.at(3));
+	training_ = read_positive(arguments.at(4));
+
+	const std::string description = (boost::format("%1$s %2$s mine %3$s") % descr->name() %
+							   production_program_name % world.get_resource(resource_)->name())
+								 .str();
+	descr->workarea_info_[distance_].insert(description);
 }
 
 void ProductionProgram::ActMine::execute(Game& game, ProductionSite& ps) const {
@@ -1268,32 +1039,27 @@
 	return ps.program_step(game);
 }
 
-ProductionProgram::ActCheckSoldier::ActCheckSoldier(char* parameters) {
-	//  TODO(unknown): This is currently hardcoded for "soldier", but should allow any
-	//  soldier type name.
-	if (!match_force_skip(parameters, "soldier"))
-		throw GameDataError("expected %s but found \"%s\"", "soldier type", parameters);
-	try {
-		if (match_force_skip(parameters, "health"))
-			attribute = TrainingAttribute::kHealth;
-		else if (match_force_skip(parameters, "attack"))
-			attribute = TrainingAttribute::kAttack;
-		else if (match_force_skip(parameters, "defense"))
-			attribute = TrainingAttribute::kDefense;
-		else if (match_force_skip(parameters, "evade"))
-			attribute = TrainingAttribute::kEvade;
-		else
-			throw GameDataError("expected %s but found \"%s\"",
-			                    "{\"health\"|\"attack\"|\"defense\"|\"evade\"}", parameters);
-
-		char* endp;
-		unsigned long long int const value = strtoull(parameters, &endp, 0);
-		level = value;
-		if (*endp || level != value)
-			throw GameDataError("expected %s but found \"%s\"", "level", parameters);
-	} catch (const WException& e) {
-		throw GameDataError("checksoldier: %s", e.what());
-	}
+ProductionProgram::ActCheckSoldier::ActCheckSoldier(const std::vector<std::string>& arguments) {
+	if (arguments.size() != 3) {
+		throw GameDataError("Usage: checksoldier=soldier <training attribute> <level>");
+	}
+
+	if (arguments.front() != "soldier") {
+		throw GameDataError("Expected 'soldier' but found '%s'", arguments.front().c_str());
+	}
+
+	if (arguments.at(1) == "health") {
+		attribute_ = TrainingAttribute::kHealth;
+	} else if (arguments.at(1) == "attack") {
+		attribute_ = TrainingAttribute::kAttack;
+	} else if (arguments.at(1) == "defense") {
+		attribute_ = TrainingAttribute::kDefense;
+	} else if (arguments.at(1) == "evade") {
+		attribute_ = TrainingAttribute::kEvade;
+	} else {
+		throw GameDataError("Expected health|attack|defense|evade after 'soldier' but found '%s'", arguments.at(1).c_str());
+	}
+	level_ = read_int(arguments.at(2), 0);
 }
 
 void ProductionProgram::ActCheckSoldier::execute(Game& game, ProductionSite& ps) const {
@@ -1304,8 +1070,8 @@
 		ps.set_production_result(_("No soldier to train!"));
 		return ps.program_end(game, ProgramResult::kSkipped);
 	}
-	ps.molog("  Checking soldier (%u) level %d)\n", static_cast<unsigned int>(attribute),
-	         static_cast<unsigned int>(level));
+	ps.molog("  Checking soldier (%u) level %d)\n", static_cast<unsigned int>(attribute_),
+	         static_cast<unsigned int>(level_));
 
 	const std::vector<Soldier*>::const_iterator soldiers_end = soldiers.end();
 	for (std::vector<Soldier*>::const_iterator it = soldiers.begin();; ++it) {
@@ -1313,67 +1079,52 @@
 			ps.set_production_result(_("No soldier found for this training level!"));
 			return ps.program_end(game, ProgramResult::kSkipped);
 		}
-		if (attribute == TrainingAttribute::kHealth) {
-			if ((*it)->get_health_level() == level)
-				break;
-		} else if (attribute == TrainingAttribute::kAttack) {
-			if ((*it)->get_attack_level() == level)
-				break;
-		} else if (attribute == TrainingAttribute::kDefense) {
-			if ((*it)->get_defense_level() == level)
-				break;
-		} else if (attribute == TrainingAttribute::kEvade) {
-			if ((*it)->get_evade_level() == level)
+		if (attribute_ == TrainingAttribute::kHealth) {
+			if ((*it)->get_health_level() == level_)
+				break;
+		} else if (attribute_ == TrainingAttribute::kAttack) {
+			if ((*it)->get_attack_level() == level_)
+				break;
+		} else if (attribute_ == TrainingAttribute::kDefense) {
+			if ((*it)->get_defense_level() == level_)
+				break;
+		} else if (attribute_ == TrainingAttribute::kEvade) {
+			if ((*it)->get_evade_level() == level_)
 				break;
 		}
 	}
 	ps.molog("    okay\n");  // okay, do nothing
 
 	upcast(TrainingSite, ts, &ps);
-	ts->training_attempted(attribute, level);
+	ts->training_attempted(attribute_, level_);
 
 	ps.molog("  Check done!\n");
 
 	return ps.program_step(game);
 }
 
-ProductionProgram::ActTrain::ActTrain(char* parameters) {
-	//  TODO(unknown): This is currently hardcoded for "soldier", but should allow any
-	//  soldier type name.
-	if (!match_force_skip(parameters, "soldier"))
-		throw GameDataError("expected %s but found \"%s\"", "soldier type", parameters);
-	try {
-		if (match_force_skip(parameters, "health"))
-			attribute = TrainingAttribute::kHealth;
-		else if (match_force_skip(parameters, "attack"))
-			attribute = TrainingAttribute::kAttack;
-		else if (match_force_skip(parameters, "defense"))
-			attribute = TrainingAttribute::kDefense;
-		else if (match_force_skip(parameters, "evade"))
-			attribute = TrainingAttribute::kEvade;
-		else
-			throw GameDataError("expected %s but found \"%s\"",
-			                    "{\"health\"|\"attack\"|\"defense\"|\"evade\"}", parameters);
-
-		{
-			char* endp;
-			unsigned long long int const value = strtoull(parameters, &endp, 0);
-			level = value;
-			if (*endp != ' ' || level != value)
-				throw GameDataError("expected %s but found \"%s\"", "level", parameters);
-			parameters = endp;
-		}
-
-		{
-			char* endp;
-			unsigned long long int const value = strtoull(parameters, &endp, 0);
-			target_level = value;
-			if (*endp || target_level != value || target_level <= level)
-				throw GameDataError("expected level > %u but found \"%s\"", level, parameters);
-		}
-	} catch (const WException& e) {
-		throw GameDataError("train: %s", e.what());
-	}
+ProductionProgram::ActTrain::ActTrain(const std::vector<std::string>& arguments) {
+	if (arguments.size() != 4) {
+		throw GameDataError("Usage: checksoldier=soldier <training attribute> <level before> <level after>");
+	}
+
+	if (arguments.front() != "soldier") {
+		throw GameDataError("Expected 'soldier' but found '%s'", arguments.front().c_str());
+	}
+
+	if (arguments.at(1) == "health") {
+		attribute_ = TrainingAttribute::kHealth;
+	} else if (arguments.at(1) == "attack") {
+		attribute_ = TrainingAttribute::kAttack;
+	} else if (arguments.at(1) == "defense") {
+		attribute_ = TrainingAttribute::kDefense;
+	} else if (arguments.at(1) == "evade") {
+		attribute_ = TrainingAttribute::kEvade;
+	} else {
+		throw GameDataError("Expected health|attack|defense|evade after 'soldier' but found '%s'", arguments.at(1).c_str());
+	}
+	level_ = read_int(arguments.at(2), 0);
+	target_level_ = read_positive(arguments.at(3));
 }
 
 void ProductionProgram::ActTrain::execute(Game& game, ProductionSite& ps) const {
@@ -1382,41 +1133,41 @@
 	const std::vector<Soldier*>::const_iterator soldiers_end = soldiers.end();
 	std::vector<Soldier*>::const_iterator it = soldiers.begin();
 
-	ps.molog("  Training soldier's %u (%d to %d)", static_cast<unsigned int>(attribute),
-	         static_cast<unsigned int>(level), static_cast<unsigned int>(target_level));
+	ps.molog("  Training soldier's %u (%d to %d)", static_cast<unsigned int>(attribute_),
+	         static_cast<unsigned int>(level_), static_cast<unsigned int>(target_level_));
 
 	for (;; ++it) {
 		if (it == soldiers_end) {
 			ps.set_production_result(_("No soldier found for this training level!"));
 			return ps.program_end(game, ProgramResult::kSkipped);
 		}
-		if (attribute == TrainingAttribute::kHealth) {
-			if ((*it)->get_health_level() == level)
-				break;
-		} else if (attribute == TrainingAttribute::kAttack) {
-			if ((*it)->get_attack_level() == level)
-				break;
-		} else if (attribute == TrainingAttribute::kDefense) {
-			if ((*it)->get_defense_level() == level)
-				break;
-		} else if (attribute == TrainingAttribute::kEvade) {
-			if ((*it)->get_evade_level() == level)
+		if (attribute_ == TrainingAttribute::kHealth) {
+			if ((*it)->get_health_level() == level_)
+				break;
+		} else if (attribute_ == TrainingAttribute::kAttack) {
+			if ((*it)->get_attack_level() == level_)
+				break;
+		} else if (attribute_ == TrainingAttribute::kDefense) {
+			if ((*it)->get_defense_level() == level_)
+				break;
+		} else if (attribute_ == TrainingAttribute::kEvade) {
+			if ((*it)->get_evade_level() == level_)
 				break;
 		}
 	}
 	ps.molog("    okay\n");  // okay, do nothing
 
 	try {
-		if (attribute == TrainingAttribute::kHealth)
-			(*it)->set_health_level(target_level);
-		else if (attribute == TrainingAttribute::kAttack)
-			(*it)->set_attack_level(target_level);
-
-		else if (attribute == TrainingAttribute::kDefense)
-			(*it)->set_defense_level(target_level);
-
-		else if (attribute == TrainingAttribute::kEvade)
-			(*it)->set_evade_level(target_level);
+		if (attribute_ == TrainingAttribute::kHealth)
+			(*it)->set_health_level(target_level_);
+		else if (attribute_ == TrainingAttribute::kAttack)
+			(*it)->set_attack_level(target_level_);
+
+		else if (attribute_ == TrainingAttribute::kDefense)
+			(*it)->set_defense_level(target_level_);
+
+		else if (attribute_ == TrainingAttribute::kEvade)
+			(*it)->set_evade_level(target_level_);
 
 	} catch (...) {
 		throw wexception("Fail training soldier!!");
@@ -1428,61 +1179,32 @@
 	   (boost::format(_("Completed %s")) % ps.top_state().program->descname()).str());
 
 	upcast(TrainingSite, ts, &ps);
-	ts->training_successful(attribute, level);
+	ts->training_successful(attribute_, level_);
 
 	return ps.program_step(game);
 }
 
-ProductionProgram::ActPlaySound::ActPlaySound(char* parameters) {
-	try {
-		bool reached_end;
-		const char* const name = next_word(parameters, reached_end);
-		fx = SoundHandler::register_fx(SoundType::kAmbient, name);
-
-		if (!reached_end) {
-			char* endp;
-			unsigned long long int const value = strtoull(parameters, &endp, 0);
-			priority = value;
-			if (*endp || priority != value)
-				throw GameDataError("expected %s but found \"%s\"", "priority", parameters);
-		} else {
-			priority = kFxPriorityAllowMultiple - 1;
-		}
-		if (priority < kFxPriorityLowest) {
-			throw GameDataError("Minmum priority for sounds is %d, but only %d was specified for %s",
-			                    kFxPriorityLowest, priority, name);
-		}
-	} catch (const WException& e) {
-		throw GameDataError("playsound: %s", e.what());
-	}
+ProductionProgram::ActPlaySound::ActPlaySound(const std::vector<std::string>& arguments) {
+	parameters = MapObjectProgram::parse_act_play_sound(arguments, kFxPriorityAllowMultiple - 1);
 }
 
 void ProductionProgram::ActPlaySound::execute(Game& game, ProductionSite& ps) const {
-	Notifications::publish(NoteSound(SoundType::kAmbient, fx, ps.position_, priority));
+	Notifications::publish(NoteSound(SoundType::kAmbient, parameters.fx, ps.position_, parameters.priority));
 	return ps.program_step(game);
 }
 
-ProductionProgram::ActConstruct::ActConstruct(char* parameters,
+ProductionProgram::ActConstruct::ActConstruct(const std::vector<std::string>& arguments,
                                               const std::string& production_program_name,
                                               ProductionSiteDescr* descr) {
-	try {
-		std::vector<std::string> params = split_string(parameters, " ");
-
-		if (params.size() != 3)
-			throw GameDataError("usage: construct object-name worker-program radius:NN");
-
-		objectname = params[0];
-		workerprogram = params[1];
-		radius = boost::lexical_cast<uint32_t>(params[2]);
-
-		std::set<std::string>& building_radius_infos = descr->workarea_info_[radius];
-		std::string description = descr->name() + ' ' + production_program_name;
-		description += " construct ";
-		description += objectname;
-		building_radius_infos.insert(description);
-	} catch (const WException& e) {
-		throw GameDataError("construct: %s", e.what());
+	if (arguments.size() != 3) {
+		throw GameDataError("Usage: construct=<object name> <worker program> <workarea radius>");
 	}
+	objectname = arguments.at(0);
+	workerprogram = arguments.at(1);
+	radius = read_positive(arguments.at(2));
+
+	const std::string description = descr->name() + ' ' + production_program_name + " construct " + objectname;
+	descr->workarea_info_[radius].insert(description);
 }
 
 const ImmovableDescr&
@@ -1632,96 +1354,90 @@
                                      const Tribes& tribes,
                                      const World& world,
                                      ProductionSiteDescr* building)
-   : name_(init_name), descname_(init_descname) {
-
-	for (const std::string& action_string : actions_table->array_entries<std::string>()) {
-		std::vector<std::string> parts;
-		boost::split(parts, action_string, boost::is_any_of("="));
-		if (parts.size() != 2) {
-			throw GameDataError(
-			   "invalid line: \"%s\" in production program \"%s\" for building \"%s\"",
-			   action_string.c_str(), name().c_str(), building->name().c_str());
-		}
-		std::unique_ptr<char[]> arguments(new char[parts[1].size() + 1]);
-		strncpy(arguments.get(), parts[1].c_str(), parts[1].size() + 1);
-
-		if (boost::iequals(parts[0], "return")) {
-			actions_.push_back(std::unique_ptr<ProductionProgram::Action>(
-			   new ActReturn(arguments.get(), *building, tribes)));
-		} else if (boost::iequals(parts[0], "call")) {
-			actions_.push_back(
-			   std::unique_ptr<ProductionProgram::Action>(new ActCall(arguments.get(), *building)));
-		} else if (boost::iequals(parts[0], "sleep")) {
-			actions_.push_back(
-			   std::unique_ptr<ProductionProgram::Action>(new ActSleep(arguments.get())));
-		} else if (boost::iequals(parts[0], "animate")) {
-			actions_.push_back(
-			   std::unique_ptr<ProductionProgram::Action>(new ActAnimate(arguments.get(), building)));
-		} else if (boost::iequals(parts[0], "consume")) {
-			actions_.push_back(std::unique_ptr<ProductionProgram::Action>(
-			   new ActConsume(arguments.get(), *building, tribes)));
-		} else if (boost::iequals(parts[0], "produce")) {
-			actions_.push_back(std::unique_ptr<ProductionProgram::Action>(
-			   new ActProduce(arguments.get(), *building, tribes)));
-		} else if (boost::iequals(parts[0], "recruit")) {
-			actions_.push_back(std::unique_ptr<ProductionProgram::Action>(
-			   new ActRecruit(arguments.get(), *building, tribes)));
-		} else if (boost::iequals(parts[0], "callworker")) {
-			actions_.push_back(std::unique_ptr<ProductionProgram::Action>(
-			   new ActCallWorker(arguments.get(), name(), building, tribes)));
-		} else if (boost::iequals(parts[0], "mine")) {
-			actions_.push_back(std::unique_ptr<ProductionProgram::Action>(
-			   new ActMine(arguments.get(), world, name(), building)));
-		} else if (boost::iequals(parts[0], "checksoldier")) {
-			actions_.push_back(
-			   std::unique_ptr<ProductionProgram::Action>(new ActCheckSoldier(arguments.get())));
-		} else if (boost::iequals(parts[0], "train")) {
-			actions_.push_back(
-			   std::unique_ptr<ProductionProgram::Action>(new ActTrain(arguments.get())));
-		} else if (boost::iequals(parts[0], "playsound")) {
-			actions_.push_back(
-			   std::unique_ptr<ProductionProgram::Action>(new ActPlaySound(arguments.get())));
-		} else if (boost::iequals(parts[0], "construct")) {
-			actions_.push_back(std::unique_ptr<ProductionProgram::Action>(
-			   new ActConstruct(arguments.get(), name(), building)));
-		} else if (boost::iequals(parts[0], "checkmap")) {
-			actions_.push_back(
-			   std::unique_ptr<ProductionProgram::Action>(new ActCheckMap(arguments.get())));
-		} else {
-			throw GameDataError(
-			   "unknown command type \"%s\" in production program \"%s\" for building \"%s\"",
-			   arguments.get(), name().c_str(), building->name().c_str());
-		}
-
-		const ProductionProgram::Action& action = *actions_.back();
-		for (const auto& group : action.consumed_wares_workers()) {
-			consumed_wares_workers_.push_back(group);
-		}
-		// Add produced wares. If the ware already exists, increase the amount
-		for (const auto& ware : action.produced_wares()) {
-			if (produced_wares_.count(ware.first) == 1) {
-				produced_wares_.at(ware.first) += ware.second;
-			} else {
-				produced_wares_.insert(ware);
-			}
-		}
-		// Add recruited workers. If the worker already exists, increase the amount
-		for (const auto& worker : action.recruited_workers()) {
-			if (recruited_workers_.count(worker.first) == 1) {
-				recruited_workers_.at(worker.first) += worker.second;
-			} else {
-				recruited_workers_.insert(worker);
-			}
-		}
-	}
-	if (actions_.empty())
-		throw GameDataError("no actions in production program \"%s\" for building \"%s\"",
-		                    name().c_str(), building->name().c_str());
-}
-
-const std::string& ProductionProgram::name() const {
-	return name_;
-}
+   : MapObjectProgram(init_name), descname_(init_descname) {
+
+	for (const std::string& line : actions_table->array_entries<std::string>()) {
+		if (line.empty()) {
+			throw GameDataError("Empty line");
+		}
+		try {
+			ProgramParseInput parseinput = parse_program_string(line);
+
+			if (parseinput.name == "return") {
+				actions_.push_back(std::unique_ptr<ProductionProgram::Action>(
+				   new ActReturn(parseinput.arguments, *building, tribes)));
+			} else if (parseinput.name == "call") {
+				actions_.push_back(
+				   std::unique_ptr<ProductionProgram::Action>(new ActCall(parseinput.arguments, *building)));
+			} else if (parseinput.name == "sleep") {
+				actions_.push_back(
+				   std::unique_ptr<ProductionProgram::Action>(new ActSleep(parseinput.arguments)));
+			} else if (parseinput.name == "animate") {
+				actions_.push_back(
+				   std::unique_ptr<ProductionProgram::Action>(new ActAnimate(parseinput.arguments, building)));
+			} else if (parseinput.name =="consume") {
+				actions_.push_back(std::unique_ptr<ProductionProgram::Action>(
+				   new ActConsume(parseinput.arguments, *building, tribes)));
+			} else if (parseinput.name == "produce") {
+				actions_.push_back(std::unique_ptr<ProductionProgram::Action>(
+				   new ActProduce(parseinput.arguments, *building, tribes)));
+			} else if (parseinput.name == "recruit") {
+				actions_.push_back(std::unique_ptr<ProductionProgram::Action>(
+				   new ActRecruit(parseinput.arguments, *building, tribes)));
+			} else if (parseinput.name =="callworker") {
+				actions_.push_back(std::unique_ptr<ProductionProgram::Action>(
+				   new ActCallWorker(parseinput.arguments, name(), building, tribes)));
+			} else if (parseinput.name == "mine") {
+				actions_.push_back(std::unique_ptr<ProductionProgram::Action>(
+				   new ActMine(parseinput.arguments, world, name(), building)));
+			} else if (parseinput.name == "checksoldier") {
+				actions_.push_back(
+				   std::unique_ptr<ProductionProgram::Action>(new ActCheckSoldier(parseinput.arguments)));
+			} else if (parseinput.name == "train") {
+				actions_.push_back(
+				   std::unique_ptr<ProductionProgram::Action>(new ActTrain(parseinput.arguments)));
+			} else if (parseinput.name == "playsound") {
+				actions_.push_back(
+				   std::unique_ptr<ProductionProgram::Action>(new ActPlaySound(parseinput.arguments)));
+			} else if (parseinput.name == "construct") {
+				actions_.push_back(std::unique_ptr<ProductionProgram::Action>(
+				   new ActConstruct(parseinput.arguments, name(), building)));
+			} else if (parseinput.name == "checkmap") {
+				actions_.push_back(
+				   std::unique_ptr<ProductionProgram::Action>(new ActCheckMap(parseinput.arguments)));
+			} else {
+				throw GameDataError("Unknown command '%s' in line '%s'", parseinput.name.c_str(), line.c_str());
+			}
+
+			const ProductionProgram::Action& action = *actions_.back();
+			for (const auto& group : action.consumed_wares_workers()) {
+				consumed_wares_workers_.push_back(group);
+			}
+			// Add produced wares. If the ware already exists, increase the amount
+			for (const auto& ware : action.produced_wares()) {
+				if (produced_wares_.count(ware.first) == 1) {
+					produced_wares_.at(ware.first) += ware.second;
+				} else {
+					produced_wares_.insert(ware);
+				}
+			}
+			// Add recruited workers. If the worker already exists, increase the amount
+			for (const auto& worker : action.recruited_workers()) {
+				if (recruited_workers_.count(worker.first) == 1) {
+					recruited_workers_.at(worker.first) += worker.second;
+				} else {
+					recruited_workers_.insert(worker);
+				}
+			}
+		} catch (const std::exception& e) {
+			throw GameDataError("Error reading line '%s': %s", line.c_str(), e.what());
+		}
+	}
+	if (actions_.empty()) {
+		throw GameDataError("No actions found");
+	}
+}
+
 const std::string& ProductionProgram::descname() const {
 	return descname_;
 }

=== modified file 'src/logic/map_objects/tribes/production_program.h'
--- src/logic/map_objects/tribes/production_program.h	2019-05-11 13:48:12 +0000
+++ src/logic/map_objects/tribes/production_program.h	2019-05-26 08:37:46 +0000
@@ -32,6 +32,7 @@
 #include "base/log.h"
 #include "base/macros.h"
 #include "logic/map_objects/buildcost.h"
+#include "logic/map_objects/map_object_program.h"
 #include "logic/map_objects/tribes/bill_of_materials.h"
 #include "logic/map_objects/tribes/program_result.h"
 #include "logic/map_objects/tribes/training_attribute.h"
@@ -51,7 +52,7 @@
 class World;
 
 /// Ordered sequence of actions (at least 1). Has a name.
-struct ProductionProgram {
+struct ProductionProgram : public MapObjectProgram {
 
 	/// A group of ware types with a count.
 	using WareTypeGroup = std::pair<std::set<std::pair<DescriptionIndex, WareWorker>>, uint8_t>;
@@ -97,13 +98,11 @@
 		DISALLOW_COPY_AND_ASSIGN(Action);
 	};
 
-	/// Parse a group of ware types followed by an optional count and terminated
-	/// by a space or null. Example: "fish,meat:2".
-	static void parse_ware_type_group(char*& parameters,
-	                                  WareTypeGroup& group,
-	                                  const Tribes& tribes,
-	                                  const BillOfMaterials& input_wares,
-	                                  const BillOfMaterials& input_workers);
+	/// Parse a group of ware types followed by an optional count within a vector range. Example: "fish,meat:2".
+	static Groups parse_ware_type_groups(std::vector<std::string>::const_iterator begin, std::vector<std::string>::const_iterator end, const ProductionSiteDescr& descr, const Tribes& tribes);
+
+	/// Parse a ware or worker list with optional amounts and ensure that the building's outputs match. Example: "fish:2".
+	static BillOfMaterials parse_bill_of_materials(const std::vector<std::string>& arguments, WareWorker ww, const ProductionSiteDescr& descr, const Tribes& tribes);
 
 	/// Returns from the program.
 	///
@@ -156,7 +155,7 @@
 	/// Note: If the execution reaches the end of the program. the return value
 	/// is implicitly set to Completed.
 	struct ActReturn : public Action {
-		ActReturn(char* parameters, const ProductionSiteDescr&, const Tribes& tribes);
+		ActReturn(const std::vector<std::string>& arguments, const ProductionSiteDescr&, const Tribes& tribes);
 		~ActReturn() override;
 		void execute(Game&, ProductionSite&) const override;
 
@@ -167,11 +166,10 @@
 			virtual std::string description_negation(const Tribes&) const = 0;
 		};
 		static Condition*
-		create_condition(char*& parameters, const ProductionSiteDescr&, const Tribes& tribes);
+		create_condition(std::vector<std::string>::const_iterator& begin, std::vector<std::string>::const_iterator& end, const ProductionSiteDescr&, const Tribes& tribes);
+
 		struct Negation : public Condition {
-			Negation(char*& parameters, const ProductionSiteDescr& descr, const Tribes& tribes)
-			   : operand(create_condition(parameters, descr, tribes)) {
-			}
+			Negation(std::vector<std::string>::const_iterator& begin, std::vector<std::string>::const_iterator& end, const ProductionSiteDescr& descr, const Tribes& tribes);
 			~Negation() override;
 			bool evaluate(const ProductionSite&) const override;
 			// Just a dummy to satisfy the superclass interface. Do not use.
@@ -211,7 +209,7 @@
 		/// wares, combining from any of the types specified, in its input
 		/// queues.
 		struct SiteHas : public Condition {
-			SiteHas(char*& parameters, const ProductionSiteDescr&, const Tribes& tribes);
+			SiteHas(std::vector<std::string>::const_iterator begin, std::vector<std::string>::const_iterator end, const ProductionSiteDescr& descr, const Tribes& tribes);
 			bool evaluate(const ProductionSite&) const override;
 			std::string description(const Tribes& tribes) const override;
 			std::string description_negation(const Tribes& tribes) const override;
@@ -269,7 +267,7 @@
 	///         but no statistics are calculated (with the same effect as
 	///         executing "return=no_stats")
 	struct ActCall : public Action {
-		ActCall(char* parameters, const ProductionSiteDescr&);
+		ActCall(const std::vector<std::string>& arguments, const ProductionSiteDescr&);
 		void execute(Game&, ProductionSite&) const override;
 
 	private:
@@ -285,7 +283,7 @@
 	///    program:
 	///       The name of a program defined in the productionsite's main worker.
 	struct ActCallWorker : public Action {
-		ActCallWorker(char* parameters,
+		ActCallWorker(const std::vector<std::string>& arguments,
 		              const std::string& production_program_name,
 		              ProductionSiteDescr*,
 		              const Tribes& tribes);
@@ -311,7 +309,7 @@
 	///
 	/// Blocks the execution of the program for the specified duration.
 	struct ActSleep : public Action {
-		explicit ActSleep(char* parameters);
+		explicit ActSleep(const std::vector<std::string>& arguments);
 		void execute(Game&, ProductionSite&) const override;
 
 	private:
@@ -329,7 +327,7 @@
 	///
 	/// Ends the program if the feature is not enabled.
 	struct ActCheckMap : public Action {
-		explicit ActCheckMap(char* parameters);
+		explicit ActCheckMap(const std::vector<std::string>& arguments);
 		void execute(Game&, ProductionSite&) const override;
 
 	private:
@@ -354,12 +352,11 @@
 	/// animation will not be stopped by this command. It will run until another
 	/// animation is started.)
 	struct ActAnimate : public Action {
-		ActAnimate(char* parameters, ProductionSiteDescr*);
+		ActAnimate(const std::vector<std::string>& arguments, ProductionSiteDescr*);
 		void execute(Game&, ProductionSite&) const override;
 
 	private:
-		std::string animation_name_;
-		Duration duration_;
+		AnimationParameters parameters;
 	};
 
 	/// Consumes wares from the input storages.
@@ -406,7 +403,7 @@
 	/// types of a group are sorted.
 	// TODO(unknown): change this!
 	struct ActConsume : public Action {
-		ActConsume(char* parameters, const ProductionSiteDescr&, const Tribes& tribes);
+		ActConsume(const std::vector<std::string>& arguments, const ProductionSiteDescr& descr, const Tribes& tribes);
 		void execute(Game&, ProductionSite&) const override;
 	};
 
@@ -426,7 +423,7 @@
 	/// produced wares are of the type specified in the group. How the produced
 	/// wares are handled is defined by the productionsite.
 	struct ActProduce : public Action {
-		ActProduce(char* parameters, const ProductionSiteDescr&, const Tribes& tribes);
+		ActProduce(const std::vector<std::string>& arguments, const ProductionSiteDescr&, const Tribes& tribes);
 		void execute(Game&, ProductionSite&) const override;
 		bool get_building_work(Game&, ProductionSite&, Worker&) const override;
 	};
@@ -447,13 +444,13 @@
 	/// The recruited workers are of the type specified in the group. How the
 	/// recruited workers are handled is defined by the productionsite.
 	struct ActRecruit : public Action {
-		ActRecruit(char* parameters, const ProductionSiteDescr&, const Tribes& tribes);
+		ActRecruit(const std::vector<std::string>& arguments, const ProductionSiteDescr&, const Tribes& tribes);
 		void execute(Game&, ProductionSite&) const override;
 		bool get_building_work(Game&, ProductionSite&, Worker&) const override;
 	};
 
 	struct ActMine : public Action {
-		ActMine(char* parameters,
+		ActMine(const std::vector<std::string>& arguments,
 		        const World&,
 		        const std::string& production_program_name,
 		        ProductionSiteDescr*);
@@ -468,22 +465,22 @@
 	};
 
 	struct ActCheckSoldier : public Action {
-		explicit ActCheckSoldier(char* parameters);
+		explicit ActCheckSoldier(const std::vector<std::string>& arguments);
 		void execute(Game&, ProductionSite&) const override;
 
 	private:
-		TrainingAttribute attribute;
-		uint8_t level;
+		TrainingAttribute attribute_;
+		uint8_t level_;
 	};
 
 	struct ActTrain : public Action {
-		explicit ActTrain(char* parameters);
+		explicit ActTrain(const std::vector<std::string>& arguments);
 		void execute(Game&, ProductionSite&) const override;
 
 	private:
-		TrainingAttribute attribute;
-		uint8_t level;
-		uint8_t target_level;
+		TrainingAttribute attribute_;
+		uint8_t level_;
+		uint8_t target_level_;
 	};
 
 	/// Plays a sound effect.
@@ -501,12 +498,11 @@
 	/// Plays the specified sound effect with the specified priority. Whether the
 	/// sound effect is actually played is determined by the sound handler.
 	struct ActPlaySound : public Action {
-		explicit ActPlaySound(char* parameters);
+		explicit ActPlaySound(const std::vector<std::string>& arguments);
 		void execute(Game&, ProductionSite&) const override;
 
 	private:
-		FxId fx;
-		uint8_t priority;
+		PlaySoundParameters parameters;
 	};
 
 	/// Sends a building worker to construct at an immovable.
@@ -522,7 +518,7 @@
 	///    radius
 	///       Activity radius
 	struct ActConstruct : public Action {
-		ActConstruct(char* parameters,
+		ActConstruct(const std::vector<std::string>& arguments,
 		             const std::string& production_program_name,
 		             ProductionSiteDescr*);
 		void execute(Game&, ProductionSite&) const override;
@@ -544,7 +540,6 @@
 	                  const World& world,
 	                  ProductionSiteDescr* building);
 
-	const std::string& name() const;
 	const std::string& descname() const;
 
 	size_t size() const;
@@ -555,7 +550,6 @@
 	const Buildcost& recruited_workers() const;
 
 private:
-	std::string name_;
 	std::string descname_;
 	std::vector<std::unique_ptr<Action>> actions_;
 	ProductionProgram::Groups consumed_wares_workers_;

=== modified file 'src/logic/map_objects/tribes/productionsite.cc'
--- src/logic/map_objects/tribes/productionsite.cc	2019-05-22 15:46:08 +0000
+++ src/logic/map_objects/tribes/productionsite.cc	2019-05-26 08:37:46 +0000
@@ -91,9 +91,11 @@
                                          const std::string& msgctxt,
                                          MapObjectType init_type,
                                          const LuaTable& table,
-                                         const Tribes& tribes,
+										 const Tribes& tribes,
                                          const World& world)
    : BuildingDescr(init_descname, init_type, table, tribes),
+	 ware_demand_checks_(new std::set<DescriptionIndex>()),
+	 worker_demand_checks_(new std::set<DescriptionIndex>()),
      out_of_resource_productivity_threshold_(100) {
 	if (msgctxt.empty()) {
 		throw Widelands::GameDataError(
@@ -188,10 +190,11 @@
 	items_table = table.get_table("programs");
 	for (std::string program_name : items_table->keys<std::string>()) {
 		std::transform(program_name.begin(), program_name.end(), program_name.begin(), tolower);
+		if (programs_.count(program_name)) {
+			throw GameDataError("Program '%s' has already been declared for productionsite '%s'",
+								program_name.c_str(), name().c_str());
+		}
 		try {
-			if (programs_.count(program_name)) {
-				throw wexception("this program has already been declared");
-			}
 			std::unique_ptr<LuaTable> program_table = items_table->get_table(program_name);
 
 			// Allow use of both gettext and pgettext. This way, we can have a lower workload on
@@ -205,7 +208,7 @@
 			   new ProductionProgram(program_name, program_descname,
 			                         program_table->get_table("actions"), tribes, world, this));
 		} catch (const std::exception& e) {
-			throw wexception("program %s: %s", program_name.c_str(), e.what());
+			throw GameDataError("%s: Error in productionsite program %s: %s", name().c_str(), program_name.c_str(), e.what());
 		}
 	}
 
@@ -251,6 +254,19 @@
 	return *new ProductionSite(*this);
 }
 
+std::set<DescriptionIndex>* ProductionSiteDescr::ware_demand_checks() const {
+	return ware_demand_checks_.get();
+}
+std::set<DescriptionIndex>* ProductionSiteDescr::worker_demand_checks() const {
+	return worker_demand_checks_.get();
+}
+void ProductionSiteDescr::clear_demand_checks() {
+	ware_demand_checks_->clear();
+	ware_demand_checks_.reset(nullptr);
+	worker_demand_checks_->clear();
+	worker_demand_checks_.reset(nullptr);
+}
+
 /*
 ==============================
 

=== modified file 'src/logic/map_objects/tribes/productionsite.h'
--- src/logic/map_objects/tribes/productionsite.h	2019-05-22 15:46:08 +0000
+++ src/logic/map_objects/tribes/productionsite.h	2019-05-26 08:37:46 +0000
@@ -73,6 +73,13 @@
 
 	Building& create_object() const override;
 
+	// List of wares to register having economy checks. Parsed by the tribes during postload and must be nullptr after loading has finished
+	std::set<DescriptionIndex>* ware_demand_checks() const;
+	// List of workers to register having economy checks. Parsed by the tribes during postload and must be nullptr after loading has finished
+	std::set<DescriptionIndex>* worker_demand_checks() const;
+	// Clear ware and worker demand check info
+	void clear_demand_checks();
+
 	uint32_t nr_working_positions() const {
 		uint32_t result = 0;
 		for (const auto& working_pos : working_positions()) {
@@ -129,6 +136,8 @@
 	}
 
 private:
+	std::unique_ptr<std::set<DescriptionIndex>> ware_demand_checks_;
+	std::unique_ptr<std::set<DescriptionIndex>> worker_demand_checks_;
 	BillOfMaterials working_positions_;
 	BillOfMaterials input_wares_;
 	BillOfMaterials input_workers_;

=== modified file 'src/logic/map_objects/tribes/soldier.cc'
--- src/logic/map_objects/tribes/soldier.cc	2019-05-18 20:43:25 +0000
+++ src/logic/map_objects/tribes/soldier.cc	2019-05-26 08:37:46 +0000
@@ -32,7 +32,6 @@
 #include "economy/flag.h"
 #include "graphic/graphic.h"
 #include "graphic/rendertarget.h"
-#include "helper.h"
 #include "io/fileread.h"
 #include "io/filewrite.h"
 #include "logic/editor_game_base.h"

=== modified file 'src/logic/map_objects/tribes/trainingsite.cc'
--- src/logic/map_objects/tribes/trainingsite.cc	2019-05-05 14:05:07 +0000
+++ src/logic/map_objects/tribes/trainingsite.cc	2019-05-26 08:37:46 +0000
@@ -28,7 +28,6 @@
 #include "base/macros.h"
 #include "base/wexception.h"
 #include "economy/request.h"
-#include "helper.h"
 #include "logic/editor_game_base.h"
 #include "logic/game.h"
 #include "logic/map_objects/tribes/production_program.h"

=== modified file 'src/logic/map_objects/tribes/tribes.cc'
--- src/logic/map_objects/tribes/tribes.cc	2019-05-22 11:23:14 +0000
+++ src/logic/map_objects/tribes/tribes.cc	2019-05-26 08:37:46 +0000
@@ -293,15 +293,6 @@
 	return tribes_->get_mutable(tribeindex);
 }
 
-void Tribes::set_ware_type_has_demand_check(const DescriptionIndex& wareindex,
-                                            const std::string& tribename) const {
-	wares_->get_mutable(wareindex)->set_has_demand_check(tribename);
-}
-
-void Tribes::set_worker_type_has_demand_check(const DescriptionIndex& workerindex) const {
-	workers_->get_mutable(workerindex)->set_has_demand_check();
-}
-
 void Tribes::load_graphics() {
 	for (size_t tribeindex = 0; tribeindex < nrtribes(); ++tribeindex) {
 		TribeDescr* tribe = tribes_->get_mutable(tribeindex);
@@ -355,6 +346,11 @@
 		TribeDescr* tribe_descr = tribes_->get_mutable(i);
 		tribe_descr->resize_ware_orders(number);
 
+		// Register which wares and workers have economy demand checks for each tribe
+		for (const DescriptionIndex building_index : tribe_descr->buildings()) {
+			postload_register_economy_demand_checks(*buildings_->get_mutable(building_index), *tribe_descr);
+		}
+
 		// Verify that the preciousness has been set for all of the tribe's wares
 		for (const DescriptionIndex wi : tribe_descr->wares()) {
 			if (tribe_descr->get_ware_descr(wi)->ai_hints().preciousness(tribe_descr->name()) ==
@@ -367,7 +363,36 @@
 	}
 }
 
-// Set default trainingsites proportions for AI. Make sure that we get a sum of ca. 100
+/// Register wares and workers that have economy demand checks for a building
+void Tribes::postload_register_economy_demand_checks(BuildingDescr& building_descr, const TribeDescr& tribe_descr)
+{
+	if (upcast(ProductionSiteDescr, prodsite, &building_descr)) {
+		// This function can be called only once per loading of tribes
+		assert(prodsite->ware_demand_checks() != nullptr);
+		// NOCOM
+		for (const DescriptionIndex ware_index : *prodsite->ware_demand_checks()) {
+			if (!tribe_descr.has_ware(ware_index)) {
+				throw GameDataError("Productionsite '%s' for tribe '%s' has an economy demand check for ware '%s', but the tribe does not use this ware",
+									prodsite->name().c_str(),
+									tribe_descr.name().c_str(),
+									get_ware_descr(ware_index)->name().c_str());
+			}
+			wares_->get_mutable(ware_index)->set_has_demand_check(tribe_descr.name());
+		}
+		for (const DescriptionIndex worker_index : *prodsite->worker_demand_checks()) {
+			if (!tribe_descr.has_worker(worker_index)) {
+				throw GameDataError("Productionsite '%s' for tribe '%s' has an economy demand check for worker '%s', but the tribe does not use this worker",
+									prodsite->name().c_str(),
+									tribe_descr.name().c_str(),
+									get_worker_descr(worker_index)->name().c_str());
+			}
+			workers_->get_mutable(worker_index)->set_has_demand_check();
+		}
+		prodsite->clear_demand_checks();
+	}
+}
+
+/// Set default trainingsites proportions for AI. Make sure that we get a sum of ca. 100
 void Tribes::postload_calculate_trainingsites_proportions() {
 	for (DescriptionIndex i = 0; i < tribes_->size(); ++i) {
 		TribeDescr* tribe_descr = tribes_->get_mutable(i);

=== modified file 'src/logic/map_objects/tribes/tribes.h'
--- src/logic/map_objects/tribes/tribes.h	2019-05-16 09:15:03 +0000
+++ src/logic/map_objects/tribes/tribes.h	2019-05-26 08:37:46 +0000
@@ -133,10 +133,6 @@
 	const WorkerDescr* get_worker_descr(DescriptionIndex worker_index) const;
 	const TribeDescr* get_tribe_descr(DescriptionIndex tribe_index) const;
 
-	void set_ware_type_has_demand_check(const DescriptionIndex& ware_index,
-	                                    const std::string& tribename) const;
-	void set_worker_type_has_demand_check(const DescriptionIndex& worker_index) const;
-
 	/// Load tribes' graphics
 	void load_graphics();
 
@@ -147,6 +143,7 @@
 
 private:
 	void postload_calculate_trainingsites_proportions();
+	void postload_register_economy_demand_checks(BuildingDescr& building_descr, const TribeDescr& tribe_descr);
 
 	std::unique_ptr<DescriptionMaintainer<BuildingDescr>> buildings_;
 	std::unique_ptr<DescriptionMaintainer<ImmovableDescr>> immovables_;

=== modified file 'src/logic/map_objects/tribes/worker.cc'
--- src/logic/map_objects/tribes/worker.cc	2019-05-25 10:47:18 +0000
+++ src/logic/map_objects/tribes/worker.cc	2019-05-26 08:37:46 +0000
@@ -35,7 +35,6 @@
 #include "economy/transfer.h"
 #include "graphic/rendertarget.h"
 #include "graphic/text_constants.h"
-#include "helper.h"
 #include "io/fileread.h"
 #include "io/filewrite.h"
 #include "logic/cmd_incorporate.h"
@@ -3146,7 +3145,7 @@
 	return Bob::Loader::get_task(name);
 }
 
-const BobProgramBase* Worker::Loader::get_program(const std::string& name) {
+const MapObjectProgram* Worker::Loader::get_program(const std::string& name) {
 	Worker& worker = get<Worker>();
 	return worker.descr().get_program(name);
 }

=== modified file 'src/logic/map_objects/tribes/worker.h'
--- src/logic/map_objects/tribes/worker.h	2019-04-24 06:01:37 +0000
+++ src/logic/map_objects/tribes/worker.h	2019-05-26 08:37:46 +0000
@@ -303,7 +303,7 @@
 
 	protected:
 		const Task* get_task(const std::string& name) override;
-		const BobProgramBase* get_program(const std::string& name) override;
+		const MapObjectProgram* get_program(const std::string& name) override;
 
 	private:
 		uint32_t location_;

=== modified file 'src/logic/map_objects/tribes/worker_descr.cc'
--- src/logic/map_objects/tribes/worker_descr.cc	2019-05-26 08:37:42 +0000
+++ src/logic/map_objects/tribes/worker_descr.cc	2019-05-26 08:37:46 +0000
@@ -106,16 +106,14 @@
 		std::unique_ptr<LuaTable> programs_table = table.get_table("programs");
 		for (std::string program_name : programs_table->keys<std::string>()) {
 			std::transform(program_name.begin(), program_name.end(), program_name.begin(), tolower);
-
+			if (programs_.count(program_name)) {
+				throw GameDataError("Program '%s' has already been declared for worker '%s'", program_name.c_str(), name().c_str());
+			}
 			try {
-				if (programs_.count(program_name))
-					throw wexception("this program has already been declared");
-
-				programs_[program_name] =
-				   std::unique_ptr<WorkerProgram>(new WorkerProgram(program_name, *this, tribes_));
-				programs_[program_name]->parse(*programs_table->get_table(program_name));
+				programs_[program_name] = std::unique_ptr<WorkerProgram>(
+				   new WorkerProgram(program_name, *programs_table->get_table(program_name), *this, tribes_));
 			} catch (const std::exception& e) {
-				throw wexception("program %s: %s", program_name.c_str(), e.what());
+				throw GameDataError("%s: Error in worker program %s: %s", name().c_str(), program_name.c_str(), e.what());
 			}
 		}
 	}
@@ -136,8 +134,9 @@
 WorkerProgram const* WorkerDescr::get_program(const std::string& programname) const {
 	Programs::const_iterator it = programs_.find(programname);
 
-	if (it == programs_.end())
-		throw wexception("%s has no program '%s'", name().c_str(), programname.c_str());
+	if (it == programs_.end()) {
+		throw GameDataError("%s has no program '%s'", name().c_str(), programname.c_str());
+	}
 
 	return it->second.get();
 }

=== modified file 'src/logic/map_objects/tribes/worker_program.cc'
--- src/logic/map_objects/tribes/worker_program.cc	2019-05-11 13:48:12 +0000
+++ src/logic/map_objects/tribes/worker_program.cc	2019-05-26 08:37:46 +0000
@@ -23,7 +23,6 @@
 #include <string>
 
 #include "base/log.h"
-#include "helper.h"
 #include "logic/game_data_error.h"
 #include "logic/map_objects/findnode.h"
 #include "sound/sound_handler.h"
@@ -98,54 +97,45 @@
 
    {nullptr, nullptr}};
 
+
 /**
  * Parse a program
  */
-void WorkerProgram::parse(const LuaTable& table) {
-	for (const std::string& string : table.array_entries<std::string>()) {
-		if (string.empty()) {
-			log("Worker program %s for worker %s contains empty string\n", name_.c_str(),
-			    worker_.name().c_str());
-			break;
+WorkerProgram::WorkerProgram(const std::string& init_name, const LuaTable& actions_table, const WorkerDescr& worker, const Tribes& tribes)
+   : MapObjectProgram(init_name), worker_(worker), tribes_(tribes) {
+
+	for (const std::string& line : actions_table.array_entries<std::string>()) {
+		if (line.empty()) {
+			throw GameDataError("Empty line");
 		}
 		try {
-			std::vector<std::string> cmd(split_string(string, "="));
-			if (cmd.empty()) {
-				continue;
-			}
+
+			ProgramParseInput parseinput = parse_program_string(line);
 
 			// Find the appropriate parser
 			Worker::Action act;
 			uint32_t mapidx;
 
 			for (mapidx = 0; parsemap_[mapidx].name; ++mapidx) {
-				if (cmd[0] == parsemap_[mapidx].name) {
+				if (parseinput.name == parsemap_[mapidx].name) {
 					break;
 				}
 			}
 
 			if (!parsemap_[mapidx].name) {
-				throw wexception("unknown command type \"%s\"", cmd[0].c_str());
-			}
-
-			// TODO(GunChleoc): Quick and dirty solution, don't do it like that when we unify the
-			// program parsers
-			if (cmd.size() == 2) {
-				const std::vector<std::string> parameters(split_string(cmd[1], " \t\n"));
-				cmd.pop_back();
-				for (const std::string& parameter : parameters) {
-					cmd.push_back(parameter);
-				}
-			}
-
-			(this->*parsemap_[mapidx].function)(&act, cmd);
+				throw GameDataError("Unknown command '%s' in line '%s'", parseinput.name.c_str(), line.c_str());
+			}
+
+			(this->*parsemap_[mapidx].function)(&act, parseinput.arguments);
 
 			actions_.push_back(act);
 		} catch (const std::exception& e) {
-			throw wexception("Error reading line '%s' in worker program %s for worker %s: %s",
-			                 string.c_str(), name_.c_str(), worker_.name().c_str(), e.what());
+			throw GameDataError("Error reading line '%s': %s", line.c_str(), e.what());
 		}
 	}
+	if (actions_.empty()) {
+		throw GameDataError("No actions found");
+	}
 }
 
 /* RST
@@ -172,11 +162,12 @@
  * iparam1 = ware index
  */
 void WorkerProgram::parse_createware(Worker::Action* act, const std::vector<std::string>& cmd) {
-	if (cmd.size() != 2)
+	if (cmd.size() != 1) {
 		throw wexception("Usage: createware=<ware type>");
+	}
 
 	act->function = &Worker::run_createware;
-	act->iparam1 = tribes_.safe_ware_index(cmd[1]);
+	act->iparam1 = tribes_.safe_ware_index(cmd[0]);
 }
 
 /* RST
@@ -207,16 +198,13 @@
  * sparam1 = resource
  */
 void WorkerProgram::parse_mine(Worker::Action* act, const std::vector<std::string>& cmd) {
-	if (cmd.size() != 3)
-		throw wexception("Usage: mine=<ware type> <area>");
+	if (cmd.size() != 2) {
+		throw GameDataError("Usage: mine=<ware type> <workarea radius>");
+	}
 
 	act->function = &Worker::run_mine;
-	act->sparam1 = cmd[1];
-	char* endp;
-	act->iparam1 = strtol(cmd[2].c_str(), &endp, 0);
-
-	if (*endp)
-		throw wexception("Bad area '%s'", cmd[2].c_str());
+	act->sparam1 = cmd[0];
+	act->iparam1 = read_positive(cmd[1]);
 }
 
 /* RST
@@ -244,16 +232,13 @@
  * sparam1 = resource
  */
 void WorkerProgram::parse_breed(Worker::Action* act, const std::vector<std::string>& cmd) {
-	if (cmd.size() != 3)
-		throw wexception("Usage: breed=<ware type> <area>");
+	if (cmd.size() != 2) {
+		throw GameDataError("Usage: breed=<ware type> <workarea radius>");
+	}
 
 	act->function = &Worker::run_breed;
-	act->sparam1 = cmd[1];
-	char* endp;
-	act->iparam1 = strtol(cmd[2].c_str(), &endp, 0);
-
-	if (*endp)
-		throw wexception("Bad area '%s'", cmd[2].c_str());
+	act->sparam1 = cmd[0];
+	act->iparam1 = read_positive(cmd[1]);
 }
 
 /* RST
@@ -296,37 +281,26 @@
  * sparam1 = type
  */
 void WorkerProgram::parse_findobject(Worker::Action* act, const std::vector<std::string>& cmd) {
-	uint32_t i;
-
 	act->function = &Worker::run_findobject;
 	act->iparam1 = -1;
 	act->iparam2 = -1;
 	act->sparam1 = "immovable";
 
 	// Parse predicates
-	for (i = 1; i < cmd.size(); ++i) {
-		uint32_t idx = cmd[i].find(':');
-		std::string const key = cmd[i].substr(0, idx);
-		std::string const value = cmd[i].substr(idx + 1);
-
-		if (key == "radius") {
-			char* endp;
-
-			act->iparam1 = strtol(value.c_str(), &endp, 0);
-			if (*endp)
-				throw wexception("Bad findobject radius '%s'", value.c_str());
-
-		} else if (key == "attrib") {
-			act->iparam2 = MapObjectDescr::get_attribute_id(value);
-		} else if (key == "type") {
-			act->sparam1 = value;
-		} else
-			throw wexception("Bad findobject predicate %s:%s", key.c_str(), value.c_str());
+	for (const std::string& argument : cmd) {
+		const std::pair<std::string, std::string> item = read_key_value_pair(argument, ':');
+
+		if (item.first == "radius") {
+			act->iparam1 = read_positive(item.second);
+		} else if (item.first == "attrib") {
+			act->iparam2 = MapObjectDescr::get_attribute_id(item.second);
+		} else if (item.first == "type") {
+			act->sparam1 = item.second;
+		} else {
+			throw GameDataError("Unknown findobject predicate %s", argument.c_str());
+		}
 	}
 
-	if (act->iparam1 <= 0)
-		throw wexception("findobject: must specify radius");
-
 	workarea_info_[act->iparam1].insert(" findobject");
 }
 
@@ -408,8 +382,6 @@
  * sparam1 = Resource
  */
 void WorkerProgram::parse_findspace(Worker::Action* act, const std::vector<std::string>& cmd) {
-	uint32_t i;
-
 	act->function = &Worker::run_findspace;
 	act->iparam1 = -1;
 	act->iparam2 = -1;
@@ -420,61 +392,58 @@
 	act->sparam1 = "";
 
 	// Parse predicates
-	for (i = 1; i < cmd.size(); ++i) {
-		uint32_t idx = cmd[i].find(':');
-		std::string key = cmd[i].substr(0, idx);
-		std::string value = cmd[i].substr(idx + 1);
-
-		if (key == "radius") {
-			char* endp;
-
-			act->iparam1 = strtol(value.c_str(), &endp, 0);
-			if (*endp)
-				throw wexception("Bad findspace radius '%s'", value.c_str());
-
-		} else if (key == "size") {
-			static const struct {
-				char const* name;
-				int32_t val;
-			} sizenames[] = {{"any", FindNodeSize::sizeAny},     {"build", FindNodeSize::sizeBuild},
-			                 {"small", FindNodeSize::sizeSmall}, {"medium", FindNodeSize::sizeMedium},
-			                 {"big", FindNodeSize::sizeBig},     {"mine", FindNodeSize::sizeMine},
-			                 {"port", FindNodeSize::sizePort},   {nullptr, 0}};
-
-			int32_t index;
-
-			for (index = 0; sizenames[index].name; ++index)
-				if (value == sizenames[index].name)
-					break;
-
-			if (!sizenames[index].name)
-				throw wexception("Bad findspace size '%s'", value.c_str());
-
-			act->iparam2 = sizenames[index].val;
-		} else if (key == "breed") {
-			act->iparam4 = 1;
-		} else if (key == "resource") {
-			act->sparam1 = value;
-		} else if (key == "space") {
-			act->iparam3 = 1;
-		} else if (key == "avoid") {
-			act->iparam5 = MapObjectDescr::get_attribute_id(value);
-		} else if (key == "saplingsearches") {
-			int ival = strtol(value.c_str(), nullptr, 10);
-			if (1 != act->iparam6 || 1 > ival) {
-				throw wexception("Findspace: Forester should consider at least one spot.%d %d %s",
-				                 act->iparam6, ival, value.c_str());
+	for (const std::string& argument : cmd) {
+		try {
+			const std::pair<std::string, std::string> item = read_key_value_pair(argument, ':');
+
+			if (item.first == "radius") {
+				act->iparam1 = read_positive(item.second);
+			} else if (item.first == "size") {
+				static const struct {
+					char const* name;
+					int32_t val;
+				} sizenames[] = {{"any", FindNodeSize::sizeAny},     {"build", FindNodeSize::sizeBuild},
+								 {"small", FindNodeSize::sizeSmall}, {"medium", FindNodeSize::sizeMedium},
+								 {"big", FindNodeSize::sizeBig},     {"mine", FindNodeSize::sizeMine},
+								 {"port", FindNodeSize::sizePort},   {nullptr, 0}};
+
+				int32_t index;
+
+				for (index = 0; sizenames[index].name; ++index) {
+					if (item.second == sizenames[index].name) {
+						break;
+					}
+				}
+
+				if (!sizenames[index].name) {
+					throw GameDataError("Bad findspace size '%s'", item.second.c_str());
+				}
+
+				act->iparam2 = sizenames[index].val;
+			} else if (item.first == "breed") {
+				act->iparam4 = 1;
+			} else if (item.first == "resource") {
+				act->sparam1 = item.second;
+			} else if (item.first == "space") {
+				act->iparam3 = 1;
+			} else if (item.first == "avoid") {
+				act->iparam5 = MapObjectDescr::get_attribute_id(item.second);
+			} else if (item.first == "saplingsearches") {
+				act->iparam6 = read_positive(item.second);
 			} else {
-				act->iparam6 = ival;
+				throw GameDataError("Unknown findspace predicate %s", item.first.c_str());
 			}
-		} else
-			throw wexception("Bad findspace predicate %s:%s", key.c_str(), value.c_str());
+		} catch (const GameDataError& e) {
+			throw GameDataError("Malformed findspace argument %s: %s", argument.c_str(), e.what());
+		}
 	}
 
-	if (act->iparam1 <= 0)
-		throw wexception("findspace: must specify radius");
-	if (act->iparam2 < 0)
-		throw wexception("findspace: must specify size");
+	if (act->iparam1 <= 0) {
+		throw GameDataError("findspace: must specify radius");
+	}
+	if (act->iparam2 < 0) {
+		throw GameDataError("findspace: must specify size");
+	}
 	workarea_info_[act->iparam1].insert(" findspace");
 }
 
@@ -526,19 +495,21 @@
  * iparam1 = walkXXX
  */
 void WorkerProgram::parse_walk(Worker::Action* act, const std::vector<std::string>& cmd) {
-	if (cmd.size() != 2)
-		throw wexception("Usage: walk=<where>");
+	if (cmd.size() != 1) {
+		throw GameDataError("Usage: walk=object|coords|object-or-coords");
+	}
 
 	act->function = &Worker::run_walk;
 
-	if (cmd[1] == "object")
+	if (cmd[0] == "object") {
 		act->iparam1 = Worker::Action::walkObject;
-	else if (cmd[1] == "coords")
+	} else if (cmd[0] == "coords") {
 		act->iparam1 = Worker::Action::walkCoords;
-	else if (cmd[1] == "object-or-coords")
+	} else if (cmd[0] == "object-or-coords") {
 		act->iparam1 = Worker::Action::walkObject | Worker::Action::walkCoords;
-	else
-		throw wexception("Bad walk destination '%s'", cmd[1].c_str());
+	} else {
+		throw GameDataError("Bad walk destination '%s'", cmd[0].c_str());
+	}
 }
 
 /* RST
@@ -565,28 +536,14 @@
  * iparam2 = duration
  */
 void WorkerProgram::parse_animate(Worker::Action* act, const std::vector<std::string>& cmd) {
-	char* endp;
-
-	if (cmd.size() != 3)
-		throw GameDataError("Usage: animate=<name> <time>");
-
-	if (!worker_.is_animation_known(cmd[1])) {
-		throw GameDataError("unknown animation \"%s\" in worker program for worker \"%s\"",
-		                    cmd[1].c_str(), worker_.name().c_str());
-	}
+	AnimationParameters parameters = MapObjectProgram::parse_act_animate(cmd, worker_, true);
 
 	act->function = &Worker::run_animate;
 	// If the second parameter to MapObjectDescr::get_animation is ever used for anything other than
 	// level-dependent soldier animations, or we want to write a worker program for a soldier,
-	// we will need to store the animation name as a string in an iparam
-	act->iparam1 = worker_.get_animation(cmd[1], nullptr);
-
-	act->iparam2 = strtol(cmd[2].c_str(), &endp, 0);
-	if (*endp)
-		throw GameDataError("Bad duration '%s'", cmd[2].c_str());
-
-	if (act->iparam2 <= 0)
-		throw GameDataError("animation duration must be positive");
+	// we will need to store the animation name as a string in an sparam
+	act->iparam1 = parameters.animation;
+	act->iparam2 = parameters.duration;
 }
 
 /* RST
@@ -604,7 +561,10 @@
 /**
  * iparam1 = 0: don't drop ware on flag, 1: do drop ware on flag
  */
-void WorkerProgram::parse_return(Worker::Action* act, const std::vector<std::string>&) {
+void WorkerProgram::parse_return(Worker::Action* act, const std::vector<std::string>& cmd) {
+	if (!cmd.empty()) {
+		throw GameDataError("Usage: return");
+	}
 	act->function = &Worker::run_return;
 	act->iparam1 = 1;  // drop a ware on our owner's flag
 }
@@ -634,11 +594,12 @@
  * sparam1 = callobject command name
  */
 void WorkerProgram::parse_callobject(Worker::Action* act, const std::vector<std::string>& cmd) {
-	if (cmd.size() != 2)
-		throw wexception("Usage: callobject=<program name>");
+	if (cmd.size() != 1) {
+		throw GameDataError("Usage: callobject=<program name>");
+	}
 
 	act->function = &Worker::run_callobject;
-	act->sparam1 = cmd[1];
+	act->sparam1 = cmd[0];
 }
 
 /* RST
@@ -691,36 +652,28 @@
  * iparam1  one of plantXXX
  */
 void WorkerProgram::parse_plant(Worker::Action* act, const std::vector<std::string>& cmd) {
-	if (cmd.size() < 2)
-		throw wexception(
-		   "Usage: plant plant=attrib:<attribute> [attrib:<attribute> ...] [unless object]");
+	if (cmd.empty()) {
+		throw GameDataError(
+		   "Usage: plant=attrib:<attribute> [attrib:<attribute> ...] [unless object]");
+	}
 
 	act->function = &Worker::run_plant;
 	act->iparam1 = Worker::Action::plantAlways;
-	for (uint32_t i = 1; i < cmd.size(); ++i) {
-		if (i >= 2 && cmd[i] == "unless") {
+	for (uint32_t i = 0; i < cmd.size(); ++i) {
+		if (cmd[i] == "unless") {
 			++i;
-			if (i >= cmd.size())
-				throw GameDataError("plant: something expected after unless");
-			if (cmd[i] == "object")
+			if (i >= cmd.size()) {
+				throw GameDataError("plant: something expected after 'unless'");
+			}
+			if (cmd[i] == "object") {
 				act->iparam1 = Worker::Action::plantUnlessObject;
-			else
+			} else {
 				throw GameDataError("plant: 'unless %s' not understood", cmd[i].c_str());
-
+			}
 			continue;
 		}
 
-		// Check if immovable type exists
-		std::vector<std::string> const list(split_string(cmd[i], ":"));
-		if (list.size() != 2 || list.at(0) != "attrib") {
-			throw GameDataError("plant takes a list of attrib:<attribute> that reference world and/or "
-			                    "tribe immovables");
-		}
-
-		const std::string& attrib_name = list[1];
-		if (attrib_name.empty()) {
-			throw GameDataError("plant has an empty attrib:<attribute> at position %d", i);
-		}
+		const std::string attrib_name = read_key_value_pair(cmd[i], ':', "", "attrib").second;
 
 		// This will throw a GameDataError if the attribute doesn't exist.
 		ImmovableDescr::get_attribute_id(attrib_name);
@@ -750,14 +703,12 @@
 */
 // TODO(GunChleoc): attrib:eatable would be much better, then depend on terrain too
 void WorkerProgram::parse_createbob(Worker::Action* act, const std::vector<std::string>& cmd) {
-	if (cmd.size() < 2)
-		throw wexception("Usage: createbob=<bob name> <bob name> ...");
+	if (cmd.empty()) {
+		throw GameDataError("Usage: createbob=<bob name> <bob name> ...");
+	}
 
 	act->function = &Worker::run_createbob;
-
-	for (uint32_t i = 1; i < cmd.size(); ++i) {
-		act->sparamv.push_back(cmd[i]);
-	}
+	act->sparamv = std::move(cmd);
 }
 
 /* RST
@@ -804,22 +755,14 @@
  * sparam1 = subcommand
  */
 void WorkerProgram::parse_repeatsearch(Worker::Action* act, const std::vector<std::string>& cmd) {
-	char* endp;
-
-	if (cmd.size() != 4)
-		throw wexception("Usage: repeatsearch=<repeat #> <radius> <subcommand>");
+	if (cmd.size() != 3) {
+		throw GameDataError("Usage: repeatsearch=<repeat #> <radius> <subcommand>");
+	}
 
 	act->function = &Worker::run_repeatsearch;
-
-	act->iparam1 = strtol(cmd[1].c_str(), &endp, 0);
-	if (*endp)
-		throw wexception("Bad repeat count '%s'", cmd[1].c_str());
-
-	act->iparam2 = strtol(cmd[2].c_str(), &endp, 0);
-	if (*endp)
-		throw wexception("Bad radius '%s'", cmd[2].c_str());
-
-	act->sparam1 = cmd[3];
+	act->iparam1 = read_positive(cmd[0]);
+	act->iparam2 = read_positive(cmd[1]);
+	act->sparam1 = cmd[2];
 }
 
 /* RST
@@ -840,8 +783,9 @@
       }
 */
 void WorkerProgram::parse_findresources(Worker::Action* act, const std::vector<std::string>& cmd) {
-	if (cmd.size() != 1)
-		throw wexception("Usage: findresources");
+	if (!cmd.empty()) {
+		throw GameDataError("Usage: findresources");
+	}
 
 	act->function = &Worker::run_findresources;
 }
@@ -867,11 +811,12 @@
  * iparam2 = time
  */
 void WorkerProgram::parse_scout(Worker::Action* act, const std::vector<std::string>& cmd) {
-	if (cmd.size() != 3)
-		throw wexception("Usage: scout=<radius> <time>");
+	if (cmd.size() != 2) {
+		throw GameDataError("Usage: scout=<radius> <time>");
+	}
 
-	act->iparam1 = atoi(cmd[1].c_str());
-	act->iparam2 = atoi(cmd[2].c_str());
+	act->iparam1 = atoi(cmd[0].c_str());
+	act->iparam2 = atoi(cmd[1].c_str());
 	act->function = &Worker::run_scout;
 }
 
@@ -902,17 +847,12 @@
       }
 */
 void WorkerProgram::parse_playsound(Worker::Action* act, const std::vector<std::string>& cmd) {
-	if (cmd.size() < 3 || cmd.size() > 4)
-		throw wexception("Usage: playsound <sound_dir> <sound_name> [priority]");
-
-	act->iparam2 = SoundHandler::register_fx(SoundType::kAmbient, cmd[1]);
-
+	//  50% chance to play, only one instance at a time
+	PlaySoundParameters parameters = MapObjectProgram::parse_act_play_sound(cmd, kFxPriorityMedium);
+
+	act->iparam1 = parameters.priority;
+	act->iparam2 = parameters.fx;
 	act->function = &Worker::run_playsound;
-	act->iparam1 = cmd.size() == 2 ? kFxPriorityMedium : atoi(cmd[2].c_str());
-	if (act->iparam1 < kFxPriorityLowest) {
-		throw GameDataError("Minmum priority for sounds is %d, but only %d was specified for %s",
-		                    kFxPriorityLowest, act->iparam1, cmd[1].c_str());
-	}
 }
 
 /* RST
@@ -943,8 +883,9 @@
  * for construction. This is used in ship building.
  */
 void WorkerProgram::parse_construct(Worker::Action* act, const std::vector<std::string>& cmd) {
-	if (cmd.size() != 1)
-		throw wexception("Usage: construct");
+	if (!cmd.empty()) {
+		throw GameDataError("Usage: construct");
+	}
 
 	act->function = &Worker::run_construct;
 }

=== modified file 'src/logic/map_objects/tribes/worker_program.h'
--- src/logic/map_objects/tribes/worker_program.h	2019-02-23 11:00:49 +0000
+++ src/logic/map_objects/tribes/worker_program.h	2019-05-26 08:37:46 +0000
@@ -24,6 +24,7 @@
 
 #include "base/macros.h"
 #include "logic/map_objects/bob.h"
+#include "logic/map_objects/map_object_program.h"
 #include "logic/map_objects/tribes/tribes.h"
 #include "logic/map_objects/tribes/workarea_info.h"
 #include "logic/map_objects/tribes/worker.h"
@@ -35,20 +36,13 @@
 // declaration (Chicken-and-egg problem)
 class WorkerDescr;
 
-struct WorkerProgram : public BobProgramBase {
+struct WorkerProgram : public MapObjectProgram {
 
 	using ParseWorkerProgramFn = void (WorkerProgram::*)(Worker::Action*,
 	                                                     const std::vector<std::string>&);
 
-	WorkerProgram(const std::string& name, const WorkerDescr& worker, const Tribes& tribes)
-	   : name_(name), worker_(worker), tribes_(tribes) {
-	}
-	~WorkerProgram() override {
-	}
+	WorkerProgram(const std::string& init_name, const LuaTable& actions_table, const WorkerDescr& worker, const Tribes& tribes);
 
-	std::string get_name() const override {
-		return name_;
-	}
 	using Actions = std::vector<Worker::Action>;
 	Actions::size_type get_size() const {
 		return actions_.size();
@@ -62,7 +56,6 @@
 		return &actions_[idx];
 	}
 
-	void parse(const LuaTable& table);
 	const WorkareaInfo& get_workarea_info() const {
 		return workarea_info_;
 	}
@@ -92,7 +85,6 @@
 	void parse_playsound(Worker::Action* act, const std::vector<std::string>& cmd);
 	void parse_construct(Worker::Action* act, const std::vector<std::string>& cmd);
 
-	const std::string name_;
 	const WorkerDescr& worker_;
 	const Tribes& tribes_;
 	Actions actions_;

=== modified file 'src/logic/map_objects/world/critter.cc'
--- src/logic/map_objects/world/critter.cc	2019-05-26 08:37:42 +0000
+++ src/logic/map_objects/world/critter.cc	2019-05-26 08:37:46 +0000
@@ -27,22 +27,21 @@
 #include <stdint.h>
 
 #include "base/wexception.h"
-#include "helper.h"
 #include "io/fileread.h"
 #include "io/filewrite.h"
 #include "logic/field.h"
 #include "logic/game.h"
 #include "logic/game_data_error.h"
+#include "logic/map_objects/map_object_program.h"
 #include "logic/map_objects/tribes/tribe_descr.h"
-#include "logic/map_objects/world/critter_program.h"
 #include "logic/map_objects/world/world.h"
 #include "map_io/world_legacy_lookup_table.h"
 #include "scripting/lua_table.h"
 
 namespace Widelands {
 
-void CritterProgram::parse(const std::vector<std::string>& lines) {
-	for (const std::string& line : lines) {
+CritterProgram::CritterProgram(const std::string& name, const LuaTable& actions_table) : MapObjectProgram(name) {
+	for (const std::string& line : actions_table.array_entries<std::string>()) {
 		try {
 			const std::vector<std::string> cmd(split_string(line, " \t\r\n"));
 			if (cmd.empty())
@@ -109,9 +108,7 @@
 	std::unique_ptr<LuaTable> programs = table.get_table("programs");
 	for (const std::string& program_name : programs->keys<std::string>()) {
 		try {
-			std::unique_ptr<CritterProgram> prog(new CritterProgram(program_name));
-			prog->parse(programs->get_table(program_name)->array_entries<std::string>());
-			programs_[program_name] = prog.release();
+			programs_[program_name] = std::unique_ptr<CritterProgram>(new CritterProgram(program_name, *programs->get_table(program_name).get()));
 		} catch (const std::exception& e) {
 			throw wexception("Parse error in program %s: %s", program_name.c_str(), e.what());
 		}
@@ -126,9 +123,6 @@
 }
 
 CritterDescr::~CritterDescr() {
-	for (auto program : programs_) {
-		delete program.second;
-	}
 }
 
 bool CritterDescr::is_swimming() const {
@@ -145,7 +139,7 @@
 	Programs::const_iterator const it = programs_.find(programname);
 	if (it == programs_.end())
 		throw wexception("%s has no program '%s'", name().c_str(), programname.c_str());
-	return it->second;
+	return it->second.get();
 }
 
 uint32_t CritterDescr::movecaps() const {
@@ -281,7 +275,7 @@
 	return Bob::Loader::get_task(name);
 }
 
-const BobProgramBase* Critter::Loader::get_program(const std::string& name) {
+const MapObjectProgram* Critter::Loader::get_program(const std::string& name) {
 	Critter& critter = get<Critter>();
 	return critter.descr().get_program(name);
 }

=== modified file 'src/logic/map_objects/world/critter.h'
--- src/logic/map_objects/world/critter.h	2019-02-23 11:00:49 +0000
+++ src/logic/map_objects/world/critter.h	2019-05-26 08:37:46 +0000
@@ -20,9 +20,12 @@
 #ifndef WL_LOGIC_MAP_OBJECTS_WORLD_CRITTER_H
 #define WL_LOGIC_MAP_OBJECTS_WORLD_CRITTER_H
 
+#include <memory>
+
 #include "base/macros.h"
 #include "graphic/diranimations.h"
 #include "logic/map_objects/bob.h"
+#include "logic/map_objects/world/critter_program.h"
 
 class LuaTable;
 class WorldLegacyLookupTable;
@@ -54,7 +57,7 @@
 
 private:
 	DirAnimations walk_anims_;
-	using Programs = std::map<std::string, CritterProgram*>;
+	using Programs = std::map<std::string, std::unique_ptr<const CritterProgram>>;
 	Programs programs_;
 	EditorCategory* editor_category_;  // not owned.
 	DISALLOW_COPY_AND_ASSIGN(CritterDescr);
@@ -83,7 +86,7 @@
 		Loader();
 
 		const Task* get_task(const std::string& name) override;
-		const BobProgramBase* get_program(const std::string& name) override;
+		const MapObjectProgram* get_program(const std::string& name) override;
 	};
 
 private:

=== modified file 'src/logic/map_objects/world/critter_program.h'
--- src/logic/map_objects/world/critter_program.h	2019-02-23 11:00:49 +0000
+++ src/logic/map_objects/world/critter_program.h	2019-05-26 08:37:46 +0000
@@ -21,8 +21,10 @@
 #define WL_LOGIC_MAP_OBJECTS_WORLD_CRITTER_PROGRAM_H
 
 #include "logic/map_objects/bob.h"
+#include "logic/map_objects/map_object_program.h"
 
 namespace Widelands {
+class Critter;
 
 struct CritterAction {
 	using CritterExecuteActionFn = bool (Critter::*)(Game&, Bob::State&, const CritterAction&);
@@ -40,15 +42,9 @@
 	std::vector<std::string> sparamv;
 };
 
-struct CritterProgram : public BobProgramBase {
-	explicit CritterProgram(const std::string& name) : name_(name) {
-	}
-	~CritterProgram() override {
-	}
+struct CritterProgram : public MapObjectProgram {
+	explicit CritterProgram(const std::string& name, const LuaTable& actions_table);
 
-	std::string get_name() const override {
-		return name_;
-	}
 	int32_t get_size() const {
 		return actions_.size();
 	}
@@ -57,10 +53,7 @@
 		return actions_[idx];
 	}
 
-	void parse(const std::vector<std::string>& lines);
-
 private:
-	std::string name_;
 	std::vector<CritterAction> actions_;
 };
 }  // namespace Widelands

=== modified file 'src/logic/map_objects/world/resource_description.cc'
--- src/logic/map_objects/world/resource_description.cc	2019-02-23 11:00:49 +0000
+++ src/logic/map_objects/world/resource_description.cc	2019-05-26 08:37:46 +0000
@@ -21,7 +21,6 @@
 
 #include <memory>
 
-#include "helper.h"
 #include "logic/game_data_error.h"
 #include "scripting/lua_table.h"
 

=== modified file 'src/network/CMakeLists.txt'
--- src/network/CMakeLists.txt	2019-05-04 15:37:33 +0000
+++ src/network/CMakeLists.txt	2019-05-26 08:37:46 +0000
@@ -44,7 +44,6 @@
     build_info
     chat
     game_io
-    helper
     io_fileread
     io_filesystem
     io_stream

=== modified file 'src/network/gameclient.cc'
--- src/network/gameclient.cc	2019-04-29 16:22:08 +0000
+++ src/network/gameclient.cc	2019-05-26 08:37:46 +0000
@@ -31,7 +31,6 @@
 #include "build_info.h"
 #include "config.h"
 #include "game_io/game_loader.h"
-#include "helper.h"
 #include "io/fileread.h"
 #include "io/filesystem/filesystem_exceptions.h"
 #include "io/filewrite.h"

=== modified file 'src/network/gamehost.cc'
--- src/network/gamehost.cc	2019-05-11 22:55:40 +0000
+++ src/network/gamehost.cc	2019-05-26 08:37:46 +0000
@@ -40,7 +40,6 @@
 #include "chat/chat.h"
 #include "game_io/game_loader.h"
 #include "game_io/game_preload_packet.h"
-#include "helper.h"
 #include "io/fileread.h"
 #include "io/filesystem/layered_filesystem.h"
 #include "logic/filesystem_constants.h"

=== modified file 'src/scripting/CMakeLists.txt'
--- src/scripting/CMakeLists.txt	2019-05-05 18:53:14 +0000
+++ src/scripting/CMakeLists.txt	2019-05-26 08:37:46 +0000
@@ -70,7 +70,6 @@
     base_i18n
     base_macros
     build_info
-    helper
     io_filesystem
     scripting_base
     scripting_errors

=== modified file 'src/scripting/lua_path.cc'
--- src/scripting/lua_path.cc	2019-05-26 08:37:42 +0000
+++ src/scripting/lua_path.cc	2019-05-26 08:37:46 +0000
@@ -24,7 +24,6 @@
 #include <boost/lexical_cast.hpp>
 
 #include "base/macros.h"
-#include "helper.h"
 #include "io/filesystem/layered_filesystem.h"
 
 namespace {

=== modified file 'src/sound/CMakeLists.txt'
--- src/sound/CMakeLists.txt	2019-04-08 13:32:28 +0000
+++ src/sound/CMakeLists.txt	2019-05-26 08:37:46 +0000
@@ -28,7 +28,6 @@
   DEPENDS
     base_i18n
     base_log
-    helper
     io_fileread
     io_filesystem
     logic_exceptions

=== modified file 'src/sound/fxset.cc'
--- src/sound/fxset.cc	2019-05-26 08:37:42 +0000
+++ src/sound/fxset.cc	2019-05-26 08:37:46 +0000
@@ -25,7 +25,6 @@
 #include <boost/regex.hpp>
 
 #include "base/log.h"
-#include "helper.h"
 #include "io/fileread.h"
 #include "io/filesystem/layered_filesystem.h"
 #include "logic/game_data_error.h"

=== modified file 'src/sound/songset.cc'
--- src/sound/songset.cc	2019-05-26 08:37:42 +0000
+++ src/sound/songset.cc	2019-05-26 08:37:46 +0000
@@ -25,7 +25,6 @@
 #include <boost/regex.hpp>
 
 #include "base/log.h"
-#include "helper.h"
 #include "io/fileread.h"
 #include "io/filesystem/layered_filesystem.h"
 

=== modified file 'src/ui_fsmenu/CMakeLists.txt'
--- src/ui_fsmenu/CMakeLists.txt	2019-05-04 15:37:33 +0000
+++ src/ui_fsmenu/CMakeLists.txt	2019-05-26 08:37:46 +0000
@@ -11,7 +11,6 @@
     graphic_text
     graphic_text_constants
     graphic_text_layout
-    helper
     io_filesystem
     logic_filesystem_constants
     scripting_lua_interface
@@ -104,7 +103,6 @@
     graphic
     graphic_playercolor
     graphic_text_constants
-    helper
     io_filesystem
     logic
     logic_game_controller

=== modified file 'src/ui_fsmenu/launch_spg.cc'
--- src/ui_fsmenu/launch_spg.cc	2019-04-26 11:33:10 +0000
+++ src/ui_fsmenu/launch_spg.cc	2019-05-26 08:37:46 +0000
@@ -27,7 +27,6 @@
 #include "base/warning.h"
 #include "base/wexception.h"
 #include "graphic/text_constants.h"
-#include "helper.h"
 #include "io/filesystem/layered_filesystem.h"
 #include "logic/game.h"
 #include "logic/game_controller.h"

=== modified file 'src/ui_fsmenu/options.cc'
--- src/ui_fsmenu/options.cc	2019-03-18 18:03:08 +0000
+++ src/ui_fsmenu/options.cc	2019-05-26 08:37:46 +0000
@@ -37,7 +37,6 @@
 #include "graphic/text/font_set.h"
 #include "graphic/text_constants.h"
 #include "graphic/text_layout.h"
-#include "helper.h"
 #include "io/filesystem/layered_filesystem.h"
 #include "logic/filesystem_constants.h"
 #include "profile/profile.h"

=== modified file 'src/wui/CMakeLists.txt'
--- src/wui/CMakeLists.txt	2019-05-05 18:53:14 +0000
+++ src/wui/CMakeLists.txt	2019-05-26 08:37:46 +0000
@@ -84,7 +84,6 @@
     base_i18n
     base_log
     base_time_string
-    helper
     game_io
     graphic_fonthandler
     graphic_image_io

=== modified file 'src/wui/load_or_save_game.cc'
--- src/wui/load_or_save_game.cc	2019-05-26 08:37:42 +0000
+++ src/wui/load_or_save_game.cc	2019-05-26 08:37:46 +0000
@@ -31,7 +31,6 @@
 #include "game_io/game_loader.h"
 #include "game_io/game_preload_packet.h"
 #include "graphic/font_handler.h"
-#include "helper.h"
 #include "io/filesystem/filesystem_exceptions.h"
 #include "io/filesystem/layered_filesystem.h"
 #include "logic/filesystem_constants.h"


Follow ups