# RPG Maker VX Ace data conversion utility

This topic is 1660 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

## Recommended Posts

I picked up RPG Maker VX Ace in the Steam summer sale, and really my only complaint is that it stores the game data all in binary so using version control with it is highly annoying. While there are scripts to convert the data to YAML for VX and XP I couldn't find one that worked with VX Ace, so I wrote my own. (And as an exercise in masochism I tinkered with it until it would work with VX and XP.) Since I noticed some discussion about other people picking up RPG Maker during the Steam sale, I've decided to share it here.

I used cygwin's ruby 1.9.3 and the Psych 2.0.0 ruby gem, which appears to be the most recent version. However, Psych 2.0.0 has some bugs that impacted the generated YAML (one major and one minor) which I monkey patched, and since I was already rewriting the Psych code, I added some functionality to make the generated YAML prettier. Long story short, this code probably won't work with any version of Psych but 2.0.0.

Basic functionality: you point the RGSS.serialize function at the directory that contains the project file and it will read the contents of the Data/ directory and dump YAML for the data files in a new YAML/ directory and the scripts in a Scripts/ directory. It can also convert save files to YAML which will be in the same directory as the project file. And, of course, it can reverse all these operations.

psych_mods.rb:
[spoiler]
=begin
This file contains significant portions of Psych 2.0.0 to modify behavior and to fix

Copyright 2009 Aaron Patterson, et al.

Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the 'Software'), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to permit
persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or
substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
=end

gem 'psych', '2.0.0'
require 'psych'

if Psych::VERSION == '2.0.0'
# Psych bugs:
#
# 1) Psych has a bug where it stores an anchor to the YAML for an object, but indexes
# the reference by object_id. This doesn't keep the object alive, so if it gets garbage
# collected, Ruby might generate an object with the same object_id and try to generate a
# reference to the stored anchor. This monkey-patches the Registrar to keep the object
# alive so incorrect references aren't generated. The bug is also present in Psych 1.3.4
# but there isn't a convenient way to patch that.
#
# 2) Psych also doesn't create references and anchors for classes that implement
# encode_with. This modifies dump_coder to handle that situation.
#
# :sort - sort hashes and instance variables for objects
# :flow_classes - array of class types that will automatically emit with flow style
#                 rather than block style
module Psych
module Visitors
class YAMLTree < Psych::Visitors::Visitor
class Registrar
old_initialize = self.instance_method(:initialize)
define_method(:initialize) do
old_initialize.bind(self).call
@obj_to_obj  = {}
end

old_register = self.instance_method(:register)
define_method(:register) do |target, node|
old_register.bind(self).call(target, node)
@obj_to_obj[target.object_id] = target
end
end

remove_method(:visit_Hash)
def visit_Hash o
tag      = o.class == ::Hash ? nil : "!ruby/hash:#{o.class}"
implicit = !tag

register(o, @emitter.start_mapping(nil, tag, implicit, Nodes::Mapping::BLOCK))

keys = o.keys
keys = keys.sort if @options[:sort]
keys.each do |k|
accept k
accept o[k]
end

@emitter.end_mapping
end

remove_method(:visit_Object)
def visit_Object o
tag = Psych.dump_tags[o.class]
unless tag
klass = o.class == Object ? nil : o.class.name
tag   = ['!ruby/object', klass].compact.join(':')
end

if @options[:flow_classes] && @options[:flow_classes].include?(o.class)
style = Nodes::Mapping::FLOW
else
style = Nodes::Mapping::BLOCK
end

map = @emitter.start_mapping(nil, tag, false, style)
register(o, map)

dump_ivars o
@emitter.end_mapping
end

remove_method(:dump_coder)
def dump_coder o
@coders << o
tag = Psych.dump_tags[o.class]
unless tag
klass = o.class == Object ? nil : o.class.name
tag   = ['!ruby/object', klass].compact.join(':')
end

c = Psych::Coder.new(tag)
o.encode_with(c)
register o, emit_coder(c)
end

remove_method(:dump_ivars)
def dump_ivars target
ivars = find_ivars target
ivars = ivars.sort() if @options[:sort]

