Writing Wyvern Clients

/*
 * @(#)Borderer.java	1.0 Jul 02, 1998
 * 
 * Copyright (c) 1998-2007 Cabochon Technologies, L.L.C.  All Rights Reserved.
 * 
 * This software is the confidential and proprietary information of 
 * Cabochon Technologies, L.L.C. ("Confidential Information").  You
 * may use the software in the creation of new software clients for 
 * connecting to the Wyvern server.  The Terrain Bordering algorithm 
 * implemented in this software is the property of Cabochon, and you may
 * not use it for purposes other than creating Wyvern game clients. <p>
 * 
 * CABOCHON MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY 
 * OF THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 
 * TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
 * PURPOSE, OR NON-INFRINGEMENT. CABOCHON SHALL NOT BE LIABLE FOR ANY 
 * DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR 
 * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES.
 * 
 * Copyright Version 1.0
 */

package wyvern.common.util;

import java.awt.Image;
import java.util.Hashtable;

import wyvern.common.config.Wyvern;
import wyvern.common.util.IntHashtable;

/**
 * This class handles choosing terrain borders.  It determines
 * whether borders are required and caches information about them
 * for later. <p>
 *
 * The methods of this class are unsynchronized and unprotected
 * against access by multiple threads.  The class is shared by the
 * game client and the map editor, although the map editor uses
 * a subclass that doesn't try to fetch images that it can't find.
 *
 * @version	1.0, Jul 02, 1998
 * @author	Steve Yegge
 */
public abstract class Borderer
{
    // keeps track of which tiles have borders
    IntHashtable bordered_;

    // keep track of border base names
    Hashtable names_;

    // maps border file names to the images.  Hashtable for 1.1. compliance.
    Hashtable borders_;

    static final String BORDERS_DIR = "terrain/borders/";

    /**
     * Constructs a new Borderer
     * @author stevey
     */
    public Borderer ()
    {
	// create a table for keeping track of which tiles have borders.
	bordered_ = new IntHashtable();

	borders_ = new Hashtable();
	names_ = new Hashtable();

    } // end of Borderer constructor

    /**
     * Chooses a set of borders for a given location in the map.
     *
     * @param map an array of TerrainInfo objects.  It must be at
     * least large enough to examine the 8 adjacent neighbors of the
     * passed location in the array; thus the minimum size is 3x3.
     *
     * @param x the array x index to choose borders for
     * @param y the array y index to choose borders for
     *
     * @return an array of border Images to draw at this square, or 
     * null if there are no borders to draw.  Any of the elements of
     * the returned array may also be null, if that particular border
     * number isn't to be drawn here.
     *
     * @author kiz
     * @author stevey
     */
    public Image[] chooseBorders ( int x, int y,
				   TerrainInfo[][] map )
    {
	// get info for current square
	TerrainInfo info = map[x][y];

	// if this square doesn't let borders in, we're done
	if ( !info.getLetsIn() ) return null;

	// preprocess the neighbors, adjusting layer priorities
	// if they can't contribute borders for any reason.
	TerrainInfo[] n = new TerrainInfo[8];
	n[0] = getNeighbor ( x-1, y-1, map, info );
	n[1] = getNeighbor ( x,   y-1, map, info );
	n[2] = getNeighbor ( x+1, y-1, map, info );
	n[3] = getNeighbor ( x-1, y,   map, info );
	n[4] = getNeighbor ( x+1, y,   map, info );
	n[5] = getNeighbor ( x-1, y+1, map, info );
	n[6] = getNeighbor ( x,   y+1, map, info );
	n[7] = getNeighbor ( x+1, y+1, map, info );

	// get current square's layer priority
	int prio = info.getPriority();

	// trivial reject:  if none of the neighbors has a higher
	// priority than this square, return null.
	boolean found = false;
	for ( int i = 0; i < n.length; i++ ) {
	    if ( n[i].getPriority() > prio ) found = true;
	}
	if ( !found ) return null;

	// The basic algorithm is to choose up to 8 border bitmaps
	// to draw into the center square, based on the neighbors.
	// Each corner of the center square has three neighbors, and
	// the same checks are required for each corner.  We call the
	// same function (getBorders()) on each corner, adding a fixed
	// offset to the result depending on which corner it is.
	//
	//  0 | 1 | 2 	The neighbors of the center square are
	// ---+---+---  numbered counterclockwise from the upper-left.
	//  3 |   | 4 
	// ---+---+---  A tile with a higher layer priority draws
	//  5 | 6 | 7   its borders over a lower-priority tile.
	//	     
	//printNeighbors ( map, x, y );

	// Make an array for holding the borders.  Each successive
	// two ints are either -1 (if that border isn't to be drawn)
	// or the tile number and border number (1-20) to draw.
	int[] borders = new int[16];

	// choose the border tile groups for each 3 corner tiles
	getBorders ( n[3], n[0], n[1], borders, prio, 0 );	// nw
	getBorders ( n[1], n[2], n[4], borders, prio, 1 );	// ne
	getBorders ( n[4], n[7], n[6], borders, prio, 2 );	// se
	getBorders ( n[6], n[5], n[3], borders, prio, 3 );	// sw

	// convert the tile and border numbers into actual border images
	Image[] images = new Image [ 8 ];
	for ( int i = 0; i < images.length; i++ ) {

	    // load the image
	    int index = i*2;
	    int tile = borders [ index ];
	    if ( tile == -1 ) continue;
	    int border = borders [ index + 1 ];

	    // I don't know why I had to add this in; I'm seeing
	    // tile=0, border=0 getting passed to getBorder a lot,
	    // but I haven't had chance to go find out why.  It
	    // doesn't seem to be a big issue; this works around it.
	    if ( border == 0 ) continue;
	    images[i] = getBorder ( tile, border );
	}

	return images;

    } // end of chooseBorders

