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.
47
loading/runtime_save_load/README.md
Normal file
@@ -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
|
||||
|
||||

|
||||
|
||||
## 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
loading/runtime_save_load/examples/.gdignore
Normal file
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 620 KiB |
|
After Width: | Height: | Size: 548 KiB |
|
After Width: | Height: | Size: 615 KiB |
BIN
loading/runtime_save_load/examples/audio/item_spawn.ogg
Normal file
94
loading/runtime_save_load/examples/fonts/LICENSE.txt
Normal file
@@ -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
Normal file
BIN
loading/runtime_save_load/examples/fonts/inter_black.otf
Normal file
BIN
loading/runtime_save_load/examples/images/godot_icon.bmp
Normal file
|
After Width: | Height: | Size: 384 KiB |
BIN
loading/runtime_save_load/examples/images/godot_icon.jpg
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
loading/runtime_save_load/examples/images/godot_icon.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
79
loading/runtime_save_load/examples/images/godot_icon.svg
Normal file
@@ -0,0 +1,79 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="256"
|
||||
height="384"
|
||||
viewBox="0 0 256 384"
|
||||
version="1.1"
|
||||
id="svg6"
|
||||
sodipodi:docname="godot_icon.svg"
|
||||
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs6" />
|
||||
<sodipodi:namedview
|
||||
id="namedview6"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="1.8828125"
|
||||
inkscape:cx="161.72614"
|
||||
inkscape:cy="126.40664"
|
||||
inkscape:window-width="3840"
|
||||
inkscape:window-height="2132"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg6" />
|
||||
<g
|
||||
fill="#ffffff"
|
||||
id="g4"
|
||||
transform="matrix(0.25,0,0,0.25,0,-0.330465)">
|
||||
<path
|
||||
d="m 105,673 v 33 q 407,354 814,0 v -33 z"
|
||||
id="path1" />
|
||||
<path
|
||||
fill="#478cbf"
|
||||
d="m 105,673 152,14 q 12,1 15,14 l 4,67 132,10 8,-61 q 2,-11 15,-15 h 162 q 13,4 15,15 l 8,61 132,-10 4,-67 q 3,-13 15,-14 L 919,673 V 427 q 30,-39 56,-81 -35,-59 -83,-108 -43,20 -82,47 -40,-37 -88,-64 7,-51 8,-102 -59,-28 -123,-42 -26,43 -46,89 -49,-7 -98,0 -20,-46 -46,-89 -64,14 -123,42 1,51 8,102 -48,27 -88,64 -39,-27 -82,-47 -48,49 -83,108 26,42 56,81 z m 0,33 v 39 c 0,276 813,276 813,0 v -39 l -134,12 -5,69 q -2,10 -14,13 l -162,11 q -12,0 -16,-11 L 577,735 H 447 l -10,65 q -4,11 -16,11 L 259,800 q -12,-3 -14,-13 l -5,-69 z"
|
||||
id="path2" />
|
||||
<path
|
||||
d="m 483,600 c 3,34 55,34 58,0 v -86 c -3,-34 -55,-34 -58,0 z"
|
||||
id="path3" />
|
||||
<circle
|
||||
cx="725"
|
||||
cy="526"
|
||||
r="90"
|
||||
id="circle3" />
|
||||
<circle
|
||||
cx="299"
|
||||
cy="526"
|
||||
r="90"
|
||||
id="circle4" />
|
||||
</g>
|
||||
<g
|
||||
fill="#414042"
|
||||
id="g6"
|
||||
transform="matrix(0.25,0,0,0.25,0,-0.330465)">
|
||||
<circle
|
||||
cx="307"
|
||||
cy="532"
|
||||
r="60"
|
||||
id="circle5" />
|
||||
<circle
|
||||
cx="717"
|
||||
cy="532"
|
||||
r="60"
|
||||
id="circle6" />
|
||||
</g>
|
||||
<path
|
||||
style="font-size:85.3333px;font-family:Montserrat;-inkscape-font-specification:Montserrat;fill:#ffffff;stroke-width:5.96887;stroke-opacity:0.3;paint-order:stroke fill markers"
|
||||
d="m 83.213934,327.31768 q -4.966091,0 -9.550176,-1.5917 -4.520416,-1.65536 -7.003461,-4.20207 l 1.846367,-3.62907 q 2.355709,2.29205 6.303115,3.88375 4.011074,1.52802 8.404155,1.52802 4.202076,0 6.812459,-1.01869 2.674047,-1.08235 3.883737,-2.86505 1.273357,-1.78271 1.273357,-3.9474 0,-2.61038 -1.528028,-4.20208 -1.46436,-1.59171 -3.883738,-2.48305 -2.419378,-0.95501 -5.348098,-1.65536 -2.928721,-0.70036 -5.857441,-1.46436 -2.928722,-0.82768 -5.411767,-2.16471 -2.419378,-1.33703 -3.947406,-3.50172 -1.46436,-2.22839 -1.46436,-5.79378 0,-3.31073 1.719032,-6.04844 1.7827,-2.80139 5.411767,-4.45676 3.629065,-1.71903 9.295503,-1.71903 3.756402,0 7.449137,1.08236 3.692735,1.01869 6.366783,2.86505 l -1.591697,3.7564 q -2.865052,-1.91004 -6.112111,-2.80138 -3.183392,-0.89136 -6.175781,-0.89136 -4.011072,0 -6.621454,1.08236 -2.61038,1.08236 -3.883738,2.92872 -1.209688,1.7827 -1.209688,4.07474 0,2.61037 1.46436,4.20209 1.528027,1.59169 3.947406,2.48303 2.483045,0.89136 5.411766,1.59169 2.92872,0.70036 5.793773,1.52804 2.92872,0.82768 5.348098,2.1647 2.483044,1.27337 3.947405,3.43806 1.528024,2.16471 1.528024,5.66644 0,3.24706 -1.782694,6.04845 -1.782701,2.73771 -5.475435,4.45676 -3.629066,1.65535 -9.359171,1.65535 z m 39.537726,-0.382 -19.73704,-44.5675 h 5.09342 l 18.46366,41.95712 h -2.80137 l 18.591,-41.95712 h 4.7751 l -19.73703,44.5675 z m 49.97074,0.382 q -5.09342,0 -9.42284,-1.65535 -4.26574,-1.71905 -7.44915,-4.77511 -3.11972,-3.05605 -4.90241,-7.19446 -1.71903,-4.13842 -1.71903,-9.04083 0,-4.90241 1.71903,-9.04083 1.78269,-4.13841 4.96609,-7.19446 3.1834,-3.05606 7.44914,-4.71143 4.32942,-1.71903 9.42283,-1.71903 5.09343,0 9.29552,1.5917 4.26574,1.59168 7.25813,4.83876 l -2.92872,2.99239 q -2.80139,-2.80139 -6.1758,-4.01108 -3.37438,-1.20969 -7.25812,-1.20969 -4.13843,0 -7.64015,1.40069 -3.43805,1.33703 -6.04843,3.88374 -2.54672,2.48305 -4.01108,5.85744 -1.40069,3.31072 -1.40069,7.3218 0,3.94742 1.40069,7.3218 1.46436,3.37439 4.01108,5.92111 2.61038,2.48305 6.04843,3.88375 3.50172,1.33701 7.57648,1.33701 3.82007,0 7.19446,-1.14602 3.43808,-1.14602 6.30313,-3.88374 l 2.67404,3.56541 q -3.1834,2.80138 -7.44914,4.26574 -4.26574,1.40069 -8.91349,1.40069 z m 11.84221,-6.23945 v -16.4263 h 4.52042 v 16.99932 z"
|
||||
id="text6"
|
||||
aria-label="SVG" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
BIN
loading/runtime_save_load/examples/images/godot_icon.tga
Normal file
|
After Width: | Height: | Size: 384 KiB |
BIN
loading/runtime_save_load/examples/images/godot_icon.webp
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
loading/runtime_save_load/examples/misc/example.zip
Normal file
1
loading/runtime_save_load/examples/misc/file.txt
Normal file
@@ -0,0 +1 @@
|
||||
Plain text file.
|
||||
1
loading/runtime_save_load/icon.svg
Normal file
@@ -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>
|
||||
|
After Width: | Height: | Size: 654 B |
37
loading/runtime_save_load/icon.svg.import
Normal file
@@ -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
loading/runtime_save_load/project.godot
Normal file
@@ -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
loading/runtime_save_load/runtime_save_load.gd
Normal file
@@ -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
loading/runtime_save_load/runtime_save_load.tscn
Normal file
@@ -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
loading/runtime_save_load/screenshots/.gdignore
Normal file
BIN
loading/runtime_save_load/screenshots/runtime_save_load.webp
Normal file
|
After Width: | Height: | Size: 66 KiB |