540 lines
15 KiB
GDScript3
540 lines
15 KiB
GDScript3
|
|
class_name PlayerSaveManager
|
||
|
|
extends Node
|
||
|
|
|
||
|
|
const FishCatchType = preload("res://fish/fish_catch.gd")
|
||
|
|
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
||
|
|
const CollectionLogType = preload("res://collection/collection_log.gd")
|
||
|
|
const PlayerWalletType = preload("res://economy/player_wallet.gd")
|
||
|
|
const FishPoolType = preload("res://fish/fish_pool.gd")
|
||
|
|
const SaveInspectionType = preload("res://save/player_save_inspection.gd")
|
||
|
|
|
||
|
|
const SAVE_VERSION: int = 1
|
||
|
|
const SAVE_PATH: String = "user://player_save.json"
|
||
|
|
const TEMP_PATH: String = "user://player_save.json.tmp"
|
||
|
|
const BACKUP_PATH: String = "user://player_save.json.backup"
|
||
|
|
const MAX_SAFE_BALANCE: int = 1000000000000
|
||
|
|
|
||
|
|
class LoadSnapshot:
|
||
|
|
extends RefCounted
|
||
|
|
|
||
|
|
var catches: Array[FishCatchType] = []
|
||
|
|
var discovered_ids: Array[StringName] = []
|
||
|
|
var wallet_balance: int = 0
|
||
|
|
var next_catch_sequence: int = 1
|
||
|
|
|
||
|
|
|
||
|
|
@export_range(0.05, 5.0, 0.05) var autosave_delay: float = 0.5
|
||
|
|
|
||
|
|
var _inventory: FishInventoryType
|
||
|
|
var _collection_log: CollectionLogType
|
||
|
|
var _wallet: PlayerWalletType
|
||
|
|
var _catalog: FishPoolType
|
||
|
|
var _autosave_timer: Timer
|
||
|
|
var _is_configured: bool = false
|
||
|
|
var _is_restoring: bool = false
|
||
|
|
var _is_dirty: bool = false
|
||
|
|
var _automatic_saving_blocked: bool = false
|
||
|
|
var _autosave_enabled: bool = false
|
||
|
|
|
||
|
|
|
||
|
|
func _ready() -> void:
|
||
|
|
_autosave_timer = Timer.new()
|
||
|
|
_autosave_timer.one_shot = true
|
||
|
|
_autosave_timer.timeout.connect(_on_autosave_timeout)
|
||
|
|
add_child(_autosave_timer)
|
||
|
|
|
||
|
|
|
||
|
|
func setup(
|
||
|
|
inventory: FishInventoryType,
|
||
|
|
collection_log: CollectionLogType,
|
||
|
|
wallet: PlayerWalletType,
|
||
|
|
catalog: FishPoolType,
|
||
|
|
) -> void:
|
||
|
|
_inventory = inventory
|
||
|
|
_collection_log = collection_log
|
||
|
|
_wallet = wallet
|
||
|
|
_catalog = catalog
|
||
|
|
_is_configured = (
|
||
|
|
_inventory != null
|
||
|
|
and _collection_log != null
|
||
|
|
and _wallet != null
|
||
|
|
and _catalog != null
|
||
|
|
)
|
||
|
|
if not _is_configured:
|
||
|
|
push_error("PlayerSaveManager setup is missing required references.")
|
||
|
|
return
|
||
|
|
if not _inventory.catches_changed.is_connected(_mark_dirty):
|
||
|
|
_inventory.catches_changed.connect(_mark_dirty)
|
||
|
|
if not _collection_log.fish_discovered.is_connected(
|
||
|
|
_on_fish_discovered
|
||
|
|
):
|
||
|
|
_collection_log.fish_discovered.connect(_on_fish_discovered)
|
||
|
|
if not _wallet.balance_changed.is_connected(_on_balance_changed):
|
||
|
|
_wallet.balance_changed.connect(_on_balance_changed)
|
||
|
|
|
||
|
|
|
||
|
|
func load_player_data() -> bool:
|
||
|
|
if not _is_configured:
|
||
|
|
return false
|
||
|
|
_automatic_saving_blocked = false
|
||
|
|
_recover_interrupted_write()
|
||
|
|
if not FileAccess.file_exists(SAVE_PATH):
|
||
|
|
_is_dirty = false
|
||
|
|
return true
|
||
|
|
|
||
|
|
var save_file := FileAccess.open(SAVE_PATH, FileAccess.READ)
|
||
|
|
if save_file == null:
|
||
|
|
_handle_corrupt_save("Unable to open player save.")
|
||
|
|
return false
|
||
|
|
var json_text: String = save_file.get_as_text()
|
||
|
|
save_file.close()
|
||
|
|
|
||
|
|
var json := JSON.new()
|
||
|
|
var parse_error: Error = json.parse(json_text)
|
||
|
|
if parse_error != OK or typeof(json.data) != TYPE_DICTIONARY:
|
||
|
|
_handle_corrupt_save("Player save contains malformed JSON.")
|
||
|
|
return false
|
||
|
|
var save_data: Dictionary = json.data
|
||
|
|
var version: int = _read_integer(save_data.get("save_version"), -1)
|
||
|
|
if version > SAVE_VERSION:
|
||
|
|
_automatic_saving_blocked = true
|
||
|
|
push_warning(
|
||
|
|
(
|
||
|
|
"Player save version %d is newer than supported "
|
||
|
|
+ "version %d; the file was left untouched."
|
||
|
|
)
|
||
|
|
% [version, SAVE_VERSION]
|
||
|
|
)
|
||
|
|
_restore_defaults()
|
||
|
|
return false
|
||
|
|
if version != SAVE_VERSION:
|
||
|
|
save_data = _migrate_save(save_data, version)
|
||
|
|
if save_data.is_empty():
|
||
|
|
_handle_corrupt_save(
|
||
|
|
"Player save version %d is unsupported." % version
|
||
|
|
)
|
||
|
|
return false
|
||
|
|
|
||
|
|
var snapshot: LoadSnapshot = _build_load_snapshot(save_data)
|
||
|
|
if snapshot == null:
|
||
|
|
_handle_corrupt_save("Player save failed structural validation.")
|
||
|
|
return false
|
||
|
|
|
||
|
|
_is_restoring = true
|
||
|
|
var inventory_restored: bool = _inventory.replace_all_catches(
|
||
|
|
snapshot.catches,
|
||
|
|
snapshot.next_catch_sequence
|
||
|
|
)
|
||
|
|
var collection_restored: bool = (
|
||
|
|
_collection_log.replace_discovered_ids(snapshot.discovered_ids)
|
||
|
|
)
|
||
|
|
var wallet_restored: bool = _wallet.restore_balance(
|
||
|
|
snapshot.wallet_balance
|
||
|
|
)
|
||
|
|
_is_restoring = false
|
||
|
|
if (
|
||
|
|
not inventory_restored
|
||
|
|
or not collection_restored
|
||
|
|
or not wallet_restored
|
||
|
|
):
|
||
|
|
push_error("Validated player save could not be restored.")
|
||
|
|
return false
|
||
|
|
|
||
|
|
_is_dirty = false
|
||
|
|
print(
|
||
|
|
"Loaded player save version %d with %d catches."
|
||
|
|
% [SAVE_VERSION, snapshot.catches.size()]
|
||
|
|
)
|
||
|
|
return true
|
||
|
|
|
||
|
|
|
||
|
|
func inspect_save() -> SaveInspectionType:
|
||
|
|
var result := SaveInspectionType.new()
|
||
|
|
_recover_interrupted_write()
|
||
|
|
result.has_primary_file = FileAccess.file_exists(SAVE_PATH)
|
||
|
|
if not result.has_primary_file:
|
||
|
|
result.status = SaveInspectionType.Status.MISSING
|
||
|
|
result.message = "No save found."
|
||
|
|
return result
|
||
|
|
var save_file := FileAccess.open(SAVE_PATH, FileAccess.READ)
|
||
|
|
if save_file == null:
|
||
|
|
result.status = SaveInspectionType.Status.IO_ERROR
|
||
|
|
result.message = "The save could not be read."
|
||
|
|
return result
|
||
|
|
var json := JSON.new()
|
||
|
|
var parse_error: Error = json.parse(save_file.get_as_text())
|
||
|
|
save_file.close()
|
||
|
|
if parse_error != OK or typeof(json.data) != TYPE_DICTIONARY:
|
||
|
|
result.status = SaveInspectionType.Status.MALFORMED
|
||
|
|
result.message = "The save is corrupt and was preserved."
|
||
|
|
return result
|
||
|
|
var save_data: Dictionary = json.data
|
||
|
|
result.detected_version = _read_integer(save_data.get("save_version"), -1)
|
||
|
|
if result.detected_version > SAVE_VERSION:
|
||
|
|
result.status = SaveInspectionType.Status.UNSUPPORTED_VERSION
|
||
|
|
result.message = "This save belongs to a newer game version."
|
||
|
|
return result
|
||
|
|
if result.detected_version != SAVE_VERSION:
|
||
|
|
result.status = SaveInspectionType.Status.MALFORMED
|
||
|
|
result.message = "The save version is unsupported."
|
||
|
|
return result
|
||
|
|
var snapshot: LoadSnapshot = _build_load_snapshot(save_data)
|
||
|
|
if snapshot == null:
|
||
|
|
result.status = SaveInspectionType.Status.MALFORMED
|
||
|
|
result.message = "The save is structurally invalid and was preserved."
|
||
|
|
return result
|
||
|
|
result.status = SaveInspectionType.Status.VALID_SUPPORTED
|
||
|
|
result.catch_count = snapshot.catches.size()
|
||
|
|
result.wallet_balance = snapshot.wallet_balance
|
||
|
|
result.discovered_species_count = snapshot.discovered_ids.size()
|
||
|
|
result.message = "Save ready."
|
||
|
|
return result
|
||
|
|
|
||
|
|
|
||
|
|
func initialize_new_game() -> bool:
|
||
|
|
if not _is_configured:
|
||
|
|
return false
|
||
|
|
_automatic_saving_blocked = false
|
||
|
|
_restore_defaults()
|
||
|
|
return true
|
||
|
|
|
||
|
|
|
||
|
|
func delete_progression_save() -> bool:
|
||
|
|
if FileAccess.file_exists(SAVE_PATH) and not _remove_if_present(SAVE_PATH):
|
||
|
|
return false
|
||
|
|
for path: String in [TEMP_PATH, BACKUP_PATH]:
|
||
|
|
if FileAccess.file_exists(path) and not _remove_if_present(path):
|
||
|
|
push_warning("Unable to remove stale player-save auxiliary file.")
|
||
|
|
if _autosave_timer != null:
|
||
|
|
_autosave_timer.stop()
|
||
|
|
_is_dirty = false
|
||
|
|
_automatic_saving_blocked = false
|
||
|
|
return true
|
||
|
|
|
||
|
|
|
||
|
|
func set_autosave_enabled(enabled: bool) -> void:
|
||
|
|
_autosave_enabled = enabled
|
||
|
|
if not enabled and _autosave_timer != null:
|
||
|
|
_autosave_timer.stop()
|
||
|
|
|
||
|
|
|
||
|
|
func save_if_dirty() -> bool:
|
||
|
|
return not _is_dirty or save_now()
|
||
|
|
|
||
|
|
|
||
|
|
func is_dirty() -> bool:
|
||
|
|
return _is_dirty
|
||
|
|
|
||
|
|
|
||
|
|
func cancel_pending_autosave() -> void:
|
||
|
|
if _autosave_timer != null:
|
||
|
|
_autosave_timer.stop()
|
||
|
|
_is_dirty = false
|
||
|
|
|
||
|
|
|
||
|
|
func save_now() -> bool:
|
||
|
|
if (
|
||
|
|
not _is_configured
|
||
|
|
or _automatic_saving_blocked
|
||
|
|
or not _autosave_enabled
|
||
|
|
):
|
||
|
|
return false
|
||
|
|
var save_data: Dictionary = _build_save_dictionary()
|
||
|
|
if save_data.is_empty():
|
||
|
|
return false
|
||
|
|
var json_text: String = JSON.stringify(save_data, "\t")
|
||
|
|
if json_text.is_empty():
|
||
|
|
return false
|
||
|
|
|
||
|
|
var temp_file := FileAccess.open(TEMP_PATH, FileAccess.WRITE)
|
||
|
|
if temp_file == null:
|
||
|
|
push_warning("Unable to open temporary player save for writing.")
|
||
|
|
return false
|
||
|
|
temp_file.store_string(json_text)
|
||
|
|
temp_file.flush()
|
||
|
|
var write_error: Error = temp_file.get_error()
|
||
|
|
temp_file.close()
|
||
|
|
if write_error != OK:
|
||
|
|
_remove_if_present(TEMP_PATH)
|
||
|
|
push_warning("Unable to finish writing temporary player save.")
|
||
|
|
return false
|
||
|
|
|
||
|
|
var had_primary: bool = FileAccess.file_exists(SAVE_PATH)
|
||
|
|
_remove_if_present(BACKUP_PATH)
|
||
|
|
if had_primary and not _rename_file(SAVE_PATH, BACKUP_PATH):
|
||
|
|
_remove_if_present(TEMP_PATH)
|
||
|
|
push_warning("Unable to preserve the previous player save.")
|
||
|
|
return false
|
||
|
|
if not _rename_file(TEMP_PATH, SAVE_PATH):
|
||
|
|
if had_primary:
|
||
|
|
_rename_file(BACKUP_PATH, SAVE_PATH)
|
||
|
|
push_warning("Unable to promote the temporary player save.")
|
||
|
|
return false
|
||
|
|
_remove_if_present(BACKUP_PATH)
|
||
|
|
_is_dirty = false
|
||
|
|
return true
|
||
|
|
|
||
|
|
|
||
|
|
func delete_save_for_debug() -> bool:
|
||
|
|
return delete_progression_save()
|
||
|
|
|
||
|
|
|
||
|
|
func _exit_tree() -> void:
|
||
|
|
if _is_dirty and _autosave_enabled and not _automatic_saving_blocked:
|
||
|
|
save_now()
|
||
|
|
|
||
|
|
|
||
|
|
func _build_save_dictionary() -> Dictionary:
|
||
|
|
var serialized_catches: Array[Dictionary] = []
|
||
|
|
for fish_catch: FishCatchType in _inventory.get_all_catches():
|
||
|
|
if fish_catch == null or not fish_catch.is_valid():
|
||
|
|
push_warning(
|
||
|
|
"Player save aborted because inventory contains an "
|
||
|
|
+ "invalid runtime catch."
|
||
|
|
)
|
||
|
|
return {}
|
||
|
|
serialized_catches.append(fish_catch.to_save_dict())
|
||
|
|
|
||
|
|
var discovered_strings: Array[String] = []
|
||
|
|
for fish_id: StringName in _collection_log.get_discovered_ids():
|
||
|
|
if fish_id.is_empty():
|
||
|
|
return {}
|
||
|
|
discovered_strings.append(String(fish_id))
|
||
|
|
if (
|
||
|
|
_wallet.get_balance() < 0
|
||
|
|
or _wallet.get_balance() > MAX_SAFE_BALANCE
|
||
|
|
or _inventory.get_next_catch_sequence() < 1
|
||
|
|
or (
|
||
|
|
_inventory.get_next_catch_sequence()
|
||
|
|
> FishCatchType.MAX_SAFE_SEQUENCE
|
||
|
|
)
|
||
|
|
):
|
||
|
|
return {}
|
||
|
|
|
||
|
|
return {
|
||
|
|
"save_version": SAVE_VERSION,
|
||
|
|
"wallet": {
|
||
|
|
"balance": _wallet.get_balance(),
|
||
|
|
},
|
||
|
|
"collection": {
|
||
|
|
"discovered_fish_ids": discovered_strings,
|
||
|
|
},
|
||
|
|
"inventory": {
|
||
|
|
"next_catch_sequence": _inventory.get_next_catch_sequence(),
|
||
|
|
"catches": serialized_catches,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
|
||
|
|
if (
|
||
|
|
typeof(save_data.get("wallet")) != TYPE_DICTIONARY
|
||
|
|
or typeof(save_data.get("collection")) != TYPE_DICTIONARY
|
||
|
|
or typeof(save_data.get("inventory")) != TYPE_DICTIONARY
|
||
|
|
):
|
||
|
|
return null
|
||
|
|
var wallet_data: Dictionary = save_data["wallet"]
|
||
|
|
var collection_data: Dictionary = save_data["collection"]
|
||
|
|
var inventory_data: Dictionary = save_data["inventory"]
|
||
|
|
if (
|
||
|
|
not wallet_data.has("balance")
|
||
|
|
or typeof(collection_data.get("discovered_fish_ids")) != TYPE_ARRAY
|
||
|
|
or typeof(inventory_data.get("catches")) != TYPE_ARRAY
|
||
|
|
or not inventory_data.has("next_catch_sequence")
|
||
|
|
):
|
||
|
|
return null
|
||
|
|
|
||
|
|
var balance: int = _read_integer(
|
||
|
|
wallet_data["balance"],
|
||
|
|
-1,
|
||
|
|
MAX_SAFE_BALANCE
|
||
|
|
)
|
||
|
|
var requested_next_sequence: int = _read_integer(
|
||
|
|
inventory_data["next_catch_sequence"],
|
||
|
|
-1,
|
||
|
|
FishCatchType.MAX_SAFE_SEQUENCE
|
||
|
|
)
|
||
|
|
if balance < 0 or requested_next_sequence < 1:
|
||
|
|
return null
|
||
|
|
|
||
|
|
var snapshot := LoadSnapshot.new()
|
||
|
|
snapshot.wallet_balance = balance
|
||
|
|
var discovered_values: Array = collection_data["discovered_fish_ids"]
|
||
|
|
var seen_discoveries: Dictionary[StringName, bool] = {}
|
||
|
|
for value: Variant in discovered_values:
|
||
|
|
if typeof(value) != TYPE_STRING and typeof(value) != TYPE_STRING_NAME:
|
||
|
|
continue
|
||
|
|
var fish_id: StringName = StringName(str(value))
|
||
|
|
if fish_id.is_empty() or seen_discoveries.has(fish_id):
|
||
|
|
continue
|
||
|
|
seen_discoveries[fish_id] = true
|
||
|
|
snapshot.discovered_ids.append(fish_id)
|
||
|
|
|
||
|
|
var seen_ids: Dictionary[StringName, bool] = {}
|
||
|
|
var seen_sequences: Dictionary[int, bool] = {}
|
||
|
|
var maximum_sequence: int = 0
|
||
|
|
var catch_values: Array = inventory_data["catches"]
|
||
|
|
for catch_index: int in range(catch_values.size()):
|
||
|
|
var catch_value: Variant = catch_values[catch_index]
|
||
|
|
if typeof(catch_value) != TYPE_DICTIONARY:
|
||
|
|
push_warning(
|
||
|
|
"Skipped invalid saved catch at index %d." % catch_index
|
||
|
|
)
|
||
|
|
continue
|
||
|
|
var catch_data: Dictionary = catch_value
|
||
|
|
var fish_id: StringName = StringName(str(catch_data.get("fish_id", "")))
|
||
|
|
var fish_data = _catalog.get_fish_by_id(fish_id)
|
||
|
|
if fish_data == null:
|
||
|
|
push_warning(
|
||
|
|
"Skipped saved catch with missing fish ID '%s'."
|
||
|
|
% String(fish_id)
|
||
|
|
)
|
||
|
|
continue
|
||
|
|
var fish_catch: FishCatchType = FishCatchType.from_save_dict(
|
||
|
|
catch_data,
|
||
|
|
fish_data
|
||
|
|
)
|
||
|
|
if fish_catch == null:
|
||
|
|
push_warning(
|
||
|
|
"Skipped structurally invalid saved catch at index %d."
|
||
|
|
% catch_index
|
||
|
|
)
|
||
|
|
continue
|
||
|
|
if seen_ids.has(fish_catch.catch_id):
|
||
|
|
push_warning(
|
||
|
|
"Skipped duplicate saved catch ID '%s'."
|
||
|
|
% String(fish_catch.catch_id)
|
||
|
|
)
|
||
|
|
continue
|
||
|
|
if seen_sequences.has(fish_catch.catch_sequence):
|
||
|
|
push_warning(
|
||
|
|
"Skipped duplicate saved catch sequence %d."
|
||
|
|
% fish_catch.catch_sequence
|
||
|
|
)
|
||
|
|
continue
|
||
|
|
seen_ids[fish_catch.catch_id] = true
|
||
|
|
seen_sequences[fish_catch.catch_sequence] = true
|
||
|
|
maximum_sequence = maxi(
|
||
|
|
maximum_sequence,
|
||
|
|
fish_catch.catch_sequence
|
||
|
|
)
|
||
|
|
snapshot.catches.append(fish_catch)
|
||
|
|
|
||
|
|
snapshot.next_catch_sequence = maxi(
|
||
|
|
requested_next_sequence,
|
||
|
|
maximum_sequence + 1
|
||
|
|
)
|
||
|
|
return snapshot
|
||
|
|
|
||
|
|
|
||
|
|
func _migrate_save(
|
||
|
|
data: Dictionary,
|
||
|
|
from_version: int,
|
||
|
|
) -> Dictionary:
|
||
|
|
if from_version == SAVE_VERSION:
|
||
|
|
return data
|
||
|
|
return {}
|
||
|
|
|
||
|
|
|
||
|
|
func _mark_dirty() -> void:
|
||
|
|
if (
|
||
|
|
_is_restoring
|
||
|
|
or _automatic_saving_blocked
|
||
|
|
or not _autosave_enabled
|
||
|
|
):
|
||
|
|
return
|
||
|
|
_is_dirty = true
|
||
|
|
if _autosave_timer != null and _autosave_timer.is_stopped():
|
||
|
|
_autosave_timer.start(maxf(autosave_delay, 0.05))
|
||
|
|
|
||
|
|
|
||
|
|
func _on_fish_discovered(_fish_id: StringName) -> void:
|
||
|
|
_mark_dirty()
|
||
|
|
|
||
|
|
|
||
|
|
func _on_balance_changed(_new_balance: int, _delta: int) -> void:
|
||
|
|
_mark_dirty()
|
||
|
|
|
||
|
|
|
||
|
|
func _on_autosave_timeout() -> void:
|
||
|
|
if _is_dirty:
|
||
|
|
save_now()
|
||
|
|
|
||
|
|
|
||
|
|
func _recover_interrupted_write() -> void:
|
||
|
|
if FileAccess.file_exists(SAVE_PATH):
|
||
|
|
_remove_if_present(TEMP_PATH)
|
||
|
|
_remove_if_present(BACKUP_PATH)
|
||
|
|
return
|
||
|
|
if FileAccess.file_exists(BACKUP_PATH):
|
||
|
|
_rename_file(BACKUP_PATH, SAVE_PATH)
|
||
|
|
_remove_if_present(TEMP_PATH)
|
||
|
|
|
||
|
|
|
||
|
|
func _handle_corrupt_save(message: String) -> void:
|
||
|
|
push_warning(message)
|
||
|
|
_restore_defaults()
|
||
|
|
if not FileAccess.file_exists(SAVE_PATH):
|
||
|
|
return
|
||
|
|
var timestamp: int = int(Time.get_unix_time_from_system())
|
||
|
|
var corrupt_path: String = (
|
||
|
|
"%s.corrupt-%d" % [SAVE_PATH, timestamp]
|
||
|
|
)
|
||
|
|
if FileAccess.file_exists(corrupt_path):
|
||
|
|
corrupt_path += "-%d" % Time.get_ticks_usec()
|
||
|
|
if not _rename_file(SAVE_PATH, corrupt_path):
|
||
|
|
_automatic_saving_blocked = true
|
||
|
|
push_warning(
|
||
|
|
"Corrupt player save was left in place; automatic saving is "
|
||
|
|
+ "disabled for this run."
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
func _restore_defaults() -> void:
|
||
|
|
if not _is_configured:
|
||
|
|
return
|
||
|
|
_is_restoring = true
|
||
|
|
var empty_catches: Array[FishCatchType] = []
|
||
|
|
var empty_discoveries: Array[StringName] = []
|
||
|
|
_inventory.replace_all_catches(empty_catches, 1)
|
||
|
|
_collection_log.replace_discovered_ids(empty_discoveries)
|
||
|
|
_wallet.restore_balance(0)
|
||
|
|
_is_restoring = false
|
||
|
|
_is_dirty = false
|
||
|
|
|
||
|
|
|
||
|
|
func _read_integer(
|
||
|
|
value: Variant,
|
||
|
|
invalid_value: int,
|
||
|
|
maximum_value: int = 2147483647,
|
||
|
|
) -> int:
|
||
|
|
if typeof(value) == TYPE_INT:
|
||
|
|
var integer_value: int = int(value)
|
||
|
|
if integer_value >= 0 and integer_value <= maximum_value:
|
||
|
|
return integer_value
|
||
|
|
elif typeof(value) == TYPE_FLOAT:
|
||
|
|
var float_value: float = float(value)
|
||
|
|
if (
|
||
|
|
is_finite(float_value)
|
||
|
|
and float_value >= 0.0
|
||
|
|
and float_value <= float(maximum_value)
|
||
|
|
and is_equal_approx(float_value, round(float_value))
|
||
|
|
):
|
||
|
|
return int(round(float_value))
|
||
|
|
return invalid_value
|
||
|
|
|
||
|
|
|
||
|
|
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
|