Project: M.u.l.e.

Wokani came up with a great idea about trying an open-development approach for spectrum games, and I thought why not try with a game I have under development now that needs a final push to get done.

His original idea was to start a game from scratch and have it progress as a kind of tutorial with everyone making contributions and learning as the project went on. This project will be a little different as you'll be entering in the middle to late stage of development. I also wouldn't call this a "beginner project" in the sense that this is a "real" game and not a project designed to be easy to accomplish. However, as this project is in C, I think beginners (even those only familiar with basic) will be able to pick up on how things work.

A FEW WORDS ABOUT THE GAME

MULE is a bit of a cult classic on the C64 and Atari 8-bit computers. In my opinion it is one of the better 8-bit games made in the '80s and it is a crying shame it never saw a conversion to the spectrum. As this is such a good game it deserves a cracking conversion and I expect everyone to be picky about graphics, sound, code and game responsiveness so that we end up with something to be proud of.

Here are some links to begin with:

The introduction and game tune:
http://atarimule.neotechgaming.com/

The game's original manual:
http://muttoo.on.ca/mulemanual/

And, yes, the game is popular enough to have forums and a following on the web:
http://www.worldofmule.net/tiki-index.php

MULE is not an action game, but a game of economic domination. You are transported with four other players (computer or human, but always four) to the planet Irata for colonization. Your task is to develop the lands of Irata in order to maximize your economic value by the end of the game (12 turns or years). Nominally you want the value of the entire colony to be high in order to please Earth, but really you just want to clobber your fellow colonists.

Each turn progresses through several stages:

1. A land grant where players competitively select one plot of available land.
2. Land Auctions where players or the colony may sell plots of land.
3. Development where the player can develop his lands to produce one of four colony goods: food, energy, smithore or crystite. Not all lands are equal in their production capacity; river plots, eg, are better at producing food whereas mountainous plots are better for smithore mining.
4. Random events, good and bad. Shit happens, as they say, and sometimes it's good and sometimes it's bad. Sometimes it only affects you (your mule goes crazy and leaves one of your plots undeveloped) and sometimes it's colony-wide (an acid rainstorm increases food production by 4 and decreases energy production by 2 along the row of plots it strikes).
5. Goods production where player developments generate goods for the turn. The amount of goods each plot produces depends on the production potential of the plot for that good, a random element, economies of scale and learning curve theory.
6. Auctions where you have an opportunity to buy, sell and trade in the colony's goods. You as a player must make sure you have sufficient food (time for plot development) and energy for your production mules for the next turn. The store must have some smithore to continue producing MULEs, the basic workhorse of plot developments. No smithore, no MULEs, no plot development. It is at these times you can choose to corner the market in goods and manipulate market prices for your own benefit. Or you can be nice.
7. Standings are shown at the end of each round so everyone knows their place in the game.

Read the manual above for more details.

DEVELOPMENT

This game is fairly far in development now but there are still plenty of interesting things to do, including display layout, some game stages and the whopper: proper AI for computer players. I am hoping that some of you will be able to contribute to the game and see it to completion.

The game is written in C and uses the SP1 engine for sprites. The SP1 engine divides the screen into character squares, each of which can contain one of 256 character codes as background. These can be things like the letter 'A' or a fancy UDG graphic you've designed. Unfortunately this is not a perfect match for MULE. In MULE the planet map is a 9 row x 5 column array of plots. Each plot is 3x3 chars in size (24x24 pixels). The graphics for each plot consist of:

* background depending on terrain type (river, plain or store)
* mountains (randomly generated for each game)
* player development (none, farm, solar collector, smithore mine, crystite dig)
* production capacity dots (small dots below the development graphic showing the production potential the plot has for that development)
* player colour showing ownership
* real production dots (larger dots showing actual production for the turn)

All possible plot graphics cannot be generated in just 256 UDGs. Instead plot graphics must be drawn in a buffer (72 bytes for 9 UDGs per plot) from which SP1 will read background for each plot.

Additionally, we have the need to print double width text and scroll double width text messages.

Therefore I decided on a mixture of graphics output routines. SP1 will do sprites and some backgrounds, some routines will be written to directly draw to the display for text and doublewidth text, and code is provided that draws plot graphics into memory buffers from which SP1 can read background information.

ORIGINAL GAME INFORMATION

What has made this game infinitely easier to port is that kroah has taken the time to decompile it and place notes on the web on how the game works. Almost everything is in his document (minus some important bits of AI) and I am using that as reference. Here is the site:

http://bringerp.free.fr/RE/Mule/news.php5

Click on decompilation to see his notes. This, together with the manual and a video of the game being played are enough to make an accurate reproduction of the game.

GETTING UP TO SPEED

The project contains around 3000 lines of source, not including graphics and sound. I think the best way to get people up to speed quickly is if I present things in a few stages. Don't let the size worry you too much -- it should be fairly easy to keep the whole in mind as everything is neatly divided into modules and functions use names that should make it obvious what they are doing.

The approach I took in designing the game will be the same way I present it. Since this is not an original game but a port, the process was a little different. It started by reading kroah's documentation several times over and getting from that a bunch of small subroutines and data structures (variables for plots, etc) that would need to be written for the game. This is called bottom up development where we identify and write a lot of fundamental building blocks (subroutines) that we'll need for the game.

With the low level stuff there, it's a lot easier to write the game as a whole using those building blocks. Next step is moving to the high level where we'll be interested in making the game run with display elements, moving through the player rounds, individual stages in each run, and putting together the game logic from those building blocks.

WHAT YOU'LL NEED

The game is being compiled using the next version of z88dk (1.7) due for release on July 15. Things have changed since the last snapshot I posted last month, so I will post another snapshot should we need it before July 15. I tend to write almost the entire project before even trying to compile so I don't think I'll need to do that, but if there is demand I will do it.

A TASTE

But before we get into it, I think a taste of things to come and some exposure to C under z88dk will be helpful to get things rolling and whet the appetite.

The game requires two text-printing routines: a regular 32 column text printer and a double-width text printer.

The 32 column printer needs to be a little bit special: it has to be able to print multiple lines (you can store many lines in a string, with each line terminated by a \n character -- \n is ascii code 13) and it has to be able to left, center or right justify text in a particular line. These requirements come from watching the game play on video and I think it's safe to take my word for it for now :-)

The double-width printer also has to be somewhat special. We want to be able to print double-width text to screen occasionally but the game also scrolls double-width messages across the bottom of the screen. So what is needed is a routine that will print double width text to either the screen or to a buffer in memory. The scroller reads the rendered double-width text bitmap from the buffer and scrolls that on the screen.

The code and a compiled test program can be downloaded from here.

Run the tap file. The first test is the "32 column" string being printed at random locations in either left, center or right justification. Press a key to move to the next test.

