feat: Add ModuleProbe tool

ModuleProbe generates a JSON file describing a game module.
This commit is contained in:
Vsevolod Kremianskii 2021-01-18 00:48:16 +07:00
parent ceb4c21387
commit 6fd165cd38
7 changed files with 691 additions and 50 deletions

View file

@ -659,6 +659,7 @@ endif()
if(BUILD_TOOLS)
set(TOOLS_HEADERS
tools/moduleprobe.h
tools/program.h
tools/tools.h)
@ -669,6 +670,7 @@ if(BUILD_TOOLS)
tools/erftool.cpp
tools/gfftool.cpp
tools/keytool.cpp
tools/moduleprobe.cpp
tools/program.cpp
tools/rimtool.cpp
tools/tlktool.cpp

540
tools/moduleprobe.cpp Normal file
View file

@ -0,0 +1,540 @@
/*
* 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 "moduleprobe.h"
#include <stdexcept>
#include <boost/property_tree/json_parser.hpp>
#include "../src/common/pathutil.h"
#include "../src/common/streamutil.h"
#include "../src/resource/lytfile.h"
namespace fs = boost::filesystem;
namespace pt = boost::property_tree;
using namespace std;
using namespace reone::resource;
namespace reone {
namespace tools {
void ModuleProbe::probe(const string &name, const fs::path &gamePath, const fs::path &destPath) {
_gamePath = gamePath;
loadResources(name);
pt::ptree description(describeModule());
writeDescription(name, description, destPath);
}
void ModuleProbe::loadResources(const string &moduleName) {
fs::path keyPath(getPathIgnoreCase(_gamePath, "chitin.key", false));
if (!fs::exists(keyPath)) {
throw runtime_error("Key file not found: " + keyPath.string());
}
_keyFile.load(keyPath);
fs::path tlkPath(getPathIgnoreCase(_gamePath, "dialog.tlk", false));
if (!fs::exists(tlkPath)) {
throw runtime_error("TLK file not found: " + tlkPath.string());
}
TlkFile tlk;
tlk.load(tlkPath);
_talkTable = tlk.table();
fs::path modulesPath(getPathIgnoreCase(_gamePath, "modules", false));
if (!fs::exists(modulesPath)) {
throw runtime_error("Modules directory not found: " + modulesPath.string());
}
fs::path mainPath(getPathIgnoreCase(modulesPath, moduleName + ".rim", false));
if (!fs::exists(mainPath)) {
throw runtime_error("Main module RIM not found: " + mainPath.string());
}
_rimMain.load(mainPath);
fs::path blueprintsPath(getPathIgnoreCase(modulesPath, moduleName + "_s.rim", false));
if (!fs::exists(blueprintsPath)) {
throw runtime_error("Module blueprints RIM not found: " + blueprintsPath.string());
}
_rimBlueprints.load(blueprintsPath);
}
static void putIfNotEmpty(const std::string &name, std::string value, pt::ptree &tree) {
if (!value.empty()) {
tree.put(name, move(value));
}
}
pt::ptree ModuleProbe::describeModule() {
pt::ptree description;
GffFile ifo;
ifo.load(wrap(_rimMain.find("module", ResourceType::ModuleInfo)));
auto ifoGffs = ifo.top();
// Entry
pt::ptree entry;
entry.put("x", ifoGffs->getFloat("Mod_Entry_X"));
entry.put("y", ifoGffs->getFloat("Mod_Entry_Y"));
entry.put("z", ifoGffs->getFloat("Mod_Entry_Z"));
entry.put("dir_x", ifoGffs->getFloat("Mod_Entry_Dir_X"));
entry.put("dir_y", ifoGffs->getFloat("Mod_Entry_Dir_Y"));
description.add_child("entry", entry);
// END Entry
// Scripts
pt::ptree scripts;
putIfNotEmpty("on_heartbeat", ifoGffs->getString("Mod_OnHeartbeat"), scripts);
putIfNotEmpty("on_mod_load", ifoGffs->getString("Mod_OnModLoad"), scripts);
putIfNotEmpty("on_client_entr", ifoGffs->getString("Mod_OnClientEntr"), scripts);
putIfNotEmpty("on_client_leav", ifoGffs->getString("Mod_OnClientLeav"), scripts);
putIfNotEmpty("on_actvt_item", ifoGffs->getString("Mod_OnActvtItem"), scripts);
putIfNotEmpty("on_acquir_item", ifoGffs->getString("Mod_OnAcquirItem"), scripts);
putIfNotEmpty("on_usr_defined", ifoGffs->getString("Mod_OnUsrDefined"), scripts);
putIfNotEmpty("on_un_aqre_item", ifoGffs->getString("Mod_OnUnAqreItem"), scripts);
putIfNotEmpty("on_plr_death", ifoGffs->getString("Mod_OnPlrDeath"), scripts);
putIfNotEmpty("on_plr_dying", ifoGffs->getString("Mod_OnPlrDying"), scripts);
putIfNotEmpty("on_plr_lvl_up", ifoGffs->getString("Mod_OnPlrLvlUp"), scripts);
putIfNotEmpty("on_spawn_btn_dn", ifoGffs->getString("Mod_OnSpawnBtnDn"), scripts);
putIfNotEmpty("on_plr_death", ifoGffs->getString("Mod_OnPlrDeath"), scripts);
putIfNotEmpty("on_plr_rest", ifoGffs->getString("Mod_OnPlrRest"), scripts);
if (!scripts.empty()) {
description.add_child("scripts", scripts);
}
// END Scripts
TwoDaFile appearance;
appearance.load(wrap(getResource("appearance", ResourceType::TwoDa)));
auto appearanceTable = appearance.table();
// Areas
pt::ptree areas;
for (auto &areaGffs : ifoGffs->getList("Mod_Area_list")) {
string name(areaGffs->getString("Area_Name"));
areas.push_back(make_pair("", describeArea(name, *appearanceTable)));
}
description.add_child("areas", areas);
// END Areas
return move(description);
}
pt::ptree ModuleProbe::describeArea(const string &name, const TwoDaTable &appearance) {
GffFile are;
are.load(wrap(_rimMain.find(name, ResourceType::Area)));
auto areGffs = are.top();
GffFile git;
git.load(wrap(_rimMain.find(name, ResourceType::GameInstance)));
auto gitGffs = git.top();
LytFile lyt;
lyt.load(wrap(getResource(name, ResourceType::AreaLayout)));
// Rooms
pt::ptree rooms;
for (auto &lytRoom : lyt.rooms()) {
pt::ptree position;
position.put("x", lytRoom.position.x);
position.put("y", lytRoom.position.y);
position.put("z", lytRoom.position.z);
pt::ptree room;
room.put("name", lytRoom.name);
room.add_child("position", position);
rooms.push_back(make_pair("", room));
}
// END Rooms
// Creatures
pt::ptree creatures;
for (auto &gitCreature : gitGffs->getList("Creature List")) {
creatures.push_back(make_pair("", describeCreature(*gitCreature, appearance)));
}
// END Creatures
// Doors
pt::ptree doors;
for (auto &gitDoor : gitGffs->getList("Door List")) {
doors.push_back(make_pair("", describeDoor(*gitDoor)));
}
// END Doors
// Placeables
pt::ptree placeables;
for (auto &gitPlaceable : gitGffs->getList("Placeable List")) {
placeables.push_back(make_pair("", describePlaceable(*gitPlaceable)));
}
// END Placeables
// Triggers
pt::ptree triggers;
for (auto &gitTrigger : gitGffs->getList("TriggerList")) {
triggers.push_back(make_pair("", describeTrigger(*gitTrigger)));
}
// END Triggers
// Scripts
pt::ptree scripts;
putIfNotEmpty("on_enter", areGffs->getString("OnEnter"), scripts);
putIfNotEmpty("on_exit", areGffs->getString("OnExit"), scripts);
putIfNotEmpty("on_heartbeat", areGffs->getString("OnHeartbeat"), scripts);
putIfNotEmpty("on_user_defined", areGffs->getString("OnUserDefined"), scripts);
// END Scripts
pt::ptree area;
area.put("name", name);
area.put("title", getString(areGffs->getInt("Name")));
area.add_child("rooms", rooms);
area.add_child("creatures", creatures);
area.add_child("doors", doors);
area.add_child("placeables", placeables);
area.add_child("triggers", triggers);
if (!scripts.empty()) {
area.add_child("scripts", scripts);
}
return move(area);
}
static string describeFaction(int id) {
static unordered_map<int, string> descriptions {
{ 1, "Hostile1" },
{ 2, "Friendly1" },
{ 3, "Hostile2" },
{ 4, "Friendly2" },
{ 5, "Neutral" },
{ 6, "Insane" },
{ 7, "Tuskan" },
{ 8, "GlobalXor" },
{ 9, "Surrender1" },
{ 10, "Surrender2" },
{ 11, "Predator" },
{ 12, "Prey" },
{ 13, "Trap" },
{ 14, "EndarSpire" },
{ 15, "Rancor" },
{ 16, "Gizka1" },
{ 17, "Gizka2" },
{ 21, "SelfLoathing" },
{ 22, "OneOnOne" },
{ 23, "PartyPuppet" }
};
auto maybeDescription = descriptions.find(id);
return maybeDescription != descriptions.end() ? maybeDescription->second : to_string(id);
}
pt::ptree ModuleProbe::describeCreature(const GffStruct &gitCreature, const TwoDaTable &appearance) {
pt::ptree position;
position.put("x", gitCreature.getFloat("XPosition"));
position.put("y", gitCreature.getFloat("YPosition"));
position.put("z", gitCreature.getFloat("ZPosition"));
pt::ptree orientation;
orientation.put("x", gitCreature.getFloat("XOrientation"));
orientation.put("y", gitCreature.getFloat("YOrientation"));
GffFile utc;
utc.load(wrap(getResource(gitCreature.getString("TemplateResRef"), ResourceType::CreatureBlueprint)));
auto utcGffs = utc.top();
pt::ptree appearanceTree;
appearanceTree.put("label", appearance.getString(utcGffs->getInt("Appearance_Type"), "label"));
pt::ptree equipment;
for (auto &utcItem : utcGffs->getList("Equip_ItemList")) {
pt::ptree item;
item.put("resref", utcItem->getString("EquippedRes"));
equipment.push_back(make_pair("", item));
}
pt::ptree items;
for (auto &utcItem : utcGffs->getList("ItemList")) {
pt::ptree item;
item.put("resref", utcItem->getString("InventoryRes"));
item.put("dropable", utcItem->getBool("Dropable"));
items.push_back(make_pair("", item));
}
pt::ptree scripts;
putIfNotEmpty("heartbeat", utcGffs->getString("ScriptHeartbeat"), scripts);
putIfNotEmpty("on_notice", utcGffs->getString("ScriptOnNotice"), scripts);
putIfNotEmpty("spell_at", utcGffs->getString("ScriptSpellAt"), scripts);
putIfNotEmpty("attacked", utcGffs->getString("ScriptAttacked"), scripts);
putIfNotEmpty("damaged", utcGffs->getString("ScriptDamaged"), scripts);
putIfNotEmpty("end_round", utcGffs->getString("ScriptEndRound"), scripts);
putIfNotEmpty("end_dialogu", utcGffs->getString("ScriptEndDialogu"), scripts);
putIfNotEmpty("dialogue", utcGffs->getString("ScriptDialogue"), scripts);
putIfNotEmpty("spawn", utcGffs->getString("ScriptSpawn"), scripts);
putIfNotEmpty("rested", utcGffs->getString("ScriptRested"), scripts);
putIfNotEmpty("death", utcGffs->getString("ScriptDeath"), scripts);
putIfNotEmpty("user_define", utcGffs->getString("ScriptUserDefine"), scripts);
putIfNotEmpty("on_blocked", utcGffs->getString("ScriptOnBlocked"), scripts);
pt::ptree blueprint;
blueprint.put("resref", utcGffs->getString("TemplateResRef"));
blueprint.put("tag", utcGffs->getString("Tag"));
putIfNotEmpty("first_name", getString(utcGffs->getInt("FirstName")), blueprint);
putIfNotEmpty("last_name", getString(utcGffs->getInt("LastName")), blueprint);
blueprint.add_child("appearance", appearanceTree);
blueprint.put("faction", describeFaction(utcGffs->getInt("FactionID")));
putIfNotEmpty("conversation", utcGffs->getString("Conversation"), blueprint);
if (!equipment.empty()) {
blueprint.add_child("equipment", equipment);
}
if (!items.empty()) {
blueprint.add_child("items", items);
}
if (!scripts.empty()) {
blueprint.add_child("scripts", scripts);
}
pt::ptree creature;
creature.add_child("position", position);
creature.add_child("orientation", orientation);
creature.add_child("blueprint", blueprint);
return move(creature);
}
pt::ptree ModuleProbe::describeDoor(const GffStruct &gitDoor) {
GffFile utd;
utd.load(wrap(getResource(gitDoor.getString("TemplateResRef"), ResourceType::DoorBlueprint)));
auto utdGffs = utd.top();
pt::ptree scripts;
putIfNotEmpty("on_closed", utdGffs->getString("OnClosed"), scripts);
putIfNotEmpty("on_damaged", utdGffs->getString("OnDamaged"), scripts);
putIfNotEmpty("on_death", utdGffs->getString("OnDeath"), scripts);
putIfNotEmpty("on_disarm", utdGffs->getString("OnDisarm"), scripts);
putIfNotEmpty("on_hearbeat", utdGffs->getString("OnHeartbeat"), scripts);
putIfNotEmpty("on_lock", utdGffs->getString("OnLock"), scripts);
putIfNotEmpty("on_melee_attacked", utdGffs->getString("OnMeleeAttacked"), scripts);
putIfNotEmpty("on_open", utdGffs->getString("OnOpen"), scripts);
putIfNotEmpty("on_spell_cast_at", utdGffs->getString("OnSpellCastAt"), scripts);
putIfNotEmpty("on_trap_triggered", utdGffs->getString("OnTrapTriggered"), scripts);
putIfNotEmpty("on_unlock", utdGffs->getString("OnUnlock"), scripts);
putIfNotEmpty("on_user_defined", utdGffs->getString("OnUserDefined"), scripts);
putIfNotEmpty("on_click", utdGffs->getString("OnClick"), scripts);
putIfNotEmpty("on_fail_to_open", utdGffs->getString("OnFailToOpen"), scripts);
pt::ptree blueprint;
blueprint.put("resref", utdGffs->getString("TemplateResRef"));
blueprint.put("loc_name", getString(utdGffs->getInt("LocName")));
blueprint.put("static", utdGffs->getBool("Static"));
putIfNotEmpty("conversation", utdGffs->getString("Conversation"), blueprint);
blueprint.put("locked", utdGffs->getBool("Locked"));
blueprint.put("key_required", utdGffs->getBool("KeyRequired"));
if (!scripts.empty()) {
blueprint.add_child("scripts", scripts);
}
pt::ptree door;
door.put("x", gitDoor.getFloat("X"));
door.put("y", gitDoor.getFloat("Y"));
door.put("z", gitDoor.getFloat("Z"));
door.put("bearing", gitDoor.getFloat("Bearing"));
door.put("tag", gitDoor.getString("Tag"));
putIfNotEmpty("linked_to_module", gitDoor.getString("LinkedToModule"), door);
putIfNotEmpty("linked_to", gitDoor.getString("LinkedTo"), door);
door.add_child("blueprint", blueprint);
return move(door);
}
pt::ptree ModuleProbe::describePlaceable(const GffStruct &gitPlaceable) {
GffFile utp;
utp.load(wrap(getResource(gitPlaceable.getString("TemplateResRef"), ResourceType::PlaceableBlueprint)));
auto utpGffs = utp.top();
pt::ptree items;
for (auto &utcItem : utpGffs->getList("ItemList")) {
pt::ptree item;
item.put("resref", utcItem->getString("InventoryRes"));
items.push_back(make_pair("", item));
}
pt::ptree scripts;
putIfNotEmpty("on_closed", utpGffs->getString("OnClosed"), scripts);
putIfNotEmpty("on_damaged", utpGffs->getString("OnDamaged"), scripts);
putIfNotEmpty("on_death", utpGffs->getString("OnDeath"), scripts);
putIfNotEmpty("on_disarm", utpGffs->getString("OnDisarm"), scripts);
putIfNotEmpty("on_heartbeat", utpGffs->getString("OnHeartbeat"), scripts);
putIfNotEmpty("on_lock", utpGffs->getString("OnLock"), scripts);
putIfNotEmpty("on_melee_attacked", utpGffs->getString("OnMeleeAttacked"), scripts);
putIfNotEmpty("on_open", utpGffs->getString("OnOpen"), scripts);
putIfNotEmpty("on_spell_cast_at", utpGffs->getString("OnSpellCastAt"), scripts);
putIfNotEmpty("on_trap_triggered", utpGffs->getString("OnTrapTriggered"), scripts);
putIfNotEmpty("on_unlock", utpGffs->getString("OnUnlock"), scripts);
putIfNotEmpty("on_user_defined", utpGffs->getString("OnUserDefined"), scripts);
putIfNotEmpty("on_end_dialogue", utpGffs->getString("OnEndDialogue"), scripts);
putIfNotEmpty("on_inv_disturbed", utpGffs->getString("OnInvDisturbed"), scripts);
putIfNotEmpty("on_used", utpGffs->getString("OnUsed"), scripts);
pt::ptree blueprint;
blueprint.put("resref", utpGffs->getString("TemplateResRef"));
blueprint.put("loc_name", getString(utpGffs->getInt("LocName")));
blueprint.put("static", utpGffs->getBool("Static"));
putIfNotEmpty("conversation", utpGffs->getString("Conversation"), blueprint);
blueprint.put("locked", utpGffs->getBool("Locked"));
blueprint.put("key_required", utpGffs->getBool("KeyRequired"));
if (!items.empty()) {
blueprint.add_child("items", items);
}
if (!scripts.empty()) {
blueprint.add_child("scripts", scripts);
}
pt::ptree placeable;
placeable.put("x", gitPlaceable.getFloat("X"));
placeable.put("y", gitPlaceable.getFloat("Y"));
placeable.put("z", gitPlaceable.getFloat("Z"));
placeable.put("bearing", gitPlaceable.getFloat("Bearing"));
placeable.add_child("blueprint", blueprint);
return move(placeable);
}
pt::ptree ModuleProbe::describeTrigger(const GffStruct &gitTrigger) {
GffFile utt;
utt.load(wrap(getResource(gitTrigger.getString("TemplateResRef"), ResourceType::TriggerBlueprint)));
auto uttGffs = utt.top();
pt::ptree position;
position.put("x", gitTrigger.getFloat("XPosition"));
position.put("y", gitTrigger.getFloat("YPosition"));
position.put("z", gitTrigger.getFloat("ZPosition"));
pt::ptree orientation;
orientation.put("x", gitTrigger.getFloat("XOrientation"));
orientation.put("y", gitTrigger.getFloat("YOrientation"));
orientation.put("z", gitTrigger.getFloat("ZOrientation"));
pt::ptree geometry;
for (auto &gitPoint : gitTrigger.getList("Geometry")) {
pt::ptree point;
point.put("point_x", gitPoint->getFloat("PointX"));
point.put("point_y", gitPoint->getFloat("PointY"));
point.put("point_z", gitPoint->getFloat("PointZ"));
geometry.push_back(make_pair("", point));
}
pt::ptree scripts;
putIfNotEmpty("on_disarm", uttGffs->getString("OnDisarm"), scripts);
putIfNotEmpty("on_trap_triggered", uttGffs->getString("OnTrapTriggered"), scripts);
putIfNotEmpty("on_click", uttGffs->getString("OnClick"), scripts);
putIfNotEmpty("heartbeat", uttGffs->getString("ScriptHeartbeat"), scripts);
putIfNotEmpty("on_enter", uttGffs->getString("ScriptOnEnter"), scripts);
putIfNotEmpty("on_exit", uttGffs->getString("ScriptOnExit"), scripts);
putIfNotEmpty("user_define", uttGffs->getString("ScriptUserDefine"), scripts);
pt::ptree blueprint;
blueprint.put("resref", uttGffs->getString("TemplateResRef"));
blueprint.put("localized_name", getString(uttGffs->getInt("LocalizedName")));
if (!scripts.empty()) {
blueprint.add_child("scripts", scripts);
}
pt::ptree trigger;
trigger.add_child("position", position);
trigger.add_child("orientation", orientation);
trigger.add_child("points", geometry);
trigger.add_child("blueprint", blueprint);
return move(trigger);
}
void ModuleProbe::writeDescription(const string &moduleName, const pt::ptree &tree, const fs::path &destPath) {
fs::path jsonPath(destPath);
jsonPath.append(moduleName + ".json");
fs::ofstream json(jsonPath);
pt::write_json(json, tree);
}
shared_ptr<ByteArray> ModuleProbe::getResource(const string &resRef, ResourceType type) {
shared_ptr<ByteArray> result;
string cacheKey(resRef + "." + to_string(static_cast<int>(type)));
auto maybeResource = _resourceCache.find(cacheKey);
if (maybeResource != _resourceCache.end()) return maybeResource->second;
KeyFile::KeyEntry key;
if (_keyFile.find(resRef, type, key)) {
auto maybeBif = _bifByIndex.find(key.bifIdx);
if (maybeBif != _bifByIndex.end()) {
result = make_shared<ByteArray>(maybeBif->second->getResourceData(key.resIdx));
} else {
fs::path bifPath(_gamePath);
bifPath.append(_keyFile.getFilename(key.bifIdx));
auto bif = make_unique<BifFile>();
bif->load(bifPath);
result = make_shared<ByteArray>(bif->getResourceData(key.resIdx));
_bifByIndex.insert(make_pair(key.bifIdx, move(bif)));
}
} else {
result = _rimMain.find(resRef, type);
if (!result) {
result = _rimBlueprints.find(resRef, type);
if (!result) {
throw runtime_error("Resource not found: " + resRef + " " + to_string(static_cast<int>(type)));
}
}
}
_resourceCache.insert(make_pair(cacheKey, result));
return move(result);
}
string ModuleProbe::getString(int strRef) const {
if (strRef == -1) return "";
return _talkTable->getString(strRef).text;
}
} // namespace tools
} // namespace reone

74
tools/moduleprobe.h Normal file
View file

@ -0,0 +1,74 @@
/*
* 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 <string>
#include <memory>
#include <unordered_map>
#include <boost/filesystem/path.hpp>
#include <boost/property_tree/json_parser.hpp>
#include "../src/resource/2dafile.h"
#include "../src/resource/biffile.h"
#include "../src/resource/gfffile.h"
#include "../src/resource/keyfile.h"
#include "../src/resource/rimfile.h"
#include "../src/resource/tlkfile.h"
namespace reone {
namespace tools {
/**
* Tool to process a game module and produce a JSON file describing it.
*/
class ModuleProbe {
public:
/**
* Finds a module with the specified name in the specified game path,
* processes it and produces a JSON file in the specified destination
* directory, describing it.
*/
void probe(const std::string &name, const boost::filesystem::path &gamePath, const boost::filesystem::path &destPath);
private:
boost::filesystem::path _gamePath;
resource::KeyFile _keyFile;
std::unordered_map<int, std::unique_ptr<resource::BifFile>> _bifByIndex;
std::shared_ptr<resource::TalkTable> _talkTable;
resource::RimFile _rimMain;
resource::RimFile _rimBlueprints;
std::unordered_map<std::string, std::shared_ptr<ByteArray>> _resourceCache;
void loadResources(const std::string &moduleName);
boost::property_tree::ptree describeModule();
boost::property_tree::ptree describeArea(const std::string &name, const resource::TwoDaTable &appearance);
boost::property_tree::ptree describeCreature(const resource::GffStruct &gitCreature, const resource::TwoDaTable &appearance);
boost::property_tree::ptree describeDoor(const resource::GffStruct &gitDoor);
boost::property_tree::ptree describePlaceable(const resource::GffStruct &gitPlaceable);
boost::property_tree::ptree describeTrigger(const resource::GffStruct &gitTrigger);
void writeDescription(const std::string &moduleName, const boost::property_tree::ptree &tree, const boost::filesystem::path &destPath);
std::shared_ptr<ByteArray> getResource(const std::string &resRef, resource::ResourceType type);
std::string getString(int strRef) const;
};
} // namespace tools
} // namespace reone

