diff --git a/godot/.godot/global_script_class_cache.cfg b/godot/.godot/global_script_class_cache.cfg index d1d7612a..32d85931 100644 --- a/godot/.godot/global_script_class_cache.cfg +++ b/godot/.godot/global_script_class_cache.cfg @@ -131,6 +131,12 @@ list=Array[Dictionary]([{ "language": &"GDScript", "path": "res://src/test/avatar/spawn_and_move.gd" }, { +"base": &"DclTestingTools", +"class": &"TestingTools", +"icon": "", +"language": &"GDScript", +"path": "res://src/test/testing_tool.gd" +}, { "base": &"Control", "class": &"VirtualJoystick", "icon": "", diff --git a/godot/src/global.gd b/godot/src/global.gd index 6873ba90..ea74b19b 100644 --- a/godot/src/global.gd +++ b/godot/src/global.gd @@ -49,6 +49,9 @@ func _ready(): self.realm = Realm.new() self.realm.set_name("realm") + + self.testing_tools = TestingTools.new() + self.testing_tools.set_name("testing_tool") self.content_manager = ContentManager.new() self.content_manager.set_name("content_manager") @@ -69,6 +72,7 @@ func _ready(): get_tree().root.add_child.call_deferred(self.comms) get_tree().root.add_child.call_deferred(self.avatars) get_tree().root.add_child.call_deferred(self.portable_experience_controller) + get_tree().root.add_child.call_deferred(self.testing_tools) # TODO: enable raycast debugger add_child(raycast_debugger) diff --git a/godot/src/logic/realm.gd b/godot/src/logic/realm.gd index 3c6db528..f5853967 100644 --- a/godot/src/logic/realm.gd +++ b/godot/src/logic/realm.gd @@ -112,9 +112,11 @@ func set_realm(new_realm_string: String) -> void: if parsed_urn != null: realm_global_scene_urns.push_back(parsed_urn) - realm_city_loader_content_base_url = Realm.ensure_ends_with_slash( - configuration.get("cityLoaderContentServer", "") - ) + realm_city_loader_content_base_url = configuration.get("cityLoaderContentServer", "") + if not realm_city_loader_content_base_url.is_empty(): + realm_city_loader_content_base_url = Realm.ensure_ends_with_slash( + configuration.get("cityLoaderContentServer", "") + ) realm_name = configuration.get("realmName", "no_realm_name") diff --git a/godot/src/test/testing_tool.gd b/godot/src/test/testing_tool.gd new file mode 100644 index 00000000..342cf4b0 --- /dev/null +++ b/godot/src/test/testing_tool.gd @@ -0,0 +1,65 @@ +extends DclTestingTools +class_name TestingTools + +func take_and_compare_snapshot(id: String, camera_position: Vector3, camera_target: Vector3, snapshot_frame_size: Vector2, tolerance: float, dcl_rpc_sender: DclRpcSender): + prints("take_and_compare_snapshot", id, camera_position, camera_target, snapshot_frame_size, tolerance, dcl_rpc_sender) + + # TODO: make this configurable + var hide_player := true + var update_snapshot := false + var create_snapshot_if_does_not_exist := true + + var snapshot_path := "user://snapshot_" + id.replace(" ", "_") + ".png" + + var existing_snapshot: Image = null + if not update_snapshot and FileAccess.file_exists(snapshot_path): + existing_snapshot = Image.load_from_file(snapshot_path) + + + RenderingServer.set_default_clear_color(Color(0, 0, 0, 0)) + var viewport = get_viewport() + var camera = viewport.get_camera_3d() + var previous_camera_position = camera.global_position + var previous_camera_rotation = camera.global_rotation + var previous_viewport_size = viewport.size + + viewport.size = snapshot_frame_size + camera.global_position = camera_position + camera.look_at(camera_target) + + get_node("/root/explorer").set_visible_ui(false) + if hide_player: + get_node("/root/explorer/Player").hide() + + await get_tree().process_frame + await get_tree().process_frame + await get_tree().process_frame + + var viewport_img := viewport.get_texture().get_image() + + get_node("/root/explorer").set_visible_ui(true) + if hide_player: + get_node("/root/explorer/Player").show() + + viewport.size = previous_viewport_size + camera.global_position = previous_camera_position + camera.global_rotation = previous_camera_rotation + + var similarity := 0.0 + var updated := false + + if existing_snapshot != null: + similarity = self.compute_image_similarity(existing_snapshot, viewport_img) + prints("similarity factor ", similarity) + + if update_snapshot or (existing_snapshot == null and create_snapshot_if_does_not_exist): + viewport_img.save_png(snapshot_path) + updated = true + + dcl_rpc_sender.send({ + "is_match": similarity >= tolerance, + "similarity": similarity, + "was_exist": existing_snapshot != null, + "replaced": updated, + }) + diff --git a/godot/src/ui/explorer.gd b/godot/src/ui/explorer.gd index 5d26a5fb..e785911e 100644 --- a/godot/src/ui/explorer.gd +++ b/godot/src/ui/explorer.gd @@ -323,3 +323,11 @@ func capture_mouse(): func release_mouse(): Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) label_crosshair.hide() + +func set_visible_ui(value: bool): + if value: + $UI.show() + $voice_chat.show() + else: + $UI.hide() + $voice_chat.hide() diff --git a/godot/src/ui/explorer.tscn b/godot/src/ui/explorer.tscn index ea63e809..c0323f71 100644 --- a/godot/src/ui/explorer.tscn +++ b/godot/src/ui/explorer.tscn @@ -28,7 +28,7 @@ texture_filter = 0 [sub_resource type="Theme" id="Theme_1ufu0"] -[sub_resource type="ButtonGroup" id="ButtonGroup_5lp3f"] +[sub_resource type="ButtonGroup" id="ButtonGroup_nn4lh"] resource_name = "Tabs" [node name="explorer" type="Node3D"] @@ -173,7 +173,7 @@ layout_mode = 2 [node name="Control_Menu" parent="UI" instance=ExtResource("5_mso44")] visible = false layout_mode = 1 -group = SubResource("ButtonGroup_5lp3f") +group = SubResource("ButtonGroup_nn4lh") [node name="DialogStack" parent="UI" instance=ExtResource("10_y1lkn")] layout_mode = 1 @@ -196,6 +196,9 @@ grow_horizontal = 2 grow_vertical = 0 mouse_filter = 1 +[node name="CSGBox3D" type="CSGBox3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.30259, 1.22605, 2.26942) + [connection signal="timeout" from="UI/Timer_FPSLabel" to="." method="_on_timer_timeout"] [connection signal="request_open_map" from="UI/Control_Minimap" to="." method="_on_control_minimap_request_open_map"] [connection signal="submit_message" from="UI/LineEdit_Command" to="." method="_on_line_edit_command_submit_message"] diff --git a/rust/decentraland-godot-lib/src/dcl/js/js_modules/Testing.js b/rust/decentraland-godot-lib/src/dcl/js/js_modules/Testing.js index 2e346ec4..b4811ecd 100644 --- a/rust/decentraland-godot-lib/src/dcl/js/js_modules/Testing.js +++ b/rust/decentraland-godot-lib/src/dcl/js/js_modules/Testing.js @@ -11,10 +11,10 @@ function emptyTesting() { } function testingModule() { - function takeAndCompareSnapshot(body) { + async function takeAndCompareSnapshot(body) { const { id, cameraPosition, cameraTarget, snapshotFrameSize, tolerance } = body - return Deno.core.ops.op_take_and_compare_snapshot( + return await Deno.core.ops.op_take_and_compare_snapshot( id, [cameraPosition.x, cameraPosition.y, cameraPosition.z], [cameraTarget.x, cameraTarget.y, cameraTarget.z], diff --git a/rust/decentraland-godot-lib/src/dcl/js/mod.rs b/rust/decentraland-godot-lib/src/dcl/js/mod.rs index d978302e..847d0314 100644 --- a/rust/decentraland-godot-lib/src/dcl/js/mod.rs +++ b/rust/decentraland-godot-lib/src/dcl/js/mod.rs @@ -197,6 +197,12 @@ pub(crate) fn scene_thread( state.borrow_mut().put(Vec::::new()); state.borrow_mut().put(Vec::::new()); + // TODO: receive from main thread, and managed by command line params + state.borrow_mut().put(SceneEnv { + enable_know_env: true, + testing_enable: true, + }); + if let Some(scene_main_crdt) = scene_main_crdt { state.borrow_mut().put(scene_main_crdt); } @@ -212,7 +218,6 @@ pub(crate) fn scene_thread( .borrow_mut() .put(SceneStartTime(std::time::SystemTime::now())); - let script = runtime.execute_script("", ascii_str!("require (\"~scene.js\")")); let script = match script { diff --git a/rust/decentraland-godot-lib/src/dcl/js/testing.rs b/rust/decentraland-godot-lib/src/dcl/js/testing.rs index 8595c2f0..7b7506f6 100644 --- a/rust/decentraland-godot-lib/src/dcl/js/testing.rs +++ b/rust/decentraland-godot-lib/src/dcl/js/testing.rs @@ -1,3 +1,5 @@ +use std::{cell::RefCell, rc::Rc}; + use deno_core::{ anyhow::{self, anyhow}, error::AnyError, @@ -5,7 +7,7 @@ use deno_core::{ }; use godot::builtin::{Vector2, Vector3}; -use crate::dcl::{SceneResponse, TakeAndCompareSnapshotResponse}; +use crate::dcl::{SceneId, SceneResponse, TakeAndCompareSnapshotResponse}; use super::SceneEnv; @@ -14,14 +16,15 @@ pub fn ops() -> Vec { } #[op] -fn op_take_and_compare_snapshot( - state: &mut OpState, +async fn op_take_and_compare_snapshot( + op_state: Rc>, id: String, camera_position: [f32; 3], camera_target: [f32; 3], snapshot_frame_size: [f32; 2], tolerance: f32, ) -> Result { + let mut state = op_state.borrow_mut(); let scene_env = state.borrow::(); if !scene_env.testing_enable { return Err(anyhow::anyhow!("Testing mode not available")); @@ -30,19 +33,21 @@ fn op_take_and_compare_snapshot( let (sx, rx) = tokio::sync::oneshot::channel::>(); + let scene_id = *state.borrow::(); let sender = state.borrow_mut::>(); sender .send(SceneResponse::TakeSnapshot { + scene_id, id, camera_position: Vector3 { x: camera_position[0], y: camera_position[1], - z: camera_position[2], + z: -camera_position[2], }, camera_target: Vector3 { x: camera_target[0], y: camera_target[1], - z: camera_target[2], + z: -camera_target[2], }, snapshot_frame_size: Vector2 { x: snapshot_frame_size[0], @@ -52,8 +57,10 @@ fn op_take_and_compare_snapshot( response: sx.into(), }) .expect("error sending scene response!!"); + drop(state); - rx.blocking_recv() + let response = rx.await; + response .map_err(|e| anyhow::anyhow!(e))? .map_err(|e| anyhow!(e)) } diff --git a/rust/decentraland-godot-lib/src/dcl/mod.rs b/rust/decentraland-godot-lib/src/dcl/mod.rs index a2a1d98c..4714778a 100644 --- a/rust/decentraland-godot-lib/src/dcl/mod.rs +++ b/rust/decentraland-godot-lib/src/dcl/mod.rs @@ -4,7 +4,7 @@ pub mod js; pub mod scene_apis; pub mod serialization; -use godot::builtin::{Vector3, Vector2}; +use godot::builtin::{Vector2, Vector3}; use serde::Serialize; use crate::wallet::Wallet; @@ -61,6 +61,7 @@ pub enum SceneResponse { ), RemoveGodotScene(SceneId, Vec), TakeSnapshot { + scene_id: SceneId, id: String, camera_position: Vector3, camera_target: Vector3, @@ -74,13 +75,13 @@ pub enum SceneResponse { #[serde(rename_all = "camelCase")] pub struct TakeAndCompareSnapshotResponse { // true if the threshold was met, false otherwise or if it wasn't previously exist - is_match: bool, + pub is_match: bool, // from 0 to 1 how similar the snapshot taken is to the previous one - similarity: f32, + pub similarity: f32, // true if the snapshot already exists in the snapshot folder, false otherwise - was_exist: bool, + pub was_exist: bool, // true if the snapshot was created and saved, false otherwise - replaced: bool, + pub replaced: bool, } pub type SharedSceneCrdtState = Arc>; diff --git a/rust/decentraland-godot-lib/src/dcl/scene_apis.rs b/rust/decentraland-godot-lib/src/dcl/scene_apis.rs index f757ea2f..14fac4a9 100644 --- a/rust/decentraland-godot-lib/src/dcl/scene_apis.rs +++ b/rust/decentraland-godot-lib/src/dcl/scene_apis.rs @@ -45,7 +45,7 @@ pub struct UserData { pub avatar: Option, } -#[derive(Debug, Clone)] +#[derive(Default, Debug, Clone)] pub struct RpcResultSender(Arc>>>); impl RpcResultSender { diff --git a/rust/decentraland-godot-lib/src/godot_classes/dcl_global.rs b/rust/decentraland-godot-lib/src/godot_classes/dcl_global.rs index 3ae3b12f..49a58596 100644 --- a/rust/decentraland-godot-lib/src/godot_classes/dcl_global.rs +++ b/rust/decentraland-godot-lib/src/godot_classes/dcl_global.rs @@ -7,6 +7,7 @@ use crate::{ avatars::avatar_scene::AvatarScene, comms::communication_manager::CommunicationManager, scene_runner::{scene_manager::SceneManager, tokio_runtime::TokioRuntime}, + test_runner::testing_tools::DclTestingTools, }; use super::{dcl_realm::DclRealm, portables::DclPortableExperienceController}; @@ -28,6 +29,8 @@ pub struct DclGlobal { pub realm: Gd, #[var] pub portable_experience_controller: Gd, + #[var] + pub testing_tools: Gd, } #[godot_api] @@ -63,6 +66,7 @@ impl NodeVirtual for DclGlobal { tokio_runtime, realm: Gd::new_default(), portable_experience_controller: Gd::new_default(), + testing_tools: Gd::new_default(), } } } diff --git a/rust/decentraland-godot-lib/src/godot_classes/dcl_rpc_sender.rs b/rust/decentraland-godot-lib/src/godot_classes/dcl_rpc_sender.rs new file mode 100644 index 00000000..f608fcd8 --- /dev/null +++ b/rust/decentraland-godot-lib/src/godot_classes/dcl_rpc_sender.rs @@ -0,0 +1,66 @@ +use godot::engine::RefCounted; +use godot::prelude::*; + +use crate::dcl::scene_apis::RpcResultSender; +use crate::dcl::TakeAndCompareSnapshotResponse; + +#[derive(GodotClass)] +#[class(init, base=RefCounted)] +pub struct DclRpcSender { + sender: Option>>, + + #[base] + _base: Base, +} + +impl godot::builtin::meta::GodotConvert for TakeAndCompareSnapshotResponse { + type Via = Dictionary; +} + +impl FromGodot for TakeAndCompareSnapshotResponse { + fn try_from_godot(via: Dictionary) -> Option { + let is_match = via.get("is_match")?.to::(); + let similarity = via.get("similarity")?.to::(); + let was_exist = via.get("was_exist")?.to::(); + let replaced = via.get("replaced")?.to::(); + Some(Self { + is_match, + similarity, + was_exist, + replaced, + }) + } +} + +impl DclRpcSender { + pub fn set_sender( + &mut self, + sender: RpcResultSender>, + ) { + self.sender = Some(sender); + } +} + +#[godot_api] +impl DclRpcSender { + #[func] + fn send(&mut self, response: Variant) { + if let Some(sender) = self.sender.as_ref() { + let response = response.try_to::().unwrap(); + let response = TakeAndCompareSnapshotResponse::from_godot(response); + let sender: tokio::sync::oneshot::Sender< + Result, + > = sender.take(); + let ret = sender.send(Ok(response)); + + match ret { + Ok(_) => { + tracing::info!("Response sent"); + } + Err(e) => { + tracing::info!("Error sending response {:?}", e); + } + } + } + } +} diff --git a/rust/decentraland-godot-lib/src/godot_classes/mod.rs b/rust/decentraland-godot-lib/src/godot_classes/mod.rs index 7f758be9..24bf8c32 100644 --- a/rust/decentraland-godot-lib/src/godot_classes/mod.rs +++ b/rust/decentraland-godot-lib/src/godot_classes/mod.rs @@ -9,6 +9,7 @@ pub mod dcl_ether; pub mod dcl_global; pub mod dcl_gltf_container; pub mod dcl_realm; +pub mod dcl_rpc_sender; pub mod dcl_scene_node; pub mod dcl_ui_background; pub mod dcl_ui_control; diff --git a/rust/decentraland-godot-lib/src/scene_runner/scene_manager.rs b/rust/decentraland-godot-lib/src/scene_runner/scene_manager.rs index 4e74dfeb..5ed3a8cd 100644 --- a/rust/decentraland-godot-lib/src/scene_runner/scene_manager.rs +++ b/rust/decentraland-godot-lib/src/scene_runner/scene_manager.rs @@ -11,11 +11,12 @@ use crate::{ SceneEntityId, }, js::SceneLogLevel, - scene_apis::RpcResultSender, DclScene, RendererResponse, SceneDefinition, SceneId, SceneResponse, - TakeAndCompareSnapshotResponse, }, - godot_classes::{dcl_camera_3d::DclCamera3D, dcl_ui_control::DclUiControl}, + godot_classes::{ + dcl_camera_3d::DclCamera3D, dcl_global::DclGlobal, dcl_rpc_sender::DclRpcSender, + dcl_ui_control::DclUiControl, + }, wallet::Wallet, }; use godot::{engine::PhysicsRayQueryParameters3D, prelude::*}; @@ -73,9 +74,6 @@ pub struct SceneManager { input_state: InputState, last_raycast_result: Option, - snapshot_sender: - HashMap>>, - #[export] pointer_tooltips: VariantArray, } @@ -465,13 +463,44 @@ impl SceneManager { } SceneResponse::TakeSnapshot { + scene_id, id, camera_position, camera_target, snapshot_frame_size, tolerance, response, - } => {} + } => { + let offset = if let Some(scene) = self.scenes.get(&scene_id) { + Vector3::new( + scene.definition.base.x as f32 * 16.0, + 0.0, + -scene.definition.base.y as f32 * 16.0, + ) + } else { + Vector3::new(0.0, 0.0, 0.0) + }; + + let mut testing_tools = DclGlobal::singleton().bind().get_testing_tools(); + if testing_tools.has_method("take_and_compare_snapshot".into()) { + let mut dcl_rpc_sender: Gd = Gd::new_default(); + dcl_rpc_sender.bind_mut().set_sender(response); + + testing_tools.call( + "take_and_compare_snapshot".into(), + &[ + id.to_variant(), + (camera_position + offset).to_variant(), + (camera_position + camera_target).to_variant(), + snapshot_frame_size.to_variant(), + tolerance.to_variant(), + dcl_rpc_sender.to_variant(), + ], + ); + } else { + response.send(Err("Testing tools not available".to_string())); + } + } }, Err(std::sync::mpsc::TryRecvError::Empty) => return, Err(std::sync::mpsc::TryRecvError::Disconnected) => { @@ -667,8 +696,6 @@ impl NodeVirtual for SceneManager { input_state: InputState::default(), last_raycast_result: None, pointer_tooltips: VariantArray::new(), - - snapshot_sender: HashMap::new(), } } diff --git a/rust/decentraland-godot-lib/src/test_runner/mod.rs b/rust/decentraland-godot-lib/src/test_runner/mod.rs index 0953eed7..913537a9 100644 --- a/rust/decentraland-godot-lib/src/test_runner/mod.rs +++ b/rust/decentraland-godot-lib/src/test_runner/mod.rs @@ -1 +1,2 @@ pub mod test_suite; +pub mod testing_tools; diff --git a/rust/decentraland-godot-lib/src/test_runner/testing_tools.rs b/rust/decentraland-godot-lib/src/test_runner/testing_tools.rs new file mode 100644 index 00000000..2adb8c21 --- /dev/null +++ b/rust/decentraland-godot-lib/src/test_runner/testing_tools.rs @@ -0,0 +1,65 @@ +use ethers::core::k256::elliptic_curve::consts::U9; +use godot::engine::{Image, Node}; +use godot::prelude::*; + +#[derive(GodotClass)] +#[class(init, base=Node)] +pub struct DclTestingTools { + #[base] + _base: Base, +} + +fn to_gray(rgb: &[u8]) -> f32 { + let r = rgb[0]; + let g = rgb[1]; + let b = rgb[2]; + + ((0.299 * f32::from(r) + 0.587 * f32::from(g) + 0.114 * f32::from(b)) / u8::MAX as f32).round() + as f32 +} + +#[godot_api] +impl DclTestingTools { + #[func] + fn compute_image_similarity(&self, mut img_a: Gd, mut img_b: Gd) -> f64 { + if img_a.get_width() != img_b.get_width() || img_a.get_height() != img_b.get_height() { + tracing::info!("compute_image_similarity have different sizes"); + return 0.0; + } + + let width = img_a.get_width() as usize; + let height = img_a.get_height() as usize; + let pixel_count = width * height; + + if img_a.get_format() != godot::engine::image::Format::FORMAT_RGB8 { + img_a.convert(godot::engine::image::Format::FORMAT_RGB8); + } + + if img_b.get_format() != godot::engine::image::Format::FORMAT_RGB8 { + img_b.convert(godot::engine::image::Format::FORMAT_RGB8); + } + + let a_data = img_a.get_data(); + let b_data = img_b.get_data(); + let data_a = a_data.as_slice(); + let data_b = b_data.as_slice(); + let mut data_diff = Vec::with_capacity(pixel_count); + + for pixel_index in 0..pixel_count { + let index = pixel_index * 3; + data_diff.push( + 1.0 - (to_gray(&data_b[index..index + 3]) - to_gray(&data_a[index..index + 3])) + .abs(), + ); + } + let score: f64 = 1. + - (data_diff + .iter() + .map(|p| (1. - *p as f64).powi(2)) + .sum::() + / (pixel_count as f64)) + .sqrt(); + + score + } +}