class Fable::StoryState

All story state information is included in the StoryState class, including global variables, read counts, the pointer to the current point in the story, the call stack (for tunnels, functions, etc), and a few other smaller bits and pieces. You can save the current state using the serialization functions

Constants

CURRENT_INK_SAVE_STATE_VERSION
MINIMUM_COMPATIBLE_INK_LOAD_VERSION
MULTIPLE_WHITESPACE_REGEX

Attributes

callstack[RW]
current_choices[RW]
current_errors[RW]
current_tags[RW]
current_text[RW]
current_turn_index[RW]
current_warnings[RW]
did_safe_exit[RW]
did_safe_exit?[RW]
diverted_pointer[RW]
evaluation_stack[RW]
output_stream[RW]
output_stream_tags_dirty[RW]
output_stream_text_dirty[RW]
patch[RW]
previous_random[RW]
story[RW]
story_seed[RW]
turn_indicies[RW]
variables_state[RW]
visit_counts[RW]

Public Class Methods

new(story) click to toggle source
# File lib/fable/story_state.rb, line 24
def initialize(story)
  self.story = story
  self.output_stream = []
  self.output_stream_dirty!

  self.evaluation_stack = []

  self.callstack = CallStack.new(story)
  self.variables_state = VariablesState.new(callstack, story.list_definitions)

  self.visit_counts = {}
  self.turn_indicies = {}

  self.current_turn_index = -1

  # Seed the shuffle random numbers
  time_seed = Time.now.to_r * 1_000.0
  self.story_seed = IntValue.new(Random.new(time_seed).rand(100))
  self.previous_random = 0

  self.current_choices = []

  self.diverted_pointer = Pointer.null_pointer
  self.current_pointer = Pointer.null_pointer

  self.go_to_start!
end

Public Instance Methods

add_error(message, options = {is_warning: false}) click to toggle source
# File lib/fable/story_state.rb, line 532
def add_error(message, options = {is_warning: false})
  if !options[:is_warning]
    self.current_errors ||= []
    self.current_errors << message
  else
    self.current_warnings ||= []
    self.current_warnings << message
  end

  puts current_errors.inspect
  puts current_warnings.inspect
end
apply_any_patch!() click to toggle source
# File lib/fable/story_state.rb, line 513
def apply_any_patch!
  return if self.patch.nil?

  variables_state.apply_patch!

  patch.visit_counts.each do |container, new_count|
    self.visit_counts[container.path.to_s] = new_count
  end

  patch.turn_indicies.each do |container, new_count|
    self.turn_indicies[container.path.to_s] = new_count
  end
end
assert!(condition, message=nil) click to toggle source
# File lib/fable/story_state.rb, line 893
def assert!(condition, message=nil)
  story.assert!(condition, message)
end
callstack_depth() click to toggle source
# File lib/fable/story_state.rb, line 134
def callstack_depth
  callstack.depth
end
can_continue?() click to toggle source
# File lib/fable/story_state.rb, line 174
def can_continue?
  !current_pointer.null_pointer? && !has_error?
end
clean_output_whitespace(string) click to toggle source

Cleans inline whitespace in the following way:

  • Removes all whitespace from the start/end of line (including just before an n)

  • Turns all consecutive tabs & space runs into single spaces (HTML-style)

# File lib/fable/story_state.rb, line 404
def clean_output_whitespace(string)
  x = ""

  current_whitespace_start = -1
  start_of_line = 0

  string.each_char.with_index do |character, i|
    is_inline_whitespace = (character == " " || character == "\t")

    if is_inline_whitespace && current_whitespace_start == -1
      current_whitespace_start = i
    end

    if !is_inline_whitespace
      if(character != "\n" && (current_whitespace_start > 0) && current_whitespace_start != start_of_line)
        x += " "
      end

      current_whitespace_start = -1
    end

    if character == "\n"
      start_of_line = i + 1
    end

    if !is_inline_whitespace
      x << character
    end
  end

  return x

  # x = string.each_line(chomp: true).map do |line|
  #   if line.empty?
  #     nil
  #   else
  #     line.strip.gsub(MULTIPLE_WHITESPACE_REGEX, ' ') + "\n"
  #   end
  # end
  # cleaned_string = x.compact.join("\n")

  # cleaned_string
