snes_utils 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module SnesUtils
2
4
  module Wdc65816
3
5
  class Definitions
@@ -6,70 +8,70 @@ module SnesUtils
6
8
  HEX16 = "\\$?(#{HEX_DIGIT}{3,4})"
7
9
  HEX24 = "\\$?(#{HEX_DIGIT}{5,6})"
8
10
 
9
- SINGLE_OPERAND_INSTRUCTIONS = [:imm, :iml, :imm8, :imm16, :sr, :dp, :dpx, :dpy, :idp, :idx, :idy, :idl, :idly, :isy, :abs, :abx, :aby, :abl, :alx, :ind, :iax, :ial, :rel, :rell]
10
- DOUBLE_OPERAND_INSTRUCTIONS = [:bm]
11
- REL_INSTRUCTIONS = [:rel, :rell]
12
- BIT_INSTRUCTIONS = []
11
+ SINGLE_OPERAND_INSTRUCTIONS = %i[imm iml imm8 imm16 sr dp dpx dpy idp idx idy idl idly isy abs abx aby abl alx ind iax ial rel rell].freeze
12
+ DOUBLE_OPERAND_INSTRUCTIONS = [:bm].freeze
13
+ REL_INSTRUCTIONS = %i[rel rell].freeze
14
+ BIT_INSTRUCTIONS = [].freeze
13
15
 
