'OHRRPGCE GAME - Saving and loading games
'(C) Copyright 1997-2020 James Paige, Ralph Versteegen, and the OHRRPGCE Developers
'Dual licensed under the GNU GPL v2+ and MIT Licenses. Read LICENSE.txt for terms and disclaimer of liability.

#include "config.bi"
#include "udts.bi"
#include "allmodex.bi"
#include "bmodsubs.bi"
#include "common.bi"
#include "gglobals.bi"
#include "const.bi"
#include "loading.bi"
#include "reload.bi"
#include "reloadext.bi"
#include "savegame.bi"
#include "game.bi"
#include "moresubs.bi"
#include "menustuf.bi"
#include "scriptcommands.bi"
#include "achievements_runtime.bi"

USING Reload
USING Reload.Ext

'--Local subs and functions

DECLARE SUB gamestate_to_reload(byval node as Reload.NodePtr)
DECLARE SUB gamestate_state_to_reload(byval parent as Reload.NodePtr)
DECLARE SUB gamestate_script_to_reload(byval parent as Reload.NodePtr)
DECLARE SUB gamestate_strings_to_reload(byval parent as Reload.NodePtr)
DECLARE FUNCTION script_trigger_from_reload(byval node as Reload.NodePtr) as integer
DECLARE SUB gamestate_globals_to_reload(byval parent as Reload.NodePtr, byval first as integer=0, byval last as integer=maxScriptGlobals)
DECLARE SUB gamestate_maps_to_reload(byval parent as Reload.NodePtr)
DECLARE SUB gamestate_npcs_to_reload(byval parent as Reload.NodePtr, byval map as integer)
DECLARE SUB gamestate_tags_to_reload(byval parent as Reload.NodePtr)
DECLARE SUB gamestate_onetime_to_reload(byval parent as Reload.NodePtr)
DECLARE SUB gamestate_party_to_reload(byval parent as Reload.NodePtr)
DECLARE SUB gamestate_spelllist_to_reload(byval hero_slot as integer, byval spell_list as integer, byval parent as Reload.NodePtr)
DECLARE SUB gamestate_inventory_to_reload(byval parent as Reload.NodePtr)
DECLARE SUB gamestate_shops_to_reload(byval parent as Reload.NodePtr)
DECLARE SUB gamestate_vehicle_to_reload(byval parent as Reload.NodePtr)
DECLARE SUB gamestate_slices_to_reload(byval parent as Reload.NodePtr)

DECLARE FUNCTION new_loadgame(byval slot as integer, prefix as string="") as string
DECLARE SUB gamestate_from_reload(byval node as Reload.NodePtr)
DECLARE SUB gamestate_state_from_reload(byval node as Reload.NodePtr)
DECLARE SUB gamestate_script_from_reload(byval node as Reload.NodePtr)
DECLARE SUB gamestate_strings_from_reload(byval stringsnode as Reload.NodePtr)
DECLARE SUB script_trigger_to_reload(byval parent as Reload.NodePtr, node_name as string, byval script_id as integer)
DECLARE SUB gamestate_globals_from_reload(byval parent as Reload.NodePtr, byval first as integer, byval last as integer)
DECLARE SUB gamestate_maps_from_reload(byval node as Reload.NodePtr)
DECLARE SUB gamestate_npcs_from_reload(byval node as Reload.NodePtr, byval map as integer)
DECLARE SUB gamestate_tags_from_reload(byval node as Reload.NodePtr)
DECLARE SUB gamestate_onetime_from_reload(byval node as Reload.NodePtr)
DECLARE SUB gamestate_party_from_reload(byval node as Reload.NodePtr)
DECLARE SUB gamestate_spelllist_from_reload(byval hero_slot as integer, byval spell_list as integer, byval parent as Reload.NodePtr)
DECLARE SUB gamestate_inventory_from_reload(byval node as Reload.NodePtr)
DECLARE SUB gamestate_shops_from_reload(byval node as Reload.NodePtr)
DECLARE SUB gamestate_vehicle_from_reload(byval node as Reload.NodePtr)
DECLARE SUB gamestate_slices_from_reload(byval node as Reload.NodePtr)

DECLARE SUB rsav_warn (s as string)

DECLARE SUB new_loadglobalvars (byval slot as integer, byval first as integer, byval last as integer)

DECLARE FUNCTION new_save_slot_used (byval slot as integer, prefix as string="") as bool
DECLARE FUNCTION new_count_used_save_slots() as integer
DECLARE SUB new_get_save_slot_preview(byval slot as integer, pv as SaveSlotPreview)

'--old save/load support
DECLARE FUNCTION old_loadgame (byval slot as integer) as string
DECLARE SUB old_loadglobalvars (byval slot as integer, byval first as integer, byval last as integer)
DECLARE SUB old_get_save_slot_preview(byval slot as integer, pv as SaveSlotPreview)
DECLARE SUB show_load_index(byval z as integer, caption as string, byval slot as integer=0)
DECLARE SUB rebuild_inventory_captions (invent() as InventSlot)
DECLARE FUNCTION old_save_slot_used (byval slot as integer) as bool
DECLARE FUNCTION old_count_used_save_slots() as integer
DECLARE SUB old_erase_save_slot (byval slot as integer)

DIM SHARED old_savefile as string
DIM savedir as string  'Global

DIM SHARED current_save_slot as integer '--used in rsav_warn
DIM SHARED current_save_prefix as string '--used in rsav_warn

'-----------------------------------------------------------------------

'Creating if necessary, return directory in which to put saves if the default isn't writable
FUNCTION fallback_save_location(savename as string) as string
 IF savename = "" THEN
  RETURN prefsdir & SLASH
 ELSE
  'This prefsdir (which assumably is the name of another .rpg file) might not already exist.
  'Need to use find_file_anycase because mismatched cases will work on Windows
  DIM ret as string = find_file_anycase(settings_dir & SLASH & savename)
  IF NOT isdir(ret) THEN makedir ret
  RETURN ret & SLASH
 END IF
END FUNCTION

SUB init_save_system()
 DIM rpg_folder as string = add_trailing_slash(trimfilename(sourcerpg))

 DIM gen_root as NodePtr = get_general_reld()
 DIM savename as string
 'Don't use .default() because it doesn't provide a default for null nodes or zero-length strings
 savename = fixfilename(gen_root."saved_games"."savename".string)
 IF LEN(savename) = 0 THEN savename = game_fname

 '--set up old savegame file
 old_savefile = rpg_folder & savename + ".sav"
 #IFDEF __FB_UNIX__
 IF NOT fileisreadable(old_savefile) THEN
  'For a systemwide unix install, the current directory may be unwriteable
  '(so old_savefile is unreadable if it doesn't exist), so .sav file might be in the prefs dir
  'Note: there's no point handling an unreadable old_savefile on other OSes,
  'because on other OSes we never put SAV files anywhere else.
  old_savefile = fallback_save_location(savename) & savename & ".sav"
 END IF
 #ENDIF

 '--set up new rsav folder
 IF force_prefsdir_save = NO ANDALSO diriswriteable(rpg_folder) THEN
  '--default location is same as the RPG file (if possible)
  'Find the directory case-insensitively
  savedir = find_file_anycase(rpg_folder & savename & ".saves", fileTypeDirectory)
 ELSE
  savedir = fallback_save_location(savename) & "saves"
 END IF
 
 #IFDEF __FB_JS__
 'When running on web, tell the virtual filesystem to storage the saves folder in IndexDB
 web_mount_persistent_storage(savedir)
 #ENDIF
 
 IF NOT isdir(savedir) THEN makedir savedir

 debuginfo "savedir: " & savedir
END SUB

'Note: resetgame (only if a game was previously started) + initgamedefaults MUST be called before loadgame
'to initialise anything missing from the save file
SUB loadgame (byval slot as integer, prefix as string="")
 debuginfo "Loading save slot " & prefix & slot
 DIM slot_count as integer = gen(genSaveSlotCount)
 IF slot_count = 0 THEN slot_count = 4
 IF prefix = "" ANDALSO slot >= 0 ANDALSO slot <= maxSaveSlotCount ANDALSO slot <= slot_count - 1 THEN
  'only update lastsaveslot for normal prefixless slots
  debuginfo "loadgame: " & slot & " is a valid normal slot, updating lastsaveslot"
  lastsaveslot = slot + 1
 ELSE
  debuginfo "loadgame: " & slot & " is special non-normal slot, skipping update of lastsaveslot"
 END IF

 DIM errmsg as string = "Save slot empty"
 DIM filename as string
 filename = savedir & SLASH & prefix & slot & ".rsav"
 IF isfile(filename) THEN
  errmsg = new_loadgame(slot, prefix)
 ELSEIF isfile(old_savefile) THEN
  'Fall back to old save format
  IF prefix = "" THEN
   errmsg = old_loadgame(slot)
  END IF
 END IF

 'The game state has already been reset, so any error in loading is unrecoverable
 IF LEN(errmsg) THEN fatalerror "Couldn't load save slot " & prefix & slot & ": " & errmsg
END SUB

'Load a range of global variables from a save slot
'Reads zeroes, without an error, on empty save slots
SUB loadglobalvars (byval slot as integer, byval first as integer, byval last as integer)
 VAR prev_subtimer = main_timer.switch(TimerIDs.FileIO)

 FOR i as integer = first TO last
  global(i) = 0
 NEXT i

 'see also saveslot_global() if you just need a very simple function to read a single global from a save slot
 DIM filename as string
 filename = savedir & SLASH & slot & ".rsav"
 IF isfile(filename) THEN
  new_loadglobalvars slot, first, last
 ELSE
  old_loadglobalvars slot, first, last
 END IF

 main_timer.switch(prev_subtimer)
END SUB

SUB get_save_slot_preview(byval slot as integer, pv as SaveSlotPreview)
 IF new_save_slot_used(slot) THEN
  'debuginfo "use nifty new save slot preview for slot " & slot
  new_get_save_slot_preview slot, pv
 ELSE
  'debuginfo "fall back to boring old save slot preview for " & slot
  old_get_save_slot_preview slot, pv
 END IF
END SUB

FUNCTION save_slot_used (byval slot as integer, prefix as string = "") as integer
 IF new_save_slot_used(slot, prefix) THEN
  RETURN YES
 END IF
 'rsav save slot does not exist, fall back to checking the .sav
 IF prefix <> "" THEN RETURN NO
 RETURN old_save_slot_used(slot)
END FUNCTION

FUNCTION count_used_save_slots() as integer
 'Counts how many saves are visible to the player, ignoring hidden slots and quicksaves.
 DIM count as integer = new_count_used_save_slots()
 IF count > 0 THEN
  RETURN count
 END IF
 RETURN old_count_used_save_slots()
END FUNCTION

'-----------------------------------------------------------------------

SUB savegame (byval slot as integer, prefix as string="")

 BUG_IF(prefix <> trimpath(prefix), "Not allowed to include path in save prefix!")

 VAR prev_subtimer = main_timer.switch(TimerIDs.FileIO)

 DIM slot_count as integer = gen(genSaveSlotCount)
 IF slot_count = 0 THEN slot_count = 4
 IF prefix = "" ANDALSO slot >= 0 ANDALSO slot <= maxSaveSlotCount - 1 ANDALSO slot <= slot_count - 1 THEN
  'Only remember lastsaveslot for valid normal slots
  debuginfo "savegame: " & slot & " is a valid normal slot, updating lastsaveslot"
  lastsaveslot = slot + 1
 ELSE
  debuginfo "savegame: " & slot & " is special non-normal slot, skipping update of lastsaveslot"
 END IF

 debuginfo "Saving in slot " & prefix & slot
 current_save_slot = slot
 current_save_prefix = prefix
 
 DIM doc as DocPtr
 doc = CreateDocument()
 
 DIM node as NodePtr
 node = CreateNode(doc, "rsav")
 SetRootNode(doc, node)
 gamestate_to_reload node
 
 DIM filename as string
 filename = savedir & SLASH & prefix & slot & ".rsav"
 SerializeBin filename, doc
 
 FreeDocument doc

#ifdef __FB_JS__
web_sync_persistent_storage()
#endif

 current_save_slot = -1
 current_save_prefix = ""
 main_timer.switch(prev_subtimer)
END SUB

'Returns an error message on failure
FUNCTION new_loadgame(byval slot as integer, prefix as string="") as string

 IF prefix <> trimpath(prefix) THEN
  RETURN "loadgame: save slot prefix is not allowed to include path"
 END IF

 DIM filename as string
 filename = savedir & SLASH & prefix & slot & ".rsav"

 DIM doc as DocPtr
 doc = LoadDocument(filename, optNoDelay)

 IF doc = NULL THEN RETURN "Save file couldn't be opened or is corrupt: " & filename

 current_save_slot = slot
 current_save_prefix = prefix

 DIM node as NodePtr
 node = DocumentRoot(doc)
 
 gamestate_from_reload node
 
 FreeDocument doc
 current_save_slot = -1
 current_save_prefix = ""
END FUNCTION

SUB rsav_warn (s as string)
 debug "Save slot " & current_save_prefix & current_save_slot & ": " & s
END SUB

'-----------------------------------------------------------------------

'These are RELOADBasic directives, see reloadbasic/RELOADbasic.txt and reloadtest.bas
#warn_func = rsav_warn
#error_func = rsav_warn

'Works under the assumption that resetgame has already been called.
SUB gamestate_from_reload(byval node as Reload.NodePtr)
 IF NodeName(node) <> "rsav" THEN rsav_warn "root node is not rsav"

 IF node."ver" > CURRENT_RSAV_VERSION THEN
  rsav_warn "new save file on old game player. Some data might get lost"
  fadein
  pop_warning "This save file was created with a more recent version of the OHRRPGCE, and some things will not load correctly. Download the latest version at http://HamsterRepublic.com"
  fadeout 0, 0, 0
 END IF

 Achievements.runtime_reset ' notify the acheivement subsystem that tags are going to go crazy for a bit

 READNODE node, default
  node."ver".ignore
  node."game_client".ignore
  gamestate_state_from_reload     node."state".warn.ptr
  gamestate_script_from_reload    node."script".warn.ptr
  gamestate_maps_from_reload      node."maps".warn.ptr
  gamestate_tags_from_reload      node."tags".warn.ptr
  gamestate_onetime_from_reload   node."onetime".warn.ptr
  gamestate_party_from_reload     node."party".warn.ptr
  gamestate_inventory_from_reload node."inventory".warn.ptr
  gamestate_shops_from_reload     node."shops".warn.ptr
  gamestate_vehicle_from_reload   node."vehicle".warn.ptr
  'Added later
  gamestate_slices_from_reload    node."slices".ptr
  Achievements.runtime_load       node."achievements".default(null).ptr
 END READNODE
END SUB

SUB gamestate_state_from_reload(byval node as Reload.NodePtr)
 gam.map.id = node."current_map".warn
 IF gam.map.id > gen(genMaxMap) THEN
  visible_debug "This save file is corrupt: it is saved on nonexistent map " & gam.map.id
  gam.map.id = 0
 END IF

 DIM map_offset as XYPair
 map_offset = load_map_pos_save_offset(gam.map.id)

 READNODE node, default
  node."current_map".ignore
  node."status_char".ignore  'Removed in rsav version 6

  READNODE node."caterpillar".warn as caterpillar
   WITHNODE caterpillar."hero" as n
    DIM i as integer = GetInteger(n)
    SELECT CASE i
     CASE 0 TO 3
      READNODE n, default
       (herox(i)) = n."x" + map_offset.x * 20
       (heroy(i)) = n."y" + map_offset.y * 20
       (herodir(i)) = n."d"
      END READNODE
     CASE ELSE
      rsav_warn "invalid caterpillar hero index " & i
    END SELECT
   END WITHNODE
  END READNODE

  gam.random_battle_countdown = node."random_battle_countdown"

  READNODE node."camera".warn as ch, default
   mapx = ch."x"
   mapy = ch."y"
   gen(genCameraMode) = ch."mode"
   gen(genCameraArg1) = ch."arg1"
   gen(genCameraArg2) = ch."arg2"
   gen(genCameraArg3) = ch."arg3"
   gen(genCameraArg4) = ch."arg4"
  END READNODE

  gold = node."gold"

  READNODE node."playtime".warn as ch, default
   gen(genDays) = ch."days"
   gen(genHours) = ch."hours"
   gen(genMinutes) = ch."minutes"
   gen(genSeconds) = ch."seconds"
  END READNODE

  'FIXME: There is no point to restoring this with no other textbox state being saved
  gen(genTextboxBackdrop) = node."textbox"."backdrop"

  'Damage and level caps are only saved if they were modified.
  '(So we don't want these to default, but we're inside a READNODE that defaults)
  gen(genDamageCap) = node."damage_cap".default(gen(genDamageCap))
  gen(genLevelCap) = small(node."level_cap".default(gen(genLevelCap)), gen(genMaxLevel))

  READNODE node."stats".warn as stats
   WITHNODE stats."stat" as n
    DIM i as integer = GetInteger(n)
    SELECT CASE i
     CASE 0 TO 11
      gen(genStatCap + i) = n."cap"
     CASE ELSE
      rsav_warn "invalid stat cap index " & i
    END SELECT
   END WITHNODE
  END READNODE

 END READNODE
END SUB

SUB gamestate_script_from_reload(byval node as Reload.NodePtr)
 gamestate_globals_from_reload node, 0, maxScriptGlobals

 DIM load_script_ids as integer = (prefbit(18) = NO)  '"Don't save gameover/loadgame script IDs" off

 READNODE node, default
  node."globals".ignore

  'It's not actually necessary for this function to always be called (defaulted)
  gamestate_strings_from_reload node."strings".ptr

  'If these aren't loaded then they take the value set in the .rpg file
  IF load_script_ids THEN gen(genGameoverScript) = script_trigger_from_reload(node."gameover_script".ptr)
  IF load_script_ids THEN gen(genLoadgameScript) = script_trigger_from_reload(node."loadgame_script".ptr)

  READNODE node."suspend".warn as ch, default
   setbit gen(), genSuspendBits, 0, ch."npcs".exists
   setbit gen(), genSuspendBits, 1, ch."player".exists
   setbit gen(), genSuspendBits, 2, ch."obstruction".exists
   setbit gen(), genSuspendBits, 3, ch."herowalls".exists
   setbit gen(), genSuspendBits, 4, ch."npcwalls".exists
   setbit gen(), genSuspendBits, 5, ch."caterpillar".exists
   setbit gen(), genSuspendBits, 6, ch."randomenemies".exists
   setbit gen(), genSuspendBits, 7, ch."boxadvance".exists
   setbit gen(), genSuspendBits, 8, ch."overlay".exists
   setbit gen(), genSuspendBits, 9, ch."ambientmusic".exists
  END READNODE

  gen(genScrBackdrop) = node."backdrop"
 END READNODE
END SUB

SUB gamestate_strings_from_reload(byval stringsnode as Reload.NodePtr)
 DIM gen_root as NodePtr = get_general_reld()
 IF gen_root."saved_games"."strings".exists = NO THEN EXIT SUB

 READNODE stringsnode
  WITHNODE stringsnode."string" as n
   DIM id as integer = GetInteger(n)
   IF id < LBOUND(plotstr) OR id > UBOUND(plotstr) THEN
    rsav_warn "invalid plotstring id " & id
   ELSE
    WITH plotstr(id)
     DIM style as integer
     READNODE n
      .s = n."str".string
      .x = n."x"
      .y = n."y"
      .col = n."col"
      .bgcol = n."bgcol"
      IF n."visible".bool THEN .bits += 1
      style = n."style"
     END READNODE
     IF style < 0 OR style > 1 THEN
      rsav_warn "invalid plotstr style " & style
     END IF
     IF style = 1 THEN .bits += 2
    END WITH
   END IF
  END WITHNODE
 END READNODE
END SUB

'Note: node may be NULL
SUB gamestate_slices_from_reload(byval node as Reload.NodePtr)
 DIM gen_root as NodePtr = get_general_reld()

 DIM layernode as Reload.NodePtr
 layernode = node."sprite_layer".ptr
 IF layernode <> NULL THEN
  IF gen_root."saved_games"."sprite_layer".exists = NO THEN
   debuginfo "RSAV: Ignoring saved spritelayer"
  ELSE
   WITH SliceTable
    IF .ScriptSprite->NumChildren <> 0 THEN
     showbug "RSAV: old sprite_layer meant to be empty"
    END IF
    DeleteSlice @.ScriptSprite
    .ScriptSprite = NewSliceOfType(slSpecial, .Root)
    InsertSliceBefore .textbox, .ScriptSprite
    .ScriptSprite->Fill = YES
    RefreshSliceScreenPos(.ScriptSprite)  'Apply Fill

    SliceLoadFromNode .ScriptSprite, layernode, YES   'Load slice handles
    IF .ScriptSprite->Lookup <> SL_SCRIPT_LAYER THEN
     debugerror "RSAV: sprite_layer has unexpected lookup code " & .ScriptSprite->Lookup
     .ScriptSprite->Lookup = SL_SCRIPT_LAYER
    END IF
   END WITH
  END IF
 END IF
END SUB

FUNCTION script_trigger_from_reload(byval node as NodePtr) as integer
 IF node = 0 THEN RETURN 0
 IF node."name".exists THEN
  rsav_warn "still don't have code to lookup a script id by string!"
  'FIXME: this doesn't work yet!
  RETURN 0
 ELSEIF node."id".exists THEN
  RETURN node."id"
 END IF
 rsav_warn "neither 'id' nor 'name' found in script trigger node"
 RETURN 0 
END FUNCTION

SUB gamestate_globals_from_reload(byval parent as Reload.NodePtr, byval first as integer, byval last as integer)
 FOR i as integer = first TO last
  global(i) = 0
 NEXT i

 READNODE parent."globals".warn as node
  WITHNODE node."global" as n
   DIM i as integer
   i = GetInteger(n)
   IF i >= first AND i <= last THEN
    global(i) = n."int"
   ELSEIF i < 0 OR i > maxScriptGlobals THEN
    rsav_warn "invalid global id " & i
   END IF
  END WITHNODE
 END READNODE
END SUB

SUB gamestate_maps_from_reload(byval node as Reload.NodePtr)
 DIM i as integer
 DIM loaded_current as bool = NO
 
 READNODE node
  WITHNODE node."map" as n
  'FIXME: currently only supports saving the current map
   i = GetInteger(n)
   IF i = gam.map.id THEN
    gamestate_npcs_from_reload n."npcs".ptr, gam.map.id
    loaded_current = YES
   END IF
  END WITHNODE
 END READNODE

 IF loaded_current = NO THEN
  rsav_warn "couldn't find saved data for current map " & gam.map.id
 END IF
END SUB

SUB gamestate_npcs_from_reload(byval node as Reload.NodePtr, byval map_id as integer)
 DIM map_offset as XYPair
 map_offset = load_map_pos_save_offset(map_id)

 CleanNPCL npc()

 DIM i as integer
 READNODE node
  WITHNODE node."npc" as n
   i = GetInteger(n)
   SELECT CASE i
    CASE 0 TO UBOUND(npc)
     load_npc_instance n, npc(i), map_offset
    CASE ELSE
     rsav_warn "invalid npc instance " & i
   END SELECT
  END WITHNODE
 END READNODE
END SUB

SUB gamestate_tags_from_reload(byval node as Reload.NodePtr)
 DIM count as integer
 count = node."count"
 IF count > (max_tag()+1) THEN
  rsav_warn "too many saved tags " & (max_tag()+1) & " < " & count
  count = max_tag()+1
 END IF

 DIM buf(count \ 16) as integer

 LoadBitsetArray(node."data".ptr, buf(), UBOUND(buf))
 
 FOR i as integer = 0 TO count - 1
  settag i, readbit(buf(), 0, i)
 NEXT i
END SUB

SUB gamestate_onetime_from_reload(byval node as Reload.NodePtr)
 DIM count as integer
 count = node."count"
 IF count > max_onetime + 1 THEN
  rsav_warn "too many saved onetime tags " & count & " > " & (max_onetime + 1)
  count = max_onetime + 1
 END IF

 DIM buf(count \ 16) as integer

 LoadBitsetArray(node."data".ptr, buf(), UBOUND(buf))
 
 FOR i as integer = 0 TO count - 1
  settag onetime(), i, readbit(buf(), 0, i)
 NEXT i
END SUB

SUB gamestate_party_from_reload(byval node as Reload.NodePtr)
 DIM as integer i, j
 
 READNODE node
  WITHNODE node."slot" as slot

   i = GetInteger(slot)
   SELECT CASE i
    CASE 0 TO 40

     WITH gam.hero(i)

      IF slot."id".exists THEN
       addhero slot."id", i, slot."lev", NO, YES  'allow_rename=NO, loading=YES
      END IF

      'Other data must be loaded even if the hero slot is empty, for compatibility

      DIM base_stats_missing as bool = NO

      READNODE slot
       slot."id".ignore

       .name = slot."name".string

       .locked = slot."locked".exists

       READNODE slot."stats".warn as ch
        WITHNODE ch."stat" as n
         j = GetInteger(n)
         SELECT CASE j
          CASE 0 TO statLast
           '(All of cur, max, base are omitted if all zero)
           .stat.cur.sta(j) = n."cur"
           .stat.max.sta(j) = n."max"
           IF n."base".exists THEN
            .stat.base.sta(j) = n."base"
           ELSE
            base_stats_missing = YES
           END IF
          CASE ELSE
           rsav_warn "invalid stat id " & j
         END SELECT
        END WITHNODE
       END READNODE

       .lev = slot."lev"
       .lev_gain = slot."lev_gain"
       .exp_cur = slot."exp"
       .exp_next = slot."exp_next"
       .exp_mult = slot."exp_mult".double
       .def_wep = slot."def_wep"

       READNODE slot."in_battle".warn as ch
        .battle_pic = ch."pic"
        .battle_pal = ch."pal"
       END READNODE

       READNODE slot."walkabout".warn as ch
        .pic = ch."pic"
        .pal = ch."pal"
       END READNODE

       READNODE slot."portrait" as ch
        .portrait_pic = ch."pic".integer
        .portrait_pal = ch."pal".integer
       END READNODE

       READNODE slot."hand" as hand
        WITHNODE hand."frame" as fr
         j = GetInteger(fr)
         IF j >= 0 AND j <= UBOUND(.hand_pos) THEN
          .hand_pos_overridden = YES
          .hand_pos(j).x = fr."x"
          .hand_pos(j).y = fr."y"
         ELSE
          rsav_warn "Invalid hand position frame num " & j
         END IF
        END WITHNODE
       END READNODE

       .auto_battle = slot."auto_battle".bool.default(NO)

       ' Abandoned data no longer loaded (obsolete)
       slot."rename_on_add".ignore
       slot."rename_on_status".ignore
       slot."hide_empty_lists".ignore
       slot."battle_menus".ignore
       slot."wep".ignore

       READNODE slot."elements" as ch
        ch."weak".ignore
        ch."strong".ignore
        ch."absorb".ignore

        WITHNODE ch."element" as n
         DIM j as integer = GetInteger(n)
         IF j < gen(genNumElements) THEN
          .elementals(j) = n."damage".float.default(1.0)
         END IF
        END WITHNODE
       END READNODE

       READNODE slot."spell_lists".warn as ch
        WITHNODE ch."list" as n
         j = GetInteger(n)
         SELECT CASE j
          CASE 0 TO 3
           gamestate_spelllist_from_reload(i, j, n)
          CASE ELSE
           rsav_warn "invalid spell list id " & j
         END SELECT
        END WITHNODE
       END READNODE

       READNODE slot."level_mp".warn as ch
        WITHNODE ch."lev" as n
         j = GetInteger(n)
         SELECT CASE j
          CASE 0 TO maxMPLevel
           .levelmp(j) = n."val"
          CASE ELSE
           rsav_warn "invalid level mp slot " & j
         END SELECT
        END WITHNODE
       END READNODE

       READNODE slot."equipment".warn as ch
        WITHNODE ch."equip" as n
         j = GetInteger(n)
         SELECT CASE j
          CASE 0 TO 4
           .equip(j).id = n."item".default(-1)
          CASE ELSE
           rsav_warn "invalid equip slot " & j
         END SELECT
        END WITHNODE
       END READNODE

      END READNODE 'slot

      IF base_stats_missing THEN
       compute_hero_base_stats_from_max i
      END IF

     END WITH 'gam.hero(i)

    CASE ELSE
     rsav_warn "invalid hero party slot " & i
   END SELECT

  END WITHNODE
 END READNODE
END SUB

SUB gamestate_spelllist_from_reload(byval hero_slot as integer, byval spell_list as integer, byval parent as Reload.NodePtr)
 DIM node as NodePtr
 node = parent."spells".warn.ptr

 IF spell_list <> GetInteger(node) THEN
  rsav_warn "spell list id mismatch " & spell_list & "<>" & GetInteger(node)
 END IF
 
 READNODE node
  WITHNODE node."spell" as n
   DIM i as integer = GetInteger(n)
   SELECT CASE i
    CASE 0 TO 23
     gam.hero(hero_slot).spells(spell_list, i) = n."attack" + 1
    CASE ELSE
     rsav_warn "invalid spell list slot " & i
   END SELECT
  END WITHNODE
 END READNODE
 
END SUB

SUB gamestate_inventory_from_reload(byval node as Reload.NodePtr)
 DIM i as integer

 gen(genMaxInventory) = node."size"
 'Calling last_inv_slot clamps gen(genMaxInventory) to a valid value
 DIM last as integer
 last = last_inv_slot()

 READNODE node."slots".warn as ch
  WITHNODE ch."slot" as n
   i = GetInteger(n)
   SELECT CASE i
    CASE 0 TO last
     WITH inventory(i)
      .used = YES
      .id = n."item".warn
      .num = n."num".warn
     END WITH
    CASE ELSE
     rsav_warn "invalid inventory slot id " & i
   END SELECT
  END WITHNODE
 END READNODE
 
 rebuild_inventory_captions inventory()
 
END SUB

SUB gamestate_shops_from_reload(byval node as Reload.NodePtr)
 DIM as integer i, j
 DIM shoptmp(19) as integer

 READNODE node
  WITHNODE node."shop" as n
   i = GetInteger(n)
   SELECT CASE i
    CASE 0 TO gen(genMaxShop)
     loadrecord shoptmp(), game & ".sho", 20, i
     READNODE n."slots".warn as slots
      WITHNODE slots."slot" as n2
       j = GetInteger(n2)
       SELECT CASE j
        CASE 0 TO shoptmp(16)
         gam.stock(i, j) = n2."stock".default(0)
         WITH gam.original_stock(i, j)
          .thingtype = n2."orig_type".default(-1)
          .thingid   = n2."orig_id"
          .stock     = n2."orig_stock"
         END WITH
        CASE ELSE
         rsav_warn "invalid shop " & i & " stuff slot " & j
       END SELECT
      END WITHNODE
     END READNODE

    CASE ELSE
     rsav_warn "invalid shop id " & i
   END SELECT
  END WITHNODE
 END READNODE
END SUB

SUB gamestate_vehicle_from_reload(byval node as Reload.NodePtr)
 WITH vstate

  IF node."id".exists THEN
   READNODE node, default
    READNODE node."state" as ch, default
     .active    = ch."active"
     .npc       = ch."npc"
     .old_speed = ch."old_speed"
     .mounting        = ch."mounting".exists
     .rising          = ch."rising".exists
     .falling         = ch."falling".exists
     .init_dismount   = ch."init_dismount".exists
     .trigger_cleanup = ch."trigger_cleanup".exists
     .ahead           = ch."ahead".exists
    END READNODE
    .id = node."id"
   END READNODE
   IF .id >= 0 THEN
    LoadVehicle game & ".veh", .dat, .id
   END IF
  END IF
  
 END WITH
END SUB

'-----------------------------------------------------------------------

SUB gamestate_to_reload(byval node as Reload.NodePtr)
 'increment this to produce a warning message when
 'loading a new rsav file in an old game player
 SetChildNode(node, "ver", CURRENT_RSAV_VERSION)
 write_engine_version_node node, "game_client"
 
 gamestate_state_to_reload node
 gamestate_script_to_reload node
 gamestate_slices_to_reload node
 gamestate_maps_to_reload node
 gamestate_tags_to_reload node
 gamestate_onetime_to_reload node
 gamestate_party_to_reload node
 gamestate_inventory_to_reload node
 gamestate_shops_to_reload node
 gamestate_vehicle_to_reload node
 Achievements.runtime_save node
END SUB

SUB gamestate_state_to_reload(byval parent as Reload.NodePtr)
 REDIM original_gen(499) as integer
 xbload game + ".gen", original_gen(), ".gen unreadable"

 DIM node as NodePtr
 node = SetChildNode(parent, "state")
 DIM ch as NodePtr 'used for sub-containers
 DIM n as NodePtr 'used for numbered containers
 
 SetChildNode(node, "current_map", gam.map.id)

 DIM map_offset as XYPair
 map_offset = load_map_pos_save_offset(gam.map.id)

 ch = SetChildNode(node, "caterpillar")
 FOR i as integer = 0 TO 3
  n = AppendChildNode(ch, "hero", i)
  SetChildNode(n, "x", herox(i) - map_offset.x * 20)
  SetChildNode(n, "y", heroy(i) - map_offset.y * 20)
  SetChildNode(n, "d", herodir(i))
 NEXT i

 SetChildNode(node, "random_battle_countdown", gam.random_battle_countdown)
 
 ch = SetChildNode(node, "camera")
 SetChildNode(ch, "x", mapx)
 SetChildNode(ch, "y", mapy)
 SetChildNode(ch, "mode", gen(genCameraMode))
 FOR i as integer = 0 TO 3
  SetChildNode(ch, "arg" & i+1, gen(genCameraArg1 + i))
 NEXT i
 
 SetChildNode(node, "gold", gold)

 ch = SetChildNode(node, "playtime")
 SetChildNode(ch, "days", gen(genDays))
 SetChildNode(ch, "hours", gen(genHours))
 SetChildNode(ch, "minutes", gen(genMinutes))
 SetChildNode(ch, "seconds", gen(genSeconds))

 'FIXME: There is no point to saving this with no other textbox state being saved
 ch = SetChildNode(node, "textbox")
 SetChildNode(ch, "backdrop", gen(genTextboxBackdrop))

 'Save damage and level caps only if modified (by "set damage cap" and "set level cap")
 IF gen(genDamageCap) <> original_gen(genDamageCap) THEN
  SetChildNode(node, "damage_cap", gen(genDamageCap))
 END IF
 IF gen(genLevelCap) <> original_gen(genLevelCap) THEN
  SetChildNode(node, "level_cap", gen(genLevelCap))
 END IF

 ch = SetChildNode(node, "stats")
 FOR i as integer = 0 TO 11
  n = AppendChildNode(ch, "stat", i)
  SetChildNode(n, "cap", gen(genStatCap + i))
 NEXT i

END SUB

SUB gamestate_script_to_reload(byval parent as Reload.NodePtr)
 'FIXME: currently only stores a tiny bit of script state, but could store
 'a lot more in the future

 DIM node as NodePtr
 node = SetChildNode(parent, "script")
 DIM ch as NodePtr 'used for sub-containers
 DIM n as NodePtr 'used for numbered containers

 gamestate_globals_to_reload node
 gamestate_strings_to_reload node

 script_trigger_to_reload(node, "gameover_script", gen(genGameoverScript))
 script_trigger_to_reload(node, "loadgame_script", gen(genLoadgameScript))
 
 ch = SetChildNode(node, "suspend")
 IF readbit(gen(), genSuspendBits, 0) THEN SetChildNode(ch, "npcs")
 IF readbit(gen(), genSuspendBits, 1) THEN SetChildNode(ch, "player")
 IF readbit(gen(), genSuspendBits, 2) THEN SetChildNode(ch, "obstruction")
 IF readbit(gen(), genSuspendBits, 3) THEN SetChildNode(ch, "herowalls")
 IF readbit(gen(), genSuspendBits, 4) THEN SetChildNode(ch, "npcwalls")
 IF readbit(gen(), genSuspendBits, 5) THEN SetChildNode(ch, "caterpillar")
 IF readbit(gen(), genSuspendBits, 6) THEN SetChildNode(ch, "randomenemies")
 IF readbit(gen(), genSuspendBits, 7) THEN SetChildNode(ch, "boxadvance")
 IF readbit(gen(), genSuspendBits, 8) THEN SetChildNode(ch, "overlay")
 IF readbit(gen(), genSuspendBits, 9) THEN SetChildNode(ch, "ambientmusic")
 
 SetChildNode(node, "backdrop", gen(genScrBackdrop))
END SUB

SUB script_trigger_to_reload(byval parent as Reload.NodePtr, node_name as string, byval script_id as integer)
 DIM node as NodePtr
 node = SetChildNode(parent, node_name)
 'IF script_id <= 16383 THEN
  '--old style
  SetChildNode(node, "id", script_id)
 'ELSE
 ' '--new style
 ' This isn't saved yet because we can't load it yet
 ' ...but many years later, I no longer see the need for this. Trigger IDs are stable.
 ' SetChildNode(node, "name", scriptname(script_id))
 'END IF
END SUB

SUB gamestate_globals_to_reload(byval parent as Reload.NodePtr, byval first as integer=0, byval last as integer=maxScriptGlobals)

 DIM node as NodePtr
 node = SetChildNode(parent, "globals")

 DIM n as NodePtr
 DIM nextch as NodePtr
 
 '--delete any old global nodes in the range
 n = FirstChild(node, "global")
 DO WHILE n
  nextch = NextSibling(n, "global")
  SELECT CASE GetInteger(n)
   CASE first TO last
    FreeNode(n)
  END SELECT
  n = nextch
 LOOP

 FOR i as integer = first TO last
  IF global(i) <> 0 THEN
   n = AppendChildNode(node, "global", i)
   AppendChildNode n, "int", global(i)
  END IF
 NEXT i

END SUB

SUB gamestate_strings_to_reload(byval parent as Reload.NodePtr)
 DIM gen_root as NodePtr = get_general_reld()
 IF gen_root."saved_games"."strings".exists = NO THEN EXIT SUB

 DIM node as NodePtr
 node = AppendChildNode(parent, "strings")

 FOR i as integer = LBOUND(plotstr) to UBOUND(plotstr)
  WITH plotstr(i)
   DIM n as NodePtr
   'Check whether equal to initial blank state
   IF LEN(.s) = 0 ANDALSO .col = -1 AND (.x OR .y OR .bgcol OR .bits) = 0 THEN CONTINUE FOR
   n = AppendChildNode(node, "string", i)
   AppendChildNode n, "str", .s
   AppendChildNode n, "x", .x
   AppendChildNode n, "y", .y
   AppendChildNode n, "col", .col
   AppendChildNode n, "bgcol", .bgcol
   AppendChildNode n, "visible", (.bits AND 1)
   'style 0 is edged, 1 is flat
   DIM style as integer = IIF(.bits AND 2, 1, 0)
   AppendChildNode n, "style", style
  END WITH
 NEXT i
END SUB

SUB gamestate_slices_to_reload(byval parent as Reload.NodePtr)
 DIM gen_root as NodePtr = get_general_reld()
 DIM node as Reload.NodePtr
 node = SetChildNode(parent, "slices")

 IF gen_root."saved_games"."sprite_layer".exists THEN
  DIM spritelayernode as Reload.NodePtr
  spritelayernode = SetChildNode(node, "sprite_layer")
  SliceSaveToNode SliceTable.ScriptSprite, spritelayernode, YES  'Save slice handles
 END IF
END SUB

SUB gamestate_maps_to_reload(byval parent as Reload.NodePtr)
 DIM node as NodePtr
 node = SetChildNode(parent, "maps")
 DIM n as NodePtr 'used for numbered containers
 
 'FIXME: currently only supports saving the current map
 n = AppendChildNode(node, "map", gam.map.id)
 gamestate_npcs_to_reload n, gam.map.id
 
END SUB

SUB gamestate_npcs_to_reload(byval parent as Reload.NodePtr, byval map_id as integer)
 DIM node as NodePtr
 node = SetChildNode(parent, "npcs")

 DIM map_offset as XYPair
 map_offset = load_map_pos_save_offset(map_id)

 DIM n as NodePtr
 FOR i as integer = 0 TO UBOUND(npc)
  IF npc(i).id <> 0 ANDALSO NO THEN 'currently disabled for all NPCs
   n = AppendChildNode(node, "npc", i)
   save_npc_instance n, i, npc(i), map_offset
  END IF
 NEXT i
END SUB

SUB gamestate_tags_to_reload(byval parent as Reload.NodePtr)
 DIM node as NodePtr
 node = SetChildNode(parent, "tags")
 
 DIM count as integer = max_tag()+1
 SetChildNode(node, "count", count)
 
 DIM buf(count \ 16) as integer
 FOR i as integer = 0 TO count - 1
  setbit buf(), 0, i, readbit(tag(), 0, i)
 NEXT i
 
 DIM ch as NodePtr
 ch = SetChildNode(node, "data")
 SaveBitsetArray(ch, buf(), UBOUND(buf))
END SUB

SUB gamestate_onetime_to_reload(byval parent as Reload.NodePtr)
 DIM node as NodePtr
 node = SetChildNode(parent, "onetime")
 
 DIM count as integer = max_onetime + 1
 SetChildNode(node, "count", count)
 
 DIM buf(count \ 16) as integer
 FOR i as integer = 0 TO count - 1
  setbit buf(), 0, i, readbit(onetime(), 0, i)
 NEXT i
 
 DIM ch as NodePtr
 ch = SetChildNode(node, "data")
 SaveBitsetArray(ch, buf(), UBOUND(buf))
END SUB

'TODO: a lot of hero data should only be saved if it differs from the hero
'definition, so that it will update if the hero definition is edited.
SUB gamestate_party_to_reload(byval parent as Reload.NodePtr)
 DIM node as NodePtr
 node = SetChildNode(parent, "party")
 DIM slot as NodePtr
 DIM ch as NodePtr 'used for sub-containers
 DIM n as NodePtr 'used for numbered containers
 DIM her as HeroDef 'used for comparing current values to defaults (portrait)
 
 FOR i as integer = 0 TO 40
  slot = AppendChildNode(node, "slot", i)
  
  IF gam.hero(i).id >= 0 THEN AppendChildNode(slot, "id", gam.hero(i).id)
  AppendChildNode(slot, "name", gam.hero(i).name)
  
  IF gam.hero(i).locked THEN
   AppendChildNode(slot, "locked")
  END IF

  WITH gam.hero(i)
   
   ch = AppendChildNode(slot, "stats")
   FOR j as integer = 0 TO statLast
    WITH .stat
     IF .cur.sta(j) <> 0 OR .max.sta(j) <> 0 OR .base.sta(j) <> 0 THEN
      n = AppendChildNode(ch, "stat", j)
      AppendChildNode(n, "cur", .cur.sta(j))
      AppendChildNode(n, "max", .max.sta(j))
      AppendChildNode(n, "base", .base.sta(j))
     END IF
    END WITH
   NEXT j
   
   AppendChildNode(slot, "lev", .lev)
   AppendChildNode(slot, "lev_gain", .lev_gain)
   AppendChildNode(slot, "exp", .exp_cur)
   AppendChildNode(slot, "exp_next", .exp_next)
   AppendChildNode(slot, "exp_mult", .exp_mult)
   AppendChildNode(slot, "def_wep", .def_wep)
   AppendChildNodeBool(slot, "auto_battle", .auto_battle)

   IF gam.hero(i).id >= 0 THEN
    IF .hand_pos_overridden THEN
     ch = AppendChildNode(slot, "hand")
     FOR j as integer = 0 TO UBOUND(.hand_pos)
      n = AppendChildNode(ch, "frame", j)
      AppendChildNode(n, "x", .hand_pos(j).x)
      AppendChildNode(n, "y", .hand_pos(j).y)
     NEXT j
    END IF
   END IF

   ch = AppendChildNode(slot, "in_battle")
   AppendChildNode(ch, "pic", .battle_pic)
   AppendChildNode(ch, "pal", .battle_pal)
 
   ch = AppendChildNode(slot, "walkabout")
   AppendChildNode(ch, "pic", .pic)
   AppendChildNode(ch, "pal", .pal)

   IF gam.hero(i).id >= 0 THEN
    'Only save this node on non-empty slots
    ch = AppendChildNode(slot, "portrait")
    loadherodata her, gam.hero(i).id
    'Only save these nodes when they differ from the default for this hero
    IF .portrait_pic <> her.portrait    THEN AppendChildNode(ch, "pic", .portrait_pic)
    IF .portrait_pal <> her.portrait_pal THEN AppendChildNode(ch, "pal", .portrait_pal)
   END IF
   
   ch = AppendChildNode(slot, "spell_lists")
   FOR j as integer = 0 TO 3
    n = AppendChildNode(ch, "list", j)
    gamestate_spelllist_to_reload(i, j, n)
   NEXT j
   
   ch = AppendChildNode(slot, "level_mp")
   FOR j as integer = 0 TO 7
    IF max_levelmp_for_hero(gam.hero(i), j) <> 0 THEN
     SetKeyValueNode(ch, "lev", j, gam.hero(i).levelmp(j), "val")
    END IF
   NEXT j
  
   ch = AppendChildNode(slot, "equipment")
   FOR j as integer = 0 TO 4
    IF gam.hero(i).equip(j).id >= 0 THEN
     SetKeyValueNode(ch, "equip", j, gam.hero(i).equip(j).id, "item")
    END IF
   NEXT j
 
   IF gam.hero(i).id >= 0 THEN
    'Unlike all the other hero commands, hero elemental commands aren't allowed
    'to read/write empty hero slots, so don't need to save those
 
    ch = AppendChildNode(slot, "elements")
    FOR j as integer = 0 TO gen(genNumElements) - 1
     n = AppendChildNode(ch, "element", j)
     AppendChildNode(n, "damage", cast(double, .elementals(j)))
    NEXT j
   END IF
  END WITH
  
 NEXT i
END SUB

SUB gamestate_spelllist_to_reload(byval hero_slot as integer, byval spell_list as integer, byval parent as Reload.NodePtr)
 DIM node as NodePtr
 node = SetChildNode(parent, "spells", spell_list)
 DIM n as NodePtr 'used for numbered containers

 DIM atk_id as integer
 FOR i as integer = 0 TO 23
  atk_id = gam.hero(hero_slot).spells(spell_list, i) - 1
  IF atk_id >= 0 THEN
   n = AppendChildNode(node, "spell", i)
   AppendChildNode(n, "attack", atk_id)
  END IF
 NEXT i
 
END SUB

SUB gamestate_inventory_to_reload(byval parent as Reload.NodePtr)
 DIM node as NodePtr
 node = SetChildNode(parent, "inventory")
 DIM ch as NodePtr 'used for sub-containers
 DIM n as NodePtr 'used for numbered containers

 DIM last as integer = last_inv_slot()
 SetChildNode(node, "size", last)

 ch = SetChildNode(node, "slots")
 FOR i as integer = 0 TO last
  WITH inventory(i)
   IF .used THEN
    n = AppendChildNode(ch, "slot", i)
    AppendChildNode(n, "item", .id)
    AppendChildNode(n, "num", .num)
   END IF
  END WITH
 NEXT i
END SUB

SUB gamestate_shops_to_reload(byval parent as Reload.NodePtr)
 DIM node as NodePtr
 node = SetChildNode(parent, "shops")
 DIM ch as NodePtr 'used for sub-containers
 DIM n as NodePtr 'used for numbered containers
 DIM n2 as NodePtr 'also used for numbered containers

 DIM shoptmp(19) as integer
 
 FOR i as integer = 0 TO gen(genMaxShop)
  n = AppendChildNode(node, "shop", i)
  loadrecord shoptmp(), game & ".sho", 20, i
  ch = SetChildNode(n, "slots")
  FOR j as integer = 0 TO shoptmp(16)
   IF gam.stock(i, j) <> 0 THEN  'Has been initialised
    n2 = AppendChildNode(ch, "slot", j)
    AppendChildNode(n2, "stock", gam.stock(i, j))
    WITH gam.original_stock(i, j)
     IF .thingtype > -1 THEN  'Has been initialised
      AppendChildNode(n2, "orig_type",  .thingtype)
      AppendChildNode(n2, "orig_id",    .thingid)
      AppendChildNode(n2, "orig_stock", .stock)
     END IF
    END WITH
   END IF
  NEXT j
 NEXT i
 
END SUB

SUB gamestate_vehicle_to_reload(byval parent as Reload.NodePtr)
 DIM node as NodePtr
 node = SetChildNode(parent, "vehicle")
 DIM ch as NodePtr 'used for sub-containers
 DIM n as NodePtr 'used for numbered containers
 
 WITH vstate
  IF .id >= 0 THEN
   SetChildNode(node, "id", .id)
 
   ch = SetChildNode(node, "state")
   SetChildNode(ch, "active", .active)
   SetChildNode(ch, "npc", .npc)
   SetChildNode(ch, "old_speed", .old_speed)
   IF .mounting        THEN SetChildNode(ch, "mounting")
   IF .rising          THEN SetChildNode(ch, "rising")
   IF .falling         THEN SetChildNode(ch, "falling")
   IF .init_dismount   THEN SetChildNode(ch, "init_dismount")
   IF .trigger_cleanup THEN SetChildNode(ch, "trigger_cleanup")
   IF .ahead           THEN SetChildNode(ch, "ahead")
  END IF
 END WITH
END SUB

'-----------------------------------------------------------------------

SUB saveglobalvars (byval slot as integer, byval first as integer, byval last as integer)
 VAR prev_subtimer = main_timer.switch(TimerIDs.FileIO)

 current_save_slot = slot
 
 DIM filename as string
 filename = savedir & SLASH & slot & ".rsav"

 DIM doc as DocPtr
 DIM rsav_node as NodePtr
 DIM script_node as NodePtr
 DIM globals_node as NodePtr

 IF NOT isfile(filename) THEN
  debuginfo "Save file missing: " & filename
  debuginfo "generating a globals-only file"
  doc = CreateDocument()
  rsav_node = CreateNode(doc, "rsav")
  SetRootNode(doc, rsav_node)
  ' The RSAV doc must not have a "ver" node, so that the slot is considered
  ' unused by save/load menus.
  script_node = SetChildNode(rsav_node, "script")
  globals_node = SetChildNode(script_node, "globals")
 ELSE
  '--save already exists
  doc = LoadDocument(filename, optNoDelay)

  '--get the old globals node
  rsav_node = DocumentRoot(doc)
  script_node = rsav_node."script".ptr
  globals_node = script_node."globals".ptr
 END IF
 
 '--replace all global nodes withing the requested range
 gamestate_globals_to_reload script_node, first, last

 '--re-save with the changed globals
 SerializeBin filename, doc
 
 FreeDocument doc

#ifdef __FB_JS__
web_sync_persistent_storage()
#endif

 current_save_slot = -1

 main_timer.switch(prev_subtimer)
END SUB

'Load a range of global variables from a save slot
'Does nothing, without an error, on empty save slots
SUB new_loadglobalvars (byval slot as integer, byval first as integer, byval last as integer)
 DIM filename as string
 filename = savedir & SLASH & slot & ".rsav"
 IF NOT isfile(filename) THEN EXIT SUB

 DIM doc as DocPtr
 doc = LoadDocument(filename)

 DIM root as NodePtr
 root = DocumentRoot(doc)

 current_save_slot = slot

 'Can't check the RSAV version (root."ver"), because globals-only saves don't store the version number!

 '--load the globals in the range
 gamestate_globals_from_reload root."script".ptr, first, last
 
 FreeDocument doc
 current_save_slot = -1
END SUB

'-----------------------------------------------------------------------

SUB erase_save_slot (byval slot as integer)
 DIM filename as string
 filename = savedir & SLASH & slot & ".rsav"
 safekill filename
 old_erase_save_slot slot
END SUB

FUNCTION new_save_slot_used (byval slot as integer, prefix as string="") as bool
 DIM result as bool = NO

 IF prefix <> trimpath(prefix) THEN
  visible_debug "save_slot_used: save slot prefix may not include path"
  RETURN NO
 END IF

 DIM filename as string
 filename = savedir & SLASH & prefix & slot & ".rsav"
 
 IF isfile(filename) THEN
  DIM doc as DocPtr
  doc = LoadDocument(filename)
  DIM node as NodePtr
  node = DocumentRoot(doc)
  'Globals-only saves don't have a "ver" node
  IF node."ver".exists THEN
   result = YES
  END IF
  FreeDocument doc
 END IF
 
 RETURN result
END FUNCTION

FUNCTION new_count_used_save_slots() as integer
 DIM count as integer = 0
 
 DIM slot_count as integer = gen(genSaveSlotCount)
 IF slot_count = 0 THEN slot_count = 4
 FOR slot as integer = 0 TO slot_count - 1
  IF new_save_slot_used(slot) THEN count += 1
 NEXT slot
 
 RETURN count
END FUNCTION

'-----------------------------------------------------------------------

SUB new_get_save_slot_preview(byval slot as integer, pv as SaveSlotPreview)
 current_save_slot = slot

 pv.valid = NO

 DIM filename as string
 filename = savedir & SLASH & slot & ".rsav"

 IF NOT isfile(filename) THEN
  current_save_slot = -1
  EXIT SUB
 END IF

 DIM doc as DocPtr
 doc = LoadDocument(filename)
 
 DIM parent as NodePtr
 parent = DocumentRoot(doc)

 '--if there is no version data, don't continue
 ' (this could happen if export globals was used into an empty slot)
 IF parent."ver".exists = NO THEN
  FreeDocument doc
  current_save_slot = -1
  EXIT SUB
 END IF

 '--Loaded data okay! populate the SaveSlotPreview object
 pv.valid = YES

 pv.cur_map = parent."state"."current_map".warn

 DIM h as NodePtr
 DIM stat as NodePtr
 DIM foundleader as bool = NO
 FOR i as integer = 0 TO 3
  pv.hero_id(i) = 0
  h = NodeByPath(parent, "/party/slot[" & i & "]")
  IF h THEN
   IF h."id".exists THEN
    pv.hero_id(i) = h."id" + 1
    IF foundleader = NO THEN
     pv.leader_name = h."name".string
     pv.leader_lev = h."lev"
     foundleader = YES
    END IF
    WITH pv.hero(i)
     .lev = h."lev"
     FOR j as integer = 0 TO statLast
      stat = NodeByPath(h, "/stats/stat[" & j & "]")
      IF stat THEN
       .stat.cur.sta(j) = stat."cur"
       .stat.max.sta(j) = stat."max"
       'We don't load the base stats here: they're not needed, and we would have to
       'load equipment to compute them if they're missing
      END IF
     NEXT j
     .def_wep = h."def_wep"
     .battle_pic = h."in_battle"."pic"
     .battle_pal = h."in_battle"."pal"
     .pic = h."walkabout"."pic"
     .pal = h."walkabout"."pal"
    END WITH
   END IF
  END IF
 NEXT i
  
 DIM ch as NodePtr
 ch = parent."state"."playtime".ptr
 pv.playtime = playtime(ch."days", ch."hours", ch."minutes")
 
 FreeDocument doc
 current_save_slot = -1
END SUB

'-----------------------------------------------------------------------
'=======================================================================
'-----------------------------------------------------------------------

'Returns error message on an error
FUNCTION old_loadgame (byval slot as integer) as string
DIM gmaptmp(dimbinsize(binMAP)) as integer

DIM as integer i, j, o, z

loadrecord buffer(), old_savefile, 15000, slot * 2

DIM savver as integer = buffer(0)
IF savver = 0 THEN
 RETURN "Save slot is empty"
ELSEIF savver < 2 OR savver > 3 THEN
 RETURN "Unsupported or corrupt (SAV version " & savver & ")"
END IF
debuginfo "loading from slot " & slot & " of old " & old_savefile

gam.map.id = buffer(1)
IF buffer(1) > gen(genMaxMap) THEN
 visible_debug "This save file is corrupt: it is saved on nonexistent map " & gam.map.id
 gam.map.id = 0
END IF

loadrecord gmaptmp(), game + ".map", getbinsize(binMAP) \ 2, gam.map.id
(herox(0)) = buffer(2) + gmaptmp(20) * 20
(heroy(0)) = buffer(3) + gmaptmp(21) * 20
(herodir(0)) = buffer(4)
gam.random_battle_countdown = buffer(5)
'leader = buffer(6)
mapx = buffer(7)
mapy = buffer(8)

DIM gold_str as string = ""
FOR i = 0 TO 24
 IF buffer(i + 9) < 0 OR buffer(i + 9) > 255 THEN buffer(i + 9) = 0
 IF buffer(i + 9) > 0 THEN gold_str &= CHR(buffer(i + 9))
NEXT i
gold = str2int(gold_str)

'--Load parts of gen().
'--Note that gen() has already been reloaded from .gen by resetgame.
z = 34
show_load_index z, "gen"
FOR i = 0 TO 500
 SELECT CASE i
  'Only certain gen() values should be read from the saved game.
  'See http://rpg.HamsterRepublic.com/ohrrpgce/GEN
  CASE 42, 57 'genGameoverScript and genLoadgameScript
   IF prefbit(18) = NO THEN  '"Don't save gameover/loadgame script IDs" off
    gen(i) = buffer(z)
   END IF
  CASE 44 TO 54, 58, 60 TO 76, 85
   gen(i) = buffer(z)
 END SELECT
 z = z + 1
NEXT i

show_load_index z, "npcl"
DeserNPCL npc(),z,buffer(),300,gmaptmp(20),gmaptmp(21)
show_load_index z, "unused"
z=z+1 'fix an old bug

show_load_index z, "tags"
DIM tagtemp(126) as integer
FOR i = 0 TO 126
 tagtemp(i) = buffer(z): z = z + 1
NEXT i
FOR i = 0 TO 999
 'copy over the tags (only to 999 because the max was hardcoded in the old format)
 setbit tag(), 0, i, readbit(tagtemp(), 0, i)
NEXT i
FOR i = 0 TO 1031
 'copy over the onetime bits (only to 1031 because that max was hardcoded in the old format)
 setbit onetime(), 0, i, readbit(tagtemp(), 0, i + 1000)
NEXT i
show_load_index z, "heroes"
FOR i = 0 TO 40
 DIM who as integer = buffer(z) - 1: z = z + 1
 IF who > -1 THEN addhero who, i, , NO, YES  'allow_rename=NO, loading=YES
NEXT i
show_load_index z, "unused a"
FOR i = 0 TO 500
 '--used to be the useless a() buffer
 z = z + 1
NEXT i
show_load_index z, "stats"
FOR i = 0 TO 40
 FOR j = 0 TO 11  'statLast
  gam.hero(i).stat.cur.sta(j) = buffer(z): z = z + 1
 NEXT j
 gam.hero(i).lev = buffer(z): z = z + 1
 z = z + 1  'Used to be hero weapon pic (.wep_pic)
 FOR j = 0 TO 11  'statLast
  gam.hero(i).stat.max.sta(j) = buffer(z): z = z + 1
 NEXT j
 gam.hero(i).lev_gain = buffer(z): z = z + 1
 z = z + 1  'Used to be hero weapon palette (.wep_pal)
NEXT i
show_load_index z, "bmenu"
FOR i = 0 TO 40
 FOR o = 0 TO 5
  'this was the obsolete bmenu() array
  z = z + 1
 NEXT o
NEXT i
show_load_index z, "spell"
FOR i = 0 TO 40
 FOR o = 0 TO 3
  FOR j = 0 TO 23
   gam.hero(i).spells(o, j) = buffer(z): z = z + 1
  NEXT j
  z = z + 1'--skip extra data
 NEXT o
NEXT i
show_load_index z, "lmp"
FOR i = 0 TO 40
 FOR o = 0 TO 7
  gam.hero(i).levelmp(o) = buffer(z): z = z + 1
 NEXT o
NEXT i
show_load_index z, "exlev"
DIM exp_str as string
FOR i = 0 TO 40
 FOR o = 0 TO 1
  exp_str = ""
  FOR j = 0 TO 25
   IF buffer(z) < 0 OR buffer(z) > 255 THEN buffer(z) = 0
   IF buffer(z) > 0 THEN exp_str &= CHR(buffer(z))
   z = z + 1
  NEXT j
  IF o = 0 THEN gam.hero(i).exp_cur = str2int(exp_str)
  IF o = 1 THEN gam.hero(i).exp_next = str2int(exp_str)
 NEXT o
NEXT i
show_load_index z, "names"
FOR i = 0 TO 40
 gam.hero(i).name = ""
 FOR j = 0 TO 16
  IF buffer(z) < 0 OR buffer(z) > 255 THEN buffer(z) = 0
  IF buffer(z) > 0 THEN gam.hero(i).name &= CHR(buffer(z))
  z = z + 1
 NEXT j
NEXT i

show_load_index z, "inv_mode"
DIM inv_mode as integer
inv_mode = buffer(z)
show_load_index z, "inv 8bit"
IF inv_mode = 0 THEN ' Read 8-bit inventory data from old SAV files
 DeserInventory8Bit inventory(), z, buffer()
ELSE
 'Skip this section
 z = 14595
END IF

show_load_index z, "eqstuff"
FOR i = 0 TO 40
 FOR o = 0 TO 4
  gam.hero(i).equip(o).id = buffer(z) - 1: z = z + 1
 NEXT o
NEXT i

show_load_index z, "inv 16bit"
IF inv_mode = 1 THEN ' Read 16-bit inventory data from newer SAV files
 LoadInventory16Bit inventory(), z, buffer(), 0, 99
END IF
show_load_index z, "after inv 16bit"

'RECORD 2

loadrecord buffer(), old_savefile, 15000, slot * 2 + 1

z = 0

show_load_index z, "stock", 1
FOR i = 0 TO 99
 FOR o = 0 TO 49
  IF i <= gen(genMaxShop) THEN
   gam.stock(i, o) = buffer(z)
  END IF
  z = z + 1
 NEXT o
NEXT i
show_load_index z, "hero lock bits", 1
DIM hmask_temp(2) as integer
FOR i = 0 TO 3
 IF i <= UBOUND(hmask_temp) THEN
  hmask_temp(i) = buffer(z)
 END IF
 z = z + 1
NEXT i
FOR i = 0 TO 40
 gam.hero(i).locked = xreadbit(hmask_temp(), i)
NEXT i
show_load_index z, "cathero", 1
FOR i = 1 TO 3
 (herox(i)) = buffer(z) + gmaptmp(20) * 20: z = z + 1
 (heroy(i)) = buffer(z) + gmaptmp(21) * 20: z = z + 1
 (herodir(i)) = buffer(z): z = z + 1
NEXT i
show_load_index z, "globals low", 1
FOR i = 0 TO 1024
 global(i) = (buffer(z) AND &hFFFF): z = z + 1
NEXT i
show_load_index z, "vstate", 1
WITH vstate
 .active = buffer(z+0) <> 0
 .npc    = buffer(z+5)
 .id     = -1
 ' We set .id by looking at .npc's definition. But we can't do that here, npc() isn't loaded until preparemap
 .mounting        = xreadbit(buffer(), 0, z+6)
 .rising          = xreadbit(buffer(), 1, z+6)
 .falling         = xreadbit(buffer(), 2, z+6)
 .init_dismount   = xreadbit(buffer(), 3, z+6)
 .trigger_cleanup = xreadbit(buffer(), 4, z+6)
 .ahead           = xreadbit(buffer(), 5, z+6)
 .old_speed = buffer(z+7)
 WITH .dat
  .speed = buffer(z+8)
  .pass_walls = xreadbit(buffer(), 0, z+9)
  .pass_npcs  = xreadbit(buffer(), 1, z+9)
  .enable_npc_activation = xreadbit(buffer(), 2, z+9)
  .enable_door_use       = xreadbit(buffer(), 3, z+9)
  .do_not_hide_leader    = xreadbit(buffer(), 4, z+9)
  .do_not_hide_party     = xreadbit(buffer(), 5, z+9)
  .dismount_ahead        = xreadbit(buffer(), 6, z+9)
  .pass_walls_while_dismounting = xreadbit(buffer(), 7, z+9)
  .disable_flying_shadow        = xreadbit(buffer(), 8, z+9)
  .random_battles = buffer(z+11)
  .use_button     = buffer(z+12)
  .menu_button    = buffer(z+13)
  .riding_tag     = buffer(z+14)
  .on_mount       = buffer(z+15)
  .on_dismount    = buffer(z+16)
  .override_walls = buffer(z+17)
  .blocked_by     = buffer(z+18)
  .mount_from     = buffer(z+19)
  .dismount_to    = buffer(z+20)
  .elevation      = buffer(z+21)
 END WITH
END WITH
z += 22
'--picture and palette
show_load_index z, "picpal magic", 1
DIM picpalmagicnum as integer = buffer(z): z = z + 1
show_load_index z, "picpalwep", 1
FOR i = 0 TO 40
 IF picpalmagicnum = 4444 THEN
  gam.hero(i).battle_pic = buffer(z)
  gam.hero(i).battle_pal = buffer(z+1)
  gam.hero(i).def_wep = buffer(z+2)
  gam.hero(i).pic = buffer(z+3)
  gam.hero(i).pal = buffer(z+4)
 END IF
 z = z + 6
NEXT i
'native hero bitsets
show_load_index z, "hbit magic", 1
DIM nativebitmagicnum as integer = buffer(z): z = z + 1
show_load_index z, "hbits", 1
'Just totally ignore all the hero bits, as none of them are/were modifiable anyway;
'nativehbits() was removed
z += 205
'top global variable bits
show_load_index z, "global high", 1
FOR i = 0 TO 1024
 global(i) or= buffer(z) shl 16: z = z + 1
NEXT i
show_load_index z, "global ext", 1
FOR i = 1025 TO 4095 'maxScriptGlobals const is NOT appropriate here
 global(i) = buffer(z) and &hFFFF: z = z + 1
 global(i) or= buffer(z) shl 16: z = z + 1
NEXT i
show_load_index z, "inv 16bit ext", 1
IF inv_mode = 1 THEN ' Read 16-bit inventory data from newer SAV files
 IF inventoryMax <> 599 THEN debug "Warning: inventoryMax=" & inventoryMax & ", does not fit in old SAV format"
 LoadInventory16Bit inventory(), z, buffer(), 100, 599
ELSE
 'skip this section for old saves
 z = 29680 - 15000
END IF
show_load_index z, "unused", 1
rebuild_inventory_captions inventory()

'---BLOODY BACKWARD COMPATABILITY---
'fix doors... 'James guesses that "fix doors" is a thing that we need to do, but are not doing yet?
IF savver = 2 THEN gen(genVersion) = 3

'Here we load hero state that is currently in RSAV files but that was
'not stored in the old .SAV format
DIM her as HeroDef

FOR i = 0 TO 40
 IF gam.hero(i).id >= 0 THEN
  loadherodata her, gam.hero(i).id

  gam.hero(i).rename_on_status = xreadbit(her.bits(), 25)

  '--fix appearance settings
  IF picpalmagicnum <> 4444 THEN
   gam.hero(i).battle_pic = her.sprite
   gam.hero(i).battle_pal = her.sprite_pal
   gam.hero(i).pic = her.walk_sprite
   gam.hero(i).pal = her.walk_sprite_pal
   gam.hero(i).def_wep = her.def_weapon + 1'default weapon
  END IF

  FOR j = 0 TO gen(genNumElements) - 1
   gam.hero(i).elementals(j) = her.elementals(j)
  NEXT

  FOR j = 0 TO 1
   gam.hero(i).hand_pos(j) = her.hand_pos(j)
  NEXT j
  gam.hero(i).hand_pos_overridden = YES  'Don't know, be cautious
 END IF

 compute_hero_base_stats_from_max i
