diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index aaf7f233f0..6bd2226ef7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -287,7 +287,7 @@ macOS12_Xcode13: CCACHE_SIZE: 3G variables: &engine-targets - targets: "openmw,openmw-iniimporter,openmw-launcher,openmw-wizard,openmw-navmeshtool" + targets: "openmw,openmw-iniimporter,openmw-launcher,openmw-wizard,openmw-navmeshtool,openmw-bulletobjecttool" package: "Engine" variables: &cs-targets diff --git a/CI/before_script.android.sh b/CI/before_script.android.sh index 43422f68c1..80a3f11e57 100755 --- a/CI/before_script.android.sh +++ b/CI/before_script.android.sh @@ -23,6 +23,7 @@ cmake \ -DBUILD_OPENCS=0 \ -DBUILD_WIZARD=0 \ -DBUILD_NAVMESHTOOL=OFF \ +-DBUILD_BULLETOBJECTTOOL=OFF \ -DOPENMW_USE_SYSTEM_MYGUI=OFF \ -DOPENMW_USE_SYSTEM_SQLITE3=OFF \ .. diff --git a/CMakeLists.txt b/CMakeLists.txt index 20c00fb757..bc4fa56e25 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,6 +40,7 @@ option(BUILD_WITH_CODE_COVERAGE "Enable code coverage with gconv" OFF) option(BUILD_UNITTESTS "Enable Unittests with Google C++ Unittest" OFF) option(BUILD_BENCHMARKS "Build benchmarks with Google Benchmark" OFF) option(BUILD_NAVMESHTOOL "Build navmesh tool" ON) +option(BUILD_BULLETOBJECTTOOL "Build Bullet object tool" ON) set(OpenGL_GL_PREFERENCE LEGACY) # Use LEGACY as we use GL2; GLNVD is for GL3 and up. @@ -619,6 +620,10 @@ if (BUILD_NAVMESHTOOL) add_subdirectory(apps/navmeshtool) endif() +if (BUILD_BULLETOBJECTTOOL) + add_subdirectory( apps/bulletobjecttool ) +endif() + if (WIN32) if (MSVC) foreach( OUTPUTCONFIG ${CMAKE_CONFIGURATION_TYPES} ) @@ -719,6 +724,10 @@ if (WIN32) if (BUILD_NAVMESHTOOL) set_target_properties(openmw-navmeshtool PROPERTIES COMPILE_FLAGS "${WARNINGS}") endif() + + if (BUILD_BULLETOBJECTTOOL) + set_target_properties(openmw-bulletobjecttool PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") + endif() endif(MSVC) # TODO: At some point release builds should not use the console but rather write to a log file @@ -964,6 +973,9 @@ elseif(NOT APPLE) if(BUILD_NAVMESHTOOL) install(PROGRAMS "${INSTALL_SOURCE}/openmw-navmeshtool" DESTINATION "${BINDIR}" ) endif() + IF(BUILD_BULLETOBJECTTOOL) + INSTALL(PROGRAMS "${INSTALL_SOURCE}/openmw-bulletobjecttool" DESTINATION "${BINDIR}" ) + ENDIF(BUILD_BULLETOBJECTTOOL) # Install licenses INSTALL(FILES "files/mygui/DejaVuFontLicense.txt" DESTINATION "${LICDIR}" ) diff --git a/apps/bulletobjecttool/CMakeLists.txt b/apps/bulletobjecttool/CMakeLists.txt new file mode 100644 index 0000000000..6beb411e20 --- /dev/null +++ b/apps/bulletobjecttool/CMakeLists.txt @@ -0,0 +1,20 @@ +set(BULLETMESHTOOL + main.cpp +) +source_group(apps\\bulletobjecttool FILES ${BULLETMESHTOOL}) + +openmw_add_executable(openmw-bulletobjecttool ${BULLETMESHTOOL}) + +target_link_libraries(openmw-bulletobjecttool + ${Boost_PROGRAM_OPTIONS_LIBRARY} + components +) + +if (BUILD_WITH_CODE_COVERAGE) + add_definitions(--coverage) + target_link_libraries(openmw-bulletobjecttool gcov) +endif() + +if (WIN32) + install(TARGETS openmw-bulletobjecttool RUNTIME DESTINATION ".") +endif() diff --git a/apps/bulletobjecttool/main.cpp b/apps/bulletobjecttool/main.cpp new file mode 100644 index 0000000000..c80883fe78 --- /dev/null +++ b/apps/bulletobjecttool/main.cpp @@ -0,0 +1,203 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace +{ + namespace bpo = boost::program_options; + + using StringsVector = std::vector; + + bpo::options_description makeOptionsDescription() + { + using Fallback::FallbackMap; + + bpo::options_description result; + + result.add_options() + ("help", "print help message") + + ("version", "print version information and quit") + + ("data", bpo::value()->default_value(Files::MaybeQuotedPathContainer(), "data") + ->multitoken()->composing(), "set data directories (later directories have higher priority)") + + ("data-local", bpo::value()->default_value(Files::MaybeQuotedPathContainer::value_type(), ""), + "set local data directory (highest priority)") + + ("fallback-archive", bpo::value()->default_value(StringsVector(), "fallback-archive") + ->multitoken()->composing(), "set fallback BSA archives (later archives have higher priority)") + + ("resources", bpo::value()->default_value(Files::MaybeQuotedPath(), "resources"), + "set resources directory") + + ("content", bpo::value()->default_value(StringsVector(), "") + ->multitoken()->composing(), "content file(s): esm/esp, or omwgame/omwaddon/omwscripts") + + ("fs-strict", bpo::value()->implicit_value(true) + ->default_value(false), "strict file system handling (no case folding)") + + ("encoding", bpo::value()-> + default_value("win1252"), + "Character encoding used in OpenMW game messages:\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") + + ("fallback", bpo::value()->default_value(FallbackMap(), "") + ->multitoken()->composing(), "fallback values") + ; + + Files::ConfigurationManager::addCommonOptions(result); + + return result; + } + + struct WriteArray + { + const float (&mValue)[3]; + + friend std::ostream& operator <<(std::ostream& stream, const WriteArray& value) + { + for (std::size_t i = 0; i < 2; ++i) + stream << std::setprecision(std::numeric_limits::max_exponent10) << value.mValue[i] << ", "; + return stream << std::setprecision(std::numeric_limits::max_exponent10) << value.mValue[2]; + } + }; + + std::string toHex(std::string_view value) + { + std::string buffer(value.size() * 2, '0'); + char* out = buffer.data(); + for (const char v : value) + { + const std::ptrdiff_t space = static_cast(static_cast(v) <= 0xf); + const auto [ptr, ec] = std::to_chars(out + space, out + space + 2, static_cast(v), 16); + if (ec != std::errc()) + throw std::system_error(std::make_error_code(ec)); + out += 2; + } + return buffer; + } + + int runBulletObjectTool(int argc, char *argv[]) + { + bpo::options_description desc = makeOptionsDescription(); + + bpo::parsed_options options = bpo::command_line_parser(argc, argv) + .options(desc).allow_unregistered().run(); + bpo::variables_map variables; + + bpo::store(options, variables); + bpo::notify(variables); + + if (variables.find("help") != variables.end()) + { + getRawStdout() << desc << std::endl; + return 0; + } + + Files::ConfigurationManager config; + + bpo::variables_map composingVariables = Files::separateComposingVariables(variables, desc); + config.readConfiguration(variables, desc); + Files::mergeComposingVariables(variables, composingVariables, desc); + + const std::string encoding(variables["encoding"].as()); + Log(Debug::Info) << ToUTF8::encodingUsingMessage(encoding); + ToUTF8::Utf8Encoder encoder(ToUTF8::calculateEncoding(encoding)); + + Files::PathContainer dataDirs(asPathContainer(variables["data"].as())); + + auto local = variables["data-local"].as(); + if (!local.empty()) + dataDirs.push_back(std::move(local)); + + config.processPaths(dataDirs); + + const auto fsStrict = variables["fs-strict"].as(); + const auto resDir = variables["resources"].as(); + Version::Version v = Version::getOpenmwVersion(resDir.string()); + Log(Debug::Info) << v.describe(); + dataDirs.insert(dataDirs.begin(), resDir / "vfs"); + const auto fileCollections = Files::Collections(dataDirs, !fsStrict); + const auto archives = variables["fallback-archive"].as(); + const auto contentFiles = variables["content"].as(); + + Fallback::Map::init(variables["fallback"].as().mMap); + + VFS::Manager vfs(fsStrict); + + VFS::registerArchives(&vfs, fileCollections, archives, true); + + Settings::Manager settings; + settings.load(config); + + std::vector readers(contentFiles.size()); + EsmLoader::Query query; + query.mLoadActivators = true; + query.mLoadCells = true; + query.mLoadContainers = true; + query.mLoadDoors = true; + query.mLoadGameSettings = true; + query.mLoadLands = true; + query.mLoadStatics = true; + const EsmLoader::EsmData esmData = EsmLoader::loadEsmData(query, contentFiles, fileCollections, readers, &encoder); + + Resource::ImageManager imageManager(&vfs); + Resource::NifFileManager nifFileManager(&vfs); + Resource::SceneManager sceneManager(&vfs, &imageManager, &nifFileManager); + Resource::BulletShapeManager bulletShapeManager(&vfs, &sceneManager, &nifFileManager); + + Resource::forEachBulletObject(readers, vfs, bulletShapeManager, esmData, + [] (const ESM::Cell& cell, const Resource::BulletObject& object) + { + Log(Debug::Verbose) << "Found bullet object in " << (cell.isExterior() ? "exterior" : "interior") + << " cell \"" << cell.getDescription() << "\":" + << " fileName=\"" << object.mShape->mFileName << '"' + << " fileHash=" << toHex(object.mShape->mFileHash) + << " collisionShape=" << std::boolalpha << (object.mShape->mCollisionShape == nullptr) + << " avoidCollisionShape=" << std::boolalpha << (object.mShape->mAvoidCollisionShape == nullptr) + << " position=(" << WriteArray {object.mPosition.pos} << ')' + << " rotation=(" << WriteArray {object.mPosition.rot} << ')' + << " scale=" << std::setprecision(std::numeric_limits::max_exponent10) << object.mScale; + }); + + Log(Debug::Info) << "Done"; + + return 0; + } +} + +int main(int argc, char *argv[]) +{ + return wrapApplication(runBulletObjectTool, argc, argv, "BulletObjectTool"); +} diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 9a1ffc847c..790cd6b646 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -46,7 +46,7 @@ add_component_dir (vfs add_component_dir (resource scenemanager keyframemanager imagemanager bulletshapemanager bulletshape niffilemanager objectcache multiobjectcache resourcesystem - resourcemanager stats animation + resourcemanager stats animation foreachbulletobject ) add_component_dir (shader diff --git a/components/resource/foreachbulletobject.cpp b/components/resource/foreachbulletobject.cpp new file mode 100644 index 0000000000..aaaa6fa5f9 --- /dev/null +++ b/components/resource/foreachbulletobject.cpp @@ -0,0 +1,161 @@ +#include "foreachbulletobject.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace Resource +{ + namespace + { + struct CellRef + { + ESM::RecNameInts mType; + ESM::RefNum mRefNum; + std::string mRefId; + float mScale; + ESM::Position mPos; + + CellRef(ESM::RecNameInts type, ESM::RefNum refNum, std::string&& refId, float scale, const ESM::Position& pos) + : mType(type), mRefNum(refNum), mRefId(std::move(refId)), mScale(scale), mPos(pos) {} + }; + + ESM::RecNameInts getType(const EsmLoader::EsmData& esmData, std::string_view refId) + { + const auto it = std::lower_bound(esmData.mRefIdTypes.begin(), esmData.mRefIdTypes.end(), + refId, EsmLoader::LessById {}); + if (it == esmData.mRefIdTypes.end() || it->mId != refId) + return {}; + return it->mType; + } + + std::vector loadCellRefs(const ESM::Cell& cell, const EsmLoader::EsmData& esmData, + std::vector& readers) + { + std::vector> cellRefs; + + for (std::size_t i = 0; i < cell.mContextList.size(); i++) + { + ESM::ESMReader& reader = readers[static_cast(cell.mContextList[i].index)]; + cell.restore(reader, static_cast(i)); + ESM::CellRef cellRef; + bool deleted = false; + while (ESM::Cell::getNextRef(reader, cellRef, deleted)) + { + Misc::StringUtils::lowerCaseInPlace(cellRef.mRefID); + const ESM::RecNameInts type = getType(esmData, cellRef.mRefID); + if (type == ESM::RecNameInts {}) + continue; + cellRefs.emplace_back(deleted, type, cellRef.mRefNum, std::move(cellRef.mRefID), + cellRef.mScale, cellRef.mPos); + } + } + + Log(Debug::Debug) << "Loaded " << cellRefs.size() << " cell refs"; + + const auto getKey = [] (const EsmLoader::Record& v) -> const ESM::RefNum& { return v.mValue.mRefNum; }; + std::vector result = prepareRecords(cellRefs, getKey); + + Log(Debug::Debug) << "Prepared " << result.size() << " unique cell refs"; + + return result; + } + + template + void forEachObject(const ESM::Cell& cell, const EsmLoader::EsmData& esmData, const VFS::Manager& vfs, + Resource::BulletShapeManager& bulletShapeManager, std::vector& readers, + F&& f) + { + std::vector cellRefs = loadCellRefs(cell, esmData, readers); + + Log(Debug::Debug) << "Prepared " << cellRefs.size() << " unique cell refs"; + + for (CellRef& cellRef : cellRefs) + { + std::string model(getModel(esmData, cellRef.mRefId, cellRef.mType)); + if (model.empty()) + continue; + + if (cellRef.mType != ESM::REC_STAT) + model = Misc::ResourceHelpers::correctActorModelPath(model, &vfs); + + osg::ref_ptr shape = [&] + { + try + { + return bulletShapeManager.getShape("meshes/" + model); + } + catch (const std::exception& e) + { + Log(Debug::Warning) << "Failed to load cell ref \"" << cellRef.mRefId << "\" model \"" << model << "\": " << e.what(); + return osg::ref_ptr(); + } + } (); + + if (shape == nullptr) + continue; + + switch (cellRef.mType) + { + case ESM::REC_ACTI: + case ESM::REC_CONT: + case ESM::REC_DOOR: + case ESM::REC_STAT: + f(BulletObject {std::move(shape), cellRef.mPos, cellRef.mScale}); + break; + default: + break; + } + } + } + } + + void forEachBulletObject(std::vector& readers, const VFS::Manager& vfs, + Resource::BulletShapeManager& bulletShapeManager, const EsmLoader::EsmData& esmData, + std::function callback) + { + Log(Debug::Info) << "Processing " << esmData.mCells.size() << " cells..."; + + for (std::size_t i = 0; i < esmData.mCells.size(); ++i) + { + const ESM::Cell& cell = esmData.mCells[i]; + const bool exterior = cell.isExterior(); + + Log(Debug::Debug) << "Processing " << (exterior ? "exterior" : "interior") + << " cell (" << (i + 1) << "/" << esmData.mCells.size() << ") \"" << cell.getDescription() << "\""; + + std::size_t objects = 0; + + forEachObject(cell, esmData, vfs, bulletShapeManager, readers, + [&] (const BulletObject& object) + { + callback(cell, object); + ++objects; + }); + + Log(Debug::Info) << "Processed " << (exterior ? "exterior" : "interior") + << " cell (" << (i + 1) << "/" << esmData.mCells.size() << ") " << cell.getDescription() + << " with " << objects << " objects"; + } + } +} diff --git a/components/resource/foreachbulletobject.hpp b/components/resource/foreachbulletobject.hpp new file mode 100644 index 0000000000..5b1495e4cc --- /dev/null +++ b/components/resource/foreachbulletobject.hpp @@ -0,0 +1,48 @@ +#ifndef OPENMW_COMPONENTS_RESOURCE_FOREACHBULLETOBJECT_H +#define OPENMW_COMPONENTS_RESOURCE_FOREACHBULLETOBJECT_H + +#include +#include +#include + +#include + +#include +#include + +namespace ESM +{ + class ESMReader; + struct Cell; +} + +namespace VFS +{ + class Manager; +} + +namespace Resource +{ + class BulletShapeManager; +} + +namespace EsmLoader +{ + struct EsmData; +} + +namespace Resource +{ + struct BulletObject + { + osg::ref_ptr mShape; + ESM::Position mPosition; + float mScale; + }; + + void forEachBulletObject(std::vector& readers, const VFS::Manager& vfs, + Resource::BulletShapeManager& bulletShapeManager, const EsmLoader::EsmData& esmData, + std::function callback); +} + +#endif