]> de.git.xonotic.org Git - xonotic/darkplaces.git/blobdiff - cl_particles.c
added bufferobject and bufferoffset parameters to all R_Mesh_*Pointer functions and...
[xonotic/darkplaces.git] / cl_particles.c
index 59586171f049a1d8d66e496598dd7dc557bb4751..fb1c128284e49a56ccf3788955171b7f452eee07 100644 (file)
@@ -22,6 +22,7 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 
 #include "cl_collision.h"
 #include "image.h"
+#include "r_shadow.h"
 
 // must match ptype_t values
 particletype_t particletype[pt_total] =
@@ -40,6 +41,80 @@ particletype_t particletype[pt_total] =
        {PBLEND_ALPHA, PARTICLE_BILLBOARD, false}, //pt_entityparticle
 };
 
+#define PARTICLEEFFECT_UNDERWATER 1
+#define PARTICLEEFFECT_NOTUNDERWATER 2
+
+typedef struct particleeffectinfo_s
+{
+       int effectnameindex; // which effect this belongs to
+       // PARTICLEEFFECT_* bits
+       int flags;
+       // blood effects may spawn very few particles, so proper fraction-overflow
+       // handling is very important, this variable keeps track of the fraction
+       double particleaccumulator;
+       // the math is: countabsolute + requestedcount * countmultiplier * quality
+       // absolute number of particles to spawn, often used for decals
+       // (unaffected by quality and requestedcount)
+       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
+       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)
+       float trailspacing;
+       // type of particle to spawn (defines some aspects of behavior)
+       ptype_t particletype;
+       // range of colors to choose from in hex RRGGBB (like HTML color tags),
+       // randomly interpolated at spawn
+       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
+       int tex[2];
+       // range of size values randomly chosen when spawning, plus size increase over time
+       float size[3];
+       // range of alpha values randomly chosen when spawning, plus alpha fade
+       float alpha[3];
+       // how long the particle should live (note it is also removed if alpha drops to 0)
+       float time[2];
+       // how much gravity affects this particle (negative makes it fly up!)
+       float gravity;
+       // how much bounce the particle has when it hits a surface
+       // if negative the particle is removed on impact
+       float bounce;
+       // if in air this friction is applied
+       // if negative the particle accelerates
+       float airfriction;
+       // if in liquid (water/slime/lava) this friction is applied
+       // if negative the particle accelerates
+       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)
+       float originoffset[3];
+       float velocityoffset[3];
+       float originjitter[3];
+       float velocityjitter[3];
+       float velocitymultiplier;
+       // an effect can also spawn a dlight
+       float lightradiusstart;
+       float lightradiusfade;
+       float lighttime;
+       float lightcolor[3];
+       qboolean lightshadow;
+       int lightcubemapnum;
+}
+particleeffectinfo_t;
+
+#define MAX_PARTICLEEFFECTNAME 256
+char particleeffectname[MAX_PARTICLEEFFECTNAME][64];
+
+#define MAX_PARTICLEEFFECTINFO 4096
+
+particleeffectinfo_t particleeffectinfo[MAX_PARTICLEEFFECTINFO];
+
 static int particlepalette[256] =
 {
        0x000000,0x0f0f0f,0x1f1f1f,0x2f2f2f,0x3f3f3f,0x4b4b4b,0x5b5b5b,0x6b6b6b, // 0-7
@@ -87,7 +162,7 @@ static const int tex_smoke[8] = {0, 1, 2, 3, 4, 5, 6, 7};
 static const int tex_bulletdecal[8] = {8, 9, 10, 11, 12, 13, 14, 15};
 static const int tex_blooddecal[8] = {16, 17, 18, 19, 20, 21, 22, 23};
 static const int tex_bloodparticle[8] = {24, 25, 26, 27, 28, 29, 30, 31};
-static const int tex_rainsplash[16] = {32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47};
+static const int tex_rainsplash = 32;
 static const int tex_particle = 63;
 static const int tex_bubble = 62;
 static const int tex_raindrop = 61;
@@ -97,15 +172,15 @@ cvar_t cl_particles = {CVAR_SAVE, "cl_particles", "1", "enables particle effects
 cvar_t cl_particles_quality = {CVAR_SAVE, "cl_particles_quality", "1", "multiplies number of particles and reduces their alpha"};
 cvar_t cl_particles_size = {CVAR_SAVE, "cl_particles_size", "1", "multiplies particle size"};
 cvar_t cl_particles_quake = {CVAR_SAVE, "cl_particles_quake", "0", "makes particle effects look mostly like the ones in Quake"};
-cvar_t cl_particles_bloodshowers = {CVAR_SAVE, "cl_particles_bloodshowers", "1", "enables blood shower effects"};
 cvar_t cl_particles_blood = {CVAR_SAVE, "cl_particles_blood", "1", "enables blood effects"};
 cvar_t cl_particles_blood_alpha = {CVAR_SAVE, "cl_particles_blood_alpha", "0.5", "opacity of blood"};
 cvar_t cl_particles_blood_bloodhack = {CVAR_SAVE, "cl_particles_blood_bloodhack", "1", "make certain quake particle() calls create blood effects instead"};
 cvar_t cl_particles_bulletimpacts = {CVAR_SAVE, "cl_particles_bulletimpacts", "1", "enables bulletimpact effects"};
-cvar_t cl_particles_explosions_bubbles = {CVAR_SAVE, "cl_particles_explosions_bubbles", "1", "enables bubbles from underwater explosions"};
 cvar_t cl_particles_explosions_smoke = {CVAR_SAVE, "cl_particles_explosions_smokes", "0", "enables smoke from explosions"};
 cvar_t cl_particles_explosions_sparks = {CVAR_SAVE, "cl_particles_explosions_sparks", "1", "enables sparks from explosions"};
 cvar_t cl_particles_explosions_shell = {CVAR_SAVE, "cl_particles_explosions_shell", "0", "enables polygonal shell from explosions"};
+cvar_t cl_particles_rain = {CVAR_SAVE, "cl_particles_rain", "1", "enables rain effects"};
+cvar_t cl_particles_snow = {CVAR_SAVE, "cl_particles_snow", "1", "enables snow effects"};
 cvar_t cl_particles_smoke = {CVAR_SAVE, "cl_particles_smoke", "1", "enables smoke (used by multiple effects)"};
 cvar_t cl_particles_smoke_alpha = {CVAR_SAVE, "cl_particles_smoke_alpha", "0.5", "smoke brightness"};
 cvar_t cl_particles_smoke_alphafade = {CVAR_SAVE, "cl_particles_smoke_alphafade", "0.55", "brightness fade per second"};
@@ -115,6 +190,220 @@ cvar_t cl_decals = {CVAR_SAVE, "cl_decals", "0", "enables decals (bullet holes,
 cvar_t cl_decals_time = {CVAR_SAVE, "cl_decals_time", "0", "how long before decals start to fade away"};
 cvar_t cl_decals_fadetime = {CVAR_SAVE, "cl_decals_fadetime", "20", "how long decals take to fade away"};
 
+
+void CL_Particles_ParseEffectInfo(const char *textstart, const char *textend)
+{
+       int arrayindex;
+       int argc;
+       int effectinfoindex;
+       int linenumber;
+       particleeffectinfo_t *info = NULL;
+       const char *text = textstart;
+       char argv[16][1024];
+       effectinfoindex = -1;
+       for (linenumber = 1;;linenumber++)
+       {
+               argc = 0;
+               for (arrayindex = 0;arrayindex < 16;arrayindex++)
+                       argv[arrayindex][0] = 0;
+               for (;;)
+               {
+                       if (!COM_ParseToken(&text, 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("effectinfo.txt:%i: error while parsing: %s given %i parameters, should be %i parameters\n", 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])
+               if (!strcmp(argv[0], "effect"))
+               {
+                       int effectnameindex;
+                       checkparms(2);
+                       effectinfoindex++;
+                       if (effectinfoindex >= MAX_PARTICLEEFFECTINFO)
+                       {
+                               Con_Printf("effectinfo.txt:%i: too many effects!\n", 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("effectinfo.txt:%i: too many effects!\n", linenumber);
+                               break;
+                       }
+                       info = particleeffectinfo + effectinfoindex;
+                       info->effectnameindex = effectnameindex;
+                       info->particletype = pt_alphastatic;
+                       info->tex[0] = tex_particle;
+                       info->tex[1] = tex_particle;
+                       info->color[0] = 0xFFFFFF;
+                       info->color[1] = 0xFFFFFF;
+                       info->size[0] = 1;
+                       info->size[1] = 1;
+                       info->alpha[0] = 0;
+                       info->alpha[1] = 256;
+                       info->alpha[2] = 256;
+                       info->time[0] = 9999;
+                       info->time[1] = 9999;
+                       VectorSet(info->lightcolor, 1, 1, 1);
+                       info->lightshadow = true;
+                       info->lighttime = 9999;
+               }
+               else if (info == NULL)
+               {
+                       Con_Printf("effectinfo.txt:%i: command %s encountered before effect\n", linenumber, argv[0]);
+                       break;
+               }
+               else 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;
+                       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("effectinfo.txt:%i: unrecognized particle type %s\n", 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")) {readints(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], "velocityoffset")) {readfloats(info->velocityoffset, 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")) {readint(info->lightshadow);}
+               else if (!strcmp(argv[0], "lightcubemapnum")) {readint(info->lightcubemapnum);}
+               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
+                       Con_Printf("effectinfo.txt:%i: skipping unknown command %s\n", 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"
+};
+
+void CL_Particles_LoadEffectInfo(void)
+{
+       int i;
+       unsigned char *filedata;
+       fs_offset_t filesize;
+       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]));
+       filedata = FS_LoadFile("effectinfo.txt", tempmempool, true, &filesize);
+       if (filedata)
+       {
+               CL_Particles_ParseEffectInfo((const char *)filedata, (const char *)filedata + filesize);
+               Mem_Free(filedata);
+       }
+};
+
 /*
 ===============
 CL_InitParticles
@@ -124,20 +413,21 @@ void CL_ReadPointFile_f (void);
 void CL_Particles_Init (void)
 {
        Cmd_AddCommand ("pointfile", CL_ReadPointFile_f, "display point file produced by qbsp when a leak was detected in the map (a line leading through the leak hole, to an entity inside the level)");
+       Cmd_AddCommand ("cl_particles_reloadeffects", CL_Particles_LoadEffectInfo, "reloads effectinfo.txt");
 
        Cvar_RegisterVariable (&cl_particles);
        Cvar_RegisterVariable (&cl_particles_quality);
        Cvar_RegisterVariable (&cl_particles_size);
        Cvar_RegisterVariable (&cl_particles_quake);
-       Cvar_RegisterVariable (&cl_particles_bloodshowers);
        Cvar_RegisterVariable (&cl_particles_blood);
        Cvar_RegisterVariable (&cl_particles_blood_alpha);
        Cvar_RegisterVariable (&cl_particles_blood_bloodhack);
-       Cvar_RegisterVariable (&cl_particles_explosions_bubbles);
        Cvar_RegisterVariable (&cl_particles_explosions_smoke);
        Cvar_RegisterVariable (&cl_particles_explosions_sparks);
        Cvar_RegisterVariable (&cl_particles_explosions_shell);
        Cvar_RegisterVariable (&cl_particles_bulletimpacts);
+       Cvar_RegisterVariable (&cl_particles_rain);
+       Cvar_RegisterVariable (&cl_particles_snow);
        Cvar_RegisterVariable (&cl_particles_smoke);
        Cvar_RegisterVariable (&cl_particles_smoke_alpha);
        Cvar_RegisterVariable (&cl_particles_smoke_alphafade);
@@ -165,7 +455,7 @@ void CL_Particles_Shutdown (void)
 // px,py,pz - starting origin of particle
 // pvx,pvy,pvz - starting velocity of particle
 // pfriction - how much the particle slows down per second (0-1 typically, can slowdown faster than 1)
-particle_t *particle(particletype_t *ptype, int pcolor1, int pcolor2, int ptex, float psize, float palpha, float palphafade, float pgravity, float pbounce, float px, float py, float pz, float pvx, float pvy, float pvz, float pfriction, float originjitter, float velocityjitter)
+static particle_t *particle(particletype_t *ptype, int pcolor1, int pcolor2, int ptex, float psize, float psizeincrease, float palpha, float palphafade, float pgravity, float pbounce, float px, float py, float pz, float pvx, float pvy, float pvz, float pairfriction, float pliquidfriction, float originjitter, float velocityjitter)
 {
        int l1, l2;
        particle_t *part;
@@ -186,6 +476,7 @@ particle_t *particle(particletype_t *ptype, int pcolor1, int pcolor2, int ptex,
        part->color[3] = 0xFF;
        part->texnum = ptex;
        part->size = psize;
+       part->sizeincrease = psizeincrease;
        part->alpha = palpha;
        part->alphafade = palphafade;
        part->gravity = pgravity;
@@ -198,7 +489,8 @@ particle_t *particle(particletype_t *ptype, int pcolor1, int pcolor2, int ptex,
        part->vel[1] = pvy + velocityjitter * v[1];
        part->vel[2] = pvz + velocityjitter * v[2];
        part->time2 = 0;
-       part->friction = pfriction;
+       part->airfriction = pairfriction;
+       part->liquidfriction = pliquidfriction;
        return part;
 }
 
@@ -207,15 +499,17 @@ void CL_SpawnDecalParticleForSurface(int hitent, const vec3_t org, const vec3_t
        particle_t *p;
        if (!cl_decals.integer)
                return;
-       p = particle(particletype + pt_decal, color1, color2, texnum, size, alpha, 0, 0, 0, org[0] + normal[0], org[1] + normal[1], org[2] + normal[2], normal[0], normal[1], normal[2], 0, 0, 0);
+       p = particle(particletype + pt_decal, color1, color2, texnum, size, 0, alpha, 0, 0, 0, org[0] + normal[0], org[1] + normal[1], org[2] + normal[2], normal[0], normal[1], normal[2], 0, 0, 0, 0);
        if (p)
        {
                p->time2 = cl.time;
                p->owner = hitent;
                p->ownermodel = cl.entities[p->owner].render.model;
+               VectorAdd(org, normal, p->org);
+               VectorCopy(normal, p->vel);
+               // these relative things are only used to regenerate p->org and p->vel if p->owner is not world (0)
                Matrix4x4_Transform(&cl.entities[p->owner].render.inversematrix, org, p->relativeorigin);
                Matrix4x4_Transform3x3(&cl.entities[p->owner].render.inversematrix, normal, p->relativedirection);
-               VectorAdd(p->relativeorigin, p->relativedirection, p->relativeorigin);
        }
 }
 
@@ -231,7 +525,7 @@ void CL_SpawnDecalParticleForPoint(const vec3_t org, float maxdist, float size,
        {
                VectorRandom(org2);
                VectorMA(org, maxdist, org2, org2);
-               trace = CL_TraceBox(org, vec3_origin, vec3_origin, org2, true, &hitent, SUPERCONTENTS_SOLID | SUPERCONTENTS_SKY, false);
+               trace = CL_Move(org, vec3_origin, vec3_origin, org2, MOVE_NOMONSTERS, NULL, SUPERCONTENTS_SOLID | SUPERCONTENTS_SKY, true, false, &hitent, false);
                // take the closest trace result that doesn't end up hitting a NOMARKS
                // surface (sky for example)
                if (bestfrac > trace.fraction && !(trace.hitq3surfaceflags & Q3SURFACEFLAG_NOMARKS))
@@ -246,910 +540,960 @@ void CL_SpawnDecalParticleForPoint(const vec3_t org, float maxdist, float size,
                CL_SpawnDecalParticleForSurface(besthitent, bestorg, bestnormal, color1, color2, texnum, size, alpha);
 }
 
-/*
-===============
-CL_EntityParticles
-===============
-*/
-void CL_EntityParticles (entity_t *ent)
+static void CL_Sparks(const vec3_t originmins, const vec3_t originmaxs, const vec3_t velocitymins, const vec3_t velocitymaxs, float sparkcount);
+static void CL_Smoke(const vec3_t originmins, const vec3_t originmaxs, const vec3_t velocitymins, const vec3_t velocitymaxs, float smokecount);
+void CL_ParticleEffect_Fallback(int effectnameindex, float count, const vec3_t originmins, const vec3_t originmaxs, const vec3_t velocitymins, const vec3_t velocitymaxs, entity_t *ent, int palettecolor, qboolean spawndlight, qboolean spawnparticles)
 {
-       int i;
-       float pitch, yaw, dist = 64, beamlength = 16, org[3], v[3];
-       static vec3_t avelocities[NUMVERTEXNORMALS];
-       if (!cl_particles.integer) return;
-
-       Matrix4x4_OriginFromMatrix(&ent->render.matrix, org);
-
-       if (!avelocities[0][0])
-               for (i = 0;i < NUMVERTEXNORMALS * 3;i++)
-                       avelocities[0][i] = lhrandom(0, 2.55);
-
-       for (i = 0;i < NUMVERTEXNORMALS;i++)
+       vec3_t center;
+       matrix4x4_t tempmatrix;
+       VectorLerp(originmins, 0.5, originmaxs, center);
+       Matrix4x4_CreateTranslate(&tempmatrix, center[0], center[1], center[2]);
+       if (effectnameindex == EFFECT_SVC_PARTICLE)
        {
-               yaw = cl.time * avelocities[i][0];
-               pitch = cl.time * avelocities[i][1];
-               v[0] = org[0] + m_bytenormals[i][0] * dist + (cos(pitch)*cos(yaw)) * beamlength;
-               v[1] = org[1] + m_bytenormals[i][1] * dist + (cos(pitch)*sin(yaw)) * beamlength;
-               v[2] = org[2] + m_bytenormals[i][2] * dist + (-sin(pitch)) * beamlength;
-               particle(particletype + pt_entityparticle, particlepalette[0x6f], particlepalette[0x6f], tex_particle, 1, 255, 0, 0, 0, v[0], v[1], v[2], 0, 0, 0, 0, 0, 0);
+               if (cl_particles.integer)
+               {
+                       // bloodhack checks if this effect's color matches regular or lightning blood and if so spawns a blood effect instead
+                       if (count == 1024)
+                               CL_ParticleExplosion(center);
+                       else if (cl_particles_blood_bloodhack.integer && !cl_particles_quake.integer && (palettecolor == 73 || palettecolor == 225))
+                               CL_ParticleEffect(EFFECT_TE_BLOOD, count / 6.0f, originmins, originmaxs, velocitymins, velocitymaxs, NULL, 0);
+                       else
+                       {
+                               count *= cl_particles_quality.value;
+                               for (;count > 0;count--)
+                               {
+                                       int k = particlepalette[palettecolor + (rand()&7)];
+                                       if (cl_particles_quake.integer)
+                                               particle(particletype + pt_alphastatic, k, k, tex_particle, 1.5, 0, lhrandom(51, 255), 512, 0.05, 0, lhrandom(originmins[0], originmaxs[0]), lhrandom(originmins[1], originmaxs[1]), lhrandom(originmins[2], originmaxs[2]), lhrandom(velocitymins[0], velocitymaxs[0]), lhrandom(velocitymins[1], velocitymaxs[1]), lhrandom(velocitymins[2], velocitymaxs[2]), 0, 0, 8, 0);
+                                       else if (gamemode == GAME_GOODVSBAD2)
+                                               particle(particletype + pt_alphastatic, k, k, tex_particle, 5, 0, 255, 300, 0, 0, lhrandom(originmins[0], originmaxs[0]), lhrandom(originmins[1], originmaxs[1]), lhrandom(originmins[2], originmaxs[2]), lhrandom(velocitymins[0], velocitymaxs[0]), lhrandom(velocitymins[1], velocitymaxs[1]), lhrandom(velocitymins[2], velocitymaxs[2]), 0, 0, 8, 10);
+                                       else
+                                               particle(particletype + pt_alphastatic, k, k, tex_particle, 1.5, 0, 255, 512, 0, 0, lhrandom(originmins[0], originmaxs[0]), lhrandom(originmins[1], originmaxs[1]), lhrandom(originmins[2], originmaxs[2]), lhrandom(velocitymins[0], velocitymaxs[0]), lhrandom(velocitymins[1], velocitymaxs[1]), lhrandom(velocitymins[2], velocitymaxs[2]), 0, 0, 8, 15);
+                               }
+                       }
+               }
        }
-}
-
-
-void CL_ReadPointFile_f (void)
-{
-       vec3_t org, leakorg;
-       int r, c, s;
-       char *pointfile = NULL, *pointfilepos, *t, tchar;
-       char name[MAX_OSPATH];
-
-       if (!cl.worldmodel)
-               return;
-
-       FS_StripExtension (cl.worldmodel->name, name, sizeof (name));
-       strlcat (name, ".pts", sizeof (name));
-       pointfile = (char *)FS_LoadFile(name, tempmempool, true, NULL);
-       if (!pointfile)
+       else if (effectnameindex == EFFECT_TE_WIZSPIKE)
+               CL_ParticleEffect(EFFECT_SVC_PARTICLE, 30*count, originmins, originmaxs, velocitymins, velocitymaxs, NULL, 20);
+       else if (effectnameindex == EFFECT_TE_KNIGHTSPIKE)
+               CL_ParticleEffect(EFFECT_SVC_PARTICLE, 20*count, originmins, originmaxs, velocitymins, velocitymaxs, NULL, 226);
+       else if (effectnameindex == EFFECT_TE_SPIKE)
        {
-               Con_Printf("Could not open %s\n", name);
-               return;
+               if (cl_particles_bulletimpacts.integer)
+               {
+                       if (cl_particles_quake.integer)
+                       {
+                               if (cl_particles_smoke.integer)
+                                       CL_ParticleEffect(EFFECT_SVC_PARTICLE, 10*count, originmins, originmaxs, velocitymins, velocitymaxs, NULL, 0);
+                       }
+                       else
+                       {
+                               CL_Smoke(originmins, originmaxs, velocitymins, velocitymaxs, 4*count);
+                               CL_Sparks(originmins, originmaxs, velocitymins, velocitymaxs, 15*count);
+                       }
+               }
+               // bullet hole
+               if (cl_stainmaps.integer) R_Stain(center, 32, 96, 96, 96, 24, 128, 128, 128, 24);
+               CL_SpawnDecalParticleForPoint(center, 6, 3, 255, tex_bulletdecal[rand()&7], 0xFFFFFF, 0xFFFFFF);
        }
-
-       Con_Printf("Reading %s...\n", name);
-       VectorClear(leakorg);
-       c = 0;
-       s = 0;
-       pointfilepos = pointfile;
-       while (*pointfilepos)
+       else if (effectnameindex == EFFECT_TE_SPIKEQUAD)
        {
-               while (*pointfilepos == '\n' || *pointfilepos == '\r')
-                       pointfilepos++;
-               if (!*pointfilepos)
-                       break;
-               t = pointfilepos;
-               while (*t && *t != '\n' && *t != '\r')
-                       t++;
-               tchar = *t;
-               *t = 0;
-               r = sscanf (pointfilepos,"%f %f %f", &org[0], &org[1], &org[2]);
-               *t = tchar;
-               pointfilepos = t;
-               if (r != 3)
-                       break;
-               if (c == 0)
-                       VectorCopy(org, leakorg);
-               c++;
-
-               if (cl.num_particles < cl.max_particles - 3)
+               if (cl_particles_bulletimpacts.integer)
                {
-                       s++;
-                       particle(particletype + pt_static, particlepalette[(-c)&15], particlepalette[(-c)&15], tex_particle, 2, 255, 0, 0, 0, org[0], org[1], org[2], 0, 0, 0, 0, 0, 0);
+                       if (cl_particles_quake.integer)
+                       {
+                               if (cl_particles_smoke.integer)
+                                       CL_ParticleEffect(EFFECT_SVC_PARTICLE, 10*count, originmins, originmaxs, velocitymins, velocitymaxs, NULL, 0);
+                       }
+                       else
+                       {
+                               CL_Smoke(originmins, originmaxs, velocitymins, velocitymaxs, 4*count);
+                               CL_Sparks(originmins, originmaxs, velocitymins, velocitymaxs, 15*count);
+                       }
                }
+               // bullet hole
+               if (cl_stainmaps.integer) R_Stain(center, 32, 96, 96, 96, 24, 128, 128, 128, 24);
+               CL_SpawnDecalParticleForPoint(center, 6, 3, 255, tex_bulletdecal[rand()&7], 0xFFFFFF, 0xFFFFFF);
+               CL_AllocLightFlash(NULL, &tempmatrix, 100, 0.15f, 0.15f, 1.5f, 500, 0.2, 0, -1, true, 1, 0.25, 1, 0, 0, LIGHTFLAG_NORMALMODE | LIGHTFLAG_REALTIMEMODE);
        }
-       Mem_Free(pointfile);
-       VectorCopy(leakorg, org);
-       Con_Printf("%i points read (%i particles spawned)\nLeak at %f %f %f\n", c, s, org[0], org[1], org[2]);
-
-       particle(particletype + pt_beam, 0xFF0000, 0xFF0000, tex_beam, 64, 255, 0, 0, 0, org[0] - 4096, org[1], org[2], org[0] + 4096, org[1], org[2], 0, 0, 0);
-       particle(particletype + pt_beam, 0x00FF00, 0x00FF00, tex_beam, 64, 255, 0, 0, 0, org[0], org[1] - 4096, org[2], org[0], org[1] + 4096, org[2], 0, 0, 0);
-       particle(particletype + pt_beam, 0x0000FF, 0x0000FF, tex_beam, 64, 255, 0, 0, 0, org[0], org[1], org[2] - 4096, org[0], org[1], org[2] + 4096, 0, 0, 0);
-}
-
-/*
-===============
-CL_ParseParticleEffect
-
-Parse an effect out of the server message
-===============
-*/
-void CL_ParseParticleEffect (void)
-{
-       vec3_t org, dir;
-       int i, count, msgcount, color;
-
-       MSG_ReadVector(org, cls.protocol);
-       for (i=0 ; i<3 ; i++)
-               dir[i] = MSG_ReadChar ();
-       msgcount = MSG_ReadByte ();
-       color = MSG_ReadByte ();
-
-       if (msgcount == 255)
-               count = 1024;
-       else
-               count = msgcount;
-
-       if (cl_particles_blood_bloodhack.integer && !cl_particles_quake.integer)
+       else if (effectnameindex == EFFECT_TE_SUPERSPIKE)
        {
-               if (color == 73)
+               if (cl_particles_bulletimpacts.integer)
                {
-                       // regular blood
-                       CL_BloodPuff(org, dir, count / 2);
-                       return;
+                       if (cl_particles_quake.integer)
+                       {
+                               if (cl_particles_smoke.integer)
+                                       CL_ParticleEffect(EFFECT_SVC_PARTICLE, 20*count, originmins, originmaxs, velocitymins, velocitymaxs, NULL, 0);
+                       }
+                       else
+                       {
+                               CL_Smoke(originmins, originmaxs, velocitymins, velocitymaxs, 8*count);
+                               CL_Sparks(originmins, originmaxs, velocitymins, velocitymaxs, 30*count);
+                       }
                }
-               if (color == 225)
+               // bullet hole
+               if (cl_stainmaps.integer) R_Stain(center, 32, 96, 96, 96, 24, 128, 128, 128, 24);
+               CL_SpawnDecalParticleForPoint(center, 6, 3, 255, tex_bulletdecal[rand()&7], 0xFFFFFF, 0xFFFFFF);
+       }
+       else if (effectnameindex == EFFECT_TE_SUPERSPIKEQUAD)
+       {
+               if (cl_particles_bulletimpacts.integer)
                {
-                       // lightning blood
-                       CL_BloodPuff(org, dir, count / 2);
+                       if (cl_particles_quake.integer)
+                       {
+                               if (cl_particles_smoke.integer)
+                                       CL_ParticleEffect(EFFECT_SVC_PARTICLE, 20*count, originmins, originmaxs, velocitymins, velocitymaxs, NULL, 0);
+                       }
+                       else
+                       {
+                               CL_Smoke(originmins, originmaxs, velocitymins, velocitymaxs, 8*count);
+                               CL_Sparks(originmins, originmaxs, velocitymins, velocitymaxs, 30*count);
+                       }
+               }
+               // bullet hole
+               if (cl_stainmaps.integer) R_Stain(center, 32, 96, 96, 96, 24, 128, 128, 128, 24);
+               CL_SpawnDecalParticleForPoint(center, 6, 3, 255, tex_bulletdecal[rand()&7], 0xFFFFFF, 0xFFFFFF);
+               CL_AllocLightFlash(NULL, &tempmatrix, 100, 0.15f, 0.15f, 1.5f, 500, 0.2, 0, -1, true, 1, 0.25, 1, 0, 0, LIGHTFLAG_NORMALMODE | LIGHTFLAG_REALTIMEMODE);
+       }
+       else if (effectnameindex == EFFECT_TE_BLOOD)
+       {
+               if (!cl_particles_blood.integer)
                        return;
+               if (cl_particles_quake.integer)
+                       CL_ParticleEffect(EFFECT_SVC_PARTICLE, 20*count, originmins, originmaxs, velocitymins, velocitymaxs, NULL, 73);
+               else
+               {
+                       static double bloodaccumulator = 0;
+                       bloodaccumulator += count * 0.333 * cl_particles_quality.value;
+                       for (;bloodaccumulator > 0;bloodaccumulator--)
+                               particle(particletype + pt_blood, 0xFFFFFF, 0xFFFFFF, tex_bloodparticle[rand()&7], 8, 0, cl_particles_blood_alpha.value * 768, cl_particles_blood_alpha.value * 384, 0, -1, lhrandom(originmins[0], originmaxs[0]), lhrandom(originmins[1], originmaxs[1]), lhrandom(originmins[2], originmaxs[2]), lhrandom(velocitymins[0], velocitymaxs[0]), lhrandom(velocitymins[1], velocitymaxs[1]), lhrandom(velocitymins[2], velocitymaxs[2]), 1, 4, 0, 64);
                }
        }
-       CL_RunParticleEffect (org, dir, color, count);
-}
-
-/*
-===============
-CL_ParticleExplosion
-
-===============
-*/
-void CL_ParticleExplosion (vec3_t org)
-{
-       int i;
-       trace_t trace;
-       //vec3_t v;
-       //vec3_t v2;
-       if (cl_stainmaps.integer)
-               R_Stain(org, 96, 80, 80, 80, 64, 176, 176, 176, 64);
-       CL_SpawnDecalParticleForPoint(org, 40, 48, 255, tex_bulletdecal[rand()&7], 0xFFFFFF, 0xFFFFFF);
-
-       if (cl_particles_quake.integer)
+       else if (effectnameindex == EFFECT_TE_SPARK)
+               CL_Sparks(originmins, originmaxs, velocitymins, velocitymaxs, count);
+       else if (effectnameindex == EFFECT_TE_PLASMABURN)
        {
-               for (i = 0;i < 1024;i++)
+               // plasma scorch mark
+               if (cl_stainmaps.integer) R_Stain(center, 48, 96, 96, 96, 32, 128, 128, 128, 32);
+               CL_SpawnDecalParticleForPoint(center, 6, 6, 255, tex_bulletdecal[rand()&7], 0xFFFFFF, 0xFFFFFF);
+               CL_AllocLightFlash(NULL, &tempmatrix, 200, 1, 1, 1, 1000, 0.2, 0, -1, true, 1, 0.25, 1, 0, 0, LIGHTFLAG_NORMALMODE | LIGHTFLAG_REALTIMEMODE);
+       }
+       else if (effectnameindex == EFFECT_TE_GUNSHOT)
+       {
+               if (cl_particles_bulletimpacts.integer)
                {
-                       int r, color;
-                       r = rand()&3;
-                       if (i & 1)
+                       if (cl_particles_quake.integer)
+                               CL_ParticleEffect(EFFECT_SVC_PARTICLE, 20*count, originmins, originmaxs, velocitymins, velocitymaxs, NULL, 0);
+                       else
                        {
-                               color = particlepalette[ramp1[r]];
-                               particle(particletype + pt_alphastatic, color, color, tex_particle, 1, 32 * (8 - r), 318, 0, 0, org[0], org[1], org[2], 0, 0, 0, -4, 16, 256);
+                               CL_Smoke(originmins, originmaxs, velocitymins, velocitymaxs, 4*count);
+                               CL_Sparks(originmins, originmaxs, velocitymins, velocitymaxs, 20*count);
                        }
+               }
+               // bullet hole
+               if (cl_stainmaps.integer) R_Stain(center, 32, 96, 96, 96, 24, 128, 128, 128, 24);
+               CL_SpawnDecalParticleForPoint(center, 6, 3, 255, tex_bulletdecal[rand()&7], 0xFFFFFF, 0xFFFFFF);
+       }
+       else if (effectnameindex == EFFECT_TE_GUNSHOTQUAD)
+       {
+               if (cl_particles_bulletimpacts.integer)
+               {
+                       if (cl_particles_quake.integer)
+                               CL_ParticleEffect(EFFECT_SVC_PARTICLE, 20*count, originmins, originmaxs, velocitymins, velocitymaxs, NULL, 0);
                        else
                        {
-                               color = particlepalette[ramp2[r]];
-                               particle(particletype + pt_alphastatic, color, color, tex_particle, 1, 32 * (8 - r), 478, 0, 0, org[0], org[1], org[2], 0, 0, 0, 1, 16, 256);
+                               CL_Smoke(originmins, originmaxs, velocitymins, velocitymaxs, 4*count);
+                               CL_Sparks(originmins, originmaxs, velocitymins, velocitymaxs, 20*count);
                        }
                }
+               // bullet hole
+               if (cl_stainmaps.integer) R_Stain(center, 32, 96, 96, 96, 24, 128, 128, 128, 24);
+               CL_SpawnDecalParticleForPoint(center, 6, 3, 255, tex_bulletdecal[rand()&7], 0xFFFFFF, 0xFFFFFF);
+               CL_AllocLightFlash(NULL, &tempmatrix, 100, 0.15f, 0.15f, 1.5f, 500, 0.2, 0, -1, true, 1, 0.25, 1, 0, 0, LIGHTFLAG_NORMALMODE | LIGHTFLAG_REALTIMEMODE);
        }
-       else
+       else if (effectnameindex == EFFECT_TE_EXPLOSION)
        {
-               i = CL_PointSuperContents(org);
-               if (i & (SUPERCONTENTS_SLIME | SUPERCONTENTS_WATER))
+               CL_ParticleExplosion(center);
+               CL_AllocLightFlash(NULL, &tempmatrix, 350, 4.0f, 2.0f, 0.50f, 700, 0.5, 0, -1, true, 1, 0.25, 0.25, 1, 1, LIGHTFLAG_NORMALMODE | LIGHTFLAG_REALTIMEMODE);
+       }
+       else if (effectnameindex == EFFECT_TE_EXPLOSIONQUAD)
+       {
+               CL_ParticleExplosion(center);
+               CL_AllocLightFlash(NULL, &tempmatrix, 350, 2.5f, 2.0f, 4.0f, 700, 0.5, 0, -1, true, 1, 0.25, 0.25, 1, 1, LIGHTFLAG_NORMALMODE | LIGHTFLAG_REALTIMEMODE);
+       }
+       else if (effectnameindex == EFFECT_TE_TAREXPLOSION)
+       {
+               if (cl_particles_quake.integer)
                {
-                       if (cl_particles.integer && cl_particles_bubbles.integer && cl_particles_explosions_bubbles.integer)
-                               for (i = 0;i < 128 * cl_particles_quality.value;i++)
-                                       particle(particletype + pt_bubble, 0x404040, 0x808080, tex_bubble, 2, lhrandom(128, 255), 128, -0.125, 1.5, org[0], org[1], org[2], 0, 0, 0, (1.0 / 16.0), 16, 96);
+                       int i;
+                       for (i = 0;i < 1024 * cl_particles_quality.value;i++)
+                       {
+                               if (i & 1)
+                                       particle(particletype + pt_static, particlepalette[66], particlepalette[71], tex_particle, 1.5f, 0, lhrandom(182, 255), 182, 0, 0, center[0], center[1], center[2], 0, 0, 0, -4, -4, 16, 256);
+                               else
+                                       particle(particletype + pt_static, particlepalette[150], particlepalette[155], tex_particle, 1.5f, 0, lhrandom(182, 255), 182, 0, 0, center[0], center[1], center[2], 0, 0, lhrandom(-256, 256), 0, 0, 16, 0);
+                       }
                }
                else
+                       CL_ParticleExplosion(center);
+               CL_AllocLightFlash(NULL, &tempmatrix, 600, 1.6f, 0.8f, 2.0f, 1200, 0.5, 0, -1, true, 1, 0.25, 0.25, 1, 1, LIGHTFLAG_NORMALMODE | LIGHTFLAG_REALTIMEMODE);
+       }
+       else if (effectnameindex == EFFECT_TE_SMALLFLASH)
+               CL_AllocLightFlash(NULL, &tempmatrix, 200, 2, 2, 2, 1000, 0.2, 0, -1, true, 1, 0.25, 0.25, 1, 1, LIGHTFLAG_NORMALMODE | LIGHTFLAG_REALTIMEMODE);
+       else if (effectnameindex == EFFECT_TE_FLAMEJET)
+       {
+               count *= cl_particles_quality.value;
+               while (count-- > 0)
+                       particle(particletype + pt_smoke, 0x6f0f00, 0xe3974f, tex_particle, 4, 0, lhrandom(64, 128), 384, -1, 1.1, lhrandom(originmins[0], originmaxs[0]), lhrandom(originmins[1], originmaxs[1]), lhrandom(originmins[2], originmaxs[2]), lhrandom(velocitymins[0], velocitymaxs[0]), lhrandom(velocitymins[1], velocitymaxs[1]), lhrandom(velocitymins[2], velocitymaxs[2]), 1, 4, 0, 128);
+       }
+       else if (effectnameindex == EFFECT_TE_LAVASPLASH)
+       {
+               float i, j, inc, vel;
+               vec3_t dir, org;
+
+               inc = 8 / cl_particles_quality.value;
+               for (i = -128;i < 128;i += inc)
                {
-                       // LordHavoc: smoke effect similar to UT2003, chews fillrate too badly up close
-                       // smoke puff
-                       if (cl_particles.integer && cl_particles_smoke.integer && cl_particles_explosions_smoke.integer)
+                       for (j = -128;j < 128;j += inc)
                        {
-                               for (i = 0;i < 32;i++)
-                               {
-                                       int k;
-                                       vec3_t v, v2;
-                                       for (k = 0;k < 16;k++)
-                                       {
-                                               v[0] = org[0] + lhrandom(-48, 48);
-                                               v[1] = org[1] + lhrandom(-48, 48);
-                                               v[2] = org[2] + lhrandom(-48, 48);
-                                               trace = CL_TraceBox(org, vec3_origin, vec3_origin, v, true, NULL, SUPERCONTENTS_SOLID, false);
-                                               if (trace.fraction >= 0.1)
-                                                       break;
-                                       }
-                                       VectorSubtract(trace.endpos, org, v2);
-                                       VectorScale(v2, 2.0f, v2);
-                                       particle(particletype + pt_smoke, 0x202020, 0x404040, tex_smoke[rand()&7], 12, 32, 64, 0, 0, org[0], org[1], org[2], v2[0], v2[1], v2[2], 0, 0, 0);
-                               }
+                               dir[0] = j + lhrandom(0, inc);
+                               dir[1] = i + lhrandom(0, inc);
+                               dir[2] = 256;
+                               org[0] = center[0] + dir[0];
+                               org[1] = center[1] + dir[1];
+                               org[2] = center[2] + lhrandom(0, 64);
+                               vel = lhrandom(50, 120) / VectorLength(dir); // normalize and scale
+                               particle(particletype + pt_alphastatic, particlepalette[224], particlepalette[231], tex_particle, 1.5f, 0, inc * lhrandom(24, 32), inc * 12, 0.05, 0, org[0], org[1], org[2], dir[0] * vel, dir[1] * vel, dir[2] * vel, 0, 0, 0, 0);
                        }
-
-                       if (cl_particles.integer && cl_particles_sparks.integer && cl_particles_explosions_sparks.integer)
-                               for (i = 0;i < 128 * cl_particles_quality.value;i++)
-                                       particle(particletype + pt_spark, 0x903010, 0xFFD030, tex_particle, 1.0f, lhrandom(0, 255), 512, 1, 0, org[0], org[1], org[2], 0, 0, 80, 0.2, 0, 256);
                }
        }
-
-       if (cl_particles_explosions_shell.integer)
-               R_NewExplosion(org);
-}
-
-/*
-===============
-CL_ParticleExplosion2
-
-===============
-*/
-void CL_ParticleExplosion2 (vec3_t org, int colorStart, int colorLength)
-{
-       int i, k;
-       if (!cl_particles.integer) return;
-
-       for (i = 0;i < 512 * cl_particles_quality.value;i++)
-       {
-               k = particlepalette[colorStart + (i % colorLength)];
-               if (cl_particles_quake.integer)
-                       particle(particletype + pt_static, k, k, tex_particle, 1, 255, 850, 0, 0, org[0], org[1], org[2], 0, 0, 0, -4, 8, 256);
-               else
-                       particle(particletype + pt_static, k, k, tex_particle, lhrandom(0.5, 1.5), 255, 512, 0, 0, org[0], org[1], org[2], 0, 0, 0, lhrandom(1.5, 3), 8, 192);
-       }
-}
-
-/*
-===============
-CL_BlobExplosion
-
-===============
-*/
-void CL_BlobExplosion (vec3_t org)
-{
-       int i, k;
-       if (!cl_particles.integer) return;
-
-       if (!cl_particles_quake.integer)
+       else if (effectnameindex == EFFECT_TE_TELEPORT)
        {
-               CL_ParticleExplosion(org);
-               return;
-       }
+               float i, j, k, inc, vel;
+               vec3_t dir;
 
-       for (i = 0;i < 1024 * cl_particles_quality.value;i++)
-       {
-               if (i & 1)
+               inc = 8 / cl_particles_quality.value;
+               for (i = -16;i < 16;i += inc)
                {
-                       k = particlepalette[66 + rand()%6];
-                       particle(particletype + pt_static, k, k, tex_particle, 1, lhrandom(182, 255), 182, 0, 0, org[0], org[1], org[2], 0, 0, 0, -4, 16, 256);
+                       for (j = -16;j < 16;j += inc)
+                       {
+                               for (k = -24;k < 32;k += inc)
+                               {
+                                       VectorSet(dir, i*8, j*8, k*8);
+                                       VectorNormalize(dir);
+                                       vel = lhrandom(50, 113);
+                                       particle(particletype + pt_alphastatic, particlepalette[7], particlepalette[14], tex_particle, 1.5f, 0, inc * lhrandom(37, 63), inc * 187, 0, 0, center[0] + i + lhrandom(0, inc), center[1] + j + lhrandom(0, inc), center[2] + k + lhrandom(0, inc), dir[0] * vel, dir[1] * vel, dir[2] * vel, 0, 0, 0, 0);
+                               }
+                       }
                }
-               else
+               particle(particletype + pt_static, particlepalette[14], particlepalette[14], tex_particle, 30, 0, 256, 512, 0, 0, center[0], center[1], center[2], 0, 0, 0, 0, 0, 0, 0);
+               CL_AllocLightFlash(NULL, &tempmatrix, 200, 2.0f, 2.0f, 2.0f, 400, 99.0f, 0, -1, true, 1, 0.25, 1, 0, 0, LIGHTFLAG_NORMALMODE | LIGHTFLAG_REALTIMEMODE);
+       }
+       else if (effectnameindex == EFFECT_TE_TEI_G3)
+               particle(particletype + pt_beam, 0xFFFFFF, 0xFFFFFF, tex_beam, 8, 0, 256, 256, 0, 0, originmins[0], originmins[1], originmins[2], originmaxs[0], originmaxs[1], originmaxs[2], 0, 0, 0, 0);
+       else if (effectnameindex == EFFECT_TE_TEI_SMOKE)
+       {
+               if (cl_particles_smoke.integer)
                {
-                       k = particlepalette[150 + rand()%6];
-                       particle(particletype + pt_static, k, k, tex_particle, 1, lhrandom(182, 255), 182, 0, 0, org[0], org[1], org[2], 0, 0, lhrandom(-256, 256), 0, 16, 0);
+                       count *= 0.25f * cl_particles_quality.value;
+                       while (count-- > 0)
+                               particle(particletype + pt_smoke, 0x202020, 0x404040, tex_smoke[rand()&7], 5, 0, 255, 512, 0, 0, lhrandom(originmins[0], originmaxs[0]), lhrandom(originmins[1], originmaxs[1]), lhrandom(originmins[2], originmaxs[2]), lhrandom(velocitymins[0], velocitymaxs[0]), lhrandom(velocitymins[1], velocitymaxs[1]), lhrandom(velocitymins[2], velocitymaxs[2]), 0, 0, 1.5f, 6.0f);
                }
        }
-}
-
-/*
-===============
-CL_RunParticleEffect
-
-===============
-*/
-void CL_RunParticleEffect (vec3_t org, vec3_t dir, int color, int count)
-{
-       int k;
-
-       if (count == 1024)
+       else if (effectnameindex == EFFECT_TE_TEI_BIGEXPLOSION)
        {
-               CL_ParticleExplosion(org);
-               return;
+               CL_ParticleExplosion(center);
+               CL_AllocLightFlash(NULL, &tempmatrix, 500, 2.5f, 2.0f, 1.0f, 500, 9999, 0, -1, true, 1, 0.25, 0.5, 1, 1, LIGHTFLAG_NORMALMODE | LIGHTFLAG_REALTIMEMODE);
        }
-       if (!cl_particles.integer) return;
-       if (cl_particles_quake.integer)
+       else if (effectnameindex == EFFECT_TE_TEI_PLASMAHIT)
        {
-               count *= cl_particles_quality.value;
-               while (count--)
-               {
-                       k = particlepalette[color + (rand()&7)];
-                       particle(particletype + pt_alphastatic, k, k, tex_particle, 1, lhrandom(51, 255), 512, 0, 0.05, org[0], org[1], org[2], dir[0], dir[1], dir[2], 0, 8, 0);
-               }
+               float f;
+               if (cl_stainmaps.integer)
+                       R_Stain(center, 40, 96, 96, 96, 40, 128, 128, 128, 40);
+               CL_SpawnDecalParticleForPoint(center, 6, 8, 255, tex_bulletdecal[rand()&7], 0xFFFFFF, 0xFFFFFF);
+               if (cl_particles_smoke.integer)
+                       for (f = 0;f < count;f += 4.0f / cl_particles_quality.value)
+                               particle(particletype + pt_smoke, 0x202020, 0x404040, tex_smoke[rand()&7], 5, 0, 255, 512, 0, 0, lhrandom(originmins[0], originmaxs[0]), lhrandom(originmins[1], originmaxs[1]), lhrandom(originmins[2], originmaxs[2]), lhrandom(velocitymins[0], velocitymaxs[0]), lhrandom(velocitymins[1], velocitymaxs[1]), lhrandom(velocitymins[2], velocitymaxs[2]), 0, 0, 20, 155);
+               if (cl_particles_sparks.integer)
+                       for (f = 0;f < count;f += 1.0f / cl_particles_quality.value)
+                               particle(particletype + pt_spark, 0x2030FF, 0x80C0FF, tex_particle, 2.0f, 0, lhrandom(64, 255), 512, 0, 0, lhrandom(originmins[0], originmaxs[0]), lhrandom(originmins[1], originmaxs[1]), lhrandom(originmins[2], originmaxs[2]), lhrandom(velocitymins[0], velocitymaxs[0]), lhrandom(velocitymins[1], velocitymaxs[1]), lhrandom(velocitymins[2], velocitymaxs[2]), 0, 0, 0, 465);
+               CL_AllocLightFlash(NULL, &tempmatrix, 500, 0.6f, 1.2f, 2.0f, 2000, 9999, 0, -1, true, 1, 0.25, 0.25, 1, 1, LIGHTFLAG_NORMALMODE | LIGHTFLAG_REALTIMEMODE);
        }
-       else
+       else if (effectnameindex == EFFECT_EF_FLAME)
        {
-               count *= cl_particles_quality.value;
-               while (count--)
-               {
-                       k = particlepalette[color + (rand()&7)];
-                       if (gamemode == GAME_GOODVSBAD2)
-                               particle(particletype + pt_alphastatic, k, k, tex_particle, 5, 255, 300, 0, 0, org[0], org[1], org[2], 0, 0, 0, 0, 8, 10);
-                       else
-                               particle(particletype + pt_alphastatic, k, k, tex_particle, 1, 255, 512, 0, 0, org[0], org[1], org[2], dir[0], dir[1], dir[2], 0, 8, 15);
-               }
+               count *= 300 * cl_particles_quality.value;
+               while (count-- > 0)
+                       particle(particletype + pt_smoke, 0x6f0f00, 0xe3974f, tex_particle, 4, 0, lhrandom(64, 128), 384, -1, 0, lhrandom(originmins[0], originmaxs[0]), lhrandom(originmins[1], originmaxs[1]), lhrandom(originmins[2], originmaxs[2]), lhrandom(velocitymins[0], velocitymaxs[0]), lhrandom(velocitymins[1], velocitymaxs[1]), lhrandom(velocitymins[2], velocitymaxs[2]), 1, 4, 16, 128);
+               CL_AllocLightFlash(NULL, &tempmatrix, 200, 2.0f, 1.5f, 0.5f, 0, 0, 0, -1, true, 1, 0.25, 0.25, 1, 1, LIGHTFLAG_NORMALMODE | LIGHTFLAG_REALTIMEMODE);
        }
-}
+       else if (effectnameindex == EFFECT_EF_STARDUST)
+       {
+               count *= 200 * cl_particles_quality.value;
+               while (count-- > 0)
+                       particle(particletype + pt_static, 0x903010, 0xFFD030, tex_particle, 4, 0, lhrandom(64, 128), 128, 1, 0, lhrandom(originmins[0], originmaxs[0]), lhrandom(originmins[1], originmaxs[1]), lhrandom(originmins[2], originmaxs[2]), lhrandom(velocitymins[0], velocitymaxs[0]), lhrandom(velocitymins[1], velocitymaxs[1]), lhrandom(velocitymins[2], velocitymaxs[2]), 0.2, 0.8, 16, 128);
+               CL_AllocLightFlash(NULL, &tempmatrix, 200, 1.0f, 0.7f, 0.3f, 0, 0, 0, -1, true, 1, 0.25, 0.25, 1, 1, LIGHTFLAG_NORMALMODE | LIGHTFLAG_REALTIMEMODE);
+       }
+       else if (!strncmp(particleeffectname[effectnameindex], "TR_", 3))
+       {
+               vec3_t dir, pos;
+               float len, dec, qd;
+               int smoke, blood, bubbles, r, color;
 
-// LordHavoc: added this for spawning sparks/dust (which have strong gravity)
-/*
-===============
-CL_SparkShower
-===============
-*/
-void CL_SparkShower (vec3_t org, vec3_t dir, int count, vec_t gravityscale, vec_t radius)
-{
-       int k;
+               if (spawndlight && r_refdef.numlights < MAX_DLIGHTS)
+               {
+                       vec4_t light;
+                       Vector4Set(light, 0, 0, 0, 0);
 
-       if (!cl_particles.integer) return;
+                       if (effectnameindex == EFFECT_TR_ROCKET)
+                               Vector4Set(light, 3.0f, 1.5f, 0.5f, 200);
+                       else if (effectnameindex == EFFECT_TR_VORESPIKE)
+                       {
+                               if (gamemode == GAME_PRYDON && !cl_particles_quake.integer)
+                                       Vector4Set(light, 0.3f, 0.6f, 1.2f, 100);
+                               else
+                                       Vector4Set(light, 1.2f, 0.5f, 1.0f, 200);
+                       }
+                       else if (effectnameindex == EFFECT_TR_NEXUIZPLASMA)
+                               Vector4Set(light, 0.75f, 1.5f, 3.0f, 200);
 
-       if (cl_particles_sparks.integer)
-       {
-               // sparks
-               count *= cl_particles_quality.value;
-               while(count--)
-               {
-                       k = particlepalette[0x68 + (rand() & 7)];
-                       particle(particletype + pt_spark, k, k, tex_particle, 0.4f, lhrandom(64, 255), 512, gravityscale, 0, org[0], org[1], org[2], dir[0], dir[1], dir[2] + sv_gravity.value * 0.1, 0, radius, 64);
+                       if (light[3])
+                       {
+                               matrix4x4_t tempmatrix;
+                               Matrix4x4_CreateFromQuakeEntity(&tempmatrix, originmaxs[0], originmaxs[1], originmaxs[2], 0, 0, 0, light[3]);
+                               R_RTLight_Update(&r_refdef.lights[r_refdef.numlights++], false, &tempmatrix, light, -1, NULL, true, 1, 0.25, 0, 1, 1, LIGHTFLAG_NORMALMODE | LIGHTFLAG_REALTIMEMODE);
+                       }
                }
-       }
-}
 
-void CL_Smoke (vec3_t org, vec3_t dir, int count, vec_t radius)
-{
-       vec3_t org2;
-       int k;
-       trace_t trace;
+               if (!spawnparticles)
+                       return;
 
-       if (!cl_particles.integer) return;
+               if (originmaxs[0] == originmins[0] && originmaxs[1] == originmins[1] && originmaxs[2] == originmins[2])
+                       return;
 
-       // smoke puff
-       if (cl_particles_smoke.integer)
-       {
-               k = count * 0.25 * cl_particles_quality.value;
-               while(k--)
+               VectorSubtract(originmaxs, originmins, dir);
+               len = VectorNormalizeLength(dir);
+               if (ent)
                {
-                       org2[0] = org[0] + 0.125f * lhrandom(-count, count);
-                       org2[1] = org[1] + 0.125f * lhrandom(-count, count);
-                       org2[2] = org[2] + 0.125f * lhrandom(-count, count);
-                       trace = CL_TraceBox(org, vec3_origin, vec3_origin, org2, true, NULL, SUPERCONTENTS_SOLID, false);
-                       particle(particletype + pt_smoke, 0x101010, 0x202020, tex_smoke[rand()&7], 3, 255, 1024, 0, 0, trace.endpos[0], trace.endpos[1], trace.endpos[2], 0, 0, 0, 0, radius, 8);
-               }
-       }
-}
+                       dec = -ent->persistent.trail_time;
+                       ent->persistent.trail_time += len;
+                       if (ent->persistent.trail_time < 0.01f)
+                               return;
 
-void CL_BulletMark (vec3_t org)
-{
-       if (cl_stainmaps.integer)
-               R_Stain(org, 32, 96, 96, 96, 24, 128, 128, 128, 24);
-       CL_SpawnDecalParticleForPoint(org, 6, 3, 255, tex_bulletdecal[rand()&7], 0xFFFFFF, 0xFFFFFF);
-}
+                       // if we skip out, leave it reset
+                       ent->persistent.trail_time = 0.0f;
+               }
+               else
+                       dec = 0;
 
-void CL_PlasmaBurn (vec3_t org)
-{
-       if (cl_stainmaps.integer)
-               R_Stain(org, 48, 96, 96, 96, 32, 128, 128, 128, 32);
-       CL_SpawnDecalParticleForPoint(org, 6, 6, 255, tex_bulletdecal[rand()&7], 0xFFFFFF, 0xFFFFFF);
-}
+               // advance into this frame to reach the first puff location
+               VectorMA(originmins, dec, dir, pos);
+               len -= dec;
 
-static float bloodcount = 0;
-void CL_BloodPuff (vec3_t org, vec3_t vel, int count)
-{
-       float s;
-       vec3_t org2;
-       trace_t trace;
-       // bloodcount is used to accumulate counts too small to cause a blood particle
-       if (!cl_particles.integer) return;
-       if (cl_particles_quake.integer)
-       {
-               CL_RunParticleEffect(org, vel, 73, count * 2);
-               return;
-       }
-       if (!cl_particles_blood.integer) return;
-
-       s = count + 64.0f;
-       count *= 5.0f;
-       if (count > 1000)
-               count = 1000;
-       bloodcount += count * cl_particles_quality.value;
-       while(bloodcount > 0)
-       {
-               org2[0] = org[0] + 0.125f * lhrandom(-bloodcount, bloodcount);
-               org2[1] = org[1] + 0.125f * lhrandom(-bloodcount, bloodcount);
-               org2[2] = org[2] + 0.125f * lhrandom(-bloodcount, bloodcount);
-               trace = CL_TraceBox(org, vec3_origin, vec3_origin, org2, true, NULL, SUPERCONTENTS_SOLID, false);
-               particle(particletype + pt_blood, 0xFFFFFF, 0xFFFFFF, tex_bloodparticle[rand()&7], 8, cl_particles_blood_alpha.value * 768, cl_particles_blood_alpha.value * 384, 0, -1, trace.endpos[0], trace.endpos[1], trace.endpos[2], vel[0], vel[1], vel[2], 1, 0, s);
-               bloodcount -= 16;
-       }
-}
+               smoke = cl_particles.integer && cl_particles_smoke.integer;
+               blood = cl_particles.integer && cl_particles_blood.integer;
+               bubbles = cl_particles.integer && cl_particles_bubbles.integer && !cl_particles_quake.integer && (CL_PointSuperContents(pos) & (SUPERCONTENTS_WATER | SUPERCONTENTS_SLIME));
+               qd = 1.0f / cl_particles_quality.value;
 
-void CL_BloodShower (vec3_t mins, vec3_t maxs, float velspeed, int count)
-{
-       vec3_t org, vel, diff, center, velscale;
-       if (!cl_particles.integer) return;
-       if (!cl_particles_bloodshowers.integer) return;
-       if (!cl_particles_blood.integer) return;
-
-       VectorSubtract(maxs, mins, diff);
-       center[0] = (mins[0] + maxs[0]) * 0.5;
-       center[1] = (mins[1] + maxs[1]) * 0.5;
-       center[2] = (mins[2] + maxs[2]) * 0.5;
-       velscale[0] = velspeed * 2.0 / diff[0];
-       velscale[1] = velspeed * 2.0 / diff[1];
-       velscale[2] = velspeed * 2.0 / diff[2];
-
-       bloodcount += count * 5.0f * cl_particles_quality.value;
-       while (bloodcount > 0)
-       {
-               org[0] = lhrandom(mins[0], maxs[0]);
-               org[1] = lhrandom(mins[1], maxs[1]);
-               org[2] = lhrandom(mins[2], maxs[2]);
-               vel[0] = (org[0] - center[0]) * velscale[0];
-               vel[1] = (org[1] - center[1]) * velscale[1];
-               vel[2] = (org[2] - center[2]) * velscale[2];
-               bloodcount -= 16;
-               particle(particletype + pt_blood, 0xFFFFFF, 0xFFFFFF, tex_bloodparticle[rand()&7], 8, cl_particles_blood_alpha.value * 768, cl_particles_blood_alpha.value * 384, 0, -1, org[0], org[1], org[2], vel[0], vel[1], vel[2], 1, 0, 0);
+               while (len >= 0)
+               {
+                       dec = 3;
+                       if (blood)
+                       {
+                               if (effectnameindex == EFFECT_TR_BLOOD)
+                               {
+                                       if (cl_particles_quake.integer)
+                                       {
+                                               color = particlepalette[67 + (rand()&3)];
+                                               particle(particletype + pt_alphastatic, color, color, tex_particle, 1.5f, 0, 255, 128, 0, -0.05, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 3, 0);
+                                       }
+                                       else
+                                       {
+                                               dec = 16;
+                                               particle(particletype + pt_blood, 0xFFFFFF, 0xFFFFFF, tex_bloodparticle[rand()&7], 8, 0, qd * cl_particles_blood_alpha.value * 768.0f, qd * cl_particles_blood_alpha.value * 384.0f, 0, -1, pos[0], pos[1], pos[2], lhrandom(velocitymins[0], velocitymaxs[0]), lhrandom(velocitymins[1], velocitymaxs[1]), lhrandom(velocitymins[2], velocitymaxs[2]), 1, 4, 0, 64);
+                                       }
+                               }
+                               else if (effectnameindex == EFFECT_TR_SLIGHTBLOOD)
+                               {
+                                       if (cl_particles_quake.integer)
+                                       {
+                                               dec = 6;
+                                               color = particlepalette[67 + (rand()&3)];
+                                               particle(particletype + pt_alphastatic, color, color, tex_particle, 1.5f, 0, 255, 128, 0, -0.05, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 3, 0);
+                                       }
+                                       else
+                                       {
+                                               dec = 32;
+                                               particle(particletype + pt_blood, 0xFFFFFF, 0xFFFFFF, tex_bloodparticle[rand()&7], 8, 0, qd * cl_particles_blood_alpha.value * 768.0f, qd * cl_particles_blood_alpha.value * 384.0f, 0, -1, pos[0], pos[1], pos[2], lhrandom(velocitymins[0], velocitymaxs[0]), lhrandom(velocitymins[1], velocitymaxs[1]), lhrandom(velocitymins[2], velocitymaxs[2]), 1, 4, 0, 64);
+                                       }
+                               }
+                       }
+                       if (smoke)
+                       {
+                               if (effectnameindex == EFFECT_TR_ROCKET)
+                               {
+                                       if (cl_particles_quake.integer)
+                                       {
+                                               r = rand()&3;
+                                               color = particlepalette[ramp3[r]];
+                                               particle(particletype + pt_alphastatic, color, color, tex_particle, 1.5f, 0, 42*(6-r), 306, 0, -0.05, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 3, 0);
+                                       }
+                                       else
+                                       {
+                                               particle(particletype + pt_smoke, 0x303030, 0x606060, tex_smoke[rand()&7], 3, 0, cl_particles_smoke_alpha.value*62, cl_particles_smoke_alphafade.value*62, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 0, 0);
+                                               particle(particletype + pt_static, 0x801010, 0xFFA020, tex_smoke[rand()&7], 3, 0, cl_particles_smoke_alpha.value*288, cl_particles_smoke_alphafade.value*1400, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 0, 20);
+                                       }
+                               }
+                               else if (effectnameindex == EFFECT_TR_GRENADE)
+                               {
+                                       if (cl_particles_quake.integer)
+                                       {
+                                               r = 2 + (rand()%5);
+                                               color = particlepalette[ramp3[r]];
+                                               particle(particletype + pt_alphastatic, color, color, tex_particle, 1.5f, 0, 42*(6-r), 306, 0, -0.05, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 3, 0);
+                                       }
+                                       else
+                                       {
+                                               particle(particletype + pt_smoke, 0x303030, 0x606060, tex_smoke[rand()&7], 3, 0, cl_particles_smoke_alpha.value*50, cl_particles_smoke_alphafade.value*75, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 0, 0);
+                                       }
+                               }
+                               else if (effectnameindex == EFFECT_TR_WIZSPIKE)
+                               {
+                                       if (cl_particles_quake.integer)
+                                       {
+                                               dec = 6;
+                                               color = particlepalette[52 + (rand()&7)];
+                                               particle(particletype + pt_alphastatic, color, color, tex_particle, 1.5f, 0, 255, 512, 0, 0, pos[0], pos[1], pos[2], 30*dir[1], 30*-dir[0], 0, 0, 0, 0, 0);
+                                               particle(particletype + pt_alphastatic, color, color, tex_particle, 1.5f, 0, 255, 512, 0, 0, pos[0], pos[1], pos[2], 30*-dir[1], 30*dir[0], 0, 0, 0, 0, 0);
+                                       }
+                                       else if (gamemode == GAME_GOODVSBAD2)
+                                       {
+                                               dec = 6;
+                                               particle(particletype + pt_static, 0x00002E, 0x000030, tex_particle, 6, 0, 128, 384, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 0, 0);
+                                       }
+                                       else
+                                       {
+                                               color = particlepalette[20 + (rand()&7)];
+                                               particle(particletype + pt_static, color, color, tex_particle, 2, 0, 64, 192, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 0, 0);
+                                       }
+                               }
+                               else if (effectnameindex == EFFECT_TR_KNIGHTSPIKE)
+                               {
+                                       if (cl_particles_quake.integer)
+                                       {
+                                               dec = 6;
+                                               color = particlepalette[230 + (rand()&7)];
+                                               particle(particletype + pt_alphastatic, color, color, tex_particle, 1.5f, 0, 255, 512, 0, 0, pos[0], pos[1], pos[2], 30*dir[1], 30*-dir[0], 0, 0, 0, 0, 0);
+                                               particle(particletype + pt_alphastatic, color, color, tex_particle, 1.5f, 0, 255, 512, 0, 0, pos[0], pos[1], pos[2], 30*-dir[1], 30*dir[0], 0, 0, 0, 0, 0);
+                                       }
+                                       else
+                                       {
+                                               color = particlepalette[226 + (rand()&7)];
+                                               particle(particletype + pt_static, color, color, tex_particle, 2, 0, 64, 192, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 0, 0);
+                                       }
+                               }
+                               else if (effectnameindex == EFFECT_TR_VORESPIKE)
+                               {
+                                       if (cl_particles_quake.integer)
+                                       {
+                                               color = particlepalette[152 + (rand()&3)];
+                                               particle(particletype + pt_alphastatic, color, color, tex_particle, 1.5f, 0, 255, 850, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 8, 0);
+                                       }
+                                       else if (gamemode == GAME_GOODVSBAD2)
+                                       {
+                                               dec = 6;
+                                               particle(particletype + pt_alphastatic, particlepalette[0 + (rand()&255)], particlepalette[0 + (rand()&255)], tex_particle, 6, 0, 255, 384, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 0, 0);
+                                       }
+                                       else if (gamemode == GAME_PRYDON)
+                                       {
+                                               dec = 6;
+                                               particle(particletype + pt_static, 0x103040, 0x204050, tex_particle, 6, 0, 64, 192, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 0, 0);
+                                       }
+                                       else
+                                               particle(particletype + pt_static, 0x502030, 0x502030, tex_particle, 3, 0, 64, 192, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 0, 0);
+                               }
+                               else if (effectnameindex == EFFECT_TR_NEHAHRASMOKE)
+                               {
+                                       dec = 7;
+                                       particle(particletype + pt_alphastatic, 0x303030, 0x606060, tex_smoke[rand()&7], 7, 0, 64, 320, 0, 0, pos[0], pos[1], pos[2], 0, 0, lhrandom(4, 12), 0, 0, 0, 4);
+                               }
+                               else if (effectnameindex == EFFECT_TR_NEXUIZPLASMA)
+                               {
+                                       dec = 4;
+                                       particle(particletype + pt_static, 0x283880, 0x283880, tex_particle, 4, 0, 255, 1024, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 0, 16);
+                               }
+                               else if (effectnameindex == EFFECT_TR_GLOWTRAIL)
+                                       particle(particletype + pt_alphastatic, particlepalette[palettecolor], particlepalette[palettecolor], tex_particle, 5, 0, 128, 320, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 0, 0);
+                       }
+                       if (bubbles)
+                       {
+                               if (effectnameindex == EFFECT_TR_ROCKET)
+                                       particle(particletype + pt_bubble, 0x404040, 0x808080, tex_bubble, 2, 0, lhrandom(64, 255), 256, -0.25, 1.5, pos[0], pos[1], pos[2], 0, 0, 0, 0.0625, 0.25, 0, 16);
+                               else if (effectnameindex == EFFECT_TR_GRENADE)
+                                       particle(particletype + pt_bubble, 0x404040, 0x808080, tex_bubble, 2, 0, lhrandom(64, 255), 256, -0.25, 1.5, pos[0], pos[1], pos[2], 0, 0, 0, 0.0625, 0.25, 0, 16);
+                       }
+                       // advance to next time and position
+                       dec *= qd;
+                       len -= dec;
+                       VectorMA (pos, dec, dir, pos);
+               }
+               if (ent)
+                       ent->persistent.trail_time = len;
        }
+       else if (developer.integer >= 1)
+               Con_Printf("CL_ParticleEffect_Fallback: no fallback found for effect %s\n", particleeffectname[effectnameindex]);
 }
 
-void CL_ParticleCube (vec3_t mins, vec3_t maxs, vec3_t dir, int count, int colorbase, int gravity, int randomvel)
+// this is also called on point effects with spawndlight = true and
+// spawnparticles = true
+// it is called CL_ParticleTrail because most code does not want to supply
+// these parameters, only trail handling does
+void CL_ParticleTrail(int effectnameindex, float pcount, const vec3_t originmins, const vec3_t originmaxs, const vec3_t velocitymins, const vec3_t velocitymaxs, entity_t *ent, int palettecolor, qboolean spawndlight, qboolean spawnparticles)
 {
-       int k;
-       float t;
-       if (!cl_particles.integer) return;
-       if (maxs[0] <= mins[0]) {t = mins[0];mins[0] = maxs[0];maxs[0] = t;}
-       if (maxs[1] <= mins[1]) {t = mins[1];mins[1] = maxs[1];maxs[1] = t;}
-       if (maxs[2] <= mins[2]) {t = mins[2];mins[2] = maxs[2];maxs[2] = t;}
-
-       count *= cl_particles_quality.value;
-       while (count--)
+       vec3_t center;
+       qboolean found = false;
+       if (effectnameindex < 1 || effectnameindex >= MAX_PARTICLEEFFECTNAME)
+               return; // invalid effect index
+       if (!particleeffectname[effectnameindex][0])
+               return; // no such effect
+       VectorLerp(originmins, 0.5, originmaxs, center);
+       if (!cl_particles_quake.integer && particleeffectinfo[0].effectnameindex)
        {
-               k = particlepalette[colorbase + (rand()&3)];
-               particle(particletype + pt_alphastatic, k, k, tex_particle, 2, 255, 128, gravity ? 1 : 0, 0, lhrandom(mins[0], maxs[0]), lhrandom(mins[1], maxs[1]), lhrandom(mins[2], maxs[2]), dir[0], dir[1], dir[2], 0, 0, randomvel);
-       }
-}
-
-void CL_ParticleRain (vec3_t mins, vec3_t maxs, vec3_t dir, int count, int colorbase, int type)
-{
-       int k;
-       float t, z, minz, maxz;
-       particle_t *p;
-       if (!cl_particles.integer) return;
-       if (maxs[0] <= mins[0]) {t = mins[0];mins[0] = maxs[0];maxs[0] = t;}
-       if (maxs[1] <= mins[1]) {t = mins[1];mins[1] = maxs[1];maxs[1] = t;}
-       if (maxs[2] <= mins[2]) {t = mins[2];mins[2] = maxs[2];maxs[2] = t;}
-       if (dir[2] < 0) // falling
-               z = maxs[2];
-       else // rising??
-               z = mins[2];
-
-       minz = z - fabs(dir[2]) * 0.1;
-       maxz = z + fabs(dir[2]) * 0.1;
-       minz = bound(mins[2], minz, maxs[2]);
-       maxz = bound(mins[2], maxz, maxs[2]);
+               int effectinfoindex;
+               int supercontents;
+               int tex;
+               particleeffectinfo_t *info;
+               vec3_t center;
+               vec3_t centervelocity;
+               vec3_t traildir;
+               vec3_t trailpos;
+               vec3_t rvec;
+               vec_t traillen;
+               vec_t trailstep;
+               qboolean underwater;
+               // note this runs multiple effects with the same name, each one spawns only one kind of particle, so some effects need more than one
+               VectorLerp(originmins, 0.5, originmaxs, center);
+               VectorLerp(velocitymins, 0.5, velocitymaxs, centervelocity);
+               supercontents = CL_PointSuperContents(center);
+               underwater = (supercontents & (SUPERCONTENTS_WATER | SUPERCONTENTS_SLIME)) != 0;
+               VectorSubtract(originmaxs, originmins, traildir);
+               traillen = VectorLength(traildir);
+               VectorNormalize(traildir);
+               for (effectinfoindex = 0, info = particleeffectinfo;effectinfoindex < MAX_PARTICLEEFFECTINFO && info->effectnameindex;effectinfoindex++, info++)
+               {
+                       if (info->effectnameindex == effectnameindex)
+                       {
+                               found = true;
+                               if ((info->flags & PARTICLEEFFECT_UNDERWATER) && !underwater)
+                                       continue;
+                               if ((info->flags & PARTICLEEFFECT_NOTUNDERWATER) && underwater)
+                                       continue;
 
-       count *= cl_particles_quality.value;
+                               // spawn a dlight if requested
+                               if (info->lightradiusstart > 0 && spawndlight)
+                               {
+                                       matrix4x4_t tempmatrix;
+                                       if (info->trailspacing > 0)
+                                               Matrix4x4_CreateTranslate(&tempmatrix, originmaxs[0], originmaxs[1], originmaxs[2]);
+                                       else
+                                               Matrix4x4_CreateTranslate(&tempmatrix, center[0], center[1], center[2]);
+                                       if (info->lighttime > 0 && info->lightradiusfade > 0)
+                                       {
+                                               // light flash (explosion, etc)
+                                               // called when effect starts
+                                               CL_AllocLightFlash(NULL, &tempmatrix, info->lightradiusstart, info->lightcolor[0], info->lightcolor[1], info->lightcolor[2], info->lightradiusfade, info->lighttime, info->lightcubemapnum, -1, info->lightshadow, 1, 0.25, 0, 1, 1, LIGHTFLAG_NORMALMODE | LIGHTFLAG_REALTIMEMODE);
+                                       }
+                                       else
+                                       {
+                                               // glowing entity
+                                               // called by CL_LinkNetworkEntity
+                                               Matrix4x4_Scale(&tempmatrix, info->lightradiusstart, 1);
+                                               R_RTLight_Update(&r_refdef.lights[r_refdef.numlights++], false, &tempmatrix, info->lightcolor, -1, info->lightcubemapnum > 0 ? va("cubemaps/%i", info->lightcubemapnum) : NULL, info->lightshadow, 1, 0.25, 0, 1, 1, LIGHTFLAG_NORMALMODE | LIGHTFLAG_REALTIMEMODE);
+                                       }
+                               }
 
-       switch(type)
-       {
-       case 0:
-               count *= 4; // ick, this should be in the mod or maps?
+                               if (!spawnparticles)
+                                       continue;
 
-               while(count--)
-               {
-                       k = particlepalette[colorbase + (rand()&3)];
-                       if (gamemode == GAME_GOODVSBAD2)
-                               particle(particletype + pt_rain, k, k, tex_particle, 20, lhrandom(8, 16), 0, 0, -1, lhrandom(mins[0], maxs[0]), lhrandom(mins[1], maxs[1]), lhrandom(minz, maxz), dir[0], dir[1], dir[2], 0, 0, 0);
-                       else
-                               particle(particletype + pt_rain, k, k, tex_particle, 0.5, lhrandom(8, 16), 0, 0, -1, lhrandom(mins[0], maxs[0]), lhrandom(mins[1], maxs[1]), lhrandom(minz, maxz), dir[0], dir[1], dir[2], 0, 0, 0);
-               }
-               break;
-       case 1:
-               while(count--)
-               {
-                       k = particlepalette[colorbase + (rand()&3)];
-                       if (gamemode == GAME_GOODVSBAD2)
-                               p = particle(particletype + pt_snow, k, k, tex_particle, 20, lhrandom(64, 128), 0, 0, -1, lhrandom(mins[0], maxs[0]), lhrandom(mins[1], maxs[1]), lhrandom(minz, maxz), dir[0], dir[1], dir[2], 0, 0, 0);
-                       else
-                               p = particle(particletype + pt_snow, k, k, tex_particle, 1, lhrandom(64, 128), 0, 0, -1, lhrandom(mins[0], maxs[0]), lhrandom(mins[1], maxs[1]), lhrandom(minz, maxz), dir[0], dir[1], dir[2], 0, 0, 0);
-                       if (p)
-                               VectorCopy(p->vel, p->relativedirection);
+                               // spawn particles
+                               tex = info->tex[0];
+                               if (info->tex[1] > info->tex[0])
+                               {
+                                       tex = (int)lhrandom(info->tex[0], info->tex[1]);
+                                       tex = min(tex, info->tex[1] - 1);
+                               }
+                               if (info->particletype == pt_decal)
+                                       CL_SpawnDecalParticleForPoint(center, info->originjitter[0], lhrandom(info->size[0], info->size[1]), lhrandom(info->alpha[0], info->alpha[1]), tex, info->color[0], info->color[1]);
+                               else if (info->particletype == pt_beam)
+                                       particle(particletype + info->particletype, info->color[0], info->color[1], tex, lhrandom(info->size[0], info->size[1]), info->size[2], lhrandom(info->alpha[0], info->alpha[1]), info->alpha[2], 0, 0, originmins[0], originmins[1], originmins[2], originmaxs[0], originmaxs[1], originmaxs[2], 0, 0, 0, 0);
+                               else
+                               {
+                                       if (!cl_particles.integer)
+                                               continue;
+                                       switch (info->particletype)
+                                       {
+                                       case pt_smoke: if (!cl_particles_smoke.integer) continue;break;
+                                       case pt_spark: if (!cl_particles_sparks.integer) continue;break;
+                                       case pt_bubble: if (!cl_particles_bubbles.integer) continue;break;
+                                       case pt_blood: if (!cl_particles_blood.integer) continue;break;
+                                       case pt_rain: if (!cl_particles_rain.integer) continue;break;
+                                       case pt_snow: if (!cl_particles_snow.integer) continue;break;
+                                       default: break;
+                                       }
+                                       VectorCopy(originmins, trailpos);
+                                       if (info->trailspacing > 0)
+                                       {
+                                               info->particleaccumulator += traillen / info->trailspacing * cl_particles_quality.value;
+                                               trailstep = info->trailspacing / cl_particles_quality.value;
+                                       }
+                                       else
+                                       {
+                                               info->particleaccumulator += info->countabsolute + pcount * info->countmultiplier * cl_particles_quality.value;
+                                               trailstep = 0;
+                                       }
+                                       info->particleaccumulator = bound(0, info->particleaccumulator, 16384);
+                                       for (;info->particleaccumulator >= 1;info->particleaccumulator--)
+                                       {
+                                               if (info->tex[1] > info->tex[0])
+                                               {
+                                                       tex = (int)lhrandom(info->tex[0], info->tex[1]);
+                                                       tex = min(tex, info->tex[1] - 1);
+                                               }
+                                               if (!trailstep)
+                                               {
+                                                       trailpos[0] = lhrandom(originmins[0], originmaxs[0]);
+                                                       trailpos[1] = lhrandom(originmins[1], originmaxs[1]);
+                                                       trailpos[2] = lhrandom(originmins[2], originmaxs[2]);
+                                               }
+                                               VectorRandom(rvec);
+                                               particle(particletype + info->particletype, info->color[0], info->color[1], tex, lhrandom(info->size[0], info->size[1]), info->size[2], lhrandom(info->alpha[0], info->alpha[1]), info->alpha[2], info->gravity, info->bounce, trailpos[0] + info->originoffset[0] + info->originjitter[0] * rvec[0], trailpos[1] + info->originoffset[1] + info->originjitter[1] * rvec[1], trailpos[2] + info->originoffset[2] + info->originjitter[2] * rvec[2], lhrandom(velocitymins[0], velocitymaxs[0]) * info->velocitymultiplier + info->velocityoffset[0] + info->velocityjitter[0] * rvec[0], lhrandom(velocitymins[1], velocitymaxs[1]) * info->velocitymultiplier + info->velocityoffset[1] + info->velocityjitter[1] * rvec[1], lhrandom(velocitymins[2], velocitymaxs[2]) * info->velocitymultiplier + info->velocityoffset[2] + info->velocityjitter[2] * rvec[2], info->airfriction, info->liquidfriction, 0, 0);
+                                               if (trailstep)
+                                                       VectorMA(trailpos, trailstep, traildir, trailpos);
+                                       }
+                               }
+                       }
                }
-               break;
-       default:
-               Con_Printf ("CL_ParticleRain: unknown type %i (0 = rain, 1 = snow)\n", type);
        }
+       if (!found)
+               CL_ParticleEffect_Fallback(effectnameindex, pcount, originmins, originmaxs, velocitymins, velocitymaxs, ent, palettecolor, spawndlight, spawnparticles);
+}
+
+void CL_ParticleEffect(int effectnameindex, float pcount, const vec3_t originmins, const vec3_t originmaxs, const vec3_t velocitymins, const vec3_t velocitymaxs, entity_t *ent, int palettecolor)
+{
+       CL_ParticleTrail(effectnameindex, pcount, originmins, originmaxs, velocitymins, velocitymaxs, ent, palettecolor, true, true);
 }
 
-void CL_Stardust (vec3_t mins, vec3_t maxs, int count)
+/*
+===============
+CL_EntityParticles
+===============
+*/
+void CL_EntityParticles (const entity_t *ent)
 {
-       int k;
-       float t;
-       vec3_t o, v, center;
+       int i;
+       float pitch, yaw, dist = 64, beamlength = 16, org[3], v[3];
+       static vec3_t avelocities[NUMVERTEXNORMALS];
        if (!cl_particles.integer) return;
 
-       if (maxs[0] <= mins[0]) {t = mins[0];mins[0] = maxs[0];maxs[0] = t;}
-       if (maxs[1] <= mins[1]) {t = mins[1];mins[1] = maxs[1];maxs[1] = t;}
-       if (maxs[2] <= mins[2]) {t = mins[2];mins[2] = maxs[2];maxs[2] = t;}
+       Matrix4x4_OriginFromMatrix(&ent->render.matrix, org);
 
-       center[0] = (mins[0] + maxs[0]) * 0.5f;
-       center[1] = (mins[1] + maxs[1]) * 0.5f;
-       center[2] = (mins[2] + maxs[2]) * 0.5f;
+       if (!avelocities[0][0])
+               for (i = 0;i < NUMVERTEXNORMALS * 3;i++)
+                       avelocities[0][i] = lhrandom(0, 2.55);
 
-       count *= cl_particles_quality.value;
-       while (count--)
+       for (i = 0;i < NUMVERTEXNORMALS;i++)
        {
-               k = particlepalette[224 + (rand()&15)];
-               o[0] = lhrandom(mins[0], maxs[0]);
-               o[1] = lhrandom(mins[1], maxs[1]);
-               o[2] = lhrandom(mins[2], maxs[2]);
-               VectorSubtract(o, center, v);
-               VectorNormalize(v);
-               VectorScale(v, 100, v);
-               v[2] += sv_gravity.value * 0.15f;
-               particle(particletype + pt_static, 0x903010, 0xFFD030, tex_particle, 1.5, lhrandom(64, 128), 128, 1, 0, o[0], o[1], o[2], v[0], v[1], v[2], 0.2, 0, 0);
+               yaw = cl.time * avelocities[i][0];
+               pitch = cl.time * avelocities[i][1];
+               v[0] = org[0] + m_bytenormals[i][0] * dist + (cos(pitch)*cos(yaw)) * beamlength;
+               v[1] = org[1] + m_bytenormals[i][1] * dist + (cos(pitch)*sin(yaw)) * beamlength;
+               v[2] = org[2] + m_bytenormals[i][2] * dist + (-sin(pitch)) * beamlength;
+               particle(particletype + pt_entityparticle, particlepalette[0x6f], particlepalette[0x6f], tex_particle, 1, 0, 255, 0, 0, 0, v[0], v[1], v[2], 0, 0, 0, 0, 0, 0, 0);
        }
 }
 
-void CL_FlameCube (vec3_t mins, vec3_t maxs, int count)
+
+void CL_ReadPointFile_f (void)
 {
-       int k;
-       float t;
-       if (!cl_particles.integer) return;
-       if (maxs[0] <= mins[0]) {t = mins[0];mins[0] = maxs[0];maxs[0] = t;}
-       if (maxs[1] <= mins[1]) {t = mins[1];mins[1] = maxs[1];maxs[1] = t;}
-       if (maxs[2] <= mins[2]) {t = mins[2];mins[2] = maxs[2];maxs[2] = t;}
+       vec3_t org, leakorg;
+       int r, c, s;
+       char *pointfile = NULL, *pointfilepos, *t, tchar;
+       char name[MAX_OSPATH];
 
-       count *= cl_particles_quality.value;
-       while (count--)
+       if (!cl.worldmodel)
+               return;
+
+       FS_StripExtension (cl.worldmodel->name, name, sizeof (name));
+       strlcat (name, ".pts", sizeof (name));
+       pointfile = (char *)FS_LoadFile(name, tempmempool, true, NULL);
+       if (!pointfile)
        {
-               k = particlepalette[224 + (rand()&15)];
-               particle(particletype + pt_static, k, k, tex_particle, 4, lhrandom(64, 128), 384, -1, 0, lhrandom(mins[0], maxs[0]), lhrandom(mins[1], maxs[1]), lhrandom(mins[2], maxs[2]), 0, 0, 32, 1, 0, 32);
-               if (count & 1)
-                       particle(particletype + pt_static, 0x303030, 0x606060, tex_smoke[rand()&7], 6, lhrandom(48, 96), 64, 0, 0, lhrandom(mins[0], maxs[0]), lhrandom(mins[1], maxs[1]), lhrandom(mins[2], maxs[2]), 0, 0, 24, 0, 0, 8);
+               Con_Printf("Could not open %s\n", name);
+               return;
        }
-}
-
-void CL_Flames (vec3_t org, vec3_t vel, int count)
-{
-       int k;
-       if (!cl_particles.integer) return;
 
-       count *= cl_particles_quality.value;
-       while (count--)
+       Con_Printf("Reading %s...\n", name);
+       VectorClear(leakorg);
+       c = 0;
+       s = 0;
+       pointfilepos = pointfile;
+       while (*pointfilepos)
        {
-               k = particlepalette[224 + (rand()&15)];
-               particle(particletype + pt_static, k, k, tex_particle, 4, lhrandom(64, 128), 384, -1, 1.1, org[0], org[1], org[2], vel[0], vel[1], vel[2], 1, 0, 128);
-       }
-}
+               while (*pointfilepos == '\n' || *pointfilepos == '\r')
+                       pointfilepos++;
+               if (!*pointfilepos)
+                       break;
+               t = pointfilepos;
+               while (*t && *t != '\n' && *t != '\r')
+                       t++;
+               tchar = *t;
+               *t = 0;
+               r = sscanf (pointfilepos,"%f %f %f", &org[0], &org[1], &org[2]);
+               *t = tchar;
+               pointfilepos = t;
+               if (r != 3)
+                       break;
+               if (c == 0)
+                       VectorCopy(org, leakorg);
+               c++;
 
+               if (cl.num_particles < cl.max_particles - 3)
+               {
+                       s++;
+                       particle(particletype + pt_static, particlepalette[(-c)&15], particlepalette[(-c)&15], tex_particle, 2, 0, 255, 0, 0, 0, org[0], org[1], org[2], 0, 0, 0, 0, 0, 0, 0);
+               }
+       }
+       Mem_Free(pointfile);
+       VectorCopy(leakorg, org);
+       Con_Printf("%i points read (%i particles spawned)\nLeak at %f %f %f\n", c, s, org[0], org[1], org[2]);
 
+       particle(particletype + pt_beam, 0xFF0000, 0xFF0000, tex_beam, 64, 0, 255, 0, 0, 0, org[0] - 4096, org[1], org[2], org[0] + 4096, org[1], org[2], 0, 0, 0, 0);
+       particle(particletype + pt_beam, 0x00FF00, 0x00FF00, tex_beam, 64, 0, 255, 0, 0, 0, org[0], org[1] - 4096, org[2], org[0], org[1] + 4096, org[2], 0, 0, 0, 0);
+       particle(particletype + pt_beam, 0x0000FF, 0x0000FF, tex_beam, 64, 0, 255, 0, 0, 0, org[0], org[1], org[2] - 4096, org[0], org[1], org[2] + 4096, 0, 0, 0, 0);
+}
 
 /*
 ===============
-CL_LavaSplash
+CL_ParseParticleEffect
 
+Parse an effect out of the server message
 ===============
 */
-void CL_LavaSplash (vec3_t origin)
+void CL_ParseParticleEffect (void)
 {
-       float i, j, inc, vel;
-       int k, l;
-       vec3_t          dir, org;
-       if (!cl_particles.integer) return;
+       vec3_t org, dir;
+       int i, count, msgcount, color;
 
-       if (cl_particles_quake.integer)
-       {
-               inc = 8 / cl_particles_quality.value;
-               for (i = -128;i < 128;i += inc)
-               {
-                       for (j = -128;j < 128;j += inc)
-                       {
-                               dir[0] = j + lhrandom(0, inc);
-                               dir[1] = i + lhrandom(0, inc);
-                               dir[2] = 256;
-                               org[0] = origin[0] + dir[0];
-                               org[1] = origin[1] + dir[1];
-                               org[2] = origin[2] + lhrandom(0, 64);
-                               vel = lhrandom(50, 120) / VectorLength(dir); // normalize and scale
-                               k = l = particlepalette[224 + (rand()&7)];
-                               particle(particletype + pt_alphastatic, k, l, tex_particle, 1, inc * lhrandom(24, 32), inc * 12, 0.05, 0, org[0], org[1], org[2], dir[0] * vel, dir[1] * vel, dir[2] * vel, 0, 0, 0);
-                       }
-               }
-       }
+       MSG_ReadVector(org, cls.protocol);
+       for (i=0 ; i<3 ; i++)
+               dir[i] = MSG_ReadChar ();
+       msgcount = MSG_ReadByte ();
+       color = MSG_ReadByte ();
+
+       if (msgcount == 255)
+               count = 1024;
        else
-       {
-               inc = 32 / cl_particles_quality.value;
-               for (i = -128;i < 128;i += inc)
-               {
-                       for (j = -128;j < 128;j += inc)
-                       {
-                               dir[0] = j + lhrandom(0, inc);
-                               dir[1] = i + lhrandom(0, inc);
-                               dir[2] = 256;
-                               org[0] = origin[0] + dir[0];
-                               org[1] = origin[1] + dir[1];
-                               org[2] = origin[2] + lhrandom(0, 64);
-                               vel = lhrandom(50, 120) / VectorLength(dir); // normalize and scale
-                               if (gamemode == GAME_GOODVSBAD2)
-                               {
-                                       k = particlepalette[0 + (rand()&255)];
-                                       l = particlepalette[0 + (rand()&255)];
-                                       particle(particletype + pt_static, k, l, tex_particle, 12, inc * 8, inc * 8, 0.05, 1, org[0], org[1], org[2], dir[0] * vel, dir[1] * vel, dir[2] * vel, 0, 0, 0);
-                               }
-                               else
-                               {
-                                       k = l = particlepalette[224 + (rand()&7)];
-                                       particle(particletype + pt_static, k, l, tex_particle, 12, inc * 8, inc * 8, 0.05, 0, org[0], org[1], org[2], dir[0] * vel, dir[1] * vel, dir[2] * vel, 0, 0, 0);
-                               }
-                       }
-               }
-       }
+               count = msgcount;
+
+       CL_ParticleEffect(EFFECT_SVC_PARTICLE, count, org, org, dir, dir, NULL, color);
 }
 
 /*
 ===============
-CL_TeleportSplash
+CL_ParticleExplosion
 
 ===============
 */
-void CL_TeleportSplash (vec3_t org)
+void CL_ParticleExplosion (const vec3_t org)
 {
-       float i, j, k, inc;
-       if (!cl_particles.integer) return;
+       int i;
+       trace_t trace;
+       //vec3_t v;
+       //vec3_t v2;
+       if (cl_stainmaps.integer)
+               R_Stain(org, 96, 80, 80, 80, 64, 176, 176, 176, 64);
+       CL_SpawnDecalParticleForPoint(org, 40, 48, 255, tex_bulletdecal[rand()&7], 0xFFFFFF, 0xFFFFFF);
 
        if (cl_particles_quake.integer)
        {
-               inc = 4 / cl_particles_quality.value;
-               for (i = -16;i < 16;i += inc)
+               for (i = 0;i < 1024;i++)
                {
-                       for (j = -16;j < 16;j += inc)
+                       int r, color;
+                       r = rand()&3;
+                       if (i & 1)
                        {
-                               for (k = -24;k < 32;k += inc)
-                               {
-                                       vec3_t dir;
-                                       float vel;
-                                       VectorSet(dir, i*8, j*8, k*8);
-                                       VectorNormalize(dir);
-                                       vel = lhrandom(50, 113);
-                                       particle(particletype + pt_alphastatic, particlepalette[7], particlepalette[14], tex_particle, 1, inc * lhrandom(37, 63), inc * 187, 0, 0, org[0] + i + lhrandom(0, inc), org[1] + j + lhrandom(0, inc), org[2] + k + lhrandom(0, inc), dir[0] * vel, dir[1] * vel, dir[2] * vel, 0, 0, 0);
-                               }
+                               color = particlepalette[ramp1[r]];
+                               particle(particletype + pt_alphastatic, color, color, tex_particle, 1.5f, 0, 32 * (8 - r), 318, 0, 0, org[0], org[1], org[2], 0, 0, 0, -4, -4, 16, 256);
+                       }
+                       else
+                       {
+                               color = particlepalette[ramp2[r]];
+                               particle(particletype + pt_alphastatic, color, color, tex_particle, 1.5f, 0, 32 * (8 - r), 478, 0, 0, org[0], org[1], org[2], 0, 0, 0, 1, 1, 16, 256);
                        }
                }
        }
        else
        {
-               inc = 8 / cl_particles_quality.value;
-               for (i = -16;i < 16;i += inc)
-                       for (j = -16;j < 16;j += inc)
-                               for (k = -24;k < 32;k += inc)
-                                       particle(particletype + pt_static, 0xA0A0A0, 0xFFFFFF, tex_particle, 10, inc * lhrandom(8, 16), inc * 32, 0, 0, org[0] + i + lhrandom(0, inc), org[1] + j + lhrandom(0, inc), org[2] + k + lhrandom(0, inc), 0, 0, lhrandom(-256, 256), 1, 0, 0);
-       }
-}
-
-void CL_RocketTrail (vec3_t start, vec3_t end, int type, int color, entity_t *ent)
-{
-       vec3_t vec, dir, vel, pos;
-       float len, dec, speed, qd;
-       int smoke, blood, bubbles, r;
-
-       if (end[0] == start[0] && end[1] == start[1] && end[2] == start[2])
-               return;
-
-       VectorSubtract(end, start, dir);
-       VectorNormalize(dir);
-
-       VectorSubtract (end, start, vec);
-       len = VectorNormalizeLength (vec);
-       dec = -ent->persistent.trail_time;
-       ent->persistent.trail_time += len;
-       if (ent->persistent.trail_time < 0.01f)
-               return;
-
-       // if we skip out, leave it reset
-       ent->persistent.trail_time = 0.0f;
-
-       speed = ent->state_current.time - ent->state_previous.time;
-       if (speed)
-               speed = 1.0f / speed;
-       VectorSubtract(ent->state_current.origin, ent->state_previous.origin, vel);
-       color = particlepalette[color];
-       VectorScale(vel, speed, vel);
-
-       // advance into this frame to reach the first puff location
-       VectorMA(start, dec, vec, pos);
-       len -= dec;
-
-       smoke = cl_particles.integer && cl_particles_smoke.integer;
-       blood = cl_particles.integer && cl_particles_blood.integer;
-       bubbles = cl_particles.integer && cl_particles_bubbles.integer && (CL_PointSuperContents(pos) & (SUPERCONTENTS_WATER | SUPERCONTENTS_SLIME));
-       qd = 1.0f / cl_particles_quality.value;
-
-       while (len >= 0)
-       {
-               switch (type)
+               i = CL_PointSuperContents(org);
+               if (i & (SUPERCONTENTS_SLIME | SUPERCONTENTS_WATER))
                {
-                       case 0: // rocket trail
-                               if (cl_particles_quake.integer)
-                               {
-                                       dec = 3;
-                                       r = rand()&3;
-                                       color = particlepalette[ramp3[r]];
-                                       particle(particletype + pt_alphastatic, color, color, tex_particle, 1, 42*(6-r), 306, 0, -0.05, pos[0], pos[1], pos[2], 0, 0, 0, 0, 3, 0);
-                               }
-                               else
-                               {
-                                       dec = 3;
-                                       if (smoke)
-                                       {
-                                               particle(particletype + pt_smoke, 0x303030, 0x606060, tex_smoke[rand()&7], 3, cl_particles_smoke_alpha.value*62, cl_particles_smoke_alphafade.value*62, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 0);
-                                               particle(particletype + pt_static, 0x801010, 0xFFA020, tex_smoke[rand()&7], 3, cl_particles_smoke_alpha.value*288, cl_particles_smoke_alphafade.value*1400, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 20);
-                                       }
-                                       if (bubbles)
-                                               particle(particletype + pt_bubble, 0x404040, 0x808080, tex_bubble, 2, lhrandom(64, 255), 256, -0.25, 1.5, pos[0], pos[1], pos[2], 0, 0, 0, (1.0 / 16.0), 0, 16);
-                               }
-                               break;
-
-                       case 1: // grenade trail
-                               if (cl_particles_quake.integer)
-                               {
-                                       dec = 3;
-                                       r = 2 + (rand()%5);
-                                       color = particlepalette[ramp3[r]];
-                                       particle(particletype + pt_alphastatic, color, color, tex_particle, 1, 42*(6-r), 306, 0, -0.05, pos[0], pos[1], pos[2], 0, 0, 0, 0, 3, 0);
-                               }
-                               else
-                               {
-                                       dec = 3;
-                                       if (smoke)
-                                               particle(particletype + pt_smoke, 0x303030, 0x606060, tex_smoke[rand()&7], 3, cl_particles_smoke_alpha.value*50, cl_particles_smoke_alphafade.value*50, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 0);
-                               }
-                               break;
-
-
-                       case 2: // blood
-                       case 4: // slight blood
-                               if (cl_particles_quake.integer)
-                               {
-                                       if (type == 2)
-                                       {
-                                               dec = 3;
-                                               color = particlepalette[67 + (rand()&3)];
-                                               particle(particletype + pt_alphastatic, color, color, tex_particle, 1, 255, 128, 0, -0.05, pos[0], pos[1], pos[2], 0, 0, 0, 0, 3, 0);
-                                       }
-                                       else
-                                       {
-                                               dec = 6;
-                                               color = particlepalette[67 + (rand()&3)];
-                                               particle(particletype + pt_alphastatic, color, color, tex_particle, 1, 255, 128, 0, -0.05, pos[0], pos[1], pos[2], 0, 0, 0, 0, 3, 0);
-                                       }
-                               }
-                               else
-                               {
-                                       dec = 16;
-                                       if (blood)
-                                               particle(particletype + pt_blood, 0xFFFFFF, 0xFFFFFF, tex_bloodparticle[rand()&7], 8, qd * cl_particles_blood_alpha.value * 768.0f, qd * cl_particles_blood_alpha.value * 384.0f, 0, -1, pos[0], pos[1], pos[2], vel[0] * 0.5f, vel[1] * 0.5f, vel[2] * 0.5f, 1, 0, 64);
-                               }
-                               break;
-
-                       case 3: // green tracer
-                               if (cl_particles_quake.integer)
-                               {
-                                       dec = 6;
-                                       color = particlepalette[52 + (rand()&7)];
-                                       particle(particletype + pt_alphastatic, color, color, tex_particle, 1, 255, 512, 0, 0, pos[0], pos[1], pos[2], 30*vec[1], 30*-vec[0], 0, 0, 0, 0);
-                                       particle(particletype + pt_alphastatic, color, color, tex_particle, 1, 255, 512, 0, 0, pos[0], pos[1], pos[2], 30*-vec[1], 30*vec[0], 0, 0, 0, 0);
-                               }
-                               else
-                               {
-                                       dec = 16;
-                                       if (smoke)
-                                       {
-                                               if (gamemode == GAME_GOODVSBAD2)
-                                               {
-                                                       dec = 6;
-                                                       particle(particletype + pt_static, 0x00002E, 0x000030, tex_particle, 6, 128, 384, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 0);
-                                               }
-                                               else
-                                               {
-                                                       dec = 3;
-                                                       color = particlepalette[20 + (rand()&7)];
-                                                       particle(particletype + pt_static, color, color, tex_particle, 2, 64, 192, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 0);
-                                               }
-                                       }
-                               }
-                               break;
-
-                       case 5: // flame tracer
-                               if (cl_particles_quake.integer)
-                               {
-                                       dec = 6;
-                                       color = particlepalette[230 + (rand()&7)];
-                                       particle(particletype + pt_alphastatic, color, color, tex_particle, 1, 255, 512, 0, 0, pos[0], pos[1], pos[2], 30*vec[1], 30*-vec[0], 0, 0, 0, 0);
-                                       particle(particletype + pt_alphastatic, color, color, tex_particle, 1, 255, 512, 0, 0, pos[0], pos[1], pos[2], 30*-vec[1], 30*vec[0], 0, 0, 0, 0);
-                               }
-                               else
+                       if (cl_particles.integer && cl_particles_bubbles.integer)
+                               for (i = 0;i < 128 * cl_particles_quality.value;i++)
+                                       particle(particletype + pt_bubble, 0x404040, 0x808080, tex_bubble, 2, 0, lhrandom(128, 255), 128, -0.125, 1.5, org[0], org[1], org[2], 0, 0, 0, 0.0625, 0.25, 16, 96);
+               }
+               else
+               {
+                       // LordHavoc: smoke effect similar to UT2003, chews fillrate too badly up close
+                       // smoke puff
+                       if (cl_particles.integer && cl_particles_smoke.integer && cl_particles_explosions_smoke.integer)
+                       {
+                               for (i = 0;i < 32;i++)
                                {
-                                       dec = 3;
-                                       if (smoke)
+                                       int k;
+                                       vec3_t v, v2;
+                                       for (k = 0;k < 16;k++)
                                        {
-                                               color = particlepalette[226 + (rand()&7)];
-                                               particle(particletype + pt_static, color, color, tex_particle, 2, 64, 192, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 0);
+                                               v[0] = org[0] + lhrandom(-48, 48);
+                                               v[1] = org[1] + lhrandom(-48, 48);
+                                               v[2] = org[2] + lhrandom(-48, 48);
+                                               trace = CL_Move(org, vec3_origin, vec3_origin, v, MOVE_NOMONSTERS, NULL, SUPERCONTENTS_SOLID, true, false, NULL, false);
+                                               if (trace.fraction >= 0.1)
+                                                       break;
                                        }
+                                       VectorSubtract(trace.endpos, org, v2);
+                                       VectorScale(v2, 2.0f, v2);
+                                       particle(particletype + pt_smoke, 0x202020, 0x404040, tex_smoke[rand()&7], 12, 0, 32, 64, 0, 0, org[0], org[1], org[2], v2[0], v2[1], v2[2], 0, 0, 0, 0);
                                }
-                               break;
+                       }
 
-                       case 6: // voor trail
-                               if (cl_particles_quake.integer)
-                               {
-                                       dec = 3;
-                                       color = particlepalette[152 + (rand()&3)];
-                                       particle(particletype + pt_alphastatic, color, color, tex_particle, 1, 255, 850, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 8, 0);
-                               }
-                               else
+                       if (cl_particles.integer && cl_particles_sparks.integer && cl_particles_explosions_sparks.integer)
+                       {
+                               for (i = 0;i < 512 * cl_particles_quality.value;i++)
                                {
-                                       dec = 16;
-                                       if (smoke)
-                                       {
-                                               if (gamemode == GAME_GOODVSBAD2)
-                                               {
-                                                       dec = 6;
-                                                       particle(particletype + pt_alphastatic, particlepalette[0 + (rand()&255)], particlepalette[0 + (rand()&255)], tex_particle, 6, 255, 384, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 0);
-                                               }
-                                               else if (gamemode == GAME_PRYDON)
-                                               {
-                                                       dec = 6;
-                                                       particle(particletype + pt_static, 0x103040, 0x204050, tex_particle, 6, 64, 192, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 0);
-                                               }
-                                               else
-                                               {
-                                                       dec = 3;
-                                                       particle(particletype + pt_static, 0x502030, 0x502030, tex_particle, 3, 64, 192, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 0);
-                                               }
+                                       int k;
+                                       vec3_t v, v2;
+                                       for (k = 0;k < 16;k++)
+                                       {
+                                               VectorRandom(v2);
+                                               VectorMA(org, 128, v2, v);
+                                               trace = CL_Move(org, vec3_origin, vec3_origin, v, MOVE_NOMONSTERS, NULL, SUPERCONTENTS_SOLID, true, false, NULL, false);
+                                               if (trace.fraction >= 0.1)
+                                                       break;
                                        }
+                                       VectorSubtract(trace.endpos, org, v2);
+                                       VectorScale(v2, 2.0f, v2);
+                                       particle(particletype + pt_spark, 0x903010, 0xFFD030, tex_particle, 1.0f, 0, lhrandom(0, 255), 512, 0, 0, org[0], org[1], org[2], v2[0], v2[1], v2[2], 0, 0, 0, 0);
                                }
-                               break;
-                       case 7: // Nehahra smoke tracer
-                               dec = 7;
-                               if (smoke)
-                                       particle(particletype + pt_alphastatic, 0x303030, 0x606060, tex_smoke[rand()&7], 7, 64, 320, 0, 0, pos[0], pos[1], pos[2], 0, 0, lhrandom(4, 12), 0, 0, 4);
-                               break;
-                       case 8: // Nexuiz plasma trail
-                               dec = 4;
-                               if (smoke)
-                                       particle(particletype + pt_static, 0x283880, 0x283880, tex_particle, 4, 255, 1024, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 16);
-                               break;
-                       case 9: // glow trail
-                               dec = 3;
-                               if (smoke)
-                                       particle(particletype + pt_alphastatic, color, color, tex_particle, 5, 128, 320, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 0);
-                               break;
-                       default:
-                               Sys_Error("CL_RocketTrail: unknown trail type %i", type);
+                       }
                }
+       }
 
-               // advance to next time and position
-               dec *= qd;
-               len -= dec;
-               VectorMA (pos, dec, vec, pos);
+       if (cl_particles_explosions_shell.integer)
+               R_NewExplosion(org);
+}
+
+/*
+===============
+CL_ParticleExplosion2
+
+===============
+*/
+void CL_ParticleExplosion2 (const vec3_t org, int colorStart, int colorLength)
+{
+       int i, k;
+       if (!cl_particles.integer) return;
+
+       for (i = 0;i < 512 * cl_particles_quality.value;i++)
+       {
+               k = particlepalette[colorStart + (i % colorLength)];
+               if (cl_particles_quake.integer)
+                       particle(particletype + pt_static, k, k, tex_particle, 1, 0, 255, 850, 0, 0, org[0], org[1], org[2], 0, 0, 0, -4, -4, 8, 256);
+               else
+                       particle(particletype + pt_static, k, k, tex_particle, lhrandom(0.5, 1.5), 0, 255, 512, 0, 0, org[0], org[1], org[2], 0, 0, 0, lhrandom(1.5, 3), lhrandom(1.5, 3), 8, 192);
+       }
+}
+
+static void CL_Sparks(const vec3_t originmins, const vec3_t originmaxs, const vec3_t velocitymins, const vec3_t velocitymaxs, float sparkcount)
+{
+       if (cl_particles_sparks.integer)
+       {
+               sparkcount *= cl_particles_quality.value;
+               while(sparkcount-- > 0)
+                       particle(particletype + pt_spark, particlepalette[0x68], particlepalette[0x6f], tex_particle, 0.5f, 0, lhrandom(64, 255), 512, 1, 0, lhrandom(originmins[0], originmaxs[0]), lhrandom(originmins[1], originmaxs[1]), lhrandom(originmins[2], originmaxs[2]), lhrandom(velocitymins[0], velocitymaxs[0]), lhrandom(velocitymins[1], velocitymaxs[1]), lhrandom(velocitymins[2], velocitymaxs[2]) + sv_gravity.value * 0.1f, 0, 0, 0, 64);
        }
-       ent->persistent.trail_time = len;
 }
 
-void CL_BeamParticle (const vec3_t start, const vec3_t end, vec_t radius, float red, float green, float blue, float alpha, float lifetime)
+static void CL_Smoke(const vec3_t originmins, const vec3_t originmaxs, const vec3_t velocitymins, const vec3_t velocitymaxs, float smokecount)
 {
-       int tempcolor2, cr, cg, cb;
-       cr = red * 255;
-       cg = green * 255;
-       cb = blue * 255;
-       tempcolor2 = (bound(0, cr, 255) << 16) | (bound(0, cg, 255) << 8) | bound(0, cb, 255);
-       particle(particletype + pt_beam, tempcolor2, tempcolor2, tex_beam, radius, alpha * 255, alpha * 255 / lifetime, 0, 0, start[0], start[1], start[2], end[0], end[1], end[2], 0, 0, 0);
+       if (cl_particles_smoke.integer)
+       {
+               smokecount *= cl_particles_quality.value;
+               while(smokecount-- > 0)
+                       particle(particletype + pt_smoke, 0x101010, 0x101010, tex_smoke[rand()&7], 2, 2, 255, 256, 0, 0, lhrandom(originmins[0], originmaxs[0]), lhrandom(originmins[1], originmaxs[1]), lhrandom(originmins[2], originmaxs[2]), lhrandom(velocitymins[0], velocitymaxs[0]), lhrandom(velocitymins[1], velocitymaxs[1]), lhrandom(velocitymins[2], velocitymaxs[2]), 0, 0, 0, smokecount > 0 ? 16 : 0);
+       }
 }
 
-void CL_Tei_Smoke(const vec3_t org, const vec3_t dir, int count)
+void CL_ParticleCube (const vec3_t mins, const vec3_t maxs, const vec3_t dir, int count, int colorbase, vec_t gravity, vec_t randomvel)
 {
-       float f;
+       int k;
        if (!cl_particles.integer) return;
 
-       // smoke puff
-       if (cl_particles_smoke.integer)
-               for (f = 0;f < count;f += 4.0f / cl_particles_quality.value)
-                       particle(particletype + pt_smoke, 0x202020, 0x404040, tex_smoke[rand()&7], 5, 255, 512, 0, 0, org[0], org[1], org[2], dir[0], dir[1], dir[2], 0, count * 0.125f, count * 0.5f);
+       count = (int)(count * cl_particles_quality.value);
+       while (count--)
+       {
+               k = particlepalette[colorbase + (rand()&3)];
+               particle(particletype + pt_alphastatic, k, k, tex_particle, 2, 0, 255, 128, gravity, 0, lhrandom(mins[0], maxs[0]), lhrandom(mins[1], maxs[1]), lhrandom(mins[2], maxs[2]), dir[0], dir[1], dir[2], 0, 0, 0, randomvel);
+       }
 }
 
-void CL_Tei_PlasmaHit(const vec3_t org, const vec3_t dir, int count)
+void CL_ParticleRain (const vec3_t mins, const vec3_t maxs, const vec3_t dir, int count, int colorbase, int type)
 {
-       float f;
+       int k;
+       float z, minz, maxz;
+       particle_t *p;
        if (!cl_particles.integer) return;
+       if (dir[2] < 0) // falling
+               z = maxs[2];
+       else // rising??
+               z = mins[2];
 
-       if (cl_stainmaps.integer)
-               R_Stain(org, 40, 96, 96, 96, 40, 128, 128, 128, 40);
-       CL_SpawnDecalParticleForPoint(org, 6, 8, 255, tex_bulletdecal[rand()&7], 0xFFFFFF, 0xFFFFFF);
+       minz = z - fabs(dir[2]) * 0.1;
+       maxz = z + fabs(dir[2]) * 0.1;
+       minz = bound(mins[2], minz, maxs[2]);
+       maxz = bound(mins[2], maxz, maxs[2]);
 
-       // smoke puff
-       if (cl_particles_smoke.integer)
-               for (f = 0;f < count;f += 4.0f / cl_particles_quality.value)
-                       particle(particletype + pt_smoke, 0x202020, 0x404040, tex_smoke[rand()&7], 5, 255, 512, 0, 0, org[0], org[1], org[2], dir[0], dir[1], dir[2], 0, count * 0.125f, count);
+       count = (int)(count * cl_particles_quality.value);
 
-       // sparks
-       if (cl_particles_sparks.integer)
-               for (f = 0;f < count;f += 1.0f / cl_particles_quality.value)
-                       particle(particletype + pt_spark, 0x2030FF, 0x80C0FF, tex_particle, 2.0f, lhrandom(64, 255), 512, 0, 0, org[0], org[1], org[2], dir[0], dir[1], dir[2], 0, 0, count * 3.0f);
+       switch(type)
+       {
+       case 0:
+               if (!cl_particles_rain.integer) break;
+               count *= 4; // ick, this should be in the mod or maps?
+
+               while(count--)
+               {
+                       k = particlepalette[colorbase + (rand()&3)];
+                       if (gamemode == GAME_GOODVSBAD2)
+                               particle(particletype + pt_rain, k, k, tex_particle, 20, 0, lhrandom(8, 16), 0, 0, -1, lhrandom(mins[0], maxs[0]), lhrandom(mins[1], maxs[1]), lhrandom(minz, maxz), dir[0], dir[1], dir[2], 0, 0, 0, 0);
+                       else
+                               particle(particletype + pt_rain, k, k, tex_particle, 0.5, 0, lhrandom(8, 16), 0, 0, -1, lhrandom(mins[0], maxs[0]), lhrandom(mins[1], maxs[1]), lhrandom(minz, maxz), dir[0], dir[1], dir[2], 0, 0, 0, 0);
+               }
+               break;
+       case 1:
+               if (!cl_particles_snow.integer) break;
+               while(count--)
+               {
+                       k = particlepalette[colorbase + (rand()&3)];
+                       if (gamemode == GAME_GOODVSBAD2)
+                               p = particle(particletype + pt_snow, k, k, tex_particle, 20, 0, lhrandom(64, 128), 0, 0, -1, lhrandom(mins[0], maxs[0]), lhrandom(mins[1], maxs[1]), lhrandom(minz, maxz), dir[0], dir[1], dir[2], 0, 0, 0, 0);
+                       else
+                               p = particle(particletype + pt_snow, k, k, tex_particle, 1, 0, lhrandom(64, 128), 0, 0, -1, lhrandom(mins[0], maxs[0]), lhrandom(mins[1], maxs[1]), lhrandom(minz, maxz), dir[0], dir[1], dir[2], 0, 0, 0, 0);
+                       if (p)
+                               VectorCopy(p->vel, p->relativedirection);
+               }
+               break;
+       default:
+               Con_Printf ("CL_ParticleRain: unknown type %i (0 = rain, 1 = snow)\n", type);
+       }
 }
 
 /*
@@ -1161,7 +1505,8 @@ void CL_MoveParticles (void)
 {
        particle_t *p;
        int i, maxparticle, j, a, content;
-       float gravity, dvel, bloodwaterfade, frametime, f, dist, org[3], oldorg[3];
+       float gravity, dvel, decalfade, frametime, f, dist, org[3], oldorg[3];
+       particletype_t *decaltype, *bloodtype;
        int hitent;
        trace_t trace;
 
@@ -1172,25 +1517,67 @@ void CL_MoveParticles (void)
                return;
        }
 
-       frametime = cl.time - cl.oldtime;
+       frametime = bound(0, cl.time - cl.oldtime, 0.1);
        gravity = frametime * sv_gravity.value;
        dvel = 1+4*frametime;
-       bloodwaterfade = max(cl_particles_blood_alpha.value, 0.01f) * frametime * 128.0f;
+       decalfade = frametime * 255 / cl_decals_fadetime.value;
+       decaltype = particletype + pt_decal;
+       bloodtype = particletype + pt_blood;
 
        maxparticle = -1;
        j = 0;
        for (i = 0, p = cl.particles;i < cl.num_particles;i++, p++)
        {
                if (!p->type)
+               {
+                       if (cl.free_particle > i)
+                               cl.free_particle = i;
                        continue;
+               }
                maxparticle = i;
+
+               // heavily optimized decal case
+               if (p->type == decaltype)
+               {
+                       // FIXME: this has fairly wacky handling of alpha
+                       if (cl.time > p->time2 + cl_decals_time.value)
+                       {
+                               p->alpha -= decalfade;
+                               if (p->alpha <= 0)
+                               {
+                                       p->type = NULL;
+                                       if (cl.free_particle > i)
+                                               cl.free_particle = i;
+                                       continue;
+                               }
+                       }
+                       if (p->owner)
+                       {
+                               if (cl.entities[p->owner].render.model == p->ownermodel)
+                               {
+                                       Matrix4x4_Transform(&cl.entities[p->owner].render.matrix, p->relativeorigin, p->org);
+                                       Matrix4x4_Transform3x3(&cl.entities[p->owner].render.matrix, p->relativedirection, p->vel);
+                               }
+                               else
+                               {
+                                       p->type = NULL;
+                                       if (cl.free_particle > i)
+                                               cl.free_particle = i;
+                               }
+                       }
+                       continue;
+               }
+
                content = 0;
 
+               p->size += p->sizeincrease * frametime;
                p->alpha -= p->alphafade * frametime;
 
                if (p->alpha <= 0)
                {
                        p->type = NULL;
+                       if (cl.free_particle > i)
+                               cl.free_particle = i;
                        continue;
                }
 
@@ -1201,7 +1588,7 @@ void CL_MoveParticles (void)
                        VectorCopy(p->org, org);
                        if (p->bounce)
                        {
-                               trace = CL_TraceBox(oldorg, vec3_origin, vec3_origin, p->org, true, &hitent, SUPERCONTENTS_SOLID | (p->type == particletype + pt_rain ? SUPERCONTENTS_LIQUIDSMASK : 0), false);
+                               trace = CL_Move(oldorg, vec3_origin, vec3_origin, p->org, MOVE_NOMONSTERS, NULL, SUPERCONTENTS_SOLID | SUPERCONTENTS_BODY | (p->type == particletype + pt_rain ? SUPERCONTENTS_LIQUIDSMASK : 0), true, false, &hitent, false);
                                // if the trace started in or hit something of SUPERCONTENTS_NODROP
                                // or if the trace hit something flagged as NOIMPACT
                                // then remove the particle
@@ -1222,18 +1609,20 @@ void CL_MoveParticles (void)
                                                VectorCopy(trace.plane.normal, p->vel);
                                                VectorAdd(p->org, p->vel, p->org);
                                                p->type = particletype + pt_raindecal;
-                                               p->texnum = tex_rainsplash[0];
+                                               p->texnum = tex_rainsplash;
                                                p->time2 = cl.time;
                                                p->alphafade = p->alpha / 0.4;
                                                p->bounce = 0;
-                                               p->friction = 0;
+                                               p->airfriction = 0;
+                                               p->liquidfriction = 0;
                                                p->gravity = 0;
-                                               p->size = 8.0;
+                                               p->size *= 1.0f;
+                                               p->sizeincrease = p->size * 2;
                                                count = rand() & 3;
                                                while(count--)
-                                                       particle(particletype + pt_spark, 0x000000, 0x707070, tex_particle, 0.25f, lhrandom(64, 255), 512, 1, 0, p->org[0], p->org[1], p->org[2], p->vel[0]*16, p->vel[1]*16, 32 + p->vel[2]*16, 0, 0, 32);
+                                                       particle(particletype + pt_spark, 0x000000, 0x707070, tex_particle, 0.25f, 0, lhrandom(64, 255), 512, 1, 0, p->org[0], p->org[1], p->org[2], p->vel[0]*16, p->vel[1]*16, sv_gravity.value * 0.04 + p->vel[2]*16, 0, 0, 0, 32);
                                        }
-                                       else if (p->type == particletype + pt_blood)
+                                       else if (p->type == bloodtype)
                                        {
                                                // blood - splash on solid
                                                if (trace.hitq3surfaceflags & Q3SURFACEFLAG_NOMARKS)
@@ -1241,6 +1630,8 @@ void CL_MoveParticles (void)
                                                        p->type = NULL;
                                                        continue;
                                                }
+                                               if (cl_stainmaps.integer)
+                                                       R_Stain(p->org, 32, 32, 16, 16, (int)(p->alpha * p->size * (1.0f / 40.0f)), 192, 48, 48, (int)(p->alpha * p->size * (1.0f / 40.0f)));
                                                if (!cl_decals.integer)
                                                {
                                                        p->type = NULL;
@@ -1249,19 +1640,19 @@ void CL_MoveParticles (void)
                                                // convert from a blood particle to a blood decal
                                                VectorCopy(trace.plane.normal, p->vel);
                                                VectorAdd(p->org, p->vel, p->org);
-                                               if (cl_stainmaps.integer)
-                                                       R_Stain(p->org, 32, 32, 16, 16, p->alpha * p->size * (1.0f / 40.0f), 192, 48, 48, p->alpha * p->size * (1.0f / 40.0f));
 
                                                p->type = particletype + pt_decal;
                                                p->texnum = tex_blooddecal[rand()&7];
                                                p->owner = hitent;
                                                p->ownermodel = cl.entities[hitent].render.model;
+                                               // these relative things are only used to regenerate p->org and p->vel if p->owner is not world (0)
                                                Matrix4x4_Transform(&cl.entities[hitent].render.inversematrix, p->org, p->relativeorigin);
                                                Matrix4x4_Transform3x3(&cl.entities[hitent].render.inversematrix, p->vel, p->relativedirection);
                                                p->time2 = cl.time;
                                                p->alphafade = 0;
                                                p->bounce = 0;
-                                               p->friction = 0;
+                                               p->airfriction = 0;
+                                               p->liquidfriction = 0;
                                                p->gravity = 0;
                                                p->size *= 2.0f;
                                        }
@@ -1283,12 +1674,14 @@ void CL_MoveParticles (void)
                        }
                        p->vel[2] -= p->gravity * gravity;
 
-                       if (p->friction)
+                       if (p->liquidfriction && CL_PointSuperContents(p->org) & SUPERCONTENTS_LIQUIDSMASK)
+                       {
+                               f = 1.0f - min(p->liquidfriction * frametime, 1);
+                               VectorScale(p->vel, f, p->vel);
+                       }
+                       else if (p->airfriction)
                        {
-                               f = p->friction * frametime;
-                               if (CL_PointSuperContents(p->org) & SUPERCONTENTS_LIQUIDSMASK)
-                                       f *= 4;
-                               f = 1.0f - f;
+                               f = 1.0f - min(p->airfriction * frametime, 1);
                                VectorScale(p->vel, f, p->vel);
                        }
                }
@@ -1326,7 +1719,7 @@ void CL_MoveParticles (void)
                                break;
                        case pt_rain:
                                a = CL_PointSuperContents(p->org);
-                               if (a & (SUPERCONTENTS_SOLID | SUPERCONTENTS_LIQUIDSMASK))
+                               if (a & (SUPERCONTENTS_SOLID | SUPERCONTENTS_BODY | SUPERCONTENTS_LIQUIDSMASK))
                                        p->type = NULL;
                                break;
                        case pt_snow:
@@ -1339,28 +1732,7 @@ void CL_MoveParticles (void)
                                        //p->vel[2] = p->relativedirection[2] + lhrandom(-32, 32);
                                }
                                a = CL_PointSuperContents(p->org);
-                               if (a & (SUPERCONTENTS_SOLID | SUPERCONTENTS_LIQUIDSMASK))
-                                       p->type = NULL;
-                               break;
-                       case pt_smoke:
-                               //p->size += frametime * 15;
-                               break;
-                       case pt_decal:
-                               // FIXME: this has fairly wacky handling of alpha
-                               p->alphafade = cl.time > (p->time2 + cl_decals_time.value) ? (255 / cl_decals_fadetime.value) : 0;
-                               if (cl.entities[p->owner].render.model == p->ownermodel)
-                               {
-                                       Matrix4x4_Transform(&cl.entities[p->owner].render.matrix, p->relativeorigin, p->org);
-                                       Matrix4x4_Transform3x3(&cl.entities[p->owner].render.matrix, p->relativedirection, p->vel);
-                               }
-                               else
-                                       p->type = NULL;
-                               break;
-                       case pt_raindecal:
-                               a = max(0, (cl.time - p->time2) * 40);
-                               if (a < 16)
-                                       p->texnum = tex_rainsplash[a];
-                               else
+                               if (a & (SUPERCONTENTS_SOLID | SUPERCONTENTS_BODY | SUPERCONTENTS_LIQUIDSMASK))
                                        p->type = NULL;
                                break;
                        default:
@@ -1369,7 +1741,6 @@ void CL_MoveParticles (void)
                }
        }
        cl.num_particles = maxparticle + 1;
-       cl.free_particle = 0;
 }
 
 #define MAX_PARTICLETEXTURES 64
@@ -1428,10 +1799,6 @@ static void setuptex(int texnum, unsigned char *data, unsigned char *particletex
        int basex, basey, y;
        basex = ((texnum >> 0) & 7) * PARTICLETEXTURESIZE;
        basey = ((texnum >> 3) & 7) * PARTICLETEXTURESIZE;
-       particletexture[texnum].s1 = (basex + 1) / (float)PARTICLEFONTSIZE;
-       particletexture[texnum].t1 = (basey + 1) / (float)PARTICLEFONTSIZE;
-       particletexture[texnum].s2 = (basex + PARTICLETEXTURESIZE - 1) / (float)PARTICLEFONTSIZE;
-       particletexture[texnum].t2 = (basey + PARTICLETEXTURESIZE - 1) / (float)PARTICLEFONTSIZE;
        for (y = 0;y < PARTICLETEXTURESIZE;y++)
                memcpy(particletexturedata + ((basey + y) * PARTICLEFONTSIZE + basex) * 4, data + y * PARTICLETEXTURESIZE * 4, PARTICLETEXTURESIZE * 4);
 }
@@ -1455,9 +1822,9 @@ void particletextureblotch(unsigned char *data, float radius, float red, float g
                        if (f > 0)
                        {
                                d = data + (y * PARTICLETEXTURESIZE + x) * 4;
-                               d[0] += f * (red   - d[0]);
-                               d[1] += f * (green - d[1]);
-                               d[2] += f * (blue  - d[2]);
+                               d[0] += (int)(f * (red   - d[0]));
+                               d[1] += (int)(f * (green - d[1]));
+                               d[2] += (int)(f * (blue  - d[2]));
                        }
                }
        }
@@ -1517,13 +1884,14 @@ static void R_InitBloodTextures (unsigned char *particletexturedata)
 
 }
 
+//uncomment this to make engine save out particle font to a tga file when run
+//#define DUMPPARTICLEFONT
+
 static void R_InitParticleTexture (void)
 {
        int x, y, d, i, k, m;
-       float dx, dy, radius, f, f2;
-       unsigned char data[PARTICLETEXTURESIZE][PARTICLETEXTURESIZE][4], noise3[64][64], data2[64][16][4];
+       float dx, dy, f;
        vec3_t light;
-       unsigned char *particletexturedata;
 
        // a note: decals need to modulate (multiply) the background color to
        // properly darken it (stain), and they need to be able to alpha fade,
@@ -1534,172 +1902,186 @@ static void R_InitParticleTexture (void)
        // and white on black background) so we can alpha fade it to black, then
        // we invert it again during the blendfunc to make it work...
 
-       particletexturedata = (unsigned char *)Mem_Alloc(tempmempool, PARTICLEFONTSIZE*PARTICLEFONTSIZE*4);
-       memset(particletexturedata, 255, PARTICLEFONTSIZE*PARTICLEFONTSIZE*4);
-
-       // smoke
-       for (i = 0;i < 8;i++)
+#ifndef DUMPPARTICLEFONT
+       particlefonttexture = loadtextureimage(particletexturepool, "particles/particlefont.tga", 0, 0, false, TEXF_ALPHA | TEXF_PRECACHE);
+       if (!particlefonttexture)
+#endif
        {
-               memset(&data[0][0][0], 255, sizeof(data));
-               do
-               {
-                       unsigned char noise1[PARTICLETEXTURESIZE*2][PARTICLETEXTURESIZE*2], noise2[PARTICLETEXTURESIZE*2][PARTICLETEXTURESIZE*2];
+               unsigned char *particletexturedata = (unsigned char *)Mem_Alloc(tempmempool, PARTICLEFONTSIZE*PARTICLEFONTSIZE*4);
+               unsigned char data[PARTICLETEXTURESIZE][PARTICLETEXTURESIZE][4];
+               memset(particletexturedata, 255, PARTICLEFONTSIZE*PARTICLEFONTSIZE*4);
 
-                       fractalnoise(&noise1[0][0], PARTICLETEXTURESIZE*2, PARTICLETEXTURESIZE/8);
-                       fractalnoise(&noise2[0][0], PARTICLETEXTURESIZE*2, PARTICLETEXTURESIZE/4);
-                       m = 0;
-                       for (y = 0;y < PARTICLETEXTURESIZE;y++)
+               // smoke
+               for (i = 0;i < 8;i++)
+               {
+                       memset(&data[0][0][0], 255, sizeof(data));
+                       do
                        {
-                               dy = (y - 0.5f*PARTICLETEXTURESIZE) / (PARTICLETEXTURESIZE*0.5f-1);
-                               for (x = 0;x < PARTICLETEXTURESIZE;x++)
+                               unsigned char noise1[PARTICLETEXTURESIZE*2][PARTICLETEXTURESIZE*2], noise2[PARTICLETEXTURESIZE*2][PARTICLETEXTURESIZE*2];
+
+                               fractalnoise(&noise1[0][0], PARTICLETEXTURESIZE*2, PARTICLETEXTURESIZE/8);
+                               fractalnoise(&noise2[0][0], PARTICLETEXTURESIZE*2, PARTICLETEXTURESIZE/4);
+                               m = 0;
+                               for (y = 0;y < PARTICLETEXTURESIZE;y++)
                                {
-                                       dx = (x - 0.5f*PARTICLETEXTURESIZE) / (PARTICLETEXTURESIZE*0.5f-1);
-                                       d = (noise2[y][x] - 128) * 3 + 192;
-                                       if (d > 0)
-                                               d = d * (1-(dx*dx+dy*dy));
-                                       d = (d * noise1[y][x]) >> 7;
-                                       d = bound(0, d, 255);
-                                       data[y][x][3] = (unsigned char) d;
-                                       if (m < d)
-                                               m = d;
+                                       dy = (y - 0.5f*PARTICLETEXTURESIZE) / (PARTICLETEXTURESIZE*0.5f-1);
+                                       for (x = 0;x < PARTICLETEXTURESIZE;x++)
+                                       {
+                                               dx = (x - 0.5f*PARTICLETEXTURESIZE) / (PARTICLETEXTURESIZE*0.5f-1);
+                                               d = (noise2[y][x] - 128) * 3 + 192;
+                                               if (d > 0)
+                                                       d = (int)(d * (1-(dx*dx+dy*dy)));
+                                               d = (d * noise1[y][x]) >> 7;
+                                               d = bound(0, d, 255);
+                                               data[y][x][3] = (unsigned char) d;
+                                               if (m < d)
+                                                       m = d;
+                                       }
                                }
                        }
+                       while (m < 224);
+                       setuptex(tex_smoke[i], &data[0][0][0], particletexturedata);
                }
-               while (m < 224);
-               setuptex(tex_smoke[i], &data[0][0][0], particletexturedata);
-       }
 
-       // rain splash
-       for (i = 0;i < 16;i++)
-       {
+               // rain splash
                memset(&data[0][0][0], 255, sizeof(data));
-               radius = i * 3.0f / 4.0f / 16.0f;
-               f2 = 255.0f * ((15.0f - i) / 15.0f);
                for (y = 0;y < PARTICLETEXTURESIZE;y++)
                {
                        dy = (y - 0.5f*PARTICLETEXTURESIZE) / (PARTICLETEXTURESIZE*0.5f-1);
                        for (x = 0;x < PARTICLETEXTURESIZE;x++)
                        {
                                dx = (x - 0.5f*PARTICLETEXTURESIZE) / (PARTICLETEXTURESIZE*0.5f-1);
-                               f = f2 * (1.0 - 4.0f * fabs(radius - sqrt(dx*dx+dy*dy)));
+                               f = 255.0f * (1.0 - 4.0f * fabs(10.0f - sqrt(dx*dx+dy*dy)));
                                data[y][x][3] = (int) (bound(0.0f, f, 255.0f));
                        }
                }
-               setuptex(tex_rainsplash[i], &data[0][0][0], particletexturedata);
-       }
+               setuptex(tex_rainsplash, &data[0][0][0], particletexturedata);
 
-       // normal particle
-       memset(&data[0][0][0], 255, sizeof(data));
-       for (y = 0;y < PARTICLETEXTURESIZE;y++)
-       {
-               dy = (y - 0.5f*PARTICLETEXTURESIZE) / (PARTICLETEXTURESIZE*0.5f-1);
-               for (x = 0;x < PARTICLETEXTURESIZE;x++)
+               // normal particle
+               memset(&data[0][0][0], 255, sizeof(data));
+               for (y = 0;y < PARTICLETEXTURESIZE;y++)
                {
-                       dx = (x - 0.5f*PARTICLETEXTURESIZE) / (PARTICLETEXTURESIZE*0.5f-1);
-                       d = 256 * (1 - (dx*dx+dy*dy));
-                       d = bound(0, d, 255);
-                       data[y][x][3] = (unsigned char) d;
+                       dy = (y - 0.5f*PARTICLETEXTURESIZE) / (PARTICLETEXTURESIZE*0.5f-1);
+                       for (x = 0;x < PARTICLETEXTURESIZE;x++)
+                       {
+                               dx = (x - 0.5f*PARTICLETEXTURESIZE) / (PARTICLETEXTURESIZE*0.5f-1);
+                               d = (int)(256 * (1 - (dx*dx+dy*dy)));
+                               d = bound(0, d, 255);
+                               data[y][x][3] = (unsigned char) d;
+                       }
                }
-       }
-       setuptex(tex_particle, &data[0][0][0], particletexturedata);
+               setuptex(tex_particle, &data[0][0][0], particletexturedata);
 
-       // rain
-       memset(&data[0][0][0], 255, sizeof(data));
-       light[0] = 1;light[1] = 1;light[2] = 1;
-       VectorNormalize(light);
-       for (y = 0;y < PARTICLETEXTURESIZE;y++)
-       {
-               dy = (y - 0.5f*PARTICLETEXTURESIZE) / (PARTICLETEXTURESIZE*0.5f-1);
-               // stretch upper half of bubble by +50% and shrink lower half by -50%
-               // (this gives an elongated teardrop shape)
-               if (dy > 0.5f)
-                       dy = (dy - 0.5f) * 2.0f;
-               else
-                       dy = (dy - 0.5f) / 1.5f;
-               for (x = 0;x < PARTICLETEXTURESIZE;x++)
+               // rain
+               memset(&data[0][0][0], 255, sizeof(data));
+               light[0] = 1;light[1] = 1;light[2] = 1;
+               VectorNormalize(light);
+               for (y = 0;y < PARTICLETEXTURESIZE;y++)
                {
-                       dx = (x - 0.5f*PARTICLETEXTURESIZE) / (PARTICLETEXTURESIZE*0.5f-1);
-                       // shrink bubble width to half
-                       dx *= 2.0f;
-                       data[y][x][3] = shadebubble(dx, dy, light);
+                       dy = (y - 0.5f*PARTICLETEXTURESIZE) / (PARTICLETEXTURESIZE*0.5f-1);
+                       // stretch upper half of bubble by +50% and shrink lower half by -50%
+                       // (this gives an elongated teardrop shape)
+                       if (dy > 0.5f)
+                               dy = (dy - 0.5f) * 2.0f;
+                       else
+                               dy = (dy - 0.5f) / 1.5f;
+                       for (x = 0;x < PARTICLETEXTURESIZE;x++)
+                       {
+                               dx = (x - 0.5f*PARTICLETEXTURESIZE) / (PARTICLETEXTURESIZE*0.5f-1);
+                               // shrink bubble width to half
+                               dx *= 2.0f;
+                               data[y][x][3] = shadebubble(dx, dy, light);
+                       }
                }
-       }
-       setuptex(tex_raindrop, &data[0][0][0], particletexturedata);
+               setuptex(tex_raindrop, &data[0][0][0], particletexturedata);
 
-       // bubble
-       memset(&data[0][0][0], 255, sizeof(data));
-       light[0] = 1;light[1] = 1;light[2] = 1;
-       VectorNormalize(light);
-       for (y = 0;y < PARTICLETEXTURESIZE;y++)
-       {
-               dy = (y - 0.5f*PARTICLETEXTURESIZE) / (PARTICLETEXTURESIZE*0.5f-1);
-               for (x = 0;x < PARTICLETEXTURESIZE;x++)
+               // bubble
+               memset(&data[0][0][0], 255, sizeof(data));
+               light[0] = 1;light[1] = 1;light[2] = 1;
+               VectorNormalize(light);
+               for (y = 0;y < PARTICLETEXTURESIZE;y++)
                {
-                       dx = (x - 0.5f*PARTICLETEXTURESIZE) / (PARTICLETEXTURESIZE*0.5f-1);
-                       data[y][x][3] = shadebubble(dx, dy, light);
+                       dy = (y - 0.5f*PARTICLETEXTURESIZE) / (PARTICLETEXTURESIZE*0.5f-1);
+                       for (x = 0;x < PARTICLETEXTURESIZE;x++)
+                       {
+                               dx = (x - 0.5f*PARTICLETEXTURESIZE) / (PARTICLETEXTURESIZE*0.5f-1);
+                               data[y][x][3] = shadebubble(dx, dy, light);
+                       }
                }
-       }
-       setuptex(tex_bubble, &data[0][0][0], particletexturedata);
+               setuptex(tex_bubble, &data[0][0][0], particletexturedata);
 
-       // Blood particles and blood decals
-       R_InitBloodTextures (particletexturedata);
+               // Blood particles and blood decals
+               R_InitBloodTextures (particletexturedata);
 
-       // bullet decals
-       for (i = 0;i < 8;i++)
-       {
-               memset(&data[0][0][0], 255, sizeof(data));
-               for (k = 0;k < 12;k++)
-                       particletextureblotch(&data[0][0][0], PARTICLETEXTURESIZE/16, 0, 0, 0, 128);
-               for (k = 0;k < 3;k++)
-                       particletextureblotch(&data[0][0][0], PARTICLETEXTURESIZE/2, 0, 0, 0, 160);
-               //particletextureclamp(&data[0][0][0], 64, 64, 64, 255, 255, 255);
-               particletextureinvert(&data[0][0][0]);
-               setuptex(tex_bulletdecal[i], &data[0][0][0], particletexturedata);
-       }
+               // bullet decals
+               for (i = 0;i < 8;i++)
+               {
+                       memset(&data[0][0][0], 255, sizeof(data));
+                       for (k = 0;k < 12;k++)
+                               particletextureblotch(&data[0][0][0], PARTICLETEXTURESIZE/16, 0, 0, 0, 128);
+                       for (k = 0;k < 3;k++)
+                               particletextureblotch(&data[0][0][0], PARTICLETEXTURESIZE/2, 0, 0, 0, 160);
+                       //particletextureclamp(&data[0][0][0], 64, 64, 64, 255, 255, 255);
+                       particletextureinvert(&data[0][0][0]);
+                       setuptex(tex_bulletdecal[i], &data[0][0][0], particletexturedata);
+               }
 
-#if 0
-       Image_WriteTGARGBA ("particles/particlefont.tga", PARTICLEFONTSIZE, PARTICLEFONTSIZE, particletexturedata);
+#ifdef DUMPPARTICLEFONT
+               Image_WriteTGARGBA ("particles/particlefont.tga", PARTICLEFONTSIZE, PARTICLEFONTSIZE, particletexturedata);
 #endif
 
-       particlefonttexture = loadtextureimage(particletexturepool, "particles/particlefont.tga", 0, 0, false, TEXF_ALPHA | TEXF_PRECACHE);
-       if (!particlefonttexture)
                particlefonttexture = R_LoadTexture2D(particletexturepool, "particlefont", PARTICLEFONTSIZE, PARTICLEFONTSIZE, particletexturedata, TEXTYPE_RGBA, TEXF_ALPHA | TEXF_PRECACHE, NULL);
+
+               Mem_Free(particletexturedata);
+       }
        for (i = 0;i < MAX_PARTICLETEXTURES;i++)
+       {
+               int basex = ((i >> 0) & 7) * PARTICLETEXTURESIZE;
+               int basey = ((i >> 3) & 7) * PARTICLETEXTURESIZE;
                particletexture[i].texture = particlefonttexture;
+               particletexture[i].s1 = (basex + 1) / (float)PARTICLEFONTSIZE;
+               particletexture[i].t1 = (basey + 1) / (float)PARTICLEFONTSIZE;
+               particletexture[i].s2 = (basex + PARTICLETEXTURESIZE - 1) / (float)PARTICLEFONTSIZE;
+               particletexture[i].t2 = (basey + PARTICLETEXTURESIZE - 1) / (float)PARTICLEFONTSIZE;
+       }
 
-       // nexbeam
-       fractalnoise(&noise3[0][0], 64, 4);
-       m = 0;
-       for (y = 0;y < 64;y++)
+#ifndef DUMPPARTICLEFONT
+       particletexture[tex_beam].texture = loadtextureimage(particletexturepool, "particles/nexbeam.tga", 0, 0, false, TEXF_ALPHA | TEXF_PRECACHE);
+       if (!particletexture[tex_beam].texture)
+#endif
        {
-               dy = (y - 0.5f*64) / (64*0.5f-1);
-               for (x = 0;x < 16;x++)
+               unsigned char noise3[64][64], data2[64][16][4];
+               // nexbeam
+               fractalnoise(&noise3[0][0], 64, 4);
+               m = 0;
+               for (y = 0;y < 64;y++)
                {
-                       dx = (x - 0.5f*16) / (16*0.5f-2);
-                       d = (1 - sqrt(fabs(dx))) * noise3[y][x];
-                       data2[y][x][0] = data2[y][x][1] = data2[y][x][2] = (unsigned char) bound(0, d, 255);
-                       data2[y][x][3] = 255;
+                       dy = (y - 0.5f*64) / (64*0.5f-1);
+                       for (x = 0;x < 16;x++)
+                       {
+                               dx = (x - 0.5f*16) / (16*0.5f-2);
+                               d = (int)((1 - sqrt(fabs(dx))) * noise3[y][x]);
+                               data2[y][x][0] = data2[y][x][1] = data2[y][x][2] = (unsigned char) bound(0, d, 255);
+                               data2[y][x][3] = 255;
+                       }
                }
-       }
 
-#if 0
-       Image_WriteTGARGBA ("particles/nexbeam.tga", 64, 64, &data2[0][0][0]);
+#ifdef DUMPPARTICLEFONT
+               Image_WriteTGARGBA ("particles/nexbeam.tga", 64, 64, &data2[0][0][0]);
 #endif
-
-       particletexture[tex_beam].texture = loadtextureimage(particletexturepool, "particles/nexbeam.tga", 0, 0, false, TEXF_ALPHA | TEXF_PRECACHE);
-       if (!particletexture[tex_beam].texture)
                particletexture[tex_beam].texture = R_LoadTexture2D(particletexturepool, "nexbeam", 16, 64, &data2[0][0][0], TEXTYPE_RGBA, TEXF_PRECACHE, NULL);
+       }
        particletexture[tex_beam].s1 = 0;
        particletexture[tex_beam].t1 = 0;
        particletexture[tex_beam].s2 = 1;
        particletexture[tex_beam].t2 = 1;
-       Mem_Free(particletexturedata);
 }
 
 static void r_part_start(void)
 {
        particletexturepool = R_AllocTexturePool();
        R_InitParticleTexture ();
+       CL_Particles_LoadEffectInfo();
 }
 
 static void r_part_shutdown(void)
@@ -1711,89 +2093,126 @@ static void r_part_newmap(void)
 {
 }
 
+#define BATCHSIZE 256
+int particle_element3i[BATCHSIZE*6];
+float particle_vertex3f[BATCHSIZE*12], particle_texcoord2f[BATCHSIZE*8], particle_color4f[BATCHSIZE*16];
+
 void R_Particles_Init (void)
 {
+       int i;
+       for (i = 0;i < BATCHSIZE;i++)
+       {
+               particle_element3i[i*6+0] = i*4+0;
+               particle_element3i[i*6+1] = i*4+1;
+               particle_element3i[i*6+2] = i*4+2;
+               particle_element3i[i*6+3] = i*4+0;
+               particle_element3i[i*6+4] = i*4+2;
+               particle_element3i[i*6+5] = i*4+3;
+       }
+
        Cvar_RegisterVariable(&r_drawparticles);
        R_RegisterModule("R_Particles", r_part_start, r_part_shutdown, r_part_newmap);
 }
 
-float particle_vertex3f[12], particle_texcoord2f[8];
-
-void R_DrawParticle_TransparentCallback(const entity_render_t *ent, int surfacenumber, const rtlight_t *rtlight)
+void R_DrawParticle_TransparentCallback(const entity_render_t *ent, const rtlight_t *rtlight, int numsurfaces, int *surfacelist)
 {
-       const particle_t *p = cl.particles + surfacenumber;
-       rmeshstate_t m;
+       int surfacelistindex;
+       int batchstart, batchcount;
+       const particle_t *p;
        pblend_t blendmode;
-       float org[3], up2[3], v[3], right[3], up[3], fog, ifog, cr, cg, cb, ca, size;
-       particletexture_t *tex;
-
-       VectorCopy(p->org, org);
-
-       blendmode = p->type->blendmode;
-       tex = &particletexture[p->texnum];
-       cr = p->color[0] * (1.0f / 255.0f);
-       cg = p->color[1] * (1.0f / 255.0f);
-       cb = p->color[2] * (1.0f / 255.0f);
-       ca = p->alpha * (1.0f / 255.0f);
-       if (blendmode == PBLEND_MOD)
-       {
-               cr *= ca;
-               cg *= ca;
-               cb *= ca;
-               cr = min(cr, 1);
-               cg = min(cg, 1);
-               cb = min(cb, 1);
-               ca = 1;
-       }
-       ca /= cl_particles_quality.value;
-       if (p->type->lighting)
-       {
-               float ambient[3], diffuse[3], diffusenormal[3];
-               R_CompleteLightPoint(ambient, diffuse, diffusenormal, org, true);
-               cr *= (ambient[0] + 0.5 * diffuse[0]);
-               cg *= (ambient[1] + 0.5 * diffuse[1]);
-               cb *= (ambient[2] + 0.5 * diffuse[2]);
-       }
-       if (fogenabled)
-       {
-               fog = VERTEXFOGTABLE(VectorDistance(org, r_vieworigin));
-               ifog = 1 - fog;
-               cr = cr * ifog;
-               cg = cg * ifog;
-               cb = cb * ifog;
-               if (blendmode == PBLEND_ALPHA)
-               {
-                       cr += fogcolor[0] * fog;
-                       cg += fogcolor[1] * fog;
-                       cb += fogcolor[2] * fog;
-               }
-       }
+       rtexture_t *texture;
+       float *v3f, *t2f, *c4f;
 
        R_Mesh_Matrix(&identitymatrix);
-
-       memset(&m, 0, sizeof(m));
-       m.tex[0] = R_GetTexture(tex->texture);
-       m.pointer_texcoord[0] = particle_texcoord2f;
-       m.pointer_vertex = particle_vertex3f;
-       R_Mesh_State(&m);
-
-       GL_Color(cr, cg, cb, ca);
-
-       if (blendmode == PBLEND_ALPHA)
-               GL_BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
-       else if (blendmode == PBLEND_ADD)
-               GL_BlendFunc(GL_SRC_ALPHA, GL_ONE);
-       else //if (blendmode == PBLEND_MOD)
-               GL_BlendFunc(GL_ZERO, GL_ONE_MINUS_SRC_COLOR);
+       R_Mesh_ResetTextureState();
+       R_Mesh_VertexPointer(particle_vertex3f, 0, 0);
+       R_Mesh_TexCoordPointer(0, 2, particle_texcoord2f, 0, 0);
+       R_Mesh_ColorPointer(particle_color4f, 0, 0);
        GL_DepthMask(false);
        GL_DepthTest(true);
-       size = p->size * cl_particles_size.value;
-       if (p->type->orientation == PARTICLE_BILLBOARD || p->type->orientation == PARTICLE_ORIENTED_DOUBLESIDED)
+       GL_CullFace(GL_FRONT); // quake is backwards, this culls back faces
+
+       // first generate all the vertices at once
+       for (surfacelistindex = 0, v3f = particle_vertex3f, t2f = particle_texcoord2f, c4f = particle_color4f;surfacelistindex < numsurfaces;surfacelistindex++, v3f += 3*4, t2f += 2*4, c4f += 4*4)
        {
-               if (p->type->orientation == PARTICLE_ORIENTED_DOUBLESIDED)
+               particletexture_t *tex;
+               const float *org;
+               float up2[3], v[3], right[3], up[3], fog, ifog, cr, cg, cb, ca, size;
+
+               p = cl.particles + surfacelist[surfacelistindex];
+
+               blendmode = p->type->blendmode;
+
+               cr = p->color[0] * (1.0f / 255.0f) * r_view.colorscale;
+               cg = p->color[1] * (1.0f / 255.0f) * r_view.colorscale;
+               cb = p->color[2] * (1.0f / 255.0f) * r_view.colorscale;
+               ca = p->alpha * (1.0f / 255.0f);
+               if (blendmode == PBLEND_MOD)
+               {
+                       cr *= ca;
+                       cg *= ca;
+                       cb *= ca;
+                       cr = min(cr, 1);
+                       cg = min(cg, 1);
+                       cb = min(cb, 1);
+                       ca = 1;
+               }
+               ca /= cl_particles_quality.value;
+               if (p->type->lighting)
+               {
+                       float ambient[3], diffuse[3], diffusenormal[3];
+                       R_CompleteLightPoint(ambient, diffuse, diffusenormal, p->org, true);
+                       cr *= (ambient[0] + 0.5 * diffuse[0]);
+                       cg *= (ambient[1] + 0.5 * diffuse[1]);
+                       cb *= (ambient[2] + 0.5 * diffuse[2]);
+               }
+               if (r_refdef.fogenabled)
+               {
+                       fog = VERTEXFOGTABLE(VectorDistance(p->org, r_view.origin));
+                       ifog = 1 - fog;
+                       cr = cr * ifog;
+                       cg = cg * ifog;
+                       cb = cb * ifog;
+                       if (blendmode == PBLEND_ALPHA)
+                       {
+                               cr += r_refdef.fogcolor[0] * fog * r_view.colorscale;
+                               cg += r_refdef.fogcolor[1] * fog * r_view.colorscale;
+                               cb += r_refdef.fogcolor[2] * fog * r_view.colorscale;
+                       }
+               }
+               c4f[0] = c4f[4] = c4f[8] = c4f[12] = cr;
+               c4f[1] = c4f[5] = c4f[9] = c4f[13] = cg;
+               c4f[2] = c4f[6] = c4f[10] = c4f[14] = cb;
+               c4f[3] = c4f[7] = c4f[11] = c4f[15] = ca;
+
+               size = p->size * cl_particles_size.value;
+               org = p->org;
+               tex = &particletexture[p->texnum];
+               if (p->type->orientation == PARTICLE_BILLBOARD)
+               {
+                       VectorScale(r_view.left, -size, right);
+                       VectorScale(r_view.up, size, up);
+                       v3f[ 0] = org[0] - right[0] - up[0];
+                       v3f[ 1] = org[1] - right[1] - up[1];
+                       v3f[ 2] = org[2] - right[2] - up[2];
+                       v3f[ 3] = org[0] - right[0] + up[0];
+                       v3f[ 4] = org[1] - right[1] + up[1];
+                       v3f[ 5] = org[2] - right[2] + up[2];
+                       v3f[ 6] = org[0] + right[0] + up[0];
+                       v3f[ 7] = org[1] + right[1] + up[1];
+                       v3f[ 8] = org[2] + right[2] + up[2];
+                       v3f[ 9] = org[0] + right[0] - up[0];
+                       v3f[10] = org[1] + right[1] - up[1];
+                       v3f[11] = org[2] + right[2] - up[2];
+                       t2f[0] = tex->s1;t2f[1] = tex->t2;
+                       t2f[2] = tex->s1;t2f[3] = tex->t1;
+                       t2f[4] = tex->s2;t2f[5] = tex->t1;
+                       t2f[6] = tex->s2;t2f[7] = tex->t2;
+               }
+               else if (p->type->orientation == PARTICLE_ORIENTED_DOUBLESIDED)
                {
                        // double-sided
-                       if (DotProduct(p->vel, r_vieworigin) > DotProduct(p->vel, org))
+                       if (DotProduct(p->vel, r_view.origin) > DotProduct(p->vel, org))
                        {
                                VectorNegate(p->vel, v);
                                VectorVectors(v, right, up);
@@ -1802,58 +2221,93 @@ void R_DrawParticle_TransparentCallback(const entity_render_t *ent, int surfacen
                                VectorVectors(p->vel, right, up);
                        VectorScale(right, size, right);
                        VectorScale(up, size, up);
+                       v3f[ 0] = org[0] - right[0] - up[0];
+                       v3f[ 1] = org[1] - right[1] - up[1];
+                       v3f[ 2] = org[2] - right[2] - up[2];
+                       v3f[ 3] = org[0] - right[0] + up[0];
+                       v3f[ 4] = org[1] - right[1] + up[1];
+                       v3f[ 5] = org[2] - right[2] + up[2];
+                       v3f[ 6] = org[0] + right[0] + up[0];
+                       v3f[ 7] = org[1] + right[1] + up[1];
+                       v3f[ 8] = org[2] + right[2] + up[2];
+                       v3f[ 9] = org[0] + right[0] - up[0];
+                       v3f[10] = org[1] + right[1] - up[1];
+                       v3f[11] = org[2] + right[2] - up[2];
+                       t2f[0] = tex->s1;t2f[1] = tex->t2;
+                       t2f[2] = tex->s1;t2f[3] = tex->t1;
+                       t2f[4] = tex->s2;t2f[5] = tex->t1;
+                       t2f[6] = tex->s2;t2f[7] = tex->t2;
+               }
+               else if (p->type->orientation == PARTICLE_SPARK)
+               {
+                       VectorMA(org, -0.02, p->vel, v);
+                       VectorMA(org, 0.02, p->vel, up2);
+                       R_CalcBeam_Vertex3f(v3f, v, up2, size);
+                       t2f[0] = tex->s1;t2f[1] = tex->t2;
+                       t2f[2] = tex->s1;t2f[3] = tex->t1;
+                       t2f[4] = tex->s2;t2f[5] = tex->t1;
+                       t2f[6] = tex->s2;t2f[7] = tex->t2;
+               }
+               else if (p->type->orientation == PARTICLE_BEAM)
+               {
+                       R_CalcBeam_Vertex3f(v3f, org, p->vel, size);
+                       VectorSubtract(p->vel, org, up);
+                       VectorNormalize(up);
+                       v[0] = DotProduct(org, up) * (1.0f / 64.0f);
+                       v[1] = DotProduct(p->vel, up) * (1.0f / 64.0f);
+                       t2f[0] = 1;t2f[1] = v[0];
+                       t2f[2] = 0;t2f[3] = v[0];
+                       t2f[4] = 0;t2f[5] = v[1];
+                       t2f[6] = 1;t2f[7] = v[1];
                }
                else
                {
-                       VectorScale(r_viewleft, -size, right);
-                       VectorScale(r_viewup, size, up);
+                       Con_Printf("R_DrawParticles: unknown particle orientation %i\n", p->type->orientation);
+                       return;
                }
-               particle_vertex3f[ 0] = org[0] - right[0] - up[0];
-               particle_vertex3f[ 1] = org[1] - right[1] - up[1];
-               particle_vertex3f[ 2] = org[2] - right[2] - up[2];
-               particle_vertex3f[ 3] = org[0] - right[0] + up[0];
-               particle_vertex3f[ 4] = org[1] - right[1] + up[1];
-               particle_vertex3f[ 5] = org[2] - right[2] + up[2];
-               particle_vertex3f[ 6] = org[0] + right[0] + up[0];
-               particle_vertex3f[ 7] = org[1] + right[1] + up[1];
-               particle_vertex3f[ 8] = org[2] + right[2] + up[2];
-               particle_vertex3f[ 9] = org[0] + right[0] - up[0];
-               particle_vertex3f[10] = org[1] + right[1] - up[1];
-               particle_vertex3f[11] = org[2] + right[2] - up[2];
-               particle_texcoord2f[0] = tex->s1;particle_texcoord2f[1] = tex->t2;
-               particle_texcoord2f[2] = tex->s1;particle_texcoord2f[3] = tex->t1;
-               particle_texcoord2f[4] = tex->s2;particle_texcoord2f[5] = tex->t1;
-               particle_texcoord2f[6] = tex->s2;particle_texcoord2f[7] = tex->t2;
-       }
-       else if (p->type->orientation == PARTICLE_SPARK)
-       {
-               VectorMA(p->org, -0.02, p->vel, v);
-               VectorMA(p->org, 0.02, p->vel, up2);
-               R_CalcBeam_Vertex3f(particle_vertex3f, v, up2, size);
-               particle_texcoord2f[0] = tex->s1;particle_texcoord2f[1] = tex->t2;
-               particle_texcoord2f[2] = tex->s1;particle_texcoord2f[3] = tex->t1;
-               particle_texcoord2f[4] = tex->s2;particle_texcoord2f[5] = tex->t1;
-               particle_texcoord2f[6] = tex->s2;particle_texcoord2f[7] = tex->t2;
-       }
-       else if (p->type->orientation == PARTICLE_BEAM)
-       {
-               R_CalcBeam_Vertex3f(particle_vertex3f, p->org, p->vel, size);
-               VectorSubtract(p->vel, p->org, up);
-               VectorNormalize(up);
-               v[0] = DotProduct(p->org, up) * (1.0f / 64.0f);
-               v[1] = DotProduct(p->vel, up) * (1.0f / 64.0f);
-               particle_texcoord2f[0] = 1;particle_texcoord2f[1] = v[0];
-               particle_texcoord2f[2] = 0;particle_texcoord2f[3] = v[0];
-               particle_texcoord2f[4] = 0;particle_texcoord2f[5] = v[1];
-               particle_texcoord2f[6] = 1;particle_texcoord2f[7] = v[1];
        }
-       else
+
+       // now render batches of particles based on blendmode and texture
+       blendmode = PBLEND_ADD;
+       GL_BlendFunc(GL_SRC_ALPHA, GL_ONE);
+       texture = particletexture[63].texture;
+       R_Mesh_TexBind(0, R_GetTexture(texture));
+       GL_LockArrays(0, numsurfaces*4);
+       batchstart = 0;
+       batchcount = 0;
+       for (surfacelistindex = 0;surfacelistindex < numsurfaces;surfacelistindex++)
        {
-               Con_Printf("R_DrawParticles: unknown particle orientation %i\n", p->type->orientation);
-               return;
-       }
+               p = cl.particles + surfacelist[surfacelistindex];
+
+               if (blendmode != p->type->blendmode)
+               {
+                       if (batchcount > 0)
+                               R_Mesh_Draw(batchstart * 4, batchcount * 4, batchcount * 2, particle_element3i + batchstart * 6, 0, 0);
+                       batchcount = 0;
+                       batchstart = surfacelistindex;
+                       blendmode = p->type->blendmode;
+                       if (blendmode == PBLEND_ALPHA)
+                               GL_BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+                       else if (blendmode == PBLEND_ADD)
+                               GL_BlendFunc(GL_SRC_ALPHA, GL_ONE);
+                       else //if (blendmode == PBLEND_MOD)
+                               GL_BlendFunc(GL_ZERO, GL_ONE_MINUS_SRC_COLOR);
+               }
+               if (texture != particletexture[p->texnum].texture)
+               {
+                       if (batchcount > 0)
+                               R_Mesh_Draw(batchstart * 4, batchcount * 4, batchcount * 2, particle_element3i + batchstart * 6, 0, 0);
+                       batchcount = 0;
+                       batchstart = surfacelistindex;
+                       texture = particletexture[p->texnum].texture;
+                       R_Mesh_TexBind(0, R_GetTexture(texture));
+               }
 
-       R_Mesh_Draw(0, 4, 2, polygonelements);
+               batchcount++;
+       }
+       if (batchcount > 0)
+               R_Mesh_Draw(batchstart * 4, batchcount * 4, batchcount * 2, particle_element3i + batchstart * 6, 0, 0);
+       GL_LockArrays(0, 0);
 }
 
 void R_DrawParticles (void)
@@ -1866,21 +2320,16 @@ void R_DrawParticles (void)
        if ((!cl.num_particles) || (!r_drawparticles.integer))
                return;
 
-       minparticledist = DotProduct(r_vieworigin, r_viewforward) + 4.0f;
+       minparticledist = DotProduct(r_view.origin, r_view.forward) + 4.0f;
 
        // LordHavoc: only render if not too close
        for (i = 0, p = cl.particles;i < cl.num_particles;i++, p++)
        {
                if (p->type)
                {
-                       renderstats.particles++;
-                       if (DotProduct(p->org, r_viewforward) >= minparticledist || p->type->orientation == PARTICLE_BEAM)
-                       {
-                               if (p->type == particletype + pt_decal)
-                                       R_DrawParticle_TransparentCallback(0, i, 0);
-                               else
-                                       R_MeshQueue_AddTransparent(p->org, R_DrawParticle_TransparentCallback, NULL, i, NULL);
-                       }
+                       r_refdef.stats.particles++;
+                       if (DotProduct(p->org, r_view.forward) >= minparticledist || p->type->orientation == PARTICLE_BEAM)
+                               R_MeshQueue_AddTransparent(p->org, R_DrawParticle_TransparentCallback, NULL, i, NULL);
                }
        }
 }