aboutsummaryrefslogtreecommitdiff
path: root/Makefile
blob: 5d8341948b5de672360892ab0ac21c81d13497dc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
# Makefile for Atari 800 Taipan, by B. Watson
# Written for GNU make, but seems to work with BSD make (though
# I don't test BSD make every time I change anything).

# 'make help' will show the list of targets.

# cl65 binary:
CC=cl65

# System cl65 will compile for. Don't expect this to work if you change it
# (though possibly atarixl might work someday, with newer cc65s)
SYS=atari

# Optimization flags for cc65.
#COPT=-Oirs
COPT=-O

# This is used for embedding the date and git info in the game binary.
# It'll appear on the title screen.
VER=0.99alpha
TODAY=`date +%Y%m%d`
#BRANCH=`( git branch 2> /dev/null || echo ' NOGIT' )| cut -d' ' -f2`
REV=`git rev-parse --short HEAD 2>/dev/null || echo UNKNOWN`
#VERSION="v$(VER)-$(TODAY)-$(BRANCH)-$(REV)"
VERSION="v$(VER)-$(TODAY)-$(REV)"

# Memory layout. Controlled by the Makefile from now on, no longer
# hardcoded addresses in various scripts. These are in hex. If any
# of them are changed, you *must* run 'make clean'.

# Font. Default is for the xex build. Cartridge build changes this.
FONT_ADDR=0x2000

# Title screen data will be decompressed to this address. XEX only,
# cartridge doesn't use compressed title.
TITLE_DATA_ADDR=0x2400

# Title display list, decompression code, and menu code. Should be
# TITLE_DATA_ADDR plus 0x1744. XEX only.
TITLE_CODE_ADDR=0x3b44

# Main game start address. All the code in TAIMAIN_C_SRC and
# TAIMAIN_ASM_SRC loads here. We get this address by looking for
# 'C code can load at' message when making newtitle.xex. It's the
# next byte after the end of the display list. Default is for the
# XEX, the cart changes it.
#TAIMAIN_ADDR=0x3d00
TAIMAIN_ADDR=0x3cc7

# Size of cc65 parameter stack in bytes. Default is 0x200, which is
# much bigger than actually necessary.
STACK_SIZE=0x200

# for older cc65, we needed a custom linker file.
#CFLAGS=-t $(SYS) -C custom.cfg -I. -L. $(COPT)

# for recent git cc65, we can reserve memory on the command line.
# -D__RESERVED_MEMORY__=1056 because cc65's default end of memory
# is $BC20, and we want it to end just below our font, which starts
# at $B800. cc65's runtime stack starts just below this, and is
# 2K in size (bottom is at 0xb000).
# -D__SYSTEM_CHECK__=1 stops cl65 from prepending a bit of code that
# checks to make sure there's enough memory. Older cc65 didn't do this,
# and I never missed it.
# The meaning of the -l flag is different between cc65-2.13.3
# and the later github cc65, so it's been removed here.
#CFLAGS=-t $(SYS) -T -I. -L. -Wl -D__SYSTEM_CHECK__=1 -Wl -D__RESERVED_MEMORY__=1056 $(COPT)

CFLAGS=-t $(SYS) -T -I. -L. -Wl -D__SYSTEM_CHECK__=1 -DFONT_ADDR=$(FONT_ADDR) --start-addr $(TAIMAIN_ADDR) -Wl -D__STACKSIZE__=$(STACK_SIZE) $(COPT) $(EXTRACFLAGS)

# These aren't really used:
AS=ca65
ASFLAGS=
AR=ar65

# C compiler for host system. Currently only used for building convfont
# and mkcart.
HOSTCC=gcc
HOSTCFLAGS=-Wall

# Perl binary. This Makefile relies heavily on perl.
PERL=perl

# Flags to pass to perl. We're dealing with raw binary data, not
# utf-8 encoded unicode text. The -C0 tells perl to ignore any
# PERL_UNICODE setting in the environment (see "perldoc perlrun").
PERLFLAGS=-C0

