Doing an HTTP GET with Atari Basic with mode 12.

[Developers] When Doing HTTP GET, use Mode 12 !

TL:DR – When opening an HTTP GET connection, use mode = 12. Mode = 4 now does URL encoding of the path when it’s passed in, and will probably not be what you want.

While Mode = 4 is also an HTTP GET, it will attempt to resolve filenames and encode them so that they can be safely passed to and from a web server. It is intended for accessing plain files on HTTP and WebDAV servers.

So if you’re doing this in ATARI BASIC:

Change it to this:

For cross-platform C programs written in fujinet-lib, use the HTTP_GET_H constant in nopen():

network_open(buf, OPEN_MODE_HTTP_GET_H, OPEN_TRANS_NONE);

While Apple2 users using the AppleSoft BASIC extension should do:

& NOPEN 0, 12,0, "N:HTTPS://APPS.IRATA.ONLINE/Homesoft/?query=1234"

The Change, in Detail

Web servers, and in fact the standard that dictates the format of a URL give special meaning to characters like ? and &, as well as disallowing spaces. If these characters are to be used as part of a file name, they must be encoded so that the web server can pass them through literally.

On the ATARI, at least. You have the N: device. It can open any type of network connection, including to a networked filesystem. This can be used to access files stored on web servers. The encoding of filenames can break the seamless transparency expected, so some additional processing now happens in the FujiNet firmware to encode filenames so that web servers will treat them literally.

For example, with the latest change, this command works as expected:

On Open, the HTTP protocol adapter executes this piece of code here: https://github.com/FujiNetWIFI/fujinet-firmware/blob/master/lib/network-protocol/HTTP.cpp#L322

    if (aux1_open == 4 || aux1_open == 8)
{
// We are opening a file, URL encode the path.
std::string encoded = mstr::urlEncode(url->path);
url->path = encoded;
url->rebuildUrl();
}

return !client->begin(url->url);
}

Which transforms the above entered URL into:

Atari_8-bit%2FGames%2FHomesoft%2FFrogger%20%28Parker%20Brothers%29.xex

…before opening, so the web server is happy.

It does, however, make a bit of a mess when dealing with query parameters, as characters like ? get encoded, and the web server treats them literally as part of the path, rather than as the special query character:

Atari_8-bit/Games/Homesoft/F/Frogger/Frogger (Parker Brothers).xex?query=foo

gets the query part mangled into:

%2FAtari_8-bit%2FGames%2FHomesoft%2FFrogger%20%28Parker%20Brothers%29.xex%3Fquery%3Dfoo

…a mess.

This is why if you’re utilizing some web address that does an HTTP GET, and accepts query parameters, to please use mode 12, which does not do any transformation of the input, and passes it to the web server, unmolested.

Showing the NCD and NDIR from the N tools in SpartaDOS X

[ATARI] Why the N tools?

FujiNet exposes several devices to your ATARI computer. One of them is the network device, which can access local or Internet network resources. While we provide a handler that adds an N: device, certain programs (such as the DUP.SYS in ATARI DOS 2.0) write over the handler when they load into memory, making the N: device unusable within DUP.

To work around this, a series of tools exist which access the FujiNet network device directly via SIO, thus do not require the presence of the N: handler (also called NDEV.COM).

Each tool has been tested in every possible DOS, and have been made to adapt accordingly. Since SpartaDOS, OS/A+ / DOS XL, and XDOS all support command line parameters, the tools automatically fetch parameters if they are present. The tools fall back to interactive mode if no parameters are given on the command line.

Each tool (with the exception of NCOPY) has been written to assume that no use of N: means to refer to N1:. There are 8 such N[x]: devices, numbered N1: to N8:. Both the N: handler (NDEV.COM) and the N tools refer to the network devices in this same way.

Furthermore, the companion disk, fnc-tools-doc.atr contains documentation for each tool mentioned below.

The Tools

The tools themselves are not only present on the fnc-tools.atr disk on the apps.irata.online TNFS server, they are also present on each and every disk in the DOS folder on the same server. They are called the N tools, because they all begin with the letter ‘N‘:

  • NCD. Changes the directory pointed to by the selected N[x]: device. It’s useful so you don’t have to type the full path to a URL, each and every time you refer to an N[x]: device. So you can type N:FROG.EXE instead of N:FTP://ftp.pigwa.net/stuff/collections/holmes cd/Holmes 2/Atari Archives/Antic Files/88/FROG.EXE each time! In other words, this tool affects all of the other N tools, as well as the N: device.
  • NCOPY. Copies files. Takes a source, and a destination. Either source or destination can be a local device (D:, E:, P:, whatever.), or the network (N[x]:). It supports wildcards.
  • NDEL. Deletes a file from the network.
  • NDEV. Loads the N: handler into memory. Can be AUTORUN.
  • NDIR. Lists the directory of a network location. It asks the FujiNet for a long directory listing, which preserves case, spacing, and wraps the output every 31 characters. File size is shown approximated to bytes, kilobytes, or megabytes, as needed.
  • NLOAD. Loads and runs any standard ATARI binary load file, directly from any network endpoint.
  • NLOGIN. Sets login and password credentials for network protocols that need them, such as SMB.
  • NMKDIR. Tells a supported network protocol to create a new directory at the given path.
  • NPWD. Prints the current directory pointed to by the N[x]: device.
  • NREN. Renames an existing file, to a new name, separated by a comma. This is analogous to a move on some protocols.
  • NRMDIR. Removes a directory pointed to by the N[x]: device. Depending on the protocol, the directory may need to be empty beforehand.
  • NTRANS. Sets the translation mode for a given N[x]:, allowing you to convert text files to and from ATASCII into a compatible ASCII format, and back again. Setting a value of 0 turns off translation.

Relationship to NDEV.COM and the N: Handler

