Patch old save files with new data

Tsukihime

Veteran
Veteran
Joined
Jun 30, 2012
Messages
8,564
Reaction score
3,846
First Language
English
I was looking at the Object Reinitializer and Zalerinian mentioned there were a few issues to sort out.

While trying to figure out how to solve the issues, I wrote a couple tests to verify that it works.

Here's one tricky test:

class Game_Actor < Game_Battler alias :th_savepatcher_test_init :initialize def initialize(*args) @test_value = 9999 th_savepatcher_test_init(*args) end alias :th_savepatcher_test_param_base :param_base def param_base(*args) th_savepatcher_test_param_base(*args) + @test_value endendThis test simulates the addition of a new instance variable that will be used in the script from now on.The expected result is that when you open the menu, you'll see your actors with 9999 HP and MP.

Old save files will break because they do not properly initialize that value, because the objects were created in a version of the game when that variable did not exist.

The object re-initializer's goal is initialize that value so that older save files can be used in newer versions of the game (ie: patching the save file)

I failed this test immediately when I tried writing my own script, and the re-initializer passes it just fine.

If you can think of any strange cases that would be good for testing that would be great.
 
Last edited by a moderator:

Zeriab

Huggins!
Veteran
Joined
Mar 20, 2012
Messages
1,268
Reaction score
1,422
First Language
English
Primarily Uses
RMXP
Versioning is the answer. With a save header that is read before the normal save data. (Separate file, a Marshal.load before, doesn't matter too much)

When a new save file version is required you can preserve the old loading mechanism and use that when an old version is found. Afterwards you can have a script that updates data as needed.

Of course may still be impossible to accurately update old saves. Sometimes missing data just cannot be deterministically created.

*hugs*

 - Zeriab
 

Tsukihime

Veteran
Veteran
Joined
Jun 30, 2012
Messages
8,564
Reaction score
3,846
First Language
English
If you're referring to the savefile doctor, it doesn't pass the actor test for me.


My save file was created before I added the save file doctor, if that makes a difference.


However, it does work in conjunction with the object re-initializer script.

Versioning is the answer. With a save header that is read before the normal save data. (Separate file, a Marshal.load before, doesn't matter too much)


When a new save file version is required you can preserve the old loading mechanism and use that when an old version is found. Afterwards you can have a script that updates data as needed.


Of course may still be impossible to accurately update old saves. Sometimes missing data just cannot be deterministically created.


*hugs*


 - Zeriab
Versioning is a step forward in identifying that we are loading data that is out-of-date.


How can a game dev, who may not have much knowledge of scripting or just happens to be using some pretty complex scripts that introduce a bunch of instance variables, make use of versioning to bring old save files up to date?
 
Last edited by a moderator:

♥SOURCE♥

Too sexy for your party.
Veteran
Joined
Mar 14, 2012
Messages
693
Reaction score
411
Primarily Uses
If you're referring to the savefile doctor, it doesn't pass the actor test for me.

My save file was created before I added the save file doctor, if that makes a difference.

However, it does work in conjunction with the object re-initializer script.
Oh yes, that one slipped because of how Game_Actor creation is handled inside Game_Actors:

def [](actor_id) return nil unless $data_actors[actor_id] @data[actor_id] ||= Game_Actor.new(actor_id) endGame_Actor objects are created on demand, so a little special handling is required. Save File Doctor can easily handle special cases for any Object:

Code:
#==============================================================================# ** MakerSystems#------------------------------------------------------------------------------#  #==============================================================================module MakerSystems    #============================================================================  # ** SaveFileDoctor  #----------------------------------------------------------------------------  #    #============================================================================    module SaveFileDoctor        #----------------------------------------------------------------------    # * Custom Case for Game_Actors.                                  [NEW]    #----------------------------------------------------------------------    def self.custom_case_for_game_actors(patient, current)      # Get data array that contains each Actor.      patient_data = patient.instance_variable_get('@data')      # Go through each Actor.      patient_data.each do |actor|         # Next if actor is nil. Needed because how Game_Actors handles [].        next unless actor        # Get current actor from current Game_Actors using patient's id.        current_actor = current[actor.instance_variable_get('@actor_id')]        # Fixes each of patient's Game_Actor objects.        fix_object(actor, current_actor)        # Healing process for each instance variable of this actor.        child_health(object_data(actor), object_data(current_actor))      end    end    #----------------------------------------------------------------------    # * Heal.                                                         [NEW]    #----------------------------------------------------------------------    def self.heal(contents)      # Will keep track of repaired objects to avoid a stack overflow.      @ms_save_file_doctor_ids = []      # Goes through each object in contents hash.      contents.each_value do |patient|        # Updated version of this Object.        current = patient.class.new        # Fixes this (loaded) Object  with the help of its updated version.        fix_object(patient, current)        # Starts the healing process for each instance variable.        child_health(object_data(patient), object_data(current))      end      # No need to keep the objects ids in memory.      @ms_save_file_doctor_ids = nil    end    #----------------------------------------------------------------------    # * Enumerable Child Processing.                                  [NEW]    #----------------------------------------------------------------------    def self.enum_child(patient, current)      # Iterates loaded version and updated version as a sequence.      patient.zip(current).each do |p_value, c_value|        # If Enumerable, there might be instanceable Objects inside it.        enum_child(p_value.to_a, c_value.to_a) if Enumerable === p_value        # Ignore if it can't be instanced.        next unless p_value.class.respond_to?(:new)        # Stop if this Object was already healed to avoid a stack overflow.        return if @ms_save_file_doctor_ids.include?(p_value.__id__)        # Saves current Object id.        @ms_save_file_doctor_ids << p_value.__id__        # Fixes this Object with the help of its updated version.        fix_object(p_value, c_value)        # Healing process for each instance variable of this Object.        child_health(object_data(p_value), object_data(c_value))      end    end    #----------------------------------------------------------------------    # * Object Data.                                                  [NEW]    #----------------------------------------------------------------------    def self.object_data(object)      # Returns a hash with each instance variable name and value.      Hash[        object.instance_variables.map do |name|          [name, object.instance_variable_get(name)]        end      ]    end    #----------------------------------------------------------------------    # * Fix Object.                                                   [NEW]    #----------------------------------------------------------------------    def self.fix_object(patient, current)      # Name of the method that should especially handle this object.      special = "custom_case_for_#{patient.class.name.downcase}"      # Attempts special handling.      send(special, patient, current) if respond_to?(special)      # Adds new data to loaded Object's data target.      data = object_data(current).merge(object_data(patient))      # Uses the correct data target to set each instance variable.      data.each { |name, value| patient.instance_variable_set(name, value) }    end    #----------------------------------------------------------------------    # * Child Health.                                                 [NEW]    #----------------------------------------------------------------------    def self.child_health(patient, current)      # Goes through each instance variable.      patient.each do |name, value|        # If Enumerable, there might be instanceable Objects inside it.        enum_child(value.to_a, current[name].to_a) if Enumerable === value        # Ignore if it can't be instanced.        next unless value.class.respond_to?(:new)        # Stop if this Object was already healed to avoid a stack overflow.        return if @ms_save_file_doctor_ids.include?(value.__id__)        # Saves current Object id.        @ms_save_file_doctor_ids << value.__id__        # Fixes this Object with the help of its updated version.        fix_object(value, current[name])        # Healing process for each instance variable of this Object.        child_health(object_data(value), object_data(current[name]))      end    end            end  end#==============================================================================# ** DataManager#------------------------------------------------------------------------------#  This module manages the database and game objects. Almost all of the # global variables used by the game are initialized by this module.#==============================================================================module DataManager    class << self        #------------------------------------------------------------------------    # * Alias Extract Save Contents.                                    [NEW]    #------------------------------------------------------------------------    alias_method(:ms_save_file_doctor_original_extract_save_contents,                  :extract_save_contents)    #------------------------------------------------------------------------    # * Extract Save Contents.                                          [MOD]    #------------------------------------------------------------------------    def extract_save_contents(contents)      # Healthcare. Object repairing is handled at a local level.      MakerSystems::SaveFileDoctor.heal(contents)      # Proceeds with the original method, using repaired contents.      ms_save_file_doctor_original_extract_save_contents(contents)    end    end  end
 

Tsukihime

Veteran
Veteran
Joined
Jun 30, 2012
Messages
8,564
Reaction score
3,846
First Language
English
Oh yes, that one slipped because of how Game_Actor creation is handled inside Game_Actors:

def [](actor_id) return nil unless $data_actors[actor_id] @data[actor_id] ||= Game_Actor.new(actor_id) endGame_Actor objects are created on demand, so a little special handling is required. Save File Doctor can easily handle special cases for any Object:
The reason why the script originally didn't work wasn't because of the way actors are created. They are stored in an array, which your enumerable check would take care of.

It's because it's returning false for the "class.respond_to?:)new)" check and skipping the actor object completely.
 

♥SOURCE♥

Too sexy for your party.
Veteran
Joined
Mar 14, 2012
Messages
693
Reaction score
411
Primarily Uses
The reason why the script originally didn't work wasn't because of the way actors are created. They are stored in an array, which your enumerable check would take care of.

It's because it's returning false for the "class.respond_to?:)new)" check and skipping the actor object completely.
No, you are very mistaken. 

actors = Game_Actors.newmsgbox actors.instance_variable_get('@data')Creating an instance of Game_Actors alone doesn't provide the actor data needed to update the save contents since each Game_Actor instance is created when calling the [] method of Game_Actors:

def [](actor_id) return nil unless $data_actors[actor_id] @data[actor_id] ||= Game_Actor.new(actor_id) endUnless you do game_actors_instance[actor_id], game_actors_instance won't have an actor with actor_id and the comparison between the loaded data and the new one won't yield any object to account for.

  1. You don't understand the enum_child method.
  2. It seems you also don't understand how Save FIle Doctor works in general.
  3. It's okay, because I didn't explain how it works yet and even then you don't have to understand it.
  4. I find funny that (after 1, 2 and 3) you are trying to tell me how my script works and why it didn't in that case.
 

It's because it's returning false for the "class.respond_to?:)new)" check and skipping the actor object completely.
nil.respond_to?:)new)It wasn't there in the first place.
 

