ShiVa Environments pt.1: Local Save Data – ShiVa Engine

ShiVa Environments pt.1: Local Save Data

With every larger game that cannot be completed in one sitting, there comes a point when you should offer your users to save their progress and come back later to continue their playthrough. There are several methods you could use, like managed cloud saves, XML, JSON, or an online database of your own design, however the easiest, quickest and probably most secure way would be ShiVa Environments.

Setting, Reading, Unsetting

ShiVa Environments are essentially encrypted databases where you can store strings, numbers and booleans. Before we start, you should give your environment a proper name, otherwise the application will create a file named "Default.sts" which is not very descriptive. Rename your environment like so:

  1. application.setCurrentUserEnvironmentName ( "MyEnv1" )

Saving data into an environment is as easy as a single line of code:

  1. application.setCurrentUserEnvironmentVariable ( "nVar1", 10 )
  2. application.setCurrentUserEnvironmentVariable ( "sVar4", "sometext" )
  3. application.setCurrentUserEnvironmentVariable ( "bVar5", true )

Now, all your scripts in your game are able to access the data stored under nVar1, sVar4 and bVar5 through the corresponding getter function:

  1. local retrieved = application.getCurrentUserEnvironmentVariable ( "nVar1" )

Invalid data types are NIL, tables, hashtables, and objects. To store objects, you could store tags instead of a list of items that make up the current object (base model name, attachments, material overrides, etc). If you need to store (hash)tables, you could unroll them first and make use of grouping through the dot syntax:

  1. -- create a potions table
  2. local potions = hashtable.newInstance ( )
  3. hashtable.add ( potions, "health", 12 )
  4. hashtable.add ( potions, "magica", 22 )
  5. hashtable.add ( potions, "stamina", 3 )
  6.  
  7. -- unroll potions table into environment
  8. for i=0, hashtable.getSize ( potions )-1 do
  9. local key = hashtable.getKeyAt ( potions, i )
  10. application.setCurrentUserEnvironmentVariable ( "potions." ..key, hashtable.get ( potions, key ) )
  11. end

All your potions are now stored with a key using the dot syntax, for instance "potions.stamina". Dot syntax has a number of advantages and pitfalls, which we will talk about shortly.

  1. log.message ( "stamina potions: " ..application.getCurrentUserEnvironmentVariable ( "potions.stamina" ) )

Once you have no longer need of any of your environment variables, you can unset them. Remember that setting a variable = NIL is not an option, since NIL is not an accepted data type for environments. However, you will see later that this is not an optimal solution.

  1. application.unsetCurrentUserEnvironmentVariable ( "potions.stamina" )
  2. log.message ( application.getCurrentUserEnvironmentVariable ( "potions.stamina" ) ) -- logs NIL

Storing and Loading

Up until now, we have only manipulated environments within the running game. To make the data survive a game shutdown, we must save the data to disk. To dump the whole database onto disk, you can use:

  1. application.saveCurrentUserEnvironment ( )

However once your databases get sufficiently big, or you have to transmit all your data to a remote server, dumping everything every single time you make a tiny modification may not be worth the wait. Instead, you can save only single variable:

  1. application.saveCurrentUserEnvironmentVariable ( "potions.stamina" )

If you are using the dot syntax, you can use the asterisk character as a wildcard for all variables in the potions category, so that health, magica and stamina potions will all be saved with a single command:

  1. application.saveCurrentUserEnvironmentVariable ( "potions.*" )

By default, environments are stored in the "Save" folder of your project. If your environment has no name, the file will be called "Default.sts", otherwise it will carry the name you defined through setCurrentUserEnvironmentName. Loading from disk is equally simple:

  1. application.loadCurrentUserEnvironment ( "NameOfYourEnvironment" )

Please note that it is not possible to use multiple environments concurrently. Every user can have only one CurrentUserEnvironment active at a time. In other words, it is not possible to load different saves for different categories, like one STS for potions, one STS for weapons, one STS for level data. Variables of the current environment will also not survive the loading of another environment. This code will fail:

  1. application.setCurrentUserEnvironmentName ( "Env1" )
  2. application.setCurrentUserEnvironmentVariable ( "nVar1", 10 )
  3. -- check data
  4. log.message ( application.getCurrentUserEnvironmentVariable ( "nVar1" ) ) -- 10
  5. --load different environment
  6. application.loadCurrentUserEnvironment ( "Default" )
  7. log.message ( application.getCurrentUserEnvironmentVariable ( "nVar1" ) ) -- NIL

