+particleeffectinfo_t baselineparticleeffectinfo =
+{
+ 0, //int effectnameindex; // which effect this belongs to
+ // PARTICLEEFFECT_* bits
+ 0, //int flags;
+ // blood effects may spawn very few particles, so proper fraction-overflow
+ // handling is very important, this variable keeps track of the fraction
+ 0.0, //double particleaccumulator;
+ // the math is: countabsolute + requestedcount * countmultiplier * quality
+ // absolute number of particles to spawn, often used for decals
+ // (unaffected by quality and requestedcount)
+ 0.0f, //float countabsolute;
+ // multiplier for the number of particles CL_ParticleEffect was told to
+ // spawn, most effects do not really have a count and hence use 1, so
+ // this is often the actual count to spawn, not merely a multiplier
+ 0.0f, //float countmultiplier;
+ // if > 0 this causes the particle to spawn in an evenly spaced line from
+ // originmins to originmaxs (causing them to describe a trail, not a box)
+ 0.0f, //float trailspacing;
+ // type of particle to spawn (defines some aspects of behavior)
+ pt_alphastatic, //ptype_t particletype;
+ // blending mode used on this particle type
+ PBLEND_ALPHA, //pblend_t blendmode;
+ // orientation of this particle type (BILLBOARD, SPARK, BEAM, etc)
+ PARTICLE_BILLBOARD, //porientation_t orientation;
+ // range of colors to choose from in hex RRGGBB (like HTML color tags),
+ // randomly interpolated at spawn
+ {0xFFFFFF, 0xFFFFFF}, //unsigned int color[2];
+ // a random texture is chosen in this range (note the second value is one
+ // past the last choosable, so for example 8,16 chooses any from 8 up and
+ // including 15)
+ // if start and end of the range are the same, no randomization is done
+ {63, 63 /* tex_particle */}, //int tex[2];
+ // range of size values randomly chosen when spawning, plus size increase over time
+ {1, 1, 0.0f}, //float size[3];
+ // range of alpha values randomly chosen when spawning, plus alpha fade
+ {0.0f, 256.0f, 256.0f}, //float alpha[3];
+ // how long the particle should live (note it is also removed if alpha drops to 0)
+ {16777216.0f, 16777216.0f}, //float time[2];
+ // how much gravity affects this particle (negative makes it fly up!)
+ 0.0f, //float gravity;
+ // how much bounce the particle has when it hits a surface
+ // if negative the particle is removed on impact
+ 0.0f, //float bounce;
+ // if in air this friction is applied
+ // if negative the particle accelerates
+ 0.0f, //float airfriction;
+ // if in liquid (water/slime/lava) this friction is applied
+ // if negative the particle accelerates
+ 0.0f, //float liquidfriction;
+ // these offsets are added to the values given to particleeffect(), and
+ // then an ellipsoid-shaped jitter is added as defined by these
+ // (they are the 3 radii)
+ 1.0f, //float stretchfactor;
+ // stretch velocity factor (used for sparks)
+ {0.0f, 0.0f, 0.0f}, //float originoffset[3];
+ {0.0f, 0.0f, 0.0f}, //float relativeoriginoffset[3];
+ {0.0f, 0.0f, 0.0f}, //float velocityoffset[3];
+ {0.0f, 0.0f, 0.0f}, //float relativevelocityoffset[3];
+ {0.0f, 0.0f, 0.0f}, //float originjitter[3];
+ {0.0f, 0.0f, 0.0f}, //float velocityjitter[3];
+ 0.0f, //float velocitymultiplier;
+ // an effect can also spawn a dlight
+ 0.0f, //float lightradiusstart;
+ 0.0f, //float lightradiusfade;
+ 16777216.0f, //float lighttime;
+ {1.0f, 1.0f, 1.0f}, //float lightcolor[3];
+ true, //qboolean lightshadow;
+ 0, //int lightcubemapnum;
+ {1.0f, 0.25f}, //float lightcorona[2];
+ {(unsigned int)-1, (unsigned int)-1}, //unsigned int staincolor[2]; // note: 0x808080 = neutral (particle's own color), these are modding factors for the particle's original color!
+ {-1, -1}, //int staintex[2];
+ {1.0f, 1.0f}, //float stainalpha[2];
+ {2.0f, 2.0f}, //float stainsize[2];
+ // other parameters
+ {0.0f, 360.0f, 0.0f, 0.0f}, //float rotate[4]; // min/max base angle, min/max rotation over time
+};
+
+cvar_t cl_particles = {CVAR_CLIENT | CVAR_SAVE, "cl_particles", "1", "enables particle effects"};
+cvar_t cl_particles_quality = {CVAR_CLIENT | CVAR_SAVE, "cl_particles_quality", "1", "multiplies number of particles"};
+cvar_t cl_particles_alpha = {CVAR_CLIENT | CVAR_SAVE, "cl_particles_alpha", "1", "multiplies opacity of particles"};
+cvar_t cl_particles_size = {CVAR_CLIENT | CVAR_SAVE, "cl_particles_size", "1", "multiplies particle size"};
+cvar_t cl_particles_quake = {CVAR_CLIENT | CVAR_SAVE, "cl_particles_quake", "0", "makes particle effects look mostly like the ones in Quake"};
+cvar_t cl_particles_blood = {CVAR_CLIENT | CVAR_SAVE, "cl_particles_blood", "1", "enables blood effects"};
+cvar_t cl_particles_blood_alpha = {CVAR_CLIENT | CVAR_SAVE, "cl_particles_blood_alpha", "1", "opacity of blood, does not affect decals"};
+cvar_t cl_particles_blood_decal_alpha = {CVAR_CLIENT | CVAR_SAVE, "cl_particles_blood_decal_alpha", "1", "opacity of blood decal"};
+cvar_t cl_particles_blood_decal_scalemin = {CVAR_CLIENT | CVAR_SAVE, "cl_particles_blood_decal_scalemin", "1.5", "minimal random scale of decal"};
+cvar_t cl_particles_blood_decal_scalemax = {CVAR_CLIENT | CVAR_SAVE, "cl_particles_blood_decal_scalemax", "2", "maximal random scale of decal"};
+cvar_t cl_particles_blood_bloodhack = {CVAR_CLIENT | CVAR_SAVE, "cl_particles_blood_bloodhack", "1", "make certain quake particle() calls create blood effects instead"};
+cvar_t cl_particles_bulletimpacts = {CVAR_CLIENT | CVAR_SAVE, "cl_particles_bulletimpacts", "1", "enables bulletimpact effects"};
+cvar_t cl_particles_explosions_sparks = {CVAR_CLIENT | CVAR_SAVE, "cl_particles_explosions_sparks", "1", "enables sparks from explosions"};
+cvar_t cl_particles_explosions_shell = {CVAR_CLIENT | CVAR_SAVE, "cl_particles_explosions_shell", "0", "enables polygonal shell from explosions"};
+cvar_t cl_particles_rain = {CVAR_CLIENT | CVAR_SAVE, "cl_particles_rain", "1", "enables rain effects"};
+cvar_t cl_particles_snow = {CVAR_CLIENT | CVAR_SAVE, "cl_particles_snow", "1", "enables snow effects"};
+cvar_t cl_particles_smoke = {CVAR_CLIENT | CVAR_SAVE, "cl_particles_smoke", "1", "enables smoke (used by multiple effects)"};
+cvar_t cl_particles_smoke_alpha = {CVAR_CLIENT | CVAR_SAVE, "cl_particles_smoke_alpha", "0.5", "smoke brightness"};
+cvar_t cl_particles_smoke_alphafade = {CVAR_CLIENT | CVAR_SAVE, "cl_particles_smoke_alphafade", "0.55", "brightness fade per second"};
+cvar_t cl_particles_sparks = {CVAR_CLIENT | CVAR_SAVE, "cl_particles_sparks", "1", "enables sparks (used by multiple effects)"};
+cvar_t cl_particles_bubbles = {CVAR_CLIENT | CVAR_SAVE, "cl_particles_bubbles", "1", "enables bubbles (used by multiple effects)"};
+cvar_t cl_particles_visculling = {CVAR_CLIENT | CVAR_SAVE, "cl_particles_visculling", "0", "perform a costly check if each particle is visible before drawing"};
+cvar_t cl_particles_collisions = {CVAR_CLIENT | CVAR_SAVE, "cl_particles_collisions", "1", "allow costly collision detection on particles (sparks that bounce, particles not going through walls, blood hitting surfaces, etc)"};
+cvar_t cl_particles_forcetraileffects = {CVAR_CLIENT, "cl_particles_forcetraileffects", "0", "force trails to be displayed even if a non-trail draw primitive was used (debug/compat feature)"};
+cvar_t cl_decals = {CVAR_CLIENT | CVAR_SAVE, "cl_decals", "1", "enables decals (bullet holes, blood, etc)"};
+cvar_t cl_decals_time = {CVAR_CLIENT | CVAR_SAVE, "cl_decals_time", "20", "how long before decals start to fade away"};
+cvar_t cl_decals_fadetime = {CVAR_CLIENT | CVAR_SAVE, "cl_decals_fadetime", "1", "how long decals take to fade away"};
+cvar_t cl_decals_newsystem_intensitymultiplier = {CVAR_CLIENT | CVAR_SAVE, "cl_decals_newsystem_intensitymultiplier", "2", "boosts intensity of decals (because the distance fade can make them hard to see otherwise)"};
+cvar_t cl_decals_newsystem_immediatebloodstain = {CVAR_CLIENT | CVAR_SAVE, "cl_decals_newsystem_immediatebloodstain", "2", "0: no on-spawn blood stains; 1: on-spawn blood stains for pt_blood; 2: always use on-spawn blood stains"};
+cvar_t cl_decals_newsystem_bloodsmears = {CVAR_CLIENT | CVAR_SAVE, "cl_decals_newsystem_bloodsmears", "1", "enable use of particle velocity as decal projection direction rather than surface normal"};
+cvar_t cl_decals_models = {CVAR_CLIENT | CVAR_SAVE, "cl_decals_models", "0", "enables decals on animated models"};
+cvar_t cl_decals_bias = {CVAR_CLIENT | CVAR_SAVE, "cl_decals_bias", "0.125", "distance to bias decals from surface to prevent depth fighting"};
+cvar_t cl_decals_max = {CVAR_CLIENT | CVAR_SAVE, "cl_decals_max", "4096", "maximum number of decals allowed to exist in the world at once"};
+
+
+static void CL_Particles_ParseEffectInfo(const char *textstart, const char *textend, const char *filename)
+{
+ int arrayindex;
+ int argc;
+ int i;
+ int linenumber;
+ particleeffectinfo_t *info = NULL;
+ const char *text = textstart;
+ char argv[16][1024];
+ for (linenumber = 1;;linenumber++)
+ {
+ argc = 0;
+ for (arrayindex = 0;arrayindex < 16;arrayindex++)
+ argv[arrayindex][0] = 0;
+ for (;;)
+ {
+ if (!COM_ParseToken_Simple(&text, true, false, true))
+ return;
+ if (!strcmp(com_token, "\n"))
+ break;
+ if (argc < 16)
+ {
+ strlcpy(argv[argc], com_token, sizeof(argv[argc]));
+ argc++;
+ }
+ }
+ if (argc < 1)
+ continue;
+#define checkparms(n) if (argc != (n)) {Con_Printf("%s:%i: error while parsing: %s given %i parameters, should be %i parameters\n", filename, linenumber, argv[0], argc, (n));break;}
+#define readints(array, n) checkparms(n+1);for (arrayindex = 0;arrayindex < argc - 1;arrayindex++) array[arrayindex] = strtol(argv[1+arrayindex], NULL, 0)
+#define readfloats(array, n) checkparms(n+1);for (arrayindex = 0;arrayindex < argc - 1;arrayindex++) array[arrayindex] = atof(argv[1+arrayindex])
+#define readint(var) checkparms(2);var = strtol(argv[1], NULL, 0)
+#define readfloat(var) checkparms(2);var = atof(argv[1])
+#define readbool(var) checkparms(2);var = strtol(argv[1], NULL, 0) != 0
+ if (!strcmp(argv[0], "effect"))
+ {
+ int effectnameindex;
+ checkparms(2);
+ if (numparticleeffectinfo >= MAX_PARTICLEEFFECTINFO)
+ {
+ Con_Printf("%s:%i: too many effects!\n", filename, linenumber);
+ break;
+ }
+ for (effectnameindex = 1;effectnameindex < MAX_PARTICLEEFFECTNAME;effectnameindex++)
+ {
+ if (particleeffectname[effectnameindex][0])
+ {
+ if (!strcmp(particleeffectname[effectnameindex], argv[1]))
+ break;
+ }
+ else
+ {
+ strlcpy(particleeffectname[effectnameindex], argv[1], sizeof(particleeffectname[effectnameindex]));
+ break;
+ }
+ }
+ // if we run out of names, abort
+ if (effectnameindex == MAX_PARTICLEEFFECTNAME)
+ {
+ Con_Printf("%s:%i: too many effects!\n", filename, linenumber);
+ break;
+ }
+ for(i = 0; i < numparticleeffectinfo; ++i)
+ {
+ info = particleeffectinfo + i;
+ if(!(info->flags & PARTICLEEFFECT_DEFINED))
+ if(info->effectnameindex == effectnameindex)
+ break;
+ }
+ if(i < numparticleeffectinfo)
+ continue;
+ info = particleeffectinfo + numparticleeffectinfo++;
+ // copy entire info from baseline, then fix up the nameindex
+ *info = baselineparticleeffectinfo;
+ info->effectnameindex = effectnameindex;
+ continue;
+ }
+ else if (info == NULL)
+ {
+ Con_Printf("%s:%i: command %s encountered before effect\n", filename, linenumber, argv[0]);
+ break;
+ }
+
+ info->flags |= PARTICLEEFFECT_DEFINED;
+ if (!strcmp(argv[0], "countabsolute")) {readfloat(info->countabsolute);}
+ else if (!strcmp(argv[0], "count")) {readfloat(info->countmultiplier);}
+ else if (!strcmp(argv[0], "type"))
+ {
+ checkparms(2);
+ if (!strcmp(argv[1], "alphastatic")) info->particletype = pt_alphastatic;
+ else if (!strcmp(argv[1], "static")) info->particletype = pt_static;
+ else if (!strcmp(argv[1], "spark")) info->particletype = pt_spark;
+ else if (!strcmp(argv[1], "beam")) info->particletype = pt_beam;
+ else if (!strcmp(argv[1], "rain")) info->particletype = pt_rain;
+ else if (!strcmp(argv[1], "raindecal")) info->particletype = pt_raindecal;
+ else if (!strcmp(argv[1], "snow")) info->particletype = pt_snow;
+ else if (!strcmp(argv[1], "bubble")) info->particletype = pt_bubble;
+ else if (!strcmp(argv[1], "blood")) {info->particletype = pt_blood;info->gravity = 1;}
+ else if (!strcmp(argv[1], "smoke")) info->particletype = pt_smoke;
+ else if (!strcmp(argv[1], "decal")) info->particletype = pt_decal;
+ else if (!strcmp(argv[1], "entityparticle")) info->particletype = pt_entityparticle;
+ else Con_Printf("%s:%i: unrecognized particle type %s\n", filename, linenumber, argv[1]);
+ info->blendmode = particletype[info->particletype].blendmode;
+ info->orientation = particletype[info->particletype].orientation;
+ }
+ else if (!strcmp(argv[0], "blend"))
+ {
+ checkparms(2);
+ if (!strcmp(argv[1], "alpha")) info->blendmode = PBLEND_ALPHA;
+ else if (!strcmp(argv[1], "add")) info->blendmode = PBLEND_ADD;
+ else if (!strcmp(argv[1], "invmod")) info->blendmode = PBLEND_INVMOD;
+ else Con_Printf("%s:%i: unrecognized blendmode %s\n", filename, linenumber, argv[1]);
+ }
+ else if (!strcmp(argv[0], "orientation"))
+ {
+ checkparms(2);
+ if (!strcmp(argv[1], "billboard")) info->orientation = PARTICLE_BILLBOARD;
+ else if (!strcmp(argv[1], "spark")) info->orientation = PARTICLE_SPARK;
+ else if (!strcmp(argv[1], "oriented")) info->orientation = PARTICLE_ORIENTED_DOUBLESIDED;
+ else if (!strcmp(argv[1], "beam")) info->orientation = PARTICLE_HBEAM;
+ else Con_Printf("%s:%i: unrecognized orientation %s\n", filename, linenumber, argv[1]);
+ }
+ else if (!strcmp(argv[0], "color")) {readints(info->color, 2);}
+ else if (!strcmp(argv[0], "tex")) {readints(info->tex, 2);}
+ else if (!strcmp(argv[0], "size")) {readfloats(info->size, 2);}
+ else if (!strcmp(argv[0], "sizeincrease")) {readfloat(info->size[2]);}
+ else if (!strcmp(argv[0], "alpha")) {readfloats(info->alpha, 3);}
+ else if (!strcmp(argv[0], "time")) {readfloats(info->time, 2);}
+ else if (!strcmp(argv[0], "gravity")) {readfloat(info->gravity);}
+ else if (!strcmp(argv[0], "bounce")) {readfloat(info->bounce);}
+ else if (!strcmp(argv[0], "airfriction")) {readfloat(info->airfriction);}
+ else if (!strcmp(argv[0], "liquidfriction")) {readfloat(info->liquidfriction);}
+ else if (!strcmp(argv[0], "originoffset")) {readfloats(info->originoffset, 3);}
+ else if (!strcmp(argv[0], "relativeoriginoffset")) {readfloats(info->relativeoriginoffset, 3);}
+ else if (!strcmp(argv[0], "velocityoffset")) {readfloats(info->velocityoffset, 3);}
+ else if (!strcmp(argv[0], "relativevelocityoffset")) {readfloats(info->relativevelocityoffset, 3);}
+ else if (!strcmp(argv[0], "originjitter")) {readfloats(info->originjitter, 3);}
+ else if (!strcmp(argv[0], "velocityjitter")) {readfloats(info->velocityjitter, 3);}
+ else if (!strcmp(argv[0], "velocitymultiplier")) {readfloat(info->velocitymultiplier);}
+ else if (!strcmp(argv[0], "lightradius")) {readfloat(info->lightradiusstart);}
+ else if (!strcmp(argv[0], "lightradiusfade")) {readfloat(info->lightradiusfade);}
+ else if (!strcmp(argv[0], "lighttime")) {readfloat(info->lighttime);}
+ else if (!strcmp(argv[0], "lightcolor")) {readfloats(info->lightcolor, 3);}
+ else if (!strcmp(argv[0], "lightshadow")) {readbool(info->lightshadow);}
+ else if (!strcmp(argv[0], "lightcubemapnum")) {readint(info->lightcubemapnum);}
+ else if (!strcmp(argv[0], "lightcorona")) {readints(info->lightcorona, 2);}
+ else if (!strcmp(argv[0], "underwater")) {checkparms(1);info->flags |= PARTICLEEFFECT_UNDERWATER;}
+ else if (!strcmp(argv[0], "notunderwater")) {checkparms(1);info->flags |= PARTICLEEFFECT_NOTUNDERWATER;}
+ else if (!strcmp(argv[0], "trailspacing")) {readfloat(info->trailspacing);if (info->trailspacing > 0) info->countmultiplier = 1.0f / info->trailspacing;}
+ else if (!strcmp(argv[0], "stretchfactor")) {readfloat(info->stretchfactor);}
+ else if (!strcmp(argv[0], "staincolor")) {readints(info->staincolor, 2);}
+ else if (!strcmp(argv[0], "stainalpha")) {readfloats(info->stainalpha, 2);}
+ else if (!strcmp(argv[0], "stainsize")) {readfloats(info->stainsize, 2);}
+ else if (!strcmp(argv[0], "staintex")) {readints(info->staintex, 2);}
+ else if (!strcmp(argv[0], "stainless")) {info->staintex[0] = -2; info->staincolor[0] = (unsigned int)-1; info->staincolor[1] = (unsigned int)-1; info->stainalpha[0] = 1; info->stainalpha[1] = 1; info->stainsize[0] = 2; info->stainsize[1] = 2; }
+ else if (!strcmp(argv[0], "rotate")) {readfloats(info->rotate, 4);}
+ else if (!strcmp(argv[0], "forcenearest")) {checkparms(1);info->flags |= PARTICLEEFFECT_FORCENEAREST;}
+ else
+ Con_Printf("%s:%i: skipping unknown command %s\n", filename, linenumber, argv[0]);
+#undef checkparms
+#undef readints
+#undef readfloats
+#undef readint
+#undef readfloat
+ }
+}
+
+int CL_ParticleEffectIndexForName(const char *name)
+{
+ int i;
+ for (i = 1;i < MAX_PARTICLEEFFECTNAME && particleeffectname[i][0];i++)
+ if (!strcmp(particleeffectname[i], name))
+ return i;
+ return 0;
+}
+
+const char *CL_ParticleEffectNameForIndex(int i)
+{
+ if (i < 1 || i >= MAX_PARTICLEEFFECTNAME)
+ return NULL;
+ return particleeffectname[i];
+}
+
+// MUST match effectnameindex_t in client.h
+static const char *standardeffectnames[EFFECT_TOTAL] =
+{
+ "",
+ "TE_GUNSHOT",
+ "TE_GUNSHOTQUAD",
+ "TE_SPIKE",
+ "TE_SPIKEQUAD",
+ "TE_SUPERSPIKE",
+ "TE_SUPERSPIKEQUAD",
+ "TE_WIZSPIKE",
+ "TE_KNIGHTSPIKE",
+ "TE_EXPLOSION",
+ "TE_EXPLOSIONQUAD",
+ "TE_TAREXPLOSION",
+ "TE_TELEPORT",
+ "TE_LAVASPLASH",
+ "TE_SMALLFLASH",
+ "TE_FLAMEJET",
+ "EF_FLAME",
+ "TE_BLOOD",
+ "TE_SPARK",
+ "TE_PLASMABURN",
+ "TE_TEI_G3",
+ "TE_TEI_SMOKE",
+ "TE_TEI_BIGEXPLOSION",
+ "TE_TEI_PLASMAHIT",
+ "EF_STARDUST",
+ "TR_ROCKET",
+ "TR_GRENADE",
+ "TR_BLOOD",
+ "TR_WIZSPIKE",
+ "TR_SLIGHTBLOOD",
+ "TR_KNIGHTSPIKE",
+ "TR_VORESPIKE",
+ "TR_NEHAHRASMOKE",
+ "TR_NEXUIZPLASMA",
+ "TR_GLOWTRAIL",
+ "SVC_PARTICLE"
+};
+
+static void CL_Particles_LoadEffectInfo(const char *customfile)
+{
+ int i;
+ int filepass;
+ unsigned char *filedata;
+ fs_offset_t filesize;
+ char filename[MAX_QPATH];
+ numparticleeffectinfo = 0;
+ memset(particleeffectinfo, 0, sizeof(particleeffectinfo));
+ memset(particleeffectname, 0, sizeof(particleeffectname));
+ for (i = 0;i < EFFECT_TOTAL;i++)
+ strlcpy(particleeffectname[i], standardeffectnames[i], sizeof(particleeffectname[i]));
+ for (filepass = 0;;filepass++)
+ {
+ if (filepass == 0)
+ {
+ if (customfile)
+ strlcpy(filename, customfile, sizeof(filename));
+ else
+ strlcpy(filename, "effectinfo.txt", sizeof(filename));
+ }
+ else if (filepass == 1)
+ {
+ if (!cl.worldbasename[0] || customfile)
+ continue;
+ dpsnprintf(filename, sizeof(filename), "%s_effectinfo.txt", cl.worldnamenoextension);
+ }
+ else
+ break;
+ filedata = FS_LoadFile(filename, tempmempool, true, &filesize);
+ if (!filedata)
+ continue;
+ CL_Particles_ParseEffectInfo((const char *)filedata, (const char *)filedata + filesize, filename);
+ Mem_Free(filedata);
+ }
+}
+
+static void CL_Particles_LoadEffectInfo_f(cmd_state_t *cmd)
+{
+ CL_Particles_LoadEffectInfo(Cmd_Argc(cmd) > 1 ? Cmd_Argv(cmd, 1) : NULL);
+}