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 = &current_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