n65 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +23 -0
- data/Gemfile +4 -0
- data/LICENSE +340 -0
- data/README.md +126 -0
- data/Rakefile +2 -0
- data/bin/n65 +11 -0
- data/data/opcodes.yaml +1030 -0
- data/examples/beep.asm +24 -0
- data/examples/mario2.asm +260 -0
- data/examples/mario2.char +0 -0
- data/examples/music_driver.asm +202 -0
- data/examples/noise.asm +93 -0
- data/examples/pulse_chord.asm +213 -0
- data/images/assembler_demo.png +0 -0
- data/lib/n65.rb +243 -0
- data/lib/n65/directives/ascii.rb +42 -0
- data/lib/n65/directives/bytes.rb +102 -0
- data/lib/n65/directives/dw.rb +86 -0
- data/lib/n65/directives/enter_scope.rb +55 -0
- data/lib/n65/directives/exit_scope.rb +35 -0
- data/lib/n65/directives/inc.rb +67 -0
- data/lib/n65/directives/incbin.rb +51 -0
- data/lib/n65/directives/ines_header.rb +53 -0
- data/lib/n65/directives/label.rb +46 -0
- data/lib/n65/directives/org.rb +47 -0
- data/lib/n65/directives/segment.rb +45 -0
- data/lib/n65/directives/space.rb +46 -0
- data/lib/n65/front_end.rb +90 -0
- data/lib/n65/instruction.rb +308 -0
- data/lib/n65/instruction_base.rb +29 -0
- data/lib/n65/memory_space.rb +150 -0
- data/lib/n65/opcodes.rb +9 -0
- data/lib/n65/parser.rb +85 -0
- data/lib/n65/regexes.rb +33 -0
- data/lib/n65/symbol_table.rb +198 -0
- data/lib/n65/version.rb +3 -0
- data/n65.gemspec +23 -0
- data/nes_lib/nes.sym +105 -0
- data/test/test_memory_space.rb +82 -0
- data/test/test_symbol_table.rb +238 -0
- data/utils/midi/Makefile +3 -0
- data/utils/midi/c_scale.mid +0 -0
- data/utils/midi/convert +0 -0
- data/utils/midi/guitar.mid +0 -0
- data/utils/midi/include/event.h +93 -0
- data/utils/midi/include/file.h +57 -0
- data/utils/midi/include/helpers.h +14 -0
- data/utils/midi/include/track.h +45 -0
- data/utils/midi/lil_melody.mid +0 -0
- data/utils/midi/mi_feabhra.mid +0 -0
- data/utils/midi/midi_to_nes.rb +204 -0
- data/utils/midi/source/convert.cpp +16 -0
- data/utils/midi/source/event.cpp +96 -0
- data/utils/midi/source/file.cpp +37 -0
- data/utils/midi/source/helpers.cpp +46 -0
- data/utils/midi/source/track.cpp +37 -0
- data/utils/opcode_table_to_yaml.rb +91 -0
- metadata +133 -0
data/examples/noise.asm
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
;------------------------------------------------------------------------------
|
2
|
+
; This is a direct port of Michael Martin's tutorial project for NES101
|
3
|
+
; With some modifications to the tile map, and extra comments, and ported to
|
4
|
+
; suit my assembler. - Saf
|
5
|
+
; See:
|
6
|
+
; http://hackipedia.org/Platform/Nintendo/NES/tutorial,%20NES%20programming%20101/NES101.html
|
7
|
+
;
|
8
|
+
;;;;
|
9
|
+
; Create an iNES header
|
10
|
+
.ines {"prog": 1, "char": 0, "mapper": 0, "mirror": 0}
|
11
|
+
|
12
|
+
|
13
|
+
;;;;
|
14
|
+
; Setup the interrupt vectors
|
15
|
+
.org $FFFA
|
16
|
+
.dw vblank
|
17
|
+
.dw main
|
18
|
+
.dw irq
|
19
|
+
|
20
|
+
|
21
|
+
.org $C000
|
22
|
+
;;;;
|
23
|
+
; Here is our code entry point, which we'll call main.
|
24
|
+
.scope main
|
25
|
+
; Disable interrupts and decimal flag
|
26
|
+
sei
|
27
|
+
cld
|
28
|
+
|
29
|
+
; Wait for 2 vblanks
|
30
|
+
wait_vb1:
|
31
|
+
lda $2002
|
32
|
+
bpl wait_vb1
|
33
|
+
wait_vb2:
|
34
|
+
lda $2002
|
35
|
+
bpl wait_vb2
|
36
|
+
|
37
|
+
; Now we want to initialize the hardware to a known state
|
38
|
+
lda #$00
|
39
|
+
ldx #$00
|
40
|
+
clear_segments:
|
41
|
+
sta $00, X
|
42
|
+
sta $0100, X
|
43
|
+
sta $0200, X
|
44
|
+
sta $0300, X
|
45
|
+
sta $0400, X
|
46
|
+
sta $0500, X
|
47
|
+
sta $0600, X
|
48
|
+
sta $0700, X
|
49
|
+
inx
|
50
|
+
bne clear_segments
|
51
|
+
|
52
|
+
; Reset the stack pointer
|
53
|
+
ldx #$FF
|
54
|
+
txs
|
55
|
+
|
56
|
+
; Disable all graphics.
|
57
|
+
lda #$00
|
58
|
+
sta $2000
|
59
|
+
sta $2001
|
60
|
+
|
61
|
+
; Init APU
|
62
|
+
ldx #$0F
|
63
|
+
stx $4015
|
64
|
+
|
65
|
+
; Turn on noise tone
|
66
|
+
ldx #$85
|
67
|
+
stx $400E
|
68
|
+
|
69
|
+
; Set volume to max
|
70
|
+
ldx #$3F
|
71
|
+
stx $400C
|
72
|
+
|
73
|
+
; Load Length counter
|
74
|
+
ldx #$01
|
75
|
+
stx $400F
|
76
|
+
|
77
|
+
; Resume interrupts and loop here forever
|
78
|
+
cli
|
79
|
+
forever:
|
80
|
+
jmp forever
|
81
|
+
.
|
82
|
+
|
83
|
+
|
84
|
+
;;;;
|
85
|
+
; Update everything on every vblank
|
86
|
+
vblank:
|
87
|
+
rti
|
88
|
+
|
89
|
+
|
90
|
+
;;;;
|
91
|
+
; Don't do anything on IRQ
|
92
|
+
irq:
|
93
|
+
rti
|
@@ -0,0 +1,213 @@
|
|
1
|
+
;;;;
|
2
|
+
; Create an iNES header
|
3
|
+
.ines {"prog": 1, "char": 0, "mapper": 0, "mirror": 0}
|
4
|
+
|
5
|
+
|
6
|
+
;;;;
|
7
|
+
; Include all the symbols in the nes library
|
8
|
+
.inc <nes.sym>
|
9
|
+
|
10
|
+
|
11
|
+
;;;;
|
12
|
+
; Open the prog section bank 0
|
13
|
+
.segment prog 0
|
14
|
+
|
15
|
+
|
16
|
+
;;;;
|
17
|
+
; Structure to keep track of input
|
18
|
+
.org $0000
|
19
|
+
.scope controller_state
|
20
|
+
.space b 1
|
21
|
+
.space a 1
|
22
|
+
.
|
23
|
+
|
24
|
+
|
25
|
+
;;;;
|
26
|
+
; Setup the interrupt vectors
|
27
|
+
.org $FFFA
|
28
|
+
.dw vblank
|
29
|
+
.dw reset
|
30
|
+
.dw irq
|
31
|
+
|
32
|
+
|
33
|
+
;;;;
|
34
|
+
; Here is our code entry point
|
35
|
+
.org $C000
|
36
|
+
.scope reset
|
37
|
+
sei ; SEt Interrupt (Disables them)
|
38
|
+
cld ; CLear Decimal Mode
|
39
|
+
|
40
|
+
ldx #$ff
|
41
|
+
txs ; Set the stack pointer
|
42
|
+
|
43
|
+
ldx #$00
|
44
|
+
stx nes.ppu.control
|
45
|
+
stx nes.ppu.mask ; Disable Vblank & Rendering
|
46
|
+
|
47
|
+
jsr zero_apu ; Zero all APU registers
|
48
|
+
|
49
|
+
; We need to wait for at least 2 Vblanks to happen
|
50
|
+
; before we know the PPU has stabilized at startup
|
51
|
+
; Here we wait for the first one.
|
52
|
+
wait_vblank1:
|
53
|
+
bit nes.ppu.status
|
54
|
+
bpl wait_vblank1
|
55
|
+
|
56
|
+
; Before we wait for the second vblank, lets
|
57
|
+
; zero all of the working RAM $0 to $800
|
58
|
+
; The $200s are sprite OAM, and should be set to $ff
|
59
|
+
clear_ram:
|
60
|
+
lda #$00
|
61
|
+
sta $00, x
|
62
|
+
sta $100, x
|
63
|
+
sta $300, x
|
64
|
+
sta $400, x
|
65
|
+
sta $500, x
|
66
|
+
sta $600, x
|
67
|
+
sta $700, x
|
68
|
+
lda #$ff
|
69
|
+
sta $200, x
|
70
|
+
inx
|
71
|
+
bne clear_ram
|
72
|
+
|
73
|
+
; Now wait for the second vblank
|
74
|
+
wait_vblank2:
|
75
|
+
bit nes.ppu.status
|
76
|
+
bpl wait_vblank2
|
77
|
+
|
78
|
+
jsr initialize
|
79
|
+
|
80
|
+
forever:
|
81
|
+
jmp forever
|
82
|
+
rti
|
83
|
+
.
|
84
|
+
|
85
|
+
|
86
|
+
;;;;
|
87
|
+
; Initialize everything
|
88
|
+
.scope initialize
|
89
|
+
; Enable pulse1 and pulse2 in the APU
|
90
|
+
lda #%00000011
|
91
|
+
sta nes.apu.channel_enable
|
92
|
+
|
93
|
+
; Initialize the controller states
|
94
|
+
lda #$00
|
95
|
+
sta controller_state.a zp
|
96
|
+
sta controller_state.b zp
|
97
|
+
|
98
|
+
; Reenable interrupts, Turn Vblank back on
|
99
|
+
lda #%10000000
|
100
|
+
sta nes.ppu.control
|
101
|
+
cli
|
102
|
+
rts
|
103
|
+
.
|
104
|
+
|
105
|
+
|
106
|
+
;;;;
|
107
|
+
; VBlank is called 60 times per second
|
108
|
+
.scope vblank
|
109
|
+
jsr read_input
|
110
|
+
rti
|
111
|
+
.
|
112
|
+
|
113
|
+
|
114
|
+
;;;;
|
115
|
+
; IRQ, we are not using
|
116
|
+
.scope irq
|
117
|
+
rti
|
118
|
+
.
|
119
|
+
|
120
|
+
|
121
|
+
;;;;
|
122
|
+
; Zero all the APU registers
|
123
|
+
.scope zero_apu
|
124
|
+
lda #$00
|
125
|
+
ldx #$00
|
126
|
+
loop:
|
127
|
+
sta $4000, x
|
128
|
+
inx
|
129
|
+
cpx $18
|
130
|
+
bne loop
|
131
|
+
rts
|
132
|
+
.
|
133
|
+
|
134
|
+
|
135
|
+
;;;;
|
136
|
+
; Read input from controller 1
|
137
|
+
.scope read_input
|
138
|
+
lda #$01 ; strobe joypad
|
139
|
+
sta nes.controller1
|
140
|
+
lda #$00
|
141
|
+
sta nes.controller1
|
142
|
+
|
143
|
+
; Handle Button A
|
144
|
+
lda nes.controller1
|
145
|
+
and #$01
|
146
|
+
beq update_a_state
|
147
|
+
|
148
|
+
; A is pressed, but did it just change to being pressed now?
|
149
|
+
ldx controller_state.a zp
|
150
|
+
bne update_a_state
|
151
|
+
|
152
|
+
; do the thing A does
|
153
|
+
jsr play_e329
|
154
|
+
|
155
|
+
update_a_state:
|
156
|
+
sta controller_state.a zp
|
157
|
+
|
158
|
+
; Handle Button B
|
159
|
+
lda nes.controller1
|
160
|
+
and #$01
|
161
|
+
beq update_b_state
|
162
|
+
|
163
|
+
; B is pressed, but did it just change to being pressed now?
|
164
|
+
ldx controller_state.b zp
|
165
|
+
bne update_b_state
|
166
|
+
|
167
|
+
; Do the thing B does
|
168
|
+
jsr play_a220
|
169
|
+
|
170
|
+
update_b_state:
|
171
|
+
sta controller_state.b zp
|
172
|
+
|
173
|
+
rts
|
174
|
+
.
|
175
|
+
|
176
|
+
|
177
|
+
;;;;
|
178
|
+
;; This will play an A 220hz note
|
179
|
+
;; On the pulse1 generator
|
180
|
+
.scope play_a220
|
181
|
+
pha
|
182
|
+
lda #%10011111
|
183
|
+
sta nes.apu.pulse1.control
|
184
|
+
|
185
|
+
lda #%11111011
|
186
|
+
sta nes.apu.pulse1.ft
|
187
|
+
|
188
|
+
lda #%11111001
|
189
|
+
sta nes.apu.pulse1.ct
|
190
|
+
|
191
|
+
pla
|
192
|
+
rts
|
193
|
+
.
|
194
|
+
|
195
|
+
|
196
|
+
;;;;
|
197
|
+
;; This will play an E 329.63hz note
|
198
|
+
;; On the pulse2 generator
|
199
|
+
.scope play_e329
|
200
|
+
pha
|
201
|
+
lda #%10011111
|
202
|
+
sta nes.apu.pulse2.control
|
203
|
+
|
204
|
+
lda #%01010010
|
205
|
+
sta nes.apu.pulse2.ft
|
206
|
+
|
207
|
+
lda #%11111001
|
208
|
+
sta nes.apu.pulse2.ct
|
209
|
+
|
210
|
+
pla
|
211
|
+
rts
|
212
|
+
.
|
213
|
+
|
Binary file
|
data/lib/n65.rb
ADDED
@@ -0,0 +1,243 @@
|
|
1
|
+
require_relative 'n65/version'
|
2
|
+
require_relative 'n65/symbol_table'
|
3
|
+
require_relative 'n65/memory_space'
|
4
|
+
require_relative 'n65/parser'
|
5
|
+
|
6
|
+
module N65
|
7
|
+
|
8
|
+
class Assembler
|
9
|
+
attr_reader :program_counter, :current_segment, :current_bank, :symbol_table, :virtual_memory, :promises
|
10
|
+
|
11
|
+
##### Custom exceptions
|
12
|
+
class AddressOutOfRange < StandardError; end
|
13
|
+
class InvalidSegment < StandardError; end
|
14
|
+
class WriteOutOfBounds < StandardError; end
|
15
|
+
class INESHeaderAlreadySet < StandardError; end
|
16
|
+
class FileNotFound < StandardError; end
|
17
|
+
|
18
|
+
|
19
|
+
####
|
20
|
+
## Assemble from an asm file to a nes ROM
|
21
|
+
def self.from_file(infile, outfile)
|
22
|
+
fail(FileNotFound, infile) unless File.exists?(infile)
|
23
|
+
|
24
|
+
assembler = self.new
|
25
|
+
program = File.read(infile)
|
26
|
+
|
27
|
+
puts "Building #{infile}"
|
28
|
+
## Process each line in the file
|
29
|
+
program.split(/\n/).each_with_index do |line, line_number|
|
30
|
+
begin
|
31
|
+
assembler.assemble_one_line(line)
|
32
|
+
rescue StandardError => e
|
33
|
+
STDERR.puts("\n\n#{e.class}\n#{line}\n#{e}\nOn line #{line_number}")
|
34
|
+
exit(1)
|
35
|
+
end
|
36
|
+
print '.'
|
37
|
+
end
|
38
|
+
puts
|
39
|
+
|
40
|
+
## Second pass to resolve any missing symbols.
|
41
|
+
print "Second pass, resolving symbols..."
|
42
|
+
assembler.fulfill_promises
|
43
|
+
puts " Done."
|
44
|
+
|
45
|
+
## Let's not export the symbol table to a file anymore
|
46
|
+
## Will add an option for this later.
|
47
|
+
#print "Writing symbol table to #{outfile}.yaml..."
|
48
|
+
#File.open("#{outfile}.yaml", 'w') do |fp|
|
49
|
+
#fp.write(assembler.symbol_table.export_to_yaml)
|
50
|
+
#end
|
51
|
+
#puts "Done."
|
52
|
+
|
53
|
+
## For right now, let's just emit the first prog bank
|
54
|
+
File.open(outfile, 'w') do |fp|
|
55
|
+
fp.write(assembler.emit_binary_rom)
|
56
|
+
end
|
57
|
+
puts "All Done :)"
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
####
|
62
|
+
## Initialize with a bank 1 of prog space for starters
|
63
|
+
def initialize
|
64
|
+
@ines_header = nil
|
65
|
+
@program_counter = 0x0
|
66
|
+
@current_segment = :prog
|
67
|
+
@current_bank = 0x0
|
68
|
+
@symbol_table = SymbolTable.new
|
69
|
+
@promises = []
|
70
|
+
@virtual_memory = {
|
71
|
+
:prog => [MemorySpace.create_prog_rom],
|
72
|
+
:char => []
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
####
|
78
|
+
## Return an object that contains the assembler's current state
|
79
|
+
def get_current_state
|
80
|
+
saved_program_counter, saved_segment, saved_bank = @program_counter, @current_segment, @current_bank
|
81
|
+
saved_scope = symbol_table.scope_stack.dup
|
82
|
+
OpenStruct.new(program_counter: saved_program_counter, segment: saved_segment, bank: saved_bank, scope: saved_scope)
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
####
|
87
|
+
## Set the current state from an OpenStruct
|
88
|
+
def set_current_state(struct)
|
89
|
+
@program_counter, @current_segment, @current_bank = struct.program_counter, struct.segment, struct.bank
|
90
|
+
symbol_table.scope_stack = struct.scope.dup
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
####
|
95
|
+
## This is the main assemble method, it parses one line into an object
|
96
|
+
## which when given a reference to this assembler, controls the assembler
|
97
|
+
## itself through public methods, executing assembler directives, and
|
98
|
+
## emitting bytes into our virtual memory spaces. Empty lines or lines
|
99
|
+
## with only comments parse to nil, and we just ignore them.
|
100
|
+
def assemble_one_line(line)
|
101
|
+
parsed_object = Parser.parse(line)
|
102
|
+
|
103
|
+
unless parsed_object.nil?
|
104
|
+
exec_result = parsed_object.exec(self)
|
105
|
+
|
106
|
+
## If we have returned a promise save it for the second pass
|
107
|
+
@promises << exec_result if exec_result.kind_of?(Proc)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
####
|
113
|
+
## This will empty out our promise queue and try to fullfil operations
|
114
|
+
## that required an undefined symbol when first encountered.
|
115
|
+
def fulfill_promises
|
116
|
+
while promise = @promises.pop
|
117
|
+
promise.call
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
####
|
123
|
+
## This rewinds the state of the assembler, so a promise can be
|
124
|
+
## executed with a previous state, for example if we can't resolve
|
125
|
+
## a symbol right now, and want to try during the second pass
|
126
|
+
def with_saved_state(&block)
|
127
|
+
## Save the current state of the assembler
|
128
|
+
old_state = get_current_state
|
129
|
+
|
130
|
+
lambda do
|
131
|
+
|
132
|
+
## Set the assembler state back to the old state and run the block like that
|
133
|
+
set_current_state(old_state)
|
134
|
+
block.call(self)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
|
139
|
+
####
|
140
|
+
## Write to memory space. Typically, we are going to want to write
|
141
|
+
## to the location of the current PC, current segment, and current bank.
|
142
|
+
## Bounds check is inside MemorySpace#write
|
143
|
+
def write_memory(bytes, pc = @program_counter, segment = @current_segment, bank = @current_bank)
|
144
|
+
memory_space = get_virtual_memory_space(segment, bank)
|
145
|
+
memory_space.write(pc, bytes)
|
146
|
+
@program_counter += bytes.size
|
147
|
+
end
|
148
|
+
|
149
|
+
|
150
|
+
####
|
151
|
+
## Set the iNES header
|
152
|
+
def set_ines_header(ines_header)
|
153
|
+
fail(INESHeaderAlreadySet) unless @ines_header.nil?
|
154
|
+
@ines_header = ines_header
|
155
|
+
end
|
156
|
+
|
157
|
+
|
158
|
+
####
|
159
|
+
## Set the program counter
|
160
|
+
def program_counter=(address)
|
161
|
+
fail(AddressOutOfRange) unless address_within_range?(address)
|
162
|
+
@program_counter = address
|
163
|
+
end
|
164
|
+
|
165
|
+
|
166
|
+
####
|
167
|
+
## Set the current segment, prog or char.
|
168
|
+
def current_segment=(segment)
|
169
|
+
segment = segment.to_sym
|
170
|
+
unless valid_segment?(segment)
|
171
|
+
fail(InvalidSegment, "#{segment} is not a valid segment. Try prog or char")
|
172
|
+
end
|
173
|
+
@current_segment = segment
|
174
|
+
end
|
175
|
+
|
176
|
+
|
177
|
+
####
|
178
|
+
## Set the current bank, create it if it does not exist
|
179
|
+
def current_bank=(bank_number)
|
180
|
+
memory_space = get_virtual_memory_space(@current_segment, bank_number)
|
181
|
+
if memory_space.nil?
|
182
|
+
@virtual_memory[@current_segment][bank_number] = MemorySpace.create_bank(@current_segment)
|
183
|
+
end
|
184
|
+
@current_bank = bank_number
|
185
|
+
end
|
186
|
+
|
187
|
+
|
188
|
+
####
|
189
|
+
## Emit a binary ROM
|
190
|
+
def emit_binary_rom
|
191
|
+
progs = @virtual_memory[:prog]
|
192
|
+
chars = @virtual_memory[:char]
|
193
|
+
puts "iNES Header"
|
194
|
+
puts "+ #{progs.size} PROG ROM bank#{progs.size != 1 ? 's' : ''}"
|
195
|
+
puts "+ #{chars.size} CHAR ROM bank#{chars.size != 1 ? 's' : ''}"
|
196
|
+
|
197
|
+
rom_size = 0x10
|
198
|
+
rom_size += MemorySpace::BankSizes[:prog] * progs.size
|
199
|
+
rom_size += MemorySpace::BankSizes[:char] * chars.size
|
200
|
+
|
201
|
+
puts "= Output ROM will be #{rom_size} bytes"
|
202
|
+
rom = MemorySpace.new(rom_size, :rom)
|
203
|
+
|
204
|
+
offset = 0x0
|
205
|
+
offset += rom.write(0x0, @ines_header.emit_bytes)
|
206
|
+
|
207
|
+
progs.each do |prog|
|
208
|
+
offset += rom.write(offset, prog.read(0x8000, MemorySpace::BankSizes[:prog]))
|
209
|
+
end
|
210
|
+
|
211
|
+
chars.each do |char|
|
212
|
+
offset += rom.write(offset, char.read(0x0, MemorySpace::BankSizes[:char]))
|
213
|
+
end
|
214
|
+
rom.emit_bytes.pack('C*')
|
215
|
+
end
|
216
|
+
|
217
|
+
|
218
|
+
private
|
219
|
+
|
220
|
+
|
221
|
+
####
|
222
|
+
## Get virtual memory space
|
223
|
+
def get_virtual_memory_space(segment, bank_number)
|
224
|
+
@virtual_memory[segment][bank_number]
|
225
|
+
end
|
226
|
+
|
227
|
+
|
228
|
+
####
|
229
|
+
## Is this a 16-bit address within range?
|
230
|
+
def address_within_range?(address)
|
231
|
+
address >= 0 && address < 2**16
|
232
|
+
end
|
233
|
+
|
234
|
+
|
235
|
+
####
|
236
|
+
## Is this a valid segment?
|
237
|
+
def valid_segment?(segment)
|
238
|
+
[:prog, :char].include?(segment)
|
239
|
+
end
|
240
|
+
|
241
|
+
end
|
242
|
+
|
243
|
+
end
|