Implement JSON to TLK tool

While at it, make TLK to JSON tool output SoundResRef.
This commit is contained in:
Vsevolod Kremianskii 2021-04-10 15:13:52 +07:00
parent ddb680c120
commit 6905d35e1f
13 changed files with 223 additions and 28 deletions

View file

@ -114,6 +114,7 @@ set(RESOURCE_HEADERS
src/resource/format/rimwriter.h
src/resource/format/ssfreader.h
src/resource/format/tlkreader.h
src/resource/format/tlkwriter.h
src/resource/format/visreader.h
src/resource/keybifprovider.h
src/resource/resourceprovider.h
@ -143,6 +144,7 @@ set(RESOURCE_SOURCES
src/resource/format/rimwriter.cpp
src/resource/format/ssfreader.cpp
src/resource/format/tlkreader.cpp
src/resource/format/tlkwriter.cpp
src/resource/format/visreader.cpp
src/resource/keybifprovider.cpp
src/resource/resources.cpp

View file

@ -45,6 +45,11 @@ public:
void putBytes(const ByteArray &bytes);
void putBytes(int count, uint8_t val = 0);
template <class T>
void putStruct(const T &val) {
_stream->write(reinterpret_cast<const char *>(&val), sizeof(T));
}
size_t tell() const;
private:

View file

@ -44,7 +44,6 @@ void TlkReader::doLoad() {
void TlkReader::loadStrings() {
_table = make_shared<TalkTable>();
_table->_strings.reserve(_stringCount);
for (uint32_t i = 0; i < _stringCount; ++i) {
uint32_t flags = readUint32();
@ -58,15 +57,14 @@ void TlkReader::loadStrings() {
uint32_t stringSize = readUint32();
float soundLength = readFloat();
TalkTableString string;
TalkTableString tableString;
if (flags & StringFlags::textPresent) {
string.text = readString(_stringsOffset + stringOffset, stringSize);
tableString.text = readString(_stringsOffset + stringOffset, stringSize);
}
if (flags & StringFlags::soundPresent) {
string.soundResRef = soundResRef;
tableString.soundResRef = soundResRef;
}
_table->_strings.push_back(string);
_table->addString(move(tableString));
}
}

View file

@ -0,0 +1,87 @@
/*
* Copyright (c) 2020-2021 The reone project contributors
*
* This program 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.
*
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "tlkwriter.h"
#include <stdexcept>
#include <boost/filesystem.hpp>
#include "../../common/streamwriter.h"
#include "../../common/stringutil.h"
using namespace std;
namespace fs = boost::filesystem;
namespace reone {
namespace resource {
struct StringFlags {
static constexpr int textPresent = 1;
static constexpr int soundPresent = 2;
static constexpr int soundLengthPresent = 4;
};
TlkWriter::TlkWriter(shared_ptr<TalkTable> talkTable) : _talkTable(talkTable) {
if (!talkTable) {
throw invalid_argument("talkTable must not be null");
}
}
void TlkWriter::save(const fs::path &path) {
vector<StringDataElement> strData;
uint32_t offString = 0;
for (int i = 0; i < _talkTable->getStringCount(); ++i) {
const TalkTableString &str = _talkTable->getString(i);
auto strSize = static_cast<uint32_t>(str.text.length());
StringDataElement strDataElem;
strDataElem.flags = 7; // all strings in dialog.tlk have this
strncpy(strDataElem.soundResRef, str.soundResRef.c_str(), str.soundResRef.length());
memset(strDataElem.soundResRef, 0, 16);
strDataElem.offString = offString;
strDataElem.stringSize = strSize;
strData.push_back(move(strDataElem));
offString += strSize + 1;
}
auto tlk = make_shared<fs::ofstream>(path, ios::binary);
StreamWriter writer(tlk);
FileHeader fileHeader;
fileHeader.languageId = 0;
fileHeader.numStrings = _talkTable->getStringCount();
fileHeader.offStringEntries = 8 + sizeof(FileHeader) + _talkTable->getStringCount() * sizeof(StringDataElement);
writer.putString("TLK V3.0");
writer.putStruct(fileHeader);
for (int i = 0; i < _talkTable->getStringCount(); ++i) {
writer.putStruct(strData[i]);
}
for (int i = 0; i < _talkTable->getStringCount(); ++i) {
writer.putCString(_talkTable->getString(i).text);
}
}
} // namespace resource
} // namespace reone

View file

@ -0,0 +1,60 @@
/*
* Copyright (c) 2020-2021 The reone project contributors
*
* This program 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.
*
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <cstdint>
#include <memory>
#include <boost/filesystem/path.hpp>
#include <boost/noncopyable.hpp>
#include "../talktable.h"
namespace reone {
namespace resource {
class TlkWriter : boost::noncopyable {
public:
TlkWriter(std::shared_ptr<TalkTable> talkTable);
void save(const boost::filesystem::path &path);
private:
struct FileHeader {
uint32_t languageId { 0 };
uint32_t numStrings { 0 };
uint32_t offStringEntries { 0 };
};
struct StringDataElement {
uint32_t flags { 0 };
char soundResRef[16];
uint32_t volumeVariance { 0 };
uint32_t pitchVariance { 0 };
uint32_t offString { 0 };
uint32_t stringSize { 0 };
float soundLength { 0.0f };
};
std::shared_ptr<TalkTable> _talkTable;
};
} // namespace resource
} // namespace reone

View file

@ -17,16 +17,27 @@
#include "talktable.h"
#include <stdexcept>
using namespace std;
namespace reone {
namespace resource {
void TalkTable::addString(TalkTableString &&string) {
_strings.push_back(string);
}
int TalkTable::getStringCount() const {
return static_cast<int>(_strings.size());
}
const TalkTableString &TalkTable::getString(int32_t ref) const {
return _strings[ref];
const TalkTableString &TalkTable::getString(int index) const {
if (index < 0 || index >= static_cast<int>(_strings.size())) {
throw out_of_range("index is out of range");
}
return _strings[index];
}
} // namespace resource

View file

@ -33,13 +33,13 @@ struct TalkTableString {
struct TalkTable : boost::noncopyable {
public:
void addString(TalkTableString &&string);
int getStringCount() const;
const TalkTableString &getString(int32_t ref) const;
const TalkTableString &getString(int index) const;
private:
std::vector<TalkTableString> _strings;
friend class TlkReader;
};
} // namespace resource

View file

@ -236,14 +236,14 @@ static unique_ptr<GffStruct> treeToGffStruct(const pt::ptree &tree) {
}
void GffTool::toGFF(const fs::path &path, const fs::path &destPath) {
pt::ptree tree;
pt::read_json(path.string(), tree);
if (path.extension() != ".json") {
cerr << "Input file must have extension '.json'" << endl;
return;
}
pt::ptree tree;
pt::read_json(path.string(), tree);
fs::path extensionless(path);
extensionless.replace_extension();
ResourceType resType = getResTypeByExt(extensionless.extension().string().substr(1));

View file

@ -53,7 +53,8 @@ static const unordered_map<string, Operation> g_operations {
{ "to-erf", Operation::ToERF },
{ "to-mod", Operation::ToMOD },
{ "to-pth", Operation::ToPTH },
{ "to-ascii", Operation::ToASCII }
{ "to-ascii", Operation::ToASCII },
{ "to-tlk", Operation::ToTLK }
};
Program::Program(int argc, char **argv) : _argc(argc), _argv(argv) {
@ -66,7 +67,7 @@ int Program::run() {
switch (_operation) {
case Operation::None:
cout << _cmdLineOpts << endl;
cout << _optsCmdLine << endl;
break;
default: {
auto tool = getTool();
@ -83,11 +84,11 @@ int Program::run() {
}
void Program::initOptions() {
_commonOpts.add_options()
_optsCommon.add_options()
("game", po::value<string>(), "path to game directory")
("dest", po::value<string>(), "path to destination directory");
_cmdLineOpts.add(_commonOpts).add_options()
_optsCmdLine.add(_optsCommon).add_options()
("list", "list file contents")
("extract", "extract file contents")
("to-json", "convert 2DA, GFF or TLK file to JSON")
@ -99,6 +100,7 @@ void Program::initOptions() {
("to-mod", "create MOD archive from directory")
("to-pth", "convert ASCII PTH to binary PTH")
("to-ascii", "convert binary PTH to ASCII")
("to-tlk", "convert JSON to TLK")
("target", po::value<string>(), "target name or path to input file");
}
@ -119,14 +121,14 @@ void Program::loadOptions() {
positional.add("target", 1);
po::parsed_options parsedCmdLineOpts = po::command_line_parser(_argc, _argv)
.options(_cmdLineOpts)
.options(_optsCmdLine)
.positional(positional)
.run();
po::variables_map vars;
po::store(parsedCmdLineOpts, vars);
if (fs::exists(kConfigFilename)) {
po::store(po::parse_config_file<char>(kConfigFilename, _commonOpts), vars);
po::store(po::parse_config_file<char>(kConfigFilename, _optsCommon), vars);
}
po::notify(vars);

View file

@ -43,8 +43,8 @@ private:
int _argc;
char **_argv;
boost::program_options::options_description _commonOpts;
boost::program_options::options_description _cmdLineOpts { "Usage" };
boost::program_options::options_description _optsCommon;
boost::program_options::options_description _optsCmdLine { "Usage" };
boost::filesystem::path _gamePath;
boost::filesystem::path _destPath;

View file

@ -20,6 +20,7 @@
#include <boost/property_tree/json_parser.hpp>
#include "../src/resource/format/tlkreader.h"
#include "../src/resource/format/tlkwriter.h"
using namespace std;
@ -35,6 +36,8 @@ namespace tools {
void TlkTool::invoke(Operation operation, const fs::path &target, const fs::path &gamePath, const fs::path &destPath) {
if (operation == Operation::ToJSON) {
toJSON(target, destPath);
} else if (operation == Operation::ToTLK) {
toTLK(target, destPath);
}
}
@ -47,11 +50,12 @@ void TlkTool::toJSON(const fs::path &path, const fs::path &destPath) {
shared_ptr<TalkTable> table(tlk.table());
for (int i = 0; i < table->getStringCount(); ++i) {
string s(table->getString(i).text);
const TalkTableString &tableString = table->getString(i);
pt::ptree child;
child.put("index", i);
child.put("string", s);
child.put("_index", i);
child.put("text", tableString.text);
child.put("soundResRef", tableString.soundResRef);
children.push_back(make_pair("", child));
}
@ -63,11 +67,35 @@ void TlkTool::toJSON(const fs::path &path, const fs::path &destPath) {
pt::write_json(jsonPath.string(), tree);
}
void TlkTool::toTLK(const fs::path &path, const fs::path &destPath) {
pt::ptree tree;
pt::read_json(path.string(), tree);
auto talkTable = make_shared<TalkTable>();
for (auto &str : tree.get_child("strings")) {
TalkTableString talkTableString;
talkTableString.text = str.second.get("text", "");
talkTableString.soundResRef = str.second.get("soundResRef", "");
talkTable->addString(move(talkTableString));
}
fs::path extensionless(path);
extensionless.replace_extension(); // remove .json
if (extensionless.extension() == ".tlk") {
extensionless.replace_extension();
}
fs::path tlkPath(destPath);
tlkPath.append(extensionless.filename().string() + ".tlk");
TlkWriter writer(move(talkTable));
writer.save(tlkPath);
}
bool TlkTool::supports(Operation operation, const fs::path &target) const {
return
!fs::is_directory(target) &&
target.extension() == ".tlk" &&
operation == Operation::ToJSON;
((target.extension() == ".tlk" && operation == Operation::ToJSON) || (target.extension() == ".json" && operation == Operation::ToTLK));
}
} // namespace tools

View file

@ -132,6 +132,7 @@ public:
private:
void toJSON(const boost::filesystem::path &path, const boost::filesystem::path &destPath);
void toTLK(const boost::filesystem::path &path, const boost::filesystem::path &destPath);
};
class TpcTool : public ITool {

View file

@ -33,7 +33,8 @@ enum class Operation {
ToERF,
ToMOD,
ToPTH,
ToASCII
ToASCII,
ToTLK
};
} // namespace tools