added firebase and rudimentary leaderboard support

This commit is contained in:
derek
2025-04-09 11:19:02 -05:00
parent ce08df66e6
commit 25eb9e725a
121 changed files with 4987 additions and 4 deletions

View 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

View File

@@ -0,0 +1 @@
uid://ba2spgo4jo2qw

View 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]

View File

@@ -0,0 +1 @@
uid://bo3i7q8s2bfmq

View File

@@ -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")

View File

@@ -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"]

View 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)

View File

@@ -0,0 +1 @@
uid://d0b2x1kc1w1w3

View 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()

View File

@@ -0,0 +1 @@
uid://ofyy1lc3qlfn

View 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})

View File

@@ -0,0 +1 @@
uid://cbqame2gc2atr