Files
aetherbound-guild/runtime/entry/settings_store.gd
T

130 lines
4.9 KiB
GDScript

class_name ABGEntrySettingsStore
extends RefCounted
const SETTINGS_VERSION := 2
const DEFAULT_PATH := "user://aetherbound_guild_settings.json"
const TEXT_SCALES := [1.0, 1.15, 1.3]
const SAFE_AREA_PRESETS := [0, 24, 48]
var path := DEFAULT_PATH
func defaults() -> Dictionary:
return {
"settings_version": SETTINGS_VERSION,
"setup_completed": false,
"locale": "zh_CN",
"safe_area": 24,
"text_scale": 1.0,
"brightness": 1.0,
"color_cues": true,
"reduced_motion": false,
"reduced_flashes": false,
"shake_intensity": 1.0,
"hit_pause": true,
"particle_density": "standard",
"captions": true,
"high_contrast": false,
"low_power": false,
"master_muted": false,
"music_volume": 0.8,
"ambience_volume": 0.8,
"sfx_volume": 0.9,
"voice_volume": 0.8,
"dynamic_range": "standard",
"caption_detail": "critical",
"hold_duration": 0.6,
"vibration": true,
}.duplicate(true)
func load_settings() -> Dictionary:
if not FileAccess.file_exists(path):
return {"ok": true, "code": "defaults", "settings": defaults()}
var parser := JSON.new()
var parse_error := parser.parse(FileAccess.get_file_as_string(path))
if parse_error != OK or not parser.data is Dictionary:
return {"ok": false, "code": "SETTINGS_FILE_INVALID"}
if int(parser.data.get("settings_version", 0)) == 1:
var migrated := defaults()
for key in parser.data.keys():
if migrated.has(key):
migrated[key] = parser.data[key]
migrated.settings_version = SETTINGS_VERSION
var migration_save := save_settings(migrated)
if not migration_save.ok:
return {"ok": false, "code": "SETTINGS_MIGRATION_FAILED"}
return {"ok": true, "code": "migrated_v1", "settings": migrated}
var validation := validate(parser.data)
if not validation.ok:
return validation
return {"ok": true, "code": "loaded", "settings": parser.data.duplicate(true)}
func save_settings(value: Dictionary) -> Dictionary:
var validation := validate(value)
if not validation.ok:
return validation
var candidate_path := "%s.candidate" % path
var candidate := FileAccess.open(candidate_path, FileAccess.WRITE)
if candidate == null:
return {"ok": false, "code": "SETTINGS_WRITE_UNAVAILABLE"}
candidate.store_string(JSON.stringify(value))
candidate.close()
var verify_parser := JSON.new()
if verify_parser.parse(FileAccess.get_file_as_string(candidate_path)) != OK or not verify_parser.data is Dictionary or not validate(verify_parser.data).ok:
DirAccess.remove_absolute(candidate_path)
return {"ok": false, "code": "SETTINGS_WRITE_VERIFY_FAILED"}
var backup_path := "%s.bak" % path
if FileAccess.file_exists(backup_path):
DirAccess.remove_absolute(backup_path)
if FileAccess.file_exists(path):
var backup_error := DirAccess.rename_absolute(path, backup_path)
if backup_error != OK:
DirAccess.remove_absolute(candidate_path)
return {"ok": false, "code": "SETTINGS_BACKUP_FAILED"}
var promote_error := DirAccess.rename_absolute(candidate_path, path)
if promote_error != OK:
if FileAccess.file_exists(backup_path) and not FileAccess.file_exists(path):
DirAccess.rename_absolute(backup_path, path)
return {"ok": false, "code": "SETTINGS_PROMOTE_FAILED"}
return {"ok": true, "code": "saved", "settings": value.duplicate(true)}
func validate(value: Dictionary) -> Dictionary:
var required := defaults().keys()
for key in required:
if not value.has(key):
return {"ok": false, "code": "SETTINGS_FIELD_MISSING"}
if int(value.settings_version) != SETTINGS_VERSION:
return {"ok": false, "code": "SETTINGS_VERSION_UNSUPPORTED"}
if String(value.locale) not in ["en", "zh_CN"]:
return {"ok": false, "code": "SETTINGS_LOCALE_INVALID"}
if int(value.safe_area) not in SAFE_AREA_PRESETS:
return {"ok": false, "code": "SETTINGS_SAFE_AREA_INVALID"}
if not _float_in(float(value.text_scale), TEXT_SCALES):
return {"ok": false, "code": "SETTINGS_TEXT_SCALE_INVALID"}
if String(value.dynamic_range) not in ["wide", "standard", "night"]:
return {"ok": false, "code": "SETTINGS_DYNAMIC_RANGE_INVALID"}
if String(value.caption_detail) not in ["critical", "full"]:
return {"ok": false, "code": "SETTINGS_CAPTION_DETAIL_INVALID"}
if String(value.particle_density) not in ["low", "standard"]:
return {"ok": false, "code": "SETTINGS_PARTICLES_INVALID"}
if float(value.brightness) < 0.7 or float(value.brightness) > 1.2:
return {"ok": false, "code": "SETTINGS_BRIGHTNESS_INVALID"}
if float(value.shake_intensity) < 0.0 or float(value.shake_intensity) > 1.0:
return {"ok": false, "code": "SETTINGS_SHAKE_INVALID"}
for key in ["music_volume", "ambience_volume", "sfx_volume", "voice_volume"]:
if float(value[key]) < 0.0 or float(value[key]) > 1.0:
return {"ok": false, "code": "SETTINGS_VOLUME_INVALID"}
if float(value.hold_duration) < 0.3 or float(value.hold_duration) > 1.5:
return {"ok": false, "code": "SETTINGS_HOLD_INVALID"}
return {"ok": true, "code": "valid"}
func _float_in(value: float, options: Array) -> bool:
for option in options:
if is_equal_approx(value, float(option)):
return true
return false