Files
BMagnu 856e676e79 Add render path to dispatch Particles as Decals (#7507)
* Add path to dispatch particles as decals

* Testing and bugfixing

* Address feedback

* Fix Typo

* Fix obj attachment
2026-07-17 13:38:45 +09:00

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"
};
}