mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-07 08:15:59 -10:00
* Add path to dispatch particles as decals * Testing and bugfixing * Address feedback * Fix Typo * Fix obj attachment
795 lines
27 KiB
C++
795 lines
27 KiB
C++
#include "particle/ParticleManager.h"
|
|
#include "particle/ParticleEffect.h"
|
|
#include "particle/volumes/ConeVolume.h"
|
|
#include "particle/volumes/PointVolume.h"
|
|
#include "particle/volumes/RingVolume.h"
|
|
#include "particle/volumes/SpheroidVolume.h"
|
|
|
|
#include <anl.h>
|
|
|
|
#include "volumes/ModelSurfaceVolume.h"
|
|
|
|
namespace particle {
|
|
|
|
//
|
|
// ------------ INDIVIDUAL FIELD PARSERS ------------
|
|
//
|
|
|
|
struct ParticleParse {
|
|
static void parseBitmaps(ParticleEffect &effect) {
|
|
if (internal::required_string_if_new("+Filename:", false)) {
|
|
effect.m_bitmap_list = internal::parseAnimationList(true);
|
|
|
|
if (effect.m_bitmap_list.empty()) {
|
|
error_display(1, "No bitmap defined for particle effect!");
|
|
} else {
|
|
effect.m_bitmap_range = ::util::UniformRange<size_t>(0, effect.m_bitmap_list.size() - 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void parseBitmapReversed(ParticleEffect &effect) {
|
|
if (optional_string("+Animation Reversed:")) {
|
|
stuff_boolean(&effect.m_reverseAnimation);
|
|
}
|
|
}
|
|
|
|
template<bool modern = true> static void parseRadius(ParticleEffect &effect) {
|
|
if (optional_string(modern ? "+Radius:" : "+Size:")) {
|
|
effect.m_radius = ::util::ParsedRandomFloatRange::parseRandomRange();
|
|
} else if (optional_string(modern ? "+Parent Radius Factor:" : "+Parent Size Factor:")) {
|
|
effect.m_radius = ::util::ParsedRandomFloatRange::parseRandomRange();
|
|
effect.m_parentScale = true;
|
|
} else {
|
|
error_display(1, modern ? "Missing +Radius or +Parent Radius Factor" : "Missing +Size or +Parent Size Factor");
|
|
}
|
|
}
|
|
|
|
static void parseLength(ParticleEffect &effect) {
|
|
if (optional_string("+Length:")) {
|
|
effect.m_length = ::util::ParsedRandomFloatRange::parseRandomRange();
|
|
}
|
|
}
|
|
|
|
static void parseLifetime(ParticleEffect &effect) {
|
|
if (optional_string("+Lifetime:")) {
|
|
if (optional_string("<none>")) {
|
|
// Use lifetime of effect
|
|
effect.m_hasLifetime = false;
|
|
} else {
|
|
effect.m_hasLifetime = true;
|
|
effect.m_lifetime = ::util::ParsedRandomFloatRange::parseRandomRange();
|
|
}
|
|
} else if (optional_string("+Parent Lifetime Factor:")) {
|
|
effect.m_hasLifetime = true;
|
|
effect.m_parentLifetime = true;
|
|
effect.m_lifetime = ::util::ParsedRandomFloatRange::parseRandomRange();
|
|
}
|
|
}
|
|
|
|
template<bool modern = true> static void parseRotationType(ParticleEffect &effect) {
|
|
if (optional_string(modern ? "+Bitmap Alignment:" : "+Rotation:")) {
|
|
char buf[NAME_LENGTH];
|
|
stuff_string(buf, F_NAME, NAME_LENGTH);
|
|
if (!stricmp(buf, "DEFAULT")) {
|
|
effect.m_rotation_type = ParticleEffect::RotationType::DEFAULT;
|
|
} else if (!stricmp(buf, "RANDOM")) {
|
|
effect.m_rotation_type = ParticleEffect::RotationType::RANDOM;
|
|
} else if (!stricmp(buf, "SCREEN_ALIGNED") || !stricmp(buf, "SCREEN-ALIGNED") || !stricmp(buf, "SCREEN ALIGNED")) {
|
|
effect.m_rotation_type = ParticleEffect::RotationType::SCREEN_ALIGNED;
|
|
} else {
|
|
// in the future we may want to support additional types, or even a specific angle, but that is TBD
|
|
error_display(0, "Rotation Type %s not supported", buf);
|
|
}
|
|
}
|
|
}
|
|
|
|
template<bool modern = true> static void parseOffset(ParticleEffect& effect) {
|
|
if (optional_string(modern ? "+Position Offset:" : "+Offset:")) {
|
|
stuff_vec3d(&effect.m_manual_offset.emplace());
|
|
}
|
|
|
|
if (optional_string("+Velocity Offset:")) {
|
|
stuff_vec3d(&effect.m_manual_velocity_offset.emplace());
|
|
}
|
|
}
|
|
|
|
static void parseLocalPositionScaling(ParticleEffect& effect) {
|
|
if (optional_string("+Local position scaling:")) {
|
|
effect.m_local_position_scaling = ::util::ParsedRandomFloatRange::parseRandomRange();
|
|
}
|
|
}
|
|
|
|
static void parseParentLocal(ParticleEffect& effect) {
|
|
if (optional_string("+Remain local to parent:")) {
|
|
stuff_boolean(&effect.m_parent_local);
|
|
}
|
|
if (optional_string("+Intransitive parenting:")) {
|
|
stuff_boolean(&effect.m_parent_is_transitive);
|
|
effect.m_parent_is_transitive = !effect.m_parent_is_transitive;
|
|
}
|
|
}
|
|
|
|
template<bool modern = true> static void parseParticleNumber(ParticleEffect &effect) {
|
|
if (internal::required_string_if_new(modern ? "+Particle Count Per Spawn:" : "+Number:", false)) {
|
|
effect.m_particleNum = ::util::ParsedRandomFloatRange::parseRandomRange();
|
|
}
|
|
}
|
|
|
|
static void parseDirection(ParticleEffect &effect) {
|
|
if (optional_string("+Direction:")) {
|
|
SCP_string dirStr;
|
|
stuff_string(dirStr, F_NAME);
|
|
|
|
if (!stricmp(dirStr.c_str(), "Incoming") || !stricmp(dirStr.c_str(), "Aligned")) {
|
|
effect.m_direction = ParticleEffect::ShapeDirection::ALIGNED;
|
|
} else if (!stricmp(dirStr.c_str(), "Normal") || !stricmp(dirStr.c_str(), "HitNormal")) {
|
|
effect.m_direction = ParticleEffect::ShapeDirection::HIT_NORMAL;
|
|
} else if (!stricmp(dirStr.c_str(), "Reflected")) {
|
|
effect.m_direction = ParticleEffect::ShapeDirection::REFLECTED;
|
|
} else if (!stricmp(dirStr.c_str(), "Reverse")) {
|
|
effect.m_direction = ParticleEffect::ShapeDirection::REVERSE;
|
|
} else {
|
|
error_display(0, "Unknown direction name '%s'!", dirStr.c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
static std::shared_ptr<ParticleVolume> parseVolume() {
|
|
|
|
int type = required_string_one_of(5, "Spheroid", "Cone", "Ring", "Point", "ModelSurface"); //... and future volumes
|
|
std::shared_ptr<ParticleVolume> volume;
|
|
|
|
switch (type) {
|
|
case 0:
|
|
required_string("Spheroid"); //Because required string one of, for some reason, does not advance parsing.
|
|
volume = std::make_shared<SpheroidVolume>();
|
|
break;
|
|
case 1:
|
|
required_string("Cone");
|
|
volume = std::make_shared<ConeVolume>();
|
|
break;
|
|
case 2:
|
|
required_string("Ring");
|
|
volume = std::make_shared<RingVolume>();
|
|
break;
|
|
case 3:
|
|
required_string("Point");
|
|
volume = std::make_shared<PointVolume>();
|
|
break;
|
|
case 4:
|
|
required_string("ModelSurface");
|
|
volume = std::make_shared<ModelSurfaceVolume>();
|
|
break;
|
|
default:
|
|
Warning(LOCATION, "Invalid volume type specified!");
|
|
return nullptr;
|
|
}
|
|
volume->parse();
|
|
return volume;
|
|
}
|
|
|
|
template<bool modern = true> static void parseVelocityInherit(ParticleEffect &effect) {
|
|
if (optional_string(modern ? "+Velocity Inherit:" : "+Parent Velocity Factor:")) {
|
|
effect.m_vel_inherit = ::util::ParsedRandomFloatRange::parseRandomRange();
|
|
}
|
|
else if (optional_string("+Absolute Velocity Inherit:")) {
|
|
effect.m_vel_inherit = ::util::ParsedRandomFloatRange::parseRandomRange();
|
|
effect.m_vel_inherit_absolute = true;
|
|
}
|
|
|
|
if (optional_string("+Ignore Velocity Inherit If Parented:")) {
|
|
stuff_boolean(&effect.m_ignore_velocity_inherit_if_has_parent);
|
|
}
|
|
}
|
|
|
|
static void parseVelocityVolume(ParticleEffect &effect) {
|
|
if (optional_string("+Velocity Volume:")) {
|
|
effect.m_velocityVolume = parseVolume();
|
|
}
|
|
}
|
|
|
|
static void parseVelocityNoise(ParticleEffect &effect) {
|
|
if (optional_string("+Velocity Noise:")) {
|
|
SCP_string func;
|
|
stuff_string(func, F_RAW);
|
|
anl::CKernel kernel;
|
|
anl::CExpressionBuilder builder(kernel);
|
|
anl::CInstructionIndex instruction = builder.eval(func);
|
|
effect.m_velocityNoise = std::make_shared<std::pair<anl::CKernel, anl::CInstructionIndex>>(std::move(kernel), std::move(instruction));
|
|
}
|
|
if (optional_string("+Velocity Noise Scale:")) {
|
|
effect.m_velocity_noise_scaling = ::util::ParsedRandomFloatRange::parseRandomRange();
|
|
}
|
|
}
|
|
|
|
template<bool modern = true> static void parseVelocityVolumeScale(ParticleEffect &effect) {
|
|
if constexpr (modern) {
|
|
if (optional_string("+Velocity Volume Scale:")) {
|
|
effect.m_velocity_scaling = ::util::ParsedRandomFloatRange::parseRandomRange();
|
|
}
|
|
}
|
|
else {
|
|
if (internal::required_string_if_new("+Velocity:", false)) {
|
|
effect.m_velocity_scaling = ::util::ParsedRandomFloatRange::parseRandomRange();
|
|
}
|
|
}
|
|
}
|
|
|
|
static void parseVelocityDirectionScale(ParticleEffect &effect) {
|
|
if (optional_string("+Velocity Direction Scale:")) {
|
|
SCP_string dirStr;
|
|
stuff_string(dirStr, F_NAME);
|
|
|
|
if (!stricmp(dirStr.c_str(), "None") || !stricmp(dirStr.c_str(), "Normal")) {
|
|
effect.m_velocity_directional_scaling = ParticleEffect::VelocityScaling::NONE;
|
|
} else if (!stricmp(dirStr.c_str(), "Dot")) {
|
|
effect.m_velocity_directional_scaling = ParticleEffect::VelocityScaling::DOT;
|
|
} else if (!stricmp(dirStr.c_str(), "Inverse Dot")) {
|
|
effect.m_velocity_directional_scaling = ParticleEffect::VelocityScaling::DOT_INVERSE;
|
|
} else {
|
|
error_display(0, "Unknown velocity direction scaling name '%s'!", dirStr.c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
static void parsePositionVolume(ParticleEffect &effect) {
|
|
if (optional_string("+Spawn Position Volume:")) {
|
|
effect.m_spawnVolume = parseVolume();
|
|
}
|
|
}
|
|
|
|
static void parsePositionNoise(ParticleEffect &effect) {
|
|
if (optional_string("+Spawn Position Noise:")) {
|
|
SCP_string func;
|
|
stuff_string(func, F_RAW);
|
|
anl::CKernel kernel;
|
|
anl::CExpressionBuilder builder(kernel);
|
|
anl::CInstructionIndex instruction = builder.eval(func);
|
|
effect.m_spawnNoise = std::make_shared<std::pair<anl::CKernel, anl::CInstructionIndex>>(std::move(kernel), std::move(instruction));
|
|
}
|
|
if (optional_string("+Spawn Position Noise Scale:")) {
|
|
effect.m_position_noise_scaling = ::util::ParsedRandomFloatRange::parseRandomRange();
|
|
}
|
|
}
|
|
|
|
static void parseVelocityInheritFromPosition(ParticleEffect &effect) {
|
|
if (optional_string("+Velocity From Position:")) {
|
|
effect.m_vel_inherit_from_position.emplace(::util::ParsedRandomFloatRange::parseRandomRange());
|
|
}
|
|
else if (optional_string("+Absolute Velocity From Position:")) {
|
|
effect.m_vel_inherit_from_position.emplace(::util::ParsedRandomFloatRange::parseRandomRange());
|
|
effect.m_vel_inherit_from_position_absolute = true;
|
|
}
|
|
}
|
|
|
|
static void parseVelocityInheritFromOrientation(ParticleEffect &effect) {
|
|
if (optional_string("+Velocity From Orientation:")) {
|
|
effect.m_vel_inherit_from_orientation.emplace(::util::ParsedRandomFloatRange::parseRandomRange());
|
|
}
|
|
}
|
|
|
|
template<bool modern = true> static void parseTiming(ParticleEffect &effect) {
|
|
if (optional_string("+Duration:")) {
|
|
if (optional_string("ONETIME")) {
|
|
effect.m_duration = ParticleEffect::Duration::ONETIME;
|
|
}
|
|
else if (optional_string("Always")) {
|
|
effect.m_duration = ParticleEffect::Duration::ALWAYS;
|
|
}
|
|
else {
|
|
effect.m_duration = ParticleEffect::Duration::RANGE;
|
|
effect.m_durationRange = ::util::ParsedRandomFloatRange::parseRandomRange(0.0f);
|
|
}
|
|
}
|
|
|
|
if (optional_string("+Delay:")) {
|
|
effect.m_delayRange = ::util::ParsedRandomFloatRange::parseRandomRange(0.0f);
|
|
}
|
|
|
|
if (optional_string(modern ? "+Spawns per Second:" : "+Effects per second:")) {
|
|
effect.m_particlesPerSecond = ::util::ParsedRandomFloatRange::parseRandomRange();
|
|
if (effect.m_particlesPerSecond.min() < 0.001f) {
|
|
error_display(0, "Invalid effects per second minimum %f. Setting was disabled.", effect.m_particlesPerSecond.min());
|
|
effect.m_particlesPerSecond = ::util::UniformFloatRange(-1.f);
|
|
}
|
|
if (effect.m_particlesPerSecond.max() > 1000.0f) {
|
|
error_display(0, "Effects per second maximum %f is above 1000. Delay between effects will be clamped to 1 millisecond.", effect.m_particlesPerSecond.max());
|
|
}
|
|
}
|
|
}
|
|
|
|
static void parseLightEmissionSettings(ParticleEffect& effect) {
|
|
if (optional_string("$Emits Light:")) {
|
|
ParticleEffect::LightInformation& light = effect.m_light_source.emplace();
|
|
|
|
required_string("+Type:");
|
|
switch(required_string_one_of(4, "Point", "As Particle", "To Last Position", "Cone")) {
|
|
case 0:
|
|
Mp += 5;
|
|
light.light_source_mode = ParticleEffect::LightInformation::LightSourceMode::POINT;
|
|
break;
|
|
case 1:
|
|
Mp += 11;
|
|
light.light_source_mode = ParticleEffect::LightInformation::LightSourceMode::AS_PARTICLE;
|
|
break;
|
|
case 2:
|
|
Mp += 16;
|
|
light.light_source_mode = ParticleEffect::LightInformation::LightSourceMode::TO_LAST_POS;
|
|
break;
|
|
case 3:
|
|
Mp+=4;
|
|
light.light_source_mode = ParticleEffect::LightInformation::LightSourceMode::CONE;
|
|
break;
|
|
}
|
|
|
|
required_string("+Light Radius:");
|
|
stuff_float(&light.light_radius);
|
|
|
|
if (optional_string("+Light Source Radius:")) {
|
|
stuff_float(&light.source_radius);
|
|
}
|
|
|
|
required_string("+Intensity:");
|
|
stuff_float(&light.intensity);
|
|
|
|
required_string("+Color:");
|
|
stuff_float(&light.r);
|
|
stuff_float(&light.g);
|
|
stuff_float(&light.b);
|
|
|
|
if (optional_string("+Cone Angle:")) {
|
|
stuff_float(&light.cone_angle);
|
|
}
|
|
|
|
if (optional_string("+Cone Inner Angle:")) {
|
|
stuff_float(&light.cone_inner_angle);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void parseRenderAsDecal(ParticleEffect &effect) {
|
|
if (optional_string("$Render As Decal:")) {
|
|
stuff_boolean(&effect.m_renderAsDecal);
|
|
|
|
if (effect.m_renderAsDecal) {
|
|
if (!effect.m_parent_local) {
|
|
error_display(0, "Particle must be local to parent for decal rendering. Disabling decal rendering.");
|
|
effect.m_renderAsDecal = false;
|
|
//We still parse the rest to allow parsing to properly complete for the other effects.
|
|
}
|
|
|
|
if (optional_string("+Orientation Mode:")) {
|
|
switch (required_string_one_of(1, "Towards Center")) {
|
|
case 0:
|
|
required_string("Towards Center");
|
|
effect.m_decalOrientationMode = ParticleEffect::DecalOrientationMode::TOWARDS_CENTER;
|
|
break;
|
|
default:
|
|
UNREACHABLE("Unexpected orientation mode");
|
|
}
|
|
}
|
|
|
|
if (optional_string("+Emissive:")) {
|
|
stuff_boolean(&effect.m_decalEmissive);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void parseModularCurvesLifetime(ParticleEffect& effect) {
|
|
effect.m_lifetime_curves.parse("$Particle Lifetime Curve:");
|
|
}
|
|
|
|
static void parseModularCurvesSource(ParticleEffect& effect) {
|
|
effect.m_modular_curves.parse("$Particle Source Curve:");
|
|
}
|
|
|
|
//
|
|
// ------------ MODERN TABLES CODE ------------
|
|
//
|
|
|
|
static void parseModernTrail(ParticleEffect& effect, bool top_layer) {
|
|
if (optional_string("$Trail Effect:")) {
|
|
SCP_string name;
|
|
stuff_string(name, F_NAME);
|
|
|
|
effect.m_particleTrail = ParticleManager::get()->getEffectByName(name);
|
|
}
|
|
else if (top_layer && optional_string("$Trail Inline Effect:")) {
|
|
SCP_string name;
|
|
stuff_string(name, F_NAME);
|
|
|
|
SCP_vector<ParticleEffect> trail {constructModernEffect(name, false)};
|
|
while (optional_string("$Continue Trail Effect:"))
|
|
trail.emplace_back(constructModernEffect(name));
|
|
|
|
effect.m_particleTrail = ParticleManager::get()->addEffect(std::move(trail));
|
|
}
|
|
}
|
|
|
|
static void parseModernDeathEffect(ParticleEffect& effect, bool top_layer) {
|
|
if (optional_string("$Death Effect:")) {
|
|
SCP_string name;
|
|
stuff_string(name, F_NAME);
|
|
|
|
effect.m_deathEffect = ParticleManager::get()->getEffectByName(name);
|
|
}
|
|
else if (top_layer && optional_string("$Death Inline Effect:")) {
|
|
SCP_string name;
|
|
stuff_string(name, F_NAME);
|
|
|
|
SCP_vector<ParticleEffect> death {constructModernEffect(name, false)};
|
|
while (optional_string("$Continue Death Effect:"))
|
|
death.emplace_back(constructModernEffect(name));
|
|
|
|
effect.m_deathEffect = ParticleManager::get()->addEffect(std::move(death));
|
|
}
|
|
}
|
|
|
|
static ParticleEffect constructModernEffect(const SCP_string& name, bool top_layer = true) {
|
|
ParticleEffect effect(name);
|
|
|
|
//Particle Settings
|
|
parseBitmaps(effect);
|
|
parseBitmapReversed(effect);
|
|
parseRotationType(effect);
|
|
parseRadius(effect);
|
|
parseLength(effect);
|
|
parseLifetime(effect);
|
|
parseLocalPositionScaling(effect);
|
|
parseParentLocal(effect);
|
|
parseRenderAsDecal(effect);
|
|
parseLightEmissionSettings(effect);
|
|
|
|
//Spawner Settings
|
|
parseTiming(effect);
|
|
parseParticleNumber(effect);
|
|
parseDirection(effect);
|
|
parseOffset(effect);
|
|
parsePositionVolume(effect);
|
|
parsePositionNoise(effect);
|
|
parseVelocityVolume(effect);
|
|
parseVelocityVolumeScale(effect);
|
|
parseVelocityNoise(effect);
|
|
parseVelocityDirectionScale(effect);
|
|
parseVelocityInheritFromPosition(effect);
|
|
parseVelocityInheritFromOrientation(effect);
|
|
parseVelocityInherit(effect);
|
|
|
|
//Curves
|
|
parseModularCurvesLifetime(effect);
|
|
parseModularCurvesSource(effect);
|
|
|
|
parseModernTrail(effect, top_layer);
|
|
parseModernDeathEffect(effect, top_layer);
|
|
|
|
return effect;
|
|
}
|
|
|
|
//
|
|
// ------------ LEGACY TABLES CODE ------------
|
|
//
|
|
|
|
static void parseSizeLifetimeCurve(ParticleEffect &effect) {
|
|
if (optional_string("+Size over lifetime curve:")) {
|
|
effect.m_lifetime_curves.add_curve("Lifetime", ParticleEffect::ParticleLifetimeCurvesOutput::RADIUS_MULT, modular_curves_entry{curve_parse("")});
|
|
}
|
|
}
|
|
|
|
static void parseVelocityLifetimeCurve(ParticleEffect &effect) {
|
|
if (optional_string("+Velocity scalar over lifetime curve:")) {
|
|
effect.m_lifetime_curves.add_curve("Lifetime", ParticleEffect::ParticleLifetimeCurvesOutput::VELOCITY_MULT, modular_curves_entry{curve_parse("")});
|
|
}
|
|
}
|
|
|
|
static void parseParticleProperties(ParticleEffect &effect) {
|
|
//Emulates parsing in the legacy order, analogous to the old particle properties
|
|
parseBitmaps(effect);
|
|
parseRadius<false>(effect);
|
|
parseLength(effect);
|
|
parseLifetime(effect);
|
|
parseSizeLifetimeCurve(effect);
|
|
parseVelocityLifetimeCurve(effect);
|
|
parseRotationType<false>(effect);
|
|
parseOffset<false>(effect);
|
|
parseParentLocal(effect);
|
|
}
|
|
|
|
enum class ParticleEffectLegacyType: int8_t {
|
|
Invalid = -1,
|
|
Single,
|
|
Composite,
|
|
Cone,
|
|
Sphere,
|
|
Volume,
|
|
|
|
MAX,
|
|
};
|
|
static constexpr size_t legacyEffectTypeNamesMax = static_cast<size_t>(ParticleEffectLegacyType::MAX);
|
|
static std::array<const char*, legacyEffectTypeNamesMax> legacyEffectTypeNames;
|
|
|
|
static ParticleEffectLegacyType parseLegacyEffectType() {
|
|
if(!optional_string("$Type:"))
|
|
return ParticleEffectLegacyType::Invalid;
|
|
|
|
SCP_string type;
|
|
stuff_string(type, F_NAME);
|
|
|
|
int i = string_lookup(type.c_str(), legacyEffectTypeNames, legacyEffectTypeNamesMax, "ParticleEffectLegacyType", true);
|
|
if (i >= 0)
|
|
return static_cast<ParticleEffectLegacyType>(i);
|
|
else
|
|
return ParticleEffectLegacyType::Invalid;
|
|
}
|
|
|
|
static SCP_vector<ParticleEffect> parseLegacyCompositeElement(const SCP_string &name) {
|
|
if (!optional_string("$New Effect")) {
|
|
SCP_string newName;
|
|
stuff_string(newName, F_NAME);
|
|
|
|
auto index = ParticleManager::get()->getEffectByName(newName);
|
|
|
|
if (!index.isValid()) {
|
|
error_display(0, "Unknown particle effect name '%s' encountered!", newName.c_str());
|
|
}
|
|
|
|
auto effect = ParticleManager::get()->getEffect(index);
|
|
if (!effect.empty()) {
|
|
effect.front().m_name = name;
|
|
}
|
|
|
|
return effect;
|
|
}
|
|
|
|
ParticleEffectLegacyType type = parseLegacyEffectType();
|
|
if (type == ParticleEffectLegacyType::Invalid) {
|
|
error_display(0, "Legacy inline particle effect only accepts inline sub-effects with a legacy type!");
|
|
}
|
|
|
|
return constructLegacyEffect(name, type);
|
|
}
|
|
|
|
static ParticleEffectHandle parseLegacyCompositeElementByID(const SCP_string &name) {
|
|
if (!optional_string("$New Effect")) {
|
|
SCP_string newName;
|
|
stuff_string(newName, F_NAME);
|
|
|
|
auto index = ParticleManager::get()->getEffectByName(newName);
|
|
|
|
if (!index.isValid()) {
|
|
error_display(0, "Unknown particle effect name '%s' encountered!", newName.c_str());
|
|
}
|
|
|
|
return index;
|
|
}
|
|
|
|
ParticleEffectLegacyType type = parseLegacyEffectType();
|
|
if (type == ParticleEffectLegacyType::Invalid) {
|
|
error_display(0, "Legacy inline particle effect only accepts inline sub-effects with a legacy type!");
|
|
}
|
|
|
|
return ParticleManager::get()->addEffect(constructLegacyEffect(name, type));
|
|
}
|
|
|
|
static void parseLegacyChance(ParticleEffect& effect) {
|
|
if (optional_string("+Chance:")) {
|
|
float chance;
|
|
stuff_float(&chance);
|
|
if (chance <= 0.0f) {
|
|
error_display(0, "Particle %s tried to set +Chance: %f\nChances below 0 would result in no particles.", effect.m_name.c_str(), chance);
|
|
} else if (chance > 1.0f) {
|
|
error_display(0, "Particle %s tried to set +Chance: %f\nChances above 1 are ignored, please use +Number: (min,max) to spawn multiple particles.", effect.m_name.c_str(), chance);
|
|
chance = 1.0f;
|
|
}
|
|
effect.m_particleChance = chance;
|
|
}
|
|
}
|
|
|
|
static SCP_vector<ParticleEffect> constructLegacyEffect(const SCP_string& name, ParticleEffectLegacyType type) {
|
|
SCP_vector<ParticleEffect> result;
|
|
result.emplace_back(name);
|
|
ParticleEffect& effect = result.back();
|
|
|
|
switch (type) {
|
|
case ParticleEffectLegacyType::Single:
|
|
parseParticleProperties(effect);
|
|
parseVelocityInherit<false>(effect);
|
|
parseTiming<false>(effect);
|
|
break;
|
|
case ParticleEffectLegacyType::Composite: {
|
|
bool first = true;
|
|
result.pop_back();
|
|
while (optional_string("+Child effect:")) {
|
|
const auto& child_effects = parseLegacyCompositeElement(first ? name : "");
|
|
result.insert(result.end(), std::make_move_iterator(child_effects.begin()), std::make_move_iterator(child_effects.end()));
|
|
first = false;
|
|
}
|
|
if (result.empty()) {
|
|
error_display(0, "Composite effect %s must have at least one child effect!", name.c_str());
|
|
}
|
|
break;
|
|
}
|
|
case ParticleEffectLegacyType::Cone: {
|
|
parseParticleProperties(effect);
|
|
effect.m_velocity_directional_scaling = ParticleEffect::VelocityScaling::DOT;
|
|
|
|
if (required_string("+Deviation:")) {
|
|
float deviation;
|
|
stuff_float(&deviation);
|
|
|
|
if (deviation < 0.001f) {
|
|
error_display(0, "A standard deviation of %f is not valid. Must be greater than 0. Defaulting to 1.", deviation);
|
|
deviation = 1.0f;
|
|
}
|
|
|
|
effect.m_velocityVolume = std::make_shared<ConeVolume>(::util::BoundedNormalFloatRange(::util::BoundedNormalDistribution::param_type{ std::normal_distribution<float>::param_type(0.f, fl_radians(deviation)), -PI, PI }), 1.f);
|
|
}
|
|
|
|
parseVelocityVolumeScale<false>(effect);
|
|
parseParticleNumber<false>(effect);
|
|
parseLegacyChance(effect);
|
|
parseDirection(effect);
|
|
|
|
bool saw_deprecated_effect_location = false;
|
|
if (optional_string("+Trail effect:")) {
|
|
// This is the deprecated location since this introduces ambiguities in the parsing process
|
|
effect.m_particleTrail = parseLegacyCompositeElementByID("");
|
|
saw_deprecated_effect_location = true;
|
|
}
|
|
|
|
parseVelocityInherit<false>(effect);
|
|
parseTiming<false>(effect);
|
|
|
|
if (optional_string("+Trail effect:")) {
|
|
// This is the new and correct location. This might create duplicate effects but the warning should be clear
|
|
// enough to avoid that
|
|
if (saw_deprecated_effect_location) {
|
|
error_display(0, "Found two trail effect options! Specifying '+Trail effect:' before '+Duration:' is "
|
|
"deprecated since that can cause issues with conflicting effect options.");
|
|
}
|
|
effect.m_particleTrail = parseLegacyCompositeElementByID("");
|
|
}
|
|
break;
|
|
}
|
|
case ParticleEffectLegacyType::Sphere: {
|
|
parseParticleProperties(effect);
|
|
|
|
effect.m_velocityVolume = std::make_shared<SpheroidVolume>(1.f, 1.f, 1.f);
|
|
|
|
parseVelocityVolumeScale<false>(effect);
|
|
parseParticleNumber<false>(effect);
|
|
parseLegacyChance(effect);
|
|
parseDirection(effect);
|
|
|
|
bool saw_deprecated_effect_location = false;
|
|
if (optional_string("+Trail effect:")) {
|
|
// This is the deprecated location since this introduces ambiguities in the parsing process
|
|
effect.m_particleTrail = parseLegacyCompositeElementByID("");
|
|
saw_deprecated_effect_location = true;
|
|
}
|
|
|
|
parseVelocityInherit<false>(effect);
|
|
parseTiming<false>(effect);
|
|
|
|
if (optional_string("+Trail effect:")) {
|
|
// This is the new and correct location. This might create duplicate effects but the warning should be clear
|
|
// enough to avoid that
|
|
if (saw_deprecated_effect_location) {
|
|
error_display(0, "Found two trail effect options! Specifying '+Trail effect:' before '+Duration:' is "
|
|
"deprecated since that can cause issues with conflicting effect options.");
|
|
}
|
|
effect.m_particleTrail = parseLegacyCompositeElementByID("");
|
|
}
|
|
break;
|
|
}
|
|
case ParticleEffectLegacyType::Volume: {
|
|
parseParticleProperties(effect);
|
|
|
|
effect.m_vel_inherit_from_position_absolute = true;
|
|
//This is, unfortunately, not semantically identical to the other velocity, so this is handled manually
|
|
if (required_string("+Velocity:")) {
|
|
effect.m_vel_inherit_from_position.emplace(::util::ParsedRandomFloatRange::parseRandomRange());
|
|
}
|
|
|
|
parseParticleNumber<false>(effect);
|
|
parseLegacyChance(effect);
|
|
|
|
float radius = 10.f;
|
|
if (required_string("+Volume radius:")) {
|
|
stuff_float(&radius);
|
|
|
|
if (radius < 0.001f) {
|
|
error_display(0, "A volume radius of %f is not valid. Must be greater than 0. Defaulting to 10.", radius);
|
|
radius = 10.0f;
|
|
}
|
|
}
|
|
|
|
float bias = 1.f;
|
|
if (optional_string("+Bias:")) {
|
|
stuff_float(&bias);
|
|
|
|
if (bias < 0.001f) {
|
|
error_display(0, "A volume bias value of %f is not valid. Must be greater than 0.", bias);
|
|
bias = 1.0f;
|
|
}
|
|
}
|
|
|
|
float stretch = 1.f;
|
|
if (optional_string("+Stretch:")) {
|
|
stuff_float(&stretch);
|
|
|
|
if (stretch < 0.001f) {
|
|
error_display(0, "A volume stretch value of %f is not valid. Must be greater than 0.", stretch);
|
|
stretch = 1.0f;
|
|
}
|
|
}
|
|
|
|
effect.m_spawnVolume = std::make_shared<SpheroidVolume>(bias, stretch, radius);
|
|
|
|
parseVelocityInherit<false>(effect);
|
|
parseTiming<false>(effect);
|
|
break;
|
|
}
|
|
default: {
|
|
Error(LOCATION, "Unimplemented effect type %d encountered! Get a coder!", static_cast<int>(type));
|
|
throw std::runtime_error("Unimplemented effect type encountered!");
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
//
|
|
// ------------ MAIN CALLBACK ------------
|
|
//
|
|
|
|
static void parseCallback(const char* fileName) {
|
|
using namespace particle;
|
|
|
|
try {
|
|
read_file_text(fileName, CF_TYPE_TABLES);
|
|
|
|
reset_parse();
|
|
|
|
required_string("#Particle Effects");
|
|
|
|
while (optional_string("$Effect:")) {
|
|
SCP_string name;
|
|
stuff_string(name, F_NAME);
|
|
|
|
auto type = parseLegacyEffectType();
|
|
|
|
if (type == ParticleEffectLegacyType::Invalid) {
|
|
SCP_vector<ParticleEffect> effect {constructModernEffect(name)};
|
|
while (optional_string("$Continue Effect:"))
|
|
effect.emplace_back(constructModernEffect(name));
|
|
ParticleManager::get()->addEffect(std::move(effect));
|
|
}
|
|
else
|
|
ParticleManager::get()->addEffect(constructLegacyEffect(name, type));
|
|
}
|
|
|
|
required_string("#End");
|
|
}
|
|
catch (const parse::ParseException& e)
|
|
{
|
|
mprintf(("TABLES: Unable to parse '%s'! Error message = %s.\n", fileName, e.what()));
|
|
return;
|
|
}
|
|
}
|
|
};
|
|
|
|
void ParticleManager::parseConfigFiles() {
|
|
parse_modular_table("*-part.tbm", ParticleParse::parseCallback);
|
|
}
|
|
|
|
std::array<const char*, ParticleParse::legacyEffectTypeNamesMax> ParticleParse::legacyEffectTypeNames = {
|
|
"Single",
|
|
"Composite",
|
|
"Cone",
|
|
"Sphere",
|
|
"Volume"
|
|
};
|
|
}
|