class_name PersistentWorld extends Node ## Persistent World Data Object ## ## The [PersistentWorld] object holds information about the world. This ## information can be stored to encrypted save-files, and then loaded back at ## a later time. ## ## It's assumed this (or an extended script) is instanced as a singleton, ## preferably as an autoloaded script. ## ## Multiple instances of [PersistentWorld] objects can be created - for example ## to inspect other saved games without affecting the main instance. ## Signal invoked before loading world-data signal world_loading ## Signal invoked after loading world-data signal world_loaded ## Signal invoked before saving world-data signal world_saving ## Signal invoked after saving world-data signal world_saved @export_group("Persistence Settings") ## Password for encrypted save files @export var save_file_password := "" ## Database of all persistent zones in the game @export var zone_database : PersistentZoneDatabase ## Database of all persistent item types in the game @export var item_database : PersistentItemDatabase # World data dictionary var _data := {} # Mutex protecting data var _mutex := Mutex.new() ## Static instance of the world data static var instance : PersistentWorld = null # Check for configuration issues on this node func _get_configuration_warnings() -> PackedStringArray: var warnings := PackedStringArray() # Check for blank password if save_file_password == "": warnings.append("Save password not set - saves will be unencrypted") # Check for zone database if not zone_database: warnings.append("Zone database not set") # Check for item database if not item_database: warnings.append("Item database not set") # Return warnings return warnings ## This method creates a unique ID starting with [param base] follwed by a ## random number. The [param value] is stored using this ID, and the ID is ## returned to the caller. func set_unique(base : String, value : Variant) -> String: # Lock while trying to create the ID _mutex.lock() # Loop generating random IDs until we find a free one var id : String while true: id = base + str(randi() % 999999) if not _data.has(id): break # Save the value under the ID, then return the ID _data[id] = value _mutex.unlock() return id ## This method saves the [param value] under the [param id]. func set_value(id : String, value : Variant) -> void: _mutex.lock() _data[id] = value _mutex.unlock() ## This method gets the value stored under the [param id]. If the [param id] ## does not exist then the [param default] value is returned. func get_value(id : String, default : Variant = null): # -> Variant _mutex.lock() var value = _data.get(id, default) _mutex.unlock() return value ## This method clears a value under the [param id]. func clear_value(id : String) -> void: _mutex.lock() _data.erase(id) _mutex.unlock() ## This method clears all values matching the glob [param pattern]. See ## [method String.match] for pattern matching rules. func clear_matching(pattern : String) -> void: _mutex.lock() for _key in _data.keys(): var key : String = _key if key.match(pattern): _data.erase(key) _mutex.unlock() ## This method clears all values. func clear_all() -> void: _mutex.lock() _data.clear() _mutex.unlock() ## This method loads the summary information for the saved world-data ## associated with the specified [param file_name]. If the world-data does ## not exist then this method returns null. func load_summary(file_name : String) -> Variant: # Open the world-data save file for reading var file := _open_file(file_name, FileAccess.READ) if not file: return null # Read the summary var summary = file.get_var() file.close() # Return the summary return summary ## This method loads the world-data associated with the specified ## [param file_name]. If the world-data does not exist or is invalid then ## this method returns false. func load_file(file_name : String) -> bool: # Report start of world-data loading world_loading.emit() # Open the world-data save file for reading var file := _open_file(file_name, FileAccess.READ) if not file: return false # Skip the summary file.get_var() # Read the data var new_data = file.get_var() file.close() # Skip if not dictionary if not new_data is Dictionary: return false # Use the new data _mutex.lock() _data = new_data _mutex.unlock() # Report world-data loaded world_loaded.emit() # Report success return true ## This method saves the world-data under the specified [param file_name]. ## If the save fails then this method returns false. The [param file_name] ## string must be a legal part of a file name. func save_file(file_name : String, summary : Variant) -> bool: # Report start of world-data saving world_saving.emit() # Open the world-data save file for writing var file := _open_file(file_name, FileAccess.WRITE) if not file: return false # Write the summary file.store_var(summary) # Write the data _mutex.lock() file.store_var(_data) _mutex.unlock() # Close the file file.close() # Report world-data saved world_saved.emit() return true ## This method deletes the world-data associated with the specified ## [param file_name]. If the world-data does not exist then this method ## returns false. static func delete_file(file_name : String) -> bool: # Construct the file name var file_path := "user://save_%s.data" % file_name # Remove the file return DirAccess.remove_absolute(file_path) == OK ## This method returns a list of the names of all the saved world-data ## instances. func list_saves() -> Array[String]: # Construct the return list var ret : Array[String] = [] # Build a regular expression to match save file names var regex := RegEx.new() regex.compile("^save_(?.*)\\.data$") # Process all files in the user folder for file in DirAccess.get_files_at("user://"): var result := regex.search(file) if result: ret.append(result.get_string("name")) # Return the save files return ret # Open a world-data save file. func _open_file(file_name : String, mode : FileAccess.ModeFlags) -> FileAccess: # Construct the file name var file_path := "user://save_%s.data" % file_name # Warn about unencrypted save files for debugging if save_file_password == "": push_warning("Unencrypted save file: ", file_path) return FileAccess.open(file_path, mode) # Handle encrypted file with password return FileAccess.open_encrypted_with_pass(file_path, mode, save_file_password)