PERLF=$(PERL) $(PERLFLAGS)

# A few files have no make rules here. LORCHA.DAT is generated as a
# side-effect of generating taifont.xex. It's a 49-byte (7x7) blob of
# Atari "internal" screen codes.

# LORCHA.DAT, and PORTSTAT.DAT aren't deleted by a
# 'make clean'.

# romfont is the 1K font extracted from the Atari 800 OS, with a
# command like:
# dd if=atariosb.rom of=1 bs=256 skip=8 count=4
# ...where atariosb.rom comes from e.g. the PC-Xformer 2.5 zip file.

# The game binary:
XEX=taipan.xex

# All the C and asm sources for taimain.xex:
TAIMAIN_HDRS=sounds.h
TAIMAIN_C_SRC=taipan.c strtonum.c
TAIMAIN_ASM_SRC=rand.s draw_lorcha.s timed_getch.s portstat.s console.s cprintul.s soundasm.s explosion.s textdecomp.s arrayutils.s
TAIMAIN_LIBS=conio/conio.lib

# Comment these lines out to build without big number support.
# This will stop being possible at some point.
BIGNUM_SRC=bigfloat.s
BIGNUM_HDRS=bignum.h bigfloat.h
BIGNUM_CFLAGS=-DBIGNUM=BIGFLOAT

# Uncomment these for experimental int48 big numbers. Support
# hasn't been added, so leave these commented for now.
#BIGNUM_SRC=bigint48.s
#BIGNUM_HDRS=bignum.h bigint48.h
#BIGNUM_CFLAGS=-DBIGNUM=BIGINT48

# Default rule for plain 'make' command is to build the binary.
all: checkenv $(XEX) tags

biginttest: clean
	$(MAKE) BIGNUM_SRC=bigint48.s BIGNUM_HDRS="bignum.h bigint48.h" BIGNUM_CFLAGS=-DBIGNUM=BIGINT48
	atari800 -nobasic $(XEX)

# I have F10 in my editor bound to 'make test', so:
test: clean all
	atari800 -nobasic $(XEX)

# Check the build environment.
checkenv:
	@$(PERL) -e1 && echo "perl found" && exit 0 || echo "perl missing" && exit 1
	@$(PERL) -MImage::Magick -e1 && echo "perl Image::Magick found" && exit 0 || echo "perl Image::Magick missing" && exit 1
	@$(CC) --help > /dev/null && echo "cl65 found" && exit 0 || echo "cl65 missing (install cc65)" && exit 1
	@echo "int main() { return 0; }" > 1.c && exit 0 || echo "can't create files in current directory" && exit 1
	@$(HOSTCC) -o 1 1.c && echo "host C compiler found" && exit 0 || echo "host C compiler missing or broken" && exit 1

# ctags. I forgot it can handle cc65 asm source. One wrinkle: C
# identifiers get prepended with an underscore from the POV of assembly
# code, and asm identifiers need a leading underscore to be used from
# C. There's probably a clever way to get ctags to handle this, but I
# don't know it, so I wrote a perl script to do the job.
tags:
	@ctags $(TAIMAIN_C_SRC) $(TAIMAIN_ASM_SRC) 2>/dev/null && $(PERLF) fixtags.pl tags || true

