From 031eec45509c3b157dd4fce9e366ac4fcfffe571 Mon Sep 17 00:00:00 2001 From: scrawl Date: Sat, 17 Jan 2015 00:11:36 +0100 Subject: [PATCH] Starting ESS importer for Morrowind save files --- CMakeLists.txt | 11 + apps/essimporter/CMakeLists.txt | 25 +++ apps/essimporter/converter.cpp | 40 ++++ apps/essimporter/converter.hpp | 262 +++++++++++++++++++++++ apps/essimporter/importacdt.hpp | 25 +++ apps/essimporter/importcellref.cpp | 28 +++ apps/essimporter/importcellref.hpp | 33 +++ apps/essimporter/importcrec.cpp | 13 ++ apps/essimporter/importcrec.hpp | 22 ++ apps/essimporter/importer.cpp | 306 +++++++++++++++++++++++++++ apps/essimporter/importer.hpp | 25 +++ apps/essimporter/importercontext.cpp | 0 apps/essimporter/importercontext.hpp | 65 ++++++ apps/essimporter/importnpcc.cpp | 18 ++ apps/essimporter/importnpcc.hpp | 29 +++ apps/essimporter/importplayer.cpp | 59 ++++++ apps/essimporter/importplayer.hpp | 58 +++++ apps/essimporter/main.cpp | 60 ++++++ components/esm/esmreader.hpp | 1 + components/esm/loadcell.cpp | 5 + components/esm/loadnpcc.hpp | 29 +-- components/esm/loadscpt.cpp | 26 +++ components/esm/loadtes3.cpp | 19 ++ components/esm/loadtes3.hpp | 14 ++ 24 files changed, 1161 insertions(+), 12 deletions(-) create mode 100644 apps/essimporter/CMakeLists.txt create mode 100644 apps/essimporter/converter.cpp create mode 100644 apps/essimporter/converter.hpp create mode 100644 apps/essimporter/importacdt.hpp create mode 100644 apps/essimporter/importcellref.cpp create mode 100644 apps/essimporter/importcellref.hpp create mode 100644 apps/essimporter/importcrec.cpp create mode 100644 apps/essimporter/importcrec.hpp create mode 100644 apps/essimporter/importer.cpp create mode 100644 apps/essimporter/importer.hpp create mode 100644 apps/essimporter/importercontext.cpp create mode 100644 apps/essimporter/importercontext.hpp create mode 100644 apps/essimporter/importnpcc.cpp create mode 100644 apps/essimporter/importnpcc.hpp create mode 100644 apps/essimporter/importplayer.cpp create mode 100644 apps/essimporter/importplayer.hpp create mode 100644 apps/essimporter/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 0bbce7ad4..6e971b8fb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,6 +58,7 @@ option(BUILD_BSATOOL "build BSA extractor" ON) option(BUILD_ESMTOOL "build ESM inspector" ON) option(BUILD_LAUNCHER "build Launcher" ON) option(BUILD_MWINIIMPORTER "build MWiniImporter" ON) +option(BUILD_ESSIMPORTER "build ESS (Morrowind save game) importer" ON) option(BUILD_OPENCS "build OpenMW Construction Set" ON) option(BUILD_WIZARD "build Installation Wizard" ON) option(BUILD_WITH_CODE_COVERAGE "Enable code coverage with gconv" OFF) @@ -414,6 +415,9 @@ IF(NOT WIN32 AND NOT APPLE) IF(BUILD_MWINIIMPORTER) INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/mwiniimport" DESTINATION "${BINDIR}" ) ENDIF(BUILD_MWINIIMPORTER) + IF(BUILD_ESSIMPORTER) + INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/openmw-essimporter" DESTINATION "${BINDIR}" ) + ENDIF(BUILD_ESSIMPORTER) IF(BUILD_OPENCS) INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/opencs" DESTINATION "${BINDIR}" ) ENDIF(BUILD_OPENCS) @@ -472,6 +476,9 @@ if(WIN32) IF(BUILD_MWINIIMPORTER) INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/Release/mwiniimport.exe" DESTINATION ".") ENDIF(BUILD_MWINIIMPORTER) + IF(BUILD_ESSIMPORTER) + INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/Release/openmw-essimporter.exe" DESTINATION ".") + ENDIF(BUILD_ESSIMPORTER) IF(BUILD_OPENCS) INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/Release/opencs.exe" DESTINATION ".") INSTALL(FILES "${OpenMW_BINARY_DIR}/opencs.ini" DESTINATION ".") @@ -582,6 +589,10 @@ if (BUILD_MWINIIMPORTER) add_subdirectory( apps/mwiniimporter ) endif() +if (BUILD_ESSIMPORTER) + add_subdirectory (apps/essimporter ) +endif() + if (BUILD_OPENCS) add_subdirectory (apps/opencs) endif() diff --git a/apps/essimporter/CMakeLists.txt b/apps/essimporter/CMakeLists.txt new file mode 100644 index 000000000..231e31fd8 --- /dev/null +++ b/apps/essimporter/CMakeLists.txt @@ -0,0 +1,25 @@ +set(ESSIMPORTER_FILES + main.cpp + importer.cpp + importplayer.cpp + importnpcc.cpp + importcrec.cpp + importcellref.cpp + importacdt.hpp + importercontext.cpp + converter.cpp +) + +add_executable(openmw-essimporter + ${ESSIMPORTER_FILES} +) + +target_link_libraries(openmw-essimporter + ${Boost_LIBRARIES} + components +) + +if (BUILD_WITH_CODE_COVERAGE) + add_definitions (--coverage) + target_link_libraries(openmw-essimporter gcov) +endif() diff --git a/apps/essimporter/converter.cpp b/apps/essimporter/converter.cpp new file mode 100644 index 000000000..4f36f13ee --- /dev/null +++ b/apps/essimporter/converter.cpp @@ -0,0 +1,40 @@ +#include "converter.hpp" + +#include + +namespace +{ + + void convertImage(char* data, int size, int width, int height, Ogre::PixelFormat pf, const std::string& out) + { + Ogre::Image screenshot; + Ogre::DataStreamPtr stream (new Ogre::MemoryDataStream(data, size)); + screenshot.loadRawData(stream, width, height, 1, pf); + screenshot.save(out); + } + +} + +namespace ESSImport +{ + + + struct MAPH + { + unsigned int size; + unsigned int value; + }; + + void ConvertFMAP::read(ESM::ESMReader &esm) + { + MAPH maph; + esm.getHNT(maph, "MAPH"); + std::vector data; + esm.getSubNameIs("MAPD"); + esm.getSubHeader(); + data.resize(esm.getSubSize()); + esm.getExact(&data[0], data.size()); + convertImage(&data[0], data.size(), maph.size, maph.size, Ogre::PF_BYTE_RGB, "map.tga"); + } + +} diff --git a/apps/essimporter/converter.hpp b/apps/essimporter/converter.hpp new file mode 100644 index 000000000..2662f8c75 --- /dev/null +++ b/apps/essimporter/converter.hpp @@ -0,0 +1,262 @@ +#ifndef OPENMW_ESSIMPORT_CONVERTER_H +#define OPENMW_ESSIMPORT_CONVERTER_H + +#include +#include + +#include +#include +#include +#include +#include "importcrec.hpp" + +#include "importercontext.hpp" +#include "importcellref.hpp" + +namespace ESSImport +{ + +class Converter +{ +public: + /// @return the order for writing this converter's records to the output file, in relation to other converters + virtual int getStage() { return 1; } + + virtual ~Converter() {} + + void setContext(Context& context) { mContext = &context; } + + virtual void read(ESM::ESMReader& esm) + { + } + + /// Called after the input file has been read in completely, which may be necessary + /// if the conversion process relies on information in other records + virtual void write(ESM::ESMWriter& esm) + { + + } + +protected: + Context* mContext; +}; + +/// Default converter: simply reads the record and writes it unmodified to the output +template +class DefaultConverter : public Converter +{ +public: + virtual int getStage() { return 0; } + + virtual void read(ESM::ESMReader& esm) + { + std::string id = esm.getHNString("NAME"); + T record; + record.load(esm); + mRecords[id] = record; + } + + virtual void write(ESM::ESMWriter& esm) + { + for (typename std::map::const_iterator it = mRecords.begin(); it != mRecords.end(); ++it) + { + esm.startRecord(T::sRecordId); + esm.writeHNString("NAME", it->first); + it->second.save(esm); + esm.endRecord(T::sRecordId); + } + } + +protected: + std::map mRecords; +}; + +class ConvertNPC : public Converter +{ +public: + virtual void read(ESM::ESMReader &esm) + { + // this is always the player + ESM::NPC npc; + std::string id = esm.getHNString("NAME"); + assert (id == "player"); + npc.load(esm); + mContext->mPlayer.mObject.mCreatureStats.mLevel = npc.mNpdt52.mLevel; + mContext->mPlayerBase = npc; + std::map empty; + for (std::vector::const_iterator it = npc.mSpells.mList.begin(); it != npc.mSpells.mList.end(); ++it) + mContext->mPlayer.mObject.mCreatureStats.mSpells.mSpells[*it] = empty; + } +}; + +class ConvertGlobal : public DefaultConverter +{ +public: + virtual void read(ESM::ESMReader &esm) + { + std::string id = esm.getHNString("NAME"); + ESM::Global global; + global.load(esm); + if (Misc::StringUtils::ciEqual(id, "gamehour")) + mContext->mHour = global.mValue.getFloat(); + if (Misc::StringUtils::ciEqual(id, "day")) + mContext->mDay = global.mValue.getInteger(); + if (Misc::StringUtils::ciEqual(id, "month")) + mContext->mMonth = global.mValue.getInteger(); + if (Misc::StringUtils::ciEqual(id, "year")) + mContext->mYear = global.mValue.getInteger(); + mRecords[id] = global; + } +}; + +class ConvertClass : public DefaultConverter +{ +public: + virtual void read(ESM::ESMReader &esm) + { + std::string id = esm.getHNString("NAME"); + ESM::Class class_; + class_.load(esm); + + if (id == "NEWCLASSID_CHARGEN") + mContext->mCustomPlayerClassName = class_.mName; + + mRecords[id] = class_; + } +}; + +class ConvertBook : public DefaultConverter +{ +public: + virtual void read(ESM::ESMReader &esm) + { + std::string id = esm.getHNString("NAME"); + ESM::Book book; + book.load(esm); + if (book.mData.mSkillID == -1) + mContext->mPlayer.mObject.mNpcStats.mUsedIds.push_back(Misc::StringUtils::lowerCase(id)); + + mRecords[id] = book; + } +}; + +class ConvertNPCC : public Converter +{ +public: + virtual void read(ESM::ESMReader &esm) + { + std::string id = esm.getHNString("NAME"); + NPCC npcc; + npcc.load(esm); + if (id == "PlayerSaveGame") + { + mContext->mPlayer.mObject.mNpcStats.mReputation = npcc.mNPDT.mReputation; + } + } +}; + +class ConvertREFR : public Converter +{ +public: + virtual void read(ESM::ESMReader &esm) + { + REFR refr; + refr.load(esm); + assert(refr.mRefID == "PlayerSaveGame"); + mContext->mPlayer.mObject.mPosition = refr.mPos; + + ESM::CreatureStats& cStats = mContext->mPlayer.mObject.mCreatureStats; + for (int i=0; i<3; ++i) + { + int writeIndex = translateDynamicIndex(i); + cStats.mDynamic[writeIndex].mBase = refr.mACDT.mDynamic[i][1]; + cStats.mDynamic[writeIndex].mMod = refr.mACDT.mDynamic[i][1]; + cStats.mDynamic[writeIndex].mCurrent = refr.mACDT.mDynamic[i][0]; + } + for (int i=0; i<8; ++i) + { + cStats.mAttributes[i].mBase = refr.mACDT.mAttributes[i][1]; + cStats.mAttributes[i].mMod = refr.mACDT.mAttributes[i][0]; + cStats.mAttributes[i].mCurrent = refr.mACDT.mAttributes[i][0]; + } + ESM::NpcStats& npcStats = mContext->mPlayer.mObject.mNpcStats; + for (int i=0; imPlayer.mBirthsign = pcdt.mBirthsign; + mContext->mPlayer.mObject.mNpcStats.mBounty = pcdt.mBounty; + for (std::vector::const_iterator it = pcdt.mFactions.begin(); it != pcdt.mFactions.end(); ++it) + { + ESM::NpcStats::Faction faction; + faction.mExpelled = it->mFlags & 0x2; + faction.mRank = it->mRank; + faction.mReputation = it->mReputation; + mContext->mPlayer.mObject.mNpcStats.mFactions[it->mFactionName.toString()] = faction; + } + + } +}; + +class ConvertCREC : public Converter +{ +public: + virtual void read(ESM::ESMReader &esm) + { + std::string id = esm.getHNString("NAME"); + CREC crec; + crec.load(esm); + } +}; + +class ConvertFMAP : public Converter +{ +public: + virtual void read(ESM::ESMReader &esm); +}; + +class ConvertCell : public Converter +{ +public: + virtual void read(ESM::ESMReader& esm) + { + ESM::Cell cell; + std::string id = esm.getHNString("NAME"); + cell.load(esm, false); + CellRef ref; + while (esm.hasMoreSubs()) + { + ref.load (esm); + if (esm.isNextSub("DELE")) + std::cout << "deleted ref " << ref.mIndexedRefId << std::endl; + } + } + +}; + +} + +#endif diff --git a/apps/essimporter/importacdt.hpp b/apps/essimporter/importacdt.hpp new file mode 100644 index 000000000..1d79f899e --- /dev/null +++ b/apps/essimporter/importacdt.hpp @@ -0,0 +1,25 @@ +#ifndef OPENMW_ESSIMPORT_ACDT_H +#define OPENMW_ESSIMPORT_ACDT_H + +namespace ESSImport +{ + + /// Actor data, shared by (at least) REFR and CellRef + struct ACDT + { + unsigned char mUnknown1[40]; + float mDynamic[3][2]; + unsigned char mUnknown2[16]; + float mAttributes[8][2]; + unsigned char mUnknown3[120]; + }; + + /// Unknown, shared by (at least) REFR and CellRef + struct ACSC + { + unsigned char unknown[112]; + }; + +} + +#endif diff --git a/apps/essimporter/importcellref.cpp b/apps/essimporter/importcellref.cpp new file mode 100644 index 000000000..f59358d8b --- /dev/null +++ b/apps/essimporter/importcellref.cpp @@ -0,0 +1,28 @@ +#include "importcellref.hpp" + +#include + +namespace ESSImport +{ + + void CellRef::load(ESM::ESMReader &esm) + { + esm.getHNT(mRefNum.mIndex, "FRMR"); // TODO: adjust RefNum + + mIndexedRefId = esm.getHNString("NAME"); + + esm.getHNT(mACDT, "ACDT"); + + ACSC acsc; + esm.getHNOT(acsc, "ACSC"); + esm.getHNOT(acsc, "ACSL"); + + if (esm.isNextSub("CRED")) + esm.skipHSub(); + + if (esm.isNextSub("ND3D")) + esm.skipHSub(); + esm.getHNOT(mPos, "DATA", 24); + } + +} diff --git a/apps/essimporter/importcellref.hpp b/apps/essimporter/importcellref.hpp new file mode 100644 index 000000000..f06278ba5 --- /dev/null +++ b/apps/essimporter/importcellref.hpp @@ -0,0 +1,33 @@ +#ifndef OPENMW_ESSIMPORT_CELLREF_H +#define OPENMW_ESSIMPORT_CELLREF_H + +#include + +#include + +#include "importacdt.hpp" + +namespace ESM +{ + class ESMReader; +} + +namespace ESSImport +{ + + // Not sure if we can share any code with ESM::CellRef here + struct CellRef + { + std::string mIndexedRefId; + ESM::RefNum mRefNum; + + ACDT mACDT; + + ESM::Position mPos; + + void load(ESM::ESMReader& esm); + }; + +} + +#endif diff --git a/apps/essimporter/importcrec.cpp b/apps/essimporter/importcrec.cpp new file mode 100644 index 000000000..c770b2386 --- /dev/null +++ b/apps/essimporter/importcrec.cpp @@ -0,0 +1,13 @@ +#include "importcrec.hpp" + +#include + +namespace ESSImport +{ + + void CREC::load(ESM::ESMReader &esm) + { + esm.getHNT(mIndex, "INDX"); + } + +} diff --git a/apps/essimporter/importcrec.hpp b/apps/essimporter/importcrec.hpp new file mode 100644 index 000000000..62881c582 --- /dev/null +++ b/apps/essimporter/importcrec.hpp @@ -0,0 +1,22 @@ +#ifndef OPENMW_ESSIMPORT_CREC_H +#define OPENMW_ESSIMPORT_CREC_H + +namespace ESM +{ + class ESMReader; +} + +namespace ESSImport +{ + + /// Creature changes + struct CREC + { + int mIndex; + + void load(ESM::ESMReader& esm); + }; + +} + +#endif diff --git a/apps/essimporter/importer.cpp b/apps/essimporter/importer.cpp new file mode 100644 index 000000000..96b79e029 --- /dev/null +++ b/apps/essimporter/importer.cpp @@ -0,0 +1,306 @@ +#include "importer.hpp" + +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "importercontext.hpp" + +#include "converter.hpp" + +namespace +{ + + void writeScreenshot(const ESM::Header& fileHeader, ESM::SavedGame& out) + { + Ogre::Image screenshot; + std::vector screenshotData = fileHeader.mSCRS; // MemoryDataStream doesn't work with const data :( + Ogre::DataStreamPtr screenshotStream (new Ogre::MemoryDataStream(&screenshotData[0], screenshotData.size())); + screenshot.loadRawData(screenshotStream, 128, 128, 1, Ogre::PF_BYTE_BGRA); + Ogre::DataStreamPtr encoded = screenshot.encode("jpg"); + out.mScreenshot.resize(encoded->size()); + encoded->read(&out.mScreenshot[0], encoded->size()); + } + +} + +namespace ESSImport +{ + + Importer::Importer(const std::string &essfile, const std::string &outfile) + : mEssFile(essfile) + , mOutFile(outfile) + { + + } + + struct File + { + struct Subrecord + { + std::string mName; + size_t mFileOffset; + std::vector mData; + }; + + struct Record + { + std::string mName; + size_t mFileOffset; + std::vector mSubrecords; + }; + + std::vector mRecords; + }; + + void read(const std::string& 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 should be blacklisted + std::set > blacklist; + blacklist.insert(std::make_pair("GLOB", "FLTV")); // gamehour + blacklist.insert(std::make_pair("REFR", "DATA")); // player position + + File file1; + read(mEssFile, file1); + File file2; + read(mOutFile, file2); // todo rename variable + + for (unsigned int i=0; i= file2.mRecords.size()) + { + std::cout << "Record in file1 not present in file2: (1) 0x" << std::hex << rec.mFileOffset; + return; + } + + File::Record rec2 = file2.mRecords[i]; + + if (rec.mName != rec2.mName) + { + std::cout << "Different record name at (2) 0x" << std::hex << rec2.mFileOffset << std::endl; + return; // TODO: try to recover + } + + // FIXME: use max(size1, size2) + for (unsigned int j=0; j= rec2.mSubrecords.size()) + { + std::cout << "Subrecord in file1 not present in file2: (1) 0x" << std::hex << sub.mFileOffset; + return; + } + + File::Subrecord sub2 = rec2.mSubrecords[j]; + + if (sub.mName != sub2.mName) + { + 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; + return; // TODO: try to recover + } + + if (sub.mData != sub2.mData) + { + if (blacklist.find(std::make_pair(rec.mName, sub.mName)) != blacklist.end()) + continue; + + 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 globals; + + const unsigned int recREFR = ESM::FourCC<'R','E','F','R'>::value; + const unsigned int recPCDT = ESM::FourCC<'P','C','D','T'>::value; + const unsigned int recFMAP = ESM::FourCC<'F','M','A','P'>::value; + + std::map > converters; + converters[ESM::REC_GLOB] = boost::shared_ptr(new ConvertGlobal()); + converters[ESM::REC_BOOK] = boost::shared_ptr(new ConvertBook()); + converters[ESM::REC_NPC_] = boost::shared_ptr(new ConvertNPC()); + converters[ESM::REC_NPCC] = boost::shared_ptr(new ConvertNPCC()); + converters[ESM::REC_CREC] = boost::shared_ptr(new ConvertCREC()); + converters[recREFR] = boost::shared_ptr(new ConvertREFR()); + converters[recPCDT] = boost::shared_ptr(new ConvertPCDT()); + converters[recFMAP] = boost::shared_ptr(new ConvertFMAP()); + converters[ESM::REC_CELL] = boost::shared_ptr(new ConvertCell()); + converters[ESM::REC_ALCH] = boost::shared_ptr(new DefaultConverter()); + converters[ESM::REC_CLAS] = boost::shared_ptr(new ConvertClass()); + converters[ESM::REC_SPEL] = boost::shared_ptr(new DefaultConverter()); + converters[ESM::REC_ARMO] = boost::shared_ptr(new DefaultConverter()); + converters[ESM::REC_WEAP] = boost::shared_ptr(new DefaultConverter()); + converters[ESM::REC_CLOT] = boost::shared_ptr(new DefaultConverter()); + converters[ESM::REC_ENCH] = boost::shared_ptr(new DefaultConverter()); + converters[ESM::REC_WEAP] = boost::shared_ptr(new DefaultConverter()); + converters[ESM::REC_LEVC] = boost::shared_ptr(new DefaultConverter()); + converters[ESM::REC_LEVI] = boost::shared_ptr(new DefaultConverter()); + + + for (std::map >::const_iterator it = converters.begin(); + it != converters.end(); ++it) + { + it->second->setContext(context); + } + + while (esm.hasMoreRecs()) + { + ESM::NAME n = esm.getRecName(); + esm.getRecHeader(); + + std::map >::iterator it = converters.find(n.val); + if (it != converters.end()) + { + it->second->read(esm); + } + else + { + std::cerr << "unknown record " << n.toString() << std::endl; + esm.skipRecord(); + } + } + + const ESM::Header& header = esm.getHeader(); + + ESM::ESMWriter writer; + + writer.setFormat (ESM::Header::CurrentFormat); + + std::ofstream stream(mOutFile.c_str(), std::ios::binary); + // all unused + writer.setVersion(0); + writer.setType(0); + writer.setAuthor(""); + writer.setDescription(""); + writer.setRecordCount (0); + writer.save (stream); + + ESM::SavedGame profile; + for (std::vector::const_iterator it = header.mMaster.begin(); + it != header.mMaster.end(); ++it) + { + profile.mContentFiles.push_back(it->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.mPlayerCell = header.mGameData.mCurrentCell.toString(); + if (context.mPlayerBase.mClass == "NEWCLASSID_CHARGEN") + profile.mPlayerClassName = context.mCustomPlayerClassName; + else + profile.mPlayerClassId = context.mPlayerBase.mClass; + profile.mPlayerLevel = context.mPlayerBase.mNpdt52.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 (std::map >::const_iterator it = converters.begin(); + it != converters.end(); ++it) + { + if (it->second->getStage() != 0) + continue; + it->second->write(writer); + } + + writer.startRecord(ESM::REC_NPC_); + writer.writeHNString("NAME", "player"); + context.mPlayerBase.save(writer); + writer.endRecord(ESM::REC_NPC_); + + for (std::map >::const_iterator it = converters.begin(); + it != converters.end(); ++it) + { + if (it->second->getStage() != 1) + continue; + it->second->write(writer); + } + + writer.startRecord(ESM::REC_PLAY); + context.mPlayer.save(writer); + writer.endRecord(ESM::REC_PLAY); + } + + +} diff --git a/apps/essimporter/importer.hpp b/apps/essimporter/importer.hpp new file mode 100644 index 000000000..eb199b6df --- /dev/null +++ b/apps/essimporter/importer.hpp @@ -0,0 +1,25 @@ +#ifndef OPENMW_ESSIMPORTER_IMPORTER_H +#define OPENMW_ESSIMPORTER_IMPORTER_H + +#include + +namespace ESSImport +{ + + class Importer + { + public: + Importer(const std::string& essfile, const std::string& outfile); + + void run(); + + void compare(); + + private: + std::string mEssFile; + std::string mOutFile; + }; + +} + +#endif diff --git a/apps/essimporter/importercontext.cpp b/apps/essimporter/importercontext.cpp new file mode 100644 index 000000000..e69de29bb diff --git a/apps/essimporter/importercontext.hpp b/apps/essimporter/importercontext.hpp new file mode 100644 index 000000000..e9af77678 --- /dev/null +++ b/apps/essimporter/importercontext.hpp @@ -0,0 +1,65 @@ +#ifndef OPENMW_ESSIMPORT_CONTEXT_H +#define OPENMW_ESSIMPORT_CONTEXT_H + +#include + +#include "importnpcc.hpp" +#include "importplayer.hpp" + +#include + +namespace ESSImport +{ + + struct Context + { + ESM::Player mPlayer; + ESM::NPC mPlayerBase; + std::string mCustomPlayerClassName; + + int mDay, mMonth, mYear; + float mHour; + + Context() + { + mPlayer.mAutoMove = 0; + ESM::CellId playerCellId; + playerCellId.mPaged = true; + playerCellId.mIndex.mX = playerCellId.mIndex.mY = 0; + mPlayer.mCellId = playerCellId; + //mPlayer.mLastKnownExteriorPosition + mPlayer.mHasMark = 0; // TODO + mPlayer.mCurrentCrimeId = 0; // TODO + mPlayer.mObject.mCount = 1; + mPlayer.mObject.mEnabled = 1; + mPlayer.mObject.mHasLocals = false; + mPlayer.mObject.mRef.mRefID = "player"; // REFR.mRefID would be PlayerSaveGame + mPlayer.mObject.mCreatureStats.mHasAiSettings = true; + mPlayer.mObject.mCreatureStats.mDead = false; + mPlayer.mObject.mCreatureStats.mDied = false; + mPlayer.mObject.mCreatureStats.mKnockdown = false; + mPlayer.mObject.mCreatureStats.mKnockdownOneFrame = false; + mPlayer.mObject.mCreatureStats.mKnockdownOverOneFrame = false; + mPlayer.mObject.mCreatureStats.mHitRecovery = false; + mPlayer.mObject.mCreatureStats.mBlock = false; + mPlayer.mObject.mCreatureStats.mMovementFlags = 0; + mPlayer.mObject.mCreatureStats.mAttackStrength = 0.f; + mPlayer.mObject.mCreatureStats.mFallHeight = 0.f; + mPlayer.mObject.mCreatureStats.mRecalcDynamicStats = false; + mPlayer.mObject.mCreatureStats.mDrawState = 0; + mPlayer.mObject.mCreatureStats.mDeathAnimation = 0; + mPlayer.mObject.mNpcStats.mIsWerewolf = false; + mPlayer.mObject.mNpcStats.mTimeToStartDrowning = 20; + mPlayer.mObject.mNpcStats.mLevelProgress = 0; + mPlayer.mObject.mNpcStats.mDisposition = 0; + mPlayer.mObject.mNpcStats.mTimeToStartDrowning = 20; + mPlayer.mObject.mNpcStats.mReputation = 0; + mPlayer.mObject.mNpcStats.mCrimeId = -1; + mPlayer.mObject.mNpcStats.mWerewolfKills = 0; + mPlayer.mObject.mNpcStats.mProfit = 0; + } + }; + +} + +#endif diff --git a/apps/essimporter/importnpcc.cpp b/apps/essimporter/importnpcc.cpp new file mode 100644 index 000000000..192fe5da4 --- /dev/null +++ b/apps/essimporter/importnpcc.cpp @@ -0,0 +1,18 @@ +#include "importnpcc.hpp" + +#include + +namespace ESSImport +{ + + void NPCC::load(ESM::ESMReader &esm) + { + esm.getHNT(mNPDT, "NPDT"); + + // container: + // XIDX + // XHLT - condition + // WIDX - equipping? + } + +} diff --git a/apps/essimporter/importnpcc.hpp b/apps/essimporter/importnpcc.hpp new file mode 100644 index 000000000..12135850f --- /dev/null +++ b/apps/essimporter/importnpcc.hpp @@ -0,0 +1,29 @@ +#ifndef OPENMW_ESSIMPORT_NPCC_H +#define OPENMW_ESSIMPORT_NPCC_H + +#include +#include + +namespace ESM +{ + class ESMReader; +} + +namespace ESSImport +{ + + struct NPCC : public ESM::NPC + { + struct NPDT + { + unsigned char unknown[2]; + unsigned char mReputation; + unsigned char unknown2[5]; + } mNPDT; + + void load(ESM::ESMReader &esm); + }; + +} + +#endif diff --git a/apps/essimporter/importplayer.cpp b/apps/essimporter/importplayer.cpp new file mode 100644 index 000000000..81f81e764 --- /dev/null +++ b/apps/essimporter/importplayer.cpp @@ -0,0 +1,59 @@ +#include "importplayer.hpp" + +#include + +namespace ESSImport +{ + + void REFR::load(ESM::ESMReader &esm) + { + esm.getHNT(mRefNum.mIndex, "FRMR"); + + mRefID = esm.getHNString("NAME"); + + if (esm.isNextSub("STPR")) + esm.skipHSub(); // ESS TODO + + esm.getHNT(mACDT, "ACDT"); + + ACSC acsc; + esm.getHNOT(acsc, "ACSC"); + esm.getHNOT(acsc, "ACSL"); + + esm.getHNExact(mSkills, 27*2*sizeof(int), "CHRD"); + + if (esm.isNextSub("ND3D")) + esm.skipHSub(); // ESS TODO (1 byte) + + esm.getHNOT(mPos, "DATA", 24); + } + + void PCDT::load(ESM::ESMReader &esm) + { + if (esm.isNextSub("PNAM")) + esm.skipHSub(); + if (esm.isNextSub("SNAM")) + esm.skipHSub(); + if (esm.isNextSub("NAM9")) + esm.skipHSub(); + + mBounty = 0; + esm.getHNOT(mBounty, "CNAM"); + + mBirthsign = esm.getHNOString("BNAM"); + + if (esm.isNextSub("ENAM")) + esm.skipHSub(); + + while (esm.isNextSub("FNAM")) + { + FNAM fnam; + esm.getHT(fnam); + mFactions.push_back(fnam); + } + + if (esm.isNextSub("KNAM")) + esm.skipHSub(); + } + +} diff --git a/apps/essimporter/importplayer.hpp b/apps/essimporter/importplayer.hpp new file mode 100644 index 000000000..8a0a087a5 --- /dev/null +++ b/apps/essimporter/importplayer.hpp @@ -0,0 +1,58 @@ +#ifndef OPENMW_ESSIMPORT_PLAYER_H +#define OPENMW_ESSIMPORT_PLAYER_H + +#include +#include + +#include +#include +#include + +#include "importacdt.hpp" + +namespace ESM +{ + class ESMReader; +} + +namespace ESSImport +{ + +/// Player-agnostic player data +struct REFR +{ + ACDT mACDT; + + std::string mRefID; + ESM::Position mPos; + ESM::RefNum mRefNum; + + int mSkills[27][2]; + float mAttributes[8][2]; + + void load(ESM::ESMReader& esm); +}; + +/// Other player data +struct PCDT +{ + int mBounty; + std::string mBirthsign; + + struct FNAM + { + unsigned char mRank; + unsigned char mUnknown1[3]; + int mReputation; + unsigned char mFlags; // 0x1: unknown, 0x2: expelled + unsigned char mUnknown2[3]; + ESM::NAME32 mFactionName; + }; + std::vector mFactions; + + void load(ESM::ESMReader& esm); +}; + +} + +#endif diff --git a/apps/essimporter/main.cpp b/apps/essimporter/main.cpp new file mode 100644 index 000000000..0e5c65e95 --- /dev/null +++ b/apps/essimporter/main.cpp @@ -0,0 +1,60 @@ +#include +#include +#include +#include + +#include "importer.hpp" + +namespace bpo = boost::program_options; +namespace bfs = boost::filesystem; + + + +int main(int argc, const char** argv) +{ + try + { + bpo::options_description desc("Syntax: openmw-essimporter infile.ess outfile.omwsave\nAllowed options"); + bpo::positional_options_description p_desc; + desc.add_options() + ("help,h", "produce help message") + ("mwsave,m", bpo::value(), "morrowind .ess save file") + ("output,o", bpo::value(), "output file (.omwsave)") + ("compare,c", "compare two .ess files") + ; + p_desc.add("mwsave", 1).add("output", 1); + + bpo::variables_map vm; + + bpo::parsed_options parsed = bpo::command_line_parser(argc, argv) + .options(desc) + .positional(p_desc) + .run(); + + bpo::store(parsed, vm); + + if(vm.count("help") || !vm.count("mwsave") || !vm.count("output")) { + std::cout << desc; + return 0; + } + + bpo::notify(vm); + + std::string essFile = vm["mwsave"].as(); + std::string outputFile = vm["output"].as(); + + ESSImport::Importer importer(essFile, outputFile); + + if (vm.count("compare")) + importer.compare(); + else + importer.run(); + } + catch (std::exception& e) + { + std::cerr << "Error: " << e.what() << std::endl; + return 1; + } + + return 0; +} diff --git a/components/esm/esmreader.hpp b/components/esm/esmreader.hpp index 1549e15f5..642ec4168 100644 --- a/components/esm/esmreader.hpp +++ b/components/esm/esmreader.hpp @@ -36,6 +36,7 @@ public: const std::string getAuthor() const { return mHeader.mData.author.toString(); } const std::string getDesc() const { return mHeader.mData.desc.toString(); } const std::vector &getGameFiles() const { return mHeader.mMaster; } + const Header& getHeader() const { return mHeader; } int getFormat() const; const NAME &retSubName() const { return mCtx.subName; } uint32_t getSubSize() const { return mCtx.leftSub; } diff --git a/components/esm/loadcell.cpp b/components/esm/loadcell.cpp index e4f847dec..f6649d94a 100644 --- a/components/esm/loadcell.cpp +++ b/components/esm/loadcell.cpp @@ -111,6 +111,11 @@ void Cell::loadData(ESMReader &esm) } esm.getHNT(mData, "DATA", 12); + + // ess only, unknown + char nam8[32]; + if (esm.isNextSub("NAM8")) + esm.getHExact(nam8, 32); } void Cell::postLoad(ESMReader &esm) diff --git a/components/esm/loadnpcc.hpp b/components/esm/loadnpcc.hpp index c87c2545f..6c087fd01 100644 --- a/components/esm/loadnpcc.hpp +++ b/components/esm/loadnpcc.hpp @@ -17,26 +17,29 @@ class ESMWriter; * * Some general observations about savegames: * - * Magical items/potions/spells/etc are added normally as new ALCH, - * SPEL, etc. records, with unique numeric identifiers. - * - * Books with ability enhancements are listed in the save if they have - * been read. - * - * GLOB records set global variables. - * * SCPT records do not define new scripts, but assign values to the * variables of existing ones. * * STLN - stolen items, ONAM is the owner * - * GAME - contains a GMDT (game data) of unknown format - * - * VFXM, SPLM, KLST - no clue + * GAME - weather data + * struct GMDT + { + char mCellName[64]; + int mFogColour; + float mFogDensity; + int mCurrentWeather, mNextWeather; + int mWeatherTransition; // 0-100 transition between weathers, top 3 bytes may be garbage + float mTimeOfNextTransition; // weather changes when gamehour == timeOfNextTransition + int masserPhase, secundaPhase; // top 3 bytes may be garbage + }; + * + * VFXM, SPLM - no clue + * KLST - kill counter * * PCDT - seems to contain a lot of DNAMs, strings? * - * FMAP - MAPH and MAPD, probably map data. + * FMAP - MAPH and MAPD, global map image. * * JOUR - the entire journal in html * @@ -44,6 +47,8 @@ class ESMWriter; * ones you have done or begun. * * REGN - lists all regions in the game, even unvisited ones. + * notable differences to Regions in ESM files: mMapColor may be missing, and includes an unknown WNAM subrecord. + * * * The DIAL/INFO blocks contain changes to characters' dialog status. * diff --git a/components/esm/loadscpt.cpp b/components/esm/loadscpt.cpp index 07561c2ea..f2f13d086 100644 --- a/components/esm/loadscpt.cpp +++ b/components/esm/loadscpt.cpp @@ -92,6 +92,32 @@ void Script::load(ESMReader &esm) if (esm.isNextSub("SCVR")) { esm.skipHSub(); } + + // ESS only, TODO: move + if (esm.isNextSub("SLCS")) + { + esm.getSubHeader(); + char unknown[12]; + esm.getExact(unknown, 12); + } + if (esm.isNextSub("SLSD")) + { + float read; + esm.getSubHeader(); + // values of variables? + for (int i=0; i mSCRD; // Used in .ess savegames only, screenshot? + std::vector mSCRS; // Used in .ess savegames only, screenshot? + Data mData; int mFormat; std::vector mMaster;