diff --git a/CMakeLists.txt b/CMakeLists.txt index 81388aeee..72fb1a8c0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -581,6 +581,7 @@ set(client_headers ${SOURCE_DIR}/ez_scrollpane.h ${SOURCE_DIR}/ez_slider.h ${SOURCE_DIR}/ez_window.h + ${SOURCE_DIR}/ezcsqc.h ${SOURCE_DIR}/fchecks.h ${SOURCE_DIR}/fmod.h ${SOURCE_DIR}/fonts.h @@ -642,6 +643,7 @@ set(client ${SOURCE_DIR}/cl_cmd.c ${SOURCE_DIR}/cl_demo.c ${SOURCE_DIR}/cl_ents.c + ${SOURCE_DIR}/cl_ezcsqc.c ${SOURCE_DIR}/cl_input.c ${SOURCE_DIR}/cl_main.c ${SOURCE_DIR}/cl_multiview.c diff --git a/src/cl_ents.c b/src/cl_ents.c index e5867c06b..2ce4f049a 100644 --- a/src/cl_ents.c +++ b/src/cl_ents.c @@ -25,6 +25,9 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include "qmb_particles.h" #include "rulesets.h" #include "teamplay.h" +#ifdef MVD_PEXT1_EZCSQC +#include "ezcsqc.h" +#endif static int MVD_TranslateFlags(int src); void TP_ParsePlayerInfo(player_state_t *, player_state_t *, player_info_t *info); @@ -1635,6 +1638,60 @@ void CL_ParsePlayerinfo (void) SetupPlayerEntity(num + 1, state); } +// #ifdef MVD_PEXT1_EZCSQC +void CL_EZCSQC_ParseEntities(void) +{ + ezcsqc_entity_t *ent; + unsigned int entnum; + qbool removeflag; + qbool is_new; + int amt = 0; + + ezcsqc.active = true; + + for(;;) + { + entnum = (unsigned short)MSG_ReadShort(); + removeflag = !!(entnum & 0x8000); + entnum &= ~0x8000; + + if ((!entnum && !removeflag) || msg_badread) + break; + + amt++; + + is_new = false; + if (removeflag) + { //remove + ent = ezcsqc_networkedents[entnum]; + ezcsqc_networkedents[entnum] = NULL; + + if (!ent) //wtf, server is gaslighting us or something catastophic happened + continue; + + CL_EZCSQC_Ent_Remove(ent); + // traditionally we would leave the cleanup to CSQC, but since we're faking all this + // we just kinda call one function. any further cleanup behavior can be jammed into that function. + } + else + { + ent = ezcsqc_networkedents[entnum]; + if (ent == NULL) + { + ent = CL_EZCSQC_Ent_Spawn(); + ezcsqc_networkedents[entnum] = ent; + + ent->entnum = entnum; + is_new = true; + } + + CL_EZCSQC_Ent_Update(ent, is_new); + } + } +} +// #endif + + // Called when the CTF flags are set void CL_AddFlagModels (entity_t *ent, int team) { @@ -2237,6 +2294,9 @@ void CL_EmitEntities (void) } CL_UpdateTEnts(); +#ifdef MVD_PEXT1_EZCSQC + CL_EZCSQC_UpdateView(); +#endif } int mvd_fixangle; diff --git a/src/cl_ezcsqc.c b/src/cl_ezcsqc.c new file mode 100644 index 000000000..69ac6b4a0 --- /dev/null +++ b/src/cl_ezcsqc.c @@ -0,0 +1,1154 @@ +/* +Copyright (C) 2021-2023 Sam "Reki" Piper + +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 the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +#include "quakedef.h" +#ifdef MVD_PEXT1_EZCSQC +#include "ezcsqc.h" +#include "pmove.h" +#include "qsound.h" +#include "gl_local.h" +#include "gl_model.h" +#include "qmb_particles.h" +#include "vx_stuff.h" + +ezcsqc_weapon_state_t ws_server[UPDATE_BACKUP]; +ezcsqc_weapon_state_t ws_predicted; +ezcsqc_weapon_state_t ws_saved[UPDATE_BACKUP]; +ezcsqc_entity_t ezcsqc_entities[MAX_EDICTS]; +ezcsqc_entity_t* ezcsqc_networkedents[MAX_EDICTS]; +ezcsqc_world_t ezcsqc; +weppredsound_t* predictionsoundlist; +int projectile_ringbufferseek; +static int is_effectframe; +static int last_effectframe; +static float lg_twidth; + +cvar_t cl_predict_weaponsounds = { "cl_predict_weaponsounds", "1" }; + +#define MAX_PREDWEPS 16 +weppreddef_t wpredict_definitions[MAX_PREDWEPS]; +#define WEPANIM(wep, frame) (&wep->anim_states[frame]) +#define LENGTH2S(length) ((float)length / 1000) + + +ezcsqc_entity_t* CL_EZCSQC_Ent_Spawn(void) +{ + int i; + double time_safety = Sys_DoubleTime() - 0.5; + + for (i = 0; i < MAX_EDICTS; i++) + { + ezcsqc_entity_t *ent = &ezcsqc_entities[i]; + if (!ent->isfree) // entity is not currently used + { + continue; + } + if (ent->freetime > time_safety) // entity was not *very* recently freed + { + continue; + } + memset(ent, 0, sizeof(ezcsqc_entity_t)); + return ent; + } + + // uhh... not good, we're out of entities! + return &ezcsqc_entities[0]; +} + +void CL_EZCSQC_Ent_Remove(ezcsqc_entity_t *ent) +{ + if (ent->isfree) + { + return; + } + + ent->isfree = true; + ent->freetime = Sys_DoubleTime(); +} + +void CL_EZCSQC_InitializeEntities(void) +{ + int i; + + memset(&ezcsqc, 0, sizeof(ezcsqc_world_t)); + + last_effectframe = 0; + lg_twidth = 0; + projectile_ringbufferseek = 0; + + // clear weapon parameters + for (i = 0; i < MAX_PREDWEPS; i++) + { + weppreddef_t *wep = &wpredict_definitions[i]; + memset(wep, 0, sizeof(weppreddef_t)); + } + + // clear entity array + for (i = 0; i < MAX_EDICTS; i++) + { + ezcsqc_entity_t *ent = &ezcsqc_entities[i]; + memset(ent, 0, sizeof(ezcsqc_entity_t)); + ent->isfree = true; + ezcsqc_networkedents[i] = NULL; + } + + // clear prediction sound list + weppredsound_t *snd = predictionsoundlist; + while(snd) + { + predictionsoundlist = snd->next; + free(snd); + snd = predictionsoundlist; + } + predictionsoundlist = NULL; +} + +/* +============================================================================== + Predraws +============================================================================== +*/ + +#define EF_NAILTRAIL 0x40000000 +extern void R_ParticleTrail(vec3_t start, vec3_t end, trail_type_t type); +static void R_CSQC_MissileTrail(vec3_t start, vec3_t end, vec3_t angles, int trail_num) +{ + if ((trail_num == 8 || trail_num == 10 || trail_num == 11) && !qmb_initialized) + { + trail_num = 1; + } + + if (trail_num == 1) + { + R_ParticleTrail(start, end, ROCKET_TRAIL); + } + else if (trail_num == 2) + { + R_ParticleTrail(start, end, GRENADE_TRAIL); + } + else if (trail_num == 3) + { + R_ParticleTrail(start, end, ALT_ROCKET_TRAIL); + } + else if (trail_num == 4) + { + R_ParticleTrail(start, end, BLOOD_TRAIL); + } + else if (trail_num == 5) + { + R_ParticleTrail(start, end, BIG_BLOOD_TRAIL); + } + else if (trail_num == 6) + { + R_ParticleTrail(start, end, TRACER1_TRAIL); + } + else if (trail_num == 7) + { + R_ParticleTrail(start, end, TRACER2_TRAIL); + } + else if (trail_num == 8) + { + // VULT PARTICLES + byte color[3]; + color[0] = 0; color[1] = 70; color[2] = 255; + FireballTrail(start, end, color, 2, 0.5f); + } + else if (trail_num == 9) + { + R_ParticleTrail(start, end, LAVA_TRAIL); + } + else if (trail_num == 10) + { + // VULT PARTICLES + FuelRodGunTrail(start, end, angles); + } + else if (trail_num == 11) + { + byte color[3]; + color[0] = 255; color[1] = 70; color[2] = 5; + FireballTrail(start, end, color, 2, 0.5f); + } + else if (trail_num == 12) + { + R_ParticleTrail(start, end, AMF_ROCKET_TRAIL); + } + else if (trail_num == 14) + { + R_ParticleTrail(start, end, RAIL_TRAIL); + } + else + { + R_ParticleTrail(start, end, GRENADE_TRAIL); + } +} + + +qbool Predraw_Projectile(ezcsqc_entity_t *self) +{ + customlight_t cst_lt = { 0 }; + vec3_t dist_vec; + float dist, freq; + + float dt = cl.servertime - self->s_time; + VectorMA(self->s_origin, dt, self->s_velocity, self->origin); + + if (self->modelflags & EF_ROCKET) + { + // Add rocket lights + float rocketlightsize = 200.0f * bound(0, r_rocketlight.value, 1); + if (rocketlightsize >= 1) + { + extern cvar_t gl_rl_globe; + int bubble = gl_rl_globe.integer ? 2 : 1; + + if ((r_rockettrail.value < 8 || r_rockettrail.value == 12) || !gl_part_trails.integer) + { + dlightColorEx(r_rocketlightcolor.value, r_rocketlightcolor.string, lt_rocket, false, &cst_lt); + CL_NewDlightEx(self->entnum, self->origin, rocketlightsize, 0.1f, &cst_lt, bubble); + if (!ISPAUSED && amf_coronas.integer) + { + //VULT CORONAS + R_CoronasNew(C_ROCKETLIGHT, self->origin); + } + } + + if (r_rockettrail.value == 9 || r_rockettrail.value == 11) + { + CL_NewDlight(self->entnum, self->origin, rocketlightsize, 0.1f, lt_default, bubble); + } + else if (r_rockettrail.value == 8) + { + // PLASMA ROCKETS + CL_NewDlight(self->entnum, self->origin, rocketlightsize, 0.1f, lt_blue, bubble); + } + else if (r_rockettrail.value == 10) + { + // FUEL ROD GUN + CL_NewDlight(self->entnum, self->origin, rocketlightsize, 0.1f, lt_green, bubble); + } + } + + if (r_rockettrail.integer) + { + if (!gl_part_trails.integer || r_rockettrail.integer <= 5 || r_rockettrail.integer >= 12) + { + freq = 14; + } + else + { + freq = 2; + } + + VectorSubtract(self->origin, self->trail_origin, dist_vec); + dist = VectorLength(dist_vec); + VectorNormalize(dist_vec); + if (dist > freq) + { + R_CSQC_MissileTrail(self->trail_origin, self->origin, self->angles, r_rockettrail.integer); + + //R_ParticleTrail(self->trail_origin, self->origin, ROCKET_TRAIL); + + /* + vec3_t part_origin; + while (dist > 4) + { + VectorMA(self->trail_origin, dist, dist_vec, part_origin); + + + + dist -= 4; + } + */ + VectorCopy(self->origin, self->trail_origin); + } + } + } + else if (self->modelflags & EF_NAILTRAIL) + { + if (amf_nailtrail.integer) + { + VectorSubtract(self->origin, self->trail_origin, dist_vec); + dist = VectorLength(dist_vec); + VectorNormalize(dist_vec); + + if (amf_nailtrail_plasma.integer) + { + if (dist > 3) + { + byte color[3]; + color[0] = 0; color[1] = 70; color[2] = 255; + FireballTrail(self->trail_origin, self->origin, color, 0.6f, 0.3f); + + VectorCopy(self->origin, self->trail_origin); + } + } + else + { + if (dist > 14) + { + ParticleNailTrail_NoEntity(self->trail_origin, self->origin, 2.0f, 0.014f); + + VectorCopy(self->origin, self->trail_origin); + } + } + } + } + + return true; +} + +/* +============================================================================== + Weapon Definitons +============================================================================== +*/ + +void EntUpdate_WeaponDef(ezcsqc_entity_t *self, qbool is_new) +{ + int i, k; + + int sendflags = MSG_ReadByte(); + + //Con_Printf("EntUpdate_WeaponDef %i\n", sendflags); + + int wep_index = MSG_ReadByte(); + wep_index = bound(0, wep_index, MAX_PREDWEPS - 1); + weppreddef_t *wep = &wpredict_definitions[wep_index]; + + if (sendflags & 1) + { + wep->attack_time = MSG_ReadShort(); + wep->modelindex = MSG_ReadShort(); + wep->modelindex = bound(0, wep->modelindex, MAX_MODELS - 1); + } + + if (sendflags & 2) + { + wep->impulse = MSG_ReadByte(); + wep->itemflag = MSG_ReadByte(); + if (wep->itemflag != 255) + { + wep->itemflag = 1 << wep->itemflag; + } + } + + if (sendflags & 4) + { + wep->anim_number = MSG_ReadByte(); + for (i = 0; i < wep->anim_number; i++) + { + weppredanim_t *anim = &wep->anim_states[i]; + + anim->mdlframe = MSG_ReadByte() - 127; + anim->flags = MSG_ReadByte(); + if (anim->flags & WEPPREDANIM_MOREBYTES) + { + anim->flags |= MSG_ReadByte() << 8; + } + if (anim->flags & WEPPREDANIM_SOUND) + { + anim->sound = MSG_ReadShort(); + anim->sound = bound(0, anim->sound, MAX_SOUNDS - 1); + anim->soundmask = MSG_ReadShort(); + + qbool add_sound_to_list = true; + weppredsound_t *hold = NULL; + weppredsound_t *snd = predictionsoundlist; + while (snd) + { + if (snd->index == anim->sound && snd->mask == anim->soundmask) + { + add_sound_to_list = false; + break; + } + + hold = snd; + snd = snd->next; + } + + if (add_sound_to_list) + { + snd = malloc(sizeof(weppredsound_t)); + snd->index = anim->sound; + snd->mask = anim->soundmask; + snd->chan = (anim->flags & WEPPREDANIM_SOUNDAUTO) ? 0 : 1; + snd->next = NULL; + + if (hold) + { + hold->next = snd; + } + else + { + predictionsoundlist = snd; + } + } + } + if (anim->flags & WEPPREDANIM_PROJECTILE) + { + anim->projectile_model = MSG_ReadShort(); + anim->projectile_model = bound(0, anim->projectile_model, MAX_MODELS - 1); + for (k = 0; k < 3; k++) + { + anim->projectile_velocity[k] = MSG_ReadShort(); + } + for (k = 0; k < 3; k++) + { + anim->projectile_offset[k] = MSG_ReadByte(); + } + } + anim->nextanim = MSG_ReadByte(); + if (anim->flags & WEPPREDANIM_BRANCH) + { + anim->altanim = MSG_ReadByte(); + } + anim->length = MSG_ReadByte() * 10; + } + } +} + + +/* +============================================================================== + Weapon Prediction +============================================================================== +*/ + +#define IT_HOOK 32768 +#define PRDFL_MIDAIR 1 +#define PRDFL_COILGUN 2 +#define PRDFL_FORCEOFF 255 +ezcsqc_entity_t *viewweapon; + +#if 0 +qbool WeaponPred_SetModel(ezcsqc_entity_t *self) +{ + int oldframe = self->frame; + int oldmodel = self->modelindex; + + switch (ws_predicted.weapon) + { + case IT_AXE: self->modelindex = cl_modelindices[mi_vaxe]; break; + case IT_SHOTGUN: + if (ws_server.client_predflags & PRDFL_COILGUN) + { + self->modelindex = cl_modelindices[mi_vgrap]; //FIXME: replace with coilgun + } + else + { + self->modelindex = cl_modelindices[mi_weapon2]; + } + break; + case IT_SUPER_SHOTGUN: self->modelindex = cl_modelindices[mi_weapon3]; break; + case IT_NAILGUN: self->modelindex = cl_modelindices[mi_weapon4]; break; + case IT_SUPER_NAILGUN: self->modelindex = cl_modelindices[mi_weapon5]; break; + case IT_GRENADE_LAUNCHER: self->modelindex = cl_modelindices[mi_weapon6]; break; + case IT_ROCKET_LAUNCHER: self->modelindex = cl_modelindices[mi_weapon7]; break; + case IT_LIGHTNING: self->modelindex = cl_modelindices[mi_weapon8]; break; + case IT_HOOK: self->modelindex = cl_modelindices[mi_vgrap]; break; + } + + self->frame = ws_predicted.frame; +} + +void W_SetCurrentAmmo(ezcsqc_weapon_state_t *ws) +{ + switch (ws->weapon) + { + case IT_AXE: + ws->current_ammo = 0; + ws->weapon_index = 1; + break; + case IT_SHOTGUN: + ws->current_ammo = ws->ammo_shells; + if (ws->client_predflags & PRDFL_COILGUN) + ws->weapon_index = 9; + else + ws->weapon_index = 2; + break; + case IT_SUPER_SHOTGUN: + ws->current_ammo = ws->ammo_shells; + ws->weapon_index = 3; + break; + case IT_NAILGUN: + ws->current_ammo = ws->ammo_nails; + ws->weapon_index = 4; + break; + case IT_SUPER_NAILGUN: + ws->current_ammo = ws->ammo_nails; + ws->weapon_index = 5; + break; + case IT_GRENADE_LAUNCHER: + ws->current_ammo = ws->ammo_rockets; + ws->weapon_index = 6; + break; + case IT_ROCKET_LAUNCHER: + ws->current_ammo = ws->ammo_rockets; + ws->weapon_index = 7; + break; + case IT_LIGHTNING: + ws->current_ammo = ws->ammo_cells; + ws->weapon_index = 8; + break; + case IT_HOOK: + ws->current_ammo = 0; + ws->weapon_index = 10; + break; + } +} +#endif + +void WeaponPred_SetModel(ezcsqc_entity_t *self) +{ + weppreddef_t *wep = &wpredict_definitions[bound(0, ws_predicted.weapon_index, MAX_PREDWEPS-1)]; + // weppredanim_t *anim = WEPANIM(wep, ws_predicted.client_thinkindex); + + //self->modelindex = wep->modelindex; + if (wep->modelindex) + { + self->modelindex = wep->modelindex; + self->frame = ws_predicted.frame; + } + + //Con_Printf("frame %i\n", ws_predicted.frame); + //self->frame = ws_predicted.frame; +} + +void WeaponPred_PlayEffects(usercmd_t *u, player_state_t *ps, ezcsqc_weapon_state_t *ws, weppredanim_t *anim) +{ + if (is_effectframe) // if is effect frame + { + if (anim->flags & WEPPREDANIM_SOUND) + { + int chan = 1; + if (anim->flags & WEPPREDANIM_SOUNDAUTO) + { + chan = 0; + } + + if (!(anim->flags & WEPPREDANIM_LTIME) || ws->client_time >= lg_twidth) + { + // play sound + if (cl_predict_weaponsounds.integer == 1 || (cl_predict_weaponsounds.integer && !(cl_predict_weaponsounds.integer & anim->soundmask))) + { + S_StartSound(cl.playernum + 1, chan, cl.sound_precache[anim->sound], pmove.origin, 1, 0); + } + + if (anim->flags & WEPPREDANIM_LTIME) + { + lg_twidth = ws->client_time + 0.6f; + } + } + } + + if (anim->flags & WEPPREDANIM_MUZZLEFLASH) + { + // spawn muzzleflash + } + + if (anim->flags & WEPPREDANIM_LGBEAM) + { + // spawn beam tent + } + + if (anim->flags & WEPPREDANIM_PROJECTILE) + { + // spawn projectile + } + } +} + + +void WeaponPred_StartFrame(usercmd_t *u, player_state_t *ps, ezcsqc_weapon_state_t *ws, weppreddef_t *wep, int framenum) +{ + weppredanim_t *anim = WEPANIM(wep, framenum); + + if (!(anim->flags & WEPPREDANIM_ATTACK && !(anim->flags & WEPPREDANIM_BRANCH))) // if this anim wants to hold until +attack, don't play effects + { + WeaponPred_PlayEffects(u, ps, ws, anim); + } + + ws->client_thinkindex = framenum; + ws->client_nextthink = ws->client_time + LENGTH2S(anim->length); + if (!anim->length) + { + ws->client_nextthink = 0; + } + + if (anim->mdlframe >= 0) + { + ws->frame = anim->mdlframe; + } + else + { + ws->frame++; + if (ws->frame > -anim->mdlframe) + { + ws->frame = 1; + } + } +} + + +void WeaponPred_WAttack(usercmd_t *u, player_state_t *ps, ezcsqc_weapon_state_t *ws) +{ + weppreddef_t *wep = &wpredict_definitions[bound(0, ws->weapon_index, MAX_PREDWEPS - 1)]; + weppredanim_t *anim = WEPANIM(wep, ws->client_thinkindex); + + if (anim->flags & WEPPREDANIM_ATTACK && !(anim->flags & WEPPREDANIM_BRANCH)) + { + //if (anim->flags & WEPPREDANIM_DEFAULT && ws->client_time < ws->attack_finished) + if (ws->client_time < ws->attack_finished) + { + return; + } + + if (!(u->buttons & BUTTON_ATTACK)) + { + return; + } + + WeaponPred_PlayEffects(u, ps, ws, anim); + + ws->attack_finished = ws->client_time + LENGTH2S(wep->attack_time); + WeaponPred_StartFrame(u, ps, ws, wep, anim->nextanim); + } + else + { + int i; + + if (ws->client_time < ws->attack_finished) + { + return; + } + + if (!(u->buttons & BUTTON_ATTACK)) + { + return; + } + + for (i = 0; i < WEPPRED_MAXSTATES; i++) + { + anim = WEPANIM(wep, i); + + if (!(anim->flags & WEPPREDANIM_DEFAULT)) + { + continue; + } + + if (!(anim->flags & WEPPREDANIM_ATTACK)) + { + break; + } + + WeaponPred_PlayEffects(u, ps, ws, anim); + + ws->attack_finished = ws->client_time + LENGTH2S(wep->attack_time); + WeaponPred_StartFrame(u, ps, ws, wep, anim->nextanim); + } + } +} + + +void WeaponPred_Logic(usercmd_t *u, player_state_t *ps, ezcsqc_weapon_state_t *ws) +{ + weppreddef_t *wep = &wpredict_definitions[bound(0, ws->weapon_index, MAX_PREDWEPS - 1)]; + weppredanim_t *anim = &wep->anim_states[ws->client_thinkindex]; + + if (anim->flags & WEPPREDANIM_DEFAULT) + { + return; + } + + if (!(anim->flags & WEPPREDANIM_ATTACK) || (anim->flags & WEPPREDANIM_BRANCH)) + { + float time_held = ws->client_time; + int frame_to_go = anim->nextanim; + + if (ws->client_time < ws->client_nextthink) + { + return; + } + + //if (hold_time) // set time to the nextthink + ws->client_time = ws->client_nextthink; + + if (anim->flags & WEPPREDANIM_ATTACK) + { + if (!(u->buttons & BUTTON_ATTACK) || ws->impulse) + { + frame_to_go = anim->altanim; + } + else + { + ws->attack_finished = ws->client_time + LENGTH2S(wep->attack_time); + } + } + + WeaponPred_StartFrame(u, ps, ws, wep, frame_to_go); + + //if (hold_time) // restore time + ws->client_time = time_held; + } +} + + +qbool WeaponPred_SwitchWeapon(int impulse, ezcsqc_weapon_state_t *ws) +{ + int i, found, items; + + if (ws->client_time < ws->attack_finished) + { + return false; + } + + found = false; + items = cl.stats[STAT_ITEMS]; + for (i = 0; i < MAX_PREDWEPS; i++) + { + weppreddef_t *wep = &wpredict_definitions[i]; + if (wep->impulse != impulse) + { + continue; + } + + if (ws->weapon_index == i) + { + found = true; + continue; + } + + if (items & wep->itemflag) + { + ws->weapon_index = i; + ws->weapon = wep->itemflag; + ws->client_thinkindex = 0; + ws->client_nextthink = 0; + ws->frame = WEPANIM(wep, 0)->mdlframe; + return true; + } + } + + return found; +} + + +void WeaponPred_Simulate(usercmd_t u, player_state_t ps, ezcsqc_weapon_state_t *ws) +{ + if (ps.pm_type == PM_DEAD || ps.pm_type == PM_NONE || ps.pm_type == PM_LOCK) + { + ws->impulse = 0; + ws->attack_finished = ws->client_time + 0.05f; + return; + } + + ws->client_time += u.msec * 0.001f; + + if (u.impulse) + { + ws->impulse = u.impulse; + } + + WeaponPred_Logic(&u, &ps, ws); + + if (ws->impulse) + { + if (WeaponPred_SwitchWeapon(ws->impulse, ws)) + { + ws->impulse = 0; + } + } + + WeaponPred_WAttack(&u, &ps, ws); +} + + +qbool WeaponPred_Predraw(ezcsqc_entity_t *self) +{ + int i = 0; + + if (cls.netchan.outgoing_sequence - cl.validsequence <= 2) + { + i = -3; + //Con_Printf("use oldserver\n"); + } + + ws_predicted = ws_server[(cl.validsequence + i) & UPDATE_MASK]; + + for (; i < UPDATE_BACKUP - 1 && cl.validsequence + i < cls.netchan.outgoing_sequence; i++) + { + frame_t *to; + to = &cl.frames[(cl.validsequence + i) & UPDATE_MASK]; + + is_effectframe = false; + if ((cl.validsequence + i > last_effectframe) && + ((cl.validsequence + i) < (cls.netchan.outgoing_sequence - 1))) + { + is_effectframe = true; + last_effectframe = cl.validsequence + i; + } + + WeaponPred_Simulate(to->cmd, to->playerstate[cl.playernum], &ws_predicted); + ws_saved[(cl.validsequence + i + 1) & UPDATE_MASK] = ws_predicted; + } + + WeaponPred_SetModel(self); + + return false; +} + +// Recieve struct update from SSQC +void EntUpdate_WeaponInfo(ezcsqc_entity_t *self, qbool is_new) +{ + int sendflags; + ezcsqc_weapon_state_t *ws_current; + ezcsqc_weapon_state_t *ws_error; + sendflags = MSG_ReadByte(); + ws_current = &ws_server[(cl.validsequence + 1) & UPDATE_MASK]; + *ws_current = ws_server[(cl.validsequence) & UPDATE_MASK]; + ws_error = &ws_saved[(cl.validsequence + 1) & UPDATE_MASK]; + + // parse the delta update to the struct + if (sendflags & 0x01) + { + ws_current->impulse = MSG_ReadByte(); + ws_current->weapon_index = MSG_ReadByte(); + } + if (sendflags & 0x02) + { + ws_current->ammo_shells = MSG_ReadByte(); + } + if (sendflags & 0x04) + { + ws_current->ammo_nails = MSG_ReadByte(); + } + if (sendflags & 0x08) + { + ws_current->ammo_rockets = MSG_ReadByte(); + } + if (sendflags & 0x10) + { + ws_current->ammo_cells = MSG_ReadByte(); + } + if (sendflags & 0x20) + { + ws_current->attack_finished = MSG_ReadFloat(); + ws_current->client_nextthink = MSG_ReadFloat(); + ws_current->client_thinkindex = MSG_ReadByte(); + } + if (sendflags & 0x40) + { + ws_current->client_time = MSG_ReadFloat(); + ws_current->frame = MSG_ReadByte(); + } + if (sendflags & 0x80) + { + ws_current->client_predflags = MSG_ReadByte(); + ws_current->client_ping = MSG_ReadByte() / 1000; + } + +#if 0 + if (ws_current->impulse != ws_error->impulse) + { + Con_Printf("%s: impulse desync, %i -> %i\n", __func__, (int)ws_error->impulse, (int)ws_current->impulse); + } +#endif +#if 0 + if (ws_current->weapon_index != ws_error->weapon_index) + { + Con_Printf("%s: weapon_index desync, %i -> %i\n", __func__, ws_error->weapon_index, ws_current->weapon_index); + } +#endif +#if 0 + if (ws_current->attack_finished != ws_error->attack_finished) + { + Con_Printf("%s: attack_finished desync, %g -> %g\n", __func__, ws_error->attack_finished, ws_current->attack_finished); + } +#endif +#if 0 + if (ws_current->client_nextthink != ws_error->client_nextthink) + { + Con_Printf("%s: client_nextthink desync, %g -> %g\n", __func__, ws_error->client_nextthink, ws_current->client_nextthink); + } +#endif +#if 1 + if (ws_current->client_thinkindex != ws_error->client_thinkindex) + { + Con_Printf("%s: client_thinkindex desync, %i -> %i\n", __func__, ws_error->client_thinkindex, ws_current->client_thinkindex); + } +#endif +#if 0 + if (ws_current->client_time != ws_error->client_time) + { + Con_Printf("%s: client_time desync, %g -> %g\n", __func__, ws_error->client_time, ws_current->client_time); + } +#endif +#if 0 + if (ws_current->frame != ws_error->frame) + { + Con_Printf("%s: frame desync, %i -> %i\n", __func__, ws_error->frame, ws_current->frame); + } +#endif + + // if this is our first update, we need to do some entity setup + if (is_new) + { + self->drawmask = DRAWMASK_VIEWMODEL; + self->predraw = WeaponPred_Predraw; + ezcsqc.weapon_prediction = true; + viewweapon = self; + } +} + +void EntUpdate_Projectile(ezcsqc_entity_t *self, qbool is_new) +{ + int sendflags; + sendflags = MSG_ReadByte(); + + if (sendflags & 1) + { + self->s_origin[0] = MSG_ReadCoord(); + self->s_origin[1] = MSG_ReadCoord(); + self->s_origin[2] = MSG_ReadCoord(); + + self->s_velocity[0] = MSG_ReadCoord(); + self->s_velocity[1] = MSG_ReadCoord(); + self->s_velocity[2] = MSG_ReadCoord(); + + self->s_time = MSG_ReadFloat(); + } + + + if (sendflags & 2) + { + self->modelindex = MSG_ReadShort(); + self->effects = MSG_ReadShort(); + + self->modelflags = (cl.model_precache[self->modelindex])->flags; + if ((cl.model_precache[self->modelindex])->modhint == MOD_SPIKE) + { + self->modelflags |= EF_NAILTRAIL; + } + } + + + if (sendflags & 4) + { + self->angles[0] = MSG_ReadAngle(); + self->angles[1] = MSG_ReadAngle(); + self->angles[2] = MSG_ReadAngle(); + } + + + if (sendflags & 8) + { + self->ownernum = MSG_ReadShort(); + } + + + if (sendflags & 16) + { + vec3_t spawn_origin, diff; + spawn_origin[0] = MSG_ReadCoord(); + spawn_origin[1] = MSG_ReadCoord(); + spawn_origin[2] = MSG_ReadCoord(); + + VectorSubtract(spawn_origin, self->s_origin, diff); + if (VectorLength(diff) < 150) // reconcile trail if it's less than 150u from the current origin + { + VectorCopy(spawn_origin, self->trail_origin); + } + } + + + if (is_new) + { + self->drawmask = DRAWMASK_PROJECTILE; + self->predraw = Predraw_Projectile; + + // if we didn't get a trail + if (self->trail_origin[0] == 0 && self->trail_origin[1] == 0 && self->trail_origin[2] == 0) + { + VectorCopy(self->s_origin, self->trail_origin); + } + + VectorCopy(self->s_origin, self->oldorigin); + } +} + +/* +============================================================================== + Networking +============================================================================== +*/ + +qbool CL_EZCSQC_Event_Sound(int entnum, int channel, int soundnumber, float vol, float attenuation, vec3_t pos, float pitchmod, float flags) +{ + weppredsound_t *snd; + + if (entnum == cl.playernum + 1) + { + if (!cl_predict_weaponsounds.integer) + { + return false; + } + + for (snd = predictionsoundlist; snd; snd = snd->next) + { + if (snd->chan != channel) + { + continue; + } + + if (snd->index != soundnumber) + { + continue; + } + + if (cl_predict_weaponsounds.integer & snd->mask) + { + continue; + } + + return true; + } + } + + return false; +} + +void CL_EZCSQC_Ent_Update(ezcsqc_entity_t *self, qbool is_new) +{ + int type; + type = MSG_ReadByte(); + + switch (type) + { + case EZCSQC_PROJECTILE: + EntUpdate_Projectile(self, is_new); + break; + case EZCSQC_WEAPONINFO: + EntUpdate_WeaponInfo(self, is_new); + break; + case EZCSQC_WEAPONDEF: + EntUpdate_WeaponDef(self, is_new); + break; + } +} + +/* +============================================================================== + Rendering +============================================================================== +*/ + +void CL_EZCSQC_ViewmodelUpdate(player_state_t *view_message) +{ + if (ezcsqc.weapon_prediction && viewweapon) + { + cl.stats[STAT_WEAPON] = viewweapon->modelindex; + view_message->weaponframe = viewweapon->frame; + } +} + +void CL_EZCSQC_RenderEntity(ezcsqc_entity_t *self) +{ + entity_t ent; + + memset(&ent, 0, sizeof(entity_t)); + ent.colormap = vid.colormap; + VectorCopy(self->origin, ent.origin); + VectorCopy(self->angles, ent.angles); + ent.model = cl.model_precache[self->modelindex]; + ent.frame = self->frame; + + CL_AddEntity(&ent); +} + +void CL_EZCSQC_RenderEntities(int drawmask) +{ + int i; + + for (i = 0; i < MAX_EDICTS; i++) + { + ezcsqc_entity_t *ent = &ezcsqc_entities[i]; + if (ent->isfree) // entity is not currently used + { + continue; + } + + if (!(ent->drawmask & drawmask)) + { + continue; + } + + if (ent->predraw) + { + if (ent->predraw(ent)) + { + CL_EZCSQC_RenderEntity(ent); + } + continue; + } + + CL_EZCSQC_RenderEntity(ent); + } +} + +void CL_EZCSQC_RenderEntities_Ring(int drawmask, int index) +{ + int i = index; + do + { + ezcsqc_entity_t *ent = &ezcsqc_entities[i]; + if (ent->isfree) // entity is not currently used + { + goto cont; + } + + if (!(ent->drawmask & drawmask)) + { + goto cont; + } + + if (ent->predraw) + { + if (ent->predraw(ent)) + { + CL_EZCSQC_RenderEntity(ent); + } + goto cont; + } + + CL_EZCSQC_RenderEntity(ent); + + cont: + i = (i + 1) & (MAX_EDICTS - 1); + } while (i != index); +} + +void CL_EZCSQC_UpdateView(void) +{ + CL_EZCSQC_RenderEntities(DRAWMASK_ENGINE | DRAWMASK_VIEWMODEL); + + // add projectiles and increment the index, so trails are kinda evenly spotty. + CL_EZCSQC_RenderEntities_Ring(DRAWMASK_PROJECTILE, projectile_ringbufferseek = (projectile_ringbufferseek + 7) & (MAX_EDICTS - 1)); +} +#endif \ No newline at end of file diff --git a/src/cl_main.c b/src/cl_main.c index 737b304db..0712a854a 100644 --- a/src/cl_main.c +++ b/src/cl_main.c @@ -70,6 +70,9 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include "r_renderer.h" #include "r_performance.h" #include "r_program.h" +#ifdef MVD_PEXT1_EZCSQC +#include "ezcsqc.h" +#endif extern qbool ActiveApp, Minimized; @@ -115,6 +118,9 @@ cvar_t cl_pext_lagteleport = { "cl_pext_lagteleport", "0" }; // server-side adj #ifdef MVD_PEXT1_SERVERSIDEWEAPON cvar_t cl_pext_serversideweapon = { "cl_pext_serversideweapon", "0", 0, onchange_pext_serversideweapon }; // server-side weapon selection #endif +#ifdef MVD_PEXT1_EZCSQC +cvar_t cl_pext_ezcsqc = { "cl_pext_ezcsqc", "1" }; // basic client-side game logic support +#endif #endif #ifdef FTE_PEXT_256PACKETENTITIES cvar_t cl_pext_256packetentities = {"cl_pext_256packetentities", "1"}; @@ -560,6 +566,12 @@ unsigned int CL_SupportedMVDExtensions1(void) } #endif +#ifdef MVD_PEXT1_EZCSQC + if (cl_pext_ezcsqc.value) { + extensions_supported |= MVD_PEXT1_EZCSQC; + } +#endif + return extensions_supported; } #endif @@ -1195,6 +1207,11 @@ void CL_ClearState (void) memset(cl_entities, 0, sizeof(cl_entities)); memset(cl_static_entities, 0, sizeof(cl_static_entities)); +#ifdef MVD_PEXT1_EZCSQC + // clear csqc state + CL_EZCSQC_InitializeEntities(); +#endif + // Set entnum for all entity baselines for (i = 0; i < sizeof(cl_entities) / sizeof(cl_entities[0]); ++i) { cl_entities[i].baseline.number = i; @@ -1863,6 +1880,10 @@ static void CL_InitLocal(void) #ifdef MVD_PEXT1_SERVERSIDEWEAPON Cvar_Register(&cl_pext_serversideweapon); #endif +#ifdef MVD_PEXT1_EZCSQC + Cvar_Register(&cl_pext_ezcsqc); + Cvar_Register(&cl_predict_weaponsounds); +#endif #endif // PROTOCOL_VERSION_FTE #ifdef FTE_PEXT_256PACKETENTITIES Cvar_Register(&cl_pext_256packetentities); diff --git a/src/cl_parse.c b/src/cl_parse.c index c4a106420..812625ef1 100644 --- a/src/cl_parse.c +++ b/src/cl_parse.c @@ -45,6 +45,9 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include "qtv.h" #include "r_brushmodel_sky.h" #include "central.h" +#ifdef MVD_PEXT1_EZCSQC +#include "ezcsqc.h" +#endif int CL_LoginImageId(const char* name); @@ -628,6 +631,15 @@ void CL_Prespawn (void) MSG_WriteByte (&cls.netchan.message, clc_stringcmd); MSG_WriteString (&cls.netchan.message, va("prespawn %i 0 %i", cl.servercount, cl.map_checksum2)); } + +#ifdef MVD_PEXT1_EZCSQC + if (cls.mvdprotocolextensions1 & MVD_PEXT1_EZCSQC) + { + MSG_WriteByte(&cls.netchan.message, clc_stringcmd); + MSG_WriteString(&cls.netchan.message, "enablecsqc"); + Con_Printf("enabling CSQC...\n"); + } +#endif } /* @@ -1965,7 +1977,18 @@ void CL_ParseStartSoundPacket(void) if (CL_Demo_SkipMessage(true)) return; - S_StartSound (ent, channel, cl.sound_precache[sound_num], pos, volume/255.0, attenuation); +#ifdef MVD_PEXT1_EZCSQC + if (ezcsqc.active) + { + if (CL_EZCSQC_Event_Sound(ent, channel, sound_num, volume/255.0, attenuation, pos, 1, 0)) + { + // if ezcsqc intercepted the sound, don't continue with engine default behavior + return; + } + } +#endif + + S_StartSound (ent, channel, cl.sound_precache[sound_num], pos, volume/255.0, attenuation); if (ent == cl.playernum+1) TP_CheckPickupSound (cl.sound_name[sound_num], pos); @@ -3587,9 +3610,19 @@ void CL_ParseServerMessage (void) } if (cmd == svc_qizmovoice) + { SHOWNET("svc_qizmovoice") + } else if (cmd < num_svc_strings) + { SHOWNET(svc_strings[cmd]); + } +#ifdef MVD_PEXT1_EZCSQC + else if (cmd == svc_fte_csqcentities) + { + SHOWNET("svc_fte_csqcentities"); + } +#endif // Update msg no: if (cmd < NUMMSG) @@ -4008,6 +4041,13 @@ void CL_ParseServerMessage (void) break; } #endif // PROTOCOL_VERSION_FTE +#ifdef MVD_PEXT1_EZCSQC + case svc_fte_csqcentities: + { + CL_EZCSQC_ParseEntities(); + break; + } +#endif case svc_soundlist: { CL_ParseSoundlist(); diff --git a/src/cl_view.c b/src/cl_view.c index a2d56ebee..dd022aa9c 100644 --- a/src/cl_view.c +++ b/src/cl_view.c @@ -35,6 +35,9 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include "r_local.h" #include "r_renderer.h" #include "r_brushmodel.h" +#ifdef MVD_PEXT1_EZCSQC +#include "ezcsqc.h" +#endif /* The view is allowed to move slightly from its true position for bobbing, @@ -1008,6 +1011,14 @@ static void V_CalcRefdef(void) //VULT CAMERAS CameraUpdate(view_message.flags & PF_DEAD); +#ifdef MVD_PEXT1_EZCSQC + // EZCSQC viewmodel manipulation + if (ezcsqc.active) + { + CL_EZCSQC_ViewmodelUpdate(&view_message); + } +#endif + // meag: really viewheight shouldn't be here, but it was incorrectly passed for years instead of bob, // and so without it the gun is rendered too far forward if e.g. viewheight -6 V_AddViewWeapon(bob + height_adjustment); diff --git a/src/client.h b/src/client.h index 1b59bdc1b..af08e95c2 100644 --- a/src/client.h +++ b/src/client.h @@ -773,6 +773,10 @@ typedef struct { float map_fog_density; qbool map_fog_enabled; float map_fog_sky; + +#ifdef MVD_PEXT1_EZCSQC + qbool ezcsqc_active; +#endif } clientState_t; #define SCORING_SYSTEM_DEFAULT 0 diff --git a/src/common.h b/src/common.h index 19610688d..8730659f8 100644 --- a/src/common.h +++ b/src/common.h @@ -42,6 +42,11 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include "cmdline_params.h" #include "fs.h" +// TODO: protocol.h +#define MVD_PEXT1_EZCSQC (1 << 7) // TODO remove me +#define svc_fte_csqcentities 76 + + #if defined(_WIN64) && !defined(__MINGW64__) int Q_strlen(const char* s); #define strlen Q_strlen diff --git a/src/ezcsqc.h b/src/ezcsqc.h new file mode 100644 index 000000000..2d8f333db --- /dev/null +++ b/src/ezcsqc.h @@ -0,0 +1,169 @@ +/* +Copyright (C) 2021-2023 Sam "Reki" Piper + +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 the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + + + +#define EZCSQC_WEAPONINFO 1 +#define EZCSQC_PROJECTILE 2 +#define EZCSQC_PLAYER 3 +#define EZCSQC_WEAPONDEF 4 +#define EZCSQC_HUDELEMENT 32 + +#define DRAWMASK_ENGINE 0x01 +#define DRAWMASK_VIEWMODEL 0x02 +#define DRAWMASK_PROJECTILE 0x04 + +typedef struct +{ + qbool active; + double time; + + int weapon_prediction; + int weaponframe; + int weaponmodel; +} ezcsqc_world_t; + +extern ezcsqc_world_t ezcsqc; + +typedef struct ezcsqc_entity_s +{ + qbool isfree; + double freetime; + + int entnum; + int drawmask; + qbool (*predraw)(struct ezcsqc_entity_s *self); + + int frame; + int modelindex; + float alpha; + + int flags; + int modelflags; + int effects; + + vec3_t origin; + vec3_t angles; + + +// additional entity fields + int ownernum; + + vec3_t trail_origin; + + vec3_t oldorigin; + vec3_t s_origin; + vec3_t s_velocity; + double s_time; + +} ezcsqc_entity_t; + +typedef struct +{ + byte state; + + byte impulse; + int weapon; + int items; + + int frame; + + int ammo_shells; + int ammo_nails; + int ammo_rockets; + int ammo_cells; + + float attack_finished; + float client_time; + + float client_nextthink; + int client_thinkindex; + + int client_predflags; + float client_ping; + + // additional fields + int current_ammo; + int weapon_index; +} ezcsqc_weapon_state_t; + +// +#define WEPPRED_MAXSTATES 32 +#define WEPPREDANIM_SOUND 0x0001 +#define WEPPREDANIM_PROJECTILE 0x0002 +#define WEPPREDANIM_LGBEAM 0x0004 +#define WEPPREDANIM_MUZZLEFLASH 0x0008 +#define WEPPREDANIM_DEFAULT 0x0010 // +attack will always be checked on this (unless current frame is +attack waiting +#define WEPPREDANIM_ATTACK 0x0020 +#define WEPPREDANIM_BRANCH 0x0040 +#define WEPPREDANIM_MOREBYTES 0x0080 // mark if we need "full" 16 bits of flags +#define WEPPREDANIM_SOUNDAUTO 0x0100 +#define WEPPREDANIM_LTIME 0x0200 // use hacky ltime sound cooldown +typedef struct weppredanim_s +{ + signed char mdlframe; // frame number in model + unsigned short flags; // flags from WEPPREDANIM + unsigned short sound; // WEPPREDANIM_SOUND: sound index to play + unsigned short soundmask; // WEPPREDANIM_SOUND: bitmask for sound (cl_predict_weaponsound) + unsigned short projectile_model; // WEPPREDANIM_PROJECTILE: model index of projectile + short projectile_velocity[3]; // WEPPREDANIM_PROJECTILE: projectile velocity (v_right, v_forward, v_up) + byte projectile_offset[3]; // WEPPREDANIM_PROJECTILE: projectile spawn position (v_right, v_forward, z) + byte nextanim; // next anim state index + byte altanim; // WEPPREDANIM_BRANCH: next anim state if condition is fullfilled + short length; // msec length of anim state (networked in 10ms increments) +} weppredanim_t; + +typedef struct weppreddef_s +{ + unsigned short modelindex; // view model index + unsigned short attack_time; // attack time in msec + + byte impulse; // impulse for equipping this weapon + int itemflag; // .items bit for this item + + byte anim_number; // number of anim frames + weppredanim_t anim_states[WEPPRED_MAXSTATES]; +} weppreddef_t; + +typedef struct weppredsound_s +{ + unsigned short index; + byte chan; + unsigned short mask; + + struct weppredsound_s *next; +} weppredsound_t; + +extern weppredsound_t *predictionsoundlist; +// + +extern cvar_t cl_predict_weaponsounds; +extern ezcsqc_entity_t ezcsqc_entities[MAX_EDICTS]; +extern ezcsqc_entity_t* ezcsqc_networkedents[MAX_EDICTS]; + +void CL_EZCSQC_ParseEntities(void); +void CL_EZCSQC_Ent_Update(ezcsqc_entity_t *self, qbool is_new); +void CL_EZCSQC_InitializeEntities(void); +ezcsqc_entity_t* CL_EZCSQC_Ent_Spawn(void); +void CL_EZCSQC_Ent_Remove(ezcsqc_entity_t *ent); +qbool CL_EZCSQC_Event_Sound(int entnum, int channel, int soundnumber, float vol, float attenuation, vec3_t pos, float pitchmod, float flags); + +void CL_EZCSQC_ViewmodelUpdate(player_state_t *view_message); +void CL_EZCSQC_UpdateView(void); \ No newline at end of file diff --git a/src/r_part_trails.c b/src/r_part_trails.c index 6cbdd4b68..77d6a7533 100644 --- a/src/r_part_trails.c +++ b/src/r_part_trails.c @@ -68,19 +68,19 @@ static void R_MissileTrail(centity_t *cent, int trail_num) // VULT PARTICLES byte color[3]; color[0] = 0; color[1] = 70; color[2] = 255; - FireballTrail(cent, color, 2, 0.5); + FireballTrailEntity(cent, color, 2.0f, 0.5f); } else if (trail_num == 9) { R_EntityParticleTrail(cent, LAVA_TRAIL); } else if (trail_num == 10) { // VULT PARTICLES - FuelRodGunTrail(cent); + FuelRodGunTrailEntity(cent); } else if (trail_num == 11) { byte color[3]; color[0] = 255; color[1] = 70; color[2] = 5; - FireballTrailWave(cent, color, 2, 0.5, cent->current.angles); + FireballTrailWaveEntity(cent, color, 2.0f, 0.5f, cent->current.angles); } else if (trail_num == 12) { R_EntityParticleTrail(cent, AMF_ROCKET_TRAIL); @@ -202,7 +202,7 @@ void CL_AddParticleTrail(entity_t* ent, centity_t* cent, customlight_t* cst_lt, if (amf_nailtrail_plasma.integer) { byte color[3]; color[0] = 0; color[1] = 70; color[2] = 255; - FireballTrail(cent, color, 0.6, 0.3); + FireballTrailEntity(cent, color, 0.6f, 0.3f); } else { ParticleNailTrail(cent, 2, 0.4f); diff --git a/src/r_particles_qmb_trails.c b/src/r_particles_qmb_trails.c index ebbe892ca..32df85580 100644 --- a/src/r_particles_qmb_trails.c +++ b/src/r_particles_qmb_trails.c @@ -32,11 +32,12 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. extern cvar_t gl_part_tracer1_color, gl_part_tracer1_size, gl_part_tracer1_time; extern cvar_t gl_part_tracer2_color, gl_part_tracer2_size, gl_part_tracer2_time; +extern cvar_t r_particles_count; __inline static void AddParticleTrailImpl(part_type_t type, vec3_t start, vec3_t end, float size, float time, col_t col, centity_t* cent, int trail_index) { byte *color; - int i, j, num_particles; + int i, j, num_particles, cull_threshold; float count = 0.0, theta = 0.0; vec3_t point, delta; particle_t *p; @@ -133,8 +134,20 @@ __inline static void AddParticleTrailImpl(part_type_t type, vec3_t start, vec3_t goto done; } + cull_threshold = r_particles_count.value * 0.75f; + VectorScale(delta, 1.0 / num_particles, delta); for (i = 0; i < num_particles && free_particles; i++) { + // reki: if particles are choking, cull some randomly. + // this isn't elegant (earlier ents still take priority) but it does help a bit + if (ParticleCount > cull_threshold) + { + if (rand() > RAND_MAX * 0.15f) + { + continue; + } + } + color = col ? col : ColorForParticle(type); INIT_NEW_PARTICLE(pt, p, color, size, time); p->entity_ref = entity_ref; @@ -290,8 +303,13 @@ void ParticleNailTrail(centity_t* client_ent, float size, float life) } } +void ParticleNailTrail_NoEntity(vec3_t start, vec3_t end, float size, float life) +{ + AddParticleTrail(p_nailtrail, start, end, size, life, NULL); +} + //VULT PARTICLES -void FireballTrail(centity_t* cent, byte col[3], float size, float life) +void FireballTrailEntity(centity_t* cent, byte col[3], float size, float life) { col_t color; @@ -314,8 +332,39 @@ void FireballTrail(centity_t* cent, byte col[3], float size, float life) AddEntityParticleTrail(p_trailpart, cent, 2, size * 3, life, color); } +void FireballTrail(vec3_t start, vec3_t end, byte col[3], float size, float life) +{ + col_t color; + + color[0] = col[0]; + color[1] = col[1]; + color[2] = col[2]; + color[3] = 255; + + //head + AddParticleTrail(p_trailpart, start, end, size * 7.0f, 0.15f, color); + + //head-white part + color[0] = 255; color[1] = 255; color[2] = 255; + AddParticleTrail(p_trailpart, start, end, size * 5.0f, 0.15f, color); + + //medium trail + color[0] = col[0]; + color[1] = col[1]; + color[2] = col[2]; + AddParticleTrail(p_trailpart, start, end, size * 3.0f, life, color); +} + //VULT PARTICLES -void FireballTrailWave(centity_t* cent, byte col[3], float size, float life, vec3_t angle) +void FireballTrailWaveEntity(centity_t* cent, byte col[3], float size, float life, vec3_t angle) +{ + vec3_t vec; + AngleVectors(angle, vec, NULL, NULL); + vec[2] *= -1; + FireballTrailEntity(cent, col, size, life); +} + +void FireballTrailWave(vec3_t start, vec3_t end, byte col[3], float size, float life, vec3_t angle) { int i, j; vec3_t dir, vec; @@ -327,19 +376,19 @@ void FireballTrailWave(centity_t* cent, byte col[3], float size, float life, vec AngleVectors(angle, vec, NULL, NULL); vec[2] *= -1; - FireballTrail(cent, col, size, life); - if (cent->trails[2].lasttime == particle_time) { - for (i = 0; i < 3; i++) { - for (j = 0; j < 3; j++) { - dir[j] = vec[j] * -600 + ((rand() % 100) - 50); - } - AddParticle(p_streakwave, qmb_end(cent), 1, 1, 1, color, dir); + FireballTrail(start, end, col, size, life); + + for (i = 0; i < 3; i++) { + for (j = 0; j < 3; j++) { + dir[j] = vec[j] * -600 + ((rand() % 100) - 50); } + AddParticle(p_streakwave, end, 1, 1, 1, color, dir); } } + //VULT PARTICLES -void FuelRodGunTrail(centity_t* cent) +void FuelRodGunTrailEntity(centity_t* cent) { col_t color; int i, j; @@ -372,6 +421,21 @@ void FuelRodGunTrail(centity_t* cent) } } +void FuelRodGunTrail(vec3_t start, vec3_t end, vec3_t angles) +{ + col_t color; + color[3] = 255; + + color[0] = 0; color[1] = 255; color[2] = 0; + AddParticleTrail(p_trailpart, start, end, 15.0f, 0.2f, color); + color[0] = 255; color[1] = 255; color[2] = 255; + AddParticleTrail(p_trailpart, start, end, 10.0f, 0.2f, color); + color[0] = 0; color[1] = 128; color[2] = 0; + AddParticleTrail(p_trailpart, start, end, 10.0f, 0.5f, color); + color[0] = 0; color[1] = 22; color[2] = 0; + AddParticleTrail(p_trailpart, start, end, 2.0f, 3.0f, color); +} + //VULT PARTICLES //VULT - These trails were my initial motivation behind AMFQUAKE. void VX_ParticleTrail(vec3_t start, vec3_t end, float size, float time, col_t color) diff --git a/src/vx_stuff.h b/src/vx_stuff.h index 3a2fab0a5..96be86265 100644 --- a/src/vx_stuff.h +++ b/src/vx_stuff.h @@ -156,9 +156,13 @@ void VXTeleport(vec3_t org); void VXBlobExplosion(vec3_t org); void VXExplosion(vec3_t org); void VXBlood(vec3_t org, float count); -void FuelRodGunTrail(centity_t* cent); -void FireballTrail(centity_t* cent, byte col[3], float size, float life); -void FireballTrailWave(centity_t* cent, byte col[3], float size, float life, vec3_t angle); +void FuelRodGunTrailEntity(centity_t* cent); +void FuelRodGunTrail(vec3_t start, vec3_t end, vec3_t angles); +void FireballTrailEntity(centity_t* cent, byte col[3], float size, float life); +void FireballTrail(vec3_t start, vec3_t end, byte col[3], float size, float life); +void FireballTrailWaveEntity(centity_t* cent, byte col[3], float size, float life, vec3_t angle); +void FireballTrailWave(vec3_t start, vec3_t end, byte col[3], float size, float life, vec3_t angle); +void ParticleNailTrail_NoEntity(vec3_t start, vec3_t end, float size, float life); void DrawMuzzleflash(vec3_t start, vec3_t angle, vec3_t vel); void VXNailhit(vec3_t org, float count);