First work on dialogic, resized guild, and started implementing portraits.
This commit is contained in:
65
addons/dialogic/Modules/Save/event_save.gd
Normal file
65
addons/dialogic/Modules/Save/event_save.gd
Normal file
@@ -0,0 +1,65 @@
|
||||
@tool
|
||||
class_name DialogicSaveEvent
|
||||
extends DialogicEvent
|
||||
|
||||
## Event that allows saving to a specific slot.
|
||||
|
||||
|
||||
### Settings
|
||||
|
||||
## The name of the slot to save to. Learn more in the saving subsystem.
|
||||
## If empty, the event will attempt to save to the latest slot, and otherwise use the default.
|
||||
var slot_name := ""
|
||||
|
||||
|
||||
################################################################################
|
||||
## INITIALIZE
|
||||
################################################################################
|
||||
|
||||
func _execute() -> void:
|
||||
if slot_name.is_empty():
|
||||
if dialogic.Save.get_latest_slot():
|
||||
dialogic.Save.save(dialogic.Save.get_latest_slot())
|
||||
else:
|
||||
dialogic.Save.save()
|
||||
else:
|
||||
dialogic.Save.save(slot_name)
|
||||
finish()
|
||||
|
||||
|
||||
################################################################################
|
||||
## INITIALIZE
|
||||
################################################################################
|
||||
|
||||
func _init() -> void:
|
||||
event_name = "Save"
|
||||
set_default_color('Color6')
|
||||
event_category = "Other"
|
||||
event_sorting_index = 0
|
||||
|
||||
|
||||
func _get_icon() -> Resource:
|
||||
return load(self.get_script().get_path().get_base_dir().path_join('icon.svg'))
|
||||
|
||||
|
||||
################################################################################
|
||||
## SAVING/LOADING
|
||||
################################################################################
|
||||
|
||||
func get_shortcode() -> String:
|
||||
return "save"
|
||||
|
||||
|
||||
func get_shortcode_parameters() -> Dictionary:
|
||||
return {
|
||||
#param_name : property_info
|
||||
"slot" : {"property": "slot_name", "default": "Default"},
|
||||
}
|
||||
|
||||
|
||||
################################################################################
|
||||
## EDITOR REPRESENTATION
|
||||
################################################################################
|
||||
|
||||
func build_event_editor() -> void:
|
||||
add_header_edit('slot_name', ValueType.SINGLELINE_TEXT, {'left_text':'Save to slot'})
|
||||
1
addons/dialogic/Modules/Save/event_save.gd.uid
Normal file
1
addons/dialogic/Modules/Save/event_save.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://h8vt17gfxwas
|
||||
8
addons/dialogic/Modules/Save/icon.svg
Normal file
8
addons/dialogic/Modules/Save/icon.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg width="63.999996" height="63.999996" viewBox="0 0 16.933332 16.933332" version="1.1" id="svg5" inkscape:export-filename="save-icon.svg" inkscape:export-xdpi="96" inkscape:export-ydpi="96" sodipodi:docname="icon.svg" inkscape:version="1.2.2 (732a01da63, 2022-12-09)" 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">
|
||||
<sodipodi:namedview id="namedview7" pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:document-units="mm" showgrid="false" showguides="true" inkscape:zoom="2.3119079" inkscape:cx="-62.069948" inkscape:cy="-17.301728" inkscape:window-width="1920" inkscape:window-height="1017" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="svg5" />
|
||||
<defs id="defs2" />
|
||||
<path id="path19705" style="stroke-width:0.703602;stroke-dasharray:none;fill:#ffffff;stroke:#ffffff;stroke-linecap:round;stroke-linejoin:round" d="M 3.1598245,1.4940293 V 13.603677 H 14.561748 V 4.1279555 l -2.75211,-2.6339262 z m 1.1818312,0.81583 H 10.936549 V 5.5452368 H 4.3416557 Z M 8.861015,7.5286978 A 2.4575739,2.4575739 0 0 1 11.318583,9.9862656 2.4575739,2.4575739 0 0 1 8.861015,12.443833 2.4575739,2.4575739 0 0 1 6.4034473,9.9862656 2.4575739,2.4575739 0 0 1 8.861015,7.5286978 Z" transform="matrix(0.82460301,0,0,0.82460301,1.1600358,2.2418596)" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
44
addons/dialogic/Modules/Save/icon.svg.import
Normal file
44
addons/dialogic/Modules/Save/icon.svg.import
Normal file
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://d3scfgnvgqvir"
|
||||
path="res://.godot/imported/icon.svg-aad0408eebe298e9d92c76d6f2c79cf6.ctex"
|
||||
metadata={
|
||||
"has_editor_variant": true,
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/dialogic/Modules/Save/icon.svg"
|
||||
dest_files=["res://.godot/imported/icon.svg-aad0408eebe298e9d92c76d6f2c79cf6.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
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/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
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=true
|
||||
editor/convert_colors_with_editor_theme=true
|
||||
14
addons/dialogic/Modules/Save/index.gd
Normal file
14
addons/dialogic/Modules/Save/index.gd
Normal file
@@ -0,0 +1,14 @@
|
||||
@tool
|
||||
extends DialogicIndexer
|
||||
|
||||
|
||||
func _get_events() -> Array:
|
||||
return [this_folder.path_join('event_save.gd')]
|
||||
|
||||
|
||||
func _get_subsystems() -> Array:
|
||||
return [{'name':'Save', 'script':this_folder.path_join('subsystem_save.gd')}]
|
||||
|
||||
|
||||
func _get_settings_pages() -> Array:
|
||||
return [this_folder.path_join('settings_save.tscn')]
|
||||
1
addons/dialogic/Modules/Save/index.gd.uid
Normal file
1
addons/dialogic/Modules/Save/index.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://byhm3myhwnykr
|
||||
57
addons/dialogic/Modules/Save/settings_save.gd
Normal file
57
addons/dialogic/Modules/Save/settings_save.gd
Normal file
@@ -0,0 +1,57 @@
|
||||
@tool
|
||||
extends DialogicSettingsPage
|
||||
|
||||
## Settings page that contains settings for the saving subsystem
|
||||
|
||||
|
||||
func _get_priority() -> int:
|
||||
return 0
|
||||
|
||||
|
||||
func _refresh() -> void:
|
||||
%Autosave.button_pressed = ProjectSettings.get_setting('dialogic/save/autosave', false)
|
||||
%AutosaveMode.select(ProjectSettings.get_setting('dialogic/save/autosave_mode', 0))
|
||||
%AutosaveDelay.value = ProjectSettings.get_setting('dialogic/save/autosave_delay', 60)
|
||||
|
||||
%AutosaveModeLabel.visible = %Autosave.button_pressed
|
||||
%AutosaveModeContent.visible = %Autosave.button_pressed
|
||||
%AutosaveDelay.visible = %AutosaveMode.selected == 1
|
||||
|
||||
%DefaultSaveSlotName.text = ProjectSettings.get_setting('dialogic/save/default_slot', 'Default')
|
||||
|
||||
%EncryptionPassword.text = ProjectSettings.get_setting('dialogic/save/encryption_password', "")
|
||||
%EncryptionOnExportsSection.visible = !%EncryptionPassword.text.is_empty()
|
||||
%EncryptionOnExports.button_pressed = ProjectSettings.get_setting('dialogic/save/encryption_on_exports_only', true)
|
||||
|
||||
func _on_autosave_toggled(button_pressed:bool) -> void:
|
||||
ProjectSettings.set_setting('dialogic/save/autosave', button_pressed)
|
||||
ProjectSettings.save()
|
||||
%AutosaveModeLabel.visible = button_pressed
|
||||
%AutosaveModeContent.visible = button_pressed
|
||||
|
||||
|
||||
func _on_autosave_mode_item_selected(index:int):
|
||||
ProjectSettings.set_setting('dialogic/save/autosave_mode', index)
|
||||
ProjectSettings.save()
|
||||
%AutosaveDelay.visible = %AutosaveMode.selected == 1
|
||||
|
||||
|
||||
func _on_autosave_delay_value_changed(value:float):
|
||||
ProjectSettings.set_setting('dialogic/save/autosave_delay', value)
|
||||
ProjectSettings.save()
|
||||
|
||||
|
||||
func _on_default_save_slot_name_text_changed(new_text:String):
|
||||
ProjectSettings.set_setting('dialogic/save/default_slot', new_text)
|
||||
ProjectSettings.save()
|
||||
|
||||
|
||||
func _on_encryption_password_text_changed(new_text: String) -> void:
|
||||
ProjectSettings.set_setting('dialogic/save/encryption_password', new_text)
|
||||
ProjectSettings.save()
|
||||
%EncryptionOnExportsSection.visible = !new_text.is_empty()
|
||||
|
||||
|
||||
func _on_encryption_on_exports_toggled(toggled_on:bool) -> void:
|
||||
ProjectSettings.set_setting('dialogic/save/encryption_on_exports_only', toggled_on)
|
||||
ProjectSettings.save()
|
||||
1
addons/dialogic/Modules/Save/settings_save.gd.uid
Normal file
1
addons/dialogic/Modules/Save/settings_save.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cnqfcutyiwjns
|
||||
143
addons/dialogic/Modules/Save/settings_save.tscn
Normal file
143
addons/dialogic/Modules/Save/settings_save.tscn
Normal file
@@ -0,0 +1,143 @@
|
||||
[gd_scene load_steps=5 format=3 uid="uid://cd340w7blofak"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/dialogic/Modules/Save/settings_save.gd" id="2"]
|
||||
[ext_resource type="PackedScene" uid="uid://dbpkta2tjsqim" path="res://addons/dialogic/Editor/Common/hint_tooltip_icon.tscn" id="2_v2wt8"]
|
||||
|
||||
[sub_resource type="Image" id="Image_oatpr"]
|
||||
data = {
|
||||
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
|
||||
"format": "RGBA8",
|
||||
"height": 16,
|
||||
"mipmaps": false,
|
||||
"width": 16
|
||||
}
|
||||
|
||||
[sub_resource type="ImageTexture" id="ImageTexture_dbvsu"]
|
||||
image = SubResource("Image_oatpr")
|
||||
|
||||
[node name="Saving" type="VBoxContainer"]
|
||||
offset_right = 1084.0
|
||||
offset_bottom = 212.0
|
||||
script = ExtResource("2")
|
||||
|
||||
[node name="Grid" type="GridContainer" parent="."]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
columns = 2
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="Grid"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="Grid/HBoxContainer"]
|
||||
layout_mode = 2
|
||||
text = "Autosave"
|
||||
|
||||
[node name="HintTooltip" parent="Grid/HBoxContainer" instance=ExtResource("2_v2wt8")]
|
||||
layout_mode = 2
|
||||
tooltip_text = "If enabled dialogic will autosave the full state to the current slot depending on the autosave method."
|
||||
texture = SubResource("ImageTexture_dbvsu")
|
||||
hint_text = "If enabled dialogic will autosave the full state to the current slot depending on the autosave method."
|
||||
|
||||
[node name="Autosave" type="CheckBox" parent="Grid"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
|
||||
[node name="AutosaveModeLabel" type="HBoxContainer" parent="Grid"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label2" type="Label" parent="Grid/AutosaveModeLabel"]
|
||||
layout_mode = 2
|
||||
text = "Autosave Mode"
|
||||
|
||||
[node name="AutosaveModeContent" type="HBoxContainer" parent="Grid"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
|
||||
[node name="AutosaveMode" type="OptionButton" parent="Grid/AutosaveModeContent"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
item_count = 3
|
||||
selected = 0
|
||||
popup/item_0/text = "Timeline Start+End+Jump"
|
||||
popup/item_0/id = 0
|
||||
popup/item_1/text = "Each X seconds"
|
||||
popup/item_1/id = 1
|
||||
popup/item_2/text = "Every Text Event"
|
||||
popup/item_2/id = 2
|
||||
|
||||
[node name="AutosaveDelay" type="SpinBox" parent="Grid/AutosaveModeContent"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
max_value = 1000.0
|
||||
suffix = "s"
|
||||
|
||||
[node name="HBoxContainer2" type="HBoxContainer" parent="Grid"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label4" type="Label" parent="Grid/HBoxContainer2"]
|
||||
layout_mode = 2
|
||||
text = "Default slot name"
|
||||
|
||||
[node name="HintTooltip3" parent="Grid/HBoxContainer2" instance=ExtResource("2_v2wt8")]
|
||||
layout_mode = 2
|
||||
tooltip_text = "The name of the default slot. "
|
||||
texture = SubResource("ImageTexture_dbvsu")
|
||||
hint_text = "The name of the default slot. "
|
||||
|
||||
[node name="DefaultSaveSlotName" type="LineEdit" parent="Grid"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 0
|
||||
expand_to_text_length = true
|
||||
|
||||
[node name="HBoxContainer3" type="HBoxContainer" parent="Grid"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="EncryptionPasswordLabel" type="Label" parent="Grid/HBoxContainer3"]
|
||||
layout_mode = 2
|
||||
text = "Encryption Password"
|
||||
|
||||
[node name="HintTooltip" parent="Grid/HBoxContainer3" instance=ExtResource("2_v2wt8")]
|
||||
layout_mode = 2
|
||||
tooltip_text = "The encryption password used to encrypt save files. When left empty, the save files will not be encrypted."
|
||||
texture = SubResource("ImageTexture_dbvsu")
|
||||
hint_text = "The encryption password used to encrypt save files. When left empty, the save files will not be encrypted."
|
||||
|
||||
[node name="HBoxContainer4" type="HBoxContainer" parent="Grid"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="EncryptionPassword" type="LineEdit" parent="Grid/HBoxContainer4"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
expand_to_text_length = true
|
||||
|
||||
[node name="EncryptionOnExportsSection" type="HBoxContainer" parent="Grid/HBoxContainer4"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
|
||||
[node name="EncryptionPasswordLabel" type="Label" parent="Grid/HBoxContainer4/EncryptionOnExportsSection"]
|
||||
layout_mode = 2
|
||||
text = "Use on exports only"
|
||||
|
||||
[node name="HintTooltip" parent="Grid/HBoxContainer4/EncryptionOnExportsSection" instance=ExtResource("2_v2wt8")]
|
||||
layout_mode = 2
|
||||
tooltip_text = "For easier debugging dialogic will only encrypt saves made by exported project.
|
||||
Exported projects with debug mode on or saves made when running in editor
|
||||
will not use encryption."
|
||||
texture = SubResource("ImageTexture_dbvsu")
|
||||
hint_text = "For easier debugging dialogic will only encrypt saves made by exported project.
|
||||
Exported projects with debug mode on or saves made when running in editor
|
||||
will not use encryption."
|
||||
|
||||
[node name="EncryptionOnExports" type="CheckBox" parent="Grid/HBoxContainer4/EncryptionOnExportsSection"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
|
||||
[connection signal="toggled" from="Grid/Autosave" to="." method="_on_autosave_toggled"]
|
||||
[connection signal="item_selected" from="Grid/AutosaveModeContent/AutosaveMode" to="." method="_on_autosave_mode_item_selected"]
|
||||
[connection signal="value_changed" from="Grid/AutosaveModeContent/AutosaveDelay" to="." method="_on_autosave_delay_value_changed"]
|
||||
[connection signal="text_changed" from="Grid/DefaultSaveSlotName" to="." method="_on_default_save_slot_name_text_changed"]
|
||||
[connection signal="text_changed" from="Grid/HBoxContainer4/EncryptionPassword" to="." method="_on_encryption_password_text_changed"]
|
||||
[connection signal="toggled" from="Grid/HBoxContainer4/EncryptionOnExportsSection/EncryptionOnExports" to="." method="_on_encryption_on_exports_toggled"]
|
||||
519
addons/dialogic/Modules/Save/subsystem_save.gd
Normal file
519
addons/dialogic/Modules/Save/subsystem_save.gd
Normal file
@@ -0,0 +1,519 @@
|
||||
extends DialogicSubsystem
|
||||
## Subsystem to save and load game states.
|
||||
##
|
||||
## This subsystem has many different helper methods to save Dialogic or custom
|
||||
## game data to named save slots.
|
||||
##
|
||||
## You can listen to saves via [signal saved]. \
|
||||
## If you want to save, you can call [method save]. \
|
||||
|
||||
|
||||
## Emitted when a save happened with the following info:
|
||||
## [br]
|
||||
## Key | Value Type | Value [br]
|
||||
## ----------- | ------------- | ----- [br]
|
||||
## `slot_name` | [type String] | The name of the slot that the game state was saved to. [br]
|
||||
## `is_autosave` | [type bool] | `true`, if the save was an autosave. [br]
|
||||
signal saved(info: Dictionary)
|
||||
|
||||
|
||||
## The directory that will be saved to.
|
||||
const SAVE_SLOTS_DIR := "user://dialogic/saves/"
|
||||
|
||||
## The project settings key for the auto-save enabled settings.
|
||||
const AUTO_SAVE_SETTINGS := "dialogic/save/autosave"
|
||||
|
||||
## The project settings key for the auto-save mode settings.
|
||||
const AUTO_SAVE_MODE_SETTINGS := "dialogic/save/autosave_mode"
|
||||
|
||||
## The project settings key for the auto-save delay settings.
|
||||
const AUTO_SAVE_TIME_SETTINGS := "dialogic/save/autosave_delay"
|
||||
|
||||
## Temporarily stores a taken screen capture when using [take_slot_image()].
|
||||
enum ThumbnailMode {NONE, TAKE_AND_STORE, STORE_ONLY}
|
||||
var latest_thumbnail: Image = null
|
||||
|
||||
|
||||
## The different types of auto-save triggers.
|
||||
## If one of these occurs in the game, an auto-save may happen
|
||||
## if [member autosave_enabled] is `true`.
|
||||
enum AutoSaveMode {
|
||||
## Includes timeline start, end, and jump events.
|
||||
ON_TIMELINE_JUMPS = 0,
|
||||
## Saves after a certain time interval.
|
||||
ON_TIMER = 1,
|
||||
## Saves after every text event.
|
||||
ON_TEXT_EVENT = 2
|
||||
}
|
||||
|
||||
## Whether the auto-save feature is enabled.
|
||||
## The initial value can be set in the project settings via th Dialogic editor.
|
||||
##
|
||||
## This can be toggled during the game.
|
||||
var autosave_enabled := false:
|
||||
set(enabled):
|
||||
autosave_enabled = enabled
|
||||
|
||||
if enabled:
|
||||
autosave_timer.start()
|
||||
else:
|
||||
autosave_timer.stop()
|
||||
|
||||
|
||||
## Under what conditions the auto-save feature will trigger if
|
||||
## [member autosave_enabled] is `true`.
|
||||
var autosave_mode := AutoSaveMode.ON_TIMELINE_JUMPS
|
||||
|
||||
## After what time interval the auto-save feature will trigger if
|
||||
## [member autosave_enabled] is `true` and [member autosave_mode] is
|
||||
## `AutoSaveMode.ON_TIMER`.
|
||||
var autosave_time := 60:
|
||||
set(timer_time):
|
||||
autosave_time = timer_time
|
||||
autosave_timer.wait_time = timer_time
|
||||
|
||||
|
||||
#region STATE
|
||||
####################################################################################################
|
||||
|
||||
## Built-in, called by DialogicGameHandler.
|
||||
func clear_game_state(_clear_flag := DialogicGameHandler.ClearFlags.FULL_CLEAR) -> void:
|
||||
_make_sure_slot_dir_exists()
|
||||
|
||||
|
||||
## Built-in, called by DialogicGameHandler.
|
||||
func pause() -> void:
|
||||
autosave_timer.paused = true
|
||||
|
||||
|
||||
## Built-in, called by DialogicGameHandler.
|
||||
func resume() -> void:
|
||||
autosave_timer.paused = false
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region MAIN METHODS
|
||||
####################################################################################################
|
||||
|
||||
## Saves the current state to the given slot.
|
||||
## If no slot is given, the default slot is used. You can change this name in
|
||||
## the Dialogic editor.
|
||||
## If you want to save to the last used slot, you can get its slot name with the
|
||||
## [method get_latest_slot()] method.
|
||||
func save(slot_name := "", is_autosave := false, thumbnail_mode := ThumbnailMode.TAKE_AND_STORE, slot_info := {}) -> Error:
|
||||
# check if to save (if this is an autosave)
|
||||
if is_autosave and !autosave_enabled:
|
||||
return OK
|
||||
|
||||
if slot_name.is_empty():
|
||||
slot_name = get_default_slot()
|
||||
|
||||
set_latest_slot(slot_name)
|
||||
|
||||
var save_error := save_file(slot_name, 'state.txt', dialogic.get_full_state())
|
||||
|
||||
if save_error:
|
||||
return save_error
|
||||
|
||||
if thumbnail_mode == ThumbnailMode.TAKE_AND_STORE:
|
||||
take_thumbnail()
|
||||
save_slot_thumbnail(slot_name)
|
||||
elif thumbnail_mode == ThumbnailMode.STORE_ONLY:
|
||||
save_slot_thumbnail(slot_name)
|
||||
|
||||
if slot_info:
|
||||
set_slot_info(slot_name, slot_info)
|
||||
|
||||
saved.emit({"slot_name": slot_name, "is_autosave": is_autosave})
|
||||
print('[Dialogic] Saved to slot "'+slot_name+'".')
|
||||
return OK
|
||||
|
||||
|
||||
## Loads all info from the given slot in the DialogicGameHandler (Dialogic Autoload).
|
||||
## If no slot is given, the default slot is used.
|
||||
## To check if something is saved in that slot use has_slot().
|
||||
## If the slot does not exist, this method will fail.
|
||||
func load(slot_name := "") -> Error:
|
||||
if slot_name.is_empty(): slot_name = get_default_slot()
|
||||
|
||||
if !has_slot(slot_name):
|
||||
printerr("[Dialogic Error] Tried loading from invalid save slot '"+slot_name+"'.")
|
||||
return ERR_FILE_NOT_FOUND
|
||||
|
||||
var set_latest_error := set_latest_slot(slot_name)
|
||||
if set_latest_error:
|
||||
push_error("[Dialogic Error]: Failed to store latest slot to global info. Error %d '%s'" % [set_latest_error, error_string(set_latest_error)])
|
||||
|
||||
var state: Dictionary = load_file(slot_name, 'state.txt', {})
|
||||
dialogic.load_full_state(state)
|
||||
|
||||
if state.is_empty():
|
||||
return FAILED
|
||||
else:
|
||||
return OK
|
||||
|
||||
|
||||
## Saves a variable to a file in the given slot.
|
||||
##
|
||||
## Be aware, the [param slot_name] will be used as a filesystem folder name.
|
||||
## Some operating systems do not support every character in folder names.
|
||||
## It is recommended to use only letters, numbers, and underscores.
|
||||
##
|
||||
## This method allows you to build your own save and load system.
|
||||
## You may be looking for the simple [method save] method to save the game state.
|
||||
func save_file(slot_name: String, file_name: String, data: Variant) -> Error:
|
||||
if slot_name.is_empty():
|
||||
slot_name = get_default_slot()
|
||||
|
||||
if slot_name.is_empty():
|
||||
push_error("[Dialogic Error]: No fallback slot name set.")
|
||||
return ERR_FILE_NOT_FOUND
|
||||
|
||||
if !has_slot(slot_name):
|
||||
add_empty_slot(slot_name)
|
||||
|
||||
var encryption_password := get_encryption_password()
|
||||
var file: FileAccess
|
||||
|
||||
if encryption_password.is_empty():
|
||||
file = FileAccess.open(SAVE_SLOTS_DIR.path_join(slot_name).path_join(file_name), FileAccess.WRITE)
|
||||
else:
|
||||
file = FileAccess.open_encrypted_with_pass(SAVE_SLOTS_DIR.path_join(slot_name).path_join(file_name), FileAccess.WRITE, encryption_password)
|
||||
|
||||
if file:
|
||||
file.store_var(data)
|
||||
return OK
|
||||
else:
|
||||
var error := FileAccess.get_open_error()
|
||||
push_error("[Dialogic Error]: Could not save slot to file. Error: %d '%s'" % [error, error_string(error)])
|
||||
return error
|
||||
|
||||
|
||||
## Loads a file using [param slot_name] and returns the contained info.
|
||||
##
|
||||
## This method allows you to build your own save and load system.
|
||||
## You may be looking for the simple [method load] method to load the game state.
|
||||
func load_file(slot_name: String, file_name: String, default: Variant) -> Variant:
|
||||
if slot_name.is_empty(): slot_name = get_default_slot()
|
||||
|
||||
var path := get_slot_path(slot_name).path_join(file_name)
|
||||
if FileAccess.file_exists(path):
|
||||
var encryption_password := get_encryption_password()
|
||||
var file: FileAccess
|
||||
|
||||
if encryption_password.is_empty():
|
||||
file = FileAccess.open(path, FileAccess.READ)
|
||||
else:
|
||||
file = FileAccess.open_encrypted_with_pass(path, FileAccess.READ, encryption_password)
|
||||
|
||||
if file:
|
||||
return file.get_var()
|
||||
else:
|
||||
push_error(FileAccess.get_open_error())
|
||||
return default
|
||||
|
||||
|
||||
## Data set in global info can be accessed unrelated to the save slots.
|
||||
## For instance, you may want to store game settings in here, as they
|
||||
## affect the game globally unrelated to the slot used.
|
||||
func set_global_info(key: String, value: Variant) -> Error:
|
||||
var global_info := ConfigFile.new()
|
||||
var encryption_password := get_encryption_password()
|
||||
|
||||
if encryption_password.is_empty():
|
||||
var load_error := global_info.load(SAVE_SLOTS_DIR.path_join('global_info.txt'))
|
||||
if load_error:
|
||||
printerr("[Dialogic Error]: Couldn't access global saved info file.")
|
||||
return load_error
|
||||
|
||||
else:
|
||||
global_info.set_value('main', key, value)
|
||||
return global_info.save(SAVE_SLOTS_DIR.path_join('global_info.txt'))
|
||||
|
||||
else:
|
||||
var load_error := global_info.load_encrypted_pass(SAVE_SLOTS_DIR.path_join('global_info.txt'), encryption_password)
|
||||
if load_error:
|
||||
printerr("[Dialogic Error]: Couldn't access global saved info file.")
|
||||
return load_error
|
||||
|
||||
else:
|
||||
global_info.set_value('main', key, value)
|
||||
return global_info.save_encrypted_pass(SAVE_SLOTS_DIR.path_join('global_info.txt'), encryption_password)
|
||||
|
||||
|
||||
## Access the data unrelated to a save slot.
|
||||
## First, the data must have been set with [method set_global_info].
|
||||
func get_global_info(key: String, default: Variant) -> Variant:
|
||||
var global_info := ConfigFile.new()
|
||||
var encryption_password := get_encryption_password()
|
||||
|
||||
if encryption_password.is_empty():
|
||||
|
||||
if global_info.load(SAVE_SLOTS_DIR.path_join('global_info.txt')) == OK:
|
||||
return global_info.get_value('main', key, default)
|
||||
|
||||
printerr("[Dialogic Error]: Couldn't access global saved info file.")
|
||||
|
||||
elif global_info.load_encrypted_pass(SAVE_SLOTS_DIR.path_join('global_info.txt'), encryption_password) == OK:
|
||||
return global_info.get_value('main', key, default)
|
||||
|
||||
return default
|
||||
|
||||
|
||||
## Gets the encryption password from the project settings if it has been set.
|
||||
## If no password has been set, an empty string is returned.
|
||||
func get_encryption_password() -> String:
|
||||
if OS.is_debug_build() and ProjectSettings.get_setting('dialogic/save/encryption_on_exports_only', true):
|
||||
return ""
|
||||
return ProjectSettings.get_setting("dialogic/save/encryption_password", "")
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region SLOT HELPERS
|
||||
####################################################################################################
|
||||
## Returns a list of all available slots. Useful for iterating over all slots,
|
||||
## e.g., when building a UI with all save slots.
|
||||
func get_slot_names() -> Array[String]:
|
||||
var save_folders: Array[String] = []
|
||||
|
||||
if DirAccess.dir_exists_absolute(SAVE_SLOTS_DIR):
|
||||
var directory := DirAccess.open(SAVE_SLOTS_DIR)
|
||||
var _list_dir := directory.list_dir_begin()
|
||||
var file_name := directory.get_next()
|
||||
|
||||
while not file_name.is_empty():
|
||||
|
||||
if directory.current_is_dir() and not file_name.begins_with("."):
|
||||
save_folders.append(file_name)
|
||||
|
||||
file_name = directory.get_next()
|
||||
|
||||
return save_folders
|
||||
|
||||
return []
|
||||
|
||||
|
||||
## Returns true if the given slot exists.
|
||||
func has_slot(slot_name: String) -> bool:
|
||||
if slot_name.is_empty():
|
||||
slot_name = get_default_slot()
|
||||
|
||||
return slot_name in get_slot_names()
|
||||
|
||||
|
||||
## Removes all the given slot along with all it's info/files.
|
||||
func delete_slot(slot_name: String) -> Error:
|
||||
var path := SAVE_SLOTS_DIR.path_join(slot_name)
|
||||
|
||||
if DirAccess.dir_exists_absolute(path):
|
||||
var directory := DirAccess.open(path)
|
||||
if not directory:
|
||||
return DirAccess.get_open_error()
|
||||
var _list_dir := directory.list_dir_begin()
|
||||
var file_name := directory.get_next()
|
||||
|
||||
while not file_name.is_empty():
|
||||
var remove_error := directory.remove(file_name)
|
||||
if remove_error:
|
||||
push_warning("[Dialogic Error]: Encountered error while removing '%s': %d\t%s" % [path.path_join(file_name), remove_error, error_string(remove_error)])
|
||||
file_name = directory.get_next()
|
||||
|
||||
# Delete the folder.
|
||||
return directory.remove(SAVE_SLOTS_DIR.path_join(slot_name))
|
||||
|
||||
push_warning("[Dialogic Warning]: Save slot '%s' has already been deleted." % path)
|
||||
return OK
|
||||
|
||||
|
||||
## This adds a new save folder with the given name
|
||||
func add_empty_slot(slot_name: String) -> Error:
|
||||
if DirAccess.dir_exists_absolute(SAVE_SLOTS_DIR):
|
||||
var directory := DirAccess.open(SAVE_SLOTS_DIR)
|
||||
if directory:
|
||||
return directory.make_dir(slot_name)
|
||||
return DirAccess.get_open_error()
|
||||
|
||||
push_error("[Dialogic Error]: Path to '%s' does not exist." % SAVE_SLOTS_DIR)
|
||||
return ERR_FILE_BAD_PATH
|
||||
|
||||
|
||||
## Reset the state of the given save folder (or default)
|
||||
func reset_slot(slot_name := "") -> Error:
|
||||
if slot_name.is_empty():
|
||||
slot_name = get_default_slot()
|
||||
|
||||
return save_file(slot_name, 'state.txt', {})
|
||||
|
||||
|
||||
## Returns the full path to the given slot folder
|
||||
func get_slot_path(slot_name: String) -> String:
|
||||
return SAVE_SLOTS_DIR.path_join(slot_name)
|
||||
|
||||
|
||||
## Returns the default slot name defined in the dialogic settings
|
||||
func get_default_slot() -> String:
|
||||
return ProjectSettings.get_setting('dialogic/save/default_slot', 'Default')
|
||||
|
||||
|
||||
## Returns the latest slot or empty if nothing was saved yet
|
||||
func get_latest_slot() -> String:
|
||||
var latest_slot := ""
|
||||
|
||||
if Engine.get_main_loop().has_meta('dialogic_latest_saved_slot'):
|
||||
latest_slot = Engine.get_main_loop().get_meta('dialogic_latest_saved_slot', '')
|
||||
|
||||
else:
|
||||
latest_slot = get_global_info('latest_save_slot', '')
|
||||
Engine.get_main_loop().set_meta('dialogic_latest_saved_slot', latest_slot)
|
||||
|
||||
|
||||
if !has_slot(latest_slot):
|
||||
return ''
|
||||
|
||||
return latest_slot
|
||||
|
||||
|
||||
func set_latest_slot(slot_name:String) -> Error:
|
||||
Engine.get_main_loop().set_meta('dialogic_latest_saved_slot', slot_name)
|
||||
return set_global_info('latest_save_slot', slot_name)
|
||||
|
||||
|
||||
func _make_sure_slot_dir_exists() -> Error:
|
||||
if not DirAccess.dir_exists_absolute(SAVE_SLOTS_DIR):
|
||||
var make_dir_result := DirAccess.make_dir_recursive_absolute(SAVE_SLOTS_DIR)
|
||||
if make_dir_result:
|
||||
return make_dir_result
|
||||
|
||||
var global_info_path := SAVE_SLOTS_DIR.path_join('global_info.txt')
|
||||
|
||||
if not FileAccess.file_exists(global_info_path):
|
||||
var config := ConfigFile.new()
|
||||
var password := get_encryption_password()
|
||||
|
||||
if password.is_empty():
|
||||
return config.save(global_info_path)
|
||||
|
||||
else:
|
||||
return config.save_encrypted_pass(global_info_path, password)
|
||||
|
||||
return OK
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region SLOT INFO
|
||||
####################################################################################################
|
||||
|
||||
func set_slot_info(slot_name:String, info: Dictionary) -> Error:
|
||||
if slot_name.is_empty():
|
||||
slot_name = get_default_slot()
|
||||
|
||||
return save_file(slot_name, 'info.txt', info)
|
||||
|
||||
|
||||
func get_slot_info(slot_name := "") -> Dictionary:
|
||||
if slot_name.is_empty():
|
||||
slot_name = get_default_slot()
|
||||
|
||||
return load_file(slot_name, 'info.txt', {})
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region SLOT IMAGE
|
||||
####################################################################################################
|
||||
|
||||
## This method creates a thumbnail of the current game view, it allows to
|
||||
## save the game without having the UI on the save slot image.
|
||||
## The thumbnail will be stored in [member latest_thumbnail].
|
||||
##
|
||||
## Call this method before opening your save & load menu.
|
||||
## After that, call [method save] with [constant ThumbnailMode.STORE_ONLY].
|
||||
## The [method save] will automatically use the stored thumbnail.
|
||||
func take_thumbnail() -> void:
|
||||
latest_thumbnail = get_viewport().get_texture().get_image()
|
||||
|
||||
|
||||
## No need to call from outside.
|
||||
## Used to store the latest thumbnail to the given slot.
|
||||
func save_slot_thumbnail(slot_name: String) -> Error:
|
||||
if latest_thumbnail:
|
||||
var path := get_slot_path(slot_name).path_join('thumbnail.png')
|
||||
return latest_thumbnail.save_png(path)
|
||||
|
||||
push_warning("[Dialogic Warning]: No thumbnail has been set yet.")
|
||||
return OK
|
||||
|
||||
|
||||
## Returns the thumbnail of the given slot.
|
||||
func get_slot_thumbnail(slot_name: String) -> ImageTexture:
|
||||
if slot_name.is_empty():
|
||||
slot_name = get_default_slot()
|
||||
|
||||
var path := get_slot_path(slot_name).path_join('thumbnail.png')
|
||||
|
||||
if FileAccess.file_exists(path):
|
||||
return ImageTexture.create_from_image(Image.load_from_file(path))
|
||||
|
||||
return null
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region AUTOSAVE
|
||||
####################################################################################################
|
||||
## Reference to the autosave timer.
|
||||
var autosave_timer := Timer.new()
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
autosave_timer.one_shot = true
|
||||
DialogicUtil.update_timer_process_callback(autosave_timer)
|
||||
autosave_timer.name = "AutosaveTimer"
|
||||
var _result := autosave_timer.timeout.connect(_on_autosave_timer_timeout)
|
||||
add_child(autosave_timer)
|
||||
|
||||
autosave_enabled = ProjectSettings.get_setting(AUTO_SAVE_SETTINGS, autosave_enabled)
|
||||
autosave_mode = ProjectSettings.get_setting(AUTO_SAVE_MODE_SETTINGS, autosave_mode)
|
||||
autosave_time = ProjectSettings.get_setting(AUTO_SAVE_TIME_SETTINGS, autosave_time)
|
||||
|
||||
_result = dialogic.event_handled.connect(_on_dialogic_event_handled)
|
||||
_result = dialogic.timeline_started.connect(_on_start_or_end_autosave)
|
||||
_result = dialogic.timeline_ended.connect(_on_start_or_end_autosave)
|
||||
|
||||
if autosave_enabled:
|
||||
autosave_timer.start(autosave_time)
|
||||
|
||||
|
||||
func _on_autosave_timer_timeout() -> void:
|
||||
if autosave_mode == AutoSaveMode.ON_TIMER:
|
||||
perform_autosave()
|
||||
|
||||
autosave_timer.start(autosave_time)
|
||||
|
||||
|
||||
func _on_dialogic_event_handled(event: DialogicEvent) -> void:
|
||||
if event is DialogicJumpEvent:
|
||||
|
||||
if autosave_mode == AutoSaveMode.ON_TIMELINE_JUMPS:
|
||||
perform_autosave()
|
||||
|
||||
if event is DialogicTextEvent:
|
||||
|
||||
if autosave_mode == AutoSaveMode.ON_TEXT_EVENT:
|
||||
perform_autosave()
|
||||
|
||||
|
||||
func _on_start_or_end_autosave() -> void:
|
||||
if autosave_mode == AutoSaveMode.ON_TIMELINE_JUMPS:
|
||||
perform_autosave()
|
||||
|
||||
|
||||
## Perform an autosave.
|
||||
## This method will be called automatically if the auto-save mode is enabled.
|
||||
func perform_autosave() -> Error:
|
||||
return save("", true)
|
||||
|
||||
#endregion
|
||||
1
addons/dialogic/Modules/Save/subsystem_save.gd.uid
Normal file
1
addons/dialogic/Modules/Save/subsystem_save.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ckdysfblcy4nl
|
||||
Reference in New Issue
Block a user