Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add map projections #335

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions extension/doc_classes/MapItemSingleton.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,78 @@
<brief_description>
</brief_description>
<description>
This singleton provides methods of optaining [code]billboard[/code] and [code]projection[/code] type graphics objects. It also provides methods for getting the proper location of these objects on the map.
</description>
<tutorials>
</tutorials>
<methods>
<method name="get_billboards" qualifiers="const">
<return type="Dictionary[]" />
<description>
Returns an array of Dictionaries. Each dictionary contains the keys [code]name[/code] [StringName], [code]texture[/code] [StringName], [code]scale[/code] [float], [code]noOfFrames[/code] [int] used to make a billboard object. [code]texture[/code] is a [StringName] path to the billboard texture. [code]noOfFrames[/code] is a [float] scaling factor. [code]noOfFrames[/code] is an [int] specifying how many icons are in the texture image.
</description>
</method>
<method name="get_capital_positions" qualifiers="const">
<return type="PackedVector2Array" />
<description>
Returns an array of the positions of country capital billboards for all existing countries. The capital billboard position is the [code]city[/code] property of a province which is a capital in the game defines.
</description>
</method>
<method name="get_clicked_port_province_index" qualifiers="const">
<return type="int" />
<param index="0" name="position" type="Vector2" />
<description>
Searches the mouse map coordinate for any port within a radius. Returns the province index of a found port, or [code]0[/code] if no port was found within the radius of the click.
</description>
</method>
<method name="get_crime_icons" qualifiers="const">
<return type="PackedByteArray" />
<description>
Returns an array of icon indices to use on the crimes texture to get the appropriate crime icons for every land province. An index of [code]0[/code] indicates there is no crime for that province.
</description>
</method>
<method name="get_max_capital_count" qualifiers="const">
<return type="int" />
<description>
Returns the size of the number of defined countries. This is the maximum possible number of capital billboards the game could have to display.
</description>
</method>
<method name="get_national_focus_icons" qualifiers="const">
<return type="PackedByteArray" />
<description>
TODO: WIP Function awaiting implementation of national focuses. Returns an array of icon indices used with the national focus icons texture for every province. For provinces which aren't state capitals, or don't have have a national focus applied, this will be [code]0[/code].
</description>
</method>
<method name="get_port_position_by_province_index" qualifiers="const">
<return type="Vector2" />
<param index="0" name="index" type="int" />
<description>
Given a province [param index], returns the position of the province's port. Emits an error and returns [code]0,0[/code] if the province does not have a port.
</description>
</method>
<method name="get_projections" qualifiers="const">
<return type="Dictionary[]" />
<description>
Returns an array of Dictionaries. Each dictionary contains the keys [code]name[/code] [StringName], [code]texture[/code] [StringName], [code]size[/code] [float], [code]spin[/code] [float], [code]expanding[/code] [float], [code]duration[/code] [float], [code]additative[/code] [bool].
</description>
</method>
<method name="get_province_positions" qualifiers="const">
<return type="PackedVector2Array" />
<description>
Returns an array of billboard positions for every land province. This corresponds to a province's [code]city[/code] define.
</description>
</method>
<method name="get_rgo_icons" qualifiers="const">
<return type="PackedByteArray" />
<description>
Returns an array of icon indices used with the trade goods (RGO) icons texture for every land province. If the province the province does not have an RGO, it will be 0.
</description>
</method>
<method name="get_unit_position_by_province_index" qualifiers="const">
<return type="Vector2" />
<param index="0" name="index" type="int" />
<description>
Given a province [param index], returns the province's unit position.
</description>
</method>
</methods>
Expand Down
150 changes: 135 additions & 15 deletions extension/src/openvic-extension/singletons/MapItemSingleton.cpp
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
#include "MapItemSingleton.hpp"
#include <string_view>

