From eefc38fb42427fad5b585a2e7922bc0d7f49ae2c Mon Sep 17 00:00:00 2001 From: Tiger Jove Date: Fri, 16 Jan 2026 13:03:39 +0100 Subject: [PATCH] feat: savegames save as .tres feat: savegames restore player position fix: savegames now more robust --- .../transition/subway_sequence.gd | 3 + .../volunteer_room/shared_flat.gd | 14 +- .../youth_room/youth_room.gd | 11 +- .../youth_room/youth_room.tscn | 4 +- .../youth_room/youth_room_scene_player.gd | 6 +- src/dev-util/room_template.gd | 18 +- src/dev-util/savegame.gd | 197 +++++++++--------- src/logic-scenes/interactable/interactable.gd | 2 +- .../player_controller/player_controller.gd | 10 + src/ui/menu_main/save_game_list.gd | 8 +- 10 files changed, 156 insertions(+), 117 deletions(-) diff --git a/src/base-environments/transition/subway_sequence.gd b/src/base-environments/transition/subway_sequence.gd index f34c743..d16fccb 100644 --- a/src/base-environments/transition/subway_sequence.gd +++ b/src/base-environments/transition/subway_sequence.gd @@ -48,6 +48,9 @@ func pull_save_state(save: SaveGame) -> void: save.sequences_enabled = Scenes.enabled_sequences save.current_room = State.rooms.ADULTHOOD save_game = save + + # Call parent to restore player position + super.pull_save_state(save) func prepare_transition(): for child in %Stations.get_children(): diff --git a/src/base-environments/volunteer_room/shared_flat.gd b/src/base-environments/volunteer_room/shared_flat.gd index d6905dd..ad1c12f 100644 --- a/src/base-environments/volunteer_room/shared_flat.gd +++ b/src/base-environments/volunteer_room/shared_flat.gd @@ -29,14 +29,24 @@ func _ready(): card_picker.cards_picked.connect(card_board.populate_board) func pull_save_state(save: SaveGame) -> void: - save.board_state = card_board.get_save_dict() save.current_room = State.rooms.ADULTHOOD - save.mementos_complete = Scenes.completed_sequences + #FIXME: fix the bloddy card board loading algorythm + #card_board.initialise_from_save(save) + + # Call parent to restore player position + super.pull_save_state(save) func _on_scene_finished(_id: int, _repeat:bool): await get_tree().create_timer(3).timeout save_room() +func save_room(): + # Update board state before saving + save_game.board_state = card_board.get_save_dict() + save_game.mementos_complete = Scenes.completed_sequences + save_game.sequences_enabled = Scenes.enabled_sequences + super.save_room() + func prepare_transition(): pass diff --git a/src/base-environments/youth_room/youth_room.gd b/src/base-environments/youth_room/youth_room.gd index 2c8a395..23bd1eb 100644 --- a/src/base-environments/youth_room/youth_room.gd +++ b/src/base-environments/youth_room/youth_room.gd @@ -62,17 +62,26 @@ func _ready(): func pull_save_state(save: SaveGame) -> void: save_game = save save_game.current_room = id - save_game.board_state = card_board.get_save_dict() + + # Load board state from save first, before overwriting it card_board.initialise_from_save(save_game) Scenes.started_sequences = save_game.mementos_complete Scenes.completed_sequences = save_game.mementos_complete + + # Call parent to restore player position + super.pull_save_state(save) func _on_scene_finished(_id: int, _repeat:bool): await get_tree().create_timer(3).timeout save_room() +func save_room(): + # Update board state before saving + save_game.board_state = card_board.get_save_dict() + save_game.mementos_complete = Scenes.completed_sequences + super.save_room() func prepare_transition(): save_room() diff --git a/src/base-environments/youth_room/youth_room.tscn b/src/base-environments/youth_room/youth_room.tscn index e8a5dfa..5c920e1 100644 --- a/src/base-environments/youth_room/youth_room.tscn +++ b/src/base-environments/youth_room/youth_room.tscn @@ -1871,7 +1871,7 @@ light_size = 20.0 omni_range = 16.8518 [node name="MaskInteractable" parent="logic" instance=ExtResource("22_ks23q")] -transform = Transform3D(-0.8827416, 0, 0.4698562, 0, 1, 0, -0.4698562, 0, -0.8827416, -0.025371574, 0.55708295, 2.5263817) +transform = Transform3D(-0.8827416, 0, 0.4698562, 0, 1, 0, -0.4698562, 0, -0.8827416, 0.028929986, 0.58693635, 2.552513) interaction = ExtResource("12_viwxf") [node name="MindBoardInteractable" parent="logic" instance=ExtResource("22_ks23q")] @@ -1890,7 +1890,7 @@ interaction = ExtResource("13_v3447") transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.36574292, 0.099999994, 0.032779038) [node name="ClothesInteractable" parent="logic" instance=ExtResource("22_ks23q")] -transform = Transform3D(0.7935111, 0, -0.60855323, 0, 1, 0, 0.60855323, 0, 0.7935111, 1.6713148, 1.089737, -0.92289597) +transform = Transform3D(0.7935111, 0, -0.60855323, 0, 1, 0, 0.60855323, 0, 0.7935111, 1.6334484, 1.1331886, -0.8914416) interaction = ExtResource("12_x3dlb") [node name="Sprite3D" parent="logic/ClothesInteractable/View" index="0"] diff --git a/src/base-environments/youth_room/youth_room_scene_player.gd b/src/base-environments/youth_room/youth_room_scene_player.gd index b77c688..cdf4c3b 100644 --- a/src/base-environments/youth_room/youth_room_scene_player.gd +++ b/src/base-environments/youth_room/youth_room_scene_player.gd @@ -13,7 +13,7 @@ func start_soundtrack(): $VoiceTraining.play(70) func _on_scene_starting(scene_id: Scenes.id, _repeat: bool) -> void: - + # Handle scene-specific setup (music, chest animation, etc.) match scene_id: Scenes.id.YOUTH_CHILDHOOD: @@ -28,13 +28,13 @@ func _on_scene_starting(scene_id: Scenes.id, _repeat: bool) -> void: func _on_scene_finished(scene_id: Scenes.id, _repeat: bool) -> void: print_debug("YouthRoomScenePlayer._on_scene_finished(%s)" % Scenes.id.keys()[scene_id]) - + match scene_id: Scenes.id.YOUTH_CHILDHOOD: play_backwards("childhood_music") Scenes.id.YOUTH_VOICE_TRAINING: play_backwards("voice_music") - + queue("RESET") func _play_chest_animation() -> void: diff --git a/src/dev-util/room_template.gd b/src/dev-util/room_template.gd index d631ec2..2e1882b 100644 --- a/src/dev-util/room_template.gd +++ b/src/dev-util/room_template.gd @@ -41,7 +41,23 @@ func start_room(): pass func pull_save_state(_save: SaveGame) -> void: - pass + # Try to restore player position from save + restore_player_from_save(_save) + +## Attempts to find player controller and restore position/rotation from save +func restore_player_from_save(save: SaveGame) -> void: + var player: PlayerController = null + + # Try to find player controller in common locations + if has_node("%PlayerController"): + player = get_node("%PlayerController") + elif has_node("logic/PlayerController"): + player = get_node("logic/PlayerController") + + if player and player is PlayerController: + player.restore_from_save(save) + else: + print_debug("RoomTemplate: Could not find PlayerController to restore position") func save_room(): save_game.save_to_file(get_tree().root.get_texture()) diff --git a/src/dev-util/savegame.gd b/src/dev-util/savegame.gd index 922b12d..97e63a5 100644 --- a/src/dev-util/savegame.gd +++ b/src/dev-util/savegame.gd @@ -1,5 +1,6 @@ class_name SaveGame extends Resource + var _is_initialised: bool = false var current_room_path: String: @@ -38,6 +39,18 @@ var current_room_path: String: childhood_mementos = value if _is_initialised: changed.emit() @export var is_childhood_board_complete: bool = false +@export var player_position: Vector3 = Vector3.ZERO: + set(value): + player_position = value + if _is_initialised: changed.emit() +@export var player_yaw: float = 0.0: + set(value): + player_yaw = value + if _is_initialised: changed.emit() +@export var player_pitch: float = 0.0: + set(value): + player_pitch = value + if _is_initialised: changed.emit() @export var thumbnail: Texture = preload("res://import/interface-elements/empty_save_slot.png"): set(value): thumbnail = value @@ -52,7 +65,7 @@ var current_room_path: String: @export var is_empty: bool = true: get(): return not FileAccess.file_exists(filepath) or (current_room == State.rooms.NULL) - + @export var save_manually: bool = false: set(val): if val: save_to_file(thumbnail) @@ -67,107 +80,81 @@ func _validate_property(property: Dictionary): if property.name == "is_empty": property.usage |= PROPERTY_USAGE_READ_ONLY -func _init(initial_filepath = "") -> void: - if initial_filepath == "": - filepath = "%s/%s.json" % [State.user_saves_path, unique_save_name] - elif initial_filepath == "DEBUG": - filepath = initial_filepath - else: - filepath = initial_filepath - unique_save_name = initial_filepath.get_file() - - read_save_file() - _is_initialised = true - -func read_save_file() -> void: - if not DirAccess.dir_exists_absolute(filepath.get_base_dir()): - DirAccess.make_dir_absolute(filepath.get_base_dir()) - - if filepath == "DEBUG": +## Creates a NEW save game (for starting a new game) +func _init(filepath_or_debug: String = "") -> void: + if filepath_or_debug == "": + filepath = "%s/%s.tres" % [State.user_saves_path, unique_save_name] + elif filepath_or_debug == "DEBUG": + filepath = "DEBUG" if OS.has_feature("debug") or OS.has_feature("demo"): push_warning("Created DEBUG savegame. Progress will not be stored!") else: print_debug(get_stack()) push_error("Created DEBUG savegame outside of demo or debug environment. This is unintentional and will lead to data loss. Please contact support and attatch the stack above.") - #TODO maybe cause a crash here? - return - - if FileAccess.file_exists(filepath): - print_debug("Savegame: Reading from: %s" % filepath) - var file := FileAccess.open(filepath, FileAccess.READ) - var raw_json := FileAccess.get_file_as_string(filepath) - file.close() - var parsed: Dictionary = JSON.parse_string(raw_json) - - var tmp_img: Image - - if FileAccess.file_exists("%s/thumbnails/%s.png" % [filepath.get_base_dir(), unique_save_name]): - tmp_img = Image.load_from_file("%s/thumbnails/%s.png" % [filepath.get_base_dir(), unique_save_name]) - - var are_types_valid := ( - parsed["unique_save_name"] is String and - parsed["current_room"] is float and - parsed["mementos_complete"] is float and - parsed["board_state"] is Dictionary and - parsed["is_childhood_board_complete"] is bool and - parsed["last_saved"] is float# and FIXME - #parsed["demo"] is bool and last_saved != 0 - ) - - if are_types_valid: - for key in parsed.keys(): - set(key, parsed[key]) - - for dict:Dictionary in [board_state["cards"], board_state["stickies"]]: - for key in dict.keys(): - if dict[key] is String: - if dict[key].begins_with("("): - dict[key] = parse_vec_from_string(dict[key]) - - var cards: Dictionary[StringName, Variant] - var stickies: Dictionary[StringName, Variant] - var randoms: Array[StringName] - - for cardname:String in board_state["cards"]: - cards[StringName(cardname)] = board_state["cards"][cardname] - for sticky_name:String in board_state["stickies"]: - stickies[StringName(sticky_name)] = board_state["stickies"][sticky_name] - for random_name:StringName in board_state["randoms"]: - randoms.append( random_name ) - - board_state = { - "cards": cards, - "stickies": stickies, - "randoms": randoms - } - - is_valid = are_types_valid \ - and current_room >= 0 \ - and current_room < State.rooms.keys().size() \ - and validate_board_state() - - if not is_valid: - push_error("Parsing of Save failed.") - - if tmp_img != null: - thumbnail = ImageTexture.create_from_image(tmp_img) - is_empty = false else: - print_debug("Savegame: Creating (in memory) for path: %s" % filepath) - is_valid = true + filepath = filepath_or_debug + unique_save_name = filepath_or_debug.get_file().get_basename() -func _get_save_dict() -> Dictionary: - return { - "unique_save_name": unique_save_name, - "current_room": current_room, - "mementos_complete": mementos_complete, - "sequences_enabled": sequences_enabled, - "childhood_mementos": childhood_mementos, - "board_state": board_state, - "is_childhood_board_complete": is_childhood_board_complete, - "last_saved": last_saved, - "is_demo": is_demo - } + # Ensure save directory exists + if filepath != "DEBUG" and not DirAccess.dir_exists_absolute(filepath.get_base_dir()): + DirAccess.make_dir_absolute(filepath.get_base_dir()) + + print_debug("Savegame: Creating new save for path: %s" % filepath) + is_valid = true + _is_initialised = true + +## Static factory method to load an EXISTING save from disk +static func load_from_file(save_filepath: String) -> SaveGame: + if not FileAccess.file_exists(save_filepath): + push_error("SaveGame: File does not exist: %s" % save_filepath) + return null + + print_debug("Savegame: Loading from: %s" % save_filepath) + + var loaded: SaveGame = ResourceLoader.load(save_filepath, "", ResourceLoader.CACHE_MODE_IGNORE) + + if not loaded: + push_error("Failed to load SaveGame resource from: %s" % save_filepath) + return null + + # Update filepath to actual location (in case file was moved) + loaded.filepath = save_filepath + loaded.unique_save_name = save_filepath.get_file().get_basename() + + # Ensure randoms array exists (backwards compatibility) + if "randoms" not in loaded.board_state: + loaded.board_state["randoms"] = [] + + # Load thumbnail separately (not stored in .tres) + var thumbnail_path := "%s/thumbnails/%s.png" % [save_filepath.get_base_dir(), loaded.unique_save_name] + if FileAccess.file_exists(thumbnail_path): + var tmp_img: Image = Image.load_from_file(thumbnail_path) + if tmp_img != null: + loaded.thumbnail = ImageTexture.create_from_image(tmp_img) + + # Validate the loaded data + loaded.is_valid = loaded.current_room >= 0 \ + and loaded.current_room < State.rooms.keys().size() \ + and loaded.validate_board_state() + + if not loaded.is_valid: + push_error("Validation of loaded save failed: %s" % save_filepath) + return null + + loaded._is_initialised = true + + return loaded + +## Captures current player position and camera rotation +func capture_player_state() -> void: + if State.player: + player_position = State.player.global_position + # Access yaw and pitch nodes + var yaw: Node3D = State.player.get_node("Yaw") + var pitch: Node3D = yaw.get_node("Pitch") + player_yaw = yaw.rotation.y + player_pitch = pitch.rotation.x + print_debug("SaveGame: Captured player state - pos: %s, yaw: %.2f, pitch: %.2f" % [player_position, player_yaw, player_pitch]) func save_to_file(screen_shot: Texture) -> void: print_debug("Savegame: Saving to file: %s" % filepath) @@ -180,7 +167,12 @@ func save_to_file(screen_shot: Texture) -> void: push_warning("Savegame: Not saving empty savegame.") return + # Capture player state before saving + capture_player_state() + last_saved = int(Time.get_unix_time_from_system()) + + # Save thumbnail separately as PNG var thumbnail_image: Image = screen_shot.get_image() thumbnail_image.convert(Image.Format.FORMAT_RGB8) thumbnail_image.linear_to_srgb() @@ -193,10 +185,13 @@ func save_to_file(screen_shot: Texture) -> void: var thumbnail_path: String = "%s/thumbnails/%s.png" % [filepath.get_base_dir(), unique_save_name] thumbnail_image.save_png(thumbnail_path) - print_debug(filepath.get_base_dir()) - var file := FileAccess.open(filepath, FileAccess.WRITE) - file.store_string(JSON.stringify(_get_save_dict())) - file.close() + + # Save the resource using Godot's native serialization + var save_result := ResourceSaver.save(self, filepath) + if save_result != OK: + push_error("Failed to save resource to: %s (Error code: %d)" % [filepath, save_result]) + else: + print_debug("Successfully saved to: %s" % filepath) func calculate_completed_sequences() -> int: @@ -226,7 +221,3 @@ func validate_board_state() -> bool: return false return true return false - -func parse_vec_from_string(string: String) -> Vector2: - var string_array := string.replace("(", "").replace(")", "").split(", ") - return Vector2(float(string_array[0]), float(string_array[1])) diff --git a/src/logic-scenes/interactable/interactable.gd b/src/logic-scenes/interactable/interactable.gd index ad80cc2..46be23c 100644 --- a/src/logic-scenes/interactable/interactable.gd +++ b/src/logic-scenes/interactable/interactable.gd @@ -47,6 +47,7 @@ func _player_active(value: bool) -> void: func expand() -> void: shown = true + _process_billboard() if tween and tween.is_valid(): tween.kill() @@ -72,7 +73,6 @@ func collapse() -> void: tween.parallel().tween_property(frame, "scale", Vector3.ONE * 2.0, 1.0).set_trans(Tween.TRANS_QUAD) func _process(_delta: float) -> void: - _process_billboard() _process_hover() func _process_billboard() -> void: diff --git a/src/logic-scenes/player_controller/player_controller.gd b/src/logic-scenes/player_controller/player_controller.gd index 2df6061..58314cf 100644 --- a/src/logic-scenes/player_controller/player_controller.gd +++ b/src/logic-scenes/player_controller/player_controller.gd @@ -128,6 +128,16 @@ func _ready(): func _on_player_enable(enable: bool) -> void: enabled = enable +## Restores player position and camera rotation from save game +func restore_from_save(save: SaveGame) -> void: + if save.player_position != Vector3.ZERO: + global_position = save.player_position + yaw.rotation.y = save.player_yaw + pitch.rotation.x = save.player_pitch + print_debug("PlayerController: Restored position %s, yaw %.2f, pitch %.2f" % [save.player_position, save.player_yaw, save.player_pitch]) + else: + print_debug("PlayerController: No saved position data, using default spawn") + func _process(_delta) -> void: if not enabled: return diff --git a/src/ui/menu_main/save_game_list.gd b/src/ui/menu_main/save_game_list.gd index e2888a2..cd9e1a8 100644 --- a/src/ui/menu_main/save_game_list.gd +++ b/src/ui/menu_main/save_game_list.gd @@ -43,10 +43,10 @@ func _load_games(): var filepaths: PackedStringArray = save_game_dir.get_files() for path in filepaths: - if path is String and path.ends_with(".json"): - var save := SaveGame.new("%s/%s" % [State.user_saves_path, path.get_basename()]) - # HACK: Skip empty saves (we decide later what to do with them) - if not save.is_empty: + if path is String and path.ends_with(".tres"): + var save := SaveGame.load_from_file("%s/%s" % [State.user_saves_path, path]) + # Skip invalid/empty saves + if save != null and not save.is_empty: saves.append(save) _sort_saves()