Add addons

This commit is contained in:
Joey Yakimowich-Payne 2023-01-03 16:53:53 -07:00
commit 3367bad24f
No known key found for this signature in database
GPG key ID: 6BFE655FA5ABD1E1
527 changed files with 36197 additions and 0 deletions

View file

@ -0,0 +1,52 @@
@tool
extends ResourceFormatLoader
# Needed by godot
class_name DialogicCharacterFormatLoader
# returns all excepted extenstions
func _get_recognized_extensions() -> PackedStringArray:
return PackedStringArray(["dch"])
# Returns "Rrsource" if this file can/should be loaded by this script
func _get_resource_type(path: String) -> String:
var ext = path.get_extension().to_lower()
if ext == "dch":
return "Resource"
return ""
# Return true if this type is handled
func _handles_type(typename: StringName) -> bool:
return ClassDB.is_parent_class(typename, "Resource")
# parse the file and return a resource
func _load(path: String, original_path: String, use_sub_threads: bool, cache_mode: int):
# print('[Dialogic] Reimporting character "' , path, '"')
if FileAccess.file_exists(path):
var file = FileAccess.open(path, FileAccess.READ)
return dict_to_inst(str_to_var(file.get_as_text()))
else:
push_error("File does not exists")
return false
func _get_dependencies(path:String, add_type:bool):
var depends_on : PackedStringArray
var character:DialogicCharacter = load(path)
for p in character.portraits.values():
if 'path' in p and p.path:
depends_on.append(p.path)
return depends_on
func _rename_dependencies(path: String, renames: Dictionary):
var character:DialogicCharacter = load(path)
for p in character.portraits:
if 'path' in character.portraits[p] and character.portraits[p].path in renames:
character.portraits[p].path = renames[character.portraits[p].path]
ResourceSaver.save(character, path)
return OK

View file

@ -0,0 +1,26 @@
@tool
extends ResourceFormatSaver
class_name DialogicCharacterFormatSaver
func _get_recognized_extensions(resource: Resource) -> PackedStringArray:
return PackedStringArray(["dch"])
# Return true if this resource should be loaded as a DialogicCharacter
func _recognize(resource: Resource) -> bool:
# Cast instead of using "is" keyword in case is a subclass
resource = resource as DialogicCharacter
if resource:
return true
return false
# Save the resource
func _save(resource: Resource, path: String = '', flags: int = 0):
var file := FileAccess.open(path, FileAccess.WRITE)
var result := var_to_str(inst_to_dict(resource))
file.store_string(result)
# print('[Dialogic] Saved character "' , path, '"')

View file

@ -0,0 +1,50 @@
@tool
extends ResourceFormatLoader
# Needed by godot
class_name DialogicTimelineFormatLoader
# returns all excepted extenstions
func _get_recognized_extensions() -> PackedStringArray:
return PackedStringArray(["dtl"])
# Returns "Rrsource" if this file can/should be loaded by this script
func _get_resource_type(path: String) -> String:
var ext = path.get_extension().to_lower()
if ext == "dtl":
return "Resource"
return ""
# Return true if this type is handled
func _handles_type(typename: StringName) -> bool:
return ClassDB.is_parent_class(typename, "Resource")
# parse the file and return a resource
func _load(path: String, original_path: String, use_sub_threads: bool, cache_mode: int):
# print('[Dialogic] Reimporting timeline "' , path, '"')
if FileAccess.file_exists(path):
var file := FileAccess.open(path, FileAccess.READ)
var res := DialogicTimeline.new()
var text : String = file.get_as_text()
# Parse the lines as seperate events and insert them in an array, so they can be converted to DialogicEvent's when processed later
var prev_indent := ""
var events := []
var lines := text.split('\n', true)
var idx := -1
while idx < len(lines)-1:
idx += 1
var line :String = lines[idx]
var line_stripped :String = line.strip_edges(true, true)
events.append(line)
res.events = events
res.events_processed = false
return res

View file

