• Hi, Guest!
    Some images might be missing as we move away from using embedded images, sorry for the mess!
    From now on, you'll be required to use a third party to host images. You can learn how to add images here, and if your thread is missing images you can request them here.
    Do not use Discord to host any images you post, these links expire quickly!
Resource icon

v19 Introducing the new save data system 1

This resource pertains to version 19 of Pokémon Essentials.
Essentials v19: Introducing the new save data system

Essentials v19 comes with a new, flexible save data system that gives developers the tools to add new values and conversions to save data without hassle. To achieve this, some changes have been made:

The biggest changes in v19

New data location

Save data is no longer located in C:\Users\USERNAME\Saved Games\GAMENAME. The new locations look like:

Windows
Code:
C:\Users\USERNAME\AppData\Roaming\GAMENAME

Mac OS
Code:
/Users/USERNAME/Library/Application Support/GAMENAME

Linux
Code:
/home/USERNAME/.local/share/GAMENAME

The paths have been changed with the migration to mkxp-z. v19 looks for save files in the old Saved Games directory, and attempts to move them to AppData for a smooth transfer.

To get the current data directory in your scripts, use System.data_directory.

NOTE: This affects the error logs as well, and everything else that uses RTP.getSaveFileName, which now uses System.data_directory.

New API

pbSave has been deprecated and will be removed in v20. To save the game, use Game.save instead. For more information, refer to the "Saving and loading" section below.

Save data is a hash now

Previously, save data was constructed by appending variables into the save file in a fixed order. While this approach was simple and efficient, it lacked flexibility. In v19, the save data is now a hash (key-value structure) like this:
Code:
{
  game_player: [Game_Player object],
  frame_count: [number of frames],
  # etc...
}

This entire hash is saved into the save file. The benefit of this is that save values can be added and removed during the lifecycle of a game without any major issues. Giving identifiers to values in save data allows for them to be configured extensively. More info is below.

Adding new save values

In-game values
Ruby:
SaveData.register(:frame_count) do
  ensure_class :Integer
  save_value { Graphics.frame_count }
  load_value { |value| Graphics.frame_count = value }
  new_game_value { 0 }
  from_old_format { |old_format| old_format[1] }
end

The code above is from v19. It defines the save value for the frame count. Let's dive deeper into it, line by line.
Ruby:
SaveData.register(:frame_count) do

We're calling SaveData.register, the function used for adding new values into save data. :frame_count is the identifier of our new value. It is used internally to distinguish it from the rest. do starts a code block where the value is configured.
Ruby:
ensure_class :Integer

ensure_class ensures that our new value is of the Integer type when the value is saved or loaded. If the value is anything else, the game will crash. Usage of ensure_class is optional, but recommended.

Note:
ensure_class assumes that the type's value never changes during development. If it was changed in an update, save files from older versions would crash. If a value's type has to be changed and validated, validation should be done elsewhere, like save_value and load_value:
Ruby:
save_value { Graphics.frame_count }

save_value is required, and specifies the actual value that is stored in save data. It is given a code block that evaluates to the desired value. In this case, it is the current frame count.
Ruby:
load_value { |value| Graphics.frame_count = value }

load_value is required, and specifies how the stored value is loaded. Like save_value, it is given a code block. The difference is that the code block is given the fetched value as an argument. In this case, we are storing the value inside Graphics.frame_count.

NOTE: Save values are loaded in the order they are defined in.
Ruby:
new_game_value { 0 }

new_game_value is optional, and specifies what value should be loaded upon starting a new game. It is given a code block which evaluates to the desired value. In this case, we set the frame count to 0.
Ruby:
from_old_format { |old_format| old_format[1] }

from_old_format is optional, and its purpose is to ensure backwards compatibility with save files in the old, pre-v19 format. It is given a code block, where the argument is an Array of data, each element belonging to an entry in the old save data. The frame count is the second entry. So if we encounter a save file in the old format, we attempt to fetch the frame count from the second index in the given array.
Ruby:
end

This line ends the code block. It must exist, otherwise the game will crash with a syntax error.

Global values

The value we just created is only accessible after starting a new game or continuing with an existing save file. But what if we want to access our value from the main menu, before jumping into the game? That's where load_in_bootup comes in.
Ruby:
SaveData.register(:pokemon_system) do
  load_in_bootup
  ensure_class :PokemonSystem
  save_value { $PokemonSystem }
  load_value { |value| $PokemonSystem = value }
  new_game_value { PokemonSystem.new }
  from_old_format { |old_format| old_format[3] }
end

PokemonSystem is an important class in Essentials. It contains all of the user-defined settings, and as such has to be loaded when the application starts.
Ruby:
load_in_bootup

This line tells Essentials to load the save value when the application starts, as opposed to when starting a game session. The value specified in new_game_value is loaded during bootup if no save file exists.

For more examples, see the Game_SaveValues script file under the "Save data" header.

Advanced example

Now that we've seen how Essentials defines two of its save values, let's look into creating our own. Let's pretend that we have a complicated class with many instance variables, most of which are calculated during run-time. If possible, we can reduce the amount of data saved into the save file:
Ruby:
class MyComplexClass
  attr_reader :foo
  attr_reader :bar
  attr_reader :baz

  # other property and method definitions

  def construct_from_save_data(values)
    @foo = values[0]
    @bar = values[1]
    @baz = values[2]
    # code that sets other instance variables
  end
end

SaveData.register(:my_save_value) do
  save_value { [$my_value.foo, $my_value.bar, $my_value.baz] }
  load_value do |values|
    $my_value = MyComplexClass.new
    $my_value.construct_from_save_data(values)
  end
end

