Class: Fable::VariablesState

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

Overview

Encompasses all the global variables in an ink Story, and allows binding of a variable_changed event so that that game code can be notified whenever the global variables change.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(callstack, list_definitions_origins) ⇒ VariablesState

Returns a new instance of VariablesState.



88
89
90
91
92
93
94
# File 'lib/fable/variables_state.rb', line 88

def initialize(callstack, list_definitions_origins)
  self.globals = {}
  self.callstack = callstack
  self.list_definitions_origins = list_definitions_origins
  self.dont_save_default_values = true
  self.variable_observers = {}
end

Instance Attribute Details

#batch_observing_variable_changesObject

Returns the value of attribute batch_observing_variable_changes.



6
7
8
# File 'lib/fable/variables_state.rb', line 6

def batch_observing_variable_changes
  @batch_observing_variable_changes
end

#callstackObject

Returns the value of attribute callstack.



6
7
8
# File 'lib/fable/variables_state.rb', line 6

def callstack
  @callstack
end

#changed_variables_for_batch_observingObject

Returns the value of attribute changed_variables_for_batch_observing.



6
7
8
# File 'lib/fable/variables_state.rb', line 6

def changed_variables_for_batch_observing
  @changed_variables_for_batch_observing
end

#default_global_variablesObject

Returns the value of attribute default_global_variables.



6
7
8
# File 'lib/fable/variables_state.rb', line 6

def default_global_variables
  @default_global_variables
end

#dont_save_default_valuesObject Also known as: dont_save_default_values?

When saving out state, we can skip saving global values that remain equal to the initial values that were declared in ink. This makes the save object (potentially) much smaller assuming that at least a portion of the globals haven’t changed. However, it can also take marginally longer to save in the case that the majority HAVE changed, since it has to compare all globals. It may also be useful to turn this off for testing worst case save timing.



129
130
131
# File 'lib/fable/variables_state.rb', line 129

def dont_save_default_values
  @dont_save_default_values
end

#globalsObject

Returns the value of attribute globals.



6
7
8
# File 'lib/fable/variables_state.rb', line 6

def globals
  @globals
end

#list_definitions_originsObject

Returns the value of attribute list_definitions_origins.



6
7
8
# File 'lib/fable/variables_state.rb', line 6

def list_definitions_origins
  @list_definitions_origins
end

#patchObject

Returns the value of attribute patch.



6
7
8
# File 'lib/fable/variables_state.rb', line 6

def patch
  @patch
end

#variable_observersObject

Returns the value of attribute variable_observers.



6
7
8
# File 'lib/fable/variables_state.rb', line 6

def variable_observers
  @variable_observers
end

Instance Method Details

#[](variable_name) ⇒ Object



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/fable/variables_state.rb', line 55

def [](variable_name)
  if !patch.nil? && patch.get_global(variable_name)
    return patch.get_global(variable_name).value_object
  end

  # Search main dictionary first
  # If it's not found, it might be because the story content has
  # changed, and the original default value hasn't been instantiated
  variable_value = @globals[variable_name] || @default_global_variables[variable_name]
  if !variable_value.nil?
    return variable_value.value_object
  else
    return nil
  end
end

#[]=(variable_name, given_value) ⇒ Object



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/fable/variables_state.rb', line 71

def []=(variable_name, given_value)
  if !default_global_variables.has_key?(variable_name)
    raise Error, "Cannot assign to a variable (#{variable_name}) that hasn't been declared in the story"
  end

  value = Value.create(given_value)
  if value.nil?
    if given_value.nil?
      raise Error, "Cannot pass nil to VariableState"
    else
      raise Error, "Invalid value passed to VariableState: #{given_value}"
    end
  end

  set_global(variable_name, value)
end

#add_variable_observer(variable_name, &block) ⇒ Object



16
17
18
19
20
# File 'lib/fable/variables_state.rb', line 16

def add_variable_observer(variable_name, &block)
  self.variable_observers[variable_name] ||= []
  self.variable_observers[variable_name] << block
  return true
end

#apply_patch!Object



96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/fable/variables_state.rb', line 96

def apply_patch!
  patch.globals.each do |name, value|
    self.globals[name] = value
  end

  if !changed_variables_for_batch_observing.nil?
    patch.changed_variables.each do |name|
      changed_variables_for_batch_observing << name
    end
  end

  patch = nil
