← Back to team overview

widelands-dev team mailing list archive

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

 

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

Commit message:
Add capability to add custom scenario buildings

- map:scripting/tribes/init.lua is now parsed when loading a scenario
- The contents of map:scripting/tribes are included in savegames
- Refactored the calculate_trainingsites_proportions to be called once during postload
- Added 2 new functions to Lua interface: list_directory and is_directory


Requested reviews:
  Widelands Developers (widelands-dev)
Related bugs:
  Bug #1705950 in widelands: "empire mission 4"
  https://bugs.launchpad.net/widelands/+bug/1705950

For more details, see:
https://code.launchpad.net/~widelands-dev/widelands/add_custom_building/+merge/334062

Since I don't know to fix https://code.launchpad.net/~widelands-dev/widelands/dynamic_tribe_loading/+merge/329198/comments/874618, here's a branch that has the adding of scenario tribes only in it.

I have attached a test scenario to https://bugs.launchpad.net/widelands/+bug/1705950/comments/38


-- 
Your team Widelands Developers is requested to review the proposed merge of lp:~widelands-dev/widelands/add_custom_building into lp:widelands.
=== modified file 'src/game_io/game_loader.cc'
--- src/game_io/game_loader.cc	2017-07-02 21:00:58 +0000
+++ src/game_io/game_loader.cc	2017-11-21 17:15:37 +0000
@@ -84,6 +84,15 @@
 	M.read(fs_, game_);
 	log("Game: Reading Map Data took %ums\n", timer.ms_since_last_query());
 
+	// This has to be loaded after the map packet so that the map's filesystem will exist.
+	// The custom tribe scripts are saved when the map scripting packet is saved, but we need
+	// to load them as early as possible here.
+	if (fs_.file_exists("map/scripting/tribes/init.lua")) {
+		log("Game: Reading Scenario Tribes ... ");
+		game_.lua().run_script("map:scripting/tribes/init.lua");
+		log("Game: Reading Scenario Tribes took %ums\n", timer.ms_since_last_query());
+	}
+
 	log("Game: Reading Player Info ... ");
 	{
 		GamePlayerInfoPacket p;

=== modified file 'src/io/filewrite.cc'
--- src/io/filewrite.cc	2017-08-18 14:27:26 +0000
+++ src/io/filewrite.cc	2017-11-21 17:15:37 +0000
@@ -36,7 +36,7 @@
 	filepos_ = 0;
 }
 
