diff --git a/Launcher/Test/start_maps_tests.cpp b/Launcher/Test/start_maps_tests.cpp new file mode 100644 index 00000000..f6b22e63 --- /dev/null +++ b/Launcher/Test/start_maps_tests.cpp @@ -0,0 +1,74 @@ +// The maps Settings offers to start in, and the "load" the launcher asks the game for. +#include "Launcher/start_maps.h" + +#include +#include +#include +#include + +using namespace dingosdk; +namespace maps = launcher_start_maps; +namespace fs = std::filesystem; + +namespace { + +int failures = 0; + +void check(bool condition, const char* what) { + if (condition) return; + std::cerr << "FAIL: " << what << '\n'; + ++failures; +} + +void write(const fs::path& path, const std::string& text) { + fs::create_directories(path.parent_path()); + std::ofstream(path, std::ios::binary) << text; +} + +} // namespace + +int main(int argc, char** argv) { + const fs::path root = argc > 1 ? fs::path(argv[1]) : fs::temp_directory_path() / "reskate-start-maps-tests"; + std::error_code ignored; + fs::remove_all(root, ignored); + + // No Mods folder: the game's own maps, San Vansterdam first. + auto list = maps::start_maps(root); + check(list.size() == 6, "The game's six maps are not all offered"); + check(!list.empty() && list.front().asset == maps::san_vansterdam && list.front().name == "San Vansterdam", + "San Vansterdam is not first"); + check(maps::load_arguments(list.front()).empty(), "San Vansterdam asks for a load"); + const auto* grom = maps::find_start_map(list, "levels/game/dingolevel_isle_of_grom/dingolevel_isle_of_grom"); + check(grom && grom->name == "Isle of Grom" && grom->mod.empty(), "A retail map is not found whatever the case"); + check(grom && maps::load_arguments(*grom) == L"Levels/Game/DingoLevel_Isle_of_Grom/DingoLevel_Isle_of_Grom", + "A retail map does not load by its asset"); + + write(root / "Mods" / "Map Pack" / "reskate-levels.json", R"({"schema":1,"levels":[ + {"asset":"levels/game/dingolevel_reskate_one/dingolevel_reskate_one","displayName":"One","startPoints":["only"]}, + {"asset":"levels/game/dingolevel_reskate_two/dingolevel_reskate_two","displayName":"Two","startPoints":["Front Gate","back"]}, + {"asset":"levels/game/dingolevel_reskate_three/dingolevel_reskate_three","displayName":"Three","startPoints":["a","b"]}]})"); + write(root / "Mods" / "Switched Off" / "reskate-levels.json", R"({"schema":1,"levels":[ + {"asset":"levels/game/dingolevel_reskate_off/dingolevel_reskate_off","displayName":"Off","startPoints":[]}]})"); + write(root / "Mods" / "Broken" / "reskate-levels.json", R"({"schema":1,"levels":[{"asset":"../x"}]})"); + write(root / "Mods" / "mods.json", R"({"schema":1,"mods":[{"name":"Switched Off","enabled":false}]})"); + + list = maps::start_maps(root); + check(list.size() == 9, "The switched-on map mod's maps are not all offered, or others are"); + check(!maps::find_start_map(list, "levels/game/dingolevel_reskate_off/dingolevel_reskate_off"), + "A switched-off mod's map is offered"); + const auto* one = maps::find_start_map(list, "levels/game/dingolevel_reskate_one/dingolevel_reskate_one"); + check(one && one->name == "One" && one->mod == "Map Pack", "A map mod's map is not named with its mod"); + check(one && maps::load_arguments(*one) == L"levels/game/dingolevel_reskate_one/dingolevel_reskate_one", + "A map with one start point is given one"); + const auto* two = maps::find_start_map(list, "levels/game/dingolevel_reskate_two/dingolevel_reskate_two"); + check(two && maps::load_arguments(*two) == L"levels/game/dingolevel_reskate_two/dingolevel_reskate_two 'Front Gate'", + "A map with several start points does not start at its first, quoted"); + const auto* three = maps::find_start_map(list, "levels/game/dingolevel_reskate_three/dingolevel_reskate_three"); + check(three && maps::load_arguments(*three) == L"levels/game/dingolevel_reskate_three/dingolevel_reskate_three a", + "A plain start point is quoted"); + + fs::remove_all(root, ignored); + if (failures) return 1; + std::cout << "start maps: ok\n"; + return 0; +} diff --git a/Launcher/gui_internal.h b/Launcher/gui_internal.h index 4c976168..610cf583 100644 --- a/Launcher/gui_internal.h +++ b/Launcher/gui_internal.h @@ -5,6 +5,7 @@ #include "launch.h" #include "text_encoding.h" #include "mod_profiles.h" +#include "start_maps.h" #include "thunderstore.h" #include "Engine/Vfs/mod_conflicts.h" @@ -194,6 +195,8 @@ struct Settings { bool gpu_diagnostics{}; bool discord_status{true}; bool offline{}; + // The map the game starts in, by its level asset (start_maps.h); empty: San Vansterdam. + std::string start_map; int menu_key{static_cast(launcher::default_menu_key)}; int console_key{static_cast(launcher::default_console_key)}; int log_level{2}; @@ -433,6 +436,9 @@ struct Ui { Changelog notes; Legal legal; int settings_tab{}; // GAME, DISPLAY, KEYS, ADVANCED, then the game's own pages + // The maps Settings offers to start in, read from the Mods folder when GAME is first drawn + // after Settings opens. + std::optional> start_maps; int binding{}; // 1 = menu key, 2 = console key, while waiting for a press std::string key_error; bool mods{}; diff --git a/Launcher/gui_launcher.cpp b/Launcher/gui_launcher.cpp index a0ed4ac2..8fa842ef 100644 --- a/Launcher/gui_launcher.cpp +++ b/Launcher/gui_launcher.cpp @@ -1,6 +1,7 @@ #include "gui_internal.h" #include "depot_output.h" +#include "start_maps.h" #include "Engine/Core/Json/json.h" #include "Engine/Core/Log/logging.h" @@ -50,6 +51,7 @@ Settings load_settings(const fs::path& path) { settings.gpu_diagnostics = json.value("gpu_diagnostics", settings.gpu_diagnostics); settings.discord_status = json.value("discord_status", settings.discord_status); settings.offline = json.value("offline", settings.offline); + settings.start_map = json.value("start_map", std::string()); settings.menu_key = json.value("menu_key", settings.menu_key); settings.console_key = json.value("console_key", settings.console_key); if (!launcher::bindable_key(static_cast(settings.menu_key)) || @@ -89,6 +91,7 @@ void save_settings(const fs::path& path, const Settings& settings) { json["gpu_diagnostics"] = settings.gpu_diagnostics; json["discord_status"] = settings.discord_status; json["offline"] = settings.offline; + json["start_map"] = settings.start_map; json["menu_key"] = settings.menu_key; json["console_key"] = settings.console_key; json["log_level"] = log_levels[static_cast(settings.log_level)]; @@ -134,6 +137,29 @@ launcher::LaunchOptions launch_options(const Settings& settings) { return launcher::parse_launch_options(arguments); } +// The start map chosen in Settings, as the -map the game is given (empty: San Vansterdam). Read at +// each launch, after the merge: a map whose mod is gone, switched off or left out is not asked for, +// because the game would wait for it for ever; the game then starts in San Vansterdam. +std::wstring start_map_option(const Settings& settings, const fs::path& game_directory) { + if (settings.start_map.empty()) return {}; + const auto maps = launcher_start_maps::start_maps(game_directory); + if (const auto* map = launcher_start_maps::find_start_map(maps, settings.start_map)) { + logging::log(logging::Level::info, logging::Channel::launcher, "Start map: {}{}.", map->name, + map->mod.empty() ? "" : " (mod " + map->mod + ")"); + return launcher_start_maps::load_arguments(*map); + } + logging::log(logging::Level::warning, logging::Channel::launcher, + "Start map {} is not installed, or its mod is switched off or could not be merged; starting in San Vansterdam.", + settings.start_map); + return {}; +} + +launcher::LaunchOptions launch_options(const Settings& settings, const fs::path& game_directory) { + auto options = launch_options(settings); + options.map = start_map_option(settings, game_directory); + return options; +} + } // namespace Launcher::Launcher(const launcher_app::Session& session, std::vector arguments) @@ -536,7 +562,7 @@ void Launcher::run_play() { ignore_mod_problems_ = false; set(Phase::launching, "Starting Skate"); save(); - auto options = launch_options(settings_); + auto options = launch_options(settings_, session_.paths.directory); // The launcher's own command line can request offline mode too. if (std::find(arguments_.begin(), arguments_.end(), L"-offline") != arguments_.end() || std::find(arguments_.begin(), arguments_.end(), L"--offline") != arguments_.end()) @@ -563,7 +589,7 @@ bool prereleases_enabled(const launcher_app::Session& session) { return detail::load_settings(detail::settings_path(session)).prereleases; } launcher::LaunchOptions saved_launch_options(const launcher_app::Session& session) { - return detail::launch_options(detail::load_settings(detail::settings_path(session))); + return detail::launch_options(detail::load_settings(detail::settings_path(session)), session.paths.directory); } void apply_crash_report_setting() noexcept { try { diff --git a/Launcher/gui_settings.cpp b/Launcher/gui_settings.cpp index 01bf3ac6..2655fe73 100644 --- a/Launcher/gui_settings.cpp +++ b/Launcher/gui_settings.cpp @@ -1,5 +1,7 @@ #include "gui_internal.h" +#include "Engine/Game/World/world_names.h" + #include #include @@ -314,6 +316,54 @@ bool key_bindings(const Fonts& fonts, Settings& settings, Ui& ui) { return changed; } +// Settings > GAME: the map the game starts in. Returns true when it changed, so the caller can +// save straight away. +bool start_map_choice(Settings& settings, const launcher_app::Session& session, Ui& ui) { + if (!ui.start_maps) { + try { + ui.start_maps = launcher_start_maps::start_maps(session.paths.directory); + } catch (const std::exception&) { + ui.start_maps.emplace(); + } + } + const auto& maps = *ui.start_maps; + const auto* chosen = settings.start_map.empty() ? nullptr : launcher_start_maps::find_start_map(maps, settings.start_map); + std::string preview = settings.start_map.empty() ? world_level_name(launcher_start_maps::san_vansterdam) + : chosen ? chosen->name : world_level_short_name(settings.start_map) + " (not installed)"; + bool changed = false; + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted("Start map"); + ImGui::SameLine(S(200)); + ImGui::SetNextItemWidth(-1); + if (ImGui::BeginCombo("##start_map", preview.c_str())) { + bool mods_shown = false; + for (std::size_t index = 0; index < maps.size(); ++index) { + const auto& map = maps[index]; + if (!map.mod.empty() && !mods_shown) { + mods_shown = true; + ImGui::SeparatorText("MAP MODS"); + } + const bool san_vansterdam = map.asset == launcher_start_maps::san_vansterdam; + const bool selected = san_vansterdam ? settings.start_map.empty() : &map == chosen; + ImGui::PushID(static_cast(index)); + if (ImGui::Selectable(map.name.c_str(), selected) && !selected) { + settings.start_map = san_vansterdam ? std::string() : map.asset; + changed = true; + } + if (!map.mod.empty() && ImGui::IsItemHovered()) ImGui::SetTooltip("%s", map.mod.c_str()); + if (selected) ImGui::SetItemDefaultFocus(); + ImGui::PopID(); + } + ImGui::EndCombo(); + } + setting_note(!settings.start_map.empty() && !chosen ? + "This map's mod is not installed, is switched off, or could not be merged: Skate starts in San Vansterdam " + "until it is back." : + "skate. always opens in San Vansterdam; another map loads as soon as the game is up. A map mod that is " + "switched off or removed later falls back to San Vansterdam."); + return changed; +} + } // namespace void settings_window(Launcher& launcher, const Fonts& fonts, ImVec2 size, Ui& ui, HWND window) { @@ -387,6 +437,7 @@ void settings_window(Launcher& launcher, const Fonts& fonts, ImVec2 size, Ui& ui settings.loose_files); setting_check("Discord status", "Show on your Discord profile where you are skating: the map, the server or lobby " "and how many are in it. Off, the game does not talk to Discord at all.", settings.discord_status); + if (start_map_choice(settings, launcher.session(), ui)) launcher.save(); ImGui::Spacing(); section_caption(fonts, "EXTRA GAME ARGUMENTS"); std::array arguments{}; @@ -497,8 +548,9 @@ void settings_window(Launcher& launcher, const Fonts& fonts, ImVec2 size, Ui& ui if (!open) { launcher.save(); launcher.check_again_if_update_choices_changed(); - // Read again the next time: the game may have run in between. + // Read again the next time: the game may have run in between, and mods may have changed. ui.game.loaded = false; + ui.start_maps.reset(); } } diff --git a/Launcher/launch.cpp b/Launcher/launch.cpp index bb15ab0f..18730d9d 100644 --- a/Launcher/launch.cpp +++ b/Launcher/launch.cpp @@ -852,14 +852,15 @@ DWORD start_game(const Session& session, const launcher::LaunchOptions& options, } set_environment(L"RESKATE_GPU_DIAGNOSTICS", options.gpu_diagnostics ? L"1" : L"0"); // -map: the game loads it once it is up, as the console's "load" would (client_tick.cpp), - // ahead of any startup commands already asked for. - if (!options.map.empty()) { - std::wstring commands = L"load " + options.map; - std::wstring already(4096, L'\0'); - const auto length = GetEnvironmentVariableW(L"RESKATE_STARTUP_COMMANDS", already.data(), 4096); - if (length && length < 4096) commands += L";" + already.substr(0, length); - set_environment(L"RESKATE_STARTUP_COMMANDS", commands.c_str()); - } + // ahead of any startup commands already asked for. Those are the launcher's own, read once: a + // launcher kept open sets the variable again at each launch, and must not add to the last one. + static const std::wstring asked = [] { + std::wstring value(4096, L'\0'); + const auto length = GetEnvironmentVariableW(L"RESKATE_STARTUP_COMMANDS", value.data(), 4096); + return length && length < 4096 ? value.substr(0, length) : std::wstring{}; + }(); + std::wstring commands = options.map.empty() ? asked : L"load " + options.map + (asked.empty() ? L"" : L";" + asked); + set_environment(L"RESKATE_STARTUP_COMMANDS", commands.empty() ? nullptr : commands.c_str()); set_environment(L"RESKATE_DISCORD", options.discord ? L"1" : L"0"); if (options.menu_key == options.console_key) throw std::runtime_error("The menu and console need different keys. Change one in Settings."); diff --git a/Launcher/start_maps.cpp b/Launcher/start_maps.cpp new file mode 100644 index 00000000..4d86dafb --- /dev/null +++ b/Launcher/start_maps.cpp @@ -0,0 +1,73 @@ +#include "start_maps.h" + +#include "Engine/Game/World/custom_level_manifest.h" +#include "Engine/Game/World/world_names.h" +#include "Engine/Vfs/mod_list.h" + +#include +#include + +namespace dingosdk::launcher_start_maps { +namespace { + +bool same(std::string_view a, std::string_view b) { + const auto fold = [](char c) { return c >= 'A' && c <= 'Z' ? static_cast(c + ('a' - 'A')) : c; }; + return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin(), [&](char x, char y) { return fold(x) == fold(y); }); +} + +// The retail maps, as a dedicated server lists them (server_config.cpp). +constexpr std::array retail{ + san_vansterdam, + "Levels/Game/DingoLevel_Isle_of_Grom/DingoLevel_Isle_of_Grom", + "Levels/Game/DingoLevel_MPR/DingoLevel_MPR", + "Levels/Game/DingoLevel_FTUE_Island/DingoLevel_FTUE_Island", + "Levels/Game/DingoLevel_SDM/DingoLevel_SDM_Int_001/DingoLevel_SDM_Int_001", + "Levels/Game/DingoLevel_SDM/DingoLevel_SDM_Int_002/DingoLevel_SDM_Int_002"}; + +} // namespace + +std::vector start_maps(const std::filesystem::path& game_directory) { + std::vector maps; + for (const auto asset : retail) maps.push_back({std::string(asset), world_level_name(asset), {}, {}}); + // The mods the game's level list reads (mod_catalog.cpp, level_manifest_paths): switched on, + // not left out by the last merge, and made for this game build. + const auto list = mods::scan_mods(game_directory); + if (!list.issue.empty()) return maps; + for (const auto& entry : list.entries) { + const auto& mod = entry.mod; + if (!entry.enabled || !mod.provides_levels || !mod.outdated.empty() || list.excluded.contains(mod.name)) continue; + const auto manifest = read_custom_level_manifest(mod.directory / "reskate-levels.json"); + if (!manifest.issue.empty()) continue; + for (const auto& level : manifest.levels) { + if (find_start_map(maps, level.asset)) continue; + StartMap map{level.asset, level.display_name, mod.name, {}}; + // A map with one start point starts there; one with several must be given one. + if (level.start_points.size() > 1 && level.manifest_start_point.empty()) + map.start_point = level.start_points.front().name; + maps.push_back(std::move(map)); + } + } + return maps; +} + +const StartMap* find_start_map(const std::vector& maps, std::string_view asset) { + for (const auto& map : maps) + if (same(map.asset, asset)) return ↦ + return nullptr; +} + +std::wstring load_arguments(const StartMap& map) { + if (same(map.asset, san_vansterdam)) return {}; + // Asset and start point names are plain ASCII (custom_level_manifest.cpp); a start point with + // a space in it is quoted, as the console reads it. + std::wstring text(map.asset.begin(), map.asset.end()); + if (!map.start_point.empty()) { + const bool spaced = map.start_point.find(' ') != std::string::npos; + text += spaced ? L" '" : L" "; + text.append(map.start_point.begin(), map.start_point.end()); + if (spaced) text += L"'"; + } + return text; +} + +} // namespace dingosdk::launcher_start_maps diff --git a/Launcher/start_maps.h b/Launcher/start_maps.h new file mode 100644 index 00000000..34b0bc45 --- /dev/null +++ b/Launcher/start_maps.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include +#include + +// The map the game starts in (Settings > GAME > Start map). The game always starts in San +// Vansterdam; another map is loaded once it is up, as the console's "load" would (the launcher's +// -map, launch.cpp), so San Vansterdam is chosen by asking for nothing. +namespace dingosdk::launcher_start_maps { + +inline constexpr std::string_view san_vansterdam = "Levels/Game/BAM_LevelRoot/BAM_LevelRoot"; + +struct StartMap { + std::string asset; // the level, as reskate-levels.json or the game names it + std::string name; // as the game's menus show it + std::string mod; // the mod folder that adds it; empty for one of the game's own + std::string start_point; // its first, for a custom map with several; empty: the map's own choice +}; + +// San Vansterdam first, the game's other maps, then the custom maps of the mods in +// `game_directory`\Mods that load (switched on, merged, made for this game build), in load order. +std::vector start_maps(const std::filesystem::path& game_directory); +// The map with this asset (ASCII case ignored), or null. +const StartMap* find_start_map(const std::vector& maps, std::string_view asset); +// What follows "load " for the map: its asset, and its start point when it has several and names +// none to start at. Empty for San Vansterdam, which the game is in already. +std::wstring load_arguments(const StartMap& map); + +} // namespace dingosdk::launcher_start_maps diff --git a/README.md b/README.md index 57e1593c..b86922de 100644 --- a/README.md +++ b/README.md @@ -248,7 +248,7 @@ Useful launcher flags: | `--log-trace` | the same as `--log-level=trace`; an explicit `--log-level` wins | | `-wconsole` | also show the log live in a console window while the game runs | | `--menu-key=0x2D`, `--console-key=0xC0` | menu and console keys (virtual-key codes) | -| `-map ` | load that map as soon as the game is up, e.g. `-map "San Vansterdam"`; the name is a map's name, short name or asset path, or the start of one | +| `-map ` | load that map as soon as the game is up, e.g. `-map "San Vansterdam"`; the name is a map's name, short name or asset path, or the start of one. Over the start map chosen in Settings > GAME | | `--no-loose-files` | ignore loose Lua and config files beside the game | | `--gpu-diagnostics` | record extra detail when the graphics driver crashes (DRED) | diff --git a/Runtime/client_tick.cpp b/Runtime/client_tick.cpp index b7a45ff8..ae697d24 100644 --- a/Runtime/client_tick.cpp +++ b/Runtime/client_tick.cpp @@ -583,7 +583,9 @@ void update_model(std::uintptr_t client, TickState& frame) { // RESKATE_STARTUP_COMMANDS="load skate1map;noclip 1" (development, and the launcher's // -map , which is a "load" at the front of it): console commands // queued in order once the world is up and can accept them, so a build can be exercised - // without anyone driving the overlay. A load waits for its destination to be listed. + // without anyone driving the overlay. A load waits for its destination to be listed, for 20 + // seconds once the world can load (the launcher's start map, start_maps.h, falls back to the + // map the game opened in). { static std::optional> startup_commands; if (!startup_commands) { @@ -624,12 +626,26 @@ void update_model(std::uintptr_t client, TickState& frame) { } } bool ready = !wait && r.model.can_queue_load && !r.requests.loading(); + // A destination still not listed this long after the world could load it is not + // installed (a map mod switched off or left out of the merge): the load is dropped and + // the game stays where it started, instead of the list waiting on it for ever. + static std::optional missing_since; if (ready && lower(text).starts_with("load ")) { const auto wanted = lower(text.substr(5, text.find(' ', 5) == std::string::npos ? std::string::npos : text.find(' ', 5) - 5)); ready = std::ranges::any_of(r.model.levels, [&](const dingosdk::overlay::Level& level) { return level.can_load && (lower(level.asset).find(wanted) != std::string::npos || lower(level.display_name) == wanted); }); + const auto now = std::chrono::steady_clock::now(); + if (ready) missing_since.reset(); + else if (!missing_since) missing_since = now; + else if (now - *missing_since >= std::chrono::seconds(20)) { + dingosdk::logging::log(dingosdk::logging::Level::warning, dingosdk::logging::Channel::level, + "Startup command dropped: {} is not an installed map (is its mod switched off?); staying in this map.", + text.substr(5)); + missing_since.reset(); + startup_commands->erase(startup_commands->begin()); + } } if (ready && r.requests.enqueue(ConsoleRequest{text}, request_context(r), GetCurrentThreadId())) { dingosdk::logging::log(dingosdk::logging::Level::info, dingosdk::logging::Channel::level, diff --git a/cmake/Apps.cmake b/cmake/Apps.cmake index 12a84d84..3e4e99d2 100644 --- a/cmake/Apps.cmake +++ b/cmake/Apps.cmake @@ -4,9 +4,9 @@ if(WIN32) Launcher/gui_settings.cpp Launcher/gui_sign_in.cpp Launcher/gui_mods.cpp Launcher/gui_mods_browse.cpp Launcher/gui_mods_profiles.cpp Launcher/gui_gamepad.cpp Launcher/gamepad_input.cpp Launcher/gui_changelog.cpp Launcher/gui_legal.cpp Launcher/game_settings.cpp Launcher/updater.cpp Launcher/mod_manager.cpp Launcher/mod_profiles.cpp Launcher/thunderstore.cpp - Launcher/problem.h) + Launcher/start_maps.cpp Launcher/problem.h) target_link_libraries(dingosdk_launcher PRIVATE dingosdk_logging dingosdk_content_cache_install dingosdk_world_layer_scan dingosdk_launcher_support dingosdk_initfs - dingosdk_mod_list dingosdk_mods dingosdk_json dingosdk_miniz dingosdk_imgui dingosdk_playstation_input winhttp shell32 dwmapi windowscodecs ole32) + dingosdk_mod_list dingosdk_custom_level_manifest dingosdk_mods dingosdk_json dingosdk_miniz dingosdk_imgui dingosdk_playstation_input winhttp shell32 dwmapi windowscodecs ole32) set_target_properties(dingosdk_launcher PROPERTIES OUTPUT_NAME "ReSkateLauncher") dingosdk_version_info(dingosdk_launcher "ReSkate launcher" "ReSkateLauncher.exe" VFT_APP) endif() @@ -40,6 +40,10 @@ if(DINGOSDK_BUILD_LAUNCHER_TESTS AND WIN32) target_link_libraries(dingosdk_thunderstore_package_tests PRIVATE dingosdk_json) target_include_directories(dingosdk_thunderstore_package_tests PRIVATE "${PROJECT_SOURCE_DIR}") add_test(NAME thunderstore_package COMMAND dingosdk_thunderstore_package_tests) + add_executable(dingosdk_start_maps_tests Launcher/Test/start_maps_tests.cpp Launcher/start_maps.cpp) + target_link_libraries(dingosdk_start_maps_tests PRIVATE dingosdk_mod_list dingosdk_custom_level_manifest) + target_include_directories(dingosdk_start_maps_tests PRIVATE "${PROJECT_SOURCE_DIR}") + add_test(NAME launcher_start_maps COMMAND dingosdk_start_maps_tests "${CMAKE_CURRENT_BINARY_DIR}/start-maps-fixture") add_executable(dingosdk_game_settings_tests Launcher/Test/game_settings_tests.cpp Launcher/game_settings.cpp) target_include_directories(dingosdk_game_settings_tests PRIVATE "${PROJECT_SOURCE_DIR}") add_test(NAME launcher_game_settings COMMAND dingosdk_game_settings_tests)