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