Legendes Coding Standards

This document is the coding standards for both C and data files for the Legendes project.

Contents


Preamble

Legendes is an adventure game and also a meta game. In other words, it is not only an adventure game but it also features easily modifiable data files through which other games can be constructed at will, using the same executables.

Legendes is free software and is distributed under the terms of the GNU General Public License (either version 2 of the license, or, at your option, any later version) as published by the Free Software Foundation.

The founding authors of Legendes are (in alphabetical order):

Other authors are welcome to join in at any time. We can find a use for anyone, especially for those who can program in C and for artists.

The full name of Legendes is ``Legendes II'', with reference to an older game written in Turbo-Pascal for MS-DOS by Philippe Clavel, David Madore and Laurent Pénet with special contributions by Antoine Allain; this older game, ``Légendes'', is not maintained any more, but can nevertheless be downloaded (in source plus compiled version):

Legendes II does not make any further reference than its name to the older game. No element of source code shall be directly copied from the one to the other, though algorithms can be inspired by what was previously done.

As specified by the GNU General Public License, Legendes comes with absolutely no warranty, not even the implied warranty of merchantability or fitness for a particular purpose. The authors assume no responsability for any damage that may result as a consequence of running Legendes.

Contributions

The Legendes project can only accept contributions that do not conflict with the GPL on the project as a whole. In particular, the author of such contributions must allow their free distribution.

The authors recognize that the ``Ultima'' games by Origin, Inc, have motivated the development of Legendes and wish to salute their authors. However, the previous statement should not be interpreted in meaning that Origin (or anyone else) acquires special rights over Legendes.

Target architectures

The main and host architecture for Legendes is an Intel-based computer running Linux.

Philippe Clavel is in charge of porting and maintaining the program under Microsoft Windows 95/NT and possibly other similar architectures. David Madore is in charge of porting and maintaining the program under any recent version of Unix with X windows.

Actual coding conventions

Syntaxical conventions

Every C file (whether .c or .h) in the project must start with the following lines:
/* This file is part of Legendes II */
/* Copyright (c) 1998 by
 *   Philippe Clavel (philippe.clavel@supelec.fr)
 *   David Madore (david.madore@ens.fr) */

/*
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version
 * 2 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty
 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
 * the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
followed by a description of what the file contains and does (see the file lgd0.h for an example) and then a ^L character on a line by itself.

If any more authors join the project, their names will be added, in alphabetical order, to the copyright notice of each source file (not just the ones the author has contributed to). If the list of authors gets too long (hope keepeth man alive :-) the copyright notice will be replaced by the following:

/* Copyright (c) 1998 by the Legendes team. */
In any case, the names and addresses of the authors are kept (also in alphabetical order), together with a description of their contribution to the project, in the top-level AUTHORS file.

The preferred program for editing all the files of Legendes is GNU emacs.

Writing comments

Comments are of two kinds: short comments that fit on the same line as that which they describe, and long comments that (can) span several lines. Long comments precede what they comment; they are formatted exactly as follows:

/* This is a long comment. Long comments can span several lines. The
 * first line begins with a slash on column zero, an asterisk on
 * column one, and a space on column three. Each consecutive line
 * begins with a space on column zero, an asterisk on column one and a
 * space on column three. The last characters in the comment, at the
 * end of the last line, should be space, asterisk, slash. Lines in a
 * comment are made as long as possible so as not to exceed 70
 * characters, including the three first space-star-space characters.
 * To summarize, comments are formatted exactly as emacs does it. */

Formatting C source

Indentations in C are two characters wide. Four levels of indentation (eight spaces) may optionally be replaced by one ascii tabulation (^I).

Each brace level gives one level of indentation. if and similar statements also give one level of indentation (but for one line only). Here are a few examples:
(on one line):

if (t==0) printf("Zero!\n");
else printf("Non zero.\n");
(on two lines):
if (t==0)
  printf("Zero!\n");
else
  printf("Non zero.\n");
(with braces on the same line):
if (t==0) {
  printf("Zero!\n");
} else {
  printf("Non zero.\n");
}
(the GNU way):
if (t==0)
  {
    printf("Zero!\n");
  }
else
  {
    printf("Non zero.\n");
  }
