Skip to content

Commit

Permalink
Actually allow maps and lua files to be placed within the campaign fo…
Browse files Browse the repository at this point in the history
…lder.

Although this is documented it was not implemented.
Make both folders optional defaulting to the campaign folder.
  • Loading branch information
Flamefire committed Nov 7, 2024
1 parent 9491d60 commit cad6d09
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 33 deletions.
29 changes: 16 additions & 13 deletions doc/AddingCustomCampaign.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,23 +81,26 @@ You need to implement this and return the version your script works with. If it

If you want a field to be translated you have to add the translation as described above and set the variable to _"<key>". The _"..." will translate the text during application execution depending on your language settings.

1. `version` simple a number for versioning of the campaign
2. `author` human readable string of the campaign creator
3. `name` the name of the campaign
4. `shortDescription` Short description of the campaign (like a head line to get a rough imagination of the campaign)
5. `longDescription` Extended description describing the campaign in detail. Will be shown in the campaign selection screen, when the campaign is selected.
6. `image` Path to an image displayed in the campaign selection screen. You can ommit this if you do no want to provide an image.
7. `maxHumanPlayers` for now this is always 1 until we support multiplayer campaigns
8. `difficulty` difficulty of the campaign. Should be one of the valus easy, medium or hard.
9. `mapFolder` and `luaFolder` Path to the folder containing the campaign maps and associated lua files. Usually your campaign folder or a subfolder of it
10. `maps` List of the names of the files of the campaigns mission maps
11. `selectionMap` Optional parameter. See [map selection screen](#selection-map) for detailed explanations.
1. `version`: Simple a number for versioning of the campaign
2. `author`: Human readable string of the campaign creator
3. `name`: The name of the campaign
4. `shortDescription`: Short description of the campaign (like a head line to get a rough imagination of the campaign)
5. `longDescription`: Extended description describing the campaign in detail. Will be shown in the campaign selection screen, when the campaign is selected.
6. `image`: Path to an image displayed in the campaign selection screen. You can ommit this if you do no want to provide an image.
7. `maxHumanPlayers`: For now this is always 1 until we support multiplayer campaigns
8. `difficulty`: Difficulty of the campaign. Should be one of the valus easy, medium or hard.
9. `mapFolder` and `luaFolder`: Path to the folder containing the campaign maps and associated lua files. Usually your campaign folder or a subfolder of it.
10. `maps`: List of the names of the files of the campaigns mission maps
11. `selectionMap`: Optional parameter. See [map selection screen](#selection-map) for detailed explanations.

Hints:
- The lua file of a map must have the same name as the map it self but with the extension `.lua` to be found correctly. The lua and the map file must not be in the same folder because the path can be specified differently.
- To work on case sensitive os (like linux) the file name of the lua file must have the same case as the map file name. This applies to the map names in the campaign.lua file too.
For example: `MISS01.WLD, MISS01.lua` is correct and `MISS01.WLD, miss01.lua` will not work on linux
- All paths can contain placeholders like `<RTTR_RTTR>, ...`
- The lua file of a map must have the same name as the map itself but with the extension `.lua` to be found.
- The lua and the map file don't need to be in the same folder because the path can be specified separately.
- If `mapFolder` is not specified or empty it defaults to the folder containing the campaign lua file.
- If `luaFolder` is not specified it defaults to the `mapFolder`.
- Both paths can start with placeholders like `<RTTR_GAME>`, otherwise they need to be only the name of a folder relative to the folder containing the campaign lua file. I.e. multiple levels are not supported.

### Optional map selection screen {#selection-map}

Expand Down
28 changes: 19 additions & 9 deletions libs/libGamedata/gameData/CampaignDescription.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (C) 2005 - 2023 Settlers Freaks (sf-team at siedler25.org)
// Copyright (C) 2005 - 2024 Settlers Freaks (sf-team at siedler25.org)
//
// SPDX-License-Identifier: GPL-2.0-or-later

Expand All @@ -9,7 +9,7 @@
#include "lua/LuaHelpers.h"
#include "mygettext/mygettext.h"

CampaignDescription::CampaignDescription(const kaguya::LuaRef& table)
CampaignDescription::CampaignDescription(const boost::filesystem::path& campaignPath, const kaguya::LuaRef& table)
{
CheckedLuaTable luaData(table);
luaData.getOrThrow(version, "version");
Expand All @@ -30,11 +30,21 @@ CampaignDescription::CampaignDescription(const kaguya::LuaRef& table)
if(difficulty != gettext_noop("easy") && difficulty != gettext_noop("medium") && difficulty != gettext_noop("hard"))
throw std::invalid_argument(helpers::format(_("Invalid difficulty: %1%"), difficulty));

luaData.getOrThrow(mapFolder, "mapFolder");
luaData.getOrThrow(luaFolder, "luaFolder");
lua::validatePath(mapFolder);
lua::validatePath(luaFolder);
mapNames = luaData.getOrDefault("maps", std::vector<std::string>());
auto resolveFolder = [campaignPath](const std::string& folder) {
const boost::filesystem::path tmpPath = folder;
// If it is only a filename or empty use path relative to campaign folder
if(!tmpPath.has_parent_path())
return campaignPath / tmpPath;
// Otherwise it must be a valid path inside the game files
lua::validatePath(folder);
return RTTRCONFIG.ExpandPath(folder);
};

const auto mapFolder = luaData.getOrDefault("mapFolder", std::string{});
mapFolder_ = resolveFolder(mapFolder);
// Default lua folder to map folder, i.e. LUA files are side by side with the maps
luaFolder_ = resolveFolder(luaData.getOrDefault("luaFolder", mapFolder));
mapNames_ = luaData.getOrDefault("maps", std::vector<std::string>());
selectionMapData = luaData.getOptional<SelectionMapInputData>("selectionMap");
luaData.checkUnused();
}
Expand All @@ -46,10 +56,10 @@ const std::optional<SelectionMapInputData>& CampaignDescription::getSelectionMap

boost::filesystem::path CampaignDescription::getLuaFilePath(const size_t idx) const
{
return (RTTRCONFIG.ExpandPath(luaFolder) / getMapName(idx)).replace_extension("lua");
return (luaFolder_ / getMapName(idx)).replace_extension("lua");
}

boost::filesystem::path CampaignDescription::getMapFilePath(const size_t idx) const
{
return RTTRCONFIG.ExpandPath(mapFolder) / getMapName(idx);
return mapFolder_ / getMapName(idx);
}
13 changes: 6 additions & 7 deletions libs/libGamedata/gameData/CampaignDescription.h
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (C) 2005 - 2023 Settlers Freaks (sf-team at siedler25.org)
// Copyright (C) 2005 - 2024 Settlers Freaks (sf-team at siedler25.org)
//
// SPDX-License-Identifier: GPL-2.0-or-later

Expand Down Expand Up @@ -26,15 +26,14 @@ struct CampaignDescription
std::optional<SelectionMapInputData> selectionMapData;

CampaignDescription() = default;
explicit CampaignDescription(const kaguya::LuaRef& table);
size_t getNumMaps() const { return mapNames.size(); }
const std::string& getMapName(const size_t idx) const { return mapNames.at(idx); }
explicit CampaignDescription(const boost::filesystem::path& campaignPath, const kaguya::LuaRef& table);
size_t getNumMaps() const { return mapNames_.size(); }
const std::string& getMapName(const size_t idx) const { return mapNames_.at(idx); }
boost::filesystem::path getLuaFilePath(size_t idx) const;
boost::filesystem::path getMapFilePath(size_t idx) const;
const std::optional<SelectionMapInputData>& getSelectionMapData() const;

private:
std::string mapFolder;
std::string luaFolder;
std::vector<std::string> mapNames;
boost::filesystem::path mapFolder_, luaFolder_;
std::vector<std::string> mapNames_;
};
2 changes: 1 addition & 1 deletion libs/libGamedata/lua/CampaignDataLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ bool CampaignDataLoader::Load()
if(entry.type() != LUA_TTABLE)
throw std::runtime_error("Campaign table variable missing.");

campaignDesc_ = CampaignDescription(entry);
campaignDesc_ = CampaignDescription(basePath_, entry);
} catch(std::exception& e)
{
LOG.write("Failed to load campaign data!\nReason: %1%\nCurrent file being processed: %2%\n") % e.what()
Expand Down
107 changes: 104 additions & 3 deletions tests/s25Main/campaign/testCampaignLuaFile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,108 @@ BOOST_AUTO_TEST_CASE(LoadCampaignWithoutImage)
BOOST_TEST(!desc.image);
}

