Sprite (Tutorial): Difference between revisions

From MZXWiki
Jump to navigation Jump to search
(→‎Sprite Tutorial: Finished Exercise 3.2, rough cut of Exercise 3.3. Code examples still need writing.)
Line 241: Line 241:
: : "edgecollide"
: : "edgecollide"
: send "global" to "edge&spr('local')_lastmove&"
: send "global" to "edge&spr('local')_lastmove&"
: goto "#return"</tt>
: goto "#return"</tt></li>
<li>Now we're going to use a nice little shortcut so that we can keep track of board adjacencies with the standard MZX system instead of writing another data structure to do it.  All it requires is that the four corners of each board you use it on be empty.  That's because we're going to use the player object itself to move between boards, instead of using teleport player.  Write each edge# method similar to this:<tt>
<li>Now we're going to use a nice little shortcut so that we can keep track of board adjacencies with the standard MZX system instead of writing another data structure to do it.  All it requires is that the four corners of each board you use it on be empty.  That's because we're going to use the player object itself to move between boards, instead of using teleport player.  Write each edge# method similar to this:<tt>
: : "edge0"
: : "edge0"

Revision as of 08:33, 9 November 2007

Sprites were introduced to MegaZeux in version 2.65, primarily as a method of making it easier to implement large object representations in the game (for example, engines for handling a multiple character player or enemies). They include many features such as collision detection, easy drawing, and configurable draw order. Despite being designed to make coding easier, many MZXers avoid their use in favor of more traditional methods such as overlay buffering and hand-rolled collision routines. This is largely out of a perception that Sprites are difficult to use.

Sprite Tutorial

Despite the mystery and trepidation that generally surrounds them, sprites are not that hard to work with and are designed to be easy to use, with the right program model. Two common and effective models, which can be used together or alone depending on the requirements of the engine, are the sprite-layer model and the sprite-object model. The structure of your code will depend on the application and the model being used, but there are some basics to cover first.

The Basics: Drawing Sprites

All sprites, regardless of their function, require some basic initialization and setup before they can be used. First, you need to set aside space on the board (or the Vlayer) for the sprite source image. You don't even have to put anything there yet, some advanced techniques construct the image data dynamically. But you need a block of space that is going to be used for sprite data. Next, all sprites need initialization code somewhere that designates this area.

set "spr#_refx" to XCOORD      These counters specify the x coordinate, y coordinate,
set "spr#_refy" to YCOORD      width, and height of the bounding rectangle for the source image
set "spr#_width" to WIDTH      Replace # with the number of the sprite, from 0 to 255.
set "spr#_height" to HEIGHT    Counter interpolation is acceptable and in fact common for this.

Finally, all sprites need to be placed somewhere in order to be viewed. While "spr#_x" and "spr#_y" counters do exist, and they can be written to place and move the sprite, we recommend you treat them as read-only and use the "put" command for readability and clarity of purpose:

put c?? Sprite # at X Y        # is the number of the sprite to be placed, as a parameter.  Can be a counter.

Exercise 1.1: Making a sprite based player

A common application for sprites is to have all actors in the game, including the player, be represented by sprites. This lends itself well to a sprite-object model, but we'll come to that later. Our first exercise will be to make a player sprite that can be moved around with the arrow keys.

  1. First, start editing a new world, and draw a customblock smiley face on the board. It doesn't really matter what it looks like, or where it is, but for the sake of example put it at (80,0) off the right edge of the screen, and make it 3x3 characters large.
  2. Create a new robot called "sprite" to handle the sprite drawing and moving code. I like to put my control robots in a horizontal line starting at (1,0), but again, it really doesn't matter. The very first line of the robot should be "lockplayer", just to keep the player from moving around.
  3. Let's use sprite 0 for our player. So, the next 4 commands should be:
    set "spr0_refx" to 80
    set "spr0_refy" to 0
    set "spr0_width" to 3
    set "spr0_height" to 3
  4. Now we need a control loop that draws the sprite and handles input.
    : "drawloop"
    wait for 1
    put c?? Sprite p00 at "x" "y"
    if uppressed then "up"
    if leftpressed then "left"
    if rightpressed then "right"
    if downpressed then "down"
    goto "drawloop"
    Note that x and y will default to 0 since they haven't been used yet. It's generally a good idea to keep this information in a pair of local counters and initialize them along with the rest of the sprite data.
  5. Now, you just need code to modify the values of "x" and "y" for each label "up", "left", "right", and "down".
    : "up"
    dec "y" by 1
    goto "drawloop"
    And the rest should be obvious: inc "y" by 1 for "down", dec "x" by 1 for "left", and inc "x" by 1 for "right".

Example 1.1 Code

Exercise 1.2: Improving and generalizing the code

That's it, you can now test your world and move the sprite around. You'll notice that the sprite gets "stuck" if you move it off the top or left of the board, and disappears off the bottom right. That's because we didn't do any bounds checking. In fact, there are a lot of improvements we can make to this code before we move on.

  1. First let's make the sprite initialization dynamic. This may seem like more work now, but it'll make things much easier when you want to play with a lot of objects later, and decide that you need to reallocate sprite numbers. We'll declare local counters for the sprite draw location while we're at it.
    set "local" to 0
    set "local2" to 5
    set "local3" to 5
    set "spr&local&_refx" to 80
    set "spr&local&_refy" to 0
    set "spr&local&_width" to 3
    set "spr&local&_height" to 3
    : "drawloop"
    wait for 1
    put c?? Sprite "local" at "local2" "local3"
  2. We also need some bounds checking. I prefer to define variables for zone boundaries, but we'll use constants with expressions for now.
    : "up"
    if "local3" <= 0 then "drawloop"
    dec "local3" by 1
    goto "drawloop"
    : "down"
    if "local3" >= "(25-'spr&local&_height')" then "drawloop"
    inc "local3" by 1
    goto "drawloop"
    Remember that "local3" is "y".
  3. You also probably noticed that the previous movement routine favored certain directions over others; you can fix this by turning the label calls into subroutines (with diagonal movement as a free bonus!)
    if uppressed then "#up"
    ...
    : "#up"
    if "local3" <= 0 then "#return"
    dec "local3" by 1
    goto "#return"
  4. One pitfall that comes with using subroutines for movement like this is that it's possible to get the sprite stuck on corners as you move it. This is because the sprite position is not updated after the sprite counters are changed, but only after every move direction has been checked. One way to fix this is to just use the spr#_x and spr#_y counters instead of local counters, since those are directly tied to sprite position. But for reasons we'll discuss later, I recommend against this, or at least keeping copies of them in local counters. The other way is to make sure the sprite is updated (i.e. drawn) after each move. This is accomplished most easily through subroutines as well:
    : "#draw"
    put c?? Sprite "local" at "local2" "local3"
    goto "#return"
    Then, for every location where the sprite needs updating, that is at the end of all movement subroutines and within the main loop, call the subroutine "#draw" with a goto. In fact, it's not really even necessary to call this in every iteration of the main loop if it's called on movement. It only needs to be called once after all the sprite counters have been initialized.
  5. Since this is our player sprite, it would be nice if we could get the screen scrolling to act like it. Fortunately there's a really easy way to do this without having to work out a bunch of bothersome math. Just put the following functional counter in your #draw subroutine, after you draw the sprite:
    set "spr&local&_setview" to 1
    Really simple, and another good reason to have a separate subroutine for the drawing code, since that makes it easy to update and augment. You can now expand the board area and bounds checking beyond the confines of the viewport.

This leaves us with this final code: Example 1.2 Code

The Not So Basics: Sprite Collision

So we can now draw a sprite and move it around the screen. This is great, but except for some very limited cases is not particularly useful for gameplay. Simple bounds checking is easy enough to implement, but what if we want to have our player sprite move around inside a defined terrain, with walls and solid objects and impassible barriers? Worse, what if we want to interact with the environment, or with other sprites? Designing this from scratch would involve doing a lot of checking for customblocks, a way to figure out which sprite is at a specific location, and depending on the size of the sprite being moved, would require checking multiple locations each time in order to ensure consistency. It would be difficult to make the system general enough to be transplantable from game to game, and if you did manage it it would be hard to read and understand the code.

Fortunately, MZX provides a way to do all that work with one statement:

if c?? Sprite_colliding p## at X Y then LABEL   p## is the number of the sprite you want to move, X and Y are relative coordinates.

One of the reasons people find this simple command so difficult to use is that they don't understand what it actually does. The first important requirement is that the sprite being moved define a collision rectangle. This should be done along with the other sprite initialization counters like so:

set "spr#_cx" to X             These X and Y coordinates are relative values.
set "spr#_cy" to Y             That means you set them relative to (0,0) as the top left corner of the sprite.
set "spr#_cwidth" to WIDTH     So if you want the collision rectangle to be the same size and area as the sprite itself,
set "spr#_cheight" to HEIGHT   cx and cy should both be 0, and cwidth and cheight should be the same as width and height.

Most people understand this much. What often gets confused is that the Sprite_colliding object in the if statement is NOT this collision rectangle. Nor does it directly represent the collision rectangle of another sprite, though it is necessary for other sprites to have collision rectangles in order for collision to work. But there isn't an actual sprite_colliding object anywhere on the board, this is simply the syntax used to call collision detection for a sprite in advance of movement. The command says "if I hypothetically move this sprite (specified by the parameter) X by Y from its current location, will it collide with anything." And then it branches depending on whether the answer is true or false.

The two key points about X and Y are that they are relative to the sprite's current location, and they specify the movement of the sprite, not the position of something else. The color term is co-opted to perform a non-intuitive task of specifying relative versus absolute movement. c?? means that X and Y are relative values, and is what you will normally want to use. c00 (or any other absolute color) make X and Y absolute coordinates, but again the statement checks to see what would happen if you put the sprite at that location, not for the presence of some other object at that location. This is vital to understand, since without it you will probably try to do much more work than you need to do, and will probably achieve unexpected and incorrect results for your efforts.

Exercise 2: Adding basic collision detection

Let's see if we can't improve our player sprite code and turn it into something you might actually want to use in a game.

  1. First, take that board you were working on and draw some walls on it. Sprite collision detection only works with customblocks and other sprites, so in any game where you want to use sprite collision, all of your collidable scenery should be customblock. But if you're really interested in advanced MZX programming and artwork, you should already be doing this anyway.
  2. In order to create the illusion of depth and a 3/4 camera angle, we'll define the collision rectangle as being only the bottom row of the sprite. The sprite dimensions are 3x3, so that means:
    set "spr&local&_cx" to 0
    set "spr&local&_cy" to 2
    set "spr&local&_cwidth" to 3
    set "spr&local&_cheight" to 1
  3. Update each of your movement subroutines to include a collision check along with the bounds check:
    : "#up"
    if "local3" <= 0 then "#return"
    if c?? Sprite_colliding "local" at 0 -1 then "#return"
    dec "local3" by 1
    goto "#return"
  4. Finally, going along with that 3/4 camera thing, you will generally want game sprites farther down the screen to appear "in front" of sprites above them. Sprites have a globally applicable drawing mode to handle this very thing:
    set "spr_yorder" to 1
    Note that this counter is global to all sprites, and is not a per-sprite counter. You can stick it anywhere you like, but it makes the most sense to put it in the global robot. We will discuss how to use layer-like sprites with yorder later. For now, all you need to know is that this makes sprites draw in order of position from the top of the screen to the bottom, instead of in order of their sprite numbers.

And that's it! That's really all you have to do!

Example 2 Code

The Sprite-Object Model: Active Collision

It's wonderful and all that we can make sprites not do something (i.e. move) if they collide. But what if we want to make them do something else instead? What if we want that thing to be different depending on the object of the collision? When you perform a collision check with "if sprite_colliding", it has the side effect of populating an array-like construct that details what, if anything, was collided with. This array is called "spr_clist", its length is stored in the counter "spr_collisions", and it is accessed as pseudo-arrays usually are in MZX, through counter interpolation. Here is a typical and flexible collision handler:

: "collision"
loop start
goto "#collide('spr_clist&loopcount&')"        This will only goto labels that actually exist, making it easily pluggable.
send "spr('spr_clist&loopcount&')" to "touch"  This will send to a robot named with the same number as the colliding sprite.
loop for "('spr_collisions'-1)"                Don't forget about the loop termination quirk, where the terminator is inclusive.
goto "#return"                                 Short circuits whatever else would have been done had collision not happened.
: "#collide-1"                                 -1 is the background, meaning the sprite collided with a customblock.
* "~fOuch, I bumped into a wall!"              Normally you wouldn't bother with this label though, since a wall is a wall.
goto "#return"                                 Continues collision handling.  Remember, subroutines go on a call stack.

The send command is the beginning of understanding sprites with an object model, where each sprite is bound to code in a robot specific to that sprite, so that everything relevant to that sprite (including counters you may create) can be accessed with a single number. Here, we use that number to send the sprite (and I find it useful to make no distinction between the sprite and the robot controlling it) a touch label.

You should devise and maintain a convention for what robots that control sprites should be called. "sprite#" or "spr#" are normal, or you could just reference them by number. In order to make things as code driven as possible, so that you only have to change a single line to reassign a sprite number in a robot, you should do this dynamically in your robot setup with a rename command. And there's some other stuff you can do to make it easy to move sprites around just by moving the robots that control them.

set "local" to "robot_id"   This makes things very dynamic, since you can copy the same robot around the board with no changes
set "local2" to "thisx"     and make multiple copies of the sprite.  Setting local2 and local3 based on the robot's position
set "local3" to "thisy"     means that you don't have to configure the initial sprite placement, either.  Then you can move the
gotoxy "local" 0            robot into a robot bank at the top of the board, and the unique value of local ensures you won't
. "@spr&local&"             overwrite anything.  But this is not always appropriate, sometimes you'll want to choose a specific ID

Exercise 3.1: Keys and doors with sprites

Now that we have an idea of how to make our sprite player touch things, let's create some things for him to touch.

  1. First, the player needs a collision handler. You can use the one we just discussed with no modification (though you may want to take out the #collide-1 label since that's just to demonstrate how to do something special based on what you collided with). You'll also need to change the target label for the collision check to "collision":
    if c?? Sprite_colliding "local" at 0 -1 then "collision"
    And so on.
  2. You need to create some key objects and some door objects, at least one of each, but creating more than one will be really, really easy. Each of these will be sprites, and each of them will have a robot in control. First, draw some graphics over with the player source you had before. Remember what their parameters are. If you want to do something REALLY cool, create a list of constants and put it in the global robot to keep track of these numbers for you. This way, if you ever change them, or do something like making your sprites vlayer based, you'll have a much easier time of things since you'll only have to change the global robot. In any case, use what you've learned about sprites so far to create a robot called "key" with a dynamic initialization routine:
    set "local" to "robot_id"
    set "local2" to "thisx"
    set "local3" to "thisy"
    set "local4" to "this_color"
    gotoxy "local" 0
    . "@spr&local&"
    set "spr&local&_refx" to XCOORD
    set "spr&local&_refy" to YCOORD
    set "spr&local&_width" to WIDTH
    set "spr&local&_height" to HEIGHT
    set "spr&local&_cx" to CX
    set "spr&local&_cy" to CY
    set "spr&local&_cwidth" to CWIDTH
    set "spr&local&_cheight" to CHEIGHT
    put "local4" Sprite "local" at "local2" "local3"
    end
    Of course you'll need to provide your own values for those counters, and as I've said, I really recommend assigning them to constant counters whose values you set all in one place. It makes things much easier later, when you want to change the way things are done. Also notice that in addition to reading the coordinates of the key based on where you put the robot, it also reads the color of the key and draws the sprite accordingly. This means we can use exactly the same robot code for different colored keys, without changing a thing except the color of the robot itself.
  3. Now you need a ":touch" label. In the case of a key, if you touch it you just collect the key and it disappears. So we need a counter to keep track of the key, and we need to make the sprite disappear.
    : "touch"
    inc "key_&local4&" by 1
    set "spr&local&_off" to 1
    die
    What we've done here is increment a counter that is unique to the color of the key. This means we can collect more than one of the same color key, and open just as many doors. The spr#_off counter is a special functional counter which turns off sprite drawing and sprite collision when it is set to something. It does NOT set the sprite's counters to zero, though.
  4. Now, copy that key robot and make a door robot out of it. I'm serious, the changes you'll be making are so slight that they don't warrant starting from scratch. Just change the values of the sprite initialization to suit, and change the behavior in the touch routine:
    : "touch"
    if "key_&local4& <= 0 then "end"
    dec "key_&local4&" by 1
    set "spr&local&_off" to 1
    die
    : "end"
    end
    We perform a simple check to make sure that the player collected a key that is the same color as this door. Other than that, it's a mirror image of the key's touch routine.
  5. Now, copy those two robots all over the board. You may actually want to do this with copyrobot commands, so that you only have to change the code in one place, when you need to change it. Or, if you want to be clever, save the robot code into a file and load from that. Whatever you do, put a bunch of copies of the key and door code onto your board, using as many different colors as you like. Now play around with it.

Example 3.1 Code

Exercise 3.2: Keeping track of sprites on the board

So far, the only time we've needed to have sprites talk to other sprites or refer to them by ID is when they collided, and then the numbers are provided for us. But suppose we want to know which sprite is the player and where it is, so that we can allow another robot controlling another sprite (like an enemy) to target it? Or what if we want to have the same sprite run around the board stealing keys (we'll actually do this in an upcoming example)? It'll need to know where they are too.

  1. For the player, this is simple enough, we can just have the following line in our initialization:
    set "playersprite" to "local"
    Can't get much easier than that. But for keys and doors, we need to be a little bit more clever, since there can be arbitrarily many of those. Just using their color (local4) as an identifier isn't enough, since that might not be unique on the board, and besides that we'd like a solution to enumerate a list of things.
  2. First, in the global robot, initialize a couple of counters to keep track of the number of keys and doors on the board:
    set "num_keys" to 0
    set "num_doors" to 0
  3. Then in the key init code (and similarly for the door code), add each key to the end of a list and advance the counter:
    set "spr&local&_lpos" to "num_keys"
    set "keysprite('spr&local&_lpos')" to "local"
    inc "num_keys" by 1
    We also store the sprite's position in the list and attach it to the sprite, double linking them.
  4. Why bother spending overhead on this, and making this counter public? So that we can easily prune the list when the player collects a key or opens a door. Here's a neat trick to quickly remove an item from an array without leaving a hole:
    dec "num_keys" by 1
    set "spr('keysprite&num_keys&')_id" to "spr&local&_id"
    set "keysprite('spr&local&_id')" to "keysprite&num_keys&"
    set "spr&local&_off" 1
    die

Let's pause here and explain what we've just done in more detail. First, we create a list of sprite/robot ids (they're the same in our system, remember). We populate this list by first setting the length of the list to zero, and adding each item to the end of the list as its robot gets executed. We also know that these items are subject to being removed from the list of active sprites in certain game situations. Now we can set things up such that the list will be automatically regenerated if we leave the board and come back (more on that in the next example). But when we start using lists to keep track of lots of objects that get added to and deleted from the list a lot (e.g. bullets), we'll really appreciate having a way to perform those add and delete operations in constant time, while limiting the time to iterate over the list down to the number of currently active items.
The key observation that makes the above trick work is that the list doesn't need to be sorted. So all we need to do to deal with the hole left by a deleted list item is find another item to put in the hole. And there's always an item at the end of the list, even if it's the very item we're deleting. We tell the sprite at the end of the list that it's list position (spr#_id) is now the position of the item we're deleting (this is why that counter needed to be public). And then we tell the list that the id at that position is now the id at the end of the list. Finally we decrease the length of the list (it's safe to do this first as shown in the code because the data doesn't actually go anywhere), effectively removing the last item on the list.

  1. This exercise has largely been in preparation for things to come. But in order to demonstrate its usefulness quickly, let's create a simple debug robot to print the status of all of our sprites on the board.
    end
    : "keyt"
    [ "The player is sprite &playersprite&, located at (('spr&playersprite&_x'), ('spr&playersprite&_y'))."
    set "local" to 0
    : "keyloop"
    if "num_keys" <= 0 then "doorloop"
    [ "Key sprite #&local& is located at (('spr('keysprite&local&')_x'), ('spr('keysprite&local&')_y'))."
    inc "local" by 1
    if "local" < "num_keys" then "keyloop"
    set "local" to 0
    : "doorloop"
    if "num_doors" <= 0 then "end"
    [ "Door sprite #&local& is located at (('spr('doorsprite&local&')_x'), ('spr('doorsprite&local&')_y'))."
    inc "local" by 1
    if "local" < "num_doors" then "doorloop"
    : "end"
    end
    Just copy this into a new robot (put it somewhere other than the top row of the board) and press 'T' to access it.

Example 3.2 Code

Exercise 3.3: Moving the player sprite between boards

Almost any real game is going to involve more than one board (single board content management via MZMs aside). However, successfully managing sprites across multiple boards requires some overhead and foresight. Before we go any further, we need to address one of the major gotchas of sprites, which is that you have 256 of them to use in the entire GAME, not per board. This means that, especially for a dynamic sprite system, you need a way to ensure that sprites are kept consistent and in the same places across boards.

  1. What this generally means is that we want to have the sprite initialization happen every time you load the board, implying the use of :justentered. But there are also parts of our sprite initialization code that we want to happen only one time, like setting the initial sprite position by using the initial robot position and then moving the robot. Fortunately there's a simple trick for pulling this off:
    . "One time execution code goes here."
    restore "justentered" 1
    | "justentered"
    . "Static initialization code goes here (sprite ref and collision box among others)."
    . "Fallthrough to main program loop."
    If you remember your robotic, a pipe is a pre-zapped label. This should be applied to the global robot as well, for sprite list setup.
  2. Of course there are things, especially for sprites that are going to move around, that must be preserved on a board change but which are subject to change. This is the reason I suggested the use of "local2" and "local3" to keep track of sprite position, instead of "spr#_x" and "spr#_y". But the player needs a system that doesn't simply preserve its position on each board, but which accurately sets its position based on where it was on the previous board. This means that the player sprite needs to use global counters instead of local counters. So do a search and replace on "local2" and "local3" in the player robot (CTRL+R is your friend) for counters like "player_x" and "player_y". Then add this code to the one time execution segment of the global robot:
    set "local2" to "playerx"
    set "local3" to "playery"
    The reason for the locals as temps will become clear in a moment. Now add this code to the board init section:
    set "player_x" to "local2"
    set "player_y" to "local3"
    put player 0 0
    Now you can us the player object itself to mark the start position of the player sprite, and be able to test from that position on any board. It no longer really matters where you put the player robot, as long as you don't put it in the top row (for safety reasons, since all the sprite robots move to that line and it might get overwritten before it moves). Also note the use of "player_x" instead of "playerx", since "playerx" and "playery" are built-in and read-only.
  3. Now we need to know when the sprite moves off the board. Fortunately bounds checking allows us to do just that. For each movement routine, add a sprite counter that tells which direction the sprite last moved, and then route the bounds check to a label that sends the global robot a message, like so:
    : "#up"
    set "spr&local&_lastmove" to 0
    if "player_y" <= "bminy" then "edgecollide"
    ...
    : "edgecollide"
    send "global" to "edge&spr('local')_lastmove&"
    goto "#return"
  4. Now we're going to use a nice little shortcut so that we can keep track of board adjacencies with the standard MZX system instead of writing another data structure to do it. All it requires is that the four corners of each board you use it on be empty. That's because we're going to use the player object itself to move between boards, instead of using teleport player. Write each edge# method similar to this:
    : "edge0"
    . "North"
    set "local2" to "player_x"
    set "local3" to "('bmaxy'-'spr_p_h')"
    . "That is, the bottom of the sprite playing field minus the height of the player sprite."
    put player 0 0
    . "Use board_w-1 and board_h-1 to move south and east."
    move player NORTH
    end
    The idea here is to set temp counters to the expected location of the player sprite on the next board, then move the actual player to that board. We don't use the actual counters so that if the move fails (there is no board in that direction), nothing happens to the sprite. But if the move succeeds, the justentered label will be triggered and the values will be committed.
  5. Obviously for this to work right, those "bminy" and "bmaxy" counters need to actually exist. They also need to be the same across all boards; if the boards need to have different dimensions you will need a more complex system. But for this simple case, just add those to the one-time code of the global.
    set "bminx" to 0
    set "bminy" to 0
    set "bmaxx" to 80
    set "bmaxy" to 25
    You can use whatever values you like, of course, at this point it's more up to the requirements of your game than anything else. You can and should replace all hard-coded values in the player bounds checking with these counters, as seen above.

That's really all that needs to be done at this point. Test it out by creating a few interlinked boards and spread your doors and keys across them. You'll need to copy the sprite source data between the boards too. (Writing an MZM loader for this into the global robot, or using the vlayer for storage, is left as an exercise to the reader. Though the second option is a really good idea, so read the vlayer tutorial for more info.)

Example 3.3 Code

(More to come...)

External Links

Saike's Sprite Tutorial - Slightly out of date with regards to other MZX features.