ivars.each do |iv|
@emitter.scalar("#{iv.to_s.sub(/^@/, '')}", nil, nil, true, false, Nodes::Scalar::ANY)
accept target.instance_variable_get(iv)
end
end

end
end
end
else
warn "Warning: Psych 2.0.0 not detected" if $VERBOSE end  [/spoiler] RGSS.rb: [spoiler] =begin Copyright (c) 2013 Howard Jeng Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. =end require 'scanf' class Table def initialize(bytes) @dim, @x, @y, @z, items, *@data = bytes.unpack('L5 S*') raise "Size mismatch loading Table from data" unless items == @data.length raise "Size mismatch loading Table from data" unless @x * @y * @z == items end MAX_ROW_LENGTH = 20 def encode_with(coder) coder.style = Psych::Nodes::Mapping::BLOCK coder['dim'] = @dim coder['x'] = @x coder['y'] = @y coder['z'] = @z if @x * @y * @z > 0 stride = @x < 2 ? (@y < 2 ? @z : @y) : @x rows = @data.each_slice(stride).to_a if MAX_ROW_LENGTH != -1 && stride > MAX_ROW_LENGTH block_length = (stride + MAX_ROW_LENGTH - 1) / MAX_ROW_LENGTH row_length = (stride + block_length - 1) / block_length rows = rows.collect{|x| x.each_slice(row_length).to_a}.flatten(1) end rows = rows.collect{|x| x.collect{|y| "%04x" % y}.join(" ")} coder['data'] = rows else coder['data'] = [] end end def init_with(coder) @dim = coder['dim'] @x = coder['x'] @y = coder['y'] @z = coder['z'] @data = coder['data'].collect{|x| x.split(" ").collect{|y| y.hex}}.flatten items = @x * @y * @z raise "Size mismatch loading Table from YAML" unless items == @data.length end def _dump(*ignored) return [@dim, @x, @y, @z, @x * @y * @z, *@data].pack('L5 S*') end def self._load(bytes) Table.new(bytes) end end class Color def initialize(bytes) @r, @g, @b, @a = *bytes.unpack('D4') end def _dump(*ignored) return [@r, @g, @b, @a].pack('D4') end def self._load(bytes) Color.new(bytes) end end class Tone def initialize(bytes) @r, @g, @b, @a = *bytes.unpack('D4') end def _dump(*ignored) return [@r, @g, @b, @a].pack('D4') end def self._load(bytes) Tone.new(bytes) end end class Rect def initialize(bytes) @x, @y, @width, @height = *bytes.unpack('i4') end def _dump(*ignored) return [@x, @y, @width, @height].pack('i4') end def self._load(bytes) Rect.new(bytes) end end def remove_defined_method(scope, name) scope.send(:remove_method, name) if scope.instance_methods(false).include?(name) end def reset_method(scope, name, method) remove_defined_method(scope, name) scope.send(:define_method, name, method) end def reset_const(scope, sym, value) scope.send(:remove_const, sym) if scope.const_defined?(sym) scope.send(:const_set, sym, value) end def array_to_hash(arr, &block) h = {} arr.each_with_index do |val, index| r = block_given? ? block.call(val) : val h[index] = r unless r.nil? end if arr.length > 0 last = arr.length - 1 h[last] = nil unless h.has_key?(last) end return h end def hash_to_array(hash) arr = [] hash.each do |k, v| arr[k] = v end return arr end module BasicCoder def encode_with(coder) ivars.each do |var| name = var.to_s.sub(/^@/, '') value = instance_variable_get(var) coder[name] = encode(name, value) end end def encode(name, value) return value end def init_with(coder) coder.map.each do |key, value| sym = "@#{key}".to_sym instance_variable_set(sym, decode(key, value)) end end def decode(name, value) return value end def ivars return instance_variables end INCLUDED_CLASSES = [] def self.included(mod) INCLUDED_CLASSES.push(mod) end def self.set_ivars_methods(version) INCLUDED_CLASSES.each do |c| if version == :ace reset_method(c, :ivars, ->{ return instance_variables }) else reset_method(c, :ivars, ->{ return instance_variables.sort }) end end end end class Game_Switches include BasicCoder def encode(name, value) return array_to_hash(value) end def decode(name, value) return hash_to_array(value) end end class Game_Variables include BasicCoder def encode(name, value) return array_to_hash(value) end def decode(name, value) return hash_to_array(value) end end class Game_SelfSwitches include BasicCoder def encode(name, value) return Hash[value.collect {|pair| key, value = pair next ["%03d %03d %s" % key, value] }] end def decode(name, value) return Hash[value.collect {|pair| key, value = pair next [key.scanf("%d %d %s"), value] }] end end class Game_System include BasicCoder def encode(name, value) if name == 'version_id' return map_version(value) else return value end end end module RPG class System include BasicCoder HASHED_VARS = ['variables', 'switches'] def encode(name, value) if HASHED_VARS.include?(name) return array_to_hash(value) {|val| reduce_string(val)} elsif name == 'version_id' return map_version(value) else return value end end def decode(name, value) if HASHED_VARS.include?(name) return hash_to_array(value) else return value end end end class EventCommand def encode_with(coder) raise 'Unexpected number of instance variables' if instance_variables.length != 3 clean case @code when MOVE_LIST_CODE # move list coder.style = Psych::Nodes::Mapping::BLOCK else coder.style = Psych::Nodes::Mapping::FLOW end coder['i'], coder['c'], coder['p'] = @indent, @code, @parameters end def init_with(coder) @indent, @code, @parameters = coder['i'], coder['c'], coder['p'] end end end module RGSS # creates an empty class in a potentially nested scope def self.process(root, name, *args) if args.length > 0 process(root.const_get(name), *args) else root.const_set(name, Class.new) unless root.const_defined?(name, false) end end # other classes that don't need definitions [ # RGSS data structures [:RPG, :Actor], [:RPG, :Animation], [:RPG, :Animation, :Frame], [:RPG, :Animation, :Timing], [:RPG, :Area], [:RPG, :Armor], [:RPG, :AudioFile], [:RPG, :BaseItem], [:RPG, :BaseItem, :Feature], [:RPG, :BGM], [:RPG, :BGS], [:RPG, :Class], [:RPG, :Class, :Learning], [:RPG, :CommonEvent], [:RPG, :Enemy], [:RPG, :Enemy, :Action], [:RPG, :Enemy, :DropItem], [:RPG, :EquipItem], [:RPG, :Event], [:RPG, :Event, :Page], [:RPG, :Event, :Page, :Condition], [:RPG, :Event, :Page, :Graphic], [:RPG, :Item], [:RPG, :Map], [:RPG, :Map, :Encounter], [:RPG, :MapInfo], [:RPG, :ME], [:RPG, :MoveCommand], [:RPG, :MoveRoute], [:RPG, :SE], [:RPG, :Skill], [:RPG, :State], [:RPG, :System, :Terms], [:RPG, :System, :TestBattler], [:RPG, :System, :Vehicle], [:RPG, :System, :Words], [:RPG, :Tileset], [:RPG, :Troop], [:RPG, :Troop, :Member], [:RPG, :Troop, :Page], [:RPG, :Troop, :Page, :Condition], [:RPG, :UsableItem], [:RPG, :UsableItem, :Damage], [:RPG, :UsableItem, :Effect], [:RPG, :Weapon], # Script classes serialized in save game files [:Game_ActionResult], [:Game_Actor], [:Game_Actors], [:Game_BaseItem], [:Game_BattleAction], [:Game_CommonEvent], [:Game_Enemy], [:Game_Event], [:Game_Follower], [:Game_Followers], [:Game_Interpreter], [:Game_Map], [:Game_Message], [:Game_Party], [:Game_Picture], [:Game_Pictures], [:Game_Player], [:Game_System], [:Game_Timer], [:Game_Troop], [:Game_Screen], [:Game_Vehicle], [:Interpreter] ].each {|x| process(Object, *x)} def self.setup_system(version, options) # convert variable and switch name arrays to a hash when serialized # if round_trip isn't set change version_id to fixed number if options[:round_trip] iso = ->(val) { return val } reset_method(RPG::System, :reduce_string, iso) reset_method(RPG::System, :map_version, iso) reset_method(Game_System, :map_version, iso) else reset_method(RPG::System, :reduce_string, ->(str) { return nil if str.nil? stripped = str.strip return stripped.empty? ? nil : stripped }) # These magic numbers should be different. If they are the same, the saved version # of the map in save files will be used instead of any updated version of the map reset_method(RPG::System, :map_version, ->(ignored) { return 12345678 }) reset_method(Game_System, :map_version, ->(ignored) { return 87654321 }) end end def self.setup_interpreter(version) # Game_Interpreter is marshalled differently in VX Ace if version == :ace reset_method(Game_Interpreter, :marshal_dump, ->{ return @data }) reset_method(Game_Interpreter, :marshal_load, ->(obj) { @data = obj }) else remove_defined_method(Game_Interpreter, :marshal_dump) remove_defined_method(Game_Interpreter, :marshal_load) end end def self.setup_event_command(version, options) # format event commands to flow style for the event codes that aren't move commands if options[:round_trip] reset_method(RPG::EventCommand, :clean, ->{}) else reset_method(RPG::EventCommand, :clean, ->{ @parameters[0].rstrip! if @code == 401 }) end reset_const(RPG::EventCommand, :MOVE_LIST_CODE, version == :xp ? 209 : 205) end def self.setup_classes(version, options) setup_system(version, options) setup_interpreter(version) setup_event_command(version, options) BasicCoder.set_ivars_methods(version) end FLOW_CLASSES = [Color, Tone, RPG::BGM, RPG::BGS, RPG::MoveCommand, RPG::SE] SCRIPTS_BASE = 'Scripts' ACE_DATA_EXT = '.rvdata2' VX_DATA_EXT = '.rvdata' XP_DATA_EXT = '.rxdata' YAML_EXT = '.yaml' RUBY_EXT = '.rb' def self.get_data_directory(base) return File.join(base, 'Data') end def self.get_yaml_directory(base) return File.join(base, 'YAML') end def self.get_script_directory(base) return File.join(base, 'Scripts') end end  [/spoiler] serialize.rb: [spoiler] =begin Copyright (c) 2013 Howard Jeng Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. =end require_relative './RGSS' require_relative './psych_mods' require 'fileutils' require 'zlib' module RGSS def self.change_extension(file, new_ext) return File.basename(file, '.*') + new_ext end def self.sanitize_filename(filename) return filename.gsub(/[^0-9A-Za-z]+/, '_') end def self.files_with_extension(directory, extension) return Dir.entries(directory).select{|file| File.extname(file) == extension} end def self.inflate(str) text = Zlib::Inflate.inflate(str) return text.force_encoding("UTF-8") end def self.deflate(str) return Zlib::Deflate.deflate(str, Zlib::BEST_COMPRESSION) end def self.dump_data_file(file, data, time, options) File.open(file, "wb") do |f| Marshal.dump(data, f) end File.utime(time, time, file) end def self.dump_yaml_file(file, data, time, options) File.open(file, "wb") do |f| Psych::dump(data, f, options) end File.utime(time, time, file) end def self.dump_save(file, data, time, options) File.open(file, "wb") do |f| data.each do |chunk| Marshal.dump(chunk, f) end end File.utime(time, time, file) end def self.dump_raw_file(file, data, time, options) File.open(file, "wb") do |f| f.write(data) end File.utime(time, time, file) end def self.dump(dumper, file, data, time, options) self.method(dumper).call(file, data, time, options) rescue warn "Exception dumping #{file}" raise end def self.load_data_file(file) File.open(file, "rb") do |f| return Marshal.load(f) end end def self.load_yaml_file(file) File.open(file, "rb") do |f| return Psych::load(f) end end def self.load_raw_file(file) File.open(file, "rb") do |f| return f.read end end def self.load_save(file) File.open(file, "rb") do |f| data = [] while not f.eof? o = Marshal.load(f) data.push(o) end return data end end def self.load(loader, file) return self.method(loader).call(file) rescue warn "Exception loading #{file}" raise end def self.scripts_to_text(dirs, src, dest, options) src_file = File.join(dirs[:data], src) dest_file = File.join(dirs[:yaml], dest) raise "Missing #{src}" unless File.exists?(src_file) script_entries = load(:load_data_file, src_file) check_time = !options[:force] && File.exists?(dest_file) oldest_time = File.mtime(dest_file) if check_time file_map, script_index, script_code = Hash.new(-1), [], {} script_entries.each do |script| magic_number, script_name, code = script[0], script[1], inflate(script[2]) script_name.force_encoding("UTF-8") if code.length > 0 filename = script_name.empty? ? 'blank' : sanitize_filename(script_name) key = filename.upcase value = (file_map[key] += 1) actual_filename = filename + (value == 0 ? "" : ".#{value}") + RUBY_EXT script_index.push([magic_number, script_name, actual_filename]) full_filename = File.join(dirs[:script], actual_filename) script_code[full_filename] = code check_time = false unless File.exists?(full_filename) oldest_time = [File.mtime(full_filename), oldest_time].min if check_time else script_index.push([magic_number, script_name, nil]) end end src_time = File.mtime(src_file) if check_time && (src_time - 1) < oldest_time puts "Skipping scripts to text" if$VERBOSE
else
puts "Converting scripts to text" if $VERBOSE dump(:dump_yaml_file, dest_file, script_index, src_time, options) script_code.each {|file, code| dump(:dump_raw_file, file, code, src_time, options)} end end def self.scripts_to_binary(dirs, src, dest, options) src_file = File.join(dirs[:yaml], src) dest_file = File.join(dirs[:data], dest) raise "Missing #{src}" unless File.exists?(src_file) check_time = !options[:force] && File.exists?(dest_file) newest_time = File.mtime(src_file) if check_time index = load(:load_yaml_file, src_file) script_entries = [] index.each do |entry| magic_number, script_name, filename = entry code = '' if filename full_filename = File.join(dirs[:script], filename) raise "Missing script file #{filename}" unless File.exists?(full_filename) newest_time = [File.mtime(full_filename), newest_time].max if check_time code = load(:load_raw_file, full_filename) end script_entries.push([magic_number, script_name, deflate(code)]) end if check_time && (newest_time - 1) < File.mtime(dest_file) puts "Skipping scripts to binary" if$VERBOSE
else
puts "Converting scripts to binary" if $VERBOSE dump(:dump_data_file, dest_file, script_entries, newest_time, options) end end def self.process_file(file, src_file, dest_file, dest_ext, loader, dumper, options) src_time = File.mtime(src_file) if !options[:force] && File.exists?(dest_file) && (src_time - 1) < File.mtime(dest_file) puts "Skipping #{file}" if$VERBOSE
else
puts "Converting #{file} to #{dest_ext}" if $VERBOSE data = load(loader, src_file) dump(dumper, dest_file, data, src_time, options) end end def self.convert(src, dest, options) files = files_with_extension(src[:directory], src[:ext]) files -= src[:exclude] files.each do |file| src_file = File.join(src[:directory], file) dest_file = File.join(dest[:directory], change_extension(file, dest[:ext])) process_file(file, src_file, dest_file, dest[:ext], src[:load_file], dest[:dump_file], options) end end def self.convert_saves(base, src, dest, options) save_files = files_with_extension(base, src[:ext]) save_files.each do |file| src_file = File.join(base, file) dest_file = File.join(base, change_extension(file, dest[:ext])) process_file(file, src_file, dest_file, dest[:ext], src[:load_save], dest[:dump_save], options) end end # [version] one of :ace, :vx, :xp # [direction] one of :data_bin_to_text, :data_text_to_bin, :save_bin_to_text, # :save_text_to_bin, :scripts_bin_to_text, :scripts_text_to_bin, # :all_text_to_bin, :all_bin_to_text # [directory] directory that project file is in # [options] :force - ignores file dates when converting (default false) # :round_trip - create yaml data that matches original marshalled data skips # data cleanup operations (default false) # :line_width - line width form YAML files, -1 for no line width limit # (default 130) # :table_width - maximum number of entries per row for table data, -1 for no # table row limit (default 20) def self.serialize(version, direction, directory, options = {}) raise "#{directory} not found" unless File.exist?(directory) setup_classes(version, options) options = options.clone() options[:sort] = true if [:vx, :xp].include?(version) options[:flow_classes] = FLOW_CLASSES options[:line_width] ||= 130 table_width = options[:table_width] reset_const(Table, :MAX_ROW_LENGTH, table_width ? table_width : 20) base = File.realpath(directory) dirs = { :base => base, :data => get_data_directory(base), :yaml => get_yaml_directory(base), :script => get_script_directory(base) } dirs.values.each do |d| FileUtils.mkdir(d) unless File.exists?(d) end exts = { :ace => ACE_DATA_EXT, :vx => VX_DATA_EXT, :xp => XP_DATA_EXT } yaml_scripts = SCRIPTS_BASE + YAML_EXT yaml = { :directory => dirs[:yaml], :exclude => [yaml_scripts], :ext => YAML_EXT, :load_file => :load_yaml_file, :dump_file => :dump_yaml_file, :load_save => :load_yaml_file, :dump_save => :dump_yaml_file } scripts = SCRIPTS_BASE + exts[version] data = { :directory => dirs[:data], :exclude => [scripts], :ext => exts[version], :load_file => :load_data_file, :dump_file => :dump_data_file, :load_save => :load_save, :dump_save => :dump_save } case direction when :data_bin_to_text convert(data, yaml, options) scripts_to_text(dirs, scripts, yaml_scripts, options) when :data_text_to_bin convert(yaml, data, options) scripts_to_binary(dirs, yaml_scripts, scripts, options) when :save_bin_to_text convert_saves(base, data, yaml, options) when :save_text_to_bin convert_saves(base, yaml, data, options) when :scripts_bin_to_text scripts_to_text(dirs, scripts, yaml_scripts, options) when :scripts_text_to_bin scripts_to_binary(dirs, yaml_scripts, scripts, options) when :all_bin_to_text convert(data, yaml, options) scripts_to_text(dirs, scripts, yaml_scripts, options) convert_saves(base, data, yaml, options) when :all_text_to_bin convert(yaml, data, options) scripts_to_binary(dirs, yaml_scripts, scripts, options) convert_saves(base, yaml, data, options) else raise "Unrecognized direction :#{direction}" end end end if __FILE__ ==$0
options = {:force => true, :line_width => -1, :table_width => -1}

