Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ef7ee15
Updating gitignore for new Godot structure
WolfgangSenff Feb 4, 2023
c769d56
Attempt to get 4.0 working
WolfgangSenff Feb 4, 2023
dacecf8
Fix remaining 4.x issues
WolfgangSenff Feb 6, 2023
37b0f9d
Update for once and delete
WolfgangSenff Dec 8, 2023
ca1abce
Add 'Web' to OS.get_name() for HTML and UWP exports
BearDooks Jan 18, 2024
b42667b
Some debugging stuff
WolfgangSenff Jan 21, 2024
d9414f5
Fixing some issues hopefully
WolfgangSenff Apr 21, 2024
569f730
Big refactor changes
WolfgangSenff May 25, 2024
f111636
Update from main Firebase plugin
WolfgangSenff Jun 2, 2024
35aee61
Fix database tests slightly
WolfgangSenff May 27, 2024
9259903
Start switch to more nodes than RefCounted
WolfgangSenff May 29, 2024
2845d35
Initial tests refactor, add listener
WolfgangSenff May 29, 2024
63a36cd
Big refactor part 2
WolfgangSenff Jun 2, 2024
b5864e7
Big refactor part 3
WolfgangSenff Jun 4, 2024
04dc883
Merge pull request #6 from GodotNuts/firestore-refactor
WolfgangSenff Jun 4, 2024
2371fc5
Storage rewrite (#7)
WolfgangSenff Jun 8, 2024
685374d
Fix issue with _from_firebase_type_recursive (#8)
WolfgangSenff Jun 10, 2024
4e73019
Update Firestore fields for correct types
WolfgangSenff Jun 27, 2024
c0dbe8e
Firestore query updates (#11)
WolfgangSenff Jul 19, 2024
dc8c0bb
Fix null ref issue
WolfgangSenff Jul 19, 2024
120f06f
Add getter and unsafe documents, fix replace
WolfgangSenff Oct 19, 2024
a95b076
Update for basic checks
WolfgangSenff Oct 19, 2024
6549772
Update test harness
WolfgangSenff Nov 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Normalize EOL for all files that Git considers text files.
* text=auto eol=lf
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ export_presets.cfg
.mono/
data_*/
addons/godot-firebase/.env
*.DS_Store
*.DS_Store
.godot/
*.tmp
345 changes: 345 additions & 0 deletions addons/godot-firebase/Utilities.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,345 @@
extends Node
class_name Utilities

static func get_json_data(value):
if value is PackedByteArray:
value = value.get_string_from_utf8()
var json = JSON.new()
var json_parse_result = json.parse(value)
if json_parse_result == OK:
return json.data

return null


# Pass a dictionary { 'key' : 'value' } to format it in a APIs usable .fields
# Field Path3D using the "dot" (`.`) notation are supported:
# ex. { "PATH.TO.SUBKEY" : "VALUE" } ==> { "PATH" : { "TO" : { "SUBKEY" : "VALUE" } } }
static func dict2fields(dict : Dictionary) -> Dictionary:
var fields = {}
var var_type : String = ""
for field in dict.keys():
var field_value = dict[field]
if field is String and "." in field:
var keys: Array = field.split(".")
field = keys.pop_front()
keys.reverse()
for key in keys:
field_value = { key : field_value }

match typeof(field_value):
TYPE_NIL: var_type = "nullValue"
TYPE_BOOL: var_type = "booleanValue"
TYPE_INT: var_type = "integerValue"
TYPE_FLOAT: var_type = "doubleValue"
TYPE_STRING: var_type = "stringValue"
TYPE_DICTIONARY:
if is_field_timestamp(field_value):
var_type = "timestampValue"
field_value = dict2timestamp(field_value)
else:
var_type = "mapValue"
field_value = dict2fields(field_value)
TYPE_ARRAY:
var_type = "arrayValue"
field_value = {"values": array2fields(field_value)}

if fields.has(field) and fields[field].has("mapValue") and field_value.has("fields"):
for key in field_value["fields"].keys():
fields[field]["mapValue"]["fields"][key] = field_value["fields"][key]
else:
fields[field] = { var_type : field_value }

return {'fields' : fields}

class FirebaseTypeConverter extends RefCounted:
var converters = {
"nullValue": _to_null,
"booleanValue": _to_bool,
"integerValue": _to_int,
"doubleValue": _to_float
}

func convert_value(type, value):
if converters.has(type):
return converters[type].call(value)

return value

func _to_null(value):
return null

func _to_bool(value):
return bool(value)

func _to_int(value):
return int(value)

func _to_float(value):
return float(value)

static func from_firebase_type(value):
if value == null:
return null

if value.has("mapValue"):
value = fields2dict(value.values()[0])
elif value.has("arrayValue"):
value = fields2array(value.values()[0])
elif value.has("timestampValue"):
value = Time.get_datetime_dict_from_datetime_string(value.values()[0], false)
else:
var converter = FirebaseTypeConverter.new()
value = converter.convert_value(value.keys()[0], value.values()[0])

return value


static func to_firebase_type(value : Variant) -> Dictionary:
var var_type : String = ""

match typeof(value):
TYPE_NIL: var_type = "nullValue"
TYPE_BOOL: var_type = "booleanValue"
TYPE_INT: var_type = "integerValue"
TYPE_FLOAT: var_type = "doubleValue"
TYPE_STRING: var_type = "stringValue"
TYPE_DICTIONARY:
if is_field_timestamp(value):
var_type = "timestampValue"
value = dict2timestamp(value)
else:
var_type = "mapValue"
value = dict2fields(value)
TYPE_ARRAY:
var_type = "arrayValue"
value = {"values": array2fields(value)}

return { var_type : value }

# Pass the .fields inside a Firestore Document to print out the Dictionary { 'key' : 'value' }
static func fields2dict(doc) -> Dictionary:
var dict = {}
if doc.has("fields"):
var fields = doc["fields"]

for field in fields.keys():
if fields[field].has("mapValue"):
dict[field] = (fields2dict(fields[field].mapValue))
elif fields[field].has("timestampValue"):
dict[field] = timestamp2dict(fields[field].timestampValue)
elif fields[field].has("arrayValue"):
dict[field] = fields2array(fields[field].arrayValue)
elif fields[field].has("integerValue"):
dict[field] = fields[field].values()[0] as int
elif fields[field].has("doubleValue"):
dict[field] = fields[field].values()[0] as float
elif fields[field].has("booleanValue"):
dict[field] = fields[field].values()[0] as bool
elif fields[field].has("nullValue"):
dict[field] = null
else:
dict[field] = fields[field].values()[0]
return dict

# Pass an Array to parse it to a Firebase arrayValue
static func array2fields(array : Array) -> Array:
var fields : Array = []
var var_type : String = ""
for field in array:
match typeof(field):
TYPE_DICTIONARY:
if is_field_timestamp(field):
var_type = "timestampValue"
field = dict2timestamp(field)
else:
var_type = "mapValue"
field = dict2fields(field)
TYPE_NIL: var_type = "nullValue"
TYPE_BOOL: var_type = "booleanValue"
TYPE_INT: var_type = "integerValue"
TYPE_FLOAT: var_type = "doubleValue"
TYPE_STRING: var_type = "stringValue"
TYPE_ARRAY: var_type = "arrayValue"
_: var_type = "FieldTransform"
fields.append({ var_type : field })
return fields

# Pass a Firebase arrayValue Dictionary to convert it back to an Array
static func fields2array(array : Dictionary) -> Array:
var fields : Array = []
if array.has("values"):
for field in array.values:
var item
match field.keys()[0]:
"mapValue":
item = fields2dict(field.mapValue)
"arrayValue":
item = fields2array(field.arrayValue)
"integerValue":
item = field.values()[0] as int
"doubleValue":
item = field.values()[0] as float
"booleanValue":
item = field.values()[0] as bool
"timestampValue":
item = timestamp2dict(field.timestampValue)
"nullValue":
item = null
_:
item = field.values()[0]
fields.append(item)
return fields

# Converts a gdscript Dictionary (most likely obtained with Time.get_datetime_dict_from_system()) to a Firebase Timestamp
static func dict2timestamp(dict : Dictionary) -> String:
#dict.erase('weekday')
#dict.erase('dst')
#var dict_values : Array = dict.values()
var time = Time.get_datetime_string_from_datetime_dict(dict, false)
return time
#return "%04d-%02d-%02dT%02d:%02d:%02d.00Z" % dict_values

# Converts a Firebase Timestamp back to a gdscript Dictionary
static func timestamp2dict(timestamp : String) -> Dictionary:
return Time.get_datetime_dict_from_datetime_string(timestamp, false)
#var datetime : Dictionary = {year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0}
#var dict : PackedStringArray = timestamp.split("T")[0].split("-")
#dict.append_array(timestamp.split("T")[1].split(":"))
#for value in dict.size():
#datetime[datetime.keys()[value]] = int(dict[value])
#return datetime

static func is_field_timestamp(field : Dictionary) -> bool:
return field.has_all(['year','month','day','hour','minute','second'])


# HTTPRequeust seems to have an issue in Web exports where the body returns empty
# This appears to be caused by the gzip compression being unsupported, so we
# disable it when web export is detected.
static func fix_http_request(http_request):
if is_web():
http_request.accept_gzip = false

static func is_web() -> bool:
return OS.get_name() in ["HTML5", "Web"]


class MultiSignal extends RefCounted:
signal completed(with_signal)
signal all_completed()

var _has_signaled := false
var _early_exit := false

var signal_count := 0

func _init(sigs : Array[Signal], early_exit := true, should_oneshot := true) -> void:
_early_exit = early_exit
for sig in sigs:
add_signal(sig, should_oneshot)

func add_signal(sig : Signal, should_oneshot) -> void:
signal_count += 1
sig.connect(
func():
if not _has_signaled and _early_exit:
completed.emit(sig)
_has_signaled = true
elif not _early_exit:
completed.emit(sig)
signal_count -= 1
if signal_count <= 0: # Not sure how it could be less than
all_completed.emit()
, CONNECT_ONE_SHOT if should_oneshot else CONNECT_REFERENCE_COUNTED
)

class SignalReducer extends RefCounted: # No need for a node, as this deals strictly with signals, which can be on any object.
signal completed

var awaiters : Array[Signal] = []

var reducers = {
0 : func(): completed.emit(),
1 : func(p): completed.emit(),
2 : func(p1, p2): completed.emit(),
3 : func(p1, p2, p3): completed.emit(),
4 : func(p1, p2, p3, p4): completed.emit()
}

func add_signal(sig : Signal, param_count : int = 0) -> void:
assert(param_count < 5, "Too many parameters to reduce, just add more!")
sig.connect(reducers[param_count], CONNECT_ONE_SHOT) # May wish to not just one-shot, but instead track all of them firing

class SignalReducerWithResult extends RefCounted: # No need for a node, as this deals strictly with signals, which can be on any object.
signal completed(result)

var awaiters : Array[Signal] = []

var reducers = {
0 : func(): completed.emit(),
1 : func(p): completed.emit({1 : p}),
2 : func(p1, p2): completed.emit({ 1 : p1, 2 : p2 }),
3 : func(p1, p2, p3): completed.emit({ 1 : p1, 2 : p2, 3 : p3 }),
4 : func(p1, p2, p3, p4): completed.emit({ 1 : p1, 2 : p2, 3 : p3, 4 : p4 })
}

func add_signal(sig : Signal, param_count : int = 0) -> void:
assert(param_count < 5, "Too many parameters to reduce, just add more!")
sig.connect(reducers[param_count], CONNECT_ONE_SHOT) # May wish to not just one-shot, but instead track all of them firing

class ObservableDictionary extends RefCounted:
signal keys_changed()

var _internal : Dictionary
var is_notifying := true

func _init(copy : Dictionary = {}) -> void:
_internal = copy

func add(key : Variant, value : Variant) -> void:
_internal[key] = value
if is_notifying:
keys_changed.emit()

func update(key : Variant, value : Variant) -> void:
_internal[key] = value
if is_notifying:
keys_changed.emit()

func has(key : Variant) -> bool:
return _internal.has(key)

func keys():
return _internal.keys()

func values():
return _internal.values()

func erase(key : Variant) -> bool:
var result = _internal.erase(key)
if is_notifying:
keys_changed.emit()

return result

func get_value(key : Variant) -> Variant:
return _internal[key]

func _get(property: StringName) -> Variant:
if _internal.has(property):
return _internal[property]

return false

func _set(property: StringName, value: Variant) -> bool:
update(property, value)
return true

class AwaitDetachable extends Node2D:
var awaiter : Signal

func _init(freeable_node, await_signal : Signal) -> void:
awaiter = await_signal
add_child(freeable_node)
awaiter.connect(queue_free)
1 change: 1 addition & 0 deletions addons/godot-firebase/Utilities.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://dgmbytds3kkmh
Loading