Figments-of-the-Night/addons/inkgd/runtime/story_state.gd
Gerard Gascón b99855351d init
2025-04-24 17:23:34 +02:00

1144 lines
34 KiB
GDScript

# warning-ignore-all:shadowed_variable
# warning-ignore-all:unused_class_variable
# ############################################################################ #
# 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 InkBase
class_name InkStoryState
var ValueType = preload("res://addons/inkgd/runtime/values/value_type.gd").ValueType
var InkPointer = preload("res://addons/inkgd/runtime/structs/pointer.gd")
var InkPath = preload("res://addons/inkgd/runtime/ink_path.gd")
# ############################################################################ #
var InkValue = load("res://addons/inkgd/runtime/values/value.gd")
var InkStringValue = load("res://addons/inkgd/runtime/values/string_value.gd")
var InkSimpleJSON = preload("res://addons/inkgd/runtime/simple_json.gd")
var InkStatePatch = preload("res://addons/inkgd/runtime/state_patch.gd")
var InkCallStack = load("res://addons/inkgd/runtime/callstack.gd")
var InkVariablesState = load("res://addons/inkgd/runtime/variables_state.gd")
var InkFlow = load("res://addons/inkgd/runtime/flow.gd")
# ############################################################################ #
# Self-reference
# ############################################################################ #
static func InkStoryState() -> GDScript:
return load("res://addons/inkgd/runtime/story_state.gd") as GDScript
# ############################################################################ #
const INK_SAVE_STATE_VERSION: int = 10
const MIN_COMPATIBLE_LOAD_VERSION: int = 8
# ############################################################################ #
signal on_did_load_state()
# ############################################################################ #
func to_json() -> String:
var writer: InkSimpleJSON.Writer = InkSimpleJSON.Writer.new()
write_json(writer)
return writer._to_string()
func load_json(json: String) -> void:
var jobject: Dictionary = InkSimpleJSON.text_to_dictionary(json)
load_json_obj(jobject)
emit_signal("on_did_load_state")
func visit_count_at_path_string(path_string: String) -> int:
if self._patch != null:
var path = InkPath.new_with_components_string(path_string)
var container: InkContainer = self.story.content_at_path(path).container
if container == null:
InkUtils.throw_exception("Content at path not found: %s" % path_string)
return 0
var visit_count: InkTryGetResult = self._patch.try_get_visit_count(container)
if visit_count.exists:
return visit_count.result
if self._visit_counts.has(path_string):
return self._visit_counts[path_string]
return 0
func visit_count_for_container(container: InkContainer) -> int:
if !container.visits_should_be_counted:
self.story.error(
"Read count for target (%s - on %s) " % [container.name, container.debugMetadata] +
"unknown. The story may need to be compiled with countAllVisits flag (-c)."
)
return 0
var count: int = 0
if self._patch != null:
var visit_count: InkTryGetResult = self._patch.try_get_visit_count(container)
if visit_count.exists:
return visit_count.result
var container_path_str: String = container.path._to_string()
if self._visit_counts.has(container_path_str):
count = self._visit_counts[container_path_str]
return count
func increment_visit_count_for_container(container: InkContainer) -> void:
if self._patch != null:
var curr_count: int = visit_count_for_container(container)
curr_count += 1
self._patch.set_visit_count(container, curr_count)
return
var count: int = 0
var container_path_str: String = container.path._to_string()
if self._visit_counts.has(container_path_str):
count = self._visit_counts[container_path_str]
count += 1
self._visit_counts[container_path_str] = count
func record_turn_index_visit_to_container(container: InkContainer) -> void:
if self._patch != null:
self._patch.set_turn_index(container, self.current_turn_index)
return
var container_path_str: String = container.path._to_string()
self._turn_indices[container_path_str] = self.current_turn_index
# (InkContainer) -> int
func turns_since_for_container(container: InkContainer) -> int:
if !container.turn_index_should_be_counted:
self.story.error(
"TURNS_SINCE() for target (%s - on %s) " \
% [container.name, container.debugMetadata] +
"unknown. The story may need to be compiled with countAllVisits flag (-c)."
)
return 0
if self._patch != null:
var turn_index: InkTryGetResult = self._patch.try_get_turn_index(container)
if turn_index.exists:
return self.current_turn_index - turn_index.result
var container_path_str: String = container.path._to_string()
if self._turn_indices.has(container_path_str):
return self.current_turn_index - self._turn_indices[container_path_str]
else:
return -1
var callstack_depth: int:
get:
return self.callstack.depth
var output_stream: Array: # Array<InkObject>
get:
return self._current_flow.output_stream
var current_choices: Array: # Array<Choice>
get:
if self.can_continue:
return []
return self._current_flow.current_choices
var generated_choices: Array: # Array<Choice>
get:
return self._current_flow.current_choices
# Array<String>
var current_errors = null
# Array<String>
var current_warnings = null
# InkVariablesState
var variables_state
var callstack: InkCallStack: get = get_callstack
func get_callstack() -> InkCallStack:
return self._current_flow.callstack
# Array<InkObject>
var evaluation_stack: Array
# Pointer
var diverted_pointer: InkPointer = InkPointer.null_pointer
var current_turn_index: int = 0
var story_seed: int = 0
var previous_random: int = 0
var did_safe_exit: bool = false
var story : get = get_story
func get_story():
return _story.get_ref()
var _story = WeakRef.new()
# String?
var current_path_string : get = get_current_path_string
func get_current_path_string():
var pointer = self.current_pointer
if pointer.is_null:
return null
else:
return pointer.path._to_string()
var current_pointer: InkPointer: get = get_current_pointer, set = set_current_pointer
func get_current_pointer() -> InkPointer:
var pointer = self.callstack.current_element.current_pointer
return self.callstack.current_element.current_pointer
func set_current_pointer(value: InkPointer):
var current_element = self.callstack.current_element
current_element.current_pointer = value
var previous_pointer: InkPointer: get = get_previous_pointer, set = set_previous_pointer
func get_previous_pointer() -> InkPointer:
return self.callstack.current_thread.previous_pointer
func set_previous_pointer(value: InkPointer):
var current_thread = self.callstack.current_thread
current_thread.previous_pointer = value
var can_continue: bool: get = get_can_continue
func get_can_continue() -> bool:
return !self.current_pointer.is_null && !self.has_error
var has_error: bool: get = get_has_error
func get_has_error() -> bool:
return self.current_errors != null && self.current_errors.size() > 0
var has_warning: bool: get = get_has_warning
func get_has_warning() -> bool:
return self.current_warnings != null && self.current_warnings.size() > 0
var current_text: String: get = get_current_text
func get_current_text():
if self._output_stream_text_dirty:
var _str := ""
var in_tag := false
for output_obj in self.output_stream:
var text_content = InkUtils.as_or_null(output_obj, "StringValue")
if !in_tag && text_content != null:
_str += text_content.value
else:
var control_command = InkUtils.as_or_null(output_obj, "ControlCommand")
if control_command != null:
if control_command.command_type == InkControlCommand.CommandType.BEGIN_TAG:
in_tag = true
elif control_command.command_type == InkControlCommand.CommandType.END_TAG:
in_tag = false
self._current_text = self.clean_output_whitespace(_str)
self._output_stream_text_dirty = false
return self._current_text
var _current_text: String = ""
# (String) -> String
func clean_output_whitespace(str_to_clean: String) -> String:
var _str: String = ""
var current_whitespace_start: int = -1
var start_of_line: int = 0
var i: int = 0
while(i < str_to_clean.length()):
var c: String = str_to_clean[i]
var is_inline_whitespace: bool = (c == " " || c == "\t")
if is_inline_whitespace && current_whitespace_start == -1:
current_whitespace_start = i
if !is_inline_whitespace:
if (c != "\n" && current_whitespace_start > 0 && current_whitespace_start != start_of_line):
_str += " "
current_whitespace_start = -1
if c == "\n":
start_of_line = i + 1
if !is_inline_whitespace:
_str += c
i += 1
return _str
# Array<String>
var current_tags: Array: get = get_current_tags
func get_current_tags():
if self._output_stream_tags_dirty:
self._current_tags = []
var in_tag := false
var sb := ""
for output_obj in self.output_stream:
var control_command = InkUtils.as_or_null(output_obj, "ControlCommand")
if control_command != null:
if control_command.command_type == InkControlCommand.CommandType.BEGIN_TAG:
if in_tag && sb.length() > 0:
var txt = self.clean_output_whitespace(sb)
self._current_tags.append(txt)
sb = ""
in_tag = true
elif control_command.command_type == InkControlCommand.CommandType.END_TAG:
if sb.length() > 0:
var txt = self.clean_output_whitespace(sb)
self._current_tags.append(txt)
sb = ""
in_tag = false
elif in_tag:
var str_val = InkUtils.as_or_null(output_obj, "StringValue")
if str_val != null:
sb += str_val.value
else:
var tag = InkUtils.as_or_null(output_obj, "Tag")
if tag != null && tag.text != null && !tag.text.is_empty():
self._current_tags.append(tag.text)
if !sb.is_empty():
var txt = self.clean_output_whitespace(sb)
self._current_tags.append(txt)
sb = ""
self._output_stream_tags_dirty = false
return self._current_tags
# Array<String>
var _current_tags: Array = []
var current_flow_name: String: get = get_current_flow_name
func get_current_flow_name() -> String:
return self._current_flow.name
var current_flow_is_default_flow: bool: get = get_current_flow_is_default_flow
func get_current_flow_is_default_flow() -> bool:
return self._current_flow.name == DEFAULT_FLOW_NAME
var alive_flow_names: Array: get = get_alive_flow_names
func get_alive_flow_names() -> Array:
if self._alive_flow_names_dirty:
self._alive_flow_names = []
if self._named_flows != null:
for flow_name in self._named_flows.keys():
if flow_name != DEFAULT_FLOW_NAME:
self._alive_flow_names.append(flow_name)
self._alive_flow_names_dirty = false
return self._alive_flow_names
var _alive_flow_names: Array = []
var in_expression_evaluation: bool:
get:
return self.callstack.current_element.in_expression_evaluation
set(value):
var current_element = self.callstack.current_element
current_element.in_expression_evaluation = value
# (InkStory) -> InkStoryState
func _init(story, ink_runtime = null):
find_static_objects(ink_runtime)
self._story = weakref(story)
self._current_flow = InkFlow.new_with_name(DEFAULT_FLOW_NAME, story, self.StaticJSON)
self.output_stream_dirty()
self._alive_flow_names_dirty = true
self.evaluation_stack = []
self.variables_state = InkVariablesState.new(self.callstack, self.story.list_definitions, self._ink_runtime)
self._visit_counts = {}
self._turn_indices = {}
self.current_turn_index = -1
randomize()
self.story_seed = randi() % 100
self.previous_random = 0
self.go_to_start()
func go_to_start() -> void:
var current_element = self.callstack.current_element
current_element.current_pointer = InkPointer.start_of(self.story.main_content_container)
func switch_flow_internal(flow_name: String) -> void:
if flow_name == null:
InkUtils.throw_exception("Must pass a non-null string to Story.SwitchFlow")
if self._named_flows == null:
self._named_flows = {} # Dictionary<String, Flow>
self._named_flows[DEFAULT_FLOW_NAME] = self._current_flow
if flow_name == self._current_flow.name:
return
var flow
if self._named_flows.has(flow_name):
flow = self._named_flows[flow_name]
else:
flow = InkFlow.new_with_name(flow_name, self.story, self.StaticJSON)
self._named_flows[flow_name] = flow
self._alive_flow_names_dirty = true
self._current_flow = flow
self.variables_state.callstack = self._current_flow.callstack
self.output_stream_dirty()
func switch_to_default_flow_internal() -> void:
if self._named_flows == null:
return
self.switch_flow_internal(DEFAULT_FLOW_NAME)
func remove_flow_internal(flow_name: String) -> void:
if flow_name == null:
InkUtils.throw_exception("Must pass a non-null string to Story.DestroyFlow")
return
if flow_name == DEFAULT_FLOW_NAME:
InkUtils.throw_exception("Cannot destroy default flow")
return
if self._current_flow.name == flow_name:
self.switch_to_default_flow_internal()
self._named_flows.erase(flow_name)
self._alive_flow_names_dirty = true
# () -> InkStoryState
func copy_and_start_patching():
var copy = InkStoryState().new(self.story)
copy._patch = InkStatePatch.new(self._patch)
copy._current_flow.name = self._current_flow.name
copy._current_flow.callstack = InkCallStack.new(self._current_flow.callstack, self.StaticJSON)
copy._current_flow.current_choices += self._current_flow.current_choices
copy._current_flow.output_stream += self._current_flow.output_stream
copy.output_stream_dirty()
if self._named_flows != null:
copy._named_flows = {} # Dictionary<String, Flow>
for named_flow_key in self._named_flows.keys():
var named_flow_value = self._named_flows[named_flow_key]
copy._named_flows[named_flow_key] = named_flow_value
copy._named_flows[self._current_flow.name] = copy._current_flow
copy._alive_flow_names_dirty = true
if self.has_error:
copy.current_errors = [] # Array<String>
copy.current_errors += self.current_errors
if self.has_warning:
copy.current_warnings = [] # Array<String>
copy.current_warnings += self.current_warnings
copy.variables_state = variables_state
copy.variables_state.callstack = copy.callstack
copy.variables_state.patch = copy._patch
copy.evaluation_stack += self.evaluation_stack
if !diverted_pointer.is_null:
copy.diverted_pointer = self.diverted_pointer
copy.previous_pointer = self.previous_pointer
copy._visit_counts = self._visit_counts
copy._turn_indices = self._turn_indices
copy.current_turn_index = self.current_turn_index
copy.story_seed = self.story_seed
copy.previous_random = self.previous_random
copy.did_safe_exit = self.did_safe_exit
return copy
func restore_after_patch() -> void:
self.variables_state.callstack = self.callstack
self.variables_state.patch = self._patch
func apply_any_patch() -> void:
if self._patch == null:
return
self.variables_state.apply_patch()
for path_to_count_key in self._patch.visit_counts:
apply_count_changes(path_to_count_key, self._patch.visit_counts[path_to_count_key], true)
for path_to_index_key in self._patch.turn_indices:
apply_count_changes(path_to_index_key, self._patch.turn_indices[path_to_index_key], false)
self._patch = null
func apply_count_changes(container: InkContainer, new_count: int, is_visit: bool) -> void:
var counts = self._visit_counts if is_visit else self._turn_indices
counts[container.path._to_string()] = new_count
func write_json(writer: InkSimpleJSON.Writer) -> void:
writer.write_object_start()
writer.write_property_start("flows")
writer.write_object_start()
if self._named_flows != null:
for named_flow_key in self._named_flows.keys():
var named_flow_value = self._named_flows[named_flow_key]
writer.write_property(named_flow_key, Callable(named_flow_value, "write_json"))
else:
writer.write_property(self._current_flow.name, Callable(self._current_flow, "write_json"))
writer.write_object_end()
writer.write_property_end()
writer.write_property("currentFlowName", self._current_flow.name)
writer.write_property("variablesState", Callable(self.variables_state, "write_json"))
writer.write_property("evalStack", Callable(self, "_anonymous_write_property_eval_stack"))
if !self.diverted_pointer.is_null:
writer.write_property("currentDivertTarget", self.diverted_pointer.path.components_string)
writer.write_property("visitCounts", Callable(self, "_anonymous_write_property_visit_counts"))
writer.write_property("turnIndices", Callable(self, "_anonymous_write_property_turn_indices"))
writer.write_property("turnIdx", self.current_turn_index)
writer.write_property("storySeed", self.story_seed)
writer.write_property("previousRandom", self.previous_random)
writer.write_property("inkSaveVersion", INK_SAVE_STATE_VERSION)
writer.write_property("inkFormatVersion", self.story.INK_VERSION_CURRENT)
writer.write_object_end()
func load_json_obj(jobject: Dictionary) -> void:
var jsave_version = null # Variant
if !jobject.has("inkSaveVersion"):
InkUtils.throw_exception("ink save format incorrect, can't load.")
return
else:
jsave_version = int(jobject["inkSaveVersion"])
if jsave_version < MIN_COMPATIBLE_LOAD_VERSION:
InkUtils.throw_exception(
"Ink save format isn't compatible with the current version (saw " +
"'%d', but minimum is %d " % [jsave_version, MIN_COMPATIBLE_LOAD_VERSION] +
"), so can't load."
)
return
if jobject.has("flows"):
var flows_obj_dict = jobject["flows"]
if flows_obj_dict.size() == 1:
self._named_flows = null
elif self._named_flows == null:
self._named_flows = {} # Dictionary<String, Flow>
else:
self._named_flows.clear()
for named_flow_obj_key in flows_obj_dict.keys():
var name = named_flow_obj_key
var flow_obj = flows_obj_dict[named_flow_obj_key]
var flow = InkFlow.new_with_name_and_jobject(name, self.story, flow_obj, self.StaticJSON)
if flows_obj_dict.size() == 1:
self._current_flow = InkFlow.new_with_name_and_jobject(name, self.story, flow_obj, self.StaticJSON)
else:
self._named_flows[name] = flow
if self._named_flows != null && self._named_flows.size() > 1:
var curr_flow_name = jobject["currentFlowName"]
self._current_flow = self._named_flows[curr_flow_name]
else:
self._named_flows = null
self._current_flow.name = DEFAULT_FLOW_NAME
self._current_flow.callstack.set_json_token(jobject["callstackThreads"], self.story)
self._current_flow.output_stream = self.StaticJSON.jarray_to_runtime_obj_list(jobject["outputStream"])
self._current_flow.current_choices = self.StaticJSON.jarray_to_runtime_obj_list(jobject["currentChoices"])
var jchoice_threads_obj = jobject["choiceThreads"] if jobject.has("choiceThreads") else null
self._current_flow.load_flow_choice_threads(jchoice_threads_obj, self.story)
self.output_stream_dirty()
self._alive_flow_names_dirty = true
self.variables_state.set_json_token(jobject["variablesState"])
self.variables_state.callstack = self._current_flow.callstack
self.evaluation_stack = self.StaticJSON.jarray_to_runtime_obj_list(jobject["evalStack"])
if jobject.has("currentDivertTarget"):
var current_divert_target_path = jobject["currentDivertTarget"]
var divert_path = InkPath.new_with_components_string(current_divert_target_path._to_string())
self.diverted_pointer = self.story.pointer_at_path(divert_path)
self._visit_counts = self.StaticJSON.jobject_to_int_dictionary(jobject["visitCounts"])
self._turn_indices = self.StaticJSON.jobject_to_int_dictionary(jobject["turnIndices"])
self.current_turn_index = int(jobject["turnIdx"])
self.story_seed = int(jobject["storySeed"])
# inkjs bug
if jobject.has("previousRandom"):
self.previous_random = int(jobject["previousRandom"])
else:
self.previous_random = 0
# () -> void
func reset_errors() -> void:
self.current_errors = null
self.current_warnings = null
# (Array<InkObject>?) -> void
func reset_output(objs = null) -> void:
self.output_stream.clear()
if objs != null: self.output_stream += objs
self.output_stream_dirty()
func push_to_output_stream(obj: InkObject) -> void:
var text = InkUtils.as_or_null(obj, "StringValue")
if text:
var list_text = self.try_splitting_head_tail_whitespace(text)
if list_text != null:
for text_obj in list_text:
self.push_to_output_stream_individual(text_obj)
self.output_stream_dirty()
return
self.push_to_output_stream_individual(obj)
self.output_stream_dirty()
func pop_from_output_stream(count: int) -> void:
InkUtils.remove_range(self.output_stream, self.output_stream.size() - count, count)
self.output_stream_dirty()
func try_splitting_head_tail_whitespace(single: InkStringValue): # Array<StringValue>
var _str = single.value
var head_first_newline_idx = -1
var head_last_newline_idx = -1
var i = 0
while (i < _str.length()):
var c = _str[i]
if (c == "\n"):
if head_first_newline_idx == -1:
head_first_newline_idx = i
head_last_newline_idx = i
elif c == " " || c == "\t":
i += 1
continue
else:
break
i += 1
var tail_last_newline_idx = -1
var tail_first_newline_idx = -1
var j = _str.length() - 1
while (j >= 0):
var c = _str[j]
if (c == "\n"):
if tail_last_newline_idx == -1:
tail_last_newline_idx = j
tail_first_newline_idx = j
elif c == ' ' || c == '\t':
j -= 1
continue
else:
break
j -= 1
if head_first_newline_idx == -1 && tail_last_newline_idx == -1:
return null
var list_texts = [] # Array<StringValue>
var inner_str_start = 0
var inner_str_end = _str.length()
if head_first_newline_idx != -1:
if head_first_newline_idx > 0:
var leading_spaces = InkStringValue.new_with(_str.substr(0, head_first_newline_idx))
list_texts.append(leading_spaces)
list_texts.append(InkStringValue.new_with("\n"))
inner_str_start = head_last_newline_idx + 1
if tail_last_newline_idx != -1:
inner_str_end = tail_first_newline_idx
if inner_str_end > inner_str_start:
var inner_str_text = _str.substr(inner_str_start, inner_str_end - inner_str_start)
list_texts.append(InkStringValue.new(inner_str_text))
if tail_last_newline_idx != -1 && tail_first_newline_idx > head_last_newline_idx:
list_texts.append(InkStringValue.new("\n"))
if tail_last_newline_idx < _str.length() - 1:
var num_spaces = (_str.length() - tail_last_newline_idx) - 1
var trailing_spaces = InkStringValue.new(_str.substr(tail_last_newline_idx + 1, num_spaces))
list_texts.append(trailing_spaces)
return list_texts
func push_to_output_stream_individual(obj: InkObject) -> void:
var glue = InkUtils.as_or_null(obj, "Glue")
var text = InkUtils.as_or_null(obj, "StringValue")
var include_in_output = true
if glue:
self.trim_newlines_from_output_stream()
include_in_output = true
elif text:
var function_trim_index = -1
var curr_el = self.callstack.current_element
if curr_el.type == Ink.PushPopType.FUNCTION:
function_trim_index = curr_el.function_start_in_ouput_stream
var glue_trim_index = -1
var i = self.output_stream.size() - 1
while (i >= 0):
var o = self.output_stream[i]
var c = InkUtils.as_or_null(o, "ControlCommand")
var g = InkUtils.as_or_null(o, "Glue")
if g:
glue_trim_index = i
break
elif c && c.command_type == InkControlCommand.CommandType.BEGIN_STRING:
if i >= function_trim_index:
function_trim_index = -1
break
i -= 1
var trim_index = -1
if glue_trim_index != -1 && function_trim_index != -1:
trim_index = min(function_trim_index, glue_trim_index)
elif glue_trim_index != -1:
trim_index = glue_trim_index
else:
trim_index = function_trim_index
if trim_index != -1:
if text.is_newline:
include_in_output = false
elif text.is_non_whitespace:
if glue_trim_index > -1:
self.remove_existing_glue()
if function_trim_index > -1:
var callstack_elements = self.callstack.elements
var j = callstack_elements.size() - 1
while j >= 0:
var el = callstack_elements[j]
if el.type == Ink.PushPopType.FUNCTION:
el.function_start_in_ouput_stream = -1
else:
break
j -= 1
elif text.is_newline:
if self.output_stream_ends_in_newline || !self.output_stream_contains_content:
include_in_output = false
if include_in_output:
self.output_stream.append(obj)
self.output_stream_dirty()
func trim_newlines_from_output_stream() -> void:
var remove_whitespace_from = -1 # int
var i = self.output_stream.size() - 1
while i >= 0:
var obj = self.output_stream[i]
var cmd = InkUtils.as_or_null(obj, "ControlCommand")
var txt = InkUtils.as_or_null(obj, "StringValue")
if cmd || (txt && txt.is_non_whitespace):
break
elif txt && txt.is_newline:
remove_whitespace_from = i
i -= 1
if remove_whitespace_from >= 0:
i = remove_whitespace_from
while i < self.output_stream.size():
var text = InkUtils.as_or_null(self.output_stream[i], "StringValue")
if text:
self.output_stream.remove_at(i)
else:
i += 1
self.output_stream_dirty()
func remove_existing_glue() -> void:
var i = self.output_stream.size() - 1
while (i >= 0):
var c = self.output_stream[i]
if InkUtils.is_ink_class(c, "Glue"):
self.output_stream.remove_at(i)
elif InkUtils.is_ink_class(c, "ControlCommand"):
break
i -= 1
self.output_stream_dirty()
var output_stream_ends_in_newline: bool: get = get_output_stream_ends_in_newline
func get_output_stream_ends_in_newline() -> bool:
if self.output_stream.size() > 0:
var i = self.output_stream.size() - 1
while (i >= 0):
var obj = self.output_stream[i]
if InkUtils.is_ink_class(obj, "ControlCommand"):
break
var text = InkUtils.as_or_null(self.output_stream[i], "StringValue")
if text:
if text.is_newline:
return true
elif text.is_non_whitespace:
break
i -= 1
return false
var output_stream_contains_content: bool: get = get_output_stream_contains_content
func get_output_stream_contains_content() -> bool:
for content in self.output_stream:
if InkUtils.is_ink_class(content, "StringValue"):
return true
return false
var in_string_evaluation: bool: get = get_in_string_evaluation
func get_in_string_evaluation() -> bool:
var i = self.output_stream.size() - 1
while (i >= 0):
var cmd = InkUtils.as_or_null(self.output_stream[i], "ControlCommand")
if cmd && cmd.command_type == InkControlCommand.CommandType.BEGIN_STRING:
return true
i -= 1
return false
# (InkObject) -> void
func push_evaluation_stack(obj: InkObject) -> void:
var list_value = InkUtils.as_or_null(obj, "ListValue")
if list_value:
var raw_list = list_value.value
if raw_list.origin_names != null:
if raw_list.origins == null: raw_list.origins = [] # Array<ListDefinition>
raw_list.origins.clear()
for n in raw_list.origin_names:
var def: InkTryGetResult = self.story.list_definitions.try_list_get_definition(n)
if raw_list.origins.find(def.result) < 0:
raw_list.origins.append(def.result)
self.evaluation_stack.append(obj)
# () -> InkObject
func peek_evaluation_stack() -> InkObject:
return self.evaluation_stack.back()
# This method combines both methods found in upstream.
# (int) -> InkObject | Array<InkObject>
func pop_evaluation_stack(number_of_objects: int = -1):
if number_of_objects == -1:
# This code raises an exception to match the behaviour of upstream.
# `pop_back` doesn't raise an error on an empty collection.
if self.evaluation_stack.size() == 0:
InkUtils.throw_exception("trying to pop an empty evaluation stack")
else :
return self.evaluation_stack.pop_back()
if number_of_objects > self.evaluation_stack.size():
InkUtils.throw_exception("trying to pop too many objects")
return []
var popped = InkUtils.get_range(self.evaluation_stack,
self.evaluation_stack.size() - number_of_objects,
number_of_objects)
InkUtils.remove_range(
self.evaluation_stack,
self.evaluation_stack.size() - number_of_objects, number_of_objects
)
return popped
# () -> void
func force_end() -> void:
self.callstack.reset()
self._current_flow.current_choices.clear()
self.current_pointer = InkPointer.null_pointer
self.previous_pointer = InkPointer.null_pointer
self.did_safe_exit = true
func trim_whitespace_from_function_end() -> void:
assert(self.callstack.current_element.type == Ink.PushPopType.FUNCTION)
var function_start_point = self.callstack.current_element.function_start_in_ouput_stream
if function_start_point == -1:
function_start_point = 0
var i = self.output_stream.size() - 1
while (i >= function_start_point):
var obj = self.output_stream[i]
var txt = InkUtils.as_or_null(obj, "StringValue")
var cmd = InkUtils.as_or_null(obj, "ControlCommand")
if !txt:
i -= 1
continue
if cmd: break
if txt.is_newline || txt.is_inline_whitespace:
self.output_stream.remove_at(i)
self.output_stream_dirty()
else:
break
i -= 1
# (Ink.PushPopType?) -> void
func pop_callstack(pop_type = null) -> void:
if (self.callstack.current_element.type == Ink.PushPopType.FUNCTION):
self.trim_whitespace_from_function_end()
self.callstack.pop(pop_type)
# (InkPath, bool) -> void
func set_chosen_path(path: InkPath, incrementing_turn_index: bool) -> void:
self._current_flow.current_choices.clear()
var new_pointer = self.story.pointer_at_path(path)
if !new_pointer.is_null && new_pointer.index == -1:
new_pointer = InkPointer.new(new_pointer.container, 0)
self.current_pointer = new_pointer
if incrementing_turn_index:
self.current_turn_index += 1
# (InkContainer, Array<InkObject>?) -> void
func start_function_evaluation_from_game(func_container: InkContainer, arguments) -> void:
self.callstack.push(Ink.PushPopType.FUNCTION_EVALUATION_FROM_GAME, self.evaluation_stack.size())
var current_element = self.callstack.current_element
current_element.current_pointer = InkPointer.start_of(func_container)
self.pass_arguments_to_evaluation_stack(arguments)
# (Array<InkObject>?) -> void
func pass_arguments_to_evaluation_stack(arguments) -> void:
if arguments != null:
var i = 0
while (i < arguments.size()):
if !(arguments[i] is int || arguments[i] is float || arguments[i] is String || arguments[i] is bool || ((arguments[i] is Object) && arguments[i].is_ink_class("InkList"))):
InkUtils.throw_argument_exception(
"ink arguments when calling EvaluateFunction / " +
"ChoosePathStringWithParameters must be int, " +
"float, string, bool or InkList. Argument was " +
("null" if arguments[i] == null else InkUtils.typename_of(arguments[i]))
)
return
push_evaluation_stack(InkValue.create(arguments[i]))
i += 1
# () -> bool
func try_exit_function_evaluation_from_game() -> bool:
if self.callstack.current_element.type == Ink.PushPopType.FUNCTION_EVALUATION_FROM_GAME:
self.current_pointer = InkPointer.null_pointer
self.did_safe_exit = true
return true
return false
# () -> Variant
func complete_function_evaluation_from_game():
if self.callstack.current_element.type != Ink.PushPopType.FUNCTION_EVALUATION_FROM_GAME:
InkUtils.throw_exception(
"Expected external function evaluation to be complete. Stack trace: %s" % \
self.callstack_trace
)
return null
var original_evaluation_stack_height = self.callstack.current_element.evaluation_stack_height_when_pushed
var returned_obj = null
while (self.evaluation_stack.size() > original_evaluation_stack_height):
var popped_obj = self.pop_evaluation_stack()
if returned_obj == null:
returned_obj = popped_obj
self.pop_callstack(Ink.PushPopType.FUNCTION_EVALUATION_FROM_GAME)
if returned_obj:
if InkUtils.is_ink_class(returned_obj, "Void"):
return null
var return_val = InkUtils.as_or_null(returned_obj, "Value")
if return_val.value_type == ValueType.DIVERT_TARGET:
return return_val.value_object._to_string()
return return_val.value_object
return null
func add_error(message: String, is_warning: bool) -> void:
if !is_warning:
if self.current_errors == null:
self.current_errors = [] # Array<string>
self.current_errors.append(message)
else:
if self.current_warnings == null:
self.current_warnings = [] # Array<string>
self.current_warnings.append(message)
func output_stream_dirty() -> void:
self._output_stream_text_dirty = true
self._output_stream_tags_dirty = true
# ############################################################################ #
# Dictionary<string, Int>
var _visit_counts: Dictionary
# Dictionary<string, Int>
var _turn_indices: Dictionary
var _output_stream_text_dirty: bool = true # bool
var _output_stream_tags_dirty: bool = true # bool
var _patch # StatePatch?
var _current_flow = null # Flow?
var _named_flows = null # Dictionary<String, Flow>?
const DEFAULT_FLOW_NAME: String = "DEFAULT_FLOW" # String
var _alive_flow_names_dirty := true
# C# Actions & Delegates ##################################################### #
func _anonymous_write_property_eval_stack(writer) -> void:
self.StaticJSON.write_list_runtime_objs(writer, self.evaluation_stack)
func _anonymous_write_property_visit_counts(writer) -> void:
self.StaticJSON.write_int_dictionary(writer, self._visit_counts)
func _anonymous_write_property_turn_indices(writer) -> void:
self.StaticJSON.write_int_dictionary(writer, self._turn_indices)
# ############################################################################ #
# GDScript extra methods
# ############################################################################ #
func is_ink_class(type: String) -> bool:
return type == "StoryState" || super.is_ink_class(type)
func get_ink_class() -> String:
return "StoryState"
# ############################################################################ #
var StaticJSON: InkStaticJSON:
get: return self._ink_runtime.json
var _ink_runtime:
get: return _weak_ink_runtime.get_ref()
var _weak_ink_runtime: WeakRef
func find_static_objects(ink_runtime = null):
if ink_runtime != null:
_weak_ink_runtime = weakref(ink_runtime)
return
var runtime = Engine.get_main_loop().root.get_node("__InkRuntime")
InkUtils.__assert__(
runtime != null,
"[StoryState] Could not retrieve 'InkRuntime' singleton from the scene tree."
)
_weak_ink_runtime = weakref(runtime)