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)