re-implement dynamic loading during menu

This commit is contained in:
betalars 2026-06-03 14:40:20 +02:00
parent b2fd6af9f4
commit 8244f85790
8 changed files with 1181 additions and 153 deletions

View File

@ -6,7 +6,8 @@ class_name Room
@onready var ui: Control = %UI
## Tells the main loop to proceed to the next scene
signal proceed(next_scene_path: String)
signal prepare_next(room_id: Room.ids)
signal proceed_to(room_id: Room.ids)
enum ids {
NULL,
@ -49,19 +50,18 @@ func get_ready_async():
func start_room_async():
prints("----------", "START_ROOM", self.name, "--------------")
await Main.curtain.open()
get_tree().paused = false
await get_tree().process_frame # so this registers as a coroutine in IDE
func play() -> String:
for i in range(20): await get_tree().process_frame #HACK - can probably be removed
func play() -> void:
await get_ready_async()
await start_room_async()
var next_room : StringName = await proceed
prints("----------", "PROCEEDING", next_room, "--------------")
return next_room
func prepare_for_unload(next_room: Room.ids):
prints("----------", "PREPARE_UNLOAD", Room.ids.keys()[id], "PREPARE_LOAD", Room.ids.keys()[next_room], "--------------")
prepare_next.emit(next_room)
func pull_save_state(save: SaveGame) -> void:
# Override this function to load the state of the chapter from State.save_game

View File

@ -8,8 +8,8 @@ class_name SaveGame extends Resource
@export var unique_save_name: String = ""
@export var current_room: Room.ids = Room.ids.NULL
@export_flags("Intro", "Childhood", "Voice Training", "Jui Jutsu") var mementos_complete: int = 0
@export_flags_2d_physics var sequences_enabled: int = 63
@export_flags_2d_physics var mementos_complete: int = 0
@export_flags_2d_physics var sequences_enabled: int = 0
# Board state - properly typed fields
@export var board_positions: Dictionary[StringName, Vector2] = {} # Position of all cards and stickies
@ -19,7 +19,7 @@ class_name SaveGame extends Resource
@export var seen : Array[StringName] = []
@export var childhood_board_complete: bool = false
@export var subway_burnout : bool = false
@export var subway_uni : bool = false
@export var player_position : Vector3 = Vector3.ZERO
@export var player_yaw : float = 0.0
@ -35,7 +35,7 @@ var current_room_path: String:
get: return Main.room_paths[current_room] if Main else ""
var is_empty: bool:
get: return not FileAccess.file_exists(file_name) or (current_room == State.rooms.NULL)
get: return not FileAccess.file_exists(file_name) or (current_room == Room.ids.NULL)
var completed_sequences: int:
get:
@ -53,7 +53,8 @@ var total_connections: int:
# === State Variables / External Data ===
## Where to save the savegame to / where it was loaded from
var file_name: String = ""
## This ensures compatibility with Steam, because it will give absolute paths only.
var absolute_path: String = ""
## Screenshot or placeholder image
var thumbnail: Texture = preload("res://import/interface-elements/empty_save_slot.png")
@ -103,6 +104,15 @@ static func load_from_file(save_filepath: String) -> SaveGame:
push_error("SaveGame: Validation failed: %s" % save_filepath)
return null
if not save_filepath.is_absolute_path():
var helper:= FileAccess.open(save_filepath, FileAccess.READ)
loaded.absolute_path = helper.get_path_absolute()
helper.close()
else:
loaded.absolute_path = save_filepath
Steamworks.save_list[loaded.absolute_path] = loaded
return loaded
## Helper to load thumbnail from separate PNG file
@ -148,6 +158,9 @@ func save_to_file(screen_shot: Texture2D) -> void:
capture_player_state()
last_saved = int(Time.get_unix_time_from_system())
if Steamworks.steam_cloud_on:
Steam.beginFileWriteBatch()
# Save thumbnail
_save_thumbnail(screen_shot)
@ -158,6 +171,16 @@ func save_to_file(screen_shot: Texture2D) -> void:
else:
print("Successfully saved to: %s" % file_name)
if not file_name.is_absolute_path():
var helper:= FileAccess.open(file_name, FileAccess.READ)
absolute_path = helper.get_path_absolute()
helper.close()
else:
absolute_path = file_name
if Steamworks.steam_cloud_on:
Steam.endFileWriteBatch()
## Processes and saves thumbnail as PNG
func _save_thumbnail(screen_shot: Texture2D) -> void:
var img: Image = screen_shot.get_image()

View File

@ -24,8 +24,8 @@ func _ready():
func vanish():
super.vanish()
await Main.curtain.black() # Go straight to loading screen
State.room.proceed.emit(Main.transition_room_path)
await Main.curtain.blackout() # Go straight to loading screen
State.active_room.proceed_to.emit(Room.ids.TRANSITION)
## Main play coroutine - simple linear flow for burning a card
@ -43,6 +43,7 @@ func play() -> void:
await $AnimationPlayer.animation_finished
print("CardBurner: Sequence complete")
vanish()
func _populate() -> void:

View File

@ -229,7 +229,7 @@ func pick_cards(id: Scenes.id):
if id == Scenes.id.YOUTH_DRAVEN:
$Meaning.play()
State.room.scene_player.play("intro")
State.active_room.scene_player.play("intro")
await cards_picked
hide()

View File

@ -12,8 +12,9 @@ var all_ready: bool:
return _settings_initialized and _savegame_initialized
func saves_loaded(save: SaveGame):
save_game = save
_savegame_initialized = true
if not _savegame_initialized:
save_game = save
_savegame_initialized = true
var active_room: Room

View File

@ -1,14 +1,15 @@
extends Control
var normal_boot : bool = false
@onready var menu_animation: AnimationNodeStateMachinePlayback = %MenuAnimationTree.get("parameters/playback")
@onready var menu_animation: AnimationPlayer = %MenuAnimationPlayer
@onready var load_animation: AnimationPlayer = %LoadAnimation
@export_file(".tscn") var youth_room_path: String
@export_file(".tscn") var transition_room_path: String
@export_file(".tscn") var adulthood_room_path: String
@export_file(".tscn") var ending_path: String
@onready var curtain: Panel = %Curtain
@onready var curtain: Curtain = %Curtain
@onready var credits_roll: Control = %CreditsRoll
@onready var main_menu: MainMenu = %MainMenu
@onready var pause_menu: PauseMenu = %PauseMenu
@ -25,23 +26,38 @@ enum AppState {INIT, LOADING, MENU, PLAY, PAUSE, CREDITS}
var state: AppState = AppState.INIT:
set(value):
print("main.gd: app_state changing to: %s" % str(state))
print("main.gd: app_state changing to: %s" % str(value))
match value:
AppState.INIT:
pass
AppState.LOADING:
%MenuAnimationTree["parameters/conditions/loading_done"] = false
credits_roll.hide()
main_menu.hide()
pause_menu.hide()
AppState.MENU:
menu_animation.travel("loading_menu")
await main_menu.execute()
credits_roll.hide()
pause_menu.hide()
main_menu.execute()
hotswap_ready = true
AppState.LOADING:
credits_roll.hide()
main_menu.hide()
pause_menu.hide()
AppState.PLAY:
menu_animation.travel("start_game")
await_ui_clear()
hide()
credits_roll.hide()
main_menu.hide()
pause_menu.hide()
hotswap_ready = false
menu_animation.play("hide_pause_menu")
if state == AppState.PAUSE:
menu_animation.play("hide_pause_menu")
AppState.PAUSE:
menu_animation.travel("reveal_pause_menu")
credits_roll.hide()
main_menu.hide()
pause_menu.appear()
menu_animation.play("reveal_pause_menu")
AppState.CREDITS:
menu_animation.travel("credits_roll")
main_menu.hide()
pause_menu.hide()
credits_roll.play()
state = value
@ -52,94 +68,164 @@ func _ready() -> void:
print("main.gd: _ready()")
main_menu.continue_button.pressed.connect(func(): state = AppState.PLAY)
main_menu.credits_button.pressed.connect(func(): state = AppState.CREDITS)
State.savegame_changed.connect(_on_savegame_changed)
#await get_tree().process_frame
await await_boot_completed()
await _boot_completed()
if normal_boot:
print("main.gd: normal boot (loading last save and showing main menu)")
call_deferred("start_menu")
startup.call_deferred()
else:
curtain.open()
print("main.gd: direct boot (hiding menus and entering main loop)")
state = AppState.PLAY
func start_menu():
#region sequence handling
if Steam.resume_from_steamdeck():
start_game()
initialise_room(State.save_game.current_room)
state = AppState.MENU
func await_boot_completed():
func _boot_completed():
print("main.gd: Awaiting Boot Completion ...")
while not State.all_ready:
await get_tree().process_frame
print("main.gd: Boot Completed.")
func await_ui_clear():
print("main.gd: Awaiting Menu Clear ...")
while not menu_animation.get_current_node() == "start_game":
await get_tree().process_frame
print("main.gd: Menu Cleared.")
func startup():
state = AppState.MENU
initialise_room(State.get_room_id())
func start_game(save: SaveGame = State.save_game) -> void:
print("main.gd: play_game()")
initialise_room(save.current_room)
State.active_room.play()
state = AppState.PLAY
if await initialise_room(save.current_room):
State.active_room.play()
State.active_room.prepare_next.connect(prepare_next_room)
State.active_room.proceed_to.connect(swap_room)
state = AppState.PLAY
else:
push_error("main.gd: Room failed to load during game startup!")
func prepare_next_room(new_room: Room.ids):
_load_room(room_paths.get(new_room, youth_room_path) as String)
func swap_room(new_room: Room.ids):
print("main.gd: Swapping rooms ...")
get_tree().paused = true
state = AppState.LOADING
State.save_game.current_room = new_room
# initialize_room is blocked from completing because of hotswap being false
start_game()
await curtain.blackout()
hotswap_ready = true
# room will start as soon as loading is done.
# Ending? Roll credits?
func is_game_active() -> bool:
return state == AppState.PLAY or state == AppState.PAUSE
func initialise_room(room_id: Room.ids):
func initialise_room(room_id: Room.ids) -> bool:
if State.active_room:
if State.active_room.id == room_id:
return
else:
menu_animation.travel("change_savegame")
_load_room(room_paths.get(room_id, youth_room_path) as String)
return true
return await _load_room(room_paths.get(room_id, youth_room_path) as String)
func _load_room(scene_path: String) -> void:
ResourceLoader.load_threaded_request(scene_path, "PackedScene", true)
## Loops for at least one frame and until a room is safely loaded.
func assure_room_initialized():
# just to make extra sure there is no room put to queue_free.
await get_tree().physics_frame
while not (current_loadpath == null and State.active_room):
await get_tree().physics_frame
await get_tree().create_timer(0.1).timeout
func handle_save_update(files_changed: PackedStringArray, files_deleted: PackedStringArray):
main_menu.save_game_list.reload_all_saves()
if files_deleted.has(State.save_game.absolute_path):
State.save_game = main_menu.save_game_list.get_most_recent_save()
elif files_changed.has(State.save_game.absolute_path):
State.save_game = Steamworks.save_list[State.save_game.absolute_path]
#region load handling
var load_id: int
var current_loadpath: String
var hotswap_ready: bool = true
## Unloads the current room, there is no harm in calling this multiple times.
func _unload_current_room():
if State.active_room:
State.active_room.unload()
State.active_room.queue_free()
State.active_room = null
while true:
## This function starts loading a new room in the background. Calling it twice will result in the previous load being abandoned.
func _load_room(scene_path: String) -> bool:
var this_load_id := randi()
var error:= ResourceLoader.load_threaded_request(scene_path, "PackedScene", true)
if error == Error.OK:
%LoadAnimation.play("LoadAnimation/loading_idle")
load_id = this_load_id
current_loadpath = scene_path
while true:
await get_tree().process_frame
var load_state := ResourceLoader.load_threaded_get_status(scene_path)
if this_load_id != load_id:
push_warning("main.gd: Interrupt while loading a room, aborting.")
if current_loadpath != scene_path:
call_deferred("_clear_loader_cache", scene_path)
return false
match load_state:
ResourceLoader.THREAD_LOAD_LOADED:
while not hotswap_ready:
await get_tree().process_frame
_unload_current_room()
var next_scene := ResourceLoader.load_threaded_get(scene_path) as PackedScene
current_loadpath = ""
State.active_room = next_scene.instantiate() as Room
%Stage.add_child(State.active_room)
await get_tree().process_frame
load_animation.play("LoadAnimation/loading_done")
curtain.open()
return true
ResourceLoader.THREAD_LOAD_FAILED:
break
push_error("main.gd: Couldn't load room %s" % scene_path)
else:
push_error(error_string(error))
return false
# As a load request cannot be aborted, this makes sure the Cache is cleared.
func _clear_loader_cache(path):
while ResourceLoader.load_threaded_get_status(path) == ResourceLoader.THREAD_LOAD_IN_PROGRESS:
await get_tree().process_frame
var load_state := ResourceLoader.load_threaded_get_status(scene_path)
match load_state:
ResourceLoader.THREAD_LOAD_LOADED:
var next_scene := ResourceLoader.load_threaded_get(scene_path) as PackedScene
State.active_room = next_scene.instantiate() as Room
%Stage.add_child(State.active_room)
await get_tree().process_frame
%MenuAnimationTree["parameters/conditions/loading_done"] = true
return
ResourceLoader.THREAD_LOAD_FAILED:
push_error("Failed to load room.")
break
if ResourceLoader.load_threaded_get_status(path) == ResourceLoader.THREAD_LOAD_LOADED:
var dump = ResourceLoader.load_threaded_get(path)
assert(false, "Couldn't load room %s" % scene_path)
func _on_savegame_changed():
if is_game_active():
push_warning("SaveGame change during active play. Might be unintentional.")
get_tree().paused = true
await curtain.blackout()
if State.active_room.id != State.save_game.current_room:
state = AppState.MENU
_unload_current_room()
else:
Prompts.display_hint("reload_hint", 5)
await State.active_room.play()
return
initialise_room(State.save_game.current_room)
var last_mode := DisplayServer.WINDOW_MODE_WINDOWED
func _unhandled_input(event: InputEvent) -> void:
#if event.is_actionxxx_type(): print_debug("Unhandled Input", event)
if event.is_action_pressed("ui_pause") and state == AppState.PLAY:
if event.is_action_pressed("pause") and state == AppState.PLAY:
state = AppState.PAUSE
if not Engine.is_editor_hint():

File diff suppressed because it is too large Load Diff

View File

@ -6,34 +6,62 @@ var _tween : Tween = null
func _ready() -> void:
print("curtain.gd: ready()")
visible = true
_check_boot.call_deferred()
_check_boot()
func _check_boot():
self.visible = Main.normal_boot
if visible:
self.modulate = Color.BLACK
_tween_with_interrupt(Color.WHITE, 1.0, true)
## Conceals the Game Stage
func close() -> void:
visible = true
print("curtain.gd: show()")
if _tween: _tween.kill()
_tween = create_tween()
_tween.tween_property(self, "modulate", Color.WHITE, 0.7)
await _tween.finished
func close() -> bool:
if visible and modulate == Color.WHITE:
return true
if await blackout():
return await _tween_with_interrupt(Color.WHITE, 0.7, true)
return false
## Conceals the Game Stage
func black() -> void:
func blackout() -> bool:
if visible and modulate == Color.BLACK:
return true
visible = true
print("curtain.gd: show()")
if _tween: _tween.kill()
_tween = create_tween()
_tween.tween_property(self, "modulate", Color.BLACK, 0.7)
await _tween.finished
return await _tween_with_interrupt(Color.BLACK, 0.2, true)
## Makes the Game Stage Visible
func open() -> void:
func open() -> bool:
if not visible:
return true
print("curtain.gd: hide()")
if _tween: _tween.kill()
if not visible: return true
var no_interrupt = await _tween_with_interrupt(Color.TRANSPARENT, 0.2, false)
if no_interrupt:
visible = false
return true
else:
return false
signal _tween_finished(successful: bool)
var _target_visibility: bool = false
## This allows multiple places to call the curtain close to each other with the only interference caused by mismatched visibility requirements.
func _tween_with_interrupt(to_color: Color, for_duration: float, target_visible: bool) -> bool:
if _tween:
if target_visible and _target_visibility:
return await _tween_finished
else:
_tween.kill()
_tween = create_tween()
_tween.tween_property(self, "modulate", Color.TRANSPARENT, 0.7)
await _tween.finished
visible = false
_tween.tween_property(self, "modulate", to_color, for_duration)
while true:
if _tween.is_valid():
if not _tween.is_running():
_tween_finished.emit(true)
return true
await get_tree().process_frame
else:
_tween_finished.emit(false)
return false
return false