aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Hop311 <hop3114@gmail.com>2023-04-03 19:38:27 +0200
committer Hop311 <hop3114@gmail.com>2023-04-03 19:38:27 +0200
commitc7def7396da00b39eced666ad360397733712bfd (patch)
tree06fd1047a6193e9062c53375eeea2a8419f3cc1d
parent60ddfc88fd6dc259792532fadf9cc4407f90e95f (diff)
Basic province id and shape loading and rendering.
-rw-r--r--extension/src/MapSingleton.cpp150
-rw-r--r--extension/src/MapSingleton.hpp30
-rw-r--r--extension/src/openvic2/Map.cpp29
-rw-r--r--extension/src/openvic2/Map.hpp28
-rw-r--r--extension/src/register_types.cpp8
-rw-r--r--game/common/map/provinces.json9
-rw-r--r--game/common/map/provinces.pngbin0 -> 9464 bytes
-rw-r--r--game/common/map/provinces.png.import3
-rw-r--r--game/src/Autoload/Events.gd9
-rw-r--r--game/src/GameSession/MapView.gd111
-rw-r--r--game/src/GameSession/MapView.tscn6
-rw-r--r--game/src/GameSession/TerrainMap.gdshader23
12 files changed, 383 insertions, 23 deletions
diff --git a/extension/src/MapSingleton.cpp b/extension/src/MapSingleton.cpp
new file mode 100644
index 0000000..10b750a
--- /dev/null
+++ b/extension/src/MapSingleton.cpp
@@ -0,0 +1,150 @@
+#include "MapSingleton.hpp"
+
+#include <godot_cpp/variant/utility_functions.hpp>
+#include <godot_cpp/classes/file_access.hpp>
+#include <godot_cpp/classes/json.hpp>
+
+using namespace godot;
+using namespace OpenVic2;
+
+MapSingleton* MapSingleton::singleton = nullptr;
+
+void MapSingleton::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("load_province_identifier_file", "file_path"), &MapSingleton::load_province_identifier_file);
+ ClassDB::bind_method(D_METHOD("load_province_shape_file", "file_path"), &MapSingleton::load_province_shape_file);
+ ClassDB::bind_method(D_METHOD("get_province_shape_image"), &MapSingleton::get_province_shape_image);
+}
+
+MapSingleton* MapSingleton::get_singleton() {
+ return singleton;
+}
+
+MapSingleton::MapSingleton() {
+ ERR_FAIL_COND(singleton != nullptr);
+ singleton = this;
+}
+
+MapSingleton::~MapSingleton() {
+ ERR_FAIL_COND(singleton != this);
+ singleton = nullptr;
+}
+
+Error MapSingleton::load_province_identifier_file(String const& file_path) {
+ UtilityFunctions::print("Loading identifier file: ", file_path);
+ Ref<FileAccess> file = FileAccess::open(file_path, FileAccess::ModeFlags::READ);
+ Error err = FileAccess::get_open_error();
+ if (err != OK || file.is_null()) {
+ UtilityFunctions::push_error("Failed to load province identifier file: ", file_path);
+ return err == OK ? FAILED : err;
+ }
+ String json_string = file->get_as_text();
+ Ref<JSON> json;
+ json.instantiate();
+ err = json->parse(json_string);
+ if (err) {
+ UtilityFunctions::push_error("Failed to parse province identifier file as JSON: ", file_path,
+ "\nError at line ", json->get_error_line(), ": ", json->get_error_message());
+ return err;
+ }
+ Variant json_var = json->get_data();
+ Variant::Type type = json_var.get_type();
+ if (type != Variant::DICTIONARY) {
+ UtilityFunctions::push_error("Invalid province identifier JSON: root has type ",
+ Variant::get_type_name(type), " (expected Dictionary)");
+ return FAILED;
+ }
+ Dictionary prov_dict = json_var;
+ Array prov_identifiers = prov_dict.keys();
+ for (int idx = 0; idx < prov_identifiers.size(); ++idx) {
+ String const& identifier = prov_identifiers[idx];
+ Variant const& colour_var = prov_dict[identifier];
+ if (identifier.is_empty()) {
+ UtilityFunctions::push_error("Empty province identifier with colour: ", colour_var);
+ err = FAILED;
+ continue;
+ }
+ static const String prov_prefix = "prov_";
+ if (!identifier.begins_with(prov_prefix))
+ UtilityFunctions::push_warning("Province identifier missing prefix: ", identifier);
+ type = colour_var.get_type();
+ Province::colour_t colour = Province::NULL_COLOUR;
+ if (type == Variant::ARRAY) {
+ Array colour_array = colour_var;
+ if (colour_array.size() == 3) {
+ for (int jdx = 0; jdx < 3; ++jdx) {
+ Variant var = colour_array[jdx];
+ if (var.get_type() != Variant::FLOAT) {
+ colour = Province::NULL_COLOUR;
+ break;
+ }
+ double colour_double = var;
+ if (std::trunc(colour_double) != colour_double) {
+ colour = Province::NULL_COLOUR;
+ break;
+ }
+ int64_t colour_int = static_cast<int64_t>(colour_double);
+ if (colour_int < 0 || colour_int > 255) {
+ colour = Province::NULL_COLOUR;
+ break;
+ }
+ colour = (colour << 8) | colour_int;
+ }
+ }
+ } else if (type == Variant::STRING) {
+ String colour_string = colour_var;
+ if (colour_string.is_valid_hex_number()) {
+ int64_t colour_int = colour_string.hex_to_int();
+ if (0 <= colour_int && colour_int <= 0xFFFFFF)
+ colour = colour_int;
+ }
+ }
+ if (colour == Province::NULL_COLOUR) {
+ UtilityFunctions::push_error("Invalid province identifier colour for ", identifier, ": ", colour_var);
+ err = FAILED;
+ continue;
+ }
+ std::string error_message;
+ if (map.add_province(identifier.utf8().get_data(), colour, error_message)) {
+ UtilityFunctions::print(error_message.c_str());
+ } else {
+ UtilityFunctions::push_error(error_message.c_str());
+ err = FAILED;
+ }
+ }
+ return err;
+}
+
+Error MapSingleton::load_province_shape_file(String const& file_path) {
+ if (province_shape_image.is_valid()) {
+ UtilityFunctions::push_error("Province shape file has already been loaded, cannot load: ", file_path);
+ return FAILED;
+ }
+ province_shape_image.instantiate();
+ Error err = province_shape_image->load(file_path);
+ if (err != OK) {
+ UtilityFunctions::push_error("Failed to load province shape file: ", file_path);
+ province_shape_image.unref();
+ return err;
+ }
+ int32_t width = province_shape_image->get_width();
+ int32_t height = province_shape_image->get_height();
+ if (width < 1 || height < 1) {
+ UtilityFunctions::push_error("Invalid dimensions (", width, "x", height, ") for province shape file: ", file_path);
+ err = FAILED;
+ }
+ static const Image::Format expected_format = Image::Format::FORMAT_RGB8;
+ Image::Format format = province_shape_image->get_format();
+ if (format != expected_format) {
+ UtilityFunctions::push_error("Invalid format (", format, ", should be ", expected_format, ") for province shape file: ", file_path);
+ err = FAILED;
+ }
+ if (err) {
+ province_shape_image.unref();
+ return err;
+ }
+ return err;
+}
+
+Ref<Image> MapSingleton::get_province_shape_image() const {
+ return province_shape_image;
+}
diff --git a/extension/src/MapSingleton.hpp b/extension/src/MapSingleton.hpp
new file mode 100644
index 0000000..767ae88
--- /dev/null
+++ b/extension/src/MapSingleton.hpp
@@ -0,0 +1,30 @@
+#pragma once
+
+#include <godot_cpp/core/class_db.hpp>
+#include <godot_cpp/classes/image.hpp>
+#include "openvic2/Map.hpp"
+
+namespace OpenVic2 {
+ class MapSingleton : public godot::Object {
+
+ GDCLASS(MapSingleton, godot::Object)
+
+ static MapSingleton* singleton;
+
+ godot::Ref<godot::Image> province_shape_image;
+ Map map;
+
+ protected:
+ static void _bind_methods();
+
+ public:
+ static MapSingleton* get_singleton();
+
+ MapSingleton();
+ ~MapSingleton();
+
+ godot::Error load_province_identifier_file(godot::String const& file_path);
+ godot::Error load_province_shape_file(godot::String const& file_path);
+ godot::Ref<godot::Image> get_province_shape_image() const;
+ };
+}
diff --git a/extension/src/openvic2/Map.cpp b/extension/src/openvic2/Map.cpp
new file mode 100644
index 0000000..c53b86d
--- /dev/null
+++ b/extension/src/openvic2/Map.cpp
@@ -0,0 +1,29 @@
+#include "Map.hpp"
+
+#include <sstream>
+#include <iomanip>
+
+using namespace OpenVic2;
+
+std::string Province::to_string() const {
+ std::ostringstream stream;
+ stream << "(" << identifier << ", " << std::hex << std::setfill('0') << std::setw(6) << colour << ")";
+ return stream.str();
+}
+
+bool Map::add_province(std::string const& identifier, Province::colour_t colour, std::string& error_message) {
+ Province new_province = { identifier, colour };
+ for (Province const& province : provinces) {
+ if (province.identifier == identifier) {
+ error_message = "Duplicate province identifiers: " + province.to_string() + " and " + new_province.to_string();
+ return false;
+ }
+ if (province.colour == colour) {
+ error_message = "Duplicate province colours: " + province.to_string() + " and " + new_province.to_string();
+ return false;
+ }
+ }
+ provinces.push_back(new_province);
+ error_message = "Added province: " + new_province.to_string();
+ return true;
+} \ No newline at end of file
diff --git a/extension/src/openvic2/Map.hpp b/extension/src/openvic2/Map.hpp
new file mode 100644
index 0000000..3c9c6de
--- /dev/null
+++ b/extension/src/openvic2/Map.hpp
@@ -0,0 +1,28 @@
+#pragma once
+
+#include <string>
+#include <cstdint>
+#include <vector>
+
+namespace OpenVic2 {
+
+ struct Province {
+ using colour_t = uint32_t;
+
+ static const colour_t NULL_COLOUR = 0;
+
+ std::string identifier;
+ colour_t colour;
+
+ std::string to_string() const;
+ };
+
+ struct Map {
+ private:
+ std::vector<Province> provinces;
+
+ public:
+ bool add_province(std::string const& identifier, Province::colour_t colour, std::string& error_message);
+ };
+
+}
diff --git a/extension/src/register_types.cpp b/extension/src/register_types.cpp
index d1613a5..ef5428c 100644
--- a/extension/src/register_types.cpp
+++ b/extension/src/register_types.cpp
@@ -9,6 +9,7 @@
#include "Simulation.hpp"
#include "Checksum.hpp"
#include "LoadLocalisation.hpp"
+#include "MapSingleton.hpp"
using namespace godot;
using namespace OpenVic2;
@@ -17,6 +18,7 @@ static TestSingleton* _test_singleton;
static Simulation* _simulation;
static Checksum* _checksum;
static LoadLocalisation* _load_localisation;
+static MapSingleton* _map_singleton;
void initialize_openvic2_types(ModuleInitializationLevel p_level)
{
@@ -40,6 +42,9 @@ void initialize_openvic2_types(ModuleInitializationLevel p_level)
_load_localisation = memnew(LoadLocalisation);
Engine::get_singleton()->register_singleton("LoadLocalisation", LoadLocalisation::get_singleton());
+ ClassDB::register_class<MapSingleton>();
+ _map_singleton = memnew(MapSingleton);
+ Engine::get_singleton()->register_singleton("MapSingleton", MapSingleton::get_singleton());
}
void uninitialize_openvic2_types(ModuleInitializationLevel p_level) {
@@ -58,6 +63,9 @@ void uninitialize_openvic2_types(ModuleInitializationLevel p_level) {
Engine::get_singleton()->unregister_singleton("LoadLocalisation");
memdelete(_load_localisation);
+
+ Engine::get_singleton()->unregister_singleton("MapSingleton");
+ memdelete(_map_singleton);
}
extern "C"
diff --git a/game/common/map/provinces.json b/game/common/map/provinces.json
new file mode 100644
index 0000000..66de29b
--- /dev/null
+++ b/game/common/map/provinces.json
@@ -0,0 +1,9 @@
+{
+ "prov_britain": [150, 0, 0],
+ "prov_ireland": [23, 147, 31],
+ "prov_iceland": "343D91",
+ "prov_cuba": "1E29FF",
+ "prov_madagascar": "790091",
+ "prov_ceylon": "FF6A00",
+ "prov_formosa": "82B1FF"
+}
diff --git a/game/common/map/provinces.png b/game/common/map/provinces.png
new file mode 100644
index 0000000..68bf528
--- /dev/null
+++ b/game/common/map/provinces.png
Binary files differ
diff --git a/game/common/map/provinces.png.import b/game/common/map/provinces.png.import
new file mode 100644
index 0000000..8dd0c09
--- /dev/null
+++ b/game/common/map/provinces.png.import
@@ -0,0 +1,3 @@
+[remap]
+
+importer="keep"
diff --git a/game/src/Autoload/Events.gd b/game/src/Autoload/Events.gd
index 25a185f..040cb06 100644
--- a/game/src/Autoload/Events.gd
+++ b/game/src/Autoload/Events.gd
@@ -2,3 +2,12 @@ extends Node
var Options = preload("Events/Options.gd").new()
var Localisation = preload("Events/Localisation.gd").new()
+
+const _province_identifier_file : String = "res://common/map/provinces.json"
+const _province_shape_file : String = "res://common/map/provinces.png"
+
+func _ready():
+ if MapSingleton.load_province_identifier_file(_province_identifier_file) != OK:
+ push_error("Failed to load province identifiers")
+ if MapSingleton.load_province_shape_file(_province_shape_file) != OK:
+ push_error("Failed to load province shapes")
diff --git a/game/src/GameSession/MapView.gd b/game/src/GameSession/MapView.gd
index 4d06066..982271d 100644
--- a/game/src/GameSession/MapView.gd
+++ b/game/src/GameSession/MapView.gd
@@ -7,6 +7,11 @@ const _action_west : StringName = &"map_west"
const _action_zoomin : StringName = &"map_zoomin"
const _action_zoomout : StringName = &"map_zoomout"
+const _shader_param_provinces : StringName = &"province_tex"
+const _shader_param_mouse_pos : StringName = &"mouse_pos"
+
+@export var _camera : Camera3D
+
@export var _move_speed : float = 1.0
@export var _zoom_target_min : float = 0.2
@@ -18,8 +23,53 @@ const _action_zoomout : StringName = &"map_zoomout"
get: return _zoom_target
set(v): _zoom_target = clamp(v, _zoom_target_min, _zoom_target_max)
-@export var _camera : Camera3D
@export var _map_mesh : MeshInstance3D
+var _map_shader_material : ShaderMaterial
+var _map_aspect_ratio : float = 1.0
+var _map_mesh_corner : Vector2
+var _map_mesh_dims : Vector2
+
+var _mouse_pos : Vector2 = Vector2(0.5, 0.5)
+
+func _ready():
+ if _camera == null:
+ push_error("MapView's _camera variable hasn't been set!")
+ return
+ if _map_mesh == null:
+ push_error("MapView's _map_mesh variable hasn't been set!")
+ return
+ var province_shape_image : Image = MapSingleton.get_province_shape_image()
+ if province_shape_image == null:
+ push_error("Failed to get province shape image!")
+ return
+
+ # Set map mesh size and get bounds
+ _map_aspect_ratio = float(province_shape_image.get_width()) / float(province_shape_image.get_height())
+ if _map_mesh.mesh.get_class() != "PlaneMesh":
+ push_error("Invalid map mesh class: ", _map_mesh.mesh.get_class())
+ else:
+ # Width is doubled so that the map appears to loop horizontally
+ (_map_mesh.mesh as PlaneMesh).size = Vector2(_map_aspect_ratio * 2, 1)
+ var map_mesh_aabb := _map_mesh.get_aabb() * _map_mesh.transform
+ _map_mesh_corner = Vector2(
+ min(map_mesh_aabb.position.x, map_mesh_aabb.end.x),
+ min(map_mesh_aabb.position.z, map_mesh_aabb.end.z)
+ )
+ _map_mesh_dims = abs(Vector2(
+ map_mesh_aabb.position.x - map_mesh_aabb.end.x,
+ map_mesh_aabb.position.z - map_mesh_aabb.end.z
+ ))
+
+ var map_material = _map_mesh.get_active_material(0)
+ if map_material == null:
+ push_error("Map mesh is missing material!")
+ return
+ if map_material.get_class() != "ShaderMaterial":
+ push_error("Invalid map mesh material class: ", map_material.get_class())
+ return
+ _map_shader_material = map_material
+ var texture := ImageTexture.create_from_image(province_shape_image)
+ _map_shader_material.set_shader_parameter(_shader_param_provinces, texture)
func _input(event : InputEvent):
if event.is_action_pressed(_action_zoomin, true):
@@ -27,33 +77,60 @@ func _input(event : InputEvent):
elif event.is_action_pressed(_action_zoomout, true):
_zoom_target += _zoom_target_step
-func _physics_process(delta):
+func _physics_process(delta : float):
# Process movement
- var height : float = _camera.transform.origin.y
+ _move_process(delta)
+ # Keep within map bounds
+ _clamp_over_map()
+ # Process zooming
+ _zoom_process(delta)
+ # Orient based on height
+ _update_orientation()
+ # Calculate where the mouse lies on the map
+ _update_mouse_map_position()
+
+func _move_process(delta : float) -> void:
var move := Vector3(
float(Input.is_action_pressed(_action_east)) - float(Input.is_action_pressed(_action_west)),
- 0.0,
- float(Input.is_action_pressed(_action_south)) - float(Input.is_action_pressed(_action_north)),
+ 0,
+ float(Input.is_action_pressed(_action_south)) - float(Input.is_action_pressed(_action_north))
)
- move *= _move_speed * height * delta
+ # Scale movement speed with height
+ move *= _move_speed * _camera.transform.origin.y * delta
_camera.global_translate(move)
- # Keep within map bounds
- var bounds := _map_mesh.get_aabb() * _map_mesh.transform
- var width := bounds.end.x - bounds.position.x
- var left := bounds.position.x + 0.25 * width
- var longitude := fposmod(_camera.transform.origin.x - left, width * 0.5)
+func _clamp_over_map() -> void:
+ var left := _map_mesh_corner.x + 0.25 * _map_mesh_dims.x
+ var longitude := fposmod(_camera.transform.origin.x - left, _map_mesh_dims.x * 0.5)
_camera.transform.origin.x = left + longitude
- _camera.transform.origin.z = clamp(_camera.transform.origin.z, bounds.position.z, bounds.end.z)
+ _camera.transform.origin.z = clamp(_camera.transform.origin.z, _map_mesh_corner.y, _map_mesh_corner.y + _map_mesh_dims.y)
- # Process zooming
- var zoom : float = _zoom_target - height
+func _zoom_process(delta : float) -> void:
+ var height := _camera.transform.origin.y
+ var zoom := _zoom_target - height
height += zoom * _zoom_speed * delta
- var new_zoom : float = _zoom_target - height
+ var new_zoom := _zoom_target - height
+ # Set to target if height is within _zoom_epsilon of it or has overshot past it
if abs(new_zoom) < _zoom_epsilon or sign(zoom) != sign(new_zoom):
height = _zoom_target
_camera.transform.origin.y = height
- # Orient based on height
- var dir := Vector3(0, -1, -exp(-height * 2.0 + 0.5))
+func _update_orientation() -> void:
+ var dir := Vector3(0, -1, -exp(-_camera.transform.origin.y * 2.0 + 0.5))
_camera.look_at(_camera.transform.origin + dir)
+
+func _update_mouse_map_position() -> void:
+ var mouse_pos_window := get_viewport().get_mouse_position()
+ var ray_origin := _camera.project_ray_origin(mouse_pos_window)
+ var ray_normal := _camera.project_ray_normal(mouse_pos_window)
+ # Plane with normal (0,1,0) facing upwards, at a distance 0 from the origin
+ var intersection = Plane(0, 1, 0, 0).intersects_ray(ray_origin, ray_normal)
+ if typeof(intersection) == TYPE_VECTOR3:
+ var intersection_vec := intersection as Vector3
+ # This loops both horizontally (good) and vertically (bad)
+ _mouse_pos = (Vector2(intersection_vec.x, intersection_vec.z) - _map_mesh_corner) / _map_mesh_dims
+ _map_shader_material.set_shader_parameter(_shader_param_mouse_pos, _mouse_pos)
+ else:
+ # Normals parallel to the xz-plane could cause null intersections,
+ # but the camera's orientation should prevent such normals
+ push_error("Invalid intersection: ", intersection)
diff --git a/game/src/GameSession/MapView.tscn b/game/src/GameSession/MapView.tscn
index 4c34035..97a72a8 100644
--- a/game/src/GameSession/MapView.tscn
+++ b/game/src/GameSession/MapView.tscn
@@ -7,10 +7,10 @@
[sub_resource type="ShaderMaterial" id="ShaderMaterial_tayeg"]
render_priority = 0
shader = ExtResource("1_upocn")
-shader_parameter/tex = ExtResource("3_l8pnf")
+shader_parameter/mouse_pos = Vector2(0.5, 0.5)
+shader_parameter/terrain_tex = ExtResource("3_l8pnf")
[sub_resource type="PlaneMesh" id="PlaneMesh_skc48"]
-size = Vector2(10.8, 2)
[node name="MapView" type="Node3D" node_paths=PackedStringArray("_camera", "_map_mesh")]
script = ExtResource("1_exccw")
@@ -21,6 +21,6 @@ _map_mesh = NodePath("MapMeshInstance")
transform = Transform3D(1, 0, 0, 0, 0.707107, 0.707107, 0, -0.707107, 0.707107, 0, 1, 1)
[node name="MapMeshInstance" type="MeshInstance3D" parent="."]
-transform = Transform3D(5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0)
+transform = Transform3D(10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0)
material_override = SubResource("ShaderMaterial_tayeg")
mesh = SubResource("PlaneMesh_skc48")
diff --git a/game/src/GameSession/TerrainMap.gdshader b/game/src/GameSession/TerrainMap.gdshader
index 522e0f3..0c5f3e1 100644
--- a/game/src/GameSession/TerrainMap.gdshader
+++ b/game/src/GameSession/TerrainMap.gdshader
@@ -2,9 +2,26 @@ shader_type spatial;
render_mode unshaded;
-uniform sampler2D tex: source_color, repeat_enable;
+// Cosmetic terrain texture
+uniform sampler2D terrain_tex: source_color, repeat_enable;
+// Province shape texture
+uniform sampler2D province_tex: source_color, repeat_enable;
+
+// Mouse position in UV coords over the map mesh
+uniform vec2 mouse_pos;
+
+// Transform map mesh UV coordinates to account for the extra
+// half map on either side. This takes the x-coord from [0 -> 1]
+// to [-0.5 -> 1.5], while leaving the y-coord unchanged.
+vec2 fix_uv(vec2 uv) {
+ return vec2(uv.x * 2.0 - 0.5, uv.y);
+}
void fragment() {
- vec2 new_uv = vec2(UV.x * 2.0 - 0.5, UV.y);
- ALBEDO = texture(tex, new_uv).rgb;
+ vec2 fixed_uv = fix_uv(UV);
+ vec3 prov_colour = texture(province_tex, fixed_uv).rgb;
+ vec3 mouse_colour = texture(province_tex, fix_uv(mouse_pos)).rgb;
+ // Boost prov_colour's contribution if the mouse is over that colour and it isn't (0,0,0)
+ float mix_val = prov_colour == mouse_colour && mouse_colour != vec3(0.0) ? 0.8 : 0.4;
+ ALBEDO = mix(texture(terrain_tex, fixed_uv).rgb, prov_colour, mix_val);
}