class Fable::VariablesState

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.

Attributes

batch_observing_variable_changes[RW]
callstack[RW]
changed_variables_for_batch_observing[RW]
default_global_variables[RW]
dont_save_default_values[RW]

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.

dont_save_default_values?[RW]

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.

globals[RW]
list_definitions_origins[RW]
patch[RW]
variable_observers[RW]

Public Class Methods

new(callstack, list_definitions_origins) click to toggle source
# 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

Public Instance Methods

[](variable_name) click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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!() click to toggle source
# 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) click to toggle source
# 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
batch_observing_variable_changes=(value) click to toggle source
# File lib/fable/variables_state.rb, line 36
def batch_observing_variable_changes=(value)
  @batch_observing_variable_changes = value
  if value
    @changed_variables_for_batch_observing = []

  # Finished observing variables in a batch, now send
  # notifications for changed variables all in one go
  else
    if @changed_variables_for_batch_observing != nil
      @changed_variables_for_batch_observing.uniq.each do |variable_name|
        current_value = @globals[variable_name]
        variable_changed_event(variable_name, current_value)
      end
    end

    @changed_variables_for_batch_observing = nil
  end
end
from_hash!(hash_to_use) click to toggle source
# 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) click to toggle source

0 if named variable is global

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

# 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) click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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?() click to toggle source
# 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) click to toggle source
# 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) click to toggle source

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.

# 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) click to toggle source
# 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) click to toggle source
# 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_globals() click to toggle source
# File lib/fable/variables_state.rb, line 261
def snapshot_default_globals
  self.default_global_variables = self.globals.dup
end
to_hash() click to toggle source
# 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) click to toggle source
# 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