fable 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +30 -0
  3. data/.gitignore +57 -0
  4. data/.ruby-version +1 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +7 -0
  7. data/Gemfile.lock +30 -0
  8. data/LICENSE +21 -0
  9. data/README.md +2 -0
  10. data/Rakefile +10 -0
  11. data/bin/console +14 -0
  12. data/bin/setup +8 -0
  13. data/bin/test +8 -0
  14. data/fable.gemspec +34 -0
  15. data/fable.sublime-project +8 -0
  16. data/lib/fable.rb +49 -0
  17. data/lib/fable/call_stack.rb +351 -0
  18. data/lib/fable/choice.rb +31 -0
  19. data/lib/fable/choice_point.rb +65 -0
  20. data/lib/fable/container.rb +218 -0
  21. data/lib/fable/control_command.rb +156 -0
  22. data/lib/fable/debug_metadata.rb +13 -0
  23. data/lib/fable/divert.rb +100 -0
  24. data/lib/fable/glue.rb +7 -0
  25. data/lib/fable/ink_list.rb +425 -0
  26. data/lib/fable/list_definition.rb +44 -0
  27. data/lib/fable/list_definitions_origin.rb +35 -0
  28. data/lib/fable/native_function_call.rb +324 -0
  29. data/lib/fable/native_function_operations.rb +149 -0
  30. data/lib/fable/observer.rb +205 -0
  31. data/lib/fable/path.rb +186 -0
  32. data/lib/fable/pointer.rb +42 -0
  33. data/lib/fable/profiler.rb +287 -0
  34. data/lib/fable/push_pop_type.rb +11 -0
  35. data/lib/fable/runtime_object.rb +159 -0
  36. data/lib/fable/search_result.rb +20 -0
  37. data/lib/fable/serializer.rb +560 -0
  38. data/lib/fable/state_patch.rb +47 -0
  39. data/lib/fable/story.rb +1447 -0
  40. data/lib/fable/story_state.rb +915 -0
  41. data/lib/fable/tag.rb +14 -0
  42. data/lib/fable/value.rb +334 -0
  43. data/lib/fable/variable_assignment.rb +20 -0
  44. data/lib/fable/variable_reference.rb +38 -0
  45. data/lib/fable/variables_state.rb +327 -0
  46. data/lib/fable/version.rb +3 -0
  47. data/lib/fable/void.rb +4 -0
  48. data/zork_mode.rb +23 -0
  49. metadata +149 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 348d754ded39e5e54b960b19da8b893e562a93e0725700b5b6099a97bca47ab4
