diff --git a/Data/Scripts/002_Save data/001_SaveData.rb b/Data/Scripts/002_Save data/001_SaveData.rb index 92f188414..d4bcb7c89 100644 --- a/Data/Scripts/002_Save data/001_SaveData.rb +++ b/Data/Scripts/002_Save data/001_SaveData.rb @@ -8,16 +8,10 @@ module SaveData DIRECTORY = (File.directory?(System.data_directory)) ? System.data_directory : "./" FILENAME_REGEX = /Game(\d*)\.rxdata$/ - # Contains the file path of the save file. - FILE_PATH = if File.directory?(System.data_directory) - System.data_directory + "/Game.rxdata" - else - "./Game.rxdata" - end - # @return [Boolean] whether the save file exists + # @return [Boolean] whether any save files exist def self.exists? - return File.file?(FILE_PATH) + return !all_save_files.empty? end # @return[Array] array of filenames in the save folder that are save files @@ -60,7 +54,7 @@ module SaveData def self.read_from_file(file_path) validate file_path => String save_data = get_data_from_file(file_path) - save_data = to_hash_format(save_data) if save_data.is_a?(Array) + save_data = to_hash_format(save_data) if save_data.is_a?(Array) # Pre-v19 save file support if !save_data.empty? && run_conversions(save_data) File.open(file_path, "wb") { |file| Marshal.dump(save_data, file) } end @@ -84,6 +78,11 @@ module SaveData File.delete(DIRECTORY + filename + ".bak") if File.file?(DIRECTORY + filename + ".bak") end + def self.filename_from_index(index = 0) + return "Game.rxdata" if index <= 0 + return "Game#{index}.rxdata" + end + # Converts the pre-v19 format data to the new format. # @param old_format [Array] pre-v19 format save data # @return [Hash] save data in new format diff --git a/Data/Scripts/002_Save data/003_SaveData_Conversion.rb b/Data/Scripts/002_Save data/003_SaveData_Conversion.rb index d1469124b..9eb81688a 100644 --- a/Data/Scripts/002_Save data/003_SaveData_Conversion.rb +++ b/Data/Scripts/002_Save data/003_SaveData_Conversion.rb @@ -201,7 +201,8 @@ module SaveData validate save_data => Hash conversions_to_run = self.get_conversions(save_data) return false if conversions_to_run.none? - File.open(SaveData::FILE_PATH + ".bak", "wb") { |f| Marshal.dump(save_data, f) } + filepath = SaveData::DIRECTORY + SaveData.filename_from_index(save_data[:stats].save_filename_number || 0) + File.open(filepath + ".bak", "wb") { |f| Marshal.dump(save_data, f) } Console.echo_h1(_INTL("Converting save file")) conversions_to_run.each do |conversion| Console.echo_li("#{conversion.title}...") diff --git a/Data/Scripts/003_Game processing/001_StartGame.rb b/Data/Scripts/003_Game processing/001_StartGame.rb index ae33b817f..ca4917a89 100644 --- a/Data/Scripts/003_Game processing/001_StartGame.rb +++ b/Data/Scripts/003_Game processing/001_StartGame.rb @@ -27,9 +27,7 @@ module Game pbSetResizeFactor([$PokemonSystem.screensize, 4].min) # Set language (and choose language if there is no save file) if !Settings::LANGUAGES.empty? - # TODO: Change this to check for any save file. - save_data = (SaveData.exists?) ? SaveData.read_from_file(SaveData::FILE_PATH) : {} - $PokemonSystem.language = pbChooseLanguage if save_data.empty? && Settings::LANGUAGES.length >= 2 + $PokemonSystem.language = pbChooseLanguage if !SaveData.exists? && Settings::LANGUAGES.length >= 2 MessageTypes.load_message_files(Settings::LANGUAGES[$PokemonSystem.language][1]) end end @@ -41,7 +39,6 @@ module Game $game_map.events.each_value { |event| event.clear_starting } end $game_temp.common_event_id = 0 if $game_temp - $game_temp.begun_new_game = true pbMapInterpreter&.clear pbMapInterpreter&.setup(nil, 0, 0) $scene = Scene_Map.new @@ -104,18 +101,21 @@ module Game end # Saves the game. Returns whether the operation was successful. - # @param save_file [String] the save file path + # @param index [Integer] the number to put in the save file's name Game#.rzdata + # @param directory [String] the folder to put the save file in # @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 save(save_file = SaveData::FILE_PATH, safe: false) - validate save_file => String, safe => [TrueClass, FalseClass] + def save(index, directory = SaveData::DIRECTORY, safe: false) + validate index => Integer, directory => String, safe => [TrueClass, FalseClass] + filename = SaveData.filename_from_index(index) $PokemonGlobal.safesave = safe $game_system.save_count += 1 $game_system.magic_number = $data_system.magic_number $stats.set_time_last_saved + $stats.save_filename_number = index begin - SaveData.save_to_file(save_file) + SaveData.save_to_file(directory + filename) Graphics.frame_reset rescue IOError, SystemCallError $game_system.save_count -= 1 diff --git a/Data/Scripts/003_Game processing/004_Interpreter_Commands.rb b/Data/Scripts/003_Game processing/004_Interpreter_Commands.rb index 1f862cdcb..3abd9cf09 100644 --- a/Data/Scripts/003_Game processing/004_Interpreter_Commands.rb +++ b/Data/Scripts/003_Game processing/004_Interpreter_Commands.rb @@ -1153,9 +1153,7 @@ class Interpreter # * Call Save Screen #----------------------------------------------------------------------------- def command_352 - scene = PokemonSave_Scene.new - screen = PokemonSaveScreen.new(scene) - screen.pbSaveScreen + pbFadeOutIn { UI::Save.new.main } return true end diff --git a/Data/Scripts/003_New_UI_Settings.rb b/Data/Scripts/003_New_UI_Settings.rb new file mode 100644 index 000000000..7d4f31d7e --- /dev/null +++ b/Data/Scripts/003_New_UI_Settings.rb @@ -0,0 +1,8 @@ +#=============================================================================== +# NOTE: Some Settings in here will be moved elsewhere eventually. They're all +# just gathered here while the new UI is being written. +#=============================================================================== +module Settings + # :one, :adventure, :multiple + SAVE_SLOTS = :multiple +end diff --git a/Data/Scripts/004_Game classes/001_Switches and Variables/001_Game_Temp.rb b/Data/Scripts/004_Game classes/001_Switches and Variables/001_Game_Temp.rb index 7f15670fa..a08dfc479 100644 --- a/Data/Scripts/004_Game classes/001_Switches and Variables/001_Game_Temp.rb +++ b/Data/Scripts/004_Game classes/001_Switches and Variables/001_Game_Temp.rb @@ -41,7 +41,6 @@ class Game_Temp attr_accessor :background_bitmap attr_accessor :fadestate # for sprite hashes # Other - attr_accessor :begun_new_game # new game flag (true fron new game until saving) attr_accessor :menu_beep # menu: play sound effect flag attr_accessor :menu_last_choice # pause menu: index of last selection attr_accessor :memorized_bgm # set when trainer intro BGM is played @@ -79,7 +78,6 @@ class Game_Temp @transition_name = "" @fadestate = 0 # Other - @begun_new_game = false @menu_beep = false @memorized_bgm = nil @memorized_bgm_position = 0 diff --git a/Data/Scripts/004_Game classes/002_Game_System.rb b/Data/Scripts/004_Game classes/002_Game_System.rb index c401a5d27..58e90cadd 100644 --- a/Data/Scripts/004_Game classes/002_Game_System.rb +++ b/Data/Scripts/004_Game classes/002_Game_System.rb @@ -22,21 +22,27 @@ class Game_System attr_accessor :bgm_position def initialize - @map_interpreter = Interpreter.new(0, true) - @battle_interpreter = Interpreter.new(0, false) - @timer_start = nil - @timer_duration = 0 - @save_disabled = false - @menu_disabled = false - @encounter_disabled = false - @message_position = 2 - @message_frame = 0 - @save_count = 0 - @magic_number = 0 - @autoscroll_x_speed = 0 - @autoscroll_y_speed = 0 - @bgm_position = 0 - @bgs_position = 0 + @map_interpreter = Interpreter.new(0, true) + @battle_interpreter = Interpreter.new(0, false) + @timer_start = nil + @timer_duration = 0 + @save_disabled = false + @menu_disabled = false + @encounter_disabled = false + @message_position = 2 + @message_frame = 0 + @save_count = 0 + @magic_number = 0 + @adventure_magic_number = rand(2**32) + @autoscroll_x_speed = 0 + @autoscroll_y_speed = 0 + @bgm_position = 0 + @bgs_position = 0 + end + + def adventure_magic_number + @adventure_magic_number ||= rand(2**32) + return @adventure_magic_number end def battle_bgm diff --git a/Data/Scripts/004_Game classes/012_Game_Stats.rb b/Data/Scripts/004_Game classes/012_Game_Stats.rb index 396ae3e18..241d2bc2c 100644 --- a/Data/Scripts/004_Game classes/012_Game_Stats.rb +++ b/Data/Scripts/004_Game classes/012_Game_Stats.rb @@ -62,6 +62,7 @@ class GameStats attr_accessor :play_sessions attr_accessor :time_last_saved # In seconds attr_reader :real_time_saved + attr_accessor :save_filename_number # -1 if haven't saved yet def initialize # Travel @@ -151,6 +152,7 @@ class GameStats @play_sessions = 0 @time_last_saved = 0 @real_time_saved = 0 + @save_filename_number = -1 end def distance_moved @@ -185,6 +187,11 @@ class GameStats return @play_time end + # For looking at a save file's play time. + def real_play_time + return @play_time + end + def play_time_per_session return play_time / @play_sessions end diff --git a/Data/Scripts/016b_UI redesign/001_UI_PauseMenu.rb b/Data/Scripts/016b_UI redesign/001_UI_PauseMenu.rb index 0968012ab..6c813339b 100644 --- a/Data/Scripts/016b_UI redesign/001_UI_PauseMenu.rb +++ b/Data/Scripts/016b_UI redesign/001_UI_PauseMenu.rb @@ -286,16 +286,17 @@ MenuHandlers.add(:pause_menu, :save, { next $game_system && !$game_system.save_disabled && !pbInSafari? && !pbInBugContest? }, "effect" => proc { |menu| - menu.hide_menu - scene = PokemonSave_Scene.new - screen = PokemonSaveScreen.new(scene) - if screen.pbSaveScreen - menu.silent_end_screen - next true + pbPlayDecisionSE + ret = false + pbFadeOutIn do + ret = UI::Save.new.main + if ret + menu.silent_end_screen + else + menu.refresh + end end - menu.refresh - menu.show_menu - next false + next ret } }) @@ -335,10 +336,6 @@ MenuHandlers.add(:pause_menu, :quit_game, { "effect" => proc { |menu| menu.hide_menu if pbConfirmMessage(_INTL("Are you sure you want to quit the game?")) - scene = PokemonSave_Scene.new - screen = PokemonSaveScreen.new(scene) - screen.pbSaveScreen - menu.silent_end_screen $scene = nil next true end diff --git a/Data/Scripts/016b_UI redesign/013_UI_Load.rb b/Data/Scripts/016b_UI redesign/013_UI_Load.rb index 64cf60846..ad7b04fbd 100644 --- a/Data/Scripts/016b_UI redesign/013_UI_Load.rb +++ b/Data/Scripts/016b_UI redesign/013_UI_Load.rb @@ -2,6 +2,8 @@ # #=============================================================================== class UI::LoadPanel < UI::SpriteContainer + attr_writer :label + GRAPHICS_FOLDER = "Load/" TEXT_COLOR_THEMES = { # These color themes are added to @sprites[:overlay] :default => [Color.new(88, 88, 80), Color.new(168, 184, 184)] # Base and shadow colour @@ -258,13 +260,16 @@ end #=============================================================================== class UI::LoadVisuals < UI::BaseVisuals attr_reader :slot_index + attr_reader :save_data GRAPHICS_FOLDER = "Load/" # Subfolder in Graphics/UI PANEL_SPACING_EDGE = 4 PANEL_SPACING = PANEL_SPACING_EDGE * 2 - # save_data here is an array of all save files' data. It has been compacted. - # commands is {:continue => _INTL("Continue"), :new_game => _INTL("New Game")}, etc. + # save_data here is an array of [save filename, save data hash]. It has been + # compacted. + # commands is {:continue => _INTL("Continue"), :new_game => _INTL("New Game")}, + # etc. def initialize(commands, save_data, default_slot_index = 0) @save_data = save_data @commands = commands @@ -372,7 +377,13 @@ class UI::LoadVisuals < UI::BaseVisuals # Determine whether the Mystery Gift option is visible @sprites[:mystery_gift].visible = @save_data[@slot_index][1][:player].mystery_gift_unlocked refresh_panel_positions + # Set the options, and change the language if relevant + old_language = $PokemonSystem.language SaveData.load_bootup_values(@save_data[@slot_index][1], true) + if $PokemonSystem.language != old_language + MessageTypes.load_message_files(Settings::LANGUAGES[$PokemonSystem.language][1]) + full_refresh + end pbPlayCursorSE if !forced end @@ -395,6 +406,7 @@ class UI::LoadVisuals < UI::BaseVisuals def full_refresh refresh + refresh_labels @sprites.each_pair { |key, sprite| sprite.refresh if sprite.respond_to?(:refresh) } end @@ -431,6 +443,16 @@ class UI::LoadVisuals < UI::BaseVisuals @sprites[:continue_next]&.selected = false end + def refresh_labels + MenuHandlers.each_available(:load_screen, self) do |option, _hash, name| + @sprites[option].label = name + if option == :continue + @sprites[:continue_previous].label = name + @sprites[:continue_next].label = name + end + end + end + def refresh_on_index_changed(old_index) refresh_selected_panel refresh_panel_positions @@ -503,7 +525,7 @@ class UI::Load < UI::BaseScreen SCREEN_ID = :load_screen def initialize - load_save_data + load_all_save_data if $DEBUG && !FileTest.exist?("Game.rgssad") && Settings::SKIP_CONTINUE_SCREEN @disposed = true perform_action((@save_data.empty?) ? :new_game : :continue) @@ -532,22 +554,15 @@ class UI::Load < UI::BaseScreen #----------------------------------------------------------------------------- - def load_save_data + # TODO: Move this kind of code into module SaveData. + def load_all_save_data @save_data = [] @default_slot_index = 0 last_edited_time = nil files = SaveData.all_save_files files.each do |file| # Load the save file - this_save_data = SaveData.read_from_file(SaveData::DIRECTORY + file) - if !SaveData.valid?(this_save_data) - if File.file?(SaveData::DIRECTORY + file + ".bak") - show_message(_INTL("The save file is corrupt. A backup will be loaded.")) - this_save_data = load_save_file(SaveData::FILE_PATH + ".bak") - else - prompt_corrupted_save_deletion(file) - end - end + this_save_data = load_save_file(SaveData::DIRECTORY, file) @save_data.push([file, this_save_data]) # Find the most recently edited save file; default to selecting that one save_time = this_save_data[:stats].real_time_saved || 0 @@ -557,14 +572,31 @@ class UI::Load < UI::BaseScreen end end SaveData.load_bootup_values(@save_data[@default_slot_index][1], true) if !@save_data.empty? + MessageTypes.load_message_files(Settings::LANGUAGES[$PokemonSystem.language][1]) + end + + # TODO: Move this kind of code into module SaveData. + def load_save_file(directory, filename) + ret = SaveData.read_from_file(directory + filename) + if !SaveData.valid?(ret) + if File.file?(directory + filename + ".bak") + show_message(_INTL("The save file is corrupt. A backup will be loaded.")) + ret = load_save_file(directory, filename + ".bak") + end + if prompt_corrupted_save_deletion(filename) + delete_save_data(filename) + $PokemonSystem = PokemonSystem.new + else + exit + end + end + return ret end def prompt_corrupted_save_deletion(filename) show_message(_INTL("The save file is corrupt, or is incompatible with this game.") + "\1") pbPlayDecisionSE - exit if !show_confirm_serious_message(_INTL("Do you want to delete the save file and start anew?")) - delete_save_data(filename) - $PokemonSystem = PokemonSystem.new + return show_confirm_serious_message(_INTL("Do you want to delete the save file and start anew?")) end def prompt_save_deletion(filename) @@ -672,6 +704,10 @@ UIActionHandlers.add(UI::Load::SCREEN_ID, :delete_save, { #=============================================================================== # Menu options that exist in the load screen. +# NOTE: This can also be called when screen is UI::LoadVisuals (for +# retranslating the names upon selecting a save file with a different +# language). Try not to have a condition that references things only in +# UI::Load. #=============================================================================== MenuHandlers.add(:load_screen, :continue, { "name" => _INTL("Continue"), diff --git a/Data/Scripts/016b_UI redesign/014_UI_Save.rb b/Data/Scripts/016b_UI redesign/014_UI_Save.rb new file mode 100644 index 000000000..8bed7402b --- /dev/null +++ b/Data/Scripts/016b_UI redesign/014_UI_Save.rb @@ -0,0 +1,641 @@ +#=============================================================================== +# +#=============================================================================== +class UI::SavePanel < UI::SpriteContainer + attr_reader :sprites + + GRAPHICS_FOLDER = "Save/" + TEXT_COLOR_THEMES = { # These color themes are added to @sprites[:overlay] + :default => [Color.new(88, 88, 80), Color.new(168, 184, 184)], # Base and shadow colour + :white => [Color.new(248, 248, 248), Color.new(172, 188, 188)], + :male => [Color.new(0, 112, 248), Color.new(120, 184, 232)], + :female => [Color.new(232, 32, 16), Color.new(248, 168, 184)] + } + PANEL_WIDTH = 384 + PANEL_HEIGHT = 204 + + def initialize(save_data, viewport) + @save_data = save_data + @show_arrows = false + super(viewport) + refresh + end + + def initialize_sprites + initialize_panel_background + initialize_overlay + initialize_player_sprite + initialize_pokemon_icons + initialize_arrow_sprites + end + + def initialize_panel_background + @sprites[:background] = ChangelingSprite.new(0, 0, @viewport) + panel_srcs.each_pair do |key, values| + @sprites[:background].add_bitmap(key, values) + end + @sprites[:background].change_bitmap(:default) + record_values(:background) + end + + def initialize_overlay + add_overlay(:overlay, @sprites[:background].width, @sprites[:background].height) + @sprites[:overlay].z = 10 + record_values(:overlay) + end + + def initialize_player_sprite + meta = GameData::PlayerMetadata.get(@save_data[:player].character_ID) + filename = pbGetPlayerCharset(meta.walk_charset, @save_data[:player], true) + @sprites[:player] = TrainerWalkingCharSprite.new(filename, @viewport) + if !@sprites[:player].bitmap + raise _INTL("Player character {1}'s walking charset was not found (filename: \"{2}\").", + @save_data[:player].character_ID, filename) + end + @sprites[:player].x = 44 - (@sprites[:player].bitmap.width / 8) + @sprites[:player].y = 36 - (@sprites[:player].bitmap.height / 8) + @sprites[:player].z = 1 + record_values(:player) + end + + def initialize_pokemon_icons + Settings::MAX_PARTY_SIZE.times do |i| + @sprites["pokemon_#{i}"] = PokemonIconSprite.new(@save_data[:player].party[i], @viewport) + @sprites["pokemon_#{i}"].x, @sprites["pokemon_#{i}"].y = pokemon_coords(i) + @sprites["pokemon_#{i}"].z = 1 + @sprites["pokemon_#{i}"].setOffset + record_values("pokemon_#{i}") + end + end + + def initialize_arrow_sprites + @sprites[:left_arrow] = AnimatedSprite.new(UI_FOLDER + "left_arrow", 8, 40, 28, 2, @viewport) + @sprites[:left_arrow].x = -16 + @sprites[:left_arrow].y = (height / 2) - 14 + @sprites[:left_arrow].z = 20 + @sprites[:left_arrow].visible = false + @sprites[:left_arrow].play + record_values(:left_arrow) + @sprites[:right_arrow] = AnimatedSprite.new(UI_FOLDER + "right_arrow", 8, 40, 28, 2, @viewport) + @sprites[:right_arrow].x = width - 24 + @sprites[:right_arrow].y = (height / 2) - 14 + @sprites[:right_arrow].z = 20 + @sprites[:right_arrow].visible = false + @sprites[:right_arrow].play + record_values(:right_arrow) + end + + #----------------------------------------------------------------------------- + + def width + return PANEL_WIDTH + end + + def height + return PANEL_HEIGHT + end + + def panel_srcs + return { + :default => [graphics_folder + "panels", 0, 0, PANEL_WIDTH, PANEL_HEIGHT], + :new_slot => [graphics_folder + "panels", 0, PANEL_HEIGHT, PANEL_WIDTH, PANEL_HEIGHT] + } + end + + def pokemon_coords(index) + return 272 + (66 * (index % 2)), + 36 + (50 * (index / 2)) + end + + def show_arrows=(value) + return if @show_arrows == value + @show_arrows = value + @sprites[:left_arrow].visible = value + @sprites[:right_arrow].visible = value + end + + def set_data(save_data) + @save_data = save_data + @sprites[:background].change_bitmap((@save_data) ? :default : :new_slot) + set_player_sprite + refresh + end + + def set_player_sprite + if !@save_data + @sprites[:player].visible = false + return + end + @sprites[:player].visible = true + meta = GameData::PlayerMetadata.get(@save_data[:player].character_ID) + filename = pbGetPlayerCharset(meta.walk_charset, @save_data[:player], true) + @sprites[:player].charset = filename + if !@sprites[:player].bitmap + raise _INTL("Player character {1}'s walking charset was not found (filename: \"{2}\").", + @save_data[:player].character_ID, filename) + end + end + + #----------------------------------------------------------------------------- + + def refresh + super + refresh_pokemon + draw_save_file_text + end + + def refresh_pokemon + Settings::MAX_PARTY_SIZE.times do |i| + if @save_data + @sprites["pokemon_#{i}"].pokemon = @save_data[:player].party[i] + @sprites["pokemon_#{i}"].visible = true + else + @sprites["pokemon_#{i}"].visible = false + end + end + end + + def draw_save_file_text + if !@save_data + draw_text(_INTL("Create a new save file"), width / 2, (height / 2) - 10, align: :center) + return + end + gender_theme = :default + if @save_data[:player].male? + gender_theme = :male + elsif @save_data[:player].female? + gender_theme = :female + end + # Player's name + draw_text(@save_data[:player].name, 78, 30, theme: gender_theme) + # Location + map_id = @save_data[:map_factory].map.map_id + map_name = pbGetMapNameFromId(map_id) + map_name = map_name.gsub(/\\PN/, @save_data[:player].name) + map_name = map_name.gsub(/\\v\[(\d+)\]/) { |num| @save_data[:variables][$~[1].to_i].to_s } + draw_text(map_name, 14, 78) + # Gym Badges + draw_text(_INTL("Badges:"), 14, 110, theme: :white) + draw_text(@save_data[:player].badge_count.to_s, 222, 110, align: :right) + # Pokédex owned count + draw_text(_INTL("Pokédex:"), 14, 142, theme: :white) + draw_text(@save_data[:player].pokedex.seen_count.to_s, 222, 142, align: :right) + # Time played + draw_text(_INTL("Play time:"), 14, 174, theme: :white) + play_time = @save_data[:stats]&.real_play_time.to_i || 0 + hour = (play_time / 60) / 60 + min = (play_time / 60) % 60 + play_time_text = (hour > 0) ? _INTL("{1}h {2}m", hour, min) : _INTL("{1}m", min) + draw_text(play_time_text, 222, 174, align: :right) + save_time = @save_data[:stats]&.real_time_saved + if save_time + save_time = Time.at(save_time) + if System.user_language[3..4] == "US" # If the user is in the United States + save_text = save_time.strftime("%-m/&-d/%Y") + else + save_text = save_time.strftime("%-d/%-m/%Y") + end + draw_text(save_text, PANEL_WIDTH - 14, 174, align: :right) + else + draw_text("???", PANEL_WIDTH - 14, 174, align: :right) + end + end + + def refresh_existing_pokemon + Settings::MAX_PARTY_SIZE.times do |i| + @sprites["pokemon_#{i}"].pokemon = @save_data[:player].party[i] + end + end +end + +#=============================================================================== +# +#=============================================================================== +class UI::SaveVisuals < UI::BaseVisuals + attr_reader :index + + GRAPHICS_FOLDER = "Save/" # Subfolder in Graphics/UI + TEXT_COLOR_THEMES = { # These color themes are added to @sprites[:overlay] + :default => [Color.new(80, 80, 88), Color.new(176, 192, 192)] # Base and shadow colour + } + PANEL_SPACING = 8 + + # save_data here is an array of [save filename, save data hash]. It has been + # compacted. + def initialize(save_data, current_save_data, default_index = 0) + @save_data = save_data + @current_save_data = current_save_data + @index = default_index # Which save slot is selected + @choosing_save_file = false + super() + end + + def initialize_sprites + initialize_continue_panels + end + + def initialize_continue_panels + # Continue panel in middle + this_index = @index + @sprites[:continue] = create_slot_panel(this_index) + # Continue panel to left + if !@save_data.empty? + previous_index = this_index - 1 + @sprites[:continue_previous] = create_slot_panel(this_index - 1) + @sprites[:continue_previous].x = @sprites[:continue].x - @sprites[:continue].width - PANEL_SPACING + @sprites[:continue_previous].visible = false + # Continue panel to right + next_index = this_index + 1 + @sprites[:continue_next] = create_slot_panel(this_index + 1) + @sprites[:continue_next].x = @sprites[:continue].x + @sprites[:continue].width + PANEL_SPACING + @sprites[:continue_next].visible = false + end + end + + #----------------------------------------------------------------------------- + + def create_slot_panel(slot_index, initializing = true) + slot_index += @save_data.length + 1 if slot_index < 0 + slot_index -= @save_data.length + 1 if slot_index >= @save_data.length + 1 + if initializing + this_save_data = @current_save_data[1] + else + this_save_data = (@save_data[slot_index]) ? @save_data[slot_index][1] : nil + end + ret = UI::SavePanel.new(this_save_data, @viewport) + ret.x = (Graphics.width - ret.width) / 2 + ret.y = 40 + return ret + end + + #----------------------------------------------------------------------------- + + def set_index(new_index, forced = false) + while new_index < 0 + new_index += @save_data.length + 1 + end + while new_index >= @save_data.length + 1 + new_index -= @save_data.length + 1 + end + return if !forced && @index == new_index + # Set the new index + @index = new_index + # Show the newly selected slot's information in the Continue panel + this_save_data = (@save_data[@index]) ? @save_data[@index][1] : nil + @sprites[:continue].set_data(this_save_data) + # Show the newly adjacent slots' information in the adjacent Continue panels + prev_index = @index - 1 + prev_index += @save_data.length + 1 if prev_index < 0 + this_save_data = (@save_data[prev_index]) ? @save_data[prev_index][1] : nil + @sprites[:continue_previous]&.set_data(this_save_data) + next_index = (@index + 1) % (@save_data.length + 1) + this_save_data = (@save_data[next_index]) ? @save_data[next_index][1] : nil + @sprites[:continue_next]&.set_data(this_save_data) + refresh + pbPlayCursorSE if !forced + end + + def go_to_next_save_slot + set_index(@index + 1) + end + + def go_to_previous_save_slot + set_index(@index - 1) + end + + #----------------------------------------------------------------------------- + + def start_choose_save_file + @choosing_save_file = true + @sprites[:continue_previous].visible = true + @sprites[:continue_next].visible = true + @sprites[:continue].show_arrows = true + set_index(@index, true) + end + + #----------------------------------------------------------------------------- + + def refresh_overlay + super + if @choosing_save_file + if @save_data[index] && + @save_data[index][1][:game_system].adventure_magic_number + if @save_data[index][1][:game_system].adventure_magic_number == $game_system.adventure_magic_number + save_time = @save_data[index][1][:stats].real_play_time + delta_time = ($stats.play_time - save_time).to_i + hour = (delta_time / 60) / 60 + min = (delta_time / 60) % 60 + if hour > 0 + draw_text(_INTL("Play time since save: {1}h {2}m", hour, min), 8, 4) + else + draw_text(_INTL("Play time since save: {1}m", min), 8, 4) + end + else + draw_text(_INTL("Different adventure!"), 8, 4) + end + end + if @save_data[@index] + draw_text(sprintf("%d/%d", @index + 1, @save_data.length), Graphics.width - 8, 4, align: :right) + end + elsif $stats.save_count > 0 && $stats.real_time_saved + save_time = Time.at($stats.real_time_saved) + if System.user_language[3..4] == "US" # If the user is in the United States + date_text = save_time.strftime("%-m/&-d/%Y") + else + date_text = save_time.strftime("%-d/%-m/%Y") + end + time_text = save_time.strftime("%H:%M") + draw_text(_INTL("Last saved on {1} at {2}", date_text, time_text), 8, 4) + end + end + + def full_refresh + refresh + @sprites.each_pair { |key, sprite| sprite.refresh if sprite.respond_to?(:refresh) } + end + + #----------------------------------------------------------------------------- + + def update_input + # Check for movement to a different save slot + if Input.repeat?(Input::LEFT) + go_to_previous_save_slot + elsif Input.repeat?(Input::RIGHT) + go_to_next_save_slot + end + # Check for interaction + if Input.trigger?(Input::USE) + return update_interaction(Input::USE) + elsif Input.trigger?(Input::BACK) + return update_interaction(Input::BACK) + end + return nil + end + + def update_interaction(input) + case input + when Input::USE + pbPlayDecisionSE + return :choose_slot + when Input::BACK + pbPlayCancelSE + return :quit + end + return nil + end + + #----------------------------------------------------------------------------- + + def navigate + help_text = _INTL("Choose a file to save in.") + help_window = Window_AdvancedTextPokemon.newWithSize( + help_text, 0, 0, Graphics.width, 96, @viewport + ) + help_window.z = 2000 + help_window.setSkin(MessageConfig.pbGetSpeechFrame) + help_window.letterbyletter = false + pbBottomRight(help_window) + # Navigate loop + ret = super + # Clean up + help_window.dispose + return ret + end +end + +#=============================================================================== +# +#=============================================================================== +class UI::Save < UI::BaseScreen + attr_reader :save_data + + SCREEN_ID = :save_screen + + def initialize + create_current_save_data + load_all_save_data + super + end + + def initialize_visuals + @visuals = UI::SaveVisuals.new(@save_data, @current_save_data, @default_index) + end + + #----------------------------------------------------------------------------- + + def index + return @visuals&.index || @default_index + end + + #----------------------------------------------------------------------------- + + # This is pseudo-save data containing the current state of the game. + def create_current_save_data + @current_save_data = [0, { + :player => $player, + :map_factory => $map_factory, + :variables => $game_variables, + :stats => $stats + }] + end + + def load_all_save_data + # Load the save file + @save_data = [] + files = SaveData.all_save_files + files.each do |file| + this_save_data = load_save_file(SaveData::DIRECTORY, file) + @save_data.push([file, this_save_data]) + end + # Find the save file index matching the current game's filename number + if $stats.save_filename_number && $stats.save_filename_number >= 0 + expected_filename = SaveData.filename_from_index($stats.save_filename_number) + @default_index = @save_data.index { |sav| sav[0] == expected_filename } + @default_index ||= @save_data.length # Just in case + else + @default_index = @save_data.length # New save slot + end + end + + def load_save_file(directory, filename) + ret = SaveData.read_from_file(directory + filename) + if !SaveData.valid?(ret) + if File.file?(directory + filename + ".bak") + show_message(_INTL("The save file is corrupt. A backup will be loaded.")) + ret = load_save_file(directory, filename + ".bak") + end + if prompt_corrupted_save_deletion(filename) + delete_save_data(filename) + else + exit + end + end + return ret + end + + def prompt_corrupted_save_deletion(filename) + show_message(_INTL("The save file is corrupt, or is incompatible with this game.") + "\1") + pbPlayDecisionSE + return show_confirm_serious_message(_INTL("Do you want to delete the save file and start anew?")) + end + + def delete_save_data(filename) + begin + SaveData.delete_file(filename) + yield if block_given? + show_message(_INTL("The save file was deleted.")) + rescue SystemCallError + show_message(_INTL("The save file could not be deleted.")) + end + end + + def different_adventure?(slot_index) + return false if !@save_data[slot_index] + return false if !@save_data[slot_index][1][:game_system].adventure_magic_number + return @save_data[slot_index][1][:game_system].adventure_magic_number != $game_system.adventure_magic_number + end + + def prompt_overwrite_save_file(slot_index) + if different_adventure?(slot_index) + show_message(_INTL("WARNING!") + "\1") + show_message(_INTL("There is a different game file that is already saved.") + "\1") + show_message(_INTL("If you save now, the other file's adventure, including items and Pokémon, will be entirely lost.") + "\1") + if !show_confirm_serious_message(_INTL("Are you sure you want to save now and overwrite the other save file?")) + return false + end + end + return true + end + + # NOTE: Save filenames are "Game#.rxdata" where "#" is slot_index, except for + # 0 which just produces "Game.rzdata". This is to support old save + # files which are that name. + def save_game(file_number) + # TODO: I don't know about this "GUI save choice" being here. +# pbSEPlay("GUI save choice") + if Game.save(file_number) + # Refresh the panels to show the new save's data + file = SaveData.filename_from_index(file_number) + slot_index = @save_data.index { |sav| sav[0] == file } + slot_index ||= @save_data.length # New save file + this_save_data = load_save_file(SaveData::DIRECTORY, file) + @save_data[slot_index] = [file, this_save_data] + @visuals.set_index(slot_index, true) + # Announce the save success + show_message(_INTL("{1} saved the game.", $player.name)) { + # TODO: Stop SE. + pbMEPlay("GUI save game") + # TODO: Wait for ME to finish playing, then auto-close the message. + } + @result = true + else + show_message(_INTL("Save failed.")) + # TODO: Auto-close this message. + @result = false + end + end + + def get_save_file_number(slot_index = -1) + filename = (slot_index >= 0 && @save_data[slot_index]) ? @save_data[slot_index][0] : @save_data.last[0] + filename[SaveData::FILENAME_REGEX] # Just to get the number in the filename + ret = $~[1].to_i + ret += 1 if slot_index < 0 || !@save_data[slot_index] + return ret + end + + #----------------------------------------------------------------------------- + + def full_refresh + @visuals.full_refresh + end + + #----------------------------------------------------------------------------- + + def main + start_screen + # If the player doesn't want to save, just exit the screen + if !show_confirm_message(_INTL("Would you like to save the game?")) + end_screen + return false + end + # If there are no existing save files, just save in the first slot + if @save_data.empty? + save_game(0) + end_screen + return @result + end + # If there are existing save files, do something depending on which save + # files are allowed to be made + case Settings::SAVE_SLOTS + when :multiple + # Choose a save slot to replace + @visuals.start_choose_save_file + loop do + command = @visuals.navigate + break if command == :quit + if !@save_data[index] || + show_confirm_message(_INTL("Do you want to overwrite this save file?")) + if different_adventure?(index) + show_message(_INTL("WARNING!") + "\1") + pbPlayDecisionSE + show_message(_INTL("This save file is a different adventure.") + "\1") + pbPlayDecisionSE + show_message(_INTL("If you save now, that adventure, including items and Pokémon, will be entirely lost.") + "\1") + pbPlayDecisionSE + next if !show_confirm_serious_message(_INTL("Are you sure you want to overwrite it?")) + end + file_number = get_save_file_number(index) + save_game(file_number) + break + end + end + when :adventure + if $stats.save_filename_number && $stats.save_filename_number >= 0 # Was saved previously + save_game($stats.save_filename_number) + else + file_number = get_save_file_number(-1) # New save slot + save_game(file_number) + end + when :one + save_game(0) if prompt_overwrite_save_file(0) + end + end_screen + return @result + end +end + +#=============================================================================== +# +#=============================================================================== +def pbSaveScreen + ret = false + pbFadeOutIn { ret = UI::Save.new.main } + return ret +end + +def pbEmergencySave + oldscene = $scene + $scene = nil + pbMessage(_INTL("The script is taking too long. The game will restart.")) + return if !$player + filename_number = $stats.save_filename_number || -1 + filename = SaveData.filename_from_index(filename_number) + if SaveData.exists? + File.open(SaveData::DIRECTORY + filename, "rb") do |r| + File.open(SaveData::DIRECTORY + filename + ".bak", "wb") do |w| + loop do + s = r.read(4096) + break if !s + w.write(s) + end + end + end + end + if Game.save(filename_number) + pbMessage("\\se[]" + _INTL("The game was saved.") + "\\me[GUI save game]\\wtnp[20]") + pbMessage("\\se[]" + _INTL("The previous save file has been backed up.") + "\\wtnp[20]") + else + pbMessage("\\se[]" + _INTL("Save failed.") + "\\wtnp[30]") + end + $scene = oldscene +end diff --git a/Data/Scripts/016_UI/014_UI_Save.rb b/Data/Scripts/016c_UI_old/014_UI_old_Save.rb similarity index 97% rename from Data/Scripts/016_UI/014_UI_Save.rb rename to Data/Scripts/016c_UI_old/014_UI_old_Save.rb index 56c2c97d6..249106a03 100644 --- a/Data/Scripts/016_UI/014_UI_Save.rb +++ b/Data/Scripts/016c_UI_old/014_UI_old_Save.rb @@ -1,3 +1,4 @@ +=begin #=============================================================================== # #=============================================================================== @@ -107,9 +108,8 @@ end # #=============================================================================== def pbSaveScreen - scene = PokemonSave_Scene.new - screen = PokemonSaveScreen.new(scene) - ret = screen.pbSaveScreen + ret = false + pbFadeOutIn { ret = UI::Save.new.main } return ret end @@ -137,3 +137,4 @@ def pbEmergencySave end $scene = oldscene end +=end