-void FileWrite::write(FileSystem& fs, char const* const filename) {
+void FileWrite::write(FileSystem& fs, const std::string& filename) {
 	fs.write(filename, data_, length_);
 	clear();
 }

=== modified file 'src/io/filewrite.h'
--- src/io/filewrite.h	2017-08-18 17:32:16 +0000
+++ src/io/filewrite.h	2017-11-21 17:15:37 +0000
@@ -76,7 +76,7 @@
 	/// Write the file out to disk. If successful, this clears the buffers.
 	/// Otherwise, an exception is thrown but the buffer remains intact (don't
 	/// worry, it will be cleared by the destructor).
-	void write(FileSystem& fs, char const* const filename);
+	void write(FileSystem& fs, const std::string& filename);
 
 	/// Get the position that will be written to in the next write operation that
 	/// does not specify a position.

=== modified file 'src/logic/game.cc'
--- src/logic/game.cc	2017-11-06 20:19:56 +0000
+++ src/logic/game.cc	2017-11-21 17:15:37 +0000
@@ -211,6 +211,12 @@
 	loader_ui.step(_("Loading tribes"));
 	tribes();
 
+	// If the scenario has custrom tribe entites, load them.
+	const std::string custom_tribe_script = mapname + "/scripting/tribes/init.lua";
+	if (g_fs->file_exists(custom_tribe_script)) {
+		lua().run_script(custom_tribe_script);
+	}
+
 	// We have to create the players here.
 	loader_ui.step(_("Creating players"));
 	PlayerNumber const nr_players = map().get_nrplayers();

=== modified file 'src/logic/map_objects/tribes/tribe_descr.cc'
--- src/logic/map_objects/tribes/tribe_descr.cc	2017-07-19 20:40:32 +0000
+++ src/logic/map_objects/tribes/tribe_descr.cc	2017-11-21 17:15:37 +0000
@@ -165,66 +165,7 @@
 
 		for (const std::string& buildingname :
 		     table.get_table("buildings")->array_entries<std::string>()) {
-			try {
-				DescriptionIndex index = tribes_.safe_building_index(buildingname);
-				if (has_building(index)) {
-					throw GameDataError("Duplicate definition of building '%s'", buildingname.c_str());
-				}
-				buildings_.push_back(index);
-
-				// Register trainigsites
-				if (get_building_descr(index)->type() == MapObjectType::TRAININGSITE) {
-					trainingsites_.push_back(index);
-				}
-
-				// Register construction materials
-				for (const auto& build_cost : get_building_descr(index)->buildcost()) {
-					if (!is_construction_material(build_cost.first)) {
-						construction_materials_.insert(build_cost.first);
-					}
-				}
-				for (const auto& enhancement_cost : get_building_descr(index)->enhancement_cost()) {
-					if (!is_construction_material(enhancement_cost.first)) {
-						construction_materials_.insert(enhancement_cost.first);
-					}
-				}
-			} catch (const WException& e) {
-				throw GameDataError("Failed adding building '%s': %s", buildingname.c_str(), e.what());
-			}
-		}
-
-		// Set default trainingsites proportions for AI. Make sure that we get a sum of ca. 100
-		float trainingsites_without_percent = 0.f;
-		int used_percent = 0;
-		for (const DescriptionIndex& index : trainingsites_) {
-			const BuildingDescr& descr = *tribes_.get_building_descr(index);
-			if (descr.hints().trainingsites_max_percent() == 0) {
-				++trainingsites_without_percent;
-			} else {
-				used_percent += descr.hints().trainingsites_max_percent();
-			}
-		}
-		if (trainingsites_without_percent > 0.f && used_percent > 100) {
-			throw GameDataError(
-			   "Predefined training sites proportions add up to > 100%%: %d", used_percent);
-		} else if (trainingsites_without_percent > 0) {
-			const int percent_to_use = std::ceil((100 - used_percent) / trainingsites_without_percent);
-			if (percent_to_use < 1) {
-				throw GameDataError("Training sites without predefined proportions add up to < 1%% and "
-				                    "will never be built: %d",
-				                    used_percent);
-			}
-			for (const DescriptionIndex& index : trainingsites_) {
-				BuildingDescr* descr = tribes_.get_mutable_building_descr(index);
-				if (descr->hints().trainingsites_max_percent() == 0) {
-					descr->set_hints_trainingsites_max_percent(percent_to_use);
-					used_percent += percent_to_use;
-				}
-			}
-		}
-		if (used_percent < 100) {
-			throw GameDataError(
-			   "Final training sites proportions add up to < 100%%: %d", used_percent);
+			add_building(buildingname);
 		}
 
 		// Special types
@@ -503,6 +444,35 @@
 	}
 }
 
+void TribeDescr::add_building(const std::string& buildingname) {
+	try {
+		DescriptionIndex index = tribes_.safe_building_index(buildingname);
+		if (has_building(index)) {
+			throw GameDataError("Duplicate definition of building '%s'", buildingname.c_str());
+		}
+		buildings_.push_back(index);
+
+		// Register trainigsites
+		if (get_building_descr(index)->type() == MapObjectType::TRAININGSITE) {
+			trainingsites_.push_back(index);
+		}
+
+		// Register construction materials
+		for (const auto& build_cost : get_building_descr(index)->buildcost()) {
+			if (!is_construction_material(build_cost.first)) {
+				construction_materials_.insert(build_cost.first);
+			}
+		}
+		for (const auto& enhancement_cost : get_building_descr(index)->enhancement_cost()) {
+			if (!is_construction_material(enhancement_cost.first)) {
+				construction_materials_.insert(enhancement_cost.first);
+			}
+		}
+	} catch (const WException& e) {
+		throw GameDataError("Failed adding building '%s': %s", buildingname.c_str(), e.what());
+	}
+}
+
 /**
   * Helper functions
   */

=== modified file 'src/logic/map_objects/tribes/tribe_descr.h'
--- src/logic/map_objects/tribes/tribe_descr.h	2017-07-19 20:40:32 +0000
+++ src/logic/map_objects/tribes/tribe_descr.h	2017-11-21 17:15:37 +0000
@@ -157,6 +157,8 @@
 		return ship_names_;
 	}
 
