diff --git a/loading/runtime_save_load/README.md b/loading/runtime_save_load/README.md
new file mode 100644
index 0000000000..9906df2a75
--- /dev/null
+++ b/loading/runtime_save_load/README.md
@@ -0,0 +1,47 @@
+# Run-time File Saving and Loading
+
+This project showcases how to load and save various file types without going
+through Godot's resource importing system.
+
+This is useful to load/save images, sounds, 3D scenes and ZIP archives at
+run-time such as user-generated content, without requiring users to generate a
+PCK file through Godot.
+
+Can be loaded and saved at run-time:
+
+- Images (JPEG, PNG, WebP)
+- 3D scenes (glTF 2.0)
+- ZIP archives
+- Plain text files[^1]
+
+Can be loaded at run-time:
+
+- Images (TGA, BMP, SVG[^2])
+- Audio (Ogg Vorbis)
+- Fonts (TTF, OTF, WOFF, WOFF2, PFB, PFM, BMFont)
+
+[^1]: Manipulating custom binary formats is possible using the FileAccess and
+PackedByteArray classes, but this is not shown in this demo.
+
+[^2]: It is possible to procedurally generate SVG as text and save it to a file
+with `.svg` extension using the FileAccess class, but this is not shown in
+this demo.
+
+See the [Saving and Loading (Serialization)](/loading/serialization/) demo for
+an example of saving/loading game progress.
+
+Language: GDScript
+
+Renderer: Compatibility
+
+## Screenshots
+
+![Screenshot](screenshots/runtime_save_load.webp)
+
+## Licenses
+
+- Files in `examples/3d_scenes/plastic_monobloc_chair_01_1k/` are copyright
+ [Poly Haven](https://polyhaven.com/a/plastic_monobloc_chair_01)
+ and are licensed under [CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/).
+- Files in `examples/audio/` are copyright [Red Eclipse](https://redeclipse.net)
+ and are licensed under [CC BY-SA 4.0 International](https://www.creativecommons.org/licenses/by-sa/4.0/).
diff --git a/loading/runtime_save_load/examples/.gdignore b/loading/runtime_save_load/examples/.gdignore
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/loading/runtime_save_load/examples/3d_scenes/plastic_monobloc_chair_01_1k/plastic_monobloc_chair_01.bin b/loading/runtime_save_load/examples/3d_scenes/plastic_monobloc_chair_01_1k/plastic_monobloc_chair_01.bin
new file mode 100644
index 0000000000..a6dd20b508
Binary files /dev/null and b/loading/runtime_save_load/examples/3d_scenes/plastic_monobloc_chair_01_1k/plastic_monobloc_chair_01.bin differ
diff --git a/loading/runtime_save_load/examples/3d_scenes/plastic_monobloc_chair_01_1k/plastic_monobloc_chair_01_1k.gltf b/loading/runtime_save_load/examples/3d_scenes/plastic_monobloc_chair_01_1k/plastic_monobloc_chair_01_1k.gltf
new file mode 100644
index 0000000000..673d200afb
--- /dev/null
+++ b/loading/runtime_save_load/examples/3d_scenes/plastic_monobloc_chair_01_1k/plastic_monobloc_chair_01_1k.gltf
@@ -0,0 +1,159 @@
+{
+ "asset": {
+ "generator": "Khronos glTF Blender I/O v3.3.32",
+ "version": "2.0"
+ },
+ "scene": 0,
+ "scenes": [
+ {
+ "name": "Scene",
+ "nodes": [
+ 0
+ ]
+ }
+ ],
+ "nodes": [
+ {
+ "mesh": 0,
+ "name": "plastic_monobloc_chair_01"
+ }
+ ],
+ "materials": [
+ {
+ "doubleSided": true,
+ "name": "plastic_monobloc_chair_01",
+ "normalTexture": {
+ "index": 0
+ },
+ "pbrMetallicRoughness": {
+ "baseColorTexture": {
+ "index": 1
+ },
+ "metallicRoughnessTexture": {
+ "index": 2
+ }
+ }
+ }
+ ],
+ "meshes": [
+ {
+ "name": "Plane.002",
+ "primitives": [
+ {
+ "attributes": {
+ "POSITION": 0,
+ "NORMAL": 1,
+ "TEXCOORD_0": 2
+ },
+ "indices": 3,
+ "material": 0
+ }
+ ]
+ }
+ ],
+ "textures": [
+ {
+ "sampler": 0,
+ "source": 0
+ },
+ {
+ "sampler": 0,
+ "source": 1
+ },
+ {
+ "sampler": 0,
+ "source": 2
+ }
+ ],
+ "images": [
+ {
+ "mimeType": "image/jpeg",
+ "name": "plastic_monobloc_chair_01_nor_gl",
+ "uri": "textures/plastic_monobloc_chair_01_nor_gl_1k.jpg"
+ },
+ {
+ "mimeType": "image/jpeg",
+ "name": "plastic_monobloc_chair_01_diff",
+ "uri": "textures/plastic_monobloc_chair_01_diff_1k.jpg"
+ },
+ {
+ "mimeType": "image/jpeg",
+ "name": "plastic_monobloc_chair_01_metal-plastic_monobloc_chair_01_rough",
+ "uri": "textures/plastic_monobloc_chair_01_arm_1k.jpg"
+ }
+ ],
+ "accessors": [
+ {
+ "bufferView": 0,
+ "componentType": 5126,
+ "count": 3271,
+ "max": [
+ 0.3209305703639984,
+ 0.8798216581344604,
+ 0.2916412651538849
+ ],
+ "min": [
+ -0.3209305703639984,
+ -0.00001953914761543274,
+ -0.335950642824173
+ ],
+ "type": "VEC3"
+ },
+ {
+ "bufferView": 1,
+ "componentType": 5126,
+ "count": 3271,
+ "type": "VEC3"
+ },
+ {
+ "bufferView": 2,
+ "componentType": 5126,
+ "count": 3271,
+ "type": "VEC2"
+ },
+ {
+ "bufferView": 3,
+ "componentType": 5123,
+ "count": 10068,
+ "type": "SCALAR"
+ }
+ ],
+ "bufferViews": [
+ {
+ "buffer": 0,
+ "byteLength": 39252,
+ "byteOffset": 0,
+ "target": 34962
+ },
+ {
+ "buffer": 0,
+ "byteLength": 39252,
+ "byteOffset": 39252,
+ "target": 34962
+ },
+ {
+ "buffer": 0,
+ "byteLength": 26168,
+ "byteOffset": 78504,
+ "target": 34962
+ },
+ {
+ "buffer": 0,
+ "byteLength": 20136,
+ "byteOffset": 104672,
+ "target": 34963
+ }
+ ],
+ "samplers": [
+ {
+ "magFilter": 9729,
+ "minFilter": 9987
+ }
+ ],
+ "buffers": [
+ {
+ "byteLength": 124808,
+ "uri": "plastic_monobloc_chair_01.bin"
+ }
+ ]
+}
diff --git a/loading/runtime_save_load/examples/3d_scenes/plastic_monobloc_chair_01_1k/textures/plastic_monobloc_chair_01_arm_1k.jpg b/loading/runtime_save_load/examples/3d_scenes/plastic_monobloc_chair_01_1k/textures/plastic_monobloc_chair_01_arm_1k.jpg
new file mode 100644
index 0000000000..cd7c9642dd
Binary files /dev/null and b/loading/runtime_save_load/examples/3d_scenes/plastic_monobloc_chair_01_1k/textures/plastic_monobloc_chair_01_arm_1k.jpg differ
diff --git a/loading/runtime_save_load/examples/3d_scenes/plastic_monobloc_chair_01_1k/textures/plastic_monobloc_chair_01_diff_1k.jpg b/loading/runtime_save_load/examples/3d_scenes/plastic_monobloc_chair_01_1k/textures/plastic_monobloc_chair_01_diff_1k.jpg
new file mode 100644
index 0000000000..389fa4bfde
Binary files /dev/null and b/loading/runtime_save_load/examples/3d_scenes/plastic_monobloc_chair_01_1k/textures/plastic_monobloc_chair_01_diff_1k.jpg differ
diff --git a/loading/runtime_save_load/examples/3d_scenes/plastic_monobloc_chair_01_1k/textures/plastic_monobloc_chair_01_nor_gl_1k.jpg b/loading/runtime_save_load/examples/3d_scenes/plastic_monobloc_chair_01_1k/textures/plastic_monobloc_chair_01_nor_gl_1k.jpg
new file mode 100644
index 0000000000..97c6c30fb1
Binary files /dev/null and b/loading/runtime_save_load/examples/3d_scenes/plastic_monobloc_chair_01_1k/textures/plastic_monobloc_chair_01_nor_gl_1k.jpg differ
diff --git a/loading/runtime_save_load/examples/audio/item_spawn.ogg b/loading/runtime_save_load/examples/audio/item_spawn.ogg
new file mode 100644
index 0000000000..5ffa5b56f2
Binary files /dev/null and b/loading/runtime_save_load/examples/audio/item_spawn.ogg differ
diff --git a/loading/runtime_save_load/examples/fonts/LICENSE.txt b/loading/runtime_save_load/examples/fonts/LICENSE.txt
new file mode 100644
index 0000000000..ff80f8c615
--- /dev/null
+++ b/loading/runtime_save_load/examples/fonts/LICENSE.txt
@@ -0,0 +1,94 @@
+Copyright (c) 2016-2020 The Inter Project Authors.
+"Inter" is trademark of Rasmus Andersson.
+https://github.com/rsms/inter
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION AND CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/loading/runtime_save_load/examples/fonts/hack_regular.woff2 b/loading/runtime_save_load/examples/fonts/hack_regular.woff2
new file mode 100644
index 0000000000..730fd81ce5
Binary files /dev/null and b/loading/runtime_save_load/examples/fonts/hack_regular.woff2 differ
diff --git a/loading/runtime_save_load/examples/fonts/inter_black.otf b/loading/runtime_save_load/examples/fonts/inter_black.otf
new file mode 100644
index 0000000000..8e18e362ba
Binary files /dev/null and b/loading/runtime_save_load/examples/fonts/inter_black.otf differ
diff --git a/loading/runtime_save_load/examples/fonts/newsreader_9pt_regular.ttf b/loading/runtime_save_load/examples/fonts/newsreader_9pt_regular.ttf
new file mode 100644
index 0000000000..74f06fbebe
Binary files /dev/null and b/loading/runtime_save_load/examples/fonts/newsreader_9pt_regular.ttf differ
diff --git a/loading/runtime_save_load/examples/images/godot_icon.bmp b/loading/runtime_save_load/examples/images/godot_icon.bmp
new file mode 100644
index 0000000000..0ff62d0bbe
Binary files /dev/null and b/loading/runtime_save_load/examples/images/godot_icon.bmp differ
diff --git a/loading/runtime_save_load/examples/images/godot_icon.jpg b/loading/runtime_save_load/examples/images/godot_icon.jpg
new file mode 100644
index 0000000000..3ef552ae7e
Binary files /dev/null and b/loading/runtime_save_load/examples/images/godot_icon.jpg differ
diff --git a/loading/runtime_save_load/examples/images/godot_icon.png b/loading/runtime_save_load/examples/images/godot_icon.png
new file mode 100644
index 0000000000..d07867cd25
Binary files /dev/null and b/loading/runtime_save_load/examples/images/godot_icon.png differ
diff --git a/loading/runtime_save_load/examples/images/godot_icon.svg b/loading/runtime_save_load/examples/images/godot_icon.svg
new file mode 100644
index 0000000000..6804ab7e3c
--- /dev/null
+++ b/loading/runtime_save_load/examples/images/godot_icon.svg
@@ -0,0 +1,79 @@
+
+
diff --git a/loading/runtime_save_load/examples/images/godot_icon.tga b/loading/runtime_save_load/examples/images/godot_icon.tga
new file mode 100644
index 0000000000..b340c56491
Binary files /dev/null and b/loading/runtime_save_load/examples/images/godot_icon.tga differ
diff --git a/loading/runtime_save_load/examples/images/godot_icon.webp b/loading/runtime_save_load/examples/images/godot_icon.webp
new file mode 100644
index 0000000000..5a3264ab26
Binary files /dev/null and b/loading/runtime_save_load/examples/images/godot_icon.webp differ
diff --git a/loading/runtime_save_load/examples/misc/example.zip b/loading/runtime_save_load/examples/misc/example.zip
new file mode 100644
index 0000000000..0cb5c5ebb5
Binary files /dev/null and b/loading/runtime_save_load/examples/misc/example.zip differ
diff --git a/loading/runtime_save_load/examples/misc/file.txt b/loading/runtime_save_load/examples/misc/file.txt
new file mode 100644
index 0000000000..e63e4206ed
--- /dev/null
+++ b/loading/runtime_save_load/examples/misc/file.txt
@@ -0,0 +1 @@
+Plain text file.
diff --git a/loading/runtime_save_load/icon.svg b/loading/runtime_save_load/icon.svg
new file mode 100644
index 0000000000..42f30d56b2
--- /dev/null
+++ b/loading/runtime_save_load/icon.svg
@@ -0,0 +1 @@
+
diff --git a/loading/runtime_save_load/icon.svg.import b/loading/runtime_save_load/icon.svg.import
new file mode 100644
index 0000000000..fefc288d92
--- /dev/null
+++ b/loading/runtime_save_load/icon.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bpf0p4mn3trr3"
+path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icon.svg"
+dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/loading/runtime_save_load/project.godot b/loading/runtime_save_load/project.godot
new file mode 100644
index 0000000000..a46fdd5c14
--- /dev/null
+++ b/loading/runtime_save_load/project.godot
@@ -0,0 +1,36 @@
+; Engine configuration file.
+; It's best edited using the editor UI and not directly,
+; since the parameters that go here are not all obvious.
+;
+; Format:
+; [section] ; section goes between []
+; param=value ; assign values to parameters
+
+config_version=5
+
+[application]
+
+config/name="Run-time File Saving and Loading"
+config/description="This project showcases how to load and save various file types without going
+through Godot's resource importing system.
+
+This is useful to load/save images, sounds, 3D scenes and ZIP archives at
+run-time such as user-generated content, without requiring users to generate a
+PCK file through Godot."
+config/tags=PackedStringArray("demo", "filesystem", "official")
+run/main_scene="res://runtime_save_load.tscn"
+config/features=PackedStringArray("4.2")
+run/low_processor_mode=true
+config/icon="res://icon.svg"
+
+[display]
+
+window/stretch/mode="canvas_items"
+window/stretch/aspect="expand"
+window/vsync/vsync_mode=0
+
+[rendering]
+
+renderer/rendering_method="gl_compatibility"
+lights_and_shadows/directional_shadow/size=8192
+lights_and_shadows/directional_shadow/soft_shadow_filter_quality=5
diff --git a/loading/runtime_save_load/runtime_save_load.gd b/loading/runtime_save_load/runtime_save_load.gd
new file mode 100644
index 0000000000..c8475eb02a
--- /dev/null
+++ b/loading/runtime_save_load/runtime_save_load.gd
@@ -0,0 +1,249 @@
+extends Control
+
+@onready var file_path_edit := $MarginContainer/VBoxContainer/HBoxContainer/FilePath as LineEdit
+@onready var file_dialog := $MarginContainer/VBoxContainer/HBoxContainer/FileDialog as FileDialog
+@onready var plain_text_viewer := $MarginContainer/VBoxContainer/Result/PlainTextViewer as ScrollContainer
+@onready var plain_text_viewer_label := $MarginContainer/VBoxContainer/Result/PlainTextViewer/Label as Label
+@onready var texture_viewer := $MarginContainer/VBoxContainer/Result/TextureViewer as TextureRect
+@onready var audio_player := $MarginContainer/VBoxContainer/Result/AudioPlayer as Button
+@onready var audio_stream_player := $MarginContainer/VBoxContainer/Result/AudioPlayer/AudioStreamPlayer as AudioStreamPlayer
+@onready var scene_viewer := $MarginContainer/VBoxContainer/Result/SceneViewer as SubViewportContainer
+@onready var scene_viewer_camera := $MarginContainer/VBoxContainer/Result/SceneViewer/SubViewport/Camera3D as Camera3D
+@onready var font_viewer := $MarginContainer/VBoxContainer/Result/FontViewer as Label
+@onready var zip_viewer := $MarginContainer/VBoxContainer/Result/ZIPViewer as HSplitContainer
+@onready var zip_viewer_file_list := $MarginContainer/VBoxContainer/Result/ZIPViewer/FileList as ItemList
+@onready var zip_viewer_file_preview := $MarginContainer/VBoxContainer/Result/ZIPViewer/FilePreview as Label
+@onready var error_label := $MarginContainer/VBoxContainer/Result/ErrorLabel as Label
+
+@onready var export_button := $MarginContainer/VBoxContainer/Export as Button
+@onready var export_file_dialog := $MarginContainer/VBoxContainer/Export/FileDialog as FileDialog
+
+var zip_reader := ZIPReader.new()
+
+# Keeps reference to the root node imported in the 3D scene viewer,
+# so that it can be exported later.
+var scene_viewer_root_node: Node
+
+func _on_browse_pressed() -> void:
+ file_dialog.popup_centered_ratio()
+
+
+func _on_file_path_text_submitted(new_text: String) -> void:
+ open_file(new_text)
+ # Put the caret at the end of the submitted text.
+ file_path_edit.caret_column = file_path_edit.text.length()
+
+
+func _on_file_dialog_file_selected(path: String) -> void:
+ open_file(path)
+
+
+func reset_visibility() -> void:
+ plain_text_viewer.visible = false
+ texture_viewer.visible = false
+ audio_player.visible = false
+
+ scene_viewer.visible = false
+ var last_child := scene_viewer.get_child(-1)
+ if last_child is Node3D:
+ scene_viewer.remove_child(last_child)
+ last_child.queue_free()
+
+ font_viewer.visible = false
+
+ zip_viewer.visible = false
+ zip_viewer_file_list.clear()
+
+ error_label.visible = false
+ export_button.disabled = false
+
+
+func _on_audio_player_pressed() -> void:
+ audio_stream_player.play()
+
+
+func _on_scene_viewer_zoom_value_changed(value: float) -> void:
+ # Slider uses negative value so that it can be inverted easily
+ # (lower Camera3D orthogonal size is more zoomed *in*).
+ scene_viewer_camera.size = abs(value)
+
+
+func _on_zip_viewer_item_selected(index: int) -> void:
+ zip_viewer_file_preview.text = zip_reader.read_file(
+ zip_viewer_file_list.get_item_text(index)
+ ).get_string_from_utf8()
+
+
+#region File exporting
+func _on_export_pressed() -> void:
+ export_file_dialog.popup_centered_ratio()
+
+
+func _on_export_file_dialog_file_selected(path: String) -> void:
+ if plain_text_viewer.visible:
+ var file_access := FileAccess.open(path, FileAccess.WRITE)
+ file_access.store_string(plain_text_viewer_label.text)
+ file_access.close()
+
+ elif texture_viewer.visible:
+ var image := texture_viewer.texture.get_image()
+ if path.ends_with(".png"):
+ image.save_png(path)
+ if path.ends_with(".jpg") or path.ends_with(".jpeg"):
+ const JPG_QUALITY = 0.9
+ image.save_jpg(path, JPG_QUALITY)
+ if path.ends_with(".webp"):
+ # Saving WebP is lossless by default, but can be made lossy using
+ # optional parameters in `Image.save_webp()`.
+ image.save_webp(path)
+
+ elif audio_player.visible:
+ # Ogg Vorbis audio can't be exported at runtime to a standard format
+ # (only WAV files can be using `AudioStreamWAV.save_to_wav()`).
+ pass
+
+ elif scene_viewer.visible:
+ var gltf_document := GLTFDocument.new()
+ var gltf_state := GLTFState.new()
+ gltf_document.append_from_scene(scene_viewer_root_node, gltf_state)
+ # The file extension in the output `path` (`.gltf` or `.glb`) determines
+ # whether the output uses text or binary format. Binary format is faster
+ # to write and smaller, but harder to debug. The binary format is also
+ # more suited to embedding textures.
+ gltf_document.write_to_filesystem(gltf_state, path)
+
+ elif font_viewer.visible:
+ # Fonts can't be exported at runtime to a standard format
+ # (only to a Godot-specific `.res` format using the ResourceSaver class).
+ pass
+
+ elif zip_viewer.visible:
+ var zip_packer := ZIPPacker.new()
+ var error := zip_packer.open(path)
+ if error != OK:
+ push_error("An error occurred while trying to save a ZIP archive to: %s" % path)
+ return
+
+ for file in zip_reader.get_files():
+ zip_packer.start_file(file)
+ zip_packer.write_file(zip_reader.read_file(file))
+ zip_packer.close_file()
+
+ zip_packer.close()
+#endregion
+
+
+func show_error(message: String) -> void:
+ reset_visibility()
+ error_label.text = "ERROR: %s" % message
+ error_label.visible = true
+
+
+func open_file(path: String) -> void:
+ print_rich("Opening: [u]%s[/u]" % path)
+ file_path_edit.text = path
+ var path_lower := path.to_lower()
+
+ # Images.
+ if (
+ path_lower.ends_with(".jpg")
+ or path_lower.ends_with(".jpeg")
+ or path_lower.ends_with(".png")
+ or path_lower.ends_with(".webp")
+ or path_lower.ends_with(".svg")
+ or path_lower.ends_with(".tga")
+ or path_lower.ends_with(".bmp")
+ ):
+ # This method handles everything, from format detection based on
+ # file extension to reading the file from disk. If you need error handling
+ # or more control (such as changing the scale SVG is loaded at),
+ # use the `load_*_from_buffer()` (where `*` is a file extension)
+ # and `load_svg_from_string()` methods from the Image class.
+ var image := Image.load_from_file(path)
+ reset_visibility()
+ export_file_dialog.filters = ["*.png ; PNG Image", "*.jpg, *.jpeg ; JPEG Image", "*.webp ; WebP Image"]
+ texture_viewer.visible = true
+ texture_viewer.texture = ImageTexture.create_from_image(image)
+
+ # Audio.
+ # Run-time MP3 and WAV loading aren't supported by the engine yet.
+ elif path_lower.ends_with(".ogg"):
+ # `AudioStreamOggVorbis.load_from_buffer()` can alternatively be used
+ # if you have Ogg Vorbis data in a PackedByteArray instead of a file.
+ audio_stream_player.stream = AudioStreamOggVorbis.load_from_file(path)
+ reset_visibility()
+ export_button.disabled = true
+ audio_player.visible = true
+
+ # 3D scenes.
+ elif path_lower.ends_with(".gltf") or path_lower.ends_with(".glb"):
+ # GLTFState is used by GLTFDocument to store the loaded scene's state.
+ # GLTFDocument is the class that handles actually loading glTF data into a Godot node tree,
+ # which means it supports glTF features such as lights and cameras.
+ var gltf_document := GLTFDocument.new()
+ var gltf_state := GLTFState.new()
+ var error := gltf_document.append_from_file(path, gltf_state)
+ if error == OK:
+ scene_viewer_root_node = gltf_document.generate_scene(gltf_state)
+ reset_visibility()
+ scene_viewer.add_child(scene_viewer_root_node)
+ export_file_dialog.filters = ["*.gltf ; glTF Text Scene", "*.glb ; glTF Binary Scene"]
+ scene_viewer.visible = true
+ else:
+ show_error('Couldn\'t load "%s" as a glTF scene (error code: %s).' % [path.get_file(), error_string(error)])
+
+ # Fonts.
+ elif (
+ path_lower.ends_with(".ttf")
+ or path_lower.ends_with(".otf")
+ or path_lower.ends_with(".woff")
+ or path_lower.ends_with(".woff2")
+ or path_lower.ends_with(".pfb")
+ or path_lower.ends_with(".pfm")
+ or path_lower.ends_with(".fnt")
+ or path_lower.ends_with(".font")
+ ):
+ var font_file := FontFile.new()
+ if path_lower.ends_with(".fnt") or path_lower.ends_with(".font"):
+ font_file.load_bitmap_font(path)
+ else:
+ font_file.load_dynamic_font(path)
+
+ if not font_file.data.is_empty():
+ font_viewer.add_theme_font_override("font", font_file)
+ reset_visibility()
+ font_viewer.visible = true
+ export_button.disabled = true
+ else:
+ show_error('Couldn\'t load "%s" as a font.' % path.get_file())
+
+ # ZIP archives.
+ elif path_lower.ends_with(".zip"):
+ # This supports any ZIP file, including files generated by Godot's "Export PCK/ZIP" functionality
+ # (although these will contain imported Godot resources rather than the original project files).
+ #
+ # Use `ProjectSettings.load_resource_pack()` to load PCK or ZIP files exported by Godot as
+ # additional data packs. That approach is preferred for DLCs, as it makes interacting with
+ # additional data packs seamless (virtual filesystem).
+ zip_reader.open(path)
+ var files := zip_reader.get_files()
+ files.sort()
+ export_file_dialog.filters = ["*.zip ; ZIP Archive"]
+ reset_visibility()
+ for file in files:
+ zip_viewer_file_list.add_item(file, null)
+ # Make folders disabled in the list.
+ zip_viewer_file_list.set_item_disabled(-1, file.ends_with("/"))
+
+ zip_viewer.visible = true
+
+ # Fallback.
+ else:
+ # Open as plain text and display contents if possible.
+ var file_contents := FileAccess.get_file_as_string(path)
+ if file_contents.is_empty():
+ show_error("File is empty or is a binary file.")
+ else:
+ plain_text_viewer_label.text = file_contents
+ reset_visibility()
+ plain_text_viewer.visible = true
diff --git a/loading/runtime_save_load/runtime_save_load.tscn b/loading/runtime_save_load/runtime_save_load.tscn
new file mode 100644
index 0000000000..7797f38300
--- /dev/null
+++ b/loading/runtime_save_load/runtime_save_load.tscn
@@ -0,0 +1,195 @@
+[gd_scene load_steps=2 format=3 uid="uid://ca0d8q5aicxfr"]
+
+[ext_resource type="Script" path="res://runtime_save_load.gd" id="1_2gu2h"]
+
+[node name="RuntimeLoadSave" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_2gu2h")
+
+[node name="MarginContainer" type="MarginContainer" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_constants/margin_left = 20
+theme_override_constants/margin_top = 20
+theme_override_constants/margin_right = 20
+theme_override_constants/margin_bottom = 20
+
+[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
+layout_mode = 2
+theme_override_constants/separation = 10
+
+[node name="Help" type="Label" parent="MarginContainer/VBoxContainer"]
+layout_mode = 2
+theme_override_colors/font_color = Color(1, 1, 1, 0.752941)
+text = "This project showcases how to load and save various file types without going through Godot's resource importing system.
+This is useful to load/save images, sounds, 3D scenes and ZIP archives at run-time such as user-generated content,
+without requiring users to generate a PCK file through Godot."
+autowrap_mode = 2
+
+[node name="Instructions" type="Label" parent="MarginContainer/VBoxContainer"]
+layout_mode = 2
+text = "Select a file to load (look in the \"examples\" folder):"
+autowrap_mode = 2
+
+[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
+layout_mode = 2
+
+[node name="FilePath" type="LineEdit" parent="MarginContainer/VBoxContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+placeholder_text = "Click \"Browse\" on the right or enter path to file"
+
+[node name="Browse" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer"]
+custom_minimum_size = Vector2(100, 0)
+layout_mode = 2
+text = "Browse"
+
+[node name="FileDialog" type="FileDialog" parent="MarginContainer/VBoxContainer/HBoxContainer"]
+title = "Open a File"
+size = Vector2i(392, 159)
+ok_button_text = "Open"
+file_mode = 0
+access = 2
+
+[node name="Result" type="CenterContainer" parent="MarginContainer/VBoxContainer"]
+custom_minimum_size = Vector2(0, 400)
+layout_mode = 2
+
+[node name="PlainTextViewer" type="ScrollContainer" parent="MarginContainer/VBoxContainer/Result"]
+visible = false
+custom_minimum_size = Vector2(1050, 400)
+layout_mode = 2
+horizontal_scroll_mode = 0
+
+[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Result/PlainTextViewer"]
+custom_minimum_size = Vector2(1050, 400)
+layout_mode = 2
+theme_override_colors/font_color = Color(1, 0.941176, 0.627451, 1)
+autowrap_mode = 2
+
+[node name="TextureViewer" type="TextureRect" parent="MarginContainer/VBoxContainer/Result"]
+visible = false
+custom_minimum_size = Vector2(0, 400)
+layout_mode = 2
+expand_mode = 3
+
+[node name="AudioPlayer" type="Button" parent="MarginContainer/VBoxContainer/Result"]
+visible = false
+layout_mode = 2
+theme_override_font_sizes/font_size = 24
+text = "Play Audio"
+
+[node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="MarginContainer/VBoxContainer/Result/AudioPlayer"]
+volume_db = -10.0
+
+[node name="SceneViewer" type="SubViewportContainer" parent="MarginContainer/VBoxContainer/Result"]
+visible = false
+custom_minimum_size = Vector2(1050, 400)
+layout_mode = 2
+stretch = true
+
+[node name="SubViewport" type="SubViewport" parent="MarginContainer/VBoxContainer/Result/SceneViewer"]
+handle_input_locally = false
+msaa_3d = 2
+size = Vector2i(1050, 400)
+render_target_update_mode = 0
+
+[node name="Camera3D" type="Camera3D" parent="MarginContainer/VBoxContainer/Result/SceneViewer/SubViewport"]
+transform = Transform3D(0.877582, -0.229849, 0.420736, 0, 0.877582, 0.479426, -0.479426, -0.420736, 0.770151, 26.1772, 30.2846, 47.917)
+projection = 1
+size = 1.2
+near = 0.001
+far = 100.0
+
+[node name="KeyLight" type="DirectionalLight3D" parent="MarginContainer/VBoxContainer/Result/SceneViewer/SubViewport"]
+transform = Transform3D(0.775472, 0.453626, -0.439166, 0, 0.695563, 0.718465, 0.631382, -0.557149, 0.53939, -2.78761, 4.56046, 3.42378)
+shadow_enabled = true
+directional_shadow_mode = 0
+directional_shadow_fade_start = 1.0
+directional_shadow_max_distance = 20.0
+
+[node name="FillLight" type="DirectionalLight3D" parent="MarginContainer/VBoxContainer/Result/SceneViewer/SubViewport"]
+transform = Transform3D(-0.775472, -0.453626, 0.439166, 4.2942e-09, 0.695563, 0.718465, -0.631382, 0.557149, -0.53939, -2.78761, 2.56046, 3.42378)
+light_energy = 0.3
+shadow_bias = 0.04
+directional_shadow_mode = 0
+directional_shadow_max_distance = 30.0
+
+[node name="BackLight" type="DirectionalLight3D" parent="MarginContainer/VBoxContainer/Result/SceneViewer/SubViewport"]
+transform = Transform3D(1, 0, 0, 0, -4.37114e-08, -1, 0, 1, -4.37114e-08, -2.78761, 0.56046, 3.42378)
+light_energy = 0.1
+shadow_bias = 0.04
+directional_shadow_mode = 0
+directional_shadow_max_distance = 30.0
+
+[node name="Zoom" type="HSlider" parent="MarginContainer/VBoxContainer/Result/SceneViewer"]
+custom_minimum_size = Vector2(1050, 0)
+layout_mode = 2
+min_value = -100.0
+max_value = -0.1
+step = 0.0
+value = -1.2
+
+[node name="FontViewer" type="Label" parent="MarginContainer/VBoxContainer/Result"]
+visible = false
+custom_minimum_size = Vector2(1050, 400)
+layout_mode = 2
+theme_override_font_sizes/font_size = 48
+text = "abcdefghijklmnopqrstuvwxyz
+ABCDEFGHIJKLM
+NOPQRSTUVWXYZ
+1234567890
+()[]{}<> -+:!?$@ éàç ×÷±≠ø ↔"
+horizontal_alignment = 1
+vertical_alignment = 1
+
+[node name="ZIPViewer" type="HSplitContainer" parent="MarginContainer/VBoxContainer/Result"]
+visible = false
+custom_minimum_size = Vector2(1050, 400)
+layout_mode = 2
+split_offset = 525
+
+[node name="FileList" type="ItemList" parent="MarginContainer/VBoxContainer/Result/ZIPViewer"]
+custom_minimum_size = Vector2(150, 0)
+layout_mode = 2
+
+[node name="FilePreview" type="Label" parent="MarginContainer/VBoxContainer/Result/ZIPViewer"]
+custom_minimum_size = Vector2(150, 0)
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 1
+theme_override_colors/font_color = Color(0.631373, 0.862745, 1, 1)
+autowrap_mode = 2
+text_overrun_behavior = 3
+
+[node name="ErrorLabel" type="Label" parent="MarginContainer/VBoxContainer/Result"]
+visible = false
+layout_mode = 2
+theme_override_colors/font_color = Color(1, 0.501961, 0.501961, 1)
+theme_override_font_sizes/font_size = 24
+
+[node name="Export" type="Button" parent="MarginContainer/VBoxContainer"]
+layout_mode = 2
+text = "Export"
+
+[node name="FileDialog" type="FileDialog" parent="MarginContainer/VBoxContainer/Export"]
+title = "Export File"
+access = 2
+
+[connection signal="text_submitted" from="MarginContainer/VBoxContainer/HBoxContainer/FilePath" to="." method="_on_file_path_text_submitted"]
+[connection signal="pressed" from="MarginContainer/VBoxContainer/HBoxContainer/Browse" to="." method="_on_browse_pressed"]
+[connection signal="file_selected" from="MarginContainer/VBoxContainer/HBoxContainer/FileDialog" to="." method="_on_file_dialog_file_selected"]
+[connection signal="pressed" from="MarginContainer/VBoxContainer/Result/AudioPlayer" to="." method="_on_audio_player_pressed"]
+[connection signal="value_changed" from="MarginContainer/VBoxContainer/Result/SceneViewer/Zoom" to="." method="_on_scene_viewer_zoom_value_changed"]
+[connection signal="item_selected" from="MarginContainer/VBoxContainer/Result/ZIPViewer/FileList" to="." method="_on_zip_viewer_item_selected"]
+[connection signal="pressed" from="MarginContainer/VBoxContainer/Export" to="." method="_on_export_pressed"]
+[connection signal="file_selected" from="MarginContainer/VBoxContainer/Export/FileDialog" to="." method="_on_export_file_dialog_file_selected"]
diff --git a/loading/runtime_save_load/screenshots/.gdignore b/loading/runtime_save_load/screenshots/.gdignore
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/loading/runtime_save_load/screenshots/runtime_save_load.webp b/loading/runtime_save_load/screenshots/runtime_save_load.webp
new file mode 100644
index 0000000000..94e98b3fe9
Binary files /dev/null and b/loading/runtime_save_load/screenshots/runtime_save_load.webp differ
diff --git a/loading/serialization/README.md b/loading/serialization/README.md
index 058f388f6f..e3a1f51b7a 100644
--- a/loading/serialization/README.md
+++ b/loading/serialization/README.md
@@ -11,6 +11,10 @@ More formats may be added in the future.
For more information, see this documentation article:
https://docs.godotengine.org/en/latest/tutorials/io/saving_games.html
+See the [Run-time File Saving and Loading](/loading/runtime_save_load/) demo for
+an example of loading various file types in an exported project without needing
+to import them.
+
Language: GDScript
Renderer: Mobile