You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
585 lines
18 KiB
C++
585 lines
18 KiB
C++
#include <iostream>
|
|
#include <vector>
|
|
#include <deque>
|
|
#include <list>
|
|
#include <map>
|
|
#include <set>
|
|
|
|
#include <boost/program_options.hpp>
|
|
|
|
#include <components/esm/esmreader.hpp>
|
|
#include <components/esm/esmwriter.hpp>
|
|
#include <components/esm/records.hpp>
|
|
|
|
#include "record.hpp"
|
|
|
|
#define ESMTOOL_VERSION 1.2
|
|
|
|
// Create a local alias for brevity
|
|
namespace bpo = boost::program_options;
|
|
|
|
struct ESMData
|
|
{
|
|
std::string author;
|
|
std::string description;
|
|
unsigned int version;
|
|
std::vector<ESM::Header::MasterData> masters;
|
|
|
|
std::deque<EsmTool::RecordBase *> mRecords;
|
|
std::map<ESM::Cell *, std::deque<ESM::CellRef> > mCellRefs;
|
|
std::map<int, int> mRecordStats;
|
|
|
|
static const std::set<int> sLabeledRec;
|
|
};
|
|
|
|
static const int sLabeledRecIds[] = {
|
|
ESM::REC_GLOB, ESM::REC_CLAS, ESM::REC_FACT, ESM::REC_RACE, ESM::REC_SOUN,
|
|
ESM::REC_REGN, ESM::REC_BSGN, ESM::REC_LTEX, ESM::REC_STAT, ESM::REC_DOOR,
|
|
ESM::REC_MISC, ESM::REC_WEAP, ESM::REC_CONT, ESM::REC_SPEL, ESM::REC_CREA,
|
|
ESM::REC_BODY, ESM::REC_LIGH, ESM::REC_ENCH, ESM::REC_NPC_, ESM::REC_ARMO,
|
|
ESM::REC_CLOT, ESM::REC_REPA, ESM::REC_ACTI, ESM::REC_APPA, ESM::REC_LOCK,
|
|
ESM::REC_PROB, ESM::REC_INGR, ESM::REC_BOOK, ESM::REC_ALCH, ESM::REC_LEVI,
|
|
ESM::REC_LEVC, ESM::REC_SNDG, ESM::REC_CELL, ESM::REC_DIAL
|
|
};
|
|
|
|
const std::set<int> ESMData::sLabeledRec =
|
|
std::set<int>(sLabeledRecIds, sLabeledRecIds + 34);
|
|
|
|
// Based on the legacy struct
|
|
struct Arguments
|
|
{
|
|
bool raw_given;
|
|
bool quiet_given;
|
|
bool loadcells_given;
|
|
bool plain_given;
|
|
|
|
std::string mode;
|
|
std::string encoding;
|
|
std::string filename;
|
|
std::string outname;
|
|
|
|
std::vector<std::string> types;
|
|
std::string name;
|
|
|
|
ESMData data;
|
|
ESM::ESMReader reader;
|
|
ESM::ESMWriter writer;
|
|
};
|
|
|
|
bool parseOptions (int argc, char** argv, Arguments &info)
|
|
{
|
|
bpo::options_description desc("Inspect and extract from Morrowind ES files (ESM, ESP, ESS)\nSyntax: esmtool [options] mode infile [outfile]\nAllowed modes:\n dump\t Dumps all readable data from the input file.\n clone\t Clones the input file to the output file.\n comp\t Compares the given files.\n\nAllowed options");
|
|
|
|
desc.add_options()
|
|
("help,h", "print help message.")
|
|
("version,v", "print version information and quit.")
|
|
("raw,r", "Show an unformatted list of all records and subrecords.")
|
|
// The intention is that this option would interact better
|
|
// with other modes including clone, dump, and raw.
|
|
("type,t", bpo::value< std::vector<std::string> >(),
|
|
"Show only records of this type (four character record code). May "
|
|
"be specified multiple times. Only affects dump mode.")
|
|
("name,n", bpo::value<std::string>(),
|
|
"Show only the record with this name. Only affects dump mode.")
|
|
("plain,p", "Print contents of dialogs, books and scripts. "
|
|
"(skipped by default)"
|
|
"Only affects dump mode.")
|
|
("quiet,q", "Supress all record information. Useful for speed tests.")
|
|
("loadcells,C", "Browse through contents of all cells.")
|
|
|
|
( "encoding,e", bpo::value<std::string>(&(info.encoding))->
|
|
default_value("win1252"),
|
|
"Character encoding used in ESMTool:\n"
|
|
"\n\twin1250 - Central and Eastern European such as Polish, Czech, Slovak, Hungarian, Slovene, Bosnian, Croatian, Serbian (Latin script), Romanian and Albanian languages\n"
|
|
"\n\twin1251 - Cyrillic alphabet such as Russian, Bulgarian, Serbian Cyrillic and other languages\n"
|
|
"\n\twin1252 - Western European (Latin) alphabet, used by default")
|
|
;
|
|
|
|
std::string finalText = "\nIf no option is given, the default action is to parse all records in the archive\nand display diagnostic information.";
|
|
|
|
// input-file is hidden and used as a positional argument
|
|
bpo::options_description hidden("Hidden Options");
|
|
|
|
hidden.add_options()
|
|
( "mode,m", bpo::value<std::string>(), "esmtool mode")
|
|
( "input-file,i", bpo::value< std::vector<std::string> >(), "input file")
|
|
;
|
|
|
|
bpo::positional_options_description p;
|
|
p.add("mode", 1).add("input-file", 2);
|
|
|
|
// there might be a better way to do this
|
|
bpo::options_description all;
|
|
all.add(desc).add(hidden);
|
|
bpo::variables_map variables;
|
|
|
|
try
|
|
{
|
|
bpo::parsed_options valid_opts = bpo::command_line_parser(argc, argv)
|
|
.options(all).positional(p).run();
|
|
|
|
bpo::store(valid_opts, variables);
|
|
}
|
|
catch(boost::program_options::unknown_option & x)
|
|
{
|
|
std::cerr << "ERROR: " << x.what() << std::endl;
|
|
return false;
|
|
}
|
|
catch(boost::program_options::invalid_command_line_syntax & x)
|
|
{
|
|
std::cerr << "ERROR: " << x.what() << std::endl;
|
|
return false;
|
|
}
|
|
|
|
bpo::notify(variables);
|
|
|
|
if (variables.count ("help"))
|
|
{
|
|
std::cout << desc << finalText << std::endl;
|
|
return false;
|
|
}
|
|
if (variables.count ("version"))
|
|
{
|
|
std::cout << "ESMTool version " << ESMTOOL_VERSION << std::endl;
|
|
return false;
|
|
}
|
|
if (!variables.count("mode"))
|
|
{
|
|
std::cout << "No mode specified!" << std::endl << std::endl
|
|
<< desc << finalText << std::endl;
|
|
return false;
|
|
}
|
|
|
|
if (variables.count("type") > 0)
|
|
info.types = variables["type"].as< std::vector<std::string> >();
|
|
if (variables.count("name") > 0)
|
|
info.name = variables["name"].as<std::string>();
|
|
|
|
info.mode = variables["mode"].as<std::string>();
|
|
if (!(info.mode == "dump" || info.mode == "clone" || info.mode == "comp"))
|
|
{
|
|
std::cout << std::endl << "ERROR: invalid mode \"" << info.mode << "\"" << std::endl << std::endl
|
|
<< desc << finalText << std::endl;
|
|
return false;
|
|
}
|
|
|
|
if ( !variables.count("input-file") )
|
|
{
|
|
std::cout << "\nERROR: missing ES file\n\n";
|
|
std::cout << desc << finalText << std::endl;
|
|
return false;
|
|
}
|
|
|
|
// handling gracefully the user adding multiple files
|
|
/* if (variables["input-file"].as< std::vector<std::string> >().size() > 1)
|
|
{
|
|
std::cout << "\nERROR: more than one ES file specified\n\n";
|
|
std::cout << desc << finalText << std::endl;
|
|
return false;
|
|
}*/
|
|
|
|
info.filename = variables["input-file"].as< std::vector<std::string> >()[0];
|
|
if (variables["input-file"].as< std::vector<std::string> >().size() > 1)
|
|
info.outname = variables["input-file"].as< std::vector<std::string> >()[1];
|
|
|
|
info.raw_given = variables.count ("raw") != 0;
|
|
info.quiet_given = variables.count ("quiet") != 0;
|
|
info.loadcells_given = variables.count ("loadcells") != 0;
|
|
info.plain_given = variables.count("plain") != 0;
|
|
|
|
// Font encoding settings
|
|
info.encoding = variables["encoding"].as<std::string>();
|
|
if(info.encoding != "win1250" && info.encoding != "win1251" && info.encoding != "win1252")
|
|
{
|
|
std::cout << info.encoding << " is not a valid encoding option." << std::endl;
|
|
info.encoding = "win1252";
|
|
}
|
|
std::cout << ToUTF8::encodingUsingMessage(info.encoding) << std::endl;
|
|
|
|
return true;
|
|
}
|
|
|
|
void printRaw(ESM::ESMReader &esm);
|
|
void loadCell(ESM::Cell &cell, ESM::ESMReader &esm, Arguments& info);
|
|
|
|
int load(Arguments& info);
|
|
int clone(Arguments& info);
|
|
int comp(Arguments& info);
|
|
|
|
int main(int argc, char**argv)
|
|
{
|
|
try
|
|
{
|
|
Arguments info;
|
|
if(!parseOptions (argc, argv, info))
|
|
return 1;
|
|
|
|
if (info.mode == "dump")
|
|
return load(info);
|
|
else if (info.mode == "clone")
|
|
return clone(info);
|
|
else if (info.mode == "comp")
|
|
return comp(info);
|
|
else
|
|
{
|
|
std::cout << "Invalid or no mode specified, dying horribly. Have a nice day." << std::endl;
|
|
return 1;
|
|
}
|
|
}
|
|
catch (std::exception& e)
|
|
{
|
|
std::cerr << "ERROR: " << e.what() << std::endl;
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void loadCell(ESM::Cell &cell, ESM::ESMReader &esm, Arguments& info)
|
|
{
|
|
bool quiet = (info.quiet_given || info.mode == "clone");
|
|
bool save = (info.mode == "clone");
|
|
|
|
// Skip back to the beginning of the reference list
|
|
// FIXME: Changes to the references backend required to support multiple plugins have
|
|
// almost certainly broken this following line. I'll leave it as is for now, so that
|
|
// the compiler does not complain.
|
|
cell.restore(esm, 0);
|
|
|
|
// Loop through all the references
|
|
ESM::CellRef ref;
|
|
if(!quiet) std::cout << " References:\n";
|
|
|
|
bool deleted = false;
|
|
while(cell.getNextRef(esm, ref, deleted))
|
|
{
|
|
if (save) {
|
|
info.data.mCellRefs[&cell].push_back(ref);
|
|
}
|
|
|
|
if(quiet) continue;
|
|
|
|
std::cout << " Refnum: " << ref.mRefNum.mIndex << std::endl;
|
|
std::cout << " ID: '" << ref.mRefID << "'\n";
|
|
std::cout << " Owner: '" << ref.mOwner << "'\n";
|
|
std::cout << " Global: '" << ref.mGlobalVariable << "'" << std::endl;
|
|
std::cout << " Faction: '" << ref.mFaction << "'" << std::endl;
|
|
std::cout << " Faction rank: '" << ref.mFactionRank << "'" << std::endl;
|
|
std::cout << " Enchantment charge: '" << ref.mEnchantmentCharge << "'\n";
|
|
std::cout << " Uses/health: '" << ref.mChargeInt << "'\n";
|
|
std::cout << " Gold value: '" << ref.mGoldValue << "'\n";
|
|
std::cout << " Blocked: '" << static_cast<int>(ref.mReferenceBlocked) << "'" << std::endl;
|
|
std::cout << " Deleted: " << deleted << std::endl;
|
|
if (!ref.mKey.empty())
|
|
std::cout << " Key: '" << ref.mKey << "'" << std::endl;
|
|
}
|
|
}
|
|
|
|
void printRaw(ESM::ESMReader &esm)
|
|
{
|
|
while(esm.hasMoreRecs())
|
|
{
|
|
ESM::NAME n = esm.getRecName();
|
|
std::cout << "Record: " << n.toString() << std::endl;
|
|
esm.getRecHeader();
|
|
while(esm.hasMoreSubs())
|
|
{
|
|
uint64_t offs = esm.getOffset();
|
|
esm.getSubName();
|
|
esm.skipHSub();
|
|
n = esm.retSubName();
|
|
std::ios::fmtflags f(std::cout.flags());
|
|
std::cout << " " << n.toString() << " - " << esm.getSubSize()
|
|
<< " bytes @ 0x" << std::hex << offs << "\n";
|
|
std::cout.flags(f);
|
|
}
|
|
}
|
|
}
|
|
|
|
int load(Arguments& info)
|
|
{
|
|
ESM::ESMReader& esm = info.reader;
|
|
ToUTF8::Utf8Encoder encoder (ToUTF8::calculateEncoding(info.encoding));
|
|
esm.setEncoder(&encoder);
|
|
|
|
std::string filename = info.filename;
|
|
std::cout << "Loading file: " << filename << std::endl;
|
|
|
|
std::list<int> skipped;
|
|
|
|
try {
|
|
|
|
if(info.raw_given && info.mode == "dump")
|
|
{
|
|
std::cout << "RAW file listing:\n";
|
|
|
|
esm.openRaw(filename);
|
|
|
|
printRaw(esm);
|
|
|
|
return 0;
|
|
}
|
|
|
|
bool quiet = (info.quiet_given || info.mode == "clone");
|
|
bool loadCells = (info.loadcells_given || info.mode == "clone");
|
|
bool save = (info.mode == "clone");
|
|
|
|
esm.open(filename);
|
|
|
|
info.data.author = esm.getAuthor();
|
|
info.data.description = esm.getDesc();
|
|
info.data.masters = esm.getGameFiles();
|
|
|
|
if (!quiet)
|
|
{
|
|
std::cout << "Author: " << esm.getAuthor() << std::endl
|
|
<< "Description: " << esm.getDesc() << std::endl
|
|
<< "File format version: " << esm.getFVer() << std::endl;
|
|
std::vector<ESM::Header::MasterData> m = esm.getGameFiles();
|
|
if (!m.empty())
|
|
{
|
|
std::cout << "Masters:" << std::endl;
|
|
for(unsigned int i=0;i<m.size();i++)
|
|
std::cout << " " << m[i].name << ", " << m[i].size << " bytes" << std::endl;
|
|
}
|
|
}
|
|
|
|
// Loop through all records
|
|
while(esm.hasMoreRecs())
|
|
{
|
|
ESM::NAME n = esm.getRecName();
|
|
uint32_t flags;
|
|
esm.getRecHeader(flags);
|
|
|
|
// Is the user interested in this record type?
|
|
bool interested = true;
|
|
if (!info.types.empty())
|
|
{
|
|
std::vector<std::string>::iterator match;
|
|
match = std::find(info.types.begin(), info.types.end(),
|
|
n.toString());
|
|
if (match == info.types.end()) interested = false;
|
|
}
|
|
|
|
std::string id = esm.getHNOString("NAME");
|
|
if (id.empty())
|
|
id = esm.getHNOString("INAM");
|
|
|
|
if (!info.name.empty() && !Misc::StringUtils::ciEqual(info.name, id))
|
|
interested = false;
|
|
|
|
if(!quiet && interested)
|
|
std::cout << "\nRecord: " << n.toString()
|
|
<< " '" << id << "'\n";
|
|
|
|
EsmTool::RecordBase *record = EsmTool::RecordBase::create(n);
|
|
|
|
if (record == 0) {
|
|
if (std::find(skipped.begin(), skipped.end(), n.val) == skipped.end())
|
|
{
|
|
std::cout << "Skipping " << n.toString() << " records." << std::endl;
|
|
skipped.push_back(n.val);
|
|
}
|
|
|
|
esm.skipRecord();
|
|
if (quiet) break;
|
|
std::cout << " Skipping\n";
|
|
} else {
|
|
if (record->getType().val == ESM::REC_GMST) {
|
|
// preset id for GameSetting record
|
|
record->cast<ESM::GameSetting>()->get().mId = id;
|
|
}
|
|
record->setId(id);
|
|
record->setFlags((int) flags);
|
|
record->setPrintPlain(info.plain_given);
|
|
record->load(esm);
|
|
if (!quiet && interested) record->print();
|
|
|
|
if (record->getType().val == ESM::REC_CELL && loadCells && interested) {
|
|
loadCell(record->cast<ESM::Cell>()->get(), esm, info);
|
|
}
|
|
|
|
if (save) {
|
|
info.data.mRecords.push_back(record);
|
|
} else {
|
|
delete record;
|
|
}
|
|
++info.data.mRecordStats[n.val];
|
|
}
|
|
}
|
|
|
|
} catch(std::exception &e) {
|
|
std::cout << "\nERROR:\n\n " << e.what() << std::endl;
|
|
|
|
typedef std::deque<EsmTool::RecordBase *> RecStore;
|
|
RecStore &store = info.data.mRecords;
|
|
for (RecStore::iterator it = store.begin(); it != store.end(); ++it)
|
|
{
|
|
delete *it;
|
|
}
|
|
store.clear();
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
#include <iomanip>
|
|
|
|
int clone(Arguments& info)
|
|
{
|
|
if (info.outname.empty())
|
|
{
|
|
std::cout << "You need to specify an output name" << std::endl;
|
|
return 1;
|
|
}
|
|
|
|
if (load(info) != 0)
|
|
{
|
|
std::cout << "Failed to load, aborting." << std::endl;
|
|
return 1;
|
|
}
|
|
|
|
size_t recordCount = info.data.mRecords.size();
|
|
|
|
int digitCount = 1; // For a nicer output
|
|
if (recordCount > 9) ++digitCount;
|
|
if (recordCount > 99) ++digitCount;
|
|
if (recordCount > 999) ++digitCount;
|
|
if (recordCount > 9999) ++digitCount;
|
|
if (recordCount > 99999) ++digitCount;
|
|
if (recordCount > 999999) ++digitCount;
|
|
|
|
std::cout << "Loaded " << recordCount << " records:" << std::endl << std::endl;
|
|
|
|
ESM::NAME name;
|
|
|
|
int i = 0;
|
|
typedef std::map<int, int> Stats;
|
|
Stats &stats = info.data.mRecordStats;
|
|
for (Stats::iterator it = stats.begin(); it != stats.end(); ++it)
|
|
{
|
|
name.val = it->first;
|
|
int amount = it->second;
|
|
std::cout << std::setw(digitCount) << amount << " " << name.toString() << " ";
|
|
|
|
if (++i % 3 == 0)
|
|
std::cout << std::endl;
|
|
}
|
|
|
|
if (i % 3 != 0)
|
|
std::cout << std::endl;
|
|
|
|
std::cout << std::endl << "Saving records to: " << info.outname << "..." << std::endl;
|
|
|
|
ESM::ESMWriter& esm = info.writer;
|
|
ToUTF8::Utf8Encoder encoder (ToUTF8::calculateEncoding(info.encoding));
|
|
esm.setEncoder(&encoder);
|
|
esm.setAuthor(info.data.author);
|
|
esm.setDescription(info.data.description);
|
|
esm.setVersion(info.data.version);
|
|
esm.setRecordCount (recordCount);
|
|
|
|
for (std::vector<ESM::Header::MasterData>::iterator it = info.data.masters.begin(); it != info.data.masters.end(); ++it)
|
|
esm.addMaster(it->name, it->size);
|
|
|
|
std::fstream save(info.outname.c_str(), std::fstream::out | std::fstream::binary);
|
|
esm.save(save);
|
|
|
|
int saved = 0;
|
|
typedef std::deque<EsmTool::RecordBase *> Records;
|
|
Records &records = info.data.mRecords;
|
|
for (Records::iterator it = records.begin(); it != records.end() && i > 0; ++it)
|
|
{
|
|
EsmTool::RecordBase *record = *it;
|
|
|
|
name.val = record->getType().val;
|
|
|
|
esm.startRecord(name.toString(), record->getFlags());
|
|
|
|
// TODO wrap this with std::set
|
|
if (ESMData::sLabeledRec.count(name.val) > 0) {
|
|
esm.writeHNCString("NAME", record->getId());
|
|
} else {
|
|
esm.writeHNOString("NAME", record->getId());
|
|
}
|
|
|
|
record->save(esm);
|
|
|
|
if (name.val == ESM::REC_CELL) {
|
|
ESM::Cell *ptr = &record->cast<ESM::Cell>()->get();
|
|
if (!info.data.mCellRefs[ptr].empty()) {
|
|
typedef std::deque<ESM::CellRef> RefList;
|
|
RefList &refs = info.data.mCellRefs[ptr];
|
|
for (RefList::iterator refIt = refs.begin(); refIt != refs.end(); ++refIt)
|
|
{
|
|
refIt->save(esm);
|
|
}
|
|
}
|
|
}
|
|
|
|
esm.endRecord(name.toString());
|
|
|
|
saved++;
|
|
int perc = (int)((saved / (float)recordCount)*100);
|
|
if (perc % 10 == 0)
|
|
{
|
|
std::cerr << "\r" << perc << "%";
|
|
}
|
|
}
|
|
|
|
std::cout << "\rDone!" << std::endl;
|
|
|
|
esm.close();
|
|
save.close();
|
|
|
|
return 0;
|
|
}
|
|
|
|
int comp(Arguments& info)
|
|
{
|
|
if (info.filename.empty() || info.outname.empty())
|
|
{
|
|
std::cout << "You need to specify two input files" << std::endl;
|
|
return 1;
|
|
}
|
|
|
|
Arguments fileOne;
|
|
Arguments fileTwo;
|
|
|
|
fileOne.raw_given = 0;
|
|
fileTwo.raw_given = 0;
|
|
|
|
fileOne.mode = "clone";
|
|
fileTwo.mode = "clone";
|
|
|
|
fileOne.encoding = info.encoding;
|
|
fileTwo.encoding = info.encoding;
|
|
|
|
fileOne.filename = info.filename;
|
|
fileTwo.filename = info.outname;
|
|
|
|
if (load(fileOne) != 0)
|
|
{
|
|
std::cout << "Failed to load " << info.filename << ", aborting comparison." << std::endl;
|
|
return 1;
|
|
}
|
|
|
|
if (load(fileTwo) != 0)
|
|
{
|
|
std::cout << "Failed to load " << info.outname << ", aborting comparison." << std::endl;
|
|
return 1;
|
|
}
|
|
|
|
if (fileOne.data.mRecords.size() != fileTwo.data.mRecords.size())
|
|
{
|
|
std::cout << "Not equal, different amount of records." << std::endl;
|
|
return 1;
|
|
}
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
}
|