+	void add_building(const std::string& buildingname);
+
 private:
 	// Helper function for adding a special worker type (carriers etc.)
 	DescriptionIndex add_special_worker(const std::string& workername);

=== modified file 'src/logic/map_objects/tribes/tribes.cc'
--- src/logic/map_objects/tribes/tribes.cc	2017-09-22 19:54:27 +0000
+++ src/logic/map_objects/tribes/tribes.cc	2017-11-21 17:15:37 +0000
@@ -178,6 +178,17 @@
 	}
 }
 
+void Tribes::add_custom_building(const LuaTable& table) {
+	const std::string tribename = table.get_string("tribename");
+	if (Widelands::tribe_exists(tribename)) {
+		TribeDescr* descr = tribes_->get_mutable(tribe_index(tribename));
+		const std::string buildingname = table.get_string("buildingname");
+		descr->add_building(buildingname);
+	} else {
+		throw GameDataError("The tribe '%s'' has no preload file.", tribename.c_str());
+	}
+}
+
 size_t Tribes::nrbuildings() const {
 	return buildings_->size();
 }
@@ -194,9 +205,15 @@
 	return workers_->size();
 }
 
+bool Tribes::ware_exists(const std::string& warename) const {
+	return wares_->exists(warename) != nullptr;
+}
 bool Tribes::ware_exists(const DescriptionIndex& index) const {
 	return wares_->get_mutable(index) != nullptr;
 }
+bool Tribes::worker_exists(const std::string& workername) const {
+	return workers_->exists(workername) != nullptr;
+}
 bool Tribes::worker_exists(const DescriptionIndex& index) const {
 	return workers_->get_mutable(index) != nullptr;
 }
@@ -360,12 +377,74 @@
 			buildings_->get_mutable(enhancement)->set_enhanced_from(i);
 		}
 	}
+
+	// Calculate the trainingsites proportions.
+	postload_calculate_trainingsites_proportions();
+
 	// Resize the configuration of our wares if they won't fit in the current window (12 = info label
-	// size)
+	// size).
 	int number = (g_gr->get_yres() - 290) / (WARE_MENU_PIC_HEIGHT + WARE_MENU_PIC_PAD_Y + 12);
 	for (DescriptionIndex i = 0; i < tribes_->size(); ++i) {
-		tribes_->get_mutable(i)->resize_ware_orders(number);
-	}
-}
+		TribeDescr* tribe_descr = tribes_->get_mutable(i);
+		tribe_descr->resize_ware_orders(number);
+	}
+}
+
+
+// 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);
+		unsigned int trainingsites_without_percent = 0;
+		int used_percent = 0;
+		std::vector<BuildingDescr*> traingsites_with_percent;
+		for (const DescriptionIndex& index : tribe_descr->trainingsites()) {
+			BuildingDescr* descr = get_mutable_building_descr(index);
+			if (descr->hints().trainingsites_max_percent() == 0) {
+				++trainingsites_without_percent;
+			} else {
+				used_percent += descr->hints().trainingsites_max_percent();
+				traingsites_with_percent.push_back(descr);
+			}
+		}
+
+		log("%s trainingsites: We have used up %d%% on %lu sites, there are %d without\n", tribe_descr->name().c_str(), used_percent, traingsites_with_percent.size(), trainingsites_without_percent);
+
+		// Adjust used_percent if we don't have at least 5% for each remaining trainingsite
+		const float limit = 100 - trainingsites_without_percent * 5;
+		if (used_percent > limit) {
+			const int deductme = (used_percent - limit) / traingsites_with_percent.size();
+			used_percent = 0;
+			for (BuildingDescr* descr : traingsites_with_percent) {
+					descr->set_hints_trainingsites_max_percent(descr->hints().trainingsites_max_percent() - deductme);
+					used_percent += descr->hints().trainingsites_max_percent();
+			}
+			log("%s trainingsites: Used percent was adjusted to %d%%\n", tribe_descr->name().c_str(), used_percent);
+		}
+
+		// Now adjust for trainingsites that didn't have their max_percent set
+		if (trainingsites_without_percent > 0) {
+			const int percent_to_use = std::ceil((100 - used_percent) / trainingsites_without_percent);
+			log("%s trainingsites: Assigning %d%% to each of the remaining %d sites\n", tribe_descr->name().c_str(), percent_to_use, trainingsites_without_percent);
+			if (percent_to_use < 1) {
+				throw GameDataError("%s: Training sites without predefined proportions add up to < 1%% and "
+										  "will never be built: %d",
+										  tribe_descr->name().c_str(), used_percent);
+			}
+			for (const DescriptionIndex& index : tribe_descr->trainingsites()) {
+				BuildingDescr* descr = get_mutable_building_descr(index);
+				if (descr->hints().trainingsites_max_percent() == 0) {
+					descr->set_hints_trainingsites_max_percent(percent_to_use);
+					used_percent += percent_to_use;
+				}
+			}
+		}
+		if (used_percent < 100) {
+			throw GameDataError(
+				"%s: Final training sites proportions add up to < 100%%: %d", tribe_descr->name().c_str(), used_percent);
+		}
+	}
+}
+
 
 }  // namespace Widelands

