The Battle for Wesnoth  1.19.19+dev
halo.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2003 - 2025
3  by David White <dave@whitevine.net>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
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  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
16 /**
17  * @file
18  * Maintain halo-effects for units and items.
19  * Examples: white mage, lighthouse.
20  */
21 
22 #include "animated.hpp"
23 #include "display.hpp"
24 #include "draw.hpp"
25 #include "draw_manager.hpp"
26 #include "halo.hpp"
27 #include "log.hpp"
29 #include "sdl/rect.hpp"
30 #include "sdl/texture.hpp"
31 
32 static lg::log_domain log_halo("halo");
33 #define ERR_HL LOG_STREAM(err, log_halo)
34 #define WRN_HL LOG_STREAM(warn, log_halo)
35 #define LOG_HL LOG_STREAM(info, log_halo)
36 #define DBG_HL LOG_STREAM(debug, log_halo)
37 
38 using namespace std::chrono_literals;
39 
40 namespace halo
41 {
42 
43 class halo_impl
44 {
45  class effect
46  {
47  public:
48  effect(
49  int xpos, int ypos,
51  const map_location& loc, ORIENTATION, bool infinite
52  );
53 
54  void set_location(int x, int y);
55  rect get_draw_location();
56 
57  /** Whether the halo is currently visible */
58  bool visible();
59 
60  void queue_undraw();
61  void queue_redraw();
62  void update();
63  bool render();
64 
65  bool expired() const { return !images_.cycles() && images_.animation_finished(); }
66  bool need_update() const { return images_.need_update(); }
67  bool does_change() const { return !images_.does_not_change(); }
68 
69  private:
70 
71  const image::locator& current_image() const { return images_.get_current_frame(); }
72 
74 
76 
77  // The mid-point of the halo in pixels relative to the absolute top-left of the map, in screen coordinates.
78  // Yes it's just as ridiculous as it sounds...
79  // TODO: make this something sane. Like a floating-point map location.
80  point abs_mid_ = {0, 0};
81 
82  // The current halo image frame
83  texture tex_ = {};
84  // Track the last used locator to avoid redundant cache lookups
85  image::locator last_locator_ = image::locator();
86  // The current location where the halo will be drawn on the screen
87  rect screen_loc_ = {};
88  // The last drawn location
89  rect last_draw_loc_ = {};
90  // The display zoom level, cached so we can compensate when it changes.
91  double cached_zoom_ = 1.0;
92 
93  // The map location the halo is attached to, if any
94  map_location map_loc_ = {-1, -1};
95 
96  display* disp = nullptr;
97  };
98 
99  std::map<int, effect> haloes;
100  int halo_id{1};
101 
102  /**
103  * Upon unrendering, an invalidation list is send. All haloes in that area and
104  * the other invalidated haloes are stored in this set. Then there'll be
105  * tested which haloes overlap and they're also stored in this set.
106  */
107  std::set<int> invalidated_haloes;
108 
109  /**
110  * Upon deleting, a halo isn't deleted but added to this set, upon unrendering
111  * the image is unrendered and deleted.
112  */
113  std::set<int> deleted_haloes;
114 
115  /**
116  * Haloes that have an animation or expiration time need to be checked every
117  * frame and are stored in this set.
118  */
119  std::set<int> changing_haloes;
120 
121 public:
122  int add(int x, int y, const std::string& image, const map_location& loc,
123  ORIENTATION orientation=NORMAL, bool infinite=true);
124 
125  /** Set the position of an existing haloing effect, according to its handle. */
126  void set_location(int handle, int x, int y);
127 
128  /** Remove the halo with the given handle. */
129  void remove(int handle);
130 
131  void update();
132 
133  /** Render all halos overlapping the given region */
134  void render(const rect&);
135 
136 }; //end halo_impl
137 
138 halo_impl::effect::effect(int xpos, int ypos,
140  const map_location& loc, ORIENTATION orientation, bool infinite) :
141  images_(img),
142  orientation_(orientation),
143  map_loc_(loc),
144  disp(display::get_singleton())
145 {
146  assert(disp != nullptr);
147 
149 
150  set_location(xpos, ypos);
151 
152  images_.start_animation(0ms, infinite);
153 
154  update();
155 }
156 
158 {
159  point new_center = point{x, y} - disp->get_location(map_location::ZERO());
160  if(new_center != abs_mid_) {
161  DBG_HL << "setting halo location " << new_center;
162  abs_mid_ = new_center;
163  }
164 }
165 
167 {
168  return screen_loc_;
169 }
170 
171 /** Update the current location, animation frame, etc. */
173 {
174  double zf = disp->get_zoom_factor();
175 
176  if(map_loc_.x != -1 && map_loc_.y != -1) {
177  // If the halo is attached to a particular map location,
178  // make sure it stays attached.
179  auto [x, y] = disp->get_location_rect(map_loc_).center();
180  set_location(x, y);
181  } else {
182  // It would be good to attach to a position within a hex,
183  // or persistently to an item or unit. That's not the case,
184  // so we use some horrible hacks to compensate for zoom changes.
185  if(cached_zoom_ != zf) {
186  abs_mid_.x *= zf / cached_zoom_;
187  abs_mid_.y *= zf / cached_zoom_;
188  cached_zoom_ = zf;
189  }
190  }
191 
192  // Load texture for current animation frame
193  const image::locator& loc = current_image();
194  if (loc != last_locator_ || !tex_) {
195  tex_ = image::get_texture(loc);
196  last_locator_ = loc;
197  }
198  if(!tex_) {
199  ERR_HL << "no texture found for current halo animation frame";
200  screen_loc_ = {};
201  return;
202  }
203 
204  // Update draw location
205  int w(tex_.w() * disp->get_zoom_factor());
206  int h(tex_.h() * disp->get_zoom_factor());
207 
208  const auto [zero_x, zero_y] = disp->get_location(map_location::ZERO());
209 
210  const int xpos = zero_x + abs_mid_.x - w/2;
211  const int ypos = zero_y + abs_mid_.y - h/2;
212 
213  screen_loc_ = {xpos, ypos, w, h};
214 
215  // Queue display updates if position has changed
216  if(screen_loc_ != last_draw_loc_) {
217  queue_undraw();
218  queue_redraw();
219  last_draw_loc_ = screen_loc_;
220  }
221 }
222 
224 {
225  // Source is shrouded
226  // The halo will be completely obscured here, even if it would
227  // technically be large enough to peek out of the shroud.
228  if(map_loc_.x != -1 && map_loc_.y != -1 && disp->shrouded(map_loc_)) {
229  return false;
230  }
231 
232  // Halo is completely off screen
233  if(!screen_loc_.overlaps(disp->map_outside_area())) {
234  return false;
235  }
236 
237  return true;
238 }
239 
241 {
242  // This should only be set if we actually draw something
243  last_draw_loc_ = {};
244 
245  // Update animation frame, even if we didn't actually draw it
246  images_.update_last_draw_time();
247 
248  if(!visible()) {
249  return false;
250  }
251 
252  // Make sure we clip to the map area
253  auto clipper = draw::reduce_clip(disp->map_outside_area());
254 
255  DBG_HL << "drawing halo at " << screen_loc_;
256 
257  if (orientation_ == NORMAL) {
258  draw::blit(tex_, screen_loc_);
259  } else {
260  draw::flipped(tex_, screen_loc_,
261  orientation_ == HREVERSE || orientation_ == HVREVERSE,
262  orientation_ == VREVERSE || orientation_ == HVREVERSE);
263  }
264 
265  last_draw_loc_ = screen_loc_;
266 
267  return true;
268 }
269 
271 {
272  if(!last_draw_loc_.overlaps(disp->map_outside_area())) {
273  return;
274  }
275  DBG_HL << "queueing halo undraw at " << last_draw_loc_;
276  draw_manager::invalidate_region(last_draw_loc_);
277 }
278 
280 {
281  if(!visible()) {
282  return;
283  }
284  DBG_HL << "queueing halo redraw at " << screen_loc_;
285  draw_manager::invalidate_region(screen_loc_);
286 }
287 
288 
289 /*************/
290 /* halo_impl */
291 /*************/
292 
293 
294 int halo_impl::add(int x, int y, const std::string& image, const map_location& loc,
295  ORIENTATION orientation, bool infinite)
296 {
297  const int id = halo_id++;
298  DBG_HL << "adding halo " << id;
300  std::vector<std::string> items = utils::square_parenthetical_split(image, ',');
301 
302  for(const std::string& item : items) {
303  const std::vector<std::string>& sub_items = utils::split(item, ':');
304  std::string str = item;
305  auto time = 100ms;
306 
307  if(sub_items.size() > 1) {
308  str = sub_items.front();
309  try {
310  time = std::chrono::milliseconds{std::stoi(sub_items.back())};
311  } catch(const std::invalid_argument&) {
312  ERR_HL << "Invalid time value found when constructing halo: " << sub_items.back();
313  }
314  }
315  image_vector.emplace_back(time, image::locator(str));
316  }
317  haloes.try_emplace(id, x, y, image_vector, loc, orientation, infinite);
318  invalidated_haloes.insert(id);
319  if(haloes.find(id)->second.does_change() || !infinite) {
320  changing_haloes.insert(id);
321  }
322  return id;
323 }
324 
325 void halo_impl::set_location(int handle, int x, int y)
326 {
327  const std::map<int,effect>::iterator itor = haloes.find(handle);
328  if(itor != haloes.end()) {
329  itor->second.set_location(x,y);
330  }
331 }
332 
334 {
335  // Silently ignore invalid haloes.
336  // This happens when Wesnoth is being terminated as well.
337  if(handle == NO_HALO || haloes.find(handle) == haloes.end()) {
338  return;
339  }
340 
341  deleted_haloes.insert(handle);
342 }
343 
345 {
346  if(haloes.empty()) {
347  return;
348  }
349 
350  // Mark expired haloes for removal
351  for(auto& [id, effect] : haloes) {
352  if(effect.expired()) {
353  DBG_HL << "expiring halo " << id;
354  deleted_haloes.insert(id);
355  }
356  }
357  // Make sure deleted halos get undrawn
358  for(int id : deleted_haloes) {
359  DBG_HL << "invalidating deleted halo " << id;
360  haloes.at(id).queue_undraw();
361  }
362  // Remove deleted halos
363  for(int id : deleted_haloes) {
364  DBG_HL << "deleting halo " << id;
365  changing_haloes.erase(id);
366  haloes.erase(id);
367  }
368  deleted_haloes.clear();
369 
370  // Update the location and animation frame of the remaining halos
371  for(auto& [id, halo] : haloes) {
372  halo.update();
373  }
374 
375  // Invalidate any animated halos which need updating
376  for(int id : changing_haloes) {
377  auto& halo = haloes.at(id);
378  if(halo.need_update() && halo.visible()) {
379  DBG_HL << "invalidating changed halo " << id;
380  halo.queue_redraw();
381  }
382  }
383 }
384 
385 void halo_impl::render(const rect& region)
386 {
387  if(haloes.empty()) {
388  return;
389  }
390 
391  for(auto& [id, effect] : haloes) {
392  if(region.overlaps(effect.get_draw_location())) {
393  DBG_HL << "drawing intersected halo " << id;
394  effect.render();
395  }
396  }
397 }
398 
399 
400 /*****************/
401 /* halo::manager */
402 /*****************/
403 
404 
406 {}
407 
408 handle manager::add(int x, int y, const std::string& image, const map_location& loc,
409  ORIENTATION orientation, bool infinite)
410 {
411  int new_halo = impl_->add(x,y,image, loc, orientation, infinite);
412  return handle(new halo_record(new_halo, impl_));
413 }
414 
415 /** Set the position of an existing haloing effect, according to its handle. */
416 void manager::set_location(const handle & h, int x, int y)
417 {
418  impl_->set_location(h->id_,x,y);
419 }
420 
421 /** Remove the halo with the given handle. */
422 void manager::remove(const handle & h)
423 {
424  impl_->remove(h->id_);
425  h->id_ = NO_HALO;
426 }
427 
429 {
430  impl_->update();
431 }
432 
433 void manager::render(const rect& r)
434 {
435  impl_->render(r);
436 }
437 
438 // end halo::manager implementation
439 
440 
441 /**
442  * halo::halo_record implementation
443  */
444 
446  id_(NO_HALO), //halo::NO_HALO
447  my_manager_()
448 {}
449 
450 halo_record::halo_record(int id, const std::shared_ptr<halo_impl> & my_manager) :
451  id_(id),
452  my_manager_(my_manager)
453 {}
454 
456 {
457  if (!valid()) return;
458 
459  std::shared_ptr<halo_impl> man = my_manager_.lock();
460 
461  if(man) {
462  man->remove(id_);
463  }
464 }
465 
466 } //end namespace halo
map_location loc
Definition: move.cpp:172
Animate units.
void start_animation(const std::chrono::milliseconds &start_time, bool cycles=false)
Starts an animation cycle.
Sort-of-Singleton that many classes, both GUI and non-GUI, use to access the game data.
Definition: display.hpp:88
static double get_zoom_factor()
Returns the current zoom factor.
Definition: display.hpp:261
bool visible()
Whether the halo is currently visible.
Definition: halo.cpp:223
bool expired() const
Definition: halo.cpp:65
bool does_change() const
Definition: halo.cpp:67
bool need_update() const
Definition: halo.cpp:66
ORIENTATION orientation_
Definition: halo.cpp:75
rect get_draw_location()
Definition: halo.cpp:166
void set_location(int x, int y)
Definition: halo.cpp:157
animated< image::locator > images_
Definition: halo.cpp:73
const image::locator & current_image() const
Definition: halo.cpp:71
void update()
Update the current location, animation frame, etc.
Definition: halo.cpp:172
std::set< int > changing_haloes
Haloes that have an animation or expiration time need to be checked every frame and are stored in thi...
Definition: halo.cpp:119
void set_location(int handle, int x, int y)
Set the position of an existing haloing effect, according to its handle.
Definition: halo.cpp:325
void render(const rect &)
Render all halos overlapping the given region.
Definition: halo.cpp:385
std::set< int > invalidated_haloes
Upon unrendering, an invalidation list is send.
Definition: halo.cpp:107
int add(int x, int y, const std::string &image, const map_location &loc, ORIENTATION orientation=NORMAL, bool infinite=true)
Definition: halo.cpp:294
std::set< int > deleted_haloes
Upon deleting, a halo isn't deleted but added to this set, upon unrendering the image is unrendered a...
Definition: halo.cpp:113
std::map< int, effect > haloes
Definition: halo.cpp:99
void update()
Definition: halo.cpp:344
void remove(int handle)
Remove the halo with the given handle.
Definition: halo.cpp:333
RAII object which manages a halo.
Definition: halo.hpp:77
std::weak_ptr< halo_impl > my_manager_
Definition: halo.hpp:93
halo_record()
halo::halo_record implementation
Definition: halo.cpp:445
bool valid() const
Definition: halo.hpp:86
std::shared_ptr< halo_impl > impl_
Definition: halo.hpp:70
void update()
Process animations, remove deleted halos, and invalidate screen regions now requiring redraw.
Definition: halo.cpp:428
void set_location(const handle &h, int x, int y)
Set the position of an existing haloing effect, according to its handle.
Definition: halo.cpp:416
handle add(int x, int y, const std::string &image, const map_location &loc, halo::ORIENTATION orientation=NORMAL, bool infinite=true)
Add a haloing effect using 'image centered on (x,y).
Definition: halo.cpp:408
void remove(const handle &h)
Remove the halo with the given handle.
Definition: halo.cpp:422
void render(const rect &r)
Render halos in region.
Definition: halo.cpp:433
Generic locator abstracting the location of an image.
Definition: picture.hpp:59
Wrapper class to encapsulate creation and management of an SDL_Texture.
Definition: texture.hpp:33
map_display and display: classes which take care of displaying the map and game-data on the screen.
Drawing functions, for drawing things on the screen.
std::string id
Text to match against addon_info.tags()
Definition: manager.cpp:199
#define ERR_HL
Definition: halo.cpp:33
#define DBG_HL
Definition: halo.cpp:36
static lg::log_domain log_halo("halo")
Standard logging facilities (interface).
@ NORMAL
Definition: cursor.hpp:28
void invalidate_region(const rect &region)
Mark a region of the screen as requiring redraw.
static void render()
static void update()
clip_setter reduce_clip(const ::rect &clip)
Set the clipping area to the intersection of the current clipping area and the given rectangle.
Definition: draw.cpp:586
void flipped(const texture &tex, const ::rect &dst, bool flip_h=true, bool flip_v=false)
Draws a texture, or part of a texture, at the given location, also mirroring/flipping the texture hor...
Definition: draw.cpp:424
void blit(const texture &tex, const ::rect &dst)
Draws a texture, or part of a texture, at the given location.
Definition: draw.cpp:394
void remove()
Removes a tip.
Definition: tooltip.cpp:94
Definition: halo.cpp:41
ORIENTATION
Definition: halo.hpp:35
@ HVREVERSE
Definition: halo.hpp:35
@ VREVERSE
Definition: halo.hpp:35
@ HREVERSE
Definition: halo.hpp:35
@ NORMAL
Definition: halo.hpp:35
const int NO_HALO
Definition: halo.hpp:37
std::shared_ptr< halo_record > handle
Definition: halo.hpp:31
Functions to load and save images from/to disk.
texture get_texture(const image::locator &i_locator, TYPE type, bool skip_cache)
Returns an image texture suitable for hardware-accelerated rendering.
Definition: picture.cpp:953
std::string img(const std::string &src, const std::string &align, bool floating)
Generates a Help markup tag corresponding to an image.
Definition: markup.cpp:36
int stoi(std::string_view str)
Same interface as std::stoi and meant as a drop in replacement, except:
Definition: charconv.hpp:156
std::vector< std::string > square_parenthetical_split(const std::string &val, const char separator, const std::string &left, const std::string &right, const int flags)
Similar to parenthetical_split, but also expands embedded square brackets.
std::vector< std::string > split(const config_attribute_value &val)
std::string::const_iterator iterator
Definition: tokenizer.hpp:25
int w
Definition: pathfind.cpp:188
pump_impl & impl_
Definition: pump.cpp:131
Contains the SDL_Rect helper code.
Encapsulates the map of the game.
Definition: location.hpp:46
static const map_location & ZERO()
Definition: location.hpp:97
Holds a 2D point.
Definition: point.hpp:25
An abstract description of a rectangle with integer coordinates.
Definition: rect.hpp:49
bool overlaps(const rect &r) const
Whether the given rectangle and this rectangle overlap.
Definition: rect.cpp:74
#define h