snes_utils 0.1.1 → 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.
@@ -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