diff options
Diffstat (limited to 'extension/src/openvic-extension/singletons')
8 files changed, 1257 insertions, 118 deletions
diff --git a/extension/src/openvic-extension/singletons/AssetManager.cpp b/extension/src/openvic-extension/singletons/AssetManager.cpp index 083d934..6646c8b 100644 --- a/extension/src/openvic-extension/singletons/AssetManager.cpp +++ b/extension/src/openvic-extension/singletons/AssetManager.cpp @@ -13,9 +13,14 @@ using OpenVic::Utilities::godot_to_std_string; using OpenVic::Utilities::std_to_godot_string; void AssetManager::_bind_methods() { - OV_BIND_METHOD(AssetManager::get_image, { "path", "cache", "flip_y" }, DEFVAL(true), DEFVAL(false)); - OV_BIND_METHOD(AssetManager::get_texture, { "path", "flip_y" }, DEFVAL(false)); + OV_BIND_METHOD(AssetManager::get_image, { "path", "load_flags" }, DEFVAL(LOAD_FLAG_CACHE_IMAGE)); + OV_BIND_METHOD(AssetManager::get_texture, { "path", "load_flags" }, DEFVAL(LOAD_FLAG_CACHE_TEXTURE)); OV_BIND_METHOD(AssetManager::get_font, { "name" }); + + BIND_ENUM_CONSTANT(LOAD_FLAG_NONE); + BIND_ENUM_CONSTANT(LOAD_FLAG_CACHE_IMAGE); + BIND_ENUM_CONSTANT(LOAD_FLAG_CACHE_TEXTURE); + BIND_ENUM_CONSTANT(LOAD_FLAG_FLIP_Y); } AssetManager* AssetManager::get_singleton() { @@ -32,55 +37,95 @@ AssetManager::~AssetManager() { _singleton = nullptr; } -Ref<Image> AssetManager::_load_image(StringName const& path) { +Ref<Image> AssetManager::_load_image(StringName const& path, bool flip_y) { GameSingleton* game_singleton = GameSingleton::get_singleton(); ERR_FAIL_NULL_V(game_singleton, nullptr); + const String lookedup_path = std_to_godot_string(game_singleton->get_dataloader().lookup_image_file(godot_to_std_string(path)).string()); ERR_FAIL_COND_V_MSG(lookedup_path.is_empty(), nullptr, vformat("Failed to look up image: %s", path)); + const Ref<Image> image = Utilities::load_godot_image(lookedup_path); ERR_FAIL_COND_V_MSG( - image.is_null() || image->is_empty(), nullptr, vformat("Failed to load image: %s (looked up: %s)", path, lookedup_path) + image.is_null() || image->is_empty(), nullptr, + vformat("Failed to load image: %s (looked up: %s)", path, lookedup_path) ); - return image; -} -AssetManager::image_asset_t* AssetManager::_get_image_asset(StringName const& path, bool flip_y) { - image_asset_map_t::iterator it = image_assets.find(path); - if (it != image_assets.end()) { - return &it.value(); - } - const Ref<Image> image = _load_image(path); - ERR_FAIL_NULL_V(image, nullptr); if (flip_y) { image->flip_y(); } - return &image_assets.emplace(std::move(path), AssetManager::image_asset_t { image, nullptr }).first.value(); + + return image; } -Ref<Image> AssetManager::get_image(StringName const& path, bool cache, bool flip_y) { - if (cache) { - image_asset_t const* asset = _get_image_asset(path, flip_y); - ERR_FAIL_NULL_V(asset, nullptr); - return asset->image; +Ref<Image> AssetManager::get_image(StringName const& path, LoadFlags load_flags) { + /* Check for an existing image entry indicating a previous load attempt, whether successful or not. */ + const image_asset_map_t::iterator it = image_assets.find(path); + if (it != image_assets.end()) { + std::optional<Ref<Image>> const& cached_image = it->second.image; + + if (cached_image.has_value()) { + ERR_FAIL_NULL_V_MSG(*cached_image, nullptr, vformat("Failed to load image previously: %s", path)); + + return *cached_image; + } + } + + /* No load attempt has been made yet, so we try now. */ + const Ref<Image> image = _load_image(path, load_flags & LOAD_FLAG_FLIP_Y); + + if (image.is_valid()) { + if (load_flags & LOAD_FLAG_CACHE_IMAGE) { + image_assets[path].image = image; + } + + return image; } else { - return _load_image(path); + /* Mark both image and texture as failures, regardless of cache flags, in case of future load/creation attempts. */ + image_assets[path] = { nullptr, nullptr }; + + ERR_FAIL_V_MSG(nullptr, vformat("Failed to load image: %s", path)); } } -Ref<ImageTexture> AssetManager::get_texture(StringName const& path, bool flip_y) { - image_asset_t* asset = _get_image_asset(path, flip_y); - ERR_FAIL_NULL_V(asset, nullptr); - if (asset->texture.is_null()) { - asset->texture = ImageTexture::create_from_image(asset->image); - ERR_FAIL_NULL_V_MSG(asset->texture, nullptr, vformat("Failed to turn image into texture: %s", path)); +Ref<ImageTexture> AssetManager::get_texture(StringName const& path, LoadFlags load_flags) { + /* Check for an existing texture entry indicating a previous creation attempt, whether successful or not. */ + const image_asset_map_t::const_iterator it = image_assets.find(path); + if (it != image_assets.end()) { + std::optional<Ref<ImageTexture>> const& cached_texture = it->second.texture; + + if (cached_texture.has_value()) { + ERR_FAIL_NULL_V_MSG(*cached_texture, nullptr, vformat("Failed to create texture previously: %s", path)); + + return *cached_texture; + } + } + + /* No creation attempt has yet been made, so we try now starting by finding the corresponding image. */ + const Ref<Image> image = get_image(path, load_flags); + ERR_FAIL_NULL_V_MSG(image, nullptr, vformat("Failed to load image for texture: %s", path)); + + const Ref<ImageTexture> texture = ImageTexture::create_from_image(image); + + if (texture.is_valid()) { + if (load_flags & LOAD_FLAG_CACHE_TEXTURE) { + image_assets[path].texture = texture; + } + + return texture; + } else { + /* Mark texture as a failure, regardless of cache flags, in case of future creation attempts. */ + image_assets[path].texture = nullptr; + + ERR_FAIL_V_MSG(nullptr, vformat("Failed to create texture: %s", path)); } - return asset->texture; } Ref<Font> AssetManager::get_font(StringName const& name) { const font_map_t::const_iterator it = fonts.find(name); if (it != fonts.end()) { + ERR_FAIL_NULL_V_MSG(it->second, nullptr, vformat("Failed to load font previously: %s", name)); + return it->second; } @@ -89,18 +134,35 @@ Ref<Font> AssetManager::get_font(StringName const& name) { static const String image_ext = ".tga"; const StringName image_path = font_dir + name + image_ext; - const Ref<Image> image = get_image(image_path); - ERR_FAIL_NULL_V_MSG(image, nullptr, vformat("Failed to load font image %s for the font named %s", image_path, name)); + const Ref<Image> image = get_image(image_path, LOAD_FLAG_NONE); + if (image.is_null()) { + fonts.emplace(name, nullptr); + + ERR_FAIL_V_MSG(nullptr, vformat("Failed to load font image %s for the font named %s", image_path, name)); + } + GameSingleton* game_singleton = GameSingleton::get_singleton(); ERR_FAIL_NULL_V(game_singleton, nullptr); + const String font_path = font_dir + name + font_ext; const String lookedup_font_path = std_to_godot_string(game_singleton->get_dataloader().lookup_file(godot_to_std_string(font_path)).string()); + if (lookedup_font_path.is_empty()) { + fonts.emplace(name, nullptr); + + ERR_FAIL_V_MSG(nullptr, vformat("Failed to look up font: %s", font_path)); + } + const Ref<Font> font = Utilities::load_godot_font(lookedup_font_path, image); - ERR_FAIL_NULL_V_MSG( - font, nullptr, - vformat("Failed to load font file %s (looked up: %s) for the font named %s", font_path, lookedup_font_path, name) - ); - fonts.emplace(std::move(name), font); + if (font.is_null()) { + fonts.emplace(name, nullptr); + + ERR_FAIL_V_MSG( + nullptr, + vformat("Failed to load font file %s (looked up: %s) for the font named %s", font_path, lookedup_font_path, name) + ); + } + + fonts.emplace(name, font); return font; } diff --git a/extension/src/openvic-extension/singletons/AssetManager.hpp b/extension/src/openvic-extension/singletons/AssetManager.hpp index 0416e5b..0856d05 100644 --- a/extension/src/openvic-extension/singletons/AssetManager.hpp +++ b/extension/src/openvic-extension/singletons/AssetManager.hpp @@ -5,7 +5,7 @@ #include <godot_cpp/classes/image_texture.hpp> #include <godot_cpp/core/class_db.hpp> -#include <openvic-simulation/interface/GFX.hpp> +#include <openvic-simulation/interface/GFXSprite.hpp> namespace OpenVic { class AssetManager : public godot::Object { @@ -13,9 +13,22 @@ namespace OpenVic { static inline AssetManager* _singleton = nullptr; + public: + enum LoadFlags { + LOAD_FLAG_NONE = 0, + LOAD_FLAG_CACHE_IMAGE = 1 << 0, + LOAD_FLAG_CACHE_TEXTURE = 1 << 1, + LOAD_FLAG_FLIP_Y = 1 << 2 + }; + + constexpr friend LoadFlags operator|(LoadFlags lhs, LoadFlags rhs) { + return static_cast<LoadFlags>(static_cast<int>(lhs) | static_cast<int>(rhs)); + } + + private: struct image_asset_t { - godot::Ref<godot::Image> image; - godot::Ref<godot::ImageTexture> texture; + std::optional<godot::Ref<godot::Image>> image; + std::optional<godot::Ref<godot::ImageTexture>> texture; }; /* deque_ordered_map to avoid the need to reallocate. */ using image_asset_map_t = deque_ordered_map<godot::StringName, image_asset_t>; @@ -24,8 +37,7 @@ namespace OpenVic { image_asset_map_t image_assets; font_map_t fonts; - static godot::Ref<godot::Image> _load_image(godot::StringName const& path); - image_asset_t* _get_image_asset(godot::StringName const& path, bool flip_y); + static godot::Ref<godot::Image> _load_image(godot::StringName const& path, bool flip_y); protected: static void _bind_methods(); @@ -37,16 +49,27 @@ namespace OpenVic { ~AssetManager(); /* Search for and load an image at the specified path relative to the game defines, first checking the AssetManager's - * image cache (if cache is true) in case it has already been loaded, and returning nullptr if image loading fails. */ - godot::Ref<godot::Image> get_image(godot::StringName const& path, bool cache = true, bool flip_y = false); + * image cache in case it has already been loaded, and returning nullptr if image loading fails. If the cache image + * load flag is set then the loaded image will be stored in the AssetManager's image cache for future access; if the + * flip y load flag is set then the image will be flipped vertically before being returned (if the image is already + * in the cache then no flipping will occur, regardless of whether it was orginally flipped or not). */ + godot::Ref<godot::Image> get_image(godot::StringName const& path, LoadFlags load_flags = LOAD_FLAG_CACHE_IMAGE); - /* Create a texture from an image found at the specified path relative to the game defines, fist checking - * AssetManager's texture cache in case it has already been loaded, and returning nullptr if image loading - * or texture creation fails. */ - godot::Ref<godot::ImageTexture> get_texture(godot::StringName const& path, bool flip_y = false); + /* Create a texture from an image found at the specified path relative to the game defines, fist checking the + * AssetManager's texture cache in case it has already been loaded, and returning nullptr if image loading or texture + * creation fails. If the cache image load flag is set then the loaded image will be stored in the AssetManager's + * image cache for future access; if the cache texture load flag is set then the created texture will be stored in the + * AssetManager's texture cache for future access; if the flip y load flag is set then the image will be flipped + * vertically before being used to create the texture (if the image is already in the cache then no flipping will + * occur, regardless of whether it was orginally flipped or not). */ + godot::Ref<godot::ImageTexture> get_texture( + godot::StringName const& path, LoadFlags load_flags = LOAD_FLAG_CACHE_TEXTURE + ); /* Search for and load a font with the specified name from the game defines' font directory, first checking the * AssetManager's font cache in case it has already been loaded, and returning nullptr if font loading fails. */ godot::Ref<godot::Font> get_font(godot::StringName const& name); }; } + +VARIANT_ENUM_CAST(OpenVic::AssetManager::LoadFlags); diff --git a/extension/src/openvic-extension/singletons/GameSingleton.cpp b/extension/src/openvic-extension/singletons/GameSingleton.cpp index 459b2c8..838542d 100644 --- a/extension/src/openvic-extension/singletons/GameSingleton.cpp +++ b/extension/src/openvic-extension/singletons/GameSingleton.cpp @@ -9,6 +9,7 @@ #include "openvic-extension/singletons/AssetManager.hpp" #include "openvic-extension/singletons/LoadLocalisation.hpp" +#include "openvic-extension/singletons/MenuSingleton.hpp" #include "openvic-extension/utility/ClassBindings.hpp" #include "openvic-extension/utility/Utilities.hpp" @@ -50,12 +51,17 @@ void GameSingleton::_bind_methods() { OV_BIND_METHOD(GameSingleton::get_map_width); OV_BIND_METHOD(GameSingleton::get_map_height); + OV_BIND_METHOD(GameSingleton::get_map_dims); OV_BIND_METHOD(GameSingleton::get_map_aspect_ratio); OV_BIND_METHOD(GameSingleton::get_terrain_texture); + OV_BIND_METHOD(GameSingleton::get_flag_dims); + OV_BIND_METHOD(GameSingleton::get_flag_sheet_texture); OV_BIND_METHOD(GameSingleton::get_province_shape_image_subdivisions); OV_BIND_METHOD(GameSingleton::get_province_shape_texture); OV_BIND_METHOD(GameSingleton::get_province_colour_texture); + OV_BIND_METHOD(GameSingleton::get_province_names); + OV_BIND_METHOD(GameSingleton::get_mapmode_count); OV_BIND_METHOD(GameSingleton::get_mapmode_identifier); OV_BIND_METHOD(GameSingleton::set_mapmode, { "identifier" }); @@ -115,6 +121,7 @@ Error GameSingleton::setup_game(int32_t bookmark_index) { Bookmark const* bookmark = game_manager.get_history_manager().get_bookmark_manager().get_bookmark_by_index(bookmark_index); ERR_FAIL_NULL_V_MSG(bookmark, FAILED, vformat("Failed to get bookmark with index: %d", bookmark_index)); bool ret = game_manager.load_bookmark(bookmark); + for (Province& province : game_manager.get_map().get_provinces()) { province.set_crime( game_manager.get_crime_manager().get_crime_modifier_by_index( @@ -122,13 +129,17 @@ Error GameSingleton::setup_game(int32_t bookmark_index) { ) ); } + + MenuSingleton* menu_singleton = MenuSingleton::get_singleton(); + ERR_FAIL_NULL_V(menu_singleton, FAILED); + menu_singleton->_population_menu_update_provinces(); + return ERR(ret); } int32_t GameSingleton::get_province_index_from_uv_coords(Vector2 const& coords) const { - const size_t x_mod_w = UtilityFunctions::fposmod(coords.x, 1.0f) * get_map_width(); - const size_t y_mod_h = UtilityFunctions::fposmod(coords.y, 1.0f) * get_map_height(); - return game_manager.get_map().get_province_index_at(x_mod_w, y_mod_h); + const Vector2 pos = coords.posmod(1.0f) * get_map_dims(); + return game_manager.get_map().get_province_index_at(Utilities::from_godot_ivec2(pos)); } int32_t GameSingleton::get_map_width() const { @@ -139,6 +150,10 @@ int32_t GameSingleton::get_map_height() const { return game_manager.get_map().get_height(); } +Vector2i GameSingleton::get_map_dims() const { + return Utilities::to_godot_ivec2(game_manager.get_map().get_dims()); +} + float GameSingleton::get_map_aspect_ratio() const { return static_cast<float>(get_map_width()) / static_cast<float>(get_map_height()); } @@ -147,19 +162,36 @@ Ref<Texture2DArray> GameSingleton::get_terrain_texture() const { return terrain_texture; } -Ref<Image> GameSingleton::get_flag_image(Country const* country, StringName const& flag_type) const { - ERR_FAIL_NULL_V(country, nullptr); - const typename decltype(flag_image_map)::const_iterator it = flag_image_map.find(country); +Ref<Image> GameSingleton::get_flag_sheet_image() const { + return flag_sheet_image; +} + +Ref<ImageTexture> GameSingleton::get_flag_sheet_texture() const { + return flag_sheet_texture; +} + +int32_t GameSingleton::get_flag_sheet_index(int32_t country_index, godot::StringName const& flag_type) const { ERR_FAIL_COND_V_MSG( - it == flag_image_map.end(), nullptr, - vformat("Failed to find flags for country: %s", std_view_to_godot_string(country->get_identifier())) + country_index < 0 || country_index >= game_manager.get_country_manager().get_country_count(), -1, + vformat("Invalid country index: %d", country_index) ); - const typename decltype(it->second)::const_iterator it2 = it->second.find(flag_type); + + const typename decltype(flag_type_index_map)::const_iterator it = flag_type_index_map.find(flag_type); + ERR_FAIL_COND_V_MSG(it == flag_type_index_map.end(), -1, vformat("Invalid flag type %s", flag_type)); + + return flag_type_index_map.size() * country_index + it->second; +} + +Rect2i GameSingleton::get_flag_sheet_rect(int32_t flag_index) const { ERR_FAIL_COND_V_MSG( - it2 == it->second.end(), nullptr, - vformat("Failed to find %s flag for country: %s", flag_type, std_view_to_godot_string(country->get_identifier())) + flag_index < 0 || flag_index >= flag_sheet_count, {}, vformat("Invalid flag sheet index: %d", flag_index) ); - return it2->second; + + return { Vector2i { flag_index % flag_sheet_dims.x, flag_index / flag_sheet_dims.x } * flag_dims, flag_dims }; +} + +Rect2i GameSingleton::get_flag_sheet_rect(int32_t country_index, godot::StringName const& flag_type) const { + return get_flag_sheet_rect(get_flag_sheet_index(country_index, flag_type)); } Vector2i GameSingleton::get_province_shape_image_subdivisions() const { @@ -185,11 +217,11 @@ Error GameSingleton::_update_colour_image() { static constexpr int32_t colour_image_width = PROVINCE_INDEX_SQRT * sizeof(Mapmode::base_stripe_t) / sizeof(colour_argb_t); /* Province count + null province, rounded up to next multiple of PROVINCE_INDEX_SQRT. * Rearranged from: (map.get_province_count() + 1) + (PROVINCE_INDEX_SQRT - 1) */ - static const int32_t colour_image_height = (map.get_province_count() + PROVINCE_INDEX_SQRT) / PROVINCE_INDEX_SQRT; + const int32_t colour_image_height = (map.get_province_count() + PROVINCE_INDEX_SQRT) / PROVINCE_INDEX_SQRT; static PackedByteArray colour_data_array; - static const int64_t colour_data_array_size = colour_image_width * colour_image_height * sizeof(colour_argb_t); - colour_data_array.resize(colour_data_array_size); + const int64_t colour_data_array_size = colour_image_width * colour_image_height * sizeof(colour_argb_t); + ERR_FAIL_COND_V(colour_data_array.resize(colour_data_array_size) != OK, FAILED); Error err = OK; if (!map.generate_mapmode_colours(mapmode_index, colour_data_array.ptrw())) { @@ -213,6 +245,39 @@ Error GameSingleton::_update_colour_image() { return err; } +TypedArray<Dictionary> GameSingleton::get_province_names() const { + static const StringName identifier_key = "identifier"; + static const StringName position_key = "position"; + static const StringName rotation_key = "rotation"; + static const StringName scale_key = "scale"; + + TypedArray<Dictionary> ret; + ERR_FAIL_COND_V(ret.resize(game_manager.get_map().get_province_count()) != OK, {}); + + for (int32_t index = 0; index < game_manager.get_map().get_province_count(); ++index) { + Province const& province = game_manager.get_map().get_provinces()[index]; + + Dictionary province_dict; + + province_dict[identifier_key] = std_view_to_godot_string(province.get_identifier()); + province_dict[position_key] = Utilities::to_godot_fvec2(province.get_text_position()) / get_map_dims(); + + const float rotation = province.get_text_rotation().to_float(); + if (rotation != 0.0f) { + province_dict[rotation_key] = rotation; + } + + const float scale = province.get_text_scale().to_float(); + if (scale != 1.0f) { + province_dict[scale_key] = scale; + } + + ret[index] = std::move(province_dict); + } + + return ret; +} + int32_t GameSingleton::get_mapmode_count() const { return game_manager.get_map().get_mapmode_count(); } @@ -275,19 +340,25 @@ Error GameSingleton::_load_map_images() { } Map::shape_pixel_t const* province_shape_data = game_manager.get_map().get_province_shape_image().data(); + const Vector2i divided_dims = province_dims / image_subdivisions; + const int64_t subdivision_width = divided_dims.x * sizeof(Map::shape_pixel_t); + const int64_t subdivision_size = subdivision_width * divided_dims.y; + TypedArray<Image> province_shape_images; - province_shape_images.resize(image_subdivisions.x * image_subdivisions.y); + ERR_FAIL_COND_V(province_shape_images.resize(image_subdivisions.x * image_subdivisions.y) != OK, FAILED); + + PackedByteArray index_data_array; + ERR_FAIL_COND_V(index_data_array.resize(subdivision_size) != OK, FAILED); + for (int32_t v = 0; v < image_subdivisions.y; ++v) { for (int32_t u = 0; u < image_subdivisions.x; ++u) { - PackedByteArray index_data_array; - index_data_array.resize(divided_dims.x * divided_dims.y * sizeof(Map::shape_pixel_t)); for (int32_t y = 0; y < divided_dims.y; ++y) { memcpy( - index_data_array.ptrw() + y * divided_dims.x * sizeof(Map::shape_pixel_t), + index_data_array.ptrw() + y * subdivision_width, province_shape_data + (v * divided_dims.y + y) * province_dims.x + u * divided_dims.x, - divided_dims.x * sizeof(Map::shape_pixel_t) + subdivision_width ); } @@ -322,7 +393,7 @@ Error GameSingleton::_load_terrain_variants() { AssetManager* asset_manager = AssetManager::get_singleton(); ERR_FAIL_NULL_V(asset_manager, FAILED); // Load the terrain texture sheet and prepare to slice it up - Ref<Image> terrain_sheet = asset_manager->get_image(terrain_texturesheet_path); + Ref<Image> terrain_sheet = asset_manager->get_image(terrain_texturesheet_path, AssetManager::LOAD_FLAG_NONE); ERR_FAIL_NULL_V_MSG(terrain_sheet, FAILED, vformat("Failed to load terrain texture sheet: %s", terrain_texturesheet_path)); static constexpr int32_t SHEET_DIMS = 8, SHEET_SIZE = SHEET_DIMS * SHEET_DIMS; @@ -337,6 +408,7 @@ Error GameSingleton::_load_terrain_variants() { const int32_t slice_size = sheet_width / SHEET_DIMS; TypedArray<Image> terrain_images; + ERR_FAIL_COND_V(terrain_images.resize(SHEET_SIZE + 1) != OK, FAILED); { /* This is a placeholder image so that we don't have to branch to avoid looking up terrain index 0 (water). * It should never appear in game, and so is bright red to to make it obvious if it slips through. */ @@ -344,72 +416,136 @@ Error GameSingleton::_load_terrain_variants() { { 1.0f, 0.0f, 0.0f }, slice_size, slice_size, terrain_sheet->get_format() ); ERR_FAIL_NULL_V_EDMSG(water_image, FAILED, "Failed to create water terrain image"); - terrain_images.append(water_image); + terrain_images[0] = water_image; } - Error err = OK; + for (int32_t idx = 0; idx < SHEET_SIZE; ++idx) { const Rect2i slice { idx % SHEET_DIMS * slice_size, idx / SHEET_DIMS * slice_size, slice_size, slice_size }; const Ref<Image> terrain_image = terrain_sheet->get_region(slice); - if (terrain_image.is_null() || terrain_image->is_empty()) { - UtilityFunctions::push_error( - "Failed to extract terrain texture slice ", slice, " from ", terrain_texturesheet_path - ); - err = FAILED; - } - terrain_images.append(terrain_image); + + ERR_FAIL_COND_V_MSG(terrain_image.is_null() || terrain_image->is_empty(), FAILED, vformat( + "Failed to extract terrain texture slice %s from %s", slice, terrain_texturesheet_path + )); + + terrain_images[idx + 1] = terrain_image; } terrain_texture.instantiate(); ERR_FAIL_COND_V_MSG( terrain_texture->create_from_images(terrain_images) != OK, FAILED, "Failed to create terrain texture array!" ); - return err; + return OK; } -Error GameSingleton::_load_flag_images() { - ERR_FAIL_COND_V_MSG(!flag_image_map.empty(), FAILED, "Flag images have already been loaded!"); +const Vector2i GameSingleton::flag_dims { 128, 64 }; + +Error GameSingleton::_load_flag_sheet() { + ERR_FAIL_COND_V_MSG( + flag_sheet_image.is_valid() || flag_sheet_texture.is_valid(), FAILED, + "Flag sheet image and/or texture has already been generated!" + ); GovernmentTypeManager const& government_type_manager = game_manager.get_politics_manager().get_government_type_manager(); ERR_FAIL_COND_V_MSG( - !government_type_manager.government_types_are_locked(), FAILED, - "Cannot load flag images before government types are locked!" + government_type_manager.get_flag_types().empty() || !government_type_manager.government_types_are_locked(), FAILED, + "Cannot load flag images if flag types are empty or government types are not locked!" ); CountryManager const& country_manager = game_manager.get_country_manager(); ERR_FAIL_COND_V_MSG( - !country_manager.countries_are_locked(), FAILED, "Cannot load flag images before countries are locked!" + country_manager.countries_empty() || !country_manager.countries_are_locked(), FAILED, + "Cannot load flag images if countries are empty or not locked!" ); AssetManager* asset_manager = AssetManager::get_singleton(); ERR_FAIL_NULL_V(asset_manager, FAILED); - static const String flag_directory = "gfx/flags/"; - static const String flag_separator = "_"; - static const String flag_extension = ".tga"; - - std::vector<StringName> flag_types; + /* Generate flag type - index lookup map */ + flag_type_index_map.clear(); for (std::string const& type : government_type_manager.get_flag_types()) { - flag_types.emplace_back(std_to_godot_string_name(type)); + flag_type_index_map.emplace(std_to_godot_string_name(type), static_cast<int32_t>(flag_type_index_map.size())); } - flag_image_map.reserve(country_manager.get_countries().size()); + flag_sheet_count = country_manager.get_countries().size() * flag_type_index_map.size(); + + std::vector<Ref<Image>> flag_images; + flag_images.reserve(flag_sheet_count); + + static constexpr Image::Format flag_format = Image::FORMAT_RGB8; Error ret = OK; for (Country const& country : country_manager.get_countries()) { - ordered_map<StringName, Ref<Image>>& flag_images = flag_image_map[&country]; - flag_images.reserve(flag_types.size()); const String country_name = std_view_to_godot_string(country.get_identifier()); - for (StringName const& flag_type : flag_types) { + + for (auto const& [flag_type, flag_type_index] : flag_type_index_map) { + static const String flag_directory = "gfx/flags/"; + static const String flag_separator = "_"; + static const String flag_extension = ".tga"; + const StringName flag_path = flag_directory + country_name + (flag_type.is_empty() ? "" : flag_separator + flag_type) + flag_extension; - const Ref<Image> flag_image = asset_manager->get_image(flag_path); + + /* Do not cache flag image, they should be freed after the flag sheet has been generated. */ + const Ref<Image> flag_image = asset_manager->get_image(flag_path, AssetManager::LOAD_FLAG_NONE); + if (flag_image.is_valid()) { - flag_images.emplace(flag_type, flag_image); + + if (flag_image->get_format() != flag_format) { + flag_image->convert(flag_format); + } + + if (flag_image->get_size() != flag_dims) { + if (flag_image->get_width() > flag_dims.x || flag_image->get_height() > flag_dims.y) { + UtilityFunctions::push_warning( + "Flag image ", flag_path, " (", flag_image->get_size(), ") is larger than the sheet flag size (", + flag_dims, ")" + ); + } + + flag_image->resize(flag_dims.x, flag_dims.y, Image::INTERPOLATE_NEAREST); + } } else { UtilityFunctions::push_error("Failed to load flag image: ", flag_path); ret = FAILED; } + + /* Add flag_image to the vector even if it's null to ensure each flag has the right index. */ + flag_images.push_back(flag_image); } } + + ERR_FAIL_COND_V(flag_images.size() != flag_sheet_count, FAILED); + + /* Calculate the width that will make the sheet as close to a square as possible (taking flag dimensions into account.) */ + flag_sheet_dims.x = (fixed_point_t { static_cast<int32_t>(flag_images.size()) } * flag_dims.y / flag_dims.x).sqrt().ceil(); + + /* Calculated corresponding height (rounded up). */ + flag_sheet_dims.y = (static_cast<int32_t>(flag_images.size()) + flag_sheet_dims.x - 1 ) / flag_sheet_dims.x; + + const Vector2i sheet_dims = flag_sheet_dims * flag_dims; + + flag_sheet_image = Image::create(sheet_dims.x, sheet_dims.y, false, flag_format); + ERR_FAIL_NULL_V_MSG(flag_sheet_image, FAILED, "Failed to create flag sheet image!"); + + static const Rect2i flag_rect { { 0, 0 }, flag_dims }; + + /* Fill the flag sheet with the flag images. */ + for (int32_t index = 0; index < flag_images.size(); ++index) { + Ref<Image> const& flag_image = flag_images[index]; + + const Vector2i sheet_pos = Vector2i { index % flag_sheet_dims.x, index / flag_sheet_dims.x } * flag_dims; + + if (flag_image.is_valid()) { + flag_sheet_image->blit_rect(flag_image, flag_rect, sheet_pos); + } else { + static const Color error_colour { 1.0f, 0.0f, 1.0f, 1.0f }; /* Magenta */ + + flag_sheet_image->fill_rect({ sheet_pos, flag_dims }, error_colour); + } + } + + flag_sheet_texture = ImageTexture::create_from_image(flag_sheet_image); + ERR_FAIL_NULL_V_MSG(flag_sheet_texture, FAILED, "Failed to create flag sheet texture!"); + return ret; } @@ -433,8 +569,8 @@ Error GameSingleton::load_defines_compatibility_mode(PackedStringArray const& fi UtilityFunctions::push_error("Failed to load terrain variants!"); err = FAILED; } - if (_load_flag_images() != OK) { - UtilityFunctions::push_error("Failed to load flag textures!"); + if (_load_flag_sheet() != OK) { + UtilityFunctions::push_error("Failed to load flag sheet!"); err = FAILED; } if (_load_map_images() != OK) { diff --git a/extension/src/openvic-extension/singletons/GameSingleton.hpp b/extension/src/openvic-extension/singletons/GameSingleton.hpp index e84e366..f2b88ac 100644 --- a/extension/src/openvic-extension/singletons/GameSingleton.hpp +++ b/extension/src/openvic-extension/singletons/GameSingleton.hpp @@ -22,7 +22,13 @@ namespace OpenVic { godot::Ref<godot::ImageTexture> province_colour_texture; Mapmode::index_t mapmode_index = 0; godot::Ref<godot::Texture2DArray> terrain_texture; - ordered_map<Country const*, ordered_map<godot::StringName, godot::Ref<godot::Image>>> flag_image_map; + + static const godot::Vector2i PROPERTY(flag_dims); /* The size in pixels of an individual flag. */ + int32_t flag_sheet_count = 0; /* The number of flags in the flag sheet. */ + godot::Vector2i flag_sheet_dims; /* The size of the flag sheet in flags, rather than pixels. */ + godot::Ref<godot::Image> flag_sheet_image; + godot::Ref<godot::ImageTexture> flag_sheet_texture; + ordered_map<godot::StringName, int32_t> flag_type_index_map; static godot::StringName const& _signal_gamestate_updated(); static godot::StringName const& _signal_province_selected(); @@ -30,7 +36,7 @@ namespace OpenVic { godot::Error _load_map_images(); godot::Error _load_terrain_variants(); - godot::Error _load_flag_images(); + godot::Error _load_flag_sheet(); /* Generate the province_colour_texture from the current mapmode. */ godot::Error _update_colour_image(); @@ -61,14 +67,20 @@ namespace OpenVic { int32_t get_map_width() const; int32_t get_map_height() const; + godot::Vector2i get_map_dims() const; float get_map_aspect_ratio() const; /* The cosmetic terrain textures stored in a Texture2DArray. */ godot::Ref<godot::Texture2DArray> get_terrain_texture() const; - /* The flag image corresponding to the requested country / flag_type - * combination, or nullptr if no such flag can be found. */ - godot::Ref<godot::Image> get_flag_image(Country const* country, godot::StringName const& flag_type) const; + godot::Ref<godot::Image> get_flag_sheet_image() const; + godot::Ref<godot::ImageTexture> get_flag_sheet_texture() const; + + /* The index of the flag in the flag sheet corresponding to the requested country / flag_type + * combination, or -1 if no such flag can be found. */ + int32_t get_flag_sheet_index(int32_t country_index, godot::StringName const& flag_type) const; + godot::Rect2i get_flag_sheet_rect(int32_t flag_index) const; + godot::Rect2i get_flag_sheet_rect(int32_t country_index, godot::StringName const& flag_type) const; /* Number of (vertical, horizontal) subdivisions the province shape image * was split into when making the province_shape_texture to ensure no @@ -83,6 +95,8 @@ namespace OpenVic { /* The base and stripe colours for each province. */ godot::Ref<godot::ImageTexture> get_province_colour_texture() const; + godot::TypedArray<godot::Dictionary> get_province_names() const; + int32_t get_mapmode_count() const; godot::String get_mapmode_identifier(int32_t index) const; godot::Error set_mapmode(godot::String const& identifier); diff --git a/extension/src/openvic-extension/singletons/LoadLocalisation.cpp b/extension/src/openvic-extension/singletons/LoadLocalisation.cpp index 8860105..16ebe57 100644 --- a/extension/src/openvic-extension/singletons/LoadLocalisation.cpp +++ b/extension/src/openvic-extension/singletons/LoadLocalisation.cpp @@ -111,7 +111,7 @@ Error LoadLocalisation::load_localisation_dir(String const& dir_path) const { ERR_FAIL_COND_V_MSG( !DirAccess::dir_exists_absolute(dir_path), FAILED, vformat("Localisation directory does not exist: %s", dir_path) ); - PackedStringArray const dirs = DirAccess::get_directories_at(dir_path); + const PackedStringArray dirs = DirAccess::get_directories_at(dir_path); ERR_FAIL_COND_V_MSG( dirs.size() < 1, FAILED, vformat("Localisation directory does not contain any sub-directories: %s", dir_path) ); diff --git a/extension/src/openvic-extension/singletons/MenuSingleton.cpp b/extension/src/openvic-extension/singletons/MenuSingleton.cpp index 52fb6af..993549c 100644 --- a/extension/src/openvic-extension/singletons/MenuSingleton.cpp +++ b/extension/src/openvic-extension/singletons/MenuSingleton.cpp @@ -15,6 +15,19 @@ using namespace OpenVic; using OpenVic::Utilities::std_to_godot_string; using OpenVic::Utilities::std_view_to_godot_string; +StringName const& MenuSingleton::_signal_population_menu_province_list_changed() { + static const StringName signal_population_menu_province_list_changed = "population_menu_province_list_changed"; + return signal_population_menu_province_list_changed; +} +StringName const& MenuSingleton::_signal_population_menu_province_list_selected_changed() { + static const StringName signal_population_menu_province_list_selected_changed = "population_menu_province_list_selected_changed"; + return signal_population_menu_province_list_selected_changed; +} +StringName const& MenuSingleton::_signal_population_menu_pops_changed() { + static const StringName signal_population_menu_pops_changed = "population_menu_pops_changed"; + return signal_population_menu_pops_changed; +} + void MenuSingleton::_bind_methods() { /* PROVINCE OVERVIEW PANEL */ OV_BIND_METHOD(MenuSingleton::get_province_info_from_index, { "index" }); @@ -35,6 +48,60 @@ void MenuSingleton::_bind_methods() { OV_BIND_METHOD(MenuSingleton::can_increase_speed); OV_BIND_METHOD(MenuSingleton::can_decrease_speed); OV_BIND_METHOD(MenuSingleton::get_longform_date); + + /* POPULATION MENU */ + OV_BIND_METHOD(MenuSingleton::get_population_menu_province_list_row_count); + OV_BIND_METHOD(MenuSingleton::get_population_menu_province_list_rows, { "start", "count" }); + OV_BIND_METHOD( + MenuSingleton::population_menu_select_province_list_entry, { "select_index", "set_scroll_index" }, DEFVAL(false) + ); + OV_BIND_METHOD(MenuSingleton::population_menu_select_province, { "province_index" }); + OV_BIND_METHOD(MenuSingleton::population_menu_toggle_expanded, { "toggle_index", "emit_selected_changed" }, DEFVAL(true)); + + OV_BIND_METHOD(MenuSingleton::population_menu_select_sort_key, { "sort_key" }); + OV_BIND_METHOD(MenuSingleton::get_population_menu_pop_rows, { "start", "count" }); + OV_BIND_METHOD(MenuSingleton::get_population_menu_pop_row_count); + + OV_BIND_METHOD(MenuSingleton::get_population_menu_pop_filter_setup_info); + OV_BIND_METHOD(MenuSingleton::get_population_menu_pop_filter_info); + OV_BIND_METHOD(MenuSingleton::population_menu_toggle_pop_filter, { "filter_index" }); + OV_BIND_METHOD(MenuSingleton::population_menu_select_all_pop_filters); + OV_BIND_METHOD(MenuSingleton::population_menu_deselect_all_pop_filters); + + OV_BIND_METHOD(MenuSingleton::get_population_menu_distribution_setup_info); + OV_BIND_METHOD(MenuSingleton::get_population_menu_distribution_info); + + ADD_SIGNAL(MethodInfo(_signal_population_menu_province_list_changed())); + ADD_SIGNAL( + MethodInfo(_signal_population_menu_province_list_selected_changed(), PropertyInfo(Variant::INT, "scroll_index")) + ); + ADD_SIGNAL(MethodInfo(_signal_population_menu_pops_changed())); + + using enum population_menu_t::ProvinceListEntry; + BIND_ENUM_CONSTANT(LIST_ENTRY_NONE); + BIND_ENUM_CONSTANT(LIST_ENTRY_COUNTRY); + BIND_ENUM_CONSTANT(LIST_ENTRY_STATE); + BIND_ENUM_CONSTANT(LIST_ENTRY_PROVINCE); + + using enum population_menu_t::PopSortKey; + BIND_ENUM_CONSTANT(NONE); + BIND_ENUM_CONSTANT(SORT_SIZE); + BIND_ENUM_CONSTANT(SORT_TYPE); + BIND_ENUM_CONSTANT(SORT_CULTURE); + BIND_ENUM_CONSTANT(SORT_RELIGION); + BIND_ENUM_CONSTANT(SORT_LOCATION); + BIND_ENUM_CONSTANT(SORT_MILITANCY); + BIND_ENUM_CONSTANT(SORT_CONSCIOUSNESS); + BIND_ENUM_CONSTANT(SORT_IDEOLOGY); + BIND_ENUM_CONSTANT(SORT_ISSUES); + BIND_ENUM_CONSTANT(SORT_UNEMPLOYMENT); + BIND_ENUM_CONSTANT(SORT_CASH); + BIND_ENUM_CONSTANT(SORT_LIFE_NEEDS); + BIND_ENUM_CONSTANT(SORT_EVERYDAY_NEEDS); + BIND_ENUM_CONSTANT(SORT_LUXURY_NEEDS); + BIND_ENUM_CONSTANT(SORT_REBEL_FACTION); + BIND_ENUM_CONSTANT(SORT_SIZE_CHANGE); + BIND_ENUM_CONSTANT(SORT_LITERACY); } MenuSingleton* MenuSingleton::get_singleton() { @@ -143,11 +210,17 @@ Dictionary MenuSingleton::get_province_info_from_index(int32_t index) const { std::vector<Country const*> const& cores = province->get_cores(); if (!cores.empty()) { PackedStringArray cores_array; - cores_array.resize(cores.size()); - for (size_t idx = 0; idx < cores.size(); ++idx) { - cores_array[idx] = std_view_to_godot_string(cores[idx]->get_identifier()); + if (cores_array.resize(cores.size()) == OK) { + for (size_t idx = 0; idx < cores.size(); ++idx) { + cores_array[idx] = std_view_to_godot_string(cores[idx]->get_identifier()); + } + ret[province_info_cores_key] = std::move(cores_array); + } else { + UtilityFunctions::push_error( + "Failed to resize cores array to the correct size (", static_cast<int64_t>(cores.size()), ") for province ", + std_view_to_godot_string(province->get_identifier()) + ); } - ret[province_info_cores_key] = cores_array; } static const StringName building_info_level_key = "level"; @@ -161,20 +234,26 @@ Dictionary MenuSingleton::get_province_info_from_index(int32_t index) const { /* This system relies on the province buildings all being present in the right order. It will have to * be changed if we want to support variable combinations and permutations of province buildings. */ TypedArray<Dictionary> buildings_array; - buildings_array.resize(buildings.size()); - for (size_t idx = 0; idx < buildings.size(); ++idx) { - BuildingInstance const& building = buildings[idx]; - - Dictionary building_dict; - building_dict[building_info_level_key] = static_cast<int32_t>(building.get_level()); - building_dict[building_info_expansion_state_key] = static_cast<int32_t>(building.get_expansion_state()); - building_dict[building_info_start_date_key] = std_to_godot_string(building.get_start_date().to_string()); - building_dict[building_info_end_date_key] = std_to_godot_string(building.get_end_date().to_string()); - building_dict[building_info_expansion_progress_key] = building.get_expansion_progress(); - - buildings_array[idx] = building_dict; + if (buildings_array.resize(buildings.size()) == OK) { + for (size_t idx = 0; idx < buildings.size(); ++idx) { + BuildingInstance const& building = buildings[idx]; + + Dictionary building_dict; + building_dict[building_info_level_key] = static_cast<int32_t>(building.get_level()); + building_dict[building_info_expansion_state_key] = static_cast<int32_t>(building.get_expansion_state()); + building_dict[building_info_start_date_key] = std_to_godot_string(building.get_start_date().to_string()); + building_dict[building_info_end_date_key] = std_to_godot_string(building.get_end_date().to_string()); + building_dict[building_info_expansion_progress_key] = building.get_expansion_progress(); + + buildings_array[idx] = std::move(building_dict); + } + ret[province_info_buildings_key] = std::move(buildings_array); + } else { + UtilityFunctions::push_error( + "Failed to resize buildings array to the correct size (", static_cast<int64_t>(buildings.size()), + ") for province ", std_view_to_godot_string(province->get_identifier()) + ); } - ret[province_info_buildings_key] = buildings_array; } return ret; } diff --git a/extension/src/openvic-extension/singletons/MenuSingleton.hpp b/extension/src/openvic-extension/singletons/MenuSingleton.hpp index 6bd90c5..fd1b6c5 100644 --- a/extension/src/openvic-extension/singletons/MenuSingleton.hpp +++ b/extension/src/openvic-extension/singletons/MenuSingleton.hpp @@ -2,10 +2,12 @@ #include <godot_cpp/classes/image.hpp> +#include <openvic-simulation/pop/Pop.hpp> #include <openvic-simulation/types/OrderedContainers.hpp> namespace OpenVic { struct GameManager; + struct Region; class MenuSingleton : public godot::Object { GDCLASS(MenuSingleton, godot::Object) @@ -14,6 +16,70 @@ namespace OpenVic { GameManager* game_manager; + public: + struct population_menu_t { + enum ProvinceListEntry { + LIST_ENTRY_NONE, LIST_ENTRY_COUNTRY, LIST_ENTRY_STATE, LIST_ENTRY_PROVINCE + }; + + struct country_entry_t { + Country const& country; + bool selected = true; + }; + + struct state_entry_t { + // TODO - change to State + Region const& state; + bool selected = true, expanded = false; + }; + + struct province_entry_t { + Province const& province; + bool selected = true; + }; + + using province_list_entry_t = std::variant<country_entry_t, state_entry_t, province_entry_t>; + + std::vector<province_list_entry_t> province_list_entries; + int32_t visible_province_list_entries = 0; + + struct pop_filter_t { + Pop::pop_size_t count, promotion_demotion_change; + bool selected; + }; + ordered_map<PopType const*, pop_filter_t> pop_filters; + + static constexpr int32_t DISTRIBUTION_COUNT = 6; + /* Distributions: + * - Workforce (PopType) + * - Religion + * - Ideology + * - Nationality (Culture) + * - Issues + * - Vote */ + std::array<fixed_point_map_t<HasIdentifierAndColour const*>, DISTRIBUTION_COUNT> distributions; + + enum PopSortKey { + NONE, SORT_SIZE, SORT_TYPE, SORT_CULTURE, SORT_RELIGION, SORT_LOCATION, SORT_MILITANCY, SORT_CONSCIOUSNESS, + SORT_IDEOLOGY, SORT_ISSUES, SORT_UNEMPLOYMENT, SORT_CASH, SORT_LIFE_NEEDS, SORT_EVERYDAY_NEEDS, + SORT_LUXURY_NEEDS, SORT_REBEL_FACTION, SORT_SIZE_CHANGE, SORT_LITERACY, MAX_SORT_KEY + } sort_key = NONE; + bool sort_descending = true; + + std::vector<Pop const*> pops, filtered_pops; + }; + + private: + population_menu_t population_menu; + + /* Emitted when the number of visible province list rows changes (list generated or state entry expanded).*/ + static godot::StringName const& _signal_population_menu_province_list_changed(); + /* Emitted when the state of visible province list rows changes (selection changes). Provides an integer argument + * which, if not negative, the province list scroll index should be updated to. */ + static godot::StringName const& _signal_population_menu_province_list_selected_changed(); + /* Emitted when the selected/filtered collection of pops changes. */ + static godot::StringName const& _signal_population_menu_pops_changed(); + protected: static void _bind_methods(); @@ -44,5 +110,35 @@ namespace OpenVic { bool can_increase_speed() const; bool can_decrease_speed() const; godot::String get_longform_date() const; + + /* POPULATION MENU */ + void _population_menu_update_provinces(); + int32_t get_population_menu_province_list_row_count() const; + godot::TypedArray<godot::Dictionary> get_population_menu_province_list_rows(int32_t start, int32_t count) const; + godot::Error population_menu_select_province_list_entry(int32_t select_index, bool set_scroll_index = false); + godot::Error population_menu_select_province(int32_t province_index); + godot::Error population_menu_toggle_expanded(int32_t toggle_index, bool emit_selected_changed = true); + + void _population_menu_update_pops(); + void _population_menu_update_filtered_pops(); + using sort_func_t = std::function<bool(Pop const*, Pop const*)>; + sort_func_t _get_population_menu_sort_func(population_menu_t::PopSortKey sort_key) const; + void _population_menu_sort_pops(); + godot::Error population_menu_select_sort_key(population_menu_t::PopSortKey sort_key); + godot::TypedArray<godot::Dictionary> get_population_menu_pop_rows(int32_t start, int32_t count) const; + int32_t get_population_menu_pop_row_count() const; + + godot::PackedInt32Array get_population_menu_pop_filter_setup_info(); + godot::TypedArray<godot::Dictionary> get_population_menu_pop_filter_info() const; + godot::Error population_menu_toggle_pop_filter(int32_t filter_index); + void population_menu_select_all_pop_filters(); + void population_menu_deselect_all_pop_filters(); + + godot::PackedStringArray get_population_menu_distribution_setup_info() const; + /* Array of GFXPieChartTexture::godot_pie_chart_data_t. */ + godot::TypedArray<godot::Array> get_population_menu_distribution_info() const; }; } + +VARIANT_ENUM_CAST(OpenVic::MenuSingleton::population_menu_t::ProvinceListEntry); +VARIANT_ENUM_CAST(OpenVic::MenuSingleton::population_menu_t::PopSortKey); diff --git a/extension/src/openvic-extension/singletons/PopulationMenu.cpp b/extension/src/openvic-extension/singletons/PopulationMenu.cpp new file mode 100644 index 0000000..a598ceb --- /dev/null +++ b/extension/src/openvic-extension/singletons/PopulationMenu.cpp @@ -0,0 +1,729 @@ +#include "MenuSingleton.hpp" + +#include <godot_cpp/variant/utility_functions.hpp> + +#include <openvic-simulation/GameManager.hpp> + +#include "openvic-extension/classes/GFXPieChartTexture.hpp" +#include "openvic-extension/utility/Utilities.hpp" + +using namespace godot; +using namespace OpenVic; + +using OpenVic::Utilities::std_view_to_godot_string; + +/* POPULATION MENU */ + +void MenuSingleton::_population_menu_update_provinces() { + ERR_FAIL_NULL(game_manager); + + population_menu.province_list_entries.clear(); + population_menu.visible_province_list_entries = 0; + + for (Country const* country : { + // Example country + game_manager->get_country_manager().get_country_by_identifier("ENG") + }) { + ERR_CONTINUE(country == nullptr); + + population_menu.province_list_entries.emplace_back(population_menu_t::country_entry_t { *country }); + population_menu.visible_province_list_entries++; + + // TODO - change to State + for (Region const& state : game_manager->get_map().get_regions()) { + + population_menu.province_list_entries.emplace_back(population_menu_t::state_entry_t { state }); + population_menu.visible_province_list_entries++; + + for (Province const* province : state.get_provinces()) { + population_menu.province_list_entries.emplace_back(population_menu_t::province_entry_t { *province }); + } + } + } + + population_menu.sort_key = population_menu_t::NONE; + + emit_signal(_signal_population_menu_province_list_changed()); + + // TODO - may need to emit population_menu_province_list_selected_changed if _update_info cannot be guaranteed + + _population_menu_update_pops(); +} + +int32_t MenuSingleton::get_population_menu_province_list_row_count() const { + return population_menu.visible_province_list_entries; +} + +TypedArray<Dictionary> MenuSingleton::get_population_menu_province_list_rows(int32_t start, int32_t count) const { + // TODO - remove when country population is used instead of total map population + ERR_FAIL_NULL_V(game_manager, {}); + + if (population_menu.province_list_entries.empty()) { + return {}; + } + + ERR_FAIL_INDEX_V_MSG( + start, population_menu.visible_province_list_entries, {}, + vformat("Invalid start for population menu province list rows: %d", start) + ); + ERR_FAIL_COND_V_MSG(count <= 0, {}, vformat("Invalid count for population menu province list rows: %d", count)); + + static const StringName type_key = "type"; + static const StringName index_key = "index"; + static const StringName name_key = "name"; + static const StringName size_key = "size"; + static const StringName change_key = "change"; + static const StringName selected_key = "selected"; + /* State-only keys */ + static const StringName expanded_key = "expanded"; + static const StringName colonial_status_key = "colony"; + // TODO - national focus + + struct entry_visitor_t { + + int32_t& start_counter; + int32_t& count_counter; + + // TODO - remove when country population is used instead of total map population + const Pop::pop_size_t total_map_population; + + /* This is the index among all entries, not just visible ones unlike start and count. */ + int32_t index = 0; + + bool is_expanded = true; + + TypedArray<Dictionary> array {}; + + /* Overloads return false if count_counter reaches 0 and the function should return, + * otherwise true indicating the province list loop should continue. */ + + bool operator()(population_menu_t::country_entry_t const& country_entry) { + if (start_counter-- <= 0) { + Dictionary country_dict; + + country_dict[type_key] = population_menu_t::LIST_ENTRY_COUNTRY; + country_dict[index_key] = index; + country_dict[name_key] = std_view_to_godot_string(country_entry.country.get_identifier()); + country_dict[size_key] = total_map_population; + country_dict[change_key] = 0; + country_dict[selected_key] = country_entry.selected; + + array.push_back(country_dict); + + return --count_counter > 0; + } + + return true; + } + + bool operator()(population_menu_t::state_entry_t const& state_entry) { + is_expanded = state_entry.expanded; + + if (start_counter-- <= 0) { + Dictionary state_dict; + + state_dict[type_key] = population_menu_t::LIST_ENTRY_STATE; + state_dict[index_key] = index; + state_dict[name_key] = std_view_to_godot_string(state_entry.state.get_identifier()); + state_dict[size_key] = state_entry.state.calculate_total_population(); + state_dict[change_key] = 0; + state_dict[selected_key] = state_entry.selected; + state_dict[expanded_key] = state_entry.expanded; + state_dict[colonial_status_key] = false; + + array.push_back(state_dict); + + return --count_counter > 0; + } + + return true; + } + + bool operator()(population_menu_t::province_entry_t const& province_entry) { + if (is_expanded && start_counter-- <= 0) { + Dictionary province_dict; + + province_dict[type_key] = population_menu_t::LIST_ENTRY_PROVINCE; + province_dict[index_key] = index; + province_dict[name_key] = std_view_to_godot_string(province_entry.province.get_identifier()); + province_dict[size_key] = province_entry.province.get_total_population(); + province_dict[change_key] = 0; + province_dict[selected_key] = province_entry.selected; + + array.push_back(province_dict); + + return --count_counter > 0; + } + + return true; + } + } entry_visitor { start, count, game_manager->get_map().get_total_map_population() }; + + while (entry_visitor.index < population_menu.province_list_entries.size() + && std::visit(entry_visitor, population_menu.province_list_entries[entry_visitor.index])) { + entry_visitor.index++; + } + + return entry_visitor.array; +} + +Error MenuSingleton::population_menu_select_province_list_entry(int32_t select_index, bool set_scroll_index) { + ERR_FAIL_INDEX_V(select_index, population_menu.province_list_entries.size(), FAILED); + + struct entry_visitor { + + const int32_t _select_index; + + int32_t index = 0, visible_index = 0; + bool is_expanded = true; + + int32_t selected_visible_index = -1; + + using enum population_menu_t::ProvinceListEntry; + population_menu_t::ProvinceListEntry select_level = LIST_ENTRY_NONE; + + void operator()(population_menu_t::country_entry_t& country_entry) { + if (index == _select_index) { + select_level = LIST_ENTRY_COUNTRY; + + country_entry.selected = true; + + selected_visible_index = visible_index; + } else { + select_level = LIST_ENTRY_NONE; + + country_entry.selected = false; + } + + visible_index++; + } + + void operator()(population_menu_t::state_entry_t& state_entry) { + if (select_level == LIST_ENTRY_COUNTRY) { + state_entry.selected = true; + } else if (index == _select_index) { + select_level = LIST_ENTRY_STATE; + + state_entry.selected = true; + + selected_visible_index = visible_index; + } else { + select_level = LIST_ENTRY_NONE; + state_entry.selected = false; + } + + visible_index++; + + is_expanded = state_entry.expanded; + } + + void operator()(population_menu_t::province_entry_t& province_entry) { + if (select_level == LIST_ENTRY_COUNTRY || select_level == LIST_ENTRY_STATE) { + province_entry.selected = true; + } else if (index == _select_index) { + province_entry.selected = true; + + selected_visible_index = visible_index; + } else { + province_entry.selected = false; + } + + if (is_expanded) { + visible_index++; + } + } + + } entry_visitor { select_index }; + + while (entry_visitor.index < population_menu.province_list_entries.size()) { + std::visit(entry_visitor, population_menu.province_list_entries[entry_visitor.index]); + entry_visitor.index++; + } + + emit_signal( + _signal_population_menu_province_list_selected_changed(), + set_scroll_index ? entry_visitor.selected_visible_index : -1 + ); + + _population_menu_update_pops(); + + return OK; +} + +Error MenuSingleton::population_menu_select_province(int32_t province_index) { + ERR_FAIL_NULL_V(game_manager, FAILED); + + ERR_FAIL_COND_V(province_index <= 0 || province_index > game_manager->get_map().get_province_count(), FAILED); + + struct entry_visitor_t { + + MenuSingleton& menu_singleton; + + const int32_t _province_index; + + int32_t index = 0; + + int32_t state_entry_to_expand = -1; + + bool ret = true; + + /* Overloads return false if the province entry is found and the loop can stop, true otherwise. */ + + bool operator()(population_menu_t::country_entry_t& country_entry) { + return true; + } + + bool operator()(population_menu_t::state_entry_t& state_entry) { + if (state_entry.expanded) { + state_entry_to_expand = -1; + } else { + state_entry_to_expand = index; + } + return true; + } + + bool operator()(population_menu_t::province_entry_t& province_entry) { + if (province_entry.province.get_index() == _province_index) { + + if (state_entry_to_expand >= 0) { + ret &= menu_singleton.population_menu_toggle_expanded(state_entry_to_expand, false) == OK; + } + + ret &= menu_singleton.population_menu_select_province_list_entry(index, true) == OK; + + return false; + } + return true; + } + + } entry_visitor { *this, province_index }; + + while (entry_visitor.index < population_menu.province_list_entries.size() + && std::visit(entry_visitor, population_menu.province_list_entries[entry_visitor.index])) { + entry_visitor.index++; + } + + ERR_FAIL_COND_V_MSG( + entry_visitor.index >= population_menu.province_list_entries.size(), FAILED, + vformat("Cannot select province index %d - not found in population menu province list!", province_index) + ); + + return ERR(entry_visitor.ret); +} + +Error MenuSingleton::population_menu_toggle_expanded(int32_t toggle_index, bool emit_selected_changed) { + ERR_FAIL_INDEX_V(toggle_index, population_menu.province_list_entries.size(), FAILED); + + population_menu_t::state_entry_t* state_entry = + std::get_if<population_menu_t::state_entry_t>(&population_menu.province_list_entries[toggle_index]); + + ERR_FAIL_NULL_V_MSG(state_entry, FAILED, vformat("Cannot toggle expansion of a non-state entry! (%d)", toggle_index)); + + int32_t provinces = 0; + + while (++toggle_index < population_menu.province_list_entries.size() + && std::holds_alternative<population_menu_t::province_entry_t>(population_menu.province_list_entries[toggle_index])) { + provinces++; + } + + if (state_entry->expanded) { + state_entry->expanded = false; + population_menu.visible_province_list_entries -= provinces; + } else { + state_entry->expanded = true; + population_menu.visible_province_list_entries += provinces; + } + + emit_signal(_signal_population_menu_province_list_changed()); + + if (emit_selected_changed) { + emit_signal(_signal_population_menu_province_list_selected_changed(), -1); + } + + return OK; +} + +void MenuSingleton::_population_menu_update_pops() { + for (auto [pop_type, filter] : mutable_iterator(population_menu.pop_filters)) { + filter.count = 0; + filter.promotion_demotion_change = 0; + } + + population_menu.pops.clear(); + + for (int32_t index = 0; index < population_menu.province_list_entries.size(); index++) { + population_menu_t::province_entry_t const* province_entry = + std::get_if<population_menu_t::province_entry_t>(&population_menu.province_list_entries[index]); + + if (province_entry != nullptr && province_entry->selected) { + for (Pop const& pop : province_entry->province.get_pops()) { + population_menu.pops.push_back(&pop); + population_menu_t::pop_filter_t& filter = population_menu.pop_filters[&pop.get_type()]; + filter.count += pop.get_size(); + // TODO - set filter.promotion_demotion_change + } + } + } + + _population_menu_update_filtered_pops(); +} + +void MenuSingleton::_population_menu_update_filtered_pops() { + population_menu.filtered_pops.clear(); + + for (fixed_point_map_t<HasIdentifierAndColour const*>& distribution : population_menu.distributions) { + distribution.clear(); + } + + for (Pop const* pop : population_menu.pops) { + if (population_menu.pop_filters[&pop->get_type()].selected) { + population_menu.filtered_pops.push_back(pop); + } + } + + for (Pop const* pop : population_menu.filtered_pops) { + population_menu.distributions[0][&pop->get_type()] += pop->get_size(); + population_menu.distributions[1][&pop->get_religion()] += pop->get_size(); + population_menu.distributions[2] += + cast_map<HasIdentifierAndColour>(pop->get_ideologies() * static_cast<int32_t>(pop->get_size())); + population_menu.distributions[3][&pop->get_culture()] += pop->get_size(); + population_menu.distributions[4] += + cast_map<HasIdentifierAndColour>(pop->get_issues() * static_cast<int32_t>(pop->get_size())); + population_menu.distributions[5] += + cast_map<HasIdentifierAndColour>(pop->get_votes() * static_cast<int32_t>(pop->get_size())); + } + + for (fixed_point_map_t<HasIdentifierAndColour const*>& distribution : population_menu.distributions) { + normalise_fixed_point_map(distribution); + } + + _population_menu_sort_pops(); +} + +MenuSingleton::sort_func_t MenuSingleton::_get_population_menu_sort_func(population_menu_t::PopSortKey sort_key) const { + using enum population_menu_t::PopSortKey; + switch (sort_key) { + case SORT_SIZE: + return [](Pop const* a, Pop const* b) -> bool { + return a->get_size() < b->get_size(); + }; + case SORT_TYPE: + return [this](Pop const* a, Pop const* b) -> bool { + return tr(std_view_to_godot_string(a->get_type().get_identifier())) + < tr(std_view_to_godot_string(b->get_type().get_identifier())); + }; + case SORT_CULTURE: + return [this](Pop const* a, Pop const* b) -> bool { + return tr(std_view_to_godot_string(a->get_culture().get_identifier())) + < tr(std_view_to_godot_string(b->get_culture().get_identifier())); + }; + case SORT_RELIGION: + return [this](Pop const* a, Pop const* b) -> bool { + return tr(std_view_to_godot_string(a->get_religion().get_identifier())) + < tr(std_view_to_godot_string(b->get_religion().get_identifier())); + }; + case SORT_LOCATION: + return [this](Pop const* a, Pop const* b) -> bool { + return tr(a->get_location() != nullptr ? std_view_to_godot_string(a->get_location()->get_identifier()) : String {}) + < tr(b->get_location() != nullptr ? std_view_to_godot_string(b->get_location()->get_identifier()) : String {}); + }; + case SORT_MILITANCY: + return [](Pop const* a, Pop const* b) -> bool { + return a->get_militancy() < b->get_militancy(); + }; + case SORT_CONSCIOUSNESS: + return [](Pop const* a, Pop const* b) -> bool { + return a->get_consciousness() < b->get_consciousness(); + }; + case SORT_IDEOLOGY: + return [](Pop const* a, Pop const* b) -> bool { + return sorted_fixed_map_less_than(a->get_ideologies(), b->get_ideologies()); + }; + case SORT_ISSUES: + return [](Pop const* a, Pop const* b) -> bool { + return sorted_fixed_map_less_than(a->get_issues(), b->get_issues()); + }; + case SORT_UNEMPLOYMENT: + return [](Pop const* a, Pop const* b) -> bool { + return a->get_unemployment() < b->get_unemployment(); + }; + case SORT_CASH: + return [](Pop const* a, Pop const* b) -> bool { + return a->get_cash() < b->get_cash(); + }; + case SORT_LIFE_NEEDS: + return [](Pop const* a, Pop const* b) -> bool { + return a->get_life_needs_fulfilled() < b->get_life_needs_fulfilled(); + }; + case SORT_EVERYDAY_NEEDS: + return [](Pop const* a, Pop const* b) -> bool { + return a->get_everyday_needs_fulfilled() < b->get_everyday_needs_fulfilled(); + }; + case SORT_LUXURY_NEEDS: + return [](Pop const* a, Pop const* b) -> bool { + return a->get_luxury_needs_fulfilled() < b->get_luxury_needs_fulfilled(); + }; + case SORT_REBEL_FACTION: + return [](Pop const* a, Pop const* b) -> bool { return false; }; // TODO - implement + case SORT_SIZE_CHANGE: + return [](Pop const* a, Pop const* b) -> bool { + return a->get_total_change() < b->get_total_change(); + }; + case SORT_LITERACY: + return [](Pop const* a, Pop const* b) -> bool { + return a->get_literacy() < b->get_literacy(); + }; + default: + UtilityFunctions::push_error("Invalid population menu sort key: ", sort_key); + return [](Pop const* a, Pop const* b) -> bool { return false; }; + } +} + +void MenuSingleton::_population_menu_sort_pops() { + if (population_menu.sort_key != population_menu_t::NONE) { + const sort_func_t base_sort_func = _get_population_menu_sort_func(population_menu.sort_key); + + const sort_func_t sort_func = population_menu.sort_descending + ? base_sort_func + : [base_sort_func](Pop const* a, Pop const* b) { return base_sort_func(b, a); }; + + std::sort(population_menu.filtered_pops.begin(), population_menu.filtered_pops.end(), sort_func); + } + + emit_signal(_signal_population_menu_pops_changed()); +} + +Error MenuSingleton::population_menu_select_sort_key(population_menu_t::PopSortKey sort_key) { + using enum population_menu_t::PopSortKey; + /* sort_key must be cast here to avoid causing clang to segfault during compilation. */ + ERR_FAIL_INDEX_V_MSG( + static_cast<int32_t>(sort_key), static_cast<int32_t>(MAX_SORT_KEY), FAILED, + vformat("Invalid population menu sort key: %d (must be under %d)", sort_key, MAX_SORT_KEY) + ); + + if (sort_key == population_menu.sort_key) { + /* Re-selecting the current sort key reverses sort order. */ + population_menu.sort_descending = !population_menu.sort_descending; + } else { + /* Selecting a new sort key switches sorting to that key, preserving the existing sort order. */ + population_menu.sort_key = sort_key; + } + + _population_menu_sort_pops(); + + return OK; +} + +TypedArray<Dictionary> MenuSingleton::get_population_menu_pop_rows(int32_t start, int32_t count) const { + if (population_menu.filtered_pops.empty()) { + return {}; + } + ERR_FAIL_INDEX_V_MSG( + start, population_menu.filtered_pops.size(), {}, vformat("Invalid start for population menu pop rows: %d", start) + ); + ERR_FAIL_COND_V_MSG(count <= 0, {}, vformat("Invalid count for population menu pop rows: %d", count)); + + if (start + count > population_menu.filtered_pops.size()) { + count = population_menu.filtered_pops.size() - start; + } + + static const StringName pop_size_key = "size"; + + static const StringName pop_type_icon_key = "pop_type_icon"; + // TODO - pop type name + // TODO - promotions (target pop type and count) + // TODO - demotions (target pop type and count) + // TODO - good being produced (artisans, farmers, labourers, slaves) + // TODO - military unit and army (soldiers) + + static const StringName pop_culture_key = "culture"; + // TODO - cultural assimilation (primary/accepted, or number, target culture, and conditional weights breakdown) + + static const StringName pop_religion_icon_key = "religion_icon"; + // TODO - religion name + // TODO - religious conversion (accepted, or number, target religion, and conditional weights breakdown) + + static const StringName pop_location_key = "location"; + // TODO - internal, external and colonial migration + + static const StringName pop_militancy_key = "militancy"; + // TODO - monthly militancy change and modifier breakdown + + static const StringName pop_consciousness_key = "consciousness"; + // TODO - monthly consciousness change and modifier breakdown + + static const StringName pop_ideology_key = "ideology"; + + static const StringName pop_issues_key = "issues"; + + static const StringName pop_unemployment_key = "unemployment"; + + static const StringName pop_cash_key = "cash"; + // TODO - daily income, needs, salary and savings + + static const StringName pop_life_needs_key = "life_needs"; + static const StringName pop_everyday_needs_key = "everyday_needs"; + static const StringName pop_luxury_needs_key = "luxury_needs"; + // TODO - goods not available on market or goods not affordale + price (for all 3 needs types) + + // TODO - rebel faction icon and name/description + + static const StringName pop_size_change_key = "size_change"; + // TODO - size change breakdown + + static const StringName pop_literacy_key = "literacy"; + // TODO - monthly change + + TypedArray<Dictionary> array; + ERR_FAIL_COND_V(array.resize(count) != OK, {}); + + for (int32_t idx = 0; idx < count; ++idx) { + Pop const* pop = population_menu.filtered_pops[start + idx]; + Dictionary pop_dict; + + pop_dict[pop_size_key] = pop->get_size(); + pop_dict[pop_type_icon_key] = pop->get_type().get_sprite(); + pop_dict[pop_culture_key] = std_view_to_godot_string(pop->get_culture().get_identifier()); + pop_dict[pop_religion_icon_key] = pop->get_religion().get_icon(); + pop_dict[pop_location_key] = + pop->get_location() != nullptr ? std_view_to_godot_string(pop->get_location()->get_identifier()) : String {}; + pop_dict[pop_militancy_key] = pop->get_militancy().to_float(); + pop_dict[pop_consciousness_key] = pop->get_consciousness().to_float(); + pop_dict[pop_ideology_key] = GFXPieChartTexture::distribution_to_slices_array(pop->get_ideologies()); + pop_dict[pop_issues_key] = GFXPieChartTexture::distribution_to_slices_array(pop->get_issues()); + pop_dict[pop_unemployment_key] = pop->get_unemployment().to_float(); + pop_dict[pop_cash_key] = pop->get_cash().to_float(); + pop_dict[pop_life_needs_key] = pop->get_life_needs_fulfilled().to_float(); + pop_dict[pop_everyday_needs_key] = pop->get_everyday_needs_fulfilled().to_float(); + pop_dict[pop_luxury_needs_key] = pop->get_luxury_needs_fulfilled().to_float(); + pop_dict[pop_size_change_key] = pop->get_total_change(); + pop_dict[pop_literacy_key] = pop->get_literacy().to_float(); + + array[idx] = std::move(pop_dict); + } + + return array; +} + +int32_t MenuSingleton::get_population_menu_pop_row_count() const { + return population_menu.filtered_pops.size(); +} + +PackedInt32Array MenuSingleton::get_population_menu_pop_filter_setup_info() { + ERR_FAIL_NULL_V(game_manager, {}); + + if (population_menu.pop_filters.empty()) { + for (PopType const& pop_type : game_manager->get_pop_manager().get_pop_types()) { + population_menu.pop_filters.emplace(&pop_type, population_menu_t::pop_filter_t { 0, 0, true }); + } + } + ERR_FAIL_COND_V_MSG(population_menu.pop_filters.empty(), {}, "Failed to generate population menu pop filters!"); + + PackedInt32Array array; + ERR_FAIL_COND_V(array.resize(population_menu.pop_filters.size()) != OK, {}); + + for (int32_t idx = 0; idx < array.size(); ++idx) { + array[idx] = population_menu.pop_filters.data()[idx].first->get_sprite(); + } + + return array; +} + +TypedArray<Dictionary> MenuSingleton::get_population_menu_pop_filter_info() const { + static const StringName pop_filter_count_key = "count"; + static const StringName pop_filter_change_key = "change"; + static const StringName pop_filter_selected_key = "selected"; + + TypedArray<Dictionary> array; + ERR_FAIL_COND_V(array.resize(population_menu.pop_filters.size()) != OK, {}); + + for (int32_t idx = 0; idx < array.size(); ++idx) { + population_menu_t::pop_filter_t const& filter = population_menu.pop_filters.data()[idx].second; + + Dictionary filter_dict; + + filter_dict[pop_filter_count_key] = filter.count; + filter_dict[pop_filter_change_key] = filter.promotion_demotion_change; + filter_dict[pop_filter_selected_key] = filter.selected; + + array[idx] = std::move(filter_dict); + } + + return array; +} + +Error MenuSingleton::population_menu_toggle_pop_filter(int32_t index) { + ERR_FAIL_COND_V_MSG( + index < 0 || index >= population_menu.pop_filters.size(), FAILED, vformat("Invalid pop filter index: %d", index) + ); + + population_menu_t::pop_filter_t& filter = mutable_iterator(population_menu.pop_filters).begin()[index].second; + filter.selected = !filter.selected; + + _population_menu_update_filtered_pops(); + + return OK; +} + +void MenuSingleton::population_menu_select_all_pop_filters() { + bool changed = false; + + for (auto [pop_type, filter] : mutable_iterator(population_menu.pop_filters)) { + if (!filter.selected) { + filter.selected = true; + changed = true; + } + } + + if (changed) { + _population_menu_update_filtered_pops(); + } +} + +void MenuSingleton::population_menu_deselect_all_pop_filters() { + bool changed = false; + for (auto [pop_type, filter] : mutable_iterator(population_menu.pop_filters)) { + if (filter.selected) { + filter.selected = false; + changed = true; + } + } + if (changed) { + _population_menu_update_filtered_pops(); + } +} + +PackedStringArray MenuSingleton::get_population_menu_distribution_setup_info() const { + static const PackedStringArray distribution_names = []() -> PackedStringArray { + constexpr std::array<char const*, population_menu_t::DISTRIBUTION_COUNT> NAMES { + /* Workforce (PopType) */ "WORKFORCE_DISTTITLE", + /* Religion */ "RELIGION_DISTTITLE", + /* Ideology */ "IDEOLOGY_DISTTITLE", + /* Nationality (Culture) */ "NATIONALITY_DISTTITLE", + /* Issues */ "DOMINANT_ISSUES_DISTTITLE", + /* Vote */ "ELECTORATE_DISTTITLE" + }; + + PackedStringArray array; + ERR_FAIL_COND_V(array.resize(NAMES.size()) != OK, {}); + + for (int32_t idx = 0; idx < array.size(); ++idx) { + array[idx] = NAMES[idx]; + } + + return array; + }(); + + return distribution_names; +} + +TypedArray<Array> MenuSingleton::get_population_menu_distribution_info() const { + TypedArray<Array> array; + ERR_FAIL_COND_V(array.resize(population_menu.distributions.size()) != OK, {}); + + for (int32_t idx = 0; idx < array.size(); ++idx) { + array[idx] = GFXPieChartTexture::distribution_to_slices_array(population_menu.distributions[idx]); + } + + return array; +} |