module SaveData # Contains Value objects for each save element. # Populated during runtime by SaveData.register calls. # @type [Array] @values = [] # An error raised if an invalid save value is being saved or loaded. class InvalidValueError < RuntimeError; end #============================================================================= # Represents a single value in save data. # New values are added using {SaveData.register}. class Value # @return [Symbol] the value id attr_reader :id # @param id [Symbol] value id def initialize(id, &block) validate id => Symbol, block => Proc @id = id @loaded = false @load_in_bootup = false instance_eval(&block) raise "No save_value defined for save value #{id.inspect}" if @save_proc.nil? raise "No load_value defined for save value #{id.inspect}" if @load_proc.nil? end # @param value [Object] value to check # @return [Boolean] whether the given value is valid def valid?(value) return true if @ensured_class.nil? return value.is_a?(Object.const_get(@ensured_class)) end # Calls the value's load proc with the given argument passed into it. # @param value [Object] load proc argument # @raise [InvalidValueError] if an invalid value is being loaded def load(value) validate_value(value) @load_proc.call(value) @loaded = true end # Calls the value's save proc and returns its value. # @return [Object] save proc value # @raise [InvalidValueError] if an invalid value is being saved def save value = @save_proc.call validate_value(value) return value end # @return [Boolean] whether the value has a new game value proc defined def has_new_game_proc? return @new_game_value_proc.is_a?(Proc) end # Calls the save value's load proc with the value fetched # from the defined new game value proc. # @raise (see #load) def load_new_game_value unless self.has_new_game_proc? raise "Save value #{@id.inspect} has no new_game_value defined" end self.load(@new_game_value_proc.call) end # @return [Boolean] whether the value should be loaded during bootup def load_in_bootup? return @load_in_bootup end # @return [Boolean] whether the value has been loaded def loaded? return @loaded end # Marks value as unloaded. def mark_as_unloaded @loaded = false end # Uses the {#from_old_format} proc to select the correct data from # +old_format+ and return it. # Returns nil if the proc is undefined. # @param old_format [Array] old format to load value from # @return [Object] data from the old format def get_from_old_format(old_format) return nil if @old_format_get_proc.nil? return @old_format_get_proc.call(old_format) end private # Raises an {InvalidValueError} if the given value is invalid. # @param value [Object] value to check # @raise [InvalidValueError] if the value is invalid def validate_value(value) return if self.valid?(value) raise InvalidValueError, "Save value #{@id.inspect} is not a #{@ensured_class} (#{value.class.name} given)" end # @!group Configuration # If present, ensures that the value is of the given class. # @param class_name [Symbol] class to enforce # @see SaveData.register def ensure_class(class_name) validate class_name => Symbol @ensured_class = class_name end # Defines how the loaded value is placed into a global variable. # Requires a block with the loaded value as its parameter. # @see SaveData.register def load_value(&block) raise ArgumentError, 'No block given to load_value' unless block_given? @load_proc = block end # Defines what is saved into save data. Requires a block. # @see SaveData.register def save_value(&block) raise ArgumentError, 'No block given to save_value' unless block_given? @save_proc = block end # If present, defines what the value is set to at the start of a new game. # @see SaveData.register def new_game_value(&block) raise ArgumentError, 'No block given to new_game_value' unless block_given? @new_game_value_proc = block end # If present, sets the value to be loaded during bootup. # @see SaveData.register def load_in_bootup @load_in_bootup = true end # If present, defines how the value should be fetched from the pre-v19 # save format. Requires a block with the old format array as its parameter. # @see SaveData.register def from_old_format(&block) raise ArgumentError, 'No block given to from_old_format' unless block_given? @old_format_get_proc = block end # @!endgroup end #============================================================================= # Registers a {Value} to be saved into save data. # Takes a block which defines the value's saving ({Value#save_value}) # and loading ({Value#load_value}) procedures. # # It is also possible to provide a proc for fetching the value # from the pre-v19 format ({Value#from_old_format}), define # a value to be set upon starting a new game with {Value#new_game_value} # and ensure that the saved and loaded value is of the correct # class with {Value#ensure_class}. # # Values can be registered to be loaded on bootup with # {Value#load_in_bootup}. If a new_game_value proc is defined, it # will be called when the game is launched for the first time, # or if the save data does not contain the value in question. # # @example Registering a new value # SaveData.register(:foo) do # ensure_class :Foo # save_value { $foo } # load_value { |value| $foo = value } # new_game_value { Foo.new } # from_old_format { |old_format| old_format[16] if old_format[16].is_a?(Foo) } # end # @example Registering a value to be loaded on bootup # SaveData.register(:bar) do # load_in_bootup # save_value { $bar } # load_value { |value| $bar = value } # new_game_value { Bar.new } # end # @param id [Symbol] value id # @yieldself [Value] def self.register(id, &block) validate id => Symbol unless block_given? raise ArgumentError, 'No block given to SaveData.register' end @values << Value.new(id, &block) end # @param save_data [Hash] save data to validate # @return [Boolean] whether the given save data is valid def self.valid?(save_data) validate save_data => Hash return @values.all? { |value| value.valid?(save_data[value.id]) } end # Loads values from the given save data. # An optional condition can be passed. # @param save_data [Hash] save data to load from # @param condition_block [Proc] optional condition # @api private def self.load_values(save_data, &condition_block) @values.each do |value| next if block_given? && !condition_block.call(value) if save_data.has_key?(value.id) value.load(save_data[value.id]) elsif value.has_new_game_proc? value.load_new_game_value end end end # Loads the values from the given save data by # calling each {Value} object's {Value#load_value} proc. # Values that are already loaded are skipped. # If a value does not exist in the save data and has # a {Value#new_game_value} proc defined, that value # is loaded instead. # @param save_data [Hash] save data to load # @raise [InvalidValueError] if an invalid value is being loaded def self.load_all_values(save_data) validate save_data => Hash load_values(save_data) { |value| !value.loaded? } end # Marks all values that aren't loaded on bootup as unloaded. def self.mark_values_as_unloaded @values.each do |value| value.mark_as_unloaded unless value.load_in_bootup? end end # Loads each value from the given save data that has # been set to be loaded during bootup. Done when a save file exists. # @param save_data [Hash] save data to load # @raise [InvalidValueError] if an invalid value is being loaded def self.load_bootup_values(save_data) validate save_data => Hash load_values(save_data) { |value| !value.loaded? && value.load_in_bootup? } end # Goes through each value with {Value#load_in_bootup} enabled and loads their # new game value, if one is defined. Done when no save file exists. def self.initialize_bootup_values @values.each do |value| next unless value.load_in_bootup? value.load_new_game_value if value.has_new_game_proc? && !value.loaded? end end # Loads each {Value}'s new game value, if one is defined. Done when starting a # new game. def self.load_new_game_values @values.each do |value| value.load_new_game_value if value.has_new_game_proc? && !value.loaded? end end # @return [Hash{Symbol => Object}] a hash representation of the save data # @raise [InvalidValueError] if an invalid value is being saved def self.compile_save_hash save_data = {} @values.each { |value| save_data[value.id] = value.save } return save_data end end