A Way To Save/Load Between Versions on Desktop or Online: SaveLoadCode Library (Quest 5.8)

(Below is a copy of my post from the main Quest Forum here. Posted here in Libraries and Code Samples so it won't get buried.)

Please see the github (https://github.com/Leviathon44/SaveLoadCode/) for the latest version!

Hello everyone!

Been a big fan of text adventure games for a while, but just recently decided to try my hand at making one of my own! One of the things I wanted to be able to do with my game was give the player the ability to save their game and resume playing even on a future version of the game.

I considered using the SaveLoad library created by Pix (https://github.com/ThePix/quest/wiki/Library:-Save-and-Load), but unfortunately some of its limitations didn't quite work for what I had in mind. In particular, I wanted the player to be able to save their game to an actual file that could then be loaded on either the online player or desktop player. The solution I came up with was a copy/pastable code that could then be saved locally, but you may prefer Pix's cleaner in-game save/load menu solution. Additionally, I wanted to be able to change the player object (game.pov) mid-game, which is unfortunately a limitation of Pix's library.

Besides that, one of the primary differences between Pix's SaveLoad library and mine is that their library saves objects explicitly, rather than procedurally, which allows the author to ensure they're ONLY saving the attributes they want to in exchange for some extra legwork. My library, on the other hand, grabs everything it can, then excludes the attributes you don't want. This means that although my library is (theoretically) easier to integrate into existing games, extra care may be required to ensure you're saving all the attributes you truly want to (especially when it comes to ensuring compatibility between saves). I highly recommend checking out Pix's library as well as mine, as both have their merits!

This may be something that others have already encountered and come up with solutions for, but I wasn't able to find any concrete examples in my online or forum searches, so I figured I would share my solution here in case others would like to use it as well!

What I came up with is a system to save and load the player's game by creating a SaveGameCode function that collects nearly every attribute in the player's game, as well as a LoadGameCode function that parses out the saved code to update any changed attributes. Paired with some javascript to add a popup UI for the functions as well as base64 encoding (to limit the player's ability to directly edit savegames), I tried to keep everything as modular as possible, with self-explanatory comments, so others could use it too.

What do these functions do?

These functions allow you to save your game in the form of a base64-encoded code. This code can be saved locally, then copied into the load function to allow the player to load their game.

What this means is that players can play the game online, save their progress to a save code, then load and resume that progress on a desktop version of the same game, or vice-versa. This also means that saves can be stored SEPARATELY from the rest of the Quest game, potentially allowing a player to continue their old save even on a new version of the game (provided you, as the author, account for that, but more on that in the wiki). Furthermore, loading a save code is typically MUCH faster than loading a traditional Quest 5.8 save, as traditional Quest saves save the ENTIRE game each time (and thus require you to load the entire game each time).

The SaveGameCode function

I encourage you to look at the source code to learn more, but at a high level the SaveGameCode function works by parsing through all objects, exits, and turnscripts in the game to save as many changeable attributes as possible in the form of a long 'SaveString'. There are some limitations to this (see the 'Limitations' section in the wiki), but for most applications the SaveString that is created is enough to completely recreate the player's game state when fed back in to the LoadGameCode function. And all of this happens more or less automatically after installing the functions and javascript, so it can potentially be added on to nearly any existing game (barring limitations)!

The LoadGameCode function

You probably can guess, but the LoadGameCode function is responsible for parsing a given SaveString created by the SaveGameCode function, converting it back into usable object and attribute data, and updating the corresponding attributes in the game to recreate the player's saved state. There's a bit more to it, but that's the high-level explanation. To learn more, I again highly recommend checking out the wiki and/or source code!

The SaveLoadJavaCode.js javascript

In addition to those two functions, I also included some additional javascript functions for converting the savecode to/from Base64 encoding (to make it a little harder to 'cheat' by editing your save state), as well as provide a nice UI for presenting the savecode and a nice UI for the player to paste existing savecodes into.

Additional Included Functions:

The SaveCheckpoints function

SaveCheckpoint works very similarly to SaveGameCode, except instead of saving the SaveString to present it to the player, it instead will save a SaveString to a game.checkpoints stringdictionary. If game.checkpoints does not exist when SaveCheckpoints is called, then it will create it.

This is useful for saving checkpoints internally that can then be loaded internally using the LoadCheckpoint function.

See the wiki for more details.

The LoadCheckpoint function

LoadCheckpoint works very similarly to LoadGameCode, except it will not print any messages and it will not overwrite existing checkpoint data. It will also only load existing checkpoints. If it cannot find a matching checkpoint in game.checkpoints, it will throw an error.

Useful for loading internally-created checkpoints (i.e. after a "you have died! Would you like to reload before you died?" type screen, for instance).

The GetSaveGameCodeDelims function

GetSaveGameCodeDelims() returns a stringlist of the delimiters used by the SaveGameCode function.

Useful for checking whether a user-entered input contains a banned delimiter before letting them change an attribute, (i.e. like in a "enter your name" screen taking user input to change the player.alias attribute). As a reminder, if any attributes saved by SaveGameCode contain a banned delimiter, the game will not save/load properly.

The GetSaveCheckpointDelims function

GetSaveCheckpointDelims() returns a stringlist of the delimiters used by the SaveCheckpoint function.

Useful for checking whether a user-entered input contains a banned delimiter before letting them change an attribute, (i.e. like in a "enter your name" screen taking user input to change the player.alias attribute). As a reminder, if any attributes saved by SaveCheckpoint contain a banned delimiter, the game will not save/load checkpoints properly.


I've now completed the wiki page for these functions. Check it out if you're interested, as it goes into great detail regarding how to use these functions in your game, their limitations, workarounds, etc: https://github.com/Leviathon44/SaveLoadCode/wiki

I also created a small test game to show off some of the functionality here (albeit slightly out of date): http://textadventures.co.uk/games/view/gsxdvwowaumaamc28spsga/

I've also set up a git repository with these functions and commands drawn up in a library, 'SaveLoadCodeFunctionsLibrary.aslx', and the required javascript, 'SaveLoadJavaCode.js', here: https://github.com/Leviathon44/SaveLoadCode.

Again, please check the github (https://github.com/Leviathon44/SaveLoadCode/wiki) for the latest version!

Special thanks to KV, mrangel, and Pix! Your forum posts and functions over the years were a valuable resource in getting this made, and mrangel's help in particular helped me solve some of the trickier issues allowing me to improve this library! I hope others find this useful!


The code for the entire test game:

<!--Saved by Quest 5.8.6836.13983-->
<asl version="580">
  <include ref="English.aslx" />
  <include ref="Core.aslx" />
  <game name="Save Testing">
    <gameid>932164b2-0eae-47d6-94b4-402d8a2238d9</gameid>
    <version>3.0</version>
    <firstpublished>2022</firstpublished>
    <feature_devmode />
    <attr name="devmode_setinitscript" type="boolean">false</attr>
    <attr name="devmode_changepovpos" type="boolean">false</attr>
    <devmode_setverbs />
    <gridmap />
    <showscore type="boolean">false</showscore>
    <showhealth type="boolean">false</showhealth>
    <showmoney type="boolean">false</showmoney>
    <attr name="feature_limitinventory" type="boolean">false</attr>
    <attr name="feature_lightdark" type="boolean">false</attr>
    <attr name="feature_asktell" type="boolean">false</attr>
    <attr name="feature_annotations" type="boolean">false</attr>
    <attr name="feature_advancedwearables" type="boolean">false</attr>
    <attr name="feature_advancedscripts" type="boolean">false</attr>
    <appendobjectdescription type="boolean">false</appendobjectdescription>
    <allowlookdirections type="boolean">false</allowlookdirections>
    <multiplecommands type="boolean">false</multiplecommands>
    <command_newline />
    <clearscreenonroomenter type="boolean">false</clearscreenonroomenter>
    <autodisplayverbs />
    <author>Leviathon</author>
    <start type="script">
      // Initialization script for SaveLoadCode functionality...
      // Create a list of all object and exit names present in game at start. This way we can keep track of which objects/exits the player has created/destroyed while playing by comparing to this list when saving/loading.
      set (game, "StartingObjStrList", NewStringList ())
      foreach (startobj, AllObjects()) {
        list add (game.StartingObjStrList, startobj.name)
      }
      foreach (startobj, AllExits()) {
        list add (game.StartingObjStrList, startobj.name)
      }
      set (game, "checkpoints", NewStringDictionary())
      // End of SaveLoadCode initialization script
    </start>
  </game>
  <object name="room">
    <inherit name="editor_room" />
    <isroom />
    <description><![CDATA[This is the starting room of the SaveLoadCode functionality demo.<br/><br/>Type "SaveCode" at any time to create a save code, allowing you to load later. Copy the code to a notepad document to save it for later!<br/><br/>Type "LoadCode" at any time to load a previously saved code. Paste in your previously-saved save code to load your game!]]></description>
    <object name="player">
      <inherit name="editor_object" />
      <inherit name="editor_player" />
      <StringAttribute>Stringy Bingy</StringAttribute>
      <alias>Francis</alias>
      <ObjectListRedux type="objectlist"></ObjectListRedux>
    </object>
    <object name="BoingoBall">
      <inherit name="editor_object" />
      <IsBall />
      <MemoryDict type="stringdictionary">
        <item>
          <key>Mem1</key>
          <value>Memories of 1</value>
        </item>
        <item>
          <key>Mem2</key>
          <value>Memories of 2</value>
        </item>
      </MemoryDict>
      <ListOfString type="stringlist">
        <value>Brock</value>
        <value>Misty</value>
        <value>Ash</value>
        <value>Pikachu</value>
      </ListOfString>
      <look>This is an object with a bunch of attributes attached to it. The SaveGameCode and LoadGameCode functions will save all its attributes and update them on load if they've changed.</look>
    </object>
    <exit name="KeyRoomDoor" alias="east" to="Key Room">
      <inherit name="eastdirection" />
      <locked />
      <runscript />
      <script type="script"><![CDATA[
        if (DoorKey.parent=game.pov and this.locked=true) {
          msg ("<i>You unlock the door and enter</i>")
          this.locked = false
          game.pov.parent = Key Room
        }
        else if (this.locked=true) {
          msg ("That way is locked.")
        }
        else {
          game.pov.parent = Key Room
        }
      ]]></script>
    </exit>
    <exit alias="north" to="SideRoom">
      <inherit name="northdirection" />
    </exit>
    <object name="DoorKey">
      <inherit name="editor_object" />
      <take />
      <alias>Key</alias>
      <feature_usegive />
      <use type="script"><![CDATA[
        if (game.pov.parent=room and KeyRoomDoor.locked=True) {
          msg ("<i>Using the key, you unlock the door!</i>")
          KeyRoomDoor.locked = False
          msg ("<br>As you walk through the door, the key disintegrates in your hand, <i>destroying</i> it. (i.e. The Key object itself has been destroyed. If you type 'SaveCode' now, when you load that save the Key will still be destroyed!)")
          destroy ("DoorKey")
        }
        else if (game.pov.parent=room and KeyRoomDoor.locked=False) {
          msg ("<i>The door is already unlocked!</i>")
        }
        else {
          msg ("<i>There are no locks to use this on here!</i>")
        }
      ]]></use>
    </object>
    <object name="SaveCheckpoint Button">
      <inherit name="editor_object" />
      <look><![CDATA[Using this button will create a checkpoint! Using it more than once will overwrite the existing checkpoint.<br/><br/>If you find the LoadCheckpoint Button, you can then load the checkpoint created here.]]></look>
      <feature_usegive />
      <displayverbs type="stringlist">
        <value>Look at</value>
        <value>Use</value>
      </displayverbs>
      <use type="script">
        SaveCheckpoint ("Checkpoint")
      </use>
    </object>
  </object>
  <object name="Key Room">
    <inherit name="editor_room" />
    <description><![CDATA[You made it in!<br/><br/>If you save now and load from a new game, this room will still be unlocked!]]></description>
    <enter type="script">
    </enter>
    <firstenter type="script">
    </firstenter>
    <exit alias="west" to="room">
      <inherit name="westdirection" />
    </exit>
  </object>
  <object name="SideRoom">
    <inherit name="editor_room" />
    <description><![CDATA[A side room. When you load, this room will show up on the map since you've explored it now! (It was harder to make this happen than you think it would be...)<br/><br/>{here DoorKey:You see a {object:DoorKey} on the floor!}]]></description>
    <exit alias="south" to="room">
      <inherit name="southdirection" />
    </exit>
    <exit alias="west" to="OtherSideRoom">
      <inherit name="westdirection" />
    </exit>
    <exit alias="down" to="Basement">
      <inherit name="downdirection" />
    </exit>
    <object name="LoadCheckpoint Button">
      <inherit name="editor_object" />
      <feature_usegive />
      <use type="script">
        LoadCheckpoint ("Checkpoint")
      </use>
      <displayverbs type="stringlist">
        <value>Look at</value>
        <value>Use</value>
      </displayverbs>
      <look>Using this button will load the checkpoint created by the SaveCheckpoint Button (if one was created...)</look>
    </object>
  </object>
  <object name="OtherSideRoom">
    <inherit name="editor_room" />
    <exit alias="east" to="SideRoom">
      <inherit name="eastdirection" />
    </exit>
  </object>
  <object name="Basement">
    <inherit name="editor_room" />
    <attr name="grid_fill">Teal</attr>
    <exit alias="up" to="SideRoom">
      <inherit name="updirection" />
    </exit>
    <exit alias="east" to="Basement Side">
      <inherit name="eastdirection" />
    </exit>
  </object>
  <object name="Basement Side">
    <inherit name="editor_room" />
    <attr name="grid_fill">Teal</attr>
    <exit alias="west" to="Basement">
      <inherit name="westdirection" />
    </exit>
  </object>
  <command>
    <pattern>SaveCode</pattern>
    <script>
      SuppressTurnscripts
      SaveGameCode (True)
    </script>
  </command>
  <command>
    <pattern>LoadCode</pattern>
    <script>
      SuppressTurnscripts
      JS.LoadGamePrompt ()
    </script>
  </command>
  <function name="LoadGameCode" parameters="SaveGameCodeDecoded"><![CDATA[
    // LoadGameCode(SaveGameCodeDecoded) function to load a SaveGameCode save-state. Takes a decoded (not in base64) SaveGameCode created by SaveGameCode and decoded from base64 by java functions. Requires SaveLoadJavaCode.js in order to function!
    SuppressTurnscripts
    // NOTE: Requires either JS.LoadSaveCode to be called with a SaveGameCode as an input parameter, or JS.LoadGamePrompt to be called elsewhere (i.e. by a custom "LoadGame" command), which will make a popup appear for the player to enter their SaveCode, eventually piping it to this function.
    // TO DEVELOPER: Set the OldestAllowedVersion to the oldest compatible game version that a player can load saved game data from. Setting OldestAllowedVersion=0 will essentially allow the player to load saves from any old version. Setting OldestAllowedVersion=game.version will make it so the player can ONLY load saves from the current game version.
    OldestAllowedVersion = 2.0
    // TO DEVELOPER: Setting DebugMode to 'True' will enable the printing of debug messages to the screen when running. Very useful for testing out the function if you've made any custom edits for compatibility or the like.
    DebugMode = False
    // Msg for Debugging:
    if (DebugMode) {
      msg ("<br>Full decoded SaveCode:<br>"+SaveGameCodeDecoded)
    }
    // Set up other variables for later
    bla => {
    }
    upgradesave = False
    Proceed = False
    SkippedAttList = ""
    CreatedObjDebugList = ""
    DestroyedObjDebugList = ""
    // Check for a "✓✓" at the end of the SaveGameCodeDecoded string. If it's there, then the function knows this savecode is for an older game version. "But how do we know that ✓✓ isn't being used as a custom delimiter?" Because custom delimiters can only be one character long and two delimiters cannot be the same. Also ✓ (theoretically) cannot be converted to base64, so the user would get an error trying to make a SaveGameCode with ✓ as a delimiter.
    CheckForCheck = Right(SaveGameCodeDecoded,2)
    if (CheckForCheck="✓✓") {
      upgradesave = True
      SaveGameCodeDecoded = Left(SaveGameCodeDecoded, LengthOf(SaveGameCodeDecoded)-2)
    }
    // Retrieve delimiters from end of SaveGameCodeDecoded
    Dls = Right(SaveGameCodeDecoded,4)
    D1 = Mid (Dls, 1, 1)
    D2 = Mid (Dls, 2, 1)
    D3 = Mid (Dls, 3, 1)
    D4 = Mid (Dls, 4, 1)
    // Remove delimiters from end of SaveGameCode
    SaveCode = Left(SaveGameCodeDecoded, LengthOf(SaveGameCodeDecoded)-(LengthOf(Dls)))
    // Special logic to extract saved game.checkpoints attribute
    D4toD1 = D4+D3+D2+D1
    CheckpointSplit = Split(SaveCode, D4toD1)
    SaveCode = ListItem(CheckpointSplit,0)
    CheckpointKeys = ListItem(CheckpointSplit,1)
    // If CheckpointKeys not an empty string, then that means there was game.checkpoints data saved. Extract it.
    if (not CheckpointKeys="") {
      CheckpointKeyList = Split(CheckpointKeys, D3)
      game.checkpoints = NewStringDictionary()
      for (xx, 0, ListCount(CheckpointKeyList)-1) {
        key = ListItem(CheckpointKeyList,xx)
        val = ListItem(CheckpointSplit,xx+1)
        DictionaryAdd (game.checkpoints, key, val)
      }
    }
    // Extract the Created/Destroyed object lists. The START of the created/destroyed section should be after the LAST D4 delimiter...
    CreatedDestroyedInfo = Right(SaveCode, LengthOf(SaveCode)-InstrRev(SaveCode, D4))
    if (DebugMode) {
      msg ("<br>CreatedDestroyedInfo: "+CreatedDestroyedInfo)
    }
    CDList = Split(CreatedDestroyedInfo,D1)
    CSection = ListItem (CDList, 0)
    DSection = ListItem (CDList, 1)
    if (CSection="") {
      CreatedList = NewStringList()
    }
    else {
      CreatedList = Split (CSection, D3)
      // The way the lists are saved, when you load it, a blank entry will be created. Let's remove that...
      list remove (CreatedList, " ")
    }
    if (DSection="") {
      DestroyedList = NewStringList()
    }
    else {
      DestroyedList = Split (DSection, D3)
      // The way the lists are saved, when you load it, a blank entry will be created. Let's remove that...
      list remove (DestroyedList, " ")
    }
    // Remove Created/Destroyed list from end of SaveCode, also removing the final D1 and D4 delimiter...
    SaveCode = Left(SaveCode, LengthOf(SaveCode)-(LengthOf(CreatedDestroyedInfo)+2))
    // Extract the player.grid_coordinates info separately from the rest of the savecode. It has special rules for decoding it since it is a dictionary of dictionaries.
    GridGInfo = Right(SaveCode, LengthOf(SaveCode)-Instr(SaveCode, D4))
    if (DebugMode) {
      msg ("<br>GridGInfo: "+GridGInfo)
    }
    // Remove player.grid_coordinates info from end of SaveCode also remove the final D1 & D4 delimiter separating the grid_coordinates from the rest of the attributes
    SaveCode = Left(SaveCode, LengthOf(SaveCode)-(LengthOf(GridGInfo)+2))
    if (DebugMode) {
      msg ("<br>SaveCode w/o player.grid_coordinate or create/destroy info:<br>"+SaveCode)
    }
    // Note: if the "SaveCode" begins with the word "Error:", then an error was encountered when trying to convert from Base64.
    // Extract SaveCode game info. This includes the gameid, game version, and current player (game.pov)...
    GameIdDelim = Instr (SaveCode, D1)
    GameVersionDelim = Instr(GameIdDelim+1,SaveCode,D1)
    GamePOVDelim = Instr(GameVersionDelim+1,SaveCode,D1)
    GameInfo = Split(Left(SaveCode,GamePOVDelim-1),D1)
    GameIdObjectEl = ListItem (GameInfo,0)
    GameIdElements = Split(GameIdObjectEl,D2)
    Loaded_GameId = ListItem (GameIdElements, 3)
    GameVerObjectEl = ListItem (GameInfo,1)
    GameVerElements = Split(GameVerObjectEl,D2)
    VersionString = ListItem (GameVerElements, 3)
    Loaded_GameVersion = ToDouble(VersionString)
    GamePOVObjectEl = ListItem (GameInfo,2)
    GamePOVElements = Split(GamePOVObjectEl,D2)
    GamePOVName = ListItem (GamePOVElements, 3)
    if (StartsWith(GamePOVName,"Object: ")) {
      GamePOVName = Right(GamePOVName,LengthOf(GamePOVName)-LengthOf("Object: "))
    }
    GamePOVObject = GetObject (GamePOVName)
    GamePOVParent = GetAttribute (GamePOVObject, "parent")
    // Check that the save belongs to this game by comparing gameIds
    if (not Loaded_GameId=game.gameid) {
      error ("Load Aborted: SaveCode not identified by this game. GameID mismatch.")
    }
    else {
      // Compare version of game in SaveCode to version of game loading it
      ThisGame_GameVersion = ToDouble(game.version)
      if (not TypeOf(OldestAllowedVersion)="double") {
        OldestAllowedVersion_Double = ToDouble(OldestAllowedVersion)
      }
      else {
        OldestAllowedVersion_Double = OldestAllowedVersion
      }
      // If upgrading from an old game version, then arbitrarily set Loaded_GameVersion to ThisGameVersion to proceed.
      if (Loaded_GameVersion<ThisGame_GameVersion) {
        if (upgradesave = False) {
          if (OldestAllowedVersion_Double<=Loaded_GameVersion) {
            msg ("WARNING! The SaveCode you are attempting to load is from an older game version.<br>Saved Game: v"+ToString(Loaded_GameVersion)+"<br>This Game: v"+ToString(ThisGame_GameVersion)+"<br><br>Would you like to attempt to upgrade this save to the current version? (Results may vary...)")
            // Need to save SaveGameCodeDecoded as an attribute temporarily so it can be used by the ShowMenu function
            create ("SaveGameDecodedObj")
            set (SaveGameDecodedObj, "value", SaveGameCodeDecoded)
            ShowMenu ("", Split("Yes;No"), false) {
              switch (result) {
                case ("Yes") {
                  SuppressTurnscripts
                  msg ("Save code identified! Proceeding with load, please wait...")
                  OlderSaveCode = SaveGameDecodedObj.value+"✓✓"
                  LoadGameCode (OlderSaveCode)
                  destroy ("SaveGameDecodedObj")
                }
                case ("No") {
                  SuppressTurnscripts
                  msg ("Load Aborted.")
                  destroy ("SaveGameDecodedObj")
                }
              }
            }
          }
          else {
            error ("ERROR: The SaveCode you are attempting to load is from an INCOMPATIBLE older game version, and thus cannot be loaded by this version of the game.<br>Saved Game: v"+ToString(Loaded_GameVersion)+"<br>This Game: v"+ToString(ThisGame_GameVersion)+"<br>Oldest Compatible Version: v"+ToString(OldestAllowedVersion)+"<br><br>Loading aborted...")
          }
        }
        else {
          msg ("Applying savecode from older version...")
          Proceed = True
        }
      }
      else if (Loaded_GameVersion>ThisGame_GameVersion) {
        error ("ERROR: The SaveCode you are attempting to load is from a newer version of this game and is not compatible.<br>Saved Game: v"+ToString(Loaded_GameVersion)+"<br>This Game: v"+ToString(ThisGame_GameVersion)+"<br>Please try a different SaveCode or use an updated game file.<br><br>Load aborted.")
      }
      else {
        msg ("Proceeding with load...")
        Proceed = True
      }
      if (Proceed=True) {
        // Create any objects noted in the CreatedList, if there are any, so their relevant attributes can be added without error...
        if (ListCount(CreatedList)>0) {
          foreach (o, CreatedList) {
            // Check that objects don't already exist...
            IsThere = GetObject(o)
            if (Equal(IsThere,null)) {
              // If not, create the object
              create (o)
              CreatedObjDebugList = CreatedObjDebugList+o+"<br>"
            }
          }
        }
        player.grid_coordinates = null
        // Split the save code up into all objects. Then parse through the value of each object attribute
        SavedObjectList = Split(SaveCode, D1)
        foreach (o, SavedObjectList) {
          Skip_Att = False
          objelements = Split(o, D2)
          objectname = ListItem (objelements, 0)
          object = GetObject (objectname)
          attributename = ListItem (objelements, 1)
          fullname = objectname+"."+attributename
          preload_att_value = GetAttribute (object, attributename)
          att_datatype = ListItem (objelements, 2)
          if (ListCount(objelements)=3) {
            att_value = ""
          }
          else {
            att_value = ListItem (objelements, 3)
          }
          // Check that the attribute is supported
          if (not att_datatype="string" and not att_datatype="boolean" and not att_datatype="object" and not att_datatype="int" and not att_datatype="double" and not att_datatype="stringlist" and not att_datatype="objectlist" and not att_datatype="stringdictionary" and not att_datatype="objectdictionary") {
            msg ("WARNING! Unsupported datatype \""+att_datatype+"\" detected in SaveCode attribute \""+fullname+"\"! Skipping and moving on to next attribute...")
            Skip_Att = True
            SkippedAttList = SkippedAttList+fullname+"<br>"
          }
          // Convert the string attribute value and convert it to the datatype that it actually needs to be. This att_value_obj variable will also be directly compared to preload_att_value to determine if the pre- and post- load values are equal or not...
          att_value_obj = att_value
          if (att_datatype="object") {
            if (StartsWith(att_value,"Object: ")) {
              att_value_obj = GetObject(Right(att_value,LengthOf(att_value)-LengthOf("Object: ")))
            }
            else {
              att_value_obj = GetObject(att_value)
            }
          }
          else if (att_datatype="boolean") {
            if (att_value="True") {
              att_value_obj = True
            }
            else {
              att_value_obj = False
            }
          }
          else if (att_datatype="int") {
            att_value_obj = ToInt(att_value)
          }
          else if (att_datatype="double") {
            att_value_obj = ToDouble(att_value)
          }
          else if (att_datatype="stringlist") {
            if (att_value="") {
              att_value_obj = NewStringList()
            }
            else {
              att_value_obj = Split (att_value, D3)
              // The way the lists are saved, when you load it, a blank entry will be created. Let's remove that...
              list remove (att_value_obj, " ")
            }
          }
          else if (att_datatype="objectlist") {
            if (att_value="") {
              att_value_obj = NewObjectList()
            }
            else {
              att_value_obj = NewObjectList()
              objlistlist = Split (att_value, D3)
              foreach (olt, objlistlist) {
                // Need to remove the "Object: " that will preceed each entry, and turn the string entry into the actual object before re-adding to list. We put it into the following "if" statement in order to exclude the blank list entry that gets created at the end of the list by loading
                if (StartsWith(olt,"Object: ")) {
                  value = GetObject(Right(olt,LengthOf(olt)-LengthOf("Object: ")))
                  if (not value=null) {
                    list add (att_value_obj, value)
                  }
                  else {
                    msg ("WARNING! Object \""+olt+"\" detected in saved objectlist \""+fullname+"\" does not exist! Object \""+olt+"\" not added to list! Loaded game may not work properly!")
                    SkippedAttList = SkippedAttList+"Objectlist '"+fullname+"' item: "+olt+"<br>"
                  }
                }
              }
            }
          }
          else if (att_datatype="stringdictionary") {
            if (att_value="") {
              att_value_obj = NewStringDictionary()
            }
            else {
              att_value_obj = NewStringDictionary()
              // Add dictionary values from SaveGame
              dictrows = Split(att_value, ";")
              foreach (kv, dictrows) {
                if (DebugMode) {
                  msg ("StringDict '"+fullname+"' key-value: "+ToString(kv))
                }
                KeyValList = Split(kv," = ")
                key = ListItem(KeyValList, 0)
                value = ListItem(KeyValList, 1)
                DictionaryAdd (att_value_obj, key, value)
              }
            }
          }
          else if (att_datatype="objectdictionary") {
            if (att_value="") {
              att_value_obj = NewObjectDictionary()
            }
            else {
              att_value_obj = NewObjectDictionary()
              dictrows = Split(att_value, ";")
              foreach (kv, dictrows) {
                if (DebugMode) {
                  msg ("ObjDict  '"+fullname+"' key-value: "+ToString(kv))
                }
                KeyValList = Split(kv," = ")
                key = ListItem(KeyValList, 0)
                obj = ListItem(KeyValList, 1)
                if (StartsWith(obj,"Object: ")) {
                  value = GetObject(Right(value,LengthOf(value)-LengthOf("Object: ")))
                }
                else {
                  value = obj
                }
                if (not value=null) {
                  DictionaryAdd (att_value_obj, key, value)
                }
                else {
                  msg ("WARNING! Object \""+obj+"\" detected in saved objectdictionary \""+fullname+"\" does not exist! Object \""+obj+"\" not added to dictionary! Loaded game may not work properly!")
                  SkippedAttList = SkippedAttList+"Objectdictionary '"+fullname+"' item: "+olt+"<br>"
                }
              }
            }
          }
          if (objectname=GamePOVName and attributename="parent") {
            // Check that the attribute is NOT game.pov.parent. If so, we want to make sure that gets updated last
            Skip_Att = True
            GamePOVParent = att_value_obj
          }
          // Make sure the object you are trying to add/update the attribute to exists, otherwise you'd get an error trying to update/create its attribute. If the attribute doesn't exist but the object does, then this function will create it. Also, don't update the game.version or game.pov: The game.version should not be updated from the savecode if you're loading from a previous version, and the game.pov is updated last.
          if (not Equal(object,null) and not Equal(att_value_obj,null) and not fullname="game.gameid" and not fullname="game.version" and not fullname="game.pov" and not Skip_Att=True) {
            if (Equal(preload_att_value,null)) {
              if (DebugMode) {
                msg ("<br>ATTENTION: Attribute '"+fullname+"' does NOT exist in current game, but its parent object '"+objectname+"' does, so attribute will be created!<br>")
              }
              preload_att_value = "null"
            }
            // Msgs for debugging:
            if (DebugMode) {
              msg ("objectname="+objectname)
              msg ("attributename="+attributename)
              msg ("att_datatype="+att_datatype)
              msg ("preload_att_value="+ToString(preload_att_value))
              if (Equal(preload_att_value,"null")) {
                msg ("preload_att_datatype=null")
              }
              else {
                msg ("preload_att_datatype="+TypeOf(preload_att_value))
              }
              msg ("att_value="+ToString(att_value))
              msg ("att_value_obj="+ToString(att_value_obj))
              msg ("isEqual att_value=preload_att_value?: "+ToString(Equal(att_value,preload_att_value)))
              msg ("isEqual att_value_obj=preload_att_value?: "+ToString(Equal(att_value_obj,preload_att_value)))
              msg ("isEqual ToString(att_value_obj)=ToString(preload_att_value)?: "+ToString(Equal(ToString(att_value_obj),ToString(preload_att_value))))
              msg ("<br>")
            }
            // If attributes are already equal to those in the savecode, no need to change them. Else, change 'em.
            if (not Equal(att_value,preload_att_value) and not Equal(att_value_obj,preload_att_value) and not Equal(ToString(att_value_obj),ToString(preload_att_value))) {
              if (DebugMode) {
                msg ("Updating attribute: "+fullname+"<br><br>")
              }
              // Check if attribute has an associated change script. If so, this section will make sure that setting the attribute on load WON'T activate its associate turnscript
              cha = "changed" + attributename
              if (HasAttribute (object, cha)) {
                // If the attribute DOES have an associated change script, temporarily blank it out so it does not execute during loading
                scr = GetAttribute (object, cha)
                set (object, cha, bla)
              }
              // Update the attributes in the game with those from the SaveCode...
              if (att_datatype="boolean") {
                set (object, attributename, att_value_obj)
              }
              else if (att_datatype="int") {
                set (object, attributename, att_value_obj)
              }
              else if (att_datatype="double") {
                set (object, attributename, att_value_obj)
              }
              else if (att_datatype="object") {
                set (object, attributename, att_value_obj)
              }
              else if (att_datatype="stringlist" or att_datatype="objectlist" or att_datatype="stringdictionary" or att_datatype="objectdictionary") {
                // NOTE TO DEVELOPER:
                // Alter the following logic below to fit your needs. Especially important to make sure this works properly for YOUR game for compatibility between game versions!
                // If ReplaceContents = True, then any list or dictionary in your game will be COMPLETELY REPLACED by its corresponding list/dictionary from the savecode.
                // If ReplaceContents = False, then the list/dictionary contents in the SaveCode will be ADDED to the existing corresponding list/dictionary. NOTE: When adding to an existing list/dict, the code, as-written, will REMOVE ANY DUPLICATES from the lists/dictionaries! ALSO, be careful where you allow LoadGame() to be called in cases where ReplaceContents=False, ESPECIALLY if the list/dict contents can change through the course of the game! Calling this LoadGameCode function only from a titlescreen (before any lists/dictionaries have changed), for instance, may be one possible way to account for this.
                // ReplaceContents=True by default, but this may not be desirable in all cases (i.e. if you updated the contents of a permanent dictionary/list between versions), so it is up to YOU to ensure this section behaves as you want it to. Remember that the "object" and "attributename" variables exist at this point to call out specific list/dictionary objects.
                if (upgradesave = True) {
                  // This section will trigger if the player is loading a save from a previous game version
                  ReplaceContents = True
                }
                else {
                  // If this savecode is NOT coming from a previous game version, then I assume it is safe to completely replace the existing dictionary with the saved one.
                  ReplaceContents = True
                }
                if (att_datatype="stringlist") {
                  if (ReplaceContents = True) {
                    // Completely replace stringlist contents with those found in the SaveCode
                    set (object, attributename, att_value_obj)
                  }
                  else {
                    // Add the contents of the saved stringlist TO the existing stringlist in-game
                    FinalList = NewStringList()
                    // Retrieve the contents of the existing list
                    PreLoadList = preload_att_value
                    CombinedList = ListCombine (PreLoadList, att_value_obj)
                    // Remove duplicates
                    CompactList = ListCompact (CombinedList)
                    // CompactList will be a generic "list" type object, need to convert it back to a stringlist...
                    foreach (olt, CompactList) {
                      list add (FinalList, olt)
                    }
                    set (object, attributename, FinalList)
                  }
                }
                else if (att_datatype="objectlist") {
                  if (ReplaceContents = True) {
                    // Completely replace objectlist contents with those found in the SaveCode
                    set (object, attributename, att_value_obj)
                  }
                  else {
                    // Add the contents of the saved objectlist TO the existing objectlist in-game
                    // Retrieve the contents of the existing list
                    PreLoadList = preload_att_value
                    CombinedList = ListCombine (PreLoadList, att_value_obj)
                    // Remove duplicates
                    FinalList = ObjectListCompact (CombinedList)
                    set (object, attributename, FinalList)
                  }
                }
                else if (att_datatype="stringdictionary") {
                  if (ReplaceContents = True) {
                    // Then completely overwrite existing stringdictionary contents with those in the savecode
                    set (object, attributename, att_value_obj)
                    Dummy = NewStringDictionary()
                  }
                  else {
                    // Add the contents of the saved stringdictionary TO the existing stringdictionary in-game
                    Dummy = preload_att_value
                    // Add dictionary values from SaveGame
                    dictrows = Split(att_value, ";")
                    foreach (kv, dictrows) {
                      KeyValList = Split(kv," = ")
                      key = ListItem(KeyValList, 0)
                      value = ListItem(KeyValList, 1)
                      DictionaryAdd (Dummy, key, value)
                    }
                    set (object, attributename, Dummy)
                  }
                }
                else if (att_datatype="objectdictionary") {
                  if (upgradesave = False) {
                    // Then completely overwrite existing objectdictionary contents with those in the savecode
                    set (object, attributename, att_value_obj)
                  }
                  else {
                    // Add the contents of the saved objectdictionary TO the existing objectdictionary in-game
                    Dummy = preload_att_value
                    // Add dictionary values from SaveGame
                    dictrows = Split(att_value, ";")
                    foreach (kv, dictrows) {
                      KeyValList = Split(kv," = ")
                      key = ListItem(KeyValList, 0)
                      value = ListItem(KeyValList, 1)
                      if (StartsWith(value,"Object: ")) {
                        value = GetObject(Right(value,LengthOf(value)-LengthOf("Object: ")))
                      }
                      DictionaryAdd (Dummy, key, value)
                    }
                    set (object, attributename, Dummy)
                  }
                }
              }
              else if (att_datatype="string") {
                set (object, attributename, att_value)
              }
              else {
                error ("ERROR: Unsupported object type detected in SaveCode: "+fullname+" of "+att_datatype+" datatype.")
              }
              if (HasAttribute (object, cha)) {
                // If a change script exists for this attribute, set change script back to original value after attribute has been changed
                set (object, cha, scr)
                scr => {
                }
              }
            }
          }
        }
        // Extract and update map data from saved grid_coordinates. Because grid_coordinates is a dictionary of dictionaries, it needed to be saved in a special way. Thus, it needs to be loaded in a special way as well.
        // If D1=|,D2=$,D3=;,and D4=@, then grid_coordinates will be saved in form: "ObjectOwner$MapAttributeName&%&Key1$Lkey1 = Lvalue1:type;Lkey2 = Lvalue2:type;|Key2$Lkey1 = Lvalue1:type;Lkey2 = Lvalue2:type;Lkey3 = Lvalue3:type|" etc.
        AllSavedGrids = Split(GridGInfo,D4)
        if (DebugMode) {
          msg ("<br>AllSavedGrids: "+ToString(AllSavedGrids))
        }
        foreach (A, AllSavedGrids) {
          UDictionary = NewDictionary()
          ItemAndValue = Split(A,"&%&")
          ObjAndAtt = ListItem(ItemAndValue,0)
          ObjAndAtt = Split(ObjAndAtt,D2)
          objectname = ListItem(ObjAndAtt,0)
          attributename = ListItem(ObjAndAtt,1)
          object = GetObject(objectname)
          GridVals = ListItem(ItemAndValue,1)
          GridVals = Split(GridVals,D1)
          foreach (B, GridVals) {
            UKeyAndUVal = Split(B,D2)
            UKey = ListItem(UKeyAndUVal,0)
            UVal = ListItem(UKeyAndUVal,1)
            UVal = Split(UVal,D3)
            LDictionary = NewDictionary()
            foreach (C, UVal) {
              LkeyAndLval = Split(C," = ")
              Lkey = ListItem(LkeyAndLval,0)
              LvalAndType = ListItem(LkeyAndLval,1)
              LvalAndType = Split(LvalAndType,":")
              Lval_str = ListItem(LvalAndType,0)
              LType = ListItem(LvalAndType,1)
              if (LType="int") {
                Lval = ToInt(Lval_str)
              }
              else if (LType="double") {
                Lval = ToDouble(Lval_str)
              }
              else if (LType="boolean") {
                if (Lval_str="True") {
                  Lval = True
                }
                else {
                  Lval = False
                }
              }
              else {
                error ("ERROR: Unsupported datatype found in "+objectname+"."+attributename+"! Datatype '"+LType+"' not supported!")
              }
              DictionaryAdd (LDictionary, Lkey, Lval)
            }
            DictionaryAdd (UDictionary, UKey, LDictionary)
          }
          if (DebugMode) {
            msg ("<br>"+objectname+"."+attributename+" UDictionary: "+ToString(UDictionary))
          }
          set (object, attributename, UDictionary)
        }
        // Destroy any objects that the player destroyed during their saved game, if any
        if (ListCount(DestroyedList)>0) {
          foreach (o, DestroyedList) {
            // Check that objects still exist...
            IsThere = GetObject(o)
            if (not Equal(IsThere,null)) {
              // If its there, destroy the object
              destroy (o)
              DestroyedObjDebugList = DestroyedObjDebugList+o+"<br>"
            }
          }
        }
        msg ("Load complete!")
        if (DebugMode) {
          msg ("Created objects: "+CreatedObjDebugList)
          msg ("Destroyed objects: "+DestroyedObjDebugList)
          msg ("Skipped Attributes:<br>"+SkippedAttList)
        }
        // Finally, update game.pov.parent and game.pov
        wait {
          set (GamePOVObject, "parent", GamePOVParent)
          game.pov = GamePOVObject
          // player.grid_coordinates = null
          JS.Grid_ClearAllLayers ()
          Grid_Redraw
          Grid_DrawPlayerInRoom (game.pov.parent)
          ClearScreen
          ShowRoomDescription
        }
      }
    }
  ]]></function>
  <function name="SaveGameCode" parameters="ShowCodePopupFlag" type="string"><![CDATA[
    // SaveGameCode(ShowCodePopupFlag) Function to collect changeable attributes into a string in order to generate a SaveCode for LoadGameCode to load.
    // The ShowCodePopupFlag input parameter is a boolean value. If TRUE, then the function will present the player with a popup window containing their encoded save code. If FALSE, the function will instead RETURN the SaveString (so if X=SaveGameCode(False), then X will equal the generated SaveString).
    // Will not save non-string/non-object lists/dictionaries (with grid_coordinates as an exception), will not save script attributes, will not save script dictionaries, will not save delegate attributes, will not save command patterns, will not save the "look" attribute (as it should not change), and will not save the "description" attribute (as it should not change).
    // Will not grab any other attributes attached to the "game" object except for game.gameid, game.version, game.pov, and game.timeelapsed. IF YOU WOULD LIKE TO SAVE ANY ADDITIONAL ATTRIBUTES ATTACHED TO "game", you will need to add the attribute names to a game.SaveAtts STRINGLIST attribute OR EDIT THIS FUNCTION TO CALL THEM OUT SPECIFICALLY. If you'd like to go the latter route I've noted the section below where I would recommend adding custom "game" attributes with a ***
    // Will not save timer status UNLESS the names of the timers are added to a game.SaveTimers STRINGLIST attribute OR YOU EDIT THIS FUNCTION DIRECTLY! If you want to go the latter route, I would recommend adding these to the section below marked with ***
    SuppressTurnscripts
    // Make sure ShowCodePopupFlag is of type 'boolean'
    if (not TypeOf(ShowCodePopupFlag)="boolean") {
      X = TypeOf(ShowCodePopupFlag)
      error ("ERROR: SaveGameCode function expected input 'ShowCodePopupFlag' to be of type 'boolean', but instead recieved an input of type '"+X+"'!")
    }
    SaveString = ""
    CreatedObj = NewStringList()
    DestroyedObj = NewStringList()
    // Set delimiters.
    // WARNING: D1 or D2 CANNOT be present in any object names, attribute names, or attribute values that you intend to save. Otherwise, the save will not load properly.
    // WARNING: D3 CANNOT be present in any object names or attribute names, but CAN be present in an attribute's value. This is because D3 MUST be the delimiter used to separate List entries in order to load lists properly
    // D1 delimiter will separate full object attributes, D2 delimiter will separate the data that comprises an attribute.
    // D3 is not set by this function, but instead is what the LoadGame() function will assume separates all list entries. As a reminder, by-default Quest will use ; as the List delimiter.
    D1 = "|"
    D2 = "$"
    D3 = ";"
    D4 = "@"
    // Save the player's current map before saving
    // Make sure first two entries are gameid and version (for load function)
    SaveString = SaveString+"game"+D2+"gameid"+D2+"string"+D2+game.gameid+D1
    SaveString = SaveString+"game"+D2+"version"+D2+"string"+D2+game.version+D1
    // Grab current active player (game.pov). This way the LoadGame knows who the player object is and to update its parent last
    SaveString = SaveString+"game"+D2+"pov"+D2+"object"+D2+game.pov+D1
    // Record all changable object attributes
    foreach (o, AllObjects()) {
      objectname = o.name
      // Check to see if object was created by player mid-game by comparing to the objectlist at start of game
      if (not ListContains(game.StartingObjStrList, objectname)) {
        // Then object was created by player. Double-check that it isn't already in CreatedObj list. If not, add it.
        if (not ListContains(CreatedObj, objectname)) {
          list add (CreatedObj, objectname)
        }
        // If the object was created mid-game, then we might want to capture additional inherited type info to help when it gets recreated on load...
        IncludeTypeFlag = True
      }
      else {
        IncludeTypeFlag = False
      }
      foreach (attributename, GetAttributeNames(o,IncludeTypeFlag)) {
        fullname = objectname+"."+attributename
        att_datatype = ToString(TypeOf(o, attributename))
        if (not att_datatype="script" and not att_datatype="scriptdictionary" and not att_datatype="dictionary" and not att_datatype="list" and not att_datatype="delegate"and not att_datatype="command pattern" and not attributename="look" and not attributename="description") {
          if (att_datatype="object") {
            v = GetAttribute (o, attributename)
            att_value = v.name
          }
          else if (att_datatype="stringlist" or att_datatype="objectlist") {
            X = ToString(GetAttribute (o, attributename))
            // Cut off the "List: " string that preceeds its values when you use the ToString() command
            att_value = Right(X,LengthOf(X)-LengthOf("List: "))
            // Confirm there are no banned delimiters in the list entries
            v = GetAttribute (o, attributename)
            if (ListCount(v)>0) {
              if (att_datatype="stringlist") {
                foreach (listcheck, v) {
                  // Check if there are delimiters in the names of the list entries. If so, warn the player that their save won't work.
                  if (Instr(listcheck,D1)>0 or Instr(listcheck,D2)>0 or Instr(listcheck,D3)>0 or Instr(listcheck,D4)>0) {
                    error ("ERROR: Banned delimiter detected in \""+fullname+"\" list entry '"+listcheck+"'! Consider editting SaveGameCode function to change delimiters, or renaming list entry. Current banned list entry delimiters: "+D1+" "+D2+" "+D3+" "+D4)
                  }
                }
              }
            }
          }
          else if (att_datatype="stringdictionary" or att_datatype="objectdictionary") {
            X = ToString(GetAttribute (o, attributename))
            // Cut off the "Dictionary: " string that preceeds its values when you use the ToString() command
            att_value = Right(X,LengthOf(X)-LengthOf("Dictionary: "))
            // Confirm there are no banned delimiters in the dictionary entries
            v = GetAttribute (o, attributename)
            if (DictionaryCount(v)>0) {
              foreach (dictkey, v) {
                if (Instr(dictkey,D1)>0 or Instr(dictkey,D2)>0 or Instr(dictkey,D3)>0 or Instr(dictkey,D4)>0) {
                  error ("ERROR: Banned delimiter detected in \""+fullname+"\" dictionary key '"+dictkey+"'! Consider editting SaveGameCode function to change delimiters, or renaming dictionary key. Current banned dictionary key delimiters: "+D1+" "+D2+" "+D3+" "+D4)
                }
                if (att_datatype="stringdictionary") {
                  dictitm = DictionaryItem (v, dictkey)
                  // Check if there are delimiters in the names of the list entries. If so, warn the player that their save won't work.
                  if (Instr(dictitm,D1)>0 or Instr(dictitm,D2)>0 or Instr(dictitm,D3)>0 or Instr(dictitm,D4)>0) {
                    error ("ERROR: Banned delimiter detected in \""+fullname+"\" dictionary key.value '"+dictkey+"."+dictitm+"'! Consider editting SaveGameCode function to change delimiters, or renaming dictionary value. Current banned dictionary entry delimiters: "+D1+" "+D2+" "+D3+" "+D4)
                  }
                }
              }
            }
          }
          else {
            att_value = ToString(GetAttribute (o, attributename))
          }
          // Check if there are delimiters in any of the names/values. If so, warn the player that their save won't work.
          if (Instr(objectname,D1)>0 or Instr(objectname,D2)>0 or Instr(objectname,D3)>0 or Instr(objectname,D4)>0) {
            error ("ERROR: Banned delimiter detected in \""+fullname+"\" object name! Consider editting SaveGameCode function to change delimiters, or renaming object.Current banned objectname delimiters: "+D1+" "+D2+" "+D3+" "+D4)
          }
          else if (Instr(attributename,D1)>0 or Instr(attributename,D2)>0 or Instr(attributename,D3)>0 or Instr(objectname,D4)>0) {
            error ("ERROR: Banned delimiter detected in \""+fullname+"\" attribute name! Consider editting SaveGameCode function to change delimiters, or renaming attribute. Current banned attributename delimiters: "+D1+" "+D2+" "+D3+" "+D4)
          }
          else if (Instr(att_value,D1)>0 or Instr(att_value,D2)>0 or Instr(objectname,D4)>0) {
            error ("ERROR: Banned delimiter detected in \""+fullname+"\" attribute value! Consider editting SaveGameCode function to change delimiters, or changing attribute value. Current banned attribute value delimiters: "+D1+" "+D2+" "+D4)
          }
          SaveString = SaveString+objectname+D2+attributename+D2+att_datatype+D2+att_value+D1
        }
      }
    }
    foreach (o, AllExits()) {
      objectname = o.name
      // Check to see if exit was created by player mid-game by comparing to the objectlist at start of game
      if (not ListContains(game.StartingObjStrList, objectname)) {
        // Then object was created by player. Double-check that it isn't already in CreatedObj list. If not, add it.
        if (not ListContains(CreatedObj, objectname)) {
          list add (CreatedObj, objectname)
        }
        // If the object was created mid-game, then we might want to capture additional inherited type info to help when it gets recreated on load...
        IncludeTypeFlag = True
      }
      else {
        IncludeTypeFlag = False
      }
      foreach (attributename, GetAttributeNames(o,IncludeTypeFlag)) {
        fullname = objectname+"."+attributename
        att_datatype = ToString(TypeOf(o, attributename))
        if (not att_datatype="script" and not att_datatype="scriptdictionary" and not att_datatype="dictionary" and not att_datatype="list" and not att_datatype="delegate" and not att_datatype="command pattern" and not attributename="look" and not attributename="description") {
          if (att_datatype="object") {
            v = GetAttribute (o, attributename)
            att_value = v.name
          }
          else if (att_datatype="stringlist" or att_datatype="objectlist") {
            X = ToString(GetAttribute (o, attributename))
            // Cut off the "List: " string that preceeds its values when you use the ToString() command
            att_value = Right(X,LengthOf(X)-LengthOf("List: "))
          }
          else if (att_datatype="stringdictionary" or att_datatype="objectdictionary") {
            X = ToString(GetAttribute (o, attributename))
            // Cut off the "Dictionary: " string that preceeds its values when you use the ToString() command
            att_value = Right(X,LengthOf(X)-LengthOf("Dictionary: "))
          }
          else {
            att_value = ToString(GetAttribute (o, attributename))
          }
          // Check if there are delimiters in any of the names/values. If so, warn the player that their save won't work.
          if (Instr(objectname,D1)>0 or Instr(objectname,D2)>0 or Instr(objectname,D3)>0 or Instr(objectname,D4)>0) {
            error ("ERROR: Banned delimiter detected in \""+fullname+"\" object name! Consider editting SaveGameCode function to change delimiters, or renaming object. Current banned objectname delimiters: "+D1+" "+D2+" "+D3+" "+D4)
          }
          else if (Instr(attributename,D1)>0 or Instr(attributename,D2)>0 or Instr(attributename,D3)>0 or Instr(objectname,D4)>0) {
            error ("ERROR: Banned delimiter detected in \""+fullname+"\" attribute name! Consider editting SaveGameCode function to change delimiters, or renaming attribute. Current banned attributename delimiters: "+D1+" "+D2+" "+D3+" "+D4)
          }
          else if (Instr(att_value,D1)>0 or Instr(att_value,D2)>0 or Instr(objectname,D4)>0) {
            error ("ERROR: Banned delimiter detected in \""+fullname+"\" attribute value! Consider editting SaveGameCode function to change delimiters, or changing attribute value. Current banned attribute value delimiters: "+D1+" "+D2+" "+D4)
          }
          SaveString = SaveString+objectname+D2+attributename+D2+att_datatype+D2+att_value+D1
        }
      }
    }
    foreach (turnscript, AllTurnScripts()) {
      // Check for which turnscripts are enabled/disabled
      if (GetBoolean(turnscript, "enabled")) {
        SaveString = SaveString+turnscript.name+D2+"enabled"+D2+"boolean"+D2+"True"+D1
      }
      else {
        SaveString = SaveString+turnscript.name+D2+"enabled"+D2+"boolean"+D2+"False"+D1
      }
    }
    // Determine if any objects were destroyed by the player since game start...
    foreach (objectname, game.StartingObjStrList) {
      IsThere = GetObject(objectname)
      if (Equal(IsThere,null)) {
        list add (DestroyedObj, objectname)
      }
    }
    // Save the game.timeelapsed attribute
    SaveString = SaveString+"game"+D2+"timeelapsed"+D2+"int"+D2+ToString(game.timeelapsed)+D1
    // Check if game.SaveAtts and/or game.SaveTimers exists.
    // game.SaveAtts is expected to be a stringlist containing a list of game attributes to save.
    if (HasAttribute (game, "SaveAtts")) {
      SaveAttType = TypeOf(game.SaveAtts)
      if (SaveAttType="stringlist") {
        if (ListCount(game.SaveAtts)>0) {
          foreach (x, game.SaveAtts) {
            AttValue = GetAttribute (game, x)
            att_datatype = TypeOf(AttValue)
            if (not Equal(AttValue,null) and not Equal(x,"checkpoints") and not att_datatype="script" and not att_datatype="scriptdictionary" and not att_datatype="dictionary" and not att_datatype="list" and not att_datatype="delegate"and not att_datatype="command pattern") {
              SaveString = SaveString+"game"+D2+x+D2+att_datatype+D2+ToString(AttValue)+D1
            }
            else if (Equal(x,"checkpoints")) {
              error ("ERROR: game.SaveAtts - Banned attribute 'checkpoints' found in game.SaveAtts. game.checkpoints cannot be saved using game.SaveAtts!")
            }
            else if (Equal(AttValue,null)) {
              error ("ERROR: game.SaveAtts - Attribute entry '"+x+"' not found attached to game object!")
            }
            else {
              error ("ERROR: game.SaveAtts - Attribute entry '"+x+"' not allowed. SaveGameCode cannot save attributes of type: "+att_datatype)
            }
          }
        }
      }
      else {
        error ("ERROR: game.SaveAtts expected to be a stringlist containing a list of game attributes to save. Instead, game.SaveAtts found is of datatype: "+SaveAttType)
      }
    }
    // game.SaveTimers is expected to be an stringlist containing a list of the names of all timers in the game that the author wants to save (ideally, all timers in the game).
    if (HasAttribute (game, "SaveTimers")) {
      SaveAttType = TypeOf(game.SaveTimers)
      if (SaveAttType="stringlist") {
        if (ListCount(game.SaveTimers)>0) {
          foreach (x, game.SaveTimers) {
            T = GetObject(x)
            if (not Equal(T,null)) {
              TimerName = x.name
              TimerValue1 = x.trigger
              TimerValue2 = x.interval
              TimerValue3 = x.enabled
              TimerValue1Type = TypeOf(TimerValue1)
              TimerValue2Type = TypeOf(TimerValue2)
              TimerValue3Type = TypeOf(TimerValue3)
              SaveString = SaveString+TimerName+D2+"trigger"+D2+TimerValue1Type+D2+ToString(TimerValue1)+D1
              SaveString = SaveString+TimerName+D2+"interval"+D2+TimerValue2Type+D2+ToString(TimerValue2)+D1
              SaveString = SaveString+TimerName+D2+"enabled"+D2+TimerValue3Type+D2+ToString(TimerValue3)+D1
            }
            else {
              error ("ERROR: game.SaveTimers - Timer named '"+x+"' not found!")
            }
          }
        }
      }
      else {
        error ("ERROR: game.SaveTimers expected to be a stringlist containing a list of the names of timers. Instead, game.SaveTimers found is of datatype: "+SaveAttType)
      }
    }
    // If neither of those attributes exist, then the developer can also add their own custom attributes to save using the template below...
    // *** TO DEVELOPER: Recommend putting timer status and other "game" attributes that can change based on user action during the game in this function below:
    // The template to save additional attributes is SaveString=SaveString+{string objectname}+D2+{string attributename}+D2+{datatype string}+D2+ToString({attribute value})+D1
    // For example, to save the "timer.enabled" attribute for a timer named BeeTimer: SaveString=SaveString+"BeeTimer"+D2+"enabled"+D2+"boolean"+D2+ToString(BeeTimer.enabled)+D1
    // ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
    // DO NOT APPEND THE SAVESTRING WITH ANY ADDITIONAL ATTRIBUTES BELOW THIS POINT. The game.pov.grid_coordinates, Created/Destroyed objectlist, and delimiters MUST be added on last in order for the LoadGame() Function to load properly.
    // Save the player.grid_coordinates so the player's map is saved. Because it is a dictionary of dictionaries, it must be saved in a special way...
    // grid_coordinates will be saved in form: "StuffBefore|@ObjectOwner$MapAttributeName&%&Key1$Lkey1 = Lvalue1:type;Lkey2 = Lvalue2:type;|Key2$Lkey1 = Lvalue1:type;Lkey2 = Lvalue2:type;Lkey3 = Lvalue3:type|"
    foreach (o, AllObjects()) {
      foreach (attributename, GetAttributeNames(o,false)) {
        objectname = o.name
        fullname = objectname+"."+attributename
        att_datatype = ToString(TypeOf(o, attributename))
        if (att_datatype="dictionary" and StartsWith(attributename,"saved_map_for_")) {
          // ASSUMES THAT ANY SAVED MAP DATA (for teleporting and keeping your map) STARTS WITH 'saved_map_for'. This follows the naming convention recommended by https://docs.textadventures.co.uk/quest/showing_a_map.html
          SaveString = SaveString + D4 + objectname + D2 + attributename + "&%&"
          foreach (UKey, GetAttribute(o, attributename)) {
            SaveString = SaveString+UKey+D2
            UVal = DictionaryItem(GetAttribute(o, attributename), UKey)
            foreach (Lkey, UVal) {
              Lval = DictionaryItem(UVal, Lkey)
              Lval = ToString(Lval)+":"+ToString(TypeOf(Lval))
              SaveString = SaveString+Lkey+" = "+ToString(Lval)+D3
            }
            SaveString = Left(SaveString,(LengthOf(SaveString)-1))+D1
          }
        }
        else if (att_datatype="dictionary" and attributename="grid_coordinates") {
          // Save the current map. Typically this is game.pov.grid_coordinates, but if player character can change, there may be multiple occurences of 'grid_coordinates'. Save them all.
          SaveString = SaveString + D4 + objectname + D2 + attributename + "&%&"
          foreach (UKey, GetAttribute(o, attributename)) {
            SaveString = SaveString+UKey+D2
            UVal = DictionaryItem(GetAttribute(o, attributename), UKey)
            foreach (Lkey, UVal) {
              Lval = DictionaryItem(UVal, Lkey)
              Lval = ToString(Lval)+":"+ToString(TypeOf(Lval))
              SaveString = SaveString+Lkey+" = "+ToString(Lval)+D3
            }
            SaveString = Left(SaveString,(LengthOf(SaveString)-1))+D1
          }
        }
      }
    }
    // Add on the list of created/destroyed objects...
    X = ToString(CreatedObj)
    // Cut off the "List: " string that preceeds its values when you use the ToString() command
    CreatedObjStr = Right(X,LengthOf(X)-LengthOf("List: "))
    X = ToString(DestroyedObj)
    // Cut off the "List: " string that preceeds its values when you use the ToString() command
    DestroyedObjStr = Right(X,LengthOf(X)-LengthOf("List: "))
    SaveString = SaveString+D4+CreatedObjStr+D1+DestroyedObjStr
    // Special logic needed in order to save the game.checkpoints attribute
    D4toD1 = D4+D3+D2+D1
    if (HasAttribute(game,"checkpoints")) {
      if (DictionaryCount(game.checkpoints)>0) {
        KeyList = ""
        CheckValList = ""
        foreach (k, game.checkpoints) {
          KeyList = KeyList+k+D3
          val = DictionaryItem(game.checkpoints, k)
          CheckValList = CheckValList+val+D4toD1
        }
        // Remove final D3 from KeyList string and final D4toD1 from CheckValList
        KeyList = Left(KeyList, LengthOf(KeyList)-LengthOf(D3))
        CheckValList = Left(CheckValList, LengthOf(CheckValList)-LengthOf(D4toD1))
        // Add game.checkpoints data to SaveString
        SaveString = SaveString+D4toD1+KeyList+D4toD1+CheckValList
      }
      else {
        // If game.checkpoints empty, just add D4toD1 to SaveString.
        SaveString = SaveString+D4toD1
      }
    }
    else {
      // If game.checkpoints non-existant, just add D4toD1 to SaveString.
      SaveString = SaveString+D4toD1
    }
    // Append the end of the SaveString with the delimiters used, so LoadGame() knows what delimiter maps to what...
    SaveString = SaveString+D1+D2+D3+D4
    // msg for Debugging:
    // msg (SaveString+"<br><br>")
    if (ShowCodePopupFlag=True) {
      // Create save code and present to player in textbox
      JS.CreateSaveCode (SaveString)
      JS.setCss ("#msgbox", "word-wrap:break-word;max-height:250px;")
    }
    return (SaveString)
  ]]></function>
  <function name="SaveCheckpoint" parameters="CheckpointName" type="string"><![CDATA[
    // SaveCheckpoint(CheckPointName) Function to locally save checkpoints to the game.checkpoints parameter. Functionally works just like SaveGameCode (minus saving game.checkpoints), except it does not convert the SaveString to base64 or present the SaveCode to the player, instead storing it in the game.checkpoints stringdictionary.
    // The CheckpointName input parameter is a string value that will become the Key in the game.checkpoints string dictionary for the generated checkpoint SaveString value. If the CheckpointName already exists in game.checkpoints, then this function will overwrite it, allowing checkpoint names to be re-used multiple times.
    // If CheckpointName="", then the SaveString will simply be returned as an output, rather than saved to game.checkpoints.
    // Will not save non-string/non-object lists/dictionaries (with grid_coordinates as an exception), will not save script attributes, will not save script dictionaries, will not save delegate attributes, will not save command patterns, will not save the "look" attribute (as it should not change), and will not save the "description" attribute (as it should not change).
    // Will not grab any other attributes attached to the "game" object except for game.gameid, game.version, game.pov, and game.timeelapsed. IF YOU WOULD LIKE TO SAVE ANY ADDITIONAL ATTRIBUTES ATTACHED TO "game", you will need to add the attribute names to a game.SaveAtts STRINGLIST attribute OR EDIT THIS FUNCTION TO CALL THEM OUT SPECIFICALLY. If you'd like to go the latter route I've noted the section below where I would recommend adding custom "game" attributes with a ***
    // Will not save timer status UNLESS the names of the timers are added to a game.SaveTimers STRINGLIST attribute OR YOU EDIT THIS FUNCTION DIRECTLY! If you want to go the latter route, I would recommend adding these to the section below marked with ***
    SuppressTurnscripts
    // Check if game.checkpoints exists. If not, create it.
    if (not HasAttribute(game, "checkpoints")) {
      set (game, "checkpoints", NewStringDictionary())
    }
    // Make sure CheckpointName input is of 'string' datatype
    if (not TypeOf(CheckpointName)="string") {
      X = TypeOf(CheckpointName)
      error ("ERROR: SaveCheckpoint function expected input 'CheckpointName' to be of type 'string', but instead recieved an input of type '"+X+"'!")
    }
    SaveString = ""
    CreatedObj = NewStringList()
    DestroyedObj = NewStringList()
    // Set delimiters.
    // WARNING: D1 or D2 CANNOT be present in any object names, attribute names, or attribute values that you intend to save. Otherwise, the save will not load properly.
    // WARNING: D3 CANNOT be present in any object names or attribute names, but CAN be present in an attribute's value. This is because D3 MUST be the delimiter used to separate List entries in order to load lists properly
    // D1 delimiter will separate full object attributes, D2 delimiter will separate the data that comprises an attribute.
    // D3 is not set by this function, but instead is what the LoadGame() function will assume separates all list entries. As a reminder, by-default Quest will use ; as the List delimiter.
    D1 = "|"
    D2 = "$"
    D3 = ";"
    D4 = "@"
    // Save the player's current map before saving
    // Make sure first two entries are gameid and version (for load function)
    SaveString = SaveString+"game"+D2+"gameid"+D2+"string"+D2+game.gameid+D1
    SaveString = SaveString+"game"+D2+"version"+D2+"string"+D2+game.version+D1
    // Grab current active player (game.pov). This way the LoadGame knows who the player object is and to update its parent last
    SaveString = SaveString+"game"+D2+"pov"+D2+"object"+D2+game.pov+D1
    // Record all changable object attributes
    foreach (o, AllObjects()) {
      objectname = o.name
      // Check to see if object was created by player mid-game by comparing to the objectlist at start of game
      if (not ListContains(game.StartingObjStrList, objectname)) {
        // Then object was created by player. Double-check that it isn't already in CreatedObj list. If not, add it.
        if (not ListContains(CreatedObj, objectname)) {
          list add (CreatedObj, objectname)
        }
        // If the object was created mid-game, then we might want to capture additional inherited type info to help when it gets recreated on load...
        IncludeTypeFlag = True
      }
      else {
        IncludeTypeFlag = False
      }
      foreach (attributename, GetAttributeNames(o,IncludeTypeFlag)) {
        fullname = objectname+"."+attributename
        att_datatype = ToString(TypeOf(o, attributename))
        if (not att_datatype="script" and not att_datatype="scriptdictionary" and not att_datatype="dictionary" and not att_datatype="list" and not att_datatype="delegate"and not att_datatype="command pattern" and not attributename="look" and not attributename="description") {
          if (att_datatype="object") {
            v = GetAttribute (o, attributename)
            att_value = v.name
          }
          else if (att_datatype="stringlist" or att_datatype="objectlist") {
            X = ToString(GetAttribute (o, attributename))
            // Cut off the "List: " string that preceeds its values when you use the ToString() command
            att_value = Right(X,LengthOf(X)-LengthOf("List: "))
            // Confirm there are no banned delimiters in the list entries
            v = GetAttribute (o, attributename)
            if (ListCount(v)>0) {
              if (att_datatype="stringlist") {
                foreach (listcheck, v) {
                  // Check if there are delimiters in the names of the list entries. If so, warn the player that their save won't work.
                  if (Instr(listcheck,D1)>0 or Instr(listcheck,D2)>0 or Instr(listcheck,D3)>0 or Instr(listcheck,D4)>0) {
                    error ("ERROR: Banned delimiter detected in \""+fullname+"\" list entry '"+listcheck+"'! Consider editting SaveGameCode function to change delimiters, or renaming list entry. Current banned list entry delimiters: "+D1+" "+D2+" "+D3+" "+D4)
                  }
                }
              }
            }
          }
          else if (att_datatype="stringdictionary" or att_datatype="objectdictionary") {
            X = ToString(GetAttribute (o, attributename))
            // Cut off the "Dictionary: " string that preceeds its values when you use the ToString() command
            att_value = Right(X,LengthOf(X)-LengthOf("Dictionary: "))
            // Confirm there are no banned delimiters in the dictionary entries
            v = GetAttribute (o, attributename)
            if (DictionaryCount(v)>0) {
              foreach (dictkey, v) {
                if (Instr(dictkey,D1)>0 or Instr(dictkey,D2)>0 or Instr(dictkey,D3)>0 or Instr(dictkey,D4)>0) {
                  error ("ERROR: Banned delimiter detected in \""+fullname+"\" dictionary key '"+dictkey+"'! Consider editting SaveGameCode function to change delimiters, or renaming dictionary key. Current banned dictionary key delimiters: "+D1+" "+D2+" "+D3+" "+D4)
                }
                if (att_datatype="stringdictionary") {
                  dictitm = DictionaryItem (v, dictkey)
                  // Check if there are delimiters in the names of the list entries. If so, warn the player that their save won't work.
                  if (Instr(dictitm,D1)>0 or Instr(dictitm,D2)>0 or Instr(dictitm,D3)>0 or Instr(dictitm,D4)>0) {
                    error ("ERROR: Banned delimiter detected in \""+fullname+"\" dictionary key.value '"+dictkey+"."+dictitm+"'! Consider editting SaveGameCode function to change delimiters, or renaming dictionary value. Current banned dictionary entry delimiters: "+D1+" "+D2+" "+D3+" "+D4)
                  }
                }
              }
            }
          }
          else {
            att_value = ToString(GetAttribute (o, attributename))
          }
          // Check if there are delimiters in any of the names/values. If so, warn the player that their save won't work.
          if (Instr(objectname,D1)>0 or Instr(objectname,D2)>0 or Instr(objectname,D3)>0 or Instr(objectname,D4)>0) {
            error ("ERROR: Banned delimiter detected in \""+fullname+"\" object name! Consider editting SaveGameCode function to change delimiters, or renaming object.Current banned objectname delimiters: "+D1+" "+D2+" "+D3+" "+D4)
          }
          else if (Instr(attributename,D1)>0 or Instr(attributename,D2)>0 or Instr(attributename,D3)>0 or Instr(objectname,D4)>0) {
            error ("ERROR: Banned delimiter detected in \""+fullname+"\" attribute name! Consider editting SaveGameCode function to change delimiters, or renaming attribute. Current banned attributename delimiters: "+D1+" "+D2+" "+D3+" "+D4)
          }
          else if (Instr(att_value,D1)>0 or Instr(att_value,D2)>0 or Instr(objectname,D4)>0) {
            error ("ERROR: Banned delimiter detected in \""+fullname+"\" attribute value! Consider editting SaveGameCode function to change delimiters, or changing attribute value. Current banned attribute value delimiters: "+D1+" "+D2+" "+D4)
          }
          SaveString = SaveString+objectname+D2+attributename+D2+att_datatype+D2+att_value+D1
        }
      }
    }
    foreach (o, AllExits()) {
      objectname = o.name
      // Check to see if exit was created by player mid-game by comparing to the objectlist at start of game
      if (not ListContains(game.StartingObjStrList, objectname)) {
        // Then object was created by player. Double-check that it isn't already in CreatedObj list. If not, add it.
        if (not ListContains(CreatedObj, objectname)) {
          list add (CreatedObj, objectname)
        }
        // If the object was created mid-game, then we might want to capture additional inherited type info to help when it gets recreated on load...
        IncludeTypeFlag = True
      }
      else {
        IncludeTypeFlag = False
      }
      foreach (attributename, GetAttributeNames(o,IncludeTypeFlag)) {
        fullname = objectname+"."+attributename
        att_datatype = ToString(TypeOf(o, attributename))
        if (not att_datatype="script" and not att_datatype="scriptdictionary" and not att_datatype="dictionary" and not att_datatype="list" and not att_datatype="delegate" and not att_datatype="command pattern" and not attributename="look" and not attributename="description") {
          if (att_datatype="object") {
            v = GetAttribute (o, attributename)
            att_value = v.name
          }
          else if (att_datatype="stringlist" or att_datatype="objectlist") {
            X = ToString(GetAttribute (o, attributename))
            // Cut off the "List: " string that preceeds its values when you use the ToString() command
            att_value = Right(X,LengthOf(X)-LengthOf("List: "))
          }
          else if (att_datatype="stringdictionary" or att_datatype="objectdictionary") {
            X = ToString(GetAttribute (o, attributename))
            // Cut off the "Dictionary: " string that preceeds its values when you use the ToString() command
            att_value = Right(X,LengthOf(X)-LengthOf("Dictionary: "))
          }
          else {
            att_value = ToString(GetAttribute (o, attributename))
          }
          // Check if there are delimiters in any of the names/values. If so, warn the player that their save won't work.
          if (Instr(objectname,D1)>0 or Instr(objectname,D2)>0 or Instr(objectname,D3)>0 or Instr(objectname,D4)>0) {
            error ("ERROR: Banned delimiter detected in \""+fullname+"\" object name! Consider editting SaveGameCode function to change delimiters, or renaming object. Current banned objectname delimiters: "+D1+" "+D2+" "+D3+" "+D4)
          }
          else if (Instr(attributename,D1)>0 or Instr(attributename,D2)>0 or Instr(attributename,D3)>0 or Instr(objectname,D4)>0) {
            error ("ERROR: Banned delimiter detected in \""+fullname+"\" attribute name! Consider editting SaveGameCode function to change delimiters, or renaming attribute. Current banned attributename delimiters: "+D1+" "+D2+" "+D3+" "+D4)
          }
          else if (Instr(att_value,D1)>0 or Instr(att_value,D2)>0 or Instr(objectname,D4)>0) {
            error ("ERROR: Banned delimiter detected in \""+fullname+"\" attribute value! Consider editting SaveGameCode function to change delimiters, or changing attribute value. Current banned attribute value delimiters: "+D1+" "+D2+" "+D4)
          }
          SaveString = SaveString+objectname+D2+attributename+D2+att_datatype+D2+att_value+D1
        }
      }
    }
    foreach (turnscript, AllTurnScripts()) {
      // Check for which turnscripts are enabled/disabled
      if (GetBoolean(turnscript, "enabled")) {
        SaveString = SaveString+turnscript.name+D2+"enabled"+D2+"boolean"+D2+"True"+D1
      }
      else {
        SaveString = SaveString+turnscript.name+D2+"enabled"+D2+"boolean"+D2+"False"+D1
      }
    }
    // Determine if any objects were destroyed by the player since game start...
    foreach (objectname, game.StartingObjStrList) {
      IsThere = GetObject(objectname)
      if (Equal(IsThere,null)) {
        list add (DestroyedObj, objectname)
      }
    }
    // Save the game.timeelapsed attribute
    SaveString = SaveString+"game"+D2+"timeelapsed"+D2+"int"+D2+ToString(game.timeelapsed)+D1
    // Check if game.SaveAtts and/or game.SaveTimers exists.
    // game.SaveAtts is expected to be a stringlist containing a list of game attributes to save.
    if (HasAttribute (game, "SaveAtts")) {
      SaveAttType = TypeOf(game.SaveAtts)
      if (SaveAttType="stringlist") {
        if (ListCount(game.SaveAtts)>0) {
          foreach (x, game.SaveAtts) {
            AttValue = GetAttribute (game, x)
            att_datatype = TypeOf(AttValue)
            if (not Equal(AttValue,null) and not Equal(x,"checkpoints") and not att_datatype="script" and not att_datatype="scriptdictionary" and not att_datatype="dictionary" and not att_datatype="list" and not att_datatype="delegate"and not att_datatype="command pattern") {
              SaveString = SaveString+"game"+D2+x+D2+att_datatype+D2+ToString(AttValue)+D1
            }
            else if (Equal(x,"checkpoints")) {
              error ("ERROR: game.SaveAtts - Banned attribute 'checkpoints' found in game.SaveAtts. game.checkpoints cannot be saved using game.SaveAtts!")
            }
            else if (Equal(AttValue,null)) {
              error ("ERROR: game.SaveAtts - Attribute entry '"+x+"' not found attached to game object!")
            }
            else {
              error ("ERROR: game.SaveAtts - Attribute entry '"+x+"' not allowed. SaveGameCode cannot save attributes of type: "+att_datatype)
            }
          }
        }
      }
      else {
        error ("ERROR: game.SaveAtts expected to be a stringlist containing a list of game attributes to save. Instead, game.SaveAtts found is of datatype: "+SaveAttType)
      }
    }
    // game.SaveTimers is expected to be an stringlist containing a list of the names of all timers in the game that the author wants to save (ideally, all timers in the game).
    if (HasAttribute (game, "SaveTimers")) {
      SaveAttType = TypeOf(game.SaveTimers)
      if (SaveAttType="stringlist") {
        if (ListCount(game.SaveTimers)>0) {
          foreach (x, game.SaveTimers) {
            T = GetObject(x)
            if (not Equal(T,null)) {
              TimerName = x.name
              TimerValue1 = x.trigger
              TimerValue2 = x.interval
              TimerValue3 = x.enabled
              TimerValue1Type = TypeOf(TimerValue1)
              TimerValue2Type = TypeOf(TimerValue2)
              TimerValue3Type = TypeOf(TimerValue3)
              SaveString = SaveString+TimerName+D2+"trigger"+D2+TimerValue1Type+D2+ToString(TimerValue1)+D1
              SaveString = SaveString+TimerName+D2+"interval"+D2+TimerValue2Type+D2+ToString(TimerValue2)+D1
              SaveString = SaveString+TimerName+D2+"enabled"+D2+TimerValue3Type+D2+ToString(TimerValue3)+D1
            }
            else {
              error ("ERROR: game.SaveTimers - Timer named '"+x+"' not found!")
            }
          }
        }
      }
      else {
        error ("ERROR: game.SaveTimers expected to be a stringlist containing a list of the names of timers. Instead, game.SaveTimers found is of datatype: "+SaveAttType)
      }
    }
    // If neither of those attributes exist, then the developer can also add their own custom attributes to save using the template below...
    // *** TO DEVELOPER: Recommend putting timer status and other "game" attributes that can change based on user action during the game in this function below:
    // The template to save additional attributes is SaveString=SaveString+{string objectname}+D2+{string attributename}+D2+{datatype string}+D2+ToString({attribute value})+D1
    // For example, to save the "timer.enabled" attribute for a timer named BeeTimer: SaveString=SaveString+"BeeTimer"+D2+"enabled"+D2+"boolean"+D2+ToString(BeeTimer.enabled)+D1
    // ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
    // DO NOT APPEND THE SAVESTRING WITH ANY ADDITIONAL ATTRIBUTES BELOW THIS POINT. The game.pov.grid_coordinates, Created/Destroyed objectlist, and delimiters MUST be added on last in order for the LoadGame() Function to load properly.
    // Save the player.grid_coordinates so the player's map is saved. Because it is a dictionary of dictionaries, it must be saved in a special way...
    // grid_coordinates will be saved in form: "StuffBefore|@ObjectOwner$MapAttributeName&%&Key1$Lkey1 = Lvalue1:type;Lkey2 = Lvalue2:type;|Key2$Lkey1 = Lvalue1:type;Lkey2 = Lvalue2:type;Lkey3 = Lvalue3:type|"
    foreach (o, AllObjects()) {
      foreach (attributename, GetAttributeNames(o,false)) {
        objectname = o.name
        fullname = objectname+"."+attributename
        att_datatype = ToString(TypeOf(o, attributename))
        if (att_datatype="dictionary" and StartsWith(attributename,"saved_map_for_")) {
          // ASSUMES THAT ANY SAVED MAP DATA (for teleporting and keeping your map) STARTS WITH 'saved_map_for'. This follows the naming convention recommended by https://docs.textadventures.co.uk/quest/showing_a_map.html
          SaveString = SaveString + D4 + objectname + D2 + attributename + "&%&"
          foreach (UKey, GetAttribute(o, attributename)) {
            SaveString = SaveString+UKey+D2
            UVal = DictionaryItem(GetAttribute(o, attributename), UKey)
            foreach (Lkey, UVal) {
              Lval = DictionaryItem(UVal, Lkey)
              Lval = ToString(Lval)+":"+ToString(TypeOf(Lval))
              SaveString = SaveString+Lkey+" = "+ToString(Lval)+D3
            }
            SaveString = Left(SaveString,(LengthOf(SaveString)-1))+D1
          }
        }
        else if (att_datatype="dictionary" and attributename="grid_coordinates") {
          // Save the current map. Typically this is game.pov.grid_coordinates, but if player character can change, there may be multiple occurences of 'grid_coordinates'. Save them all.
          SaveString = SaveString + D4 + objectname + D2 + attributename + "&%&"
          foreach (UKey, GetAttribute(o, attributename)) {
            SaveString = SaveString+UKey+D2
            UVal = DictionaryItem(GetAttribute(o, attributename), UKey)
            foreach (Lkey, UVal) {
              Lval = DictionaryItem(UVal, Lkey)
              Lval = ToString(Lval)+":"+ToString(TypeOf(Lval))
              SaveString = SaveString+Lkey+" = "+ToString(Lval)+D3
            }
            SaveString = Left(SaveString,(LengthOf(SaveString)-1))+D1
          }
        }
      }
    }
    // Add on the list of created/destroyed objects...
    X = ToString(CreatedObj)
    // Cut off the "List: " string that preceeds its values when you use the ToString() command
    CreatedObjStr = Right(X,LengthOf(X)-LengthOf("List: "))
    X = ToString(DestroyedObj)
    // Cut off the "List: " string that preceeds its values when you use the ToString() command
    DestroyedObjStr = Right(X,LengthOf(X)-LengthOf("List: "))
    SaveString = SaveString+D4+CreatedObjStr+D1+DestroyedObjStr
    // Append the end of the SaveString with the delimiters used, so LoadGame() knows what delimiter maps to what...
    SaveString = SaveString+D1+D2+D3+D4
    // msg for Debugging:
    // msg (SaveString+"<br><br>")
    // Save SaveString to game.checkpoints if CheckpointName not empty. Else if CheckpointName="", then simply return the SaveString.
    if (not CheckpointName="") {
      DictionaryAdd (game.checkpoints, CheckpointName, SaveString)
    }
    return (SaveString)
  ]]></function>
  <function name="LoadCheckpoint" parameters="CheckpointName"><![CDATA[
    // LoadCheckpoint(CheckpointName) function to load a SaveCheckpoint checkpoint. Works similarly to LoadGameCode except it does not print any messages to the player. Also will not prompt the player if a save is found to be from an older version, it will just load according to the OldestAllowedVersion variable.
    // Input CheckpointName is the name of a checkpoint saved in the game.checkpoints stringdictionary attribute that you would like to load.
    SuppressTurnscripts
    // TO DEVELOPER: Set the OldestAllowedVersion to the oldest compatible game version that a player can load saved game data from. Setting OldestAllowedVersion=0 will essentially allow the player to load saves from any old version. Setting OldestAllowedVersion=game.version will make it so the player can ONLY load saves from the current game version.
    OldestAllowedVersion = 2.0
    // TO DEVELOPER: Setting DebugMode to 'True' will enable the printing of debug messages to the screen when running. Very useful for testing out the function if you've made any custom edits for compatibility or the like.
    DebugMode = False
    // Msg for Debugging:
    if (DebugMode) {
      msg ("<br>Full decoded SaveCode:<br>"+SaveGameCodeDecoded)
    }
    // Make sure CheckpointName input is of type 'string'
    if (not TypeOf(CheckpointName)="string") {
      X = TypeOf(CheckpointName)
      error ("ERROR: LoadCheckpoint function expected input 'CheckpointName' to be of type 'string', but instead recieved an input of type '"+X+"'!")
    }
    // Retrieve SaveString from game.checkpoints dictionary
    if (HasAttribute(game, "checkpoints")) {
      if (DictionaryContains(game.checkpoints, CheckpointName)) {
        SaveGameCodeDecoded = DictionaryItem(game.checkpoints, CheckpointName)
      }
      else {
        error ("ERROR: Checkpoint named '"+CheckpointName+"' not found!")
      }
    }
    else {
      error ("ERROR: Cannot load checkpoint as game.checkpoints attribute does not exist!")
    }
    // Set up other variables for later
    bla => {
    }
    upgradesave = False
    Proceed = False
    SkippedAttList = ""
    CreatedObjDebugList = ""
    DestroyedObjDebugList = ""
    // Retrieve delimiters from end of SaveGameCodeDecoded
    Dls = Right(SaveGameCodeDecoded,4)
    D1 = Mid (Dls, 1, 1)
    D2 = Mid (Dls, 2, 1)
    D3 = Mid (Dls, 3, 1)
    D4 = Mid (Dls, 4, 1)
    // Remove delimiters from end of SaveGameCode
    SaveCode = Left(SaveGameCodeDecoded, LengthOf(SaveGameCodeDecoded)-(LengthOf(Dls)))
    // Extract the Created/Destroyed object lists. The START of the created/destroyed section should be after the LAST D4 delimiter...
    CreatedDestroyedInfo = Right(SaveCode, LengthOf(SaveCode)-InstrRev(SaveCode, D4))
    if (DebugMode) {
      msg ("<br>CreatedDestroyedInfo: "+CreatedDestroyedInfo)
    }
    CDList = Split(CreatedDestroyedInfo,D1)
    CSection = ListItem (CDList, 0)
    DSection = ListItem (CDList, 1)
    if (CSection="") {
      CreatedList = NewStringList()
    }
    else {
      CreatedList = Split (CSection, D3)
      // The way the lists are saved, when you load it, a blank entry will be created. Let's remove that...
      list remove (CreatedList, " ")
    }
    if (DSection="") {
      DestroyedList = NewStringList()
    }
    else {
      DestroyedList = Split (DSection, D3)
      // The way the lists are saved, when you load it, a blank entry will be created. Let's remove that...
      list remove (DestroyedList, " ")
    }
    // Remove Created/Destroyed list from end of SaveCode, also removing the final D1 and D4 delimiter...
    SaveCode = Left(SaveCode, LengthOf(SaveCode)-(LengthOf(CreatedDestroyedInfo)+2))
    // Extract the player.grid_coordinates info separately from the rest of the savecode. It has special rules for decoding it since it is a dictionary of dictionaries.
    GridGInfo = Right(SaveCode, LengthOf(SaveCode)-Instr(SaveCode, D4))
    if (DebugMode) {
      msg ("<br>GridGInfo: "+GridGInfo)
    }
    // Remove player.grid_coordinates info from end of SaveCode also remove the final D1 & D4 delimiter separating the grid_coordinates from the rest of the attributes
    SaveCode = Left(SaveCode, LengthOf(SaveCode)-(LengthOf(GridGInfo)+2))
    if (DebugMode) {
      msg ("<br>SaveCode w/o player.grid_coordinate or create/destroy info:<br>"+SaveCode)
    }
    // Note: if the "SaveCode" begins with the word "Error:", then an error was encountered when trying to convert from Base64.
    // Extract SaveCode game info. This includes the gameid, game version, and current player (game.pov)...
    GameIdDelim = Instr (SaveCode, D1)
    GameVersionDelim = Instr(GameIdDelim+1,SaveCode,D1)
    GamePOVDelim = Instr(GameVersionDelim+1,SaveCode,D1)
    GameInfo = Split(Left(SaveCode,GamePOVDelim-1),D1)
    GameIdObjectEl = ListItem (GameInfo,0)
    GameIdElements = Split(GameIdObjectEl,D2)
    Loaded_GameId = ListItem (GameIdElements, 3)
    GameVerObjectEl = ListItem (GameInfo,1)
    GameVerElements = Split(GameVerObjectEl,D2)
    VersionString = ListItem (GameVerElements, 3)
    Loaded_GameVersion = ToDouble(VersionString)
    GamePOVObjectEl = ListItem (GameInfo,2)
    GamePOVElements = Split(GamePOVObjectEl,D2)
    GamePOVName = ListItem (GamePOVElements, 3)
    if (StartsWith(GamePOVName,"Object: ")) {
      GamePOVName = Right(GamePOVName,LengthOf(GamePOVName)-LengthOf("Object: "))
    }
    GamePOVObject = GetObject (GamePOVName)
    GamePOVParent = GetAttribute (GamePOVObject, "parent")
    // Check that the save belongs to this game by comparing gameIds
    if (not Loaded_GameId=game.gameid) {
      error ("Load Aborted: SaveCode not identified by this game. GameID mismatch.")
    }
    else {
      // Compare version of game in SaveCode to version of game loading it
      ThisGame_GameVersion = ToDouble(game.version)
      if (not TypeOf(OldestAllowedVersion)="double") {
        OldestAllowedVersion_Double = ToDouble(OldestAllowedVersion)
      }
      else {
        OldestAllowedVersion_Double = OldestAllowedVersion
      }
      // If upgrading from an old game version, then arbitrarily set Loaded_GameVersion to ThisGameVersion to proceed.
      if (Loaded_GameVersion<ThisGame_GameVersion) {
        if (OldestAllowedVersion_Double<=Loaded_GameVersion) {
          upgradesave = True
          Proceed = True
        }
        else {
          error ("ERROR: The SaveCode you are attempting to load is from an INCOMPATIBLE older game version, and thus cannot be loaded by this version of the game.<br>Saved Game: v"+ToString(Loaded_GameVersion)+"<br>This Game: v"+ToString(ThisGame_GameVersion)+"<br>Oldest Compatible Version: v"+ToString(OldestAllowedVersion)+"<br><br>Loading aborted...")
        }
      }
      else if (Loaded_GameVersion>ThisGame_GameVersion) {
        error ("ERROR: The SaveCode you are attempting to load is from a newer version of this game and is not compatible.<br>Saved Game: v"+ToString(Loaded_GameVersion)+"<br>This Game: v"+ToString(ThisGame_GameVersion)+"<br>Please try a different SaveCode or use an updated game file.<br><br>Load aborted.")
      }
      else {
        Proceed = True
      }
      if (Proceed=True) {
        // Create any objects noted in the CreatedList, if there are any, so their relevant attributes can be added without error...
        if (ListCount(CreatedList)>0) {
          foreach (o, CreatedList) {
            // Check that objects don't already exist...
            IsThere = GetObject(o)
            if (Equal(IsThere,null)) {
              // If not, create the object
              create (o)
              CreatedObjDebugList = CreatedObjDebugList+o+"<br>"
            }
          }
        }
        player.grid_coordinates = null
        // Split the save code up into all objects. Then parse through the value of each object attribute
        SavedObjectList = Split(SaveCode, D1)
        foreach (o, SavedObjectList) {
          Skip_Att = False
          objelements = Split(o, D2)
          objectname = ListItem (objelements, 0)
          object = GetObject (objectname)
          attributename = ListItem (objelements, 1)
          fullname = objectname+"."+attributename
          preload_att_value = GetAttribute (object, attributename)
          att_datatype = ListItem (objelements, 2)
          if (ListCount(objelements)=3) {
            att_value = ""
          }
          else {
            att_value = ListItem (objelements, 3)
          }
          // Check that the attribute is supported
          if (not att_datatype="string" and not att_datatype="boolean" and not att_datatype="object" and not att_datatype="int" and not att_datatype="double" and not att_datatype="stringlist" and not att_datatype="objectlist" and not att_datatype="stringdictionary" and not att_datatype="objectdictionary") {
            msg ("WARNING! Unsupported datatype \""+att_datatype+"\" detected in SaveCode attribute \""+fullname+"\"! Skipping and moving on to next attribute...")
            Skip_Att = True
            SkippedAttList = SkippedAttList+fullname+"<br>"
          }
          // Convert the string attribute value and convert it to the datatype that it actually needs to be. This att_value_obj variable will also be directly compared to preload_att_value to determine if the pre- and post- load values are equal or not...
          att_value_obj = att_value
          if (att_datatype="object") {
            if (StartsWith(att_value,"Object: ")) {
              att_value_obj = GetObject(Right(att_value,LengthOf(att_value)-LengthOf("Object: ")))
            }
            else {
              att_value_obj = GetObject(att_value)
            }
          }
          else if (att_datatype="boolean") {
            if (att_value="True") {
              att_value_obj = True
            }
            else {
              att_value_obj = False
            }
          }
          else if (att_datatype="int") {
            att_value_obj = ToInt(att_value)
          }
          else if (att_datatype="double") {
            att_value_obj = ToDouble(att_value)
          }
          else if (att_datatype="stringlist") {
            if (att_value="") {
              att_value_obj = NewStringList()
            }
            else {
              att_value_obj = Split (att_value, D3)
              // The way the lists are saved, when you load it, a blank entry will be created. Let's remove that...
              list remove (att_value_obj, " ")
            }
          }
          else if (att_datatype="objectlist") {
            if (att_value="") {
              att_value_obj = NewObjectList()
            }
            else {
              att_value_obj = NewObjectList()
              objlistlist = Split (att_value, D3)
              foreach (olt, objlistlist) {
                // Need to remove the "Object: " that will preceed each entry, and turn the string entry into the actual object before re-adding to list. We put it into the following "if" statement in order to exclude the blank list entry that gets created at the end of the list by loading
                if (StartsWith(olt,"Object: ")) {
                  value = GetObject(Right(olt,LengthOf(olt)-LengthOf("Object: ")))
                  if (not value=null) {
                    list add (att_value_obj, value)
                  }
                  else {
                    msg ("WARNING! Object \""+olt+"\" detected in saved objectlist \""+fullname+"\" does not exist! Object \""+olt+"\" not added to list! Loaded game may not work properly!")
                    SkippedAttList = SkippedAttList+"Objectlist '"+fullname+"' item: "+olt+"<br>"
                  }
                }
              }
            }
          }
          else if (att_datatype="stringdictionary") {
            if (att_value="") {
              att_value_obj = NewStringDictionary()
            }
            else {
              att_value_obj = NewStringDictionary()
              // Add dictionary values from SaveGame
              dictrows = Split(att_value, ";")
              foreach (kv, dictrows) {
                if (DebugMode) {
                  msg ("StringDict '"+fullname+"' key-value: "+ToString(kv))
                }
                KeyValList = Split(kv," = ")
                key = ListItem(KeyValList, 0)
                value = ListItem(KeyValList, 1)
                DictionaryAdd (att_value_obj, key, value)
              }
            }
          }
          else if (att_datatype="objectdictionary") {
            if (att_value="") {
              att_value_obj = NewObjectDictionary()
            }
            else {
              att_value_obj = NewObjectDictionary()
              dictrows = Split(att_value, ";")
              foreach (kv, dictrows) {
                if (DebugMode) {
                  msg ("ObjDict  '"+fullname+"' key-value: "+ToString(kv))
                }
                KeyValList = Split(kv," = ")
                key = ListItem(KeyValList, 0)
                obj = ListItem(KeyValList, 1)
                if (StartsWith(obj,"Object: ")) {
                  value = GetObject(Right(value,LengthOf(value)-LengthOf("Object: ")))
                }
                else {
                  value = obj
                }
                if (not value=null) {
                  DictionaryAdd (att_value_obj, key, value)
                }
                else {
                  msg ("WARNING! Object \""+obj+"\" detected in saved objectdictionary \""+fullname+"\" does not exist! Object \""+obj+"\" not added to dictionary! Loaded game may not work properly!")
                  SkippedAttList = SkippedAttList+"Objectdictionary '"+fullname+"' item: "+olt+"<br>"
                }
              }
            }
          }
          if (objectname=GamePOVName and attributename="parent") {
            // Check that the attribute is NOT game.pov.parent. If so, we want to make sure that gets updated last
            Skip_Att = True
            GamePOVParent = att_value_obj
          }
          // Make sure the object you are trying to add/update the attribute to exists, otherwise you'd get an error trying to update/create its attribute. If the attribute doesn't exist but the object does, then this function will create it. Also, don't update the game.version or game.pov: The game.version should not be updated from the savecode if you're loading from a previous version, and the game.pov is updated last.
          if (not Equal(object,null) and not Equal(att_value_obj,null) and not fullname="game.gameid" and not fullname="game.version" and not fullname="game.pov" and not Skip_Att=True) {
            if (Equal(preload_att_value,null)) {
              if (DebugMode) {
                msg ("<br>ATTENTION: Attribute '"+fullname+"' does NOT exist in current game, but its parent object '"+objectname+"' does, so attribute will be created!<br>")
              }
              preload_att_value = "null"
            }
            // Msgs for debugging:
            if (DebugMode) {
              msg ("objectname="+objectname)
              msg ("attributename="+attributename)
              msg ("att_datatype="+att_datatype)
              msg ("preload_att_value="+ToString(preload_att_value))
              if (Equal(preload_att_value,"null")) {
                msg ("preload_att_datatype=null")
              }
              else {
                msg ("preload_att_datatype="+TypeOf(preload_att_value))
              }
              msg ("att_value="+ToString(att_value))
              msg ("att_value_obj="+ToString(att_value_obj))
              msg ("isEqual att_value=preload_att_value?: "+ToString(Equal(att_value,preload_att_value)))
              msg ("isEqual att_value_obj=preload_att_value?: "+ToString(Equal(att_value_obj,preload_att_value)))
              msg ("isEqual ToString(att_value_obj)=ToString(preload_att_value)?: "+ToString(Equal(ToString(att_value_obj),ToString(preload_att_value))))
              msg ("<br>")
            }
            // If attributes are already equal to those in the savecode, no need to change them. Else, change 'em.
            if (not Equal(att_value,preload_att_value) and not Equal(att_value_obj,preload_att_value) and not Equal(ToString(att_value_obj),ToString(preload_att_value))) {
              if (DebugMode) {
                msg ("Updating attribute: "+fullname+"<br><br>")
              }
              // Check if attribute has an associated change script. If so, this section will make sure that setting the attribute on load WON'T activate its associate turnscript
              cha = "changed" + attributename
              if (HasAttribute (object, cha)) {
                // If the attribute DOES have an associated change script, temporarily blank it out so it does not execute during loading
                scr = GetAttribute (object, cha)
                set (object, cha, bla)
              }
              // Update the attributes in the game with those from the SaveCode...
              if (att_datatype="boolean") {
                set (object, attributename, att_value_obj)
              }
              else if (att_datatype="int") {
                set (object, attributename, att_value_obj)
              }
              else if (att_datatype="double") {
                set (object, attributename, att_value_obj)
              }
              else if (att_datatype="object") {
                set (object, attributename, att_value_obj)
              }
              else if (att_datatype="stringlist" or att_datatype="objectlist" or att_datatype="stringdictionary" or att_datatype="objectdictionary") {
                // NOTE TO DEVELOPER:
                // Alter the following logic below to fit your needs. Especially important to make sure this works properly for YOUR game for compatibility between game versions!
                // If ReplaceContents = True, then any list or dictionary in your game will be COMPLETELY REPLACED by its corresponding list/dictionary from the savecode.
                // If ReplaceContents = False, then the list/dictionary contents in the SaveCode will be ADDED to the existing corresponding list/dictionary. NOTE: When adding to an existing list/dict, the code, as-written, will REMOVE ANY DUPLICATES from the lists/dictionaries! ALSO, be careful where you allow LoadGame() to be called in cases where ReplaceContents=False, ESPECIALLY if the list/dict contents can change through the course of the game! Calling this LoadGameCode function only from a titlescreen (before any lists/dictionaries have changed), for instance, may be one possible way to account for this.
                // ReplaceContents=True by default, but this may not be desirable in all cases (i.e. if you updated the contents of a permanent dictionary/list between versions), so it is up to YOU to ensure this section behaves as you want it to. Remember that the "object" and "attributename" variables exist at this point to call out specific list/dictionary objects.
                if (upgradesave = True) {
                  // This section will trigger if the player is loading a save from a previous game version
                  ReplaceContents = True
                }
                else {
                  // If this savecode is NOT coming from a previous game version, then I assume it is safe to completely replace the existing dictionary with the saved one.
                  ReplaceContents = True
                }
                if (att_datatype="stringlist") {
                  if (ReplaceContents = True) {
                    // Completely replace stringlist contents with those found in the SaveCode
                    set (object, attributename, att_value_obj)
                  }
                  else {
                    // Add the contents of the saved stringlist TO the existing stringlist in-game
                    FinalList = NewStringList()
                    // Retrieve the contents of the existing list
                    PreLoadList = preload_att_value
                    CombinedList = ListCombine (PreLoadList, att_value_obj)
                    // Remove duplicates
                    CompactList = ListCompact (CombinedList)
                    // CompactList will be a generic "list" type object, need to convert it back to a stringlist...
                    foreach (olt, CompactList) {
                      list add (FinalList, olt)
                    }
                    set (object, attributename, FinalList)
                  }
                }
                else if (att_datatype="objectlist") {
                  if (ReplaceContents = True) {
                    // Completely replace objectlist contents with those found in the SaveCode
                    set (object, attributename, att_value_obj)
                  }
                  else {
                    // Add the contents of the saved objectlist TO the existing objectlist in-game
                    // Retrieve the contents of the existing list
                    PreLoadList = preload_att_value
                    CombinedList = ListCombine (PreLoadList, att_value_obj)
                    // Remove duplicates
                    FinalList = ObjectListCompact (CombinedList)
                    set (object, attributename, FinalList)
                  }
                }
                else if (att_datatype="stringdictionary") {
                  if (ReplaceContents = True) {
                    // Then completely overwrite existing stringdictionary contents with those in the savecode
                    set (object, attributename, att_value_obj)
                    Dummy = NewStringDictionary()
                  }
                  else {
                    // Add the contents of the saved stringdictionary TO the existing stringdictionary in-game
                    Dummy = preload_att_value
                    // Add dictionary values from SaveGame
                    dictrows = Split(att_value, ";")
                    foreach (kv, dictrows) {
                      KeyValList = Split(kv," = ")
                      key = ListItem(KeyValList, 0)
                      value = ListItem(KeyValList, 1)
                      DictionaryAdd (Dummy, key, value)
                    }
                    set (object, attributename, Dummy)
                  }
                }
                else if (att_datatype="objectdictionary") {
                  if (upgradesave = False) {
                    // Then completely overwrite existing objectdictionary contents with those in the savecode
                    set (object, attributename, att_value_obj)
                  }
                  else {
                    // Add the contents of the saved objectdictionary TO the existing objectdictionary in-game
                    Dummy = preload_att_value
                    // Add dictionary values from SaveGame
                    dictrows = Split(att_value, ";")
                    foreach (kv, dictrows) {
                      KeyValList = Split(kv," = ")
                      key = ListItem(KeyValList, 0)
                      value = ListItem(KeyValList, 1)
                      if (StartsWith(value,"Object: ")) {
                        value = GetObject(Right(value,LengthOf(value)-LengthOf("Object: ")))
                      }
                      DictionaryAdd (Dummy, key, value)
                    }
                    set (object, attributename, Dummy)
                  }
                }
              }
              else if (att_datatype="string") {
                set (object, attributename, att_value)
              }
              else {
                error ("ERROR: Unsupported object type detected in SaveCode: "+fullname+" of "+att_datatype+" datatype.")
              }
              if (HasAttribute (object, cha)) {
                // If a change script exists for this attribute, set change script back to original value after attribute has been changed
                set (object, cha, scr)
                scr => {
                }
              }
            }
          }
        }
        // Extract and update map data from saved grid_coordinates. Because grid_coordinates is a dictionary of dictionaries, it needed to be saved in a special way. Thus, it needs to be loaded in a special way as well.
        // If D1=|,D2=$,D3=;,and D4=@, then grid_coordinates will be saved in form: "ObjectOwner$MapAttributeName&%&Key1$Lkey1 = Lvalue1:type;Lkey2 = Lvalue2:type;|Key2$Lkey1 = Lvalue1:type;Lkey2 = Lvalue2:type;Lkey3 = Lvalue3:type|" etc.
        AllSavedGrids = Split(GridGInfo,D4)
        if (DebugMode) {
          msg ("<br>AllSavedGrids: "+ToString(AllSavedGrids))
        }
        foreach (A, AllSavedGrids) {
          UDictionary = NewDictionary()
          ItemAndValue = Split(A,"&%&")
          ObjAndAtt = ListItem(ItemAndValue,0)
          ObjAndAtt = Split(ObjAndAtt,D2)
          objectname = ListItem(ObjAndAtt,0)
          attributename = ListItem(ObjAndAtt,1)
          object = GetObject(objectname)
          GridVals = ListItem(ItemAndValue,1)
          GridVals = Split(GridVals,D1)
          foreach (B, GridVals) {
            UKeyAndUVal = Split(B,D2)
            UKey = ListItem(UKeyAndUVal,0)
            UVal = ListItem(UKeyAndUVal,1)
            UVal = Split(UVal,D3)
            LDictionary = NewDictionary()
            foreach (C, UVal) {
              LkeyAndLval = Split(C," = ")
              Lkey = ListItem(LkeyAndLval,0)
              LvalAndType = ListItem(LkeyAndLval,1)
              LvalAndType = Split(LvalAndType,":")
              Lval_str = ListItem(LvalAndType,0)
              LType = ListItem(LvalAndType,1)
              if (LType="int") {
                Lval = ToInt(Lval_str)
              }
              else if (LType="double") {
                Lval = ToDouble(Lval_str)
              }
              else if (LType="boolean") {
                if (Lval_str="True") {
                  Lval = True
                }
                else {
                  Lval = False
                }
              }
              else {
                error ("ERROR: Unsupported datatype found in "+objectname+"."+attributename+"! Datatype '"+LType+"' not supported!")
              }
              DictionaryAdd (LDictionary, Lkey, Lval)
            }
            DictionaryAdd (UDictionary, UKey, LDictionary)
          }
          if (DebugMode) {
            msg ("<br>"+objectname+"."+attributename+" UDictionary: "+ToString(UDictionary))
          }
          set (object, attributename, UDictionary)
        }
        // Destroy any objects that the player destroyed during their saved game, if any
        if (ListCount(DestroyedList)>0) {
          foreach (o, DestroyedList) {
            // Check that objects still exist...
            IsThere = GetObject(o)
            if (not Equal(IsThere,null)) {
              // If its there, destroy the object
              destroy (o)
              DestroyedObjDebugList = DestroyedObjDebugList+o+"<br>"
            }
          }
        }
        if (DebugMode) {
          msg ("Created objects: "+CreatedObjDebugList)
          msg ("Destroyed objects: "+DestroyedObjDebugList)
          msg ("Skipped Attributes:<br>"+SkippedAttList)
        }
        // Finally, update game.pov.parent and game.pov
        set (GamePOVObject, "parent", GamePOVParent)
        game.pov = GamePOVObject
        // player.grid_coordinates = null
        JS.Grid_ClearAllLayers ()
        Grid_Redraw
        Grid_DrawPlayerInRoom (game.pov.parent)
        ClearScreen
        ShowRoomDescription
      }
    }
  ]]></function>
  <function name="GetSaveGameCodeDelims" type="stringlist">
    // GetSaveGameCodeDelims() function that returns the delimiters used by the SaveGameCode function in a stringlist in the order [D1,D2,D3,D4].
    // Useful for getting a list of delimiters to ban from user-entered fields (i.e. "enter your name")
    DelimList = NewStringList()
    SaveString = SaveGameCode(False)
    // Retrieve delimiters from end of SaveString
    Dls = Right(SaveString,4)
    D1 = Mid (Dls, 1, 1)
    D2 = Mid (Dls, 2, 1)
    D3 = Mid (Dls, 3, 1)
    D4 = Mid (Dls, 4, 1)
    // Add to list
    list add (DelimList, D1)
    list add (DelimList, D2)
    list add (DelimList, D3)
    list add (DelimList, D4)
    // Return the list of delimiters as a string list
    return (DelimList)
  </function>
  <function name="GetSaveCheckpointDelims" type="stringlist">
    // GetSaveCheckpointDelims() function that returns the delimiters used by the SaveCheckpoint function in a stringlist in the order [D1,D2,D3,D4].
    // Useful for getting a list of delimiters to ban from user-entered fields (i.e. "enter your name")
    DelimList = NewStringList()
    SaveString = SaveCheckpoint("")
    // Retrieve delimiters from end of SaveString
    Dls = Right(SaveString,4)
    D1 = Mid (Dls, 1, 1)
    D2 = Mid (Dls, 2, 1)
    D3 = Mid (Dls, 3, 1)
    D4 = Mid (Dls, 4, 1)
    // Add to list
    list add (DelimList, D1)
    list add (DelimList, D2)
    list add (DelimList, D3)
    list add (DelimList, D4)
    // Return the list of delimiters as a string list
    return (DelimList)
  </function>
  <javascript src="SaveLoadJavaCode.js" />
</asl>

This topic is now closed. Topics are closed after 60 days of inactivity.

Support

Forums