diff --git a/CMakeLists.txt b/CMakeLists.txt
index 402db282b..195db19d4 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -21,6 +21,7 @@ option(SOURCEPP_USE_KVPP "Build kvpp library"
option(SOURCEPP_USE_MDLPP "Build mdlpp library" ON)
option(SOURCEPP_USE_STEAMPP "Build steampp library" ON)
option(SOURCEPP_USE_VICEPP "Build vicepp library" ON)
+option(SOURCEPP_USE_VMTPP "Build vmtpp library" ON)
option(SOURCEPP_USE_VPKPP "Build vpkpp library" ON)
option(SOURCEPP_USE_VTFPP "Build vtfpp library" ON)
option(SOURCEPP_BUILD_C_WRAPPERS "Build C wrappers for supported libraries" ON)
@@ -32,6 +33,9 @@ option(SOURCEPP_USE_STATIC_MSVC_RUNTIME "Link to static MSVC runtime library"
if(SOURCEPP_USE_STEAMPP)
set(SOURCEPP_USE_KVPP ON CACHE INTERNAL "")
endif()
+if(SOURCEPP_USE_VMTPP)
+ set(SOURCEPP_USE_KVPP ON CACHE INTERNAL "")
+endif()
if(SOURCEPP_USE_VPKPP)
set(SOURCEPP_USE_BSPPP ON CACHE INTERNAL "")
set(SOURCEPP_USE_KVPP ON CACHE INTERNAL "")
@@ -92,6 +96,7 @@ add_sourcepp_library(kvpp)
add_sourcepp_library(mdlpp)
add_sourcepp_library(steampp C)
add_sourcepp_library(vicepp C CSHARP)
+add_sourcepp_library(vmtpp)
add_sourcepp_library(vpkpp C CSHARP NO_TEST)
add_sourcepp_library(vtfpp)
diff --git a/README.md b/README.md
index 9eb73c5a8..e11aba039 100644
--- a/README.md
+++ b/README.md
@@ -102,6 +102,17 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one
✅ |
C C# |
+
+ vmtpp |
+
+
+ |
+ ✅ |
+ ❌ |
+ |
+
vpkpp |
@@ -135,7 +146,7 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one
|
-(\*) Many text-based formats in Source are close to (if not identical to) KeyValues v1, such as [VDF](https://developer.valvesoftware.com/wiki/VDF), [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)).
(†) The MDL parser is not complete. It is usable in its current state, but it does not currently parse more complex components like animations. This parser is still in development.
diff --git a/include/vmtpp/EntityAccess.h b/include/vmtpp/EntityAccess.h
new file mode 100644
index 000000000..487d98f0f
--- /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/vmtpp.h b/include/vmtpp/vmtpp.h
new file mode 100644
index 000000000..b4d0de69e
--- /dev/null
+++ b/include/vmtpp/vmtpp.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/src/vmtpp/EntityAccess.cpp b/src/vmtpp/EntityAccess.cpp
new file mode 100644
index 000000000..dab492077
--- /dev/null
+++ b/src/vmtpp/EntityAccess.cpp
@@ -0,0 +1,82 @@
+#include
+
+#include
+
+using namespace vmtpp;
+
+EntityAccessEmpty::EntityAccessEmpty() {
+ 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;
+}
+
+sourcepp::math::Vec3f EntityAccessEmpty::getCurrentPlayerPosition() const {
+ return {};
+}
+
+float EntityAccessEmpty::getSpeed() const {
+ return 0.f;
+}
+
+sourcepp::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;
+}
+
+sourcepp::math::Vec3f EntityAccessEmpty::getWorldDimensionsMinimum() const {
+ return {};
+}
+
+sourcepp::math::Vec3f EntityAccessEmpty::getWorldDimensionsMaximum() const {
+ return {};
+}
+
+sourcepp::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..654afb632
--- /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/_vmtpp.cmake b/src/vmtpp/_vmtpp.cmake
new file mode 100644
index 000000000..a93e204b4
--- /dev/null
+++ b/src/vmtpp/_vmtpp.cmake
@@ -0,0 +1,9 @@
+add_pretty_parser(vmtpp
+ DEPS kvpp
+ SOURCES
+ "${CMAKE_CURRENT_SOURCE_DIR}/include/vmtpp/EntityAccess.h"
+ "${CMAKE_CURRENT_SOURCE_DIR}/include/vmtpp/Proxy.h"
+ "${CMAKE_CURRENT_SOURCE_DIR}/include/vmtpp/vmtpp.h"
+ "${CMAKE_CURRENT_LIST_DIR}/EntityAccess.cpp"
+ "${CMAKE_CURRENT_LIST_DIR}/Proxy.cpp"
+ "${CMAKE_CURRENT_LIST_DIR}/vmtpp.cpp")
diff --git a/src/vmtpp/vmtpp.cpp b/src/vmtpp/vmtpp.cpp
new file mode 100644
index 000000000..595bfebb8
--- /dev/null
+++ b/src/vmtpp/vmtpp.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/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;