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,346 @@
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)