aboutsummaryrefslogtreecommitdiff
path: root/extension/src/openvic-extension
diff options
context:
space:
mode:
Diffstat (limited to 'extension/src/openvic-extension')
-rw-r--r--extension/src/openvic-extension/Checksum.hpp37
-rw-r--r--extension/src/openvic-extension/GameSingleton.cpp395
-rw-r--r--extension/src/openvic-extension/GameSingleton.hpp158
-rw-r--r--extension/src/openvic-extension/LoadGameCompatibility.cpp110
-rw-r--r--extension/src/openvic-extension/LoadGameOpenVic.cpp132
-rw-r--r--extension/src/openvic-extension/LoadLocalisation.cpp136
-rw-r--r--extension/src/openvic-extension/LoadLocalisation.hpp28
-rw-r--r--extension/src/openvic-extension/MapMesh.cpp150
-rw-r--r--extension/src/openvic-extension/MapMesh.hpp34
-rw-r--r--extension/src/openvic-extension/Utilities.cpp106
-rw-r--r--extension/src/openvic-extension/Utilities.hpp30
-rw-r--r--extension/src/openvic-extension/register_types.cpp63
-rw-r--r--extension/src/openvic-extension/register_types.hpp6
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);