end
complete_function_evaluation_from_game() click to toggle source
# File lib/fable/story_state.rb, line 350
def complete_function_evaluation_from_game
  if callstack.current_element.type != PushPopType::TYPES[:function_evaluation_from_game]
    raise Error, "Expected external function evaluation to be complete. Stack trace: #{callstack.call_stack_trace}"
  end

  original_evaluation_stack_height = callstack.current_element.evaluation_stack_height_when_pushed

  # do we have a returned value?
  # Potentially pop multiple values off the stack, in case we need to clean up after ourselves
  # (e.g: caller of evaluate_function may have passed too many arguments, and we currently have no way
  # to check for that)
  returned_object = nil
  while evaluation_stack.size > original_evaluation_stack_height
    popped_object = pop_evaluation_stack
    if returned_object.nil?
      returned_object = popped_object
    end
  end

  # Finally, pop the external function evaluation
  pop_callstack(PushPopType::TYPES[:function_evaluation_from_game])

  # What did we get back?
  if !returned_object.nil?
    if returned_object.is_a?(Void)
      return nil
    end

    # DivertTargets get returned as the string of components
    # (rather than a Path, which isn't public)
    if returned_object.is_a?(DivertTargetValue)
      return returned_object.value_object.to_s
    end

    # Other types can just have their exact object type.
    # VariablePointers get returned as strings.
    return returned_object.value_object
  end

  return nil
end
copy_and_start_patching!() click to toggle source

WARNING: Any RuntimeObject content referenced within the StoryState will be re-referenced rather than cloned. This is generally okay though, since RuntimeObjects are treated as immutable after they've been set up. (eg: We don't edit a StringValue after it's been created and added)

# File lib/fable/story_state.rb, line 457
def copy_and_start_patching!
  copy = self.class.new(story)
  copy.patch = StatePatch.new(self.patch)

  copy.output_stream += self.output_stream
  copy.output_stream_dirty!

  copy.current_choices += @current_choices
  if has_error?
    copy.current_errors = []
    copy.current_errors += self.current_errors
  end

  if has_warning?
    copy.current_warnings = []
    copy.current_warnings += self.current_warnings
  end

  copy.callstack = CallStack.new(story).from_hash!(self.callstack.to_hash, story)
  # reference copoy- exactly the same variable state!
  # we're expected not to read it only while in patch mode
  # (though the callstack will be modified)
  copy.variables_state = self.variables_state
  copy.variables_state.callstack = copy.callstack
  copy.variables_state.patch = copy.patch

  copy.evaluation_stack += self.evaluation_stack

  if !self.diverted_pointer.null_pointer?
    copy.diverted_pointer = self.diverted_pointer
  end

  copy.previous_pointer = self.previous_pointer

  # Visit counts & turn indicies will be read-only, not modified
  # while in patch mode
  copy.visit_counts = self.visit_counts
  copy.turn_indicies = self.turn_indicies

  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
end
current_path_string() click to toggle source
# File lib/fable/story_state.rb, line 150
def current_path_string
  if current_pointer.null_pointer?
    return nil
  else
    return current_pointer.path.to_s
  end
end
current_pointer() click to toggle source
# File lib/fable/story_state.rb, line 158
def current_pointer
  callstack.current_element.current_pointer
end
current_pointer=(value) click to toggle source
# File lib/fable/story_state.rb, line 162
def current_pointer=(value)
  callstack.current_element.current_pointer = value
end
exit_function_evaluation_from_game?() click to toggle source
# File lib/fable/story_state.rb, line 340
def exit_function_evaluation_from_game?
  if callstack.current_element.type == PushPopType::TYPES[:function_evaluation_from_game]
    self.current_pointer = Pointer.null_pointer
    self.did_safe_exit = true
    return true
  end

  return false
end
force_end!() click to toggle source

<summary> Ends the current ink flow, unwrapping the callstack but without affecting any variables. Useful if the ink is (say) in the middle a nested tunnel, and you want it to reset so that you can divert elsewhere using choose_path_string. Otherwise, after finishing the content you diverted to, it would continue where it left off. Calling this is equivalent to calling -> END in ink. </summary>

