diff --git a/.gitignore b/.gitignore index 4760075..7a8a240 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /src/.godot /src/android/ +/src/addons/godot-sqlite +/src/setup_downloads/ diff --git a/README.md b/README.md index 282669f..b420812 100644 --- a/README.md +++ b/README.md @@ -16,26 +16,35 @@ Client is the interface used to connect and interact with the virtual world of O ## Development Quick Start -### 1: Download source code +### Download source code For cloning this project on Linux: ```bash # Clone this repository. git clone --recurse-submodules https://github.com/OpenMinerva/client ``` -### 2: Import project into Godot. -2.a: Open your copy of the [Godot Engine](https://godotengine.org/). +### Setup the environment +This repository does not ship some required addons in order to prevent unnecessary weight in the git history. Instead there is a script `/src/setup_environment.sh` (Linux only) that can be executed to automatically download the addons. Windows users will need to install these addons manually for now. -2.b: Click on "Import" near the top of the application window. +For simplicity, here is a list of the missing addons: +- [godot-sqlite](https://github.com/2shady4u/godot-sqlite) - Tagged `v4.8` - [Download](https://github.com/2shady4u/godot-sqlite/releases/download/v4.8/addons.zip) -2.c: Navigate to the location where the source code was downloaded to. - -2.d: Navigate to the subdirectory `/src`. - -2.e: Select `project.godot` to import this project. +Download these addons into the `/src/addons/` folder of this repository. +Example: +``` +/src + /addons + /godot-sqlite + /discord-rpc-gd + /rpc-await +``` -### 3: Open -Double click on the added entry. +### Import project into Godot. +- Open your copy of the [Godot Engine](https://godotengine.org/). +- Click on "Import" near the top of the application window. +- Navigate to the location where the source code was downloaded to. +- Navigate to the sub-directory `/src`. +- Select `project.godot` to import this project. ## Contributing See [CONTRIBUTING.md](https://github.com/OpenMinerva/client/blob/alpha/CONTRIBUTING.md) for guidelines, and information. diff --git a/src/project.godot b/src/project.godot index f1dfe33..f75edad 100644 --- a/src/project.godot +++ b/src/project.godot @@ -38,6 +38,7 @@ HTTP="*uid://d3cnfdwjxopsx" FileDrop="*uid://btgqgfpx0av4m" Events="*uid://c656spc3ppdlw" StateManager="*uid://b5vvw77bmn4yc" +Database="*uid://cbm5uyffux8ca" OAuth2Client="*uid://c4b880pwgfrty" UrlParser="*uid://budprjmmpally" @@ -54,7 +55,7 @@ window/stretch/aspect="expand" [editor_plugins] -enabled=PackedStringArray("res://addons/Gizmo3DScript/plugin.cfg", "res://addons/discord-rpc-gd/plugin.cfg", "res://addons/godot-oauth2client/plugin.cfg", "res://addons/godot-urlparser/plugin.cfg", "res://addons/rpc-await/plugin.cfg") +enabled=PackedStringArray("res://addons/Gizmo3DScript/plugin.cfg", "res://addons/discord-rpc-gd/plugin.cfg", "res://addons/godot-oauth2client/plugin.cfg", "res://addons/godot-sqlite/plugin.cfg", "res://addons/godot-urlparser/plugin.cfg", "res://addons/rpc-await/plugin.cfg") [gui] diff --git a/src/scenes/managers/app/network.gd b/src/scenes/managers/app/network.gd index f66ef08..14baa27 100644 --- a/src/scenes/managers/app/network.gd +++ b/src/scenes/managers/app/network.gd @@ -34,7 +34,7 @@ var _heartbeats = { } @onready var scene_m = get_node("../SceneManager") -func start_server(port: int = 0, root_scene: Enum.BaseLevel = Enum.BaseLevel.GRID) -> bool: +func start_server(port: int = 0, root_scene: Enum.BaseLevel = Enum.BaseLevel.GRID, scene_dir: String = "") -> bool: GlobalLogger.log("Starting a new server.") # Get an available port. If port was defined, force that port or fail. @@ -68,10 +68,7 @@ func start_server(port: int = 0, root_scene: Enum.BaseLevel = Enum.BaseLevel.GRI _session_db.set(_scene, _create_instance_db_entry(_mp_api, _scene, Enum.PrivacyLevel.INVITE, port, "host")) # Create server root scene. - if root_scene: - scene_m.set_master_root_from_program(_scene, root_scene) - else: - scene_m.set_master_root_from_program(_scene, Enum.BaseLevel.GRID) + scene_m.set_master_root_from_program(_scene, root_scene, scene_dir) scene_m.start_master_scene(_scene) scene_m.set_active_session(_scene) diff --git a/src/scenes/managers/app/scene.gd b/src/scenes/managers/app/scene.gd index 575935d..c66a32b 100644 --- a/src/scenes/managers/app/scene.gd +++ b/src/scenes/managers/app/scene.gd @@ -51,7 +51,7 @@ func destroy_master_scene(id: String): return -func set_master_root_from_program(id: String, scene_type: Enum.BaseLevel) -> void: +func set_master_root_from_program(id: String, scene_type: Enum.BaseLevel, scene_dir: String = "") -> void: var _scene = get_master_scene(id) var _root_scene: String = _get_scene_by_type(scene_type) @@ -72,7 +72,15 @@ func set_master_root_from_program(id: String, scene_type: Enum.BaseLevel) -> voi await get_tree().process_frame # Use spawnable system to read the TSCN file, and instantiate it into the multiplayer instance. - await spawnable_file_handling.load_spawnable(_root_scene) + if scene_type == Enum.BaseLevel.CUSTOM: + if scene_dir == "": + GlobalLogger.log("Tried to load a custom scene, but there was not a directiory!", Enum.LogLevel.WARNING) + _root_scene = _get_scene_by_type(Enum.BaseLevel.GRID) + await spawnable_file_handling.load_spawnable(_root_scene) + else: + await spawnable_file_handling.load_spawnable(scene_dir) + else: + await spawnable_file_handling.load_spawnable(_root_scene) # Remove the "root" node of the world, and instead parent all nodes under the true instance root. # HACK: Force reparent the children of the node to the world root. diff --git a/src/scenes/managers/app/scene.gd.uid b/src/scenes/managers/app/scene.gd.uid index 02824f9..3d7795d 100644 --- a/src/scenes/managers/app/scene.gd.uid +++ b/src/scenes/managers/app/scene.gd.uid @@ -1 +1 @@ -uid://3ylijxenr1bg +uid://c82mspg5iieer diff --git a/src/scripts/enum.gd b/src/scripts/enum.gd index 5ac1a37..75783b2 100644 --- a/src/scripts/enum.gd +++ b/src/scripts/enum.gd @@ -19,6 +19,7 @@ enum BaseLevel { HOME = 1, EMPTY = 2, GRID = 3, + CUSTOM = 4, } enum PrivacyLevel { INVITE = 0, @@ -35,11 +36,8 @@ enum LogLevel { ERROR = 3, } enum SpawnableType { - EMPTY = 0, - BOX = 1, - RIGIDBODY = 2, - CAPSULE = 3, - MODEL = 10, + ITEM = 0, + WORLD = 1, } const Settings = { diff --git a/src/scripts/spawnable_file_handling.gd b/src/scripts/spawnable_file_handling.gd index 11ac60a..3cec42d 100644 --- a/src/scripts/spawnable_file_handling.gd +++ b/src/scripts/spawnable_file_handling.gd @@ -15,13 +15,14 @@ func _ready() -> void: return -func save_spawnable(root: Node, name_override: String = "") -> void: +func save_spawnable(root: Node, type: Enum.SpawnableType = Enum.SpawnableType.ITEM, name_override: String = "") -> void: # Take a node path from the scene, and save that node to the file # In order to save nodes using the ResourceSaver, we need to make all nodes that are a child of the root have the owner of the root. # FIXME: When saving the node scene, The FileManager should be used entirely. # ResourceLoader / ResourceSaver should not be called from here. var _spawnable_name: String = "" + # TODO: Get hash of file, and use that hash for filename on disk. if name_override != "": _spawnable_name = name_override elif root.has_meta("pretty_name") == true: @@ -29,23 +30,32 @@ func save_spawnable(root: Node, name_override: String = "") -> void: else: _spawnable_name = "Untitled" - # Duplicate the node, this is so we can make modifications to it (if required to) # This duplicate does not touch the scene tree. root = root.duplicate() var original_owners = _get_node_ownership(root) _set_temporary_ownership_recursive(root, root) - # Externalize assets makes it so that networking things are consistent. - _externalize_assets(root) - - # Remove PlayerControllers. _remove_invalid_nodes(root) var packed_scene = _create_packed_scene(root) - var file_path = FileManager._current_path() + "/" + _spawnable_name + ".tscn" + var _data_hash: String = var_to_str(packed_scene).sha256_text() + + _externalize_assets(root, _data_hash) + + var file_path = FileManager._current_path() + _data_hash + ".tscn" ResourceSaver.save(packed_scene, file_path) + # Add spawnable to the database. + var _database_row: Dictionary = { + "hash": _data_hash, + "name": _spawnable_name, + "directory": file_path, + "creation_date": int(Time.get_unix_time_from_system()), + "type": type, + } + Database.set_spawnable(_data_hash, _database_row) + # I have no idea what ownership is in terms of nodes are right now, so put it back as to not break anything _restore_ownership(root, original_owners) @@ -141,6 +151,11 @@ func load_spawnable(path: String) -> void: return +func _add_spawnable_asset_relation(spawnable_hash: String, asset_hash: String) -> void: + Database.set_spawnable_asset_rel(spawnable_hash, asset_hash) + return + + func _flatten_resource(resource: Resource): var _resource_props = resource.get_property_list() var _response: Dictionary = { "properties": [], "class": "" } @@ -155,22 +170,43 @@ func _flatten_resource(resource: Resource): return _response -func _externalize_assets(root: Node) -> Node: +func _externalize_assets(root: Node, spawnable_hash: String) -> Node: + # TODO: This way of externalizing assets functions, but is not what is intended. I need to create a thorough walk-through of a node hierarchy and individually get all resources of that node. Right now this seems to just do a surface level extraction. for property in root.get_property_list(): var value = root.get(property.name) if value is Resource and value.resource_path.is_empty(): - # TODO: Hash the files to use as names? - var external_path = "user://spawnables_assets/%d.res" % randi() - ResourceSaver.save(value, external_path) + # TODO: Throw this onto a separate thread. + var _data_hash: String = var_to_str(value).sha256_text() + var _data_size: int + var _external_path = "user://spawnables_assets/%s.res" % _data_hash + + ResourceSaver.save(value, _external_path) - var external_resource = load(external_path) - external_resource.take_over_path(external_path) + var external_resource = load(_external_path) + external_resource.take_over_path(_external_path) root.set(property.name, external_resource) + if Database.get_asset(_data_hash) == { }: + # Add to the database if we do not already have the asset in the database. + + # Get the file size of the asset + var _file = FileAccess.open(_external_path, FileAccess.READ) + _data_size = _file.get_length() + _file.close() + + var _database_row: Dictionary = { + "hash": _data_hash, + "directory": _external_path, + "size": _data_size, + } + Database.set_asset(_data_hash, _database_row) + + _add_spawnable_asset_relation(spawnable_hash, _data_hash) + for child in root.get_children(): - _externalize_assets(child) + _externalize_assets(child, spawnable_hash) return root diff --git a/src/scripts/spawnable_file_handling.gd.uid b/src/scripts/spawnable_file_handling.gd.uid index e52df7d..9a6fae3 100644 --- a/src/scripts/spawnable_file_handling.gd.uid +++ b/src/scripts/spawnable_file_handling.gd.uid @@ -1 +1 @@ -uid://4rhvu1oluj66 +uid://bl8yv02xhnx6s diff --git a/src/scripts/utils/database.gd b/src/scripts/utils/database.gd new file mode 100644 index 0000000..a4d7c30 --- /dev/null +++ b/src/scripts/utils/database.gd @@ -0,0 +1,171 @@ +# --- License +# File: /client/src/scripts/utils/database.gd +# Project: OpenMinerva +# Created Date: 05 August 2026 +# Copyright (c) 2026 OpenMinerva +# License: MIT License +# Authors: Armored Dragon +# --- License +extends Node + +const DATABASE_PATH: String = "user://spawnable_database.db" + +var _db: SQLite = SQLite.new() + + +func _ready() -> void: + _db.path = DATABASE_PATH + + # Ensure Database it set up + if _database_exists() == false: + GlobalLogger.log("Could not open the spawnable database", Enum.LogLevel.ERROR) + # Since this is a critical error, I should probably display an error window before force-closing the app? + return + + _build_database_schema() + + # Preform sanity check to ensure database is not corrupted. Or that it has not changed from when we last changed it. + # Should we hash the database on close, then read the hash on open to ensure not-corrupted / changed? + # If there was a difference, would we need to rebuild the database? What would that look like? Sounds expensive! + return + + +func _exit_tree() -> void: + _db.close_db() + return + + +func get_spawnable(item_hash: String) -> Dictionary: + _db.query("SELECT * FROM spawnable WHERE hash = '%s'" % item_hash) + + if _db.query_result.is_empty(): + GlobalLogger.log("[ Database ] Spawnable '%s' does not exist." % item_hash, Enum.LogLevel.DEBUG) + return { } + + return _db.query_result[0] + + +func get_spawnables_by_directory(directory: String) -> Array: + var _query: String = "SELECT * FROM spawnable WHERE directory LIKE ? AND directory NOT LIKE ?" + + _db.query_with_bindings(_query, [directory + "%", directory + "%/%"]) + + return _db.query_result + + +func set_spawnable(item_hash: String, new_data: Dictionary) -> bool: + GlobalLogger.log("[ Database ] Updating spawnable '%s'." % item_hash, Enum.LogLevel.DEBUG) + var _success: bool = _db.insert_row("spawnable", new_data) + return _success + + +func delete_spawnable(item_hash: String) -> bool: + var _asset_deletion_queue: Array = [] + var _assets: Array = get_spawnable_assets(item_hash) + + for _single_asset in _assets: + if is_asset_used_by_one_spawnable(_single_asset) == true: + _asset_deletion_queue.append(_single_asset) + + for _orphan_asset in _asset_deletion_queue: + FileManager.delete_file("user://spawnables_assets/%s.res" % _orphan_asset) + delete_asset(_orphan_asset) + + var _success: bool = _db.delete_rows("spawnable", "hash = '%s'" % item_hash) + _db.delete_rows("spawnable_asset_rel", "spawnable = '%s'" % item_hash) + + return _success + + +func get_asset(hash: String) -> Dictionary: + # TODO: Use query bindings? + GlobalLogger.log("[ Database ] Getting asset '%s'." % hash, Enum.LogLevel.DEBUG) + _db.query("SELECT * FROM asset WHERE hash = '%s'" % hash) + + if _db.query_result.is_empty(): + GlobalLogger.log("[ Database ] Asset '%s' does not exist." % hash, Enum.LogLevel.DEBUG) + return { } + + return _db.query_result[0] + + +func set_asset(item_hash: String, new_data: Dictionary) -> bool: + # TODO: Validation before inserting into the database. + GlobalLogger.log("[ Database ] Updating asset '%s'." % item_hash, Enum.LogLevel.DEBUG) + var _success: bool = _db.insert_row("asset", new_data) + return _success + + +func delete_asset(asset_hash: String) -> bool: + var _success: bool = _db.delete_rows("asset", "hash = '%s'" % asset_hash) + + return _success + + +func set_spawnable_asset_rel(item_hash: String, asset_hash: String) -> bool: + GlobalLogger.log("[ Database ] Adding item relationship '%s' to '%s'." % [item_hash, asset_hash], Enum.LogLevel.DEBUG) + + var new_data: Dictionary = { "spawnable": item_hash, "asset": asset_hash } + var _success: bool = _db.insert_row("spawnable_asset_rel", new_data) + + return _success + + +func get_spawnable_assets(item_hash: String) -> Array: + var _query_success = _db.query("SELECT * FROM spawnable_asset_rel WHERE spawnable = '%s'" % item_hash) + var _assets = _db.query_result + return _assets.map(func(entry): return entry.asset) + + +func is_asset_used_by_one_spawnable(asset_hash: String) -> bool: + var _query_success = _db.query("SELECT * FROM spawnable_asset_rel WHERE asset = '%s'" % asset_hash) + + var _spawnables = _db.query_result + return _spawnables.size() == 1 + + +func _database_exists() -> bool: + var _is_open = _db.open_db() + return _is_open + + +func _build_database_schema() -> void: + _db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='spawnable'") + + if _db.query_result.is_empty(): + GlobalLogger.log("The database table does not exist, this probably means the database was not initialized. Adding the tables now.") + + var _spawnable_table: Dictionary = Dictionary() + _spawnable_table = { + "hash": { "data_type": "text", "primary_key": true, "unique": true }, + "name": { "data_type": "text", "not_null": true }, + "thumbnail": { "data_type": "text" }, + "directory": { "data_type": "text", "not_null": true }, + "original_owner": { "data_type": "text" }, + "creation_date": { "data_type": "int", "default": -1 }, + "modified_date": { "data_type": "int", "default": -1 }, + "type": { "data_type": "int", "default": -1 }, + } + + var _asset_table: Dictionary = Dictionary() + _asset_table = { + "hash": { "data_type": "text", "primary_key": true, "unique": true }, + "size": { "data_type": "int", "default": -1 }, + "directory": { "data_type": "text" }, + "type": { "data_type": "int", "default": -1 }, + } + + var _spawnable_asset_rel_table: Dictionary = Dictionary() + _spawnable_asset_rel_table = { + "spawnable": { "data_type": "text" }, + "asset": { "data_type": "text" }, + } + + _db.create_table("spawnable", _spawnable_table) + _db.create_table("asset", _asset_table) + _db.create_table("spawnable_asset_rel", _spawnable_asset_rel_table) + + return + + GlobalLogger.log("[ Database ] Database tables exist.", Enum.LogLevel.DEBUG) + return diff --git a/src/scripts/utils/database.gd.uid b/src/scripts/utils/database.gd.uid new file mode 100644 index 0000000..018b9fa --- /dev/null +++ b/src/scripts/utils/database.gd.uid @@ -0,0 +1 @@ +uid://cbm5uyffux8ca diff --git a/src/scripts/utils/files.gd b/src/scripts/utils/files.gd index 3ec3ac6..e9dc642 100644 --- a/src/scripts/utils/files.gd +++ b/src/scripts/utils/files.gd @@ -6,16 +6,18 @@ # License: MIT License # Authors: Armored Dragon # --- License - extends Node const BASE_SPAWNABLE_DIR = "user://spawnables/" + var spawnables_dir: Array[String] = [] + func _ready() -> void: _initialize_spawnable_folder() return + # Creates a log file following the internal format. # FIXME: Logger library should handle this. func create_log_file() -> String: @@ -31,6 +33,7 @@ func create_log_file() -> String: GlobalLogger.log("Log file '%s' created." % log_file_path) return log_file_path + func create_file(dir: String, file_name: String) -> void: _maybe_make_directory(dir) # TODO: Sanitize name @@ -38,10 +41,55 @@ func create_file(dir: String, file_name: String) -> void: GlobalLogger.log("File '%s' created at '%s'." % [file_name, dir]) file.close() + +func get_inv_filelist() -> Dictionary: + var response = { "files": [], "directories": [] } + + var _files = Database.get_spawnables_by_directory(_current_path()) + + response.files = _files + response.directories = DirAccess.get_directories_at(_current_path()) + + return response + + +func move_inv_deeper(folder: String) -> void: + # Move to a new folder from the current _maybe_make_directory + # TODO: Validate folder exists + spawnables_dir.append(folder) + return + + +func move_inv_relocate(target: int) -> void: + # Relocate the current spawnable file path to a previous position in the path. + spawnables_dir.resize(target) + return + + +func create_folder(folder_name: String = "New Folder") -> void: + # TODO: Sanatize file name + DirAccess.make_dir_recursive_absolute(_current_path() + "/%s" % folder_name) + return + + +func delete_folder(folder_name: String) -> void: + # TODO: Sanatize file name + # TODO: Recursive delete for all files + DirAccess.remove_absolute(_current_path() + "/%s" % folder_name) + return + + +func delete_file(file_path: String) -> void: + # TODO: Validate that we have a file path and not a directory. + DirAccess.remove_absolute(file_path) + return + + func _maybe_make_directory(dir: String): var dir_access = DirAccess.open("user://") dir_access.make_dir_recursive(dir) + func _parse_log_file_name(file_name: String) -> Dictionary: var date = file_name.split(".")[1].split("-") var year = date[0].split("_")[0] @@ -53,11 +101,13 @@ func _parse_log_file_name(file_name: String) -> Dictionary: var time_dictionary = Time.get_datetime_dict_from_datetime_string("%s-%s-%sT%s:%s:%s" % [year, month, day, hour, minute, second], true) return time_dictionary + func _get_today_log_file_name() -> String: var current_timestring = Time.get_datetime_string_from_system() var file_name = current_timestring.replace("-", "_").replace("T", "-").replace(":", "_") return file_name + # TODO: Initialization section to make sure all folders exist. # Spawnables local file management func _initialize_spawnable_folder() -> void: @@ -66,35 +116,10 @@ func _initialize_spawnable_folder() -> void: return -func get_inv_filelist() -> Dictionary: - var response = {"files": [], "directories": []} - - response.files = DirAccess.get_files_at(_current_path()) - response.directories = DirAccess.get_directories_at(_current_path()) - - return response func _current_path() -> String: - return BASE_SPAWNABLE_DIR + "/".join(spawnables_dir) - -func move_inv_deeper(folder: String) -> void: - # Move to a new folder from the current _maybe_make_directory - # TODO: Validate folder exists - spawnables_dir.append(folder) - return - -func move_inv_relocate(target: int) -> void: - # Relocate the current spawnable file path to a previous position in the path. - spawnables_dir.resize(target) - return - -func create_folder(folder_name: String = "New Folder") -> void: - # TODO: Sanatize file name - DirAccess.make_dir_recursive_absolute(_current_path() + "/%s" % folder_name) - return + var _response: String = BASE_SPAWNABLE_DIR + "/".join(spawnables_dir) + if _response.ends_with("/") == false: + _response = _response + "/" -func delete_folder(folder_name: String) -> void: - # TODO: Sanatize file name - # TODO: Recursive delete for all files - DirAccess.remove_absolute(_current_path() + "/%s" % folder_name) - return + return _response diff --git a/src/setup_environment.sh b/src/setup_environment.sh new file mode 100755 index 0000000..226f778 --- /dev/null +++ b/src/setup_environment.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +GODOT_SQL_TAG="v4.8" +GODOT_SQL_URL="https://github.com/2shady4u/godot-sqlite/releases/download/${GODOT_SQL_TAG}/addons.zip" +GODOT_SQL_HASH="f81531b5b9f1d4f422e9f882c409701adf197a17f894ca65ee2fa25759c25270" + +echo "" +echo "This script will download all of the required addons that are not currently present in the repository." +echo "" +sleep 3s + +mkdir -p "setup_downloads" + +echo "Downloading 'godot-sqlite' tagged '${GODOT_SQL_TAG}'" + +# Download Godot-SQLite +if [ ! -f "setup_downloads/godot-sqlite.zip" ]; then + curl -sSL -H "Accept: application/octet-stream" -o "setup_downloads/godot-sqlite.zip" "${GODOT_SQL_URL}" +fi + +# Validate SHA256 +downloaded_godot_sql_hash=$(sha256sum "setup_downloads/godot-sqlite.zip" | awk '{print $1}' ) +if [ $downloaded_godot_sql_hash != $GODOT_SQL_HASH ]; then + echo "Hash check for downloaded 'godot-sqlite' zip failed" + exit +fi + +echo "godot-sqlite hash validated." + +# Extract to addons folder +unzip -q "setup_downloads/godot-sqlite.zip" -d "." + +# Delete setup_downloads folder +# rm -r "setup_downloads" diff --git a/src/userinterface/client_edit_mode/desktop/scripts/inspector.gd b/src/userinterface/client_edit_mode/desktop/scripts/inspector.gd index 859764b..3a74965 100644 --- a/src/userinterface/client_edit_mode/desktop/scripts/inspector.gd +++ b/src/userinterface/client_edit_mode/desktop/scripts/inspector.gd @@ -8,9 +8,24 @@ # --- License extends Control +# Gizmos +var world_gizmos: Array[Dictionary] = [] +var my_gizmo: Node +var _gizmo_mode: int = 0 +var _gizmo_space_local: bool = true +# Inspector data +var _inspector_selected: TreeItem = null +var _inspector_editing: TreeItem = null +var _inspector_focused: TreeItem = null +var _inspector_opened_tree_nodes: Array[Node] = [] +var _inspector_selected_node: Node = null +var _inspector_tree_filters: Array[String] = [] +var _inspector_tree_search: String = "" +var _cem_state: bool = false +var _cem_camera: bool = false + # FIXME: Selecting Gizmo crashes # FIXME: You can select the selected spawnable. (Safe but probably should be changed) - # External Libraries / scripts # FIXME: DevSpawnableManager node @onready var _app_spawnable_file_handling_m: Node = get_tree().current_scene.get_node("SpawnableFileHandling") @@ -20,13 +35,6 @@ extends Control @onready var session_signalbus: Node @onready var session_players_m: Node @onready var session_root: Node3D - -# Gizmos -var world_gizmos: Array[Dictionary] = [] -var my_gizmo: Node -var _gizmo_mode: int = 0 -var _gizmo_space_local: bool = true - # UI Elements @onready var _node_toolbar: Control = get_node("VBoxContainer/Toolbar") @onready var _node_toolbar_gizmo_control_container: Control = _node_toolbar.get_node("MarginContainer/HBoxContainer/GizmoControl") @@ -38,23 +46,10 @@ var _gizmo_space_local: bool = true @onready var _inspector_add_node_window: Node = get_node("AddNodeWindow") @onready var _inspector_filter_search: LineEdit = get_node("VBoxContainer/HBoxContainer/HSplitContainer/MarginContainer/VBoxContainer/Container/VBoxContainer/InputString/MarginContainer/HBoxContainer/MarginContainer/LineEdit") @onready var _inspector_filters: Node = get_node("VBoxContainer/HBoxContainer/HSplitContainer/MarginContainer/VBoxContainer/Container/VBoxContainer/Filters/HBoxContainer") - @onready var _node_property_editor: Node = get_node("VBoxContainer/HBoxContainer/HSplitContainer/Properties") - # UI Inspector @onready var _inspector_tree: Tree = get_node("VBoxContainer/HBoxContainer/HSplitContainer/MarginContainer/VBoxContainer/Container/VBoxContainer/MarginContainer/Tree") -# Inspector data -var _inspector_selected: TreeItem = null -var _inspector_editing: TreeItem = null -var _inspector_focused: TreeItem = null -var _inspector_opened_tree_nodes: Array[Node] = [] -var _inspector_selected_node: Node = null -var _inspector_tree_filters: Array[String] = [] -var _inspector_tree_search: String = "" - -var _cem_state: bool = false -var _cem_camera: bool = false func _ready() -> void: _set_state(false) @@ -74,21 +69,36 @@ func _ready() -> void: _gizmo_set_mode() return -func _add_drag_events() -> void: - _inspector_tree.set_drag_forwarding( - self._get_drag_data, - self._can_drop_data, - self._drop_data - ) - return -func _get_drag_data(mouse_position: Vector2): - var _target_tree_item: TreeItem = _inspector_tree.get_item_at_position(mouse_position) +func _input(event: InputEvent): + # Used to remove focus of the search area when clicking inside of the scene. + if event is InputEventMouseButton && event.pressed: + var focused = get_viewport().gui_get_focus_owner() + if focused: + focused.release_focus() - if _target_tree_item && _target_tree_item.get_parent(): - return {"item": _target_tree_item, "node": _target_tree_item.get_metadata(0)} + # Close the inspector popup + if _inspector_popup.visible == true: + var _rect = _inspector_popup.get_global_rect() + var _mouse_pos = event.position + + if _rect.has_point(_mouse_pos) == false: + _inspector_popup.visible = false + + if event.is_action_pressed("cem_activate"): + _cem_state = !_cem_state + Events.emit_signal("cem_set_state", _cem_state) + + StateManager.update_mouse_state() + + get_viewport().set_input_as_handled() + + if event.is_action_pressed("cem_camera"): + _cem_camera = !_cem_camera + _cem_camera_state(_cem_camera) + + return - return null func _can_drop_data(mouse_position: Vector2, data: Variant): var _target_item: TreeItem = _inspector_tree.get_item_at_position(mouse_position) @@ -110,6 +120,7 @@ func _can_drop_data(mouse_position: Vector2, data: Variant): return true + func _drop_data(mouse_position: Vector2, data: Variant): var _dragged_node: Node = data["node"] var _target_tree_item: TreeItem = _inspector_tree.get_item_at_position(mouse_position) @@ -125,36 +136,25 @@ func _drop_data(mouse_position: Vector2, data: Variant): _inspector_build() -func _input(event: InputEvent): - # Used to remove focus of the search area when clicking inside of the scene. - if event is InputEventMouseButton && event.pressed: - var focused = get_viewport().gui_get_focus_owner() - if focused: - focused.release_focus() - - # Close the inspector popup - if _inspector_popup.visible == true: - var _rect = _inspector_popup.get_global_rect() - var _mouse_pos = event.position - - if _rect.has_point(_mouse_pos) == false: - _inspector_popup.visible = false +func _get_drag_data(mouse_position: Vector2): + var _target_tree_item: TreeItem = _inspector_tree.get_item_at_position(mouse_position) - if event.is_action_pressed("cem_activate"): - _cem_state = !_cem_state - Events.emit_signal("cem_set_state", _cem_state) + if _target_tree_item && _target_tree_item.get_parent(): + return { "item": _target_tree_item, "node": _target_tree_item.get_metadata(0) } - StateManager.update_mouse_state() + return null - get_viewport().set_input_as_handled() - - if event.is_action_pressed("cem_camera"): - _cem_camera = !_cem_camera - _cem_camera_state(_cem_camera) +func _add_drag_events() -> void: + _inspector_tree.set_drag_forwarding( + self._get_drag_data, + self._can_drop_data, + self._drop_data, + ) return + # Signals and other event listeners func _add_signalbus_event_listeners() -> void: GlobalLogger.log("Adding signal bus event listeners to the inspector.") @@ -163,6 +163,7 @@ func _add_signalbus_event_listeners() -> void: # TODO: Events.cem_set_gizmo_state.connect(_toggle_gizmos) return + func _add_gizmo_event_listeners() -> void: var _node_children: Array[Node] = _node_toolbar_gizmo_control_container.get_children() var _misc_node_children: Array[Node] = _node_toolbar_gizmo_control_container_misc.get_children() @@ -174,6 +175,7 @@ func _add_gizmo_event_listeners() -> void: _misc_node_children[0].get_node("Button").pressed.connect(_gizmo_set_mode) return + func _add_button_event_listeners() -> void: _inspector_popup.entry_clicked.connect(_inspector_popup_button_pressed) _inspector_tree.item_mouse_selected.connect(_inspector_tree_item_mouse_selected) @@ -183,18 +185,20 @@ func _add_button_event_listeners() -> void: # When clicking on a filter button, apply the filter immediately. for _filter_button in _inspector_filters.get_children(): _filter_button.get_node("Button").pressed.connect( - func (): + func(): _inspector_get_filters() _inspector_build() ) return + func _add_misc_event_listeners() -> void: _inspector_add_node_window.selection.connect(_inspector_spawn_node) _inspector_tree.item_edited.connect(_inspector_item_edited) return + func _inspector_tree_item_mouse_selected(mouse_position: Vector2, mouse_button_index: int) -> void: if mouse_button_index == MOUSE_BUTTON_LEFT: _inspector_selected = _inspector_tree.get_item_at_position(mouse_position) @@ -222,6 +226,7 @@ func _inspector_tree_item_mouse_selected(mouse_position: Vector2, mouse_button_i _inspector_popup.visible = true return + func _set_state(state: bool) -> void: GlobalLogger.log("Inspector state is being set to '%s'." % state) _gizmo_visibility(state) @@ -229,12 +234,14 @@ func _set_state(state: bool) -> void: _node_crosshair.visible = !state return + func _on_node_created(_node: Node) -> void: # TODO: Instead of rebuilding the entire tree, just update the respective entry. GlobalLogger.log("Inspector: Node created.") _inspector_build() return + func _on_node_destroyed(node_entry: Dictionary) -> void: # TODO: Instead of rebuilding the entire tree, just update the respective entry. GlobalLogger.log("Inspector: Node destroyed.") @@ -246,6 +253,7 @@ func _on_node_destroyed(node_entry: Dictionary) -> void: _inspector_build() return + func _on_session_changed(_session_id: String) -> void: session_signalbus = app_scene_m.get_master_scene(app_scene_m.active_session).get_node("SignalBus") session_spawnable_m = app_scene_m.get_master_scene(app_scene_m.active_session).get_node("SpawnableManager") @@ -261,6 +269,7 @@ func _on_session_changed(_session_id: String) -> void: session_signalbus.node_destroyed.connect(_on_node_destroyed) return + # User Interface func _inspector_build(root_node: Node = session_root) -> void: # TODO: Proper wait until building the inspector. @@ -284,6 +293,7 @@ func _inspector_build(root_node: Node = session_root) -> void: return + func _inspector_save_openness() -> void: # Parse the entire tree, and save node values that are open _inspector_opened_tree_nodes = [] @@ -304,6 +314,7 @@ func _inspector_save_openness() -> void: return + func _inspector_add_node(node: Node, parent: TreeItem) -> void: if node.get_meta("scene_node", false) == false: return @@ -325,6 +336,7 @@ func _inspector_add_node(node: Node, parent: TreeItem) -> void: _inspector_add_node(child, _tree_node) return + # Inspector filters func _inspector_apply_filters(): var _all_tree_items: Array[TreeItem] = _inspector_get_all_tree_entries(_inspector_tree) @@ -373,6 +385,7 @@ func _inspector_apply_filters(): _tree_item.set_visible(_is_visible) return + func _inspector_get_filters(): _inspector_tree_filters = [] @@ -382,11 +395,13 @@ func _inspector_get_filters(): return + func _inspector_search_changed(text) -> void: _inspector_tree_search = text _inspector_build() return + func _inspector_get_all_tree_entries(tree: Tree) -> Array[TreeItem]: var _tree_items: Array[TreeItem] = [] var _tree_current = tree.get_root() @@ -398,6 +413,7 @@ func _inspector_get_all_tree_entries(tree: Tree) -> Array[TreeItem]: return _tree_items + func _inspector_get_all_tree_ancestors(starting_item: TreeItem) -> Array[TreeItem]: var _tree_ancestors: Array[TreeItem] = [] var _tree_item_parent: TreeItem = starting_item.get_parent() @@ -413,16 +429,19 @@ func _inspector_get_all_tree_ancestors(starting_item: TreeItem) -> Array[TreeIte return _tree_ancestors + # Inspector events func _inspector_item_edited() -> void: _inspector_editing.set_editable(0, false) _inspector_editing.get_metadata(0).set_meta("pretty_name", _inspector_editing.get_text(0)) + func _inspector_clear() -> void: GlobalLogger.log("Clearing the inspector.") _inspector_tree.clear() return + func _inspector_popup_button_pressed(label: String) -> void: GlobalLogger.log("Pressed button: '%s' on '%s'" % [label, _inspector_selected.get_text(0)]) _inspector_popup.visible = false @@ -445,11 +464,12 @@ func _inspector_popup_button_pressed(label: String) -> void: _inspector_editing = _inspector_selected "Save": GlobalLogger.log("Saving spawnable.") - _app_spawnable_file_handling_m.save_spawnable(_node) + _app_spawnable_file_handling_m.save_spawnable(_node, Enum.SpawnableType.ITEM) _: GlobalLogger.log("Unhandled inspector popup selection", Enum.LogLevel.WARNING) return + func _inspector_spawn_node(type: String, parent: int) -> void: GlobalLogger.log("Spawning node type '%s' with parent '%s'" % [type, parent]) var _entity = await session_spawnable_m.create(type, parent) @@ -457,13 +477,14 @@ func _inspector_spawn_node(type: String, parent: int) -> void: # TODO: Gizmo select the new entity return + func _select(target_node: int) -> void: var _node_db_entry = session_spawnable_m.get_by_id(target_node) if my_gizmo != null: _gizmo_delete() - if _node_db_entry == {}: + if _node_db_entry == { }: GlobalLogger.log("Tried to select a node that should not exist!", Enum.LogLevel.ERROR) _inspector_selected_node = null @@ -481,11 +502,13 @@ func _select(target_node: int) -> void: # HACK: Update ALL of the node properties after a transformation was done. my_gizmo.transform_end.connect(func(_mode): _node_property_editor.update_node_properties(_node_db_entry.node)) + func _gizmo_delete() -> void: my_gizmo.clear_selection() await session_spawnable_m.destroy(int(my_gizmo.name)) return + func _gizmo_set_mode() -> void: var _gizmo_modes: Dictionary var _node_children: Array[Node] = _node_toolbar_gizmo_control_container.get_children() @@ -514,6 +537,7 @@ func _gizmo_set_mode() -> void: return + func _gizmo_visibility(to_set_visible: bool = false) -> void: var _gizmo_target_mode = 0 @@ -529,6 +553,7 @@ func _gizmo_visibility(to_set_visible: bool = false) -> void: gizmo.mode = _gizmo_target_mode return + # CEM Camera func _cem_camera_state(state: bool) -> void: var _active_session_id = app_scene_m.active_session @@ -548,6 +573,7 @@ func _cem_camera_state(state: bool) -> void: Events.emit_signal("cem_camera_state", state) return + # Node Property Editor func _set_npe_state(state: bool) -> void: _node_property_editor.visible = state diff --git a/src/userinterface/client_edit_mode/desktop/scripts/inspector.gd.uid b/src/userinterface/client_edit_mode/desktop/scripts/inspector.gd.uid index 7610cb6..8595bd5 100644 --- a/src/userinterface/client_edit_mode/desktop/scripts/inspector.gd.uid +++ b/src/userinterface/client_edit_mode/desktop/scripts/inspector.gd.uid @@ -1 +1 @@ -uid://dlituslp07gvw +uid://wwky3vsyqilq diff --git a/src/userinterface/client_edit_mode/desktop/scripts/moveable_window.gd b/src/userinterface/client_edit_mode/desktop/scripts/moveable_window.gd index 39031cb..6b2bc8c 100644 --- a/src/userinterface/client_edit_mode/desktop/scripts/moveable_window.gd +++ b/src/userinterface/client_edit_mode/desktop/scripts/moveable_window.gd @@ -9,27 +9,39 @@ extends Control signal selection(type: String, parent: int) +signal closed() @export var window_name: String = "Window Name" +var _is_dragging: bool = false +var _drag_offset: Vector2 + @onready var _button_close = get_node("BackgroundColor/MarginContainer/VBoxContainer/HBoxContainer/GenericButton") @onready var _node_window_title = get_node("BackgroundColor/MarginContainer/VBoxContainer/HBoxContainer/Label") -var _is_dragging: bool = false -var _drag_offset: Vector2 func _ready() -> void: _node_window_title.text = window_name _button_close.clicked.connect(_close) - gui_input.connect(_on_input) gui_input.connect(_on_input) return + func _close() -> void: visible = false + closed.emit() return + +func _open() -> void: + var _half_window_size: Vector2 = DisplayServer.window_get_size() / 2 + var _target_pos: Vector2 = _half_window_size - (size / 2) + + global_position = _target_pos + return + + func _on_input(event) -> void: if event is InputEventMouseButton: _mouse_click(event) @@ -41,6 +53,7 @@ func _on_input(event) -> void: return + func _mouse_drag(_event) -> void: if _is_dragging: var _game_size: Vector2 = DisplayServer.window_get_size() @@ -51,6 +64,7 @@ func _mouse_drag(_event) -> void: global_position = _target_position return + func _mouse_click(event) -> void: if event.button_index != MOUSE_BUTTON_LEFT: return diff --git a/src/userinterface/client_edit_mode/desktop/scripts/moveable_window.gd.uid b/src/userinterface/client_edit_mode/desktop/scripts/moveable_window.gd.uid index 4f55547..3115a45 100644 --- a/src/userinterface/client_edit_mode/desktop/scripts/moveable_window.gd.uid +++ b/src/userinterface/client_edit_mode/desktop/scripts/moveable_window.gd.uid @@ -1 +1 @@ -uid://bapubedba8eiu +uid://fpegx2jfo73q diff --git a/src/userinterface/dash/dashboard.tscn b/src/userinterface/dash/dashboard.tscn index 0d71779..06fb659 100644 --- a/src/userinterface/dash/dashboard.tscn +++ b/src/userinterface/dash/dashboard.tscn @@ -35,6 +35,7 @@ [ext_resource type="Script" uid="uid://cah3jpmu1x72b" path="res://userinterface/dash/scripts/pages/exit.gd" id="24_wvg7d"] [ext_resource type="PackedScene" uid="uid://b6qb2oq26yooy" path="res://userinterface/client_edit_mode/desktop/partial/movable_window.tscn" id="32_rmxmm"] [ext_resource type="Texture2D" uid="uid://bb6karp2363a5" path="res://resources/icons/at-icons/rocketship.svg" id="33_h0jai"] +[ext_resource type="Script" uid="uid://c2ljo75xjxkm6" path="res://userinterface/dash/scripts/new_world_dialog.gd" id="35_owuc4"] [sub_resource type="ShaderMaterial" id="ShaderMaterial_v7inj"] shader = ExtResource("1_co0dt") @@ -878,7 +879,9 @@ selected_theme_type = "Danger" [node name="NewWorld" parent="." unique_id=524781090 instance=ExtResource("32_rmxmm")] visible = false layout_mode = 1 -window_name = "Create New World" +script = ExtResource("35_owuc4") +target_scene_path = "" +base = 3 [node name="VBoxContainer" type="VBoxContainer" parent="NewWorld/BackgroundColor/MarginContainer/VBoxContainer" parent_id_path=PackedInt32Array(524781090, 969090938) index="1" unique_id=1889766629] layout_mode = 2 @@ -889,18 +892,32 @@ layout_mode = 2 text = "World Name" [node name="WorldNameVal" type="LineEdit" parent="NewWorld/BackgroundColor/MarginContainer/VBoxContainer/VBoxContainer" unique_id=2124841622] +unique_name_in_owner = true layout_mode = 2 placeholder_text = "My New World" alignment = 1 +[node name="WorldLoadDir" type="Label" parent="NewWorld/BackgroundColor/MarginContainer/VBoxContainer/VBoxContainer" unique_id=166016776] +unique_name_in_owner = true +layout_mode = 2 +text = "Base World Directory" + +[node name="WorldLoadDirVal" type="LineEdit" parent="NewWorld/BackgroundColor/MarginContainer/VBoxContainer/VBoxContainer" unique_id=863656716] +unique_name_in_owner = true +layout_mode = 2 +placeholder_text = "user://spawnables/..." +alignment = 1 +editable = false + [node name="Base" type="Label" parent="NewWorld/BackgroundColor/MarginContainer/VBoxContainer/VBoxContainer" unique_id=413752113] layout_mode = 2 text = "Base" [node name="BaseVal" type="OptionButton" parent="NewWorld/BackgroundColor/MarginContainer/VBoxContainer/VBoxContainer" unique_id=2057051427] +unique_name_in_owner = true layout_mode = 2 selected = 3 -item_count = 4 +item_count = 5 popup/item_0/text = "Debug" popup/item_0/id = 0 popup/item_0/disabled = true @@ -912,12 +929,18 @@ popup/item_2/id = 2 popup/item_2/disabled = true popup/item_3/text = "Grid" popup/item_3/id = 3 +popup/item_4/text = "Custom" +popup/item_4/id = 4 +popup/item_4/disabled = true [node name="Privacy" type="Label" parent="NewWorld/BackgroundColor/MarginContainer/VBoxContainer/VBoxContainer" unique_id=1738386897] +visible = false layout_mode = 2 text = "Privacy" [node name="PrivacyVal" type="OptionButton" parent="NewWorld/BackgroundColor/MarginContainer/VBoxContainer/VBoxContainer" unique_id=1867545901] +unique_name_in_owner = true +visible = false layout_mode = 2 selected = 0 item_count = 6 diff --git a/src/userinterface/dash/scripts/new_world_dialog.gd b/src/userinterface/dash/scripts/new_world_dialog.gd new file mode 100644 index 0000000..1084c5c --- /dev/null +++ b/src/userinterface/dash/scripts/new_world_dialog.gd @@ -0,0 +1,69 @@ +# --- License +# File: /client/src/userinterface/dash/scripts/new_world_dialog.gd +# Project: OpenMinerva +# Created Date: 06 August 2026 +# Copyright (c) 2026 OpenMinerva +# License: MIT License +# Authors: Armored Dragon +# --- License +extends "res://userinterface/client_edit_mode/desktop/scripts/moveable_window.gd" + +@export var target_scene_path: String = "" +@export var base: Enum.BaseLevel = Enum.BaseLevel.GRID + +@onready var network_m = get_tree().current_scene.get_node("NetworkManager") +@onready var scene_m = get_tree().current_scene.get_node("SceneManager") +@onready var dashboard = get_tree().current_scene.get_node("Dashboard") +@onready var _create_world_button: Control = get_node("%Create") +@onready var _base_val_ui: Control = get_node("%BaseVal") +@onready var _world_load_dir_ui: Control = get_node("%WorldLoadDir") +@onready var _world_load_dir_val_ui: Control = get_node("%WorldLoadDirVal") + + +func _ready() -> void: + super._ready() + closed.connect(func(): close_window(false)) + _create_world_button.clicked.connect(start_world) + _base_val_ui.item_selected.connect(func(_index): update_ui()) + update_ui() + return + + +func show_window() -> void: + update_ui() + super._open() + visible = true + return + + +func close_window(call_super: bool = true) -> void: + if call_super == true: + super._close() + + _base_val_ui.selected = Enum.BaseLevel.GRID + base = Enum.BaseLevel.GRID + return + + +func update_ui() -> void: + base = _base_val_ui.selected + + _world_load_dir_val_ui.visible = base == Enum.BaseLevel.CUSTOM + _world_load_dir_ui.visible = base == Enum.BaseLevel.CUSTOM + return + + +func start_world() -> void: + var _world_name: String = dashboard.get_node("%WorldNameVal").text + var _world_base: Enum.BaseLevel = dashboard.get_node("%BaseVal").selected + var _world_privacy: Enum.PrivacyLevel = dashboard.get_node("%PrivacyVal").selected + var _world_load_dir: String = dashboard.get_node("%WorldLoadDirVal").text + + network_m.start_server(0, _world_base, _world_load_dir) + + # HACK: For some reason, the active session does not get updated when I expect it to. This will force the active session to switch to the newly created session. + var _sessions: Array = network_m.get_connected_sessions() + scene_m.set_active_session(_sessions.back().name) + + close_window() + return diff --git a/src/userinterface/dash/scripts/new_world_dialog.gd.uid b/src/userinterface/dash/scripts/new_world_dialog.gd.uid new file mode 100644 index 0000000..9d86cea --- /dev/null +++ b/src/userinterface/dash/scripts/new_world_dialog.gd.uid @@ -0,0 +1 @@ +uid://c2ljo75xjxkm6 diff --git a/src/userinterface/dash/scripts/pages/home.gd b/src/userinterface/dash/scripts/pages/home.gd index 50a5306..4ea021e 100644 --- a/src/userinterface/dash/scripts/pages/home.gd +++ b/src/userinterface/dash/scripts/pages/home.gd @@ -20,8 +20,7 @@ func _ready() -> void: Events.connect("session_left", _show_joined_sessions) _show_joined_sessions() - get_node("%CreateNewWorld").clicked.connect(_on_create_new_world_button_pressed) - dashboard.get_node("NewWorld").get_node("%Create").clicked.connect(_on_start_world) + get_node("%CreateNewWorld").clicked.connect(func(): dashboard.get_node("NewWorld").show_window()) return @@ -44,22 +43,3 @@ func _show_joined_sessions() -> void: _entry_close.pressed.connect(network_m.leave_server.bind(session.id)) active_sessions_container.add_child(_entry) return - - -func _on_create_new_world_button_pressed() -> void: - dashboard.get_node("NewWorld").visible = true - return - - -func _on_start_world() -> void: - var _world_name: String = dashboard.get_node("NewWorld/BackgroundColor/MarginContainer/VBoxContainer/VBoxContainer/WorldNameVal").text - var _world_base: Enum.BaseLevel = dashboard.get_node("NewWorld/BackgroundColor/MarginContainer/VBoxContainer/VBoxContainer/BaseVal").selected - var _world_privacy: Enum.PrivacyLevel = dashboard.get_node("NewWorld/BackgroundColor/MarginContainer/VBoxContainer/VBoxContainer/PrivacyVal").selected - - network_m.start_server(0, _world_base) - _show_joined_sessions() - - # HACK: For some reason, the active session does not get updated when I expect it to. This will force the active session to switch to the newly created session. - var _sessions: Array = network_m.get_connected_sessions() - scene_m.set_active_session(_sessions.back().name) - return diff --git a/src/userinterface/dash/scripts/pages/instance.gd b/src/userinterface/dash/scripts/pages/instance.gd index 0727d03..46b20c4 100644 --- a/src/userinterface/dash/scripts/pages/instance.gd +++ b/src/userinterface/dash/scripts/pages/instance.gd @@ -57,5 +57,5 @@ func _save_world() -> void: var _root_node: Node3D = scene_m.get_master_root(scene_m.active_session) var _name: String = _instance_name.get_value() - _app_spawnable_file_handling.save_spawnable(_root_node, _name) + _app_spawnable_file_handling.save_spawnable(_root_node, Enum.SpawnableType.WORLD, _name) return diff --git a/src/userinterface/dash/scripts/pages/spawnables.gd b/src/userinterface/dash/scripts/pages/spawnables.gd index 2ea3819..e645e57 100644 --- a/src/userinterface/dash/scripts/pages/spawnables.gd +++ b/src/userinterface/dash/scripts/pages/spawnables.gd @@ -6,7 +6,6 @@ # License: MIT License # Authors: Armored Dragon # --- License - extends "res://userinterface/dash/scripts/pages/left_nav_container.gd" const BASE_DIR = "user://inventory/" @@ -20,12 +19,12 @@ var _selected_folder: Node @onready var _path_container = $"HBox/Right/Local/Path" @onready var _folder_container = $"HBox/Right/Local/Folders" @onready var _file_container = $"HBox/Right/Local/Files" - @onready var _create_folder_btn = $"HBox/Right/Local/Actions/NewFolder/Button" @onready var _delete_selected_btn = $"HBox/Right/Local/Actions/DeleteSelected/Button" - @onready var _folder_creation_dialog = $"FolderCreation" @onready var _deletion_dialog = $"DeleteConfirmation" +@onready var dashboard = get_tree().current_scene.get_node("Dashboard") + func _ready(): super._ready() @@ -41,12 +40,14 @@ func _ready(): _build_view() return + func _handle_page_opened(page_name) -> void: if page_name != "Spawnables": return return + func _build_view() -> void: var _filelist = FileManager.get_inv_filelist() var _files = _filelist["files"] @@ -55,13 +56,24 @@ func _build_view() -> void: await _clear_view() for file in _files: - var _file_name = file.get_basename() - var _listing = _create_button(_file_name, "", _load_file.bind(file)) + var _file_pretty_name = file.name + var _file_name: String = file.hash + var _listing + if file.type == Enum.SpawnableType.WORLD: + _listing = _create_button(_file_pretty_name, "", _create_world.bind(_file_name + ".tscn")) + else: + _listing = _create_button(_file_pretty_name, "", _load_file.bind(_file_name + ".tscn")) + + _listing.set_meta("type", "spawnable") + _listing.set_meta("file_name", _file_name) _file_container.add_child(_listing) continue for folder in _dirs: var _listing = _create_button(folder, "Folder", _dir_deeper.bind(folder)) + + _listing.set_meta("type", "folder") + _folder_container.add_child(_listing) continue @@ -88,21 +100,38 @@ func _build_view() -> void: continue return + func _dir_deeper(path: String) -> void: FileManager.move_inv_deeper(path) _build_view() return + func _dir_relocate(index: int) -> void: FileManager.move_inv_relocate(index + 1) _build_view() return + func _load_file(file_name: String) -> void: - var _path = FileManager._current_path() + "/" + file_name + var _path = FileManager._current_path() + file_name _app_spawnable_file_handling_m.load_spawnable(_path) return + +func _create_world(file_name: String) -> void: + var _dialog: Control = dashboard.get_node("NewWorld") + var _world_load_dir_val: Control = _dialog.get_node("%WorldLoadDirVal") + var _base_val: Control = _dialog.get_node("%BaseVal") + var _file_path = FileManager._current_path() + file_name + + _world_load_dir_val.text = _file_path + _base_val.selected = Enum.BaseLevel.CUSTOM + + _dialog.show_window() + return + + func _create_button(btn_name: String, icon: String = "", double_click = null, min_size: Vector2 = Vector2(350, 50)) -> Node: var _listing = _template_button.instantiate() _listing.set_meta("label", btn_name) @@ -119,6 +148,7 @@ func _create_button(btn_name: String, icon: String = "", double_click = null, mi _action_buttons.append(_listing) return _listing + func _button_selected(selected: Node) -> void: var _button_name = selected.get_meta("label") @@ -130,6 +160,7 @@ func _button_selected(selected: Node) -> void: _selected_folder = selected return + func _clear_view() -> void: _action_buttons = [] _selected_folder = null @@ -147,10 +178,12 @@ func _clear_view() -> void: return + func _open_file_dialog() -> void: _folder_creation_dialog.show() return + func _create_folder() -> void: var _folder_name = _folder_creation_dialog.get_node("LineEdit").text GlobalLogger.log("Creating folder '%s'" % _folder_name) @@ -159,6 +192,7 @@ func _create_folder() -> void: _build_view() return + func _open_delete_dialog() -> void: if _selected_folder == null: GlobalLogger.log("Tried to open the delete dialog without anything selected.", Enum.LogLevel.WARNING) @@ -172,9 +206,25 @@ func _open_delete_dialog() -> void: _deletion_dialog.show() return + func _delete_selected() -> void: var _folder_name = _selected_folder.get_meta("label") + var _type: String = _selected_folder.get_meta("type") GlobalLogger.log("Deleting Item '%s'" % _folder_name) - FileManager.delete_folder(_folder_name) + + if _type == "spawnable": + var _file_name: String = _selected_folder.get_meta("file_name") + var _db_entry: Dictionary = Database.get_spawnable(_file_name) + + if _db_entry.has("directory") == false: + GlobalLogger.log("Tried to delete file '%s' but the directory could not be found!" % _file_name, Enum.LogLevel.WARNING) + return + + FileManager.delete_file(_db_entry.directory) + Database.delete_spawnable(_db_entry.hash) + + if _type == "folder": + FileManager.delete_folder(_folder_name) + _build_view() return