wesnoth/src/display.cpp

2373 lines
65 KiB
C++

/* $Id$ */
/*
Copyright (C) 2003 - 2009 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2
or at your option any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
/**
* @file display.cpp
* Routines to set up the display, scroll and zoom the map.
*/
#include "global.hpp"
#include "actions.hpp"
#include "builder.hpp"
#include "cursor.hpp"
#include "display.hpp"
#include "events.hpp"
#include "foreach.hpp"
#include "game_config.hpp"
#include "game_preferences.hpp"
#include "gettext.hpp"
#include "halo.hpp"
#include "hotkeys.hpp"
#include "language.hpp"
#include "log.hpp"
#include "marked-up_text.hpp"
#include "map.hpp"
#include "map_label.hpp"
#include "minimap.hpp"
#include "preferences.hpp"
#include "sdl_utils.hpp"
#include "text.hpp"
#include "tooltips.hpp"
#include "SDL_image.h"
#include <algorithm>
#include <cassert>
#ifdef __SUNPRO_CC
// GCC doesn't have hypot in cmath so include it for Sun Studio
#include <math.h>
#endif
#include <cmath>
#include <iostream>
#include <sstream>
static lg::log_domain log_display("display");
#define ERR_DP LOG_STREAM(err, log_display)
#define LOG_DP LOG_STREAM(info, log_display)
#define DBG_DP LOG_STREAM(debug, log_display)
namespace {
#ifdef USE_TINY_GUI
const int DefaultZoom = 36;
const int SmallZoom = 16;
#else
const int DefaultZoom = 72;
const int SmallZoom = 36;
#endif
const int MinZoom = 4;
const int MaxZoom = 200;
size_t sunset_delay = 0;
size_t sunset_timer = 0;
bool benchmark = false;
}
int display::last_zoom_ = SmallZoom;
display::display(CVideo& video, const gamemap* map, const config& theme_cfg, const config& cfg, const config& level) :
screen_(video),
map_(map),
viewpoint_(NULL),
xpos_(0),
ypos_(0),
theme_(theme_cfg, screen_area()),
zoom_(DefaultZoom),
builder_(new terrain_builder(cfg, level, map, theme_.border().tile_image)),
minimap_(NULL),
minimap_location_(empty_rect),
redrawMinimap_(false),
redraw_background_(true),
invalidateAll_(true),
grid_(false),
diagnostic_label_(0),
panelsDrawn_(false),
turbo_speed_(2),
turbo_(false),
invalidateGameStatus_(true),
map_labels_(new map_labels(*this, 0)),
shroud_image_("terrain/" + get_map().get_terrain_info(t_translation::VOID_TERRAIN).minimap_image() + ".png"),
fog_image_("terrain/" + get_map().get_terrain_info(t_translation::FOGGED).minimap_image() + ".png"),
tod_(time_of_day()),
scroll_event_("scrolled"),
nextDraw_(0),
report_(),
buttons_(),
invalidated_(),
previous_invalidated_(),
selected_hex_overlay_(0),
mouseover_hex_overlay_(0),
selectedHex_(),
mouseoverHex_(),
keys_(),
#if TDRAWING_BUFFER_USES_VECTOR
drawing_buffer_(LAYER_LAST_LAYER),
#else
drawing_buffer_(),
#endif
map_screenshot_(false),
fps_handle_(0),
invalidated_hexes_(0),
drawn_hexes_(0),
idle_anim_(preferences::idle_anim()),
idle_anim_rate_(1.0),
map_screenshot_surf_(NULL),
redraw_observers_(),
draw_coordinates_(false),
draw_terrain_codes_(false)
{
if(non_interactive()
&& (get_video_surface() != NULL
&& video.faked())) {
screen_.lock_updates(true);
}
set_idle_anim_rate(preferences::idle_anim_rate());
std::fill(reportRects_,reportRects_+reports::NUM_REPORTS,empty_rect);
image::set_zoom(zoom_);
}
display::~display()
{
}
void display::rebuild_all()
{
builder_->rebuild_all();
}
void display::reload_map()
{
builder_->reload_map();
}
void display::change_map(const gamemap* m)
{
map_ = m;
builder_->change_map(m);
}
const SDL_Rect& display::max_map_area() const
{
static SDL_Rect max_area = {0, 0, 0, 0};
// hex_size() is always a multiple of 4
// and hex_width() a multiple of 3,
// so there shouldn't be off-by-one-errors
// due to rounding.
// To display a hex fully on screen,
// a little bit extra space is needed.
// Also added the border two times.
max_area.w = static_cast<int>((get_map().w() + 2 * theme_.border().size + 1.0/3.0) * hex_width());
max_area.h = static_cast<int>((get_map().h() + 2 * theme_.border().size + 0.5) * hex_size());
return max_area;
}
const SDL_Rect& display::map_area() const
{
static SDL_Rect max_area;
max_area = max_map_area();
// if it's for map_screenshot, maximize and don't recenter
if (map_screenshot_) {
return max_area;
}
static SDL_Rect res;
res = map_outside_area();
if(max_area.w < res.w) {
// map is smaller, center
res.x += (res.w - max_area.w)/2;
res.w = max_area.w;
}
if(max_area.h < res.h) {
// map is smaller, center
res.y += (res.h - max_area.h)/2;
res.h = max_area.h;
}
return res;
}
bool display::outside_area(const SDL_Rect& area, const int x, const int y) const
{
const int x_thresh = hex_size();
const int y_thresh = hex_size();
return (x < area.x || x > area.x + area.w - x_thresh ||
y < area.y || y > area.y + area.h - y_thresh);
}
// This function use the screen as reference
const map_location display::hex_clicked_on(int xclick, int yclick) const
{
const SDL_Rect& rect = map_area();
if(point_in_rect(xclick,yclick,rect) == false) {
return map_location();
}
xclick -= rect.x;
yclick -= rect.y;
return pixel_position_to_hex(xpos_ + xclick, ypos_ + yclick);
}
// This function use the rect of map_area as reference
const map_location display::pixel_position_to_hex(int x, int y) const
{
// adjust for the border
x -= static_cast<int>(theme_.border().size * hex_width());
y -= static_cast<int>(theme_.border().size * hex_size());
// The editor can modify the border and this will result in a negative y
// value. Instead of adding extra cases we just shift the hex. Since the
// editor doesn't use the direction this is no problem.
const int offset = y < 0 ? 1 : 0;
if(offset) {
x += hex_width();
y += hex_size();
}
const int s = hex_size();
const int tesselation_x_size = hex_width() * 2;
const int tesselation_y_size = s;
const int x_base = x / tesselation_x_size * 2;
const int x_mod = x % tesselation_x_size;
const int y_base = y / tesselation_y_size;
const int y_mod = y % tesselation_y_size;
int x_modifier = 0;
int y_modifier = 0;
if (y_mod < tesselation_y_size / 2) {
if ((x_mod * 2 + y_mod) < (s / 2)) {
x_modifier = -1;
y_modifier = -1;
} else if ((x_mod * 2 - y_mod) < (s * 3 / 2)) {
x_modifier = 0;
y_modifier = 0;
} else {
x_modifier = 1;
y_modifier = -1;
}
} else {
if ((x_mod * 2 - (y_mod - s / 2)) < 0) {
x_modifier = -1;
y_modifier = 0;
} else if ((x_mod * 2 + (y_mod - s / 2)) < s * 2) {
x_modifier = 0;
y_modifier = 0;
} else {
x_modifier = 1;
y_modifier = 0;
}
}
return map_location(x_base + x_modifier - offset, y_base + y_modifier - offset);
// NOTE: This code to get nearest_hex and second_nearest_hex
// is not used anymore. However, it can be usefull later.
// So, keep it here for the moment.
/*
if(nearest_hex != NULL) {
// Our x and y use the map_area as reference.
// The coordinates given by get_location use the screen as reference,
// so we need to convert it.
const int centerx = (get_location_x(res) - map_area().x + xpos_) + hex_size()/2 - hex_width();
const int centery = (get_location_y(res) - map_area().y + ypos_) + hex_size()/2 - hex_size();
const int x_offset = x - centerx;
const int y_offset = y - centery;
if(y_offset > 0) {
if(x_offset > y_offset/2) {
*nearest_hex = map_location::SOUTH_EAST;
if(second_nearest_hex != NULL) {
if(x_offset/2 > y_offset) {
*second_nearest_hex = map_location::NORTH_EAST;
} else {
*second_nearest_hex = map_location::SOUTH;
}
}
} else if(-x_offset > y_offset/2) {
*nearest_hex = map_location::SOUTH_WEST;
if(second_nearest_hex != NULL) {
if(-x_offset/2 > y_offset) {
*second_nearest_hex = map_location::NORTH_WEST;
} else {
*second_nearest_hex = map_location::SOUTH;
}
}
} else {
*nearest_hex = map_location::SOUTH;
if(second_nearest_hex != NULL) {
if(x_offset > 0) {
*second_nearest_hex = map_location::SOUTH_EAST;
} else {
*second_nearest_hex = map_location::SOUTH_WEST;
}
}
}
} else { // y_offset <= 0
if(x_offset > -y_offset/2) {
*nearest_hex = map_location::NORTH_EAST;
if(second_nearest_hex != NULL) {
if(x_offset/2 > -y_offset) {
*second_nearest_hex = map_location::SOUTH_EAST;
} else {
*second_nearest_hex = map_location::NORTH;
}
}
} else if(-x_offset > -y_offset/2) {
*nearest_hex = map_location::NORTH_WEST;
if(second_nearest_hex != NULL) {
if(-x_offset/2 > -y_offset) {
*second_nearest_hex = map_location::SOUTH_WEST;
} else {
*second_nearest_hex = map_location::NORTH;
}
}
} else {
*nearest_hex = map_location::NORTH;
if(second_nearest_hex != NULL) {
if(x_offset > 0) {
*second_nearest_hex = map_location::NORTH_EAST;
} else {
*second_nearest_hex = map_location::NORTH_WEST;
}
}
}
}
}
*/
}
void display::rect_of_hexes::iterator::operator++()
{
if (loc_.y < rect_.bottom[loc_.x & 1])
loc_.y++;
else {
loc_.x++;
loc_.y = rect_.top[loc_.x & 1];
}
}
// begin is top left, and end is after bottom right
display::rect_of_hexes::iterator display::rect_of_hexes::begin() const
{
return iterator(map_location(left, top[left & 1]), *this);
}
display::rect_of_hexes::iterator display::rect_of_hexes::end() const
{
return iterator(map_location(right+1, top[(right+1) & 1]), *this);
}
const display::rect_of_hexes display::hexes_under_rect(const SDL_Rect& r) const
{
rect_of_hexes res;
if (r.w<=0 || r.h<=0) {
// empty rect, return dummy values giving begin=end
res.left = 0;
res.right = -1; // end is right+1
res.top[0] = 0;
res.top[1] = 0;
res.bottom[0] = 0;
res.bottom[1] = 0;
return res;
}
SDL_Rect map_rect = map_area();
// translate rect coordinates from screen-based to map_area-based
int x = xpos_ - map_rect.x + r.x;
int y = ypos_ - map_rect.y + r.y;
// we use the "double" type to avoid important rounding error (size of an hex!)
// we will also need to use std::floor to avoid bad rounding at border (negative values)
double tile_width = hex_width();
double tile_size = hex_size();
double border = theme_.border().size;
// the "-0.25" is for the horizontal imbrication of hexes (1/4 overlaps).
res.left = static_cast<int>(std::floor(-border + x / tile_width - 0.25));
// we remove 1 pixel of the rectangle dimensions
// (the rounded division take one pixel more than needed)
res.right = static_cast<int>(std::floor(-border + (x + r.w-1) / tile_width));
// for odd x, we must shift up one half-hex. Since x will vary along the edge,
// we store here the y values for even and odd x, respectively
res.top[0] = static_cast<int>(std::floor(-border + y / tile_size));
res.top[1] = static_cast<int>(std::floor(-border + y / tile_size - 0.5));
res.bottom[0] = static_cast<int>(std::floor(-border + (y + r.h-1) / tile_size));
res.bottom[1] = static_cast<int>(std::floor(-border + (y + r.h-1) / tile_size - 0.5));
// TODO: in some rare cases (1/16), a corner of the big rect is on a tile
// (the 72x72 rectangle containing the hex) but not on the hex itself
// Can maybe be optimized by using pixel_position_to_hex
return res;
}
int display::get_location_x(const map_location& loc) const
{
return static_cast<int>(map_area().x + (loc.x + theme_.border().size) * hex_width() - xpos_);
}
int display::get_location_y(const map_location& loc) const
{
return static_cast<int>(map_area().y + (loc.y + theme_.border().size) * zoom_ - ypos_ + (is_odd(loc.x) ? zoom_/2 : 0));
}
map_location display::minimap_location_on(int x, int y)
{
//TODO: don't return location for this,
// instead directly scroll to the clicked pixel position
if (!point_in_rect(x, y, minimap_area())) {
return map_location();
}
// we transfom the coordinates from minimap to the full map image
// probably more adjustements to do (border, minimap shift...)
// but the mouse and human capacity to evaluate the rectangle center
// is not pixel precise.
int px = (x - minimap_location_.x) * get_map().w()*hex_width() / minimap_location_.w;
int py = (y - minimap_location_.y) * get_map().h()*hex_size() / minimap_location_.h;
map_location loc = pixel_position_to_hex(px, py);
if (loc.x < 0)
loc.x = 0;
else if (loc.x >= get_map().w())
loc.x = get_map().w() - 1;
if (loc.y < 0)
loc.y = 0;
else if (loc.y >= get_map().h())
loc.y = get_map().h() - 1;
return loc;
}
int display::screenshot(std::string filename, bool map_screenshot)
{
int size = 0;
if (!map_screenshot) {
surface screenshot_surf = screen_.getSurface();
SDL_SaveBMP(screenshot_surf, filename.c_str());
size = screenshot_surf->w * screenshot_surf->h;
} else {
if (get_map().empty()) {
// Map Screenshot are big, abort and warn the user if he does strange things
std::cerr << "No map, can't do a Map Screenshot. If it was not wanted, check your hotkey.\n";
return -1;
}
SDL_Rect area = max_map_area();
map_screenshot_surf_ = create_compatible_surface(screen_.getSurface(), area.w, area.h);
if (map_screenshot_surf_ == NULL) {
// Memory problem ?
std::cerr << "Can't create the screenshot surface. Maybe too big, try dezooming.\n";
return -1;
}
size = map_screenshot_surf_->w * map_screenshot_surf_->h;
// back up the current map view position and move to top-left
int old_xpos = xpos_;
int old_ypos = ypos_;
xpos_ = 0;
ypos_ = 0;
// we reroute render output to the screenshot surface and invalidate all
map_screenshot_= true ;
invalidateAll_ = true;
DBG_DP << "draw() with map_screenshot\n";
draw(true,true);
// finally save the image on disk
SDL_SaveBMP(map_screenshot_surf_, filename.c_str());
//NOTE: need to be sure that we free this huge surface (is it enough?)
map_screenshot_surf_ = NULL;
// restore normal rendering
map_screenshot_= false;
xpos_ = old_xpos;
ypos_ = old_ypos;
// some drawing functions are confused by the temporary change
// of the map_area and thus affect the UI outside of the map
redraw_everything();
}
// convert pixel size to BMP size
size = (2048 + size*3);
return size;
}
gui::button* display::find_button(const std::string& id)
{
for (size_t i = 0; i < buttons_.size(); ++i) {
if(buttons_[i].id() == id) {
return &buttons_[i];
}
}
return NULL;
}
void display::create_buttons()
{
std::vector<gui::button> work;
DBG_DP << "creating buttons...\n";
const std::vector<theme::menu>& buttons = theme_.menus();
for(std::vector<theme::menu>::const_iterator i = buttons.begin(); i != buttons.end(); ++i) {
gui::button b(screen_,i->title(),string_to_button_type(i->type()),i->image());
DBG_DP << "drawing button " << i->get_id() << "\n";
b.set_id(i->get_id());
const SDL_Rect& loc = i->location(screen_area());
b.set_location(loc.x,loc.y);
if (!i->tooltip().empty()){
tooltips::add_tooltip(loc, i->tooltip());
}
if(rects_overlap(b.location(),map_outside_area())) {
b.set_volatile(true);
}
gui::button* b_prev = find_button(b.id());
if(b_prev) b.enable(b_prev->enabled());
work.push_back(b);
}
buttons_.swap(work);
DBG_DP << "buttons created\n";
}
gui::button::TYPE display::string_to_button_type(std::string type)
{
gui::button::TYPE res = gui::button::TYPE_PRESS;
if (type == "checkbox") { res = gui::button::TYPE_CHECK; }
else if (type == "image") { res = gui::button::TYPE_IMAGE; }
return res;
}
static const std::string& get_direction(size_t n)
{
static std::string const dirs[6] = { "-n", "-ne", "-se", "-s", "-sw", "-nw" };
return dirs[n >= sizeof(dirs)/sizeof(*dirs) ? 0 : n];
}
std::vector<std::string> display::get_fog_shroud_graphics(const map_location& loc)
{
std::vector<std::string> res;
map_location adjacent[6];
get_adjacent_tiles(loc,adjacent);
t_translation::t_terrain tiles[6];
static const t_translation::t_terrain terrain_types[] =
{ t_translation::FOGGED, t_translation::VOID_TERRAIN, t_translation::NONE_TERRAIN };
for(int i = 0; i != 6; ++i) {
if(shrouded(adjacent[i])) {
tiles[i] = t_translation::VOID_TERRAIN;
} else if(!fogged(loc) && fogged(adjacent[i])) {
tiles[i] = t_translation::FOGGED;
} else {
tiles[i] = t_translation::NONE_TERRAIN;
}
}
for(const t_translation::t_terrain *terrain = terrain_types;
*terrain != t_translation::NONE_TERRAIN; terrain ++) {
// Find somewhere that doesn't have overlap to use as a starting point
int start;
for(start = 0; start != 6; ++start) {
if(tiles[start] != *terrain) {
break;
}
}
if(start == 6) {
start = 0;
}
// Find all the directions overlap occurs from
for(int i = (start+1)%6, n = 0; i != start && n != 6; ++n) {
if(tiles[i] == *terrain) {
std::ostringstream stream;
std::string name;
// if(*terrain == terrain_type::VOID_TERRAIN)
// stream << "void";
//else
// stream << "fog";
stream << "terrain/" << get_map().get_terrain_info(*terrain).minimap_image();
for(int n = 0; *terrain == tiles[i] && n != 6; i = (i+1)%6, ++n) {
stream << get_direction(i);
if(!image::exists(stream.str() + ".png")) {
// If we don't have any surface at all,
// then move onto the next overlapped area
if(name.empty()) {
i = (i+1)%6;
}
break;
} else {
name = stream.str();
}
}
if(!name.empty()) {
res.push_back(name + ".png");
}
} else {
i = (i+1)%6;
}
}
}
return res;
}
std::vector<surface> display::get_terrain_images(const map_location &loc,
const std::string& timeid,
image::TYPE image_type,
ADJACENT_TERRAIN_TYPE terrain_type)
{
std::vector<surface> res;
if(terrain_type == ADJACENT_FOGSHROUD) {
const std::vector<std::string> fog_shroud = get_fog_shroud_graphics(loc);
if(!fog_shroud.empty()) {
for(std::vector<std::string>::const_iterator it = fog_shroud.begin(); it != fog_shroud.end(); ++it) {
image::locator image(*it);
const surface surface(image::get_image(image, image_type));
if (!surface.null()) {
res.push_back(surface);
}
}
}
return res;
}
terrain_builder::ADJACENT_TERRAIN_TYPE builder_terrain_type =
(terrain_type == ADJACENT_FOREGROUND ?
terrain_builder::ADJACENT_FOREGROUND : terrain_builder::ADJACENT_BACKGROUND);
const terrain_builder::imagelist* const terrains = builder_->get_terrain_at(loc,
timeid, builder_terrain_type);
if(terrains != NULL) {
// Cache the offmap name.
// Since it is themabel it can change,
// so don't make it static.
const std::string off_map_name = "terrain/" + theme_.border().tile_image + ".png";
for(std::vector<animated<image::locator> >::const_iterator it =
terrains->begin(); it != terrains->end(); ++it) {
const image::locator& image = preferences::animate_map() ?
it->get_current_frame() : it->get_first_frame();
// We prevent ToD colouring and brightening of off-map tiles,
// except if we are not in_game and so in the editor.
// We need to test for the tile to be rendered and
// not the location, since the transitions are rendered
// over the offmap-terrain and these need a ToD colouring.
const bool off_map = (image.get_filename() == off_map_name);
const surface surface(image::get_image(image,
off_map ? image::UNMASKED : image_type));
if (!surface.null()) {
res.push_back(surface);
}
}
}
return res;
}
void display::drawing_buffer_commit()
{
SDL_Rect clip_rect = map_area();
surface const dst(get_screen_surface());
clip_rect_setter set_clip_rect(dst, clip_rect);
/*
* Info regarding the rendering algorithm.
*
* In order to render a hex properly it needs to be rendered per row. On
* this row several layers need to be drawn at the same time. Mainly the
* unit and the background terrain. This is needed since both can spill
* in the next hex. The foreground terrain needs to be drawn before to
* avoid decapitation a unit.
*
* This ended in the following algorithm.
*
* - Loop over all layer groups.
* - For all layers in this group proceed to render from low to high.
* - Render all rows (a row has a constant y) from low to high.
* - In the row render the columns from low to high.
*/
// The drawing is done per layer_group, the range per group is [low, high).
static const tdrawing_layer layer_groups[] = {
LAYER_TERRAIN_BG,
LAYER_UNIT_FIRST,
LAYER_UNIT_MOVE_DEFAULT,
// Make sure the movement doesn't show above fog and reachmap.
LAYER_REACHMAP,
LAYER_LAST_LAYER };
for(size_t z = 1; z < sizeof(layer_groups)/sizeof(layer_groups[0]); ++z) {
for(int y = -get_map().border_size();
y < get_map().total_height(); ++y) {
#if TDRAWING_BUFFER_USES_VECTOR
int layer = -1;
#endif
for(tdrawing_buffer::const_iterator
layer_itor = drawing_buffer_.begin(),
layer_itor_end = drawing_buffer_.end();
layer_itor != layer_itor_end; ++layer_itor) {
#if TDRAWING_BUFFER_USES_VECTOR
++layer;
if(!(layer >= layer_groups[z - 1] &&
layer < layer_groups[z])) {
// The current layer is not in the layer group, skip.
continue;
}
#else
if(!(layer_itor->first >= layer_groups[z - 1] &&
layer_itor->first < layer_groups[z])) {
// The current layer is not in the layer group, skip.
continue;
}
#endif
for(tblit_map::const_iterator
#if TDRAWING_BUFFER_USES_VECTOR
drawing_iterator = layer_itor->begin(),
drawing_iterator_end = layer_itor->end();
#else
drawing_iterator = layer_itor->second.begin(),
drawing_iterator_end = layer_itor->second.end();
#endif
drawing_iterator != drawing_iterator_end;
++drawing_iterator) {
if(drawing_iterator->first.y != y) {
// The current row is not the row to render, skip.
continue;
}
for(std::vector<tblit>::const_iterator
blit_itor = drawing_iterator->second.begin(),
blit_itor_end = drawing_iterator->second.end();
blit_itor != blit_itor_end; ++blit_itor) {
for(std::vector<surface>::const_iterator
surface_itor = blit_itor->surf.begin(),
surface_itor_end = blit_itor->surf.end();
surface_itor != surface_itor_end; ++surface_itor) {
// Note that dstrect can be changed by SDL_BlitSurface
// and so a new instance should be initialized
// to pass to each call to SDL_BlitSurface.
SDL_Rect dstrect = { blit_itor->x, blit_itor->y, 0, 0 };
if(blit_itor->clip.x || blit_itor->clip.y
||blit_itor->clip.w ||blit_itor->clip.h) {
SDL_Rect srcrect = blit_itor->clip;
SDL_BlitSurface(*surface_itor,
&srcrect, dst, &dstrect);
} else {
SDL_BlitSurface(*surface_itor,
NULL, dst, &dstrect);
}
}
update_rect(blit_itor->x, blit_itor->y, zoom_, zoom_);
} // for blit_itor
} // for drawing_iterator
} // for layer_itor
} // for y
} // for z
drawing_buffer_clear();
}
void display::drawing_buffer_clear()
{
#if TDRAWING_BUFFER_USES_VECTOR
// Note clear the items, the vector should remain the same size.
for(tdrawing_buffer::iterator layer_itor =
drawing_buffer_.begin(),
layer_itor_end = drawing_buffer_.end();
layer_itor != layer_itor_end; ++layer_itor) {
layer_itor->clear();
}
#else
drawing_buffer_.clear();
#endif
}
void display::sunset(const size_t delay)
{
// This allow both parametric and toggle use
sunset_delay = (sunset_delay == 0 && delay == 0) ? 5 : delay;
}
void display::toggle_benchmark()
{
benchmark = !benchmark;
}
void display::flip()
{
if(video().faked()) {
return;
}
const surface frameBuffer = get_video_surface();
// This is just the debug function "sunset" to progressively darken the map area
if (sunset_delay && ++sunset_timer > sunset_delay) {
sunset_timer = 0;
SDL_Rect r = map_outside_area(); // Use frameBuffer to also test the UI
const Uint32 color = SDL_MapRGBA(video().getSurface()->format,0,0,0,255);
// Adjust the alpha if you want to balance cpu-cost / smooth sunset
fill_rect_alpha(r, color, 1, frameBuffer);
update_rect(r);
}
font::draw_floating_labels(frameBuffer);
events::raise_volatile_draw_event();
cursor::draw(frameBuffer);
video().flip();
cursor::undraw(frameBuffer);
events::raise_volatile_undraw_event();
font::undraw_floating_labels(frameBuffer);
}
void display::update_display()
{
if (screen_.update_locked()) {
return;
}
if(preferences::show_fps() || benchmark) {
static int last_sample = SDL_GetTicks();
static int frames = 0;
++frames;
const int sample_freq = 10;
if(frames == sample_freq) {
const int this_sample = SDL_GetTicks();
const int fps = (frames*1000)/(this_sample - last_sample);
last_sample = this_sample;
frames = 0;
if(fps_handle_ != 0) {
font::remove_floating_label(fps_handle_);
fps_handle_ = 0;
}
std::ostringstream stream;
stream << "fps: " << fps;
if (game_config::debug) {
stream << "\nhex: " << drawn_hexes_*1.0/sample_freq;
if (drawn_hexes_ != invalidated_hexes_)
stream << " (" << (invalidated_hexes_-drawn_hexes_)*1.0/sample_freq << ")";
}
drawn_hexes_ = 0;
invalidated_hexes_ = 0;
fps_handle_ = font::add_floating_label(stream.str(),12,
benchmark ? font::BAD_COLOUR : font::NORMAL_COLOUR,
10,100,0,0,-1,screen_area(),font::LEFT_ALIGN);
}
} else if(fps_handle_ != 0) {
font::remove_floating_label(fps_handle_);
fps_handle_ = 0;
drawn_hexes_ = 0;
invalidated_hexes_ = 0;
}
flip();
}
static void draw_panel(CVideo& video, const theme::panel& panel, std::vector<gui::button>& buttons)
{
//log_scope("draw panel");
DBG_DP << "drawing panel " << panel.get_id() << "\n";
surface surf(image::get_image(panel.image()));
const SDL_Rect screen = screen_area();
SDL_Rect& loc = panel.location(screen);
DBG_DP << "panel location: x=" << loc.x << ", y=" << loc.y
<< ", w=" << loc.w << ", h=" << loc.h << "\n";
if(!surf.null()) {
if(surf->w != loc.w || surf->h != loc.h) {
surf.assign(scale_surface(surf,loc.w,loc.h));
}
video.blit_surface(loc.x,loc.y,surf);
update_rect(loc);
}
static bool first_time = true;
for(std::vector<gui::button>::iterator b = buttons.begin(); b != buttons.end(); ++b) {
if(rects_overlap(b->location(),loc)) {
b->set_dirty(true);
if (first_time){
/**
* @todo FixMe YogiHH:
* This is only made to have the buttons store their background
* information, otherwise the background will appear completely
* black. It would more straightforward to call bg_update, but
* that is not public and there seems to be no other way atm to
* call it. I will check if bg_update can be made public.
*/
b->hide(true);
b->hide(false);
}
}
}
}
static void draw_label(CVideo& video, surface target, const theme::label& label)
{
//log_scope("draw label");
std::stringstream temp;
Uint32 RGB=label.font_rgb();
int red = (RGB & 0x00FF0000)>>16;
int green = (RGB & 0x0000FF00)>>8;
int blue = (RGB & 0x000000FF);
std::string c_start="<";
std::string c_sep=",";
std::string c_end=">";
std::stringstream color;
color<< c_start << red << c_sep << green << c_sep << blue << c_end;
std::string text = label.text();
if(label.font_rgb_set()) {
color<<text;
text = color.str();
}
const std::string& icon = label.icon();
SDL_Rect& loc = label.location(screen_area());
if(icon.empty() == false) {
surface surf(image::get_image(icon));
if(!surf.null()) {
if(surf->w > loc.w || surf->h > loc.h) {
surf.assign(scale_surface(surf,loc.w,loc.h));
}
SDL_BlitSurface(surf,NULL,target,&loc);
}
if(text.empty() == false) {
tooltips::add_tooltip(loc,text);
}
} else if(text.empty() == false) {
font::draw_text(&video,loc,label.font_size(),font::NORMAL_COLOUR,text,loc.x,loc.y);
}
update_rect(loc);
}
void display::draw_all_panels()
{
surface const screen(screen_.getSurface());
const std::vector<theme::panel>& panels = theme_.panels();
for(std::vector<theme::panel>::const_iterator p = panels.begin(); p != panels.end(); ++p) {
draw_panel(video(),*p,buttons_);
}
const std::vector<theme::label>& labels = theme_.labels();
for(std::vector<theme::label>::const_iterator i = labels.begin(); i != labels.end(); ++i) {
draw_label(video(),screen,*i);
}
create_buttons();
}
static void draw_background(surface screen, const SDL_Rect& area, const std::string& image)
{
const surface background(image::get_image(image));
if(background.null()) {
return;
}
const unsigned int width = background->w;
const unsigned int height = background->h;
const unsigned int w_count = static_cast<int>(std::ceil(static_cast<double>(area.w) / static_cast<double>(width)));
const unsigned int h_count = static_cast<int>(std::ceil(static_cast<double>(area.h) / static_cast<double>(height)));
for(unsigned int w = 0, w_off = area.x; w < w_count; ++w, w_off += width) {
for(unsigned int h = 0, h_off = area.y; h < h_count; ++h, h_off += height) {
SDL_Rect clip = {w_off, h_off, 0, 0};
SDL_BlitSurface(background, NULL, screen, &clip);
}
}
}
void display::draw_text_in_hex(const map_location& loc,
const tdrawing_layer layer, const std::string& text,
size_t font_size, SDL_Color color, double x_in_hex, double y_in_hex)
{
if (text.empty()) return;
const size_t font_sz = static_cast<size_t>(font_size * get_zoom_factor()
#ifdef USE_TINY_GUI
/ 2 // the hex is only half size
#endif
);
surface text_surf = font::get_rendered_text(text, font_sz, color);
surface back_surf = font::get_rendered_text(text, font_sz, font::BLACK_COLOUR);
const int x = get_location_x(loc) - text_surf->w/2
+ static_cast<int>(x_in_hex* hex_size());
const int y = get_location_y(loc) - text_surf->h/2
+ static_cast<int>(y_in_hex* hex_size());
for (int dy=-1; dy <= 1; dy++) {
for (int dx=-1; dx <= 1; dx++) {
if (dx!=0 || dy!=0) {
drawing_buffer_add(layer, loc, tblit(x + dx, y + dy, back_surf));
}
}
}
drawing_buffer_add(layer, loc, tblit(x, y, text_surf));
}
void display::render_unit_image(int x, int y,const display::tdrawing_layer drawing_layer,
const map_location& loc, surface image,
bool hreverse, bool greyscale, fixed_t alpha,
Uint32 blendto, double blend_ratio, double submerged,bool vreverse)
{
if (image==NULL)
return;
SDL_Rect image_rect = {x, y, image->w, image->h};
SDL_Rect clip_rect = map_area();
if (!rects_overlap(image_rect, clip_rect))
return;
surface surf(image);
if(hreverse) {
surf = image::reverse_image(surf);
}
if(vreverse) {
surf = flop_surface(surf, false);
}
if(greyscale) {
surf = greyscale_image(surf, false);
}
if(blend_ratio != 0) {
surf = blend_surface(surf, blend_ratio, blendto, false);
}
if(alpha > ftofxp(1.0)) {
surf = brighten_image(surf, alpha, false);
//} else if(alpha != 1.0 && blendto != 0) {
// surf.assign(blend_surface(surf,1.0-alpha,blendto));
} else if(alpha != ftofxp(1.0)) {
surf = adjust_surface_alpha(surf, alpha, false);
}
if(surf == NULL) {
ERR_DP << "surface lost...\n";
return;
}
const int submerge_height = std::min<int>(surf->h,std::max<int>(0,int(surf->h*(1.0-submerged))));
SDL_Rect srcrect = {0,0,surf->w,submerge_height};
drawing_buffer_add(drawing_layer, loc, tblit(x, y, surf, srcrect));
if(submerge_height != surf->h) {
surf.assign(adjust_surface_alpha(surf,ftofxp(0.2),false));
srcrect.y = submerge_height;
srcrect.h = surf->h-submerge_height;
y += submerge_height;
drawing_buffer_add(drawing_layer, loc, tblit(x, y, surf, srcrect));
}
}
void display::select_hex(map_location hex)
{
invalidate(selectedHex_);
selectedHex_ = hex;
invalidate(selectedHex_);
}
void display::highlight_hex(map_location hex)
{
invalidate(mouseoverHex_);
mouseoverHex_ = hex;
invalidate(mouseoverHex_);
}
void display::set_diagnostic(const std::string& msg)
{
if(diagnostic_label_ != 0) {
font::remove_floating_label(diagnostic_label_);
diagnostic_label_ = 0;
}
if(msg != "") {
diagnostic_label_ = font::add_floating_label(msg,font::SIZE_PLUS,font::YELLOW_COLOUR,300.0,50.0,0.0,0.0,-1,map_outside_area());
}
}
bool display::draw_init()
{
bool changed = false;
if (get_map().empty()) {
return changed;
}
if(benchmark) {
redraw_background_ = true;
invalidateAll_ = true;
}
if(!panelsDrawn_) {
draw_all_panels();
panelsDrawn_ = true;
changed = true;
}
if(redraw_background_) {
// Full redraw of the background
const SDL_Rect clip_rect = map_outside_area();
const surface screen = get_screen_surface();
clip_rect_setter set_clip_rect(screen, clip_rect);
draw_background(screen, clip_rect, theme_.border().background_image);
update_rect(clip_rect);
redraw_background_ = false;
// Force a full map redraw
invalidateAll_ = true;
}
if(invalidateAll_) {
DBG_DP << "draw() with invalidateAll\n";
// toggle invalidateAll_ first to allow regular invalidations
invalidateAll_ = false;
invalidate_locations_in_rect(map_area());
redrawMinimap_ = true;
}
return changed;
}
void display::draw_wrap(bool update,bool force,bool changed)
{
static const int time_between_draws = preferences::draw_delay();
const int current_time = SDL_GetTicks();
const int wait_time = nextDraw_ - current_time;
if(redrawMinimap_) {
redrawMinimap_ = false;
draw_minimap();
changed = true;
}
if(update) {
if(force || changed) {
if(!force && wait_time > 0) {
// If it's not time yet to draw, delay until it is
SDL_Delay(wait_time);
}
update_display();
}
// Set the theortical next draw time
nextDraw_ += time_between_draws;
// If the next draw already should have been finished,
// we'll enter an update frenzy, so make sure that the
// too late value doesn't keep growing.
// Note: if force is used too often,
// we can also get the opposite effect.
nextDraw_ = std::max<int>(nextDraw_, SDL_GetTicks());
}
}
void display::delay(unsigned int milliseconds) const
{
if (!game_config::no_delay)
SDL_Delay(milliseconds);
}
const theme::menu* display::menu_pressed()
{
for(std::vector<gui::button>::iterator i = buttons_.begin(); i != buttons_.end(); ++i) {
if(i->pressed()) {
const size_t index = i - buttons_.begin();
if(index >= theme_.menus().size()) {
assert(false);
return 0;
}
return &theme_.menus()[index];
}
}
return 0;
}
void display::enable_menu(const std::string& item, bool enable)
{
for(std::vector<theme::menu>::const_iterator menu = theme_.menus().begin();
menu != theme_.menus().end(); ++menu) {
std::vector<std::string>::const_iterator hasitem =
std::find(menu->items().begin(), menu->items().end(), item);
if(hasitem != menu->items().end()) {
const size_t index = menu - theme_.menus().begin();
if(index >= buttons_.size()) {
assert(false);
return;
}
buttons_[index].enable(enable);
}
}
}
void display::announce(const std::string& message, const SDL_Color& colour)
{
font::add_floating_label(message,
font::SIZE_XLARGE,
colour,
map_outside_area().w/2,
map_outside_area().h/3,
0.0,0.0,100,
map_outside_area(),
font::CENTER_ALIGN);
}
void display::draw_border(const map_location& loc, const int xpos, const int ypos)
{
/**
* at the moment the border must be between 0.0 and 0.5
* and the image should always be prepared for a 0.5 border.
* This way this code doesn't need modifications for other border sizes.
*/
// First handle the corners :
if(loc.x == -1 && loc.y == -1) { // top left corner
drawing_buffer_add(LAYER_BORDER, loc, tblit(xpos + zoom_/4, ypos,
image::get_image(theme_.border().corner_image_top_left, image::SCALED_TO_ZOOM)));
} else if(loc.x == get_map().w() && loc.y == -1) { // top right corner
// We use the map idea of odd and even, and map coords are internal coords + 1
if(loc.x%2 == 0) {
drawing_buffer_add(LAYER_BORDER, loc, tblit(xpos, ypos + zoom_/2,
image::get_image(theme_.border().corner_image_top_right_odd, image::SCALED_TO_ZOOM)));
} else {
drawing_buffer_add(LAYER_BORDER, loc, tblit(xpos, ypos,
image::get_image(theme_.border().corner_image_top_right_even, image::SCALED_TO_ZOOM)));
}
} else if(loc.x == -1 && loc.y == get_map().h()) { // bottom left corner
drawing_buffer_add(LAYER_BORDER, loc, tblit(xpos + zoom_/4, ypos,
image::get_image(theme_.border().corner_image_bottom_left, image::SCALED_TO_ZOOM)));
} else if(loc.x == get_map().w() && loc.y == get_map().h()) { // bottom right corner
// We use the map idea of odd and even, and map coords are internal coords + 1
if(loc.x%2 == 1) {
drawing_buffer_add(LAYER_BORDER, loc, tblit(xpos, ypos,
image::get_image(theme_.border().corner_image_bottom_right_even, image::SCALED_TO_ZOOM)));
} else {
drawing_buffer_add(LAYER_BORDER, loc, tblit(xpos, ypos,
image::get_image(theme_.border().corner_image_bottom_right_odd, image::SCALED_TO_ZOOM)));
}
// Now handle the sides:
} else if(loc.x == -1) { // left side
drawing_buffer_add(LAYER_BORDER, loc, tblit(xpos + zoom_/4, ypos,
image::get_image(theme_.border().border_image_left, image::SCALED_TO_ZOOM)));
} else if(loc.x == get_map().w()) { // right side
drawing_buffer_add(LAYER_BORDER, loc, tblit(xpos + zoom_/4, ypos,
image::get_image(theme_.border().border_image_right, image::SCALED_TO_ZOOM)));
} else if(loc.y == -1) { // top side
// We use the map idea of odd and even, and map coords are internal coords + 1
if(loc.x%2 == 1) {
drawing_buffer_add(LAYER_BORDER, loc, tblit(xpos, ypos,
image::get_image(theme_.border().border_image_top_even, image::SCALED_TO_ZOOM)));
} else {
drawing_buffer_add(LAYER_BORDER, loc, tblit(xpos, ypos + zoom_/2,
image::get_image(theme_.border().border_image_top_odd, image::SCALED_TO_ZOOM)));
}
} else if(loc.y == get_map().h()) { // bottom side
// We use the map idea of odd and even, and map coords are internal coords + 1
if(loc.x%2 == 1) {
drawing_buffer_add(LAYER_BORDER, loc, tblit(xpos, ypos,
image::get_image(theme_.border().border_image_bottom_even, image::SCALED_TO_ZOOM)));
} else {
drawing_buffer_add(LAYER_BORDER, loc, tblit(xpos, ypos + zoom_/2,
image::get_image(theme_.border().border_image_bottom_odd, image::SCALED_TO_ZOOM)));
}
}
}
void display::draw_minimap()
{
const SDL_Rect& area = minimap_area();
if(minimap_ == NULL || minimap_->w > area.w || minimap_->h > area.h) {
minimap_ = image::getMinimap(area.w, area.h, get_map(), viewpoint_);
if(minimap_ == NULL) {
return;
}
}
const surface screen(screen_.getSurface());
clip_rect_setter clip_setter(screen, area);
SDL_Color back_color = {31,31,23,255};
draw_centered_on_background(minimap_, area, back_color, screen);
//update the minimap location for mouse and units functions
minimap_location_.x = area.x + (area.w - minimap_->w) / 2;
minimap_location_.y = area.y + (area.h - minimap_->h) / 2;
minimap_location_.w = minimap_->w;
minimap_location_.h = minimap_->h;
draw_minimap_units();
// calculate the visible portion of the map:
// scaling between minimap and full map images
double xscaling = 1.0*minimap_->w / (get_map().w()*hex_width());
double yscaling = 1.0*minimap_->h / (get_map().h()*hex_size());
// we need to shift with the border size
// and the 0.25 from the minimap balanced drawing
// and the possible difference between real map and outside off-map
SDL_Rect map_rect = map_area();
SDL_Rect map_out_rect = map_outside_area();
double border = theme_.border().size;
double shift_x = - border*hex_width() - (map_out_rect.w - map_rect.w) / 2;
double shift_y = - (border+0.25)*hex_size() - (map_out_rect.h - map_rect.h) / 2;
int view_x = static_cast<int>((xpos_ + shift_x) * xscaling);
int view_y = static_cast<int>((ypos_ + shift_y) * yscaling);
int view_w = static_cast<int>(map_out_rect.w * xscaling);
int view_h = static_cast<int>(map_out_rect.h * yscaling);
const Uint32 box_color = SDL_MapRGB(minimap_->format,0xFF,0xFF,0xFF);
draw_rectangle(minimap_location_.x + view_x - 1,
minimap_location_.y + view_y - 1,
view_w + 2, view_h + 2,
box_color, screen);
}
bool display::scroll(int xmove, int ymove)
{
const int orig_x = xpos_;
const int orig_y = ypos_;
xpos_ += xmove;
ypos_ += ymove;
bounds_check_position();
const int dx = orig_x - xpos_; // dx = -xmove
const int dy = orig_y - ypos_; // dy = -ymove
// Only invalidate if we've actually moved
if(dx == 0 && dy == 0)
return false;
labels().scroll(dx, dy);
font::scroll_floating_labels(dx, dy);
surface screen(screen_.getSurface());
SDL_Rect dstrect = map_area();
dstrect.x += dx;
dstrect.y += dy;
dstrect = intersect_rects(dstrect, map_area());
SDL_Rect srcrect = dstrect;
srcrect.x -= dx;
srcrect.y -= dy;
if (!screen_.update_locked())
SDL_BlitSurface(screen,&srcrect,screen,&dstrect);
//This is necessary to avoid a crash in some SDL versions on some systems
//see http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=462794
//FIXME remove this once the latest stable SDL release doesn't crash as 1.2.13 does
#ifdef _MSC_VER
__asm{cld};
#elif defined(__GNUG__) && (defined(__i386__) || defined(__x86_64__))
asm("cld");
#endif
// Invalidate locations in the newly visible rects
if (dy != 0) {
SDL_Rect r = map_area();
if(dy < 0)
r.y = r.y + r.h + dy;
r.h = abs(dy);
invalidate_locations_in_rect(r);
}
if (dx != 0) {
SDL_Rect r = map_area();
if (dx < 0)
r.x = r.x + r.w + dx;
r.w = abs(dx);
invalidate_locations_in_rect(r);
}
scroll_event_.notify_observers();
update_rect(map_area());
redrawMinimap_ = true;
return true;
}
void display::set_zoom(int amount)
{
int new_zoom = zoom_ + amount;
if (new_zoom < MinZoom) {
new_zoom = MinZoom;
}
if (new_zoom > MaxZoom) {
new_zoom = MaxZoom;
}
if (new_zoom != zoom_) {
SDL_Rect const &area = map_area();
xpos_ += (xpos_ + area.w / 2) * amount / zoom_;
ypos_ += (ypos_ + area.h / 2) * amount / zoom_;
zoom_ = new_zoom;
bounds_check_position();
if (zoom_ != DefaultZoom) {
last_zoom_ = zoom_;
}
image::set_zoom(zoom_);
labels().recalculate_labels();
redraw_background_ = true;
invalidate_all();
// Forces a redraw after zooming.
// This prevents some graphic glitches from occurring.
draw();
}
}
void display::set_default_zoom()
{
if (zoom_ != DefaultZoom) {
last_zoom_ = zoom_;
set_zoom(DefaultZoom - zoom_ );
} else {
// When we are already at the default zoom,
// switch to the last zoom used
set_zoom(last_zoom_ - zoom_);
}
}
bool display::tile_fully_on_screen(const map_location& loc)
{
int x = get_location_x(loc);
int y = get_location_y(loc);
return !outside_area(map_area(), x, y);
}
bool display::tile_nearly_on_screen(const map_location& loc) const
{
int x = get_location_x(loc);
int y = get_location_y(loc);
const SDL_Rect &area = map_area();
int hw = hex_width(), hs = hex_size();
return x + hs >= area.x - hw && x < area.x + area.w + hw &&
y + hs >= area.y - hs && y < area.y + area.h + hs;
}
void display::scroll_to_xy(int screenxpos, int screenypos, SCROLL_TYPE scroll_type, bool force)
{
if(!force && !preferences::scroll_to_action()) return;
if(screen_.update_locked()) {
return;
}
const SDL_Rect area = map_area();
const int xmove_expected = screenxpos - (area.x + area.w/2);
const int ymove_expected = screenypos - (area.y + area.h/2);
int xpos = xpos_ + xmove_expected;
int ypos = ypos_ + ymove_expected;
bounds_check_position(xpos, ypos);
int xmove = xpos - xpos_;
int ymove = ypos - ypos_;
if(scroll_type == WARP || turbo_speed() > 2.0 || preferences::scroll_speed() > 99) {
scroll(xmove,ymove);
draw();
return;
}
// Doing an animated scroll, with acceleration etc.
int x_old = 0;
int y_old = 0;
const double dist_total = hypot(xmove, ymove);
double dist_moved = 0.0;
int t_prev = SDL_GetTicks();
double velocity = 0.0;
while (dist_moved < dist_total) {
events::pump();
int t = SDL_GetTicks();
double dt = (t - t_prev) / 1000.0;
if (dt > 0.200) {
// Do not skip too many frames on slow PCs
dt = 0.200;
}
t_prev = t;
//std::cout << t << " " << hypot(x_old, y_old) << "\n";
/** @todo Those values might need some fine-tuning: */
const double accel_time = 0.3 / turbo_speed(); // seconds until full speed is reached
const double decel_time = 0.4 / turbo_speed(); // seconds from full speed to stop
double velocity_max = preferences::scroll_speed() * 60.0;
velocity_max *= turbo_speed();
double accel = velocity_max / accel_time;
double decel = velocity_max / decel_time;
// If we started to decelerate now, where would we stop?
double stop_time = velocity / decel;
double dist_stop = dist_moved + velocity*stop_time - 0.5*decel*stop_time*stop_time;
if (dist_stop > dist_total || velocity > velocity_max) {
velocity -= decel * dt;
if (velocity < 1.0) velocity = 1.0;
} else {
velocity += accel * dt;
if (velocity > velocity_max) velocity = velocity_max;
}
dist_moved += velocity * dt;
if (dist_moved > dist_total) dist_moved = dist_total;
int x_new = round_double(xmove * dist_moved / dist_total);
int y_new = round_double(ymove * dist_moved / dist_total);
int dx = x_new - x_old;
int dy = y_new - y_old;
scroll(dx,dy);
x_old += dx;
y_old += dy;
draw();
}
}
void display::scroll_to_tile(const map_location& loc, SCROLL_TYPE scroll_type, bool check_fogged, bool force)
{
if(get_map().on_board(loc) == false) {
ERR_DP << "Tile at " << loc << " isn't on the map, can't scroll to the tile.\n";
return;
}
std::vector<map_location> locs;
locs.push_back(loc);
scroll_to_tiles(locs, scroll_type, check_fogged,false,0.0,force);
}
void display::scroll_to_tiles(map_location loc1, map_location loc2,
SCROLL_TYPE scroll_type, bool check_fogged,
double add_spacing, bool force)
{
std::vector<map_location> locs;
locs.push_back(loc1);
locs.push_back(loc2);
scroll_to_tiles(locs, scroll_type, check_fogged, false, add_spacing,force);
}
void display::scroll_to_tiles(const std::vector<map_location>& locs,
SCROLL_TYPE scroll_type, bool check_fogged,
bool only_if_possible, double add_spacing, bool force)
{
// basically we calculate the min/max coordinates we want to have on-screen
int minx = 0;
int maxx = 0;
int miny = 0;
int maxy = 0;
bool valid = false;
bool first_tile_on_screen = false;
for(std::vector<map_location>::const_iterator itor = locs.begin(); itor != locs.end() ; itor++) {
if(get_map().on_board(*itor) == false) continue;
if(check_fogged && fogged(*itor)) continue;
int x = get_location_x(*itor);
int y = get_location_y(*itor);
if (!valid) {
minx = x;
maxx = x;
miny = y;
maxy = y;
valid = true;
first_tile_on_screen = !outside_area(map_area(), x, y);
} else {
int minx_new = std::min<int>(minx,x);
int miny_new = std::min<int>(miny,y);
int maxx_new = std::max<int>(maxx,x);
int maxy_new = std::max<int>(maxy,y);
SDL_Rect r = map_area();
r.x = minx_new;
r.y = miny_new;
if(outside_area(r, maxx_new, maxy_new)) {
// we cannot fit all locations to the screen
if (only_if_possible) return;
break;
}
minx = minx_new;
miny = miny_new;
maxx = maxx_new;
maxy = maxy_new;
}
}
//if everything is fogged or the locs list is empty
if(!valid) return;
if (scroll_type == ONSCREEN) {
SDL_Rect r = map_area();
int spacing = round_double(add_spacing*hex_size());
r.x += spacing;
r.y += spacing;
r.w -= 2*spacing;
r.h -= 2*spacing;
if (!outside_area(r, minx,miny) && !outside_area(r, maxx,maxy)) {
return;
}
}
// let's do "normal" rectangle math from now on
SDL_Rect locs_bbox;
locs_bbox.x = minx;
locs_bbox.y = miny;
locs_bbox.w = maxx - minx + hex_size();
locs_bbox.h = maxy - miny + hex_size();
// target the center
int target_x = locs_bbox.x + locs_bbox.w/2;
int target_y = locs_bbox.y + locs_bbox.h/2;
if (scroll_type == ONSCREEN) {
// when doing an ONSCREEN scroll we do not center the target unless needed
SDL_Rect r = map_area();
int map_center_x = r.x + r.w/2;
int map_center_y = r.y + r.h/2;
int h = r.h;
int w = r.w;
// we do not want to be only inside the screen rect, but center a bit more
double inside_frac = 0.5; // 0.0 = always center the target, 1.0 = scroll the minimum distance
w = static_cast<int>(w * inside_frac);
h = static_cast<int>(h * inside_frac);
// shrink the rectangle by the size of the locations rectangle we found
// such that the new task to fit a point into a rectangle instead of rectangle into rectangle
w -= locs_bbox.w;
h -= locs_bbox.h;
if (w < 1) w = 1;
if (h < 1) h = 1;
r.x = target_x - w/2;
r.y = target_y - h/2;
r.w = w;
r.h = h;
// now any point within r is a possible target to scroll to
// we take the one with the minimum distance to map_center
// which will always be at the border of r
if (map_center_x < r.x) {
target_x = r.x;
target_y = map_center_y;
if (target_y < r.y) target_y = r.y;
if (target_y > r.y+r.h-1) target_y = r.y+r.h-1;
} else if (map_center_x > r.x+r.w-1) {
target_x = r.x+r.w-1;
target_y = map_center_y;
if (target_y < r.y) target_y = r.y;
if (target_y >= r.y+r.h) target_y = r.y+r.h-1;
} else if (map_center_y < r.y) {
target_y = r.y;
target_x = map_center_x;
if (target_x < r.x) target_x = r.x;
if (target_x > r.x+r.w-1) target_x = r.x+r.w-1;
} else if (map_center_y > r.y+r.h-1) {
target_y = r.y+r.h-1;
target_x = map_center_x;
if (target_x < r.x) target_x = r.x;
if (target_x > r.x+r.w-1) target_x = r.x+r.w-1;
} else {
ERR_DP << "Bug in the scrolling code? Looks like we would not need to scroll after all...\n";
// keep the target at the center
}
}
scroll_to_xy(target_x, target_y,scroll_type,force);
}
void display::bounds_check_position()
{
const int orig_zoom = zoom_;
if(zoom_ < MinZoom) {
zoom_ = MinZoom;
}
if(zoom_ > MaxZoom) {
zoom_ = MaxZoom;
}
bounds_check_position(xpos_, ypos_);
if(zoom_ != orig_zoom) {
image::set_zoom(zoom_);
}
}
void display::bounds_check_position(int& xpos, int& ypos)
{
const int tile_width = hex_width();
// Adjust for the border 2 times
const int xend = static_cast<int>(tile_width * (get_map().w() + 2 * theme_.border().size) + tile_width/3);
const int yend = static_cast<int>(zoom_ * (get_map().h() + 2 * theme_.border().size) + zoom_/2);
if(xpos > xend - map_area().w) {
xpos = xend - map_area().w;
}
if(ypos > yend - map_area().h) {
ypos = yend - map_area().h;
}
if(xpos < 0) {
xpos = 0;
}
if(ypos < 0) {
ypos = 0;
}
}
double display::turbo_speed() const
{
bool res = turbo_;
if(keys_[SDLK_LSHIFT] || keys_[SDLK_RSHIFT]) {
res = !res;
}
res |= screen_.faked();
if (res)
return turbo_speed_;
else
return 1.0;
}
void display::set_idle_anim_rate(int rate)
{
idle_anim_rate_ = std::pow(2.0, -rate/10.0);
}
void display::redraw_everything()
{
if(screen_.update_locked())
return;
invalidateGameStatus_ = true;
for(size_t n = 0; n != reports::NUM_REPORTS; ++n) {
reportRects_[n] = empty_rect;
reportSurfaces_[n].assign(NULL);
reports_[n] = reports::report();
}
bounds_check_position();
tooltips::clear_tooltips();
theme_.set_resolution(screen_area());
if(buttons_.empty() == false) {
create_buttons();
}
panelsDrawn_ = false;
labels().recalculate_labels();
redraw_background_ = true;
int ticks1 = SDL_GetTicks();
invalidate_all();
int ticks2 = SDL_GetTicks();
draw(true,true);
int ticks3 = SDL_GetTicks();
LOG_DP << "invalidate and draw: " << (ticks3 - ticks2) << " and " << (ticks2 - ticks1) << "\n";
foreach (boost::function<void(display&)> f, redraw_observers_) {
f(*this);
}
}
void display::add_redraw_observer(boost::function<void(display&)> f)
{
redraw_observers_.push_back(f);
}
void display::clear_redraw_observers()
{
redraw_observers_.clear();
}
void display::draw(bool update,bool force) {
// log_scope("display::draw");
if (screen_.update_locked()) {
return;
}
bool changed = draw_init();
pre_draw();
// invalidate all that needs to be invalidated
invalidate_animations();
update_time_of_day();
// at this stage we have everything that needs to be invalidated for this redraw
// save it as the previous invalidated, and merge with the previous invalidated_
// we merge with the previous redraw because if a hex had a unit last redraw but
// not this one, nobody will tell us to redraw (cleanup)
previous_invalidated_.swap(invalidated_);
invalidated_.insert(previous_invalidated_.begin(),previous_invalidated_.end());
// these new invalidations can not cause any propagation because
// if a hex was invalidated last turn but not this turn, then
// * case of no unit in neighbour hex=> no propagation
// * case of unit in hex but was there last turn=>its hexes are invalidated too
// * case of unit inhex not there last turn => it moved, so was invalidated previously
if(!get_map().empty()) {
//int simulate_delay = 0;
/*
* draw_invalidated() also invalidates the halos, so also needs to be
* ran if invalidated_.empty() == true.
*/
if(!invalidated_.empty() || preferences::show_haloes()) {
changed = true;
draw_invalidated();
invalidated_.clear();
}
drawing_buffer_commit();
post_commit();
draw_sidebar();
/** @todo FIXME: This changed can probably be smarter */
changed = true;
// Simulate slow PC:
//SDL_Delay(2*simulate_delay + rand() % 20);
}
draw_wrap(update, force, changed);
}
map_labels& display::labels()
{
return *map_labels_;
}
const map_labels& display::labels() const
{
return *map_labels_;
}
void display::clear_screen()
{
surface const disp(screen_.getSurface());
SDL_Rect area = screen_area();
SDL_FillRect(disp, &area, SDL_MapRGB(disp->format, 0, 0, 0));
}
const SDL_Rect& display::get_clip_rect()
{
return map_area();
}
void display::draw_invalidated() {
// log_scope("display::draw_invalidated");
SDL_Rect clip_rect = get_clip_rect();
surface screen = get_screen_surface();
clip_rect_setter set_clip_rect(screen, clip_rect);
foreach (map_location loc, invalidated_) {
int xpos = get_location_x(loc);
int ypos = get_location_y(loc);
const bool on_map = get_map().on_board(loc);
const bool off_map_tile = (get_map().get_terrain(loc) == t_translation::OFF_MAP_USER);
SDL_Rect hex_rect = {xpos, ypos, zoom_, zoom_};
if(!rects_overlap(hex_rect,clip_rect)) {
continue;
}
draw_hex(loc);
drawn_hexes_+=1;
// If the tile is at the border, we start to blend it
if(!on_map && !off_map_tile) {
draw_border(loc, xpos, ypos);
}
}
invalidated_hexes_ += invalidated_.size();
}
void display::draw_hex(const map_location& loc) {
int xpos = get_location_x(loc);
int ypos = get_location_y(loc);
image::TYPE image_type = get_image_type(loc);
const bool on_map = get_map().on_board(loc);
const bool off_map_tile = (get_map().get_terrain(loc) == t_translation::OFF_MAP_USER);
if(!shrouded(loc)) {
// unshrouded terrain (the normal case)
drawing_buffer_add(LAYER_TERRAIN_BG, loc, tblit(xpos, ypos,
get_terrain_images(loc,tod_.id, image_type, ADJACENT_BACKGROUND)));
drawing_buffer_add(LAYER_TERRAIN_FG, loc, tblit(xpos, ypos,
get_terrain_images(loc,tod_.id,image_type,ADJACENT_FOREGROUND)));
// Draw the grid, if that's been enabled
if(grid_ && on_map && !off_map_tile) {
drawing_buffer_add(LAYER_TERRAIN_TMP_BG, loc, tblit(xpos, ypos,
image::get_image(game_config::grid_image, image::SCALED_TO_HEX)));
}
}
// Paint selection and mouseover overlays
if(loc == selectedHex_ && on_map && selected_hex_overlay_ != NULL) {
drawing_buffer_add(LAYER_TERRAIN_TMP_BG, loc, tblit(xpos, ypos, selected_hex_overlay_));
}
if(loc == mouseoverHex_ && (on_map || (in_editor() && get_map().on_board_with_border(loc))) && mouseover_hex_overlay_ != NULL) {
drawing_buffer_add(LAYER_TERRAIN_TMP_BG, loc, tblit(xpos, ypos, mouseover_hex_overlay_));
}
// Apply shroud, fog and linger overlay
if(shrouded(loc)) {
// We apply void also on off-map tiles
// to shroud the half-hexes too
drawing_buffer_add(LAYER_FOG_SHROUD, loc, tblit(xpos, ypos,
image::get_image(shroud_image_, image_type)));
} else if(fogged(loc)) {
drawing_buffer_add(LAYER_FOG_SHROUD, loc, tblit(xpos, ypos,
image::get_image(fog_image_, image_type)));
}
if(!shrouded(loc)) {
drawing_buffer_add(LAYER_FOG_SHROUD, loc, tblit(xpos, ypos,
get_terrain_images(loc, tod_.id, image_type, ADJACENT_FOGSHROUD)));
}
if (on_map) {
if (draw_coordinates_) {
int off_x = xpos + hex_size()/2;
int off_y = ypos + hex_size()/2;
surface text = font::get_rendered_text(lexical_cast<std::string>(loc), font::SIZE_SMALL, font::NORMAL_COLOUR);
surface bg = create_neutral_surface(text->w, text->h);
SDL_Rect bg_rect = {0, 0, text->w, text->h};
SDL_FillRect(bg, &bg_rect, 0xaa000000);
off_x -= text->w / 2;
if (draw_terrain_codes_) {
off_y -= text->h;
} else {
off_y -= text->h / 2;
}
drawing_buffer_add(LAYER_FOG_SHROUD, loc, tblit(off_x, off_y, bg));
drawing_buffer_add(LAYER_FOG_SHROUD, loc, tblit(off_x, off_y, text));
}
if (draw_terrain_codes_ && (game_config::debug || !shrouded(loc))) {
int off_x = xpos + hex_size()/2;
int off_y = ypos + hex_size()/2;
surface text = font::get_rendered_text(lexical_cast<std::string>(get_map().get_terrain(loc)), font::SIZE_SMALL, font::NORMAL_COLOUR);
surface bg = create_neutral_surface(text->w, text->h);
SDL_Rect bg_rect = {0, 0, text->w, text->h};
SDL_FillRect(bg, &bg_rect, 0xaa000000);
off_x -= text->w / 2;
if (!draw_coordinates_) {
off_y -= text->h / 2;
}
drawing_buffer_add(LAYER_FOG_SHROUD, loc, tblit(off_x, off_y, bg));
drawing_buffer_add(LAYER_FOG_SHROUD, loc, tblit(off_x, off_y, text));
}
}
}
image::TYPE display::get_image_type(const map_location& /*loc*/) {
return image::SCALED_TO_HEX;
}
void display::update_time_of_day() {
//no action
}
void display::draw_sidebar() {
}
void display::draw_image_for_report(surface& img, SDL_Rect& rect)
{
SDL_Rect visible_area = get_non_transparent_portion(img);
SDL_Rect target = rect;
if(visible_area.x != 0 || visible_area.y != 0 || visible_area.w != img->w || visible_area.h != img->h) {
if(visible_area.w == 0 || visible_area.h == 0) {
return;
}
if(visible_area.w > rect.w || visible_area.h > rect.h) {
img.assign(get_surface_portion(img,visible_area,false));
img.assign(scale_surface(img,rect.w,rect.h));
visible_area.x = 0;
visible_area.y = 0;
visible_area.w = img->w;
visible_area.h = img->h;
} else {
target.x = rect.x + (rect.w - visible_area.w)/2;
target.y = rect.y + (rect.h - visible_area.h)/2;
target.w = visible_area.w;
target.h = visible_area.h;
}
SDL_BlitSurface(img,&visible_area,screen_.getSurface(),&target);
} else {
if(img->w != rect.w || img->h != rect.h) {
img.assign(scale_surface(img,rect.w,rect.h));
}
SDL_BlitSurface(img,NULL,screen_.getSurface(),&target);
}
}
void display:: set_report_content(const reports::TYPE which_report, const std::string &content) {
report_[which_report] = content;
}
void display::refresh_report(reports::TYPE report_num, reports::report report,
bool brighten)
{
const theme::status_item* const item = theme_.get_status_item(reports::report_name(report_num));
if (!item) {
reportSurfaces_[report_num].assign(NULL);
return;
}
SDL_Rect &rect = reportRects_[report_num];
const SDL_Rect &new_rect = item->location(screen_area());
// Report and its location is unchanged since last time. Do nothing.
if (rect == new_rect && reports_[report_num] == report) {
return;
}
reports_[report_num] = report;
surface &surf = reportSurfaces_[report_num];
if (surf) {
SDL_BlitSurface(surf, NULL, screen_.getSurface(), &rect);
update_rect(rect);
}
// If the rectangle has just changed, assign the surface to it
if (new_rect != rect || !surf)
{
surf.assign(NULL);
rect = new_rect;
// If the rectangle is present, and we are blitting text,
// then we need to backup the surface.
// (Images generally won't need backing up,
// unless they are transperant, but that is done later).
if (rect.w > 0 && rect.h > 0) {
surf.assign(get_surface_portion(screen_.getSurface(), rect));
if (reportSurfaces_[report_num] == NULL) {
ERR_DP << "Could not backup background for report!\n";
}
}
update_rect(rect);
}
tooltips::clear_tooltips(rect);
if (report.empty()) return;
int x = rect.x, y = rect.y;
// Add prefix, postfix elements.
// Make sure that they get the same tooltip
// as the guys around them.
std::string str = item->prefix();
if (!str.empty()) {
report.insert(report.begin(), reports::element(str, "", report.begin()->tooltip));
}
str = item->postfix();
if (!str.empty()) {
report.push_back(reports::element(str, "", report.back().tooltip));
}
// Loop through and display each report element.
int tallest = 0;
int image_count = 0;
bool used_ellipsis = false;
std::ostringstream ellipsis_tooltip;
SDL_Rect ellipsis_area = rect;
foreach (const reports::element &e, report)
{
SDL_Rect area = { x, y, rect.w + rect.x - x, rect.h + rect.y - y };
if (area.h <= 0) break;
if (!e.text.empty())
{
if (used_ellipsis) goto skip_element;
// Draw a text element.
font::ttext text;
if (item->font_rgb_set()) {
text.set_foreground_colour(item->font_rgb());
}
std::string t = e.text;
bool eol = false;
if (t[t.size() - 1] == '\n') {
eol = true;
t = t.substr(0, t.size() - 1);
}
text.set_font_size(item->font_size());
text.set_text(t, true);
text.set_maximum_width(area.w);
text.set_maximum_height(area.h, false);
surface s = text.render();
screen_.blit_surface(x, y, s);
area.w = s->w;
area.h = s->h;
if (area.h > tallest) {
tallest = area.h;
}
if (eol) {
x = rect.x;
y += tallest;
tallest = 0;
} else {
x += area.w;
}
}
else if (!e.image.get_filename().empty())
{
if (used_ellipsis) goto skip_element;
// Draw an image element.
surface img(image::get_image(e.image));
if (!img) {
ERR_DP << "could not find image for report: '" << e.image.get_filename() << "'\n";
continue;
}
if (area.w < img->w && image_count) {
// We have more than one image, and this one doesn't fit.
img = surface(image::get_image(game_config::ellipsis_image));
used_ellipsis = true;
}
if (img->w < area.w) area.w = img->w;
if (img->h < area.h) area.h = img->h;
draw_image_for_report(img, area);
if (brighten) {
surface tod_bright(image::get_image(game_config:: tod_bright_image));
if (tod_bright)
draw_image_for_report(tod_bright, area);
}
++image_count;
if (area.h > tallest) {
tallest = area.h;
}
if (!used_ellipsis) {
x += area.w;
} else {
ellipsis_area = area;
}
}
else
{
// No text nor image, skip this element
continue;
}
skip_element:
if (!e.tooltip.empty()) {
if (!used_ellipsis) {
tooltips::add_tooltip(area, e.tooltip);
} else {
// Collect all tooltips for the ellipsis.
ellipsis_tooltip << e.tooltip << '\n';
}
}
}
if (used_ellipsis) {
tooltips::add_tooltip(ellipsis_area, ellipsis_tooltip.str());
}
}
void display::invalidate_all()
{
DBG_DP << "invalidate_all()\n";
invalidateAll_ = true;
invalidated_.clear();
update_rect(map_area());
}
bool display::invalidate(const map_location& loc)
{
if(invalidateAll_)
return false;
return invalidated_.insert(loc).second;
}
bool display::invalidate(const std::set<map_location>& locs)
{
if(invalidateAll_)
return false;
bool ret = false;
foreach (const map_location& loc, locs) {
ret = invalidated_.insert(loc).second || ret;
}
return ret;
}
bool display::propagate_invalidation(const std::set<map_location>& locs)
{
if(invalidateAll_)
return false;
if(locs.size()<=1)
return false; // propagation never needed
// search the first hex invalidated (if any)
std::set<map_location>::const_iterator i = locs.begin();
for(; i != locs.end() && invalidated_.count(*i) == 0 ; ++i) {}
if (i == locs.end())
return false; // no invalidation, don't propagate
// propagate invalidation
// 'i' is already in, but I suspect that splitting the range is bad
// especially because locs are often adjacents
size_t previous_size = invalidated_.size();
invalidated_.insert(locs.begin(), locs.end());
return previous_size < invalidated_.size();
}
bool display::invalidate_visible_locations_in_rect(const SDL_Rect& rect)
{
return invalidate_locations_in_rect(intersect_rects(map_area(),rect));
}
bool display::invalidate_locations_in_rect(const SDL_Rect& rect)
{
if(invalidateAll_)
return false;
bool result = false;
foreach (const map_location &loc, hexes_under_rect(rect)) {
result |= invalidate(loc);
}
return result;
}
bool display::rectangle_need_update(const SDL_Rect& rect) const
{
SDL_Rect visible_rect = intersect_rects(map_area(), rect);
// check if rectangle is visible
if(visible_rect.w <=0 || visible_rect.h <=0)
return false;
// invalidateAll_ is about visible hexes, so only check if visible
if(invalidateAll_)
return true;
foreach (const map_location &loc, hexes_under_rect(visible_rect)) {
if (invalidated_.find(loc) != invalidated_.end())
return true;
}
return false;
}
void display::invalidate_animations()
{
if (!preferences::animate_map()) {
return;
}
foreach (const map_location &loc, get_visible_hexes())
{
if (shrouded(loc)) continue;
if (builder_->update_animation(loc)) {
invalidate(loc);
} else {
invalidate_animations_location(loc);
}
}
}