class_name DialogicGameHandler extends Node ## Class that is used as the Dialogic autoload. ## Autoload script that allows you to interact with all of Dialogic's systems:[br] ## - Holds all important information about the current state of Dialogic.[br] ## - Provides access to all the subsystems.[br] ## - Has methods to start/end timelines.[br] ## States indicating different phases of dialog. enum States { IDLE, ## Dialogic is awaiting input to advance. REVEALING_TEXT, ## Dialogic is currently revealing text. ANIMATING, ## Some animation is happening. AWAITING_CHOICE, ## Dialogic awaits the selection of a choice WAITING ## Dialogic is currently awaiting something. } ## Flags indicating what to clear when calling [method clear]. enum ClearFlags { FULL_CLEAR = 0, ## Clears all subsystems KEEP_VARIABLES = 1, ## Clears all subsystems and info except for variables TIMELINE_INFO_ONLY = 2 ## Doesn't clear subsystems but current timeline and index } ## Reference to the currently executed timeline. var current_timeline: DialogicTimeline = null ## Copy of the [member current_timeline]'s events. var current_timeline_events: Array = [] ## Index of the event the timeline handling is currently at. var current_event_idx: int = 0 ## Current state (see [member States] enum). var current_state := States.IDLE: get: return current_state set(new_state): current_state = new_state state_changed.emit(new_state) ## Emitted when [member current_state] change. signal state_changed(new_state:States) ## When `true`, many dialogic processes won't continue until it's `false` again. var paused := false: set(value): paused = value if paused: for subsystem in get_children(): if subsystem is DialogicSubsystem: (subsystem as DialogicSubsystem)._pause() dialogic_paused.emit() else: for subsystem in get_children(): if subsystem is DialogicSubsystem: (subsystem as DialogicSubsystem)._resume() dialogic_resumed.emit() ## A timeline that will be played when dialog ends. ## By default this timeline only contains a clear event. var dialog_ending_timeline: DialogicTimeline ## Emitted when [member paused] changes to `true`. signal dialogic_paused ## Emitted when [member paused] changes to `false`. signal dialogic_resumed ## Emitted when a timeline starts by calling either [method start] ## or [method start_timeline]. signal timeline_started ## Emitted when the timeline ends. ## This can be a timeline ending or [method end_timeline] being called. signal timeline_ended ## Emitted when an event starts being executed. ## The event may not have finished executing yet. signal event_handled(resource: DialogicEvent) ## Emitted when a [class SignalEvent] event was reached. @warning_ignore("unused_signal") # This is emitted by the signal event. signal signal_event(argument: Variant) ## Emitted when a signal event gets fired from a [class TextEvent] event. @warning_ignore("unused_signal") # This is emitted by the text subsystem. signal text_signal(argument: String) # Careful, this section is repopulated automatically at certain moments. #region SUBSYSTEMS const AnimationSubsystem = preload("res://addons/dialogic/Modules/Core/subsystem_animation.gd") var Animations: AnimationSubsystem: get: return get_subsystem("Animations") const AudioSubsystem = preload("res://addons/dialogic/Modules/Audio/subsystem_audio.gd") var Audio: AudioSubsystem: get: return get_subsystem("Audio") const BackgroundsSubsystem = preload("res://addons/dialogic/Modules/Background/subsystem_backgrounds.gd") var Backgrounds: BackgroundsSubsystem: get: return get_subsystem("Backgrounds") const ChoicesSubsystem = preload("res://addons/dialogic/Modules/Choice/subsystem_choices.gd") var Choices: ChoicesSubsystem: get: return get_subsystem("Choices") const ExpressionSubsystem = preload("res://addons/dialogic/Modules/Core/subsystem_expression.gd") var Expressions: ExpressionSubsystem: get: return get_subsystem("Expressions") const GlossarySubsystem = preload("res://addons/dialogic/Modules/Glossary/subsystem_glossary.gd") var Glossary: GlossarySubsystem: get: return get_subsystem("Glossary") const HistorySubsystem = preload("res://addons/dialogic/Modules/History/subsystem_history.gd") var History: HistorySubsystem: get: return get_subsystem("History") const InputsSubsystem = preload("res://addons/dialogic/Modules/Core/subsystem_input.gd") var Inputs: InputsSubsystem: get: return get_subsystem("Inputs") const JumpSubsystem = preload("res://addons/dialogic/Modules/Jump/subsystem_jump.gd") var Jump: JumpSubsystem: get: return get_subsystem("Jump") const PortraitContainersSubsystem = preload("res://addons/dialogic/Modules/Character/subsystem_containers.gd") var PortraitContainers: PortraitContainersSubsystem: get: return get_subsystem("PortraitContainers") const PortraitsSubsystem = preload("res://addons/dialogic/Modules/Character/subsystem_portraits.gd") var Portraits: PortraitsSubsystem: get: return get_subsystem("Portraits") const SaveSubsystem = preload("res://addons/dialogic/Modules/Save/subsystem_save.gd") var Save: SaveSubsystem: get: return get_subsystem("Save") const SettingsSubsystem = preload("res://addons/dialogic/Modules/Settings/subsystem_settings.gd") var Settings: SettingsSubsystem: get: return get_subsystem("Settings") const StylesSubsystem = preload("res://addons/dialogic/Modules/Style/subsystem_styles.gd") var Styles: StylesSubsystem: get: return get_subsystem("Styles") const TextSubsystem = preload("res://addons/dialogic/Modules/Text/subsystem_text.gd") var Text: TextSubsystem: get: return get_subsystem("Text") const TextInputSubsystem = preload("res://addons/dialogic/Modules/TextInput/subsystem_text_input.gd") var TextInput: TextInputSubsystem: get: return get_subsystem("TextInput") const VARSubsystem = preload("res://addons/dialogic/Modules/Variable/subsystem_variables.gd") var VAR: VARSubsystem: get: return get_subsystem("VAR") const VoiceSubsystem = preload("res://addons/dialogic/Modules/Voice/subsystem_voice.gd") var Voice: VoiceSubsystem: get: return get_subsystem("Voice") const WaitSubsystem = preload("res://addons/dialogic/Modules/Wait/subsystem_wait.gd") var Wait: WaitSubsystem: get: return get_subsystem("Wait") #endregion ## Autoloads are added first, so this happens REALLY early on game startup. func _ready() -> void: _collect_subsystems() clear() DialogicResourceUtil.update_event_cache() dialog_ending_timeline = DialogicTimeline.new() dialog_ending_timeline.from_text("[clear]") if OS.is_debug_build(): instantiate_debug_overlay() #region TIMELINE & EVENT HANDLING ################################################################################ ## Method to start a timeline AND ensure that a layout scene is present. ## For argument info, checkout [method start_timeline]. ## -> returns the layout node func start(timeline:Variant, label_or_idx:Variant="") -> Node: # If we don't have a style subsystem, default to just start_timeline() if not has_subsystem("Styles"): printerr("[Dialogic] You called Dialogic.start() but the Styles subsystem is missing!") clear(ClearFlags.KEEP_VARIABLES) start_timeline(timeline, label_or_idx) return null # Otherwise make sure there is a style active. var scene: Node = null if not self.Styles.has_active_layout_node(): scene = self.Styles.load_style() else: scene = self.Styles.get_layout_node() scene.show() if not scene.is_node_ready(): if not scene.ready.is_connected(clear.bind(ClearFlags.KEEP_VARIABLES)): scene.ready.connect(clear.bind(ClearFlags.KEEP_VARIABLES)) if not scene.ready.is_connected(start_timeline.bind(timeline, label_or_idx)): scene.ready.connect(start_timeline.bind(timeline, label_or_idx)) else: start_timeline(timeline, label_or_idx) return scene ## Method to start a timeline without adding a layout scene. ## [param timeline] can be either a loaded timeline resource or a path to a timeline file. ## [param label_or_idx] can be a label (string) or index (int) to skip to immediatly. func start_timeline(timeline:Variant, label_or_idx:Variant = "") -> void: # load the resource if only the path is given if typeof(timeline) in [TYPE_STRING, TYPE_STRING_NAME]: #check the lookup table if it's not a full file name if "://" in timeline: timeline = load(timeline) else: timeline = DialogicResourceUtil.get_timeline_resource(timeline) if timeline == null: printerr("[Dialogic] There was an error loading this timeline. Check the filename, and the timeline for errors") return (timeline as DialogicTimeline).process() current_timeline = timeline current_timeline_events = current_timeline.events for event in current_timeline_events: event.dialogic = self current_event_idx = -1 if typeof(label_or_idx) in [TYPE_STRING, TYPE_STRING_NAME]: if label_or_idx: if has_subsystem("Jump"): Jump.jump_to_label((label_or_idx as String)) elif typeof(label_or_idx) == TYPE_INT: if label_or_idx >-1: current_event_idx = label_or_idx -1 if not current_timeline == dialog_ending_timeline: timeline_started.emit() handle_next_event() ## Preloader function, prepares a timeline and returns an object to hold for later ## [param timeline_resource] can be either a path (string) or a loaded timeline (resource) func preload_timeline(timeline_resource:Variant) -> Variant: # I think ideally this should be on a new thread, will test if typeof(timeline_resource) in [TYPE_STRING, TYPE_STRING_NAME]: if "://" in timeline_resource: timeline_resource = load(timeline_resource) else: timeline_resource = DialogicResourceUtil.get_timeline_resource(timeline_resource) if timeline_resource == null: printerr("[Dialogic] There was an error preloading this timeline. Check the filename, and the timeline for errors") return null (timeline_resource as DialogicTimeline).process() return timeline_resource ## Clears and stops the current timeline. ## If [param skip_ending] is `true`, the dialog_ending_timeline is not getting played func end_timeline(skip_ending := false) -> void: if not skip_ending and dialog_ending_timeline and current_timeline != dialog_ending_timeline: start(dialog_ending_timeline) return await clear(ClearFlags.TIMELINE_INFO_ONLY) if Styles.has_active_layout_node() and Styles.get_layout_node().is_inside_tree(): match ProjectSettings.get_setting("dialogic/layout/end_behaviour", 0): 0: Styles.get_layout_node().get_parent().remove_child(Styles.get_layout_node()) Styles.get_layout_node().queue_free() 1: Styles.get_layout_node().hide() timeline_ended.emit() ## Method to check if timeline exists. ## @timeline can be either a loaded timeline resource or a path to a timeline file. func timeline_exists(timeline:Variant) -> bool: if typeof(timeline) in [TYPE_STRING, TYPE_STRING_NAME]: if "://" in timeline and ResourceLoader.exists(timeline): return load(timeline) is DialogicTimeline else: return DialogicResourceUtil.timeline_resource_exists(timeline) return timeline is DialogicTimeline ## Handles the next event. func handle_next_event(_ignore_argument: Variant = "") -> void: handle_event(current_event_idx+1) ## Handles the event at the given index [param event_index]. ## You can call this manually, but if another event is still executing, it might have unexpected results. func handle_event(event_index:int) -> void: if not current_timeline: return _cleanup_previous_event() if paused: await dialogic_resumed if event_index >= len(current_timeline_events): end_timeline() return # TODO: Check if necessary. This should be impossible. #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 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) set_meta("previous_event", current_timeline_events[event_index]) current_timeline_events[event_index].execute(self) event_handled.emit(current_timeline_events[event_index]) ## Resets Dialogic's state fully or partially. ## By using the clear flags from the [member ClearFlags] enum you can specify ## what info should be kept. ## For example, at timeline end usually it doesn't clear node or subsystem info. func clear(clear_flags := ClearFlags.FULL_CLEAR) -> void: _cleanup_previous_event() if not clear_flags & ClearFlags.TIMELINE_INFO_ONLY: for subsystem in get_children(): if subsystem is DialogicSubsystem: (subsystem as DialogicSubsystem)._clear_state(clear_flags) current_event_idx = -1 current_timeline_events = [] current_state = States.IDLE # Resetting variables var previous_timeline := current_timeline current_timeline = null if previous_timeline: await previous_timeline.clean() ## Cleanup after previous event (if any). func _cleanup_previous_event(): if has_meta("previous_event") and get_meta("previous_event") is DialogicEvent: var event := get_meta("previous_event") as DialogicEvent if event.event_finished.is_connected(handle_next_event): event.event_finished.disconnect(handle_next_event) event._clear_state() remove_meta("previous_event") #endregion #region SAVING & LOADING ################################################################################ ## Returns a dictionary containing all necessary information to later recreate the same state with load_full_state. ## The [subsystem Save] subsystem might be more useful for you. ## However, this can be used to integrate the info into your own save system. func get_full_state() -> DialogicSaveState: var state := DialogicSaveState.new() if current_timeline: state.event_index = current_event_idx state.timeline = current_timeline.resource_path else: state.event_index = -1 state.timeline = "" for subsystem in get_children(): var sub_state := (subsystem as DialogicSubsystem).get_state() if sub_state: state.subsystems[subsystem.name] = sub_state return state ## This method tries to load the state from the given [param state_info]. ## Will automatically start a timeline and add a layout if a timeline was running when ## the dictionary was retrieved with [method get_full_state]. func load_full_state(state:DialogicSaveState) -> void: await clear() if state == null: printerr("[Dialogic] Attempted to load state, but given state was [null].") return for subsystem in get_children(): if subsystem.name in state.subsystems: subsystem.unpack_state(state.subsystems[subsystem.name]) ### The Style subsystem needs to run first for others to load correctly. var scene: Node = null if has_subsystem("Styles"): get_subsystem("Styles").load_state() scene = self.Styles.get_layout_node() var load_subsystems := func() -> void: for subsystem in get_children(): if subsystem.name == "Styles": continue (subsystem as DialogicSubsystem).load_state() if null != scene and not scene.is_node_ready(): scene.ready.connect(load_subsystems) else: await get_tree().process_frame load_subsystems.call() # if state.timeline: start_timeline(state.timeline, state.event_index) else: end_timeline.call_deferred(true) #endregion #region SUB-SYSTEMS ################################################################################ func _collect_subsystems() -> void: var subsystem_nodes := [] as Array[DialogicSubsystem] for indexer in DialogicUtil.get_indexers(): for subsystem in indexer._get_subsystems(): var subsystem_node := add_subsystem(str(subsystem.name), str(subsystem.script)) subsystem_nodes.push_back(subsystem_node) for subsystem in subsystem_nodes: subsystem._post_install() ## Returns `true` if a subystem with the given [param subsystem_name] exists. func has_subsystem(subsystem_name:String) -> bool: return has_node(subsystem_name) ## Returns the subsystem node of the given [param subsystem_name] or null if it doesn't exist. func get_subsystem(subsystem_name:String) -> DialogicSubsystem: return get_node(subsystem_name) ## Adds a subsystem node with the given [param subsystem_name] and [param script_path]. func add_subsystem(subsystem_name:String, script_path:String) -> DialogicSubsystem: var existing_subsystem_node = get_node_or_null(subsystem_name) # If two Subsystem have the same name, we override the existing one with the new one if is_instance_valid(existing_subsystem_node): existing_subsystem_node.set_script(load(script_path)) existing_subsystem_node.dialogic = self existing_subsystem_node._ready() return existing_subsystem_node var node: Node = Node.new() node.name = subsystem_name node.set_script(load(script_path)) node = node as DialogicSubsystem node.dialogic = self add_child(node) return node #endregion #region HELPERS ################################################################################ func print_debug_moment() -> void: if not current_timeline: return printerr("\t> On line {line} of {timeline_identifier} ({timeline_path}) at event {event_idx} ({event_name} Event).".format( { "event_idx": current_event_idx+1, "event_name":current_timeline_events[current_event_idx].event_name, "timeline_identifier": current_timeline.get_identifier(), "timeline_path":current_timeline.resource_path, "line":current_timeline.get_text_line_from_index(current_event_idx+1) })) print("\n") func instantiate_debug_overlay() -> void: var overlay: Node = load("res://addons/dialogic/Editor/TimelineEditor/dialogic_debug_overlay.tscn").instantiate() get_parent().add_child.call_deferred(overlay) #endregion