From 720d0acece921ddbbab5505137fd60d6a3d3458f Mon Sep 17 00:00:00 2001 From: craftablescience Date: Tue, 23 Jul 2024 02:19:16 -0400 Subject: [PATCH] feat(vmtpp): add VMT library --- CMakeLists.txt | 7 +- README.md | 10 +- docs/index.md | 9 +- include/vmtpp/EntityAccess.h | 97 +++++++ include/vmtpp/Proxy.h | 35 +++ include/vmtpp/VMT.h | 71 +++++ include/vmtpp/vmtpp.h | 10 + src/vmtpp/EntityAccess.cpp | 84 ++++++ src/vmtpp/Proxy.cpp | 530 +++++++++++++++++++++++++++++++++++ src/vmtpp/VMT.cpp | 174 ++++++++++++ src/vmtpp/_vmtpp.cmake | 11 + test/vmtpp.cpp | 5 + 12 files changed, 1040 insertions(+), 3 deletions(-) create mode 100644 include/vmtpp/EntityAccess.h create mode 100644 include/vmtpp/Proxy.h create mode 100644 include/vmtpp/VMT.h create mode 100644 include/vmtpp/vmtpp.h create mode 100644 src/vmtpp/EntityAccess.cpp create mode 100644 src/vmtpp/Proxy.cpp create mode 100644 src/vmtpp/VMT.cpp create mode 100644 src/vmtpp/_vmtpp.cmake create mode 100644 test/vmtpp.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a7c4fdd51..c50f5b4be 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,6 +24,7 @@ option(SOURCEPP_USE_MDLPP "Build mdlpp library" ${SOURC option(SOURCEPP_USE_STEAMPP "Build steampp library" ${SOURCEPP_LIBS_START_ENABLED}) option(SOURCEPP_USE_TOOLPP "Build toolpp library" ${SOURCEPP_LIBS_START_ENABLED}) option(SOURCEPP_USE_VCRYPTPP "Build vcryptpp library" ${SOURCEPP_LIBS_START_ENABLED}) +option(SOURCEPP_USE_VMTPP "Build vmtpp library" ${SOURCEPP_LIBS_START_ENABLED}) option(SOURCEPP_USE_VPKPP "Build vpkpp library" ${SOURCEPP_LIBS_START_ENABLED}) option(SOURCEPP_USE_VTFPP "Build vtfpp library" ${SOURCEPP_LIBS_START_ENABLED}) @@ -54,6 +55,9 @@ endif() if(SOURCEPP_USE_TOOLPP) set(SOURCEPP_USE_KVPP ON CACHE INTERNAL "" FORCE) endif() +if(SOURCEPP_USE_VMTPP) + set(SOURCEPP_USE_KVPP ON CACHE INTERNAL "" FORCE) +endif() if(SOURCEPP_USE_VPKPP) set(SOURCEPP_USE_KVPP ON CACHE INTERNAL "" FORCE) endif() @@ -194,6 +198,7 @@ add_sourcepp_library(mdlpp ) # sourcepp::mdlpp add_sourcepp_library(steampp C PYTHON ) # sourcepp::steampp add_sourcepp_library(toolpp PYTHON ) # sourcepp::toolpp add_sourcepp_library(vcryptpp C CSHARP PYTHON ) # sourcepp::vcryptpp +add_sourcepp_library(vmtpp ) # sourcepp::vmtpp add_sourcepp_library(vpkpp C CSHARP NO_TEST ) # sourcepp::vpkpp add_sourcepp_library(vtfpp PYTHON BENCH) # sourcepp::vtfpp @@ -257,7 +262,7 @@ endif() # Print options print_options(OPTIONS - USE_BSPPP USE_DMXPP USE_GAMEPP USE_KVPP USE_MDLPP USE_STEAMPP USE_TOOLPP USE_VCRYPTPP USE_VPKPP USE_VTFPP + USE_BSPPP USE_DMXPP USE_GAMEPP USE_KVPP USE_MDLPP USE_STEAMPP USE_TOOLPP USE_VCRYPTPP USE_VMTPP USE_VPKPP USE_VTFPP BUILD_BENCHMARKS BUILD_C_WRAPPERS BUILD_CSHARP_WRAPPERS BUILD_PYTHON_WRAPPERS BUILD_WITH_OPENCL BUILD_WITH_TBB BUILD_WITH_THREADS BUILD_TESTS BUILD_WIN7_COMPAT LINK_STATIC_MSVC_RUNTIME VPKPP_SUPPORT_VPK_V54) diff --git a/README.md b/README.md index 72a49178e..a14775f93 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,14 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one ❌ + + vmtpp + VMT + ✅ + ❌ + + + vpkpp 007 v1.1, v1.3 (007 - Nightfire) @@ -321,7 +329,7 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one Libraries not starred should be considered stable, and their existing interfaces will not change much if at all. Note that wrappers only exist for stable libraries. -(†) Many text-based formats in Source are close to (if not identical to) KeyValues v1, such as [VMT](https://developer.valvesoftware.com/wiki/VMT) and [VMF](https://developer.valvesoftware.com/wiki/VMF_(Valve_Map_Format)). +(†) Many text-based formats in Source are close to (if not identical to) KeyValues v1, such as [RES](https://developer.valvesoftware.com/wiki/Resource_list_(Source)), [VDF](https://developer.valvesoftware.com/wiki/VDF), and [VMF](https://developer.valvesoftware.com/wiki/VMF_(Valve_Map_Format)). ## Wrappers diff --git a/docs/index.md b/docs/index.md index 6b5aa2152..5f3781fb7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -114,6 +114,13 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one ✅ ❌ + + vmtpp + VMT + ✅ + ❌ + + vpkpp 007 v1.1, v1.3 (007 - Nightfire) @@ -280,7 +287,7 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one Libraries not starred should be considered stable, and their existing interfaces will not change much if at all. Note that wrappers only exist for stable libraries. -(†) Many text-based formats in Source are close to (if not identical to) KeyValues v1, such as [VMT](https://developer.valvesoftware.com/wiki/VMT) and [VMF](https://developer.valvesoftware.com/wiki/VMF_(Valve_Map_Format)). +(†) Many text-based formats in Source are close to (if not identical to) KeyValues v1, such as [RES](https://developer.valvesoftware.com/wiki/Resource_list_(Source)), [VDF](https://developer.valvesoftware.com/wiki/VDF), and [VMF](https://developer.valvesoftware.com/wiki/VMF_(Valve_Map_Format)). ## Wrappers diff --git a/include/vmtpp/EntityAccess.h b/include/vmtpp/EntityAccess.h new file mode 100644 index 000000000..1b8a2690b --- /dev/null +++ b/include/vmtpp/EntityAccess.h @@ -0,0 +1,97 @@ +#pragma once + +#include + +namespace vmtpp { + +/// Expose an interface to read values from the entity the VMT is attached to for material proxies. +class IEntityAccess { +public: + virtual ~IEntityAccess() = default; + + /// "The number of seconds the current map has been running on the server for." + [[nodiscard]] virtual uint64_t getCurrentTime() const = 0; + + [[nodiscard]] virtual float getRenderAlpha() const = 0; + + [[nodiscard]] virtual float getAnimationProgress() const = 0; + + /// "The distance in units between the current player and the origin of the entity that the material is applied to." + [[nodiscard]] virtual float getDistanceToCurrentPlayer() const = 0; + + [[nodiscard]] virtual int getCurrentPlayerTeam() const = 0; + + [[nodiscard]] virtual int getTeam() const = 0; + + /// "The dot product of the current player's view angle and the relative origin of the material's entity." + [[nodiscard]] virtual float getCurrentPlayerViewDotProduct() const = 0; + + [[nodiscard]] virtual float getCurrentPlayerSpeed() const = 0; + + [[nodiscard]] virtual sourcepp::math::Vec3f getCurrentPlayerPosition() const = 0; + + [[nodiscard]] virtual float getSpeed() const = 0; + + [[nodiscard]] virtual sourcepp::math::Vec3f getOrigin() const = 0; + + /// "A static random number associated with the entity the material is applied to." + [[nodiscard]] virtual float getRandomNumber() const = 0; + + [[nodiscard]] virtual float getHealth() const = 0; + + [[nodiscard]] virtual bool isNPC() const = 0; + + [[nodiscard]] virtual bool isViewModel() const = 0; + + [[nodiscard]] virtual sourcepp::math::Vec3f getWorldDimensionsMinimum() const = 0; + + [[nodiscard]] virtual sourcepp::math::Vec3f getWorldDimensionsMaximum() const = 0; + + [[nodiscard]] virtual sourcepp::math::Vec3f getCurrentPlayerCrosshairColor() const = 0; +}; + +class EntityAccessEmpty : public IEntityAccess { +public: + EntityAccessEmpty(); + + [[nodiscard]] uint64_t getCurrentTime() const override; + + [[nodiscard]] float getRenderAlpha() const override; + + [[nodiscard]] float getAnimationProgress() const override; + + [[nodiscard]] float getDistanceToCurrentPlayer() const override; + + [[nodiscard]] int getCurrentPlayerTeam() const override; + + [[nodiscard]] int getTeam() const override; + + [[nodiscard]] float getCurrentPlayerViewDotProduct() const override; + + [[nodiscard]] float getCurrentPlayerSpeed() const override; + + [[nodiscard]] sourcepp::math::Vec3f getCurrentPlayerPosition() const override; + + [[nodiscard]] float getSpeed() const override; + + [[nodiscard]] sourcepp::math::Vec3f getOrigin() const override; + + [[nodiscard]] float getRandomNumber() const override; + + [[nodiscard]] float getHealth() const override; + + [[nodiscard]] bool isNPC() const override; + + [[nodiscard]] bool isViewModel() const override; + + [[nodiscard]] sourcepp::math::Vec3f getWorldDimensionsMinimum() const override; + + [[nodiscard]] sourcepp::math::Vec3f getWorldDimensionsMaximum() const override; + + [[nodiscard]] sourcepp::math::Vec3f getCurrentPlayerCrosshairColor() const override; + +private: + float random; +}; + +} // namespace vmtpp diff --git a/include/vmtpp/Proxy.h b/include/vmtpp/Proxy.h new file mode 100644 index 000000000..84b8420b0 --- /dev/null +++ b/include/vmtpp/Proxy.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +namespace vmtpp { + +class IEntityAccess; + +namespace Proxy { + +// We're going to try to implement proxies in a way that's distinct from but still roughly +// comparable to the SDK, so it's easy to compare functionality and get a nice reference! + +struct Data { + std::string name; + std::unordered_map variables; +}; + +using Function = void(*)(Data&, std::unordered_map&, const IEntityAccess&); + +Function add(const std::string& name, Function proxy); + +Function get(const std::string& name); + +void exec(Data& data, std::unordered_map& vmtVariables, const IEntityAccess& entity); + +void remove(const std::string& name); + +} // namespace Proxy + +} // namespace vmtpp + +#define VMTPP_MATERIAL_PROXY(name, proxy) \ + vmtpp::Proxy::Function VMTPP_MATERIAL_PROXY_##name = vmtpp::Proxy::add(#name, proxy) diff --git a/include/vmtpp/VMT.h b/include/vmtpp/VMT.h new file mode 100644 index 000000000..b4d0de69e --- /dev/null +++ b/include/vmtpp/VMT.h @@ -0,0 +1,71 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "EntityAccess.h" +#include "Proxy.h" + +namespace vmtpp { + +namespace Value { + +enum class Type { + INT, + FLOAT, + VEC3, + COLOR, +}; + +[[nodiscard]] Type getProbableType(std::string_view value); + +[[nodiscard]] Type getProbableTypeBasedOnAssociatedValues(std::string_view value, std::initializer_list others); + +[[nodiscard]] int toInt(std::string_view value); + +[[nodiscard]] std::string fromInt(int value); + +[[nodiscard]] float toFloat(std::string_view value); + +[[nodiscard]] std::string fromFloat(float value); + +[[nodiscard]] sourcepp::math::Vec3f toVec3(std::string_view value); + +[[nodiscard]] std::string fromVec3(sourcepp::math::Vec3f value); + +[[nodiscard]] sourcepp::math::Vec4f toColor(std::string_view value); + +[[nodiscard]] std::string fromColor(sourcepp::math::Vec4f value); + +} // namespace Value + +class VMT { +public: + explicit VMT(std::string_view vmt, const IEntityAccess& entityAccess_ = EntityAccessEmpty{}, int dxLevel = 98, int shaderDetailLevel = 3, std::string_view shaderFallbackSuffix = "DX9"); + + [[nodiscard]] std::string_view getShader() const; + + [[nodiscard]] bool hasCompileFlag(std::string_view flag) const; + + [[nodiscard]] const std::vector& getCompileFlags() const; + + [[nodiscard]] bool hasVariable(std::string_view key) const; + + [[nodiscard]] std::string_view getVariable(std::string_view key) const; + + [[nodiscard]] std::string_view operator[](std::string_view key) const; + + void update(); + +private: + const IEntityAccess& entityAccess; + std::string shader; + std::vector compileFlags; + std::unordered_map variables; + std::vector proxies; +}; + +} // namespace vmtpp diff --git a/include/vmtpp/vmtpp.h b/include/vmtpp/vmtpp.h new file mode 100644 index 000000000..a5c1a162c --- /dev/null +++ b/include/vmtpp/vmtpp.h @@ -0,0 +1,10 @@ +#pragma once + +/* + * This header is just included so consumers of this library can + * include it the same way as any of the other SourcePP libraries. + */ + +#include "EntityAccess.h" +#include "Proxy.h" +#include "VMT.h" diff --git a/src/vmtpp/EntityAccess.cpp b/src/vmtpp/EntityAccess.cpp new file mode 100644 index 000000000..1aa8293fe --- /dev/null +++ b/src/vmtpp/EntityAccess.cpp @@ -0,0 +1,84 @@ +#include + +#include + +using namespace sourcepp; +using namespace vmtpp; + +EntityAccessEmpty::EntityAccessEmpty() { + // MLP:FIM S09E20 04:58 + static float randomNumberGeneratorWinkWink = 0.f; + this->random = randomNumberGeneratorWinkWink++; +} + +uint64_t EntityAccessEmpty::getCurrentTime() const { + return std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); +} + +float EntityAccessEmpty::getRenderAlpha() const { + return 1.f; +} + +float EntityAccessEmpty::getAnimationProgress() const { + return 0.f; +} + +float EntityAccessEmpty::getDistanceToCurrentPlayer() const { + return 0.f; +} + +int EntityAccessEmpty::getCurrentPlayerTeam() const { + return 0; +} + +int EntityAccessEmpty::getTeam() const { + return 0; +} + +float EntityAccessEmpty::getCurrentPlayerViewDotProduct() const { + return 0.f; +} + +float EntityAccessEmpty::getCurrentPlayerSpeed() const { + return 0.f; +} + +math::Vec3f EntityAccessEmpty::getCurrentPlayerPosition() const { + return {}; +} + +float EntityAccessEmpty::getSpeed() const { + return 0.f; +} + +math::Vec3f EntityAccessEmpty::getOrigin() const { + return {}; +} + +float EntityAccessEmpty::getRandomNumber() const { + return this->random; +} + +float EntityAccessEmpty::getHealth() const { + return 0.f; +} + +bool EntityAccessEmpty::isNPC() const { + return false; +} + +bool EntityAccessEmpty::isViewModel() const { + return false; +} + +math::Vec3f EntityAccessEmpty::getWorldDimensionsMinimum() const { + return {}; +} + +math::Vec3f EntityAccessEmpty::getWorldDimensionsMaximum() const { + return {}; +} + +math::Vec3f EntityAccessEmpty::getCurrentPlayerCrosshairColor() const { + return {1.f, 1.f, 1.f}; +} diff --git a/src/vmtpp/Proxy.cpp b/src/vmtpp/Proxy.cpp new file mode 100644 index 000000000..3cefb21db --- /dev/null +++ b/src/vmtpp/Proxy.cpp @@ -0,0 +1,530 @@ +#include + +#include + +#include +#include + +using namespace sourcepp; +using namespace vmtpp; + +/* + * Unimplemented proxies and rationale: + * + * - AnimatedEntityTexture | treated the same as AnimatedTexture, maybe added later, need to figure out the interface + * - AnimateSpecificTexture | unused + * - BreakableSurface | maybe added later, need to figure out the interface + * - ConveyorScroll | maybe added later, need to figure out the interface + * - Camo | unused, broken + * - Dummy | pointless + * - FleshInterior | niche, don't understand how it works + * - MaterialModify | maybe added later, need to figure out the interface + * - MaterialModifyAnimated | maybe added later, need to figure out the interface + * - ParticleSphereProxy | used by one HL2 material, possibly pointless + * - WaterLOD | maybe added later, don't know how this entity works + * - CustomSteamImageOnModel | TF2/CSGO-specific, there's no point to implementing an empty proxy + * - invis | TF2-specific, implementation details unclear + * - spy_invis | TF2-specific, implementation details unclear + * - weapon_invis | TF2-specific, implementation details unclear + * - vm_invis | TF2-specific, implementation details unclear + * - building_invis | TF2-specific, unused, broken + * - AnimatedWeaponSheen | TF2-specific, implementation details unclear + * - WeaponSkin | TF2-specific, there's no point to implementing an empty proxy + * - BBQLevel | L4D2-specific, ??? + * - PortalOpenAmount | P1/2-specific, should really be handled somewhere else + * - PortalStaticModel | P1/2-specific, implementation details unclear + * - PortalStatic | P1/2-specific, should really be handled somewhere else + * - PortalPickAlphaMask | P1/2-specific, implementation details unclear + * - WheatlyEyeGlow | P2-specific, unused, broken + * - LightedFloorButton | P2-specific, unused, broken + * - survivalteammate | CSGO-specific, should really be handled somewhere else + * - MoneyProxy | CSGO-specific, niche + * - WeaponLabelText | CSGO-specific, niche + * - C4CompassArrow | CSGO-specific, niche, implementation details unknown + * + * Proxies still left to implement: + * + * - SelectFirstIfNonZero + * - WrapMinMax + * - Exponential + * - Sine + * - LinearRamp + * - CurrentTime + * - UniformNoise + * - GaussianNoise + * - MatrixRotate + * - Alpha + * - Cycle + * - PlayerProximity + * - PlayerTeamMatch + * - PlayerView + * - PlayerSpeed + * - PlayerPosition + * - EntitySpeed + * - EntityOrigin + * - EntityRandom + * - Health + * - IsNPC + * - WorldDims + * - CrosshairColor + * - AnimatedTexture / AnimatedOffsetTexture + * - Pupil + * - TextureTransform + * - TextureScroll + * - LampBeam + * - LampHalo + * - HeliBlade + * - PlayerLogo + * - Shadow + * - ShadowModel + * - Thermal + * - ToggleTexture + * - Empty + * - ConVar + * - EntityOriginAlyx + * - Ep1IntroVortRefract + * - VortEmissive + * - Shield + * - CommunityWeapon + * - InvulnLevel + * - BurnLevel + * - YellowLevel + * - ModelGlowColor + * - ItemTintColor + * - BuildingRescueLevel + * - TeamTexture + * - WeaponSkin + * - ShieldFalloff + * - StatTrakIllum + * - StatTrakDigit + * - StatTrakIcon + * - StickybombGlowColor + * - SniperRifleCharge + * - Heartbeat + * - WheatlyEyeGlow + * - BenefactorLevel + * - PlayerTeam + * - BloodyHands + * - IT + * - BurnLevel + * - NightVisionSelfIllum + * - AlienSurfaceFX + * - LanguagePreference + * - FizzlerVortex + * - Lightedmouth + * - TractorBeam + * - TauCharge + * - Select + * - GetTeamNumber + * - RemapValClamp + * - IronSightAmount + * - ApproachValue + */ + +namespace { + +std::unordered_map& getRegisteredProxies() { + static std::unordered_map proxies; + return proxies; +} + +} // namespace + +Proxy::Function Proxy::add(const std::string& name, Function proxy) { + getRegisteredProxies()[name] = proxy; + return proxy; +} + +Proxy::Function Proxy::get(const std::string& name) { + if (!getRegisteredProxies().contains(name)) { + return nullptr; + } + return getRegisteredProxies()[name]; +} + +void Proxy::exec(Data& data, std::unordered_map& vmtVariables, const IEntityAccess& entity) { + if (auto func = get(data.name)) { + func(data, vmtVariables, entity); + } +} + +void Proxy::remove(const std::string& name) { + getRegisteredProxies().erase(name); +} + +/** + * Add - add two variables + * in: srcVar1 + * in: srcVar2 + * out: resultVar + */ +VMTPP_MATERIAL_PROXY(Add, ([](Proxy::Data& data, std::unordered_map& vmtVariables, const IEntityAccess&) { + if (!data.variables.contains("srcvar1") || !data.variables.contains("srcvar2") || !data.variables.contains("resultvar")) { + return; + } + // todo: srcvars value might be an index into a vector variable + if (!vmtVariables.contains(data.variables["srcvar1"]) || !vmtVariables.contains(data.variables["srcvar2"])) { + return; + } + const auto& srcvar1 = vmtVariables[data.variables["srcvar1"]]; + const auto& srcvar2 = vmtVariables[data.variables["srcvar2"]]; + auto type = Value::getProbableTypeBasedOnAssociatedValues(srcvar1, {srcvar2}); + if (type != Value::getProbableType(srcvar2)) { + return; + } + switch (type) { + case Value::Type::INT: + vmtVariables[data.variables["resultvar"]] = Value::fromInt(Value::toInt(srcvar1) + Value::toInt(srcvar2)); + return; + case Value::Type::FLOAT: + vmtVariables[data.variables["resultvar"]] = Value::fromFloat(Value::toFloat(srcvar1) + Value::toFloat(srcvar2)); + return; + case Value::Type::VEC3: + vmtVariables[data.variables["resultvar"]] = Value::fromVec3(Value::toVec3(srcvar1) + Value::toVec3(srcvar2)); + return; + case Value::Type::COLOR: + vmtVariables[data.variables["resultvar"]] = Value::fromColor(Value::toColor(srcvar1) + Value::toColor(srcvar2)); + return; + } +})); + +/** + * Subtract - subtract two variables + * in: srcVar1 + * in: srcVar2 + * out: resultVar + */ +VMTPP_MATERIAL_PROXY(Subtract, ([](Proxy::Data& data, std::unordered_map& vmtVariables, const IEntityAccess&) { + if (!data.variables.contains("srcvar1") || !data.variables.contains("srcvar2") || !data.variables.contains("resultvar")) { + return; + } + if (!vmtVariables.contains(data.variables["srcvar1"]) || !vmtVariables.contains(data.variables["srcvar2"])) { + return; + } + const auto& srcvar1 = vmtVariables[data.variables["srcvar1"]]; + const auto& srcvar2 = vmtVariables[data.variables["srcvar2"]]; + auto type = Value::getProbableTypeBasedOnAssociatedValues(srcvar1, {srcvar2}); + if (type != Value::getProbableType(srcvar2)) { + return; + } + switch (type) { + case Value::Type::INT: + vmtVariables[data.variables["resultvar"]] = Value::fromInt(Value::toInt(srcvar1) - Value::toInt(srcvar2)); + return; + case Value::Type::FLOAT: + vmtVariables[data.variables["resultvar"]] = Value::fromFloat(Value::toFloat(srcvar1) - Value::toFloat(srcvar2)); + return; + case Value::Type::VEC3: + vmtVariables[data.variables["resultvar"]] = Value::fromVec3(Value::toVec3(srcvar1) - Value::toVec3(srcvar2)); + return; + case Value::Type::COLOR: + vmtVariables[data.variables["resultvar"]] = Value::fromColor(Value::toColor(srcvar1) - Value::toColor(srcvar2)); + return; + } +})); + +/** + * Multiply - multiply two variables + * in: srcVar1 + * in: srcVar2 + * out: resultVar + */ +VMTPP_MATERIAL_PROXY(Multiply, ([](Proxy::Data& data, std::unordered_map& vmtVariables, const IEntityAccess&) { + if (!data.variables.contains("srcvar1") || !data.variables.contains("srcvar2") || !data.variables.contains("resultvar")) { + return; + } + if (!vmtVariables.contains(data.variables["srcvar1"]) || !vmtVariables.contains(data.variables["srcvar2"])) { + return; + } + const auto& srcvar1 = vmtVariables[data.variables["srcvar1"]]; + const auto& srcvar2 = vmtVariables[data.variables["srcvar2"]]; + auto type = Value::getProbableTypeBasedOnAssociatedValues(srcvar1, {srcvar2}); + if (type != Value::getProbableType(srcvar2)) { + return; + } + switch (type) { + case Value::Type::INT: + vmtVariables[data.variables["resultvar"]] = Value::fromInt(Value::toInt(srcvar1) * Value::toInt(srcvar2)); + return; + case Value::Type::FLOAT: + vmtVariables[data.variables["resultvar"]] = Value::fromFloat(Value::toFloat(srcvar1) * Value::toFloat(srcvar2)); + return; + case Value::Type::VEC3: + vmtVariables[data.variables["resultvar"]] = Value::fromVec3(Value::toVec3(srcvar1).mul(Value::toVec3(srcvar2))); + return; + case Value::Type::COLOR: + vmtVariables[data.variables["resultvar"]] = Value::fromColor(Value::toColor(srcvar1).mul(Value::toColor(srcvar2))); + return; + } +})); + +/** + * Divide - divide two variables + * in: srcVar1 + * in: srcVar2 + * out: resultVar + */ +VMTPP_MATERIAL_PROXY(Divide, ([](Proxy::Data& data, std::unordered_map& vmtVariables, const IEntityAccess&) { + if (!data.variables.contains("srcvar1") || !data.variables.contains("srcvar2") || !data.variables.contains("resultvar")) { + return; + } + if (!vmtVariables.contains(data.variables["srcvar1"]) || !vmtVariables.contains(data.variables["srcvar2"])) { + return; + } + const auto& srcvar1 = vmtVariables[data.variables["srcvar1"]]; + const auto& srcvar2 = vmtVariables[data.variables["srcvar2"]]; + auto type = Value::getProbableTypeBasedOnAssociatedValues(srcvar1, {srcvar2}); + if (type != Value::getProbableType(srcvar2)) { + return; + } + switch (type) { + case Value::Type::INT: + vmtVariables[data.variables["resultvar"]] = Value::fromInt(Value::toInt(srcvar1) / Value::toInt(srcvar2)); + return; + case Value::Type::FLOAT: + vmtVariables[data.variables["resultvar"]] = Value::fromFloat(Value::toFloat(srcvar1) / Value::toFloat(srcvar2)); + return; + case Value::Type::VEC3: + vmtVariables[data.variables["resultvar"]] = Value::fromVec3(Value::toVec3(srcvar1).div(Value::toVec3(srcvar2))); + return; + case Value::Type::COLOR: + vmtVariables[data.variables["resultvar"]] = Value::fromColor(Value::toColor(srcvar1).div(Value::toColor(srcvar2))); + return; + } +})); + +/** + * Modulo - take the modulus of two variables + * in: srcVar1 + * in: srcVar2 + * out: resultVar + */ +VMTPP_MATERIAL_PROXY(Modulo, ([](Proxy::Data& data, std::unordered_map& vmtVariables, const IEntityAccess&) { + if (!data.variables.contains("srcvar1") || !data.variables.contains("srcvar2") || !data.variables.contains("resultvar")) { + return; + } + if (!vmtVariables.contains(data.variables["srcvar1"]) || !vmtVariables.contains(data.variables["srcvar2"])) { + return; + } + const auto& srcvar1 = vmtVariables[data.variables["srcvar1"]]; + const auto& srcvar2 = vmtVariables[data.variables["srcvar2"]]; + auto type = Value::getProbableTypeBasedOnAssociatedValues(srcvar1, {srcvar2}); + if (type != Value::getProbableType(srcvar2)) { + return; + } + switch (type) { + case Value::Type::INT: + vmtVariables[data.variables["resultvar"]] = Value::fromInt(Value::toInt(srcvar1) % Value::toInt(srcvar2)); + return; + case Value::Type::FLOAT: + vmtVariables[data.variables["resultvar"]] = Value::fromFloat(std::fmod(Value::toFloat(srcvar1), Value::toFloat(srcvar2))); + return; + case Value::Type::VEC3: + vmtVariables[data.variables["resultvar"]] = Value::fromVec3(Value::toVec3(srcvar1).mod(Value::toVec3(srcvar2))); + return; + case Value::Type::COLOR: + vmtVariables[data.variables["resultvar"]] = Value::fromColor(Value::toColor(srcvar1).mod(Value::toColor(srcvar2))); + return; + } +})); + +/** + * Equals - set one variable equal to another + * in: srcVar1 + * out: resultVar + */ +VMTPP_MATERIAL_PROXY(Equals, ([](Proxy::Data& data, std::unordered_map& vmtVariables, const IEntityAccess&) { + if (!data.variables.contains("srcvar1") || !data.variables.contains("resultvar")) { + return; + } + if (!vmtVariables.contains(data.variables["srcvar1"])) { + return; + } + vmtVariables[data.variables["resultvar"]] = vmtVariables[data.variables["srcvar1"]]; +})); + +/** + * Abs - compute the absolute value of a variable + * in: srcVar1 + * out: resultVar + */ +VMTPP_MATERIAL_PROXY(Abs, ([](Proxy::Data& data, std::unordered_map& vmtVariables, const IEntityAccess&) { + if (!data.variables.contains("srcvar1") || !data.variables.contains("resultvar")) { + return; + } + if (!vmtVariables.contains(data.variables["srcvar1"])) { + return; + } + const auto& srcvar1 = vmtVariables[data.variables["srcvar1"]]; + switch (Value::getProbableType(srcvar1)) { + case Value::Type::INT: + vmtVariables[data.variables["resultvar"]] = Value::fromInt(std::abs(Value::toInt(srcvar1))); + return; + case Value::Type::FLOAT: + vmtVariables[data.variables["resultvar"]] = Value::fromFloat(std::abs(Value::toFloat(srcvar1))); + return; + case Value::Type::VEC3: + vmtVariables[data.variables["resultvar"]] = Value::fromVec3(Value::toVec3(srcvar1).abs()); + return; + case Value::Type::COLOR: + vmtVariables[data.variables["resultvar"]] = Value::fromColor(Value::toColor(srcvar1).abs()); + return; + } +})); + +/** + * Frac - get the decimal part of a variable + * in: srcVar1 + * out: resultVar + */ +VMTPP_MATERIAL_PROXY(Frac, ([](Proxy::Data& data, std::unordered_map& vmtVariables, const IEntityAccess&) { + if (!data.variables.contains("srcvar1") || !data.variables.contains("resultvar")) { + return; + } + if (!vmtVariables.contains(data.variables["srcvar1"])) { + return; + } + const auto& srcvar1 = vmtVariables[data.variables["srcvar1"]]; + switch (Value::getProbableType(srcvar1)) { + case Value::Type::INT: + vmtVariables[data.variables["resultvar"]] = srcvar1; + return; + case Value::Type::FLOAT: { + float out; + std::modf(Value::toFloat(srcvar1), &out); + vmtVariables[data.variables["resultvar"]] = Value::fromFloat(out); + return; + } + case Value::Type::VEC3: { + math::Vec3f out; + auto in = Value::toVec3(srcvar1); + std::modf(in[0], &out[0]); + std::modf(in[1], &out[1]); + std::modf(in[2], &out[2]); + vmtVariables[data.variables["resultvar"]] = Value::fromVec3(out); + return; + } + case Value::Type::COLOR: { + math::Vec4f out; + auto in = Value::toColor(srcvar1); + std::modf(in[0], &out[0]); + std::modf(in[1], &out[1]); + std::modf(in[2], &out[2]); + std::modf(in[3], &out[3]); + vmtVariables[data.variables["resultvar"]] = Value::fromColor(out); + return; + } + } +})); + +/** + * Int - get the integer part of a variable + * in: srcVar1 + * out: resultVar + */ +VMTPP_MATERIAL_PROXY(Int, ([](Proxy::Data& data, std::unordered_map& vmtVariables, const IEntityAccess&) { + if (!data.variables.contains("srcvar1") || !data.variables.contains("resultvar")) { + return; + } + if (!vmtVariables.contains(data.variables["srcvar1"])) { + return; + } + const auto& srcvar1 = vmtVariables[data.variables["srcvar1"]]; + switch (Value::getProbableType(srcvar1)) { + case Value::Type::INT: + vmtVariables[data.variables["resultvar"]] = srcvar1; + return; + case Value::Type::FLOAT: + vmtVariables[data.variables["resultvar"]] = Value::fromFloat(static_cast(Value::toFloat(srcvar1))); + return; + case Value::Type::VEC3: { + math::Vec3f out; + auto in = Value::toVec3(srcvar1); + out[0] = static_cast(in[0]); + out[1] = static_cast(in[1]); + out[2] = static_cast(in[2]); + vmtVariables[data.variables["resultvar"]] = Value::fromVec3(out); + return; + } + case Value::Type::COLOR: { + math::Vec4f out; + auto in = Value::toColor(srcvar1); + out[0] = static_cast(in[0]); + out[1] = static_cast(in[1]); + out[2] = static_cast(in[2]); + out[3] = static_cast(in[3]); + vmtVariables[data.variables["resultvar"]] = Value::fromColor(out); + return; + } + } +})); + +/** + * Clamp - clamp a variable's value between two ends + * in: min + * in: max + * in: srcVar1 + * out: resultVar + */ +VMTPP_MATERIAL_PROXY(Clamp, ([](Proxy::Data& data, std::unordered_map& vmtVariables, const IEntityAccess&) { + if (!data.variables.contains("min") || !data.variables.contains("max") || !data.variables.contains("srcvar1") || !data.variables.contains("resultvar")) { + return; + } + if (!vmtVariables.contains(data.variables["min"]) || !vmtVariables.contains(data.variables["max"]) || !vmtVariables.contains(data.variables["srcvar1"])) { + return; + } + const auto& min = vmtVariables[data.variables["min"]]; + const auto& max = vmtVariables[data.variables["max"]]; + const auto& srcvar1 = vmtVariables[data.variables["srcvar1"]]; + switch (Value::getProbableType(srcvar1)) { + case Value::Type::INT: + vmtVariables[data.variables["resultvar"]] = Value::fromInt(static_cast(std::clamp(Value::toFloat(srcvar1), Value::toFloat(min), Value::toFloat(max)))); + return; + case Value::Type::FLOAT: + vmtVariables[data.variables["resultvar"]] = Value::fromFloat(std::clamp(Value::toFloat(srcvar1), Value::toFloat(min), Value::toFloat(max))); + return; + case Value::Type::VEC3: { + math::Vec3f out; + auto in = Value::toVec3(srcvar1); + out[0] = std::clamp(in[0], Value::toFloat(min), Value::toFloat(max)); + out[1] = std::clamp(in[1], Value::toFloat(min), Value::toFloat(max)); + out[2] = std::clamp(in[2], Value::toFloat(min), Value::toFloat(max)); + vmtVariables[data.variables["resultvar"]] = Value::fromVec3(out); + return; + } + case Value::Type::COLOR: { + math::Vec4f out; + auto in = Value::toColor(srcvar1); + out[0] = std::clamp(in[0], Value::toFloat(min), Value::toFloat(max)); + out[1] = std::clamp(in[1], Value::toFloat(min), Value::toFloat(max)); + out[2] = std::clamp(in[2], Value::toFloat(min), Value::toFloat(max)); + out[3] = std::clamp(in[3], Value::toFloat(min), Value::toFloat(max)); + vmtVariables[data.variables["resultvar"]] = Value::fromColor(out); + return; + } + } +})); + +/** + * LessOrEqual - compares the first value to the second + * in: lessEqualVar + * in: greaterVar + * in: srcVar1 + * in: srcVar2 + * out: resultVar + */ +VMTPP_MATERIAL_PROXY(LessOrEqual, ([](Proxy::Data& data, std::unordered_map& vmtVariables, const IEntityAccess&) { + if (!data.variables.contains("lessequalvar") || !data.variables.contains("greatervar") || !data.variables.contains("srcvar1") || !data.variables.contains("srcvar2") || !data.variables.contains("resultvar")) { + return; + } + if (!vmtVariables.contains(data.variables["lessequalvar"]) || !vmtVariables.contains(data.variables["greatervar"]) || !vmtVariables.contains(data.variables["srcvar1"]) || !vmtVariables.contains(data.variables["srcvar2"])) { + return; + } + const auto& srcvar1 = vmtVariables[data.variables["srcvar1"]]; + const auto& srcvar2 = vmtVariables[data.variables["srcvar2"]]; + if (Value::toFloat(srcvar1) <= Value::toFloat(srcvar2)) { + vmtVariables[data.variables["resultvar"]] = vmtVariables[data.variables["lessequalvar"]]; + } else { + vmtVariables[data.variables["resultvar"]] = vmtVariables[data.variables["greatervar"]]; + } +})); diff --git a/src/vmtpp/VMT.cpp b/src/vmtpp/VMT.cpp new file mode 100644 index 000000000..c2e9881a2 --- /dev/null +++ b/src/vmtpp/VMT.cpp @@ -0,0 +1,174 @@ +#include + +#include +#include + +#include +#include +#include + +using namespace kvpp; +using namespace sourcepp; +using namespace vmtpp; + +Value::Type Value::getProbableType(std::string_view value) { + if (auto spaceCount = std::count(value.begin(), value.end(), ' '); spaceCount >= 3) { + return Type::COLOR; + } else if (spaceCount == 2) { + return Type::VEC3; + } else if (value.find('.') != std::string_view::npos || value.find('e') != std::string_view::npos) { + return Type::FLOAT; + } else { + return Type::INT; + } +} + +Value::Type Value::getProbableTypeBasedOnAssociatedValues(std::string_view value, std::initializer_list others) { + if (auto type = getProbableType(value); type != Type::INT) { + return type; + } + for (auto other : others) { + if (getProbableType(other) == Type::FLOAT) { + return Type::FLOAT; + } + } + return Type::INT; +} + +int Value::toInt(std::string_view value) { + int num = 0; + string::toInt(string::trim(value), num); + return num; +} + +std::string Value::fromInt(int value) { + return std::to_string(value); +} + +float Value::toFloat(std::string_view value) { + float num = 0.f; + string::toFloat(string::trim(value), num); + return num; +} + +std::string Value::fromFloat(float value) { + return std::to_string(value); +} + +math::Vec3f Value::toVec3(std::string_view value) { + float scale = 1.f; + if (value.starts_with('{')) { + scale = 1.f / 255.f; + } + auto values = string::split(string::trim(string::trim(value, "{}[]")), ' '); + math::Vec3f out{}; + if (values.size() != 3) { + return out; + } + string::toFloat(values[0], out[0]); + string::toFloat(values[1], out[1]); + string::toFloat(values[2], out[2]); + out *= scale; + return out; +} + +std::string Value::fromVec3(math::Vec3f value) { + return '[' + std::to_string(value[0]) + ' ' + std::to_string(value[1]) + ' ' + std::to_string(value[2]) + ']'; +} + +math::Vec4f Value::toColor(std::string_view value) { + float scale = 1.f; + if (value.starts_with('{')) { + scale = 1.f / 255.f; + } + auto values = string::split(string::trim(string::trim(value, "{}[]")), ' '); + math::Vec4f out{}; + if (values.size() != 3 && values.size() != 4) { + return out; + } + string::toFloat(values[0], out[0]); + string::toFloat(values[1], out[1]); + string::toFloat(values[2], out[2]); + out *= scale; + if (values.size() == 4) { + string::toFloat(values[3], out[3]); + out[4] *= scale; + } else { + out[4] = 1.f; + } + return out; +} + +std::string Value::fromColor(math::Vec4f value) { + return '[' + std::to_string(value[0]) + ' ' + std::to_string(value[1]) + ' ' + std::to_string(value[2]) + (value[3] == 1.f ? (' ' + std::to_string(value[3])) : "") + ']'; +} + +VMT::VMT(std::string_view vmt, const IEntityAccess& entityAccess_, int dxLevel, int shaderDetailLevel, std::string_view shaderFallbackSuffix) + : entityAccess(entityAccess_) { + KV1 keyvalues{vmt}; + if (keyvalues.getChildCount() < 1) { + return; + } + + // Read shader name + this->shader = keyvalues[0].getKey(); + + for (const auto& element : keyvalues[0].getChildren()) { + // Add compile flags + if (element.getKey().starts_with('%') && Value::toInt(element.getValue())) { + this->compileFlags.emplace_back(element.getKey().substr(1)); + string::toLower(this->compileFlags.back()); + } + + // todo: careful parsing! ALL keys need to be lowercased and values need to be normalized for value getters and proxies to work + + // todo: check every key for gpu levels / dx levels / shader fallbacks / proxies + + this->variables[string::toLower(element.getKey().substr(1))] = element.getValue(); + } +} + +std::string_view VMT::getShader() const { + return this->shader; +} + +bool VMT::hasCompileFlag(std::string_view flag) const { + if (flag.starts_with('%')) { + flag = flag.substr(1); + } + return std::any_of(this->compileFlags.begin(), this->compileFlags.end(), [flag_=string::toLower(flag)](const std::string& existingFlag) { + return existingFlag == flag_; + }); +} + +const std::vector& VMT::getCompileFlags() const { + return this->compileFlags; +} + +bool VMT::hasVariable(std::string_view key) const { + if (key.starts_with('$')) { + key = key.substr(1); + } + return this->variables.contains(string::toLower(key)); +} + +std::string_view VMT::getVariable(std::string_view key) const { + if (key.starts_with('$')) { + key = key.substr(1); + } + const auto keyLower = string::toLower(key); + if (!this->variables.contains(keyLower)) { + return ""; + } + return this->variables.at(string::toLower(key)); +} + +std::string_view VMT::operator[](std::string_view key) const { + return this->getVariable(key); +} + +void VMT::update() { + for (auto& proxy : this->proxies) { + Proxy::exec(proxy, this->variables, this->entityAccess); + } +} diff --git a/src/vmtpp/_vmtpp.cmake b/src/vmtpp/_vmtpp.cmake new file mode 100644 index 000000000..9bff94375 --- /dev/null +++ b/src/vmtpp/_vmtpp.cmake @@ -0,0 +1,11 @@ +add_pretty_parser(vmtpp + DEPS sourcepp::kvpp + PRECOMPILED_HEADERS + "${CMAKE_CURRENT_SOURCE_DIR}/include/vmtpp/EntityAccess.h" + "${CMAKE_CURRENT_SOURCE_DIR}/include/vmtpp/Proxy.h" + "${CMAKE_CURRENT_SOURCE_DIR}/include/vmtpp/VMT.h" + "${CMAKE_CURRENT_SOURCE_DIR}/include/vmtpp/vmtpp.h" + SOURCES + "${CMAKE_CURRENT_LIST_DIR}/EntityAccess.cpp" + "${CMAKE_CURRENT_LIST_DIR}/Proxy.cpp" + "${CMAKE_CURRENT_LIST_DIR}/VMT.cpp") diff --git a/test/vmtpp.cpp b/test/vmtpp.cpp new file mode 100644 index 000000000..87bc9ce11 --- /dev/null +++ b/test/vmtpp.cpp @@ -0,0 +1,5 @@ +#include + +#include + +using namespace vmtpp;