Copyright © 2020-2022 by Víctor Parada
Current version: 1.7 (2022-02-02)
Every time I needed to review the file structure of an Atari 8-bit binary file (EXE, COM or OBJ, all of them known today as XEX), I had to write and/or customize a special and personalized tool to display the required information in a simple way (other than to use a sector editor or an hex editor). More tools were needed to modify them or to manipulate to create new binary files in the way I wanted.
In the 80's I did it in Atari BASIC (with some USR routines). I created a whole toolkit of utilities to perform very different tasks, and I had to run each of the required ones when I needed to do many operations in a sequence to get a final EXE binary file.
During the recent years, I got fun time rewriting some of those old tools in perl language, in order to work over XEX files directly on my modern computer instead of having to setup an ATR file and run my original tools in an emulator, which takes too much extra time. These new tools became more flexible than their respective original ones in BASIC, and they allowed me to add more features and to include many steps in a single run.
This year (2020), as most of the tools were similar in its internals (some scripts were modifications of previous ones), I decided to merge them in a single bigger tool, in order to allow me to perfom many different tasks at the same time. Then, this XEX Filter toolkit was born.
A8 binary files contains programs and/or data in a special file structure: a sequence of data blocks. These blocks specify where in memory to load, and it might be an initialization and/or a run vector.
Each block has the following structure:
Position | Content | Description |
---|---|---|
1-2 | Binary header | Contains the value $FFFF. This must be present in the first segment, and it is optional for the next ones. |
3-4 | Initial load address | Pointer (A) to the memory address where the data of this block will be loaded. |
5-6 | Last load address | Pointer (B) to the memory address for the last byte of this block. |
7-x | Data | Sequence of (B-A+1) bytes to be loaded into memory. |
There are two special memory locations that perform different actions when data is loaded there:
If none of these memory adresses are loaded, it depends on the loader or DOS if the control is transfered to the loaded data, usually the start address of the first or the last block.
XEX Filter is a tool that reads one or more XEX files, analyses and shows their binary structure, and optionally creates a new XEX file applying one or more filters to manipulate the file structure and it contents. With these filters it is possible to extract a binary block with its address headers from the file, remove unwanted blocks from it (like a splash screen that is displayed during the load), split or join blocks in a file, split or join files, change the loading order of the blocks, extract portions of the file as raw data (images or charsets to be edited), assign a new load address to a given block, etc.
Help screen is as follows:
xex-filter version 1.7 (2022-02-02) Copyright (c) 2020-2022 by Victor Parada <https://www.vitoco.cl/atari/xex-filter/> Usage: xex-filter.pl [-option]... [--] FILE1.XEX [FILE2.XEX]... Options: -i n1,n2-n3,... List of indexes for blocks selection -x n1,n2-n3,... List of indexes for blocks exclusion -s a1,a2-a3,... List of memory addresses where to split blocks -e a1,a2-a3,... List of memory addresses for data extraction -a a1,a2-a3,... New address list to assign to output blocks -v a1=v1,v2,... Append blocks with byte value at $0000 or address -w a1=v1,v2,... Append blocks with word value at RUNAD or address -z max Max number of bytes to fill between blocks -r Removes zeros if there are more than 4 in a row -d Writes data without address pointers or header -b Reads input as blocks up to 64K of data at $0000 -f Fix corrupt files by filling or discarding data -m Makes a memory map with the selected blocks -o NEW.XEX Output file (mandatory for -i -x -s -e -a -z -r -d and -m) Addresses between 0 and 65535 or in $hhhh format. Use "-" for a range.
The structure of each input file is displayed during the load of them, and a sequence number is assigned to each of the identified blocks. Then, a new file is created applying one or more filters to the selected blocks.
The output file will always have a leading $FFFF word, and any other instance of it in the middle of the input files will be ignored and removed.
Both decimal and hexadecimal numbers are used to display input and output file structures. If a block start address has a label assigned to it, that label will be displayed in the block description.
Some filter options requires a list of values, and values may be specified using decimal or hexadecimal notation, or using one of the defined labels.
Currently defined labels are:
RUNAD=$2E0 SDLSTL=560 CH1=754 INITAD=$2E2 COLOR0=708 CHBAS=756 PORTB=$D301 COLOR1=709 CH=764 BOOT=9 COLOR2=710 BASICF=1016 DOSVEC=10 COLOR3=711 BANK=$4000 SOUNDR=65 COLOR4=712 CARTB=$8000 RAMTOP=106 MEMTOP=$2E5 CARTA=$A000 COLDST=580 MEMLO=$2E7 ATRACT=77 SDMCTL=559
This tool will abort with an error message if an undefined label is found in an option.
Some options change the behavior on how input XEX files are read, and some options modifies the way the blocks are written to the new XEX file. Some options might be used in conjunction and some of them cannot be used at the same time, having to run the tool in steps if required.
Notes about options:
When no options are specified, the file structure is displayed for all the input files.
Specifies a filename for a new XEX file to be created. This option is required for many other options. Warning: If the file already exists, it will be overwritten!
Examples: 2 3 4 5 6 7 8 9 10 11
Allows the selection of the required blocks from the input file(s) to be sent to the output file, probably altered by any other option. If this option is not specified, all the blocks from the input files will be included in the same original order. The list of selected blocks must be specified as integer decimal numbers separated by comma, and the specified order is relevant because that will be the order to be written in the output file. Ranges are allowed by using a hyphen between two numbers. Decreasing order is allowed for any of the specified ranges.
You can specify the blocks from the input file(s) that you don't want to be in the output file. This is the complement to the -i option and both option cannot be used in the same command line. The blocks order is not relevant, as the blocks not excluded will be written in the same order they are read from the input file. The list of excluded blocks must be specified as integer decimal numbers separated by comma. Ranges are allowed by using a hyphen between two numbers. Decreasing order is allowed for any of the specified ranges, but it is not relevant in the result.
Examples: 3
List of addresses where blocks had to be splitted. With this option, if a selected block contains a byte located at any address from the list, the block will be split there, with that byte being the first one of the next block. If a range of address is specified, two splits should be done, resulting in a isolated block with those address in the header (if both bytes were in the same block from the input file) between the reminders of the originally selected block.
Specifies a list of new loading memory addresses, one for each block selected for output, which must be in the same order, and addresses for all blocks must be provided. If a range of addresses is specified for a block, an implicit split will happen if the block was larger than the range, and a fill with zeros will happen to complete the range if the block was shorter.
Simulates the load into memory of the selected blocks from the input files and creates new blocks from the specified ranges. A single memory address in the list identifies a starting byte for a block, which will end at the beginning of the next block in the list, or at $FFFF if there are no more blocks in the list.
Examples: 7
Dumps a 64K memory map block which will be the result of the "load" of the selected blocks from the input files. The block will be initialized with zeros. The output is the same than "-e0".
Examples: 8
Includes a block that contains a word value (two bytes) to be loaded at the specified address, in low-high order. It can be used to load a vector into any system pointer. The list can contain many values separated by comma, and the load address for each of them may be included in the form "addr=val". If no address is specified, it continues with the next address from the previous value of the list. The default address for the first value is RUNAD ($02E0 or 736).
Examples: 9
Includes a block that contains a byte value to be loaded at the specified address. It can be used to modify any system register during a load. The list can contain many values separated by comma, and the load address for each of them may be included in the form "addr=val". If no address is specified for a value, it continues with the next address from the previous value of the list. The default address for the first value is 0.
Merges consecutive blocks, filling with up to the specified number of bytes with the value of zero (null byte). If more bytes are required to merge two blocks, neither the merge nor the fill will be done there. Specify "-z0" to merge only immediately consecutive blocks, where no filling is required.
Removes sequences of null bytes longer than 4 bytes from the blocks, splitting them if required. This is used to reduce the size of the XEX file under the asumption that the memory locations for those omitted bytes were clean before the load. If a block only has null bytes, the whole block will be removed. Null bytes at the beginning or the end of a block will also be removed, even when they are less than four per side. It does not removes zeros from blocks shorter than 3 bytes, because those usually are NOT simple data, but a vector or a value for some system register.
Examples: 6
Do not write address pointers to the output file, just the data. Be aware that if more than one block have to be written, all the data will be concatenated without any filler.
Examples: 8
This is usefull to add load address information from input files that only contains raw data, in order to create blocks for XEX files. If the input file already has address information, that will be considered as part of the data. Every input file is asigned to a different block with address 0. If the length of a file is more than 64K, it will be splitted into many blocks. To assign any load address to a raw data block, use "-a" option.
Examples: 8
Allow some common errors in XEX binary files to be ignored: file without $FFFF headers, truncated files and gargabe at the end. Truncated blocks are filled with null bytes instead of to adjust the end address.
Examples: 10
This example simply shows the binary structure of an XEX file. There is no output XEX file.
> xex-filter.pl GAME.XEX Analyzing "GAME.XEX"... -: 65535 [$FFFF] BINHEAD 1: 8192-9029 [$2000-$2345] (838) <- CODE/DATA 2: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA 3: 736-737 [$02E0-$02E1] (2) RUNAD -> 8192 [$2000]
To insert a splash screen to be displayed while loading a long game, just concatenate both files into a new one.
> xex-filter.pl -o NEWGAME.XEX SPLASH.XEX GAME.XEX Analyzing "SPLASH.XEX"... -: 65535 [$FFFF] BINHEAD 1: 1536-1791 [$0600-$06FF] (256) <- CODE/DATA 2: 738-739 [$02E2-$02E3] (2) INITAD -> 1536 [$0600] Analyzing "GAME.XEX"... -: 65535 [$FFFF] BINHEAD 3: 8192-9029 [$2000-$2345] (838) <- CODE/DATA 4: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA 5: 736-737 [$02E0-$02E1] (2) RUNAD -> 8192 [$2000] Writing "NEWGAME.XEX"... -: 65535 [$FFFF] BINHEAD 1: 1536-1791 [$0600-$06FF] (256) <- CODE/DATA 2: 738-739 [$02E2-$02E3] (2) INITAD -> 1536 [$0600] 3: 8192-9029 [$2000-$2345] (838) <- CODE/DATA 4: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA 5: 736-737 [$02E0-$02E1] (2) RUNAD -> 8192 [$2000] 17504 bytes written
To remove unwanted blocks from a file, just list them using -x.
> xex-filter.pl -o GAMEONLY.XEX -x 1,2 NEWGAME.XEX Analyzing "NEWGAME.XEX"... -: 65535 [$FFFF] BINHEAD 1: 1536-1791 [$0600-$06FF] (256) <- CODE/DATA 2: 738-739 [$02E2-$02E3] (2) INITAD -> 1536 [$0600] 3: 8192-9029 [$2000-$2345] (838) <- CODE/DATA 4: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA 5: 736-737 [$02E0-$02E1] (2) RUNAD -> 8192 [$2000] Writing "GAMEONLY.XEX"... -: 65535 [$FFFF] BINHEAD 1: 8192-9029 [$2000-$2345] (838) <- CODE/DATA 2: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA 3: 736-737 [$02E0-$02E1] (2) RUNAD -> 8192 [$2000] 17238 bytes written
Other way to get the same result is to select only the required ones to be included in the new XEX file with -i. In this case, a range of blocks is selected.
> xex-filter.pl -o GAMEONLY.XEX -i 3-5 NEWGAME.XEX Analyzing "NEWGAME.XEX"... -: 65535 [$FFFF] BINHEAD 1: 1536-1791 [$0600-$06FF] (256) <- CODE/DATA 2: 738-739 [$02E2-$02E3] (2) INITAD -> 1536 [$0600] 3: 8192-9029 [$2000-$2345] (838) <- CODE/DATA 4: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA 5: 736-737 [$02E0-$02E1] (2) RUNAD -> 8192 [$2000] Writing "GAMEONLY.XEX"... -: 65535 [$FFFF] BINHEAD 1: 8192-9029 [$2000-$2345] (838) <- CODE/DATA 2: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA 3: 736-737 [$02E0-$02E1] (2) RUNAD -> 8192 [$2000] 17238 bytes written
This command line splits a block at a given address. The resulting blocks will have their own address pointers.
> xex-filter.pl -o SPLITTED.XEX -s $6000 GAME.XEX Analyzing "GAME.XEX"... -: 65535 [$FFFF] BINHEAD 1: 8192-9029 [$2000-$2345] (838) <- CODE/DATA 2: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA 3: 736-737 [$02E0-$02E1] (2) RUNAD -> 8192 [$2000] Writing "SPLITTED.XEX"... -: 65535 [$FFFF] BINHEAD 1: 8192-9029 [$2000-$2345] (838) <- CODE/DATA 2: 16384-24575 [$4000-$5FFF] (8192) BANK <- CODE/DATA 3: 24576-32767 [$6000-$7FFF] (8192) <- CODE/DATA 4: 736-737 [$02E0-$02E1] (2) RUNAD -> 8192 [$2000] 17242 bytes written
To join two consecutive block, specify a zero bytes fill.
> xex-filter.pl -o JOINED.XEX -z 0 SPLITTED.XEX Analyzing "SPLITTED.XEX"... -: 65535 [$FFFF] BINHEAD 1: 8192-9029 [$2000-$2345] (838) <- CODE/DATA 2: 16384-24575 [$4000-$5FFF] (8192) BANK <- CODE/DATA 3: 24576-32767 [$6000-$7FFF] (8192) <- CODE/DATA 4: 736-737 [$02E0-$02E1] (2) RUNAD -> 8192 [$2000] Writing "JOINED.XEX"... -: 65535 [$FFFF] BINHEAD 1: 8192-9029 [$2000-$2345] (838) <- CODE/DATA 2: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA 3: 736-737 [$02E0-$02E1] (2) RUNAD -> 8192 [$2000] 17238 bytes written
To shrink an XEX file, it is possible to remove rows of the byte zero from within the blocks. Be careful, if memory of the Atari is not clean at the loading addresses range, the squeezed XEX will fail when run.
> xex-filter.pl -o SQUEEZED.XEX -r GAME.XEX
Analyzing "GAME.XEX"...
-: 65535 [$FFFF] BINHEAD
1: 8192-9029 [$2000-$2345] (838) <- CODE/DATA
2: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA
3: 736-737 [$02E0-$02E1] (2) RUNAD -> 8192 [$2000]
Writing "SQUEEZED.XEX"...
-: 65535 [$FFFF] BINHEAD
1: 8192-8198 [$2000-$2006] (7) <- CODE/DATA
2: 8254-8275 [$203E-$2053] (22) <- CODE/DATA
3: 8292-8313 [$2064-$2079] (22) <- CODE/DATA
{removed}
81: 32677-32684 [$7FA5-$7FAC] (8) <- CODE/DATA
82: 32694-32697 [$7FB6-$7FB9] (4) <- CODE/DATA
83: 32754-32756 [$7FF2-$7FF4] (3) <- CODE/DATA
84: 736-737 [$02E0-$02E1] (2) RUNAD -> 8192 [$2000]
14170 bytes written
A squeezed XEX file has many blocks with non-zero data and can be loaded into clean memory to get the full program and run it. When Atari's memory was previously used by another program, it is better to load a single block with its zeros included. The following command inserts up to 250 null bytes between blocks.
> xex-filter.pl -o RECOVERED.XEX -z 250 SQUEEZED.XEX
Analyzing "SQUEEZED.XEX"...
-: 65535 [$FFFF] BINHEAD
1: 8192-8198 [$2000-$2006] (7) <- CODE/DATA
2: 8254-8275 [$203E-$2053] (22) <- CODE/DATA
3: 8292-8313 [$2064-$2079] (22) <- CODE/DATA
{removed}
81: 32677-32684 [$7FA5-$7FAC] (8) <- CODE/DATA
82: 32694-32697 [$7FB6-$7FB9] (4) <- CODE/DATA
83: 32754-32756 [$7FF2-$7FF4] (3) <- CODE/DATA
84: 736-737 [$02E0-$02E1] (2) RUNAD -> 8192 [$2000]
Writing "RECOVERED.XEX"...
-: 65535 [$FFFF] BINHEAD
1: 8192-9027 [$2000-$2343] (836) <- CODE/DATA
2: 16384-32756 [$4000-$7FF4] (16373) BANK <- CODE/DATA
3: 736-737 [$02E0-$02E1] (2) RUNAD -> 8192 [$2000]
17225 bytes written
A filler of up to 250 bytes was enough to recover de file, but note that both main blocks are 2 bytes shorter than before. If we didn't know that, probably the new XEX would be fine, but if we really want to fix them, we have different ways to do that:
Method 1: Simulate a load and extract the same blocks with the right length:
> xex-filter.pl -o RECOVERED-1.XEX -e $2000-$2345,$4000-$7FFF,736-737 RECOVERED.XEX nalyzing "RECOVERED.XEX"... -: 65535 [$FFFF] BINHEAD 1: 8192-9027 [$2000-$2343] (836) <- CODE/DATA 2: 16384-32756 [$4000-$7FF4] (16373) BANK <- CODE/DATA 3: 736-737 [$02E0-$02E1] (2) RUNAD -> 8192 [$2000] Writing "RECOVERED-1.XEX"... -: 65535 [$FFFF] BINHEAD 1: 8192-9029 [$2000-$2345] (838) <- CODE/DATA 2: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA 3: 736-737 [$02E0-$02E1] (2) RUNAD -> 8192 [$2000] 17238 bytes written
Method 2: Include bytes at the original memory location of each block's last byte, then change the sequence of the blocks and fill with enough zeros to join the corresponding sub-blocks:
> xex-filter.pl -o RECOVERED-2.XEX -v $2345=0,$7FFF=0 -i 1,4,2,5,3 -z 100 RECOVERED.XEX Analyzing "RECOVERED.XEX"... -: 65535 [$FFFF] BINHEAD 1: 8192-9027 [$2000-$2343] (836) <- CODE/DATA 2: 16384-32756 [$4000-$7FF4] (16373) BANK <- CODE/DATA 3: 736-737 [$02E0-$02E1] (2) RUNAD -> 8192 [$2000] Adding values... 4: 9029-9029 [$2345-$2345] (1) = 0 [$00] '00000000' 5: 32767-32767 [$7FFF-$7FFF] (1) = 0 [$00] '00000000' Writing "RECOVERED-2.XEX"... -: 65535 [$FFFF] BINHEAD 1: 8192-9029 [$2000-$2345] (838) <- CODE/DATA 2: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA 3: 736-737 [$02E0-$02E1] (2) RUNAD -> 8192 [$2000] 17238 bytes written
To change the characters or sprites of a game, you can use any character editor if you give it the right file. If the address where the charset resides inside an XEX has not been identified yet, it is possible to map the whole XEX into a 64K block and search for the bitmaps inside it.
> xex-filter.pl -o MAP.DAT -m -d GAME.XEX Analyzing "GAME.XEX"... -: 65535 [$FFFF] BINHEAD 1: 8192-9029 [$2000-$2345] (838) <- CODE/DATA 2: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA 3: 736-737 [$02E0-$02E1] (2) RUNAD -> 8192 [$2000] Writing "MAP.DAT"... 1: (65536) 65536 bytes written
In this example, the charset was identified to be at $5400, so you can extract it and then insert it again after customization. The extraction could be easyly done using "-e $5400-$5BFF" option, but it won't be easy to insert it back if the XEX file is not previously prepared, so the first step is to create a 2K block inside the file by splitting at that addresses:
> xex-filter.pl -o GAMECHAR.XEX -s $5400-$5BFF GAME.XEX Analyzing "GAME.XEX"... -: 65535 [$FFFF] BINHEAD 1: 8192-9029 [$2000-$2345] (838) <- CODE/DATA 2: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA 3: 736-737 [$02E0-$02E1] (2) RUNAD -> 8192 [$2000] Writing "GAMECHAR.XEX"... -: 65535 [$FFFF] BINHEAD 1: 8192-9029 [$2000-$2345] (838) <- CODE/DATA 2: 16384-21503 [$4000-$53FF] (5120) BANK <- CODE/DATA 3: 21504-23551 [$5400-$5BFF] (2048) <- CODE/DATA 4: 23552-32767 [$5C00-$7FFF] (9216) <- CODE/DATA 5: 736-737 [$02E0-$02E1] (2) RUNAD -> 8192 [$2000] 17246 bytes written
The second step is to extract the 2K block and save it without binary headers:
> xex-filter.pl -o CHARSET.FNT -i 3 -d GAMECHAR.XEX Analyzing "GAMECHAR.XEX"... -: 65535 [$FFFF] BINHEAD 1: 8192-9029 [$2000-$2345] (838) <- CODE/DATA 2: 16384-21503 [$4000-$53FF] (5120) BANK <- CODE/DATA 3: 21504-23551 [$5400-$5BFF] (2048) <- CODE/DATA 4: 23552-32767 [$5C00-$7FFF] (9216) <- CODE/DATA 5: 736-737 [$02E0-$02E1] (2) RUNAD -> 8192 [$2000] Writing "CHARSET.FNT"... 1: (2048) 2048 bytes written
After the new charset has been saved by your favourite editor, it must be converted to XEX format:
> xex-filter.pl -o CHARSET.XEX -b -a $5400 CHARSET.FNT Analyzing "CHARSET.FNT"... 1: 0-2047 [$0000-$07FF] (2048) <- CODE/DATA Writing "CHARSET.XEX"... -: 65535 [$FFFF] BINHEAD 1: 21504-23551 [$5400-$5BFF] (2048) <- CODE/DATA 2054 bytes written
The last step is to merge the new charset into the game, replacing the original 3rd block with the new one:
> xex-filter.pl -o MYGAME.XEX -i 1-2,6,4-5 -z 0 GAMECHAR.XEX CHARSET.XEX Analyzing "GAMECHAR.XEX"... -: 65535 [$FFFF] BINHEAD 1: 8192-9029 [$2000-$2345] (838) <- CODE/DATA 2: 16384-21503 [$4000-$53FF] (5120) BANK <- CODE/DATA 3: 21504-23551 [$5400-$5BFF] (2048) <- CODE/DATA 4: 23552-32767 [$5C00-$7FFF] (9216) <- CODE/DATA 5: 736-737 [$02E0-$02E1] (2) RUNAD -> 8192 [$2000] Analyzing "CHARSET.XEX"... -: 65535 [$FFFF] BINHEAD 6: 21504-23551 [$5400-$5BFF] (2048) <- CODE/DATA Writing "MYGAME.XEX"... -: 65535 [$FFFF] BINHEAD 1: 8192-9029 [$2000-$2345] (838) <- CODE/DATA 2: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA 3: 736-737 [$02E0-$02E1] (2) RUNAD -> 8192 [$2000] 17238 bytes written
If a cartridge game was converted to an XEX, it is usually loaded at low memory and extra code is included to setup the game after the load and start it. It is possible to discard the setup routine and load the cartridge data directly in its original address, and this is how that could be set up.
The first step is to move the game data into a temporary file, and change the load address at the same time:
> xex-filter.pl -o CART.XEX -i 2 -a $8000 MYGAME.XEX Analyzing "MYGAME.XEX"... -: 65535 [$FFFF] BINHEAD 1: 8192-9029 [$2000-$2345] (838) <- CODE/DATA 2: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA 3: 736-737 [$02E0-$02E1] (2) RUNAD -> 8192 [$2000] Writing "CART.XEX"... -: 65535 [$FFFF] BINHEAD 1: 32768-49151 [$8000-$BFFF] (16384) CARTB <- CODE/DATA 16390 bytes written
As the cartridge data will be loaded on top of the display list and garbage could be seen, we can add a mini block that loads a zero over SDMCTL register (559 or $22F), turning off the screen. Also, the load will fail if BASIC ROM is enabled on XL/XE computers, so we can also disable it by writing a $B3 into PORTB ($D301 or 54017), and we must move RAMTOP (106 or $6A) just below the cartridge data. At last, a RUNAD must specified to call the cartridge initialization routine, which in this example is at $8000. All this steps can be done in a single command:
> xex-filter.pl -o CARTGAME.XEX -v 559=0,PORTB=$B3,106=$80 -w 8000 -i 2-4,1,5 CART.XEX Analyzing "CART.XEX"... -: 65535 [$FFFF] BINHEAD 1: 32768-49151 [$8000-$BFFF] (16384) CARTB <- CODE/DATA Adding values... 2: 559-559 [$022F-$022F] (1) SDMCTL = 0 [$00] '00000000' 3: 54017-54017 [$D301-$D301] (1) PORTB = 179 [$B3] '10110011' 4: 106-106 [$006A-$006A] (1) RAMTOP = 128 [$80] '10000000' Adding words... 5: 736-737 [$02E0-$02E1] (2) RUNAD -> 8000 [$1F40] Writing "CARTGAME.XEX"... -: 65535 [$FFFF] BINHEAD 1: 559-559 [$022F-$022F] (1) SDMCTL = 0 [$00] '00000000' 2: 54017-54017 [$D301-$D301] (1) PORTB = 179 [$B3] '10110011' 3: 106-106 [$006A-$006A] (1) RAMTOP = 128 [$80] '10000000' 4: 32768-49151 [$8000-$BFFF] (16384) CARTB <- CODE/DATA 5: 736-737 [$02E0-$02E1] (2) RUNAD -> 8000 [$1F40] 16411 bytes written
Some tape loaders were unable to detect the end of file from the tape chunks, as well as some boot disk loaders that only managed sequential sectors instead of a file system, so copiers added a couple of bytes (usually $FEFE or $FFFE) as a flag at the end of the stored XEX, and that the loader could identify to finish the loading loop and start the game. You can detect this situation by an error message during the XEX analysis:
> xex-filter.pl TAPEGAME.XEX Analyzing "TAPEGAME.XEX"... -: 65535 [$FFFF] BINHEAD 1: 8192-9029 [$2000-$2345] (838) <- CODE/DATA 2: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA 3: 736-737 [$02E0-$02E1] (2) RUNAD -> 8192 [$2000] ERROR - Extra pointers at end of file
In order to discard those extra bytes, create a new XEX using the following command:
> xex-filter.pl -o CLEANED.XEX -f TAPEGAME.XEX Analyzing "TAPEGAME.XEX"... -: 65535 [$FFFF] BINHEAD 1: 8192-9029 [$2000-$2345] (838) <- CODE/DATA 2: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA 3: 736-737 [$02E0-$02E1] (2) RUNAD -> 8192 [$2000] Writing "CLEANED.XEX"... -: 65535 [$FFFF] BINHEAD 1: 8192-9029 [$2000-$2345] (838) <- CODE/DATA 2: 16384-32767 [$4000-$7FFF] (16384) BANK <- CODE/DATA 3: 736-737 [$02E0-$02E1] (2) RUNAD -> 8192 [$2000] 17238 bytes written
If you have a squeezed XEX file, composed by a lot of short blocks, it is possible to make them load in reverse order, just for fun or to confuse curious people. "-i" option allows ranges from a high value to a lower one, and this is useful for this trick:
> xex-filter.pl -o REVERSED.XEX -i 83-1,84 SQUEEZED.XEX Analyzing "SQUEEZED.XEX"... -: 65535 [$FFFF] BINHEAD 1: 8192-8198 [$2000-$2006] (7) <- CODE/DATA 2: 8254-8275 [$203E-$2053] (22) <- CODE/DATA 3: 8292-8313 [$2064-$2079] (22) <- CODE/DATA {removed} 81: 32677-32684 [$7FA5-$7FAC] (8) <- CODE/DATA 82: 32694-32697 [$7FB6-$7FB9] (4) <- CODE/DATA 83: 32754-32756 [$7FF2-$7FF4] (3) <- CODE/DATA 84: 736-737 [$02E0-$02E1] (2) RUNAD -> 8192 [$2000] Writing "REVERSED.XEX"... -: 65535 [$FFFF] BINHEAD 1: 32754-32756 [$7FF2-$7FF4] (3) <- CODE/DATA 2: 32694-32697 [$7FB6-$7FB9] (4) <- CODE/DATA 3: 32677-32684 [$7FA5-$7FAC] (8) <- CODE/DATA {removed} 81: 8292-8313 [$2064-$2079] (22) <- CODE/DATA 82: 8254-8275 [$203E-$2053] (22) <- CODE/DATA 83: 8192-8198 [$2000-$2006] (7) <- CODE/DATA 84: 736-737 [$02E0-$02E1] (2) RUNAD -> 8192 [$2000] 14170 bytes written
XEX Filter version 1.7 (2022-02-02)
To run this tool you need to have the perl interpreter without any special module in your computer. It might be a linux box (perl is native there), Windows (with Strawberry Perl or ActivePerl) or Mac. It doesn't have a GUI, it just works in the command line (shell), using unix-like options.
Outside Windows, it might be needed to change the shebang (first line of the perl script) to set the path to the perl 5 interpreter (like "#!/usr/bin/perl" or "#!/usr/local/bin/perl").
I'd like to receive bug reports and suggestions to improve this tool. Help requests or any comment about it are welcome too. Please post them in the "XEX Filter" thread at AtariAge forum or PM me through the forum.