View file

@ -24,6 +24,8 @@
#include "../src/common/pathutil.h"
#include "moduleprobe.h"
using namespace std;
using namespace reone::resource;
@ -42,17 +44,27 @@ int Program::run() {
initOptions();
loadOptions();
initGameVersion();
initTool();
switch (_command) {
case Command::List:
_tool->list(_inputFilePath, _keyPath);
break;
case Command::Extract:
_tool->extract(_inputFilePath, _keyPath, _destPath);
break;
case Command::Convert:
_tool->convert(_inputFilePath, _destPath);
initFileTool();
switch (_command) {
case Command::List:
_tool->list(_target, _keyPath);
break;
case Command::Extract:
_tool->extract(_target, _keyPath, _destPath);
break;
case Command::Convert:
_tool->convert(_target, _destPath);
break;
default:
break;
}
case Command::ModuleProbe:
ModuleProbe().probe(_target, _gamePath, _destPath);
break;
default:
cout << _cmdLineOpts << endl;
@ -68,17 +80,18 @@ void Program::initOptions() {
("list", "list file contents")
("extract", "extract file contents")
("convert", "convert 2DA or GFF file to JSON")
("modprobe", "probe module and produce a JSON file describing it")
("game", po::value<string>(), "path to game directory")
("dest", po::value<string>(), "path to destination directory")
("input-file", po::value<string>(), "path to input file");
("target", po::value<string>(), "target name or path to input file");
}
static fs::path getDestination(const po::variables_map &vars) {
fs::path result;
if (vars.count("dest") > 0) {
result = vars["dest"].as<string>();
} else if (vars.count("input-file") > 0) {
result = fs::path(vars["input-file"].as<string>()).parent_path();
} else if (vars.count("target") > 0) {
result = fs::path(vars["target"].as<string>()).parent_path();
} else {
result = fs::current_path();
}
@ -87,7 +100,7 @@ static fs::path getDestination(const po::variables_map &vars) {
void Program::loadOptions() {
po::positional_options_description positional;
positional.add("input-file", 1);
positional.add("target", 1);
po::parsed_options parsedCmdLineOpts = po::command_line_parser(_argc, _argv)
.options(_cmdLineOpts)
@ -100,7 +113,7 @@ void Program::loadOptions() {
_gamePath = vars.count("game") > 0 ? vars["game"].as<string>() : fs::current_path();
_destPath = getDestination(vars);
_inputFilePath = vars.count("input-file") > 0 ? vars["input-file"].as<string>() : "";
_target = vars.count("target") > 0 ? vars["target"].as<string>() : "";
_keyPath = getPathIgnoreCase(_gamePath, "chitin.key");
if (vars.count("help")) {
@ -111,6 +124,8 @@ void Program::loadOptions() {
_command = Command::Extract;
} else if (vars.count("convert")) {
_command = Command::Convert;
} else if (vars.count("modprobe")) {
_command = Command::ModuleProbe;
}
}
@ -119,18 +134,18 @@ void Program::initGameVersion() {
_version = exePath.empty() ? GameVersion::KotOR : GameVersion::TheSithLords;
}
void Program::initTool() {
void Program::initFileTool() {
switch (_command) {
case Command::List:
case Command::Extract:
case Command::Convert:
if (!fs::exists(_inputFilePath)) {
throw runtime_error("Input file does not exist: " + _inputFilePath.string());
if (!fs::exists(_target)) {
throw runtime_error("Input file does not exist: " + _target);
}
_tool = getToolByPath(_version, _inputFilePath);
_tool = getFileToolByPath(_version, _target);
break;
default:
break;
throw logic_error("Unsupported file tool command: " + to_string(static_cast<int>(_command)));
}
}

View file

@ -17,6 +17,7 @@
#pragma once
#include <string>
#include <memory>
#include <boost/filesystem/path.hpp>
@ -40,16 +41,17 @@ private:
Help,
List,
Extract,
Convert
Convert,
ModuleProbe
};
boost::filesystem::path _gamePath;
boost::filesystem::path _destPath;
boost::filesystem::path _inputFilePath;
std::string _target;
boost::filesystem::path _keyPath;
resource::GameVersion _version { resource::GameVersion::KotOR };
Command _command { Command::None };
std::unique_ptr<Tool> _tool;
std::unique_ptr<FileTool> _tool;
// Command line arguments
@ -66,7 +68,7 @@ private:
void initOptions();
void loadOptions();
void initGameVersion();
void initTool();
void initFileTool();
};
} // namespace tools

View file

@ -27,41 +27,48 @@ namespace reone {
namespace tools {
void Tool::list(const fs::path &path, const fs::path &keyPath) const {
void FileTool::list(const fs::path &path, const fs::path &keyPath) const {
throwNotImplemented();
}
void Tool::extract(const fs::path &path, const fs::path &keyPath, const fs::path &destPath) const {
throwNotImplemented();
}
void Tool::convert(const fs::path &path, const fs::path &destPath) const {
throwNotImplemented();
}
void Tool::throwNotImplemented() const {
void FileTool::throwNotImplemented() const {
throw logic_error("Not implemented");
}
unique_ptr<Tool> getToolByPath(GameVersion version, const fs::path &path) {
void FileTool::extract(const fs::path &path, const fs::path &keyPath, const fs::path &destPath) const {
throwNotImplemented();
}
void FileTool::convert(const fs::path &path, const fs::path &destPath) const {
throwNotImplemented();
}
unique_ptr<FileTool> getFileToolByPath(GameVersion version, const fs::path &path) {
if (fs::is_directory(path)) {
throw invalid_argument("path must not point to a directory");
}
unique_ptr<FileTool> result;
string ext(path.extension().string());
if (ext == ".key") {
return make_unique<KeyTool>();
result = make_unique<KeyTool>();
} else if (ext == ".bif") {
return make_unique<BifTool>();
result = make_unique<BifTool>();
} else if (ext == ".erf" || ext == ".mod" || ext == ".sav") {
return make_unique<ErfTool>();
result = make_unique<ErfTool>();
} else if (ext == ".rim") {
return make_unique<RimTool>();
result = make_unique<RimTool>();
} else if (ext == ".2da") {
return make_unique<TwoDaTool>();
result = make_unique<TwoDaTool>();
} else if (ext == ".tlk") {
return make_unique<TlkTool>();
result = make_unique<TlkTool>();
} else if (ext == ".tpc") {
return make_unique<TpcTool>();
result = make_unique<TpcTool>();
} else {
return make_unique<GffTool>();
result = make_unique<GffTool>();
}
return move(result);
}
} // namespace tools

View file

@ -37,14 +37,15 @@ class GffStruct;
namespace tools {
/**
* Abstract tool class. Each implementation covers a single file format.
* Abstract tool, operating on a single file. Each implementation covers a
* particular file format.
*
* Operations:
* - list list file contents
* - extract extract file contents
* - convert convert file to a more practical format, e.g. JSON, TGA
*/
class Tool {
class FileTool {
public:
virtual void list(const boost::filesystem::path &path, const boost::filesystem::path &keyPath) const;
virtual void extract(const boost::filesystem::path &path, const boost::filesystem::path &keyPath, const boost::filesystem::path &destPath) const;
@ -54,14 +55,14 @@ private:
void throwNotImplemented() const;
};
std::unique_ptr<Tool> getToolByPath(resource::GameVersion version, const boost::filesystem::path &path);
std::unique_ptr<FileTool> getFileToolByPath(resource::GameVersion version, const boost::filesystem::path &path);
class KeyTool : public Tool {
class KeyTool : public FileTool {
public:
void list(const boost::filesystem::path &path, const boost::filesystem::path &keyPath) const override;
};
class BifTool : public Tool {
class BifTool : public FileTool {
public:
void list(const boost::filesystem::path &path, const boost::filesystem::path &keyPath) const override;
void extract(const boost::filesystem::path &path, const boost::filesystem::path &keyPath, const boost::filesystem::path &destPath) const override;
@ -70,29 +71,29 @@ private:
int getFileIndexByFilename(const std::vector<resource::KeyFile::FileEntry> &files, const std::string &filename) const;
};
class ErfTool : public Tool {
class ErfTool : public FileTool {
public:
void list(const boost::filesystem::path &path, const boost::filesystem::path &keyPath) const override;
void extract(const boost::filesystem::path &path, const boost::filesystem::path &keyPath, const boost::filesystem::path &destPath) const override;
};
class RimTool : public Tool {
class RimTool : public FileTool {
public:
void list(const boost::filesystem::path &path, const boost::filesystem::path &keyPath) const override;
void extract(const boost::filesystem::path &path, const boost::filesystem::path &keyPath, const boost::filesystem::path &destPath) const override;
};
class TwoDaTool : public Tool {
class TwoDaTool : public FileTool {
public:
void convert(const boost::filesystem::path &path, const boost::filesystem::path &destPath) const override;
};
class TlkTool : public Tool {
class TlkTool : public FileTool {
public:
void convert(const boost::filesystem::path &path, const boost::filesystem::path &destPath) const override;
};
class GffTool : public Tool {
class GffTool : public FileTool {
public:
void convert(const boost::filesystem::path &path, const boost::filesystem::path &destPath) const override;
@ -100,7 +101,7 @@ private:
boost::property_tree::ptree getPropertyTree(const resource::GffStruct &gffs) const;
};
class TpcTool : public Tool {
class TpcTool : public FileTool {
public:
void convert(const boost::filesystem::path &path, const boost::filesystem::path &destPath) const override;
};