class WolfTrans::Patch
Public Class Methods
new(game_path, patch_path)
click to toggle source
# File lib/wolftrans.rb, line 10 def initialize(game_path, patch_path) @strings = Hash.new { |hash, key| hash[key] = Hash.new } load_data(game_path) load_patch(patch_path) end
Public Instance Methods
apply(out_dir)
click to toggle source
Apply the patch to the files in the game path and write them to the output directory
# File lib/wolftrans/patch_data.rb, line 111 def apply(out_dir) out_dir = Util.sanitize_path(out_dir) out_data_dir = "#{out_dir}/Data" # Clear out directory FileUtils.rm_rf(out_dir) FileUtils.mkdir_p("#{out_data_dir}/BasicData") #TODO create directories for each asset # Patch the databases @databases.each do |db_name, db| db.types.each_with_index do |type, type_index| next if type.name.empty? type.data.each_with_index do |datum, datum_index| datum.each_translatable do |str, field| context = Context::Database.from_data(db_name, type_index, type, datum_index, datum, field) yield_translation(str, context) do |newstr| datum[field] = newstr end end end end name_noext = "#{out_data_dir}/BasicData/#{db_name}" db.dump("#{name_noext}.project", "#{name_noext}.dat") end # Patch the common events @common_events.events.each do |event| event.commands.each_with_index do |command, cmd_index| context = Context::CommonEvent.from_data(event, cmd_index, command) patch_command(command, context) end end @common_events.dump("#{out_data_dir}/BasicData/CommonEvent.dat") # Patch Game.dat patch_game_dat @game_dat.dump("#{out_dir}/#{@game_dat_filename}") # Patch all the maps @maps.each do |map_name, map| map.events.each do |event| next unless event event.pages.each do |page| page.commands.each_with_index do |command, cmd_index| context = Context::MapEvent.from_data(map_name, event, page, cmd_index, command) patch_command(command, context) end end end # Translate path assetpath = @assets["mapdata/#{map_name.downcase}.mps"] fullpath = "#{out_data_dir}/#{assetpath}" map.dump(fullpath) end # Copy remaining BasicData files copy_data_files(Util.join_path_nocase(@game_data_dir, 'basicdata'), ['xxxxx', 'dat', 'project', 'png'], "#{out_data_dir}/BasicData") # Copy remaining assets @assets.each_pair do |fn, newfn| filename = get_asset_filename(fn) next unless filename FileUtils.cp(filename, "#{out_data_dir}/#{newfn}") end # Copy fonts if @patch_data_dir copy_data_files(@patch_data_dir, ['ttf','ttc','otf'], out_data_dir) end copy_data_files(@game_data_dir, ['ttf','ttc','otf'], out_data_dir) # Copy remainder of files in the base patch/game dirs copy_files(@patch_assets_dir, out_dir) copy_files(@game_dir, out_dir) end
load_data(game_dir)
click to toggle source
# File lib/wolftrans/patch_data.rb, line 12 def load_data(game_dir) @game_dir = Util.sanitize_path(game_dir) unless Dir.exist? @game_dir raise "could not find game folder '#{@game_dir}'" end @game_data_dir = Util.join_path_nocase(@game_dir, 'data') if @game_data_dir == nil raise "could not find Data folder in '#{@game_dir}'" end # Load databases, Game.dat, and common events @databases = {} basicdata_dir = Util.join_path_nocase(@game_data_dir, 'basicdata') if basicdata_dir == nil raise "could not find BasicData folder in '#{@game_data_dir}'" end Dir.entries(basicdata_dir).each do |entry| entry_downcase = entry.downcase filename = "#{basicdata_dir}/#{entry}" if entry_downcase == 'game.dat' @game_dat_filename = 'Data/BasicData/Game.dat' load_game_dat(filename) elsif entry_downcase.end_with?('.project') next if entry_downcase == 'sysdatabasebasic.project' basename = File.basename(entry_downcase, '.*') dat_filename = Util.join_path_nocase(basicdata_dir, "#{basename}.dat") next if dat_filename == nil load_game_database(filename, dat_filename) elsif entry_downcase == 'commonevent.dat' load_common_events(filename) end end # Game.dat is in a different place on older versions unless @game_dat Dir.entries(@game_dir).each do |entry| if entry.downcase == 'game.dat' @game_dat_filename = 'Game.dat' load_game_dat("#{@game_dir}/#{entry}") break end end end # Gather list of asset and map filenames map_names = Set.new @assets = {} @databases.each_value do |db| db.each_filename do |fn| fn_downcase = fn.downcase @assets[fn_downcase] = fn if fn_downcase.end_with?('.mps') map_names.add(File.basename(fn_downcase, '.*')) end end end @game_dat.each_filename do |fn| @assets[fn.downcase] = fn end @common_events.each_filename do |fn| @assets[fn.downcase] = fn end # Load maps maps_path = Util.join_path_nocase(@game_data_dir, 'mapdata') if maps_path == nil raise "could not find MapData folder in '#{@game_data_dir}'" end @maps = {} map_names.each do |name| map_path = Util.join_path_nocase(maps_path, name + '.mps') if map_path == nil STDERR.puts "warn: could not find map '#{name}'" next end load_map(map_path) end # Gather remaining asset filenames @maps.each_value do |map| map.each_filename do |fn| @assets[fn.downcase] = fn end end # Make sure not to treat certain kinds of filenames as assets @assets.reject! { |k, v| k.start_with?('save/') } # Rewrite asset filenames extcounts = Hash.new(0) @assets.keys.sort.each do |fn| ext = File.extname(fn)[1..-1] @assets[fn] = '%04d.%s' % [extcounts[ext], ext] extcounts[ext] += 1 end end
load_patch(patch_dir)
click to toggle source
Loading Patch
data #
# File lib/wolftrans/patch_text.rb, line 11 def load_patch(patch_dir) @patch_dir = Util.sanitize_path(patch_dir) @patch_assets_dir = "#{@patch_dir}/Assets" @patch_strings_dir = "#{@patch_dir}/Patch" # Make sure these directories all exist [@patch_assets_dir, @patch_strings_dir].each do |dir| FileUtils.mkdir_p dir end # Find data dir @patch_data_dir = Util.join_path_nocase(@patch_assets_dir, 'data') # Load blacklist @file_blacklist = [] if File.exists? "#{patch_dir}/blacklist.txt" Util.read_txt("#{patch_dir}/blacklist.txt").each_line do |line| line.strip! next if line.empty? if line.include? '\\' raise "file specified in blacklist contains a backslash (use a forward slash instead)" end @file_blacklist << line.downcase! end end # Load strings Find.find(@patch_strings_dir) do |path| next if FileTest.directory? path next unless File.extname(path).casecmp '.txt' process_patch_file(path, :load) end # Write back to patch files processed_filenames = [] Find.find(@patch_strings_dir) do |path| next if FileTest.directory? path next unless File.extname(path).casecmp '.txt' process_patch_file(path, :update) processed_filenames << path[@patch_strings_dir.length+1..-1] end # Now "process" any files that should be generated @strings.each do |string, contexts| contexts.each do |context, trans| unless processed_filenames.include? trans.patch_filename process_patch_file("#{@patch_strings_dir}/#{trans.patch_filename}", :update) processed_filenames << trans.patch_filename end end end end
process_patch_file(filename, mode)
click to toggle source
Load the translation strings indicated in the patch file, generate a new patch file with updated context information, and overwrite the patch
# File lib/wolftrans/patch_text.rb, line 68 def process_patch_file(filename, mode) patch_filename = filename[@patch_strings_dir.length+1..-1] txt_version = nil # Parser state information state = :expecting original_string = '' contexts = [] translated_string = '' new_contexts = nil # Variables for the revised patch context_comments = {} # The revised patch output = '' output_write = false pristine_translated_string = '' if File.exists? filename output_write = true if mode == :update Util.read_txt(filename).each_line.with_index do |pristine_line, index| # Remove comments and strip pristine_line.gsub!(/\n$/, '') line = pristine_line.gsub(/(?!\\)#.*$/, '').rstrip comment = pristine_line.match(/(?<!\\)#.*$/).to_s.rstrip line_num = index + 1 if line.start_with? '>' instruction = line.gsub(/^>\s+/, '') # Parse the patch version parse_instruction(instruction, 'WOLF TRANS PATCH FILE VERSION') do |args| unless txt_version == nil raise "two version strings in file (line #{line_num})" end txt_version = Version.new(str: args.first) if txt_version > TXT_VERSION raise "patch version (#{new_version}) newer than can be read (#{TXT_VERSION})" end if mode == :update output << "> WOLF TRANS PATCH FILE VERSION #{TXT_VERSION}" output << comment unless comment.empty? output << "\n" end end # Make sure we have a version specified before reading other instructions if txt_version == nil raise "no version specified before first instruction" end # Now parse the instructions parse_instruction(instruction, 'BEGIN STRING') do |args| unless state == :expecting raise "began another string without ending previous string (line #{line_num})" end state = :reading_original original_string = '' if mode == :update output << pristine_line << "\n" end end parse_instruction(instruction, 'END STRING') do |args| if state == :expecting raise "ended string without a begin (line #{line_num})" elsif state == :reading_original raise "ended string without a translation block (line #{line_num})" end state = :expecting new_contexts = [] end parse_instruction(instruction, 'CONTEXT') do |args| if state == :expecting raise "context outside of begin/end block (line #{line_num})" end if args.empty? raise "no context string provided in context line (line #{line_num})" end # After a context, we're no longer reading the original text. state = :reading_translation begin new_context = Context.from_string(args.shift) rescue => e raise e, "#{e} (line #{line_num})", e.backtrace end # Append context if translated_string is empty, since that means # no translation was given. if translated_string.empty? contexts << new_context else new_contexts = [new_context] end if mode == :update # Save the comment for later context_comments[new_context] = comment end end # If we have a new context list queued, flush the translation to all # of the collected contexts if new_contexts original_string_new = unescape_string(original_string, false) translated_string_new = unescape_string(translated_string, true) contexts.each do |context| if mode == :update # Write an appropriate context line to the output output << '> CONTEXT ' if @strings.include?(original_string_new) && @strings[original_string_new].include?(context) output << @strings[original_string_new].select { |k,v| k.eql? context }.keys.first.to_s output << ' < UNTRANSLATED' if translated_string_new.empty? else output << context.to_s << ' < UNUSED' end output << " " << context_comments[context] unless comment.empty? output << "\n" else # Put translation in hash @strings[original_string_new][context] = Translation.new(patch_filename, translated_string_new, false) end end if mode == :update # Write the translation output << pristine_translated_string.rstrip << "\n" # If the state is "expecting", that means we need to write the END STRING # line to the output too. if state == :expecting output << pristine_line << "\n" end end # Reset variables for next read translated_string = '' pristine_translated_string = '' contexts = new_contexts new_contexts = nil end else # Parse text if state == :expecting unless line.empty? raise "stray text outside of begin/end block (line #{line_num})" end elsif state == :reading_original original_string << line << "\n" elsif state == :reading_translation translated_string << line << "\n" if mode == :update pristine_translated_string << pristine_line << "\n" end end # Make no modifications to the patch line if we're not reading translations unless state == :reading_translation if mode == :update output << pristine_line << "\n" end end end end # Final error checking if state != :expecting raise "final begin/end block has no end" end else # It's a new file, so just stick a header on it if mode == :update output << "> WOLF TRANS PATCH FILE VERSION #{TXT_VERSION}\n" end end if mode == :update # Write all the new strings to the file @strings.each do |orig_string, contexts| if contexts.values.any? { |trans| trans.autogenerate? && trans.patch_filename == patch_filename } output_write = true output << "\n> BEGIN STRING\n#{escape_string(orig_string)}\n" contexts.each do |context, trans| next unless trans.autogenerate? trans.autogenerate = false output << "> CONTEXT " << context.to_s << " < UNTRANSLATED\n" end output << "\n> END STRING\n" end end # Write the output to the file if output_write FileUtils.mkdir_p(File.dirname(filename)) File.open(filename, 'wb') { |file| file.write(output) } end end end
Private Instance Methods
copy_data_files(src_dir, extensions, out_dir)
click to toggle source
Copy data files
# File lib/wolftrans/patch_data.rb, line 367 def copy_data_files(src_dir, extensions, out_dir) Dir.entries(src_dir).each do |entry| # Don't care about directories next if entry == '.' || entry == '..' path = "#{src_dir}/#{entry}" next if FileTest.directory? path # Skip invalid file extensions next unless extensions.include? File.extname(entry)[1..-1] # Copy the file if it doesn't already exist next if Util.join_path_nocase(out_dir, entry) FileUtils.cp(path, "#{out_dir}/#{entry}") end end
copy_files(src_dir, out_dir)
click to toggle source
Copy normal, non-data files
# File lib/wolftrans/patch_data.rb, line 342 def copy_files(src_dir, out_dir) Find.find(src_dir) do |path| basename = File.basename(path) basename_downcase = basename.downcase # Don't do anything in Data/ Find.prune if basename_downcase == 'data' && File.dirname(path) == src_dir # Skip directories next if FileTest.directory? path # "Short name", relative to the game base dir short_path = path[src_dir.length+1..-1] Find.prune if @file_blacklist.include? short_path.downcase out_path = "#{out_dir}/#{short_path}" next if ['thumbs.db', 'desktop.ini', '.ds_store'].include? basename_downcase next if File.exist? out_path # Make directory only only when copying a file to avoid making empty directories FileUtils.mkdir_p(File.dirname(out_path)) FileUtils.cp(path, out_path) end end
escape_string(string)
click to toggle source
Escapes a string for writing into a patch file
# File lib/wolftrans/patch_text.rb, line 305 def escape_string(string) string = string.gsub("\\", "\x00") #HACK string.gsub!("\t", "\\t") string.gsub!("\r", "\\r") string.gsub!(">", "\\>") string.gsub!("#", "\\#") string.gsub!("\x00") { "\\\\" } #HACK # Replace trailing spaces with \s string.gsub!(/ *$/) { |m| "\\s" * m.length } # Replace trailing newlines with \n string.gsub!(/\n*\z/m) { |m| "\\n" * m.length } string end
get_asset_filename(fn)
click to toggle source
Get a full filename based on a short asset filename
# File lib/wolftrans/patch_data.rb, line 385 def get_asset_filename(fn) # Find the correct filename case dirname, basename = fn.split('/') if @patch_data_dir path = Util.join_path_nocase(@patch_data_dir, dirname) if path path = Util.join_path_nocase(path, basename) return path if path end end path = Util.join_path_nocase(@game_data_dir, dirname) if path path = Util.join_path_nocase(path, basename) return path if path end return nil end
load_common_events(filename)
click to toggle source
# File lib/wolftrans/patch_data.rb, line 248 def load_common_events(filename) @common_events = WolfRpg::CommonEvents.new(filename) @common_events.events.each do |event| patch_filename = "dump/common/#{'%03d' % event.id}_#{Util.escape_path(event.name)}.txt" event.commands.each_with_index do |command, cmd_index| strings_of_command(command) do |string| @strings[string][Context::CommonEvent.from_data(event, cmd_index, command)] ||= Translation.new(patch_filename) end end end end
load_game_dat(filename)
click to toggle source
# File lib/wolftrans/patch_data.rb, line 211 def load_game_dat(filename) patch_filename = 'dump/GameDat.txt' @game_dat = WolfRpg::GameDat.new(filename) unless @game_dat.title.empty? @strings[@game_dat.title][Context::GameDat.from_data('Title')] = Translation.new(patch_filename) end unless @game_dat.version.empty? @strings[@game_dat.version][Context::GameDat.from_data('Version')] = Translation.new(patch_filename) end unless @game_dat.font.empty? @strings[@game_dat.font][Context::GameDat.from_data('Font')] = Translation.new(patch_filename) end @game_dat.subfonts.each_with_index do |sf, i| unless sf.empty? name = 'SubFont' + (i + 1).to_s @strings[sf][Context::GameDat.from_data(name)] ||= Translation.new(patch_filename) end end end
load_game_database(project_filename, dat_filename)
click to toggle source
# File lib/wolftrans/patch_data.rb, line 232 def load_game_database(project_filename, dat_filename) db_name = File.basename(project_filename, '.*') db = WolfRpg::Database.new(project_filename, dat_filename) db.types.each_with_index do |type, type_index| next if type.name.empty? patch_filename = "dump/db/#{db_name}/#{Util.escape_path(type.name)}.txt" type.data.each_with_index do |datum, datum_index| datum.each_translatable do |str, field| context = Context::Database.from_data(db_name, type_index, type, datum_index, datum, field) @strings[str][context] ||= Translation.new(patch_filename) end end end @databases[db_name] = db end
load_map(filename)
click to toggle source
# File lib/wolftrans/patch_data.rb, line 192 def load_map(filename) map_name = File.basename(filename, '.*') patch_filename = "dump/mps/#{map_name}.txt" map = WolfRpg::Map.new(filename) map.events.each do |event| next unless event event.pages.each do |page| page.commands.each_with_index do |command, cmd_index| strings_of_command(command) do |string| @strings[string][Context::MapEvent.from_data(map_name, event, page, cmd_index, command)] ||= Translation.new(patch_filename) end end end end @maps[map_name] = map end
parse_instruction(instruction, tested_instruction) { || ... }
click to toggle source
Yields the arguments of the instruction if it matches tested_instruction
# File lib/wolftrans/patch_text.rb, line 270 def parse_instruction(instruction, tested_instruction) if instruction.start_with? tested_instruction args = instruction.slice(tested_instruction.length..-1).strip.split(/\s+<\s+/) if args.first == nil || args.first.empty? yield [] else yield args end end end
patch_command(command, context)
click to toggle source
# File lib/wolftrans/patch_data.rb, line 282 def patch_command(command, context) case command when WolfRpg::Command::Message yield_translation(command.text, context) do |str| command.text = str end when WolfRpg::Command::Choices command.text.each_with_index do |text, i| yield_translation(text, context) do |str| command.text[i] = str end end when WolfRpg::Command::StringCondition command.string_args.each_with_index do |arg, i| next if arg.empty? yield_translation(arg, context) do |str| command.string_args[i] = str end end when WolfRpg::Command::SetString yield_translation(command.text, context) do |str| command.text = str end when WolfRpg::Command::Picture if command.type == :text yield_translation(command.text, context) do |str| command.text = str end end end end
patch_game_dat()
click to toggle source
# File lib/wolftrans/patch_data.rb, line 314 def patch_game_dat yield_translation(@game_dat.title, Context::GameDat.from_data('Title')) do |str| @game_dat.title = str end yield_translation(@game_dat.version, Context::GameDat.from_data('Version')) do |str| @game_dat.version = str end yield_translation(@game_dat.font, Context::GameDat.from_data('Font')) do |str| @game_dat.font = str end @game_dat.subfonts.each_with_index do |sf, i| name = 'SubFont' + (i + 1).to_s yield_translation(sf, Context::GameDat.from_data(name)) do |str| @game_dat.subfonts[i] = str end end end
strings_of_command(command) { |text| ... }
click to toggle source
# File lib/wolftrans/patch_data.rb, line 261 def strings_of_command(command) case command when WolfRpg::Command::Message yield command.text if Util.translatable? command.text when WolfRpg::Command::Choices command.text.each do |s| yield s if Util.translatable? s end when WolfRpg::Command::StringCondition command.string_args.each do |s| yield s if Util.translatable? s end when WolfRpg::Command::SetString yield command.text if Util.translatable? command.text when WolfRpg::Command::Picture if command.type == :text yield command.text if Util.translatable? command.text end end end
unescape_string(string, do_rtrim)
click to toggle source
Unescapes a patch file string
# File lib/wolftrans/patch_text.rb, line 282 def unescape_string(string, do_rtrim) # Remove trailing whitespace or just newline if do_rtrim string = string.rstrip else string = string.gsub(/\n\z/m, '') end # Change escape sequences string.match(/(?<!\\)\\[^sntr><#\\]/) do |match| raise "unknown escape sequence '#{match}' #{string}" end string.gsub!("\\\\", "\x00") #HACK string.gsub!("\\s", " ") string.gsub!("\\n", "\n") string.gsub!("\\t", "\t") string.gsub!("\\r", "\r") string.gsub!("\\>", ">") string.gsub!("\\#", "#") string.gsub!("\x00") { "\\" } #HACK string end
yield_translation(string, context) { |str| ... }
click to toggle source
Yield a translation for the given string and context if it exists
# File lib/wolftrans/patch_data.rb, line 333 def yield_translation(string, context) return unless Util.translatable? string if @strings.include? string str = @strings[string][context].string yield str if Util.translatable? str end end