RGSS.serialize(:ace, :data_bin_to_text, '.', options)
RGSS.serialize(:ace, :save_bin_to_text, '.', options)
RGSS.serialize(:ace, :data_text_to_bin, '.', options)
RGSS.serialize(:ace, :save_text_to_bin, '.', options)
end

[/spoiler]
Note that this is my first ruby program so some bits are written in a convoluted way just because I wanted to try out an interesting language feature.

##### Share on other sites

VX Ace correctly implements 'require'? That was one of my main beefs with XP. How much are they selling it for?

Ah, nvm, you're running this from Ruby.

This looks like normal Ruby, btw. It's not that convoluted. I'm not sure if you have to remove a method before replacing it, though. I think you can just overwrite them.

Also:

def hash_to_array(hash)
arr = []
hash.each do |k, v|
arr[k] = v #what if the key is not an integer?
end
return arr
end

If you have contiguous key values then you can just:

array = hash.sort.values

Although looking at the inverse function I'm not really sure about what's going on there.

Edited by Khatharr

##### Share on other sites
You don't need to remove a method before replacing it, but doing so silences a warning if you run ruby with -w. Since I output some information based on \$VERBOSE, I don't want language warnings cluttering the output.

As for array_to_hash and has_to_array, there are some potentially sparse arrays in the data. A good example are the Game_Switches and Game_Variables classes. Let's say you want to see if a switch or variable has the correct value in a save file. If I left the data in an array the YAML output would look something like
  -
-
- true
- false
-

and so on. One minor problem: this wastes a lot of vertical space. However, it's also really annoying to count through the array elements to find the one your looking for. So I convert the array to a hash where the keys are the original array indices. Then the YAML looks like:
 2: true
3: false

Since the hash should always be generated from an array, there shouldn't be a situation where the key isn't an integer. Also, since these are normal ruby arrays indexed starting from 0, but RPG Maker starts the switches and variables from 1, the situation where there are contiguous key values is pretty much never going to happen.

The list of switch and variable names stored in the System class are similar, though the use case for me is more along the lines of having the index there so I can get useful information if I grep the YAML output. So if I don't remember which variable number I made the faction standing variable, I can use grep "faction standing" System.yaml and get the number quickly.

##### Share on other sites

Ah, I get ya. I didn't look to see what it was used for.