wolftrans 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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