# File lib/fable/story_state.rb, line 271
def force_end!
  callstack.reset!
  @current_choices.clear
  self.current_pointer = Pointer.null_pointer
  self.previous_pointer = Pointer.null_pointer
  self.did_safe_exit = true
end
from_hash!(loaded_state) click to toggle source

Load a previously saved state from a Hash

# File lib/fable/story_state.rb, line 848
def from_hash!(loaded_state)
  if loaded_state["inkSaveVersion"].nil?
    raise Error, "ink save format incorrect, can't load."
  end

  if loaded_state["inkSaveVersion"] < MINIMUM_COMPATIBLE_INK_LOAD_VERSION
    raise Error, "Ink save format isn't compatible with the current version (saw #{loaded_state["inkSaveVersion"]}, but minimum is #{MINIMUM_COMPATIBLE_INK_LOAD_VERSION}), so can't load."
  end

  self.callstack.from_hash!(loaded_state["callstackThreads"], story)
  self.variables_state.from_hash!(loaded_state["variablesState"])

  self.evaluation_stack = Serializer.convert_to_runtime_objects(loaded_state["evalStack"])
  self.output_stream = Serializer.convert_to_runtime_objects(loaded_state["outputStream"])
  self.output_stream_dirty!

  self.current_choices = Serializer.convert_to_runtime_objects(loaded_state["currentChoices"])

  if loaded_state.has_key?("currentDivertTarget")
    divert_path = Path.new(loaded_state["currentDivertTarget"])
    self.diverted_pointer = story.pointer_at_path(divert_path)
  end

  self.visit_counts = loaded_state["visitCounts"]
  self.turn_indicies = loaded_state["turnIndicies"]

  self.current_turn_index = loaded_state["turnIdx"]
  self.story_seed = loaded_state["storySeed"]

  self.previous_random = loaded_state["previousRandom"] || 0


  saved_choice_threads = loaded_state["choiceThreads"] || {}

  @current_choices.each do |choice|
    found_active_thread = callstack.thread_with_index(choice.original_thread_index)
    if !found_active_thread.nil?
      choice.thread_at_generation = found_active_thread.copy
    else
      saved_choice_thread = saved_choice_threads[choice.original_thread_index.to_s]
      choice.thread_at_generation = CallStack::Thread.new(saved_choice_thread, story)
    end
  end
end
generated_choices() click to toggle source
# File lib/fable/story_state.rb, line 146
def generated_choices
  return @current_choices
end
go_to_start!() click to toggle source
# File lib/fable/story_state.rb, line 397
def go_to_start!
  callstack.current_element.current_pointer = Pointer.start_of(story.main_content_container)
end
has_error?() click to toggle source
# File lib/fable/story_state.rb, line 178
def has_error?
  !current_errors.nil? && current_errors.size > 0
end
has_patch?() click to toggle source
# File lib/fable/story_state.rb, line 448
def has_patch?
  !patch.nil?
end
has_warning?() click to toggle source
# File lib/fable/story_state.rb, line 182
def has_warning?
  !current_warnings.nil? && current_warnings.size > 0
end
in_expression_evaluation=(value) click to toggle source
# File lib/fable/story_state.rb, line 210
def in_expression_evaluation=(value)
  callstack.current_element.in_expression_evaluation = value
end
in_expression_evaluation?() click to toggle source
# File lib/fable/story_state.rb, line 206
def in_expression_evaluation?
  callstack.current_element.in_expression_evaluation?
end
in_string_evaluation?() click to toggle source
# File lib/fable/story_state.rb, line 214
def in_string_evaluation?
  @output_stream.reverse_each.any? do |item|
    item.is_a?(ControlCommand) && item.command_type == :BEGIN_STRING_EVALUATION_MODE
  end
end
increment_visit_count_for_container!(container) click to toggle source
# File lib/fable/story_state.rb, line 93
def increment_visit_count_for_container!(container)
  if has_patch?
    current_count = visit_count_for_container(container)
    patch.set_visit_count(container, current_count.value + 1)
    return
  end

  container_path_string = container.path.to_s
  count = (visit_counts[container_path_string] || 0)
  count += 1
  visit_counts[container_path_string] = count
