Class: WolfTrans::Patch

Inherits:
Object
  • Object
show all
Defined in:
lib/wolftrans.rb,
lib/wolftrans/patch_data.rb,
lib/wolftrans/patch_text.rb

Instance Method Summary collapse

Constructor Details

#initialize(game_path, patch_path) ⇒ Patch

Returns a new instance of Patch.



10
11
12
13
14
# 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

Instance Method Details

#apply(out_dir) ⇒ Object

Apply the patch to the files in the game path and write them to the output directory



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# 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) ⇒ Object



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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
102
103
104
105
106
107
# 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) ⇒ Object

Loading Patch data #



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# 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) ⇒ Object

Load the translation strings indicated in the patch file, generate a new patch file with updated context information, and overwrite the patch



68
69
70
71
72
73
74
75
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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# 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