diff --git a/CMakeLists.txt b/CMakeLists.txt
index 48a20eea..f3848b29 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -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
diff --git a/tools/moduleprobe.cpp b/tools/moduleprobe.cpp
new file mode 100644
index 00000000..79475cb9
--- /dev/null
+++ b/tools/moduleprobe.cpp
@@ -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 .
+ */
+
+#include "moduleprobe.h"
+
+#include
+
+#include
+
+#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 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 ModuleProbe::getResource(const string &resRef, ResourceType type) {
+ shared_ptr result;
+ string cacheKey(resRef + "." + to_string(static_cast(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(maybeBif->second->getResourceData(key.resIdx));
+
+ } else {
+ fs::path bifPath(_gamePath);
+ bifPath.append(_keyFile.getFilename(key.bifIdx));
+
+ auto bif = make_unique();
+ bif->load(bifPath);
+
+ result = make_shared(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(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
diff --git a/tools/moduleprobe.h b/tools/moduleprobe.h
new file mode 100644
index 00000000..dc6f5403
--- /dev/null
+++ b/tools/moduleprobe.h
@@ -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 .
+ */
+
+#pragma once
+
+#include
+#include
+#include
+
+#include
+#include
+
+#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> _bifByIndex;
+ std::shared_ptr _talkTable;
+ resource::RimFile _rimMain;
+ resource::RimFile _rimBlueprints;
+ std::unordered_map> _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 getResource(const std::string &resRef, resource::ResourceType type);
+ std::string getString(int strRef) const;
+};
+
+} // namespace tools
+
+} // namespace reone
diff --git a/tools/program.cpp b/tools/program.cpp
index bcc72870..f66dfd73 100644
--- a/tools/program.cpp
+++ b/tools/program.cpp
@@ -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(), "path to game directory")
("dest", po::value(), "path to destination directory")
- ("input-file", po::value(), "path to input file");
+ ("target", po::value(), "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();
- } else if (vars.count("input-file") > 0) {
- result = fs::path(vars["input-file"].as()).parent_path();
+ } else if (vars.count("target") > 0) {
+ result = fs::path(vars["target"].as()).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() : fs::current_path();
_destPath = getDestination(vars);
- _inputFilePath = vars.count("input-file") > 0 ? vars["input-file"].as() : "";
+ _target = vars.count("target") > 0 ? vars["target"].as() : "";
_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(_command)));
}
}
diff --git a/tools/program.h b/tools/program.h
index 9739bb0e..d27e0c15 100644
--- a/tools/program.h
+++ b/tools/program.h
@@ -17,6 +17,7 @@
#pragma once
+#include
#include
#include
@@ -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;
+ std::unique_ptr _tool;
// Command line arguments
@@ -66,7 +68,7 @@ private:
void initOptions();
void loadOptions();
void initGameVersion();
- void initTool();
+ void initFileTool();
};
} // namespace tools
diff --git a/tools/tools.cpp b/tools/tools.cpp
index ac103215..78ff0633 100644
--- a/tools/tools.cpp
+++ b/tools/tools.cpp
@@ -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 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 getFileToolByPath(GameVersion version, const fs::path &path) {
+ if (fs::is_directory(path)) {
+ throw invalid_argument("path must not point to a directory");
+ }
+ unique_ptr result;
+
string ext(path.extension().string());
if (ext == ".key") {
- return make_unique();
+ result = make_unique();
} else if (ext == ".bif") {
- return make_unique();
+ result = make_unique();
} else if (ext == ".erf" || ext == ".mod" || ext == ".sav") {
- return make_unique();
+ result = make_unique();
} else if (ext == ".rim") {
- return make_unique();
+ result = make_unique();
} else if (ext == ".2da") {
- return make_unique();
+ result = make_unique();
} else if (ext == ".tlk") {
- return make_unique();
+ result = make_unique();
} else if (ext == ".tpc") {
- return make_unique();
+ result = make_unique();
} else {
- return make_unique();
+ result = make_unique();
}
+
+ return move(result);
}
} // namespace tools
diff --git a/tools/tools.h b/tools/tools.h
index 30637e24..3556a8c7 100644
--- a/tools/tools.h
+++ b/tools/tools.h
@@ -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 getToolByPath(resource::GameVersion version, const boost::filesystem::path &path);
+std::unique_ptr 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 &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;
};