Class: Gemba::SaveStateManager

Inherits:
Object
  • Object
show all
Includes:
Locale::Translatable
Defined in:
lib/gemba/save_state_manager.rb

Overview

Manages save state persistence: save, load, screenshot capture, debounce, and backup rotation.

All dependencies are injected via the constructor so the class can be tested with lightweight mocks (no real mGBA Core or Tk interpreter).

Examples:

Production usage (inside Player)

@save_mgr = SaveStateManager.new(core: @core, config: @config, app: @app)
success, msg = @save_mgr.save_state(1)
show_toast(msg)

Test usage (with mocks)

mgr = SaveStateManager.new(core: mock_core, config: config, app: mock_app)
success, msg = mgr.save_state(1)
assert success

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(core:, config:, app:, platform:) ⇒ SaveStateManager

Returns a new instance of SaveStateManager.



24
25
26
27
28
29
30
31
32
33
# File 'lib/gemba/save_state_manager.rb', line 24

def initialize(core:, config:, app:, platform:)
  @core = core
  @config = config
  @app = app
  @platform = platform
  @last_save_time = 0
  @state_dir = nil
  @quick_save_slot = config.quick_save_slot
  @backup = config.save_state_backup?
end

Instance Attribute Details

#backupBoolean

Returns whether to create .bak files.

Returns:

  • (Boolean)

    whether to create .bak files



39
40
41
# File 'lib/gemba/save_state_manager.rb', line 39

def backup
  @backup
end

#coreCore

Returns the mGBA core (swappable for reset/ROM change).

Returns:

  • (Core)

    the mGBA core (swappable for reset/ROM change)



42
43
44
# File 'lib/gemba/save_state_manager.rb', line 42

def core
  @core
end

#quick_save_slotInteger

Returns quick save/load slot.

Returns:

  • (Integer)

    quick save/load slot



36
37
38
# File 'lib/gemba/save_state_manager.rb', line 36

def quick_save_slot
  @quick_save_slot
end

#state_dirString?

Returns current state directory.

Returns:

  • (String, nil)

    current state directory



59
60
61
# File 'lib/gemba/save_state_manager.rb', line 59

def state_dir
  @state_dir
end

Instance Method Details

#load_state(slot) ⇒ Array(Boolean, String)

Load the emulator state from the given slot.

Parameters:

  • slot (Integer)

Returns:

  • (Array(Boolean, String))

    success flag and translated message



106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/gemba/save_state_manager.rb', line 106

def load_state(slot)
  return [false, nil] unless @core && !@core.destroyed?

  ss = state_path(slot)
  unless File.exist?(ss)
    return [false, translate('toast.no_state', slot: slot)]
  end

  if @core.load_state_from_file(ss)
    [true, translate('toast.state_loaded', slot: slot)]
  else
    [false, translate('toast.load_failed')]
  end
end

#quick_loadArray(Boolean, String)

Load from the quick save slot.

Returns:

  • (Array(Boolean, String))


129
130
131
# File 'lib/gemba/save_state_manager.rb', line 129

def quick_load
  load_state(@quick_save_slot)
end

#quick_saveArray(Boolean, String)

Save to the quick save slot.

Returns:

  • (Array(Boolean, String))


123
124
125
# File 'lib/gemba/save_state_manager.rb', line 123

def quick_save
  save_state(@quick_save_slot)
end

#save_screenshot(path) ⇒ Object

Save a PNG screenshot of the current frame via Tk photo image. Uses @app.command() to drive Tk’s image subsystem.

Parameters:

  • path (String)

    output PNG file path



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/gemba/save_state_manager.rb', line 136

def save_screenshot(path)
  return unless @core && !@core.destroyed?

  pixels = @core.video_buffer_argb
  photo_name = "__gemba_ss_#{object_id}"

  @app.command(:image, :create, :photo, photo_name,
               width: @platform.width, height: @platform.height)
  @app.interp.photo_put_block(photo_name, pixels, @platform.width, @platform.height, format: :argb)
  @app.command(photo_name, :write, path, format: :png)
  @app.command(:image, :delete, photo_name)
rescue StandardError => e
  warn "gemba: screenshot failed for #{path}: #{e.message} (#{e.class})"
  @app.command(:image, :delete, photo_name) rescue nil
end

#save_state(slot) ⇒ Array(Boolean, String)

Save the emulator state to the given slot.

Parameters:

  • slot (Integer)

Returns:

  • (Array(Boolean, String))

    success flag and translated message



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/gemba/save_state_manager.rb', line 76

def save_state(slot)
  return [false, nil] unless @core && !@core.destroyed?

  now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  if now - @last_save_time < @config.save_state_debounce
    return [false, translate('toast.save_blocked')]
  end

  FileUtils.mkdir_p(@state_dir) unless File.directory?(@state_dir)

  # Backup rotation: rename existing files → .bak
  ss = state_path(slot)
  png = screenshot_path(slot)
  if @backup
    File.rename(ss, "#{ss}.bak") if File.exist?(ss)
    File.rename(png, "#{png}.bak") if File.exist?(png)
  end

  if @core.save_state_to_file(ss)
    @last_save_time = now
    save_screenshot(png)
    [true, translate('toast.state_saved', slot: slot)]
  else
    [false, translate('toast.save_failed')]
  end
end

#screenshot_path(slot) ⇒ String

Returns path to the screenshot PNG for this slot.

Parameters:

  • slot (Integer)

Returns:

  • (String)

    path to the screenshot PNG for this slot



69
70
71
# File 'lib/gemba/save_state_manager.rb', line 69

def screenshot_path(slot)
  File.join(@state_dir, "state#{slot}.png")
end

#state_dir_for_rom(core) ⇒ String

Build per-ROM state directory path using game code + CRC32. e.g. states/AGB-BTKE-A1B2C3D4/

Parameters:

  • core (Core, #game_code, #checksum)

    the emulator core

Returns:

  • (String)

    directory path



48
49
50
51
52
# File 'lib/gemba/save_state_manager.rb', line 48

def state_dir_for_rom(core)
  code = core.game_code.gsub(/[^a-zA-Z0-9_.-]/, '_')
  crc  = format('%08X', core.checksum)
  File.join(@config.states_dir, "#{code}-#{crc}")
end

#state_path(slot) ⇒ String

Returns path to the save state file for this slot.

Parameters:

  • slot (Integer)

Returns:

  • (String)

    path to the save state file for this slot



63
64
65
# File 'lib/gemba/save_state_manager.rb', line 63

def state_path(slot)
  File.join(@state_dir, "state#{slot}.ss")
end