Tsukihime

Veteran
Veteran
Joined
Jun 30, 2012
Messages
8,564
Reaction score
3,846
First Language
English
Sure.

Here's your original code. I've added a print statement to line 45. Interpret it however you wish. I've attached a save file to make things easier.

#==============================================================================# ** MakerSystems#------------------------------------------------------------------------------# #==============================================================================module MakerSystems #============================================================================ # ** SaveFileDoctor #---------------------------------------------------------------------------- # #============================================================================ module SaveFileDoctor #---------------------------------------------------------------------- # * Heal. [NEW] #---------------------------------------------------------------------- def self.heal(contents) # Will keep track of repaired objects to avoid a stack overflow. @ms_save_file_doctor_ids = [] # Goes through each object in contents hash. contents.each_value do |patient| # Updated version of this Object. current = patient.class.new # Fixes this (loaded) Object with the help of its updated version. fix_object(patient, current) # Starts the healing process for each instance variable. child_health(object_data(patient), object_data(current)) end # No need to keep the objects ids in memory. @ms_save_file_doctor_ids = nil end #---------------------------------------------------------------------- # * Enumerable Child Processing. [NEW] #---------------------------------------------------------------------- def self.enum_child(patient, current) # Iterates loaded version and updated version as a sequence. patient.zip(current).each do |p_value, c_value| # If Enumerable, there might be instanceable Objects inside it. enum_child(p_value.to_a, c_value.to_a) if Enumerable === p_value # Ignore if it can't be instanced. p "Game_Actor has new? %s" %p_value.class.respond_to?:)new) if p_value.is_a?(Game_Actor) next unless p_value.class.respond_to?:)new) # Stop if this Object was already healed to avoid a stack overflow. return if @ms_save_file_doctor_ids.include?(p_value.__id__) # Saves current Object id. @ms_save_file_doctor_ids << p_value.__id__ # Fixes this Object with the help of its updated version. fix_object(p_value, c_value) # Healing process for each instance variable of this Object. child_health(object_data(p_value), object_data(c_value)) end end #---------------------------------------------------------------------- # * Object Data. [NEW] #---------------------------------------------------------------------- def self.object_data(object) # Returns a hash with each instance variable name and value. Hash[ object.instance_variables.map do |name| [name, object.instance_variable_get(name)] end ] end #---------------------------------------------------------------------- # * Fix Object. [NEW] #---------------------------------------------------------------------- def self.fix_object(patient, current) # Adds new data to loaded Object's data target. data = object_data(current).merge(object_data(patient)) # Uses the correct data target to set each instance variable. data.each { |name, value| patient.instance_variable_set(name, value) } end #---------------------------------------------------------------------- # * Child Health. [NEW] #---------------------------------------------------------------------- def self.child_health(patient, current) # Goes through each instance variable. patient.each do |name, value| # If Enumerable, there might be instanceable Objects inside it. enum_child(value.to_a, current[name].to_a) if Enumerable === value # Ignore if it can't be instanced. next unless value.class.respond_to?:)new) # Stop if this Object was already healed to avoid a stack overflow. return if @ms_save_file_doctor_ids.include?(value.__id__) # Saves current Object id. @ms_save_file_doctor_ids << value.__id__ # Fixes this Object with the help of its updated version. fix_object(value, current[name]) # Healing process for each instance variable of this Object. child_health(object_data(value), object_data(current[name])) end end end end#==============================================================================# ** DataManager#------------------------------------------------------------------------------# This module manages the database and game objects. Almost all of the # global variables used by the game are initialized by this module.#==============================================================================module DataManager class << self #------------------------------------------------------------------------ # * Alias Extract Save Contents. [NEW] #------------------------------------------------------------------------ alias_method:)ms_save_file_doctor_original_extract_save_contents, :extract_save_contents) #------------------------------------------------------------------------ # * Extract Save Contents. [MOD] #------------------------------------------------------------------------ def extract_save_contents(contents) # Healthcare. Object repairing is handled at a local level. MakerSystems::SaveFileDoctor.heal(contents) # Proceeds with the original method, using repaired contents. ms_save_file_doctor_original_extract_save_contents(contents) end end end[/MOD]
[MOD]Save01.zip[/mod]
 

Attachments

Last edited by a moderator:

♥SOURCE♥

Too sexy for your party.
Veteran
Joined
Mar 14, 2012
Messages
693
Reaction score
411
Primarily Uses
Sure.

Here's your original code. I've added a print statement to line 45. Interpret it however you wish. I've attached a save file to make things easier.
You are still misunderstanding how Save File Doctor works. 

enum_child iterates through pairs consisting of the "patient" (that represents the loaded data) and the "current" (that represents the data created in that gameplay).

In that version, It finds a Game_Actor inside patient's @data, but it doesn't finds any in current's to compare it to (because how Game_Actor instances are created on demand, as I explained in previous posts).

The solution is simple, a special case to handle Game_Actors that will call the [] method for each Game_Actor found in the patient, before performing the standard healing process.
 

Tsukihime

Veteran
Veteran
Joined
Jun 30, 2012
Messages
8,564
Reaction score
3,846
First Language
English
My understanding of the code is based on what the code says, not what you intended it to say.

You've provided a lot of comments, which helped me understand what's going on.

#----------------------------------------------------------------------# * Enumerable Child Processing. [NEW]#----------------------------------------------------------------------def self.enum_child(patient, current) # Iterates loaded version and updated version as a sequence. patient.zip(current).each do |p_value, c_value| # If Enumerable, there might be instanceable Objects inside it. enum_child(p_value.to_a, c_value.to_a) if Enumerable === p_value # Ignore if it can't be instanced. p "Game_Actor has new? %s" %p_value.class.respond_to?:)new) if p_value.is_a?(Game_Actor) next unless p_value.class.respond_to?:)new) # Stop if this Object was already healed to avoid a stack overflow.The comments are pretty clear: if it can't be instanced, ignore it.Since the print statement shows that we're working with an actor, then the code is suggesting that an actor couldn't be instanced, so ignore it.