help:
	@echo "Top-level targets:"
	@echo "make          - builds taipan.xex (disk version)"
	@echo "make cart     - builds taipan.rom and taipan.cart (cartridge version)"
	@echo "make test     - builds & runs taipan.xex in atari800 emulator"
	@echo "make testcart - builds & runs taipan.cart in atari800 emulator"
	@echo "make dos2     - builds & runs taipan.xex in atari800 on a "; \
	 echo "                DOS 2.0S floppy image [*]"
	@echo "make mydos    - builds & runs taipan.xex in atari800 on a "; \
	 echo "                MyDOS 4.50 floppy image [*]"
	@echo "make fenders  - builds & runs taipan.xex in atari800 on a "; \
	 echo "                Fenders 3-sector Loader floppy image [*]"
	@echo
	@echo "[*] floppy image targets require 'axe' utility, from:"
	@echo "    https://slackware.uk/~urchlay/src/axe-0.2.0.tar.gz"
	@echo
	@echo "Useful variables to add to the make command:"
	@echo "  CC          - path to cl65 binary (default: cl65, searches PATH)"
	@echo "  EXTRACFLAGS - extra options to pass to cl65"
	@echo "  HOSTCC      - path to native C compiler (default: gcc, searches PATH)"
	@echo "  HOSTCFLAGS  - extra options to pass to HOSTCC (default: -Wall)"
	@echo "  PERL        - path to perl binary (default: perl, searches PATH)"

# The above is fast & easy, but from time to time it's necessary
# to test slow loading, make sure the title screen stuff works OK.
dos2: clean all
	cp dos2.atr.in dos2.atr
	cp taipan.xex TAIPAN
	axe -w TAIPAN dos2.atr
	rm -f TAIPAN
	atari800 -nobasic -nopatch dos2.atr

fenders: clean all
	perl axecheck.pl taipan.xex
	cp taipan.xex TAIPAN
	cp fenders.atr.in fenders.atr
	axe -w TAIPAN fenders.atr
	rm -f TAIPAN
	atari800 -nopatch fenders.atr

mydos: clean all
	cp taipan.xex TAIPAN
	cp mydos.atr.in mydos.atr
	axe -w TAIPAN mydos.atr
	rm -f TAIPAN
	atari800 -nopatch mydos.atr

# We use our own custom conio instead of the one cc65 ships.
# see conion/README for details.
conio/conio.lib:
	cd conio && $(MAKE)

# The game binary is a multi-part binary load file. This rule
# depends on all the pieces, and just concatenates them. We used
# to just use cat, but reportedly some (dumb, broken) xex loaders
# have trouble with $FFFF markers at the start of the second and
# further segments, so multixex.pl skips them.
$(XEX): checkmem.xex taimain.xex taifont.xex newtitle.xex comptitle.xex
	$(PERLF) multixex.pl checkmem.xex comptitle.xex newtitle.xex taifont.xex taimain.xex > $(XEX)
	$(PERLF) size.pl $(TAIMAIN_ADDR) $(STACK_SIZE)

# Bitmap data for the title screen, 256x184 = 47104 pixels, 8 bits
# per pixel, or 5888 bytes. Displayed in ANTIC mode F (aka GR.8),
# using GTIA narrow playfield. The original title screen for the Apple
# is a 280x192 bitmap with a few blank lines at the top & bottom. I
# squished it horizontally to 256 pixels and got rid of the blank lines,
# to save load time. Note that titledata.dat is not built into
# the xex binary as-is: it's now used as input for creating
# comptitle.xex, the compressed title screen. For the cartridge,
# titledata.dat is included as-is.
titledata.dat: newtitle.pl newtitle.png
	$(PERLF) newtitle.pl > titledata.dat

# compressed title, for faster loading. see titlecompression.txt
# for gory details.
comptitle.xex: titledata.dat titlecomp.pl comptitle.s.in
	$(PERLF) titlecomp.pl 133 < titledata.dat
	$(CC) -l comptitle.lst -o comptitle.xex -t none --asm-define destination=$(TITLE_DATA_ADDR) comptitle.s

# tiny 1-sector memory checker, aborts the laod if a cart is present.
checkmem.xex: checkmem.s
	$(CC) -o checkmem.xex -t none checkmem.s

