/god_of_thunder_save

Edit your God of Thunder saved games with Ruby!

Primary LanguageRuby

God of Thunder saved game editor for Ruby

Edit your God of Thunder saved games with Ruby!

Usage

Load a saved game file, edit it to your liking, and write the changes!

require "god_of_thunder_save"

save = GodOfThunderSave.new("SAVEGAM1.GOT")

save.name = "My renamed game"
save.keys = 5
save.jewels = 500
save.wind_power = true

save.write!

#attributes will also return everything the library knows about:

save.attributes

{:name=>"god_of_thunder_save",
 :health=>145,
 :magic=>130,
 :item=>:enchanted_apple,
 :jewels=>420,
 :keys=>69,
 :score=>31337,
 :enchanted_apple=>true,
 :lightning_power=>true,
 :winged_boots=>false,
 :wind_power=>false,
 :amulet_of_protection=>false,
 :thunder_power=>false}

Every key in this list has a getter and a setter, like in the code example above.

This library only reads and writes data indicated by the library, so your position, progress, etc. will remain intact.

Don't forget to make a backup first!

Development

So you want to contribute! Great! Here are some resources to get started!

Adding a new setter and getter

All setters and getters are defined in the ENTRIES Hash here:

ENTRIES = {
name: StringValue.new(pos: 0x00, length: 22),
health: IntegerValue.new(pos: 0x63, bytes: 1),
magic: IntegerValue.new(pos: 0x64, bytes: 1),
item: EnumValue.new(pos: 0x68, enums: ITEM_ENUMS),
jewels: IntegerValue.new(pos: 0x65, bytes: 2),
keys: IntegerValue.new(pos: 0x67, bytes: 1),
score: IntegerValue.new(pos: 0x70, bytes: 4),
enchanted_apple: BitmaskValue.new(pos: 0x69, bitmask: 0x01),
lightning_power: BitmaskValue.new(pos: 0x69, bitmask: 0x02),
winged_boots: BitmaskValue.new(pos: 0x69, bitmask: 0x04),
wind_power: BitmaskValue.new(pos: 0x69, bitmask: 0x08),
amulet_of_protection: BitmaskValue.new(pos: 0x69, bitmask: 0x10),
thunder_power: BitmaskValue.new(pos: 0x69, bitmask: 0x20)
}.freeze

The keys define the setter and getter methods, and the value classes handle reading and writing the data.

If you're looking to add support for a new attribute, it may only be necessary to add a new key/value pair and utilize one of the existing value classes.

Adding a new value class

If you need to add a new class to support a new value type, create a new file in lib/god_of_thunder_save/, and start with something like this:

class GodOfThunderSave
  class NewValue
    attr_reader :pos, :bytes

    def initialize(pos:, bytes:)
      @pos = pos
      @bytes = bytes
    end

    def read(file)
      file.seek(pos)
      file.read(bytes)
    end

    def write(file, value)
      file.seek(pos)
      file.write(value)
    end
  end
end

Add a key/value pair of the new attribute and instance of your class to ENTRIES Hash, and you're good to go!

Whenever GodOfThunderSave#read! is called, #read will be called on your class with the save game as a File instance. When GodOfThunderSave#write! is called, #write will be called with a File instance opened in read/write mode, and the data to write.

Each value class is responsible for ensuring that data is read and written correctly, and can safely handle values that could possibly be incorrect or out-of-range for the save game data.

Writing tests

Any new feature should include tests. They are written in RSpec and live in spec/.

For write tests, a real GodOfThunderSave instance is created on real save game data, data is written by the instance, and the save game data is then tested itself. The tests make use of the excellent FakeFS library to mock files, so the fixture data is never modified when tests are performed.

To ensure that we're only altering the save game data we expect, the write tests always inspect the entire file for changes. The save_game_data_changed subject will return a Hash of file positions and their changed values, which is used in every test:

let!(:save_game_data_before) { File.read(save_game_path) }
let(:save_game_data_after) { File.open(save_game_path) }
subject(:save_game_data_changed) do
save_game_data_before.each_char.each_with_object({}) do |byte_before, changes|
pos = save_game_data_after.pos
byte_after = save_game_data_after.readchar
changes[pos] = byte_after if byte_before != byte_after
end
end

Here is an example of a test for writing data:

context "with a new enchanted_apple value" do
before(:each) do
god_of_thunder_save.enchanted_apple = true
write!
end
it { should eq(0x69 => "\x01") }
end

For read tests, the getter methods are used to ensure that the library can correctly parse the save game data:

describe "#enchanted_apple" do
subject(:enchanted_apple) { god_of_thunder_save.enchanted_apple }
it { should eq(false) }
end

Running tests

To run the entire test suite, call rspec with bundle:

bundle exec rspec --format=documentation