wolftrans 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,193 @@
1
+ module WolfTrans
2
+ # Represents the context of a translatable string
3
+ class Context
4
+ def eql?(other)
5
+ self.class == other.class
6
+ end
7
+
8
+ # Parse a string to determine context
9
+ def self.from_string(string)
10
+ pair = string.split(':', 2)
11
+ if pair.size != 2
12
+ raise "malformed context line"
13
+ end
14
+ type, path = pair
15
+ path = path.split('/')
16
+
17
+ case type
18
+ when 'MPS'
19
+ return MapEvent.from_string(path)
20
+ when 'GAMEDAT'
21
+ return GameDat.from_string(path)
22
+ when 'DB'
23
+ return Database.from_string(path)
24
+ when 'COMMONEVENT'
25
+ return CommonEvent.from_string(path)
26
+ end
27
+ raise "unrecognized context type '#{type}'"
28
+ end
29
+
30
+ class MapEvent < Context
31
+ attr_reader :map_name
32
+ attr_reader :event_num
33
+ attr_reader :page_num
34
+ attr_reader :line_num
35
+ attr_reader :command_name
36
+
37
+ def initialize(map_name, event_num, page_num, line_num, command_name)
38
+ @map_name = map_name
39
+ @event_num = event_num
40
+ @page_num = page_num
41
+ @line_num = line_num
42
+ @command_name = command_name
43
+ end
44
+
45
+ def eql?(other)
46
+ super &&
47
+ @map_name == other.map_name &&
48
+ @event_num == other.event_num &&
49
+ @page_num == other.page_num
50
+ end
51
+
52
+ def hash
53
+ [@map_name, @event_num, @page_num].hash
54
+ end
55
+
56
+ def to_s
57
+ "MPS:#{@map_name}/events/#{@event_num}/pages/#{@page_num}/#{@line_num}/#{@command_name}"
58
+ end
59
+
60
+ def self.from_data(map_name, event, page, cmd_index, command)
61
+ MapEvent.new(map_name, event.id, page.id + 1, cmd_index + 1, command.class.name.split('::').last)
62
+ end
63
+
64
+ def self.from_string(path)
65
+ map_name, events_str, event_num, pages_str, page_num, line_num, command_name = path
66
+ if events_str != 'events' || pages_str != 'pages'
67
+ raise "unexpected path element in MPS context line"
68
+ end
69
+ MapEvent.new(map_name, event_num.to_i, page_num.to_i, line_num.to_i, command_name)
70
+ end
71
+ end
72
+
73
+ class CommonEvent < Context
74
+ attr_reader :event_num
75
+ attr_reader :line_num
76
+ attr_reader :command_name
77
+
78
+ def initialize(event_num, line_num, command_name)
79
+ @event_num = event_num
80
+ @line_num = line_num
81
+ @command_name = command_name
82
+ end
83
+
84
+ def eql?(other)
85
+ super && @event_num == other.event_num
86
+ end
87
+
88
+ def hash
89
+ @event_num.hash
90
+ end
91
+
92
+ def to_s
93
+ "COMMONEVENT:#{@event_num}/#{@line_num}/#{@command_name}"
94
+ end
95
+
96
+ def self.from_data(event, cmd_index, command)
97
+ CommonEvent.new(event.id, cmd_index + 1, command.class.name.split('::').last)
98
+ end
99
+
100
+ def self.from_string(path)
101
+ event_num, line_num, command_name = path
102
+ CommonEvent.new(event_num.to_i, line_num.to_i, command_name)
103
+ end
104
+ end
105
+
106
+ class GameDat < Context
107
+ attr_reader :name
108
+
109
+ def initialize(name)
110
+ @name = name
111
+ end
112
+
113
+ def eql?(other)
114
+ super && @name == other.name
115
+ end
116
+
117
+ def hash
118
+ @name.hash
119
+ end
120
+
121
+ def to_s
122
+ "GAMEDAT:#{@name}"
123
+ end
124
+
125
+ def self.from_data(name)
126
+ GameDat.new(name)
127
+ end
128
+
129
+ def self.from_string(path)
130
+ if path.size != 1
131
+ raise "invalid path specified for GAMEDAT context line"
132
+ end
133
+ GameDat.new(path.first)
134
+ end
135
+ end
136
+
137
+ class Database < Context
138
+ attr_reader :db_name
139
+ attr_reader :type_index
140
+ attr_reader :type_name
141
+ attr_reader :datum_index
142
+ attr_reader :datum_name
143
+ attr_reader :field_index
144
+ attr_reader :field_name
145
+
146
+ def initialize(db_name, type_index, type_name, datum_index, datum_name, field_index, field_name)
147
+ @db_name = db_name
148
+ @type_index = type_index
149
+ @type_name = WolfTrans.full_strip(type_name)
150
+ @datum_index = datum_index
151
+ @datum_name = WolfTrans.full_strip(datum_name)
152
+ @field_index = field_index
153
+ @field_name = WolfTrans.full_strip(field_name)
154
+ end
155
+
156
+ def eql?(other)
157
+ super &&
158
+ @db_name == db_name &&
159
+ @type_index == other.type_index &&
160
+ @datum_index == other.datum_index &&
161
+ @field_index == other.field_index
162
+ end
163
+
164
+ def hash
165
+ [@db_name, @type_index, @datum_index, @field_index].hash
166
+ end
167
+
168
+ def to_s
169
+ "DB:#{@db_name}/[#{@type_index}]#{@type_name}/[#{@datum_index}]#{@datum_name}/[#{@field_index}]#{@field_name}"
170
+ end
171
+
172
+ def self.from_data(db_name, type_index, type, datum_index, datum, field)
173
+ Database.new(db_name, type_index, type.name, datum_index, datum.name, field.index, field.name)
174
+ end
175
+
176
+ def self.from_string(path)
177
+ if path.size != 4
178
+ raise "invalid path specified for DB context line"
179
+ end
180
+ indices = Array.new(3)
181
+ path.each_with_index do |str, i|
182
+ next if i == 0
183
+ str.match(/^\[\d+\]/) do |m|
184
+ indices[i-1] = m.to_s[1..-2].to_i
185
+ end
186
+ str.sub!(/^\[\d+\]/, '')
187
+ end
188
+
189
+ Database.new(path[0], indices[0], path[1], indices[1], path[2], indices[2], path[3])
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,341 @@
1
+ require 'wolftrans/context'
2
+ require 'wolfrpg'
3
+
4
+ require 'fileutils'
5
+ require 'find'
6
+
7
+ #####################
8
+ # Loading Game data #
9
+ module WolfTrans
10
+ class Patch
11
+ def load_data(game_dir)
12
+ @game_dir = WolfTrans.sanitize_path(game_dir)
13
+ unless Dir.exist? @game_dir
14
+ raise "could not find game folder '#{@game_dir}'"
15
+ end
16
+ @game_data_dir = WolfTrans.join_path_nocase(@game_dir, 'data')
17
+ if @game_data_dir == nil
18
+ raise "could not find data folder in '#{@game_dir}'"
19
+ end
20
+
21
+ @maps = {}
22
+ @databases = {}
23
+
24
+ # Find and read all necessary data
25
+ Dir.entries(@game_data_dir).each do |parent_name|
26
+ parent_name_downcase = parent_name.downcase
27
+ next unless ['basicdata', 'mapdata'].include? parent_name_downcase
28
+ parent_path = "#{@game_data_dir}/#{parent_name}"
29
+ Dir.entries(parent_path).each do |basename|
30
+ basename_downcase = basename.downcase
31
+ extension = File.extname(basename_downcase)
32
+ basename_noext = File.basename(basename_downcase, '.*')
33
+ filename = "#{parent_path}/#{basename}"
34
+ case parent_name_downcase
35
+ when 'mapdata'
36
+ load_map(filename) if extension == '.mps'
37
+ when 'basicdata'
38
+ if basename_downcase == 'game.dat'
39
+ load_game_dat(filename)
40
+ elsif extension == '.project'
41
+ next if basename_downcase == 'sysdatabasebasic.project'
42
+ dat_filename = WolfTrans.join_path_nocase(parent_path, "#{basename_noext}.dat")
43
+ next if dat_filename == nil
44
+ load_game_database(filename, dat_filename)
45
+ elsif basename_downcase == 'commonevent.dat'
46
+ load_common_events(filename)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ # Apply the patch to the files in the game path and write them to the
54
+ # output directory
55
+ def apply(out_dir)
56
+ out_dir = WolfTrans.sanitize_path(out_dir)
57
+ out_data_dir = "#{out_dir}/Data"
58
+
59
+ # Clear out directory
60
+ FileUtils.rm_rf(out_dir)
61
+
62
+ # Patch all the maps and dump them
63
+ FileUtils.mkdir_p("#{out_data_dir}/MapData")
64
+ @maps.each do |map_name, map|
65
+ map.events.each do |event|
66
+ event.pages.each do |page|
67
+ page.commands.each_with_index do |command, cmd_index|
68
+ context = Context::MapEvent.from_data(map_name, event, page, cmd_index, command)
69
+ patch_command(command, context)
70
+ end
71
+ end
72
+ end
73
+ map.dump("#{out_data_dir}/MapData/#{map_name}.mps")
74
+ end
75
+
76
+ # Patch the databases
77
+ FileUtils.mkdir_p("#{out_data_dir}/BasicData")
78
+ @databases.each do |db_name, db|
79
+ db.types.each_with_index do |type, type_index|
80
+ next if type.name.empty?
81
+ type.data.each_with_index do |datum, datum_index|
82
+ datum.each_translatable do |str, field|
83
+ context = Context::Database.from_data(db_name, type_index, type, datum_index, datum, field)
84
+ yield_translation(str, context) do |newstr|
85
+ datum[field] = newstr
86
+ end
87
+ end
88
+ end
89
+ end
90
+ name_noext = "#{out_data_dir}/BasicData/#{db_name}"
91
+ db.dump("#{name_noext}.project", "#{name_noext}.dat")
92
+ end
93
+
94
+ # Patch the common events
95
+ @common_events.events.each do |event|
96
+ event.commands.each_with_index do |command, cmd_index|
97
+ context = Context::CommonEvent.from_data(event, cmd_index, command)
98
+ patch_command(command, context)
99
+ end
100
+ end
101
+ @common_events.dump("#{out_data_dir}/BasicData/CommonEvent.dat")
102
+
103
+ # Patch Game.dat
104
+ FileUtils.mkdir_p("#{out_data_dir}/BasicData")
105
+ patch_game_dat
106
+ @game_dat.dump("#{out_data_dir}/BasicData/Game.dat")
107
+
108
+ # Copy image files
109
+ [
110
+ 'BattleEffect',
111
+ 'CharaChip',
112
+ 'EnemyGraphic',
113
+ 'Fog_BackGround',
114
+ 'MapChip',
115
+ 'Picture',
116
+ 'SystemFile',
117
+ ].each do |dirname|
118
+ copy_data_files(out_data_dir, dirname, ['png','jpg','jpeg','bmp'])
119
+ end
120
+
121
+ # Copy sound/music files
122
+ [
123
+ 'BGM',
124
+ 'SE',
125
+ 'SystemFile',
126
+ ].each do |dirname|
127
+ copy_data_files(out_data_dir, dirname, ['ogg','mp3','wav','mid','midi'])
128
+ end
129
+
130
+ # Copy BasicData
131
+ copy_data_files(out_data_dir, 'BasicData', ['dat','project','xxxxx','png'])
132
+
133
+ # Copy fonts
134
+ copy_data_files(out_data_dir, '', ['ttf','ttc'])
135
+
136
+ # Copy remainder of files in the base patch/game dirs
137
+ copy_files(@patch_assets_dir, @patch_data_dir, out_dir)
138
+ copy_files(@game_dir, @game_data_dir, out_dir)
139
+ end
140
+
141
+ private
142
+ def load_map(filename)
143
+ map_name = File.basename(filename, '.*')
144
+ patch_filename = "dump/mps/#{map_name}.txt"
145
+
146
+ map = WolfRpg::Map.new(filename)
147
+ map.events.each do |event|
148
+ event.pages.each do |page|
149
+ page.commands.each_with_index do |command, cmd_index|
150
+ strings_of_command(command) do |string|
151
+ @strings[string][Context::MapEvent.from_data(map_name, event, page, cmd_index, command)] ||=
152
+ Translation.new(patch_filename)
153
+ end
154
+ end
155
+ end
156
+ end
157
+ @maps[map_name] = map
158
+ end
159
+
160
+ def load_game_dat(filename)
161
+ patch_filename = 'dump/GameDat.txt'
162
+ @game_dat = WolfRpg::GameDat.new(filename)
163
+ unless @game_dat.title.empty?
164
+ @strings[@game_dat.title][Context::GameDat.from_data('Title')] = Translation.new(patch_filename)
165
+ end
166
+ unless @game_dat.version.empty?
167
+ @strings[@game_dat.version][Context::GameDat.from_data('Version')] = Translation.new(patch_filename)
168
+ end
169
+ unless @game_dat.font.empty?
170
+ @strings[@game_dat.font][Context::GameDat.from_data('Font')] = Translation.new(patch_filename)
171
+ end
172
+ @game_dat.subfonts.each_with_index do |sf, i|
173
+ unless sf.empty?
174
+ name = 'SubFont' + (i + 1).to_s
175
+ @strings[sf][Context::GameDat.from_data(name)] ||=
176
+ Translation.new(patch_filename)
177
+ end
178
+ end
179
+ end
180
+
181
+ def load_game_database(project_filename, dat_filename)
182
+ db = WolfRpg::Database.new(project_filename, dat_filename)
183
+ db.types.each_with_index do |type, type_index|
184
+ next if type.name.empty?
185
+ patch_filename = "dump/db/#{db.name}/#{WolfTrans.escape_path(type.name)}.txt"
186
+ type.data.each_with_index do |datum, datum_index|
187
+ datum.each_translatable do |str, field|
188
+ context = Context::Database.from_data(db.name, type_index, type, datum_index, datum, field)
189
+ @strings[str][context] ||= Translation.new(patch_filename)
190
+ end
191
+ end
192
+ end
193
+ @databases[db.name] = db
194
+ end
195
+
196
+ def load_common_events(filename)
197
+ @common_events = WolfRpg::CommonEvents.new(filename)
198
+ @common_events.events.each do |event|
199
+ patch_filename = "dump/common/#{'%03d' % event.id}_#{WolfTrans.escape_path(event.name)}.txt"
200
+ event.commands.each_with_index do |command, cmd_index|
201
+ strings_of_command(command) do |string|
202
+ @strings[string][Context::CommonEvent.from_data(event, cmd_index, command)] ||=
203
+ Translation.new(patch_filename)
204
+ end
205
+ end
206
+ end
207
+ end
208
+
209
+ def strings_of_command(command)
210
+ case command
211
+ when WolfRpg::Command::Message
212
+ yield command.text unless command.text.empty?
213
+ when WolfRpg::Command::Choices
214
+ command.text.each do |s|
215
+ yield s
216
+ end
217
+ when WolfRpg::Command::StringCondition
218
+ command.string_args.each do |s|
219
+ yield s unless s.empty?
220
+ end
221
+ when WolfRpg::Command::SetString
222
+ yield command.text unless command.text.empty?
223
+ when WolfRpg::Command::Picture
224
+ if command.type == :text
225
+ yield command.text unless command.text.empty?
226
+ end
227
+ end
228
+ end
229
+
230
+ def patch_command(command, context)
231
+ case command
232
+ when WolfRpg::Command::Message
233
+ yield_translation(command.text, context) do |str|
234
+ command.text = str
235
+ end
236
+ when WolfRpg::Command::Choices
237
+ command.text.each_with_index do |text, i|
238
+ yield_translation(text, context) do |str|
239
+ command.text[i] = str
240
+ end
241
+ end
242
+ when WolfRpg::Command::StringCondition
243
+ command.string_args.each_with_index do |arg, i|
244
+ next if arg.empty?
245
+ yield_translation(arg, context) do |str|
246
+ command.string_args[i] = str
247
+ end
248
+ end
249
+ when WolfRpg::Command::SetString
250
+ yield_translation(command.text, context) do |str|
251
+ command.text = str
252
+ end
253
+ when WolfRpg::Command::Picture
254
+ if command.type == :text
255
+ yield_translation(command.text, context) do |str|
256
+ command.text = str
257
+ end
258
+ end
259
+ end
260
+ end
261
+
262
+ def patch_game_dat
263
+ yield_translation(@game_dat.title, Context::GameDat.from_data('Title')) do |str|
264
+ @game_dat.title = str
265
+ end
266
+ yield_translation(@game_dat.version, Context::GameDat.from_data('Version')) do |str|
267
+ @game_dat.version = str
268
+ end
269
+ yield_translation(@game_dat.font, Context::GameDat.from_data('Font')) do |str|
270
+ @game_dat.font = str
271
+ end
272
+ @game_dat.subfonts.each_with_index do |sf, i|
273
+ name = 'SubFont' + (i + 1).to_s
274
+ yield_translation(sf, Context::GameDat.from_data(name)) do |str|
275
+ @game_dat.subfonts[i] = str
276
+ end
277
+ end
278
+ end
279
+
280
+ # Yield a translation for the given string and context if it exists
281
+ def yield_translation(string, context)
282
+ return if string.empty?
283
+ if @strings.include? string
284
+ unless @strings[string][context].string.empty?
285
+ yield @strings[string][context].string
286
+ end
287
+ end
288
+ end
289
+
290
+ # Copy normal, non-data files
291
+ def copy_files(src_dir, src_data_dir, out_dir)
292
+ Find.find(src_dir) do |path|
293
+ next if path == src_dir
294
+ Find.prune if path == src_data_dir
295
+ short_path = path[src_dir.length+1..-1]
296
+ Find.prune if @file_blacklist.include? short_path.downcase
297
+ out_path = "#{out_dir}/#{short_path}"
298
+ if FileTest.directory? path
299
+ FileUtils.mkdir_p(out_path)
300
+ else
301
+ next if ['thumbs.db', 'desktop.ini', '.ds_store'].include? File.basename(path).downcase
302
+ FileUtils.cp(path, out_path) unless File.exist? out_path
303
+ end
304
+ end
305
+ end
306
+
307
+ # Copy data files
308
+ def copy_data_files(out_data_dir, dirname, extensions)
309
+ copy_data_files_from(@game_data_dir, out_data_dir, dirname, extensions)
310
+ if @patch_data_dir
311
+ copy_data_files_from(@patch_data_dir, out_data_dir, dirname, extensions)
312
+ end
313
+ end
314
+
315
+ def copy_data_files_from(src_data_dir, out_data_dir, dirname, extensions)
316
+ out_dir = File.join(out_data_dir, dirname)
317
+ FileUtils.mkdir_p(out_dir)
318
+
319
+ Find.find(src_data_dir) do |path|
320
+ if dirname.empty?
321
+ if FileTest.directory? path
322
+ Find.prune if path != src_data_dir
323
+ next
324
+ end
325
+ else
326
+ next if path == src_data_dir
327
+ if FileTest.directory?(path)
328
+ Find.prune unless File.basename(path).casecmp(dirname) == 0
329
+ next
330
+ end
331
+ next if File.dirname(path) == src_data_dir
332
+ end
333
+ basename = File.basename(path)
334
+ next unless extensions.include? File.extname(basename)[1..-1]
335
+ next if @file_blacklist.include? "data/#{dirname.downcase}/#{basename.downcase}"
336
+ out_name = "#{out_dir}/#{basename}"
337
+ FileUtils.cp(path, out_name) unless File.exist? out_name
338
+ end
339
+ end
340
+ end
341
+ end