#include "godot_cpp/core/error_macros.hpp"
#include "godot_cpp/variant/packed_int32_array.hpp"
#include "godot_cpp/variant/packed_vector2_array.hpp"
#include "godot_cpp/variant/typed_array.hpp"
#include "godot_cpp/variant/utility_functions.hpp"
#include "godot_cpp/variant/vector2.hpp"
#include "openvic-extension/singletons/GameSingleton.hpp"
#include "openvic-extension/utility/ClassBindings.hpp"
#include "openvic-extension/utility/Utilities.hpp"
#include "openvic-simulation/DefinitionManager.hpp"
#include "openvic-simulation/country/CountryDefinition.hpp"
#include "openvic-simulation/country/CountryInstance.hpp"
#include "openvic-simulation/DefinitionManager.hpp"
#include "openvic-simulation/economy/BuildingType.hpp"
#include "openvic-simulation/interface/GFXObject.hpp"
#include "openvic-simulation/map/ProvinceDefinition.hpp"
#include "openvic-simulation/map/ProvinceInstance.hpp"
Expand All @@ -29,6 +30,11 @@ void MapItemSingleton::_bind_methods() {
OV_BIND_METHOD(MapItemSingleton::get_crime_icons);
OV_BIND_METHOD(MapItemSingleton::get_rgo_icons);
OV_BIND_METHOD(MapItemSingleton::get_national_focus_icons);
OV_BIND_METHOD(MapItemSingleton::get_projections);
OV_BIND_METHOD(MapItemSingleton::get_unit_position_by_province_index,{"index"});
OV_BIND_METHOD(MapItemSingleton::get_port_position_by_province_index,{"index"});
OV_BIND_METHOD(MapItemSingleton::get_clicked_port_province_index, {"position"});

}

