Project: M.u.l.e.

124

Comments

  • edited September 2007
    JamesD wrote: »
    I want to see a downloadable archive so I can play with the code! :)

    Just bare with me to the weekend :)

    We're almost through this preliminary code (in fact after the next very brief post) so there will be a new code package available this weekend with everything we've seen so far plus the next bit (the events code). I'm also going to include the tentative game graphics, designed by TechnicianSi, as a separate download.

    The code should be enough to play with -- I have more yet to come but it needs to be tidied up and modified a bit since I've been making a few changes to how things work.

    The events code is complete, logic-wise, but does not have any of the graphics effects done so I'm hoping to get people to help out with that once I've given an example of how the sprite library is used.

    Note:
    The scroller, which has been hanging out there for a month now, will need to be done for the events code so if you had plans to write one now is the time to present it here. Otherwise I am going to write it in the next day or two!
  • edited September 2007
    This is the last module in the preliminary code package released so long ago. I didn't think it would take this long to get here but other obligations got in the way. Oh well.

    This last module is the store. The store module keeps track of the amount of commodities held in stock and the current prices. At this time the store data structure has only these two items in it but we may need more for the next round price generation logic; I haven't looked into the price generation description in kroah's document at any depth to know for sure.

    store.h:
    #ifndef STORE_H
    #define STORE_H
    
    #include <sys/types.h>
    
    /// DATA STRUCTURES
    
    #define COMMODITY_FOOD      0
    #define COMMODITY_ENERGY    1
    #define COMMODITY_SMITHORE  2
    #define COMMODITY_CRYSTITE  3
    #define COMMODITY_MULE      4
    
    struct store {
    
       // goods in stock
    
       uint goods[5];              // indexed by COMMODITY_*
       
       // goods price
    
       uint price[5];              // indexed by COMMODITY_*
          
    };
    
    extern struct store g_store;
    
    /// FUNCTIONS
    
    extern void s_StoreInit(void);
    
    #endif
    

    All self-explanatory I think. There is exactly one store held in the global variable "g_store".

    At this time there is only one store-related function and that is the store initialization when the game starts up:
    static struct store store_template = {
       8, 8, 8, 0, 14, 30, 25, 50, 100, 100
    };
    
    static uint beginner_stock[] = {
       16, 16, 0, 0, 25
    };
    
    void s_StoreInit(void)
    {
       memcpy(g_store, store_template, sizeof(struct store));
       if (g_level == LEVEL_BEGINNER)
          memcpy(&g_store.goods, beginner_stock, 10);
    }
    

    As we've seen in previous data structure initializations, the strategy is to copy a template and then make necessary modifications. In this case there is only one modifcation for the beginner level of the game where the store has its stocks initially increased.

    And that's it! Coming up soon we'll review the outstanding programming assignments, see the tentative game graphics, get the next code package containing the events code, and introduce a bunch of opportunities to contribute through programming the graphics effects for all the events.
  • edited September 2007
    Just bare with me to the weekend :)
    We're almost through this preliminary code (in fact after the next very brief post) so there will be a new code package available this weekend with everything we've seen so far plus the next bit (the events code). I'm also going to include the tentative game graphics, designed by TechnicianSi, as a separate download.

    The code should be enough to play with -- I have more yet to come but it needs to be tidied up and modified a bit since I've been making a few changes to how things work.
    ...
    Sounds great. I want to get it running on my 2068 and start on a port to another machine if all goes well.
    I realize it's not complete yet but I haven't run the z88dk stuff in a couple years and it gives me a place to start.
  • edited September 2007
    Download MULE code package Oct 21, 2007
    Download tentative game graphics TommyGun package Sep 27, 2007

    The code package contains:

    * All code presented up to this point

    * A modified double width text printer "d_PrintDoubleWidth()" in display.c that you may want to look at. It has been simplified as it is no longer needed for the fat text scroller.

    * Addition of "pyr_PlayerClose()" function to "player.c" that will deallocate memory automatically allocated by the sprite creation functions in "pyr_PlayerInit()".

    * Double width text scroller routines added to "display.h / display.c" :- d_ScrollStart(), d_ScrollStop() and scroll_isr().

    * New module "interrupt.h / interrupt.c" contains a few small subroutines to be run in the 50Hz interrupt.

    * New module "events.h / events.c" contains the player and round events code.

    The graphics package contains most of the tentative game graphics drawn by TechnicianSI. For sure things will need to be changed (code and graphics) while the game is being tweaked to look as good as possible. The graphics come in a TommyGun package but currently there is no way to get TG to generate graphics data in the format z88dk's SP1 library requires so one of the things we'll need to do is transfer these images to SevenuP so that graphics code generation becomes seemless in the project. I intended to do two code packages: one with TG and one with SevenuP so this will not be lost effort! TG can also import SevenuP graphics so if TG supports SP1 at a later date importing them will be a breeze.
  • edited September 2007
    JamesD wrote: »
    Sounds great. I want to get it running on my 2068 and start on a port to another machine if all goes well.
    I realize it's not complete yet but I haven't run the z88dk stuff in a couple years and it gives me a place to start.

    The 2068 target should be easy as the ROM is not used at all by this code. The only minor change would be different port assignments for the AY chip in the sound routine (when we get there). I too had in mind the 2068 as target and may even go for a small run of 2068 / IF2 carts just for fun. I was considering a high-colour mode version but I don't think this game would really benefit too much by it.

    Porting to another machine (I think I know which :D ) would be something interesting too. I like the idea of using C as an '80s cross-machine porting tool where several retro communities can share software across platforms. I'm hoping to do this within z88dk by porting various libraries to various z80 platforms but there's only so much time and energy in the world so it's been slow progress. There's also a neat 3d isometric engine written in 6502 for the Oric with a C front end that, if ported to z88dk, would make it possible to develop 3d isometric games for 6502 and z80 machines using the same source code. CEZ (the spanish mainly spectrum outfit) does this to a limited extent where some games have been written in C on the spectrum and that C source has been used as an intermediate language to port to other z80 machines.
    Crisis wrote: »
    I think this tread has a record for the LONGEST PAGE in one tread (!)

    The limit per post is 16k characters in case anyone was wondering :)
    aowen wrote: »
    Once I have the whole tune in Garage Band and it sounds right I'll convert it to Music Box format. It should just about fit without needing any external looping. Try and keep at least 2K of memory free for the tune and interrupt driven playback routine.

    [edit]

    Argh! Over the note limit. There's going to have to be some 3rd party code to stitch the loops together.

    Ah great, thanks Andrew. The extra code shouldn't be too much trouble as long as you write it :P or let me know how it works. I expect it will be simple :) Does it still look like the beeper interrupt-driven music will work?

    I always worry that this thing won't fit in 48k because compiled C code is many times larger than hand-written assembler... I hope we're okay only time will tell.

    I've just finished up the events code so if you've already downloaded the code package you may want to do it again. Only events.h / events.c are affected.
  • edited October 2007
    The 2068 target should be easy as the ROM is not used at all by this code. The only minor change would be different port assignments for the AY chip in the sound routine (when we get there). I too had in mind the 2068 as target and may even go for a small run of 2068 / IF2 carts just for fun. I was considering a high-colour mode version but I don't think this game would really benefit too much by it.
    Yeah, the original wasn't exactly colorful.
    Porting to another machine (I think I know which :D ) would be something interesting too.
    Well, I haven't exactly hidden the fact that my first machine was a CoCo.
    I think a version with graphics that would work on any 256x192x2 color screen (or possibly a lower res 4 color mode) would be a good start since it could easily be made to run on anything with the 6847 and would require less work due to the resolution.
    Then I figured a CoCo3 version with redone gfx for 320x200x16.

    Right now I'm just trying to get the development environment/tools working.
    I like the idea of using C as an '80s cross-machine porting tool where several retro communities can share software across platforms. I'm hoping to do this within z88dk by porting various libraries to various z80 platforms but there's only so much time and energy in the world so it's been slow progress.
    Yeah, I thought it would be cool to "write once, run anywhere" at least for machines with the same gfx resolution.
    I played with several compilers and wasn't pleased with any of them.
    z88dk still isn't ANSI enough for my taste, SDCC died just compiling my code and there wasn't a C compiler worth trying for the 6809.

    Now...
    z88dk is better but still missing some stuff.
    SDCC actually compiles my code now and I think generates better code than z88dk. (I'd like to compare binaries against z88dk but porting all the libs... eeewww)
    Now GCC seems to be making good progress for the 6809. I was working on a port of SDCC but the code generator has a long way to go.
    There's also a neat 3d isometric engine written in 6502 for the Oric with a C front end that, if ported to z88dk, would make it possible to develop 3d isometric games for 6502 and z80 machines using the same source code.
    Yeah, I've been following the Space 1999 game myself but the gfx all need redone since the Oric screen is so odd. It also results in a different aspect ratio. Now if someone were to redo the gfx I'd be happy to help out a little.
    CEZ (the spanish mainly spectrum outfit) does this to a limited extent where some games have been written in C on the spectrum and that C source has been used as an intermediate language to port to other z80 machines.
    I'll have to look them up.
  • edited October 2007
    We, at CEZGS, in joint effort with several other people, are producing C-friendly versions of libraries which work not exactly the same but simmilar to the splib, simmilar enough so porting to CPC or MSX is very easy (but not direct, which would be either impossible or unefficent). I'm pretty positive that they will be released sometime in the future when they are "shareable". Right now they are quite rough.

    I'll be soon in the works of porting Phantomasa: in the land of the Grunge Wizards v2.0 to the MSX and I'm sure I'll be able to reuse 90% of the code, which is quite nice ;).

    Btw, this project looks great :)
  • edited October 2007
    I'm just going to do as quick an overview of the new code as possible, then fix some problems with the round events code (I made last minute changes to how things work... and didn't think things through in my haste to post the new code package) and then let loose a bunch of new programming assignments, many of which will be graphical effects for the round events.

    Let's start out gently.

    interrupt.h, interrupt.c

    The game contains sound effects and music that will be played using an AY driver that must run in the 50Hz interrupt.

    The game requires timers; exactly how many is unknown at this time but at least one up timer (as in a timer that counts up to a maximum value) and one down timer (as in a timer that counts down to zero) are required. The down timer can be used to keep track of remaining time in a player's turn. The up timer is necessary because the game measures how long a player takes to start his turn in order to calculate when the "wumpus" will appear during his turn. These timers will also run in the 50Hz interrupt.

    The fat-text scrolling routine must run during the interrupt for two reasons. One is we want the scroll to occur at a constant rate (1 pixel per interrupt or 50 pixels per second). But the more important reason is we don't want the scroll to flicker on screen. When the 50Hz interrupt occurs we know we have about 14000 cycles before the TV raster begins drawing the top pixel row of the screen and thereafter 224 cycles per each pixel row. Since the scroll will always occur in row 20 we have about 14000 + 20*8*224 = 49840 cycles to complete the scroll after the interrupt to avoid flicker. This is very easy to meet if the scroll isr is the first isr run after interrupt.

    Lastly we need something to randomize the pseudo-random number generator's seed value. As you probably know, the random numbers generated by computers are not random at all but are computed from a formula using the "seed" value (the last random number generated). A consequence of this is that if the seed is set to the same value before a game begins, the game will generate exactly the same sequence of random numbers. In order to prevent gamer hell by having players relive the same game over and over again we try to randomize the seed value using a true random variable -- how long it takes the players to start the game. So what we'll be doing is adding 1 to the seed value during each interrupt while the game is in the menu. Once the game starts we will stop adding one to the seed. MULE is a network-friendly game and I would like to make an attempt at running it over an IF1-network on real machines. By locking in a seed value once the game has started and communicating the same seed value to all machines on the network we guarantee that all machines will generate the same random number sequence while the game runs therefore guaranteeing that all machines will see the same random events occurring. So, to summarize, we want to constantly increase the seed value while the game sits in the player menu and stop doing that once the game has started.

    z88dk has an im2 interrupt library that allows us to register functions to run during an interrupt. We will have four different isr functions for the four requirements above that will be registered with the im2 library for execution during the spectrum's 50Hz interrupt. Let's have a look at them now (from interrupt.c):
    // void scroll_isr(void)
    // found in display.c
    
    void seed_isr(void)
    {
       ++std_seed;
       return_nc;
    }
    
    void timer_isr(void)
    {
       if (g_downtimer0)
          --g_downtimer0;
       
       if (g_uptimer0)
          ++g_uptimer0;
       
       return_nc;
    }
    
    void music_isr(void)
    {
       // insert LeeDC's music player code here
       
       return_nc;
    }
    

    seed_isr() is going to add one to the seed value while the game is in menu mode. The two timers can be spotted in timer_isr(); both timers are inactive with zero counts and can be started by writing a non-zero value into their counts. The music_isr code will be either LeeDC's music / sound effects player (which is in turn a modification of Sergei Bulba's pt3 player code) or aowen's beeper stuff and will be added to the project at a later date. The scroll isr code is in display.c and discussion of that can be found below.

    All the isr functions end with a special z88dk keyword "return_nc" which is similar to the standard "return" but also ensures the carry flag is reset on return from the function. This is because all these isr routines will be installed on the same interrupt vector and will be run one after the other by the im2 library. The library will stop running isr routines in the list if any of them returns with the carry flag set. So by adding this "return_nc" keyword to the end of all the isrs we ensure that all these isrs will be run on interrupt.

    pyr_PlayerClose() (found in "player.c")

    I thought I'd better talk about this addition to player.c briefly. We've already seen the calls to sp1_CreateSpr() and sp1_AddColSpr() in "pyr_PlayerInit()" that creates the player's sprite. As mentioned when this function was discussed, the sp1 library internally breaks up a sprite into its characters and represents each character by a 24-byte "struct sp1_cs". The call to sp1_CreateSpr() also returns a pointer to a 20-byte "struct sp1_ss" that is used by the library to control the sprite as a whole. So the code that creates a single player's sprite causes the sp1 library to create nine 24-byte "struct sp1_cs" and one 20-byte "struct sp1_ss". A good question to ask is where is the memory coming from for these structs? So far we've been using global and local variables exclusively. Memory for local variables are allocated on the stack when a function executes (ie PUSHed) and are removed from the stack when the function terminates (ie POPed). Memory for global variables is reserved by assigning specific fixed memory locations to hold variable contents. However there is a third type of memory allocation known as dynamic memory allocation which is memory requested by the program while it runs. The sp1 library requests some available memory (any memory address will do) to create the necessary "struct sp1_cs" and "struct sp1_ss" as the program runs. It makes this request by calling a function the program must provide (called u_malloc()) whose purpose is to manage available memory segments in the z80's address space. (Note: some may be wondering why the library doesn't just transparently call malloc() to get memory; because the z80's address space is so small there are reasons that a memory manager like malloc, which supports variable size blocks, may not be ideal and those are discussed on the z88dk memory allocation wiki page). The point is there is a limited amount of free memory available and if that memory is not returned for reuse, after several games are played, there will no longer be any memory available to make more sprites. So of course the right thing to do is to release that memory for reuse when the game is over. This is the purpose of the "pyr_Close()" function:
    void pyr_PlayerClose(struct player *p)
    {
       sp1_MoveSprAbs(p->sprite, gfx_clip, 0, 0, 32, 0, 0);        // move sprite off screen
       sp1_DeleteSpr(p->sprite);
    }
    

    Before releasing a sprite's allocated memory blocks we have to make sure that it doesn't appear anywhere on screen by moving it off screen using one of the move sprite functions (in this case we're moving it to column 32 and row 0). This is because while a sprite appears on screen its "struct sp_cs" blocks are present in linked lists the library maintains internally.

    With the sprite off screen "sp1_DeleteSpr()" is called which takes care of freeing all memory associated with the sprite (all the "struct sp1_cs" and "struct sp1_ss"). It returns this memory for reuse by calling a function the program must supply "u_free()". So our program must supply two functions that work together: u_malloc for providing the address of free memory blocks of requested size and u_free for reabsorbing previously allocated blocks for reuse and reallocation by u_malloc. These we'll worry about much later.

    d_ScrollStart(), d_ScrollStop() and scroll_isr() in display.c

    These functions comprise the fat-text scroller that scrolls a full screen wide double-width message on row 20 (tentatively that is depending on final screen layouts). scroll_isr() is where the actual scrolling occurs; it is meant to be an interrupt routine run in the 50Hz interrupt so that we 1- have a constant scroll rate (1 pixel per interrupt) and 2- know where the TV raster is so that all drawing to screen can be done flicker-free.

    d_ScrollStart() is called to start scrolling the message and d_ScrollStop() is called to stop scrolling. The message to be scrolled is a standard C-string (that is a list of ascii characters terminated by a '\0') stored at address "g_textBuffer". The current character within that string being drawn at the right edge of the screen is pointed at by "g_scrollPos" and the number of pixels of that character left to draw is in "g_scrollShift" (which can be 0-15 since we're dealing with double width characters). scroll_isr() is written such that a zero in "g_scrollPos" indicates that there is no message to scroll. Which makes "d_ScrollStop" really simple:
    void d_ScrollStop(void)
    {
       g_scrollPos = 0;                      // scroll message pointer at zero prevents scroll_isr from doing anything
    }
    

    "d_ScrollStart" is only slightly more complicated because it actually erases row 20 before setting the scroll in motion:
    void d_ScrollStart(void)
    {
       uchar *disp;
       
       disp = zx_cyx2saddr(20, 0);           // get display address of top scanline in char row where scroll occurs
       
       do
       {
          memset(disp, 0, 32);               // clear horizontal scanline
          disp += 0x0100;                    // next scanline down
       } while (disp & 0x0700);              // do for all eight scanlines in row
       
       g_scrollShift = 16;                   // initialize current scroll shift
       g_scrollPos = g_textBuffer;           // initialize scroll message pointer to start of text buffer
    }
    

    "disp" gets set to the address in the display file of the top scanline of the character position row=20, column=0. The do-while loop operates for each of the 8 scanlines in the character. For each scanline memset() is called to write 32 zeroes to clear all 32 columns horizontally in the scanline. The address of the next scanline in a char is 256 bytes away so once that is done "disp" gets 256 added (0x0100) to move down to the next scanline in the char. The while condition ensures the loop runs 8 times. Understanding this bit of code completely requires knowledge of how the spectrum display file is organized.

    We then set the number of pixels yet to draw in the current char in the message to 16 and let loose scroll_isr by setting "g_scrollPos" to the first character in the message to be scrolled.

    And next comes the meat in scroll_isr whose purpose is to scroll the message on row 20 left one pixel:
    void scroll_isr(void)                    // an interrupt routine called every 1/50s
    {
       uchar *bmp, *disp;
       
       if (!g_scrollPos)                     // if there is no message to scroll exit
          return;
          
       if (!(g_scrollShift--))               // if the current char has been plotted 16 times it is completely on screen
       {
          g_scrollShift = 15;                // initialize scroll shift for next char in message
          if (!(*(++g_scrollPos)))           // if the next char in message is end-of-string '\0'
             g_scrollPos = g_textBuffer;     //   loop back to the start of the message
       }
       
       bmp = g_chars + (*g_scrollPos)*8;     // get UDG address for char to display from message
       disp = zx_cyx2saddr(20, 31);          // get display address at far right edge of scrolled row
       
       do
       {
          memopd(disp, disp, 32, 8);                           // scroll scanline left using operation #8 (rotate left source)
          *disp |= (*(bmp++) >> (g_scrollShift/2)) & 0x01;     // OR in single pixel from message char on far right
          disp += 0x0100;                                      // next scanline
       } while (disp & 0x0700);              // do for all eight scanlines in row
       
       return_nc;
    }
    

    The first "if" causes the subroutine to exit if there is no message to scroll.

    The second "if" checks if all 16 pixels of the current message char being drawn has already been displayed (this happens if "g_scrollPos" reaches zero) and if so resets "g_scrollPos" to 15 and moves "g_scrollPos" to the next char in the message. If that char is C's end of string marker (0) then "g_scrollPos" wraps back to the start of the message.

    With that out of the way, "bmp" gets the address of the message char's UDG definition and "disp" is initialized with the address of the top scanline of the char at row=20, column=31 which, since we are implementing a right to left scroll, is where the current message char is being drawn. The do-while loop runs 8 times, once for each scanline in row 20.

    memopd() is very similar to memopi() which we've already seen; both functions are non-standard additions to string.h and are described in the z88dk wiki. Both functions copy N bytes from a source address to a destination address whilst performing an operation on the source and destination bytes. memopi() increases both destination and source address after each byte move (like LDIR) but memopd() decreases both addresses (like LDDR). In this case we are using memopd() with operation #8 (rotate source byte left) which starts at the rightmost edge of the screen, reads the byte there, rotates it left, writes it back and then moves one column left, repeated 32 times. This causes the scanline to shift left one pixel, leaving the rightmost pixel clear and ready to receive the message char pixel to be written next.

    Writing the message char pixel is accomplished with the complicated looking "*disp |= (*(bmp++) >> (g_scrollShift/2)) & 0x01;". This simply states that we poke into the rightmost column the byte that is already there ORed with "(*(bmp++) >> (g_scrollShift/2)) & 0x01;". The UDG byte for the message char is shifted right (">>" in C) by "g_scrollShift/2" pixels and only the rightmost pixel is kept ("& 0x01"). "g_scrollShift" goes from 15 down to 0 to mark the number of pixels of the char that is yet to be drawn -- you should be able to work out how this statement gets us fat characters.

    And then we end the scroll_isr() with the special "return_nc" as explained under "interrupt.c" above.

    Next up.. events.h/events.c
  • edited October 2007
    The graphics come in a TommyGun package but currently there is no way to get TG to generate graphics data in the format z88dk's SP1 library requires

    Could be sooner than you think ;)
    But probably not as soon as you would like :D

    days - possibly - weeks - maybe - months - definitely not
  • edited October 2007
    JamesD wrote: »
    Now...
    z88dk is better but still missing some stuff.
    SDCC actually compiles my code now and I think generates better code than z88dk. (I'd like to compare binaries against z88dk but porting all the libs... eeewww)

    Yeah z88dk is still not quite ANSI but honestly I don't really miss anything. And I can settle one thing for sure.. SDCC does generate better code than z88dk. However z88dk's strength is very much in its libraries, which is miles ahead of all the other z80 compilers. dom has expressed interest in redoing the compiler core to generate better code in the past but the individual(s) offering help ski-daddled. Maybe it will come up again after the new assembler is incorporated.
    Kiwi wrote: »
    Could be sooner than you think ;)
    But probably not as soon as you would like :D

    days - possibly - weeks - maybe - months - definitely not

    Sweet :) There is probably a week to two before we *have* to have the graphics available... will see what turns up.
  • edited October 2007
    Yeah z88dk is still not quite ANSI but honestly I don't really miss anything. And I can settle one thing for sure.. SDCC does generate better code than z88dk. However z88dk's strength is very much in its libraries, which is miles ahead of all the other z80 compilers. dom has expressed interest in redoing the compiler core to generate better code in the past but the individual(s) offering help ski-daddled. Maybe it will come up again after the new assembler is incorporated.

    Why not spend the effort on porting the libs to the better compiler rather than trying to rewrite the compiler?
  • edited October 2007
    Crisis wrote: »
    I hear my self thinking
    "or should it be an
    sbuf = (f)(--sbuff);
    "
    still have a decrease, then declare sbuf again.

    Yep, you've figured it out. We want sbuf to hold the address of the original display address (before the ++ / -- stuff) when we call function "f" to modify the address to move down one pixel row.

    You'll see that the latest version does this:
    *disp = doublew_helper(*bmp, 0x80);
    *(disp+1) = doublew_helper(*(bmp++), 0x08);
    

    where we don't change "disp" at all with ++/-- and instead just add 1 to the address in the second memory write. Changing the value of sbuf/disp at all is just wasted effort!
    Stepping down 1 pixel row means increasing the MSB-screenadres-byte with one, resulting in increasing the screen adres with 256.
    But that must be enclosed some were else, in the function sbuf of 'f'?

    Yes the function "f" supplied to the subroutine does this. The reason why it doesn't just add 256 is that we wanted this double width printer to be able to print to the screen AND to a block of memory (for the double width scroller) where scanlines would not be separated by 256. For screen printing you'd pass a function "f" that adds 256 and for buffer printing you'd pass a different function "f" that added another amount. Although in hindsight it would have been a better idea to use one of the global variables to hold the value to add and do a "sbuf += g_uchar0;" (or something) rather than incur the overhead of a function call.

    That's all abandoned now and the present double width printer only prints to screen. There is no problem with the scroller being fast enough to avoid flicker so the scrolling now gets applied directly to screen rather than doing it to a buffer and then copying the buffer to screen.
  • edited October 2007
    JamesD wrote: »
    Why not spend the effort on porting the libs to the better compiler rather than trying to rewrite the compiler?

    When I say the C translation by sdcc is better it's not the same as saying it's good :) I think there are other free z80 C compilers that are better than sdcc but they are native z80 applications (one that comes to mind is msx-C I think it was called). No matter what present-day C compiler you use, the code generated from the C will be larger, slower and klunkier by an order of magnitude compared to anything hand-written by a competent z80 programmer. So my view is make the libraries slick and hand-written in assembler and use the C like glue holding together calls to the libraries where most time should be spent. This way you get performance that can approach all-asm projects. And since the code generated by present-day C compilers is so bad (relatively speaking) and we want most execution time to be spent in asm libraries, it is also my view that the compilers, as they are, should be aiming for code compactness rather than speed.

    z88dk has the library angle covered and it has special calling conventions for library (read asm) routines that does an effective job reducing program size. But the main reason I invest time in it is that it is still an active project (I think z80 development on sdcc is pretty much dead and the native z80 C compilers are a dead end in terms of further development). Lastly I think a new compiler written from scratch for the z80 would do a much better job than what the present day z80 C compilers are doing and this is the angle I'd like to pursue once the asm-library angle (the most important in terms of performance) is exhausted.
  • edited October 2007
    When I say the C translation by sdcc is better it's not the same as saying it's good :) I think there are other free z80 C compilers that are better than sdcc but they are native z80 applications (one that comes to mind is msx-C I think it was called). No matter what present-day C compiler you use, the code generated from the C will be larger, slower and klunkier by an order of magnitude compared to anything hand-written by a competent z80 programmer. So my view is make the libraries slick and hand-written in assembler and use the C like glue holding together calls to the libraries where most time should be spent. This way you get performance that can approach all-asm projects. And since the code generated by present-day C compilers is so bad (relatively speaking) and we want most execution time to be spent in asm libraries, it is also my view that the compilers, as they are, should be aiming for code compactness rather than speed.

    z88dk has the library angle covered and it has special calling conventions for library (read asm) routines that does an effective job reducing program size. But the main reason I invest time in it is that it is still an active project (I think z80 development on sdcc is pretty much dead and the native z80 C compilers are a dead end in terms of further development). Lastly I think a new compiler written from scratch for the z80 would do a much better job than what the present day z80 C compilers are doing and this is the angle I'd like to pursue once the asm-library angle (the most important in terms of performance) is exhausted.

    Or you could just make them work with SDCC like I actually meant. I didn't mean port them to C.

    If you are going to improve the code generation of a compiler at least start with a better compiler. The front end of SDCC is pretty good but more effort could be placed on the back end and additional peephole optimizations.

    A thread on the topic I see you've already participated in:
    http://br.msx.org/forumtopic7077.html

    If you like CALLEE linkage so much... why not add it to SDCC?
    But this is off topic and perhaps we should put it in another thread.
  • edited October 2007
    JamesD wrote: »
    Or you could just make them work with SDCC like I actually meant. I didn't mean port them to C.

    As you found out :D porting the libraries is still a major exercise (as in a pain-in-the-butt variety) and any gains from it I see as marginal since the code generated is still poor (relatively speaking) and the libraries, not the compiler, carry the performance onus. And accepting that the code generated is still poor my preference for compiler output is for smaller code which I think z88dk has an edge on for larger projects. Besides the quality of code generated there are other considerations like the calling convention used for asm libraries (the CALLEE / FASTCALL thing you mentioned) which does impact the size of the executable, how the stack frame is managed (I don't recall how sdcc does it but I do not like the use of IX to index the stack frame because I believe it is usually slower and it interferes with some z80 targets that reserve the index registers), and what registers are reserved by the compiler.
    If you like CALLEE linkage so much... why not add it to SDCC?
    But this is off topic and perhaps we should put it in another thread.

    I see much less scope in working within sdcc. I'm not sure how it would fit into sdcc's mission of supplying a one-stop compiler for several uP if the z80 target suddenly had its own asm libraries with its own quirks that diverged from all the other targets. Right now sdcc's libraries are written in C with very little in native asm and are shared across targets. Adding in a bunch of "vendor-specific" z80 libraries without hope of being available to the other uPs may also not fit into what sdcc is doing. Both these things would mean you'd have sdcc C programs that only worked properly or could only have a hope of compiling for one target. I am also not sure I like the >64k model that the ASxxx assemblers use (not sure yet because I haven't thought enough about what an ideal model would be for z80 targets but reading about it didn't make me say "that's how it should be done") and this is something that I can have some influence on in z88dk.

    The reason why sdcc is usually generating better code than z88dk is because of the global optimizers it has and z88dk lacks. However the kind of optimizations it does are the types described in introductory compiler courses and ultimately I would like to go further than that since good results are still not had from the compiler. Doing so within sdcc would mean taking care that the code generation isn't broken for the half dozen cpus it supports, some of which I am not familiar with, and the main reason I think I personally can improve on z80 code generation is because I am very familiar with the z80 and I could be more aggressive if I knew the only target was the z80. I also suspect the code generation core would need to see a rewrite anyway, just like z88dk's core so no real gains there. With z88dk I know there are at least two other active members who are willing to improve things so that if this path is taken it wouldn't have to be a solo project.

    So there you have it, it's probably not going to be me who takes on sdcc's z80 mantle :D I think there is a misplacement of importance out there being applied to how much better, in some respect, that the output of one compiler is than another. We are still talking bad code no matter what compiler we speak of and all of which is in the same league; the libraries, as I keep emphasizing, is where the performance comes from. What is needed is a new compiler core that is demonstrably better (speed *and* code size or an [adjustable/intelligent] compromise) and that generates code that at least doesn't look like it was written by a novice. Then you'll have all the z80 experts on the same wagon. But for now it's going to be each individual's preference that gets his attention with possibly borrowing of library code from others' efforts.
  • edited October 2007
    aowen wrote: »
    Haven't given up on the help project for TG, but things have been a bit hectic lately.
    I know how you feel ;-)
    Our game is being released on Oct 16th, so things are hectic here too.
    90 hour work weeks do that to a person.
    aowen wrote: »
    I know you were looking at porting TG to .NET at one point, but I was thinking it might make more sense to port it to Eclipse (which has the added benefit of platform independence.
    Sorry, but personally I can't stand Eclipse and its java platform crap - I find it way too slow and very annoying to use. Just my opinion is all.
    Don't worry I'm not porting TommyGun to .NET either.
  • edited October 2007
    aowen wrote: »
    Once our respecitive lives are less crazy lets talk about sorting out the TG help then. They've got me writing use case documents at work at the moment and I need to keep my hand in with help systems.

    pm me - so we don't get off topic here - cheers
  • edited October 2007
    There are two kinds of events in MULE. One is the player event which affects individual players and the other is the round event which affects the entire colony. Each event can only occur a finite number of times in a game (each player event, of which there are 22, can only occur once but each round event, of which there are 8, can occur either 2 or 3 times depending on the event).

    One round of the game proceeds in the following sequence:

    1. Land Grant: players competively vie for ownership of one plot of land each
    2. Land Auctions: plots of land put up by the colony or by players are auctioned off
    3. Player Turns: a random player event has a 27.5% chance of occurring at the start of a player turn.
    4. Production: the amount of goods produced on developed plots of land are shown.
    5. Round Event: a random round event affecting the entire colony always occurs. The graphical show for the event may occur prior to the goods production step (step 4) or afterward (in step 5) but the effect of the event is always applied after the goods production has already been calculated.
    ...

    Let's talk about the player events first.

    Player Event

    At the start of a player's turn there is a 27.5% chance that something happens to him. The specific player event that happens is chosen from the 22 different player events defined in the game. Each of these events can only occur once in the game so if any have already occurred they must be passed over when selecting one. There are also some other restrictions on which event can be chosen. The player in first place cannot have a good thing happen to him, only bad. The players in third and fourth place can only have a good thing happen. The player in second place can have either good or bad things happening to him.

    A player event is always presented in the same way: a scrolling message informs the player exactly what has happened and a short tune plays, one for good events and one for bad events so that the player knows immediately whether a good thing has happened or bad. He has to read the message to know exactly what has happened. Two player events (gain a plot and lose a plot) have additional graphics drawn on the display.

    The game uses the amount of time it takes for the player to start his turn after the plot screen has been drawn to determine how long into his turn it will take for the wumpus to appear (the wumpus is a flashing dot on one of the mountains which the player can move over to 'catch the wumpus' and receive a monetary reward). The sooner the player starts his turn, the shorter the time before the wumpus appears and the more time the player will have to catch him. If a player event occurs the game allows the player some time to read the message before it starts to time the player.

    So the player turn pseudo-code will look something like this:
    1. draw colony's land plots on screen with player's plots flashing.
    2. determine if a player event occurs (27.5% chance) and if so select one and start the message scroll
    3. start a timer
    4. wait for player to start turn
    5. stop the timer
    6. stop the message scroll
    7. if a player event occurred take some time off the timer
    ...
    

    The code in events.c (specifically function "e_PlayerEvent()") carries out the latter part of step 2: "select a random player event and start the message scrolling". But before we get to that let's have a look at how the player events are represented.
    static void *playerEventsFunc[] = {
    
       // Good Events (first 13)
       
       pe_RecvPkg, pe_WandTrav, pe_MuleBest, pe_MuleDance, pe_ColonyFood,
       pe_ColonyWorm, pe_Museum, pe_Eel, pe_Charity, pe_Investments,
       pe_RelDied, pe_Rat, pe_ExtraLand,
    
       // Bad Events (last 9)
    
       pe_Elves, pe_MuleBolt, pe_MiningMule, pe_EnergyMule, pe_Gypsy,
       pe_Bugs, pe_Betting, pe_Bat, pe_LostLand
       
    };
    
    static uchar playerEventsInit[] = {
       22,                                      // number of events
       13,                                      // number of good events
       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,   // number of times each good event can occur
       1, 1, 1, 1, 1, 1, 1, 1, 1                // number of times each bad event can occur
    };
    

    Each of the 22 player events has a function defined for it that is run to set up the appropriate text message and calculate the reward/punishment. The address of each of these functions is stored in the "void *playerEventsFunc[]" array as shown. Each element in the array is a 2-byte address (void*). I've taken care to list all the good events first followed by all the bad events as this will help a little later in implementing the player restrictions specified by the game (the 1st place player can only have bad things happen, etc).

    In order to ensure that each player event only occurs once in the game we add the "uchar playerEventsInit[]" array. The first element of the array, playerEvents[0], keeps track of the total number of events remaining which starts at 22 as there are 22 events. playerEvents[1] keeps track of the number of good events remaining; from these two elements it is a simple matter to compute the number of bad events remaining with "playerEvents[0] - playerEvents[1]". The remaining 22 elements of the array (playerEvents[2] through playerEvents[23]) indicate the number of times the corresponding event can occur. These numbers are listed in the same order as the player event functions in "void *playerEventsFunc[]". So, if we wanted to know how many more times the "Received a package" event can occur (whose function address is in playerEventsFunc[0]), we'd look at playerEventsFunc[2] (indices 0 and 1 are used to keep track of the number of remaining events and the number of remaining good events, remember). So the plan is once a particular event is seen, its corresponding entry in playerEvents[] is decremented from 1 to 0 so that event cannot happen again.

    Let's have a look at how this works together in the function responsible for selecting a player event:
    uint e_PlayerEvent(struct player *p)
    {
       int j, i, n;
       
       for (j = 10; j; --j)                     // try to find a qualified player event a maximum of ten times
       {
          if (p == g_sortedPlayers[0])
          {
             // first place player
          
             if (!(n = g_playerEvents[0] - g_playerEvents[1]))
                return 0;
          
             // n = # bad events remaining
       
             n = u_rnd(n) + g_playerEvents[1];
          }
          else if (p == g_sortedPlayers[1])
          {
             // second place player
          
             if (!(n = g_playerEvents[0]))
                return 0;
          
             // n = # of events good or bad
          
             n = u_rnd(n);
          }
          else
          {
             // third or fourth place player
          
             if (!(n = g_playerEvents[1]))
                return 0;
          
             // n = # good events
          
             n = u_rnd(n);
          }
       
          // 'n'th event has been selected
       
          for (i = 2; (n -= g_playerEvents[i]) >= 0; ++i) ;
       
          if ((playerEventsFunc[i-2])(p))       // if the player event is qualified (and with this call has been run)
          {
             --g_playerEvents[0];               // number of available events decreased
             --g_playerEvents[i];               // this particular event can occur one less time
             
             if (i < 15)                        // if this was a good event...
             {
                --g_playerEvents[1];            // decrease number of available good events
                // play good thing ditty
             }
             else
             {
                // play bad thing ditty
             }
    
             strcat(g_textBuffer, "   ");       // add spaces to end of message for looped scroll
             d_ScrollStart();                   // start scrolling event text message
             return 1;                          // indicate qualified event found
          }
       }
       
       return 0;                                // failed to find a qualified player event
    }
    

    When "e_PlayerEvent()" is called it has already been determined that a player event will occur (the 27.5% chance succeeded). However that does not necessarily mean a player event can occur -- there may not be any left (there are only 22 events but 4*12=48 opportunities for events to occur in a game) or a qualified event may not be available anymore. By qualified event I mean an event that is allowed to happen to the player. For example, the first place player cannot have a good thing happen to him so if there are no bad events left he doesn't get one. Likewise if the only event left has the player pay for repairs to his energy mules when he doesn't own any, this event will not happen either.

    So the way I've handled this is to try 10 times to select a random event for the player. If after ten times a qualified event can't be found then no event happens. The function returns a value of 0 if a player event can't be found and 1 if a player event was selected and in this way the caller knows whether to adjust the "wumpus timer".

    The first "for" loop runs 10 times giving us 10 opportunities to find a qualified event for the player. Inside that loop we choose a random event. First place, second place and everyone else are treated differently. The first place player case, "if (p == g_sortedPlayers[0])" ..., first computes the number of bad events remaining in "n". If that's found to be zero the function returns 0 indicating that no qualified event was found. Otherwise n is reassigned a random number selecting one of those bad events. The total number of good events remaining gets added to this number for reasons that will be clear in a minute. The other cases are similar with the second place player allowed to have any event happen (good or bad) and the third/fourth place players only allowed to have good things happen.

    With a random event chosen in "n" this "for" statement is run to find the specific event "n" corresponds to:
    for (i = 2; (n -= g_playerEvents[i]) >= 0; ++i) ;
    

    "n" identifies one of the remaining available events since it was randomly selected from the total number of available events. The "g_playerEvents[]" array is walked through with "n" being decreased when the corresponding event indexed by "i" (ie "g_playerEvents") is still available. When "n" goes negative we have found the available event randomly selected. The first place player code discussed above had the total number of available good events added to its "n"; in this way this loop will skip over all the good events and arrive at a bad one.

    "i" now contains an index identifying a specific event. But there is still no guarantee that the event is qualified for the player. To find out we actually have to run the event code. All 22 event functions are written to return 0 if the event is disqualified for the player or 1 if it was successful.

          if ((playerEventsFunc[i-2])(p))       // if the player event is qualified (and with this call has been run)
          {
             --g_playerEvents[0];               // number of available events decreased
             --g_playerEvents[i];               // this particular event can occur one less time
             
             if (i < 15)                        // if this was a good event...
             {
                --g_playerEvents[1];            // decrease number of available good events
                // play good thing ditty
             }
             else
             {
                // play bad thing ditty
             }
    
             strcat(g_textBuffer, "   ");       // add spaces to end of message for looped scroll
             d_ScrollStart();                   // start scrolling event text message
             return 1;                          // indicate qualified event found
          }
    

    The "if ((playerEventsFunc[i-2])(p))" my look a little complicated but looks are deceiving. "playerEventsFunc[i-2]" returns the function address for the player event. We call this function by enclosing it in brackets and passing it its parameter (all player events take a pointer to the player struct as parameter) like so: "(...)(p)". This causes the player event function to be called and a return value of 1 (true) is used to indicate the player event is qualified. If that was the case the event has already been run and we continue inside the "if" block.

    First the total number of available events is reduced ("--g_playerEvents[0]") and we make sure this particular event will not occur again with another decrement ("--g_playerEvents"). Next it is determined if the event was good or bad (the first 13 are good, recall, and the array has two counters at the start so if "i" is less than 15 we know it was a good event). If good, the good ditty is played and the total number of available good events is decreased. If bad the bad ditty is played.

    The player event code will have placed its text message for scrolling into a text buffer pointed at by the global variable "g_textBuffer". Some spaces are added to the end so that the message does not run together should the player wait long enough that the message loops. Then the scrolling is started with a call to "d_ScrollStart()", which was described in a previous post, and 1 is returned to indicate a player event was found.

    ... cont'd
  • edited October 2007
    ... cont'd

    And that's it! Let's have a look at the code for one of the player events so that we have a complete picture of how things work:
    static int pe_MiningMule(struct player *p)
    {
       uint y;
       uint t;
       
       if (!(y = p->numPlots[COMMODITY_SMITHORE] + p->numPlots[COMMODITY_CRYSTITE]))
          return 0;
       
       t = event_multiplier() * 2;
       strcpy(g_textBuffer, "YOUR MINING MULES HAVE DETERIORATED FROM HEAVY USE AND COST $");
       u_AppendInt(g_textBuffer, t);
       strcat(g_textBuffer, " EACH TO REPAIR. THE TOTAL COST IS $");
       u_AppendInt(g_textBuffer, t *= y);
       p->money = u_lmax(p->money - (long)(t), 0L);
       return 1;
    }
    

    This is one of the bad events where the player has to pay for repairs to all his mining mules (both smithore and crystite). The function name "pe_MiningMule()" has a leading "pe_" to indicate it is a player event. It is declared "static" so that it can't be called from outside this file, it accepts the player as parameter and returns a 0 (no) or 1 (yes) to indicate if the event can occur to the player. All 22 player events have this same signature and you are welcome to look them over.

    The first task in "pe_MiningMule()" is to determine if the event can occur to the player. Since this event requires that the player owns mining mules the first thing done is to count the number of smithore and crystite developments the player owns. If the total is zero, the function returns 0 indicating the event is to be disqualified.

    Otherwise a text message is built up in a buffer pointed at be "g_textBuffer" indicating the total amount owed by the player. "strcpy()" and "strcat()" are well-known functions found in "string.h"; if you need a refresher as to what they do you can consult the z88dk wiki. Although it's been a while since we visited it, you may recall that "u_AppendInt()" writes the text representation of an integer at the end of the string passed to it. In any case you should recognize from the leading "u_" in the name that this is a function from this project whose definition can be found in "utility.c" which you can revisit if you need a reminder of how it works.

    The total cost calculation is specified in kroah's document, as is the "event_multiplier()" number, which is a cost function that increases as the game carries on.

    Once the text message is built up, the player's wallet is docked the requisite $ but we make use of "u_lmax()" to make sure the player's money doesn't go below zero. Again, this function has a leading "u_" meaning you'll find it in "utility.c" should you need a reminder of how it works.

    With all that carried out "1" is returned to indicate the event was successful. The message built up in "g_textBuffer" is scrolled by the scrolling isr we've seen already which is in turn switched on by the "e_PlayerEvent()" function as we've just seen.

    So this is the pattern for all 22 functions. All are very simple and don't require any further work except for the "land gain" and "land lost" events; for these we will need to flash the plot affected and this I'll leave as an easy assignment once I've collected together the to-do list.

    Round Events

    A round event affecting the colony as a whole always occurs in each round. There are 8 different kinds but this time each kind is allowed to occur either 2 or 3 times in a game depending on the event. Like player events, some round events cannot occur unless conditions are met so they too must be "qualified". The round event functions and the number of times remaining that each round event can occur are stored in two arrays just like the ones used for the player events:
    static void *roundEventsFunc[] = {
       re_AcidRain, re_Sunspot, re_Planetquake,
       re_PestAttack, re_PirateShip, re_Meteorite,
       re_Radiation, re_Fire
    };
    
    static uchar roundEventsInit[] = {
       20,                                      // sum of the following numbers
       3, 3, 3, 3, 2, 2, 2, 2                   // number of times each round event can occur
    };
    

    The address of the each of the eight round event functions are stored in the array "roundEventsFunc[]". Again the order is significant; the first three functions are round events whose graphical presentation should be displayed before the lands production is displayed. All other round events have their graphical presentation shown after the lands production is displayed.

    The "roundEventsInit[]" array holds the number of times each event can occur in the game. "roundEventsInit[0]" is the total number of round events still available. So, eg, the first round event is "Acid Rain" as you can see from "roundEventsFunc[0]". The number of times this event can occur is held in "roundEventsInit[1]" (we add 1 to the index because roundEventsInit[0] holds the total number of available events).

    This set-up is very similar to the player events arrangment. Now the code that selects a round event:
    void *e_RoundEvent(uint *flag)
    {
       int i, n;
       
       do
       {
          n = u_rnd(g_roundEvents[0]);          // choose random round event
          for (i = 1; (n -= g_roundEvents[i]) >= 0; ++i) ;
       } while (!((roundEventsFunc[i-1])(1)));  // choose another if this round event doesn't qualify
       
       --g_roundEvents[0];                      // number of round events available decreased
       --g_roundEvents[i];                      // number of times this event can occur decreased
       *flag = (i < 3) ? 0 : 1;                 // set to 1 if event should occur after production is shown else before
       
       return roundEventsFunc[i-1];             // return address of round event function to call
    }
    

    This time we know a round event will always be picked and that there will always be a qualified round event available so no more "for" loop with a maximum of 10 tries; this time there's a "do-while" loop that keeps on going until a qualified event is found.

    The algorithm is the same as was used for the player events. We pick a random event from the total number we know are available. We find which event index "i" corresponds to the random event number chosen and we check to see if the round event is qualified in the "while (!((roundEventsFunc[i-1])(1)))" condition. Again this looks more complicated than it really is. "roundEventsFunc[i-1]" is the address of the event function selected. We execute that function by enclosing it in brackets and passing its single required parameter as in "(...)(1)". The round event will return 0 if the event is unqualified and in that case the "do-while" loop will repeat in order to choose another event. If we get out of the loop we have found our round event with index "i-1".

    The round events come in two groups :- the first group are events that should be run before the land productions are plotted on screen and the second group are events that should be run after the land productions are plotted. So this round events subroutine does not run the event itself but only selects a qualified event to be run by the caller. Accordingly all round events functions have a "test" mode where if they are passed a "1" as parameter (as they were in the do-while loop above) the events function does not actually perform the event but only checks to see if the event is qualified. If the parameter passed is 0, the events function does perform the round event which includes any graphical presentation and calculation of effects.

    So all this function does is find a random qualified event and return the address of the event's function so that the caller can run the event at the right time. It also returns a flag value in "flag" which indicates when the event should be run (before the land production is shown or after).

    The rest of the code above should be self-explanatory. After selecting a qualified round event, the number of available events is decreased by one and the number of times the selected event can occur is also decreased by one.

    I have been speaking of the "roundEventsInit[]" array and the "playerEventsInit[]" array in the text but in the code you'll see "g_roundEvents[]" and "g_playerEvents[]" used instead. The reason is these array are modified while the game runs. If another game starts the original values must be restored to play properly. So, the "roundEventsInit[]" and "playerEventsInit[]" are constant templates that store the start values for these arrays and the actual arrays used in the game are "g_roundEvents[]" and "g_playerEvents[]". Before a game starts the function "e_EventsInit()":
    void e_EventsInit(void)
    {
       memcpy(g_playerEvents, playerEventsInit, 24);
       memcpy(g_roundEvents, roundEventsInit, 9);
    }
    

    is run to initialize the game arrays with their start values by copying the corresponding templates.

    All 8 round events functions have their logic complete but all 8 are missing the graphical portion of their presentation. This will be added to the to-do list as something people can contribute code to. We'll take a look at these round events functions in more detail when the work required for them is spelled out in detail.

    If you've noticed that the code I am quoting here is different from what you have from the last code package, that's because it is. I have replaced the download link in the download post so you can pick up a new version. EDIT: that's a number of posts back so I'm also putting the link here.

    So that's it for the events code... next up is a bunch of programming assignments. Here is your chance to contribute and have your name forever in the source code.
  • edited October 2007
    just read it from beginning to end :)
  • edited October 2007
    Fikee wrote: »
    just read it from beginning to end :)

    Ah, glad to hear it is worthwhile reading :)
    Crisis wrote: »
    which file should i compile first
    or are there 'only' blocks now still not bound.
    i stranded with that last time.

    The project still isn't compilable as it's missing a lot of pieces yet. I have a pretty good idea of how an entire project is to work before I write the first line of code so it's not usually until the project is close to completion that it is ready to compile.

    However the pieces of project that haven't been completed yet, which mainly consists of all the graphics stuff, that will be offered up for contributions can be compiled separately so that people can see if the work they do is correct. Then we'll extract the relevant code and add it to the project.

    Again short of time this past week so no updates (yet). I am thinking about including the rest of the code in one shot and then opening it up for contributions instead of this piecemeal thing that's going on now.
  • edited February 2008
    aowen wrote: »
    block copy will do. If I can keep the rest of the code under 172 bytes then the whole lot will come in at 2.5K of RAM.

    Will that leave enough room for the rest of the code/data?

    Ah nice, thanks Andrew. It's going to be pushing it I think but I believe it will be workable.

    The project's been on hiatus a bit as you can tell as I've been quite busy and haven't got back to spectrum things for a while. But I'm hoping to resume this week.
  • edited February 2008
    I've almost finished the SevenuP parser for TommyGun.
    Should be ready on the weekend.
    It will be missing the interleave options (uses sprite by default)
    Basically because I couldn't understand the miles of code in the SevenuP conversion routines with the variables names used - all too confusing.

    Anyway new update on the weekend and it also contains a bug fix for the Text drawing tool for those you have emailed me about it.
  • edited February 2008
    TommyGun now has the option to Parse data the same (or similar) way to SevenuP.

    Get the latest version from my website or WoS.

    I hope it works ok for your project - Good luck with it all.
    Cheers
  • edited February 2008
    Kiwi wrote: »
    TommyGun now has the option to Parse data the same (or similar) way to SevenuP.

    Get the latest version from my website or WoS.

    I hope it works ok for your project - Good luck with it all.
    Cheers

    Cheers Kiwi! I am now getting off my arse and back into the swing of things. An early test shows it seems to be doing what it should. I noticed that there can only be one "view" of a resource per file (ie I can't put a graphics-only definition and an attribute only definition of the same sprite in the same file) but I think I prefer to have graphics in one file and attributes in another file anyway. Also I haven't tested it properly yet but it looks the the mask option for sprite attributes is writing two copies of the mask rather than than a (mask,attr) pair. I will have to investigate further since TechSi did some weird stuff with the sprite graphics (eg drawing white pixels on black paper) which may be interacting with my idea of what should be in the definitions.
  • edited February 2008
    Cheers Kiwi! I am now getting off my arse and back into the swing of things.
    Your welcome - me too I had to get off my arse and finish the plugin :D
    An early test shows it seems to be doing what it should. I noticed that there can only be one "view" of a resource per file (ie I can't put a graphics-only definition and an attribute only definition of the same sprite in the same file) but I think I prefer to have graphics in one file and attributes in another file anyway.
    This is because TG uses the sprite name in the reparse tags.
    So it sees the sprite name and reparses it into the new format.
    And yep - a separate file is the only and best way to go.
    Also I haven't tested it properly yet but it looks the the mask option for sprite attributes is writing two copies of the mask rather than than a (mask,attr) pair. I will have to investigate further since TechSi did some weird stuff with the sprite graphics (eg drawing white pixels on black paper) which may be interacting with my idea of what should be in the definitions.
    You are half right.
    I construct the mask value then apply the mask to the attribute value before it is written out. The Attribute mask is always constant so where is the sense in writing out a constant value x number of times?
    But it only writes out 1 mask value (with the attribute mask applied if required)
    Is this the correct behaviour?
    My initial tests between SevenuP and TG show the data count to be correct.
    But I have just checked the attribute output and it is incorrect.
    I will fix it and upload a new version soon.
  • edited February 2008
    There is an update on my site. www.users.on.net/~tonyt73/TommyGun.exe
    0.9.24

    Only fixes the SevenuP Attribute output.
  • edited February 2008
    Kiwi wrote: »
    I construct the mask value then apply the mask to the attribute value before it is written out. The Attribute mask is always constant so where is the sense in writing out a constant value x number of times?

    The (attr_mask, attr) pair is a sprite character property so one pair for each character in the sprite is necessary. When drawing, the engine does "(attr_mask AND screen_attr) OR attr" to compute the next colour of the character square so that the attr_mask has bits set where we want to keep the background colour. The most general thing is to not apply any masking to the attr despite what attr_mask is.

    If you want a sprite to be INK colour only (ie the sprite does not affect the background's PAPER, FLASH, BRIGHT) then you'd set the attr_mask to have set bits everywhere except the INK bits (in TG you'd check all the boxes except the INK one) and you'd draw the sprite with black PAPER, FLASH off and BRIGHT off (this way the OR operation done when the sprite is drawn will not affect the background PAPER, FLASH, BRIGHT).

    As to why there is an independent attr_mask for each sprite character -- well it speeds things up a little as when the engine is drawing it only has access to sprite character data and not overall sprite info. But the main reason is I wanted to be able to set attr_mask independently for each character square so that you could mitigate colour clash issues depending on how much the sprite was rotated horizontally and/or vertically. There's a fast function in SP1 that will recolour a sprite by blittiing these (attr_mask,attr) pairs into the sprite structures so that it would be possible, eg, to have statements like "if the sprite is rotated right by 5 pixels, use these masks and colours; if it's rotated right by 7 pixels, use these mask/attr, etc" and maybe this will help to reduce the effect of clash. Although I don't know as I haven't fiddled with this; all I knew as I was implementing this is this was the most general thing to do and it didn't cost anything in speed (in fact sped it up!) .

    Anyhow, since attribute masking on a character level doesn't fit into current spectrum graphics editors a fair compromise is to apply the same mask value to all sprite characters when outputting the (attr_mask, attr) pairs. Then you'll get the INK-only, PAPER-only sprite colours without any trouble (althought to save on memory it's best to algorithmically colour the sprite char squares in these cases). But my idea is for specialized circumstances you would go in there and edit the attr_masks by hand. In SevenuP you can just edit the output file. In TG you have the resource thing which will overwrite any hand-modified attr_masks but I think what I would do is when all is done, copy and paste the data outside the resource block where the data can be hand-modified as desired and erase the resource block when the project is ready to go.
    Kiwi wrote: »
    There is an update on my site. www.users.on.net/~tonyt73/TommyGun.exe
    0.9.24

    Only fixes the SevenuP Attribute output.

    It looks like there is still a problem. Here's a snippet of one sprite (attribute only):
    ; [RESOURCE: <Parser:SevenuP Image Parser><Resource:Sprite\Packer (R)><Data::scAsm:apDEFB:doAt:MB:AM:abIn:MI:ilSp:soXC:soYC:soCL:soMk:soFN:Bin>]
    ;-------------------------------------------------------------------------;
    ; C source file created by TommyGun (Based on output from SevenuP v1.20   ;
    ; SevenuP  (C) Copyright 2002-2006 by Jaime Tejedor Gomez, aka Metalbrain ;
    ; TommyGun (C) Copyright 2005-2007 by Tony Thompson, aka Kiwi             ;
    ;-------------------------------------------------------------------------;
    ; Name   : Packer (R)
    ; Frames : 8
    ; Size   : 16x24
    ; Palette: ZX Spectrum
    ; Masked : No
    ; RowOrder: Classic
    Packer (R):
    	DEFB	&#37;00100111, %00000111, %00000111, %00000111, %00000111, %00100111, %00100111, %00000111
    	DEFB	%00000000, %00000111, %00000111, %00000111, %00000111, %00000111, %00000111, %00000111
    	DEFB	%00000111, %00000000, %00000111, %00000111, %00000111, %00000111, %00000111, %00000111
    	DEFB	%00000111, %00000111, %00000000, %00000111, %00000111, %00000111, %00000111, %00000111
    	DEFB	%00000111, %00000111, %00000111, %00000000, %00000111, %00000111, %00000111, %00000111
    	DEFB	%00000111, %00000111, %00000111, %00000111, %00000000, %00000111, %00000111, %00000111
    	DEFB	%00000111, %00000111, %00000111, %00000111, %00000111, %00000000, %00000111, %00000111
    	DEFB	%00000111, %00000111, %00000111, %00000111, %00000111, %00000111, %00000000, %00000111
    	DEFB	%00000111, %00000111, %00000111, %00000111, %00000111, %00000111, %00000111, %00000000
    ; ]RESOURCE: <Parser:SevenuP Image Parser><Resource:Sprite\Packer (R)>]
    

    The sprite is 2 chars across, 3 chars tall and 8 frames. So that's a total of 48 char squares so I should be seeing 48 (attr_mask, attr) pairs for a total of 96 bytes. But I'm only seeing 72.

    It also looks like the output is (attr, attr_mask), ie reverse order. I don't think there is an option to change that ordering? I tried the graphics button (mask before graphics) but it didn't have an effect.

    The output order I chose is intended to go left to right then top to down which I think it's doing but with the bytes missing some things may be appearing in the wrong order.

    The sprite is coloured mainly as white ink on black paper (%00000111) with INK set in the attr_mask (%00000111). I put some white ink on green paper (%00100111) in the first frame (top left char, bottom left char) which should appear in the 1st and 5th (attr_mask,attr) pairs. Ie the first two bytes in the first DEFB line and the first two bytes in the second DEFB line. Something happened to the attr in the second DEFB line -- I don't know what but this could also be a pair from somewhere else due to the missing bytes. Although there aren't any black on black attributes in any of the frames...
  • edited February 2008
    Ok, I reckon I have fixed it now. :D

    You can either get the full version (0.9.25) from my site www.users.on.net/~tonyt73/TommyGun or you can simply grab the SevenuP parser plugin www.users.on.net/~tonyt73/SevenuPImageParser.cpi and place into the TommyGun\Plugins\_ZX Spectrum folder.
  • edited February 2008
    After a long hiatus the project is ready to resume!

    Since TechSi has already drawn the graphics in TG and TG now supports SP1's crazy output format, I've rolled the entire project into TG. You can download the lot here (the graphics and code that we've covered thus far):

    TG-mule-pack
    Unzip into {TommyGun}/Projects and you should get a "mule" subdir there.
    When starting TG you select open a new project and locate the project file in the mule subdir.

    The project will ultimately be released in two forms: a TG project (IDE approach) and a more traditional source tarball with Makefile and SevenuP used to generate graphics.

    I only made a few changes since the last time the code was discussed, none worth mentioning that I can recall. The plan is to get the rest of the necessary code done so that the game can compile and play (sans AI) so that all the gameplay / graphics / animation/ sound effects / music elements can be added and polished by you folks out there :). And if someone wants to do AI, well, that would be fantastic because AI is a bit scary.

    The next block of code I'll be adding will do this -- expect things to not look right for sure -- and this will be the last bit of code I already have written. After this it will be your contributions (with some from me too; I'm not going anywhere) that make it look and play right.

    Compiled C code, as we all know -- or will find out shortly --, is much bigger than hand assembled code. So I thought it might be interesting to estimate how big our compiled code is going to be thus far. I assembled several of the .c source files using "zcc -a foo.c" that translates C code into assembler. From this we can take a guess at how large the assembly will be once assembled.

    All code generated by the compiler pretty much looks the same throughout. You'll see this if you investigate the *.opt files (the result of "zcc -a") I included in the above TG code package. Compiled code pretty much consists of pushing stuff onto the stack, calling subroutines, popping stuff off the stack and performing stack-related manipulations. The result is that the compiled code is surprisingly homogenous.

    This means I can open up one of the translated C files (events.opt) and look at the number of bytes the assembly version would take up in, say, 100 lines of asm and extrapalote that information to make a good guess at how much memory the entire program will require. I did just that -- I counted the number of assembled bytes in lines 17 through 128 inclusive of events.opt and came up with 176 bytes, which works out to 176/(129-17) = 1.57 bytes per line of assembly code.

    Let's use 1.6 bytes per line. Having a look at the number of assembly code lines in several key files we see the following:
    FILE                LINES    CODE SIZE ESTIMATE
    
    display.opt         1540     2464 bytes
    events.opt          2767     4427 + 2378 (text) = 6805 bytes
    player.opt          1205     1928 bytes
    plot.opt            1407     2251 bytes
    store.opt             25     40 bytes
    utility.opt          245     392 bytes
    
    TOTAL               7189     13880 bytes
    

    Add in about 5k of library code and the 12k or so of data area SP1 reserves by default and we're already up to 30k. Fortunately most of the program code is done but we've got AI, music, graphics and the intro to worry about yet. SP1 can be configured to be smaller so there's some space to be reclaimed there but it's going to be tight! Aiming for a 48k implementation is the goal so we may have to resort to data compression and perhaps redoing some large C functions in assembler. However that's a task for the end of the project so we don't need to worry about it yet.
Sign In or Register to comment.