Browse Source

Add a run-time saving/loading demo project

This is useful to load user-generated content without requiring users
to create a PCK file for it.
Hugo Locurcio 1 year ago
parent
commit
a2557fce51
28 changed files with 898 additions and 0 deletions
  1. 47 0
      loading/runtime_save_load/README.md
  2. 0 0
      loading/runtime_save_load/examples/.gdignore
  3. BIN
      loading/runtime_save_load/examples/3d_scenes/plastic_monobloc_chair_01_1k/plastic_monobloc_chair_01.bin
  4. 159 0
      loading/runtime_save_load/examples/3d_scenes/plastic_monobloc_chair_01_1k/plastic_monobloc_chair_01_1k.gltf
  5. BIN
      loading/runtime_save_load/examples/3d_scenes/plastic_monobloc_chair_01_1k/textures/plastic_monobloc_chair_01_arm_1k.jpg
  6. BIN
      loading/runtime_save_load/examples/3d_scenes/plastic_monobloc_chair_01_1k/textures/plastic_monobloc_chair_01_diff_1k.jpg
  7. BIN
      loading/runtime_save_load/examples/3d_scenes/plastic_monobloc_chair_01_1k/textures/plastic_monobloc_chair_01_nor_gl_1k.jpg
  8. BIN
      loading/runtime_save_load/examples/audio/item_spawn.ogg
  9. 94 0
      loading/runtime_save_load/examples/fonts/LICENSE.txt
  10. BIN
      loading/runtime_save_load/examples/fonts/hack_regular.woff2
  11. BIN
      loading/runtime_save_load/examples/fonts/inter_black.otf
  12. BIN
      loading/runtime_save_load/examples/fonts/newsreader_9pt_regular.ttf
  13. BIN
      loading/runtime_save_load/examples/images/godot_icon.bmp
  14. BIN
      loading/runtime_save_load/examples/images/godot_icon.jpg
  15. BIN
      loading/runtime_save_load/examples/images/godot_icon.png
  16. 75 0
      loading/runtime_save_load/examples/images/godot_icon.svg
  17. BIN
      loading/runtime_save_load/examples/images/godot_icon.tga
  18. BIN
      loading/runtime_save_load/examples/images/godot_icon.webp
  19. BIN
      loading/runtime_save_load/examples/misc/example.zip
  20. 1 0
      loading/runtime_save_load/examples/misc/file.txt
  21. 1 0
      loading/runtime_save_load/icon.svg
  22. 37 0
      loading/runtime_save_load/icon.svg.import
  23. 36 0
      loading/runtime_save_load/project.godot
  24. 249 0
      loading/runtime_save_load/runtime_save_load.gd
  25. 195 0
      loading/runtime_save_load/runtime_save_load.tscn
  26. 0 0
      loading/runtime_save_load/screenshots/.gdignore
  27. BIN
      loading/runtime_save_load/screenshots/runtime_save_load.webp
  28. 4 0
      loading/serialization/README.md

+ 47 - 0
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/).

+ 0 - 0
loading/runtime_save_load/examples/.gdignore


BIN
loading/runtime_save_load/examples/3d_scenes/plastic_monobloc_chair_01_1k/plastic_monobloc_chair_01.bin


+ 159 - 0
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"
+    }
+  ]
+}

BIN
loading/runtime_save_load/examples/3d_scenes/plastic_monobloc_chair_01_1k/textures/plastic_monobloc_chair_01_arm_1k.jpg


BIN
loading/runtime_save_load/examples/3d_scenes/plastic_monobloc_chair_01_1k/textures/plastic_monobloc_chair_01_diff_1k.jpg


BIN
loading/runtime_save_load/examples/3d_scenes/plastic_monobloc_chair_01_1k/textures/plastic_monobloc_chair_01_nor_gl_1k.jpg


BIN
loading/runtime_save_load/examples/audio/item_spawn.ogg


+ 94 - 0
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.

BIN
loading/runtime_save_load/examples/fonts/hack_regular.woff2


BIN
loading/runtime_save_load/examples/fonts/inter_black.otf


BIN
loading/runtime_save_load/examples/fonts/newsreader_9pt_regular.ttf


BIN
loading/runtime_save_load/examples/images/godot_icon.bmp


BIN
loading/runtime_save_load/examples/images/godot_icon.jpg


BIN
loading/runtime_save_load/examples/images/godot_icon.png


File diff suppressed because it is too large
+ 75 - 0
loading/runtime_save_load/examples/images/godot_icon.svg


BIN
loading/runtime_save_load/examples/images/godot_icon.tga


BIN
loading/runtime_save_load/examples/images/godot_icon.webp


BIN
loading/runtime_save_load/examples/misc/example.zip


+ 1 - 0
loading/runtime_save_load/examples/misc/file.txt

@@ -0,0 +1 @@
+Plain text file.

+ 1 - 0
loading/runtime_save_load/icon.svg

@@ -0,0 +1 @@
+<svg height="128" viewBox="0 0 128 128" width="128" xmlns="http://www.w3.org/2000/svg"><path d="m27.999994 10.00007a7.9999944 7.9999944 0 0 0 -7.999994 7.999994v91.999936a7.9999944 7.9999944 0 0 0 7.999994 7.99999h71.99995a7.9999944 7.9999944 0 0 0 7.999996-7.99999v-65.999954a1.9999986 1.9999986 0 0 0 -.57-1.413999l-31.999979-31.999977a1.9999986 1.9999986 0 0 0 -1.429999-.586zm0 3.999997h43.99997v23.999984a7.9999944 7.9999944 0 0 0 7.999994 7.999994h23.999982v63.999955a3.9999972 3.9999972 0 0 1 -3.999996 4h-71.99995a3.9999972 3.9999972 0 0 1 -3.999997-4v-91.999936a3.9999972 3.9999972 0 0 1 3.999997-3.999997z" fill="#8a0" stroke-width="2"/></svg>

+ 37 - 0
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

+ 36 - 0
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

+ 249 - 0
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

+ 195 - 0
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"]

+ 0 - 0
loading/runtime_save_load/screenshots/.gdignore


BIN
loading/runtime_save_load/screenshots/runtime_save_load.webp


+ 4 - 0
loading/serialization/README.md

@@ -11,6 +11,10 @@ More formats may be added in the future.
 For more information, see this documentation article:
 For more information, see this documentation article:
 https://docs.godotengine.org/en/latest/tutorials/io/saving_games.html
 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
 Language: GDScript
 
 
 Renderer: Mobile
 Renderer: Mobile

Some files were not shown because too many files changed in this diff