@ -0,0 +1,96 @@
@tool
extends ResourceFormatSaver
class_name DialogicTimelineFormatSaver
func _get_recognized_extensions(resource: Resource) -> PackedStringArray:
return PackedStringArray(["dtl"])
# Return true if this resource should be loaded as a DialogicTimeline
func _recognize(resource: Resource) -> bool:
# Cast instead of using "is" keyword in case is a subclass
resource = resource as DialogicTimeline
if resource:
return true
return false
# Save the resource
func _save(resource: Resource, path: String = '', flags: int = 0) -> int:
if resource.get_meta("timeline_not_saved", false):
if len(resource.events) == 0:
printerr("[Dialogic] Timeline save was called, but there are no events. Timeline will not be saved, to prevent accidental data loss. Please delete the timeline file if you are trying to clear all of the events.")
return ERR_INVALID_DATA
# print('[Dialogic] Beginning saving timeline. Safety checks will be performed before writing, and temporary file will be created and removed if saving is successful...')
#prepare everything before writing, we will only open the file if it's successfuly prepared, as that will clear the file contents
var timeline_as_text :String = ""
# if events are resources, create text
if resource.events_processed:
var indent := 0
for idx in range(0, len(resource.events)):
var event :DialogicEvent= resource.events[idx]
if event.event_name == 'End Branch':
indent -=1
continue
for i in event.empty_lines_above:
timeline_as_text += '\t'.repeat(indent) + '\n'
if event != null:
timeline_as_text += "\t".repeat(indent)+ event.event_node_as_text + "\n"
if event.can_contain_events:
indent += 1
if indent < 0:
indent = 0
# if events are string lines, just save them
else:
for event in resource.events:
timeline_as_text += event + "\n"
# Now do the actual saving
if (len(timeline_as_text) > 0):
var file := FileAccess.open(path.replace(".dtl", ".tmp"), FileAccess.WRITE)
file.store_string(timeline_as_text)
file = null
var dir = DirAccess.open("res://")
if dir.file_exists(path.replace(".dtl", ".tmp")):
file = FileAccess.open(path.replace(".dtl", ".tmp"), FileAccess.READ)
var check_length = file.get_length()
if check_length > 0:
var check_result = file.get_as_text()
file = null
if timeline_as_text == check_result:
dir.copy(path.replace(".dtl", ".tmp"), path)
file = FileAccess.open(path, FileAccess.READ)
var check_result2 = file.get_as_text()
if timeline_as_text == check_result2:
# print('[Dialogic] Completed saving timeline "' , path, '"')
dir.remove(path.replace(".dtl", ".tmp"))
else:
printerr("[Dialogic] " + path + ": Overwriting .dtl file failed! Temporary file was saved as .tmp extension, please check to see if it matches your timeline, and rename to .dtl manually.")
return ERR_INVALID_DATA
else:
printerr("[Dialogic] " + path + ": Temporary timeline file contents do not match what was written! Temporary file was saved as .tmp extension, please check to see if it matches your timeline, and rename to .dtl manually.")
return ERR_INVALID_DATA
else:
printerr("[Dialogic] " + path + ": Temporary timeline file is empty! Timeline was not saved!")
dir.remove(path.replace(".dtl", ".tmp"))
return ERR_INVALID_DATA
else:
printerr("[Dialogic] " + path + ": Temporary timeline file failed to create! Timeline was not saved!")
return ERR_INVALID_DATA
else:
printerr("[Dialogic] " + path + ": Timeline failed to convert to text for saving! Timeline was not saved!")
return ERR_INVALID_DATA
return OK

View file

@ -0,0 +1,33 @@
@tool
extends Resource
class_name DialogicCharacter
@export var display_name:String = ""
@export var nicknames:Array = []
@export var color:Color = Color()
@export var description:String = ""
@export var scale:float = 1.0
@export var offset:Vector2 = Vector2()
@export var mirror:bool = false
@export var default_portrait:String = ""
@export var portraits:Dictionary = {}
@export var custom_info:Dictionary = {}
func __get_property_list() -> Array:
return []
func _to_string() -> String:
return "[{name}:{id}]".format({"name":get_character_name(), "id":get_instance_id()})
func _hide_script_from_inspector() -> bool:
return true
## Returns the name of the file (without the extension).
func get_character_name() -> String:
return resource_path.get_file().trim_suffix('.dch')

View file