Both NDEV.COM and the N tools use the same network device on the FujiNet, in exactly the same way.

The N tools do not require NDEV.COM to be loaded, but the N tools help navigate network file systems, because the DOS you may be using may not know how to do so. NDEV.COM and the N tools work hand-in-hand to provide a transparent way to access network resources.

But the N: handler has some shortcomings:

  • ATARI DOS 2, and DOS 2.5’s Disk Utility Package (DUP.SYS) is loaded into a fixed location in memory, which happens to overwrite where NDEV.COM loads, obliterating the N: handler. In addition, MEM.SAV must be enabled if you want to go back and forth between the DUP and another program, so that the handler can be restored on exit from the DUP. The N tools provide a sensible alternative to access the same network resources even though the handler can’t be used.
  • MyDOS does not obliterate NDEV.COM in memory, so it can be used in its Disk Utility Package, so you can list directories and manipulate files using the usual commands. However, since the BINARY LOAD command has been moved to DOS.SYS, it is only accessible via the “D:” device, and calls it via CIO call #39, which NDEV.COM does not implement yet. The N tools provide NLOAD.COM, which can BINARY LOAD from the network.
  • SpartaDOS (disk and X) has the same BINARY LOAD issue as MyDOS. In addition, SpartaDOS X’s command processor thinks of N: as “DN:” instead of device N:. While there will be a SpartaDOS driver for FujiNet’s network device in the future, the N tools can be used to access the network device from the SpartaDOS X command processor. (Disk based SpartaDOS can actually use N: directly, with some quirks.)
  • NDEV.COM does not yet implement burst mode (anyone want to help implement it?)

Demo Video

Here is a demo video showing why the N tools are important. Chapter Index below.

https://youtu.be/BUR_KRTRWk0
Demo video showing Why the N tools are important.

Chapter Index for Video

00:00 The DOS folder
01:10 What happens when you try to use N: in DOS 2.0 DUP?
04:40 DOS 2.0…What happens when you try to go back to BASIC without MEM.SAV?
05:20 Creating MEM.SAV so we can preserve the handler for BASIC and other applications.
07:45 The N tools
10:20 using NCOPY to copy from the network, to a floppy.
12:00 using NCOPY to copy from a floppy, to the network.
14:30 using NLOAD to load Cyclod on DOS 2.
15:30 Comparing to MYDOS 4.53
16:40 comparing Viewing directory via the N: device
16:58 comparing Copying via the N: device
17:55 Whoops! can’t load a binary file in MYDOS via the N: device.
19:08 Loading binary file with NLOAD.
19:55 Using N tools in SpartaDOS X

BURST MODE

Hi guys, reaching out for anyone who may be able to help implement a feature missing from the N: handler, “burst mode”

Specifically, when more than 1 byte is requested by an IOCB, set up a read for that many bytes directly into the buffer specified by the IOCB.

The nice thing is, that an intermediate buffer will no longer be required (it’s only required for disk because of the fact that you must read at least a whole sector’s worth of data at a time), however on the N: device, you use DAUX1/DAUX2 in tandem with DBYT to specify how many bytes to read at a time, with DBUF pointing to a target buffer.

I did implement this in an early draft of the NDEV handler 5 years ago, but was unable to fully debug it.

If you can help, the current code is here: https://github.com/FujiNetWIFI/fujinet-nhandler/blob/master/handler/src/ndev.s

CIO GET: https://github.com/FujiNetWIFI/fujinet-nhandler/blob/master/handler/src/ndev.s#L224

CIO PUT: https://github.com/FujiNetWIFI/fujinet-nhandler/blob/master/handler/src/ndev.s#L431

The above two routines are currently built to use a 128 byte buffer RBUF, and an index RLEN,X, but with burst they would hopefully be simpler.

Programming for the IBM PC FujiNet

The IBM PC FujiNet is being developed to use several physical interfaces. While initially we will be providing an RS-232-C version, we also want to do versions that work over parallel port, as well as ISA interfaces, including the sidecar ISA on the IBM PCjr. Because it’s not desirable that specific versions of FujiNet programs would need to be written or compiled for these specific interfaces, it was decided to implement a FujiNet BIOS interface that hooks into software interrupt (INT) F5.

MOV AH,00
MOV AL,70
MOV CL,FF
INT F5

This interface is loaded into memory via the FUJINET.SYS driver placed in CONFIG.SYS. This device driver not only provides the MS-DOS interface to FujiNet’s virtual disk drives giving each drive slot its own drive letter, it also provides the INT F5 service.

DEVICE=FUJINET.SYS FUJI_BPS=9600 FUJI_PORT=2

Once the driver is loaded, commands can be sent to the FujiNet via the INT F5 trap, such as this snippet of assembler to send a RESET to the FujiNet:

MOV AH,00                ; Command type: No Payload
MOV AL,70 ; send to FUJI sub-device
MOV CL,FF ; Send Reset command ($FF)
INT F5 ; Do it. Return value is 'C' in AL
MOV AH,4C ; Select Return to DOS command
INT 21 ; Execute DOS service, Exit.

More information can be found about this programming interface, in our wiki:

https://github.com/FujiNetWIFI/fujinet-firmware/wiki/MS%E2%80%90DOS-BIOS-Specification

For ATARI users: How to create your own SpartaDOS X Cartridge + FujiNet Tools (using SIDE3 as an example.)

The following YouTube video shows how to create your own SpartaDOS X cartridge with the FujiNet tools, from scratch, using the SpartaDOS X Imager (SDXImager) tool.

For this video, I installed a fresh copy of Ubuntu Linux Desktop using default options, and added the following packages via a terminal:

sudo apt install build-essential git wine

Once everything is built, you can transfer it to the nearest FujiNet via WebDAV, by selecting Network in Files, and Connecting to the address of your FujiNet:

DAV://192.168.1.21/dav/

Links Used in the video

Ubuntu Linux: https://ubuntu.com/download/desktop

CC65 Compiler: https://github.com/cc65/cc65

MAD Assembler: https://github.com/tebe6502/Mad-Assembler

FujiNet Tools: https://github.com/FujiNetWIFI/fujinet-tools

FujiNet N: Handler: https://github.com/FujiNetWIFI/fujinet-nhandler

Altirra Emulator: https://www.virtualdub.org/altirra.html

ATARI: SpartaDOS X 4.50 ROM for Ultimate 1MB with FujiNet tools.

The FNCTOOLS-U1MB-ROM.ATR disk image on apps.irata.online, which provides a ROM of SpartaDOS X for Ultimate 1MB users, that includes the FujiNet tools, has been updated to SpartaDOS 4.50, and is available on the apps.irata.online server. The UFLASH tool is also provided on the disk.

Three versions of the ROM are provided:

  • 192K, which contains most of the tools.
  • 256K, which contains all of the tools.
  • 320K, which contains all of the tools, as well as their corresponding MAN pages.

FujiNet Virtual Printer Examples

FujiNet has a virtual printer that is exposed to the host computer. This printer simulates a variety of vintage printers, accepting the commands specific to the desired printer and rendering the result as a PDF file that you can download from the web interface and print on a modern printer.

The following example PDFs were printed from various applications, showing the type of output you can expect from FujiNet’s virtual printer. For each of these emulations, Jeff Piepmeier emulated not only the behavior of the printer, but also painstakingly recreated the character set and rendering characteristics of each printer.

ATARI 820

The Atari 820 was the first announced printer for the 400 and 800 series computers in 1979. It was based on an Eaton 7000 printer mechanism used in field logging printers, and its carriage had a 40 character width. It was very unusual in that it could also print on the horizontal axis of the paper, which limited its potential width to 28 characters. It was also designed to be the same width as the Atari 810 and 815 disk drives, allowing it to be stacked on top.

Atari 820 Drive... or is it? - Atari 8-Bit Computers - AtariAge Forums

ATARI 822

The ATARI 822 was a silent thermal printer that ATARI licensed from Trendcom, a variation of their model 100. It too had a carriage that could print 40 columns across. It required specially treated thermal paper to operate, which you can still purchase, as the type and size of paper are still used in some fax machines today.

Atari 822 Thermal Printer - Printer - Computing History

The 80 column variant of this unit, the Trendcom 200 was used as the mechanism for the Apple Silentype printer.

apple: 1980 "silentype" printer

These printers could not only print text, but they also could emit bitmapped graphics. The Atari 822 has a horizontal line resolution of 480 pixels.

ATARI 825

For the 825, ATARI licensed the Centronics 737 as their high-end printer for the 400 and 800 series computers. It had a carriage capable of printing 80 column text, and had advanced features such as multiple fonts, and the ability to handle vertical tabs, which moved the printer page upward and allowed for multi column printing. Its 80 column width also made it usable for printing program listings. (picture courtesy of AtariMania.com)

All of the dot-matrix printers shown here worked with either friction fed roll paper, or with tractor-fed paper. The FujiNet will seperate each page on the PDF, so that it can be printed on modern printers.

Because Centronics did not license the bare mechanism to ATARI, and insisted that it be sold as is, the 825 required the use of the Atari 850 interface module’s parallel port. The FujiNet implementation has no such requirement.

Atari 400 800 XL XE catalog - Atari - 1981 - English

ATARI 1020

