Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 23 additions & 13 deletions doc/AddingCustomCampaign.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ SPDX-License-Identifier: GPL-2.0-or-later

# Add a custom campaign

You can have a look into the already existing original Settlers 2 campaigns in the subfolders `RTTR/campaigns/roman` and `RTTR/campaigns/continent` for an example.
You can take a look into the already existing original Settlers 2 campaigns in the subfolders `RTTR/campaigns/roman` and `RTTR/campaigns/continent` for an example.
We will now create an example campaign `garden`.

## Location for adding a new campaign
Expand Down Expand Up @@ -41,18 +41,20 @@ campaign = {
name = _"name",
shortDescription = _"shortDescription",
longDescription = _"longDescription",
image = "<RTTR_RTTR>/campaigns/garden/garden.bmp",
image = "<RTTR_RTTR>/campaigns/garden/garden.bmp", -- Same as: image = "garden.bmp",
Comment thread
Flamefire marked this conversation as resolved.
maxHumanPlayers= 1,
difficulty = "easy",
mapFolder = "<RTTR_RTTR>/campaigns/garden",
luaFolder = "<RTTR_RTTR>/campaigns/garden",
mapFolder = "<RTTR_RTTR>/campaigns/garden", -- optional
luaFolder = "<RTTR_RTTR>/campaigns/garden", -- optional
maps = { "MISS01.WLD","MISS02.WLD"},
selectionMap = {
background = {"<RTTR_GAME>/campaigns/garden/mapscreen/background.bmp", 0},
map = {"<RTTR_GAME>/campaigns/garden/mapscreen/map.bmp", 0},
missionMapMask = {"<RTTR_GAME>/campaigns/garden/mapscreen/map_mask.bmp", 0},
marker = {"<RTTR_GAME>/campaigns/garden/mapscreen/marker.bmp", 0},
conquered = {"<RTTR_GAME>/campaigns/garden/mapscreen/conquered.bmp", 0},
background = {"<RTTR_RTTR>/campaigns/garden/mapscreen/background.bmp", 0},
map = {"<RTTR_RTTR>/campaigns/garden/mapscreen/map.bmp", 0},
missionMapMask = {"<RTTR_RTTR>/campaigns/garden/mapscreen/map_mask.bmp", 0},
marker = {"<RTTR_RTTR>/campaigns/garden/mapscreen/marker.bmp", 0},
conquered = {"<RTTR_RTTR>/campaigns/garden/mapscreen/conquered.bmp", 0},
-- Each '<RTTR_RTTR>/campaigns/garden/' is optional: Paths are treated as relative to campaign file.
-- E.g.: conquered = {"mapscreen/conquered.bmp", 0},
backgroundOffset = {0, 0},
disabledColor = 0x70000000,
missionSelectionInfos = {
Expand All @@ -75,7 +77,7 @@ The Lua campaign interface is versioned using a major version. Every time a feat

Every map script must have 1 function:
`getRequiredLuaVersion()`
You need to implement this and return the version your script works with. If it does not match the current version an error will be shown and the script will not be used.
You need to implement this and return the version your script works with. If it is higher than the current version an error will be shown and the script will not be used.

### Explanation of the campaign table fields

Expand All @@ -86,7 +88,7 @@ If you want a field to be translated you have to add the translation as describe
3. `name`: The name of the campaign
4. `shortDescription`: Short description of the campaign (like a headline 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 omit this if you do no want to provide an image.
6. `image`: Path to an image displayed in the campaign selection screen. Can be any "archive" with an image, e.g. `.bmp`, `.lbm`. You can omit 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 values 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.
Expand All @@ -95,7 +97,7 @@ If you want a field to be translated you have to add the translation as describe

Hints:

- 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.
- 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
- 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.
Expand All @@ -107,7 +109,7 @@ For example: `MISS01.WLD, MISS01.lua` is correct and `MISS01.WLD, miss01.lua` wi

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

This parameter is optional and can be omitted in the Lua campaign file. If this parameter is specified the selection screen for the missions of a campaign is replaced by a selection map. Like the one used in the original settlers 2 world campaign.
This parameter is optional and can be omitted in the Lua campaign file. If this parameter is specified the selection screen for the missions of a campaign is replaced by a selection map. Like the one used in the original Settlers 2 world campaign.

We have the following parameters:

Expand All @@ -124,6 +126,11 @@ Hint:
All the images are described by the path to the image file and an index parameter. Usually the index parameter is zero.
For special image formats containing multiple images in an archive this is the index of the image to use.

### Image paths

The paths to the campaign image and selection map images can be relative to the campaign folder and at most inside a single subfolder.
E.g. `images/garden.bmp` and `mapscreen/conquered.bmp` work, but `images/mapscreen/conquered.bmp` does not.

## Final view of the example garden campaign folder

```sh
Expand All @@ -140,3 +147,6 @@ RTTR/campaigns/garden/mapscreen/map_mask.bmp
RTTR/campaigns/garden/mapscreen/marker.bmp
RTTR/campaigns/garden/mapscreen/conquered.bmp
```

Note that for custom campaigns, placed in `<RTTR_USERDATA>/campaigns/` the top-level `RTTR` folder won't exist.
Hence, `<RTTR_RTTR>/campaigns/` should not be used in the campaign description file to refer to campaign files.
5 changes: 3 additions & 2 deletions doc/lua/functions.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!--
Copyright (C) 2005 - 2021 Settlers Freaks <sf-team at siedler25.org>
Copyright (C) 2005 - 2026 Settlers Freaks <sf-team at siedler25.org>

SPDX-License-Identifier: GPL-2.0-or-later
-->
Expand All @@ -24,7 +24,8 @@ Reference: [libs/libGamedata/lua/LuaInterfaceBase.cpp](../../libs/libGamedata/lu
**rttr:GetFeatureLevel()**
Get the current feature level of the LUA interface.
Increases here indicate new features.
The current version is **6**.
The current version is **7**.
See [list of changes](main.md#versioning).

**rttr:Log(message)**
Log the message to console.
Expand Down
6 changes: 5 additions & 1 deletion doc/lua/main.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!--
Copyright (C) 2005 - 2021 Settlers Freaks <sf-team at siedler25.org>
Copyright (C) 2005 - 2026 Settlers Freaks <sf-team at siedler25.org>

SPDX-License-Identifier: GPL-2.0-or-later
-->
Expand Down Expand Up @@ -47,6 +47,10 @@ You need to implement this and return the major/main version your script works w
If it does not match the current version an error will be shown and the script will not be used.
See also `rttr:GetFeatureLevel()`.

### Feature level 7

- Allow relative paths for images referenced by campaign files.

## Example

```lua
Expand Down
65 changes: 49 additions & 16 deletions libs/libGamedata/gameData/CampaignDescription.cpp
Original file line number Diff line number Diff line change
@@ -1,25 +1,56 @@
// Copyright (C) 2005 - 2024 Settlers Freaks (sf-team at siedler25.org)
// Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org)
//
// SPDX-License-Identifier: GPL-2.0-or-later

#include "CampaignDescription.h"
#include "RttrConfig.h"
#include "helpers/containerUtils.h"
#include "helpers/format.hpp"
#include "lua/CheckedLuaTable.h"
#include "lua/LuaHelpers.h"
#include "mygettext/mygettext.h"

CampaignDescription::CampaignDescription(const boost::filesystem::path& campaignPath, const kaguya::LuaRef& table)
{
const auto resolveCampaignPath = [campaignPath](const std::string& path, bool isFolder) {
const boost::filesystem::path tmpPath = path;
if(tmpPath.is_relative())
{
// If it is only a file/folder name or empty use path relative to campaign folder
if(!tmpPath.has_parent_path())
return (campaignPath / tmpPath).string();
if(!isFolder)
{
// For files only allow a single sub folder
const auto parentPath = tmpPath.parent_path();
if(!parentPath.parent_path().has_parent_path())
{
// Only alpha-numeric folder names are allowed
Comment thread
Flamefire marked this conversation as resolved.
const auto isAlNum = [](const char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9');
};
const auto isNonAlNum = [isAlNum](const char c) { return !isAlNum(c); };
lua::assertTrue(!helpers::contains_if(parentPath.string(), isNonAlNum),
helpers::format(_("Invalid path '%1%': Must be alpha-numeric"), path));
return (campaignPath / tmpPath).string();
}
}
}
// Otherwise it must be a valid path inside the game files
lua::validatePath(path);
return path;
};

CheckedLuaTable luaData(table);
luaData.getOrThrow(version, "version");
luaData.getOrThrow(author, "author");
luaData.getOrThrow(name, "name");
luaData.getOrThrow(shortDescription, "shortDescription");
luaData.getOrThrow(longDescription, "longDescription");
image = luaData.getOptional<std::string>("image");
if(image && image->empty())
image = std::nullopt;
const auto imageValue = luaData.getOptional<std::string>("image");
if(imageValue && !imageValue->empty())
image = resolveCampaignPath(*imageValue, false);

luaData.getOrThrow(maxHumanPlayers, "maxHumanPlayers");

if(maxHumanPlayers != 1)
Expand All @@ -30,22 +61,24 @@ CampaignDescription::CampaignDescription(const boost::filesystem::path& campaign
if(difficulty != gettext_noop("easy") && difficulty != gettext_noop("medium") && difficulty != gettext_noop("hard"))
throw std::invalid_argument(helpers::format(_("Invalid difficulty: %1%"), difficulty));

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);
mapFolder_ = RTTRCONFIG.ExpandPath(resolveCampaignPath(mapFolder, true));
// Default lua folder to map folder, i.e. LUA files are side by side with the maps
luaFolder_ = resolveFolder(luaData.getOrDefault("luaFolder", mapFolder));
luaFolder_ = RTTRCONFIG.ExpandPath(resolveCampaignPath(luaData.getOrDefault("luaFolder", mapFolder), true));
mapNames_ = luaData.getOrDefault("maps", std::vector<std::string>());
selectionMapData = luaData.getOptional<SelectionMapInputData>("selectionMap");
if(selectionMapData)
{
const auto updatePath = [resolveCampaignPath](std::string& path) {
if(!path.empty())
path = resolveCampaignPath(path, false);
};
updatePath(selectionMapData->background.filePath);
updatePath(selectionMapData->map.filePath);
updatePath(selectionMapData->missionMapMask.filePath);
updatePath(selectionMapData->marker.filePath);
updatePath(selectionMapData->conquered.filePath);
}
luaData.checkUnused();
}

Expand Down
2 changes: 1 addition & 1 deletion libs/libGamedata/gameData/CampaignDescription.h
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (C) 2005 - 2024 Settlers Freaks (sf-team at siedler25.org)
// Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org)
//
// SPDX-License-Identifier: GPL-2.0-or-later

Expand Down
3 changes: 1 addition & 2 deletions libs/libGamedata/gameData/SelectionMapInputData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,7 @@ struct lua_type_traits<ImageResource>
const LuaStackRef table(l, index);
if(table.type() != LUA_TTABLE || table.size() != 2)
throw LuaTypeMismatch();
std::string path = table[1];
return get_type(boost::filesystem::path(path), table[2]);
return get_type(table[1], table[2]);
}
static int push(lua_State* l, push_type v) { return util::push_args(l, v.filePath, v.index); }
};
Expand Down
5 changes: 2 additions & 3 deletions libs/libGamedata/gameData/SelectionMapInputData.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@ class LuaRef;

struct ImageResource
{
boost::filesystem::path filePath;
std::string filePath;
unsigned index;
ImageResource(boost::filesystem::path path = boost::filesystem::path(), unsigned index = 0)
: filePath(std::move(path)), index(index){};
ImageResource(std::string path = "", unsigned index = 0) : filePath(std::move(path)), index(index){};
};

struct MissionSelectionInfo
Expand Down
4 changes: 2 additions & 2 deletions libs/libGamedata/lua/CampaignDataLoader.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 - 2026 Settlers Freaks (sf-team at siedler25.org)
//
// SPDX-License-Identifier: GPL-2.0-or-later

Expand All @@ -17,7 +17,7 @@

unsigned CampaignDataLoader::GetVersion()
{
return 2;
return 3;
}

CampaignDataLoader::CampaignDataLoader(CampaignDescription& campaignDesc, const boost::filesystem::path& basePath)
Expand Down
4 changes: 2 additions & 2 deletions libs/s25main/controls/ctrlMapSelection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
ctrlMapSelection::MapImages::MapImages(const SelectionMapInputData& data)
{
auto getImage = [](const ImageResource& res) {
auto* img = LOADER.GetImageN(ResourceId::make(res.filePath), res.index);
auto* img = LOADER.GetImageN(ResourceId::fromPath(res.filePath), res.index);
if(!img)
throw std::runtime_error(
helpers::format(_("Loading of images %s for map selection failed."), res.filePath));
Expand All @@ -31,7 +31,7 @@ ctrlMapSelection::MapImages::MapImages(const SelectionMapInputData& data)
{
std::vector<std::string> pathsToLoad;
for(const auto& res : {data.background, data.map, data.missionMapMask, data.marker, data.conquered})
pathsToLoad.push_back(res.filePath.string());
pathsToLoad.push_back(res.filePath);
LOADER.LoadFiles(pathsToLoad);
}

Expand Down
2 changes: 1 addition & 1 deletion libs/s25main/desktops/dskCampaignSelection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ void dskCampaignSelection::Msg_TableSelectItem(const unsigned ctrl_id, const boo
mapSelection->setMissionsStatus(std::vector<MissionStatus>(campaign.getNumMaps(), {true, true}));
mapSelection->setPreview(true);
} else if(campaign.image)
campaignImage_ = LOADER.GetImageN(ResourceId::fromPath(*campaign.image), 0);
campaignImage_ = LOADER.GetImageN(ResourceId::make(*campaign.image), 0);
else
campaignImage_ = nullptr;
}
Expand Down
4 changes: 2 additions & 2 deletions libs/s25main/lua/LuaInterfaceGameBase.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (C) 2005 - 2021 Settlers Freaks (sf-team at siedler25.org)
// Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org)
//
// SPDX-License-Identifier: GPL-2.0-or-later

Expand All @@ -16,7 +16,7 @@ unsigned LuaInterfaceGameBase::GetVersion()

unsigned LuaInterfaceGameBase::GetFeatureLevel()
{
return 6;
return 7;
}

LuaInterfaceGameBase::LuaInterfaceGameBase(const ILocalGameState& localGameState) : localGameState(localGameState)
Expand Down
38 changes: 32 additions & 6 deletions tests/libGameData/testCampaignLuaFile.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (C) 2005 - 2024 Settlers Freaks (sf-team at siedler25.org)
// Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org)
//
// SPDX-License-Identifier: GPL-2.0-or-later

Expand Down Expand Up @@ -212,7 +212,8 @@ BOOST_AUTO_TEST_CASE(HandleMapAndLuaPaths)
longDescription = "long",
maxHumanPlayers = 1,
difficulty = "easy",
maps = { "map.WLD" }
maps = { "map.WLD" },
image = "myimage.LBM"
}
function getRequiredLuaVersion() return 1 end
)";
Expand All @@ -225,6 +226,8 @@ BOOST_AUTO_TEST_CASE(HandleMapAndLuaPaths)
BOOST_TEST_REQUIRE(loader.Load());
BOOST_TEST(desc.getMapFilePath(0) == tmp / "map.WLD");
BOOST_TEST(desc.getLuaFilePath(0) == tmp / "map.lua");
// Similar for image
BOOST_TEST(desc.image == tmp / "myimage.LBM");
}

// Only folder name is a subdirectory to the campaign
Expand Down Expand Up @@ -298,6 +301,28 @@ BOOST_AUTO_TEST_CASE(HandleMapAndLuaPaths)
BOOST_TEST_REQUIRE(!loader.Load());
RTTR_REQUIRE_LOG_CONTAINS_SOME("Invalid path 'subdir/maps", false);
}

{
bnw::ofstream file(tmp / "campaign.lua", std::ios_base::app);
file << R"(
campaign["mapFolder"] = ""
campaign["luaFolder"] = ""
)";
}
// Relative image paths are only allowed to be a single sub folder with alpha-numeric name
for(const auto& invValue : {"sub/subsub/img", "../sub/img", "sub/../img", "../img", "Th!s/img", "/abs", "/abs/img"})
{
BOOST_TEST_INFO_SCOPE("Value: " << invValue);
// Similar the image must only be a name if not a <RTTR template
{
bnw::ofstream file(tmp / "campaign.lua", std::ios_base::app);
file << "campaign[\"image\"] = \"" << invValue << '"';
}
CampaignDescription desc;
CampaignDataLoader loader(desc, tmp);
BOOST_TEST_REQUIRE(!loader.Load());
RTTR_REQUIRE_LOG_CONTAINS_SOME("Invalid path '" + std::string(invValue), false);
}
}

BOOST_AUTO_TEST_CASE(LoadCampaignDescriptionFailsDueToMissingCampaignVariable)
Expand Down Expand Up @@ -474,8 +499,9 @@ BOOST_AUTO_TEST_CASE(OptionalSelectionMapLoadTest)
background = {"<RTTR_GAME>/GFX/PICS/SETUP990.LBM", 0},
map = {"<RTTR_GAME>/GFX/PICS/WORLD.LBM", 0},
missionMapMask = {"<RTTR_GAME>/GFX/PICS/WORLDMSK.LBM", 0},
marker = {"<RTTR_GAME>/DATA/IO/IO.DAT", 231},
conquered = {"<RTTR_GAME>/DATA/IO/IO.DAT", 232},
-- Can be relative to campaign folder
marker = {"marker.DAT", 231},
conquered = {"imgs/conquered.DAT", 232},
backgroundOffset = {64, 70},
disabledColor = 0x70000000,
missionSelectionInfos = {
Expand Down Expand Up @@ -521,9 +547,9 @@ BOOST_AUTO_TEST_CASE(OptionalSelectionMapLoadTest)
BOOST_TEST(selectionMap->map.index == 0u);
BOOST_TEST(selectionMap->missionMapMask.filePath == "<RTTR_GAME>/GFX/PICS/WORLDMSK.LBM");
BOOST_TEST(selectionMap->missionMapMask.index == 0u);
BOOST_TEST(selectionMap->marker.filePath == "<RTTR_GAME>/DATA/IO/IO.DAT");
BOOST_TEST(selectionMap->marker.filePath == tmp / "marker.DAT");
BOOST_TEST(selectionMap->marker.index == 231u);
BOOST_TEST(selectionMap->conquered.filePath == "<RTTR_GAME>/DATA/IO/IO.DAT");
BOOST_TEST(selectionMap->conquered.filePath == tmp / "imgs/conquered.DAT");
BOOST_TEST(selectionMap->conquered.index == 232u);
BOOST_TEST(selectionMap->mapOffsetInBackground == Position(64, 70));
BOOST_TEST(selectionMap->disabledColor == 0x70000000u);
Expand Down
Loading
Loading