1
0
Fork 0
mirror of https://github.com/OpenMW/openmw.git synced 2025-01-16 18:29:55 +00:00
openmw/apps/essimporter/importer.cpp
florent.teppe 65cdd489fb create a specific esm reader function for RefID to avoid allocation for string and then again for RefId
Fixed some types

removed useless header

applied clang format

fixed compile tests

fixed clang tidy, and closer to logic before this MR

Removed hardcoded refids

unless there is a returned value we don't use static RefIds
can use == between RefId and hardcoded string

Fix clang format

Fixed a few instances where std::string was used, when only const std::string& was needed

removed unused variable
2022-12-27 19:15:57 +01:00

446 lines
16 KiB
C++

#include "importer.hpp"
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <osg/ImageUtils>
#include <osgDB/ReadFile>
#include <components/esm/defs.hpp>
#include <components/esm3/esmreader.hpp>
#include <components/esm3/esmwriter.hpp>
#include <components/esm3/player.hpp>
#include <components/esm3/savedgame.hpp>
#include <components/esm3/loadalch.hpp>
#include <components/esm3/loadarmo.hpp>
#include <components/esm3/loadclot.hpp>
#include <components/esm3/loadench.hpp>
#include <components/esm3/loadlevlist.hpp>
#include <components/esm3/loadspel.hpp>
#include <components/esm3/loadweap.hpp>
#include <components/misc/constants.hpp>
#include <components/to_utf8/to_utf8.hpp>
#include "importercontext.hpp"
#include "converter.hpp"
namespace
{
void writeScreenshot(const ESM::Header& fileHeader, ESM::SavedGame& out)
{
if (fileHeader.mSCRS.size() != 128 * 128 * 4)
{
std::cerr << "Error: unexpected screenshot size " << std::endl;
return;
}
osg::ref_ptr<osg::Image> image(new osg::Image);
image->allocateImage(128, 128, 1, GL_RGB, GL_UNSIGNED_BYTE);
// need to convert pixel format from BGRA to RGB as the jpg readerwriter doesn't support it otherwise
auto it = fileHeader.mSCRS.begin();
for (int y = 0; y < 128; ++y)
{
for (int x = 0; x < 128; ++x)
{
assert(image->data(x, y));
*(image->data(x, y) + 2) = *it++;
*(image->data(x, y) + 1) = *it++;
*image->data(x, y) = *it++;
++it; // skip alpha
}
}
image->flipVertical();
std::stringstream ostream;
osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension("jpg");
if (!readerwriter)
{
std::cerr << "Error: can't write screenshot: no jpg readerwriter found" << std::endl;
return;
}
osgDB::ReaderWriter::WriteResult result = readerwriter->writeImage(*image, ostream);
if (!result.success())
{
std::cerr << "Error: can't write screenshot: " << result.message() << " code " << result.status()
<< std::endl;
return;
}
std::string data = ostream.str();
out.mScreenshot = std::vector<char>(data.begin(), data.end());
}
}
namespace ESSImport
{
Importer::Importer(
const std::filesystem::path& essfile, const std::filesystem::path& outfile, const std::string& encoding)
: mEssFile(essfile)
, mOutFile(outfile)
, mEncoding(encoding)
{
}
struct File
{
struct Subrecord
{
std::string mName;
size_t mFileOffset;
std::vector<unsigned char> mData;
};
struct Record
{
std::string mName;
size_t mFileOffset;
std::vector<Subrecord> mSubrecords;
};
std::vector<Record> mRecords;
};
void read(const std::filesystem::path& filename, File& file)
{
ESM::ESMReader esm;
esm.open(filename);
while (esm.hasMoreRecs())
{
ESM::NAME n = esm.getRecName();
esm.getRecHeader();
File::Record rec;
rec.mName = n.toString();
rec.mFileOffset = esm.getFileOffset();
while (esm.hasMoreSubs())
{
File::Subrecord sub;
esm.getSubName();
esm.getSubHeader();
sub.mFileOffset = esm.getFileOffset();
sub.mName = esm.retSubName().toString();
sub.mData.resize(esm.getSubSize());
esm.getExact(&sub.mData[0], sub.mData.size());
rec.mSubrecords.push_back(sub);
}
file.mRecords.push_back(rec);
}
}
void Importer::compare()
{
// data that always changes (and/or is already fully decoded) should be blacklisted
std::set<std::pair<std::string, std::string>> blacklist;
blacklist.insert(std::make_pair("GLOB", "FLTV")); // gamehour
blacklist.insert(std::make_pair("REFR", "DATA")); // player position
blacklist.insert(std::make_pair("CELL", "NAM8")); // fog of war
blacklist.insert(std::make_pair("GAME", "GMDT")); // weather data, current time always changes
blacklist.insert(std::make_pair("CELL", "DELE")); // first 3 bytes are uninitialized
// this changes way too often, name suggests some renderer internal data?
blacklist.insert(std::make_pair("CELL", "ND3D"));
blacklist.insert(std::make_pair("REFR", "ND3D"));
File file1;
read(mEssFile, file1);
File file2;
read(mOutFile, file2); // todo rename variable
// FIXME: use max(size1, size2)
for (unsigned int i = 0; i < file1.mRecords.size(); ++i)
{
File::Record rec = file1.mRecords[i];
if (i >= file2.mRecords.size())
{
std::ios::fmtflags f(std::cout.flags());
std::cout << "Record in file1 not present in file2: (1) 0x" << std::hex << rec.mFileOffset << std::endl;
std::cout.flags(f);
return;
}
File::Record rec2 = file2.mRecords[i];
if (rec.mName != rec2.mName)
{
std::ios::fmtflags f(std::cout.flags());
std::cout << "Different record name at (2) 0x" << std::hex << rec2.mFileOffset << std::endl;
std::cout.flags(f);
return; // TODO: try to recover
}
// FIXME: use max(size1, size2)
for (unsigned int j = 0; j < rec.mSubrecords.size(); ++j)
{
File::Subrecord sub = rec.mSubrecords[j];
if (j >= rec2.mSubrecords.size())
{
std::ios::fmtflags f(std::cout.flags());
std::cout << "Subrecord in file1 not present in file2: (1) 0x" << std::hex << sub.mFileOffset
<< std::endl;
std::cout.flags(f);
return;
}
File::Subrecord sub2 = rec2.mSubrecords[j];
if (sub.mName != sub2.mName)
{
std::ios::fmtflags f(std::cout.flags());
std::cout << "Different subrecord name (" << rec.mName << "." << sub.mName << " vs. " << sub2.mName
<< ") at (1) 0x" << std::hex << sub.mFileOffset << " (2) 0x" << sub2.mFileOffset
<< std::endl;
std::cout.flags(f);
break; // TODO: try to recover
}
if (sub.mData != sub2.mData)
{
if (blacklist.find(std::make_pair(rec.mName, sub.mName)) != blacklist.end())
continue;
std::ios::fmtflags f(std::cout.flags());
std::cout << "Different subrecord data for " << rec.mName << "." << sub.mName << " at (1) 0x"
<< std::hex << sub.mFileOffset << " (2) 0x" << sub2.mFileOffset << std::endl;
std::cout << "Data 1:" << std::endl;
for (unsigned int k = 0; k < sub.mData.size(); ++k)
{
bool different = false;
if (k >= sub2.mData.size() || sub2.mData[k] != sub.mData[k])
different = true;
if (different)
std::cout << "\033[033m";
std::cout << std::hex << std::setw(2) << std::setfill('0') << (int)sub.mData[k] << " ";
if (different)
std::cout << "\033[0m";
}
std::cout << std::endl;
std::cout << "Data 2:" << std::endl;
for (unsigned int k = 0; k < sub2.mData.size(); ++k)
{
bool different = false;
if (k >= sub.mData.size() || sub.mData[k] != sub2.mData[k])
different = true;
if (different)
std::cout << "\033[033m";
std::cout << std::hex << std::setw(2) << std::setfill('0') << (int)sub2.mData[k] << " ";
if (different)
std::cout << "\033[0m";
}
std::cout << std::endl;
std::cout.flags(f);
}
}
}
}
void Importer::run()
{
ToUTF8::Utf8Encoder encoder(ToUTF8::calculateEncoding(mEncoding));
ESM::ESMReader esm;
esm.open(mEssFile);
esm.setEncoder(&encoder);
Context context;
const ESM::Header& header = esm.getHeader();
context.mPlayerCellName = header.mGameData.mCurrentCell.toString();
const unsigned int recREFR = ESM::fourCC("REFR");
const unsigned int recPCDT = ESM::fourCC("PCDT");
const unsigned int recFMAP = ESM::fourCC("FMAP");
const unsigned int recKLST = ESM::fourCC("KLST");
const unsigned int recSTLN = ESM::fourCC("STLN");
const unsigned int recGAME = ESM::fourCC("GAME");
const unsigned int recJOUR = ESM::fourCC("JOUR");
const unsigned int recSPLM = ESM::fourCC("SPLM");
std::map<unsigned int, std::unique_ptr<Converter>> converters;
converters[ESM::REC_GLOB] = std::make_unique<ConvertGlobal>();
converters[ESM::REC_BOOK] = std::make_unique<ConvertBook>();
converters[ESM::REC_NPC_] = std::make_unique<ConvertNPC>();
converters[ESM::REC_CREA] = std::make_unique<ConvertCREA>();
converters[ESM::REC_NPCC] = std::make_unique<ConvertNPCC>();
converters[ESM::REC_CREC] = std::make_unique<ConvertCREC>();
converters[recREFR] = std::make_unique<ConvertREFR>();
converters[recPCDT] = std::make_unique<ConvertPCDT>();
converters[recFMAP] = std::make_unique<ConvertFMAP>();
converters[recKLST] = std::make_unique<ConvertKLST>();
converters[recSTLN] = std::make_unique<ConvertSTLN>();
converters[recGAME] = std::make_unique<ConvertGAME>();
converters[ESM::REC_CELL] = std::make_unique<ConvertCell>();
converters[ESM::REC_ALCH] = std::make_unique<DefaultConverter<ESM::Potion>>();
converters[ESM::REC_CLAS] = std::make_unique<ConvertClass>();
converters[ESM::REC_SPEL] = std::make_unique<DefaultConverter<ESM::Spell>>();
converters[ESM::REC_ARMO] = std::make_unique<DefaultConverter<ESM::Armor>>();
converters[ESM::REC_WEAP] = std::make_unique<DefaultConverter<ESM::Weapon>>();
converters[ESM::REC_CLOT] = std::make_unique<DefaultConverter<ESM::Clothing>>();
converters[ESM::REC_ENCH] = std::make_unique<DefaultConverter<ESM::Enchantment>>();
converters[ESM::REC_WEAP] = std::make_unique<DefaultConverter<ESM::Weapon>>();
converters[ESM::REC_LEVC] = std::make_unique<DefaultConverter<ESM::CreatureLevList>>();
converters[ESM::REC_LEVI] = std::make_unique<DefaultConverter<ESM::ItemLevList>>();
converters[ESM::REC_CNTC] = std::make_unique<ConvertCNTC>();
converters[ESM::REC_FACT] = std::make_unique<ConvertFACT>();
converters[ESM::REC_INFO] = std::make_unique<ConvertINFO>();
converters[ESM::REC_DIAL] = std::make_unique<ConvertDIAL>();
converters[ESM::REC_QUES] = std::make_unique<ConvertQUES>();
converters[recJOUR] = std::make_unique<ConvertJOUR>();
converters[ESM::REC_SCPT] = std::make_unique<ConvertSCPT>();
converters[ESM::REC_PROJ] = std::make_unique<ConvertPROJ>();
converters[recSPLM] = std::make_unique<ConvertSPLM>();
// TODO:
// - REGN (weather in certain regions?)
// - VFXM
// - SPLM (active spell effects)
std::set<unsigned int> unknownRecords;
for (const auto& converter : converters)
{
converter.second->setContext(context);
}
while (esm.hasMoreRecs())
{
ESM::NAME n = esm.getRecName();
esm.getRecHeader();
auto it = converters.find(n.toInt());
if (it != converters.end())
{
it->second->read(esm);
}
else
{
if (unknownRecords.insert(n.toInt()).second)
{
std::ios::fmtflags f(std::cerr.flags());
std::cerr << "Error: unknown record " << n.toString() << " (0x" << std::hex << esm.getFileOffset()
<< ")" << std::endl;
std::cerr.flags(f);
}
esm.skipRecord();
}
}
ESM::ESMWriter writer;
writer.setFormat(ESM::SavedGame::sCurrentFormat);
std::ofstream stream(mOutFile, std::ios::out | std::ios::binary);
// all unused
writer.setVersion(0);
writer.setType(0);
writer.setAuthor("");
writer.setDescription("");
writer.setRecordCount(0);
for (const auto& master : header.mMaster)
writer.addMaster(master.name, 0); // not using the size information anyway -> use value of 0
writer.save(stream);
ESM::SavedGame profile;
for (const auto& master : header.mMaster)
{
profile.mContentFiles.push_back(master.name);
}
profile.mDescription = esm.getDesc();
profile.mInGameTime.mDay = context.mDay;
profile.mInGameTime.mGameHour = context.mHour;
profile.mInGameTime.mMonth = context.mMonth;
profile.mInGameTime.mYear = context.mYear;
profile.mTimePlayed = 0;
profile.mPlayerCell = ESM::RefId::stringRefId(header.mGameData.mCurrentCell.toString());
if (context.mPlayerBase.mClass == "NEWCLASSID_CHARGEN")
profile.mPlayerClassName = context.mCustomPlayerClassName;
else
profile.mPlayerClassId = context.mPlayerBase.mClass;
profile.mPlayerLevel = context.mPlayerBase.mNpdt.mLevel;
profile.mPlayerName = header.mGameData.mPlayerName.toString();
writeScreenshot(header, profile);
writer.startRecord(ESM::REC_SAVE);
profile.save(writer);
writer.endRecord(ESM::REC_SAVE);
// Writing order should be Dynamic Store -> Cells -> Player,
// so that references to dynamic records can be recognized when loading
for (auto it = converters.begin(); it != converters.end(); ++it)
{
if (it->second->getStage() != 0)
continue;
it->second->write(writer);
}
writer.startRecord(ESM::REC_NPC_);
context.mPlayerBase.mId = ESM::RefId::stringRefId("Player");
context.mPlayerBase.save(writer);
writer.endRecord(ESM::REC_NPC_);
for (auto it = converters.begin(); it != converters.end(); ++it)
{
if (it->second->getStage() != 1)
continue;
it->second->write(writer);
}
writer.startRecord(ESM::REC_PLAY);
if (context.mPlayer.mCellId.mPaged)
{
// exterior cell -> determine cell coordinates based on position
int cellX
= static_cast<int>(std::floor(context.mPlayer.mObject.mPosition.pos[0] / Constants::CellSizeInUnits));
int cellY
= static_cast<int>(std::floor(context.mPlayer.mObject.mPosition.pos[1] / Constants::CellSizeInUnits));
context.mPlayer.mCellId.mIndex.mX = cellX;
context.mPlayer.mCellId.mIndex.mY = cellY;
}
context.mPlayer.save(writer);
writer.endRecord(ESM::REC_PLAY);
writer.startRecord(ESM::REC_ACTC);
writer.writeHNT("COUN", context.mNextActorId);
writer.endRecord(ESM::REC_ACTC);
// Stage 2 requires cell references to be written / actors IDs assigned
for (auto it = converters.begin(); it != converters.end(); ++it)
{
if (it->second->getStage() != 2)
continue;
it->second->write(writer);
}
writer.startRecord(ESM::REC_DIAS);
context.mDialogueState.save(writer);
writer.endRecord(ESM::REC_DIAS);
writer.startRecord(ESM::REC_INPU);
context.mControlsState.save(writer);
writer.endRecord(ESM::REC_INPU);
}
}