MapItemSingleton* MapItemSingleton::get_singleton() {
Expand Down Expand Up @@ -63,27 +69,21 @@ GFX::Billboard const* MapItemSingleton::get_billboard(std::string_view name, boo
}

// repackage the billboard object into a godot dictionary for the Billboard manager to work with
bool MapItemSingleton::add_billboard_dict(std::string_view name, TypedArray<Dictionary>& billboard_dict_array) const {
void MapItemSingleton::add_billboard_dict(GFX::Billboard const& billboard, TypedArray<Dictionary>& billboard_dict_array) const {

static const StringName name_key = "name";
static const StringName texture_key = "texture";
static const StringName scale_key = "scale";
static const StringName noOfFrames_key = "noFrames";

GFX::Billboard const* billboard = get_billboard(name, false);

ERR_FAIL_NULL_V_MSG(billboard, false, vformat("Failed to find billboard \"%s\"", Utilities::std_to_godot_string(name)));

Dictionary dict;

dict[name_key] = Utilities::std_to_godot_string(billboard->get_name());
dict[texture_key] = Utilities::std_to_godot_string(billboard->get_texture_file());
dict[scale_key] = billboard->get_scale().to_float();
dict[noOfFrames_key] = billboard->get_no_of_frames();
dict[name_key] = Utilities::std_to_godot_string(billboard.get_name());
dict[texture_key] = Utilities::std_to_godot_string(billboard.get_texture_file());
dict[scale_key] = billboard.get_scale().to_float();
dict[noOfFrames_key] = billboard.get_no_of_frames();

billboard_dict_array.push_back(dict);

return true;
}

//get an array of all the billboard dictionaries
Expand All @@ -94,8 +94,64 @@ TypedArray<Dictionary> MapItemSingleton::get_billboards() const {
TypedArray<Dictionary> ret;

for (std::unique_ptr<GFX::Object> const& obj : game_singleton->get_definition_manager().get_ui_manager().get_objects()) {
if (obj->is_type<GFX::Billboard>()) {
add_billboard_dict(obj->get_name(), ret);
GFX::Billboard const* billboard = obj->cast_to<GFX::Billboard>();
if (billboard != nullptr) {
add_billboard_dict(*billboard, ret);
}
}

return ret;
}


GFX::Projection const* MapItemSingleton::get_projection(std::string_view name, bool error_on_fail) const {
GameSingleton const* game_singleton = GameSingleton::get_singleton();
ERR_FAIL_NULL_V(game_singleton, nullptr);
Comment on lines +108 to +109
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For future reference, most of the time we don't need to worry about singletons being null, that can't happen without something going catastophically wrong during startup, so might as well just use them directly if we only need them once or store them in references if we need them multiple times.


GFX::Projection const* projection =
game_singleton->get_definition_manager().get_ui_manager().get_cast_object_by_identifier<GFX::Projection>(name);

if (error_on_fail) {
ERR_FAIL_NULL_V_MSG(
projection, nullptr, vformat("Failed to find projection \"%s\"", Utilities::std_to_godot_string(name))
);
}

return projection;
}

void MapItemSingleton::add_projection_dict(GFX::Projection const& projection, TypedArray<Dictionary>& projection_dict_array) const {
static const StringName name_key = "name";
static const StringName texture_key = "texture";
static const StringName size_key = "size";
static const StringName spin_key = "spin";
static const StringName expanding_key = "expanding";
static const StringName duration_key = "duration";
static const StringName additative_key = "additative";

Dictionary dict;

dict[name_key] = Utilities::std_to_godot_string(projection.get_name());
dict[texture_key] = Utilities::std_to_godot_string(projection.get_texture_file());
dict[size_key] = projection.get_size().to_float();
dict[spin_key] = projection.get_spin().to_float();
dict[expanding_key] = projection.get_expanding().to_float();
dict[duration_key] = projection.get_duration().to_float();
dict[additative_key] = projection.get_additative();

projection_dict_array.push_back(dict);
}

TypedArray<Dictionary> MapItemSingleton::get_projections() const {
GameSingleton const* game_singleton = GameSingleton::get_singleton();
ERR_FAIL_NULL_V(game_singleton, {});

TypedArray<Dictionary> ret;

for (std::unique_ptr<GFX::Object> const& obj : game_singleton->get_definition_manager().get_ui_manager().get_objects()) {
GFX::Projection const* projection = obj->cast_to<GFX::Projection>();
if (projection != nullptr) {
add_projection_dict(*projection, ret);
}
}

Expand Down Expand Up @@ -255,3 +311,67 @@ PackedByteArray MapItemSingleton::get_national_focus_icons() const {

return icons;
}


Vector2 MapItemSingleton::get_unit_position_by_province_index(int32_t province_index) const {
GameSingleton const* game_singleton = GameSingleton::get_singleton();
ERR_FAIL_NULL_V(game_singleton, {});

ProvinceDefinition const* province = game_singleton->get_definition_manager().get_map_definition()
.get_province_definition_by_index(province_index);
ERR_FAIL_NULL_V_MSG(province, {}, vformat("Cannot get unit position - invalid province index: %d", province_index));

return Utilities::to_godot_fvec2(province->get_unit_position())
/ GameSingleton::get_singleton()->get_map_dims();
Comment on lines +324 to +325
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return Utilities::to_godot_fvec2(province->get_unit_position())
/ GameSingleton::get_singleton()->get_map_dims();
return game_singleton->normalise_map_position(province->get_unit_position());

We now have a helper function for converting fvec2_ts to map-normalised Vector2s

}

Vector2 MapItemSingleton::get_port_position_by_province_index(int32_t province_index) const {
GameSingleton const* game_singleton = GameSingleton::get_singleton();
ERR_FAIL_NULL_V(game_singleton, {});

ProvinceDefinition const* province = game_singleton->get_definition_manager().get_map_definition()
.get_province_definition_by_index(province_index);
ERR_FAIL_NULL_V_MSG(province, {}, vformat("Cannot get port position - invalid province index: %d", province_index));
ERR_FAIL_COND_V_MSG(!province->has_port(), {},vformat("Cannot get port position - invalid province index: %d", province_index) );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has the same error message as the province index not referring to a real province, maybe specify that it's invalid due to lacking a port rather than not being a province at all?


BuildingType const* port_building_type = game_singleton->get_definition_manager().get_economy_manager().get_building_type_manager().get_port_building_type();
fvec2_t const* port_position = province->get_building_position(port_building_type);

return Utilities::to_godot_fvec2(*port_position) / GameSingleton::get_singleton()->get_map_dims();
Comment on lines +338 to +340
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't need a null check as province->has_port() is only set if the province has a port position, but it would be good to add a comment explaining this as it isn't obvious without familiarity with the map SIM code. Also this again can use the normalise_map_position function

}

const static float port_radius = 0.0006; //how close we have to click for a detection
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const static float port_radius = 0.0006; //how close we have to click for a detection
using namespace OpenVic::Utilities::literals;
static constexpr real_t port_radius = 0.0006_real; //how close we have to click for a detection
  • constexpr compile time constant
  • Might as well use Godot's real_t which this is compared to later on (in practice it's float but better to be safe than sorry)
  • We can use the fancy _real literal operator Spartan made 😄


//Searches provinces near the one clicked and finds
int32_t MapItemSingleton::get_clicked_port_province_index(Vector2 click_position) const {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is very nice, probably faster than doing full bounding box collision detection. We might want to vary the detection radius based on the dimensions of the port model, but custom port models are fairly rare in mods so not a serious priority.
I'm trying to think of scenarios this could fail in, but it would require two non-adjacent provinces (so with a third province in between them) where the model in one province spills over into the non-adjacent province. All the ways I can imagine this involve very weird province shapes and/or tiny/insignificant protrusions of pixels where this wouldn't detect the nearby port, we should still experiment to see if this could be a serious problem but for now I'm happy with this algorithm as-is.

GameSingleton const* game_singleton = GameSingleton::get_singleton();
ERR_FAIL_NULL_V(game_singleton, {});

int32_t initial_province_index = game_singleton->get_province_index_from_uv_coords(click_position);

ProvinceDefinition const* province = game_singleton->get_definition_manager().get_map_definition()
.get_province_definition_by_index(initial_province_index);
ERR_FAIL_NULL_V_MSG(province, {}, vformat("Cannot get port position - invalid province index: %d", initial_province_index));

BuildingType const* port_building_type = game_singleton->get_definition_manager().get_economy_manager().get_building_type_manager().get_port_building_type();

if(province->has_port()){
Vector2 port_position = Utilities::to_godot_fvec2(*province->get_building_position(port_building_type)) / GameSingleton::get_singleton()->get_map_dims();
if(click_position.distance_to(port_position) <= port_radius){
return province->get_index();
}
}
else if(province->is_water()){
// search the adjacent provinces for ones with ports
for(ProvinceDefinition::adjacency_t const& adjacency : province->get_adjacencies()) {
ProvinceDefinition const* adjacent_province = adjacency.get_to();
if(!adjacent_province->has_port()) { continue; } // skip provinces without ports (ie. other water provinces)
Vector2 port_position = Utilities::to_godot_fvec2(*adjacent_province->get_building_position(port_building_type)) / GameSingleton::get_singleton()->get_map_dims();
if(click_position.distance_to(port_position) <= port_radius){
return adjacent_province->get_index();
}
}
}

return 0;
}
14 changes: 11 additions & 3 deletions extension/src/openvic-extension/singletons/MapItemSingleton.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
#include <openvic-simulation/interface/GFXObject.hpp>
#include <openvic-simulation/types/OrderedContainers.hpp>

//billboards, projections, and progress bar
//for now though, only billboards
//billboards, projections, and progress bar (no progress bar yet)

namespace OpenVic {
class MapItemSingleton : public godot::Object {
Expand All @@ -25,14 +24,23 @@ namespace OpenVic {

private:
GFX::Billboard const* get_billboard(std::string_view name, bool error_on_fail = true) const;
bool add_billboard_dict(std::string_view name, godot::TypedArray<godot::Dictionary>& billboard_dict_array) const;
void add_billboard_dict(GFX::Billboard const& billboard, godot::TypedArray<godot::Dictionary>& billboard_dict_array) const;
godot::TypedArray<godot::Dictionary> get_billboards() const;

GFX::Projection const* get_projection(std::string_view name, bool error_on_fail = true) const;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function and get_billboard aren't used anywhere, they might be useful in the future but if not we should think about removing them.

Also it occurs to me that all these functions could be made static, the only changes needed would be:

  • adding static to their declarations here
  • removing const from the end of their declarations here and from their cpp implementations
  • changing _bind_methods to use OV_BIND_SMETHOD (and dropping the MapItemSingleton:: as it's not nececssary for static methods for some reason, probably because they don't implicitly take in this as an argument)

void add_projection_dict(GFX::Projection const& projection, godot::TypedArray<godot::Dictionary>& projection_dict_array) const;
godot::TypedArray<godot::Dictionary> get_projections() const;

godot::PackedVector2Array get_province_positions() const;
int32_t get_max_capital_count() const;
godot::PackedVector2Array get_capital_positions() const;

godot::PackedByteArray get_crime_icons() const;
godot::PackedByteArray get_rgo_icons() const;
godot::PackedByteArray get_national_focus_icons() const;

godot::Vector2 get_unit_position_by_province_index(int32_t province_index) const;
godot::Vector2 get_port_position_by_province_index(int32_t province_index) const;
int32_t get_clicked_port_province_index(godot::Vector2 click_position) const;
};
}
5 changes: 5 additions & 0 deletions game/project.godot
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ menu_pause={
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194305,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
select_add={
"deadzone": 0.5,
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":false,"double_click":false,"script":null)
]
}

[internationalization]

Expand Down
Loading
Loading