From 8921ec4d9e79840264cd3540caa2d71fc7ab67a0 Mon Sep 17 00:00:00 2001 From: BrickPi Date: Tue, 6 Aug 2024 16:39:24 -0500 Subject: Simulation Side Rivers --- src/openvic-simulation/dataloader/Dataloader.cpp | 5 +- src/openvic-simulation/map/MapDefinition.cpp | 214 ++++++++++++++++++++++- src/openvic-simulation/map/MapDefinition.hpp | 15 +- 3 files changed, 223 insertions(+), 11 deletions(-) diff --git a/src/openvic-simulation/dataloader/Dataloader.cpp b/src/openvic-simulation/dataloader/Dataloader.cpp index 1be040f..99a8d68 100644 --- a/src/openvic-simulation/dataloader/Dataloader.cpp +++ b/src/openvic-simulation/dataloader/Dataloader.cpp @@ -683,7 +683,7 @@ bool Dataloader::_load_map_dir(DefinitionManager& definition_manager) const { static constexpr std::string_view default_provinces = "provinces.bmp"; static constexpr std::string_view default_positions = "positions.txt"; static constexpr std::string_view default_terrain = "terrain.bmp"; - static constexpr std::string_view default_rivers = "rivers.bmp"; // TODO - load rivers into map pixel data + static constexpr std::string_view default_rivers = "rivers.bmp"; static constexpr std::string_view default_terrain_definition = "terrain.txt"; static constexpr std::string_view default_tree_definition = "trees.txt"; /* Tree textures and density values (unused). */ static constexpr std::string_view default_continent = "continent.txt"; @@ -781,7 +781,8 @@ bool Dataloader::_load_map_dir(DefinitionManager& definition_manager) const { if (!map_definition.load_map_images( lookup_file(append_string_views(map_directory, provinces)), - lookup_file(append_string_views(map_directory, terrain)), false + lookup_file(append_string_views(map_directory, terrain)), + lookup_file(append_string_views(map_directory, rivers)), false )) { Logger::error("Failed to load map images!"); ret = false; diff --git a/src/openvic-simulation/map/MapDefinition.cpp b/src/openvic-simulation/map/MapDefinition.cpp index f3bb619..7ed2919 100644 --- a/src/openvic-simulation/map/MapDefinition.cpp +++ b/src/openvic-simulation/map/MapDefinition.cpp @@ -1,9 +1,11 @@ #include "MapDefinition.hpp" +#include #include #include "openvic-simulation/types/Colour.hpp" #include "openvic-simulation/types/OrderedContainers.hpp" +#include "openvic-simulation/types/Vector.hpp" #include "openvic-simulation/utility/BMP.hpp" #include "openvic-simulation/utility/Logger.hpp" @@ -12,6 +14,8 @@ using namespace OpenVic::NodeTools; MapDefinition::MapDefinition() : dims { 0, 0 }, max_provinces { ProvinceDefinition::MAX_INDEX } {} +RiverSegment::RiverSegment(uint8_t new_size, std::vector new_points) : size { new_size }, points { std::move(new_points) } {} + bool MapDefinition::add_province_definition(std::string_view identifier, colour_t colour) { if (province_definitions.size() >= max_provinces) { Logger::error( @@ -490,7 +494,7 @@ static constexpr colour_t colour_at(uint8_t const* colour_data, int32_t idx) { return { colour_data[idx + 2], colour_data[idx + 1], colour_data[idx] }; } -bool MapDefinition::load_map_images(fs::path const& province_path, fs::path const& terrain_path, bool detailed_errors) { +bool MapDefinition::load_map_images(fs::path const& province_path, fs::path const& terrain_path, fs::path const& rivers_path, bool detailed_errors) { if (!province_definitions_are_locked()) { Logger::error("Province index image cannot be generated until after provinces are locked!"); return false; @@ -500,12 +504,14 @@ bool MapDefinition::load_map_images(fs::path const& province_path, fs::path cons return false; } + static constexpr uint16_t expected_province_bpp = 24; + static constexpr uint16_t expected_terrain_rivers_bpp = 8; + BMP province_bmp; if (!(province_bmp.open(province_path) && province_bmp.read_header() && province_bmp.read_pixel_data())) { Logger::error("Failed to read BMP for compatibility mode province image: ", province_path); return false; } - static constexpr uint16_t expected_province_bpp = 24; if (province_bmp.get_bits_per_pixel() != expected_province_bpp) { Logger::error( "Invalid province BMP bits per pixel: ", province_bmp.get_bits_per_pixel(), " (expected ", expected_province_bpp, @@ -519,18 +525,33 @@ bool MapDefinition::load_map_images(fs::path const& province_path, fs::path cons Logger::error("Failed to read BMP for compatibility mode terrain image: ", terrain_path); return false; } - static constexpr uint16_t expected_terrain_bpp = 8; - if (terrain_bmp.get_bits_per_pixel() != expected_terrain_bpp) { + if (terrain_bmp.get_bits_per_pixel() != expected_terrain_rivers_bpp) { + Logger::error( + "Invalid terrain BMP bits per pixel: ", terrain_bmp.get_bits_per_pixel(), " (expected ", expected_terrain_rivers_bpp, ")" + ); + return false; + } + + BMP rivers_bmp; + if (!(rivers_bmp.open(rivers_path) && rivers_bmp.read_header() && rivers_bmp.read_palette() && rivers_bmp.read_pixel_data())) { + Logger::error("Failed to read BMP for compatibility mode river image: ", rivers_path); + return false; + } + if (rivers_bmp.get_bits_per_pixel() != expected_terrain_rivers_bpp) { Logger::error( - "Invalid terrain BMP bits per pixel: ", terrain_bmp.get_bits_per_pixel(), " (expected ", expected_terrain_bpp, ")" + "Invalid rivers BMP bits per pixel: ", rivers_bmp.get_bits_per_pixel(), " (expected ", expected_terrain_rivers_bpp, ")" ); return false; } - if (province_bmp.get_width() != terrain_bmp.get_width() || province_bmp.get_height() != terrain_bmp.get_height()) { + if (province_bmp.get_width() != terrain_bmp.get_width() || + province_bmp.get_height() != terrain_bmp.get_height() || + province_bmp.get_width() != rivers_bmp.get_width() || + province_bmp.get_height() != rivers_bmp.get_height() + ) { Logger::error( - "Mismatched province and terrain BMP dims: ", province_bmp.get_width(), "x", province_bmp.get_height(), " vs ", - terrain_bmp.get_width(), "x", terrain_bmp.get_height() + "Mismatched map BMP dims: provinces:", province_bmp.get_width(), "x", province_bmp.get_height(), ", terrain: ", + terrain_bmp.get_width(), "x", terrain_bmp.get_height(), ", rivers: ", rivers_bmp.get_width(), "x", rivers_bmp.get_height() ); return false; } @@ -637,6 +658,183 @@ bool MapDefinition::load_map_images(fs::path const& province_path, fs::path cons Logger::warning("Province image is missing ", missing, " province colours"); } + // Constants in the River BMP Palette + static constexpr uint8_t START_COLOUR = 0; + static constexpr uint8_t MERGE_COLOUR = 1; + static constexpr uint8_t RIVER_SIZE_1 = 2; + static constexpr uint8_t RIVER_SIZE_2 = 3; + static constexpr uint8_t RIVER_SIZE_3 = 4; + static constexpr uint8_t RIVER_SIZE_4 = 5; + static constexpr uint8_t RIVER_SIZE_5 = 6; + static constexpr uint8_t RIVER_SIZE_6 = 7; + static constexpr uint8_t RIVER_SIZE_7 = 8; + static constexpr uint8_t RIVER_SIZE_8 = 9; + static constexpr uint8_t RIVER_SIZE_9 = 10; + static constexpr uint8_t RIVER_SIZE_10 = 11; + + uint8_t const* river_data = rivers_bmp.get_pixel_data().data(); + + /** Generating River Segments - Unoptimised & Unprofiled + 1. check pixels up, right, down, and left from last_segment_end for a colour <12 + 2. add first point + 3. set size of segment based on color value at first point + 4. loop, adding adjacent points until the colour value changes (to make sure we don't backtrack, last_segment_direction provides a pixel to automatically ignore) + last_segment_direction: + 0 -> start, ignore nothing + 1 -> ignore up + 2 -> ignore down + 3 -> ignore left + 4 -> ignore right + 5. if the colour value changes to MERGE_COLOUR, add the point & finish the segment + 6. if there is no further point, finish the segment + 7. if the colour value changes to a different river size (>1 && <12), recursively call this function on the next segment + */ + const std::function next_segment = [&river_data, &rivers_bmp, &next_segment](ivec2_t last_segment_end, uint8_t last_segment_direction, river_t& river) { + size_t idx = last_segment_end.x + last_segment_end.y * rivers_bmp.get_width(); + + std::vector points; + uint8_t direction; + + // check pixel above + if (last_segment_end.y > 0 && last_segment_direction != 1) { // check for bounds & ignore direction + if (river_data[idx - rivers_bmp.get_width()] < 12) { + points.push_back({ last_segment_end.x, last_segment_end.y - 1 }); + direction = 2; + } + } + // check pixel to right + if (last_segment_end.x < rivers_bmp.get_width() - 1 && last_segment_direction != 4) { + if (river_data[idx + 1] < 12) { + points.push_back({ last_segment_end.x + 1, last_segment_end.y }); + direction = 3; + } + } + // check pixel below + if (last_segment_end.y < rivers_bmp.get_height() - 1 && last_segment_direction != 2) { + if (river_data[idx + rivers_bmp.get_width()] < 12) { + points.push_back({ last_segment_end.x, last_segment_end.y + 1 }); + direction = 1; + } + } + // check pixel to left + if (last_segment_end.x > 0 && last_segment_direction != 3) { + if (river_data[idx - 1] < 12) { + points.push_back({ last_segment_end.x - 1, last_segment_end.y }); + direction = 4; + } + } + + uint8_t size = river_data[points.front().x + points.front().y * rivers_bmp.get_width()] - 1; // size of river from 1 - 10 determined by colour + + bool river_complete = false; + ivec2_t new_point; + + size_t limit = 0; // stops infinite loop + + while (true) { + limit++; + if (limit == 4096) { + Logger::error("River segment starting at (", points.front().x, ", ", points.front().y, ") is longer than limit 4096, check for misplaced pixels or other definition errors!"); + river_complete = true; + break; + } + + idx = points.back().x + points.back().y * rivers_bmp.get_width(); + + ivec2_t merge_location; + bool merge; + + // check pixel above + if (points.back().y > 0 && direction != 1) { // check for bounds & ignore direction + if (river_data[idx - rivers_bmp.get_width()] == size + 1) { // now checking if size changes too + points.push_back({ points.back().x, points.back().y - 1 }); + direction = 2; + continue; + } else if (river_data[idx - rivers_bmp.get_width()] == MERGE_COLOUR) { // check for merge node + merge_location = { points.back().x, points.back().y - 1 }; + merge = true; + } else if (river_data[idx - rivers_bmp.get_width()] > 1 && river_data[idx - rivers_bmp.get_width()] < 12) { // new segment + new_point = { points.back().x, points.back().y - 1 }; + direction = 2; + break; + } + } + // check pixel to right + if (points.back().x < rivers_bmp.get_width() - 1 && direction != 4) { + if (river_data[idx + 1] == size + 1) { + points.push_back({ points.back().x + 1, points.back().y }); + direction = 3; + continue; + } else if (river_data[idx + 1] == MERGE_COLOUR) { + merge_location = { points.back().x + 1, points.back().y }; + merge = true; + } else if (river_data[idx + 1] > 1 && river_data[idx + 1] < 12) { // new segment + new_point = { points.back().x + 1, points.back().y }; + direction = 3; + break; + } + } + // check pixel below + if (points.back().y < rivers_bmp.get_height() - 1 && direction != 2) { + if (river_data[idx + rivers_bmp.get_width()] == size + 1) { + points.push_back({ points.back().x, points.back().y + 1 }); + direction = 1; + continue; + } else if (river_data[idx + rivers_bmp.get_width()] == MERGE_COLOUR) { + merge_location = { points.back().x, points.back().y + 1 }; + merge = true; + } else if (river_data[idx + rivers_bmp.get_width()] > 1 && river_data[idx + rivers_bmp.get_width()] < 12) { // new segment + new_point = { points.back().x, points.back().y + 1 }; + direction = 1; + break; + } + } + // check pixel to left + if (points.back().x > 0 && direction != 3) { + if (river_data[idx - 1] == size + 1) { + points.push_back({ points.back().x - 1, points.back().y }); + direction = 4; + continue; + } else if (river_data[idx - 1] == MERGE_COLOUR) { + merge_location = { points.back().x - 1, points.back().y }; + merge = true; + } else if (river_data[idx - 1] > 1 && river_data[idx - 1] < 12) { // new segment + new_point = { points.back().x - 1, points.back().y }; + direction = 4; + break; + } + } + + // no further points + if (merge) points.push_back(merge_location); + river_complete = true; + break; + } + + // TODO: implement optimisation algorithm to remove irrelevant points. Ramer-Douglas-Peucker? + + // add segment then recursively call if neeeded + river.push_back({ size, points }); + + if (river_complete) return; + next_segment(new_point, direction, river); + }; + + // find every river source and then run the segment algorithm. + int temp = 0; + for (int y = 0; y < rivers_bmp.get_height(); ++y) { + for (int x = 0; x < rivers_bmp.get_width(); ++x) { + if (river_data[x + y * rivers_bmp.get_width()] == START_COLOUR) { // start of a river + river_t river; + + next_segment({ x, y }, 0, river); + + rivers.push_back(river); + } + } + } + Logger::info("Generated ", rivers.size(), " rivers."); + return ret; } diff --git a/src/openvic-simulation/map/MapDefinition.hpp b/src/openvic-simulation/map/MapDefinition.hpp index 9ec4367..bd3b5ae 100644 --- a/src/openvic-simulation/map/MapDefinition.hpp +++ b/src/openvic-simulation/map/MapDefinition.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -20,6 +21,16 @@ namespace OpenVic { struct BuildingTypeManager; struct ModifierManager; + struct RiverSegment { + friend struct MapDefinition; + + private: + const uint8_t PROPERTY(size); + const std::vector PROPERTY(points); + + RiverSegment(uint8_t new_size, std::vector new_points); + }; + /* REQUIREMENTS: * MAP-4 */ @@ -35,6 +46,7 @@ namespace OpenVic { private: using colour_index_map_t = ordered_map; + using river_t = std::vector; IdentifierRegistry IDENTIFIER_REGISTRY_CUSTOM_INDEX_OFFSET(province_definition, 1); IdentifierRegistry IDENTIFIER_REGISTRY(region); @@ -43,6 +55,7 @@ namespace OpenVic { ProvinceSet water_provinces; TerrainTypeManager PROPERTY_REF(terrain_type_manager); + std::vector PROPERTY(rivers); // TODO: calculate provinces affected by crossing ivec2_t PROPERTY(dims); std::vector PROPERTY(province_shape_image); colour_index_map_t colour_index_map; @@ -104,7 +117,7 @@ namespace OpenVic { bool load_province_positions(BuildingTypeManager const& building_type_manager, ast::NodeCPtr root); static bool load_region_colours(ast::NodeCPtr root, std::vector& colours); bool load_region_file(ast::NodeCPtr root, std::vector const& colours); - bool load_map_images(fs::path const& province_path, fs::path const& terrain_path, bool detailed_errors); + bool load_map_images(fs::path const& province_path, fs::path const& terrain_path, fs::path const& rivers_path, bool detailed_errors); bool generate_and_load_province_adjacencies(std::vector const& additional_adjacencies); bool load_climate_file(ModifierManager const& modifier_manager, ast::NodeCPtr root); bool load_continent_file(ModifierManager const& modifier_manager, ast::NodeCPtr root); -- cgit v1.2.3-56-ga3b1