extends Reference static func dialog_from_text(text): var label_regex = RegEx.new() label_regex.compile("[A-Z0-9_]+:") # validation var has_question = false var lines = text.split("\n") var new_dialog = Dialog.new() # intermediate state var current_choice_segment var choices = 0 if current_choice_segment == null: current_choice_segment = ChoiceSegment.new() var line_number = 0 for line in lines: line_number += 1 # comment lines if line.begins_with("#"): pass # is flag unset elif line.begins_with("!@"): var l = line.right(3).strip_edges() var flag = l.left(l.length() - 1) # if OS.is_debug_build(): print("flag to false: ", flag) new_dialog.add_segment(FlagSetSegment.new(flag, false)) # is goto, or conditional goto elif line.begins_with("!"): var label_end = line.find(",") # unconditional goto if label_end == -1: var label = line.substr(2, line.length() - 3).strip_edges() # if OS.is_debug_build(): print("goto: ", label) new_dialog.add_segment(GotoSegment.new(label)) else: # conditional goto var label = line.substr(2, label_end - 2).strip_edges() var target_end = line.find("]") var target = line.substr(label_end + 1, (target_end - 1) - label_end).strip_edges() # if OS.is_debug_build(): print("goto: ", target, " - ", label) new_dialog.add_segment(ConditionalGotoSegment.new(target, label, true)) # is either random goto or question elif line.begins_with("?"): var str_line = line.strip_edges().right(1) # question, initiates dialog, treat first encountered question as question if str_line.begins_with("<"): var q_line = str_line.strip_edges() has_question = true var subject_right_bracket_index = q_line.find(">") var subject = q_line.substr(1, subject_right_bracket_index - 1).strip_edges() var question_text = q_line.right(subject_right_bracket_index + 1).strip_edges() var question_segment = QuestionSegment.new(question_text, subject) new_dialog.add_segment(question_segment) else: # random goto: picks one of the choices to goto, used like ?[P1, P2, P3] var left_bracket = str_line.find("[") var right_bracket = str_line.find("]") if left_bracket == -1: printerr("couldn't find left [ on line: %d" % line_number) assert(false) if right_bracket == -1: printerr("couldn't find right ] on line: %d" % line_number) assert(false) # if there's actually even a single thing inside if right_bracket - left_bracket > 1: var arg_string = str_line.substr(left_bracket + 1, (right_bracket - left_bracket) - 1) var args = arg_string.split(",", false) var stripped_args = [] for arg in args: stripped_args.push_back(arg.strip_edges()) # if OS.is_debug_build(): print("random goto, arg_string: %s" % arg_string) new_dialog.add_segment(RandomGotoSegment.new(stripped_args)) # is flag set, either a normal set to true with @[SOME_FLAG] or a set to string with @[FLAG, STR] elif line.begins_with("@"): var l = line.right(2) var comma_index = l.find(",") if comma_index == -1: var flag = l.left(l.length() - 1) # if OS.is_debug_build(): print("flag to true: ", flag) new_dialog.add_segment(FlagSetSegment.new(flag, true)) else: var flag = l.left(comma_index).strip_edges() var arg_end = l.find("]") var arg_int = l.right(comma_index + 1) var arg = arg_int.left(arg_int.length() - 1).strip_edges() # if OS.is_debug_build(): print("flag: ", flag, " with arg: ", arg) new_dialog.add_segment(FlagSetSegment.new(flag, arg)) # is label, can goto elif line.ends_with(":") and label_regex.search(line): var label = line.left(line.length() - 1).strip_edges() # if OS.is_debug_build(): print("label: ", label) new_dialog.add_segment(LabelSegment.new(label)) # is choice in conversation by player elif line.begins_with("["): var right_end_index = line.find("]") var choice = line.substr(1, right_end_index - 1).strip_edges() var response = line.right(right_end_index + 1).strip_edges() # is it a jump choice? var left_brace_index = response.find("[") var right_brace_index = response.find("]") if left_brace_index != -1 and right_brace_index != -1: var jump_target = response.substr(left_brace_index + 1, right_brace_index - 1) # if OS.is_debug_build(): print("choice: ", choice, " target: ", jump_target) current_choice_segment.add_choice_with_jump(choice, jump_target) else: var subject_right_comma_index = response.find(",") var subject_right_bracket_index = response.find(">") # is response with if response.begins_with("<") and subject_right_bracket_index > 0 and subject_right_comma_index < subject_right_bracket_index: var subject = response.substr(1, subject_right_comma_index - 1).strip_edges() var expression = response.substr(subject_right_comma_index + 1, (subject_right_bracket_index - 1) - subject_right_comma_index).strip_edges() var line_text = response.right(subject_right_bracket_index + 1).strip_edges() # if OS.is_debug_build(): print("choice: ", choice, " response: ", line_text, " with: <", subject, ", ", expression, ">") current_choice_segment.add_choice(choice, DialogWithSubjectExpressionSegment.new(line_text, subject, expression)) elif response.begins_with("<"): # is response with var subject = response.substr(1, subject_right_bracket_index - 1).strip_edges() var line_text = response.right(subject_right_bracket_index + 1).strip_edges() # if OS.is_debug_build(): print("choice: ", choice, " response: ", line_text, " with: <", subject, ">") current_choice_segment.add_choice(choice, DialogWithSubjectSegment.new(line_text, subject)) else: # if OS.is_debug_build(): print("choice: ", choice, " response: ", response) current_choice_segment.add_choice(choice, response) choices += 1 # we're full if choices == 3: # if OS.is_debug_build(): print("choice segment created.") new_dialog.add_segment(current_choice_segment) # setup new one for next round current_choice_segment = ChoiceSegment.new() choices = 0 elif line.begins_with("<"): # test for right comma and > var subject_right_comma_index = line.find(",") var subject_right_bracket_index = line.find(">") # is dialog if subject_right_comma_index > 0 and subject_right_comma_index < subject_right_bracket_index: var subject = line.substr(1, subject_right_comma_index - 1).strip_edges() var expression = line.substr(subject_right_comma_index + 1, (subject_right_bracket_index - 1) - subject_right_comma_index).strip_edges() var line_text = line.right(subject_right_bracket_index + 1).strip_edges() var dialog_subject_segment = DialogWithSubjectExpressionSegment.new(line_text, subject, expression) new_dialog.add_segment(dialog_subject_segment) else: # is dialog var subject = line.substr(1, subject_right_bracket_index - 1).strip_edges() var line_text = line.right(subject_right_bracket_index + 1).strip_edges() var dialog_subject_segment = DialogWithSubjectSegment.new(line_text, subject) new_dialog.add_segment(dialog_subject_segment) # simple function calls, can look like: > FUNC, but also like > FUNC [A, B, C, D] # yieldable function calls can look like: >> FUNC, but also like >> FUNC [A, B, C, D] elif line.begins_with(">"): var str_line = line.right(1).strip_edges() var is_yielding_function = false if str_line.begins_with(">"): str_line = str_line.right(1).strip_edges() is_yielding_function = true var left_bracket = str_line.find("[") var right_bracket = str_line.find("]") # has left bracket, should have right bracket, has params if (left_bracket != -1 and right_bracket != -1) or \ (left_bracket != -1 and right_bracket == -1) or \ (left_bracket == -1 and right_bracket != -1): var command_end = str_line.find("[") var command = str_line.left(command_end).strip_edges() # if OS.is_debug_build(): print("parsing command with args: ", command) if left_bracket == -1: printerr("couldn't find left [ on line: %d" % line_number) assert(false) if right_bracket == -1: printerr("couldn't find right ] on line: %d" % line_number) assert(false) var args_str = str_line.substr(left_bracket + 1, (right_bracket - left_bracket) - 1).strip_edges() var args = args_str.split(",", false) var stripped_args = [] for a in args: stripped_args.push_back(a.strip_edges()) if is_yielding_function: var call_segment = YieldCallSegment.new(command, stripped_args) new_dialog.add_segment(call_segment) else: var call_segment = CallSegment.new(command, stripped_args) new_dialog.add_segment(call_segment) else: # no params, argless call var command = str_line.strip_edges() # if OS.is_debug_build(): print("parsing command: ", command) if is_yielding_function: var call_segment = YieldCallSegment.new(command) new_dialog.add_segment(call_segment) else: var call_segment = CallSegment.new(command) new_dialog.add_segment(call_segment) else: # is normal text var line_text = line.strip_edges() # if line is not empty, process if not line_text.empty(): var dialog_segment = DialogSegment.new(line_text) new_dialog.add_segment(dialog_segment) return new_dialog static func parse_dialog_line(text): pass # types of segments of text, held in Dialogs array class QuestionSegment: var subject var question func _init(q, s): question = q subject = s func get_content(): return question class DialogSegment: var text func _init(t): text = t func get_content(): return text class DialogWithSubjectSegment: var text var subject func _init(t, s): text = t subject = s func get_content(): return text class DialogWithSubjectExpressionSegment: var text var subject var expression func _init(t, s, e): text = t subject = s expression = e func get_content(): return text class Choice: var choice var response func _init(c, r): choice = c response = r func get_choice(): return choice func get_response(): return response class ChoiceWithJump: var choice var target func _init(c, t): choice = c target = t func get_choice(): return choice func get_target(): return target class ChoiceSegment: var choices func _init(): choices = [] func add_choice(choice, response): var new_choice = Choice.new(choice, response) choices.push_back(new_choice) func add_choice_with_jump(choice, target): var new_choice = ChoiceWithJump.new(choice, target) choices.push_back(new_choice) func get_choices(): return choices class LabelSegment: var label func _init(l): label = l class GotoSegment: var target func _init(t): target = t class RandomGotoSegment: var targets func _init(t): targets = t class ConditionalGotoSegment extends GotoSegment: var variable var expected_value func _init(t, v, e).(t): variable = v expected_value = e func test(registry): if variable in registry: var v = registry[variable] return v else: return false class CallSegment: var fname var args func _init(f, a = null): fname = f args = a func call(l, r): if l.has(fname): if args != null: return l[fname].call_func(args) else: return l[fname].call_func([]) elif r.has(fname): if args != null: return r[fname].call_func(args) else: return r[fname].call_func([]) class YieldCallSegment extends CallSegment: var started var finished func _init(f, a = null).(f, a): started = false func call(l, r): started = true finished = false return .call(l, r) func get_content(): return "..." class FlagSetSegment: var flag var value func _init(f, v): flag = f value = v func set(registry): registry[flag] = value class Dialog: var segment_index = -1 var segments # goto label dictionary var labels # local registry var locals # dependency, registry of stuff var registry # yield state var is_yielding func _init(): is_yielding = false segments = [] labels = {} locals = {} func get_local_registry(): return locals func set_registry(r): registry = r func add_segment(s): if s is GotoSegment: if not s.target in labels: labels[s.target] = -1 elif s is RandomGotoSegment: for t in s.targets: if not t in labels: labels[t] = -1 elif s is LabelSegment: var sz = segments.size() if not labels.has(s.label) or labels[s.label] == -1: # if OS.is_debug_build(): print("added label: ", s.label, " to: ", sz) labels[s.label] = sz else: printerr("ERROR: label with name: %s already registered at %d!" % [s.label, labels[s.label]]) assert(labels[s.label] != -1) segments.push_back(s) func push_instruction(): if is_eof(): return null var new_segment = segments[segment_index] while (new_segment is GotoSegment \ or new_segment is RandomGotoSegment \ or new_segment is LabelSegment \ or new_segment is CallSegment \ or new_segment is FlagSetSegment) and not is_yielding and not is_eof(): new_segment = segments[segment_index] if new_segment is ConditionalGotoSegment: if new_segment.test(registry): # if true, jump to offset var offset = labels[new_segment.target] if offset == -1: printerr("could not find label: %s" % new_segment.target) assert(offset != -1) # if OS.is_debug_build(): print("conditionally jumped to offset: ", offset) segment_index = offset else: # else skip segment_index += 1 elif new_segment is RandomGotoSegment: var rand_idx = floor(rand_range(0, new_segment.targets.size())) var rand_target = new_segment.targets[rand_idx] var offset = labels[rand_target] if offset == -1: printerr("could not find label: %s" % rand_target) assert(offset != -1) # if OS.is_debug_build(): print("random jumped to: ", offset, " with: ", rand_target) segment_index = offset elif new_segment is GotoSegment: var offset = labels[new_segment.target] if offset == -1: printerr("could not find label: %s" % new_segment.target) assert(offset != -1) # if OS.is_debug_build(): print("jumped to: ", offset, " with: ", new_segment.target, " at: ", segment_index) segment_index = offset elif new_segment is YieldCallSegment: if not new_segment.started: var state = new_segment.call(locals, registry) if state != null: var obj = state[0] var sgn = state[1] is_yielding = true _do_yield_wait(obj, sgn, new_segment) else: segment_index += 1 elif new_segment is CallSegment: new_segment.call(locals, registry) segment_index += 1 elif new_segment is FlagSetSegment: new_segment.set(registry) segment_index += 1 elif new_segment is LabelSegment: segment_index += 1 if not is_eof(): return segments[segment_index] else: return null func advance(): if not is_yielding: segment_index += 1 return push_instruction() else: return push_instruction() # external jump, is fudge func jump(target): segment_index = labels[target] return push_instruction() func is_eof(): return segment_index >= segments.size() func reset(): segment_index = -1 func _do_yield_wait(obj, sgn, segment): yield(obj, sgn) segment.finished = true is_yielding = false # is OS.is_debug_build(): printt("Dialog VM: %s %s %s" % [obj, sgn, "completed"])