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