Integrate per-save identity and interaction UI

This commit is contained in:
Alexander Sellite 2026-09-01 17:30:50 -04:00
parent ac82a28f3b
commit 4fda6e97f6
58 changed files with 3471 additions and 461 deletions

View file

@ -35,6 +35,13 @@ const WorldLayoutType = preload("res://world/world_layout.gd")
const PlayerJobServiceType = preload("res://jobs/player_job_service.gd")
const PlayerType = preload("res://player/player.gd")
const PlayerHomeStateType = preload("res://homes/player_home_state.gd")
const DecorCatalogType = preload("res://homes/decor_catalog.gd")
const NetworkProfilePreferencesType = preload(
"res://network/network_profile_preferences.gd"
)
const PlayerAppearanceStoreType = preload(
"res://progression/player_appearance_store.gd"
)
const SAVE_VERSION: int = 11
const LEGACY_SAVE_FILENAME := "player_save.json"
@ -78,6 +85,9 @@ class LoadSnapshot:
)
var jobs_data: Dictionary = PlayerJobServiceType.default_save_data()
var home_data: Dictionary = PlayerHomeStateType.default_save_data()
var has_character_data: bool = false
var character_profile_data: Dictionary = {}
var appearance_data: Dictionary = {}
@export_range(0.05, 5.0, 0.05) var autosave_delay: float = 0.5
@ -99,6 +109,9 @@ var _world_weather: WorldWeatherServiceType
var _jobs: PlayerJobServiceType
var _player: PlayerType
var _home_state: PlayerHomeStateType
var _network_profile: NetworkProfilePreferencesType
var _appearance_store: PlayerAppearanceStoreType
var _legacy_character_data: Dictionary = {}
var _autosave_timer: Timer
var _is_configured: bool = false
var _is_restoring: bool = false
@ -179,6 +192,8 @@ func setup(
jobs: PlayerJobServiceType,
player: PlayerType,
home_state: PlayerHomeStateType,
network_profile: NetworkProfilePreferencesType = null,
appearance_store: PlayerAppearanceStoreType = null,
) -> void:
_inventory = inventory
_collection_log = collection_log
@ -197,6 +212,9 @@ func setup(
_jobs = jobs
_player = player
_home_state = home_state
_network_profile = network_profile
_appearance_store = appearance_store
capture_legacy_character_fallback()
_is_configured = (
_inventory != null
and _collection_log != null
@ -263,6 +281,132 @@ func setup(
_player.active_lure_changed.connect(_on_active_tackle_changed)
if not _home_state.changed.is_connected(_mark_dirty):
_home_state.changed.connect(_mark_dirty)
if (
_network_profile != null
and not _network_profile.profile_changed.is_connected(_mark_dirty)
):
_network_profile.profile_changed.connect(_mark_dirty)
if (
_appearance_store != null
and not _appearance_store.appearance_changed.is_connected(_mark_dirty)
):
_appearance_store.appearance_changed.connect(_mark_dirty)
func capture_legacy_character_fallback() -> void:
_legacy_character_data = _current_character_data()
func load_character_profile() -> bool:
if _network_profile == null or _appearance_store == null:
return true
_recover_interrupted_write()
var read_path: String = _read_path()
var previous_restoring: bool = _is_restoring
_is_restoring = true
if read_path.is_empty():
var reset_ok: bool = _reset_character_profile()
_is_restoring = previous_restoring
return reset_ok
var decoded: Dictionary = ProgressionSaveCodec.read_local_save(
read_path,
read_path == _legacy_save_path(),
)
if not bool(decoded.get("ok", false)):
_is_restoring = previous_restoring
return false
var save_data: Dictionary = decoded["data"]
var version: int = _read_integer(save_data.get("save_version"), -1)
if version < 1 or version > SAVE_VERSION:
_is_restoring = previous_restoring
return false
if version != SAVE_VERSION:
save_data = _migrate_save(save_data, version)
if save_data.is_empty():
_is_restoring = previous_restoring
return false
var character: Dictionary = _character_data_from_save(save_data)
var restored: bool = (
not character.is_empty()
and _restore_character_data(character)
)
_is_restoring = previous_restoring
return restored
func _current_character_data() -> Dictionary:
if _network_profile == null or _appearance_store == null:
return {}
var profile: Dictionary = _network_profile.to_save_data()
var appearance: Dictionary = _appearance_store.get_snapshot()
if (
not NetworkProfilePreferencesType.validate_save_data(profile)
or not CharacterCustomizationCatalog.validate_snapshot(appearance)
):
return {}
return {
"profile": profile,
"appearance": appearance,
}
func _character_data_from_save(save_data: Dictionary) -> Dictionary:
if not save_data.has("character"):
return _legacy_character_data.duplicate(true)
if typeof(save_data.get("character")) != TYPE_DICTIONARY:
return {}
var character: Dictionary = save_data["character"]
if (
not NetworkProfilePreferencesType.validate_save_data(
character.get("profile")
)
or typeof(character.get("appearance")) != TYPE_DICTIONARY
or not CharacterCustomizationCatalog.validate_snapshot(
character.get("appearance")
)
):
return {}
return {
"profile": (character["profile"] as Dictionary).duplicate(true),
"appearance": CharacterCustomizationCatalog.sanitized_snapshot(
character["appearance"]
),
}
func _restore_character_data(character: Dictionary) -> bool:
if _network_profile == null or _appearance_store == null:
return true
# The selected progression save is authoritative here. Applying it must not
# rewrite the compatibility profile mirrors: their guarded writes may be
# stale after a Syncthing edit, and that must not make a valid slot unloadable.
var previous_profile: Dictionary = _network_profile.to_save_data()
var previous_appearance: Dictionary = _appearance_store.get_snapshot()
if not _network_profile.restore_from_save_data(
character.get("profile"), false
):
return false
if _appearance_store.restore_from_save_data(
character.get("appearance"), false
):
return true
_network_profile.restore_from_save_data(previous_profile, false)
_appearance_store.restore_from_save_data(previous_appearance, false)
return false
func _reset_character_profile() -> bool:
if _network_profile == null or _appearance_store == null:
return true
var previous_profile: Dictionary = _network_profile.to_save_data()
var previous_appearance: Dictionary = _appearance_store.get_snapshot()
if not _network_profile.reset_to_defaults(false):
return false
if _appearance_store.reset_to_defaults(false):
return true
_network_profile.restore_from_save_data(previous_profile, false)
_appearance_store.restore_from_save_data(previous_appearance, false)
return false
func load_player_data() -> bool:
@ -315,6 +459,12 @@ func load_player_data() -> bool:
return false
_is_restoring = true
var character_restored: bool = true
if snapshot.has_character_data:
character_restored = _restore_character_data({
"profile": snapshot.character_profile_data,
"appearance": snapshot.appearance_data,
})
var inventory_restored: bool = _inventory.replace_all_catches(
snapshot.catches,
snapshot.next_catch_sequence
@ -390,13 +540,14 @@ func load_player_data() -> bool:
or not world_weather_restored
or not jobs_restored
or not home_restored
or not character_restored
):
push_error(
(
"Validated player save could not be restored: "
+ "inventory=%s collection=%s wallet=%s bag=%s tackle=%s hotbar=%s "
+ "upgrades=%s cooler=%s layout=%s art=%s experience=%s "
+ "time=%s weather=%s jobs=%s home=%s"
+ "time=%s weather=%s jobs=%s home=%s character=%s"
)
% [
inventory_restored,
@ -414,6 +565,7 @@ func load_player_data() -> bool:
world_weather_restored,
jobs_restored,
home_restored,
character_restored,
]
)
return false
@ -669,6 +821,7 @@ func copy_progression_to_path(
func initialize_new_game(
world_seed: int = DEFAULT_WORLD_SEED,
world_layout: StringName = WorldLayoutType.GENERATED,
reset_character_profile: bool = true,
) -> bool:
if not _is_configured:
return false
@ -679,6 +832,12 @@ func initialize_new_game(
):
return false
_automatic_saving_blocked = false
if reset_character_profile:
_is_restoring = true
var character_reset: bool = _reset_character_profile()
_is_restoring = false
if not character_reset:
return false
_restore_defaults()
_world_layout = world_layout
_world_seed = world_seed
@ -872,7 +1031,7 @@ func _build_save_dictionary() -> Dictionary:
):
return {}
return {
var save_data: Dictionary = {
"save_version": SAVE_VERSION,
"wallet": {
"balance": _wallet.get_balance(),
@ -916,6 +1075,12 @@ func _build_save_dictionary() -> Dictionary:
"jobs": _jobs.to_save_data(),
"home": _home_state.to_save_data(),
}
var character: Dictionary = _current_character_data()
if _network_profile != null and _appearance_store != null:
if character.is_empty():
return {}
save_data["character"] = character
return save_data
func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
@ -998,6 +1163,17 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
return null
var snapshot := LoadSnapshot.new()
var character: Dictionary = _character_data_from_save(save_data)
if save_data.has("character") and character.is_empty():
return null
if not character.is_empty():
snapshot.has_character_data = true
snapshot.character_profile_data = (
character["profile"] as Dictionary
).duplicate(true)
snapshot.appearance_data = (
character["appearance"] as Dictionary
).duplicate(true)
if world_data.has("layout"):
if not WorldLayoutType.is_valid(world_data["layout"]):
return null
@ -1208,7 +1384,12 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
)
continue
var maximum: int = (
item_data.max_stack if item_data.stackable else 1
mini(
item_data.max_stack,
DecorCatalogType.MAX_OWNED_PER_PRODUCT,
)
if DecorCatalogType.has_product(item_id)
else item_data.max_stack if item_data.stackable else 1
)
if quantity > maximum:
push_warning(