    /**
     * Returns information about whether the passed square needs borders.
     *
     * @param x the x array location
     * @param y the y array location
     * @param map the terrain array
     *
     * @return the TerrainInfo for the specified location, with its
     * layer priority set to -1 if it can't extend any borders, which
     * could be from any of the following:
     *
     *	<ul>
     *    <li> it's out of bounds
     *    <li> the terrain type has no border images
     *    <li> the terrain is flagged as not letting borders out
     *	</ul>
     *
     * @author kiz
     * @author stevey
     */
    private TerrainInfo getNeighbor ( int x, int y,
				      TerrainInfo[][] map,
				      TerrainInfo caller )
    {
	TerrainInfo info = map[x][y];

	// check bounds
	if ( x < 0 || x >= map.length ||
	     y < 0 || y >= map[0].length ) {
	    info.setPriority ( -1 );
	    return info;
	}

	// if this tile has no borders, it doesn't participate
	if ( !info.hasBorders() ) {
	    info.setPriority ( -1 );
	    return info;
	}

	// if this tile doesn't let borders out, it doesn't participate
	if ( !info.getLetsOut() ) {
	    info.setPriority ( -1 );
	    return info;
	}

	// if this tile is the same terrain type as the caller, it
	// doesn't participate
	if ( info.getTile() == caller.getTile() ) {
	    info.setPriority ( -1 );
	    return info;
	}

	// return with no priority adjustment
	return info;

    } // end of getNeighbor

