@icon("res://components/Persister/hard-drive.svg") extends Base ## Data persister to persist data through locations ## ## A data persister that is used to persist data through the game, run, round, ## or room. A function is called to set data with a key in one of the categories ## and then other functions can be called to clear the data for a key or all ## data in a category. signal data_persisted(key: String, value, category: PersisterEnums.Scope) var _persisted = {} func _spawned(): if _triggerer: _info("Connecting to data scope triggers") _triggerer.listen("game", _on_game_triggered) _triggerer.listen("run", _on_run_triggered) _triggerer.listen("round", _on_round_triggered) _triggerer.listen("room", _on_room_triggered) _load() ## Store data in a category func persist_data(key: String, value, category := PersisterEnums.Scope.RUN) -> void: # Only allow ints, bools or strings to be persisted and as Strings var original_pass = value if value is int: value = str(value) if value is bool: value = str(value) if not value is String: _warn("Attempted to persist data for key <%s> with an invalid data type |%s|" % [key, type_string(typeof(value))]) return # Create category if does not exist if not _persisted.has(category): _info("Created new persister category {%s}" % [_get_category_name(category)]) _persisted[category] = {} # If key is already set to value exit early if _persisted[category].has(key) and _persisted[category][key] == value: return _info("Set key <%s> in category {%s} to value |%s|" % [key, _get_category_name(category), value]) _persisted[category][key] = value if _triggerer: _triggerer.trigger(key, {"value": original_pass}) _emit_change(key, value, category) ## Get the value associated with a key in the highest priority or specified category func get_value(key: String, category: PersisterEnums.Scope = PersisterEnums.Scope.UNKNOWN, default = null): if category == PersisterEnums.Scope.UNKNOWN: category = _get_key_category(key) if category == PersisterEnums.Scope.UNKNOWN: return default if _persisted[category][key].is_valid_int(): return int(_persisted[category][key]) if _persisted[category][key] == "true" || _persisted[category][key] == "false": return _persisted[category][key] == "true" return _persisted[category][key] ## Delete data associated with a key from a certain category func clear_data(key: String, category := PersisterEnums.Scope.RUN) -> void: if not _persisted.has(category): _info("Attempted to clear key <%s> in category {%s} but the category did not exist" % [key, _get_category_name(category)]) return if not _persisted[category].has(key): _info("Attempted to clear key <%s> in category {%s} but it did not exist" % [key, _get_category_name(category)]) return _info("Cleared key <%s> in category {%s}" % [key, _get_category_name(category)]) _persisted[category].erase(key) _emit_removal(key, category) ## Delete a category including all data within it func clear_category(category := PersisterEnums.Scope.RUN) -> void: if not _persisted.has(category): _error("Attempted to clear category {%s} but it did not exist" % [_get_category_name(category)]) return var keys = _persisted[category].keys() _info("Cleared category {%s}" % [_get_category_name(category)]) _persisted.erase(category) for key in keys: _emit_removal(key, category) ## Add a number to a number value with the given key from a certain category func change_value(key: String, value: int, category := PersisterEnums.Scope.RUN) -> void: if not _persisted.has(category): _persisted[category] = {} if not _persisted[category].has(key): persist_data(key, value, category) return if is_nan(int(_persisted[category][key])): _error("Attempted to add number |%d| to key <%s> in category {%s} that is not a number (value: |%s|)" % [value, key, category, _persisted[category][key]]) return var old_value = int(_persisted[category][key]) _info("Added number |%d| to key <%s> in category {%s} (old: |%d|) (new: |%d|)" % [value, key, _get_category_name(category), old_value, old_value + value]) persist_data(key, value + old_value, category) func change_value_clamp_min(key: String, value: int, min: int, category := PersisterEnums.Scope.RUN) -> void: change_value_clamp(key, value, min, INF, category) func change_value_clamp_max(key: String, value: int, max: int, category := PersisterEnums.Scope.RUN) -> void: change_value_clamp(key, value, -INF, max, category) func change_value_clamp(key: String, value: int, min: int, max: int, category := PersisterEnums.Scope.RUN) -> void: if min > max and not min == -9223372036854775808 and not max == -9223372036854775808: _warn("Attempted to change clamp value %s with higher min %s than max %s" % [key, min, max]) return if not _persisted.has(category): _persisted[category] = {} if not _persisted[category].has(key): if value < min and not min == -9223372036854775808: value = min if value > max and not max == -9223372036854775808: value = max persist_data(key, value, category) return if is_nan(int(_persisted[category][key])): _error("Attempted to add number |%d| to key <%s> in category {%s} that is not a number (value: |%s|)" % [value, key, category, _persisted[category][key]]) return var old_value = int(_persisted[category][key]) var new_value = value + old_value if new_value < min and not min == -9223372036854775808: new_value = min if new_value > max and not max == -9223372036854775808: new_value = max if old_value == new_value: return _info("Added clamped number |%d| to key <%s> in category {%s} (old: |%d|) (new: |%d|)" % [value, key, _get_category_name(category), old_value, new_value]) persist_data(key, new_value, category) func change_save(index: int): _save() persist_data("save", index) _load() func save(): _save() func _emit_change(key: String, value: String, category: PersisterEnums.Scope) -> void: var key_category = _get_key_category(key) # Only emit change if the category set has the highest priority if key_category == category: if is_nan(int(value)): data_persisted.emit(key, value, category) else: data_persisted.emit(key, int(value), category) func _emit_removal(key: String, category: PersisterEnums.Scope) -> void: var key_category = _get_key_category(key) if key_category == PersisterEnums.Scope.UNKNOWN: return var category_priority = _get_category_priority(category) var key_category_priority = _get_category_priority(key_category) if category_priority > key_category_priority: var value = _persister[key_category][key] if is_nan(int(value)): data_persisted.emit(key, value, key_category) else: data_persisted.emit(key, int(value), key_category) func _get_category_name(category: PersisterEnums.Scope) -> String: match category: PersisterEnums.Scope.PERMANENT: return "permanent" PersisterEnums.Scope.SAVE: return "save" PersisterEnums.Scope.GAME: return "game" PersisterEnums.Scope.RUN: return "run" PersisterEnums.Scope.ROUND: return "round" PersisterEnums.Scope.ROOM: return "room" _: return "unknown" func _get_category_priority(category: PersisterEnums.Scope) -> int: match category: PersisterEnums.Scope.PERMANENT: return 1 PersisterEnums.Scope.SAVE: return 2 PersisterEnums.Scope.GAME: return 3 PersisterEnums.Scope.RUN: return 4 PersisterEnums.Scope.ROUND: return 5 PersisterEnums.Scope.ROOM: return 6 _: return 0 func _get_key_category(key: String) -> PersisterEnums.Scope: var category_order = [] for category in PersisterEnums.Scope: var category_value = PersisterEnums.Scope[category] category_order.push_back( { "priority": _get_category_priority(category_value), "category": category_value } ) category_order.sort_custom(func(a, b): return a.priority > b.priority ) for value in category_order: var category = value.category if _persisted.has(category): if _persisted[category].has(key): return category return PersisterEnums.Scope.UNKNOWN func _get_all_category_data(category: PersisterEnums.Scope) -> Dictionary: if not _persisted.has(category): return {} return _persisted[category] func _on_game_triggered(data: Dictionary) -> void: clear_category(PersisterEnums.Scope.GAME) func _on_run_triggered(data: Dictionary) -> void: clear_category(PersisterEnums.Scope.RUN) func _on_round_triggered(data: Dictionary) -> void: clear_category(PersisterEnums.Scope.ROUND) func _on_room_triggered(data: Dictionary) -> void: clear_category(PersisterEnums.Scope.ROOM) func _save(): _info("Saving") _save_permanent_data() _save_save_data() func _load(): _info("Loading") _load_permanent_data() if not get_value("save"): _info("Creating starting save") persist_data("save", 1, PersisterEnums.Scope.PERMANENT) clear_category(PersisterEnums.Scope.SAVE) _load_save_data() func _save_permanent_data(): var save_file = FileAccess.open("user://data.json", FileAccess.WRITE) var data = _get_all_category_data(PersisterEnums.Scope.PERMANENT) var data_string = JSON.stringify(data) _info("Saving permanent save data") save_file.store_line(data_string) func _save_save_data(): if not FileAccess.file_exists("user://saves"): DirAccess.make_dir_absolute("user://saves") var save = get_value("save") var save_file = FileAccess.open("user://saves/save-%d.json" % [save], FileAccess.WRITE) var data = _get_all_category_data(PersisterEnums.Scope.SAVE) var data_string = JSON.stringify(data) _info("Saving save save data") save_file.store_line(data_string) func _load_permanent_data(): var save_file = FileAccess.open("user://data.json", FileAccess.READ) if not save_file: return _info("Loading permanent save data") var json_string = "" while save_file.get_position() < save_file.get_length(): json_string += save_file.get_line() var json = JSON.new() var result = json.parse(json_string) var data = json.get_data() for key in data: Persister.persist_data(key, data[key], PersisterEnums.Scope.PERMANENT) func _load_save_data(): var save = get_value("save") var save_file = FileAccess.open("user://saves/save-%s.json" % [save], FileAccess.READ) if not save_file: return _info("Loading save save data") var json_string = "" while save_file.get_position() < save_file.get_length(): json_string += save_file.get_line() var json = JSON.new() var result = json.parse(json_string) var data = json.get_data() Persister.clear_category(PersisterEnums.Scope.SAVE) for key in data: Persister.persist_data(key, data[key], PersisterEnums.Scope.SAVE) func _on_timer_timeout(): _save()