Files
2026-02-02 04:50:13 +01:00

734 lines
22 KiB
C++

/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* 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 3 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; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "ultima/ultima4/gfx/image.h"
#include "ultima/ultima4/gfx/imageloader_u4.h"
#include "ultima/ultima4/gfx/imageloader_fmtowns.h"
#include "ultima/ultima4/gfx/imagemgr.h"
#include "ultima/ultima4/controllers/intro_controller.h"
#include "ultima/ultima4/core/config.h"
#include "ultima/ultima4/core/settings.h"
#include "ultima/ultima4/ultima4.h"
#include "image/png.h"
namespace Ultima {
namespace Ultima4 {
bool ImageInfo::hasBlackBackground() {
return this->_filetype == "image/x-u4raw";
}
class ImageSet {
public:
~ImageSet();
Common::String _name;
Common::String _location;
Common::String _extends;
Common::HashMap<Common::String, ImageInfo *> _info;
};
ImageMgr *ImageMgr::_instance = nullptr;
ImageMgr *ImageMgr::getInstance() {
if (_instance == nullptr) {
_instance = new ImageMgr();
_instance->init();
}
return _instance;
}
void ImageMgr::destroy() {
if (_instance != nullptr) {
delete _instance;
_instance = nullptr;
}
}
ImageMgr::ImageMgr() : _baseSet(nullptr), _abyssData(nullptr) {
settings.addObserver(this);
}
ImageMgr::~ImageMgr() {
settings.deleteObserver(this);
for (auto &i : _imageSets)
delete i._value;
delete[] _abyssData;
}
void ImageMgr::init() {
/*
* register the "screen" image representing the entire screen
*/
Image *screen = Image::createScreenImage();
_screenInfo._name = "screen";
_screenInfo._filename = "";
_screenInfo._width = screen->width();
_screenInfo._height = screen->height();
_screenInfo._depth = 0;
_screenInfo._prescale = 0;
_screenInfo._filetype = "";
_screenInfo._tiles = 0;
_screenInfo._introOnly = false;
_screenInfo._transparentIndex = -1;
_screenInfo._xu4Graphic = false;
_screenInfo._fixup = FIXUP_NONE;
_screenInfo._image = screen;
/*
* register all the images declared in the config files
*/
const Config *config = Config::getInstance();
Std::vector<ConfigElement> graphicsConf = config->getElement("graphics").getChildren();
for (const auto &conf : graphicsConf) {
if (conf.getName() == "imageset") {
ImageSet *set = loadImageSetFromConf(conf);
_imageSets[set->_name] = set;
// all image sets include the "screen" image
set->_info[_screenInfo._name] = &_screenInfo;
}
}
_imageSetNames.clear();
for (const auto &set : _imageSets)
_imageSetNames.push_back(set._key);
update(&settings);
}
ImageSet *ImageMgr::loadImageSetFromConf(const ConfigElement &conf) {
ImageSet *set;
set = new ImageSet();
set->_name = conf.getString("name");
set->_location = conf.getString("location");
set->_extends = conf.getString("extends");
Std::vector<ConfigElement> children = conf.getChildren();
for (const auto &i : children) {
if (i.getName() == "image") {
ImageInfo *info = loadImageInfoFromConf(i);
if (set->_info.contains(info->_name))
delete set->_info[info->_name];
set->_info[info->_name] = info;
}
}
return set;
}
ImageInfo *ImageMgr::loadImageInfoFromConf(const ConfigElement &conf) {
ImageInfo *info;
static const char *const fixupEnumStrings[] = { "none", "intro", "abyss", "abacus", "dungns", "blackTransparencyHack", "fmtownsscreen", nullptr };
info = new ImageInfo();
info->_name = conf.getString("name");
info->_filename = conf.getString("filename");
info->_width = conf.getInt("width", -1);
info->_height = conf.getInt("height", -1);
info->_depth = conf.getInt("depth", -1);
info->_prescale = conf.getInt("prescale");
info->_filetype = conf.getString("filetype");
info->_tiles = conf.getInt("tiles");
info->_introOnly = conf.getBool("introOnly");
info->_transparentIndex = conf.getInt("transparentIndex", -1);
info->_xu4Graphic = conf.getBool("xu4Graphic");
info->_fixup = static_cast<ImageFixup>(conf.getEnum("fixup", fixupEnumStrings));
info->_image = nullptr;
Std::vector<ConfigElement> children = conf.getChildren();
for (const auto &i : children) {
if (i.getName() == "subimage") {
SubImage *subimage = loadSubImageFromConf(info, i);
info->_subImages[subimage->_name] = subimage;
}
}
return info;
}
SubImage *ImageMgr::loadSubImageFromConf(const ImageInfo *info, const ConfigElement &conf) {
SubImage *subimage;
static int x = 0,
y = 0,
last_width = 0,
last_height = 0;
subimage = new SubImage();
subimage->_name = conf.getString("name");
subimage->setWidth(conf.getInt("width"));
subimage->setHeight(conf.getInt("height"));
subimage->_srcImageName = info->_name;
if (conf.exists("x") && conf.exists("y")) {
x = conf.getInt("x");
y = conf.getInt("y");
subimage->moveTo(x, y);
} else {
// Automatically increment our position through the base image
x += last_width;
if (x >= last_width) {
x = 0;
y += last_height;
}
subimage->moveTo(x, y);
}
// "remember" the width and height of this subimage
last_width = subimage->width();
last_height = subimage->height();
return subimage;
}
void ImageMgr::fixupIntro(Image *im, int prescale) {
const byte *sigData;
int i, x, y;
bool alpha = im->isAlphaOn();
RGBA color;
sigData = g_intro->getSigData();
im->alphaOff();
if (settings._videoType != "VGA-ALLPNG" && settings._videoType != "new") {
/* ----------------------------
* update the position of "and"
* ---------------------------- */
im->drawSubRectOn(im, 148 * prescale, 17 * prescale,
153 * prescale,
17 * prescale,
11 * prescale,
4 * prescale);
im->drawSubRectOn(im, 159 * prescale, 17 * prescale,
165 * prescale,
18 * prescale,
1 * prescale,
4 * prescale);
im->drawSubRectOn(im, 160 * prescale, 17 * prescale,
164 * prescale,
17 * prescale,
16 * prescale,
4 * prescale);
/* ---------------------------------------------
* update the position of "Origin Systems, Inc."
* --------------------------------------------- */
im->drawSubRectOn(im, 86 * prescale, 21 * prescale,
88 * prescale,
21 * prescale,
114 * prescale,
9 * prescale);
im->drawSubRectOn(im, 199 * prescale, 21 * prescale,
202 * prescale,
21 * prescale,
6 * prescale,
9 * prescale);
im->drawSubRectOn(im, 207 * prescale, 21 * prescale,
208 * prescale,
21 * prescale,
28 * prescale,
9 * prescale);
/* ---------------------------------------------
* update the position of "Ultima IV"
* --------------------------------------------- */
// move this *prior* to moving "present"
im->drawSubRectOn(im, 59 * prescale, 33 * prescale,
61 * prescale,
33 * prescale,
204 * prescale,
46 * prescale);
/* ---------------------------------------------
* update the position of "Quest of the Avatar"
* --------------------------------------------- */
im->drawSubRectOn(im, 69 * prescale, 80 * prescale, // quEst
70 * prescale,
80 * prescale,
11 * prescale,
13 * prescale);
im->drawSubRectOn(im, 82 * prescale, 80 * prescale, // queST
84 * prescale,
80 * prescale,
27 * prescale,
13 * prescale);
im->drawSubRectOn(im, 131 * prescale, 80 * prescale, // oF
132 * prescale,
80 * prescale,
11 * prescale,
13 * prescale);
im->drawSubRectOn(im, 150 * prescale, 80 * prescale, // THE
149 * prescale,
80 * prescale,
40 * prescale,
13 * prescale);
im->drawSubRectOn(im, 166 * prescale, 80 * prescale, // tHe
165 * prescale,
80 * prescale,
11 * prescale,
13 * prescale);
im->drawSubRectOn(im, 200 * prescale, 80 * prescale, // AVATAR
201 * prescale,
80 * prescale,
81 * prescale,
13 * prescale);
im->drawSubRectOn(im, 227 * prescale, 80 * prescale, // avAtar
228 * prescale,
80 * prescale,
11 * prescale,
13 * prescale);
}
/* -----------------------------------------------------------------------------
* copy "present" to new location between "Origin Systems, Inc." and "Ultima IV"
* ----------------------------------------------------------------------------- */
// do this *after* moving "Ultima IV"
im->drawSubRectOn(im, 132 * prescale, 33 * prescale,
135 * prescale,
0 * prescale,
56 * prescale,
5 * prescale);
if (alpha) {
im->alphaOn();
}
/* ----------------------------
* erase the original "present"
* ---------------------------- */
im->fillRect(135 * prescale, 0 * prescale, 56 * prescale, 5 * prescale, 0, 0, 0);
/* -------------------------
* update the colors for VGA
* ------------------------- */
if (settings._videoType == "VGA") {
ImageInfo *borderInfo = imageMgr->get(BKGD_BORDERS, true);
// ImageInfo *charsetInfo = imageMgr->get(BKGD_CHARSET);
if (!borderInfo)
error("ERROR 1001: Unable to load the \"%s\" data file", BKGD_BORDERS);
delete borderInfo->_image;
borderInfo->_image = nullptr;
borderInfo = imageMgr->get(BKGD_BORDERS, true);
im->setPaletteFromImage(borderInfo->_image);
// update the color of "and" and "present"
(void)im->setPaletteIndex(15, 226, 226, 255);
// update the color of "Origin Systems, Inc."
(void)im->setPaletteIndex(9, 129, 129, 255);
#ifdef TODO
borderInfo->_image->save("border.png");
#endif
// update the border appearance
borderInfo->_image->alphaOff();
borderInfo->_image->drawSubRectOn(im, 0, 96, 0, 0, 16, 56);
for (i = 0; i < 9; i++) {
borderInfo->_image->drawSubRectOn(im, 16 + (i * 32), 96, 144, 0, 48, 48);
}
im->drawSubRectInvertedOn(im, 0, 144, 0, 104, 320, 40);
im->drawSubRectOn(im, 0, 184, 0, 96, 320, 8);
borderInfo->_image->alphaOn();
delete borderInfo->_image;
borderInfo->_image = nullptr;
}
/* -----------------------------
* draw "Lord British" signature
* ----------------------------- */
color = im->setColor(0, 255, 255); // cyan for EGA
int blue[16] = {255, 250, 226, 226, 210, 194, 161, 161,
129, 97, 97, 64, 64, 32, 32, 0
};
i = 0;
while (sigData[i] != 0) {
/* (x/y) are unscaled coordinates, i.e. in 320x200 */
x = sigData[i] + 0x14;
y = 0xBF - sigData[i + 1];
if (settings._videoType != "EGA") {
// yellow gradient
color = im->setColor(255, (y == 1 ? 250 : 255), blue[y]);
}
im->fillRect(x * prescale, y * prescale,
2 * prescale, prescale,
color.r, color.g, color.b);
i += 2;
}
/* --------------------------------------------------------------
* draw the red line between "Origin Systems, Inc." and "present"
* -------------------------------------------------------------- */
/* we're still working with an unscaled surface */
if (settings._videoType != "EGA") {
color = im->setColor(0, 0, 161); // dark blue
} else {
color = im->setColor(128, 0, 0); // dark red for EGA
}
for (i = 84; i < 236; i++) // 152 px wide
im->fillRect(i * prescale, 31 * prescale,
prescale, prescale,
color.r, color.g, color.b);
}
void ImageMgr::fixupAbyssVision(Image *im, int prescale) {
// Ignore fixups for xu4 PNG images - they're already correct
if (im->isIndexed())
return;
/*
* Each VGA vision components must be XORed with all the previous
* vision components to get the actual image.
*/
if (_abyssData) {
for (int y = 0; y < im->height(); y++) {
for (int x = 0; x < im->width(); x++) {
uint index;
im->getPixelIndex(x, y, index);
index ^= _abyssData[y * im->width() + x];
im->putPixelIndex(x, y, index);
}
}
} else {
_abyssData = new uint[im->width() * im->height()];
}
for (int y = 0; y < im->height(); y++) {
for (int x = 0; x < im->width(); x++) {
uint index;
im->getPixelIndex(x, y, index);
_abyssData[y * im->width() + x] = index;
}
}
}
void ImageMgr::fixupAbacus(Image *im, int prescale) {
/*
* surround each bead with a row green pixels to avoid artifacts
* when scaling
*/
im->fillRect(7 * prescale, 186 * prescale, prescale, 14 * prescale, 0, 255, 80); /* green */
im->fillRect(16 * prescale, 186 * prescale, prescale, 14 * prescale, 0, 255, 80); /* green */
im->fillRect(8 * prescale, 186 * prescale, prescale * 8, prescale, 0, 255, 80); /* green */
im->fillRect(8 * prescale, 199 * prescale, prescale * 8, prescale, 0, 255, 80); /* green */
im->fillRect(23 * prescale, 186 * prescale, prescale, 14 * prescale, 0, 255, 80); /* green */
im->fillRect(32 * prescale, 186 * prescale, prescale, 14 * prescale, 0, 255, 80); /* green */
im->fillRect(24 * prescale, 186 * prescale, prescale * 8, prescale, 0, 255, 80); /* green */
im->fillRect(24 * prescale, 199 * prescale, prescale * 8, prescale, 0, 255, 80); /* green */
}
void ImageMgr::fixupDungNS(Image *im, int prescale) {
for (int y = 0; y < im->height(); y++) {
for (int x = 0; x < im->width(); x++) {
uint index;
im->getPixelIndex(x, y, index);
if (index == 1)
im->putPixelIndex(x, y, 2);
else if (index == 2)
im->putPixelIndex(x, y, 1);
}
}
}
void ImageMgr::fixupFMTowns(Image *im, int prescale) {
for (int y = 20; y < im->height(); y++) {
for (int x = 0; x < im->width(); x++) {
uint index;
im->getPixelIndex(x, y, index);
im->putPixelIndex(x, y - 20, index);
}
}
}
ImageSet *ImageMgr::getSet(const Common::String &setname) {
Common::HashMap<Common::String, ImageSet *>::iterator i = _imageSets.find(setname);
if (i != _imageSets.end())
return i->_value;
else
return nullptr;
}
ImageInfo *ImageMgr::getInfo(const Common::String &name) {
return getInfoFromSet(name, _baseSet);
}
ImageInfo *ImageMgr::getInfoFromSet(const Common::String &name, ImageSet *imageset) {
if (!imageset)
return nullptr;
/* if the image set contains the image we want, AND IT EXISTS we are done */
Common::HashMap<Common::String, ImageInfo *>::iterator i = imageset->_info.find(name);
if (i != imageset->_info.end())
if (imageExists(i->_value))
return i->_value;
/* otherwise if this image set extends another, check the base image set */
while (imageset->_extends != "") {
imageset = getSet(imageset->_extends);
return getInfoFromSet(name, imageset);
}
//warning("Searched recursively from imageset %s through to %s and couldn't find %s", baseSet->name.c_str(), imageset->name.c_str(), name.c_str());
return nullptr;
}
Common::String ImageMgr::guessFileType(const Common::String &filename) {
if (filename.size() >= 4 && filename.hasSuffixIgnoreCase(".png")) {
return "image/png";
} else {
return "";
}
}
bool ImageMgr::imageExists(ImageInfo *info) {
if (info->_filename.empty()) //If it is an abstract image like "screen"
return true;
Common::File *file = getImageFile(info);
if (file) {
delete file;
return true;
}
return false;
}
Common::File *ImageMgr::getImageFile(ImageInfo *info) {
Common::String filename = info->_filename;
if (filename.empty())
return nullptr;
Common::File *file = new Common::File();
if (!info->_xu4Graphic) {
// It's a file in the game folder
if (file->open(Common::Path(filename)))
return file;
}
if (file->open(Common::Path("data/graphics/").joinInPlace(filename)))
return file;
delete file;
return nullptr;
}
ImageInfo *ImageMgr::get(const Common::String &name, bool returnUnscaled) {
ImageInfo *info = getInfo(name);
if (!info)
return nullptr;
/* return if already loaded */
if (info->_image != nullptr)
return info;
Common::File *file = getImageFile(info);
Image *unscaled = nullptr;
if (file) {
if (info->_filetype.empty())
info->_filetype = guessFileType(info->_filename);
Common::String filetype = info->_filetype;
::Image::ImageDecoder *decoder = createDecoder(filetype, info->_width, info->_height, info->_depth);
if (decoder == nullptr) {
warning("can't find loader to load image \"%s\" with type \"%s\"", info->_filename.c_str(), filetype.c_str());
} else {
if (!decoder->loadStream(*file)) {
warning("can't load image \"%s\" with type \"%s\"", info->_filename.c_str(), filetype.c_str());
} else {
const Graphics::Surface *surface = decoder->getSurface();
unscaled = Image::create(surface->w, surface->h, surface->format);
unscaled->blitFrom(*surface);
const Graphics::Palette &pal = decoder->getPalette();
if (!pal.empty()) {
unscaled->setPalette(pal.data(), pal.size());
}
if (info->_width == -1) {
// Write in the values for later use.
info->_width = unscaled->width();
info->_height = unscaled->height();
// ### info->depth = ???
}
}
delete decoder;
}
delete file;
} else {
warning("Failed to open file %s for reading.", info->_filename.c_str());
return nullptr;
}
if (unscaled == nullptr)
return nullptr;
if (info->_transparentIndex != -1)
unscaled->setTransparentIndex(info->_transparentIndex);
if (info->_prescale == 0)
info->_prescale = 1;
/*
* fixup the image before scaling it
*/
switch (info->_fixup) {
case FIXUP_NONE:
break;
case FIXUP_INTRO:
fixupIntro(unscaled, info->_prescale);
break;
case FIXUP_ABYSS:
fixupAbyssVision(unscaled, info->_prescale);
break;
case FIXUP_ABACUS:
fixupAbacus(unscaled, info->_prescale);
break;
case FIXUP_DUNGNS:
fixupDungNS(unscaled, info->_prescale);
break;
case FIXUP_FMTOWNSSCREEN:
fixupFMTowns(unscaled, info->_prescale);
break;
case FIXUP_BLACKTRANSPARENCYHACK:
//Apply transparency shadow hack to ultima4 ega and vga upgrade classic graphics.
if (Settings::getInstance()._enhancements && Settings::getInstance()._enhancementsOptions._u4TileTransparencyHack) {
// TODO: Pick a more optimal pixel format?
Image *unscaled_original = unscaled;
unscaled = Image::duplicate(unscaled, Graphics::PixelFormat(4, 8, 8, 8, 8, 24, 16, 8, 0));
delete unscaled_original;
int transparency_shadow_size = Settings::getInstance()._enhancementsOptions._u4TrileTransparencyHackShadowBreadth;
int black_index = 0;
int opacity = Settings::getInstance()._enhancementsOptions._u4TileTransparencyHackPixelShadowOpacity;
int frames = info->_tiles;
for (int f = 0; f < frames; ++f)
unscaled->performTransparencyHack(black_index, frames, f, transparency_shadow_size, opacity);
}
break;
}
if (returnUnscaled) {
info->_image = unscaled;
return info;
}
int imageScale = settings._scale;
if ((settings._scale % info->_prescale) != 0) {
int orig_scale = settings._scale;
settings._scale = info->_prescale;
settings.write();
error("image %s is prescaled to an incompatible size: %d\nResetting the scale to %d. Sorry about the inconvenience, please restart.", info->_filename.c_str(), orig_scale, settings._scale);
}
imageScale /= info->_prescale;
info->_image = g_screen->screenScale(unscaled, imageScale, info->_tiles, 1);
delete unscaled;
return info;
}
SubImage *ImageMgr::getSubImage(const Common::String &name) {
Common::String setname;
ImageSet *set = _baseSet;
while (set != nullptr) {
for (auto &i : set->_info) {
ImageInfo *info = (ImageInfo *) i._value;
Common::HashMap<Common::String, SubImage *>::iterator j = info->_subImages.find(name);
if (j != info->_subImages.end())
return j->_value;
}
set = getSet(set->_extends);
}
return nullptr;
}
void ImageMgr::freeIntroBackgrounds() {
for (const auto &i : _imageSets) {
ImageSet *set = i._value;
for (auto &j : set->_info) {
ImageInfo *info = j._value;
if (info->_image != nullptr && info->_introOnly) {
delete info->_image;
info->_image = nullptr;
}
}
}
}
const Std::vector<Common::String> &ImageMgr::getSetNames() {
return _imageSetNames;
}
void ImageMgr::update(Settings *newSettings) {
Common::String setname;
setname = newSettings->_videoType;
_baseSet = getSet(setname);
}
::Image::ImageDecoder *ImageMgr::createDecoder(const Common::String &fileType, int width, int height, int bpp) {
if (fileType == "image/png")
return new ::Image::PNGDecoder();
if (fileType == "image/x-u4raw")
return new U4RawImageDecoder(width, height, bpp);
if (fileType == "image/x-u4rle")
return new U4RleImageDecoder(width, height, bpp);
if (fileType == "image/x-u4lzw")
return new U4LzwImageDecoder(width, height, bpp);
if (fileType == "image/fmtowns-tif")
return new FMTOWNSImageDecoder(width, height, bpp, 510);
return nullptr;
}
ImageSet::~ImageSet() {
for (const auto &i : _info) {
ImageInfo *imageInfo = i._value;
if (imageInfo->_name != "screen")
delete imageInfo;
}
}
ImageInfo::~ImageInfo() {
for (auto &i : _subImages)
delete i._value;
if (_image != nullptr)
delete _image;
}
} // End of namespace Ultima4
} // End of namespace Ultima