# Init segment that loads after the title screen data. It sets up
# a custom display list and sets the GTIA for narrow playfield,
# then waits for a keypress. Afterwards, it restores the OS's
# display list and sets the GTIA back to normal, then exits.
# Notice this is built with "-t none" instead of "-t atari",
# since it's a lot easier to homebrew an init segment than it is
# to get cc65 to build an init segment (would need a custom linker
# script at least).
newtitle.xex: newtitle.s ver.dat help.dat
	$(CC) -l newtitle.lst -m newtitle.map -o newtitle.xex -t none --asm-define screendata=$(TITLE_DATA_ADDR) --asm-define origin=$(TITLE_CODE_ADDR) newtitle.s

# Version number in Atari screen-data form
ver.dat: text2screen.pl
	echo "$(VERSION)" | $(PERLF) text2screen.pl > ver.dat

# Help text for the title screen
help.dat: help.txt text2screen.pl
	$(PERLF) text2screen.pl < help.txt > help.dat

#ver.dat: mkver.pl
#	$(PERL) mkver.pl $(VERSION) > ver.dat

# The main executable. All the C and asm code goes here, except the init
# segment in newtitle.s.
taimain.xex: $(TAIMAIN_C_SRC) $(TAIMAIN_ASM_SRC) $(TAIMAIN_HDRS) $(BIGNUM_SRC) $(BIGNUM_HDRS) $(TAIMAIN_LIBS) messages.c
	$(CC) -m taipan.map $(CFLAGS) $(BIGNUM_CFLAGS) -o taimain.xex $(TAIMAIN_C_SRC) $(TAIMAIN_ASM_SRC) $(BIGNUM_SRC) $(TAIMAIN_LIBS)

#cl65 --mapfile taipan.map $(CFLAGS) -o taimain.xex taipan.c sounds.c rand.s draw_lorcha.s timed_getch.s jsleep.s portstat.s console.s

# With newer cc65s, I have to do this to get an assembly listing of just
# taipan.c. This rule not used as part of the main build, it's only for
# debugging.
taipan.lst: taipan.c
	$(CC) -m taipan.map $(CFLAGS) $(BIGNUM_CFLAGS) -c -o /dev/null -l taipan.lst -T taipan.c

# Another such rule for sounds.c:
sounds.lst: sounds.c sounds.h
	$(CC) -m sounds.map $(CFLAGS) -c -o /dev/null -l sounds.lst -T sounds.c

# The font gets loaded into RAM, in the area reserved by the
# -D__RESERVED_MEMORY__ option to cl65. To actually use the font,
# taimain.xex contains code that sets CHBAS ($02f4).
# Not mentioned in any make rule: convfont also creates LORCHA.DAT,
# which draw_lorcha.s depends on (so we touch draw_lorcha.s here).
taifont.xex: convfont romfont font
	cat romfont font | ./convfont -x > taifont.xex
	touch draw_lorcha.s

# Used by the cartridge build, but not the disk (xex) binary. This just
# builds the font without the Atari 6-byte binary load header. It ends
# up at the top of one of the cartridge banks, and is also useful for
# eyeballing the font in bitmapdump.pl or converting to other formats.
taifont: convfont romfont font
	cat romfont font | ./convfont > taifont

# PORTSTAT.DAT gets incbin'ed directly in portstat.s. It's screen-code
# data for the static part of the port status screen, which ends up in
# an array that the C code can memcpy() into screen RAM as needed.
# This make rule actually runs atari800. For it to work, you'll need
# the H: device enabled, pointed at the current directory, and set
# to writable.
PORTSTAT.DAT: mkportstats.xex
	atari800 -nobasic mkportstats.xex
	touch portstat.s

# Host tool that builds our custom font from the data ripped out of
# the Apple version, plus the Atari OS ROM. Also, all the custom
# characters for the enemy ships are defined here (as hex data. Yes,
# I converted them manually from eyeballing a screenshot of the Apple
# combat screen).
convfont: convfont.c
	$(HOSTCC) $(HOSTCFLAGS) -DFONT_ADDR=$(FONT_ADDR) -o convfont convfont.c

