From ad2f0702877be6ba8c91e114c8f6335f10a746ae Mon Sep 17 00:00:00 2001 From: Armored Dragon Date: Wed, 5 Aug 2026 20:07:13 -0500 Subject: [PATCH 01/10] Added the godot-SQLite addon. Added a basic SQLite database for the application. Assets are added to the database. Assets now properly hash their file contents before going onto the disk. Minor benchmarking notes. Note: The godot-SQLite addon is not in the repository as I look for a good way to include this addon without actually commiting heavy binaries to the repository. (Version 4.8 of the addon was used at this time.) --- .gitignore | 1 + src/project.godot | 3 +- src/scripts/spawnable_file_handling.gd | 29 +++++-- src/scripts/utils/database.gd | 101 +++++++++++++++++++++++++ src/scripts/utils/database.gd.uid | 1 + 5 files changed, 129 insertions(+), 6 deletions(-) create mode 100644 src/scripts/utils/database.gd create mode 100644 src/scripts/utils/database.gd.uid diff --git a/.gitignore b/.gitignore index 47600759..b43acb88 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /src/.godot /src/android/ +/src/addons/godot-sqlite diff --git a/src/project.godot b/src/project.godot index f1dfe330..8f1e0ced 100644 --- a/src/project.godot +++ b/src/project.godot @@ -40,6 +40,7 @@ Events="*uid://c656spc3ppdlw" StateManager="*uid://b5vvw77bmn4yc" OAuth2Client="*uid://c4b880pwgfrty" UrlParser="*uid://budprjmmpally" +Database="*uid://cpkrdq8no0g48" [debug] @@ -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/scripts/spawnable_file_handling.gd b/src/scripts/spawnable_file_handling.gd index 11ac60ae..ab63e1a5 100644 --- a/src/scripts/spawnable_file_handling.gd +++ b/src/scripts/spawnable_file_handling.gd @@ -156,19 +156,38 @@ func _flatten_resource(resource: Resource): func _externalize_assets(root: Node) -> 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 - var external_resource = load(external_path) - external_resource.take_over_path(external_path) + ResourceSaver.save(value, _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) + for child in root.get_children(): _externalize_assets(child) diff --git a/src/scripts/utils/database.gd b/src/scripts/utils/database.gd new file mode 100644 index 00000000..7a6c4088 --- /dev/null +++ b/src/scripts/utils/database.gd @@ -0,0 +1,101 @@ +# --- 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(id: int) -> Dictionary: + return { } + + +func set_spawnable(id: int, new_data: Dictionary) -> Dictionary: + return { } + + +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(hash: String, new_data: Dictionary) -> bool: + # TODO: Validation before inserting into the database. + GlobalLogger.log("[ Database ] Updating asset '%s'." % hash, Enum.LogLevel.DEBUG) + var _success: bool = _db.insert_row("asset", new_data) + return _success + + +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 = { + "id": { "data_type": "int", "primary_key": true, "unique": true }, + "name": { "data_type": "text", "not_null": true }, + "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 }, + } + + _db.create_table("spawnable", _spawnable_table) + _db.create_table("asset", _asset_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 00000000..018b9faf --- /dev/null +++ b/src/scripts/utils/database.gd.uid @@ -0,0 +1 @@ +uid://cbm5uyffux8ca From 44b9dd5e0728656e52fd7b3f7a136656b18a62af Mon Sep 17 00:00:00 2001 From: Armored Dragon Date: Wed, 5 Aug 2026 20:46:52 -0500 Subject: [PATCH 02/10] Save spawnables. Adds basic support for adding spawnables to the database. --- src/scripts/enum.gd | 7 +- src/scripts/spawnable_file_handling.gd | 20 ++- src/scripts/spawnable_file_handling.gd.uid | 2 +- src/scripts/utils/database.gd | 12 +- .../desktop/scripts/inspector.gd | 144 +++++++++++------- .../desktop/scripts/inspector.gd.uid | 1 - .../dash/scripts/pages/instance.gd | 2 +- 7 files changed, 113 insertions(+), 75 deletions(-) delete mode 100644 src/userinterface/client_edit_mode/desktop/scripts/inspector.gd.uid diff --git a/src/scripts/enum.gd b/src/scripts/enum.gd index 5ac1a376..6afd1e3d 100644 --- a/src/scripts/enum.gd +++ b/src/scripts/enum.gd @@ -35,11 +35,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 ab63e1a5..2f63e184 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: @@ -39,13 +40,26 @@ func save_spawnable(root: Node, name_override: String = "") -> void: # Externalize assets makes it so that networking things are consistent. _externalize_assets(root) - # Remove PlayerControllers. + # Remove nodes that should not be saved. _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() + 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, + "original_owner": "", + "creation_date": -1, + "modified_date": -1, + "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) diff --git a/src/scripts/spawnable_file_handling.gd.uid b/src/scripts/spawnable_file_handling.gd.uid index e52df7d4..9a6fae39 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 index 7a6c4088..5188c828 100644 --- a/src/scripts/utils/database.gd +++ b/src/scripts/utils/database.gd @@ -39,8 +39,10 @@ func get_spawnable(id: int) -> Dictionary: return { } -func set_spawnable(id: int, new_data: Dictionary) -> Dictionary: - return { } +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 get_asset(hash: String) -> Dictionary: @@ -55,9 +57,9 @@ func get_asset(hash: String) -> Dictionary: return _db.query_result[0] -func set_asset(hash: String, new_data: Dictionary) -> bool: +func set_asset(item_hash: String, new_data: Dictionary) -> bool: # TODO: Validation before inserting into the database. - GlobalLogger.log("[ Database ] Updating asset '%s'." % hash, Enum.LogLevel.DEBUG) + GlobalLogger.log("[ Database ] Updating asset '%s'." % item_hash, Enum.LogLevel.DEBUG) var _success: bool = _db.insert_row("asset", new_data) return _success @@ -75,7 +77,7 @@ func _build_database_schema() -> void: var _spawnable_table: Dictionary = Dictionary() _spawnable_table = { - "id": { "data_type": "int", "primary_key": true, "unique": true }, + "hash": { "data_type": "text", "primary_key": true, "unique": true }, "name": { "data_type": "text", "not_null": true }, "directory": { "data_type": "text", "not_null": true }, "original_owner": { "data_type": "text" }, diff --git a/src/userinterface/client_edit_mode/desktop/scripts/inspector.gd b/src/userinterface/client_edit_mode/desktop/scripts/inspector.gd index 859764bc..3a74965c 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 deleted file mode 100644 index 7610cb6b..00000000 --- a/src/userinterface/client_edit_mode/desktop/scripts/inspector.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dlituslp07gvw diff --git a/src/userinterface/dash/scripts/pages/instance.gd b/src/userinterface/dash/scripts/pages/instance.gd index 0727d034..46b20c48 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 From d297d1fb0386365cd86d309df37bea6ca3c23767 Mon Sep 17 00:00:00 2001 From: Armored Dragon Date: Wed, 5 Aug 2026 21:47:40 -0500 Subject: [PATCH 03/10] Spawnable dashboard page uses database to get filelist. Added thumbnail column to the spawnable database. Set the creation date when saving a spawnable. --- src/scripts/spawnable_file_handling.gd | 4 +- src/scripts/utils/database.gd | 9 +++ src/scripts/utils/files.gd | 79 ++++++++++++++++---------- 3 files changed, 59 insertions(+), 33 deletions(-) diff --git a/src/scripts/spawnable_file_handling.gd b/src/scripts/spawnable_file_handling.gd index 2f63e184..d0c5a1ae 100644 --- a/src/scripts/spawnable_file_handling.gd +++ b/src/scripts/spawnable_file_handling.gd @@ -53,9 +53,7 @@ func save_spawnable(root: Node, type: Enum.SpawnableType = Enum.SpawnableType.IT "hash": _data_hash, "name": _spawnable_name, "directory": file_path, - "original_owner": "", - "creation_date": -1, - "modified_date": -1, + "creation_date": int(Time.get_unix_time_from_system()), "type": type, } Database.set_spawnable(_data_hash, _database_row) diff --git a/src/scripts/utils/database.gd b/src/scripts/utils/database.gd index 5188c828..00258d1a 100644 --- a/src/scripts/utils/database.gd +++ b/src/scripts/utils/database.gd @@ -57,6 +57,14 @@ func get_asset(hash: String) -> Dictionary: 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_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) @@ -79,6 +87,7 @@ func _build_database_schema() -> void: _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 }, diff --git a/src/scripts/utils/files.gd b/src/scripts/utils/files.gd index 3ec3ac6b..7239ee84 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,49 @@ 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.map(func(entry): return entry.name) + 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 _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 +95,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 +110,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 + var _response: String = BASE_SPAWNABLE_DIR + "/".join(spawnables_dir) + if _response.ends_with("/") == false: + _response = _response + "/" -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 + return _response From 7c4644df4d63c2a3961aec6f74b49b494b42cb0a Mon Sep 17 00:00:00 2001 From: Armored Dragon Date: Wed, 5 Aug 2026 21:53:53 -0500 Subject: [PATCH 04/10] Fixed loading spawnables. --- src/scripts/utils/files.gd | 2 +- .../dash/scripts/pages/spawnables.gd | 23 ++++++++++++++----- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/scripts/utils/files.gd b/src/scripts/utils/files.gd index 7239ee84..38962672 100644 --- a/src/scripts/utils/files.gd +++ b/src/scripts/utils/files.gd @@ -47,7 +47,7 @@ func get_inv_filelist() -> Dictionary: var _files = Database.get_spawnables_by_directory(_current_path()) - response.files = _files.map(func(entry): return entry.name) + response.files = _files response.directories = DirAccess.get_directories_at(_current_path()) return response diff --git a/src/userinterface/dash/scripts/pages/spawnables.gd b/src/userinterface/dash/scripts/pages/spawnables.gd index 2ea38191..c1ff9eef 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,13 +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" + func _ready(): super._ready() @@ -41,12 +39,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,8 +55,9 @@ 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 = _create_button(_file_pretty_name, "", _load_file.bind(_file_name + ".tscn")) _file_container.add_child(_listing) continue @@ -88,21 +89,25 @@ 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_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 +124,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 +136,7 @@ func _button_selected(selected: Node) -> void: _selected_folder = selected return + func _clear_view() -> void: _action_buttons = [] _selected_folder = null @@ -147,10 +154,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 +168,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,6 +182,7 @@ func _open_delete_dialog() -> void: _deletion_dialog.show() return + func _delete_selected() -> void: var _folder_name = _selected_folder.get_meta("label") GlobalLogger.log("Deleting Item '%s'" % _folder_name) From 88be303434e3acf7f0232eff7012bc97decf27b1 Mon Sep 17 00:00:00 2001 From: Armored Dragon Date: Thu, 6 Aug 2026 03:17:10 -0500 Subject: [PATCH 05/10] Basic world loading from spawnable menu. When double clicking on the world in the spawnables menu, you are now prompted to create a new world with the selected world as the base. --- src/project.godot | 2 +- src/scenes/managers/app/network.gd | 7 ++----- src/scenes/managers/app/scene.gd | 7 +++++-- src/scenes/managers/app/scene.gd.uid | 2 +- .../desktop/scripts/inspector.gd.uid | 1 + src/userinterface/dash/dashboard.tscn | 10 ++++++++++ src/userinterface/dash/scripts/pages/home.gd | 4 ++-- .../dash/scripts/pages/spawnables.gd | 16 +++++++++++++++- 8 files changed, 37 insertions(+), 12 deletions(-) create mode 100644 src/userinterface/client_edit_mode/desktop/scripts/inspector.gd.uid diff --git a/src/project.godot b/src/project.godot index 8f1e0ced..f75edadf 100644 --- a/src/project.godot +++ b/src/project.godot @@ -38,9 +38,9 @@ HTTP="*uid://d3cnfdwjxopsx" FileDrop="*uid://btgqgfpx0av4m" Events="*uid://c656spc3ppdlw" StateManager="*uid://b5vvw77bmn4yc" +Database="*uid://cbm5uyffux8ca" OAuth2Client="*uid://c4b880pwgfrty" UrlParser="*uid://budprjmmpally" -Database="*uid://cpkrdq8no0g48" [debug] diff --git a/src/scenes/managers/app/network.gd b/src/scenes/managers/app/network.gd index f66ef086..14baa276 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 575935d2..4d4dff0a 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,10 @@ 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_dir != "": + 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 02824f9f..d4868375 100644 --- a/src/scenes/managers/app/scene.gd.uid +++ b/src/scenes/managers/app/scene.gd.uid @@ -1 +1 @@ -uid://3ylijxenr1bg +uid://ckk3aq3kx7hfr diff --git a/src/userinterface/client_edit_mode/desktop/scripts/inspector.gd.uid b/src/userinterface/client_edit_mode/desktop/scripts/inspector.gd.uid new file mode 100644 index 00000000..8595bd57 --- /dev/null +++ b/src/userinterface/client_edit_mode/desktop/scripts/inspector.gd.uid @@ -0,0 +1 @@ +uid://wwky3vsyqilq diff --git a/src/userinterface/dash/dashboard.tscn b/src/userinterface/dash/dashboard.tscn index 0d71779a..1ed61a11 100644 --- a/src/userinterface/dash/dashboard.tscn +++ b/src/userinterface/dash/dashboard.tscn @@ -893,6 +893,16 @@ layout_mode = 2 placeholder_text = "My New World" alignment = 1 +[node name="WorldLoadDir" type="Label" parent="NewWorld/BackgroundColor/MarginContainer/VBoxContainer/VBoxContainer" unique_id=166016776] +layout_mode = 2 +text = "Base World Directory" + +[node name="WorldLoadDirVal" type="LineEdit" parent="NewWorld/BackgroundColor/MarginContainer/VBoxContainer/VBoxContainer" unique_id=863656716] +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" diff --git a/src/userinterface/dash/scripts/pages/home.gd b/src/userinterface/dash/scripts/pages/home.gd index 50a53065..acc58add 100644 --- a/src/userinterface/dash/scripts/pages/home.gd +++ b/src/userinterface/dash/scripts/pages/home.gd @@ -55,8 +55,8 @@ 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) + var _world_load_dir: String = dashboard.get_node("NewWorld/BackgroundColor/MarginContainer/VBoxContainer/VBoxContainer/WorldLoadDirVal").text + network_m.start_server(0, _world_base, _world_load_dir) _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. diff --git a/src/userinterface/dash/scripts/pages/spawnables.gd b/src/userinterface/dash/scripts/pages/spawnables.gd index c1ff9eef..2a67e64d 100644 --- a/src/userinterface/dash/scripts/pages/spawnables.gd +++ b/src/userinterface/dash/scripts/pages/spawnables.gd @@ -23,6 +23,7 @@ var _selected_folder: Node @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(): @@ -57,7 +58,12 @@ func _build_view() -> void: for file in _files: var _file_pretty_name = file.name var _file_name: String = file.hash - var _listing = _create_button(_file_pretty_name, "", _load_file.bind(_file_name + ".tscn")) + 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")) + _file_container.add_child(_listing) continue @@ -108,6 +114,14 @@ func _load_file(file_name: String) -> void: return +func _create_world(file_name: String) -> void: + var _world_load_dir = dashboard.get_node("NewWorld/BackgroundColor/MarginContainer/VBoxContainer/VBoxContainer/WorldLoadDirVal") + var _file_path = FileManager._current_path() + file_name + _world_load_dir.text = _file_path + dashboard.get_node("NewWorld").visible = true + 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) From c2066b5464915ff02a26f9ef3b150fe468a01ff7 Mon Sep 17 00:00:00 2001 From: Armored Dragon Date: Thu, 6 Aug 2026 22:34:06 -0500 Subject: [PATCH 06/10] Better world loading handling. When loading a world, the create new world dialog is reset. When closing the dialog, it is reset. Moved dialog code to dedicated script file. Added "CUSTOM" base world type. It's only to be used when loading a world from disk. --- src/scenes/managers/app/scene.gd | 9 ++- src/scenes/managers/app/scene.gd.uid | 2 +- src/scripts/enum.gd | 1 + .../desktop/scripts/moveable_window.gd | 12 +++- .../desktop/scripts/moveable_window.gd.uid | 2 +- src/userinterface/dash/dashboard.tscn | 17 ++++- .../dash/scripts/new_world_dialog.gd | 68 +++++++++++++++++++ .../dash/scripts/new_world_dialog.gd.uid | 1 + src/userinterface/dash/scripts/pages/home.gd | 22 +----- .../dash/scripts/pages/spawnables.gd | 11 ++- 10 files changed, 112 insertions(+), 33 deletions(-) create mode 100644 src/userinterface/dash/scripts/new_world_dialog.gd create mode 100644 src/userinterface/dash/scripts/new_world_dialog.gd.uid diff --git a/src/scenes/managers/app/scene.gd b/src/scenes/managers/app/scene.gd index 4d4dff0a..c66a32b2 100644 --- a/src/scenes/managers/app/scene.gd +++ b/src/scenes/managers/app/scene.gd @@ -72,8 +72,13 @@ func set_master_root_from_program(id: String, scene_type: Enum.BaseLevel, scene_ await get_tree().process_frame # Use spawnable system to read the TSCN file, and instantiate it into the multiplayer instance. - if scene_dir != "": - await spawnable_file_handling.load_spawnable(scene_dir) + 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) diff --git a/src/scenes/managers/app/scene.gd.uid b/src/scenes/managers/app/scene.gd.uid index d4868375..3d7795df 100644 --- a/src/scenes/managers/app/scene.gd.uid +++ b/src/scenes/managers/app/scene.gd.uid @@ -1 +1 @@ -uid://ckk3aq3kx7hfr +uid://c82mspg5iieer diff --git a/src/scripts/enum.gd b/src/scripts/enum.gd index 6afd1e3d..75783b21 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, 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 39031cbc..8a289ef3 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,31 @@ 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 _on_input(event) -> void: if event is InputEventMouseButton: _mouse_click(event) @@ -41,6 +45,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 +56,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 4f555474..3115a455 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 1ed61a11..06fb6598 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,15 +892,18 @@ 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 @@ -908,9 +914,10 @@ 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 @@ -922,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 00000000..e939130c --- /dev/null +++ b/src/userinterface/dash/scripts/new_world_dialog.gd @@ -0,0 +1,68 @@ +# --- 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() + 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 00000000..9d86ceae --- /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 acc58add..4ea021e7 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 - var _world_load_dir: String = dashboard.get_node("NewWorld/BackgroundColor/MarginContainer/VBoxContainer/VBoxContainer/WorldLoadDirVal").text - network_m.start_server(0, _world_base, _world_load_dir) - _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/spawnables.gd b/src/userinterface/dash/scripts/pages/spawnables.gd index 2a67e64d..17fa16da 100644 --- a/src/userinterface/dash/scripts/pages/spawnables.gd +++ b/src/userinterface/dash/scripts/pages/spawnables.gd @@ -115,10 +115,15 @@ func _load_file(file_name: String) -> void: func _create_world(file_name: String) -> void: - var _world_load_dir = dashboard.get_node("NewWorld/BackgroundColor/MarginContainer/VBoxContainer/VBoxContainer/WorldLoadDirVal") + 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.text = _file_path - dashboard.get_node("NewWorld").visible = true + + _world_load_dir_val.text = _file_path + _base_val.selected = Enum.BaseLevel.CUSTOM + + _dialog.show_window() return From 79bfdb7cfccee1de5c00f08115d3d2ed6458c3fb Mon Sep 17 00:00:00 2001 From: Armored Dragon Date: Thu, 6 Aug 2026 22:41:01 -0500 Subject: [PATCH 07/10] Open New World dialog in center of scren. When the dialog is to be shown, it is moved to the center of the screen. --- .../client_edit_mode/desktop/scripts/moveable_window.gd | 8 ++++++++ src/userinterface/dash/scripts/new_world_dialog.gd | 1 + 2 files changed, 9 insertions(+) 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 8a289ef3..6b2bc8c7 100644 --- a/src/userinterface/client_edit_mode/desktop/scripts/moveable_window.gd +++ b/src/userinterface/client_edit_mode/desktop/scripts/moveable_window.gd @@ -34,6 +34,14 @@ func _close() -> void: 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) diff --git a/src/userinterface/dash/scripts/new_world_dialog.gd b/src/userinterface/dash/scripts/new_world_dialog.gd index e939130c..1084c5c6 100644 --- a/src/userinterface/dash/scripts/new_world_dialog.gd +++ b/src/userinterface/dash/scripts/new_world_dialog.gd @@ -31,6 +31,7 @@ func _ready() -> void: func show_window() -> void: update_ui() + super._open() visible = true return From 7476e3fce524e2a56010893d26c842155f39a555 Mon Sep 17 00:00:00 2001 From: Armored Dragon Date: Thu, 6 Aug 2026 23:33:39 -0500 Subject: [PATCH 08/10] Delete spawnables. This fixes deleting spawnbles, and will now also delete the spawnables from the database. --- src/scripts/utils/database.gd | 33 +++++++++++++------ src/scripts/utils/files.gd | 6 ++++ .../dash/scripts/pages/spawnables.gd | 22 ++++++++++++- 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/src/scripts/utils/database.gd b/src/scripts/utils/database.gd index 00258d1a..69d2bca1 100644 --- a/src/scripts/utils/database.gd +++ b/src/scripts/utils/database.gd @@ -35,8 +35,22 @@ func _exit_tree() -> void: return -func get_spawnable(id: int) -> Dictionary: - 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: @@ -45,6 +59,13 @@ func set_spawnable(item_hash: String, new_data: Dictionary) -> bool: return _success +func delete_spawnable(item_hash: String) -> bool: + # TODO: Find associated assets and delete them if they are not used elsewhere. + var _success: bool = _db.delete_rows("spawnable", "hash = '%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) @@ -57,14 +78,6 @@ func get_asset(hash: String) -> Dictionary: 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_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) diff --git a/src/scripts/utils/files.gd b/src/scripts/utils/files.gd index 38962672..e9dc642a 100644 --- a/src/scripts/utils/files.gd +++ b/src/scripts/utils/files.gd @@ -79,6 +79,12 @@ func delete_folder(folder_name: String) -> void: 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) diff --git a/src/userinterface/dash/scripts/pages/spawnables.gd b/src/userinterface/dash/scripts/pages/spawnables.gd index 17fa16da..e645e574 100644 --- a/src/userinterface/dash/scripts/pages/spawnables.gd +++ b/src/userinterface/dash/scripts/pages/spawnables.gd @@ -64,11 +64,16 @@ func _build_view() -> void: 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 @@ -204,7 +209,22 @@ func _open_delete_dialog() -> void: 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 From 487a888d0aed852893cba012cf8398f31a2d68cb Mon Sep 17 00:00:00 2001 From: Armored Dragon Date: Fri, 7 Aug 2026 01:19:16 -0500 Subject: [PATCH 09/10] Spawnable asset relationship. Adds a table describing the relationship between assets and spawnables. If a spawnable is deleted, all assets related to that spawnable (if they are not being used by another spawnable) are deleted. --- src/scripts/spawnable_file_handling.gd | 19 ++++++---- src/scripts/utils/database.gd | 48 +++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/src/scripts/spawnable_file_handling.gd b/src/scripts/spawnable_file_handling.gd index d0c5a1ae..3cec42d8 100644 --- a/src/scripts/spawnable_file_handling.gd +++ b/src/scripts/spawnable_file_handling.gd @@ -30,21 +30,19 @@ func save_spawnable(root: Node, type: Enum.SpawnableType = Enum.SpawnableType.IT 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 nodes that should not be saved. _remove_invalid_nodes(root) var packed_scene = _create_packed_scene(root) 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) @@ -153,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": "" } @@ -167,7 +170,7 @@ 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) @@ -200,8 +203,10 @@ func _externalize_assets(root: Node) -> Node: } 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/utils/database.gd b/src/scripts/utils/database.gd index 69d2bca1..a4d7c30b 100644 --- a/src/scripts/utils/database.gd +++ b/src/scripts/utils/database.gd @@ -60,8 +60,19 @@ func set_spawnable(item_hash: String, new_data: Dictionary) -> bool: func delete_spawnable(item_hash: String) -> bool: - # TODO: Find associated assets and delete them if they are not used elsewhere. + 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 @@ -85,6 +96,34 @@ func set_asset(item_hash: String, new_data: Dictionary) -> bool: 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 @@ -116,8 +155,15 @@ func _build_database_schema() -> void: "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 From 837bc99817092fc1192b8594ebb06dbc07f0bf74 Mon Sep 17 00:00:00 2001 From: Armored Dragon Date: Fri, 7 Aug 2026 02:56:25 -0500 Subject: [PATCH 10/10] Added setup instructions and install script for linux. Since the modules are not shipped with the project, provide an install script so that a developer can quickly get the addons required. Also provide instructions on how to download the files manually. --- .gitignore | 1 + README.md | 31 ++++++++++++++++++++----------- src/setup_environment.sh | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 11 deletions(-) create mode 100755 src/setup_environment.sh diff --git a/.gitignore b/.gitignore index b43acb88..7a8a2405 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /src/.godot /src/android/ /src/addons/godot-sqlite +/src/setup_downloads/ diff --git a/README.md b/README.md index 282669f5..b420812d 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/setup_environment.sh b/src/setup_environment.sh new file mode 100755 index 00000000..226f778a --- /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"