The Atari 1020 was a very small pen plotter licensed from Alps Microelectronics. Its pen holder could select between four colored pens, and the firmware on the printer could accept both text (which it would render caligraphically, and the graphics commands to move the pen across the paper to draw lines and shapes.

Atari 1020 - Wikipedia

The 1020 emulation is unique in that it renders directly to SVG.

ATARI 1025

For the 1025, Atari licensed the Oki Microline 80. It was intended to replace the ATARI 825 printer in the line-up as the 80 column dot matrix printer option, and also had a connection for an automatic sheet feeder, which ATARI never licensed. Like the 825, the 1025 had multiple fonts for different character widths, but lacked the proportional font that was present on the 825. It did, however, have support for the European character set additions that were present on the ATARI XL and XE systems.

Vintage Atari 1025 Printer for 400/800/XL/XE Computer

ATARI 1027

The Mannesmann-Tally Riteman LQ was used as the basis for the ATARI 1027, a letter quality printer that accepted single sheets of paper. Because the rubber in this printer mechanism is guaranteed to decompose over time, the FujiNet is now one of the only ways to experience how this printer actually functioned.

Early Printer were noisy, but they were also slow and generally expensive

The typeface used by this printer is Prestige Elite 12.

ATARI 1029

The ATARI 1029 was a printer which saw limited release, mostly in Europe, at the end of the XL series line. It was licensed from Seikosha, and the same mechanism was licensed to other companies such as Commodore. It too is an 80 column printer, with international character set support.

planet-IRATA - 30 - ATARI 1029 Printer

ATARI XMM801

With the XE series, ATARI opted to license printer mechanisms from Citizen. This 80 column printer matches the XE series in industrial design, and is compatible with the Epson MX-80 control codes, and can also do graphics.

WTB TOP COVER & SPARES FOR ATARI XMM801 PRINTER - Wanted - AtariAge Forums

ATARI XDM121

To complement the XMM801 dot-matrix printer, ATARI released the XDM121 daisy-wheel printer, again using a printer mechanism licensed from Citizen. The typeface used here in this simulation is Pica 10.

EPSON MX/FX-80

The EPSON MX-80 was an inexpensive; ubiquitous 80-column printer that was available for any microcomputer with a suitable parallel port. The MX-80 was also available with a graphics printing option called GrafTrax, which is also present in FujiNet’s emulation, allowing programs like Print Shop to print as-is. Traditionally, ATARI users needed a parallel port interface such as the Atari 850, P:R: Connection, or MPP-1150 to provide the necessary connection; this is not needed for FujiNet. The extended commands brought by the EPSON FX-80 are also supported.

Epson FX-80 Dot Matrix Printer

Because the MX-80 emulation in FujiNet supports GrafTrax, it will work with programs that print graphics, such as Print Shop.

Okidata OKIMATE 10

The OKIMATE 10 was a novel color thermal wax printer produced by Okidata starting in 1984. It was also unique in that the printer had interface modules for Atari, Commodore, Apple ][, and IBM PC systems that plugged into the printer, and exposed the appropriate port.

Note: There seems to have been a regression that crept into the color output mode.

HTML Printer

The remaining printers are special.

The HTML Printer outputs an HTML document for anything fed to it.

HTML ATASCII Printer

The ATASCII printer is a variant of the HTML printer that can emit the entire ATASCII character set, by using a specific ATASCII font embedded into the document. This can be useful for emitting listings that need to preserve their special characters, such as BASIC listings.

Other Platforms

Other platforms can choose to provide these printers to their host systems. For example, the COLECO ADAM version of FujiNet provides a complete emulation of the SmartWriter printer, complete with bi-directional printing support, needed in programs such as SmartWriter and AdamCalc.

Platforms such as Apple ][ need to find a way to interface the virtual printer in the firmware to the outside world. If you’d like to help, please engage us on the Discord.

Special Thanks

A special thanks to Jeff Piepmeier, who implemented the majority of the printer emulations and designed their fonts. Without his work, none of this would exist.

Also a special thanks to Oscar Fowler, who implemented the HTML printers.

How FujiNet High Score Enabled Games bring a Community Together

FujiNet is more than hardware. I see it as a way to bring people together via a WiFi network adapter. The FujiNet community not only do this by providing ways for people to load software, and make new networkable programs that we all can use, but we provide ways to do things together like competing for high scores in games.

To this end, FujiNet implements a feature called “High Score Enabled” which is a simple way to allow for games to specify where on a disk their high score tables are stored, and to allow for those areas to be written to, even with the game being mounted as read only. When a game marked as High Score Enabled, any writes to the high score sectors cause the FujiNet to temporarily re-mount the disk as read-write, write the resulting sectors, and then close the temporary write, re-mounting again as read-only. The apps.irata.online TNFS server contains a growing number of games which have been adapted to work in this manner.

In addition to allowing the read-write of high score tables in these specially marked disk images, there are web pages which get automatically updated with small “scraper” programs, running on the same server and disk as the high-score enabled disk image, which detect the changes in the ATR disk image, and generate a new web page for public viewing, you can see the scores from apps.irata.online‘s High Score Enabled games via scores.irata.online.

How can I make my game High Score Enabled?

Making your game High Score Enabled can be as simple as:

  • Store your high scores on the disk image.
  • Alter the ATR header to mark the sectors occupied by high score as writable.
  • Implement a scraper if you want to make a web page (optional).

Storing High Scores

This is entirely up to you, as to how you implement it, FujiNet does not care. In the four dozen games that I’ve ported thus far, I have put their implementation in three categories:

  • Games that already have a high score table, and save them to disk. Not much to do here but to find where the high score table is, and mark it in the ATR header. Examples of this are Jumpman, Gorf, and Spelunker.
  • Games that have a High Score table, but do not save it to disk. We just needed to add routines to write the high score to a disk sector, and to read it back again. An example of this is Baja Buggies.
  • Games that do not have a high score table at all. Examples of this are PAC-MAN, Defender, Dig-Dug, and Pengo.

Altering the ATR Header

The initial 16-byte ATR header has a few unused bytes which we are using to hold the High Score Enabled information. They are very simple:

OffsetSizeDescription
+121The number of sectors of the high score table.
+132The starting sector number (0-65535) of the high score table.
The changed bytes of the ATR header to add High Score Enabled data.

In fact, the following C program can easily adapt any ATR file:

/**
* Set high-score-mode bytes in ATR file
*/

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int high_score_enable(char *atr, unsigned short start, unsigned char len)
{
FILE *f = fopen(atr,"r+");
unsigned char id[2];

if (!f)
{
perror("error opening file.");
return 1;
}

fread(&id,sizeof(unsigned char),2,f);

if ((id[0] != 0x96) || (id[1] != 0x02))
{
printf("Not a valid ATR file. Aborting.\n");
fclose(f);
return 1;
}

fseek(f,0x0C,SEEK_SET);

fwrite(&len,sizeof(unsigned char),1,f);
fwrite(&start,sizeof(unsigned short),1,f);

fclose(f);

return 0;
}

int main(int argc, char* argv[])
{
if (argc < 4)
{
printf("%s <file.atr> <starting sector> <number of sectors>\n",argv[0]);
return 1;
}

return high_score_enable(argv[1],atoi(argv[2]),atoi(argv[3]));
}

Writing to the Disk: Baja Buggies

Baja Buggies already had a functional high score routine, but it did not write it to disk. The solution to this was to patch Baja Buggies to add two routines to read the high score sectors to and from disk.

The most straightforward way to do this, is to find the location where the high scores are being displayed from main memory, and to use the operating system SIOV routines to write them to disk. Dumping the display list of the high score screen using Altirra, we see the following:

Altirra> .dumpdlist
37BA: x4 blank 8
37BE: mode 7 @ 37D7
37C1: x2 blank 8
37C3: mode 6
37C4: blank 8
37C5: mode 6
37C6: blank 8
37C7: mode 6
37C8: blank 8
37C9: mode 6
37CA: blank 8
37CB: mode 6
37CC: blank 8
37CD: mode 6
37CE: blank 8
37CF: mode 6
37D0: blank 8
37D1: blank 4
37D2: x2 blank 1
37D4: waitvbl 37BA

So display memory starts at 37D7. Let’s dig in a bit, and see where the high score data starts:

Altirra> dbi 37d7
37D7: 00 00 62 75 67 67 79 00 73 63 6F 72 65 62 6F 61 | buggy scoreboa|
37E7: 72 64 00 00 00 B2 A1 AE AB 00 A9 AE A9 B4 A9 A1 |rd .... ......|
37F7: AC B3 00 B3 B0 A5 A5 A4 00 00 11 00 00 00 00 00 |.. ..... 1 |
3807: 68 6A 78 00 00 00 00 16 17 0E 19 10 00 00 12 00 |hjx 67.90 2 |
3817: 00 00 00 00 63 6C 6C 00 00 00 00 16 17 0E 19 10 | cll 67.90|
3827: 00 00 13 00 00 00 00 00 63 6C 6C 00 00 00 00 16 | 3 cll 6|
3837: 17 0E 18 15 00 00 14 00 00 00 00 00 65 6A 64 00 |7.85 4 ejd |
3847: 00 00 00 16 17 0E 18 15 00 00 15 00 00 00 00 00 | 67.85 5 |
Altirra> dbi 3857
3857: 65 6A 72 00 00 00 00 16 17 0E 12 10 00 00 16 00 |ejr 67.20 6 |
3867: 00 00 00 00 5F 00 00 00 00 00 00 16 17 0E 10 15 | . 67.05|
3877: 00 00 00 B5 B3 A5 00 AB A5 B9 A2 AF A1 B2 A4 00 | ... ........ |
3887: B4 AF 00 00 00 00 A5 AE B4 A5 B2 00 13 00 A9 AE |.. ..... 3 ..|
3897: A9 B4 A9 A1 AC B3 00 00 A9 E0 8D F4 02 A9 08 8D |...... ....".(.|
38A7: C8 02 A9 36 8D C5 02 A9 86 8D C6 02 A9 0E 8D C4 |.".V.."...."....|
38B7: 02 A9 BA 8D 30 02 A9 37 8D 31 02 A9 22 8D 2F 02 |"...P".W.Q".B.O"|
38C7: A9 00 8D 1D D0 8D 03 D2 A9 03 8D 1D 02 8D 1C 02 |. .=..#..#.=".<"|

We see the High Score data starting rather cleanly at $3780, and continuing on a bit past $3880. This means that we need three 128 byte sectors to hold the high score data.

But where do we store it?

Tracing how Baja Buggies loads, we can see that the high score data gets loaded in as part of the boot process:

Altirra> .tracesio on
SIO call tracing is now on.
...
SIO: Device $31[1], command $52, buffer $3780, length $0080, aux $0088 timeout 7.5s | Disk: Read sector
SIO: Device $31[1], command $52, buffer $3800, length $0080, aux $0089 timeout 7.5s | Disk: Read sector
SIO: Device $31[1], command $52, buffer $3880, length $0080, aux $008A timeout 7.5s | Disk: Read sector
...

Tracing the read, we see that the three areas in memory that are occupied by the high score are on sectors $88, $89, and $8A. We just need to write a set of routines which can read and write those sectors, and patch them into the game ATR.

As it turns out, there is some space below the scratch area that Baja Buggies uses at $0489 which is large enough to hold a read and write routine. The following bit of assembler is all that is needed to read and write the high scores to and from disk:

;; 
;; Write high scores to disk
;;

OPT h-

DDEVIC = $0300 ;peripheral bus ID number
DUNIT = $0301 ;unit number
DCOMND = $0302 ;bus command ordinal
DSTATS = $0303 ;command type/status return
DBUFLO = $0304 ;data buffer pointer
DBUFHI = $0305
DTIMLO = $0306 ;device timeout in seconds
DBYTLO = $0308 ;number of bytes transferred
DBYTHI = $0309
DAUX1 = $030A ;command auxiliary bytes
DAUX2 = $030B

SIOV = $E459 ; SIO Vector

ORG $0489

LDA #$00 ; We need to put the bottom of the display back to blank
STA $37D2 ; because we are writing it back to disk.
STA $37D3 ; otherwise all hell breaks loose.

LDA #$31 ; Drive 1
STA DDEVIC
LDA #$01 ; Unit 1 (D1:)
STA DUNIT
LDA #'W' ; Write
STA DCOMND
LDA #$80 ; ->Drive
STA DSTATS
LDA #$80 ; $3780
STA DBUFLO
LDA #$37
STA DBUFHI
LDA #$80 ; 128 bytes
STA DBYTLO
LDA #$00
STA DBYTHI
LDA #$88 ; Sector $88
STA DAUX1
LDA #$00
JSR SIOV ; Do it

LDA #$80
STA DSTATS
LDA #$00 ; $3800
STA DBUFLO
LDA #$38
STA DBUFHI
LDA #$89 ; Sector $89
STA DAUX1
JSR SIOV ; do it

LDA #$80
STA DSTATS
LDA #$80 ; $3880
STA DBUFLO
LDA #$38
STA DBUFHI
LDA #$8A ; Sector $8A
STA DAUX1
JSR SIOV ; do it.

JMP $38DF ; Finish and back to Attract mode.

Spending some time in the debugger, we find the attract mode at $38DF, and we find a jump to location $0506 in sector 1 where we can place our additional code. So we assemble the above routine into a bin file (headerless), and write the following bit of C code to inject the code into the unused portion of the disk sector, where it can be called after the game is loaded:

/**
* Patch Baja Buggies to add score code.
* @author Thomas Cherryhomes
* @email thom dot cherryhomes at gmail dot com
* @license gpl v. 3
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define SEEK_POS 0x19 /* Right after JMP $0506 in Sector 1 */
#define JUMP_POS 0x45EC /* Replacing the 38DF */

#define MAX_PATCH_SIZE 115

char sectorBuf[MAX_PATCH_SIZE];

const char jump[2]={0x89,0x04};

int main(int argc, char *argv[])
{
FILE *afp; FILE *bfp;
size_t len;

if (argc<3)
{
printf("%s <atr-file> <bin-file>\n",argv[0]);
return 1;
}

afp = fopen(argv[1],"r+");

if (!afp)
{
perror("Could not open ATR file");
return 1; // Bail.
}

bfp = fopen(argv[2],"r");

if (!bfp)
{
perror("Could not open BIN file");
fclose(afp);
return 1; // Bail.
}

if (fseek(afp,SEEK_POS,SEEK_SET))
{
perror("Could not seek to ATR sector");
fclose(afp);
fclose(bfp);
return 1;
}

len = fread(&sectorBuf[0],sizeof(char),sizeof(sectorBuf),bfp);

fclose(bfp);

fwrite(&sectorBuf[0],sizeof(char),MAX_PATCH_SIZE,afp);

// Patch 0x45EC to jump to our new routine
fseek(afp,JUMP_POS,SEEK_SET);
fwrite(&jump[0],sizeof(const char),sizeof(jump),afp);

fclose(afp);

// Done.
return 0;
}

Implementing a High Score Table: Kid Grid

Games like Kid Grid, PAC-MAN, Defender, Donkey Kong, and many others do not have any high score capability in them, and need to be patched. Using everything we’ve covered thus far, we’ll add a routine to deal with entering high scores to add this to Kid Grid.

We start by getting the initial game in XEX format, putting it into a form which can be disassembled, disassemble the game, add the high score routine, and assemble the result into a form which can be booted on an ATR disk, with the high score taking up the last two sectors of the disk, 719 and 720.

To get the initial game, we use the Homesoft version of Kid Grid, which can be retrieved from apps.irata.online/Atari_8-bit/Games/Homesoft/K/Kid Grid.xex, and run it directly in Altirra. This has the effect of showing the individual binary segments:

EXE: Loading program 0006-0020 to 2020-203A
EXE: Loading program 0025-0025 to 0244-0244
EXE: Loading program 002A-003B to 2300-2311
EXE: Loading program 0040-0041 to 02E2-02E3
EXE: Jumping to 2300
EXE: Loading program 0046-0270 to 2300-252A
EXE: Loading program 0275-052F to 2533-27ED
EXE: Loading program 0534-0540 to 27F3-27FF
EXE: Loading program 0545-095D to 2827-2C3F
EXE: Loading program 0962-097D to 2C46-2C61
EXE: Loading program 0982-099D to 2C6E-2C89
EXE: Loading program 09A2-09BB to 2C97-2CB0
EXE: Loading program 09C0-09DA to 2CBF-2CD9
EXE: Loading program 09DF-09F9 to 2CE7-2D01
EXE: Loading program 09FE-0A05 to 2D18-2D1F
EXE: Loading program 0A0A-0A18 to 2D64-2D72
EXE: Loading program 0A1D-0A2B to 2D8C-2D9A
EXE: Loading program 0A30-0A3F to 2DB4-2DC3
EXE: Loading program 0A44-0A53 to 2DDC-2DEB
EXE: Loading program 0A58-0A67 to 2E04-2E13
EXE: Loading program 0A6C-0A81 to 2E51-2E66
EXE: Loading program 0A86-0A87 to 2E79-2E7A
EXE: Loading program 0A8C-0A9A to 2E80-2E8E
EXE: Loading program 0A9F-0AB5 to 2EA1-2EB7
EXE: Loading program 0ABA-0AD0 to 2EC9-2EDF
EXE: Loading program 0AD5-0AEB to 2EF1-2F07
EXE: Loading program 0AF0-0B02 to 2F43-2F55
EXE: Loading program 0B07-0B15 to 2F6D-2F7B
EXE: Loading program 0B1A-0B3A to 2FB3-2FD3
EXE: Loading program 0B3F-0B95 to 3000-3056
EXE: Loading program 0B9A-0C00 to 305D-30C3
EXE: Loading program 0C05-0C6A to 30CC-3131
EXE: Loading program 0C6F-0CD6 to 3137-319E
EXE: Loading program 0CDB-1B2E to 31A4-3FF7
EXE: Loading program 1B33-1B34 to 02E2-02E3
EXE: Jumping to 2B00

The Homesoft version does a lot of small loads (as part of its compressed format), which will get in the way of us actually patching the program, so we need to let the loader do its thing, and save the resulting memory image. Again, I use Altirra to do this:

Altirra> .writemem kidgrid.bin 2000 L2000
Wrote 2000-3FFF to kidgrid.bin

The resulting binary can be loaded into the excellent dis6502 tool as a raw binary image loaded at $2000, and the resulting disassembly saved.

We need to make two modifications to the resulting assembly, to add an ORG and a run address:

;
; Start of code
;
org $2000
; ...
; ... at the bottom...
;

icl "hiscore.asm"

org $02E0
.word $2B00

And we add a new assembly file called hiscore.asm, containing our high score routine. I will not post it here, but you can thumb through it at your leisure. It contains the routines to read and write the high score table to disk, as well as calculate if and where to place the score on the board, and read characters from the keyboard.

Note: Many games, will either completely re-vector the various routines for the SIO, vertical blank, and keyboard interrupts. These interrupts need to be restored when this routine is running, and restored back to their game values when the game resumes.

If you read the README.md for kid-grid, you’ll see that the high score was found at location $0480 in memory, stored high nibble for each digit, so the resulting data for 999999 points is:

90 90 90 90 90 90

Other README.md files in the atari-game-ports directory show what needed to be found and changed as part of the reverse engineering process for each game. Some are still a work in progress that could use some help.

For Kid Grid, I opted to place the high score board in the middle of the screen, using mode 6 characters (20 characters per line, 4 possible colors, and one background), and an altered display list is presented to show this:

hiscore_dlist:
dta $70, $70, $70

dta $4E, $10, $05
dta $0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e

dta $46, .lo(hiscore_txt), .hi(hiscore_txt)
dta $06, $06, $06, $06, $06, $06, $06, $06, $06, $06, $06, $06, $06

dta $4E, $D0, $1C
;dta $4E, $B8, $16
dta $0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e
dta $4E, $00, $20, $0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e,$0e

dta $41, .lo(hiscore_dlist), .hi(hiscore_dlist)

For the high score screen area, space is reserved, which is filled with the data from the two high score sectors, 719, and 720:

hiscore_txt:
.sb ' '
.sb ' high scores '
.sb ' '

HISTR: .ds 128
HISTR2: .ds 128

The actual high score table is assembled separately, without a binary header, and assembles exactly to 256 bytes, so that it can fit within two disk sectors.

	;; The High score table. Will be assembled sans header
;; to be written to disk using write-high-score.c

opt h-

HISTR: .SB " 1. "
.SB " 2. "
.SB " 3. "
.SB " 4. "
.SB " 5. "
.SB " 6. "
.SB " 7. "
.SB " 8. "
.SB " 9. "
.SB " 10. "
.SB " "
.SB " "
.SB " "

We also need to write a small tool called write-high-score to place the above data onto the last two sectors of the disk:

/**
* write-high-score - Takes <binfile> and writes to sector 720 of <atr>
*
* @author: Thomas Cherryhomes
* @email: thom dot cherryhomes at gmail dot com
* @license: gpl v. 3
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define SEEK_POS 0x16710 // sector 0x2CF

char sectorBuf[256];

int main(int argc, char *argv[])
{
FILE *afp; FILE *bfp;
size_t len;

if (argc<3)
{
printf("%s <atr-file> <bin-file>\n",argv[0]);
return 1;
}

afp = fopen(argv[1],"r+");

if (!afp)
{
perror("Could not open ATR file");
return 1; // Bail.
}

bfp = fopen(argv[2],"r");

if (!bfp)
{
perror("Could not open BIN file");
fclose(afp);
return 1; // Bail.
}

if (fseek(afp,SEEK_POS,SEEK_SET))
{
perror("Could not seek to ATR sector");
fclose(afp);
fclose(bfp);
return 1;
}

len = fread(&sectorBuf[0],sizeof(char),sizeof(sectorBuf),bfp);

if (len != sizeof(sectorBuf))
{
perror("Could not read BIN file");
fclose(afp);
fclose(bfp);
return 1;
}

fclose(bfp);

fwrite(&sectorBuf[0],sizeof(char),sizeof(sectorBuf),afp);

fclose(afp);

// Done.
return 0;
}

To make this a bootable ATR, we use the dir2atr utility that’s part of Hias’ AtariSIO tools. We use this, along with a copy of picoboot.bin to create an ATR which contains the game, and a boot sector to load it. The write-high-score tool is used to then write the two high score sectors to disk, using the following Makefile:

AS=mads
CP=cp
SRC=kid_grid.asm
XEX=AUTORUN
LST=kid_grid.lst
ATR="Kid Grid.atr"
BUILD=build
MKDIR=mkdir
DIR2ATR=dir2atr
WRITE_HIGH_SCORE=./write-high-score
HISCORE_TABLE_ASM=hiscore_table.asm
HISCORE_TABLE_BIN=hiscore_table.bin
HIGH_SCORE_ENABLE=./high-score-enable
BOOT_PROGRAM=picoboot.bin

.PHONY: clean pre

all: clean pre xex dist hiscore hiscore_enable

pre:
$(RM) -rf $(BUILD)
$(MKDIR) -p $(BUILD)

xex:
$(AS) $(SRC) -o:$(BUILD)/$(XEX) -l:$(LST)

dist:
$(DIR2ATR) -B $(BOOT_PROGRAM) -S $(ATR) $(BUILD)

hiscore:
$(AS) $(HISCORE_TABLE_ASM) -o:$(BUILD)/$(HISCORE_TABLE_BIN)
$(CC) -o$(WRITE_HIGH_SCORE) $(WRITE_HIGH_SCORE).c
$(WRITE_HIGH_SCORE) $(ATR) $(BUILD)/$(HISCORE_TABLE_BIN)

hiscore_enable:
$(CC) -o$(HIGH_SCORE_ENABLE) $(HIGH_SCORE_ENABLE).c
$(HIGH_SCORE_ENABLE) $(ATR) 719 2

clean:
$(RM) -rf $(BUILD)
$(RM) -rf $(ATR)
$(RM) -rf $(WRITE_HIGH_SCORE)
$(RM) -rf $(HIGH_SCORE_ENABLE)
$(RM) -rf $(LST)

tnfs:
cp kid_grid.atr ~/tnfs

The Score Scraper – Creating the HTML page.

For each of these games, the high score format is determined or created as needed, and because of this, the same data residing in each disk sector can be scraped and reformatted to be displayed as part of an HTML page. Because the scraper and the TNFS server are running on the same machine, each game’s disk image ATR can be monitored independently using the inotify system calls in Linux to detect when the disk image changes, and the resulting HTML page can be re-built.

Kid Grid’s scraper is implemented using the following C code:

/**
* Grab high score from Kid Grid, write to HTML
*
* Linux required. (uses inotify)
*
* @author Thomas Cherryhomes
* @email thom dot cherryhomes at gmail dot com
* @license gpl v. 3
*/

#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/inotify.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#include <linux/limits.h>

#define EVENT_SIZE ( sizeof(struct inotify_event) )
#define EVENT_BUF_LEN ( 1024 * ( EVENT_SIZE + NAME_MAX ) )

#define PACMAN_SEEK_POS (0x16710)

#define LINE_WIDTH 20

static volatile bool ctrlc = false;

int inotify_fd, inotify_wd;
int inotify_event_len;

char event_buffer[EVENT_BUF_LEN];

void setctrlc(int dummy)
{
ctrlc = true;
}

void kidgrid(char *atr, char *html)
{
unsigned char buf[256];
FILE *fa, *fh;
int i, offset;

printf("Writing new kidgrid.html\n");

fa = fopen(atr,"rb");
fh = fopen(html,"w");

fseek(fa,PACMAN_SEEK_POS,SEEK_SET);

fread(buf,sizeof(unsigned char),sizeof(buf),fa);

/* Process text */
for (i=0;i<sizeof(buf);i++)
{
/* Do very simple ANTIC screen code conversion to ASCII */
unsigned char c = buf[i];

if (c>0x89 && c<0x9A)
c -= 0x80;

if (c>127)
c -= 0xA0;
else if (c<64)
c+=32;
else if (c>64)
c-=32;

buf[i]=c;
}

/* small fix, erase first char in buf */
buf[0]=0x20;

/* start html */
fprintf(fh,"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n");
fprintf(fh,"<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\" lang=\"en\">\n");
fprintf(fh," <head>\n");
fprintf(fh," <title>Latest Kid Grid High Scores</title>\n");
fprintf(fh," <meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" />\n");
fprintf(fh," <meta http-equiv=\"refresh\" content=\"30\" />");
fprintf(fh," <meta name=\"keywords\" content=\" \" />\n");
fprintf(fh," <meta name=\"description\" content=\" \" />\n");
fprintf(fh," <link rel=\"stylesheet\" type=\"text/css\" href=\"kidgrid.css\" media=\"screen\" />\n");
fprintf(fh," <link rel=\"icon\" type=\"image/png\" href=\"icon.png\" />\n");
fprintf(fh," </head>\n");
fprintf(fh," <body>\n");
fprintf(fh," <pre>\n");

/* start body */

offset=0; /* buffer start for hi scores */

fprintf(fh,"\n== KID GRID TOP SCORES ==\n");

for (i=0;i<sizeof(buf);i++)
{
fprintf(fh, "%c", buf[offset++]);

if ((i % LINE_WIDTH) == 0)
fprintf(fh, "\n ");
}

/* end body */

fprintf(fh,"\n");
fprintf(fh," </pre>\n");
fprintf(fh," </body>\n");
fprintf(fh,"</html>\n");

fclose(fh);
fclose(fa);
}

int main(int argc, char *argv[])
{
if (argc < 3)
{
printf("%s <path-to-kidgrid-atr> <path-to-output-html>\n",argv[0]);
return 1;
}

kidgrid(argv[1],argv[2]);

signal(SIGINT, setctrlc);
signal(SIGTERM, setctrlc);

inotify_fd = inotify_init();

if (inotify_fd < 0)
{
perror("inotify_init");
return 1;
}

inotify_wd = inotify_add_watch(inotify_fd, argv[1], IN_MODIFY);

if (inotify_wd == -1)
{
perror("inotify_add_watch");
goto bye2;
}

/* Set for non-blocking */
fcntl (inotify_fd, F_SETFL, fcntl (inotify_fd, F_GETFL) | O_NONBLOCK);

while (!ctrlc)
{
int i;

inotify_event_len = read(inotify_fd, event_buffer, EVENT_BUF_LEN);

i=0;

if (inotify_event_len < 0)
{
usleep(100000);
continue;
}

while (i < inotify_event_len)
{
struct inotify_event *event = ( struct inotify_event * ) &event_buffer[ i ];

kidgrid(argv[1],argv[2]);

i += EVENT_SIZE + event->len;
}
}

/* ctrl-C or termination, close it off. */

printf("Exiting %s\n",argv[0]);
bye:
inotify_rm_watch(inotify_fd,inotify_wd);
bye2:
close(inotify_fd);

return 0;
}

Each of these are compiled and run as system services using a systemd service unit such as:

[Unit]
Description=Atari Kid Grid Hi-scores
After=remote-fs.target
After=syslog.target

# replace /tnfs with your TNFS directory

[Service]
User=thomc
Group=thomc
ExecStart=/usr/local/sbin/kidgrid "/home/thomc/apps.irata.online/Atari_8-bit/Games/High Score Enabled/Kid Grid.atr" "/home/thomc/scores/kidgrid.html"

[Install]
WantedBy=multi-user.target

Since each of these services are written in C, they are not only very small in memory footprint, they have no dependencies other than needing to run on Linux, due to inotify. This can be adjusted to use the notification calls for your favorite operating system:

● kidgrid.service - Atari Kid Grid Hi-scores
Loaded: loaded (/etc/systemd/system/kidgrid.service; enabled; preset: enabled)
Active: active (running) since Mon 2024-12-02 00:03:48 UTC; 1 month 8 days ago
Main PID: 1152 (kidgrid)
Tasks: 1 (limit: 9246)
Memory: 192.0K (peak: 1.5M)
CPU: 5min 53.763s
CGroup: /system.slice/kidgrid.service
└─1152 /usr/local/sbin/kidgrid

Some Games still need help to convert.

The following games are in the atari-game-ports directory, and need help to finish their port.

  • Berzerk – Need to find the best place to inject where High Score Table can be triggered.
  • moon-patrol-redux – Needs the whole treatment.
  • gyruss – Needs a disassembly that is stable and can be patched well.
  • star-trek – Needs the whole treatment
  • embargo – Can’t get a stable disassembly for #@!(%$

Is there another game that needs to be here? Come help us hack on it!