diff options
author | Hop311 <Hop3114@gmail.com> | 2023-09-14 21:17:04 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-09-14 21:17:04 +0200 |
commit | 9de41a0ed5f050f205219ab2a9f114800d048c14 (patch) | |
tree | 559681d5a2569adc307409d818311760edc12654 /extension/src/openvic-extension | |
parent | 1013b3c21226f703caba954664628694aedde469 (diff) | |
parent | 70c040d042cb536e5ce16b0cfff0e0afa39e8ed7 (diff) |
Merge pull request #151 from OpenVicProject/dataloading
Build system cleanup + dataloading scaffolding + JSON dataloading removed
Diffstat (limited to 'extension/src/openvic-extension')
-rw-r--r-- | extension/src/openvic-extension/Checksum.hpp | 37 | ||||
-rw-r--r-- | extension/src/openvic-extension/GameSingleton.cpp | 395 | ||||
-rw-r--r-- | extension/src/openvic-extension/GameSingleton.hpp | 158 | ||||
-rw-r--r-- | extension/src/openvic-extension/LoadGameCompatibility.cpp | 110 | ||||
-rw-r--r-- | extension/src/openvic-extension/LoadGameOpenVic.cpp | 132 | ||||
-rw-r--r-- | extension/src/openvic-extension/LoadLocalisation.cpp | 136 | ||||
-rw-r--r-- | extension/src/openvic-extension/LoadLocalisation.hpp | 28 | ||||
-rw-r--r-- | extension/src/openvic-extension/MapMesh.cpp | 150 | ||||
-rw-r--r-- | extension/src/openvic-extension/MapMesh.hpp | 34 | ||||
-rw-r--r-- | extension/src/openvic-extension/Utilities.cpp | 106 | ||||
-rw-r--r-- | extension/src/openvic-extension/Utilities.hpp | 30 | ||||
-rw-r--r-- | extension/src/openvic-extension/register_types.cpp | 63 | ||||
-rw-r--r-- | extension/src/openvic-extension/register_types.hpp | 6 |
13 files changed, 1385 insertions, 0 deletions
diff --git a/extension/src/openvic-extension/Checksum.hpp b/extension/src/openvic-extension/Checksum.hpp new file mode 100644 index 0000000..b12a9cd --- /dev/null +++ b/extension/src/openvic-extension/Checksum.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include <godot_cpp/core/class_db.hpp> + +namespace OpenVic { + class Checksum : public godot::Object { + GDCLASS(Checksum, godot::Object) + + // BEGIN BOILERPLATE + inline static Checksum* _checksum = nullptr; + + protected: + static void _bind_methods() { + godot::ClassDB::bind_method(godot::D_METHOD("get_checksum_text"), &Checksum::get_checksum_text); + } + + public: + inline static Checksum* get_singleton() { return _checksum; } + + inline Checksum() { + ERR_FAIL_COND(_checksum != nullptr); + _checksum = this; + } + inline ~Checksum() { + ERR_FAIL_COND(_checksum != this); + _checksum = nullptr; + } + // END BOILERPLATE + + /* REQUIREMENTS: + * DAT-8 + */ + inline godot::String get_checksum_text() { + return godot::String("1234abcd"); + } + }; +} diff --git a/extension/src/openvic-extension/GameSingleton.cpp b/extension/src/openvic-extension/GameSingleton.cpp new file mode 100644 index 0000000..a164b23 --- /dev/null +++ b/extension/src/openvic-extension/GameSingleton.cpp @@ -0,0 +1,395 @@ +#include "GameSingleton.hpp" + +#include <godot_cpp/variant/utility_functions.hpp> + +#include <openvic-simulation/utility/Logger.hpp> + +#include "openvic-extension/Utilities.hpp" + +using namespace godot; +using namespace OpenVic; + +TerrainVariant::TerrainVariant(const std::string_view new_identfier, + colour_t new_colour, Ref<Image> const& new_image) + : HasIdentifierAndColour { new_identfier, new_colour, true }, + image { new_image } {} + +Ref<Image> TerrainVariant::get_image() const { + return image; +} + +GameSingleton* GameSingleton::singleton = nullptr; + +void GameSingleton::_bind_methods() { + ClassDB::bind_static_method("GameSingleton", D_METHOD("setup_logger"), &GameSingleton::setup_logger); + ClassDB::bind_method(D_METHOD("load_defines_compatibility_mode", "file_paths"), &GameSingleton::load_defines_compatibility_mode); + ClassDB::bind_method(D_METHOD("lookup_file", "path"), &GameSingleton::lookup_file); + ClassDB::bind_method(D_METHOD("setup_game"), &GameSingleton::setup_game); + + ClassDB::bind_method(D_METHOD("get_province_index_from_uv_coords", "coords"), &GameSingleton::get_province_index_from_uv_coords); + ClassDB::bind_method(D_METHOD("get_province_info_from_index", "index"), &GameSingleton::get_province_info_from_index); + ClassDB::bind_method(D_METHOD("get_width"), &GameSingleton::get_width); + ClassDB::bind_method(D_METHOD("get_height"), &GameSingleton::get_height); + ClassDB::bind_method(D_METHOD("get_aspect_ratio"), &GameSingleton::get_aspect_ratio); + ClassDB::bind_method(D_METHOD("get_terrain_texture"), &GameSingleton::get_terrain_texture); + ClassDB::bind_method(D_METHOD("get_province_shape_image_subdivisions"), &GameSingleton::get_province_shape_image_subdivisions); + ClassDB::bind_method(D_METHOD("get_province_shape_texture"), &GameSingleton::get_province_shape_texture); + ClassDB::bind_method(D_METHOD("get_province_colour_texture"), &GameSingleton::get_province_colour_texture); + + ClassDB::bind_method(D_METHOD("get_mapmode_count"), &GameSingleton::get_mapmode_count); + ClassDB::bind_method(D_METHOD("get_mapmode_identifier", "index"), &GameSingleton::get_mapmode_identifier); + ClassDB::bind_method(D_METHOD("set_mapmode", "identifier"), &GameSingleton::set_mapmode); + ClassDB::bind_method(D_METHOD("get_selected_province_index"), &GameSingleton::get_selected_province_index); + ClassDB::bind_method(D_METHOD("set_selected_province", "index"), &GameSingleton::set_selected_province); + + ClassDB::bind_method(D_METHOD("expand_building", "province_index", "building_type_identifier"), &GameSingleton::expand_building); + ClassDB::bind_method(D_METHOD("get_good_icon_texture", "identifier"), &GameSingleton::get_good_icon_texture); + + ClassDB::bind_method(D_METHOD("set_paused", "paused"), &GameSingleton::set_paused); + ClassDB::bind_method(D_METHOD("toggle_paused"), &GameSingleton::toggle_paused); + ClassDB::bind_method(D_METHOD("is_paused"), &GameSingleton::is_paused); + ClassDB::bind_method(D_METHOD("increase_speed"), &GameSingleton::increase_speed); + ClassDB::bind_method(D_METHOD("decrease_speed"), &GameSingleton::decrease_speed); + ClassDB::bind_method(D_METHOD("can_increase_speed"), &GameSingleton::can_increase_speed); + ClassDB::bind_method(D_METHOD("can_decrease_speed"), &GameSingleton::can_decrease_speed); + ClassDB::bind_method(D_METHOD("get_longform_date"), &GameSingleton::get_longform_date); + ClassDB::bind_method(D_METHOD("try_tick"), &GameSingleton::try_tick); + + ADD_SIGNAL(MethodInfo("state_updated")); + ADD_SIGNAL(MethodInfo("province_selected", PropertyInfo(Variant::INT, "index"))); + + ClassDB::bind_static_method("GameSingleton", D_METHOD("get_province_info_province_key"), &GameSingleton::get_province_info_province_key); + ClassDB::bind_static_method("GameSingleton", D_METHOD("get_province_info_region_key"), &GameSingleton::get_province_info_region_key); + ClassDB::bind_static_method("GameSingleton", D_METHOD("get_province_info_life_rating_key"), &GameSingleton::get_province_info_life_rating_key); + ClassDB::bind_static_method("GameSingleton", D_METHOD("get_province_info_total_population_key"), &GameSingleton::get_province_info_total_population_key); + ClassDB::bind_static_method("GameSingleton", D_METHOD("get_province_info_pop_types_key"), &GameSingleton::get_province_info_pop_types_key); + ClassDB::bind_static_method("GameSingleton", D_METHOD("get_province_info_pop_ideologies_key"), &GameSingleton::get_province_info_pop_ideologies_key); + ClassDB::bind_static_method("GameSingleton", D_METHOD("get_province_info_pop_cultures_key"), &GameSingleton::get_province_info_pop_cultures_key); + ClassDB::bind_static_method("GameSingleton", D_METHOD("get_province_info_rgo_key"), &GameSingleton::get_province_info_rgo_key); + ClassDB::bind_static_method("GameSingleton", D_METHOD("get_province_info_buildings_key"), &GameSingleton::get_province_info_buildings_key); + + ClassDB::bind_static_method("GameSingleton", D_METHOD("get_building_info_building_key"), &GameSingleton::get_building_info_building_key); + ClassDB::bind_static_method("GameSingleton", D_METHOD("get_building_info_level_key"), &GameSingleton::get_building_info_level_key); + ClassDB::bind_static_method("GameSingleton", D_METHOD("get_building_info_expansion_state_key"), &GameSingleton::get_building_info_expansion_state_key); + ClassDB::bind_static_method("GameSingleton", D_METHOD("get_building_info_start_date_key"), &GameSingleton::get_building_info_start_date_key); + ClassDB::bind_static_method("GameSingleton", D_METHOD("get_building_info_end_date_key"), &GameSingleton::get_building_info_end_date_key); + ClassDB::bind_static_method("GameSingleton", D_METHOD("get_building_info_expansion_progress_key"), &GameSingleton::get_building_info_expansion_progress_key); + + ClassDB::bind_static_method("GameSingleton", D_METHOD("get_piechart_info_size_key"), &GameSingleton::get_piechart_info_size_key); + ClassDB::bind_static_method("GameSingleton", D_METHOD("get_piechart_info_colour_key"), &GameSingleton::get_piechart_info_colour_key); + + ClassDB::bind_static_method("GameSingleton", D_METHOD("draw_pie_chart", "image", "stopAngles", "colours", "radius", + "shadow_displacement", "shadow_tightness", "shadow_radius", "shadow_thickness", + "trim_colour", "trim_size", "gradient_falloff", "gradient_base", + "donut", "donut_inner_trim", "donut_inner_radius"), &GameSingleton::draw_pie_chart); +} + +void GameSingleton::draw_pie_chart(Ref<Image> image, + Array const& stopAngles, Array const& colours, float radius, + Vector2 shadow_displacement, float shadow_tightness, float shadow_radius, float shadow_thickness, + Color trim_colour, float trim_size, float gradient_falloff, float gradient_base, + bool donut, bool donut_inner_trim, float donut_inner_radius) { + + OpenVic::draw_pie_chart(image, stopAngles, colours, radius, shadow_displacement, shadow_tightness, shadow_radius, shadow_thickness, + trim_colour, trim_size, gradient_falloff, gradient_base, + donut, donut_inner_trim, donut_inner_radius); +} + +GameSingleton* GameSingleton::get_singleton() { + return singleton; +} + +void GameSingleton::_on_state_updated() { + _update_colour_image(); + emit_signal("state_updated"); +} + +/* REQUIREMENTS: + * MAP-21, MAP-23, MAP-25, MAP-32, MAP-33, MAP-34 + */ +GameSingleton::GameSingleton() : game_manager { [this]() { _on_state_updated(); } }, + terrain_variants { "terrain variants" } { + ERR_FAIL_COND(singleton != nullptr); + singleton = this; +} + +void GameSingleton::setup_logger() { + Logger::set_info_func([](std::string&& str) { UtilityFunctions::print(std_to_godot_string(str)); }); + Logger::set_warning_func([](std::string&& str) { UtilityFunctions::push_warning(std_to_godot_string(str)); }); + Logger::set_error_func([](std::string&& str) { UtilityFunctions::push_error(std_to_godot_string(str)); }); +} + +GameSingleton::~GameSingleton() { + ERR_FAIL_COND(singleton != this); + singleton = nullptr; +} + +Error GameSingleton::setup_game() { + bool ret = game_manager.setup(); + ret &= dataloader.load_pop_history(game_manager, "history/pops/" + game_manager.get_today().to_string()); + 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_width(); + const size_t y_mod_h = UtilityFunctions::fposmod(coords.y, 1.0f) * get_height(); + return game_manager.map.get_province_index_at(x_mod_w, y_mod_h); +} + +StringName const& GameSingleton::get_province_info_province_key() { + static const StringName key = "province"; + return key; +} +StringName const& GameSingleton::get_province_info_region_key() { + static const StringName key = "region"; + return key; +} +StringName const& GameSingleton::get_province_info_life_rating_key() { + static const StringName key = "life_rating"; + return key; +} +StringName const& GameSingleton::get_province_info_total_population_key() { + static const StringName key = "total_population"; + return key; +} +StringName const& GameSingleton::get_province_info_pop_types_key() { + static const StringName key = "pop_types"; + return key; +} +StringName const& GameSingleton::get_province_info_pop_ideologies_key() { + static const StringName key = "pop_ideologies"; + return key; +} +StringName const& GameSingleton::get_province_info_pop_cultures_key() { + static const StringName key = "pop_cultures"; + return key; +} +StringName const& GameSingleton::get_province_info_rgo_key() { + static const StringName key = "rgo"; + return key; +} +StringName const& GameSingleton::get_province_info_buildings_key() { + static const StringName key = "buildings"; + return key; +} + +StringName const& GameSingleton::get_building_info_building_key() { + static const StringName key = "building"; + return key; +} +StringName const& GameSingleton::get_building_info_level_key() { + static const StringName key = "level"; + return key; +} +StringName const& GameSingleton::get_building_info_expansion_state_key() { + static const StringName key = "expansion_state"; + return key; +} +StringName const& GameSingleton::get_building_info_start_date_key() { + static const StringName key = "start_date"; + return key; +} +StringName const& GameSingleton::get_building_info_end_date_key() { + static const StringName key = "end_date"; + return key; +} +StringName const& GameSingleton::get_building_info_expansion_progress_key() { + static const StringName key = "expansion_progress"; + return key; +} + +StringName const& GameSingleton::get_piechart_info_size_key() { + static const StringName key = "size"; + return key; +} +StringName const& GameSingleton::get_piechart_info_colour_key() { + static const StringName key = "colour"; + return key; +} + +Dictionary GameSingleton::_distribution_to_dictionary(distribution_t const& dist) const { + Dictionary dict; + for (distribution_t::value_type const& p : dist) { + Dictionary sub_dict; + sub_dict[get_piechart_info_size_key()] = p.second; + sub_dict[get_piechart_info_colour_key()] = to_godot_color(p.first->get_colour()); + dict[std_to_godot_string(p.first->get_identifier())] = sub_dict; + } + return dict; +} + +Dictionary GameSingleton::get_province_info_from_index(int32_t index) const { + Province const* province = game_manager.map.get_province_by_index(index); + if (province == nullptr) return {}; + Dictionary ret; + + ret[get_province_info_province_key()] = std_to_godot_string(province->get_identifier()); + + Region const* region = province->get_region(); + if (region != nullptr) ret[get_province_info_region_key()] = std_to_godot_string(region->get_identifier()); + + Good const* rgo = province->get_rgo(); + if (rgo != nullptr) ret[get_province_info_rgo_key()] = std_to_godot_string(rgo->get_identifier()); + + ret[get_province_info_life_rating_key()] = province->get_life_rating(); + ret[get_province_info_total_population_key()] = province->get_total_population(); + distribution_t const& pop_types = province->get_pop_type_distribution(); + if (!pop_types.empty()) ret[get_province_info_pop_types_key()] = _distribution_to_dictionary(pop_types); + //distribution_t const& ideologies = province->get_ideology_distribution(); + //if (!ideologies.empty()) ret[get_province_info_pop_ideologies_key()] = _distribution_to_dictionary(ideologies); + distribution_t const& cultures = province->get_culture_distribution(); + if (!cultures.empty()) ret[get_province_info_pop_cultures_key()] = _distribution_to_dictionary(cultures); + + std::vector<Building> const& buildings = province->get_buildings(); + if (!buildings.empty()) { + Array buildings_array; + buildings_array.resize(buildings.size()); + for (size_t idx = 0; idx < buildings.size(); ++idx) { + Building const& building = buildings[idx]; + + Dictionary building_dict; + building_dict[get_building_info_building_key()] = std_to_godot_string(building.get_identifier()); + building_dict[get_building_info_level_key()] = static_cast<int32_t>(building.get_level()); + building_dict[get_building_info_expansion_state_key()] = static_cast<int32_t>(building.get_expansion_state()); + building_dict[get_building_info_start_date_key()] = std_to_godot_string(building.get_start_date().to_string()); + building_dict[get_building_info_end_date_key()] = std_to_godot_string(building.get_end_date().to_string()); + building_dict[get_building_info_expansion_progress_key()] = building.get_expansion_progress(); + + buildings_array[idx] = building_dict; + } + ret[get_province_info_buildings_key()] = buildings_array; + } + return ret; +} + +int32_t GameSingleton::get_width() const { + return game_manager.map.get_width(); +} + +int32_t GameSingleton::get_height() const { + return game_manager.map.get_height(); +} + +float GameSingleton::get_aspect_ratio() const { + return static_cast<float>(get_width()) / static_cast<float>(get_height()); +} + +Ref<Texture> GameSingleton::get_terrain_texture() const { + return terrain_texture; +} + +Vector2i GameSingleton::get_province_shape_image_subdivisions() const { + return image_subdivisions; +} + +Ref<Texture> GameSingleton::get_province_shape_texture() const { + return province_shape_texture; +} + +Ref<Texture> GameSingleton::get_province_colour_texture() const { + return province_colour_texture; +} + +Error GameSingleton::_update_colour_image() { + static PackedByteArray colour_data_array; + static constexpr int64_t colour_data_array_size = (static_cast<int64_t>(Province::MAX_INDEX) + 1) * Map::MAPMODE_COLOUR_SIZE; + colour_data_array.resize(colour_data_array_size); + + Error err = OK; + if (!game_manager.map.generate_mapmode_colours(mapmode_index, colour_data_array.ptrw())) + err = FAILED; + + static constexpr int32_t PROVINCE_INDEX_SQRT = 1 << (sizeof(Province::index_t) * 4); + if (province_colour_image.is_null()) { + province_colour_image.instantiate(); + ERR_FAIL_NULL_V_EDMSG(province_colour_image, FAILED, + "Failed to create province colour image"); + } + province_colour_image->set_data(PROVINCE_INDEX_SQRT, PROVINCE_INDEX_SQRT, + false, Image::FORMAT_RGBA8, colour_data_array); + if (province_colour_texture.is_null()) { + province_colour_texture = ImageTexture::create_from_image(province_colour_image); + ERR_FAIL_NULL_V_EDMSG(province_colour_texture, FAILED, + "Failed to create province colour texture"); + } else province_colour_texture->update(province_colour_image); + return err; +} + +int32_t GameSingleton::get_mapmode_count() const { + return game_manager.map.get_mapmode_count(); +} + +String GameSingleton::get_mapmode_identifier(int32_t index) const { + Mapmode const* mapmode = game_manager.map.get_mapmode_by_index(index); + if (mapmode != nullptr) return std_to_godot_string(mapmode->get_identifier()); + return String {}; +} + +Error GameSingleton::set_mapmode(String const& identifier) { + Mapmode const* mapmode = game_manager.map.get_mapmode_by_identifier(godot_to_std_string(identifier)); + if (mapmode == nullptr) { + UtilityFunctions::push_error("Failed to set mapmode to: ", identifier); + return FAILED; + } + mapmode_index = mapmode->get_index(); + _update_colour_image(); + return OK; +} + +int32_t GameSingleton::get_selected_province_index() const { + return game_manager.map.get_selected_province_index(); +} + +void GameSingleton::set_selected_province(int32_t index) { + game_manager.map.set_selected_province(index); + _update_colour_image(); + emit_signal("province_selected", index); +} + +Error GameSingleton::expand_building(int32_t province_index, String const& building_type_identifier) { + if (!game_manager.expand_building(province_index, godot_to_std_string(building_type_identifier))) { + UtilityFunctions::push_error("Failed to expand ", building_type_identifier, " at province index ", province_index); + return FAILED; + } + return OK; +} + +Ref<Texture> GameSingleton::get_good_icon_texture(String const& identifier) const { + return good_icons.get(identifier, {}); +} + +void GameSingleton::set_paused(bool paused) { + game_manager.clock.isPaused = paused; +} + +void GameSingleton::toggle_paused() { + game_manager.clock.isPaused = !game_manager.clock.isPaused; +} + +bool GameSingleton::is_paused() const { + return game_manager.clock.isPaused; +} + +void GameSingleton::increase_speed() { + game_manager.clock.increaseSimulationSpeed(); +} + +void GameSingleton::decrease_speed() { + game_manager.clock.decreaseSimulationSpeed(); +} + +bool GameSingleton::can_increase_speed() const { + return game_manager.clock.canIncreaseSimulationSpeed(); +} + +bool GameSingleton::can_decrease_speed() const { + return game_manager.clock.canDecreaseSimulationSpeed(); +} + +String GameSingleton::get_longform_date() const { + return std_to_godot_string(game_manager.get_today().to_string()); +} + +void GameSingleton::try_tick() { + game_manager.clock.conditionallyAdvanceGame(); +} diff --git a/extension/src/openvic-extension/GameSingleton.hpp b/extension/src/openvic-extension/GameSingleton.hpp new file mode 100644 index 0000000..bd6b73c --- /dev/null +++ b/extension/src/openvic-extension/GameSingleton.hpp @@ -0,0 +1,158 @@ +#pragma once + +#include <godot_cpp/classes/image_texture.hpp> +#include <godot_cpp/classes/texture2d_array.hpp> + +#include <openvic-simulation/GameManager.hpp> +#include <openvic-simulation/dataloader/Dataloader.hpp> + +namespace OpenVic { + + struct TerrainVariant : HasIdentifierAndColour { + friend class GameSingleton; + + private: + const godot::Ref<godot::Image> image; + + TerrainVariant(const std::string_view new_identfier, colour_t new_colour, + godot::Ref<godot::Image> const& new_image); + + public: + static constexpr size_t MAX_TERRIN_VARIANT_COUNT = 1 << (8 * sizeof(Map::terrain_t)); + + TerrainVariant(TerrainVariant&&) = default; + + godot::Ref<godot::Image> get_image() const; + }; + + class GameSingleton : public godot::Object { + GDCLASS(GameSingleton, godot::Object) + + static GameSingleton* singleton; + + GameManager game_manager; + Dataloader dataloader; + + godot::Vector2i image_subdivisions; + godot::Ref<godot::Texture2DArray> province_shape_texture; + godot::Ref<godot::Image> province_colour_image; + godot::Ref<godot::ImageTexture> province_colour_texture; + Mapmode::index_t mapmode_index = 0; + IdentifierRegistry<TerrainVariant> terrain_variants; + Map::terrain_variant_map_t terrain_variant_map; + godot::Ref<godot::Texture2DArray> terrain_texture; + godot::Dictionary good_icons; + + godot::Error _generate_terrain_texture_array(); + godot::Error _load_map_images(godot::String const& province_image_path, godot::String const& terrain_image_path, bool flip_vertical = false); + + godot::Error _load_terrain_variants_compatibility_mode(godot::String const& terrain_image_path, godot::String const& terrain_texturesheet_path); + + /* Generate the province_colour_texture from the current mapmode. + */ + godot::Error _update_colour_image(); + void _on_state_updated(); + + godot::Dictionary _distribution_to_dictionary(distribution_t const& dist) const; + + protected: + static void _bind_methods(); + + public: + static void draw_pie_chart(godot::Ref<godot::Image> image, + godot::Array const& stopAngles, godot::Array const& colours, float radius, + godot::Vector2 shadow_displacement, float shadow_tightness, float shadow_radius, float shadow_thickness, + godot::Color trim_colour, float trim_size, float gradient_falloff, float gradient_base, + bool donut, bool donut_inner_trim, float donut_inner_radius); + + static GameSingleton* get_singleton(); + + GameSingleton(); + ~GameSingleton(); + + static void setup_logger(); + + /* Load the game's defines in compatiblity mode from the filepath + * pointing to the defines folder. + */ + godot::Error load_defines_compatibility_mode(godot::PackedStringArray const& file_paths); + + godot::String lookup_file(godot::String const& path) const; + + /* Post-load/restart game setup - reset the game to post-load state + * and (re)generate starting data, e.g. buildings. + */ + godot::Error setup_game(); + + int32_t get_province_index_from_uv_coords(godot::Vector2 const& coords) const; + + static godot::StringName const& get_province_info_province_key(); + static godot::StringName const& get_province_info_region_key(); + static godot::StringName const& get_province_info_life_rating_key(); + static godot::StringName const& get_province_info_total_population_key(); + static godot::StringName const& get_province_info_pop_types_key(); + static godot::StringName const& get_province_info_pop_ideologies_key(); + static godot::StringName const& get_province_info_pop_cultures_key(); + static godot::StringName const& get_province_info_rgo_key(); + static godot::StringName const& get_province_info_buildings_key(); + + static godot::StringName const& get_building_info_building_key(); + static godot::StringName const& get_building_info_level_key(); + static godot::StringName const& get_building_info_expansion_state_key(); + static godot::StringName const& get_building_info_start_date_key(); + static godot::StringName const& get_building_info_end_date_key(); + static godot::StringName const& get_building_info_expansion_progress_key(); + + static godot::StringName const& get_piechart_info_size_key(); + static godot::StringName const& get_piechart_info_colour_key(); + + /* Get info to display in Province Overview Panel, packaged in + * a Dictionary using the StringNames above as keys. + */ + godot::Dictionary get_province_info_from_index(int32_t index) const; + + int32_t get_width() const; + int32_t get_height() const; + float get_aspect_ratio() const; + + /* The cosmetic terrain textures stored in a Texture2DArray. + */ + godot::Ref<godot::Texture> get_terrain_texture() const; + + /* Number of (vertical, horizontal) subdivisions the province shape image + * was split into when making the province_shape_texture to ensure no + * piece had a dimension greater than 16383. + */ + godot::Vector2i get_province_shape_image_subdivisions() const; + + /* The map, encoded in RGB8 with RG representing province index and B representing terrain texture. + * To support a wider range of GPUs, the image is divided so that no piece has a dimension + * greater than 16383 and the pieces are stored in a Texture2DArray. + */ + godot::Ref<godot::Texture> get_province_shape_texture() const; + + /* The colour each province should be tinted, arranged in + * index order into a 256x256 RGB8 texture. + */ + godot::Ref<godot::Texture> get_province_colour_texture() const; + + int32_t get_mapmode_count() const; + godot::String get_mapmode_identifier(int32_t index) const; + godot::Error set_mapmode(godot::String const& identifier); + int32_t get_selected_province_index() const; + void set_selected_province(int32_t index); + + godot::Error expand_building(int32_t province_index, godot::String const& building_type_identifier); + godot::Ref<godot::Texture> get_good_icon_texture(godot::String const& identifier) const; + + void set_paused(bool paused); + void toggle_paused(); + bool is_paused() const; + void increase_speed(); + void decrease_speed(); + bool can_increase_speed() const; + bool can_decrease_speed() const; + godot::String get_longform_date() const; + void try_tick(); + }; +} diff --git a/extension/src/openvic-extension/LoadGameCompatibility.cpp b/extension/src/openvic-extension/LoadGameCompatibility.cpp new file mode 100644 index 0000000..e8e3314 --- /dev/null +++ b/extension/src/openvic-extension/LoadGameCompatibility.cpp @@ -0,0 +1,110 @@ +#include "GameSingleton.hpp" + +#include <godot_cpp/classes/file_access.hpp> +#include <godot_cpp/variant/utility_functions.hpp> + +#include <openvic-simulation/utility/BMP.hpp> + +#include "openvic-extension/Utilities.hpp" + +using namespace godot; +using namespace OpenVic; + +Error GameSingleton::_load_terrain_variants_compatibility_mode(String const& terrain_image_path, String const& terrain_texturesheet_path) { + // Read BMP's palette to determine terrain variant colours which texture they're associated with + BMP bmp; + if (!(bmp.open(godot_to_std_string(terrain_image_path).c_str()) && bmp.read_header() && bmp.read_palette())) { + UtilityFunctions::push_error("Failed to read BMP palette from compatibility mode terrain image: ", terrain_image_path); + return FAILED; + } + std::vector<colour_t> const& palette = bmp.get_palette(); + static constexpr int32_t SHEET_DIMS = 8, PALETTE_SIZE = SHEET_DIMS * SHEET_DIMS; + if (palette.size() == 0 || palette.size() < PALETTE_SIZE) { + UtilityFunctions::push_error("Invalid BMP palette size for terrain image: ", static_cast<uint64_t>(palette.size()), " (expected ", PALETTE_SIZE, ")"); + return FAILED; + } + + // Load the terrain texture sheet and prepare to slice it up + Ref<Image> terrain_sheet = load_godot_image(terrain_texturesheet_path); + if (terrain_sheet.is_null()) { + UtilityFunctions::push_error("Failed to load terrain texture sheet: ", terrain_texturesheet_path); + return FAILED; + } + terrain_sheet->flip_y(); + const int32_t sheet_width = terrain_sheet->get_width(), sheet_height = terrain_sheet->get_height(); + if (sheet_width < 1 || sheet_width % SHEET_DIMS != 0 || sheet_width != sheet_height) { + UtilityFunctions::push_error("Invalid terrain texture sheet dims: ", sheet_width, "x", sheet_height, " (must be square with dims positive multiples of ", SHEET_DIMS, ")"); + return FAILED; + } + const int32_t slice_size = sheet_width / SHEET_DIMS; + + { + static constexpr colour_t TERRAIN_WATER_INDEX_COLOUR = 0xFFFFFF; + Ref<Image> water_image = Image::create(slice_size, slice_size, false, terrain_sheet->get_format()); + ERR_FAIL_NULL_V_EDMSG(water_image, FAILED, "Failed to create water terrain image"); + water_image->fill({ 0.1f, 0.1f, 0.5f }); + terrain_variants.add_item({ "terrain_water", TERRAIN_WATER_INDEX_COLOUR, water_image }); + } + Error err = OK; + for (int32_t idx = 0; idx < PALETTE_SIZE; ++idx) { + const Rect2i slice { (idx % SHEET_DIMS) * slice_size, (7 - (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; + continue; + } + if (!terrain_variants.add_item({ "terrain_" + std::to_string(idx), palette[idx], terrain_image })) err = FAILED; + } + terrain_variants.lock(); + if (_generate_terrain_texture_array() != OK) return FAILED; + return err; +} + +Error GameSingleton::load_defines_compatibility_mode(PackedStringArray const& file_paths) { + static const fs::path province_image_file = "map/provinces.bmp"; + static const fs::path terrain_image_file = "map/terrain.bmp"; + static const fs::path terrain_texture_file = "map/terrain/texturesheet.tga"; + + Dataloader::path_vector_t roots; + for (String const& path : file_paths) { + roots.push_back(godot_to_std_string(path)); + } + + Error err = OK; + + if (!dataloader.set_roots(roots)) { + Logger::error("Failed to set dataloader roots!"); + err = FAILED; + } + + if (!dataloader.load_defines(game_manager)) { + UtilityFunctions::push_error("Failed to load defines!"); + err = FAILED; + } + + game_manager.map.lock_regions(); + if (_load_terrain_variants_compatibility_mode( + std_to_godot_string(dataloader.lookup_file(terrain_image_file).string()), + std_to_godot_string(dataloader.lookup_file(terrain_texture_file).string()) + ) != OK) { + UtilityFunctions::push_error("Failed to load terrain variants!"); + err = FAILED; + } + if (_load_map_images( + std_to_godot_string(dataloader.lookup_file(province_image_file).string()), + std_to_godot_string(dataloader.lookup_file(terrain_image_file).string()), + true) != OK) { + UtilityFunctions::push_error("Failed to load map images!"); + err = FAILED; + } + if (!game_manager.load_hardcoded_defines()) { + UtilityFunctions::push_error("Failed to hardcoded defines!"); + err = FAILED; + } + return err; +} + +String GameSingleton::lookup_file(String const& path) const { + return std_to_godot_string(dataloader.lookup_file(godot_to_std_string(path)).string()); +} diff --git a/extension/src/openvic-extension/LoadGameOpenVic.cpp b/extension/src/openvic-extension/LoadGameOpenVic.cpp new file mode 100644 index 0000000..87c66da --- /dev/null +++ b/extension/src/openvic-extension/LoadGameOpenVic.cpp @@ -0,0 +1,132 @@ +#include "GameSingleton.hpp" + +#include <godot_cpp/classes/file_access.hpp> +#include <godot_cpp/classes/json.hpp> +#include <godot_cpp/variant/utility_functions.hpp> + +#include "openvic-extension/Utilities.hpp" + +using namespace godot; +using namespace OpenVic; + +Error GameSingleton::_generate_terrain_texture_array() { + Error err = OK; + if (terrain_variants.size() == 0) { + UtilityFunctions::push_error("Failed to load terrain textures!"); + return FAILED; + } + // TerrainVariant count is limited by the data type representing it in the map image + if (terrain_variants.size() > TerrainVariant::MAX_TERRIN_VARIANT_COUNT) { + UtilityFunctions::push_error("Too many terrain textures - all after the first ", + static_cast<uint64_t>(TerrainVariant::MAX_TERRIN_VARIANT_COUNT), " will be ignored"); + err = FAILED; + } + + Array terrain_images; + for (size_t i = 0; i < terrain_variants.size() && i < TerrainVariant::MAX_TERRIN_VARIANT_COUNT; ++i) { + TerrainVariant const& var = *terrain_variants.get_item_by_index(i); + terrain_variant_map[var.get_colour()] = i; + terrain_images.append(var.get_image()); + } + + terrain_texture.instantiate(); + if (terrain_texture->create_from_images(terrain_images) != OK) { + UtilityFunctions::push_error("Failed to create terrain texture array!"); + return FAILED; + } + return err; +} + +Error GameSingleton::_load_map_images(String const& province_image_path, String const& terrain_image_path, bool flip_vertical) { + if (province_shape_texture.is_valid()) { + UtilityFunctions::push_error("Map images have already been loaded, cannot load: ", province_image_path, " and ", terrain_image_path); + return FAILED; + } + + // Load images + Ref<Image> province_image = load_godot_image(province_image_path); + if (province_image.is_null()) { + UtilityFunctions::push_error("Failed to load province image: ", province_image_path); + return FAILED; + } + Ref<Image> terrain_image = load_godot_image(terrain_image_path); + if (terrain_image.is_null()) { + UtilityFunctions::push_error("Failed to load terrain image: ", terrain_image_path); + return FAILED; + } + + if (flip_vertical) { + province_image->flip_y(); + terrain_image->flip_y(); + } + + // Validate dimensions and format + Error err = OK; + const Vector2i province_dims = province_image->get_size(), terrain_dims = terrain_image->get_size(); + if (province_dims.x < 1 || province_dims.y < 1) { + UtilityFunctions::push_error("Invalid dimensions (", province_dims.x, "x", province_dims.y, ") for province image: ", province_image_path); + err = FAILED; + } + if (province_dims != terrain_dims) { + UtilityFunctions::push_error("Invalid dimensions (", terrain_dims.x, "x", terrain_dims.y, ") for terrain image: ", + terrain_image_path, " (must match province image: (", province_dims.x, "x", province_dims.x, "))"); + err = FAILED; + } + static constexpr Image::Format expected_format = Image::FORMAT_RGB8; + if (province_image->get_format() == Image::FORMAT_RGBA8) province_image->convert(expected_format); + if (terrain_image->get_format() == Image::FORMAT_RGBA8) terrain_image->convert(expected_format); + if (province_image->get_format() != expected_format) { + UtilityFunctions::push_error("Invalid format (", province_image->get_format(), ", should be ", expected_format, ") for province image: ", province_image_path); + err = FAILED; + } + if (terrain_image->get_format() != expected_format) { + UtilityFunctions::push_error("Invalid format (", terrain_image->get_format(), ", should be ", expected_format, ") for terrain image: ", terrain_image_path); + err = FAILED; + } + if (err != OK) return err; + + // Generate interleaved province and terrain ID image + if (!game_manager.map.generate_province_shape_image(province_dims.x, province_dims.y, + province_image->get_data().ptr(), terrain_image->get_data().ptr(), terrain_variant_map, + false /* <-- whether to print detailed map errors or not (specific missing/unrecognised colours) */ + )) err = FAILED; + + static constexpr int32_t GPU_DIM_LIMIT = 0x3FFF; + // For each dimension of the image, this finds the small number of equal subdivisions required get the individual texture dims under GPU_DIM_LIMIT + for (int i = 0; i < 2; ++i) + for (image_subdivisions[i] = 1; province_dims[i] / image_subdivisions[i] > GPU_DIM_LIMIT || + province_dims[i] % image_subdivisions[i] != 0; ++image_subdivisions[i]); + + Map::shape_pixel_t const* province_shape_data = game_manager.map.get_province_shape_image().data(); + const Vector2i divided_dims = province_dims / image_subdivisions; + Array province_shape_images; + province_shape_images.resize(image_subdivisions.x * image_subdivisions.y); + 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), + province_shape_data + (v * divided_dims.y + y) * province_dims.x + u * divided_dims.x, + divided_dims.x * sizeof(Map::shape_pixel_t)); + + const Ref<Image> province_shape_subimage = Image::create_from_data(divided_dims.x, divided_dims.y, false, Image::FORMAT_RGB8, index_data_array); + if (province_shape_subimage.is_null()) { + UtilityFunctions::push_error("Failed to create province shape image (", u, ", ", v, ")"); + err = FAILED; + } + province_shape_images[u + v * image_subdivisions.x] = province_shape_subimage; + } + } + + province_shape_texture.instantiate(); + if (province_shape_texture->create_from_images(province_shape_images) != OK) { + UtilityFunctions::push_error("Failed to create terrain texture array!"); + err = FAILED; + } + + if (_update_colour_image() != OK) err = FAILED; + + return err; +} diff --git a/extension/src/openvic-extension/LoadLocalisation.cpp b/extension/src/openvic-extension/LoadLocalisation.cpp new file mode 100644 index 0000000..dc7702c --- /dev/null +++ b/extension/src/openvic-extension/LoadLocalisation.cpp @@ -0,0 +1,136 @@ +#include "LoadLocalisation.hpp" + +#include <godot_cpp/classes/dir_access.hpp> +#include <godot_cpp/classes/file_access.hpp> +#include <godot_cpp/classes/translation_server.hpp> +#include <godot_cpp/variant/utility_functions.hpp> + +using namespace godot; +using namespace OpenVic; + +LoadLocalisation* LoadLocalisation::singleton = nullptr; + +void LoadLocalisation::_bind_methods() { + ClassDB::bind_method(D_METHOD("load_file", "file_path", "locale"), &LoadLocalisation::load_file); + ClassDB::bind_method(D_METHOD("load_locale_dir", "dir_path", "locale"), &LoadLocalisation::load_locale_dir); + ClassDB::bind_method(D_METHOD("load_localisation_dir", "dir_path"), &LoadLocalisation::load_localisation_dir); +} + +LoadLocalisation* LoadLocalisation::get_singleton() { + return singleton; +} + +LoadLocalisation::LoadLocalisation() { + ERR_FAIL_COND(singleton != nullptr); + singleton = this; +} + +LoadLocalisation::~LoadLocalisation() { + ERR_FAIL_COND(singleton != this); + singleton = nullptr; +} + +Error LoadLocalisation::_load_file_into_translation(String const& file_path, Ref<Translation> translation) { + const Ref<FileAccess> file = FileAccess::open(file_path, FileAccess::ModeFlags::READ); + Error err = FileAccess::get_open_error(); + if (err != OK || file.is_null()) { + UtilityFunctions::push_error("Failed to load localisation file: ", file_path); + return err == OK ? FAILED : err; + } + int line_number = 0; + while (!file->eof_reached()) { + static const String delimeter = ";"; + const PackedStringArray line = file->get_csv_line(delimeter); + line_number++; + if (line.size() < 2 || line[0].is_empty() || line[1].is_empty()) { + if (!line[0].is_empty()) { + UtilityFunctions::push_warning("Key \"", line[0], "\" missing value on line ", line_number, " in file: ", file_path); + err = FAILED; + } else if (line.size() >= 2 && !line[1].is_empty()) { + UtilityFunctions::push_warning("Value \"", line[1], "\" missing key on line ", line_number, " in file: ", file_path); + err = FAILED; + } + continue; + } + translation->add_message(line[0], line[1].c_unescape()); + } + return err; +} + +Ref<Translation> LoadLocalisation::_get_translation(String const& locale) { + TranslationServer* server = TranslationServer::get_singleton(); + if (server == nullptr) { + UtilityFunctions::push_error("Failed to get TranslationServer singleton"); + return nullptr; + } + Ref<Translation> translation = server->get_translation_object(locale); + if (translation.is_null() || translation->get_locale() != locale) { + translation.instantiate(); + translation->set_locale(locale); + server->add_translation(translation); + } + return translation; +} + +Error LoadLocalisation::load_file(String const& file_path, String const& locale) { + return _load_file_into_translation(file_path, _get_translation(locale)); +} + +/* REQUIREMENTS + * FS-18, FS-24, FS-25 + */ +Error LoadLocalisation::load_locale_dir(String const& dir_path, String const& locale) { + if (!DirAccess::dir_exists_absolute(dir_path)) { + UtilityFunctions::push_error("Locale directory does not exist: ", dir_path); + return FAILED; + } + /* This will add the locale to the list of loaded locales even if it has no + * localisation files - this is useful for testing other aspects of localisation + * such as number formatting and text direction. To disable this behaviour and + * only show non-empty localisations, move the `_get_translation` call to after + * the `files.size()` check. + */ + const Ref<Translation> translation = _get_translation(locale); + const PackedStringArray files = DirAccess::get_files_at(dir_path); + if (files.size() < 1) { + UtilityFunctions::push_error("Locale directory does not contain any files: ", dir_path); + return FAILED; + } + Error err = OK; + for (String const& file_name : files) { + if (file_name.get_extension().to_lower() == "csv") { + if (_load_file_into_translation(dir_path.path_join(file_name), translation) != OK) + err = FAILED; + } + } + return err; +} + +/* REQUIREMENTS + * FS-23 + */ +Error LoadLocalisation::load_localisation_dir(String const& dir_path) { + if (!DirAccess::dir_exists_absolute(dir_path)) { + UtilityFunctions::push_error("Localisation directory does not exist: ", dir_path); + return FAILED; + } + PackedStringArray const dirs = DirAccess::get_directories_at(dir_path); + if (dirs.size() < 1) { + UtilityFunctions::push_error("Localisation directory does not contain any sub-directories: ", dir_path); + return FAILED; + } + TranslationServer* server = TranslationServer::get_singleton(); + if (server == nullptr) { + UtilityFunctions::push_error("Failed to get TranslationServer singleton"); + return FAILED; + } + Error err = OK; + for (String const& locale_name : dirs) { + if (locale_name != server->standardize_locale(locale_name)) + UtilityFunctions::push_error("Invalid locale directory name: ", locale_name); + else if (load_locale_dir(dir_path.path_join(locale_name), locale_name) == OK) + continue; + err = FAILED; + } + return err; +} diff --git a/extension/src/openvic-extension/LoadLocalisation.hpp b/extension/src/openvic-extension/LoadLocalisation.hpp new file mode 100644 index 0000000..04ec5c7 --- /dev/null +++ b/extension/src/openvic-extension/LoadLocalisation.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include <godot_cpp/classes/translation.hpp> + +namespace OpenVic { + class LoadLocalisation : public godot::Object { + + GDCLASS(LoadLocalisation, godot::Object) + + static LoadLocalisation* singleton; + + godot::Error _load_file_into_translation(godot::String const& file_path, godot::Ref<godot::Translation> translation); + godot::Ref<godot::Translation> _get_translation(godot::String const& locale); + + protected: + static void _bind_methods(); + + public: + static LoadLocalisation* get_singleton(); + + LoadLocalisation(); + ~LoadLocalisation(); + + godot::Error load_file(godot::String const& file_path, godot::String const& locale); + godot::Error load_locale_dir(godot::String const& dir_path, godot::String const& locale); + godot::Error load_localisation_dir(godot::String const& dir_path); + }; +} diff --git a/extension/src/openvic-extension/MapMesh.cpp b/extension/src/openvic-extension/MapMesh.cpp new file mode 100644 index 0000000..269360a --- /dev/null +++ b/extension/src/openvic-extension/MapMesh.cpp @@ -0,0 +1,150 @@ +#include "MapMesh.hpp" + +#include <godot_cpp/templates/vector.hpp> + +using namespace godot; +using namespace OpenVic; + +void MapMesh::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_aspect_ratio", "ratio"), &MapMesh::set_aspect_ratio); + ClassDB::bind_method(D_METHOD("get_aspect_ratio"), &MapMesh::get_aspect_ratio); + + ClassDB::bind_method(D_METHOD("set_repeat_proportion", "proportion"), &MapMesh::set_repeat_proportion); + ClassDB::bind_method(D_METHOD("get_repeat_proportion"), &MapMesh::get_repeat_proportion); + + ClassDB::bind_method(D_METHOD("set_subdivide_width", "divisions"), &MapMesh::set_subdivide_width); + ClassDB::bind_method(D_METHOD("get_subdivide_width"), &MapMesh::get_subdivide_width); + + ClassDB::bind_method(D_METHOD("set_subdivide_depth", "divisions"), &MapMesh::set_subdivide_depth); + ClassDB::bind_method(D_METHOD("get_subdivide_depth"), &MapMesh::get_subdivide_depth); + + ClassDB::bind_method(D_METHOD("get_core_aabb"), &MapMesh::get_core_aabb); + ClassDB::bind_method(D_METHOD("is_valid_uv_coord"), &MapMesh::is_valid_uv_coord); + + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "aspect_ratio", PROPERTY_HINT_NONE, "suffix:m"), "set_aspect_ratio", "get_aspect_ratio"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "repeat_proportion", PROPERTY_HINT_NONE, "suffix:m"), "set_repeat_proportion", "get_repeat_proportion"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "subdivide_width", PROPERTY_HINT_RANGE, "0,100,1,or_greater"), "set_subdivide_width", "get_subdivide_width"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "subdivide_depth", PROPERTY_HINT_RANGE, "0,100,1,or_greater"), "set_subdivide_depth", "get_subdivide_depth"); +} + +void MapMesh::_request_update() { + // Hack to trigger _update_lightmap_size and _request_update in PrimitiveMesh + set_add_uv2(get_add_uv2()); +} + +void MapMesh::set_aspect_ratio(const float ratio) { + aspect_ratio = ratio; + _request_update(); +} + +float MapMesh::get_aspect_ratio() const { + return aspect_ratio; +} + +void MapMesh::set_repeat_proportion(const float proportion) { + repeat_proportion = proportion; + _request_update(); +} + +float MapMesh::get_repeat_proportion() const { + return repeat_proportion; +} + +void MapMesh::set_subdivide_width(const int32_t divisions) { + subdivide_w = divisions > 0 ? divisions : 0; + _request_update(); +} + +int32_t MapMesh::get_subdivide_width() const { + return subdivide_w; +} + +void MapMesh::set_subdivide_depth(const int32_t divisions) { + subdivide_d = divisions > 0 ? divisions : 0; + _request_update(); +} + +int32_t MapMesh::get_subdivide_depth() const { + return subdivide_d; +} + +AABB MapMesh::get_core_aabb() const { + const Vector3 size { aspect_ratio, 0.0f, 1.0f }; + return AABB { size * -0.5f, size }; +} + +bool MapMesh::is_valid_uv_coord(godot::Vector2 const& uv) const { + return 0.0f <= uv.y && uv.y <= 1.0f; +} + +Array MapMesh::_create_mesh_array() const { + Array arr; + arr.resize(Mesh::ARRAY_MAX); + + const int32_t vertex_count = (subdivide_w + 2) * (subdivide_d + 2); + const int32_t indice_count = (subdivide_w + 1) * (subdivide_d + 1) * 6; + + PackedVector3Array points; + PackedVector3Array normals; + PackedFloat32Array tangents; + PackedVector2Array uvs; + PackedInt32Array indices; + + points.resize(vertex_count); + normals.resize(vertex_count); + tangents.resize(vertex_count * 4); + uvs.resize(vertex_count); + indices.resize(indice_count); + + static const Vector3 normal { 0.0f, 1.0f, 0.0f }; + const Size2 uv_size { 1.0f + 2.0f * repeat_proportion, 1.0f }; + const Size2 size { aspect_ratio * uv_size.x, uv_size.y }, start_pos = size * -0.5f; + + int32_t point_index = 0, thisrow = 0, prevrow = 0, indice_index = 0; + Vector2 subdivide_step { 1.0f / (subdivide_w + 1.0f), 1.0f / (subdivide_d + 1.0f) }; + Vector3 point { 0.0f, 0.0f, start_pos.y }; + Vector2 point_step = subdivide_step * size; + Vector2 uv {}, uv_step = subdivide_step * uv_size; + + for (int32_t j = 0; j <= subdivide_d + 1; ++j) { + point.x = start_pos.x; + uv.x = -repeat_proportion; + + for (int32_t i = 0; i <= subdivide_w + 1; ++i) { + points[point_index] = point; + normals[point_index] = normal; + tangents[point_index * 4 + 0] = 1.0f; + tangents[point_index * 4 + 1] = 0.0f; + tangents[point_index * 4 + 2] = 0.0f; + tangents[point_index * 4 + 3] = 1.0f; + uvs[point_index] = uv; + point_index++; + + if (i > 0 && j > 0) { + indices[indice_index + 0] = prevrow + i - 1; + indices[indice_index + 1] = prevrow + i; + indices[indice_index + 2] = thisrow + i - 1; + indices[indice_index + 3] = prevrow + i; + indices[indice_index + 4] = thisrow + i; + indices[indice_index + 5] = thisrow + i - 1; + indice_index += 6; + } + + point.x += point_step.x; + uv.x += uv_step.x; + } + + point.z += point_step.y; + uv.y += uv_step.y; + prevrow = thisrow; + thisrow = point_index; + } + + arr[Mesh::ARRAY_VERTEX] = points; + arr[Mesh::ARRAY_NORMAL] = normals; + arr[Mesh::ARRAY_TANGENT] = tangents; + arr[Mesh::ARRAY_TEX_UV] = uvs; + arr[Mesh::ARRAY_INDEX] = indices; + + return arr; +} diff --git a/extension/src/openvic-extension/MapMesh.hpp b/extension/src/openvic-extension/MapMesh.hpp new file mode 100644 index 0000000..38b208c --- /dev/null +++ b/extension/src/openvic-extension/MapMesh.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include <godot_cpp/classes/primitive_mesh.hpp> + +namespace OpenVic { + class MapMesh : public godot::PrimitiveMesh { + GDCLASS(MapMesh, godot::PrimitiveMesh) + + float aspect_ratio = 2.0f, repeat_proportion = 0.5f; + int32_t subdivide_w = 0, subdivide_d = 0; + + protected: + static void _bind_methods(); + void _request_update(); + + public: + void set_aspect_ratio(const float ratio); + float get_aspect_ratio() const; + + void set_repeat_proportion(const float proportion); + float get_repeat_proportion() const; + + void set_subdivide_width(const int32_t divisions); + int32_t get_subdivide_width() const; + + void set_subdivide_depth(const int32_t divisions); + int32_t get_subdivide_depth() const; + + godot::AABB get_core_aabb() const; + bool is_valid_uv_coord(godot::Vector2 const& uv) const; + + godot::Array _create_mesh_array() const override; + }; +} diff --git a/extension/src/openvic-extension/Utilities.cpp b/extension/src/openvic-extension/Utilities.cpp new file mode 100644 index 0000000..4ca6855 --- /dev/null +++ b/extension/src/openvic-extension/Utilities.cpp @@ -0,0 +1,106 @@ +#include "Utilities.hpp" + +#include <numbers> + +#include <godot_cpp/classes/resource_loader.hpp> +#include <godot_cpp/variant/utility_functions.hpp> + +using namespace godot; +using namespace OpenVic; + +Ref<Image> OpenVic::load_godot_image(String const& path) { + if (path.begins_with("res://")) { + ResourceLoader* loader = ResourceLoader::get_singleton(); + return loader ? loader->load(path) : nullptr; + } else { + return Image::load_from_file(path); + } +} + +// Get the polar coordinates of a pixel relative to the center +static Vector2 getPolar(Vector2 UVin, Vector2 center) { + Vector2 relcoord = (UVin - center); + float dist = relcoord.length(); + float theta = std::numbers::pi / 2 + atan2(relcoord.y, relcoord.x); + if (theta < 0.0f) theta += std::numbers::pi * 2; + return { dist, theta }; +} + +// From thebookofshaders, returns a gradient falloff +static inline float parabola(float base, float x, float k) { + return powf(base * x * (1.0 - x), k); +} + +static inline float parabola_shadow(float base, float x) { + return base * x * x; +} + +static Color pie_chart_fragment(Vector2 UV, float radius, Array const& stopAngles, Array const& colours, + Vector2 shadow_displacement, float shadow_tightness, float shadow_radius, float shadow_thickness, + Color trim_colour, float trim_size, float gradient_falloff, float gradient_base, + bool donut, bool donut_inner_trim, float donut_inner_radius) { + + Vector2 coords = getPolar(UV, { 0.5, 0.5 }); + float dist = coords.x; + float theta = coords.y; + + Vector2 shadow_polar = getPolar(UV, shadow_displacement); + float shadow_peak = radius + (radius - donut_inner_radius) / 2.0; + float shadow_gradient = shadow_thickness + parabola_shadow(shadow_tightness * -10.0, shadow_polar.x + shadow_peak - shadow_radius); + + // Inner hole of the donut => make it transparent + if (donut && dist <= donut_inner_radius) { + return { 0.1, 0.1, 0.1, shadow_gradient }; + } + // Inner trim + else if (donut && donut_inner_trim && dist <= donut_inner_radius + trim_size) { + return { trim_colour, 1.0 }; + } + // Interior + else if (dist <= radius - trim_size) { + Color col { 1.0f, 0.0f, 0.0f }; + for (int i = 0; i < stopAngles.size(); i++) { + if (theta <= float(stopAngles[i])) { + col = colours[i]; + break; + } + } + float gradient = parabola(gradient_base, dist, gradient_falloff); + return { col * (1.0 - gradient), 1.0 }; + } + // Outer trim + else if (dist <= radius) { + return { trim_colour, 1.0 }; + } + // Outside the circle + else { + return { 0.1, 0.1, 0.1, shadow_gradient }; + } +} + +void OpenVic::draw_pie_chart(Ref<Image> image, + Array const& stopAngles, Array const& colours, float radius, + Vector2 shadow_displacement, float shadow_tightness, float shadow_radius, float shadow_thickness, + Color trim_colour, float trim_size, float gradient_falloff, float gradient_base, + bool donut, bool donut_inner_trim, float donut_inner_radius) { + + ERR_FAIL_NULL_EDMSG(image, "Cannot draw pie chart to null image."); + const int32_t width = image->get_width(); + const int32_t height = image->get_height(); + ERR_FAIL_COND_EDMSG(width <= 0 || height <= 0, "Cannot draw pie chart to empty image."); + if (width != height) { + UtilityFunctions::push_warning("Drawing pie chart to non-square image: ", width, "x", height); + } + const int32_t size = std::min(width, height); + for (int32_t y = 0; y < size; ++y) { + for (int32_t x = 0; x < size; ++x) { + image->set_pixel(x, y, pie_chart_fragment( + { static_cast<float>(x) / static_cast<float>(size), + static_cast<float>(y) / static_cast<float>(size) }, + radius, stopAngles, colours, + shadow_displacement, shadow_tightness, shadow_radius, shadow_thickness, + trim_colour, trim_size, gradient_falloff, gradient_base, + donut, donut_inner_trim, donut_inner_radius)); + } + } +} diff --git a/extension/src/openvic-extension/Utilities.hpp b/extension/src/openvic-extension/Utilities.hpp new file mode 100644 index 0000000..32e7cb5 --- /dev/null +++ b/extension/src/openvic-extension/Utilities.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include <godot_cpp/classes/image.hpp> + +#include <openvic-simulation/types/Colour.hpp> + +#define ERR(x) ((x) ? OK : FAILED) + +namespace OpenVic { + + inline std::string godot_to_std_string(godot::String const& str) { + return str.ascii().get_data(); + } + + inline godot::String std_to_godot_string(std::string const& str) { + return str.c_str(); + } + + inline godot::Color to_godot_color(colour_t colour) { + return { colour_byte_to_float((colour >> 16) & 0xFF), colour_byte_to_float((colour >> 8) & 0xFF), colour_byte_to_float(colour & 0xFF) }; + } + + godot::Ref<godot::Image> load_godot_image(godot::String const& path); + + void draw_pie_chart(godot::Ref<godot::Image> image, + godot::Array const& stopAngles, godot::Array const& colours, float radius, + godot::Vector2 shadow_displacement, float shadow_tightness, float shadow_radius, float shadow_thickness, + godot::Color trim_colour, float trim_size, float gradient_falloff, float gradient_base, + bool donut, bool donut_inner_trim, float donut_inner_radius); +} diff --git a/extension/src/openvic-extension/register_types.cpp b/extension/src/openvic-extension/register_types.cpp new file mode 100644 index 0000000..273bb85 --- /dev/null +++ b/extension/src/openvic-extension/register_types.cpp @@ -0,0 +1,63 @@ +#include "register_types.hpp" + +#include <godot_cpp/classes/engine.hpp> + +#include "openvic-extension/Checksum.hpp" +#include "openvic-extension/GameSingleton.hpp" +#include "openvic-extension/LoadLocalisation.hpp" +#include "openvic-extension/MapMesh.hpp" + +using namespace godot; +using namespace OpenVic; + +static Checksum* _checksum; +static LoadLocalisation* _load_localisation; +static GameSingleton* _map_singleton; + +void initialize_openvic_types(ModuleInitializationLevel p_level) { + if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { + return; + } + + ClassDB::register_class<Checksum>(); + _checksum = memnew(Checksum); + Engine::get_singleton()->register_singleton("Checksum", Checksum::get_singleton()); + + ClassDB::register_class<LoadLocalisation>(); + _load_localisation = memnew(LoadLocalisation); + Engine::get_singleton()->register_singleton("LoadLocalisation", LoadLocalisation::get_singleton()); + + ClassDB::register_class<GameSingleton>(); + _map_singleton = memnew(GameSingleton); + Engine::get_singleton()->register_singleton("GameSingleton", GameSingleton::get_singleton()); + + ClassDB::register_class<MapMesh>(); +} + +void uninitialize_openvic_types(ModuleInitializationLevel p_level) { + if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { + return; + } + + Engine::get_singleton()->unregister_singleton("Checksum"); + memdelete(_checksum); + + Engine::get_singleton()->unregister_singleton("LoadLocalisation"); + memdelete(_load_localisation); + + Engine::get_singleton()->unregister_singleton("GameSingleton"); + memdelete(_map_singleton); +} + +extern "C" { + // Initialization. + GDExtensionBool GDE_EXPORT openvic_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, GDExtensionClassLibraryPtr p_library, GDExtensionInitialization* r_initialization) { + GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization); + + init_obj.register_initializer(initialize_openvic_types); + init_obj.register_terminator(uninitialize_openvic_types); + init_obj.set_minimum_library_initialization_level(MODULE_INITIALIZATION_LEVEL_SCENE); + + return init_obj.init(); + } +} diff --git a/extension/src/openvic-extension/register_types.hpp b/extension/src/openvic-extension/register_types.hpp new file mode 100644 index 0000000..dd24689 --- /dev/null +++ b/extension/src/openvic-extension/register_types.hpp @@ -0,0 +1,6 @@ +#pragma once + +#include <godot_cpp/godot.hpp> + +void initialize_openvic_types(godot::ModuleInitializationLevel); +void uninitialize_openvic_types(godot::ModuleInitializationLevel); |