/* cl_temp_entities.c Client side temporary entity management Copyright (C) 1996-1997 Id Software, Inc. Copyright (C) 2021 Bill Currie Author: Bill Currie Date: 2021/3/10 This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to: Free Software Foundation, Inc. 59 Temple Place - Suite 330 Boston, MA 02111-1307, USA */ #ifdef HAVE_CONFIG_H # include "config.h" #endif #ifdef HAVE_STRING_H # include #endif #ifdef HAVE_STRINGS_H # include #endif #include "QF/msg.h" #include "QF/progs.h" // for PR_RESMAP #include "QF/quakefs.h" #include "QF/render.h" #include "QF/sound.h" #include "QF/plugin/vid_render.h" //FIXME #include "QF/scene/entity.h" #include "QF/scene/scene.h" #include "client/effects.h" #include "client/entities.h" #include "client/particles.h" #include "client/temp_entities.h" #include "client/world.h" typedef struct tent_s { struct tent_s *next; entity_t *ent; } tent_t; typedef struct { int entity; struct model_s *model; float endtime; vec4f_t start, end; vec4f_t rotation; tent_t *tents; int seed; } beam_t; #define BEAM_SEED_INTERVAL 72 #define BEAM_SEED_PRIME 3191 typedef struct { float start; tent_t *tent; } explosion_t; typedef struct tent_obj_s { struct tent_obj_s *next; union { beam_t beam; explosion_t ex; } to; } tent_obj_t; static PR_RESMAP (tent_t) temp_entities; static PR_RESMAP (tent_obj_t) tent_objects; static tent_obj_t *cl_beams; static tent_obj_t *cl_explosions; static tent_t *cl_projectiles; static sfx_t *cl_sfx_wizhit; static sfx_t *cl_sfx_knighthit; static sfx_t *cl_sfx_tink1; static sfx_t *cl_sfx_r_exp3; static sfx_t *cl_sfx_ric[4]; static model_t *cl_mod_beam; static model_t *cl_mod_bolt; static model_t *cl_mod_bolt2; static model_t *cl_mod_bolt3; static model_t *cl_spr_explod; static model_t *cl_spike; static vec4f_t beam_rolls[360]; static void CL_TEnts_Precache (int phase) { if (!phase) { return; } cl_sfx_wizhit = S_PrecacheSound ("wizard/hit.wav"); cl_sfx_knighthit = S_PrecacheSound ("hknight/hit.wav"); cl_sfx_tink1 = S_PrecacheSound ("weapons/tink1.wav"); cl_sfx_ric[3] = S_PrecacheSound ("weapons/ric1.wav"); cl_sfx_ric[2] = S_PrecacheSound ("weapons/ric2.wav"); cl_sfx_ric[1] = S_PrecacheSound ("weapons/ric3.wav"); cl_sfx_ric[0] = cl_sfx_ric[1]; cl_sfx_r_exp3 = S_PrecacheSound ("weapons/r_exp3.wav"); cl_mod_bolt = Mod_ForName ("progs/bolt.mdl", true); cl_mod_bolt2 = Mod_ForName ("progs/bolt2.mdl", true); cl_mod_bolt3 = Mod_ForName ("progs/bolt3.mdl", true); cl_spr_explod = Mod_ForName ("progs/s_explod.spr", true); cl_mod_beam = Mod_ForName ("progs/beam.mdl", false); cl_spike = Mod_ForName ("progs/spike.mdl", false); if (!cl_mod_beam) { cl_mod_beam = cl_mod_bolt; } } void CL_TEnts_Init (void) { QFS_GamedirCallback (CL_TEnts_Precache); CL_TEnts_Precache (1); for (int i = 0; i < 360; i++) { float ang = i * M_PI / 360; beam_rolls[i] = (vec4f_t) { sin (ang), 0, 0, cos (ang) }; } } void CL_Init_Entity (entity_t *ent) { memset (&ent->animation, 0, sizeof (ent->animation)); memset (&ent->visibility, 0, sizeof (ent->visibility)); memset (&ent->renderer, 0, sizeof (ent->renderer)); ent->active = 1; ent->old_origin = (vec4f_t) {}; ent->renderer.skin = 0; QuatSet (1.0, 1.0, 1.0, 1.0, ent->renderer.colormod); ent->animation.pose1 = ent->animation.pose2 = -1; } static tent_t * new_temp_entity (void) { tent_t *tent = PR_RESNEW_NC (temp_entities); tent->ent = Scene_CreateEntity (cl_world.scene); tent->next = 0; CL_Init_Entity (tent->ent); return tent; } static void free_temp_entities (tent_t *tents) { tent_t **t = &tents; while (*t) { Scene_DestroyEntity (cl_world.scene, (*t)->ent);//FIXME reuse? t = &(*t)->next; } *t = temp_entities._free; temp_entities._free = tents; } static tent_obj_t * new_tent_object (void) { tent_obj_t *tobj = PR_RESNEW_NC (tent_objects); tobj->next = 0; return tobj; } static void free_tent_object (tent_obj_t *tobj) { tobj->next = tent_objects._free; tent_objects._free = tobj; } void CL_ClearTEnts (void) { PR_RESRESET (temp_entities); PR_RESRESET (tent_objects); cl_beams = 0; cl_explosions = 0; } static inline void beam_clear (beam_t *b) { if (b->tents) { tent_t *t; for (t = b->tents; t; t = t->next) { R_RemoveEfrags (t->ent); t->ent->visibility.efrag = 0; } free_temp_entities (b->tents); b->tents = 0; } } static inline void beam_setup (beam_t *b, qboolean transform, double time, TEntContext_t *ctx) { tent_t *tent; float d; int ent_count; vec4f_t dist, org; vec4f_t rotation; vec4f_t scale = { 1, 1, 1, 1 }; unsigned seed; // calculate pitch and yaw dist = b->end - b->start; // FIXME interpolation may be off when passing through the -x axis if (dist[0] < 0 && dist[1] == 0 && dist[2] == 0) { // anti-parallel with the +x axis, so assome 180 degree rotation around // the z-axis rotation = (vec4f_t) { 0, 0, 1, 0 }; } else { rotation = qrotf ((vec4f_t) { 1, 0, 0, 0 }, dist); } b->rotation = rotation; // add new entities for the lightning org = b->start; d = magnitudef (dist)[0]; dist = normalf (dist) * 30; ent_count = ceil (d / 30); d = 0; seed = b->seed + ((int) (time * BEAM_SEED_INTERVAL) % BEAM_SEED_INTERVAL); while (ent_count--) { tent = new_temp_entity (); tent->next = b->tents; b->tents = tent; vec4f_t position = org + d * dist; d += 1.0; tent->ent->renderer.model = b->model; if (transform) { seed = seed * BEAM_SEED_PRIME; Transform_SetLocalTransform (tent->ent->transform, scale, qmulf (rotation, beam_rolls[seed % 360]), position); } else { Transform_SetLocalPosition (tent->ent->transform, position); } R_AddEfrags (&cl_world.worldmodel->brush, tent->ent); } } static void CL_ParseBeam (qmsg_t *net_message, model_t *m, double time, TEntContext_t *ctx) { tent_obj_t *to; beam_t *b; int ent; vec4f_t start, end; ent = MSG_ReadShort (net_message); MSG_ReadCoordV (net_message, &start[0]);//FIXME MSG_ReadCoordV (net_message, &end[0]);//FIXME start[3] = end[3] = 1;//FIXME to = 0; if (ent) { for (to = cl_beams; to; to = to->next) { if (to->to.beam.entity == ent) { break; } } } if (!to) { to = new_tent_object (); to->next = cl_beams; cl_beams = to; to->to.beam.tents = 0; to->to.beam.entity = ent; } b = &to->to.beam; beam_clear (b); b->model = m; b->endtime = time + 0.2; b->seed = rand (); b->end = end; if (b->entity != ctx->playerEntity) { // this will be done in CL_UpdateBeams b->start = start; beam_setup (b, true, time, ctx); } } static void parse_tent (qmsg_t *net_message, double time, TEntContext_t *ctx, TE_Effect type) { dlight_t *dl; tent_obj_t *to; explosion_t *ex; int colorStart, colorLength; quat_t color; vec4f_t position = {0, 0, 0, 1}; int count; const char *name; switch (type) { case TE_NoEffect: // invalid mapping, can't do anything break; case TE_Beam: CL_ParseBeam (net_message, cl_mod_beam, time, ctx); break; case TE_Blood: count = MSG_ReadByte (net_message) * 20; MSG_ReadCoordV (net_message, &position[0]); clp_funcs->BloodPuffEffect (position, count); break; case TE_Explosion: MSG_ReadCoordV (net_message, &position[0]); // particles clp_funcs->ParticleExplosion (position); // light dl = R_AllocDlight (0); if (dl) { VectorCopy (position, dl->origin); dl->radius = 350; dl->die = time + 0.5; dl->decay = 300; QuatSet (0.86, 0.31, 0.24, 0.7, dl->color); //FIXME? nq: QuatSet (1.0, 0.5, 0.25, 0.7, dl->color); } // sound S_StartSound (-1, 0, cl_sfx_r_exp3, &position[0], 1, 1); // sprite to = new_tent_object (); to->next = cl_explosions; cl_explosions = to; ex = &to->to.ex; ex->tent = new_temp_entity (); ex->start = time; //FIXME need better model management if (!cl_spr_explod->cache.data) { cl_spr_explod = Mod_ForName ("progs/s_explod.spr", true); } ex->tent->ent->renderer.model = cl_spr_explod; Transform_SetLocalPosition (ex->tent->ent->transform,//FIXME (vec4f_t) {VectorExpand (position), 1}); break; case TE_Explosion2: MSG_ReadCoordV (net_message, &position[0]); colorStart = MSG_ReadByte (net_message); colorLength = MSG_ReadByte (net_message); S_StartSound (-1, 0, cl_sfx_r_exp3, &position[0], 1, 1); clp_funcs->ParticleExplosion2 (position, colorStart, colorLength); dl = R_AllocDlight (0); if (!dl) break; VectorCopy (position, dl->origin); dl->radius = 350; dl->die = time + 0.5; dl->decay = 300; colorStart = (colorStart + (rand () % colorLength)) * 3; VectorScale (&r_data->vid->palette[colorStart], 1.0 / 255.0, dl->color); dl->color[3] = 0.7; break; case TE_Explosion3: MSG_ReadCoordV (net_message, &position[0]); MSG_ReadCoordV (net_message, color); // OUCH! color[3] = 0.7; clp_funcs->ParticleExplosion (position); S_StartSound (-1, 0, cl_sfx_r_exp3, &position[0], 1, 1); dl = R_AllocDlight (0); if (dl) { VectorCopy (position, dl->origin); dl->radius = 350; dl->die = time + 0.5; dl->decay = 300; QuatCopy (color, dl->color); } break; case TE_Gunshot1: MSG_ReadCoordV (net_message, &position[0]); clp_funcs->GunshotEffect (position, 20); break; case TE_Gunshot2: count = MSG_ReadByte (net_message) * 20; MSG_ReadCoordV (net_message, &position[0]); clp_funcs->GunshotEffect (position, count); break; case TE_KnightSpike: MSG_ReadCoordV (net_message, &position[0]); clp_funcs->KnightSpikeEffect (position); S_StartSound (-1, 0, cl_sfx_knighthit, &position[0], 1, 1); break; case TE_LavaSplash: MSG_ReadCoordV (net_message, &position[0]); clp_funcs->LavaSplash (position); break; case TE_Lightning1: CL_ParseBeam (net_message, cl_mod_bolt, time, ctx); break; case TE_Lightning2: CL_ParseBeam (net_message, cl_mod_bolt2, time, ctx); break; case TE_Lightning3: CL_ParseBeam (net_message, cl_mod_bolt3, time, ctx); break; case TE_Lightning4: name = MSG_ReadString (net_message); CL_ParseBeam (net_message, Mod_ForName (name, true), time, ctx); break; case TE_LightningBlood: MSG_ReadCoordV (net_message, &position[0]); // light dl = R_AllocDlight (0); if (dl) { VectorCopy (position, dl->origin); dl->radius = 150; dl->die = time + 0.1; dl->decay = 200; QuatSet (0.25, 0.40, 0.65, 1, dl->color); } clp_funcs->LightningBloodEffect (position); break; case TE_Spike: MSG_ReadCoordV (net_message, &position[0]); clp_funcs->SpikeEffect (position); { int i; sfx_t *sound; i = (rand () % 20) - 16; if (i >= 0) { sound = cl_sfx_ric[i]; } else { sound = cl_sfx_tink1; } S_StartSound (-1, 0, sound, &position[0], 1, 1); } break; case TE_SuperSpike: MSG_ReadCoordV (net_message, &position[0]); clp_funcs->SuperSpikeEffect (position); { int i; sfx_t *sound; i = (rand () % 20) - 16; if (i >= 0) { sound = cl_sfx_ric[i]; } else { sound = cl_sfx_tink1; } S_StartSound (-1, 0, sound, &position[0], 1, 1); } break; case TE_TarExplosion: MSG_ReadCoordV (net_message, &position[0]); clp_funcs->BlobExplosion (position); S_StartSound (-1, 0, cl_sfx_r_exp3, &position[0], 1, 1); break; case TE_Teleport: MSG_ReadCoordV (net_message, &position[0]); clp_funcs->TeleportSplash (position); break; case TE_WizSpike: MSG_ReadCoordV (net_message, &position[0]); clp_funcs->WizSpikeEffect (position); S_StartSound (-1, 0, cl_sfx_wizhit, &position[0], 1, 1); break; } } // the effect type is a byte so a max of 256 values static const TE_Effect nqEffects[256] = { [TE_nqSpike] = TE_Spike, [TE_nqSuperSpike] = TE_SuperSpike, [TE_nqGunshot] = TE_Gunshot1, [TE_nqExplosion] = TE_Explosion, [TE_nqTarExplosion] = TE_TarExplosion, [TE_nqLightning1] = TE_Lightning1, [TE_nqLightning2] = TE_Lightning2, [TE_nqWizSpike] = TE_WizSpike, [TE_nqKnightSpike] = TE_KnightSpike, [TE_nqLightning3] = TE_Lightning3, [TE_nqLavaSplash] = TE_LavaSplash, [TE_nqTeleport] = TE_Teleport, [TE_nqExplosion2] = TE_Explosion2, [TE_nqBeam] = TE_Beam, [TE_nqExplosion3] = TE_Explosion3, [TE_nqLightning4] = TE_Lightning4, }; void CL_ParseTEnt_nq (qmsg_t *net_message, double time, TEntContext_t *ctx) { byte type = MSG_ReadByte (net_message); parse_tent (net_message, time, ctx, nqEffects[type]); } // the effect type is a byte so a max of 256 values static const TE_Effect qwEffects[256] = { [TE_qwSpike] = TE_Spike, [TE_qwSuperSpike] = TE_SuperSpike, [TE_qwGunshot] = TE_Gunshot2, [TE_qwExplosion] = TE_Explosion, [TE_qwTarExplosion] = TE_TarExplosion, [TE_qwLightning1] = TE_Lightning1, [TE_qwLightning2] = TE_Lightning2, [TE_qwWizSpike] = TE_WizSpike, [TE_qwKnightSpike] = TE_KnightSpike, [TE_qwLightning3] = TE_Lightning3, [TE_qwLavaSplash] = TE_LavaSplash, [TE_qwTeleport] = TE_Teleport, [TE_qwBlood] = TE_Blood, [TE_qwLightningBlood] = TE_LightningBlood, [TE_qwExplosion2] = TE_Explosion2, [TE_qwBeam] = TE_Beam, }; void CL_ParseTEnt_qw (qmsg_t *net_message, double time, TEntContext_t *ctx) { byte type = MSG_ReadByte (net_message); parse_tent (net_message, time, ctx, qwEffects[type]); } static void CL_UpdateBeams (double time, TEntContext_t *ctx) { tent_obj_t **to; beam_t *b; unsigned seed; tent_t *t; // update lightning for (to = &cl_beams; *to; ) { b = &(*to)->to.beam; if (!b->endtime) continue; if (!b->model || b->endtime < time) { tent_obj_t *_to; b->endtime = 0; beam_clear (b); _to = *to; *to = _to->next; free_tent_object (_to); continue; } to = &(*to)->next; // if coming from the player, update the start position if (b->entity == ctx->playerEntity) { beam_clear (b); b->start = ctx->simorg; beam_setup (b, false, time, ctx); } seed = b->seed + ((int) (time * BEAM_SEED_INTERVAL) % BEAM_SEED_INTERVAL); // add new entities for the lightning for (t = b->tents; t; t = t->next) { seed = seed * BEAM_SEED_PRIME; Transform_SetLocalRotation (t->ent->transform, qmulf (b->rotation, beam_rolls[seed % 360])); } } } static void CL_UpdateExplosions (double time, TEntContext_t *ctx) { int f; tent_obj_t **to; explosion_t *ex; entity_t *ent; for (to = &cl_explosions; *to; ) { ex = &(*to)->to.ex; ent = ex->tent->ent; f = 10 * (time - ex->start); if (f >= ent->renderer.model->numframes) { tent_obj_t *_to; R_RemoveEfrags (ent); ent->visibility.efrag = 0; free_temp_entities (ex->tent); _to = *to; *to = _to->next; free_tent_object (_to); continue; } to = &(*to)->next; ent->animation.frame = f; if (!ent->visibility.efrag) { R_AddEfrags (&cl_world.worldmodel->brush, ent); } } } void CL_UpdateTEnts (double time, TEntContext_t *ctx) { CL_UpdateBeams (time, ctx); CL_UpdateExplosions (time, ctx); } /* CL_ParseParticleEffect Parse an effect out of the server message */ void CL_ParseParticleEffect (qmsg_t *net_message) { int i, count, color; vec4f_t org = {0, 0, 0, 1}, dir = {}; MSG_ReadCoordV (net_message, &org[0]); for (i = 0; i < 3; i++) dir[i] = ((signed char) MSG_ReadByte (net_message)) * (15.0 / 16.0); count = MSG_ReadByte (net_message); color = MSG_ReadByte (net_message); if (count == 255) clp_funcs->ParticleExplosion (org); else clp_funcs->RunParticleEffect (org, dir, color, count); } void CL_ClearProjectiles (void) { tent_t *tent; for (tent = cl_projectiles; tent; tent = tent->next) { R_RemoveEfrags (tent->ent); tent->ent->visibility.efrag = 0; } free_temp_entities (cl_projectiles); cl_projectiles = 0; } /* Nails are passed as efficient temporary entities */ void CL_ParseProjectiles (qmsg_t *net_message, qboolean nail2, TEntContext_t *ctx) { tent_t *tent; tent_t *head = 0, **tail = &head; byte bits[6]; int i, c, j, num; entity_t *pr; vec4f_t position = { 0, 0, 0, 1 }; vec3_t angles; c = MSG_ReadByte (net_message); for (i = 0; i < c; i++) { if (nail2) num = MSG_ReadByte (net_message); else num = 0; (void) num; //FIXME for (j = 0; j < 6; j++) bits[j] = MSG_ReadByte (net_message); tent = new_temp_entity (); *tail = tent; tail = &tent->next; pr = tent->ent; pr->renderer.model = cl_spike; pr->renderer.skin = 0; position[0] = ((bits[0] + ((bits[1] & 15) << 8)) << 1) - 4096; position[1] = (((bits[1] >> 4) + (bits[2] << 4)) << 1) - 4096; position[2] = ((bits[3] + ((bits[4] & 15) << 8)) << 1) - 4096; angles[0] = (bits[4] >> 4) * (360.0 / 16.0); angles[1] = bits[5] * (360.0 / 256.0); angles[2] = 0; CL_TransformEntity (tent->ent, 1, angles, position); R_AddEfrags (&cl_world.worldmodel->brush, tent->ent); } *tail = cl_projectiles; cl_projectiles = head; }