wolftrans 0.1.0 → 0.2.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e5810f165d8477fd00571f8cf4c0b599f4ee2bd7
4
- data.tar.gz: d3fb00f064a84ef6f740329fe26ea98158522ecc
3
+ metadata.gz: c841512ed31767357a132f115dd9c9d5d4d85ae3
4
+ data.tar.gz: 13b81d6e79ce6e6e7a3ca202073dd4b8c2a22dd8
5
5
  SHA512:
6
- metadata.gz: 11fa3045617bc45a79ca011ee8f42f44e5d05e1bc0b11917395936b2f22e12471e4010aabf623a1a775db050013d4a29b583e468c82b9ee6e9e737d4a1e5f7a4
7
- data.tar.gz: 8741262f4c7622bb688554f2e65bcb6273c5cbb7d6b917e6d3770ce24b94f6010f87d59813da42d02bf02b3a53fb0fda92f860f3fcccafd1c7faff45c5b2ef0f
6
+ metadata.gz: 4b63ce1e61df8e456faf61ae7064c8732dfc8626b75262641a9afe76b5f271c5e813daa97042435ea237fedf923e7129b74e363c6fbac6b5322f41a6316a5901
7
+ data.tar.gz: faea22c7bbf27d5e4171fa3056306014e869e4c57e4ea9c5697c6359a0a84255ae1b0ccf5ea14ee9d474d2fb00b8f4328790a96e93bbff84443b408d687ea7bc
@@ -0,0 +1,6 @@
1
+ # Ignore generated files
2
+ *.gem
3
+
4
+ # Ignore any zip archives or extracted data in test/
5
+ test/*/*
6
+ test/*.zip
data/Rakefile CHANGED
@@ -17,6 +17,10 @@ task :install => :build do
17
17
  system "gem install #{GEM_FILENAME}"
18
18
  end
19
19
 
20
+ task :uninstall do
21
+ system "gem uninstall wolftrans --version #{WolfTrans::VERSION}"
22
+ end
23
+
20
24
  task :clean do
21
25
  File.delete(GEM_FILENAME) if File.exist? GEM_FILENAME
22
26
  end
@@ -1,5 +1,7 @@
1
1
  require 'wolfrpg/route'
2
2
 
3
+ # Special thanks to vgperson for mapping most of these command IDs out.
4
+
3
5
  module WolfRpg
4
6
  class Command
5
7
  attr_reader :cid
@@ -13,6 +15,9 @@ module WolfRpg
13
15
  class Blank < Command
14
16
  end
15
17
 
18
+ class Checkpoint < Command
19
+ end
20
+
16
21
  class Message < Command
17
22
  def text
18
23
  @string_args[0]
@@ -34,15 +39,27 @@ module WolfRpg
34
39
  end
35
40
  end
36
41
 
42
+ class ForceStopMessage < Command
43
+ end
44
+
37
45
  class DebugMessage < Command
38
46
  def text
39
47
  @string_args[0]
40
48
  end
41
49
  end
42
50
 
51
+ class ClearDebugText < Command
52
+ end
53
+
54
+ class VariableCondition < Command
55
+ end
56
+
43
57
  class StringCondition < Command
44
58
  end
45
59
 
60
+ class SetVariable < Command
61
+ end
62
+
46
63
  class SetString < Command
47
64
  def text
48
65
  if @string_args.length > 0
@@ -57,6 +74,24 @@ module WolfRpg
57
74
  end
58
75
  end
59
76
 
77
+ class InputKey < Command
78
+ end
79
+
80
+ class SetVariableEx < Command
81
+ end
82
+
83
+ class AutoInput < Command
84
+ end
85
+
86
+ class BanInput < Command
87
+ end
88
+
89
+ class Teleport < Command
90
+ end
91
+
92
+ class Sound < Command
93
+ end
94
+
60
95
  class Picture < Command
61
96
  def type
62
97
  case (args[0] >> 4) & 0x07
@@ -111,25 +146,51 @@ module WolfRpg
111
146
  end
112
147
  end
113
148
 
114
- #class
149
+ class ChangeColor < Command
150
+ end
115
151
 
116
- private
117
- ##########################
118
- # Map of CIDs to classes #
152
+ class SetTransition < Command
153
+ end
119
154
 
120
- CID_TO_CLASS = {
121
- 0 => Command::Blank,
122
- 101 => Command::Message,
123
- 102 => Command::Choices,
124
- 103 => Command::Comment,
125
- 106 => Command::DebugMessage,
126
- 112 => Command::StringCondition,
127
- 122 => Command::SetString,
128
- 150 => Command::Picture,
129
- }
130
- CID_TO_CLASS.default = Command
155
+ class PrepareTransition < Command
156
+ end
157
+
158
+ class ExecuteTransition < Command
159
+ end
160
+
161
+ class StartLoop < Command
162
+ end
163
+
164
+ class BreakLoop < Command
165
+ end
166
+
167
+ class BreakEvent < Command
168
+ end
169
+
170
+ class EraseEvent < Command
171
+ end
172
+
173
+ class ReturnToTitle < Command
174
+ end
175
+
176
+ class EndGame < Command
177
+ end
178
+
179
+ class LoopToStart < Command
180
+ end
181
+
182
+ class StopNonPic < Command
183
+ end
184
+
185
+ class ResumeNonPic < Command
186
+ end
187
+
188
+ class LoopTimes < Command
189
+ end
190
+
191
+ class Wait < Command
192
+ end
131
193
 
132
- public
133
194
  class Move < Command
134
195
  def initialize(cid, args, string_args, indent, coder)
135
196
  super(cid, args, string_args, indent)
@@ -162,6 +223,155 @@ module WolfRpg
162
223
  end
163
224
  end
164
225
 
226
+ class WaitForMove < Command
227
+ end
228
+
229
+ class CommonEvent < Command
230
+ end
231
+
232
+ class CommonEventReserve < Command
233
+ end
234
+
235
+ class SetLabel < Command
236
+ end
237
+
238
+ class JumpLabel < Command
239
+ end
240
+
241
+ class SaveLoad < Command
242
+ end
243
+
244
+ class LoadGame < Command
245
+ end
246
+
247
+ class SaveGame < Command
248
+ end
249
+
250
+ class MoveDuringEventOn < Command
251
+ end
252
+
253
+ class MoveDuringEventOff < Command
254
+ end
255
+
256
+ class Chip < Command
257
+ end
258
+
259
+ class ChipSet < Command
260
+ end
261
+
262
+ class ChipOverwrite < Command
263
+ end
264
+
265
+ class Database < Command
266
+ end
267
+
268
+ class ImportDatabase < Command
269
+ end
270
+
271
+ class Party < Command
272
+ end
273
+
274
+ class MapEffect < Command
275
+ end
276
+
277
+ class ScrollScreen < Command
278
+ end
279
+
280
+ class Effect < Command
281
+ end
282
+
283
+ class CommonEventByName < Command
284
+ end
285
+
286
+ class ChoiceCase < Command
287
+ end
288
+
289
+ class SpecialChoiceCase < Command
290
+ end
291
+
292
+ class ElseCase < Command
293
+ end
294
+
295
+ class CancelCase < Command
296
+ end
297
+
298
+ class LoopEnd < Command
299
+ end
300
+
301
+ class BranchEnd < Command
302
+ end
303
+
304
+ #class
305
+
306
+ private
307
+ ##########################
308
+ # Map of CIDs to classes #
309
+
310
+ CID_TO_CLASS = {
311
+ 0 => Command::Blank,
312
+ 99 => Command::Checkpoint,
313
+ 101 => Command::Message,
314
+ 102 => Command::Choices,
315
+ 103 => Command::Comment,
316
+ 105 => Command::ForceStopMessage,
317
+ 106 => Command::DebugMessage,
318
+ 107 => Command::ClearDebugText,
319
+ 111 => Command::VariableCondition,
320
+ 112 => Command::StringCondition,
321
+ 121 => Command::SetVariable,
322
+ 122 => Command::SetString,
323
+ 123 => Command::InputKey,
324
+ 124 => Command::SetVariableEx,
325
+ 125 => Command::AutoInput,
326
+ 126 => Command::BanInput,
327
+ 130 => Command::Teleport,
328
+ 140 => Command::Sound,
329
+ 150 => Command::Picture,
330
+ 151 => Command::ChangeColor,
331
+ 160 => Command::SetTransition,
332
+ 161 => Command::PrepareTransition,
333
+ 162 => Command::ExecuteTransition,
334
+ 170 => Command::StartLoop,
335
+ 171 => Command::BreakLoop,
336
+ 172 => Command::BreakEvent,
337
+ 173 => Command::EraseEvent,
338
+ 174 => Command::ReturnToTitle,
339
+ 175 => Command::EndGame,
340
+ 176 => Command::StartLoop,
341
+ 177 => Command::StopNonPic,
342
+ 178 => Command::ResumeNonPic,
343
+ 179 => Command::LoopTimes,
344
+ 180 => Command::Wait,
345
+ 201 => Command::Move, # special case
346
+ 202 => Command::WaitForMove,
347
+ 210 => Command::CommonEvent,
348
+ 211 => Command::CommonEventReserve,
349
+ 212 => Command::SetLabel,
350
+ 213 => Command::JumpLabel,
351
+ 220 => Command::SaveLoad,
352
+ 221 => Command::LoadGame,
353
+ 222 => Command::SaveGame,
354
+ 230 => Command::MoveDuringEventOn,
355
+ 231 => Command::MoveDuringEventOff,
356
+ 240 => Command::Chip,
357
+ 241 => Command::ChipSet,
358
+ 250 => Command::Database,
359
+ 251 => Command::ImportDatabase,
360
+ 270 => Command::Party,
361
+ 280 => Command::MapEffect,
362
+ 281 => Command::ScrollScreen,
363
+ 290 => Command::Effect,
364
+ 300 => Command::CommonEventByName,
365
+ 401 => Command::ChoiceCase,
366
+ 402 => Command::SpecialChoiceCase,
367
+ 420 => Command::ElseCase,
368
+ 421 => Command::CancelCase,
369
+ 498 => Command::LoopEnd,
370
+ 499 => Command::BranchEnd
371
+ }
372
+ CID_TO_CLASS.default = Command
373
+
374
+ public
165
375
  # Load from the file and create the appropriate class object
166
376
  def self.create(coder)
167
377
  # Read all data for this command from file
@@ -87,7 +87,8 @@ module WolfRpg
87
87
  raise "expected 0x91, got 0x#{indicator.to_s(16)}"
88
88
  end
89
89
  @unknown9 = coder.read_string
90
- if (indicator = coder.read_byte) != 0x92
90
+ return if (indicator = coder.read_byte) == 0x91
91
+ unless indicator == 0x92
91
92
  raise "expected 0x92, got 0x#{indicator.to_s(16)}"
92
93
  end
93
94
  @unknown10 = coder.read_string
@@ -138,10 +139,14 @@ module WolfRpg
138
139
  end
139
140
  coder.write_byte(0x91)
140
141
  coder.write_string(@unknown9)
141
- coder.write_byte(0x92)
142
- coder.write_string(@unknown10)
143
- coder.write_int(@unknown12)
144
- coder.write_byte(0x92)
142
+ if @unknown10
143
+ coder.write_byte(0x92)
144
+ coder.write_string(@unknown10)
145
+ coder.write_int(@unknown12)
146
+ coder.write_byte(0x92)
147
+ else
148
+ coder.write_byte(0x91)
149
+ end
145
150
  end
146
151
 
147
152
  private
@@ -2,6 +2,12 @@ module WolfRpg
2
2
  class Database
3
3
  attr_accessor :types
4
4
 
5
+ def encrypted?
6
+ @crypt_header != nil
7
+ end
8
+
9
+ DAT_SEED_INDICES = [0, 3, 9]
10
+
5
11
  def initialize(project_filename, dat_filename)
6
12
  FileCoder.open(project_filename, :read) do |coder|
7
13
  @types = Array.new(coder.read_int)
@@ -9,8 +15,13 @@ module WolfRpg
9
15
  @types[i] = Type.new(coder)
10
16
  end
11
17
  end
12
- FileCoder.open(dat_filename, :read) do |coder|
13
- coder.verify(DAT_MAGIC_NUMBER)
18
+ FileCoder.open(dat_filename, :read, DAT_SEED_INDICES) do |coder|
19
+ if coder.encrypted?
20
+ @crypt_header = coder.crypt_header
21
+ @unknown_encrypted_1 = coder.read_byte
22
+ else
23
+ coder.verify(DAT_MAGIC_NUMBER)
24
+ end
14
25
  num_types = coder.read_int
15
26
  unless num_types == @types.size
16
27
  raise "database project and dat Type count mismatch (#{@types.size} vs. #{num_types})"
@@ -30,8 +41,12 @@ module WolfRpg
30
41
  type.dump_project(coder)
31
42
  end
32
43
  end
33
- FileCoder.open(dat_filename, :write) do |coder|
34
- coder.write(DAT_MAGIC_NUMBER)
44
+ FileCoder.open(dat_filename, :write, DAT_SEED_INDICES, @crypt_header) do |coder|
45
+ if encrypted?
46
+ coder.write_byte(@unknown_encrypted_1)
47
+ else
48
+ coder.write(DAT_MAGIC_NUMBER)
49
+ end
35
50
  coder.write_int(@types.size)
36
51
  @types.each do |type|
37
52
  type.dump_dat(coder)
@@ -300,7 +315,7 @@ module WolfRpg
300
315
  end
301
316
 
302
317
  DAT_MAGIC_NUMBER = [
303
- 0x00, 0x57, 0x00, 0x00, 0x4F, 0x4C, 0x00, 0x46, 0x4D, 0x00, 0xC1
318
+ 0x57, 0x00, 0x00, 0x4F, 0x4C, 0x00, 0x46, 0x4D, 0x00, 0xC1
304
319
  ].pack('C*')
305
320
  DAT_TYPE_SEPARATOR = [
306
321
  0xFE, 0xFF, 0xFF, 0xFF
@@ -1,19 +1,49 @@
1
1
  module WolfRpg
2
2
  class FileCoder
3
+ ##############
4
+ # Attributes #
3
5
  attr_reader :io
4
-
5
- def initialize(io)
6
- @io = io
6
+ attr_reader :crypt_header
7
+ def encrypted?
8
+ @crypt_header != nil
7
9
  end
8
10
 
9
- def self.open(filename, mode)
11
+ #############
12
+ # Constants #
13
+ CRYPT_HEADER_SIZE = 10
14
+ DECRYPT_INTERVALS = [1, 2, 5]
15
+
16
+ #################
17
+ # Class methods #
18
+ def self.open(filename, mode, seed_indices = nil, crypt_header = nil)
10
19
  case mode
11
20
  when :read
12
- file = File.open(filename, 'rb')
21
+ coder = FileCoder.new(File.open(filename, 'rb'))
22
+
23
+ # If encryptable,
24
+ # we need to make an extra check to see if it needs decrypting
25
+ if seed_indices
26
+ unless (indicator = coder.read_byte) == 0
27
+ header = [indicator]
28
+ (CRYPT_HEADER_SIZE - 1).times { |i| header << coder.read_byte }
29
+ seeds = seed_indices.map { |i| header[i] }
30
+ data = crypt(coder.read, seeds)
31
+ coder = FileCoder.new(StringIO.new(data, 'rb'), header)
32
+ end
33
+ end
13
34
  when :write
14
- file = File.open(filename, 'wb')
35
+ # If encryptable, open a StringIO and pass the encryption options
36
+ # to the FileCoder
37
+ if seed_indices && crypt_header
38
+ coder = FileCoder.new(StringIO.new(''.force_encoding('BINARY'), 'wb'),
39
+ crypt_header, filename, seed_indices)
40
+ coder.write(crypt_header.pack('C*'))
41
+ else
42
+ coder = FileCoder.new(File.open(filename, 'wb'))
43
+ coder.write_byte(0) if seed_indices
44
+ end
15
45
  end
16
- coder = FileCoder.new(file)
46
+
17
47
  if block_given?
18
48
  begin
19
49
  yield coder
@@ -24,6 +54,14 @@ module WolfRpg
24
54
  return coder
25
55
  end
26
56
 
57
+ ##############
58
+ # Initialize #
59
+ def initialize(io, crypt_header = nil, filename = nil, seed_indices = nil)
60
+ @io = io
61
+ @crypt_header = crypt_header
62
+ @filename = filename
63
+ end
64
+
27
65
  ########
28
66
  # Read #
29
67
  def read(size = nil)
@@ -50,6 +88,30 @@ module WolfRpg
50
88
  return str
51
89
  end
52
90
 
91
+ def read_byte_array
92
+ bytes = Array.new(read_int)
93
+ bytes.each_index do |i|
94
+ bytes[i] = read_byte
95
+ end
96
+ return bytes
97
+ end
98
+
99
+ def read_int_array
100
+ ints = Array.new(read_int)
101
+ ints.each_index do |i|
102
+ ints[i] = read_int
103
+ end
104
+ return ints
105
+ end
106
+
107
+ def read_string_array
108
+ strings = Array.new(read_int)
109
+ strings.each_index do |i|
110
+ strings[i] = read_string
111
+ end
112
+ return strings
113
+ end
114
+
53
115
  def verify(data)
54
116
  got = read(data.length)
55
117
  if got != data
@@ -84,12 +146,15 @@ module WolfRpg
84
146
  def write(data)
85
147
  @io.write(data)
86
148
  end
149
+
87
150
  def write_byte(data)
88
151
  @io.write(data.chr)
89
152
  end
153
+
90
154
  def write_int(data)
91
155
  @io.write([data].pack('l<'))
92
156
  end
157
+
93
158
  def write_string(str)
94
159
  str = str.encode(Encoding::WINDOWS_31J, Encoding::UTF_8)
95
160
  write_int(str.bytesize + 1)
@@ -97,9 +162,37 @@ module WolfRpg
97
162
  write_byte(0)
98
163
  end
99
164
 
165
+ def write_byte_array(bytes)
166
+ write_int(bytes.size)
167
+ bytes.each do |b|
168
+ write_byte(b)
169
+ end
170
+ end
171
+
172
+ def write_int_array(ints)
173
+ write_int(ints.size)
174
+ ints.each do |i|
175
+ write_int(i)
176
+ end
177
+ end
178
+
179
+ def write_string_array(strings)
180
+ write_int(strings.size)
181
+ strings.each do |s|
182
+ write_string(s)
183
+ end
184
+ end
185
+
100
186
  #########
101
187
  # Other #
102
188
  def close
189
+ if @crypt_header && @filename && @seed_indices
190
+ File.open(@filename, 'wb') do |file|
191
+ file.write(@crypt_header.pack('C*'))
192
+ seeds = @seed_indices.map { |i| crypt_header[i] }
193
+ file.write(FileCoder.crypt(@io.string, seeds))
194
+ end
195
+ end
103
196
  @io.close
104
197
  end
105
198
 
@@ -108,7 +201,25 @@ module WolfRpg
108
201
  end
109
202
 
110
203
  def tell
111
- @io.tell
204
+ if encrypted?
205
+ @io.tell + CRYPT_HEADER_SIZE
206
+ else
207
+ @io.tell
208
+ end
209
+ end
210
+
211
+ #################
212
+ # Private class #
213
+ private
214
+ def self.crypt(data_str, seeds)
215
+ data = data_str.unpack('C*')
216
+ seeds.each_with_index do |seed, s|
217
+ (0...data.size).step(DECRYPT_INTERVALS[s]) do |i|
218
+ seed = (seed * 0x343FD + 0x269EC3) & 0xFFFFFFFF
219
+ data[i] ^= (seed >> 28) & 7
220
+ end
221
+ end
222
+ return data.pack('C*')
112
223
  end
113
224
  end
114
225
  end
@@ -2,11 +2,8 @@ require 'stringio'
2
2
 
3
3
  module WolfRpg
4
4
  class GameDat
5
- attr_reader :legacy
6
- alias_method :legacy?, :legacy
7
-
8
5
  attr_accessor :unknown1
9
- attr_accessor :unknown4
6
+ attr_accessor :file_version # only a guess
10
7
  attr_accessor :title
11
8
  attr_accessor :unknown2
12
9
  attr_accessor :font
@@ -15,40 +12,29 @@ module WolfRpg
15
12
  attr_accessor :version
16
13
  attr_accessor :unknown3
17
14
 
15
+ def encrypted?
16
+ @crypt_header != nil
17
+ end
18
+
19
+ SEED_INDICES = [0, 8, 6]
20
+ #XSEED_INDICES = [3, 4, 5]
21
+
18
22
  def initialize(filename)
19
- FileCoder.open(filename, :read) do |coder|
20
- if (first_byte = coder.read_byte) == 0
21
- @legacy = false
22
- coder.verify(MAGIC_NUMBER)
23
+ FileCoder.open(filename, :read, SEED_INDICES) do |coder|
24
+ if coder.encrypted?
25
+ @crypt_header = coder.crypt_header
23
26
  else
24
- @legacy = true
25
- @seeds = Array.new(3)
26
- @xseeds = Array.new(3)
27
- @seeds[0] = first_byte
28
- coder.skip(1) # garbage
29
- @xseeds[0] = coder.read_byte
30
- @xseeds[1] = coder.read_byte
31
- @xseeds[2] = coder.read_byte
32
- coder.skip(1) # garbage
33
- @seeds[2] = coder.read_byte
34
- coder.skip(1) # garbage
35
- @seeds[1] = coder.read_byte
36
- coder.skip(1) # garbage
37
- coder = FileCoder.new(StringIO.new(crypt(coder.read)))
27
+ coder.verify(MAGIC_NUMBER)
38
28
  end
39
29
 
40
30
  #TODO what is most of the junk in this file?
41
- unknown1_size = coder.read_int
42
- @unknown1 = coder.read(unknown1_size)
43
- @unknown4 = coder.read_int
44
-
31
+ @unknown1 = coder.read_byte_array
32
+ @file_version = coder.read_int
45
33
  @title = coder.read_string
46
34
  if (magic_string = coder.read_string) != MAGIC_STRING
47
35
  raise "magic string invalid (got #{magic_string})"
48
36
  end
49
-
50
- unknown2_size = coder.read_int
51
- @unknown2 = coder.read(unknown2_size)
37
+ @unknown2 = coder.read_byte_array
52
38
 
53
39
  @font = coder.read_string
54
40
  @subfonts = Array.new(3)
@@ -57,7 +43,11 @@ module WolfRpg
57
43
  end
58
44
 
59
45
  @default_pc_graphic = coder.read_string
60
- @version = coder.read_string
46
+ if @file_version >= 9
47
+ @version = coder.read_string
48
+ else
49
+ @version = ''
50
+ end
61
51
 
62
52
  # This is the size of the file minus one.
63
53
  # We don't need it, so discard it.
@@ -71,61 +61,23 @@ module WolfRpg
71
61
  end
72
62
 
73
63
  def dump(filename)
74
- begin
75
- if @legacy
76
- coder = FileCoder.new(StringIO.new('', 'wb'))
77
- else
78
- coder = FileCoder.open(filename, :write)
79
- coder.write_byte(0)
80
- coder.write(MAGIC_NUMBER)
81
- end
64
+ FileCoder.open(filename, :write, SEED_INDICES, @crypt_header) do |coder|
65
+ coder.write(MAGIC_NUMBER) unless encrypted?
82
66
 
83
- coder.write_int(@unknown1.size)
84
- coder.write(@unknown1)
85
- coder.write_int(@unknown4)
67
+ coder.write_byte_array(@unknown1)
68
+ coder.write_int(@file_version)
86
69
  coder.write_string(@title)
87
70
  coder.write_string(MAGIC_STRING)
88
- coder.write_int(@unknown2.bytesize)
89
- coder.write(@unknown2)
71
+ coder.write_byte_array(@unknown2)
90
72
  coder.write_string(@font)
91
73
  @subfonts.each do |subfont|
92
74
  coder.write_string(subfont)
93
75
  end
94
76
  coder.write_string(@default_pc_graphic)
95
- coder.write_string(@version)
96
- coder.write_int(coder.tell + 4 + @unknown3.bytesize - 1 + (@legacy ? 10 : 0))
77
+ coder.write_string(@version) if @file_version >= 9
78
+ coder.write_int(coder.tell + 4 + @unknown3.bytesize - 1)
97
79
  coder.write(@unknown3)
98
-
99
- if @legacy
100
- data = crypt(coder.io.string)
101
- FileCoder.open(filename, :write) do |coder|
102
- coder.write_byte(@seeds[0])
103
- coder.write_byte(0) # garbage
104
- coder.write_byte(@xseeds[0])
105
- coder.write_byte(@xseeds[1])
106
- coder.write_byte(@xseeds[2])
107
- coder.write_byte(0) # garbage
108
- coder.write_byte(@seeds[2])
109
- coder.write_byte(0) # garbage
110
- coder.write_byte(@seeds[1])
111
- coder.write_byte(0) # garbage
112
- coder.write(data)
113
- end
114
- end
115
- ensure
116
- coder.close
117
- end
118
- end
119
-
120
- def crypt(data_str)
121
- data = data_str.unpack('C*')
122
- @seeds.each_with_index do |seed, s|
123
- (0...data.size).step(DECRYPT_INTERVALS[s]) do |i|
124
- seed = (seed * 0x343FD + 0x269EC3) & 0xFFFFFFFF
125
- data[i] ^= (seed >> 28) & 7
126
- end
127
80
  end
128
- return data.pack('C*')
129
81
  end
130
82
 
131
83
  private
@@ -133,6 +85,5 @@ module WolfRpg
133
85
  0x57, 0x00, 0x00, 0x4f, 0x4c, 0x00, 0x46, 0x4d, 0x00
134
86
  ].pack('C*')
135
87
  MAGIC_STRING = "0000-0000" # who knows what this is supposed to be
136
- DECRYPT_INTERVALS = [1, 2, 5]
137
88
  end
138
89
  end
@@ -18,7 +18,8 @@ module WolfRpg
18
18
  # Read basic data
19
19
  @width = coder.read_int
20
20
  @height = coder.read_int
21
- @events = Array.new(coder.read_int)
21
+ coder.skip(4) # skip event count
22
+ @events = []
22
23
 
23
24
  # Read tiles
24
25
  #TODO: interpret this data later
@@ -26,8 +27,7 @@ module WolfRpg
26
27
 
27
28
  # Read events
28
29
  while (indicator = coder.read_byte) == 0x6F
29
- event = Event.new(coder)
30
- @events[event.id] = event
30
+ @events << Event.new(coder)
31
31
  end
32
32
  if indicator != 0x66
33
33
  raise "unexpected event indicator: #{indicator.to_s(16)}"
@@ -83,27 +83,6 @@ module WolfTrans
83
83
  end
84
84
  end
85
85
 
86
- # IO functions
87
- module IO
88
- def self.read_txt(filename)
89
- # Read file into memory, forcing UTF-8 without BOM.
90
- text = nil
91
- File.open(filename, 'rb:UTF-8') do |file|
92
- if text = file.read(3)
93
- if text == "\xEF\xBB\xBF"
94
- raise "UTF-8 BOM detected; refusing to read file"
95
- end
96
- text << file.read
97
- else
98
- STDERR.puts "warning: empty patch file '#{filename}'"
99
- return ''
100
- end
101
- end
102
- # Convert Windows newlines and return
103
- text.gsub(/\r\n?/, "\n")
104
- end
105
- end
106
-
107
86
  # Latest patch version format that can be read
108
87
  TXT_VERSION = Version.new(major: 1, minor: 0)
109
88
  end
@@ -36,6 +36,7 @@ module WolfTrans
36
36
  load_map(filename) if extension == '.mps'
37
37
  when 'basicdata'
38
38
  if basename_downcase == 'game.dat'
39
+ @game_dat_filename = 'Data/BasicData/Game.dat'
39
40
  load_game_dat(filename)
40
41
  elsif extension == '.project'
41
42
  next if basename_downcase == 'sysdatabasebasic.project'
@@ -48,19 +49,34 @@ module WolfTrans
48
49
  end
49
50
  end
50
51
  end
52
+
53
+ # Game.dat is in a different place on older versions
54
+ unless @game_dat
55
+ Dir.entries(@game_dir).each do |entry|
56
+ if entry.downcase == 'game.dat'
57
+ @game_dat_filename = 'Game.dat'
58
+ load_game_dat("#{@game_dir}/#{entry}")
59
+ break
60
+ end
61
+ end
62
+ end
51
63
  end
52
64
 
53
65
  # Apply the patch to the files in the game path and write them to the
54
66
  # output directory
67
+ DATA_FILE_EXTENSIONS = ['png','jpg','jpeg','bmp','ogg',
68
+ 'mp3','wav','mid','midi',
69
+ 'dat','project','xxxxx']
55
70
  def apply(out_dir)
56
71
  out_dir = Util.sanitize_path(out_dir)
57
72
  out_data_dir = "#{out_dir}/Data"
58
73
 
59
74
  # Clear out directory
60
75
  FileUtils.rm_rf(out_dir)
76
+ FileUtils.mkdir_p("#{out_data_dir}/BasicData")
77
+ FileUtils.mkdir_p("#{out_data_dir}/MapData")
61
78
 
62
79
  # Patch all the maps and dump them
63
- FileUtils.mkdir_p("#{out_data_dir}/MapData")
64
80
  @maps.each do |map_name, map|
65
81
  map.events.each do |event|
66
82
  next unless event
@@ -75,7 +91,6 @@ module WolfTrans
75
91
  end
76
92
 
77
93
  # Patch the databases
78
- FileUtils.mkdir_p("#{out_data_dir}/BasicData")
79
94
  @databases.each do |db_name, db|
80
95
  db.types.each_with_index do |type, type_index|
81
96
  next if type.name.empty?
@@ -102,43 +117,40 @@ module WolfTrans
102
117
  @common_events.dump("#{out_data_dir}/BasicData/CommonEvent.dat")
103
118
 
104
119
  # Patch Game.dat
105
- FileUtils.mkdir_p("#{out_data_dir}/BasicData")
106
120
  patch_game_dat
107
- @game_dat.dump("#{out_data_dir}/BasicData/Game.dat")
108
-
109
- # Copy image files
110
- [
111
- 'BattleEffect',
112
- 'CharaChip',
113
- 'EnemyGraphic',
114
- 'Fog',
115
- 'Fog_BackGround',
116
- 'MapChip',
117
- 'Picture',
118
- 'SystemFile',
119
- 'SystemGraphic',
120
- ].each do |dirname|
121
- copy_data_files(out_data_dir, dirname, ['png','jpg','jpeg','bmp'])
122
- end
121
+ @game_dat.dump("#{out_dir}/#{@game_dat_filename}")
123
122
 
124
- # Copy sound/music files
125
- [
126
- 'BGM',
127
- 'SE',
128
- 'SystemFile',
129
- ].each do |dirname|
130
- copy_data_files(out_data_dir, dirname, ['ogg','mp3','wav','mid','midi'])
131
- end
123
+ # Copy image/sound/music files
124
+ Dir.entries(@game_data_dir).each do |entry|
125
+ # Skip dot and dot-dot and non-directories
126
+ next if entry == '.' || entry == '..'
127
+ path = "#{@game_data_dir}/#{entry}"
128
+ next unless FileTest.directory? path
132
129
 
133
- # Copy BasicData
134
- copy_data_files(out_data_dir, 'BasicData', ['dat','project','xxxxx','png'])
130
+ # Find the corresponding folder in the out dir
131
+ unless (out_path = Util.join_path_nocase(out_data_dir, entry))
132
+ out_path = "#{out_data_dir}/#{entry}"
133
+ FileUtils.mkdir_p(out_path)
134
+ end
135
+
136
+ # Find the corresponding folder in the patch
137
+ if @patch_data_dir && (asset_entry = Util.join_path_nocase(@patch_data_dir, entry))
138
+ copy_data_files("#{@patch_data_dir}/#{asset_entry}", DATA_FILE_EXTENSIONS, out_path)
139
+ end
140
+
141
+ # Copy the original game files
142
+ copy_data_files(path, DATA_FILE_EXTENSIONS, out_path)
143
+ end
135
144
 
136
145
  # Copy fonts
137
- copy_data_files(out_data_dir, '', ['ttf','ttc'])
146
+ if @patch_data_dir
147
+ copy_data_files(@patch_data_dir, ['ttf','ttc','otf'], out_data_dir)
148
+ end
149
+ copy_data_files(@game_data_dir, ['ttf','ttc','otf'], out_data_dir)
138
150
 
139
151
  # Copy remainder of files in the base patch/game dirs
140
- copy_files(@patch_assets_dir, @patch_data_dir, out_dir)
141
- copy_files(@game_dir, @game_data_dir, out_dir)
152
+ copy_files(@patch_assets_dir, out_dir)
153
+ copy_files(@game_dir, out_dir)
142
154
  end
143
155
 
144
156
  private
@@ -214,20 +226,20 @@ module WolfTrans
214
226
  def strings_of_command(command)
215
227
  case command
216
228
  when WolfRpg::Command::Message
217
- yield command.text unless command.text.empty?
229
+ yield command.text if Util.translatable? command.text
218
230
  when WolfRpg::Command::Choices
219
231
  command.text.each do |s|
220
- yield s
232
+ yield s if Util.translatable? s
221
233
  end
222
234
  when WolfRpg::Command::StringCondition
223
235
  command.string_args.each do |s|
224
- yield s unless s.empty?
236
+ yield s if Util.translatable? s
225
237
  end
226
238
  when WolfRpg::Command::SetString
227
- yield command.text unless command.text.empty?
239
+ yield command.text if Util.translatable? command.text
228
240
  when WolfRpg::Command::Picture
229
241
  if command.type == :text
230
- yield command.text unless command.text.empty?
242
+ yield command.text if Util.translatable? command.text
231
243
  end
232
244
  end
233
245
  end
@@ -284,63 +296,53 @@ module WolfTrans
284
296
 
285
297
  # Yield a translation for the given string and context if it exists
286
298
  def yield_translation(string, context)
287
- return if string.empty?
299
+ return unless Util.translatable? string
288
300
  if @strings.include? string
289
- unless @strings[string][context].string.empty?
290
- yield @strings[string][context].string
291
- end
301
+ str = @strings[string][context].string
302
+ yield str if Util.translatable? str
292
303
  end
293
304
  end
294
305
 
295
306
  # Copy normal, non-data files
296
- def copy_files(src_dir, src_data_dir, out_dir)
307
+ def copy_files(src_dir, out_dir)
297
308
  Find.find(src_dir) do |path|
298
- next if path == src_dir
299
- Find.prune if path == src_data_dir
309
+ basename = File.basename(path)
310
+ basename_downcase = basename.downcase
311
+
312
+ # Don't do anything in Data/
313
+ Find.prune if basename_downcase == 'data' && File.dirname(path) == src_dir
314
+
315
+ # Skip directories
316
+ next if FileTest.directory? path
317
+
318
+ # "Short name", relative to the game base dir
300
319
  short_path = path[src_dir.length+1..-1]
301
320
  Find.prune if @file_blacklist.include? short_path.downcase
321
+
302
322
  out_path = "#{out_dir}/#{short_path}"
303
- if FileTest.directory? path
304
- FileUtils.mkdir_p(out_path)
305
- else
306
- next if ['thumbs.db', 'desktop.ini', '.ds_store'].include? File.basename(path).downcase
307
- FileUtils.cp(path, out_path) unless File.exist? out_path
308
- end
323
+ next if ['thumbs.db', 'desktop.ini', '.ds_store'].include? basename_downcase
324
+ next if File.exist? out_path
325
+ # Make directory only only when copying a file to avoid making empty directories
326
+ FileUtils.mkdir_p(File.dirname(out_path))
327
+ FileUtils.cp(path, out_path)
309
328
  end
310
329
  end
311
330
 
312
331
  # Copy data files
313
- def copy_data_files(out_data_dir, dirname, extensions)
314
- copy_data_files_from(@game_data_dir, out_data_dir, dirname, extensions)
315
- if @patch_data_dir
316
- copy_data_files_from(@patch_data_dir, out_data_dir, dirname, extensions)
317
- end
318
- end
332
+ def copy_data_files(src_dir, extensions, out_dir)
333
+ Dir.entries(src_dir).each do |entry|
334
+ # Don't care about directories
335
+ next if entry == '.' || entry == '..'
336
+ path = "#{src_dir}/#{entry}"
337
+ next if FileTest.directory? path
319
338
 
320
- def copy_data_files_from(src_data_dir, out_data_dir, dirname, extensions)
321
- out_dir = File.join(out_data_dir, dirname)
339
+ # Skip invalid file extensions
340
+ next unless extensions.include? File.extname(entry)[1..-1]
322
341
 
323
- Find.find(src_data_dir) do |path|
324
- if dirname.empty?
325
- if FileTest.directory? path
326
- Find.prune if path != src_data_dir
327
- next
328
- end
329
- else
330
- next if path == src_data_dir
331
- if FileTest.directory?(path)
332
- Find.prune unless File.basename(path).casecmp(dirname) == 0
333
- next
334
- end
335
- next if File.dirname(path) == src_data_dir
336
- end
337
- basename = File.basename(path)
338
- next unless extensions.include? File.extname(basename)[1..-1]
339
- next if @file_blacklist.include? "data/#{dirname.downcase}/#{basename.downcase}"
340
- out_name = "#{out_dir}/#{basename}"
341
- next if File.exist? out_name
342
- FileUtils.mkdir_p(out_dir)
343
- FileUtils.cp(path, out_name)
342
+ # Copy the file if it doesn't already exist
343
+ next if Util.join_path_nocase(out_dir, entry)
344
+
345
+ FileUtils.cp(path, "#{out_dir}/#{entry}")
344
346
  end
345
347
  end
346
348
  end
@@ -24,7 +24,7 @@ module WolfTrans
24
24
  # Load blacklist
25
25
  @file_blacklist = []
26
26
  if File.exists? "#{patch_dir}/blacklist.txt"
27
- IO.read_txt("#{patch_dir}/blacklist.txt").each_line do |line|
27
+ Util.read_txt("#{patch_dir}/blacklist.txt").each_line do |line|
28
28
  line.strip!
29
29
  next if line.empty?
30
30
  if line.include? '\\'
@@ -87,7 +87,7 @@ module WolfTrans
87
87
 
88
88
  if File.exists? filename
89
89
  output_write = true if mode == :update
90
- IO.read_txt(filename).each_line.with_index do |pristine_line, index|
90
+ Util.read_txt(filename).each_line.with_index do |pristine_line, index|
91
91
  # Remove comments and strip
92
92
  pristine_line.gsub!(/\n$/, '')
93
93
  line = pristine_line.gsub(/(?!\\)#.*$/, '').rstrip
@@ -10,7 +10,8 @@ module WolfTrans
10
10
 
11
11
  # Get the name of a path case-insensitively
12
12
  def self.join_path_nocase(parent, child)
13
- child_case = Dir.entries(parent).select { |e| e.downcase == child }.first
13
+ child_downcase = child.downcase
14
+ child_case = Dir.entries(parent).select { |e| e.downcase == child_downcase }.first
14
15
  return nil unless child_case
15
16
  return "#{parent}/#{child_case}"
16
17
  end
@@ -25,5 +26,30 @@ module WolfTrans
25
26
  def self.escape_path(path)
26
27
  full_strip(path).gsub(/[\x00\/\\:\*\?\"<>\|]/, '_')
27
28
  end
29
+
30
+ # Determine if a string should be added to the translation map.
31
+ # All non-nill, non-empty, and "black square" character strings are good.
32
+ def self.translatable?(string)
33
+ string && !string.empty? && string != "\u25A0"
34
+ end
35
+
36
+ # Read all lines of a txt file as UTF-8, rejecting
37
+ # anything with a BOM
38
+ def self.read_txt(filename)
39
+ text = nil
40
+ File.open(filename, 'rb:UTF-8') do |file|
41
+ if text = file.read(3)
42
+ if text == "\xEF\xBB\xBF"
43
+ raise "UTF-8 BOM detected; refusing to read file"
44
+ end
45
+ text << file.read
46
+ else
47
+ STDERR.puts "warning: empty patch file '#{filename}'"
48
+ return ''
49
+ end
50
+ end
51
+ # Convert Windows newlines and return
52
+ text.gsub(/\r\n?/, "\n")
53
+ end
28
54
  end
29
55
  end
@@ -1,3 +1,3 @@
1
1
  module WolfTrans
2
- VERSION = '0.1.0'
2
+ VERSION = '0.2.0'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wolftrans
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mathew Velasquez
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-09-16 00:00:00.000000000 Z
11
+ date: 2015-12-01 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A utility to translate Wolf RPG Editor games
14
14
  email: mathewvq@gmail.com
@@ -17,6 +17,7 @@ executables:
17
17
  extensions: []
18
18
  extra_rdoc_files: []
19
19
  files:
20
+ - ".gitignore"
20
21
  - LICENSE
21
22
  - README.md
22
23
  - Rakefile