Add saves, settings, and multiplayer-safe menus
This commit is contained in:
parent
e2067e3bf4
commit
0d050b08cc
32 changed files with 2468 additions and 49 deletions
175
settings/player_settings_manager.gd
Normal file
175
settings/player_settings_manager.gd
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue