godot-vn/dialog_vm.gd

571 lines
15 KiB
GDScript

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"])