@ -0,0 +1,419 @@
@tool
class_name DialogicEvent
extends Resource
## Base event class for all dialogic events.
## Implements basic properties, translation, shortcode saving and usefull methods for creating
## the editor UI.
## Emmited when the event starts.
## The signal is emmited with the event resource [code]event_resource[/code]
signal event_started(event_resource)
## Emmited when the event finish.
## The signal is emmited with the event resource [code]event_resource[/code]
signal event_finished(event_resource)
### Main Event Properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
## The event name that'll be displayed in the editor.
var event_name:String = "Event"
## Unique identifier used for translatable events.
var _translation_id :String= ""
## A reference to dialogic during execution, can be used the same as Dialogic (reference to the autoload)
var dialogic = null
### Special Event Properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
### (these properties store how this event affects indentation/flow of timeline)
## If true this event can not be toplevel (e.g. Choice)
var needs_indentation : bool = false
## If true this event will spawn with an END BRANCH event and higher the indentation
var can_contain_events : bool = false
## If [can_contain_events] is true this is a reference to the end branch event
var end_branch_event : DialogicEndBranchEvent = null
## If this is true this event expects a specific parent event.
var needs_parent_event : bool = false
## If true the next event will be played without awaiting input from the player
var continue_at_end:bool = true
### Saving/Loading Properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
## Stores the event in a text format. Does NOT automatically update.
var event_node_as_text: String = ""
## Flags if the event has been processed or is only stored as text
var event_node_ready: bool = false
## How many empty lines are before this event
var empty_lines_above:int = 0
### Editor UI Properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
## The event color that event node will take in the editor
var event_color: Color = Color("FBB13C")
## If you are using the default color palette
var dialogic_color_name: String = ''
## To sort the buttons shown in the editor. Lower index is placed at the top of a category
var event_sorting_index: int = 0
## If false the name is not displayed on the event.
var display_name: bool = true
## If true the event will not have a button in the visual editor sidebar
var disable_editor_button: bool = false
## If false the event will hide it's body by default. Recommended for most events
var expand_by_default : bool = true
## The URL to open when right_click>Documentation is selected
var help_page_path : String = ""
## Reference to the node, that represents this event. Only works while in visual editor mode.
## Use with care.
var _editor_node : Control = null
## The categories and which one to put it in (in the visual editor sidebar)
enum Category {Main, Logic, Timeline, AudioVisual, Godot, Other}
var event_category:int = Category.Other
### Editor UI creation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
## To differentiate fields that should go to the header and to the body
enum Location {Header, Body}
## To differentiate the different types of fields for event properties in the visual editor
enum ValueType {
# Strings
Label, MultilineText, SinglelineText, Condition,
# Booleans
Bool,
# Resources
ComplexPicker, File,
# Array
StringArray,
# Integers
FixedOptionSelector, Integer, Vector2,
# Floats
Float, Decibel,
# Other
Custom, Button,
}
## List that stores the fields for the editor
var editor_list : Array = []
## Makes this resource printable.
func _to_string() -> String:
return "[{name}:{id}]".format({"name":event_name, "id":get_instance_id()})
################################################################################
## EXECUTION
################################################################################
## Executes the event behaviour. In subclasses [_execute] (not this one) should be overriden!
func execute(_dialogic_game_handler) -> void:
emit_signal("event_started", self)
dialogic = _dialogic_game_handler
call_deferred("_execute")
## Ends the event behaviour.
func finish() -> void:
emit_signal("event_finished", self)
## To be overridden by subclasses.
func _execute() -> void:
finish()
################################################################################
## OVERRIDABLES
################################################################################
## to be overridden by sub-classes
## if needs_parent_event is true, this needs to return true if the event is that event
func is_expected_parent_event(event:DialogicEvent) -> bool:
return false
## to be overridden by sub-classes
## only called if can_contain_events is true.
## return a control node that should show on the END BRANCH node
func get_end_branch_control() -> Control:
return null
## to be overridden by sub-classes
## only called if can_contain_events is true and the previous event was an end-branch event
## return true if this event should be executed if the previous event was an end-branch event
## basically only important for the Condition event but who knows. Some day someone might need this.
func should_execute_this_branch() -> bool:
return false
################################################################################
## TRANSLATIONS
################################################################################
## Overwrite if this events needs translation.
func _get_translatable_properties() -> Array:
return []
## Overwrite if this events needs translation.
func _get_property_original_translation(property_name:String) -> String:
return ''
## Returns true if there is any translatable properties on this event.
## Overwrite [_get_translatable_properties()] to change this.
func can_be_translated() -> bool:
return !_get_translatable_properties().is_empty()
## This is automatically called, no need to use this.
func add_translation_id() -> String:
_translation_id = DialogicUtil.get_next_translation_id()
return _translation_id
func remove_translation_id() -> void:
_translation_id = ""
func get_property_translation_key(property_name:String) -> String:
return event_name.path_join(_translation_id).path_join(property_name)
## Call this whenever you are using a translatable property
func get_property_translated(property_name:String) -> String:
if !_translation_id.is_empty() and DialogicUtil.get_project_setting('dialogic/translation_enabled', false):
var translation = tr(get_property_translation_key(property_name))
# if no translation is found tr() returns the id, but we want to fallback to the original
return translation if translation != _translation_id else _get_property_original_translation(property_name)
else:
return _get_property_original_translation(property_name)
################################################################################
## SAVE / LOAD (internal, don't override)
################################################################################
### These functions are used by the timeline loader/saver
### They mainly use the overridable behaviour below, but enforce the unique_id saving
## Used by the Timeline saver.
func _store_as_string() -> String:
if !_translation_id.is_empty() and can_be_translated():
return to_text() + ' #id:'+str(_translation_id)
else:
return to_text()
## Call this if you updated an event and want the changes to be saved.
func update_text_version() -> void:
event_node_as_text = _store_as_string()
## Used by timeline processor (DGH).
func _load_from_string(string:String) -> void:
if '#id:' in string and can_be_translated():
_translation_id = string.get_slice('#id:', 1).strip_edges()
from_text(string.get_slice('#id:', 0))
event_node_ready = true
else:
from_text(string)
event_node_ready = true
## Used by the timeline processor (DGH).
func _test_event_string(string:String) -> bool:
if '#id:' in string and can_be_translated():
return is_valid_event(string.get_slice('#id:', 0))
return is_valid_event(string.strip_edges())
################################################################################
## SAVE / LOAD
################################################################################
### All of these functions can/should be overridden by the sub classes
## if this uses the short-code format, return the shortcode
func get_shortcode() -> String:
return 'default_shortcode'
## if this uses the short-code format, return the parameters and corresponding property names
func get_shortcode_parameters() -> Dictionary:
return {}
## returns a readable presentation of the event (This is how it's stored)
## by default it uses a shortcode format, but can be overridden
func to_text() -> String:
var result_string : String = "["+self.get_shortcode()
var params : Dictionary = get_shortcode_parameters()
for parameter in params.keys():
if get(params[parameter]["property"]) != params[parameter]["default"]:
if typeof(get(params[parameter]["property"])) == TYPE_OBJECT:
result_string += " "+parameter+'="'+str(get(params[parameter]["property"]).resource_path)+'"'
elif typeof(get(params[parameter]["property"])) == TYPE_STRING:
result_string += " "+parameter+'="'+get(params[parameter]["property"]).replace('=', "\\=")+'"'
else:
result_string += " "+parameter+'="'+var_to_str(get(params[parameter]["property"])).replace('=', "\\=")+'"'
result_string += "]"
return result_string
## loads the variables from the string stored above
## by default it uses the shortcode format, but can be overridden
func from_text(string:String) -> void:
var data : Dictionary = parse_shortcode_parameters(string)
var params : Dictionary = get_shortcode_parameters()
for parameter in params.keys():
if not parameter in data:
continue
#if typeof(data[parameter]) == TYPE_STRING and (data[parameter].ends_with(".dtl") or data[parameter].ends_with(".dch")):
if typeof(data[parameter]) == TYPE_STRING and (data[parameter].ends_with(".dch")):
set(params[parameter]['property'], load(data[parameter]))
else:
var value :Variant
if str_to_var(data[parameter].replace('\\=', '=')) != null:
value = str_to_var(data[parameter].replace('\\=', '='))
else:
value = data[parameter].replace('\\=', '=')
set(params[parameter]['property'], value)
## has to return true, if the given string can be interpreted as this event
## by default it uses the shortcode formta, but can be overridden
func is_valid_event(string:String) -> bool:
if string.strip_edges().begins_with('['+get_shortcode()):
return true
return false
## has to return true if this string seems to be a full event of this kind
## (only tested if is_valid_event() returned true)
## if a shortcode it used it will default to true if the string ends with ']'
func is_string_full_event(string:String) -> bool:
if get_shortcode() != 'default_shortcode': return string.strip_edges().ends_with(']')
return true
## used to get all the shortcode parameters in a string as a dictionary
func parse_shortcode_parameters(shortcode : String) -> Dictionary:
var regex:RegEx = RegEx.new()
regex.compile('((?<parameter>[^\\s=]*)\\s*=\\s*"(?<value>([^=]|\\\\=)*)(?<!\\\\)")')
var dict : Dictionary = {}
for result in regex.search_all(shortcode):
dict[result.get_string('parameter')] = result.get_string('value')
return dict
################################################################################
## EDITOR REPRESENTATION
################################################################################
func _get_icon() -> Resource:
var ext : String = '.png'
var icon := load(self.get_script().get_path().get_base_dir() + "/icon" + ext)
if icon:
return icon
return load("res://addons/dialogic/Editor/Images/Pieces/warning.svg")
func set_default_color(value) -> void:
dialogic_color_name = value
event_color = DialogicUtil.get_color(value)
func _get_property_list() -> Array:
if Engine.is_editor_hint():
if editor_list != null:
editor_list.clear()
else:
editor_list = []
build_event_editor()
return editor_list
else:
return []
## to be overwritten by the sub_classes
func build_event_editor() -> void:
pass
## For the methods below the arguments are mostly similar:
## @variable: String name of the property this field is for
## @condition: String that will be executed as an expression. If it false
## @editor_type: One of the ValueTypes (see ValueType enum). Defines type of field.
## @left_text: Text that will be shown to the left of the field
## @right_text: Text that will be shown to the right of the field
## @extra_info: Allows passing a lot more info to the field.
## What info can be passed is differnet for every field
func add_header_label(text:String, condition:String = "") -> void:
editor_list.append({
"name" : "something",
"type" : TYPE_STRING,
"location" : Location.Header,
"usage" : PROPERTY_USAGE_EDITOR,
"dialogic_type" : ValueType.Label,
"display_info" : {"text":text},
"condition" : condition
})
func add_header_edit(variable:String, editor_type = ValueType.Label, left_text:String = "", right_text:String = "", extra_info:Dictionary = {}, condition:String = "") -> void:
editor_list.append({
"name" : variable,
"type" : typeof(get(variable)),
"location" : Location.Header,
"usage" : PROPERTY_USAGE_DEFAULT,
"dialogic_type" : editor_type,
"display_info" : extra_info,
"left_text" : left_text,
"right_text" : right_text,
"condition" : condition,
})
func add_header_button(text:String, callable:Callable, tooltip:String, icon: Variant = null, condition:String = "") -> void:
editor_list.append({
"name" : "Button",
"type" : TYPE_STRING,
"location" : Location.Header,
"usage" : PROPERTY_USAGE_DEFAULT,
"dialogic_type" : ValueType.Button,
"display_info" : {'text':text, 'tooltip':tooltip, 'callable':callable, 'icon':icon},
"condition" : condition,
})
func add_body_edit(variable:String, editor_type = ValueType.Label, left_text:String= "", right_text:String="", extra_info:Dictionary = {}, condition:String = "") -> void:
editor_list.append({
"name" : variable,
"type" : typeof(get(variable)),
"location" : Location.Body,
"usage" : PROPERTY_USAGE_DEFAULT,
"dialogic_type" : editor_type,
"display_info" : extra_info,
"left_text" : left_text,
"right_text" : right_text,
"condition" : condition,
})
func add_body_line_break(condition:String = "") -> void:
editor_list.append({
"name" : "linebreak",
"type" : TYPE_BOOL,
"location" : Location.Body,
"usage" : PROPERTY_USAGE_DEFAULT,
"condition" : condition,
})

