diff options
author | Hop311 <Hop3114@gmail.com> | 2023-05-22 19:44:27 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-05-22 19:44:27 +0200 |
commit | ac36a373139e3e815f70720b37d4ffc8d9062df9 (patch) | |
tree | 04c25f263a2364eab5bc3647b9e1a4ccc9fc3bf2 | |
parent | 30fa7d74ddcfd4e64a5526995a392f7b51feb869 (diff) | |
parent | 42b4b45d7d1216770077e0676cd66750cae26932 (diff) |
Merge pull request #123 from OpenVicProject/compatibility-mode
Map compatibility mode
m--------- | extension/deps/openvic-simulation | 0 | ||||
-rw-r--r-- | extension/src/GameSingleton.cpp | 523 | ||||
-rw-r--r-- | extension/src/GameSingleton.hpp | 45 | ||||
-rw-r--r-- | extension/src/LoadGameCompatibility.cpp | 154 | ||||
-rw-r--r-- | extension/src/LoadGameOpenVic.cpp | 470 | ||||
-rw-r--r-- | extension/src/LoadLocalisation.cpp | 61 | ||||
-rw-r--r-- | extension/src/Utilities.hpp | 22 | ||||
-rw-r--r-- | extension/src/register_types.h | 2 | ||||
-rw-r--r-- | game/src/Autoload/Events.gd | 16 | ||||
-rw-r--r-- | game/src/GameSession/MapView.gd | 2 |
10 files changed, 769 insertions, 526 deletions
diff --git a/extension/deps/openvic-simulation b/extension/deps/openvic-simulation -Subproject 15e960f93ced8c94a6a45ebb2b44d0705ff7f8f +Subproject 35b038fe91d4f3c4b3ffdc4f48385ac3b208266 diff --git a/extension/src/GameSingleton.cpp b/extension/src/GameSingleton.cpp index e067517..cb7d0a7 100644 --- a/extension/src/GameSingleton.cpp +++ b/extension/src/GameSingleton.cpp @@ -1,20 +1,19 @@ #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/Logger.hpp" +#include "openvic/utility/Logger.hpp" + +#include "Utilities.hpp" using namespace godot; using namespace OpenVic; -#define ERR(x) ((x) == SUCCESS ? OK : FAILED) - GameSingleton* GameSingleton::singleton = nullptr; void GameSingleton::_bind_methods() { ClassDB::bind_method(D_METHOD("load_defines", "file_dict"), &GameSingleton::load_defines); + ClassDB::bind_method(D_METHOD("load_defines_compatibility_mode", "file_path"), &GameSingleton::load_defines_compatibility_mode); 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); @@ -27,7 +26,6 @@ void GameSingleton::_bind_methods() { 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("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); @@ -54,6 +52,7 @@ void GameSingleton::_bind_methods() { ClassDB::bind_static_method("GameSingleton", D_METHOD("get_water_province_file_key"), &GameSingleton::get_water_province_file_key); ClassDB::bind_static_method("GameSingleton", D_METHOD("get_region_file_key"), &GameSingleton::get_region_file_key); ClassDB::bind_static_method("GameSingleton", D_METHOD("get_terrain_variant_file_key"), &GameSingleton::get_terrain_variant_file_key); + ClassDB::bind_static_method("GameSingleton", D_METHOD("get_terrain_texture_dir_key"), &GameSingleton::get_terrain_texture_dir_key); ClassDB::bind_static_method("GameSingleton", D_METHOD("get_province_image_file_key"), &GameSingleton::get_province_image_file_key); ClassDB::bind_static_method("GameSingleton", D_METHOD("get_terrain_image_file_key"), &GameSingleton::get_terrain_image_file_key); ClassDB::bind_static_method("GameSingleton", D_METHOD("get_goods_file_key"), &GameSingleton::get_goods_file_key); @@ -77,9 +76,8 @@ GameSingleton* GameSingleton::get_singleton() { return singleton; } - void GameSingleton::_on_state_updated() { - update_colour_image(); + _update_colour_image(); emit_signal("state_updated"); } @@ -91,13 +89,17 @@ GameSingleton::GameSingleton() : game_manager { [this]() { _on_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()); }); + Logger::set_info_func([](std::string&& str) { UtilityFunctions::print(std_to_godot_string(str)); }); + Logger::set_error_func([](std::string&& str) { UtilityFunctions::push_error(std_to_godot_string(str)); }); +} + +Error GameSingleton::_load_hardcoded_defines() { + Error err = OK; static constexpr colour_t LOW_ALPHA_VALUE = float_to_alpha_value(0.4f); static constexpr colour_t HIGH_ALPHA_VALUE = float_to_alpha_value(0.7f); using mapmode_t = std::pair<std::string, Mapmode::colour_func_t>; - const std::vector<mapmode_t> mapmodes = { + const std::vector<mapmode_t> mapmodes { { "mapmode_terrain", [](Map const&, Province const& province) -> colour_t { return LOW_ALPHA_VALUE | (province.is_water() ? 0x4287F5 : 0x0D7017); @@ -129,9 +131,9 @@ GameSingleton::GameSingleton() : game_manager { [this]() { _on_state_updated(); if (railroad != nullptr) { colour_t val = fraction_to_colour_byte(railroad->get_level(), railroad->get_type().get_max_level(), 0.5f, 1.0f); switch (railroad->get_expansion_state()) { - case Building::ExpansionState::CannotExpand: val <<= 16; break; - case Building::ExpansionState::CanExpand: break; - default: val <<= 8; break; + case Building::ExpansionState::CannotExpand: val <<= 16; break; + case Building::ExpansionState::CanExpand: break; + default: val <<= 8; break; } return HIGH_ALPHA_VALUE | val; } @@ -139,16 +141,20 @@ GameSingleton::GameSingleton() : game_manager { [this]() { _on_state_updated(); } } }; for (mapmode_t const& mapmode : mapmodes) - game_manager.map.add_mapmode(mapmode.first, mapmode.second); + if (game_manager.map.add_mapmode(mapmode.first, mapmode.second) != SUCCESS) + err = FAILED; 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 = { + 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)); + if (game_manager.building_manager.add_building_type(std::get<0>(type), std::get<1>(type), std::get<2>(type)) != SUCCESS) + err = FAILED; game_manager.building_manager.lock_building_types(); + + return err; } GameSingleton::~GameSingleton() { @@ -156,451 +162,10 @@ GameSingleton::~GameSingleton() { 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<Error(String const&, 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; -} - -static colour_t _parse_colour(Variant const& var) { - const Variant::Type type = var.get_type(); - if (type == Variant::ARRAY) { - Array const& colour_array = var; - if (colour_array.size() == 3) { - colour_t colour = NULL_COLOUR; - for (int jdx = 0; jdx < 3; ++jdx) { - Variant const& var = colour_array[jdx]; - if (var.get_type() != Variant::FLOAT) return NULL_COLOUR; - const double colour_double = var; - if (std::trunc(colour_double) != colour_double) return NULL_COLOUR; - const int64_t colour_int = static_cast<int64_t>(colour_double); - if (colour_int < 0 || colour_int > 255) return NULL_COLOUR; - colour = (colour << 8) | colour_int; - } - return colour; - } - } else if (type == Variant::STRING) { - String const& colour_string = var; - if (colour_string.is_valid_hex_number()) { - const int64_t colour_int = colour_string.hex_to_int(); - if (colour_int != NULL_COLOUR && colour_int <= MAX_COLOUR_RGB) - return colour_int; - } - } - return NULL_COLOUR; -} - -Error GameSingleton::_parse_province_identifier_entry(String const& identifier, Variant const& entry) { - const colour_t colour = _parse_colour(entry); - if (colour == NULL_COLOUR || colour > MAX_COLOUR_RGB) { - 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; -} - -TerrainVariant::TerrainVariant(std::string const& new_identfier, colour_t new_colour, Ref<Image> const& new_image) - : HasIdentifier(new_identfier), - HasColour(new_colour), - image(new_image) {} - -Ref<Image> TerrainVariant::get_image() const { return image; } - -Error GameSingleton::_parse_terrain_entry(String const& identifier, Variant const& entry) { - const colour_t colour = _parse_colour(entry); - if (colour == NULL_COLOUR || colour > MAX_COLOUR_RGB) { - UtilityFunctions::push_error("Invalid colour for terrain texture \"", identifier, "\": ", entry); - return FAILED; - } - static const String terrain_folder = "res://art/terrain/"; - const String terrain_path = terrain_folder + identifier; - Ref<Image> terrain_image; - terrain_image.instantiate(); - const Error err = terrain_image->load(terrain_path); - if (err != OK) { - UtilityFunctions::push_error("Failed to load terrain image: ", terrain_path); - return err; - } - return ERR(terrain_variants.add_item({ identifier.utf8().get_data(), colour, terrain_image })); -} - -Error GameSingleton::load_terrain_variant_file(String const& file_path) { - const Error err = _parse_json_dictionary_file("terrain variants", file_path, "", - [this](String const& identifier, Variant const& entry) -> Error { - return _parse_terrain_entry(identifier, entry); - }); - terrain_variants.lock(); - if (err != OK || terrain_variants.get_item_count() == 0) { - UtilityFunctions::push_error("Failed to load terrain textures!"); - return FAILED; - } - - Array terrain_images; - for (TerrainVariant const& var : terrain_variants.get_items()) { - terrain_variant_map[var.get_colour()] = terrain_images.size(); - 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 OK; -} - -Error GameSingleton::load_map_images(String const& province_image_path, String const& terrain_image_path) { - 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, terrain_image; - province_image.instantiate(); - terrain_image.instantiate(); - Error err = province_image->load(province_image_path); - if (err != OK) { - UtilityFunctions::push_error("Failed to load province image: ", province_image_path); - return err; - } - err = terrain_image->load(terrain_image_path); - if (err != OK) { - UtilityFunctions::push_error("Failed to load terrain image: ", terrain_image_path); - return err; - } - - // Validate dimensions and format - 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; - const Image::Format province_format = province_image->get_format(), terrain_format = terrain_image->get_format(); - if (province_format != expected_format) { - UtilityFunctions::push_error("Invalid format (", province_format, ", should be ", expected_format, ") for province image: ", province_image_path); - err = FAILED; - } - if (terrain_format != expected_format) { - UtilityFunctions::push_error("Invalid format (", terrain_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) != SUCCESS) return 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; -} - -Error GameSingleton::_parse_good_entry(String const& identifier, Variant const& entry) { - if (entry.get_type() != Variant::DICTIONARY) { - UtilityFunctions::push_error("Invalid good entry for ", identifier, ": ", entry); - return FAILED; - } - Dictionary const& dict = entry; - - static const String key_category = "category"; - Variant const& var_category = dict.get(key_category, ""); - String category; - if (var_category.get_type() == Variant::STRING) category = var_category; - else UtilityFunctions::push_error("Invalid good category for ", identifier, ": ", var_category); - - static const String key_base_price = "base_price"; - Variant const& var_base_price = dict.get(key_base_price, NULL_PRICE); - price_t base_price = NULL_PRICE; - if (var_base_price.get_type() == Variant::FLOAT) base_price = var_base_price; - else UtilityFunctions::push_error("Invalid good base price for ", identifier, ": ", var_base_price); - - static const String key_colour = "colour"; - Variant const& var_colour = dict.get(key_colour, ""); - const colour_t colour = _parse_colour(var_colour); - if (colour > MAX_COLOUR_RGB) { - UtilityFunctions::push_error("Invalid good colour for ", identifier, ": ", var_colour); - return FAILED; - } - - static const String key_default_available = "default_available"; - Variant const& var_default_available = dict.get(key_default_available, true); - bool default_available = false; - if (var_default_available.get_type() == Variant::BOOL) default_available = var_default_available; - else UtilityFunctions::push_error("Invalid good available default bool value for ", identifier, ": ", var_default_available); - - static const String key_tradeable = "tradeable"; - Variant const& var_tradeable = dict.get(key_tradeable, true); - bool tradeable = false; - if (var_tradeable.get_type() == Variant::BOOL) tradeable = var_tradeable; - else UtilityFunctions::push_error("Invalid good tradeable bool value for ", identifier, ": ", var_tradeable); - - static const String key_currency = "currency"; - Variant const& var_currency = dict.get(key_currency, true); - bool currency = false; - if (var_currency.get_type() == Variant::BOOL) currency = var_currency; - else UtilityFunctions::push_error("Invalid good currency bool value for ", identifier, ": ", var_currency); - - static const String key_overseas_maintenance = "overseas_maintenance"; - Variant const& var_overseas_maintenance = dict.get(key_overseas_maintenance, true); - bool overseas_maintenance = false; - if (var_overseas_maintenance.get_type() == Variant::BOOL) overseas_maintenance = var_overseas_maintenance; - else UtilityFunctions::push_error("Invalid good overseas maintenance bool value for ", identifier, ": ", var_overseas_maintenance); - - return ERR(game_manager.good_manager.add_good(identifier.utf8().get_data(), category.utf8().get_data(), - colour, base_price, default_available, tradeable, currency, overseas_maintenance)); -} - -Error GameSingleton::load_goods(String const& defines_path, String const& icons_dir_path) { - Error err = _parse_json_dictionary_file("good", defines_path, "good_", - [this](String const& identifier, Variant const& entry) -> Error { - return _parse_good_entry(identifier, entry); - }); - game_manager.good_manager.lock_goods(); - for (Good const& good : game_manager.good_manager.get_goods()) { - Ref<Image> image; - image.instantiate(); - const String path = icons_dir_path + String{ "/" } + good.get_identifier().c_str() + ".png"; - const Error good_err = image->load(path); - if (good_err || image.is_null()) { - UtilityFunctions::push_error("Failed to load good icon image: ", path); - err = FAILED; - continue; - } - Ref<Texture> tex = ImageTexture::create_from_image(image); - if (tex.is_null()) { - UtilityFunctions::push_error("Failed to generate good icon texture: ", path); - err = FAILED; - continue; - } - good_icons[good.get_identifier().c_str()] = tex; - } - return err; -} - -StringName const& GameSingleton::get_province_identifier_file_key() { - static const StringName key = "province_identifiers"; - return key; -} -StringName const& GameSingleton::get_water_province_file_key() { - static const StringName key = "water_provinces"; - return key; -} -StringName const& GameSingleton::get_region_file_key() { - static const StringName key = "regions"; - return key; -} -StringName const& GameSingleton::get_terrain_variant_file_key() { - static const StringName key = "terrain_variants"; - return key; -} -StringName const& GameSingleton::get_province_image_file_key() { - static const StringName key = "province_image"; - return key; -} -StringName const& GameSingleton::get_terrain_image_file_key() { - static const StringName key = "terrain_image"; - return key; -} -StringName const& GameSingleton::get_goods_file_key() { - static const StringName key = "goods"; - return key; -} -StringName const& GameSingleton::get_good_icons_dir_key() { - static const StringName key = "good_icons"; - return key; -} - -Error GameSingleton::load_defines(Dictionary const& file_dict) { - Error err = OK; - if (load_province_identifier_file(file_dict.get(get_province_identifier_file_key(), "")) != OK) { - UtilityFunctions::push_error("Failed to load province identifiers!"); - err = FAILED; - } - if (load_water_province_file(file_dict.get(get_water_province_file_key(), "")) != OK) { - UtilityFunctions::push_error("Failed to load water provinces!"); - err = FAILED; - } - if (load_region_file(file_dict.get(get_region_file_key(), "")) != OK) { - UtilityFunctions::push_error("Failed to load regions!"); - err = FAILED; - } - if (load_terrain_variant_file(file_dict.get(get_terrain_variant_file_key(), "")) != OK) { - UtilityFunctions::push_error("Failed to load terrain variants!"); - err = FAILED; - } - if (load_map_images(file_dict.get(get_province_image_file_key(), ""), file_dict.get(get_terrain_image_file_key(), "")) != OK) { - UtilityFunctions::push_error("Failed to load map images!"); - err = FAILED; - } - if (load_goods(file_dict.get(get_goods_file_key(), ""), file_dict.get(get_good_icons_dir_key(), "")) != OK) { - UtilityFunctions::push_error("Failed to load goods!"); - err = FAILED; - } - return err; -} - 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(); @@ -658,13 +223,13 @@ Dictionary GameSingleton::get_province_info_from_index(int32_t index) const { if (province == nullptr) return {}; Dictionary ret; - ret[get_province_info_province_key()] = province->get_identifier().c_str(); + 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()] = region->get_identifier().c_str(); + 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()] = rgo->get_identifier().c_str(); + 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(); @@ -676,11 +241,11 @@ Dictionary GameSingleton::get_province_info_from_index(int32_t index) const { Building const& building = buildings[idx]; Dictionary building_dict; - building_dict[get_building_info_building_key()] = building.get_identifier().c_str(); + 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()] = static_cast<std::string>(building.get_start_date()).c_str(); - building_dict[get_building_info_end_date_key()] = static_cast<std::string>(building.get_end_date()).c_str(); + building_dict[get_building_info_start_date_key()] = std_to_godot_string(static_cast<std::string>(building.get_start_date())); + building_dict[get_building_info_end_date_key()] = std_to_godot_string(static_cast<std::string>(building.get_end_date())); building_dict[get_building_info_expansion_progress_key()] = building.get_expansion_progress(); buildings_array[idx] = building_dict; @@ -718,7 +283,7 @@ Ref<Texture> GameSingleton::get_province_colour_texture() const { return province_colour_texture; } -Error GameSingleton::update_colour_image() { +Error GameSingleton::_update_colour_image() { static PackedByteArray colour_data_array; static constexpr int64_t colour_data_array_size = (MAX_INDEX + 1) * Map::MAPMODE_COLOUR_SIZE; colour_data_array.resize(colour_data_array_size); @@ -728,18 +293,18 @@ Error GameSingleton::update_colour_image() { err = FAILED; static constexpr int32_t PROVINCE_INDEX_SQRT = 1 << (sizeof(index_t) * 4); - if (province_colour_image.is_null()) + 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_image.is_null()) { - UtilityFunctions::push_error("Failed to update province colour image"); - return FAILED; - } - if (province_colour_texture.is_null()) + if (province_colour_texture.is_null()) { province_colour_texture = ImageTexture::create_from_image(province_colour_image); - else - province_colour_texture->update(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; } @@ -749,18 +314,18 @@ int32_t GameSingleton::get_mapmode_count() const { 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(); + 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(identifier.utf8().get_data()); + 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(); + _update_colour_image(); return OK; } @@ -770,12 +335,12 @@ int32_t GameSingleton::get_selected_province_index() const { void GameSingleton::set_selected_province(int32_t index) { game_manager.map.set_selected_province(index); - update_colour_image(); + _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, building_type_identifier.utf8().get_data()) != SUCCESS) { + if (game_manager.expand_building(province_index, godot_to_std_string(building_type_identifier)) != SUCCESS) { UtilityFunctions::push_error("Failed to expand ", building_type_identifier, " at province index ", province_index); return FAILED; } @@ -815,7 +380,7 @@ bool GameSingleton::can_decrease_speed() const { } String GameSingleton::get_longform_date() const { - return static_cast<std::string>(game_manager.get_today()).c_str(); + return std_to_godot_string(static_cast<std::string>(game_manager.get_today())); } void GameSingleton::try_tick() { diff --git a/extension/src/GameSingleton.hpp b/extension/src/GameSingleton.hpp index 99fe7bb..6bfb741 100644 --- a/extension/src/GameSingleton.hpp +++ b/extension/src/GameSingleton.hpp @@ -11,11 +11,16 @@ namespace OpenVic { const godot::Ref<godot::Image> image; public: + static constexpr size_t MAX_INDEX = 1 << (8 * sizeof(Map::terrain_t)); + TerrainVariant(std::string const& new_identfier, colour_t new_colour, - godot::Ref<godot::Image> const& new_image); + godot::Ref<godot::Image> const& new_image) + : HasIdentifier { new_identfier }, + HasColour { new_colour }, + image { new_image } {} TerrainVariant(TerrainVariant&&) = default; - godot::Ref<godot::Image> get_image() const; + godot::Ref<godot::Image> get_image() const { return image; } }; class GameSingleton : public godot::Object { GDCLASS(GameSingleton, godot::Object) @@ -36,16 +41,28 @@ namespace OpenVic { 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); - godot::Error _parse_terrain_entry(godot::String const& identifier, godot::Variant const& entry); + godot::Error _parse_terrain_entry(godot::String const& identifier, godot::Variant const& entry, godot::String const& terrain_texture_dir_path); godot::Error _parse_good_entry(godot::String const& identifier, godot::Variant const& entry); - 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_terrain_variant_file(godot::String const& file_path); - godot::Error load_map_images(godot::String const& province_image_path, godot::String const& terrain_image_path); - godot::Error load_goods(godot::String const& defines_path, godot::String const& icons_dir_path); + 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_terrain_variants(godot::String const& terrain_identifiers_path, godot::String const& terrain_texture_dir_path); + 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_goods(godot::String const& defines_path, godot::String const& icons_dir_path); + + godot::Error _load_province_identifier_file_compatibility_mode(godot::String const& file_path); + godot::Error _load_terrain_variants_compatibility_mode(godot::String const& terrain_image_path, godot::String const& terrain_texturesheet_path); + + /* Hardcoded data for defining things for which parsing from files has + * not been implemented, currently mapmodes and building types. + */ + godot::Error _load_hardcoded_defines(); + /* Generate the province_colour_texture from the current mapmode. + */ + godot::Error _update_colour_image(); void _on_state_updated(); protected: @@ -61,6 +78,7 @@ namespace OpenVic { static godot::StringName const& get_water_province_file_key(); static godot::StringName const& get_region_file_key(); static godot::StringName const& get_terrain_variant_file_key(); + static godot::StringName const& get_terrain_texture_dir_key(); static godot::StringName const& get_province_image_file_key(); static godot::StringName const& get_terrain_image_file_key(); static godot::StringName const& get_goods_file_key(); @@ -71,6 +89,11 @@ namespace OpenVic { */ godot::Error load_defines(godot::Dictionary const& file_dict); + /* Load the game's defines in compatiblity mode from the filepath + * pointing to the defines folder. + */ + godot::Error load_defines_compatibility_mode(godot::String const& file_path); + /* Post-load/restart game setup - reset the game to post-load state * and (re)generate starting data, e.g. buildings. */ @@ -121,10 +144,6 @@ namespace OpenVic { */ godot::Ref<godot::Texture> get_province_colour_texture() const; - /* Generate the province_colour_texture from the current mapmode. - */ - 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); diff --git a/extension/src/LoadGameCompatibility.cpp b/extension/src/LoadGameCompatibility.cpp new file mode 100644 index 0000000..29b773e --- /dev/null +++ b/extension/src/LoadGameCompatibility.cpp @@ -0,0 +1,154 @@ +#include "GameSingleton.hpp" + +#include <godot_cpp/classes/file_access.hpp> +#include <godot_cpp/variant/utility_functions.hpp> + +#include "openvic/utility/BMP.hpp" + +#include "Utilities.hpp" + +using namespace godot; +using namespace OpenVic; + +Error GameSingleton::_load_province_identifier_file_compatibility_mode(String const& file_path) { + UtilityFunctions::print("Loading compatibility mode province identifier 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 compatibility mode province identifier file: ", file_path); + return err == OK ? FAILED : err; + } + + int line_number = 0; + while (!file->eof_reached()) { + const PackedStringArray line = file->get_csv_line(";"); + line_number++; + + if (line.is_empty() || (line.size() == 1 && line[0].is_empty())) + continue; + + if (line_number < 2) continue; // skip header line + index_t id = NULL_INDEX; + colour_t colour = NULL_COLOUR; + if (line.size() > 0) { + if (line[0].is_empty()) { + id = game_manager.map.get_province_count() + 1; + } else if (line[0].is_valid_int()) { + const int64_t val = line[0].to_int(); + if (val > NULL_INDEX && val <= MAX_INDEX) id = val; + } + for (int i = 1; i < 4; ++i) { + if (line.size() > i) { + if (line[i].is_valid_int()) { + const int64_t int_val = line[i].to_int(); + if (int_val >= NULL_COLOUR && int_val <= FULL_COLOUR) { + colour = (colour << 8) | int_val; + continue; + } + } else if (line[i].is_valid_float()) { + const double double_val = line[i].to_float(); + if (std::trunc(double_val) == double_val) { + const int64_t int_val = double_val; + if (int_val >= NULL_COLOUR && int_val <= FULL_COLOUR) { + colour = (colour << 8) | int_val; + continue; + } + } + } + } + colour = NULL_COLOUR; + break; + } + } + if (id == NULL_INDEX || colour == NULL_COLOUR) { + UtilityFunctions::push_error("Invalid province ID-colour entry \"", line, "\" on line ", line_number, " in file: ", file_path); + err = FAILED; + continue; + } + static const std::string province_prefix = "PROV"; + if (game_manager.map.add_province(province_prefix + std::to_string(id), colour) != SUCCESS) err = FAILED; + } + game_manager.map.lock_provinces(); + return err; +} + +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_c_string(terrain_image_path)) != SUCCESS || bmp.read_header() != SUCCESS || bmp.read_palette() != SUCCESS) { + 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; + terrain_sheet.instantiate(); + if (terrain_sheet->load(terrain_texturesheet_path) != OK) { + 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; + + { + 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.0f, 0.0f, 1.0f }); + terrain_variants.add_item({ "terrain_water", 0xFFFFFF, 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 }) != SUCCESS) err = FAILED; + } + terrain_variants.lock(); + if (_generate_terrain_texture_array() != OK) return FAILED; + return err; +} + +Error GameSingleton::load_defines_compatibility_mode(String const& file_path) { + static const String province_identifier_file = "/map/definition.csv"; + static const String province_image_file = "/map/provinces.bmp"; + static const String terrain_image_file = "/map/terrain.bmp"; + static const String terrain_texture_dir = "/map/terrain/texturesheet.tga"; + + Error err = OK; + if (_load_province_identifier_file_compatibility_mode(file_path + province_identifier_file) != OK) { + UtilityFunctions::push_error("Failed to load province identifiers!"); + err = FAILED; + } + game_manager.map.lock_water_provinces(); + game_manager.map.lock_regions(); + if (_load_terrain_variants_compatibility_mode(file_path + terrain_image_file, file_path + terrain_texture_dir) != OK) { + UtilityFunctions::push_error("Failed to load terrain variants!"); + err = FAILED; + } + if (_load_map_images(file_path + province_image_file, file_path + terrain_image_file, true) != OK) { + UtilityFunctions::push_error("Failed to load map images!"); + err = FAILED; + } + game_manager.good_manager.lock_goods(); + if (_load_hardcoded_defines() != OK) { + UtilityFunctions::push_error("Failed to hardcoded defines!"); + err = FAILED; + } + return err; +} diff --git a/extension/src/LoadGameOpenVic.cpp b/extension/src/LoadGameOpenVic.cpp new file mode 100644 index 0000000..f856fd6 --- /dev/null +++ b/extension/src/LoadGameOpenVic.cpp @@ -0,0 +1,470 @@ +#include "GameSingleton.hpp" + +#include <godot_cpp/classes/file_access.hpp> +#include <godot_cpp/classes/json.hpp> +#include <godot_cpp/variant/utility_functions.hpp> + +#include "Utilities.hpp" + +using namespace godot; +using namespace OpenVic; + +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<Error(String const&, 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; +} + +static colour_t _parse_colour(Variant const& var) { + const Variant::Type type = var.get_type(); + if (type == Variant::ARRAY) { + Array const& colour_array = var; + if (colour_array.size() == 3) { + colour_t colour = NULL_COLOUR; + for (int jdx = 0; jdx < 3; ++jdx) { + Variant const& var = colour_array[jdx]; + if (var.get_type() != Variant::FLOAT) return NULL_COLOUR; + const double colour_double = var; + if (std::trunc(colour_double) != colour_double) return NULL_COLOUR; + const int64_t colour_int = static_cast<int64_t>(colour_double); + if (colour_int < 0 || colour_int > 255) return NULL_COLOUR; + colour = (colour << 8) | colour_int; + } + return colour; + } + } else if (type == Variant::STRING) { + String const& colour_string = var; + if (colour_string.is_valid_hex_number()) { + const int64_t colour_int = colour_string.hex_to_int(); + if (colour_int != NULL_COLOUR && colour_int <= MAX_COLOUR_RGB) + return colour_int; + } + } + return NULL_COLOUR; +} + +Error GameSingleton::_parse_province_identifier_entry(String const& identifier, Variant const& entry) { + const colour_t colour = _parse_colour(entry); + if (colour == NULL_COLOUR || colour > MAX_COLOUR_RGB) { + UtilityFunctions::push_error("Invalid colour for province identifier \"", identifier, "\": ", entry); + return FAILED; + } + return ERR(game_manager.map.add_province(godot_to_std_string(identifier), 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::_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(godot_to_std_string(identifier)) != SUCCESS) + err = FAILED; + } + } + game_manager.map.lock_water_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(godot_to_std_string(province_string)); + } 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(godot_to_std_string(identifier), 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::_parse_terrain_entry(String const& identifier, Variant const& entry, String const& terrain_texture_dir_path) { + const colour_t colour = _parse_colour(entry); + if (colour == NULL_COLOUR || colour > MAX_COLOUR_RGB) { + UtilityFunctions::push_error("Invalid colour for terrain texture \"", identifier, "\": ", entry); + return FAILED; + } + const String terrain_path = terrain_texture_dir_path + identifier; + Ref<Image> terrain_image; + terrain_image.instantiate(); + const Error err = terrain_image->load(terrain_path); + if (err != OK) { + UtilityFunctions::push_error("Failed to load terrain image: ", terrain_path); + return err; + } + return ERR(terrain_variants.add_item({ godot_to_std_string(identifier), colour, terrain_image })); +} + +Error GameSingleton::_load_terrain_variants(String const& terrain_identifiers_path, String const& terrain_texture_dir_path) { + Error err = _parse_json_dictionary_file("terrain variants", terrain_identifiers_path, "", + [this, terrain_texture_dir_path](String const& identifier, Variant const& entry) -> Error { + return _parse_terrain_entry(identifier, entry, terrain_texture_dir_path + String { "/" }); + }); + terrain_variants.lock(); + if (_generate_terrain_texture_array() != OK) return FAILED; + return err; +} + +Error GameSingleton::_generate_terrain_texture_array() { + Error err = OK; + if (terrain_variants.get_item_count() == 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.get_item_count() > TerrainVariant::MAX_INDEX) { + UtilityFunctions::push_error("Too many terrain textures - all after the first ", MAX_INDEX, " will be ignored"); + err = FAILED; + } + + Array terrain_images; + for (size_t i = 0; i < terrain_variants.get_item_count() && i < TerrainVariant::MAX_INDEX; ++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, terrain_image; + province_image.instantiate(); + terrain_image.instantiate(); + Error err = province_image->load(province_image_path); + if (err != OK) { + UtilityFunctions::push_error("Failed to load province image: ", province_image_path); + return err; + } + err = terrain_image->load(terrain_image_path); + if (err != OK) { + UtilityFunctions::push_error("Failed to load terrain image: ", terrain_image_path); + return err; + } + + if (flip_vertical) { + province_image->flip_y(); + terrain_image->flip_y(); + } + + // Validate dimensions and format + 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) != SUCCESS) 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; +} + +Error GameSingleton::_parse_good_entry(String const& identifier, Variant const& entry) { + if (entry.get_type() != Variant::DICTIONARY) { + UtilityFunctions::push_error("Invalid good entry for ", identifier, ": ", entry); + return FAILED; + } + Dictionary const& dict = entry; + + static const String key_category = "category"; + Variant const& var_category = dict.get(key_category, ""); + String category; + if (var_category.get_type() == Variant::STRING) category = var_category; + else UtilityFunctions::push_error("Invalid good category for ", identifier, ": ", var_category); + + static const String key_base_price = "base_price"; + Variant const& var_base_price = dict.get(key_base_price, NULL_PRICE); + price_t base_price = NULL_PRICE; + if (var_base_price.get_type() == Variant::FLOAT) base_price = var_base_price; + else UtilityFunctions::push_error("Invalid good base price for ", identifier, ": ", var_base_price); + + static const String key_colour = "colour"; + Variant const& var_colour = dict.get(key_colour, ""); + const colour_t colour = _parse_colour(var_colour); + if (colour > MAX_COLOUR_RGB) { + UtilityFunctions::push_error("Invalid good colour for ", identifier, ": ", var_colour); + return FAILED; + } + + static const String key_default_available = "default_available"; + Variant const& var_default_available = dict.get(key_default_available, true); + bool default_available = false; + if (var_default_available.get_type() == Variant::BOOL) default_available = var_default_available; + else UtilityFunctions::push_error("Invalid good available default bool value for ", identifier, ": ", var_default_available); + + static const String key_tradeable = "tradeable"; + Variant const& var_tradeable = dict.get(key_tradeable, true); + bool tradeable = false; + if (var_tradeable.get_type() == Variant::BOOL) tradeable = var_tradeable; + else UtilityFunctions::push_error("Invalid good tradeable bool value for ", identifier, ": ", var_tradeable); + + static const String key_currency = "currency"; + Variant const& var_currency = dict.get(key_currency, true); + bool currency = false; + if (var_currency.get_type() == Variant::BOOL) currency = var_currency; + else UtilityFunctions::push_error("Invalid good currency bool value for ", identifier, ": ", var_currency); + + static const String key_overseas_maintenance = "overseas_maintenance"; + Variant const& var_overseas_maintenance = dict.get(key_overseas_maintenance, true); + bool overseas_maintenance = false; + if (var_overseas_maintenance.get_type() == Variant::BOOL) overseas_maintenance = var_overseas_maintenance; + else UtilityFunctions::push_error("Invalid good overseas maintenance bool value for ", identifier, ": ", var_overseas_maintenance); + + return ERR(game_manager.good_manager.add_good(godot_to_std_string(identifier), godot_to_std_string(category), + colour, base_price, default_available, tradeable, currency, overseas_maintenance)); +} + +Error GameSingleton::_load_goods(String const& defines_path, String const& icons_dir_path) { + Error err = _parse_json_dictionary_file("good", defines_path, "good_", + [this](String const& identifier, Variant const& entry) -> Error { + return _parse_good_entry(identifier, entry); + }); + game_manager.good_manager.lock_goods(); + for (Good const& good : game_manager.good_manager.get_goods()) { + Ref<Image> image; + image.instantiate(); + const String path = icons_dir_path + String { "/" } + std_to_godot_string(good.get_identifier()) + ".png"; + const Error good_err = image->load(path); + if (good_err || image.is_null()) { + UtilityFunctions::push_error("Failed to load good icon image: ", path); + err = FAILED; + continue; + } + Ref<Texture> tex = ImageTexture::create_from_image(image); + if (tex.is_null()) { + UtilityFunctions::push_error("Failed to generate good icon texture: ", path); + err = FAILED; + continue; + } + good_icons[std_to_godot_string(good.get_identifier())] = tex; + } + return err; +} + +StringName const& GameSingleton::get_province_identifier_file_key() { + static const StringName key = "province_identifiers"; + return key; +} +StringName const& GameSingleton::get_water_province_file_key() { + static const StringName key = "water_provinces"; + return key; +} +StringName const& GameSingleton::get_region_file_key() { + static const StringName key = "regions"; + return key; +} +StringName const& GameSingleton::get_terrain_variant_file_key() { + static const StringName key = "terrain_variants"; + return key; +} +StringName const& GameSingleton::get_terrain_texture_dir_key() { + static const StringName key = "terrain_textures"; + return key; +} +StringName const& GameSingleton::get_province_image_file_key() { + static const StringName key = "province_image"; + return key; +} +StringName const& GameSingleton::get_terrain_image_file_key() { + static const StringName key = "terrain_image"; + return key; +} +StringName const& GameSingleton::get_goods_file_key() { + static const StringName key = "goods"; + return key; +} +StringName const& GameSingleton::get_good_icons_dir_key() { + static const StringName key = "good_icons"; + return key; +} + +Error GameSingleton::load_defines(Dictionary const& file_dict) { + Error err = OK; + if (_load_province_identifier_file(file_dict.get(get_province_identifier_file_key(), "")) != OK) { + UtilityFunctions::push_error("Failed to load province identifiers!"); + err = FAILED; + } + if (_load_water_province_file(file_dict.get(get_water_province_file_key(), "")) != OK) { + UtilityFunctions::push_error("Failed to load water provinces!"); + err = FAILED; + } + if (_load_region_file(file_dict.get(get_region_file_key(), "")) != OK) { + UtilityFunctions::push_error("Failed to load regions!"); + err = FAILED; + } + if (_load_terrain_variants(file_dict.get(get_terrain_variant_file_key(), ""), + file_dict.get(get_terrain_texture_dir_key(), "")) != OK) { + UtilityFunctions::push_error("Failed to load terrain variants!"); + err = FAILED; + } + if (_load_map_images(file_dict.get(get_province_image_file_key(), ""), file_dict.get(get_terrain_image_file_key(), "")) != OK) { + UtilityFunctions::push_error("Failed to load map images!"); + err = FAILED; + } + if (_load_goods(file_dict.get(get_goods_file_key(), ""), file_dict.get(get_good_icons_dir_key(), "")) != OK) { + UtilityFunctions::push_error("Failed to load goods!"); + err = FAILED; + } + if (_load_hardcoded_defines() != OK) { + UtilityFunctions::push_error("Failed to hardcoded defines!"); + err = FAILED; + } + return err; +} diff --git a/extension/src/LoadLocalisation.cpp b/extension/src/LoadLocalisation.cpp index e80d996..b660435 100644 --- a/extension/src/LoadLocalisation.cpp +++ b/extension/src/LoadLocalisation.cpp @@ -31,7 +31,7 @@ LoadLocalisation::~LoadLocalisation() { } Error LoadLocalisation::_load_file_into_translation(String const& file_path, Ref<Translation> translation) { - Ref<FileAccess> file = FileAccess::open(file_path, FileAccess::ModeFlags::READ); + 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); @@ -42,15 +42,18 @@ Error LoadLocalisation::_load_file_into_translation(String const& file_path, Ref PackedStringArray line = file->get_csv_line(); line_number++; if (line.size() < 2 || line[0].is_empty() || line[1].is_empty()) { - if (!line[0].is_empty()) + if (!line[0].is_empty()) { UtilityFunctions::push_warning("Key \"", line[0], "\" missing value on line ", line_number, " in file: ", file_path); - else if (line.size() >= 2 && !line[1].is_empty()) + 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 OK; + return err; } Ref<Translation> LoadLocalisation::_get_translation(String const& locale) { @@ -72,40 +75,38 @@ Error LoadLocalisation::load_file(String const& file_path, String const& locale) * 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; + } Ref<Translation> translation = _get_translation(locale); - if (DirAccess::dir_exists_absolute(dir_path)) { - Error err = OK; - for (String const& file_name : DirAccess::get_files_at(dir_path)) { - if (file_name.get_extension().to_lower() == "csv") { - String file_path = dir_path.path_join(file_name); - if (_load_file_into_translation(file_path, translation) != OK) - err = FAILED; - } + Error err = OK; + for (String const& file_name : DirAccess::get_files_at(dir_path)) { + if (file_name.get_extension().to_lower() == "csv") { + String file_path = dir_path.path_join(file_name); + if (_load_file_into_translation(file_path, translation) != OK) + err = FAILED; } - return err; } - UtilityFunctions::push_error("Locale directory does not exist: ", dir_path); - return FAILED; + return err; } /* REQUIREMENTS * FS-23 */ Error LoadLocalisation::load_localisation_dir(String const& dir_path) { - if (DirAccess::dir_exists_absolute(dir_path)) { - 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)) { - if (load_locale_dir(dir_path.path_join(locale_name), locale_name) != OK) - err = FAILED; - } else { - err = FAILED; - UtilityFunctions::push_error("Invalid locale directory name: ", locale_name); - } - } - return err; + if(!DirAccess::dir_exists_absolute(dir_path)) { + UtilityFunctions::push_error("Localisation directory does not exist: ", dir_path); + return FAILED; + } + 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)) + 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; } - UtilityFunctions::push_error("Localisation directory does not exist: ", dir_path); - return FAILED; + return err; } diff --git a/extension/src/Utilities.hpp b/extension/src/Utilities.hpp new file mode 100644 index 0000000..16193e8 --- /dev/null +++ b/extension/src/Utilities.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include <godot_cpp/variant/string.hpp> + +#include "openvic/Types.hpp" + +#define ERR(x) ((x) == SUCCESS ? OK : FAILED) + +namespace OpenVic { + + inline char const* godot_to_c_string(godot::String const& str) { + return str.ascii().get_data(); + } + + inline std::string godot_to_std_string(godot::String const& str) { + return godot_to_c_string(str); + } + + inline godot::String std_to_godot_string(std::string const& str) { + return str.c_str(); + } +} diff --git a/extension/src/register_types.h b/extension/src/register_types.h index cd20e8d..dd24689 100644 --- a/extension/src/register_types.h +++ b/extension/src/register_types.h @@ -3,4 +3,4 @@ #include <godot_cpp/godot.hpp> void initialize_openvic_types(godot::ModuleInitializationLevel); -void uninitialize_openvic_types(godot::ModuleInitializationLevel);
\ No newline at end of file +void uninitialize_openvic_types(godot::ModuleInitializationLevel); diff --git a/game/src/Autoload/Events.gd b/game/src/Autoload/Events.gd index db63330..0193a08 100644 --- a/game/src/Autoload/Events.gd +++ b/game/src/Autoload/Events.gd @@ -10,17 +10,29 @@ var _define_filepaths_dict : Dictionary = { GameSingleton.get_water_province_file_key(): "res://common/map/water.json", GameSingleton.get_region_file_key(): "res://common/map/regions.json", GameSingleton.get_terrain_variant_file_key(): "res://common/map/terrain.json", + GameSingleton.get_terrain_texture_dir_key(): "res://art/terrain/", GameSingleton.get_province_image_file_key(): "res://common/map/provinces.png", GameSingleton.get_terrain_image_file_key(): "res://common/map/terrain.png", GameSingleton.get_goods_file_key(): "res://common/goods.json", GameSingleton.get_good_icons_dir_key(): "res://art/economy/goods" } +# Set this to your Vic2 install dir or a mod's dir to enable compatibility mode +# (this won't work for mods which rely on vanilla map assets, copy missing assets +# into the mod's dir for a temporary fix) +const _compatibility_mode_path : String = "" + # REQUIREMENTS # * FS-333, FS-334, FS-335, FS-341 func _ready(): var start := Time.get_ticks_usec() - if GameSingleton.load_defines(_define_filepaths_dict) != OK: - push_error("Failed to load game defines!") + + if _compatibility_mode_path: + if GameSingleton.load_defines_compatibility_mode(_compatibility_mode_path) != OK: + push_error("Errors loading game defines!") + else: + if GameSingleton.load_defines(_define_filepaths_dict) != OK: + push_error("Errors loading game defines!") + var end := Time.get_ticks_usec() print("Loading took ", float(end - start) / 1000000, " seconds") diff --git a/game/src/GameSession/MapView.gd b/game/src/GameSession/MapView.gd index 22a7e6b..ced8bb7 100644 --- a/game/src/GameSession/MapView.gd +++ b/game/src/GameSession/MapView.gd @@ -69,7 +69,7 @@ func _ready(): _map_mesh = _map_mesh_instance.mesh # Set map mesh size and get bounds - const pixels_per_terrain_tile : float = 64.0 + const pixels_per_terrain_tile : float = 32.0 _map_shader_material.set_shader_parameter(Events.ShaderManager.param_terrain_tile_factor, float(GameSingleton.get_height()) / pixels_per_terrain_tile) var map_mesh_aabb := _map_mesh.get_core_aabb() * _map_mesh_instance.transform |