GodotVR/components/persistent/persistent_world.gd
2025-10-08 11:10:13 +02:00

259 lines
6.4 KiB
GDScript

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_(?<name>.*)\\.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)