diff options
author | Hop311 <hop3114@gmail.com> | 2023-09-08 18:12:22 +0200 |
---|---|---|
committer | Hop311 <hop3114@gmail.com> | 2023-09-08 18:12:22 +0200 |
commit | 7772f8871348b7b52cb0a478bb76df68d8799a07 (patch) | |
tree | fd8c4626b2cee69a9fe9250365af6b18eea63c70 /src/openvic-simulation | |
parent | 7f9a9a8241ba81be9213e6606b8be4a48f1cbaab (diff) |
More refactoring and duplicate code removal
Diffstat (limited to 'src/openvic-simulation')
40 files changed, 4938 insertions, 0 deletions
diff --git a/src/openvic-simulation/GameAdvancementHook.cpp b/src/openvic-simulation/GameAdvancementHook.cpp new file mode 100644 index 0000000..ac16158 --- /dev/null +++ b/src/openvic-simulation/GameAdvancementHook.cpp @@ -0,0 +1,76 @@ +#include "GameAdvancementHook.hpp" + +using namespace OpenVic; + +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) { + time_point_t 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/src/openvic-simulation/GameAdvancementHook.hpp b/src/openvic-simulation/GameAdvancementHook.hpp new file mode 100644 index 0000000..59e43a4 --- /dev/null +++ b/src/openvic-simulation/GameAdvancementHook.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include <chrono> +#include <functional> +#include <vector> + +namespace OpenVic { + // 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: + using time_point_t = std::chrono::time_point<std::chrono::high_resolution_clock>; + + time_point_t 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/src/openvic-simulation/GameManager.cpp b/src/openvic-simulation/GameManager.cpp new file mode 100644 index 0000000..ee5050d --- /dev/null +++ b/src/openvic-simulation/GameManager.cpp @@ -0,0 +1,132 @@ +#include "GameManager.hpp" + +#include "openvic-simulation/utility/Logger.hpp" + +using namespace OpenVic; + +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; + } +} + +/* REQUIREMENTS: + * SS-98, SS-101 + */ +void GameManager::tick() { + today++; + Logger::info("Tick: ", today); + map.tick(today); + set_needs_update(); +} + +bool GameManager::setup() { + session_start = time(nullptr); + clock.reset(); + today = { 1836 }; + good_manager.reset_to_defaults(); + bool ret = map.setup(good_manager, building_manager, pop_manager); + set_needs_update(); + return ret; +} + +Date const& GameManager::get_today() const { + return today; +} + +bool GameManager::expand_building(Province::index_t province_index, const std::string_view building_type_identifier) { + set_needs_update(); + Province* province = map.get_province_by_index(province_index); + if (province == nullptr) { + Logger::error("Invalid province index ", province_index, " while trying to expand building ", building_type_identifier); + return false; + } + return province->expand_building(building_type_identifier); +} + +bool GameManager::load_hardcoded_defines() { + bool ret = true; + + 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 { + { "mapmode_terrain", + [](Map const&, Province const& province) -> colour_t { + return LOW_ALPHA_VALUE | (province.is_water() ? 0x4287F5 : 0x0D7017); + } }, + { "mapmode_province", + [](Map const&, Province const& province) -> colour_t { + return HIGH_ALPHA_VALUE | province.get_colour(); + } }, + { "mapmode_region", + [](Map const&, Province const& province) -> colour_t { + Region const* region = province.get_region(); + if (region != nullptr) return HIGH_ALPHA_VALUE | region->get_colour(); + return NULL_COLOUR; + } }, + { "mapmode_index", + [](Map const& map, Province const& province) -> colour_t { + const colour_t f = fraction_to_colour_byte(province.get_index(), map.get_province_count() + 1); + return HIGH_ALPHA_VALUE | (f << 16) | (f << 8) | f; + } }, + { "mapmode_rgo", + [](Map const& map, Province const& province) -> colour_t { + Good const* rgo = province.get_rgo(); + if (rgo != nullptr) return HIGH_ALPHA_VALUE | rgo->get_colour(); + return NULL_COLOUR; + } }, + { "mapmode_infrastructure", + [](Map const& map, Province const& province) -> colour_t { + Building const* railroad = province.get_building_by_identifier("building_railroad"); + if (railroad != nullptr) { + colour_t val = fraction_to_colour_byte(railroad->get_level(), railroad->get_type().get_max_level() + 1, 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; + } + return HIGH_ALPHA_VALUE | val; + } + return NULL_COLOUR; + } }, + { "mapmode_population", + [](Map const& map, Province const& province) -> colour_t { + return HIGH_ALPHA_VALUE | (fraction_to_colour_byte(province.get_total_population(), map.get_highest_province_population() + 1, 0.1f, 1.0f) << 8); + } }, + { "mapmode_culture", + [](Map const& map, Province const& province) -> colour_t { + HasIdentifierAndColour const* largest = get_largest_item(province.get_culture_distribution()).first; + return largest != nullptr ? HIGH_ALPHA_VALUE | largest->get_colour() : NULL_COLOUR; + } }, + { "mapmode_religion", + [](Map const& map, Province const& province) -> colour_t { + HasIdentifierAndColour const* largest = get_largest_item(province.get_religion_distribution()).first; + return largest != nullptr ? HIGH_ALPHA_VALUE | largest->get_colour() : NULL_COLOUR; + } } + }; + for (mapmode_t const& mapmode : mapmodes) + ret &= map.add_mapmode(mapmode.first, mapmode.second); + 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) + ret &= building_manager.add_building_type(std::get<0>(type), std::get<1>(type), std::get<2>(type)); + building_manager.lock_building_types(); + + return ret; +} diff --git a/src/openvic-simulation/GameManager.hpp b/src/openvic-simulation/GameManager.hpp new file mode 100644 index 0000000..d221a99 --- /dev/null +++ b/src/openvic-simulation/GameManager.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include "openvic-simulation/GameAdvancementHook.hpp" +#include "openvic-simulation/economy/Good.hpp" +#include "openvic-simulation/map/Map.hpp" + +namespace OpenVic { + struct GameManager { + using state_updated_func_t = std::function<void()>; + + Map map; + BuildingManager building_manager; + GoodManager good_manager; + PopManager pop_manager; + GameAdvancementHook clock; + + private: + time_t session_start; /* SS-54, as well as allowing time-tracking */ + 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); + + bool setup(); + + Date const& get_today() const; + bool expand_building(Province::index_t province_index, const std::string_view building_type_identifier); + + /* Hardcoded data for defining things for which parsing from files has + * not been implemented, currently mapmodes and building types. + */ + bool load_hardcoded_defines(); + }; +} diff --git a/src/openvic-simulation/dataloader/Dataloader.cpp b/src/openvic-simulation/dataloader/Dataloader.cpp new file mode 100644 index 0000000..334d5b8 --- /dev/null +++ b/src/openvic-simulation/dataloader/Dataloader.cpp @@ -0,0 +1,288 @@ +#include "Dataloader.hpp" + +#include "openvic-simulation/GameManager.hpp" +#include "openvic-simulation/utility/Logger.hpp" + +#include <openvic-dataloader/csv/Parser.hpp> +#include <openvic-dataloader/detail/CallbackOStream.hpp> +#include <openvic-dataloader/v2script/Parser.hpp> + +using namespace OpenVic; +using namespace OpenVic::NodeTools; +using namespace ovdl; + +bool Dataloader::set_roots(std::vector<std::filesystem::path> new_roots) { + if (!roots.empty()) { + Logger::error("Overriding existing dataloader roots!"); + roots.clear(); + } + bool ret = true; + for (std::reverse_iterator<std::vector<std::filesystem::path>::const_iterator> it = new_roots.crbegin(); it != new_roots.crend(); ++it) { + if (std::find(roots.begin(), roots.end(), *it) == roots.end()) { + if (std::filesystem::is_directory(*it)) { + Logger::info("Adding dataloader root: ", *it); + roots.push_back(*it); + } else { + Logger::error("Invalid dataloader root (must be an existing directory): ", *it); + ret = false; + } + } else { + Logger::error("Duplicate dataloader root: ", *it); + ret = false; + } + } + if (roots.empty()) { + Logger::error("Dataloader has no roots after attempting to add ", new_roots.size()); + ret = false; + } + return ret; +} + +std::filesystem::path Dataloader::lookup_file(std::filesystem::path const& path) const { + for (std::filesystem::path const& root : roots) { + const std::filesystem::path composed = root / path; + if (std::filesystem::is_regular_file(composed)) { + return composed; + } + } + Logger::error("Lookup for ", path, " failed!"); + return {}; +} + +const std::filesystem::path Dataloader::TXT = ".txt"; + +static bool contains_file_with_name(std::vector<std::filesystem::path> const& paths, + std::filesystem::path const& name) { + + for (std::filesystem::path const& path : paths) { + if (path.filename() == name) return true; + } + return false; +} + +std::vector<std::filesystem::path> Dataloader::lookup_files_in_dir(std::filesystem::path const& path, + std::filesystem::path const* extension) const { + + std::vector<std::filesystem::path> ret; + for (std::filesystem::path const& root : roots) { + const std::filesystem::path composed = root / path; + std::error_code ec; + for (std::filesystem::directory_entry const& entry : std::filesystem::directory_iterator { composed, ec }) { + if (entry.is_regular_file()) { + const std::filesystem::path file = entry; + if (extension == nullptr || file.extension() == *extension) { + if (!contains_file_with_name(ret, file.filename())) { + ret.push_back(file); + } + } + } + } + } + return ret; +} + +bool Dataloader::apply_to_files_in_dir(std::filesystem::path const& path, + std::function<bool(std::filesystem::path const&)> callback, + std::filesystem::path const* extension) const { + + bool ret = true; + for (std::filesystem::path const& file : lookup_files_in_dir(path, extension)) { + ret &= callback(file); + } + return ret; +} + +template<std::derived_from<detail::BasicParser> Parser, bool(Parser::*parse_func)()> +static Parser _run_ovdl_parser(std::filesystem::path const& path) { + Parser parser; + std::string buffer; + auto error_log_stream = ovdl::detail::CallbackStream { + [](void const* s, std::streamsize n, void* user_data) -> std::streamsize { + if (s != nullptr && n > 0 && user_data != nullptr) { + static_cast<std::string*>(user_data)->append(static_cast<char const*>(s), n); + return n; + } else { + Logger::error("Invalid input to parser error log callback: ", s, " / ", n, " / ", user_data); + return 0; + } + }, &buffer + }; + parser.set_error_log_to(error_log_stream); + parser.load_from_file(path); + if (!buffer.empty()) { + Logger::error("Parser load errors:\n\n", buffer, "\n"); + buffer.clear(); + } + if (parser.has_fatal_error() || parser.has_error()) { + Logger::error("Parser errors while loading ", path); + return parser; + } + if (!(parser.*parse_func)()) { + Logger::error("Parse function returned false!"); + } + if (!buffer.empty()) { + Logger::error("Parser parse errors:\n\n", buffer, "\n"); + buffer.clear(); + } + if (parser.has_fatal_error() || parser.has_error()) { + Logger::error("Parser errors while parsing ", path); + } + return parser; +} + +static v2script::Parser _parse_defines(std::filesystem::path const& path) { + return _run_ovdl_parser<v2script::Parser, &v2script::Parser::simple_parse>(path); +} + +static csv::Windows1252Parser _parse_csv(std::filesystem::path const& path) { + return _run_ovdl_parser<csv::Windows1252Parser, &csv::Windows1252Parser::parse_csv>(path); +} + +bool Dataloader::_load_pop_types(PopManager& pop_manager, std::filesystem::path const& pop_type_directory) const { + const bool ret = apply_to_files_in_dir(pop_type_directory, + [&pop_manager](std::filesystem::path const& file) -> bool { + return pop_manager.load_pop_type_file(file, _parse_defines(file).get_file_node()); + } + ); + if (!ret) { + Logger::error("Failed to load pop types!"); + } + pop_manager.lock_pop_types(); + return ret; +} + +bool Dataloader::_load_map_dir(Map& map, std::filesystem::path const& map_directory) const { + static const std::filesystem::path defaults_filename = "default.map"; + static const std::string default_definitions = "definition.csv"; + static const std::string default_provinces = "provinces.bmp"; + static const std::string default_positions = "positions.txt"; + static const std::string default_terrain = "terrain.bmp"; + static const std::string default_rivers = "rivers.bmp"; + static const std::string default_terrain_definition = "terrain.txt"; + static const std::string default_tree_definition = "trees.txt"; + static const std::string default_continent = "continent.txt"; + static const std::string default_adjacencies = "adjacencies.csv"; + static const std::string default_region = "region.txt"; + static const std::string default_region_sea = "region_sea.txt"; + static const std::string default_province_flag_sprite = "province_flag_sprites"; + + const v2script::Parser parser = _parse_defines(lookup_file(map_directory / defaults_filename)); + + std::vector<std::string_view> water_province_identifiers; + +#define APPLY_TO_MAP_PATHS(F) \ + F(definitions) F(provinces) F(positions) F(terrain) F(rivers) \ + F(terrain_definition) F(tree_definition) F(continent) F(adjacencies) \ + F(region) F(region_sea) F(province_flag_sprite) + +#define MAP_PATH_VAR(X) std::string_view X = default_##X; + APPLY_TO_MAP_PATHS(MAP_PATH_VAR) +#undef MAP_PATH_VAR + + bool ret = expect_dictionary_keys( + "max_provinces", ONE_EXACTLY, + expect_uint( + [&map](uint64_t val) -> bool { + if (Province::NULL_INDEX < val && val <= Province::MAX_INDEX) { + return map.set_max_provinces(val); + } + Logger::error("Invalid max province count ", val, " (out of valid range ", Province::NULL_INDEX, " < max_provinces <= ", Province::MAX_INDEX, ")"); + return false; + } + ), + "sea_starts", ONE_EXACTLY, + expect_list_reserve_length( + water_province_identifiers, + expect_identifier( + [&water_province_identifiers](std::string_view identifier) -> bool { + water_province_identifiers.push_back(identifier); + return true; + } + ) + ), + +#define MAP_PATH_DICT_ENTRY(X) \ + #X, ONE_EXACTLY, expect_string(assign_variable_callback(X)), + APPLY_TO_MAP_PATHS(MAP_PATH_DICT_ENTRY) +#undef MAP_PATH_DICT_ENTRY + +#undef APPLY_TO_MAP_PATHS + + "border_heights", ZERO_OR_ONE, success_callback, + "terrain_sheet_heights", ZERO_OR_ONE, success_callback, + "tree", ZERO_OR_ONE, success_callback, + "border_cutoff", ZERO_OR_ONE, success_callback + )(parser.get_file_node()); + + if (!ret) { + Logger::error("Failed to load map default file!"); + } + + if (!map.load_province_definitions(_parse_csv(lookup_file(map_directory / definitions)).get_lines())) { + Logger::error("Failed to load province definitions file!"); + ret = false; + } + + if (!map.set_water_province_list(water_province_identifiers)) { + Logger::error("Failed to set water provinces!"); + ret = false; + } + map.lock_water_provinces(); + + return ret; +} + +bool Dataloader::load_defines(GameManager& game_manager) const { + static const std::filesystem::path good_file = "common/goods.txt"; + static const std::filesystem::path pop_type_directory = "poptypes"; + static const std::filesystem::path graphical_culture_type_file = "common/graphicalculturetype.txt"; + static const std::filesystem::path culture_file = "common/cultures.txt"; + static const std::filesystem::path religion_file = "common/religion.txt"; + static const std::filesystem::path map_directory = "map"; + + bool ret = true; + + if (!game_manager.good_manager.load_good_file(_parse_defines(lookup_file(good_file)).get_file_node())) { + Logger::error("Failed to load goods!"); + ret = false; + } + if (!_load_pop_types(game_manager.pop_manager, pop_type_directory)) { + Logger::error("Failed to load pop types!"); + ret = false; + } + if (!game_manager.pop_manager.culture_manager.load_graphical_culture_type_file(_parse_defines(lookup_file(graphical_culture_type_file)).get_file_node())) { + Logger::error("Failed to load graphical culture types!"); + ret = false; + } + if (!game_manager.pop_manager.culture_manager.load_culture_file(_parse_defines(lookup_file(culture_file)).get_file_node())) { + Logger::error("Failed to load cultures!"); + ret = false; + } + if (!game_manager.pop_manager.religion_manager.load_religion_file(_parse_defines(lookup_file(religion_file)).get_file_node())) { + Logger::error("Failed to load religions!"); + ret = false; + } + if (!_load_map_dir(game_manager.map, map_directory)) { + Logger::error("Failed to load map!"); + ret = false; + } + + return ret; +} + +bool Dataloader::load_pop_history(GameManager& game_manager, std::filesystem::path const& path) const { + return apply_to_files_in_dir(path, + [&game_manager](std::filesystem::path const& file) -> bool { + return expect_dictionary( + [&game_manager](std::string_view province_key, ast::NodeCPtr province_node) -> bool { + Province* province = game_manager.map.get_province_by_identifier(province_key); + if (province == nullptr) { + Logger::error("Invalid province id: ", province_key); + return false; + } + return province->load_pop_list(game_manager.pop_manager, province_node); + } + )(_parse_defines(file).get_file_node()); + } + ); +} diff --git a/src/openvic-simulation/dataloader/Dataloader.hpp b/src/openvic-simulation/dataloader/Dataloader.hpp new file mode 100644 index 0000000..f723803 --- /dev/null +++ b/src/openvic-simulation/dataloader/Dataloader.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include <filesystem> +#include <functional> +#include <vector> + +namespace OpenVic { + struct GameManager; + struct PopManager; + struct Map; + + class Dataloader { + std::vector<std::filesystem::path> roots; + + bool _load_pop_types(PopManager& pop_manager, std::filesystem::path const& pop_type_directory) const; + bool _load_map_dir(Map& map, std::filesystem::path const& map_directory) const; + + public: + Dataloader() = default; + + /* In reverse-load order, so base defines first and final loaded mod last */ + bool set_roots(std::vector<std::filesystem::path> new_roots); + + std::filesystem::path lookup_file(std::filesystem::path const& path) const; + static const std::filesystem::path TXT; + std::vector<std::filesystem::path> lookup_files_in_dir(std::filesystem::path const& path, + std::filesystem::path const* extension = &TXT) const; + bool apply_to_files_in_dir(std::filesystem::path const& path, + std::function<bool(std::filesystem::path const&)> callback, + std::filesystem::path const* extension = &TXT) const; + + bool load_defines(GameManager& game_manager) const; + bool load_pop_history(GameManager& game_manager, std::filesystem::path const& path) const; + }; +} diff --git a/src/openvic-simulation/dataloader/NodeTools.cpp b/src/openvic-simulation/dataloader/NodeTools.cpp new file mode 100644 index 0000000..3b05d04 --- /dev/null +++ b/src/openvic-simulation/dataloader/NodeTools.cpp @@ -0,0 +1,256 @@ +#include "NodeTools.hpp" + +#include <charconv> + +using namespace OpenVic; +using namespace OpenVic::NodeTools; + +template<typename T> +static node_callback_t _expect_type(callback_t<T const&> callback) { + return [callback](ast::NodeCPtr node) -> bool { + if (node != nullptr) { + T const* cast_node = node->cast_to<T>(); + if (cast_node != nullptr) { + return callback(*cast_node); + } + Logger::error("Invalid node type ", node->get_type(), " when expecting ", T::get_type_static()); + } else { + Logger::error("Null node when expecting ", T::get_type_static()); + } + return false; + }; +} + +template<typename T = ast::AbstractStringNode> +requires(std::derived_from<T, ast::AbstractStringNode>) +static callback_t<T const&> abstract_string_node_callback(callback_t<std::string_view> callback) { + return [callback](T const& node) -> bool { + return callback(node._name); + }; +} + +node_callback_t NodeTools::expect_identifier(callback_t<std::string_view> callback) { + return _expect_type<ast::IdentifierNode>(abstract_string_node_callback<ast::IdentifierNode>(callback)); +} + +node_callback_t NodeTools::expect_string(callback_t<std::string_view> callback) { + return _expect_type<ast::StringNode>(abstract_string_node_callback<ast::StringNode>(callback)); +} + +node_callback_t NodeTools::expect_identifier_or_string(callback_t<std::string_view> callback) { + return [callback](ast::NodeCPtr node) -> bool { + if (node != nullptr) { + ast::AbstractStringNode const* cast_node = node->cast_to<ast::IdentifierNode>(); + if (cast_node == nullptr) { + cast_node = node->cast_to<ast::StringNode>(); + } + if (cast_node != nullptr) { + return abstract_string_node_callback(callback)(*cast_node); + } + Logger::error("Invalid node type ", node->get_type(), " when expecting ", ast::IdentifierNode::get_type_static(), " or ", ast::StringNode::get_type_static()); + } else { + Logger::error("Null node when expecting ", ast::IdentifierNode::get_type_static(), " or ", ast::StringNode::get_type_static()); + } + return false; + }; +} + +node_callback_t NodeTools::expect_bool(callback_t<bool> callback) { + return expect_identifier( + [callback](std::string_view identifier) -> bool { + if (identifier == "yes") { + return callback(true); + } else if (identifier == "no") { + return callback(false); + } + Logger::error("Invalid bool identifier text: ", identifier); + return false; + } + ); +} + +node_callback_t NodeTools::expect_int(callback_t<int64_t> callback) { + return expect_identifier( + [callback](std::string_view identifier) -> bool { + bool successful = false; + const int64_t val = StringUtils::string_to_int64(identifier, &successful, 10); + if (successful) { + return callback(val); + } + Logger::error("Invalid int identifier text: ", identifier); + return false; + } + ); +} + +node_callback_t NodeTools::expect_uint(callback_t<uint64_t> callback) { + return expect_identifier( + [callback](std::string_view identifier) -> bool { + bool successful = false; + const uint64_t val = StringUtils::string_to_uint64(identifier, &successful, 10); + if (successful) { + return callback(val); + } + Logger::error("Invalid uint identifier text: ", identifier); + return false; + } + ); +} + +node_callback_t NodeTools::expect_fixed_point(callback_t<fixed_point_t> callback) { + return expect_identifier( + [callback](std::string_view identifier) -> bool { + bool successful = false; + const fixed_point_t val = fixed_point_t::parse(identifier.data(), identifier.length(), &successful); + if (successful) { + return callback(val); + } + Logger::error("Invalid fixed point identifier text: ", identifier); + return false; + } + ); +} + +node_callback_t NodeTools::expect_colour(callback_t<colour_t> callback) { + return [callback](ast::NodeCPtr node) -> bool { + colour_t col = NULL_COLOUR; + uint32_t components = 0; + bool ret = expect_list_of_length(3, + expect_fixed_point( + [&col, &components](fixed_point_t val) -> bool { + components++; + col <<= 8; + if (val < 0 || val > 255) { + Logger::error("Invalid colour component: ", val); + return false; + } else { + if (val <= 1) val *= 255; + col |= val.to_int32_t(); + return true; + } + } + ) + )(node); + ret &= callback(col << 8 * (3 - components)); + return ret; + }; +} + +node_callback_t NodeTools::expect_date(callback_t<Date> callback) { + return expect_identifier( + [callback](std::string_view identifier) -> bool { + bool successful = false; + const Date date = Date::from_string(identifier, &successful); + if (successful) { + return callback(date); + } + Logger::error("Invalid date identifier text: ", identifier); + return false; + } + ); +} + +node_callback_t NodeTools::expect_assign(key_value_callback_t callback) { + return _expect_type<ast::AssignNode>( + [callback](ast::AssignNode const& assign_node) -> bool { + return callback(assign_node._name, assign_node._initializer.get()); + } + ); +} + +node_callback_t NodeTools::expect_list_and_length(length_callback_t length_callback, node_callback_t callback) { + return _expect_type<ast::AbstractListNode>( + [length_callback, callback](ast::AbstractListNode const& list_node) -> bool { + std::vector<ast::NodeUPtr> const& list = list_node._statements; + bool ret = true; + size_t size = length_callback(list.size()); + if (size > list.size()) { + Logger::error("Trying to read more values than the list contains: ", size, " > ", list.size()); + size = list.size(); + ret = false; + } + std::for_each(list.begin(), list.begin() + size, + [callback, &ret](ast::NodeUPtr const& sub_node) -> void { + ret &= callback(sub_node.get()); + } + ); + return ret; + } + ); +} + +node_callback_t NodeTools::expect_list_of_length(size_t length, node_callback_t callback) { + return [length, callback](ast::NodeCPtr node) -> bool { + bool ret = true; + ret &= expect_list_and_length( + [length, &ret](size_t size) -> size_t { + if (size != length) { + Logger::error("List length ", size, " does not match expected length ", length); + ret = false; + if (length < size) return length; + } + return size; + }, + callback + )(node); + return ret; + }; +} + +node_callback_t NodeTools::expect_list(node_callback_t callback) { + return expect_list_and_length(default_length_callback, callback); +} + +node_callback_t NodeTools::expect_dictionary_and_length(length_callback_t length_callback, key_value_callback_t callback) { + return expect_list_and_length(length_callback, expect_assign(callback)); +} + +node_callback_t NodeTools::expect_dictionary(key_value_callback_t callback) { + return expect_dictionary_and_length(default_length_callback, callback); +} + +node_callback_t NodeTools::_expect_dictionary_keys_and_length(length_callback_t length_callback, bool allow_other_keys, key_map_t&& key_map) { + return [length_callback, allow_other_keys, key_map = std::move(key_map)](ast::NodeCPtr node) mutable -> bool { + bool ret = expect_dictionary_and_length( + length_callback, + [&key_map, allow_other_keys](std::string_view key, ast::NodeCPtr value) -> bool { + const key_map_t::iterator it = key_map.find(key); + if (it == key_map.end()) { + if (allow_other_keys) return true; + Logger::error("Invalid dictionary key: ", key); + return false; + } + dictionary_entry_t& entry = it->second; + if (++entry.count > 1 && !entry.can_repeat()) { + Logger::error("Invalid repeat of dictionary key: ", key); + return false; + } + return entry.callback(value); + } + )(node); + for (key_map_t::value_type const& key_entry : key_map) { + dictionary_entry_t const& entry = key_entry.second; + if (entry.must_appear() && entry.count < 1) { + Logger::error("Mandatory dictionary key not present: ", key_entry.first); + ret = false; + } + } + return ret; + }; +} + +node_callback_t NodeTools::name_list_callback(std::vector<std::string>& list) { + return expect_list_reserve_length( + list, + expect_identifier_or_string( + [&list](std::string_view str) -> bool { + if (!str.empty()) { + list.push_back(std::string { str }); + return true; + } + Logger::error("Empty identifier or string"); + return false; + } + ) + ); +} diff --git a/src/openvic-simulation/dataloader/NodeTools.hpp b/src/openvic-simulation/dataloader/NodeTools.hpp new file mode 100644 index 0000000..805de94 --- /dev/null +++ b/src/openvic-simulation/dataloader/NodeTools.hpp @@ -0,0 +1,164 @@ +#pragma once + +#include <map> + +#include "openvic-simulation/types/Colour.hpp" +#include "openvic-simulation/types/Date.hpp" +#include "openvic-simulation/types/fixed_point/FixedPoint.hpp" + +#include <openvic-dataloader/v2script/AbstractSyntaxTree.hpp> + +namespace OpenVic { + namespace ast = ovdl::v2script::ast; + + namespace NodeTools { + + template<typename... Args> + using callback_t = std::function<bool(Args...)>; + + using node_callback_t = callback_t<ast::NodeCPtr>; + constexpr bool success_callback(ast::NodeCPtr) { return true; } + + using key_value_callback_t = callback_t<std::string_view, ast::NodeCPtr>; + + node_callback_t expect_identifier(callback_t<std::string_view> callback); + node_callback_t expect_string(callback_t<std::string_view> callback); + node_callback_t expect_identifier_or_string(callback_t<std::string_view> callback); + node_callback_t expect_bool(callback_t<bool> callback); + node_callback_t expect_int(callback_t<int64_t> callback); + node_callback_t expect_uint(callback_t<uint64_t> callback); + node_callback_t expect_fixed_point(callback_t<fixed_point_t> callback); + node_callback_t expect_colour(callback_t<colour_t> callback); + node_callback_t expect_date(callback_t<Date> callback); + node_callback_t expect_assign(key_value_callback_t callback); + + using length_callback_t = std::function<size_t(size_t)>; + constexpr size_t default_length_callback(size_t size) { return size; }; + + node_callback_t expect_list_and_length(length_callback_t length_callback, node_callback_t callback); + node_callback_t expect_list_of_length(size_t length, node_callback_t callback); + node_callback_t expect_list(node_callback_t callback); + + node_callback_t expect_dictionary_and_length(length_callback_t length_callback, key_value_callback_t callback); + node_callback_t expect_dictionary(key_value_callback_t callback); + + struct dictionary_entry_t { + const enum class expected_count_t : uint8_t { + _MUST_APPEAR = 0b01, + _CAN_REPEAT = 0b10, + + ZERO_OR_ONE = 0, + ONE_EXACTLY = _MUST_APPEAR, + ZERO_OR_MORE = _CAN_REPEAT, + ONE_OR_MORE = _MUST_APPEAR | _CAN_REPEAT + } expected_count; + const node_callback_t callback; + size_t count; + + dictionary_entry_t(expected_count_t new_expected_count, node_callback_t new_callback) + : expected_count { new_expected_count }, callback { new_callback }, count { 0 } {} + + constexpr bool must_appear() const { + return static_cast<uint8_t>(expected_count) & static_cast<uint8_t>(expected_count_t::_MUST_APPEAR); + } + constexpr bool can_repeat() const { + return static_cast<uint8_t>(expected_count) & static_cast<uint8_t>(expected_count_t::_CAN_REPEAT); + } + }; + using enum dictionary_entry_t::expected_count_t; + using key_map_t = std::map<std::string, dictionary_entry_t, std::less<void>>; + + constexpr struct allow_other_keys_t {} ALLOW_OTHER_KEYS; + + node_callback_t _expect_dictionary_keys_and_length(length_callback_t length_callback, bool allow_other_keys, key_map_t&& key_map); + + template<typename... Args> + node_callback_t _expect_dictionary_keys_and_length(length_callback_t length_callback, + bool allow_other_keys, key_map_t&& key_map, + const std::string_view key, dictionary_entry_t::expected_count_t expected_count, node_callback_t callback, + Args... args) { + if (key_map.find(key) == key_map.end()) { + key_map.emplace(key, dictionary_entry_t { expected_count, callback }); + } else { + Logger::error("Duplicate expected dictionary key: ", key); + } + return _expect_dictionary_keys_and_length(length_callback, allow_other_keys, std::move(key_map), args...); + } + + template<typename... Args> + node_callback_t expect_dictionary_keys_and_length(length_callback_t length_callback, + const std::string_view key, dictionary_entry_t::expected_count_t expected_count, node_callback_t callback, + Args... args) { + return _expect_dictionary_keys_and_length(length_callback, false, {}, key, expected_count, callback, args...); + } + + template<typename... Args> + node_callback_t expect_dictionary_keys_and_length(length_callback_t length_callback, + allow_other_keys_t, Args... args) { + return _expect_dictionary_keys_and_length(length_callback, true, {}, args...); + } + + template<typename... Args> + node_callback_t expect_dictionary_keys(Args... args) { + return expect_dictionary_keys_and_length(default_length_callback, args...); + } + + template<typename T> + concept Reservable = requires(T& t) { + { t.size() } -> std::same_as<size_t>; + t.reserve( size_t {} ); + }; + template<Reservable T> + node_callback_t expect_list_reserve_length(T& t, node_callback_t callback) { + return expect_list_and_length( + [&t](size_t size) -> size_t { + t.reserve(t.size() + size); + return size; + }, + callback + ); + } + template<Reservable T> + node_callback_t expect_dictionary_reserve_length(T& t, key_value_callback_t callback) { + return expect_list_reserve_length(t, expect_assign(callback)); + } + + node_callback_t name_list_callback(std::vector<std::string>& list); + + template<typename T> + callback_t<T> assign_variable_callback(T& var) { + return [&var](T val) -> bool { + var = val; + return true; + }; + } + + template<typename T> + requires(std::integral<T>) + callback_t<uint64_t> assign_variable_callback_uint(const std::string_view name, T& var) { + return [&var, name](uint64_t val) -> bool { + if (val <= std::numeric_limits<T>::max()) { + var = val; + return true; + } + Logger::error("Invalid ", name, ": ", val, " (valid range: [0, ", static_cast<uint64_t>(std::numeric_limits<T>::max()), "])"); + return false; + }; + } + + template<typename T> + requires(std::integral<T>) + callback_t<int64_t> assign_variable_callback_int(const std::string_view name, T& var) { + return [&var, name](int64_t val) -> bool { + if (std::numeric_limits<T>::lowest() <= val && val <= std::numeric_limits<T>::max()) { + var = val; + return true; + } + Logger::error("Invalid ", name, ": ", val, " (valid range: [", + static_cast<int64_t>(std::numeric_limits<T>::lowest()), ", ", + static_cast<uint64_t>(std::numeric_limits<T>::max()), "])"); + return false; + }; + } + } +} diff --git a/src/openvic-simulation/economy/Good.cpp b/src/openvic-simulation/economy/Good.cpp new file mode 100644 index 0000000..e3dbd3e --- /dev/null +++ b/src/openvic-simulation/economy/Good.cpp @@ -0,0 +1,140 @@ +#include "Good.hpp" + +#include <cassert> + +using namespace OpenVic; +using namespace OpenVic::NodeTools; + +GoodCategory::GoodCategory(const std::string_view new_identifier) : HasIdentifier { new_identifier } {} + +Good::Good(const std::string_view new_identifier, colour_t new_colour, GoodCategory const& new_category, price_t new_base_price, + bool new_available_from_start, bool new_tradeable, bool new_money, bool new_overseas_penalty) + : HasIdentifierAndColour { new_identifier, new_colour, true }, + category { new_category }, + base_price { new_base_price }, + available_from_start { new_available_from_start }, + tradeable { new_tradeable }, + money { new_money }, + overseas_penalty { new_overseas_penalty } { + assert(base_price > NULL_PRICE); +} + +GoodCategory const& Good::get_category() const { + return category; +} + +Good::price_t Good::get_base_price() const { + return base_price; +} + +Good::price_t Good::get_price() const { + return price; +} + +bool Good::get_available_from_start() const { + return available_from_start; +} + +bool Good::get_available() const { + return available; +} + +bool Good::get_tradeable() const { + return tradeable; +} + +bool Good::get_money() const { + return money; +} + +bool Good::get_overseas_penalty() { + return overseas_penalty; +} + +void Good::reset_to_defaults() { + available = available_from_start; + price = base_price; +} + +GoodManager::GoodManager() : good_categories { "good categories" }, goods { "goods" } {} + +bool GoodManager::add_good_category(const std::string_view identifier) { + if (identifier.empty()) { + Logger::error("Invalid good category identifier - empty!"); + return false; + } + return good_categories.add_item({ identifier }); +} + +bool GoodManager::add_good(const std::string_view identifier, colour_t colour, GoodCategory const* category, + Good::price_t base_price, bool available_from_start, bool tradeable, bool money, bool overseas_penalty) { + if (identifier.empty()) { + Logger::error("Invalid good identifier - empty!"); + return false; + } + if (colour > MAX_COLOUR_RGB) { + Logger::error("Invalid good colour for ", identifier, ": ", colour_to_hex_string(colour)); + return false; + } + if (category == nullptr) { + Logger::error("Invalid good category for ", identifier, ": null"); + return false; + } + if (base_price <= Good::NULL_PRICE) { + Logger::error("Invalid base price for ", identifier, ": ", base_price); + return false; + } + return goods.add_item({ identifier, colour, *category, base_price, available_from_start, tradeable, money, overseas_penalty }); +} + +void GoodManager::reset_to_defaults() { + for (Good& good : goods.get_items()) + good.reset_to_defaults(); +} + +bool GoodManager::load_good_file(ast::NodeCPtr root) { + size_t total_expected_goods = 0; + bool ret = expect_dictionary_reserve_length( + good_categories, + [this, &total_expected_goods](std::string_view key, ast::NodeCPtr value) -> bool { + bool ret = expect_list_and_length( + [&total_expected_goods](size_t size) -> size_t { + total_expected_goods += size; + return 0; + }, + success_callback + )(value); + ret &= add_good_category(key); + return ret; + } + )(root); + lock_good_categories(); + goods.reserve(goods.size() + total_expected_goods); + ret &= expect_dictionary( + [this](std::string_view good_category_key, ast::NodeCPtr good_category_value) -> bool { + GoodCategory const* good_category = get_good_category_by_identifier(good_category_key); + + return expect_dictionary( + [this, good_category](std::string_view key, ast::NodeCPtr value) -> bool { + colour_t colour = NULL_COLOUR; + Good::price_t base_price; + bool available_from_start, tradeable = true; + bool money, overseas_penalty = false; + + bool ret = expect_dictionary_keys( + "color", ONE_EXACTLY, expect_colour(assign_variable_callback(colour)), + "cost", ONE_EXACTLY, expect_fixed_point(assign_variable_callback(base_price)), + "available_from_start", ZERO_OR_ONE, expect_bool(assign_variable_callback(available_from_start)), + "tradeable", ZERO_OR_ONE, expect_bool(assign_variable_callback(tradeable)), + "money", ZERO_OR_ONE, expect_bool(assign_variable_callback(money)), + "overseas_penalty", ZERO_OR_ONE, expect_bool(assign_variable_callback(overseas_penalty)) + )(value); + ret &= add_good(key, colour, good_category, base_price, available_from_start, tradeable, money, overseas_penalty); + return ret; + } + )(good_category_value); + } + )(root); + lock_goods(); + return ret; +} diff --git a/src/openvic-simulation/economy/Good.hpp b/src/openvic-simulation/economy/Good.hpp new file mode 100644 index 0000000..d5cd532 --- /dev/null +++ b/src/openvic-simulation/economy/Good.hpp @@ -0,0 +1,79 @@ +#pragma once + +#include "openvic-simulation/types/IdentifierRegistry.hpp" +#include "openvic-simulation/dataloader/NodeTools.hpp" + +namespace OpenVic { + struct GoodManager; + + struct GoodCategory : HasIdentifier { + friend struct GoodManager; + + private: + GoodCategory(const std::string_view new_identifier); + + public: + GoodCategory(GoodCategory&&) = default; + }; + + /* REQUIREMENTS: + * + * ECON-3 , ECON-4 , ECON-5 , ECON-6 , ECON-7 , ECON-8 , ECON-9 , ECON-10, ECON-11, ECON-12, ECON-13, ECON-14, + * ECON-15, ECON-16, ECON-17, ECON-18, ECON-19, ECON-20, ECON-21, ECON-22, ECON-23, ECON-24, ECON-25, ECON-26, + * ECON-27, ECON-28, ECON-29, ECON-30, ECON-31, ECON-32, ECON-33, ECON-34, ECON-35, ECON-36, ECON-37, ECON-38, + * ECON-39, ECON-40, ECON-41, ECON-42, ECON-43, ECON-44, ECON-45, ECON-46, ECON-47, ECON-48, ECON-49, ECON-50 + * + * ECON-123, ECON-124, ECON-125, ECON-126, ECON-127, ECON-128, ECON-129, ECON-130, ECON-131, ECON-132, ECON-133, ECON-134, + * ECON-135, ECON-136, ECON-137, ECON-138, ECON-139, ECON-140, ECON-141, ECON-142, ECON-234, ECON-235, ECON-236, ECON-237, + * ECON-238, ECON-239, ECON-240, ECON-241, ECON-242, ECON-243, ECON-244, ECON-245, ECON-246, ECON-247, ECON-248, ECON-249, + * ECON-250, ECON-251, ECON-252, ECON-253, ECON-254, ECON-255, ECON-256, ECON-257, ECON-258, ECON-259, ECON-260, ECON-261 + */ + struct Good : HasIdentifierAndColour { + friend struct GoodManager; + + using price_t = fixed_point_t; + static constexpr price_t NULL_PRICE = fixed_point_t::_0(); + + private: + GoodCategory const& category; + const price_t base_price; + price_t price; + const bool available_from_start, tradeable, money, overseas_penalty; + bool available; + + Good(const std::string_view new_identifier, colour_t new_colour, GoodCategory const& new_category, price_t new_base_price, + bool new_available_from_start, bool new_tradeable, bool new_money, bool new_overseas_penalty); + + public: + Good(Good&&) = default; + + GoodCategory const& get_category() const; + price_t get_base_price() const; + price_t get_price() const; + bool get_available_from_start() const; + bool get_available() const; + bool get_tradeable() const; + bool get_money() const; + bool get_overseas_penalty(); + void reset_to_defaults(); + }; + + struct GoodManager { + private: + IdentifierRegistry<GoodCategory> good_categories; + IdentifierRegistry<Good> goods; + + public: + GoodManager(); + + bool add_good_category(const std::string_view identifier); + IDENTIFIER_REGISTRY_ACCESSORS_CUSTOM_PLURAL(GoodCategory, good_category, good_categories) + + bool add_good(const std::string_view identifier, colour_t colour, GoodCategory const* category, Good::price_t base_price, + bool available_from_start, bool tradeable, bool money, bool overseas_penalty); + IDENTIFIER_REGISTRY_ACCESSORS(Good, good) + + void reset_to_defaults(); + bool load_good_file(ast::NodeCPtr root); + }; +} diff --git a/src/openvic-simulation/map/Building.cpp b/src/openvic-simulation/map/Building.cpp new file mode 100644 index 0000000..6e5cf18 --- /dev/null +++ b/src/openvic-simulation/map/Building.cpp @@ -0,0 +1,127 @@ +#include "Building.hpp" + +#include <cassert> + +#include "openvic-simulation/map/Province.hpp" +#include "openvic-simulation/utility/Logger.hpp" + +using namespace OpenVic; + +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; +} + +bool Building::expand() { + if (expansion_state == ExpansionState::CanExpand) { + expansion_state = ExpansionState::Preparing; + expansion_progress = 0.0f; + return true; + } + return false; +} + +/* 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(const std::string_view 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(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" } {} + +bool BuildingManager::add_building_type(const std::string_view identifier, Building::level_t max_level, Timespan build_time) { + if (identifier.empty()) { + Logger::error("Invalid building type identifier - empty!"); + return false; + } + if (max_level < 0) { + Logger::error("Invalid building type max level for ", identifier, ": ", max_level); + return false; + } + if (build_time < 0) { + Logger::error("Invalid building type build time for ", identifier, ": ", build_time); + return false; + } + return building_types.add_item({ identifier, max_level, build_time }); +} + +bool BuildingManager::generate_province_buildings(Province& province) const { + province.reset_buildings(); + if (!building_types.is_locked()) { + Logger::error("Cannot generate buildings until building types are locked!"); + return false; + } + bool ret = true; + if (!province.is_water()) { + for (BuildingType const& type : building_types.get_items()) { + ret &= province.add_building({ type }); + } + } + province.lock_buildings(); + return ret; +} diff --git a/src/openvic-simulation/map/Building.hpp b/src/openvic-simulation/map/Building.hpp new file mode 100644 index 0000000..d36dfd4 --- /dev/null +++ b/src/openvic-simulation/map/Building.hpp @@ -0,0 +1,85 @@ +#pragma once + +#include <vector> + +#include "openvic-simulation/types/Date.hpp" +#include "openvic-simulation/types/IdentifierRegistry.hpp" + +namespace OpenVic { + + struct BuildingManager; + 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 BuildingManager; + + 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; + + bool expand(); + void update_state(Date const& today); + void tick(Date const& today); + }; + + struct BuildingType : HasIdentifier { + friend struct BuildingManager; + + private: + const Building::level_t max_level; + const Timespan build_time; + + BuildingType(const std::string_view 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 Province; + + struct BuildingManager { + private: + IdentifierRegistry<BuildingType> building_types; + + public: + BuildingManager(); + + bool add_building_type(const std::string_view identifier, Building::level_t max_level, Timespan build_time); + IDENTIFIER_REGISTRY_ACCESSORS(BuildingType, building_type) + bool generate_province_buildings(Province& province) const; + }; +} diff --git a/src/openvic-simulation/map/Map.cpp b/src/openvic-simulation/map/Map.cpp new file mode 100644 index 0000000..3f86ccc --- /dev/null +++ b/src/openvic-simulation/map/Map.cpp @@ -0,0 +1,509 @@ +#include "Map.hpp" + +#include <cassert> +#include <unordered_set> + +#include "openvic-simulation/economy/Good.hpp" +#include "openvic-simulation/utility/Logger.hpp" + +using namespace OpenVic; + +Mapmode::Mapmode(const std::string_view new_identifier, index_t new_index, colour_func_t new_colour_func) + : HasIdentifier { new_identifier }, + index { new_index }, + colour_func { new_colour_func } { + assert(colour_func != nullptr); +} + +const Mapmode Mapmode::ERROR_MAPMODE { "mapmode_error", 0, + [](Map const& map, Province const& province) -> colour_t { return 0xFFFF0000; } }; + +Mapmode::index_t Mapmode::get_index() const { + return index; +} + +colour_t Mapmode::get_colour(Map const& map, Province const& province) const { + return colour_func ? colour_func(map, province) : NULL_COLOUR; +} + +Map::Map() : provinces { "provinces" }, + regions { "regions" }, + mapmodes { "mapmodes" } {} + +bool Map::add_province(const std::string_view identifier, colour_t colour) { + if (provinces.size() >= max_provinces) { + Logger::error("The map's province list is full - maximum number of provinces is ", max_provinces, " (this can be at most ", Province::MAX_INDEX, ")"); + return false; + } + if (identifier.empty()) { + Logger::error("Invalid province identifier - empty!"); + return false; + } + if (colour == NULL_COLOUR || colour > MAX_COLOUR_RGB) { + Logger::error("Invalid province colour for ", identifier, ": ", colour_to_hex_string(colour)); + return false; + } + Province new_province { identifier, colour, static_cast<Province::index_t>(provinces.size() + 1) }; + const Province::index_t index = get_index_from_colour(colour); + if (index != Province::NULL_INDEX) { + Logger::error("Duplicate province colours: ", get_province_by_index(index)->to_string(), " and ", new_province.to_string()); + return false; + } + colour_index_map[new_province.get_colour()] = new_province.get_index(); + return provinces.add_item(std::move(new_province)); +} + +void Map::lock_provinces() { + provinces.lock(); +} + +bool Map::set_water_province(const std::string_view identifier) { + if (water_provinces.is_locked()) { + Logger::error("The map's water provinces have already been locked!"); + return false; + } + Province* province = get_province_by_identifier(identifier); + if (province == nullptr) { + Logger::error("Unrecognised water province identifier: ", identifier); + return false; + } + if (province->is_water()) { + Logger::error("Province ", identifier, " is already a water province!"); + return false; + } + if (!water_provinces.add_province(province)) { + Logger::error("Failed to add province ", identifier, " to water province set!"); + return false; + } + province->water = true; + return true; +} + +bool Map::set_water_province_list(std::vector<std::string_view> const& list) { + bool ret = true; + water_provinces.reserve(water_provinces.size() + list.size()); + for (std::string_view const& identifier : list) { + ret &= set_water_province(identifier); + } + return ret; +} + +void Map::lock_water_provinces() { + water_provinces.lock(); + Logger::info("Locked water provinces after registering ", water_provinces.size()); +} + +bool Map::add_region(const std::string_view identifier, std::vector<std::string_view> const& province_identifiers) { + if (identifier.empty()) { + Logger::error("Invalid region identifier - empty!"); + return false; + } + Region new_region { identifier }; + bool ret = true; + for (const std::string_view 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, " in region ", identifier); + ret = false; + } 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.size()) + Logger::error("Cannot add province ", province_identifier, " to region ", identifier, " - it is already part of ", regions.get_item_by_index(other_region_index)->get_identifier()); + else + Logger::error("Cannot add province ", province_identifier, " to region ", identifier, " - it is already part of an unknown region with index ", other_region_index); + ret = false; + } else if (!new_region.add_province(province)) { + Logger::error("Failed to add province ", province_identifier, " to region ", identifier); + ret = false; + } + } + } else { + Logger::error("Invalid province identifier ", province_identifier, " for region ", identifier); + ret = false; + } + } + new_region.lock(); + if (new_region.empty()) { + Logger::error("No valid provinces in list for ", identifier); + return false; + } + + // 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.size()); + for (Province* province : new_region.get_provinces()) + province->region = tmp_region_index; + ret &= regions.add_item(std::move(new_region)); + 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.size(); +} + +std::vector<Province> const& Map::get_provinces() const { + return provinces.get_items(); +} + +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(const std::string_view identifier) { + return provinces.get_item_by_identifier(identifier); +} + +Province const* Map::get_province_by_identifier(const std::string_view identifier) const { + return provinces.get_item_by_identifier(identifier); +} + +Province::index_t Map::get_index_from_colour(colour_t colour) const { + const colour_index_map_t::const_iterator it = colour_index_map.find(colour); + if (it != colour_index_map.end()) return it->second; + return Province::NULL_INDEX; +} + +Province::index_t Map::get_province_index_at(size_t x, size_t y) const { + if (x < width && y < height) return province_shape_image[x + y * width].index; + return Province::NULL_INDEX; +} + +bool Map::set_max_provinces(Province::index_t new_max_provinces) { + if (new_max_provinces <= Province::NULL_INDEX) { + Logger::error("Trying to set max province count to an invalid value ", new_max_provinces, " (must be greater than ", Province::NULL_INDEX, ")"); + return false; + } + if (!provinces.empty() || provinces.is_locked()) { + Logger::error("Trying to set max province count to ", new_max_provinces, " after provinces have already been added and/or locked"); + return false; + } + max_provinces = new_max_provinces; + return true; +} + +Province::index_t Map::get_max_provinces() const { + return max_provinces; +} + +void Map::set_selected_province(Province::index_t index) { + if (index > get_province_count()) { + Logger::error("Trying to set selected province to an invalid index ", index, " (max index is ", get_province_count(), ")"); + selected_province = Province::NULL_INDEX; + } else { + selected_province = index; + } +} + +Province::index_t Map::get_selected_province_index() const { + return selected_province; +} + +Province const* Map::get_selected_province() const { + return get_province_by_index(get_selected_province_index()); +} + +Region* Map::get_region_by_identifier(const std::string_view identifier) { + return regions.get_item_by_identifier(identifier); +} + +Region const* Map::get_region_by_identifier(const std::string_view identifier) const { + return regions.get_item_by_identifier(identifier); +} + +size_t Map::get_region_count() const { + return regions.size(); +} + +std::vector<Region> const& Map::get_regions() const { + return regions.get_items(); +} + +static colour_t colour_at(uint8_t const* colour_data, int32_t idx) { + idx *= 3; + return (colour_data[idx] << 16) | (colour_data[idx + 1] << 8) | colour_data[idx + 2]; +} + +bool Map::generate_province_shape_image(size_t new_width, size_t new_height, uint8_t const* colour_data, + uint8_t const* terrain_data, terrain_variant_map_t const& terrain_variant_map, bool detailed_errors) { + if (!province_shape_image.empty()) { + Logger::error("Province index image has already been generated!"); + return false; + } + if (!provinces.is_locked()) { + Logger::error("Province index image cannot be generated until after provinces are locked!"); + return false; + } + if (new_width < 1 || new_height < 1) { + Logger::error("Invalid province image dimensions: ", new_width, "x", new_height); + return false; + } + if (colour_data == nullptr) { + Logger::error("Province colour data pointer is null!"); + return false; + } + if (terrain_data == nullptr) { + Logger::error("Province terrain data pointer is null!"); + return false; + } + width = new_width; + height = new_height; + province_shape_image.resize(width * height); + + std::vector<bool> province_checklist(provinces.size()); + bool ret = true; + std::unordered_set<colour_t> unrecognised_province_colours, unrecognised_terrain_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 colour_t terrain_colour = colour_at(terrain_data, idx); + const terrain_variant_map_t::const_iterator it = terrain_variant_map.find(terrain_colour); + if (it != terrain_variant_map.end()) province_shape_image[idx].terrain = it->second; + else { + if (unrecognised_terrain_colours.find(terrain_colour) == unrecognised_terrain_colours.end()) { + unrecognised_terrain_colours.insert(terrain_colour); + if (detailed_errors) { + Logger::error("Unrecognised terrain colour ", colour_to_hex_string(terrain_colour), + " at (", x, ", ", y, ")"); + } + } + province_shape_image[idx].terrain = 0; + } + + const colour_t province_colour = colour_at(colour_data, idx); + if (x > 0) { + const int32_t jdx = idx - 1; + if (colour_at(colour_data, jdx) == province_colour) { + province_shape_image[idx].index = province_shape_image[jdx].index; + continue; + } + } + if (y > 0) { + const int32_t jdx = idx - width; + if (colour_at(colour_data, jdx) == province_colour) { + province_shape_image[idx].index = province_shape_image[jdx].index; + continue; + } + } + const Province::index_t index = get_index_from_colour(province_colour); + if (index != Province::NULL_INDEX) { + province_checklist[index - 1] = true; + province_shape_image[idx].index = index; + continue; + } + if (unrecognised_province_colours.find(province_colour) == unrecognised_province_colours.end()) { + unrecognised_province_colours.insert(province_colour); + if (detailed_errors) { + Logger::error("Unrecognised province colour ", colour_to_hex_string(province_colour), + " at (", x, ", ", y, ")"); + } + } + province_shape_image[idx].index = Province::NULL_INDEX; + } + } + if (!unrecognised_province_colours.empty()) { + Logger::error("Province image contains ", unrecognised_province_colours.size(), " unrecognised province colours"); + ret = false; + } + if (!unrecognised_terrain_colours.empty()) { + Logger::error("Terrain image contains ", unrecognised_terrain_colours.size(), " unrecognised terrain colours"); + ret = false; + } + + size_t missing = 0; + for (size_t idx = 0; idx < province_checklist.size(); ++idx) { + if (!province_checklist[idx]) { + if (detailed_errors) { + Logger::error("Province missing from shape image: ", provinces.get_item_by_index(idx)->to_string()); + } + missing++; + } + } + if (missing > 0) { + Logger::error("Province image is missing ", missing, " province colours"); + ret = false; + } + return ret; +} + +size_t Map::get_width() const { + return width; +} + +size_t Map::get_height() const { + return height; +} + +std::vector<Map::shape_pixel_t> const& Map::get_province_shape_image() const { + return province_shape_image; +} + +bool Map::add_mapmode(const std::string_view identifier, Mapmode::colour_func_t colour_func) { + if (identifier.empty()) { + Logger::error("Invalid mapmode identifier - empty!"); + return false; + } + if (colour_func == nullptr) { + Logger::error("Mapmode colour function is null for identifier: ", identifier); + return false; + } + return mapmodes.add_item({ identifier, mapmodes.size(), colour_func }); +} + +bool Map::generate_mapmode_colours(Mapmode::index_t index, uint8_t* target) const { + if (target == nullptr) { + Logger::error("Mapmode colour target pointer is null!"); + return false; + } + bool ret = true; + Mapmode const* mapmode = mapmodes.get_item_by_index(index); + if (mapmode == nullptr) { + // Not an error if mapmodes haven't yet been loaded, + // e.g. if we want to allocate the province colour + // texture before mapmodes are loaded. + if (!(mapmodes.empty() && index == 0)) { + Logger::error("Invalid mapmode index: ", index); + ret = false; + } + mapmode = &Mapmode::ERROR_MAPMODE; + } + // Skip past Province::NULL_INDEX + for (size_t i = 0; i < MAPMODE_COLOUR_SIZE; ++i) + *target++ = 0; + for (Province const& province : provinces.get_items()) { + const colour_t colour = mapmode->get_colour(*this, province); + *target++ = (colour >> 16) & FULL_COLOUR; + *target++ = (colour >> 8) & FULL_COLOUR; + *target++ = colour & FULL_COLOUR; + *target++ = (colour >> 24) & FULL_COLOUR; + } + return ret; +} + +void Map::update_highest_province_population() { + highest_province_population = 0; + for (Province const& province : provinces.get_items()) { + highest_province_population = std::max(highest_province_population, province.get_total_population()); + } +} + +Pop::pop_size_t Map::get_highest_province_population() const { + return highest_province_population; +} + +void Map::update_total_map_population() { + total_map_population = 0; + for (Province const& province : provinces.get_items()) { + total_map_population += province.get_total_population(); + } +} + +Pop::pop_size_t Map::get_total_map_population() const { + return total_map_population; +} + +bool Map::setup(GoodManager const& good_manager, BuildingManager const& building_manager, PopManager const& pop_manager) { + bool ret = true; + for (Province& province : provinces.get_items()) { + province.clear_pops(); + // Set all land provinces to have an RGO based on their index to test them + if (!province.is_water() && good_manager.get_good_count() > 0) + province.rgo = good_manager.get_good_by_index(province.get_index() % good_manager.get_good_count()); + ret &= building_manager.generate_province_buildings(province); + } + return ret; +} + +void Map::update_state(Date const& today) { + for (Province& province : provinces.get_items()) + province.update_state(today); + update_highest_province_population(); + update_total_map_population(); +} + +void Map::tick(Date const& today) { + for (Province& province : provinces.get_items()) + province.tick(today); +} + +using namespace ovdl::csv; + +static bool validate_province_definitions_header(LineObject const& header) { + static const std::vector<std::string> standard_header { "province", "red", "green", "blue" }; + for (size_t i = 0; i < standard_header.size(); ++i) { + const std::string_view val = header.get_value_for(i); + if (i == 0 && val.empty()) break; + if (val != standard_header[i]) return false; + } + return true; +} + +static bool parse_province_colour(colour_t& colour, std::array<std::string_view, 3> components) { + bool ret = true; + colour = NULL_COLOUR; + for (std::string_view& c : components) { + colour <<= 8; + if (c.ends_with('.')) c.remove_suffix(1); + bool successful = false; + uint64_t val = StringUtils::string_to_uint64(c, &successful, 10); + if (successful && val <= 255) { + colour |= val; + } else { + ret = false; + } + } + return ret; +} + +bool Map::load_province_definitions(std::vector<LineObject> const& lines) { + if (lines.empty()) { + Logger::error("No header or entries in province definition file!"); + return false; + } + { + LineObject const& header = lines.front(); + if (!validate_province_definitions_header(header)) { + Logger::error("Non-standard province definition file header - make sure this is not a province definition: ", header); + } + } + if (lines.size() <= 1) { + Logger::error("No entries in province definition file!"); + return false; + } + provinces.reserve(lines.size() - 1); + bool ret = true; + std::for_each(lines.begin() + 1, lines.end(), + [this, &ret](LineObject const& line) -> void { + const std::string_view identifier = line.get_value_for(0); + if (!identifier.empty()) { + colour_t colour; + if (!parse_province_colour(colour, { + line.get_value_for(1), + line.get_value_for(2), + line.get_value_for(3) + })) { + Logger::error("Error reading colour in province definition: ", line); + ret = false; + } + ret &= add_province(identifier, colour); + } + } + ); + lock_provinces(); + return ret; +} diff --git a/src/openvic-simulation/map/Map.hpp b/src/openvic-simulation/map/Map.hpp new file mode 100644 index 0000000..71719e2 --- /dev/null +++ b/src/openvic-simulation/map/Map.hpp @@ -0,0 +1,117 @@ +#pragma once + +#include <functional> + +#include <openvic-dataloader/csv/LineObject.hpp> + +#include "openvic-simulation/map/Region.hpp" + +namespace OpenVic { + + struct Mapmode : HasIdentifier { + friend struct Map; + + using colour_func_t = std::function<colour_t(Map const&, Province const&)>; + using index_t = size_t; + + private: + const index_t index; + const colour_func_t colour_func; + + Mapmode(const std::string_view new_identifier, index_t new_index, colour_func_t new_colour_func); + + public: + static const Mapmode ERROR_MAPMODE; + + Mapmode(Mapmode&&) = default; + + index_t get_index() const; + colour_t get_colour(Map const& map, Province const& province) const; + }; + + struct GoodManager; + + /* REQUIREMENTS: + * MAP-4 + */ + struct Map { + using terrain_t = uint8_t; + using terrain_variant_map_t = std::map<colour_t, terrain_t>; + +#pragma pack(push, 1) + struct shape_pixel_t { + Province::index_t index; + terrain_t terrain; + }; +#pragma pack(pop) + private: + using colour_index_map_t = std::map<colour_t, Province::index_t>; + + IdentifierRegistry<Province> provinces; + IdentifierRegistry<Region> regions; + IdentifierRegistry<Mapmode> mapmodes; + ProvinceSet water_provinces; + + size_t width = 0, height = 0; + std::vector<shape_pixel_t> province_shape_image; + colour_index_map_t colour_index_map; + Province::index_t max_provinces = Province::MAX_INDEX; + Province::index_t selected_province = Province::NULL_INDEX; + + Pop::pop_size_t highest_province_population, total_map_population; + + Province::index_t get_index_from_colour(colour_t colour) const; + + public: + Map(); + + bool add_province(const std::string_view identifier, colour_t colour); + void lock_provinces(); + bool set_water_province(const std::string_view identifier); + bool set_water_province_list(std::vector<std::string_view> const& list); + void lock_water_provinces(); + bool add_region(const std::string_view identifier, std::vector<std::string_view> const& province_identifiers); + void lock_regions(); + + size_t get_province_count() const; + std::vector<Province> const& get_provinces() 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(const std::string_view identifier); + Province const* get_province_by_identifier(const std::string_view identifier) const; + Province::index_t get_province_index_at(size_t x, size_t y) const; + bool set_max_provinces(Province::index_t new_max_provinces); + Province::index_t get_max_provinces() const; + void set_selected_province(Province::index_t index); + Province::index_t get_selected_province_index() const; + Province const* get_selected_province() const; + + Region* get_region_by_identifier(const std::string_view identifier); + Region const* get_region_by_identifier(const std::string_view identifier) const; + size_t get_region_count() const; + std::vector<Region> const& get_regions() const; + + bool generate_province_shape_image(size_t new_width, size_t new_height, uint8_t const* colour_data, + uint8_t const* terrain_data, terrain_variant_map_t const& terrain_variant_map, bool detailed_errors); + size_t get_width() const; + size_t get_height() const; + std::vector<shape_pixel_t> const& get_province_shape_image() const; + + bool add_mapmode(const std::string_view identifier, Mapmode::colour_func_t colour_func); + IDENTIFIER_REGISTRY_ACCESSORS(Mapmode, mapmode) + static constexpr size_t MAPMODE_COLOUR_SIZE = 4; + bool generate_mapmode_colours(Mapmode::index_t index, uint8_t* target) const; + + bool setup(GoodManager const& good_manager, BuildingManager const& building_manager, PopManager const& pop_manager); + + void update_highest_province_population(); + Pop::pop_size_t get_highest_province_population() const; + void update_total_map_population(); + Pop::pop_size_t get_total_map_population() const; + + void update_state(Date const& today); + void tick(Date const& today); + + bool load_province_definitions(std::vector<ovdl::csv::LineObject> const& lines); + }; +} diff --git a/src/openvic-simulation/map/Province.cpp b/src/openvic-simulation/map/Province.cpp new file mode 100644 index 0000000..43eddd1 --- /dev/null +++ b/src/openvic-simulation/map/Province.cpp @@ -0,0 +1,129 @@ +#include "Province.hpp" + +#include <cassert> +#include <iomanip> +#include <sstream> + +using namespace OpenVic; +using namespace OpenVic::NodeTools; + +Province::Province(const std::string_view new_identifier, colour_t new_colour, index_t new_index) + : HasIdentifierAndColour { new_identifier, new_colour, false }, + index { new_index }, + buildings { "buildings", false } { + assert(index != NULL_INDEX); +} + +Province::index_t Province::get_index() const { + return index; +} + +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; +} + +bool Province::add_building(Building&& building) { + return buildings.add_item(std::move(building)); +} + +void Province::reset_buildings() { + buildings.reset(); +} + +bool Province::expand_building(const std::string_view building_type_identifier) { + Building* building = buildings.get_item_by_identifier(building_type_identifier); + if (building == nullptr) return false; + return building->expand(); +} + +Good const* Province::get_rgo() const { + return rgo; +} + +std::string Province::to_string() const { + std::stringstream stream; + stream << "(#" << std::to_string(index) << ", " << get_identifier() << ", 0x" << colour_to_hex_string() << ")"; + return stream.str(); +} + +bool Province::load_pop_list(PopManager const& pop_manager, ast::NodeCPtr root) { + return expect_list_reserve_length( + pops, + [this, &pop_manager](ast::NodeCPtr pop_node) -> bool { + return pop_manager.load_pop_into_province(*this, pop_node); + } + )(root); +} + +bool Province::add_pop(Pop&& pop) { + if (!is_water()) { + pops.push_back(std::move(pop)); + return true; + } else { + Logger::error("Trying to add pop to water province ", get_identifier()); + return false; + } +} + +void Province::clear_pops() { + pops.clear(); +} + +size_t Province::get_pop_count() const { + return pops.size(); +} + +std::vector<Pop> const& Province::get_pops() const { + return pops; +} + +Pop::pop_size_t Province::get_total_population() const { + return total_population; +} + +distribution_t const& Province::get_pop_type_distribution() const { + return pop_types; +} + +distribution_t const& Province::get_culture_distribution() const { + return cultures; +} + +distribution_t const& Province::get_religion_distribution() const { + return religions; +} + +/* REQUIREMENTS: + * MAP-65 + */ +void Province::update_pops() { + total_population = 0; + pop_types.clear(); + cultures.clear(); + religions.clear(); + for (Pop const& pop : pops) { + total_population += pop.get_size(); + pop_types[&pop.get_type()] += pop.get_size(); + cultures[&pop.get_culture()] += pop.get_size(); + religions[&pop.get_religion()] += pop.get_size(); + } +} + +void Province::update_state(Date const& today) { + for (Building& building : buildings.get_items()) + building.update_state(today); + update_pops(); +} + +void Province::tick(Date const& today) { + for (Building& building : buildings.get_items()) + building.tick(today); +} diff --git a/src/openvic-simulation/map/Province.hpp b/src/openvic-simulation/map/Province.hpp new file mode 100644 index 0000000..682fc16 --- /dev/null +++ b/src/openvic-simulation/map/Province.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include "openvic-simulation/map/Building.hpp" +#include "openvic-simulation/pop/Pop.hpp" + +namespace OpenVic { + struct Map; + struct Region; + struct Good; + + /* REQUIREMENTS: + * MAP-5, MAP-7, MAP-8, MAP-43, MAP-47 + */ + struct Province : HasIdentifierAndColour { + friend struct Map; + + using index_t = uint16_t; + using life_rating_t = int8_t; + + static constexpr index_t NULL_INDEX = 0, MAX_INDEX = (1 << (8 * sizeof(index_t))) - 1; + + private: + const index_t index; + Region* region = nullptr; + bool water = false; + life_rating_t life_rating = 0; + IdentifierRegistry<Building> buildings; + // TODO - change this into a factory-like structure + Good const* rgo = nullptr; + + std::vector<Pop> pops; + Pop::pop_size_t total_population; + distribution_t pop_types, cultures, religions; + + Province(const std::string_view new_identifier, colour_t new_colour, index_t new_index); + + public: + Province(Province&&) = default; + + index_t get_index() const; + Region* get_region() const; + bool is_water() const; + life_rating_t get_life_rating() const; + bool add_building(Building&& building); + IDENTIFIER_REGISTRY_ACCESSORS(Building, building) + void reset_buildings(); + bool expand_building(const std::string_view building_type_identifier); + Good const* get_rgo() const; + std::string to_string() const; + + bool load_pop_list(PopManager const& pop_manager, ast::NodeCPtr root); + bool add_pop(Pop&& pop); + void clear_pops(); + size_t get_pop_count() const; + std::vector<Pop> const& get_pops() const; + Pop::pop_size_t get_total_population() const; + distribution_t const& get_pop_type_distribution() const; + distribution_t const& get_culture_distribution() const; + distribution_t const& get_religion_distribution() const; + void update_pops(); + + void update_state(Date const& today); + void tick(Date const& today); + }; +} diff --git a/src/openvic-simulation/map/Region.cpp b/src/openvic-simulation/map/Region.cpp new file mode 100644 index 0000000..33092c5 --- /dev/null +++ b/src/openvic-simulation/map/Region.cpp @@ -0,0 +1,69 @@ +#include "Region.hpp" + +using namespace OpenVic; + +bool ProvinceSet::add_province(Province* province) { + if (locked) { + Logger::error("Cannot add province to province set - locked!"); + return false; + } + if (province == nullptr) { + Logger::error("Cannot add province to province set - null province!"); + return false; + } + if (contains_province(province)) { + Logger::error("Cannot add province ", province->get_identifier(), " to province set - already in the set!"); + return false; + } + provinces.push_back(province); + return true; +} + +void ProvinceSet::lock(bool log) { + if (locked) { + Logger::error("Failed to lock province set - already locked!"); + } else { + locked = true; + if (log) Logger::info("Locked province set with ", size(), " provinces"); + } +} + +bool ProvinceSet::is_locked() const { + return locked; +} + +void ProvinceSet::reset() { + provinces.clear(); + locked = false; +} + +bool ProvinceSet::empty() const { + return provinces.empty(); +} + +size_t ProvinceSet::size() const { + return provinces.size(); +} + +void ProvinceSet::reserve(size_t size) { + if (locked) { + Logger::error("Failed to reserve space for ", size, " items in province set - already locked!"); + } else { + provinces.reserve(size); + } +} + +bool ProvinceSet::contains_province(Province const* province) const { + return province && std::find(provinces.begin(), provinces.end(), province) != provinces.end(); +} + +std::vector<Province*> const& ProvinceSet::get_provinces() const { + return provinces; +} + +Region::Region(const std::string_view new_identifier) : HasIdentifier { new_identifier } {} + +colour_t Region::get_colour() const { + if (provinces.empty()) return FULL_COLOUR << 16; + return provinces.front()->get_colour(); +} diff --git a/src/openvic-simulation/map/Region.hpp b/src/openvic-simulation/map/Region.hpp new file mode 100644 index 0000000..2fccf06 --- /dev/null +++ b/src/openvic-simulation/map/Region.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include "openvic-simulation/map/Province.hpp" + +namespace OpenVic { + + struct ProvinceSet { + protected: + std::vector<Province*> provinces; + bool locked = false; + + public: + bool add_province(Province* province); + void lock(bool log = false); + bool is_locked() const; + void reset(); + bool empty() const; + size_t size() const; + void reserve(size_t size); + bool contains_province(Province const* province) const; + std::vector<Province*> const& get_provinces() const; + }; + + /* REQUIREMENTS: + * MAP-6, MAP-44, MAP-48 + */ + struct Region : HasIdentifier, ProvinceSet { + friend struct Map; + + private: + Region(const std::string_view new_identifier); + + public: + Region(Region&&) = default; + + colour_t get_colour() const; + }; +} diff --git a/src/openvic-simulation/pop/Culture.cpp b/src/openvic-simulation/pop/Culture.cpp new file mode 100644 index 0000000..7b595fb --- /dev/null +++ b/src/openvic-simulation/pop/Culture.cpp @@ -0,0 +1,199 @@ +#include "Culture.hpp" + +#include "openvic-simulation/dataloader/NodeTools.hpp" + +using namespace OpenVic; +using namespace OpenVic::NodeTools; + +GraphicalCultureType::GraphicalCultureType(const std::string_view new_identifier) : HasIdentifier { new_identifier } {} + +CultureGroup::CultureGroup(const std::string_view new_identifier, const std::string_view new_leader, + GraphicalCultureType const& new_unit_graphical_culture_type, bool new_is_overseas) + : HasIdentifier { new_identifier }, leader { new_leader }, + unit_graphical_culture_type { new_unit_graphical_culture_type }, + is_overseas { new_is_overseas } {} + + +std::string const& CultureGroup::get_leader() const { + return leader; +} + +GraphicalCultureType const& CultureGroup::get_unit_graphical_culture_type() const { + return unit_graphical_culture_type; +} + +bool CultureGroup::get_is_overseas() const { + return is_overseas; +} + +Culture::Culture(const std::string_view new_identifier, colour_t new_colour, CultureGroup const& new_group, + std::vector<std::string> const& new_first_names, std::vector<std::string> const& new_last_names) + : HasIdentifierAndColour { new_identifier, new_colour, true }, + group { new_group }, + first_names { new_first_names }, + last_names { new_last_names } {} + +CultureGroup const& Culture::get_group() const { + return group; +} + +std::vector<std::string> const& Culture::get_first_names() const { + return first_names; +} + +std::vector<std::string> const& Culture::get_last_names() const { + return last_names; +} + +CultureManager::CultureManager() + : graphical_culture_types { "graphical culture types" }, + culture_groups { "culture groups" }, + cultures { "cultures" } {} + +bool CultureManager::add_graphical_culture_type(const std::string_view identifier) { + if (identifier.empty()) { + Logger::error("Invalid culture group identifier - empty!"); + return false; + } + return graphical_culture_types.add_item({ identifier }); +} + +bool CultureManager::add_culture_group(const std::string_view identifier, const std::string_view leader, GraphicalCultureType const* graphical_culture_type, bool is_overseas) { + if (!graphical_culture_types.is_locked()) { + Logger::error("Cannot register culture groups until graphical culture types are locked!"); + return false; + } + if (identifier.empty()) { + Logger::error("Invalid culture group identifier - empty!"); + return false; + } + if (leader.empty()) { + Logger::error("Invalid culture group leader - empty!"); + return false; + } + if (graphical_culture_type == nullptr) { + Logger::error("Null graphical culture type for ", identifier); + return false; + } + return culture_groups.add_item({ identifier, leader, *graphical_culture_type, is_overseas }); +} + +bool CultureManager::add_culture(const std::string_view identifier, colour_t colour, CultureGroup const* group, std::vector<std::string> const& first_names, std::vector<std::string> const& last_names) { + if (!culture_groups.is_locked()) { + Logger::error("Cannot register cultures until culture groups are locked!"); + return false; + } + if (identifier.empty()) { + Logger::error("Invalid culture identifier - empty!"); + return false; + } + if (group == nullptr) { + Logger::error("Null culture group for ", identifier); + return false; + } + if (colour > MAX_COLOUR_RGB) { + Logger::error("Invalid culture colour for ", identifier, ": ", colour_to_hex_string(colour)); + return false; + } + return cultures.add_item({ identifier, colour, *group, first_names, last_names }); +} + +bool CultureManager::load_graphical_culture_type_file(ast::NodeCPtr root) { + const bool ret = expect_list_reserve_length( + graphical_culture_types, + expect_identifier( + std::bind(&CultureManager::add_graphical_culture_type, this, std::placeholders::_1) + ) + )(root); + lock_graphical_culture_types(); + return ret; +} + +bool CultureManager::_load_culture_group(size_t& total_expected_cultures, + GraphicalCultureType const* default_unit_graphical_culture_type, + const std::string_view culture_group_key, ast::NodeCPtr culture_group_node) { + + std::string_view leader; + GraphicalCultureType const* unit_graphical_culture_type = default_unit_graphical_culture_type; + bool is_overseas = true; + + bool ret = expect_dictionary_keys_and_length( + [&total_expected_cultures](size_t size) -> size_t { + total_expected_cultures += size; + return size; + }, + ALLOW_OTHER_KEYS, + "leader", ONE_EXACTLY, [&total_expected_cultures, &leader](ast::NodeCPtr node) -> bool { + total_expected_cultures--; + return expect_identifier(assign_variable_callback(leader))(node); + }, + "unit", ZERO_OR_ONE, [this, &total_expected_cultures, &unit_graphical_culture_type](ast::NodeCPtr node) -> bool { + total_expected_cultures--; + return expect_graphical_culture_type(unit_graphical_culture_type)(node); + }, + "union", ZERO_OR_ONE, [&total_expected_cultures](ast::NodeCPtr) -> bool { + total_expected_cultures--; + return true; + }, + "is_overseas", ZERO_OR_ONE, [&total_expected_cultures, &is_overseas](ast::NodeCPtr node) -> bool { + total_expected_cultures--; + return expect_bool(assign_variable_callback(is_overseas))(node); + } + )(culture_group_node); + ret &= add_culture_group(culture_group_key, leader, unit_graphical_culture_type, is_overseas); + return ret; +} + +bool CultureManager::_load_culture(CultureGroup const* culture_group, + const std::string_view culture_key, ast::NodeCPtr culture_node) { + + colour_t colour = NULL_COLOUR; + std::vector<std::string> first_names, last_names; + + bool ret = expect_dictionary_keys( + "color", ONE_EXACTLY, expect_colour(assign_variable_callback(colour)), + "first_names", ONE_EXACTLY, name_list_callback(first_names), + "last_names", ONE_EXACTLY, name_list_callback(last_names), + "radicalism", ZERO_OR_ONE, success_callback, + "primary", ZERO_OR_ONE, success_callback + )(culture_node); + ret &= add_culture(culture_key, colour, culture_group, first_names, last_names); + return ret; +} + +bool CultureManager::load_culture_file(ast::NodeCPtr root) { + if (!graphical_culture_types.is_locked()) { + Logger::error("Cannot load culture groups until graphical culture types are locked!"); + return false; + } + + static const std::string default_unit_graphical_culture_type_identifier = "Generic"; + GraphicalCultureType const* const default_unit_graphical_culture_type = get_graphical_culture_type_by_identifier(default_unit_graphical_culture_type_identifier); + if (default_unit_graphical_culture_type == nullptr) { + Logger::error("Failed to find default unit graphical culture type: ", default_unit_graphical_culture_type_identifier); + } + + size_t total_expected_cultures = 0; + bool ret = expect_dictionary_reserve_length( + culture_groups, + [this, default_unit_graphical_culture_type, &total_expected_cultures](std::string_view key, ast::NodeCPtr value) -> bool { + return _load_culture_group(total_expected_cultures, default_unit_graphical_culture_type, key, value); + } + )(root); + lock_culture_groups(); + cultures.reserve(cultures.size() + total_expected_cultures); + + ret &= expect_dictionary( + [this](std::string_view culture_group_key, ast::NodeCPtr culture_group_value) -> bool { + CultureGroup const* culture_group = get_culture_group_by_identifier(culture_group_key); + return expect_dictionary( + [this, culture_group](std::string_view key, ast::NodeCPtr value) -> bool { + if (key == "leader" || key == "unit" || key == "union" || key == "is_overseas") return true; + return _load_culture(culture_group, key, value); + } + )(culture_group_value); + } + )(root); + lock_cultures(); + return ret; +} diff --git a/src/openvic-simulation/pop/Culture.hpp b/src/openvic-simulation/pop/Culture.hpp new file mode 100644 index 0000000..d27c7c9 --- /dev/null +++ b/src/openvic-simulation/pop/Culture.hpp @@ -0,0 +1,83 @@ +#pragma once + +#include "openvic-simulation/types/IdentifierRegistry.hpp" + +namespace OpenVic { + + struct CultureManager; + + struct GraphicalCultureType : HasIdentifier { + friend struct CultureManager; + + private: + GraphicalCultureType(const std::string_view new_identifier); + + public: + GraphicalCultureType(GraphicalCultureType&&) = default; + }; + + struct CultureGroup : HasIdentifier { + friend struct CultureManager; + + private: + const std::string leader; + GraphicalCultureType const& unit_graphical_culture_type; + const bool is_overseas; + + // TODO - union tag + + CultureGroup(const std::string_view new_identifier, const std::string_view new_leader, GraphicalCultureType const& new_unit_graphical_culture_type, bool new_is_overseas); + + public: + CultureGroup(CultureGroup&&) = default; + + std::string const& get_leader() const; + GraphicalCultureType const& get_unit_graphical_culture_type() const; + bool get_is_overseas() const; + }; + + struct Culture : HasIdentifierAndColour { + friend struct CultureManager; + + private: + CultureGroup const& group; + const std::vector<std::string> first_names, last_names; + + // TODO - radicalism, primary tag + + Culture(const std::string_view new_identifier, colour_t new_colour, CultureGroup const& new_group, std::vector<std::string> const& new_first_names, std::vector<std::string> const& new_last_names); + + public: + Culture(Culture&&) = default; + + CultureGroup const& get_group() const; + std::vector<std::string> const& get_first_names() const; + std::vector<std::string> const& get_last_names() const; + }; + + struct CultureManager { + private: + IdentifierRegistry<GraphicalCultureType> graphical_culture_types; + IdentifierRegistry<CultureGroup> culture_groups; + IdentifierRegistry<Culture> cultures; + + bool _load_culture_group(size_t& total_expected_cultures, GraphicalCultureType const* default_unit_graphical_culture_type, + const std::string_view culture_group_key, ast::NodeCPtr culture_group_node); + bool _load_culture(CultureGroup const* culture_group, const std::string_view culture_key, ast::NodeCPtr node); + + public: + CultureManager(); + + bool add_graphical_culture_type(const std::string_view identifier); + IDENTIFIER_REGISTRY_ACCESSORS(GraphicalCultureType, graphical_culture_type) + + bool add_culture_group(const std::string_view identifier, const std::string_view leader, GraphicalCultureType const* new_graphical_culture_type, bool is_overseas); + IDENTIFIER_REGISTRY_ACCESSORS(CultureGroup, culture_group) + + bool add_culture(const std::string_view identifier, colour_t colour, CultureGroup const* group, std::vector<std::string> const& first_names, std::vector<std::string> const& last_names); + IDENTIFIER_REGISTRY_ACCESSORS(Culture, culture) + + bool load_graphical_culture_type_file(ast::NodeCPtr root); + bool load_culture_file(ast::NodeCPtr root); + }; +} diff --git a/src/openvic-simulation/pop/Pop.cpp b/src/openvic-simulation/pop/Pop.cpp new file mode 100644 index 0000000..7b4dd60 --- /dev/null +++ b/src/openvic-simulation/pop/Pop.cpp @@ -0,0 +1,163 @@ +#include "Pop.hpp" + +#include <cassert> + +#include "openvic-simulation/dataloader/NodeTools.hpp" +#include "openvic-simulation/map/Province.hpp" +#include "openvic-simulation/utility/Logger.hpp" + +using namespace OpenVic; +using namespace OpenVic::NodeTools; + +Pop::Pop(PopType const& new_type, Culture const& new_culture, Religion const& new_religion, pop_size_t new_size) + : type { new_type }, + culture { new_culture }, + religion { new_religion }, + size { new_size } { + assert(size > 0); +} + +PopType const& Pop::get_type() const { + return type; +} + +Culture const& Pop::get_culture() const { + return culture; +} + +Religion const& Pop::get_religion() const { + return religion; +} + +Pop::pop_size_t Pop::get_size() const { + return size; +} + +Pop::pop_size_t Pop::get_num_promoted() const { + return num_promoted; +} + +Pop::pop_size_t Pop::get_num_demoted() const { + return num_demoted; +} + +Pop::pop_size_t Pop::get_num_migrated() const { + return num_migrated; +} + +Pop::pop_size_t Pop::get_pop_daily_change() const { + return Pop::get_num_promoted() - (Pop::get_num_demoted() + Pop::get_num_migrated()); +} + +PopType::PopType(const std::string_view new_identifier, colour_t new_colour, + strata_t new_strata, sprite_t new_sprite, + Pop::pop_size_t new_max_size, Pop::pop_size_t new_merge_max_size, + bool new_state_capital_only, bool new_demote_migrant, bool new_is_artisan, bool new_is_slave) + : HasIdentifierAndColour { new_identifier, new_colour, true }, + strata { new_strata }, + sprite { new_sprite }, + max_size { new_max_size }, + merge_max_size { new_merge_max_size }, + state_capital_only { new_state_capital_only }, + demote_migrant { new_demote_migrant }, + is_artisan { new_is_artisan }, + is_slave { new_is_slave } { + assert(sprite > 0); + assert(max_size > 0); + assert(merge_max_size > 0); +} + +PopType::sprite_t PopType::get_sprite() const { + return sprite; +} + +PopType::strata_t PopType::get_strata() const { + return strata; +} + +Pop::pop_size_t PopType::get_max_size() const { + return max_size; +} + +Pop::pop_size_t PopType::get_merge_max_size() const { + return merge_max_size; +} + +bool PopType::get_state_capital_only() const { + return state_capital_only; +} + +bool PopType::get_demote_migrant() const { + return demote_migrant; +} + +bool PopType::get_is_artisan() const { + return is_artisan; +} + +bool PopType::get_is_slave() const { + return is_slave; +} + +PopManager::PopManager() : pop_types { "pop types" } {} + +bool PopManager::add_pop_type(const std::string_view identifier, colour_t colour, PopType::strata_t strata, PopType::sprite_t sprite, + Pop::pop_size_t max_size, Pop::pop_size_t merge_max_size, bool state_capital_only, bool demote_migrant, bool is_artisan, bool is_slave) { + if (identifier.empty()) { + Logger::error("Invalid pop type identifier - empty!"); + return false; + } + if (colour > MAX_COLOUR_RGB) { + Logger::error("Invalid pop type colour for ", identifier, ": ", colour_to_hex_string(colour)); + return false; + } + if (sprite <= 0) { + Logger::error("Invalid pop type sprite index for ", identifier, ": ", sprite); + return false; + } + if (max_size <= 0) { + Logger::error("Invalid pop type max size for ", identifier, ": ", max_size); + return false; + } + if (merge_max_size <= 0) { + Logger::error("Invalid pop type merge max size for ", identifier, ": ", merge_max_size); + return false; + } + return pop_types.add_item({ identifier, colour, strata, sprite, max_size, merge_max_size, state_capital_only, demote_migrant, is_artisan, is_slave }); +} + +bool PopManager::load_pop_type_file(std::filesystem::path const& path, ast::NodeCPtr root) { + + // TODO - pop type loading + + if (pop_types.empty()) + return add_pop_type("test_pop_type", 0xFF0000, PopType::strata_t::POOR, 1, 1, 1, false, false, false, false); + return true; +} + +bool PopManager::load_pop_into_province(Province& province, ast::NodeCPtr root) const { + static PopType const* type = get_pop_type_by_identifier("test_pop_type"); + Culture const* culture = nullptr; + Religion const* religion = nullptr; + Pop::pop_size_t size = 0; + + bool ret = expect_assign( + [this, &culture, &religion, &size](std::string_view, ast::NodeCPtr pop_node) -> bool { + return expect_dictionary_keys( + "culture", ONE_EXACTLY, culture_manager.expect_culture(culture), + "religion", ONE_EXACTLY, religion_manager.expect_religion(religion), + "size", ONE_EXACTLY, expect_uint(assign_variable_callback_uint("pop size", size)), + "militancy", ZERO_OR_ONE, success_callback, + "rebel_type", ZERO_OR_ONE, success_callback + )(pop_node); + } + )(root); + + if (type != nullptr && culture != nullptr && religion != nullptr && size > 0) { + ret &= province.add_pop({ *type, *culture, *religion, size }); + } else { + Logger::error("Some pop arguments are invalid: type = ", type, ", culture = ", culture, ", religion = ", religion, ", size = ", size); + ret = false; + } + return ret; +} diff --git a/src/openvic-simulation/pop/Pop.hpp b/src/openvic-simulation/pop/Pop.hpp new file mode 100644 index 0000000..3c635a4 --- /dev/null +++ b/src/openvic-simulation/pop/Pop.hpp @@ -0,0 +1,101 @@ +#pragma once + +#include <filesystem> + +#include "openvic-simulation/pop/Culture.hpp" +#include "openvic-simulation/pop/Religion.hpp" + +namespace OpenVic { + + struct PopManager; + struct PopType; + + /* REQUIREMENTS: + * POP-18, POP-19, POP-20, POP-21 + */ + struct Pop { + friend struct PopManager; + + using pop_size_t = int64_t; + + private: + PopType const& type; + Culture const& culture; + Religion const& religion; + pop_size_t size, num_promoted, num_demoted, num_migrated; + + Pop(PopType const& new_type, Culture const& new_culture, Religion const& new_religion, pop_size_t new_size); + + public: + Pop(Pop const&) = delete; + Pop(Pop&&) = default; + Pop& operator=(Pop const&) = delete; + Pop& operator=(Pop&&) = delete; + + PopType const& get_type() const; + Culture const& get_culture() const; + Religion const& get_religion() const; + pop_size_t get_size() const; + pop_size_t get_num_promoted() const; + pop_size_t get_num_demoted() const; + pop_size_t get_num_migrated() const; + pop_size_t get_pop_daily_change() const; + }; + + /* REQUIREMENTS: + * POP-26 + */ + struct PopType : HasIdentifierAndColour { + friend struct PopManager; + + using sprite_t = uint8_t; + + private: + const enum class strata_t { + POOR, + MIDDLE, + RICH + } strata; + const sprite_t sprite; + const Pop::pop_size_t max_size, merge_max_size; + const bool state_capital_only, demote_migrant, is_artisan, is_slave; + + // TODO - rebel composition, life/everyday/luxury needs, country and province migration targets, promote_to targets, ideologies and issues + + PopType(const std::string_view new_identifier, colour_t new_colour, strata_t new_strata, sprite_t new_sprite, Pop::pop_size_t new_max_size, Pop::pop_size_t new_merge_max_size, + bool new_state_capital_only, bool new_demote_migrant, bool new_is_artisan, bool new_is_slave); + + public: + PopType(PopType&&) = default; + + strata_t get_strata() const; + sprite_t get_sprite() const; + Pop::pop_size_t get_max_size() const; + Pop::pop_size_t get_merge_max_size() const; + bool get_state_capital_only() const; + bool get_demote_migrant() const; + bool get_is_artisan() const; + bool get_is_slave() const; + }; + + struct Province; + + struct PopManager { + private: + IdentifierRegistry<PopType> pop_types; + + public: + CultureManager culture_manager; + ReligionManager religion_manager; + + PopManager(); + + bool add_pop_type(const std::string_view identifier, colour_t new_colour, PopType::strata_t strata, PopType::sprite_t sprite, + Pop::pop_size_t max_size, Pop::pop_size_t merge_max_size, bool state_capital_only, bool demote_migrant, + bool is_artisan, bool is_slave); + IDENTIFIER_REGISTRY_ACCESSORS(PopType, pop_type) + + bool load_pop_type_file(std::filesystem::path const& path, ast::NodeCPtr root); + bool load_pop_into_province(Province& province, ast::NodeCPtr root) const; + }; +} diff --git a/src/openvic-simulation/pop/Religion.cpp b/src/openvic-simulation/pop/Religion.cpp new file mode 100644 index 0000000..b336ae1 --- /dev/null +++ b/src/openvic-simulation/pop/Religion.cpp @@ -0,0 +1,110 @@ +#include "Religion.hpp" + +#include <cassert> + +using namespace OpenVic; +using namespace OpenVic::NodeTools; + +ReligionGroup::ReligionGroup(const std::string_view new_identifier) : HasIdentifier { new_identifier } {} + +Religion::Religion(const std::string_view new_identifier, colour_t new_colour, + ReligionGroup const& new_group, icon_t new_icon, bool new_pagan) + : HasIdentifierAndColour { new_identifier, new_colour, true }, + group { new_group }, + icon { new_icon }, + pagan { new_pagan } { + assert(icon > 0); +} + +ReligionGroup const& Religion::get_group() const { + return group; +} + +Religion::icon_t Religion::get_icon() const { + return icon; +} + +bool Religion::get_pagan() const { + return pagan; +} + +ReligionManager::ReligionManager() + : religion_groups { "religion groups" }, + religions { "religions" } {} + +bool ReligionManager::add_religion_group(const std::string_view identifier) { + if (identifier.empty()) { + Logger::error("Invalid religion group identifier - empty!"); + return false; + } + return religion_groups.add_item({ identifier }); +} + +bool ReligionManager::add_religion(const std::string_view identifier, colour_t colour, ReligionGroup const* group, Religion::icon_t icon, bool pagan) { + if (!religion_groups.is_locked()) { + Logger::error("Cannot register religions until religion groups are locked!"); + return false; + } + if (identifier.empty()) { + Logger::error("Invalid religion identifier - empty!"); + return false; + } + if (group == nullptr) { + Logger::error("Null religion group for ", identifier); + return false; + } + if (colour > MAX_COLOUR_RGB) { + Logger::error("Invalid religion colour for ", identifier, ": ", colour_to_hex_string(colour)); + return false; + } + if (icon <= 0) { + Logger::error("Invalid religion icon for ", identifier, ": ", icon); + return false; + } + return religions.add_item({ identifier, colour, *group, icon, pagan }); +} + +bool ReligionManager::load_religion_file(ast::NodeCPtr root) { + + size_t total_expected_religions = 0; + bool ret = expect_dictionary_reserve_length( + religion_groups, + [this, &total_expected_religions](std::string_view key, ast::NodeCPtr value) -> bool { + bool ret = expect_list_and_length( + [&total_expected_religions](size_t size) -> size_t { + total_expected_religions += size; + return 0; + }, + success_callback + )(value); + ret &= add_religion_group(key); + return ret; + } + )(root); + lock_religion_groups(); + religions.reserve(religions.size() + total_expected_religions); + ret &= expect_dictionary( + [this](std::string_view religion_group_key, ast::NodeCPtr religion_group_value) -> bool { + + ReligionGroup const* religion_group = get_religion_group_by_identifier(religion_group_key); + + return expect_dictionary( + [this, religion_group](std::string_view key, ast::NodeCPtr value) -> bool { + colour_t colour = NULL_COLOUR; + Religion::icon_t icon = 0; + bool pagan = false; + + bool ret = expect_dictionary_keys( + "icon", ONE_EXACTLY, expect_uint(assign_variable_callback_uint("religion icon", icon)), + "color", ONE_EXACTLY, expect_colour(assign_variable_callback(colour)), + "pagan", ZERO_OR_ONE, expect_bool(assign_variable_callback(pagan)) + )(value); + ret &= add_religion(key, colour, religion_group, icon, pagan); + return ret; + } + )(religion_group_value); + } + )(root); + lock_religions(); + return ret; +} diff --git a/src/openvic-simulation/pop/Religion.hpp b/src/openvic-simulation/pop/Religion.hpp new file mode 100644 index 0000000..bd65321 --- /dev/null +++ b/src/openvic-simulation/pop/Religion.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include "openvic-simulation/types/IdentifierRegistry.hpp" +#include "openvic-simulation/dataloader/NodeTools.hpp" + +namespace OpenVic { + + struct ReligionManager; + + struct ReligionGroup : HasIdentifier { + friend struct ReligionManager; + + private: + ReligionGroup(const std::string_view new_identifier); + + public: + ReligionGroup(ReligionGroup&&) = default; + }; + + struct Religion : HasIdentifierAndColour { + friend struct ReligionManager; + + using icon_t = uint8_t; + + private: + ReligionGroup const& group; + const icon_t icon; + const bool pagan; + + Religion(const std::string_view new_identifier, colour_t new_colour, ReligionGroup const& new_group, icon_t new_icon, bool new_pagan); + + public: + Religion(Religion&&) = default; + + ReligionGroup const& get_group() const; + icon_t get_icon() const; + bool get_pagan() const; + }; + + struct ReligionManager { + private: + IdentifierRegistry<ReligionGroup> religion_groups; + IdentifierRegistry<Religion> religions; + + public: + ReligionManager(); + + bool add_religion_group(const std::string_view identifier); + IDENTIFIER_REGISTRY_ACCESSORS(ReligionGroup, religion_group) + + bool add_religion(const std::string_view identifier, colour_t colour, ReligionGroup const* group, Religion::icon_t icon, bool pagan); + IDENTIFIER_REGISTRY_ACCESSORS(Religion, religion) + + bool load_religion_file(ast::NodeCPtr root); + }; +} diff --git a/src/openvic-simulation/types/Colour.hpp b/src/openvic-simulation/types/Colour.hpp new file mode 100644 index 0000000..01f3852 --- /dev/null +++ b/src/openvic-simulation/types/Colour.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include <algorithm> +#include <cstdint> +#include <iomanip> +#include <sstream> +#include <string> + +namespace OpenVic { + // Represents a 24-bit RGB integer OR a 32-bit ARGB integer + using colour_t = uint32_t; + /* When colour_t is used as an identifier, NULL_COLOUR is disallowed + * and should be reserved as an error value. + * When colour_t is used in a purely graphical context, NULL_COLOUR + * should be allowed. + */ + static constexpr colour_t NULL_COLOUR = 0, FULL_COLOUR = 0xFF, MAX_COLOUR_RGB = 0xFFFFFF; + constexpr colour_t float_to_colour_byte(float f, float min = 0.0f, float max = 1.0f) { + return static_cast<colour_t>(std::clamp(min + f * (max - min), min, max) * 255.0f); + } + constexpr colour_t fraction_to_colour_byte(int n, int d, float min = 0.0f, float max = 1.0f) { + return float_to_colour_byte(static_cast<float>(n) / static_cast<float>(d), min, max); + } + constexpr colour_t float_to_alpha_value(float a) { + return float_to_colour_byte(a) << 24; + } + constexpr float colour_byte_to_float(colour_t colour) { + return std::clamp(static_cast<float>(colour) / 255.0f, 0.0f, 1.0f); + } + + inline std::string colour_to_hex_string(colour_t colour) { + std::ostringstream stream; + stream << std::hex << std::setfill('0') << std::setw(6) << colour; + return stream.str(); + } +} diff --git a/src/openvic-simulation/types/Date.cpp b/src/openvic-simulation/types/Date.cpp new file mode 100644 index 0000000..3449591 --- /dev/null +++ b/src/openvic-simulation/types/Date.cpp @@ -0,0 +1,262 @@ +#include "Date.hpp" + +#include <algorithm> +#include <cassert> +#include <cctype> +#include <charconv> + +#include "openvic-simulation/utility/Logger.hpp" +#include "openvic-simulation/utility/StringUtils.hpp" + +using namespace OpenVic; + +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; +} + +std::string Timespan::to_string() const { + return std::to_string(days); +} + +Timespan::operator std::string() const { + return to_string(); +} + +std::ostream& OpenVic::operator<<(std::ostream& out, Timespan const& timespan) { + return out << timespan.to_string(); +} + +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; +} + +Timespan::day_t const* Date::DAYS_UP_TO_MONTH = generate_days_up_to_month(); + +Timespan::day_t const* Date::generate_days_up_to_month() { + static Timespan::day_t days_up_to_month[MONTHS_IN_YEAR]; + Timespan::day_t days = 0; + for (int month = 0; month < MONTHS_IN_YEAR; + days_up_to_month[month] = days, days += DAYS_IN_MONTH[month++]); + assert(days == DAYS_IN_YEAR); + return days_up_to_month; +} + +Date::month_t const* Date::MONTH_FROM_DAY_IN_YEAR = generate_month_from_day_in_year(); + +Date::month_t const* Date::generate_month_from_day_in_year() { + static month_t month_from_day_in_year[DAYS_IN_YEAR]; + Timespan::day_t days_left = 0; + for (int day = 0, month = 0; day < DAYS_IN_YEAR; + days_left = (days_left > 0 ? days_left : DAYS_IN_MONTH[month++]) - 1, + month_from_day_in_year[day++] = month); + assert(days_left == 0); + assert(month_from_day_in_year[DAYS_IN_YEAR - 1] == MONTHS_IN_YEAR); + return month_from_day_in_year; +} + +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 MONTH_FROM_DAY_IN_YEAR[static_cast<Timespan::day_t>(timespan) % DAYS_IN_YEAR]; +} + +Date::day_t Date::getDay() const { + return (static_cast<Timespan::day_t>(timespan) % DAYS_IN_YEAR) - DAYS_UP_TO_MONTH[getMonth() - 1] + 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; +} + +std::string Date::to_string() const { + std::stringstream ss; + ss << *this; + return ss.str(); +} + +Date::operator std::string() const { + return to_string(); +} + +std::ostream& OpenVic::operator<<(std::ostream& out, Date const& date) { + return out << static_cast<int>(date.getYear()) << Date::SEPARATOR_CHARACTER << static_cast<int>(date.getMonth()) << Date::SEPARATOR_CHARACTER << static_cast<int>(date.getDay()); +} + +// Parsed from string of the form YYYY.MM.DD +Date Date::from_string(char const* const str, char const* const end, bool* successful) { + if (successful != nullptr) *successful = true; + + year_t year = 0; + month_t month = 1; + day_t day = 1; + + if (str == nullptr || end <= str) { + Logger::error("Invalid string start/end pointers: ", static_cast<void const*>(str), " - ", static_cast<void const*>(end)); + if (successful != nullptr) *successful = false; + return { year, month, day }; + } + + char const* year_end = str; + while (std::isdigit(*year_end) && ++year_end < end); + + if (year_end <= str) { + Logger::error("Failed to find year digits in date: ", std::string_view { str, static_cast<size_t>(end - str) }); + if (successful != nullptr) *successful = false; + return { year, month, day }; + } + + bool sub_successful = false; + uint64_t val = StringUtils::string_to_uint64(str, year_end, &sub_successful, 10); + if (!sub_successful || val >= 1 << (8 * sizeof(year_t))) { + Logger::error("Failed to read year: ", std::string_view { str, static_cast<size_t>(end - str) }); + if (successful != nullptr) *successful = false; + return { year, month, day }; + } + year = val; + if (year_end < end) { + if (*year_end == SEPARATOR_CHARACTER) { + char const* const month_start = year_end + 1; + char const* month_end = month_start; + if (month_start < end) { + while (std::isdigit(*month_end) && ++month_end < end); + } + if (month_start >= month_end) { + Logger::error("Failed to find month digits in date: ", std::string_view { str, static_cast<size_t>(end - str) }); + if (successful != nullptr) *successful = false; + } else { + sub_successful = false; + val = StringUtils::string_to_uint64(month_start, month_end, &sub_successful, 10); + if (!sub_successful || val < 1 || val > MONTHS_IN_YEAR) { + Logger::error("Failed to read month: ", std::string_view { str, static_cast<size_t>(end - str) }); + if (successful != nullptr) *successful = false; + } else { + month = val; + if (month_end < end) { + if (*month_end == SEPARATOR_CHARACTER) { + char const* const day_start = month_end + 1; + char const* day_end = day_start; + if (day_start < end) { + while (std::isdigit(*day_end) && ++day_end < end); + } + if (day_start >= day_end) { + Logger::error("Failed to find day digits in date: ", std::string_view { str, static_cast<size_t>(end - str) }); + if (successful != nullptr) *successful = false; + } else { + sub_successful = false; + val = StringUtils::string_to_uint64(day_start, day_end, &sub_successful); + if (!sub_successful || val < 1 || val > DAYS_IN_MONTH[month - 1]) { + Logger::error("Failed to read day: ", std::string_view { str, static_cast<size_t>(end - str) }); + if (successful != nullptr) *successful = false; + } else { + day = val; + if (day_end < end) { + Logger::error("Unexpected string \"", std::string_view { day_end, static_cast<size_t>(end - day_end) }, "\" at the end of date ", std::string_view { str, static_cast<size_t>(end - str) }); + if (successful != nullptr) *successful = false; + } + } + } + } else { + Logger::error("Unexpected character \"", *month_end, "\" in month of date ", std::string_view { str, static_cast<size_t>(end - str) }); + if (successful != nullptr) *successful = false; + } + } + } + } + } else { + Logger::error("Unexpected character \"", *year_end, "\" in year of date ", std::string_view { str, static_cast<size_t>(end - str) }); + if (successful != nullptr) *successful = false; + } + } + return { year, month, day }; +}; + +Date Date::from_string(char const* str, size_t length, bool* successful) { + return from_string(str, str + length, successful); +} + +Date Date::from_string(const std::string_view str, bool* successful) { + return from_string(str.data(), str.length(), successful); +} diff --git a/src/openvic-simulation/types/Date.hpp b/src/openvic-simulation/types/Date.hpp new file mode 100644 index 0000000..601f9dc --- /dev/null +++ b/src/openvic-simulation/types/Date.hpp @@ -0,0 +1,96 @@ +#pragma once + +#include <cstdint> +#include <ostream> +#include <string> + +namespace OpenVic { + // 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; + std::string to_string() const; + explicit operator std::string() const; + }; + std::ostream& operator<<(std::ostream& out, Timespan const& 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 Timespan::day_t const* DAYS_UP_TO_MONTH; + static month_t const* MONTH_FROM_DAY_IN_YEAR; + + static constexpr char SEPARATOR_CHARACTER = '.'; + + private: + // Number of days since Jan 1st, Year 0 + Timespan timespan; + + static Timespan _dateToTimespan(year_t year, month_t month, day_t day); + static Timespan::day_t const* generate_days_up_to_month(); + static month_t const* generate_month_from_day_in_year(); + + 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); + + std::string to_string() const; + explicit operator std::string() const; + // Parsed from string of the form YYYY.MM.DD + static Date from_string(char const* str, char const* end, bool* successful = nullptr); + static Date from_string(char const* str, size_t length, bool* successful = nullptr); + static Date from_string(const std::string_view str, bool* successful = nullptr); + }; + std::ostream& operator<<(std::ostream& out, Date const& date); +} diff --git a/src/openvic-simulation/types/IdentifierRegistry.cpp b/src/openvic-simulation/types/IdentifierRegistry.cpp new file mode 100644 index 0000000..6d5221b --- /dev/null +++ b/src/openvic-simulation/types/IdentifierRegistry.cpp @@ -0,0 +1,37 @@ +#include "IdentifierRegistry.hpp" + +#include <cassert> + +using namespace OpenVic; + +HasIdentifier::HasIdentifier(const std::string_view new_identifier) + : identifier { new_identifier } { + assert(!identifier.empty()); +} + +std::string const& HasIdentifier::get_identifier() const { + return identifier; +} + +HasColour::HasColour(colour_t const new_colour, bool can_be_null) : colour(new_colour) { + assert((can_be_null || colour != NULL_COLOUR) && colour <= MAX_COLOUR_RGB); +} + +colour_t HasColour::get_colour() const { return colour; } + +std::string HasColour::colour_to_hex_string() const { + return OpenVic::colour_to_hex_string(colour); +} + +HasIdentifierAndColour::HasIdentifierAndColour(const std::string_view new_identifier, + const colour_t new_colour, bool can_be_null) + : HasIdentifier { new_identifier }, + HasColour { new_colour, can_be_null } {} + +distribution_t::value_type OpenVic::get_largest_item(distribution_t const& dist) { + const distribution_t::const_iterator result = std::max_element(dist.begin(), dist.end(), + [](distribution_t::value_type a, distribution_t::value_type b) -> bool { + return a.second < b.second; + }); + return result != dist.end() ? *result : distribution_t::value_type { nullptr, -1.0f }; +} diff --git a/src/openvic-simulation/types/IdentifierRegistry.hpp b/src/openvic-simulation/types/IdentifierRegistry.hpp new file mode 100644 index 0000000..494ff3e --- /dev/null +++ b/src/openvic-simulation/types/IdentifierRegistry.hpp @@ -0,0 +1,197 @@ +#pragma once + +#include <map> +#include <vector> + +#include "openvic-simulation/dataloader/NodeTools.hpp" +#include "openvic-simulation/utility/Logger.hpp" + +namespace OpenVic { + /* + * 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(const std::string_view 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; + }; + + /* + * Base class for objects with associated colour information. + */ + class HasColour { + const colour_t colour; + + protected: + HasColour(const colour_t new_colour, bool can_be_null); + + public: + HasColour(HasColour const&) = delete; + HasColour(HasColour&&) = default; + HasColour& operator=(HasColour const&) = delete; + HasColour& operator=(HasColour&&) = delete; + + colour_t get_colour() const; + std::string colour_to_hex_string() const; + }; + + /* + * Base class for objects with a unique string identifier + * and associated colour information. + */ + class HasIdentifierAndColour : public HasIdentifier, public HasColour { + protected: + HasIdentifierAndColour(const std::string_view new_identifier, const colour_t new_colour, bool can_be_null); + + public: + HasIdentifierAndColour(HasIdentifierAndColour const&) = delete; + HasIdentifierAndColour(HasIdentifierAndColour&&) = default; + HasIdentifierAndColour& operator=(HasIdentifierAndColour const&) = delete; + HasIdentifierAndColour& operator=(HasIdentifierAndColour&&) = delete; + }; + + using distribution_t = std::map<HasIdentifierAndColour const*, float>; + + distribution_t::value_type get_largest_item(distribution_t const& dist); + + /* + * 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<typename T> + requires(std::derived_from<T, HasIdentifier>) + class IdentifierRegistry { + using identifier_index_map_t = std::map<std::string, size_t, std::less<void>>; + + const std::string name; + const bool log_lock; + std::vector<T> items; + bool locked = false; + identifier_index_map_t identifier_index_map; + + public: + IdentifierRegistry(const std::string_view new_name, bool new_log_lock = true) + : name { new_name }, log_lock { new_log_lock } {} + + std::string const& get_name() const { + return name; + } + + bool add_item(T&& item) { + if (locked) { + Logger::error("Cannot add item to the ", name, " registry - locked!"); + return false; + } + 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 false; + } + identifier_index_map[item.get_identifier()] = items.size(); + items.push_back(std::move(item)); + return true; + } + + void lock() { + if (locked) { + Logger::error("Failed to lock ", name, " registry - already locked!"); + } else { + locked = true; + if (log_lock) Logger::info("Locked ", name, " registry after registering ", size(), " items"); + } + } + + bool is_locked() const { + return locked; + } + + void reset() { + identifier_index_map.clear(); + items.clear(); + locked = false; + } + + size_t size() const { + return items.size(); + } + + bool empty() const { + return items.empty(); + } + + void reserve(size_t size) { + if (locked) { + Logger::error("Failed to reserve space for ", size, " items in ", name, " registry - already locked!"); + } else { + items.reserve(size); + } + } + + T* get_item_by_identifier(const std::string_view identifier) { + const identifier_index_map_t::const_iterator it = identifier_index_map.find(identifier); + if (it != identifier_index_map.end()) return &items[it->second]; + return nullptr; + } + + T const* get_item_by_identifier(const std::string_view identifier) const { + const identifier_index_map_t::const_iterator it = identifier_index_map.find(identifier); + if (it != identifier_index_map.end()) return &items[it->second]; + 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; + } + + NodeTools::node_callback_t expect_item(T const*& ret) const { + return NodeTools::expect_identifier( + [this, &ret](std::string_view identifier) -> bool { + ret = get_item_by_identifier(identifier); + if (ret != nullptr) return true; + Logger::error("Invalid ", name, ": ", identifier); + return false; + } + ); + } + }; + +#define IDENTIFIER_REGISTRY_ACCESSORS_CUSTOM_PLURAL(type, singular, plural) \ + void lock_##plural() { plural.lock(); } \ + type const* get_##singular##_by_index(size_t index) const { \ + return plural.get_item_by_index(index); } \ + type const* get_##singular##_by_identifier(const std::string_view identifier) const { \ + return plural.get_item_by_identifier(identifier); } \ + size_t get_##singular##_count() const { \ + return plural.size(); } \ + std::vector<type> const& get_##plural() const { \ + return plural.get_items(); } \ + NodeTools::node_callback_t expect_##singular(type const*& ret) const { \ + return plural.expect_item(ret); } + +#define IDENTIFIER_REGISTRY_ACCESSORS(type, name) IDENTIFIER_REGISTRY_ACCESSORS_CUSTOM_PLURAL(type, name, name##s) +} diff --git a/src/openvic-simulation/types/fixed_point/FixedPoint.hpp b/src/openvic-simulation/types/fixed_point/FixedPoint.hpp new file mode 100644 index 0000000..913f237 --- /dev/null +++ b/src/openvic-simulation/types/fixed_point/FixedPoint.hpp @@ -0,0 +1,584 @@ +#pragma once + +#include <cerrno> +#include <cmath> +#include <cstdint> +#include <cstdlib> +#include <limits> +#include <string_view> + +#include "openvic-simulation/utility/Logger.hpp" +#include "openvic-simulation/utility/NumberUtils.hpp" +#include "openvic-simulation/utility/StringUtils.hpp" + +#include "FixedPointLUT.hpp" + +namespace OpenVic { + struct fixed_point_t { + static constexpr size_t SIZE = 8; + + static constexpr int32_t PRECISION = FPLUT::SIN_LUT_PRECISION; + static constexpr int64_t ONE = 1 << PRECISION; + + constexpr fixed_point_t() : value { 0 } {} + constexpr fixed_point_t(int64_t new_value) : value { new_value } {} + constexpr fixed_point_t(int32_t new_value) : value { static_cast<int64_t>(new_value) << PRECISION } {} + + // Trivial destructor + ~fixed_point_t() = default; + + static constexpr fixed_point_t max() { + return std::numeric_limits<int64_t>::max(); + } + + static constexpr fixed_point_t min() { + return std::numeric_limits<int64_t>::min(); + } + + static constexpr fixed_point_t usable_max() { + return static_cast<int64_t>(2147483648LL); + } + + static constexpr fixed_point_t usable_min() { + return -usable_max(); + } + + static constexpr fixed_point_t _0() { + return 0; + } + + static constexpr fixed_point_t _1() { + return 1; + } + + static constexpr fixed_point_t _2() { + return 2; + } + + static constexpr fixed_point_t _3() { + return 3; + } + + static constexpr fixed_point_t _4() { + return 4; + } + + static constexpr fixed_point_t _5() { + return 5; + } + + static constexpr fixed_point_t _6() { + return 6; + } + + static constexpr fixed_point_t _7() { + return 7; + } + + static constexpr fixed_point_t _8() { + return 8; + } + + static constexpr fixed_point_t _9() { + return 9; + } + + static constexpr fixed_point_t _10() { + return 10; + } + + static constexpr fixed_point_t _50() { + return 50; + } + + static constexpr fixed_point_t _100() { + return 100; + } + + static constexpr fixed_point_t _200() { + return 200; + } + + static constexpr fixed_point_t _0_01() { + return _1() / _100(); + } + + static constexpr fixed_point_t _0_02() { + return _0_01() * 2; + } + + static constexpr fixed_point_t _0_03() { + return _0_01() * 3; + } + + static constexpr fixed_point_t _0_04() { + return _0_01() * 4; + } + + static constexpr fixed_point_t _0_05() { + return _0_01() * 5; + } + + static constexpr fixed_point_t _0_10() { + return _1() / 10; + } + + static constexpr fixed_point_t _0_20() { + return _0_10() * 2; + } + + static constexpr fixed_point_t _0_25() { + return _1() / 4; + } + + static constexpr fixed_point_t _0_33() { + return _1() / 3; + } + + static constexpr fixed_point_t _0_50() { + return _1() / 2; + } + + static constexpr fixed_point_t _0_75() { + return _1() - _0_25(); + } + + static constexpr fixed_point_t _0_95() { + return _1() - _0_05(); + } + + static constexpr fixed_point_t _0_99() { + return _1() - _0_01(); + } + + static constexpr fixed_point_t _1_01() { + return _1() + _0_01(); + } + + static constexpr fixed_point_t _1_10() { + return _1() + _0_10(); + } + + static constexpr fixed_point_t _1_50() { + return _1() + _0_50(); + } + + static constexpr fixed_point_t minus_one() { + return -1; + } + + static constexpr fixed_point_t pi() { + return static_cast<int64_t>(205887LL); + } + + static constexpr fixed_point_t pi2() { + return pi() * 2; + } + + static constexpr fixed_point_t pi_quarter() { + return pi() / 4; + } + + static constexpr fixed_point_t pi_half() { + return pi() / 2; + } + + static constexpr fixed_point_t one_div_pi2() { + return 1 / pi2(); + } + + static constexpr fixed_point_t deg2rad() { + return static_cast<int64_t>(1143LL); + } + + static constexpr fixed_point_t rad2deg() { + return static_cast<int64_t>(3754936LL); + } + + static constexpr fixed_point_t e() { + return static_cast<int64_t>(178145LL); + } + + constexpr bool is_negative() const { + return value < 0; + } + + constexpr fixed_point_t abs() const { + return !is_negative() ? value : -value; + } + + // Doesn't account for sign, so -n.abc -> 1 - 0.abc + constexpr fixed_point_t get_frac() const { + return value & (ONE - 1); + } + + constexpr int64_t to_int64_t() const { + return value >> PRECISION; + } + + constexpr void set_raw_value(int64_t value) { + this->value = value; + } + + constexpr int64_t get_raw_value() const { + return value; + } + + constexpr int32_t to_int32_t() const { + return static_cast<int32_t>(to_int64_t()); + } + + constexpr float to_float() const { + return value / static_cast<float>(ONE); + } + + constexpr float to_float_rounded() const { + return static_cast<float>(NumberUtils::round_to_int64((value / static_cast<float>(ONE)) * 100000.0f)) / 100000.0f; + } + + constexpr double to_double() const { + return value / static_cast<double>(ONE); + } + + constexpr float to_double_rounded() const { + return NumberUtils::round_to_int64((value / static_cast<double>(ONE)) * 100000.0) / 100000.0; + } + + std::string to_string() const { + fixed_point_t val = abs(); + std::string str = std::to_string(val.to_int64_t()) + "."; + if (is_negative()) str = "-" + str; + val = val.get_frac(); + do { + val *= 10; + str.push_back('0' + static_cast<char>(val.to_int64_t())); + val = val.get_frac(); + } while (val > 0); + return str; + } + + // Deterministic + static constexpr fixed_point_t parse_raw(int64_t value) { + return value; + } + + // Deterministic + static constexpr fixed_point_t parse(int64_t value) { + return value << PRECISION; + } + + // Deterministic + static constexpr fixed_point_t parse(char const* str, char const* const end, bool* successful = nullptr) { + if (successful != nullptr) *successful = false; + + if (str == nullptr || str >= end) { + return _0(); + } + + bool negative = false; + + if (*str == '-') { + negative = true; + ++str; + if (str == end) return _0(); + } + + char const* dot_pointer = str; + while (*dot_pointer != '.' && ++dot_pointer != end); + + if (dot_pointer == str && dot_pointer + 1 == end) { + // Invalid: ".", "+." or "-." + return _0(); + } + + fixed_point_t result = _0(); + if (successful != nullptr) *successful = true; + + if (dot_pointer != str) { + // Non-empty integer part + bool int_successful = false; + result += parse_integer(str, dot_pointer, &int_successful); + if (!int_successful && successful != nullptr) *successful = false; + } + + if (dot_pointer + 1 < end) { + // Non-empty fractional part + bool frac_successful = false; + result += parse_fraction(dot_pointer + 1, end, &frac_successful); + if (!frac_successful && successful != nullptr) *successful = false; + } + + return negative ? -result : result; + } + + static constexpr fixed_point_t parse(char const* str, size_t length, bool* successful = nullptr) { + return parse(str, str + length, successful); + } + + static fixed_point_t parse(const std::string_view str, bool* successful = nullptr) { + return parse(str.data(), str.length(), successful); + } + + // Not Deterministic + static constexpr fixed_point_t parse_unsafe(float value) { + return static_cast<int64_t>(value * ONE + 0.5f * (value < 0 ? -1 : 1)); + } + + // Not Deterministic + static fixed_point_t parse_unsafe(char const* value) { + char* endpointer; + double double_value = std::strtod(value, &endpointer); + + if (*endpointer != '\0') { + Logger::error("Unsafe fixed point parse failed to parse the end of a string: \"", endpointer, "\""); + } + + int64_t integer_value = static_cast<long>(double_value * ONE + 0.5 * (double_value < 0 ? -1 : 1)); + + return integer_value; + } + + constexpr operator int32_t() const { + return to_int32_t(); + } + + constexpr operator int64_t() const { + return to_int64_t(); + } + + constexpr operator float() const { + return to_float(); + } + + constexpr operator double() const { + return to_double(); + } + + operator std::string() const { + return to_string(); + } + + friend std::ostream& operator<<(std::ostream& stream, fixed_point_t const& obj) { + return stream << obj.to_string(); + } + + constexpr friend fixed_point_t operator-(fixed_point_t const& obj) { + return -obj.value; + } + + constexpr friend fixed_point_t operator+(fixed_point_t const& obj) { + return +obj.value; + } + + constexpr friend fixed_point_t operator+(fixed_point_t const& lhs, fixed_point_t const& rhs) { + return lhs.value + rhs.value; + } + + constexpr friend fixed_point_t operator+(fixed_point_t const& lhs, int32_t const& rhs) { + return lhs.value + (static_cast<int64_t>(rhs) << PRECISION); + } + + constexpr friend fixed_point_t operator+(int32_t const& lhs, fixed_point_t const& rhs) { + return (static_cast<int64_t>(lhs) << PRECISION) + rhs.value; + } + + constexpr fixed_point_t operator+=(fixed_point_t const& obj) { + value += obj.value; + return *this; + } + + constexpr fixed_point_t operator+=(int32_t const& obj) { + value += (static_cast<int64_t>(obj) << PRECISION); + return *this; + } + + constexpr friend fixed_point_t operator-(fixed_point_t const& lhs, fixed_point_t const& rhs) { + return lhs.value - rhs.value; + } + + constexpr friend fixed_point_t operator-(fixed_point_t const& lhs, int32_t const& rhs) { + return lhs.value - (static_cast<int64_t>(rhs) << PRECISION); + } + + constexpr friend fixed_point_t operator-(int32_t const& lhs, fixed_point_t const& rhs) { + return (static_cast<int64_t>(lhs) << PRECISION) - rhs.value; + } + + constexpr fixed_point_t operator-=(fixed_point_t const& obj) { + value -= obj.value; + return *this; + } + + constexpr fixed_point_t operator-=(int32_t const& obj) { + value -= (static_cast<int64_t>(obj) << PRECISION); + return *this; + } + + constexpr friend fixed_point_t operator*(fixed_point_t const& lhs, fixed_point_t const& rhs) { + return lhs.value * rhs.value >> PRECISION; + } + + constexpr friend fixed_point_t operator*(fixed_point_t const& lhs, int32_t const& rhs) { + return lhs.value * rhs; + } + + constexpr friend fixed_point_t operator*(int32_t const& lhs, fixed_point_t const& rhs) { + return lhs * rhs.value; + } + + constexpr fixed_point_t operator*=(fixed_point_t const& obj) { + value *= obj.value >> PRECISION; + return *this; + } + + constexpr fixed_point_t operator*=(int32_t const& obj) { + value *= obj; + return *this; + } + + constexpr friend fixed_point_t operator/(fixed_point_t const& lhs, fixed_point_t const& rhs) { + return (lhs.value << PRECISION) / rhs.value; + } + + constexpr friend fixed_point_t operator/(fixed_point_t const& lhs, int32_t const& rhs) { + return lhs.value / rhs; + } + + constexpr friend fixed_point_t operator/(int32_t const& lhs, fixed_point_t const& rhs) { + return (static_cast<int64_t>(lhs) << (2 * PRECISION)) / rhs.value; + } + + constexpr fixed_point_t operator/=(fixed_point_t const& obj) { + value = (value << PRECISION) / obj.value; + return *this; + } + + constexpr fixed_point_t operator/=(int32_t const& obj) { + value /= obj; + return *this; + } + + constexpr friend fixed_point_t operator%(fixed_point_t const& lhs, fixed_point_t const& rhs) { + return lhs.value % rhs.value; + } + + constexpr friend fixed_point_t operator%(fixed_point_t const& lhs, int32_t const& rhs) { + return lhs.value % (static_cast<int64_t>(rhs) << PRECISION); + } + + constexpr friend fixed_point_t operator%(int32_t const& lhs, fixed_point_t const& rhs) { + return (static_cast<int64_t>(lhs) << PRECISION) % rhs.value; + } + + constexpr fixed_point_t operator%=(fixed_point_t const& obj) { + value %= obj.value; + return *this; + } + + constexpr fixed_point_t operator%=(int32_t const& obj) { + value %= (static_cast<int64_t>(obj) << PRECISION); + return *this; + } + + constexpr friend bool operator<(fixed_point_t const& lhs, fixed_point_t const& rhs) { + return lhs.value < rhs.value; + } + + constexpr friend bool operator<(fixed_point_t const& lhs, int32_t const& rhs) { + return lhs.value < static_cast<int64_t>(rhs) << PRECISION; + } + + constexpr friend bool operator<(int32_t const& lhs, fixed_point_t const& rhs) { + return static_cast<int64_t>(lhs) << PRECISION < rhs.value; + } + + constexpr friend bool operator<=(fixed_point_t const& lhs, fixed_point_t const& rhs) { + return lhs.value <= rhs.value; + } + + constexpr friend bool operator<=(fixed_point_t const& lhs, int32_t const& rhs) { + return lhs.value <= static_cast<int64_t>(rhs) << PRECISION; + } + + constexpr friend bool operator<=(int32_t const& lhs, fixed_point_t const& rhs) { + return static_cast<int64_t>(lhs) << PRECISION <= rhs.value; + } + + constexpr friend bool operator>(fixed_point_t const& lhs, fixed_point_t const& rhs) { + return lhs.value > rhs.value; + } + + constexpr friend bool operator>(fixed_point_t const& lhs, int32_t const& rhs) { + return lhs.value > static_cast<int64_t>(rhs) << PRECISION; + } + + constexpr friend bool operator>(int32_t const& lhs, fixed_point_t const& rhs) { + return static_cast<int64_t>(lhs) << PRECISION > rhs.value; + } + + constexpr friend bool operator>=(fixed_point_t const& lhs, fixed_point_t const& rhs) { + return lhs.value >= rhs.value; + } + + constexpr friend bool operator>=(fixed_point_t const& lhs, int32_t const& rhs) { + return lhs.value >= static_cast<int64_t>(rhs) << PRECISION; + } + + constexpr friend bool operator>=(int32_t const& lhs, fixed_point_t const& rhs) { + return static_cast<int64_t>(lhs) << PRECISION >= rhs.value; + } + + constexpr friend bool operator==(fixed_point_t const& lhs, fixed_point_t const& rhs) { + return lhs.value == rhs.value; + } + + constexpr friend bool operator==(fixed_point_t const& lhs, int32_t const& rhs) { + return lhs.value == static_cast<int64_t>(rhs) << PRECISION; + } + + constexpr friend bool operator==(int32_t const& lhs, fixed_point_t const& rhs) { + return static_cast<int64_t>(lhs) << PRECISION == rhs.value; + } + + constexpr friend bool operator!=(fixed_point_t const& lhs, fixed_point_t const& rhs) { + return lhs.value != rhs.value; + } + + constexpr friend bool operator!=(fixed_point_t const& lhs, int32_t const& rhs) { + return lhs.value != static_cast<int64_t>(rhs) << PRECISION; + } + + constexpr friend bool operator!=(int32_t const& lhs, fixed_point_t const& rhs) { + return static_cast<int64_t>(lhs) << PRECISION != rhs.value; + } + + private: + int64_t value; + + static constexpr fixed_point_t parse_integer(char const* str, char const* const end, bool* successful) { + int64_t parsed_value = StringUtils::string_to_int64(str, end, successful, 10); + return parse(parsed_value); + } + + static constexpr fixed_point_t parse_fraction(char const* str, char const* end, bool* successful) { + char const* const read_end = str + PRECISION; + if (read_end < end) end = read_end; + uint64_t parsed_value = StringUtils::string_to_uint64(str, end, successful, 10); + while (end++ < read_end) { + parsed_value *= 10; + } + uint64_t decimal = NumberUtils::pow(static_cast<uint64_t>(10), PRECISION); + int64_t ret = 0; + for (int i = PRECISION - 1; i >= 0; --i) { + decimal >>= 1; + if (parsed_value > decimal) { + parsed_value -= decimal; + ret |= 1 << i; + } + } + return ret; + } + }; + + static_assert(sizeof(fixed_point_t) == fixed_point_t::SIZE, "fixed_point_t is not 8 bytes"); +} diff --git a/src/openvic-simulation/types/fixed_point/FixedPointLUT.hpp b/src/openvic-simulation/types/fixed_point/FixedPointLUT.hpp new file mode 100644 index 0000000..466517b --- /dev/null +++ b/src/openvic-simulation/types/fixed_point/FixedPointLUT.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include <array> +#include <cmath> +#include <cstddef> +#include <cstdint> +#include <numbers> +#include <utility> + +namespace OpenVic::FPLUT { + + #include "FixedPointLUT_sin.hpp" + + constexpr int32_t SHIFT = SIN_LUT_PRECISION - SIN_LUT_COUNT_LOG2; + + constexpr int64_t sin(int64_t value) { + int sign = 1; + if (value < 0) { + value = -value; + sign = -1; + } + + int index = value >> SHIFT; + int64_t a = SIN_LUT[index]; + int64_t b = SIN_LUT[index + 1]; + int64_t fraction = (value - (index << SHIFT)) << SIN_LUT_COUNT_LOG2; + int64_t result = a + (((b - a) * fraction) >> SIN_LUT_PRECISION); + return result * sign; + } +} diff --git a/src/openvic-simulation/types/fixed_point/FixedPointLUT_sin.hpp b/src/openvic-simulation/types/fixed_point/FixedPointLUT_sin.hpp new file mode 100644 index 0000000..0c75efe --- /dev/null +++ b/src/openvic-simulation/types/fixed_point/FixedPointLUT_sin.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include <cstdint> + +static constexpr int32_t SIN_LUT_PRECISION = 16; +static constexpr int32_t SIN_LUT_COUNT_LOG2 = 9; + +static constexpr int64_t SIN_LUT[(1 << SIN_LUT_COUNT_LOG2) + 1] = { + 0, 804, 1608, 2412, 3216, 4019, 4821, 5623, 6424, 7224, 8022, 8820, 9616, 10411, 11204, 11996, + 12785, 13573, 14359, 15143, 15924, 16703, 17479, 18253, 19024, 19792, 20557, 21320, 22078, 22834, 23586, 24335, + 25080, 25821, 26558, 27291, 28020, 28745, 29466, 30182, 30893, 31600, 32303, 33000, 33692, 34380, 35062, 35738, + 36410, 37076, 37736, 38391, 39040, 39683, 40320, 40951, 41576, 42194, 42806, 43412, 44011, 44604, 45190, 45769, + 46341, 46906, 47464, 48015, 48559, 49095, 49624, 50146, 50660, 51166, 51665, 52156, 52639, 53114, 53581, 54040, + 54491, 54934, 55368, 55794, 56212, 56621, 57022, 57414, 57798, 58172, 58538, 58896, 59244, 59583, 59914, 60235, + 60547, 60851, 61145, 61429, 61705, 61971, 62228, 62476, 62714, 62943, 63162, 63372, 63572, 63763, 63944, 64115, + 64277, 64429, 64571, 64704, 64827, 64940, 65043, 65137, 65220, 65294, 65358, 65413, 65457, 65492, 65516, 65531, + 65536, 65531, 65516, 65492, 65457, 65413, 65358, 65294, 65220, 65137, 65043, 64940, 64827, 64704, 64571, 64429, + 64277, 64115, 63944, 63763, 63572, 63372, 63162, 62943, 62714, 62476, 62228, 61971, 61705, 61429, 61145, 60851, + 60547, 60235, 59914, 59583, 59244, 58896, 58538, 58172, 57798, 57414, 57022, 56621, 56212, 55794, 55368, 54934, + 54491, 54040, 53581, 53114, 52639, 52156, 51665, 51166, 50660, 50146, 49624, 49095, 48559, 48015, 47464, 46906, + 46341, 45769, 45190, 44604, 44011, 43412, 42806, 42194, 41576, 40951, 40320, 39683, 39040, 38391, 37736, 37076, + 36410, 35738, 35062, 34380, 33692, 33000, 32303, 31600, 30893, 30182, 29466, 28745, 28020, 27291, 26558, 25821, + 25080, 24335, 23586, 22834, 22078, 21320, 20557, 19792, 19024, 18253, 17479, 16703, 15924, 15143, 14359, 13573, + 12785, 11996, 11204, 10411, 9616, 8820, 8022, 7224, 6424, 5623, 4821, 4019, 3216, 2412, 1608, 804, + 0, -804, -1608, -2412, -3216, -4019, -4821, -5623, -6424, -7224, -8022, -8820, -9616, -10411, -11204, -11996, + -12785, -13573, -14359, -15143, -15924, -16703, -17479, -18253, -19024, -19792, -20557, -21320, -22078, -22834, -23586, -24335, + -25080, -25821, -26558, -27291, -28020, -28745, -29466, -30182, -30893, -31600, -32303, -33000, -33692, -34380, -35062, -35738, + -36410, -37076, -37736, -38391, -39040, -39683, -40320, -40951, -41576, -42194, -42806, -43412, -44011, -44604, -45190, -45769, + -46341, -46906, -47464, -48015, -48559, -49095, -49624, -50146, -50660, -51166, -51665, -52156, -52639, -53114, -53581, -54040, + -54491, -54934, -55368, -55794, -56212, -56621, -57022, -57414, -57798, -58172, -58538, -58896, -59244, -59583, -59914, -60235, + -60547, -60851, -61145, -61429, -61705, -61971, -62228, -62476, -62714, -62943, -63162, -63372, -63572, -63763, -63944, -64115, + -64277, -64429, -64571, -64704, -64827, -64940, -65043, -65137, -65220, -65294, -65358, -65413, -65457, -65492, -65516, -65531, + -65536, -65531, -65516, -65492, -65457, -65413, -65358, -65294, -65220, -65137, -65043, -64940, -64827, -64704, -64571, -64429, + -64277, -64115, -63944, -63763, -63572, -63372, -63162, -62943, -62714, -62476, -62228, -61971, -61705, -61429, -61145, -60851, + -60547, -60235, -59914, -59583, -59244, -58896, -58538, -58172, -57798, -57414, -57022, -56621, -56212, -55794, -55368, -54934, + -54491, -54040, -53581, -53114, -52639, -52156, -51665, -51166, -50660, -50146, -49624, -49095, -48559, -48015, -47464, -46906, + -46341, -45769, -45190, -44604, -44011, -43412, -42806, -42194, -41576, -40951, -40320, -39683, -39040, -38391, -37736, -37076, + -36410, -35738, -35062, -34380, -33692, -33000, -32303, -31600, -30893, -30182, -29466, -28745, -28020, -27291, -26558, -25821, + -25080, -24335, -23586, -22834, -22078, -21320, -20557, -19792, -19024, -18253, -17479, -16703, -15924, -15143, -14359, -13573, + -12785, -11996, -11204, -10411, -9616, -8820, -8022, -7224, -6424, -5623, -4821, -4019, -3216, -2412, -1608, -804, + 0 +}; diff --git a/src/openvic-simulation/types/fixed_point/FixedPointMath.hpp b/src/openvic-simulation/types/fixed_point/FixedPointMath.hpp new file mode 100644 index 0000000..c0b5f42 --- /dev/null +++ b/src/openvic-simulation/types/fixed_point/FixedPointMath.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "FixedPoint.hpp" + +namespace OpenVic::FPMath { + constexpr fixed_point_t sin(fixed_point_t number) { + number %= fixed_point_t::pi2(); + number *= fixed_point_t::one_div_pi2(); + return FPLUT::sin(number.get_raw_value()); + } +} diff --git a/src/openvic-simulation/types/fixed_point/lut_generator/lut_generator.py b/src/openvic-simulation/types/fixed_point/lut_generator/lut_generator.py new file mode 100644 index 0000000..8ae7a32 --- /dev/null +++ b/src/openvic-simulation/types/fixed_point/lut_generator/lut_generator.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +from math import pi, sin +from argparse import ArgumentParser +from sys import exit + +def generate_sin_lut(precision : int, count_log2 : int): + one = 1 << precision + count = 1 << count_log2 + + SinLut = [] + + for i in range(count): + angle = 2 * pi * i / count + + sin_value = sin(angle) + moved_sin = sin_value * one + rounded_sin = int(moved_sin + 0.5) if moved_sin > 0 else int(moved_sin - 0.5) + SinLut.append(rounded_sin) + + SinLut.append(SinLut[0]) + + output = [ + "#pragma once", + "", + "#include <cstdint>", + "", + f"static constexpr int32_t SIN_LUT_PRECISION = {precision};", + f"static constexpr int32_t SIN_LUT_COUNT_LOG2 = {count_log2};", + "", + "static constexpr int64_t SIN_LUT[(1 << SIN_LUT_COUNT_LOG2) + 1] = {" + ] + + VALS_PER_LINE = 16 + + lines = [SinLut[i : i + VALS_PER_LINE] for i in range(0, len(SinLut), VALS_PER_LINE)] + + for line in lines: + output.append("\t" + ", ".join(str(value) for value in line) + ",") + + output[-1] = output[-1][:-1] # Remove the last comma + output += ["};", ""] + + cpp_code = "\n".join(output) + + with open(f"FixedPointLUT_sin.hpp", "w", newline="\n") as file: + file.write(cpp_code) + +PRECISION = 16 +COUNT = 9 + +if __name__ == "__main__": + + parser = ArgumentParser(prog="Fixed Point LUT Generator", description="Fixed-Point number Look-Up Table generator") + parser.add_argument("-p", "--precision", type=int, default=PRECISION, choices=range(1, 65), help="The number of bits after the point (fractional bits)") + parser.add_argument("-c", "--count", type=int, default=COUNT, choices=range(1, 65), help="The base 2 log of the number of values in the look-up table (must be <= precision)") + args = parser.parse_args() + + if args.precision < args.count: + print("ERROR: invalid count ", args.count, " - can't be greater than precision (", args.precision, ")") + exit(-1) + else: + generate_sin_lut(args.precision, args.count) + exit(0) diff --git a/src/openvic-simulation/utility/BMP.cpp b/src/openvic-simulation/utility/BMP.cpp new file mode 100644 index 0000000..531870b --- /dev/null +++ b/src/openvic-simulation/utility/BMP.cpp @@ -0,0 +1,163 @@ +#include "BMP.hpp" + +#include <cstring> +#include <set> + +#include "openvic-simulation/utility/Logger.hpp" + +using namespace OpenVic; + +BMP::~BMP() { + close(); +} + +bool BMP::open(char const* filepath) { + reset(); + errno = 0; + file = fopen(filepath, "rb"); + if (file == nullptr || errno != 0) { + Logger::error("Failed to open BMP file \"", filepath, "\" (errno = ", errno, ")"); + file = nullptr; + return false; + } + return true; +} + +bool BMP::read_header() { + if (header_validated) { + Logger::error("BMP header already validated!"); + return false; + } + if (file == nullptr) { + Logger::error("Cannot read BMP header before opening a file"); + return false; + } + if (fseek(file, 0, SEEK_SET) != 0) { + Logger::error("Failed to move to the beginning of the BMP file!"); + return false; + } + if (fread(&header, sizeof(header), 1, file) != 1) { + Logger::error("Failed to read BMP header!"); + return false; + } + + header_validated = true; + + // Validate constants + static constexpr uint16_t BMP_SIGNATURE = 0x4d42; + if (header.signature != BMP_SIGNATURE) { + Logger::error("Invalid BMP signature: ", header.signature, " (must be ", BMP_SIGNATURE, ")"); + header_validated = false; + } + static constexpr uint32_t DIB_HEADER_SIZE = 40; + if (header.dib_header_size != DIB_HEADER_SIZE) { + Logger::error("Invalid BMP DIB header size: ", header.dib_header_size, " (must be ", DIB_HEADER_SIZE, ")"); + header_validated = false; + } + static constexpr uint16_t NUM_PLANES = 1; + if (header.num_planes != NUM_PLANES) { + Logger::error("Invalid BMP plane count: ", header.num_planes, " (must be ", NUM_PLANES, ")"); + header_validated = false; + } + static constexpr uint16_t COMPRESSION = 0; // Only support uncompressed BMPs + if (header.compression != COMPRESSION) { + Logger::error("Invalid BMP compression method: ", header.compression, " (must be ", COMPRESSION, ")"); + header_validated = false; + } + + // Validate sizes and dimensions + // TODO - image_size_bytes can be 0 for non-compressed BMPs + if (header.file_size != header.offset + header.image_size_bytes) { + Logger::error("Invalid BMP memory sizes: file size = ", header.file_size, " != ", header.offset + header.image_size_bytes, + " = ", header.offset, " + ", header.image_size_bytes, " = image data offset + image data size"); + header_validated = false; + } + // TODO - support negative widths (i.e. horizontal flip) + if (header.width_px <= 0) { + Logger::error("Invalid BMP width: ", header.width_px, " (must be positive)"); + header_validated = false; + } + // TODO - support negative heights (i.e. vertical flip) + if (header.height_px <= 0) { + Logger::error("Invalid BMP height: ", header.height_px, " (must be positive)"); + header_validated = false; + } + // TODO - validate x_resolution_ppm + // TODO - validate y_resolution_ppm + + // Validate colours +#define VALID_BITS_PER_PIXEL 1, 2, 4, 8, 16, 24, 32 +#define STR(x) #x + static const std::set<uint16_t> BITS_PER_PIXEL { VALID_BITS_PER_PIXEL }; + if (!BITS_PER_PIXEL.contains(header.bits_per_pixel)) { + Logger::error("Invalid BMP bits per pixel: ", header.bits_per_pixel, " (must be one of " STR(VALID_BITS_PER_PIXEL) ")"); + header_validated = false; + } +#undef VALID_BITS_PER_PIXEL +#undef STR + static constexpr uint16_t PALETTE_BITS_PER_PIXEL_LIMIT = 8; + if (header.num_colours != 0 && header.bits_per_pixel > PALETTE_BITS_PER_PIXEL_LIMIT) { + Logger::error("Invalid BMP palette size: ", header.num_colours, " (should be 0 as bits per pixel is ", header.bits_per_pixel, " > 8)"); + header_validated = false; + } + // TODO - validate important_colours + + palette_size = header.bits_per_pixel > PALETTE_BITS_PER_PIXEL_LIMIT ? 0 + // Use header.num_colours if it's greater than 0 and at most 1 << header.bits_per_pixel + : 0 < header.num_colours && header.num_colours - 1 >> header.bits_per_pixel == 0 ? header.num_colours + : 1 << header.bits_per_pixel; + + const uint32_t expected_offset = palette_size * PALETTE_COLOUR_SIZE + sizeof(header); + if (header.offset != expected_offset) { + Logger::error("Invalid BMP image data offset: ", header.offset, " (should be ", expected_offset, ")"); + header_validated = false; + } + + return header_validated; +} + +bool BMP::read_palette() { + if (file == nullptr) { + Logger::error("Cannot read BMP palette before opening a file"); + return false; + } + if (!header_validated) { + Logger::error("Cannot read palette before BMP header is validated!"); + return false; + } + if (palette_size == 0) { + Logger::error("Cannot read BMP palette - header indicates this file doesn't have one"); + return false; + } + if (fseek(file, sizeof(header), SEEK_SET) != 0) { + Logger::error("Failed to move to the palette in the BMP file!"); + return false; + } + palette.resize(palette_size); + if (fread(palette.data(), palette_size * PALETTE_COLOUR_SIZE, 1, file) != 1) { + Logger::error("Failed to read BMP header!"); + palette.clear(); + return false; + } + return true; +} + +void BMP::close() { + if (file != nullptr) { + if (fclose(file) != 0) + Logger::error("Failed to close BMP!"); + file = nullptr; + } +} + +void BMP::reset() { + close(); + memset(&header, 0, sizeof(header)); + header_validated = false; + palette_size = 0; + palette.clear(); +} + +std::vector<colour_t> const& BMP::get_palette() const { + return palette; +} diff --git a/src/openvic-simulation/utility/BMP.hpp b/src/openvic-simulation/utility/BMP.hpp new file mode 100644 index 0000000..f04b41a --- /dev/null +++ b/src/openvic-simulation/utility/BMP.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include <cstdio> +#include <vector> + +#include "openvic-simulation/types/Colour.hpp" + +namespace OpenVic { + class BMP { +#pragma pack(push) +#pragma pack(1) + struct header_t { + uint16_t signature; // Signature: 0x4d42 (or 'B' 'M') + uint32_t file_size; // File size in bytes + uint16_t reserved1; // Not used + uint16_t reserved2; // Not used + uint32_t offset; // Offset to image data in bytes from beginning of file + uint32_t dib_header_size; // DIB header size in bytes + int32_t width_px; // Width of the image + int32_t height_px; // Height of image + uint16_t num_planes; // Number of colour planes + uint16_t bits_per_pixel; // Bits per pixel + uint32_t compression; // Compression type + uint32_t image_size_bytes; // Image size in bytes + int32_t x_resolution_ppm; // Pixels per meter + int32_t y_resolution_ppm; // Pixels per meter + uint32_t num_colours; // Number of colours + uint32_t important_colours; // Important colours + } header; +#pragma pack(pop) + + FILE* file = nullptr; + bool header_validated = false; + uint32_t palette_size = 0; + std::vector<colour_t> palette; + + public: + static constexpr uint32_t PALETTE_COLOUR_SIZE = sizeof(colour_t); + + BMP() = default; + ~BMP(); + + bool open(char const* filepath); + bool read_header(); + bool read_palette(); + void close(); + void reset(); + + std::vector<colour_t> const& get_palette() const; + }; +} diff --git a/src/openvic-simulation/utility/Logger.cpp b/src/openvic-simulation/utility/Logger.cpp new file mode 100644 index 0000000..bf9a67c --- /dev/null +++ b/src/openvic-simulation/utility/Logger.cpp @@ -0,0 +1,20 @@ +#include "Logger.hpp" + +#include <iostream> + +using namespace OpenVic; + +Logger::log_func_t Logger::info_func {}; +Logger::log_queue_t Logger::info_queue {}; +Logger::log_func_t Logger::error_func {}; +Logger::log_queue_t Logger::error_queue {}; + +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; +} diff --git a/src/openvic-simulation/utility/Logger.hpp b/src/openvic-simulation/utility/Logger.hpp new file mode 100644 index 0000000..417aba7 --- /dev/null +++ b/src/openvic-simulation/utility/Logger.hpp @@ -0,0 +1,87 @@ +#pragma once + +#include <functional> +#include <queue> +#include <sstream> +#ifdef __cpp_lib_source_location +#include <source_location> +#endif + +namespace OpenVic { + +#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&&)>; + using log_queue_t = std::queue<std::string>; + +#ifdef __cpp_lib_source_location + using source_location = std::source_location; +#else + using source_location = OpenVic::source_location; +#endif + + static char const* get_filename(char const* filepath); + + template<typename... Ts> + struct log { + log(log_func_t log_func, log_queue_t& log_queue, Ts&&... ts, source_location const& location) { + std::stringstream stream; + stream << "\n" << get_filename(location.file_name()) << "(" + << location.line() << ") `" << location.function_name() << "`: "; + ((stream << std::forward<Ts>(ts)), ...); + stream << std::endl; + log_queue.push(stream.str()); + if (log_func) { + do { + log_func(std::move(log_queue.front())); + log_queue.pop(); + } while (!log_queue.empty()); + } + } + }; + +#define LOG_FUNC(name) \ + private: \ + static log_func_t name##_func; \ + static log_queue_t name##_queue; \ + public: \ + static void set_##name##_func(log_func_t log_func) { \ + name##_func = log_func; \ + } \ + template <typename... Ts> \ + struct name { \ + name(Ts&&... ts, source_location const& location = source_location::current()) { \ + log<Ts...>{ name##_func, name##_queue, std::forward<Ts>(ts)..., location }; \ + } \ + }; \ + template <typename... Ts> \ + name(Ts&&...) -> name<Ts...>; + + LOG_FUNC(info) + LOG_FUNC(error) + +#undef LOG_FUNC + }; +} diff --git a/src/openvic-simulation/utility/NumberUtils.hpp b/src/openvic-simulation/utility/NumberUtils.hpp new file mode 100644 index 0000000..6211772 --- /dev/null +++ b/src/openvic-simulation/utility/NumberUtils.hpp @@ -0,0 +1,27 @@ +#include <cstdint> + +namespace OpenVic::NumberUtils { + constexpr int64_t round_to_int64(float num) { + return (num > 0.0f) ? (num + 0.5f) : (num - 0.5f); + } + + constexpr int64_t round_to_int64(double num) { + return (num > 0.0) ? (num + 0.5) : (num - 0.5); + } + + constexpr uint64_t pow(uint64_t base, size_t exponent) { + uint64_t ret = 1; + while (exponent-- > 0) { + ret *= base; + } + return ret; + } + + constexpr int64_t pow(int64_t base, size_t exponent) { + int64_t ret = 1; + while (exponent-- > 0) { + ret *= base; + } + return ret; + } +} diff --git a/src/openvic-simulation/utility/StringUtils.hpp b/src/openvic-simulation/utility/StringUtils.hpp new file mode 100644 index 0000000..5d6001c --- /dev/null +++ b/src/openvic-simulation/utility/StringUtils.hpp @@ -0,0 +1,127 @@ +#include <cstdint> +#include <limits> +#include <string_view> + +namespace OpenVic::StringUtils { + /* The constexpr function 'string_to_uint64' will convert a string into a uint64_t integer value. + * The function takes four parameters: the input string (as a pair of pointers marking the start and + * end of the string), a bool pointer for reporting success, and the base for numerical conversion. + * The base parameter defaults to 10 (decimal), but it can be any value between 2 and 36. If the base + * given is 0, it will be set to 16 if the string starts with "0x" or "0X", otherwise 8 if the string + * still starts with "0", otherwise 10. The success bool pointer parameter is used to report whether + * or not conversion was successful. It can be nullptr if this information is not needed. + */ + constexpr uint64_t string_to_uint64(char const* str, const char* const end, bool* successful = nullptr, int base = 10) { + if (successful != nullptr) *successful = false; + + // Base value should be between 2 and 36. If it's not, return 0 as an invalid case. + if (str == nullptr || end <= str || base < 0 || base == 1 || base > 36) + return 0; + + // The result of the conversion will be stored in this variable. + uint64_t result = 0; + + // If base is zero, base is determined by the string prefix. + if (base == 0) { + if (*str == '0') { + if (str + 1 != end && (str[1] == 'x' || str[1] == 'X')) { + base = 16; // Hexadecimal. + str += 2; // Skip '0x' or '0X' + if (str == end) return 0; + } else { + base = 8; // Octal. + } + } else { + base = 10; // Decimal. + } + } else if (base == 16) { + // If base is 16 and string starts with '0x' or '0X', skip these characters. + if (*str == '0' && str + 1 != end && (str[1] == 'x' || str[1] == 'X')) { + str += 2; + if (str == end) return 0; + } + } + + // Convert the number in the string. + for (; str != end; ++str) { + int digit; + if (*str >= '0' && *str <= '9') { + digit = *str - '0'; // Calculate digit value for '0'-'9'. + } else if (*str >= 'a' && *str <= 'z') { + digit = *str - 'a' + 10; // Calculate digit value for 'a'-'z'. + } else if (*str >= 'A' && *str <= 'Z') { + digit = *str - 'A' + 10; // Calculate digit value for 'A'-'Z'. + } else { + break; // Stop conversion if current character is not a digit. + } + + if (digit >= base) { + break; // Stop conversion if current digit is greater than or equal to the base. + } + + // Check for overflow on multiplication + if (result > std::numeric_limits<uint64_t>::max() / base) { + return std::numeric_limits<uint64_t>::max(); + } + + result *= base; + + // Check for overflow on addition + if (result > std::numeric_limits<uint64_t>::max() - digit) { + return std::numeric_limits<uint64_t>::max(); + } + + result += digit; + } + + // If successful is not null and the entire string was parsed, + // set *successful to true (if not it is already false). + if (successful != nullptr && str == end) *successful = true; + + return result; + } + + constexpr uint64_t string_to_uint64(char const* str, size_t length, bool* successful = nullptr, int base = 10) { + return string_to_uint64(str, str + length, successful, base); + } + + inline uint64_t string_to_uint64(const std::string_view str, bool* successful = nullptr, int base = 10) { + return string_to_uint64(str.data(), str.length(), successful, base); + } + + constexpr int64_t string_to_int64(char const* str, const char* const end, bool* successful = nullptr, int base = 10) { + if (successful != nullptr) *successful = false; + + if (str == nullptr || end <= str) return 0; + + // This flag will be set if the number is negative. + bool is_negative = false; + + // Check if there is a sign character. + if (*str == '+' || *str == '-') { + if (*str == '-') + is_negative = true; + ++str; + if (str == end) return 0; + } + + const uint64_t result = string_to_uint64(str, end, successful, base); + if (!is_negative) { + if (result >= std::numeric_limits<int64_t>::max()) + return std::numeric_limits<int64_t>::max(); + return result; + } else { + if (result > std::numeric_limits<int64_t>::max()) + return std::numeric_limits<int64_t>::min(); + return -result; + } + } + + constexpr int64_t string_to_int64(char const* str, size_t length, bool* successful = nullptr, int base = 10) { + return string_to_int64(str, str + length, successful, base); + } + + inline int64_t string_to_int64(const std::string_view str, bool* successful = nullptr, int base = 10) { + return string_to_int64(str.data(), str.length(), successful, base); + } +} |