end
output_stream_contains_content?() click to toggle source
# File lib/fable/story_state.rb, line 800
def output_stream_contains_content?
  @output_stream.any?{|x| x.is_a?(StringValue) }
end
output_stream_dirty!() click to toggle source
# File lib/fable/story_state.rb, line 392
def output_stream_dirty!
  @output_stream_text_dirty = true
  @output_stream_tags_dirty = true
end
output_stream_ends_in_newline?() click to toggle source
# File lib/fable/story_state.rb, line 795
def output_stream_ends_in_newline?
  return false if @output_stream.empty?
  return @output_stream.last.is_a?(StringValue) && @output_stream.last.is_newline?
end
pass_arguments_to_evaluation_stack(arguments) click to toggle source
# File lib/fable/story_state.rb, line 322
def pass_arguments_to_evaluation_stack(arguments)
  if !arguments.nil?
    arguments.each do |argument|
      if !(argument.is_a?(Numeric) || argument.is_a?(String) || argument.is_a?(InkList))
        raise ArgumentError, "ink arguments when calling evaluate_function/choose_path_string_with_parameters must be int, float, string, or InkList. Argument was #{argument.class.to_s}"
      end

      push_evaluation_stack(Value.create(argument))
    end
  end
end
peek_evaluation_stack() click to toggle source
# File lib/fable/story_state.rb, line 258
def peek_evaluation_stack
  return evaluation_stack.last
end
pop_callstack(pop_type=nil) click to toggle source
# File lib/fable/story_state.rb, line 313
def pop_callstack(pop_type=nil)
  # At the end of a function call, trim any whitespace from the end
  if callstack.current_element.type == PushPopType::TYPES[:function]
    trim_whitespace_from_function_end!
  end

  callstack.pop!(pop_type)
end
pop_evaluation_stack(number_of_items = nil) click to toggle source
# File lib/fable/story_state.rb, line 246
def pop_evaluation_stack(number_of_items = nil)
  if number_of_items.nil?
    return evaluation_stack.pop
  end

  if number_of_items > evaluation_stack.size
    raise Error, "trying to pop too many objects"
  end

  return evaluation_stack.pop(number_of_items)
end
pop_from_output_stream() click to toggle source
# File lib/fable/story_state.rb, line 571
def pop_from_output_stream
  results = output_stream.pop
  output_stream_dirty!
  return results
end
previous_pointer() click to toggle source
# File lib/fable/story_state.rb, line 166
def previous_pointer
  callstack.current_thread.previous_pointer
end
previous_pointer=(value) click to toggle source
# File lib/fable/story_state.rb, line 170
def previous_pointer=(value)
  callstack.current_thread.previous_pointer = value
end
push_evaluation_stack(object) click to toggle source
# File lib/fable/story_state.rb, line 220
def push_evaluation_stack(object)
  # include metadata about the origin List for list values when they're used
  # so that lower-level functions can make sure of the origin list to get
  # Related items, or make comparisons with integer values
  if object.is_a?(ListValue)
    # Update origin when list has something to indicate the list origin
    raw_list = object.value
    if !raw_list.origin_names.nil?
      if raw_list.origins.nil?
        raw_list.origins = []
      end

      raw_list.origins.clear

      raw_list.origin_names.each do |name|
        list_definition = story.list_definitions.find_list(name)
        if !raw_list.origins.include?(list_definition)
          raw_list.origins << list_definition
        end
      end
    end
  end

  evaluation_stack << object
