175 lines
5.3 KiB
GDScript
175 lines
5.3 KiB
GDScript
class_name PlayerSettingsManager
|
|
extends Node
|
|
|
|
const SETTINGS_VERSION: int = 1
|
|
const SETTINGS_PATH: String = "user://player_settings.json"
|
|
const TEMP_PATH: String = "user://player_settings.json.tmp"
|
|
const BACKUP_PATH: String = "user://player_settings.json.backup"
|
|
|
|
signal settings_changed(settings: PlayerSettings)
|
|
|
|
var current_settings: PlayerSettings = PlayerSettings.new()
|
|
var _is_dirty: bool = false
|
|
|
|
|
|
func load_settings() -> bool:
|
|
_recover_interrupted_write()
|
|
if not FileAccess.file_exists(SETTINGS_PATH):
|
|
current_settings = PlayerSettings.new()
|
|
emit_signal("settings_changed", current_settings)
|
|
return true
|
|
var file := FileAccess.open(SETTINGS_PATH, FileAccess.READ)
|
|
if file == null:
|
|
push_warning("Unable to open player settings; using defaults.")
|
|
current_settings = PlayerSettings.new()
|
|
emit_signal("settings_changed", current_settings)
|
|
return false
|
|
var json := JSON.new()
|
|
var error: Error = json.parse(file.get_as_text())
|
|
file.close()
|
|
if error != OK or typeof(json.data) != TYPE_DICTIONARY:
|
|
return _use_defaults_after_corruption("Player settings JSON is malformed.")
|
|
var data: Dictionary = json.data
|
|
if _read_integer(data.get("settings_version"), -1) != SETTINGS_VERSION:
|
|
return _use_defaults_after_corruption("Player settings version is unsupported.")
|
|
if (
|
|
typeof(data.get("accessibility")) != TYPE_DICTIONARY
|
|
or typeof(data.get("camera")) != TYPE_DICTIONARY
|
|
):
|
|
return _use_defaults_after_corruption("Player settings structure is invalid.")
|
|
var accessibility: Dictionary = data["accessibility"]
|
|
var camera: Dictionary = data["camera"]
|
|
if (
|
|
typeof(accessibility.get("auto_click_enabled")) != TYPE_BOOL
|
|
or typeof(camera.get("invert_vertical")) != TYPE_BOOL
|
|
):
|
|
return _use_defaults_after_corruption("Player settings values are invalid.")
|
|
var loaded := PlayerSettings.new()
|
|
loaded.auto_click_enabled = accessibility["auto_click_enabled"]
|
|
loaded.auto_click_interval = _read_float(
|
|
accessibility.get("auto_click_interval"),
|
|
-1.0
|
|
)
|
|
loaded.mouse_camera_sensitivity = _read_float(
|
|
camera.get("mouse_sensitivity"),
|
|
-1.0
|
|
)
|
|
loaded.controller_camera_sensitivity = _read_float(
|
|
camera.get("controller_sensitivity"),
|
|
-1.0
|
|
)
|
|
loaded.invert_camera_y = camera["invert_vertical"]
|
|
if not loaded.is_valid():
|
|
return _use_defaults_after_corruption("Player settings values are invalid.")
|
|
current_settings = loaded
|
|
_is_dirty = false
|
|
emit_signal("settings_changed", current_settings)
|
|
return true
|
|
|
|
|
|
func apply_settings(settings: PlayerSettings) -> bool:
|
|
if settings == null or not settings.is_valid():
|
|
return false
|
|
var previous: PlayerSettings = current_settings
|
|
current_settings = settings.copy()
|
|
_is_dirty = true
|
|
if not save_now():
|
|
current_settings = previous
|
|
return false
|
|
emit_signal("settings_changed", current_settings)
|
|
return true
|
|
|
|
|
|
func save_now() -> bool:
|
|
if current_settings == null or not current_settings.is_valid():
|
|
return false
|
|
var data: Dictionary = {
|
|
"settings_version": SETTINGS_VERSION,
|
|
"accessibility": {
|
|
"auto_click_enabled": current_settings.auto_click_enabled,
|
|
"auto_click_interval": current_settings.auto_click_interval,
|
|
},
|
|
"camera": {
|
|
"mouse_sensitivity": current_settings.mouse_camera_sensitivity,
|
|
"controller_sensitivity": current_settings.controller_camera_sensitivity,
|
|
"invert_vertical": current_settings.invert_camera_y,
|
|
},
|
|
}
|
|
var file := FileAccess.open(TEMP_PATH, FileAccess.WRITE)
|
|
if file == null:
|
|
return false
|
|
file.store_string(JSON.stringify(data, "\t"))
|
|
file.flush()
|
|
var write_error: Error = file.get_error()
|
|
file.close()
|
|
if write_error != OK:
|
|
_remove_if_present(TEMP_PATH)
|
|
return false
|
|
var had_primary: bool = FileAccess.file_exists(SETTINGS_PATH)
|
|
_remove_if_present(BACKUP_PATH)
|
|
if had_primary and not _rename_file(SETTINGS_PATH, BACKUP_PATH):
|
|
_remove_if_present(TEMP_PATH)
|
|
return false
|
|
if not _rename_file(TEMP_PATH, SETTINGS_PATH):
|
|
if had_primary:
|
|
_rename_file(BACKUP_PATH, SETTINGS_PATH)
|
|
return false
|
|
_remove_if_present(BACKUP_PATH)
|
|
_is_dirty = false
|
|
return true
|
|
|
|
|
|
func save_if_dirty() -> bool:
|
|
return not _is_dirty or save_now()
|
|
|
|
|
|
func _exit_tree() -> void:
|
|
save_if_dirty()
|
|
|
|
|
|
func _use_defaults_after_corruption(message: String) -> bool:
|
|
push_warning(message)
|
|
current_settings = PlayerSettings.new()
|
|
_is_dirty = false
|
|
emit_signal("settings_changed", current_settings)
|
|
return false
|
|
|
|
|
|
func _recover_interrupted_write() -> void:
|
|
if FileAccess.file_exists(SETTINGS_PATH):
|
|
_remove_if_present(TEMP_PATH)
|
|
_remove_if_present(BACKUP_PATH)
|
|
return
|
|
if FileAccess.file_exists(BACKUP_PATH):
|
|
_rename_file(BACKUP_PATH, SETTINGS_PATH)
|
|
_remove_if_present(TEMP_PATH)
|
|
|
|
|
|
func _read_integer(value: Variant, fallback: int) -> int:
|
|
if typeof(value) == TYPE_INT:
|
|
return int(value)
|
|
if typeof(value) == TYPE_FLOAT and is_equal_approx(value, round(value)):
|
|
return int(round(value))
|
|
return fallback
|
|
|
|
|
|
func _read_float(value: Variant, fallback: float) -> float:
|
|
if typeof(value) not in [TYPE_FLOAT, TYPE_INT]:
|
|
return fallback
|
|
var result: float = float(value)
|
|
return result if is_finite(result) else fallback
|
|
|
|
|
|
func _rename_file(from_path: String, to_path: String) -> bool:
|
|
return DirAccess.rename_absolute(
|
|
ProjectSettings.globalize_path(from_path),
|
|
ProjectSettings.globalize_path(to_path)
|
|
) == OK
|
|
|
|
|
|
func _remove_if_present(path: String) -> bool:
|
|
if not FileAccess.file_exists(path):
|
|
return true
|
|
return DirAccess.remove_absolute(
|
|
ProjectSettings.globalize_path(path)
|
|
) == OK
|