# Auto Multi Save by http404error # For Pokemon Essentials v19.1 # Description: # Adds multiple save slots and the abliity to auto-save. # Included is code to autosave every 30 overworld steps. Feel free to edit or delete it (it's right at the top). # On the Load screen you can use the left and right buttons while "Continue" is selected to cycle through files. # When saving, you can quickly save to the same slot you loaded from, or pick another slot. # Battle Challenges are NOT supported. # Customization: # I recommend altering your pause menu to quit to the title screen or load screen instead of exiting entirely. # -> For instance, just change the menu text to "Quit to Title" and change `$scene = nil` to `$scene = pbCallTitle`. # Call Game.auto_save whenever you want. # -> Autosaving during an event script will correctly resume event execution when you load the game. # -> I haven't investigated if it might be possible to autosave on closing the window with the X or Alt-F4 yet. # You can rename the slots to your liking, or change how many there are. # In some cases, you might want to remove the option to save to a different slot than the one you loaded from. # Notes: # On the first Load, the old Game.rxdata will be copied to the first slot in MANUAL_SLOTS. It won't have a known save time though. # The interface to `Game.save` has been changed. # Due to the slots, alters the save backup system in the case of save corruption/crashes - backups will be named Backup000.rxdata and so on. # Heavily modifies the SaveData module and Save and Load screens. This may cause incompatibility with some other plugins or custom game code. # Not everything here has been tested extensively, only what applies to normal usage of my game. Please let me know if you run into any problems. # Future development ideas: # There isn't currently support for unlimited slots but it wouldn't be too hard. # Letting the user name their slots seems cool. # It would be nice if there was a sliding animation for switching files on that load screen. :) # It would be nice if the file select arrows used nicer animated graphics, kind of like the Bag. # Maybe auto-save slots should act like a queue instead of cycling around. # Autosave every 30 steps # Events.onStepTaken += proc { # $Trainer.autosave_steps = 0 if !$Trainer.autosave_steps # $Trainer.autosave_steps += 1 # if $Trainer.autosave_steps >= 30 # echo("Autosaving...") # $Trainer.autosave_steps = 0 # Game.auto_save # echoln("done.") # end # } #=============================================================================== # #=============================================================================== module SaveData # You can rename these slots or change the amount of them # They change the actual save file names though, so it would take some extra work to use the translation system on them. AUTO_SLOTS = [ 'Auto 1', 'Auto 2' ] MANUAL_SLOTS = [ 'File A', 'File B', 'File C', 'File D', 'File E', 'File F', 'File G', 'File H' ] # For compatibility with games saved without this plugin OLD_SAVE_SLOT = 'Game' SAVE_DIR = if File.directory?(System.data_directory) System.data_directory else '.' end def self.each_slot (AUTO_SLOTS + MANUAL_SLOTS).each { |f| yield f } end def self.get_full_path(file) return "#{SAVE_DIR}/#{file}.rxdata" end def self.get_backup_file_path backup_file = "Backup000" while File.file?(self.get_full_path(backup_file)) backup_file.next! end return self.get_full_path(backup_file) end # Given a list of save file names and a file name in it, return the next file after it which exists # If no other file exists, will just return the same file again def self.get_next_slot(file_list, file) old_index = file_list.find_index(file) ordered_list = file_list.rotate(old_index + 1) ordered_list.each do |f| return f if File.file?(self.get_full_path(f)) end # should never reach here since the original file should always exist return file end # See self.get_next_slot def self.get_prev_slot(file_list, file) return self.get_next_slot(file_list.reverse, file) end # Returns nil if there are no saves # Returns the first save if there's a tie for newest # Old saves from previous version don't store their saved time, so are treated as very old def self.get_newest_save_slot newest_time = Time.at(0) # the Epoch newest_slot = nil self.each_slot do |file_slot| full_path = self.get_full_path(file_slot) next if !File.file?(full_path) temp_save_data = self.read_from_file(full_path) save_time = temp_save_data[:player].last_time_saved || Time.at(1) if save_time > newest_time newest_time = save_time newest_slot = file_slot end end # Port old save if newest_slot.nil? && File.file?(self.get_full_path(OLD_SAVE_SLOT)) file_copy(self.get_full_path(OLD_SAVE_SLOT), self.get_full_path(MANUAL_SLOTS[0])) return MANUAL_SLOTS[0] end return newest_slot end # @return [Boolean] whether any save file exists def self.exists? self.each_slot do |slot| full_path = SaveData.get_full_path(slot) return true if File.file?(full_path) end return false end # This is used in a hidden function (ctrl+down+cancel on title screen) or if the save file is corrupt # Pass nil to delete everything, or a file path to just delete that one # @raise [Error::ENOENT] def self.delete_file(file_path=nil) if file_path File.delete(file_path) if File.file?(file_path) else self.each_slot do |slot| full_path = self.get_full_path(slot) File.delete(full_path) if File.file?(full_path) end end end # Moves a save file from the old Saved Games folder to the new # location specified by {MANUAL_SLOTS[0]}. Does nothing if a save file # already exists in {MANUAL_SLOTS[0]}. def self.move_old_windows_save return if self.exists? game_title = System.game_title.gsub(/[^\w ]/, '_') home = ENV['HOME'] || ENV['HOMEPATH'] return if home.nil? old_location = File.join(home, 'Saved Games', game_title) return unless File.directory?(old_location) old_file = File.join(old_location, 'Game.rxdata') return unless File.file?(old_file) File.move(old_file, MANUAL_SLOTS[0]) end # Runs all possible conversions on the given save data. # Saves a backup before running conversions. # @param save_data [Hash] save data to run conversions on # @return [Boolean] whether conversions were run def self.run_conversions(save_data) validate save_data => Hash conversions_to_run = self.get_conversions(save_data) return false if conversions_to_run.none? File.open(SaveData.get_backup_file_path, 'wb') { |f| Marshal.dump(save_data, f) } echoln "Backed up save to #{SaveData.get_backup_file_path}" echoln "Running #{conversions_to_run.length} conversions..." conversions_to_run.each do |conversion| echo "#{conversion.title}..." conversion.run(save_data) echoln ' done.' end echoln '' if conversions_to_run.length > 0 save_data[:essentials_version] = Essentials::VERSION save_data[:game_version] = Settings::GAME_VERSION return true end end #=============================================================================== # #=============================================================================== class PokemonLoad_Scene def pbChoose(commands, continue_idx) @sprites["cmdwindow"].commands = commands loop do Graphics.update Input.update pbUpdate if Input.trigger?(Input::USE) return @sprites["cmdwindow"].index elsif @sprites["cmdwindow"].index == continue_idx @sprites["leftarrow"].visible=true @sprites["rightarrow"].visible=true if Input.trigger?(Input::LEFT) return -3 elsif Input.trigger?(Input::RIGHT) return -2 end else @sprites["leftarrow"].visible=false @sprites["rightarrow"].visible=false end end end def pbStartScene(commands, show_continue, trainer, frame_count, map_id) @commands = commands @sprites = {} @viewport = Viewport.new(0, 0, Graphics.width, Graphics.height) @viewport.z = 99998 addBackgroundOrColoredPlane(@sprites,"background","loadbg",Color.new(248,248,248),@viewport) @sprites["leftarrow"] = AnimatedSprite.new("Graphics/Pictures/leftarrow",8,40,28,2,@viewport) @sprites["leftarrow"].x=10 @sprites["leftarrow"].y=140 @sprites["leftarrow"].play #@sprites["leftarrow"].visible=true @sprites["rightarrow"] = AnimatedSprite.new("Graphics/Pictures/rightarrow",8,40,28,2,@viewport) @sprites["rightarrow"].x = 460 @sprites["rightarrow"].y = 140 @sprites["rightarrow"].play #@sprites["rightarrow"].visible=true y = 16*2 for i in 0...commands.length @sprites["panel#{i}"] = PokemonLoadPanel.new(i,commands[i], (show_continue) ? (i==0) : false,trainer,frame_count,map_id,@viewport) @sprites["panel#{i}"].x = 24*2 @sprites["panel#{i}"].y = y @sprites["panel#{i}"].pbRefresh y += (show_continue && i==0) ? 112*2 : 24*2 end @sprites["cmdwindow"] = Window_CommandPokemon.new([]) @sprites["cmdwindow"].viewport = @viewport @sprites["cmdwindow"].visible = false end end #=============================================================================== # #=============================================================================== class PokemonLoadScreen def initialize(scene) @scene = scene @selected_file = SaveData.get_newest_save_slot end # @param file_path [String] file to load save data from # @return [Hash] save data def load_save_file(file_path) save_data = SaveData.read_from_file(file_path) unless SaveData.valid?(save_data) if File.file?(file_path + ".bak") pbMessage(_INTL("The save file is corrupt. A backup will be loaded.")) save_data = load_save_file(file_path + ".bak") else self.prompt_save_deletion(file_path) return {} end end return save_data end # Called if save file is invalid. # Prompts the player to delete the save files. def prompt_save_deletion(file_path) pbMessage(_INTL("A save file is corrupt, or is incompatible with this game.")) self.delete_save_data(file_path) if pbConfirmMessageSerious( _INTL("Do you want to delete that save file? The game will exit afterwards either way.") ) exit end # nil deletes all, otherwise just the given file def delete_save_data(file_path=nil) begin SaveData.delete_file(file_path) pbMessage(_INTL("The save data was deleted.")) rescue SystemCallError pbMessage(_INTL("The save data could not be deleted.")) end end def pbStartLoadScreen copyKeybindings() save_file_list = SaveData::AUTO_SLOTS + SaveData::MANUAL_SLOTS first_time = true loop do # Outer loop is used for switching save files if @selected_file @save_data = load_save_file(SaveData.get_full_path(@selected_file)) else @save_data = {} end commands = [] cmd_continue = -1 cmd_new_game = -1 cmd_options = -1 cmd_language = -1 cmd_mystery_gift = -1 cmd_debug = -1 cmd_quit = -1 show_continue = !@save_data.empty? new_game_plus = show_continue && (@save_data[:player].new_game_plus_unlocked || $DEBUG) if show_continue commands[cmd_continue = commands.length] = "#{@selected_file}" if @save_data[:player].mystery_gift_unlocked commands[cmd_mystery_gift = commands.length] = _INTL('Mystery Gift') # Honestly I have no idea how to make Mystery Gift work well with this. end end commands[cmd_new_game = commands.length] = _INTL('New Game') if new_game_plus commands[cmd_new_game_plus = commands.length] = _INTL('New Game +') end commands[cmd_options = commands.length] = _INTL('Options') commands[cmd_language = commands.length] = _INTL('Language') if Settings::LANGUAGES.length >= 2 commands[cmd_debug = commands.length] = _INTL('Debug') if $DEBUG commands[cmd_quit = commands.length] = _INTL('Quit Game') cmd_left = -3 cmd_right = -2 map_id = show_continue ? @save_data[:map_factory].map.map_id : 0 @scene.pbStartScene(commands, show_continue, @save_data[:player], @save_data[:frame_count] || 0, map_id) @scene.pbSetParty(@save_data[:player]) if show_continue if first_time @scene.pbStartScene2 first_time = false else @scene.pbUpdate end loop do # Inner loop is used for going to other menus and back and stuff (vanilla) command = @scene.pbChoose(commands, cmd_continue) pbPlayDecisionSE if command != cmd_quit case command when cmd_continue @scene.pbEndScene Game.load(@save_data) return when cmd_new_game @scene.pbEndScene Game.start_new return when cmd_new_game_plus @scene.pbEndScene Game.start_new(@save_data[:bag],@save_data[:storage_system],@save_data[:player]) @save_data[:player].new_game_plus_unlocked=true return when cmd_mystery_gift pbFadeOutIn { pbDownloadMysteryGift(@save_data[:player]) } when cmd_options pbFadeOutIn do scene = PokemonOption_Scene.new screen = PokemonOptionScreen.new(scene) screen.pbStartScreen(true) end when cmd_language @scene.pbEndScene $PokemonSystem.language = pbChooseLanguage pbLoadMessages('Data/' + Settings::LANGUAGES[$PokemonSystem.language][1]) if show_continue @save_data[:pokemon_system] = $PokemonSystem File.open(SaveData.get_full_path(@selected_file), 'wb') { |file| Marshal.dump(@save_data, file) } end $scene = pbCallTitle return when cmd_debug pbFadeOutIn { pbDebugMenu(false) } when cmd_quit pbPlayCloseMenuSE @scene.pbEndScene $scene = nil return when cmd_left @scene.pbCloseScene @selected_file = SaveData.get_prev_slot(save_file_list, @selected_file) break # to outer loop when cmd_right @scene.pbCloseScene @selected_file = SaveData.get_next_slot(save_file_list, @selected_file) break # to outer loop else pbPlayBuzzerSE end end end end end #=============================================================================== # #=============================================================================== class PokemonSave_Scene def pbUpdateSlotInfo(slottext) pbDisposeSprite(@sprites, "slotinfo") @sprites["slotinfo"]=Window_AdvancedTextPokemon.new(slottext) @sprites["slotinfo"].viewport=@viewport @sprites["slotinfo"].x=0 @sprites["slotinfo"].y=160 @sprites["slotinfo"].width=228 if @sprites["slotinfo"].width<228 @sprites["slotinfo"].visible=true end end #=============================================================================== # #=============================================================================== class PokemonSaveScreen def doSave(slot) if Game.save(slot) pbMessage(_INTL("\\se[]{1} saved the game.\\me[GUI save game]\\wtnp[30]", $Trainer.name)) return true else pbMessage(_INTL("\\se[]Save failed.\\wtnp[30]")) return false end end # Return true if pause menu should close after this is done (if the game was saved successfully) def pbSaveScreen ret = false @scene.pbStartScreen if !$Trainer.save_slot # New Game - must select slot ret = slotSelect else choices = [ _INTL("Save to #{$Trainer.save_slot}"), _INTL("Save to another slot"), _INTL("Don't save") ] opt = pbMessage(_INTL('Would you like to save the game?'),choices,3) if opt == 0 pbSEPlay('GUI save choice') ret = doSave($Trainer.save_slot) elsif opt == 1 pbPlayDecisionSE ret = slotSelect else pbPlayCancelSE end end @scene.pbEndScreen return ret end # Call this to open the slot select screen # Returns true if the game was saved, otherwise false def slotSelect ret = false choices = SaveData::MANUAL_SLOTS choice_info = SaveData::MANUAL_SLOTS.map { |s| getSaveInfoBoxContents(s) } index = slotSelectCommands(choices, choice_info) if index >= 0 slot = SaveData::MANUAL_SLOTS[index] # Confirm if slot not empty if !File.file?(SaveData.get_full_path(slot)) || pbConfirmMessageSerious(_INTL("Are you sure you want to overwrite the save in #{slot}?")) # If the slot names were changed this grammar might need adjustment. pbSEPlay('GUI save choice') ret = doSave(slot) end end pbPlayCloseMenuSE if !ret return ret end # Handles the UI for the save slot select screen. Returns the index of the chosen slot, or -1. # Based on pbShowCommands def slotSelectCommands(choices, choice_info, defaultCmd=0) msgwindow = Window_AdvancedTextPokemon.new(_INTL("Which slot to save in?")) msgwindow.z = 99999 msgwindow.visible = true msgwindow.letterbyletter = true msgwindow.back_opacity = MessageConfig::WINDOW_OPACITY pbBottomLeftLines(msgwindow, 2) $game_temp.message_window_showing = true if $game_temp msgwindow.setSkin(MessageConfig.pbGetSpeechFrame) cmdwindow = Window_CommandPokemonEx.new(choices) cmdwindow.z = 99999 cmdwindow.visible = true cmdwindow.resizeToFit(cmdwindow.commands) pbPositionNearMsgWindow(cmdwindow,msgwindow,:right) cmdwindow.index=defaultCmd command=0 loop do @scene.pbUpdateSlotInfo(choice_info[cmdwindow.index]) Graphics.update Input.update cmdwindow.update msgwindow.update if msgwindow if Input.trigger?(Input::BACK) command = -1 break end if Input.trigger?(Input::USE) command = cmdwindow.index break end pbUpdateSceneMap end ret = command cmdwindow.dispose msgwindow.dispose $game_temp.message_window_showing = false if $game_temp Input.update return ret end # Show the player some data about their currently selected save slot for quick identification # This doesn't use player gender for coloring, unlike the default save window def getSaveInfoBoxContents(slot) full_path = SaveData.get_full_path(slot) if !File.file?(full_path) return _INTL("(empty)") end temp_save_data = SaveData.read_from_file(full_path) # Last save time time = temp_save_data[:player].last_time_saved if time date_str = time.strftime("%x") time_str = time.strftime(_INTL("%I:%M%p")) datetime_str = "#{date_str}#{time_str}
" else datetime_str = _INTL("(old save)") end # Map name map_str = pbGetMapNameFromId(temp_save_data[:map_factory].map.map_id) # Elapsed time totalsec = (temp_save_data[:frame_count] || 0) / Graphics.frame_rate hour = totalsec / 60 / 60 min = totalsec / 60 % 60 if hour > 0 elapsed_str = _INTL("Time{1}h {2}m
", hour, min) else elapsed_str = _INTL("Time{1}m
", min) end return "#{datetime_str}"+ # blue "#{map_str}"+ # green "#{elapsed_str}" end end #=============================================================================== # #=============================================================================== module Game # Fix New Game bug (if you saved during an event script) # This fix is from Essentials v20.1 Hotfixes 1.0.5 def self.start_new(ngp_bag = nil, ngp_storage = nil, ngp_trainer = nil) if $game_map && $game_map.events $game_map.events.each_value { |event| event.clear_starting } end $game_temp.common_event_id = 0 if $game_temp $PokemonTemp.begunNewGame = true pbMapInterpreter&.clear pbMapInterpreter&.setup(nil, 0, 0) $scene = Scene_Map.new SaveData.load_new_game_values $MapFactory = PokemonMapFactory.new($data_system.start_map_id) $game_player.moveto($data_system.start_x, $data_system.start_y) $game_player.refresh $PokemonEncounters = PokemonEncounters.new $PokemonEncounters.setup($game_map.map_id) $game_map.autoplay $game_map.update # # if ngp_bag != nil # $PokemonBag = ngp_clean_item_data(ngp_bag) # end if ngp_storage != nil $PokemonStorage = ngp_clean_pc_data(ngp_storage, ngp_trainer.party) end end # Loads bootup data from save file (if it exists) or creates bootup data (if # it doesn't). def self.set_up_system SaveData.move_old_windows_save if System.platform[/Windows/] save_slot = SaveData.get_newest_save_slot if save_slot save_data = SaveData.read_from_file(SaveData.get_full_path(save_slot)) else save_data = {} end if save_data.empty? SaveData.initialize_bootup_values else SaveData.load_bootup_values(save_data) end # Set resize factor pbSetResizeFactor([$PokemonSystem.screensize, 4].min) # Set language (and choose language if there is no save file) if Settings::LANGUAGES.length >= 2 $PokemonSystem.language = pbChooseLanguage if save_data.empty? pbLoadMessages('Data/' + Settings::LANGUAGES[$PokemonSystem.language][1]) end end # Saves the game. Returns whether the operation was successful. # @param save_file [String] the save file path # @param safe [Boolean] whether $PokemonGlobal.safesave should be set to true # @return [Boolean] whether the operation was successful # @raise [SaveData::InvalidValueError] if an invalid value is being saved def self.save(slot=nil, auto=false, safe: false) slot = $Trainer.save_slot if slot.nil? return false if slot.nil? file_path = SaveData.get_full_path(slot) $PokemonGlobal.safesave = safe $game_system.save_count += 1 $game_system.magic_number = $data_system.magic_number $Trainer.save_slot = slot unless auto $Trainer.last_time_saved = Time.now begin SaveData.save_to_file(file_path) Graphics.frame_reset rescue IOError, SystemCallError $game_system.save_count -= 1 return false end return true end # Overwrites the first empty autosave slot, otherwise the oldest existing autosave def self.auto_save oldest_time = nil oldest_slot = nil SaveData::AUTO_SLOTS.each do |slot| full_path = SaveData.get_full_path(slot) if !File.file?(full_path) oldest_slot = slot break end temp_save_data = SaveData.read_from_file(full_path) save_time = temp_save_data[:player].last_time_saved || Time.at(1) if oldest_time.nil? || save_time < oldest_time oldest_time = save_time oldest_slot = slot end end self.save(oldest_slot, true) end end #=============================================================================== # #=============================================================================== # Lol who needs the FileUtils gem? # This is the implementation from the original pbEmergencySave. def file_copy(src, dst) File.open(src, 'rb') do |r| File.open(dst, 'wb') do |w| while s = r.read(4096) w.write s end end end end # When I needed extra data fields in the save file I put them in Player because it seemed easier than figuring out # how to make a save file conversion, and I prefer to maintain backwards compatibility. class Player attr_accessor :last_time_saved attr_accessor :save_slot attr_accessor :autosave_steps end def pbEmergencySave oldscene = $scene $scene = nil pbMessage(_INTL("The script is taking too long. The game will restart.")) return if !$Trainer return if !$Trainer.save_slot current_file = SaveData.get_full_path($Trainer.save_slot) backup_file = SaveData.get_backup_file_path file_copy(current_file, backup_file) if Game.save pbMessage(_INTL("\\se[]The game was saved.\\me[GUI save game] The previous save file has been backed up.\\wtnp[30]")) else pbMessage(_INTL("\\se[]Save failed.\\wtnp[30]")) end $scene = oldscene end