diff options
Diffstat (limited to 'extension/src')
30 files changed, 2171 insertions, 159 deletions
diff --git a/extension/src/Checksum.hpp b/extension/src/Checksum.hpp index ebdbd43..717910e 100644 --- a/extension/src/Checksum.hpp +++ b/extension/src/Checksum.hpp @@ -1,15 +1,13 @@ #pragma once -#include <godot_cpp/classes/object.hpp> #include <godot_cpp/core/class_db.hpp> -#include <godot_cpp/variant/utility_functions.hpp> namespace OpenVic2 { class Checksum : public godot::Object { GDCLASS(Checksum, godot::Object) //BEGIN BOILERPLATE - static Checksum* _checksum; + inline static Checksum* _checksum = nullptr; protected: static void _bind_methods() { @@ -33,6 +31,4 @@ namespace OpenVic2 { return godot::String("1234abcd"); } }; - - Checksum* Checksum::_checksum = nullptr; -}
\ No newline at end of file +} diff --git a/extension/src/GameSingleton.cpp b/extension/src/GameSingleton.cpp new file mode 100644 index 0000000..3811dea --- /dev/null +++ b/extension/src/GameSingleton.cpp @@ -0,0 +1,463 @@ +#include "GameSingleton.hpp" + +#include <godot_cpp/variant/utility_functions.hpp> +#include <godot_cpp/classes/file_access.hpp> +#include <godot_cpp/classes/json.hpp> + +#include "openvic2/Logger.hpp" + +using namespace godot; +using namespace OpenVic2; + +#define ERR(x) ((x) == SUCCESS ? OK : FAILED) + +GameSingleton* GameSingleton::singleton = nullptr; + +void GameSingleton::_bind_methods() { + ClassDB::bind_method(D_METHOD("load_province_identifier_file", "file_path"), &GameSingleton::load_province_identifier_file); + ClassDB::bind_method(D_METHOD("load_water_province_file", "file_path"), &GameSingleton::load_water_province_file); + ClassDB::bind_method(D_METHOD("load_region_file", "file_path"), &GameSingleton::load_region_file); + ClassDB::bind_method(D_METHOD("load_province_shape_file", "file_path"), &GameSingleton::load_province_shape_file); + ClassDB::bind_method(D_METHOD("setup"), &GameSingleton::setup); + + 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_province_index_images"), &GameSingleton::get_province_index_images); + ClassDB::bind_method(D_METHOD("get_province_colour_image"), &GameSingleton::get_province_colour_image); + + ClassDB::bind_method(D_METHOD("update_colour_image"), &GameSingleton::update_colour_image); + 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("expand_building", "province_index", "building_type_identifier"), &GameSingleton::expand_building); + + 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")); +} + +GameSingleton* GameSingleton::get_singleton() { + return singleton; +} + +/* REQUIREMENTS: + * MAP-21, MAP-25 + */ +GameSingleton::GameSingleton() : game_manager{ [this]() { emit_signal("state_updated"); } } { + ERR_FAIL_COND(singleton != nullptr); + singleton = this; + + Logger::set_info_func([](std::string&& str) { UtilityFunctions::print(str.c_str()); }); + Logger::set_error_func([](std::string&& str) { UtilityFunctions::push_error(str.c_str()); }); + + using mapmode_t = std::pair<std::string, Mapmode::colour_func_t>; + const std::vector<mapmode_t> mapmodes = { + { "mapmode_province", [](Map const&, Province const& province) -> Province::colour_t { return province.get_colour(); } }, + { "mapmode_region", [](Map const&, Province const& province) -> Province::colour_t { + Region const* region = province.get_region(); + if (region != nullptr) return region->get_colour(); + return province.get_colour(); + } }, + { "mapmode_terrain", [](Map const&, Province const& province) -> Province::colour_t { + return province.is_water() ? 0x4287F5 : 0x0D7017; + } }, + { "mapmode_index", [](Map const& map, Province const& province) -> Province::colour_t { + const uint8_t f = static_cast<float>(province.get_index()) / static_cast<float>(map.get_province_count()) * 255.0f; + return (f << 16) | (f << 8) | f; + } } + }; + for (mapmode_t const& mapmode : mapmodes) + game_manager.map.add_mapmode(mapmode.first, mapmode.second); + game_manager.map.lock_mapmodes(); + + using building_type_t = std::tuple<std::string, Building::level_t, Timespan>; + const std::vector<building_type_t> building_types = { + { "building_fort", 4, 8 }, { "building_naval_base", 6, 15 }, { "building_railroad", 5, 10 } + }; + for (building_type_t const& type : building_types) + game_manager.building_manager.add_building_type(std::get<0>(type), std::get<1>(type), std::get<2>(type)); + game_manager.building_manager.lock_building_types(); + +} + +GameSingleton::~GameSingleton() { + ERR_FAIL_COND(singleton != this); + singleton = nullptr; +} + +static Error load_json_file(String const& file_description, String const& file_path, Variant& result) { + result.clear(); + UtilityFunctions::print("Loading ", file_description, " file: ", file_path); + 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 ", file_description, " file: ", file_path); + return err == OK ? FAILED : err; + } + const String json_string = file->get_as_text(); + JSON json; + err = json.parse(json_string); + if (err != OK) { + UtilityFunctions::push_error("Failed to parse ", file_description, " file as JSON: ", file_path, + "\nError at line ", json.get_error_line(), ": ", json.get_error_message()); + return err; + } + result = json.get_data(); + return err; +} + +using parse_json_entry_func_t = std::function<godot::Error (godot::String const&, godot::Variant const&)>; + +static Error parse_json_dictionary_file(String const& file_description, String const& file_path, + String const& identifier_prefix, parse_json_entry_func_t parse_entry) { + Variant json_var; + Error err = load_json_file(file_description, file_path, json_var); + if (err != OK) return err; + const Variant::Type type = json_var.get_type(); + if (type != Variant::DICTIONARY) { + UtilityFunctions::push_error("Invalid ", file_description, " JSON: root has type ", + Variant::get_type_name(type), " (expected Dictionary)"); + return FAILED; + } + Dictionary const& dict = json_var; + const Array identifiers = dict.keys(); + for (int64_t idx = 0; idx < identifiers.size(); ++idx) { + String const& identifier = identifiers[idx]; + Variant const& entry = dict[identifier]; + if (identifier.is_empty()) { + UtilityFunctions::push_error("Empty identifier in ", file_description, " file with entry: ", entry); + err = FAILED; + continue; + } + if (!identifier.begins_with(identifier_prefix)) + UtilityFunctions::push_warning("Identifier in ", file_description, " file missing \"", identifier_prefix, "\" prefix: ", identifier); + if (parse_entry(identifier, entry) != OK) err = FAILED; + } + return err; +} + +Error GameSingleton::_parse_province_identifier_entry(String const& identifier, Variant const& entry) { + const Variant::Type type = entry.get_type(); + Province::colour_t colour = Province::NULL_COLOUR; + if (type == Variant::ARRAY) { + Array const& colour_array = entry; + if (colour_array.size() == 3) { + for (int jdx = 0; jdx < 3; ++jdx) { + Variant const& var = colour_array[jdx]; + if (var.get_type() != Variant::FLOAT) { + colour = Province::NULL_COLOUR; + break; + } + const double colour_double = var; + if (std::trunc(colour_double) != colour_double) { + colour = Province::NULL_COLOUR; + break; + } + const int64_t colour_int = static_cast<int64_t>(colour_double); + if (colour_int < 0 || colour_int > 255) { + colour = Province::NULL_COLOUR; + break; + } + colour = (colour << 8) | colour_int; + } + } + } else if (type == Variant::STRING) { + String const& colour_string = entry; + if (colour_string.is_valid_hex_number()) { + const int64_t colour_int = colour_string.hex_to_int(); + if (0 <= colour_int && colour_int <= 0xFFFFFF) + colour = colour_int; + } + } + if (colour == Province::NULL_COLOUR) { + UtilityFunctions::push_error("Invalid colour for province identifier \"", identifier, "\": ", entry); + return FAILED; + } + return ERR(game_manager.map.add_province(identifier.utf8().get_data(), colour)); +} + +Error GameSingleton::load_province_identifier_file(String const& file_path) { + const Error err = parse_json_dictionary_file("province identifier", file_path, "prov_", + [this](String const& identifier, Variant const& entry) -> Error { + return _parse_province_identifier_entry(identifier, entry); + }); + game_manager.map.lock_provinces(); + return err; +} + +Error GameSingleton::_parse_region_entry(String const& identifier, Variant const& entry) { + Error err = OK; + Variant::Type type = entry.get_type(); + std::vector<std::string> province_identifiers; + if (type == Variant::ARRAY) { + Array const& province_array = entry; + for (int64_t idx = 0; idx < province_array.size(); ++idx) { + Variant const& province_var = province_array[idx]; + type = province_var.get_type(); + if (type == Variant::STRING) { + String const& province_string = province_var; + province_identifiers.push_back(province_string.utf8().get_data()); + } else { + UtilityFunctions::push_error("Invalid province identifier for region \"", identifier, "\": ", entry); + err = FAILED; + } + } + } + if (province_identifiers.empty()) { + UtilityFunctions::push_error("Invalid province list for region \"", identifier, "\": ", entry); + return FAILED; + } + return ERR(game_manager.map.add_region(identifier.utf8().get_data(), province_identifiers)); +} + +Error GameSingleton::load_region_file(String const& file_path) { + const Error err = parse_json_dictionary_file("region", file_path, "region_", + [this](String const& identifier, Variant const& entry) -> Error { + return _parse_region_entry(identifier, entry); + }); + game_manager.map.lock_regions(); + return err; +} + +Error GameSingleton::load_province_shape_file(String const& file_path) { + if (province_index_image[0].is_valid()) { + UtilityFunctions::push_error("Province shape file has already been loaded, cannot load: ", file_path); + return FAILED; + } + Ref<Image> province_shape_image; + province_shape_image.instantiate(); + Error err = province_shape_image->load(file_path); + if (err != OK) { + UtilityFunctions::push_error("Failed to load province shape file: ", file_path); + return err; + } + const int32_t width = province_shape_image->get_width(); + const int32_t height = province_shape_image->get_height(); + if (width < 1 || height < 1) { + UtilityFunctions::push_error("Invalid dimensions (", width, "x", height, ") for province shape file: ", file_path); + err = FAILED; + } + if (width % image_width_divide != 0) { + UtilityFunctions::push_error("Invalid width ", width, " (must be divisible by ", image_width_divide, ") for province shape file: ", file_path); + err = FAILED; + } + static constexpr Image::Format expected_format = Image::FORMAT_RGB8; + const Image::Format format = province_shape_image->get_format(); + if (format != expected_format) { + UtilityFunctions::push_error("Invalid format (", format, ", should be ", expected_format, ") for province shape file: ", file_path); + err = FAILED; + } + if (err != OK) return err; + err = ERR(game_manager.map.generate_province_index_image(width, height, province_shape_image->get_data().ptr())); + + std::vector<Province::index_t> const& province_index_data = game_manager.map.get_province_index_image(); + const int32_t divided_width = width / image_width_divide; + for (int32_t i = 0; i < image_width_divide; ++i) { + PackedByteArray index_data_array; + index_data_array.resize(divided_width * height * sizeof(Province::index_t)); + for (int32_t y = 0; y < height; ++y) + memcpy(index_data_array.ptrw() + y * divided_width * sizeof(Province::index_t), + province_index_data.data() + y * width + i * divided_width, + divided_width * sizeof(Province::index_t)); + province_index_image[i] = Image::create_from_data(divided_width, height, false, Image::FORMAT_RG8, index_data_array); + if (province_index_image[i].is_null()) { + UtilityFunctions::push_error("Failed to create province ID image #", i); + err = FAILED; + } + } + + if (update_colour_image() != OK) err = FAILED; + + return err; +} + +godot::Error GameSingleton::setup() { + return ERR(game_manager.setup()); +} + +Error GameSingleton::load_water_province_file(String const& file_path) { + Variant json_var; + Error err = load_json_file("water province", file_path, json_var); + if (err != OK) return err; + Variant::Type type = json_var.get_type(); + if (type != Variant::ARRAY) { + UtilityFunctions::push_error("Invalid water province JSON: root has type ", + Variant::get_type_name(type), " (expected Array)"); + err = FAILED; + } else { + Array const& array = json_var; + for (int64_t idx = 0; idx < array.size(); ++idx) { + Variant const& entry = array[idx]; + type = entry.get_type(); + if (type != Variant::STRING) { + UtilityFunctions::push_error("Invalid water province identifier: ", entry); + err = FAILED; + continue; + } + String const& identifier = entry; + if (game_manager.map.set_water_province(identifier.utf8().get_data()) != SUCCESS) + err = FAILED; + } + } + game_manager.map.lock_water_provinces(); + return err; +} + +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); +} + +#define KEY(x) static const StringName x##_key = #x; +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 {}; + KEY(province) KEY(region) KEY(life_rating) KEY(buildings) + Dictionary ret; + + ret[province_key] = province->get_identifier().c_str(); + + Region const* region = province->get_region(); + if (region != nullptr) ret[region_key] = region->get_identifier().c_str(); + + ret[life_rating_key] = province->get_life_rating(); + + 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) { + KEY(building) KEY(level) KEY(expansion_state) KEY(start_date) KEY(end_date) KEY(expansion_progress) + + Dictionary building_dict; + Building const& building = buildings[idx]; + building_dict[building_key] = building.get_identifier().c_str(); + building_dict[level_key] = static_cast<int32_t>(building.get_level()); + building_dict[expansion_state_key] = static_cast<int32_t>(building.get_expansion_state()); + building_dict[start_date_key] = static_cast<std::string>(building.get_start_date()).c_str(); + building_dict[end_date_key] = static_cast<std::string>(building.get_end_date()).c_str(); + building_dict[expansion_progress_key] = building.get_expansion_progress(); + + buildings_array[idx] = building_dict; + } + ret[buildings_key] = buildings_array; + } + return ret; +} +#undef KEY + +int32_t GameSingleton::get_width() const { + return game_manager.map.get_width(); +} + +int32_t GameSingleton::get_height() const { + return game_manager.map.get_height(); +} + +Array GameSingleton::get_province_index_images() const { + Array ret; + for (int i = 0; i < image_width_divide; ++i) + ret.append(province_index_image[i]); + return ret; +} + +Ref<Image> GameSingleton::get_province_colour_image() const { + return province_colour_image; +} + +Error GameSingleton::update_colour_image() { + static PackedByteArray colour_data_array; + static constexpr int64_t colour_data_array_size = (Province::MAX_INDEX + 1) * 4; + colour_data_array.resize(colour_data_array_size); + + Error err = OK; + if (game_manager.map.generate_mapmode_colours(mapmode_index, colour_data_array.ptrw()) != SUCCESS) + 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(); + province_colour_image->set_data(PROVINCE_INDEX_SQRT, PROVINCE_INDEX_SQRT, + false, Image::FORMAT_RGBA8, colour_data_array); + if (province_colour_image.is_null()) { + UtilityFunctions::push_error("Failed to update province colour image"); + return FAILED; + } + 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 mapmode->get_identifier().c_str(); + return String{}; +} + +Error GameSingleton::set_mapmode(godot::String const& identifier) { + Mapmode const* mapmode = game_manager.map.get_mapmode_by_identifier(identifier.utf8().get_data()); + if (mapmode == nullptr) { + UtilityFunctions::push_error("Failed to set mapmode to: ", identifier); + return FAILED; + } + mapmode_index = mapmode->get_index(); + return OK; +} + +Error GameSingleton::expand_building(int32_t province_index, String const& building_type_identifier) { + if (game_manager.expand_building(province_index, building_type_identifier.utf8().get_data()) != SUCCESS) { + UtilityFunctions::push_error("Failed to expand ", building_type_identifier, " at province index ", province_index); + return FAILED; + } + return OK; +} + +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 static_cast<std::string>(game_manager.get_today()).c_str(); +} + +void GameSingleton::try_tick() { + game_manager.clock.conditionallyAdvanceGame(); +} diff --git a/extension/src/GameSingleton.hpp b/extension/src/GameSingleton.hpp new file mode 100644 index 0000000..d9879ef --- /dev/null +++ b/extension/src/GameSingleton.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include <functional> + +#include <godot_cpp/classes/image.hpp> + +#include "openvic2/GameManager.hpp" + +namespace OpenVic2 { + class GameSingleton : public godot::Object { + GDCLASS(GameSingleton, godot::Object) + + static GameSingleton* singleton; + + GameManager game_manager; + + static constexpr int image_width_divide = 2; + godot::Ref<godot::Image> province_index_image[image_width_divide], province_colour_image; + Mapmode::index_t mapmode_index = 0; + + godot::Error _parse_province_identifier_entry(godot::String const& identifier, godot::Variant const& entry); + godot::Error _parse_region_entry(godot::String const& identifier, godot::Variant const& entry); + void _tick(); + protected: + static void _bind_methods(); + + public: + static GameSingleton* get_singleton(); + + GameSingleton(); + ~GameSingleton(); + + godot::Error load_province_identifier_file(godot::String const& file_path); + godot::Error load_water_province_file(godot::String const& file_path); + godot::Error load_region_file(godot::String const& file_path); + godot::Error load_province_shape_file(godot::String const& file_path); + godot::Error setup(); + + int32_t get_province_index_from_uv_coords(godot::Vector2 const& coords) const; + godot::Dictionary get_province_info_from_index(int32_t index) const; + int32_t get_width() const; + int32_t get_height() const; + godot::Array get_province_index_images() const; + godot::Ref<godot::Image> get_province_colour_image() const; + + godot::Error update_colour_image(); + int32_t get_mapmode_count() const; + godot::String get_mapmode_identifier(int32_t index) const; + godot::Error set_mapmode(godot::String const& identifier); + + godot::Error expand_building(int32_t province_index, godot::String const& building_type_identifier); + + 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/LoadLocalisation.cpp b/extension/src/LoadLocalisation.cpp index c95c08b..8698bb2 100644 --- a/extension/src/LoadLocalisation.cpp +++ b/extension/src/LoadLocalisation.cpp @@ -8,7 +8,7 @@ using namespace godot; using namespace OpenVic2; -LoadLocalisation *LoadLocalisation::singleton = nullptr; +LoadLocalisation* LoadLocalisation::singleton = nullptr; void LoadLocalisation::_bind_methods() { ClassDB::bind_method(D_METHOD("load_file", "file_path", "locale"), &LoadLocalisation::load_file); @@ -16,7 +16,7 @@ void LoadLocalisation::_bind_methods() { ClassDB::bind_method(D_METHOD("load_localisation_dir", "dir_path"), &LoadLocalisation::load_localisation_dir); } -LoadLocalisation *LoadLocalisation::get_singleton() { +LoadLocalisation* LoadLocalisation::get_singleton() { return singleton; } @@ -54,7 +54,7 @@ Error LoadLocalisation::_load_file_into_translation(String const& file_path, Ref } Ref<Translation> LoadLocalisation::_get_translation(String const& locale) { - TranslationServer *server = TranslationServer::get_singleton(); + TranslationServer* server = TranslationServer::get_singleton(); Ref<Translation> translation = server->get_translation_object(locale); if (translation.is_null() || translation->get_locale() != locale) { translation.instantiate(); @@ -93,7 +93,7 @@ Error LoadLocalisation::load_locale_dir(String const& dir_path, String const& lo */ Error LoadLocalisation::load_localisation_dir(String const& dir_path) { if (DirAccess::dir_exists_absolute(dir_path)) { - TranslationServer *server = TranslationServer::get_singleton(); + TranslationServer* server = TranslationServer::get_singleton(); Error err = OK; for (String const& locale_name : DirAccess::get_directories_at(dir_path)) { if (locale_name == server->standardize_locale(locale_name)) { diff --git a/extension/src/LoadLocalisation.hpp b/extension/src/LoadLocalisation.hpp index 90f3158..49c0313 100644 --- a/extension/src/LoadLocalisation.hpp +++ b/extension/src/LoadLocalisation.hpp @@ -1,14 +1,13 @@ #pragma once -#include <godot_cpp/core/class_db.hpp> #include <godot_cpp/classes/translation.hpp> namespace OpenVic2 { - class LoadLocalisation : public godot::Object - { + class LoadLocalisation : public godot::Object { + GDCLASS(LoadLocalisation, godot::Object) - static LoadLocalisation *singleton; + 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); @@ -17,7 +16,7 @@ namespace OpenVic2 { static void _bind_methods(); public: - static LoadLocalisation *get_singleton(); + static LoadLocalisation* get_singleton(); LoadLocalisation(); ~LoadLocalisation(); diff --git a/extension/src/MapMesh.cpp b/extension/src/MapMesh.cpp new file mode 100644 index 0000000..91c7611 --- /dev/null +++ b/extension/src/MapMesh.cpp @@ -0,0 +1,150 @@ +#include "MapMesh.hpp" + +#include <godot_cpp/templates/vector.hpp> + +using namespace godot; +using namespace OpenVic2; + +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 int divisions) { + subdivide_w = divisions > 0 ? divisions : 0; + _request_update(); +} + +int MapMesh::get_subdivide_width() const { + return subdivide_w; +} + +void MapMesh::set_subdivide_depth(const int divisions) { + subdivide_d = divisions > 0 ? divisions : 0; + _request_update(); +} + +int 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 int vertex_count = (subdivide_w + 2) * (subdivide_d + 2); + const int 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; + + int 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 (int j = 0; j <= subdivide_d + 1; ++j) { + point.x = start_pos.x; + uv.x = -repeat_proportion; + + for (int 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/MapMesh.hpp b/extension/src/MapMesh.hpp new file mode 100644 index 0000000..d8727cf --- /dev/null +++ b/extension/src/MapMesh.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include <godot_cpp/classes/primitive_mesh.hpp> + +namespace OpenVic2 { + class MapMesh : public godot::PrimitiveMesh { + GDCLASS(MapMesh, godot::PrimitiveMesh) + + float aspect_ratio = 2.0f, repeat_proportion = 0.5f; + int 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 int divisions); + int get_subdivide_width() const; + + void set_subdivide_depth(const int divisions); + int 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/Simulation.hpp b/extension/src/Simulation.hpp deleted file mode 100644 index 8959310..0000000 --- a/extension/src/Simulation.hpp +++ /dev/null @@ -1,52 +0,0 @@ -#pragma once - -#include <godot_cpp/classes/object.hpp> -#include <godot_cpp/core/class_db.hpp> -#include <godot_cpp/variant/utility_functions.hpp> -#include <vector> - -namespace OpenVic2 { - class Simulation : public godot::Object { - GDCLASS(Simulation, godot::Object) - std::vector<uint64_t> exampleProvinces; - - //BEGIN BOILERPLATE - static Simulation* _simulation; - - protected: - static void _bind_methods() { - godot::ClassDB::bind_method(godot::D_METHOD("conductSimulationStep"), &Simulation::conductSimulationStep); - godot::ClassDB::bind_method(godot::D_METHOD("queryProvinceSize"), &Simulation::queryProvinceSize); - } - - public: - inline static Simulation* get_singleton() { return _simulation; } - - inline Simulation() { - ERR_FAIL_COND(_simulation != nullptr); - _simulation = this; - - exampleProvinces.resize(10, 1); - } - inline ~Simulation() { - ERR_FAIL_COND(_simulation != this); - _simulation = nullptr; - } - //END BOILERPLATE - - inline void conductSimulationStep() { - for (uint64_t x = 0; x < exampleProvinces.size(); x++) { - exampleProvinces[x] += (x + 1); - } - } - - inline uint64_t queryProvinceSize(uint64_t provinceID) { - if (provinceID >= exampleProvinces.size()) { - return 0; - } - return exampleProvinces[provinceID]; - } - }; - - Simulation* Simulation::_simulation = nullptr; -} diff --git a/extension/src/TestSingleton.cpp b/extension/src/TestSingleton.cpp deleted file mode 100644 index 0855a30..0000000 --- a/extension/src/TestSingleton.cpp +++ /dev/null @@ -1,36 +0,0 @@ -#include "TestSingleton.hpp" - -#include <godot_cpp/core/class_db.hpp> -#include <godot_cpp/variant/utility_functions.hpp> - -using namespace godot; -using namespace OpenVic2; - -TestSingleton *TestSingleton::singleton = nullptr; - -void TestSingleton::_bind_methods() -{ - ClassDB::bind_method(D_METHOD("hello_singleton"), &TestSingleton::hello_singleton); -} - -TestSingleton *TestSingleton::get_singleton() -{ - return singleton; -} - -TestSingleton::TestSingleton() -{ - ERR_FAIL_COND(singleton != nullptr); - singleton = this; -} - -TestSingleton::~TestSingleton() -{ - ERR_FAIL_COND(singleton != this); - singleton = nullptr; -} - -void TestSingleton::hello_singleton() -{ - UtilityFunctions::print("Hello GDExtension Singleton!"); -}
\ No newline at end of file diff --git a/extension/src/TestSingleton.hpp b/extension/src/TestSingleton.hpp deleted file mode 100644 index de27589..0000000 --- a/extension/src/TestSingleton.hpp +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#include <godot_cpp/classes/object.hpp> -#include <godot_cpp/core/class_db.hpp> - -namespace OpenVic2 { - class TestSingleton : public godot::Object - { - GDCLASS(TestSingleton, godot::Object) - - static TestSingleton *singleton; - - protected: - static void _bind_methods(); - - public: - static TestSingleton *get_singleton(); - - TestSingleton(); - ~TestSingleton(); - - void hello_singleton(); - }; -}
\ No newline at end of file diff --git a/extension/src/openvic2/Date.cpp b/extension/src/openvic2/Date.cpp new file mode 100644 index 0000000..bb891fd --- /dev/null +++ b/extension/src/openvic2/Date.cpp @@ -0,0 +1,161 @@ +#include "openvic2/Date.hpp" + +#include <cctype> +#include <algorithm> + +#include "openvic2/Logger.hpp" + +using namespace OpenVic2; + +Timespan::Timespan(day_t value) : days{value} {} + +bool Timespan::operator<(Timespan other) const { return days < other.days; }; +bool Timespan::operator>(Timespan other) const { return days > other.days; }; +bool Timespan::operator<=(Timespan other) const { return days <= other.days; }; +bool Timespan::operator>=(Timespan other) const { return days >= other.days; }; +bool Timespan::operator==(Timespan other) const { return days == other.days; }; +bool Timespan::operator!=(Timespan other) const { return days != other.days; }; + +Timespan Timespan::operator+(Timespan other) const { return days + other.days; } + +Timespan Timespan::operator-(Timespan other) const { return days - other.days; } + +Timespan Timespan::operator*(day_t factor) const { return days * factor; } + +Timespan Timespan::operator/(day_t factor) const { return days / factor; } + +Timespan& Timespan::operator+=(Timespan other) { + days += other.days; + return *this; +} + +Timespan& Timespan::operator-=(Timespan other) { + days -= other.days; + return *this; +} + +Timespan& Timespan::operator++() { + days++; + return *this; +} + +Timespan Timespan::operator++(int) { + Timespan old = *this; + ++(*this); + return old; +} + +Timespan::operator day_t() const { + return days; +} + +Timespan::operator double() const { + return days; +} + +Timespan::operator std::string() const { + return std::to_string(days); +} + +std::ostream& OpenVic2::operator<<(std::ostream& out, Timespan timespan) { + return out << static_cast<std::string>(timespan); +} + +Timespan Date::_dateToTimespan(year_t year, month_t month, day_t day) { + month = std::clamp<month_t>(month, 1, MONTHS_IN_YEAR); + day = std::clamp<day_t>(day, 1, DAYS_IN_MONTH[month - 1]); + return year * DAYS_IN_YEAR + DAYS_UP_TO_MONTH[month - 1] + day - 1; +} + +Date::Date(Timespan total_days) : timespan{ total_days } { + if (timespan < 0) { + Logger::error("Invalid timespan for date: ", timespan, " (cannot be negative)"); + timespan = 0; + } +} + +Date::Date(year_t year, month_t month, day_t day) : timespan{ _dateToTimespan(year, month, day) } {} + +Date::year_t Date::getYear() const { + return static_cast<Timespan::day_t>(timespan) / DAYS_IN_YEAR; +} + +Date::month_t Date::getMonth() const { + return ((static_cast<Timespan::day_t>(timespan) % DAYS_IN_YEAR) / 32) + 1; +} + +Date::day_t Date::getDay() const { + const Timespan::day_t days_in_year = static_cast<Timespan::day_t>(timespan) % DAYS_IN_YEAR; + return days_in_year - DAYS_UP_TO_MONTH[days_in_year / 32] + 1; +} + + +bool Date::operator<(Date other) const { return timespan < other.timespan; }; +bool Date::operator>(Date other) const { return timespan > other.timespan; }; +bool Date::operator<=(Date other) const { return timespan <= other.timespan; }; +bool Date::operator>=(Date other) const { return timespan >= other.timespan; }; +bool Date::operator==(Date other) const { return timespan == other.timespan; }; +bool Date::operator!=(Date other) const { return timespan != other.timespan; }; + +Date Date::operator+(Timespan other) const { return timespan + other; } + +Timespan Date::operator-(Date other) const { return timespan - other.timespan; } + +Date& Date::operator+=(Timespan other) { + timespan += other; + return *this; +} + +Date& Date::operator-=(Timespan other) { + timespan -= other; + return *this; +} + +Date& Date::operator++() { + timespan++; + return *this; +} + +Date Date::operator++(int) { + Date old = *this; + ++(*this); + return old; +} + +Date::operator std::string() const { + std::stringstream ss; + ss << *this; + return ss.str(); +} + +std::ostream& OpenVic2::operator<<(std::ostream& out, Date date) { + return out << (int) date.getYear() << '.' << (int) date.getMonth() << '.' << (int) date.getDay(); +} + +// Parsed from string of the form YYYY.MM.DD +Date Date::from_string(std::string const& date) { + year_t year = 0; + month_t month = 1; + day_t day = 1; + + size_t first_pos = 0; + while (first_pos < date.length() && std::isdigit(date[first_pos++])); + year = atoi(date.substr(0, first_pos).c_str()); + if (first_pos < date.length()) { + if (date[first_pos] == '.') { + size_t second_pos = first_pos + 1; + while (second_pos < date.length() && std::isdigit(date[second_pos++])); + month = atoi(date.substr(first_pos, second_pos - first_pos).c_str()); + if (second_pos < date.length()) { + if (date[second_pos] == '.') { + size_t third_pos = second_pos + 1; + while (third_pos < date.length() && std::isdigit(date[third_pos++])); + day = atoi(date.substr(second_pos, third_pos - second_pos).c_str()); + if (third_pos < date.length()) + Logger::error("Unexpected string \"", date.substr(third_pos), "\" at the end of date ", date); + } else Logger::error("Unexpected character \"", date[second_pos], "\" in date ", date); + } + } else Logger::error("Unexpected character \"", date[first_pos], "\" in date ", date); + } + return _dateToTimespan(year, month, day); +}; diff --git a/extension/src/openvic2/Date.hpp b/extension/src/openvic2/Date.hpp new file mode 100644 index 0000000..b19602b --- /dev/null +++ b/extension/src/openvic2/Date.hpp @@ -0,0 +1,83 @@ +#pragma once + +#include <cstdint> +#include <string> +#include <ostream> + +namespace OpenVic2 { + // A relative period between points in time, measured in days + struct Timespan { + using day_t = int64_t; + private: + day_t days; + public: + Timespan(day_t value = 0); + + bool operator<(Timespan other) const; + bool operator>(Timespan other) const; + bool operator<=(Timespan other) const; + bool operator>=(Timespan other) const; + bool operator==(Timespan other) const; + bool operator!=(Timespan other) const; + + Timespan operator+(Timespan other) const; + Timespan operator-(Timespan other) const; + Timespan operator*(day_t factor) const; + Timespan operator/(day_t factor) const; + Timespan& operator+=(Timespan other); + Timespan& operator-=(Timespan other); + Timespan& operator++(); + Timespan operator++(int); + + explicit operator day_t() const; + explicit operator double() const; + explicit operator std::string() const; + }; + std::ostream& operator<< (std::ostream& out, Timespan timespan); + + // Represents an in-game date + // Note: Current implementation does not account for leap-years, or dates before Year 0 + struct Date { + using year_t = uint16_t; + using month_t = uint8_t; + using day_t = uint8_t; + + static constexpr Timespan::day_t MONTHS_IN_YEAR = 12; + static constexpr Timespan::day_t DAYS_IN_YEAR = 365; + static constexpr Timespan::day_t DAYS_IN_MONTH[MONTHS_IN_YEAR] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; + static constexpr Timespan::day_t DAYS_UP_TO_MONTH[MONTHS_IN_YEAR] = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 }; + private: + // Number of days since Jan 1st, Year 0 + Timespan timespan; + + static Timespan _dateToTimespan(year_t year, month_t month, day_t day); + public: + // The Timespan is considered to be the number of days since Jan 1st, Year 0 + Date(Timespan total_days); + // Year month day specification + Date(year_t year = 0, month_t month = 1, day_t day = 1); + + year_t getYear() const; + month_t getMonth() const; + day_t getDay() const; + + bool operator<(Date other) const; + bool operator>(Date other) const; + bool operator<=(Date other) const; + bool operator>=(Date other) const; + bool operator==(Date other) const; + bool operator!=(Date other) const; + + Date operator+(Timespan other) const; + Timespan operator-(Date other) const; + Date& operator+=(Timespan other); + Date& operator-=(Timespan other); + Date& operator++(); + Date operator++(int); + + explicit operator std::string() const; + // Parsed from string of the form YYYY.MM.DD + static Date from_string(std::string const& date); + }; + std::ostream& operator<< (std::ostream& out, Date date); +} diff --git a/extension/src/openvic2/GameAdvancementHook.cpp b/extension/src/openvic2/GameAdvancementHook.cpp new file mode 100644 index 0000000..4b9bc25 --- /dev/null +++ b/extension/src/openvic2/GameAdvancementHook.cpp @@ -0,0 +1,72 @@ +#include "openvic2/GameAdvancementHook.hpp" + +using namespace OpenVic2; + +const std::vector<std::chrono::milliseconds> GameAdvancementHook::GAME_SPEEDS = { + std::chrono::milliseconds{ 4000 }, + std::chrono::milliseconds{ 3000 }, + std::chrono::milliseconds{ 2000 }, + std::chrono::milliseconds{ 1000 }, + std::chrono::milliseconds{ 100 }, + std::chrono::milliseconds{ 1 } }; + +GameAdvancementHook::GameAdvancementHook(AdvancementFunction tickFunction, RefreshFunction updateFunction, bool startPaused, speed_t startingSpeed) + : triggerFunction{ tickFunction }, refreshFunction{ updateFunction }, isPaused{ startPaused } { + lastPolledTime = std::chrono::high_resolution_clock::now(); + setSimulationSpeed(startingSpeed); +} + +void GameAdvancementHook::setSimulationSpeed(speed_t speed) { + if (speed < 0) + currentSpeed = 0; + else if (speed >= GAME_SPEEDS.size()) + currentSpeed = GAME_SPEEDS.size() - 1; + else + currentSpeed = speed; +} + +GameAdvancementHook::speed_t GameAdvancementHook::getSimulationSpeed() const { + return currentSpeed; +} + +void GameAdvancementHook::increaseSimulationSpeed() { + setSimulationSpeed(currentSpeed + 1); +} + +void GameAdvancementHook::decreaseSimulationSpeed() { + setSimulationSpeed(currentSpeed - 1); +} + +bool GameAdvancementHook::canIncreaseSimulationSpeed() const { + return currentSpeed + 1 < GAME_SPEEDS.size(); +} + +bool GameAdvancementHook::canDecreaseSimulationSpeed() const { + return currentSpeed > 0; +} + +GameAdvancementHook& GameAdvancementHook::operator++() { + increaseSimulationSpeed(); + return *this; +}; + +GameAdvancementHook& GameAdvancementHook::operator--() { + decreaseSimulationSpeed(); + return *this; +}; + +void GameAdvancementHook::conditionallyAdvanceGame() { + if (!isPaused) { + std::chrono::time_point<std::chrono::high_resolution_clock> currentTime = std::chrono::high_resolution_clock::now(); + if (std::chrono::duration_cast<std::chrono::milliseconds>(currentTime - lastPolledTime) >= GAME_SPEEDS[currentSpeed]) { + lastPolledTime = currentTime; + if (triggerFunction) triggerFunction(); + } + } + if (refreshFunction) refreshFunction(); +} + +void GameAdvancementHook::reset() { + isPaused = true; + currentSpeed = 0; +} diff --git a/extension/src/openvic2/GameAdvancementHook.hpp b/extension/src/openvic2/GameAdvancementHook.hpp new file mode 100644 index 0000000..07f8414 --- /dev/null +++ b/extension/src/openvic2/GameAdvancementHook.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include <chrono> +#include <functional> +#include <vector> + +namespace OpenVic2 { + //Conditionally advances game with provided behaviour + //Class governs game speed and pause state + class GameAdvancementHook { + public: + using AdvancementFunction = std::function<void()>; + using RefreshFunction = std::function<void()>; + using speed_t = int8_t; + + //Minimum number of miliseconds before the simulation advances + static const std::vector<std::chrono::milliseconds> GAME_SPEEDS; + + private: + std::chrono::time_point<std::chrono::high_resolution_clock> lastPolledTime; + //A function pointer that advances the simulation, intended to be a capturing lambda or something similar. May need to be reworked later + AdvancementFunction triggerFunction; + RefreshFunction refreshFunction; + speed_t currentSpeed; + + public: + bool isPaused; + + GameAdvancementHook(AdvancementFunction tickFunction, RefreshFunction updateFunction, bool startPaused = true, speed_t startingSpeed = 0); + + void setSimulationSpeed(speed_t speed); + speed_t getSimulationSpeed() const; + void increaseSimulationSpeed(); + void decreaseSimulationSpeed(); + bool canIncreaseSimulationSpeed() const; + bool canDecreaseSimulationSpeed() const; + GameAdvancementHook& operator++(); + GameAdvancementHook& operator--(); + void conditionallyAdvanceGame(); + void reset(); + }; +}
\ No newline at end of file diff --git a/extension/src/openvic2/GameManager.cpp b/extension/src/openvic2/GameManager.cpp new file mode 100644 index 0000000..78992f1 --- /dev/null +++ b/extension/src/openvic2/GameManager.cpp @@ -0,0 +1,46 @@ +#include "openvic2/GameManager.hpp" + +#include "openvic2/Logger.hpp" + +using namespace OpenVic2; + +GameManager::GameManager(state_updated_func_t state_updated_callback) + : clock{ [this]() { tick(); }, [this]() { update_state(); } }, state_updated{ state_updated_callback } {} + +void GameManager::set_needs_update() { + needs_update = true; +} + +void GameManager::update_state() { + if (needs_update) { + Logger::info("Update: ", today); + map.update_state(today); + if (state_updated) state_updated(); + needs_update = false; + } +} + +void GameManager::tick() { + today++; + Logger::info("Tick: ", today); + map.tick(today); + set_needs_update(); +} + +return_t GameManager::setup() { + clock.reset(); + today = { 1836 }; + set_needs_update(); + return map.generate_province_buildings(building_manager); +} + +Date const& GameManager::get_today() const { + return today; +} + +return_t GameManager::expand_building(Province::index_t province_index, std::string const& building_type_identifier) { + set_needs_update(); + Province* province = map.get_province_by_index(province_index); + if (province == nullptr) return FAILURE; + return province->expand_building(building_type_identifier); +} diff --git a/extension/src/openvic2/GameManager.hpp b/extension/src/openvic2/GameManager.hpp new file mode 100644 index 0000000..65cd566 --- /dev/null +++ b/extension/src/openvic2/GameManager.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "openvic2/GameAdvancementHook.hpp" +#include "openvic2/map/Map.hpp" + +namespace OpenVic2 { + struct GameManager { + using state_updated_func_t = std::function<void()>; + + Map map; + BuildingManager building_manager; + GameAdvancementHook clock; + private: + Date today; + state_updated_func_t state_updated; + bool needs_update; + + void set_needs_update(); + void update_state(); + void tick(); + public: + GameManager(state_updated_func_t state_updated_callback); + + return_t setup(); + + Date const& get_today() const; + return_t expand_building(Province::index_t province_index, std::string const& building_type_identifier); + }; +} diff --git a/extension/src/openvic2/Logger.cpp b/extension/src/openvic2/Logger.cpp new file mode 100644 index 0000000..56d74ab --- /dev/null +++ b/extension/src/openvic2/Logger.cpp @@ -0,0 +1,26 @@ +#include "openvic2/Logger.hpp" + +#include <iostream> + +using namespace OpenVic2; + +Logger::log_func_t Logger::info_func = [](std::string&& str) { std::cout << str; }; +Logger::log_func_t Logger::error_func = [](std::string&& str) { std::cerr << str; }; + +char const* Logger::get_filename(char const* filepath) { + if (filepath == nullptr) return nullptr; + char const* last_slash = filepath; + while (*filepath != '\0') { + if (*filepath == '\\' || *filepath == '/') last_slash = filepath + 1; + filepath++; + } + return last_slash; +} + +void Logger::set_info_func(log_func_t log_func) { + info_func = log_func; +} + +void Logger::set_error_func(log_func_t log_func) { + error_func = log_func; +} diff --git a/extension/src/openvic2/Logger.hpp b/extension/src/openvic2/Logger.hpp new file mode 100644 index 0000000..624df29 --- /dev/null +++ b/extension/src/openvic2/Logger.hpp @@ -0,0 +1,83 @@ +#pragma once + +#include <functional> +#include <sstream> +#ifdef __cpp_lib_source_location +#include <source_location> +#endif + +namespace OpenVic2 { + + #ifndef __cpp_lib_source_location + #include <string> + //Implementation of std::source_location for compilers that do not support it + //Note: uses non-standard extensions that are supported by Clang, GCC, and MSVC + //https://clang.llvm.org/docs/LanguageExtensions.html#source-location-builtins + //https://stackoverflow.com/a/67970107 + class source_location { + std::string _file; + int _line; + std::string _function; + + public: + source_location(std::string f, int l, std::string n) : _file(f), _line(l), _function(n) {} + static source_location current(std::string f = __builtin_FILE(), int l = __builtin_LINE(), std::string n = __builtin_FUNCTION()) { + return source_location(f, l, n); + } + + inline char const* file_name() const { return _file.c_str(); } + inline int line() const {return _line; } + inline char const* function_name() const { return _function.c_str(); } + }; + #endif + + class Logger { + using log_func_t = std::function<void(std::string&&)>; + + #ifdef __cpp_lib_source_location + using source_location = std::source_location; + #else + using source_location = OpenVic2::source_location; + #endif + + static log_func_t info_func, error_func; + + static char const* get_filename(char const* filepath); + + template <typename... Ts> + struct log { + log(log_func_t log_func, Ts&&... ts, const source_location& location) { + if (log_func) { + std::stringstream stream; + stream << std::endl << get_filename(location.file_name()) << "(" << location.line() << ") `" << location.function_name() << "`: "; + ((stream << std::forward<Ts>(ts)), ...); + stream << std::endl; + log_func(stream.str()); + } + } + }; + public: + static void set_info_func(log_func_t log_func); + static void set_error_func(log_func_t log_func); + + template <typename... Ts> + struct info { + info(Ts&&... ts, const source_location& location = source_location::current()) { + log<Ts...>{ info_func, std::forward<Ts>(ts)..., location }; + } + }; + + template <typename... Ts> + info(Ts&&...) -> info<Ts...>; + + template <typename... Ts> + struct error { + error(Ts&&... ts, const source_location& location = source_location::current()) { + log<Ts...>{ error_func, std::forward<Ts>(ts)..., location }; + } + }; + + template <typename... Ts> + error(Ts&&...) -> error<Ts...>; + }; +} diff --git a/extension/src/openvic2/Types.cpp b/extension/src/openvic2/Types.cpp new file mode 100644 index 0000000..861ab50 --- /dev/null +++ b/extension/src/openvic2/Types.cpp @@ -0,0 +1,13 @@ +#include "openvic2/Types.hpp" + +#include <cassert> + +using namespace OpenVic2; + +HasIdentifier::HasIdentifier(std::string const& new_identifier) : identifier{ new_identifier } { + assert(!identifier.empty()); +} + +std::string const& HasIdentifier::get_identifier() const { + return identifier; +} diff --git a/extension/src/openvic2/Types.hpp b/extension/src/openvic2/Types.hpp new file mode 100644 index 0000000..98e92ce --- /dev/null +++ b/extension/src/openvic2/Types.hpp @@ -0,0 +1,100 @@ +#pragma once + +#include <string> +#include <vector> + +#include "openvic2/Logger.hpp" + +namespace OpenVic2 { + using return_t = bool; + // This mirrors godot::Error, where `OK = 0` and `FAILED = 1`. + static constexpr return_t SUCCESS = false, FAILURE = true; + + /* + * Base class for objects with a non-empty string identifier, + * uniquely named instances of which can be entered into an + * IdentifierRegistry instance. + */ + class HasIdentifier { + const std::string identifier; + protected: + HasIdentifier(std::string const& new_identifier); + public: + HasIdentifier(HasIdentifier const&) = delete; + HasIdentifier(HasIdentifier&&) = default; + HasIdentifier& operator=(HasIdentifier const&) = delete; + HasIdentifier& operator=(HasIdentifier&&) = delete; + + std::string const& get_identifier() const; + }; + + /* + * Template for a list of objects with unique string identifiers that can + * be locked to prevent any further additions. The template argument T is + * the type of object that the registry will store, and the second part ensures + * that HasIdentifier is a base class of T. + */ + template<class T, typename std::enable_if<std::is_base_of<HasIdentifier, T>::value>::type* = nullptr> + class IdentifierRegistry { + const std::string name; + std::vector<T> items; + bool locked = false; + public: + IdentifierRegistry(std::string const& new_name) : name(new_name) {} + return_t add_item(T&& item) { + if (locked) { + Logger::error("Cannot add item to the ", name, " registry - locked!"); + return FAILURE; + } + T const* old_item = get_item_by_identifier(item.get_identifier()); + if (old_item != nullptr) { + Logger::error("Cannot add item to the ", name, " registry - an item with the identifier \"", item.get_identifier(), "\" already exists!"); + return FAILURE; + } + items.push_back(std::move(item)); + return SUCCESS; + } + void lock(bool log = true) { + if (locked) { + Logger::error("Failed to lock ", name, " registry - already locked!"); + } else { + locked = true; + if (log) Logger::info("Locked ", name, " registry after registering ", get_item_count(), " items"); + } + } + bool is_locked() const { + return locked; + } + void reset() { + items.clear(); + locked = false; + } + size_t get_item_count() const { + return items.size(); + } + T* get_item_by_identifier(std::string const& identifier) { + if (!identifier.empty()) + for (T& item : items) + if (item.get_identifier() == identifier) return &item; + return nullptr; + } + T const* get_item_by_identifier(std::string const& identifier) const { + if (!identifier.empty()) + for (T const& item : items) + if (item.get_identifier() == identifier) return &item; + return nullptr; + } + T* get_item_by_index(size_t index) { + return index < items.size() ? &items[index] : nullptr; + } + T const* get_item_by_index(size_t index) const { + return index < items.size() ? &items[index] : nullptr; + } + std::vector<T>& get_items() { + return items; + } + std::vector<T> const& get_items() const { + return items; + } + }; +} diff --git a/extension/src/openvic2/map/Building.cpp b/extension/src/openvic2/map/Building.cpp new file mode 100644 index 0000000..00e121b --- /dev/null +++ b/extension/src/openvic2/map/Building.cpp @@ -0,0 +1,124 @@ +#include "openvic2/map/Building.hpp" + +#include <cassert> + +#include "openvic2/Logger.hpp" +#include "openvic2/map/Province.hpp" + +using namespace OpenVic2; + +Building::Building(BuildingType const& new_type) : HasIdentifier{ new_type.get_identifier() }, type{ new_type } {} + +bool Building::_can_expand() const { + return level < type.get_max_level(); +} + +BuildingType const& Building::get_type() const { + return type; +} + +Building::level_t Building::get_level() const { + return level; +} + +Building::ExpansionState Building::get_expansion_state() const { + return expansion_state; +} + +Date const& Building::get_start_date() const { + return start; +} + +Date const& Building::get_end_date() const { + return end; +} + +float Building::get_expansion_progress() const { + return expansion_progress; +} + +return_t Building::expand() { + if (expansion_state == ExpansionState::CanExpand) { + expansion_state = ExpansionState::Preparing; + expansion_progress = 0.0f; + return SUCCESS; + } + return FAILURE; +} + +/* REQUIREMENTS: + * MAP-71, MAP-74, MAP-77 + */ +void Building::update_state(Date const& today) { + switch (expansion_state) { + case ExpansionState::Preparing: + start = today; + end = start + type.get_build_time(); + break; + case ExpansionState::Expanding: + expansion_progress = static_cast<double>(today - start) / static_cast<double>(end - start); + break; + default: expansion_state = _can_expand() ? ExpansionState::CanExpand : ExpansionState::CannotExpand; + } +} + +void Building::tick(Date const& today) { + if (expansion_state == ExpansionState::Preparing) { + expansion_state = ExpansionState::Expanding; + } + if (expansion_state == ExpansionState::Expanding) { + if (end <= today) { + level++; + expansion_state = ExpansionState::CannotExpand; + } + } +} + +BuildingType::BuildingType(std::string const& new_identifier, Building::level_t new_max_level, Timespan new_build_time) : + HasIdentifier{ new_identifier }, max_level{ new_max_level }, build_time{ new_build_time } { + assert(new_max_level >= 0); + assert(build_time >= 0); +} + +Building::level_t BuildingType::get_max_level() const { + return max_level; +} + +Timespan BuildingType::get_build_time() const { + return build_time; +} + +BuildingManager::BuildingManager() : building_types{ "building types" } {} + +return_t BuildingManager::add_building_type(std::string const& identifier, Building::level_t max_level, Timespan build_time) { + if (identifier.empty()) { + Logger::error("Invalid building type identifier - empty!"); + return FAILURE; + } + if (max_level < 0) { + Logger::error("Invalid building type max level: ", max_level); + return FAILURE; + } + if (build_time < 0) { + Logger::error("Invalid building type build time: ", build_time); + return FAILURE; + } + return building_types.add_item({ identifier, max_level, build_time }); +} + +void BuildingManager::lock_building_types() { + building_types.lock(); +} + +BuildingType const* BuildingManager::get_building_type_by_identifier(std::string const& identifier) const { + return building_types.get_item_by_identifier(identifier); +} + +return_t BuildingManager::generate_province_buildings(Province& province) const { + return_t ret = SUCCESS; + province.reset_buildings(); + for (BuildingType const& type : building_types.get_items()) + if (province.add_building(type) != SUCCESS) ret = FAILURE; + province.lock_buildings(); + return ret; +} diff --git a/extension/src/openvic2/map/Building.hpp b/extension/src/openvic2/map/Building.hpp new file mode 100644 index 0000000..1305014 --- /dev/null +++ b/extension/src/openvic2/map/Building.hpp @@ -0,0 +1,75 @@ +#pragma once + +#include <vector> + +#include "openvic2/Types.hpp" +#include "openvic2/Date.hpp" + +namespace OpenVic2 { + struct Province; + struct BuildingType; + + /* REQUIREMENTS: + * MAP-11, MAP-72, MAP-73 + * MAP-12, MAP-75, MAP-76 + * MAP-13, MAP-78, MAP-79 + */ + struct Building : HasIdentifier { + friend struct Province; + + using level_t = int8_t; + + enum class ExpansionState { CannotExpand, CanExpand, Preparing, Expanding }; + private: + BuildingType const& type; + level_t level = 0; + ExpansionState expansion_state = ExpansionState::CannotExpand; + Date start, end; + float expansion_progress; + + Building(BuildingType const& new_type); + + bool _can_expand() const; + public: + Building(Building&&) = default; + + BuildingType const& get_type() const; + level_t get_level() const; + ExpansionState get_expansion_state() const; + Date const& get_start_date() const; + Date const& get_end_date() const; + float get_expansion_progress() const; + + return_t expand(); + void update_state(Date const& today); + void tick(Date const& today); + }; + + struct BuildingManager; + + struct BuildingType : HasIdentifier { + friend struct BuildingManager; + private: + const Building::level_t max_level; + const Timespan build_time; + + BuildingType(std::string const& new_identifier, Building::level_t new_max_level, Timespan new_build_time); + public: + BuildingType(BuildingType&&) = default; + + Building::level_t get_max_level() const; + Timespan get_build_time() const; + }; + + struct BuildingManager { + private: + IdentifierRegistry<BuildingType> building_types; + public: + BuildingManager(); + + return_t add_building_type(std::string const& identifier, Building::level_t max_level, Timespan build_time); + void lock_building_types(); + BuildingType const* get_building_type_by_identifier(std::string const& identifier) const; + return_t generate_province_buildings(Province& province) const; + }; +} diff --git a/extension/src/openvic2/map/Map.cpp b/extension/src/openvic2/map/Map.cpp new file mode 100644 index 0000000..b5cf144 --- /dev/null +++ b/extension/src/openvic2/map/Map.cpp @@ -0,0 +1,320 @@ +#include "openvic2/map/Map.hpp" + +#include <cassert> +#include <unordered_set> + +#include "openvic2/Logger.hpp" + +using namespace OpenVic2; + +Mapmode::Mapmode(index_t new_index, std::string const& new_identifier, colour_func_t new_colour_func) + : HasIdentifier{ new_identifier }, index{ new_index }, colour_func{ new_colour_func } { + assert(colour_func != nullptr); +} + +Mapmode::index_t Mapmode::get_index() const { + return index; +} + +Province::colour_t Mapmode::get_colour(Map const& map, Province const& province) const { + return colour_func ? colour_func(map, province) : Province::NULL_COLOUR; +} + +Map::Map() : provinces{ "provinces" }, regions{ "regions" }, mapmodes{ "mapmodes" } {} + +return_t Map::add_province(std::string const& identifier, Province::colour_t colour) { + if (provinces.get_item_count() >= Province::MAX_INDEX) { + Logger::error("The map's province list is full - there can be at most ", Province::MAX_INDEX, " provinces"); + return FAILURE; + } + if (identifier.empty()) { + Logger::error("Invalid province identifier - empty!"); + return FAILURE; + } + if (colour == Province::NULL_COLOUR || colour > Province::MAX_COLOUR) { + Logger::error("Invalid province colour: ", Province::colour_to_hex_string(colour)); + return FAILURE; + } + Province new_province{ static_cast<Province::index_t>(provinces.get_item_count() + 1), identifier, colour }; + Province const* old_province = get_province_by_colour(colour); + if (old_province != nullptr) { + Logger::error("Duplicate province colours: ", old_province->to_string(), " and ", new_province.to_string()); + return FAILURE; + } + return provinces.add_item(std::move(new_province)); +} + +void Map::lock_provinces() { + provinces.lock(); +} + +return_t Map::set_water_province(std::string const& identifier) { + if (water_provinces_locked) { + Logger::error("The map's water provinces have already been locked!"); + return FAILURE; + } + Province* province = get_province_by_identifier(identifier); + if (province == nullptr) { + Logger::error("Unrecognised water province identifier: ", identifier); + return FAILURE; + } + if (province->is_water()) { + Logger::error("Province ", identifier, " is already a water province!"); + return FAILURE; + } + province->water = true; + water_province_count++; + return SUCCESS; +} + +void Map::lock_water_provinces() { + water_provinces_locked = true; + Logger::info("Locked water provinces after registering ", water_province_count); +} + +return_t Map::add_region(std::string const& identifier, std::vector<std::string> const& province_identifiers) { + if (identifier.empty()) { + Logger::error("Invalid region identifier - empty!"); + return FAILURE; + } + Region new_region{ identifier }; + return_t ret = SUCCESS; + for (std::string const& province_identifier : province_identifiers) { + Province* province = get_province_by_identifier(province_identifier); + if (province != nullptr) { + if (new_region.contains_province(province)) { + Logger::error("Duplicate province identifier ", province_identifier); + ret = FAILURE; + } else { + size_t other_region_index = reinterpret_cast<size_t>(province->get_region()); + if (other_region_index != 0) { + other_region_index--; + if (other_region_index < regions.get_item_count()) + Logger::error("Province ", province_identifier, " is already part of ", regions.get_item_by_index(other_region_index)->get_identifier()); + else + Logger::error("Province ", province_identifier, " is already part of an unknown region with index ", other_region_index); + ret = FAILURE; + } else new_region.provinces.insert(province); + } + } else { + Logger::error("Invalid province identifier ", province_identifier); + ret = FAILURE; + } + } + if (!new_region.get_province_count()) { + Logger::error("No valid provinces in region's list"); + return FAILURE; + } + + // Used to detect provinces listed in multiple regions, will + // be corrected once regions is stable (i.e. lock_regions). + Region* tmp_region_index = reinterpret_cast<Region*>(regions.get_item_count()); + for (Province* province : new_region.get_provinces()) + province->region = tmp_region_index; + if (regions.add_item(std::move(new_region)) != SUCCESS) ret = FAILURE; + return ret; +} + +void Map::lock_regions() { + regions.lock(); + for (Region& region : regions.get_items()) + for (Province* province : region.get_provinces()) + province->region = ®ion; +} + +size_t Map::get_province_count() const { + return provinces.get_item_count(); +} + +Province* Map::get_province_by_index(Province::index_t index) { + return index != Province::NULL_INDEX ? provinces.get_item_by_index(index - 1) : nullptr; +} + +Province const* Map::get_province_by_index(Province::index_t index) const { + return index != Province::NULL_INDEX ? provinces.get_item_by_index(index - 1) : nullptr; +} + +Province* Map::get_province_by_identifier(std::string const& identifier) { + return provinces.get_item_by_identifier(identifier); +} + +Province const* Map::get_province_by_identifier(std::string const& identifier) const { + return provinces.get_item_by_identifier(identifier); +} + +Province* Map::get_province_by_colour(Province::colour_t colour) { + if (colour != Province::NULL_COLOUR) + for (Province& province : provinces.get_items()) + if (province.get_colour() == colour) return &province; + return nullptr; +} + +Province const* Map::get_province_by_colour(Province::colour_t colour) const { + if (colour != Province::NULL_COLOUR) + for (Province const& province : provinces.get_items()) + if (province.get_colour() == colour) return &province; + return nullptr; +} + +Province::index_t Map::get_province_index_at(size_t x, size_t y) const { + if (x < width && y < height) return province_index_image[x + y * width]; + return Province::NULL_INDEX; +} + +Region* Map::get_region_by_identifier(std::string const& identifier) { + return regions.get_item_by_identifier(identifier); +} + +Region const* Map::get_region_by_identifier(std::string const& identifier) const { + return regions.get_item_by_identifier(identifier); +} + +static Province::colour_t colour_at(uint8_t const* colour_data, int32_t idx) { + return (colour_data[idx * 3] << 16) | (colour_data[idx * 3 + 1] << 8) | colour_data[idx * 3 + 2]; +} + +return_t Map::generate_province_index_image(size_t new_width, size_t new_height, uint8_t const* colour_data) { + if (!province_index_image.empty()) { + Logger::error("Province index image has already been generated!"); + return FAILURE; + } + if (!provinces.is_locked()) { + Logger::error("Province index image cannot be generated until after provinces are locked!"); + return FAILURE; + } + if (new_width < 1 || new_height < 1) { + Logger::error("Invalid province image dimensions: ", new_width, "x", new_height); + return FAILURE; + } + if (colour_data == nullptr) { + Logger::error("Province colour data pointer is null!"); + return FAILURE; + } + width = new_width; + height = new_height; + province_index_image.resize(width * height); + + std::vector<bool> province_checklist(provinces.get_item_count()); + return_t ret = SUCCESS; + std::unordered_set<Province::colour_t> unrecognised_colours; + + for (int32_t y = 0; y < height; ++y) { + for (int32_t x = 0; x < width; ++x) { + const int32_t idx = x + y * width; + const Province::colour_t colour = colour_at(colour_data, idx); + if (x > 0) { + const int32_t jdx = idx - 1; + if (colour_at(colour_data, jdx) == colour) { + province_index_image[idx] = province_index_image[jdx]; + continue; + } + } + if (y > 0) { + const int32_t jdx = idx - width; + if (colour_at(colour_data, jdx) == colour) { + province_index_image[idx] = province_index_image[jdx]; + continue; + } + } + Province const* province = get_province_by_colour(colour); + if (province != nullptr) { + const Province::index_t index = province->get_index(); + province_index_image[idx] = index; + province_checklist[index - 1] = true; + continue; + } + if (unrecognised_colours.find(colour) == unrecognised_colours.end()) { + unrecognised_colours.insert(colour); + Logger::error("Unrecognised province colour ", Province::colour_to_hex_string(colour), " at (", x, ", ", y, ")"); + ret = FAILURE; + } + province_index_image[idx] = Province::NULL_INDEX; + } + } + + for (size_t idx = 0; idx < province_checklist.size(); ++idx) { + if (!province_checklist[idx]) { + Logger::error("Province missing from shape image: ", provinces.get_item_by_index(idx)->to_string()); + ret = FAILURE; + } + } + return ret; +} + +size_t Map::get_width() const { + return width; +} + +size_t Map::get_height() const { + return height; +} + +std::vector<Province::index_t> const& Map::get_province_index_image() const { + return province_index_image; +} + +return_t Map::add_mapmode(std::string const& identifier, Mapmode::colour_func_t colour_func) { + if (identifier.empty()) { + Logger::error("Invalid mapmode identifier - empty!"); + return FAILURE; + } + if (colour_func == nullptr) { + Logger::error("Mapmode colour function is null for identifier: ", identifier); + return FAILURE; + } + return mapmodes.add_item({ mapmodes.get_item_count(), identifier, colour_func }); +} + +void Map::lock_mapmodes() { + mapmodes.lock(); +} + +size_t Map::get_mapmode_count() const { + return mapmodes.get_item_count(); +} + +Mapmode const* Map::get_mapmode_by_index(size_t index) const { + return mapmodes.get_item_by_index(index); +} + +Mapmode const* Map::get_mapmode_by_identifier(std::string const& identifier) const { + return mapmodes.get_item_by_identifier(identifier); +} + +return_t Map::generate_mapmode_colours(Mapmode::index_t index, uint8_t* target) const { + if (target == nullptr) { + Logger::error("Mapmode colour target pointer is null!"); + return FAILURE; + } + Mapmode const* mapmode = mapmodes.get_item_by_index(index); + if (mapmode == nullptr) { + Logger::error("Invalid mapmode index: ", index); + return FAILURE; + } + target += 4; // Skip past Province::NULL_INDEX + for (Province const& province : provinces.get_items()) { + const Province::colour_t colour = mapmode->get_colour(*this, province); + *target++ = (colour >> 16) & 0xFF; + *target++ = (colour >> 8) & 0xFF; + *target++ = colour & 0xFF; + *target++ = province.is_water() ? 0 : 255; + } + return SUCCESS; +} + +return_t Map::generate_province_buildings(BuildingManager const& manager) { + return_t ret = SUCCESS; + for (Province& province : provinces.get_items()) + if (manager.generate_province_buildings(province) != SUCCESS) ret = FAILURE; + return ret; +} + +void Map::update_state(Date const& today) { + for (Province& province : provinces.get_items()) + province.update_state(today); +} + +void Map::tick(Date const& today) { + for (Province& province : provinces.get_items()) + province.tick(today); +} diff --git a/extension/src/openvic2/map/Map.hpp b/extension/src/openvic2/map/Map.hpp new file mode 100644 index 0000000..ebc23be --- /dev/null +++ b/extension/src/openvic2/map/Map.hpp @@ -0,0 +1,76 @@ +#pragma once + +#include <functional> + +#include "openvic2/map/Region.hpp" + +namespace OpenVic2 { + + struct Mapmode : HasIdentifier { + friend struct Map; + + using colour_func_t = std::function<Province::colour_t (Map const&, Province const&)>; + using index_t = size_t; + private: + const index_t index; + const colour_func_t colour_func; + + Mapmode(index_t new_index, std::string const& new_identifier, colour_func_t new_colour_func); + public: + index_t get_index() const; + Province::colour_t get_colour(Map const& map, Province const& province) const; + }; + + /* REQUIREMENTS: + * MAP-4 + */ + struct Map { + private: + IdentifierRegistry<Province> provinces; + IdentifierRegistry<Region> regions; + IdentifierRegistry<Mapmode> mapmodes; + bool water_provinces_locked = false; + size_t water_province_count = 0; + + size_t width = 0, height = 0; + std::vector<Province::index_t> province_index_image; + public: + Map(); + + return_t add_province(std::string const& identifier, Province::colour_t colour); + void lock_provinces(); + return_t set_water_province(std::string const& identifier); + void lock_water_provinces(); + return_t add_region(std::string const& identifier, std::vector<std::string> const& province_identifiers); + void lock_regions(); + + size_t get_province_count() const; + Province* get_province_by_index(Province::index_t index); + Province const* get_province_by_index(Province::index_t index) const; + Province* get_province_by_identifier(std::string const& identifier); + Province const* get_province_by_identifier(std::string const& identifier) const; + Province* get_province_by_colour(Province::colour_t colour); + Province const* get_province_by_colour(Province::colour_t colour) const; + Province::index_t get_province_index_at(size_t x, size_t y) const; + + Region* get_region_by_identifier(std::string const& identifier); + Region const* get_region_by_identifier(std::string const& identifier) const; + + return_t generate_province_index_image(size_t new_width, size_t new_height, uint8_t const* colour_data); + size_t get_width() const; + size_t get_height() const; + std::vector<Province::index_t> const& get_province_index_image() const; + + return_t add_mapmode(std::string const& identifier, Mapmode::colour_func_t colour_func); + void lock_mapmodes(); + size_t get_mapmode_count() const; + Mapmode const* get_mapmode_by_index(Mapmode::index_t index) const; + Mapmode const* get_mapmode_by_identifier(std::string const& identifier) const; + return_t generate_mapmode_colours(Mapmode::index_t index, uint8_t* target) const; + + return_t generate_province_buildings(BuildingManager const& manager); + + void update_state(Date const& today); + void tick(Date const& today); + }; +} diff --git a/extension/src/openvic2/map/Province.cpp b/extension/src/openvic2/map/Province.cpp new file mode 100644 index 0000000..4360bce --- /dev/null +++ b/extension/src/openvic2/map/Province.cpp @@ -0,0 +1,78 @@ +#include "openvic2/map/Province.hpp" + +#include <cassert> +#include <sstream> +#include <iomanip> + +using namespace OpenVic2; + +Province::Province(index_t new_index, std::string const& new_identifier, colour_t new_colour) : + HasIdentifier{ new_identifier }, index{ new_index }, colour{ new_colour }, buildings{ "buildings" } { + assert(index != NULL_INDEX); + assert(colour != NULL_COLOUR); +} + +std::string Province::colour_to_hex_string(colour_t colour) { + std::ostringstream stream; + stream << std::hex << std::setfill('0') << std::setw(6) << colour; + return stream.str(); +} + +Province::index_t Province::get_index() const { + return index; +} + +Province::colour_t Province::get_colour() const { + return colour; +} + +Region* Province::get_region() const { + return region; +} + +bool Province::is_water() const { + return water; +} + +Province::life_rating_t Province::get_life_rating() const { + return life_rating; +} + +return_t Province::add_building(BuildingType const& type) { + return buildings.add_item({ type }); +} + +void Province::lock_buildings() { + buildings.lock(false); +} + +void Province::reset_buildings() { + buildings.reset(); +} + +std::vector<Building> const& Province::get_buildings() const { + return buildings.get_items(); +} + +return_t Province::expand_building(std::string const& building_type_identifier) { + Building* building = buildings.get_item_by_identifier(building_type_identifier); + if (building == nullptr) return FAILURE; + return building->expand(); +} + +std::string Province::to_string() const { + std::stringstream stream; + stream << "(#" << std::to_string(index) << ", " << get_identifier() << ", 0x" << colour_to_hex_string(colour) << ")"; + return stream.str(); +} + +void Province::update_state(Date const& today) { + for (Building& building : buildings.get_items()) + building.update_state(today); + +} + +void Province::tick(Date const& today) { + for (Building& building : buildings.get_items()) + building.tick(today); +} diff --git a/extension/src/openvic2/map/Province.hpp b/extension/src/openvic2/map/Province.hpp new file mode 100644 index 0000000..aa0329c --- /dev/null +++ b/extension/src/openvic2/map/Province.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include "openvic2/map/Building.hpp" + +namespace OpenVic2 { + struct Map; + struct Region; + + /* REQUIREMENTS: + * MAP-5, MAP-8, MAP-43, MAP-47 + */ + struct Province : HasIdentifier { + friend struct Map; + + using colour_t = uint32_t; + using index_t = uint16_t; + using life_rating_t = int8_t; + + static constexpr colour_t NULL_COLOUR = 0, MAX_COLOUR = 0xFFFFFF; + static constexpr index_t NULL_INDEX = 0, MAX_INDEX = 0xFFFF; + private: + const index_t index; + const colour_t colour; + Region* region = nullptr; + bool water = false; + life_rating_t life_rating = 0; + IdentifierRegistry<Building> buildings; + + Province(index_t new_index, std::string const& new_identifier, colour_t new_colour); + public: + static std::string colour_to_hex_string(colour_t colour); + + Province(Province&&) = default; + + index_t get_index() const; + colour_t get_colour() const; + Region* get_region() const; + bool is_water() const; + life_rating_t get_life_rating() const; + return_t add_building(BuildingType const& type); + void lock_buildings(); + void reset_buildings(); + std::vector<Building> const& get_buildings() const; + return_t expand_building(std::string const& building_type_identifier); + std::string to_string() const; + + void update_state(Date const& today); + void tick(Date const& today); + }; +} diff --git a/extension/src/openvic2/map/Region.cpp b/extension/src/openvic2/map/Region.cpp new file mode 100644 index 0000000..3e5bee7 --- /dev/null +++ b/extension/src/openvic2/map/Region.cpp @@ -0,0 +1,26 @@ +#include "openvic2/map/Region.hpp" + +#include <cassert> +#include <algorithm> + +using namespace OpenVic2; + +size_t ProvinceSet::get_province_count() const { + return provinces.size(); +} + +bool ProvinceSet::contains_province(Province const* province) const { + return province && std::find(provinces.begin(), provinces.end(), province) != provinces.end(); +} + +std::set<Province*> const& ProvinceSet::get_provinces() const { + return provinces; +} + +Region::Region(std::string const& new_identifier) : HasIdentifier{ new_identifier } {} + +Province::colour_t Region::get_colour() const { + if (provinces.empty()) return 0xFF0000; + Province const* province = *provinces.cbegin(); + return province->get_colour(); +} diff --git a/extension/src/openvic2/map/Region.hpp b/extension/src/openvic2/map/Region.hpp new file mode 100644 index 0000000..04564fc --- /dev/null +++ b/extension/src/openvic2/map/Region.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include <set> + +#include "openvic2/map/Province.hpp" + +namespace OpenVic2 { + + struct ProvinceSet { + protected: + std::set<Province*> provinces; + public: + size_t get_province_count() const; + bool contains_province(Province const* province) const; + std::set<Province*> const& get_provinces() const; + }; + + /* REQUIREMENTS: + * MAP-6, MAP-44, MAP-48 + */ + struct Region : HasIdentifier, ProvinceSet { + friend struct Map; + private: + Region(std::string const& new_identifier); + public: + Region(Region&&) = default; + + Province::colour_t get_colour() const; + }; +} diff --git a/extension/src/register_types.cpp b/extension/src/register_types.cpp index d1613a5..b99f1a8 100644 --- a/extension/src/register_types.cpp +++ b/extension/src/register_types.cpp @@ -1,37 +1,24 @@ #include "register_types.h" -#include <gdextension_interface.h> -#include <godot_cpp/core/class_db.hpp> -#include <godot_cpp/core/defs.hpp> -#include <godot_cpp/godot.hpp> + #include <godot_cpp/classes/engine.hpp> -#include "TestSingleton.hpp" -#include "Simulation.hpp" #include "Checksum.hpp" #include "LoadLocalisation.hpp" +#include "GameSingleton.hpp" +#include "MapMesh.hpp" using namespace godot; using namespace OpenVic2; -static TestSingleton* _test_singleton; -static Simulation* _simulation; static Checksum* _checksum; static LoadLocalisation* _load_localisation; +static GameSingleton* _map_singleton; -void initialize_openvic2_types(ModuleInitializationLevel p_level) -{ +void initialize_openvic2_types(ModuleInitializationLevel p_level) { if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { return; } - ClassDB::register_class<TestSingleton>(); - _test_singleton = memnew(TestSingleton); - Engine::get_singleton()->register_singleton("TestSingleton", TestSingleton::get_singleton()); - - ClassDB::register_class<Simulation>(); - _simulation = memnew(Simulation); - Engine::get_singleton()->register_singleton("Simulation", Simulation::get_singleton()); - ClassDB::register_class<Checksum>(); _checksum = memnew(Checksum); Engine::get_singleton()->register_singleton("Checksum", Checksum::get_singleton()); @@ -40,6 +27,11 @@ void initialize_openvic2_types(ModuleInitializationLevel p_level) _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_openvic2_types(ModuleInitializationLevel p_level) { @@ -47,26 +39,19 @@ void uninitialize_openvic2_types(ModuleInitializationLevel p_level) { return; } - Engine::get_singleton()->unregister_singleton("TestSingleton"); - memdelete(_test_singleton); - - Engine::get_singleton()->unregister_singleton("Simulation"); - memdelete(_simulation); - Engine::get_singleton()->unregister_singleton("Checksum"); memdelete(_checksum); Engine::get_singleton()->unregister_singleton("LoadLocalisation"); memdelete(_load_localisation); -} -extern "C" -{ + Engine::get_singleton()->unregister_singleton("GameSingleton"); + memdelete(_map_singleton); +} +extern "C" { // Initialization. - - GDExtensionBool GDE_EXPORT openvic2_library_init(const GDExtensionInterface *p_interface, const GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization) - { + GDExtensionBool GDE_EXPORT openvic2_library_init(GDExtensionInterface const* p_interface, const GDExtensionClassLibraryPtr p_library, GDExtensionInitialization* r_initialization) { GDExtensionBinding::InitObject init_obj(p_interface, p_library, r_initialization); init_obj.register_initializer(initialize_openvic2_types); @@ -75,4 +60,4 @@ extern "C" return init_obj.init(); } -}
\ No newline at end of file +} diff --git a/extension/src/register_types.h b/extension/src/register_types.h index 860359f..1bc96f8 100644 --- a/extension/src/register_types.h +++ b/extension/src/register_types.h @@ -1,6 +1,6 @@ #pragma once -#include <godot_cpp/core/class_db.hpp> +#include <godot_cpp/godot.hpp> void initialize_openvic2_types(godot::ModuleInitializationLevel); void uninitialize_openvic2_types(godot::ModuleInitializationLevel);
\ No newline at end of file |