Preserve data across the Straywild transition
This commit is contained in:
parent
737d1cf2e0
commit
98a27e2f53
28 changed files with 577 additions and 94 deletions
|
|
@ -22,9 +22,9 @@ Discord: https://discord.gg/5gP22447kc
|
|||
|
||||
Matrix: https://matrix.to/#/#straywild:matrix.makearmy.io
|
||||
|
||||
Repo: https://forge.makearmy.io/woofmeow/straywild
|
||||
Repo: https://forge.makearmy.io/woofmeow/netfishing
|
||||
|
||||
Releases: https://forge.makearmy.io/woofmeow/straywild/releases
|
||||
Releases: https://forge.makearmy.io/woofmeow/netfishing/releases
|
||||
|
||||
|
||||
## Installing release builds
|
||||
|
|
|
|||
|
|
@ -47,6 +47,10 @@ directory. The templates in `scripts/portmaster/` are authoritative.
|
|||
upgraded. Device-local data, configuration, and cache remain under
|
||||
`straywild/conf/data`, `straywild/conf/config`, and
|
||||
`straywild/conf/cache`.
|
||||
- A rebrand upgrade copies an existing `netfishing/conf/` tree into the new
|
||||
`straywild/conf/` tree when the latter does not exist. It also reuses an
|
||||
existing `ports/saves/netfishing/` data root rather than creating an empty
|
||||
replacement. New installations use straywild paths exclusively.
|
||||
- The archive must not contain `conf/`, saves, identities, logs, source files,
|
||||
`.git`, or `.godot` content.
|
||||
- Release downloads must publish only the canonical `straywild.zip` for this
|
||||
|
|
@ -101,9 +105,9 @@ device directory and run:
|
|||
--offline --no-check install ./straywild.zip
|
||||
```
|
||||
|
||||
Before upgrading an existing installation, preserve
|
||||
`/mnt/mmc/ports/straywild/conf/` and `/mnt/mmc/ports/saves/straywild/` on the
|
||||
device. After installation:
|
||||
Before upgrading an existing installation, preserve either the current
|
||||
`/mnt/mmc/ports/straywild/` paths or the former `/mnt/mmc/ports/netfishing/`
|
||||
paths on the device. After installation:
|
||||
|
||||
1. Confirm the installed executable and PCK hashes match the staged release.
|
||||
2. Confirm HarbourMaster added its `# PORTMASTER:` installation signature to
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ for local README files.
|
|||
|
||||
## Related repositories
|
||||
|
||||
- [Discovery service API](https://forge.makearmy.io/woofmeow/straywild-discovery-server/src/branch/main/docs/API.md)
|
||||
- [Discovery service API](https://forge.makearmy.io/woofmeow/netfishing-discovery-server/src/branch/main/docs/API.md)
|
||||
stays with the service implementation and defines its versioned HTTP
|
||||
contract.
|
||||
- [Dedicated-server packaging](https://forge.makearmy.io/woofmeow/straywild-dedicated-server/src/branch/main/README.md)
|
||||
- [Dedicated-server packaging](https://forge.makearmy.io/woofmeow/netfishing-dedicated-server/src/branch/main/README.md)
|
||||
stays with its installer and update scripts. It consumes this repository's
|
||||
attribution and legal notices from the exact pinned game commit instead of
|
||||
maintaining copies.
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ script_export_mode=2
|
|||
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
application/bundle_identifier="io.woofmeow.straywild"
|
||||
application/bundle_identifier="io.woofmeow.netfishing"
|
||||
application/icon="res://art/exported/system_icons/straywild_1024.png"
|
||||
application/icon_interpolation=0
|
||||
application/short_version="0.18.1"
|
||||
|
|
@ -178,7 +178,7 @@ architectures/x86=false
|
|||
architectures/x86_64=false
|
||||
version/code=180100
|
||||
version/name="v0.18.1-alpha"
|
||||
package/unique_name="io.woofmeow.straywild"
|
||||
package/unique_name="io.woofmeow.netfishing"
|
||||
package/name="straywild"
|
||||
package/signed=true
|
||||
package/app_category=0
|
||||
|
|
|
|||
19
main/main.gd
19
main/main.gd
|
|
@ -277,6 +277,7 @@ var _rain_ambience: RainAmbienceType
|
|||
|
||||
|
||||
func _ready() -> void:
|
||||
_data_root.prepare_legacy_user_data()
|
||||
_performance_profile = RuntimePerformanceProfileType.from_environment()
|
||||
_dedicated_runtime = _is_dedicated_server_runtime()
|
||||
if _dedicated_runtime:
|
||||
|
|
@ -471,10 +472,7 @@ func _start_dedicated_server() -> void:
|
|||
_fail_dedicated_server(config.error_message)
|
||||
return
|
||||
var data_path: String = config.data_directory
|
||||
var manifest_path: String = data_path.path_join(
|
||||
PlayerDataRoot.MANIFEST_FILENAME
|
||||
)
|
||||
if not FileAccess.file_exists(manifest_path):
|
||||
if not _data_root.has_data_manifest(data_path):
|
||||
var created: Dictionary = _data_root.create_unbound_root(data_path)
|
||||
if not bool(created.get("ok", false)):
|
||||
_fail_dedicated_server(str(created.get(
|
||||
|
|
@ -973,6 +971,7 @@ func _initialize_application(dedicated: bool) -> void:
|
|||
_network_session,
|
||||
_interface_fonts,
|
||||
)
|
||||
_game_ui.data_root_changed.connect(_on_data_root_changed)
|
||||
_game_ui.setup_controller_mapping(_controller_mapping_manager)
|
||||
_ui_pixelation.setup_controller_mapping(_controller_mapping_manager)
|
||||
_game_ui.setup_keyboard_mouse_mapping(_keyboard_mouse_mapping_manager)
|
||||
|
|
@ -1344,6 +1343,18 @@ func _on_data_root_status(message: String) -> void:
|
|||
_configure_popup_dialog.call_deferred(dialog, dialog.get_ok_button())
|
||||
|
||||
|
||||
func _on_data_root_changed() -> void:
|
||||
_save_manager.set_autosave_enabled(false)
|
||||
_save_manager.cancel_pending_autosave()
|
||||
_configure_portable_stores()
|
||||
if not _network_profile_service.reload_persisted_profile():
|
||||
push_warning("The selected data folder's player profile could not be loaded.")
|
||||
_network_session.set_local_appearance_snapshot(
|
||||
_appearance_store.get_snapshot()
|
||||
)
|
||||
_game_ui.get_title_screen().refresh_after_data_root_change()
|
||||
|
||||
|
||||
func _on_server_trust_required(
|
||||
endpoint: String,
|
||||
expected_fingerprint: String,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ const PROFILE_ENVIRONMENT_VARIABLE: String = (
|
|||
"straywild_PERFORMANCE_PROFILE"
|
||||
)
|
||||
const LEGACY_LIGHT_ENVIRONMENT_VARIABLE: String = "straywild_LOW_END"
|
||||
const NETFISHING_PROFILE_ENVIRONMENT_VARIABLE := (
|
||||
"NETFISHING_PERFORMANCE_PROFILE"
|
||||
)
|
||||
const NETFISHING_LIGHT_ENVIRONMENT_VARIABLE := "NETFISHING_LOW_END"
|
||||
const NORMAL_PROFILE: StringName = &"normal"
|
||||
const LIGHT_PROFILE: StringName = &"light"
|
||||
const NORMAL_WORLD_RENDER_SCALE: float = 1.0
|
||||
|
|
@ -14,9 +18,19 @@ var _profile_name: StringName = NORMAL_PROFILE
|
|||
|
||||
|
||||
static func from_environment() -> RuntimePerformanceProfile:
|
||||
var profile_name: String = OS.get_environment(
|
||||
PROFILE_ENVIRONMENT_VARIABLE
|
||||
)
|
||||
if profile_name.is_empty():
|
||||
profile_name = OS.get_environment(
|
||||
NETFISHING_PROFILE_ENVIRONMENT_VARIABLE
|
||||
)
|
||||
return from_name(
|
||||
StringName(OS.get_environment(PROFILE_ENVIRONMENT_VARIABLE)),
|
||||
OS.get_environment(LEGACY_LIGHT_ENVIRONMENT_VARIABLE) == "1",
|
||||
StringName(profile_name),
|
||||
(
|
||||
OS.get_environment(LEGACY_LIGHT_ENVIRONMENT_VARIABLE) == "1"
|
||||
or OS.get_environment(NETFISHING_LIGHT_ENVIRONMENT_VARIABLE) == "1"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ signal friend_invite_finished(success: bool, message: String)
|
|||
|
||||
const BASE_URL_SETTING: String = "network/discovery/base_url"
|
||||
const BASE_URL_ENVIRONMENT: String = "straywild_DISCOVERY_URL"
|
||||
const LEGACY_BASE_URL_ENVIRONMENT: String = "NETFISHING_DISCOVERY_URL"
|
||||
const SETTINGS_PATH: String = "user://network_discovery.cfg"
|
||||
const DEFAULT_ROOM_NAME: String = "Player's Server"
|
||||
const LEGACY_DEFAULT_ROOM_NAME: String = "straywild Room"
|
||||
|
|
@ -553,6 +554,10 @@ func is_own_room(room: Dictionary) -> bool:
|
|||
|
||||
func _configured_base_url() -> String:
|
||||
var value: String = OS.get_environment(BASE_URL_ENVIRONMENT).strip_edges()
|
||||
if value.is_empty():
|
||||
value = OS.get_environment(
|
||||
LEGACY_BASE_URL_ENVIRONMENT
|
||||
).strip_edges()
|
||||
if value.is_empty():
|
||||
value = str(ProjectSettings.get_setting(BASE_URL_SETTING, "")).strip_edges()
|
||||
while value.ends_with("/"):
|
||||
|
|
|
|||
|
|
@ -17,6 +17,10 @@ var _data_root: PlayerDataRoot
|
|||
func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
|
||||
_store_path = path
|
||||
_data_root = data_root
|
||||
_namespaces.clear()
|
||||
_loaded = false
|
||||
_write_blocked = false
|
||||
_expected_hash = ""
|
||||
|
||||
|
||||
func is_banned(host_fingerprint: String, target_fingerprint: String) -> bool:
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ extends Node
|
|||
signal operation_finished(success: bool, message: String)
|
||||
|
||||
const MAGIC := "straywild_IDENTITY_BACKUP"
|
||||
const LEGACY_MAGIC := "NETFISHING_IDENTITY_BACKUP"
|
||||
const FORMAT_VERSION := 1
|
||||
const MAX_BACKUP_BYTES := 256 * 1024
|
||||
const MIN_PASSPHRASE_LENGTH := 12
|
||||
|
|
@ -110,8 +111,9 @@ func inspect_backup(
|
|||
if json.parse(text) != OK or typeof(json.data) != TYPE_DICTIONARY:
|
||||
return {"ok": false}
|
||||
var data: Dictionary = json.data
|
||||
var legacy_backup: bool = data.get("magic") == LEGACY_MAGIC
|
||||
if (
|
||||
data.get("magic") != MAGIC
|
||||
data.get("magic") not in [MAGIC, LEGACY_MAGIC]
|
||||
or data.get("format_version") != FORMAT_VERSION
|
||||
or data.get("identity_type") != expected_type
|
||||
or data.get("algorithm") != NetworkIdentityCrypto.ALGORITHM
|
||||
|
|
@ -134,12 +136,23 @@ func inspect_backup(
|
|||
var signature: PackedByteArray = Marshalls.base64_to_raw(
|
||||
str(data.get("self_signature", ""))
|
||||
)
|
||||
if not NetworkIdentityCrypto.verify_fields(
|
||||
NetworkIdentityCrypto.load_public_key(public_pem),
|
||||
"identity_backup_self_test",
|
||||
[expected_type, fingerprint],
|
||||
signature,
|
||||
):
|
||||
var public_key: CryptoKey = NetworkIdentityCrypto.load_public_key(public_pem)
|
||||
var backup_signature_valid: bool = (
|
||||
NetworkIdentityCrypto.verify_legacy_fields(
|
||||
public_key,
|
||||
"identity_backup_self_test",
|
||||
[expected_type, fingerprint],
|
||||
signature,
|
||||
)
|
||||
if legacy_backup
|
||||
else NetworkIdentityCrypto.verify_fields(
|
||||
public_key,
|
||||
"identity_backup_self_test",
|
||||
[expected_type, fingerprint],
|
||||
signature,
|
||||
)
|
||||
)
|
||||
if not backup_signature_valid:
|
||||
return {"ok": false}
|
||||
var fresh: PackedByteArray = NetworkIdentityCrypto.sign_fields(
|
||||
key, "identity_import_self_test", [fingerprint]
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ var _data_root: PlayerDataRoot
|
|||
func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
|
||||
_store_path = path
|
||||
_data_root = data_root
|
||||
_records.clear()
|
||||
_loaded = false
|
||||
_write_blocked = false
|
||||
_expected_hash = ""
|
||||
|
||||
|
||||
func observe(fingerprint: String, display_name: String) -> String:
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ const FINGERPRINT_LENGTH: int = 64
|
|||
const MAX_PUBLIC_KEY_BYTES: int = 8192
|
||||
const MAX_SIGNATURE_BYTES: int = 1024
|
||||
const DOMAIN_PREFIX: String = "straywild"
|
||||
const LEGACY_DOMAIN_PREFIX: String = "NETFISHING"
|
||||
const IDENTITY_VERSION: String = "identity_v1"
|
||||
|
||||
|
||||
|
|
@ -50,8 +51,16 @@ static func secure_id(byte_count: int = 16) -> String:
|
|||
|
||||
|
||||
static func canonical_bytes(domain: String, fields: Array) -> PackedByteArray:
|
||||
return canonical_bytes_for_prefix(DOMAIN_PREFIX, domain, fields)
|
||||
|
||||
|
||||
static func canonical_bytes_for_prefix(
|
||||
prefix: String,
|
||||
domain: String,
|
||||
fields: Array,
|
||||
) -> PackedByteArray:
|
||||
var output := PackedByteArray()
|
||||
_append_string(output, DOMAIN_PREFIX)
|
||||
_append_string(output, prefix)
|
||||
_append_string(output, IDENTITY_VERSION)
|
||||
_append_string(output, domain)
|
||||
for value: Variant in fields:
|
||||
|
|
@ -117,6 +126,37 @@ static func verify_fields(
|
|||
)
|
||||
|
||||
|
||||
static func verify_legacy_fields(
|
||||
public_key: CryptoKey,
|
||||
domain: String,
|
||||
fields: Array,
|
||||
signature: PackedByteArray,
|
||||
) -> bool:
|
||||
if (
|
||||
public_key == null
|
||||
or signature.is_empty()
|
||||
or signature.size() > MAX_SIGNATURE_BYTES
|
||||
):
|
||||
return false
|
||||
var bytes: PackedByteArray = canonical_bytes_for_prefix(
|
||||
LEGACY_DOMAIN_PREFIX,
|
||||
domain,
|
||||
fields,
|
||||
)
|
||||
if bytes.is_empty():
|
||||
return false
|
||||
var context := HashingContext.new()
|
||||
if context.start(HashingContext.HASH_SHA256) != OK:
|
||||
return false
|
||||
context.update(bytes)
|
||||
return Crypto.new().verify(
|
||||
HashingContext.HASH_SHA256,
|
||||
context.finish(),
|
||||
signature,
|
||||
public_key,
|
||||
)
|
||||
|
||||
|
||||
static func load_public_key(public_pem: String) -> CryptoKey:
|
||||
var normalized := normalize_public_pem(public_pem)
|
||||
if normalized.is_empty() or normalized.to_utf8_buffer().size() > MAX_PUBLIC_KEY_BYTES:
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ var _future_version := false
|
|||
func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
|
||||
_profile_path = path
|
||||
_data_root = data_root
|
||||
_expected_hash = ""
|
||||
_future_version = false
|
||||
|
||||
|
||||
func _temp_path() -> String:
|
||||
|
|
|
|||
|
|
@ -51,6 +51,17 @@ func setup(
|
|||
_apply_local_voice_to_avatar()
|
||||
|
||||
|
||||
func reload_persisted_profile() -> bool:
|
||||
if (
|
||||
not _preferences.load_or_create()
|
||||
or not _appearance_store.load_preferences()
|
||||
):
|
||||
return false
|
||||
_apply_to_avatar(1, _appearance_store.get_snapshot())
|
||||
_apply_local_voice_to_avatar()
|
||||
return true
|
||||
|
||||
|
||||
func get_persisted_name() -> String:
|
||||
return _preferences.display_name
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ var _data_root: PlayerDataRoot
|
|||
func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
|
||||
_store_path = path
|
||||
_data_root = data_root
|
||||
_records.clear()
|
||||
_loaded = false
|
||||
_write_blocked = false
|
||||
_expected_hash = ""
|
||||
|
||||
|
||||
func is_muted(fingerprint: String) -> bool:
|
||||
|
|
|
|||
|
|
@ -26,10 +26,7 @@ static func migrate_legacy_to(
|
|||
) -> Dictionary:
|
||||
var normalized: String = destination.simplify_path().trim_suffix("/")
|
||||
if DirAccess.dir_exists_absolute(normalized):
|
||||
var existing_manifest: String = normalized.path_join(
|
||||
PlayerDataRoot.MANIFEST_FILENAME
|
||||
)
|
||||
if FileAccess.file_exists(existing_manifest):
|
||||
if data_root.has_data_manifest(normalized):
|
||||
return {
|
||||
"ok": false,
|
||||
"requires_existing_root_decision": true,
|
||||
|
|
@ -106,7 +103,7 @@ static func migrate_active_to(
|
|||
"message": "This data folder is already active.",
|
||||
}
|
||||
if DirAccess.dir_exists_absolute(normalized) and not _directory_empty(normalized):
|
||||
if FileAccess.file_exists(normalized.path_join(PlayerDataRoot.MANIFEST_FILENAME)):
|
||||
if data_root.has_data_manifest(normalized):
|
||||
return {
|
||||
"ok": false,
|
||||
"requires_existing_root_decision": true,
|
||||
|
|
@ -207,8 +204,7 @@ static func replace_existing_with_legacy(
|
|||
destination: String,
|
||||
) -> Dictionary:
|
||||
var normalized: String = destination.simplify_path().trim_suffix("/")
|
||||
var manifest: String = normalized.path_join(PlayerDataRoot.MANIFEST_FILENAME)
|
||||
if not FileAccess.file_exists(manifest):
|
||||
if not data_root.has_data_manifest(normalized):
|
||||
return {"ok": false, "message": "The selected folder is not a straywild data root."}
|
||||
var recovery: String = ProjectSettings.globalize_path(
|
||||
"user://migration-recovery"
|
||||
|
|
@ -229,7 +225,7 @@ static func replace_existing_with_active(
|
|||
destination: String,
|
||||
) -> Dictionary:
|
||||
var normalized: String = destination.simplify_path().trim_suffix("/")
|
||||
if not FileAccess.file_exists(normalized.path_join(PlayerDataRoot.MANIFEST_FILENAME)):
|
||||
if not data_root.has_data_manifest(normalized):
|
||||
return {"ok": false, "message": "The selected folder is not a straywild data root."}
|
||||
var recovery: String = ProjectSettings.globalize_path(
|
||||
"user://migration-recovery"
|
||||
|
|
|
|||
|
|
@ -22,6 +22,12 @@ var _data_root: PlayerDataRoot
|
|||
func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
|
||||
_store_path = path
|
||||
_data_root = data_root
|
||||
_saved_entries.clear()
|
||||
_recent_entries.clear()
|
||||
_loaded = false
|
||||
_recovery_warning = ""
|
||||
_write_blocked = false
|
||||
_expected_hash = ""
|
||||
|
||||
|
||||
func _temp_path() -> String:
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ var _data_root: PlayerDataRoot
|
|||
func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
|
||||
_store_path = path
|
||||
_data_root = data_root
|
||||
_records.clear()
|
||||
_loaded = false
|
||||
_write_blocked = false
|
||||
_expected_hash = ""
|
||||
|
||||
|
||||
func verify(endpoint: ConnectionEndpoint, fingerprint: String) -> Verification:
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ var _data_root: PlayerDataRoot
|
|||
func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
|
||||
_profile_path = path
|
||||
_data_root = data_root
|
||||
_snapshot = CharacterCustomizationCatalog.default_snapshot()
|
||||
_loaded = false
|
||||
_future_version = false
|
||||
_expected_hash = ""
|
||||
|
||||
|
||||
func _temp_path() -> String:
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ class_name ProgressionSaveCodec
|
|||
extends RefCounted
|
||||
|
||||
const MAGIC := "straywild_PROGRESSION_CONTAINER"
|
||||
const LEGACY_MAGIC := "NETFISHING_PROGRESSION_CONTAINER"
|
||||
const FORMAT_VERSION := 1
|
||||
const LOCAL_KIND := "local_save"
|
||||
const ARCHIVE_KIND := "portable_archive"
|
||||
|
|
@ -9,8 +10,10 @@ const MAX_CONTAINER_BYTES := 16 * 1024 * 1024
|
|||
|
||||
# This key keeps routine progression files opaque to casual editing. Since the
|
||||
# client is open source, it is intentionally not treated as a security secret.
|
||||
# Its original value is a stable file-format detail so released saves remain
|
||||
# readable after the Straywild rebrand.
|
||||
const CONTAINER_PASSPHRASE := (
|
||||
"straywild progression container v1 / not an identity credential"
|
||||
"NETfishing progression container v1 / not an identity credential"
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -102,15 +105,9 @@ static func _read_container(path: String, expected_kind: String) -> Dictionary:
|
|||
or FileAccess.get_size(path) > MAX_CONTAINER_BYTES
|
||||
):
|
||||
return {"ok": false, "message": "progression file is unavailable."}
|
||||
var file: FileAccess = FileAccess.open_encrypted_with_pass(
|
||||
path,
|
||||
FileAccess.READ,
|
||||
CONTAINER_PASSPHRASE,
|
||||
)
|
||||
if file == null:
|
||||
var envelope_text: String = _read_encrypted_text(path)
|
||||
if envelope_text.is_empty():
|
||||
return {"ok": false, "message": "progression file could not be opened."}
|
||||
var envelope_text: String = file.get_as_text()
|
||||
file.close()
|
||||
var envelope_json := JSON.new()
|
||||
if (
|
||||
envelope_json.parse(envelope_text) != OK
|
||||
|
|
@ -118,8 +115,9 @@ static func _read_container(path: String, expected_kind: String) -> Dictionary:
|
|||
):
|
||||
return {"ok": false, "message": "progression container is malformed."}
|
||||
var envelope: Dictionary = envelope_json.data
|
||||
var legacy_container: bool = envelope.get("magic") == LEGACY_MAGIC
|
||||
if (
|
||||
envelope.get("magic") != MAGIC
|
||||
envelope.get("magic") not in [MAGIC, LEGACY_MAGIC]
|
||||
or int(envelope.get("format_version", -1)) != FORMAT_VERSION
|
||||
or str(envelope.get("kind", "")) != expected_kind
|
||||
):
|
||||
|
|
@ -144,9 +142,23 @@ static func _read_container(path: String, expected_kind: String) -> Dictionary:
|
|||
"game_version": str(envelope.get("game_version", "")),
|
||||
"created_at_unix": int(envelope.get("created_at_unix", 0)),
|
||||
"legacy_plaintext": false,
|
||||
"legacy_container": legacy_container,
|
||||
}
|
||||
|
||||
|
||||
static func _read_encrypted_text(path: String) -> String:
|
||||
var file: FileAccess = FileAccess.open_encrypted_with_pass(
|
||||
path,
|
||||
FileAccess.READ,
|
||||
CONTAINER_PASSPHRASE,
|
||||
)
|
||||
if file == null:
|
||||
return ""
|
||||
var text: String = file.get_as_text()
|
||||
file.close()
|
||||
return text
|
||||
|
||||
|
||||
static func _read_plaintext_dictionary(path: String) -> Dictionary:
|
||||
var bytes: PackedByteArray = PortableFileGuard.read_bytes(
|
||||
path,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ readonly WINDOWS_DIR="${BUILD_ROOT}/windows-x86_64"
|
|||
readonly LINUX_DIR="${BUILD_ROOT}/linux-x86_64"
|
||||
readonly README_SOURCE="${PROJECT_ROOT}/docs/README-PLAYTEST.txt"
|
||||
readonly SOURCE_COMMIT="$(git -C "${PROJECT_ROOT}" rev-parse HEAD)"
|
||||
readonly SOURCE_URL="https://forge.makearmy.io/woofmeow/straywild"
|
||||
readonly SOURCE_URL="https://forge.makearmy.io/woofmeow/netfishing"
|
||||
readonly WINDOWS_ZIP="${BUILD_ROOT}/straywild-v0.18.1-alpha-windows-x86_64.zip"
|
||||
readonly LINUX_ZIP="${BUILD_ROOT}/straywild-v0.18.1-alpha-linux-x86_64.zip"
|
||||
readonly GODOT_BIN="${GODOT_BIN:-godot}"
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ cat >"${GAME_ROOT}/licenses/SOURCE-CODE.md" <<EOF
|
|||
|
||||
straywild code is licensed under GPL-3.0-or-later.
|
||||
|
||||
Source repository: https://forge.makearmy.io/woofmeow/straywild
|
||||
Source repository: https://forge.makearmy.io/woofmeow/netfishing
|
||||
Exact source revision: ${TAG_COMMIT}
|
||||
Release tag: ${RELEASE_TAG}
|
||||
PortMaster packaging revision: ${HEAD_COMMIT}
|
||||
|
|
@ -262,6 +262,10 @@ grep -Fq 'CONTROLLER_MAPPING_FILE="$CONFDIR/cache/controller_mapping.txt"' \
|
|||
"${STAGE_ROOT}/straywild.sh"
|
||||
grep -Fq 'SAVEDIR="$PORTS_ROOT/saves/straywild"' \
|
||||
"${STAGE_ROOT}/straywild.sh"
|
||||
grep -Fq 'LEGACY_SAVEDIR="$PORTS_ROOT/saves/netfishing"' \
|
||||
"${STAGE_ROOT}/straywild.sh"
|
||||
grep -Fq 'cp -a "$LEGACY_CONFDIR/." "$CONFDIR/"' \
|
||||
"${STAGE_ROOT}/straywild.sh"
|
||||
grep -Fq '"straywild_DATA_DIR=$SAVEDIR"' \
|
||||
"${STAGE_ROOT}/straywild.sh"
|
||||
grep -Fq '"straywild_CREATE_DATA_DIR=1"' \
|
||||
|
|
|
|||
|
|
@ -21,7 +21,11 @@ GAMEDIR="/${directory}/ports/straywild"
|
|||
CONFDIR="$GAMEDIR/conf"
|
||||
PORTS_ROOT="${GAMEDIR%/straywild}"
|
||||
SAVEDIR="$PORTS_ROOT/saves/straywild"
|
||||
LEGACY_GAMEDIR="$PORTS_ROOT/netfishing"
|
||||
LEGACY_CONFDIR="$LEGACY_GAMEDIR/conf"
|
||||
LEGACY_SAVEDIR="$PORTS_ROOT/saves/netfishing"
|
||||
DATA_BOOTSTRAP_DIR="$CONFDIR/data/godot/app_userdata/straywild"
|
||||
LEGACY_DATA_BOOTSTRAP_DIR="$CONFDIR/data/godot/app_userdata/NETFISHING"
|
||||
GAME_EXECUTABLE="$GAMEDIR/straywild.aarch64"
|
||||
GAME_LAUNCHER="$GAMEDIR/launch-straywild.sh"
|
||||
GPTOKEYB_CONFIG="$GAMEDIR/straywild.gptk"
|
||||
|
|
@ -33,13 +37,30 @@ HARBOURMASTER="$controlfolder/harbourmaster"
|
|||
mkdir -p "$CONFDIR/data" "$CONFDIR/config" "$CONFDIR/cache" "$WESTON_DIR"
|
||||
chmod +x "$GAME_EXECUTABLE"
|
||||
|
||||
# A renamed PortMaster entry installs beside the former NETfishing port. Copy
|
||||
# its device-local configuration once and continue using its established save
|
||||
# root when present. New installations use straywild paths exclusively.
|
||||
if [ -d "$LEGACY_CONFDIR" ] && [ ! -f "$CONFDIR/.netfishing-imported" ]; then
|
||||
cp -a "$LEGACY_CONFDIR/." "$CONFDIR/"
|
||||
touch "$CONFDIR/.netfishing-imported"
|
||||
fi
|
||||
if [ ! -f "$SAVEDIR/straywild_data.json" ] \
|
||||
&& [ ! -f "$SAVEDIR/netfishing_data.json" ] \
|
||||
&& { [ -f "$LEGACY_SAVEDIR/straywild_data.json" ] \
|
||||
|| [ -f "$LEGACY_SAVEDIR/netfishing_data.json" ]; }; then
|
||||
SAVEDIR="$LEGACY_SAVEDIR"
|
||||
fi
|
||||
|
||||
# straywild uses a predictable save directory beside the port instead of
|
||||
# presenting a folder picker on a small screen. Preserve an established
|
||||
# data-root choice when upgrading an existing installation.
|
||||
straywild_DATA_ENVIRONMENT=()
|
||||
if [ -f "$SAVEDIR/straywild_data.json" ] || {
|
||||
if [ -f "$SAVEDIR/straywild_data.json" ] \
|
||||
|| [ -f "$SAVEDIR/netfishing_data.json" ] || {
|
||||
[ ! -f "$DATA_BOOTSTRAP_DIR/data_root_bootstrap.json" ] &&
|
||||
[ ! -f "$DATA_BOOTSTRAP_DIR/data_root_bootstrap.json.backup" ]
|
||||
[ ! -f "$DATA_BOOTSTRAP_DIR/data_root_bootstrap.json.backup" ] &&
|
||||
[ ! -f "$LEGACY_DATA_BOOTSTRAP_DIR/data_root_bootstrap.json" ] &&
|
||||
[ ! -f "$LEGACY_DATA_BOOTSTRAP_DIR/data_root_bootstrap.json.backup" ]
|
||||
}; then
|
||||
mkdir -p "$SAVEDIR"
|
||||
straywild_DATA_ENVIRONMENT+=(
|
||||
|
|
|
|||
|
|
@ -31,8 +31,10 @@ static func from_runtime() -> DedicatedServerConfig:
|
|||
result.discovery_url = str(ProjectSettings.get_setting(
|
||||
"network/discovery/base_url", ""
|
||||
)).strip_edges()
|
||||
var config_path: String = OS.get_environment(
|
||||
"straywild_SERVER_CONFIG"
|
||||
var config_path: String = _environment_string(
|
||||
"straywild_SERVER_CONFIG",
|
||||
"NETFISHING_SERVER_CONFIG",
|
||||
"",
|
||||
).strip_edges()
|
||||
for argument: String in OS.get_cmdline_user_args():
|
||||
if argument.begins_with("--config="):
|
||||
|
|
@ -87,36 +89,44 @@ func _load_file(path: String) -> bool:
|
|||
|
||||
func _apply_environment() -> void:
|
||||
server_name = _environment_string(
|
||||
"straywild_SERVER_NAME", server_name
|
||||
"straywild_SERVER_NAME", "NETFISHING_SERVER_NAME", server_name
|
||||
)
|
||||
bind_address = _environment_string(
|
||||
"straywild_SERVER_BIND", bind_address
|
||||
"straywild_SERVER_BIND", "NETFISHING_SERVER_BIND", bind_address
|
||||
)
|
||||
port = _environment_int(
|
||||
"straywild_SERVER_PORT", "NETFISHING_SERVER_PORT", port
|
||||
)
|
||||
port = _environment_int("straywild_SERVER_PORT", port)
|
||||
max_players = _environment_int(
|
||||
"straywild_SERVER_MAX_PLAYERS", max_players
|
||||
"straywild_SERVER_MAX_PLAYERS",
|
||||
"NETFISHING_SERVER_MAX_PLAYERS",
|
||||
max_players,
|
||||
)
|
||||
world_seed = _environment_int(
|
||||
"straywild_WORLD_SEED", world_seed
|
||||
"straywild_WORLD_SEED", "NETFISHING_WORLD_SEED", world_seed
|
||||
)
|
||||
public_listing = _environment_bool(
|
||||
"straywild_SERVER_PUBLIC", public_listing
|
||||
"straywild_SERVER_PUBLIC", "NETFISHING_SERVER_PUBLIC", public_listing
|
||||
)
|
||||
discovery_url = _environment_string(
|
||||
"straywild_DISCOVERY_URL", discovery_url
|
||||
"straywild_DISCOVERY_URL", "NETFISHING_DISCOVERY_URL", discovery_url
|
||||
)
|
||||
data_directory = _environment_string(
|
||||
"straywild_DATA_DIR", data_directory
|
||||
"straywild_DATA_DIR", "NETFISHING_DATA_DIR", data_directory
|
||||
)
|
||||
chat_logging = _environment_bool(
|
||||
"straywild_CHAT_LOGGING", chat_logging
|
||||
"straywild_CHAT_LOGGING", "NETFISHING_CHAT_LOGGING", chat_logging
|
||||
)
|
||||
chat_log_path = _environment_string(
|
||||
"straywild_CHAT_LOG_PATH", chat_log_path
|
||||
"straywild_CHAT_LOG_PATH", "NETFISHING_CHAT_LOG_PATH", chat_log_path
|
||||
)
|
||||
if OS.has_environment("straywild_SERVER_OPERATORS"):
|
||||
var operators_variable: String = _available_environment_variable(
|
||||
"straywild_SERVER_OPERATORS",
|
||||
"NETFISHING_SERVER_OPERATORS",
|
||||
)
|
||||
if not operators_variable.is_empty():
|
||||
operator_fingerprints = _parse_fingerprint_list(
|
||||
OS.get_environment("straywild_SERVER_OPERATORS")
|
||||
OS.get_environment(operators_variable)
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -211,21 +221,45 @@ static func _safe_text(value: String) -> bool:
|
|||
return true
|
||||
|
||||
|
||||
static func _environment_string(name: String, fallback: String) -> String:
|
||||
return OS.get_environment(name) if OS.has_environment(name) else fallback
|
||||
static func _available_environment_variable(
|
||||
name: String,
|
||||
legacy_name: String,
|
||||
) -> String:
|
||||
if OS.has_environment(name):
|
||||
return name
|
||||
return legacy_name if OS.has_environment(legacy_name) else ""
|
||||
|
||||
|
||||
static func _environment_int(name: String, fallback: int) -> int:
|
||||
static func _environment_string(
|
||||
name: String,
|
||||
legacy_name: String,
|
||||
fallback: String,
|
||||
) -> String:
|
||||
var variable: String = _available_environment_variable(name, legacy_name)
|
||||
return OS.get_environment(variable) if not variable.is_empty() else fallback
|
||||
|
||||
|
||||
static func _environment_int(
|
||||
name: String,
|
||||
legacy_name: String,
|
||||
fallback: int,
|
||||
) -> int:
|
||||
var variable: String = _available_environment_variable(name, legacy_name)
|
||||
return (
|
||||
_parse_int(OS.get_environment(name), fallback)
|
||||
if OS.has_environment(name) else fallback
|
||||
_parse_int(OS.get_environment(variable), fallback)
|
||||
if not variable.is_empty() else fallback
|
||||
)
|
||||
|
||||
|
||||
static func _environment_bool(name: String, fallback: bool) -> bool:
|
||||
if not OS.has_environment(name):
|
||||
static func _environment_bool(
|
||||
name: String,
|
||||
legacy_name: String,
|
||||
fallback: bool,
|
||||
) -> bool:
|
||||
var variable: String = _available_environment_variable(name, legacy_name)
|
||||
if variable.is_empty():
|
||||
return fallback
|
||||
var value: String = OS.get_environment(name).strip_edges().to_lower()
|
||||
var value: String = OS.get_environment(variable).strip_edges().to_lower()
|
||||
return value in ["1", "true", "yes", "on"]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,15 @@ func _run() -> void:
|
|||
var decoded: Dictionary = ProgressionSaveCodec.read_local_save(save_path)
|
||||
assert(bool(decoded.get("ok", false)))
|
||||
assert(int((decoded["data"] as Dictionary)["save_version"]) == 10)
|
||||
var legacy_container_path: String = data_root.progression_backup_directory().path_join(
|
||||
"netfishing-container.nfsave"
|
||||
)
|
||||
_assert_legacy_progression_container(
|
||||
legacy_container_path,
|
||||
decoded["data"] as Dictionary,
|
||||
)
|
||||
_assert_legacy_data_manifest(data_root.root_path.get_base_dir())
|
||||
_assert_legacy_identity_backup(main, data_root)
|
||||
|
||||
var archive_path: String = data_root.progression_backup_directory().path_join(
|
||||
"progression-test.nfsave"
|
||||
|
|
@ -135,6 +144,9 @@ func _run() -> void:
|
|||
assert(settings_panel.get_node_or_null("%ExportProgression") == null)
|
||||
assert(settings_panel.get_node_or_null("%ImportProgression") == null)
|
||||
var settings_panel: SettingsPanel = settings_panels.front() as SettingsPanel
|
||||
settings_panel.set(
|
||||
"_presentation_mode", SettingsPanel.PresentationMode.TITLE_EMBEDDED
|
||||
)
|
||||
var unchanged_root: String = data_root.root_path
|
||||
settings_panel.call("_choose_data_folder")
|
||||
var data_folder_dialog := (
|
||||
|
|
@ -169,11 +181,14 @@ func _run() -> void:
|
|||
var migrated_root: String = data_root.root_path.get_base_dir().path_join(
|
||||
"progression-migrated-data"
|
||||
)
|
||||
var data_migration: Dictionary = PortableDataMigration.migrate_active_to(
|
||||
data_root, migrated_root
|
||||
settings_panel.call("_change_data_folder", migrated_root)
|
||||
for _frame: int in 4:
|
||||
await process_frame
|
||||
assert(data_root.root_path == migrated_root)
|
||||
assert(
|
||||
(settings_panel.get_node("%SettingsFeedback") as Label).text
|
||||
== "data folder changed."
|
||||
)
|
||||
assert(bool(data_migration.get("ok", false)))
|
||||
assert(bool(data_migration.get("changed", false)))
|
||||
var migrated_save: String = migrated_root.path_join(
|
||||
"player/player_save.nfsave"
|
||||
)
|
||||
|
|
@ -193,6 +208,18 @@ func _run() -> void:
|
|||
"ok", false
|
||||
)
|
||||
))
|
||||
var live_slots: Array[Dictionary] = save_slots.list_slots()
|
||||
assert(live_slots.size() == 2)
|
||||
var live_created: Dictionary = save_slots.create_empty_slot(
|
||||
"live root switch"
|
||||
)
|
||||
assert(bool(live_created.get("ok", false)))
|
||||
var live_slot_id: String = str(live_created.get("slot_id", ""))
|
||||
assert(save_slots.rename_slot(live_slot_id, "live root ready"))
|
||||
assert(
|
||||
str(save_slots.get_slot(live_slot_id).get("display_name", ""))
|
||||
== "live root ready"
|
||||
)
|
||||
|
||||
main.queue_free()
|
||||
for _frame: int in 4:
|
||||
|
|
@ -202,6 +229,124 @@ func _run() -> void:
|
|||
quit()
|
||||
|
||||
|
||||
func _assert_legacy_progression_container(
|
||||
path: String,
|
||||
payload: Dictionary,
|
||||
) -> void:
|
||||
var payload_json: String = JSON.stringify(payload)
|
||||
var envelope: Dictionary = {
|
||||
"magic": ProgressionSaveCodec.LEGACY_MAGIC,
|
||||
"format_version": ProgressionSaveCodec.FORMAT_VERSION,
|
||||
"kind": ProgressionSaveCodec.LOCAL_KIND,
|
||||
"game_version": "0.18.1-alpha",
|
||||
"created_at_unix": 1,
|
||||
"payload_sha256": PortableFileGuard.hash_bytes(
|
||||
payload_json.to_utf8_buffer()
|
||||
),
|
||||
"payload_json": payload_json,
|
||||
}
|
||||
var file: FileAccess = FileAccess.open_encrypted_with_pass(
|
||||
path,
|
||||
FileAccess.WRITE,
|
||||
ProgressionSaveCodec.CONTAINER_PASSPHRASE,
|
||||
)
|
||||
assert(file != null)
|
||||
file.store_string(JSON.stringify(envelope))
|
||||
file.close()
|
||||
var decoded: Dictionary = ProgressionSaveCodec.read_local_save(path)
|
||||
assert(bool(decoded.get("ok", false)))
|
||||
assert(bool(decoded.get("legacy_container", false)))
|
||||
|
||||
|
||||
func _assert_legacy_data_manifest(parent: String) -> void:
|
||||
var legacy_root: String = parent.path_join("netfishing-manifest-root")
|
||||
assert(DirAccess.make_dir_recursive_absolute(legacy_root) == OK)
|
||||
var legacy_id: String = "0123456789abcdef0123456789abcdef"
|
||||
var legacy_manifest: Dictionary = {
|
||||
"format_version": PlayerDataRoot.MANIFEST_VERSION,
|
||||
"layout_version": PlayerDataRoot.LAYOUT_VERSION,
|
||||
"application": PlayerDataRoot.LEGACY_APPLICATION_ID,
|
||||
"root_id": legacy_id,
|
||||
"created_at_unix": 1,
|
||||
"last_opened_at_unix": 1,
|
||||
}
|
||||
var legacy_path: String = legacy_root.path_join(
|
||||
PlayerDataRoot.LEGACY_MANIFEST_FILENAME
|
||||
)
|
||||
var file := FileAccess.open(legacy_path, FileAccess.WRITE)
|
||||
assert(file != null)
|
||||
file.store_string(JSON.stringify(legacy_manifest, "\t"))
|
||||
file.close()
|
||||
var legacy_data_root := PlayerDataRoot.new()
|
||||
root.add_child(legacy_data_root)
|
||||
assert(legacy_data_root.use_existing_root(legacy_root))
|
||||
assert(legacy_data_root.root_id == legacy_id)
|
||||
assert(FileAccess.file_exists(legacy_path))
|
||||
var canonical_path: String = legacy_root.path_join(
|
||||
PlayerDataRoot.MANIFEST_FILENAME
|
||||
)
|
||||
assert(FileAccess.file_exists(canonical_path))
|
||||
var parser := JSON.new()
|
||||
assert(parser.parse(FileAccess.get_file_as_string(canonical_path)) == OK)
|
||||
assert((parser.data as Dictionary).get("application") == PlayerDataRoot.APPLICATION_ID)
|
||||
legacy_data_root.queue_free()
|
||||
|
||||
|
||||
func _assert_legacy_identity_backup(main: Node, data_root: PlayerDataRoot) -> void:
|
||||
var player_identity := main.get("_player_identity") as PlayerIdentityStore
|
||||
var identity_backups := main.get("_identity_backups") as IdentityBackupService
|
||||
assert(player_identity != null and player_identity.is_ready())
|
||||
assert(identity_backups != null)
|
||||
var material: Dictionary = player_identity.export_identity_material()
|
||||
var fingerprint: String = str(material.get("fingerprint", ""))
|
||||
var private_key := CryptoKey.new()
|
||||
assert(private_key.load_from_string(str(material.get("private_pem", ""))) == OK)
|
||||
var canonical: PackedByteArray = NetworkIdentityCrypto.canonical_bytes_for_prefix(
|
||||
NetworkIdentityCrypto.LEGACY_DOMAIN_PREFIX,
|
||||
"identity_backup_self_test",
|
||||
["player", fingerprint],
|
||||
)
|
||||
var context := HashingContext.new()
|
||||
assert(context.start(HashingContext.HASH_SHA256) == OK)
|
||||
context.update(canonical)
|
||||
var signature: PackedByteArray = Crypto.new().sign(
|
||||
HashingContext.HASH_SHA256,
|
||||
context.finish(),
|
||||
private_key,
|
||||
)
|
||||
var envelope: Dictionary = {
|
||||
"magic": IdentityBackupService.LEGACY_MAGIC,
|
||||
"format_version": IdentityBackupService.FORMAT_VERSION,
|
||||
"identity_type": "player",
|
||||
"algorithm": NetworkIdentityCrypto.ALGORITHM,
|
||||
"private_pem": material.get("private_pem", ""),
|
||||
"public_pem": material.get("public_pem", ""),
|
||||
"fingerprint": fingerprint,
|
||||
"created_at_unix": 1,
|
||||
"source_device_id": data_root.device_id,
|
||||
"self_signature": Marshalls.raw_to_base64(signature),
|
||||
}
|
||||
var backup_path: String = data_root.identity_backup_directory().path_join(
|
||||
"netfishing-player.nfidentity"
|
||||
)
|
||||
var passphrase := "legacy-test-passphrase"
|
||||
var file: FileAccess = FileAccess.open_encrypted_with_pass(
|
||||
backup_path,
|
||||
FileAccess.WRITE,
|
||||
passphrase,
|
||||
)
|
||||
assert(file != null)
|
||||
file.store_string(JSON.stringify(envelope))
|
||||
file.close()
|
||||
var inspected: Dictionary = identity_backups.inspect_backup(
|
||||
backup_path,
|
||||
passphrase,
|
||||
"player",
|
||||
)
|
||||
assert(bool(inspected.get("ok", false)))
|
||||
assert(str(inspected.get("fingerprint", "")) == fingerprint)
|
||||
|
||||
|
||||
func _assert_opaque(path: String) -> void:
|
||||
var bytes: PackedByteArray = PortableFileGuard.read_bytes(path)
|
||||
assert(not bytes.is_empty())
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ signal shop_backdrop_visibility_changed(is_visible: bool)
|
|||
signal virtual_pointer_mode_changed(is_active: bool)
|
||||
signal social_prompt_accepted
|
||||
signal social_prompt_declined
|
||||
signal data_root_changed
|
||||
|
||||
const VIRTUAL_MOUSE_INPUT_OWNER: StringName = &"controller_virtual_mouse"
|
||||
const EMOTE_RADIAL_CAMERA_OWNER: StringName = &"emote_radial_menu"
|
||||
|
|
@ -271,6 +272,8 @@ func _ready() -> void:
|
|||
_pause_settings_panel.crisp_reset_focus_requested.connect(
|
||||
crisp_reset_focus_requested.emit
|
||||
)
|
||||
_title_settings_panel.data_root_changed.connect(data_root_changed.emit)
|
||||
_pause_settings_panel.data_root_changed.connect(data_root_changed.emit)
|
||||
if not Input.joy_connection_changed.is_connected(
|
||||
_on_controller_connection_changed
|
||||
):
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ signal opened
|
|||
signal crisp_reset_focus_requested
|
||||
signal panel_visibility_changed(is_visible: bool)
|
||||
signal navigation_transition_started
|
||||
signal data_root_changed
|
||||
|
||||
enum PresentationMode {
|
||||
TITLE_EMBEDDED,
|
||||
|
|
@ -612,6 +613,7 @@ func _refresh_data_page() -> void:
|
|||
%DataStorageMode.text = "storage mode: " + _data_root.storage_mode_text()
|
||||
%ChangeDataFolder.disabled = (
|
||||
_data_root.override_active
|
||||
or _presentation_mode != PresentationMode.TITLE_EMBEDDED
|
||||
or (_network_session != null and _network_session.is_session_active())
|
||||
)
|
||||
var fingerprint: String = (
|
||||
|
|
@ -668,6 +670,9 @@ func _choose_data_folder() -> void:
|
|||
if _data_root == null or _data_root.override_active:
|
||||
_feedback.text = "the data folder is externally managed."
|
||||
return
|
||||
if _presentation_mode != PresentationMode.TITLE_EMBEDDED:
|
||||
_feedback.text = "return to title to change the data folder."
|
||||
return
|
||||
if _network_session != null and _network_session.is_session_active():
|
||||
_feedback.text = "return to title to change the data folder."
|
||||
return
|
||||
|
|
@ -684,6 +689,12 @@ func _choose_data_folder() -> void:
|
|||
|
||||
|
||||
func _change_data_folder(path: String) -> void:
|
||||
if (
|
||||
_presentation_mode != PresentationMode.TITLE_EMBEDDED
|
||||
or (_network_session != null and _network_session.is_session_active())
|
||||
):
|
||||
_feedback.text = "return to title to change the data folder."
|
||||
return
|
||||
var result: Dictionary = PortableDataMigration.migrate_active_to(
|
||||
_data_root, path
|
||||
)
|
||||
|
|
@ -697,9 +708,7 @@ func _change_data_folder(path: String) -> void:
|
|||
).to_lower()
|
||||
_refresh_data_page()
|
||||
return
|
||||
_feedback.text = "data folder changed. straywild will close safely."
|
||||
_refresh_data_page()
|
||||
get_tree().call_deferred("quit")
|
||||
_complete_data_root_change("data folder changed.")
|
||||
else:
|
||||
_feedback.text = str(
|
||||
result.get("message", "could not change the data folder.")
|
||||
|
|
@ -717,8 +726,7 @@ func _show_existing_data_folder_choice(path: String) -> void:
|
|||
dialog.add_button("replace selected data", false, "replace")
|
||||
dialog.confirmed.connect(func() -> void:
|
||||
if _data_root.use_existing_root(path):
|
||||
_feedback.text = "data folder changed. straywild will close safely."
|
||||
get_tree().call_deferred("quit")
|
||||
_complete_data_root_change("data folder changed.")
|
||||
else:
|
||||
_feedback.text = _data_root.error_message
|
||||
dialog.queue_free()
|
||||
|
|
@ -733,7 +741,7 @@ func _show_existing_data_folder_choice(path: String) -> void:
|
|||
result.get("message", "could not replace selected data.")
|
||||
)
|
||||
if bool(result.get("ok", false)):
|
||||
get_tree().call_deferred("quit")
|
||||
_complete_data_root_change(_feedback.text.to_lower())
|
||||
dialog.queue_free()
|
||||
)
|
||||
_interface_fonts.apply_utility_theme(dialog)
|
||||
|
|
@ -744,6 +752,12 @@ func _show_existing_data_folder_choice(path: String) -> void:
|
|||
)
|
||||
|
||||
|
||||
func _complete_data_root_change(message: String) -> void:
|
||||
_feedback.text = message
|
||||
_refresh_data_page()
|
||||
data_root_changed.emit()
|
||||
|
||||
|
||||
func _choose_identity_export(identity_type: String) -> void:
|
||||
if not _identity_operation_allowed():
|
||||
return
|
||||
|
|
|
|||
|
|
@ -243,6 +243,11 @@ func reopen_to_menu() -> void:
|
|||
_set_title_bubbles_interactive(true)
|
||||
|
||||
|
||||
func refresh_after_data_root_change() -> void:
|
||||
_save_slots_page.refresh_page()
|
||||
_refresh_save_inspection()
|
||||
|
||||
|
||||
func open_join_game_page(endpoint: String = "") -> void:
|
||||
_cancel_title_entry_transition()
|
||||
_awaiting_start_input = false
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue