'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) 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 ' 'FIXME: this isn't saved yet because we can't load it yet ' 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) SetChildNode 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 299 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 SetChildNode(slot, "id", gam.hero(i).id) SetChildNode(slot, "name", gam.hero(i).name) IF gam.hero(i).locked THEN SetChildNode(slot, "locked") END IF WITH gam.hero(i) ch = SetChildNode(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) SetChildNode(n, "cur", .cur.sta(j)) SetChildNode(n, "max", .max.sta(j)) SetChildNode(n, "base", .base.sta(j)) END IF END WITH NEXT j SetChildNode(slot, "lev", .lev) SetChildNode(slot, "lev_gain", .lev_gain) SetChildNode(slot, "exp", .exp_cur) SetChildNode(slot, "exp_next", .exp_next) SetChildNode(slot, "exp_mult", .exp_mult) SetChildNode(slot, "def_wep", .def_wep) SetChildNodeBool(slot, "auto_battle", .auto_battle) IF gam.hero(i).id >= 0 THEN IF .hand_pos_overridden THEN ch = SetChildNode(slot, "hand") FOR j as integer = 0 TO UBOUND(.hand_pos) n = AppendChildNode(ch, "frame", j) SetChildNode(n, "x", .hand_pos(j).x) SetChildNode(n, "y", .hand_pos(j).y) NEXT j END IF END IF ch = SetChildNode(slot, "in_battle") SetChildNode(ch, "pic", .battle_pic) SetChildNode(ch, "pal", .battle_pal) ch = SetChildNode(slot, "walkabout") SetChildNode(ch, "pic", .pic) SetChildNode(ch, "pal", .pal) IF gam.hero(i).id >= 0 THEN 'Only save this node on non-empty slots ch = SetChildNode(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 SetChildNode(ch, "pic", .portrait_pic) IF .portrait_pal <> her.portrait_pal THEN SetChildNode(ch, "pal", .portrait_pal) END IF ch = SetChildNode(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 = SetChildNode(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 = SetChildNode(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 = SetChildNode(slot, "elements") FOR j as integer = 0 TO gen(genNumElements) - 1 n = AppendChildNode(ch, "element", j) SetChildNode(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) SetChildNode(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) SetChildNode(n, "item", .id) SetChildNode(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) SetChildNode(n2, "stock", gam.stock(i, j)) WITH gam.original_stock(i, j) IF .thingtype > -1 THEN 'Has been initialised SetChildNode(n2, "orig_type", .thingtype) SetChildNode(n2, "orig_id", .thingid) SetChildNode(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