130 lines
4.9 KiB
GDScript
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
|