1 /*
2    Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
3    Part of the Battle for Wesnoth Project https://www.wesnoth.org/
4 
5    This program is free software; you can redistribute it and/or modify
6    it under the terms of the GNU General Public License as published by
7    the Free Software Foundation; either version 2 of the License, or
8    (at your option) any later version.
9    This program is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY.
11 
12    See the COPYING file for more details.
13 */
14 
15 /**
16  * @file
17  * Maintain halo-effects for units and items.
18  * Examples: white mage, lighthouse.
19  */
20 
21 #include "animated.hpp"
22 #include "display.hpp"
23 #include "preferences/game.hpp"
24 #include "halo.hpp"
25 #include "log.hpp"
26 #include "serialization/string_utils.hpp"
27 
28 #include <iostream>
29 
30 static lg::log_domain log_display("display");
31 #define ERR_DP LOG_STREAM(err, log_display)
32 
33 namespace halo
34 {
35 
36 class halo_impl
37 {
38 
39 class effect
40 {
41 public:
42 	effect(display * screen, int xpos, int ypos, const animated<image::locator>::anim_description& img,
43 			const map_location& loc, ORIENTATION, bool infinite);
44 
45 	void set_location(int x, int y);
46 
47 	bool render();
48 	void unrender();
49 
expired() const50 	bool expired()     const { return !images_.cycles() && images_.animation_finished(); }
need_update() const51 	bool need_update() const { return images_.need_update(); }
does_change() const52 	bool does_change() const { return !images_.does_not_change(); }
53 	bool on_location(const std::set<map_location>& locations) const;
54 	bool location_not_known() const;
55 
56 	void add_overlay_location(std::set<map_location>& locations);
57 private:
58 
current_image() const59 	const image::locator& current_image() const { return images_.get_current_frame(); }
60 
61 	animated<image::locator> images_;
62 
63 	ORIENTATION orientation_;
64 
65 	int x_, y_;
66 	surface surf_, buffer_;
67 	SDL_Rect rect_;
68 
69 	/** The location of the center of the halo. */
70 	map_location loc_;
71 
72 	/** All locations over which the halo lies. */
73 	std::vector<map_location> overlayed_hexes_;
74 
75 	display * disp;
76 };
77 
78 display* disp;
79 
80 std::map<int, effect> haloes;
81 int halo_id;
82 
83 /**
84  * Upon unrendering, an invalidation list is send. All haloes in that area and
85  * the other invalidated haloes are stored in this set. Then there'll be
86  * tested which haloes overlap and they're also stored in this set.
87  */
88 std::set<int> invalidated_haloes;
89 
90 /**
91  * Upon deleting, a halo isn't deleted but added to this set, upon unrendering
92  * the image is unrendered and deleted.
93  */
94 std::set<int> deleted_haloes;
95 
96 /**
97  * Haloes that have an animation or expiration time need to be checked every
98  * frame and are stored in this set.
99  */
100 std::set<int> changing_haloes;
101 
102 public:
103 /**
104  * impl's of exposed functions
105  */
106 
halo_impl(display & screen)107 explicit halo_impl(display & screen) :
108 	disp(&screen),
109 	haloes(),
110 	halo_id(1),
111 	invalidated_haloes(),
112 	deleted_haloes(),
113 	changing_haloes()
114 {}
115 
116 
117 int add(int x, int y, const std::string& image, const map_location& loc,
118 		ORIENTATION orientation=NORMAL, bool infinite=true);
119 
120 /** Set the position of an existing haloing effect, according to its handle. */
121 void set_location(int handle, int x, int y);
122 
123 /** Remove the halo with the given handle. */
124 void remove(int handle);
125 
126 /**
127  * Render and unrender haloes.
128  *
129  * Which haloes are rendered is determined by invalidated_locations and the
130  * internal state in the control sets (in halo.cpp).
131  */
132 void unrender(std::set<map_location> invalidated_locations);
133 void render();
134 
135 }; //end halo_impl
136 
effect(display * screen,int xpos,int ypos,const animated<image::locator>::anim_description & img,const map_location & loc,ORIENTATION orientation,bool infinite)137 halo_impl::effect::effect(display * screen, int xpos, int ypos, const animated<image::locator>::anim_description& img,
138 		const map_location& loc, ORIENTATION orientation, bool infinite) :
139 	images_(img),
140 	orientation_(orientation),
141 	x_(0),
142 	y_(0),
143 	surf_(nullptr),
144 	buffer_(nullptr),
145 	rect_(sdl::empty_rect),
146 	loc_(loc),
147 	overlayed_hexes_(),
148 	disp(screen)
149 {
150 	assert(disp != nullptr);
151 
152 	set_location(xpos,ypos);
153 
154 	images_.start_animation(0,infinite);
155 
156 }
157 
set_location(int x,int y)158 void halo_impl::effect::set_location(int x, int y)
159 {
160 	int new_x = x - disp->get_location_x(map_location::ZERO());
161 	int new_y = y - disp->get_location_y(map_location::ZERO());
162 	if (new_x != x_ || new_y != y_) {
163 		x_ = new_x;
164 		y_ = new_y;
165 		buffer_ = nullptr;
166 		overlayed_hexes_.clear();
167 	}
168 }
169 
render()170 bool halo_impl::effect::render()
171 {
172 	if(disp == nullptr) {
173 		return false;
174 	}
175 
176 	if(loc_.x != -1 && loc_.y != -1) {
177 		if(disp->shrouded(loc_)) {
178 			return false;
179 		} else {
180 			// The location of a halo is an x,y value and not a map location.
181 			// This means when a map is zoomed, the halo's won't move,
182 			// This glitch is most visible on [item] haloes.
183 			// This workaround always recalculates the location of the halo
184 			// (item haloes have a location parameter to hide them under the shroud)
185 			// and reapplies that location.
186 			// It might be optimized by storing and comparing the zoom value.
187 			set_location(
188 				disp->get_location_x(loc_) + disp->hex_size() / 2,
189 				disp->get_location_y(loc_) + disp->hex_size() / 2);
190 		}
191 	}
192 
193 	images_.update_last_draw_time();
194 	surf_ = image::get_image(current_image(),image::SCALED_TO_ZOOM);
195 	if(surf_ == nullptr) {
196 		return false;
197 	}
198 	if(orientation_ == HREVERSE || orientation_ == HVREVERSE) {
199 		surf_ = image::reverse_image(surf_);
200 	}
201 	if(orientation_ == VREVERSE || orientation_ == HVREVERSE) {
202 		surf_ = flop_surface(surf_);
203 	}
204 
205 	const int screenx = disp->get_location_x(map_location::ZERO());
206 	const int screeny = disp->get_location_y(map_location::ZERO());
207 
208 	const int xpos = x_ + screenx - surf_->w/2;
209 	const int ypos = y_ + screeny - surf_->h/2;
210 
211 	SDL_Rect rect {xpos, ypos, surf_->w, surf_->h};
212 	rect_ = rect;
213 	SDL_Rect clip_rect = disp->map_outside_area();
214 
215 	// If rendered the first time, need to determine the area affected.
216 	// If a halo changes size, it is not updated.
217 	if(location_not_known()) {
218 		display::rect_of_hexes hexes = disp->hexes_under_rect(rect);
219 		display::rect_of_hexes::iterator i = hexes.begin(), end = hexes.end();
220 		for (;i != end; ++i) {
221 			overlayed_hexes_.push_back(*i);
222 		}
223 	}
224 
225 	if(sdl::rects_overlap(rect,clip_rect) == false) {
226 		buffer_ = nullptr;
227 		return false;
228 	}
229 
230 	surface& screen = disp->get_screen_surface();
231 
232 	const clip_rect_setter clip_setter(screen, &clip_rect);
233 	if(buffer_ == nullptr || buffer_->w != rect.w || buffer_->h != rect.h) {
234 		SDL_Rect rect2 = rect_;
235 		buffer_ = get_surface_portion(screen,rect2);
236 	} else {
237 		SDL_Rect rect2 = rect_;
238 		sdl_copy_portion(screen,&rect2,buffer_,nullptr);
239 	}
240 
241 	sdl_blit(surf_,nullptr,screen,&rect);
242 
243 	return true;
244 }
245 
unrender()246 void halo_impl::effect::unrender()
247 {
248 	if (!surf_ || !buffer_) {
249 		return;
250 	}
251 
252 	// Shrouded haloes are never rendered unless shroud has been re-placed; in
253 	// that case, unrendering causes the hidden terrain (and previous halo
254 	// frame, when dealing with animated halos) to glitch through shroud. We
255 	// don't need to unrender them because shroud paints over the underlying
256 	// area anyway.
257 	if (loc_.x != -1 && loc_.y != -1 && disp->shrouded(loc_)) {
258 		return;
259 	}
260 
261 	surface& screen = disp->get_screen_surface();
262 
263 	SDL_Rect clip_rect = disp->map_outside_area();
264 	const clip_rect_setter clip_setter(screen, &clip_rect);
265 
266 	// Due to scrolling, the location of the rendered halo
267 	// might have changed; recalculate
268 	const int screenx = disp->get_location_x(map_location::ZERO());
269 	const int screeny = disp->get_location_y(map_location::ZERO());
270 
271 	const int xpos = x_ + screenx - surf_->w/2;
272 	const int ypos = y_ + screeny - surf_->h/2;
273 
274 	SDL_Rect rect {xpos, ypos, surf_->w, surf_->h};
275 	sdl_blit(buffer_,nullptr,screen,&rect);
276 }
277 
on_location(const std::set<map_location> & locations) const278 bool halo_impl::effect::on_location(const std::set<map_location>& locations) const
279 {
280 	for(std::vector<map_location>::const_iterator itor = overlayed_hexes_.begin();
281 			itor != overlayed_hexes_.end(); ++itor) {
282 		if(locations.find(*itor) != locations.end()) {
283 			return true;
284 		}
285 	}
286 	return false;
287 }
288 
location_not_known() const289 bool halo_impl::effect::location_not_known() const
290 {
291 	return overlayed_hexes_.empty();
292 }
293 
add_overlay_location(std::set<map_location> & locations)294 void halo_impl::effect::add_overlay_location(std::set<map_location>& locations)
295 {
296 	for(std::vector<map_location>::const_iterator itor = overlayed_hexes_.begin();
297 			itor != overlayed_hexes_.end(); ++itor) {
298 
299 		locations.insert(*itor);
300 	}
301 }
302 
303 // End halo_impl::effect impl's
304 
add(int x,int y,const std::string & image,const map_location & loc,ORIENTATION orientation,bool infinite)305 int halo_impl::add(int x, int y, const std::string& image, const map_location& loc,
306 		ORIENTATION orientation, bool infinite)
307 {
308 	const int id = halo_id++;
309 	animated<image::locator>::anim_description image_vector;
310 	std::vector<std::string> items = utils::square_parenthetical_split(image, ',');
311 
312 	for(const std::string& item : items) {
313 		const std::vector<std::string>& sub_items = utils::split(item, ':');
314 		std::string str = item;
315 		int time = 100;
316 
317 		if(sub_items.size() > 1) {
318 			str = sub_items.front();
319 			try {
320 				time = std::stoi(sub_items.back());
321 			} catch(const std::invalid_argument&) {
322 				ERR_DP << "Invalid time value found when constructing halo: " << sub_items.back() << "\n";
323 			}
324 		}
325 		image_vector.push_back(animated<image::locator>::frame_description(time,image::locator(str)));
326 
327 	}
328 	haloes.emplace(id, effect(disp, x, y, image_vector, loc, orientation, infinite));
329 	invalidated_haloes.insert(id);
330 	if(haloes.find(id)->second.does_change() || !infinite) {
331 		changing_haloes.insert(id);
332 	}
333 	return id;
334 }
335 
set_location(int handle,int x,int y)336 void halo_impl::set_location(int handle, int x, int y)
337 {
338 	const std::map<int,effect>::iterator itor = haloes.find(handle);
339 	if(itor != haloes.end()) {
340 		itor->second.set_location(x,y);
341 	}
342 }
343 
remove(int handle)344 void halo_impl::remove(int handle)
345 {
346 	// Silently ignore invalid haloes.
347 	// This happens when Wesnoth is being terminated as well.
348 	if(handle == NO_HALO || haloes.find(handle) == haloes.end())  {
349 		return;
350 	}
351 
352 	deleted_haloes.insert(handle);
353 }
354 
unrender(std::set<map_location> invalidated_locations)355 void halo_impl::unrender(std::set<map_location> invalidated_locations)
356 {
357 	if(preferences::show_haloes() == false || haloes.empty()) {
358 		return;
359 	}
360 	//assert(invalidated_haloes.empty());
361 
362 	// Remove expired haloes
363 	std::map<int, effect>::iterator itor = haloes.begin();
364 	for(; itor != haloes.end(); ++itor ) {
365 		if(itor->second.expired()) {
366 			deleted_haloes.insert(itor->first);
367 		}
368 	}
369 
370 	// Add the haloes marked for deletion to the invalidation set
371 	std::set<int>::const_iterator set_itor = deleted_haloes.begin();
372 	for(;set_itor != deleted_haloes.end(); ++set_itor) {
373 		invalidated_haloes.insert(*set_itor);
374 		haloes.find(*set_itor)->second.add_overlay_location(invalidated_locations);
375 	}
376 
377 	// Test the multi-frame haloes whether they need an update
378 	for(set_itor = changing_haloes.begin();
379 			set_itor != changing_haloes.end(); ++set_itor) {
380 		if(haloes.find(*set_itor)->second.need_update()) {
381 			invalidated_haloes.insert(*set_itor);
382 			haloes.find(*set_itor)->second.add_overlay_location(invalidated_locations);
383 		}
384 	}
385 
386 	// Find all halo's in a the invalidated area
387 	size_t halo_count;
388 
389 	// Repeat until set of haloes in the invalidated area didn't change
390 	// (including none found) or all existing haloes are found.
391 	do {
392 		halo_count = invalidated_haloes.size();
393 		for(itor = haloes.begin(); itor != haloes.end(); ++itor) {
394 			// Test all haloes not yet in the set
395 			// which match one of the locations
396 			if(invalidated_haloes.find(itor->first) == invalidated_haloes.end() &&
397 					(itor->second.location_not_known() ||
398 					itor->second.on_location(invalidated_locations))) {
399 
400 				// If found, add all locations which the halo invalidates,
401 				// and add it to the set
402 				itor->second.add_overlay_location(invalidated_locations);
403 				invalidated_haloes.insert(itor->first);
404 			}
405 		}
406 	} while (halo_count != invalidated_haloes.size() && halo_count != haloes.size());
407 
408 	if(halo_count == 0) {
409 		return;
410 	}
411 
412 	// Render the haloes:
413 	// iterate through all the haloes and invalidate if in set
414 	for(std::map<int, effect>::reverse_iterator ritor = haloes.rbegin(); ritor != haloes.rend(); ++ritor) {
415 		if(invalidated_haloes.find(ritor->first) != invalidated_haloes.end()) {
416 			ritor->second.unrender();
417 		}
418 	}
419 
420 	// Really delete the haloes marked for deletion
421 	for(set_itor = deleted_haloes.begin(); set_itor != deleted_haloes.end(); ++set_itor) {
422 		changing_haloes.erase(*set_itor);
423 		invalidated_haloes.erase(*set_itor);
424 		haloes.erase(*set_itor);
425 	}
426 
427 	deleted_haloes.clear();
428 }
429 
render()430 void halo_impl::render()
431 {
432 	if(preferences::show_haloes() == false || haloes.empty() ||
433 			invalidated_haloes.empty()) {
434 		return;
435 	}
436 
437 	// Render the haloes: draw all invalidated haloes
438 	for(int id : invalidated_haloes) {
439 		haloes.at(id).render();
440 	}
441 
442 	invalidated_haloes.clear();
443 }
444 
445 // end halo_impl implementations
446 
447 // begin halo::manager
448 
manager(display & screen)449 manager::manager(display& screen) : impl_(new halo_impl(screen))
450 {}
451 
add(int x,int y,const std::string & image,const map_location & loc,ORIENTATION orientation,bool infinite)452 handle manager::add(int x, int y, const std::string& image, const map_location& loc,
453 		ORIENTATION orientation, bool infinite)
454 {
455 	int new_halo = impl_->add(x,y,image, loc, orientation, infinite);
456 	return handle(new halo_record(new_halo, impl_));
457 }
458 
459 /** Set the position of an existing haloing effect, according to its handle. */
set_location(const handle & h,int x,int y)460 void manager::set_location(const handle & h, int x, int y)
461 {
462 	impl_->set_location(h->id_,x,y);
463 }
464 
465 /** Remove the halo with the given handle. */
remove(const handle & h)466 void manager::remove(const handle & h)
467 {
468 	impl_->remove(h->id_);
469 	h->id_ = NO_HALO;
470 }
471 
472 /**
473  * Render and unrender haloes.
474  *
475  * Which haloes are rendered is determined by invalidated_locations and the
476  * internal state in the control sets (in halo.cpp).
477  */
unrender(std::set<map_location> invalidated_locations)478 void manager::unrender(std::set<map_location> invalidated_locations)
479 {
480 	impl_->unrender(invalidated_locations);
481 }
482 
render()483 void manager::render()
484 {
485 	impl_->render();
486 }
487 
488 // end halo::manager implementation
489 
490 
491 /**
492  * halo::halo_record implementation
493  */
494 
halo_record()495 halo_record::halo_record() :
496 	id_(NO_HALO), //halo::NO_HALO
497 	my_manager_()
498 {}
499 
halo_record(int id,const std::shared_ptr<halo_impl> & my_manager)500 halo_record::halo_record(int id, const std::shared_ptr<halo_impl> & my_manager) :
501 	id_(id),
502 	my_manager_(my_manager)
503 {}
504 
~halo_record()505 halo_record::~halo_record()
506 {
507 	if (!valid()) return;
508 
509 	std::shared_ptr<halo_impl> man = my_manager_.lock();
510 
511 	if (man) {
512 		try {
513 			man->remove(id_);
514 		} catch (std::exception & e) {
515 			std::cerr << "Caught an exception in halo::halo_record destructor: \n" << e.what() << std::endl;
516 		} catch (...) {}
517 	}
518 }
519 
520 } //end namespace halo
521