571 lines
15 KiB
GDScript3
571 lines
15 KiB
GDScript3
|
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 <SUBJECT, EXPR>
|
||
|
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 <SUBJECT>
|
||
|
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 <SUBJECT, EXPRESSION> 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 <SUBJECT> 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
|
||
|
printt("Dialog VM: %s %s %s" % [obj, sgn, "completed"])
|