To work around this limitation and simulate groups and categories, you have to use the dot syntax.

Dot syntax and wildcards

Using dot syntax to group the items in an environment items has a number of advantages. For one, it is easier for you to keep track of your variables, since they are logically organized. Furthermore, you can easily select grouped items for storage through a wildcard. In the following snippet, we are going to save only the potion variables instead of the entire environment:

  1. application.saveCurrentUserEnvironmentVariable ( "potions.*" )

There are 2 common pitfalls with wildcard selectors. First of all, the command above will only save all potions.* variables that are in the currentUserEnvironment in the game. It will not perform checks for unused/unset variables in the file on disk. Consider the following code, where the player has depleted his FrostResist potion stash:

  1. application.setCurrentUserEnvironmentVariable ( "potions.stamina", 3 )
  2. application.setCurrentUserEnvironmentVariable ( "potions.frostResist", 1 )
  3. -- later, frostResist potion gets depleted (mistake)
  4. application.unsetCurrentUserEnvironmentVariable ( "potions.frostResist" )
  5. -- save with wildcard
  6. application.saveCurrentUserEnvironmentVariable ( "potions.*" )

The problem with that code is obvious once you reload the game. potions.frostResist has not been unset in the STS file, and only the modified potions.stamina variable was saved. potions.frostResist will still carry its old value, and you have effectively given players a never ending supply of frostResist potions as long as he reloads over and over. The solution is simple, do not unset environment variables unless you are absolutely sure they are never referenced again by any of your code. Instead, set potions.frostResist to 0, or use a reserved keyword like "EMPTY" for non-numerical data.

Alternatively, you could also just save everything with application.saveCurrentUserEnvironment ( ), which blindy copies the whole environment to disk after wiping the old save clean. This way, all your set and unset variables will be matching perfectly.

The other problem is related to loading variables with the wildcard selector. You can save to and restore variables from disk using wildcards, like so:

  1. application.saveCurrentUserEnvironmentVariable ( "potions.*" )
  2. -- clear environment for the test
  3. application.clearCurrentUserEnvironment ( )
  4. -- load from disk
  5. application.loadCurrentUserEnvironmentVariable ( "potions.*" )
  6. log.message ( application.getCurrentUserEnvironmentVariable ( "potions.stamina" ) )

However you will have no such luck using wildcards for retrieving data in already loaded environment variables. Instead, you will have to know the exact name of the key, and the function will only return a single value, not a table or hashtable.

  1. local retrieved = application.getCurrentUserEnvironmentVariable ( "potions.*" )
  2. log.message ( "Type " ..type(retrieved) ) -- type NIL

XML Comparison

XML is the easiest way to store data in an organized, human-readable way both on a local storage device or on a remote server. You are probably quite familiar with code like this:

  1. --------------------------------------------------------------------------------
  2. function envtestMain._createXML ( )
  3. --------------------------------------------------------------------------------
  4.  
  5. local hXML = xml.newInstance ( )
  6. local hRoot = xml.getRootElement ( hXML )
  7. xml.appendElementChild ( hRoot, "user.nVar7", "10" )
  8. xml.appendElementChild ( hRoot, "user.sVar2", "sometext" )
  9. xml.appendElementChild ( hRoot, "user.bVar3", "true" )
  10.  
  11. local path = "file://" ..application.getPackDirectory ( ) .."/Save/Default.xml"
  12. xml.send ( hXML, path )
  13.  
  14. --------------------------------------------------------------------------------
  15. end
  16. --------------------------------------------------------------------------------

In this code, 3 variables and their string values are saved to a local XML file. All values are string types, so you have to manually convert "true" and "10" back to bool/number when you restore the game. The XML output looks like this:

  1. <xml>
  2. <user.nVar7>10</user.nVar7>
  3. <user.sVar2>sometext</user.sVar2>
  4. <user.bVar3>true</user.bVar3>
  5. </xml>

Everyone can read and possibly modify these files. Storing critical data inside XML would leave your game saves open for modding, or in the worst case, hacking and cheating.

ShiVa environments on the other hand are not human-readable. They get saves as STS, which on top of being a binary-only format, is also encrypted. Not even a hex editor can make proper sense of its contents:

Looking forward

This week, we had a look at the basics of Environments. Creating a database, modifying entries, saving and loading should now be quite familiar to you. Next time, we will be looking at distant environments, status loops, and the ShiVa Environment tab.