2095 lines
63 KiB
GDScript
2095 lines
63 KiB
GDScript
# warning-ignore-all:shadowed_variable
|
|
# warning-ignore-all:unused_class_variable
|
|
# warning-ignore-all:unused_signal
|
|
# ############################################################################ #
|
|
# Copyright © 2015-2021 inkle Ltd.
|
|
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
|
|
# All Rights Reserved
|
|
#
|
|
# This file is part of inkgd.
|
|
# inkgd is licensed under the terms of the MIT license.
|
|
# ############################################################################ #
|
|
|
|
extends InkObject
|
|
|
|
class_name InkStory
|
|
|
|
const INK_VERSION_CURRENT := 21
|
|
const INK_VERSION_MINIMUM_COMPATIBLE := 18
|
|
|
|
# ############################################################################ #
|
|
# Imports
|
|
# ############################################################################ #
|
|
|
|
var InkStopWatch := preload("res://addons/inkgd/runtime/extra/stopwatch.gd") as GDScript
|
|
var InkProfiler := preload("res://addons/inkgd/runtime/profiler.gd") as GDScript
|
|
|
|
var InkSimpleJSON := preload("res://addons/inkgd/runtime/simple_json.gd") as GDScript
|
|
var InkStringSet := preload("res://addons/inkgd/runtime/extra/string_set.gd") as GDScript
|
|
var InkListDefinitionsOrigin := preload("res://addons/inkgd/runtime/lists/list_definitions_origin.gd") as GDScript
|
|
|
|
var InkPointer := preload("res://addons/inkgd/runtime/structs/pointer.gd") as GDScript
|
|
|
|
var StoryErrorMetadata := preload("res://addons/inkgd/runtime/extra/story_error_metadata.gd") as GDScript
|
|
|
|
# ############################################################################ #
|
|
|
|
var InkValue := load("res://addons/inkgd/runtime/values/value.gd") as GDScript
|
|
var InkIntValue := load("res://addons/inkgd/runtime/values/int_value.gd") as GDScript
|
|
var InkStringValue := load("res://addons/inkgd/runtime/values/string_value.gd") as GDScript
|
|
var InkVariablePointerValue := load("res://addons/inkgd/runtime/values/variable_pointer_value.gd") as GDScript
|
|
var InkListValue := load("res://addons/inkgd/runtime/values/list_value.gd") as GDScript
|
|
|
|
var InkList := load("res://addons/inkgd/runtime/lists/ink_list.gd") as GDScript
|
|
|
|
var InkStoryState := load("res://addons/inkgd/runtime/story_state.gd") as GDScript
|
|
|
|
# ############################################################################ #
|
|
|
|
var current_choices: Array: # Array<Choice>
|
|
get:
|
|
var choices: Array = [] # Array<Choice>
|
|
|
|
for c in self._state.current_choices:
|
|
if !c.is_invisible_default:
|
|
c.index = choices.size()
|
|
choices.append(c)
|
|
|
|
return choices
|
|
|
|
|
|
var current_text: # String?
|
|
get:
|
|
if async_we_cant("call currentText since it's a work in progress"):
|
|
return null
|
|
|
|
return self.state.current_text
|
|
|
|
# Array?
|
|
var current_tags: # Array<String>
|
|
get:
|
|
if async_we_cant("call currentTags since it's a work in progress"):
|
|
return null
|
|
|
|
return self.state.current_tags
|
|
|
|
var current_errors: # Array<String>?
|
|
get: return self.state.current_errors
|
|
|
|
var current_warnings: # Array<String>?
|
|
get: return self.state.current_warnings
|
|
|
|
var current_flow_name: String:
|
|
get: return self.state.current_flow_name
|
|
|
|
var has_error: bool:
|
|
get: return self.state.has_error
|
|
|
|
var has_warning: bool:
|
|
get: return self.state.has_warning
|
|
|
|
var variables_state: InkVariablesState:
|
|
get: return self.state.variables_state
|
|
|
|
var list_definitions: # ListDefinitionsOrigin?
|
|
get: return self._list_definitions
|
|
|
|
var state: InkStoryState:
|
|
get: return self._state
|
|
|
|
signal on_error(message, type)
|
|
|
|
signal on_did_continue()
|
|
|
|
signal on_make_choice(choice)
|
|
|
|
signal on_evaluate_function(function_name, arguments)
|
|
|
|
signal on_complete_evaluate_function(function_name, arguments, text_output, result)
|
|
|
|
signal on_choose_path_string(path, arguments)
|
|
|
|
|
|
func start_profiling() -> InkProfiler:
|
|
if async_we_cant ("Start Profiling"):
|
|
return null
|
|
|
|
_profiler = InkProfiler.new()
|
|
return _profiler
|
|
|
|
|
|
func end_profiling() -> void:
|
|
_profiler = null
|
|
|
|
|
|
# (InkContainer, Array<ListDefinition>) -> void
|
|
func _init_with(content_container: InkContainer, lists = null, runtime = null):
|
|
_initialize_runtime(runtime)
|
|
self._main_content_container = content_container
|
|
|
|
if lists != null:
|
|
self._list_definitions = InkListDefinitionsOrigin.new(lists)
|
|
|
|
self._externals = {} # Dictionary<String, ExternalFunctionDef>
|
|
|
|
|
|
func _init(json_string: String, runtime = null):
|
|
_init_with(null, null, runtime)
|
|
|
|
var root_object = InkSimpleJSON.text_to_dictionary(json_string)
|
|
|
|
var version_obj = root_object["inkVersion"]
|
|
if version_obj == null:
|
|
InkUtils.throw_exception(
|
|
"ink version number not found. " +
|
|
"Are you sure it's a valid .ink.json file?"
|
|
)
|
|
return
|
|
|
|
var format_from_file = int(version_obj)
|
|
if format_from_file > INK_VERSION_CURRENT:
|
|
InkUtils.throw_exception(
|
|
"Version of ink used to build story was newer " +
|
|
"than the current version of the engine"
|
|
)
|
|
return
|
|
elif format_from_file < INK_VERSION_MINIMUM_COMPATIBLE:
|
|
InkUtils.throw_exception(
|
|
"Version of ink used to build story is too old " +
|
|
"to be loaded by this version of the engine"
|
|
)
|
|
return
|
|
elif format_from_file != INK_VERSION_CURRENT:
|
|
print(
|
|
"[Ink] [WARNING] Version of ink used to build story doesn't match " +
|
|
"current version of engine. Non-critical, but recommend synchronising."
|
|
)
|
|
|
|
var root_token = root_object["root"]
|
|
if root_token == null:
|
|
InkUtils.throw_exception(
|
|
"Root node for ink not found. Are you sure it's a valid .ink.json file?"
|
|
)
|
|
return
|
|
|
|
if root_object.has("listDefs"):
|
|
self._list_definitions = self.StaticJSON.jtoken_to_list_definitions(root_object["listDefs"])
|
|
|
|
self._main_content_container = InkUtils.as_or_null(
|
|
self.StaticJSON.jtoken_to_runtime_object(root_token),
|
|
"InkContainer"
|
|
)
|
|
|
|
self.reset_state()
|
|
|
|
|
|
# () -> String
|
|
func to_json() -> String:
|
|
var writer: InkSimpleJSON.Writer = InkSimpleJSON.Writer.new()
|
|
to_json_with_writer(writer)
|
|
return writer._to_string()
|
|
|
|
|
|
func write_root_property(writer: InkSimpleJSON.Writer) -> void:
|
|
self.StaticJSON.write_runtime_container(writer, self._main_content_container)
|
|
|
|
|
|
func to_json_with_writer(writer: InkSimpleJSON.Writer) -> void:
|
|
writer.write_object_start()
|
|
|
|
writer.write_property("inkVersion", INK_VERSION_CURRENT)
|
|
|
|
writer.write_property("root", Callable(self, "write_root_property"))
|
|
|
|
if self._list_definitions != null:
|
|
writer.write_property_start("listDefs")
|
|
writer.write_object_start()
|
|
|
|
for def in self._list_definitions.lists:
|
|
writer.write_property_start(def.name)
|
|
writer.write_object_start()
|
|
|
|
for item_to_val_key in def.items:
|
|
var item = InkListItem.from_serialized_key(item_to_val_key)
|
|
var val = def.items[item_to_val_key]
|
|
writer.write_property(item.item_name, val)
|
|
|
|
writer.write_object_end()
|
|
writer.write_property_end()
|
|
|
|
writer.write_object_end()
|
|
writer.write_property_end()
|
|
|
|
writer.write_object_end()
|
|
|
|
|
|
# () -> void
|
|
func reset_state() -> void:
|
|
if async_we_cant ("ResetState"):
|
|
return
|
|
|
|
self._state = InkStoryState.new(self, self._ink_runtime)
|
|
self._state.variables_state.connect("variable_changed", Callable(self, "variable_state_did_change_event"))
|
|
|
|
self.reset_globals()
|
|
|
|
|
|
# () -> void
|
|
func reset_errors() -> void:
|
|
self._state.reset_errors()
|
|
|
|
|
|
# () -> void
|
|
func reset_callstack() -> void:
|
|
if async_we_cant("ResetCallstack"):
|
|
return
|
|
|
|
self._state.force_end()
|
|
|
|
|
|
# () -> void
|
|
func reset_globals() -> void:
|
|
if (self._main_content_container.named_content.has("global decl")):
|
|
var original_pointer = self.state.current_pointer
|
|
|
|
self.choose_path(InkPath.new_with_components_string("global decl"), false)
|
|
|
|
self.continue_internal()
|
|
|
|
self.state.current_pointer = original_pointer
|
|
|
|
self.state.variables_state.snapshot_default_globals()
|
|
|
|
|
|
func switch_flow(flow_name: String) -> void:
|
|
if async_we_cant("SwitchFlow"):
|
|
return
|
|
|
|
if self._async_saving:
|
|
InkUtils.throw_exception("Story is already in background saving mode, can't switch flow to " + flow_name)
|
|
|
|
self.state.switch_flow_internal(flow_name)
|
|
|
|
|
|
func remove_flow(flow_name: String) -> void:
|
|
self.state.remove_flow_internal(flow_name)
|
|
|
|
|
|
func switch_to_default_flow() -> void:
|
|
self.state.switch_to_default_flow_internal()
|
|
|
|
|
|
func continue_story() -> String:
|
|
self.continue_async(0)
|
|
return self.current_text
|
|
|
|
|
|
var can_continue: bool: get = get_continue
|
|
func get_continue() -> bool:
|
|
return self.state.can_continue
|
|
|
|
|
|
var async_continue_complete: bool: get = get_async_continue_complete
|
|
func get_async_continue_complete() -> bool:
|
|
return !self._async_continue_active
|
|
|
|
|
|
func continue_async(millisecs_limit_async: float):
|
|
if !self._has_validated_externals:
|
|
self.validate_external_bindings()
|
|
|
|
continue_internal(millisecs_limit_async)
|
|
|
|
|
|
func continue_internal(millisecs_limit_async: float = 0) -> void:
|
|
if _profiler != null:
|
|
_profiler.pre_continue()
|
|
|
|
var is_async_time_limited = millisecs_limit_async > 0
|
|
|
|
self._recursive_continue_count += 1
|
|
|
|
if !self._async_continue_active:
|
|
self._async_continue_active = is_async_time_limited
|
|
|
|
if !self.can_continue:
|
|
InkUtils.throw_exception("Can't continue - should check canContinue before calling Continue")
|
|
return
|
|
|
|
self._state.did_safe_exit = false
|
|
self._state.reset_output()
|
|
|
|
if self._recursive_continue_count == 1:
|
|
self._state.variables_state.batch_observing_variable_changes = true
|
|
|
|
var duration_stopwatch = InkStopWatch.new()
|
|
duration_stopwatch.start()
|
|
|
|
var output_stream_ends_in_newline = false
|
|
self._saw_lookahead_unsafe_function_after_newline = false
|
|
|
|
# In the original code, exceptions raised during 'continue_single_step()'
|
|
# are catched and added to the error array. Since exceptions don't exist
|
|
# in GDScript, they are recorded instead. See 'ink_runtime.gd' for more
|
|
# information.
|
|
self._enable_story_exception_recording(true)
|
|
var first_time = true
|
|
while (first_time || self.can_continue):
|
|
first_time = false
|
|
|
|
output_stream_ends_in_newline = self.continue_single_step()
|
|
var recorded_exceptions = _get_and_clear_recorded_story_exceptions()
|
|
if recorded_exceptions.size() > 0:
|
|
for error in recorded_exceptions:
|
|
add_story_error(error)
|
|
break
|
|
|
|
if output_stream_ends_in_newline:
|
|
break
|
|
|
|
if _async_continue_active && duration_stopwatch.elapsed_milliseconds > millisecs_limit_async:
|
|
break
|
|
|
|
self._enable_story_exception_recording(false)
|
|
duration_stopwatch.stop()
|
|
|
|
if output_stream_ends_in_newline || !self.can_continue:
|
|
|
|
if self._state_snapshot_at_last_newline != null:
|
|
self.restore_state_snapshot()
|
|
|
|
if !self.can_continue:
|
|
if self.state.callstack.can_pop_thread:
|
|
add_error("Thread available to pop, threads should always be flat by the end of evaluation?")
|
|
|
|
if self.state.generated_choices.size() == 0 && !self.state.did_safe_exit && self._temporary_evaluation_container == null:
|
|
if self.state.callstack.can_pop_type(Ink.PushPopType.TUNNEL):
|
|
add_error("unexpectedly reached end of content. Do you need a '->->' to return from a tunnel?")
|
|
elif self.state.callstack.can_pop_type(Ink.PushPopType.FUNCTION):
|
|
add_error("unexpectedly reached end of content. Do you need a '~ return'?")
|
|
elif !self.state.callstack.can_pop:
|
|
add_error("ran out of content. Do you need a '-> DONE' or '-> END'?")
|
|
else:
|
|
add_error("unexpectedly reached end of content for unknown reason. Please debug compiler!")
|
|
|
|
self.state.did_safe_exit = false
|
|
self._saw_lookahead_unsafe_function_after_newline = false
|
|
|
|
if _recursive_continue_count == 1:
|
|
_state.variables_state.batch_observing_variable_changes = false
|
|
|
|
self._async_continue_active = false
|
|
emit_signal("on_did_continue")
|
|
|
|
self._recursive_continue_count -= 1
|
|
|
|
if _profiler != null:
|
|
_profiler.post_continue()
|
|
|
|
if self.state.has_error || self.state.has_warning:
|
|
if !self.get_signal_connection_list("on_error").is_empty():
|
|
if self.state.has_error:
|
|
for err in self.state.current_errors:
|
|
emit_signal("on_error", err, Ink.ErrorType.ERROR)
|
|
|
|
if self.state.has_warning:
|
|
for err in self.state.current_warnings:
|
|
emit_signal("on_error", err, Ink.ErrorType.WARNING)
|
|
|
|
self.reset_errors()
|
|
else:
|
|
var exception = "Ink had "
|
|
|
|
if self.state.has_error:
|
|
exception += str(self.state.current_errors.size())
|
|
exception += " error" if self.state.current_errors.size() == 1 else " errors"
|
|
if self.state.has_warning:
|
|
exception += " and "
|
|
|
|
if self.state.has_warning:
|
|
exception += str(self.state.current_warnings.size())
|
|
exception += " warning" if self.state.current_warnings.size() == 1 else " warnings"
|
|
|
|
exception += ". It is strongly suggested that you assign an error handler to story.onError. The first issue was: "
|
|
exception += self.state.current_errors[0] if self.state.has_error else self.state.current_warnings[0]
|
|
|
|
# If you get this exception, please connect an error handler to the appropriate signal: "on_error".
|
|
self._throw_story_exception(exception)
|
|
|
|
|
|
func continue_single_step() -> bool:
|
|
if _profiler != null:
|
|
_profiler.pre_step()
|
|
|
|
self.step()
|
|
|
|
if _profiler != null:
|
|
_profiler.post_step()
|
|
|
|
|
|
if !self.can_continue && !self.state.callstack.element_is_evaluate_from_game:
|
|
self.try_follow_default_invisible_choice()
|
|
|
|
|
|
if _profiler != null:
|
|
_profiler.pre_snapshot()
|
|
|
|
if !self.state.in_string_evaluation:
|
|
if self._state_snapshot_at_last_newline != null:
|
|
|
|
var change = calculate_newline_output_state_change(
|
|
self._state_snapshot_at_last_newline.current_text, self.state.current_text,
|
|
self._state_snapshot_at_last_newline.current_tags.size(), self.state.current_tags.size()
|
|
)
|
|
|
|
if change == OutputStateChange.EXTENDED_BEYOND_NEWLINE || self._saw_lookahead_unsafe_function_after_newline:
|
|
self.restore_state_snapshot()
|
|
|
|
return true
|
|
elif change == OutputStateChange.NEWLINE_REMOVED:
|
|
self.discard_snapshot()
|
|
|
|
if self.state.output_stream_ends_in_newline:
|
|
if self.can_continue:
|
|
if self._state_snapshot_at_last_newline == null:
|
|
self.state_snapshot()
|
|
else:
|
|
self.discard_snapshot()
|
|
|
|
if _profiler != null:
|
|
_profiler.post_snapshot()
|
|
|
|
return false
|
|
|
|
|
|
enum OutputStateChange {
|
|
NO_CHANGE,
|
|
EXTENDED_BEYOND_NEWLINE,
|
|
NEWLINE_REMOVED
|
|
}
|
|
|
|
|
|
# (String, String, int, int) -> OutputStateChange
|
|
func calculate_newline_output_state_change(
|
|
prev_text: String,
|
|
curr_text: String,
|
|
prev_tag_count: int,
|
|
curr_tag_count: int
|
|
) -> int:
|
|
var newline_still_exists = curr_text.length() >= prev_text.length() && prev_text.length() > 0 && curr_text[prev_text.length() - 1] == "\n"
|
|
if (prev_tag_count == curr_tag_count && prev_text.length() == curr_text.length() && newline_still_exists):
|
|
return OutputStateChange.NO_CHANGE
|
|
|
|
if !newline_still_exists:
|
|
return OutputStateChange.NEWLINE_REMOVED
|
|
|
|
if curr_tag_count > prev_tag_count:
|
|
return OutputStateChange.EXTENDED_BEYOND_NEWLINE
|
|
|
|
var i = prev_text.length()
|
|
while i < curr_text.length():
|
|
var c = curr_text[i]
|
|
|
|
if c != " " && c != "\t":
|
|
return OutputStateChange.EXTENDED_BEYOND_NEWLINE
|
|
|
|
i += 1
|
|
|
|
return OutputStateChange.NO_CHANGE
|
|
|
|
|
|
func continue_story_maximally() -> String:
|
|
if async_we_cant("ContinueMaximally"):
|
|
return ""
|
|
|
|
var _str = ""
|
|
|
|
while (self.can_continue):
|
|
_str += self.continue_story()
|
|
|
|
return _str
|
|
|
|
|
|
func content_at_path(path: InkPath) -> InkSearchResult:
|
|
return self.main_content_container.content_at_path(path)
|
|
|
|
|
|
func knot_container_with_name(name: String) -> InkContainer:
|
|
if self.main_content_container.named_content.has(name):
|
|
return InkUtils.as_or_null(self.main_content_container.named_content[name], "InkContainer")
|
|
|
|
return null
|
|
|
|
|
|
func pointer_at_path(path: InkPath) -> InkPointer:
|
|
if (path.length == 0):
|
|
return InkPointer.null_pointer
|
|
|
|
var p = InkPointer.new()
|
|
|
|
var path_length_to_use = path.length
|
|
|
|
var result = null # SearchResult
|
|
if (path.last_component.is_index):
|
|
path_length_to_use = path.length - 1
|
|
result = self.main_content_container.content_at_path(path, 0, path_length_to_use)
|
|
p = InkPointer.new(result.container, path.last_component.index)
|
|
else:
|
|
result = self.main_content_container.content_at_path(path)
|
|
p = InkPointer.new(result.container, -1)
|
|
|
|
if result.obj == null || result.obj == self.main_content_container && path_length_to_use > 0:
|
|
error(
|
|
"Failed to find content at path '%s', " % path._to_string() +
|
|
"and no approximation of it was possible."
|
|
)
|
|
elif result.approximate:
|
|
warning(
|
|
"Failed to find content at path '%s', " % path +
|
|
"so it was approximated to: '%s'." % result.obj.path._to_string()
|
|
)
|
|
|
|
return p
|
|
|
|
|
|
func state_snapshot() -> void:
|
|
self._state_snapshot_at_last_newline = self._state
|
|
self._state = self._state.copy_and_start_patching()
|
|
|
|
|
|
func restore_state_snapshot() -> void:
|
|
self._state_snapshot_at_last_newline.restore_after_patch()
|
|
|
|
self._state = self._state_snapshot_at_last_newline
|
|
self._state_snapshot_at_last_newline = null
|
|
|
|
if !self._async_saving:
|
|
self._state.apply_any_patch()
|
|
|
|
|
|
func discard_snapshot() -> void:
|
|
if !self._async_saving:
|
|
self._state.apply_any_patch()
|
|
|
|
self._state_snapshot_at_last_newline = null
|
|
|
|
|
|
func copy_state_for_background_thread_save() -> InkStoryState:
|
|
if async_we_cant("start saving on a background thread"):
|
|
return null
|
|
|
|
if self._async_saving:
|
|
InkUtils.throw_exception(
|
|
"Story is already in background saving mode, " +
|
|
"can't call CopyStateForBackgroundThreadSave again!"
|
|
)
|
|
return null
|
|
|
|
var state_to_save = self._state
|
|
self._state = self._state.copy_and_start_patching()
|
|
self._async_saving = true
|
|
|
|
return state_to_save
|
|
|
|
|
|
func background_save_complete() -> void:
|
|
if self._state_snapshot_at_last_newline == null:
|
|
_state.apply_any_patch()
|
|
|
|
self._async_saving = false
|
|
|
|
|
|
func step() -> void:
|
|
var should_add_to_stream = true
|
|
|
|
var pointer = self.state.current_pointer
|
|
if pointer.is_null:
|
|
return
|
|
|
|
var container_to_enter = InkUtils.as_or_null(pointer.resolve(), "InkContainer")
|
|
while (container_to_enter):
|
|
self.visit_container(container_to_enter, true)
|
|
|
|
if container_to_enter.content.size() == 0:
|
|
break
|
|
|
|
pointer = InkPointer.start_of(container_to_enter)
|
|
container_to_enter = InkUtils.as_or_null(pointer.resolve(), "InkContainer")
|
|
|
|
self.state.current_pointer = pointer
|
|
|
|
if _profiler != null:
|
|
_profiler.step(state.callstack)
|
|
|
|
var current_content_obj = pointer.resolve()
|
|
var is_logic_or_flow_control = perform_logic_and_flow_control(current_content_obj)
|
|
|
|
if self.state.current_pointer.is_null:
|
|
return
|
|
|
|
if is_logic_or_flow_control:
|
|
should_add_to_stream = false
|
|
|
|
var choice_point = InkUtils.as_or_null(current_content_obj, "ChoicePoint")
|
|
if choice_point:
|
|
var choice = process_choice(choice_point)
|
|
if choice:
|
|
self.state.generated_choices.append(choice)
|
|
|
|
current_content_obj = null
|
|
should_add_to_stream = false
|
|
|
|
if InkUtils.is_ink_class(current_content_obj, "InkContainer"):
|
|
should_add_to_stream = false
|
|
|
|
if should_add_to_stream:
|
|
var var_pointer = InkUtils.as_or_null(current_content_obj, "VariablePointerValue")
|
|
if var_pointer && var_pointer.context_index == -1:
|
|
var context_idx = self.state.callstack.context_for_variable_named(var_pointer.variable_name)
|
|
current_content_obj = InkVariablePointerValue.new_with_context(var_pointer.variable_name, context_idx)
|
|
|
|
if self.state.in_expression_evaluation:
|
|
self.state.push_evaluation_stack(current_content_obj)
|
|
else:
|
|
self.state.push_to_output_stream(current_content_obj)
|
|
|
|
self.next_content()
|
|
|
|
var control_cmd = InkUtils.as_or_null(current_content_obj, "ControlCommand")
|
|
if control_cmd && control_cmd.command_type == InkControlCommand.CommandType.START_THREAD:
|
|
self.state.callstack.push_thread()
|
|
|
|
|
|
func visit_container(container: InkContainer, at_start: bool) -> void:
|
|
if !container.counting_at_start_only || at_start:
|
|
if container.visits_should_be_counted:
|
|
self.state.increment_visit_count_for_container(container)
|
|
|
|
if container.turn_index_should_be_counted:
|
|
self.state.record_turn_index_visit_to_container(container)
|
|
|
|
|
|
var _prev_containers = [] # Array<Container>
|
|
func visit_changed_containers_due_to_divert() -> void:
|
|
var previous_pointer = self.state.previous_pointer
|
|
var pointer = self.state.current_pointer
|
|
|
|
if pointer.is_null || pointer.index == -1:
|
|
return
|
|
|
|
self._prev_containers.clear()
|
|
if !previous_pointer.is_null:
|
|
var prev_ancestor = InkUtils.as_or_null(previous_pointer.resolve(), "InkContainer")
|
|
prev_ancestor = prev_ancestor if prev_ancestor else InkUtils.as_or_null(previous_pointer.container, "InkContainer")
|
|
while prev_ancestor:
|
|
self._prev_containers.append(prev_ancestor)
|
|
prev_ancestor = InkUtils.as_or_null(prev_ancestor.parent, "InkContainer")
|
|
|
|
var current_child_of_container = pointer.resolve()
|
|
|
|
if current_child_of_container == null: return
|
|
|
|
var current_container_ancestor = InkUtils.as_or_null(current_child_of_container.parent, "InkContainer")
|
|
|
|
var all_children_entered_at_start = true
|
|
while current_container_ancestor && (self._prev_containers.find(current_container_ancestor) < 0 || current_container_ancestor.counting_at_start_only):
|
|
|
|
var entering_at_start = (current_container_ancestor.content.size() > 0 &&
|
|
current_child_of_container == current_container_ancestor.content[0] &&
|
|
all_children_entered_at_start)
|
|
|
|
if !entering_at_start:
|
|
all_children_entered_at_start = false
|
|
|
|
self.visit_container(current_container_ancestor, entering_at_start)
|
|
|
|
current_child_of_container = current_container_ancestor
|
|
current_container_ancestor = InkUtils.as_or_null(current_container_ancestor.parent, "InkContainer")
|
|
|
|
|
|
# The original implementation would return the choice string and update the
|
|
# array of tags passed in parameter. Since in/out (ref) parameters are not supported
|
|
# in GDScript, the method returns a tuple (array) instead.
|
|
# (Array<String>) -> [String, Array<String>]
|
|
func pop_choice_string_and_tags(tags) -> Array:
|
|
var choice_only_str_val = InkUtils.cast(self.state.pop_evaluation_stack(), "StringValue")
|
|
|
|
while self.state.evaluation_stack.size() > 0 and InkUtils.is_ink_class(state.peek_evaluation_stack(), "Tag"):
|
|
if tags == null:
|
|
tags = []
|
|
var tag = InkUtils.cast(self.state.pop_evaluation_stack(), "Tag")
|
|
tags.insert(0, tag.text)
|
|
|
|
return [choice_only_str_val.value, tags]
|
|
|
|
|
|
func process_choice(choice_point: InkChoicePoint) -> InkChoice:
|
|
var show_choice := true
|
|
|
|
if choice_point.has_condition:
|
|
var condition_value = self.state.pop_evaluation_stack()
|
|
if !self.is_truthy(condition_value):
|
|
show_choice = false
|
|
|
|
var start_text := ""
|
|
var choice_only_text := ""
|
|
var tags = null
|
|
|
|
if choice_point.has_choice_only_content:
|
|
var choice_strings_and_tags = self.pop_choice_string_and_tags(tags)
|
|
choice_only_text = choice_strings_and_tags[0]
|
|
tags = choice_strings_and_tags[1]
|
|
|
|
if choice_point.has_start_content:
|
|
var choice_strings_and_tags = self.pop_choice_string_and_tags(tags)
|
|
start_text = choice_strings_and_tags[0]
|
|
tags = choice_strings_and_tags[1]
|
|
|
|
if choice_point.once_only:
|
|
var visit_count = self.state.visit_count_for_container(choice_point.choice_target)
|
|
if visit_count > 0:
|
|
show_choice = false
|
|
|
|
if !show_choice:
|
|
return null
|
|
|
|
var choice = InkChoice.new()
|
|
choice.target_path = choice_point.path_on_choice
|
|
choice.source_path = choice_point.path._to_string()
|
|
choice.is_invisible_default = choice_point.is_invisible_default
|
|
choice.tags = tags
|
|
choice.thread_at_generation = self.state.callstack.fork_thread()
|
|
|
|
choice.text = InkUtils.trim(start_text + choice_only_text, [" ", "\t"])
|
|
|
|
return choice
|
|
|
|
|
|
func is_truthy(obj: InkObject) -> bool:
|
|
var truthy = false
|
|
if InkUtils.is_ink_class(obj, "Value"):
|
|
var val = obj
|
|
|
|
if InkUtils.is_ink_class(obj, "DivertTargetValue"):
|
|
var div_target = val
|
|
error(str("Shouldn't use a divert target (to ", div_target.target_path._to_string(),
|
|
") as a conditional value. Did you intend a function call 'likeThis()'",
|
|
" or a read count check 'likeThis'? (no arrows)"))
|
|
return false
|
|
|
|
return val.is_truthy
|
|
|
|
return truthy
|
|
|
|
|
|
func perform_logic_and_flow_control(content_obj: InkObject) -> bool:
|
|
if (content_obj == null):
|
|
return false
|
|
|
|
if InkUtils.is_ink_class(content_obj, "Divert"):
|
|
var current_divert = content_obj
|
|
|
|
if current_divert.is_conditional:
|
|
var condition_value = self.state.pop_evaluation_stack()
|
|
|
|
if !self.is_truthy(condition_value):
|
|
return true
|
|
|
|
if current_divert.has_variable_target:
|
|
var var_name = current_divert.variable_divert_name
|
|
var var_contents = self.state.variables_state.get_variable_with_name(var_name)
|
|
|
|
if var_contents == null:
|
|
error(str("Tried to divert using a target from a variable that could not be found (",
|
|
var_name, ")"))
|
|
return false
|
|
elif !InkUtils.is_ink_class(var_contents, "DivertTargetValue"):
|
|
var int_content = InkUtils.as_or_null(var_contents, "IntValue")
|
|
|
|
var error_message = str("Tried to divert to a target from a variable,",
|
|
"but the variable (", var_name,
|
|
") didn't contain a divert target, it ")
|
|
if int_content && int_content.value == 0:
|
|
error_message += "was empty/null (the value 0)."
|
|
else:
|
|
error_message += "contained '" + var_contents + "'."
|
|
|
|
error(error_message)
|
|
return false
|
|
|
|
var target = var_contents
|
|
self.state.diverted_pointer = self.pointer_at_path(target.target_path)
|
|
|
|
elif current_divert.is_external:
|
|
call_external_function(current_divert.target_path_string, current_divert.external_args)
|
|
return true
|
|
else:
|
|
self.state.diverted_pointer = current_divert.target_pointer
|
|
|
|
if current_divert.pushes_to_stack:
|
|
self.state.callstack.push(
|
|
current_divert.stack_push_type,
|
|
0,
|
|
self.state.output_stream.size()
|
|
)
|
|
|
|
if self.state.diverted_pointer.is_null && !current_divert.is_external:
|
|
if current_divert && current_divert.debug_metadata != null && current_divert.debug_metadata.source_name != null:
|
|
error("Divert target doesn't exist: " + current_divert.debug_metadata.source_name)
|
|
return false
|
|
else:
|
|
error("Divert resolution failed: " + current_divert._to_string())
|
|
return false
|
|
|
|
return true
|
|
elif InkUtils.is_ink_class(content_obj, "ControlCommand"):
|
|
var eval_command = content_obj
|
|
|
|
match eval_command.command_type:
|
|
|
|
InkControlCommand.CommandType.EVAL_START:
|
|
self.__assert__(
|
|
self.state.in_expression_evaluation == false,
|
|
"Already in expression evaluation?"
|
|
)
|
|
self.state.in_expression_evaluation = true
|
|
|
|
InkControlCommand.CommandType.EVAL_END:
|
|
self.__assert__(
|
|
self.state.in_expression_evaluation == true,
|
|
"Not in expression evaluation mode"
|
|
)
|
|
self.state.in_expression_evaluation = false
|
|
|
|
InkControlCommand.CommandType.EVAL_OUTPUT:
|
|
if self.state.evaluation_stack.size() > 0:
|
|
var output = self.state.pop_evaluation_stack()
|
|
|
|
if !InkUtils.as_or_null(output, "Void"):
|
|
var text = InkStringValue.new_with(output._to_string())
|
|
self.state.push_to_output_stream(text)
|
|
|
|
InkControlCommand.CommandType.NO_OP:
|
|
pass
|
|
|
|
InkControlCommand.CommandType.DUPLICATE:
|
|
self.state.push_evaluation_stack(self.state.peek_evaluation_stack())
|
|
|
|
InkControlCommand.CommandType.POP_EVALUATED_VALUE:
|
|
self.state.pop_evaluation_stack()
|
|
|
|
InkControlCommand.CommandType.POP_FUNCTION, InkControlCommand.CommandType.POP_TUNNEL:
|
|
var is_pop_function = (
|
|
eval_command.command_type == InkControlCommand.CommandType.POP_FUNCTION
|
|
)
|
|
var pop_type = Ink.PushPopType.FUNCTION if is_pop_function else Ink.PushPopType.TUNNEL
|
|
|
|
var override_tunnel_return_target = null # DivertTargetValue
|
|
if pop_type == Ink.PushPopType.TUNNEL:
|
|
var popped = self.state.pop_evaluation_stack()
|
|
override_tunnel_return_target = InkUtils.as_or_null(popped, "DivertTargetValue")
|
|
if override_tunnel_return_target == null:
|
|
self.__assert__(
|
|
InkUtils.is_ink_class(popped, "Void"),
|
|
"Expected void if ->-> doesn't override target"
|
|
)
|
|
|
|
if self.state.try_exit_function_evaluation_from_game():
|
|
pass
|
|
elif self.state.callstack.current_element.type != pop_type || !self.state.callstack.can_pop:
|
|
var names = {} # Dictionary<Ink.PushPopType, String>
|
|
names[Ink.PushPopType.FUNCTION] = "function return statement (~ return)"
|
|
names[Ink.PushPopType.TUNNEL] = "tunnel onwards statement (->->)"
|
|
|
|
var expected = names[self.state.callstack.current_element.type]
|
|
if !self.state.callstack.can_pop:
|
|
expected = "end of flow (-> END or choice)"
|
|
|
|
var error_msg = "Found %s, when expected %s" % [names[pop_type], expected]
|
|
|
|
error(error_msg)
|
|
else:
|
|
self.state.pop_callstack()
|
|
|
|
if override_tunnel_return_target:
|
|
self.state.diverted_pointer = self.pointer_at_path(override_tunnel_return_target.target_path)
|
|
|
|
InkControlCommand.CommandType.BEGIN_STRING:
|
|
self.state.push_to_output_stream(eval_command)
|
|
|
|
self.__assert__(
|
|
self.state.in_expression_evaluation == true,
|
|
"Expected to be in an expression when evaluating a string"
|
|
)
|
|
self.state.in_expression_evaluation = false
|
|
|
|
InkControlCommand.CommandType.BEGIN_TAG:
|
|
self.state.push_to_output_stream(eval_command)
|
|
|
|
InkControlCommand.CommandType.END_TAG:
|
|
if self.state.in_string_evaluation:
|
|
var content_stack_for_tag := [] # Stack<InkObject>
|
|
var output_count_consumed := 0
|
|
|
|
var i = self.state.output_stream.size() - 1
|
|
while i >= 0:
|
|
var obj = self.state.output_stream[i]
|
|
|
|
output_count_consumed += 1
|
|
|
|
var command = InkUtils.as_or_null(obj, "ControlCommand")
|
|
if command != null:
|
|
if command.command_type == InkControlCommand.CommandType.BEGIN_TAG:
|
|
break
|
|
else:
|
|
self.error("Unexpected ControlCommand while extracting tag from choice")
|
|
break
|
|
|
|
if InkUtils.is_ink_class(obj, "StringValue"):
|
|
content_stack_for_tag.push_front(obj)
|
|
|
|
i -= 1
|
|
|
|
self.state.pop_from_output_stream(output_count_consumed)
|
|
|
|
var sb = ""
|
|
for str_val in content_stack_for_tag:
|
|
sb += str_val.value
|
|
|
|
var choice_tag = InkTag.new(self.state.clean_output_whitespace(sb))
|
|
|
|
self.state.push_evaluation_stack(choice_tag)
|
|
else:
|
|
self.state.push_to_output_stream(eval_command)
|
|
|
|
|
|
InkControlCommand.CommandType.END_STRING:
|
|
var content_stack_for_string := [] # Stack<InkObject>
|
|
var content_to_retain := [] # Stack<InkObject>
|
|
|
|
var output_count_consumed := 0
|
|
var i = self.state.output_stream.size() - 1
|
|
while (i >= 0):
|
|
var obj = self.state.output_stream[i]
|
|
|
|
output_count_consumed += 1
|
|
|
|
var command = InkUtils.as_or_null(obj, "ControlCommand")
|
|
if (command != null &&
|
|
command.command_type == InkControlCommand.CommandType.BEGIN_STRING):
|
|
break
|
|
|
|
if InkUtils.is_ink_class(obj, "Tag"):
|
|
content_to_retain.push_front(obj)
|
|
|
|
if InkUtils.is_ink_class(obj, "StringValue"):
|
|
content_stack_for_string.push_front(obj)
|
|
|
|
i -= 1
|
|
|
|
self.state.pop_from_output_stream(output_count_consumed)
|
|
|
|
for rescued_tag in content_to_retain:
|
|
state.push_to_output_stream(rescued_tag)
|
|
|
|
var _str := ""
|
|
for c in content_stack_for_string:
|
|
_str += c._to_string()
|
|
|
|
self.state.in_expression_evaluation = true
|
|
self.state.push_evaluation_stack(InkStringValue.new_with(_str))
|
|
|
|
InkControlCommand.CommandType.CHOICE_COUNT:
|
|
var choice_count = self.state.generated_choices.size()
|
|
self.state.push_evaluation_stack(InkIntValue.new_with(choice_count))
|
|
|
|
InkControlCommand.CommandType.TURNS:
|
|
self.state.push_evaluation_stack(InkIntValue.new_with(self.state.current_turn_index + 1))
|
|
|
|
InkControlCommand.CommandType.TURNS_SINCE, InkControlCommand.CommandType.READ_COUNT:
|
|
var target = self.state.pop_evaluation_stack()
|
|
if !InkUtils.is_ink_class(target, "DivertTargetValue"):
|
|
var extra_note = ""
|
|
if InkUtils.is_ink_class(target, "IntValue"):
|
|
extra_note = ". Did you accidentally pass a read count ('knot_name') instead of a target ('-> knot_name')?"
|
|
error(str("TURNS_SINCE expected a divert target (knot, stitch, label name), but saw ",
|
|
target, extra_note))
|
|
return false
|
|
|
|
var divert_target = InkUtils.as_or_null(target, "DivertTargetValue")
|
|
var container = InkUtils.as_or_null(self.content_at_path(divert_target.target_path).correct_obj, "InkContainer")
|
|
|
|
var either_count = 0
|
|
if container != null:
|
|
if eval_command.command_type == InkControlCommand.CommandType.TURNS_SINCE:
|
|
either_count = self.state.turns_since_for_container(container)
|
|
else:
|
|
either_count = self.state.visit_count_for_container(container)
|
|
else:
|
|
if eval_command.command_type == InkControlCommand.CommandType.TURNS_SINCE:
|
|
either_count = -1
|
|
else:
|
|
either_count = 0
|
|
|
|
warning(str("Failed to find container for ", eval_command._to_string(),
|
|
" lookup at ", divert_target.target_path._to_string()))
|
|
|
|
self.state.push_evaluation_stack(InkIntValue.new_with(either_count))
|
|
|
|
InkControlCommand.CommandType.RANDOM:
|
|
var max_int = InkUtils.as_or_null(self.state.pop_evaluation_stack(), "IntValue")
|
|
var min_int = InkUtils.as_or_null(self.state.pop_evaluation_stack(), "IntValue")
|
|
|
|
if min_int == null:
|
|
error("Invalid value for minimum parameter of RANDOM(min, max)")
|
|
return false
|
|
|
|
if max_int == null:
|
|
error("Invalid value for maximum parameter of RANDOM(min, max)")
|
|
return false
|
|
|
|
var random_range
|
|
if max_int.value == (1 << 63) - 1 && min_int.value == 0:
|
|
random_range = max_int.value
|
|
error(str("RANDOM was called with a range that exceeds the size that ink numbers can use."))
|
|
return false
|
|
else:
|
|
random_range = max_int.value - min_int.value + 1
|
|
if random_range <= 0:
|
|
error(str("RANDOM was called with minimum as ", min_int.value,
|
|
" and maximum as ", max_int.value, ". The maximum must be larger"))
|
|
return false
|
|
|
|
var result_seed = self.state.story_seed + self.state.previous_random
|
|
seed(result_seed)
|
|
|
|
var next_random = randi()
|
|
var chosen_value = (next_random % random_range) + min_int.value
|
|
self.state.push_evaluation_stack(InkIntValue.new_with(chosen_value))
|
|
|
|
self.state.previous_random = next_random
|
|
|
|
InkControlCommand.CommandType.SEED_RANDOM:
|
|
var _seed = InkUtils.as_or_null(self.state.pop_evaluation_stack(), "IntValue")
|
|
if _seed == null:
|
|
error("Invalid value passed to SEED_RANDOM")
|
|
return false
|
|
|
|
self.state.story_seed = _seed.value
|
|
self.state.previous_random = 0
|
|
|
|
self.state.push_evaluation_stack(InkVoid.new())
|
|
|
|
InkControlCommand.CommandType.VISIT_INDEX:
|
|
var count = self.state.visit_count_for_container(self.state.current_pointer.container) - 1
|
|
self.state.push_evaluation_stack(InkIntValue.new_with(count))
|
|
|
|
InkControlCommand.CommandType.SEQUENCE_SHUFFLE_INDEX:
|
|
var shuffle_index = self.next_sequence_shuffle_index()
|
|
self.state.push_evaluation_stack(InkIntValue.new_with(shuffle_index))
|
|
|
|
InkControlCommand.CommandType.START_THREAD:
|
|
pass
|
|
|
|
InkControlCommand.CommandType.DONE:
|
|
if self.state.callstack.can_pop_thread:
|
|
self.state.callstack.pop_thread()
|
|
else:
|
|
self.state.did_safe_exit = true
|
|
self.state.current_pointer = InkPointer.null_pointer
|
|
|
|
InkControlCommand.CommandType.END:
|
|
self.state.force_end()
|
|
|
|
InkControlCommand.CommandType.LIST_FROM_INT:
|
|
var int_val = InkUtils.as_or_null(self.state.pop_evaluation_stack(), "IntValue")
|
|
var list_name_val = InkUtils.as_or_null(self.state.pop_evaluation_stack(), "StringValue")
|
|
|
|
if int_val == null:
|
|
self._throw_story_exception(
|
|
"Passed non-integer when creating a list element from a numerical value."
|
|
)
|
|
return false
|
|
|
|
var generated_list_value = null # ListValue
|
|
|
|
var found_list_def: InkTryGetResult = self.list_definitions.try_list_get_definition(list_name_val.value)
|
|
if found_list_def.exists:
|
|
var found_item: InkTryGetResult = found_list_def.result.try_get_item_with_value(int_val.value)
|
|
if found_item.exists:
|
|
generated_list_value = InkListValue.new_with_single_item(
|
|
found_item.result,
|
|
int_val.value
|
|
)
|
|
else:
|
|
self._throw_story_exception("Failed to find LIST called %s" % list_name_val.value)
|
|
return false
|
|
|
|
if generated_list_value == null:
|
|
generated_list_value = InkListValue.new()
|
|
|
|
self.state.push_evaluation_stack(generated_list_value)
|
|
|
|
InkControlCommand.CommandType.LIST_RANGE:
|
|
var max_value = InkUtils.as_or_null(self.state.pop_evaluation_stack(), "Value")
|
|
var min_value = InkUtils.as_or_null(self.state.pop_evaluation_stack(), "Value")
|
|
|
|
var target_list = InkUtils.as_or_null(self.state.pop_evaluation_stack(), "ListValue")
|
|
|
|
if target_list == null || min_value == null || max_value == null:
|
|
self._throw_story_exception("Expected list, minimum and maximum for LIST_RANGE")
|
|
return false
|
|
|
|
var result = target_list.value.list_with_sub_range(min_value.value_object, max_value.value_object)
|
|
|
|
self.state.push_evaluation_stack(InkListValue.new_with(result))
|
|
|
|
InkControlCommand.CommandType.LIST_RANDOM:
|
|
|
|
var list_val = InkUtils.as_or_null(self.state.pop_evaluation_stack(), "ListValue")
|
|
if list_val == null:
|
|
self._throw_story_exception("Expected list for LIST_RANDOM")
|
|
return false
|
|
|
|
var list = list_val.value
|
|
|
|
var new_list = null # InkList
|
|
|
|
if list.size() == 0:
|
|
new_list = InkList.new()
|
|
else:
|
|
var result_seed = self.state.story_seed + self.state.previous_random
|
|
seed(result_seed)
|
|
|
|
var next_random = randi()
|
|
var list_item_index = next_random % list.size()
|
|
|
|
# Iterator-based code in replaced with this code:
|
|
if list_item_index < 0: list_item_index = 0
|
|
if list_item_index >= list.size(): list_item_index = list.size() - 1
|
|
|
|
var raw_random_item = list.raw_keys()[list_item_index]
|
|
var random_item = InkListItem.from_serialized_key(raw_random_item)
|
|
var random_item_value = list.get_raw(raw_random_item)
|
|
|
|
new_list = InkList.new_with_origin(random_item.origin_name, self)
|
|
new_list.set_raw(raw_random_item, random_item_value)
|
|
|
|
self.state.previous_random = next_random
|
|
|
|
self.state.push_evaluation_stack(InkListValue.new_with(new_list))
|
|
|
|
_:
|
|
error("unhandled ControlCommand: " + eval_command._to_string())
|
|
return false
|
|
|
|
return true
|
|
|
|
elif InkUtils.as_or_null(content_obj, "VariableAssignment"):
|
|
var var_ass = content_obj
|
|
var assigned_val = self.state.pop_evaluation_stack()
|
|
|
|
# This does not exist in the original codebase.
|
|
# It ensures the type is correct, so we execution
|
|
# isn't stopped when the argument has an incorrect
|
|
# type. (In the original codebase, this is handled
|
|
# through exceptions.)
|
|
if !InkUtils.is_ink_class(assigned_val, "InkObject"):
|
|
return false
|
|
|
|
self.state.variables_state.assign(var_ass, assigned_val)
|
|
|
|
return true
|
|
|
|
elif InkUtils.as_or_null(content_obj, "VariableReference"):
|
|
var var_ref = content_obj
|
|
var found_value = null # InkValue
|
|
|
|
if var_ref.path_for_count != null:
|
|
var container = var_ref.container_for_count
|
|
var count = self.state.visit_count_for_container(container)
|
|
found_value = InkIntValue.new_with(count)
|
|
else:
|
|
found_value = self.state.variables_state.get_variable_with_name(var_ref.name)
|
|
|
|
if found_value == null:
|
|
warning(str("Variable not found: '", var_ref.name,
|
|
"', using default value of 0 (false). this can ",
|
|
"happen with temporary variables if the declaration ",
|
|
"hasn't yet been hit. Globals are always given a default ",
|
|
"value on load if a value doesn't exist in the save state."))
|
|
found_value = InkIntValue.new_with(0)
|
|
|
|
self.state.push_evaluation_stack(found_value)
|
|
return true
|
|
|
|
elif InkUtils.as_or_null(content_obj, "NativeFunctionCall"):
|
|
var function = content_obj
|
|
var func_params = self.state.pop_evaluation_stack(function.number_of_parameters)
|
|
var result = function.call_with_parameters(func_params, _make_story_error_metadata())
|
|
self.state.push_evaluation_stack(result)
|
|
return true
|
|
|
|
return false
|
|
|
|
# (String, bool, Array) -> void
|
|
func choose_path_string(path, reset_callstack = true, arguments = null):
|
|
if async_we_cant("call ChoosePathString right now"):
|
|
return
|
|
|
|
emit_signal("on_choose_path_string", path, arguments)
|
|
|
|
if reset_callstack:
|
|
self.reset_callstack()
|
|
else:
|
|
if self.state.callstack.current_element.type == Ink.PushPopType.FUNCTION:
|
|
var func_detail = ""
|
|
var container = self.state.callstack.current_element.current_pointer.container
|
|
if container != null:
|
|
func_detail = "(" + container.path._to_string() + ") "
|
|
|
|
InkUtils.throw_exception(
|
|
("Story was running a function %s" % func_detail) +
|
|
("when you called ChoosePathString(%s) " % path) +
|
|
"- this is almost certainly not not what you want! Full stack trace: \n" +
|
|
self.state.callstack.callstack_trace
|
|
)
|
|
|
|
return
|
|
|
|
self.state.pass_arguments_to_evaluation_stack(arguments)
|
|
self.choose_path(InkPath.new_with_components_string(path))
|
|
|
|
|
|
func async_we_cant(activity_str):
|
|
if self._async_continue_active:
|
|
InkUtils.throw_exception(
|
|
"Can't %s. Story is in the middle of a ContinueAsync(). " % activity_str +
|
|
"Make more ContinueAsync() calls or a single Continue() call beforehand."
|
|
)
|
|
|
|
return _async_continue_active
|
|
|
|
|
|
# (InkPath, bool)
|
|
func choose_path(p, incrementing_turn_index = true):
|
|
self.state.set_chosen_path(p, incrementing_turn_index)
|
|
|
|
self.visit_changed_containers_due_to_divert()
|
|
|
|
|
|
# (int) -> void
|
|
func choose_choice_index(choice_idx):
|
|
var choices = self.current_choices
|
|
self.__assert__(
|
|
choice_idx >= 0 && choice_idx < choices.size(),
|
|
"choice out of range"
|
|
)
|
|
|
|
var choice_to_choose = choices[choice_idx]
|
|
emit_signal("on_make_choice", choice_to_choose)
|
|
|
|
self.state.callstack.current_thread = choice_to_choose.thread_at_generation
|
|
|
|
choose_path(choice_to_choose.target_path)
|
|
|
|
|
|
# (String) -> bool
|
|
func has_function(function_name: String) -> bool:
|
|
return knot_container_with_name(function_name) != null
|
|
|
|
|
|
# (String, Array<Variant>, bool) -> Variant
|
|
func evaluate_function(
|
|
function_name: String,
|
|
arguments = null,
|
|
return_text_output: bool = false
|
|
):
|
|
# Like inkjs, evaluate_function behaves differently than the C# version.
|
|
# In C#, you can pass a (second) parameter `out textOutput` to get the
|
|
# text outputted by the function. Instead, we maintain the regular signature,
|
|
# plus an optional third parameter return_text_output. If set to true, we will
|
|
# return both the text_output and the returned value, as a Dictionary.
|
|
|
|
emit_signal("on_evaluate_function", function_name, arguments)
|
|
if async_we_cant("evaluate a function"):
|
|
return
|
|
|
|
if function_name == null:
|
|
InkUtils.throw_exception("Function is null")
|
|
return null
|
|
elif function_name == "" || InkUtils.trim(function_name) == "":
|
|
InkUtils.throw_exception("Function is empty or white space.")
|
|
return null
|
|
|
|
var func_container = knot_container_with_name(function_name)
|
|
if func_container == null:
|
|
InkUtils.throw_exception("Function doesn't exist: '%s'" % function_name)
|
|
return null
|
|
|
|
var output_stream_before = self.state.output_stream.duplicate() # Array<InkObject>
|
|
_state.reset_output()
|
|
|
|
self.state.start_function_evaluation_from_game(func_container, arguments)
|
|
|
|
# TODO: Add comment - This code did not exist in upstream.
|
|
if self._ink_runtime.clear_raised_exceptions():
|
|
return null
|
|
|
|
var string_output = ""
|
|
while self.can_continue:
|
|
string_output += self.continue_story()
|
|
|
|
var text_output = string_output
|
|
|
|
_state.reset_output(output_stream_before)
|
|
|
|
var result = self.state.complete_function_evaluation_from_game()
|
|
|
|
emit_signal("on_complete_evaluate_function", function_name, arguments, text_output, result)
|
|
if return_text_output:
|
|
return { "result": result, "output": text_output }
|
|
else:
|
|
return result
|
|
|
|
|
|
# (InkContainer) -> InkObject
|
|
func evaluate_expression(expr_container: InkContainer) -> InkObject:
|
|
var start_callstack_height = self.state.callstack.elements.size()
|
|
|
|
self.state.callstack.push(Ink.PushPopType.TUNNEL)
|
|
|
|
_temporary_evaluation_container = expr_container
|
|
|
|
self.state.go_to_start()
|
|
|
|
var eval_stack_height = self.state.evaluation_stack.size()
|
|
|
|
self.continue_story()
|
|
|
|
_temporary_evaluation_container = null
|
|
|
|
if self.state.callstack.elements.size() > start_callstack_height:
|
|
self.state.pop_callstack()
|
|
|
|
var end_stack_height = self.state.evaluation_stack.size()
|
|
if end_stack_height > eval_stack_height:
|
|
return self.state.pop_evaluation_stack()
|
|
else:
|
|
return null
|
|
|
|
var allow_external_function_fallbacks = false # bool
|
|
|
|
|
|
# (String, int) -> void
|
|
func call_external_function(func_name: String, number_of_arguments: int) -> void:
|
|
var _func_def = null # ExternalFunctionDef
|
|
var fallback_function_container = null # InkContainer
|
|
|
|
if self._externals.has(func_name):
|
|
_func_def = self._externals.get(func_name)
|
|
if _func_def != null && !_func_def.lookahead_safe && self._state_snapshot_at_last_newline != null:
|
|
self._saw_lookahead_unsafe_function_after_newline = true
|
|
return
|
|
|
|
if _func_def == null:
|
|
if allow_external_function_fallbacks:
|
|
fallback_function_container = self.knot_container_with_name(func_name)
|
|
self.__assert__(
|
|
fallback_function_container != null,
|
|
"Trying to call EXTERNAL function '%s' " % func_name +
|
|
"which has not been bound, and fallback ink function" +
|
|
"could not be found."
|
|
)
|
|
|
|
self.state.callstack.push(
|
|
Ink.PushPopType.FUNCTION,
|
|
0,
|
|
self.state.output_stream.size()
|
|
)
|
|
|
|
self.state.diverted_pointer = InkPointer.start_of(fallback_function_container)
|
|
return
|
|
else:
|
|
self.__assert__(
|
|
false,
|
|
"Trying to call EXTERNAL function '%s' " % func_name +
|
|
"which has not been bound (and ink fallbacks disabled)."
|
|
)
|
|
return
|
|
|
|
var arguments = [] # Array<Variant>
|
|
var i = 0
|
|
while i < number_of_arguments:
|
|
var popped_obj = InkUtils.as_or_null(self.state.pop_evaluation_stack(), "Value")
|
|
var value_obj = popped_obj.value_object
|
|
arguments.append(value_obj)
|
|
|
|
i += 1
|
|
|
|
arguments.reverse()
|
|
|
|
var func_result = _func_def.execute(arguments)
|
|
|
|
var return_obj = null
|
|
if func_result != null:
|
|
return_obj = InkValue.create(func_result)
|
|
self.__assert__(
|
|
return_obj != null,
|
|
"Could not create ink value from returned object of type %s" % \
|
|
InkUtils.typename_of(typeof(func_result))
|
|
)
|
|
else:
|
|
return_obj = InkVoid.new()
|
|
|
|
self.state.push_evaluation_stack(return_obj)
|
|
|
|
|
|
# (String, Variant, ExternalFunctionDef, bool) -> void
|
|
func bind_external_function_general(
|
|
func_name: String,
|
|
object,
|
|
method: String,
|
|
lookahead_safe: bool = true
|
|
) -> void:
|
|
if async_we_cant("bind an external function"):
|
|
return
|
|
|
|
self.__assert__(
|
|
!_externals.has(func_name),
|
|
"Function '%s' has already been bound." % func_name
|
|
)
|
|
|
|
_externals[func_name] = ExternalFunctionDef.new(object, method, lookahead_safe)
|
|
|
|
|
|
# try_coerce not needed.
|
|
|
|
|
|
# (String, Variant, String, bool) -> void
|
|
func bind_external_function(
|
|
func_name: String,
|
|
object,
|
|
method_name: String,
|
|
lookahead_safe: bool = false
|
|
) -> void:
|
|
self.__assert__(
|
|
object != null || method_name != null,
|
|
"Can't bind a null function"
|
|
)
|
|
|
|
bind_external_function_general(func_name, object, method_name, lookahead_safe)
|
|
|
|
|
|
func unbind_external_function(func_name: String) -> void:
|
|
if async_we_cant("unbind an external a function"):
|
|
return
|
|
|
|
self.__assert__(
|
|
_externals.has(func_name),
|
|
"Function '%s' has not been bound." % func_name
|
|
)
|
|
_externals.erase(func_name)
|
|
|
|
|
|
func validate_external_bindings() -> void:
|
|
var missing_externals: InkStringSet = InkStringSet.new()
|
|
|
|
validate_external_bindings_with(_main_content_container, missing_externals)
|
|
_has_validated_externals = true
|
|
|
|
if missing_externals.size() == 0:
|
|
_has_validated_externals = true
|
|
else:
|
|
var message: String = "ERROR: Missing function binding for external %s: '%s' %s" % [
|
|
"s" if missing_externals.size() > 1 else "",
|
|
"', '", InkUtils.join("', '", missing_externals.to_array()),
|
|
", and no fallback ink function found." if allow_external_function_fallbacks else " (ink fallbacks disabled)"
|
|
]
|
|
|
|
error(message)
|
|
|
|
|
|
func validate_external_bindings_with(o: InkObject, missing_externals: InkStringSet) -> void:
|
|
var container = InkUtils.as_or_null(o, "InkContainer")
|
|
if container:
|
|
for inner_content in o.content:
|
|
var inner_container = InkUtils.as_or_null(inner_content, "InkContainer")
|
|
if inner_container == null || !inner_container.has_valid_name:
|
|
validate_external_bindings_with(inner_content, missing_externals)
|
|
|
|
for inner_key in o.named_content:
|
|
validate_external_bindings_with(
|
|
InkUtils.as_or_null(o.named_content[inner_key], "InkObject"),
|
|
missing_externals
|
|
)
|
|
return
|
|
|
|
var divert = InkUtils.as_or_null(o, "Divert")
|
|
if divert && divert.is_external:
|
|
var name = divert.target_path_string
|
|
|
|
if !_externals.has(name):
|
|
if allow_external_function_fallbacks:
|
|
var fallback_found = self.main_content_container.named_content.has(name)
|
|
if !fallback_found:
|
|
missing_externals.append(name)
|
|
else:
|
|
missing_externals.append(name)
|
|
|
|
|
|
# (String, Object, String) -> void
|
|
func observe_variable(variable_name: String, object, method_name: String) -> void:
|
|
if async_we_cant("observe a new variable"):
|
|
return
|
|
|
|
if _variable_observers == null:
|
|
_variable_observers = {}
|
|
|
|
if !self.state.variables_state.global_variable_exists_with_name(variable_name):
|
|
InkUtils.throw_exception(
|
|
"Cannot observe variable '%s'" % variable_name +
|
|
"because it wasn't declared in the ink story."
|
|
)
|
|
return
|
|
|
|
if _variable_observers.has(variable_name):
|
|
_variable_observers[variable_name].connect("variable_changed", Callable(object, method_name))
|
|
else:
|
|
var new_observer = VariableObserver.new(variable_name)
|
|
new_observer.connect("variable_changed", Callable(object, method_name))
|
|
|
|
_variable_observers[variable_name] = new_observer
|
|
|
|
|
|
# (Array<String>, Object, String) -> void
|
|
func observe_variables(variable_names: Array, object, method_name: String) -> void:
|
|
for var_name in variable_names:
|
|
observe_variable(var_name, object, method_name)
|
|
|
|
|
|
# (Object, String, String) -> void
|
|
# TODO: Rewrite this poor documentation and improve method beyond what
|
|
# upstream offers.
|
|
#
|
|
# Potential cases:
|
|
# - specific_variable_name is null, but object and method_name are both present
|
|
# -> all signals, pointing to object & method_name and regardless of the
|
|
# variable they listen to, are disconnected.
|
|
#
|
|
# - specific_variable_name is present, but both object and method_name are null
|
|
# -> all signals listening to changes of specific_variable_name are disconnected.
|
|
#
|
|
# - object and method_name have mismatched presence
|
|
# -> this is an unsuported case at the moment.
|
|
func remove_variable_observer(object = null, method_name = null, specific_variable_name = null):
|
|
if async_we_cant("remove a variable observer"):
|
|
return
|
|
|
|
if _variable_observers == null:
|
|
return
|
|
|
|
if specific_variable_name != null:
|
|
if _variable_observers.has(specific_variable_name):
|
|
var observer = _variable_observers[specific_variable_name]
|
|
if object != null && method_name != null:
|
|
observer.disconnect("variable_changed", Callable(object, method_name))
|
|
|
|
if observer.get_signal_connection_list("variable_changed").is_empty():
|
|
_variable_observers.erase(specific_variable_name)
|
|
else:
|
|
var connections = observer.get_signal_connection_list("variable_changed");
|
|
for connection in connections:
|
|
observer.disconnect(connection.signal.get_name(), connection.callable)
|
|
|
|
_variable_observers.erase(specific_variable_name)
|
|
|
|
elif object != null && method_name != null:
|
|
var keys_to_remove = []
|
|
for observer_key in _variable_observers:
|
|
var observer = _variable_observers[observer_key]
|
|
if observer.is_connected("variable_changed", Callable(object, method_name)):
|
|
observer.disconnect("variable_changed", Callable(object, method_name))
|
|
|
|
if observer.get_signal_connection_list("variable_changed").is_empty():
|
|
keys_to_remove.append(observer_key)
|
|
|
|
for key in keys_to_remove:
|
|
_variable_observers.erase(key)
|
|
|
|
|
|
func variable_state_did_change_event(variable_name: String, new_value_obj: InkObject) -> void:
|
|
if _variable_observers == null:
|
|
return
|
|
|
|
if _variable_observers.has(variable_name):
|
|
var observer = _variable_observers[variable_name]
|
|
|
|
if !InkUtils.is_ink_class(new_value_obj, "Value"):
|
|
InkUtils.throw_exception("Tried to get the value of a variable that isn't a standard type")
|
|
return
|
|
|
|
var val = new_value_obj
|
|
|
|
observer.emit_signal("variable_changed", variable_name, val.value_object)
|
|
|
|
|
|
var global_tags: # Array<String>
|
|
get:
|
|
return self.tags_at_start_of_flow_container_with_path_string("")
|
|
|
|
# (String) -> Array<String>?
|
|
func tags_for_content_at_path(path: String):
|
|
return self.tags_at_start_of_flow_container_with_path_string(path)
|
|
|
|
# (String) -> Array<String>?
|
|
func tags_at_start_of_flow_container_with_path_string(path_string: String):
|
|
var path = InkPath.new_with_components_string(path_string)
|
|
|
|
var flow_container = content_at_path(path).container
|
|
while (true):
|
|
var first_content = flow_container.content[0]
|
|
if InkUtils.is_ink_class(first_content, "InkContainer"):
|
|
flow_container = first_content
|
|
else: break
|
|
|
|
var in_tag := false
|
|
var tags = null # Array<String>
|
|
for c in flow_container.content:
|
|
var command = InkUtils.as_or_null(c, "ControlCommand")
|
|
if command != null:
|
|
if command.command_type == InkControlCommand.CommandType.BEGIN_TAG:
|
|
in_tag = true
|
|
elif command.command_type == InkControlCommand.CommandType.END_TAG:
|
|
in_tag = false
|
|
elif in_tag:
|
|
var _str = InkUtils.as_or_null(c, "StringValue")
|
|
if _str != null:
|
|
if tags == null:
|
|
tags = [] # Array<String>
|
|
tags.append(_str.value)
|
|
print(str("\"", _str.value, "\""))
|
|
else:
|
|
self.error(str(
|
|
"Tag contained non-text content. Only plain text is allowed when using ",
|
|
"globalTags or TagsAtContentPath. If you want to evaluate dynamic ",
|
|
"content, you need to use story.Continue()."
|
|
))
|
|
else:
|
|
break
|
|
|
|
return tags
|
|
|
|
|
|
func build_string_of_container() -> String:
|
|
# TODO: Implement
|
|
return ""
|
|
|
|
|
|
func build_string_of_container_with(container: InkContainer) -> String:
|
|
# TODO: Implement
|
|
return ""
|
|
|
|
|
|
func next_content() -> void:
|
|
|
|
self.state.previous_pointer = self.state.current_pointer
|
|
|
|
if !self.state.diverted_pointer.is_null:
|
|
|
|
self.state.current_pointer = self.state.diverted_pointer
|
|
self.state.diverted_pointer = InkPointer.null_pointer
|
|
|
|
self.visit_changed_containers_due_to_divert()
|
|
|
|
if !self.state.current_pointer.is_null:
|
|
return
|
|
|
|
var successful_pointer_increment = self.increment_content_pointer()
|
|
|
|
if !successful_pointer_increment:
|
|
var did_pop = false
|
|
|
|
if self.state.callstack.can_pop_type(Ink.PushPopType.FUNCTION):
|
|
|
|
self.state.pop_callstack(Ink.PushPopType.FUNCTION)
|
|
|
|
if self.state.in_expression_evaluation:
|
|
self.state.push_evaluation_stack(InkVoid.new())
|
|
|
|
did_pop = true
|
|
elif self.state.callstack.can_pop_thread:
|
|
self.state.callstack.pop_thread()
|
|
|
|
did_pop = true
|
|
else:
|
|
self.state.try_exit_function_evaluation_from_game()
|
|
|
|
if did_pop && !self.state.current_pointer.is_null:
|
|
self.next_content()
|
|
|
|
|
|
func increment_content_pointer() -> bool:
|
|
var successful_increment = true
|
|
|
|
var pointer = self.state.callstack.current_element.current_pointer
|
|
pointer = InkPointer.new(pointer.container, pointer.index + 1)
|
|
|
|
while pointer.index >= pointer.container.content.size():
|
|
|
|
successful_increment = false
|
|
|
|
var next_ancestor = InkUtils.as_or_null(pointer.container.parent, "InkContainer")
|
|
if !next_ancestor:
|
|
break
|
|
|
|
var index_in_ancestor = next_ancestor.content.find(pointer.container)
|
|
if index_in_ancestor == -1:
|
|
break
|
|
|
|
pointer = InkPointer.new(next_ancestor, index_in_ancestor + 1)
|
|
|
|
successful_increment = true
|
|
|
|
if !successful_increment: pointer = InkPointer.null_pointer
|
|
|
|
var current_element = self.state.callstack.current_element
|
|
current_element.current_pointer = pointer
|
|
|
|
return successful_increment
|
|
|
|
|
|
func try_follow_default_invisible_choice() -> bool:
|
|
var all_choices = _state.current_choices
|
|
|
|
var invisible_choices = []
|
|
for c in all_choices:
|
|
if c.is_invisible_default:
|
|
invisible_choices.append(c)
|
|
|
|
if invisible_choices.size() == 0 || all_choices.size() > invisible_choices.size():
|
|
return false
|
|
|
|
var choice = invisible_choices[0]
|
|
|
|
var callstack = self.state.callstack
|
|
callstack.current_thread = choice.thread_at_generation
|
|
|
|
if self._state_snapshot_at_last_newline != null:
|
|
self.state.callstack.current_thread = self.state.callstack.fork_thread()
|
|
|
|
choose_path(choice.target_path, false)
|
|
|
|
return true
|
|
|
|
|
|
func next_sequence_shuffle_index() -> int:
|
|
var num_elements_int_val = InkUtils.as_or_null(self.state.pop_evaluation_stack(), "IntValue")
|
|
if num_elements_int_val == null:
|
|
error("expected number of elements in sequence for shuffle index")
|
|
return 0
|
|
|
|
var seq_container = self.state.current_pointer.container
|
|
|
|
var num_elements = num_elements_int_val.value
|
|
|
|
var seq_count_val = InkUtils.as_or_null(self.state.pop_evaluation_stack(), "IntValue")
|
|
var seq_count = seq_count_val.value
|
|
var loop_index = seq_count / num_elements
|
|
var iteration_index = seq_count % num_elements
|
|
|
|
var seq_path_str = seq_container.path._to_string()
|
|
var sequence_hash = 0
|
|
for c in seq_path_str:
|
|
sequence_hash += int(c)
|
|
|
|
var random_seed = sequence_hash + loop_index + self.state.story_seed
|
|
seed(random_seed)
|
|
|
|
var unpicked_indices = [] # Array<int>
|
|
var i = 0
|
|
while (i < num_elements):
|
|
unpicked_indices.append(i)
|
|
i += 1
|
|
|
|
i = 0
|
|
while (i <= iteration_index):
|
|
var chosen = randi() % unpicked_indices.size()
|
|
var chosen_index = unpicked_indices[chosen]
|
|
unpicked_indices.remove_at(chosen)
|
|
|
|
if i == iteration_index:
|
|
return chosen_index
|
|
|
|
i += 1
|
|
|
|
InkUtils.throw_exception("Should never reach here")
|
|
return -1
|
|
|
|
|
|
# (String, bool) -> void
|
|
func error(message: String, use_end_line_number: bool = false) -> void:
|
|
InkUtils.throw_story_exception(message, use_end_line_number, _make_story_error_metadata())
|
|
|
|
|
|
# (String) -> void
|
|
func warning(message: String) -> void:
|
|
add_error(message, true)
|
|
|
|
|
|
# (String, bool, bool) -> void
|
|
func add_error(message: String, is_warning: bool = false, use_end_line_number: bool = false) -> void:
|
|
# This method differs from upstream, because GDScript doesn't support exceptions.
|
|
# Error formatting is handled by add_error_with_metadata, because there's a new
|
|
# method `add_story_error` used by `continue_internal` to report errors
|
|
# that occured during the step.
|
|
_add_error_with_metadata(
|
|
message,
|
|
is_warning,
|
|
use_end_line_number,
|
|
self.current_debug_metadata,
|
|
self.state.current_pointer
|
|
)
|
|
|
|
|
|
# (StoryError) -> void
|
|
#
|
|
# This method doesn't exist in upstream. It's used by `continue_internal` to
|
|
# report error that occured during the step.
|
|
func add_story_error(story_error: StoryError) -> void:
|
|
_add_error_with_metadata(
|
|
story_error.message,
|
|
false,
|
|
story_error.use_end_line_number,
|
|
story_error.metadata.debug_metadata,
|
|
story_error.metadata.pointer
|
|
)
|
|
|
|
|
|
# (bool, String?, Array<Variant>?) -> void
|
|
func __assert__(condition: bool, message = null, format_params = null) -> void:
|
|
if condition == false:
|
|
if message == null:
|
|
message = "Story assert"
|
|
|
|
if format_params != null && format_params.size() > 0:
|
|
message = message % format_params
|
|
|
|
if self.current_debug_metadata != null:
|
|
InkUtils.throw_exception("%s %s" % [message, str(self.current_debug_metadata)])
|
|
else:
|
|
InkUtils.throw_exception(message)
|
|
|
|
|
|
var current_debug_metadata: InkDebugMetadata: get = get_current_debug_metadata
|
|
func get_current_debug_metadata() -> InkDebugMetadata:
|
|
var dm # DebugMetadata
|
|
|
|
var pointer = self.state.current_pointer
|
|
if !pointer.is_null:
|
|
dm = pointer.resolve().debug_metadata
|
|
if dm != null:
|
|
return dm
|
|
|
|
var i = self.state.callstack.elements.size() - 1
|
|
while (i >= 0):
|
|
pointer = self.state.callstack.elements[i].current_pointer
|
|
if !pointer.is_null && pointer.resolve() != null:
|
|
dm = pointer.resolve().debug_metadata
|
|
if dm != null:
|
|
return dm
|
|
|
|
i -= 1
|
|
|
|
i = self.state.output_stream.size() - 1
|
|
while(i >= 0):
|
|
var output_obj = self.state.output_stream[i]
|
|
dm = output_obj.debug_metadata
|
|
if dm != null:
|
|
return dm
|
|
|
|
i -= 1
|
|
|
|
return null
|
|
|
|
|
|
var current_line_number: int: get = get_current_line_number
|
|
func get_current_line_number() -> int:
|
|
var dm = self.current_debug_metadata
|
|
if dm != null:
|
|
return dm.start_line_number
|
|
|
|
return 0
|
|
|
|
|
|
# InkContainer?
|
|
var main_content_container : get = get_main_content_container
|
|
func get_main_content_container():
|
|
if _temporary_evaluation_container:
|
|
return _temporary_evaluation_container
|
|
else:
|
|
return _main_content_container
|
|
|
|
|
|
# InkContainer?
|
|
var _main_content_container = null
|
|
# ListDefinitionsOrigin?
|
|
var _list_definitions = null
|
|
|
|
# Dictionary<String, ExternalFunctionDef>?
|
|
var _externals = null
|
|
# Dictionary<String, VariableObserver>?
|
|
var _variable_observers = null
|
|
|
|
var _has_validated_externals: bool = false
|
|
|
|
# InkContainer?
|
|
var _temporary_evaluation_container = null
|
|
|
|
# StoryState?
|
|
var _state = null
|
|
|
|
var _async_continue_active: bool = false
|
|
# StoryState?
|
|
var _state_snapshot_at_last_newline = null
|
|
var _saw_lookahead_unsafe_function_after_newline: bool = false # bool
|
|
|
|
var _recursive_continue_count: int = 0
|
|
|
|
var _async_saving: bool = false
|
|
|
|
# Profiler?
|
|
var _profiler = null
|
|
|
|
# ############################################################################ #
|
|
# GDScript extra methods
|
|
# ############################################################################ #
|
|
|
|
func is_ink_class(type: String) -> bool:
|
|
return type == "Story" || super.is_ink_class(type)
|
|
|
|
|
|
func get_ink_class() -> String:
|
|
return "Story"
|
|
|
|
|
|
func connect_exception(target: Object, method: String, binds = [], flags = 0) -> int:
|
|
var runtime = self._ink_runtime
|
|
if runtime == null:
|
|
return ERR_UNAVAILABLE
|
|
|
|
if runtime.is_connected("exception_raised", Callable(target, method)):
|
|
return OK
|
|
|
|
return runtime.connect("exception_raised", Callable(target, method).bind(binds), flags)
|
|
|
|
|
|
func _enable_story_exception_recording(enable: bool) -> void:
|
|
var runtime = self._ink_runtime
|
|
if runtime != null:
|
|
runtime.record_story_exceptions = enable
|
|
|
|
|
|
func _get_and_clear_recorded_story_exceptions() -> Array:
|
|
var runtime = self._ink_runtime
|
|
if runtime == null:
|
|
return []
|
|
|
|
var exceptions = runtime.current_story_exceptions
|
|
runtime.current_story_exceptions = []
|
|
|
|
return exceptions
|
|
|
|
|
|
func _add_error_with_metadata(
|
|
message: String,
|
|
is_warning: bool = false,
|
|
use_end_line_number: bool = false,
|
|
dm = null,
|
|
current_pointer = InkPointer.null_pointer
|
|
) -> void:
|
|
var error_type_str = "WARNING" if is_warning else "ERROR"
|
|
|
|
if dm != null:
|
|
var line_num = dm.end_line_number if use_end_line_number else dm.start_line_number
|
|
message = "RUNTIME %s: '%s' line %s: %s" % [error_type_str, dm.file_name, line_num, message]
|
|
elif !current_pointer.is_null:
|
|
message = "RUNTIME %s: (%s): %s" % [error_type_str, current_pointer.path._to_string(), message]
|
|
else:
|
|
message = "RUNTIME " + error_type_str + ": " + message
|
|
|
|
self.state.add_error(message, is_warning)
|
|
|
|
if !is_warning:
|
|
self.state.force_end()
|
|
|
|
|
|
func _throw_story_exception(message: String):
|
|
InkUtils.throw_story_exception(message, false, _make_story_error_metadata())
|
|
|
|
|
|
# This method is used to ensure that the debug metadata and pointer used
|
|
# to report errors are the ones at the moment the error occured (and not
|
|
# the current one). Since GDScript doesn't have exceptions, errors may be
|
|
# stored until they can be processed at the end of `continue_internal`.
|
|
func _make_story_error_metadata():
|
|
return StoryErrorMetadata.new(self.current_debug_metadata, self.state.current_pointer)
|
|
|
|
|
|
# ############################################################################ #
|
|
|
|
var StaticJSON: InkStaticJSON:
|
|
get: return self._ink_runtime.json
|
|
|
|
var _ink_runtime:
|
|
get: return _weak_ink_runtime.get_ref()
|
|
var _weak_ink_runtime: WeakRef
|
|
|
|
var _error_raised_during_step = []
|
|
|
|
func _initialize_runtime(ink_runtime = null):
|
|
if ink_runtime != null:
|
|
_weak_ink_runtime = weakref(ink_runtime)
|
|
else:
|
|
var runtime = Engine.get_main_loop().root.get_node("__InkRuntime")
|
|
|
|
InkUtils.__assert__(
|
|
runtime != null,
|
|
str("[InkStory] Could not retrieve 'InkRuntime' singleton from the scene tree.")
|
|
)
|
|
|
|
_weak_ink_runtime = weakref(runtime)
|
|
|
|
# ############################################################################ #
|
|
|
|
class VariableObserver extends RefCounted:
|
|
var variable_name: String
|
|
|
|
signal variable_changed(variable_name, new_value)
|
|
|
|
func _init(variable_name: String):
|
|
self.variable_name = variable_name
|
|
|
|
|
|
class ExternalFunctionDef extends InkBase:
|
|
var object: Object
|
|
var method: String
|
|
var lookahead_safe: bool
|
|
|
|
func _init(object: Object, method: String, lookahead_safe: bool):
|
|
# If object is not a reference, you're responsible to ensure it's
|
|
# still allocated.
|
|
self.object = weakref(object)
|
|
self.method = method
|
|
self.lookahead_safe = lookahead_safe
|
|
|
|
# (Array<Variant>) -> Variant
|
|
func execute(params: Array):
|
|
var object_ref = object.get_ref()
|
|
if object_ref != null:
|
|
return object_ref.callv(method, params)
|
|
else:
|
|
InkUtils.throw_exception(
|
|
"Object binded to %s has been deallocated, cannot execute." % method
|
|
)
|
|
return null
|