14
16
  MODES_REGEXES = {
15
- acc: /^$/,
16
- imp: /^$/,
17
- imm: /^#{HEX8}$/i,
18
- iml: /^#{HEX16}$/i,
19
- imm8: /^##{HEX8}$/i,
17
+ acc: /^$/,
18
+ imp: /^$/,
19
+ imm: /^#{HEX8}$/i,
20
+ iml: /^#{HEX16}$/i,
21
+ imm8: /^##{HEX8}$/i,
20
22
  imm16: /^##{HEX16}$/i,
21
- sr: /^#{HEX8},S$/i,
22
- dp: /^#{HEX8}$/i,
23
- dpx: /^#{HEX8},X$/i,
24
- dpy: /^#{HEX8},Y$/i,
25
- idp: /^\(#{HEX8}\)$/i,
26
- idx: /^\(#{HEX8},X\)$/i,
27
- idy: /^\(#{HEX8}\),Y$/i,
28
- idl: /^\[#{HEX8}\]$/i,
29
- idly: /^\[#{HEX8}\],Y$/i,
30
- isy: /^\(#{HEX8},S\),Y$/i,
31
- abs: /^#{HEX16}$/i,
32
- abx: /^#{HEX16},X$/i,
33
- aby: /^#{HEX16},Y$/i,
34
- abl: /^#{HEX24}$/i,
35
- alx: /^#{HEX24},X$/i,
36
- ind: /^\(#{HEX16}\)$/i,
37
- iax: /^\(#{HEX16},X\)$/i,
38
- ial: /^\[#{HEX16}\]$/i,
39
- rel: /^#{HEX16}$/i,
40
- rell: /^#{HEX16}$/i,
41
- bm: /^#{HEX8},#{HEX8}$/i
42
- }
23
+ sr: /^#{HEX8},S$/i,
24
+ dp: /^#{HEX8}$/i,
25
+ dpx: /^#{HEX8},X$/i,
26
+ dpy: /^#{HEX8},Y$/i,
27
+ idp: /^\(#{HEX8}\)$/i,
28
+ idx: /^\(#{HEX8},X\)$/i,
29
+ idy: /^\(#{HEX8}\),Y$/i,
30
+ idl: /^\[#{HEX8}\]$/i,
31
+ idly: /^\[#{HEX8}\],Y$/i,
32
+ isy: /^\(#{HEX8},S\),Y$/i,
33
+ abs: /^#{HEX16}$/i,
34
+ abx: /^#{HEX16},X$/i,
35
+ aby: /^#{HEX16},Y$/i,
36
+ abl: /^#{HEX24}$/i,
37
+ alx: /^#{HEX24},X$/i,
38
+ ind: /^\(#{HEX16}\)$/i,
39
+ iax: /^\(#{HEX16},X\)$/i,
40
+ ial: /^\[#{HEX16}\]$/i,
41
+ rel: /^#{HEX16}$/i,
42
+ rell: /^#{HEX16}$/i,
43
+ bm: /^#{HEX8},#{HEX8}$/i
44
+ }.freeze
43
45
 
44
46
  MODES_FORMATS = {
45
- acc: "%s",
46
- imp: "%s",
47
- imm: "%s %02X",
48
- iml: "%s %02X",
49
- imm8: "%s #%02X",
50
- imm16: "%s #%04X",
51
- sr: "%s %02X,S",
52
- dp: "%s %02X",
53
- dpx: "%s %02X,X",
54
- dpy: "%s %02X,Y",
55
- idp: "%s (%02X)",
56
- idx: "%s (%02X,X)",
57
- idy: "%s (%02X),Y",
58
- idl: "%s [%02X]",
59
- idly: "%s [%02X],Y",
60
- isy: "%s (%02X,S),Y",
61
- abs: "%s %04X",
62
- abx: "%s %04X,X",
63
- aby: "%s %04X,Y",
64
- abl: "%s %06X",
65
- alx: "%s %06X,X",
66
- ind: "%s (%04X)",
67
- iax: "%s (%04X,X)",
68
- ial: "%s [%04X]",
69
- rel: "%s %04X {%s}",
70
- rell: "%s %04X {%s}",
71
- bm: "%s %02X,%02X"
72
- }
47
+ acc: '%s',
48
+ imp: '%s',
49
+ imm: '%s %02X',
50
+ iml: '%s %02X',
51
+ imm8: '%s #%02X',
52
+ imm16: '%s #%04X',
53
+ sr: '%s %02X,S',
54
+ dp: '%s %02X',
55
+ dpx: '%s %02X,X',
56
+ dpy: '%s %02X,Y',
57
+ idp: '%s (%02X)',
58
+ idx: '%s (%02X,X)',
59
+ idy: '%s (%02X),Y',
60
+ idl: '%s [%02X]',
61
+ idly: '%s [%02X],Y',
62
+ isy: '%s (%02X,S),Y',
63
+ abs: '%s %04X',
64
+ abx: '%s %04X,X',
65
+ aby: '%s %04X,Y',
66
+ abl: '%s %06X',
67
+ alx: '%s %06X,X',
68
+ ind: '%s (%04X)',
69
+ iax: '%s (%04X,X)',
70
+ ial: '%s [%04X]',
71
+ rel: '%s %04X {%s}',
72
+ rell: '%s %04X {%s}',
73
+ bm: '%s %02X,%02X'
74
+ }.freeze
73
75
 
74
76
  OPCODES_DATA = [{ opcode: 0x61, mnemonic: 'ADC', mode: :idx, length: 2 },
75
77
  { opcode: 0x63, mnemonic: 'ADC', mode: :sr, length: 2 },
@@ -15,7 +15,7 @@ module SnesUtils
15
15
 
16
16
  raise ArgumentError, 'Image width and height must be a multiple of sprite size' if (@image.width % @char_size != 0) or (@image.height % @char_size != 0)
17
17
 
18
- alpha_first if alpha
18
+ unshift_alpha(alpha) if alpha
19
19
  fill_palette
20
20
  end
21
21
 
@@ -29,7 +29,8 @@ module SnesUtils
29
29
  end
30
30
  end
31
31
 
32
- def alpha_first
32
+ def unshift_alpha(alpha)
33
+ @palette.unshift(alpha)
33
34
  end
34
35
 
35
36
  def fill_palette
@@ -1,8 +1,7 @@
1
1
  require 'readline'
2
2
  require 'chunky_png'
3
3
  require 'matrix'
4
- require 'nokogiri'
5
-
4
+ require 'csv'
6
5
  require 'byebug'
7
6
 
8
7
  require 'mini_assembler/mini_assembler'
@@ -12,5 +11,7 @@ require 'mini_assembler/spc700/definitions'
12
11
  require 'png2snes/png2snes'
13
12
  require 'tmx2snes/tmx2snes'
14
13
 
14
+ require 'vas/vas'
15
+
15
16
  module SnesUtils
16
17
  end
@@ -14,14 +14,9 @@ module SnesUtils
14
14
  tnm = big_char ? 2 : 1 # big_char : 16x16 tiles. otherwise, 8x8 tiles
15
15
  row_offset = 16 * (tnm - 1) # Skip a row in case of 16x16 tiles ( tile #9 starts at index 32)
16
16
 
17
- doc = Nokogiri::XML(File.open(@file_path))
18
- csv_node = doc.xpath('//data').children.first.to_s
19
-
20
- csv = csv_node.split("\n").compact.reject { |e| e.empty? }.map { |row| row.split(',') }
21
-
22
- csv.each do |row|
17
+ CSV.foreach(@file_path) do |row|
23
18
  raise if row.length != 32
24
- @tilemap += row.map { |r| (r.to_i - 1)*tnm + row_offset * ((r.to_i - 1)/8).to_i }
19
+ @tilemap += row.map { |r| (r.to_i)*tnm + row_offset * ((r.to_i)/8).to_i }
25
20
  end
26
21
 
27
22
  raise if @tilemap.length != 32*32
@@ -0,0 +1,407 @@
1
+ require 'securerandom'
2
+
3
+ module SnesUtils
4
+ class Vas
5
+ WDC65816 = :wdc65816
6
+ SPC700 = :spc700
7
+
8
+ DIRECTIVE = [
9
+ '.65816', '.spc700', '.org', '.base', '.db', '.rb', '.incbin', '.incsrc', '.define'
10
+ ]
11
+
12
+ LABEL_OPERATORS = ['@', '!', '<', '>', '\^']
13
+
14
+ def initialize(filename)
15
+ raise "File not found: #{filename}" unless File.file?(filename)
16
+
17
+ @filename = filename
18
+ @file = {}
19
+ @label_registry = []
20
+ @define_registry = {}
21
+ @incbin_list = []
22
+ @byte_sequence_list = []
23
+ @memory = []
24
+ end
25
+
26
+ def assemble
27
+ construct_file
28
+
29
+ 2.times do |pass|
30
+ @program_counter = 0
31
+ @origin = 0
32
+ @base = 0
33
+ @cpu = WDC65816
34
+
35
+ assemble_file(pass)
36
+ end
37
+
38
+ write_label_registry
39
+ insert_bytes
40
+ incbin
41
+ write
42
+ end
43
+
44
+ def construct_file(filename = @filename)
45
+ File.open(filename).each_with_index do |raw_line, line_no|
46
+ line = raw_line.split(';').first.strip.chomp
47
+ next if line.empty?
48
+
49
+ if line.start_with?('.include')
50
+ directive = line.split(' ')
51
+ inc_filename = directive[1].to_s.strip.chomp
52
+
53
+ construct_file(inc_filename)
54
+ elsif line.start_with?('.define')
55
+ args = line.split(' ')
56
+ key = "#{args[1]}"
57
+ val = args[2..-1].join(' ').split(';').first.strip.chomp
58
+
59
+ raise "Already defined: #{key}" unless @define_registry[key].nil?
60
+ @define_registry[key] = val
61
+ else
62
+ new_line = replace_define(line)
63
+
64
+ @file[SecureRandom.uuid] = { line: new_line, orig_line: line, line_no: line_no + 1, filename: filename }
65
+ end
66
+ end
67
+
68
+ end
69
+
70
+ def replace_define(line)
71
+ found = nil
72
+
73
+ @define_registry.keys.each do |key|
74
+ if line.match(/\b#{key}\b/)
75
+ found = key
76
+ break
77
+ end
78
+ end
79
+
80
+
81
+ return line if found.nil?
82
+
83
+ val = @define_registry[found]
84
+
85
+ line.gsub(/\b#{found}/, val)
86
+ end
87
+
88
+ def assemble_file(pass)
89
+ @file.each do |key, val|
90
+ @line = val[:line]
91
+
92
+ if @line.include?(':')
93
+ arr = @line.split(':')
94
+ label = arr[0].strip.chomp
95
+ unless /^\w+$/ =~ label
96
+ raise "Invalid label: #{label}"
97
+ end
98
+ register_label(label) if pass == 0
99
+ next unless arr[1]
100
+ instruction = arr[1].strip.chomp
101
+ else
102
+ instruction = @line
103
+ end
104
+
105
+ next if instruction.empty?
106
+
107
+ if instruction.start_with?(*DIRECTIVE)
108
+ process_directive(instruction, pass)
109
+ next
110
+ end
111
+
112
+ begin
113
+ bytes = LineAssembler.new(instruction, **options).assemble
114
+ rescue => e
115
+ puts "Error at line #{val[:filename]}##{val[:line_no]} - (#{val[:orig_line]}) : #{e}"
116
+ exit(1)
117
+ end
118
+
119
+ insert(bytes) if pass == 1
120
+ @program_counter += bytes.size
121
+ end
122
+ end
123
+
124
+ def register_label(label)
125
+ raise "Label already defined: #{label}" if @label_registry.detect { |l| l[0] == label }
126
+ @label_registry << [label, @program_counter + @origin]
127
+ end
128
+
129
+ def insert(bytes, insert_at = insert_index)
130
+ @memory[insert_at..insert_at + bytes.size - 1] = bytes
131
+ end
132
+
133
+ def insert_index
134
+ @program_counter + @base
135
+ end
136
+
137
+ def write(filename = 'out.smc')
138
+ File.open(filename, 'w+b') do |file|
139
+ file.write([@memory.map { |i| Vas::hex(i) }.join].pack('H*'))
140
+ end
141
+
142
+ filename
143
+ end
144
+
145
+ def self.hex(num, rjust_len = 2)
146
+ (num || 0).to_s(16).rjust(rjust_len, '0').upcase
147
+ end
148
+
149
+ def options
150
+ {
151
+ program_counter: @program_counter,
152
+ origin: @origin,
153
+ cpu: @cpu,
154
+ label_registry: @label_registry
155
+ }
156
+ end
157
+
158
+ def process_directive(instruction, pass)
159
+ directive = instruction.split(' ')
160
+
161
+ case directive[0]
162
+ when '.65816'
163
+ @cpu = WDC65816
164
+ when '.spc700'
165
+ @cpu = SPC700
166
+ when '.org'
167
+ update_origin(directive[1].to_i(16))
168
+ when '.base'
169
+ @base = directive[1].to_i(16)
170
+ when '.incbin'
171
+ @program_counter += prepare_incbin(directive[1].to_s.strip.chomp, pass)
172
+ when '.db'
173
+ raw_line = directive[1..-1].join.to_s.strip.chomp
174
+ line = LineAssembler.new(raw_line, **options).replace_labels(raw_line)
175
+
176
+ @program_counter += define_bytes(line, pass)
177
+ when '.rb'
178
+ @program_counter += directive[1].to_i(16)
179
+ when '.define'
180
+ # TODO
181
+ end
182
+ end
183
+
184
+ def update_origin(param)
185
+ @origin = param
186
+ @program_counter = 0
187
+
188
+ update_base_from_origin
189
+ end
190
+
191
+ def update_base_from_origin
192
+ # TODO: automatically update base
193
+ # lorom/hirom scheme
194
+ # spc700 scheme
195
+ end
196
+
197
+ def prepare_incbin(filename, pass)
198
+ raise "Incbin: file not found: #{filename}" unless File.file?(filename)
199
+
200
+ @incbin_list << [filename, insert_index] if pass == 0
201
+ File.size(filename) || 0
202
+ end
203
+
204
+ def incbin
205
+ @incbin_list.each do |filename, index|
206
+ file = File.open(filename)
207
+ bytes = file.each_byte.to_a
208
+ @line = filename
209
+ insert(bytes, index)
210
+ end
211
+ end
212
+
213
+ def define_bytes(raw_bytes, pass)
214
+ bytes = raw_bytes.split(',').map { |rb| rb.scan(/.{2}/).reverse }.flatten.map do |b|
215
+ bv = b.to_i(16)
216
+ raise "Invalid byte: #{b} : #{@line}" if bv < 0 || bv > 0xff
217
+ bv
218
+ end
219
+
220
+ @byte_sequence_list << [bytes, insert_index] if pass == 0
221
+ bytes.size
222
+ end
223
+
224
+ def insert_bytes
225
+ @byte_sequence_list.each do |bytes, index|
226
+ insert(bytes, index)
227
+ end
228
+ end
229
+
230
+ def write_label_registry
231
+ longest = @label_registry.map{|r| r[0] }.max_by(&:length)
232
+
233
+ File.open('labels.txt', 'w+b') do |file|
234
+ @label_registry.each do |label|
235
+ adjusted_label = label[0].ljust(longest.length, ' ')
236
+ raw_address = Vas::hex(label[1], 6)
237
+ address = "#{raw_address[0..1]}/#{raw_address[2..-1]}"
238
+ file.write "#{adjusted_label} #{address}\n"
239
+ end
240
+ end
241
+ end
242
+ end
243
+
244
+ class LineAssembler
245
+ def initialize(raw_line, **options)
246
+ @line = raw_line.split(';').first.strip.chomp
247
+ @current_address = (options[:program_counter] + options[:origin])
248
+ @cpu = options[:cpu]
249
+ @label_registry = options[:label_registry]
250
+ end
251
+
252
+ def assemble
253
+ instruction = @line.split(' ')
254
+ mnemonic = instruction[0].upcase
255
+ raw_operand = instruction[1].to_s
256
+
257
+ raw_operand = replace_label(raw_operand)
258
+
259
+ opcode_data = detect_opcode(mnemonic, raw_operand)
260
+ raise "Invalid syntax" unless opcode_data
261
+
262
+ opcode = opcode_data[:opcode]
263
+ @mode = opcode_data[:mode]
264
+ @length = opcode_data[:length]
265
+
266
+ operand_data = detect_operand(raw_operand)
267
+
268
+ operand = process_operand(operand_data)
269
+
270
+ return [opcode, *operand]
271
+ end
272
+
273
+ def contains_label?(operand)
274
+ Vas::LABEL_OPERATORS.any? { |s| operand.include?(s[-1,1]) }
275
+ end
276
+
277
+ def replace_labels(operand)
278
+ while contains_label?(operand)
279
+ operand = replace_label(operand)
280
+ end
281
+
282
+ operand
283
+ end
284
+
285
+ def replace_label(operand)
286
+ return operand unless contains_label?(operand)
287
+
288
+ unless matches = /(#{Vas::LABEL_OPERATORS.join('|')})(\w+)(\+(\d+))?/.match(operand)
289
+ raise "Invalid label syntax: #{operand}"
290
+ end
291
+
292
+ mode = matches[1]
293
+ label = matches[2]
294
+ offset = matches[4].to_i
295
+
296
+ label_data = @label_registry.detect { |l| l[0] == label }
297
+
298
+ value = label_data ? label_data[1] : @current_address
299
+
300
+ value += offset
301
+
302
+ case mode
303
+ when '@'
304
+ value = value & 0x00ffff
305
+ new_value = Vas::hex(value, 4)
306
+ when '!'
307
+ value = value | (((@current_address >> 16) & 0xff) << 16)
308
+ new_value = Vas::hex(value, 6)
309
+ when '<'
310
+ value = value & 0x0000ff
311
+ new_value = Vas::hex(value)
312
+ when '>'
313
+ value = (value & 0x00ff00) >> 8
314
+ new_value = Vas::hex(value)
315
+ when '^'
316
+ mode = '\^'
317
+ value = (value & 0xff0000) >> 16
318
+ new_value = Vas::hex(value)
319
+ else
320
+ raise "Mode error: #{mode}"
321
+ end
322
+
323
+ operand.gsub(/(#{mode})\w+(\+(\d+))?/, new_value)
324
+ end
325
+
326
+ def detect_opcode(mnemonic, operand)
327
+ SnesUtils.const_get(@cpu.capitalize)::Definitions::OPCODES_DATA.detect do |row|
328
+ mode = row[:mode]
329
+ regex = SnesUtils.const_get(@cpu.capitalize)::Definitions::MODES_REGEXES[mode]
330
+ row[:mnemonic] == mnemonic && regex =~ operand
331
+ end
332
+ end
333
+
334
+ def detect_operand(raw_operand)
335
+ SnesUtils.const_get(@cpu.capitalize)::Definitions::MODES_REGEXES[@mode].match(raw_operand)
336
+ end
337
+
338
+ def process_operand(operand_data)
339
+ if double_operand_instruction?
340
+ process_double_operand_instruction(operand_data)
341
+ else
342
+ operand = operand_data[1]&.to_i(16)
343
+ rel_instruction? ? process_rel_operand(operand) : little_endian(operand, @length - 1)
344
+ end
345
+ end
346
+
347
+ def process_double_operand_instruction(operand_data)
348
+ if bit_instruction?
349
+ process_bit_operand(operand_data)
350
+ else
351
+ operands = [operand_data[1], operand_data[2]].map { |o| o.to_i(16) }
352
+ operand_2 = rel_instruction? ? process_rel_operand(operands[1]) : operands[1]
353
+
354
+ rel_instruction? ? [operands[0], operand_2] : [operand_2, operands[0]]
355
+ end
356
+ end
357
+
358
+ def process_bit_operand(operand_data)
359
+ m = operand_data[1].to_i(16)
360
+ raise "Out of range: m > 0x1fff: #{m}" if m > 0x1fff
361
+
362
+ b = operand_data[2].to_i(16)
363
+ raise "Out of range: b > 7: #{b}" if b > 7
364
+
365
+ little_endian(m << 3 | b, 2)
366
+ end
367
+
368
+ def process_rel_operand(operand)
369
+ relative_addr = operand - (@current_address & 0x00ffff) - @length
370
+
371
+ if @cpu == Vas::WDC65816 && @mode == :rell
372
+ raise "Relative address out of range: #{relative_addr}" if relative_addr < -32_768 || relative_addr > 32_767
373
+
374
+ relative_addr += 0x10000 if relative_addr < 0
375
+ little_endian(relative_addr, 2)
376
+ else
377
+ raise "Relative address out of range: #{relative_addr}" if relative_addr < -128 || relative_addr > 127
378
+
379
+ relative_addr += 0x100 if relative_addr < 0
380
+ relative_addr
381
+ end
382
+
383
+ end
384
+
385
+ def little_endian(operand, length)
386
+ if length > 2
387
+ [((operand >> 0) & 0xff), ((operand >> 8) & 0xff), ((operand >> 16) & 0xff)]
388
+ elsif length > 1
389
+ [((operand >> 0) & 0xff), ((operand >> 8) & 0xff)]
390
+ else
391
+ operand
392
+ end
393
+ end
394
+
395
+ def double_operand_instruction?
396
+ SnesUtils.const_get(@cpu.capitalize)::Definitions::DOUBLE_OPERAND_INSTRUCTIONS.include?(@mode)
397
+ end
398
+
399
+ def bit_instruction?
400
+ SnesUtils.const_get(@cpu.capitalize)::Definitions::BIT_INSTRUCTIONS.include?(@mode)
401
+ end
402
+
403
+ def rel_instruction?
404
+ SnesUtils.const_get(@cpu.capitalize)::Definitions::REL_INSTRUCTIONS.include?(@mode)
405
+ end
406
+ end
407
+ end