    /**
     * Computes the tile groups to choose borders from, based on the
     * 3 neighboring corner squares' layer priorities and tile numbers. <p>
     *
     * A "tile group" is a set of four border bitmaps that differ only
     * in the corner they're intended for.  For instance, the four
     * large diagonal border bitmaps comprise a tile group (group 5). <p>
     *
     * The borders code suffers somewhat from having been rewritten 
     * multiple times as we refined the engine.  It could use a pass
     * for eliminating unnecessary extra objects & arrays. <p>
     *
     * @param n1 the first neighbor (going clockwise around the corner)
     * @param n2 the second (diagonal) neighbor
     * @param n3 the third neighbor
     * @param borders the array in which to store the return values
     * @param center_prio the layer priority of the center tile
     * @param index the multiplier (0-3) to use for the corner, both 
     * for positioning the information in the return array, and for
     * choosing the appropriate group number.
     *
     * @return enters 0-2 points in the return array.  Each point is
     * an ordered pair consisting of the tile number of the border's
     * terrain type, and the border number (1-20).  Array elements
     * of the returned array can be null if there was no border for
     * that "case".
     *
     * @author kiz
     * @author stevey
     */
    private void getBorders ( TerrainInfo n1,
			      TerrainInfo n2,
			      TerrainInfo n3,
			      int[] borders,
			      int center_prio,
			      int index )
    {
	// In the notes below, use the following diagram for reference.
	//
	//  n2| n3|     
        // ---+---+---  
        //  n1|   |     
        // ---+---+---  
        //    |   |     
	//
	// Here are all the arrangements of neighbors that matter:
	//
	// 1)  n1 == n3			border = group 5, diagonal
	//
	// This case is the most straightforward.  n2 doesn't matter.
	//
	// 2)  n1 == n2
	//
	// If n1 == n2, the first border will be from group 1.  If n3
	// contributes a border, it'll be the 2nd border, from group 4:
	//
	//  2a)  p3 < center_prio:	border = group 1
	//  2b)  p3 > center_prio:	border1 = group 1, border2 = group 4
	//
	// 3)  n2 == n3
	//
	// Similar situation to (2) above, except symmetrically reversed:
	//
	//  3a)  p1 < center_prio:	border = group 2
	//  3b)  p1 > center_prio:	border1 = group 2, border2 = group 3
	//
	// Now we've covered all cases where 2 neighbors are the same.
	// The remaining cases are when all 3 neighbors are different.
	//
	// 4)  n1 != n2 != n3
	//
	// Basically either n1 or n3 (or both) can contribute a border.
	//  4a)  p1 > center_prio:	border1 = group 3
	//  4b)  p3 > center_prio:	border2 = group 4

	// NOTE:  the algorithm above isn't implemented below (yet)
	// (basically case 4 isn't handled).

	// get neighbor tile numbers
	int t1 = n1.getTile();
	int t2 = n2.getTile();
	int t3 = n3.getTile();

	// If any neighbor has a lower priority than the center, then
	// it doesn't participate in any of the arrangements above.
	// We force this by setting its tile number to -1.
	// (The basic problem is that two neighbors with the same
	// terrain type don't necessarily have the same layer priority.
	// See getNeighbor() for details).
	int p1 = n1.getPriority();
	int p2 = n2.getPriority();
	int p3 = n3.getPriority();
	if ( p1 < center_prio ) t1 = -1;
	if ( p2 < center_prio ) t2 = -1;
	if ( p3 < center_prio ) t3 = -1;

	// 2 possible borders from this corner
	int b1_tile = -1;
	int b1_border = -1;
	int b2_tile = -1;
	int b2_border = -1;
	int pos = index * 4;		// compute border array offset
	int quadrant = index * 5;	// compute border artwork group

	// note:  we check priorities as well as tile numbers for
	// equality.  If 2 priorities are equal, even if the tile
	// numbers are different, we consider the tiles to be in the
	// same "terrain class", which allows variants on a terrain type
	// to use the same borders.

	// check cases where p1 is contributing a border (group 1, 3 or 5)
	if ( p1 > center_prio ) {

	    // case 1:  diagonal
	    if ( t1 == t3 || p1 == p3 ) {
		borders[pos] = t1;
		borders[pos+1] = 5 + quadrant;
		return;
	    }

	    // case 2
	    if  ( t1 == t2 || p1 == p2 ) {
		b1_tile = t1;
		b1_border = 1 + quadrant;
	    }

	    // p1 needs a border and n1 != n2, so group 3
	    else {
		b1_tile = t1;
		b1_border = 3 + quadrant;
	    }

	    // see if p3 wants a border too (group 2 or group 4)
	    if ( p3 > center_prio ) {

		if ( t2 == t3 || p2 == p3 ) {
		    b2_tile = t3;
		    b2_border = 2 + quadrant;
		}
		else {
		    b2_tile = t3;
		    b2_border = 4 + quadrant;
		}
	    }

	} // end p1 > center_prio

	// cases where p1 doesn't contribute a border, but p3 does
	else if ( p3 > center_prio ) {

	    if ( t2 == t3 || p2 == p3 ) {
		b1_tile = t3;
		b1_border = 2 + quadrant; 
	    }
	    else {
		b1_tile = t3;
		b1_border = 4 + quadrant; 
	    }

	    // copy in borders and bail
	    borders[pos] = b1_tile;
	    borders[pos+1] = b1_border;
	    return;
	}

	// see whether n1 or n3 has priority for being drawn first.
	// Somewhat counter-intuitively, the higher-priority one gets
	// drawn *last* so it overlaps the other one.
	if ( p1 < p3 ) {
	    borders[pos] = b1_tile;
	    borders[pos+1] = b1_border;
	    borders[pos+2] = b2_tile;
	    borders[pos+3] = b2_border;
	} else {
	    borders[pos] = b2_tile;
	    borders[pos+1] = b2_border;
	    borders[pos+2] = b1_tile;
	    borders[pos+3] = b1_border;
	}

    } // end of getBorders

    /**
     * Gets the numbered border for a tile.
     *
     * @param tile the tile number of the image whose borders we want
     * @param num the border number (1-20) to load
     *
     * @return the border Image, which as a side-effect is cached.
     * @author stevey
     */
    public Image getBorder ( int tile, int number )
    {
	String num = Integer.toString ( number );
	
	// this will be, e.g. "terrain/sand"
	String base = getImageName ( tile );
	if ( base == null ) return null;

	// create "terrain/borders/sand/sandBorder"
	base = getBorderBase ( base );

	// get the actual border (".../sandBorder12")
	StringBuffer sb = new StringBuffer ( base );
	sb.append ( num );
	String name = sb.toString();

	// we could use the ImageCache for borders, but it would
	// require more work:  first look up the border tile number,
	// then call the ImageCache.  It's faster just to keep our
	// own table, keyed on the border names.
	Image img = (Image) borders_.get ( name );
	if ( img != null ) return img;

	// load and cache the border image
	img = loadImage ( name );
	if ( img == null ) {
	    return null;
	}

	// store under, e.g. "terrain/borders/sand/sandBorder3"
	borders_.put ( name, img );
	return img;

    } // end of getBorder

    /**
     * Returns the base for the border bitmap given a terrain type.
     * @param terrain the terrain, e.g. "terrain/clover"
     * @return e.g. "terrain/borders/clover/cloverBorder"
     * @author stevey
     */
    public String getBorderBase ( String terrain )
    {
	// we don't want to have to do this kind of string
	// surgery on every request, so we cache the results
	String base = (String) names_.get ( terrain );
	if ( base != null ) return base;

	String name = terrain;	// "terrain/sand"

	// strip off any digit at the end, converting, e.g.
	// "terrain/clover1" to "terrain/clover"
	if ( Character.isDigit ( name.charAt ( name.length() -1 ))) {
	    name = name.substring ( 0, name.length() - 1 );
	}

	// for wiz terrain, look in same directory for borders
	// e.g. "wiz/foo/terrain/aspen"(.gif) - look in wiz/foo/terrain

	if ( terrain.startsWith ( "wiz/" )) {
	    base = terrain + "Border";
	}
	else {
	    int index = name.lastIndexOf ( '/' );
	    name = name.substring ( index+1 );	// "sand"
	    String border = name + "/" + name + "Border";  // sandBorder
	    base = BORDERS_DIR + border;
	}

	names_.put ( terrain, base );
	return base;

    } // end of getBorderBase

    /**
     * Loads the image file given its name.  The client version does
     * this by trying to load it, and if it fails, asking the server
     * to send it over.  The mapedit version just loads the image directly.
     *
     * @param name something like "terrain/borders/forest/forestBorder10"
     * @return the image (prepends art root, appends extension, loads it)
     * @author stevey
     */
    protected abstract Image loadImage ( String name );

    /**
     * Looks up an image from its tile number.  This is handled
     * differently in the client and map editor - the map editor
     * always has the image available, via the TileRegistry.
     * The client has to go to the image cache and see if it's
     * available from the server yet.
     * 
     * @param tile the tile number to look up
     * @return the relative image path and name
     * @author stevey
     */
    protected String getImageName ( int tile )
    {
	// if this bothers you, or if we move ClientImageCache into
	// another package, change this to an abstract method and
	// move this call into wyvern.client.core.ClientBorderer

	return ClientImageCache.getInstance().getImageNameFromTile ( tile );

    } // end of getImageName

    /**
     * Prints out a picture of the neighbor array, for debugging.
     * Only works in the map editor; can't compile in the client
     * because of the TileRegistry reference, hence it's commented out.
     * @param neighbors the neighbor array
     * @param x x coordinate
     * @param y y coordinate
     * @author stevey
     */
//      private void printNeighbors ( TerrainInfo[][] neighbors, int x, int y )
//      {
//  	for ( int j = y-1; j <= y+1; j++ ) {
//  	    for ( int i = x-1; i <= x+1; i++ ) {

//  		// get the first letter of the terrain type
//  		TerrainInfo info = neighbors[i][j];
//  		String name = TileRegistry.lookup ( info.getTile() );
//  		name = name.substring ( "terrain/".length() );
//  		char c = name.charAt ( 0 );

//  		System.out.print ( c );
//  		if ( i != x+1 ) System.out.print ( "|" );
//  	    }
//  	    System.out.println();
//  	    if ( j != y+1 ) {
//  		System.out.println ( "-+-+-" );
//  	    }
//  	}
//  	System.out.println();

//      } // end of printNeighbors

}  // end of class Borderer