added firebase and rudimentary leaderboard support
This commit is contained in:
51
addons/godot-firebase/database/database.gd
Normal file
51
addons/godot-firebase/database/database.gd
Normal file
@@ -0,0 +1,51 @@
|
||||
## @meta-authors TODO
|
||||
## @meta-version 2.2
|
||||
## The Realtime Database API for Firebase.
|
||||
## Documentation TODO.
|
||||
@tool
|
||||
class_name FirebaseDatabase
|
||||
extends Node
|
||||
|
||||
var _base_url : String = ""
|
||||
|
||||
var _config : Dictionary = {}
|
||||
|
||||
var _auth : Dictionary = {}
|
||||
|
||||
func _set_config(config_json : Dictionary) -> void:
|
||||
_config = config_json
|
||||
_check_emulating()
|
||||
|
||||
func _check_emulating() -> void :
|
||||
## Check emulating
|
||||
if not Firebase.emulating:
|
||||
_base_url = _config.databaseURL
|
||||
else:
|
||||
var port : String = _config.emulators.ports.realtimeDatabase
|
||||
if port == "":
|
||||
Firebase._printerr("You are in 'emulated' mode, but the port for Realtime Database has not been configured.")
|
||||
else:
|
||||
_base_url = "http://localhost"
|
||||
|
||||
func _on_FirebaseAuth_login_succeeded(auth_result : Dictionary) -> void:
|
||||
_auth = auth_result
|
||||
|
||||
func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void:
|
||||
_auth = auth_result
|
||||
|
||||
func _on_FirebaseAuth_logout() -> void:
|
||||
_auth = {}
|
||||
|
||||
func get_database_reference(path : String, filter : Dictionary = {}) -> FirebaseDatabaseReference:
|
||||
var firebase_reference = load("res://addons/godot-firebase/database/firebase_database_reference.tscn").instantiate()
|
||||
firebase_reference.set_db_path(path, filter)
|
||||
firebase_reference.set_auth_and_config(_auth, _config)
|
||||
add_child(firebase_reference)
|
||||
return firebase_reference
|
||||
|
||||
func get_once_database_reference(path : String, filter : Dictionary = {}) -> FirebaseOnceDatabaseReference:
|
||||
var firebase_reference = load("res://addons/godot-firebase/database/firebase_once_database_reference.tscn").instantiate()
|
||||
firebase_reference.set_db_path(path, filter)
|
||||
firebase_reference.set_auth_and_config(_auth, _config)
|
||||
add_child(firebase_reference)
|
||||
return firebase_reference
|
||||
1
addons/godot-firebase/database/database.gd.uid
Normal file
1
addons/godot-firebase/database/database.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ba2spgo4jo2qw
|
||||
109
addons/godot-firebase/database/database_store.gd
Normal file
109
addons/godot-firebase/database/database_store.gd
Normal file
@@ -0,0 +1,109 @@
|
||||
## @meta-authors TODO
|
||||
## @meta-version 2.2
|
||||
## Data structure that holds the currently-known data at a given path (a.k.a. reference) in a Firebase Realtime Database.
|
||||
## Can process both puts and patches into the data based checked realtime events received from the service.
|
||||
@tool
|
||||
class_name FirebaseDatabaseStore
|
||||
extends Node
|
||||
|
||||
const _DELIMITER : String = "/"
|
||||
const _ROOT : String = "_root"
|
||||
|
||||
## @default false
|
||||
## Whether the store is in debug mode.
|
||||
var debug : bool = false
|
||||
var _data : Dictionary = { }
|
||||
|
||||
|
||||
## @args path, payload
|
||||
## Puts a new payload into this data store at the given path. Any existing values in this data store
|
||||
## at the specified path will be completely erased.
|
||||
func put(path : String, payload) -> void:
|
||||
_update_data(path, payload, false)
|
||||
|
||||
## @args path, payload
|
||||
## Patches an update payload into this data store at the specified path.
|
||||
## NOTE: When patching in updates to arrays, payload should contain the entire new array! Updating single elements/indexes of an array is not supported. Sometimes when manually mutating array data directly from the Firebase Realtime Database console, single-element patches will be sent out which can cause issues here.
|
||||
func patch(path : String, payload) -> void:
|
||||
_update_data(path, payload, true)
|
||||
|
||||
## @args path, payload
|
||||
## Deletes data at the reference point provided
|
||||
## NOTE: This will delete without warning, so make sure the reference is pointed to the level you want and not the root or you will lose everything
|
||||
func delete(path : String, payload) -> void:
|
||||
_update_data(path, payload, true)
|
||||
|
||||
## Returns a deep copy of this data store's payload.
|
||||
func get_data() -> Dictionary:
|
||||
return _data[_ROOT].duplicate(true)
|
||||
|
||||
#
|
||||
# Updates this data store by either putting or patching the provided payload into it at the given
|
||||
# path. The provided payload can technically be any value.
|
||||
#
|
||||
func _update_data(path: String, payload, patch: bool) -> void:
|
||||
if debug:
|
||||
print("Updating data store (patch = %s) (%s = %s)..." % [patch, path, payload])
|
||||
|
||||
#
|
||||
# Remove any leading separators.
|
||||
#
|
||||
path = path.lstrip(_DELIMITER)
|
||||
|
||||
#
|
||||
# Traverse the path.
|
||||
#
|
||||
var dict = _data
|
||||
var keys = PackedStringArray([_ROOT])
|
||||
|
||||
keys.append_array(path.split(_DELIMITER, false))
|
||||
|
||||
var final_key_idx = (keys.size() - 1)
|
||||
var final_key = (keys[final_key_idx])
|
||||
|
||||
keys.remove_at(final_key_idx)
|
||||
|
||||
for key in keys:
|
||||
if !dict.has(key):
|
||||
dict[key] = { }
|
||||
|
||||
dict = dict[key]
|
||||
|
||||
#
|
||||
# Handle non-patch (a.k.a. put) mode and then update the destination value.
|
||||
#
|
||||
var new_type = typeof(payload)
|
||||
|
||||
if !patch:
|
||||
dict.erase(final_key)
|
||||
|
||||
if new_type == TYPE_NIL:
|
||||
dict.erase(final_key)
|
||||
elif new_type == TYPE_DICTIONARY:
|
||||
if !dict.has(final_key):
|
||||
dict[final_key] = { }
|
||||
|
||||
_update_dictionary(dict[final_key], payload)
|
||||
else:
|
||||
dict[final_key] = payload
|
||||
|
||||
if debug:
|
||||
print("...Data store updated (%s)." % _data)
|
||||
|
||||
#
|
||||
# Helper method to "blit" changes in an update dictionary payload onto an original dictionary.
|
||||
# Parameters are directly changed via reference.
|
||||
#
|
||||
func _update_dictionary(original_dict: Dictionary, update_payload: Dictionary) -> void:
|
||||
for key in update_payload.keys():
|
||||
var val_type = typeof(update_payload[key])
|
||||
|
||||
if val_type == TYPE_NIL:
|
||||
original_dict.erase(key)
|
||||
elif val_type == TYPE_DICTIONARY:
|
||||
if !original_dict.has(key):
|
||||
original_dict[key] = { }
|
||||
|
||||
_update_dictionary(original_dict[key], update_payload[key])
|
||||
else:
|
||||
original_dict[key] = update_payload[key]
|
||||
1
addons/godot-firebase/database/database_store.gd.uid
Normal file
1
addons/godot-firebase/database/database_store.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bo3i7q8s2bfmq
|
||||
@@ -0,0 +1,17 @@
|
||||
[gd_scene load_steps=5 format=3 uid="uid://btltp52tywbe4"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/godot-firebase/database/reference.gd" id="1_l3oy5"]
|
||||
[ext_resource type="PackedScene" uid="uid://ctb4l7plg8kqg" path="res://addons/godot-firebase/queues/queueable_http_request.tscn" id="2_0qpk7"]
|
||||
[ext_resource type="Script" path="res://addons/http-sse-client/HTTPSSEClient.gd" id="2_4l0io"]
|
||||
[ext_resource type="Script" path="res://addons/godot-firebase/database/database_store.gd" id="3_c3r2w"]
|
||||
|
||||
[node name="FirebaseDatabaseReference" type="Node"]
|
||||
script = ExtResource("1_l3oy5")
|
||||
|
||||
[node name="Pusher" parent="." instance=ExtResource("2_0qpk7")]
|
||||
|
||||
[node name="Listener" type="Node" parent="."]
|
||||
script = ExtResource("2_4l0io")
|
||||
|
||||
[node name="DataStore" type="Node" parent="."]
|
||||
script = ExtResource("3_c3r2w")
|
||||
@@ -0,0 +1,16 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://d1u1bxp2fd60e"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/godot-firebase/database/once_reference.gd" id="1_hq5s2"]
|
||||
[ext_resource type="PackedScene" uid="uid://ctb4l7plg8kqg" path="res://addons/godot-firebase/queues/queueable_http_request.tscn" id="2_t2f32"]
|
||||
|
||||
[node name="FirebaseOnceDatabaseReference" type="Node"]
|
||||
script = ExtResource("1_hq5s2")
|
||||
|
||||
[node name="Pusher" parent="." instance=ExtResource("2_t2f32")]
|
||||
accept_gzip = false
|
||||
|
||||
[node name="Oncer" parent="." instance=ExtResource("2_t2f32")]
|
||||
accept_gzip = false
|
||||
|
||||
[connection signal="queue_request_completed" from="Pusher" to="." method="on_push_request_complete"]
|
||||
[connection signal="queue_request_completed" from="Oncer" to="." method="on_get_request_complete"]
|
||||
124
addons/godot-firebase/database/once_reference.gd
Normal file
124
addons/godot-firebase/database/once_reference.gd
Normal file
@@ -0,0 +1,124 @@
|
||||
class_name FirebaseOnceDatabaseReference
|
||||
extends Node
|
||||
|
||||
|
||||
## @meta-authors BackAt50Ft
|
||||
## @meta-version 1.0
|
||||
## A once off reference to a location in the Realtime Database.
|
||||
## Documentation TODO.
|
||||
|
||||
signal once_successful(dataSnapshot)
|
||||
signal once_failed()
|
||||
|
||||
signal push_successful()
|
||||
signal push_failed()
|
||||
|
||||
const ORDER_BY : String = "orderBy"
|
||||
const LIMIT_TO_FIRST : String = "limitToFirst"
|
||||
const LIMIT_TO_LAST : String = "limitToLast"
|
||||
const START_AT : String = "startAt"
|
||||
const END_AT : String = "endAt"
|
||||
const EQUAL_TO : String = "equalTo"
|
||||
|
||||
@onready var _oncer = $Oncer
|
||||
@onready var _pusher = $Pusher
|
||||
|
||||
var _auth : Dictionary
|
||||
var _config : Dictionary
|
||||
var _filter_query : Dictionary
|
||||
var _db_path : String
|
||||
|
||||
const _separator : String = "/"
|
||||
const _json_list_tag : String = ".json"
|
||||
const _query_tag : String = "?"
|
||||
const _auth_tag : String = "auth="
|
||||
|
||||
const _auth_variable_begin : String = "["
|
||||
const _auth_variable_end : String = "]"
|
||||
const _filter_tag : String = "&"
|
||||
const _escaped_quote : String = '"'
|
||||
const _equal_tag : String = "="
|
||||
const _key_filter_tag : String = "$key"
|
||||
|
||||
var _headers : PackedStringArray = []
|
||||
|
||||
func set_db_path(path : String, filter_query_dict : Dictionary) -> void:
|
||||
_db_path = path
|
||||
_filter_query = filter_query_dict
|
||||
|
||||
func set_auth_and_config(auth_ref : Dictionary, config_ref : Dictionary) -> void:
|
||||
_auth = auth_ref
|
||||
_config = config_ref
|
||||
|
||||
#
|
||||
# Gets a data snapshot once at the position passed in
|
||||
#
|
||||
func once(reference : String) -> void:
|
||||
var ref_pos = _get_list_url() + _db_path + _separator + reference + _get_remaining_path()
|
||||
_oncer.request(ref_pos, _headers, HTTPClient.METHOD_GET, "")
|
||||
|
||||
func _get_remaining_path(is_push : bool = true) -> String:
|
||||
var remaining_path = ""
|
||||
if _filter_query_empty() or is_push:
|
||||
remaining_path = _json_list_tag + _query_tag + _auth_tag + Firebase.Auth.auth.idtoken
|
||||
else:
|
||||
remaining_path = _json_list_tag + _query_tag + _get_filter() + _filter_tag + _auth_tag + Firebase.Auth.auth.idtoken
|
||||
|
||||
if Firebase.emulating:
|
||||
remaining_path += "&ns="+_config.projectId+"-default-rtdb"
|
||||
|
||||
return remaining_path
|
||||
|
||||
func _get_list_url(with_port:bool = true) -> String:
|
||||
var url = Firebase.Database._base_url.trim_suffix(_separator)
|
||||
if with_port and Firebase.emulating:
|
||||
url += ":" + _config.emulators.ports.realtimeDatabase
|
||||
return url + _separator
|
||||
|
||||
|
||||
func _get_filter():
|
||||
if _filter_query_empty():
|
||||
return ""
|
||||
|
||||
var filter = ""
|
||||
|
||||
if _filter_query.has(ORDER_BY):
|
||||
filter += ORDER_BY + _equal_tag + _escaped_quote + _filter_query[ORDER_BY] + _escaped_quote
|
||||
_filter_query.erase(ORDER_BY)
|
||||
else:
|
||||
filter += ORDER_BY + _equal_tag + _escaped_quote + _key_filter_tag + _escaped_quote # Presumptuous, but to get it to work at all...
|
||||
|
||||
for key in _filter_query.keys():
|
||||
filter += _filter_tag + key + _equal_tag + _filter_query[key]
|
||||
|
||||
return filter
|
||||
|
||||
func _filter_query_empty() -> bool:
|
||||
return _filter_query == null or _filter_query.is_empty()
|
||||
|
||||
func on_get_request_complete(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void:
|
||||
if response_code == HTTPClient.RESPONSE_OK:
|
||||
var bod = Utilities.get_json_data(body)
|
||||
once_successful.emit(bod)
|
||||
else:
|
||||
once_failed.emit()
|
||||
|
||||
func on_push_request_complete(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void:
|
||||
if response_code == HTTPClient.RESPONSE_OK:
|
||||
push_successful.emit()
|
||||
else:
|
||||
push_failed.emit()
|
||||
|
||||
func push(data : Dictionary) -> void:
|
||||
var to_push = JSON.stringify(data)
|
||||
_pusher.request(_get_list_url() + _db_path + _get_remaining_path(true), _headers, HTTPClient.METHOD_POST, to_push)
|
||||
|
||||
func update(path : String, data : Dictionary) -> void:
|
||||
path = path.strip_edges(true, true)
|
||||
|
||||
if path == _separator:
|
||||
path = ""
|
||||
|
||||
var to_update = JSON.stringify(data)
|
||||
var resolved_path = (_get_list_url() + _db_path + "/" + path + _get_remaining_path())
|
||||
_pusher.request(resolved_path, _headers, HTTPClient.METHOD_PATCH, to_update)
|
||||
1
addons/godot-firebase/database/once_reference.gd.uid
Normal file
1
addons/godot-firebase/database/once_reference.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://d0b2x1kc1w1w3
|
||||
176
addons/godot-firebase/database/reference.gd
Normal file
176
addons/godot-firebase/database/reference.gd
Normal file
@@ -0,0 +1,176 @@
|
||||
## @meta-authors BackAt50Ft
|
||||
## @meta-version 2.4
|
||||
## A reference to a location in the Realtime Database.
|
||||
## Documentation TODO.
|
||||
@tool
|
||||
class_name FirebaseDatabaseReference
|
||||
extends Node
|
||||
|
||||
signal new_data_update(data)
|
||||
signal patch_data_update(data)
|
||||
signal delete_data_update(data)
|
||||
|
||||
signal once_successful(dataSnapshot)
|
||||
signal once_failed()
|
||||
|
||||
signal push_successful()
|
||||
signal push_failed()
|
||||
|
||||
|
||||
const ORDER_BY : String = "orderBy"
|
||||
const LIMIT_TO_FIRST : String = "limitToFirst"
|
||||
const LIMIT_TO_LAST : String = "limitToLast"
|
||||
const START_AT : String = "startAt"
|
||||
const END_AT : String = "endAt"
|
||||
const EQUAL_TO : String = "equalTo"
|
||||
|
||||
@onready var _pusher := $Pusher
|
||||
@onready var _listener := $Listener
|
||||
@onready var _store := $DataStore
|
||||
|
||||
var _auth : Dictionary
|
||||
var _config : Dictionary
|
||||
var _filter_query : Dictionary
|
||||
var _db_path : String
|
||||
var _cached_filter : String
|
||||
var _can_connect_to_host : bool = false
|
||||
|
||||
const _put_tag : String = "put"
|
||||
const _patch_tag : String = "patch"
|
||||
const _delete_tag : String = "delete"
|
||||
const _separator : String = "/"
|
||||
const _json_list_tag : String = ".json"
|
||||
const _query_tag : String = "?"
|
||||
const _auth_tag : String = "auth="
|
||||
const _accept_header : String = "accept: text/event-stream"
|
||||
const _auth_variable_begin : String = "["
|
||||
const _auth_variable_end : String = "]"
|
||||
const _filter_tag : String = "&"
|
||||
const _escaped_quote : String = '"'
|
||||
const _equal_tag : String = "="
|
||||
const _key_filter_tag : String = "$key"
|
||||
|
||||
var _headers : PackedStringArray = []
|
||||
|
||||
func _ready() -> void:
|
||||
#region Set Listener info
|
||||
$Listener.new_sse_event.connect(on_new_sse_event)
|
||||
var base_url = _get_list_url(false).trim_suffix(_separator)
|
||||
var extended_url = _separator + _db_path + _get_remaining_path(false)
|
||||
var port = -1
|
||||
if Firebase.emulating:
|
||||
port = int(_config.emulators.ports.realtimeDatabase)
|
||||
$Listener.connect_to_host(base_url, extended_url, port)
|
||||
#endregion Set Listener info
|
||||
|
||||
#region Set Pusher info
|
||||
$Pusher.queue_request_completed.connect(on_push_request_complete)
|
||||
#endregion Set Pusher info
|
||||
|
||||
func set_db_path(path : String, filter_query_dict : Dictionary) -> void:
|
||||
_db_path = path
|
||||
_filter_query = filter_query_dict
|
||||
|
||||
func set_auth_and_config(auth_ref : Dictionary, config_ref : Dictionary) -> void:
|
||||
_auth = auth_ref
|
||||
_config = config_ref
|
||||
|
||||
func on_new_sse_event(headers : Dictionary, event : String, data : Dictionary) -> void:
|
||||
if data:
|
||||
var command = event
|
||||
if command and command != "keep-alive":
|
||||
_route_data(command, data.path, data.data)
|
||||
if command == _put_tag:
|
||||
if data.path == _separator and data.data and data.data.keys().size() > 0:
|
||||
for key in data.data.keys():
|
||||
new_data_update.emit(FirebaseResource.new(_separator + key, data.data[key]))
|
||||
elif data.path != _separator:
|
||||
new_data_update.emit(FirebaseResource.new(data.path, data.data))
|
||||
elif command == _patch_tag:
|
||||
patch_data_update.emit(FirebaseResource.new(data.path, data.data))
|
||||
elif command == _delete_tag:
|
||||
delete_data_update.emit(FirebaseResource.new(data.path, data.data))
|
||||
|
||||
func update(path : String, data : Dictionary) -> void:
|
||||
path = path.strip_edges(true, true)
|
||||
|
||||
if path == _separator:
|
||||
path = ""
|
||||
|
||||
var to_update = JSON.stringify(data)
|
||||
|
||||
var resolved_path = (_get_list_url() + _db_path + "/" + path + _get_remaining_path())
|
||||
_pusher.request(resolved_path, _headers, HTTPClient.METHOD_PATCH, to_update)
|
||||
|
||||
func push(data : Dictionary) -> void:
|
||||
var to_push = JSON.stringify(data)
|
||||
_pusher.request(_get_list_url() + _db_path + _get_remaining_path(), _headers, HTTPClient.METHOD_POST, to_push)
|
||||
|
||||
func delete(reference : String) -> void:
|
||||
_pusher.request(_get_list_url() + _db_path + _separator + reference + _get_remaining_path(), _headers, HTTPClient.METHOD_DELETE, "")
|
||||
|
||||
#
|
||||
# Returns a deep copy of the current local copy of the data stored at this reference in the Firebase
|
||||
# Realtime Database.
|
||||
#
|
||||
func get_data() -> Dictionary:
|
||||
if _store == null:
|
||||
return { }
|
||||
|
||||
return _store.get_data()
|
||||
|
||||
func _get_remaining_path(is_push : bool = true) -> String:
|
||||
var remaining_path = ""
|
||||
if _filter_query_empty() or is_push:
|
||||
remaining_path = _json_list_tag + _query_tag + _auth_tag + Firebase.Auth.auth.idtoken
|
||||
else:
|
||||
remaining_path = _json_list_tag + _query_tag + _get_filter() + _filter_tag + _auth_tag + Firebase.Auth.auth.idtoken
|
||||
|
||||
if Firebase.emulating:
|
||||
remaining_path += "&ns="+_config.projectId+"-default-rtdb"
|
||||
|
||||
return remaining_path
|
||||
|
||||
func _get_list_url(with_port:bool = true) -> String:
|
||||
var url = Firebase.Database._base_url.trim_suffix(_separator)
|
||||
if with_port and Firebase.emulating:
|
||||
url += ":" + _config.emulators.ports.realtimeDatabase
|
||||
return url + _separator
|
||||
|
||||
|
||||
func _get_filter():
|
||||
if _filter_query_empty():
|
||||
return ""
|
||||
# At the moment, this means you can't dynamically change your filter; I think it's okay to specify that in the rules.
|
||||
if _cached_filter != "":
|
||||
_cached_filter = ""
|
||||
if _filter_query.has(ORDER_BY):
|
||||
_cached_filter += ORDER_BY + _equal_tag + _escaped_quote + _filter_query[ORDER_BY] + _escaped_quote
|
||||
_filter_query.erase(ORDER_BY)
|
||||
else:
|
||||
_cached_filter += ORDER_BY + _equal_tag + _escaped_quote + _key_filter_tag + _escaped_quote # Presumptuous, but to get it to work at all...
|
||||
for key in _filter_query.keys():
|
||||
_cached_filter += _filter_tag + key + _equal_tag + _filter_query[key]
|
||||
|
||||
return _cached_filter
|
||||
|
||||
func _filter_query_empty() -> bool:
|
||||
return _filter_query == null or _filter_query.is_empty()
|
||||
|
||||
#
|
||||
# Appropriately updates the current local copy of the data stored at this reference in the Firebase
|
||||
# Realtime Database.
|
||||
#
|
||||
func _route_data(command : String, path : String, data) -> void:
|
||||
if command == _put_tag:
|
||||
_store.put(path, data)
|
||||
elif command == _patch_tag:
|
||||
_store.patch(path, data)
|
||||
elif command == _delete_tag:
|
||||
_store.delete(path, data)
|
||||
|
||||
func on_push_request_complete(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void:
|
||||
if response_code == HTTPClient.RESPONSE_OK:
|
||||
push_successful.emit()
|
||||
else:
|
||||
push_failed.emit()
|
||||
1
addons/godot-firebase/database/reference.gd.uid
Normal file
1
addons/godot-firebase/database/reference.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ofyy1lc3qlfn
|
||||
16
addons/godot-firebase/database/resource.gd
Normal file
16
addons/godot-firebase/database/resource.gd
Normal file
@@ -0,0 +1,16 @@
|
||||
## @meta-authors SIsilicon, fenix-hub
|
||||
## @meta-version 2.2
|
||||
## A generic resource used by Firebase Database.
|
||||
@tool
|
||||
class_name FirebaseResource
|
||||
extends Resource
|
||||
|
||||
var key : String
|
||||
var data
|
||||
|
||||
func _init(key : String,data):
|
||||
self.key = key.lstrip("/")
|
||||
self.data = data
|
||||
|
||||
func _to_string():
|
||||
return "{ key:{key}, data:{data} }".format({key = key, data = data})
|
||||
1
addons/godot-firebase/database/resource.gd.uid
Normal file
1
addons/godot-firebase/database/resource.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cbqame2gc2atr
|
||||
Reference in New Issue
Block a user