NEXT i

'See http://rpg.hamsterrepublic.com/ohrrpgce/SAV for docs
END FUNCTION

'Load a range of global variables from a save slot
'Does nothing, without an error, on empty save slots
SUB old_loadglobalvars (byval slot as integer, byval first as integer, byval last as integer)
DIM i as integer
'SAV only stores 4096 globals
last = small(last, 4095)

DIM buf((last - first + 1) * 2) as integer = ANY
IF isfile(old_savefile) THEN
 'debuginfo "loadglobalvars from slot " & slot & " of boring old " & old_savefile

 DIM fh as integer
 OPENFILE old_savefile, FOR_BINARY + ACCESS_READ, fh

 IF first <= 1024 THEN
  'grab first-final
  DIM final as integer = small(1024, last)
  SEEK #fh, 60000 * slot + 2 * first + 40027  '20013 * 2 + 1
  loadrecord buf(), fh, final - first + 1, -1
  FOR i = 0 TO final - first
   global(first + i) = buf(i) and &hFFFF
  NEXT
  SEEK #fh, 60000 * slot + 2 * first + 43027  '21513 * 2 + 1
  loadrecord buf(), fh, final - first + 1, -1
  FOR i = 0 TO final - first
   global(first + i) or= buf(i) shl 16
  NEXT
 END IF
 IF last >= 1025 THEN
  'grab start-last
  DIM start as integer = large(1025, first)
  SEEK #fh, 60000 * slot + 4 * (start - 1025) + 45077  '22538 * 2 + 1
  loadrecord buf(), fh, (last - start + 1) * 2, -1
  FOR i = 0 TO last - start
   global(start + i) = buf(i * 2) and &hFFFF
   global(start + i) or= buf(i * 2 + 1) shl 16
  NEXT
 END IF

 CLOSE #fh
END IF
END SUB

SUB show_load_index(byval z as integer, caption as string, byval slot as integer=0)
 'debug "SAV:" & LEFT(caption & STRING(20, " "), 20) & " int=" & z + slot * 15000
END SUB

SUB rebuild_inventory_captions (invent() as InventSlot)
 DIM i as integer
 FOR i = 0 TO inventoryMax
  update_inventory_caption i
 NEXT i
END SUB

SUB old_get_save_slot_preview(byval slot as integer, pv as SaveSlotPreview)

 loadrecord buffer(), old_savefile, 15000, slot * 2, NO
 
 IF buffer(0) <> 3 THEN
  '--currently only understands v3 binary sav format
  pv.valid = NO
  EXIT SUB
 END IF
 
 pv.valid = YES
 pv.cur_map = buffer(1)

 '-get stats
 DIM z as integer = 3305
 FOR i as integer = 0 TO 3
  FOR j as integer = 0 TO 11 'statLast
   pv.hero(i).stat.cur.sta(j) = buffer(z): z += 1
  NEXT j
  pv.hero(i).lev = buffer(z): z += 1
  z += 1 'skip weapon pic because we could care less right now
  FOR j as integer = 0 TO 11 'statLast
   pv.hero(i).stat.max.sta(j) = buffer(z): z += 1
  NEXT j
  'base stats aren't loaded or computed: not needed
 NEXT i

 '--get play time
 z = 34 + 51
 pv.playtime = playtime(buffer(z), buffer(z + 1), buffer(z + 2))

 '--leader data
 DIM foundleader as bool = NO
 pv.leader_name = ""
 FOR o as integer = 0 TO 3
  '--load hero ID
  pv.hero_id(o) = buffer(2763 + o)
  '--leader name and level
  IF foundleader = NO AND pv.hero_id(o) > 0 THEN
   foundleader = YES
   FOR j as integer = 0 TO 15
    DIM k as integer = buffer(11259 + (o * 17) + j)
    IF k > 0 AND k < 255 THEN pv.leader_name &= CHR(k)
   NEXT j
   pv.leader_lev = pv.hero(o).lev
  END IF
 NEXT o

 '--load second record
 loadrecord buffer(), old_savefile, 15000, slot * 2 + 1

 z = 6060
 DIM use_saved_pics as bool = NO
 IF buffer(z) = 4444 THEN use_saved_pics = YES
 z += 1
 FOR i as integer = 0 TO 3
  IF use_saved_pics THEN
   pv.hero(i).battle_pic = buffer(z)
   pv.hero(i).battle_pal = buffer(z+1)
   pv.hero(i).def_wep = buffer(z+2)
   pv.hero(i).pic = buffer(z+3)
   pv.hero(i).pal = buffer(z+4)
   z += 6
  ELSE
   '--backcompat (for ancient SAV files)
   IF pv.hero_id(i) > 0 THEN
    DIM her as HeroDef
    loadherodata her, pv.hero_id(i) - 1
    pv.hero(i).battle_pic = her.sprite
    pv.hero(i).battle_pal = her.sprite_pal
    pv.hero(i).pic = her.walk_sprite
    pv.hero(i).pal = her.walk_sprite_pal
    pv.hero(i).def_wep = her.def_weapon + 1
   END IF
  END IF
 NEXT i

END SUB

FUNCTION old_save_slot_used (byval slot as integer) as bool
 IF isfile(old_savefile) = 0 THEN RETURN NO
 DIM as short saveversion
 DIM savh as integer
 OPENFILE old_savefile, FOR_BINARY + ACCESS_READ, savh
 GET #savh, 1 + 60000 * slot, saveversion
 CLOSE #savh
 RETURN (saveversion = 3)
END FUNCTION

SUB old_erase_save_slot (byval slot as integer)
 'Unlike erase_save_slot, this leaves the global variables intact for importglobals.
 'It seems that no game was affected by that back-compat break.
 DIM as short saveversion = 0
 IF fileisreadable(old_savefile) = NO THEN EXIT SUB
 DIM savh as integer
 OPENFILE old_savefile, FOR_BINARY + ACCESS_READ_WRITE, savh
 IF LOF(savh) > 60000 * slot THEN
  PUT #savh, 1 + 60000 * slot, saveversion
 END IF
 CLOSE #savh
END SUB

FUNCTION old_count_used_save_slots() as integer
 DIM i as integer
 DIM n as integer
 DIM savver as integer
 n = 0
 FOR i = 0 TO 3
  loadrecord buffer(), old_savefile, 15000, i * 2, NO
  savver = buffer(0)
  IF savver = 3 THEN n += 1
 NEXT i
 RETURN n
END FUNCTION

'-----------------------------------------------------------------------
'Helper functions for reading specific data from save slots without loading

FUNCTION saveslot_quick_root_node(byval saveslot as integer) as NodePtr
 'saveslot is the 0-based index of the save slot, 0-maxSaveSlotCount-1
 IF saveslot < 0 ORELSE saveslot > maxSaveSlotCount-1 THEN
  debuginfo "saveslot_quick_load_node: called on invalid save slot " & saveslot
  RETURN 0
 END IF

 DIM filename as string
 filename = savedir & SLASH & saveslot & ".rsav"

 IF NOT isfile(filename) THEN
  'It is normal for this to be called on all save slots, including the empty ones
  ' so just silently return 0
  RETURN 0
 END IF

 DIM doc as DocPtr
 doc = LoadDocument(filename)
 
 DIM node as NodePtr
 node = DocumentRoot(doc)
 RETURN node
END FUNCTION

FUNCTION saveslot_findhero(byval saveroot as NodePtr, byval id as integer) as integer
 'id is the real ID number
 FOR i as integer = 0 to sizeParty - 1
  DIM node as NodePtr = NodeByPath(saveroot, "/party/slot[" & i & "]/id")
  IF node THEN
   IF GetInteger(node) = id THEN RETURN i
  END IF
 NEXT i
 RETURN -1 'not found
END FUNCTION

FUNCTION saveslot_rank_to_party_slot (byval saveroot as NodePtr, byval rank as integer) as integer
 'Returns the party slot of the nth hero in the party
 'This is used for converting caterpillar rank into party slot.
 'Returns -1 if there are not that many heroes in the active party
 DIM heronum as integer = -1
 FOR party_slot as integer = 0 TO 3
  IF saveslot_hero_id_by_slot(saveroot, party_slot) >= 0 THEN heronum += 1
  IF heronum = rank THEN
   RETURN party_slot
  END IF
 NEXT
 RETURN -1
END FUNCTION

FUNCTION saveslot_hero_name_by_slot(byval saveroot as NodePtr, byval slot as integer) as string
 IF saveslot_hero_id_by_slot(saveroot, slot) = -1 THEN RETURN "" 'Slot is empty
 DIM node as NodePtr = NodeByPath(saveroot, "/party/slot[" & slot & "]/name")
 IF node THEN
  RETURN GetString(node)
 END IF
 RETURN "" 'not found
END FUNCTION

FUNCTION saveslot_hero_id_by_slot(byval saveroot as NodePtr, byval slot as integer) as integer
 DIM node as NodePtr = NodeByPath(saveroot, "/party/slot[" & slot & "]/id")
 IF node THEN
  RETURN GetInteger(node)
 END IF
 RETURN -1 'not found
END FUNCTION

FUNCTION saveslot_global(byval saveroot as NodePtr, byval global_id as integer) as integer
 'See also loadglobalvars which is more complicated, but similar
 DIM node as NodePtr = NodeByPath(saveroot, "/script/globals/global[" & global_id & "]/int")
 IF node THEN
  RETURN GetInteger(node)
 END IF
 RETURN 0 'not found
END FUNCTION

FUNCTION saveslot_plotstr(byval saveroot as NodePtr, byval string_id as integer) as string
 DIM node as NodePtr = NodeByPath(saveroot, "/script/strings/string[" & string_id & "]/str")
 IF node THEN
  RETURN GetString(node)
 END IF
 RETURN "" 'not found
END FUNCTION

