diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 3fcccd8a3..7437ad5b9 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -96,7 +96,7 @@ jobs: if: success() || failure() uses: actions/upload-artifact@v4 with: - name: snapshots + name: coverage-snapshots path: | tests/snapshots/scenes/comparison/**/* tests/snapshots/avatar-image-generation/comparison/**/* diff --git a/.github/workflows/docker_build_test.yml b/.github/workflows/docker_build_test.yml index d17a4e8b9..9d87eb91c 100644 --- a/.github/workflows/docker_build_test.yml +++ b/.github/workflows/docker_build_test.yml @@ -28,7 +28,9 @@ jobs: uses: redhat-actions/buildah-build@v2 with: image: godot-explorer - tags: latest ${{ github.sha }} + tags: | + ${{ github.sha }} + ${{ github.ref == 'refs/heads/main' && 'latest' || 'next' }} dockerfiles: | ./Dockerfile build-args: | @@ -47,21 +49,36 @@ jobs: - name: Run avatar test run: | - mkdir -p output + mkdir -p avatars-output podman run --rm -v \ $(pwd)/tests/avatars-test-input.json:/app/avatars.json \ - -v $(pwd)/output:/app/output localhost/godot-explorer:latest + -v $(pwd)/avatars-output:/app/output localhost/godot-explorer:${{ github.sha }} - name: Compare images with snapshots run: | cargo run -- compare-image-folders \ --snapshots tests/snapshots/avatar-image-generation/ \ - --result ${{ github.workspace }}/output/ + --result ${{ github.workspace }}/avatars-output/ + + - name: Run scene-rendering test + run: | + mkdir -p scenes-output + podman run --rm -v \ + $(pwd)/tests/scene-renderer-test-input.json:/app/scenes.json \ + --env PRESET_ARGS="--scene-renderer --scene-input-file scenes.json" \ + -v $(pwd)/scenes-output:/app/output localhost/godot-explorer:${{ github.sha }} + + - name: Compare images with snapshots + run: | + cargo run -- compare-image-folders \ + --snapshots tests/snapshots/scene-image-generation/ \ + --result ${{ github.workspace }}/scenes-output/ - name: Upload artifacts if: success() || failure() uses: actions/upload-artifact@v4 with: - name: avatar-snapshots + name: docker-snapshots path: | - ${{ github.workspace }}/output/**/* \ No newline at end of file + ${{ github.workspace }}/avatar-output/**/* + ${{ github.workspace }}/scenes-output/**/* \ No newline at end of file diff --git a/.github/workflows/linux_builds.yml b/.github/workflows/linux_builds.yml index 34935a544..d4d9020b0 100644 --- a/.github/workflows/linux_builds.yml +++ b/.github/workflows/linux_builds.yml @@ -30,7 +30,7 @@ jobs: # Build section - name: Cargo install - run: cargo run -- install + run: cargo run -- install --platforms linux - name: Build working-directory: lib diff --git a/.github/workflows/macos_builds.yml b/.github/workflows/macos_builds.yml index a70d15d55..2ca306d24 100644 --- a/.github/workflows/macos_builds.yml +++ b/.github/workflows/macos_builds.yml @@ -28,7 +28,7 @@ jobs: # Build section - name: Cargo install - run: cargo run -- install + run: cargo run -- install --platforms macos - name: Build working-directory: lib diff --git a/.github/workflows/windows_builds.yml b/.github/workflows/windows_builds.yml index 8e45a4834..556b400a6 100644 --- a/.github/workflows/windows_builds.yml +++ b/.github/workflows/windows_builds.yml @@ -31,7 +31,7 @@ jobs: # Build section - name: Cargo install - run: cargo run -- install + run: cargo run -- install --platforms windows - name: Build working-directory: lib diff --git a/.prettierrc b/.prettierrc index de828d551..3b350c9d1 100644 --- a/.prettierrc +++ b/.prettierrc @@ -10,6 +10,14 @@ "useTabs": false, "semi": false } + }, + { + "files": "*.json", + "options": { + "tabWidth": 2, + "printWidth": 80, + "useTabs": false + } } ] -} +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 5a74ad33e..8b4ff320f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,20 +8,24 @@ "program": "${workspaceFolder}/.bin/godot/godot4_bin", "args": [ "--path", - "${workspaceFolder}/godot" + "${workspaceFolder}/godot", + "--skip-lobby", + "--realm", + "http://localhost:8000", + "--preview" ], "stopAtEntry": false, "cwd": "${workspaceRoot}/godot", "environment": [ { "name": "RUST_LOG", - "value": "debug" + "value": "dclgodot=debug" } ], "externalConsole": true, "preLaunchTask": "Build GDExtension Lib", "sourceFileMap": { - "/rustc/cc66ad468955717ab92600c770da8c1601a4ff33": "${env:HOME}${env:USERPROFILE}\\.rustup\\toolchains\\stable-x86_64-pc-windows-msvc\\lib\\rustlib\\src\\rust" + "/rustc/129f3b9964af4d4a709d1383930ade12dfe7c081": "${env:HOME}${env:USERPROFILE}\\.rustup\\toolchains\\stable-x86_64-pc-windows-msvc\\lib\\rustlib\\src\\rust" } }, { @@ -29,11 +33,7 @@ "type": "cppvsdbg", "request": "launch", "program": "${workspaceFolder}/.bin/godot/godot4_bin", - "args": [ - "--path", - "${workspaceFolder}/godot", - "--test" - ], + "args": ["--path", "${workspaceFolder}/godot", "--test"], "stopAtEntry": false, "cwd": "${workspaceRoot}/godot", "environment": [ @@ -50,11 +50,7 @@ "type": "cppvsdbg", "request": "launch", "program": "${workspaceFolder}/.bin/godot/godot4_bin", - "args": [ - "--path", - "${workspaceFolder}/godot", - "-e" - ], + "args": ["--path", "${workspaceFolder}/godot", "-e"], "stopAtEntry": false, "cwd": "${workspaceRoot}/godot", "environment": [], @@ -66,11 +62,7 @@ "type": "cppdbg", "request": "launch", "program": "${workspaceFolder}/.bin/godot/godot4_bin", - "args": [ - "--path", - "${workspaceFolder}/godot", - "-e" - ], + "args": ["--path", "${workspaceFolder}/godot", "-e"], "stopAtEntry": false, "cwd": "${workspaceRoot}/godot", "environment": [], @@ -82,10 +74,7 @@ "type": "cppdbg", "request": "launch", "program": "${workspaceFolder}/.bin/godot/godot4_bin", - "args": [ - "--path", - "${workspaceFolder}/godot" - ], + "args": ["--path", "${workspaceFolder}/godot"], "stopAtEntry": false, "cwd": "${workspaceRoot}/godot", "environment": [ @@ -111,11 +100,7 @@ "type": "cppdbg", "request": "launch", "program": "${workspaceFolder}/.bin/godot/godot4_bin", - "args": [ - "--path", - "${workspaceFolder}/godot", - "--test" - ], + "args": ["--path", "${workspaceFolder}/godot", "--test"], "stopAtEntry": false, "cwd": "${workspaceRoot}/godot", "environment": [], @@ -136,11 +121,7 @@ "type": "cppdbg", "request": "launch", "program": "${workspaceFolder}/.bin/godot/godot4_bin", - "args": [ - "--path", - "${workspaceFolder}/godot", - "-e" - ], + "args": ["--path", "${workspaceFolder}/godot", "-e"], "stopAtEntry": false, "cwd": "${workspaceRoot}/godot", "environment": [], @@ -153,10 +134,7 @@ "request": "launch", "targetArchitecture": "arm64", "program": "${workspaceFolder}/.bin/godot/godot4_bin", - "args": [ - "--path", - "${workspaceFolder}/godot" - ], + "args": ["--path", "${workspaceFolder}/godot"], "stopAtEntry": false, "cwd": "${workspaceRoot}/godot", "environment": [], @@ -172,4 +150,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/README.md b/README.md index a9933cdd8..6565062cb 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ - set `LIBCLANG_PATH` = `path to LLVM\x64\bin` (this is packaged with visual studio, or can be downloaded separately) - the `.github/workflows/ci.yml` file can be useful to guide you -4. Run `cargo run -- install` in the repo root folder. +4. Run `cargo run -- install --platforms linux` in the repo root folder (change linux to your target platform). ## Running and editing the project @@ -43,7 +43,7 @@ cd lib cd ../../ # return # Compile for Linux -cargo run -- install +cargo run -- install --platforms android cargo run -- run --only-build cd ../../ # return diff --git a/build-android-apk.sh b/build-android-apk.sh index 5f98a466e..8bd0a7e27 100755 --- a/build-android-apk.sh +++ b/build-android-apk.sh @@ -17,7 +17,7 @@ fi echo "Build for Linux x86_64" cd ${EXPLORER_PATH} -cargo run -- install +cargo run -- install --platforms android cargo run -- run --only-build echo "Link export templates" diff --git a/entry-point.sh b/entry-point.sh index eabd61c02..8f321ba20 100755 --- a/entry-point.sh +++ b/entry-point.sh @@ -2,4 +2,13 @@ /usr/bin/Xvfb -ac :99 -screen 0 1280x1024x24 & export DISPLAY=:99 -./decentraland.godot.client.x86_64 --rendering-driver opengl3 --avatar-renderer --avatars avatars.json || true + +# Check PRESET_ARGS environment variable +if [ -z "$PRESET_ARGS" ]; then + echo "PRESET_ARGS is not set. Using default arguments." + PRESET_ARGS="--avatar-renderer --avatars avatars.json" +else + echo "PRESET_ARGS is set to '$PRESET_ARGS'." +fi + +./decentraland.godot.client.x86_64 --rendering-driver opengl3 $PRESET_ARGS || true \ No newline at end of file diff --git a/godot/.gitignore b/godot/.gitignore index bacae039b..3e0fe03eb 100644 --- a/godot/.gitignore +++ b/godot/.gitignore @@ -2,6 +2,7 @@ /.godot/ /android/ +/addons/**/~*.dll # Godot-specific ignores .import/ diff --git a/godot/src/decentraland_components/avatar/wearables/lambda_wearable_request.gd b/godot/src/decentraland_components/avatar/wearables/lambda_wearable_request.gd index 07d25801c..f9bc7654d 100644 --- a/godot/src/decentraland_components/avatar/wearables/lambda_wearable_request.gd +++ b/godot/src/decentraland_components/avatar/wearables/lambda_wearable_request.gd @@ -55,7 +55,7 @@ static func _async_request( url += "?pageNum=%d" % page_number url += "&pageSize=%d" % page_size - var promise: Promise = Global.http_requester.request_json(url, HTTPClient.METHOD_GET, "", []) + var promise: Promise = Global.http_requester.request_json(url, HTTPClient.METHOD_GET, "", {}) var result = await PromiseUtils.async_awaiter(promise) diff --git a/godot/src/global.gd b/godot/src/global.gd index bd9edfeb6..04ffd98de 100644 --- a/godot/src/global.gd +++ b/godot/src/global.gd @@ -122,6 +122,7 @@ func _ready(): get_tree().root.add_child.call_deferred(self.portable_experience_controller) get_tree().root.add_child.call_deferred(self.testing_tools) get_tree().root.add_child.call_deferred(self.metrics) + get_tree().root.add_child.call_deferred(self.network_inspector) var custom_importer = load("res://src/logic/custom_gltf_importer.gd").new() GLTFDocument.register_gltf_document_extension(custom_importer) @@ -129,6 +130,11 @@ func _ready(): if args.has("--raycast-debugger"): set_raycast_debugger_enable(true) + if args.has("--network-debugger"): + self.network_inspector.set_is_active(true) + else: + self.network_inspector.set_is_active(false) + DclMeshRenderer.init_primitive_shapes() diff --git a/godot/src/logic/content/opensea_nft_fetcher.gd b/godot/src/logic/content/opensea_nft_fetcher.gd index 161e3a768..0d8ed9ccd 100644 --- a/godot/src/logic/content/opensea_nft_fetcher.gd +++ b/godot/src/logic/content/opensea_nft_fetcher.gd @@ -92,10 +92,10 @@ func fetch_nft(urn: DclUrn) -> Promise: func _async_request_nft(completed_promise: Promise, urn: DclUrn): var url = RETRIEVE_ASSETS_ENDPOINT % [urn.chain, urn.contract_address, urn.token_id] - var headers = [ - "Content-Type: application/json", - "X-API-KEY: " + API_KEY, - ] + var headers = { + "Content-Type": "application/json", + "X-API-KEY": API_KEY, + } var asset_promise: Promise = Global.http_requester.request_json( url, HTTPClient.METHOD_GET, "", headers ) diff --git a/godot/src/logic/player/player_identity.gd b/godot/src/logic/player/player_identity.gd index acb65843e..02c2894ee 100644 --- a/godot/src/logic/player/player_identity.gd +++ b/godot/src/logic/player/player_identity.gd @@ -19,7 +19,7 @@ func _on_realm_changed(): # TODO: replace with Global.content_provider.fetch_profile when it supports multiple lambda server func async_fetch_profile(address: String, requested_lambda_server_base_url: String) -> void: var url = requested_lambda_server_base_url + "profiles/" + address - var promise: Promise = Global.http_requester.request_json(url, HTTPClient.METHOD_GET, "", []) + var promise: Promise = Global.http_requester.request_json(url, HTTPClient.METHOD_GET, "", {}) var response = await PromiseUtils.async_awaiter(promise) # Are we still needing to fetch this profile? @@ -78,7 +78,7 @@ func async_deploy_profile(new_profile: DclUserProfile, has_new_snapshots: bool) return var body_payload = (ret as Dictionary).get("body_payload") - var headers := ["Content-Type: " + (ret as Dictionary).get("content_type")] + var headers := {"Content-Type": (ret as Dictionary).get("content_type")} var url := Global.realm.get_profile_content_url() + "entities/" var promise_req := Global.http_requester.request_json_bin( url, HTTPClient.METHOD_POST, body_payload, headers diff --git a/godot/src/logic/realm.gd b/godot/src/logic/realm.gd index a581d4470..c2db62f88 100644 --- a/godot/src/logic/realm.gd +++ b/godot/src/logic/realm.gd @@ -134,7 +134,7 @@ func async_set_realm(new_realm_string: String, search_new_pos: bool = false) -> realm_url = Realm.ensure_starts_with_https(realm_url) var promise: Promise = Global.http_requester.request_json( - realm_url + "about", HTTPClient.METHOD_GET, "", [] + realm_url + "about", HTTPClient.METHOD_GET, "", {} ) var res = await PromiseUtils.async_awaiter(promise) @@ -211,7 +211,7 @@ func async_request_set_position(scene_urn): prints(scene_urn) var url = scene_urn.baseUrl + scene_urn.entityId - var promise: Promise = Global.http_requester.request_json(url, HTTPClient.METHOD_GET, "", []) + var promise: Promise = Global.http_requester.request_json(url, HTTPClient.METHOD_GET, "", {}) var res = await PromiseUtils.async_awaiter(promise) if res is PromiseError: diff --git a/godot/src/logic/rust_http_requester_wrapper.gd b/godot/src/logic/rust_http_requester_wrapper.gd deleted file mode 100644 index 81eb342ed..000000000 --- a/godot/src/logic/rust_http_requester_wrapper.gd +++ /dev/null @@ -1,121 +0,0 @@ -class_name RustHttpRequesterWrapper -extends RefCounted - -# Dictionary mapping HTTP status codes to their descriptions. -const HTTP_STATUS_DESCRIPTIONS = { - 100: "Continue", - 101: "Switching Protocols", - 200: "OK", - 201: "Created", - 202: "Accepted", - 203: "Non-Authoritative Information", - 204: "No Content", - 205: "Reset Content", - 206: "Partial Content", - 300: "Multiple Choices", - 301: "Moved Permanently", - 302: "Found", - 303: "See Other", - 304: "Not Modified", - 307: "Temporary Redirect", - 308: "Permanent Redirect", - 400: "Bad Request", - 401: "Unauthorized", - 403: "Forbidden", - 404: "Not Found", - 405: "Method Not Allowed", - 406: "Not Acceptable", - 407: "Proxy Authentication Required", - 408: "Request Timeout", - 409: "Conflict", - 410: "Gone", - 411: "Length Required", - 412: "Precondition Failed", - 413: "Payload Too Large", - 414: "URI Too Long", - 415: "Unsupported Media Type", - 416: "Range Not Satisfiable", - 417: "Expectation Failed", - 418: "I'm a teapot", # Easter egg status code - 426: "Upgrade Required", - 429: "Too Many Requests", - 451: "Unavailable For Legal Reasons", - 500: "Internal Server Error", - 501: "Not Implemented", - 502: "Bad Gateway", - 503: "Service Unavailable", - 504: "Gateway Timeout", - 505: "HTTP Version Not Supported" -} - -var promises: Dictionary = {} - -var _requester := RustHttpRequester.new() - - -func get_status_description(status_code: int) -> String: - # Returns the description for the given HTTP status code. - # If the status code is not found, returns an empty string or a default message. - return ( - "Status Code " - + str(status_code) - + " " - + HTTP_STATUS_DESCRIPTIONS.get(status_code, "Unknown Status Code") - ) - - -func is_success_status_code(status_code: int) -> bool: - # Checks if the given status code is within the success range (200-299) - return 200 <= status_code and status_code <= 299 - - -func request_file(url: String, absolute_path: String) -> Promise: - var id = _requester.request_file(0, url, absolute_path) - - var promise = Promise.new() - promises[id] = promise - return promise - - -func request_json(url: String, method: int, body: String, headers: Array) -> Promise: - var id = _requester.request_json(0, url, method, body, headers) - - var promise = Promise.new() - promises[id] = promise - return promise - - -func request_json_bin(url: String, method: int, body: PackedByteArray, headers: Array) -> Promise: - var id = _requester.request_json_bin(0, url, method, body, headers) - - var promise = Promise.new() - promises[id] = promise - return promise - - -func poll(): - var res = _requester.poll() - if res is RequestResponse: - var response: RequestResponse = res - var id = response.id() - var promise: Promise = promises[id] - if response.is_error(): - promise.reject(response.get_error()) - elif !is_success_status_code(response.status_code()): - var payload = response.get_response_as_string() - if payload != null: - promise.reject(payload) - else: - promise.reject(get_status_description(response.status_code())) - else: - promise.resolve_with_data(response) - promises.erase(id) - elif res is RequestResponseError: - var error: RequestResponseError = res - var id = error.id() - var promise: Promise = promises[id] - promise.reject(error.get_error_message()) - promises.erase(id) - - if res != null: - poll() diff --git a/godot/src/main.gd b/godot/src/main.gd index df4610643..a20864c9c 100644 --- a/godot/src/main.gd +++ b/godot/src/main.gd @@ -37,14 +37,15 @@ func _start(): var args = OS.get_cmdline_args() if Global.is_xr(): + print("Running in XR mode") get_tree().change_scene_to_file("res://src/vr/vr_lobby.tscn") elif args.has("--avatar-renderer"): + print("Running in Avatar-Renderer mode") get_tree().change_scene_to_file( "res://src/tool/avatar_renderer/avatar_renderer_standalone.tscn" ) - elif args.has("--scene-renderer"): - get_tree().change_scene_to_file("res://src/tool/scene_renderer/scene.tscn") - elif args.has("--scene-test"): + elif args.has("--scene-test") or args.has("--scene-renderer"): + print("Running in Scene Test mode") Global.get_config().guest_profile = {} Global.get_config().save_to_settings_file() Global.player_identity.set_default_profile() @@ -55,5 +56,6 @@ func _start(): Global.get_config().session_account = new_stored_account get_tree().change_scene_to_file("res://src/ui/explorer.tscn") else: + print("Running in regular mode") Global.music_player.play("music_authentication") get_tree().change_scene_to_file("res://src/ui/components/auth/lobby.tscn") diff --git a/godot/src/test/integration_rust/rust_requester.gd b/godot/src/test/integration_rust/rust_requester.gd deleted file mode 100644 index 4a0dbcf66..000000000 --- a/godot/src/test/integration_rust/rust_requester.gd +++ /dev/null @@ -1,26 +0,0 @@ -extends Node - -var http_requester = RustHttpRequester.new() - - -func _ready(): - do_test.call_deferred() - - -func do_test(): - http_requester.request_file(0, "https://httpbin.org/image/png", "algo0.png") - http_requester.request_file(1, "https://httpbin.org/image/png", "algo1.png") - http_requester.request_file(2, "https://httpbin.org/image/png", "algo2.png") - - -func _process(_delta): - var some: RequestResponse = http_requester.poll() - if some != null: - print( - "> reponse with id: ", - some.id(), - " error? ", - some.is_error(), - " status_code=", - some.status_code() - ) diff --git a/godot/src/test/integration_rust/rust_requester.tscn b/godot/src/test/integration_rust/rust_requester.tscn deleted file mode 100644 index e75161872..000000000 --- a/godot/src/test/integration_rust/rust_requester.tscn +++ /dev/null @@ -1,6 +0,0 @@ -[gd_scene load_steps=2 format=3 uid="uid://0r0fn0onxq44"] - -[ext_resource type="Script" path="res://src/test/integration_rust/rust_requester.gd" id="1_gjm00"] - -[node name="rust_requester" type="Node2D"] -script = ExtResource("1_gjm00") diff --git a/godot/src/tool/avatar_renderer/avatar_renderer_standalone.gd b/godot/src/tool/avatar_renderer/avatar_renderer_standalone.gd index 6c112a120..1ae1fa77f 100644 --- a/godot/src/tool/avatar_renderer/avatar_renderer_standalone.gd +++ b/godot/src/tool/avatar_renderer/avatar_renderer_standalone.gd @@ -15,11 +15,10 @@ var current_avatar: DclAvatarWireFormat func get_params_from_cmd(): var args := OS.get_cmdline_args() + # Only use from the editor if USE_TEST_INPUT or args.has("--use-test-input"): return [ - AvatarRendererHelper.AvatarFile.from_file_path( - "res://src/tool/avatar_renderer/test-input.json" - ) + AvatarRendererHelper.AvatarFile.from_file_path("res://../tests/avatars-test-input.json") ] var avatar_data = null diff --git a/godot/src/tool/avatar_renderer/test-input.json b/godot/src/tool/avatar_renderer/test-input.json deleted file mode 100644 index 0f0eeb84f..000000000 --- a/godot/src/tool/avatar_renderer/test-input.json +++ /dev/null @@ -1,360 +0,0 @@ -{ - "baseUrl": "https://peer.decentraland.org/content", - "payload": [ - { - "entity": "bafkreid7hnqaw5qpb2ohwj2in344f5gx5ehxyc7wvlgbii5fbcbqh2am6q", - "destPath": "output/bafkreid7hnqaw5qpb2ohwj2in344f5gx5ehxyc7wvlgbii5fbcbqh2am6q.png", - "width": 256, - "height": 512, - "faceDestPath": "output/bafkreid7hnqaw5qpb2ohwj2in344f5gx5ehxyc7wvlgbii5fbcbqh2am6q_face.png", - "faceWidth": 256, - "faceHeight": 256, - "avatar": { - "bodyShape": "urn:decentraland:off-chain:base-avatars:BaseMale", - "snapshots": { - "body": "bafkreieo72ffen4llkh27q4hcgyfw4t6b6zjiofehnhms4vnynzcbf4fr4", - "face256": "bafkreiezotehjhyscnr7kxjmg4oj54igqtz23fcxwzg42qmgke4hhpmyvu" - }, - "eyes": { - "color": { - "r": 0.207, - "g": 0.207, - "b": 0.149 - } - }, - "hair": { - "color": { - "r": 0.109, - "g": 0.109, - "b": 0.109 - } - }, - "skin": { - "color": { - "r": 0.866, - "g": 0.694, - "b": 0.56 - } - }, - "emotes": [ - { - "slot": 0, - "urn": "handsair" - }, - { - "slot": 1, - "urn": "wave" - }, - { - "slot": 2, - "urn": "urn:decentraland:matic:collections-v2:0x0b472c2c04325a545a43370b54e93c87f3d5badf:0" - }, - { - "slot": 3, - "urn": "urn:decentraland:matic:collections-v2:0x54bf16bed39a02d5f8bda33664c72c59d367caf7:0" - }, - { - "slot": 4, - "urn": "urn:decentraland:matic:collections-v2:0x70eb032d4621a51945b913c3f9488d50fc1fca38:0" - }, - { - "slot": 5, - "urn": "urn:decentraland:matic:collections-v2:0x875146d1d26e91c80f25f5966a84b098d3db1fc8:1" - }, - { - "slot": 6, - "urn": "urn:decentraland:matic:collections-v2:0xa25c20f58ac447621a5f854067b857709cbd60eb:7" - }, - { - "slot": 7, - "urn": "urn:decentraland:matic:collections-v2:0xbada8a315e84e4d78e3b6914003647226d9b4001:10" - }, - { - "slot": 8, - "urn": "urn:decentraland:matic:collections-v2:0xbada8a315e84e4d78e3b6914003647226d9b4001:11" - }, - { - "slot": 9, - "urn": "urn:decentraland:matic:collections-v2:0x0c956c74518ed34afb7b137d9ddfdaea7ca13751:0" - } - ], - "wearables": [ - "urn:decentraland:off-chain:base-avatars:eyebrows_06", - "urn:decentraland:off-chain:base-avatars:eyes_00", - "urn:decentraland:off-chain:base-avatars:rounded_sun_glasses", - "urn:decentraland:off-chain:base-avatars:ruby_red_loafer", - "urn:decentraland:off-chain:base-avatars:tall_front_01", - "urn:decentraland:off-chain:base-avatars:comfortablepants", - "urn:decentraland:off-chain:base-avatars:mouth_01", - "urn:decentraland:off-chain:base-avatars:green_stone_tiara", - "urn:decentraland:off-chain:base-avatars:red_tshirt" - ] - } - }, - { - "entity": "bafkreidw3wn32cugqfi4jlqgzldakfbhjcyn377mm3qrjkquo5v4ud5k2u", - "destPath": "output/bafkreidw3wn32cugqfi4jlqgzldakfbhjcyn377mm3qrjkquo5v4ud5k2u.png", - "width": 256, - "height": 512, - "faceDestPath": "output/bafkreidw3wn32cugqfi4jlqgzldakfbhjcyn377mm3qrjkquo5v4ud5k2u_face.png", - "faceWidth": 256, - "faceHeight": 256, - "avatar": { - "bodyShape": "urn:decentraland:off-chain:base-avatars:BaseMale", - "wearables": [ - "urn:decentraland:off-chain:base-avatars:eyes_00", - "urn:decentraland:off-chain:base-avatars:mouth_00", - "urn:decentraland:off-chain:base-avatars:beard", - "urn:decentraland:off-chain:base-avatars:cool_hair", - "urn:decentraland:off-chain:base-avatars:f_eyebrows_06", - "urn:decentraland:off-chain:base-avatars:hip_hop_joggers", - "urn:decentraland:off-chain:base-avatars:sport_blue_shoes", - "urn:decentraland:off-chain:base-avatars:cyclope", - "urn:decentraland:off-chain:base-avatars:Thunder_earring", - "urn:decentraland:matic:collections-v2:0xbf83965191065487db0644812649d5238435c723:2:210624583337114373395836055367340864637790190801098222508621956528", - "urn:decentraland:matic:collections-v2:0xb854bf4d15be8e1f9b38e8b6af7d3283b81edfd8:1:105312291668557186697918027683670432318895095400549111254310982525" - ], - "forceRender": [], - "emotes": [ - { - "slot": 0, - "urn": "handsair" - }, - { - "slot": 1, - "urn": "wave" - }, - { - "slot": 2, - "urn": "fistpump" - }, - { - "slot": 3, - "urn": "dance" - }, - { - "slot": 4, - "urn": "raiseHand" - }, - { - "slot": 5, - "urn": "clap" - }, - { - "slot": 6, - "urn": "money" - }, - { - "slot": 7, - "urn": "kiss" - }, - { - "slot": 8, - "urn": "headexplode" - }, - { - "slot": 9, - "urn": "shrug" - } - ], - "snapshots": { - "body": "bafkreiepqf6n4j3iypqgtnkqykeopqx6cqzvbvqdvrbeyzlhgpsaqpxygy", - "face256": "bafkreifc54aqlg2r2qppcsyqzx6skixhjlkuq3djqwm5sgvqwvv2cyeo7i" - }, - "eyes": { - "color": { - "r": 0.37109375, - "g": 0.22265625, - "b": 0.1953125, - "a": 1 - } - }, - "hair": { - "color": { - "r": 0.98046875, - "g": 0.82421875, - "b": 0.5078125, - "a": 1 - } - }, - "skin": { - "color": { - "r": 0.60546875, - "g": 0.4609375, - "b": 0.35546875, - "a": 1 - } - } - } - }, - { - "entity": "bafkreia6x2mlr2mmdry66czgpze6khwcdmcpqbkjeu7bmrydzaxf3v4x6q", - "destPath": "output/bafkreia6x2mlr2mmdry66czgpze6khwcdmcpqbkjeu7bmrydzaxf3v4x6q.png", - "width": 256, - "height": 512, - "faceDestPath": "output/bafkreia6x2mlr2mmdry66czgpze6khwcdmcpqbkjeu7bmrydzaxf3v4x6q_face.png", - "faceWidth": 256, - "faceHeight": 256, - "avatar": { - "bodyShape": "urn:decentraland:off-chain:base-avatars:BaseFemale", - "wearables": [ - "urn:decentraland:off-chain:base-avatars:baggy_pullover", - "urn:decentraland:off-chain:base-avatars:f_brown_skirt", - "urn:decentraland:off-chain:base-avatars:Espadrilles", - "urn:decentraland:off-chain:base-avatars:hair_f_oldie_02", - "urn:decentraland:off-chain:base-avatars:f_mouth_05", - "urn:decentraland:off-chain:base-avatars:eyebrows_01", - "urn:decentraland:off-chain:base-avatars:eyes_02", - "urn:decentraland:off-chain:base-avatars:f_glasses_fashion", - "urn:decentraland:off-chain:base-avatars:blue_star_earring", - "urn:decentraland:off-chain:base-avatars:blue_bandana" - ], - "forceRender": [], - "emotes": [ - { - "slot": 0, - "urn": "handsair" - }, - { - "slot": 1, - "urn": "wave" - }, - { - "slot": 2, - "urn": "fistpump" - }, - { - "slot": 3, - "urn": "dance" - }, - { - "slot": 4, - "urn": "raiseHand" - }, - { - "slot": 5, - "urn": "clap" - }, - { - "slot": 6, - "urn": "money" - }, - { - "slot": 7, - "urn": "kiss" - }, - { - "slot": 8, - "urn": "headexplode" - }, - { - "slot": 9, - "urn": "shrug" - } - ], - "snapshots": { - "body": "bafkreigpuh2ccixcv6szsq2oqxngal6oupjqd52hx6w6xaddmrvsjndmw4", - "face256": "bafkreihjpp3hrtsgmx6tcmffnt2mbfakyt6s3muxejyddbyafiaf2ljqny" - }, - "eyes": { - "color": { - "r": 0.125490203499794, - "g": 0.7019608020782471, - "b": 0.9647058844566345, - "a": 1 - } - }, - "hair": { - "color": { - "r": 0.5960784554481506, - "g": 0.37254902720451355, - "b": 0.21568627655506134, - "a": 1 - } - }, - "skin": { - "color": { - "r": 0.800000011920929, - "g": 0.6078431606292725, - "b": 0.46666666865348816, - "a": 1 - } - } - } - }, - { - "entity": "mati_avatar_test", - "destPath": "output/mati_avatar_test.png", - "width": 256, - "height": 512, - "faceDestPath": "output/mati_avatar_test_face.png", - "faceWidth": 256, - "faceHeight": 256, - "avatar": { - "bodyShape": "urn:decentraland:off-chain:base-avatars:BaseMale", - "wearables": [ - "urn:decentraland:off-chain:base-avatars:eyebrows_00", - "urn:decentraland:off-chain:base-avatars:eyes_06", - "urn:decentraland:off-chain:base-avatars:mouth_03", - "urn:decentraland:off-chain:base-avatars:beard", - "urn:decentraland:off-chain:base-avatars:short_hair", - "urn:decentraland:matic:collections-v2:0xf1483f042614105cb943d3dd67157256cd003028:6:631873750011343120187508166102022593913370572403294667525865865225", - "urn:decentraland:matic:collections-v2:0xd50191baed16bc532feb9d499fdaa805fe01d3ff:8:842498333348457493583344221469363458551160763204392890034487820292", - "urn:decentraland:matic:collections-v2:0xf1483f042614105cb943d3dd67157256cd003028:16:1684996666696914987166688442938726917102321526408785780068975640582", - "urn:decentraland:matic:collections-v2:0xd05723401566e9d9b7a728bd4dbe07584cf8ac76:5:526561458342785933489590138418352161594475477002745556271554887855" - ], - "forceRender": [], - "emotes": [ - { - "slot": 0, - "urn": "handsair" - }, - { - "slot": 6, - "urn": "money" - }, - { - "slot": 7, - "urn": "kiss" - }, - { - "slot": 8, - "urn": "tik" - }, - { - "slot": 9, - "urn": "shrug" - } - ], - "snapshots": { - "body": "https://peer-ec1.decentraland.org/content/contents/bafkreidhmzu5wwcssv3f65wfs7swnncalwarfqavsfifxvclgv32ku3zdu", - "face256": "https://peer-ec1.decentraland.org/content/contents/bafkreigea3qbj56h6erxvt524hah2mlxnvqkpazjelcotktxldecvraohq" - }, - "eyes": { - "color": { - "r": 0.22265625, - "g": 0.484375, - "b": 0.69140625, - "a": 1 - } - }, - "hair": { - "color": { - "r": 0.98046875, - "g": 0.82421875, - "b": 0.5078125, - "a": 1 - } - }, - "skin": { - "color": { - "r": 0.94921875, - "g": 0.76171875, - "b": 0.6484375, - "a": 1 - } - } - } - } - ] -} \ No newline at end of file diff --git a/godot/src/tool/scene_renderer/scene_camera_tune.gd b/godot/src/tool/scene_renderer/scene_camera_tune.gd new file mode 100644 index 000000000..fb5463d7a --- /dev/null +++ b/godot/src/tool/scene_renderer/scene_camera_tune.gd @@ -0,0 +1,57 @@ +extends Control + +signal camera_params_updated(type, fov, ortho_size, param_position, target) + +var fov: float = 60.0 +var ortho_size: float = 10.0 + +@onready var option_button_projection = $Panel/OptionButton_Projection +@onready var line_edit_fov_size = $Panel/LineEdit_FOV_Size +@onready var line_edit_target_x = $Panel/HBoxContainer_Target/LineEdit_X +@onready var line_edit_target_y = $Panel/HBoxContainer_Target/LineEdit_Y +@onready var line_edit_target_z = $Panel/HBoxContainer_Target/LineEdit_Z +@onready var line_edit_position_x = $Panel/HBoxContainer_Position/LineEdit_X +@onready var line_edit_position_y = $Panel/HBoxContainer_Position/LineEdit_Y +@onready var line_edit_position_z = $Panel/HBoxContainer_Position/LineEdit_Z + + +func _on_option_button_projection_item_selected(index): + if index == 0: # perspective + line_edit_fov_size.text = str(fov) + else: + line_edit_fov_size.text = str(ortho_size) + + emit_updated() + + +func _on_line_edit_text_changed(_new_text): + emit_updated() + + +func _on_line_edit_fov_size_text_changed(_new_text): + if option_button_projection.selected == 0: + fov = float(line_edit_fov_size.text) + else: + ortho_size = float(line_edit_fov_size.text) + + emit_updated() + + +func emit_updated(): + var camera_option: Camera3D.ProjectionType + if option_button_projection.selected == 0: + camera_option = Camera3D.PROJECTION_PERSPECTIVE + else: + camera_option = Camera3D.PROJECTION_ORTHOGONAL + + var param_position = Vector3( + float(line_edit_position_x.text), + float(line_edit_position_y.text), + float(line_edit_position_z.text) + ) + var param_target = Vector3( + float(line_edit_target_x.text), + float(line_edit_target_y.text), + float(line_edit_target_z.text) + ) + camera_params_updated.emit(camera_option, fov, ortho_size, param_position, param_target) diff --git a/godot/src/tool/scene_renderer/scene_camera_tune.tscn b/godot/src/tool/scene_renderer/scene_camera_tune.tscn new file mode 100644 index 000000000..e4ba9188e --- /dev/null +++ b/godot/src/tool/scene_renderer/scene_camera_tune.tscn @@ -0,0 +1,144 @@ +[gd_scene load_steps=2 format=3 uid="uid://dx7885dtt4yyn"] + +[ext_resource type="Script" path="res://src/tool/scene_renderer/scene_camera_tune.gd" id="1_1rcic"] + +[node name="SceneCameraTune" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_1rcic") + +[node name="Panel" type="Panel" parent="."] +layout_mode = 0 +offset_left = 977.0 +offset_top = 361.0 +offset_right = 1250.0 +offset_bottom = 693.0 + +[node name="Label" type="Label" parent="Panel"] +modulate = Color(0, 0, 0, 1) +layout_mode = 1 +anchors_preset = 10 +anchor_right = 1.0 +offset_top = 8.0 +offset_bottom = 28.0 +grow_horizontal = 2 +text = "Camera control" +horizontal_alignment = 1 + +[node name="Label2" type="Label" parent="Panel"] +modulate = Color(0, 0, 0, 1) +layout_mode = 1 +anchors_preset = 10 +anchor_right = 1.0 +offset_top = 40.0 +offset_bottom = 60.0 +grow_horizontal = 2 +text = "Type" +horizontal_alignment = 1 + +[node name="Label3" type="Label" parent="Panel"] +modulate = Color(0, 0, 0, 1) +layout_mode = 1 +anchors_preset = 10 +anchor_right = 1.0 +offset_top = 104.0 +offset_bottom = 124.0 +grow_horizontal = 2 +text = "FOV/OrthoSize" +horizontal_alignment = 1 + +[node name="Label4" type="Label" parent="Panel"] +modulate = Color(0, 0, 0, 1) +layout_mode = 1 +anchors_preset = 10 +anchor_right = 1.0 +offset_top = 176.0 +offset_bottom = 196.0 +grow_horizontal = 2 +text = "Position" +horizontal_alignment = 1 + +[node name="Label5" type="Label" parent="Panel"] +modulate = Color(0, 0, 0, 1) +layout_mode = 1 +anchors_preset = 10 +anchor_right = 1.0 +offset_top = 256.0 +offset_bottom = 276.0 +grow_horizontal = 2 +text = "Target" +horizontal_alignment = 1 + +[node name="OptionButton_Projection" type="OptionButton" parent="Panel"] +layout_mode = 1 +anchors_preset = 10 +anchor_right = 1.0 +offset_top = 64.0 +offset_bottom = 94.0 +grow_horizontal = 2 +item_count = 2 +selected = 0 +popup/item_0/text = "Perspective" +popup/item_0/id = 0 +popup/item_1/text = "Orthogonal" +popup/item_1/id = 1 + +[node name="LineEdit_FOV_Size" type="LineEdit" parent="Panel"] +layout_mode = 1 +anchors_preset = 10 +anchor_right = 1.0 +offset_top = 128.0 +offset_bottom = 167.0 +grow_horizontal = 2 +text = "75" + +[node name="HBoxContainer_Position" type="HBoxContainer" parent="Panel"] +layout_mode = 0 +offset_top = 200.0 +offset_right = 260.0 +offset_bottom = 240.0 +alignment = 1 + +[node name="LineEdit_X" type="LineEdit" parent="Panel/HBoxContainer_Position"] +layout_mode = 2 +text = "0.0" + +[node name="LineEdit_Y" type="LineEdit" parent="Panel/HBoxContainer_Position"] +layout_mode = 2 +text = "8.0" + +[node name="LineEdit_Z" type="LineEdit" parent="Panel/HBoxContainer_Position"] +layout_mode = 2 +text = "0.0" + +[node name="HBoxContainer_Target" type="HBoxContainer" parent="Panel"] +layout_mode = 0 +offset_top = 280.0 +offset_right = 260.0 +offset_bottom = 320.0 +alignment = 1 + +[node name="LineEdit_X" type="LineEdit" parent="Panel/HBoxContainer_Target"] +layout_mode = 2 +text = "0.0" + +[node name="LineEdit_Y" type="LineEdit" parent="Panel/HBoxContainer_Target"] +layout_mode = 2 +text = "0.0" + +[node name="LineEdit_Z" type="LineEdit" parent="Panel/HBoxContainer_Target"] +layout_mode = 2 +text = "0.0" + +[connection signal="item_selected" from="Panel/OptionButton_Projection" to="." method="_on_option_button_projection_item_selected"] +[connection signal="text_changed" from="Panel/LineEdit_FOV_Size" to="." method="_on_line_edit_fov_size_text_changed"] +[connection signal="text_changed" from="Panel/HBoxContainer_Position/LineEdit_X" to="." method="_on_line_edit_text_changed"] +[connection signal="text_changed" from="Panel/HBoxContainer_Position/LineEdit_Y" to="." method="_on_line_edit_text_changed"] +[connection signal="text_changed" from="Panel/HBoxContainer_Position/LineEdit_Z" to="." method="_on_line_edit_text_changed"] +[connection signal="text_changed" from="Panel/HBoxContainer_Target/LineEdit_X" to="." method="_on_line_edit_text_changed"] +[connection signal="text_changed" from="Panel/HBoxContainer_Target/LineEdit_Y" to="." method="_on_line_edit_text_changed"] +[connection signal="text_changed" from="Panel/HBoxContainer_Target/LineEdit_Z" to="." method="_on_line_edit_text_changed"] diff --git a/godot/src/tool/scene_renderer/scene_orchestor.gd b/godot/src/tool/scene_renderer/scene_orchestor.gd new file mode 100644 index 000000000..e5ab29205 --- /dev/null +++ b/godot/src/tool/scene_renderer/scene_orchestor.gd @@ -0,0 +1,334 @@ +extends Node + +enum PayloadState { NONE = 0, LOADING, PROCESSING, DONE } + +const USE_TEST_INPUT = false +const DEFAULT_TIMEOUT_REALM_SECONDS = 15.0 +const DEFAULT_TIMEOUT_TEST_SECONDS = 15.0 +const DEFAULT_TICK_TO_BE_LOADED = 10 + +var logs: Array[String] = [] + +var scenes_to_process: SceneRendererInputHelper.SceneInputFile +var current_payload_index: int = 0 +var current_payload_state: PayloadState = PayloadState.NONE +var timeout_count: int = 0 +var scene_already_telep: bool = false + +var realm_change_emited: bool = false + +var test_camera_node: DclCamera3D +var test_player_node: Node3D +var test_camera_tune: bool = false +var test_camera_tune_base_position: Vector3 + +var are_all_scene_loaded: bool = false + + +# TODO: this can be a command line parser and get some helpers like get_string("--realm"), etc +func get_params_from_cmd(): + var args := OS.get_cmdline_args() + var scene_data = null + var camera_tune := args.find("--test-camera-tune") != -1 + + # Only use from the editor + if USE_TEST_INPUT or args.has("--use-test-input"): + print("scene-renderer: using test input") + scene_data = SceneRendererInputHelper.SceneInputFile.from_file_path( + "res://../tests/scene-renderer-test-input.json" + ) + else: + var scene_in_place := args.find("--scene-input-file") + + if scene_in_place != -1 and args.size() > scene_in_place + 1 and scene_data == null: + var file_path: String = args[scene_in_place + 1] + prints("scene-renderer: using input file from command line", file_path) + scene_data = SceneRendererInputHelper.SceneInputFile.from_file_path(file_path) + + return [scene_data, camera_tune] + + +func _ready(): + print("scene-renderer: running") + var from_params = get_params_from_cmd() + if from_params[0] == null: + printerr("param is missing or wrong, try with --scene-input-file [file]") + get_tree().quit(1) + return + + scenes_to_process = from_params[0] + test_camera_tune = from_params[1] + if scenes_to_process.scenes.is_empty(): + printerr("no scene input to process") + get_tree().quit(2) + return + + Global.get_explorer().disable_move_to = true + + Global.realm.realm_changed.connect(self.on_realm_changed) + + Global.realm.async_set_realm(scenes_to_process.realm_url) + prints( + "scene-renderer: realm", + scenes_to_process.realm_url, + "- scenes number:", + scenes_to_process.scenes.size() + ) + + get_tree().create_timer(DEFAULT_TIMEOUT_REALM_SECONDS).timeout.connect( + self.on_realm_change_timeout + ) + + if test_camera_tune: + var test_camera_tune_scene = ( + load("res://src/tool/scene_renderer/scene_camera_tune.tscn").instantiate() + ) + add_child(test_camera_tune_scene) + test_camera_tune_scene.camera_params_updated.connect(self.on_camera_params_updated) + + +func on_camera_params_updated( + type: Camera3D.ProjectionType, + fov: float, + ortho_size: float, + param_position: Vector3, + param_target: Vector3 +) -> void: + var viewport = get_viewport() + var camera = viewport.get_camera_3d() + + camera_set( + camera, type, fov, ortho_size, param_position, param_target, test_camera_tune_base_position + ) + + +func camera_set( + camera: Camera3D, + type: Camera3D.ProjectionType, + fov: float, + ortho_size: float, + param_position: Vector3, + param_target: Vector3, + base_position: Vector3 +): + var global_position = base_position + param_position + var look_at_position = base_position + param_target + + var up = Vector3.UP + if up.cross(look_at_position - global_position).is_zero_approx(): + up = Vector3.FORWARD + + camera.fov = max(min(fov, 179), 1) + camera.size = max(ortho_size, 0.001) + camera.projection = type + + camera.global_position = global_position + camera.look_at(look_at_position, up) + + +func on_realm_change_timeout(): + if not realm_change_emited: + printerr(str(DEFAULT_TIMEOUT_REALM_SECONDS) + " seconds realm changed timeout") + get_tree().quit(1) + return + + realm_change_emited = true + + +func on_realm_changed(): + realm_change_emited = true + self.process_mode = Node.PROCESS_MODE_ALWAYS + + test_camera_node = Global.scene_runner.camera_node + test_player_node = Global.scene_runner.player_node + + Global.scene_runner.set_camera_and_player_node( + test_camera_node, test_player_node, self._on_scene_console_message + ) + Global.scene_fetcher.set_scene_radius(0) + Global.comms.change_adapter("offline") + + +func _on_scene_console_message(scene_id: int, level: int, timestamp: float, text: String) -> void: + prints("SCENE_LOG", scene_id, level, timestamp, text) + + +func get_scene_child(scene_id: int) -> DclSceneNode: + var scene_child: DclSceneNode = null + for child in Global.scene_runner.get_children(): + var this_scene_progress: float = 0.0 + if child is DclSceneNode: + if child.get_scene_id() == scene_id: + scene_child = child + + return scene_child + + +func _process(_delta): + are_all_scene_loaded = true + + # tick limiter! + for child: DclSceneNode in Global.scene_runner.get_children(): + if child.get_last_tick_number() > DEFAULT_TICK_TO_BE_LOADED: + if not Global.scene_runner.get_scene_is_paused(child.get_scene_id()): + print( + "Pausing the scene ", Global.scene_runner.get_scene_title(child.get_scene_id()) + ) + Global.scene_runner.set_scene_is_paused(child.get_scene_id(), true) + else: + are_all_scene_loaded = false + + +func _on_timer_timeout(): + # Continue only when every pointer around was fetched + if Global.scene_fetcher.scene_entity_coordinator.is_busy(): + return + + if current_payload_index >= scenes_to_process.scenes.size(): + Global.testing_tools.exit_gracefully(0) + return + + var scene := scenes_to_process.scenes[current_payload_index] + var scene_id: int = Global.scene_fetcher.get_parcel_scene_id(scene.coords.x, scene.coords.y) + var scene_child: DclSceneNode = get_scene_child(scene_id) + + match current_payload_state: + PayloadState.NONE: + test_player_node.global_position = Vector3( + scene.coords.x * 16.0 + 8.0, 1.0, -scene.coords.y * 16.0 - 8.0 + ) + + test_camera_tune_base_position = Vector3( + scene.coords.x * 16.0, 0.0, -scene.coords.y * 16.0 + ) + + current_payload_state = PayloadState.LOADING + Global.scene_fetcher.set_scene_radius(scene.scene_distance) + timeout_count = 0 + + PayloadState.LOADING: + if are_all_scene_loaded: + current_payload_state = PayloadState.PROCESSING + if not test_camera_tune: + if scene.dest_path.ends_with(".png"): + async_take_camera_photo(scene) + elif scene.dest_path.ends_with(".glb"): + async_take_scene_file(scene, scene_child) + else: + # do nothing? + current_payload_state = PayloadState.DONE + else: + var camera = FreeLookCamera.new() + add_child(camera) + camera.make_current() + + print("ready") + else: + timeout_count += 1 + if timeout_count > 10: + if timeout_count % 10 == 0: + print("Waiting for scenes:") + for child: DclSceneNode in Global.scene_runner.get_children(): + var t = child.get_last_tick_number() + if t > DEFAULT_TICK_TO_BE_LOADED: + pass + else: + print( + "\t-", + Global.scene_runner.get_scene_title(child.get_scene_id()), + t + ) + + PayloadState.DONE: + current_payload_index += 1 + current_payload_state = PayloadState.NONE + + +func async_take_scene_file( + input: SceneRendererInputHelper.SceneRendererInputSpecs, child: DclSceneNode +): + var pending_promises := Global.content_provider.get_pending_promises() + if not pending_promises.is_empty(): + await PromiseUtils.async_all(Global.content_provider.get_pending_promises()) + + var dest_path = input.dest_path.replacen("$index", str(input.index)).replacen( + "$coords", str(input.coords.x) + "_" + str(input.coords.y) + ) + var gltf_document_save := GLTFDocument.new() + var gltf_state_save := GLTFState.new() + gltf_document_save.append_from_scene(child, gltf_state_save) + gltf_document_save.write_to_filesystem(gltf_state_save, dest_path) + + +func async_take_camera_photo(input: SceneRendererInputHelper.SceneRendererInputSpecs): + prints("async_take_camera_photo", input) + + var pending_promises := Global.content_provider.get_pending_promises() + if not pending_promises.is_empty(): + await PromiseUtils.async_all(Global.content_provider.get_pending_promises()) + + RenderingServer.set_default_clear_color(Color(0, 0, 0, 0)) + var viewport = get_viewport() + var previous_camera = viewport.get_camera_3d() + + var test_camera_3d = Camera3D.new() + add_child(test_camera_3d) + test_camera_3d.make_current() + + var previous_viewport_size = viewport.size + viewport.size = Vector2i(input.width, input.height) + + var base_position := Vector3(input.coords.x * 16.0, 0.0, -input.coords.y * 16.0) + var camera_type + if input.camera.projection == "ortho": + camera_type = Camera3D.PROJECTION_ORTHOGONAL + else: + camera_type = Camera3D.PROJECTION_PERSPECTIVE + + camera_set( + test_camera_3d, + camera_type, + input.camera.fov, + input.camera.ortho_size, + input.camera.position, + input.camera.target, + base_position + ) + + var explorer = Global.get_explorer() + explorer.set_visible_ui(false) + Global.scene_runner.base_ui.visible = false + + # Freeze avatars animation and hide them + for avatar in Global.avatars.get_children(): + if avatar is Avatar: + avatar.hide() + avatar.emote_controller.freeze_on_idle() + + Global.scene_runner.player_node.avatar.emote_controller.freeze_on_idle() + Global.scene_runner.player_node.avatar.hide() + + await get_tree().process_frame + await get_tree().process_frame + await get_tree().process_frame + + var viewport_img := viewport.get_texture().get_image() + + # Test: Uncomment this to see how the snapshot would look like + # await get_tree().create_timer(10.0).timeout + + get_node("/root/explorer").set_visible_ui(true) + Global.scene_runner.base_ui.visible = true + # TODO: should unfreeze avatars? + + viewport.size = previous_viewport_size + previous_camera.make_current() + remove_child(test_camera_3d) + test_camera_3d.queue_free() + + var dest_path = input.dest_path.replacen("$index", str(input.index)).replacen( + "$coords", str(input.coords.x) + "_" + str(input.coords.y) + ) + viewport_img.save_png(dest_path) + + current_payload_state = PayloadState.DONE diff --git a/godot/src/tool/scene_renderer/scene_orchestor.tscn b/godot/src/tool/scene_renderer/scene_orchestor.tscn new file mode 100644 index 000000000..1a9585e25 --- /dev/null +++ b/godot/src/tool/scene_renderer/scene_orchestor.tscn @@ -0,0 +1,11 @@ +[gd_scene load_steps=2 format=3 uid="uid://cm7kmfoxdq31o"] + +[ext_resource type="Script" path="res://src/tool/scene_renderer/scene_orchestor.gd" id="1_nee4c"] + +[node name="SceneOrchestor" type="Node"] +script = ExtResource("1_nee4c") + +[node name="Timer_Process" type="Timer" parent="."] +autostart = true + +[connection signal="timeout" from="Timer_Process" to="." method="_on_timer_timeout"] diff --git a/godot/src/tool/scene_renderer/scene_renderer_input_helper.gd b/godot/src/tool/scene_renderer/scene_renderer_input_helper.gd new file mode 100644 index 000000000..82135601e --- /dev/null +++ b/godot/src/tool/scene_renderer/scene_renderer_input_helper.gd @@ -0,0 +1,104 @@ +class_name SceneRendererInputHelper +extends RefCounted + + +class CameraOption: + var position: Vector3 + var target: Vector3 + var fov: float + var ortho_size: float + var projection: String + + static func from_dictionary(value: Dictionary, default: CameraOption) -> CameraOption: + var ret = CameraOption.new() + + ret.position = Vector3( + value.get("position", {}).get("x", default.position.x), + value.get("position", {}).get("y", default.position.y), + value.get("position", {}).get("z", default.position.z) + ) + + ret.target = Vector3( + value.get("target", {}).get("x", default.target.x), + value.get("target", {}).get("y", default.target.y), + value.get("target", {}).get("z", default.target.z) + ) + + ret.projection = value.get("projection", default.projection) + ret.fov = value.get("fov", default.fov) + ret.ortho_size = value.get("orthoSize", default.ortho_size) + + return ret + + +class SceneRendererInputSpecs: + var coords := Vector2i(-9999, -9999) + var scene_distance := 0 + var width := 2048 + var height := 2048 + var dest_path := "" + var camera := CameraOption.new() + var index: int = -1 + + static func from_dictionary( + value: Dictionary, default: SceneRendererInputSpecs + ) -> SceneRendererInputSpecs: + var ret = SceneRendererInputSpecs.new() + + var coord_array = value.get("coords", "").split(",") + if coord_array.size() == 2: + ret.coords = Vector2i(int(coord_array[0]), int(coord_array[1])) + else: + ret.coords = default.coords + + ret.dest_path = value.get("destPath", default.dest_path) + ret.scene_distance = value.get("sceneDistance", default.scene_distance) + ret.width = value.get("width", default.width) + ret.height = value.get("height", default.height) + ret.camera = CameraOption.from_dictionary(value.get("camera", {}), default.camera) + + return ret + + +class SceneInputFile: + var realm_url: String + var scenes: Array[SceneRendererInputSpecs] + + static func from_file_path(file_path: String): + var file = FileAccess.open(file_path, FileAccess.READ) + if file == null: + return null + + var json_value = JSON.parse_string(file.get_as_text()) + if json_value == null or not json_value is Dictionary: + printerr("the file has to be a valid json dictionary") + return null + + var tmp_realm_url = json_value.get("realmUrl") + var tmp_payload = json_value.get("payload") + var default_payload = SceneRendererInputSpecs.from_dictionary( + json_value.get("defaultPayload", {}), SceneRendererInputSpecs.new() + ) + if not ([tmp_realm_url, tmp_payload].all(func(v): return v != null)): + printerr("baseUrl and payload property has to be included in the the file dictionary") + return null + + if not tmp_payload is Array: + printerr("payload has to be an array") + return null + + var ret := SceneInputFile.new() + + ret.realm_url = tmp_realm_url + ret.scenes = [] + var i = 0 + for maybe_entry in tmp_payload: + var scene: SceneRendererInputSpecs = SceneRendererInputSpecs.from_dictionary( + maybe_entry, default_payload + ) + if scene != null: + scene.index = i + ret.scenes.push_back(scene) + i += 1 + + return ret diff --git a/godot/src/ui/components/discover/places/places_generator.gd b/godot/src/ui/components/discover/places/places_generator.gd index 5b44fe9f5..6a4d6be7c 100644 --- a/godot/src/ui/components/discover/places/places_generator.gd +++ b/godot/src/ui/components/discover/places/places_generator.gd @@ -179,7 +179,7 @@ func on_request(offset: int, limit: int) -> void: func _async_fetch_places(url: String, limit: int = 100): - var headers = ["Content-Type: application/json"] + var headers = {"Content-Type": "application/json"} var promise: Promise = Global.http_requester.request_json( url, HTTPClient.METHOD_GET, "", headers ) diff --git a/godot/src/ui/components/profile_settings/lambda_names_request.gd b/godot/src/ui/components/profile_settings/lambda_names_request.gd index 10c087086..6ac6b1d98 100644 --- a/godot/src/ui/components/profile_settings/lambda_names_request.gd +++ b/godot/src/ui/components/profile_settings/lambda_names_request.gd @@ -37,7 +37,7 @@ static func _async_request( url += "?pageNum=%d" % page_number url += "&pageSize=%d" % page_size - var promise: Promise = Global.http_requester.request_json(url, HTTPClient.METHOD_GET, "", []) + var promise: Promise = Global.http_requester.request_json(url, HTTPClient.METHOD_GET, "", {}) var result = await PromiseUtils.async_awaiter(promise) diff --git a/godot/src/ui/explorer.gd b/godot/src/ui/explorer.gd index 7509633c2..9c961d123 100644 --- a/godot/src/ui/explorer.gd +++ b/godot/src/ui/explorer.gd @@ -9,6 +9,7 @@ var panel_bottom_left_height: int = 0 var dirty_save_position: bool = false var debug_panel = null +var disable_move_to = false var virtual_joystick_orig_position: Vector2i @@ -185,6 +186,13 @@ func _ready(): # last ui_root.grab_focus.call_deferred() + if OS.get_cmdline_args().has("--scene-renderer"): + prints("load scene_orchestor") + var scene_renderer_orchestor = ( + load("res://src/tool/scene_renderer/scene_orchestor.tscn").instantiate() + ) + add_child(scene_renderer_orchestor) + func _on_need_open_url(url: String, _description: String) -> void: if not Global.player_identity.get_address_str().is_empty(): @@ -345,6 +353,8 @@ func _on_control_menu_request_pause_scenes(enabled): func move_to(position: Vector3, skip_loading: bool): + if disable_move_to: + return player.set_position(position) var cur_parcel_position = Vector2(player.position.x * 0.0625, -player.position.z * 0.0625) if not skip_loading: diff --git a/lib/src/analytics/metrics.rs b/lib/src/analytics/metrics.rs index bae6017d1..7cec44a49 100644 --- a/lib/src/analytics/metrics.rs +++ b/lib/src/analytics/metrics.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; use godot::{engine::Timer, prelude::*}; @@ -184,7 +184,10 @@ impl Metrics { http::Method::POST, ResponseType::AsString, Some(json_body.as_bytes().to_vec()), - Some(vec!["Content-Type: application/json".to_string()]), + Some(HashMap::from([( + "Content-Type".to_string(), + "application/json".to_string(), + )])), None, ); if let Err(err) = http_requester.request(request, 0).await { diff --git a/lib/src/comms/signed_login.rs b/lib/src/comms/signed_login.rs index 0159a2c8e..a5b9ab587 100644 --- a/lib/src/comms/signed_login.rs +++ b/lib/src/comms/signed_login.rs @@ -1,9 +1,12 @@ // https://github.com/decentraland/hammurabi/pull/33/files#diff-18afcd5f94e3688aad1ba36fa1db3e09b472b271d1e0cf5aeb59ebd32f43a328 +use std::collections::HashMap; + use http::{Method, Uri}; use crate::{ auth::ephemeral_auth_chain::EphemeralAuthChain, + godot_classes::dcl_global::DclGlobal, http_request::{ http_queue_requester::HttpQueueRequester, request_response::{RequestOption, ResponseEnum, ResponseType}, @@ -74,16 +77,12 @@ impl SignedLogin { let mut chain = ephemeral_auth_chain.auth_chain().clone(); chain.add_signed_entity(payload, signature); - let mut headers = Vec::from_iter( - chain - .headers() - .map(|(key, value)| format!("{}: {}", key, value)), - ); - - headers.push(format!("x-identity-timestamp: {unix_time}")); - headers.push(format!("x-identity-metadata: {meta}")); + let mut headers = HashMap::from_iter(chain.headers()); + headers.insert("x-identity-timestamp".into(), format!("{unix_time}")); + headers.insert("x-identity-metadata".into(), meta.to_string()); - let http_requester = HttpQueueRequester::new(1); + let http_requester = + HttpQueueRequester::new(1, DclGlobal::get_network_inspector_sender()); let response = http_requester .request( RequestOption::new( diff --git a/lib/src/content/content_provider.rs b/lib/src/content/content_provider.rs index 8bf150994..6176946bc 100644 --- a/lib/src/content/content_provider.rs +++ b/lib/src/content/content_provider.rs @@ -20,6 +20,7 @@ use crate::{ dcl::common::string::FindNthChar, godot_classes::{ dcl_config::{DclConfig, TextureQuality}, + dcl_global::DclGlobal, promise::Promise, resource_locker::ResourceLocker, }, @@ -108,7 +109,10 @@ impl INode for ContentProvider { )), #[cfg(feature = "use_resource_tracking")] resource_download_tracking, - http_queue_requester: Arc::new(HttpQueueRequester::new(6)), + http_queue_requester: Arc::new(HttpQueueRequester::new( + 6, + DclGlobal::get_network_inspector_sender(), + )), content_folder, cached: HashMap::new(), godot_single_thread: Arc::new(Semaphore::new(1)), diff --git a/lib/src/content/wearable_entities.rs b/lib/src/content/wearable_entities.rs index 1136fcb8d..2626b1096 100644 --- a/lib/src/content/wearable_entities.rs +++ b/lib/src/content/wearable_entities.rs @@ -35,7 +35,7 @@ pub async fn request_wearables( ctx: ContentProviderContext, ) -> Result, anyhow::Error> { let url = format!("{content_server_base_url}entities/active"); - let headers = vec![("Content-Type: application/json".to_string())]; + let headers = HashMap::from([("Content-Type".into(), "application/json".into())]); let payload = serde_json::to_string(&EntitiesRequest { pointers: pointers.clone(), }) diff --git a/lib/src/dcl/js/fetch/mod.rs b/lib/src/dcl/js/fetch/mod.rs index df782931a..955816c92 100644 --- a/lib/src/dcl/js/fetch/mod.rs +++ b/lib/src/dcl/js/fetch/mod.rs @@ -5,12 +5,16 @@ use http::HeaderValue; use reqwest::Response; use serde::Serialize; +use crate::tools::network_inspector::{ + NetworkInspectEvent, NetworkInspectRequestPayload, NetworkInspectResponsePayload, + NetworkInspectorId, NetworkInspectorSender, +}; + mod signed_fetch; pub fn ops() -> Vec { vec![ op_fetch_custom(), - op_fetch_consume_json(), op_fetch_consume_text(), op_fetch_consume_bytes(), signed_fetch::op_signed_fetch_headers(), @@ -39,6 +43,8 @@ struct FetchResponse { #[serde(rename = "type")] _type: String, url: String, + + network_inspector_id: u32, } impl FetchRequestsState { @@ -70,6 +76,10 @@ async fn op_fetch_custom( #[string] _redirect: String, // TODO: unimplemented timeout: u32, ) -> Result { + let maybe_network_inspector_sender = op_state + .borrow() + .try_borrow::() + .cloned(); let has_fetch_state = op_state.borrow().has::(); if !has_fetch_state { op_state @@ -114,11 +124,6 @@ async fn op_fetch_custom( HeaderValue::from_static("https://decentraland.org"), ); - let mut request = client - .request(method, url.clone()) - .headers(headers) - .timeout(Duration::from_secs(timeout as u64)); - // match redirect.as_str() { // "follow" => {} // "error" => {} @@ -126,6 +131,38 @@ async fn op_fetch_custom( // _ => {} // }; + // Inspect Network + let mut network_inspector_id = NetworkInspectorId::INVALID; + if let Some(network_inspector_sender) = maybe_network_inspector_sender.as_ref() { + let (inspect_event_id, inspect_event) = + NetworkInspectEvent::new_request(NetworkInspectRequestPayload { + url: url.clone(), + method: method.clone(), + body: if has_body { + Some(body_data.clone().as_bytes().to_vec()) + } else { + None + }, + headers: Some( + headers + .iter() + .map(|(key, value)| { + (key.to_string(), value.to_str().unwrap_or("").to_string()) + }) + .collect(), + ), + }); + network_inspector_id = inspect_event_id; + if let Err(err) = network_inspector_sender.try_send(inspect_event) { + tracing::error!("Error sending inspect event: {}", err); + } + } + + let mut request = client + .request(method.clone(), url.clone()) + .headers(headers) + .timeout(Duration::from_secs(timeout as u64)); + if has_body { request = request.body(body_data); } @@ -144,6 +181,24 @@ async fn op_fetch_custom( })); current_request.response = Some(response); + drop(state); + + // Inspect Network + if network_inspector_id.is_valid() { + if let Some(network_inspector_sender) = maybe_network_inspector_sender.as_ref() { + let inspect_event = NetworkInspectEvent::new_partial_response( + network_inspector_id, + Ok(NetworkInspectResponsePayload { + status_code: status, + headers: Some(headers.clone()), + }), + ); + if let Err(err) = network_inspector_sender.try_send(inspect_event) { + tracing::error!("Error sending inspect event: {}", err); + } + } + } + let js_response = FetchResponse { ok: true, _internal_req_id: req_id, @@ -153,40 +208,40 @@ async fn op_fetch_custom( status_text: status.to_string(), _type: "basic".into(), // TODO url: url.clone(), + network_inspector_id: network_inspector_id.to_u32(), }; + Ok(js_response) } - Err(err) => Ok(FetchResponse { - _internal_req_id: req_id, - headers: HashMap::new(), - ok: false, - redirected: false, - status: 0, - status_text: err.to_string(), - _type: "error".into(), - url: url.clone(), - }), - } -} + Err(err) => { + drop(state); -#[op2(async)] -#[serde] -async fn op_fetch_consume_json( - op_state: Rc>, - req_id: u32, -) -> Result { - let response = { - let mut state = op_state.borrow_mut(); - let fetch_request = state.borrow_mut::(); - let current_request = fetch_request.requests.get_mut(&req_id).unwrap(); - current_request.response.take() - }; + // Inspect Network + if network_inspector_id.is_valid() { + if let Some(network_inspector_sender) = maybe_network_inspector_sender.as_ref() { + let inspect_event = NetworkInspectEvent::new_partial_response( + network_inspector_id, + Err(err.to_string()), + ); + if let Err(err) = network_inspector_sender.try_send(inspect_event) { + tracing::error!("Error sending inspect event: {}", err); + } + } + } - if let Some(response) = response { - return Ok(response.json::().await?); + Ok(FetchResponse { + _internal_req_id: req_id, + headers: HashMap::new(), + ok: false, + redirected: false, + status: 0, + status_text: err.to_string(), + _type: "error".into(), + url: url.clone(), + network_inspector_id: network_inspector_id.to_u32(), + }) + } } - - Err(anyhow::Error::msg("couldn't get response")) } #[op2(async)] @@ -194,7 +249,18 @@ async fn op_fetch_consume_json( async fn op_fetch_consume_text( op_state: Rc>, req_id: u32, + inspector_network_req_id: u32, ) -> Result { + let inspector_network_req_id = NetworkInspectorId::from_u32(inspector_network_req_id); + let maybe_network_inspector_sender = if inspector_network_req_id.is_valid() { + op_state + .borrow() + .try_borrow::() + .cloned() + } else { + None + }; + let response = { let mut state = op_state.borrow_mut(); let fetch_request = state.borrow_mut::(); @@ -203,9 +269,43 @@ async fn op_fetch_consume_text( }; if let Some(response) = response { - return Ok(response.text().await?); + match response.text().await { + Ok(response) => { + if let Some(network_inspector_sender) = maybe_network_inspector_sender.as_ref() { + let inspect_event = NetworkInspectEvent::new_body_response( + inspector_network_req_id, + Ok(Some(response.clone())), + ); + if let Err(err) = network_inspector_sender.try_send(inspect_event) { + tracing::error!("Error sending inspect event: {}", err); + } + } + + return Ok(response); + } + Err(err) => { + if let Some(network_inspector_sender) = maybe_network_inspector_sender.as_ref() { + let inspect_event = NetworkInspectEvent::new_body_response( + inspector_network_req_id, + Err(err.to_string()), + ); + if let Err(err) = network_inspector_sender.try_send(inspect_event) { + tracing::error!("Error sending inspect event: {}", err); + } + } + } + } } + if let Some(network_inspector_sender) = maybe_network_inspector_sender.as_ref() { + let inspect_event = NetworkInspectEvent::new_body_response( + inspector_network_req_id, + Err("couldn't get response".into()), + ); + if let Err(err) = network_inspector_sender.try_send(inspect_event) { + tracing::error!("Error sending inspect event: {}", err); + } + } Err(anyhow::Error::msg("couldn't get response")) } #[op2(async)] @@ -213,17 +313,61 @@ async fn op_fetch_consume_text( async fn op_fetch_consume_bytes( op_state: Rc>, req_id: u32, + inspector_network_req_id: u32, ) -> Result { + let inspector_network_req_id = NetworkInspectorId::from_u32(inspector_network_req_id); + let maybe_network_inspector_sender = if inspector_network_req_id.is_valid() { + op_state + .borrow() + .try_borrow::() + .cloned() + } else { + None + }; + let response = { let mut state = op_state.borrow_mut(); let fetch_request = state.borrow_mut::(); let current_request = fetch_request.requests.get_mut(&req_id).unwrap(); + current_request.response.take() }; if let Some(response) = response { - return Ok(response.bytes().await?); + match response.bytes().await { + Ok(response) => { + if let Some(network_inspector_sender) = maybe_network_inspector_sender.as_ref() { + let inspect_event = + NetworkInspectEvent::new_body_response(inspector_network_req_id, Ok(None)); + if let Err(err) = network_inspector_sender.try_send(inspect_event) { + tracing::error!("Error sending inspect event: {}", err); + } + } + + return Ok(response); + } + Err(err) => { + if let Some(network_inspector_sender) = maybe_network_inspector_sender.as_ref() { + let inspect_event = NetworkInspectEvent::new_body_response( + inspector_network_req_id, + Err(err.to_string()), + ); + if let Err(err) = network_inspector_sender.try_send(inspect_event) { + tracing::error!("Error sending inspect event: {}", err); + } + } + } + } } + if let Some(network_inspector_sender) = maybe_network_inspector_sender.as_ref() { + let inspect_event = NetworkInspectEvent::new_body_response( + inspector_network_req_id, + Err("couldn't get response".into()), + ); + if let Err(err) = network_inspector_sender.try_send(inspect_event) { + tracing::error!("Error sending inspect event: {}", err); + } + } Err(anyhow::Error::msg("couldn't get response")) } diff --git a/lib/src/dcl/js/js_modules/SignedFetch.js b/lib/src/dcl/js/js_modules/SignedFetch.js index 1ca25ec4e..668749633 100644 --- a/lib/src/dcl/js/js_modules/SignedFetch.js +++ b/lib/src/dcl/js/js_modules/SignedFetch.js @@ -1,6 +1,5 @@ -module.exports.signedFetch = async function (body) { +module.exports.signedFetch = async function (body) { const headers = await Deno.core.ops.op_signed_fetch_headers(body.url, body.init?.method); - if (!body.init) { body.init = { headers: {} }; } @@ -9,7 +8,7 @@ module.exports.signedFetch = async function (body) { body.init.headers = {}; } - for (var i=0; i< headers.length; i++) { + for (var i = 0; i < headers.length; i++) { body.init.headers[headers[i][0]] = headers[i][1]; } @@ -25,11 +24,11 @@ module.exports.signedFetch = async function (body) { }; } -module.exports.getHeaders = async function (body) { +module.exports.getHeaders = async function (body) { const result = await Deno.core.ops.op_signed_fetch_headers(body.url, body.init?.method) const headers = {} - for (var i=0; i< result.length; i++) { + for (var i = 0; i < result.length; i++) { headers[result[i][0]] = result[i][1]; } return { diff --git a/lib/src/dcl/js/js_modules/fetch.js b/lib/src/dcl/js/js_modules/fetch.js index 08813438c..1ffc8168c 100644 --- a/lib/src/dcl/js/js_modules/fetch.js +++ b/lib/src/dcl/js/js_modules/fetch.js @@ -156,6 +156,7 @@ async function fetch(url, init) { reqMethod, url, reqHeaders, hasBody, body ?? '', reqRedirect, reqTimeout ) const reqId = response._internal_req_id + const networkInpectorReqId = response.network_inspector_id ?? 0 response.headers = new Headers(response.headers) // TODO: the headers object should be read-only @@ -180,7 +181,8 @@ async function fetch(url, init) { notifyConsume() throwErrorFailed() const data = await Deno.core.ops.op_fetch_consume_bytes( - reqId + reqId, + networkInpectorReqId ) alreadyConsumed = true return data @@ -189,7 +191,8 @@ async function fetch(url, init) { notifyConsume() throwErrorFailed() const data = await Deno.core.ops.op_fetch_consume_text( - reqId + reqId, + networkInpectorReqId ) try { let jsonData = JSON.parse(data) @@ -203,7 +206,8 @@ async function fetch(url, init) { notifyConsume() throwErrorFailed() const data = await Deno.core.ops.op_fetch_consume_text( - reqId + reqId, + networkInpectorReqId ) return data }, @@ -211,7 +215,8 @@ async function fetch(url, init) { throwErrorFailed() notifyConsume() const data = await Deno.core.ops.op_fetch_consume_bytes( - reqId + reqId, + networkInpectorReqId ) return data } diff --git a/lib/src/dcl/js/mod.rs b/lib/src/dcl/js/mod.rs index 4a2f91462..d83bda941 100644 --- a/lib/src/dcl/js/mod.rs +++ b/lib/src/dcl/js/mod.rs @@ -151,6 +151,7 @@ pub(crate) fn scene_thread( let ethereum_provider = spawn_dcl_scene_data.ethereum_provider; let ephemeral_wallet = spawn_dcl_scene_data.ephemeral_wallet; let realm_info = spawn_dcl_scene_data.realm_info; + let maybe_network_inspector_sender = spawn_dcl_scene_data.network_inspector_sender; // on main.crdt detected if !local_main_crdt_file_path.is_empty() { @@ -216,6 +217,10 @@ pub(crate) fn scene_thread( state.borrow_mut().put(thread_receive_from_main); state.borrow_mut().put(ethereum_provider); + if let Some(network_inspector_sender) = maybe_network_inspector_sender { + state.borrow_mut().put(network_inspector_sender); + } + state.borrow_mut().put(scene_id); state.borrow_mut().put(scene_crdt); diff --git a/lib/src/dcl/mod.rs b/lib/src/dcl/mod.rs index ac40d558c..6d3f1d6b9 100644 --- a/lib/src/dcl/mod.rs +++ b/lib/src/dcl/mod.rs @@ -14,6 +14,7 @@ use crate::{ auth::{ephemeral_auth_chain::EphemeralAuthChain, ethereum_provider::EthereumProvider}, content::content_mapping::ContentMappingAndUrlRef, realm::scene_definition::SceneEntityDefinition, + tools::network_inspector::NetworkInspectorSender, }; use self::{ @@ -113,6 +114,8 @@ pub struct SpawnDclSceneData { pub realm_info: DclSceneRealmData, // Inspect pub inspect: bool, + // Inspect Network sender + pub network_inspector_sender: Option, } impl DclScene { diff --git a/lib/src/godot_classes/animator_controller.rs b/lib/src/godot_classes/animator_controller.rs index 35e47e6e2..23d77d6e2 100644 --- a/lib/src/godot_classes/animator_controller.rs +++ b/lib/src/godot_classes/animator_controller.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use godot::{ builtin::{meta::ToGodot, StringName}, @@ -35,6 +35,8 @@ pub struct MultipleAnimationController { current_time: HashMap, playing_anims: HashMap, + + finished_animations: HashSet, } #[godot_api] @@ -103,6 +105,7 @@ impl MultipleAnimationController { current_time: HashMap::new(), playing_anims: HashMap::new(), existing_anims_duration, + finished_animations: HashSet::new(), }) } @@ -258,7 +261,12 @@ impl MultipleAnimationController { ); let playing_time = if let Some(playing_time) = self.current_time.remove(&anim.clip) { - playing_time + if self.finished_animations.contains(&anim.clip) { + self.finished_animations.remove(&anim.clip); + 0.0 + } else { + playing_time + } } else if !anim_item.value.playing_backward() { 0.0 } else { diff --git a/lib/src/godot_classes/dcl_global.rs b/lib/src/godot_classes/dcl_global.rs index 12d9aeb2c..3ea6431d4 100644 --- a/lib/src/godot_classes/dcl_global.rs +++ b/lib/src/godot_classes/dcl_global.rs @@ -15,6 +15,7 @@ use crate::{ http_request::rust_http_queue_requester::RustHttpQueueRequester, scene_runner::{scene_manager::SceneManager, tokio_runtime::TokioRuntime}, test_runner::testing_tools::DclTestingTools, + tools::network_inspector::{NetworkInspector, NetworkInspectorSender}, }; use super::{ @@ -84,6 +85,9 @@ pub struct DclGlobal { pub renderer_version: GString, pub is_mobile: bool, + + #[var] + pub network_inspector: Gd, } #[godot_api] @@ -143,6 +147,7 @@ impl INode for DclGlobal { ethereum_provider: Arc::new(EthereumProvider::new()), metrics: Metrics::new_alloc(), renderer_version: env!("GODOT_EXPLORER_VERSION").into(), + network_inspector: NetworkInspector::new_alloc(), } } } @@ -191,4 +196,14 @@ impl DclGlobal { pub fn singleton() -> Gd { Self::try_singleton().expect("Failed to get global singleton!") } + + pub fn get_network_inspector_sender() -> Option { + Some( + Self::try_singleton()? + .bind() + .network_inspector + .bind() + .get_sender(), + ) + } } diff --git a/lib/src/http_request/http_queue_requester.rs b/lib/src/http_request/http_queue_requester.rs index 477e0ea2f..5385a09f0 100644 --- a/lib/src/http_request/http_queue_requester.rs +++ b/lib/src/http_request/http_queue_requester.rs @@ -5,6 +5,11 @@ use std::sync::{Arc, Mutex}; use tokio::io::AsyncWriteExt; use tokio::sync::{oneshot, Semaphore}; +use crate::tools::network_inspector::{ + NetworkInspectEvent, NetworkInspectRequestPayload, NetworkInspectResponsePayload, + NetworkInspectorId, NetworkInspectorSender, NETWORK_INSPECTOR_ENABLE, +}; + use super::request_response::{ RequestOption, RequestResponse, RequestResponseError, ResponseEnum, ResponseType, }; @@ -15,6 +20,9 @@ struct QueueRequest { priority: usize, request_option: Option, response_sender: oneshot::Sender>, + + network_inspector_id: NetworkInspectorId, + network_inspector_sender: Option, } impl PartialEq for QueueRequest { @@ -44,28 +52,60 @@ pub struct HttpQueueRequester { client: Arc, queue: Arc>>, semaphore: Arc, + inspector_sender: Option, } impl HttpQueueRequester { - pub fn new(max_parallel_requests: usize) -> Self { + pub fn new( + max_parallel_requests: usize, + inspector_sender: Option, + ) -> Self { Self { client: Arc::new(Client::new()), queue: Arc::new(Mutex::new(BinaryHeap::new())), semaphore: Arc::new(Semaphore::new(max_parallel_requests)), + inspector_sender, } } + pub async fn request( &self, request_option: RequestOption, priority: usize, ) -> Result { let (response_sender, response_receiver) = oneshot::channel(); + + let (network_inspector_id, network_inspector_sender) = if NETWORK_INSPECTOR_ENABLE + .load(std::sync::atomic::Ordering::Relaxed) + && self.inspector_sender.is_some() + { + let (req_id, event) = NetworkInspectEvent::new_request(NetworkInspectRequestPayload { + url: request_option.url.clone(), + method: request_option.method.clone(), + body: request_option.body.clone(), + headers: request_option.headers.clone(), + }); + + let inspector_sender = self + .inspector_sender + .clone() + .expect("already checked for some"); + let _ = inspector_sender.send(event).await; + (req_id, Some(inspector_sender)) + } else { + (NetworkInspectorId::INVALID, None) + }; + let http_request = QueueRequest { id: request_option.id, priority, request_option: Some(request_option), response_sender, + + network_inspector_id, + network_inspector_sender, }; + self.queue.lock().unwrap().push(http_request); self.process_queue().await; response_receiver.await.unwrap() @@ -85,7 +125,42 @@ impl HttpQueueRequester { if let Some(mut queue_request) = request { let request_option = queue_request.request_option.take().unwrap(); - let response_result = Self::process_request(client, request_option).await; + let response_result = Self::process_request( + client, + request_option, + queue_request.network_inspector_id, + queue_request.network_inspector_sender.clone(), + ) + .await; + + if queue_request.network_inspector_id.is_valid() { + if let Some(network_inspector_sender) = + queue_request.network_inspector_sender.as_ref() + { + let network_inspect_response: Result< + (NetworkInspectResponsePayload, Option), + String, + > = match &response_result { + Ok(response) => Ok(( + NetworkInspectResponsePayload { + status_code: response.status_code, + headers: None, + }, + None, + )), + Err(err) => Err(err.error_message.clone()), + }; + + let inspect_event = NetworkInspectEvent::new_full_response( + queue_request.network_inspector_id, + network_inspect_response, + ); + if let Err(err) = network_inspector_sender.try_send(inspect_event) { + tracing::error!("Error sending inspect event: {}", err); + } + } + } + let _ = queue_request.response_sender.send(response_result); } }); @@ -94,6 +169,9 @@ impl HttpQueueRequester { async fn process_request( client: Arc, mut request_option: RequestOption, + // TODO: for a granular inspection, we need to pass the sender here + _network_inspector_id: NetworkInspectorId, + _maybe_network_inspector_sender: Option, ) -> Result { let timeout = request_option .timeout @@ -107,11 +185,8 @@ impl HttpQueueRequester { } if let Some(headers) = request_option.headers.take() { - for header in headers { - let parts: Vec<&str> = header.splitn(2, ':').collect(); - if parts.len() == 2 { - request = request.header(parts[0], parts[1].trim()); - } + for (key, value) in headers { + request = request.header(key, value); } } diff --git a/lib/src/http_request/http_requester.rs b/lib/src/http_request/http_requester.rs deleted file mode 100644 index 6e6837d0c..000000000 --- a/lib/src/http_request/http_requester.rs +++ /dev/null @@ -1,240 +0,0 @@ -use std::fmt::Debug; - -use tokio::sync::mpsc::{Receiver, Sender}; - -use super::request_response::*; - -pub struct HttpRequester { - sender_to_thread: tokio::sync::mpsc::Sender, - receiver_from_thread: - tokio::sync::mpsc::Receiver>, -} - -impl Debug for HttpRequester { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("HttpRequester").finish() - } -} - -impl Default for HttpRequester { - fn default() -> Self { - Self::new(None) - } -} - -async fn request_pool( - sender_to_parent: Sender>, - mut receiver_from_parent: Receiver, -) { - while let Some(request_option) = receiver_from_parent.recv().await { - let sender = sender_to_parent.clone(); - // TODO: limit the concurrent requests - tokio::spawn(async move { - let client = reqwest::Client::new(); - let url = request_option.url.clone(); - let response = HttpRequester::do_request(&client, request_option).await; - if response.is_err() { - tracing::info!("Error in request: {url:?}"); - } else { - // tracing::info!("Ok in request: {:?}", url); - } - match sender.send(response).await { - Ok(_) => { - // tracing::info!("Ok sending reqsuest: {:?}", url); - } - Err(_) => { - panic!("Failed to send response"); - } - } - }); - } -} - -impl HttpRequester { - pub fn new(runtime: Option) -> Self { - let (sender_to_thread, receiver_from_parent) = - tokio::sync::mpsc::channel::(100); - let (sender_to_parent, receiver_from_thread) = - tokio::sync::mpsc::channel::>(100); - - if let Some(rt) = runtime { - rt.spawn(async move { - request_pool(sender_to_parent, receiver_from_parent).await; - }); - } else { - std::thread::spawn(move || { - let runtime = tokio::runtime::Runtime::new(); - if runtime.is_err() { - panic!("Failed to create runtime {:?}", runtime.err()); - } - let runtime = runtime.unwrap(); - - runtime.block_on(async move { - request_pool(sender_to_parent, receiver_from_parent).await; - }); - }); - } - - Self { - sender_to_thread, - receiver_from_thread, - } - } - - pub fn send_request(&mut self, req: RequestOption) -> bool { - self.sender_to_thread.try_send(req).is_ok() - } - - pub fn poll(&mut self) -> Option> { - self.receiver_from_thread.try_recv().ok() - } - - pub async fn do_request( - client: &reqwest::Client, - mut request_option: RequestOption, - ) -> Result { - let mut request = client - .request(request_option.method.clone(), request_option.url.clone()) - .timeout(std::time::Duration::from_secs(10)); - - if let Some(body) = request_option.body.take() { - request = request.body(body); - } - - if let Some(headers) = request_option.headers.take() { - for header in headers { - let parts: Vec<&str> = header.splitn(2, ':').collect(); - if parts.len() == 2 { - request = request.header(parts[0], parts[1].trim()); - } - } - } - - let map_err_func = |e: reqwest::Error| RequestResponseError { - id: request_option.id, - error_message: e.to_string(), - }; - - let response = request.send().await.map_err(map_err_func)?; - let status_code = response.status(); - - let response_data = match request_option.response_type.clone() { - ResponseType::AsString => { - ResponseEnum::String(response.text().await.map_err(map_err_func)?) - } - ResponseType::AsBytes => { - ResponseEnum::Bytes(response.bytes().await.map_err(map_err_func)?.to_vec()) - } - ResponseType::ToFile(file_path) => { - let content = response.bytes().await.map_err(map_err_func)?.to_vec(); - let mut file = - std::fs::File::create(file_path.clone()).map_err(|e| RequestResponseError { - id: request_option.id, - error_message: e.to_string(), - })?; - let result = std::io::Write::write_all(&mut file, &content); - let result = result.map(|_| file_path); - ResponseEnum::ToFile(result) - } - ResponseType::AsJson => { - let json_string = &response.text().await.map_err(map_err_func)?; - ResponseEnum::Json(serde_json::from_str(json_string)) - } - }; - - Ok(RequestResponse { - request_option, - status_code, - response_data: Ok(response_data), - }) - } -} - -#[test] -fn test() { - // TODO: add tests - - let mut requester = HttpRequester::new(None); - - // requester.send_request(RequestOption::new( - // 0, - // "https://sdk-test-scenes.decentraland.zone/aboudt".to_string(), - // http::Method::GET, - // ResponseType::AsString, - // None, - // None, - // )); - - // requester.send_request(RequestOption::new( - // 0, - // "https://sdk-test-scenes.decentraland.zone/about".to_string(), - // http::Method::GET, - // ResponseType::AsString, - // None, - // None, - // )); - - // requester.send_request(RequestOption::new( - // 0, - // "https://sdk-test-scenes.decentraland.zone/aboudt".to_string(), - // http::Method::GET, - // ResponseType::AsBytes, - // None, - // None, - // )); - - // requester.send_request(RequestOption::new( - // 0, - // "https://sdk-test-scenes.decentraland.zone/about".to_string(), - // http::Method::GET, - // ResponseType::AsBytes, - // None, - // None, - // )); - - // requester.send_request(RequestOption::new( - // 0, - // "https://sdk-test-scenes.decentraland.zone/aboudt".to_string(), - // http::Method::GET, - // ResponseType::ToFile("test.txt".to_string()), - // None, - // None, - // )); - - // requester.send_request(RequestOption::new( - // 0, - // "https://sdk-test-scenes.decentraland.zone/about".to_string(), - // http::Method::GET, - // ResponseType::ToFile("test.txt".to_string()), - // None, - // None, - // )); - - requester.send_request(RequestOption::new( - 0, - "https://sdk-test-scenes.decentraland.zone/content/entities/active".to_string(), - http::Method::POST, - ResponseType::AsString, - Some("{\"pointers\":[\"0,0\"]}".as_bytes().to_vec()), - Some(vec!["Content-Type: application/json".to_string()]), - None, - )); - - let mut counter = 0; - - loop { - match requester.poll() { - Some(response) => { - tracing::info!("{:?}", response); - counter += 1; - } - None => { - // Sleep for a while before polling again. - std::thread::sleep(std::time::Duration::from_millis(100)); - } - } - if counter >= 1 { - break; - } - } -} diff --git a/lib/src/http_request/mod.rs b/lib/src/http_request/mod.rs index 253db0e6f..aa02188cd 100644 --- a/lib/src/http_request/mod.rs +++ b/lib/src/http_request/mod.rs @@ -1,5 +1,3 @@ pub mod http_queue_requester; -pub mod http_requester; pub mod request_response; pub mod rust_http_queue_requester; -pub mod rust_requester; diff --git a/lib/src/http_request/request_response.rs b/lib/src/http_request/request_response.rs index 5d4131f30..80437fc22 100644 --- a/lib/src/http_request/request_response.rs +++ b/lib/src/http_request/request_response.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use godot::{ obj::Gd, prelude::{GString, Variant}, @@ -32,7 +34,7 @@ pub struct RequestOption { pub url: String, pub method: http::Method, pub body: Option>, - pub headers: Option>, + pub headers: Option>, pub response_type: ResponseType, pub timeout: Option, } @@ -44,7 +46,7 @@ impl RequestOption { method: http::Method, response_type: ResponseType, body: Option>, - headers: Option>, + headers: Option>, timeout: Option, ) -> Self { Self { diff --git a/lib/src/http_request/rust_http_queue_requester.rs b/lib/src/http_request/rust_http_queue_requester.rs index 14c87e558..c145a10b9 100644 --- a/lib/src/http_request/rust_http_queue_requester.rs +++ b/lib/src/http_request/rust_http_queue_requester.rs @@ -1,8 +1,11 @@ -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; use godot::prelude::*; -use crate::{godot_classes::promise::Promise, scene_runner::tokio_runtime::TokioRuntime}; +use crate::{ + godot_classes::{dcl_global::DclGlobal, promise::Promise}, + scene_runner::tokio_runtime::TokioRuntime, +}; use super::request_response::send_result_to_promise; @@ -19,6 +22,7 @@ impl IRefCounted for RustHttpQueueRequester { Self { http_queue_requester: Arc::new(super::http_queue_requester::HttpQueueRequester::new( 10, + DclGlobal::get_network_inspector_sender(), )), } } @@ -29,6 +33,7 @@ impl Default for RustHttpQueueRequester { Self { http_queue_requester: Arc::new(super::http_queue_requester::HttpQueueRequester::new( 10, + DclGlobal::get_network_inspector_sender(), )), } } @@ -65,7 +70,7 @@ impl RustHttpQueueRequester { url: GString, method: godot::engine::http_client::Method, body: GString, - headers: VariantArray, + headers: Dictionary, ) -> Gd { let body = match body.to_string().as_str() { "" => None, @@ -80,7 +85,7 @@ impl RustHttpQueueRequester { url: GString, method: godot::engine::http_client::Method, body: PackedByteArray, - headers: VariantArray, + headers: Dictionary, ) -> Gd { self._request_json(url, method, Some(body.to_vec()), headers) } @@ -92,7 +97,7 @@ impl RustHttpQueueRequester { url: GString, method: godot::engine::http_client::Method, body: Option>, - headers: VariantArray, + headers: Dictionary, ) -> Gd { // tracing::info!("Requesting json: {:?}", url.to_string()); @@ -101,16 +106,19 @@ impl RustHttpQueueRequester { _ => http::Method::GET, }; - let headers = match headers.len() { - 0 => None, - _ => { - let mut headers_vec = Vec::new(); - for i in 0..headers.len() { - let header = headers.get(i).as_ref().unwrap().to_string(); - headers_vec.push(header); - } - Some(headers_vec) + let headers = if headers.is_empty() { + None + } else { + let mut headers_map = HashMap::new(); + let keys = headers.keys_array(); + let values = headers.values_array(); + for i in 0..headers.len() { + headers_map.insert( + keys.get(i).as_ref().unwrap().to_string(), + values.get(i).as_ref().unwrap().to_string(), + ); } + Some(headers_map) }; let request_option = crate::http_request::request_response::RequestOption::new( diff --git a/lib/src/http_request/rust_requester.rs b/lib/src/http_request/rust_requester.rs deleted file mode 100644 index cf1463ed5..000000000 --- a/lib/src/http_request/rust_requester.rs +++ /dev/null @@ -1,148 +0,0 @@ -use godot::prelude::*; - -use crate::{godot_classes::dcl_global::DclGlobal, scene_runner::tokio_runtime::TokioRuntime}; - -// Deriving GodotClass makes the class available to Godot -#[derive(GodotClass)] -#[class(base=Node)] -pub struct RustHttpRequester { - http_requester: super::http_requester::HttpRequester, -} - -#[godot_api] -impl RustHttpRequester { - #[func] - fn poll(&mut self) -> Variant { - match self.http_requester.poll() { - Some(response) => { - match response { - Ok(response) => { - // tracing::info!( - // "response {:?} ok? {:?}", - // response.request_option.url.clone(), - // !response.is_error() - // ); - Variant::from(Gd::from_object(response)) - } - Err(error) => { - tracing::info!( - "error polling http_requester id={} msg={}", - error.id, - error.error_message - ); - - Variant::from(Gd::from_object(error)) - } - } - } - _ => Variant::nil(), - } - } - - #[func] - fn request_file(&mut self, reference_id: u32, url: GString, absolute_path: GString) -> u32 { - // tracing::info!( - // "Requesting file: {:?} in {absolute_path} ", - // url.to_string() - // ); - - let request_option = crate::http_request::request_response::RequestOption::new( - reference_id, - url.to_string(), - http::Method::GET, - crate::http_request::request_response::ResponseType::ToFile(absolute_path.to_string()), - None, - None, - None, - ); - let id = request_option.id; - self.http_requester.send_request(request_option); - id - } - - #[func] - fn request_json( - &mut self, - reference_id: u32, - url: GString, - method: godot::engine::http_client::Method, - body: GString, - headers: VariantArray, - ) -> u32 { - let body = match body.to_string().as_str() { - "" => None, - _ => Some(body.to_string().into_bytes()), - }; - self._request_json(reference_id, url, method, body, headers) - } - - #[func] - fn request_json_bin( - &mut self, - reference_id: u32, - url: GString, - method: godot::engine::http_client::Method, - body: PackedByteArray, - headers: VariantArray, - ) -> u32 { - self._request_json(reference_id, url, method, Some(body.to_vec()), headers) - } -} - -#[godot_api] -impl INode for RustHttpRequester { - fn init(_base: Base) -> Self { - let runtime = if DclGlobal::has_singleton() { - TokioRuntime::static_clone_handle() - } else { - None - }; - - RustHttpRequester { - http_requester: super::http_requester::HttpRequester::new(runtime), - } - } -} - -impl RustHttpRequester { - fn _request_json( - &mut self, - reference_id: u32, - url: GString, - method: godot::engine::http_client::Method, - body: Option>, - headers: VariantArray, - ) -> u32 { - tracing::info!("Requesting json: {:?}", url.to_string()); - - let method = match method { - godot::engine::http_client::Method::POST => http::Method::POST, - _ => http::Method::GET, - }; - - let headers = match headers.len() { - 0 => None, - _ => { - let mut headers_vec = Vec::new(); - for i in 0..headers.len() { - let header = headers.get(i).as_ref().unwrap().to_string(); - headers_vec.push(header); - } - Some(headers_vec) - } - }; - - let request_option = crate::http_request::request_response::RequestOption::new( - reference_id, - url.to_string(), - method, - crate::http_request::request_response::ResponseType::AsString, - body, - headers, - None, - ); - let id = request_option.id; - self.http_requester.send_request(request_option); - id - } -} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 8a65af46d..0106abfe2 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -19,6 +19,7 @@ pub mod http_request; pub mod realm; pub mod scene_runner; pub mod test_runner; +pub mod tools; pub mod utils; struct DecentralandGodotLibrary; diff --git a/lib/src/realm/parcel.rs b/lib/src/realm/parcel.rs index d34c478bf..55c6c4c83 100644 --- a/lib/src/realm/parcel.rs +++ b/lib/src/realm/parcel.rs @@ -56,7 +56,7 @@ impl Default for ParcelRadiusCalculator { impl ParcelRadiusCalculator { pub fn new(parcel_radius: i16) -> Self { // Clamp - let parcel_radius = parcel_radius.clamp(0, 5); + let parcel_radius = parcel_radius.clamp(0, 10); let parcel_radius_squared = (parcel_radius * parcel_radius) as f32; let mut outter_parcels = HashSet::new(); diff --git a/lib/src/realm/scene_entity_coordinator.rs b/lib/src/realm/scene_entity_coordinator.rs index 0df4fcdb7..740042eb2 100644 --- a/lib/src/realm/scene_entity_coordinator.rs +++ b/lib/src/realm/scene_entity_coordinator.rs @@ -5,9 +5,15 @@ use std::{ use godot::{prelude::*, test::itest}; -use crate::http_request::{ - http_requester::HttpRequester, - request_response::{RequestOption, RequestResponse, ResponseEnum, ResponseType}, +use crate::{ + godot_classes::dcl_global::DclGlobal, + http_request::{ + http_queue_requester::HttpQueueRequester, + request_response::{ + RequestOption, RequestResponse, RequestResponseError, ResponseEnum, ResponseType, + }, + }, + scene_runner::tokio_runtime::TokioRuntime, }; use super::{ @@ -16,7 +22,7 @@ use super::{ scene_definition::{EntityBase, SceneEntityDefinition}, }; -#[derive(Debug, Default, GodotClass)] +#[derive(Debug, GodotClass)] #[class(base=Node)] struct SceneEntityCoordinator { parcel_radius_calculator: ParcelRadiusCalculator, @@ -30,7 +36,6 @@ struct SceneEntityCoordinator { requested_entity: HashMap, cache_scene_data: HashMap>, // entity_id to SceneData - http_requester: HttpRequester, entities_active_url: String, content_url: String, @@ -39,6 +44,12 @@ struct SceneEntityCoordinator { loadable_scenes: HashSet, keep_alive_scenes: HashSet, empty_parcels: HashSet, + + receiver: tokio::sync::mpsc::Receiver>, + sender: tokio::sync::mpsc::Sender>, + + http_requester: Option>, + runtime: Option, } impl SceneEntityCoordinator { @@ -49,10 +60,35 @@ impl SceneEntityCoordinator { entities_active_url: String, content_url: String, should_load_city_scenes: bool, + runtime: Option, + http_requester: Option>, ) -> Self { + let (sender, receiver) = tokio::sync::mpsc::channel(100); let mut _self = SceneEntityCoordinator { parcel_radius_calculator: ParcelRadiusCalculator::new(3), - ..Default::default() + + current_position: Coord(-1000, -1000), + should_load_city_scenes, + requested_city_pointers: Default::default(), + cache_city_pointers: Default::default(), + + global_desired_entities: Default::default(), + requested_entity: Default::default(), + cache_scene_data: Default::default(), + entities_active_url: Default::default(), + content_url: Default::default(), + + version: Default::default(), + dirty_loadable_scenes: Default::default(), + loadable_scenes: Default::default(), + keep_alive_scenes: Default::default(), + empty_parcels: Default::default(), + + receiver, + sender, + + http_requester, + runtime, }; _self._config(entities_active_url, content_url, should_load_city_scenes); @@ -77,6 +113,37 @@ impl SceneEntityCoordinator { self.dirty_loadable_scenes = true; } + fn do_request(&mut self, request_option: RequestOption) { + if self.http_requester.is_none() { + self.http_requester = Some(Arc::new(HttpQueueRequester::new(10, None))); + } + + let sender = self.sender.clone(); + let http_requester = self.http_requester.clone().unwrap(); + + if let Some(rt) = self.runtime.as_ref() { + rt.spawn(async move { + let result = http_requester.request(request_option, 0).await; + if let Err(error) = sender.send(result).await { + tracing::error!("Error sending the result: {:?}", error); + } + }); + } else { + std::thread::spawn(move || { + let runtime = tokio::runtime::Runtime::new(); + if runtime.is_err() { + panic!("Failed to create runtime {:?}", runtime.err()); + } + let runtime = runtime.unwrap(); + + runtime.block_on(async move { + let result = http_requester.request(request_option, 0).await; + let _ = sender.try_send(result); + }); + }); + } + } + fn request_pointers(&mut self, set_request_pointers: HashSet) { // Request the new pointers if !set_request_pointers.is_empty() { @@ -87,6 +154,7 @@ impl SceneEntityCoordinator { .join(","); let request_body: String = format!("{{\"pointers\":[{request_pointers_body}]}}"); + let headers = HashMap::from([("Content-Type".into(), "application/json".into())]); let request = RequestOption::new( Self::REQUEST_TYPE_SCENE_POINTERS, @@ -94,12 +162,12 @@ impl SceneEntityCoordinator { http::Method::POST, ResponseType::AsJson, Some(request_body.as_bytes().to_vec()), - Some(vec!["Content-Type: application/json".to_string()]), + Some(headers), None, ); self.requested_city_pointers .insert(request.id, set_request_pointers); - self.http_requester.send_request(request); + self.do_request(request); } } @@ -313,7 +381,7 @@ impl SceneEntityCoordinator { ); self.requested_entity.insert(request.id, entity_base); - self.http_requester.send_request(request); + self.do_request(request); } } @@ -347,7 +415,7 @@ impl SceneEntityCoordinator { self.global_desired_entities.push(entity_base.clone()); self.requested_entity.insert(request.id, entity_base); - self.http_requester.send_request(request); + self.do_request(request); } } @@ -380,7 +448,7 @@ impl SceneEntityCoordinator { } pub fn _update(&mut self) { - while let Some(response) = self.http_requester.poll() { + while let Ok(response) = self.receiver.try_recv() { match response { Ok(response) => { if response.status_code.as_u16() >= 200 && response.status_code.as_u16() < 300 { @@ -546,12 +614,24 @@ impl SceneEntityCoordinator { self.cache_scene_data.remove(&scene_id); self.update_position(self.current_position.0, self.current_position.1); } + + #[func] + pub fn is_busy(&self) -> bool { + !(self.requested_city_pointers.is_empty() && self.requested_entity.is_empty()) + } } #[godot_api] impl INode for SceneEntityCoordinator { fn init(_base: Base) -> Self { - SceneEntityCoordinator::new("".into(), "".into(), false) + let runtime = TokioRuntime::static_clone_handle(); + if let Some(global) = DclGlobal::try_singleton() { + let http_requester_gd = global.bind().get_http_requester(); + let http_requester = http_requester_gd.bind().get_http_queue_requester(); + SceneEntityCoordinator::new("".into(), "".into(), false, runtime, Some(http_requester)) + } else { + SceneEntityCoordinator::new("".into(), "".into(), false, None, None) + } } } @@ -590,8 +670,13 @@ mod tests { "https://sdk-team-cdn.decentraland.org/ipfs/goerli-plaza-main-latest/contents" .to_string(); - let mut scene_entity_coordinator = - SceneEntityCoordinator::new(entities_active_url.clone(), content_url.clone(), false); + let mut scene_entity_coordinator = SceneEntityCoordinator::new( + entities_active_url.clone(), + content_url.clone(), + false, + None, + None, + ); // Test scenes scene_entity_coordinator.set_scene_radius(0); @@ -612,7 +697,7 @@ mod tests { // Test parcels let mut scene_entity_coordinator = - SceneEntityCoordinator::new(entities_active_url, content_url, true); + SceneEntityCoordinator::new(entities_active_url, content_url, true, None, None); scene_entity_coordinator.update_position(0, 0); assert!(wait_update_or_timeout(&mut scene_entity_coordinator, 10000)); diff --git a/lib/src/scene_runner/scene.rs b/lib/src/scene_runner/scene.rs index 137b646f3..4e4987dbd 100644 --- a/lib/src/scene_runner/scene.rs +++ b/lib/src/scene_runner/scene.rs @@ -207,6 +207,8 @@ pub struct Scene { pub tweens: HashMap, // Duplicated value to async-access the animator pub dup_animator: HashMap, + + pub paused: bool, } #[derive(Debug)] @@ -297,6 +299,7 @@ impl Scene { scene_test_plan_received: false, tweens: HashMap::new(), dup_animator: HashMap::new(), + paused: false, } } @@ -357,6 +360,7 @@ impl Scene { scene_test_plan_received: false, tweens: HashMap::new(), dup_animator: HashMap::new(), + paused: false, } } } diff --git a/lib/src/scene_runner/scene_manager.rs b/lib/src/scene_runner/scene_manager.rs index 0e20e0a3c..d32812b52 100644 --- a/lib/src/scene_runner/scene_manager.rs +++ b/lib/src/scene_runner/scene_manager.rs @@ -21,6 +21,7 @@ use crate::{ JsonGodotClass, }, realm::dcl_scene_entity_definition::DclSceneEntityDefinition, + tools::network_inspector::NETWORK_INSPECTOR_ENABLE, }; use godot::{ engine::{ @@ -123,31 +124,41 @@ impl SceneManager { SceneType::Parcel }; + let dcl_global = DclGlobal::singleton(); + let new_scene_id = Scene::new_id(); let signal_data = (new_scene_id, scene_entity_definition.id.clone()); - let testing_mode_active = DclGlobal::singleton().bind().testing_scene_mode; - let ethereum_provider = DclGlobal::singleton().bind().ethereum_provider.clone(); + let testing_mode_active = dcl_global.bind().testing_scene_mode; + let ethereum_provider = dcl_global.bind().ethereum_provider.clone(); let ephemeral_wallet = DclGlobal::singleton() .bind() .player_identity .bind() .try_get_ephemeral_auth_chain(); - let realm = DclGlobal::singleton().bind().realm.clone(); + let realm = dcl_global.bind().realm.clone(); let realm = realm.bind(); let realm_name = realm.get_realm_name().to_string(); let base_url = realm.get_realm_url().to_string(); let network_id = realm.get_network_id(); - let is_preview = DclGlobal::singleton().bind().get_preview_mode(); + let is_preview = dcl_global.bind().get_preview_mode(); - let comms_adapter = DclGlobal::singleton() + let comms_adapter = dcl_global .bind() .comms .bind() .get_current_adapter_conn_str() .to_string(); + let network_inspector = dcl_global.bind().get_network_inspector(); + let network_inspector_sender = + if NETWORK_INSPECTOR_ENABLE.load(std::sync::atomic::Ordering::Relaxed) { + Some(network_inspector.bind().get_sender()) + } else { + None + }; + let dcl_scene = DclScene::spawn_new_js_dcl_scene(SpawnDclSceneData { scene_id: new_scene_id, scene_entity_definition: scene_entity_definition.clone(), @@ -166,6 +177,7 @@ impl SceneManager { is_preview, }, inspect, + network_inspector_sender, }); let new_scene = Scene::new( @@ -272,6 +284,22 @@ impl SceneManager { GString::default() } + #[func] + fn get_scene_is_paused(&self, scene_id: i32) -> bool { + if let Some(scene) = self.scenes.get(&SceneId(scene_id)) { + scene.paused + } else { + false + } + } + + #[func] + fn set_scene_is_paused(&mut self, scene_id: i32, value: bool) { + if let Some(scene) = self.scenes.get_mut(&SceneId(scene_id)) { + scene.paused = value; + } + } + #[func] pub fn get_scene_id_by_parcel_position(&self, parcel_position: Vector2i) -> i32 { for scene in self.scenes.values() { @@ -366,7 +394,7 @@ impl SceneManager { // TODO: review to define a better behavior self.sorted_scene_ids.sort_by_key(|&scene_id| { let scene = self.scenes.get_mut(&scene_id).unwrap(); - if !scene.current_dirty.waiting_process { + if !scene.current_dirty.waiting_process || scene.paused { scene.next_tick_us = start_time_us + 120000; // Set at the end of the queue: scenes without processing from scene-runtime, wait until something comes } else if scene_id == self.current_parcel_scene_id { @@ -446,7 +474,6 @@ impl SceneManager { let scene = self.scenes.get_mut(scene_id).unwrap(); match scene.state { SceneState::ToKill => { - scene.state = SceneState::KillSignal(current_time_us); if let Err(_e) = scene .dcl_scene .main_sender_to_thread @@ -464,6 +491,7 @@ impl SceneManager { let elapsed_from_kill_us = current_time_us - kill_time_us; if elapsed_from_kill_us > 10 * 1e6 as i64 { // 10 seconds from the kill signal + tracing::error!("timeout killing scene"); } } } diff --git a/lib/src/tools/mod.rs b/lib/src/tools/mod.rs new file mode 100644 index 000000000..8a93f73cc --- /dev/null +++ b/lib/src/tools/mod.rs @@ -0,0 +1 @@ +pub mod network_inspector; diff --git a/lib/src/tools/network_inspector.rs b/lib/src/tools/network_inspector.rs new file mode 100644 index 000000000..3f1984c84 --- /dev/null +++ b/lib/src/tools/network_inspector.rs @@ -0,0 +1,251 @@ +use std::{ + collections::{HashMap, HashSet}, + sync::atomic::AtomicBool, +}; + +use godot::prelude::*; + +pub type NetworkInspectorSender = tokio::sync::mpsc::Sender; + +#[derive(Hash, Eq, PartialEq, Copy, Clone, Debug)] +pub struct NetworkInspectorId(u32); + +impl Default for NetworkInspectorId { + fn default() -> Self { + Self::new() + } +} + +impl NetworkInspectorId { + pub const INVALID: NetworkInspectorId = NetworkInspectorId(0); + + pub fn new() -> Self { + Self( + NETWORK_INSPECTED_REQUEST_ID_MONOTONIC_COUNTER + .fetch_add(1, std::sync::atomic::Ordering::Relaxed), + ) + } + + pub fn is_valid(&self) -> bool { + self.0 != 0 + } + + pub fn from_u32(id: u32) -> Self { + Self(id) + } + + pub fn to_u32(&self) -> u32 { + self.0 + } +} + +struct NetworkInspectedRequest { + requested_at: f64, + request: NetworkInspectRequestPayload, + + response_received_at: Option, + response: Result, + + response_payload_received_at: Option, + response_payload: Result, String>, +} + +static NETWORK_INSPECTED_REQUEST_ID_MONOTONIC_COUNTER: once_cell::sync::Lazy< + std::sync::atomic::AtomicU32, +> = once_cell::sync::Lazy::new(|| std::sync::atomic::AtomicU32::new(0)); + +pub struct NetworkInspectEvent { + id: NetworkInspectorId, + payload: NetworkInspectPayload, +} + +pub struct NetworkInspectRequestPayload { + pub url: String, + pub method: http::Method, + pub body: Option>, + pub headers: Option>, +} + +pub struct NetworkInspectResponsePayload { + pub status_code: http::StatusCode, + pub headers: Option>, +} + +pub enum NetworkInspectPayload { + Request(NetworkInspectRequestPayload), + PartialResponse(Result), + BodyResponse(Result, String>), + FullResponse(Result<(NetworkInspectResponsePayload, Option), String>), +} + +pub static NETWORK_INSPECTOR_ENABLE: AtomicBool = AtomicBool::new(false); + +impl NetworkInspectEvent { + pub fn new_request(request: NetworkInspectRequestPayload) -> (NetworkInspectorId, Self) { + let id = NetworkInspectorId::new(); + ( + id, + NetworkInspectEvent { + id, + payload: NetworkInspectPayload::Request(request), + }, + ) + } + + pub fn new_partial_response( + id: NetworkInspectorId, + response: Result, + ) -> Self { + NetworkInspectEvent { + id, + payload: NetworkInspectPayload::PartialResponse(response), + } + } + + pub fn new_body_response( + id: NetworkInspectorId, + response: Result, String>, + ) -> Self { + NetworkInspectEvent { + id, + payload: NetworkInspectPayload::BodyResponse(response), + } + } + + pub fn new_full_response( + id: NetworkInspectorId, + response: Result<(NetworkInspectResponsePayload, Option), String>, + ) -> Self { + NetworkInspectEvent { + id, + payload: NetworkInspectPayload::FullResponse(response), + } + } +} + +#[derive(GodotClass)] +#[class(base=Node)] +pub struct NetworkInspector { + requests: HashMap, + receiver: tokio::sync::mpsc::Receiver, + sender: tokio::sync::mpsc::Sender, + _base: Base, +} + +#[godot_api] +impl NetworkInspector { + #[func] + pub fn set_is_active(&mut self, value: bool) { + NETWORK_INSPECTOR_ENABLE.store(value, std::sync::atomic::Ordering::Relaxed); + } + + #[signal] + pub fn request_changed(&self, id: u32) {} + + #[func] + pub fn get_request(&self, id: u32) -> Dictionary { + let mut dict = Dictionary::new(); + if let Some(request) = self.requests.get(&NetworkInspectorId(id)) { + let _ = dict.insert("requested_at", request.requested_at); + let _ = dict.insert( + "response_received_at", + request.response_received_at.unwrap_or(0.0), + ); + let _ = dict.insert( + "response_payload_received_at", + request.response_payload_received_at.unwrap_or(0.0), + ); + let _ = dict.insert("url", request.request.url.as_str()); + let _ = dict.insert("method", request.request.method.as_str()); + + let headers = { + let mut dict = Dictionary::new(); + if let Some(headers) = &request.request.headers { + for (key, value) in headers.iter() { + let _ = dict.insert(key.as_str().to_string(), value.as_str().to_string()); + } + } + dict + }; + let _ = dict.insert("headers", headers); + } + dict + } +} + +#[godot_api] +impl INode for NetworkInspector { + fn init(_base: Base) -> Self { + let (sender, receiver) = tokio::sync::mpsc::channel(10); + NetworkInspector { + requests: HashMap::new(), + receiver, + sender, + _base, + } + } + + fn process(&mut self, _dt: f64) { + let mut request_changed = HashSet::new(); + while let Ok(event) = self.receiver.try_recv() { + request_changed.insert(event.id.0); + match event.payload { + NetworkInspectPayload::Request(request) => { + self.requests.insert( + event.id, + NetworkInspectedRequest { + requested_at: 0.0, + request, + response_received_at: None, + response: Err("No response received".to_string()), + response_payload_received_at: None, + response_payload: Err("No response payload received".to_string()), + }, + ); + } + NetworkInspectPayload::PartialResponse(response) => { + if let Some(request) = self.requests.get_mut(&event.id) { + request.response_received_at = Some(0.0); + request.response = response; + } + } + NetworkInspectPayload::BodyResponse(response) => { + if let Some(request) = self.requests.get_mut(&event.id) { + request.response_payload_received_at = Some(0.0); + request.response_payload = response; + } + } + NetworkInspectPayload::FullResponse(response) => { + if let Some(request) = self.requests.get_mut(&event.id) { + request.response_received_at = Some(0.0); + request.response_payload_received_at = Some(0.0); + + match response { + Ok((response, body)) => { + request.response = Ok(response); + request.response_payload = Ok(body); + } + Err(err) => { + request.response = Err(err.clone()); + request.response_payload = Err(err); + } + } + } + } + } + } + + for id in request_changed { + self.base_mut().call_deferred( + "emit_signal".into(), + &["request_changed".to_variant(), id.to_variant()], + ); + } + } +} + +impl NetworkInspector { + pub fn get_sender(&self) -> tokio::sync::mpsc::Sender { + self.sender.clone() + } +} diff --git a/src/consts.rs b/src/consts.rs index 6ea576b36..986a164d0 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -9,7 +9,25 @@ pub const PROTOC_BASE_URL: &str = pub const GODOT4_BIN_BASE_URL: &str = "https://github.com/godotengine/godot/releases/download/4.3-stable/Godot_v4.3-stable_"; +pub const GODOT_CURRENT_VERSION: &str = "4.3"; + pub const GODOT4_EXPORT_TEMPLATES_BASE_URL: &str = - "https://github.com/godotengine/godot/releases/download/4.3-stable/Godot_v4.3-stable_export_templates.tpz"; + "https://github.com/decentraland/godotengine/releases/download/4.3-stable/"; -pub const GODOT_CURRENT_VERSION: &str = "4.3"; +pub const GODOT_PLATFORM_FILES: &[(&str, &[&str])] = &[ + ("ios", &["ios.zip"]), + ( + "android", + &[ + "android_debug.apk", + "android_release.apk", + "android_source.zip", + ], + ), + ("linux", &["linux_debug.x86_64", "linux_release.x86_64"]), + ("macos", &["macos.zip"]), + ( + "windows", + &["windows_debug_x86_64.exe", "windows_release_x86_64.exe"], + ), +]; diff --git a/src/copy_files.rs b/src/copy_files.rs index a126b4831..54feb6073 100644 --- a/src/copy_files.rs +++ b/src/copy_files.rs @@ -133,4 +133,4 @@ pub fn move_dir_recursive(src: &Path, dest: &Path) -> io::Result<()> { fs::remove_dir_all(src)?; Ok(()) -} \ No newline at end of file +} diff --git a/src/export.rs b/src/export.rs index cca5d5286..34a8a3d54 100644 --- a/src/export.rs +++ b/src/export.rs @@ -1,9 +1,8 @@ -use std::{fs, io, path::Path, process::ExitStatus}; +use std::{collections::HashMap, fs, io, path::Path, process::ExitStatus}; use crate::{ consts::{ - BIN_FOLDER, EXPORTS_FOLDER, GODOT4_EXPORT_TEMPLATES_BASE_URL, GODOT_CURRENT_VERSION, - GODOT_PROJECT_FOLDER, + BIN_FOLDER, EXPORTS_FOLDER, GODOT4_EXPORT_TEMPLATES_BASE_URL, GODOT_CURRENT_VERSION, GODOT_PLATFORM_FILES, GODOT_PROJECT_FOLDER }, copy_files::copy_ffmpeg_libraries, install_dependency::{download_and_extract_zip, set_executable_permission}, @@ -25,8 +24,7 @@ fn copy_dir_all(src: impl AsRef, dst: impl AsRef) -> io::Result<()> Ok(()) } -pub fn import_assets() -> ExitStatus -{ +pub fn import_assets() -> ExitStatus { let program = get_godot_path(); // Do imports and one project open @@ -132,13 +130,41 @@ pub fn export() -> Result<(), anyhow::Error> { Ok(()) } -pub fn prepare_templates() -> Result<(), anyhow::Error> { +pub fn prepare_templates(platforms: &[String]) -> Result<(), anyhow::Error> { + // Convert GODOT_PLATFORM_FILES into a HashMap + let file_map: HashMap<&str, Vec<&str>> = GODOT_PLATFORM_FILES + .iter() + .map(|(platform, files)| (*platform, files.to_vec())) + .collect(); + + // If no specific templates are provided, default to all templates + let templates = if platforms.is_empty() { + println!("No specific templates provided, downloading all templates."); + println!("For downloading for a specific platform use: `cargo run -- install --platform linux`"); + file_map.keys().map(|&k| k.to_string()).collect::>() + } else { + platforms.to_vec() + }; + + // Process each template and download the associated files let dest_path = format!("{BIN_FOLDER}godot/templates"); - download_and_extract_zip( - GODOT4_EXPORT_TEMPLATES_BASE_URL, - dest_path.as_str(), - Some(format!("{GODOT_CURRENT_VERSION}.export-templates.zip")), - )?; + + for template in templates { + if let Some(files) = file_map.get(template.as_str()) { + for file in files { + println!("Downloading file for {}: {}", template, file); + + let url = format!("{}{}.zip", GODOT4_EXPORT_TEMPLATES_BASE_URL.to_string(), file); + download_and_extract_zip( + url.as_str(), + dest_path.as_str(), + Some(format!("{GODOT_CURRENT_VERSION}.{file}.export-templates.zip")), + )?; + } + } else { + println!("No files mapped for template: {}", template); + } + } Ok(()) } diff --git a/src/image_comparison.rs b/src/image_comparison.rs index 418a51ce9..e12f196df 100644 --- a/src/image_comparison.rs +++ b/src/image_comparison.rs @@ -4,8 +4,10 @@ use std::path::{Path, PathBuf}; // Function to compare two images and calculate similarity pub fn compare_images_similarity(image_path_1: &Path, image_path_2: &Path) -> Result { - let img1 = image::open(image_path_1).map_err(|_| format!("Failed to open image: {:?}", image_path_1))?; - let img2 = image::open(image_path_2).map_err(|_| format!("Failed to open image: {:?}", image_path_2))?; + let img1 = image::open(image_path_1) + .map_err(|_| format!("Failed to open image: {:?}", image_path_1))?; + let img2 = image::open(image_path_2) + .map_err(|_| format!("Failed to open image: {:?}", image_path_2))?; if img1.dimensions() != img2.dimensions() { return Err("Images have different dimensions".to_string()); @@ -20,7 +22,9 @@ pub fn compare_images_similarity(image_path_1: &Path, image_path_2: &Path) -> Re let pixel1 = img1.get_pixel(x, y); let pixel2 = img2.get_pixel(x, y); - let diff = pixel1.channels().iter() + let diff = pixel1 + .channels() + .iter() .zip(pixel2.channels().iter()) .map(|(p1, p2)| (*p1 as f64 - *p2 as f64).powi(2)) .sum::(); @@ -41,7 +45,9 @@ pub fn compare_images_similarity(image_path_1: &Path, image_path_2: &Path) -> Re fn list_png_files(directory: &Path) -> Result, String> { let mut files = vec![]; - for entry in fs::read_dir(directory).map_err(|_| format!("Failed to read directory: {:?}", directory))? { + for entry in + fs::read_dir(directory).map_err(|_| format!("Failed to read directory: {:?}", directory))? + { let entry = entry.map_err(|_| "Failed to access entry in directory".to_string())?; let path = entry.path(); if path.extension().and_then(|ext| ext.to_str()) == Some("png") { @@ -54,7 +60,11 @@ fn list_png_files(directory: &Path) -> Result, String> { } // Function to compare all PNG files in two folders -pub fn compare_images_folders(snapshot_folder: &Path, result_folder: &Path, similarity_threshold: f64) -> Result<(), String> { +pub fn compare_images_folders( + snapshot_folder: &Path, + result_folder: &Path, + similarity_threshold: f64, +) -> Result<(), String> { let snapshot_files = list_png_files(snapshot_folder)?; let result_files = list_png_files(result_folder)?; @@ -85,6 +95,9 @@ pub fn compare_images_folders(snapshot_folder: &Path, result_folder: &Path, simi ); } - println!("All files match with 99.95% similarity or higher!"); + println!( + "All files match with {:.2}% similarity or higher!", + similarity_threshold * 100.0 + ); Ok(()) -} \ No newline at end of file +} diff --git a/src/install_dependency.rs b/src/install_dependency.rs index 41ec06f5b..19575851a 100644 --- a/src/install_dependency.rs +++ b/src/install_dependency.rs @@ -127,7 +127,7 @@ pub fn download_and_extract_zip( // If the cached file exist, use it if let Some(already_existing_file) = get_existing_cached_file(persistent_cache.clone()) { - println!("Getting cached file of {url:?}"); + println!("Getting cached file of {url:?} (local path: {already_existing_file}"); fs::copy(already_existing_file, "./tmp-file.zip")?; } else { println!("Downloading {url:?}"); @@ -202,7 +202,7 @@ pub fn get_godot_executable_path() -> Option { Some(os_url) } -pub fn install(skip_download_templates: bool) -> Result<(), anyhow::Error> { +pub fn install(skip_download_templates: bool, platforms: &[String]) -> Result<(), anyhow::Error> { let persistent_path = get_persistent_path(Some("test.zip".into())).unwrap(); println!("Using persistent path: {persistent_path:?}"); @@ -243,7 +243,7 @@ pub fn install(skip_download_templates: bool) -> Result<(), anyhow::Error> { fs::copy(program_path, dest_program_path.as_str())?; if !skip_download_templates { - prepare_templates()?; + prepare_templates(platforms)?; } Ok(()) diff --git a/src/main.rs b/src/main.rs index 0ea18be24..8be291345 100644 --- a/src/main.rs +++ b/src/main.rs @@ -57,12 +57,20 @@ fn main() -> Result<(), anyhow::Error> { ) .subcommand(Command::new("docs")) .subcommand( - Command::new("install").arg( - Arg::new("no-templates") - .long("no-templates") - .help("skip download templates") - .takes_value(false), - ), + Command::new("install") + .arg( + Arg::new("no-templates") + .long("no-templates") + .help("skip download templates") + .takes_value(false), + ) + .arg( + Arg::new("platforms") + .long("platforms") + .help("download platform, can use multiple platforms, use like `--platforms linux android`") + .takes_value(true) + .multiple_values(true), + ), ) .subcommand(Command::new("update-protocol")) .subcommand( @@ -152,7 +160,16 @@ fn main() -> Result<(), anyhow::Error> { let root = xtaskops::ops::root_dir(); let res = match subcommand { - ("install", sm) => install_dependency::install(sm.is_present("no-templates")), + ("install", sm) => { + let no_templates = sm.is_present("no-templates"); + let platforms: Vec = sm + .values_of("platforms") + .map(|vals| vals.map(String::from).collect()) + .unwrap_or_default(); + + // Call your install function and pass the templates + install_dependency::install(no_templates, &platforms) + }, ("update-protocol", _) => install_dependency::install_dcl_protocol(), ("compare-image-folders", sm) => { let snapshot_folder = Path::new(sm.value_of("snapshots").unwrap()); @@ -188,13 +205,10 @@ fn main() -> Result<(), anyhow::Error> { ("import-assets", _m) => { let status = import_assets(); if !status.success() { - println!( - "WARN: cargo build exited with non-zero status: {}", - status - ); + println!("WARN: cargo build exited with non-zero status: {}", status); } Ok(()) - }, + } ("coverage", sm) => coverage_with_itest(sm.is_present("dev")), ("test-tools", _) => test_godot_tools(None), ("vars", _) => { diff --git a/src/path.rs b/src/path.rs index 89ad8d1ba..ff3ef0ad6 100644 --- a/src/path.rs +++ b/src/path.rs @@ -16,14 +16,13 @@ pub fn adjust_canonicalization>(p: P) -> String { } } -pub fn get_godot_path() -> String -{ +pub fn get_godot_path() -> String { adjust_canonicalization( std::fs::canonicalize(format!( "{}godot/{}", BIN_FOLDER, install_dependency::get_godot_executable_path().unwrap() )) - .expect("Did you executed `cargo run -- install`?") + .expect("Did you executed `cargo run -- install`?"), ) -} \ No newline at end of file +} diff --git a/src/tests.rs b/src/tests.rs index 853a09761..91fc483b9 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -4,17 +4,73 @@ use anyhow::Ok; use crate::{copy_files::move_dir_recursive, image_comparison::compare_images_folders, run}; -pub fn test_godot_tools(with_build_envs: Option>) -> Result<(), anyhow::Error> { - let avatar_snapshot_folder = Path::new("./tests/snapshots/avatar-image-generation").canonicalize()?; +fn test_avatar_generation( + with_build_envs: Option>, +) -> Result<(), anyhow::Error> { + let avatar_snapshot_folder = + Path::new("./tests/snapshots/avatar-image-generation").canonicalize()?; let comparison_folder = avatar_snapshot_folder.join("comparison"); println!("=== running godot avatar generation ==="); + let avatar_output = Path::new("./godot/output/"); + if !avatar_output.exists() { + std::fs::create_dir_all(&avatar_output)?; + } + + let avatar_test_input = Path::new("./../tests/avatars-test-input.json"); let extra_args = [ "--rendering-driver", "opengl3", + "--rendering-method", + "gl_compatibility", "--avatar-renderer", - "--use-test-input", + "--avatars", + avatar_test_input.to_str().unwrap(), + ] + .iter() + .map(|it| it.to_string()) + .collect(); + + run::run( + false, + false, + false, + false, + false, + false, + vec![], + extra_args, + with_build_envs.clone(), + )?; + + // Move files + move_dir_recursive(&avatar_output.canonicalize()?, &comparison_folder)?; + + // Images comparison + compare_images_folders(&avatar_snapshot_folder, &comparison_folder, 0.90) + .map_err(|e| anyhow::anyhow!(e))?; + + Ok(()) +} + +fn test_scene_generation( + with_build_envs: Option>, +) -> Result<(), anyhow::Error> { + println!("=== running scene generation ==="); + let scene_output = Path::new("./godot/output/"); + if !scene_output.exists() { + std::fs::create_dir_all(&scene_output)?; + } + let scene_test_input = Path::new("./../tests/scene-renderer-test-input.json"); + let extra_args = [ + "--rendering-driver", + "opengl3", + "--rendering-method", + "gl_compatibility", + "--scene-renderer", + "--scene-input-file", + scene_test_input.to_str().unwrap(), ] .iter() .map(|it| it.to_string()) @@ -32,14 +88,27 @@ pub fn test_godot_tools(with_build_envs: Option>) -> Res with_build_envs, )?; + let scene_renderer_snapshot_folder = + Path::new("./tests/snapshots/scene-image-generation").canonicalize()?; + let comparison_folder = scene_renderer_snapshot_folder.join("comparison"); + // Move files - let avatar_output = Path::new("./godot/output/").canonicalize()?; - move_dir_recursive(&avatar_output, &comparison_folder)?; + move_dir_recursive(&scene_output.canonicalize()?, &comparison_folder)?; // Images comparison - compare_images_folders(&avatar_snapshot_folder, &comparison_folder, 0.995) + compare_images_folders(&scene_renderer_snapshot_folder, &comparison_folder, 0.90) .map_err(|e| anyhow::anyhow!(e))?; + Ok(()) +} +pub fn test_godot_tools( + with_build_envs: Option>, +) -> Result<(), anyhow::Error> { + let avatar_result = test_avatar_generation(with_build_envs.clone()); + let scene_result = test_scene_generation(with_build_envs.clone()); + + scene_result?; + avatar_result?; Ok(()) } diff --git a/tests/scene-renderer-test-input.json b/tests/scene-renderer-test-input.json new file mode 100644 index 000000000..d6268bfb8 --- /dev/null +++ b/tests/scene-renderer-test-input.json @@ -0,0 +1,29 @@ +{ + "realmUrl": "https://sdk-team-cdn.decentraland.org/ipfs/goerli-plaza-main-latest", + "defaultPayload": { + "coords": "9,-9", + "width": 512, + "height": 512, + "destPath": "output/test-scene-rendering-$index-$coords.png", + "sceneDistance": 0, + "camera": { + "position": { + "x": 8, + "y": 25, + "z": -8 + }, + "target": { + "x": 8, + "y": 0, + "z": -8 + }, + "orthoSize": 25, + "projection": "ortho" + } + }, + "payload": [ + { + "coords": "73,-6" + } + ] +} \ No newline at end of file diff --git a/tests/snapshots/scene-image-generation/test-scene-rendering-0-73_-6.png b/tests/snapshots/scene-image-generation/test-scene-rendering-0-73_-6.png new file mode 100644 index 000000000..5f1b8a674 Binary files /dev/null and b/tests/snapshots/scene-image-generation/test-scene-rendering-0-73_-6.png differ