Which is why I concluded that actors aren't being fixed because they're being ignored.
 

♥SOURCE♥

Too sexy for your party.
Veteran
Joined
Mar 14, 2012
Messages
693
Reaction score
411
Primarily Uses
My understanding of the code is based on what the code says, not what you intended it to say.
I am not contradicting the code's comments. You are assuming too much.

My understanding of the code is based on what the code says, not what you intended it to say.

You've provided a lot of comments, which helped me understand what's going on.

#----------------------------------------------------------------------# * Enumerable Child Processing. [NEW]#----------------------------------------------------------------------def self.enum_child(patient, current) # Iterates loaded version and updated version as a sequence. patient.zip(current).each do |p_value, c_value| # If Enumerable, there might be instanceable Objects inside it. enum_child(p_value.to_a, c_value.to_a) if Enumerable === p_value # Ignore if it can't be instanced. p "Game_Actor has new? %s" %p_value.class.respond_to?:)new) if p_value.is_a?(Game_Actor) next unless p_value.class.respond_to?:)new) # Stop if this Object was already healed to avoid a stack overflow.The comments are pretty clear: if it can't be instanced, ignore it.Since the print statement shows that we're working with an actor, then the code is suggesting that an actor couldn't be instanced, so ignore it.

Which is why I concluded that actors aren't being fixed because they're being ignored.
You think the problem (or lack of healing) was caused due to it skipping the healing process because of p_value.respond_to?:)new) returning false and that is not the case.

First, it is skipping the process because Game_Actor has a "def class" (way to go Enterbrain...) that overwrites Ruby's built in method that should return the class of the object. So it is actually checking an RPG::Class object.

Second, even if it doesn't skip it, it has no current reference to base the healing process on.

Skipping it because of the p_value.respond_to?:)new) was never the reason it didn't work with your test. The reason was that a special handling was needed for Game_Actors. :)
 

Users Who Are Viewing This Thread (Users: 0, Guests: 1)

Latest Threads

Latest Posts

Latest Profile Posts

Couple hours of work. Might use in my game as a secret find or something. Not sure. Fancy though no? :D
Holy stink, where have I been? Well, I started my temporary job this week. So less time to spend on game design... :(
Cartoonier cloud cover that better fits the art style, as well as (slightly) improved blending/fading... fading clouds when there are larger patterns is still somewhat abrupt for some reason.
Do you Find Tilesetting or Looking for Tilesets/Plugins more fun? Personally I like making my tileset for my Game (Cretaceous Park TM) xD
How many parameters is 'too many'??

Forum statistics

Threads
105,862
Messages
1,017,049
Members
137,569
Latest member
Shtelsky
Top