From 54760cac784da16ebff30d564c1f2c07d2264eb7 Mon Sep 17 00:00:00 2001 From: Lean Mendoza Date: Mon, 18 Sep 2023 15:55:18 -0300 Subject: [PATCH] feat: implement websocket and update godot-rust (#45) * wip, basic websocket implementation works * fix weboscket, cargo update (update gdext) * fix format * copy ffmpeg dlls * add TODO for insecure fetch and websocket fix change realm crash * fix access to properties websocket --- .github/workflows/ci.yml | 1 + godot/.godot/global_script_class_cache.cfg | 6 + godot/src/test/avatar/spawn_and_move.gd | 95 +++++++ rust/decentraland-godot-lib/Cargo.toml | 5 +- .../src/av/ffmpeg_util.rs | 2 +- .../src/av/video_context.rs | 4 +- .../src/av/video_stream.rs | 4 +- .../src/avatars/avatar_scene.rs | 84 ++++-- .../src/comms/communication_manager.rs | 4 +- .../src/comms/wallet.rs | 4 +- .../src/comms/ws_room.rs | 14 +- .../src/dcl/crdt/message.rs | 26 +- .../src/dcl/js/js_modules/fetch.js | 31 ++- .../src/dcl/js/js_modules/main.js | 22 +- .../src/dcl/js/js_modules/ws.js | 193 +++++++++++++ rust/decentraland-godot-lib/src/dcl/js/mod.rs | 42 ++- .../src/dcl/js/websocket/mod.rs | 260 ++++++++++++++++++ rust/decentraland-godot-lib/src/lib.rs | 80 +++--- .../src/realm/scene_entity_coordinator.rs | 27 +- .../scene_runner/components/avatar_shape.rs | 2 +- .../src/scene_runner/components/billboard.rs | 12 +- .../scene_runner/components/gltf_container.rs | 2 +- .../src/scene_runner/components/material.rs | 4 +- .../src/scene_runner/components/raycast.rs | 6 +- .../components/transform_and_parent.rs | 8 +- .../scene_runner/components/video_player.rs | 4 +- .../src/scene_runner/godot_dcl_scene.rs | 32 ++- .../src/scene_runner/scene_manager.rs | 14 +- .../src/scene_runner/update_scene.rs | 6 +- .../src/test_runner/test_suite.rs | 4 +- rust/xtask/src/consts.rs | 13 +- rust/xtask/src/install_dependency.rs | 7 +- rust/xtask/src/main.rs | 4 + 33 files changed, 871 insertions(+), 151 deletions(-) create mode 100644 godot/src/test/avatar/spawn_and_move.gd create mode 100644 rust/decentraland-godot-lib/src/dcl/js/js_modules/ws.js create mode 100644 rust/decentraland-godot-lib/src/dcl/js/websocket/mod.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8352fe664..0b5417180 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -235,6 +235,7 @@ jobs: if: ${{ matrix.os == 'windows-latest' }} run: | cp rust/decentraland-godot-lib/target/release/decentraland_godot_lib.dll godot/lib/ + cp .bin/ffmpeg/ffmpeg-6.0-full_build-shared/bin/*.dll godot/lib/ # Export section (multi platform) - name: Export diff --git a/godot/.godot/global_script_class_cache.cfg b/godot/.godot/global_script_class_cache.cfg index c8afdcdc8..b150f3c2c 100644 --- a/godot/.godot/global_script_class_cache.cfg +++ b/godot/.godot/global_script_class_cache.cfg @@ -53,6 +53,12 @@ list=Array[Dictionary]([{ "language": &"GDScript", "path": "res://src/logic/rust_http_requester_wrapper.gd" }, { +"base": &"Node", +"class": &"TestSpawnAndMoveAvatars", +"icon": "", +"language": &"GDScript", +"path": "res://src/test/avatar/spawn_and_move.gd" +}, { "base": &"Control", "class": &"VirtualJoystick", "icon": "", diff --git a/godot/src/test/avatar/spawn_and_move.gd b/godot/src/test/avatar/spawn_and_move.gd new file mode 100644 index 000000000..64eed65ec --- /dev/null +++ b/godot/src/test/avatar/spawn_and_move.gd @@ -0,0 +1,95 @@ +extends Node +class_name TestSpawnAndMoveAvatars + +var test_avatar_N: int = 100 +var moving_avatars: bool = false +var moving_t: float = 0 +var spawning_avatars: bool = false +var spawning_i: int = 0 +var spawning_position: Array[Vector3] = [] + +var wearable_data = {} + + +func set_wearable_data(_wearable_data): + wearable_data = _wearable_data + + +func get_random_body(): + var to_pick = [] + for wearable_id in wearable_data: + var wearable = wearable_data[wearable_id] + if Wearables.get_category(wearable) == Wearables.Categories.BODY_SHAPE: + to_pick.push_back(wearable_id) + + return to_pick.pick_random() + + +func get_random_wearable(category: String, body_shape_id: String): + var to_pick = [] + for wearable_id in wearable_data: + var wearable = wearable_data[wearable_id] + if Wearables.get_category(wearable) == category: + if Wearables.can_equip(wearable, body_shape_id): + to_pick.push_back(wearable_id) + + return to_pick.pick_random() + + +func _process(dt): + var avatar_scene: AvatarScene = get_node("/root/avatars") + if spawning_avatars and spawning_i < test_avatar_N: + var body_shape_id = get_random_body() + var avatar_data = { + "base_url": "https://peer.decentraland.org/content", + "name": "Avatar#" + str(spawning_i), + "body_shape": body_shape_id, + "eyes": Color(randf(), randf(), randf()), + "hair": Color(randf(), randf(), randf()), + "skin": Color(0.8, 0.6078, 0.4667, 1), + "wearables": + [ + get_random_wearable(Wearables.Categories.MOUTH, body_shape_id), + get_random_wearable(Wearables.Categories.HAIR, body_shape_id), + get_random_wearable(Wearables.Categories.UPPER_BODY, body_shape_id), + get_random_wearable(Wearables.Categories.LOWER_BODY, body_shape_id), + get_random_wearable(Wearables.Categories.FEET, body_shape_id), + get_random_wearable(Wearables.Categories.EYES, body_shape_id), + ], + "emotes": [] + } + + var initial_position := ( + Vector3(randf_range(-10, 10), 0.0, randf_range(-10, 10)).normalized() + ) + var transform = Transform3D(Basis.IDENTITY, initial_position) + var alias = 10000 + spawning_i + avatar_scene.add_avatar(alias) + avatar_scene.update_avatar_profile(alias, avatar_data) + avatar_scene.update_avatar_transform(alias, transform) + + spawning_position.append(initial_position) + + spawning_i = spawning_i + 1 + if spawning_i >= test_avatar_N: + spawning_avatars = false + moving_avatars = true + elif moving_avatars: + moving_t += dt + if moving_t >= 0.1: + var WALK_SPEED = 2.0 + var walk_delta = WALK_SPEED * moving_t + moving_t = 0 + for i in range(spawning_position.size()): + var delta_position = ( + Vector3(randf_range(-1, 1), 0.0, randf_range(-1, 1)).normalized() * walk_delta + ) + var current_position = spawning_position[i] + var target_position = current_position + delta_position + var transform = Transform3D(Basis.IDENTITY, current_position) + transform = transform.looking_at(target_position) + transform.origin = target_position + + spawning_position[i] = target_position + var alias = 10000 + i + avatar_scene.update_avatar_transform(alias, transform) diff --git a/rust/decentraland-godot-lib/Cargo.toml b/rust/decentraland-godot-lib/Cargo.toml index a9ac178c9..70f68f42c 100644 --- a/rust/decentraland-godot-lib/Cargo.toml +++ b/rust/decentraland-godot-lib/Cargo.toml @@ -8,7 +8,7 @@ publish = false crate-type = ["cdylib"] [dependencies] -godot = { git = "https://github.com/godot-rust/gdext", rev = "88d22f801a11e34243890b4f3d4530a6e368b0e1", features = ["threads"] } +godot = { git = "https://github.com/godot-rust/gdext", rev = "00ba8c9d", features = ["threads"] } rand = "0.8" serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0.92", features = ["raw_value"] } @@ -45,5 +45,8 @@ num = "0.4" http = "0.2.9" bytes = "1.4.0" +tokio-tungstenite = "*" +futures-util = "*" + [build-dependencies] prost-build = "0.11.8" diff --git a/rust/decentraland-godot-lib/src/av/ffmpeg_util.rs b/rust/decentraland-godot-lib/src/av/ffmpeg_util.rs index d47a28272..04fdabdae 100644 --- a/rust/decentraland-godot-lib/src/av/ffmpeg_util.rs +++ b/rust/decentraland-godot-lib/src/av/ffmpeg_util.rs @@ -102,7 +102,7 @@ impl PacketIter for InputWrapper { fn reset(&mut self) { let Some(input) = self.get_input(false) else { - return + return; }; if input.seek(0, ..).is_err() { diff --git a/rust/decentraland-godot-lib/src/av/video_context.rs b/rust/decentraland-godot-lib/src/av/video_context.rs index 407c76018..6bfc32c89 100644 --- a/rust/decentraland-godot-lib/src/av/video_context.rs +++ b/rust/decentraland-godot-lib/src/av/video_context.rs @@ -7,7 +7,7 @@ use ffmpeg_next::software::scaling::{context::Context, flag::Flags}; use ffmpeg_next::{decoder, format::context::Input, media::Type, util::frame, Packet}; use godot::engine::image::Format; use godot::engine::{Image, ImageTexture}; -use godot::prelude::{Gd, PackedByteArray, Share, Vector2}; +use godot::prelude::{Gd, PackedByteArray, Vector2}; use thiserror::Error; use tracing::debug; @@ -189,7 +189,7 @@ impl FfmpegContext for VideoContext { data_arr, ) .unwrap(); - self.texture.set_image(image.share()); + self.texture.set_image(image.clone()); self.texture.update(image); } else { let mut image = self.texture.get_image().unwrap(); diff --git a/rust/decentraland-godot-lib/src/av/video_stream.rs b/rust/decentraland-godot-lib/src/av/video_stream.rs index eba5c5549..b5d5ab1f7 100644 --- a/rust/decentraland-godot-lib/src/av/video_stream.rs +++ b/rust/decentraland-godot-lib/src/av/video_stream.rs @@ -1,7 +1,7 @@ use ffmpeg_next::format::input; use godot::{ engine::ImageTexture, - prelude::{AudioStreamPlayer, Gd, InstanceId, Share}, + prelude::{AudioStreamPlayer, Gd, InstanceId}, }; use tracing::{debug, warn}; @@ -43,7 +43,7 @@ pub fn av_sinks( // video_sender, // audio_sender, source.clone(), - tex.share(), + tex.clone(), audio_stream_player, ); diff --git a/rust/decentraland-godot-lib/src/avatars/avatar_scene.rs b/rust/decentraland-godot-lib/src/avatars/avatar_scene.rs index c36c4c376..193551de3 100644 --- a/rust/decentraland-godot-lib/src/avatars/avatar_scene.rs +++ b/rust/decentraland-godot-lib/src/avatars/avatar_scene.rs @@ -41,6 +41,69 @@ impl NodeVirtual for AvatarScene { } } +#[godot_api] +impl AvatarScene { + #[func] + pub fn update_avatar_profile(&mut self, alias: u32, profile: Dictionary) { + let entity_id = if let Some(entity_id) = self.avatar_entity.get(&alias) { + *entity_id + } else { + // TODO: handle this condition + return; + }; + + self.avatar_godot_scene + .get_mut(&entity_id) + .unwrap() + .call("update_avatar".into(), &[profile.to_variant()]); + } + + #[func] + pub fn update_avatar_transform(&mut self, alias: u32, transform: Transform3D) { + let entity_id = if let Some(entity_id) = self.avatar_entity.get(&alias) { + *entity_id + } else { + // TODO: handle this condition + return; + }; + + let dcl_transform = DclTransformAndParent::from_godot(&transform, Vector3::ZERO); + + // let avatar_scene = self.avatar_godot_scene.get_mut(&entity_id).unwrap(); + + // // TODO: the scale seted in the transform is local + // avatar_scene.set_transform(dcl_transform.to_godot_transform_3d()); + self.avatar_godot_scene.get_mut(&entity_id).unwrap().call( + "set_target".into(), + &[dcl_transform.to_godot_transform_3d().to_variant()], + ); + + self.crdt + .get_transform_mut() + .put(entity_id, Some(dcl_transform)); + } + + #[func] + pub fn add_avatar(&mut self, alias: u32) { + // TODO: the entity Self::MAX_ENTITY_ID + 1 would be a buggy avatar + let entity_id = self + .get_next_entity_id() + .unwrap_or(SceneEntityId::new(Self::MAX_ENTITY_ID + 1, 0)); + self.crdt.entities.try_init(entity_id); + + self.avatar_entity.insert(alias, entity_id); + + let new_avatar = + godot::engine::load::("res://src/decentraland_components/avatar.tscn") + .instantiate() + .unwrap() + .cast::(); + + self.base.add_child(new_avatar.clone().upcast()); + self.avatar_godot_scene.insert(entity_id, new_avatar); + } +} + impl AvatarScene { const FROM_ENTITY_ID: u16 = 10; const MAX_ENTITY_ID: u16 = 200; @@ -69,30 +132,11 @@ impl AvatarScene { } } - pub fn add_avatar(&mut self, alias: u32) { - // TODO: the entity Self::MAX_ENTITY_ID + 1 would be a buggy avatar - let entity_id = self - .get_next_entity_id() - .unwrap_or(SceneEntityId::new(Self::MAX_ENTITY_ID + 1, 0)); - self.crdt.entities.try_init(entity_id); - - self.avatar_entity.insert(alias, entity_id); - - let new_avatar = - godot::engine::load::("res://src/decentraland_components/avatar.tscn") - .instantiate() - .unwrap() - .cast::(); - - self.base.add_child(new_avatar.share().upcast()); - self.avatar_godot_scene.insert(entity_id, new_avatar); - } - pub fn remove_avatar(&mut self, alias: u32) { if let Some(entity_id) = self.avatar_entity.remove(&alias) { self.crdt.kill_entity(&entity_id); let mut avatar = self.avatar_godot_scene.remove(&entity_id).unwrap(); - self.base.remove_child(avatar.share().upcast()); + self.base.remove_child(avatar.clone().upcast()); avatar.queue_free(); } } diff --git a/rust/decentraland-godot-lib/src/comms/communication_manager.rs b/rust/decentraland-godot-lib/src/comms/communication_manager.rs index b9c3b1c9c..849291d0f 100644 --- a/rust/decentraland-godot-lib/src/comms/communication_manager.rs +++ b/rust/decentraland-godot-lib/src/comms/communication_manager.rs @@ -128,7 +128,7 @@ impl CommunicationManager { fn init_rs(&mut self) { let mut realm = self.realm(); let on_realm_changed = - Callable::from_object_method(self.base.share(), StringName::from("_on_realm_changed")); + Callable::from_object_method(self.base.clone(), StringName::from("_on_realm_changed")); realm.connect("realm_changed".into(), on_realm_changed); @@ -200,7 +200,7 @@ impl CommunicationManager { tracing::info!("comms > websocket to {}", ws_url); self.current_adapter = Adapter::WsRoom(WebSocketRoom::new( ws_url, - self.tls_client.as_ref().unwrap().share(), + self.tls_client.as_ref().unwrap().clone(), self.player_identity.clone(), avatar_scene, )); diff --git a/rust/decentraland-godot-lib/src/comms/wallet.rs b/rust/decentraland-godot-lib/src/comms/wallet.rs index 54d5d84a2..7af54e2c9 100644 --- a/rust/decentraland-godot-lib/src/comms/wallet.rs +++ b/rust/decentraland-godot-lib/src/comms/wallet.rs @@ -104,7 +104,9 @@ impl AsH160 for &str { return (&self[2..]).as_h160(); } - let Ok(hex_bytes) = hex::decode(self.as_bytes()) else { return None }; + let Ok(hex_bytes) = hex::decode(self.as_bytes()) else { + return None; + }; if hex_bytes.len() != H160::len_bytes() { return None; } diff --git a/rust/decentraland-godot-lib/src/comms/ws_room.rs b/rust/decentraland-godot-lib/src/comms/ws_room.rs index 9cf5726db..87d456188 100644 --- a/rust/decentraland-godot-lib/src/comms/ws_room.rs +++ b/rust/decentraland-godot-lib/src/comms/ws_room.rs @@ -152,12 +152,12 @@ impl WebSocketRoom { return false; } - let buf = PackedByteArray::from_iter(buf.into_iter()); + let buf = PackedByteArray::from_iter(buf); matches!(self.ws_peer.send(buf), godot::engine::global::Error::OK) } pub fn poll(&mut self) { - let mut peer = self.ws_peer.share(); + let mut peer = self.ws_peer.clone(); peer.poll(); let ws_state = peer.get_ready_state(); @@ -167,7 +167,7 @@ impl WebSocketRoom { godot::engine::web_socket_peer::State::STATE_CLOSED => { if (Instant::now() - self.last_try_to_connect).as_secs() > 1 { // TODO: see if the tls client is really required for now - let _tls_client = self.tls_client.share(); + let _tls_client = self.tls_client.clone(); let ws_protocols = { let mut v = PackedStringArray::new(); @@ -213,7 +213,7 @@ impl WebSocketRoom { }, WsRoomState::IdentMessageSent => match ws_state { godot::engine::web_socket_peer::State::STATE_OPEN => { - while let Some((packet_length, message)) = get_next_packet(peer.share()) { + while let Some((packet_length, message)) = get_next_packet(peer.clone()) { match message { ws_packet::Message::ChallengeMessage(challenge_msg) => { tracing::info!("comms > peer msg {:?}", challenge_msg); @@ -269,7 +269,7 @@ impl WebSocketRoom { }, WsRoomState::ChallengeMessageSent => match ws_state { godot::engine::web_socket_peer::State::STATE_OPEN => { - while let Some((packet_length, message)) = get_next_packet(peer.share()) { + while let Some((packet_length, message)) = get_next_packet(peer.clone()) { match message { ws_packet::Message::WelcomeMessage(welcome_msg) => { // welcome_msg. @@ -347,7 +347,7 @@ impl WebSocketRoom { } pub fn clean(&self) { - let mut peer = self.ws_peer.share(); + let mut peer = self.ws_peer.clone(); peer.close(); match peer.get_ready_state() { godot::engine::web_socket_peer::State::STATE_OPEN @@ -359,7 +359,7 @@ impl WebSocketRoom { } fn _handle_messages(&mut self) { - while let Some((_packet_length, message)) = get_next_packet(self.ws_peer.share()) { + while let Some((_packet_length, message)) = get_next_packet(self.ws_peer.clone()) { match message { ws_packet::Message::ChallengeMessage(_) | ws_packet::Message::PeerIdentification(_) diff --git a/rust/decentraland-godot-lib/src/dcl/crdt/message.rs b/rust/decentraland-godot-lib/src/dcl/crdt/message.rs index 63f669678..180ef78d2 100644 --- a/rust/decentraland-godot-lib/src/dcl/crdt/message.rs +++ b/rust/decentraland-godot-lib/src/dcl/crdt/message.rs @@ -36,8 +36,10 @@ fn process_message( if !scene_crdt_state.entities.try_init(entity) { return Ok(()); } - let Some(component_definition) = scene_crdt_state.get_lww_component_definition_mut(component) else { - return Ok(()) + let Some(component_definition) = + scene_crdt_state.get_lww_component_definition_mut(component) + else { + return Ok(()); }; component_definition.set_from_binary(entity, timestamp, stream); @@ -50,8 +52,10 @@ fn process_message( if !scene_crdt_state.entities.try_init(entity) { return Ok(()); } - let Some(component_definition) = scene_crdt_state.get_lww_component_definition_mut(component) else { - return Ok(()) + let Some(component_definition) = + scene_crdt_state.get_lww_component_definition_mut(component) + else { + return Ok(()); }; component_definition.set_none(entity, timestamp); @@ -67,8 +71,10 @@ fn process_message( if !scene_crdt_state.entities.try_init(entity) { return Ok(()); } - let Some(component_definition) = scene_crdt_state.get_gos_component_definition_mut(component) else { - return Ok(()) + let Some(component_definition) = + scene_crdt_state.get_gos_component_definition_mut(component) + else { + return Ok(()); }; component_definition.append_from_binary(entity, stream); @@ -105,10 +111,11 @@ pub fn put_or_delete_lww_component( component_id: &SceneComponentId, writer: &mut DclWriter, ) -> Result<(), String> { - let Some(component_definition) = scene_crdt_state.get_lww_component_definition(*component_id) else { + let Some(component_definition) = scene_crdt_state.get_lww_component_definition(*component_id) + else { return Err("Component not found".into()); }; - let Some(opaque_value)= component_definition.get_opaque(*entity_id) else { + let Some(opaque_value) = component_definition.get_opaque(*entity_id) else { return Err("Entity not found".into()); }; @@ -148,7 +155,8 @@ pub fn append_gos_component( elements_count: usize, writer: &mut DclWriter, ) -> Result<(), String> { - let Some(component_definition) = scene_crdt_state.get_gos_component_definition(*component_id) else { + let Some(component_definition) = scene_crdt_state.get_gos_component_definition(*component_id) + else { return Err("Component not found".into()); }; diff --git a/rust/decentraland-godot-lib/src/dcl/js/js_modules/fetch.js b/rust/decentraland-godot-lib/src/dcl/js/js_modules/fetch.js index c5452a72c..2c807cf0b 100644 --- a/rust/decentraland-godot-lib/src/dcl/js/js_modules/fetch.js +++ b/rust/decentraland-godot-lib/src/dcl/js/js_modules/fetch.js @@ -36,7 +36,31 @@ // declare function fetch(url: string, init?: RequestInit): Promise -module.exports.fetch = async function (url, init) { + +async function restrictedFetch(url, init) { + const canUseFetch = true // TODO: this should be exposed from Deno.env + const previewMode = true // TODO: this should be exposed from Deno.env + + if (url.toLowerCase().substr(0, 8) !== "https://") { + if (previewMode) { + console.log( + "⚠️ Warning: Can't make an unsafe http request in deployed scenes, please consider upgrading to https. url=" + + url + ) + } else { + return Promise.reject(new Error("Can't make an unsafe http request, please upgrade to https. url=" + url)) + } + } + + if (!canUseFetch) { + return Promise.reject(new Error("This scene is not allowed to use fetch.")) + } + + return await fetch(url, init) +} + + +async function fetch(url, init) { const { body, headers, method, redirect, timeout } = init ?? {} const hasBody = typeof body === 'string' const reqMethod = method ?? 'GET' @@ -48,6 +72,8 @@ module.exports.fetch = async function (url, init) { reqMethod, url, reqHeaders, hasBody, body ?? '', reqRedirect, reqTimeout ) const reqId = response._internal_req_id + console.log({ reqTimeout }) + // TODO: the headers object should be read-only Object.assign(response, { async arrayBuffer() { @@ -81,4 +107,5 @@ module.exports.fetch = async function (url, init) { }) return response -} \ No newline at end of file +} +module.exports.fetch = restrictedFetch \ No newline at end of file diff --git a/rust/decentraland-godot-lib/src/dcl/js/js_modules/main.js b/rust/decentraland-godot-lib/src/dcl/js/js_modules/main.js index f122b6935..1a3b03801 100644 --- a/rust/decentraland-godot-lib/src/dcl/js/js_modules/main.js +++ b/rust/decentraland-godot-lib/src/dcl/js/js_modules/main.js @@ -84,13 +84,25 @@ function logValue(value, seen) { // minimal console const console = { log: function (...args) { - Deno.core.ops.op_log("LOG " + customLog(...args)) + Deno.core.ops.op_log("LOG " + customLog(...args), false) }, error: function (...args) { - Deno.core.ops.op_error("ERROR " + customLog(...args)) + Deno.core.ops.op_error("ERROR " + customLog(...args), false) }, warn: function (...args) { - Deno.core.ops.op_log("WARN " + customLog(...args)) + Deno.core.ops.op_log("WARN " + customLog(...args), false) + }, +} + +const _internal_console = { + log: function (...args) { + Deno.core.ops.op_log("LOG " + customLog(...args), true) + }, + error: function (...args) { + Deno.core.ops.op_error("ERROR " + customLog(...args), true) + }, + warn: function (...args) { + Deno.core.ops.op_log("WARN " + customLog(...args), true) }, } @@ -99,5 +111,7 @@ globalThis.setImmediate = (fn) => Promise.resolve().then(fn) globalThis.require = require; globalThis.console = console; +globalThis._internal_console = _internal_console; -globalThis.fetch = require('fetch').fetch; \ No newline at end of file +globalThis.fetch = require('fetch').fetch; +globalThis.WebSocket = require('ws').WebSocket; \ No newline at end of file diff --git a/rust/decentraland-godot-lib/src/dcl/js/js_modules/ws.js b/rust/decentraland-godot-lib/src/dcl/js/js_modules/ws.js new file mode 100644 index 000000000..101f8a7a4 --- /dev/null +++ b/rust/decentraland-godot-lib/src/dcl/js/js_modules/ws.js @@ -0,0 +1,193 @@ + +// /// --- WebSocket --- + +// interface Event { +// readonly type: string +// } + +// interface MessageEvent extends Event { +// /** +// * Returns the data of the message. +// */ +// readonly data: any +// } + +// interface CloseEvent extends Event { +// readonly code: number +// readonly reason: string +// readonly wasClean: boolean +// } + +// interface WebSocket { +// readonly bufferedAmount: number +// readonly extensions: string +// onclose: ((this: WebSocket, ev: CloseEvent) => any) | null +// onerror: ((this: WebSocket, ev: Event) => any) | null +// onmessage: ((this: WebSocket, ev: MessageEvent) => any) | null +// onopen: ((this: WebSocket, ev: Event) => any) | null +// readonly protocol: string +// readonly readyState: number +// readonly url: string +// close(code?: number, reason?: string): void +// send(data: string): void +// readonly CLOSED: number +// readonly CLOSING: number +// readonly CONNECTING: number +// readonly OPEN: number +// } + +// declare var WebSocket: { +// prototype: WebSocket +// new (url: string, protocols?: string | string[]): WebSocket +// readonly CLOSED: number +// readonly CLOSING: number +// readonly CONNECTING: number +// readonly OPEN: number +// } + +class WebSocket { + static CLOSED = 1 + static CLOSING = 2 + static CONNECTING = 3 + static OPEN = 4 + + constructor(url, protocols) { + this.url = url + this.protocols = protocols + + this._readyState = WebSocket.CONNECTING + + this._internal_ws_id = Deno.core.ops.op_ws_create( + url, protocols ?? [] + ) + + this.onclose = null + this.onerror = null + this.onmessage = null + this.onopen = null + + this._poll().then(console.warn).catch(console.error) + } + + // There is no send buffer here + get bufferedAmount() { + return 0 + } + + get readyState() { + return this._readyState + } + + get binaryType() { + return "arraybuffer" + } + + // TODO: implement + get protocol() { + return "" + } + + // TODO: implement + get extensions() { + return "" + } + + send(data) { + if (typeof data === 'string') { + Deno.core.opAsync("op_ws_send", this._internal_ws_id, { "type": "Text", data }).then().catch(console.error) + } else if (typeof data === 'object' && data instanceof Uint8Array) { + Deno.core.opAsync("op_ws_send", this._internal_ws_id, { "type": "Binary", data: Array.from(data) }).then().catch(console.error) + } + } + + // TODO: add code and reason + close(code, reason) { + if (this._readyState != WebSocket.CLOSED) { + Deno.core.ops.op_ws_send(this._internal_ws_id, { "type": "Close" }) + this._readyState = WebSocket.CLOSED + } + } + + async _poll() { + const self = this + async function poll_from_native() { + const data = await Deno.core.opAsync( + "op_ws_poll", self._internal_ws_id + ) + + switch (data.type) { + case "BinaryData": + if (typeof self.onmessage === 'function') { + self.onmessage({ type: "binary", data: data.data }) + } + break + case "TextData": + if (typeof self.onmessage === 'function') { + self.onmessage({ type: "text", data: data.data }) + } + break + case "Connected": + if (typeof self.onopen === 'function') { + self.onopen({ type: "open" }) + } + break + case "Closed": + if (typeof self.onclose === 'function') { + self.onclose({ type: "close" }) + } + return false; + default: + throw new Error("unreached") + } + return true + } + + try { + while (true) { + if (!(await poll_from_native())) { + break + } + } + } catch (err) { + if (typeof this.onerror === 'function') { + this.onerror(err) + } + } + + this._readyState = WebSocket.CLOSED + Deno.core.ops.op_ws_cleanup(this._internal_ws_id) + } +} + +class RestrictedWebSocket extends WebSocket { + constructor(url, protocols) { + const previewMode = true // TODO: this should be exposed from Deno.env + const canUseWebsocket = true // TODO: this should be exposed from Deno.env + + if (url.toString().toLowerCase().substr(0, 4) !== 'wss:') { + if (previewMode) { + console.log( + "⚠️ Warning: can't connect to unsafe WebSocket (ws) server in deployed scenes, consider upgrading to wss." + ) + } else { + throw new Error("Can't connect to unsafe WebSocket server") + } + } + + if (!canUseWebsocket) { + throw new Error("This scene doesn't have allowed to use WebSocket") + } + + let realProtocols = [] + if (typeof protocols === 'string') { + realProtocols = [protocols] + } else if (Array.isArray(protocols)) { + realProtocols = protocols + } + + super(url.toString(), realProtocols) + } +} + + +module.exports.WebSocket = RestrictedWebSocket \ No newline at end of file diff --git a/rust/decentraland-godot-lib/src/dcl/js/mod.rs b/rust/decentraland-godot-lib/src/dcl/js/mod.rs index 296bd2745..524d17838 100644 --- a/rust/decentraland-godot-lib/src/dcl/js/mod.rs +++ b/rust/decentraland-godot-lib/src/dcl/js/mod.rs @@ -1,6 +1,7 @@ pub mod engine; pub mod fetch; pub mod runtime; +pub mod websocket; use super::{ crdt::message::process_many_messages, serialization::reader::DclReader, SceneDefinition, @@ -13,6 +14,7 @@ use std::collections::HashMap; use std::rc::Rc; use std::time::Duration; +use deno_core::error::JsError; use deno_core::{ ascii_str, error::{generic_error, AnyError}, @@ -55,13 +57,16 @@ pub fn create_runtime() -> deno_core::JsRuntime { // add core ops ext = ext.ops(vec![op_require::DECL, op_log::DECL, op_error::DECL]); - let op_sets: [Vec; 3] = [engine::ops(), runtime::ops(), fetch::ops()]; + let op_sets: [Vec; 4] = [ + engine::ops(), + runtime::ops(), + fetch::ops(), + websocket::ops(), + ]; - // add plugin registrations let mut op_map = HashMap::new(); for set in op_sets { for op in &set { - // explicitly record the ones we added so we can remove deno_fetch imposters op_map.insert(op.name, *op); } ext = ext.ops(set) @@ -224,7 +229,21 @@ pub(crate) fn scene_thread( }); if let Err(e) = result { - tracing::error!("[scene thread {scene_id:?}] script error onUpdate: {}", e); + let err_str = format!("{:?}", e); + if let Ok(err) = e.downcast::() { + tracing::error!( + "[scene thread {scene_id:?}] script error onUpdate: {} msg {:?} @ {:?}", + err_str, + err.message, + err + ); + } else { + tracing::error!( + "[scene thread {scene_id:?}] script error onUpdate: {}", + err_str + ); + } + break; } @@ -325,6 +344,7 @@ fn op_require( Ok(include_str!("js_modules/RestrictedActions.js").to_owned()) } "fetch" => Ok(include_str!("js_modules/fetch.js").to_owned()), + "ws" => Ok(include_str!("js_modules/ws.js").to_owned()), "~system/Runtime" => Ok(include_str!("js_modules/Runtime.js").to_owned()), "~system/Scene" => Ok(include_str!("js_modules/Scene.js").to_owned()), "~system/SignedFetch" => Ok(include_str!("js_modules/SignedFetch.js").to_owned()), @@ -338,7 +358,12 @@ fn op_require( } #[op(v8)] -fn op_log(state: Rc>, message: String) { +fn op_log(state: Rc>, message: String, immediate: bool) { + if immediate { + tracing::info!("{}", message); + } + tracing::debug!("{}", message); + let time = state.borrow().borrow::().0; state .borrow_mut() @@ -352,7 +377,12 @@ fn op_log(state: Rc>, message: String) { } #[op(v8)] -fn op_error(state: Rc>, message: String) { +fn op_error(state: Rc>, message: String, immediate: bool) { + if immediate { + tracing::error!("{}", message); + } + tracing::debug!("{}", message); + let time = state.borrow().borrow::().0; state .borrow_mut() diff --git a/rust/decentraland-godot-lib/src/dcl/js/websocket/mod.rs b/rust/decentraland-godot-lib/src/dcl/js/websocket/mod.rs new file mode 100644 index 000000000..8adbc954f --- /dev/null +++ b/rust/decentraland-godot-lib/src/dcl/js/websocket/mod.rs @@ -0,0 +1,260 @@ +use std::{cell::RefCell, collections::HashMap, rc::Rc}; + +use deno_core::{error::AnyError, op, Op, OpDecl, OpState}; +use futures_util::{SinkExt, StreamExt}; +use http::{HeaderName, HeaderValue}; +use serde::{Deserialize, Serialize}; +use tokio_tungstenite::tungstenite::{client::IntoClientRequest, protocol::CloseFrame}; + +pub fn ops() -> Vec { + vec![ + op_ws_create::DECL, + op_ws_cleanup::DECL, + op_ws_send::DECL, + op_ws_poll::DECL, + ] +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(tag = "type")] +enum WsSendData { + Binary { data: Vec }, + Text { data: String }, + Close, +} + +enum WsReceiveData { + BinaryData(Vec), + TextData(String), + Error(AnyError), + Close(Option>), + Connected, +} + +struct WsState { + counter: u32, + ws_receiver: HashMap>, + ws_sender: HashMap>, +} + +#[derive(Serialize)] +#[serde(tag = "type")] +enum WsPoll { + Connected, + Closed, + BinaryData { data: Vec }, + TextData { data: String }, +} + +async fn ws_poll( + url: String, + protocols: Vec, + mut receiver: tokio::sync::mpsc::Receiver, + sender: tokio::sync::mpsc::Sender, +) -> Result<(), AnyError> { + tracing::debug!("connecting to {:?}", url); + + let mut http_request = url.clone().into_client_request()?; + + if !protocols.is_empty() { + let protocols = protocols.join(","); + http_request.headers_mut().insert( + HeaderName::from_static("sec-websocket-protocol"), + HeaderValue::from_str(&protocols)?, + ); + } + + tracing::debug!("request to {:?}", http_request); + + let (ws_stream, _) = tokio_tungstenite::connect_async(http_request).await?; + + tracing::debug!("connected to {:?}", url); + sender.send(WsReceiveData::Connected).await?; + + tracing::debug!("status sent"); + let (mut ws_send, mut read) = ws_stream.split(); + + let sender_a = sender.clone(); + + tokio::join!( + async move { + loop { + let to_send = receiver.recv().await; + tracing::debug!("to send {:?}", to_send); + + let result = match to_send { + Some(WsSendData::Binary { data }) => ws_send + .send(tokio_tungstenite::tungstenite::Message::Binary(data)) + .await + .ok(), + Some(WsSendData::Text { data }) => ws_send + .send(tokio_tungstenite::tungstenite::Message::Text(data)) + .await + .ok(), + Some(WsSendData::Close) => { + let _ = ws_send.close().await; + None + } + None => { + let _ = sender_a + .send(WsReceiveData::Error(anyhow::Error::msg("none from sender"))) + .await; + let _ = ws_send.close().await; + None + } + }; + if result.is_none() { + break; + } + } + }, + async move { + loop { + let data_received = read.next().await; + tracing::debug!("receiving {:?}", data_received); + let result = match data_received { + Some(Ok(tokio_tungstenite::tungstenite::Message::Frame(_data))) => { + tracing::error!("unsupported frame type"); + Some(()) + } + Some(Ok(tokio_tungstenite::tungstenite::Message::Binary(data))) => { + sender.send(WsReceiveData::BinaryData(data)).await.ok() + } + Some(Ok(tokio_tungstenite::tungstenite::Message::Text(data))) => { + sender.send(WsReceiveData::TextData(data)).await.ok() + } + Some(Ok(tokio_tungstenite::tungstenite::Message::Ping(data))) => { + sender.send(WsReceiveData::BinaryData(data)).await.ok() + } + Some(Ok(tokio_tungstenite::tungstenite::Message::Pong(data))) => { + sender.send(WsReceiveData::BinaryData(data)).await.ok() + } + Some(Ok(tokio_tungstenite::tungstenite::Message::Close(data))) => { + let _ = sender.send(WsReceiveData::Close(data)).await; + None + } + Some(Err(err)) => { + let _ = sender.send(WsReceiveData::Error(err.into())).await; + None + } + None => { + let _ = sender + .send(WsReceiveData::Error(anyhow::Error::msg( + "data receiver closed", + ))) + .await; + None + } + }; + if result.is_none() { + break; + } + } + } + ); + Ok(()) +} + +#[op] +fn op_ws_create(op_state: Rc>, url: String, protocols: Vec) -> u32 { + let has_ws_state = op_state.borrow().has::(); + if !has_ws_state { + op_state.borrow_mut().put::(WsState { + counter: 0, + ws_receiver: HashMap::new(), + ws_sender: HashMap::new(), + }); + } + + let (ws_resource_id, recv_send_data, send_ondata) = { + let mut state = op_state.borrow_mut(); + let ws_state = state.borrow_mut::(); + ws_state.counter += 1; + + let id = ws_state.counter; + let (sender, recv_send_data) = tokio::sync::mpsc::channel(100); + let (send_ondata, receiver) = tokio::sync::mpsc::channel(100); + + ws_state.ws_receiver.insert(id, receiver); + ws_state.ws_sender.insert(id, sender); + + (id, recv_send_data, send_ondata) + }; + + tokio::spawn(async move { + let result = ws_poll(url, protocols, recv_send_data, send_ondata.clone()).await; + tracing::info!("websocket task finished with result: {:?}", result); + let _ = send_ondata.send(WsReceiveData::Close(None)).await; + }); + + ws_resource_id +} + +#[op] +async fn op_ws_poll(op_state: Rc>, res_id: u32) -> Result { + let mut receiver = { + let mut state = op_state.borrow_mut(); + let ws_state = state.borrow_mut::(); + let receiver = ws_state.ws_receiver.remove(&res_id); + + if receiver.is_none() { + return Err(anyhow::Error::msg("invalid resource id")); + } + + receiver.unwrap() + }; + + let data = match receiver.recv().await { + Some(WsReceiveData::BinaryData(data)) => Ok(WsPoll::BinaryData { data }), + Some(WsReceiveData::TextData(data)) => Ok(WsPoll::TextData { data }), + Some(WsReceiveData::Connected) => Ok(WsPoll::Connected), + Some(WsReceiveData::Error(err)) => Err(err), + Some(WsReceiveData::Close(data)) => { + if let Some(_data) = data { + Ok(WsPoll::Closed) + } else { + Ok(WsPoll::Closed) + } + } + None => Err(anyhow::Error::msg("none")), + }; + + let mut state = op_state.borrow_mut(); + let ws_state = state.borrow_mut::(); + ws_state.ws_receiver.insert(res_id, receiver); + + data +} + +#[op] +async fn op_ws_send( + op_state: Rc>, + res_id: u32, + event: WsSendData, +) -> Result<(), AnyError> { + let sender = { + let state = op_state.borrow_mut(); + let sender = state.borrow::().ws_sender.get(&res_id); + if sender.is_none() { + return Err(anyhow::Error::msg("invalid resource id")); + } + sender.unwrap().clone() + }; + + sender.send(event).await.map_err(anyhow::Error::from) +} + +#[op] +fn op_ws_cleanup(state: &mut OpState, res_id: u32) -> Result<(), AnyError> { + tracing::debug!("cleanup {:?}", res_id); + + let ws_state = state.borrow_mut::(); + + if let Some(mut receiver) = ws_state.ws_receiver.remove(&res_id) { + receiver.close(); + } + + ws_state.ws_sender.remove(&res_id); + + Ok(()) +} diff --git a/rust/decentraland-godot-lib/src/lib.rs b/rust/decentraland-godot-lib/src/lib.rs index 6899a4a05..ce883326e 100644 --- a/rust/decentraland-godot-lib/src/lib.rs +++ b/rust/decentraland-godot-lib/src/lib.rs @@ -20,49 +20,53 @@ struct DecentralandGodotLibrary; #[gdextension] unsafe impl ExtensionLibrary for DecentralandGodotLibrary {} -// Registers all the `#[itest]` tests. -godot::sys::plugin_registry!(__GODOT_ITEST: RustTestCase); +pub mod framework { + use godot::prelude::*; -/// Finds all `#[itest]` tests. -fn collect_rust_tests() -> (Vec, usize, bool) { - let mut all_files = std::collections::HashSet::new(); - let mut tests: Vec = vec![]; - let mut is_focus_run = false; + // Registers all the `#[itest]` tests. + godot::sys::plugin_registry!(pub(crate) __GODOT_ITEST: RustTestCase); - godot::sys::plugin_foreach!(__GODOT_ITEST; |test: &RustTestCase| { - // First time a focused test is encountered, switch to "focused" mode and throw everything away. - if !is_focus_run && test.focused { - tests.clear(); - all_files.clear(); - is_focus_run = true; - } + pub struct TestContext { + #[allow(dead_code)] + pub scene_tree: Gd, + } - // Only collect tests if normal mode, or focus mode and test is focused. - if !is_focus_run || test.focused { - all_files.insert(test.file); - tests.push(*test); - } - }); + #[derive(Copy, Clone)] + pub struct RustTestCase { + pub name: &'static str, + pub file: &'static str, + pub skipped: bool, + /// If one or more tests are focused, only they will be executed. Helpful for debugging and working on specific features. + pub focused: bool, + #[allow(dead_code)] + pub line: u32, + pub function: fn(&TestContext), + } - // Sort alphabetically for deterministic run order - tests.sort_by_key(|test| test.file); + /// Finds all `#[itest]` tests. + pub fn collect_rust_tests() -> (Vec, usize, bool) { + let mut all_files = std::collections::HashSet::new(); + let mut tests: Vec = vec![]; + let mut is_focus_run = false; - (tests, all_files.len(), is_focus_run) -} + godot::sys::plugin_foreach!(__GODOT_ITEST; |test: &RustTestCase| { + // First time a focused test is encountered, switch to "focused" mode and throw everything away. + if !is_focus_run && test.focused { + tests.clear(); + all_files.clear(); + is_focus_run = true; + } -pub struct TestContext { - #[allow(dead_code)] - scene_tree: Gd, -} + // Only collect tests if normal mode, or focus mode and test is focused. + if !is_focus_run || test.focused { + all_files.insert(test.file); + tests.push(*test); + } + }); + + // Sort alphabetically for deterministic run order + tests.sort_by_key(|test| test.file); -#[derive(Copy, Clone)] -struct RustTestCase { - name: &'static str, - file: &'static str, - skipped: bool, - /// If one or more tests are focused, only they will be executed. Helpful for debugging and working on specific features. - focused: bool, - #[allow(dead_code)] - line: u32, - function: fn(&TestContext), + (tests, all_files.len(), is_focus_run) + } } diff --git a/rust/decentraland-godot-lib/src/realm/scene_entity_coordinator.rs b/rust/decentraland-godot-lib/src/realm/scene_entity_coordinator.rs index 792ff21c0..7aad863b3 100644 --- a/rust/decentraland-godot-lib/src/realm/scene_entity_coordinator.rs +++ b/rust/decentraland-godot-lib/src/realm/scene_entity_coordinator.rs @@ -80,8 +80,12 @@ struct EntityBase { impl EntityBase { fn from_urn(urn_str: &str, default_base_url: &String) -> Option { - let Ok(urn) = urn::Urn::from_str(urn_str) else { return None;}; - let Some((lhs, rhs)) = urn.nss().split_once(':') else { return None; }; + let Ok(urn) = urn::Urn::from_str(urn_str) else { + return None; + }; + let Some((lhs, rhs)) = urn.nss().split_once(':') else { + return None; + }; let hash = match lhs { "entity" => rhs.to_owned(), _ => return None, @@ -196,7 +200,12 @@ impl SceneEntityCoordinator { } fn handle_scene_data(&mut self, id: u32, json: serde_json::Value) { - let entity_base = self.requested_entity.remove(&id).unwrap(); + let entity_base = if let Some(entity_base) = self.requested_entity.remove(&id) { + entity_base + } else { + return; + }; + let entity_definition = serde_json::from_value::(json); if entity_definition.is_err() { @@ -228,8 +237,14 @@ impl SceneEntityCoordinator { } fn handle_entity_pointers(&mut self, request_id: u32, json: serde_json::Value) { + let mut remaining_pointers = + if let Some(remaining_pointers) = self.requested_city_pointers.remove(&request_id) { + remaining_pointers + } else { + return; + }; + let entity_pointers = json.as_array().unwrap(); - let mut remaining_pointers = self.requested_city_pointers.remove(&request_id).unwrap(); // Add the scene data to the cache for entity_pointer in entity_pointers.iter() { @@ -358,7 +373,9 @@ impl SceneEntityCoordinator { if self.cache_scene_data.contains_key(urn_str) { continue; } - let Some(entity_base) = EntityBase::from_urn(urn_str, &self.content_url) else { continue; }; + let Some(entity_base) = EntityBase::from_urn(urn_str, &self.content_url) else { + continue; + }; let url = format!("{}{}", entity_base.base_url, entity_base.hash); let request = RequestOption::new( diff --git a/rust/decentraland-godot-lib/src/scene_runner/components/avatar_shape.rs b/rust/decentraland-godot-lib/src/scene_runner/components/avatar_shape.rs index 05fe75c82..799904c7e 100644 --- a/rust/decentraland-godot-lib/src/scene_runner/components/avatar_shape.rs +++ b/rust/decentraland-godot-lib/src/scene_runner/components/avatar_shape.rs @@ -142,7 +142,7 @@ pub fn update_avatar_shape(scene: &mut Scene, crdt_state: &mut SceneCrdtState) { new_avatar_shape.set(StringName::from("skip_process"), Variant::from(true)); new_avatar_shape.set_name(GodotString::from("AvatarShape")); - node.base.add_child(new_avatar_shape.share().upcast()); + node.base.add_child(new_avatar_shape.clone().upcast()); new_avatar_shape.call_deferred( StringName::from(GodotString::from("update_avatar")), diff --git a/rust/decentraland-godot-lib/src/scene_runner/components/billboard.rs b/rust/decentraland-godot-lib/src/scene_runner/components/billboard.rs index 7ddb243b5..76d9f87f4 100644 --- a/rust/decentraland-godot-lib/src/scene_runner/components/billboard.rs +++ b/rust/decentraland-godot-lib/src/scene_runner/components/billboard.rs @@ -31,7 +31,7 @@ pub fn update_billboard( } mod test { - use godot::prelude::{Basis, Share, Transform3D, Vector3}; + use godot::prelude::{Basis, Transform3D, Vector3}; use crate::{ dcl::{ @@ -40,8 +40,8 @@ mod test { last_write_wins::LastWriteWinsComponentOperation, SceneCrdtStateProtoComponents, }, }, + framework::TestContext, scene_runner::scene::Scene, - TestContext, }; use super::update_billboard; @@ -53,8 +53,8 @@ mod test { let mut crdt_state = crdt.try_lock().unwrap(); scene_context .scene_tree - .share() - .add_child(scene.godot_dcl_scene.root_node.share().upcast()); + .clone() + .add_child(scene.godot_dcl_scene.root_node.clone().upcast()); let camera_global_transform = Transform3D::IDENTITY; update_billboard(&mut scene, &mut crdt_state, &camera_global_transform); @@ -67,8 +67,8 @@ mod test { let mut crdt_state = crdt.try_lock().unwrap(); scene_context .scene_tree - .share() - .add_child(scene.godot_dcl_scene.root_node.share().upcast()); + .clone() + .add_child(scene.godot_dcl_scene.root_node.clone().upcast()); let camera_global_transform = Transform3D::new(Basis::IDENTITY, Vector3::new(1.0, 0.0, 1.0)); diff --git a/rust/decentraland-godot-lib/src/scene_runner/components/gltf_container.rs b/rust/decentraland-godot-lib/src/scene_runner/components/gltf_container.rs index 2ff30751d..2451d7179 100644 --- a/rust/decentraland-godot-lib/src/scene_runner/components/gltf_container.rs +++ b/rust/decentraland-godot-lib/src/scene_runner/components/gltf_container.rs @@ -89,7 +89,7 @@ pub fn update_gltf_container(scene: &mut Scene, crdt_state: &mut SceneCrdtState) Variant::from(invisible_meshes_collision_mask), ); new_gltf.set_name(GodotString::from("GltfContainer")); - node.base.add_child(new_gltf.share().upcast()); + node.base.add_child(new_gltf.clone().upcast()); scene.gltf_loading.insert(*entity); } diff --git a/rust/decentraland-godot-lib/src/scene_runner/components/material.rs b/rust/decentraland-godot-lib/src/scene_runner/components/material.rs index 4faa3dc8c..6bdd8c72e 100644 --- a/rust/decentraland-godot-lib/src/scene_runner/components/material.rs +++ b/rust/decentraland-godot-lib/src/scene_runner/components/material.rs @@ -26,7 +26,7 @@ pub fn update_material(scene: &mut Scene, crdt_state: &mut SceneCrdtState) { .root_node .get_node("/root/content_manager".into()) .unwrap() - .share(); + .clone(); let content_mapping_files = scene .content_mapping .get("content") @@ -283,7 +283,7 @@ fn check_texture( DclSourceTex::VideoTexture(video_entity_id) => { if let Some(node) = scene.godot_dcl_scene.get_node(video_entity_id) { if let Some(data) = &node.video_player_data { - material.set_texture(param, data.video_sink.tex.share().upcast()); + material.set_texture(param, data.video_sink.tex.clone().upcast()); return true; } } diff --git a/rust/decentraland-godot-lib/src/scene_runner/components/raycast.rs b/rust/decentraland-godot-lib/src/scene_runner/components/raycast.rs index 6affcf135..b8c37881c 100644 --- a/rust/decentraland-godot-lib/src/scene_runner/components/raycast.rs +++ b/rust/decentraland-godot-lib/src/scene_runner/components/raycast.rs @@ -162,7 +162,7 @@ fn do_raycast(scene: &Scene, node: &Node3DEntity, raycast: &PbRaycast) -> PbRayc let hits = match query_type { RaycastQueryType::RqtHitFirst => { - if let Some(hit) = get_raycast_hit(scene, space.share(), raycast_query.share()) { + if let Some(hit) = get_raycast_hit(scene, space.clone(), raycast_query.clone()) { vec![hit.0] } else { vec![] @@ -172,7 +172,7 @@ fn do_raycast(scene: &Scene, node: &Node3DEntity, raycast: &PbRaycast) -> PbRayc let mut counter = 0; let mut hits = vec![]; while let Some((hit, rid)) = - get_raycast_hit(scene, space.share(), raycast_query.share()) + get_raycast_hit(scene, space.clone(), raycast_query.clone()) { hits.push(hit); @@ -220,7 +220,7 @@ fn get_raycast_hit( mut space: Gd, raycast_query: Gd, ) -> Option<(RaycastHit, Rid)> { - let raycast_result = space.intersect_ray(raycast_query.share()); + let raycast_result = space.intersect_ray(raycast_query.clone()); let collider = raycast_result.get("collider")?; let has_dcl_entity_id = collider diff --git a/rust/decentraland-godot-lib/src/scene_runner/components/transform_and_parent.rs b/rust/decentraland-godot-lib/src/scene_runner/components/transform_and_parent.rs index f6c78c71d..e3f5349fd 100644 --- a/rust/decentraland-godot-lib/src/scene_runner/components/transform_and_parent.rs +++ b/rust/decentraland-godot-lib/src/scene_runner/components/transform_and_parent.rs @@ -1,4 +1,4 @@ -use godot::prelude::{Node, Share, Transform3D, Vector3}; +use godot::prelude::{Node, Transform3D, Vector3}; use crate::{ dcl::{ @@ -74,7 +74,7 @@ pub fn update_transform_and_parent(scene: &mut Scene, crdt_state: &mut SceneCrdt } } - let root_node = godot_dcl_scene.root_node.share().upcast::(); + let root_node = godot_dcl_scene.root_node.clone().upcast::(); while godot_dcl_scene.hierarchy_dirty { godot_dcl_scene.hierarchy_dirty = false; @@ -101,7 +101,7 @@ pub fn update_transform_and_parent(scene: &mut Scene, crdt_state: &mut SceneCrdt let current_node = godot_dcl_scene.ensure_node_mut(&entity); current_node .base - .reparent_ex(root_node.share()) + .reparent_ex(root_node.clone()) .keep_global_transform(false) .done(); current_node.computed_parent = SceneEntityId::ROOT; @@ -116,7 +116,7 @@ pub fn update_transform_and_parent(scene: &mut Scene, crdt_state: &mut SceneCrdt let parent_node = godot_dcl_scene .ensure_node_mut(&desired_parent) .base - .share() + .clone() .upcast::(); let current_node = godot_dcl_scene.ensure_node_mut(&entity); diff --git a/rust/decentraland-godot-lib/src/scene_runner/components/video_player.rs b/rust/decentraland-godot-lib/src/scene_runner/components/video_player.rs index 28f678aad..5b8c0202e 100644 --- a/rust/decentraland-godot-lib/src/scene_runner/components/video_player.rs +++ b/rust/decentraland-godot-lib/src/scene_runner/components/video_player.rs @@ -77,13 +77,13 @@ pub fn update_video_player(scene: &mut Scene, crdt_state: &mut SceneCrdtState) { let audio_stream_generator = AudioStreamGenerator::new(); audio_stream_player.set_stream(audio_stream_generator.upcast()); - node.base.add_child(audio_stream_player.share().upcast()); + node.base.add_child(audio_stream_player.clone().upcast()); audio_stream_player.play(); let (video_sink, audio_sink) = av_sinks( new_value.src.clone(), texture, - audio_stream_player.share(), + audio_stream_player.clone(), new_value.volume.unwrap_or(1.0), new_value.playing.unwrap_or(true), new_value.r#loop.unwrap_or(false), diff --git a/rust/decentraland-godot-lib/src/scene_runner/godot_dcl_scene.rs b/rust/decentraland-godot-lib/src/scene_runner/godot_dcl_scene.rs index 989da286d..008472a26 100644 --- a/rust/decentraland-godot-lib/src/scene_runner/godot_dcl_scene.rs +++ b/rust/decentraland-godot-lib/src/scene_runner/godot_dcl_scene.rs @@ -36,13 +36,27 @@ pub struct Node3DEntity { impl SceneDefinition { pub fn from_dict(dict: Dictionary) -> Result { - let Some(main_crdt_path) = dict.get("main_crdt_path") else { return Err("main_crdt_path not found".to_string()) }; - let Some(path) = dict.get("path") else { return Err("path not found".to_string()) }; - let Some(base) = dict.get("base") else { return Err("base not found".to_string()) }; - let Some(parcels) = dict.get("parcels") else { return Err("parcels not found".to_string()) }; - let Some(visible) = dict.get("visible") else { return Err("visible not found".to_string()) }; - let Some(is_global) = dict.get("is_global") else { return Err("is_global not found".to_string()) }; - let Some(title) = dict.get("title") else { return Err("title not found".to_string()) }; + let Some(main_crdt_path) = dict.get("main_crdt_path") else { + return Err("main_crdt_path not found".to_string()); + }; + let Some(path) = dict.get("path") else { + return Err("path not found".to_string()); + }; + let Some(base) = dict.get("base") else { + return Err("base not found".to_string()); + }; + let Some(parcels) = dict.get("parcels") else { + return Err("parcels not found".to_string()); + }; + let Some(visible) = dict.get("visible") else { + return Err("visible not found".to_string()); + }; + let Some(is_global) = dict.get("is_global") else { + return Err("is_global not found".to_string()); + }; + let Some(title) = dict.get("title") else { + return Err("title not found".to_string()); + }; let base = Vector2i::try_from_variant(&base).map_err(|_op| "couldn't get offset as Vector2i")?; @@ -97,7 +111,7 @@ impl GodotDclScene { let entities = HashMap::from([( SceneEntityId::new(0, 0), - Node3DEntity::new(root_node.share()), + Node3DEntity::new(root_node.clone()), )]); GodotDclScene { @@ -119,7 +133,7 @@ impl GodotDclScene { entity.number, entity.version ))); - self.root_node.add_child(new_node.base.share().upcast()); + self.root_node.add_child(new_node.base.clone().upcast()); self.entities.insert(*entity, new_node); } 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 b396336e7..e38712e66 100644 --- a/rust/decentraland-godot-lib/src/scene_runner/scene_manager.rs +++ b/rust/decentraland-godot-lib/src/scene_runner/scene_manager.rs @@ -88,7 +88,7 @@ impl SceneManager { let new_scene = Scene::new(new_scene_id, scene_definition, dcl_scene, content_mapping); self.base - .add_child(new_scene.godot_dcl_scene.root_node.share().upcast()); + .add_child(new_scene.godot_dcl_scene.root_node.clone().upcast()); self.scenes.insert(new_scene.dcl_scene.scene_id, new_scene); self.sorted_scene_ids.push(new_scene_id); @@ -117,15 +117,15 @@ impl SceneManager { player_node: Gd, console: Callable, ) { - self.camera_node = camera_node.share(); - self.player_node = player_node.share(); + self.camera_node = camera_node.clone(); + self.player_node = player_node.clone(); self.console = console; } #[func] fn get_scene_content_mapping(&self, scene_id: i32) -> Dictionary { if let Some(scene) = self.scenes.get(&SceneId(scene_id as u32)) { - return scene.content_mapping.share(); + return scene.content_mapping.clone(); } Dictionary::default() } @@ -364,9 +364,9 @@ impl SceneManager { let node = scene .godot_dcl_scene .root_node - .share() + .clone() .upcast::() - .share(); + .clone(); self.base.remove_child(node); scene.godot_dcl_scene.root_node.queue_free(); self.sorted_scene_ids.retain(|x| x != scene_id); @@ -506,7 +506,7 @@ impl SceneManager { #[func] fn get_tooltips(&self) -> VariantArray { - self.pointer_tooltips.share() + self.pointer_tooltips.clone() } #[signal] diff --git a/rust/decentraland-godot-lib/src/scene_runner/update_scene.rs b/rust/decentraland-godot-lib/src/scene_runner/update_scene.rs index 9a6ea3a3f..ab1ac4f04 100644 --- a/rust/decentraland-godot-lib/src/scene_runner/update_scene.rs +++ b/rust/decentraland-godot-lib/src/scene_runner/update_scene.rs @@ -1,6 +1,6 @@ use std::time::Instant; -use godot::prelude::{Share, Transform3D}; +use godot::prelude::Transform3D; use super::{ components::{ @@ -103,7 +103,7 @@ fn update_deleted_entities(scene: &mut Scene) { for (entity_id, node) in godot_dcl_scene.entities.iter_mut() { if died.contains(&node.computed_parent) && *entity_id != node.computed_parent { node.base - .reparent_ex(godot_dcl_scene.root_node.share().upcast()) + .reparent_ex(godot_dcl_scene.root_node.clone().upcast()) .keep_global_transform(false) .done(); node.computed_parent = SceneEntityId::ROOT; @@ -114,7 +114,7 @@ fn update_deleted_entities(scene: &mut Scene) { for deleted_entity in died.iter() { let node = godot_dcl_scene.ensure_node_mut(deleted_entity); - node.base.share().free(); + node.base.clone().free(); godot_dcl_scene.entities.remove(deleted_entity); } } diff --git a/rust/decentraland-godot-lib/src/test_runner/test_suite.rs b/rust/decentraland-godot-lib/src/test_runner/test_suite.rs index 8dcc48608..452547b0f 100644 --- a/rust/decentraland-godot-lib/src/test_runner/test_suite.rs +++ b/rust/decentraland-godot-lib/src/test_runner/test_suite.rs @@ -3,7 +3,7 @@ use std::time::{Duration, Instant}; use godot::prelude::*; use tracing::error; -use crate::{RustTestCase, TestContext}; +use crate::framework::{RustTestCase, TestContext}; #[derive(Debug, Default, GodotClass)] #[class(base=Node)] @@ -27,7 +27,7 @@ impl TestRunnerSuite { ) -> bool { tracing::info!("{}Run{} Godot integration tests...", FMT_CYAN_BOLD, FMT_END); - let (rust_tests, rust_file_count, focus_run) = super::super::collect_rust_tests(); + let (rust_tests, rust_file_count, focus_run) = crate::framework::collect_rust_tests(); self.focus_run = focus_run; if focus_run { tracing::info!(" {FMT_CYAN}Focused run{FMT_END} -- execute only selected Rust tests.") diff --git a/rust/xtask/src/consts.rs b/rust/xtask/src/consts.rs index 4467176a3..03b616117 100644 --- a/rust/xtask/src/consts.rs +++ b/rust/xtask/src/consts.rs @@ -1,14 +1,13 @@ -pub const REPO_FOLDER: &'static str = "./../../"; -pub const GODOT_PROJECT_FOLDER: &'static str = "./../../godot/"; -pub const BIN_FOLDER: &'static str = "./../../.bin/"; -pub const RUST_LIB_PROJECT_FOLDER: &'static str = "./../../rust/decentraland-godot-lib/"; -pub const EXPORTS_FOLDER: &'static str = "./../../exports/"; +pub const GODOT_PROJECT_FOLDER: &str = "./../../godot/"; +pub const BIN_FOLDER: &str = "./../../.bin/"; +pub const RUST_LIB_PROJECT_FOLDER: &str = "./../../rust/decentraland-godot-lib/"; +pub const EXPORTS_FOLDER: &str = "./../../exports/"; pub const PROTOC_BASE_URL: &str = "https://github.com/protocolbuffers/protobuf/releases/download/v23.2/protoc-23.2-"; pub const GODOT4_BIN_BASE_URL: &str = - "https://github.com/godotengine/godot/releases/download/4.1-stable/Godot_v4.1-stable_"; + "https://github.com/godotengine/godot/releases/download/4.1.1-stable/Godot_v4.1.1-stable_"; pub const GODOT4_EXPORT_TEMPLATES_BASE_URL: &str = - "https://github.com/godotengine/godot/releases/download/4.1-stable/Godot_v4.1-stable_export_templates.tpz"; + "https://github.com/godotengine/godot/releases/download/4.1.1-stable/Godot_v4.1.1-stable_export_templates.tpz"; diff --git a/rust/xtask/src/install_dependency.rs b/rust/xtask/src/install_dependency.rs index cd78cc4e0..814db848b 100644 --- a/rust/xtask/src/install_dependency.rs +++ b/rust/xtask/src/install_dependency.rs @@ -12,8 +12,7 @@ use crate::download_file::download_file; use crate::export::prepare_templates; use crate::consts::{ - BIN_FOLDER, EXPORTS_FOLDER, GODOT4_BIN_BASE_URL, GODOT_PROJECT_FOLDER, PROTOC_BASE_URL, - RUST_LIB_PROJECT_FOLDER, + BIN_FOLDER, GODOT4_BIN_BASE_URL, GODOT_PROJECT_FOLDER, PROTOC_BASE_URL, RUST_LIB_PROJECT_FOLDER, }; use crate::path::adjust_canonicalization; @@ -148,8 +147,8 @@ pub fn get_godot_executable_path() -> Option { let arch = env::consts::ARCH; let os_url = match (os, arch) { - ("linux", "x86_64") => Some("Godot_v4.1-stable_linux.x86_64".to_string()), - ("windows", "x86_64") => Some("Godot_v4.1-stable_win64.exe".to_string()), + ("linux", "x86_64") => Some("Godot_v4.1.1-stable_linux.x86_64".to_string()), + ("windows", "x86_64") => Some("Godot_v4.1.1-stable_win64.exe".to_string()), ("macos", _) => Some("Godot.app/Contents/MacOS/Godot".to_string()), _ => None, }?; diff --git a/rust/xtask/src/main.rs b/rust/xtask/src/main.rs index 15267c339..880f054bf 100644 --- a/rust/xtask/src/main.rs +++ b/rust/xtask/src/main.rs @@ -125,6 +125,10 @@ fn main() -> Result<(), anyhow::Error> { ), _ => unreachable!("unreachable branch"), }; + + if res.is_err() { + println!("Fail running subcommand `{:?}`", subcommand.0); + } res // xtaskops::tasks::main() }