diff --git a/package-lock.json b/package-lock.json index f8d63bdd..34de02b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -713,6 +713,13 @@ "node": ">=20.19.0" } }, + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -1778,6 +1785,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "dev": true, + "license": "MIT" + }, "node_modules/@tybys/wasm-util": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", @@ -1863,6 +1877,28 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.184.1", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.184.1.tgz", + "integrity": "sha512-6q4VdiqVsrTRqmk62/BnlcAvIrnDM0zf2ZDVKI5kZiniWrSaOHaQzmbp+BNzoggc/8tgW412pL//wZIxu2PPTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": ">=0.5.17", + "fflate": "~0.8.2", + "meshoptimizer": "~1.1.1" + } + }, "node_modules/@types/tough-cookie": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", @@ -1870,6 +1906,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@types/webxr": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.60.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz", @@ -2677,10 +2726,48 @@ } } }, + "node_modules/@vueuse/core": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.3.0.tgz", + "integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.3.0", + "@vueuse/shared": "14.3.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.3.0.tgz", + "integrity": "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.3.0.tgz", + "integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, "node_modules/@webwindowed/vite-plugin-cpp-header": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@webwindowed/vite-plugin-cpp-header/-/vite-plugin-cpp-header-1.0.0.tgz", - "integrity": "sha512-0eALUR+M6rkq45FXslE36/UJWZ2Xy9Gt+JH1GNINnlp9QH+JP7wBqp228+bUHuSxgGIiSXsSQi3C0CA+AbmxRg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@webwindowed/vite-plugin-cpp-header/-/vite-plugin-cpp-header-1.1.0.tgz", + "integrity": "sha512-3vfsU4uAKZXkzoXKUUokXByygF25Vt9lsv6BmSDWwH6iYMbVZqJ9nX/7MIobGJw0YirH7FoOCjeDtSGQTE9y4Q==", "dev": true, "license": "MIT", "peerDependencies": { @@ -3688,6 +3775,13 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "dev": true, + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -4601,6 +4695,13 @@ "node": ">= 8" } }, + "node_modules/meshoptimizer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz", + "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==", + "dev": true, + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -5679,6 +5780,12 @@ "url": "https://opencollective.com/synckit" } }, + "node_modules/three": { + "version": "0.184.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz", + "integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==", + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -5898,9 +6005,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.27.0.tgz", - "integrity": "sha512-+t2Z/GwkZQDtu00813aP66ygViGtPHKhhoFZpQKpKrE+9jIgES+Zw+mFNaDWOVRKiuJjuqKHzD3B1sfGg8+ZOQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", "dev": true, "license": "MIT", "engines": { @@ -6884,8 +6991,10 @@ "dependencies": { "@fontsource/inter": "5.2.8", "@primeuix/themes": "2.0.3", + "@vueuse/core": "14.3.0", "pinia": "3.0.4", "primevue": "4.5.5", + "three": "0.184.0", "vue": "3.5.35", "vue-router": "5.1.0" }, @@ -6893,13 +7002,14 @@ "@tsconfig/node24": "24.0.4", "@types/jsdom": "28.0.3", "@types/node": "25.9.2", + "@types/three": "0.184.1", "@vitejs/plugin-vue": "6.0.7", "@vitest/eslint-plugin": "1.6.19", "@vue/eslint-config-prettier": "10.2.0", "@vue/eslint-config-typescript": "14.8.0", "@vue/test-utils": "2.4.11", "@vue/tsconfig": "0.9.1", - "@webwindowed/vite-plugin-cpp-header": "1.0.0", + "@webwindowed/vite-plugin-cpp-header": "1.1.0", "@webwindowed/web-api": "1.0.0", "eslint": "10.4.1", "eslint-plugin-vue": "10.9.2", diff --git a/src/ModMan/Asset/Image/DynamicAssetsImage.cpp b/src/ModMan/Asset/Image/DynamicAssetsImage.cpp new file mode 100644 index 00000000..db34f250 --- /dev/null +++ b/src/ModMan/Asset/Image/DynamicAssetsImage.cpp @@ -0,0 +1,107 @@ +#include "DynamicAssetsImage.h" + +#include "Context/ModManContext.h" +#include "Game/CommonAsset.h" +#include "Image/DdsWriter.h" +#include "Image/ImageToCommonConverter.h" +#include "Pool/XAssetInfo.h" +#include "SearchPath/SearchPaths.h" +#include "Utils/Logging/Log.h" +#include "Utils/StringUtils.h" + +#include +#include +#include + +using namespace image; +namespace fs = std::filesystem; + +namespace +{ + bool FindImage(const std::string& zoneName, const std::string& assetName, XAssetInfoGeneric*& outAssetInfo, Zone*& outZone) + { + auto& context = ModManContext::Get().m_fast_file; + const auto loadedZones = context.GetLoadedZones(); + for (const auto& loadedZone : loadedZones.Data()) + { + const auto& zone = loadedZone->GetZone(); + if (zone.m_name != zoneName) + continue; + + const auto* assetTypeMapper = ICommonAssetTypeMapper::GetCommonAssetMapperByGame(zone.m_game_id); + const auto gameAssetType = assetTypeMapper->CommonToGameAssetType(CommonAssetType::IMAGE); + if (!gameAssetType) + continue; + + outAssetInfo = zone.m_pools.GetAsset(*gameAssetType, assetName); + if (outAssetInfo) + { + outZone = &loadedZone->GetZone(); + return true; + } + } + + return false; + } + + void ImageDds(const webwindowed::dynamic_asset_request& request, webwindowed::dynamic_asset_response& response) + { + const auto imageName = request.get_query("name"); + const auto zoneName = request.get_query("zone"); + if (!imageName.has_value() || imageName->empty() || !zoneName.has_value() || zoneName->empty()) + { + con::error("Bad dds request (name={} zone={})", imageName.value_or(""), zoneName.value_or("")); + response.send_response(400); + return; + } + + XAssetInfoGeneric* image; + Zone* zone; + if (!FindImage(*zoneName, *imageName, image, zone)) + { + con::warn("Could not find image {} of zone {}", *imageName, *zoneName); + response.send_response(404); + return; + } + + assert(image); + assert(zone); + + const auto gameName = GameId_Names[std::to_underlying(zone->m_game_id)]; + const auto converter = ToCommonConverter::GetForGame(zone->m_game_id); + if (!converter) + { + con::error("No image converter for game {}", gameName); + response.send_response(500); + return; + } + + std::unique_ptr texture; + { + const auto searchPaths = ModManContext::Get().m_fast_file.GetSearchPaths(); + texture = converter->Convert(*image, searchPaths.Data()); + if (!texture) + { + con::warn("Failed to convert image {} of zone {}", *imageName, *zoneName); + response.send_response(500); + return; + } + } + + std::ostringstream ss; + DdsWriter output; + output.DumpImage(ss, texture.get()); + + const auto data = ss.str(); + response.set_content_type("image/x-direct-draw-surface"); + response.send_response(data.data(), data.size()); + } +} // namespace + +namespace image +{ + void RegisterDynamicAssets(webwindowed::asset_handler_plugin& assetHandler) + { + assetHandler.add_dynamic_asset(webwindowed::dynamic_asset("image/dds", ImageDds)); + } +} // namespace image diff --git a/src/ModMan/Asset/Image/DynamicAssetsImage.h b/src/ModMan/Asset/Image/DynamicAssetsImage.h new file mode 100644 index 00000000..b875a46f --- /dev/null +++ b/src/ModMan/Asset/Image/DynamicAssetsImage.h @@ -0,0 +1,8 @@ +#pragma once + +#include "Web/WebWindowedLib.h" + +namespace image +{ + void RegisterDynamicAssets(webwindowed::asset_handler_plugin& assetHandler); +} diff --git a/src/ModMan/Asset/XModel/DynamicAssetsXModel.cpp b/src/ModMan/Asset/XModel/DynamicAssetsXModel.cpp new file mode 100644 index 00000000..db9401e9 --- /dev/null +++ b/src/ModMan/Asset/XModel/DynamicAssetsXModel.cpp @@ -0,0 +1,100 @@ +#include "DynamicAssetsXModel.h" + +#include "Context/ModManContext.h" +#include "Game/CommonAsset.h" +#include "Pool/XAssetInfo.h" +#include "Utils/Logging/Log.h" +#include "XModel/Gltf/GltfBinOutput.h" +#include "XModel/Gltf/GltfWriter.h" +#include "XModel/XModelToCommonConverter.h" + +#include + +using namespace xmodel; + +namespace +{ + bool FindXModel(const std::string& zoneName, const std::string& assetName, XAssetInfoGeneric*& outAssetInfo, Zone*& outZone) + { + auto& context = ModManContext::Get().m_fast_file; + const auto loadedZones = context.GetLoadedZones(); + for (const auto& loadedZone : loadedZones.Data()) + { + const auto& zone = loadedZone->GetZone(); + if (zone.m_name != zoneName) + continue; + + const auto* assetTypeMapper = ICommonAssetTypeMapper::GetCommonAssetMapperByGame(zone.m_game_id); + const auto gameAssetType = assetTypeMapper->CommonToGameAssetType(CommonAssetType::XMODEL); + if (!gameAssetType) + continue; + + outAssetInfo = zone.m_pools.GetAsset(*gameAssetType, assetName); + if (outAssetInfo) + { + outZone = &loadedZone->GetZone(); + return true; + } + } + + return false; + } + + void XModelGlb(const webwindowed::dynamic_asset_request& request, webwindowed::dynamic_asset_response& response) + { + const auto modelName = request.get_query("name"); + const auto zoneName = request.get_query("zone"); + if (!modelName.has_value() || modelName->empty() || !zoneName.has_value() || zoneName->empty()) + { + con::error("Bad glb request (name={} zone={})", modelName.value_or(""), zoneName.value_or("")); + response.send_response(400); + return; + } + + XAssetInfoGeneric* model; + Zone* zone; + if (!FindXModel(*zoneName, *modelName, model, zone)) + { + con::warn("Could not find xmodel {} of zone {}", *modelName, *zoneName); + response.send_response(404); + return; + } + + assert(model); + assert(zone); + + const auto gameName = GameId_Names[std::to_underlying(zone->m_game_id)]; + const auto converter = ToCommonConverter::GetForGame(zone->m_game_id); + if (!converter) + { + con::error("No xmodel converter for game {}", gameName); + response.send_response(500); + return; + } + + const auto maybeCommon = converter->Convert(*model, 0); + if (!maybeCommon) + { + con::warn("Failed to convert xmodel {} of zone {}", *modelName, *zoneName); + response.send_response(500); + return; + } + + std::ostringstream ss; + const gltf::BinOutput output(ss); + const auto gltfWriter = gltf::Writer::CreateWriter(&output, gameName, *zoneName); + gltfWriter->Write(*maybeCommon); + + const auto data = ss.str(); + response.set_content_type("model/gltf-binary"); + response.send_response(data.data(), data.size()); + } +} // namespace + +namespace xmodel +{ + void RegisterDynamicAssets(webwindowed::asset_handler_plugin& assetHandler) + { + assetHandler.add_dynamic_asset(webwindowed::dynamic_asset("xmodel/glb", XModelGlb)); + } +} // namespace xmodel diff --git a/src/ModMan/Asset/XModel/DynamicAssetsXModel.h b/src/ModMan/Asset/XModel/DynamicAssetsXModel.h new file mode 100644 index 00000000..3d77cf4a --- /dev/null +++ b/src/ModMan/Asset/XModel/DynamicAssetsXModel.h @@ -0,0 +1,8 @@ +#pragma once + +#include "Web/WebWindowedLib.h" + +namespace xmodel +{ + void RegisterDynamicAssets(webwindowed::asset_handler_plugin& assetHandler); +} diff --git a/src/ModMan/Context/FastFileContext.cpp b/src/ModMan/Context/FastFileContext.cpp index cc3a45e6..220802f9 100644 --- a/src/ModMan/Context/FastFileContext.cpp +++ b/src/ModMan/Context/FastFileContext.cpp @@ -1,5 +1,10 @@ #include "FastFileContext.h" +#include "Game/AutoSearchPaths.h" +#include "IObjLoader.h" +#include "SearchPath/IWD.h" +#include "SearchPath/SearchPathFilesystem.h" +#include "Utils/StringUtils.h" #include "Web/Binds/ZoneBinds.h" #include "Web/UiCommunication.h" #include "ZoneLoading.h" @@ -36,14 +41,82 @@ namespace std::string m_zone_name; double m_last_progress; }; + + std::unique_ptr CreateSearchPath(const std::string& searchPathStr) + { + auto searchPath = std::make_unique(searchPathStr); + con::debug("Loaded search path \"{}\"", searchPathStr); + + SearchPaths searchPaths; + bool hasIwds = false; + + std::filesystem::directory_iterator iterator(searchPathStr); + const auto end = fs::end(iterator); + for (auto i = fs::begin(iterator); i != end; ++i) + { + if (!i->is_regular_file()) + continue; + + auto extension = i->path().extension().string(); + utils::MakeStringLowerCase(extension); + if (extension == ".iwd") + { + std::string iwdPath = i->path().string(); + auto iwd = iwd::LoadFromFile(iwdPath); + if (iwd) + { + if (!hasIwds) + { + searchPaths.CommitSearchPath(std::move(searchPath)); + hasIwds = true; + } + + searchPaths.CommitSearchPath(std::move(iwd)); + con::debug("Loaded search path \"{}\"", iwdPath); + } + } + } + + if (hasIwds) + return std::make_unique(std::move(searchPaths)); + + return searchPath; + } } // namespace -LoadedZone::LoadedZone(std::unique_ptr zone, std::string filePath) - : m_zone(std::move(zone)), - m_file_path(std::move(filePath)) +ContextSearchPath::ContextSearchPath(std::unique_ptr searchPath) + : m_search_path(std::move(searchPath)), + m_ref_count(1) { } +LoadedZone::LoadedZone(std::unique_ptr zone, std::string filePath, std::vector searchPaths) + : m_zone(std::move(zone)), + m_file_path(std::move(filePath)), + m_search_paths(std::move(searchPaths)) +{ +} + +Zone& LoadedZone::GetZone() +{ + return *m_zone; +} + +const Zone& LoadedZone::GetZone() const +{ + return *m_zone; +} + +const std::string& LoadedZone::GetFilePath() const +{ + return m_file_path; +} + +const std::vector& LoadedZone::GetSearchPaths() const +{ + return m_search_paths; +} + void FastFileContext::Destroy() { // Unload all zones @@ -56,36 +129,100 @@ std::expected FastFileContext::LoadFastFile(const std: if (!zone) return std::unexpected(std::move(zone.error())); - auto loadedZone = std::make_unique(std::move(*zone), path); - - LoadedZone* result; + auto searchPathsForZone = AutoSearchPaths::GetForGame((*zone)->m_game_id)->GetSearchPathsForZonePath(path); { - std::lock_guard lock(m_zone_lock); - result = m_loaded_zones.emplace_back(std::move(loadedZone)).get(); + std::lock_guard lock(m_search_path_lock); + for (const auto& searchPathStr : searchPathsForZone) + { + const auto existingSearchPath = m_context_search_paths.find(searchPathStr); + if (existingSearchPath == m_context_search_paths.end()) + { + auto searchPath = CreateSearchPath(searchPathStr); + m_search_paths.IncludeSearchPath(searchPath.get()); + m_context_search_paths.emplace(searchPathStr, std::make_unique(std::move(searchPath))); + } + else + { + existingSearchPath->second->m_ref_count++; + } + } } - ui::NotifyZoneLoaded(*result); + auto loadedZone = std::make_unique(std::move(*zone), path, std::move(searchPathsForZone)); - return result; + LoadedZone* loadedZonePtr; + { + std::lock_guard lock(m_zone_lock); + loadedZonePtr = m_loaded_zones.emplace_back(std::move(loadedZone)).get(); + } + + { + std::shared_lock lock(m_search_path_lock); + IObjLoader::GetObjLoaderForGame(loadedZonePtr->GetZone().m_game_id)->LoadReferencedContainersForZone(m_search_paths, loadedZonePtr->GetZone()); + } + + ui::NotifyZoneLoaded(*loadedZonePtr); + + return loadedZonePtr; } std::expected FastFileContext::UnloadZone(const std::string& zoneName) { + std::unique_ptr removedLoadedZone; + { std::lock_guard lock(m_zone_lock); const auto existingZone = std::ranges::find_if(m_loaded_zones, [&zoneName](const std::unique_ptr& loadedZone) { - return loadedZone->m_zone->m_name == zoneName; + return loadedZone->GetZone().m_name == zoneName; }); - if (existingZone != m_loaded_zones.end()) + if (existingZone == m_loaded_zones.end()) + return std::unexpected(std::format("No zone with name {} loaded", zoneName)); + + removedLoadedZone = std::move(*existingZone); + m_loaded_zones.erase(existingZone); + + ui::NotifyZoneUnloaded(zoneName); + } + + assert(removedLoadedZone); + + { + std::shared_lock lock(m_search_path_lock); + IObjLoader::GetObjLoaderForGame(removedLoadedZone->GetZone().m_game_id)->UnloadContainersOfZone(removedLoadedZone->GetZone()); + } + + { + std::lock_guard lock(m_search_path_lock); + for (const auto& searchPathStr : removedLoadedZone->GetSearchPaths()) { - m_loaded_zones.erase(existingZone); - ui::NotifyZoneUnloaded(zoneName); - return {}; + const auto existingSearchPath = m_context_search_paths.find(searchPathStr); + if (existingSearchPath != m_context_search_paths.end()) + { + assert(existingSearchPath->second->m_ref_count > 0); + const auto newRefCount = --existingSearchPath->second->m_ref_count; + + if (newRefCount == 0) + { + m_search_paths.RemoveSearchPath(existingSearchPath->second->m_search_path.get()); + m_context_search_paths.erase(existingSearchPath); + con::debug("Unloaded search path \"{}\"", searchPathStr); + } + } } } - return std::unexpected(std::format("No zone with name {} loaded", zoneName)); + return {}; +} + +ReadAccess>> FastFileContext::GetLoadedZones() +{ + return ReadAccess>>(std::shared_lock(m_zone_lock), m_loaded_zones); +} + +ReadAccess FastFileContext::GetSearchPaths() +{ + return ReadAccess(std::shared_lock(m_search_path_lock), m_search_paths); } diff --git a/src/ModMan/Context/FastFileContext.h b/src/ModMan/Context/FastFileContext.h index 842b5e5f..73aaf66c 100644 --- a/src/ModMan/Context/FastFileContext.h +++ b/src/ModMan/Context/FastFileContext.h @@ -1,19 +1,58 @@ #pragma once +#include "SearchPath/SearchPaths.h" #include "Zone/Zone.h" #include #include #include +#include +#include #include +class ContextSearchPath +{ +public: + explicit ContextSearchPath(std::unique_ptr searchPath); + + std::unique_ptr m_search_path; + unsigned m_ref_count; +}; + class LoadedZone { public: + LoadedZone(std::unique_ptr zone, std::string filePath, std::vector searchPaths); + + [[nodiscard]] Zone& GetZone(); + [[nodiscard]] const Zone& GetZone() const; + + [[nodiscard]] const std::string& GetFilePath() const; + [[nodiscard]] const std::vector& GetSearchPaths() const; + +private: std::unique_ptr m_zone; std::string m_file_path; + std::vector m_search_paths; +}; - LoadedZone(std::unique_ptr zone, std::string filePath); +template class ReadAccess +{ +public: + ReadAccess(std::shared_lock lock, T& data) + : m_read_lock(std::move(lock)), + m_data(data) + { + } + + [[nodiscard]] T& Data() const + { + return m_data; + } + +private: + std::shared_lock m_read_lock; + T& m_data; }; class FastFileContext @@ -24,6 +63,14 @@ public: std::expected LoadFastFile(const std::string& path); std::expected UnloadZone(const std::string& zoneName); + ReadAccess>> GetLoadedZones(); + ReadAccess GetSearchPaths(); + +private: std::vector> m_loaded_zones; std::shared_mutex m_zone_lock; + + SearchPaths m_search_paths; + std::unordered_map> m_context_search_paths; + std::shared_mutex m_search_path_lock; }; diff --git a/src/ModMan/Web/Binds/AssetBinds.cpp b/src/ModMan/Web/Binds/AssetBinds.cpp index 0bef17a7..bc5a5af2 100644 --- a/src/ModMan/Web/Binds/AssetBinds.cpp +++ b/src/ModMan/Web/Binds/AssetBinds.cpp @@ -9,79 +9,79 @@ NLOHMANN_JSON_SERIALIZE_ENUM(CommonAssetType, { - {CommonAssetType::PHYS_PRESET, "PHYS_PRESET" }, - {CommonAssetType::XANIM, "XANIM" }, - {CommonAssetType::XMODEL, "XMODEL" }, - {CommonAssetType::MATERIAL, "MATERIAL" }, - {CommonAssetType::TECHNIQUE_SET, "TECHNIQUE_SET" }, - {CommonAssetType::IMAGE, "IMAGE" }, - {CommonAssetType::SOUND, "SOUND" }, - {CommonAssetType::SOUND_CURVE, "SOUND_CURVE" }, - {CommonAssetType::LOADED_SOUND, "LOADED_SOUND" }, - {CommonAssetType::CLIP_MAP, "CLIP_MAP" }, - {CommonAssetType::COM_WORLD, "COM_WORLD" }, - {CommonAssetType::GAME_WORLD_SP, "GAME_WORLD_SP" }, - {CommonAssetType::GAME_WORLD_MP, "GAME_WORLD_MP" }, - {CommonAssetType::MAP_ENTS, "MAP_ENTS" }, - {CommonAssetType::GFX_WORLD, "GFX_WORLD" }, - {CommonAssetType::LIGHT_DEF, "LIGHT_DEF" }, - {CommonAssetType::UI_MAP, "UI_MAP" }, - {CommonAssetType::FONT, "FONT" }, - {CommonAssetType::MENU_LIST, "MENU_LIST" }, - {CommonAssetType::MENU, "MENU" }, - {CommonAssetType::LOCALIZE_ENTRY, "LOCALIZE_ENTRY" }, - {CommonAssetType::WEAPON, "WEAPON" }, - {CommonAssetType::SOUND_DRIVER_GLOBALS, "SOUND_DRIVER_GLOBALS"}, - {CommonAssetType::FX, "FX" }, - {CommonAssetType::IMPACT_FX, "IMPACT_FX" }, - {CommonAssetType::AI_TYPE, "AI_TYPE" }, - {CommonAssetType::MP_TYPE, "MP_TYPE" }, - {CommonAssetType::CHARACTER, "CHARACTER" }, - {CommonAssetType::XMODEL_ALIAS, "XMODEL_ALIAS" }, - {CommonAssetType::RAW_FILE, "RAW_FILE" }, - {CommonAssetType::STRING_TABLE, "STRING_TABLE" }, - {CommonAssetType::XMODEL_PIECES, "XMODEL_PIECES" }, - {CommonAssetType::PHYS_COLL_MAP, "PHYS_COLL_MAP" }, - {CommonAssetType::XMODEL_SURFS, "XMODEL_SURFS" }, - {CommonAssetType::PIXEL_SHADER, "PIXEL_SHADER" }, - {CommonAssetType::VERTEX_SHADER, "VERTEX_SHADER" }, - {CommonAssetType::VERTEX_DECL, "VERTEX_DECL" }, - {CommonAssetType::FX_WORLD, "FX_WORLD" }, - {CommonAssetType::LEADERBOARD, "LEADERBOARD" }, - {CommonAssetType::STRUCTURED_DATA_DEF, "STRUCTURED_DATA_DEF" }, - {CommonAssetType::TRACER, "TRACER" }, - {CommonAssetType::VEHICLE, "VEHICLE" }, - {CommonAssetType::ADDON_MAP_ENTS, "ADDON_MAP_ENTS" }, - {CommonAssetType::GLASS_WORLD, "GLASS_WORLD" }, - {CommonAssetType::PATH_DATA, "PATH_DATA" }, - {CommonAssetType::VEHICLE_TRACK, "VEHICLE_TRACK" }, - {CommonAssetType::ATTACHMENT, "ATTACHMENT" }, - {CommonAssetType::SURFACE_FX, "SURFACE_FX" }, - {CommonAssetType::SCRIPT, "SCRIPT" }, - {CommonAssetType::PHYS_CONSTRAINTS, "PHYS_CONSTRAINTS" }, - {CommonAssetType::DESTRUCTIBLE_DEF, "DESTRUCTIBLE_DEF" }, - {CommonAssetType::SOUND_PATCH, "SOUND_PATCH" }, - {CommonAssetType::WEAPON_DEF, "WEAPON_DEF" }, - {CommonAssetType::WEAPON_VARIANT, "WEAPON_VARIANT" }, - {CommonAssetType::MP_BODY, "MP_BODY" }, - {CommonAssetType::MP_HEAD, "MP_HEAD" }, - {CommonAssetType::PACK_INDEX, "PACK_INDEX" }, - {CommonAssetType::XGLOBALS, "XGLOBALS" }, - {CommonAssetType::DDL, "DDL" }, - {CommonAssetType::GLASSES, "GLASSES" }, - {CommonAssetType::EMBLEM_SET, "EMBLEM_SET" }, - {CommonAssetType::FONT_ICON, "FONT_ICON" }, - {CommonAssetType::WEAPON_FULL, "WEAPON_FULL" }, - {CommonAssetType::ATTACHMENT_UNIQUE, "ATTACHMENT_UNIQUE" }, - {CommonAssetType::WEAPON_CAMO, "WEAPON_CAMO" }, - {CommonAssetType::KEY_VALUE_PAIRS, "KEY_VALUE_PAIRS" }, - {CommonAssetType::MEMORY_BLOCK, "MEMORY_BLOCK" }, - {CommonAssetType::SKINNED_VERTS, "SKINNED_VERTS" }, - {CommonAssetType::QDB, "QDB" }, - {CommonAssetType::SLUG, "SLUG" }, - {CommonAssetType::FOOTSTEP_TABLE, "FOOTSTEP_TABLE" }, - {CommonAssetType::FOOTSTEP_FX_TABLE, "FOOTSTEP_FX_TABLE" }, - {CommonAssetType::ZBARRIER, "ZBARRIER" }, + {CommonAssetType::PHYS_PRESET, "phys_preset" }, + {CommonAssetType::XANIM, "xanim" }, + {CommonAssetType::XMODEL, "xmodel" }, + {CommonAssetType::MATERIAL, "material" }, + {CommonAssetType::TECHNIQUE_SET, "technique_set" }, + {CommonAssetType::IMAGE, "image" }, + {CommonAssetType::SOUND, "sound" }, + {CommonAssetType::SOUND_CURVE, "sound_curve" }, + {CommonAssetType::LOADED_SOUND, "loaded_sound" }, + {CommonAssetType::CLIP_MAP, "clip_map" }, + {CommonAssetType::COM_WORLD, "com_world" }, + {CommonAssetType::GAME_WORLD_SP, "game_world_sp" }, + {CommonAssetType::GAME_WORLD_MP, "game_world_mp" }, + {CommonAssetType::MAP_ENTS, "map_ents" }, + {CommonAssetType::GFX_WORLD, "gfx_world" }, + {CommonAssetType::LIGHT_DEF, "light_def" }, + {CommonAssetType::UI_MAP, "ui_map" }, + {CommonAssetType::FONT, "font" }, + {CommonAssetType::MENU_LIST, "menu_list" }, + {CommonAssetType::MENU, "menu" }, + {CommonAssetType::LOCALIZE_ENTRY, "localize_entry" }, + {CommonAssetType::WEAPON, "weapon" }, + {CommonAssetType::SOUND_DRIVER_GLOBALS, "sound_driver_globals"}, + {CommonAssetType::FX, "fx" }, + {CommonAssetType::IMPACT_FX, "impact_fx" }, + {CommonAssetType::AI_TYPE, "ai_type" }, + {CommonAssetType::MP_TYPE, "mp_type" }, + {CommonAssetType::CHARACTER, "character" }, + {CommonAssetType::XMODEL_ALIAS, "xmodel_alias" }, + {CommonAssetType::RAW_FILE, "raw_file" }, + {CommonAssetType::STRING_TABLE, "string_table" }, + {CommonAssetType::XMODEL_PIECES, "xmodel_pieces" }, + {CommonAssetType::PHYS_COLL_MAP, "phys_coll_map" }, + {CommonAssetType::XMODEL_SURFS, "xmodel_surfs" }, + {CommonAssetType::PIXEL_SHADER, "pixel_shader" }, + {CommonAssetType::VERTEX_SHADER, "vertex_shader" }, + {CommonAssetType::VERTEX_DECL, "vertex_decl" }, + {CommonAssetType::FX_WORLD, "fx_world" }, + {CommonAssetType::LEADERBOARD, "leaderboard" }, + {CommonAssetType::STRUCTURED_DATA_DEF, "structured_data_def" }, + {CommonAssetType::TRACER, "tracer" }, + {CommonAssetType::VEHICLE, "vehicle" }, + {CommonAssetType::ADDON_MAP_ENTS, "addon_map_ents" }, + {CommonAssetType::GLASS_WORLD, "glass_world" }, + {CommonAssetType::PATH_DATA, "path_data" }, + {CommonAssetType::VEHICLE_TRACK, "vehicle_track" }, + {CommonAssetType::ATTACHMENT, "attachment" }, + {CommonAssetType::SURFACE_FX, "surface_fx" }, + {CommonAssetType::SCRIPT, "script" }, + {CommonAssetType::PHYS_CONSTRAINTS, "phys_constraints" }, + {CommonAssetType::DESTRUCTIBLE_DEF, "destructible_def" }, + {CommonAssetType::SOUND_PATCH, "sound_patch" }, + {CommonAssetType::WEAPON_DEF, "weapon_def" }, + {CommonAssetType::WEAPON_VARIANT, "weapon_variant" }, + {CommonAssetType::MP_BODY, "mp_body" }, + {CommonAssetType::MP_HEAD, "mp_head" }, + {CommonAssetType::PACK_INDEX, "pack_index" }, + {CommonAssetType::XGLOBALS, "xglobals" }, + {CommonAssetType::DDL, "ddl" }, + {CommonAssetType::GLASSES, "glasses" }, + {CommonAssetType::EMBLEM_SET, "emblem_set" }, + {CommonAssetType::FONT_ICON, "font_icon" }, + {CommonAssetType::WEAPON_FULL, "weapon_full" }, + {CommonAssetType::ATTACHMENT_UNIQUE, "attachment_unique" }, + {CommonAssetType::WEAPON_CAMO, "weapon_camo" }, + {CommonAssetType::KEY_VALUE_PAIRS, "key_value_pairs" }, + {CommonAssetType::MEMORY_BLOCK, "memory_block" }, + {CommonAssetType::SKINNED_VERTS, "skinned_verts" }, + {CommonAssetType::QDB, "qdb" }, + {CommonAssetType::SLUG, "slug" }, + {CommonAssetType::FOOTSTEP_TABLE, "footstep_table" }, + {CommonAssetType::FOOTSTEP_FX_TABLE, "footstep_fx_table" }, + {CommonAssetType::ZBARRIER, "zbarrier" }, }); namespace @@ -146,11 +146,11 @@ namespace ZoneAssetsDto result; { - std::shared_lock lock(context.m_zone_lock); + const auto loadedZones = context.GetLoadedZones(); - for (const auto& loadedZone : context.m_loaded_zones) + for (const auto& loadedZone : loadedZones.Data()) { - const auto& zone = *loadedZone->m_zone; + const auto& zone = loadedZone->GetZone(); if (zone.m_name == zoneName) return CreateZoneAssetsDto(zone); } diff --git a/src/ModMan/Web/Binds/UnlinkingBinds.cpp b/src/ModMan/Web/Binds/UnlinkingBinds.cpp index 2226a93f..82d14dd7 100644 --- a/src/ModMan/Web/Binds/UnlinkingBinds.cpp +++ b/src/ModMan/Web/Binds/UnlinkingBinds.cpp @@ -52,19 +52,23 @@ namespace std::expected UnlinkZoneInDbThread(const std::string& zoneName) { - const auto& context = ModManContext::Get().m_fast_file; - const auto existingZone = std::ranges::find_if(context.m_loaded_zones, - [&zoneName](const std::unique_ptr& loadedZone) - { - return loadedZone->m_zone->m_name == zoneName; - }); + Zone* zone; + { + auto& context = ModManContext::Get().m_fast_file; + const auto loadedZones = context.GetLoadedZones(); + const auto existingZone = std::ranges::find_if(loadedZones.Data(), + [&zoneName](const std::unique_ptr& loadedZone) + { + return loadedZone->GetZone().m_name == zoneName; + }); - if (existingZone == context.m_loaded_zones.end()) - return std::unexpected(std::format("No zone with name {} loaded", zoneName)); + if (existingZone == loadedZones.Data().end()) + return std::unexpected(std::format("No zone with name {} loaded", zoneName)); - const auto& loadedZone = *existingZone->get(); + zone = &existingZone->get()->GetZone(); + } - auto* objWriter = IObjWriter::GetObjWriterForGame(loadedZone.m_zone->m_game_id); + auto* objWriter = IObjWriter::GetObjWriterForGame(zone->m_game_id); const auto outputFolderPath = fs::path(utils::GetExecutablePath()).parent_path() / "zone_dump" / zoneName; const auto outputFolderPathStr = outputFolderPath.string(); @@ -72,7 +76,7 @@ namespace OutputPathFilesystem outputFolderOutputPath(outputFolderPath); SearchPaths searchPaths; AssetDumpingContext dumpingContext( - *loadedZone.m_zone, outputFolderPathStr, outputFolderOutputPath, searchPaths, std::make_unique(zoneName)); + *zone, outputFolderPathStr, outputFolderOutputPath, searchPaths, std::make_unique(zoneName)); objWriter->DumpZone(dumpingContext); return {}; diff --git a/src/ModMan/Web/Binds/ZoneBinds.cpp b/src/ModMan/Web/Binds/ZoneBinds.cpp index 2bc98679..298727c8 100644 --- a/src/ModMan/Web/Binds/ZoneBinds.cpp +++ b/src/ModMan/Web/Binds/ZoneBinds.cpp @@ -7,19 +7,20 @@ NLOHMANN_JSON_SERIALIZE_ENUM(GameId, { - {GameId::IW3, "IW3"}, - {GameId::IW4, "IW4"}, - {GameId::IW5, "IW5"}, - {GameId::T4, "T4" }, - {GameId::T5, "T5" }, - {GameId::T6, "T6" }, + {GameId::IW3, "iw3"}, + {GameId::IW4, "iw4"}, + {GameId::IW5, "iw5"}, + {GameId::T4, "t4" }, + {GameId::T5, "t5" }, + {GameId::T6, "t6" }, }); NLOHMANN_JSON_SERIALIZE_ENUM(GamePlatform, { - {GamePlatform::PC, "PC" }, - {GamePlatform::XBOX, "XBOX"}, - {GamePlatform::PS3, "PS3" }, + {GamePlatform::PC, "pc" }, + {GamePlatform::XBOX, "xbox"}, + {GamePlatform::PS3, "ps3" }, + {GamePlatform::WIIU, "wiiu"}, }); namespace @@ -63,10 +64,10 @@ namespace ZoneDto CreateZoneDto(const LoadedZone& loadedZone) { return ZoneDto{ - .name = loadedZone.m_zone->m_name, - .filePath = loadedZone.m_file_path, - .game = loadedZone.m_zone->m_game_id, - .platform = loadedZone.m_zone->m_platform, + .name = loadedZone.GetZone().m_name, + .filePath = loadedZone.GetFilePath(), + .game = loadedZone.GetZone().m_game_id, + .platform = loadedZone.GetZone().m_platform, }; } @@ -77,10 +78,10 @@ namespace std::vector result; { - std::shared_lock lock(context.m_zone_lock); - result.reserve(context.m_loaded_zones.size()); + const auto loadedZones = context.GetLoadedZones(); + result.reserve(loadedZones.Data().size()); - for (const auto& loadedZone : context.m_loaded_zones) + for (const auto& loadedZone : loadedZones.Data()) { result.emplace_back(CreateZoneDto(*loadedZone)); } @@ -105,7 +106,7 @@ namespace ZoneLoadedDto{ .zone = CreateZoneDto(*maybeZone.value()), }); - con::debug("Loaded zone \"{}\"", maybeZone.value()->m_zone->m_name); + con::debug("Loaded zone \"{}\"", maybeZone.value()->GetZone().m_name); } else { diff --git a/src/ModMan/main.cpp b/src/ModMan/main.cpp index a43ff958..82f6b788 100644 --- a/src/ModMan/main.cpp +++ b/src/ModMan/main.cpp @@ -6,6 +6,10 @@ #include "Web/ViteAssets.h" #include "Web/WebWindowedLib.h" +// Assets +#include "Asset/Image/DynamicAssetsImage.h" +#include "Asset/XModel/DynamicAssetsXModel.h" + #include #include #include @@ -19,7 +23,7 @@ using namespace std::string_literals; namespace { #ifdef _DEBUG - void SpawnDevToolsWindow() + void CreateDevToolsWindow() { con::debug("Creating dev tools window"); @@ -31,11 +35,19 @@ namespace newWindow.set_title("Devtools"); newWindow.set_window_size(640, 480); newWindow.set_window_min(480, 320); - (void)newWindow.navigate(std::format("http://localhost:{}/__devtools__/", VITE_DEV_SERVER_PORT)); + const auto result = newWindow.navigate(std::format("http://localhost:{}/__devtools__/", VITE_DEV_SERVER_PORT)); + if (!result.has_value()) + con::error("Dev tools window navigation failed: {}", result.error().message()); } #endif - int SpawnMainWindow() + void RegisterDynamicAssets(webwindowed::asset_handler_plugin& assetHandler) + { + image::RegisterDynamicAssets(assetHandler); + xmodel::RegisterDynamicAssets(assetHandler); + } + + int RunModManApp() { con::debug("Creating main window"); @@ -48,12 +60,21 @@ namespace #endif newWindow.set_title("OpenAssetTools ModMan"); - // newWindow.set_window_min(640, 480); + newWindow.set_window_min(640, 480); newWindow.set_window_size(1280, 640); - const auto assetHandlerPlugin = std::make_shared(VITE_ASSETS, std::extent_v); + const auto assetHandlerPlugin = std::make_shared(); assetHandlerPlugin->set_protocol_name("modman"); - newWindow.register_plugin(assetHandlerPlugin); + +#ifdef _DEBUG + // Allow assets from dev server to access dynamic assets + assetHandlerPlugin->set_allow_all_origins(true); +#endif + + for (const auto& asset : VITE_ASSETS) + assetHandlerPlugin->add_static_asset(webwindowed::static_asset(asset.filename, asset.data, asset.dataSize)); + + RegisterDynamicAssets(*assetHandlerPlugin); webwindowed::commands_builder commands; ui::RegisterAllBinds(commands); @@ -62,23 +83,30 @@ namespace #ifdef _DEBUG auto result = newWindow.navigate(VITE_DEV_SERVER ? std::format("http://localhost:{}", VITE_DEV_SERVER_PORT) : assetHandlerPlugin->get_url_for_asset("index.html")); - if (VITE_DEV_SERVER) - { - newWindow.dispatch( - [] - { - SpawnDevToolsWindow(); - }); - } #else auto result = newWindow.navigate(assetHandlerPlugin->get_url_for_asset("index.html")); #endif + if (!result.has_value()) + con::error("Main window navigation failed: {}", result.error().message()); webwindowed::app app; + app.register_plugin(assetHandlerPlugin); app.register_plugin(std::make_shared()); app.register_plugin(std::make_shared()); - (void)app.run(context.m_main_window); +#ifdef _DEBUG + if (VITE_DEV_SERVER) + { + CreateDevToolsWindow(); + result = app.open_window(context.m_dev_tools_window); + if (!result.has_value()) + con::error("Failed to open dev tools window: {}", result.error().message()); + } +#endif + + result = app.run(context.m_main_window); + if (!result.has_value()) + con::error("Error while running app: {}", result.error().message()); return 0; } @@ -128,7 +156,7 @@ int main(int argc, const char** argv) ModManContext::Get().Startup(); - const auto result = SpawnMainWindow(); + const auto result = RunModManApp(); ModManContext::Get().Destroy(); diff --git a/src/ModManUi/package.json b/src/ModManUi/package.json index 17bed55c..d9de8f16 100644 --- a/src/ModManUi/package.json +++ b/src/ModManUi/package.json @@ -16,8 +16,10 @@ "dependencies": { "@fontsource/inter": "5.2.8", "@primeuix/themes": "2.0.3", + "@vueuse/core": "14.3.0", "pinia": "3.0.4", "primevue": "4.5.5", + "three": "0.184.0", "vue": "3.5.35", "vue-router": "5.1.0" }, @@ -25,13 +27,14 @@ "@tsconfig/node24": "24.0.4", "@types/jsdom": "28.0.3", "@types/node": "25.9.2", + "@types/three": "0.184.1", "@vitejs/plugin-vue": "6.0.7", "@vitest/eslint-plugin": "1.6.19", "@vue/eslint-config-prettier": "10.2.0", "@vue/eslint-config-typescript": "14.8.0", "@vue/test-utils": "2.4.11", "@vue/tsconfig": "0.9.1", - "@webwindowed/vite-plugin-cpp-header": "1.0.0", + "@webwindowed/vite-plugin-cpp-header": "1.1.0", "@webwindowed/web-api": "1.0.0", "eslint": "10.4.1", "eslint-plugin-vue": "10.9.2", diff --git a/src/ModManUi/public/skybox/citrus_orchard_puresky.jpg b/src/ModManUi/public/skybox/citrus_orchard_puresky.jpg new file mode 100644 index 00000000..e9d182f6 Binary files /dev/null and b/src/ModManUi/public/skybox/citrus_orchard_puresky.jpg differ diff --git a/src/ModManUi/public/skybox/citrus_orchard_puresky.txt b/src/ModManUi/public/skybox/citrus_orchard_puresky.txt new file mode 100644 index 00000000..c6e4ae79 --- /dev/null +++ b/src/ModManUi/public/skybox/citrus_orchard_puresky.txt @@ -0,0 +1 @@ +https://polyhaven.com/a/citrus_orchard_puresky diff --git a/src/ModManUi/src/components/assets/xmodel/ThreeResourceTracker.ts b/src/ModManUi/src/components/assets/xmodel/ThreeResourceTracker.ts new file mode 100644 index 00000000..b5cc8548 --- /dev/null +++ b/src/ModManUi/src/components/assets/xmodel/ThreeResourceTracker.ts @@ -0,0 +1,169 @@ +import { BufferGeometry, Material, Object3D, Texture } from "three"; +import { type IUniform } from "three/src/renderers/shaders/UniformsLib.js"; + +function getMaterialsOfObject(object: Object3D) { + if ("material" in object) { + const value = object.material as Material | Material[]; + return Array.isArray(value) ? value : [value]; + } + + return []; +} + +export class ThreeResourceTracker { + private readonly textures: Record; + private readonly materials: Record; + private readonly geometries: Record; + private readonly objects: Record; + + constructor() { + this.textures = {}; + this.materials = {}; + this.geometries = {}; + this.objects = {}; + } + + refTexture(texture: Texture) { + if (!this.textures[texture.id]) { + this.textures[texture.id] = 1; + } else { + this.textures[texture.id]++; + } + } + + refMaterial(material: Material) { + if (!this.materials[material.uuid]) { + this.materials[material.uuid] = 1; + for (const property of Object.values(material)) { + if (property instanceof Texture) { + this.refTexture(property); + } + if ("uniforms" in material) { + for (const value of Object.values(material.uniforms as Record)) { + if (value) { + const uniformValue = value.value; + const uniformValues = Array.isArray(uniformValue) ? uniformValue : [uniformValue]; + + for (const maybeTexture of uniformValues) { + if (maybeTexture instanceof Texture) { + this.refTexture(maybeTexture); + } + } + } + } + } + } + } else { + this.materials[material.uuid]++; + } + } + + refGeometry(geometry: BufferGeometry) { + if (!this.geometries[geometry.id]) { + this.geometries[geometry.id] = 1; + } else { + this.geometries[geometry.id]++; + } + } + + refObject(object: Object3D) { + if (!this.objects[object.id]) { + this.objects[object.id] = 1; + for (const material of getMaterialsOfObject(object)) { + this.refMaterial(material); + } + if ("geometry" in object) { + this.refGeometry(object.geometry as BufferGeometry); + } + for (const child of object.children) { + this.refObject(child); + } + } else { + this.objects[object.id]++; + } + } + + unrefTexture(texture: Texture) { + const refCount = this.textures[texture.id]; + if (refCount === undefined) { + return; + } + + if (refCount > 1) { + this.textures[texture.id] = refCount - 1; + } else { + delete this.textures[texture.id]; + texture.dispose(); + } + } + + unrefMaterial(material: Material) { + const refCount = this.materials[material.uuid]; + if (refCount === undefined) { + return; + } + + if (refCount > 1) { + this.materials[material.uuid] = refCount - 1; + } else { + for (const property of Object.values(material)) { + if (property instanceof Texture) { + this.unrefTexture(property); + } + if ("uniforms" in material) { + for (const value of Object.values(material.uniforms as Record)) { + if (value) { + const uniformValue = value.value; + const uniformValues = Array.isArray(uniformValue) ? uniformValue : [uniformValue]; + + for (const maybeTexture of uniformValues) { + if (maybeTexture instanceof Texture) { + this.unrefTexture(maybeTexture); + } + } + } + } + } + } + material.dispose(); + delete this.materials[material.uuid]; + } + } + + unrefGeometry(geometry: BufferGeometry) { + const refCount = this.geometries[geometry.id]; + if (refCount === undefined) { + return; + } + + if (refCount > 1) { + this.geometries[geometry.id] = refCount - 1; + } else { + delete this.geometries[geometry.id]; + geometry.dispose(); + } + } + + unrefObject(object: Object3D) { + const refCount = this.objects[object.id]; + if (refCount === undefined) { + return; + } + + if (refCount > 1) { + this.objects[object.id] = refCount - 1; + } else { + for (const material of getMaterialsOfObject(object)) { + this.unrefMaterial(material); + } + if ("geometry" in object) { + this.unrefGeometry(object.geometry as BufferGeometry); + } + for (const child of object.children) { + this.unrefObject(child); + } + + delete this.objects[object.id]; + } + } +} diff --git a/src/ModManUi/src/components/assets/xmodel/XModelPreview.vue b/src/ModManUi/src/components/assets/xmodel/XModelPreview.vue new file mode 100644 index 00000000..1f82db69 --- /dev/null +++ b/src/ModManUi/src/components/assets/xmodel/XModelPreview.vue @@ -0,0 +1,249 @@ + + + + + diff --git a/src/ModManUi/src/i18n/i18n.ts b/src/ModManUi/src/i18n/i18n.ts new file mode 100644 index 00000000..4f9aed02 --- /dev/null +++ b/src/ModManUi/src/i18n/i18n.ts @@ -0,0 +1,96 @@ +import type { GameId, GamePlatform } from "@/native/ZoneBinds.ts"; +import type { CommonAssetType } from "@/native/AssetBinds.ts"; + +export function localizeGame(game: GameId) { + return game.toUpperCase(); +} + +const GAME_PLATFORM_LOOKUP: Record = { + pc: "PC", + ps3: "PS3", + xbox: "Xbox", + wiiu: "WiiU", +}; +export function localizePlatform(platform: GamePlatform) { + return GAME_PLATFORM_LOOKUP[platform]; +} + +const ASSET_TYPE_LOOKUP: Record = { + phys_preset: "Phys preset", + xanim: "XAnim", + xmodel: "XModel", + material: "Material", + technique_set: "Technique set", + image: "Image", + sound: "Sound", + sound_curve: "Sound curve", + loaded_sound: "Loaded sound", + clip_map: "Clip map", + com_world: "Com world", + game_world_sp: "Game world SP", + game_world_mp: "Game world MP", + map_ents: "Map ents", + gfx_world: "Gfx world", + light_def: "Light def", + ui_map: "UI map", + font: "Font", + menu_list: "Menu list", + menu: "Menu", + localize_entry: "Localize entry", + weapon: "Weapon", + sound_driver_globals: "Sound driver globals", + fx: "FX", + impact_fx: "Impact FX", + ai_type: "AI type", + mp_type: "MP type", + character: "Character", + xmodel_alias: "XModel alias", + raw_file: "Raw file", + string_table: "String table", + xmodel_pieces: "XModel pieces", + phys_coll_map: "Phys coll map", + xmodel_surfs: "XModel surfs", + pixel_shader: "Pixel shader", + vertex_shader: "Vertex shader", + vertex_decl: "Vertex decl", + fx_world: "FX world", + leaderboard: "Leaderboard", + structured_data_def: "Structured data def", + tracer: "Tracer", + vehicle: "Vehicle", + addon_map_ents: "Addon map ents", + glass_world: "Glass world", + path_data: "Path data", + vehicle_track: "Vehicle track", + attachment: "Attachment", + surface_fx: "Surface FX", + script: "Script", + phys_constraints: "Phys constraints", + destructible_def: "Destructible def", + sound_patch: "Sound patch", + weapon_def: "Weapon def", + weapon_variant: "Weapon variant", + mp_body: "MP body", + mp_head: "MP head", + pack_index: "Pack index", + xglobals: "XGlobals", + ddl: "DDL", + glasses: "Glasses", + emblem_set: "Emblem set", + font_icon: "Font icon", + weapon_full: "Weapon full", + attachment_unique: "Attachment unique", + weapon_camo: "Weapon camo", + key_value_pairs: "Key value pairs", + memory_block: "Memory block", + skinned_verts: "Skinned verts", + qdb: "Qdb", + slug: "Slug", + footstep_table: "Footstep table", + footstep_fx_table: "Footstep FX table", + zbarrier: "ZBarrier", +}; + +export function localizeAssetType(assetType: CommonAssetType): string { + return ASSET_TYPE_LOOKUP[assetType]; +} diff --git a/src/ModManUi/src/native/AssetBinds.ts b/src/ModManUi/src/native/AssetBinds.ts index 9b286056..2e0f9fee 100644 --- a/src/ModManUi/src/native/AssetBinds.ts +++ b/src/ModManUi/src/native/AssetBinds.ts @@ -1,80 +1,79 @@ import { getBinds } from "@webwindowed/web-api"; -export enum CommonAssetType { - PHYS_PRESET = "PHYS_PRESET", - XANIM = "XANIM", - XMODEL = "XMODEL", - MATERIAL = "MATERIAL", - TECHNIQUE_SET = "TECHNIQUE_SET", - IMAGE = "IMAGE", - SOUND = "SOUND", - SOUND_CURVE = "SOUND_CURVE", - LOADED_SOUND = "LOADED_SOUND", - CLIP_MAP = "CLIP_MAP", - COM_WORLD = "COM_WORLD", - GAME_WORLD_SP = "GAME_WORLD_SP", - GAME_WORLD_MP = "GAME_WORLD_MP", - MAP_ENTS = "MAP_ENTS", - GFX_WORLD = "GFX_WORLD", - LIGHT_DEF = "LIGHT_DEF", - UI_MAP = "UI_MAP", - FONT = "FONT", - MENU_LIST = "MENU_LIST", - MENU = "MENU", - LOCALIZE_ENTRY = "LOCALIZE_ENTRY", - WEAPON = "WEAPON", - SOUND_DRIVER_GLOBALS = "SOUND_DRIVER_GLOBALS", - FX = "FX", - IMPACT_FX = "IMPACT_FX", - AI_TYPE = "AI_TYPE", - MP_TYPE = "MP_TYPE", - CHARACTER = "CHARACTER", - XMODEL_ALIAS = "XMODEL_ALIAS", - RAW_FILE = "RAW_FILE", - STRING_TABLE = "STRING_TABLE", - XMODEL_PIECES = "XMODEL_PIECES", - PHYS_COLL_MAP = "PHYS_COLL_MAP", - XMODEL_SURFS = "XMODEL_SURFS", - PIXEL_SHADER = "PIXEL_SHADER", - VERTEX_SHADER = "VERTEX_SHADER", - VERTEX_DECL = "VERTEX_DECL", - FX_WORLD = "FX_WORLD", - LEADERBOARD = "LEADERBOARD", - STRUCTURED_DATA_DEF = "STRUCTURED_DATA_DEF", - TRACER = "TRACER", - VEHICLE = "VEHICLE", - ADDON_MAP_ENTS = "ADDON_MAP_ENTS", - GLASS_WORLD = "GLASS_WORLD", - PATH_DATA = "PATH_DATA", - VEHICLE_TRACK = "VEHICLE_TRACK", - ATTACHMENT = "ATTACHMENT", - SURFACE_FX = "SURFACE_FX", - SCRIPT = "SCRIPT", - PHYS_CONSTRAINTS = "PHYS_CONSTRAINTS", - DESTRUCTIBLE_DEF = "DESTRUCTIBLE_DEF", - SOUND_PATCH = "SOUND_PATCH", - WEAPON_DEF = "WEAPON_DEF", - WEAPON_VARIANT = "WEAPON_VARIANT", - MP_BODY = "MP_BODY", - MP_HEAD = "MP_HEAD", - PACK_INDEX = "PACK_INDEX", - XGLOBALS = "XGLOBALS", - DDL = "DDL", - GLASSES = "GLASSES", - EMBLEM_SET = "EMBLEM_SET", - FONT_ICON = "FONT_ICON", - WEAPON_FULL = "WEAPON_FULL", - ATTACHMENT_UNIQUE = "ATTACHMENT_UNIQUE", - WEAPON_CAMO = "WEAPON_CAMO", - KEY_VALUE_PAIRS = "KEY_VALUE_PAIRS", - MEMORY_BLOCK = "MEMORY_BLOCK", - SKINNED_VERTS = "SKINNED_VERTS", - QDB = "QDB", - SLUG = "SLUG", - FOOTSTEP_TABLE = "FOOTSTEP_TABLE", - FOOTSTEP_FX_TABLE = "FOOTSTEP_FX_TABLE", - ZBARRIER = "ZBARRIER", -} +export type CommonAssetType = + | "phys_preset" + | "xanim" + | "xmodel" + | "material" + | "technique_set" + | "image" + | "sound" + | "sound_curve" + | "loaded_sound" + | "clip_map" + | "com_world" + | "game_world_sp" + | "game_world_mp" + | "map_ents" + | "gfx_world" + | "light_def" + | "ui_map" + | "font" + | "menu_list" + | "menu" + | "localize_entry" + | "weapon" + | "sound_driver_globals" + | "fx" + | "impact_fx" + | "ai_type" + | "mp_type" + | "character" + | "xmodel_alias" + | "raw_file" + | "string_table" + | "xmodel_pieces" + | "phys_coll_map" + | "xmodel_surfs" + | "pixel_shader" + | "vertex_shader" + | "vertex_decl" + | "fx_world" + | "leaderboard" + | "structured_data_def" + | "tracer" + | "vehicle" + | "addon_map_ents" + | "glass_world" + | "path_data" + | "vehicle_track" + | "attachment" + | "surface_fx" + | "script" + | "phys_constraints" + | "destructible_def" + | "sound_patch" + | "weapon_def" + | "weapon_variant" + | "mp_body" + | "mp_head" + | "pack_index" + | "xglobals" + | "ddl" + | "glasses" + | "emblem_set" + | "font_icon" + | "weapon_full" + | "attachment_unique" + | "weapon_camo" + | "key_value_pairs" + | "memory_block" + | "skinned_verts" + | "qdb" + | "slug" + | "footstep_table" + | "footstep_fx_table" + | "zbarrier"; export interface AssetDto { type: CommonAssetType; diff --git a/src/ModManUi/src/native/ZoneBinds.ts b/src/ModManUi/src/native/ZoneBinds.ts index ab306c59..111c54d6 100644 --- a/src/ModManUi/src/native/ZoneBinds.ts +++ b/src/ModManUi/src/native/ZoneBinds.ts @@ -1,19 +1,8 @@ import { getBinds } from "@webwindowed/web-api"; -export enum GameId { - IW3 = "IW3", - IW4 = "IW4", - IW5 = "IW5", - T4 = "T4", - T5 = "T5", - T6 = "T6", -} +export type GameId = "iw3" | "iw4" | "iw5" | "t4" | "t5" | "t6"; -export enum GamePlatform { - PC = "PC", - XBOX = "XBOX", - PS3 = "PS3", -} +export type GamePlatform = "pc" | "xbox" | "ps3" | "wiiu"; export interface ZoneDto { name: string; diff --git a/src/ModManUi/src/utils/AssetTypeName.ts b/src/ModManUi/src/utils/AssetTypeName.ts deleted file mode 100644 index d4c3e8d7..00000000 --- a/src/ModManUi/src/utils/AssetTypeName.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { CommonAssetType } from "@/native/AssetBinds"; - -const LOOKUP_CAPITALIZED: Record = { - [CommonAssetType.PHYS_PRESET]: "Phys preset", - [CommonAssetType.XANIM]: "XAnim", - [CommonAssetType.XMODEL]: "XModel", - [CommonAssetType.MATERIAL]: "Material", - [CommonAssetType.TECHNIQUE_SET]: "Technique set", - [CommonAssetType.IMAGE]: "Image", - [CommonAssetType.SOUND]: "Sound", - [CommonAssetType.SOUND_CURVE]: "Sound curve", - [CommonAssetType.LOADED_SOUND]: "Loaded sound", - [CommonAssetType.CLIP_MAP]: "Clip map", - [CommonAssetType.COM_WORLD]: "Com world", - [CommonAssetType.GAME_WORLD_SP]: "Game world SP", - [CommonAssetType.GAME_WORLD_MP]: "Game world MP", - [CommonAssetType.MAP_ENTS]: "Map ents", - [CommonAssetType.GFX_WORLD]: "Gfx world", - [CommonAssetType.LIGHT_DEF]: "Light def", - [CommonAssetType.UI_MAP]: "UI map", - [CommonAssetType.FONT]: "Font", - [CommonAssetType.MENU_LIST]: "Menu list", - [CommonAssetType.MENU]: "Menu", - [CommonAssetType.LOCALIZE_ENTRY]: "Localize entry", - [CommonAssetType.WEAPON]: "Weapon", - [CommonAssetType.SOUND_DRIVER_GLOBALS]: "Sound driver globals", - [CommonAssetType.FX]: "FX", - [CommonAssetType.IMPACT_FX]: "Impact FX", - [CommonAssetType.AI_TYPE]: "AI type", - [CommonAssetType.MP_TYPE]: "MP type", - [CommonAssetType.CHARACTER]: "Character", - [CommonAssetType.XMODEL_ALIAS]: "XModel alias", - [CommonAssetType.RAW_FILE]: "Raw file", - [CommonAssetType.STRING_TABLE]: "String table", - [CommonAssetType.XMODEL_PIECES]: "XModel pieces", - [CommonAssetType.PHYS_COLL_MAP]: "Phys coll map", - [CommonAssetType.XMODEL_SURFS]: "XModel surfs", - [CommonAssetType.PIXEL_SHADER]: "Pixel shader", - [CommonAssetType.VERTEX_SHADER]: "Vertex shader", - [CommonAssetType.VERTEX_DECL]: "Vertex decl", - [CommonAssetType.FX_WORLD]: "FX world", - [CommonAssetType.LEADERBOARD]: "Leaderboard", - [CommonAssetType.STRUCTURED_DATA_DEF]: "Structured data def", - [CommonAssetType.TRACER]: "Tracer", - [CommonAssetType.VEHICLE]: "Vehicle", - [CommonAssetType.ADDON_MAP_ENTS]: "Addon map ents", - [CommonAssetType.GLASS_WORLD]: "Glass world", - [CommonAssetType.PATH_DATA]: "Path data", - [CommonAssetType.VEHICLE_TRACK]: "Vehicle track", - [CommonAssetType.ATTACHMENT]: "Attachment", - [CommonAssetType.SURFACE_FX]: "Surface FX", - [CommonAssetType.SCRIPT]: "Script", - [CommonAssetType.PHYS_CONSTRAINTS]: "Phys constraints", - [CommonAssetType.DESTRUCTIBLE_DEF]: "Destructible def", - [CommonAssetType.SOUND_PATCH]: "Sound patch", - [CommonAssetType.WEAPON_DEF]: "Weapon def", - [CommonAssetType.WEAPON_VARIANT]: "Weapon variant", - [CommonAssetType.MP_BODY]: "MP body", - [CommonAssetType.MP_HEAD]: "MP head", - [CommonAssetType.PACK_INDEX]: "Pack index", - [CommonAssetType.XGLOBALS]: "XGlobals", - [CommonAssetType.DDL]: "DDL", - [CommonAssetType.GLASSES]: "Glasses", - [CommonAssetType.EMBLEM_SET]: "Emblem set", - [CommonAssetType.FONT_ICON]: "Font icon", - [CommonAssetType.WEAPON_FULL]: "Weapon full", - [CommonAssetType.ATTACHMENT_UNIQUE]: "Attachment unique", - [CommonAssetType.WEAPON_CAMO]: "Weapon camo", - [CommonAssetType.KEY_VALUE_PAIRS]: "Key value pairs", - [CommonAssetType.MEMORY_BLOCK]: "Memory block", - [CommonAssetType.SKINNED_VERTS]: "Skinned verts", - [CommonAssetType.QDB]: "Qdb", - [CommonAssetType.SLUG]: "Slug", - [CommonAssetType.FOOTSTEP_TABLE]: "Footstep table", - [CommonAssetType.FOOTSTEP_FX_TABLE]: "Footstep FX table", - [CommonAssetType.ZBARRIER]: "ZBarrier", -}; - -export function getAssetTypeNameCapitalized(assetType: CommonAssetType): string { - return LOOKUP_CAPITALIZED[assetType]; -} - -export function getAssetTypeNameLower(assetType: CommonAssetType): string { - return getAssetTypeNameCapitalized(assetType).toLocaleLowerCase(); -} diff --git a/src/ModManUi/src/view/inspect/ZoneInspectorDetails.vue b/src/ModManUi/src/view/inspect/ZoneInspectorDetails.vue index f5fac169..d98fd78d 100644 --- a/src/ModManUi/src/view/inspect/ZoneInspectorDetails.vue +++ b/src/ModManUi/src/view/inspect/ZoneInspectorDetails.vue @@ -8,11 +8,11 @@ import type { ZoneDto } from "@/native/ZoneBinds"; import { useZoneStore } from "@/stores/ZoneStore"; import { computed, watch } from "vue"; import type { CommonAssetType } from "@/native/AssetBinds"; -import { getAssetTypeNameCapitalized } from "@/utils/AssetTypeName"; import { useRouter } from "vue-router"; import { PAGE } from "@/router/Page"; import { useAssetStore } from "@/stores/AssetStore"; import { storeToRefs } from "pinia"; +import { localizeAssetType, localizeGame, localizePlatform } from "@/i18n/i18n.ts"; const assetStore = useAssetStore(); const zoneStore = useZoneStore(); @@ -52,7 +52,7 @@ const meterItems = computed(() => { .filter((entry) => entry[1] > minItemCountForDisplay) .sort((e0, e1) => e1[1] - e0[1]) .map((entry) => ({ - label: getAssetTypeNameCapitalized(entry[0] as CommonAssetType), + label: localizeAssetType(entry[0] as CommonAssetType), value: Math.round((entry[1] / assetCount.value) * 100), })); @@ -105,8 +105,8 @@ watch(

{{ selectedZone ?? "No zone selected" }}