=== modified file 'src/logic/map_objects/tribes/tribes.h'
--- src/logic/map_objects/tribes/tribes.h	2017-09-22 19:54:27 +0000
+++ src/logic/map_objects/tribes/tribes.h	2017-11-21 17:15:37 +0000
@@ -106,12 +106,16 @@
 	/// Adds a specific tribe's configuration.
 	void add_tribe(const LuaTable& table, const EditorGameBase& egbase);
 
+	void add_custom_building(const LuaTable& table);
+
 	size_t nrbuildings() const;
 	size_t nrtribes() const;
 	size_t nrwares() const;
 	size_t nrworkers() const;
 
+	bool ware_exists(const std::string& warename) const;
 	bool ware_exists(const DescriptionIndex& index) const;
+	bool worker_exists(const std::string& workername) const;
 	bool worker_exists(const DescriptionIndex& index) const;
 	bool building_exists(const std::string& buildingname) const;
 	bool building_exists(const DescriptionIndex& index) const;
@@ -152,6 +156,8 @@
 	void postload();
 
 private:
+	void postload_calculate_trainingsites_proportions();
+
 	std::unique_ptr<DescriptionMaintainer<BuildingDescr>> buildings_;
 	std::unique_ptr<DescriptionMaintainer<ImmovableDescr>> immovables_;
 	std::unique_ptr<DescriptionMaintainer<ShipDescr>> ships_;

=== modified file 'src/map_io/map_scripting_packet.cc'
--- src/map_io/map_scripting_packet.cc	2017-01-25 18:55:59 +0000
+++ src/map_io/map_scripting_packet.cc	2017-11-21 17:15:37 +0000
@@ -38,6 +38,21 @@
 
 namespace {
 constexpr uint32_t kCurrentPacketVersion = 3;
+
+// Write all .lua files that exist in the given 'path' in 'map_fs' to the 'target_fs'.
+void write_lua_dir(FileSystem& target_fs, FileSystem* map_fs, const std::string& path) {
+	if (map_fs) {
+		target_fs.ensure_directory_exists(path);
+		for (const std::string& script :
+		     filter(map_fs->list_directory(path),
+		            [](const std::string& fn) { return boost::ends_with(fn, ".lua"); })) {
+			size_t length;
+			void* input_data = map_fs->load(script, length);
+			target_fs.write(script, input_data, length);
+			free(input_data);
+		}
+	}
+}
 }  // namespace
    /*
     * ========================================================================
@@ -67,18 +82,13 @@
 }
 
 void MapScriptingPacket::write(FileSystem& fs, EditorGameBase& egbase, MapObjectSaver& mos) {
-	fs.ensure_directory_exists("scripting");
-
+	// Write any scenario scripting files in the map's basic scripting dir
 	FileSystem* map_fs = egbase.map().filesystem();
-	if (map_fs) {
-		for (const std::string& script :
-		     filter(map_fs->list_directory("scripting"),
-		            [](const std::string& fn) { return boost::ends_with(fn, ".lua"); })) {
-			size_t length;
-			void* input_data = map_fs->load(script, length);
-			fs.write(script, input_data, length);
-			free(input_data);
-		}
+	write_lua_dir(fs, map_fs, "scripting");
+
+	// Write any custom scenario tribe entities
+	if (map_fs->file_exists("scripting/tribes/init.lua")) {
+		write_lua_dir(fs, map_fs, "scripting/tribes");
 	}
 
 	// Dump the global environment if this is a game and not in the editor

=== modified file 'src/scripting/lua_path.cc'
--- src/scripting/lua_path.cc	2017-06-24 08:47:46 +0000
+++ src/scripting/lua_path.cc	2017-11-21 17:15:37 +0000
@@ -143,7 +143,7 @@
    :returns: An :class:`array` of file paths in lexicographical order.
 */
 static int L_list_files(lua_State* L) {
-	std::string filename_template = luaL_checkstring(L, 1);
+	const std::string filename_template = luaL_checkstring(L, 1);
 
 	NumberGlob glob(filename_template);
 	std::string filename;
@@ -161,9 +161,48 @@
 	return 1;
 }
 