end
push_item_to_output_stream(object) click to toggle source
# File lib/fable/story_state.rb, line 577
def push_item_to_output_stream(object)
  include_in_output = true

  case object
  when Glue
    # new glue, so chomp away any whitespace from the end of the stream
    trim_newlines_from_output_stream!
    include_in_output = true
  when StringValue
    # New text: do we really want to append it, if it's whitespace?
    # Two different reasons for whitespace to be thrown away:
    # - Function start/end trimming
    # - User defined glue: <>
    # We also need to know when to stop trimming, when there's no whitespace

    # where does the current function call begin?
    function_trim_index = -1
    current_element = callstack.current_element
    if current_element.type == PushPopType::TYPES[:function]
      function_trim_index = current_element.function_start_in_output_stream
    end

    # Do 2 things:
    # - Find latest glue
    # - Check whether we're in the middle of string evaluation
    # If we're in string evaluation within the current function, we don't want to
    # trim back further than the length of the current string
    glue_trim_index = -1

    i = @output_stream.count - 1
    while i >= 0
      item_to_check = @output_stream[i]
      if item_to_check.is_a?(Glue)
        glue_trim_index = i
        break
      elsif ControlCommand.is_instance_of?(item_to_check, :BEGIN_STRING_EVALUATION_MODE)
        if i >= function_trim_index
          function_trim_index =  -1
        end
        break
      end

      i -= 1
    end

    # Where is the most aggresive (earliest) trim point?
    trim_index = -1
    if glue_trim_index != -1 && function_trim_index != -1
      trim_index = [glue_trim_index, function_trim_index].min
    elsif glue_trim_index != -1
      trim_index = glue_trim_index
    else
      trim_index = function_trim_index
    end

    # So, what are we trimming them?
    if trim_index != -1
      # While trimming, we want to throw all newlines away,
      # Whether due to glue, or start of a function
      if object.is_newline?
        include_in_output = false
      # Able to completely reset when normal text is pushed
      elsif object.is_nonwhitespace?
        if glue_trim_index > -1
          remove_existing_glue!
        end

        # Tell all functionms in callstack that we have seen proper text,
        # so trimming whitespace at the start is done
        if function_trim_index > -1
          callstack.elements.reverse_each do |element|
            if element.type == PushPopType::TYPES[:function]
              element.function_start_in_output_stream = -1
            else
              break
            end
          end
        end
      end
    # De-duplicate newlines, and don't ever lead with a newline
    elsif object.is_newline?
      if output_stream_ends_in_newline? || !output_stream_contains_content?
        include_in_output = false
      end
    end
  end

  if include_in_output
    @output_stream << object
    output_stream_dirty!
  end
end
push_to_output_stream(object) click to toggle source
# File lib/fable/story_state.rb, line 554
def push_to_output_stream(object)
  if object.is_a?(StringValue)
    lines = try_splitting_head_tail_whitespace(object.value)
    if !lines.nil?
      lines.each do |line|
        push_item_to_output_stream(line)
      end

      output_stream_dirty!
      return
    end
  end

  push_item_to_output_stream(object)
  output_stream_dirty!
end
record_turn_index_visit_to_container!(container) click to toggle source
# File lib/fable/story_state.rb, line 106
def record_turn_index_visit_to_container!(container)
  if has_patch?
    patch.set_turn_index(container, current_turn_index)
    return
  end

  container_path_string = container.path.to_s
  turn_indicies[container_path_string] = current_turn_index
end
remove_existing_glue!() click to toggle source

Only called when non-whitespace is appended

# File lib/fable/story_state.rb, line 784
def remove_existing_glue!
  @output_stream.each_with_index do |object, i|
    if object.is_a?(Glue)
      @output_stream.delete_at(i)
    elsif object.is_a?(ControlCommand)
    end
  end

  output_stream_dirty!
end
reset_errors!() click to toggle source
# File lib/fable/story_state.rb, line 527
def reset_errors!
  self.current_errors = nil
  self.current_warnings = nil
end
reset_output!(objects_to_add = nil) click to toggle source
# File lib/fable/story_state.rb, line 545
def reset_output!(objects_to_add = nil)
  self.output_stream = []
  if !objects_to_add.nil?
    self.output_stream += objects_to_add
  end

  output_stream_dirty!
end
restore_after_patch!() click to toggle source
# File lib/fable/story_state.rb, line 505
def restore_after_patch!
  # VariablesState was being borrowed by the patched state, so restore it
  # with our own callstack. patch will be nil normally, but if you're in the
  # middle of a save, it may contain a patch for save purposes
  variables_state.callstack = callstack
  variables_state.patch = self.patch
end
set_chosen_path(path, incrementing_turn_index) click to toggle source

Don't make public since the method needs to be wrapped in a story for visit countind

# File lib/fable/story_state.rb, line 898
def set_chosen_path(path, incrementing_turn_index)
  # Changing direction, assume we need to clear current set of choices
  @current_choices.clear

  new_pointer = story.pointer_at_path(path)

  if !new_pointer.null_pointer? && new_pointer.index == -1
    new_pointer.index = 0
  end

  self.current_pointer = new_pointer

  if incrementing_turn_index
    self.current_turn_index += 1
  end
