widelands-dev team mailing list archive
-
widelands-dev team
-
Mailing list archive
-
Message #11613
[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
-
[Merge] lp:~widelands-dev/widelands/add_custom_building into lp:widelands
From: noreply, 2017-11-25
-
Re: [Merge] lp:~widelands-dev/widelands/add_custom_building into lp:widelands
From: GunChleoc, 2017-11-25
-
Re: [Merge] lp:~widelands-dev/widelands/add_custom_building into lp:widelands
From: hessenfarmer, 2017-11-25
-
Re: [Merge] lp:~widelands-dev/widelands/add_custom_building into lp:widelands
From: TiborB, 2017-11-24
-
Re: [Merge] lp:~widelands-dev/widelands/add_custom_building into lp:widelands
From: hessenfarmer, 2017-11-24
-
Re: [Merge] lp:~widelands-dev/widelands/add_custom_building into lp:widelands
From: GunChleoc, 2017-11-24
-
Re: [Merge] lp:~widelands-dev/widelands/add_custom_building into lp:widelands
From: hessenfarmer, 2017-11-24
-
Re: [Merge] lp:~widelands-dev/widelands/add_custom_building into lp:widelands
From: GunChleoc, 2017-11-24
-
Re: [Merge] lp:~widelands-dev/widelands/add_custom_building into lp:widelands
From: TiborB, 2017-11-24
-
Re: [Merge] lp:~widelands-dev/widelands/add_custom_building into lp:widelands
From: hessenfarmer, 2017-11-23
-
Re: [Merge] lp:~widelands-dev/widelands/add_custom_building into lp:widelands
From: GunChleoc, 2017-11-22
-
[Merge] lp:~widelands-dev/widelands/add_custom_building into lp:widelands
From: bunnybot, 2017-11-21
-
Re: [Merge] lp:~widelands-dev/widelands/add_custom_building into lp:widelands
From: hessenfarmer, 2017-11-21