diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 6d5906d03..04f60c3ff 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -57,8 +57,10 @@ jobs: # Use C++ 17 standard (default for gcc-11) # Use system cmake # Use system boost - - { compiler: gcc-11, os: ubuntu-22.04, type: Debug, cmake: 3.22.1, boost: 1.74.0 } - - { compiler: clang-14, os: ubuntu-22.04, type: Debug, cmake: 3.22.1, boost: 1.74.0, cxx: 17 } + # Also include a (semi-)optimized build to a) shake out issues there an b) run the replay tests + - { compiler: clang-14, os: ubuntu-22.04, type: Debug, cmake: 3.22.1, boost: 1.74.0, cxx: 17 } + - { compiler: gcc-11, os: ubuntu-22.04, type: Debug, cmake: 3.22.1, boost: 1.74.0 } + - { compiler: gcc-11, os: ubuntu-22.04, type: RelWithDebInfo, cmake: 3.22.1, boost: 1.74.0 } # # Latest Compilers # GCC 12 needs boost 1.82 to compile correctly @@ -191,7 +193,7 @@ jobs: run: echo "TRAVIS_BUILD_DIR=$GITHUB_WORKSPACE" >> $GITHUB_ENV - name: Build - run: tools/ci/travisBuild.sh + run: tools/ci/build.sh - run: tools/ci/collectCoverageData.sh && external/libutil/tools/ci/uploadCoverageData.sh if: matrix.coverage && success() diff --git a/CMakeLists.txt b/CMakeLists.txt index 688d873bb..4583f4889 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,10 +4,10 @@ cmake_minimum_required(VERSION 3.9) if(POLICY CMP0093) - cmake_policy(SET CMP0093 NEW) # Use Boost_VERSION x.y.z in CMake >= 3.15 or BoostConfig + cmake_policy(SET CMP0093 NEW) # Use Boost_VERSION x.y.z in CMake >= 3.15 or BoostConfig endif() if (POLICY CMP0074) - cmake_policy(SET CMP0074 NEW) # find_package uses _ROOT variables + cmake_policy(SET CMP0074 NEW) # find_package uses _ROOT variables endif() if(NOT RTTR_VERSION) @@ -325,18 +325,18 @@ endif() option(RTTR_USE_SYSTEM_BOOST_NOWIDE "Use system installed Boost.Nowide. Fails if not found!" "${RTTR_USE_SYSTEM_LIBS}") if(Boost_VERSION_MINOR GREATER_EQUAL "74" OR RTTR_USE_SYSTEM_BOOST_NOWIDE) - # Boost 1.73 contains Boost.Nowide, 1.74 a required fix - find_package(Boost COMPONENTS nowide) + # Boost 1.73 contains Boost.Nowide, 1.74 a required fix + find_package(Boost COMPONENTS nowide) endif() # Prefer system version, either as part of Boost or preinstalled if(NOT Boost_NOWIDE_FOUND) - if(RTTR_USE_SYSTEM_BOOST_NOWIDE) - set(bnw_required REQUIRED) - else() - set(bnw_required QUIET) - endif() - find_package(boost_nowide 11.0.0 ${bnw_required}) - # Fallback in libutil, but find_package must be done here so all subprojects can use it + if(RTTR_USE_SYSTEM_BOOST_NOWIDE) + set(bnw_required REQUIRED) + else() + set(bnw_required QUIET) + endif() + find_package(boost_nowide 11.0.0 ${bnw_required}) + # Fallback in libutil, but find_package must be done here so all subprojects can use it endif() add_subdirectory(external) diff --git a/appveyor.yml b/appveyor.yml index dc03294da..1985b2c47 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -21,7 +21,7 @@ platform: - x64 environment: - BOOST_ROOT: C:\Libraries\boost_1_77_0 + BOOST_ROOT: C:\Libraries\boost_1_83_0 GENERATOR: Visual Studio 16 2019 RTTR_DISABLE_ASSERT_BREAKPOINT: 1 diff --git a/data/RTTR/MAPS b/data/RTTR/MAPS index ad6d25a72..dfb992375 160000 --- a/data/RTTR/MAPS +++ b/data/RTTR/MAPS @@ -1 +1 @@ -Subproject commit ad6d25a72e65dd9ecf25fab4c6f2e225cbfb5302 +Subproject commit dfb9923753678ed58573855ba579db3f238caca7 diff --git a/data/RTTR/campaigns/world/campaign.lua b/data/RTTR/campaigns/world/campaign.lua index 1531914e6..aa9ceb7dd 100644 --- a/data/RTTR/campaigns/world/campaign.lua +++ b/data/RTTR/campaigns/world/campaign.lua @@ -1,6 +1,6 @@ ---- Campaign lua version ------ function getRequiredLuaVersion() - return 1 + return 2 end rttr:RegisterTranslations( @@ -29,6 +29,26 @@ campaign = { maxHumanPlayers= 1, difficulty = "easy", mapFolder = "/DATA/MAPS2", - luaFolder = "/campaigns/world", - maps = { "AFRICA.WLD","AUSTRA.WLD","EUROPE.WLD","GREEN.WLD","JAPAN.WLD","NAMERICA.WLD","NASIA.WLD","SAMERICA.WLD","SASIA.WLD"} + luaFolder = "/CAMPAIGNS/WORLD", + maps = { "EUROPE.WLD","NAMERICA.WLD","SAMERICA.WLD","GREEN.WLD","AFRICA.WLD","NASIA.WLD","SASIA.WLD","JAPAN.WLD","AUSTRA.WLD"}, + selectionMap = { + background = {"/GFX/PICS/SETUP990.LBM", 0}, + map = {"/GFX/PICS/WORLD.LBM", 0}, + missionMapMask = {"/GFX/PICS/WORLDMSK.LBM", 0}, + marker = {"/DATA/IO/IO.DAT", 231}, + conquered = {"/DATA/IO/IO.DAT", 232}, + backgroundOffset = {64, 70}, + disabledColor = 0x70000000, + missionSelectionInfos = { + {0xffffff00, 243, 97}, + {0xffaf73cb, 55,78}, + {0xff008fc3, 122, 193}, + {0xff43c373, 166, 36}, + {0xff27871b, 241,176}, + {0xffc32323, 366,87}, + {0xff573327, 375,145}, + {0xffcfaf4b, 486, 136}, + {0xffbb6313, 441, 264} + } + } } diff --git a/doc/AddingCustomCampaign.md b/doc/AddingCustomCampaign.md index 51e3d4503..d4f140d12 100644 --- a/doc/AddingCustomCampaign.md +++ b/doc/AddingCustomCampaign.md @@ -47,7 +47,20 @@ campaign = { difficulty = "easy", mapFolder = "/campaigns/garden", luaFolder = "/campaigns/garden", - maps = { "MISS01.WLD","MISS02.WLD"} + maps = { "MISS01.WLD","MISS02.WLD"}, + selectionMap = { + background = {"/campaigns/garden/mapscreen/background.bmp", 0}, + map = {"/campaigns/garden/mapscreen/map.bmp", 0}, + missionMapMask = {"/campaigns/garden/mapscreen/map_mask.bmp", 0}, + marker = {"/campaigns/garden/mapscreen/marker.bmp", 0}, + conquered = {"/campaigns/garden/mapscreen/conquered.bmp", 0}, + backgroundOffset = {0, 0}, + disabledColor = 0x70000000, + missionSelectionInfos = { + {0xffffff00, 100, 50}, + {0xffaf73cb, 200, 100} + } + } } ``` @@ -78,6 +91,7 @@ If you want a field to be translated you have to add the translation as describe 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. @@ -85,6 +99,23 @@ Hints: For example: `MISS01.WLD, MISS01.lua` is correct and `MISS01.WLD, miss01.lua` will not work on linux - All paths can contain placeholders like `, ...` +### Optional map selection screen {#selection-map} + +This parameter is optional and can be obmitted 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 settler 2 world campaign. + +We have the following parameters: + 1. `background` background image for the selection map + 2. `map` the map image itself + 3. `missionMapMask` this image is a mask that describes the mission areas of the `map` image. It must be the same size as the `map` image where the color of each pixel determines the mission it belongs to. Each mission must have a unique color (specified in the `missionSelectionInfos`). Any other color is treated as neutral area and ignored. + 4. `marker` the marker image shown when a mission is selected + 5. `conquered` the image shown when a mission is already finished + 6. `backgroundOffset` offset of the `map` image and `missionMapMask` image relative to the `background` image. Can be (0,0) if no offset exists. + 7. `disabledColor` color for drawing missions not playable yet. Usually this should be a partly transparent color + 8. `missionSelectionInfos` contains an entry for each mission and must be the same order as specified in `maps` lua parameter. Each entry consists of three elements. The first is the `maskAreaColor` and the two following are the `ankerPos` x and y position. The `ankerPos` is the position the `conquered` image and the `cursor` image, if mission is selected, are displayed for this mission. The offset is always counted from the origin of the `map` image. The `maskAreaColor` is the color for the mission used in the `missionMapMask`. + +Hints: +- 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. + ## Final view of the example garden campaign folder ``` RTTR/campaigns/garden @@ -94,5 +125,10 @@ RTTR/campaigns/garden/MISS01.lua RTTR/campaigns/garden/MISS01.WLD RTTR/campaigns/garden/MISS02.lua RTTR/campaigns/garden/MISS02.WLD +RTTR/campaigns/garden/mapscreen/background.bmp +RTTR/campaigns/garden/mapscreen/map.bmp +RTTR/campaigns/garden/mapscreen/map_mask.bmp +RTTR/campaigns/garden/mapscreen/marker.bmp +RTTR/campaigns/garden/mapscreen/conquered.bmp ``` diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 7d6866e3c..8f138856f 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -13,10 +13,10 @@ else() endif() option(RTTR_INCLUDE_DEVTOOLS "Include folder with precompiled binaries for development" ${devToolsExist}) if(RTTR_INCLUDE_DEVTOOLS) - add_subdirectory(dev-tools) - set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} PARENT_SCOPE) - set(CMAKE_PROGRAM_PATH ${CMAKE_PROGRAM_PATH} PARENT_SCOPE) - set(CMAKE_FIND_ROOT_PATH ${CMAKE_FIND_ROOT_PATH} PARENT_SCOPE) + add_subdirectory(dev-tools) + set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} PARENT_SCOPE) + set(CMAKE_PROGRAM_PATH ${CMAKE_PROGRAM_PATH} PARENT_SCOPE) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_FIND_ROOT_PATH} PARENT_SCOPE) endif() # Building the updater when using non-default paths (especially absolute paths) @@ -54,12 +54,15 @@ else() include(FetchContent) FetchContent_Declare( LibSamplerate - GIT_REPOSITORY https://github.com/Flamefire/libsamplerate - GIT_TAG b5d39fbfc86b656fed68c935ac7b0a06349e4aa1 + GIT_REPOSITORY https://github.com/libsndfile/libsamplerate + GIT_TAG 0.2.2 ) - set(LIBSAMPLERATE_TESTS OFF CACHE INTERNAL "") + set(LIBSAMPLERATE_EXAMPLES OFF CACHE INTERNAL "") set(LIBSAMPLERATE_INSTALL OFF CACHE INTERNAL "") + set(old_BUILD_TESTING ${BUILD_TESTING}) + set(BUILD_TESTING OFF) FetchContent_MakeAvailable(LibSamplerate) + set(BUILD_TESTING ${old_BUILD_TESTING}) endif() # No tests for turtle diff --git a/external/dev-tools b/external/dev-tools index ab8e8aadc..408f7e701 160000 --- a/external/dev-tools +++ b/external/dev-tools @@ -1 +1 @@ -Subproject commit ab8e8aadc6d79599325a584b95d1132eafe5fc88 +Subproject commit 408f7e701197515acd29e39ea4f0865314641124 diff --git a/external/libsiedler2 b/external/libsiedler2 index 31c1312c6..4f02f7a01 160000 --- a/external/libsiedler2 +++ b/external/libsiedler2 @@ -1 +1 @@ -Subproject commit 31c1312c628c49ca725c22c6ad82a93b64439ede +Subproject commit 4f02f7a01889bb308c35641ac6a7e3e68adc3907 diff --git a/external/libutil b/external/libutil index 3af81c3ef..98d6ab5b2 160000 --- a/external/libutil +++ b/external/libutil @@ -1 +1 @@ -Subproject commit 3af81c3ef22b54d326ecd4d5a2fec6d115b70c78 +Subproject commit 98d6ab5b21bf7f35a2d5ee76d6ea43c7d416f8a1 diff --git a/external/mygettext b/external/mygettext index 5a0e0a3ad..efe8aeb1d 160000 --- a/external/mygettext +++ b/external/mygettext @@ -1 +1 @@ -Subproject commit 5a0e0a3adce7af5873e1b21fd1f01c78f56f6c6b +Subproject commit efe8aeb1d7df3ea342136dd8ca88230dc76d637f diff --git a/external/s25edit b/external/s25edit index 7633036fa..4ca9689a6 160000 --- a/external/s25edit +++ b/external/s25edit @@ -1 +1 @@ -Subproject commit 7633036fafd5ea22106346f8ab92bc3a2109d9c5 +Subproject commit 4ca9689a6700b4a75eeb6e5bbd509b0eaeec4522 diff --git a/external/s25update b/external/s25update index 02dd2a394..343dc0c46 160000 --- a/external/s25update +++ b/external/s25update @@ -1 +1 @@ -Subproject commit 02dd2a394df6f7259edc8aedbb8f7b7696e8f765 +Subproject commit 343dc0c46439d2cafa206bee2ddf3d87a73162a2 diff --git a/extras/CMakeLists.txt b/extras/CMakeLists.txt index 288349a7a..edbac0174 100644 --- a/extras/CMakeLists.txt +++ b/extras/CMakeLists.txt @@ -11,23 +11,24 @@ set(CMAKE_VISIBILITY_INLINES_HIDDEN ON) add_subdirectory(audioDrivers) add_subdirectory(videoDrivers) +add_subdirectory(ai-battle) if(RTTR_BUNDLE AND APPLE) add_subdirectory(macosLauncher) endif() if(MSVC) - option(RTTR_ENABLE_VLD "Enable use of Visual Leak Detector" OFF) - option(RTTR_ENABLE_VLD_FORCED "Force using of Visual Leak Detector (in non-Debug builds)" OFF) + option(RTTR_ENABLE_VLD "Enable use of Visual Leak Detector" OFF) + option(RTTR_ENABLE_VLD_FORCED "Force using of Visual Leak Detector (in non-Debug builds)" OFF) else() - set(RTTR_ENABLE_VLD OFF) + set(RTTR_ENABLE_VLD OFF) endif() add_library(rttr_vld INTERFACE) add_library(rttr::vld ALIAS rttr_vld) target_compile_definitions(rttr_vld INTERFACE RTTR_HAS_VLD=$) if(RTTR_ENABLE_VLD) - find_package(VLD 2.5.1 REQUIRED) - target_link_libraries(rttr_vld INTERFACE vld::vld) - if(RTTR_ENABLE_VLD_FORCED) - target_compile_definitions(rttr_vld INTERFACE VLD_FORCE_ENABLE) - endif() + find_package(VLD 2.5.1 REQUIRED) + target_link_libraries(rttr_vld INTERFACE vld::vld) + if(RTTR_ENABLE_VLD_FORCED) + target_compile_definitions(rttr_vld INTERFACE VLD_FORCE_ENABLE) + endif() endif() diff --git a/extras/ai-battle/CMakeLists.txt b/extras/ai-battle/CMakeLists.txt new file mode 100644 index 000000000..b10b41b44 --- /dev/null +++ b/extras/ai-battle/CMakeLists.txt @@ -0,0 +1,11 @@ +# Copyright (C) 2005 - 2024 Settlers Freaks +# +# SPDX-License-Identifier: GPL-2.0-or-later + +add_executable(ai-battle main.cpp HeadlessGame.cpp) +target_link_libraries(ai-battle PRIVATE s25Main Boost::program_options Boost::nowide) + +if(WIN32) + include(GatherDll) + gather_dll_copy(ai-battle) +endif() diff --git a/extras/ai-battle/HeadlessGame.cpp b/extras/ai-battle/HeadlessGame.cpp new file mode 100644 index 000000000..d8cf7f95c --- /dev/null +++ b/extras/ai-battle/HeadlessGame.cpp @@ -0,0 +1,267 @@ +// Copyright (C) 2005 - 2024 Settlers Freaks (sf-team at siedler25.org) +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "HeadlessGame.h" +#include "EventManager.h" +#include "GlobalGameSettings.h" +#include "PlayerInfo.h" +#include "Savegame.h" +#include "factories/AIFactory.h" +#include "network/PlayerGameCommands.h" +#include "world/GameWorld.h" +#include "world/MapLoader.h" +#include "gameTypes/MapInfo.h" +#include "gameData/GameConsts.h" +#include +#include +#include +#include +#ifdef WIN32 +# include "Windows.h" +#endif + +std::vector GeneratePlayerInfo(const std::vector& ais); +std::string ToString(const std::chrono::milliseconds& time); +std::string HumanReadableNumber(unsigned num); + +namespace bfs = boost::filesystem; +namespace bnw = boost::nowide; +using bfs::canonical; + +#ifdef WIN32 +HANDLE setupStdOut(); +#endif + +#if defined(__MINGW32__) && !defined(__clang__) +void printConsole(const char* fmt, ...) __attribute__((format(gnu_printf, 1, 2))); +#elif defined __GNUC__ +void printConsole(const char* fmt, ...) __attribute__((format(printf, 1, 2))); +#else +void printConsole(const char* fmt, ...); +#endif + +HeadlessGame::HeadlessGame(const GlobalGameSettings& ggs, const bfs::path& map, const std::vector& ais) + : map_(map), game_(ggs, std::make_unique(0), GeneratePlayerInfo(ais)), world_(game_.world_), + em_(*static_cast(game_.em_.get())) +{ + MapLoader loader(world_); + if(!loader.Load(map)) + throw std::runtime_error("Could not load " + map.string()); + + players_.clear(); + for(unsigned playerId = 0; playerId < world_.GetNumPlayers(); ++playerId) + players_.push_back(AIFactory::Create(world_.GetPlayer(playerId).aiInfo, playerId, world_)); + + world_.InitAfterLoad(); +} + +HeadlessGame::~HeadlessGame() +{ + Close(); +} + +void HeadlessGame::Run(unsigned maxGF) +{ + AsyncChecksum checksum; + gameStartTime_ = std::chrono::steady_clock::now(); + auto nextReport = gameStartTime_ + std::chrono::seconds(1); + + game_.Start(false); + + while(em_.GetCurrentGF() < maxGF && !game_.IsGameFinished()) + { + // In the actual game, the network frame intervall is based on ping (highest_ping < NFW-length < 20*gf_length). + bool isnfw = em_.GetCurrentGF() % 20 == 0; + + if(isnfw) + { + if(replay_.IsRecording()) + checksum = AsyncChecksum::create(game_); + for(unsigned playerId = 0; playerId < world_.GetNumPlayers(); ++playerId) + { + world_.GetPlayer(playerId); + AIPlayer* player = players_[playerId].get(); + PlayerGameCommands cmds; + cmds.gcs = player->FetchGameCommands(); + + if(replay_.IsRecording() && !cmds.gcs.empty()) + { + cmds.checksum = checksum; + replay_.AddGameCommand(em_.GetCurrentGF(), playerId, cmds); + } + + for(const gc::GameCommandPtr& gc : cmds.gcs) + gc->Execute(world_, player->GetPlayerId()); + } + } + + for(auto& player : players_) + player->RunGF(em_.GetCurrentGF(), isnfw); + + game_.RunGF(); + + if(replay_.IsRecording()) + replay_.UpdateLastGF(em_.GetCurrentGF()); + + if(std::chrono::steady_clock::now() > nextReport) + { + nextReport += std::chrono::seconds(1); + PrintState(); + } + } + PrintState(); +} + +void HeadlessGame::Close() +{ + bnw::cout << '\n'; + + if(replay_.IsRecording()) + { + replay_.StopRecording(); + bnw::cout << "Replay written to " << canonical(replayPath_) << '\n'; + } + + replay_.Close(); +} + +void HeadlessGame::RecordReplay(const bfs::path& path, unsigned random_init) +{ + // Remove old replay + bfs::remove(path); + + replayPath_ = path; + + MapInfo mapInfo; + mapInfo.filepath = map_; + mapInfo.mapData.CompressFromFile(mapInfo.filepath, &mapInfo.mapChecksum); + mapInfo.type = MapType::OldMap; + + replay_.random_init = random_init; + for(unsigned playerId = 0; playerId < world_.GetNumPlayers(); ++playerId) + replay_.AddPlayer(world_.GetPlayer(playerId)); + replay_.ggs = game_.ggs_; + if(!replay_.StartRecording(path, mapInfo)) + throw std::runtime_error("Replayfile could not be opened!"); +} + +void HeadlessGame::SaveGame(const bfs::path& path) const +{ + // Remove old savegame + bfs::remove(path); + + Savegame save; + for(unsigned playerId = 0; playerId < world_.GetNumPlayers(); ++playerId) + save.AddPlayer(world_.GetPlayer(playerId)); + save.ggs = game_.ggs_; + save.ggs.exploration = Exploration::Disabled; // no FOW + save.start_gf = em_.GetCurrentGF(); + save.sgd.MakeSnapshot(game_); + save.Save(path, "AI Battle"); + + bnw::cout << "Savegame written to " << canonical(path) << '\n'; +} + +std::string ToString(const std::chrono::milliseconds& time) +{ + char buffer[90]; + const auto hours = std::chrono::duration_cast(time); + const auto minutes = std::chrono::duration_cast(time % std::chrono::hours(1)); + const auto seconds = std::chrono::duration_cast(time % std::chrono::minutes(1)); + snprintf(buffer, std::size(buffer), "%03ld:%02ld:%02ld", static_cast(hours.count()), + static_cast(minutes.count()), static_cast(seconds.count())); + return std::string(buffer); +} + +std::string HumanReadableNumber(unsigned num) +{ + std::stringstream ss; + ss.imbue(std::locale("")); + ss << std::fixed << num; + return ss.str(); +} + +void HeadlessGame::PrintState() +{ + static bool first_run = true; + if(first_run) + first_run = false; + else + printConsole("\x1b[%dA", 8 + world_.GetNumPlayers()); // Move cursor back up + + printConsole("┌───────────────┬───────────────────────┬───────────────────────┬────────────────┐\n"); + printConsole( + "│ GF %10s │ Game Clock %s │ Wall Clock %s │ %7s GF/sec │\n", HumanReadableNumber(em_.GetCurrentGF()).c_str(), + ToString(SPEED_GF_LENGTHS[GameSpeed::Normal] * em_.GetCurrentGF()).c_str(), // elapsed time + ToString(std::chrono::duration_cast(std::chrono::steady_clock::now() - gameStartTime_)) + .c_str(), // wall clock + HumanReadableNumber(em_.GetCurrentGF() - lastReportGf_).c_str()); // GF per second + printConsole("└───────────────┴───────────────────────┴───────────────────────┴────────────────┘\n"); + printConsole("\n"); + printConsole("┌────────────────────────┬─────────────────┬─────────────┬───────────┬───────────┐\n"); + printConsole("│ Player │ Country │ Buildings │ Military │ Gold │\n"); + printConsole("├────────────────────────┼─────────────────┼─────────────┼───────────┼───────────┤\n"); + for(unsigned playerId = 0; playerId < world_.GetNumPlayers(); ++playerId) + { + const GamePlayer& player = world_.GetPlayer(playerId); + printConsole("│ %s%-22s%s │ %15s │ %11s │ %9s │ %9s │\n", player.IsDefeated() ? "\x1b[9m" : "", + player.name.c_str(), player.IsDefeated() ? "\x1b[29m" : "", + HumanReadableNumber(player.GetStatisticCurrentValue(StatisticType::Country)).c_str(), + HumanReadableNumber(player.GetStatisticCurrentValue(StatisticType::Buildings)).c_str(), + HumanReadableNumber(player.GetStatisticCurrentValue(StatisticType::Military)).c_str(), + HumanReadableNumber(player.GetStatisticCurrentValue(StatisticType::Gold)).c_str()); + } + printConsole("└────────────────────────┴─────────────────┴─────────────┴───────────┴───────────┘\n"); + + lastReportGf_ = em_.GetCurrentGF(); +} + +std::vector GeneratePlayerInfo(const std::vector& ais) +{ + std::vector ret; + for(const AI::Info& ai : ais) + { + PlayerInfo pi; + pi.ps = PlayerState::Occupied; + pi.aiInfo = ai; + switch(ai.type) + { + case AI::Type::Default: pi.name = "AIJH " + std::to_string(ret.size()); break; + case AI::Type::Dummy: + default: pi.name = "Dummy " + std::to_string(ret.size()); break; + } + pi.nation = Nation::Romans; + pi.team = Team::None; + ret.push_back(pi); + } + return ret; +} + +#ifdef WIN32 +HANDLE setupStdOut() +{ + HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE); + SetConsoleMode(h, ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING); + SetConsoleOutputCP(65001); + return h; +} +#endif + +void printConsole(const char* fmt, ...) +{ + char buffer[512]; + va_list args; + va_start(args, fmt); + const int len = vsnprintf(buffer, sizeof(buffer), fmt, args); + va_end(args); + if(len > 0 && (size_t)len < sizeof(buffer)) + { +#ifdef WIN32 + static auto h = setupStdOut(); + WriteConsoleA(h, buffer, len, 0, 0); +#else + bnw::cout << buffer; +#endif + } +} diff --git a/extras/ai-battle/HeadlessGame.h b/extras/ai-battle/HeadlessGame.h new file mode 100644 index 000000000..c52c90625 --- /dev/null +++ b/extras/ai-battle/HeadlessGame.h @@ -0,0 +1,47 @@ +// Copyright (C) 2005 - 2024 Settlers Freaks (sf-team at siedler25.org) +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "Game.h" +#include "Replay.h" +#include "ai/AIPlayer.h" +#include "gameTypes/AIInfo.h" +#include +#include +#include +#include + +class GameWorld; +class GlobalGameSettings; +class EventManager; + +/// Run an ai-only game without user-interface. +class HeadlessGame +{ +public: + HeadlessGame(const GlobalGameSettings& ggs, const boost::filesystem::path& map, const std::vector& ais); + ~HeadlessGame(); + + void Run(unsigned maxGF = std::numeric_limits::max()); + void Close(); + + void RecordReplay(const boost::filesystem::path& path, unsigned random_init); + void SaveGame(const boost::filesystem::path& path) const; + +private: + void PrintState(); + + boost::filesystem::path map_; + Game game_; + GameWorld& world_; + EventManager& em_; + std::vector> players_; + + Replay replay_; + boost::filesystem::path replayPath_; + + unsigned lastReportGf_ = 0; + std::chrono::steady_clock::time_point gameStartTime_; +}; diff --git a/extras/ai-battle/main.cpp b/extras/ai-battle/main.cpp new file mode 100644 index 000000000..72d8e4363 --- /dev/null +++ b/extras/ai-battle/main.cpp @@ -0,0 +1,124 @@ +// Copyright (C) 2005 - 2024 Settlers Freaks (sf-team at siedler25.org) +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "GlobalGameSettings.h" +#include "HeadlessGame.h" +#include "QuickStartGame.h" +#include "RTTR_Version.h" +#include "RttrConfig.h" +#include "files.h" +#include "random/Random.h" +#include "s25util/System.h" + +#include +#include +#include +#include +#include +#include + +namespace bnw = boost::nowide; +namespace bfs = boost::filesystem; +namespace po = boost::program_options; + +int main(int argc, char** argv) +{ + bnw::nowide_filesystem(); + bnw::args _(argc, argv); + + boost::optional replay_path; + boost::optional savegame_path; + unsigned random_init = static_cast(std::chrono::high_resolution_clock::now().time_since_epoch().count()); + + po::options_description desc("Allowed options"); + // clang-format off + desc.add_options() + ("help,h", "Show help") + ("map,m", po::value()->required(),"Map to load") + ("ai", po::value>()->required(),"AI player(s) to add") + ("objective", po::value()->default_value("domination"),"domination(default)|conquer") + ("replay", po::value(&replay_path),"Filename to write replay to (optional)") + ("save", po::value(&savegame_path),"Filename to write savegame to (optional)") + ("random_init", po::value(&random_init),"Seed value for the random number generator (optional)") + ("maxGF", po::value()->default_value(std::numeric_limits::max()),"Maximum number of game frames to run (optional)") + ("version", "Show version information and exit") + ; + // clang-format on + + if(argc == 1) + { + bnw::cerr << desc << std::endl; + return 1; + } + + po::variables_map options; + try + { + po::store(po::command_line_parser(argc, argv).options(desc).run(), options); + + if(options.count("help")) + { + bnw::cout << desc << std::endl; + return 0; + } + if(options.count("version")) + { + bnw::cout << rttr::version::GetTitle() << " v" << rttr::version::GetVersion() << "-" + << rttr::version::GetRevision() << std::endl + << "Compiled with " << System::getCompilerName() << " for " << System::getOSName() << std::endl; + return 0; + } + + po::notify(options); + } catch(const std::exception& e) + { + bnw::cerr << "Error: " << e.what() << std::endl; + bnw::cerr << desc << std::endl; + return 1; + } + + try + { + // We print arguments and seed in order to be able to reproduce crashes. + for(int i = 0; i < argc; ++i) + bnw::cout << argv[i] << " "; + bnw::cout << std::endl; + bnw::cout << "random_init: " << random_init << std::endl; + bnw::cout << std::endl; + + RTTRCONFIG.Init(); + RANDOM.Init(random_init); + + const bfs::path mapPath = RTTRCONFIG.ExpandPath(options["map"].as()); + const std::vector ais = ParseAIOptions(options["ai"].as>()); + + GlobalGameSettings ggs; + const auto objective = options["objective"].as(); + if(objective == "domination") + ggs.objective = GameObjective::TotalDomination; + else if(objective == "conquer") + ggs.objective = GameObjective::Conquer3_4; + else + { + bnw::cerr << "unknown objective: " << objective << std::endl; + return 1; + } + + ggs.objective = GameObjective::TotalDomination; + HeadlessGame game(ggs, mapPath, ais); + if(replay_path) + game.RecordReplay(*replay_path, random_init); + + game.Run(options["maxGF"].as()); + game.Close(); + if(savegame_path) + game.SaveGame(*savegame_path); + } catch(const std::exception& e) + { + bnw::cerr << e.what() << std::endl; + return 1; + } + + return 0; +} diff --git a/extras/audioDrivers/SDL/CMakeLists.txt b/extras/audioDrivers/SDL/CMakeLists.txt index 2ab238f92..d0db8ee59 100644 --- a/extras/audioDrivers/SDL/CMakeLists.txt +++ b/extras/audioDrivers/SDL/CMakeLists.txt @@ -6,23 +6,23 @@ set(SDL_BUILDING_LIBRARY ON) find_package(SDL_mixer 2.0.1) if(NOT SDL_MIXER_FOUND) - message(WARNING "SDL_mixer library not found: Not building SDL audiodriver") + message(WARNING "SDL_mixer library not found: Not building SDL audiodriver") else() - if(WIN32) - include(GatherDll) - gather_dll(SDL_MIXER) - gather_dll_by_name(VORBIS libvorbis-0.dll) - gather_dll_by_name(VORBISFILE libvorbisfile-3.dll) - gather_dll_by_name(OGG libogg-0.dll) - endif() + if(WIN32) + include(GatherDll) + gather_dll(SDL_MIXER) + gather_dll_by_name(VORBIS libvorbis-0.dll) + gather_dll_by_name(VORBISFILE libvorbisfile-3.dll) + gather_dll_by_name(OGG libogg-0.dll) + endif() - add_library(audioSDL SHARED ${RTTR_DRIVER_INTERFACE} AudioSDL.cpp AudioSDL.h) - target_link_libraries(audioSDL PRIVATE audiodrv SDL_mixer::SDL_mixer) - enable_warnings(audioSDL) + add_library(audioSDL SHARED ${RTTR_DRIVER_INTERFACE} AudioSDL.cpp AudioSDL.h) + target_link_libraries(audioSDL PRIVATE audiodrv SDL_mixer::SDL_mixer) + enable_warnings(audioSDL) - install(TARGETS audioSDL - RUNTIME DESTINATION ${RTTR_DRIVERDIR}/audio - LIBRARY DESTINATION ${RTTR_DRIVERDIR}/audio - ) - add_dependencies(drivers audioSDL) + install(TARGETS audioSDL + RUNTIME DESTINATION ${RTTR_DRIVERDIR}/audio + LIBRARY DESTINATION ${RTTR_DRIVERDIR}/audio + ) + add_dependencies(drivers audioSDL) endif() diff --git a/extras/videoDrivers/SDL2/CMakeLists.txt b/extras/videoDrivers/SDL2/CMakeLists.txt index 3a41bdbb5..b7453661c 100644 --- a/extras/videoDrivers/SDL2/CMakeLists.txt +++ b/extras/videoDrivers/SDL2/CMakeLists.txt @@ -6,18 +6,18 @@ set(SDL2_BUILDING_LIBRARY ON) find_package(SDL2 2.0.2) if(SDL2_FOUND) - add_library(videoSDL2 SHARED ${RTTR_DRIVER_INTERFACE} VideoSDL2.cpp VideoSDL2.h icon.h icon.cpp) - target_link_libraries(videoSDL2 PRIVATE videodrv s25util::common glad Boost::nowide SDL2::SDL2) - enable_warnings(videoSDL2) + add_library(videoSDL2 SHARED ${RTTR_DRIVER_INTERFACE} VideoSDL2.cpp VideoSDL2.h icon.h icon.cpp) + target_link_libraries(videoSDL2 PRIVATE videodrv s25util::common glad Boost::nowide SDL2::SDL2) + enable_warnings(videoSDL2) - if(WIN32) - include(GatherDll) - gather_dll_by_name(SDL2 SDL2.dll) - endif() + if(WIN32) + include(GatherDll) + gather_dll_by_name(SDL2 SDL2.dll) + endif() - install(TARGETS videoSDL2 - RUNTIME DESTINATION ${RTTR_DRIVERDIR}/video - LIBRARY DESTINATION ${RTTR_DRIVERDIR}/video - ) - add_dependencies(drivers videoSDL2) + install(TARGETS videoSDL2 + RUNTIME DESTINATION ${RTTR_DRIVERDIR}/video + LIBRARY DESTINATION ${RTTR_DRIVERDIR}/video + ) + add_dependencies(drivers videoSDL2) endif() diff --git a/extras/videoDrivers/WinAPI/CMakeLists.txt b/extras/videoDrivers/WinAPI/CMakeLists.txt index 4ad30f120..fea01f0a9 100644 --- a/extras/videoDrivers/WinAPI/CMakeLists.txt +++ b/extras/videoDrivers/WinAPI/CMakeLists.txt @@ -3,13 +3,13 @@ # SPDX-License-Identifier: GPL-2.0-or-later if(WIN32) - find_package(OpenGL REQUIRED) + find_package(OpenGL REQUIRED) - add_library(videoWinAPI SHARED ${RTTR_DRIVER_INTERFACE} WinAPI.cpp WinAPI.h) - target_link_libraries(videoWinAPI PRIVATE videodrv s25util::common gdi32 user32 ${OPENGL_gl_LIBRARY} Boost::boost) - target_include_directories(videoWinAPI SYSTEM PRIVATE ${OPENGL_INCLUDE_DIR} ${PROJECT_SOURCE_DIR}/data/win32) - enable_warnings(videoWinAPI) + add_library(videoWinAPI SHARED ${RTTR_DRIVER_INTERFACE} WinAPI.cpp WinAPI.h) + target_link_libraries(videoWinAPI PRIVATE videodrv s25util::common gdi32 user32 ${OPENGL_gl_LIBRARY} Boost::boost) + target_include_directories(videoWinAPI SYSTEM PRIVATE ${OPENGL_INCLUDE_DIR} ${PROJECT_SOURCE_DIR}/data/win32) + enable_warnings(videoWinAPI) - install(TARGETS videoWinAPI RUNTIME DESTINATION ${RTTR_DRIVERDIR}/video) - add_dependencies(drivers videoWinAPI) + install(TARGETS videoWinAPI RUNTIME DESTINATION ${RTTR_DRIVERDIR}/video) + add_dependencies(drivers videoWinAPI) endif() diff --git a/libs/common/include/helpers/EnumRange.h b/libs/common/include/helpers/EnumRange.h index 2b66d3f68..b41c5cd49 100644 --- a/libs/common/include/helpers/EnumRange.h +++ b/libs/common/include/helpers/EnumRange.h @@ -6,12 +6,23 @@ #include "MaxEnumValue.h" #include +#include #include // Using BOOST_FORCEINLINE mostly for increased debug performance (doesn't work for MSVC though) #define RTTR_CONSTEXPR_INLINE constexpr BOOST_FORCEINLINE namespace helpers { +template +struct EnumIterTraits +{ + using iterator_category = std::forward_iterator_tag; + using value_type = T; + using difference_type = std::make_signed_t, int>>; + using pointer = T*; + using reference = T&; +}; + /// Can be used in range-based for loops to iterate over each enum value /// Requires: T must be an enum with MaxEnumValue trait specialized /// T must be simple: Start at zero, no gaps @@ -19,7 +30,7 @@ template struct EnumRange { static_assert(std::is_enum::value, "Must be an enum!"); - class iterator + class iterator : public EnumIterTraits { unsigned value; @@ -41,7 +52,7 @@ template struct EnumRangeWithOffset { static_assert(std::is_enum::value, "Must be an enum!"); - class iterator + class iterator : public EnumIterTraits { unsigned value; diff --git a/libs/common/include/helpers/GetInsertIterator.hpp b/libs/common/include/helpers/GetInsertIterator.hpp deleted file mode 100644 index 869d7fb22..000000000 --- a/libs/common/include/helpers/GetInsertIterator.hpp +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (C) 2005 - 2021 Settlers Freaks (sf-team at siedler25.org) -// -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include -#include - -namespace helpers { - -/// Returns the most efficient insert operator -template -struct GetInsertIterator -{ - static auto get(T& collection) { return std::inserter(collection, collection.end()); } -}; - -template -struct GetInsertIterator().push_back(std::declval()))>> -{ - static auto get(T& collection) { return std::back_inserter(collection); } -}; - -} // namespace helpers diff --git a/libs/common/include/helpers/ReserveElements.hpp b/libs/common/include/helpers/ReserveElements.hpp deleted file mode 100644 index f2c98451f..000000000 --- a/libs/common/include/helpers/ReserveElements.hpp +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (C) 2005 - 2021 Settlers Freaks (sf-team at siedler25.org) -// -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include -#include - -namespace helpers { - -/// Reserves space in a collection if possible -template -struct ReserveElements -{ - static void reserve(T& /*collection*/, unsigned /*size*/) {} -}; - -template -struct ReserveElements().reserve(0u))>> -{ - static void reserve(T& collection, unsigned size) { collection.reserve(size); } -}; - -} // namespace helpers diff --git a/libs/common/include/helpers/containerUtils.h b/libs/common/include/helpers/containerUtils.h index bb2660638..3ae0e5ade 100644 --- a/libs/common/include/helpers/containerUtils.h +++ b/libs/common/include/helpers/containerUtils.h @@ -1,4 +1,4 @@ -// Copyright (C) 2005 - 2021 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 @@ -6,41 +6,29 @@ #pragma once -#include +#include #include #include +#include namespace helpers { namespace detail { template - struct PopFrontImpl - { - static void pop(T& container) { container.erase(container.begin()); } - }; + struct has_pop_front : std::false_type + {}; template - struct PopFrontImpl> - { - static void pop(T& container) { container.pop_front(); } - }; + struct has_pop_front> : std::true_type + {}; template - struct FindImpl - { - static auto find(T& container, const U& value) - { - using std::begin; - using std::end; - return std::find(begin(container), end(container), value); - } - }; + struct has_find : std::false_type + {}; template - struct FindImpl().find(std::declval()))>> - { - static auto find(T& container, const U& value) { return container.find(value); } - }; + struct has_find().find(std::declval()))>> : std::true_type + {}; } // namespace detail /// Removes an element from a container by its reverse iterator and returns an iterator to the next element @@ -74,14 +62,24 @@ template void pop_front(T& container) { RTTR_Assert(!container.empty()); - detail::PopFrontImpl::pop(container); + if constexpr(detail::has_pop_front::value) + container.pop_front(); + else + container.erase(container.begin()); } /// Effective implementation of find. Uses the containers find function if available template auto find(T& container, const U& value) { - return detail::FindImpl::find(container, value); + if constexpr(detail::has_find::value) + return container.find(value); + else + { + using std::begin; + using std::end; + return std::find(begin(container), end(container), value); + } } template @@ -109,7 +107,7 @@ bool contains_if(const T& container, T_Predicate&& predicate) return find_if(container, std::forward(predicate)) != end(container); } -/// Count the number of occurences of the given value. Returns an unsigned value +/// Count the number of occurrences of the given value. Returns an unsigned value template size_t count(const T& container, const U& value) { @@ -167,11 +165,10 @@ void makeUniqueStable(T& container) } /// Returns the index of the given element in the container or -1 when not found -/// Note: Only works for containers with less than 2^31 - 1 elements. template -int indexOf(const T_Container& container, const T_Element& element) +constexpr std::ptrdiff_t indexOf(const T_Container& container, const T_Element& element) { - int index = 0; + std::ptrdiff_t index = 0; for(const auto& curEl : container) { if(curEl == element) @@ -181,4 +178,18 @@ int indexOf(const T_Container& container, const T_Element& element) return -1; } +/// Returns the index of the first matching element in the container or -1 when not found +template +constexpr std::ptrdiff_t indexOf_if(const T_Container& container, T_Predicate predicate) +{ + std::ptrdiff_t index = 0; + for(const auto& curEl : container) + { + if(predicate(curEl)) + return index; + ++index; + } + return -1; +} + } // namespace helpers diff --git a/libs/driver/src/AudioDriver.cpp b/libs/driver/src/AudioDriver.cpp index 5741addb7..e04940630 100644 --- a/libs/driver/src/AudioDriver.cpp +++ b/libs/driver/src/AudioDriver.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2005 - 2021 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 @@ -62,7 +62,7 @@ void AudioDriver::StopEffect(EffectPlayId play_id) void AudioDriver::unloadSound(RawSoundHandle handle) { RTTR_Assert(handle.driverData); // Otherwise handle is invalid - const auto it = std::find(loadedSounds_.begin(), loadedSounds_.end(), handle); + const auto it = helpers::find(loadedSounds_, handle); if(it == loadedSounds_.end()) throw std::invalid_argument("Sound is not currently loaded"); doUnloadSound(handle); diff --git a/libs/libGamedata/gameData/CampaignDescription.cpp b/libs/libGamedata/gameData/CampaignDescription.cpp index d50a64454..2c653db78 100644 --- a/libs/libGamedata/gameData/CampaignDescription.cpp +++ b/libs/libGamedata/gameData/CampaignDescription.cpp @@ -33,10 +33,15 @@ CampaignDescription::CampaignDescription(const kaguya::LuaRef& table) lua::validatePath(mapFolder); lua::validatePath(luaFolder); mapNames = luaData.getOrDefault("maps", std::vector()); - + selectionMapData = luaData.getOptional("selectionMap"); luaData.checkUnused(); } +const std::optional& CampaignDescription::getSelectionMapData() const +{ + return selectionMapData; +} + boost::filesystem::path CampaignDescription::getLuaFilePath(const size_t idx) const { return (RTTRCONFIG.ExpandPath(luaFolder) / getMapName(idx)).replace_extension("lua"); diff --git a/libs/libGamedata/gameData/CampaignDescription.h b/libs/libGamedata/gameData/CampaignDescription.h index 7bc602581..327db6552 100644 --- a/libs/libGamedata/gameData/CampaignDescription.h +++ b/libs/libGamedata/gameData/CampaignDescription.h @@ -3,7 +3,9 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include "SelectionMapInputData.h" #include +#include #include #include @@ -21,6 +23,7 @@ struct CampaignDescription std::string image; unsigned maxHumanPlayers = 0; std::string difficulty; + std::optional selectionMapData; CampaignDescription() = default; explicit CampaignDescription(const kaguya::LuaRef& table); @@ -28,6 +31,7 @@ struct CampaignDescription 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& getSelectionMapData() const; private: std::string mapFolder; diff --git a/libs/libGamedata/gameData/SelectionMapInputData.cpp b/libs/libGamedata/gameData/SelectionMapInputData.cpp new file mode 100644 index 000000000..0369e6fab --- /dev/null +++ b/libs/libGamedata/gameData/SelectionMapInputData.cpp @@ -0,0 +1,247 @@ +// Copyright (C) 2024 Settlers Freaks (sf-team at siedler25.org) +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "SelectionMapInputData.h" +#include "lua/CheckedLuaTable.h" + +namespace kaguya { +template<> +struct lua_type_traits +{ + using get_type = MissionSelectionInfo; + using push_type = const MissionSelectionInfo&; + + struct checkTypeForEach + { + checkTypeForEach(bool& valid) : valid_(valid) {} + bool& valid_; + bool operator()(const LuaStackRef& k, const LuaStackRef& v) + { + if(k.isConvertible()) + { + size_t idx = k; + if(idx == 1) + valid_ = v.isConvertible(); + else if(idx == 2 || idx == 3) + valid_ = v.isConvertible(); + else + valid_ = false; + } else + valid_ = false; + return valid_; + } + }; + struct strictCheckTypeForEach + { + strictCheckTypeForEach(bool& valid) : valid_(valid) {} + bool& valid_; + bool operator()(const LuaStackRef& k, const LuaStackRef& v) + { + if(k.isType()) + { + size_t idx = k; + if(idx == 1) + valid_ = v.isType(); + else if(idx == 2 || idx == 3) + valid_ = v.isType(); + else + valid_ = false; + } else + valid_ = false; + return valid_; + } + }; + + static bool checkType(lua_State* l, int index) + { + const LuaStackRef table(l, index); + if(table.type() != LUA_TTABLE || table.size() != 3) + return false; + bool valid = true; + table.foreach_table_breakable(checkTypeForEach(valid)); + return valid; + } + static bool strictCheckType(lua_State* l, int index) + { + const LuaStackRef table(l, index); + if(table.type() != LUA_TTABLE || table.size() != 3) + return false; + bool valid = true; + table.foreach_table_breakable(strictCheckTypeForEach(valid)); + return valid; + } + static get_type get(lua_State* l, int index) + { + const LuaStackRef table(l, index); + if(table.type() != LUA_TTABLE || table.size() != 3) + throw LuaTypeMismatch(); + return get_type{table[1], Position(table[2], table[3])}; + } + static int push(lua_State* l, push_type v) + { + return util::push_args(l, v.maskAreaColor, v.ankerPos.x, v.ankerPos.y); + } +}; + +template<> +struct lua_type_traits +{ + using get_type = Position; + using push_type = const Position&; + + struct checkTypeForEach + { + checkTypeForEach(bool& valid) : valid_(valid) {} + bool& valid_; + bool operator()(const LuaStackRef& k, const LuaStackRef& v) + { + if(k.isConvertible()) + { + size_t idx = k; + if(idx == 1 || idx == 2) + valid_ = v.isConvertible(); + else + valid_ = false; + } else + valid_ = false; + return valid_; + } + }; + struct strictCheckTypeForEach + { + strictCheckTypeForEach(bool& valid) : valid_(valid) {} + bool& valid_; + bool operator()(const LuaStackRef& k, const LuaStackRef& v) + { + if(k.isType()) + { + size_t idx = k; + if(idx == 1 || idx == 2) + valid_ = v.isType(); + else + valid_ = false; + } else + valid_ = false; + return valid_; + } + }; + + static bool checkType(lua_State* l, int index) + { + const LuaStackRef table(l, index); + if(table.type() != LUA_TTABLE || table.size() != 2) + return false; + bool valid = true; + table.foreach_table_breakable(checkTypeForEach(valid)); + return valid; + } + static bool strictCheckType(lua_State* l, int index) + { + const LuaStackRef table(l, index); + if(table.type() != LUA_TTABLE || table.size() != 2) + return false; + bool valid = true; + table.foreach_table_breakable(strictCheckTypeForEach(valid)); + return valid; + } + static get_type get(lua_State* l, int index) + { + const LuaStackRef table(l, index); + if(table.type() != LUA_TTABLE || table.size() != 2) + throw LuaTypeMismatch(); + return get_type(table[1], table[2]); + } + static int push(lua_State* l, push_type v) { return util::push_args(l, v.x, v.y); } +}; + +template<> +struct lua_type_traits +{ + using get_type = ImageResource; + using push_type = const ImageResource&; + + struct checkTypeForEach + { + checkTypeForEach(bool& valid) : valid_(valid) {} + bool& valid_; + bool operator()(const LuaStackRef& k, const LuaStackRef& v) + { + if(k.isConvertible()) + { + size_t idx = k; + if(idx == 1) + valid_ = v.isConvertible(); + else if(idx == 2) + valid_ = v.isConvertible(); + else + valid_ = false; + } else + valid_ = false; + return valid_; + } + }; + struct strictCheckTypeForEach + { + strictCheckTypeForEach(bool& valid) : valid_(valid) {} + bool& valid_; + bool operator()(const LuaStackRef& k, const LuaStackRef& v) + { + if(k.isType()) + { + size_t idx = k; + if(idx == 1) + valid_ = v.isType(); + else if(idx == 2) + valid_ = v.isType(); + else + valid_ = false; + } else + valid_ = false; + return valid_; + } + }; + + static bool checkType(lua_State* l, int index) + { + const LuaStackRef table(l, index); + if(table.type() != LUA_TTABLE || table.size() != 2) + return false; + bool valid = true; + table.foreach_table_breakable(checkTypeForEach(valid)); + return valid; + } + static bool strictCheckType(lua_State* l, int index) + { + const LuaStackRef table(l, index); + if(table.type() != LUA_TTABLE || table.size() != 2) + return false; + bool valid = true; + table.foreach_table_breakable(strictCheckTypeForEach(valid)); + return valid; + } + static get_type get(lua_State* l, int index) + { + 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]); + } + static int push(lua_State* l, push_type v) { return util::push_args(l, v.filePath, v.index); } +}; +} // namespace kaguya + +SelectionMapInputData::SelectionMapInputData(const kaguya::LuaRef& table) +{ + CheckedLuaTable luaMapSelection(table); + luaMapSelection.getOrThrow(background, "background"); + luaMapSelection.getOrThrow(map, "map"); + luaMapSelection.getOrThrow(missionMapMask, "missionMapMask"); + luaMapSelection.getOrThrow(marker, "marker"); + luaMapSelection.getOrThrow(conquered, "conquered"); + luaMapSelection.getOrThrow(mapOffsetInBackground, "backgroundOffset"); + luaMapSelection.getOrThrow(disabledColor, "disabledColor"); + luaMapSelection.getOrThrow(missionSelectionInfos, "missionSelectionInfos"); + luaMapSelection.checkUnused(); +} diff --git a/libs/libGamedata/gameData/SelectionMapInputData.h b/libs/libGamedata/gameData/SelectionMapInputData.h new file mode 100644 index 000000000..155fc238e --- /dev/null +++ b/libs/libGamedata/gameData/SelectionMapInputData.h @@ -0,0 +1,70 @@ +// Copyright (C) 2024 Settlers Freaks (sf-team at siedler25.org) +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once +#include +#include +#include +#include +#include + +namespace kaguya { +class LuaRef; +} // namespace kaguya + +struct ImageResource +{ + boost::filesystem::path filePath; + unsigned index; + ImageResource(boost::filesystem::path path = boost::filesystem::path(), unsigned index = 0) + : filePath(std::move(path)), index(index){}; +}; + +struct MissionSelectionInfo +{ + /// The maskAreaColor is the color for the mission used in the missionMapMask + unsigned maskAreaColor = 0; + /// The position on which the conquered image and the marker image, if mission is selected, + /// are displayed. The offset is always counted from the origin of the map image. + Position ankerPos; +}; + +struct SelectionMapInputData +{ + SelectionMapInputData() = default; + explicit SelectionMapInputData(const kaguya::LuaRef& table); + + /// Background image for the selection map + ImageResource background; + /// The map image itself. + ImageResource map; + /// This image is a mask that describes the mission areas of the map (image). It must be the same size + /// as the map image where the color of each pixel determines the mission it belongs to. + /// Each mission must have a unique color (specified in the missionSelectionInfos). + /// Any other color is treated as neutral area and ignored. + ImageResource missionMapMask; + /// The marker image shown when a mission is selected. + ImageResource marker; + /// The image shown when a mission is already finished. + ImageResource conquered; + /// Offset of the map image and missionMapMask image relative to the background image. + Position mapOffsetInBackground = {0, 0}; + /// Color for drawing missions not playable yet. Usually this should be a partly transparent color + unsigned disabledColor = 0x70000000; + /// Contains an entry for each mission + std::vector missionSelectionInfos; +}; + +namespace kaguya { +template<> +struct lua_type_traits : private lua_type_traits +{ + using Base = lua_type_traits; + using get_type = SelectionMapInputData; + + using Base::checkType; + using Base::strictCheckType; + static get_type get(lua_State* l, int index) { return SelectionMapInputData(Base::get(l, index)); } +}; +} // namespace kaguya diff --git a/libs/libGamedata/lua/CampaignDataLoader.cpp b/libs/libGamedata/lua/CampaignDataLoader.cpp index d2f9e7fbd..b9569bc47 100644 --- a/libs/libGamedata/lua/CampaignDataLoader.cpp +++ b/libs/libGamedata/lua/CampaignDataLoader.cpp @@ -17,7 +17,7 @@ unsigned CampaignDataLoader::GetVersion() { - return 1; + return 2; } CampaignDataLoader::CampaignDataLoader(CampaignDescription& campaignDesc, const boost::filesystem::path& basePath) @@ -35,7 +35,7 @@ bool CampaignDataLoader::CheckScriptVersion() if(func.type() == LUA_TFUNCTION) { const auto scriptVersion = func.call(); - if(scriptVersion == GetVersion()) + if(scriptVersion <= GetVersion()) return true; else { diff --git a/libs/libGamedata/lua/CampaignDataLoader.h b/libs/libGamedata/lua/CampaignDataLoader.h index 38b317026..a55d58566 100644 --- a/libs/libGamedata/lua/CampaignDataLoader.h +++ b/libs/libGamedata/lua/CampaignDataLoader.h @@ -18,8 +18,6 @@ class CampaignDataLoader : public LuaInterfaceBase CampaignDataLoader(CampaignDescription& campaignDesc, const boost::filesystem::path& basePath); ~CampaignDataLoader() override; - bool CheckScriptVersion(); - /// Return version of the interface. Changes here reflect breaking changes static unsigned GetVersion(); @@ -30,4 +28,6 @@ class CampaignDataLoader : public LuaInterfaceBase private: CampaignDescription& campaignDesc_; boost::filesystem::path basePath_; + + bool CheckScriptVersion(); }; diff --git a/libs/libGamedata/lua/CheckedLuaTable.h b/libs/libGamedata/lua/CheckedLuaTable.h index 7f732ba74..7d75b25ff 100644 --- a/libs/libGamedata/lua/CheckedLuaTable.h +++ b/libs/libGamedata/lua/CheckedLuaTable.h @@ -7,6 +7,7 @@ #include "Rect.h" #include "gameData/WorldDescription.h" #include +#include #include #include #include @@ -34,6 +35,9 @@ class CheckedLuaTable /// Return the value or use the default if it doesn't exist template T getOrDefault(const std::string& fieldName, const T& defaultValue); + /// Return the value if it exists + template + std::optional getOptional(const std::string& fieldName); /// Get a Rect from lua or return the default. TODO: Add kaguya conversion function and remove this Rect getRectOrDefault(const std::string& fieldName, const Rect& defaultValue); }; @@ -75,18 +79,26 @@ inline void CheckedLuaTable::getOrThrow(T& outVal, const std::string& fieldName) } template -inline T CheckedLuaTable::getOrDefault(const std::string& fieldName, const T& defaultValue) +inline std::optional CheckedLuaTable::getOptional(const std::string& fieldName) { accessedKeys_.insert(fieldName); kaguya::LuaRef value = table[fieldName]; if(value.isNilref()) - return defaultValue; + return std::nullopt; if(value.isConvertible()) - return value; - else + { + T val = value; + return std::optional(val); + } else throw GameDataLoadError("Field '" + fieldName + "' has the wrong type"); } +template +inline T CheckedLuaTable::getOrDefault(const std::string& fieldName, const T& defaultValue) +{ + return getOptional(fieldName).value_or(defaultValue); +} + inline Rect CheckedLuaTable::getRectOrDefault(const std::string& fieldName, const Rect& defaultValue) { std::vector luaRect = getOrDefault(fieldName, std::vector()); diff --git a/libs/libsamplerate/CMakeLists.txt b/libs/libsamplerate/CMakeLists.txt index 6c34b8943..836dcffd3 100644 --- a/libs/libsamplerate/CMakeLists.txt +++ b/libs/libsamplerate/CMakeLists.txt @@ -1,9 +1,9 @@ -# Copyright (C) 2005 - 2021 Settlers Freaks +# Copyright (C) 2005 - 2024 Settlers Freaks # # SPDX-License-Identifier: GPL-2.0-or-later add_library(samplerate_cpp INTERFACE) target_include_directories(samplerate_cpp INTERFACE include) target_link_libraries(samplerate_cpp INTERFACE samplerate) -target_compile_features(samplerate_cpp INTERFACE cxx_std_11) +target_compile_features(samplerate_cpp INTERFACE cxx_std_17) enable_warnings(samplerate_cpp) \ No newline at end of file diff --git a/libs/libsamplerate/include/samplerate.hpp b/libs/libsamplerate/include/samplerate.hpp index e442f4ecd..d33b62879 100644 --- a/libs/libsamplerate/include/samplerate.hpp +++ b/libs/libsamplerate/include/samplerate.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include namespace samplerate { @@ -33,18 +34,15 @@ namespace detail { inline void throwOnError(int errorCode) { if(errorCode) - throw std::runtime_error(src_strerror(errorCode)); + throwError(errorCode); } - template - using void_t = void; - template struct has_src_clone : std::false_type {}; template - struct has_src_clone(), nullptr))>> : std::true_type + struct has_src_clone(), nullptr))>> : std::true_type {}; template::value> @@ -55,7 +53,7 @@ namespace detail { public: explicit State(SRC_STATE* state) : state_(state) {} ~State() { src_delete(state_); } - State(State&& other) noexcept : state_(other.state_) { other.state_ = nullptr; } + State(State&& other) noexcept : state_(std::exchange(other.state_, nullptr)) {} State(const State& other) : state_(nullptr) { if(other.state_) @@ -82,11 +80,10 @@ namespace detail { public: explicit State(SRC_STATE* state) : state_(state) {} ~State() { src_delete(state_); } - State(State&& other) noexcept : state_(other.state_) { other.state_ = nullptr; } + State(State&& other) noexcept : state_(std::exchange(other.state_, nullptr)) {} State& operator=(State&& other) noexcept { - state_ = other.state_; - other.state_ = nullptr; + state_ = std::exchange(other.state_, nullptr); return *this; } operator SRC_STATE*() { return state_; } diff --git a/libs/s25main/CMakeLists.txt b/libs/s25main/CMakeLists.txt index 308a6efb0..6a136b4ec 100644 --- a/libs/s25main/CMakeLists.txt +++ b/libs/s25main/CMakeLists.txt @@ -76,7 +76,7 @@ endif() # For clock_gettime etc. this is required on some platforms/compilers find_library(LIBRT rt) if(LIBRT) - target_link_libraries(s25Main PUBLIC ${LIBRT}) + target_link_libraries(s25Main PUBLIC ${LIBRT}) endif() if(CMAKE_SYSTEM_NAME STREQUAL "Linux") diff --git a/libs/s25main/Game.cpp b/libs/s25main/Game.cpp index ac96cdfdb..20e37c6bd 100644 --- a/libs/s25main/Game.cpp +++ b/libs/s25main/Game.cpp @@ -170,7 +170,7 @@ void Game::CheckObjective() } // We have a winner! - if(finished_) + if(world_.GetGameInterface() && finished_) { // If there is a team that is best and it does not only consist of the best player // then it is a team victory, else a single players victory diff --git a/libs/s25main/GamePlayer.cpp b/libs/s25main/GamePlayer.cpp index edecc0b67..50b08acde 100644 --- a/libs/s25main/GamePlayer.cpp +++ b/libs/s25main/GamePlayer.cpp @@ -1545,7 +1545,6 @@ void GamePlayer::SuggestPact(const unsigned char targetPlayerId, const PactType if(!pacts[targetPlayerId][pt].accepted && duration > 0) { - pacts[targetPlayerId][pt].accepted = false; pacts[targetPlayerId][pt].duration = duration; pacts[targetPlayerId][pt].start = world.GetEvMgr().GetCurrentGF(); GamePlayer targetPlayer = world.GetPlayer(targetPlayerId); @@ -2063,9 +2062,12 @@ void GamePlayer::TestPacts() { // Pact was running but is expired -> Cancel for both players pacts[i][pact].duration = 0; + pacts[i][pact].accepted = false; GamePlayer& otherPlayer = world.GetPlayer(i); RTTR_Assert(otherPlayer.pacts[GetPlayerId()][pact].duration); + RTTR_Assert(otherPlayer.pacts[GetPlayerId()][pact].accepted); otherPlayer.pacts[GetPlayerId()][pact].duration = 0; + otherPlayer.pacts[GetPlayerId()][pact].accepted = false; // And notify PactChanged(pact); otherPlayer.PactChanged(pact); diff --git a/libs/s25main/MusicPlayer.cpp b/libs/s25main/MusicPlayer.cpp index ab0de5893..d0f0f3b0a 100644 --- a/libs/s25main/MusicPlayer.cpp +++ b/libs/s25main/MusicPlayer.cpp @@ -74,5 +74,5 @@ void MusicPlayer::PlayNext() // Und abspielen auto* curSong = dynamic_cast(sng[0]); if(curSong) - curSong->Play(1); + curSong->Play(); } diff --git a/libs/s25main/NWFInfo.cpp b/libs/s25main/NWFInfo.cpp index 36cd24ea7..3047249ea 100644 --- a/libs/s25main/NWFInfo.cpp +++ b/libs/s25main/NWFInfo.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2005 - 2021 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 @@ -32,16 +32,14 @@ void NWFInfo::addPlayer(unsigned playerId) void NWFInfo::removePlayer(unsigned playerId) { - auto it = std::find_if(playerInfos_.begin(), playerInfos_.end(), - [playerId](const auto& info) { return info.id == playerId; }); + auto it = helpers::find_if(playerInfos_, [playerId](const auto& info) { return info.id == playerId; }); if(it != playerInfos_.end()) playerInfos_.erase(it); } bool NWFInfo::addPlayerCmds(unsigned playerId, const PlayerGameCommands& cmds) { - auto it = std::find_if(playerInfos_.begin(), playerInfos_.end(), - [playerId](const auto& info) { return info.id == playerId; }); + auto it = helpers::find_if(playerInfos_, [playerId](const auto& info) { return info.id == playerId; }); if(it == playerInfos_.end()) throw std::runtime_error("Player with given player id does not exist"); // Commands in NWF n are sent for NWF n + cmdDelay. Clients can only execute an NWF (and send their cmds) when all @@ -83,8 +81,7 @@ bool NWFInfo::isReady() const NWFPlayerInfo& NWFInfo::getPlayerInfo(unsigned playerId) const { - auto it = std::find_if(playerInfos_.begin(), playerInfos_.end(), - [playerId](const auto& info) { return info.id == playerId; }); + auto it = helpers::find_if(playerInfos_, [playerId](const auto& info) { return info.id == playerId; }); if(it == playerInfos_.end()) throw std::runtime_error("Player with given player id does not exist"); else diff --git a/libs/s25main/Replay.cpp b/libs/s25main/Replay.cpp index bf505b908..2442bcc7e 100644 --- a/libs/s25main/Replay.cpp +++ b/libs/s25main/Replay.cpp @@ -42,7 +42,7 @@ bool Replay::StopRecording() { if(!isRecording_) return true; - const unsigned replayDataSize = file_.Tell(); + const auto replayDataSize = file_.Tell(); isRecording_ = false; file_.Close(); @@ -64,11 +64,10 @@ bool Replay::StopRecording() const auto lastGF = file.ReadUnsignedInt(); RTTR_Assert(lastGF == lastGF_); compressedReplay.WriteUnsignedInt(lastGF); - file.ReadUnsignedChar(); // Ignore compressed flag - compressedReplay.WriteUnsignedChar(1); + file.ReadUnsignedChar(); // Ignore compressed flag (always zero for the temporary file) // Read and compress remaining data - const auto uncompressedSize = replayDataSize - file.Tell(); + const auto uncompressedSize = replayDataSize - file.Tell(); // Always positive as there is always some game data data.resize(uncompressedSize); file.ReadRawData(data.data(), data.size()); data = CompressedData::compress(data); @@ -76,6 +75,7 @@ bool Replay::StopRecording() // This can happen for very short replays if(data.size() >= uncompressedSize) return true; + compressedReplay.WriteUnsignedChar(1); // Compressed flag compressedReplay.WriteUnsignedInt(uncompressedSize); compressedReplay.WriteUnsignedInt(data.size()); compressedReplay.WriteRawData(data.data(), data.size()); diff --git a/libs/s25main/SerializedGameData.h b/libs/s25main/SerializedGameData.h index 0c65ac062..10f6729fd 100644 --- a/libs/s25main/SerializedGameData.h +++ b/libs/s25main/SerializedGameData.h @@ -1,4 +1,4 @@ -// Copyright (C) 2005 - 2021 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 @@ -6,10 +6,8 @@ #include "FOWObjects.h" #include "RTTR_Assert.h" -#include "helpers/GetInsertIterator.hpp" #include "helpers/MaxEnumValue.h" #include "helpers/OptionalEnum.h" -#include "helpers/ReserveElements.hpp" #include "helpers/serializeContainers.h" #include "helpers/serializeEnums.h" #include "helpers/serializePoint.h" @@ -17,6 +15,7 @@ #include "gameTypes/MapCoordinates.h" #include "s25util/Serializer.h" #include "s25util/warningSuppression.h" +#include #include #include #include @@ -30,6 +29,38 @@ class GameEvent; class Game; class ILocalGameState; +namespace detail { +template +struct has_reserve : std::false_type +{}; + +template +struct has_reserve().reserve(0u))>> : std::true_type +{}; + +template +struct has_push_back : std::false_type +{ + static auto get(T& collection) { return std::inserter(collection, collection.end()); } +}; + +template +struct has_push_back().push_back(std::declval()))>> : + std::true_type +{ + static auto get(T& collection) { return std::back_inserter(collection); } +}; + +template +auto get_back_inserter(T& collection) +{ + if constexpr(detail::has_push_back::value) + return std::back_inserter(collection); + else + return std::inserter(collection, collection.end()); +} +} // namespace detail + /// Kümmert sich um das Serialisieren der GameDaten fürs Speichern und Resynchronisieren class SerializedGameData : public Serializer { @@ -211,8 +242,10 @@ void SerializedGameData::PopObjectContainer(T& gos, helpers::OptionalEnum= 2) ? PopVarSize() : PopUnsignedInt(); gos.clear(); - helpers::ReserveElements::reserve(gos, size); - auto it = helpers::GetInsertIterator::get(gos); + if constexpr(detail::has_reserve::value) + gos.reserve(size); + + auto it = detail::get_back_inserter(gos); for(unsigned i = 0; i < size; ++i) *it = Elements(PopObject(got)); // Conversion required to support unique_ptr } diff --git a/libs/s25main/Window.cpp b/libs/s25main/Window.cpp index 1bb2b63c1..631dee92f 100644 --- a/libs/s25main/Window.cpp +++ b/libs/s25main/Window.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2005 - 2021 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 @@ -11,6 +11,7 @@ #include "driver/MouseCoords.h" #include "drivers/ScreenResizeEvent.h" #include "drivers/VideoDriverWrapper.h" +#include "helpers/containerUtils.h" #include "ogl/IRenderer.h" #include #include @@ -160,7 +161,7 @@ void Window::ActivateControls(bool activate) void Window::LockRegion(Window* window, const Rect& rect) { lockedAreas_[window] = rect; - auto it = std::find(tofreeAreas_.begin(), tofreeAreas_.end(), window); + auto it = helpers::find(tofreeAreas_, window); if(it != tofreeAreas_.end()) tofreeAreas_.erase(it); @@ -394,6 +395,12 @@ ctrlText* Window::AddText(unsigned id, const DrawPoint& pos, const std::string& return AddCtrl(new ctrlText(this, id, ScaleIf(pos), text, color, format, font)); } +ctrlMapSelection* Window::AddMapSelection(unsigned id, const DrawPoint& pos, const Extent& size, + const SelectionMapInputData& inputData) +{ + return AddCtrl(new ctrlMapSelection(this, id, ScaleIf(pos), ScaleIf(size), inputData)); +} + TextFormatSetter Window::AddFormattedText(unsigned id, const DrawPoint& pos, const std::string& text, unsigned color, FontStyle format, const glFont* font) { diff --git a/libs/s25main/Window.h b/libs/s25main/Window.h index 090c5647a..ad466a729 100644 --- a/libs/s25main/Window.h +++ b/libs/s25main/Window.h @@ -30,6 +30,7 @@ class ctrlEdit; class ctrlGroup; class ctrlImage; class ctrlList; +class ctrlMapSelection; class ctrlMultiline; class ctrlMultiSelectGroup; class ctrlOptionGroup; @@ -51,6 +52,7 @@ enum class GroupSelectType : unsigned; struct KeyEvent; struct ScreenResizeEvent; struct TableColumn; +struct SelectionMapInputData; namespace libsiedler2 { class ArchivItem_Map; @@ -176,6 +178,8 @@ class Window std::vector columns); ctrlText* AddText(unsigned id, const DrawPoint& pos, const std::string& text, unsigned color, FontStyle format, const glFont* font); + ctrlMapSelection* AddMapSelection(unsigned id, const DrawPoint& pos, const Extent& size, + const SelectionMapInputData& inputData); TextFormatSetter AddFormattedText(unsigned id, const DrawPoint& pos, const std::string& text, unsigned color, FontStyle format, const glFont* font); ctrlTimer* AddTimer(unsigned id, std::chrono::milliseconds timeout); diff --git a/libs/s25main/WindowManager.cpp b/libs/s25main/WindowManager.cpp index 87d86ffc5..d6db70482 100644 --- a/libs/s25main/WindowManager.cpp +++ b/libs/s25main/WindowManager.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2005 - 2021 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 @@ -13,7 +13,7 @@ #include "drivers/ScreenResizeEvent.h" #include "drivers/VideoDriverWrapper.h" #include "files.h" -#include "helpers/containerUtils.h" +#include "helpers/pointerContainerUtils.h" #include "helpers/reverse.h" #include "ingameWindows/IngameWindow.h" #include "ogl/FontStyle.h" @@ -665,8 +665,7 @@ IngameWindow* WindowManager::GetTopMostWindow() const void WindowManager::DoClose(IngameWindow* window) { - const auto it = - std::find_if(windows.begin(), windows.end(), [window](const auto& it) { return it.get() == window; }); + const auto it = helpers::findPtr(windows, window); RTTR_Assert(it != windows.end()); @@ -741,11 +740,11 @@ void WindowManager::DoDesktopSwitch() void WindowManager::CloseMarkedIngameWnds() { auto isWndMarkedForClose = [](const auto& wnd) { return wnd->ShouldBeClosed(); }; - auto it = std::find_if(windows.begin(), windows.end(), isWndMarkedForClose); + auto it = helpers::find_if(windows, isWndMarkedForClose); while(it != windows.end()) { DoClose(it->get()); - it = std::find_if(windows.begin(), windows.end(), isWndMarkedForClose); + it = helpers::find_if(windows, isWndMarkedForClose); } } diff --git a/libs/s25main/addons/AddonEconomyModeGameLength.h b/libs/s25main/addons/AddonEconomyModeGameLength.h index 3717295e7..47a1f366e 100644 --- a/libs/s25main/addons/AddonEconomyModeGameLength.h +++ b/libs/s25main/addons/AddonEconomyModeGameLength.h @@ -1,4 +1,4 @@ -// Copyright (C) 2005 - 2021 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 @@ -35,14 +35,15 @@ class AddonEconomyModeGameLength : public AddonList return result; } + static constexpr auto defaultIdx = helpers::indexOf(AddonEconomyModeGameLengthList, std::chrono::minutes(120)); + static_assert(defaultIdx >= 0); + public: AddonEconomyModeGameLength() : AddonList(AddonId::ECONOMY_MODE_GAME_LENGTH, AddonGroup::Economy | AddonGroup::GamePlay, _("Economy Mode: Game Length"), _("Adjust the time after which the economy mode victory condition is checked."), makeOptions(), - helpers::indexOf(AddonEconomyModeGameLengthList, std::chrono::minutes(120))) + static_cast(defaultIdx)) - { - RTTR_Assert(getDefaultStatus() != unsigned(-1)); - } + {} }; diff --git a/libs/s25main/ai/aijh/AIConstruction.cpp b/libs/s25main/ai/aijh/AIConstruction.cpp index 358f286bb..dfa39ec18 100644 --- a/libs/s25main/ai/aijh/AIConstruction.cpp +++ b/libs/s25main/ai/aijh/AIConstruction.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2005 - 2021 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 @@ -205,7 +205,7 @@ std::vector AIConstruction::FindFlags(const MapPoint pt, unsigned const auto* flag = aii.gwb.GetSpecObj(pt); if(flag) { - auto it = std::find(flags.begin(), flags.end(), flag); + auto it = helpers::find(flags, flag); if(it != flags.end()) flags.erase(it); } @@ -240,7 +240,7 @@ bool AIConstruction::MilitaryBuildingWantsRoad(const nobMilitary& milbld) == milbld.GetPos()) // upgrade bld should have road already but just in case it doesnt -> get a road asap return true; // TODO: This probably does not do what is wanted... - int bldIdx = helpers::indexOf(aii.GetMilitaryBuildings(), &milbld); + const auto bldIdx = helpers::indexOf(aii.GetMilitaryBuildings(), &milbld); return bldIdx > static_cast(aii.GetMilitaryBuildings().size() - aijh.GetNumPlannedConnectedInlandMilitaryBlds()); } diff --git a/libs/s25main/ai/aijh/AIResourceMap.cpp b/libs/s25main/ai/aijh/AIResourceMap.cpp index 191f68d25..7eed641d5 100644 --- a/libs/s25main/ai/aijh/AIResourceMap.cpp +++ b/libs/s25main/ai/aijh/AIResourceMap.cpp @@ -43,23 +43,24 @@ void AIResourceMap::init() map.Resize(mapSize); // Calculate value for each point. // This is quite expensive so do just for the diminishable resources to sort out ones where there will never be - // anything, which allows an optimization when calculating the value which must always be done on demand + // anything, which allows to skip calculating the value when updating the resource map if(isDiminishableResource) { + ETerrain requiredTerrain; + if(res == AIResource::Fish || res == AIResource::Stones) + requiredTerrain = ETerrain::Buildable; + else + { + RTTR_Assert(helpers::contains( + std::vector{AIResource::Gold, AIResource::Ironore, AIResource::Coal, AIResource::Granite}, + res)); + requiredTerrain = ETerrain::Mineable; + } RTTR_FOREACH_PT(MapPoint, mapSize) { - bool isValid = true; - if(res == AIResource::Fish) - { - isValid = - aii.gwb.IsOfTerrain(pt, [](const TerrainDesc& desc) { return desc.kind == TerrainKind::Water; }); - } else if(res == AIResource::Stones) - { - isValid = aii.gwb.IsOfTerrain(pt, [](const TerrainDesc& desc) { return desc.Is(ETerrain::Buildable); }); - } else //= granite,gold,iron,coal - { - isValid = aii.gwb.IsOfTerrain(pt, [](const TerrainDesc& desc) { return desc.Is(ETerrain::Mineable); }); - } + // Calculate only if we can build there + const bool isValid = + aii.gwb.IsOfTerrain(pt, [requiredTerrain](const TerrainDesc& desc) { return desc.Is(requiredTerrain); }); map[pt] = isValid ? aii.CalcResourceValue(pt, res) : 0; } } diff --git a/libs/s25main/buildings/noBuildingSite.cpp b/libs/s25main/buildings/noBuildingSite.cpp index 9f6413ab4..1f0251b9c 100644 --- a/libs/s25main/buildings/noBuildingSite.cpp +++ b/libs/s25main/buildings/noBuildingSite.cpp @@ -192,12 +192,10 @@ void noBuildingSite::Draw(DrawPoint drawPt) // Bretter DrawPoint doorPos = drawPt + DrawPoint(GetDoorPointX(), GetDoorPointY()); for(unsigned char i = 0; i < boards; ++i) - LOADER.GetMapTexture(WARE_STACK_TEX_MAP_OFFSET + rttr::enum_cast(GoodType::Boards)) - ->DrawFull(doorPos - DrawPoint(5, 10 + i * 4)); + LOADER.GetWareStackTex(GoodType::Boards)->DrawFull(doorPos - DrawPoint(5, 10 + i * 4)); // Steine for(unsigned char i = 0; i < stones; ++i) - LOADER.GetMapTexture(WARE_STACK_TEX_MAP_OFFSET + rttr::enum_cast(GoodType::Stones)) - ->DrawFull(doorPos + DrawPoint(8, -12 - i * 4)); + LOADER.GetWareStackTex(GoodType::Stones)->DrawFull(doorPos + DrawPoint(8, -12 - i * 4)); // bis dahin gebautes Haus zeichnen diff --git a/libs/s25main/buildings/nobHarborBuilding.cpp b/libs/s25main/buildings/nobHarborBuilding.cpp index fadd87050..773548d7f 100644 --- a/libs/s25main/buildings/nobHarborBuilding.cpp +++ b/libs/s25main/buildings/nobHarborBuilding.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2005 - 2021 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 @@ -244,13 +244,11 @@ void nobHarborBuilding::Draw(DrawPoint drawPt) // Bretter DrawPoint boardsPos = drawPt + BOARDS_POS[nation]; for(unsigned char i = 0; i < expedition.boards; ++i) - LOADER.GetMapTexture(WARE_STACK_TEX_MAP_OFFSET + rttr::enum_cast(GoodType::Boards)) - ->DrawFull(boardsPos - DrawPoint(0, i * 4)); + LOADER.GetWareStackTex(GoodType::Boards)->DrawFull(boardsPos - DrawPoint(0, i * 4)); DrawPoint stonesPos = drawPt + STONES_POS[nation]; // Steine for(unsigned char i = 0; i < expedition.stones; ++i) - LOADER.GetMapTexture(WARE_STACK_TEX_MAP_OFFSET + rttr::enum_cast(GoodType::Stones)) - ->DrawFull(stonesPos - DrawPoint(0, i * 4)); + LOADER.GetWareStackTex(GoodType::Stones)->DrawFull(stonesPos - DrawPoint(0, i * 4)); // Und den Bauarbeiter, falls er schon da ist if(expedition.builder) @@ -1109,8 +1107,8 @@ std::unique_ptr nobHarborBuilding::CancelWareForShip(Ware* ware) /// Bestellte Figur, die sich noch inder Warteschlange befindet, kommt nicht mehr und will rausgehauen werden void nobHarborBuilding::CancelFigure(noFigure* figure) { - const auto it = std::find_if(figures_for_ships.begin(), figures_for_ships.end(), - [figure](const FigureForShip& it) { return it.fig.get() == figure; }); + const auto it = + helpers::find_if(figures_for_ships, [figure](const FigureForShip& it) { return it.fig.get() == figure; }); // Figur ggf. aus der List entfernen if(it != figures_for_ships.end()) @@ -1183,7 +1181,7 @@ nobHarborBuilding::GetAttackerBuildingsForSeaAttack(const std::vector& } // Gebäude suchen, vielleicht schon vorhanden? - auto it2 = std::find(buildings.begin(), buildings.end(), static_cast(all_building)); + auto it2 = helpers::find(buildings, static_cast(all_building)); // Noch nicht vorhanden? if(it2 == buildings.end()) { diff --git a/libs/s25main/controls/controls.h b/libs/s25main/controls/controls.h index 2d43c7fbe..df567fdd8 100644 --- a/libs/s25main/controls/controls.h +++ b/libs/s25main/controls/controls.h @@ -16,6 +16,7 @@ #include "ctrlImage.h" #include "ctrlImageButton.h" #include "ctrlList.h" +#include "ctrlMapSelection.h" #include "ctrlMultiSelectGroup.h" #include "ctrlMultiline.h" #include "ctrlOptionGroup.h" diff --git a/libs/s25main/controls/ctrlMapSelection.cpp b/libs/s25main/controls/ctrlMapSelection.cpp new file mode 100644 index 000000000..3e00d62a9 --- /dev/null +++ b/libs/s25main/controls/ctrlMapSelection.cpp @@ -0,0 +1,183 @@ +// Copyright (C) 2024 Settlers Freaks (sf-team at siedler25.org) +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "ctrlMapSelection.h" +#include "CollisionDetection.h" +#include "Loader.h" +#include "RttrForeachPt.h" +#include "driver/MouseCoords.h" +#include "helpers/Range.h" +#include "helpers/containerUtils.h" +#include "helpers/format.hpp" +#include "mygettext/mygettext.h" +#include "ogl/glArchivItem_Bitmap.h" +#include +#include +#include +#include +#include + +ctrlMapSelection::MapImages::MapImages(const SelectionMapInputData& data) +{ + auto loadImage = [](const ImageResource& res) { + if(LOADER.LoadFiles({res.filePath.string()})) + { + auto* img = LOADER.GetImageN(ResourceId::make(res.filePath), res.index); + if(img) + return img; + } + throw std::runtime_error(helpers::format(_("Loading of images %s for map selection failed."), res.filePath)); + }; + + background = loadImage(data.background); + map = loadImage(data.map); + missionMapMask = loadImage(data.missionMapMask); + marker = loadImage(data.marker); + conquered = loadImage(data.conquered); + if(map->GetSize() != missionMapMask->GetSize()) + throw std::runtime_error(_("Map and mission mask have different sizes.")); + + enabledMaskMemory = + libsiedler2::getAllocator().create(libsiedler2::BobType::Bitmap); + enabledMask = dynamic_cast(enabledMaskMemory.get()); + RTTR_Assert(enabledMask); + enabledMask->init(missionMapMask->getWidth(), missionMapMask->getHeight(), libsiedler2::TextureFormat::BGRA); +} + +ctrlMapSelection::ctrlMapSelection(Window* parent, unsigned id, const DrawPoint& pos, const Extent& size, + const SelectionMapInputData& inputData) + : Window(parent, id, pos, size), mapImages(inputData), inputData(inputData), + missionStatus(inputData.missionSelectionInfos.size()), preview(false) +{ + updateEnabledMask(); +} + +ctrlMapSelection::~ctrlMapSelection() = default; + +void ctrlMapSelection::updateEnabledMask() +{ + RTTR_Assert(mapImages.enabledMask->GetSize() == mapImages.missionMapMask->GetSize()); + const libsiedler2::ColorBGRA disabledColor(inputData.disabledColor); + + RTTR_FOREACH_PT(Point, mapImages.enabledMask->GetSize()) + { + const auto pixelColor = mapImages.missionMapMask->getPixel(pt.x, pt.y).asValue(); + const auto matchingMissionIdx = helpers::indexOf_if( + inputData.missionSelectionInfos, [pixelColor](const auto& val) { return val.maskAreaColor == pixelColor; }); + + libsiedler2::ColorBGRA newColor; + if(matchingMissionIdx >= 0 && !missionStatus[matchingMissionIdx].playable) + newColor = disabledColor; + mapImages.enabledMask->setPixel(pt.x, pt.y, newColor); + } +} + +void ctrlMapSelection::setMissionsStatus(const std::vector& status) +{ + if(inputData.missionSelectionInfos.size() != status.size()) + { + throw std::runtime_error( + helpers::format("List has wrong size. %1% != %2%", inputData.missionSelectionInfos.size(), status.size())); + } + + missionStatus = status; + updateEnabledMask(); +} + +void ctrlMapSelection::setSelection(size_t select) +{ + if(select < inputData.missionSelectionInfos.size()) + { + if(missionStatus[select].playable) + currentSelectionPos = inputData.missionSelectionInfos[select].ankerPos; + } else + currentSelectionPos = Position::Invalid(); +} + +int ctrlMapSelection::getCurrentSelection() const +{ + return static_cast(helpers::indexOf_if(inputData.missionSelectionInfos, + [&](const auto& val) { return val.ankerPos == currentSelectionPos; })); +} + +void ctrlMapSelection::setPreview(bool previewOnly) +{ + preview = previewOnly; +} + +bool ctrlMapSelection::Msg_LeftUp(const MouseCoords& mc) +{ + if(!preview && IsMouseOver(mc.GetPos())) + { + const auto pickPos = invertScale(mc.GetPos() - getMapPosition()); + + const auto pixelColor = mapImages.missionMapMask + ->getPixel(helpers::clamp(pickPos.x, 0u, mapImages.map->GetSize().x - 1), + helpers::clamp(pickPos.y, 0u, mapImages.map->GetSize().y - 1)) + .asValue(); + + const auto matchingMissionIdx = helpers::indexOf_if( + inputData.missionSelectionInfos, [pixelColor](const auto& val) { return val.maskAreaColor == pixelColor; }); + + if(matchingMissionIdx >= 0) + { + setSelection(matchingMissionIdx); + GetParent()->Msg_ButtonClick(GetID()); + } + return true; + } + return false; +} + +bool ctrlMapSelection::IsMouseOver(const Position& mousePos) const +{ + return IsPointInRect(mousePos, GetDrawRect()); +} + +float ctrlMapSelection::getScaleFactor() +{ + const auto ratio = PointF(GetSize()) / mapImages.background->GetSize(); + return std::min(ratio.x, ratio.y); +} + +DrawPoint ctrlMapSelection::invertScale(const DrawPoint& scaleIt) +{ + return DrawPoint(scaleIt / getScaleFactor()); +} + +DrawPoint ctrlMapSelection::getBackgroundPosition() +{ + return GetDrawPos() + (GetSize() - scale(mapImages.background->GetSize())) / 2; +} + +DrawPoint ctrlMapSelection::getMapPosition() +{ + return getBackgroundPosition() + scale(inputData.mapOffsetInBackground); +} + +void ctrlMapSelection::drawImageOnMap(glArchivItem_Bitmap* image, const Position& drawPos) +{ + const auto originCorrection = image->GetOrigin() - scale(image->GetOrigin()); + image->DrawFull(Rect(getMapPosition() + scale(drawPos) + originCorrection, scale(image->GetSize()))); +} + +void ctrlMapSelection::Draw_() +{ + Window::Draw_(); + + mapImages.background->DrawFull(Rect(getBackgroundPosition(), scale(mapImages.background->GetSize()))); + + const Rect mapRect = Rect(getMapPosition(), scale(mapImages.map->GetSize())); + mapImages.map->DrawFull(mapRect); + mapImages.enabledMask->DrawFull(mapRect); + + for(const auto idx : helpers::range(missionStatus.size())) + { + if(missionStatus[idx].conquered) + drawImageOnMap(mapImages.conquered, inputData.missionSelectionInfos[idx].ankerPos); + } + + if(!preview && currentSelectionPos.isValid()) + drawImageOnMap(mapImages.marker, currentSelectionPos); +} diff --git a/libs/s25main/controls/ctrlMapSelection.h b/libs/s25main/controls/ctrlMapSelection.h new file mode 100644 index 000000000..5c8acaef3 --- /dev/null +++ b/libs/s25main/controls/ctrlMapSelection.h @@ -0,0 +1,73 @@ +// Copyright (C) 2024 Settlers Freaks (sf-team at siedler25.org) +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "DrawPoint.h" +#include "Window.h" +#include + +class glArchivItem_Bitmap; +namespace libsiedler2 { +class baseArchivItem_Bitmap; +} // namespace libsiedler2 + +struct MissionStatus +{ + bool playable = false; + bool conquered = false; +}; + +class ctrlMapSelection : public Window +{ +public: + ctrlMapSelection(Window* parent, unsigned id, const DrawPoint& pos, const Extent& size, + const SelectionMapInputData& inputData); + ~ctrlMapSelection() override; + + void setMissionsStatus(const std::vector& status); + void setSelection(size_t select); + int getCurrentSelection() const; + void setPreview(bool previewOnly); + + bool Msg_LeftUp(const MouseCoords& mc) override; + +protected: + bool IsMouseOver(const Position& mousePos) const; + void Draw_() override; + + void updateEnabledMask(); + + float getScaleFactor(); + template + Type scale(const Type& scaleIt) + { + return Type(scaleIt * getScaleFactor()); + } + DrawPoint invertScale(const DrawPoint& scaleIt); + + DrawPoint getBackgroundPosition(); + DrawPoint getMapPosition(); + + void drawImageOnMap(glArchivItem_Bitmap* image, const Position& drawPos); + + struct MapImages + { + MapImages(const SelectionMapInputData& data); + + glArchivItem_Bitmap* background; + glArchivItem_Bitmap* map; + glArchivItem_Bitmap* missionMapMask; + glArchivItem_Bitmap* marker; + glArchivItem_Bitmap* conquered; + glArchivItem_Bitmap* enabledMask; + std::unique_ptr enabledMaskMemory; + }; + + const MapImages mapImages; + SelectionMapInputData inputData; + std::vector missionStatus; + Position currentSelectionPos; + bool preview; +}; diff --git a/libs/s25main/desktops/dskCampaignMissionMapSelection.cpp b/libs/s25main/desktops/dskCampaignMissionMapSelection.cpp new file mode 100644 index 000000000..2cd7b7312 --- /dev/null +++ b/libs/s25main/desktops/dskCampaignMissionMapSelection.cpp @@ -0,0 +1,89 @@ +// Copyright (C) 2024 Settlers Freaks (sf-team at siedler25.org) +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "dskCampaignMissionMapSelection.h" +#include "Loader.h" +#include "WindowManager.h" +#include "controls/ctrlMapSelection.h" +#include "dskCampaignSelection.h" +#include "ingameWindows/iwConnecting.h" +#include "ingameWindows/iwMsgbox.h" +#include "lua/CampaignDataLoader.h" +#include "network/GameClient.h" +#include "gameData/CampaignDescription.h" +#include "s25util/Log.h" + +namespace bfs = boost::filesystem; +constexpr unsigned ID_msgBoxError = 0; + +namespace { +enum +{ + ID_Back, + ID_Start, + ID_MapSelection +}; + +constexpr Extent buttonSize(200, 22); +constexpr int spacingBetweenButtons = 2; + +} // namespace + +dskCampaignMissionMapSelection::dskCampaignMissionMapSelection(CreateServerInfo csi, + boost::filesystem::path campaignFolder) + : Desktop(LOADER.GetImageN("setup015", 0)), campaignFolder_(std::move(campaignFolder)), csi_(std::move(csi)) +{ + AddTextButton(ID_Start, DrawPoint(300, 530), buttonSize, TextureColor::Red1, _("Start"), NormalFont); + AddTextButton(ID_Back, DrawPoint(300, 530 + buttonSize.y + spacingBetweenButtons), buttonSize, TextureColor::Red1, + _("Back"), NormalFont); + + settings_ = std::make_unique(); + CampaignDataLoader loader(*settings_, campaignFolder_); + if(!loader.Load() || settings_->getNumMaps() == 0) + LOG.write(_("Failed to load campaign %1%.\n")) % campaignFolder_; + + if(settings_->selectionMapData.has_value()) + { + auto* mapSelection = + AddMapSelection(ID_MapSelection, DrawPoint(0, 0), Extent(800, 508), settings_->selectionMapData.value()); + mapSelection->setMissionsStatus(std::vector(settings_->getNumMaps(), {true, true})); + } +} + +void dskCampaignMissionMapSelection::StartServer(const boost::filesystem::path& mapPath, + const boost::optional& luaPath) +{ + if(!GAMECLIENT.HostGame(csi_, {mapPath, MapType::OldMap, luaPath})) + { + WINDOWMANAGER.Show(std::make_unique(_("Error"), _("Hosting of game not possible"), this, + MsgboxButton::Ok, MsgboxIcon::ExclamationRed, ID_msgBoxError)); + } else + { + iwConnecting& wnd = + WINDOWMANAGER.Show(std::make_unique(csi_.type, std::unique_ptr())); + onErrorConnection_ = wnd.onError.connect([this](ClientError error) { + WINDOWMANAGER.Show(std::make_unique(_("Error"), ClientErrorToStr(error), this, MsgboxButton::Ok, + MsgboxIcon::ExclamationRed, ID_msgBoxError)); + }); + } +} + +void dskCampaignMissionMapSelection::Msg_ButtonClick(unsigned ctrl_id) +{ + if(ctrl_id == ID_Back) + WINDOWMANAGER.Switch(std::make_unique(csi_)); + else if(ctrl_id == ID_Start) + { + if(GetCtrl(ID_MapSelection)) + { + auto selectedMission = GetCtrl(ID_MapSelection)->getCurrentSelection(); + if(selectedMission != -1) + { + const bfs::path mapPath = settings_->getMapFilePath(selectedMission); + const bfs::path luaPath = settings_->getLuaFilePath(selectedMission); + StartServer(mapPath, luaPath); + } + } + } +} diff --git a/libs/s25main/desktops/dskCampaignMissionMapSelection.h b/libs/s25main/desktops/dskCampaignMissionMapSelection.h new file mode 100644 index 000000000..e2c3fd775 --- /dev/null +++ b/libs/s25main/desktops/dskCampaignMissionMapSelection.h @@ -0,0 +1,28 @@ +// Copyright (C) 2024 Settlers Freaks (sf-team at siedler25.org) +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "desktops/Desktop.h" +#include "network/CreateServerInfo.h" +#include +#include +#include + +struct CampaignDescription; + +class dskCampaignMissionMapSelection : public Desktop +{ +public: + dskCampaignMissionMapSelection(CreateServerInfo csi, boost::filesystem::path campaignFolder); + +private: + void Msg_ButtonClick(unsigned ctrl_id) override; + void StartServer(const boost::filesystem::path& mapPath, const boost::optional& luaPath); + + boost::filesystem::path campaignFolder_; + CreateServerInfo csi_; + std::unique_ptr settings_; + boost::signals2::scoped_connection onErrorConnection_; +}; diff --git a/libs/s25main/desktops/dskCampaignSelection.cpp b/libs/s25main/desktops/dskCampaignSelection.cpp index 13fbb14d7..dbb89c7e4 100644 --- a/libs/s25main/desktops/dskCampaignSelection.cpp +++ b/libs/s25main/desktops/dskCampaignSelection.cpp @@ -8,10 +8,12 @@ #include "RttrConfig.h" #include "WindowManager.h" #include "controls/ctrlButton.h" +#include "controls/ctrlMapSelection.h" #include "controls/ctrlMultiline.h" #include "controls/ctrlTable.h" #include "controls/ctrlText.h" #include "controls/ctrlTimer.h" +#include "dskCampaignMissionMapSelection.h" #include "dskCampaignMissionSelection.h" #include "dskSinglePlayer.h" #include "files.h" @@ -41,7 +43,8 @@ enum ID_PreviewDescription, ID_Back, ID_Next, - ID_TimerFillCampaignTable + ID_TimerFillCampaignTable, + ID_MapSelection }; constexpr int startOffsetY = 20; @@ -95,7 +98,7 @@ dskCampaignSelection::dskCampaignSelection(CreateServerInfo csi) void dskCampaignSelection::Msg_TableChooseItem(const unsigned ctrl_id, const unsigned) { if(ctrl_id == ID_Table) - WINDOWMANAGER.Switch(std::make_unique(csi_, getSelectedCampaignPath())); + showCampaignMissionSelectionScreen(); } void dskCampaignSelection::Msg_TableSelectItem(const unsigned ctrl_id, const boost::optional& selection) @@ -127,6 +130,16 @@ void dskCampaignSelection::Msg_TableSelectItem(const unsigned ctrl_id, const boo { campaignImage_ = LOADER.GetImageN(ResourceId::make(RTTRCONFIG.ExpandPath(desc.image)), 0); } + + if(desc.selectionMapData.has_value()) + { + auto* mapSelection = + AddMapSelection(ID_MapSelection, DrawPoint(secondColumnOffsetX, getColumnOffsetY()), + Extent(secondColumnExtentX, campaignImageExtentY), desc.selectionMapData.value()); + mapSelection->setMissionsStatus(std::vector(desc.getNumMaps(), {true, true})); + mapSelection->setPreview(true); + } + btContinue.SetEnabled(true); showCampaignInfo(true); } @@ -137,9 +150,30 @@ void dskCampaignSelection::showCampaignInfo(bool show) { GetCtrl(ID_PreviewDescription)->SetVisible(show); GetCtrl(ID_PreviewTitle)->SetVisible(show); + + if(!show) + DeleteCtrl(ID_MapSelection); + showCampaignInfo_ = show; } +bool dskCampaignSelection::hasMapSelectionScreen() +{ + CampaignDescription desc; + CampaignDataLoader loader(desc, getSelectedCampaignPath()); + if(!loader.Load()) + return false; + return desc.getSelectionMapData().has_value(); +} + +void dskCampaignSelection::showCampaignMissionSelectionScreen() +{ + if(hasMapSelectionScreen()) + WINDOWMANAGER.Switch(std::make_unique(csi_, getSelectedCampaignPath())); + else + WINDOWMANAGER.Switch(std::make_unique(csi_, getSelectedCampaignPath())); +} + boost::filesystem::path dskCampaignSelection::getSelectedCampaignPath() { auto* table = GetCtrl(ID_Table); @@ -157,7 +191,7 @@ void dskCampaignSelection::Msg_ButtonClick(unsigned ctrl_id) if(ctrl_id == ID_Back) WINDOWMANAGER.Switch(std::make_unique()); else if(ctrl_id == ID_Next) - WINDOWMANAGER.Switch(std::make_unique(csi_, getSelectedCampaignPath())); + showCampaignMissionSelectionScreen(); } void dskCampaignSelection::Msg_Timer(unsigned ctrl_id) @@ -239,7 +273,7 @@ void dskCampaignSelection::Draw_() Desktop::Draw_(); // replace this by AddImageButton as soon as it can handle this sceneraio correctly - if(showCampaignInfo_ && campaignImage_) + if(showCampaignInfo_ && campaignImage_ && !hasMapSelectionScreen()) { campaignImage_->DrawFull(Rect(ScaleIf(DrawPoint(secondColumnOffsetX, getColumnOffsetY())), ScaleIf(Extent(secondColumnExtentX, campaignImageExtentY)))); diff --git a/libs/s25main/desktops/dskCampaignSelection.h b/libs/s25main/desktops/dskCampaignSelection.h index 9270e83b3..d1ae7d24d 100644 --- a/libs/s25main/desktops/dskCampaignSelection.h +++ b/libs/s25main/desktops/dskCampaignSelection.h @@ -24,6 +24,8 @@ class dskCampaignSelection : public Desktop void Msg_Timer(unsigned ctrl_id) override; void FillCampaignsTable(); void showCampaignInfo(bool show); + bool hasMapSelectionScreen(); + void showCampaignMissionSelectionScreen(); boost::filesystem::path getSelectedCampaignPath(); bool showCampaignInfo_; CreateServerInfo csi_; diff --git a/libs/s25main/figures/nofFarmhand.cpp b/libs/s25main/figures/nofFarmhand.cpp index 09f36d7e8..ce224a604 100644 --- a/libs/s25main/figures/nofFarmhand.cpp +++ b/libs/s25main/figures/nofFarmhand.cpp @@ -204,28 +204,29 @@ bool nofFarmhand::IsPointAvailable(const MapPoint pt) const void nofFarmhand::WalkToWorkpoint() { - // Sind wir am Ziel angekommen? - if(pos == dest) + if(GetPointQuality(dest) == PointQuality::NotPossible) + { + // Point became invalid -> Abort and go home + WorkAborted(); + StartWalkingHome(); + } else if(pos == dest) { - // Anfangen zu arbeiten + // Arrived at target -> Start working state = State::Work; current_ev = GetEvMgr().AddEvent(this, JOB_CONSTS[job_].work_length, 1); WorkStarted(); - return; - } - - // Weg suchen und gucken ob der Punkt noch in Ordnung ist - const auto dir = world->FindHumanPath(pos, dest, 20); - if(!dir || GetPointQuality(dest) == PointQuality::NotPossible) - { - // Punkt freigeben - world->SetReserved(dest, false); - // Kein Weg führt mehr zum Ziel oder Punkt ist nich mehr in Ordnung --> wieder nach Hause gehen - StartWalkingHome(); } else { - // All good, let's start walking there - StartWalking(*dir); + // Keep on walking if possible + const auto dir = world->FindHumanPath(pos, dest, 20); + if(dir) + StartWalking(*dir); + else + { + // Point now unreachable -> abort and go gome + WorkAborted(); + StartWalkingHome(); + } } } diff --git a/libs/s25main/figures/nofFisher.cpp b/libs/s25main/figures/nofFisher.cpp index a0c47cde4..5277b37d6 100644 --- a/libs/s25main/figures/nofFisher.cpp +++ b/libs/s25main/figures/nofFisher.cpp @@ -89,34 +89,34 @@ unsigned short nofFisher::GetCarryID() const return 70; } -/// Abgeleitete Klasse informieren, wenn sie anfängt zu arbeiten (Vorbereitungen) void nofFisher::WorkStarted() { - // Punkt mit Fisch suchen (mit zufälliger Richtung beginnen) + // Find a random neighbour point with fish + unsigned fishAmount = 0; for(Direction dir : helpers::enumRange(RANDOM_ENUM(Direction))) { - fishing_dir = dir; - Resource neighbourRes = world->GetNode(world->GetNeighbour(pos, fishing_dir)).resources; + const Resource neighbourRes = world->GetNode(world->GetNeighbour(pos, dir)).resources; if(neighbourRes.has(ResourceType::Fish)) + { + fishing_dir = dir; + fishAmount = neighbourRes.getAmount(); break; + } } - // Wahrscheinlichkeit, einen Fisch zu fangen sinkt mit abnehmendem Bestand - unsigned short probability = - 40 + (world->GetNode(world->GetNeighbour(pos, fishing_dir)).resources.getAmount()) * 10; - successful = (RANDOM_RAND(100) < probability); + // Probability to catch a fish is proportional to the amount of fish + const int probability = (fishAmount == 0) ? 0 : 40 + fishAmount * 10; + successful = RANDOM_RAND(100) < probability; + // On success remove the fish now to make it unavailable to others + if(successful && !world->GetGGS().isEnabled(AddonId::INEXHAUSTIBLE_FISH)) + world->ReduceResource(world->GetNeighbour(pos, fishing_dir)); } -/// Abgeleitete Klasse informieren, wenn fertig ist mit Arbeiten void nofFisher::WorkFinished() { - // Wenn ich einen Fisch gefangen habe, den Fisch "abbauen" und in die Hand nehmen if(successful) - { - if(!world->GetGGS().isEnabled(AddonId::INEXHAUSTIBLE_FISH)) - world->ReduceResource(world->GetNeighbour(pos, fishing_dir)); ware = GoodType::Fish; - } else + else ware = boost::none; } diff --git a/libs/s25main/figures/nofMetalworker.cpp b/libs/s25main/figures/nofMetalworker.cpp index 8bd8453c5..1bcc185cd 100644 --- a/libs/s25main/figures/nofMetalworker.cpp +++ b/libs/s25main/figures/nofMetalworker.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2005 - 2021 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 @@ -99,7 +99,7 @@ constexpr helpers::EnumArray CARRYTOOLS_IDS = {78, 79, 80, 91, 81 unsigned short nofMetalworker::GetCarryID() const { - const int toolIdx = helpers::indexOf(TOOL_TO_GOOD, ware); + const auto toolIdx = helpers::indexOf(TOOL_TO_GOOD, ware); return (toolIdx >= 0) ? CARRYTOOLS_IDS[static_cast(toolIdx)] : 0; } diff --git a/libs/s25main/ingameWindows/iwShip.cpp b/libs/s25main/ingameWindows/iwShip.cpp index d580807ed..e280b5d54 100644 --- a/libs/s25main/ingameWindows/iwShip.cpp +++ b/libs/s25main/ingameWindows/iwShip.cpp @@ -279,7 +279,7 @@ void iwShip::DrawCargo() const auto draw_id = convertShieldToNation(ware, owner.nation); - LOADER.GetMapTexture(WARE_STACK_TEX_MAP_OFFSET + rttr::enum_cast(draw_id))->DrawFull(drawPt); + LOADER.GetWareStackTex(draw_id)->DrawFull(drawPt); drawPt.x += xStep; lineCounter++; } diff --git a/libs/s25main/ingameWindows/iwTransport.cpp b/libs/s25main/ingameWindows/iwTransport.cpp index 2de51a2a0..aaf2d54ef 100644 --- a/libs/s25main/ingameWindows/iwTransport.cpp +++ b/libs/s25main/ingameWindows/iwTransport.cpp @@ -38,7 +38,7 @@ iwTransport::iwTransport(const GameWorldViewer& gwv, GameCommandFactory& gcFacto // Buttons der einzelnen Waren anlegen ctrlOptionGroup* group = AddOptionGroup(6, GroupSelectType::Illuminate); - auto getGoodTex = [](GoodType good) { return LOADER.GetMapTexture(WARES_TEX_MAP_OFFSET + rttr::enum_cast(good)); }; + auto getGoodTex = [](GoodType good) { return LOADER.GetWareTex(good); }; buttonData = {{{getGoodTex(GoodType::Coins), WARE_NAMES[GoodType::Coins]}, {LOADER.GetTextureN("io", 111), gettext_noop("Weapons")}, {getGoodTex(GoodType::Beer), WARE_NAMES[GoodType::Beer]}, diff --git a/libs/s25main/network/GameServer.cpp b/libs/s25main/network/GameServer.cpp index 590dd0305..4605f8a1c 100644 --- a/libs/s25main/network/GameServer.cpp +++ b/libs/s25main/network/GameServer.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2005 - 2021 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 @@ -1493,8 +1493,7 @@ bfs::path GameServer::SaveAsyncLog() unsigned numEntries = 0; for(AsyncLog& log : asyncLogs) { - auto it = std::find_if(log.randEntries.begin(), log.randEntries.end(), - [maxCtr](const auto& e) { return e.counter == maxCtr; }); + auto it = helpers::find_if(log.randEntries, [maxCtr](const auto& e) { return e.counter == maxCtr; }); log.randEntries.erase(log.randEntries.begin(), it); if(numEntries < log.randEntries.size()) numEntries = log.randEntries.size(); diff --git a/libs/s25main/nodeObjs/noFlag.cpp b/libs/s25main/nodeObjs/noFlag.cpp index 81ce49149..9e31b95aa 100644 --- a/libs/s25main/nodeObjs/noFlag.cpp +++ b/libs/s25main/nodeObjs/noFlag.cpp @@ -117,8 +117,7 @@ void noFlag::Draw(DrawPoint drawPt) // Waren (von hinten anfangen zu zeichnen) for(unsigned i = wares.size(); i > 0; --i) { - LOADER.GetMapTexture(WARE_STACK_TEX_MAP_OFFSET + rttr::enum_cast(wares[i - 1]->type)) - ->DrawFull(drawPt + WARES_POS[i - 1]); + LOADER.GetWareStackTex(wares[i - 1]->type)->DrawFull(drawPt + WARES_POS[i - 1]); } } diff --git a/libs/s25main/notifications/NotificationManager_impl.h b/libs/s25main/notifications/NotificationManager_impl.h index 66d9595a6..341089248 100644 --- a/libs/s25main/notifications/NotificationManager_impl.h +++ b/libs/s25main/notifications/NotificationManager_impl.h @@ -1,4 +1,4 @@ -// Copyright (C) 2005 - 2021 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 @@ -75,7 +75,7 @@ void NotificationManager::unsubscribe(NoteCallback* callback) noexcept RTTR_Assert(callback->IsSubscribed()); Subscribers& subs = noteId2Subscriber[T_Note::getNoteId()]; CallbackList& callbacks = subs.callbacks; - auto itEl = std::find(callbacks.begin(), callbacks.end(), callback); + auto itEl = helpers::find(callbacks, callback); RTTR_Assert(itEl != callbacks.end()); // We can't modify the list while iterating over it if(subs.isPublishing) diff --git a/libs/s25main/ogl/VBO.h b/libs/s25main/ogl/VBO.h index d523e82d5..e1cd4974f 100644 --- a/libs/s25main/ogl/VBO.h +++ b/libs/s25main/ogl/VBO.h @@ -1,4 +1,4 @@ -// Copyright (C) 2005 - 2021 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 @@ -9,13 +9,13 @@ #include "ogl/BufferHandle.h" #include "ogl/constants.h" #include -#include #include +#include namespace ogl { template -using RequireContiguousMemory = boost::void_t().data())>; +using RequireContiguousMemory = std::void_t().data())>; template class VBO diff --git a/libs/s25main/ogl/glFont.cpp b/libs/s25main/ogl/glFont.cpp index 335fd22dd..78e28e199 100644 --- a/libs/s25main/ogl/glFont.cpp +++ b/libs/s25main/ogl/glFont.cpp @@ -6,11 +6,10 @@ #include "FontStyle.h" #include "Loader.h" #include "drivers/VideoDriverWrapper.h" -#include "glArchivItem_Bitmap.h" +#include "glArchivItem_Bitmap_Raw.h" #include "helpers/containerUtils.h" #include "libsiedler2/ArchivItem_Bitmap_Player.h" #include "libsiedler2/ArchivItem_Font.h" -#include "libsiedler2/IAllocator.h" #include "libsiedler2/PixelBufferBGRA.h" #include "libsiedler2/libsiedler2.h" #include "s25util/utf8.h" @@ -27,8 +26,8 @@ using utf8 = utf::utf_traits; glFont::glFont(const libsiedler2::ArchivItem_Font& font) : maxCharSize(font.getDx(), font.getDy()), asciiMapping{} { - fontWithOutline = libsiedler2::getAllocator().create(libsiedler2::BobType::Bitmap); - fontNoOutline = libsiedler2::getAllocator().create(libsiedler2::BobType::Bitmap); + fontWithOutline = std::make_unique(); + fontNoOutline = std::make_unique(); // first, we have to find how much chars we really have unsigned numChars = 0; @@ -79,7 +78,7 @@ glFont::glFont(const libsiedler2::ArchivItem_Font& font) : maxCharSize(font.getD fontNoOutline->create(bufferNoOutline); fontWithOutline->create(bufferWithOutline); - // Set the placeholder for non-existant glyphs. Use '?' (should always be possible) + // Set the placeholder for non-existent glyphs. Use '?' (should always be possible) if(CharExist(0xFFFD)) placeHolder = GetCharInfo(0xFFFD); else if(CharExist('?')) diff --git a/libs/s25main/pathfinding/OpenListPrioQueue.h b/libs/s25main/pathfinding/OpenListPrioQueue.h index cc046db70..5749b4136 100644 --- a/libs/s25main/pathfinding/OpenListPrioQueue.h +++ b/libs/s25main/pathfinding/OpenListPrioQueue.h @@ -1,9 +1,10 @@ -// Copyright (C) 2005 - 2021 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 #pragma once +#include "helpers/containerUtils.h" #include #include #include @@ -27,7 +28,7 @@ class OpenListPrioQueue : public std::priority_queue, Pr> void rearrange(iterator it) { std::push_heap(Parent::c.begin(), it + 1, Parent::comp); } - iterator find(const T& target) { return std::find(Parent::c.begin(), Parent::c.end(), target); } + iterator find(const T& target) { return helpers::find(Parent::c, target); } void clear() { Parent::c.clear(); } diff --git a/libs/s25main/world/GameWorldBase.cpp b/libs/s25main/world/GameWorldBase.cpp index b7ea3b15c..4e475fc9e 100644 --- a/libs/s25main/world/GameWorldBase.cpp +++ b/libs/s25main/world/GameWorldBase.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2005 - 2021 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 @@ -655,8 +655,8 @@ GameWorldBase::GetSoldiersForSeaAttack(const unsigned char player_attacker, cons for(auto& itBld : tmp) { // Check if the building was already inserted - auto oldBldIt = std::find_if(buildings.begin(), buildings.end(), - nobHarborBuilding::SeaAttackerBuilding::CmpBuilding(itBld.building)); + auto oldBldIt = + helpers::find_if(buildings, nobHarborBuilding::SeaAttackerBuilding::CmpBuilding(itBld.building)); if(oldBldIt == buildings.end()) { // Not found -> Add @@ -680,8 +680,7 @@ GameWorldBase::GetSoldiersForSeaAttack(const unsigned char player_attacker, cons // Soldaten hinzufügen for(nofPassiveSoldier* soldier : tmp_soldiers) { - RTTR_Assert(std::find_if(attackers.begin(), attackers.end(), PotentialSeaAttacker::CmpSoldier(soldier)) - == attackers.end()); + RTTR_Assert(!helpers::contains_if(attackers, PotentialSeaAttacker::CmpSoldier(soldier))); attackers.push_back(PotentialSeaAttacker(soldier, bld.harbor, bld.distance)); } } diff --git a/libs/s25main/world/World.cpp b/libs/s25main/world/World.cpp index 29bfb6b29..782fa4c3b 100644 --- a/libs/s25main/world/World.cpp +++ b/libs/s25main/world/World.cpp @@ -160,7 +160,7 @@ GO_Type World::GetGOT(const MapPoint pt) const void World::ReduceResource(const MapPoint pt) { - uint8_t curAmount = GetNodeInt(pt).resources.getAmount(); + const uint8_t curAmount = GetNodeInt(pt).resources.getAmount(); RTTR_Assert(curAmount > 0); GetNodeInt(pt).resources.setAmount(curAmount - 1u); } diff --git a/tests/common/testContainerUtils.cpp b/tests/common/testContainerUtils.cpp index 4a65f32f4..7d894adf9 100644 --- a/tests/common/testContainerUtils.cpp +++ b/tests/common/testContainerUtils.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2005 - 2021 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 @@ -7,10 +7,13 @@ #include "s25util/warningSuppression.h" #include #include +#include #include BOOST_AUTO_TEST_SUITE(ContainerUtils) +constexpr boost::test_tools::per_element per_element; + BOOST_AUTO_TEST_CASE(MakeUniqueStable) { // Empty vector -> Not modified @@ -36,7 +39,7 @@ BOOST_AUTO_TEST_CASE(MakeUniqueStable) vec = {5, 6, 5, 5, 2, 6, 1, 5, 7, 7, 3, -1, 3}; std::vector expectedVec = {5, 6, 2, 1, 7, 3, -1}; helpers::makeUniqueStable(vec); - BOOST_TEST_REQUIRE(vec == expectedVec, boost::test_tools::per_element()); + BOOST_TEST(vec == expectedVec, per_element); } BOOST_AUTO_TEST_CASE(MakeUnique) @@ -58,39 +61,44 @@ BOOST_AUTO_TEST_CASE(MakeUnique) vec.push_back(-1); helpers::makeUnique(vec); BOOST_TEST_REQUIRE(vec.size() == 2u); - BOOST_TEST_REQUIRE(vec[0] == -1); - BOOST_TEST_REQUIRE(vec[1] == 1); + BOOST_TEST(vec[0] == -1); + BOOST_TEST(vec[1] == 1); // More mixed elements vec = {5, 6, 5, 5, 2, 6, 1, 5, 7, 7, 3, -1, 3}; std::vector expectedVec = {-1, 1, 2, 3, 5, 6, 7}; helpers::makeUnique(vec); - BOOST_TEST_REQUIRE(vec == expectedVec, boost::test_tools::per_element()); + BOOST_TEST(vec == expectedVec, per_element); } BOOST_AUTO_TEST_CASE(IndexOf) { std::vector vec; // Empty vector - BOOST_TEST_REQUIRE(helpers::indexOf(vec, 1) == -1); + BOOST_TEST(helpers::indexOf(vec, 1) == -1); // 1 el vec.push_back(1); - BOOST_TEST_REQUIRE(helpers::indexOf(vec, 1) == 0); - BOOST_TEST_REQUIRE(helpers::indexOf(vec, 2) == -1); + BOOST_TEST(helpers::indexOf(vec, 1) == 0); + BOOST_TEST(helpers::indexOf(vec, 2) == -1); // 2 els vec.push_back(0); - BOOST_TEST_REQUIRE(helpers::indexOf(vec, 1) == 0); - BOOST_TEST_REQUIRE(helpers::indexOf(vec, 0) == 1); - BOOST_TEST_REQUIRE(helpers::indexOf(vec, 2) == -1); + BOOST_TEST(helpers::indexOf(vec, 1) == 0); + BOOST_TEST(helpers::indexOf(vec, 0) == 1); + BOOST_TEST(helpers::indexOf(vec, 2) == -1); // Pointer vector std::vector ptrVec; - BOOST_TEST_REQUIRE(helpers::indexOf(ptrVec, (int*)1337) == -1); //-V566 - BOOST_TEST_REQUIRE(helpers::indexOf(ptrVec, (const int*)1337) == -1); //-V566 - ptrVec.push_back((int*)1336); //-V566 - ptrVec.push_back((int*)1337); //-V566 - ptrVec.push_back((int*)1338); //-V566 - BOOST_TEST_REQUIRE(helpers::indexOf(ptrVec, (int*)1337) == 1); //-V566 - BOOST_TEST_REQUIRE(helpers::indexOf(ptrVec, (const int*)1337) == 1); //-V566 + BOOST_TEST(helpers::indexOf(ptrVec, (int*)1337) == -1); //-V566 + BOOST_TEST(helpers::indexOf(ptrVec, (const int*)1337) == -1); //-V566 + ptrVec.push_back((int*)1336); //-V566 + ptrVec.push_back((int*)1337); //-V566 + ptrVec.push_back((int*)1338); //-V566 + BOOST_TEST(helpers::indexOf(ptrVec, (int*)1337) == 1); //-V566 + BOOST_TEST(helpers::indexOf(ptrVec, (const int*)1337) == 1); //-V566 + + vec = {1, 3, 5, 6}; + BOOST_TEST(helpers::indexOf_if(vec, [](int el) { return el % 2 == 0; }) == 3); + BOOST_TEST(helpers::indexOf_if(vec, [](int el) { return el > 2; }) == 1); + BOOST_TEST(helpers::indexOf_if(vec, [](int el) { return el > 6; }) == -1); } BOOST_AUTO_TEST_CASE(Reverse) @@ -109,40 +117,64 @@ BOOST_AUTO_TEST_CASE(Reverse) for(int i : helpers::reverse(vecIn)) vecOut.push_back(i); std::reverse(vecIn.begin(), vecIn.end()); - BOOST_TEST(vecIn == vecOut, boost::test_tools::per_element()); + BOOST_TEST(vecIn == vecOut, per_element); } BOOST_AUTO_TEST_CASE(Erase) { std::vector vecIn = {1, 2, 3, 4, 5}, vecExp; helpers::erase(vecIn, 42); - BOOST_TEST(vecIn == vecIn, boost::test_tools::per_element()); + BOOST_TEST(vecIn == vecIn, per_element); helpers::erase(vecIn, 2); vecExp = {1, 3, 4, 5}; - BOOST_TEST(vecIn == vecExp, boost::test_tools::per_element()); + BOOST_TEST(vecIn == vecExp, per_element); helpers::erase(vecIn, 1); vecExp = {3, 4, 5}; - BOOST_TEST(vecIn == vecExp, boost::test_tools::per_element()); + BOOST_TEST(vecIn == vecExp, per_element); helpers::erase(vecIn, 5); vecExp = {3, 4}; - BOOST_TEST(vecIn == vecExp, boost::test_tools::per_element()); + BOOST_TEST(vecIn == vecExp, per_element); helpers::erase(vecIn, 4); vecExp = {3}; - BOOST_TEST(vecIn == vecExp, boost::test_tools::per_element()); + BOOST_TEST(vecIn == vecExp, per_element); helpers::erase(vecIn, 3); vecExp = {}; - BOOST_TEST(vecIn == vecExp, boost::test_tools::per_element()); + BOOST_TEST(vecIn == vecExp, per_element); +} + +BOOST_AUTO_TEST_CASE(PopFront) +{ + std::vector vec{1, 2, 3}; + helpers::pop_front(vec); + BOOST_TEST(vec == (std::vector{2, 3})); + + std::set set{1, 2, 3}; + helpers::pop_front(set); + BOOST_TEST(set == (std::set{2, 3})); +} + +BOOST_AUTO_TEST_CASE(Find) +{ + std::vector vec{1, 2, 3}; + BOOST_TEST((helpers::find(vec, 1) == vec.begin())); + BOOST_TEST((helpers::find(vec, 2) == vec.begin() + 1)); + BOOST_TEST((helpers::find(vec, 4) == vec.end())); + + std::set set{1, 2, 3}; + BOOST_TEST((helpers::find(set, 1) == set.begin())); + BOOST_TEST((helpers::find(set, 2) == set.find(2))); + BOOST_TEST((helpers::find(set, 4) == set.end())); } BOOST_AUTO_TEST_CASE(RemoveIf) { std::vector vecIn = {1, 2, 3, 4, 5, 6}, vecExp = {1, 3, 5}; helpers::erase_if(vecIn, [](int i) { return i % 2 == 0; }); - BOOST_TEST(vecIn == vecExp, boost::test_tools::per_element()); + BOOST_TEST(vecIn == vecExp, per_element); vecIn = {1, 2, 3, 4, 5, 6}, vecExp = {2, 4, 6}; helpers::erase_if(vecIn, [](int i) { return i % 2 != 0; }); - BOOST_TEST(vecIn == vecExp, boost::test_tools::per_element()); + BOOST_TEST(vecIn == vecExp, per_element); } BOOST_AUTO_TEST_CASE(Count) diff --git a/tests/common/testEnumRange.cpp b/tests/common/testEnumRange.cpp index 86fe94810..1c7d603ae 100644 --- a/tests/common/testEnumRange.cpp +++ b/tests/common/testEnumRange.cpp @@ -4,6 +4,7 @@ #include "helpers/EnumRange.h" #include +#include enum PlainEnum { @@ -91,3 +92,17 @@ BOOST_AUTO_TEST_CASE(OffsetWorks) result.push_back(e); BOOST_TEST(result == expected); } + +BOOST_AUTO_TEST_CASE(CanAssignToRange) +{ + const auto enums = helpers::EnumRange{}; + std::vector enums_vec(enums.begin(), enums.end()); + std::vector expected{IntEnum::First, IntEnum::Second, IntEnum::Third, IntEnum::Forth}; + BOOST_TEST(enums_vec == expected); + + expected.push_back(expected.front()); + expected.erase(expected.begin()); + const auto enums2 = helpers::enumRange(IntEnum::Second); + enums_vec = std::vector(enums2.begin(), enums2.end()); + BOOST_TEST(enums_vec == expected); +} \ No newline at end of file diff --git a/tests/common/testMultiArray.cpp b/tests/common/testMultiArray.cpp index 24f527134..ec93817e9 100644 --- a/tests/common/testMultiArray.cpp +++ b/tests/common/testMultiArray.cpp @@ -40,8 +40,8 @@ BOOST_AUTO_TEST_CASE(Test2DArray) BOOST_TEST_REQUIRE(sma32[i].size() == 2u); for(int j = 0; j < 2; j++) { - BOOST_TEST_REQUIRE(sma32[i][j] == i * 10 + j); - BOOST_TEST_REQUIRE(sma32(i, j) == i * 10 + j); + BOOST_TEST(sma32[i][j] == i * 10 + j); + BOOST_TEST(sma32(i, j) == i * 10 + j); } } @@ -79,8 +79,8 @@ BOOST_AUTO_TEST_CASE(Test3DArray) BOOST_TEST_REQUIRE(sma432[i][j].size() == 2u); for(int k = 0; k < 2; k++) { - BOOST_TEST_REQUIRE(sma432[i][j][k] == i * 100 + j * 10 + k); - BOOST_TEST_REQUIRE(sma432(i, j, k) == i * 100 + j * 10 + k); + BOOST_TEST(sma432[i][j][k] == i * 100 + j * 10 + k); + BOOST_TEST(sma432(i, j, k) == i * 100 + j * 10 + k); } } } @@ -101,8 +101,8 @@ BOOST_AUTO_TEST_CASE(Test4DArray) BOOST_TEST_REQUIRE(sma3432[i][j][k].size() == 2u); for(int l = 0; l < 2; l++) { - BOOST_TEST_REQUIRE(sma3432[i][j][k][l] == i * 1000 + j * 100 + k * 10 + l); - BOOST_TEST_REQUIRE(sma3432(i, j, k, l) == i * 1000 + j * 100 + k * 10 + l); + BOOST_TEST(sma3432[i][j][k][l] == i * 1000 + j * 100 + k * 10 + l); + BOOST_TEST(sma3432(i, j, k, l) == i * 1000 + j * 100 + k * 10 + l); } } } diff --git a/tests/common/testRect.cpp b/tests/common/testRect.cpp index a546152bd..33883d266 100644 --- a/tests/common/testRect.cpp +++ b/tests/common/testRect.cpp @@ -12,16 +12,16 @@ BOOST_AUTO_TEST_CASE(RectCtor) const Extent size(5, 7); // Compound ctor Rect rect1(origin, size); - BOOST_TEST_REQUIRE(rect1.getOrigin() == origin); - BOOST_TEST_REQUIRE(rect1.getSize() == size); + BOOST_TEST(rect1.getOrigin() == origin); + BOOST_TEST(rect1.getSize() == size); // Semi-Individual ctor Rect rect2(origin, size.x, size.y); - BOOST_TEST_REQUIRE(rect2.getOrigin() == origin); - BOOST_TEST_REQUIRE(rect2.getSize() == size); + BOOST_TEST(rect2.getOrigin() == origin); + BOOST_TEST(rect2.getSize() == size); // Individual ctor Rect rect3(origin.x, origin.y, size.x, size.y); - BOOST_TEST_REQUIRE(rect3.getOrigin() == origin); - BOOST_TEST_REQUIRE(rect3.getSize() == size); + BOOST_TEST(rect3.getOrigin() == origin); + BOOST_TEST(rect3.getSize() == size); } BOOST_AUTO_TEST_CASE(Rectmove) diff --git a/tests/libGameData/testGameData.cpp b/tests/libGameData/testGameData.cpp index 0554b4d5d..fb2497b6a 100644 --- a/tests/libGameData/testGameData.cpp +++ b/tests/libGameData/testGameData.cpp @@ -36,8 +36,8 @@ BOOST_AUTO_TEST_CASE(LoadGameData) { WorldDescription worldDesc; loadGameData(worldDesc); - BOOST_TEST_REQUIRE(worldDesc.edges.size() == 3u * 5u - 1u); - BOOST_TEST_REQUIRE(worldDesc.terrain.size() >= 3u * NUM_TTS); + BOOST_TEST(worldDesc.edges.size() == 3u * 5u - 1u); + BOOST_TEST(worldDesc.terrain.size() >= 3u * NUM_TTS); for(unsigned l = 0; l < NUM_LTS; l++) { auto lt = Landscape(l); @@ -57,24 +57,23 @@ BOOST_AUTO_TEST_CASE(LoadGameData) { auto t = TerrainType(i); const TerrainDesc& desc = worldDesc.terrain.get(worldDesc.terrain.getIndex(tNames[i])); - BOOST_TEST_REQUIRE(desc.s2Id == TerrainData::GetTextureIdentifier(t)); + BOOST_TEST(desc.s2Id == TerrainData::GetTextureIdentifier(t)); EdgeType newEdge = !desc.edgeType ? ET_NONE : EdgeType((helpers::indexOf(eNames, worldDesc.get(desc.edgeType).name) + 1)); - BOOST_TEST_REQUIRE(newEdge == TerrainData::GetEdgeType(lt, t)); - BOOST_TEST_REQUIRE(desc.GetBQ() == TerrainData::GetBuildingQuality(t)); + BOOST_TEST(newEdge == TerrainData::GetEdgeType(lt, t)); + BOOST_TEST(desc.GetBQ() == TerrainData::GetBuildingQuality(t)); BOOST_TEST(!desc.Is(ETerrain::Walkable) == (TerrainData::GetBuildingQuality(t) == TerrainBQ::Nothing || TerrainData::GetBuildingQuality(t) == TerrainBQ::Danger)); - BOOST_TEST_REQUIRE(desc.Is(ETerrain::Mineable) == (TerrainData::GetBuildingQuality(t) == TerrainBQ::Mine)); - BOOST_TEST_REQUIRE(desc.Is(ETerrain::Buildable) - == (TerrainData::GetBuildingQuality(t) == TerrainBQ::Castle)); - BOOST_TEST_REQUIRE(desc.Is(ETerrain::Shippable) == TerrainData::IsUsableByShip(t)); - BOOST_TEST_REQUIRE((desc.kind == TerrainKind::Snow) == TerrainData::IsSnow(lt, t)); - - BOOST_TEST_REQUIRE((desc.IsUsableByAnimals() || desc.kind == TerrainKind::Snow) - == (TerrainData::IsUsableByAnimals(t) || TerrainData::IsSnow(lt, t))); - BOOST_TEST_REQUIRE(desc.IsVital() == TerrainData::IsVital(t)); - BOOST_TEST_REQUIRE(desc.minimapColor == TerrainData::GetColor(lt, t)); + BOOST_TEST(desc.Is(ETerrain::Mineable) == (TerrainData::GetBuildingQuality(t) == TerrainBQ::Mine)); + BOOST_TEST(desc.Is(ETerrain::Buildable) == (TerrainData::GetBuildingQuality(t) == TerrainBQ::Castle)); + BOOST_TEST(desc.Is(ETerrain::Shippable) == TerrainData::IsUsableByShip(t)); + BOOST_TEST((desc.kind == TerrainKind::Snow) == TerrainData::IsSnow(lt, t)); + + BOOST_TEST((desc.IsUsableByAnimals() || desc.kind == TerrainKind::Snow) + == (TerrainData::IsUsableByAnimals(t) || TerrainData::IsSnow(lt, t))); + BOOST_TEST(desc.IsVital() == TerrainData::IsVital(t)); + BOOST_TEST(desc.minimapColor == TerrainData::GetColor(lt, t)); } } // TerrainData::PrintEdgePrios(); @@ -84,13 +83,13 @@ BOOST_AUTO_TEST_CASE(DetectRecursion) { rttr::test::TmpFolder tmp; { - bnw::ofstream file(tmp.get() / "default.lua"); + bnw::ofstream file(tmp / "default.lua"); file << "include(\"foo.lua\")\n"; - bnw::ofstream file2(tmp.get() / "foo.lua"); + bnw::ofstream file2(tmp / "foo.lua"); file2 << "include(\"default.lua\")\n"; } WorldDescription desc; - GameDataLoader loader(desc, tmp.get()); + GameDataLoader loader(desc, tmp); rttr::test::LogAccessor logAcc; BOOST_TEST(!loader.Load()); RTTR_REQUIRE_LOG_CONTAINS("Maximum include depth", false); @@ -100,11 +99,11 @@ BOOST_AUTO_TEST_CASE(DetectInvalidFilenames) { rttr::test::TmpFolder tmp; { - bnw::ofstream file(tmp.get() / "default.lua"); + bnw::ofstream file(tmp / "default.lua"); file << "include(\"foo(=.lua\")\n"; } WorldDescription desc; - GameDataLoader loader(desc, tmp.get()); + GameDataLoader loader(desc, tmp); rttr::test::LogAccessor logAcc; BOOST_TEST(!loader.Load()); RTTR_REQUIRE_LOG_CONTAINS("disallowed chars", false); @@ -114,11 +113,11 @@ BOOST_AUTO_TEST_CASE(DetectNonexistingFile) { rttr::test::TmpFolder tmp; { - bnw::ofstream file(tmp.get() / "default.lua"); + bnw::ofstream file(tmp / "default.lua"); file << "include(\"foo.lua\")\n"; } WorldDescription desc; - GameDataLoader loader(desc, tmp.get()); + GameDataLoader loader(desc, tmp); rttr::test::LogAccessor logAcc; BOOST_TEST(!loader.Load()); RTTR_REQUIRE_LOG_CONTAINS("File not found", false); @@ -128,12 +127,12 @@ BOOST_AUTO_TEST_CASE(DetectWrongExtension) { rttr::test::TmpFolder tmp; { - bnw::ofstream file(tmp.get() / "default.lua"); + bnw::ofstream file(tmp / "default.lua"); file << "include(\"foo.txt\")\n"; - bnw::ofstream file2(tmp.get() / "foo.txt"); + bnw::ofstream file2(tmp / "foo.txt"); } WorldDescription desc; - GameDataLoader loader(desc, tmp.get()); + GameDataLoader loader(desc, tmp); rttr::test::LogAccessor logAcc; BOOST_TEST(!loader.Load()); RTTR_REQUIRE_LOG_CONTAINS("File must have .lua as the extension", false); @@ -142,12 +141,12 @@ BOOST_AUTO_TEST_CASE(DetectWrongExtension) BOOST_AUTO_TEST_CASE(DetectFolderEscape) { rttr::test::TmpFolder tmp; - bfs::path basePath(tmp.get() / "gameData"); + bfs::path basePath(tmp / "gameData"); create_directories(basePath); { bnw::ofstream file(basePath / "default.lua"); file << "include(\"../foo.lua\")\n"; - bnw::ofstream file2(tmp.get() / "foo.lua"); + bnw::ofstream file2(tmp / "foo.lua"); } WorldDescription desc; GameDataLoader loader(desc, basePath); @@ -160,7 +159,7 @@ BOOST_AUTO_TEST_CASE(DetectFolderEscape) BOOST_AUTO_TEST_CASE(DetectAbsolute) { rttr::test::TmpFolder tmp; - bfs::path basePath(tmp.get() / "gameData"); + bfs::path basePath(tmp / "gameData"); create_directories(basePath); { bnw::ofstream file(basePath / "default.lua"); @@ -178,7 +177,7 @@ BOOST_AUTO_TEST_CASE(TextureCoords) { rttr::test::TmpFolder tmp; { - bnw::ofstream file(tmp.get() / "default.lua"); + bnw::ofstream file(tmp / "default.lua"); file << "rttr:AddLandscape{\ name = \"testland\",\ mapGfx = \"/DATA/MAP_0_Z.LST\",\ @@ -203,38 +202,38 @@ BOOST_AUTO_TEST_CASE(TextureCoords) pos = { 10, 20, 32, 31 }, texType = \"rotated\" }"; } WorldDescription desc; - GameDataLoader loader(desc, tmp.get()); + GameDataLoader loader(desc, tmp); BOOST_TEST_REQUIRE(loader.Load()); // Border points are inset by half a pixel for OpenGL (sample middle of pixel!) // Overlapped uses the full rectangle const TerrainDesc::Triangle rsuO = desc.terrain.tryGet("terrain1")->GetRSUTriangle(); - BOOST_TEST_REQUIRE(rsuO.tip == PointF(10 + 32 / 2.f, 20 + 0.5f)); - BOOST_TEST_REQUIRE(rsuO.left == PointF(10 + 0.5f, 20 + 31 - 0.5f)); - BOOST_TEST_REQUIRE(rsuO.right == PointF(10 + 32 - 0.5f, 20 + 31 - 0.5f)); + BOOST_TEST(rsuO.tip == PointF(10 + 32 / 2.f, 20 + 0.5f)); + BOOST_TEST(rsuO.left == PointF(10 + 0.5f, 20 + 31 - 0.5f)); + BOOST_TEST(rsuO.right == PointF(10 + 32 - 0.5f, 20 + 31 - 0.5f)); const TerrainDesc::Triangle usdO = desc.terrain.tryGet("terrain1")->GetUSDTriangle(); - BOOST_TEST_REQUIRE(usdO.tip == PointF(10 + 32 / 2.f, 20 + 31 - 0.5f)); - BOOST_TEST_REQUIRE(usdO.left == PointF(10 + 0.5f, 20 + 0.5f)); - BOOST_TEST_REQUIRE(usdO.right == PointF(10 + 32 - 0.5f, 20 + 0.5f)); + BOOST_TEST(usdO.tip == PointF(10 + 32 / 2.f, 20 + 31 - 0.5f)); + BOOST_TEST(usdO.left == PointF(10 + 0.5f, 20 + 0.5f)); + BOOST_TEST(usdO.right == PointF(10 + 32 - 0.5f, 20 + 0.5f)); // Stacked has RSU over USD const TerrainDesc::Triangle rsuS = desc.terrain.tryGet("terrain2")->GetRSUTriangle(); - BOOST_TEST_REQUIRE(rsuS.tip == rsuO.tip); - BOOST_TEST_REQUIRE(rsuS.left == PointF(10 + 0.5f, 20 + 31 / 2.f)); - BOOST_TEST_REQUIRE(rsuS.right == PointF(10 + 32 - 0.5f, 20 + 31 / 2.f)); + BOOST_TEST(rsuS.tip == rsuO.tip); + BOOST_TEST(rsuS.left == PointF(10 + 0.5f, 20 + 31 / 2.f)); + BOOST_TEST(rsuS.right == PointF(10 + 32 - 0.5f, 20 + 31 / 2.f)); const TerrainDesc::Triangle usdS = desc.terrain.tryGet("terrain2")->GetUSDTriangle(); - BOOST_TEST_REQUIRE(usdS.tip == usdO.tip); - BOOST_TEST_REQUIRE(usdS.left == PointF(10 + 0.5f, 20 + 31 / 2.f)); - BOOST_TEST_REQUIRE(usdS.right == PointF(10 + 32 - 0.5f, 20 + 31 / 2.f)); + BOOST_TEST(usdS.tip == usdO.tip); + BOOST_TEST(usdS.left == PointF(10 + 0.5f, 20 + 31 / 2.f)); + BOOST_TEST(usdS.right == PointF(10 + 32 - 0.5f, 20 + 31 / 2.f)); // Rotated is stacked sideways (note that order stays the same) const TerrainDesc::Triangle rsuR = desc.terrain.tryGet("terrain3")->GetRSUTriangle(); - BOOST_TEST_REQUIRE(rsuR.tip == rsuS.left); - BOOST_TEST_REQUIRE(rsuR.left == rsuS.right); - BOOST_TEST_REQUIRE(rsuR.right == rsuS.tip); + BOOST_TEST(rsuR.tip == rsuS.left); + BOOST_TEST(rsuR.left == rsuS.right); + BOOST_TEST(rsuR.right == rsuS.tip); const TerrainDesc::Triangle usdR = desc.terrain.tryGet("terrain3")->GetUSDTriangle(); - BOOST_TEST_REQUIRE(usdR.tip == usdS.right); - BOOST_TEST_REQUIRE(usdR.left == usdS.tip); - BOOST_TEST_REQUIRE(usdR.right == usdS.left); + BOOST_TEST(usdR.tip == usdS.right); + BOOST_TEST(usdR.left == usdS.tip); + BOOST_TEST(usdR.right == usdS.left); } BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/libsamplerate_cpp/testSamplerate.cpp b/tests/libsamplerate_cpp/testSamplerate.cpp index 18b01c718..24f9d3fc2 100644 --- a/tests/libsamplerate_cpp/testSamplerate.cpp +++ b/tests/libsamplerate_cpp/testSamplerate.cpp @@ -45,7 +45,7 @@ BOOST_AUTO_TEST_CASE(CtorAndBaseFuncsWork) s1.setRatio(44100. / 22050.); // No exceptions thrown } -template::value>::type* = nullptr> +template::value>* = nullptr> void cloneTests(T& s1) { samplerate::State s2 = s1; @@ -56,7 +56,7 @@ void cloneTests(T& s1) BOOST_TEST(s1.getState() != s3.getState()); } -template::value>::type* = nullptr> +template::value>* = nullptr> void cloneTests(const T&) {} diff --git a/tests/s25Main/CMakeLists.txt b/tests/s25Main/CMakeLists.txt index 7e2cfc9c2..a740af244 100644 --- a/tests/s25Main/CMakeLists.txt +++ b/tests/s25Main/CMakeLists.txt @@ -51,7 +51,7 @@ add_subdirectory(UI) option(RTTR_ENABLE_BENCHMARKS "Build the benchmarks" OFF) if(RTTR_ENABLE_BENCHMARKS) - add_subdirectory(benchmarks) + add_subdirectory(benchmarks) endif() if(WIN32) diff --git a/tests/s25Main/UI/testMapSelection.cpp b/tests/s25Main/UI/testMapSelection.cpp new file mode 100644 index 000000000..e764cfd8f --- /dev/null +++ b/tests/s25Main/UI/testMapSelection.cpp @@ -0,0 +1,261 @@ +// Copyright (C) 2024 Settlers Freaks (sf-team at siedler25.org) +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Loader.h" +#include "RttrForeachPt.h" +#include "controls/ctrlMapSelection.h" +#include "driver/MouseCoords.h" +#include "uiHelper/uiHelpers.hpp" +#include "rttr/test/TmpFolder.hpp" +#include +#include +#include +#include +#include +#include + +using namespace rttr::test; + +namespace { +MOCK_BASE_CLASS(TestWindow, Window) +{public: TestWindow(): Window(nullptr, randomValue(), DrawPoint(0, 0)){}}; + +std::unique_ptr createBitmapFromBuffer(libsiedler2::PixelBufferBGRA const& buffer, + const libsiedler2::ArchivItem_Palette* palette = nullptr) +{ + using namespace libsiedler2; + auto bmpRaw = std::make_unique(); + bmpRaw->create(buffer, palette); + auto bmp = std::make_unique(); + bmp->push(std::move(bmpRaw)); + return bmp; +} + +std::unique_ptr createBitmapWithOneColor(Extent const& size, uint32_t const fillColor, + const libsiedler2::ArchivItem_Palette* palette = nullptr) +{ + using namespace libsiedler2; + return createBitmapFromBuffer(PixelBufferBGRA(size.x, size.y, ColorBGRA(fillColor)), palette); +} + +bool storeBitmap(boost::filesystem::path const& storePath, std::unique_ptr const& bitmap) +{ + return libsiedler2::Write(storePath, *bitmap); +} + +libsiedler2::PixelBufferBGRA createMapTestData(Extent const& size) +{ + /* Map Layout: + TB + BB + */ + using namespace libsiedler2; + PixelBufferBGRA buffer(size.x, size.y); + RTTR_FOREACH_PT(Position, size) + { + if(pt.x >= static_cast(size.x) / 2 || pt.y >= static_cast(size.y) / 2) + buffer.set(pt.x, pt.y, ColorBGRA(0xff0000ff)); + } + return buffer; +} + +libsiedler2::PixelBufferBGRA createMissionMapMaskTestData(Extent const& size) +{ + /* Map Layout: + TR + GB + */ + using namespace libsiedler2; + PixelBufferBGRA buffer(size.x, size.y); + RTTR_FOREACH_PT(Position, size) + { + if(pt.x >= static_cast(size.x) / 2 && pt.y < static_cast(size.y) / 2) + buffer.set(pt.x, pt.y, ColorBGRA(0xffff0000)); + if(pt.x < static_cast(size.x) / 2 && pt.y >= static_cast(size.y) / 2) + buffer.set(pt.x, pt.y, ColorBGRA(0xff00ff00)); + if(pt.x >= static_cast(size.x) / 2 && pt.y >= static_cast(size.y) / 2) + buffer.set(pt.x, pt.y, ColorBGRA(0xff0000ff)); + } + return buffer; +} + +SelectionMapInputData createInputForSelectionMap(rttr::test::TmpFolder const& tmp, Extent backgroundSize = {100, 100}, + Extent mapSize = {100, 100}, + Position mapOffsetInBackground = Position(0, 0), + Extent overlaySize = {5, 5}) +{ + uint32_t const disabledColor = 0x70000000; + uint32_t const red = 0xffff0000; + uint32_t const green = 0xff00ff00; + uint32_t const blue = 0xff0000ff; + + auto backgroundPath = tmp / "background.bmp"; + auto mapPath = tmp / "map.bmp"; + auto missionmapmaskPath = tmp / "missionmapmask.bmp"; + auto markerPath = tmp / "marker.bmp"; + auto conqueredPath = tmp / "conquered.bmp"; + + storeBitmap(backgroundPath, createBitmapWithOneColor(backgroundSize, red)); + storeBitmap(mapPath, createBitmapFromBuffer(createMapTestData(mapSize))); + storeBitmap(missionmapmaskPath, createBitmapFromBuffer(createMissionMapMaskTestData(mapSize))); + storeBitmap(markerPath, createBitmapWithOneColor(overlaySize, green)); + storeBitmap(conqueredPath, createBitmapWithOneColor(overlaySize, red)); + + SelectionMapInputData selectionMapInputData; + selectionMapInputData.background = {backgroundPath, 0}; + selectionMapInputData.map = {mapPath, 0}; + selectionMapInputData.missionMapMask = {missionmapmaskPath, 0}; + selectionMapInputData.marker = {markerPath, 0}; + selectionMapInputData.conquered = {conqueredPath, 0}; + selectionMapInputData.mapOffsetInBackground = mapOffsetInBackground; + selectionMapInputData.disabledColor = disabledColor; + selectionMapInputData.missionSelectionInfos = {{red, {Position((mapSize.x * 3) / 4, mapSize.y / 4)}}, + {green, {Position(mapSize.x / 4, (mapSize.y * 3) / 4)}}, + {blue, {Position((mapSize.x * 3) / 4, (mapSize.y * 3) / 4)}}}; + return selectionMapInputData; +} + +void testMapSelection(Position const& windowPos, Extent const& windowExtent, Extent const& mapBackgroundSize, + Extent const& mapSize, Position const& mapOffsetInBackground) +{ + TestWindow wnd; + Position offsetOfBackground = (windowExtent - mapBackgroundSize) / 2; + + const Position firstMissionMidPoint((mapSize.x * 3) / 4, mapSize.y / 4); + const Position secondMissionMidPoint(mapSize.x / 4, (mapSize.y * 3) / 4); + const Position thirdMissionMidPoint((mapSize.x * 3) / 4, (mapSize.y * 3) / 4); + const Position freeQuadrantMidPoint(mapSize.x / 4, mapSize.y / 4); + + rttr::test::TmpFolder tmp; + ctrlMapSelection ms(&wnd, 0, windowPos, windowExtent, + createInputForSelectionMap(tmp, mapBackgroundSize, mapSize, mapOffsetInBackground)); + + // Activate first mission + ms.setMissionsStatus({{true, false}, {false, false}, {false, false}}); + // Initial the selection is not set + BOOST_TEST(ms.getCurrentSelection() == -1); + // Click on non mission area => selection will not change + ms.Msg_LeftUp(MouseCoords(ms.GetPos() + offsetOfBackground + freeQuadrantMidPoint)); + BOOST_TEST(ms.getCurrentSelection() == -1); + // Click on mission area of first mission => selection will change because mission is playable + ms.Msg_LeftUp(MouseCoords(ms.GetPos() + offsetOfBackground + firstMissionMidPoint)); + BOOST_TEST(ms.getCurrentSelection() == 0); + // Click on mission area of second mission => selection will not change because mission is not playable + ms.Msg_LeftUp(MouseCoords(ms.GetPos() + offsetOfBackground + secondMissionMidPoint)); + BOOST_TEST(ms.getCurrentSelection() == 0); + // Click on mission area of third mission => selection will not change because mission is not playable + ms.Msg_LeftUp(MouseCoords(ms.GetPos() + offsetOfBackground + thirdMissionMidPoint)); + BOOST_TEST(ms.getCurrentSelection() == 0); + + // Activate first and second mission + ms.setMissionsStatus({{true, false}, {true, false}, {false, false}}); + // Selection stays the same as before + BOOST_TEST(ms.getCurrentSelection() == 0); + // Click on mission area of second mission => selection will change because mission is playable + ms.Msg_LeftUp(MouseCoords(ms.GetPos() + offsetOfBackground + secondMissionMidPoint)); + BOOST_TEST(ms.getCurrentSelection() == 1); + // Click on mission area of third mission => selection will not change because mission is not playable + ms.Msg_LeftUp(MouseCoords(ms.GetPos() + offsetOfBackground + thirdMissionMidPoint)); + BOOST_TEST(ms.getCurrentSelection() == 1); + // Click on non mission area => selection will not change + ms.Msg_LeftUp(MouseCoords(ms.GetPos() + offsetOfBackground + freeQuadrantMidPoint)); + BOOST_TEST(ms.getCurrentSelection() == 1); + // Click on mission area of first mission => selection will change because mission is playable + ms.Msg_LeftUp(MouseCoords(ms.GetPos() + offsetOfBackground + firstMissionMidPoint)); + BOOST_TEST(ms.getCurrentSelection() == 0); + + // Test selection + // Activate first and second mission + ms.setMissionsStatus({{true, false}, {true, false}, {false, false}}); + // Setting selection to third mission not possible because mission is not playable => stay at previous value + ms.setSelection(2); + BOOST_TEST(ms.getCurrentSelection() == 0); + // Setting selection to second mission is possible because mission is playable + ms.setSelection(1); + BOOST_TEST(ms.getCurrentSelection() == 1); + // Setting selection to invalid mission resets selection to not set + ms.setSelection(3); + BOOST_TEST(ms.getCurrentSelection() == -1); + // Setting selection to third mission not possible because mission is not playable => stay at previous value + ms.setSelection(2); + BOOST_TEST(ms.getCurrentSelection() == -1); + // Setting selection to first mission is possible because mission is playable + ms.setSelection(0); + BOOST_TEST(ms.getCurrentSelection() == 0); + + // Activate all missions + ms.setMissionsStatus({{true, false}, {true, false}, {true, false}}); + + // Preview mode active selection cannot change + ms.setSelection(0); + ms.setPreview(true); + BOOST_TEST(ms.getCurrentSelection() == 0); + ms.Msg_LeftUp(MouseCoords(ms.GetPos() + offsetOfBackground + secondMissionMidPoint)); + BOOST_TEST(ms.getCurrentSelection() == 0); + ms.Msg_LeftUp(MouseCoords(ms.GetPos() + offsetOfBackground + thirdMissionMidPoint)); + BOOST_TEST(ms.getCurrentSelection() == 0); + ms.Msg_LeftUp(MouseCoords(ms.GetPos() + offsetOfBackground + freeQuadrantMidPoint)); + BOOST_TEST(ms.getCurrentSelection() == 0); + ms.Msg_LeftUp(MouseCoords(ms.GetPos() + offsetOfBackground + firstMissionMidPoint)); + BOOST_TEST(ms.getCurrentSelection() == 0); + ms.setPreview(false); + + // Click all four sides and corners and midpoint of each mission to ensure mapping of areas is correct + std::vector midPoints = {firstMissionMidPoint, secondMissionMidPoint, thirdMissionMidPoint}; + for(size_t mid = 0; mid < midPoints.size(); mid++) + { + for(int i = -1; i <= 1; i++) + { + for(int j = -1; j <= 1; j++) + { + Position borderPoint(midPoints[mid].x + i * (mapSize.x / 4), midPoints[mid].y + j * (mapSize.y / 4)); + if(i == 1) + borderPoint.x--; + if(j == 1) + borderPoint.y--; + + int deselectIndex = (mid + 1) < midPoints.size() ? (mid + 1) : 0; + ms.Msg_LeftUp(MouseCoords(ms.GetPos() + offsetOfBackground + midPoints[deselectIndex])); + BOOST_TEST(ms.getCurrentSelection() == deselectIndex); + ms.Msg_LeftUp(MouseCoords(ms.GetPos() + offsetOfBackground + borderPoint)); + BOOST_TEST(ms.getCurrentSelection() == static_cast(mid)); + } + } + } +} +} // namespace + +BOOST_FIXTURE_TEST_SUITE(MapSelection, uiHelper::Fixture) + +BOOST_AUTO_TEST_CASE(MapNoOffsetAndSameSize) +{ + Position const windowPos(0, 0); + Extent const windowSize(100, 100); + Extent const mapBackgroundSize = {100, 100}; + Extent const mapSize = {100, 100}; + Position const mapOffsetInBackground = Position(0, 0); + testMapSelection(windowPos, windowSize, mapBackgroundSize, mapSize, mapOffsetInBackground); +} + +BOOST_AUTO_TEST_CASE(MapNoOffsetAndDifferentSizeInEachDirection) +{ + Position const windowPos(0, 0); + Extent const windowSize(200, 100); + Extent const mapBackgroundSize = {200, 100}; + Extent const mapSize = {200, 100}; + Position const mapOffsetInBackground = Position(0, 0); + testMapSelection(windowPos, windowSize, mapBackgroundSize, mapSize, mapOffsetInBackground); +} + +BOOST_AUTO_TEST_CASE(MapNoOffsetAndDifferentSizeInEachDirectionPlusWindowOffset) +{ + Position const windowPos(10, 10); + Extent const windowSize(200, 100); + Extent const mapBackgroundSize = {200, 100}; + Extent const mapSize = {200, 100}; + Position const mapOffsetInBackground = Position(0, 0); + testMapSelection(windowPos, windowSize, mapBackgroundSize, mapSize, mapOffsetInBackground); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/s25Main/audio/testSounds.cpp b/tests/s25Main/audio/testSounds.cpp index c1498beaf..991a3cd0b 100644 --- a/tests/s25Main/audio/testSounds.cpp +++ b/tests/s25Main/audio/testSounds.cpp @@ -3,6 +3,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "LoadMockupAudio.h" +#include "MusicPlayer.h" #include "drivers/AudioDriverWrapper.h" #include "ogl/MusicItem.h" #include "ogl/SoundEffectItem.h" @@ -30,8 +31,8 @@ BOOST_AUTO_TEST_CASE(DefaultConstructedSoundHandleIsInvalid) { SoundHandle handle; BOOST_TEST_REQUIRE(!handle); - BOOST_REQUIRE_THROW(handle.getRawHandle(), std::runtime_error); - BOOST_REQUIRE_THROW(handle.getType(), std::runtime_error); + BOOST_CHECK_THROW(handle.getRawHandle(), std::runtime_error); + BOOST_CHECK_THROW(handle.getType(), std::runtime_error); } BOOST_FIXTURE_TEST_CASE(SoundHandleGetUnloadedWhenLastGoesOutOfScope, LoadMockupAudio) @@ -42,12 +43,12 @@ BOOST_FIXTURE_TEST_CASE(SoundHandleGetUnloadedWhenLastGoesOutOfScope, LoadMockup SoundHandle handle = AUDIODRIVER.LoadEffect("Foo.wav"); BOOST_TEST_REQUIRE(MockupSoundData::numAlive == 1); BOOST_TEST_REQUIRE(handle); - BOOST_TEST_REQUIRE(handle.getType() == SoundType::Effect); + BOOST_TEST(handle.getType() == SoundType::Effect); MOCK_EXPECT(audioDriverMock->LoadMusic).once().with("Foo2.wav").calls(makeDoLoad(SoundType::Music)); SoundHandle handle2 = AUDIODRIVER.LoadMusic("Foo2.wav"); BOOST_TEST_REQUIRE(MockupSoundData::numAlive == 2); BOOST_TEST_REQUIRE(handle2); - BOOST_TEST_REQUIRE(handle2.getType() == SoundType::Music); + BOOST_TEST(handle2.getType() == SoundType::Music); { // Copy handle @@ -82,7 +83,7 @@ BOOST_FIXTURE_TEST_CASE(SoundHandlesCanBeUnloadedByDriver, LoadMockupAudio) SoundHandle handle = AUDIODRIVER.LoadEffect("Foo.wav"); BOOST_TEST_REQUIRE(MockupSoundData::numAlive == 1); BOOST_TEST_REQUIRE(handle); - BOOST_TEST_REQUIRE(handle.getType() == SoundType::Effect); + BOOST_TEST(handle.getType() == SoundType::Effect); // Release driver MOCK_EXPECT(audioDriverMock->doUnloadSound) @@ -118,9 +119,9 @@ BOOST_FIXTURE_TEST_CASE(PlayFromFile, LoadMockupAudio) MOCK_EXPECT(audioDriverMock->doPlayEffect).in(s).once().with(mock::affirm, 50, false).returns(channel); EffectPlayId id = effect->Play(50, false); //-V522 BOOST_TEST_REQUIRE(MockupSoundData::numAlive == 1); - BOOST_TEST_REQUIRE(effect->getLoadedType() == SoundType::Effect); - BOOST_TEST_REQUIRE(id != EffectPlayId::Invalid); - BOOST_TEST_REQUIRE(audioDriverMock->GetEffectChannel(id) == channel); + BOOST_TEST(effect->getLoadedType() == SoundType::Effect); + BOOST_TEST(id != EffectPlayId::Invalid); + BOOST_TEST(audioDriverMock->GetEffectChannel(id) == channel); MOCK_EXPECT(audioDriverMock->IsEffectPlaying).in(s).once().with(id).returns(true); BOOST_TEST_REQUIRE(AUDIODRIVER.IsEffectPlaying(id)); @@ -131,7 +132,7 @@ BOOST_FIXTURE_TEST_CASE(PlayFromFile, LoadMockupAudio) AUDIODRIVER.StopEffect(id); BOOST_TEST(id == EffectPlayId::Invalid); BOOST_TEST_REQUIRE(audioDriverMock->GetEffectChannel(idCopy) == -1); - BOOST_TEST_REQUIRE(!AUDIODRIVER.IsEffectPlaying(idCopy)); + BOOST_TEST(!AUDIODRIVER.IsEffectPlaying(idCopy)); // Unload when effect goes out of scope MOCK_EXPECT(audioDriverMock->doUnloadSound).in(s).once().calls(makeUnloadHandle(SoundType::Effect)); @@ -160,7 +161,7 @@ BOOST_FIXTURE_TEST_CASE(PlayFromFile, LoadMockupAudio) MOCK_EXPECT(audioDriverMock->PlayMusic).in(s).once().with(mock::any, 0); music->Play(); BOOST_TEST_REQUIRE(MockupSoundData::numAlive == 1); - BOOST_TEST_REQUIRE(music->getLoadedType() == SoundType::Music); + BOOST_TEST(music->getLoadedType() == SoundType::Music); MOCK_EXPECT(audioDriverMock->StopMusic).in(s).once(); AUDIODRIVER.StopMusic(); @@ -169,4 +170,84 @@ BOOST_FIXTURE_TEST_CASE(PlayFromFile, LoadMockupAudio) BOOST_TEST_REQUIRE(MockupSoundData::numAlive == 0); } +static auto isHandle(const driver::RawSoundHandle& expected) +{ + return [&](const driver::RawSoundHandle& actual) { return actual == expected; }; +} +static auto isType(SoundType expected) +{ + return [=](const driver::RawSoundHandle& actual) { return actual.getType() == expected; }; +} + +static void release_soundhandle(const driver::RawSoundHandle& h) +{ + delete static_cast(h.getDriverData()); +} + +BOOST_FIXTURE_TEST_CASE(PlayListRepeatsCorrect, LoadMockupAudio) +{ + libsiedler2::setAllocator(new GlAllocator); + Playlist playlist({(rttr::test::libsiedler2TestFilesDir / "test.ogg").string(), + (rttr::test::libsiedler2TestFilesDir / "testMidi.mid").string()}, + 0, false); + MusicPlayer player; + player.SetPlaylist(playlist); + + auto handleOgg1 = audioDriverMock->doLoad(SoundType::Music); + auto handleOgg2 = audioDriverMock->doLoad(SoundType::Music); + auto handleMidi = audioDriverMock->doLoad(SoundType::Music); + MOCK_EXPECT(audioDriverMock->LoadMusicFromData).once().with(mock::any, ".ogg").returns(handleOgg1); + MOCK_EXPECT(audioDriverMock->LoadMusicFromData).once().with(mock::any, ".ogg").returns(handleOgg2); + MOCK_EXPECT(audioDriverMock->LoadMusicFromData).once().with(mock::any, ".midi").returns(handleMidi); + + // Play both songs once (no repeats), then play first again + mock::sequence s; + MOCK_EXPECT(audioDriverMock->PlayMusic).in(s).once().with(isHandle(handleOgg1), 0); + MOCK_EXPECT(audioDriverMock->PlayMusic).in(s).once().with(isHandle(handleMidi), 0); + MOCK_EXPECT(audioDriverMock->PlayMusic).in(s).once().with(isHandle(handleOgg2), 0); + MOCK_EXPECT(audioDriverMock->doUnloadSound).calls(makeUnloadHandle(SoundType::Music)); + + player.Play(); + player.MusicFinished(); + player.MusicFinished(); + + mock::verify(); + mock::reset(); + + // Use the type to distinguish the 2 songs + MOCK_EXPECT(audioDriverMock->LoadMusicFromData).with(mock::any, ".ogg").calls(makeDoLoad(SoundType::Effect)); + MOCK_EXPECT(audioDriverMock->LoadMusicFromData).with(mock::any, ".midi").calls(makeDoLoad(SoundType::Music)); + MOCK_EXPECT(audioDriverMock->doUnloadSound).calls(release_soundhandle); + + // Play both songs twice (1 repeat), then play first again + Playlist playlist_repeat(playlist.getSongs(), 1, false); + player.SetPlaylist(playlist_repeat); + MOCK_EXPECT(audioDriverMock->PlayMusic).in(s).exactly(2).with(isType(SoundType::Effect), 0); + MOCK_EXPECT(audioDriverMock->PlayMusic).in(s).exactly(2).with(isType(SoundType::Music), 0); + MOCK_EXPECT(audioDriverMock->PlayMusic).in(s).once().with(isType(SoundType::Effect), 0); + + player.Play(); + player.MusicFinished(); + player.MusicFinished(); + player.MusicFinished(); + player.MusicFinished(); + + mock::verify(); + + // Same but in random order + Playlist playlist_random(playlist.getSongs(), 1, true); + player.SetPlaylist(playlist_random); + MOCK_EXPECT(audioDriverMock->PlayMusic).exactly(2).with(isType(SoundType::Effect), 0); + MOCK_EXPECT(audioDriverMock->PlayMusic).exactly(2).with(isType(SoundType::Music), 0); + MOCK_EXPECT(audioDriverMock->PlayMusic).once().with(mock::any, 0); + + player.Play(); + player.MusicFinished(); + player.MusicFinished(); + player.MusicFinished(); + player.MusicFinished(); + + mock::verify(); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/s25Main/benchmarks/CMakeLists.txt b/tests/s25Main/benchmarks/CMakeLists.txt index 5ed33b8df..9906311ea 100644 --- a/tests/s25Main/benchmarks/CMakeLists.txt +++ b/tests/s25Main/benchmarks/CMakeLists.txt @@ -4,7 +4,7 @@ option(RTTR_USE_SYSTEM_BENCHMARK "Use system installed google benchmark. Fails if not found!" "${RTTR_USE_SYSTEM_LIBS}") if(RTTR_USE_SYSTEM_BENCHMARK) - find_package(benchmark REQUIRED) + find_package(benchmark REQUIRED) else() include(FetchContent) FetchContent_Declare( @@ -20,11 +20,11 @@ endif() file(GLOB sources *.cpp) set(benchmarksCommands "") foreach(src IN LISTS sources) - get_filename_component(name ${src} NAME_WE) - set(name BM_${name}) - add_executable(${name} ${src}) - target_link_libraries(${name} PRIVATE s25Main testHelpers testConfig benchmark::benchmark benchmark::benchmark_main) - list(APPEND benchmarksCommands COMMAND ${name}) + get_filename_component(name ${src} NAME_WE) + set(name BM_${name}) + add_executable(${name} ${src}) + target_link_libraries(${name} PRIVATE s25Main testHelpers testConfig benchmark::benchmark benchmark::benchmark_main) + list(APPEND benchmarksCommands COMMAND ${name}) endforeach() add_custom_target(RUN_BENCHMARKS ${benchmarksCommands}) diff --git a/tests/s25Main/campaign/testCampaignLuaFile.cpp b/tests/s25Main/campaign/testCampaignLuaFile.cpp index e56181bdb..072b80638 100644 --- a/tests/s25Main/campaign/testCampaignLuaFile.cpp +++ b/tests/s25Main/campaign/testCampaignLuaFile.cpp @@ -2,8 +2,11 @@ // // SPDX-License-Identifier: GPL-2.0-or-later +#include "PointOutput.h" +#include "helpers/format.hpp" #include "lua/CampaignDataLoader.h" #include "gameData/CampaignDescription.h" +#include "gameData/SelectionMapInputData.h" #include "rttr/test/LocaleResetter.hpp" #include "rttr/test/LogAccessor.hpp" #include "rttr/test/TmpFolder.hpp" @@ -13,32 +16,36 @@ #include #include -/// Require that the log contains "content" -#define REQUIRE_LOG_CONTAINS(content) \ - do \ - { \ - const std::string log = logAcc.getLog(); \ - BOOST_TEST_REQUIRE((log.find(content) != std::string::npos), "Unexpected log: " << log << "\n" \ - << "Expected: " << (content)); \ - \ - } while(false) - namespace bnw = boost::nowide; BOOST_AUTO_TEST_SUITE(CampaignLuaFile) BOOST_AUTO_TEST_CASE(ScriptVersion) { - // No getRequiredLuaVersion + rttr::test::TmpFolder tmp; + const auto campaignFile = tmp / "campaign.lua"; { - rttr::test::TmpFolder tmp; - { - bnw::ofstream file(tmp.get() / "campaign.lua"); - file << ""; - } + bnw::ofstream file(campaignFile); + file << "campaign ={\ + version = \"1\",\ + author = \"Max Meier\",\ + name = \"Meine Kampagne\",\ + shortDescription = \"Sehr kurze Beschreibung\",\ + longDescription = \"Das ist die lange Beschreibung\",\ + image = \"/GFX/PICS/WORLD.LBM\",\ + maxHumanPlayers = 1,\ + difficulty = \"easy\",\ + mapFolder = \"/DATA/MAPS\",\ + luaFolder = \"/CAMPAIGNS/ROMAN\",\ + maps = { \"dessert0.WLD\", \"dessert1.WLD\", \"dessert2.WLD\"}\ + }\n"; + } + + // No getRequiredLuaVersion + { CampaignDescription desc; - CampaignDataLoader loader(desc, tmp.get()); + CampaignDataLoader loader(desc, tmp); rttr::test::LogAccessor logAcc; BOOST_TEST_REQUIRE(!loader.Load()); RTTR_REQUIRE_LOG_CONTAINS( @@ -47,37 +54,54 @@ BOOST_AUTO_TEST_CASE(ScriptVersion) // Correct version { - rttr::test::TmpFolder tmp; { - bnw::ofstream file(tmp.get() / "campaign.lua"); - file << boost::format("function getRequiredLuaVersion()\n return %1%\n end") - % CampaignDataLoader::GetVersion(); + bnw::ofstream file(campaignFile, std::ios::app); + file << helpers::format("function getRequiredLuaVersion()\n return %1%\n end\n", + CampaignDataLoader::GetVersion()); } CampaignDescription desc; - CampaignDataLoader loader(desc, tmp.get()); + CampaignDataLoader loader(desc, tmp); rttr::test::LogAccessor logAcc; - BOOST_TEST_REQUIRE(!loader.Load()); - BOOST_TEST_REQUIRE(loader.CheckScriptVersion()); + BOOST_TEST_REQUIRE(loader.Load()); logAcc.clearLog(); } + // Backwards compatibility: version 2 can load version 1 + { + { + bnw::ofstream file(tmp / "campaign.lua", std::ios::app); + file << "function getRequiredLuaVersion()\n return 1\n end\n"; + } + + CampaignDescription desc; + CampaignDataLoader loader(desc, tmp); + rttr::test::LogAccessor logAcc; + BOOST_TEST_REQUIRE(loader.Load()); + logAcc.clearLog(); + BOOST_TEST_PASSPOINT(); + } + // Wrong version { - rttr::test::TmpFolder tmp; + BOOST_TEST_PASSPOINT(); { - bnw::ofstream file(tmp.get() / "campaign.lua"); - file << boost::format("function getRequiredLuaVersion()\n return %1%\n end") - % (CampaignDataLoader::GetVersion() + 1); + BOOST_TEST_PASSPOINT(); + bnw::ofstream file(tmp / "campaign.lua", std::ios::app); + BOOST_TEST_PASSPOINT(); + file << helpers::format("function getRequiredLuaVersion()\n return %1%\n end\n", + CampaignDataLoader::GetVersion() + 1); + BOOST_TEST_PASSPOINT(); } + BOOST_TEST_PASSPOINT(); CampaignDescription desc; - CampaignDataLoader loader(desc, tmp.get()); + CampaignDataLoader loader(desc, tmp); rttr::test::LogAccessor logAcc; BOOST_TEST_REQUIRE(!loader.Load()); - RTTR_REQUIRE_LOG_CONTAINS((boost::format("Wrong lua script version: %1%. Current version: %2%.\n") - % (CampaignDataLoader::GetVersion() + 1) % CampaignDataLoader::GetVersion()) - .str(), + RTTR_REQUIRE_LOG_CONTAINS(helpers::format("Wrong lua script version: %1%. Current version: %2%.\n", + CampaignDataLoader::GetVersion() + 1, + CampaignDataLoader::GetVersion()), false); } } @@ -86,7 +110,7 @@ BOOST_AUTO_TEST_CASE(LoadCampaignDescriptionWithoutTranslation) { rttr::test::TmpFolder tmp; { - bnw::ofstream file(tmp.get() / "campaign.lua"); + bnw::ofstream file(tmp / "campaign.lua"); file << "campaign ={\ version = \"1\",\ @@ -106,7 +130,7 @@ BOOST_AUTO_TEST_CASE(LoadCampaignDescriptionWithoutTranslation) } CampaignDescription desc; - CampaignDataLoader loader(desc, tmp.get()); + CampaignDataLoader loader(desc, tmp); BOOST_TEST_REQUIRE(loader.Load()); // campaign description @@ -120,7 +144,7 @@ BOOST_AUTO_TEST_CASE(LoadCampaignDescriptionWithoutTranslation) BOOST_TEST(desc.difficulty == "easy"); // maps - BOOST_TEST(desc.getNumMaps() == static_cast(3)); + BOOST_TEST(desc.getNumMaps() == 3u); BOOST_TEST(desc.getMapFilePath(0) == RTTRCONFIG.ExpandPath("/DATA/MAPS/dessert0.WLD")); BOOST_TEST(desc.getMapFilePath(1) == RTTRCONFIG.ExpandPath("/DATA/MAPS/dessert1.WLD")); BOOST_TEST(desc.getMapFilePath(2) == RTTRCONFIG.ExpandPath("/DATA/MAPS/dessert2.WLD")); @@ -133,27 +157,27 @@ BOOST_AUTO_TEST_CASE(LoadCampaignDescriptionFailsDueToMissingCampaignVariable) { rttr::test::TmpFolder tmp; { - bnw::ofstream file(tmp.get() / "campaign.lua"); + bnw::ofstream file(tmp / "campaign.lua"); file << "roemer_campaign ={\ version = \"1\",\ }"; - file << "function getRequiredLuaVersion() return 1 end"; + file << "function getRequiredLuaVersion() return 2 end"; } CampaignDescription desc; - CampaignDataLoader loader(desc, tmp.get()); + CampaignDataLoader loader(desc, tmp); rttr::test::LogAccessor logAcc; BOOST_TEST(!loader.Load()); - REQUIRE_LOG_CONTAINS("Failed to load campaign data!\nReason: Campaign table variable missing."); + RTTR_REQUIRE_LOG_CONTAINS("Failed to load campaign data!\nReason: Campaign table variable missing.", false); } BOOST_AUTO_TEST_CASE(LoadCampaignDescriptionFailsDueToIncorrectDifficulty) { rttr::test::TmpFolder tmp; { - bnw::ofstream file(tmp.get() / "campaign.lua"); + bnw::ofstream file(tmp / "campaign.lua"); file << "campaign ={\ version = \"1\",\ @@ -173,17 +197,17 @@ BOOST_AUTO_TEST_CASE(LoadCampaignDescriptionFailsDueToIncorrectDifficulty) } CampaignDescription desc; - CampaignDataLoader loader(desc, tmp.get()); + CampaignDataLoader loader(desc, tmp); rttr::test::LogAccessor logAcc; BOOST_TEST(!loader.Load()); - REQUIRE_LOG_CONTAINS("Failed to load campaign data!\nReason: Invalid difficulty: middle"); + RTTR_REQUIRE_LOG_CONTAINS("Failed to load campaign data!\nReason: Invalid difficulty: middle", false); } BOOST_AUTO_TEST_CASE(LoadCampaignDescriptionFailsDueToMissingField) { rttr::test::TmpFolder tmp; { - bnw::ofstream file(tmp.get() / "campaign.lua"); + bnw::ofstream file(tmp / "campaign.lua"); file << "campaign ={\ version = \"1\",\ @@ -202,18 +226,18 @@ BOOST_AUTO_TEST_CASE(LoadCampaignDescriptionFailsDueToMissingField) } CampaignDescription desc; - CampaignDataLoader loader(desc, tmp.get()); + CampaignDataLoader loader(desc, tmp); rttr::test::LogAccessor logAcc; BOOST_TEST(!loader.Load()); - REQUIRE_LOG_CONTAINS( - "Failed to load campaign data!\nReason: Failed to load game data: Required field 'luaFolder' not found"); + RTTR_REQUIRE_LOG_CONTAINS( + "Failed to load campaign data!\nReason: Failed to load game data: Required field 'luaFolder' not found", false); } BOOST_AUTO_TEST_CASE(CampaignDescriptionLoadWithTranslation) { rttr::test::TmpFolder tmp; { - bnw::ofstream file(tmp.get() / "campaign.lua"); + bnw::ofstream file(tmp / "campaign.lua"); file << "rttr:RegisterTranslations(\ {\ @@ -251,7 +275,7 @@ BOOST_AUTO_TEST_CASE(CampaignDescriptionLoadWithTranslation) rttr::test::LocaleResetter loc("de"); CampaignDescription desc; - CampaignDataLoader loader(desc, tmp.get()); + CampaignDataLoader loader(desc, tmp); BOOST_TEST_REQUIRE(loader.Load()); // campaign description @@ -265,13 +289,99 @@ BOOST_AUTO_TEST_CASE(CampaignDescriptionLoadWithTranslation) BOOST_TEST(desc.difficulty == "easy"); // maps - BOOST_TEST(desc.getNumMaps() == static_cast(3)); + BOOST_TEST(desc.getNumMaps() == 3u); BOOST_TEST(desc.getMapFilePath(0) == RTTRCONFIG.ExpandPath("/DATA/MAPS/dessert0.WLD")); BOOST_TEST(desc.getMapFilePath(1) == RTTRCONFIG.ExpandPath("/DATA/MAPS/dessert1.WLD")); BOOST_TEST(desc.getMapFilePath(2) == RTTRCONFIG.ExpandPath("/DATA/MAPS/dessert2.WLD")); BOOST_TEST(desc.getLuaFilePath(0) == RTTRCONFIG.ExpandPath("/CAMPAIGNS/ROMAN/dessert0.lua")); BOOST_TEST(desc.getLuaFilePath(1) == RTTRCONFIG.ExpandPath("/CAMPAIGNS/ROMAN/dessert1.lua")); BOOST_TEST(desc.getLuaFilePath(2) == RTTRCONFIG.ExpandPath("/CAMPAIGNS/ROMAN/dessert2.lua")); + + // selection map + BOOST_TEST(!desc.getSelectionMapData().has_value()); +} + +BOOST_AUTO_TEST_CASE(OptionalSelectionMapLoadTest) +{ + rttr::test::TmpFolder tmp; + { + bnw::ofstream file(tmp / "campaign.lua"); + + file << "campaign = {\ + version = \"1\",\ + author = \"Max Meier\",\ + name = \"Meine Kampagne\",\ + shortDescription = \"Sehr kurze Beschreibung\",\ + longDescription = \"Das ist die lange Beschreibung\",\ + image = \"/GFX/PICS/WORLD.LBM\",\ + maxHumanPlayers = 1,\ + difficulty = \"easy\",\ + mapFolder = \"/DATA/MAPS\",\ + luaFolder = \"/CAMPAIGNS/ROMAN\",\ + maps = { \"dessert0.WLD\", \"dessert1.WLD\", \"dessert2.WLD\"},\ + selectionMap = {\ + background = {\"/GFX/PICS/SETUP990.LBM\", 0},\ + map = {\"/GFX/PICS/WORLD.LBM\", 0},\ + missionMapMask = {\"/GFX/PICS/WORLDMSK.LBM\", 0},\ + marker = {\"/DATA/IO/IO.DAT\", 231},\ + conquered = {\"/DATA/IO/IO.DAT\", 232},\ + backgroundOffset = {64, 70},\ + disabledColor = 0x70000000,\ + missionSelectionInfos = {\ + {0xffffff00, 243, 97},\ + {0xffaf73cb, 55, 78},\ + {0xff008fc3, 122, 193}\ + }\ + }\ + }"; + + file << "function getRequiredLuaVersion() return 2 end"; + } + + CampaignDescription desc; + CampaignDataLoader loader(desc, tmp); + BOOST_TEST_REQUIRE(loader.Load()); + + // campaign description + BOOST_TEST(desc.version == "1"); + BOOST_TEST(desc.author == "Max Meier"); + BOOST_TEST(desc.name == "Meine Kampagne"); + BOOST_TEST(desc.shortDescription == "Sehr kurze Beschreibung"); + BOOST_TEST(desc.longDescription == "Das ist die lange Beschreibung"); + BOOST_TEST(desc.image == "/GFX/PICS/WORLD.LBM"); + BOOST_TEST(desc.maxHumanPlayers == 1u); + BOOST_TEST(desc.difficulty == "easy"); + + // maps + BOOST_TEST(desc.getNumMaps() == 3u); + BOOST_TEST(desc.getMapFilePath(0) == RTTRCONFIG.ExpandPath("/DATA/MAPS/dessert0.WLD")); + BOOST_TEST(desc.getMapFilePath(1) == RTTRCONFIG.ExpandPath("/DATA/MAPS/dessert1.WLD")); + BOOST_TEST(desc.getMapFilePath(2) == RTTRCONFIG.ExpandPath("/DATA/MAPS/dessert2.WLD")); + BOOST_TEST(desc.getLuaFilePath(0) == RTTRCONFIG.ExpandPath("/CAMPAIGNS/ROMAN/dessert0.lua")); + BOOST_TEST(desc.getLuaFilePath(1) == RTTRCONFIG.ExpandPath("/CAMPAIGNS/ROMAN/dessert1.lua")); + BOOST_TEST(desc.getLuaFilePath(2) == RTTRCONFIG.ExpandPath("/CAMPAIGNS/ROMAN/dessert2.lua")); + + // selection map + auto selectionMap = desc.getSelectionMapData(); + BOOST_TEST(selectionMap->background.filePath == "/GFX/PICS/SETUP990.LBM"); + BOOST_TEST(selectionMap->background.index == 0u); + BOOST_TEST(selectionMap->map.filePath == "/GFX/PICS/WORLD.LBM"); + BOOST_TEST(selectionMap->map.index == 0u); + BOOST_TEST(selectionMap->missionMapMask.filePath == "/GFX/PICS/WORLDMSK.LBM"); + BOOST_TEST(selectionMap->missionMapMask.index == 0u); + BOOST_TEST(selectionMap->marker.filePath == "/DATA/IO/IO.DAT"); + BOOST_TEST(selectionMap->marker.index == 231u); + BOOST_TEST(selectionMap->conquered.filePath == "/DATA/IO/IO.DAT"); + BOOST_TEST(selectionMap->conquered.index == 232u); + BOOST_TEST(selectionMap->mapOffsetInBackground == Position(64, 70)); + BOOST_TEST(selectionMap->disabledColor == 0x70000000u); + BOOST_TEST(selectionMap->missionSelectionInfos.size() == 3u); + BOOST_TEST(selectionMap->missionSelectionInfos[0].maskAreaColor == 0xffffff00u); + BOOST_TEST(selectionMap->missionSelectionInfos[0].ankerPos == Position(243, 97)); + BOOST_TEST(selectionMap->missionSelectionInfos[1].maskAreaColor == 0xffaf73cbu); + BOOST_TEST(selectionMap->missionSelectionInfos[1].ankerPos == Position(55, 78)); + BOOST_TEST(selectionMap->missionSelectionInfos[2].maskAreaColor == 0xff008fc3u); + BOOST_TEST(selectionMap->missionSelectionInfos[2].ankerPos == Position(122, 193)); } BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/s25Main/integration/testAI.cpp b/tests/s25Main/integration/testAI.cpp index 3b6174e44..c8e737d8d 100644 --- a/tests/s25Main/integration/testAI.cpp +++ b/tests/s25Main/integration/testAI.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2005 - 2021 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 @@ -12,6 +12,7 @@ #include "buildings/nobMilitary.h" #include "factories/AIFactory.h" #include "factories/BuildingFactory.h" +#include "helpers/containerUtils.h" #include "network/GameMessage_Chat.h" #include "notifications/NodeNote.h" #include "worldFixtures/WorldWithGCExecution.h" @@ -33,9 +34,8 @@ using EmptyWorldFixture2P = WorldFixture; template inline bool containsBldType(const T_Col& collection, BuildingType type) { - return std::find_if(collection.begin(), collection.end(), - [type](const noBaseBuilding* bld) { return bld->GetBuildingType() == type; }) - != collection.end(); + return helpers::contains_if(collection, + [type](const noBaseBuilding* bld) { return bld->GetBuildingType() == type; }); } inline bool playerHasBld(const GamePlayer& player, BuildingType type) @@ -127,7 +127,7 @@ BOOST_FIXTURE_TEST_CASE(KeepBQUpdated, BiggerWorldWithGCExecution) RTTR_FOREACH_PT(MapPoint, world.GetSize()) { BOOST_TEST_INFO(pt); - BOOST_TEST_REQUIRE(this->world.GetBQ(pt, curPlayer) == aijh.GetAINode(pt).bq); + BOOST_TEST(this->world.GetBQ(pt, curPlayer) == aijh.GetAINode(pt).bq); } }; const auto assertBqEqualAround = [this, &aijh](const unsigned lineNr, MapPoint pt, unsigned radius) { @@ -136,7 +136,7 @@ BOOST_FIXTURE_TEST_CASE(KeepBQUpdated, BiggerWorldWithGCExecution) pt, radius, [&](const MapPoint curPt, unsigned) { BOOST_TEST_INFO(curPt); - BOOST_TEST_REQUIRE(this->world.GetBQ(curPt, curPlayer) == aijh.GetAINode(curPt).bq); + BOOST_TEST(this->world.GetBQ(curPt, curPlayer) == aijh.GetAINode(curPt).bq); return false; }, true); @@ -293,9 +293,9 @@ BOOST_FIXTURE_TEST_CASE(BuildWoodIndustry, WorldWithGCExecution<1>) && playerHasBld(player, BuildingType::Forester)) break; } - BOOST_TEST_REQUIRE(playerHasBld(player, BuildingType::Sawmill)); - BOOST_TEST_REQUIRE(playerHasBld(player, BuildingType::Woodcutter)); - BOOST_TEST_REQUIRE(playerHasBld(player, BuildingType::Forester)); + BOOST_TEST(playerHasBld(player, BuildingType::Sawmill)); + BOOST_TEST(playerHasBld(player, BuildingType::Woodcutter)); + BOOST_TEST(playerHasBld(player, BuildingType::Forester)); } BOOST_FIXTURE_TEST_CASE(ExpandWhenNoSpace, BiggerWorldWithGCExecution) diff --git a/tests/s25Main/integration/testBaseWarehouse.cpp b/tests/s25Main/integration/testBaseWarehouse.cpp index 8e6a6b826..8feb1500b 100644 --- a/tests/s25Main/integration/testBaseWarehouse.cpp +++ b/tests/s25Main/integration/testBaseWarehouse.cpp @@ -36,24 +36,26 @@ struct AddGoodsFixture : public WorldFixture, public rttr:: { nobBaseWarehouse& hq = *world.GetSpecObj(world.GetPlayer(0).GetHQPos()); for(const auto i : helpers::enumRange()) - { - BOOST_TEST_REQUIRE(hq.GetNumVisualFigures(i) == numPeople[i]); - BOOST_TEST_REQUIRE(hq.GetNumRealFigures(i) == numPeople[i]); - } + BOOST_TEST_CONTEXT("Job: " << rttr::enum_cast(i)) + { + BOOST_TEST(hq.GetNumVisualFigures(i) == numPeople[i]); + BOOST_TEST(hq.GetNumRealFigures(i) == numPeople[i]); + } for(const auto i : helpers::enumRange()) - { - BOOST_TEST_REQUIRE(hq.GetNumVisualWares(i) == numGoods[i]); - BOOST_TEST_REQUIRE(hq.GetNumRealWares(i) == numGoods[i]); - } + BOOST_TEST_CONTEXT("Good: " << rttr::enum_cast(i)) + { + BOOST_TEST(hq.GetNumVisualWares(i) == numGoods[i]); + BOOST_TEST(hq.GetNumRealWares(i) == numGoods[i]); + } } /// Asserts that the expected and actual good count match for the player void testNumGoodsPlayer() { GamePlayer& player = world.GetPlayer(0); for(const auto i : helpers::enumRange()) - BOOST_TEST_REQUIRE(player.GetInventory()[i] == numPeoplePlayer[i]); + BOOST_TEST(player.GetInventory()[i] == numPeoplePlayer[i]); for(const auto i : helpers::enumRange()) - BOOST_TEST_REQUIRE(player.GetInventory()[i] == numGoodsPlayer[i]); + BOOST_TEST(player.GetInventory()[i] == numGoodsPlayer[i]); } }; @@ -155,8 +157,8 @@ BOOST_FIXTURE_TEST_CASE(OrderJob, EmptyWorldFixture1P) } // Ordering another one fails BOOST_TEST_REQUIRE(!hq->OrderJob(Job::Builder, wh, true)); - BOOST_TEST_REQUIRE(hq->GetNumRealFigures(Job::Builder) == 0u); - BOOST_TEST_REQUIRE(hq->GetNumRealWares(GoodType::Hammer) == 0u); + BOOST_TEST(hq->GetNumRealFigures(Job::Builder) == 0u); + BOOST_TEST(hq->GetNumRealWares(GoodType::Hammer) == 0u); } BOOST_FIXTURE_TEST_CASE(DestroyBuilding, EmptyWorldFixture1P) diff --git a/tests/s25Main/integration/testGameWorldView.cpp b/tests/s25Main/integration/testGameWorldView.cpp index 00d497daf..11ffc0578 100644 --- a/tests/s25Main/integration/testGameWorldView.cpp +++ b/tests/s25Main/integration/testGameWorldView.cpp @@ -3,6 +3,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "PointOutput.h" +#include "uiHelper/uiHelpers.hpp" #include "worldFixtures/CreateEmptyWorld.h" #include "worldFixtures/WorldFixture.h" #include "world/GameWorldView.h" @@ -19,6 +20,8 @@ using EmptyWorldFixture1P = WorldFixture; BOOST_FIXTURE_TEST_CASE(HasCorrectDrawCoords, EmptyWorldFixture1P) { + uiHelper::initGUITests(); // Required for GameWorldView + GameWorldViewer gwv(0, world); const auto viewSize = rttr::test::randomPoint(100, 1000); GameWorldView view(gwv, Position(0, 0), viewSize); @@ -78,6 +81,8 @@ BOOST_FIXTURE_TEST_CASE(HasCorrectDrawCoords, EmptyWorldFixture1P) BOOST_FIXTURE_TEST_CASE(GetsCorrectMaxHeight, EmptyWorldFixture1P) { + uiHelper::initGUITests(); // Required for GameWorldView + GameWorldViewer gwv(0, world); const auto viewSize = rttr::test::randomPoint(100, 1000); GameWorldView view(gwv, Position(0, 0), viewSize); diff --git a/tests/s25Main/integration/testPacts.cpp b/tests/s25Main/integration/testPacts.cpp index 169ff0b28..6d936386f 100644 --- a/tests/s25Main/integration/testPacts.cpp +++ b/tests/s25Main/integration/testPacts.cpp @@ -8,8 +8,11 @@ #include "postSystem/PostBox.h" #include "worldFixtures/WorldWithGCExecution.h" #include "gameTypes/GameTypesOutput.h" +#include #include +namespace utf = boost::unit_test; + // LCOV_EXCL_START static std::ostream& operator<<(std::ostream& out, const PactState e) { @@ -27,20 +30,22 @@ BOOST_FIXTURE_TEST_CASE(InitialPactStates, WorldWithGCExecution3P) const GamePlayer& player = world.GetPlayer(i); for(unsigned j = 0; j < world.GetNumPlayers(); j++) { + BOOST_TEST_INFO_SCOPE(i << " vs " << j); // Self is always an ally and not attackable if(i == j) { - BOOST_TEST_REQUIRE(player.IsAlly(j)); - BOOST_TEST_REQUIRE(!player.IsAttackable(j)); + BOOST_TEST(player.IsAlly(j)); + BOOST_TEST(!player.IsAttackable(j)); } else { - BOOST_TEST_REQUIRE(!player.IsAlly(j)); - BOOST_TEST_REQUIRE(player.IsAttackable(j)); + BOOST_TEST(!player.IsAlly(j)); + BOOST_TEST(player.IsAttackable(j)); } for(const auto p : helpers::enumRange()) { - BOOST_TEST_REQUIRE(player.GetPactState(p, j) == PactState::None); - BOOST_TEST_REQUIRE(player.GetRemainingPactTime(p, j) == 0u); + BOOST_TEST_INFO_SCOPE("Pact " << p); + BOOST_TEST(player.GetPactState(p, j) == PactState::None); + BOOST_TEST(player.GetRemainingPactTime(p, j) == 0u); } } } @@ -85,8 +90,7 @@ void CheckPactState(const GameWorldBase& world, unsigned playerIdFrom, unsigned } } // namespace -BOOST_FIXTURE_TEST_CASE(MakePactTest, - WorldWithGCExecution3P) //, *utf::depends_on("PactTestSuite/TestInitialPactStates")) +BOOST_FIXTURE_TEST_CASE(MakePactTest, WorldWithGCExecution3P, *utf::depends_on("PactTestSuite/InitialPactStates")) { for(unsigned i = 0; i < world.GetNumPlayers(); i++) world.GetPostMgr().AddPostBox(i); @@ -110,9 +114,9 @@ BOOST_FIXTURE_TEST_CASE(MakePactTest, BOOST_TEST_REQUIRE(postbox2.GetNumMsgs() == 1u); const auto* msg = dynamic_cast(postbox2.GetMsg(0)); BOOST_TEST_REQUIRE(msg); - BOOST_TEST_REQUIRE(msg->GetPactType() == PactType::NonAgressionPact); //-V522 - BOOST_TEST_REQUIRE(msg->GetPlayerId() == curPlayer); - BOOST_TEST_REQUIRE(msg->IsAccept() == true); + BOOST_TEST(msg->GetPactType() == PactType::NonAgressionPact); //-V522 + BOOST_TEST(msg->GetPlayerId() == curPlayer); + BOOST_TEST(msg->IsAccept() == true); // should be in progress for player1 CheckPactState(world, 1, 2, PactType::NonAgressionPact, PactState::InProgress); @@ -134,7 +138,7 @@ BOOST_FIXTURE_TEST_CASE(MakePactTest, CheckPactState(world, 1, 2, PactType::TreatyOfAlliance, PactState::None); // Check duration - BOOST_TEST_REQUIRE(player1.GetRemainingPactTime(PactType::NonAgressionPact, 2) == duration); + BOOST_TEST(player1.GetRemainingPactTime(PactType::NonAgressionPact, 2) == duration); } // Creates a non-aggression pact between players 1 and 2 @@ -159,7 +163,7 @@ struct PactCreatedFixture : public WorldWithGCExecution3P } }; -BOOST_FIXTURE_TEST_CASE(PactDurationTest, PactCreatedFixture) //, *utf::depends_on("PactTestSuite/MakePactTest")) +BOOST_FIXTURE_TEST_CASE(PactDurationTest, PactCreatedFixture, *utf::depends_on("PactTestSuite/MakePactTest")) { GamePlayer& player1 = world.GetPlayer(1); @@ -179,11 +183,11 @@ BOOST_FIXTURE_TEST_CASE(PactDurationTest, PactCreatedFixture) //, *utf::depends_ player1.TestPacts(); } // On last GF the pact expired - BOOST_TEST_REQUIRE(player1.GetRemainingPactTime(PactType::NonAgressionPact, 2) == 0u); + BOOST_TEST(player1.GetRemainingPactTime(PactType::NonAgressionPact, 2) == 0u); CheckPactState(world, 1, 2, PactType::NonAgressionPact, PactState::None); } -BOOST_FIXTURE_TEST_CASE(PactCanceling, PactCreatedFixture) //, *utf::depends_on("PactTestSuite/MakePactTest")) +BOOST_FIXTURE_TEST_CASE(PactCanceling, PactCreatedFixture, *utf::depends_on("PactTestSuite/MakePactTest")) { PostBox& postbox1 = *world.GetPostMgr().GetPostBox(1); PostBox& postbox2 = *world.GetPostMgr().GetPostBox(2); @@ -198,9 +202,9 @@ BOOST_FIXTURE_TEST_CASE(PactCanceling, PactCreatedFixture) //, *utf::depends_on( BOOST_TEST_REQUIRE(postbox1.GetNumMsgs() == 1u); msg = dynamic_cast(postbox1.GetMsg(0)); BOOST_TEST_REQUIRE(msg); - BOOST_TEST_REQUIRE(msg->GetPlayerId() == 2u); - BOOST_TEST_REQUIRE(msg->GetPactType() == PactType::NonAgressionPact); - BOOST_TEST_REQUIRE(msg->IsAccept() == false); + BOOST_TEST(msg->GetPlayerId() == 2u); + BOOST_TEST(msg->GetPactType() == PactType::NonAgressionPact); + BOOST_TEST(msg->IsAccept() == false); // Same player tries to cancel again this->CancelPact(PactType::NonAgressionPact, 1); @@ -223,8 +227,38 @@ BOOST_FIXTURE_TEST_CASE(PactCanceling, PactCreatedFixture) //, *utf::depends_on( BOOST_TEST_REQUIRE(postbox1.GetNumMsgs() == 3u); BOOST_TEST_REQUIRE(postbox2.GetNumMsgs() == 1u); // The new message should not be a question - BOOST_TEST_REQUIRE(!dynamic_cast(postbox1.GetMsg(postbox1.GetNumMsgs() - 1))); - BOOST_TEST_REQUIRE(!dynamic_cast(postbox2.GetMsg(postbox2.GetNumMsgs() - 1))); + BOOST_TEST(!dynamic_cast(postbox1.GetMsg(postbox1.GetNumMsgs() - 1))); + BOOST_TEST(!dynamic_cast(postbox2.GetMsg(postbox2.GetNumMsgs() - 1))); +} + +BOOST_FIXTURE_TEST_CASE(SuggestNewPactAfterExpiration, PactCreatedFixture, + *utf::depends_on("PactTestSuite/MakePactTest")) +{ + GamePlayer& player1 = world.GetPlayer(1); + PostBox& postbox2 = *world.GetPostMgr().GetPostBox(2); + postbox2.Clear(); + + // Execute the GFs until the pact is expired + for(unsigned i = 0; i < duration; i++) + { + em.ExecuteNextGF(); + player1.TestPacts(); + } + + // On last GF the pact expired. Suggest new pact. + CheckPactState(world, 1, 2, PactType::NonAgressionPact, PactState::None); + curPlayer = 1; + this->SuggestPact(2, PactType::NonAgressionPact, duration); + + // Test if other player has received the suggestion + BOOST_TEST_REQUIRE(postbox2.GetNumMsgs() == 1u); + msg = dynamic_cast(postbox2.GetMsg(0)); + BOOST_TEST_REQUIRE(msg); + // Other player accepts + curPlayer = 2; + this->AcceptPact(msg->GetPactId(), PactType::NonAgressionPact, msg->GetPlayerId()); + // pact state must be accepted + CheckPactState(world, 1, 2, PactType::NonAgressionPact, PactState::Accepted); } BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/testData/200kGFs.rpl b/tests/testData/200kGFs.rpl index d353f6258..67d023383 100644 Binary files a/tests/testData/200kGFs.rpl and b/tests/testData/200kGFs.rpl differ diff --git a/tests/testData/SeaMap300kGfs.rpl b/tests/testData/SeaMap300kGfs.rpl index e9c2d5616..fbc21f440 100644 Binary files a/tests/testData/SeaMap300kGfs.rpl and b/tests/testData/SeaMap300kGfs.rpl differ diff --git a/tests/testHelpers/rttr/test/TmpFolder.hpp b/tests/testHelpers/rttr/test/TmpFolder.hpp index 266a002e4..c95f79b48 100644 --- a/tests/testHelpers/rttr/test/TmpFolder.hpp +++ b/tests/testHelpers/rttr/test/TmpFolder.hpp @@ -23,6 +23,7 @@ class TmpFolder } ~TmpFolder() { boost::filesystem::remove_all(folder); } const boost::filesystem::path& get() const { return folder; } + boost::filesystem::path operator/(const boost::filesystem::path& rhs) const { return folder / rhs; } operator const boost::filesystem::path &() const { return folder; } }; } // namespace rttr::test diff --git a/tools/ci/travisBuild.sh b/tools/ci/build.sh similarity index 93% rename from tools/ci/travisBuild.sh rename to tools/ci/build.sh index ea79674be..3753e8bdb 100755 --- a/tools/ci/travisBuild.sh +++ b/tools/ci/build.sh @@ -53,9 +53,9 @@ export RTTR_DISABLE_ASSERT_BREAKPOINT=1 export UBSAN_OPTIONS=print_stacktrace=1 export AUDIODEV=null # Avoid errors like: ALSA lib confmisc.c:768:(parse_card) cannot find card '0' export SDL_VIDEODRIVER=dummy # Avoid crash on travis -if ! ctest --output-on-failure -j2; then +if ! ctest --output-on-failure -C "${BUILD_TYPE}" -j2; then cat CMakeCache.txt echo "LD:${LD_LIBRARY_PATH:-}" echo "DYLD:${DYLD_LIBRARY_PATH:-}" - ctest --output-on-failure --rerun-failed --extra-verbose + ctest --output-on-failure -C "${BUILD_TYPE}" --rerun-failed --extra-verbose fi diff --git a/tools/release/CMakeLists.txt b/tools/release/CMakeLists.txt index ba3856244..0fd91456f 100644 --- a/tools/release/CMakeLists.txt +++ b/tools/release/CMakeLists.txt @@ -15,7 +15,7 @@ if(WIN32) set(ENV{CMAKE_INSTALL_PREFIX} \${CMAKE_INSTALL_PREFIX}) execute_process(COMMAND ${CMAKE_CURRENT_BINARY_DIR}/bundleWinFiles.sh RESULT_VARIABLE result) if(NOT result EQUAL \"0\") - message(FATAL_ERROR \"Failed preparing the release\") + message(FATAL_ERROR \"Failed preparing the release\") endif() ") endif() diff --git a/tools/release/debian/s25rttr.metainfo.xml b/tools/release/debian/s25rttr.metainfo.xml new file mode 100644 index 000000000..82ff50ecf --- /dev/null +++ b/tools/release/debian/s25rttr.metainfo.xml @@ -0,0 +1,62 @@ + + + info.rttr.Return-To-The-Roots + CC0-1.0 + GPL-2.0-or-later + Return to the Roots + Fan project that reimplements The Settlers 2 game + +

+ The original game was reimplemented with new features such as a multiplayer mode via internet as well as the support for modern hardware. It also includes some smaller gameplay upgrades that are implemented as optional addons. To do that it was necessary to rewrite the whole game, but the original graphics and sounds are still being used. +

+

+ Improvements towards the original game include: +

+
    +
  • IP based multiplayer with optional UPnP port forwarding
  • +
  • Support for higher resolution including a stepless zoom
  • +
  • A new nation: the Babylonians
  • +
  • New buildings: the charcoal burner
  • +
  • Fog of war that shrouds the area when unexplored
  • +
+

+ You will still need an original "The Settlers 2 Gold Edition" version to play Return To The Roots. +

+
+ s25rttr.desktop + + + https://www.siedler25.org/gallery/2015/2015.04.28_02.jpg + + + https://www.siedler25.org/gallery/2014/2014.02.04_9.jpg + + + https://www.siedler25.org/gallery/2013/2013.08.08_2.jpg + + + https://www.siedler25.org/gallery/2011/2011.01.19_11.jpg + + + https://www.rttr.info/ + https://github.com/Return-To-The-Roots/s25client/issues + + moderate + moderate + mild + moderate + moderate + intense + + + + + + + + + + + + +