Class: Fable::StoryState

Inherits:
Object
  • Object
show all
Defined in:
lib/fable/story_state.rb

Overview

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

Constant Summary collapse

CURRENT_INK_SAVE_STATE_VERSION =
8
MINIMUM_COMPATIBLE_INK_LOAD_VERSION =
8
MULTIPLE_WHITESPACE_REGEX =
/[ \t]{2,}/

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(story) ⇒ StoryState

Returns a new instance of StoryState.



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# 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

Instance Attribute Details

#callstackObject

Returns the value of attribute callstack.



13
14
15
# File 'lib/fable/story_state.rb', line 13

def callstack
  @callstack
end

#current_choicesObject

Returns the value of attribute current_choices.



13
14
15
# File 'lib/fable/story_state.rb', line 13

def current_choices
  @current_choices
end

#current_errorsObject

Returns the value of attribute current_errors.



13
14
15
# File 'lib/fable/story_state.rb', line 13

def current_errors
  @current_errors
end

#current_tagsObject

Returns the value of attribute current_tags.



13
14
15
# File 'lib/fable/story_state.rb', line 13

def current_tags
  @current_tags
end

#current_textObject

Returns the value of attribute current_text.



13
14
15
# File 'lib/fable/story_state.rb', line 13

def current_text
  @current_text
end

#current_turn_indexObject

Returns the value of attribute current_turn_index.



13
14
15
# File 'lib/fable/story_state.rb', line 13

def current_turn_index
  @current_turn_index
end

#current_warningsObject

Returns the value of attribute current_warnings.



13
14
15
# File 'lib/fable/story_state.rb', line 13

def current_warnings
  @current_warnings
end

#did_safe_exitObject Also known as: did_safe_exit?

Returns the value of attribute did_safe_exit.



13
14
15
# File 'lib/fable/story_state.rb', line 13

def did_safe_exit
  @did_safe_exit
end

#diverted_pointerObject

Returns the value of attribute diverted_pointer.



13
14
15
# File 'lib/fable/story_state.rb', line 13

def diverted_pointer
  @diverted_pointer
end

#evaluation_stackObject

Returns the value of attribute evaluation_stack.



13
14
15
# File 'lib/fable/story_state.rb', line 13

def evaluation_stack
  @evaluation_stack
end

#output_streamObject

Returns the value of attribute output_stream.



13
14
15
# File 'lib/fable/story_state.rb', line 13

def output_stream
  @output_stream
end

#output_stream_tags_dirtyObject

Returns the value of attribute output_stream_tags_dirty.



13
14
15
# File 'lib/fable/story_state.rb', line 13

def output_stream_tags_dirty
  @output_stream_tags_dirty
end

#output_stream_text_dirtyObject

Returns the value of attribute output_stream_text_dirty.



13
14
15
# File 'lib/fable/story_state.rb', line 13

def output_stream_text_dirty
  @output_stream_text_dirty
end

#patchObject

Returns the value of attribute patch.



13
14
15
# File 'lib/fable/story_state.rb', line 13

def patch
  @patch
end

#previous_randomObject

Returns the value of attribute previous_random.



13
14
15
# File 'lib/fable/story_state.rb', line 13

def previous_random
  @previous_random
end

#storyObject

Returns the value of attribute story.



13
14
15
# File 'lib/fable/story_state.rb', line 13

def story
  @story
end

#story_seedObject

Returns the value of attribute story_seed.



13
14
15
# File 'lib/fable/story_state.rb', line 13

def story_seed
  @story_seed
end

#turn_indiciesObject

Returns the value of attribute turn_indicies.



13
14
15
# File 'lib/fable/story_state.rb', line 13

def turn_indicies
  @turn_indicies
end

#variables_stateObject

Returns the value of attribute variables_state.



13
14
15
# File 'lib/fable/story_state.rb', line 13

def variables_state
  @variables_state
end

#visit_countsObject

Returns the value of attribute visit_counts.



13
14
15
# File 'lib/fable/story_state.rb', line 13

def visit_counts
  @visit_counts
end

Instance Method Details

#add_error(message, options = {is_warning: false}) ⇒ Object



532
533
534
535
536
537
538
539
540
541
542
543
# 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!Object



513
514
515
516
517
518
519
520
521
522
523
524
525
# 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) ⇒ Object



893
894
895
# File 'lib/fable/story_state.rb', line 893

def assert!(condition, message=nil)
  story.assert!(condition, message)
end

#callstack_depthObject



134
135
136
# File 'lib/fable/story_state.rb', line 134

def callstack_depth
  callstack.depth