end
start_function_evaluation_from_game(function_container, arguments) click to toggle source
# File lib/fable/story_state.rb, line 334
def start_function_evaluation_from_game(function_container, arguments)
  callstack.push(PushPopType::TYPES[:function_evaluation_from_game], output_stream_length_when_pushed: evaluation_stack.size)
  callstack.current_element.current_pointer = Pointer.start_of(function_container)
  pass_arguments_to_evaluation_stack(arguments)
end
to_hash() click to toggle source

Exports the current state to a hash that can be serialized in the JSON format

# File lib/fable/story_state.rb, line 806
def to_hash
  result = {}

  has_choice_threads = false

  self.current_choices.each do |choice|
    choice.original_thread_index = choice.thread_at_generation.thread_index
    if callstack.thread_with_index(choice.original_thread_index).nil?
      if !has_choice_threads
        has_choice_threads = true
        result["choiceThreads"]= {}
      end

      result["choiceThreads"][choice.original_thread_index.to_s] = choice.thread_at_generation.to_hash
    end
  end

  result["callstackThreads"] = callstack.to_hash
  result["variablesState"] = variables_state.to_hash
  result["evalStack"] = Serializer.convert_array_of_runtime_objects(self.evaluation_stack)
  result["outputStream"] = Serializer.convert_array_of_runtime_objects(self.output_stream)
  result["currentChoices"] = Serializer.convert_choices(@current_choices)

  if !self.diverted_pointer.null_pointer?
    result["currentDivertTarget"] = self.diverted_pointer.path.components_string
  end

  result["visitCounts"] = self.visit_counts
  result["turnIndicies"] = self.turn_indicies

  result["turnIdx"] = self.current_turn_index
  result["story_seed"] = self.story_seed
  result["previousRandom"] = self.previous_random

  result["inkSaveVersion"] = CURRENT_INK_SAVE_STATE_VERSION

  result["inkFormatVersion"] = Story::CURRENT_INK_VERSION

  return result
end
trim_newlines_from_output_stream!() click to toggle source
# File lib/fable/story_state.rb, line 754
def trim_newlines_from_output_stream!
  remove_whitespace_from = -1

  # Work back from the end, and try to find the point where we need to
  # start removing content.
  #  - Simply work backwards to find the first newline in a string of whitespace
  # e.g. This is the content   \n   \n\n
  #                            ^---------^ whitespace to remove
  #                        ^--- first while loop stops here
  i = @output_stream.count - 1
  while i >= 0
    object = @output_stream[i]
    if object.is_a?(ControlCommand) || (object.is_a?(StringValue) && object.is_nonwhitespace?)
      break
    elsif object.is_a?(StringValue) && object.is_newline?
      remove_whitespace_from = i
    end

    i -= 1
  end

  # Remove the whitespace
  if remove_whitespace_from >= 0
    self.output_stream = self.output_stream[0..(remove_whitespace_from-1)]
  end

  output_stream_dirty!
end
trim_whitespace_from_function_end!() click to toggle source

At the end of a function call, trim any whitespace from the end. We always trim the start and end of the text that a function produces. The start whitespace is discard as it is generated, and the end whitespace is trimmed in one go here when we pop the function.

# File lib/fable/story_state.rb, line 283
def trim_whitespace_from_function_end!
  assert!(callstack.current_element.type == PushPopType::TYPES[:function])

  function_start_point = callstack.current_element.function_start_in_output_stream

  # if the start point has become -1, it means that some non-whitespace
  # text has been pushed, so it's safe to go as far back as we're able
  if function_start_point == -1
    function_start_point = 0
  end

  i = @output_stream.count - 1

  # Trim whitespace from END of function call
  while i >= function_start_point
    object = output_stream[i]
    break if object.is_a?(ControlCommand)
    next if !object.is_a?(StringValue)

    if object.is_newline? || object.is_inline_whitespace?
      @output_stream.delete_at(i)
      output_stream_dirty!
    else
      break
    end

    i -= 1
  end