+/* RST
+.. function:: list_directory(filename)
+
+   Returns all file names contained in the given directory.
+
+   :type filename: class:`string`
+   :arg filename: The directory to read.
+
+   :returns: An :class:`array` of file names.
+*/
+static int L_list_directory(lua_State* L) {
+	lua_newtable(L);
+	int idx = 1;
+	for (const std::string& filename : g_fs->list_directory(luaL_checkstring(L, 1))) {
+		lua_pushint32(L, idx++);
+		lua_pushstring(L, filename);
+		lua_settable(L, -3);
+	}
+	return 1;
+}
+
+
+/* RST
+.. function:: is_directory(filename)
+
+   Checks whether the given filename points to a directory.
+
+   :type filename: class:`string`
+   :arg filename: The filename to check.
+
+   :returns: ``true`` if the given path is a directory.
+*/
+static int L_is_directory(lua_State* L) {
+	lua_pushboolean(L, g_fs->is_directory(luaL_checkstring(L, -1)));
+	return 1;
+}
+
 const static struct luaL_Reg path[] = {{"basename", &L_basename},
                                        {"dirname", &L_dirname},
                                        {"list_files", &L_list_files},
+													{"list_directory", &L_list_directory},
+													{"is_directory", &L_is_directory},
                                        {nullptr, nullptr}};
 
 void luaopen_path(lua_State* L) {

=== modified file 'src/scripting/lua_root.cc'
--- src/scripting/lua_root.cc	2017-09-15 12:33:58 +0000
+++ src/scripting/lua_root.cc	2017-11-21 17:15:37 +0000
@@ -517,6 +517,7 @@
    METHOD(LuaTribes, new_ware_type),
    METHOD(LuaTribes, new_warehouse_type),
    METHOD(LuaTribes, new_worker_type),
+	METHOD(LuaTribes, add_custom_building),
    {0, 0},
 };
 const PropertyType<LuaTribes> LuaTribes::Properties[] = {
@@ -877,6 +878,42 @@
 	return 0;
 }
 
+
+/* RST
+	.. method:: add_custom_building{table}
+
+		Adds a custom building to a tribe, e.g. for use in a scenario.
+		The building must already be known to the tribes and should be defined in
+		the ``map:scripting/tribes/`` directory.
+
+		**Note:** This function *has* to be called from ``map:scripting/tribes/init.lua``.
+
+		The table has the following entries:
+
+		**tribename**
+			*Mandatory*. The name of the tribe that this building will be added to.
+
+		**buildingname**
+			*Mandatory*. The name of the building to be added to the tribe.
+
+		:returns: :const:`0`
+*/
+int LuaTribes::add_custom_building(lua_State* L) {
+	if (lua_gettop(L) != 2) {
+		report_error(L, "Takes only one argument.");
+	}
+
+	try {
+		LuaTable table(L);  // Will pop the table eventually.
+		EditorGameBase& egbase = get_egbase(L);
+		egbase.mutable_tribes()->add_custom_building(table);
+	} catch (std::exception& e) {
+		report_error(L, "%s", e.what());
+	}
+	return 0;
+}
+
+
 /*
  ==========================================================
  C METHODS

=== modified file 'src/scripting/lua_root.h'
--- src/scripting/lua_root.h	2017-09-15 12:33:58 +0000
+++ src/scripting/lua_root.h	2017-11-21 17:15:37 +0000
@@ -173,6 +173,7 @@
 	int new_ware_type(lua_State* L);
 	int new_warehouse_type(lua_State* L);
 	int new_worker_type(lua_State* L);
+	int add_custom_building(lua_State* L);
 
 	/*
 	 * C methods


Follow ups