Preserve data across the Straywild transition

This commit is contained in:
Alexander Sellite 2026-08-25 22:18:20 -04:00
parent 737d1cf2e0
commit 98a27e2f53
28 changed files with 577 additions and 94 deletions

View file

@ -10,11 +10,41 @@ const BOOTSTRAP_VERSION := 1
const MANIFEST_VERSION := 1
const LAYOUT_VERSION := 1
const MANIFEST_FILENAME := "straywild_data.json"
const LEGACY_MANIFEST_FILENAME := "netfishing_data.json"
const README_FILENAME := "README.txt"
const ENVIRONMENT_VARIABLE := "straywild_DATA_DIR"
const CREATE_ENVIRONMENT_VARIABLE := "straywild_CREATE_DATA_DIR"
const LEGACY_ENVIRONMENT_VARIABLE := "NETFISHING_DATA_DIR"
const LEGACY_CREATE_ENVIRONMENT_VARIABLE := "NETFISHING_CREATE_DATA_DIR"
const APPLICATION_ID := "straywild"
const LEGACY_APPLICATION_ID := "netfishing"
const APP_DATA_PORTABLE_PATH := "user://portable-data"
const LEGACY_USER_DIRECTORY_NAME := "NETFISHING"
const LEGACY_USER_FILES := [
"data_root_bootstrap.json",
"data_root_bootstrap.json.backup",
"network_discovery.cfg",
"player_settings.json",
"player_settings.json.backup",
"controller_mappings.json",
"controller_mappings.json.backup",
"keyboard_mouse_bindings.json",
"keyboard_mouse_bindings.json.backup",
"player_save.json",
"network_profile.json",
"player_appearance.json",
"saved_servers.json",
"known_players.json",
"player_relationships.json",
"server_trust.json",
"host_bans.json",
"player_identity.key",
"player_identity.pub",
"player_identity.json",
"host_identity.key",
"host_identity.pub",
"host_identity.json",
]
const PATH_ALLOWED: StringName = &"allowed"
const PATH_SOURCE_PROJECT: StringName = &"source_project"
const PATH_INSTALLATION: StringName = &"installation"
@ -37,23 +67,29 @@ var override_active := false
func resolve() -> bool:
prepare_legacy_user_data()
_load_bootstrap_identity()
var command_line: String = _command_line_override()
if not command_line.is_empty():
override_active = true
mode = Mode.COMMAND_LINE_OVERRIDE
return _activate_existing(command_line, "", false)
if OS.has_environment(ENVIRONMENT_VARIABLE):
var environment_variable: String = _available_environment_variable(
ENVIRONMENT_VARIABLE,
LEGACY_ENVIRONMENT_VARIABLE,
)
if not environment_variable.is_empty():
override_active = true
mode = Mode.ENVIRONMENT_OVERRIDE
var environment_path: String = OS.get_environment(ENVIRONMENT_VARIABLE)
var environment_path: String = OS.get_environment(environment_variable)
if not environment_path.is_absolute_path():
return _fail("straywild_DATA_DIR must be an absolute path.")
if (
OS.get_environment(CREATE_ENVIRONMENT_VARIABLE) == "1"
and not FileAccess.file_exists(
environment_path.path_join(MANIFEST_FILENAME)
_environment_flag(
CREATE_ENVIRONMENT_VARIABLE,
LEGACY_CREATE_ENVIRONMENT_VARIABLE,
)
and not has_data_manifest(environment_path)
):
var created: Dictionary = create_unbound_root(environment_path)
if not bool(created.get("ok", false)):
@ -80,6 +116,30 @@ func resolve() -> bool:
return false
func prepare_legacy_user_data() -> void:
var current_root: String = _normalize(
ProjectSettings.globalize_path("user://")
)
if current_root.is_empty():
return
var legacy_root: String = _normalize(
current_root.get_base_dir().path_join(LEGACY_USER_DIRECTORY_NAME)
)
if legacy_root == current_root or not DirAccess.dir_exists_absolute(legacy_root):
return
for filename: String in LEGACY_USER_FILES:
var destination: String = current_root.path_join(filename)
if FileAccess.file_exists(destination):
continue
var source: String = legacy_root.path_join(filename)
if FileAccess.file_exists(source):
_copy_legacy_user_file(source, destination)
func has_data_manifest(path: String) -> bool:
return not _existing_manifest_path(_normalize(path)).is_empty()
func default_visible_path() -> String:
var documents: String = OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS)
if documents.is_empty():
@ -105,12 +165,14 @@ func select_new_root(path: String, app_data: bool = false) -> bool:
normalized = ProjectSettings.globalize_path(APP_DATA_PORTABLE_PATH)
if not _validate_candidate(normalized, true):
return false
var manifest_path: String = normalized.path_join(MANIFEST_FILENAME)
if FileAccess.file_exists(manifest_path):
var manifest_path: String = _existing_manifest_path(normalized)
if not manifest_path.is_empty():
var manifest: Dictionary = _read_json(manifest_path)
if not _valid_manifest(manifest):
return _fail("The selected folder has a malformed straywild manifest.")
root_id = str(manifest["root_id"])
if not _upgrade_legacy_manifest(normalized, manifest, manifest_path):
return _fail("The legacy data-folder manifest could not be upgraded.")
else:
if not _directory_is_empty(normalized) and not app_data:
return _fail(
@ -145,14 +207,14 @@ func create_app_data_layout_for_migration(path: String) -> Dictionary:
var normalized: String = _normalize(path)
if not _validate_candidate(normalized, false):
return {"ok": false, "message": error_message}
var manifest_path: String = normalized.path_join(MANIFEST_FILENAME)
if FileAccess.file_exists(manifest_path):
var manifest_path: String = _existing_manifest_path(normalized)
if not manifest_path.is_empty():
var manifest: Dictionary = _read_json(manifest_path)
return (
{"ok": true, "root_id": str(manifest.get("root_id", ""))}
if _valid_manifest(manifest)
else {"ok": false, "message": "The app-data manifest is malformed."}
)
if not _valid_manifest(manifest):
return {"ok": false, "message": "The app-data manifest is malformed."}
if not _upgrade_legacy_manifest(normalized, manifest, manifest_path):
return {"ok": false, "message": "The app-data manifest could not be upgraded."}
return {"ok": true, "root_id": str(manifest.get("root_id", ""))}
var id: String = Crypto.new().generate_random_bytes(16).hex_encode()
if not _create_layout(normalized, id):
return {"ok": false, "message": "The app-data layout could not be created."}
@ -236,12 +298,14 @@ func _activate_existing(path: String, expected_id: String, permit_creation: bool
var normalized: String = _normalize(path)
if not _validate_candidate(normalized, permit_creation):
return false
var manifest_path: String = normalized.path_join(MANIFEST_FILENAME)
if not FileAccess.file_exists(manifest_path):
var manifest_path: String = _existing_manifest_path(normalized)
if manifest_path.is_empty():
return _fail("The selected folder is not a straywild data folder.")
var manifest: Dictionary = _read_json(manifest_path)
if not _valid_manifest(manifest):
return _fail("The straywild data-folder manifest is malformed.")
if not _upgrade_legacy_manifest(normalized, manifest, manifest_path):
return _fail("The legacy data-folder manifest could not be upgraded.")
var found_id: String = str(manifest["root_id"])
if not expected_id.is_empty() and expected_id != found_id:
return _fail("The selected data folder does not match this device pointer.")
@ -422,12 +486,71 @@ func _valid_manifest(data: Dictionary) -> bool:
return (
data.get("format_version") == MANIFEST_VERSION
and data.get("layout_version") == LAYOUT_VERSION
and data.get("application") == APPLICATION_ID
and data.get("application") in [APPLICATION_ID, LEGACY_APPLICATION_ID]
and typeof(data.get("root_id")) == TYPE_STRING
and str(data["root_id"]).length() == 32
)
func _existing_manifest_path(path: String) -> String:
var current: String = path.path_join(MANIFEST_FILENAME)
if FileAccess.file_exists(current):
return current
var legacy: String = path.path_join(LEGACY_MANIFEST_FILENAME)
return legacy if FileAccess.file_exists(legacy) else ""
func _upgrade_legacy_manifest(
path: String,
manifest: Dictionary,
manifest_path: String,
) -> bool:
if (
manifest_path.get_file() == MANIFEST_FILENAME
and manifest.get("application") == APPLICATION_ID
):
return true
var upgraded: Dictionary = manifest.duplicate(true)
upgraded["application"] = APPLICATION_ID
upgraded["last_opened_at_unix"] = int(Time.get_unix_time_from_system())
return _write_text_atomic(
path.path_join(MANIFEST_FILENAME),
JSON.stringify(upgraded, "\t"),
)
func _available_environment_variable(primary: String, legacy: String) -> String:
if OS.has_environment(primary):
return primary
return legacy if OS.has_environment(legacy) else ""
func _environment_flag(primary: String, legacy: String) -> bool:
var variable: String = _available_environment_variable(primary, legacy)
return not variable.is_empty() and OS.get_environment(variable) == "1"
func _copy_legacy_user_file(source: String, destination: String) -> bool:
var bytes: PackedByteArray = PortableFileGuard.read_bytes(
source,
16 * 1024 * 1024,
)
if bytes.is_empty() and FileAccess.get_open_error() != OK:
return false
if DirAccess.make_dir_recursive_absolute(destination.get_base_dir()) != OK:
return false
var file: FileAccess = FileAccess.open(destination, FileAccess.WRITE)
if file == null:
return false
file.store_buffer(bytes)
file.flush()
var copied: bool = file.get_error() == OK
file.close()
if copied and source.ends_with(".key"):
FileAccess.set_unix_permissions(destination, 384)
return copied
func _directory_is_empty(path: String) -> bool:
var access: DirAccess = DirAccess.open(path)
if access == null: