Files
fs2open.github.com/code/particle/ParticleParse.cpp
T
Goober5000andClaude Opus 4.8 bed228d264 avoid undefined behavior from UNREACHABLE on reachable code paths (#7555)
* avoid undefined behavior from UNREACHABLE on reachable code paths

UNREACHABLE expands to __assume(false) / __builtin_unreachable() in
release builds, which is undefined behavior if the code is ever actually
reached. Many UNREACHABLE sites only guard against unexpected-but-
possible runtime states (values from tables, mission/save files, network
packets, scripting, or user input), so reaching them should degrade
gracefully rather than invoke UB.

Replace UNREACHABLE with Assertion(false, ...) at all such sites. It
still pops an error in debug builds, but in release it is a harmless
no-op that simply falls through to the following code. Sites that are
genuinely unreachable by local invariants (e.g. a switch over a bounded
random number) are left as UNREACHABLE.

A few converted sites had no safe fall-through -- an uninitialized read,
null dereference, or null call would have followed -- so they also get a
minimal recovery: initialize the value, guard the dereference, return
early, or skip the offending item.

Where the converted site was a simple guard of the form
    if (x) { Assertion(false, ...); return; }
hoist the check onto the invariant instead:
    Assertion(!x, ...);
    if (x) { return; }
This states the expectation directly and shows the offending value in
the debug popup, while keeping the same graceful runtime degradation.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* addendum

After consultation with Lafiel, use UNREACHABLE in situations that are effectively impossible even if not provably impossible.  UNREACHABLE = "this shouldn't ever happen"; Assertion = "if this ever happens, it's a coding error".  Also fix a few other sites and change a couple to Warnings.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-09 11:20:25 +09:00

761 lines
26 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);
}
}
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 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);
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"
};
}