Any of these presentations is tolerable. In this particular case, the first is probably the best since the condition is really simple. Use the GNU way only for really complex conditions (I don't really like it because it needlessly abandons one level of indentation). Never put two statements on the same line (such as the if and else statements) except if you writing many very short lines in a row and you want it to look nicer. Also, the third and fourth conventions above should not be mixed: if you put the brace on its own line in the if statement, then you must do the same for the else statement.

Also, in the case of nested if/else statements, always introduce the necessary braces to remove any ambiguity as to which if each else is referring to.

A function declaration looks like this:

void
dumb(int stupid)
{
  /* Many fascinating things. */
}
Note in particular that the name of the function should start in column zero of one line, and the return type is on the previous line. The function then starts with an open brace alone on column zero of one line. The prototype, on the other hand, is like this:
void dumb(int stupid);

You must put a space before an open parenthesis that follows a reserved word: if, while, for. On the other hands, function calls can use either the GNU convention

function (arg1, arg2, arg3);
or the compact convention
function(arg1,arg2,arg3);
This is mainly a matter of taste. I prefer using the compact convention when the arguments are short enough (not so much a question of length as of complexity, really).

No line should ever exceed 79 characters (where a tabulation counts for eight characters of course).

If a source file gets too long but it is not possible or not desirable to divide it in smaller files, it can be divided into sections by putting an ASCII form feed character ^L on a line by itself at section break.

Naming things

Preprocessor #defines should have names all in upper case. C identifiers, on the other hand, should have names all in lower case.

The use of the underscore to separate words in an indentifier is still an unresolved question. Generally speaking, use them; for example, write kill_hero() rather than killhero(). On the other hand, there are some cases where the underscore is undersirable, in particular for short names, which are not part of a general family with similar names, if the two parts of the name are very closely linked; for example, donothing() and int didit seem better than do_nothing() (which seems to imply that one should actually do something called ``nothing'') or int did_it. For data structures, a rule of the thumb is to use long names with underscores for public variables and short ones without underscores for local variables.

Do not use any articles in names, i.e. don't write kill_the_hero() but rather kill_hero(). Also, use the imperative form on verbs, not the third person singular (don't write kills_hero()).

If several functions have a similar effect, make sure they have similar names. Try to be as systematic as possible in that respect.

