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 #include "map/label.hpp"
16 #include "color.hpp"
17 #include "display.hpp"
18 #include "floating_label.hpp"
19 #include "formula/string_utils.hpp"
20 #include "game_board.hpp"
21 #include "game_data.hpp"
22 #include "resources.hpp"
23 #include "tooltips.hpp"
24
25 /**
26 * Our definition of map labels being obscured is if the tile is obscured,
27 * or the tile below is obscured. This is because in the case where the tile
28 * itself is visible, but the tile below is obscured, the bottom half of the
29 * tile will still be shrouded, and the label being drawn looks weird.
30 */
is_shrouded(const display * disp,const map_location & loc)31 inline bool is_shrouded(const display* disp, const map_location& loc)
32 {
33 return disp->shrouded(loc) || disp->shrouded(loc.get_direction(map_location::SOUTH));
34 }
35
36 /**
37 * Rather simple test for a hex being fogged.
38 * This only exists because is_shrouded() does. (The code looks nicer if
39 * the test for being fogged looks similar to the test for being shrouded.)
40 */
is_fogged(const display * disp,const map_location & loc)41 inline bool is_fogged(const display* disp, const map_location& loc)
42 {
43 return disp->fogged(loc);
44 }
45
map_labels(const team * team)46 map_labels::map_labels(const team* team)
47 : team_(team)
48 , labels_()
49 , enabled_(true)
50 , categories_dirty(true)
51 {
52 }
53
map_labels(const map_labels & other)54 map_labels::map_labels(const map_labels& other)
55 : team_(other.team_)
56 , labels_()
57 , enabled_(true)
58 {
59 config cfg;
60 other.write(cfg);
61 read(cfg);
62 }
63
~map_labels()64 map_labels::~map_labels()
65 {
66 clear_all();
67 }
68
operator =(const map_labels & other)69 map_labels& map_labels::operator=(const map_labels& other)
70 {
71 if(this != &other) {
72 this->~map_labels();
73 new(this) map_labels(other);
74 }
75
76 return *this;
77 }
78
write(config & res) const79 void map_labels::write(config& res) const
80 {
81 for(const auto& group : labels_) {
82 for(const auto& label : group.second) {
83 config item;
84 label.second.write(item);
85
86 res.add_child("label", std::move(item));
87 }
88 }
89 }
90
read(const config & cfg)91 void map_labels::read(const config& cfg)
92 {
93 clear_all();
94
95 for(const config& i : cfg.child_range("label")) {
96 add_label(*this, i);
97 }
98
99 recalculate_labels();
100 }
101
get_label_private(const map_location & loc,const std::string & team_name)102 terrain_label* map_labels::get_label_private(const map_location& loc, const std::string& team_name)
103 {
104 auto label_map = labels_.find(team_name);
105 if(label_map != labels_.end()) {
106 auto itor = label_map->second.find(loc);
107 if(itor != label_map->second.end()) {
108 return &itor->second;
109 }
110 }
111
112 return nullptr;
113 }
114
get_label(const map_location & loc) const115 const terrain_label* map_labels::get_label(const map_location& loc) const
116 {
117 const terrain_label* res = get_label(loc, team_name());
118
119 // No such team label. Try to find global label, except if that's what we just did.
120 // NOTE: This also avoid infinite recursion
121 if(res == nullptr && !team_name().empty()) {
122 return get_label(loc, "");
123 }
124
125 return res;
126 }
127
team_name() const128 const std::string& map_labels::team_name() const
129 {
130 if(team_) {
131 return team_->team_name();
132 }
133
134 static const std::string empty;
135 return empty;
136 }
137
set_team(const team * team)138 void map_labels::set_team(const team* team)
139 {
140 if(team_ != team) {
141 team_ = team;
142 categories_dirty = true;
143 }
144 }
145
set_label(const map_location & loc,const t_string & text,const int creator,const std::string & team_name,const color_t color,const bool visible_in_fog,const bool visible_in_shroud,const bool immutable,const std::string & category,const t_string & tooltip)146 const terrain_label* map_labels::set_label(const map_location& loc,
147 const t_string& text,
148 const int creator,
149 const std::string& team_name,
150 const color_t color,
151 const bool visible_in_fog,
152 const bool visible_in_shroud,
153 const bool immutable,
154 const std::string& category,
155 const t_string& tooltip)
156 {
157 terrain_label* res = nullptr;
158
159 // See if there is already a label in this location for this team.
160 // (We do not use get_label_private() here because we might need
161 // the label_map as well as the terrain_label.)
162 team_label_map::iterator current_label_map = labels_.find(team_name);
163 label_map::iterator current_label;
164
165 if(current_label_map != labels_.end() &&
166 (current_label = current_label_map->second.find(loc)) != current_label_map->second.end())
167 {
168 // Found old checking if need to erase it
169 if(text.str().empty()) {
170 // Erase the old label.
171 current_label_map->second.erase(current_label);
172
173 // Restore the global label in the same spot, if any.
174 if(terrain_label* global_label = get_label_private(loc, "")) {
175 global_label->recalculate();
176 }
177 } else {
178 current_label->second.update_info(
179 text, creator, tooltip, team_name, color, visible_in_fog, visible_in_shroud, immutable, category);
180
181 res = ¤t_label->second;
182 }
183 } else if(!text.str().empty()) {
184 // See if we will be replacing a global label.
185 terrain_label* global_label = get_label_private(loc, "");
186
187 // Add the new label.
188 res = add_label(
189 *this, text, creator, team_name, loc, color, visible_in_fog, visible_in_shroud, immutable, category, tooltip);
190
191 // Hide the old label.
192 if(global_label != nullptr) {
193 global_label->recalculate();
194 }
195 }
196
197 categories_dirty = true;
198 return res;
199 }
200
201 template<typename... T>
add_label(T &&...args)202 terrain_label* map_labels::add_label(T&&... args)
203 {
204 categories_dirty = true;
205
206 terrain_label t(std::forward<T>(args)...);
207 return &(*labels_[t.team_name()].emplace(t.location(), std::move(t)).first).second;
208 }
209
clear(const std::string & team_name,bool force)210 void map_labels::clear(const std::string& team_name, bool force)
211 {
212 team_label_map::iterator i = labels_.find(team_name);
213 if(i != labels_.end()) {
214 clear_map(i->second, force);
215 }
216
217 i = labels_.find("");
218 if(i != labels_.end()) {
219 clear_map(i->second, force);
220 }
221
222 categories_dirty = true;
223 }
224
clear_map(label_map & m,bool force)225 void map_labels::clear_map(label_map& m, bool force)
226 {
227 label_map::iterator i = m.begin();
228 while(i != m.end()) {
229 if(!i->second.immutable() || force) {
230 m.erase(i++);
231 } else {
232 ++i;
233 }
234 }
235
236 categories_dirty = true;
237 }
238
clear_all()239 void map_labels::clear_all()
240 {
241 labels_.clear();
242 }
243
recalculate_labels()244 void map_labels::recalculate_labels()
245 {
246 for(auto& m : labels_) {
247 for(auto& l : m.second) {
248 l.second.recalculate();
249 }
250 }
251 }
252
enable(bool is_enabled)253 void map_labels::enable(bool is_enabled)
254 {
255 if(is_enabled != enabled_) {
256 enabled_ = is_enabled;
257 recalculate_labels();
258 }
259 }
260
261 /**
262 * Returns whether or not a global (non-team) label can be shown at a
263 * specified location.
264 * (Global labels are suppressed in favor of team labels.)
265 */
visible_global_label(const map_location & loc) const266 bool map_labels::visible_global_label(const map_location& loc) const
267 {
268 if(team_ == nullptr) {
269 // We're in the editor. All global labels can be shown.
270 return true;
271 }
272
273 const team_label_map::const_iterator glabels = labels_.find(team_name());
274 return glabels == labels_.end() || glabels->second.find(loc) == glabels->second.end();
275 }
276
recalculate_shroud()277 void map_labels::recalculate_shroud()
278 {
279 for(auto& m : labels_) {
280 for(auto& l : m.second) {
281 l.second.calculate_shroud();
282 }
283 }
284 }
285
all_categories() const286 const std::vector<std::string>& map_labels::all_categories() const
287 {
288 if(categories_dirty) {
289 categories_dirty = false;
290 categories.clear();
291 categories.push_back("team");
292
293 for(size_t i = 1; i <= resources::gameboard->teams().size(); i++) {
294 categories.push_back("side:" + std::to_string(i));
295 }
296
297 std::set<std::string> unique_cats;
298 for(const auto& m : labels_) {
299 for(const auto& l : m.second) {
300 if(l.second.category().empty()) {
301 continue;
302 }
303
304 unique_cats.insert("cat:" + l.second.category());
305 }
306 }
307
308 std::copy(unique_cats.begin(), unique_cats.end(), std::back_inserter(categories));
309 }
310
311 return categories;
312 }
313
314 /** Create a new label. */
terrain_label(const map_labels & parent,const t_string & text,const int creator,const std::string & team_name,const map_location & loc,const color_t color,const bool visible_in_fog,const bool visible_in_shroud,const bool immutable,const std::string & category,const t_string & tooltip)315 terrain_label::terrain_label(const map_labels& parent,
316 const t_string& text,
317 const int creator,
318 const std::string& team_name,
319 const map_location& loc,
320 const color_t color,
321 const bool visible_in_fog,
322 const bool visible_in_shroud,
323 const bool immutable,
324 const std::string& category,
325 const t_string& tooltip)
326 : handle_(0)
327 , text_(text)
328 , tooltip_(tooltip)
329 , category_(category)
330 , team_name_(team_name)
331 , visible_in_fog_(visible_in_fog)
332 , visible_in_shroud_(visible_in_shroud)
333 , immutable_(immutable)
334 , creator_(creator)
335 , color_(color)
336 , parent_(&parent)
337 , loc_(loc)
338 {
339 draw();
340 }
341
342 /** Load label from config. */
terrain_label(const map_labels & parent,const config & cfg)343 terrain_label::terrain_label(const map_labels& parent, const config& cfg)
344 : handle_(0)
345 , tooltip_handle_(0)
346 , text_()
347 , tooltip_()
348 , team_name_()
349 , visible_in_fog_(true)
350 , visible_in_shroud_(false)
351 , immutable_(true)
352 , creator_(-1)
353 , color_()
354 , parent_(&parent)
355 , loc_()
356 {
357 read(cfg);
358 }
359
terrain_label(terrain_label && l)360 terrain_label::terrain_label(terrain_label&& l)
361 : handle_(l.handle_)
362 , tooltip_handle_(l.tooltip_handle_)
363 , text_(l.text_)
364 , tooltip_(l.tooltip_)
365 , category_(l.category_)
366 , team_name_(l.team_name_)
367 , visible_in_fog_(l.visible_in_fog_)
368 , visible_in_shroud_(l.visible_in_shroud_)
369 , immutable_(l.immutable_)
370 , creator_(l.creator_)
371 , color_(l.color_)
372 , parent_(l.parent_)
373 , loc_(l.loc_)
374 {
375 l.handle_ = 0;
376 l.tooltip_handle_ = 0;
377 }
378
~terrain_label()379 terrain_label::~terrain_label()
380 {
381 clear();
382 }
383
read(const config & cfg)384 void terrain_label::read(const config& cfg)
385 {
386 const variable_set& vs = *resources::gamedata;
387
388 loc_ = map_location(cfg, &vs);
389 color_t color = font::LABEL_COLOR;
390
391 std::string tmp_color = cfg["color"];
392
393 text_ = cfg["text"];
394 tooltip_ = cfg["tooltip"];
395 team_name_ = cfg["team_name"].str();
396 visible_in_fog_ = cfg["visible_in_fog"].to_bool(true);
397 visible_in_shroud_ = cfg["visible_in_shroud"].to_bool();
398 immutable_ = cfg["immutable"].to_bool(true);
399 category_ = cfg["category"].str();
400
401 int side = cfg["side"].to_int(-1);
402 if(side >= 0) {
403 creator_ = side - 1;
404 } else if(cfg["side"].str() == "current") {
405 config::attribute_value current_side = vs.get_variable_const("side_number");
406 if(!current_side.empty()) {
407 creator_ = current_side.to_int();
408 }
409 }
410
411 // Not moved to rendering, as that would depend on variables at render-time
412 text_ = utils::interpolate_variables_into_tstring(text_, vs);
413
414 team_name_ = utils::interpolate_variables_into_string(team_name_, vs);
415 tmp_color = utils::interpolate_variables_into_string(tmp_color, vs);
416
417 if(!tmp_color.empty()) {
418 try {
419 color = color_t::from_rgb_string(tmp_color);
420 } catch(const std::invalid_argument&) {
421 // Prior to the color_t conversion, labels were written to savefiles with an alpha key, despite alpha not
422 // being accepted in color=. Because of this, this enables the loading of older saves without an exception
423 // throwing.
424 color = color_t::from_rgba_string(tmp_color);
425 }
426 }
427
428 color_ = color;
429 }
430
write(config & cfg) const431 void terrain_label::write(config& cfg) const
432 {
433 loc_.write(cfg);
434
435 cfg["text"] = text();
436 cfg["tooltip"] = tooltip();
437 cfg["team_name"] = (this->team_name());
438 cfg["color"] = color_.to_rgb_string();
439 cfg["visible_in_fog"] = visible_in_fog_;
440 cfg["visible_in_shroud"] = visible_in_shroud_;
441 cfg["immutable"] = immutable_;
442 cfg["category"] = category_;
443 cfg["side"] = creator_ + 1;
444 }
445
update_info(const t_string & text,const int creator,const t_string & tooltip,const std::string & team_name,const color_t color)446 void terrain_label::update_info(const t_string& text,
447 const int creator,
448 const t_string& tooltip,
449 const std::string& team_name,
450 const color_t color)
451 {
452 color_ = color;
453 text_ = text;
454 tooltip_ = tooltip;
455 team_name_ = team_name;
456 creator_ = creator;
457
458 draw();
459 }
460
update_info(const t_string & text,const int creator,const t_string & tooltip,const std::string & team_name,const color_t color,const bool visible_in_fog,const bool visible_in_shroud,const bool immutable,const std::string & category)461 void terrain_label::update_info(const t_string& text,
462 const int creator,
463 const t_string& tooltip,
464 const std::string& team_name,
465 const color_t color,
466 const bool visible_in_fog,
467 const bool visible_in_shroud,
468 const bool immutable,
469 const std::string& category)
470 {
471 visible_in_fog_ = visible_in_fog;
472 visible_in_shroud_ = visible_in_shroud;
473 immutable_ = immutable;
474 category_ = category;
475
476 update_info(text, creator, tooltip, team_name, color);
477 }
478
recalculate()479 void terrain_label::recalculate()
480 {
481 draw();
482 }
483
calculate_shroud()484 void terrain_label::calculate_shroud()
485 {
486 if(handle_) {
487 font::show_floating_label(handle_, !hidden());
488 }
489
490 if(tooltip_.empty() || hidden()) {
491 tooltips::remove_tooltip(tooltip_handle_);
492 tooltip_handle_ = 0;
493 return;
494 }
495
496 // tooltips::update_tooltip(tooltip_handle, get_rect(), tooltip_.str(), "", true);
497
498 if(tooltip_handle_) {
499 tooltips::update_tooltip(tooltip_handle_, get_rect(), tooltip_.str(), "", true);
500 } else {
501 tooltip_handle_ = tooltips::add_tooltip(get_rect(), tooltip_.str());
502 }
503 }
504
get_rect() const505 SDL_Rect terrain_label::get_rect() const
506 {
507 SDL_Rect rect {0, 0, 0, 0};
508
509 display* disp = display::get_singleton();
510 if(!disp) {
511 return rect;
512 }
513
514 int hex_size = disp->hex_size();
515
516 rect.x = disp->get_location_x(loc_) + hex_size / 4;
517 rect.y = disp->get_location_y(loc_);
518 rect.h = disp->hex_size();
519 rect.w = disp->hex_size() - hex_size / 2;
520
521 return rect;
522 }
523
draw()524 void terrain_label::draw()
525 {
526 display* disp = display::get_singleton();
527 if(!disp) {
528 return;
529 }
530
531 if(text_.empty() && tooltip_.empty()) {
532 return;
533 }
534
535 clear();
536
537 if(!viewable(*disp)) {
538 return;
539 }
540
541 // Note: the y part of loc_nextx is not used at all.
542 const map_location loc_nextx = loc_.get_direction(map_location::NORTH_EAST);
543 const map_location loc_nexty = loc_.get_direction(map_location::SOUTH);
544 const int xloc = (disp->get_location_x(loc_) + disp->get_location_x(loc_nextx) * 2) / 3;
545 const int yloc = disp->get_location_y(loc_nexty) - font::SIZE_NORMAL;
546
547 // If a color is specified don't allow to override it with markup. (prevents faking map labels for example)
548 // FIXME: @todo Better detect if it's team label and not provided by the scenario.
549 bool use_markup = color_ == font::LABEL_COLOR;
550
551 font::floating_label flabel(text_.str());
552 flabel.set_color(color_);
553 flabel.set_position(xloc, yloc);
554 flabel.set_clip_rect(disp->map_outside_area());
555 flabel.set_width(font::SIZE_NORMAL * 13);
556 flabel.set_height(font::SIZE_NORMAL * 4);
557 flabel.set_scroll_mode(font::ANCHOR_LABEL_MAP);
558 flabel.use_markup(use_markup);
559
560 handle_ = font::add_floating_label(flabel);
561
562 calculate_shroud();
563 }
564
565 /**
566 * This is a lightweight test used to see if labels are revealed as a result
567 * of unit actions (i.e. fog/shroud clearing). It should not contain any tests
568 * that are invariant during unit movement (disregarding potential WML events);
569 * those belong in visible().
570 */
hidden() const571 bool terrain_label::hidden() const
572 {
573 display* disp = display::get_singleton();
574 if(!disp) {
575 return false;
576 }
577
578 // Respect user's label preferences
579 std::string category = "cat:" + category_;
580 std::string creator = "side:" + std::to_string(creator_ + 1);
581 const std::vector<std::string>& hidden_categories = disp->get_disp_context().hidden_label_categories();
582
583 if(std::find(hidden_categories.begin(), hidden_categories.end(), category) != hidden_categories.end()) {
584 return true;
585 }
586
587 if(creator_ >= 0 &&
588 std::find(hidden_categories.begin(), hidden_categories.end(), creator) != hidden_categories.end())
589 {
590 return true;
591 }
592
593 if(!team_name().empty() &&
594 std::find(hidden_categories.begin(), hidden_categories.end(), "team") != hidden_categories.end())
595 {
596 return true;
597 }
598
599 // Fog can hide some labels.
600 if(!visible_in_fog_ && is_fogged(disp, loc_)) {
601 return true;
602 }
603
604 // Shroud can hide some labels.
605 if(!visible_in_shroud_ && is_shrouded(disp, loc_)) {
606 return true;
607 }
608
609 return false;
610 }
611
612 /**
613 * This is a test used to see if we should bother with the overhead of actually
614 * creating a label. Conditions that can change during unit movement (disregarding
615 * potential WML events) should not be listed here; they belong in hidden().
616 */
viewable(const display & disp) const617 bool terrain_label::viewable(const display& disp) const
618 {
619 if(!parent_->enabled()) {
620 return false;
621 }
622
623 // In the editor, all labels are viewable.
624 if(disp.in_editor()) {
625 return true;
626 }
627
628 // Observers are not privvy to team labels.
629 const bool can_see_team_labels = !disp.get_disp_context().is_observer();
630
631 // Global labels are shown unless covered by a team label.
632 if(team_name_.empty()) {
633 return !can_see_team_labels || parent_->visible_global_label(loc_);
634 }
635
636 // Team labels are only shown to members of the team.
637 return can_see_team_labels && parent_->team_name() == team_name_;
638 }
639
clear()640 void terrain_label::clear()
641 {
642 if(handle_) {
643 font::remove_floating_label(handle_);
644 handle_ = 0;
645 }
646
647 if(tooltip_handle_) {
648 tooltips::remove_tooltip(tooltip_handle_);
649 tooltip_handle_ = 0;
650 }
651 }
652