# text compressor
textcomp: textcomp.c
	$(HOSTCC) $(HOSTCFLAGS) -o textcomp textcomp.c

# textdecomp.s includes msg.inc
textdecomp.s: msg.inc

msg.inc: messages.c

# messages.c is a generated file
messages.c: messages.pl textcomp
	perl messages.pl > messages.c

### Cartridge-related targets. The way I'm doing this isn't 'proper': I should
#   be using cc65's linker with a fancy config script to do the bank layout
#   and such. But it's a lot easier for me to use the tools I know how to use,
#   so that's what I did.

# mkcart turns a raw binary into an atar800 .cart image with 16-byte header.
# originally I wrote this for use with the DASM assembler, but there's
# nothing DASM-specific about it.
mkcart: mkcart.c
	$(HOSTCC) $(HOSTCFLAGS) -o mkcart mkcart.c

cartbank2.cfg: cartbank2.cfg.old cartbank2.cfg.new cartbank2.sh
	sh cartbank2.sh

# cc65 doc atari.html explains how to produce a raw binary file.
# using a custom crt0 to get rid of the extra RTS cc65 puts there for
# SpartaDOS compatibility (which has no effect on a cartridge image,
# except to waste 1 byte).
romable_taimain.raw: cartbank2.cfg $(TAIMAIN_C_SRC) $(TAIMAIN_ASM_SRC) $(TAIMAIN_HDRS) $(BIGNUM_SRC) $(BIGNUM_HDRS) $(TAIMAIN_LIBS) crt0_cart.s messages.c
	$(CC) --config cartbank2.cfg -m taipan.map -t atari -T -I. -L. -DFONT_ADDR=0x9c00 --start-addr 0x400 -Wl -D__STACKSIZE__=0x200 -O -Wl -D__SYSTEM_CHECK__=1 -Wl -D__AUTOSTART__=1 -Wl -D__EXEHDR__=1  -DCART_TARGET=1 --asm-define CART_TARGET=1 -DBIGNUM=BIGFLOAT -o romable_taimain.raw $(TAIMAIN_C_SRC) $(TAIMAIN_ASM_SRC) $(BIGNUM_SRC) $(TAIMAIN_LIBS) crt0_cart.s

# 256 bytes of $ff filler, for the last page of each code bank. Wasting
# this little bit of space simplifies the copying code in bank7.s (no
# partial last page to copy), and guarantees I don't accidentally end
# up with a 0 in the "cart present" byte of the cart trailer.
fill256:
	$(PERLF) -Mbytes -e 'print chr(0xff) x 256' > fill256

# 8192 bytes of $ff filler, for unused banks. Possibly these will be
# used for something like an interactive game manual/tutorial.
blankbank:
	$(PERLF) -Mbytes -e 'print chr(0xff) x 8192' > blankbank

splitrom.raw.0: splitrom.raw.2

splitrom.raw.1: splitrom.raw.2

# split romable_taimain.raw into bank-sized chunks. if we end up
# with 3 chunks, the cart won't work correctly, so stop the build here
# in that case.
splitrom.raw.2: romable_taimain.raw
	split -b 7936 -a 1 -d romable_taimain.raw splitrom.raw.
	[ -e splitrom.raw.3 ] && echo "*** romable_taimain.raw too big" && rm -f splitrom.raw.* && exit 1 || exit 0

bank0: splitrom.raw.0 fill256
	cat splitrom.raw.0 fill256 > bank0

bank1: splitrom.raw.1 fill256
	cat splitrom.raw.1 fill256 > bank1

#bank2: splitrom.raw.2 fill256
#	cl65 -l bank2.lst -m bank2.map -t none -o bank2 bank2.s

bank2: rodata.8000 bank2.s taifont romable_taimain.raw
	$(CC) -l bank2.lst -m bank2.map -t none -o bank2 bank2.s

bank3: bank3.s titledata.dat ver.dat help.dat newtitle.s splitrom.raw.2
	$(CC) -l bank3.lst -m bank3.map -t none -o bank3 bank3.s