typedef names should end in _t (one advantage of that is that emacs' syntax highlighter knows how to deal with that), structure names in _s and union names in _u. Thus write for example

typedef struct junk_s {
  char *name;
  int x,y,z;
} junk_t;

Local variable names should be short and easy to type. Thus, calling i a counter in a function is OK.

Variables global to one module should be rare and precious. Variables global to the whole project even more so. They should have rather long and self-explanatory names, but that does not dispense you from putting a comment before them explaining what purpose it serves.

Keeping a log of changes

The file where the logs are kept is ChangeLog. Its format is as follows: the first line of a group of changes contains the date in YYYY-MM-DD format, two spaces, the name of the person making the changes, two spaces and the person's canonical e-mail within angle brackets < and >; the list of changes then follows, the changes being separated by empty lines. Each change description starts by a tab followed by an asterisk and a space and the description of the change justified on the first 70 columns, each line beginning by a tab. If the change refers to a particular file, start the change description by the file name followed by a colon. If it refers to a particular function within a file, start the change desciption by the file name followed by a space then the function name within parentheses and a colon. Also, change log entries are kept in reverse chronological order, with most recent entries first. This is exaclty what emacs does (what a coincidence :-) so the best thing is simply to use it.

Semantic conventions

C is ANSI C minus the trigraph feature. I'm not saying be fanatic about it either: if it turns out that about every reasonable C compiler has a feature that is really useful and that is not part of ANSI C, then it might be wise to use it. However, a priori, don't nest functions or use any similar non-ANSI constructs.

The clean code condition will be: compiling the program with gcc -Wall must produce no warnings.

Functions and what they should do

Before writing a function, decide exactly what it should do, describe that in a comment, and make it do exactly that (and not something else also that you think might be useful at that point). Try to make the function as context-independent as possible: try to imagine that somebody else, writing a totally different program, might want to call that function and it should work in that case also. In particular, it is sometimes wise to pass to the function some variables even though they are actually global variables and the function could very well read them directly from the data region.

In principle, it should never be necessary to describe how a function changes the program global variables: global variables should always be equal to whatever they should be equal to, and the function should modify them if and only if that is part of its supposed action. For example, a teleport_hero() function would obviously modify the global position variables, so there is no need to say so. Of course, if there is anything subtle about something, you should say so.

Avoid using global variables from within a function if they are not obviously necessary; failing to do that will introduce dependencies that are hard to track.

Never ever pass a structure or union as an argument to a function: always use a pointer to that structure or union instead.

It is all right for a function to return a pointer to a malloc()ated block of memory provided that the comment explicitely states so. In that case, be very careful not to loose track of the block in question and to free it whenever the pointer to it ceases to be visible. Otherwise, a memory leak might occur (and these are especially difficult to pin down). Most importantly, never, ever, ever, use = (between pointers) when you should be using malloc() followed by memcopy(). If you start doing that, you will end up free()ing the same block of memory twice and that is BAD. Here is an example of this error (do NOT imitate):

/* Return a malloc()ated idiotic string. */
char *
get_stupid_string(void)
{
  char *s;
  s=malloc(1729);
  /* Wonderful code. */
  return s;
}

void
wrong(void)
{
  char *s;
  s=get_stupid_string();
  /* ... */
  first_structure.stupid_string=s;
  /* ... */
  second_structure.stupid_string=s;
  /* ... */
}
Instead, the function wrong() should look like this:
void
wrong(void)
{
  char *s;
  size_t i;
  s=get_stupid_string();
  i=strlen(s)+1;
  /* ... */
  first_structure.stupid_string=malloc(i);
  memcpy(first_structure.stupid_string,s,i);
  /* ... */
  second_structure.stupid_string=malloc(i);
  memcpy(second_structure.stupid_string,s,i);
  /* ... */
}
(Except that this is also not wrong because the return value of malloc() should be tested.) Similarily, never assign to a pointer that might be free()d a value that points to a static area. So s="Hello, world!" should be looked upon with caution...

Comments inside a function should be limited: when one knows what a function does, understanding how it does it should be obvious (otherwise the function is badly written). The only thing that justifies a comment is a subtle point or hack, for example /* This is not a bug */, or /* libc is buggy so we have to use this instead */ or some such thing.

Be especially wary of cut and paste: if you are blindly copying a block of code, you probably should use a function instead. If you are copying the same portion over and over again and making small changes, you should probably use a constant array.

Using library functions

The Golden Rule, of course, is always to test for the return value of every function. Even close() and write() fail sometimes. The only tolerated exception is functions of the printf() family when writing to the standard output or the standard error. By the way, it is not necessary to typecast a function such as printf() to void just because you don't use its return value.

If you detect something that shouldn't happen, don't try to correct it. The proper thing to do is to exit with a fatal error. This applies to most error conditions but not all. If scanf() (or especially sscanf()) does not return the correct number of arguments scanned when operating on user input, the user made a typo and that doesn't justify emergency termination: just prompt for the answer again.

The main part of the program should only call very standard library functions anyway. Anything a bit exotic should be encapsulated in a specific function and surrounded by tons of #ifdefs. That makes the whole thing easier to port. Thus, the wrappers in question can do their own error code testing and the main program does not have to worry about it. The wrappers for the basic library functions (memory allocation, random numbers, file handling, and so on) go in lgdlib.c.

Coding of data files

All binary data files are stored in little-endian format. All numbers are 32-bit (4 bytes) wide and are aligned on 4-byte boundaries. The functions lgd_read_littleendian_int32() and lgd_write_littleendian_int32() (in lgdlib.o) are designed to read and write such numbers in a portable fashion. Use them! The reason we chose the little-endian convention (apart from the fact that people who eat their eggs from the big end are dangerous criminals and should all be killed :-) is the fact that our host architecture (and also a rather common architecture) is the Intel-PC. The reason we made our data files binary rather than text (as is getting more and more common in the computer world), by the way, is that we want them to be easily seekable.

Generally speaking, all the data files should be ``pure''; that is, contain only one kind of data all along. For example, we do not want a data file that starts with an index table to the data in that file; rather, we would put the index table in a different file. This makes everybody's life much easier

Also, we do not make any effort toward compressing the data files: as Péter Horvai judiciously pointed out, compressing data files is stupid: modern disks are large whereas time is always precious, and you can only manage to make the file more difficult to access and you will use a bad compression method anyway, which will make it harder to compress the file further.

The map file

Each map level is a 256*256 array, but provisions will be taken to make that number modifiable in the future. Map levels are stored consecutively, the first one in the file being number 0 (the order is of course completely arbitrary, but we like to think of level 0 as being the main level, and also perhaps the start level).

Each map entry is a 32-bit number called a chunk number, which indicates what is actually there: a chunk is a 16*16 array of elementary map squares, and the chunks are described in the chunk file and indexed in the chunk index file.

To summarize, the size of the map file is 256*256*4=256k times the number of levels.

Each level reads from top (north) to bottom (south) and from left (west) to right (east); in other words, the first chunk in each level is the northwestern-most one, the second one is just to the east of it, and so on (after the first line one starts going south). This is a bit figurative since the world is cyclic.

The map file is (unsurprisingly) called map.

The chunk index file

The chunk index file is used to convert a chunk number (or index, as found in the map file) to an address in the chunk file where the chunk data is actually found. The point of this is to allow a more flexible numbering of chunks (so that, for example, many variants of a given chunk can differ by the low-order byte) rather than necessarily 0,1,2 and so on. Better yet, the data in the chunk file can be reused in subtle ways: two chunks with different numbers can have identical data, or a chunk can partially overlap another one (I'll let you imagine all the interesting effects that can achieve). Still, there are two conditions, one avisory and one compulsory, that should be met by the addresses in the chunk file: first, ``mainstream'' chunks (leaving aside any special effects) should have an address multiple of the size of one chunk, viz. 1k. Second, every address must be a multiple of one chunk line, namely 64 bytes. Incidentally, this leaves us reserved bits per entry in the chunk file.

The format of a chunk index entry is very simple: the first four bytes are the number of the chunk and the four next are the address in the chunk file, expressed in bytes (not four-byte words), and which must be a multiple of 64 as we mentioned. Of course, that also means that the chunk file may not be larger than 4 gigabytes, but I honestly don't think that's a big limitation (yes, that's only four million chunks instead of four billion).

The chunk index file is called chunkidx.

The chunk file

Each chunk is coded as a 16*16 array of 32-bit integers (elementary squares). Thus, the size of a chunk in the chunk file is 16*16*4=1024, that is one kilobyte. Of course, the chunk file is not really an array of chunks since the chunks don't have to start on the kilobyte boundary (they are indexed in the chunk index file), and the procedure for reading a chunk is to read the 1024 bytes that follow the address found in the chunk index file. As mentioned about the chunk index file, however, chunks can only start on 64-byte boundaries and most of them should start on the normal 1024-byte boundary.

Legasm files

Legasm is a small portable Legendes-dedicated assembler, not unlike the native assembler of some RISC processors in some ways. It exists in source form and in byte-compiled form. It is the latter which is interpreted by the Legasm Virtual Machine. The former must be compiled (assembled) before use.

The Legasm virtual machine handles exclusively 32-bit words. It has 256 registers, numbered R00 through Rff. Register R00 always contains 0, register R01 always 1, register R02 always 0x80000000 and register R03 always -1; registers R04 through R07 are reserved for future use. Register Rff is the program counter (instruction pointer), register Rfe the stack pointer and register Rfd the flag register; registers Rf0 through Rfc are reserved for future use. All other registers are general-purpose. The instruction set is completely orthogonal (that is, any instruction can take input from any register and put the output on any other register) even as far as the ``special'' registers are concerned (but trying to write to R00-R03 will be a no-op). The R8x registers, in particular R80 and R81, are used for all kinds of communication operations; for example, a typical legasm routine will take its input from R80 (typically, an instruction) and R81 (typically, some data) and write the result to R80 and R81. This is of course by no means necessary, it is just a useful convention. However, all service calls will read the function number from register R80.

The Legasm virtual machine uses a flat memory addressing scheme; in other words, all memory is viewed as a flat array, with addresses ranging from 0x00000000 to 0xffffffff, each address containing one machine word (32 bits). The lower part of the memory, starting with 0x00000000, contains the program, read from the disk file, and which is read-only as far as the Legasm virtual machine goes. The actual RAM starts at 0x80000000 and in the present version goes up to 0x80000fff (that makes 4 kilowords, or 16kb of memory). Plus, there is a grow-down stack which occupies the top of memory: the stack pointer (register Rfe) points to the most recently pushed word and decreases before a new word is pushed on the stack. The size of the stack is dynamically ajusted by the legasm interpreter, though the actual mechanisms are left unspecified: we only specify that a program that uses push to put things on the stack will not run out of stack space (provided, of course, it doesn't try to push more than a million or so words). In the present implementation, it is the push and pop operations which are magic and change the size of the stack, though that may change sometime. Anything outside the aforementioned addressing areas contains read-only zeroes, though we must leave that unspecified because we may want to add more addressing areas in the future. But reading anywhere in the address space is always valid (there is no kind of protection mechanism in the Legasm virtual machine). The executable area is not limited to where the program resides: it is perfectly legal for a legasm program to write some code to RAM and then execute it (or even execute things on the stack, but that is not advised).

Every instruction of the Legasm virtual machine sits on one machine word. There are no exceptions. Typically, the first hex digit (4 bits) gives the instruction type, the second hex digit (4 bits) the subtype, the next two and two hex digits give the two input registers, and the last two hex digits give the output register. For example, the instruction 0x24212210 means cmpae r21,r22,r10 (compare registers R21 and R22, and put 1 in R10 if R21 unsigned is above or equal R22 unsigned, 0 otherwise) and is decoded as follow:

          00100100001000010010001000010000
   compare^^^^   |       |       |       |
above or equal^^^^       |       |       |
      register r21^^^^^^^^       |       |
         with register r22^^^^^^^^       |
             and put result in r10^^^^^^^^

Now to get on to the acual opcodes.

Opcode 0x0... is an arithmetic or logic operation. It is coded as follows:

          0000[op][input1][input2][output]
where [op] has the following values:
BinaryHexMnemonicMeaning
00000x0 addadd
00010x1 adcadd with carry
00100x2 subsubract
00110x3 sbbsubract with borrow
01000x4 andlogical and
01010x5 orlogical or
01100x6 nxorlogical inclusive and
01110x7 xorlogical exclusive or
The carry bit is the bit zero of the flag register (register 0xfd). It is set to one when the result of a sum exceeds 0x100000000 or when the result of a difference is negative. The adc operation means ``add the two operands together with the carry bit'' and is used to add numbers that are more than 32-bit wide. Similarily for the sbb operation.

Note that an instruction that starts in

          0000000000000000[source][destin]
means add r00,source,destin. In other words, it adds zero to ``source'' and puts it in ``destin'', which means moving ``source'' to ``destin''. So that instruction is how we get a mov operation on the Legasm virtual machine.

A sub-particular case is the instruction

          00000000000000000000000000000000
(i.e. 0) which adds zero to zero and puts the result in zero. That is the nop instruction.

Opcode 0x1... is a memory operation. It looks like this:

          0001siml[ data ][ base ][index ]
    load/store^| |
direct/indirect^ |
  index multiply^^
Bit 27 is 1 if the operation is store in memory (st mnemonic) and 0 if the operation is load from memory (ld mnemonic). Bit 26 is 0 if the addressing mode is direct (the base and index registers are added to provide the actual address) or indirect (the index register is added to the contents of the memory located pointed by the base register to provide the actual address). Bits 24 and 25 specify by how many bits the index register should be shifted to the left to provide the actual index; use is deprecated (and may be removed some time in the future).

Opcode 0x2... is a comparison. This is its structure:

          0010[cp][input1][input2][output]
where [cp] has the following values:
BinaryHexMnemonicMeaning
00000x0 cmpeqtest if equal
00010x1 cmpneqtest if not equal
00100x2 cmpintest if all bits of input1 are in input2
00110x3 cmpouttest if no bits of input1 are in input2
01000x4 cmpaetest if above or equal (unsigned)
01010x5 cmpacompare if above (unsigned)
01100x6 cmpgecompare if greater or equal (signed)
01110x7 cmpgcompare if greater (signed)

Opcode 0x3... is a jump or branch. It is coded thus:

          0011rnRc[ cond ][ destination  ]
       imm/reg^|||
         bz/bnz^||
         abs/rel^|
        jump/call^
Bit 27 is 1 if the destination is a register whose number is specified in bits 0 to 7 (in which case bits 8 to 15 are reserved), 0 if the destination is an immediate 16-bit quantity (signed where relevant) contained in bits 0 to 15 of the instruction. Bit 26 is 0 if the jump condition is Rcond==0 (written bnz), 1 if the jump condition is Rcond!=0 (written bz). Bit 25 is 0 if the jump is absolute (destination replaces pc; if destination is immediate, then it only replaces the low-order bits of pc, that is, destination is in the same 64kword page as origin), 1 if the jump is relative (destination is added to pc; if destination is immediate, then it is considered as signed for this purpose). Note that the pc in question (register Rff) is the pointer to the instruction FOLLOWING the jump instruction (so that to produce an endless loop, the appropriate instruction is 0x3200ffff, whereas 0x32000000 is a no-op as well as 0x380000ff). Finally, bit 24 specifies that the previous pc should be pushed on the stack before performing the jump; in other words, that the instruction is a call (cz and cnz mnemonics).

There are no unconditional jumps; rather, use the R00 register to that effect. That is set bit 26 to 0 and ``cond'' to zero also. The appropriate mnemonics are then jmp and call.

Opcode 0x4... is a bit shift/rotate operation. It works like this:

          0100r[t][source][count ][destin]
       imm/reg^
Bit 27 is 1 if count is a register (actually, only bits 0-4 of that register), 0 if count is immediate (bits 8-12; and bits 13-15 are then reserved). The meaning of the [t] field is as follows:
BinaryHexMnemonicMeaning
0000x0/0x8 shrshift right, unsigned
0010x1/0x9 shlshift left
0100x2/0xa sarshift right, signed
1000x4/0xc rorrotate right
1010x5/0xd rolrotate left
This is self-explanatory, except perhaps for the difference between shr and sar: the point is that the former shifts the bits right by adding zeros on the left (in other words divides the unsigned quantity by a power of two) whereas the latter shifts the bits right by duplicating the most significant bit (in other words divides the signed quantity by a power of two).

Opcode 0x5... is an immediate load. In binary, it is coded as

          0101[md][ reg  ][  immediate   ]
where [op] has the following values:
BinaryHexMnemonicMeaning
00000x0 lilload low-order bits
00010x1 lihload high-order bits
00100x2 lilzload low-order bits and clear high-order
00110x3 lihzload high-order bits and clear low-order
01100x6 lilsload low-order bits sign-extend
10000x8 ailadd immediate
10010x9 aihadd immediate to high-order bits
11000xc ailsadd immediate, sign-extended
The lil and lih instructions are used to position the low (resp. high) order bits without modifying the other half. They are most frequently used consecutively to load one entire 32-bit immediate quantity in a register. For example, the two instructions 0x50105678 0x51101234 (in any order) will load the 32-bit quantity 0x12345678 in the register R10. The lilz and lilh instructions are used to load the low (resp. high) order bits while clearing the other half, in other words to load in one single instruction a small quantity (resp. a multiple of 0x00010000) in the register. The lils instruction is the same as lilz but it will use sign-extension to the high-order bits rather than zero-extension. The ail instruction adds the given, unsigned, quantity to the register (so 0x58100001 performs the same as 0x00011010). The ails instruction does the same but with a signed immediate quantity. Lastly, the aih instruction is used to add a multiple of 0x00010000.

Opcode 0x6... is push. It is this:

          011000000000000000000000[ reg  ]
and it pushes the specified register on the stack. Note that in case the sp register (Rfe) itself is pushed, it is the value before the push that is used, not after.

Opcode 0x7... is pop. It is this:

          011100000000000000000000[ reg  ]
and it pops the specified register from the stack.

Opcode 0xf... is a svc. It is this:

          11110000[ registration number  ]
This is a service call. Registration number 0 means stop the program. Registration number 1 means print the contents of register R81. Other registrations numbers are yet undefined.

Here is an example of a legasm program with both binary and mnemonic versions:
0x00000181mov 1, r81
0x00018181mainloop:add 1, r81, r81
0x00000182mov 1, r82
0x00018282tryloop:add 1, r82
0x24828110cmpae r82, r81, r10
0x3610000dbnz r10, +0x000d (print)
0x00008121mov r81, r21
0x00008222mov r82, r22
0x02212221gcdloop:sub r21, r22, r21
0x24212210cmpae r21, r22, r10
0x36100003bnz r10, +0x0003 (orderok)
0x00002130mov r21, r30
0x00002221mov r22, r21
0x00003022mov r30, r22
0x21002210orderok:cmpneq 0, r22, r10
0x3610fff8bnz r10, -0x0008 (gcdloop)
0x20012110cmpeq 1, r21, r10
0x3610fff1bnz r10, -0x000f (tryloop)
0x3200ffeejmp -0x0012 (mainloop)
0xf0000001print:svc 1
0x3200ffecjmp -0x0014 (mainloop)
What this does is obvious, isn't it? It calculates prime numbers...


David Madore