exchanger-themes/preview_scenes/PreviewLiveReload.gd
Danil Iarosh ca52efbedc Initial commit: Exchanger Theme DLC Builder
Godot 4.7 project for authoring, previewing, validating, and building Exchanger PCK themes. Includes the example and space-pixel themes, binding contracts, editor tooling, validation/build scripts, and project documentation.
2026-09-22 11:43:13 +03:00

158 lines
5.0 KiB
GDScript

extends Control
const POLL_INTERVAL_SECONDS := 0.25
const RELOAD_DEBOUNCE_SECONDS := 0.45
const SELECTED_TAB_META := &"theme_preview_selected_tab"
const SELECTED_SETTINGS_TAB_META := &"theme_preview_selected_settings_tab"
const WATCHED_DIRECTORIES := [
"res://exchanger_theme_dlc",
"res://preview_scenes",
]
const WATCHED_FILES := [
"res://preview.tscn",
]
const WATCHED_EXTENSIONS := {
"jpeg": true,
"jpg": true,
"json": true,
"gd": true,
"mp3": true,
"ogg": true,
"otf": true,
"png": true,
"svg": true,
"tres": true,
"tscn": true,
"ttf": true,
"wav": true,
"webp": true,
}
const HASHED_EXTENSIONS := {
"json": true,
"tres": true,
"tscn": true,
}
var _poll_elapsed := 0.0
var _reload_countdown := -1.0
var _snapshot: Dictionary = {}
var _pending_changes: Dictionary = {}
func _ready() -> void:
_restore_selected_tab()
_snapshot = _capture_snapshot()
print("[Theme Preview] Live reload is active.")
func _process(delta: float) -> void:
if _reload_countdown >= 0.0:
_reload_countdown -= delta
if _reload_countdown <= 0.0:
_reload_preview()
return
_poll_elapsed += delta
if _poll_elapsed < POLL_INTERVAL_SECONDS:
return
_poll_elapsed = 0.0
var next_snapshot := _capture_snapshot()
var changed_paths := _find_changed_paths(_snapshot, next_snapshot)
_snapshot = next_snapshot
if changed_paths.is_empty():
return
for path in changed_paths:
_pending_changes[path] = true
_reload_countdown = RELOAD_DEBOUNCE_SECONDS
func _restore_selected_tab() -> void:
var screens := get_node_or_null("Screens") as TabContainer
if screens == null or screens.get_tab_count() == 0:
return
var selected_tab := int(get_tree().root.get_meta(SELECTED_TAB_META, screens.current_tab))
screens.current_tab = clampi(selected_tab, 0, screens.get_tab_count() - 1)
var settings_tabs := get_node_or_null("Screens/SETTINGS/Sections") as TabContainer
if settings_tabs != null and settings_tabs.get_tab_count() > 0:
var selected_settings_tab := int(
get_tree().root.get_meta(SELECTED_SETTINGS_TAB_META, settings_tabs.current_tab)
)
settings_tabs.current_tab = clampi(
selected_settings_tab,
0,
settings_tabs.get_tab_count() - 1
)
func _capture_snapshot() -> Dictionary:
var result: Dictionary = {}
for directory_path in WATCHED_DIRECTORIES:
_scan_directory(directory_path, result)
for file_path in WATCHED_FILES:
if FileAccess.file_exists(file_path):
result[file_path] = _file_fingerprint(file_path)
return result
func _scan_directory(directory_path: String, result: Dictionary) -> void:
if not DirAccess.dir_exists_absolute(directory_path):
return
for file_name in DirAccess.get_files_at(directory_path):
var file_path := directory_path.path_join(file_name)
var extension := file_name.get_extension().to_lower()
if WATCHED_EXTENSIONS.has(extension):
result[file_path] = _file_fingerprint(file_path)
for child_directory in DirAccess.get_directories_at(directory_path):
_scan_directory(directory_path.path_join(child_directory), result)
func _file_fingerprint(file_path: String) -> String:
var modified_time := FileAccess.get_modified_time(file_path)
var file := FileAccess.open(file_path, FileAccess.READ)
var file_size := file.get_length() if file != null else -1
var extension := file_path.get_extension().to_lower()
if HASHED_EXTENSIONS.has(extension):
return "%s:%s:%s" % [modified_time, file_size, FileAccess.get_md5(file_path)]
return "%s:%s" % [modified_time, file_size]
func _find_changed_paths(previous: Dictionary, current: Dictionary) -> PackedStringArray:
var changed := PackedStringArray()
for path in current:
if not previous.has(path) or previous[path] != current[path]:
changed.append(path)
for path in previous:
if not current.has(path):
changed.append(path)
return changed
func _reload_preview() -> void:
var screens := get_node_or_null("Screens") as TabContainer
if screens != null:
get_tree().root.set_meta(SELECTED_TAB_META, screens.current_tab)
var settings_tabs := get_node_or_null("Screens/SETTINGS/Sections") as TabContainer
if settings_tabs != null:
get_tree().root.set_meta(SELECTED_SETTINGS_TAB_META, settings_tabs.current_tab)
var changed_paths := PackedStringArray(_pending_changes.keys())
changed_paths.sort()
_pending_changes.clear()
_reload_countdown = -1.0
for path in changed_paths:
if ResourceLoader.exists(path):
ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_REPLACE_DEEP)
print("[Theme Preview] Reloading after changes: %s" % ", ".join(changed_paths))
var reload_error := get_tree().reload_current_scene()
if reload_error != OK:
push_error("[Theme Preview] Scene reload failed with error %s." % reload_error)