So far in our Atari ST text adventure using STOS, we have been loading test data via inline data statements. There are some advantages to that, which we can discuss, but also some frustrations so let's take a look at moving our data outside of the code so we can work on data and the game itself separately.
Pros and Cons of External Data
As software gets more complicated, you will find programmers lean toward "separation" - ie. not having everything all in one big file.
So we will use libraries, will split the front-end from the back-end, have modules for UI, logic and data, and so on.
Not only does it simplify things, but it also allows us or someone else to work on, say, level/map creation while the coding happens in parallel. In theory, I could code up this game and then other people could take the code and build their own adventures by just changing the data files ...
For us retro-focused folks there is another major benefit - having data load from files means that the game does not have to all load into memory at once, and the game could just load data across many disks as necessary. In fact, lots of games did exactly that back in the day.
So why would you include the data right in your code as we have been doing up to now?
The first reason is portability, in that everything gets compiled together so the data is all there with the game binary in the deliverable end result.
Secondly, loading from floppy disk can be slow, and loading from tape extremely slow, therefore a lot of coders back in the day would aim to have the whole thing load into memory to prevent having to put their players through longer load times or disk-swapping.
For the coder, initially scrolling down in the listing and adding a value can be easier than having to work on multiple files and build into the code the file handling logic.
Clearly, there is no wrong or right answer, so it's best to learn both approaches and then take things from there.
The Atari ST File System
One of the many things that made the ST stand out once people got their hands on one was the "almost DOS-compatible" TOS disk operating system. 3 1/2" floppies formatted on a PC can be read on an Atari ST, but to do the reverse required special software initially.
This CP/M and almost-DOS compatible heritage meant that files are named with 8.3 characters and there is a file allocation table.
Most home gamers would have had just the included, 360k single-sided floppy drive back in the mid-1980s, but prosumers and professionals would have been much more productive with the "massive" 20mb+ hard drives and of course external floppy drives. While later models had double-sided floppy drives, much of the media that was released stuck to the more common single-sided format to keep everyone happy, even if that meant having to include more disks.
For us as programmers, this means that if you want your game to run on original hardware or the most strict of modern emulators, then your game will need to fit comfortably on one or more 360kb disks.
STOS File Functions
As in DOS, STOS provides a DIR
command to get a directory listing, but unlike DOS you use a variable, DIR$
, to specify the current directory.
When calling DIR
you can also optionally specify the path and a switch for wide view (/W
).
This only displays the results onscreen, however. In our code we would instead using DIR FIRST$
and then DIR NEXT$
until an empty string is returned:
10 rem disk
20 FILENAME$=dir first$("c:\*.asc",-1)
30 print FILENAME$
40 while FILENAME$<>""
50 FILENAME$=dir next$
60 print FILENAME$
70 wend
As well as the path and wildcard parameter, you must also specify a flag for the type of file to return:
Flag | Description |
---|---|
-1 | Return Everything |
0 | Normal Read/Write files |
1 | Read-Only files |
2 | Hidden files |
3 | System files |
4 | Disk/Volume labels |
5 | Folders |
6 | Closed Files |
For file system housekeeping there are also the following commands:
Command | Description |
---|---|
DFREE | Disk space remaining |
MKDIR | Make a folder |
RMDIR | Delete a folder |
KILL | Delete a file |
RENAME | Rename a file |
Loading Files
There are two types of file that we would concern ourselves with in STOS, regular sequential text files and random access files.
If you want to edit your files in a normal text editor then you would use the former, while the latter have the advantage that you can access a single record of data without having to load and iterate through all the data to get to the part that you want.
Example
In our adventure game we have a list of rooms with (currently) a room name for each. Eventually we will expand this to a room name and also a description.
This is a good case for using text files, which can be easily loaded in like so:
10 dim ROOMDESC$(100)
20 print "rooms"
30 open in #1,"rooms.asc"
40 for r=0 to 99
50 input #1, desc$ : roomdesc$(r)=desc$
60 print desc$
70 next r
80 close #1
Checking for End of File
We open a file and assign it the number #1, then read a line using input #1
, specifying we want the result to go into our desc$
string variable.
This is fine when we know there will be for sure exactly the correct number of rows in the file, but it would error if there are too few.
An alternative that is a little bit more forgiving is to check for eof(1)
, aka End of File.
You can use this in a WHILE
or REPEAT UNTIL
depending on where you want your check to take place.
Multiple Fields
This is fine when your data is a whole line of text, but what if it is more structured?
After getting the whole line into our string variable we could then work on splitting it up into component parts, but STOS actually makes it very straightforward.
Our room layout map is a comma-delimited grid, and that can be simply loaded like thus:
input #1, ROOMS$(0,Y), ROOMS$(1,Y), ROOMS$(2,Y), ROOMS$(3,Y), ROOMS$(4,Y), ROOMS$(5,Y), ROOMS$(6,Y), ROOMS$(7,Y), ROOMS$(8,Y), ROOMS$(9,Y)
There is also a LINE INPUT
command that allows you to specify a different delimiter for your data.
Full Listing for Part 3
10 rem Adventure boilerplate by Chris Garrett @retrogamecoders
20 rem =======================================================
30 :
40 rem init game
50 mode 1 : key off : cls
60 dim ROOMS$(10,10) : dim ROOMDESC$(100) : dim OBJECTS$(100,100)
70 gosub 890
80 :
90 rem initialize start room
100 X=5 : Y=5
110 :
120 rem ========================================
130 rem USER INPUT
140 rem ========================================
150 rem @usercommands
160 cls : locate 0,0 : room = (y * 10) + x : print "room: ",room: inverse on: print roomdesc$(room): inverse off: print "Available Exits:"
170 if val(ROOMS$(X,Y-1))>0 then print "> North"
180 if val(ROOMS$(X+1,Y))>0 then print "> East"
190 if val(ROOMS$(X-1,Y))>0 then print "> West"
200 if val(ROOMS$(X,Y+1))>0 then print "> South"
210 gosub 800
220 print "Command"; : input CMD$ : IN$=upper$(left$(CMD$,1))
230 sp=instr(CMD$," ") : rem Space between words
240 obj$=right$(CMD$,len(CMD$)-sp) : rem Specified Object
250 if IN$="N" and val(ROOMS$(X,Y-1))>0 then Y=Y-1
260 if IN$="E" and val(ROOMS$(X+1,Y))>0 then X=X+1
270 if IN$="W" and val(ROOMS$(X-1,Y))>0 then X=X-1
280 if IN$="S" and val(ROOMS$(X,Y+1))>0 then Y=Y+1
290 if IN$="G" then gosub 370 : rem get
300 if IN$="U" then gosub 480 : rem use
310 if IN$="D" then gosub 540 : rem drop
320 if IN$="A" then gosub 650 : rem attack
330 if IN$="I" then gosub 710 : rem inventory
340 if IN$="Q" then print "Goodbye!" : end
350 goto 150
360 :
370 rem @getobject
380 rem GET OBJECT
390 ofound=0
400 for ob=0 to 99
410 if upper$(objects$(ob,0))=upper$(obj$) then ofound=ob : objects$(ofound,1)="0" : rem zero = inventory
420 next ob
430 if ofound > 0 then print "You picked up: "+objects$(ofound,0)
440 if ofound = 0 then print obj$ + " not found"
450 wait key
460 return
470 :
480 rem @useobject
490 rem USE OBJECT user action
500 print obj$
510 wait key
520 return
530 :
540 rem @dropobject
550 rem DROP OBJECT user action
560 ofound=0
570 for ob=0 to 99
580 if upper$(objects$(ob,0))=upper$(obj$) then ofound=ob : objects$(ofound,1)=str$(room) : rem set to current room
590 next ob
600 if ofound > 0 then print "You dropped: "+objects$(ofound,0)
610 if ofound = 0 then print obj$ + " not found in your carried items"
620 wait key
630 return
640 :
650 rem @attack
660 rem ATTACK user action
670 print obj$
680 wait key
690 return
700 :
710 rem @inventory
720 rem INVENTORY user action
730 print "INVENTORY:"
740 for ob=1 to 100
750 if objects$(ob,1)="0" then print " . " + objects$(ob,0)
760 next ob
770 wait key
780 return
790 :
800 rem @objectsinroom
810 rem LOOKUP OBJECTS IN ROOM
820 objinroom$=""
830 for o=1 to 100
840 if val(OBJECTS$(o,1))=room then objinroom$=objinroom$ + " " + OBJECTS$(o,0)
850 next o
860 if objinroom$<>"" then print "OBJECTS VISIBLE:"+objinroom$
870 return
880 :
890 rem @loaddata
900 rem LOAD GAME DATA FROM FILE
910 locate 0,0
920 print "Starting, please wait"
930 :
940 rem MAP DATA
950 print "loading map"
960 open in #1,"map.asc"
970 for Y=0 to 9
980 input #1, ROOMS$(0,Y), ROOMS$(1,Y), ROOMS$(2,Y), ROOMS$(3,Y), ROOMS$(4,Y), ROOMS$(5,Y), ROOMS$(6,Y), ROOMS$(7,Y), ROOMS$(8,Y), ROOMS$(9,Y)
990 next Y
1000 close #1
1010 :
1020 rem ROOM DATA
1030 print "loading rooms"
1040 open in #1,"rooms.asc"
1050 rem room zero is the inventory
1060 for r=1 to 100
1070 input #1, desc$ : roomdesc$(r)=desc$
1080 next r
1090 close #1
1100 :
1110 rem OBJECTS in ROOMS
1120 print "loading objects"
1130 open in #1,"objects.asc"
1140 r=1
1150 repeat
1160 input #1, obj$,roomno$
1170 objects$(r,0)=obj$ : objects$(r,1)=roomno$
1180 inc r
1190 until eof(1)
1200 close #1
1210 :
1220 return
1230 :
1240 :