Rather than storing an entire MyComplexClass object, we can store an array that contains its most important data, and then construct the class after loading the save file.

Adding new save conversions

What if a data structure changes in a game update? Old save files would become incompatible. The conversion system is the answer to this problem. It is used in Essentials to convert v18 save data to conform with changed data structures:
Ruby:
SaveData.register_conversion(:v19_convert_game_screen) do
  essentials_version 19
  display_title 'Converting game screen'
  to_value :game_screen do |game_screen|
    game_screen.weather(game_screen.weather_type, game_screen.weather_max, 0)
  end
end

Shown above is one of many conversions defined in v19. Let's look at it line by line:

Defining a conversion
Ruby:
SaveData.register_conversion(:v19_convert_game_screen) do

SaveData.register_conversion is used to create a new conversion. It is given the ID of the conversion, which distinguishes it from the rest.

Defining its condition
Ruby:
essentials_version 19
OR
Ruby:
game_version '1.2.3'

essentials_version or game_version is used as the condition for the conversion.

essentials_version: If the Essentials version defined in the save file is less than x (19 in this case), the conversion will run.

game_version: If the game version defined in the save file is less than x (1.2.3 in the example given above), the conversion will run.

All conversions must define a condition, either essentials_version or game_version.

Defining its title
Ruby:
display_title 'Converting game screen'

display_title defines the text shown in the debug console while the conversion is running. It is optional. A title of "Running conversion ID..." is used as a fallback if no title is defined.

Defining its actual conversions
Ruby:
to_value :game_screen do |game_screen|

to_value carries out a conversion on the specified value, which in this case is :game_screen. Here, we use the value ID given in SaveData.register. to_value is given a code block, which in turn receives the value in question as the argument. In this case, the Game_Screen object.
Ruby:
game_screen.weather(game_screen.weather_type, game_screen.weather_max, 0)

This is the line that does our actual conversion. In this case, it calls the weather function of our Game_Screen object, which initializes instance variables introduced in v19.

As you can probably tell, to_value targets a single value in save data. But what if we want to add or remove values, or do other sweeping changes? That's what to_all is for.
Ruby:
SaveData.register_conversion(:v19_define_versions) do
  essentials_version 19
  display_title 'Adding game version and Essentials version to save data'
  to_all do |save_data|
    unless save_data.has_key?(:essentials_version)
      save_data[:essentials_version] = Essentials::VERSION
    end
    unless save_data.has_key?(:game_version)
      save_data[:game_version] = Settings::GAME_VERSION
    end
  end
end

This conversion is similar to the one shown prior, but with one big change: to_all is used instead of to_value.
Ruby:
to_all do |save_data|

to_all's code block is given the entire save data hash. And in this case, we are adding new values to it conditionally:
Ruby:
unless save_data.has_key?(:essentials_version)
  save_data[:essentials_version] = Essentials::VERSION
end

This code checks whether the essentials version is present in save data. If it isn't, it is added. The same is done to the game version.

For more examples, see the Game_SaveConversions script file under the "Save data" header.

NOTE: Each conversion can have a single to_all call and one to_value call for each save value. For instance, the following would be invalid:
Ruby:
SaveData.register_conversion(:invalid_conversion) do
  game_version '1.6.0'
  to_all do |save_data|
  end
  to_all do |save_data| # Multiple to_all calls! Crash!
  end
  to_value :foo do |value|
  end
  to_value :foo do |value| # Multiple to_value calls for :foo! Crash!
  end
end

It is recommended to have multiple smaller conversions as opposed to a single huge one that does multiple things.

NOTE: Conversions are run in the order they are defined in.

Saving and loading

Saving and loading are done via Game.save and Game.load:
Ruby:
Game.save(save_file -> String, safe: false) -> Boolean
# save_file: The save file path
# safe: whether $PokemonGlobal.safesave should be set to true while saving. false by default

Game.save returns whether the operation was successful. It will raise an InvalidValueError if an ensure_class check fails while saving a save value.
Ruby:
Game.load(save_data -> Hash)
# save_data: The save data hash to load

Game.load takes the save data hash to load. It will raise an InvalidValueError if an ensure_class check fails while loading a save value.

To load a save file, use SaveData.read_from_save_file alongside Game.load. See the SaveData API documentation below for more information.

The SaveData API in depth

FILE_PATH
Ruby:
SaveData::FILE_PATH -> String
Contains the file path of the save file. For instance, on Windows, this constant is set to C:\Users\USERNAME\AppData\Roaming\GAMENAME\Game.rxdata.

.exists?
Ruby:
SaveData.exists? -> Boolean
Returns whether the save file exists.

.read_from_file
Ruby:
SaveData.read_from_file(file_path -> String) -> Hash
# file_path: The path of the file to read from

Reads the data in the given file, does all necessary conversions to it and returns the save data hash. It can raise an IOError or a SystemCallError in case of insufficient file permissions or other OS-related issue.

read_from_file is used alongside Game.load to load the save file:
Ruby:
data = SaveData.read_from_file(SaveData::FILE_PATH)
Game.load(data)

.delete_file
Ruby:
SaveData.delete_file

Removes the save file in SaveData::FILE_PATH and its .bak backup file if one exists.

There are other functions in the SaveData module, but most of them are for internal use. If you're curious, check out the source file.

In summary

v19's new save data system takes out the pain from dealing with save data. It removes the need to modify Essentials code to add new save values, and makes it easy to ensure backwards compatibility in case your game's or Essentials's data structures change.

If you have any questions, feel free to ask.
Credits
No credits necessary.
Author
Savordez
Views
3,412
First release
Last update
Rating
5.00 star(s) 1 ratings

More resources from Savordez

Back
Top