mirror of
https://github.com/wesnoth/wesnoth
synced 2025-05-11 11:24:30 +00:00
1119 lines
26 KiB
C++
1119 lines
26 KiB
C++
/*
|
|
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
|
|
Part of the Battle for Wesnoth Project https://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 as published by
|
|
the Free Software Foundation; either version 2 of the License, 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
|
|
* Routines for images: load, scale, re-color, etc.
|
|
*/
|
|
|
|
#include "picture.hpp"
|
|
|
|
#include "config.hpp"
|
|
#include "display.hpp"
|
|
#include "filesystem.hpp"
|
|
#include "game_config.hpp"
|
|
#include "image_modifications.hpp"
|
|
#include "log.hpp"
|
|
#include "preferences/general.hpp"
|
|
#include "serialization/base64.hpp"
|
|
#include "serialization/string_utils.hpp"
|
|
#include "sdl/rect.hpp"
|
|
#include "utils/general.hpp"
|
|
|
|
#include <SDL2/SDL_image.h>
|
|
|
|
#include <functional>
|
|
|
|
#include <boost/algorithm/string.hpp>
|
|
#include <boost/functional/hash_fwd.hpp>
|
|
|
|
#include <set>
|
|
|
|
static lg::log_domain log_display("display");
|
|
#define ERR_DP LOG_STREAM(err, log_display)
|
|
#define LOG_DP LOG_STREAM(info, log_display)
|
|
|
|
static lg::log_domain log_config("config");
|
|
#define ERR_CFG LOG_STREAM(err, log_config)
|
|
|
|
using game_config::tile_size;
|
|
|
|
template<typename T>
|
|
struct cache_item
|
|
{
|
|
cache_item()
|
|
: item()
|
|
, loaded(false)
|
|
{
|
|
}
|
|
|
|
cache_item(const T& item)
|
|
: item(item)
|
|
, loaded(true)
|
|
{
|
|
}
|
|
|
|
T item;
|
|
bool loaded;
|
|
};
|
|
|
|
namespace std
|
|
{
|
|
template<>
|
|
struct hash<image::locator::value>
|
|
{
|
|
std::size_t operator()(const image::locator::value& val) const
|
|
{
|
|
std::size_t hash = std::hash<unsigned>{}(val.type_);
|
|
|
|
if(val.type_ == image::locator::FILE || val.type_ == image::locator::SUB_FILE) {
|
|
boost::hash_combine(hash, val.filename_);
|
|
}
|
|
|
|
if(val.type_ == image::locator::SUB_FILE) {
|
|
boost::hash_combine(hash, val.loc_.x);
|
|
boost::hash_combine(hash, val.loc_.y);
|
|
boost::hash_combine(hash, val.center_x_);
|
|
boost::hash_combine(hash, val.center_y_);
|
|
boost::hash_combine(hash, val.modifications_);
|
|
}
|
|
|
|
return hash;
|
|
}
|
|
};
|
|
}
|
|
|
|
namespace image
|
|
{
|
|
template<typename T>
|
|
class cache_type
|
|
{
|
|
public:
|
|
cache_type()
|
|
: content_()
|
|
{
|
|
}
|
|
|
|
cache_item<T>& get_element(int index)
|
|
{
|
|
if(static_cast<unsigned>(index) >= content_.size())
|
|
content_.resize(index + 1);
|
|
return content_[index];
|
|
}
|
|
|
|
void flush()
|
|
{
|
|
content_.clear();
|
|
}
|
|
|
|
private:
|
|
std::vector<cache_item<T>> content_;
|
|
};
|
|
|
|
template<typename T>
|
|
bool locator::in_cache(cache_type<T>& cache) const
|
|
{
|
|
return index_ < 0 ? false : cache.get_element(index_).loaded;
|
|
}
|
|
|
|
template<typename T>
|
|
const T& locator::locate_in_cache(cache_type<T>& cache) const
|
|
{
|
|
static T dummy;
|
|
return index_ < 0 ? dummy : cache.get_element(index_).item;
|
|
}
|
|
|
|
template<typename T>
|
|
T& locator::access_in_cache(cache_type<T>& cache) const
|
|
{
|
|
static T dummy;
|
|
return index_ < 0 ? dummy : cache.get_element(index_).item;
|
|
}
|
|
|
|
template<typename T>
|
|
void locator::add_to_cache(cache_type<T>& cache, const T& data) const
|
|
{
|
|
if(index_ >= 0) {
|
|
cache.get_element(index_) = cache_item<T>(data);
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace
|
|
{
|
|
image::locator::locator_finder_t locator_finder;
|
|
|
|
/** Definition of all image maps */
|
|
image::image_cache images_, scaled_to_zoom_, hexed_images_, scaled_to_hex_images_, tod_colored_images_,
|
|
brightened_images_;
|
|
|
|
// cache storing if each image fit in a hex
|
|
image::bool_cache in_hex_info_;
|
|
|
|
// cache storing if this is an empty hex
|
|
image::bool_cache is_empty_hex_;
|
|
|
|
// caches storing the different lighted cases for each image
|
|
image::lit_cache lit_images_, lit_scaled_images_;
|
|
// caches storing each lightmap generated
|
|
image::lit_variants lightmaps_;
|
|
|
|
// const int cache_version_ = 0;
|
|
|
|
std::map<std::string, bool> image_existence_map;
|
|
|
|
// directories where we already cached file existence
|
|
std::set<std::string> precached_dirs;
|
|
|
|
std::map<surface, surface> reversed_images_;
|
|
|
|
int red_adjust = 0, green_adjust = 0, blue_adjust = 0;
|
|
|
|
unsigned int zoom = tile_size;
|
|
unsigned int cached_zoom = 0;
|
|
|
|
const std::string data_uri_prefix = "data:";
|
|
struct parsed_data_URI{
|
|
explicit parsed_data_URI(utils::string_view data_URI);
|
|
utils::string_view scheme;
|
|
utils::string_view mime;
|
|
utils::string_view base64;
|
|
utils::string_view data;
|
|
bool good;
|
|
};
|
|
parsed_data_URI::parsed_data_URI(utils::string_view data_URI)
|
|
{
|
|
const std::size_t colon = data_URI.find(':');
|
|
const utils::string_view after_scheme = data_URI.substr(colon + 1);
|
|
|
|
const std::size_t comma = after_scheme.find(',');
|
|
const utils::string_view type_info = after_scheme.substr(0, comma);
|
|
|
|
const std::size_t semicolon = type_info.find(';');
|
|
|
|
scheme = data_URI.substr(0, colon);
|
|
base64 = type_info.substr(semicolon + 1);
|
|
mime = type_info.substr(0, semicolon);
|
|
data = after_scheme.substr(comma + 1);
|
|
good = (scheme == "data" && base64 == "base64" && mime.length() > 0 && data.length() > 0);
|
|
}
|
|
|
|
} // end anon namespace
|
|
|
|
namespace image
|
|
{
|
|
mini_terrain_cache_map mini_terrain_cache;
|
|
mini_terrain_cache_map mini_fogged_terrain_cache;
|
|
mini_terrain_cache_map mini_highlighted_terrain_cache;
|
|
|
|
static int last_index_ = 0;
|
|
|
|
void flush_cache()
|
|
{
|
|
{
|
|
images_.flush();
|
|
hexed_images_.flush();
|
|
tod_colored_images_.flush();
|
|
scaled_to_zoom_.flush();
|
|
scaled_to_hex_images_.flush();
|
|
brightened_images_.flush();
|
|
lit_images_.flush();
|
|
lit_scaled_images_.flush();
|
|
in_hex_info_.flush();
|
|
is_empty_hex_.flush();
|
|
mini_terrain_cache.clear();
|
|
mini_fogged_terrain_cache.clear();
|
|
mini_highlighted_terrain_cache.clear();
|
|
reversed_images_.clear();
|
|
image_existence_map.clear();
|
|
precached_dirs.clear();
|
|
}
|
|
/* We can't reset last_index_, since some locators are still alive
|
|
when using :refresh. That would cause them to point to the wrong
|
|
images. Not resetting the variable causes a memory leak, though. */
|
|
// last_index_ = 0;
|
|
}
|
|
|
|
void locator::init_index()
|
|
{
|
|
auto i = locator_finder.find(val_);
|
|
|
|
if(i == locator_finder.end()) {
|
|
index_ = last_index_++;
|
|
locator_finder.emplace(val_, index_);
|
|
} else {
|
|
index_ = i->second;
|
|
}
|
|
}
|
|
|
|
void locator::parse_arguments()
|
|
{
|
|
std::string& fn = val_.filename_;
|
|
if(fn.empty()) {
|
|
return;
|
|
}
|
|
|
|
if(boost::algorithm::starts_with(fn, data_uri_prefix)) {
|
|
parsed_data_URI parsed{fn};
|
|
|
|
if(!parsed.good) {
|
|
utils::string_view view{ fn };
|
|
utils::string_view stripped = view.substr(0, view.find(","));
|
|
ERR_DP << "Invalid data URI: " << stripped << std::endl;
|
|
}
|
|
|
|
val_.is_data_uri_ = true;
|
|
}
|
|
|
|
std::size_t markup_field = fn.find('~');
|
|
|
|
if(markup_field != std::string::npos) {
|
|
val_.type_ = SUB_FILE;
|
|
val_.modifications_ = fn.substr(markup_field, fn.size() - markup_field);
|
|
fn = fn.substr(0, markup_field);
|
|
}
|
|
}
|
|
|
|
locator::locator()
|
|
: index_(-1)
|
|
, val_()
|
|
{
|
|
}
|
|
|
|
locator::locator(const locator& a, const std::string& mods)
|
|
: index_(-1)
|
|
, val_(a.val_)
|
|
{
|
|
if(!mods.empty()) {
|
|
val_.modifications_ += mods;
|
|
val_.type_ = SUB_FILE;
|
|
init_index();
|
|
} else {
|
|
index_ = a.index_;
|
|
}
|
|
}
|
|
|
|
locator::locator(const char* filename)
|
|
: index_(-1)
|
|
, val_(filename)
|
|
{
|
|
parse_arguments();
|
|
init_index();
|
|
}
|
|
|
|
locator::locator(const std::string& filename)
|
|
: index_(-1)
|
|
, val_(filename)
|
|
{
|
|
parse_arguments();
|
|
init_index();
|
|
}
|
|
|
|
locator::locator(const std::string& filename, const std::string& modifications)
|
|
: index_(-1)
|
|
, val_(filename, modifications)
|
|
{
|
|
init_index();
|
|
}
|
|
|
|
locator::locator(const std::string& filename,
|
|
const map_location& loc,
|
|
int center_x,
|
|
int center_y,
|
|
const std::string& modifications)
|
|
: index_(-1)
|
|
, val_(filename, loc, center_x, center_y, modifications)
|
|
{
|
|
init_index();
|
|
}
|
|
|
|
locator& locator::operator=(const locator& a)
|
|
{
|
|
index_ = a.index_;
|
|
val_ = a.val_;
|
|
|
|
return *this;
|
|
}
|
|
|
|
locator::value::value()
|
|
: type_(NONE)
|
|
, is_data_uri_(false)
|
|
, filename_()
|
|
, loc_()
|
|
, modifications_()
|
|
, center_x_(0)
|
|
, center_y_(0)
|
|
{
|
|
}
|
|
|
|
locator::value::value(const char* filename)
|
|
: type_(FILE)
|
|
, is_data_uri_(false)
|
|
, filename_(filename)
|
|
, loc_()
|
|
, modifications_()
|
|
, center_x_(0)
|
|
, center_y_(0)
|
|
{
|
|
}
|
|
|
|
locator::value::value(const std::string& filename)
|
|
: type_(FILE)
|
|
, is_data_uri_(false)
|
|
, filename_(filename)
|
|
, loc_()
|
|
, modifications_()
|
|
, center_x_(0)
|
|
, center_y_(0)
|
|
{
|
|
}
|
|
|
|
locator::value::value(const std::string& filename, const std::string& modifications)
|
|
: type_(SUB_FILE)
|
|
, is_data_uri_(false)
|
|
, filename_(filename)
|
|
, loc_()
|
|
, modifications_(modifications)
|
|
, center_x_(0)
|
|
, center_y_(0)
|
|
{
|
|
}
|
|
|
|
locator::value::value(const std::string& filename,
|
|
const map_location& loc,
|
|
int center_x,
|
|
int center_y,
|
|
const std::string& modifications)
|
|
: type_(SUB_FILE)
|
|
, is_data_uri_(false)
|
|
, filename_(filename)
|
|
, loc_(loc)
|
|
, modifications_(modifications)
|
|
, center_x_(center_x)
|
|
, center_y_(center_y)
|
|
{
|
|
}
|
|
|
|
bool locator::value::operator==(const value& a) const
|
|
{
|
|
if(a.type_ != type_) {
|
|
return false;
|
|
} else if(type_ == FILE) {
|
|
return filename_ == a.filename_;
|
|
} else if(type_ == SUB_FILE) {
|
|
return filename_ == a.filename_ && loc_ == a.loc_ && modifications_ == a.modifications_
|
|
&& center_x_ == a.center_x_ && center_y_ == a.center_y_;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool locator::value::operator<(const value& a) const
|
|
{
|
|
if(type_ != a.type_) {
|
|
return type_ < a.type_;
|
|
} else if(type_ == FILE) {
|
|
return filename_ < a.filename_;
|
|
} else if(type_ == SUB_FILE) {
|
|
if(filename_ != a.filename_)
|
|
return filename_ < a.filename_;
|
|
if(loc_ != a.loc_)
|
|
return loc_ < a.loc_;
|
|
if(center_x_ != a.center_x_)
|
|
return center_x_ < a.center_x_;
|
|
if(center_y_ != a.center_y_)
|
|
return center_y_ < a.center_y_;
|
|
return (modifications_ < a.modifications_);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Load overlay image and compose it with the original surface.
|
|
static void add_localized_overlay(const std::string& ovr_file, surface& orig_surf)
|
|
{
|
|
filesystem::rwops_ptr rwops = filesystem::make_read_RWops(ovr_file);
|
|
surface ovr_surf = IMG_Load_RW(rwops.release(), true); // SDL takes ownership of rwops
|
|
if(!ovr_surf) {
|
|
return;
|
|
}
|
|
|
|
SDL_Rect area {0, 0, ovr_surf->w, ovr_surf->h};
|
|
|
|
sdl_blit(ovr_surf, 0, orig_surf, &area);
|
|
}
|
|
|
|
static surface load_image_file(const image::locator& loc)
|
|
{
|
|
surface res;
|
|
|
|
std::string location = filesystem::get_binary_file_location("images", loc.get_filename());
|
|
|
|
{
|
|
if(!location.empty()) {
|
|
// Check if there is a localized image.
|
|
const std::string loc_location = filesystem::get_localized_path(location);
|
|
if(!loc_location.empty()) {
|
|
location = loc_location;
|
|
}
|
|
|
|
filesystem::rwops_ptr rwops = filesystem::make_read_RWops(location);
|
|
res = IMG_Load_RW(rwops.release(), true); // SDL takes ownership of rwops
|
|
|
|
// If there was no standalone localized image, check if there is an overlay.
|
|
if(res && loc_location.empty()) {
|
|
const std::string ovr_location = filesystem::get_localized_path(location, "--overlay");
|
|
if(!ovr_location.empty()) {
|
|
add_localized_overlay(ovr_location, res);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(!res && !loc.get_filename().empty()) {
|
|
ERR_DP << "could not open image '" << loc.get_filename() << "'" << std::endl;
|
|
if(game_config::debug && loc.get_filename() != game_config::images::missing)
|
|
return get_image(game_config::images::missing, UNSCALED);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
static surface load_image_sub_file(const image::locator& loc)
|
|
{
|
|
surface surf = get_image(loc.get_filename(), UNSCALED);
|
|
if(surf == nullptr) {
|
|
return nullptr;
|
|
}
|
|
|
|
modification_queue mods = modification::decode(loc.get_modifications());
|
|
|
|
while(!mods.empty()) {
|
|
modification* mod = mods.top();
|
|
|
|
try {
|
|
surf = (*mod)(surf);
|
|
} catch(const image::modification::imod_exception& e) {
|
|
std::ostringstream ss;
|
|
ss << "\n";
|
|
|
|
for(const std::string& mod_name : utils::parenthetical_split(loc.get_modifications(), '~')) {
|
|
ss << "\t" << mod_name << "\n";
|
|
}
|
|
|
|
ERR_CFG << "Failed to apply a modification to an image:\n"
|
|
<< "Image: " << loc.get_filename() << "\n"
|
|
<< "Modifications: " << ss.str() << "\n"
|
|
<< "Error: " << e.message << "\n";
|
|
}
|
|
|
|
// NOTE: do this *after* applying the mod or you'll get crashes!
|
|
mods.pop();
|
|
}
|
|
|
|
if(loc.get_loc().valid()) {
|
|
SDL_Rect srcrect = sdl::create_rect(
|
|
((tile_size * 3) / 4) * loc.get_loc().x,
|
|
tile_size * loc.get_loc().y + (tile_size / 2) * (loc.get_loc().x % 2),
|
|
tile_size,
|
|
tile_size
|
|
);
|
|
|
|
if(loc.get_center_x() >= 0 && loc.get_center_y() >= 0) {
|
|
srcrect.x += surf->w / 2 - loc.get_center_x();
|
|
srcrect.y += surf->h / 2 - loc.get_center_y();
|
|
}
|
|
|
|
// cut and hex mask, but also check and cache if empty result
|
|
surface cut(cut_surface(surf, srcrect));
|
|
bool is_empty = false;
|
|
surf = mask_surface(cut, get_hexmask(), &is_empty);
|
|
|
|
// discard empty images to free memory
|
|
if(is_empty) {
|
|
// Safe because those images are only used by terrain rendering
|
|
// and it filters them out.
|
|
// A safer and more general way would be to keep only one copy of it
|
|
surf = nullptr;
|
|
}
|
|
|
|
loc.add_to_cache(is_empty_hex_, is_empty);
|
|
}
|
|
|
|
return surf;
|
|
}
|
|
|
|
static surface load_image_data_uri(const image::locator& loc)
|
|
{
|
|
surface surf;
|
|
|
|
parsed_data_URI parsed{loc.get_filename()};
|
|
|
|
if(!parsed.good) {
|
|
utils::string_view fn = loc.get_filename();
|
|
utils::string_view stripped = fn.substr(0, fn.find(","));
|
|
ERR_DP << "Invalid data URI: " << stripped << std::endl;
|
|
} else if(parsed.mime.substr(0, 5) != "image") {
|
|
ERR_DP << "Data URI not of image MIME type: " << parsed.mime << std::endl;
|
|
} else {
|
|
const std::vector<uint8_t> image_data = base64::decode(parsed.data);
|
|
filesystem::rwops_ptr rwops{SDL_RWFromConstMem(image_data.data(), image_data.size()), &SDL_FreeRW};
|
|
|
|
if(image_data.empty()) {
|
|
ERR_DP << "Invalid encoding in data URI" << std::endl;
|
|
} else if(parsed.mime == "image/png") {
|
|
surf = IMG_LoadTyped_RW(rwops.release(), true, "PNG");
|
|
} else if(parsed.mime == "image/jpeg") {
|
|
surf = IMG_LoadTyped_RW(rwops.release(), true, "JPG");
|
|
} else {
|
|
ERR_DP << "Invalid image MIME type: " << parsed.mime << std::endl;
|
|
}
|
|
}
|
|
|
|
return surf;
|
|
}
|
|
|
|
// small utility function to store an int from (-256,254) to an signed char
|
|
static signed char col_to_uchar(int i)
|
|
{
|
|
return static_cast<signed char>(std::min<int>(127, std::max<int>(-128, i / 2)));
|
|
}
|
|
|
|
light_string get_light_string(int op, int r, int g, int b)
|
|
{
|
|
light_string ls;
|
|
ls.reserve(4);
|
|
ls.push_back(op);
|
|
ls.push_back(col_to_uchar(r));
|
|
ls.push_back(col_to_uchar(g));
|
|
ls.push_back(col_to_uchar(b));
|
|
|
|
return ls;
|
|
}
|
|
|
|
static surface apply_light(surface surf, const light_string& ls)
|
|
{
|
|
// atomic lightmap operation are handled directly (important to end recursion)
|
|
if(ls.size() == 4) {
|
|
// if no lightmap (first char = -1) then we need the initial value
|
|
//(before the halving done for lightmap)
|
|
int m = ls[0] == -1 ? 2 : 1;
|
|
return adjust_surface_color(surf, ls[1] * m, ls[2] * m, ls[3] * m);
|
|
}
|
|
|
|
// check if the lightmap is already cached or need to be generated
|
|
surface lightmap = nullptr;
|
|
auto i = lightmaps_.find(ls);
|
|
if(i != lightmaps_.end()) {
|
|
lightmap = i->second;
|
|
} else {
|
|
// build all the paths for lightmap sources
|
|
static const std::string p = "terrain/light/light";
|
|
static const std::string lm_img[19] {
|
|
p + ".png",
|
|
p + "-concave-2-tr.png", p + "-concave-2-r.png", p + "-concave-2-br.png",
|
|
p + "-concave-2-bl.png", p + "-concave-2-l.png", p + "-concave-2-tl.png",
|
|
p + "-convex-br-bl.png", p + "-convex-bl-l.png", p + "-convex-l-tl.png",
|
|
p + "-convex-tl-tr.png", p + "-convex-tr-r.png", p + "-convex-r-br.png",
|
|
p + "-convex-l-bl.png", p + "-convex-tl-l.png", p + "-convex-tr-tl.png",
|
|
p + "-convex-r-tr.png", p + "-convex-br-r.png", p + "-convex-bl-br.png"
|
|
};
|
|
|
|
// decompose into atomic lightmap operations (4 chars)
|
|
for(std::size_t c = 0; c + 3 < ls.size(); c += 4) {
|
|
light_string sls = ls.substr(c, 4);
|
|
|
|
// get the corresponding image and apply the lightmap operation to it
|
|
// This allows to also cache lightmap parts.
|
|
// note that we avoid infinite recursion by using only atomic operation
|
|
surface lts = image::get_lighted_image(lm_img[sls[0]], sls, HEXED);
|
|
|
|
// first image will be the base where we blit the others
|
|
if(lightmap == nullptr) {
|
|
// copy the cached image to avoid modifying the cache
|
|
lightmap = lts.clone();
|
|
} else {
|
|
sdl_blit(lts, nullptr, lightmap, nullptr);
|
|
}
|
|
}
|
|
|
|
// cache the result
|
|
lightmaps_[ls] = lightmap;
|
|
}
|
|
|
|
// apply the final lightmap
|
|
return light_surface(surf, lightmap);
|
|
}
|
|
|
|
bool locator::file_exists() const
|
|
{
|
|
return val_.is_data_uri_
|
|
? parsed_data_URI{val_.filename_}.good
|
|
: !filesystem::get_binary_file_location("images", val_.filename_).empty();
|
|
}
|
|
|
|
surface load_from_disk(const locator& loc)
|
|
{
|
|
switch(loc.get_type()) {
|
|
case locator::FILE:
|
|
if(loc.is_data_uri()){
|
|
return load_image_data_uri(loc);
|
|
} else {
|
|
return load_image_file(loc);
|
|
}
|
|
case locator::SUB_FILE:
|
|
return load_image_sub_file(loc);
|
|
default:
|
|
return surface(nullptr);
|
|
}
|
|
}
|
|
|
|
manager::manager()
|
|
{
|
|
}
|
|
|
|
manager::~manager()
|
|
{
|
|
flush_cache();
|
|
}
|
|
|
|
void set_color_adjustment(int r, int g, int b)
|
|
{
|
|
if(r != red_adjust || g != green_adjust || b != blue_adjust) {
|
|
red_adjust = r;
|
|
green_adjust = g;
|
|
blue_adjust = b;
|
|
tod_colored_images_.flush();
|
|
brightened_images_.flush();
|
|
lit_images_.flush();
|
|
lit_scaled_images_.flush();
|
|
reversed_images_.clear();
|
|
}
|
|
}
|
|
|
|
void set_zoom(unsigned int amount)
|
|
{
|
|
if(amount != zoom) {
|
|
zoom = amount;
|
|
tod_colored_images_.flush();
|
|
brightened_images_.flush();
|
|
reversed_images_.clear();
|
|
|
|
// We keep these caches if:
|
|
// we use default zoom (it doesn't need those)
|
|
// or if they are already at the wanted zoom.
|
|
if(zoom != tile_size && zoom != cached_zoom) {
|
|
scaled_to_zoom_.flush();
|
|
scaled_to_hex_images_.flush();
|
|
lit_scaled_images_.flush();
|
|
cached_zoom = zoom;
|
|
}
|
|
}
|
|
}
|
|
|
|
static surface get_hexed(const locator& i_locator)
|
|
{
|
|
surface image(get_image(i_locator, UNSCALED));
|
|
// hex cut tiles, also check and cache if empty result
|
|
bool is_empty = false;
|
|
surface res = mask_surface(image, get_hexmask(), &is_empty, i_locator.get_filename());
|
|
i_locator.add_to_cache(is_empty_hex_, is_empty);
|
|
return res;
|
|
}
|
|
|
|
static surface get_scaled_to_hex(const locator& i_locator)
|
|
{
|
|
surface img = get_image(i_locator, HEXED);
|
|
// return scale_surface(img, zoom, zoom);
|
|
|
|
if(img) {
|
|
return scale_surface_nn(img, zoom, zoom);
|
|
}
|
|
|
|
return surface(nullptr);
|
|
|
|
}
|
|
|
|
static surface get_tod_colored(const locator& i_locator)
|
|
{
|
|
surface img = get_image(i_locator, SCALED_TO_HEX);
|
|
return adjust_surface_color(img, red_adjust, green_adjust, blue_adjust);
|
|
}
|
|
|
|
static surface get_scaled_to_zoom(const locator& i_locator)
|
|
{
|
|
assert(zoom != tile_size);
|
|
assert(tile_size != 0);
|
|
|
|
surface res(get_image(i_locator, UNSCALED));
|
|
// For some reason haloes seems to have invalid images, protect against crashing
|
|
if(res) {
|
|
return scale_surface_nn(res, ((res->w * zoom) / tile_size), ((res->h * zoom) / tile_size));
|
|
}
|
|
|
|
return surface(nullptr);
|
|
}
|
|
|
|
static surface get_brightened(const locator& i_locator)
|
|
{
|
|
surface image(get_image(i_locator, TOD_COLORED));
|
|
return brighten_image(image, ftofxp(game_config::hex_brightening));
|
|
}
|
|
|
|
/// translate type to a simpler one when possible
|
|
static TYPE simplify_type(const image::locator& i_locator, TYPE type)
|
|
{
|
|
switch(type) {
|
|
case SCALED_TO_ZOOM:
|
|
if(zoom == tile_size) {
|
|
type = UNSCALED;
|
|
}
|
|
|
|
break;
|
|
case BRIGHTENED:
|
|
if(ftofxp(game_config::hex_brightening) == ftofxp(1.0)) {
|
|
type = TOD_COLORED;
|
|
}
|
|
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if(type == TOD_COLORED) {
|
|
if(red_adjust == 0 && green_adjust == 0 && blue_adjust == 0) {
|
|
type = SCALED_TO_HEX;
|
|
}
|
|
}
|
|
|
|
if(type == SCALED_TO_HEX) {
|
|
if(zoom == tile_size) {
|
|
type = HEXED;
|
|
}
|
|
}
|
|
|
|
if(type == HEXED) {
|
|
// check if the image is already hex-cut by the location system
|
|
if(i_locator.get_loc().valid()) {
|
|
type = UNSCALED;
|
|
}
|
|
}
|
|
|
|
return type;
|
|
}
|
|
|
|
surface get_image(const image::locator& i_locator, TYPE type)
|
|
{
|
|
surface res;
|
|
|
|
if(i_locator.is_void()) {
|
|
return res;
|
|
}
|
|
|
|
type = simplify_type(i_locator, type);
|
|
|
|
image_cache* imap;
|
|
// select associated cache
|
|
switch(type) {
|
|
case UNSCALED:
|
|
imap = &images_;
|
|
break;
|
|
case TOD_COLORED:
|
|
imap = &tod_colored_images_;
|
|
break;
|
|
case SCALED_TO_ZOOM:
|
|
imap = &scaled_to_zoom_;
|
|
break;
|
|
case HEXED:
|
|
imap = &hexed_images_;
|
|
break;
|
|
case SCALED_TO_HEX:
|
|
imap = &scaled_to_hex_images_;
|
|
break;
|
|
case BRIGHTENED:
|
|
imap = &brightened_images_;
|
|
break;
|
|
default:
|
|
return res;
|
|
}
|
|
|
|
// return the image if already cached
|
|
bool tmp = i_locator.in_cache(*imap);
|
|
|
|
if(tmp) {
|
|
surface result = i_locator.locate_in_cache(*imap);
|
|
return result;
|
|
}
|
|
|
|
// not cached, generate it
|
|
switch(type) {
|
|
case UNSCALED:
|
|
// If type is unscaled, directly load the image from the disk.
|
|
res = load_from_disk(i_locator);
|
|
break;
|
|
case TOD_COLORED:
|
|
res = get_tod_colored(i_locator);
|
|
break;
|
|
case SCALED_TO_ZOOM:
|
|
res = get_scaled_to_zoom(i_locator);
|
|
break;
|
|
case HEXED:
|
|
res = get_hexed(i_locator);
|
|
break;
|
|
case SCALED_TO_HEX:
|
|
res = get_scaled_to_hex(i_locator);
|
|
break;
|
|
case BRIGHTENED:
|
|
res = get_brightened(i_locator);
|
|
break;
|
|
default:
|
|
return res;
|
|
}
|
|
|
|
i_locator.add_to_cache(*imap, res);
|
|
|
|
return res;
|
|
}
|
|
|
|
surface get_lighted_image(const image::locator& i_locator, const light_string& ls, TYPE type)
|
|
{
|
|
surface res;
|
|
if(i_locator.is_void()) {
|
|
return res;
|
|
}
|
|
|
|
if(type == SCALED_TO_HEX && zoom == tile_size) {
|
|
type = HEXED;
|
|
}
|
|
|
|
// select associated cache
|
|
lit_cache* imap = &lit_images_;
|
|
if(type == SCALED_TO_HEX) {
|
|
imap = &lit_scaled_images_;
|
|
}
|
|
|
|
// if no light variants yet, need to add an empty map
|
|
if(!i_locator.in_cache(*imap)) {
|
|
i_locator.add_to_cache(*imap, lit_variants());
|
|
}
|
|
|
|
// need access to add it if not found
|
|
{ // enclose reference pointing to data stored in a changing vector
|
|
const lit_variants& lvar = i_locator.locate_in_cache(*imap);
|
|
auto lvi = lvar.find(ls);
|
|
if(lvi != lvar.end()) {
|
|
return lvi->second;
|
|
}
|
|
}
|
|
|
|
// not cached yet, generate it
|
|
switch(type) {
|
|
case HEXED:
|
|
res = get_image(i_locator, HEXED);
|
|
res = apply_light(res, ls);
|
|
break;
|
|
case SCALED_TO_HEX:
|
|
// we light before scaling to reuse the unscaled cache
|
|
res = get_lighted_image(i_locator, ls, HEXED);
|
|
res = scale_surface_nn(res, zoom, zoom);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// record the lighted surface in the corresponding variants cache
|
|
i_locator.access_in_cache(*imap)[ls] = res;
|
|
|
|
return res;
|
|
}
|
|
|
|
surface get_hexmask()
|
|
{
|
|
static const image::locator terrain_mask(game_config::images::terrain_mask);
|
|
return get_image(terrain_mask, UNSCALED);
|
|
}
|
|
|
|
bool is_in_hex(const locator& i_locator)
|
|
{
|
|
bool result;
|
|
{
|
|
if(i_locator.in_cache(in_hex_info_)) {
|
|
result = i_locator.locate_in_cache(in_hex_info_);
|
|
} else {
|
|
const surface image(get_image(i_locator, UNSCALED));
|
|
|
|
bool res = in_mask_surface(image, get_hexmask());
|
|
|
|
i_locator.add_to_cache(in_hex_info_, res);
|
|
|
|
// std::cout << "in_hex : " << i_locator.get_filename()
|
|
// << " " << (res ? "yes" : "no") << "\n";
|
|
|
|
result = res;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
bool is_empty_hex(const locator& i_locator)
|
|
{
|
|
if(!i_locator.in_cache(is_empty_hex_)) {
|
|
const surface surf = get_image(i_locator, HEXED);
|
|
// emptiness of terrain image is checked during hex cut
|
|
// so, maybe in cache now, let's recheck
|
|
if(!i_locator.in_cache(is_empty_hex_)) {
|
|
// should never reach here
|
|
// but do it manually if it happens
|
|
// assert(false);
|
|
bool is_empty = false;
|
|
mask_surface(surf, get_hexmask(), &is_empty);
|
|
i_locator.add_to_cache(is_empty_hex_, is_empty);
|
|
}
|
|
}
|
|
|
|
return i_locator.locate_in_cache(is_empty_hex_);
|
|
}
|
|
|
|
surface reverse_image(const surface& surf)
|
|
{
|
|
if(surf == nullptr) {
|
|
return surface(nullptr);
|
|
}
|
|
|
|
const auto itor = reversed_images_.find(surf);
|
|
if(itor != reversed_images_.end()) {
|
|
// sdl_add_ref(itor->second);
|
|
return itor->second;
|
|
}
|
|
|
|
const surface rev(flip_surface(surf));
|
|
if(rev == nullptr) {
|
|
return surface(nullptr);
|
|
}
|
|
|
|
reversed_images_.emplace(surf, rev);
|
|
// sdl_add_ref(rev);
|
|
return rev;
|
|
}
|
|
|
|
bool exists(const image::locator& i_locator)
|
|
{
|
|
typedef image::locator loc;
|
|
loc::type type = i_locator.get_type();
|
|
if(type != loc::FILE && type != loc::SUB_FILE) {
|
|
return false;
|
|
}
|
|
|
|
// The insertion will fail if there is already an element in the cache
|
|
// and this will point to the existing element.
|
|
auto iter = image_existence_map.begin();
|
|
bool success;
|
|
|
|
std::tie(iter, success) = image_existence_map.emplace(i_locator.get_filename(), false);
|
|
|
|
bool& cache = iter->second;
|
|
if(success) {
|
|
if(i_locator.is_data_uri()) {
|
|
cache = parsed_data_URI{i_locator.get_filename()}.good;
|
|
} else {
|
|
cache = !filesystem::get_binary_file_location("images", i_locator.get_filename()).empty();
|
|
}
|
|
}
|
|
|
|
return cache;
|
|
}
|
|
|
|
static void precache_file_existence_internal(const std::string& dir, const std::string& subdir)
|
|
{
|
|
const std::string checked_dir = dir + "/" + subdir;
|
|
if(precached_dirs.find(checked_dir) != precached_dirs.end()) {
|
|
return;
|
|
}
|
|
|
|
precached_dirs.insert(checked_dir);
|
|
|
|
if(!filesystem::is_directory(checked_dir)) {
|
|
return;
|
|
}
|
|
|
|
std::vector<std::string> files_found;
|
|
std::vector<std::string> dirs_found;
|
|
filesystem::get_files_in_dir(checked_dir, &files_found, &dirs_found, filesystem::name_mode::FILE_NAME_ONLY,
|
|
filesystem::filter_mode::NO_FILTER, filesystem::reorder_mode::DONT_REORDER);
|
|
|
|
for(const auto& f : files_found) {
|
|
image_existence_map[subdir + f] = true;
|
|
}
|
|
|
|
for(const auto& d : dirs_found) {
|
|
precache_file_existence_internal(dir, subdir + d + "/");
|
|
}
|
|
}
|
|
|
|
void precache_file_existence(const std::string& subdir)
|
|
{
|
|
const std::vector<std::string>& paths = filesystem::get_binary_paths("images");
|
|
|
|
for(const auto& p : paths) {
|
|
precache_file_existence_internal(p, subdir);
|
|
}
|
|
}
|
|
|
|
bool precached_file_exists(const std::string& file)
|
|
{
|
|
const auto b = image_existence_map.find(file);
|
|
if(b != image_existence_map.end()) {
|
|
return b->second;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
save_result save_image(const locator& i_locator, const std::string& filename)
|
|
{
|
|
return save_image(get_image(i_locator), filename);
|
|
}
|
|
|
|
save_result save_image(const surface& surf, const std::string& filename)
|
|
{
|
|
if(!surf) {
|
|
return save_result::no_image;
|
|
}
|
|
|
|
if(filesystem::ends_with(filename, ".jpeg") || filesystem::ends_with(filename, ".jpg") || filesystem::ends_with(filename, ".jpe")) {
|
|
LOG_DP << "Writing a JPG image to " << filename << std::endl;
|
|
|
|
const int err = IMG_SaveJPG_RW(surf, filesystem::make_write_RWops(filename).release(), true, 75); // SDL takes ownership of the RWops
|
|
return err == 0 ? save_result::success : save_result::save_failed;
|
|
}
|
|
|
|
if(filesystem::ends_with(filename, ".png")) {
|
|
LOG_DP << "Writing a PNG image to " << filename << std::endl;
|
|
|
|
const int err = IMG_SavePNG_RW(surf, filesystem::make_write_RWops(filename).release(), true); // SDL takes ownership of the RWops
|
|
return err == 0 ? save_result::success : save_result::save_failed;
|
|
}
|
|
|
|
if(filesystem::ends_with(filename, ".bmp")) {
|
|
LOG_DP << "Writing a BMP image to " << filename << std::endl;
|
|
const int err = SDL_SaveBMP(surf, filename.c_str()) == 0;
|
|
return err == 0 ? save_result::success : save_result::save_failed;
|
|
}
|
|
|
|
return save_result::unsupported_format;
|
|
}
|
|
|
|
} // end namespace image
|