diff --git a/flatscreen-project/network_test.tscn b/flatscreen-project/network_test.tscn index 744faebe..04b2bf72 100644 --- a/flatscreen-project/network_test.tscn +++ b/flatscreen-project/network_test.tscn @@ -1,4 +1,13 @@ -[gd_scene load_steps=2 format=3 uid="uid://bbvpj46frv0ho"] +[gd_scene load_steps=3 format=3 uid="uid://bbvpj46frv0ho"] + +[sub_resource type="GDScript" id="GDScript_yo4i2"] +script/source = "extends ServerNode + +@onready var revealed_clues_label := %RevealedClues + +func _on_clue_revealed(id: int) -> void: + revealed_clues_label.text += \"\\n{}\" % id +" [sub_resource type="GDScript" id="GDScript_78ugl"] script/source = "extends ClientNode @@ -16,6 +25,10 @@ func _on_connection_changed(connected: int) -> void: client_status.text = \"CONNECTED\" elif (connected == NetworkData.CONNECTION_AUTHENTICATED): client_status.text = \"AUTHENTICATED\" + + +func _on_line_edit_text_submitted(new_text: String) -> void: + pass " [node name="Control" type="Control"] @@ -27,6 +40,7 @@ grow_horizontal = 2 grow_vertical = 2 [node name="ServerNode" type="ServerNode" parent="."] +script = SubResource("GDScript_yo4i2") [node name="ClientNode" type="ClientNode" parent="."] script = SubResource("GDScript_78ugl") @@ -43,15 +57,21 @@ grow_vertical = 2 layout_mode = 2 size_flags_horizontal = 3 -[node name="Header" type="Label" parent="HBoxContainer/ServerInfo"] +[node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer/ServerInfo"] layout_mode = 2 -text = "Server: OFF" -[node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer/ServerInfo/Header"] -layout_mode = 0 -offset_top = -312.0 -offset_right = 574.0 -offset_bottom = 336.0 +[node name="Header" type="Label" parent="HBoxContainer/ServerInfo/VBoxContainer"] +layout_mode = 2 +text = "Server:" + +[node name="ServerStatus" type="Label" parent="HBoxContainer/ServerInfo/VBoxContainer"] +layout_mode = 2 +text = "DISCONNECTED" + +[node name="RevealedClues" type="Label" parent="HBoxContainer/ServerInfo/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Revealed Clues:" [node name="ClientInfo" type="PanelContainer" parent="HBoxContainer"] layout_mode = 2 @@ -69,4 +89,10 @@ unique_name_in_owner = true layout_mode = 2 text = "DISCONNECTED" +[node name="LineEdit" type="LineEdit" parent="HBoxContainer/ClientInfo/VBoxContainer"] +layout_mode = 2 +placeholder_text = "0" + +[connection signal="clue_revealed" from="ServerNode" to="ServerNode" method="_on_clue_revealed"] [connection signal="connection_changed" from="ClientNode" to="ClientNode" method="_on_connection_changed"] +[connection signal="text_submitted" from="HBoxContainer/ClientInfo/VBoxContainer/LineEdit" to="ClientNode" method="_on_line_edit_text_submitted"] diff --git a/modules/you_done_it/client_node.cpp b/modules/you_done_it/client_node.cpp index d131f88b..4726dbb8 100644 --- a/modules/you_done_it/client_node.cpp +++ b/modules/you_done_it/client_node.cpp @@ -1,11 +1,15 @@ #include "client_node.h" -#include "ydi_client.h" +#include "core/config/engine.h" +#include "you_done_it/clue_data.h" +#include "you_done_it/clue_db.h" +#include "you_done_it/ydi_client.h" ClientNode *ClientNode::singleton_instance{ nullptr }; String const ClientNode::sig_connection_changed{ "connection_changed" }; void ClientNode::_bind_methods() { ClassDB::bind_method(D_METHOD("connect_to_server", "server_url"), &self_type::connect_to_server); + ADD_SIGNAL(MethodInfo(sig_connection_changed, PropertyInfo(Variant::INT, "connected", PROPERTY_HINT_ENUM, "Disconnected,Connected,Authenticated"))); } @@ -23,6 +27,9 @@ void ClientNode::process() { if (new_status != this->state) { this->state = new_status; emit_signal(sig_connection_changed, new_status); + if (new_status == NetworkData::CONNECTION_AUTHENTICATED) { + reveal_backlog(); + } } } @@ -33,29 +40,26 @@ void ClientNode::exit_tree() { } } +void ClientNode::reveal_backlog() { + Ref db{ ClueDB::get_singleton() }; + for (int i{ 0 }; i < NetworkData::CLUE_MAX; ++i) { + Ref data{ db->get_clue((NetworkData::ClueID)i) }; + if (data.is_valid() && data->get_revealed()) { + ydi::client::send::reveal_clue(data->get_id()); + } + } +} + void ClientNode::_notification(int what) { if (Engine::get_singleton()->is_editor_hint()) { return; } - switch (what) { - case NOTIFICATION_ENTER_TREE: - set_process(true); - return; - case NOTIFICATION_PROCESS: - process(); - return; - case NOTIFICATION_EXIT_TREE: - exit_tree(); - return; - default: - return; - } } ClientNode *ClientNode::get_singleton() { return singleton_instance; } -void ClientNode::connect_to_server(String const &server) { - ydi::client::connect(server); +void ClientNode::connect_to_server(String const &url) { + ydi::client::connect(url); } diff --git a/modules/you_done_it/client_node.h b/modules/you_done_it/client_node.h index 983c764b..717a1362 100644 --- a/modules/you_done_it/client_node.h +++ b/modules/you_done_it/client_node.h @@ -10,6 +10,7 @@ class ClientNode : Node { void enter_tree(); void process(); void exit_tree(); + void reveal_backlog(); protected: void _notification(int what); diff --git a/modules/you_done_it/clue_data.cpp b/modules/you_done_it/clue_data.cpp new file mode 100644 index 00000000..8be71c2b --- /dev/null +++ b/modules/you_done_it/clue_data.cpp @@ -0,0 +1,48 @@ +#include "clue_data.h" +#include "you_done_it/macros.h" +#include "you_done_it/ydi_client.h" +#include + +void ClueData::_bind_methods() { + BIND_HPROPERTY(Variant::INT, id, PROPERTY_HINT_ENUM, NetworkData::ClueID_hint()); +} + +void ClueData::set_revealed(bool value) { + this->revealed = value; +} + +void ClueData::set_id(NetworkData::ClueID id) { + this->id = id; +} + +NetworkData::ClueID ClueData::get_id() const { + return this->id; +} + +void ClueData::reveal() { + if (this->id == NetworkData::CLUE_MAX) { + print_error("Attempt to reveal CLUE_MAX, invalid state, aborting"); + abort(); + } + if (this->revealed) { + print_error("Attempt to reveal clue that's already revealed, returning without action"); + return; + } + this->revealed = true; + if (ydi::client::status() == NetworkData::CONNECTION_AUTHENTICATED) { + ydi::client::send::reveal_clue(this->id); + } +} + +bool ClueData::get_revealed() const { + return this->revealed; +} + +void ClueData::set_image(Ref image) { + this->image = image; + // TODO: Sync to server +} + +Ref ClueData::get_image() const { + return this->image; +} diff --git a/modules/you_done_it/clue_data.h b/modules/you_done_it/clue_data.h new file mode 100644 index 00000000..62547959 --- /dev/null +++ b/modules/you_done_it/clue_data.h @@ -0,0 +1,26 @@ +#pragma once + +#include "you_done_it/ydi_networking.h" +#include +#include + +class ClueData : public Resource { + GDCLASS(ClueData, Resource); + static void _bind_methods(); + +protected: + void set_revealed(bool value); + +public: + void set_id(NetworkData::ClueID id); + NetworkData::ClueID get_id() const; + void reveal(); + bool get_revealed() const; + void set_image(Ref image); + Ref get_image() const; + +private: + NetworkData::ClueID id{ NetworkData::CLUE_MAX }; + bool revealed{ false }; + Ref image{}; +}; diff --git a/modules/you_done_it/clue_db.cpp b/modules/you_done_it/clue_db.cpp new file mode 100644 index 00000000..ff20dbf4 --- /dev/null +++ b/modules/you_done_it/clue_db.cpp @@ -0,0 +1,62 @@ +#include "clue_db.h" +#include "core/io/resource_loader.h" +#include "you_done_it/clue_data.h" +#include "you_done_it/ydi_networking.h" + +Ref ClueDB::singleton_instance{ nullptr }; + +void ClueDB::_bind_methods() { + BIND_HPROPERTY(Variant::ARRAY, clues, PROPERTY_HINT_ARRAY_TYPE, vformat("%s/%s:ClueData", Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE)); +} + +void ClueDB::ensure_data_valid() { + this->clues.resize_initialized(this->clues.capacity()); + for (int i{ 0 }; i < NetworkData::CLUE_MAX; ++i) { + if (!this->clues[i].is_valid()) { + this->clues[i].instantiate(); + } + this->clues[i]->set_id((NetworkData::ClueID)i); + } +} + +ClueDB::~ClueDB() { + if (singleton_instance == this) { + singleton_instance = nullptr; + } +} + +Ref &ClueDB::get_singleton() { + if (!singleton_instance.is_valid()) { + singleton_instance = Ref(ResourceLoader::load("res://clue_db.tres")); + } + return singleton_instance; +} + +void ClueDB::set_clues(Array data) { + for (Variant value : data) { + Ref clue{ Object::cast_to(value) }; + if (clue.is_valid()) { + NetworkData::ClueID id{ clue->get_id() }; + if (id >= 0 && id < NetworkData::CLUE_MAX) { + this->clues[clue->get_id()] = clue; + } else { + print_error(vformat("attempt to insert clue of id %s into db (min 0 max %s)", id, NetworkData::CLUE_MAX)); + } + } + } + ensure_data_valid(); +} + +Array ClueDB::get_clues() { + ensure_data_valid(); + Array data{}; + for (int i{ 0 }; i < NetworkData::CLUE_MAX; ++i) { + data.push_back(this->clues[i]); + } + return data; +} + +Ref ClueDB::get_clue(NetworkData::ClueID id) { + ensure_data_valid(); + return this->clues[id]; +} diff --git a/modules/you_done_it/clue_db.h b/modules/you_done_it/clue_db.h new file mode 100644 index 00000000..cad0b130 --- /dev/null +++ b/modules/you_done_it/clue_db.h @@ -0,0 +1,23 @@ +#pragma once + +#include "core/io/resource.h" +#include "core/templates/fixed_vector.h" +#include "ydi_networking.h" +#include "you_done_it/clue_data.h" + +class ClueDB : public Resource { + GDCLASS(ClueDB, Resource); + static void _bind_methods(); + static Ref singleton_instance; + void ensure_data_valid(); + +public: + virtual ~ClueDB(); + static Ref &get_singleton(); + void set_clues(Array data); + Array get_clues(); + Ref get_clue(NetworkData::ClueID id); + +private: + FixedVector, NetworkData::CLUE_MAX> clues{}; +}; diff --git a/modules/you_done_it/clue_finder.cpp b/modules/you_done_it/clue_finder.cpp index 50afb267..62a30fc1 100644 --- a/modules/you_done_it/clue_finder.cpp +++ b/modules/you_done_it/clue_finder.cpp @@ -1,10 +1,16 @@ #include "clue_finder.h" -#include "scene/main/node.h" +#include "scene/3d/xr/xr_nodes.h" +#include "ydi_client.h" +#include "you_done_it/client_node.h" +#include "you_done_it/clue_db.h" #include +#include ClueFinder *ClueFinder::singleton_instance{ nullptr }; -void ClueFinder::_bind_methods() {} +void ClueFinder::_bind_methods() { + ClassDB::bind_method(D_METHOD("take_photo"), &ClueFinder::take_photo); +} void ClueFinder::enter_tree() { if (singleton_instance == nullptr) { @@ -12,6 +18,9 @@ void ClueFinder::enter_tree() { } else { queue_free(); } + if (XRController3D * controller{ cast_to(get_parent()) }) { + controller->connect("button_pressed", callable_mp(this, &self_type::on_button_pressed)); + } } void ClueFinder::exit_tree() { @@ -20,6 +29,12 @@ void ClueFinder::exit_tree() { } } +void ClueFinder::on_button_pressed(String button) { + if (button == "trigger_click") { + take_photo(); + } +} + void ClueFinder::_notification(int what) { if (Engine::get_singleton()->is_editor_hint()) { return; @@ -52,3 +67,15 @@ ClueMarker *ClueFinder::find_current_clue() { void ClueFinder::register_clue_marker(ClueMarker *marker) { this->clue_markers.insert(marker); } + +void ClueFinder::remove_clue_marker(ClueMarker *marker) { + this->clue_markers.erase(marker); +} + +void ClueFinder::take_photo() { + print_line("TAKING PHOTO"); + if (ClueMarker * found{ find_current_clue() }) { + found->reveal(); + print_line("FOUND MARKER: ", found->get_path()); + } +} diff --git a/modules/you_done_it/clue_finder.h b/modules/you_done_it/clue_finder.h index b33dc2b2..f0609bd0 100644 --- a/modules/you_done_it/clue_finder.h +++ b/modules/you_done_it/clue_finder.h @@ -10,6 +10,7 @@ class ClueFinder : public Node3D { static ClueFinder *singleton_instance; void enter_tree(); void exit_tree(); + void on_button_pressed(String button); protected: void _notification(int what); @@ -18,6 +19,8 @@ public: static ClueFinder *get_singleton(); ClueMarker *find_current_clue(); void register_clue_marker(ClueMarker *marker); + void remove_clue_marker(ClueMarker *marker); + void take_photo(); private: HashSet clue_markers{}; diff --git a/modules/you_done_it/clue_marker.cpp b/modules/you_done_it/clue_marker.cpp index c8201423..96dd26fa 100644 --- a/modules/you_done_it/clue_marker.cpp +++ b/modules/you_done_it/clue_marker.cpp @@ -1,4 +1,5 @@ #include "clue_marker.h" +#include "core/config/engine.h" #include "macros.h" #include "you_done_it/clue_finder.h" #include "you_done_it/ydi_networking.h" @@ -7,6 +8,29 @@ void ClueMarker::_bind_methods() { BIND_HPROPERTY(Variant::INT, clue_id, PROPERTY_HINT_ENUM, NetworkData::ClueID_hint()); } +void ClueMarker::enter_tree() { + ClueFinder::get_singleton()->register_clue_marker(this); +} + +void ClueMarker::exit_tree() { + ClueFinder::get_singleton()->remove_clue_marker(this); +} + +void ClueMarker::_notification(int what) { + if (Engine::get_singleton()->is_editor_hint()) { + return; + } + switch (what) { + case NOTIFICATION_ENTER_TREE: + enter_tree(); + return; + case NOTIFICATION_EXIT_TREE: + exit_tree(); + default: + return; + } +} + bool ClueMarker::is_visible() const { Transform3D const viewpoint{ ClueFinder::get_singleton()->get_global_transform() }; Basis const basis{ viewpoint.get_basis() }; @@ -16,13 +40,11 @@ bool ClueMarker::is_visible() const { return false; } Vector3 const transformed_dir{ pos_transformed.normalized() }; - if (Math::abs(transformed_dir.signed_angle_to({ 0, 0, 1 }, { 0, 1, 0 })) > Math::PI / 4.0) { - return false; - } - if (Math::abs(transformed_dir.signed_angle_to({ 0, 0, 1 }, { 1, 0, 0 })) > Math::PI / 4.0) { - return false; - } - return true; + return Math::abs(transformed_dir.x) < 0.5 || Math::abs(transformed_dir.y) < 0.5; +} + +void ClueMarker::reveal() { + ClueDB::get_singleton()->get_clue(this->id)->reveal(); } void ClueMarker::set_clue_id(NetworkData::ClueID id) { diff --git a/modules/you_done_it/clue_marker.h b/modules/you_done_it/clue_marker.h index 1494397a..0a6493a1 100644 --- a/modules/you_done_it/clue_marker.h +++ b/modules/you_done_it/clue_marker.h @@ -1,14 +1,21 @@ #pragma once +#include "clue_db.h" #include "ydi_networking.h" -#include +#include -class ClueMarker : public Node3D { - GDCLASS(ClueMarker, Node3D); +class ClueMarker : public Marker3D { + GDCLASS(ClueMarker, Marker3D); static void _bind_methods(); + void enter_tree(); + void exit_tree(); + +protected: + void _notification(int what); public: bool is_visible() const; + void reveal(); void set_clue_id(NetworkData::ClueID id); NetworkData::ClueID get_clue_id() const; diff --git a/modules/you_done_it/register_types.cpp b/modules/you_done_it/register_types.cpp index fe1ed098..12e163c1 100644 --- a/modules/you_done_it/register_types.cpp +++ b/modules/you_done_it/register_types.cpp @@ -1,11 +1,12 @@ #include "register_types.h" #include "client_node.h" -#include "clue_marker.h" -#include "server_node.h" -#include "ydi_networking.h" -#include "ydi_vr_origin.h" +#include "you_done_it/clue_data.h" #include "you_done_it/clue_finder.h" +#include "you_done_it/clue_marker.h" +#include "you_done_it/server_node.h" +#include "you_done_it/ydi_networking.h" +#include "you_done_it/ydi_vr_origin.h" #include void initialize_you_done_it_module(ModuleInitializationLevel p_level) { @@ -18,6 +19,8 @@ void initialize_you_done_it_module(ModuleInitializationLevel p_level) { ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); + ClassDB::register_class(); + ClassDB::register_class(); } void uninitialize_you_done_it_module(ModuleInitializationLevel p_level) { diff --git a/modules/you_done_it/ydi_client.cpp b/modules/you_done_it/ydi_client.cpp index be03ffc2..d30652e1 100644 --- a/modules/you_done_it/ydi_client.cpp +++ b/modules/you_done_it/ydi_client.cpp @@ -137,8 +137,12 @@ NetworkData::ConnectionStatus status() { namespace send { void reveal_clue(NetworkData::ClueID id) { - std::scoped_lock lock{ connection->mtx }; - multipart(NetworkData::MSG_REVEAL, id).send(*connection->socket); + if (connection) { + std::scoped_lock lock{ connection->mtx }; + multipart(NetworkData::MSG_REVEAL, id).send(*connection->socket); + } else { + print_error("ydi::client::send::reveal_clue: No connection, exiting without action, call connect and try again"); + } } } //namespace send } //namespace ydi::client diff --git a/vr-project/3D Models/camera_placeholder.blend b/vr-project/3D Models/camera_placeholder.blend new file mode 100644 index 00000000..ba265e9f Binary files /dev/null and b/vr-project/3D Models/camera_placeholder.blend differ diff --git a/vr-project/3D Models/camera_placeholder.blend.import b/vr-project/3D Models/camera_placeholder.blend.import new file mode 100644 index 00000000..cea6368f --- /dev/null +++ b/vr-project/3D Models/camera_placeholder.blend.import @@ -0,0 +1,59 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://cq0mv68n1jacx" +path="res://.godot/imported/camera_placeholder.blend-dbb71f8848cdf612934a27943608eb69.scn" + +[deps] + +source_file="res://3D Models/camera_placeholder.blend" +dest_files=["res://.godot/imported/camera_placeholder.blend-dbb71f8848cdf612934a27943608eb69.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +blender/nodes/visible=0 +blender/nodes/active_collection_only=false +blender/nodes/punctual_lights=true +blender/nodes/cameras=true +blender/nodes/custom_properties=true +blender/nodes/modifiers=1 +blender/meshes/colors=false +blender/meshes/uvs=true +blender/meshes/normals=true +blender/meshes/export_geometry_nodes_instances=false +blender/meshes/tangents=true +blender/meshes/skins=2 +blender/meshes/export_bones_deforming_mesh_only=false +blender/materials/unpack_enabled=true +blender/materials/export_materials=1 +blender/animation/limit_playback=true +blender/animation/always_sample=true +blender/animation/group_tracks=true +gltf/naming_version=2 diff --git a/vr-project/clue_db.tres b/vr-project/clue_db.tres new file mode 100644 index 00000000..fdb7cc5b --- /dev/null +++ b/vr-project/clue_db.tres @@ -0,0 +1,10 @@ +[gd_resource type="ClueDB" load_steps=3 format=3 uid="uid://dlf8dxiter8b8"] + +[sub_resource type="ClueData" id="ClueData_kxjsf"] +id = 0 + +[sub_resource type="ClueData" id="ClueData_du6rq"] +id = 1 + +[resource] +clues = [SubResource("ClueData_kxjsf"), SubResource("ClueData_du6rq")] diff --git a/vr-project/project.godot b/vr-project/project.godot index c658dcf8..26ad616d 100644 --- a/vr-project/project.godot +++ b/vr-project/project.godot @@ -15,6 +15,12 @@ run/main_scene="uid://b5m5h30gog3pu" config/features=PackedStringArray("4.5", "Forward Plus") config/icon="res://icon.svg" +[display] + +window/size/viewport_width=1440 +window/size/viewport_height=1600 +window/vsync/vsync_mode=0 + [rendering] lights_and_shadows/directional_shadow/size=8192 diff --git a/vr-project/scenes/game_scene.tscn b/vr-project/scenes/game_scene.tscn index 2f53c041..824711d2 100644 --- a/vr-project/scenes/game_scene.tscn +++ b/vr-project/scenes/game_scene.tscn @@ -35,16 +35,6 @@ glow_enabled = true [node name="Root" type="Node3D"] [node name="VROrigin" parent="." instance=ExtResource("2_onqr8")] -_import_path = NodePath("") -unique_name_in_owner = false -process_mode = 0 -process_priority = 0 -process_physics_priority = 0 -process_thread_group = 0 -physics_interpolation_mode = 0 -auto_translate_mode = 0 -editor_description = "" -script = null [node name="WorldEnvironment" type="WorldEnvironment" parent="."] environment = SubResource("Environment_bw6k5") @@ -65,17 +55,7 @@ shadow_blur = 0.635 transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.27989578, 0, 1.4924412) [node name="ClueMarker" type="ClueMarker" parent="."] -_import_path = NodePath("") -unique_name_in_owner = false -process_mode = 0 -process_priority = 0 -process_physics_priority = 0 -process_thread_group = 0 -physics_interpolation_mode = 0 -auto_translate_mode = 0 -editor_description = "" clue_id = 0 -script = null [node name="Stapler" parent="." instance=ExtResource("3_ycayy")] transform = Transform3D(-1.8868132, 0, -1.8585076, 0, 2.6484175, 0, 1.8585076, 0, -1.8868132, -1.2748423, 0.9004388, 2.876501) diff --git a/vr-project/vr_base.tscn b/vr-project/vr_base.tscn index f317419c..d9b06c8e 100644 --- a/vr-project/vr_base.tscn +++ b/vr-project/vr_base.tscn @@ -1,4 +1,6 @@ -[gd_scene load_steps=2 format=3 uid="uid://ctf3dsro4aqon"] +[gd_scene load_steps=3 format=3 uid="uid://ctf3dsro4aqon"] + +[ext_resource type="PackedScene" uid="uid://cq0mv68n1jacx" path="res://3D Models/camera_placeholder.blend" id="1_bibh8"] [sub_resource type="CylinderMesh" id="CylinderMesh_y71p3"] top_radius = 0.05 @@ -7,6 +9,8 @@ height = 0.05 [node name="VROrigin" type="VROrigin"] +[node name="ClientNode" type="ClientNode" parent="."] + [node name="XRCamera3D" type="XRCamera3D" parent="."] current = true @@ -19,7 +23,8 @@ mesh = SubResource("CylinderMesh_y71p3") [node name="RightController" type="XRController3D" parent="."] tracker = &"right_hand" -[node name="MeshInstance3D" type="MeshInstance3D" parent="RightController"] -mesh = SubResource("CylinderMesh_y71p3") +[node name="ClueFinder" type="ClueFinder" parent="RightController"] +transform = Transform3D(1, 0, 0, 0, -0.25849885, -0.96601164, 0, 0.96601164, -0.25849885, -0.06272071, -0.034197945, 0.03913331) -[node name="ClientNode" type="ClientNode" parent="."] +[node name="camera_placeholder" parent="RightController/ClueFinder" instance=ExtResource("1_bibh8")] +transform = Transform3D(1, 0, 0, 0, -1, -8.742278e-08, 0, 8.742278e-08, -1, 0.03785751, -0.042704105, 1.8867304e-08)