end

#assign(variable_assignment, value) ⇒ Object



223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/fable/variables_state.rb', line 223

def assign(variable_assignment, value)
  name = variable_assignment.variable_name
  context_index = -1

  # Are we assigning to a global variable?
  set_global = false
  if variable_assignment.new_declaration?
    set_global = variable_assignment.global?
  else
    set_global = global_variable_exists_with_name?(name)
  end

  # Constructing new variable pointer reference
  if variable_assignment.new_declaration?
    if value.is_a?(VariablePointerValue)
      fully_resolved_variable_pointer = resolve_variable_pointer!(value)
      value = fully_resolved_variable_pointer
    end
  # Assign to existing variable pointer?
  # then assign to the variable that the pointer is pointing to by name
  else
    # De-reference variable reference to point to
    existing_pointer = get_raw_variable_with_name(name, context_index)
    while existing_pointer && existing_pointer.is_a?(VariablePointerValue)
      name = existing_pointer.variable_name
      context_index = existing_pointer.context_index
      set_global = (context_index == 0)
      existing_pointer = get_raw_variable_with_name(name, context_index)
    end
  end

  if set_global
    set_global(name, value)
  else
    callstack.set_temporary_variable(name, value, variable_assignment.new_declaration?, context_index)
  end
end

#from_hash!(hash_to_use) ⇒ Object



110
111
112
113
114
115
116
117
118
119
# File 'lib/fable/variables_state.rb', line 110

def from_hash!(hash_to_use)
  @globals = {}
  @default_global_variables.each do |key, value|
    if hash_to_use.has_key?(key)
      @globals[key] = Serializer.convert_to_runtime_object(hash_to_use[key])
    else
      @globals[key] = value
    end
  end
end

#get_context_index_of_variable_named(variable_name) ⇒ Object

0 if named variable is global

! if named variable is a temporary in a particular callstack element



319
320
321
322
323
324
325
# File 'lib/fable/variables_state.rb', line 319

def get_context_index_of_variable_named(variable_name)
  if global_variable_exists_with_name?(variable_name)
    return 0
  end

  return callstack.current_element_index
end

#get_default_variable_value(variable_name) ⇒ Object



173
174
175
# File 'lib/fable/variables_state.rb', line 173

def get_default_variable_value(variable_name)
  return self.default_global_variables[variable_name]
end

#get_raw_variable_with_name(variable_name, context_index) ⇒ Object



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/fable/variables_state.rb', line 192

def get_raw_variable_with_name(variable_name, context_index)
  variable_value = nil
  if context_index == 0 || context_index == -1
    if !patch.nil? && patch.get_global(variable_name)
      return patch.get_global(variable_name)
    end

    if globals.has_key?(variable_name)
      return globals[variable_name]
    end

    # Getting variables can actually happen during global setup because
    # you can do VAR x = A_LIST_ITEM
    # so default_global_variables may be null
    # WE need to do this check though in case a new global is added, so we need to
    # revert to the default globals dictionary, since an initial value hasn't been set yet
    if !default_global_variables.nil? && default_global_variables.has_key?(variable_name)
      return default_global_variables[variable_name]
    end

    list_item_value = list_definitions_origins.find_single_item_list_with_name(variable_name)

    if list_item_value
      return list_item_value
    end
  end

  # Temporary
  return callstack.get_temporary_variable_with_name(variable_name, context_index)
end

#get_variable_with_name(variable_name) ⇒ Object



169
170
171
# File 'lib/fable/variables_state.rb', line 169

def get_variable_with_name(variable_name)
  return get_variable_with_name_internal(variable_name, -1)
end

#get_variable_with_name_internal(variable_name, context_index) ⇒ Object



181
182
183
184
185
186
187
188
189
190
# File 'lib/fable/variables_state.rb', line 181

def get_variable_with_name_internal(variable_name, context_index)
  variable_value = get_raw_variable_with_name(variable_name, context_index)

  # Get value from pointer?
  if variable_value.is_a?(VariablePointerValue)
    variable_value = get_variable_with_name_internal(variable_value.variable_name, variable_value.context_index)
  end

  return variable_value
end

#global_variable_exists_with_name?(variable_name) ⇒ Boolean

Returns:

  • (Boolean)