BOOST_AUTO_TEST_CASE(HandleMapAndLuaPaths)
{
rttr::test::TmpFolder tmp;
{
bnw::ofstream file(tmp / "campaign.lua");
file << R"(
campaign = {
version = "1",
author = "Max Meier",
name = "My campaign",
shortDescription = "short",
longDescription = "long",
maxHumanPlayers = 1,
difficulty = "easy",
maps = { "map.WLD" }
}
function getRequiredLuaVersion() return 1 end
)";
}

// Without mapFolder and luaFolder paths default to campaign path
{
CampaignDescription desc;
CampaignDataLoader loader(desc, tmp);
BOOST_TEST_REQUIRE(loader.Load());
BOOST_TEST(desc.getMapFilePath(0) == tmp / "map.WLD");
BOOST_TEST(desc.getLuaFilePath(0) == tmp / "map.lua");
}

// Only folder name is a subdirectory to the campaign
{
bnw::ofstream file(tmp / "campaign.lua", std::ios_base::app);
file << R"(
campaign["mapFolder"] = "maps"
)";
}
{
CampaignDescription desc;
CampaignDataLoader loader(desc, tmp);
BOOST_TEST_REQUIRE(loader.Load());
BOOST_TEST(desc.getMapFilePath(0) == tmp / "maps" / "map.WLD");
BOOST_TEST(desc.getLuaFilePath(0) == tmp / "maps" / "map.lua");
}

