diff --git a/mirror-godot-app/creator/asset_inventory/browser/recent/recents_browser_section.gd b/mirror-godot-app/creator/asset_inventory/browser/recent/recents_browser_section.gd index 260ddab5..69d36ec7 100644 --- a/mirror-godot-app/creator/asset_inventory/browser/recent/recents_browser_section.gd +++ b/mirror-godot-app/creator/asset_inventory/browser/recent/recents_browser_section.gd @@ -152,7 +152,8 @@ func _generate_new_slot(recent_asset: Dictionary) -> BaseAssetSlot: slot.slot_activated.connect(_asset_browser.asset_slot_activated) slot.slot_special_action.connect(_asset_browser.use_slot_asset) _id_to_slot_map[recent_asset["id"]] = slot - _slots_flow_container.add_child(slot) + if slot.get_parent() == null: + _slots_flow_container.add_child(slot) return slot diff --git a/mirror-godot-app/player/cameras/camera_manager.tscn b/mirror-godot-app/player/cameras/camera_manager.tscn index 41c66928..fbd08201 100644 --- a/mirror-godot-app/player/cameras/camera_manager.tscn +++ b/mirror-godot-app/player/cameras/camera_manager.tscn @@ -1,10 +1,9 @@ -[gd_scene load_steps=6 format=3 uid="uid://brbtsnjio4p2m"] +[gd_scene load_steps=5 format=3 uid="uid://brbtsnjio4p2m"] [ext_resource type="Script" path="res://player/cameras/camera_manager.gd" id="1_sgi5b"] [ext_resource type="PackedScene" uid="uid://g08bytt518wj" path="res://player/cameras/head/player_head_camera.tscn" id="2_h612i"] [ext_resource type="PackedScene" uid="uid://h5gnmb66fkgi" path="res://player/cameras/free/free_camera.tscn" id="4_35k1r"] [ext_resource type="Script" path="res://player/cameras/placement_preview.gd" id="5_h3wxg"] -[ext_resource type="PackedScene" uid="uid://clraq8sj7pby6" path="res://player/cameras/voxel_viewer/camera_voxel_viewer.tscn" id="6_n4xo0"] [node name="CameraManager" type="Node"] script = ExtResource("1_sgi5b") @@ -23,14 +22,13 @@ audio_listener_enable_3d = true size = Vector2i(2560, 1440) render_target_update_mode = 4 -[node name="PlayerHeadCamera" parent="SubViewportContainer/SubViewport" instance=ExtResource("2_h612i")] +[node name="PlayerHeadCamera" parent="SubViewportContainer/SubViewport" node_paths=PackedStringArray("_camera") instance=ExtResource("2_h612i")] +_camera = NodePath("CameraRecoilOffset/ThirdPersonCameraArm/ThirdPersonCamera") -[node name="FreeCamera" parent="SubViewportContainer/SubViewport" instance=ExtResource("4_35k1r")] +[node name="FreeCamera" parent="SubViewportContainer/SubViewport" node_paths=PackedStringArray("_camera") instance=ExtResource("4_35k1r")] +_camera = NodePath("Camera") [node name="PlacementPreview" type="Node3D" parent="SubViewportContainer/SubViewport"] script = ExtResource("5_h3wxg") [node name="JoltDebugGeometry3D" type="JoltDebugGeometry3D" parent="SubViewportContainer/SubViewport"] - -[node name="CameraVoxelViewer" parent="." node_paths=PackedStringArray("camera_manager") instance=ExtResource("6_n4xo0")] -camera_manager = NodePath("..") diff --git a/mirror-godot-app/player/equipable/equipable_controller.gd b/mirror-godot-app/player/equipable/equipable_controller.gd index 69b3d9f6..23cddd90 100644 --- a/mirror-godot-app/player/equipable/equipable_controller.gd +++ b/mirror-godot-app/player/equipable/equipable_controller.gd @@ -154,6 +154,9 @@ func _load_equipable_by_asset_id(asset_id: String) -> void: if asset_id != _current_asset_id: # It is possible that another call to this method changed the current equipable. return + if file_promise.is_error(): + push_error("Failed to download asset: ", file_promise.get_error_message()) + equipable_changed.emit(null) var file_result = file_promise.get_result() if file_result is Node3D and file_result.has_meta(&"MIRROR_equipable"): _setup_equipped_item(file_result) diff --git a/mirror-godot-app/prefabs/autoload/zone/collision_shape_generator/collision_shape_generator.gd b/mirror-godot-app/prefabs/autoload/zone/collision_shape_generator/collision_shape_generator.gd index a3f36789..a42d0074 100644 --- a/mirror-godot-app/prefabs/autoload/zone/collision_shape_generator/collision_shape_generator.gd +++ b/mirror-godot-app/prefabs/autoload/zone/collision_shape_generator/collision_shape_generator.gd @@ -75,6 +75,9 @@ func generate_shape_for_meshes(body: JBody3D, in_meshes: Array[MeshInstance3D], if is_concave: var tms: ConcavePolygonShape3D = mesh.mesh.create_trimesh_shape() shape = JMeshShape3D.new() + if tms == null: + push_error("invalid physics shape") + continue shape.faces = tms.get_faces() else: var cps: ConvexPolygonShape3D = mesh.mesh.create_convex_shape(false, false) diff --git a/mirror-godot-app/scripts/autoload/util_funcs.gd b/mirror-godot-app/scripts/autoload/util_funcs.gd index 83e2bc75..42c6cde0 100644 --- a/mirror-godot-app/scripts/autoload/util_funcs.gd +++ b/mirror-godot-app/scripts/autoload/util_funcs.gd @@ -248,27 +248,6 @@ static func looks_like_json(value) -> bool: return has_braces or has_brackets -## Loads a GLTF file from the disk as a node object. -static func load_gltf_file_as_node(path: String) -> Variant: - var state: GLTFState = GLTFState.new() - if Zone.is_host(): - # Discard the textures when on the server - state.set_handle_binary_image(GLTFState.HANDLE_BINARY_DISCARD_TEXTURES) - var doc: GLTFDocument = GLTFDocument.new() - var err = doc.append_from_file(path, state, 8) - if err: - push_error(str(err)) - return null - var node: Node = doc.generate_scene(state) - if not is_instance_valid(node): - print_debug("generate_scene failed from path:", path) - return null - # Disallow importing a model with an empty root node name. - if node.name == &"": - node.name = &"Model" - return node - - ## Converts a GLTF document (including all its external dependencies ) to a GLB byte array. ## See https://github.com/the-mirror-megaverse/mirror-godot-app/pull/261 for why static func convert_gltf_to_glb_data(path: String) -> PackedByteArray: diff --git a/mirror-godot-app/scripts/autoload/zone/client.gd b/mirror-godot-app/scripts/autoload/zone/client.gd index aa251009..aa0cd760 100644 --- a/mirror-godot-app/scripts/autoload/zone/client.gd +++ b/mirror-godot-app/scripts/autoload/zone/client.gd @@ -115,6 +115,8 @@ func _client_on_game_ui_space_loaded() -> void: ## connect_to_server func connect_to_server(server_addr: Variant, port: Variant) -> bool: + GameUI.loading_ui.populate_status("Opening connection to server") + print("Opening connection to server at ", Time.get_datetime_string_from_system()) if server_addr.is_empty(): return false client_peer = ENetMultiplayerPeer.new() @@ -162,7 +164,8 @@ func _client_on_connected_to_server() -> void: print("----------------------------------------") print("ClientPeer: Connected to a server... waiting for server to grant access") print("----------------------------------------") - + GameUI.loading_ui.populate_status("Client socket open") + print("Client connection opened at " + Time.get_datetime_string_from_system()) Analytics.track_event_client(AnalyticsEvent.TYPE.SPACE_JOIN_ATTEMPT_SUCCESS, {"spaceId": _queued_space_id}) Zone.change_to_space_scene() # TODO: Instead of true, determine if the player has creator permissions for the space. @@ -175,18 +178,22 @@ func _client_on_connected_to_server() -> void: var jwt = Firebase.Auth.get_jwt() var user_id = JWT.get_user_id_from_jwt(jwt, "test123") var client_version: String = str(Util.get_version_string()) + GameUI.loading_ui.populate_status("Requesting client spawn...") + print("Sending client init to server at " + Time.get_datetime_string_from_system()) Zone.send_data_to_server([Packet.TYPE.CLIENT_INIT, jwt, client_version]) PlayerData.acknowledge_local_user_id(user_id) # note: GDScript cannot understand Zone definition unless passed via a variable in the stack. var zone_autoload = Zone - TMSceneSync.start_sync(zone_autoload) + # TODO: gordon look here this is a bit fishy. Why start syncing things before all objects exist? + # TMSceneSync.start_sync(zone_autoload) # wait for the space to be in a loaded enough condition to join. # play servers load all objects before finishing # wait for the first spawn to complete too while not is_space_loaded(): await get_tree().create_timer(0.5).timeout + join_server_complete.emit() @@ -438,6 +445,8 @@ func start_join_localhost() -> void: func start_join_zone_by_space_id(space_id: String) -> void: + print("Join requested at ", Time.get_datetime_string_from_system()) + GameUI.loading_ui.populate_status("Joining space") _is_joining_play_space = false join_server_start.emit() if space_id == _LOCALHOST: @@ -449,6 +458,7 @@ func start_join_zone_by_space_id(space_id: String) -> void: func start_join_play_space_by_space_id(space_id: String) -> void: + GameUI.loading_ui.populate_status("Joining space") join_server_start.emit() _disconnect_from_server_peer() _is_joining_play_space = true # after disconnect so flag is not cleared @@ -544,7 +554,7 @@ func _join_new_server_locally(space_id: String) -> bool: var firebase_auth = str(Firebase.Auth.auth.refreshtoken) # For debugging this allows you to grab breakpoints from the server "--remote-debug", "tcp://127.0.0.1:6008"] # If enabled it could cause join time to be much longer when booting server - var arguments = ["--server", "--space", space_id, "--mode", "edit", "--uuid", "localhost", "--server_login", firebase_auth, "--headless"] # "--remote-debug", "tcp://127.0.0.1:6008"] + var arguments = ["--server", "--space", space_id, "--mode", "edit", "--uuid", "localhost", "--server_login", firebase_auth, "--headless", "--remote-debug", "tcp://127.0.0.1:6007"] pid = OS.create_process(OS.get_executable_path(), arguments, true) start_join_localhost() return true diff --git a/mirror-godot-app/scripts/autoload/zone/server.gd b/mirror-godot-app/scripts/autoload/zone/server.gd index 8b4848f4..1990165e 100644 --- a/mirror-godot-app/scripts/autoload/zone/server.gd +++ b/mirror-godot-app/scripts/autoload/zone/server.gd @@ -534,6 +534,7 @@ func _all_files_downloaded() -> bool: func _init_player(peer_id, jwt, client_version) -> void: + print("Received client init on server at " + Time.get_datetime_string_from_system()) # TODO: This only decodes the JWT, but it needs to validated via Firebase SDK (probs via the NestJS server for ease) # TODO: Kick the player if JWT validating fails var user_id = JWT.get_user_id_from_jwt(jwt, "test123") diff --git a/mirror-godot-app/scripts/net/file_cache.gd b/mirror-godot-app/scripts/net/file_cache.gd index 7de7a125..cc627247 100644 --- a/mirror-godot-app/scripts/net/file_cache.gd +++ b/mirror-godot-app/scripts/net/file_cache.gd @@ -1,8 +1,6 @@ class_name FileCache extends Node -signal threaded_model_loaded(cache_key: String, node: Node) - const _STORAGE_CACHE_FILENAME: String = "cache.json" var _storage_cache: Dictionary = {} @@ -22,18 +20,6 @@ func _init() -> void: _setup_storage_directory() - -func _process(_delta: float) -> void: - _manage_queue() - - -## Manages the threaded model loading queue. -func _manage_queue() -> void: - if _model_load_queue.size() == 0: - return - _thread_load_model(_model_load_queue.pop_front()) - - ## Returns true if the cache file exists on the disk. func cached_file_exists(cache_key: String) -> bool: cache_key = cache_key.uri_decode() @@ -96,6 +82,8 @@ func _save_stored_files_cache() -> void: ## Saves a bytes file to the cache location on disk and adds it to the cache library. func save_bytes_file_to_cache(cache_key: String, file_name: String, file_data: PackedByteArray) -> void: + if FileAccess.file_exists(file_name): + return var saved = save_bytes_file(file_name, file_data) if not saved: return @@ -129,7 +117,7 @@ func try_load_cached_file(cache_key: String) -> Variant: if not FileAccess.file_exists(file_path): return null if Util.path_is_model(file_path): - return TMFileUtil.load_gltf_file_as_node(file_path, Zone.is_host()) + return load_gltf_thread_task(file_path) elif Util.path_is_image(file_path): return Util.load_image(file_path) elif Util.path_is_scene(file_path): @@ -140,37 +128,39 @@ func try_load_cached_file(cache_key: String) -> Variant: return TMFileUtil.load_json_file(file_path) return null - -func load_model_threaded(cache_key: String) -> Promise: - for m in _model_load_queue: - if m.key == cache_key: - return m.promise +# in memory cache +var _cached_pairs = {} +var mutex: Mutex = Mutex.new() +func load_gltf_thread_task(cache_key: String) -> Promise: + mutex.lock() + if _cached_pairs.has(cache_key): + print("Cache found... not loading twice") + return _cached_pairs[cache_key].promise var pair = KeyPromisePair.new() pair.key = cache_key pair.promise = Promise.new() + _cached_pairs[pair.key] = pair - # TODO: Fix multithreaded model loading, newer engine versions - # complain about this not being on the main thread. - # queue for later when needed - if cached_file_exists(pair.key): - _model_load_queue.append(pair) - return pair.promise - -# _model_load_thread.start(_thread_load_model.bind(pair)) - _thread_load_model(pair) - return pair.promise - - -func _thread_load_model(pair: KeyPromisePair) -> void: if not cached_file_exists(pair.key): pair.promise.set_error("File does not exists, cannot load.") return var file_name: String = _storage_cache.get(pair.key, "") var file_path: String = get_file_path(file_name) - var node = TMFileUtil.load_gltf_file_as_node(file_path, Zone.is_host()) - _model_loaded.call_deferred(pair, node) + var task_id = WorkerThreadPool.add_task(func(): + var node = TMFileUtil.load_gltf_file_as_node(file_path, Zone.is_host()) + call_thread_safe("_cached_file_is_loaded", pair, node) + ) + mutex.unlock() + return pair.promise -func _model_loaded(pair: KeyPromisePair, node: Node) -> void: - threaded_model_loaded.emit(pair.key, node) +## THIS MUST BE ON THE MAIN THREAD +func _cached_file_is_loaded(pair, node): + mutex.lock() + print("Node name: ", node.get_name()) + if node == null: + push_error("Can't load GLTF") + pair.promise.set_error("Failed to load mesh, ignoring and skipping") + return pair.promise.set_result(node) + mutex.unlock() diff --git a/mirror-godot-app/scripts/net/file_client.gd b/mirror-godot-app/scripts/net/file_client.gd index 86a69ac5..252e25f3 100644 --- a/mirror-godot-app/scripts/net/file_client.gd +++ b/mirror-godot-app/scripts/net/file_client.gd @@ -83,7 +83,7 @@ func get_file(url: String, priority: Enums.DownloadPriority = Enums.DownloadPrio promise.set_result(files.get(url)) return promise if Util.path_is_model(url) and _file_cache.cached_file_exists(url): - var promise = _file_cache.load_model_threaded(url) + var promise = _file_cache.load_gltf_thread_task(url) promise.connect_func_to_fulfill(_on_loaded_model_threaded.bind(url, promise)) return promise var cached_file = _file_cache.try_load_cached_file(url) @@ -119,7 +119,7 @@ func get_file(url: String, priority: Enums.DownloadPriority = Enums.DownloadPrio ## so the model gets uniquely generated from a GLTFDocument. ## TODO: Assess storing GLTFDocument in memory and generating node from that instead of entire file read. func get_model_instance_promise(url: String) -> Promise: - return _file_cache.load_model_threaded(url) + return _file_cache.load_gltf_thread_task(url) func _promise_fulfill_successful(request: Dictionary, promise: Promise) -> void: diff --git a/mirror-godot-app/ui/game/loading_ui.gd b/mirror-godot-app/ui/game/loading_ui.gd index bf05a91a..851a3c6e 100644 --- a/mirror-godot-app/ui/game/loading_ui.gd +++ b/mirror-godot-app/ui/game/loading_ui.gd @@ -41,6 +41,8 @@ func _on_join_server_start() -> void: show() func _on_join_server_complete() -> void: + GameUI.loading_ui.populate_status("") + print("Loading UI has been closed at: ", Time.get_datetime_string_from_system()) hide() _progress_animation.stop()