4
+ data.tar.gz: b3afee4ea96023091455c61bddd380fb6aff0fc105550990b409744ee98bbf8e
5
+ SHA512:
6
+ metadata.gz: ac9fbe59c86847d34e3852ab472a82feb0836b7741736283386bdbb0e8f28c5c326a877626df471b0a74ba67c10f0f651706ff8f8a2df3d4e2f77d2d7a205dd2
7
+ data.tar.gz: fccfb6a20f17310827da5a4a7c0137b2f8959eb92d3e6a7f98aa55da57cf69cbe6a6041755ea9d999a620c3acdb73b01dc950f3b61fd396ecf5be7e1eabfcb10
@@ -0,0 +1,30 @@
1
+ # Ruby CircleCI 2.1 configuration file
2
+ #
3
+ # Check https://circleci.com/docs/2.0/ruby/ for more details
4
+ #
5
+ version: 2.1
6
+
7
+
8
+ orbs:
9
+ ruby: circleci/ruby@0.2.1 # Ruby orb registry: https://circleci.com/orbs/registry/orb/circleci/ruby
10
+
11
+ jobs:
12
+ build:
13
+ docker:
14
+ - image: circleci/ruby:2.6.5
15
+ steps:
16
+ - checkout
17
+ - run:
18
+ name: Install Bundler 2.1.4
19
+ command: gem install bundler:2.1.4
20
+ - run:
21
+ name: Which bundler?
22
+ command: bundle -v
23
+ - ruby/install-deps
24
+ - ruby/save-cache
25
+ - run:
26
+ name: Tests
27
+ command: bin/test
28
+
29
+ # What to do next? Set up a test job. Please see
30
+ # https://circleci.com/docs/2.0/configuration-reference/, for more info on how to get started.
@@ -0,0 +1,57 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+ *.sublime-workspace
13
+
14
+ # Used by dotenv library to load environment variables.
15
+ # .env
16
+
17
+ # Ignore Byebug command history file.
18
+ .byebug_history
19
+
20
+ ## Specific to RubyMotion:
21
+ .dat*
22
+ .repl_history
23
+ build/
24
+ *.bridgesupport
25
+ build-iPhoneOS/
26
+ build-iPhoneSimulator/
27
+
28
+ ## Specific to RubyMotion (use of CocoaPods):
29
+ #
30
+ # We recommend against adding the Pods directory to your .gitignore. However
31
+ # you should judge for yourself, the pros and cons are mentioned at:
32
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
33
+ #
34
+ # vendor/Pods/
35
+
36
+ ## Documentation cache and generated files:
37
+ /.yardoc/
38
+ /_yardoc/
39
+ /doc/
40
+ /rdoc/
41
+
42
+ ## Environment normalization:
43
+ /.bundle/
44
+ /vendor/bundle
45
+ /lib/bundler/man/
46
+
47
+ # for a library or gem, you might want to ignore these files since the code is
48
+ # intended to run in multiple environments; otherwise, check them in:
49
+ # Gemfile.lock
50
+ # .ruby-version
51
+ # .ruby-gemset
52
+
53
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
54
+ .rvmrc
55
+
56
+ # Used by RuboCop. Remote config files pulled in from inherit_from directive.
57
+ # .rubocop-https?--*
@@ -0,0 +1 @@
1
+ 2.7.1
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at tcannon00@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [https://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: https://contributor-covenant.org
74
+ [version]: https://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in fable.gemspec
4
+ gemspec
5
+
6
+ gem "rake", "~> 12.0"
7
+ gem "minitest", "~> 5.0"
@@ -0,0 +1,30 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ fable (0.5.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ byebug (11.1.1)
10
+ highline (2.0.3)
11
+ minitest (5.14.0)
12
+ minitest-line (0.6.5)
13
+ minitest (~> 5.0)
14
+ pretty-diffs (1.0.2)
15
+ rake (12.3.3)
16
+
17
+ PLATFORMS
18
+ ruby
19
+
20
+ DEPENDENCIES
21
+ byebug
22
+ fable!
23
+ highline
24
+ minitest (~> 5.0)
25
+ minitest-line
26
+ pretty-diffs
27
+ rake (~> 12.0)
28
+
29
+ BUNDLED WITH
30
+ 2.1.4
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Thomas Cannon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ # fable
2
+ A Ruby parser for Ink
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "fable"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle exec rake test
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,34 @@
1
+ require_relative 'lib/fable/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "fable"
5
+ spec.version = Fable::VERSION
6
+ spec.authors = ["Thomas Cannon"]
7
+ spec.email = ["hello@thomascannon.me"]
8
+
9
+ spec.summary = %q{An Ink runtime for Ruby}
10
+ # spec.description = %q{TODO: Write a longer description or delete this line.}
11
+ spec.homepage = "https://github.com/tcannonfodder/fable"
12
+ spec.license = "MIT"
13
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
14
+
15
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = "https://github.com/tcannonfodder/fable"
19
+ # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
+ end
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_development_dependency "byebug"
31
+ spec.add_development_dependency "highline"
32
+ spec.add_development_dependency "minitest-line"
33
+ spec.add_development_dependency "pretty-diffs"
34
+ end
@@ -0,0 +1,8 @@
1
+ {
2
+ "folders":
3
+ [
4
+ {
5
+ "path": "."
6
+ }
7
+ ]
8
+ }
@@ -0,0 +1,49 @@
1
+ module Fable
2
+ class Error < StandardError; end
3
+
4
+ class StoryError < Error
5
+ attr_accessor :use_end_line_number
6
+ alias_method :use_end_line_number?, :use_end_line_number
7
+ end
8
+
9
+ def assert!(conditional, error_message = "")
10
+ if !conditional
11
+ raise Error, error_message
12
+ end
13
+ end
14
+ end
15
+
16
+
17
+ require "fable/version"
18
+ require 'fable/runtime_object'
19
+ require "fable/void"
20
+
21
+ require 'fable/call_stack'
22
+ require 'fable/choice'
23
+ require 'fable/choice_point'
24
+ require 'fable/container'
25
+ require 'fable/control_command'
26
+ require 'fable/debug_metadata'
27
+ require 'fable/divert'
28
+ require 'fable/glue'
29
+ require 'fable/ink_list'
30
+ require 'fable/list_definition'
31
+ require 'fable/list_definitions_origin'
32
+ require 'fable/native_function_operations'
33
+ require 'fable/native_function_call'
34
+ require 'fable/observer'
35
+ require 'fable/path'
36
+ require 'fable/pointer'
37
+ require 'fable/profiler'
38
+ require 'fable/push_pop_type'
39
+ require 'fable/search_result'
40
+ require "fable/serializer"
41
+ require "fable/state_patch"
42
+ require 'fable/story'
43
+ require 'fable/variables_state'
44
+ require 'fable/story_state'
45
+ require 'fable/tag'
46
+ require 'fable/value'
47
+ require 'fable/variable_assignment'
48
+ require 'fable/variable_reference'
49
+
@@ -0,0 +1,351 @@
1
+ module Fable
2
+ class CallStack
3
+ attr_accessor :threads, :thread_counter, :start_of_root
4
+
5
+ def initialize(story_context_or_call_stack)
6
+ if story_context_or_call_stack.is_a?(Story)
7
+ start_of_root = Pointer.start_of(story_context_or_call_stack.root_content_container)
8
+ reset!
9
+ elsif story_context_or_call_stack.is_a?(CallStack)
10
+ call_stack_to_copy = story_context_or_call_stack
11
+ self.threads = []
12
+
13
+ call_stack_to_copy.threads.each do |thread|
14
+ self.threads << thread.copy
15
+ end
16
+
17
+ self.thread_container = call_stack_to_copy.thread_counter
18
+ self.start_of_root = call_stack_to_copy.start_of_root
19
+ end
20
+ end
21
+
22
+ def reset!
23
+ new_thread = Thread.new
24
+ new_thread.call_stack << Element.new(:tunnel, self.start_of_root)
25
+ self.threads = [new_thread]
26
+ self.thread_counter = 0
27
+ end
28
+
29
+ def elements
30
+ call_stack
31
+ end
32
+
33
+ def depth
34
+ elements.size
35
+ end
36
+
37
+ def current_element
38
+ thread = threads.last
39
+ thread.call_stack.last
40
+ end
41
+
42
+ def current_element_index
43
+ call_stack.size - 1
44
+ end
45
+
46
+ def current_thread
47
+ threads.last
48
+ end
49
+
50
+ def current_thread=(value)
51
+ if threads.size != 1
52
+ raise StoryError, "Shouldn't be directly setting the current thread when we have a stack of them"
53
+ end
54
+
55
+ threads.clear
56
+ threads << value
57
+ end
58
+
59
+ def can_pop_thread?
60
+ threads.size > 1 && !element_is_evaluate_from_game?
61
+ end
62
+
63
+ def element_is_evaluate_from_game?
64
+ current_element.type == PushPopType::TYPES[:function_evaluation_from_game]
65
+ end
66
+
67
+ def push(type, options = {external_evaluation_stack_height: 0, output_stream_length_when_pushed: 0})
68
+ external_evaluation_stack_height = options[:external_evaluation_stack_height] || 0
69
+ output_stream_length_when_pushed = options[:output_stream_length_when_pushed] || 0
70
+ # When pushing to callstack, maintain the current content path, but jump
71
+ # out of expressions by default
72
+ element = Element.new(type, current_element.current_pointer, in_expression_evaluation: false)
73
+ element.evaluation_stack_height_when_pushed = external_evaluation_stack_height
74
+ element.function_start_in_output_stream = output_stream_length_when_pushed
75
+
76
+ self.call_stack << element
77
+ end
78
+
79
+ def can_pop?(type = nil)
80
+ return false if call_stack.size <= 1
81
+ return true if type.nil?
82
+ return current_element.type == type
83
+ end
84
+
85
+ def pop!(type=nil)
86
+ if can_pop?(type)
87
+ call_stack.pop
88
+ else
89
+ raise Error, "Mismatched push/pop in Callstack"
90
+ end
91
+ end
92
+
93
+ # Get variable value, dereferencing a variable pointer if necessary
94
+ def get_temporary_variable_with_name(name, context_index = -1)
95
+ if context_index == -1
96
+ context_index = current_element_index + 1
97
+ end
98
+
99
+ context_element = call_stack[context_index - 1]
100
+
101
+ return context_element.temporary_variables[name]
102
+ end
103
+
104
+ def set_temporary_variable(name, value, declare_new, context_index = -1)
105
+ if context_index == -1
106
+ context_index = current_element_index + 1
107
+ end
108
+
109
+ context_element = call_stack[context_index - 1]
110
+
111
+ if !declare_new && !context_element.temporary_variables.has_key?(name)
112
+ raise Error, "Could not find temporary variable to set: #{name}"
113
+ end
114
+
115
+ if context_element.temporary_variables.has_key?(name)
116
+ old_value = context_element.temporary_variables[name]
117
+ ListValue.retain_list_origins_for_assignment(old_value, value)
118
+ end
119
+
120
+ context_element.temporary_variables[name] = value
121
+ end
122
+
123
+ # Find the most appropriate context for this variable. Are we referencing
124
+ # a temporary or global variable? Note that the compiler will have warned
125
+ # us about possible conflicts, so anything that happens here should be safe
126
+ def context_for_variable_named(name)
127
+ # Current temporary context?
128
+ # (Shouldn't attempt to access contexts higher in the callstack)
129
+ if current_element.temporary_variables.has_key?(name)
130
+ return current_element_index + 1
131
+ else
132
+ # Global
133
+ return 0
134
+ end
135
+ end
136
+
137
+ def thread_with_index(index)
138
+ threads.find{|thread| thread.thread_index == index}
139
+ end
140
+
141
+ def call_stack
142
+ current_thread.call_stack
143
+ end
144
+
145
+ def push_thread!
146
+ new_thread = current_thread.copy
147
+ self.thread_counter += 1
148
+ self.threads << new_thread
149
+ end
150
+
151
+ def fork_thread!
152
+ forked_thread = current_thread.copy
153
+ self.thread_counter += 1
154
+ forked_thread.thread_index = self.thread_counter
155
+
156
+ return forked_thread
157
+ end
158
+
159
+ def pop_thread!
160
+ if can_pop_thread?
161
+ threads.delete(current_thread)
162
+ else
163
+ raise Error, "Can't pop thread"
164
+ end
165
+ end
166
+
167
+ def from_hash!(hash_to_use, story_context)
168
+ self.threads = []
169
+
170
+ hash_to_use["threads"].each do |thread_object|
171
+ self.threads << Thread.new(thread_object, story_context)
172
+ end
173
+
174
+ self.thread_counter = hash_to_use["threadCounter"]
175
+ self.start_of_root = Pointer.start_of(story_context.root_content_container)
176
+ self
177
+ end
178
+
179
+ def to_hash
180
+ export = {}
181
+ export["threads"] = []
182
+ self.threads.each do |thread|
183
+ export["threads"] << thread.to_hash
184
+ end
185
+
186
+ export["threadCounter"] = self.thread_counter
187
+ export
188
+ end
189
+
190
+ def call_stack_trace
191
+ result = StringIO.new
192
+
193
+ self.threads.each_with_index do |thread, i|
194
+ is_current_thread = thread == current_thread
195
+
196
+ result << "=== THREAD #{i}/#{threads.count} #{'(current)' if is_current_thread }\n"
197
+
198
+ thread.call_stack.each do |element|
199
+ case element.type
200
+ when :function
201
+ result << " [FUNCTION] "
202
+ when :tunnel
203
+ result << " [TUNNEL] "
204
+ end
205
+
206
+ pointer = element.current_pointer
207
+ if !pointer.null_pointer?
208
+ result << "<SOMEWHERE IN #{pointer.container.path.to_s}>\n"
209
+ end
210
+ end
211
+ end
212
+
213
+ result.rewind
214
+ result.read
215
+ end
216
+
217
+ class Element
218
+ attr_accessor :current_pointer, :in_expression_evaluation,
219
+ :temporary_variables, :type
220
+
221
+ alias_method :in_expression_evaluation?, :in_expression_evaluation
222
+
223
+ # When this callstack element is actually a function evaluation called
224
+ # from the game, we need to keep track of when it was called so that
225
+ # we know whether there was any return value
226
+ attr_accessor :evaluation_stack_height_when_pushed
227
+
228
+ # When functions are called, we trim whitespace from the start & end of
229
+ # what they generate, so we make sure we know where the function's
230
+ # start/end are
231
+ attr_accessor :function_start_in_output_stream
232
+
233
+ def initialize(type, pointer, options = {in_expression_evaluation: false})
234
+ self.current_pointer = pointer.dup
235
+ self.in_expression_evaluation = options[:in_expression_evaluation]
236
+ self.temporary_variables = {}
237
+ self.function_start_in_output_stream = 0
238
+ self.evaluation_stack_height_when_pushed = 0
239
+ self.type = type
240
+ end
241
+
242
+ def copy
243
+ copied_element = self.class.new(type, current_pointer, in_expression_evaluation: in_expression_evaluation)
244
+ copied_element.temporary_variables = Serializer.convert_to_runtime_objects_hash(Serializer.convert_hash_of_runtime_objects(temporary_variables))
245
+ copied_element.evaluation_stack_height_when_pushed = evaluation_stack_height_when_pushed
246
+ copied_element.function_start_in_output_stream = function_start_in_output_stream
247
+ copied_element
248
+ end
249
+ end
250
+
251
+ class Thread
252
+ attr_accessor :call_stack, :thread_index, :previous_pointer
253
+
254
+
255
+ def initialize(*arguments)
256
+ self.previous_pointer = Pointer.null_pointer
257
+
258
+ if arguments.size == 0
259
+ self.call_stack = []
260
+ else
261
+ self.initialize_with_thread_object_and_story_context(arguments[0], arguments[1])
262
+ end
263
+ end
264
+
265
+ def initialize_with_thread_object_and_story_context(thread_object, story_context)
266
+ self.call_stack = []
267
+ self.thread_index = thread_object["threadIndex"]
268
+
269
+ thread_object["callstack"].each do |element|
270
+ type = PushPopType::TYPE_LOOKUP[element["type"]]
271
+ pointer = Pointer.null_pointer
272
+
273
+ current_container_path_string = element["cPath"]
274
+
275
+ if current_container_path_string
276
+ thread_pointer_result = story_context.content_at_path(Path.new(current_container_path_string))
277
+ pointer.container = thread_pointer_result.container
278
+ pointer.index = element["idx"]
279
+
280
+ if thread_pointer_result.object.nil?
281
+ raise Error, "When loading state, internal story location couldn't be found: #{current_container_path_string}. Has the story changed since this save data was created?"
282
+ elsif thread_pointer_result.approximate?
283
+ story_context.warning("When loading state, internal story location couldn't be found: #{current_container_path_string}, so it wa approximated to #{pointer.container.path.to_s} to recover. Has the story changed since this save data was created?")
284
+ end
285
+ end
286
+
287
+ in_expression_evaluation = element["exp"]
288
+
289
+ new_element = Element.new(type, pointer, in_expression_evaluation: in_expression_evaluation)
290
+
291
+ if element["temp"]
292
+ new_element.temporary_variables = Serializer.convert_to_runtime_objects_hash(element["temp"])
293
+ else
294
+ new_element.temporary_variables = {}
295
+ end
296
+
297
+ self.call_stack << new_element
298
+ end
299
+
300
+ if thread_object["previousContentObject"]
301
+ previous_path = Path.new(thread_object["previousContentObject"])
302
+ self.previous_pointer = story_context.pointer_at_path(previous_path)
303
+ end
304
+
305
+ self
306
+ end
307
+
308
+ def copy
309
+ copied_thread = self.class.new
310
+ copied_thread.thread_index = thread_index
311
+ self.call_stack.each do |element|
312
+ copied_thread.call_stack << element.copy
313
+ end
314
+
315
+ copied_thread.previous_pointer = previous_pointer
316
+ copied_thread
317
+ end
318
+
319
+ def to_hash
320
+ export = {}
321
+
322
+ export["callstack"] = []
323
+
324
+ call_stack.each do |element|
325
+ element_export = {}
326
+ if !element.current_pointer.null_pointer?
327
+ element_export["cPath"] = element.current_pointer.container.path.to_s
328
+ element_export["idx"] = element.current_pointer.index
329
+ end
330
+
331
+ element_export["exp"] = element.in_expression_evaluation?
332
+ element_export["type"] = PushPopType::TYPES[element.type]
333
+
334
+ if element.temporary_variables.any?
335
+ element_export["temp"] = Serializer.convert_hash_of_runtime_objects(element.temporary_variables)
336
+ end
337
+
338
+ export["callstack"] << element_export
339
+ end
340
+
341
+ export["threadIndex"] = thread_index
342
+
343
+ if !previous_pointer.null_pointer?
344
+ export["previousContentObject"] = self.previous_pointer.resolve!.path.to_s
345
+ end
346
+
347
+ export
348
+ end
349
+ end
350
+ end
351
+ end