// Lua folder can be different
{
bnw::ofstream file(tmp / "campaign.lua", std::ios_base::app);
file << R"(
campaign["luaFolder"] = "scripts"
)";
}
{
CampaignDescription desc;
CampaignDataLoader loader(desc, tmp);
BOOST_TEST_REQUIRE(loader.Load());
BOOST_TEST(desc.getMapFilePath(0) == tmp / "maps" / "map.WLD");
BOOST_TEST(desc.getLuaFilePath(0) == tmp / "scripts" / "map.lua");
}

// Empty folder is the same as the campaign folder
{
bnw::ofstream file(tmp / "campaign.lua", std::ios_base::app);
file << R"(
campaign["luaFolder"] = ""
)";
}
{
CampaignDescription desc;
CampaignDataLoader loader(desc, tmp);
BOOST_TEST_REQUIRE(loader.Load());
BOOST_TEST(desc.getMapFilePath(0) == tmp / "maps" / "map.WLD");
BOOST_TEST(desc.getLuaFilePath(0) == tmp / "map.lua");
}

// More than a folder name is forbidden
rttr::test::LogAccessor logAcc;
{
bnw::ofstream file(tmp / "campaign.lua", std::ios_base::app);
file << R"(
campaign["luaFolder"] = "subdir/scripts"
)";
}
{
CampaignDescription desc;
CampaignDataLoader loader(desc, tmp);
BOOST_TEST_REQUIRE(!loader.Load());
RTTR_REQUIRE_LOG_CONTAINS_SOME("Invalid path 'subdir/scripts", false);
}
{
bnw::ofstream file(tmp / "campaign.lua", std::ios_base::app);
file << R"(
campaign["mapFolder"] = "subdir/maps"
)";
}
{
CampaignDescription desc;
CampaignDataLoader loader(desc, tmp);
BOOST_TEST_REQUIRE(!loader.Load());
RTTR_REQUIRE_LOG_CONTAINS_SOME("Invalid path 'subdir/maps", false);
}
}

BOOST_AUTO_TEST_CASE(LoadCampaignDescriptionFailsDueToMissingCampaignVariable)
{
rttr::test::TmpFolder tmp;
Expand Down Expand Up @@ -264,7 +366,6 @@ BOOST_AUTO_TEST_CASE(LoadCampaignDescriptionFailsDueToMissingField)
longDescription = "This is the long description",
image = "<RTTR_GAME>/GFX/PICS/WORLD.LBM",
maxHumanPlayers = 1,
difficulty = "easy",
mapFolder = "<RTTR_GAME>/DATA/MAPS",
maps = { "dessert0.WLD", "dessert1.WLD", "dessert2.WLD"}
}
Expand All @@ -278,7 +379,7 @@ BOOST_AUTO_TEST_CASE(LoadCampaignDescriptionFailsDueToMissingField)
rttr::test::LogAccessor logAcc;
BOOST_TEST(!loader.Load());
RTTR_REQUIRE_LOG_CONTAINS(
"Failed to load campaign data!\nReason: Failed to load game data: Required field 'luaFolder' not found", false);
"Failed to load campaign data!\nReason: Failed to load game data: Required field 'difficulty' not found", false);
}

BOOST_AUTO_TEST_CASE(CampaignDescriptionLoadWithTranslation)
Expand Down Expand Up @@ -348,7 +449,7 @@ BOOST_AUTO_TEST_CASE(CampaignDescriptionLoadWithTranslation)
BOOST_TEST(desc.getLuaFilePath(2) == RTTRCONFIG.ExpandPath("<RTTR_GAME>/CAMPAIGNS/ROMAN/dessert2.lua"));

// selection map
BOOST_TEST(!desc.getSelectionMapData().has_value());
BOOST_TEST(!desc.selectionMapData);
}

BOOST_AUTO_TEST_CASE(OptionalSelectionMapLoadTest)
Expand Down
4 changes: 4 additions & 0 deletions tests/testHelpers/rttr/test/LogAccessor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ struct LogAccessor
\
} while(false)

/// Require that the log contains "content" somewhere
#define RTTR_REQUIRE_LOG_CONTAINS_SOME(content, allowEmpty) \
BOOST_TEST_REQUIRE(logAcc.getLog().find(content) != std::string::npos, "Log does not contain: " << (content))

#define RTTR_REQUIRE_ASSERT(stmt) \
do \
{ \
Expand Down

0 comments on commit cad6d09

Please sign in to comment.