The next test shows double width text being printed. Press a key to move to the final test.

The final test shows a multi-line string being separated into individual lines and being printed in separate lines, with each line individually centered.

The routines are not all-powerful and have some limitations (one being the text must not wrap across the right side of the screen) and there are two minor hacks in the code to circumvent a couple of problems in the current z88dk that is being fixed for the 1.7 release.

I'll leave the code with you for now and come back to explain it later, unless someone else wants to do it before then. Don't panic if it makes no sense to you -- it will once it's explained, even for the basic people. The code will only compile with the current cvs version of z88dk because of several additions and changes.

I don't foresee the rest of the project to be presented in this manner, but I thought this would be a good start if we went over a couple of interesting subroutines in fine detail to make people familiar with how z88dk C works and to create some exposure to z88dk's spectrum libraries.

Have fun!

Edit: Ah, yes, the www.z88dk.org/wiki is your friend for some of the libary calls. Unsure how memset(), strtok() works, look it up under the strings library. Also look up the new spectrum.h library for the strange screen address manipulators seen in this program (zx_saddr2aaddr, etc)
Post edited by Alcoholics Anonymous on
«1345

Comments

  • edited June 2007
    Source and compiled tap program can be downloaded from here.

    In starting off this experiment I think explaining in fine detail a couple of subroutines of interest would be a good first step to ensure that everyone has the tools to read and understand the source code. You'll also get to see the project's coding style and learn something about z88dk C (for those familiar with C on modern machines, it's quite similar -- it should be as it's nearly ansi-C-- and you'll see that my coding style is not much different from what I would use on a modern machine, though I do keep the z80's -- and z88dk's -- limitations in mind). Since this first snippet of code contains pointers and different size data types, people unfamiliar with C will be getting a crash course in two of the key concepts in C programming (or any other modern language). This is something that usually has to be beaten into undergrads new to computers over the freshmen year and you get to pick it up in a half hour. Hurrah :-D

    After this introductory exercise I think I will just make the entire code of the project available in multiple stages. The first stage will contain the majority of low level game logic and data structures which will be explained in a high level manner rather than in detail, unless there is something tricky in there. I will be cleaning up the code as I add stages to the project so mistakes may sneak in there but hopefully not many.

    If people ask questions about how something works or have suggestions in making things better we can discuss parts in detail. Some stages will not be complete and this will be an opportunity to contribute, and I think I will even introduce code that needs to be written fairly early on so that there is more contribution going on than one-way exposition.

    If wokani or anyone else wants things to move along differently, I'm entirely open to this. I think the main goal should be getting people able to participate early on. I think like everyone here I do have doubts whether there will be sufficient interest to develop in this way but it's such a good idea I think it's at least worth the attempt.

    The following tutorial is for pure beginners and hopefully this will be the last time things are all preachy. It certainly ended up much larger than I intended and was a lot of work too. Grrr.

    A FEW COMMENTS FOR C BEGINNERS

    C is a very small language, even smaller than Sinclair Basic. There is something like a couple dozen "commands" built into the language. On its own, not much can be done in C. This is because there is no built-in way to read input (eg from the keyboard) or write output (eg print text to the screen). Instead C is extended by the standard libraries, with each library adding some machine-specific functionality but in a cross-platform manner. "stdio", for example, contains a printf() function that allows printing to the display. printf() works the same on all machines with a C compiler but the implementation of printf() is different on every specific machine. printf(), eg, is written for a z80 machine with a crazy display layout in mind in the Spectrum's case whereas printf() for the C64 is written in 6502 for a different crazy display layout. Nevertheless C programs using printf() can be compiled and run on both the Spectrum and the C64 with no changes.

    There are several standard libraries. There is "stdio" which contains functions for reading input (eg keyboard) and writing output (eg to the screen). There is "stdlib" which contains functions for converting numbers to strings, strings to numbers, generating random numbers, etc. There is "string" which contains functions that can determine the length of a string, put two strings together, compare two strings alphabetically, etc. And more besides these. Every C programmer is familiar with what is available through these standard libraries and every C text will describe these libraries in some way. You can see what functions are available in these libraries in z88dk's wiki. This is stdlib, for example. z88dk's implementation of the standard libraries is more limited than the ansi-defined standard but you won't be missing much.

    C is very extensible. You can define new "commands" (functions from now on) simply by declaring them. This is how the new functions supplied by stdlib, stdio, etc are added -- a C program must include a list of function prototypes that tell the compiler how to call the function. A function prototype is just a function's signature that shows what parameters it expects and what value it returns. It doesn't have to include any actual code. The code is stored elsewhere. An example:
    int my_function(int a, int b);
    
    int my_function(int a, int b)
    {
       return a+b;
    }
    

    The first line above is a prototype; after your program declares this your code can call my_function() happily. Somewhere in your project you will actually have to define my_function()'s actual code and that is the second bit. As you can see, my_function() takes two integers as parameters, adds them together and returns the result.

    The standard libraries make available header files that contain all the function prototypes for the commands they add. If your program needs to use one of those functions, it will need to include the header file -- this is why you'll see a list of include directives leading every C program. The actual code for stdlib, stdio, etc are packed away in libraries. When compiling your program you must link to those libraries so the assembler knows where to pick up the actual code for those functions.

    You can add your own functions as well using the same technique. In any non-trivial C program, big programs are broken up into units of header file / source code pairs (a .h file and a .c file) with the header file containing function prototypes and the source file containing the actual function code. Any part of the C project can call a function in another unit simply by including the corresponding header file so that the compiler knows how to call the function. You will see exactly this in this MULE project and it will all be very clear once you've seen it.

    And, besides the standard libraries, z88dk also has a bunch of non-standard libraries that can be included in a program. Things like graphics commands, sprite commands, commands for reading the keyboard/joystick/mouse, etc. Since these are non-standard you can't expect other C compilers to support them; they are only available through z88dk. You'll see some of these used in MULE as well.

    C VARIABLES

    In Basic you have integer, floating point and string variables. Sinclair basic hides the distinction between integer and floating point and lets you simply declare numbers with the basic interpretter automatically converting to integer / floating point as needed.

    In C things are much closer to assembler and you manipulate variables of several types that are linked to actual bits and bytes in memory. C provides char (8-bit signed value -128 to 127), unsigned char (8-bit unsigned value 0 to 255), int (16 bit signed value -32768 to 32767), unsigned int (16 bit unsigned 0 to 65535), long (32 bit signed), unsigned long (32 bit unsigned), float and double. The C standard defines *minimum* sizes for each type above; what you see above are z88dk's sizes for each type.

    As you can see, in z88dk chars are 1 byte, ints are 2 bytes and longs are 4 bytes. chars can store any byte value but are most commonly used to store ascii characters.

    When you declare a variable of some type, the compiler allocates space for it in memory someplace:
    char c;     // 1 byte in memory taken to hold c
    int i;       // 2 bytes in memory taken to hold i
    long l;     // 4 bytes in memory taken to hold l
    

    Exactly where in memory can be found using the '&' operator. Eg, '&c' will be the address of the character c, '&i' would be the address of the integer i, etc. A memory address, on the z80, is 2-byte in size since it can only be a value from 0-65535.

    Variables holding memory addresses can be declared in a program. These are called pointers. We just have to tell the compiler what is stored at the address (what the pointer is point at). Eg:
    char *pc;     // pc is a pointer to a 1-byte char
    int *pi;       // pi is a pointer to a 2-byte  integer
    long *pl;     // pl is a pointer to a 4-byte long
    

    The asterisks mean 'is a pointer to'. So, eg, "char *pc" means "pc is a pointer to a char" or using english parlance "pc holds the address of a char". You can assign the address of the previously declared varables from above to these pointers:
    pc = &c;  // pc now holds the address of the character c
    pi = &i;    // pi now holds the address of the integer i
    pl = &pl;     // pl now holds the address of the long l
    

    You can poke values at the addresses held in pointers by dereferencing them:
    *pc = 'a';
    *pi = 1205;
    *pl = 1234567890L;
    

    Since pc held the address of the char c, after running the above the char c would now contain the letter 'a'. Trying to dereference a pointer before initializing its address is a bad idea. Because you wouldn't know what address the pointer stored you'd basically be poking a value to a random memory location. This would happen if we'd forgotten to "pc = &c;" above before "*pc = 'a';" was done.

    Here is another bad thing to do:
    pi = &c;
    

    "pi" is a pointer to a 2-byte integer but here we're assigning the address of the single-byte char c to it. The C compiler will emit a warning but will allow the program to compile. Sometimes things like this are done on purpose, on which case it's better to do it like this:
    pi = (int *)(&c);
    

    which tells the compiler you're taking the address of the character c (this makes a char*) and you're explicity going to use it as an int*. Then the assignment to "pi" can occur without a warning from the compiler.

    So why is this bad? What happens here:
    *pi = 1267;
    

    The two-byte int 1267 is poked at the address indicated by pi. However pi only points at the single byte char c. What happened? Well, one byte of the integer 1267 got poked into c and the other byte got poked into whatever follows c. This could be another variable or a bit of program code. In short, unpredictable results occur.

    Arrays of char, int, long, float, double can be declared as follows:
    char m[10];   // an array of 10 chars
    int n[12];   // an array of 12 integers
    char *q[20];   // an array of 20 pointers to char
    

    "m" is the name of the address of a block of memory holding 10 chars. Since each char is 1 byte, "m" is the name of the address of a block of memory 10 bytes long. Similarly, "n" is the name of the address of a block of memory holding 12 integers which is 24 bytes long. And "q" is the name of the address of a block of memory holding 20 pointers to char which is 40 bytes long.

    You can reference individual elements in the array with []s:
    m[0] = 'a';   // the first element in the array is at index 0
    m[9] = 'z';   // the last element of the 10-element array is at index 10-1=9
    q[0] = &m[3];
    

    In the above, "q[0]" holds the address of the fourth char (0,1,2,3 remember) in the "m" array. The assignment "*q[0] = 'q';" will poke the letter 'q' into m[3] such that m[3] contains the letter 'q'.

    Since the names of the arrays are aliases for the address of the start of each array, we can assign their values to pointers:
    char *pc;
    pc = m;
    *pc = 'b';
    pc = pc + 1;
    *pc = 'c';
    pc[4] = 'r';
    

    "pc" will hold the address of the first char in the m array. "*pc = 'b';" writes the letter 'b' into the element m[0]. The next line adds 1 to the address in "pc" so that it now holds the address of the second element in m, or &m[1]. The letter 'c' is then poked into m[1]. You may have noticed that there isn't much difference between the name of an array, which is an alias for the address of the first element, and a pointer to an element. (ONLY USEFUL FOR JOB INTERVIEWS: But there is a semantic difference! Notice I've been careful to say array names are alias for the address of....; essentially array names *are* a numerical address. Whereas pointers store the address of something. A pointer variable needs two bytes of memory to store a memory address whereas an array name *is* a memory address).

    You can, indeed, index a pointer as well. In the above, "pc[4]='r';" means add 4 to the address "pc" and poke the letter 'r' there. Since in the above code fragment "pc" was holding the address of m[1], pc[4] actually refers to m[5].

    Pointer addition is not exactly described as above. Adding "n" to a pointer in C actually means move ahead n elements. The "char *pc" holds the address of a single-byte char. Adding one to "pc" means move ahead 1 char, equivalent to adding 1 to the address since chars are 1 byte in size. However with "int *pi" where "pi" points at an integer, adding 1 to "pi" means move ahead one integer, equivalent to adding 2 to the address since integers are two bytes in size.

    Pointer subtraction is also supported, but the result is not necessarily the difference in address of the two pointers. Instead it is the number of elements being pointed to that can lie between the two pointers. So the difference between two char* is the number of chars between the two addresses which happens to equal the number of bytes between the two addresses. The difference between two int* is the number of integers between the two addresses which equals the number of bytes divided by two.

    One last complication is the void*, literally a pointer to something which we don't know what it is. Since you don't know what the address refers to, you can't dereference it (how would you? is it a 1-byte char or 2-byte integer?). Eg:
    int i;
    void *v;
    i = 5;
    v = &i;
    *v =10;   // illegal!
    

    The void* "v" is given the address of an integer. However the following line "*v = 10;" is illegal because the compiler doesn't know what "v" is a pointer to so it doesn't know how to write the value 10 there (is 10 a single byte char, a 2-byte integer, a 4-byte long. Does the compiler poke 1 byte, 2 bytes or 4 bytes?).

    Void* are useful for holding addresses temporarly but must be cast or assigned to char*, int*, etc before being used.

    Strings in C are a zero-tereminated sequence of ascii characters.
    "hello"
    "hello there"
    

    The string "hello" consists of the ascii bytes 'h', 'e', 'l', 'l', 'o', 0 poked into memory one after the other. Note the 0 byte to mark the end of the string.

    The address of a string can be stored in char* pointer:
    char *s = "my sentences are short";
    char a[] = "mine too";
    

    The variable "s" holds the address of the first character in the string "my sentences are short". In other words, "s" holds the address of the letter 'm'.

    Char arrays can also be initialized with the characters from a string. In the above a[0] would contain the letter 'm', a[7] the letter 'o' and don't forget a[8], the last element of the array, would contain the 0 byte. Using the declaration "char a[]" allows the C compiler to automatically fill in the size of the array for you.

    And we've ended this long beginner intro. Hopefully we won't have to do anything like this again :-P If you've understood the above, you're well equipped to understand the project... and now onto the first bit of code.
  • edited June 2007
    How do I get the latest CVS version of z88dk? I have 1.6 installed, but that was missing input.h. I grabbed the latest version of that, but can't find a way of getting the whole of the CVS version in one go.

    Also, when I tried to compile to see what would happen, I got this error..
    zcc + zx -vn mulemain1.c -o mulemain1.bin -create-app
            1 file(s) copied.
            1 file(s) copied.
    sccz80:"C:\PROGRA~1\z88dk\include/input.h" L:91 Warning:#17:Expected ';'
    sccz80:"C:\PROGRA~1\z88dk\include/input.h" L:91 Error:#27:Missing Open Parenthes
    is
    Compilation aborted
    

    Why is that? Is it simply because I have the wrong version of z88dk?
  • edited June 2007
    mulder wrote: »
    How do I get the latest CVS version of z88dk? I have 1.6 installed, but that was missing input.h. I grabbed the latest version of that, but can't find a way of getting the whole of the CVS version in one go.

    There are some instructions in the wiki. They could be better, I know.
    http://www.z88dk.org/wiki/doku.php/installation
    Also, when I tried to compile to see what would happen, I got this error..
    zcc + zx -vn mulemain1.c -o mulemain1.bin -create-app
            1 file(s) copied.
            1 file(s) copied.
    sccz80:"C:\PROGRA~1\z88dk\include/input.h" L:91 Warning:#17:Expected ';'
    sccz80:"C:\PROGRA~1\z88dk\include/input.h" L:91 Error:#27:Missing Open Parenthes
    is
    Compilation aborted
    

    Why is that? Is it simply because I have the wrong version of z88dk?

    Yes, that's exactly it. There have been a lot of changes since 1.6. There will be a new version in July as I mentioned that will come with a win32 installer and let you install z88dk whereever you want. In the meantime (if you'd like to avoid the cvs route), I've uploaded the current cvs version (win32 install) to mediafire which you can download and unzip into c:\ to create the c:\z88dk tree.

    z88dk-Jun-2007-snapshot

    It *must* be unzipped into c:\ because there are paths hardcoded in the install that expect it to be there. To compile a program, open up a command prompt, cd c:\z88dk, run init.bat to set up environment variables and then compile with zcc command. I left the test program in c:\z88dk\work so that you can try a compile. Ignore the "Constant blah blah" messages during compile -- these are just test messages.

    There will be several more changes to this version before July 15, so don't forget to grab a new version then.
  • edited June 2007
    Okay good,

    I read the thread up to this point and I am highly fired up for this. I grabbed a copy of Kroah's decompilation doc and read that too. I must say my French suck so I only understood 3 quarters of the entire document, nevertheless I now have a much greater idea of how what the game is and how it works, or so I hope.

    I must thank Alvin for starting this thread and keeping it as simple as possible. He just has a gift for explaining things much better than most people I know. I also hope there won't be a lack of interest in doing this, we'll just have to push through and see.

    I am in for the ride......:-)

    Cheers,
    Adriaan
  • edited June 2007
    It's certainly made me more interested in trying C on the speccy. Thanks for the snapshot AA :) I'll give it a go tomorrow or something.
  • edited June 2007
    TEST PROGRAM

    It's time to open up the test program and have a look around. At the top you'll see the included header files that contain the function prototypes for library functions the program calls. I built up that list as the program was written. Every time I decided to use a library function, I added the corresponding header file (*.h) at the top of the program.

    One of the includes "sys/types.h" I always use as it creates a couple of shortcuts for some data types. I like to use unsigned types and rather than typing out "unsigned char" or "unsigned int" all the time, it defines "uchar" and "uint" which can be used as synonyms.

    Inclusion of "stdlib.h" is a special case. You will see "#define __HAVESEED" defined prior to its inclusion. Stdlib contains a pseudo random number generator; all pseudo random number generators actually calculate the next random number from the previous one, called the seed. Under z88dk, inclusion of "stdlib.h" creates a variable "int std_seed" to hold this seed. We can prevent the seed from being declared by the header file by placing "#define __HAVESEED" prior to including "stdlib.h". I have done that in this program so that I could create my own std_seed variable that the library function rand() will use.

    The following two extern declarations:
    extern int std_seed(23672);
    extern uchar *d_chars(23606);
    

    are special variable declarations unique to z88dk. A "normal" integer variable declaration, as in "int std_seed;", causes the compiler to allocate two-bytes of memory to hold the variable's value. Under normal circumstances you have no control over where that variable is placed in memory (this is the compiler's job) -- and if you recall from the last post, you can find out at what address the variable is stored using the '&' operator while the program runs ("&std_seed").

    This special extern declaration syntax allows us to place a variable at a specific memory location. In this program I have placed stdlib's seed variable, int_seed, on top of the Spectrum's system variable FRAMES at address 23672. The value "int_seed" is the two-byte integer stored at address 23672, and the result of the "address of" operator applied to "std_seed" (as in "&std_seed") would be 23672. This means the value of the seed when the program starts up will be a random value determined by how long the computer was on before the program started. This is a good way to make sure the program generates different sequences of random numbers on every run.

    The other placed variable is "d_chars" which is located on top of the system variable CHARS. The value of "d_chars" is the 2-byte value stored at 23606, which happens to be where the basic system stores the location of the spectrum's character set. So in this C program, whenever "d_chars" appears we are retrieving the address of the spectrum's character set. The type declared for "d_chars" is "uchar *" which means the C compiler will be treating this value as a pointer to char. If we were to assign a value to d_chars within the C program, as in "d_chars = 30000;", we would be effectively poking 30000 into address 23606/23607, thus changing the location of the character set. On returning to basic all printed text would look like garbage.

    It is important to note that this placement syntax is not a standard feature of C, but only of z88dk. But it is quite handy as you can see :-)

    Following the extern declarations you'll see this:
    #asm
    XDEF _std_seed
    #endasm
    

    The #asm / #endasm wrapper tells the C compiler to ignore the intervening text completely. So anything in between is being communicated directly to the z80 assembler. This is one of the two hacks I mentioned earlier that I've used to circumvent two known problems in z88dk 1.6 that is currently being fixed for the 1.7 release. The XDEF is making sure that the rand() function in stdlib is able to find the seed variable; without it, in 1.6, the program will not compile as rand() will not be able to see std_seed. This is only temporary and will be fixed for 1.7.

    All C programs begin in the function called main(). Let's put off looking at main for now and have a look at the two print functions taken from MULE.

    DOUBLE WIDTH TEXT
    // prints double width chars without any formatting
    //    s = string to print
    //    n = max number of chars of s to print
    //  buf = destination address for printed bitmap (screen or buffer)
    //    f = uchar *(*f)(uchar *)
    //        adjusts destination address to move down one scan line
    
    void d_PrintDoubleW(uchar *s, uint n, uchar *buf, void *f);
    

    The game requires double width text to be printable to the screen and it requires double width text messages to scroll across the bottom of the screen. The approach I took was to make this function dual purpose -- it will be able to print dual width text to the screen directly *and* it will be able to print dual width text to a memory buffer. The latter will be done to simplify the scroller. The double width text bitmap will be written to the memory buffer by this routine and the scroller will just read that bitmap as it scrolls the message across the screen (the scrolling will be accomplished using memop() from the strings library; we'll see how it's done much later on but this is a heads up for anyone wanting to dream up how it might be done).

    What makes it possible to write a dual-purpose double width function is that the spectrum's display file stores pixels one byte after the other horizontally on the display. So we can render to the screen one complete pixel row by writing bytes to the display address and adding one to the address after each write. Likewise in our memory buffer we can write one pixel row by writing bytes to the buffer address, adding one to the address after each write.

    The spectrum's display organization is notorious for being a bit screwy so it's not such a simple matter to compute the display address when it comes time to move down one pixel row. A formula will have to be applied to the screen address to move down that pixel row. In the case of the memory buffer, we would want to add a constant to the buffer address to move down a pixel row. If we know the text length is 15 characters, we know that each pixel row will take up 30 bytes (double width remember) so we would want to add 30 to the buffer address to move down a pixel row. This keeps the memory buffer contiguous. How do we handle two different cases in this one subroutine? Simple, we pass the address of the "next pixel row" function to use to the subroutine and we let that function apply the correct adjustment to the destination address for the next pixel row. I think it will be clear when we look at the code.

    d_PrintDoubleW(uchar *s, uint n, uchar *buf, void *f) takes four parameters.

    s is a pointer to the string message to print (remember strings in C are a list of ascii characters terminated by a zero byte). s will hold the address of the first letter in the string.

    n is the maximum number of characters from the string to draw. We may want to print only a portion of the string for two reasons: 1- we don't want the message to spill off the edge of the screen (this routine assumes the text stays in the same character row and won't wrap to the next line). 2- the scroll routine will be dealing with messages longer than 16 chars and may need to render one character at a time as the message scrolls across the screen.

    buf is the destination address where the text will be rendered. buf will be an address in the display file or it will be the address of a memory buffer to hold the rendered text.

    Finally, f is the address of the function we'll be using to modify the buffer address when moving down one pixel row. z88dk does not allow properly declaring function pointer prototypes so I simply declare functions as a "void*" (literally meaning the address of something, we don't know what, but in this case the address of a function). Normally in C a function pointer declaration should include the return value and list of parameter types the function accepts. So in ansi-C "void *f" should actually be declared as "char *(*f)(char *)" meaning "f is a pointer to a function that accepts a single parameter that is a char* and returns a char* as its result. Sometimes z88dk's limitations can save you from a mouthful :-P
    void d_PrintDoubleW(uchar *s, uint n, uchar *buf, void *f)
    {
       uchar *sbuf, *bmp;
       uint i, j;
       
       while ((n--) && (*s))
       {
          sbuf = buf;
          bmp = d_chars + *(s++)*8;
    
          for (i=0; i!=8; ++i)
          {
             *(sbuf++) = doublew_left_helper(*bmp);
             *(sbuf--) = doublew_right_helper(*bmp++);
             sbuf = (f)(sbuf);
          }
          
          buf += 2;
       }
    }
    

    First up we declare some temporary variables needed in the function: two uchar* and two unsigned integers. Actually, as you can see, we don't actually need "j" as it's never used -- that'll disapper shortly when I reveal the newer version :-) Variables declared in C functions exist on the z80's stack and will only exist while the function is running. After the function is finished those variables are popped off the stack and are no longer valid. C also requires you to declare all variables you plan to use right at the beginning of a function (this is something that has changed in C++, which allows new variables to be declared as they are needed, but C was meant to be compiler-friendly).

    We first enter a "while" loop. While loops continue to run a single statement (where a statement is a single line terminating in a ';' or it can be a block of code enclosed in "{}") until the condition in "()" becomes false. While loops always test the true/falseness of the condition before executing its enclosed code the first time. This means if the condition is initially false, the enclosed code won't even run once.

    The condition in the while loop is "(n--) && (*s)". We have two conditions logically ANDed together ("&&" means logical AND in C and is equivalent to the Basic AND as in "IF a=0 AND b=0 THEN ...". You've already seen the single "&" operator which means "address of" when placed in front of a variable name. The single "&" in another context also means bitwise AND, which has no equivalent in Basic -- it may be time to look up a C tutorial on the web if you want to learn more about C's operators, otherwise ask here when we come across something strange).

    The two conditions ANDed together are evaluated in left to right order. The first condition is "n--". "n--" is shorthand for "n=n-1". There's also a "++" for adding one. There is a little nuance applied though: "n--" means decrease the value of n but return the original value of n as result. If "n" held 10 and you printed the value of "n--" you'd see 10 on the screen but after executing "n--" the value of "n" would be 9. This is because the result of "n--" is the original value of "n" before it was decreased. "--n" means decrease the value of "n" and return the new value as result. You can think of "n--" as return "n" and decrease "n" by one afterward and "--n" as decrease "n" first and return "n" as result. In C, false means 0 result and true is anything else. Therefore "n--" returns true if "n" was not zero to start with and then decreases "n" by one afterward.

    The other half of the condition is "*s". "s" holds the address of an ascii character in the text message. "*s" is the ascii code of the character. The result will be true if "*s" is not zero. Recall that all C strings end in a zero byte so "*s" will be true if we have not reached the end of the string. So:
    while ((n--) && (*s))
    

    means while "n" is not zero (we haven't reached the maximum number of chars we want to print) and the end of the message has not been reached (the current character *s is not the zero byte) run the enclosed code:
    sbuf = buf;
    

    First up we make a copy of the destination address for the text message. For printing to screen this will be the screen address at the top of the char being printed. Then we do this:
    bmp = d_chars + *(s++)*8;
    

    "d_chars" holds the address of the spectrum's character set. By adding 8 times the ascii code of the character we want to print we have the address of the character's UDG definition. Note the use of "*(s++)" which means we'll be retrieving the ascii code from the text string using "*s" and then add 1 to "s" afterward (this will make "s" point at the next character in the string for the next time through the loop).
    for (i=0; i!=8; ++i)
    {
       *(sbuf++) = doublew_left_helper(*bmp);
       *(sbuf--) = doublew_right_helper(*bmp++);
       sbuf = (f)(sbuf);
    }
    

    Characters on the Spectrum screen are 8 pixels tall. To render each character we need to poke 8 UDG values into the display. Since we are doing double width, we'll actually be poking 16 values -- a left and right half of a fat character in each vertical pixel row.

    For loops work as follows. The first bit of code "i=0" is always executed one time as initialization for the loop. The next bit "i!=8" is the test as in while loops. If the test results in false the for loop's enclosed code is not run. As in while loops the test is executed at the beginning of the loop code so if it is initially false, the enclosed loop code is never run. Then the loop code is executed and after the loop code is executed the last statement in the for loop "++i" is run. So this for loop sequence looks like this:
    i=0
    while (i!=8)
      run loop code
      ++i
    

    The "!=" operator means "not equal to", equivalent to "<>" in Basic. Other common C comparison operators include "==" (equal to, yes there are two of them there to make it different from plain old "=" used for assignments), "<", ">", "<=", ">=".

    So this for loop is executed 8 times, for values of "i" ranging from 0..7. After the loop finishes, "i" will equal 8 (the test condition "i!=8" is false when the loop terminates, meaning "i" is equal to 8 after the loop is exited).

    Inside the for loop, "bmp" holds the address of the next UDG byte to poke into the display and "*bmp" is the UDG byte itself. Since we're printing double width characters, single UDG bytes have to fatted up into two bytes. The subroutines doublew_left_helper() and doublew_right_helper() will do the fatting up for us and we'll look into them in a moment. For now, we'll just take their return values as the left and right halves of each fatted up UDG byte.
    *(sbuf++) = doublew_left_helper(*bmp);
    *(sbuf--) = doublew_right_helper(*bmp++);
    sbuf = (f)(sbuf);
    

    First we poke the left half UDG byte into the display with "(*sbuf++) ...". After this statement is executed, "sbuf" is increased by 1, moving the display address right one character. Then we store the right half of the UDG byte into the display with "(*sbuf--) = ...". "sbuf" is decreased by one to get back to its original display address (the left half of the fat UDG).

    And then we apply the magic. We call the function we passed as parameter to modify the destination address to move down one pixel row. "f" will take the current destination address as paramater and return a new destination address for the next pixel row.

    We repeat 8 times for all 8 vertical pixels in a character.

    After exiting the for loop we run this:
    buf += 2;
    

    "buf" was the destination address at the top of the current character ("sbuf" was the copied value that was all dirtied up inside the for loop). We add 2 to buf to skip over the left and right halves of the fatted up character now printed on the display (or in the memory buffer) and "buf" will point to the destination address at the top of the next character to be rendered.

    And we repeat that lot until we reach the end of the message (*s==0) or we reach the maximum number of characters we want to print (n==0).

    With that part all settled, let's have a look at the UDG fatting up process.

    cont'd...
  • edited June 2007
    ... cont'd

    With that part all settled, let's have a look at the UDG fatting up process.
    static uchar doublew_left_helper(uchar bmp)
    {
       uchar i, res;
       
       // have single byte 76543210 in bmp
       // looking to return 77665544
       
       res = 0;
       for (i=0; i!=4; ++i)
       {
          res *= 4;
          if (bmp & 0x80)
             res += 0x03;
          bmp *= 2;
       }
       
       return res;   
    }
    

    This subroutine takes as input a single UDG byte and returns the left half of a double-width UDG byte. The byte passed in, "bmp", consists of 8 bits which I'll refer to as "7653210". What we want to return from this function is a new byte but with the left half bits double width "77665544". I use the "res" variable as the result and return value for the function.

    In order to understand how this function works, you have to know a little bit about binary arithmetic. Namely that multiplication of a number by two is equivalent to left shifting the binary representation of the number. Multiplication by four is the same as multiplication by two twice, or shifting the binary representation two times to the left.

    So, for example, if I begin with a specific value in "bmp" with binary representation "76543210" and I multiply "bmp" by two, it will now contain a value "6543210-". "bmp" is a char that can only hold 8 bits so left shifting caused the most significant bit (the '7') to drop off the map. In the least significant bit a '-' appears (actually a 0 but I don't want to confuse with the other 0 already there which represents bit 0, not necessarily a binary 0 digit).
    This function tests the leftmost four bits of "bmp" (hence the for loop running four times) one at a time. Inside the loop we have this:
    res *= 4;
    if (bmp & 0x80)
       res += 0x03;
    bmp *= 2;
    

    We first shift the result left two bit positions by multiplying by 4 ("res *= 4" is shorthand for "res = res * 4"). Initially this does nothing as "res" initially contains 0. Then we check what's in bit 7 of "bmp" by performing a bitwise-AND of "bmp" and the hexadecimal value "0x80". The hexadecimal value "0x80" is "1000 0000" in binary. The bitwise-AND operator takes each corresponding bit and leaves a 1-bit as result only if both corresponding bits are 1. In "bmp & 0x80" all bits 6..0 in "0x80" are 0 so the result will have all zero bits in bits 6..0. Bit 7 of the result, however, will be a 1-bit if "bmp" contains a 1 in bit 7. Therefore the result of "bmp & 0x80" will be zero if bit 7 of "bmp" is zero or non-zero if bit 7 of "bmp" is non-zero. So we've find a way to test bit 7 of "bmp".

    Recall that in C any non-zero value means true. So the body of the "if" statement gets executed if bit 7 of "bmp" is a 1. If it is a 1, we write two "1" bits into bits 0 and 1 of the result by adding 3 to result.

    Once the "if" is done we shift "bmp" left one bit so that next time through the loop we'll be looking at bit 6.

    When we return to the top of the loop body the multiplication of "res" by 4 will shift the bits in "res" left by two positions. If we added two "1" bits into bits 0 and 1 in the last loop iteration, they will be moved over by this multiplication making room leaving bits 0 and 1 empty to receive the next result.

    Try running through the code with pencil and paper to see that, indeed, "res" ends up with the result "77665544" as advertised.

    The subroutine doublew_right_helper() is very similar (almost identical in fact) -- see if you can follow how that one works on your own.

    Now for a word of encouragement. If you are coming from a basic background, the above is going to be a little alien to you and it may even be a bit of a struggle to grasp (this is not the kind of code shown at the beginning of a C text, more like the middle!). But you should be able to sense the punch that C packs into such a small space and the powerful expression it makes possible. If you are having difficulty understanding everything and want to learn, now is the time to ask. As further encouragment for taking the time to understand this, you should know that C and asm are very similar and close in many respects and the above "modus operandi" expressed in C is almost identical to how it would be done in asm and the thinking behind an asm-equivalent solution.

    Now for some notes about z88dk C for C people. C provides a left shift operator "<<". I could have used "bmp <<= 1" rather than "bmp *= 2" to left shift "bmp" by one bit position. However, the "<<" operator will be translated into a call to a generic shifting subroutine by z88dk whereas "bmp *= 2" will result in a simple "add hl,hl" instruction during compilation.

    The z88dk C compiler also does not maintain a pointer to the stack frame in IX or IY as some other z80 C compilers do. Because most subroutines have a small number of parameters, it is usually quicker to access variables on the stack through pushes and pops or arithmetic using hl and sp rather than through slow accesses using instructions involving (ix+d). Not reserving any registers for its own use (including IX/IY) also means library functions can be free to use whatever registers they need. But because z88dk accesses stacked variables through pushes and pops, smaller code is generated if frequently used variables are declared last (and therefore appear at the top of the stack). It is also the case that using global variables produces much better looking and faster code, at the usual expense.

    Next time we'll look at the 32-column print routine and main() and then we'll be done with the minute treatment and we can get to business.
  • edited June 2007
    Great article Alvin, I agree with Adriaan that your way of explaining things is clear and very interesting. Good luck with this project.
  • edited June 2007
    Ok,

    These functions make perfect sense, especially when you have the WIKI open too have a look at the function explinations. I also had a look at the new include/spectrum.h and I really like the zx_{screen_addr_manipulation} funtions. Now I can't wait for the 15th of July :grin:

    I'll give the snapshot a bash a little later, maybe write something of my own using what I have learned already ...... if it works I'll post it.

    laterz,

    Adriaan
  • edited June 2007
    Hi all

    Here is a little test I did using what I have learned so far.

    You can pick it up here.

    Cheers,

    Adriaan
  • edited June 2007
    And here is the updated version.

    I am sure there is a better way of doing this. I think I must have a look at memop.

    Later,
    Adriaan
  • edited June 2007
    OK, I uninstalled 1.6 from c:\Program Files\z88dk, re-installed it directly into it's own folder C:\z88dk\, then pasted the snapshot over the top. I set the path to c:\z88dk\bin as it should be. On trying to compile the test, I got....
    C:\z88dk\work>zcc +zx -vn main1.c -o main1.bin -create-app
            1 file(s) copied.
            1 file(s) copied.
    sccz80:"main1.c" L:85 Warning:#8:Converting integer to pointer without cast
    sccz80:"main1.c" L:173 Warning:#36:In function: d_Print() line 65
    sccz80:"main1.c" L:173 Warning:#13:Pointer/pointer type mismatch
    sccz80:"main1.c" L:173 Warning:#34:Func expects: signed char *
    sccz80:"main1.c" L:173 Warning:#35:Func gets: signed int
    The system cannot execute the specified program.
    

    Maybe I should just copy the snapshot to C: and not over the top of the 1.6 installation?

    What am I doing wrong here?
  • edited June 2007
    Mulder,

    Did you set up the environment variables or are you running init.bat prior to compiling?

    Adriaan
  • edited June 2007
    wokani wrote: »
    Mulder,

    Did you set up the environment variables or are you running init.bat prior to compiling?

    Adriaan

    I changed the path to C:\z88dk instead of c:\program files\z88dk, but that was all I did. I must be half asleep today and just not paying attention properly. I'll run init.bat and report back. Cheers wokani :)
  • edited June 2007
    OK, still getting the same messages. Is it because I'm missing C:\gnu I wonder?
  • edited June 2007
    Mulder,

    It is not because you are missing c:\gnu.

    Here's the failsafe way of installing it:

    1. Uninstall z88dk first.
    2. Reinstall z88dk but choose c:\ as its install directory, NOT c:\Program Files
    3. Unzip the June snapshot that Alvin posted to c:\
    4. Check your Windows environment variables, it should be as follows:

    Path should have c:\z88dk\bin in there, there should be a variable named Z80_OZFILES with value c:\z88dk\lib\ and also a vriable named ZCCCFG with value c:\z88dk\lib\config\

    If the two extra variables don't exist just create them.

    Go to the c:\z88dk\work\MULE directory in CMD and run your compilation command.

    NOTE: Having the environment variables set up, you don't need to run init.bat before compiling. If you do not set up the environment variables you must run init.bat the first time you open a CMD window.

    And if all went well it should be working fine. Ignore the Constant messages it spits out - it should say "Terminated with exit code 0"

    That's about it for now.

    Adriaan
  • edited June 2007
    Right, I figured that wouldn't be it, but wanted to make sure. I'll get this working yet!
  • edited June 2007
    You don't even have to do that much work :-) The zip contains everything, including the binaries and compiled z80 libraries. So just get rid of any z88dk install and unzip the snapshot into c:\ to get the c:\z88dk tree.

    To compile, open up a command prompt, cd c:\z88dk, run init.bat (this sets up the necessary environment variables for this command prompt session) and then you can compile.

    That should be it!

    The c:\gnu stuff is only necessary to recompile the z80 libraries but as I've done that already it won't be necessary for now. However, you may at some point want to recompile the SP1 library (after selecting different options from the default) in which case you'd need that gnu stuff to do so.
  • edited June 2007
    wokani wrote: »
    And here is the updated version.

    Good work :-) I think we should turn the scroller into the first task. This will be something small and yet still clever. Here are the requirements:

    - It must be a pixel scroller
    - The message will occupy all 32 columns along a single text row
    - The message is double width text
    - The message must loop (it continuously scrolls across the screen over and over)

    On each call the routine must scroll the message on screen one pixel left. The plan is to have this in an interrupt service routine so that the message is scrolled one pixel left each 1/50s, the scroll happens with the raster at the top of the screen (so no tear), and other things (like sprite movement and keyboard reading) can occur in the main program without worrying about it.

    The solution I had in mind uses memset() for buffer init, memop() for pixel scroll (rotate left one bit operation), d_PrintDoubleW to print 1 char at a time into the buffer and probably memcpy() because I don't think you can use memop() directly in the display and still manage a full 32 columns. But any solution that is relatively quick will do.

    Edit: Should mention memop() has never been tested before. Just in case ;-)
  • edited June 2007
    Well, I still can't get it working. Getting the same old message 'system cannot execute the specified program'. :( I've gone to the lengths wokani described and everything. I've tried with and without init.bat. I've set the variables up myself in the MyComputer properties. I'm using the compile command outlined in the main1.c file itself. Always the same message.
  • edited June 2007
    mulder wrote: »
    Well, I still can't get it working. Getting the same old message 'system cannot execute the specified program'. :( I've gone to the lengths wokani described and everything. I've tried with and without init.bat. I've set the variables up myself in the MyComputer properties. I'm using the compile command outlined in the main1.c file itself. Always the same message.

    Mulder, can you try without the -create-app. This will result in main1.bin (run by loading the binary at address 32768 and then RAND USR 32768). create-app automatically makes the tap file. The error is complaining about not finding a binary, just trying to narrow it down.

    If that doesn't work, can you try running z80asm on its own (no params) just to see if iy can be found. Work calls again so I'm going to have to step out for a while...
  • edited June 2007
    Mulder, can you try without the -create-app. This will result in main1.bin (run by loading the binary at address 32768 and then RAND USR 32768). create-app automatically makes the tap file. The error is complaining about not finding a binary, just trying to narrow it down.

    If that doesn't work, can you try running z80asm on its own (no params) just to see if iy can be found. Work calls again so I'm going to have to step out for a while...

    Neither worked. I did also try z80asm2 out of curiosity, and that reported back with a copyright message.

    Thanks for helping with this, it's really confusing.
  • edited June 2007
    mulder wrote: »
    Neither worked. I did also try z80asm2 out of curiosity, and that reported back with a copyright message.

    Thanks for helping with this, it's really confusing.

    So z80asm can't be found? But it is in the c:\z88dk\bin directory? Try running "c:\z88dk\bin\z80asm" and see if it pops up with the copyright message. If it does, try changing your path with: set path="c:\z88dk\bin;%path%" . If the compile works with this done, you still have another z80asm in another directory someplace.

    We can also try putting in a new z80asm. One thing that you can try is copying the "z80asm2" file on top of "z80asm". z80asm2 is the older win32 binary (z80asm is the one being tried out) but it will still do the trick. Or you can visit www.z88dk.org and you'll spot a z88dk win32 binaries link in the left bar where you can get a full set of recently compiled win32 binaries. Just place them all in c:\z88dk\bin, overwriting anything there. I haven't had a chance to test these out myself yet.

    If this still doesn't work, could you reprint here the full text of all the output you're seeing from the compile command? The warnings, btw, are correct and can be ignored. I'm a bit lazy about typecasting (some functions expect one type in their declarations but I give another. Technically I should be casting them to shut up the warnings but I prefer the less encumbered source code).

    ...It is very strange indeed... these snapshots are supposed to be an easy way to set up :-D
  • edited June 2007
    One thing that you can try is copying the "z80asm2" file on top of "z80asm".

    That worked! So, something wrong with that z80asm then?

    I've just tried to copy the new binaries from the z88dk-win32-bin-20070624.zip, and now I don't even get the many error messages, just 'cannot execute specified program'. I'll copy the binaries from the snapshot back I think!

    EDIT:

    I notice that when I double click the z80asm2.exe in windows, it throws up the dos window, puts a message up and then the window closes straight away. z80asm.exe simply comes up with an error box saying the application configuration is incorrect.
  • edited June 2007
    A couple of questions.

    How long can the text message be that needs to continually scroll accross the screen? Or will it be short messages that is shorter than 16 chars?

    Will this message enter on the right hand side of the screen and keep on scrolling until you give it a end signal to exit on the left?

    Or can it be turned on and off, or will it just keep on scrolling whatever is thrown at it?

    Thanks,

    Adriaan
  • edited June 2007
    wokani wrote: »
    \How long can the text message be that needs to continually scroll accross the screen? Or will it be short messages that is shorter than 16 chars?

    I don't think any of the messages are less than 16 chars, but I think it's best if the subroutine can handle whatever is thrown at it. The message lengths are variable and will be passed as a char*.
    Will this message enter on the right hand side of the screen and keep on scrolling until you give it a end signal to exit on the left?

    Or can it be turned on and off, or will it just keep on scrolling whatever is thrown at it?

    The subroutine should scroll one pixel (and yes, the message enters at the right hand side) -- I expect some info (like the string, current ptr inside string, pixels remaining to next char, etc will have to be global; the game makes a pool of global variables available so this will be okay). The plan is the ISR will call the subroutine over and over and the program will start and stop the ISR as it needs to.

    As for initialization, it may make sense to borrow from strtok's behaviour: when passing a a new char* as parameter, strtok recognizes it is doing some initialization for a new string. On subsequent calls, if the char* passed is 0, it knows to continue using the last string.

    Those are just some musings as I haven't written the subroutine. I'll do some work on the next post instead and see what comes up :-)
  • edited June 2007
    mulder wrote: »
    I notice that when I double click the z80asm2.exe in windows, it throws up the dos window, puts a message up and then the window closes straight away. z80asm.exe simply comes up with an error box saying the application configuration is incorrect.

    Well I'm glad you found this now because we all thought everything was ok (well, I did at least). I'll have to pass this along to dom to let him know something's up. What version of Windows are you using btw? If this is an old machine perhaps that's the problem...
  • edited June 2007
    I'm on XP SP2, on a HP Pavilion laptop with Pentium 4.
  • edited June 2007
    Would this be acceptable in the scroller? Not that I think I'm ready to write it yet, but maybe it could give Wokani some ideas...

    1-The left most and right most character squares on the scroller line have the same ink and paper values. This does some masking and provides some symmetry to the look of the scroller to even things out.

    2-You get the first character you need to print.

    3-In the right most character square, you print the first half of the character.

    4-You scroll every line in the screen area the scroller is using left by one pixel, and repeat until you've done all 8 pixels.

    5-In the right most character square, you print the second half of the character.

    6-You scroll every line in the screen area the scroller is using left by one pixel, and repeat until you've done all 8 pixels.

    7-You move on to the next character in the message and go from step 3.

    The messages would need a few spaces at the start and end to stop the scroll looking messy.
  • edited June 2007
    Mulder,

    That sounds good, but I think the left- and rightmost characters must be visible. I've already battled with this and I got the logic all done, but I am struggling with the actual scrolling. I had it scroll perfectly but not the message - it was scrolling some other data :) . Now it just sits there and doesn't scroll at all - still some other crap instead of the message.

    The reason I am struggling with this is because I do not understand EXACTLY how memop works. Today I am going to try and find the .asm of memop and look in there to figure out how it exactly works. What I do know is that it copies from source to destination, n bytes of data whilst performing and operation on the source as well as the destination data. This operation comes in different flavours from LOAD, OR and XOR to rotates left and right. Exactly what we need for scrolling. What I do not know is in which direction the source is copied to the destination and if the rotate direction affects this, although I actually assume this to be the case.

    I am sure I have the logic all sorted out, but I am either not passing the pointers in the right way or not declaring it correctly or something to that affect.

    I'll carry on struggling because I want to gets this working so we can move on to the rest of it :-D I am thinking of using a custom callback function instead of the operation for better control.

    All in all I like your idea, I was also thinking along these lines initially but went against it for a number of reasons. I'll just have to wait and see how I am going to get it working.

    Later,
    Adriaan
Sign In or Register to comment.