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 +4 -4
- data/.gitignore +6 -0
- data/Rakefile +4 -0
- data/lib/wolfrpg/command.rb +226 -16
- data/lib/wolfrpg/common_events.rb +10 -5
- data/lib/wolfrpg/database.rb +20 -5
- data/lib/wolfrpg/filecoder.rb +119 -8
- data/lib/wolfrpg/game_dat.rb +27 -76
- data/lib/wolfrpg/map.rb +3 -3
- data/lib/wolftrans.rb +0 -21
- data/lib/wolftrans/patch_data.rb +81 -79
- data/lib/wolftrans/patch_text.rb +2 -2
- data/lib/wolftrans/util.rb +27 -1
- data/lib/wolftrans/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c841512ed31767357a132f115dd9c9d5d4d85ae3
|
4
|
+
data.tar.gz: 13b81d6e79ce6e6e7a3ca202073dd4b8c2a22dd8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4b63ce1e61df8e456faf61ae7064c8732dfc8626b75262641a9afe76b5f271c5e813daa97042435ea237fedf923e7129b74e363c6fbac6b5322f41a6316a5901
|
7
|
+
data.tar.gz: faea22c7bbf27d5e4171fa3056306014e869e4c57e4ea9c5697c6359a0a84255ae1b0ccf5ea14ee9d474d2fb00b8f4328790a96e93bbff84443b408d687ea7bc
|
data/.gitignore
ADDED
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
|
data/lib/wolfrpg/command.rb
CHANGED
@@ -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
|
-
|
149
|
+
class ChangeColor < Command
|
150
|
+
end
|
115
151
|
|
116
|
-
|
117
|
-
|
118
|
-
# Map of CIDs to classes #
|
152
|
+
class SetTransition < Command
|
153
|
+
end
|
119
154
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
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)
|
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
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
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
|
data/lib/wolfrpg/database.rb
CHANGED
@@ -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.
|
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
|
-
|
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
|
-
|
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
|
data/lib/wolfrpg/filecoder.rb
CHANGED
@@ -1,19 +1,49 @@
|
|
1
1
|
module WolfRpg
|
2
2
|
class FileCoder
|
3
|
+
##############
|
4
|
+
# Attributes #
|
3
5
|
attr_reader :io
|
4
|
-
|
5
|
-
def
|
6
|
-
@
|
6
|
+
attr_reader :crypt_header
|
7
|
+
def encrypted?
|
8
|
+
@crypt_header != nil
|
7
9
|
end
|
8
10
|
|
9
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/wolfrpg/game_dat.rb
CHANGED
@@ -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 :
|
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
|
21
|
-
@
|
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
|
-
|
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
|
-
|
42
|
-
@
|
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
|
-
@
|
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
|
-
|
75
|
-
|
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.
|
84
|
-
coder.
|
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.
|
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
|
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
|
data/lib/wolfrpg/map.rb
CHANGED
@@ -18,7 +18,8 @@ module WolfRpg
|
|
18
18
|
# Read basic data
|
19
19
|
@width = coder.read_int
|
20
20
|
@height = coder.read_int
|
21
|
-
|
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
|
-
|
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)}"
|
data/lib/wolftrans.rb
CHANGED
@@ -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
|
data/lib/wolftrans/patch_data.rb
CHANGED
@@ -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("#{
|
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
|
-
|
127
|
-
'
|
128
|
-
|
129
|
-
|
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
|
-
|
134
|
-
|
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
|
-
|
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,
|
141
|
-
copy_files(@game_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
|
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
|
236
|
+
yield s if Util.translatable? s
|
225
237
|
end
|
226
238
|
when WolfRpg::Command::SetString
|
227
|
-
yield command.text
|
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
|
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
|
299
|
+
return unless Util.translatable? string
|
288
300
|
if @strings.include? string
|
289
|
-
|
290
|
-
|
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,
|
307
|
+
def copy_files(src_dir, out_dir)
|
297
308
|
Find.find(src_dir) do |path|
|
298
|
-
|
299
|
-
|
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
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
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(
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
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
|
-
|
321
|
-
|
339
|
+
# Skip invalid file extensions
|
340
|
+
next unless extensions.include? File.extname(entry)[1..-1]
|
322
341
|
|
323
|
-
|
324
|
-
if
|
325
|
-
|
326
|
-
|
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
|
data/lib/wolftrans/patch_text.rb
CHANGED
@@ -24,7 +24,7 @@ module WolfTrans
|
|
24
24
|
# Load blacklist
|
25
25
|
@file_blacklist = []
|
26
26
|
if File.exists? "#{patch_dir}/blacklist.txt"
|
27
|
-
|
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
|
-
|
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
|
data/lib/wolftrans/util.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/wolftrans/version.rb
CHANGED
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.
|
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-
|
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
|