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
|
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
|
## Installing release builds
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,10 @@ directory. The templates in `scripts/portmaster/` are authoritative.
|
||||||
upgraded. Device-local data, configuration, and cache remain under
|
upgraded. Device-local data, configuration, and cache remain under
|
||||||
`straywild/conf/data`, `straywild/conf/config`, and
|
`straywild/conf/data`, `straywild/conf/config`, and
|
||||||
`straywild/conf/cache`.
|
`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,
|
- The archive must not contain `conf/`, saves, identities, logs, source files,
|
||||||
`.git`, or `.godot` content.
|
`.git`, or `.godot` content.
|
||||||
- Release downloads must publish only the canonical `straywild.zip` for this
|
- 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
|
--offline --no-check install ./straywild.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
Before upgrading an existing installation, preserve
|
Before upgrading an existing installation, preserve either the current
|
||||||
`/mnt/mmc/ports/straywild/conf/` and `/mnt/mmc/ports/saves/straywild/` on the
|
`/mnt/mmc/ports/straywild/` paths or the former `/mnt/mmc/ports/netfishing/`
|
||||||
device. After installation:
|
paths on the device. After installation:
|
||||||
|
|
||||||
1. Confirm the installed executable and PCK hashes match the staged release.
|
1. Confirm the installed executable and PCK hashes match the staged release.
|
||||||
2. Confirm HarbourMaster added its `# PORTMASTER:` installation signature to
|
2. Confirm HarbourMaster added its `# PORTMASTER:` installation signature to
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,10 @@ for local README files.
|
||||||
|
|
||||||
## Related repositories
|
## 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
|
stays with the service implementation and defines its versioned HTTP
|
||||||
contract.
|
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
|
stays with its installer and update scripts. It consumes this repository's
|
||||||
attribution and legal notices from the exact pinned game commit instead of
|
attribution and legal notices from the exact pinned game commit instead of
|
||||||
maintaining copies.
|
maintaining copies.
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ script_export_mode=2
|
||||||
|
|
||||||
custom_template/debug=""
|
custom_template/debug=""
|
||||||
custom_template/release=""
|
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="res://art/exported/system_icons/straywild_1024.png"
|
||||||
application/icon_interpolation=0
|
application/icon_interpolation=0
|
||||||
application/short_version="0.18.1"
|
application/short_version="0.18.1"
|
||||||
|
|
@ -178,7 +178,7 @@ architectures/x86=false
|
||||||
architectures/x86_64=false
|
architectures/x86_64=false
|
||||||
version/code=180100
|
version/code=180100
|
||||||
version/name="v0.18.1-alpha"
|
version/name="v0.18.1-alpha"
|
||||||
package/unique_name="io.woofmeow.straywild"
|
package/unique_name="io.woofmeow.netfishing"
|
||||||
package/name="straywild"
|
package/name="straywild"
|
||||||
package/signed=true
|
package/signed=true
|
||||||
package/app_category=0
|
package/app_category=0
|
||||||
|
|
|
||||||
19
main/main.gd
19
main/main.gd
|
|
@ -277,6 +277,7 @@ var _rain_ambience: RainAmbienceType
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
_data_root.prepare_legacy_user_data()
|
||||||
_performance_profile = RuntimePerformanceProfileType.from_environment()
|
_performance_profile = RuntimePerformanceProfileType.from_environment()
|
||||||
_dedicated_runtime = _is_dedicated_server_runtime()
|
_dedicated_runtime = _is_dedicated_server_runtime()
|
||||||
if _dedicated_runtime:
|
if _dedicated_runtime:
|
||||||
|
|
@ -471,10 +472,7 @@ func _start_dedicated_server() -> void:
|
||||||
_fail_dedicated_server(config.error_message)
|
_fail_dedicated_server(config.error_message)
|
||||||
return
|
return
|
||||||
var data_path: String = config.data_directory
|
var data_path: String = config.data_directory
|
||||||
var manifest_path: String = data_path.path_join(
|
if not _data_root.has_data_manifest(data_path):
|
||||||
PlayerDataRoot.MANIFEST_FILENAME
|
|
||||||
)
|
|
||||||
if not FileAccess.file_exists(manifest_path):
|
|
||||||
var created: Dictionary = _data_root.create_unbound_root(data_path)
|
var created: Dictionary = _data_root.create_unbound_root(data_path)
|
||||||
if not bool(created.get("ok", false)):
|
if not bool(created.get("ok", false)):
|
||||||
_fail_dedicated_server(str(created.get(
|
_fail_dedicated_server(str(created.get(
|
||||||
|
|
@ -973,6 +971,7 @@ func _initialize_application(dedicated: bool) -> void:
|
||||||
_network_session,
|
_network_session,
|
||||||
_interface_fonts,
|
_interface_fonts,
|
||||||
)
|
)
|
||||||
|
_game_ui.data_root_changed.connect(_on_data_root_changed)
|
||||||
_game_ui.setup_controller_mapping(_controller_mapping_manager)
|
_game_ui.setup_controller_mapping(_controller_mapping_manager)
|
||||||
_ui_pixelation.setup_controller_mapping(_controller_mapping_manager)
|
_ui_pixelation.setup_controller_mapping(_controller_mapping_manager)
|
||||||
_game_ui.setup_keyboard_mouse_mapping(_keyboard_mouse_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())
|
_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(
|
func _on_server_trust_required(
|
||||||
endpoint: String,
|
endpoint: String,
|
||||||
expected_fingerprint: String,
|
expected_fingerprint: String,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,10 @@ const PROFILE_ENVIRONMENT_VARIABLE: String = (
|
||||||
"straywild_PERFORMANCE_PROFILE"
|
"straywild_PERFORMANCE_PROFILE"
|
||||||
)
|
)
|
||||||
const LEGACY_LIGHT_ENVIRONMENT_VARIABLE: String = "straywild_LOW_END"
|
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 NORMAL_PROFILE: StringName = &"normal"
|
||||||
const LIGHT_PROFILE: StringName = &"light"
|
const LIGHT_PROFILE: StringName = &"light"
|
||||||
const NORMAL_WORLD_RENDER_SCALE: float = 1.0
|
const NORMAL_WORLD_RENDER_SCALE: float = 1.0
|
||||||
|
|
@ -14,9 +18,19 @@ var _profile_name: StringName = NORMAL_PROFILE
|
||||||
|
|
||||||
|
|
||||||
static func from_environment() -> RuntimePerformanceProfile:
|
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(
|
return from_name(
|
||||||
StringName(OS.get_environment(PROFILE_ENVIRONMENT_VARIABLE)),
|
StringName(profile_name),
|
||||||
OS.get_environment(LEGACY_LIGHT_ENVIRONMENT_VARIABLE) == "1",
|
(
|
||||||
|
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_SETTING: String = "network/discovery/base_url"
|
||||||
const BASE_URL_ENVIRONMENT: String = "straywild_DISCOVERY_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 SETTINGS_PATH: String = "user://network_discovery.cfg"
|
||||||
const DEFAULT_ROOM_NAME: String = "Player's Server"
|
const DEFAULT_ROOM_NAME: String = "Player's Server"
|
||||||
const LEGACY_DEFAULT_ROOM_NAME: String = "straywild Room"
|
const LEGACY_DEFAULT_ROOM_NAME: String = "straywild Room"
|
||||||
|
|
@ -553,6 +554,10 @@ func is_own_room(room: Dictionary) -> bool:
|
||||||
|
|
||||||
func _configured_base_url() -> String:
|
func _configured_base_url() -> String:
|
||||||
var value: String = OS.get_environment(BASE_URL_ENVIRONMENT).strip_edges()
|
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():
|
if value.is_empty():
|
||||||
value = str(ProjectSettings.get_setting(BASE_URL_SETTING, "")).strip_edges()
|
value = str(ProjectSettings.get_setting(BASE_URL_SETTING, "")).strip_edges()
|
||||||
while value.ends_with("/"):
|
while value.ends_with("/"):
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,10 @@ var _data_root: PlayerDataRoot
|
||||||
func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
|
func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
|
||||||
_store_path = path
|
_store_path = path
|
||||||
_data_root = data_root
|
_data_root = data_root
|
||||||
|
_namespaces.clear()
|
||||||
|
_loaded = false
|
||||||
|
_write_blocked = false
|
||||||
|
_expected_hash = ""
|
||||||
|
|
||||||
|
|
||||||
func is_banned(host_fingerprint: String, target_fingerprint: String) -> bool:
|
func is_banned(host_fingerprint: String, target_fingerprint: String) -> bool:
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ extends Node
|
||||||
signal operation_finished(success: bool, message: String)
|
signal operation_finished(success: bool, message: String)
|
||||||
|
|
||||||
const MAGIC := "straywild_IDENTITY_BACKUP"
|
const MAGIC := "straywild_IDENTITY_BACKUP"
|
||||||
|
const LEGACY_MAGIC := "NETFISHING_IDENTITY_BACKUP"
|
||||||
const FORMAT_VERSION := 1
|
const FORMAT_VERSION := 1
|
||||||
const MAX_BACKUP_BYTES := 256 * 1024
|
const MAX_BACKUP_BYTES := 256 * 1024
|
||||||
const MIN_PASSPHRASE_LENGTH := 12
|
const MIN_PASSPHRASE_LENGTH := 12
|
||||||
|
|
@ -110,8 +111,9 @@ func inspect_backup(
|
||||||
if json.parse(text) != OK or typeof(json.data) != TYPE_DICTIONARY:
|
if json.parse(text) != OK or typeof(json.data) != TYPE_DICTIONARY:
|
||||||
return {"ok": false}
|
return {"ok": false}
|
||||||
var data: Dictionary = json.data
|
var data: Dictionary = json.data
|
||||||
|
var legacy_backup: bool = data.get("magic") == LEGACY_MAGIC
|
||||||
if (
|
if (
|
||||||
data.get("magic") != MAGIC
|
data.get("magic") not in [MAGIC, LEGACY_MAGIC]
|
||||||
or data.get("format_version") != FORMAT_VERSION
|
or data.get("format_version") != FORMAT_VERSION
|
||||||
or data.get("identity_type") != expected_type
|
or data.get("identity_type") != expected_type
|
||||||
or data.get("algorithm") != NetworkIdentityCrypto.ALGORITHM
|
or data.get("algorithm") != NetworkIdentityCrypto.ALGORITHM
|
||||||
|
|
@ -134,12 +136,23 @@ func inspect_backup(
|
||||||
var signature: PackedByteArray = Marshalls.base64_to_raw(
|
var signature: PackedByteArray = Marshalls.base64_to_raw(
|
||||||
str(data.get("self_signature", ""))
|
str(data.get("self_signature", ""))
|
||||||
)
|
)
|
||||||
if not NetworkIdentityCrypto.verify_fields(
|
var public_key: CryptoKey = NetworkIdentityCrypto.load_public_key(public_pem)
|
||||||
NetworkIdentityCrypto.load_public_key(public_pem),
|
var backup_signature_valid: bool = (
|
||||||
"identity_backup_self_test",
|
NetworkIdentityCrypto.verify_legacy_fields(
|
||||||
[expected_type, fingerprint],
|
public_key,
|
||||||
signature,
|
"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}
|
return {"ok": false}
|
||||||
var fresh: PackedByteArray = NetworkIdentityCrypto.sign_fields(
|
var fresh: PackedByteArray = NetworkIdentityCrypto.sign_fields(
|
||||||
key, "identity_import_self_test", [fingerprint]
|
key, "identity_import_self_test", [fingerprint]
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,10 @@ var _data_root: PlayerDataRoot
|
||||||
func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
|
func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
|
||||||
_store_path = path
|
_store_path = path
|
||||||
_data_root = data_root
|
_data_root = data_root
|
||||||
|
_records.clear()
|
||||||
|
_loaded = false
|
||||||
|
_write_blocked = false
|
||||||
|
_expected_hash = ""
|
||||||
|
|
||||||
|
|
||||||
func observe(fingerprint: String, display_name: String) -> String:
|
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_PUBLIC_KEY_BYTES: int = 8192
|
||||||
const MAX_SIGNATURE_BYTES: int = 1024
|
const MAX_SIGNATURE_BYTES: int = 1024
|
||||||
const DOMAIN_PREFIX: String = "straywild"
|
const DOMAIN_PREFIX: String = "straywild"
|
||||||
|
const LEGACY_DOMAIN_PREFIX: String = "NETFISHING"
|
||||||
const IDENTITY_VERSION: String = "identity_v1"
|
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:
|
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()
|
var output := PackedByteArray()
|
||||||
_append_string(output, DOMAIN_PREFIX)
|
_append_string(output, prefix)
|
||||||
_append_string(output, IDENTITY_VERSION)
|
_append_string(output, IDENTITY_VERSION)
|
||||||
_append_string(output, domain)
|
_append_string(output, domain)
|
||||||
for value: Variant in fields:
|
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:
|
static func load_public_key(public_pem: String) -> CryptoKey:
|
||||||
var normalized := normalize_public_pem(public_pem)
|
var normalized := normalize_public_pem(public_pem)
|
||||||
if normalized.is_empty() or normalized.to_utf8_buffer().size() > MAX_PUBLIC_KEY_BYTES:
|
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:
|
func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
|
||||||
_profile_path = path
|
_profile_path = path
|
||||||
_data_root = data_root
|
_data_root = data_root
|
||||||
|
_expected_hash = ""
|
||||||
|
_future_version = false
|
||||||
|
|
||||||
|
|
||||||
func _temp_path() -> String:
|
func _temp_path() -> String:
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,17 @@ func setup(
|
||||||
_apply_local_voice_to_avatar()
|
_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:
|
func get_persisted_name() -> String:
|
||||||
return _preferences.display_name
|
return _preferences.display_name
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,41 @@ const BOOTSTRAP_VERSION := 1
|
||||||
const MANIFEST_VERSION := 1
|
const MANIFEST_VERSION := 1
|
||||||
const LAYOUT_VERSION := 1
|
const LAYOUT_VERSION := 1
|
||||||
const MANIFEST_FILENAME := "straywild_data.json"
|
const MANIFEST_FILENAME := "straywild_data.json"
|
||||||
|
const LEGACY_MANIFEST_FILENAME := "netfishing_data.json"
|
||||||
const README_FILENAME := "README.txt"
|
const README_FILENAME := "README.txt"
|
||||||
const ENVIRONMENT_VARIABLE := "straywild_DATA_DIR"
|
const ENVIRONMENT_VARIABLE := "straywild_DATA_DIR"
|
||||||
const CREATE_ENVIRONMENT_VARIABLE := "straywild_CREATE_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 APPLICATION_ID := "straywild"
|
||||||
|
const LEGACY_APPLICATION_ID := "netfishing"
|
||||||
const APP_DATA_PORTABLE_PATH := "user://portable-data"
|
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_ALLOWED: StringName = &"allowed"
|
||||||
const PATH_SOURCE_PROJECT: StringName = &"source_project"
|
const PATH_SOURCE_PROJECT: StringName = &"source_project"
|
||||||
const PATH_INSTALLATION: StringName = &"installation"
|
const PATH_INSTALLATION: StringName = &"installation"
|
||||||
|
|
@ -37,23 +67,29 @@ var override_active := false
|
||||||
|
|
||||||
|
|
||||||
func resolve() -> bool:
|
func resolve() -> bool:
|
||||||
|
prepare_legacy_user_data()
|
||||||
_load_bootstrap_identity()
|
_load_bootstrap_identity()
|
||||||
var command_line: String = _command_line_override()
|
var command_line: String = _command_line_override()
|
||||||
if not command_line.is_empty():
|
if not command_line.is_empty():
|
||||||
override_active = true
|
override_active = true
|
||||||
mode = Mode.COMMAND_LINE_OVERRIDE
|
mode = Mode.COMMAND_LINE_OVERRIDE
|
||||||
return _activate_existing(command_line, "", false)
|
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
|
override_active = true
|
||||||
mode = Mode.ENVIRONMENT_OVERRIDE
|
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():
|
if not environment_path.is_absolute_path():
|
||||||
return _fail("straywild_DATA_DIR must be an absolute path.")
|
return _fail("straywild_DATA_DIR must be an absolute path.")
|
||||||
if (
|
if (
|
||||||
OS.get_environment(CREATE_ENVIRONMENT_VARIABLE) == "1"
|
_environment_flag(
|
||||||
and not FileAccess.file_exists(
|
CREATE_ENVIRONMENT_VARIABLE,
|
||||||
environment_path.path_join(MANIFEST_FILENAME)
|
LEGACY_CREATE_ENVIRONMENT_VARIABLE,
|
||||||
)
|
)
|
||||||
|
and not has_data_manifest(environment_path)
|
||||||
):
|
):
|
||||||
var created: Dictionary = create_unbound_root(environment_path)
|
var created: Dictionary = create_unbound_root(environment_path)
|
||||||
if not bool(created.get("ok", false)):
|
if not bool(created.get("ok", false)):
|
||||||
|
|
@ -80,6 +116,30 @@ func resolve() -> bool:
|
||||||
return false
|
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:
|
func default_visible_path() -> String:
|
||||||
var documents: String = OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS)
|
var documents: String = OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS)
|
||||||
if documents.is_empty():
|
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)
|
normalized = ProjectSettings.globalize_path(APP_DATA_PORTABLE_PATH)
|
||||||
if not _validate_candidate(normalized, true):
|
if not _validate_candidate(normalized, true):
|
||||||
return false
|
return false
|
||||||
var manifest_path: String = normalized.path_join(MANIFEST_FILENAME)
|
var manifest_path: String = _existing_manifest_path(normalized)
|
||||||
if FileAccess.file_exists(manifest_path):
|
if not manifest_path.is_empty():
|
||||||
var manifest: Dictionary = _read_json(manifest_path)
|
var manifest: Dictionary = _read_json(manifest_path)
|
||||||
if not _valid_manifest(manifest):
|
if not _valid_manifest(manifest):
|
||||||
return _fail("The selected folder has a malformed straywild manifest.")
|
return _fail("The selected folder has a malformed straywild manifest.")
|
||||||
root_id = str(manifest["root_id"])
|
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:
|
else:
|
||||||
if not _directory_is_empty(normalized) and not app_data:
|
if not _directory_is_empty(normalized) and not app_data:
|
||||||
return _fail(
|
return _fail(
|
||||||
|
|
@ -145,14 +207,14 @@ func create_app_data_layout_for_migration(path: String) -> Dictionary:
|
||||||
var normalized: String = _normalize(path)
|
var normalized: String = _normalize(path)
|
||||||
if not _validate_candidate(normalized, false):
|
if not _validate_candidate(normalized, false):
|
||||||
return {"ok": false, "message": error_message}
|
return {"ok": false, "message": error_message}
|
||||||
var manifest_path: String = normalized.path_join(MANIFEST_FILENAME)
|
var manifest_path: String = _existing_manifest_path(normalized)
|
||||||
if FileAccess.file_exists(manifest_path):
|
if not manifest_path.is_empty():
|
||||||
var manifest: Dictionary = _read_json(manifest_path)
|
var manifest: Dictionary = _read_json(manifest_path)
|
||||||
return (
|
if not _valid_manifest(manifest):
|
||||||
{"ok": true, "root_id": str(manifest.get("root_id", ""))}
|
return {"ok": false, "message": "The app-data manifest is malformed."}
|
||||||
if _valid_manifest(manifest)
|
if not _upgrade_legacy_manifest(normalized, manifest, manifest_path):
|
||||||
else {"ok": false, "message": "The app-data manifest is malformed."}
|
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()
|
var id: String = Crypto.new().generate_random_bytes(16).hex_encode()
|
||||||
if not _create_layout(normalized, id):
|
if not _create_layout(normalized, id):
|
||||||
return {"ok": false, "message": "The app-data layout could not be created."}
|
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)
|
var normalized: String = _normalize(path)
|
||||||
if not _validate_candidate(normalized, permit_creation):
|
if not _validate_candidate(normalized, permit_creation):
|
||||||
return false
|
return false
|
||||||
var manifest_path: String = normalized.path_join(MANIFEST_FILENAME)
|
var manifest_path: String = _existing_manifest_path(normalized)
|
||||||
if not FileAccess.file_exists(manifest_path):
|
if manifest_path.is_empty():
|
||||||
return _fail("The selected folder is not a straywild data folder.")
|
return _fail("The selected folder is not a straywild data folder.")
|
||||||
var manifest: Dictionary = _read_json(manifest_path)
|
var manifest: Dictionary = _read_json(manifest_path)
|
||||||
if not _valid_manifest(manifest):
|
if not _valid_manifest(manifest):
|
||||||
return _fail("The straywild data-folder manifest is malformed.")
|
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"])
|
var found_id: String = str(manifest["root_id"])
|
||||||
if not expected_id.is_empty() and expected_id != found_id:
|
if not expected_id.is_empty() and expected_id != found_id:
|
||||||
return _fail("The selected data folder does not match this device pointer.")
|
return _fail("The selected data folder does not match this device pointer.")
|
||||||
|
|
@ -422,12 +486,71 @@ func _valid_manifest(data: Dictionary) -> bool:
|
||||||
return (
|
return (
|
||||||
data.get("format_version") == MANIFEST_VERSION
|
data.get("format_version") == MANIFEST_VERSION
|
||||||
and data.get("layout_version") == LAYOUT_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 typeof(data.get("root_id")) == TYPE_STRING
|
||||||
and str(data["root_id"]).length() == 32
|
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:
|
func _directory_is_empty(path: String) -> bool:
|
||||||
var access: DirAccess = DirAccess.open(path)
|
var access: DirAccess = DirAccess.open(path)
|
||||||
if access == null:
|
if access == null:
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,10 @@ var _data_root: PlayerDataRoot
|
||||||
func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
|
func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
|
||||||
_store_path = path
|
_store_path = path
|
||||||
_data_root = data_root
|
_data_root = data_root
|
||||||
|
_records.clear()
|
||||||
|
_loaded = false
|
||||||
|
_write_blocked = false
|
||||||
|
_expected_hash = ""
|
||||||
|
|
||||||
|
|
||||||
func is_muted(fingerprint: String) -> bool:
|
func is_muted(fingerprint: String) -> bool:
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,7 @@ static func migrate_legacy_to(
|
||||||
) -> Dictionary:
|
) -> Dictionary:
|
||||||
var normalized: String = destination.simplify_path().trim_suffix("/")
|
var normalized: String = destination.simplify_path().trim_suffix("/")
|
||||||
if DirAccess.dir_exists_absolute(normalized):
|
if DirAccess.dir_exists_absolute(normalized):
|
||||||
var existing_manifest: String = normalized.path_join(
|
if data_root.has_data_manifest(normalized):
|
||||||
PlayerDataRoot.MANIFEST_FILENAME
|
|
||||||
)
|
|
||||||
if FileAccess.file_exists(existing_manifest):
|
|
||||||
return {
|
return {
|
||||||
"ok": false,
|
"ok": false,
|
||||||
"requires_existing_root_decision": true,
|
"requires_existing_root_decision": true,
|
||||||
|
|
@ -106,7 +103,7 @@ static func migrate_active_to(
|
||||||
"message": "This data folder is already active.",
|
"message": "This data folder is already active.",
|
||||||
}
|
}
|
||||||
if DirAccess.dir_exists_absolute(normalized) and not _directory_empty(normalized):
|
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 {
|
return {
|
||||||
"ok": false,
|
"ok": false,
|
||||||
"requires_existing_root_decision": true,
|
"requires_existing_root_decision": true,
|
||||||
|
|
@ -207,8 +204,7 @@ static func replace_existing_with_legacy(
|
||||||
destination: String,
|
destination: String,
|
||||||
) -> Dictionary:
|
) -> Dictionary:
|
||||||
var normalized: String = destination.simplify_path().trim_suffix("/")
|
var normalized: String = destination.simplify_path().trim_suffix("/")
|
||||||
var manifest: String = normalized.path_join(PlayerDataRoot.MANIFEST_FILENAME)
|
if not data_root.has_data_manifest(normalized):
|
||||||
if not FileAccess.file_exists(manifest):
|
|
||||||
return {"ok": false, "message": "The selected folder is not a straywild data root."}
|
return {"ok": false, "message": "The selected folder is not a straywild data root."}
|
||||||
var recovery: String = ProjectSettings.globalize_path(
|
var recovery: String = ProjectSettings.globalize_path(
|
||||||
"user://migration-recovery"
|
"user://migration-recovery"
|
||||||
|
|
@ -229,7 +225,7 @@ static func replace_existing_with_active(
|
||||||
destination: String,
|
destination: String,
|
||||||
) -> Dictionary:
|
) -> Dictionary:
|
||||||
var normalized: String = destination.simplify_path().trim_suffix("/")
|
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."}
|
return {"ok": false, "message": "The selected folder is not a straywild data root."}
|
||||||
var recovery: String = ProjectSettings.globalize_path(
|
var recovery: String = ProjectSettings.globalize_path(
|
||||||
"user://migration-recovery"
|
"user://migration-recovery"
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,12 @@ var _data_root: PlayerDataRoot
|
||||||
func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
|
func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
|
||||||
_store_path = path
|
_store_path = path
|
||||||
_data_root = data_root
|
_data_root = data_root
|
||||||
|
_saved_entries.clear()
|
||||||
|
_recent_entries.clear()
|
||||||
|
_loaded = false
|
||||||
|
_recovery_warning = ""
|
||||||
|
_write_blocked = false
|
||||||
|
_expected_hash = ""
|
||||||
|
|
||||||
|
|
||||||
func _temp_path() -> String:
|
func _temp_path() -> String:
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,10 @@ var _data_root: PlayerDataRoot
|
||||||
func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
|
func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
|
||||||
_store_path = path
|
_store_path = path
|
||||||
_data_root = data_root
|
_data_root = data_root
|
||||||
|
_records.clear()
|
||||||
|
_loaded = false
|
||||||
|
_write_blocked = false
|
||||||
|
_expected_hash = ""
|
||||||
|
|
||||||
|
|
||||||
func verify(endpoint: ConnectionEndpoint, fingerprint: String) -> Verification:
|
func verify(endpoint: ConnectionEndpoint, fingerprint: String) -> Verification:
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,10 @@ var _data_root: PlayerDataRoot
|
||||||
func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
|
func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
|
||||||
_profile_path = path
|
_profile_path = path
|
||||||
_data_root = data_root
|
_data_root = data_root
|
||||||
|
_snapshot = CharacterCustomizationCatalog.default_snapshot()
|
||||||
|
_loaded = false
|
||||||
|
_future_version = false
|
||||||
|
_expected_hash = ""
|
||||||
|
|
||||||
|
|
||||||
func _temp_path() -> String:
|
func _temp_path() -> String:
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ class_name ProgressionSaveCodec
|
||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
|
||||||
const MAGIC := "straywild_PROGRESSION_CONTAINER"
|
const MAGIC := "straywild_PROGRESSION_CONTAINER"
|
||||||
|
const LEGACY_MAGIC := "NETFISHING_PROGRESSION_CONTAINER"
|
||||||
const FORMAT_VERSION := 1
|
const FORMAT_VERSION := 1
|
||||||
const LOCAL_KIND := "local_save"
|
const LOCAL_KIND := "local_save"
|
||||||
const ARCHIVE_KIND := "portable_archive"
|
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
|
# 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.
|
# 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 := (
|
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
|
or FileAccess.get_size(path) > MAX_CONTAINER_BYTES
|
||||||
):
|
):
|
||||||
return {"ok": false, "message": "progression file is unavailable."}
|
return {"ok": false, "message": "progression file is unavailable."}
|
||||||
var file: FileAccess = FileAccess.open_encrypted_with_pass(
|
var envelope_text: String = _read_encrypted_text(path)
|
||||||
path,
|
if envelope_text.is_empty():
|
||||||
FileAccess.READ,
|
|
||||||
CONTAINER_PASSPHRASE,
|
|
||||||
)
|
|
||||||
if file == null:
|
|
||||||
return {"ok": false, "message": "progression file could not be opened."}
|
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()
|
var envelope_json := JSON.new()
|
||||||
if (
|
if (
|
||||||
envelope_json.parse(envelope_text) != OK
|
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."}
|
return {"ok": false, "message": "progression container is malformed."}
|
||||||
var envelope: Dictionary = envelope_json.data
|
var envelope: Dictionary = envelope_json.data
|
||||||
|
var legacy_container: bool = envelope.get("magic") == LEGACY_MAGIC
|
||||||
if (
|
if (
|
||||||
envelope.get("magic") != MAGIC
|
envelope.get("magic") not in [MAGIC, LEGACY_MAGIC]
|
||||||
or int(envelope.get("format_version", -1)) != FORMAT_VERSION
|
or int(envelope.get("format_version", -1)) != FORMAT_VERSION
|
||||||
or str(envelope.get("kind", "")) != expected_kind
|
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", "")),
|
"game_version": str(envelope.get("game_version", "")),
|
||||||
"created_at_unix": int(envelope.get("created_at_unix", 0)),
|
"created_at_unix": int(envelope.get("created_at_unix", 0)),
|
||||||
"legacy_plaintext": false,
|
"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:
|
static func _read_plaintext_dictionary(path: String) -> Dictionary:
|
||||||
var bytes: PackedByteArray = PortableFileGuard.read_bytes(
|
var bytes: PackedByteArray = PortableFileGuard.read_bytes(
|
||||||
path,
|
path,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ readonly WINDOWS_DIR="${BUILD_ROOT}/windows-x86_64"
|
||||||
readonly LINUX_DIR="${BUILD_ROOT}/linux-x86_64"
|
readonly LINUX_DIR="${BUILD_ROOT}/linux-x86_64"
|
||||||
readonly README_SOURCE="${PROJECT_ROOT}/docs/README-PLAYTEST.txt"
|
readonly README_SOURCE="${PROJECT_ROOT}/docs/README-PLAYTEST.txt"
|
||||||
readonly SOURCE_COMMIT="$(git -C "${PROJECT_ROOT}" rev-parse HEAD)"
|
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 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 LINUX_ZIP="${BUILD_ROOT}/straywild-v0.18.1-alpha-linux-x86_64.zip"
|
||||||
readonly GODOT_BIN="${GODOT_BIN:-godot}"
|
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.
|
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}
|
Exact source revision: ${TAG_COMMIT}
|
||||||
Release tag: ${RELEASE_TAG}
|
Release tag: ${RELEASE_TAG}
|
||||||
PortMaster packaging revision: ${HEAD_COMMIT}
|
PortMaster packaging revision: ${HEAD_COMMIT}
|
||||||
|
|
@ -262,6 +262,10 @@ grep -Fq 'CONTROLLER_MAPPING_FILE="$CONFDIR/cache/controller_mapping.txt"' \
|
||||||
"${STAGE_ROOT}/straywild.sh"
|
"${STAGE_ROOT}/straywild.sh"
|
||||||
grep -Fq 'SAVEDIR="$PORTS_ROOT/saves/straywild"' \
|
grep -Fq 'SAVEDIR="$PORTS_ROOT/saves/straywild"' \
|
||||||
"${STAGE_ROOT}/straywild.sh"
|
"${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"' \
|
grep -Fq '"straywild_DATA_DIR=$SAVEDIR"' \
|
||||||
"${STAGE_ROOT}/straywild.sh"
|
"${STAGE_ROOT}/straywild.sh"
|
||||||
grep -Fq '"straywild_CREATE_DATA_DIR=1"' \
|
grep -Fq '"straywild_CREATE_DATA_DIR=1"' \
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,11 @@ GAMEDIR="/${directory}/ports/straywild"
|
||||||
CONFDIR="$GAMEDIR/conf"
|
CONFDIR="$GAMEDIR/conf"
|
||||||
PORTS_ROOT="${GAMEDIR%/straywild}"
|
PORTS_ROOT="${GAMEDIR%/straywild}"
|
||||||
SAVEDIR="$PORTS_ROOT/saves/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"
|
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_EXECUTABLE="$GAMEDIR/straywild.aarch64"
|
||||||
GAME_LAUNCHER="$GAMEDIR/launch-straywild.sh"
|
GAME_LAUNCHER="$GAMEDIR/launch-straywild.sh"
|
||||||
GPTOKEYB_CONFIG="$GAMEDIR/straywild.gptk"
|
GPTOKEYB_CONFIG="$GAMEDIR/straywild.gptk"
|
||||||
|
|
@ -33,13 +37,30 @@ HARBOURMASTER="$controlfolder/harbourmaster"
|
||||||
mkdir -p "$CONFDIR/data" "$CONFDIR/config" "$CONFDIR/cache" "$WESTON_DIR"
|
mkdir -p "$CONFDIR/data" "$CONFDIR/config" "$CONFDIR/cache" "$WESTON_DIR"
|
||||||
chmod +x "$GAME_EXECUTABLE"
|
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
|
# straywild uses a predictable save directory beside the port instead of
|
||||||
# presenting a folder picker on a small screen. Preserve an established
|
# presenting a folder picker on a small screen. Preserve an established
|
||||||
# data-root choice when upgrading an existing installation.
|
# data-root choice when upgrading an existing installation.
|
||||||
straywild_DATA_ENVIRONMENT=()
|
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" ] &&
|
||||||
[ ! -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
|
}; then
|
||||||
mkdir -p "$SAVEDIR"
|
mkdir -p "$SAVEDIR"
|
||||||
straywild_DATA_ENVIRONMENT+=(
|
straywild_DATA_ENVIRONMENT+=(
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,10 @@ static func from_runtime() -> DedicatedServerConfig:
|
||||||
result.discovery_url = str(ProjectSettings.get_setting(
|
result.discovery_url = str(ProjectSettings.get_setting(
|
||||||
"network/discovery/base_url", ""
|
"network/discovery/base_url", ""
|
||||||
)).strip_edges()
|
)).strip_edges()
|
||||||
var config_path: String = OS.get_environment(
|
var config_path: String = _environment_string(
|
||||||
"straywild_SERVER_CONFIG"
|
"straywild_SERVER_CONFIG",
|
||||||
|
"NETFISHING_SERVER_CONFIG",
|
||||||
|
"",
|
||||||
).strip_edges()
|
).strip_edges()
|
||||||
for argument: String in OS.get_cmdline_user_args():
|
for argument: String in OS.get_cmdline_user_args():
|
||||||
if argument.begins_with("--config="):
|
if argument.begins_with("--config="):
|
||||||
|
|
@ -87,36 +89,44 @@ func _load_file(path: String) -> bool:
|
||||||
|
|
||||||
func _apply_environment() -> void:
|
func _apply_environment() -> void:
|
||||||
server_name = _environment_string(
|
server_name = _environment_string(
|
||||||
"straywild_SERVER_NAME", server_name
|
"straywild_SERVER_NAME", "NETFISHING_SERVER_NAME", server_name
|
||||||
)
|
)
|
||||||
bind_address = _environment_string(
|
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(
|
max_players = _environment_int(
|
||||||
"straywild_SERVER_MAX_PLAYERS", max_players
|
"straywild_SERVER_MAX_PLAYERS",
|
||||||
|
"NETFISHING_SERVER_MAX_PLAYERS",
|
||||||
|
max_players,
|
||||||
)
|
)
|
||||||
world_seed = _environment_int(
|
world_seed = _environment_int(
|
||||||
"straywild_WORLD_SEED", world_seed
|
"straywild_WORLD_SEED", "NETFISHING_WORLD_SEED", world_seed
|
||||||
)
|
)
|
||||||
public_listing = _environment_bool(
|
public_listing = _environment_bool(
|
||||||
"straywild_SERVER_PUBLIC", public_listing
|
"straywild_SERVER_PUBLIC", "NETFISHING_SERVER_PUBLIC", public_listing
|
||||||
)
|
)
|
||||||
discovery_url = _environment_string(
|
discovery_url = _environment_string(
|
||||||
"straywild_DISCOVERY_URL", discovery_url
|
"straywild_DISCOVERY_URL", "NETFISHING_DISCOVERY_URL", discovery_url
|
||||||
)
|
)
|
||||||
data_directory = _environment_string(
|
data_directory = _environment_string(
|
||||||
"straywild_DATA_DIR", data_directory
|
"straywild_DATA_DIR", "NETFISHING_DATA_DIR", data_directory
|
||||||
)
|
)
|
||||||
chat_logging = _environment_bool(
|
chat_logging = _environment_bool(
|
||||||
"straywild_CHAT_LOGGING", chat_logging
|
"straywild_CHAT_LOGGING", "NETFISHING_CHAT_LOGGING", chat_logging
|
||||||
)
|
)
|
||||||
chat_log_path = _environment_string(
|
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(
|
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
|
return true
|
||||||
|
|
||||||
|
|
||||||
static func _environment_string(name: String, fallback: String) -> String:
|
static func _available_environment_variable(
|
||||||
return OS.get_environment(name) if OS.has_environment(name) else fallback
|
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 (
|
return (
|
||||||
_parse_int(OS.get_environment(name), fallback)
|
_parse_int(OS.get_environment(variable), fallback)
|
||||||
if OS.has_environment(name) else fallback
|
if not variable.is_empty() else fallback
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
static func _environment_bool(name: String, fallback: bool) -> bool:
|
static func _environment_bool(
|
||||||
if not OS.has_environment(name):
|
name: String,
|
||||||
|
legacy_name: String,
|
||||||
|
fallback: bool,
|
||||||
|
) -> bool:
|
||||||
|
var variable: String = _available_environment_variable(name, legacy_name)
|
||||||
|
if variable.is_empty():
|
||||||
return fallback
|
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"]
|
return value in ["1", "true", "yes", "on"]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,15 @@ func _run() -> void:
|
||||||
var decoded: Dictionary = ProgressionSaveCodec.read_local_save(save_path)
|
var decoded: Dictionary = ProgressionSaveCodec.read_local_save(save_path)
|
||||||
assert(bool(decoded.get("ok", false)))
|
assert(bool(decoded.get("ok", false)))
|
||||||
assert(int((decoded["data"] as Dictionary)["save_version"]) == 10)
|
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(
|
var archive_path: String = data_root.progression_backup_directory().path_join(
|
||||||
"progression-test.nfsave"
|
"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("%ExportProgression") == null)
|
||||||
assert(settings_panel.get_node_or_null("%ImportProgression") == null)
|
assert(settings_panel.get_node_or_null("%ImportProgression") == null)
|
||||||
var settings_panel: SettingsPanel = settings_panels.front() as SettingsPanel
|
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
|
var unchanged_root: String = data_root.root_path
|
||||||
settings_panel.call("_choose_data_folder")
|
settings_panel.call("_choose_data_folder")
|
||||||
var data_folder_dialog := (
|
var data_folder_dialog := (
|
||||||
|
|
@ -169,11 +181,14 @@ func _run() -> void:
|
||||||
var migrated_root: String = data_root.root_path.get_base_dir().path_join(
|
var migrated_root: String = data_root.root_path.get_base_dir().path_join(
|
||||||
"progression-migrated-data"
|
"progression-migrated-data"
|
||||||
)
|
)
|
||||||
var data_migration: Dictionary = PortableDataMigration.migrate_active_to(
|
settings_panel.call("_change_data_folder", migrated_root)
|
||||||
data_root, 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(
|
var migrated_save: String = migrated_root.path_join(
|
||||||
"player/player_save.nfsave"
|
"player/player_save.nfsave"
|
||||||
)
|
)
|
||||||
|
|
@ -193,6 +208,18 @@ func _run() -> void:
|
||||||
"ok", false
|
"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()
|
main.queue_free()
|
||||||
for _frame: int in 4:
|
for _frame: int in 4:
|
||||||
|
|
@ -202,6 +229,124 @@ func _run() -> void:
|
||||||
quit()
|
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:
|
func _assert_opaque(path: String) -> void:
|
||||||
var bytes: PackedByteArray = PortableFileGuard.read_bytes(path)
|
var bytes: PackedByteArray = PortableFileGuard.read_bytes(path)
|
||||||
assert(not bytes.is_empty())
|
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 virtual_pointer_mode_changed(is_active: bool)
|
||||||
signal social_prompt_accepted
|
signal social_prompt_accepted
|
||||||
signal social_prompt_declined
|
signal social_prompt_declined
|
||||||
|
signal data_root_changed
|
||||||
|
|
||||||
const VIRTUAL_MOUSE_INPUT_OWNER: StringName = &"controller_virtual_mouse"
|
const VIRTUAL_MOUSE_INPUT_OWNER: StringName = &"controller_virtual_mouse"
|
||||||
const EMOTE_RADIAL_CAMERA_OWNER: StringName = &"emote_radial_menu"
|
const EMOTE_RADIAL_CAMERA_OWNER: StringName = &"emote_radial_menu"
|
||||||
|
|
@ -271,6 +272,8 @@ func _ready() -> void:
|
||||||
_pause_settings_panel.crisp_reset_focus_requested.connect(
|
_pause_settings_panel.crisp_reset_focus_requested.connect(
|
||||||
crisp_reset_focus_requested.emit
|
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(
|
if not Input.joy_connection_changed.is_connected(
|
||||||
_on_controller_connection_changed
|
_on_controller_connection_changed
|
||||||
):
|
):
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ signal opened
|
||||||
signal crisp_reset_focus_requested
|
signal crisp_reset_focus_requested
|
||||||
signal panel_visibility_changed(is_visible: bool)
|
signal panel_visibility_changed(is_visible: bool)
|
||||||
signal navigation_transition_started
|
signal navigation_transition_started
|
||||||
|
signal data_root_changed
|
||||||
|
|
||||||
enum PresentationMode {
|
enum PresentationMode {
|
||||||
TITLE_EMBEDDED,
|
TITLE_EMBEDDED,
|
||||||
|
|
@ -612,6 +613,7 @@ func _refresh_data_page() -> void:
|
||||||
%DataStorageMode.text = "storage mode: " + _data_root.storage_mode_text()
|
%DataStorageMode.text = "storage mode: " + _data_root.storage_mode_text()
|
||||||
%ChangeDataFolder.disabled = (
|
%ChangeDataFolder.disabled = (
|
||||||
_data_root.override_active
|
_data_root.override_active
|
||||||
|
or _presentation_mode != PresentationMode.TITLE_EMBEDDED
|
||||||
or (_network_session != null and _network_session.is_session_active())
|
or (_network_session != null and _network_session.is_session_active())
|
||||||
)
|
)
|
||||||
var fingerprint: String = (
|
var fingerprint: String = (
|
||||||
|
|
@ -668,6 +670,9 @@ func _choose_data_folder() -> void:
|
||||||
if _data_root == null or _data_root.override_active:
|
if _data_root == null or _data_root.override_active:
|
||||||
_feedback.text = "the data folder is externally managed."
|
_feedback.text = "the data folder is externally managed."
|
||||||
return
|
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():
|
if _network_session != null and _network_session.is_session_active():
|
||||||
_feedback.text = "return to title to change the data folder."
|
_feedback.text = "return to title to change the data folder."
|
||||||
return
|
return
|
||||||
|
|
@ -684,6 +689,12 @@ func _choose_data_folder() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _change_data_folder(path: String) -> 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(
|
var result: Dictionary = PortableDataMigration.migrate_active_to(
|
||||||
_data_root, path
|
_data_root, path
|
||||||
)
|
)
|
||||||
|
|
@ -697,9 +708,7 @@ func _change_data_folder(path: String) -> void:
|
||||||
).to_lower()
|
).to_lower()
|
||||||
_refresh_data_page()
|
_refresh_data_page()
|
||||||
return
|
return
|
||||||
_feedback.text = "data folder changed. straywild will close safely."
|
_complete_data_root_change("data folder changed.")
|
||||||
_refresh_data_page()
|
|
||||||
get_tree().call_deferred("quit")
|
|
||||||
else:
|
else:
|
||||||
_feedback.text = str(
|
_feedback.text = str(
|
||||||
result.get("message", "could not change the data folder.")
|
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.add_button("replace selected data", false, "replace")
|
||||||
dialog.confirmed.connect(func() -> void:
|
dialog.confirmed.connect(func() -> void:
|
||||||
if _data_root.use_existing_root(path):
|
if _data_root.use_existing_root(path):
|
||||||
_feedback.text = "data folder changed. straywild will close safely."
|
_complete_data_root_change("data folder changed.")
|
||||||
get_tree().call_deferred("quit")
|
|
||||||
else:
|
else:
|
||||||
_feedback.text = _data_root.error_message
|
_feedback.text = _data_root.error_message
|
||||||
dialog.queue_free()
|
dialog.queue_free()
|
||||||
|
|
@ -733,7 +741,7 @@ func _show_existing_data_folder_choice(path: String) -> void:
|
||||||
result.get("message", "could not replace selected data.")
|
result.get("message", "could not replace selected data.")
|
||||||
)
|
)
|
||||||
if bool(result.get("ok", false)):
|
if bool(result.get("ok", false)):
|
||||||
get_tree().call_deferred("quit")
|
_complete_data_root_change(_feedback.text.to_lower())
|
||||||
dialog.queue_free()
|
dialog.queue_free()
|
||||||
)
|
)
|
||||||
_interface_fonts.apply_utility_theme(dialog)
|
_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:
|
func _choose_identity_export(identity_type: String) -> void:
|
||||||
if not _identity_operation_allowed():
|
if not _identity_operation_allowed():
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -243,6 +243,11 @@ func reopen_to_menu() -> void:
|
||||||
_set_title_bubbles_interactive(true)
|
_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:
|
func open_join_game_page(endpoint: String = "") -> void:
|
||||||
_cancel_title_entry_transition()
|
_cancel_title_entry_transition()
|
||||||
_awaiting_start_input = false
|
_awaiting_start_input = false
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue