/*
Copyright (C) 2014 Sebastian Herbord. All rights reserved.
This file is part of Bsa Preview plugin for MO
Bsa Preview plugin 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.
Bsa Preview plugin 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 Bsa Preview plugin. If not, see .
*/
#include "previewbsa.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "simplefiletreeitem.h"
#include "simplefiletreemodel.h"
#include
#include
#include
using namespace MOBase;
PreviewBsa::PreviewBsa() : m_MOInfo(nullptr) {}
bool PreviewBsa::init(IOrganizer* moInfo)
{
m_MOInfo = moInfo;
auto bsaPreview = std::bind(&PreviewBsa::genBsaPreview, this, std::placeholders::_1,
std::placeholders::_2);
m_PreviewGenerators["bsa"] = bsaPreview;
m_PreviewGenerators["ba2"] = bsaPreview;
return true;
}
QString PreviewBsa::name() const
{
return "Preview Bsa";
}
QString PreviewBsa::localizedName() const
{
return tr("Preview BSA");
}
QString PreviewBsa::author() const
{
return "AL12 & MO2 Team";
}
QString PreviewBsa::description() const
{
return tr("Supports previewing contents of Bethesda Archive files, BSAs and BA2s");
}
MOBase::VersionInfo PreviewBsa::version() const
{
return VersionInfo(1, 1, 0, VersionInfo::RELEASE_FINAL);
}
QList PreviewBsa::settings() const
{
QList result;
result.push_back(PluginSetting(
"enabled", tr("Enable previewing of BSA file contents"), QVariant(true)));
return result;
}
std::set PreviewBsa::supportedExtensions() const
{
std::set extensions;
for (const auto& generator : m_PreviewGenerators) {
extensions.insert(generator.first);
}
return extensions;
}
QWidget* PreviewBsa::genFilePreview(const QString& fileName, const QSize& maxSize) const
{
auto iter = m_PreviewGenerators.find(QFileInfo(fileName).suffix().toLower());
if (iter != m_PreviewGenerators.end()) {
return iter->second(fileName, maxSize);
} else {
return nullptr;
}
}
void PreviewBsa::readFiles(const BSA::Folder::Ptr archiveFolder)
{
const auto fileCount = archiveFolder->getNumFiles();
for (unsigned int i = 0; i < fileCount; ++i) {
const BSA::File::Ptr file = archiveFolder->getFile(i);
m_Files << QString::fromStdString(file->getFilePath());
}
// recurse into subdirectories
const auto dirCount = archiveFolder->getNumSubFolders();
for (unsigned int i = 0; i < dirCount; ++i) {
const BSA::Folder::Ptr folder = archiveFolder->getSubFolder(i);
readFiles(folder);
}
}
QWidget* PreviewBsa::genBsaPreview(const QString& fileName, const QSize&)
{
m_Files.clear();
QWidget* wrapper = new QWidget();
QVBoxLayout* layout = new QVBoxLayout();
QLabel* infoLabel = new QLabel();
// Kept alive via shared_ptr captured by the double-click lambda below so
// we can extract files from the still-open archive on demand without
// re-reading it each time.
auto archive = std::make_shared();
BSA::EErrorCode res =
archive->read(fileName.toLocal8Bit().constData(), true);
if ((res != BSA::ERROR_NONE) && (res != BSA::ERROR_INVALIDHASHES)) {
log::error("invalid bsa '{}', error {}", fileName, res);
infoLabel->setText("Unable to parse archive. Unrecognized format.");
archive->close();
return wrapper;
}
const BSA::Folder::Ptr archiveDir = archive->getRoot();
readFiles(archiveDir);
QString infoString =
tr("Archive Format: %1 , Compression: %2 , File count: %3 , Version: %4 , "
"Archive type: %5 , Archive flags: %6");
infoString = infoString.arg(getFormat(archive->getType()))
.arg((archive->getFlags() & 0x00000004) ? tr("yes") : tr("no"))
.arg(m_Files.size())
.arg(getVersion(archive->getType()))
.arg(archive->getType())
.arg("0x" + QString::number(archive->getFlags(), 16));
infoLabel->setText(infoString);
layout->addWidget(infoLabel);
QTreeView* view = new QTreeView();
SimpleFileTreeModel* model = new SimpleFileTreeModel(m_Files, view);
view->setModel(model);
layout->addWidget(view);
QLineEdit* lineEdit = new QLineEdit();
layout->addWidget(lineEdit);
model->setFilterWidgetEdit(lineEdit);
model->setFilterWidgetList(view);
view->setSortingEnabled(true);
view->sortByColumn(0, Qt::SortOrder::AscendingOrder);
// Double-click: extract the selected file from the archive and hand the
// raw bytes to another preview plugin via IOrganizer::previewFileData.
// The view's model is wrapped in FilterWidgetProxyModel, so walk up via
// index.parent() + DisplayRole (proxy-transparent) instead of using
// index.internalPointer() directly.
IOrganizer* mo = m_MOInfo;
QObject::connect(
view, &QTreeView::doubleClicked, view,
[mo, archive, view](const QModelIndex& index) {
if (!index.isValid()) return;
// Ignore folders — only files have no children.
if (index.model()->rowCount(index) > 0) {
return;
}
QStringList parts;
QModelIndex cur = index;
while (cur.isValid()) {
parts.prepend(cur.data(Qt::DisplayRole).toString());
cur = cur.parent();
}
const QString path = parts.join('/');
if (path.isEmpty()) return;
BSA::File::Ptr file = archive->findFile(path.toStdString());
if (!file) {
log::warn("preview_bsa: file not found in archive: {}", path);
return;
}
std::vector data;
if (archive->extractToMemory(file, data) != BSA::ERROR_NONE) {
log::warn("preview_bsa: extract failed for: {}", path);
return;
}
QByteArray bytes(reinterpret_cast(data.data()),
static_cast(data.size()));
if (!mo->previewFileData(view->window(), path, bytes)) {
log::warn("preview_bsa: no preview plugin for: {}", path);
}
});
wrapper->setLayout(layout);
return wrapper;
}
QString PreviewBsa::getFormat(ArchiveType type) const
{
switch (type) {
case ArchiveType::TYPE_MORROWIND:
return "Morrowind";
case ArchiveType::TYPE_OBLIVION:
return "Oblivion";
case ArchiveType::TYPE_FALLOUT3:
return "Fallout 3, New Vegas, Skyrim LE";
case ArchiveType::TYPE_SKYRIMSE:
return "Skyrim Special Edition";
case ArchiveType::TYPE_FALLOUT4:
return "Fallout 4";
case ArchiveType::TYPE_STARFIELD:
return "Starfield";
case ArchiveType::TYPE_STARFIELD_LZ4_TEXTURE:
return "Starfield DDS LZ4";
case ArchiveType::TYPE_FALLOUT4NG_7:
case ArchiveType::TYPE_FALLOUT4NG_8:
return "Fallout 4 NextGen";
default:
throw data_invalid_exception(makeString("invalid type %d", type));
}
}
BSAULong PreviewBsa::getVersion(ArchiveType type) const
{
switch (type) {
case TYPE_MORROWIND:
return 0x100;
case TYPE_OBLIVION:
return 0x67;
case TYPE_FALLOUT3:
return 0x68;
case TYPE_SKYRIMSE:
return 0x69;
case TYPE_FALLOUT4:
return 0x01;
case TYPE_STARFIELD:
return 0x02;
case TYPE_STARFIELD_LZ4_TEXTURE:
return 0x03;
case TYPE_FALLOUT4NG_7:
return 0x07;
case TYPE_FALLOUT4NG_8:
return 0x08;
default:
throw data_invalid_exception(makeString("invalid type %d", type));
}
}
#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0)
Q_EXPORT_PLUGIN2(previewBsa, PreviewBsa)
#endif