class_name SaveGame extends Resource var _is_initialised: bool = false var current_room_path: String: get(): return Main.room_paths[current_room] @export var filepath: String: set(value): filepath = value if _is_initialised: changed.emit() @export var unique_save_name: String = "frame_of_mind_%s_%s" % [Time.get_date_string_from_system(), Time.get_time_string_from_system().replace(":", "-")] + "-" + str(randi()): set(value): unique_save_name = value if _is_initialised: changed.emit() @export var current_room: State.rooms = State.rooms.NULL: set(value): current_room = value if _is_initialised: changed.emit() @export_flags("Intro", "Childhood", "Voice Training", "Jui Jutsu") var mementos_complete: int = 0: set(value): mementos_complete = value if _is_initialised: changed.emit() @export_flags_2d_physics var sequences_enabled: int = 63: set(value): sequences_enabled = value if _is_initialised: changed.emit() @export var board_state: Dictionary = {"cards": {}, "stickies": {}, "randoms": []}: set(value): board_state = value if _is_initialised: changed.emit() @export var childhood_mementos: Dictionary = {"cards": {}, "stickies": {}, "randoms": []}: set(value): 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 if _is_initialised: changed.emit() @export var last_saved: int = int(Time.get_unix_time_from_system()): set(value): last_saved = value if _is_initialised: changed.emit() @export var is_valid: bool = false @export var is_demo: bool = OS.has_feature("Demo") @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) func _validate_property(property: Dictionary): if property.name == filepath: property.usage |= PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED if property.name == "thumbnail": property.usage |= PROPERTY_USAGE_READ_ONLY if property.name == "is_valid": property.usage |= PROPERTY_USAGE_READ_ONLY if property.name == "is_empty": property.usage |= PROPERTY_USAGE_READ_ONLY ## 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.") else: filepath = filepath_or_debug unique_save_name = filepath_or_debug.get_file().get_basename() # 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) if filepath == "DEBUG": push_warning("Savegame: Saving DEBUG save skipped. This is intentional.") return if current_room == State.rooms.NULL: 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() thumbnail_image.resize(384, 216, Image.INTERPOLATE_LANCZOS) # nonexistent call in ViewportTexture thumbnail_image.crop(384, 216) var save_dir := DirAccess.open(filepath.get_base_dir()) if not save_dir.dir_exists("thumbnails"): save_dir.make_dir("thumbnails") var thumbnail_path: String = "%s/thumbnails/%s.png" % [filepath.get_base_dir(), unique_save_name] thumbnail_image.save_png(thumbnail_path) # 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: var i: int = mementos_complete - ((mementos_complete >> 1) & 0x55555555); i = (i & 0x33333333) + ((i >> 2) & 0x33333333); i = (i + (i >> 4)) & 0x0F0F0F0F; i *= 0x01010101; return i >> 24; func calculate_total_connections() -> int: var connections:= 0 for sticky_position in board_state.stickies.values(): connections += int(sticky_position is String) return connections func validate_board_state() -> bool: if board_state.keys().has("cards") and board_state.keys().has("stickies"): for card in board_state.cards.values(): if not card is Vector2: push_error("Save %s could not be parsed: Corrupted Cards." % unique_save_name) return false for sticky in board_state.stickies.values(): if not (sticky is int or sticky is Vector2 or sticky is float or board_state.cards.keys().has(sticky)): push_error("Save %s could not be parsed: Corrupted Sticky Notes.") return false return true return false