Skip to content

Technical Details

dfgordon edited this page Oct 7, 2023 · 13 revisions

Technical Details of Realm

This document deals with the game from a programmer's perspective. If anyone is interested in playing the game, this may contain minor spoilers.

Realm uses a combination of AppleSoft BASIC and MERLIN assembly language. Party and configuration data is saved as text, while maps, sprites, and artwork are encoded as binary files.

Realm works with either DOS 3.3 or ProDOS. The DOS 3.3 version is distributed on four 5.25" floppy disks (or images). It also works with Sider small volumes. The ProDOS version can be run on a variety of mass storage devices.

Memory Management

Realm requires 48K of RAM. Memory management is often explicit. Memory usage is extremely tight, and programs have to be frequently swapped in and out from disk.

The upper 16K of the Apple ][ address space is occupied by I/O and ROM, leaving the lower 48K for RAM. Additional RAM can only be accessed via bank switching, which Realm does not do (the ProDOS version can use a RAM disk, however). Memory maps for the DOS 3.3 and ProDOS versions are slightly different, as shown below.

Memory Map for DOS 3.3

Address Usage Format
$00-$FF All free ZP bytes are used Binary Data
$100-$1FF Stack System
$200-$2FF Input Buffer System
$300-$3B3 SOUND Machine Code
$400-$7FF Text Screen System
$800-$9B0 Outdoor Sprites Binary Data
$800-$AE8 Town Sprites Binary Data
$800-$FFB Dungeon Sprites + Brushes Binary Data
$801-$FFF Large Merchant Programs AppleSoft BASIC
$B01-$FFF Small Merchant Programs AppleSoft BASIC
$801-$3FFF Character Utilities AppleSoft BASIC
$1001-$14B0 CHAIN(1) AppleSoft BASIC
$15AC-$163D Artwork Painter, SDP.INTRP Machine Code
$163E-$1EFF Map Painter, MAP.INTRP Machine Code
$1F00-$1FA9 Submap Buffer for MAP.INTRP Binary Data
$1FC0-$1FD9 Sprite Pointers for MAP.INTRP Binary Data
$1FEE-$1FFF Parameters for MAP.INTRP Binary Data
$2000-$3FFF Graphics Screen Binary Data
$4000-$6CFF Main Programs AppleSoft BASIC
$6D00-$7FFF BASIC Variables(2) AppleSoft BASIC
$8000-$8C7F Map Data(3) Binary Data
$8C80-$9A98 Monster Artwork(4) Binary Data
$9AA5-$BFFF DOS 3.3(5) System
$C000-$CFFF Input/Output System
$D000-$FFFF Apple ][ ROM System

(1) This is not Apple's CHAIN

(2) This is set using LOMEM and HIMEM

(3) Largest range, outside maps

(4) Largest range, HYDRA

(5) Realm sets MAXFILES=1, thereby gaining a little memory back from DOS

Memory Map for ProDOS

ProDOS consumes more memory than DOS 3.3. As a result, when running under ProDOS, maps and artwork use overlapping memory, so that the dungeon map has to be saved and read back in before and after combat. ProDOS also forbids loading binary files higher than BASIC variables.

Address Usage Format
$00-$FF All free ZP bytes are used Binary Data
$100-$1FF Stack System
$200-$2FF Input Buffer System
$300-$3B3 SOUND Machine Code
$400-$7FF Text Screen System
$800-$9B0 Outdoor Sprites Binary Data
$800-$AE8 Town Sprites Binary Data
$800-$FFB Dungeon Sprites + Brushes Binary Data
$801-$FFF Large Merchant Programs AppleSoft BASIC
$B01-$FFF Small Merchant Programs AppleSoft BASIC
$801-$3FFF Character Utilities AppleSoft BASIC
$1001-$14B0 CHAIN(1) AppleSoft BASIC
$15AC-$163D Artwork Painter, SDP.INTRP Machine Code
$163E-$1EFF Map Painter, MAP.INTRP Machine Code
$1F00-$1FA9 Submap Buffer for MAP.INTRP Binary Data
$1FC0-$1FD9 Sprite Pointers for MAP.INTRP Binary Data
$1FEE-$1FFF Parameters for MAP.INTRP Binary Data
$2000-$3FFF Graphics Screen Binary Data
$4000-$6CFF Main Programs AppleSoft BASIC
$6E00-$7A7F Map Data(2) Binary Data
$6D00-$7B18 Monster Artwork (3) Binary Data
$7C00-$91FF BASIC Variables (4) AppleSoft BASIC
$9200-$95FF ProDOS file buffer (5) System
$9600-$BFFF BASIC.SYSTEM System
$C000-$CFFF Input/Output System
$D000-$FFFF Apple ][ ROM System

(1) This is not Apple's CHAIN

(2) Largest range, outside maps

(3) Largest picture, HYDRA, 3608

(4) We only set LOMEM and let ProDOS manage HIMEM

(5) ProDOS moves HIMEM down $400 while a file is opened

Chaining and Relocating BASIC Programs

Realm sometimes switches between BASIC programs loaded into disjoint regions of memory. This allows large programs to stay in memory while running a smaller program, e.g., merchant programs can be run without having to subsequently reload the larger TOWN program. This optimization matters when real floppy disk access times are involved.

The Applesoft interpreter uses little-endian addresses stored in zero page to find the start and end of a BASIC program. The start of the program is stored at location 103, and the end of the program is stored at location 175. The interpreter does not seem to need the end of the program to run it, but it is definitely needed when editing or saving. In order to chain one program into another (load a new program that remembers the variables from the old one), we load a binary file that contains a BASIC program, and spoof the interpreter into branching to the new program:

  100 PRINT CHR$(4);"BLOAD NEXTPROGRAM,A$4001": REM LOAD PROGRAM AT ADDRESS $4001
  110 POKE 16384,0: POKE 103,1: POKE 104,64: GOTO 110

Here, the BLOAD is only necessary if the program is not already loaded. Note line 110 is not an infinite loop, it branches to a different line 110 from another program. For this to work the following must be adhered to:

  • the GOTO command should follow the POKE commands on the same line
  • make sure every program is preceded by zero in memory
    • this is POKE 16384,0 in the example
  • line numbers involved in the branch must satisfy INT(L1/256) >= INT(L2/256), where L1 is the current line and L2 is the destination
    • this assures the interpreter will start the line number search at (103)

To save a BASIC program as a binary file, first get the starting address using PEEK(103) and PEEK(104). Get the ending address from PEEK(175) and PEEK(176). Calculate the length by subtracting the start from the end and type BSAVE MYPROGRAM,A$XXXX,L$YYYY, where XXXX and YYYY are the hex starting address and length, respectively. The project includes deployment scripts to do this automatically.

Applesoft Programs

Variables

In AppleSoft BASIC, variables are scoped by program. Apple provided the CHAIN program to hand off variables to a new program. Rather than use Apple's CHAIN, Realm relies on the code relocation strategy discussed above, which preserves variables as long as they don't point to program space.

An important consequence of this is that all variables are essentially global. Scoping is entirely dependent on programmer discipline. To make things more difficult, only the the first two letters of a variable name are significant in AppleSoft.

Identifier (1) Purpose Indexing Scope
AA% Auto Attack COMBAT
AB%(4) Hit Points Character global
AC%(4) Max Hit Points Character global
AD% Gold global
AE Food global
AF%(4) Strength Character global
AG%(4) Intelligence Character global
AH%(4) Wisdom Character global
AI%(4) Agility Character global
AJ%(4) Stamina Character global
AK%(4) Charisma Character global
AP$(4,8) Items Character,Item global
AX$(4,16) Spells Character,Spell global
BP$ Program directory global
CL$(4) Class Character global
CL%(4) Class Code Character global
DR%(4,3) Index Ready Character,Slot global
DT%(3) Index Ready Temporary Slot local
DV$(4) Armor Ready Character global
DW$(4) Weapon Ready Character global
DX$(4) Spell Ready Character global
D Disk Number local
FG%(6) Flags (2) Story Elements global
FL% Mode of Travel global
FO% Mode of Travel on Entry global
FS% Fast Status Mode global
GA Hit Die Monster COMBAT
GB Damage Monster COMBAT
GC$ Special Attack Monster COMBAT
GD$ Element Type (3) Monster COMBAT
GE To Hit Monster COMBAT
GF Spell Damage Monster COMBAT
GH Spell Area of Effect Monster COMBAT
GI$ Spell Name Monster COMBAT
GK$ Plural Suffix Monster COMBAT
GM Number Monster COMBAT
GN(*) Hit Points Monster COMBAT
GO$ Name Monster COMBAT
GP%(4) Front Lines Character COMBAT
GS Being Attacked COMBAT
MA Multiple Attack Counter COMBAT
MD% Map Dirty OUTSIDE,CHAIN
NA$ Party Name global
NM$(4) Name Character global
PC%(4) Paralysis Character COMBAT
PG$ Program to Chain local
PM%(10) Paralysis Monster COMBAT
PN$(10) Saved Parties CHARACTER
RA$(4) Race Character global
RB$ RAM Disk or BIN global
RD$ RAM Disk or PROG global
RM$ Realm global
SC$ Attack Type COMBAT
SD$ Attack Verb COMBAT
SE$ Attack Element (3) COMBAT
TV$ Trove DUNGEON,COMBAT
WA Attack Damage COMBAT
WB Base To Hit Chance COMBAT
WC Total To Hit Chance COMBAT
WD%(4) Drive Mappings Disk global
WR Attack Range COMBAT
WS%(4) Slot Mappings Disk global
WT%(4) Max Level Character global
WT% Denizen Code COMBAT,DUNGEON,TOWN
WU%(7,7) Proficiency Table class code,weapon code global
WV%(4) Volume Mappings Disk global
WX% Safe Dungeon Level (4) DUNGEON
WX% Town Hostile TOWN
WY% Dungeon Level DUNGEON
WY% Ship Owner TOWN
X,Y Outside Coordinates global
XT,YT Inside Coordinates DUNGEON/TOWN
ZA%(4) Experience Character global

(1) Array dimensions are given as the number elements.

(2) 0=Fonkrakis, 1=Abyss, 2=Wornoth, 3=enlightened, 4=power of light, 5=ethereal

(3) F=fire, I=ice, L=lightning, R=radiation, U=undead, M=psychic, P=poison, A=acid

(4) Above this level monsters flee, below it they attack.

Notes on Combat System

The sign of a number is sometimes used to select from two alternative combat scenarios. Once a negative number triggers an alternative, the sign is often changed back to positive at some point in the calculation.

Spell damage, WA, scales with level if positive. If negative, it does not scale with level. Note that attacks with wands, staves, and rods, are treated as spell attacks.

A weapon's to hit chance, WB, is subjected to a proficiency adjustment if positive. If negative, it is subjected to a spell failure test instead. Wands, staves, and rods, generally have WB negative. The messages that are displayed during the attack are also affected. The adjusted to-hit chance in every case is stored in WC.

The exact calculation of WC is subject to change from version to version for balancing purposes. As of this writing the calculation is

IF WB > 0 THEN WC = (WB - GA + AI%(A) + ZA%*2)*WU%(CL%(A),C)/9: WC = 100*WC/(80+0.4*WC)
IF WB < 0 THEN WC = ABS(WB): IF SC$ = "P" OR SC$ = "T" THEN WC = WC - GA
IF WC > 100 THEN WC = 100

The spell attack type SC$ takes values "I" for immediate damage, "T" for temporary status effects, and "P" for permanent status effects. Magical weapons use the same variable.

A weapon's range, WR, takes values -1,0,1. If WR=0, the weapon can only be used on the front lines. If WR=1, the weapon can be used from any range. If WR=-1, the weapon can only be used on the front lines, but can be thrown from the rear once, after which it is lost forever.

The multiple attack counter, MA, can also be positive or negative. If positive, multiple attacks will hit the same monster until it dies, and then proceed to the next monster. If negative, the attacks hit random targets, except for the initial attack.

The monster's base to-hit chance is in GE. This is used for both regular and spell attacks. The local variable I is set to a random number, and adjusted for armor and other protections, where the adjustments depend on the type of attack. The if GE>I the attack succeeds.

Special Considerations

Realm sacrifices a few features of Applesoft in order to work the way it does.

The STR$ function cannot be used, because the Realm machine code frequently writes to location $FF in zero page. This location is also used by the Applesoft intepreter, but only if STR$ is called.

The chaining system does not preserve string literals. When assigning a string that needs to be persistent, we use seemingly extraneous expressions, e.g., RM$ = "ARR"+"INEA", or AP$(A,L) = LEFT$(A$+" ",LEN(A$)), to force the interpreter to copy the string from program space to variable space. The following regular expression is useful when using a modern editor to find all AppleSoft string assignments:

[0-9:]\s+\w+\$\s*(\(.+\))?\s+\=

User defined functions DEF FN... are never used, as of this writing. Like strings, user functions exist in program space, but unlike strings, there is no simple way to move them to variable space. After chaining it might or might not be necessary to re-define the function, depending on what has been overwritten.

The garbage collection strategy employed in Realm is to run FRE(0) when swapping programs, and before intensive string operations.

Sprites and Shapes

Terrain maps are drawn using 14x12 bitmap sprites. In the Apple II high resolution screen buffer, a byte of memory is organized as seven binary pixels, packed horizontally, plus a color bit. Color is determined by the color bit along with details of pixel placement. The whole sprite takes 24 bytes. The pixels are stored as two columns of bytes, first the left 7x12 set of pixels, followed by the right 7x12 set of pixels.

Sprites are found packed in three files, OUTSPS (outside terrain), TWNSPS (town terrain and denizens), and DNGNSPS (dungeon terrain). There is no header or metadata, the sprites are simply packed in order of ascending terrain code (0-15). In OUTSPS this is followed by two sprites for representing the party, one for flying, one for walking. In TWNSPS, there is a only a sprite for a walking party, followed by sprites for 14 town denizens. In DNGNSPS the walking party is sprite 17. For some reason there is a townsman as sprite 16.

Monster artwork is drawn using basic lines, plus a standard Apple II shape table. The shape table is the basic brush set D0 in SDP. In Realm, the D0 data is actually embedded in DNGNSPS, at offset $200.

Machine Language Routines

Starting with v1.4, machine language routines are derived from MERLIN assembly language. These perform the same functions as the old Monitor routines, but with better efficiency, modularity, and extensibility.

Program SOUND

Address space: $300-$3B3

This is the code that produces the sounds. It also contains the stack repair program from the AppleSoft BASIC reference manual.

As of this writing the BASIC code directly calls RPRSTK, UPCHIRP, DNCHIRP, UPFAT, and SIREN.

Parameter Address Note
LOTONE $06 Period of the low tone in a chirp
HITONE $07 Period of the high tone in a chirp
CYCLES $08 Number of speaker pulses in a tone
REPS $09 Repetitions of a chirp or siren
RATE $1E Rate of change of the tone in a chirp

The effect of the parameters on the sounds can be explored using SOUND.TEST in the editor folder.

Program SDP.INTRP

Address space: $15AC-$163D

This is the artwork interpeter from SDP. It renders a squeezed SDP picture by stepping through a list of drawing operations. The only input is the address of the picture, stored in $06,$07. Zero page locations $EB-$EF and $FA-$FB are used as variables.

Program MAP.INTRP

Address space: $163E-1EFF

This is the most complex of the assembly language programs. It performs the following functions:

  1. Paint or scroll the player's current view of the map on the screen.
  2. Allow or forbid the player's movement into a given map square.
  3. Handle movement or death of town and dungeon denizens.
  4. Retrieve the pointer to a treasure trove appropriate to the player's location.
  5. Display the monologue appropriate to the player's location.

The map interpreter can be tested using WALK.ALL in the editor folder. This program can also serve as a terrain editor of sorts.

Realm Terrain

There are three basic types of terrain: outside, town, and dungeon. However, in a town, there is actually a fourth type of "terrain" used to display the town denizens. In particular, every grid point in the town has a corresponding grid point for the denizens (doubling the storage requirement).

Terrain is saved as a nibble (every bit is precious). The encoding is as follows:

Code Town Denizen Outside Dungeon
0 Food Guard Pyramid Stairs Up
1 Tree Human Forest Stairs Down
2 Pub Elf Mountain Earth
3 Shipyard Dwarf Swamp Corpse
4 Water Hobbit Water Water
5 Wall Adventurer Ether Stalagtites
6 Table Wizard Lava Table
7 Library Marine Castle Floor
8 Temple Dragon Volcano Door
9 Weapons Special Human Dormant Volcano Lava
10 Door Special Elf Door Chest
11 Warship Special Dwarf Warship Mordock
12 Fire Special Hobbit Iron Tower Trap
13 Road Special Dragon Dungeon Special Monster
14 Armour Corpse Town High Priest
15 Field Nil Field Monster

Format of Map Files

Each type of map consists of a packed sequence of nibbles which indicate the type of terrain located at a given grid point. The nibbles are arranged like a FORTRAN array, where the first index runs East, the second index runs South, and the third index runs down into the ground if you happen to be in a dungeon. The size of each type of map is as follows:

Type Size
Outside 80 x 80
Town 40 x 40
Dungeon 20 x 20 x 8*

* Interpretation of dungeon levels is subtle, see below.

Outside

The outside map files, ARRINEA, FONKRAKIS, ABYSS, and WORNOTH, consist simply of an 80 x 80 map. Ships are part of the map. With DOS 3.3, all parties share the same maps, and therefore also each other's ships. With ProDOS, each party has their own maps and ships. In either version, maps are auto-saved upon entering a town or dungeon, while other party data is never auto-saved (this can lead to an inconsistency, but it is tolerable).

Town

The town map consists of a 40 x 40 terrain map, a 40 x 40 people map, the town name, and monologues attached to certain town inhabitants. In particular,

Offset Data Length
0 Terrain Map 800
800 Name 50
850 People Map 800
1650 Monologues 120x7

There are seven monologues each occupying 120 bytes. They are associated with people codes 9-14 (“specials”) according to the order in which they appear in memory. Thus, there should be no more than seven specials in a town. The special must be replaced by a corpse if killed. Adding, moving, or deleting corpses outside of this construct will break the monologue system.

Some monologues start with a control character. This indicates that the monologue triggers a dialog subroutine. The control codes are:

Control Character Story Element Realm
1 Sage Arrinea
2 Archwizard Arrinea
3 Alchemist Any
4 Baron X Fonkrakis
5 Bakro Arrinea
6 Red prism Fonkrakis
7 Clear prism Arrinea
8 Prodigy Arrinea
9 Gaurdian Wornoth
10 Gatemaster Wornoth

Dungeon

The dungeon map consists of a 20 x 20 x 8 map, the dungeon name, and a list of treasure. The dungeon levels are not strictly limited to the nominal 20 x 20 rectangle, however. The dungeon interpreter views the dungeon as a single 160 x 20 (rows x columns) level, in which the function of a ladder is to teleport the adventurer by 20 rows, either north or south. The dungeon walls prevent the adventurer from simply walking between levels. Although it is not exploited much, this system allows for a complex level topography. It does require, however, limiting the user's view of the map, in order to maintain the illusion.

Offset Data Length
0 Map 1600
1600 Treasure 80x5
2000 Name 30

There are five treasure troves each occupying 80 bytes. Each treasure is a text description of several items separated by slashes. For example,

DAGGER/WAND OF FIRE/LIGHTNING ROD.

Each treasure trove is associated with terrain code 13, “special monster”. After a special monster is killed, it must be replaced by the corpse terrain code.

Realm Geography

There are four Realms: Arrinea, Fonkrakis, Wornoth, and Abyss. Travel between realms takes place at Pyramid Gates. To get inside a Pyramid Gate requires a special key, and travel through the gate is only possible with a special prism. The location of towns, castles, and dungeons, is as follows:

Arrinea

Name Type Coordinates
towne hobulus Town 13,67
the city of ophenius Town 45,15
towne vodarc Town 24,36
towne pleusoria Town 34,35
towne sondor Town 16,46
towne embius Town 45,27
towne moxalia Town 69,57
port simboria Town 30,65
towne amoria Town 61,29
towne raelton Town 38,70
tylvon harbour Town 42,9
towne kossarc Town 21,26
towne lodossia Town 60,50
the city of hethor Town 9,40
the city of mirrifius Town 63,12
enlightenment Town 8,21
castle trueblood Castle 19,32
castle lemphocym Castle 71,35
castle blackmoore Castle 45,8
castle nofpheaus Castle 34,69
the haunted chasm Dungeon 31,34
the gateway to death Dungeon 31,13
the dungeon of darkness Dungeon 51,18
the well of shadow Dungeon 43,38
the dungeon of nihm Dungeon 29,49
the dungeon of sedrik Dungeon 22,58
the devil's hole Dungeon 49,72
the cave of horror Dungeon 47,52
the pits of peril Dungeon 66,29
the hellish inferno Volcano 71,21

Fonkrakis

Name Type Coordinates
towne ghemalia Town 14,35
castle modrosia Castle 41,2
the black fortress Castle 36,48
the wicked pit Dungeon 20,31

Wornoth

Name Type Coordinates
Hellport Town 24,22
Damnation Docks Town 33,48
Inferno City Town 4,3
Iron Tower Castle 39,34
Forsaken Mine Dungeon 74,5
Dungeon of Mordock Dungeon 39,34

Abyss

Name Type Coordinates
Sulphur City Town 56,34
Famine Ridge Town 15,50
Vermin Village Town 26,64
Brimstone Bay Town 11,24
The Depths of Doom Dungeon 32,0
The Condemned Caverns Dungeon 42,39