@tool @icon("uid://j7ym07anlusi") extends "res://addons/dialogic/Resources/dialogic_identifiable_resource.gd" class_name DialogicTimeline ## Resource that defines a list of events. ## It can store them as text and load them from text too. var events: Array = [] var events_processed := false var text_lines_indexed := {} var indent_format := "\t" func _get_extension() -> String: return "dtl" func _get_resource_name() -> String: return "DialogicTimeline" ## Helper method func get_event(index:int) -> Variant: if index >= len(events): return null return events[index] ## Parses the lines as seperate events and insert them in an array, ## so they can be converted to DialogicEvent's when processed later func from_text(text:String) -> void: events = text.split('\n', true) events_processed = false ## Take an initial guess about the indentation format ## because the text editor needs it but doesn't call _process() for ev in events: indent_format = ev.substr(0, len(ev) - len(ev.strip_edges(true, false))) if indent_format: break ## Stores all events in their text format and returns them as a string func as_text() -> String: var result := "" if events_processed: var indent := 0 for idx in range(0, len(events)): var event: DialogicEvent = events[idx] if event.event_name == 'End Branch': indent -= 1 continue if event != null: for i in event.empty_lines_above: result += indent_format.repeat(indent)+"\n" result += indent_format.repeat(indent)+event.event_node_as_text.replace('\n', "\n"+indent_format.repeat(indent)) + "\n" if event.can_contain_events: indent += 1 if indent < 0: indent = 0 else: for event in events: result += str(event)+"\n" result.trim_suffix('\n') return result.strip_edges() ## Returns the index of the event that corresponds to a specific line func get_index_from_text_line(text:String, line) -> int: from_text(text) process() return text_lines_indexed[line] ## Method that loads all the event resources from the strings, if it wasn't done before func process() -> void: if len(events) == 0: return if typeof(events[0]) == TYPE_STRING: events_processed = false # if the timeline is already processed if events_processed: for event in events: event.event_node_ready = true return var event_cache := DialogicResourceUtil.get_event_cache() var end_event := DialogicEndBranchEvent.new() var prev_indent := "" indent_format = "" var processed_events := [] text_lines_indexed = {} # this is needed to add an end branch event even to empty conditions/choices var prev_was_opener := false var lines := events var idx := -1 var empty_lines := 0 while idx < len(lines)-1: idx += 1 text_lines_indexed[idx] = len(processed_events) # make sure we are using the string version, in case this was already converted var line := "" if typeof(lines[idx]) == TYPE_STRING: line = lines[idx] else: line = lines[idx].event_node_as_text ## Ignore empty lines, but record them in @empty_lines var line_stripped: String = line.strip_edges(true, false) if line_stripped.is_empty(): empty_lines += 1 continue ## Add an end event if the indent is smaller then previously var indent: String = line.substr(0,len(line)-len(line_stripped)) if indent and ((not indent_format) or not (indent_format in indent)): if not indent_format.is_empty(): printerr("Timeline contains varying indentation. Found {0} instead of expected {1}.".format([indent, indent_format])) else: indent_format = indent if len(indent) < len(prev_indent): @warning_ignore("integer_division") for i in range(len(prev_indent)/len(indent_format)-len(indent)/len(indent_format)): processed_events.append(end_event.duplicate()) ## Add an end event if the indent is the same but the previous was an opener ## (so for example choice that is empty) if prev_was_opener and len(indent) <= len(prev_indent): processed_events.append(end_event.duplicate()) prev_indent = indent ## Now we process the event into a resource ## by checking on each event if it recognizes this string var event_content: String = line_stripped var event: DialogicEvent = event_from_string(event_content, event_cache) event.empty_lines_above = empty_lines # add the following lines until the event says it's full or there is an empty line while not event.is_string_full_event(event_content): idx += 1 text_lines_indexed[idx] = len(processed_events) if idx == len(lines): break var following_line_stripped: String = lines[idx].strip_edges(true, false) if following_line_stripped.is_empty(): break event_content += "\n"+following_line_stripped event.event_node_as_text = event_content event._load_from_string(event_content) processed_events.append(event) prev_was_opener = event.can_contain_events empty_lines = 0 if not prev_indent.is_empty(): for i in range(len(prev_indent)): processed_events.append(end_event.duplicate()) events = processed_events events_processed = true if indent_format.is_empty(): indent_format = "\t" ## This method makes sure that all events in a timeline are correctly reset func clean() -> void: if not events_processed: return reference() # This is necessary because otherwise INTERNAL GODOT ONESHOT CONNECTIONS # are disconnected before they can disconnect themselves. await Engine.get_main_loop().process_frame for event:DialogicEvent in events: for con_in in event.get_incoming_connections(): con_in.signal.disconnect(con_in.callable) for sig in event.get_signal_list(): for con_out in event.get_signal_connection_list(sig.name): con_out.signal.disconnect(con_out.callable) unreference() static func event_from_string(event_content:String, event_cache:Array) -> DialogicEvent: for i in event_cache: if i._test_event_string(event_content): return i.duplicate() return DialogicTextEvent.new()