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