Something went wrong. Try again.
OpenMW is an open implementation of the Morrowind engine
Something went wrong. Try again.
17 kB · 443 lines
C++
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444#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/cellid.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/toutf8/toutf8.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.data(), sub.mData.size()); rec.mSubrecords.push_back(std::move(sub)); } file.mRecords.push_back(std::move(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 (size_t 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 (size_t 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 (size_t 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 (size_t 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.setFormatVersion(ESM::CurrentSaveGameFormatVersion);
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.mPlayerCellName = context.mPlayerCellName; 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);
writer.startRecord(ESM::REC_DIAS); context.mDialogueState.save(writer); writer.endRecord(ESM::REC_DIAS);
writer.startRecord(ESM::REC_LUAM); writer.writeHNT<double>("LUAW", 0); writer.writeFormId(context.mNextRefNum, true); writer.endRecord(ESM::REC_LUAM);
// Writing order should be Dynamic Store -> Cells -> Player, // so that references to dynamic records can be recognized when loading for (const auto& [_, converter] : converters) { if (converter->getStage() != 0) continue; converter->write(writer); }
writer.startRecord(ESM::REC_NPC_); context.mPlayerBase.mId = ESM::RefId::stringRefId("Player"); context.mPlayerBase.save(writer); writer.endRecord(ESM::REC_NPC_);
for (const auto& [_, converter] : converters) { if (converter->getStage() != 1) continue; converter->write(writer); }
writer.startRecord(ESM::REC_PLAY); ESM::CellId cellId = ESM::CellId::extractFromRefId(context.mPlayer.mCellId); 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 = ESM::Cell::generateIdForCell(cellId.mPaged, cellId.mWorldspace, cellX, cellY); context.mPlayer.save(writer); writer.endRecord(ESM::REC_PLAY);
// Stage 2 requires cell references to be written / actors IDs assigned for (const auto& [_, converter] : converters) { if (converter->getStage() != 2) continue; converter->write(writer); }
writer.startRecord(ESM::REC_INPU); context.mControlsState.save(writer); writer.endRecord(ESM::REC_INPU); }
}