frame-of-mind/src/dev-util/savegame.gd

224 lines
7.7 KiB
GDScript3
Raw Normal View History

2024-10-01 23:26:54 +00:00
class_name SaveGame extends Resource
2025-03-25 21:34:13 +00:00
var _is_initialised: bool = false
var current_room_path: String:
get():
return Main.room_paths[current_room]
2025-03-25 21:34:13 +00:00
@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):
2025-03-25 21:34:13 +00:00
unique_save_name = value
if _is_initialised: changed.emit()
2025-03-25 21:34:13 +00:00
@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()
2025-10-29 21:49:29 +00:00
@export_flags_2d_physics var sequences_enabled: int = 63:
set(value):
sequences_enabled = value
if _is_initialised: changed.emit()
2025-06-03 21:18:34 +00:00
@export var board_state: Dictionary = {"cards": {}, "stickies": {}, "randoms": []}:
2025-03-25 21:34:13 +00:00
set(value):
board_state = value
if _is_initialised: changed.emit()
2025-10-29 21:49:29 +00:00
@export var childhood_mementos: Dictionary = {"cards": {}, "stickies": {}, "randoms": []}:
set(value):
childhood_mementos = value
if _is_initialised: changed.emit()
2025-06-03 21:18:34 +00:00
@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()
2025-03-25 21:34:13 +00:00
@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()
2025-03-25 21:34:13 +00:00
@export var is_valid: bool = false
@export var is_demo: bool = OS.has_feature("Demo")
2025-03-25 21:34:13 +00:00
@export var is_empty: bool = true:
get():
return not FileAccess.file_exists(filepath) or (current_room == State.rooms.NULL)
2025-03-25 21:34:13 +00:00
@export var save_manually: bool = false:
set(val):
if val: save_to_file(thumbnail)
func _validate_property(property: Dictionary):
2025-03-25 21:34:13 +00:00
if property.name == filepath:
property.usage |= PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED
if property.name == "thumbnail":
2025-03-25 21:34:13 +00:00
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.")
2025-03-25 21:34:13 +00:00
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
2025-10-29 21:49:29 +00:00
if current_room == State.rooms.NULL:
push_warning("Savegame: Not saving empty savegame.")
2025-10-29 21:49:29 +00:00
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())
2025-03-25 21:34:13 +00:00
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)
2025-03-25 21:34:13 +00:00
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
2025-03-25 21:34:13 +00:00
for sticky_position in board_state.stickies.values():
connections += int(sticky_position is String)
2025-03-25 21:34:13 +00:00
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)):
2025-03-25 21:34:13 +00:00
push_error("Save %s could not be parsed: Corrupted Sticky Notes.")
return false
return true
return false