View file

@ -0,0 +1,104 @@
@tool
extends Resource
class_name DialogicTimeline
@export var dialogic_version:String
var events:Array = []:
get:
return events
set(value):
emit_changed()
notify_property_list_changed()
events = value
var events_processed:bool = false
func set_events(_events:Array) -> void:
events = _events
emit_changed()
notify_property_list_changed()
func add_event(event, at_position:int =-1) -> void:
var idx : int = at_position if at_position > -1 else events.size()
events.insert(idx, event)
emit_changed()
notify_property_list_changed()
func erase_event(event) -> void:
events.erase(event)
emit_changed()
notify_property_list_changed()
func remove_event(position:int) -> void:
events.erase(position)
emit_changed()
notify_property_list_changed()
func get_event(index):
if index >= len(events):
return null
return events[index]
func get_events() -> Array:
return events.duplicate()
func _set(property, value):
if str(property).begins_with("event/"):
var event_idx:int = str(property).split("/", true, 2)[1].to_int()
if event_idx < events.size():
events[event_idx] = value
else:
events.insert(event_idx, value)
emit_changed()
notify_property_list_changed()
if property == "events":
set_events(value)
return true
return false
func _get(property):
if str(property).begins_with("event/"):
var event_idx:int = str(property).split("/", true, 2)[1].to_int()
if event_idx < len(events):
return events[event_idx]
return true
func _init() -> void:
events = []
resource_name = get_class()
func _to_string() -> String:
return "[{class}:{file}]".format({"class":get_class(), "file":resource_path})
func get_class() -> String: return "Timeline"
func _get_property_list() -> Array:
var p : Array = []
var usage = PROPERTY_USAGE_SCRIPT_VARIABLE
usage |= PROPERTY_USAGE_NO_EDITOR
usage |= PROPERTY_USAGE_EDITOR # Comment this line to hide events from editor
if events != null:
for event_idx in events.size():
p.append(
{
"name":"event/{idx}".format({"idx":event_idx}),
"type":TYPE_OBJECT,
"usage":PROPERTY_USAGE_DEFAULT|PROPERTY_USAGE_SCRIPT_VARIABLE
}
)
return p