# raw ROM, for burning to EPROM/flash.
taipan.rom: bank0 bank1 bank2 bank3
	cat bank0 bank1 bank2 bank3 > taipan.rom

# .cart version with atari800-compatible header.
taipan.cart: taipan.rom mkcart
	./mkcart -otaipan.cart -t12 taipan.rom
	./mkcart -ctaipan.cart

cart: checkenv taipan.cart

testcart: clean cart
	atari800 taipan.cart


### Rules for building various file types with the cc65 toolchain.
.s.o:
	$(AS) $(ASFLAGS) -o $@ $<

.c.o:
	$(CC) $(CFLAGS) -c -o $@ $<

%.xex: %.c
	$(CC) --mapfile map $(CFLAGS) -o $@ $<

# Obligatory clean and distclean rules.
clean:
	rm -f *.o *.lst convfont mkcart gzip2deflate *.xex AUTORUN.SYS taipan.atr dos2.atr mydos.atr fenders.atr ver.dat help.dat tags cartmsg.dat splitrom.raw.* taipan.rom taipan.cart bank[0-9] fill256 blankbank romable_taimain.raw splitrom.raw.* comptitle.s comptitle.dat conio/*.o conio/*.lib messages.c textcomp titledata.dat cartbank2.cfg

distclean: clean
	rm -f *~ core .*.swp 1.* 2.* 1 2 3 map map.* *.map a b c foo bar baz

push:
	sh push.sh

size: clean all
	$(PERLF) size.pl $(TAIMAIN_ADDR) $(STACK_SIZE)

procsizes: clean all taipan.lst
	$(PERLF) procsizes.pl > procsizes
	cat procsizes

# Cruft. Was used for testing the enemy ship animation.
lorchatest: lorchatest.c draw_lorcha.s taifont.xex
	$(CC) -t atari -O -T -o lorchatest1.xex lorchatest.c draw_lorcha.s
	cat taifont.xex lorchatest1.xex > lorchatest.xex
	atari800 -nobasic lorchatest.xex

#### cruft, from when I was planning to use a 32K cart:
# this was a blind alley: zlib is too slow to decompress, plus there's
# no need to compress taimain.xex since I'm able to use a 64K cart.

# gzip2deflate downloaded from https://github.com/pfusik/zlib6502
# I could have used deflator.c that ships with cc65's source, but
# it's deprecated by its own upstream (same author as gzip2deflate).
gzip2deflate: gzip2deflate.c
	$(HOSTCC) $(HOSTCFLAGS) -o gzip2deflate gzip2deflate.c

zlibtest.xex: gzip2deflate zlibtest.c zlibtestdata.s romable_taimain.raw
	gzip -9c < romable_taimain.raw | ./gzip2deflate > rom.dfl
	$(CC) -t atari -m zlibtest.map -l zlibtest.lst -Wl -D__SYSTEM_CHECK__=1 --start-addr 0x7000 -o zlibtest.xex zlibtest.c zlibtestdata.s
romable_taimain.xex: $(TAIMAIN_C_SRC) $(TAIMAIN_ASM_SRC) $(TAIMAIN_HDRS)
	rm -f taimain.xex
	$(MAKE) TAIMAIN_ADDR=0x3ff EXTRACFLAGS="-DCART_TARGET=1 --asm-define CART_TARGET=1"
	mv taimain.xex romable_taimain.xex

###

soundtest: sounds.c
	$(CC) -DTESTXEX -t atari -o sounds.xex sounds.c
	atari800 -nobasic sounds.xex

# former textmode title screen, was generated by TITLE.LST. Replaced
# by graphical title screen.
#title.xex: TITLE.DAT
#	$(PERL) title.pl TITLE.DAT > title.xex

# old title
#$(XEX): taimain.xex taifont.xex title.xex
#	cat taifont.xex title.xex taimain.xex > $(XEX)