From dc50aaa86beb633909e5e92610f45ef853d12240 Mon Sep 17 00:00:00 2001 From: BMagnu <6238428+BMagnu@users.noreply.github.com> Date: Sun, 28 Sep 2025 17:40:46 +0900 Subject: [PATCH] Particle Scripting API (#7061) * Expose particle effects to scripting * Allow creation of particle sources * Add API for particle sources * bugfixes --- code/particle/ParticleManager.cpp | 13 ++ code/particle/ParticleManager.h | 4 + code/scripting/api/libs/tables.cpp | 13 ++ code/scripting/api/objs/particle.cpp | 239 +++++++++++++++++++++++++++ code/scripting/api/objs/particle.h | 20 +++ 5 files changed, 289 insertions(+) diff --git a/code/particle/ParticleManager.cpp b/code/particle/ParticleManager.cpp index e5ad473452..77e8e42987 100644 --- a/code/particle/ParticleManager.cpp +++ b/code/particle/ParticleManager.cpp @@ -38,6 +38,8 @@ void ParticleManager::shutdown() { ParticleSource* ParticleManager::createSource() { ParticleSource* source; + m_sourceValidityCounter++; + // If we are currently in the onFrame function, adding stuff to the vector would invalidate the iterator currently in use if (m_processingSources) { m_deferredSourceAdding.emplace_back(); @@ -80,9 +82,12 @@ void ParticleManager::doFrame(float) { TRACE_SCOPE(tracing::ProcessParticleEffects); m_processingSources = true; + bool changehappened = false; for (auto source = std::begin(m_sources); source != std::end(m_sources);) { if (!source->isValid() || !source->process()) { + changehappened = true; + // if we're sitting on the very last source, popping-back will invalidate the iterator! if (std::next(source) == m_sources.end()) { m_sources.pop_back(); @@ -102,9 +107,13 @@ void ParticleManager::doFrame(float) { m_processingSources = false; for (auto& source : m_deferredSourceAdding) { + changehappened = true; m_sources.push_back(std::move(source)); } m_deferredSourceAdding.clear(); + + if (changehappened) + m_sourceValidityCounter++; } ParticleEffectHandle ParticleManager::addEffect(ParticleEffect&& effect) @@ -179,6 +188,10 @@ void ParticleManager::clearSources() { m_deferredSourceAdding.clear(); } +uint32_t ParticleManager::getSourceValidityCounter() const { + return m_sourceValidityCounter; +} + namespace util { ParticleEffectHandle parseEffect(const SCP_string& objectName) { diff --git a/code/particle/ParticleManager.h b/code/particle/ParticleManager.h index a10620c710..395be16d10 100644 --- a/code/particle/ParticleManager.h +++ b/code/particle/ParticleManager.h @@ -37,6 +37,8 @@ class ParticleManager { SCP_vector m_sources; //!< The currently active sources bool m_processingSources = false; //!< @c true if sources are currently being processed + + uint32_t m_sourceValidityCounter = 0; /** * If the sources are currently being processed, no additional sources can be added. Instead, they are added to this * vector and then added to the main vector when processing is done. @@ -148,6 +150,8 @@ class ParticleManager { * @return A wrapper class which allows access to the created sources */ ParticleSource* createSource(ParticleEffectHandle index); + + uint32_t getSourceValidityCounter() const; }; namespace internal { diff --git a/code/scripting/api/libs/tables.cpp b/code/scripting/api/libs/tables.cpp index 66ee90a70f..572f752e20 100644 --- a/code/scripting/api/libs/tables.cpp +++ b/code/scripting/api/libs/tables.cpp @@ -11,6 +11,7 @@ #include "scripting/api/objs/team_colors.h" #include "scripting/api/objs/weaponclass.h" #include "scripting/api/objs/wingformation.h" +#include "scripting/api/objs/particle.h" #include "decals/decals.h" #include "fireball/fireballs.h" @@ -18,6 +19,7 @@ #include "mission/missionmessage.h" #include "ship/ship.h" #include "weapon/weapon.h" +#include "particle/ParticleManager.h" extern bool Ships_inited; @@ -373,5 +375,16 @@ ADE_FUNC(__len, l_Tables_TeamColors, nullptr, "Number of team colors", "number" return ade_set_args(L, "i", static_cast(Team_Names.size())); } +//*****SUBLIBRARY: Tables/ShipTypes +ADE_LIB_DERIV(l_Tables_ParticleEffects, "ParticleEffects", nullptr, nullptr, l_Tables); +ADE_INDEXER(l_Tables_ParticleEffects, "string Name", "Array of particle effects", "particle_effect", "Particle Effect handle, or invalid handle if name is invalid") +{ + const char* name; + if (!ade_get_args(L, "*s", &name)) + return ade_set_error(L, "o", l_ParticleEffect.Set(particle::ParticleEffectHandle::invalid())); + + return ade_set_args(L, "o", l_ParticleEffect.Set(particle::ParticleManager::get()->getEffectByName(name))); +} + } } diff --git a/code/scripting/api/objs/particle.cpp b/code/scripting/api/objs/particle.cpp index 70dec1931a..1ce81d7963 100644 --- a/code/scripting/api/objs/particle.cpp +++ b/code/scripting/api/objs/particle.cpp @@ -4,6 +4,9 @@ #include "particle.h" #include "vecmath.h" #include "object.h" +#include "model.h" +#include "particle/ParticleManager.h" +#include "particle/ParticleEffect.h" namespace scripting { namespace api { @@ -20,6 +23,18 @@ bool particle_h::isValid() const { return !part.expired(); } +particle_source_h::particle_source_h(particle::ParticleSource* source_p, uint32_t sourceValidityCounter_p) { + this->source = source_p; + this->sourceValidityCounter = sourceValidityCounter_p; +} + +particle::ParticleSource* particle_source_h::Get() const { + return isValid() ? this->source : nullptr; +} + +bool particle_source_h::isValid() const { + return particle::ParticleManager::get()->getSourceValidityCounter() == sourceValidityCounter; +} //**********HANDLE: Particle ADE_OBJ(l_Particle, particle_h, "particle", "Handle to a particle"); @@ -214,6 +229,230 @@ ADE_FUNC_DEPRECATED(setColor, l_Particle, "number r, number g, number b", "Sets } +ADE_OBJ(l_ParticleEffect, ::particle::ParticleEffectHandle, "particle_effect", "Handle to a tabled particle effect"); + +ADE_FUNC(getName, l_ParticleEffect, nullptr, "Returns the name under which this effect is stored", "string", "the name of the particle effect, or an empty string for an invalid handle") +{ + ::particle::ParticleEffectHandle ph; + if (!ade_get_args(L, "o", l_ParticleEffect.Get(&ph))) + return ade_set_error(L, "s", ""); + + if (!ph.isValid()) + return ade_set_error(L, "s", ""); + + const auto& particle_effect = particle::ParticleManager::get()->getEffect(ph); + if (particle_effect.empty()) + return ade_set_error(L, "s", ""); + + return ade_set_args(L, "s", particle_effect.front().getName().c_str()); +} + +ADE_FUNC(createSource, l_ParticleEffect, nullptr, "Creates a new particle source, spawning particles as per this particle effect", "particle_source", "the particle source, or nil for an invalid handle") +{ + ::particle::ParticleEffectHandle ph; + if (!ade_get_args(L, "o", l_ParticleEffect.Get(&ph))) + return ADE_RETURN_NIL; + + if (!ph.isValid()) + return ADE_RETURN_NIL; + + auto source = particle::ParticleManager::get()->createSource(ph); + return ade_set_args(L, "o", l_ParticleSource.Set(particle_source_h(source, particle::ParticleManager::get()->getSourceValidityCounter()))); +} + +ADE_OBJ(l_ParticleSource, particle_source_h, "particle_source", "Handle to a particle source. Only valid immediately after acquiring."); + +ADE_FUNC(setNormal, l_ParticleSource, "vector normal", "Sets the normal vector of this particle source.", nullptr, nullptr) +{ + particle_source_h ps; + vec3d normal; + if (!ade_get_args(L, "oo", l_ParticleSource.Get(&ps), l_Vector.Get(&normal))) + return ADE_RETURN_NIL; + + particle::ParticleSource* psp = ps.Get(); + if (psp == nullptr) + return ADE_RETURN_NIL; + + vm_vec_normalize_safe(&normal); + psp->setNormal(normal); + + return ADE_RETURN_NIL; +} + +ADE_FUNC(setTriggerRadius, l_ParticleSource, "number triggerRadius", "Sets the trigger radius of this particle source.", nullptr, nullptr) +{ + particle_source_h ps; + float trigger; + if (!ade_get_args(L, "of", l_ParticleSource.Get(&ps), &trigger)) + return ADE_RETURN_NIL; + + particle::ParticleSource* psp = ps.Get(); + if (psp == nullptr) + return ADE_RETURN_NIL; + + psp->setTriggerRadius(trigger); + + return ADE_RETURN_NIL; +} + +ADE_FUNC(setTriggerVelocity, l_ParticleSource, "number triggerVelocity", "Sets the trigger velocity of this particle source.", nullptr, nullptr) +{ + particle_source_h ps; + float trigger; + if (!ade_get_args(L, "of", l_ParticleSource.Get(&ps), &trigger)) + return ADE_RETURN_NIL; + + particle::ParticleSource* psp = ps.Get(); + if (psp == nullptr) + return ADE_RETURN_NIL; + + psp->setTriggerVelocity(trigger); + + return ADE_RETURN_NIL; +} + +ADE_FUNC(createOnCoordinates, l_ParticleSource, "vector position, orientation orientation, [vector velocity = zero_vector, orientation orientationOverride = identity, boolean orientationOverrideIsRelative = true]", "Actually spawns this particle source at the specified position.", "boolean", "returns true if the source was successfully created, false otherwise") +{ + particle_source_h ps; + vec3d position; + matrix_h orientation; + vec3d velocity = ZERO_VECTOR; + matrix_h orientationOverride(&vmd_identity_matrix); + bool orientationOverrideRelative = true; + + if (!ade_get_args(L, "ooo|oob", l_ParticleSource.Get(&ps), l_Vector.Get(&position), l_Matrix.Get(&orientation), l_Vector.Get(&velocity), l_Matrix.Get(&orientationOverride), &orientationOverrideRelative)) + return ade_set_args(L, "b", false); + + particle::ParticleSource* psp = ps.Get(); + if (psp == nullptr) + return ade_set_args(L, "b", false); + + psp->setHost(std::make_unique(position, *orientation.GetMatrix(), velocity, *orientationOverride.GetMatrix(), orientationOverrideRelative)); + psp->finishCreation(); + + return ade_set_args(L, "b", true); +} + +ADE_FUNC(createOnObject, l_ParticleSource, "object object, vector offset, [orientation orientationOverride = identity, boolean orientationOverrideIsRelative = true]", "Actually spawns this particle source, attached to the specified object.", "boolean", "returns true if the source was successfully created, false otherwise") +{ + particle_source_h ps; + object_h objh; + vec3d position; + matrix_h orientationOverride(&vmd_identity_matrix); + bool orientationOverrideRelative = true; + + if (!ade_get_args(L, "ooo|ob", l_ParticleSource.Get(&ps), l_Object.Get(&objh), l_Vector.Get(&position), l_Matrix.Get(&orientationOverride), &orientationOverrideRelative)) + return ade_set_args(L, "b", false); + + if (!objh.isValid()) + return ade_set_args(L, "b", false); + + particle::ParticleSource* psp = ps.Get(); + if (psp == nullptr) + return ade_set_args(L, "b", false); + + psp->setHost(std::make_unique(objh.objp(), position, *orientationOverride.GetMatrix(), orientationOverrideRelative)); + psp->finishCreation(); + + return ade_set_args(L, "b", true); +} + +ADE_FUNC(createOnSubmodel, l_ParticleSource, "object object, submodel submodel, vector offset, [orientation orientationOverride = identity, boolean orientationOverrideIsRelative = true]", "Actually spawns this particle source, attached to the specified submodel.", "boolean", "returns true if the source was successfully created, false otherwise") +{ + particle_source_h ps; + object_h objh; + submodel_h subobjh; + vec3d position; + matrix_h orientationOverride(&vmd_identity_matrix); + bool orientationOverrideRelative = true; + + if (!ade_get_args(L, "oooo|ob", l_ParticleSource.Get(&ps), l_Object.Get(&objh), l_Submodel.Get(&subobjh), l_Vector.Get(&position), l_Matrix.Get(&orientationOverride), &orientationOverrideRelative)) + return ade_set_args(L, "b", false); + + if (!(subobjh.isValid() && objh.isValid())) + return ade_set_args(L, "b", false); + + particle::ParticleSource* psp = ps.Get(); + if (psp == nullptr) + return ade_set_args(L, "b", false); + + psp->setHost(std::make_unique(objh.objp(), subobjh.GetSubmodelIndex(), position, *orientationOverride.GetMatrix(), orientationOverrideRelative)); + psp->finishCreation(); + + return ade_set_args(L, "b", true); +} + +ADE_FUNC(createOnTurret, l_ParticleSource, "object object, submodel submodel, number firepoint, [orientation orientationOverride = identity, boolean orientationOverrideIsRelative = true]", "Actually spawns this particle source, attached to the specified turret firepoint.", "boolean", "returns true if the source was successfully created, false otherwise") +{ + particle_source_h ps; + object_h objh; + submodel_h subobjh; + int firepoint; + matrix_h orientationOverride(&vmd_identity_matrix); + bool orientationOverrideRelative = true; + + if (!ade_get_args(L, "oooi|ob", l_ParticleSource.Get(&ps), l_Object.Get(&objh), l_Submodel.Get(&subobjh), &firepoint, l_Matrix.Get(&orientationOverride), &orientationOverrideRelative)) + return ade_set_args(L, "b", false); + + if (!(subobjh.isValid() && objh.isValid())) + return ade_set_args(L, "b", false); + + particle::ParticleSource* psp = ps.Get(); + if (psp == nullptr) + return ade_set_args(L, "b", false); + + psp->setHost(std::make_unique(objh.objp(), subobjh.GetSubmodelIndex(), firepoint, *orientationOverride.GetMatrix(), orientationOverrideRelative)); + psp->finishCreation(); + + return ade_set_args(L, "b", true); +} + +ADE_FUNC(createOnBeam, l_ParticleSource, "object object, submodel submodel, number firepoint, [orientation orientationOverride = identity, boolean orientationOverrideIsRelative = true]", "Actually spawns this particle source along the length of the beam.", "boolean", "returns true if the source was successfully created, false otherwise") +{ + particle_source_h ps; + object_h objh; + matrix_h orientationOverride(&vmd_identity_matrix); + bool orientationOverrideRelative = true; + + if (!ade_get_args(L, "ooo|ob", l_ParticleSource.Get(&ps), l_Object.Get(&objh), l_Matrix.Get(&orientationOverride), &orientationOverrideRelative)) + return ade_set_args(L, "b", false); + + if (!objh.isValid() || objh.objp()->type != OBJ_BEAM) + return ade_set_args(L, "b", false); + + particle::ParticleSource* psp = ps.Get(); + if (psp == nullptr) + return ade_set_args(L, "b", false); + + psp->setHost(std::make_unique(objh.objp(), *orientationOverride.GetMatrix(), orientationOverrideRelative)); + psp->finishCreation(); + + return ade_set_args(L, "b", true); +} + +ADE_FUNC(createOnParticle, l_ParticleSource, "particle particle, [orientation orientationOverride = identity, boolean orientationOverrideIsRelative = true]", "Actually spawns this particle source, attached to the specified persistent particle.", "boolean", "returns true if the source was successfully created, false otherwise") +{ + particle_source_h ps; + particle_h particle; + matrix_h orientationOverride(&vmd_identity_matrix); + bool orientationOverrideRelative = true; + + if (!ade_get_args(L, "ooo|ob", l_ParticleSource.Get(&ps), l_Particle.Get(&particle), l_Matrix.Get(&orientationOverride), &orientationOverrideRelative)) + return ade_set_args(L, "b", false); + + if (!particle.isValid()) + return ade_set_args(L, "b", false); + + particle::ParticleSource* psp = ps.Get(); + if (psp == nullptr) + return ade_set_args(L, "b", false); + + psp->setHost(std::make_unique(particle.Get(), *orientationOverride.GetMatrix(), orientationOverrideRelative)); + psp->finishCreation(); + + return ade_set_args(L, "b", true); +} + } } diff --git a/code/scripting/api/objs/particle.h b/code/scripting/api/objs/particle.h index 60309d6042..d347e759e5 100644 --- a/code/scripting/api/objs/particle.h +++ b/code/scripting/api/objs/particle.h @@ -2,6 +2,7 @@ #include "scripting/ade_api.h" #include "particle/particle.h" +#include "particle/ParticleSource.h" namespace scripting { namespace api { @@ -22,6 +23,25 @@ class particle_h DECLARE_ADE_OBJ(l_Particle, particle_h); +DECLARE_ADE_OBJ(l_ParticleEffect, particle::ParticleEffectHandle); + +class particle_source_h +{ + protected: + particle::ParticleSource* source = nullptr; + uint32_t sourceValidityCounter = 0; + public: + particle_source_h() = default; + + explicit particle_source_h(particle::ParticleSource* source_p, uint32_t sourceValidityCounter_p); + + particle::ParticleSource* Get() const; + + bool isValid() const; +}; + +DECLARE_ADE_OBJ(l_ParticleSource, particle_source_h); + } }