177
178
179
# File 'lib/fable/variables_state.rb', line 177

def global_variable_exists_with_name?(variable_name)
  globals.has_key?(variable_name) || (!default_global_variables.nil? && default_global_variables.has_key?(variable_name))
end

#has_variable_observers?Boolean

Returns:

  • (Boolean)


12
13
14
# File 'lib/fable/variables_state.rb', line 12

def has_variable_observers?
  self.variable_observers.all?{|name, observers| !observers.empty? }
end

#remove_variable_observer(variable_name, &block) ⇒ Object



22
23
24
25
26
# File 'lib/fable/variables_state.rb', line 22

def remove_variable_observer(variable_name, &block)
  self.variable_observers[variable_name] ||= []
  self.variable_observers[variable_name].delete(block)
  return true
end

#resolve_variable_pointer!(variable_pointer) ⇒ Object

Given a variable pointer with just the name of the target known, resolve to a variable pointer that more specifically points to the exact instance: whether it’s global, or the exact position of a temporary on the callstack.



295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/fable/variables_state.rb', line 295

def resolve_variable_pointer!(variable_pointer)
  context_index = variable_pointer.context_index

  if context_index == -1
    context_index = get_context_index_of_variable_named(variable_pointer.variable_name)
  end

  value_of_variable_pointed_to = get_raw_variable_with_name(variable_pointer.variable_name, context_index)

  # Extra layer of indirection:
  # When accessing a pointer to a pointer (e.g. when calling nested or
  # recursive functions that take a variable references, ensure we don't create
  # a chain of indirection by just returning the final target.
  if value_of_variable_pointed_to.is_a?(VariablePointerValue)
    return value_of_variable_pointed_to
  # Make a copy of the variable pointer so we're not using the value directly
  # from the runtime. Temporary must bne local to the current scope
  else
    return VariablePointerValue.new(variable_pointer.variable_name, context_index)
  end
end

#runtime_objects_equal?(object_1, object_2) ⇒ Boolean

Returns:

  • (Boolean)

Raises:



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/fable/variables_state.rb', line 152

def runtime_objects_equal?(object_1, object_2)
  if object_1.class != object_2.class
    return false
  end

  # Perform equality on int/float manually to avoid boxing
  if object_1.is_a?(IntValue) || object_1.is_a?(FloatValue)
    return object_1.value == object_2.value
  end

  if object_1.is_a?(Value)
    return object_1.value_object.equal?(object_2.value_object)
  end

  raise Error, "FastRoughDefinitelyEquals: Unsupported runtime object type: #{object_1.class}"
end

#set_global(variable_name, value) ⇒ Object



265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/fable/variables_state.rb', line 265

def set_global(variable_name, value)
  old_value = nil
  if patch.nil? || !patch.get_global(variable_name)
    old_value = globals[variable_name]
  end

  ListValue.retain_list_origins_for_assignment(old_value, value)

  if !patch.nil?
    patch.set_global(variable_name, value)
  else
    self.globals[variable_name] = value
  end
  
  if has_variable_observers? && !value.equal?(old_value)
    if batch_observing_variable_changes
      if !patch.nil?
        patch.add_changed_variable(variable_name)
      elsif !changed_variables_for_batch_observing.nil?
        changed_variables_for_batch_observing << variable_name
      end
    else
      variable_changed_event(variable_name, value)
    end
  end
end

#snapshot_default_globalsObject



261
262
263
# File 'lib/fable/variables_state.rb', line 261

def snapshot_default_globals
  self.default_global_variables = self.globals.dup
end

#to_hashObject



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/fable/variables_state.rb', line 132

def to_hash
  export = {}

  self.globals.each do |key, value|
    if dont_save_default_values?
      # Don't write out values that are the same as the default global
      # values
      if default_global_variables.has_key?(key)
        if runtime_objects_equal?(default_global_variables[key], value)
          next
        end
      end
    end

    export[key] = Serializer.convert_from_runtime_object(value)
  end

  return export
end

#variable_changed_event(variable_name, current_value) ⇒ Object



28
29
30
31
32
33
34
# File 'lib/fable/variables_state.rb', line 28

def variable_changed_event(variable_name, current_value)
  if variable_observers.has_key?(variable_name)
    variable_observers[variable_name].each do |block|
      block.call(variable_name, current_value.value)
    end
  end
end