end
try_splitting_head_tail_whitespace(string) click to toggle source

At both the start and the end of the string, split out the new lines like so:

"   \n  \n     \n  the string \n is awesome \n     \n     "
    ^-----------^                           ^-------^

Excess newlines are converted into single newlines, and spaces discarded. Outside spaces are significant and retained. “Interior” newlines within the main string are ignored, since this is for the purpose of gluing only.

- If no splitting is necessary, null is returned.
- A newline on its own is returned in a list for consistency.
# File lib/fable/story_state.rb, line 681
def try_splitting_head_tail_whitespace(string)
  head_first_newline_index = -1
  head_last_newline_index = -1

  string.each_char.each_with_index do |character, i|
    if character == "\n"
      if head_first_newline_index == -1
        head_first_newline_index = i
      end

      head_last_newline_index = i
    elsif character == " " || character == "\t"
      next
    else
      break
    end
  end

  tail_first_newline_index = -1
  tail_last_newline_index = -1
  string.reverse.each_char.each_with_index do |character, i|
    if character == "\n"
      if tail_last_newline_index == -1
        tail_last_newline_index = i
      end

      tail_first_newline_index = i
    elsif character == " " || character == "\t"
      next
    else
      break
    end
  end

  if head_first_newline_index == -1 && tail_last_newline_index == -1
    return nil
  end

  list_texts = []
  inner_string_start = 0
  inner_string_end = string.length

  if head_first_newline_index != -1
    if head_first_newline_index > 0
      leading_spaces = string[0, head_first_newline_index]
      list_texts << leading_spaces
    end

    list_texts << "\n"
    inner_string_start = head_last_newline_index + 1
  end

  if tail_last_newline_index != -1
    inner_string_end = tail_first_newline_index
  end

  if inner_string_end > inner_string_start
    inner_string_text = string[inner_string_start, (inner_string_end - inner_string_start)]
    list_texts << inner_string_text
  end

  if tail_last_newline_index != -1 && tail_first_newline_index > head_last_newline_index
    list_texts << "\n"
    if tail_last_newline_index < (string.length -1)
      number_of_spaces = (string.length - tail_last_newline_index) - 1
      trailing_spaces = string[tail_last_newline_index + 1, number_of_spaces]
      list_texts << trailing_spaces
    end
  end

  return list_texts.map{|x| StringValue.new(x) }
end
turns_since_for_container(container) click to toggle source
# File lib/fable/story_state.rb, line 116
def turns_since_for_container(container)
  if !container.turn_index_should_be_counted?
    story.add_error!("TURNS_SINCE() for target (#{container.name}) - on #{container.debug_metadata}) unknown.")
  end

  if has_patch? && patch.get_turn_index(container)
    return (current_turn_index - patch.get_turn_index(container))
  end

  container_path_string = container.path.to_s

  if turn_indicies[container_path_string]
    return current_turn_index - turn_indicies[container_path_string]
  else
    return -1
  end
end
visit_count_at_path_string(path_string) click to toggle source

<summary> Gets the visit/read count of a particular Container at the given path. For a knot or stitch, that path string will be in the form:

knot
knot.stitch

</summary> <returns>The number of times the specific knot or stitch has been enountered by the ink engine.</returns> <param name=“pathString”>The dot-separated path string of the specific knot or stitch.</param>

# File lib/fable/story_state.rb, line 64
def visit_count_at_path_string(path_string)
  if has_patch?
    container = story.content_at_path(Path.new(path_string)).container
    if container.nil?
      raise Error, "Content at path not found: #{path_string}"
    end

    if patch.get_visit_count(container)
      return patch.get_visit_count(container)
    end
  end

  return visit_counts[path_string] || 0
end
visit_count_for_container(container) click to toggle source
# File lib/fable/story_state.rb, line 79
def visit_count_for_container(container)
  if !container.visits_should_be_counted?
    story.add_error!("Read count for target (#{container.name} - on #{container.debug_metadata}) unknown.")
    return IntValue.new(0)
  end

  if has_patch? && patch.get_visit_count(container)
    return IntValue.new(patch.get_visit_count(container))
  end

  container_path_string = container.path.to_s
  return IntValue.new(visit_counts[container_path_string] || 0)
end