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,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