wolftrans 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,76 @@
1
+ require 'wolfrpg/io'
2
+
3
+ module WolfRpg
4
+ class GameDat
5
+ attr_accessor :unknown1
6
+ attr_accessor :title
7
+ attr_accessor :unknown2
8
+ attr_accessor :font
9
+ attr_accessor :subfonts
10
+ attr_accessor :default_pc_graphic
11
+ attr_accessor :version
12
+ attr_accessor :unknown3
13
+
14
+ def initialize(filename)
15
+ File.open(filename, 'rb') do |file|
16
+ IO.verify(file, MAGIC_NUMBER)
17
+ #TODO what is most of the junk in this file?
18
+ @unknown1 = IO.read(file, 25)
19
+
20
+ @title = IO.read_string(file)
21
+ if (magic_string = IO.read_string(file)) != MAGIC_STRING
22
+ raise "magic string invalid (got #{magic_string})"
23
+ end
24
+
25
+ unknown2_size = IO.read_int(file)
26
+ @unknown2 = IO.read(file, unknown2_size)
27
+
28
+ #IO.dump(file, 64)
29
+ #abort
30
+ @font = IO.read_string(file)
31
+ @subfonts = Array.new(3)
32
+ @subfonts.each_index do |i|
33
+ @subfonts[i] = IO.read_string(file)
34
+ end
35
+
36
+ @default_pc_graphic = IO.read_string(file)
37
+ @version = IO.read_string(file)
38
+
39
+ # This is the size of the file minus one.
40
+ # We don't need it, so discard it.
41
+ file.seek(4, :CUR)
42
+
43
+ # We don't care about the rest of this file for translation
44
+ # purposes.
45
+ # Someday we will know what the hell is stored in here... But not today.
46
+ @unknown3 = file.read
47
+ end
48
+ end
49
+
50
+ def dump(filename)
51
+ File.open(filename, 'wb') do |file|
52
+ IO.write(file, MAGIC_NUMBER)
53
+ IO.write(file, @unknown1)
54
+ IO.write_string(file, @title)
55
+ IO.write_string(file, MAGIC_STRING)
56
+ IO.write_int(file, @unknown2.bytesize)
57
+ IO.write(file, @unknown2)
58
+ IO.write_string(file, @font)
59
+ @subfonts.each do |subfont|
60
+ IO.write_string(file, subfont)
61
+ end
62
+ IO.write_string(file, @default_pc_graphic)
63
+ IO.write_string(file, @version)
64
+ IO.write_int(file, file.tell + 4 + @unknown3.bytesize - 1)
65
+ IO.write(file, @unknown3)
66
+ end
67
+ end
68
+
69
+ private
70
+ MAGIC_NUMBER = [
71
+ 0x00, 0x57, 0x00, 0x00, 0x4f, 0x4c, 0x00, 0x46, 0x4d, 0x00,
72
+ 0x15, 0x00, 0x00, 0x00, # likely an integer
73
+ ].pack('C*')
74
+ MAGIC_STRING = "0000-0000" # who knows what this is supposed to be
75
+ end
76
+ end
@@ -0,0 +1,63 @@
1
+ module WolfRpg
2
+ module IO
3
+ ########
4
+ # Read #
5
+ def self.read(io, size)
6
+ io.readpartial(size)
7
+ end
8
+ def self.read_byte(io)
9
+ io.readpartial(1).unpack('C').first
10
+ end
11
+ def self.read_int(io)
12
+ io.readpartial(4).unpack('l<').first
13
+ end
14
+ def self.read_string(io)
15
+ size = read_int(io)
16
+ return '' if size == 0
17
+ str = io.readpartial(size - 1).encode(Encoding::UTF_8, Encoding::WINDOWS_31J)
18
+ raise "string not null-terminated" unless read_byte(io) == 0
19
+ return str
20
+ end
21
+ def self.verify(io, data)
22
+ got = io.readpartial(data.length)
23
+ if got != data
24
+ raise "could not verify magic data (expecting #{data.unpack('C*')}, got #{got.unpack('C*')})"
25
+ end
26
+ end
27
+ def self.dump(io, length)
28
+ length.times do |i|
29
+ print " %02x" % read_byte(io)
30
+ end
31
+ print "\n"
32
+ end
33
+ def self.dump_until(pattern)
34
+ escaped_pattern = Regexp.escape(pattern)
35
+ str = ''.force_encoding('BINARY')
36
+ until str =~ /#{escaped_pattern}\z/nm
37
+ str << io.readpartial(1)
38
+ end
39
+ str.gsub(/#{escaped_pattern}\z/nm, '').each_byte do |byte|
40
+ print " %02x" % byte
41
+ end
42
+ print "\n"
43
+ end
44
+
45
+ #########
46
+ # Write #
47
+ def self.write(io, data)
48
+ io.write(data)
49
+ end
50
+ def self.write_byte(io, data)
51
+ io.write(data.chr)
52
+ end
53
+ def self.write_int(io, data)
54
+ io.write([data].pack('l<'))
55
+ end
56
+ def self.write_string(io, data)
57
+ new_data = data.encode(Encoding::WINDOWS_31J, Encoding::UTF_8)
58
+ write_int(io, new_data.bytesize + 1)
59
+ io.write(new_data)
60
+ write_byte(io, 0)
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,250 @@
1
+ require 'wolfrpg/io'
2
+ require 'wolfrpg/route'
3
+ require 'wolfrpg/command'
4
+
5
+ module WolfRpg
6
+ class Map
7
+ attr_reader :tileset_id
8
+ attr_reader :width
9
+ attr_reader :height
10
+ attr_reader :events
11
+
12
+ #DEBUG
13
+ attr_reader :filename
14
+
15
+ def initialize(filename)
16
+ @filename = File.basename(filename, '.*')
17
+ File.open(filename, 'rb') do |file|
18
+ IO.verify(file, MAGIC_NUMBER)
19
+
20
+ @tileset_id = IO.read_int(file)
21
+
22
+ # Read basic data
23
+ @width = IO.read_int(file)
24
+ @height = IO.read_int(file)
25
+ @events = Array.new(IO.read_int(file))
26
+
27
+ # Read tiles
28
+ #TODO: interpret this data later
29
+ @tiles = IO.read(file, @width * @height * 3 * 4)
30
+
31
+ # Read events
32
+ while (indicator = IO.read_byte(file)) == 0x6F
33
+ event = Event.new(file)
34
+ @events[event.id] = event
35
+ end
36
+ if indicator != 0x66
37
+ raise "unexpected event indicator: #{indicator.to_s(16)}"
38
+ end
39
+ unless file.eof?
40
+ raise "file not fully parsed"
41
+ end
42
+ end
43
+ end
44
+
45
+ def dump(filename)
46
+ File.open(filename, 'wb') do |file|
47
+ IO.write(file, MAGIC_NUMBER)
48
+ IO.write_int(file, @tileset_id)
49
+ IO.write_int(file, @width)
50
+ IO.write_int(file, @height)
51
+ IO.write_int(file, @events.size)
52
+ IO.write(file, @tiles)
53
+ @events.each do |event|
54
+ IO.write_byte(file, 0x6F)
55
+ event.dump(file)
56
+ end
57
+ IO.write_byte(file, 0x66)
58
+ end
59
+ end
60
+
61
+ #DEBUG method that searches for a string somewhere in the map
62
+ def grep(needle)
63
+ @events.each do |event|
64
+ event.pages.each do |page|
65
+ page.commands.each_with_index do |command, line|
66
+ command.string_args.each do |arg|
67
+ if m = arg.match(needle)
68
+ print "#{@filename}/#{event.id}/#{page.id+1}/#{line+1}: #{command.cid}\n\t#{command.args}\n\t#{command.string_args}\n"
69
+ break
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ def grep_cid(cid)
78
+ @events.each do |event|
79
+ event.pages.each do |page|
80
+ page.commands.each_with_index do |command, line|
81
+ if command.cid == cid
82
+ print "#{@filename}/#{event.id}/#{page.id+1}/#{line+1}: #{command.cid}\n\t#{command.args}\n\t#{command.string_args}\n"
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ class Event
90
+ attr_accessor :id
91
+ attr_accessor :name
92
+ attr_accessor :x
93
+ attr_accessor :y
94
+ attr_accessor :pages
95
+
96
+ def initialize(file)
97
+ IO.verify(file, MAGIC_NUMBER1)
98
+ @id = IO.read_int(file)
99
+ @name = IO.read_string(file)
100
+ @x = IO.read_int(file)
101
+ @y = IO.read_int(file)
102
+ @pages = Array.new(IO.read_int(file))
103
+ IO.verify(file, MAGIC_NUMBER2)
104
+
105
+ # Read pages
106
+ page_id = 0
107
+ while (indicator = IO.read_byte(file)) == 0x79
108
+ page = Page.new(file, page_id)
109
+ @pages[page_id] = page
110
+ page_id += 1
111
+ end
112
+ if indicator != 0x70
113
+ raise "unexpected event page indicator: #{indicator.to_s(16)}"
114
+ end
115
+ end
116
+
117
+ def dump(file)
118
+ IO.write(file, MAGIC_NUMBER1)
119
+ IO.write_int(file, @id)
120
+ IO.write_string(file, @name)
121
+ IO.write_int(file, @x)
122
+ IO.write_int(file, @y)
123
+ IO.write_int(file, @pages.size)
124
+ IO.write(file, MAGIC_NUMBER2)
125
+
126
+ # Write pages
127
+ @pages.each do |page|
128
+ IO.write_byte(file, 0x79)
129
+ page.dump(file)
130
+ end
131
+ IO.write_byte(file, 0x70)
132
+ end
133
+
134
+ class Page
135
+ attr_accessor :id
136
+ attr_accessor :unknown1
137
+ attr_accessor :graphic_name
138
+ attr_accessor :graphic_direction
139
+ attr_accessor :graphic_frame
140
+ attr_accessor :graphic_opacity
141
+ attr_accessor :graphic_render_mode
142
+ attr_accessor :conditions
143
+ attr_accessor :movement
144
+ attr_accessor :flags
145
+ attr_accessor :route_flags
146
+ attr_accessor :route
147
+ attr_accessor :commands
148
+ attr_accessor :shadow_graphic_num
149
+ attr_accessor :collision_width
150
+ attr_accessor :collision_height
151
+
152
+ def initialize(file, id)
153
+ @id = id
154
+
155
+ #TODO ???
156
+ @unknown1 = IO.read_int(file)
157
+
158
+ #TODO further abstract graphics options
159
+ @graphic_name = IO.read_string(file)
160
+ @graphic_direction = IO.read_byte(file)
161
+ @graphic_frame = IO.read_byte(file)
162
+ @graphic_opacity = IO.read_byte(file)
163
+ @graphic_render_mode = IO.read_byte(file)
164
+
165
+ #TODO parse conditions later
166
+ @conditions = IO.read(file, 1 + 4 + 4*4 + 4*4)
167
+ #TODO parse movement options later
168
+ @movement = IO.read(file, 4)
169
+
170
+ #TODO further abstract flags
171
+ @flags = IO.read_byte(file)
172
+
173
+ #TODO further abstract flags
174
+ @route_flags = IO.read_byte(file)
175
+
176
+ # Parse move route
177
+ @route = Array.new(IO.read_int(file))
178
+ @route.each_index do |i|
179
+ @route[i] = RouteCommand.create(file)
180
+ end
181
+
182
+ # Parse commands
183
+ @commands = Array.new(IO.read_int(file))
184
+ @commands.each_index do |i|
185
+ @commands[i] = Command.create(file)
186
+ end
187
+ IO.verify(file, COMMANDS_TERMINATOR)
188
+
189
+ #TODO abstract these options later
190
+ @shadow_graphic_num = IO.read_byte(file)
191
+ @collision_width = IO.read_byte(file)
192
+ @collision_height = IO.read_byte(file)
193
+
194
+ if (terminator = IO.read_byte(file)) != 0x7A
195
+ raise "page terminator not 7A (found #{terminator.to_s(16)})"
196
+ end
197
+ end
198
+
199
+ def dump(file)
200
+ IO.write_int(file, @unknown1)
201
+ IO.write_string(file, @graphic_name)
202
+ IO.write_byte(file, @graphic_direction)
203
+ IO.write_byte(file, @graphic_frame)
204
+ IO.write_byte(file, @graphic_opacity)
205
+ IO.write_byte(file, @graphic_render_mode)
206
+ IO.write(file, @conditions)
207
+ IO.write(file, @movement)
208
+ IO.write_byte(file, @flags)
209
+ IO.write_byte(file, @route_flags)
210
+ IO.write_int(file, @route.size)
211
+ @route.each do |cmd|
212
+ cmd.dump(file)
213
+ end
214
+ IO.write_int(file, @commands.size)
215
+ @commands.each do |cmd|
216
+ cmd.dump(file)
217
+ end
218
+ IO.write(file, COMMANDS_TERMINATOR)
219
+ IO.write_byte(file, @shadow_graphic_num)
220
+ IO.write_byte(file, @collision_width)
221
+ IO.write_byte(file, @collision_height)
222
+ IO.write_byte(file, 0x7A)
223
+ end
224
+
225
+ COMMANDS_TERMINATOR = [
226
+ 0x03, 0x00, 0x00, 0x00,
227
+ ].pack('C*')
228
+ end
229
+
230
+ private
231
+ MAGIC_NUMBER1 = [
232
+ 0x39, 0x30, 0x00, 0x00
233
+ ].pack('C*')
234
+ MAGIC_NUMBER2 = [
235
+ 0x00, 0x00, 0x00, 0x00
236
+ ].pack('C*')
237
+ end
238
+
239
+ private
240
+ MAGIC_NUMBER = [
241
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
242
+ 0x57, 0x4F, 0x4C, 0x46, 0x4D, 0x00,
243
+ 0x00, 0x00, 0x00, 0x00,
244
+ 0x64, 0x00, 0x00, 0x00,
245
+ 0x65,
246
+ 0x05, 0x00, 0x00, 0x00,
247
+ 0x82, 0xC8, 0x82, 0xB5, 0x00,
248
+ ].pack('C*')
249
+ end
250
+ end
@@ -0,0 +1,36 @@
1
+ module WolfRpg
2
+ class RouteCommand
3
+ def self.create(file)
4
+ # Read all data for this movement command from file
5
+ id = IO.read_byte(file)
6
+ args = Array.new(IO.read_byte(file))
7
+ args.each_index do |i|
8
+ args[i] = IO.read_int(file)
9
+ end
10
+ IO.verify(file, TERMINATOR)
11
+
12
+ #TODO Create proper route command
13
+ return RouteCommand.new(id, args)
14
+ end
15
+
16
+ def dump(file)
17
+ IO.write_byte(file, @id)
18
+ IO.write_byte(file, @args.size)
19
+ @args.each do |arg|
20
+ IO.write_int(file, arg)
21
+ end
22
+ IO.write(file, TERMINATOR)
23
+ end
24
+
25
+ attr_accessor :id
26
+ attr_accessor :args
27
+
28
+ def initialize(id, args)
29
+ @id = id
30
+ @args = args
31
+ end
32
+
33
+ private
34
+ TERMINATOR = [0x01, 0x00].pack('C*')
35
+ end
36
+ end
@@ -0,0 +1,169 @@
1
+ require 'wolftrans/patch_text'
2
+ require 'wolftrans/patch_data'
3
+ require 'wolfrpg'
4
+
5
+ module WolfTrans
6
+ class Patch
7
+ def initialize(game_path, patch_path)
8
+ @strings = Hash.new { |hash, key| hash[key] = Hash.new }
9
+ load_data(game_path)
10
+ load_patch(patch_path)
11
+ end
12
+ end
13
+
14
+ # Represents a translated string
15
+ class Translation
16
+ attr_reader :patch_filename
17
+ attr_reader :string
18
+ attr_accessor :autogenerate
19
+ alias_method :autogenerate?, :autogenerate
20
+
21
+ def initialize(patch_filename, string='', autogenerate=true)
22
+ @patch_filename = patch_filename
23
+ @string = string
24
+ @autogenerate = autogenerate
25
+ end
26
+
27
+ def to_s
28
+ @string
29
+ end
30
+ end
31
+
32
+ # Version; represents a major/minor version scheme
33
+ class Version
34
+ include Comparable
35
+ attr_accessor :major
36
+ attr_accessor :minor
37
+
38
+ def initialize(major: nil, minor: nil, flt: nil, str: nil)
39
+ # See if we need to parse from a string
40
+ if flt
41
+ string = flt.to_s
42
+ elsif str
43
+ string = str
44
+ else
45
+ string = nil
46
+ end
47
+
48
+ # Extract major and minor numbers
49
+ if string
50
+ if match = string.match(/(\d+)\.(\d+)/)
51
+ @major, @minor = match.captures.map { |s| s.to_i }
52
+ else
53
+ raise "could not parse version string '#{string}'"
54
+ end
55
+ elsif major && minor
56
+ @major = major
57
+ @minor = minor
58
+ else
59
+ @major = 0
60
+ @minor = 0
61
+ end
62
+ end
63
+
64
+ def to_s
65
+ "#{@major}.#{@minor}"
66
+ end
67
+
68
+ def <=>(other)
69
+ case other
70
+ when Version
71
+ return 0 if major == other.major && minor == other.minor
72
+ return 1 if major > other.major || (major == other.major && minor >= other.minor)
73
+ return -1
74
+ when String
75
+ return self == Version.new(str = other)
76
+ when Float
77
+ return self == Version.new(flt = other)
78
+ end
79
+ return nil
80
+ end
81
+ end
82
+
83
+ # IO functions
84
+ module IO
85
+ def self.read_txt(filename)
86
+ # Read file into memory, forcing UTF-8 without BOM.
87
+ text = nil
88
+ File.open(filename, 'rb:UTF-8') do |file|
89
+ if text = file.read(3)
90
+ if text == "\xEF\xBB\xBF"
91
+ raise "UTF-8 BOM detected; refusing to read file"
92
+ end
93
+ text << file.read
94
+ else
95
+ STDERR.puts "warning: empty patch file '#{filename}'"
96
+ return ''
97
+ end
98
+ end
99
+ # Convert Windows newlines and return
100
+ text.gsub(/\r\n?/, "\n")
101
+ end
102
+ end
103
+
104
+ #####################
105
+ # Utility functions #
106
+
107
+ # Sanitize a path; i.e., standardize path separators remove trailing separator
108
+ def self.sanitize_path(path)
109
+ if File::ALT_SEPARATOR
110
+ path = path.gsub(File::ALT_SEPARATOR, '/')
111
+ end
112
+ path.sub(/\/$/, '')
113
+ end
114
+
115
+ # Get the name of a path case-insensitively
116
+ def self.join_path_nocase(parent, child)
117
+ child_case = Dir.entries(parent).select { |e| e.downcase == child }.first
118
+ return nil unless child_case
119
+ return "#{parent}/#{child_case}"
120
+ end
121
+
122
+ # Strip all leading/trailing whitespace, including fullwidth spaces
123
+ def self.full_strip(str)
124
+ str.strip.sub(/^\u{3000}*/, '').sub(/\u{3000}*$/, '')
125
+ end
126
+
127
+ # Escape a string for use as a path on the filesystem
128
+ # https://stackoverflow.com/questions/2270635/invalid-chars-filter-for-file-folder-name-ruby
129
+ def self.escape_path(path)
130
+ full_strip(path).gsub(/[\x00\/\\:\*\?\"<>\|]/, '_')
131
+ end
132
+
133
+ ###################
134
+ # Debug functions #
135
+ def self.grep(dir, needle)
136
+ Find.find(dir) do |path|
137
+ next if FileTest.directory? path
138
+
139
+ basename = File.basename(path)
140
+ basename_downcase = basename.downcase
141
+ basename_noext = File.basename(basename_downcase, '.*')
142
+ parent_path = File.dirname(path)
143
+ ext = File.extname(basename_downcase)
144
+
145
+ if ext.downcase == '.mps'
146
+ WolfRpg::Map.new(path).grep(needle)
147
+ elsif ext.downcase == '.project'
148
+ next if basename_downcase == 'sysdatabasebasic.project'
149
+ dat_filename = WolfTrans.join_path_nocase(parent_path, "#{basename_noext}.dat")
150
+ next if dat_filename == nil
151
+ WolfRpg::Database.new(path, dat_filename).grep(needle)
152
+ elsif basename_downcase == 'commonevent.dat'
153
+ WolfRpg::CommonEvents.new(path).grep(needle)
154
+ end
155
+ end
156
+ end
157
+
158
+ def self.grep_cid(dir, cid)
159
+ Find.find(dir) do |path|
160
+ next if FileTest.directory? path
161
+ if File.extname(path).downcase == '.mps'
162
+ WolfRpg::Map.new(path).grep_cid(cid)
163
+ end
164
+ end
165
+ end
166
+
167
+ # Latest patch version format that can be read
168
+ TXT_VERSION = Version.new(major: 1, minor: 0)
169
+ end