runtime_save_load.gd 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. extends Control
  2. @onready var file_path_edit := $MarginContainer/VBoxContainer/HBoxContainer/FilePath as LineEdit
  3. @onready var file_dialog := $MarginContainer/VBoxContainer/HBoxContainer/FileDialog as FileDialog
  4. @onready var plain_text_viewer := $MarginContainer/VBoxContainer/Result/PlainTextViewer as ScrollContainer
  5. @onready var plain_text_viewer_label := $MarginContainer/VBoxContainer/Result/PlainTextViewer/Label as Label
  6. @onready var texture_viewer := $MarginContainer/VBoxContainer/Result/TextureViewer as TextureRect
  7. @onready var audio_player := $MarginContainer/VBoxContainer/Result/AudioPlayer as Button
  8. @onready var audio_stream_player := $MarginContainer/VBoxContainer/Result/AudioPlayer/AudioStreamPlayer as AudioStreamPlayer
  9. @onready var scene_viewer := $MarginContainer/VBoxContainer/Result/SceneViewer as SubViewportContainer
  10. @onready var scene_viewer_camera := $MarginContainer/VBoxContainer/Result/SceneViewer/SubViewport/Camera3D as Camera3D
  11. @onready var font_viewer := $MarginContainer/VBoxContainer/Result/FontViewer as Label
  12. @onready var zip_viewer := $MarginContainer/VBoxContainer/Result/ZIPViewer as HSplitContainer
  13. @onready var zip_viewer_file_list := $MarginContainer/VBoxContainer/Result/ZIPViewer/FileList as ItemList
  14. @onready var zip_viewer_file_preview := $MarginContainer/VBoxContainer/Result/ZIPViewer/FilePreview as Label
  15. @onready var error_label := $MarginContainer/VBoxContainer/Result/ErrorLabel as Label
  16. @onready var export_button := $MarginContainer/VBoxContainer/Export as Button
  17. @onready var export_file_dialog := $MarginContainer/VBoxContainer/Export/FileDialog as FileDialog
  18. var zip_reader := ZIPReader.new()
  19. # Keeps reference to the root node imported in the 3D scene viewer,
  20. # so that it can be exported later.
  21. var scene_viewer_root_node: Node
  22. func _on_browse_pressed() -> void:
  23. file_dialog.popup_centered_ratio()
  24. func _on_file_path_text_submitted(new_text: String) -> void:
  25. open_file(new_text)
  26. # Put the caret at the end of the submitted text.
  27. file_path_edit.caret_column = file_path_edit.text.length()
  28. func _on_file_dialog_file_selected(path: String) -> void:
  29. open_file(path)
  30. func reset_visibility() -> void:
  31. plain_text_viewer.visible = false
  32. texture_viewer.visible = false
  33. audio_player.visible = false
  34. scene_viewer.visible = false
  35. var last_child := scene_viewer.get_child(-1)
  36. if last_child is Node3D:
  37. scene_viewer.remove_child(last_child)
  38. last_child.queue_free()
  39. font_viewer.visible = false
  40. zip_viewer.visible = false
  41. zip_viewer_file_list.clear()
  42. error_label.visible = false
  43. export_button.disabled = false
  44. func _on_audio_player_pressed() -> void:
  45. audio_stream_player.play()
  46. func _on_scene_viewer_zoom_value_changed(value: float) -> void:
  47. # Slider uses negative value so that it can be inverted easily
  48. # (lower Camera3D orthogonal size is more zoomed *in*).
  49. scene_viewer_camera.size = abs(value)
  50. func _on_zip_viewer_item_selected(index: int) -> void:
  51. zip_viewer_file_preview.text = zip_reader.read_file(
  52. zip_viewer_file_list.get_item_text(index)
  53. ).get_string_from_utf8()
  54. #region File exporting
  55. func _on_export_pressed() -> void:
  56. export_file_dialog.popup_centered_ratio()
  57. func _on_export_file_dialog_file_selected(path: String) -> void:
  58. if plain_text_viewer.visible:
  59. var file_access := FileAccess.open(path, FileAccess.WRITE)
  60. file_access.store_string(plain_text_viewer_label.text)
  61. file_access.close()
  62. elif texture_viewer.visible:
  63. var image := texture_viewer.texture.get_image()
  64. if path.ends_with(".png"):
  65. image.save_png(path)
  66. if path.ends_with(".jpg") or path.ends_with(".jpeg"):
  67. const JPG_QUALITY = 0.9
  68. image.save_jpg(path, JPG_QUALITY)
  69. if path.ends_with(".webp"):
  70. # Saving WebP is lossless by default, but can be made lossy using
  71. # optional parameters in `Image.save_webp()`.
  72. image.save_webp(path)
  73. elif audio_player.visible:
  74. # Ogg Vorbis audio can't be exported at runtime to a standard format
  75. # (only WAV files can be using `AudioStreamWAV.save_to_wav()`).
  76. pass
  77. elif scene_viewer.visible:
  78. var gltf_document := GLTFDocument.new()
  79. var gltf_state := GLTFState.new()
  80. gltf_document.append_from_scene(scene_viewer_root_node, gltf_state)
  81. # The file extension in the output `path` (`.gltf` or `.glb`) determines
  82. # whether the output uses text or binary format. Binary format is faster
  83. # to write and smaller, but harder to debug. The binary format is also
  84. # more suited to embedding textures.
  85. gltf_document.write_to_filesystem(gltf_state, path)
  86. elif font_viewer.visible:
  87. # Fonts can't be exported at runtime to a standard format
  88. # (only to a Godot-specific `.res` format using the ResourceSaver class).
  89. pass
  90. elif zip_viewer.visible:
  91. var zip_packer := ZIPPacker.new()
  92. var error := zip_packer.open(path)
  93. if error != OK:
  94. push_error("An error occurred while trying to save a ZIP archive to: %s" % path)
  95. return
  96. for file in zip_reader.get_files():
  97. zip_packer.start_file(file)
  98. zip_packer.write_file(zip_reader.read_file(file))
  99. zip_packer.close_file()
  100. zip_packer.close()
  101. #endregion
  102. func show_error(message: String) -> void:
  103. reset_visibility()
  104. error_label.text = "ERROR: %s" % message
  105. error_label.visible = true
  106. func open_file(path: String) -> void:
  107. print_rich("Opening: [u]%s[/u]" % path)
  108. file_path_edit.text = path
  109. var path_lower := path.to_lower()
  110. # Images.
  111. if (
  112. path_lower.ends_with(".jpg")
  113. or path_lower.ends_with(".jpeg")
  114. or path_lower.ends_with(".png")
  115. or path_lower.ends_with(".webp")
  116. or path_lower.ends_with(".svg")
  117. or path_lower.ends_with(".tga")
  118. or path_lower.ends_with(".bmp")
  119. ):
  120. # This method handles everything, from format detection based on
  121. # file extension to reading the file from disk. If you need error handling
  122. # or more control (such as changing the scale SVG is loaded at),
  123. # use the `load_*_from_buffer()` (where `*` is a file extension)
  124. # and `load_svg_from_string()` methods from the Image class.
  125. var image := Image.load_from_file(path)
  126. reset_visibility()
  127. export_file_dialog.filters = ["*.png ; PNG Image", "*.jpg, *.jpeg ; JPEG Image", "*.webp ; WebP Image"]
  128. texture_viewer.visible = true
  129. texture_viewer.texture = ImageTexture.create_from_image(image)
  130. # Audio.
  131. # Run-time MP3 and WAV loading aren't supported by the engine yet.
  132. elif path_lower.ends_with(".ogg"):
  133. # `AudioStreamOggVorbis.load_from_buffer()` can alternatively be used
  134. # if you have Ogg Vorbis data in a PackedByteArray instead of a file.
  135. audio_stream_player.stream = AudioStreamOggVorbis.load_from_file(path)
  136. reset_visibility()
  137. export_button.disabled = true
  138. audio_player.visible = true
  139. # 3D scenes.
  140. elif path_lower.ends_with(".gltf") or path_lower.ends_with(".glb"):
  141. # GLTFState is used by GLTFDocument to store the loaded scene's state.
  142. # GLTFDocument is the class that handles actually loading glTF data into a Godot node tree,
  143. # which means it supports glTF features such as lights and cameras.
  144. var gltf_document := GLTFDocument.new()
  145. var gltf_state := GLTFState.new()
  146. var error := gltf_document.append_from_file(path, gltf_state)
  147. if error == OK:
  148. scene_viewer_root_node = gltf_document.generate_scene(gltf_state)
  149. reset_visibility()
  150. scene_viewer.add_child(scene_viewer_root_node)
  151. export_file_dialog.filters = ["*.gltf ; glTF Text Scene", "*.glb ; glTF Binary Scene"]
  152. scene_viewer.visible = true
  153. else:
  154. show_error('Couldn\'t load "%s" as a glTF scene (error code: %s).' % [path.get_file(), error_string(error)])
  155. # Fonts.
  156. elif (
  157. path_lower.ends_with(".ttf")
  158. or path_lower.ends_with(".otf")
  159. or path_lower.ends_with(".woff")
  160. or path_lower.ends_with(".woff2")
  161. or path_lower.ends_with(".pfb")
  162. or path_lower.ends_with(".pfm")
  163. or path_lower.ends_with(".fnt")
  164. or path_lower.ends_with(".font")
  165. ):
  166. var font_file := FontFile.new()
  167. if path_lower.ends_with(".fnt") or path_lower.ends_with(".font"):
  168. font_file.load_bitmap_font(path)
  169. else:
  170. font_file.load_dynamic_font(path)
  171. if not font_file.data.is_empty():
  172. font_viewer.add_theme_font_override("font", font_file)
  173. reset_visibility()
  174. font_viewer.visible = true
  175. export_button.disabled = true
  176. else:
  177. show_error('Couldn\'t load "%s" as a font.' % path.get_file())
  178. # ZIP archives.
  179. elif path_lower.ends_with(".zip"):
  180. # This supports any ZIP file, including files generated by Godot's "Export PCK/ZIP" functionality
  181. # (although these will contain imported Godot resources rather than the original project files).
  182. #
  183. # Use `ProjectSettings.load_resource_pack()` to load PCK or ZIP files exported by Godot as
  184. # additional data packs. That approach is preferred for DLCs, as it makes interacting with
  185. # additional data packs seamless (virtual filesystem).
  186. zip_reader.open(path)
  187. var files := zip_reader.get_files()
  188. files.sort()
  189. export_file_dialog.filters = ["*.zip ; ZIP Archive"]
  190. reset_visibility()
  191. for file in files:
  192. zip_viewer_file_list.add_item(file, null)
  193. # Make folders disabled in the list.
  194. zip_viewer_file_list.set_item_disabled(-1, file.ends_with("/"))
  195. zip_viewer.visible = true
  196. # Fallback.
  197. else:
  198. # Open as plain text and display contents if possible.
  199. var file_contents := FileAccess.get_file_as_string(path)
  200. if file_contents.is_empty():
  201. show_error("File is empty or is a binary file.")
  202. else:
  203. plain_text_viewer_label.text = file_contents
  204. reset_visibility()
  205. plain_text_viewer.visible = true