STARK: Add support for loading modded textures

This commit is contained in:
Bastien Bouclet 2019-06-02 17:13:14 +02:00
parent db33fb3e41
commit c95f711a8e
7 changed files with 402 additions and 5 deletions

View File

@ -511,6 +511,20 @@ mods for the game. These are the currently supported modding features:
for instance `mods/[my_mod]/08/02/xarc/011001.bik` is the animation where
the tree spirit lifts the egg back into the nest.
* Load replacement textures for the 3d models.
Each original `tm` file contains several textures, each with its
associated mipmaps. The replacement files are `zip` archives containing
`dds` packaged textures. The replacement archives must be placed at the root
of the mod directory and be named after the `tm` file they replace:
`mods/[my_mod]/april_waitress.tm.zip`.
Each `zip` archive must contain all the textures from the replaced `tm`
file. The textures need to be encoded in uncompressed RGB or RGBA `dds`
files with mipmaps. Files inside the archive must be named according
to the replaced texture name, but with the `bmp` extension replaced with
`dds`: `backdress-highres-battic.dds`
The `extractAllTextures` console command can be used to extract the `tm`
files to `png` files.
Contact us if you need further capabilities for your mod.
## 9. Bug reports

View File

@ -0,0 +1,199 @@
/* ResidualVM - A 3D game interpreter
*
* ResidualVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the AUTHORS
* 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 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; 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
#include "engines/stark/formats/dds.h"
#include "common/textconsole.h"
namespace Stark {
namespace Formats {
// Based on xoreos' DDS code
static const uint32 kDDSID = MKTAG('D', 'D', 'S', ' ');
static const uint32 kHeaderFlagsHasMipMaps = 0x00020000;
static const uint32 kPixelFlagsHasAlpha = 0x00000001;
static const uint32 kPixelFlagsHasFourCC = 0x00000004;
static const uint32 kPixelFlagsIsIndexed = 0x00000020;
static const uint32 kPixelFlagsIsRGB = 0x00000040;
DDS::~DDS() {
for (uint i = 0; i < _mipmaps.size(); i++) {
_mipmaps[i].free();
}
}
bool DDS::load(Common::SeekableReadStream &dds, const Common::String &name) {
assert(_mipmaps.empty());
_name = name;
if (!readHeader(dds)) {
return false;
}
return readData(dds);
}
const DDS::MipMaps &DDS::getMipMaps() const {
return _mipmaps;
}
bool DDS::readHeader(Common::SeekableReadStream &dds) {
// We found the FourCC of a standard DDS
uint32 magic = dds.readUint32BE();
if (magic != kDDSID) {
warning("Invalid DDS magic number: %d for %s", magic, _name.c_str());
return false;
}
// All DDS header should be 124 bytes (+ 4 for the FourCC)
uint32 headerSize = dds.readUint32LE();
if (headerSize != 124) {
warning("Invalid DDS header size: %d for %s", headerSize, _name.c_str());
return false;
}
// DDS features
uint32 flags = dds.readUint32LE();
// Image dimensions
uint32 height = dds.readUint32LE();
uint32 width = dds.readUint32LE();
if ((width >= 0x8000) || (height >= 0x8000)) {
warning("Unsupported DDS image dimensions (%ux%u) for %s", width, height, _name.c_str());
return false;
}
dds.skip(4 + 4); // Pitch + Depth
//uint32 pitchOrLineSize = dds.readUint32LE();
//uint32 depth = dds.readUint32LE();
uint32 mipMapCount = dds.readUint32LE();
// DDS doesn't provide any mip maps, only one full-size image
if ((flags & kHeaderFlagsHasMipMaps) == 0) {
mipMapCount = 1;
}
dds.skip(44); // Reserved
// Read the pixel data format
DDSPixelFormat format;
format.size = dds.readUint32LE();
format.flags = dds.readUint32LE();
format.fourCC = dds.readUint32BE();
format.bitCount = dds.readUint32LE();
format.rBitMask = dds.readUint32LE();
format.gBitMask = dds.readUint32LE();
format.bBitMask = dds.readUint32LE();
format.aBitMask = dds.readUint32LE();
// Detect which specific format it describes
if (!detectFormat(format)) {
return false;
}
dds.skip(16 + 4); // DDCAPS2 + Reserved
_mipmaps.resize(mipMapCount);
for (uint32 i = 0; i < mipMapCount; i++) {
_mipmaps[i].create(width, height, _format);
width >>= 1;
height >>= 1;
}
return true;
}
bool DDS::readData(Common::SeekableReadStream &dds) {
for (uint i = 0; i < _mipmaps.size(); i++) {
Graphics::Surface &mipmap = _mipmaps[i];
uint32 size = mipmap.pitch * mipmap.h;
uint32 readSize = dds.read(mipmap.getPixels(), size);
if (readSize != size) {
warning("Inconsistent read size in DDS file: %d, expected %d for %s level %d",
readSize, size, _name.c_str(), i);
return false;
}
}
return true;
}
bool DDS::detectFormat(const DDSPixelFormat &format) {
if (format.flags & kPixelFlagsHasFourCC) {
warning("Unsupported DDS feature: FourCC pixel format %d for %s", format.fourCC, _name.c_str());
return false;
}
if (format.flags & kPixelFlagsIsIndexed) {
warning("Unsupported DDS feature: Indexed %d-bits pixel format for %s", format.bitCount, _name.c_str());
return false;
}
if (!(format.flags & kPixelFlagsIsRGB)) {
warning("Only RGB DDS files are supported for %s", _name.c_str());
return false;
}
if (format.bitCount != 24 && format.bitCount != 32) {
warning("Only 24-bits and 32-bits DDS files are supported for %s", _name.c_str());
return false;
}
if ((format.flags & kPixelFlagsHasAlpha) &&
(format.bitCount == 32) &&
(format.rBitMask == 0x00FF0000) && (format.gBitMask == 0x0000FF00) &&
(format.bBitMask == 0x000000FF) && (format.aBitMask == 0xFF000000)) {
#ifdef SCUMM_BIG_ENDIAN
_format = Graphics::PixelFormat(4, 8, 8, 8, 8, 24, 0, 8, 16);
#else
_format = Graphics::PixelFormat(4, 8, 8, 8, 8, 16, 8, 0, 24);
#endif
return true;
} else if (!(format.flags & kPixelFlagsHasAlpha) &&
(format.bitCount == 24) &&
(format.rBitMask == 0x00FF0000) && (format.gBitMask == 0x0000FF00) &&
(format.bBitMask == 0x000000FF)) {
#ifdef SCUMM_BIG_ENDIAN
_format = Graphics::PixelFormat(3, 8, 8, 8, 0, 0, 8, 16, 0);
#else
_format = Graphics::PixelFormat(3, 8, 8, 8, 0, 16, 8, 0, 0);
#endif
return true;
} else {
warning("Unsupported pixel format (%X, %X, %d, %X, %X, %X, %X) for %s",
format.flags, format.fourCC, format.bitCount,
format.rBitMask, format.gBitMask, format.bBitMask, format.aBitMask,
_name.c_str());
return false;
}
}
} // End of namespace Formats
} // End of namespace Stark

101
engines/stark/formats/dds.h Normal file
View File

@ -0,0 +1,101 @@
/* ResidualVM - A 3D game interpreter
*
* ResidualVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the AUTHORS
* 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 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; 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
#ifndef STARK_FORMATS_DDS_H
#define STARK_FORMATS_DDS_H
#include "common/array.h"
#include "common/stream.h"
#include "graphics/surface.h"
namespace Stark {
namespace Formats {
// Based on xoreos' DDS code
/**
* DDS texture
*
* Only a very small subset of DDS features are supported. Especially,
* compressed formats are not supported. This class is meant to
* load a single DDS file per instance.
*/
class DDS {
public:
~DDS();
typedef Common::Array<Graphics::Surface> MipMaps;
/** Load a DDS texture from a stream */
bool load(Common::SeekableReadStream &dds, const Common::String &name);
/**
* Retrieve the mip map levels for a loaded texture
*
* The first mipmap is the full size image. Each further
* mipmap divides by two the with and the height of the
* previous one.
*/
const MipMaps &getMipMaps() const;
private:
/** The specific pixel format of the included image data. */
struct DDSPixelFormat {
/** The size of the image data in bytes */
uint32 size;
/** Features of the image data */
uint32 flags;
/** The FourCC to detect the format by */
uint32 fourCC;
/** Number of bits per pixel */
uint32 bitCount;
/** Bit mask for the red color component */
uint32 rBitMask;
/** Bit mask for the green color component */
uint32 gBitMask;
/** Bit mask for the blue color component */
uint32 bBitMask;
/** Bit mask for the alpha component */
uint32 aBitMask;
};
bool readHeader(Common::SeekableReadStream &dds);
bool readData(Common::SeekableReadStream &dds);
bool detectFormat(const DDSPixelFormat &format);
MipMaps _mipmaps;
Graphics::PixelFormat _format;
Common::String _name;
};
} // End of namespace Formats
} // End of namespace Stark
#endif // STARK_FORMATS_DDS_H

View File

@ -96,7 +96,7 @@ public:
const Texture *getTexture(const Common::String &name) const;
private:
typedef Common::HashMap<Common::String, Texture *> TextureMap;
typedef Common::HashMap<Common::String, Texture *, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo> TextureMap;
TextureMap _texMap;
};

View File

@ -16,6 +16,7 @@ MODULE_OBJS := \
gfx/texture.o \
formats/biff.o \
formats/biffmesh.o \
formats/dds.o \
formats/iss.o \
formats/tm.o \
formats/xarc.o \

View File

@ -22,13 +22,21 @@
#include "engines/stark/resources/textureset.h"
#include "engines/stark/debug.h"
#include "engines/stark/formats/dds.h"
#include "engines/stark/formats/tm.h"
#include "engines/stark/formats/xrc.h"
#include "engines/stark/gfx/driver.h"
#include "engines/stark/gfx/texture.h"
#include "engines/stark/services/archiveloader.h"
#include "engines/stark/services/services.h"
#include "engines/stark/services/settings.h"
#include "common/file.h"
#include "common/unzip.h"
#include "image/png.h"
namespace Stark {
@ -54,11 +62,17 @@ void TextureSet::readData(Formats::XRCReadStream *stream) {
}
void TextureSet::onPostRead() {
ArchiveReadStream *stream = StarkArchiveLoader->getFile(_filename, _archiveName);
if (StarkSettings->isAssetsModEnabled()) {
_textureSet = readOverrideDdsArchive();
}
_textureSet = Formats::TextureSetReader::read(stream);
if (!_textureSet) {
ArchiveReadStream *stream = StarkArchiveLoader->getFile(_filename, _archiveName);
delete stream;
_textureSet = Formats::TextureSetReader::read(stream);
delete stream;
}
}
static Common::String stripExtension(const Common::String &filename) {
@ -103,6 +117,73 @@ void TextureSet::extractArchive() {
delete stream;
}
Gfx::TextureSet *TextureSet::readOverrideDdsArchive() {
Common::String archiveName = _filename + ".zip";
debugC(kDebugModding, "Attempting to load %s", archiveName.c_str());
Common::Archive *archive = Common::makeZipArchive(archiveName);
if (!archive) {
return nullptr;
}
Common::ArchiveMemberList files;
archive->listMatchingMembers(files, "*.dds");
if (files.empty()) {
warning("No DDS files found in archive %s", archiveName.c_str());
delete archive;
return nullptr;
}
uint loadedCount = 0;
Gfx::TextureSet *textureSet = new Gfx::TextureSet();
for (Common::ArchiveMemberList::const_iterator it = files.begin(); it != files.end(); it++) {
const Common::String &name = (*it)->getName();
Common::SeekableReadStream *ddsStream = (*it)->createReadStream();
if (!ddsStream) {
warning("Unable to open %s for reading in %s", (*it)->getName().c_str(), archiveName.c_str());
continue;
}
Formats::DDS dds;
if (!dds.load(*ddsStream, name + " in " + archiveName)) {
delete ddsStream;
continue;
}
const Formats::DDS::MipMaps &mipmaps = dds.getMipMaps();
if (mipmaps.empty()) {
warning("No mipmaps in %s", name.c_str());
delete ddsStream;
continue;
}
Gfx::Texture *texture = StarkGfx->createTexture();
texture->setLevelCount(mipmaps.size());
for (uint i = 0; i < mipmaps.size(); i++) {
texture->addLevel(i, &mipmaps[i]);
}
// Remove the .dds extension, add .bmp to match the names
// used by the models.
Common::String textureName = Common::String(name.c_str(), name.size() - 4);
textureSet->addTexture(textureName + ".bmp", texture);
delete ddsStream;
loadedCount++;
}
debugC(kDebugModding, "Loaded %d textures from %s", loadedCount, archiveName.c_str());
delete archive;
return textureSet;
}
void TextureSet::printData() {
debug("filename: %s", _filename.c_str());
}

View File

@ -54,7 +54,7 @@ public:
};
TextureSet(Object *parent, byte subType, uint16 index, const Common::String &name);
virtual ~TextureSet();
~TextureSet() override;
// Resource API
void readData(Formats::XRCReadStream *stream) override;
@ -68,6 +68,7 @@ public:
protected:
void printData() override;
Gfx::TextureSet *readOverrideDdsArchive();
Common::String _filename;
Common::String _archiveName;