# 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) begin temp_save_data = self.read_from_file(full_path) rescue next end 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) begin save_data = SaveData.read_from_file(file_path) rescue save_data =try_load_backup(file_path) end unless SaveData.valid?(save_data) save_data =try_load_backup(file_path) end return save_data end def try_load_backup(file_path) 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 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 checkEnableSpritesDownload if $PokemonSystem.download_sprites && $PokemonSystem.download_sprites != 0 customSprites = getCustomSpeciesList if !customSprites promptEnableSpritesDownload else if customSprites.length < 1000 promptEnableSpritesDownload end end end end def promptEnableSpritesDownload message = "Some sprites appear to be missing from your game. \nWould you like the game to download sprites automatically while playing? (this requires an internet connection)" if pbConfirmMessage(message) $PokemonSystem.download_sprites = 0 end end def detectFakeDownload() blacklist = ["pokemoninfinitefusion.net"] current_user_directory=Dir.pwd for keyword in blacklist if current_user_directory.include?(keyword) pbMessage("The game has detected that it has been installed from an illegitimate source which likely contains viruses.") pbMessage("Please delete the game and reinstall it from the Discord or subreddit") pbMessage("discord.gg/infinitefusion\nreddit.com/r/pokemoninfinitefusion") pbMessage("Please also make sure to report the website you originally downloaded the game from to Google.") return true end end return false end def fakeWebsiteDisclaimer() pbMessage("Reminder: This game does NOT have an official website. The ONLY official sources for the game are game's official discord or subreddit.") end def pbStartLoadScreen return if detectFakeDownload fakeWebsiteDisclaimer() updateHttpSettingsFile updateCreditsFile newer_version = find_newer_available_version if newer_version pbMessage(_INTL("Version {1} is now available! Please use the game's installer to download the newest version. Check the Discord for more information.", newer_version)) end if ($game_temp.unimportedSprites && $game_temp.unimportedSprites.size > 0) handleReplaceExistingSprites() end if ($game_temp.nb_imported_sprites && $game_temp.nb_imported_sprites > 0) pbMessage(_INTL("{1} new custom sprites were imported into the game", $game_temp.nb_imported_sprites.to_s)) end checkEnableSpritesDownload $game_temp.nb_imported_sprites = nil 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) $game_switches[SWITCH_V5_1] = true $PokemonGlobal.alt_sprite_substitutions = {} if !$PokemonGlobal.alt_sprite_substitutions $PokemonGlobal.autogen_sprites_cache = {} return when cmd_new_game @scene.pbEndScene Game.start_new $PokemonGlobal.alt_sprite_substitutions = {} if !$PokemonGlobal.alt_sprite_substitutions 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