/* * @(#)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 |