1144 lines
34 KiB
GDScript
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)
|
|
|