Class: Fable::StoryState
- Inherits:
-
Object
- Object
- Fable::StoryState
- 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
-
#callstack ⇒ Object
Returns the value of attribute callstack.
-
#current_choices ⇒ Object
Returns the value of attribute current_choices.
-
#current_errors ⇒ Object
Returns the value of attribute current_errors.
-
#current_tags ⇒ Object
Returns the value of attribute current_tags.
-
#current_text ⇒ Object
Returns the value of attribute current_text.
-
#current_turn_index ⇒ Object
Returns the value of attribute current_turn_index.
-
#current_warnings ⇒ Object
Returns the value of attribute current_warnings.
-
#did_safe_exit ⇒ Object
(also: #did_safe_exit?)
Returns the value of attribute did_safe_exit.
-
#diverted_pointer ⇒ Object
Returns the value of attribute diverted_pointer.
-
#evaluation_stack ⇒ Object
Returns the value of attribute evaluation_stack.
-
#output_stream ⇒ Object
Returns the value of attribute output_stream.
-
#output_stream_tags_dirty ⇒ Object
Returns the value of attribute output_stream_tags_dirty.
-
#output_stream_text_dirty ⇒ Object
Returns the value of attribute output_stream_text_dirty.
-
#patch ⇒ Object
Returns the value of attribute patch.
-
#previous_random ⇒ Object
Returns the value of attribute previous_random.
-
#story ⇒ Object
Returns the value of attribute story.
-
#story_seed ⇒ Object
Returns the value of attribute story_seed.
-
#turn_indicies ⇒ Object
Returns the value of attribute turn_indicies.
-
#variables_state ⇒ Object
Returns the value of attribute variables_state.
-
#visit_counts ⇒ Object
Returns the value of attribute visit_counts.
Instance Method Summary collapse
- #add_error(message, options = {is_warning: false}) ⇒ Object
- #apply_any_patch! ⇒ Object
- #assert!(condition, message = nil) ⇒ Object
- #callstack_depth ⇒ Object
- #can_continue? ⇒ Boolean
-
#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).
- #complete_function_evaluation_from_game ⇒ Object
-
#copy_and_start_patching! ⇒ Object
WARNING: Any RuntimeObject content referenced within the StoryState will be re-referenced rather than cloned.
- #current_path_string ⇒ Object
- #current_pointer ⇒ Object
- #current_pointer=(value) ⇒ Object
- #exit_function_evaluation_from_game? ⇒ Boolean
-
#force_end! ⇒ Object
<summary> Ends the current ink flow, unwrapping the callstack but without affecting any variables.
-
#from_hash!(loaded_state) ⇒ Object
Load a previously saved state from a Hash.
- #generated_choices ⇒ Object
- #go_to_start! ⇒ Object
- #has_error? ⇒ Boolean
- #has_patch? ⇒ Boolean
- #has_warning? ⇒ Boolean
- #in_expression_evaluation=(value) ⇒ Object
- #in_expression_evaluation? ⇒ Boolean
- #in_string_evaluation? ⇒ Boolean
- #increment_visit_count_for_container!(container) ⇒ Object
-
#initialize(story) ⇒ StoryState
constructor
A new instance of StoryState.
- #output_stream_contains_content? ⇒ Boolean
- #output_stream_dirty! ⇒ Object
- #output_stream_ends_in_newline? ⇒ Boolean
- #pass_arguments_to_evaluation_stack(arguments) ⇒ Object
- #peek_evaluation_stack ⇒ Object
- #pop_callstack(pop_type = nil) ⇒ Object
- #pop_evaluation_stack(number_of_items = nil) ⇒ Object
- #pop_from_output_stream ⇒ Object
- #previous_pointer ⇒ Object
- #previous_pointer=(value) ⇒ Object
- #push_evaluation_stack(object) ⇒ Object
- #push_item_to_output_stream(object) ⇒ Object
- #push_to_output_stream(object) ⇒ Object
- #record_turn_index_visit_to_container!(container) ⇒ Object
-
#remove_existing_glue! ⇒ Object
Only called when non-whitespace is appended.
- #reset_errors! ⇒ Object
- #reset_output!(objects_to_add = nil) ⇒ Object
- #restore_after_patch! ⇒ Object
-
#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.
- #start_function_evaluation_from_game(function_container, arguments) ⇒ Object
-
#to_hash ⇒ Object
Exports the current state to a hash that can be serialized in the JSON format.
- #trim_newlines_from_output_stream! ⇒ Object
-
#trim_whitespace_from_function_end! ⇒ Object
At the end of a function call, trim any whitespace from the 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:.
- #turns_since_for_container(container) ⇒ Object
-
#visit_count_at_path_string(path_string) ⇒ Object
<summary> Gets the visit/read count of a particular Container at the given path.
- #visit_count_for_container(container) ⇒ Object
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
#callstack ⇒ Object
Returns the value of attribute callstack.
13 14 15 |
# File 'lib/fable/story_state.rb', line 13 def callstack @callstack end |
#current_choices ⇒ Object
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_errors ⇒ Object
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_tags ⇒ Object
Returns the value of attribute current_tags.
13 14 15 |
# File 'lib/fable/story_state.rb', line 13 def @current_tags end |
#current_text ⇒ Object
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_index ⇒ Object
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_warnings ⇒ Object
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_exit ⇒ Object 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_pointer ⇒ Object
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_stack ⇒ Object
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_stream ⇒ Object
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_dirty ⇒ Object
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 end |
#output_stream_text_dirty ⇒ Object
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 |
#patch ⇒ Object
Returns the value of attribute patch.
13 14 15 |
# File 'lib/fable/story_state.rb', line 13 def patch @patch end |
#previous_random ⇒ Object
Returns the value of attribute previous_random.
13 14 15 |
# File 'lib/fable/story_state.rb', line 13 def previous_random @previous_random end |
#story ⇒ Object
Returns the value of attribute story.
13 14 15 |
# File 'lib/fable/story_state.rb', line 13 def story @story end |
#story_seed ⇒ Object
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_indicies ⇒ Object
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_state ⇒ Object
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_counts ⇒ Object
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(, = {is_warning: false}) if ![:is_warning] self.current_errors ||= [] self.current_errors << else self.current_warnings ||= [] self.current_warnings << 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, =nil) story.assert!(condition, ) end |
#callstack_depth ⇒ Object
134 135 136 |
# File 'lib/fable/story_state.rb', line 134 def callstack_depth callstack.depth end |
#can_continue? ⇒ 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_game ⇒ Object
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_string ⇒ Object
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_pointer ⇒ Object
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
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_choices ⇒ Object
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
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
448 449 450 |
# File 'lib/fable/story_state.rb', line 448 def has_patch? !patch.nil? end |
#has_warning? ⇒ 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
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
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
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
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_stack ⇒ Object
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_stream ⇒ Object
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_pointer ⇒ Object
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_hash ⇒ Object
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 |