Help explain why the terrain files are named the way they are?

warmwaffles

Programmer
Joined
Jan 15, 2004
Messages
2,255
Location
Texas
I'm trying to build my own terrain and I am having a difficult time understanding what is meant by "lxggc.pcx". I know it means grass grass coast, but why is it in such a format?

I remember seeing a post about the reason why Firaxis chose this naming pattern, but I can't seem to find it for the life of me.
 
warmwaffles, I just sent you a PM explaining how I did this, but since there's a topic here as well, it makes sense to share it more visibly for the benefit of anyone else working with this. I've also included the complete version of the method I use for this, since regular posts can be longer than PMs.

-----------------------------------

This is a really confusing topic. It's been more than 3.5 years since I did the basic terrain work in my editor, and more than 3 years since that was available in a released version, so I'm a bit rusty on it. I remember reading that same post many years ago, and while it helped, it certainly wasn't like I read it and understood everything.

I guess I'll start by elaborating on some of DANGERBOY's topics.

Note that some .pcx files have the same type more than once, such as xggc or wooo. This means that the computer recognizes several different subtypes and allows for a greater variety of some basic terrain. This is why a single tile grass island comes in two different sizes with the basic graphics.

It is true that some terrain intersections are valid options from more than one file. In existing scenarios, I read whatever file/image combo is specified, and use that. When creating a map with my editor, it will always, for example, use the first image in wOOO for ocean surrounded by ocean. I don't know what formula Firaxis originally used for these ambiguous cases, but it doesn't matter a whole lot as long as you can find at least one valid file/image combo.

How does the ABC come into play? Since there are 3 different types in each file, and four positions, the total number of possibilities is 3^4=81. The .pcx file is a 9x9 grid. Use the following to determine what terrain types should be bordering for a particular corner:

1
3 9
27

where the number indicates how frequently the type cycles through each position, counting from left to right. In other words, the graphic located in the 3rd column, 4th row would be of the following format:

C
A A
B

If the file were xdgc, the terrain to the north would be coast, desert to the east and west, and grassland to the south.

This is really easy to get lost in. Actually, I'm not sure there's enough there to really explain it. The first thing I do when you paint a tile on the map is figure out which file to use, and what the "ABC"s that DANGERBOY talks about are. I call these TERR1, TERR2, and TERR3. Let's take a look at a picture (taken with Firaxis's editor for the coordinates over tiles):



In this case, let's say we just painted the plains at 27, 29. In the calculations I use, this is the "south" tile. By looking at the terrains in the south, west, east, and north tiles, we can figure out which file to use (which would be xDGP.pcx in this case). This code looks like the following (this code, and all the rest in here, being in Java, and my code, not Firaxis's):

Code:
//Let's do the ocean/sea cases first
        if (south.getRealTerrain() == TERR.OCEAN && east.getRealTerrain() == TERR.OCEAN && west.getRealTerrain() == TERR.OCEAN && north.getRealTerrain() == TERR.OCEAN)
        {
            if (logger.isDebugEnabled())
                logger.debug("WOOO");
            south.setFile(TILE.WOOO);
            south.setImage((byte)0);
            needToCalculateImage = false;
        }
        else if(south.getRealTerrain() == TERR.SEA && east.getRealTerrain() == TERR.SEA && west.getRealTerrain() == TERR.SEA && north.getRealTerrain() == TERR.SEA)
        {
            if (logger.isDebugEnabled())
                logger.debug("WSSS");
            south.setFile(TILE.WSSS);
            south.setImage((byte)0);
            needToCalculateImage = false;
        }
        else if (south.getRealTerrain() == TERR.TUNDRA || east.getRealTerrain() == TERR.TUNDRA || west.getRealTerrain() == TERR.TUNDRA || north.getRealTerrain() == TERR.TUNDRA)
        {
            if (logger.isDebugEnabled())
                logger.debug("XTGC");
            south.setFile(TILE.XTGC);
            terr1 = TERR.TUNDRA;
            terr2 = TERR.GRASSLAND;
            terr3 = TERR.COAST;
        }
        else if (south.getRealTerrain() == TERR.SEA || east.getRealTerrain() == TERR.SEA || west.getRealTerrain() == TERR.SEA || north.getRealTerrain() == TERR.SEA)
        {
            if (logger.isDebugEnabled())
                logger.debug("WCSO");
            south.setFile(TILE.WCSO);
            terr1 = TERR.COAST;
            terr2 = TERR.SEA;
            terr3 = TERR.OCEAN;
        }//at this point we should have all sea/ocean/tundra covered
        else if (south.getRealTerrain() != TERR.COAST && east.getRealTerrain() != TERR.COAST && west.getRealTerrain() != TERR.COAST && north.getRealTerrain() != TERR.COAST)
        {
            if (logger.isDebugEnabled())
                logger.debug("XDGP");
            south.setFile(TILE.XDGP);
            terr1 = TERR.DESERT;
            terr2 = TERR.GRASSLAND;
            terr3 = TERR.PLAINS;
        }   //all other cases should have coast
        else
        {
            if (south.getRealTerrain() == TERR.DESERT || east.getRealTerrain() == TERR.DESERT || west.getRealTerrain() == TERR.DESERT || north.getRealTerrain() == TERR.DESERT)
            {
                if (south.getRealTerrain() == TERR.PLAINS || east.getRealTerrain() == TERR.PLAINS || west.getRealTerrain() == TERR.PLAINS || north.getRealTerrain() == TERR.PLAINS)
                {
                    if (logger.isDebugEnabled())
                        logger.debug("XDPC");
                    south.setFile(TILE.XDPC);
                    terr1 = TERR.DESERT;
                    terr2 = TERR.PLAINS;
                    terr3 = TERR.COAST;
                }
                else if (south.getRealTerrain() == TERR.GRASSLAND || east.getRealTerrain() == TERR.GRASSLAND || west.getRealTerrain() == TERR.GRASSLAND || north.getRealTerrain() == TERR.GRASSLAND)
                {
                    if (logger.isDebugEnabled())
                        logger.debug("XDGC");
                    south.setFile(TILE.XDGC);
                    terr1 = TERR.DESERT;
                    terr2 = TERR.GRASSLAND;
                    terr3 = TERR.COAST;
                }
                else if (south.getRealTerrain() == TERR.COAST || east.getRealTerrain() == TERR.COAST || west.getRealTerrain() == TERR.COAST || north.getRealTerrain() == TERR.COAST)
                {
                    if (logger.isDebugEnabled())
                        logger.debug("XDPC");
                    south.setFile(TILE.XDGC);
                    terr1 = TERR.DESERT;
                    terr2 = TERR.PLAINS;
                    terr3 = TERR.COAST;
                }
                else
                {
                    logger.error("XKCD1.  X: " + x + ", Y: " + y + ", North: " + north.getRealTerrain() + ", East: " + east.getRealTerrain() + ", South: " + south.getRealTerrain() + ", West: " + west.getRealTerrain());
                    JOptionPane.showMessageDialog(null, "Error XKCD, type 1 - error calculating terrain image file.  Please report.", "Error XKCD", JOptionPane.ERROR_MESSAGE);
                    ; //TODO: ERROR
                }
            }
            else if (south.getRealTerrain() == TERR.PLAINS || east.getRealTerrain() == TERR.PLAINS || west.getRealTerrain() == TERR.PLAINS || north.getRealTerrain() == TERR.PLAINS)
            {
                if (logger.isDebugEnabled())
                    logger.debug("XPGC");
                south.setFile(TILE.XPGC);
                terr1 = TERR.PLAINS;
                terr2 = TERR.GRASSLAND;
                terr3 = TERR.COAST;
            }
            else if (south.getRealTerrain() == TERR.GRASSLAND || east.getRealTerrain() == TERR.GRASSLAND || west.getRealTerrain() == TERR.GRASSLAND || north.getRealTerrain() == TERR.GRASSLAND)
            {
                if (logger.isDebugEnabled())
                    logger.debug("XGGC");
                south.setFile(TILE.XGGC);
                terr1 = TERR.GRASSLAND;
                terr2 = TERR.GRASSLAND;
                terr3 = TERR.COAST;
            }   //TODO: Forgot the all coast case, it's XKCD'ing
            else if (south.getRealTerrain() == TERR.COAST || east.getRealTerrain() == TERR.COAST || west.getRealTerrain() == TERR.COAST || north.getRealTerrain() == TERR.COAST)
            {
                if (logger.isDebugEnabled())
                    logger.debug("WSCO Final");
                south.setFile(TILE.WCSO);
                terr1 = TERR.COAST;
                terr2 = TERR.SEA;
                terr3 = TERR.OCEAN;

            }
            else
            {
                logger.error("XKCD2  X: " + x + ", Y: " + y + ", North: " + north.getRealTerrain() + ", East: " + east.getRealTerrain() + ", South: " + south.getRealTerrain() + ", West: " + west.getRealTerrain());
                JOptionPane.showMessageDialog(null, "Error XKCD, type 2 - error calculating terrain image file.  Please report.", "Error XKCD", JOptionPane.ERROR_MESSAGE);
                ; //TODO: ERROR
            }
        }

After running through this, we know which file to use, and the TERR1, TERR2, and TERR3 byte variables have been set. For cases like all-ocean, the same image is always used; for the rest, that is the next step.

The "XKCD" error doesn't actually mean anything in particular - it's just a reference to many of the files starting with X, and a very good webcomic.

The next part goes into the latter part of what I quoted in DANGERBOY's post - figuring out which image in the file to use. That part of the quote is probably the more confusing part. However, it isn't too bad with the TERR1, TERR2, and TERR3 variables. Basically, we look at the south/east/west/north tiles (as seen in the image above), see whether their terrains match TERR1, TERR2, or TERR3, and, depending on the result, add up an integer that we'll use as an index within the PCX file. See the following code:

Code:
if (needToCalculateImage)
        {
            if (logger.isDebugEnabled())
            {
                logger.debug("Calculating image");
                logger.debug("north real: " + north.getRealTerrain());
                logger.debug("west real: " + west.getRealTerrain());
                logger.debug("east real: " + east.getRealTerrain());
                logger.debug("south real: " + south.getRealTerrain());
            }
            byte sum = 0;
            if (north.getRealTerrain() == terr2)
                sum+=1;
            if (north.getRealTerrain() == terr3)
                sum+=2;
            if (west.getRealTerrain() == terr2)
                sum+=3;
            if (west.getRealTerrain() == terr3)
                sum+=6;
            if (east.getRealTerrain() == terr2)
                sum+=9;
            if (east.getRealTerrain() == terr3)
                sum+=18;
            if (south.getRealTerrain() == terr2)
                sum+=27;
            if (south.getRealTerrain() == terr3)
                sum+=54;
            south.setImage(sum);
        }

Between these two, we can get both the file and the image. I should note, since it might cause confusion, that the "getRealTerrain()" method above actually gets the base terrain (without hills/forests/etc.) - the documentation at Apolyton had the base/real terrains flipped, and by the time I realized it, it was flipped all throughout my code base as well. If a tile's terrain matches TERR1, we don't add anything to the sum.

Also noteworthy is that when tile 27, 29 is changed, that will also change the file/image within file used for the tiles to its south, southwest, and southeast. I use the same method to recalculate their images.

Finally, if one of the neighboring tiles is off the edge of the map, I make it a dummy coast tile for the purposes of calculating the image.

The remaining issue with base terrains that isn't really clear is where within the file you'll find the actual image being used ("sum" in the above calculation). Checking my code for importing the base terrain PCX files, it turns out that index 0 is the upper-leftmost image in the terrain PCX, index 1 is one column to the right, and so on, with index 9 being the leftmost column in the second row. The code for making a 2D array of base terrains is thus the following:

Code:
        //9 for the files, 81 for the images within a file
        BufferedImage[][]baseTerrainGraphics = new BufferedImage[9][81];
        for (int i = 0; i < 9; i++)
        {
            //j is rows of PCX files
            for (int j = 0; j < 9; j++)
            {
                //k is columns of PCX files
                for (int k = 0; k < 9; k++)
                {
                    baseTerrainGraphics[i][j*9 + k] = baseTerrainsThreads[i].getBufferedImage().getSubimage(k*128, j*64, 128, 64);
                }
            }
        }

The first dimension is the actual PCX file, with the second being the index within the file. Thus it's easy to draw the graphics based on the file/index within file attributes stored in the TILE part of a BIQ file:

Code:
        if (!tile.get(i).isLandmark())
            canvas.drawImage(baseTerrainGraphics[tile.get(i).getFile()][tile.get(i).getImage()], xDrawingCoordinate, yDrawingCoordinate, null);
        else
            canvas.drawImage(lmTerrainGraphics[tile.get(i).getFile()][tile.get(i).getImage()], xDrawingCoordinate, yDrawingCoordinate, null);

Finally, the map between file name and index in the BIQ is as follows:

Code:
    public final static byte XTGC = 0;
    public final static byte XPGC = 1;
    public final static byte XDGC = 2;
    public final static byte XDPC = 3;
    public final static byte XDGP = 4;
    public final static byte XGGC = 5;
    public final static byte WCSO = 6;
    public final static byte WSSS = 7;
    public final static byte WOOO = 8;

s a supplemental resource, here's the documentation I wrote at the top of one of my code files:

Code Comment said:
/**
* Documentation for file and image.
* The file variable selects which file contains the graphics for this tile.
* There are 9 files used for base terrain
* 0 = xtgc.pcx, 1 = xgpc.pcx (xpgc - Quint), 2 = xdgc.pcx,
* 3 = xdpc.pcx, 4 = xdgp.pcx, 5 = xggc.pcx,
* 6 = wcso.pcx, 7 = wsss.pcx, 8 = wooo.pcx
* The code is x = land, w = water, t = tundra, g = grassland, c = coast,
* d = desert, c = coast, s = sea, o = ocean. Each tile can contain up to
* three surrounding types of terrain.
*
* The image variable then selects which image within the file.
* Each file is 1152x576 of 128x64 tiles; thus a 9x9 grid for 81 tiles total.
* So the image variable can go from 0 to 80.
*
* The graphics is then positioned on the map such that it is centered at
* the very top of the isometric tile. Thus it will overlap into the tiles
* NW, N, and NE of this tile. Screenshot of the Day 99 (http://www.civfanatics.com/sotd/sotd99.jpg)
* includes a grid overlay (in red) showing the grid used for where terrain
* graphics are placed.
*
* Also note that landmark terrain graphics can be different. The files for
* those are in Conquests/Art/Terrain, and have the same filenames as the
* above, but with l (lowercase L) in front, such as lwcso.pcx. We need to
* decode how to distinguish these tiles. ODDLY, messing with these files
* doesn't seem to cause in-game images to become distorted, so maybe
* they aren't actually used after all.
*
* Decoding guesses: questionMark3 = landmark tile (could also be ?2)
* questionMark = two bytes for image/file for overlay (ex. forest, hill, etc.)
* although not sure, if it's ALWAYS zero, that doesn't work
*/

That's the base terrains (and landmarks work the same way). Hills/forests/mountains aren't quite as confusing as I remember. I'll go into them in another post if you'd like (although it might be a few days - this isn't a topic you can just write about on a whim). I will note that DANGERBOY notes that "I am not entirely sure yet on how map generator picks which forest or jungle graphic, aside from generally that the larger graphics are reserved for the middle of a forest; obviously, I need to investigate further." I tried to figure this out myself, and it proved to be an exercise in futility, despite persistent efforts to get to the bottom of it. So I wouldn't waste time on trying to figure that out.

And the method to calculate what terrain file/image a tile should have after it's been changed, in its entirety (again, where it says getRealTerrain, it should be getBaseTerrain):

Code:
private void recalculateFileAndIndex(int x, int y)
    {
        int southID = biq.calculateTileIndex(x, y);
        TILE south = (southID == -1) ? null : tile.get(southID);
        int westID = biq.calculateTileIndex(x - 1, y - 1);
        TILE west = (westID == -1) ? null : tile.get(westID);
        int northID = biq.calculateTileIndex(x, y - 2);
        TILE north = (northID == -1) ? null : tile.get(northID);
        int eastID = biq.calculateTileIndex(x + 1, y - 1);
        TILE east = (eastID == -1) ? null : tile.get(eastID);

        //next three are the 'abc' in xabc.pcx
        byte terr1 = 0;
        byte terr2 = 0;
        byte terr3 = 0;

        boolean needToCalculateImage = true;

        if (south == null)
        {
            south = new TILE(null);
            south.setC3CBaseRealTerrain((byte)(TERR.COAST << 4 | TERR.COAST));
            south.setUpNibbles();
        }
        if (east == null)
        {
            east = new TILE(null);
            east.setC3CBaseRealTerrain((byte)(TERR.COAST << 4 | TERR.COAST));
            east.setUpNibbles();
        }
        if (north == null)
        {
            north = new TILE(null);
            north.setC3CBaseRealTerrain((byte)(TERR.COAST << 4 | TERR.COAST));
            north.setUpNibbles();
        }
        if (west == null)
        {
            west = new TILE(null);
            west.setC3CBaseRealTerrain((byte)(TERR.COAST << 4 | TERR.COAST));
            west.setUpNibbles();
        }

        //Let's do the ocean/sea cases first
        if (south.getRealTerrain() == TERR.OCEAN && east.getRealTerrain() == TERR.OCEAN && west.getRealTerrain() == TERR.OCEAN && north.getRealTerrain() == TERR.OCEAN)
        {
            if (logger.isDebugEnabled())
                logger.debug("WOOO");
            south.setFile(TILE.WOOO);
            south.setImage((byte)0);
            needToCalculateImage = false;
        }
        else if(south.getRealTerrain() == TERR.SEA && east.getRealTerrain() == TERR.SEA && west.getRealTerrain() == TERR.SEA && north.getRealTerrain() == TERR.SEA)
        {
            if (logger.isDebugEnabled())
                logger.debug("WSSS");
            south.setFile(TILE.WSSS);
            south.setImage((byte)0);
            needToCalculateImage = false;
        }
        else if (south.getRealTerrain() == TERR.TUNDRA || east.getRealTerrain() == TERR.TUNDRA || west.getRealTerrain() == TERR.TUNDRA || north.getRealTerrain() == TERR.TUNDRA)
        {
            if (logger.isDebugEnabled())
                logger.debug("XTGC");
            south.setFile(TILE.XTGC);
            terr1 = TERR.TUNDRA;
            terr2 = TERR.GRASSLAND;
            terr3 = TERR.COAST;
        }
        else if (south.getRealTerrain() == TERR.SEA || east.getRealTerrain() == TERR.SEA || west.getRealTerrain() == TERR.SEA || north.getRealTerrain() == TERR.SEA)
        {
            if (logger.isDebugEnabled())
                logger.debug("WCSO");
            south.setFile(TILE.WCSO);
            terr1 = TERR.COAST;
            terr2 = TERR.SEA;
            terr3 = TERR.OCEAN;
        }//at this point we should have all sea/ocean/tundra covered
        else if (south.getRealTerrain() != TERR.COAST && east.getRealTerrain() != TERR.COAST && west.getRealTerrain() != TERR.COAST && north.getRealTerrain() != TERR.COAST)
        {
            if (logger.isDebugEnabled())
                logger.debug("XDGP");
            south.setFile(TILE.XDGP);
            terr1 = TERR.DESERT;
            terr2 = TERR.GRASSLAND;
            terr3 = TERR.PLAINS;
        }   //all other cases should have coast
        else
        {
            if (south.getRealTerrain() == TERR.DESERT || east.getRealTerrain() == TERR.DESERT || west.getRealTerrain() == TERR.DESERT || north.getRealTerrain() == TERR.DESERT)
            {
                if (south.getRealTerrain() == TERR.PLAINS || east.getRealTerrain() == TERR.PLAINS || west.getRealTerrain() == TERR.PLAINS || north.getRealTerrain() == TERR.PLAINS)
                {
                    if (logger.isDebugEnabled())
                        logger.debug("XDPC");
                    south.setFile(TILE.XDPC);
                    terr1 = TERR.DESERT;
                    terr2 = TERR.PLAINS;
                    terr3 = TERR.COAST;
                }
                else if (south.getRealTerrain() == TERR.GRASSLAND || east.getRealTerrain() == TERR.GRASSLAND || west.getRealTerrain() == TERR.GRASSLAND || north.getRealTerrain() == TERR.GRASSLAND)
                {
                    if (logger.isDebugEnabled())
                        logger.debug("XDGC");
                    south.setFile(TILE.XDGC);
                    terr1 = TERR.DESERT;
                    terr2 = TERR.GRASSLAND;
                    terr3 = TERR.COAST;
                }
                else if (south.getRealTerrain() == TERR.COAST || east.getRealTerrain() == TERR.COAST || west.getRealTerrain() == TERR.COAST || north.getRealTerrain() == TERR.COAST)
                {
                    if (logger.isDebugEnabled())
                        logger.debug("XDPC");
                    south.setFile(TILE.XDGC);
                    terr1 = TERR.DESERT;
                    terr2 = TERR.PLAINS;
                    terr3 = TERR.COAST;
                }
                else
                {
                    logger.error("XKCD1.  X: " + x + ", Y: " + y + ", North: " + north.getRealTerrain() + ", East: " + east.getRealTerrain() + ", South: " + south.getRealTerrain() + ", West: " + west.getRealTerrain());
                    JOptionPane.showMessageDialog(null, "Error XKCD, type 1 - error calculating terrain image file.  Please report.", "Error XKCD", JOptionPane.ERROR_MESSAGE);
                    ; //TODO: ERROR
                }
            }
            else if (south.getRealTerrain() == TERR.PLAINS || east.getRealTerrain() == TERR.PLAINS || west.getRealTerrain() == TERR.PLAINS || north.getRealTerrain() == TERR.PLAINS)
            {
                if (logger.isDebugEnabled())
                    logger.debug("XPGC");
                south.setFile(TILE.XPGC);
                terr1 = TERR.PLAINS;
                terr2 = TERR.GRASSLAND;
                terr3 = TERR.COAST;
            }
            else if (south.getRealTerrain() == TERR.GRASSLAND || east.getRealTerrain() == TERR.GRASSLAND || west.getRealTerrain() == TERR.GRASSLAND || north.getRealTerrain() == TERR.GRASSLAND)
            {
                if (logger.isDebugEnabled())
                    logger.debug("XGGC");
                south.setFile(TILE.XGGC);
                terr1 = TERR.GRASSLAND;
                terr2 = TERR.GRASSLAND;
                terr3 = TERR.COAST;
            }   //TODO: Forgot the all coast case, it's XKCD'ing
            else if (south.getRealTerrain() == TERR.COAST || east.getRealTerrain() == TERR.COAST || west.getRealTerrain() == TERR.COAST || north.getRealTerrain() == TERR.COAST)
            {
                if (logger.isDebugEnabled())
                    logger.debug("WSCO Final");
                south.setFile(TILE.WCSO);
                terr1 = TERR.COAST;
                terr2 = TERR.SEA;
                terr3 = TERR.OCEAN;

            }
            else
            {
                logger.error("XKCD2  X: " + x + ", Y: " + y + ", North: " + north.getRealTerrain() + ", East: " + east.getRealTerrain() + ", South: " + south.getRealTerrain() + ", West: " + west.getRealTerrain());
                JOptionPane.showMessageDialog(null, "Error XKCD, type 2 - error calculating terrain image file.  Please report.", "Error XKCD", JOptionPane.ERROR_MESSAGE);
                ; //TODO: ERROR
            }
        }

        if (needToCalculateImage)
        {
            if (logger.isDebugEnabled())
            {
                logger.debug("Calculating image");
                logger.debug("north real: " + north.getRealTerrain());
                logger.debug("west real: " + west.getRealTerrain());
                logger.debug("east real: " + east.getRealTerrain());
                logger.debug("south real: " + south.getRealTerrain());
            }
            byte sum = 0;
            if (north.getRealTerrain() == terr2)
                sum+=1;
            if (north.getRealTerrain() == terr3)
                sum+=2;
            if (west.getRealTerrain() == terr2)
                sum+=3;
            if (west.getRealTerrain() == terr3)
                sum+=6;
            if (east.getRealTerrain() == terr2)
                sum+=9;
            if (east.getRealTerrain() == terr3)
                sum+=18;
            if (south.getRealTerrain() == terr2)
                sum+=27;
            if (south.getRealTerrain() == terr3)
                sum+=54;
            south.setImage(sum);
        }
    }

If the editor user tries to change a certain tile's terrain, say tile (x, y), and it's a valid change (based on the surrounding terrain), this function is called four times:

Code:
            recalculateFileAndIndex(x, y);
            recalculateFileAndIndex(x - 1, y + 1);
            recalculateFileAndIndex(x + 1, y + 1);
            recalculateFileAndIndex(x, y + 2);
 
warmwaffles, I just sent you a PM explaining how I did this, but since there's a topic here as well, it makes sense to share it more visibly for the benefit of anyone else working with this. I've also included the complete version of the method I use for this, since regular posts can be longer than PMs.

:eek: Wow, this is the best response I have seen. This should probably reside in the modiki.
 
Top Bottom