extends Node enum states {IDLE, SHOWING_TEXT, ANIMATING, AWAITING_CHOICE, WAITING} var current_timeline: Variant = null var current_timeline_events: Array = [] var timeline_jump_stack: Array = [] var character_directory: Dictionary = {} var timeline_directory: Dictionary = {} var _event_script_cache: Array[DialogicEvent] = [] var current_state: Variant = null: get: return current_state set(new_state): current_state = new_state emit_signal('state_changed', new_state) var paused := false: set(value): paused = value if paused: for subsystem in get_children(): if subsystem.has_method('pause'): subsystem.pause() dialogic_paused.emit() else: for subsystem in get_children(): if subsystem.has_method('resume'): subsystem.resume() dialogic_resumed.emit() signal dialogic_paused signal dialogic_resumed var current_event_idx: int = 0 var current_state_info: Dictionary = {} signal state_changed(new_state) signal timeline_ended() signal timeline_started() signal event_handled(resource) signal signal_event(argument) signal text_signal(argument) func _ready() -> void: rebuild_character_directory() rebuild_timeline_directory() collect_subsystems() clear() ################################################################################ ## TIMELINE+EVENT HANDLING ################################################################################ func start_timeline(timeline_resource:Variant, label_or_idx:Variant = "") -> void: # load the resource if only the path is given if typeof(timeline_resource) == TYPE_STRING: #check the lookup table if it's not a full file name if timeline_resource.contains("res://"): timeline_resource = load(timeline_resource) else: timeline_resource = load(find_timeline(timeline_resource)) if timeline_resource == null: assert(false, "There was an error loading this timeline. Check the filename, and the timeline for errors") timeline_resource = process_timeline(timeline_resource) current_timeline = timeline_resource current_timeline_events = current_timeline.get_events() current_event_idx = -1 if typeof(label_or_idx) == TYPE_STRING: if label_or_idx: jump_to_label(label_or_idx) elif typeof(label_or_idx) == TYPE_INT: if label_or_idx >-1: current_event_idx = label_or_idx -1 emit_signal('timeline_started') handle_next_event() # Preloader function, prepares a timeline and returns an object to hold for later ## TODO: Question: why is this taking a variant and then only allowing a string? func preload_timeline(timeline_resource:Variant) -> Variant: # I think ideally this should be on a new thread, will test if typeof(timeline_resource) == TYPE_STRING: timeline_resource = load(timeline_resource) if timeline_resource == null: assert(false, "There was an error loading this timeline. Check the filename, and the timeline for errors") else: timeline_resource = process_timeline(timeline_resource) return timeline_resource return false func end_timeline() -> void: current_timeline = null current_timeline_events = [] clear() emit_signal("timeline_ended") func handle_next_event(ignore_argument:Variant = "") -> void: handle_event(current_event_idx+1) func handle_event(event_index:int) -> void: if not current_timeline: return if paused: await dialogic_resumed if event_index >= len(current_timeline_events): if timeline_jump_stack.size() > 0: pop_from_jump_stack() event_index = current_event_idx+1 else: end_timeline() return #actually process the event now, since we didnt earlier at runtime #this needs to happen before we create the copy DialogicEvent variable, so it doesn't throw an error if not ready if current_timeline_events[event_index]['event_node_ready'] == false: current_timeline_events[event_index]._load_from_string(current_timeline_events[event_index]['event_node_as_text']) current_event_idx = event_index #print("\n[D] Handle Event ", event_index, ": ", event) if current_timeline_events[event_index].continue_at_end: #print(" -> WILL AUTO CONTINUE!") if not current_timeline_events[event_index].event_finished.is_connected(handle_next_event): current_timeline_events[event_index].event_finished.connect(handle_next_event, CONNECT_ONE_SHOT) if has_subsystem('History'): self.History.add_event_to_history(current_timeline.resource_path, event_index, current_timeline_events[event_index]) current_timeline_events[event_index].execute(self) emit_signal('event_handled', current_timeline_events[event_index]) func jump_to_label(label:String) -> void: if label.is_empty(): current_event_idx = 0 return var idx: int = -1 while true: idx += 1 var event: Variant = current_timeline.get_event(idx) if not event: idx = current_event_idx break if event is DialogicLabelEvent and event.name == label: break current_event_idx = idx func push_to_jump_stack() -> void: var current_point:Dictionary = {} current_point['timeline'] = current_timeline current_point['index'] = current_event_idx timeline_jump_stack.push_front(current_point) func pop_from_jump_stack() -> void: var stack_point:Dictionary = timeline_jump_stack.pop_front() current_timeline = stack_point['timeline'] current_timeline_events = current_timeline.get_events() current_event_idx = stack_point['index'] func clear() -> bool: for subsystem in get_children(): subsystem.clear_game_state() # Clearing existing Dialogic main nodes for i in get_tree().get_nodes_in_group('dialogic_main_node'): i.queue_free() # Resetting variables current_timeline = null current_event_idx = -1 current_timeline_events = [] current_state = states.IDLE return true ################################################################################ ## STATE ################################################################################ func execute_condition(condition:String) -> bool: var regex: RegEx = RegEx.new() regex.compile('{(\\w.*)}') var result := regex.search_all(condition) if result: for res in result: var r_string: String = res.get_string() var value: String = self.VAR.get_variable(r_string) if !value.is_valid_float(): value = '"'+value+'"' condition = condition.replace(r_string, value) var expr: Expression = Expression.new() # this doesn't work currently, not sure why. However you can still use autoloads in conditions # you have to put them in {} brackets tho. E.g. `if {MyAutoload.my_var} > 20:` # var autoload_names: Array = [] # var autoloads: Array = [] # for c in get_tree().root.get_children(): # autoloads.append(c) # autoload_names.append(c.name) # expr.parse(condition, autoload_names) # if expr.execute(autoloads, self): expr.parse(condition) if expr.execute([], self): return true if expr.has_execute_failed(): printerr('Dialogic: Condition failed to execute: ', expr.get_error_text()) return false ################################################################################ ## SAVING & LOADING ################################################################################ func get_full_state() -> Dictionary: if current_timeline: current_state_info['current_event_idx'] = current_event_idx current_state_info['current_timeline'] = current_timeline.resource_path else: current_state_info['current_event_idx'] = -1 current_state_info['current_timeline'] = null if has_subsystem('Portraits'): current_state_info['current_portrait_positions'] = self.Portraits.current_positions current_state_info['default_portrait_positions'] = self.Portraits._default_positions if has_subsystem('History'): if self.History.full_history_enabled: self.History.strip_events_from_full_history() current_state_info['full_history'] = self.History.full_history if self.History.text_read_history_enabled: current_state_info['text_read_history'] = self.History.text_read_history return current_state_info func load_full_state(state_info:Dictionary) -> void: for subsystem in get_children(): subsystem.clear_game_state() current_state_info = state_info if current_state_info.get('current_timeline', null): start_timeline(current_state_info.current_timeline, current_state_info.get('current_event_idx', 0)) if has_subsystem('Portraits'): if current_state_info.get('current_portrait_positions', null): self.Portraits.current_positions = current_state_info['current_portrait_positions'] self.Portraits._default_positions = current_state_info['default_portrait_positions'] if has_subsystem('History'): if self.History.full_history_enabled: self.History.full_history = current_state_info['full_history'] if self.History.text_read_history_enabled: self.History.text_read_history = current_state_info['text_read_history'] for subsystem in get_children(): subsystem.load_game_state() ################################################################################ ## SUB-SYTSEMS ################################################################################ func collect_subsystems() -> void: # This also builds the event script cache as well _event_script_cache = [] for indexer in DialogicUtil.get_indexers(): # build event cache for event in indexer._get_events(): if not 'event_end_branch.gd' in event and not 'event_text.gd' in event: _event_script_cache.append(load(event).new()) # build the subsystems (only at runtime) if !Engine.is_editor_hint(): for subsystem in indexer._get_subsystems(): add_subsytsem(subsystem.name, subsystem.script) # Events are checked in order while testing them. EndBranch needs to be first, Text needs to be last _event_script_cache.push_front(DialogicEndBranchEvent.new()) _event_script_cache.push_back(DialogicTextEvent.new()) func has_subsystem(_name:String) -> bool: return has_node(_name) func get_subsystem(_name:String) -> Variant: return get_node(_name) func add_subsytsem(_name:String, _script_path:String) -> Node: var node:Node = Node.new() node.name = _name node.set_script(load(_script_path)) node.dialogic = self add_child(node) return node func _get(property): if has_subsystem(property): return get_node(str(property)) func _set(property, value): if has_subsystem(property): return true ################################################################################ ## PROCESSING FUNCTIONS ################################################################################ func rebuild_character_directory() -> void: var characters: Array = DialogicUtil.list_resources_of_type(".dch") # First sort by length of path, so shorter paths are first characters.sort_custom(func(a, b):return a.count("/") < b.count("/")) # next we prepare the additional arrays needed for building the depth tree var shortened_paths:Array = [] var reverse_array:Array = [] var reverse_array_splits:Array = [] for i in characters.size(): characters[i] = characters[i].replace("res:///", "res://") var path = characters[i].replace("res://","").replace(".dch", "") if path[0] == "/": path = path.right(-1) shortened_paths.append(path) #split the shortened path up, and reverse it var path_breakdown = path.split("/") path_breakdown.reverse() #Add the name of the file at beginning now, and another array saving the reversed split within each element reverse_array.append(path_breakdown[0]) reverse_array_splits.append(path_breakdown) # Now the three arrays are prepped, begin the depth search var clean_search_path:bool = false var depth := 1 while !clean_search_path: var interim_array:Array = [] clean_search_path = true for i in shortened_paths.size(): if reverse_array.count(reverse_array[i]) > 1: clean_search_path = false if depth < reverse_array_splits[i].size(): interim_array.append(reverse_array_splits[i][depth] + "/" + reverse_array[i]) else: interim_array.append(reverse_array[i]) else: interim_array.append(reverse_array[i]) depth += 1 reverse_array = interim_array # Now finally build the database from those arrays for i in characters.size(): var entry:Dictionary = {} var charfile: DialogicCharacter= load(characters[i]) entry['resource'] = charfile entry['full_path'] = characters[i] entry['unique_short_path'] = reverse_array[i] character_directory[reverse_array[i]] = entry func rebuild_timeline_directory() -> void: var characters: Array = DialogicUtil.list_resources_of_type(".dtl") # First sort by length of path, so shorter paths are first characters.sort_custom(func(a, b):return a.count("/") < b.count("/")) # next we prepare the additional arrays needed for building the depth tree var shortened_paths:Array = [] var reverse_array:Array = [] var reverse_array_splits:Array = [] for i in characters.size(): characters[i] = characters[i].replace("res:///", "res://") var path = characters[i].replace("res://","").replace(".dtl", "") if path[0] == "/": path = path.right(-1) shortened_paths.append(path) #split the shortened path up, and reverse it var path_breakdown = path.split("/") path_breakdown.reverse() #Add the name of the file at beginning now, and another array saving the reversed split within each element reverse_array.append(path_breakdown[0]) reverse_array_splits.append(path_breakdown) # Now the three arrays are prepped, begin the depth search var clean_search_path:bool = false var depth = 1 while !clean_search_path: var interim_array:Array = [] clean_search_path = true for i in shortened_paths.size(): if reverse_array.count(reverse_array[i]) > 1: clean_search_path = false if depth < reverse_array_splits[i].size(): interim_array.append(reverse_array_splits[i][depth] + "/" + reverse_array[i]) else: interim_array.append(reverse_array[i]) else: interim_array.append(reverse_array[i]) depth += 1 reverse_array = interim_array # Now finally build the database from those arrays for i in characters.size(): timeline_directory[reverse_array[i]] = characters[i] func find_timeline(path: String) -> String: if path in timeline_directory.keys(): return timeline_directory[path] else: for i in timeline_directory.keys(): if timeline_directory[i].contains(path): return timeline_directory[i] return "" func process_timeline(timeline: DialogicTimeline) -> DialogicTimeline: if timeline != null: if timeline.events_processed: return timeline else: #print(str(Time.get_ticks_msec()) + ": Starting process unloaded timeline") var end_event := DialogicEndBranchEvent.new() var prev_indent := "" var events := [] # this is needed to add a end branch event even to empty conditions/choices var prev_was_opener := false var lines := timeline.events var idx := -1 var empty_lines = 0 while idx < len(lines)-1: idx += 1 ## First manage indent and creation of end branch events ## For this, we still treat the event as a string var line: String = "" if typeof(lines[idx]) == TYPE_STRING: line = lines[idx] else: line = lines[idx]['event_node_as_text'] var line_stripped :String = line.strip_edges(true, false) if line_stripped.is_empty(): empty_lines += 1 continue var indent :String= line.substr(0,len(line)-len(line_stripped)) if len(indent) < len(prev_indent): for i in range(len(prev_indent)-len(indent)): events.append(end_event.duplicate()) elif prev_was_opener and len(indent) == len(prev_indent): 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 :Variant for i in _event_script_cache: if i._test_event_string(event_content): event = i.duplicate() break event.empty_lines_above = empty_lines # add the following lines until the event says it's full there is an empty line or the indent changes while !event.is_string_full_event(event_content): idx += 1 if idx == len(lines): break var following_line :String = lines[idx] var following_line_stripped :String = following_line.strip_edges(true, false) var following_line_indent :String = following_line.substr(0,len(following_line)-len(following_line_stripped)) if following_line_stripped.is_empty(): break if following_line_indent != indent: idx -= 1 break event_content += "\n"+following_line_stripped if Engine.is_editor_hint(): # Unlike at runtime, for some reason here the event scripts can't access the scene tree to get to the character directory, so we will need to pass it to it before processing if event['event_name'] == 'Character' || event['event_name'] == 'Text': event.set_meta('editor_character_directory', character_directory) event._load_from_string(event_content) event['event_node_as_text'] = event_content events.append(event) prev_was_opener = event.can_contain_events empty_lines = 0 if !prev_indent.is_empty(): for i in range(len(prev_indent)): events.append(end_event.duplicate()) timeline.events = events timeline.events_processed = true #print(str(Time.get_ticks_msec()) + ": Finished process unloaded timeline") return timeline else: return DialogicTimeline.new() ################################################################################ ## FOR END USER ################################################################################ func start(timeline, single_instance = true): var dialog_scene_path: String = DialogicUtil.get_project_setting( 'dialogic/editor/default_dialog_scene', "res://addons/dialogic/Example Assets/example-scenes/DialogicDefaultScene.tscn") if single_instance: if get_tree().get_nodes_in_group('dialogic_main_node').is_empty(): var scene = load(dialog_scene_path).instantiate() get_parent().call_deferred("add_child", scene) Dialogic.start_timeline(timeline) func is_running() -> bool: if get_tree().get_nodes_in_group('dialogic_main_node').is_empty(): return false return true