mirror of
https://github.com/wesnoth/wesnoth
synced 2025-04-28 02:06:23 +00:00
988 lines
26 KiB
C++
988 lines
26 KiB
C++
/*
|
|
Copyright (C) 2003 - 2024
|
|
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 "filesystem.hpp"
|
|
#include "game_config.hpp"
|
|
#include "image_modifications.hpp"
|
|
#include "log.hpp"
|
|
#include "serialization/base64.hpp"
|
|
#include "serialization/string_utils.hpp"
|
|
#include "sdl/rect.hpp"
|
|
#include "sdl/texture.hpp"
|
|
|
|
#include <SDL2/SDL_image.h>
|
|
|
|
|
|
#include <boost/algorithm/string.hpp>
|
|
|
|
#include <array>
|
|
#include <set>
|
|
|
|
static lg::log_domain log_image("image");
|
|
#define ERR_IMG LOG_STREAM(err, log_image)
|
|
#define WRN_IMG LOG_STREAM(warn, log_image)
|
|
#define LOG_IMG LOG_STREAM(info, log_image)
|
|
#define DBG_IMG LOG_STREAM(debug, log_image)
|
|
|
|
static lg::log_domain log_config("config");
|
|
#define ERR_CFG LOG_STREAM(err, log_config)
|
|
|
|
using game_config::tile_size;
|
|
|
|
template<>
|
|
struct std::hash<image::locator>
|
|
{
|
|
std::size_t operator()(const image::locator& 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:
|
|
bool in_cache(const locator& item) const
|
|
{
|
|
return content_.find(item) != content_.end(); // TODO C++20: use content_.contains()
|
|
}
|
|
|
|
/** Returns a pointer to the cached value, or nullptr if not found. */
|
|
const T* locate_in_cache(const locator& item) const
|
|
{
|
|
if(auto iter = content_.find(item); iter != content_.end()) {
|
|
return &iter->second;
|
|
} else {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a reference to the cache item associated with the given key.
|
|
* If no corresponding value is found, a default instance will be created.
|
|
*/
|
|
T& access_in_cache(const locator& item)
|
|
{
|
|
return content_[item];
|
|
}
|
|
|
|
void add_to_cache(const locator& item, T data)
|
|
{
|
|
content_.insert_or_assign(item, std::move(data));
|
|
}
|
|
|
|
void flush()
|
|
{
|
|
content_.clear();
|
|
}
|
|
|
|
private:
|
|
std::unordered_map<locator, T> content_;
|
|
};
|
|
|
|
namespace
|
|
{
|
|
using surface_cache = cache_type<surface>;
|
|
using texture_cache = cache_type<texture>;
|
|
using bool_cache = cache_type<bool>;
|
|
|
|
/** Type used to pair light possibilities with the corresponding lit surface. */
|
|
using lit_surface_variants = std::map<light_string, surface>;
|
|
using lit_texture_variants = std::map<light_string, texture>;
|
|
|
|
/** Lit variants for each locator. */
|
|
using lit_surface_cache = cache_type<lit_surface_variants>;
|
|
using lit_texture_cache = cache_type<lit_texture_variants>;
|
|
|
|
/** Definition of all image maps */
|
|
std::array<surface_cache, NUM_TYPES> surfaces_;
|
|
|
|
/**
|
|
* Texture caches.
|
|
* Note that the latter two are temporary and should be removed once we have OGL and shader support.
|
|
*/
|
|
using texture_cache_map = std::map<image::scale_quality, image::texture_cache>;
|
|
|
|
texture_cache_map textures_;
|
|
texture_cache_map textures_hexed_;
|
|
texture_cache_map texture_tod_colored_;
|
|
|
|
// 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_surface_cache lit_surfaces_;
|
|
image::lit_texture_cache lit_textures_;
|
|
// caches storing each lightmap generated
|
|
image::lit_surface_variants surface_lightmaps_;
|
|
image::lit_texture_variants texture_lightmaps_;
|
|
|
|
// diagnostics for tracking skipped cache impact
|
|
std::array<bool_cache, NUM_TYPES> skipped_cache_;
|
|
int duplicate_loads_ = 0;
|
|
int total_loads_ = 0;
|
|
|
|
// 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;
|
|
|
|
int red_adjust = 0, green_adjust = 0, blue_adjust = 0;
|
|
|
|
const std::string data_uri_prefix = "data:";
|
|
struct parsed_data_URI{
|
|
explicit parsed_data_URI(std::string_view data_URI);
|
|
std::string_view scheme;
|
|
std::string_view mime;
|
|
std::string_view base64;
|
|
std::string_view data;
|
|
bool good;
|
|
};
|
|
parsed_data_URI::parsed_data_URI(std::string_view data_URI)
|
|
{
|
|
const std::size_t colon = data_URI.find(':');
|
|
const std::string_view after_scheme = data_URI.substr(colon + 1);
|
|
|
|
const std::size_t comma = after_scheme.find(',');
|
|
const std::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
|
|
|
|
void flush_cache()
|
|
{
|
|
for(surface_cache& cache : surfaces_) {
|
|
cache.flush();
|
|
}
|
|
lit_surfaces_.flush();
|
|
lit_textures_.flush();
|
|
surface_lightmaps_.clear();
|
|
texture_lightmaps_.clear();
|
|
in_hex_info_.flush();
|
|
is_empty_hex_.flush();
|
|
textures_.clear();
|
|
textures_hexed_.clear();
|
|
texture_tod_colored_.clear();
|
|
image_existence_map.clear();
|
|
precached_dirs.clear();
|
|
}
|
|
|
|
locator locator::clone(const std::string& mods) const
|
|
{
|
|
locator res = *this;
|
|
if(!mods.empty()) {
|
|
res.modifications_ += mods;
|
|
res.type_ = SUB_FILE;
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
std::ostream& operator<<(std::ostream& s, const locator& l)
|
|
{
|
|
s << l.get_filename();
|
|
if(!l.get_modifications().empty()) {
|
|
if(l.get_modifications()[0] != '~') {
|
|
s << '~';
|
|
}
|
|
s << l.get_modifications();
|
|
}
|
|
return s;
|
|
}
|
|
|
|
locator::locator(const std::string& fn)
|
|
: filename_(fn)
|
|
{
|
|
if(filename_.empty()) {
|
|
return;
|
|
}
|
|
|
|
if(boost::algorithm::starts_with(filename_, data_uri_prefix)) {
|
|
if(parsed_data_URI parsed{ filename_ }; !parsed.good) {
|
|
std::string_view view{ filename_ };
|
|
std::string_view stripped = view.substr(0, view.find(","));
|
|
ERR_IMG << "Invalid data URI: " << stripped;
|
|
}
|
|
|
|
is_data_uri_ = true;
|
|
}
|
|
|
|
if(const std::size_t markup_field = filename_.find('~'); markup_field != std::string::npos) {
|
|
type_ = SUB_FILE;
|
|
modifications_ = filename_.substr(markup_field, filename_.size() - markup_field);
|
|
filename_ = filename_.substr(0, markup_field);
|
|
} else {
|
|
type_ = FILE;
|
|
}
|
|
}
|
|
|
|
locator::locator(const std::string& filename, const std::string& modifications)
|
|
: type_(SUB_FILE)
|
|
, filename_(filename)
|
|
, modifications_(modifications)
|
|
{
|
|
}
|
|
|
|
locator::locator(
|
|
const std::string& filename,
|
|
const map_location& loc,
|
|
int center_x,
|
|
int center_y,
|
|
const std::string& modifications)
|
|
: type_(SUB_FILE)
|
|
, filename_(filename)
|
|
, modifications_(modifications)
|
|
, loc_(loc)
|
|
, center_x_(center_x)
|
|
, center_y_(center_y)
|
|
{
|
|
}
|
|
|
|
bool locator::operator==(const locator& a) const
|
|
{
|
|
if(a.type_ != type_) {
|
|
return false;
|
|
} else if(type_ == FILE) {
|
|
return filename_ == a.filename_;
|
|
} else if(type_ == SUB_FILE) {
|
|
return std::tie(filename_, loc_, modifications_, center_x_, center_y_) ==
|
|
std::tie(a.filename_, a.loc_, a.modifications_, a.center_x_, a.center_y_);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool locator::operator<(const locator& a) const
|
|
{
|
|
if(type_ != a.type_) {
|
|
return type_ < a.type_;
|
|
} else if(type_ == FILE) {
|
|
return filename_ < a.filename_;
|
|
} else if(type_ == SUB_FILE) {
|
|
return std::tie(filename_, loc_, modifications_, center_x_, center_y_) <
|
|
std::tie(a.filename_, a.loc_, a.modifications_, a.center_x_, a.center_y_);
|
|
}
|
|
|
|
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;
|
|
const std::string& name = loc.get_filename();
|
|
|
|
auto location = filesystem::get_binary_file_location("images", name);
|
|
|
|
// Many images have been converted from PNG to WEBP format,
|
|
// but the old filename may still be saved in savegame files etc.
|
|
// If the file does not exist in ".png" format, also try ".webp".
|
|
// Similarly for ".jpg", which conveniently has the same number of letters as ".png".
|
|
if(!location && (boost::algorithm::ends_with(name, ".png") || boost::algorithm::ends_with(name, ".jpg"))) {
|
|
std::string webp_name = name.substr(0, name.size() - 4) + ".webp";
|
|
location = filesystem::get_binary_file_location("images", webp_name);
|
|
if(location) {
|
|
WRN_IMG << "Replaced missing '" << name << "' with found '"
|
|
<< webp_name << "'.";
|
|
}
|
|
}
|
|
|
|
{
|
|
if(location) {
|
|
// Check if there is a localized image.
|
|
const auto loc_location = filesystem::get_localized_path(location.value());
|
|
if(loc_location) {
|
|
location = loc_location.value();
|
|
}
|
|
|
|
filesystem::rwops_ptr rwops = filesystem::make_read_RWops(location.value());
|
|
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) {
|
|
const auto ovr_location = filesystem::get_localized_path(location.value(), "--overlay");
|
|
if(ovr_location) {
|
|
add_localized_overlay(ovr_location.value(), res);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(!res && !name.empty()) {
|
|
ERR_IMG << "could not open image '" << name << "'";
|
|
if(game_config::debug && name != game_config::images::missing)
|
|
return get_surface(game_config::images::missing, UNSCALED);
|
|
if(name != game_config::images::blank)
|
|
return get_surface(game_config::images::blank, UNSCALED);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
static surface load_image_sub_file(const image::locator& loc)
|
|
{
|
|
// Create a new surface in-memory on which to apply the modifications
|
|
surface surf = get_surface(loc.get_filename(), UNSCALED).clone();
|
|
if(surf == nullptr) {
|
|
return nullptr;
|
|
}
|
|
|
|
modification_queue mods = modification::decode(loc.get_modifications());
|
|
|
|
while(!mods.empty()) {
|
|
modification* mod = mods.top();
|
|
|
|
try {
|
|
std::invoke(*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;
|
|
}
|
|
|
|
// NOTE: do this *after* applying the mod or you'll get crashes!
|
|
mods.pop();
|
|
}
|
|
|
|
if(loc.get_loc().valid()) {
|
|
rect srcrect(
|
|
((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;
|
|
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;
|
|
} else {
|
|
surf = cut;
|
|
}
|
|
|
|
is_empty_hex_.add_to_cache(loc, 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) {
|
|
std::string_view fn = loc.get_filename();
|
|
std::string_view stripped = fn.substr(0, fn.find(","));
|
|
ERR_IMG << "Invalid data URI: " << stripped;
|
|
} else if(parsed.mime.substr(0, 5) != "image") {
|
|
ERR_IMG << "Data URI not of image MIME type: " << parsed.mime;
|
|
} else {
|
|
const std::vector<uint8_t> image_data = base64::decode(parsed.data);
|
|
filesystem::rwops_ptr rwops{SDL_RWFromConstMem(image_data.data(), image_data.size())};
|
|
|
|
if(image_data.empty()) {
|
|
ERR_IMG << "Invalid encoding in data URI";
|
|
} 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_IMG << "Invalid image MIME type: " << parsed.mime;
|
|
}
|
|
}
|
|
|
|
return surf;
|
|
}
|
|
|
|
// small utility function to store an int from (-256,254) to an signed char
|
|
static int8_t col_to_uchar(int i)
|
|
{
|
|
return static_cast<int8_t>(std::clamp(i / 2, -128, 127));
|
|
}
|
|
|
|
light_string get_light_string(int op, int r, int g, int b)
|
|
{
|
|
return {
|
|
static_cast<int8_t>(op),
|
|
col_to_uchar(r),
|
|
col_to_uchar(g),
|
|
col_to_uchar(b),
|
|
};
|
|
}
|
|
|
|
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;
|
|
adjust_surface_color(surf, ls[1] * m, ls[2] * m, ls[3] * m);
|
|
return surf;
|
|
}
|
|
|
|
// check if the lightmap is already cached or need to be generated
|
|
surface lightmap = nullptr;
|
|
auto i = surface_lightmaps_.find(ls);
|
|
if(i != surface_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);
|
|
|
|
// 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
|
|
surface_lightmaps_[ls] = lightmap;
|
|
}
|
|
|
|
// apply the final lightmap
|
|
light_surface(surf, lightmap);
|
|
return surf;
|
|
}
|
|
|
|
static 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;
|
|
surfaces_[TOD_COLORED].flush();
|
|
lit_surfaces_.flush();
|
|
lit_textures_.flush();
|
|
texture_tod_colored_.clear();
|
|
}
|
|
}
|
|
|
|
static surface get_hexed(const locator& i_locator, bool skip_cache = false)
|
|
{
|
|
surface image = get_surface(i_locator, UNSCALED, skip_cache).clone();
|
|
surface mask = get_hexmask();
|
|
// Ensure the image is the correct size by cropping and/or centering.
|
|
// TODO: this should probably be a function of sdl/utils
|
|
if(image && (image->w != mask->w || image->h != mask->h)) {
|
|
DBG_IMG << "adjusting [" << image->w << ',' << image->h << ']'
|
|
<< " image to hex mask: " << i_locator;
|
|
// the fitted surface
|
|
surface fit(mask->w, mask->h);
|
|
// if the image is too large in either dimension, crop it.
|
|
if(image->w > mask->w || image->h >= mask->h) {
|
|
// fill the crop surface with transparency
|
|
SDL_FillRect(fit, nullptr, SDL_MapRGBA(fit->format, 0, 0, 0, 0));
|
|
// crop the input image to hexmask dimensions
|
|
int cutx = std::max(0, image->w - mask->w) / 2;
|
|
int cuty = std::max(0, image->h - mask->h) / 2;
|
|
int cutw = std::min(image->w, mask->w);
|
|
int cuth = std::min(image->h, mask->h);
|
|
image = cut_surface(image, {cutx, cuty, cutw, cuth});
|
|
// image will now have dimensions <= mask
|
|
}
|
|
// center image
|
|
int placex = (mask->w - image->w) / 2;
|
|
int placey = (mask->h - image->h) / 2;
|
|
rect dst = {placex, placey, image->w, image->h};
|
|
sdl_blit(image, nullptr, fit, &dst);
|
|
image = fit;
|
|
}
|
|
// hex cut tiles, also check and cache if empty result
|
|
bool is_empty = false;
|
|
mask_surface(image, mask, &is_empty, i_locator.get_filename());
|
|
is_empty_hex_.add_to_cache(i_locator, is_empty);
|
|
return image;
|
|
}
|
|
|
|
static surface get_tod_colored(const locator& i_locator, bool skip_cache = false)
|
|
{
|
|
surface img = get_surface(i_locator, HEXED, skip_cache).clone();
|
|
adjust_surface_color(img, red_adjust, green_adjust, blue_adjust);
|
|
return img;
|
|
}
|
|
|
|
/** translate type to a simpler one when possible */
|
|
static TYPE simplify_type(const image::locator& i_locator, TYPE type)
|
|
{
|
|
if(type == TOD_COLORED) {
|
|
if(red_adjust == 0 && green_adjust == 0 && blue_adjust == 0) {
|
|
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_surface(
|
|
const image::locator& i_locator,
|
|
TYPE type,
|
|
bool skip_cache)
|
|
{
|
|
surface res;
|
|
|
|
if(i_locator.is_void()) {
|
|
return res;
|
|
}
|
|
|
|
type = simplify_type(i_locator, type);
|
|
|
|
// select associated cache
|
|
if(type >= NUM_TYPES) {
|
|
WRN_IMG << "get_surface called with unknown image type";
|
|
return res;
|
|
}
|
|
surface_cache& imap = surfaces_[type];
|
|
|
|
// return the image if already cached
|
|
if(const surface* cached_surf = imap.locate_in_cache(i_locator)) {
|
|
return *cached_surf;
|
|
} else {
|
|
DBG_IMG << "surface cache [" << type << "] miss: " << i_locator;
|
|
}
|
|
|
|
// 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, skip_cache);
|
|
break;
|
|
case HEXED:
|
|
res = get_hexed(i_locator, skip_cache);
|
|
break;
|
|
default:
|
|
throw game::error("get_surface somehow lost image type?");
|
|
}
|
|
|
|
bool_cache& skip = skipped_cache_[type];
|
|
|
|
// In cache...
|
|
if(const bool* cached_value = skip.locate_in_cache(i_locator)) {
|
|
// ... and cached as true
|
|
if(*cached_value) {
|
|
DBG_IMG << "duplicate load: " << i_locator
|
|
<< " [" << type << "]"
|
|
<< " (" << duplicate_loads_ << "/" << total_loads_ << " total)";
|
|
++duplicate_loads_;
|
|
}
|
|
}
|
|
|
|
++total_loads_;
|
|
|
|
if(skip_cache) {
|
|
DBG_IMG << "surface cache [" << type << "] skip: " << i_locator;
|
|
skip.add_to_cache(i_locator, true);
|
|
} else {
|
|
imap.add_to_cache(i_locator, res);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
surface get_lighted_image(const image::locator& i_locator, const light_string& ls)
|
|
{
|
|
if(i_locator.is_void()) {
|
|
return {};
|
|
}
|
|
|
|
lit_surface_variants& lvar = lit_surfaces_.access_in_cache(i_locator);
|
|
|
|
// Check the matching list_string variants for this locator
|
|
if(auto lvi = lvar.find(ls); lvi != lvar.end()) {
|
|
return lvi->second;
|
|
}
|
|
|
|
DBG_IMG << "lit surface cache miss: " << i_locator;
|
|
|
|
// not cached yet, generate it
|
|
surface res = apply_light(get_surface(i_locator, HEXED).clone(), ls);
|
|
|
|
// record the lighted surface in the corresponding variants cache
|
|
lvar[ls] = res;
|
|
|
|
return res;
|
|
}
|
|
|
|
texture get_lighted_texture(
|
|
const image::locator& i_locator,
|
|
const light_string& ls)
|
|
{
|
|
if(i_locator.is_void()) {
|
|
return texture();
|
|
}
|
|
|
|
lit_texture_variants& lvar = lit_textures_.access_in_cache(i_locator);
|
|
|
|
// Check the matching list_string variants for this locator
|
|
if(auto lvi = lvar.find(ls); lvi != lvar.end()) {
|
|
return lvi->second;
|
|
}
|
|
|
|
DBG_IMG << "lit texture cache miss: " << i_locator;
|
|
|
|
// not cached yet, generate it
|
|
texture tex(get_lighted_image(i_locator, ls));
|
|
|
|
// record the lighted texture in the corresponding variants cache
|
|
lvar[ls] = tex;
|
|
|
|
return tex;
|
|
}
|
|
|
|
surface get_hexmask()
|
|
{
|
|
static const image::locator terrain_mask(game_config::images::terrain_mask);
|
|
return get_surface(terrain_mask, UNSCALED);
|
|
}
|
|
|
|
point get_size(const locator& i_locator, bool skip_cache)
|
|
{
|
|
if(const surface s = get_surface(i_locator, UNSCALED, skip_cache)) {
|
|
return {s->w, s->h};
|
|
} else {
|
|
return {0, 0};
|
|
}
|
|
}
|
|
|
|
bool is_in_hex(const locator& i_locator)
|
|
{
|
|
if(const bool* cached_value = in_hex_info_.locate_in_cache(i_locator)) {
|
|
return *cached_value;
|
|
} else {
|
|
bool res = in_mask_surface(get_surface(i_locator, UNSCALED), get_hexmask());
|
|
in_hex_info_.add_to_cache(i_locator, res);
|
|
return res;
|
|
}
|
|
}
|
|
|
|
bool is_empty_hex(const locator& i_locator)
|
|
{
|
|
if(const bool* cached_value = is_empty_hex_.locate_in_cache(i_locator)) {
|
|
return *cached_value;
|
|
}
|
|
|
|
surface surf = get_surface(i_locator, HEXED);
|
|
|
|
// Empty state should be cached during surface fetch. Let's check again
|
|
if(const bool* cached_value = is_empty_hex_.locate_in_cache(i_locator)) {
|
|
return *cached_value;
|
|
}
|
|
|
|
// Should never reach this point, but let's manually do it anyway.
|
|
surf = surf.clone();
|
|
bool is_empty = false;
|
|
mask_surface(surf, get_hexmask(), &is_empty);
|
|
is_empty_hex_.add_to_cache(i_locator, is_empty);
|
|
return is_empty;
|
|
}
|
|
|
|
bool exists(const image::locator& i_locator)
|
|
{
|
|
if(i_locator.is_void()) {
|
|
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, 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()).has_value();
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
for(const auto& p : filesystem::get_binary_paths("images")) {
|
|
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_surface(i_locator), filename);
|
|
}
|
|
|
|
save_result save_image(const surface& surf, const std::string& filename)
|
|
{
|
|
if(!surf) {
|
|
return save_result::no_image;
|
|
}
|
|
|
|
if(boost::algorithm::ends_with(filename, ".jpeg") || boost::algorithm::ends_with(filename, ".jpg") || boost::algorithm::ends_with(filename, ".jpe")) {
|
|
LOG_IMG << "Writing a JPG image to " << filename;
|
|
|
|
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(boost::algorithm::ends_with(filename, ".png")) {
|
|
LOG_IMG << "Writing a PNG image to " << filename;
|
|
|
|
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;
|
|
}
|
|
|
|
return save_result::unsupported_format;
|
|
}
|
|
|
|
/*
|
|
* TEXTURE INTERFACE ======================================================================
|
|
*
|
|
* The only important difference here is that textures must have their
|
|
* scale quality set before creation. All other handling is done by
|
|
* get_surface.
|
|
*/
|
|
|
|
texture get_texture(const image::locator& i_locator, TYPE type, bool skip_cache)
|
|
{
|
|
return get_texture(i_locator, scale_quality::nearest, type, skip_cache);
|
|
}
|
|
|
|
/** Returns a texture for the corresponding image. */
|
|
texture get_texture(const image::locator& i_locator, scale_quality quality, TYPE type, bool skip_cache)
|
|
{
|
|
texture res;
|
|
|
|
if(i_locator.is_void()) {
|
|
return res;
|
|
}
|
|
|
|
type = simplify_type(i_locator, type);
|
|
|
|
//
|
|
// Select the appropriate cache. We don't need caches for every single image types,
|
|
// since some types can be handled by render-time operations.
|
|
//
|
|
texture_cache* cache = nullptr;
|
|
|
|
switch(type) {
|
|
case HEXED:
|
|
cache = &textures_hexed_[quality];
|
|
break;
|
|
case TOD_COLORED:
|
|
cache = &texture_tod_colored_[quality];
|
|
break;
|
|
default:
|
|
cache = &textures_[quality];
|
|
}
|
|
|
|
//
|
|
// Now attempt to find a cached texture. If found, return it.
|
|
//
|
|
if(const texture* cached_texture = cache->locate_in_cache(i_locator)) {
|
|
return *cached_texture;
|
|
} else {
|
|
DBG_IMG << "texture cache [" << type << "] miss: " << i_locator;
|
|
}
|
|
|
|
//
|
|
// No texture was cached. In that case, create a new one. The explicit cases require special
|
|
// handling with surfaces in order to generate the desired effect. This shouldn't be the case
|
|
// once we get OGL and shader support.
|
|
//
|
|
|
|
// Get it from the surface cache, also setting the desired scale quality.
|
|
const bool linear_scaling = quality == scale_quality::linear ? true : false;
|
|
if(i_locator.get_modifications().empty()) {
|
|
// skip cache if we're loading plain files with no modifications
|
|
res = texture(get_surface(i_locator, type, true), linear_scaling);
|
|
} else {
|
|
res = texture(get_surface(i_locator, type, skip_cache), linear_scaling);
|
|
}
|
|
|
|
// Cache the texture.
|
|
if(skip_cache) {
|
|
DBG_IMG << "texture cache [" << type << "] skip: " << i_locator;
|
|
} else {
|
|
cache->add_to_cache(i_locator, res);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
} // end namespace image
|