end

#can_continue?Boolean

Returns:

  • (Boolean)


174
175
176
# File 'lib/fable/story_state.rb', line 174

def can_continue?
  !current_pointer.null_pointer? && !has_error?
end

#clean_output_whitespace(string) ⇒ Object

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)



404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
# 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_gameObject



350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
# 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!Object

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)



457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
# 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_stringObject



150
151
152
153
154
155
156
# 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_pointerObject



158
159
160
# File 'lib/fable/story_state.rb', line 158

def current_pointer
  callstack.current_element.current_pointer
end

#current_pointer=(value) ⇒ Object



162
163
164
# File 'lib/fable/story_state.rb', line 162

def current_pointer=(value)
  callstack.current_element.current_pointer = value
end

#exit_function_evaluation_from_game?Boolean

Returns:

  • (Boolean)


340
341
342
343
344
345
346
347
348
# 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!Object

<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>



271
272
273
274
275
276
277
# 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) ⇒ Object

Load a previously saved state from a Hash



848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
# 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_choicesObject



146
147
148
# File 'lib/fable/story_state.rb', line 146

def generated_choices
  return @current_choices
end

#go_to_start!Object



397
398
399
# 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?Boolean

Returns:

  • (Boolean)


178
179
180
# File 'lib/fable/story_state.rb', line 178

def has_error?
  !current_errors.nil? && current_errors.size > 0
end

#has_patch?Boolean

Returns:

  • (Boolean)


448
449
450
# File 'lib/fable/story_state.rb', line 448

def has_patch?
  !patch.nil?
end

#has_warning?Boolean

Returns:

  • (Boolean)


182
183
184
# File 'lib/fable/story_state.rb', line 182

def has_warning?
  !current_warnings.nil? && current_warnings.size > 0
end

#in_expression_evaluation=(value) ⇒ Object



210
211
212
# File 'lib/fable/story_state.rb', line 210

def in_expression_evaluation=(value)
  callstack.current_element.in_expression_evaluation = value
end

#in_expression_evaluation?Boolean

Returns:

  • (Boolean)


206
207
208
# File 'lib/fable/story_state.rb', line 206

def in_expression_evaluation?
  callstack.current_element.in_expression_evaluation?
end

#in_string_evaluation?Boolean

Returns:

  • (Boolean)


214
215
216
217
218
# 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) ⇒ Object



93
94
95
96
97
98
99
100
101
102
103
104
# 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?Boolean

Returns:

  • (Boolean)


800
801
802
# 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!Object



392
393
394
395
# 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?Boolean

Returns:

  • (Boolean)


795
796
797
798
# 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) ⇒ Object



322
323
324
325
326
327
328
329
330
331
332
# 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_stackObject



258
259
260
# File 'lib/fable/story_state.rb', line 258

def peek_evaluation_stack
  return evaluation_stack.last
end

#pop_callstack(pop_type = nil) ⇒ Object



313
314
315
316
317
318
319
320
# 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) ⇒ Object



246
247
248
249
250
251
252
253
254
255
256
# 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_streamObject



571
572
573
574
575
# File 'lib/fable/story_state.rb', line 571

def pop_from_output_stream
  results = output_stream.pop
  output_stream_dirty!
  return results
end

#previous_pointerObject



166
167
168
# File 'lib/fable/story_state.rb', line 166

def previous_pointer
  callstack.current_thread.previous_pointer
end

#previous_pointer=(value) ⇒ Object



170
171
172
# File 'lib/fable/story_state.rb', line 170

def previous_pointer=(value)
  callstack.current_thread.previous_pointer = value
end

#push_evaluation_stack(object) ⇒ Object



220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# 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) ⇒ Object



577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
# 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) ⇒ Object



554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
# 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) ⇒ Object



106
107
108
109
110
111
112
113
114
# 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!Object

Only called when non-whitespace is appended



784
785
786
787
788
789
790
791
792
793
# 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!Object



527
528
529
530
# 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) ⇒ Object



545
546
547
548
549
550
551
552
# 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!Object



505
506
507
508
509
510
511
# 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) ⇒ Object

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



898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
# 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) ⇒ Object



334
335
336
337
338
# 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_hashObject

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



806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
# 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!Object



754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
# 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!Object

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.



283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# 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) ⇒ Object

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.


681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
# 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) ⇒ Object



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# 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.}) 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) ⇒ Object

<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>



64
65
66
67
68
69
70
71
72
73
74
75
76
77
# 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) ⇒ Object



79
80
81
82
83
84
85
86
87
88
89
90
91
# 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.}) 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