1 /*************************************************************************** 2 * (C) Copyright 2003-2011 - Stendhal * 3 *************************************************************************** 4 *************************************************************************** 5 * * 6 * This program is free software; you can redistribute it and/or modify * 7 * it under the terms of the GNU General Public License as published by * 8 * the Free Software Foundation; either version 2 of the License, or * 9 * (at your option) any later version. * 10 * * 11 ***************************************************************************/ 12 package games.stendhal.client; 13 14 import java.awt.Composite; 15 import java.awt.Graphics; 16 import java.awt.geom.Rectangle2D; 17 import java.io.IOException; 18 import java.io.InputStream; 19 import java.util.ArrayList; 20 import java.util.HashMap; 21 import java.util.List; 22 import java.util.Map; 23 24 import org.apache.log4j.Logger; 25 26 import games.stendhal.client.gui.j2d.Blend; 27 import games.stendhal.client.gui.wt.core.SettingChangeAdapter; 28 import games.stendhal.client.gui.wt.core.WtWindowManager; 29 import games.stendhal.client.sprite.Tileset; 30 import games.stendhal.common.CollisionDetection; 31 import games.stendhal.common.MathHelper; 32 import games.stendhal.common.tiled.LayerDefinition; 33 import marauroa.common.game.RPObject; 34 import marauroa.common.net.InputSerializer; 35 36 /** 37 * Layer data of a zone. 38 */ 39 public class Zone { 40 /** Logger instance. */ 41 private static final Logger LOGGER = Logger.getLogger(Zone.class); 42 43 /** 44 * The name of the setting that controls whether the weather layer should 45 * be drawn. 46 */ 47 private static final String WEATHER_PROPERTY = "ui.draw_weather"; 48 49 /** Name of the zone. */ 50 private final String name; 51 /** A name that's suitable for presenting to the user. */ 52 private String readableName; 53 /** Renderers for normal layers. */ 54 private final Map<String, LayerRenderer> layers = new HashMap<String, LayerRenderer>(); 55 /** Global current zone information. */ 56 private final ZoneInfo zoneInfo = ZoneInfo.get(); 57 /** Weather renderer. */ 58 private LayerRenderer weather = new EmptyLayerRenderer(); 59 /** Name of the weather type, or <code>null</code>. */ 60 private String weatherName; 61 /** Collision layer. */ 62 private CollisionDetection collision; 63 /** Protection layer. */ 64 private CollisionDetection protection; 65 /** Tilesets. */ 66 private TileStore tileset; 67 /** 68 * <code>true</code>, if the zone has been successfully validated since the 69 * last change, <code>false</code> otherwise. 70 */ 71 private volatile boolean isValid; 72 /** 73 * If <code>true</code>, the zone needs a data layer added before it can be 74 * validated. 75 */ 76 private boolean requireData; 77 /** 78 * Update property of the zone. <code>false</code> usually, but 79 * <code>true</code> when the zone is an update (such as 80 * changed colors) to the current zone. 81 */ 82 private boolean update; 83 /** Danger level of the zone. */ 84 private double dangerLevel; 85 /** Flag to check whether the weather layer should be drawn. */ 86 private boolean drawWeather; 87 88 /** 89 * Create a new zone. 90 * 91 * @param name zone name 92 */ Zone(String name)93 Zone(String name) { 94 this.name = name; 95 96 // Follow the weather drawing setting 97 WtWindowManager.getInstance().registerSettingChangeListener(WEATHER_PROPERTY, new SettingChangeAdapter(WEATHER_PROPERTY, "true") { 98 @Override 99 public void changed(String newValue) { 100 boolean value = Boolean.parseBoolean(newValue); 101 if (drawWeather != value) { 102 drawWeather = value; 103 if (!value) { 104 weather = new EmptyLayerRenderer(); 105 } else if (weatherName != null) { 106 weather = new WeatherLayerRenderer(weatherName, zoneInfo.getZoneColor(), zoneInfo.getColorMethod()); 107 } 108 } 109 } 110 }); 111 } 112 113 /** 114 * Check if the zone is an update to another zone, rather than one where 115 * the player has just moved to. 116 * 117 * @return <code>true</code>, if the zone is an update, <code>false</code> 118 * otherwise 119 */ isUpdate()120 boolean isUpdate() { 121 return update; 122 } 123 124 /** 125 * Set the update property of the zone. Zone data that is a color update 126 * should be prepared in a background thread, as far as possible, to avoid 127 * pausing the client. For normal zone changes, the update status should be 128 * <code>false</code>. 129 * 130 * @param update <code>false</code> for normal zone changes. 131 * <code>true</code> when the zone is an update to the current zone 132 */ setUpdate(boolean update)133 void setUpdate(boolean update) { 134 this.update = update; 135 } 136 137 /** 138 * Call, if the zone requires a data layer. Calling this must happen 139 * <b>before</b> the said data layer is added. 140 */ requireDataLayer()141 void requireDataLayer() { 142 requireData = true; 143 } 144 145 /** 146 * Add a layer. 147 * 148 * @param layer layer name 149 * @param in Stream for reading the layer data 150 * 151 * @throws IOException 152 * @throws ClassNotFoundException 153 */ addLayer(String layer, InputStream in)154 void addLayer(String layer, InputStream in) throws IOException, ClassNotFoundException { 155 if (layer.equals("collision")) { 156 /* 157 * Add a collision layer. 158 */ 159 collision = new CollisionDetection(); 160 collision.setCollisionData(LayerDefinition.decode(in)); 161 } else if (layer.equals("protection")) { 162 /* 163 * Add protection 164 */ 165 protection = new CollisionDetection(); 166 protection.setCollisionData(LayerDefinition.decode(in)); 167 } else if (layer.equals("tilesets")) { 168 /* 169 * Add tileset 170 */ 171 TileStore store = new TileStore(); 172 store.addTilesets(new InputSerializer(in)); 173 tileset = store; 174 } else if (layer.equals("data_map")) { 175 readDataLayer(in); 176 } else { 177 /* 178 * It is a tile layer. 179 */ 180 TileRenderer content = new TileRenderer(); 181 content.setMapData(in); 182 layers.put(layer, content); 183 } 184 isValid = false; 185 } 186 187 /** 188 * Read the special data layer. 189 * 190 * @param in Stream for reading the layer data 191 * @throws IOException 192 */ readDataLayer(InputStream in)193 private void readDataLayer(InputStream in) throws IOException { 194 // Zone attributes 195 RPObject obj = new RPObject(); 196 obj.readObject(new InputSerializer(in)); 197 198 // *** coloring *** 199 // Ensure there's no old color left. That can happen in the 200 // morning on a daylight colored zone. 201 zoneInfo.setColorMethod(null); 202 203 // getBlend calls below may need the color, so check that one first 204 String color = obj.get("color"); 205 if (color != null && isColoringEnabled()) { 206 // Keep working, but use an obviously broken color if parsing 207 // the value fails. 208 zoneInfo.setZoneColor(MathHelper.parseIntDefault(color, 0x00ff00)); 209 zoneInfo.setColorMethod(getBlend(obj.get("color_method"))); 210 } 211 212 // * effect blend * 213 if (isColoringEnabled()) { 214 zoneInfo.setEffectBlend(getEffectBlend(obj.get("blend_method"), zoneInfo.getColorMethod())); 215 } else { 216 zoneInfo.setEffectBlend(null); 217 } 218 219 // *** Weather *** 220 String weather = obj.get("weather"); 221 if (weather != null) { 222 weatherName = weather; 223 if (drawWeather) { 224 this.weather = new WeatherLayerRenderer(weather, zoneInfo.getZoneColor(), zoneInfo.getColorMethod()); 225 } 226 } 227 228 // *** other attributes *** 229 String danger = obj.get("danger_level"); 230 if (danger != null) { 231 try { 232 dangerLevel = Double.parseDouble(danger); 233 } catch (NumberFormatException e) { 234 Logger.getLogger(Zone.class).warn("Invalid danger level: " + danger, e); 235 } 236 } 237 readableName = obj.get("readable_name"); 238 // OK to try validating after this 239 requireData = false; 240 } 241 242 /** 243 * Check if map coloring is enabled. 244 * 245 * @return <code>true</code> if map coloring is enabled, <code>false</code> 246 * otherwise 247 */ isColoringEnabled()248 private boolean isColoringEnabled() { 249 return WtWindowManager.getInstance().getPropertyBoolean("ui.colormaps", true); 250 } 251 252 /** 253 * Get blend mode for the effect layers. 254 * 255 * @param colorMode mode description 256 * @param globalMode global coloring blend mode 257 * 258 * @return effect blend 259 */ getEffectBlend(String colorMode, Composite globalMode)260 private Composite getEffectBlend(String colorMode, Composite globalMode) { 261 if ("bleach".equals(colorMode) && (globalMode != Blend.Multiply)) { 262 /* 263 * Bleach is designed to work with multiply. Fall back to generic 264 * light for zones that use something else, or have no global mode. 265 */ 266 colorMode = "generic_light"; 267 } 268 return getBlend(colorMode); 269 } 270 271 /** 272 * Get composite mode from a string identifier. 273 * 274 * @param colorMode blend mode as a string, or <code>null</code> 275 * @return blend mode, or <null> 276 */ getBlend(String colorMode)277 private Composite getBlend(String colorMode) { 278 if ("bleach".equals(colorMode)) { 279 if (zoneInfo.getZoneColor() != null) { 280 return Blend.createBleach(zoneInfo.getZoneColor()); 281 } 282 } else if ("generic_light".equals(colorMode)) { 283 return Blend.GenericLight; 284 } else if ("multiply".equals(colorMode)) { 285 return Blend.Multiply; 286 } else if ("screen".equals(colorMode)) { 287 return Blend.Screen; 288 } else if ("softlight".equals(colorMode)) { 289 return Blend.SoftLight; 290 } else if ("truecolor".equals(colorMode)) { 291 return Blend.TrueColor; 292 } else if (colorMode != null) { 293 LOGGER.warn("Unknown blend mode: '" + colorMode + "'"); 294 } 295 296 return null; 297 } 298 299 /** 300 * Get the name of the zone. 301 * 302 * @return zone name 303 */ getName()304 String getName() { 305 return name; 306 } 307 308 /** 309 * Get the user representable name of the zone. 310 * 311 * @return user readable name 312 */ getReadableName()313 public String getReadableName() { 314 if (readableName != null) { 315 return readableName; 316 } 317 return name; 318 } 319 320 /** 321 * Get the zone width. 322 * 323 * @return zone width, or 0 if the zone is not ready enough to return the 324 * real width 325 */ getWidth()326 double getWidth() { 327 if (!isValid) { 328 return 0.0; 329 } 330 return collision.getWidth(); 331 } 332 333 /** 334 * Get the zone height. 335 * 336 * @return zone height, or 0 if the zone is not ready enough to return the 337 * real height 338 */ getHeight()339 double getHeight() { 340 if (!isValid) { 341 return 0.0; 342 } 343 return collision.getHeight(); 344 } 345 346 /** 347 * Get the zone danger level. 348 * 349 * @return danger level 350 */ getDangerLevel()351 public double getDangerLevel() { 352 return dangerLevel; 353 } 354 355 /** 356 * Check if a shape collides within the zone. 357 * 358 * @param shape checked area 359 * @return <code>true</code>, if the shape overlaps the static zone 360 * collision, <code>false</code> otherwise 361 */ collides(final Rectangle2D shape)362 boolean collides(final Rectangle2D shape) { 363 if (collision != null) { 364 return collision.collides(shape); 365 } 366 return false; 367 } 368 369 /** 370 * Get the collision map. 371 * 372 * @return collision 373 */ getCollision()374 public CollisionDetection getCollision() { 375 return collision; 376 } 377 378 /** 379 * Get the protection map. 380 * 381 * @return protection. 382 */ getProtection()383 public CollisionDetection getProtection() { 384 return protection; 385 } 386 387 /** 388 * Get a composite representation of multiple tile layers. 389 * 390 * @param compositeName name to be used for the composite for caching 391 * @param adjustName name of the adjustment layer 392 * @param layerNames names of the layers making up the composite starting 393 * from the bottom 394 * @return layer corresponding to all sub layers or <code>null</code> if 395 * they can not be merged 396 */ getMerged(String compositeName, String adjustName, String ... layerNames)397 LayerRenderer getMerged(String compositeName, String adjustName, 398 String ... layerNames) { 399 LayerRenderer r = layers.get(compositeName); 400 if (r == null) { 401 List<TileRenderer> subLayers = new ArrayList<TileRenderer>(layerNames.length); 402 for (int i = 0; i < layerNames.length; i++) { 403 LayerRenderer subLayer = layers.get(layerNames[i]); 404 if (subLayer instanceof TileRenderer) { 405 subLayers.add((TileRenderer) subLayer); 406 } else if (subLayer != null) { 407 // Can't merge 408 return null; 409 } 410 } 411 412 // e.g. if 3_roof is not present for the roof bundle 413 if (subLayers.isEmpty()) { 414 return new EmptyGroupRenderer(); 415 } 416 417 TileRenderer adjLayer = null; 418 LayerRenderer subLayer = layers.get(adjustName); 419 if (subLayer instanceof TileRenderer) { 420 adjLayer = (TileRenderer) subLayer; 421 } 422 // Make sure the sub layers have their tiles defined before passing 423 // them to CompositeLayerRenderer 424 if (!isValid) { 425 return null; 426 } 427 428 // The partial sublayers won't be needed for anything anymore, and 429 // they can be dropped to save some memory 430 for (String layer : layerNames) { 431 layers.remove(layer); 432 } 433 layers.remove(adjustName); 434 435 // ** adjustment layer ** 436 Composite adjustment = zoneInfo.getEffectBlend(); 437 if (adjLayer == null) { 438 adjustment = null; 439 } 440 if (adjustment == null) { 441 // Set to null, so that we don't needlessly fetch the sprites 442 // in an unused layer. 443 adjLayer = null; 444 } 445 446 r = new CompositeLayerRenderer(subLayers, adjustment, adjLayer); 447 layers.put(compositeName, r); 448 } 449 return r; 450 } 451 452 /** 453 * Get the weather renderer. 454 * 455 * @return renderer for the weather layer. The value is always a valid 456 * renderer 457 */ getWeather()458 LayerRenderer getWeather() { 459 return weather; 460 } 461 462 /** 463 * Get the name of the weather type. 464 * 465 * @return weather name, or <code>null</code> if the zone has no special 466 * weather 467 */ getWeatherName()468 String getWeatherName() { 469 return weatherName; 470 } 471 472 /** 473 * Try validating the zone. 474 * 475 * @return <code>true</code>, if the zone has been successfully validated, 476 * <code>false</code> otherwise. 477 */ validate()478 boolean validate() { 479 if (isValid) { 480 return true; 481 } 482 483 // Tilesets are always required. Also fail validation until required 484 // data_map has been added 485 if (tileset == null || requireData) { 486 return false; 487 } 488 // Collision is always required 489 if (collision == null) { 490 return false; 491 } 492 if (!tileset.validate(zoneInfo.getZoneColor(), zoneInfo.getColorMethod())) { 493 return false; 494 } 495 496 for (final LayerRenderer lr : layers.values()) { 497 lr.setTileset(tileset); 498 } 499 500 isValid = true; 501 return true; 502 } 503 504 /** 505 * A dummy renderer for empty layer groups. 506 */ 507 private static class EmptyGroupRenderer extends LayerRenderer { 508 @Override draw(Graphics g, int x, int y, int w, int h)509 public void draw(Graphics g, int x, int y, int w, int h) { 510 } 511 512 @Override setTileset(Tileset tileset)513 public void setTileset(Tileset tileset) { 514 } 515 } 516 } 517