From a8f1730d58d7010ac207d3d267387aabe4b3320e Mon Sep 17 00:00:00 2001 From: MotoLegacy Date: Sat, 13 Jan 2024 12:53:48 -0500 Subject: [PATCH] FTE/CLIENT: Streamline CSQC Particle System --- progs/fte-client.src | 1 + source/client/main.qc | 146 ++------------------ source/client/particles.qc | 159 ++++++++++++++++++++++ source/server/clientfuncs.qc | 44 ++++-- source/server/weapons/grenade_launcher.qc | 13 +- source/server/weapons/weapon_core.qc | 28 +--- source/shared/defs/custom.qc | 70 ++++++---- 7 files changed, 248 insertions(+), 213 deletions(-) create mode 100644 source/client/particles.qc diff --git a/progs/fte-client.src b/progs/fte-client.src index 7dd2bc4..9a7c6b8 100644 --- a/progs/fte-client.src +++ b/progs/fte-client.src @@ -11,4 +11,5 @@ ../source/client/chat.qc ../source/client/user_input.qc ../source/client/view_model.qc +../source/client/particles.qc ../source/client/main.qc diff --git a/source/client/main.qc b/source/client/main.qc index b876c23..8a6303e 100644 --- a/source/client/main.qc +++ b/source/client/main.qc @@ -153,28 +153,10 @@ noref void(float apiver, string enginename, float enginever) CSQC_Init = } }; -// -// Init_Particles() -// Spawns a dummy particle for each effect -// so it loads. (FIXME - is there a better -// way?) -// -void() Init_Particles = -{ - for(int i = 0; i < 3; i++) { - pointparticles(particleeffectnum(strcat("muzzle.muzzle_pap_part", itos(i))), '0 0 0', '0 0 0', 0); - pointparticles(particleeffectnum(strcat("muzzle.muzzle_part", itos(i))), '0 0 0', '0 0 0', 0); - } - pointparticles(particleeffectnum("weapons.impact"), '0 0 0', '0 0 0', 0); - pointparticles(particleeffectnum("weapons.impact_decal"), '0 0 0', '0 0 0', 0); - pointparticles(particleeffectnum("weapons.explode"), '0 0 0', '0 0 0', 0); - pointparticles(particleeffectnum("blood.blood_particle"), '0 0 0', '0 0 0', 0); -} - noref void() CSQC_WorldLoaded = { Achievement_Init(); - Init_Particles(); + Particles_Init(); nameprint_time = time + 12; huddir = "gfx/hud/"; @@ -877,80 +859,18 @@ noref void() CSQC_Input_Frame = noref void() CSQC_Parse_Event = { - local float first = readbyte(); + float event_type = readbyte(); - switch (first) { - case EVENT_PISTOLFIRE: - local float entnum, traceent, side; - local vector pos, norm; - entnum = readentitynum(); - side = readfloat(); - pos_x = readcoord(); - pos_y = readcoord(); - pos_z = readcoord(); - norm_x = readcoord(); - norm_y = readcoord(); - norm_z = readcoord(); - - traceent = readentitynum(); - - // Muzzleflash Logic - if (entnum == player_localentnum && getstatf(STAT_WEAPONZOOM) != 2) - { - // originally was view_angles, but thats only what is reported - // to the server, weapon kick is NOT reported, so offset continued - // to grow. - getinputstate(servercommandframe); - makevectors(input_angles); - - vector muzzleflash_offset; - vector muzzleflash_position = getviewprop(VF_ORIGIN); - - // If firing the left side of a dual-wield weapon, use the left side muzzleflash offset. - if (side == 0 && IsDualWeapon(getstatf(STAT_ACTIVEWEAPON))) - muzzleflash_offset = WepDef_GetLeftFlashOffset(getstatf(STAT_ACTIVEWEAPON))/1000; - // Otherwise, use the standard offset. - else - muzzleflash_offset = GetWeaponFlash_Offset(getstatf(STAT_ACTIVEWEAPON))/1000; - - // Move to match ADS position if Zoomed in. - if(getstatf(STAT_WEAPONZOOM) == 1) { - muzzleflash_offset += GetWeaponADSOfs_PSP(getstatf(STAT_ACTIVEWEAPON))/1000; - } - - muzzleflash_position += v_forward * muzzleflash_offset_z; - muzzleflash_position += v_right * muzzleflash_offset_x; - muzzleflash_position += v_up * muzzleflash_offset_y; - - if (cvar("nzp_particles") && cvar("r_drawviewmodel")) { - float index = rint(random() * 2); - // Display Muzzleflash Particle and Dynamic Light - if (IsPapWeapon(getstatf(STAT_ACTIVEWEAPON))) { - pointparticles(particleeffectnum(strcat("muzzle.muzzle_pap_part", ftos(index))), muzzleflash_position, norm*24, 1); - - // Pack-A-Punched Weapons can display either a Blue or Red light - if (random() > 0.5) - dynamiclight_add(muzzleflash_position, 256, '0.7 0 0'); - else - dynamiclight_add(muzzleflash_position, 256, '0 0 0.7'); - } else { - pointparticles(particleeffectnum(strcat("muzzle.muzzle_part", ftos(index))), muzzleflash_position, norm*24, 1); - dynamiclight_add(muzzleflash_position, 256, '1.2 0.7 0.2'); - } - } - } - - if (GetFiretype(getstatf(STAT_ACTIVEWEAPON)) == FIRETYPE_GRENADE) - return; - - if(traceent == 0) - { - if (cvar("nzp_particles")) - pointparticles(particleeffectnum("weapons.impact"), pos, norm*24, 1); - - if (cvar("nzp_decals")) - pointparticles(particleeffectnum("weapons.impact_decal"), pos, '0 0 0', 1); - } + switch (event_type) { + case CSQC_EVENT_PARTICLE: + float particle_type = readbyte(); + float part_pos_x = readcoord(); + float part_pos_y = readcoord(); + float part_pos_z = readcoord(); + float part_optional = readbyte(); + float part_entity = readentitynum(); + vector particle_pos = [part_pos_x, part_pos_y, part_pos_z]; + Particles_RunParticle(particle_type, particle_pos, part_optional, part_entity); break; case EVENT_WEAPONRECOIL: local vector rec; @@ -960,48 +880,6 @@ noref void() CSQC_Parse_Event = gun_kick += rec; break; - case EVENT_EXPLOSION: - local vector org; - org_x = readcoord(); - org_y = readcoord(); - org_z = readcoord(); - - if (cvar("nzp_decals")) - pointparticles(particleeffectnum("weapons.explode"), org, '0 0 0', 1); - break; - case EVENT_BLOOD: - vector loc; - - loc_x = readcoord(); - loc_y = readcoord(); - loc_z = readcoord(); - - if (cvar("nzp_particles")) - pointparticles(particleeffectnum("blood.blood_particle"), loc, '0 0 0', 1); - break; - case EVENT_FLAME: - vector floc; - - floc_x = readcoord(); - floc_y = readcoord(); - floc_z = readcoord(); - - if (cvar("nzp_particles")) { - pointparticles(particleeffectnum("flames.flame_particle"), floc, '0 0 0', 1); - } - break; - case EVENT_LIMBGIB: - vector gloc; - - gloc_x = readcoord(); - gloc_y = readcoord(); - gloc_z = readcoord(); - - if (cvar("nzp_particles")) { - pointparticles(particleeffectnum("blood.blood_particle"), gloc, '0 0 0', 1); - pointparticles(particleeffectnum("blood.gibs"), gloc, '0 0 0', 1); - } - break; case EVENT_CHATMESSAGE: int sender = readbyte(); int player_id = readbyte(); diff --git a/source/client/particles.qc b/source/client/particles.qc new file mode 100644 index 0000000..aa5c2a4 --- /dev/null +++ b/source/client/particles.qc @@ -0,0 +1,159 @@ +/* + client/particles.qc + + CSQC-Driven Particle Runner. + + Copyright (C) 2021-2024 NZ:P Team + + 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 + +*/ + +// Stored outside of struct for stack efficency. +float particle_optional_field; +float particle_optional_entity; +vector particle_position; + +// +// Particles_MuzzleflashCallback() +// Executes Muzzleflash Particle effect. +// Optional: Which weapon was fired (left/right, for akimbo). +// +void() Particles_MuzzleflashCallback = +{ + // Do not display Muzzleflashes if we're using a Sniper Scope or not drawing the viewmodel. + if (getstatf(STAT_WEAPONZOOM) == 2 || !cvar("r_drawviewmodel")) + return; + + // Organized storage of optional_field and optional_entity. + float weapon_side = particle_optional_field; + float hit_entity = particle_optional_entity; + + // Obtain our input view angles. + getinputstate(servercommandframe); + makevectors(input_angles); + + vector muzzleflash_offset; + vector muzzleflash_position = getviewprop(VF_ORIGIN); + + // If firing the left side of a dual-wield weapon, use the left side muzzleflash offset. + if (weapon_side == 0 && IsDualWeapon(getstatf(STAT_ACTIVEWEAPON))) + muzzleflash_offset = WepDef_GetLeftFlashOffset(getstatf(STAT_ACTIVEWEAPON))/1000; + // Otherwise, use the standard offset. + else + muzzleflash_offset = GetWeaponFlash_Offset(getstatf(STAT_ACTIVEWEAPON))/1000; + + // Move to match ADS position if Zoomed in. + if(getstatf(STAT_WEAPONZOOM) == 1) { + muzzleflash_offset += GetWeaponADSOfs_PSP(getstatf(STAT_ACTIVEWEAPON))/1000; + } + + muzzleflash_position += v_forward * muzzleflash_offset_z; + muzzleflash_position += v_right * muzzleflash_offset_x; + muzzleflash_position += v_up * muzzleflash_offset_y; + + float muzzleflash_type = rint(random() * 2); // Choose one of three Muzzleflash variances. + + // Display Muzzleflash Particle and Dynamic Light + if (IsPapWeapon(getstatf(STAT_ACTIVEWEAPON))) { + pointparticles(particleeffectnum(strcat("muzzle.muzzle_pap_part", ftos(muzzleflash_type))), muzzleflash_position, '0 0 0', 1); + + // Pack-A-Punched Weapons can display either a Blue or Red light + if (random() > 0.5) + dynamiclight_add(muzzleflash_position, 256, '0.7 0 0'); + else + dynamiclight_add(muzzleflash_position, 256, '0 0 0.7'); + } else { + pointparticles(particleeffectnum(strcat("muzzle.muzzle_part", ftos(muzzleflash_type))), muzzleflash_position, '0 0 0', 1); + dynamiclight_add(muzzleflash_position, 256, '1.2 0.7 0.2'); + } + + // Grenade-Lunching weapons should not show decals. + if (GetFiretype(getstatf(STAT_ACTIVEWEAPON)) == FIRETYPE_GRENADE) + return; + + if(hit_entity == 0) { + pointparticles(particleeffectnum("weapons.impact"), particle_position, '0 0 0', 1); + pointparticles(particleeffectnum("weapons.impact_decal"), particle_position, '0 0 0', 1); + } +}; + +// This struct must be ordered linearly for fast array lookups. Do NOT skip indexes. +var struct +{ + float particle_type; // ID of Particle. + string particle_string; // String that refers to the Particle name/gamedir path. May be blank if callbacks utilized. + void() particle_func; // Callback that also gets executed on Particle_RunParticle, typically __NULL__. optional_field used here. +} csqc_particles[] = +{ + {CSQC_PART_MUZZLEFLASH, "", Particles_MuzzleflashCallback}, // View Entity Muzzleflashes. + {CSQC_PART_EXPLOSION, "weapons.explode", __NULL__}, // Explosion effect. + {CSQC_PART_BLOODIMPACT, "blood.blood_particle", __NULL__}, // Blood Impact effect. + {CSQC_PART_ZOMBIEGIB, "blood.gibs", __NULL__}, // Zombie Gib effect. + {CSQC_PART_FIRE, "flames.flame_particle", __NULL__} // Fire/flame effect. +}; + +// +// Particles_RunParticle(particle_type, position, optional_field, optional_entity) +// Runs specific particle given it's type, location, +// and optional fields to pass to an optional callback. +// +void(float particle_type, vector position, float optional_field, float optional_entity) Particles_RunParticle = +{ + // If the requested particle index does not have a callback function, + // run it outright. + if (csqc_particles[particle_type].particle_func == __NULL__) + pointparticles(particleeffectnum(csqc_particles[particle_type].particle_string), position, '0 0 0', 1); + // It has a callback -- so execute it instead. + else { + particle_optional_field = optional_field; + particle_optional_entity = optional_entity; + particle_position = position; + csqc_particles[particle_type].particle_func(); + particle_optional_field = particle_optional_entity = 0; + particle_position = '0 0 0'; + } +}; + +// +// Particles_Init() +// Particles never draw on their first run, so "allocate" +// them here. Particles only called via callbacks need +// invidiually referenced. +// +void() Particles_Init = +{ + int i; + // + // Callback Particles + // + + // Muzzleflashes. + for(i = 0; i < 3; i++) { + pointparticles(particleeffectnum(strcat("muzzle.muzzle_pap_part", itos(i))), '0 0 0', '0 0 0', 0); + pointparticles(particleeffectnum(strcat("muzzle.muzzle_part", itos(i))), '0 0 0', '0 0 0', 0); + } + + // + // Normal References + // + for(i = 0; i < csqc_particles.length; i++) { + pointparticles(particleeffectnum(csqc_particles[i].particle_string), '0 0 0', '0 0 0', 0); + } +}; \ No newline at end of file diff --git a/source/server/clientfuncs.qc b/source/server/clientfuncs.qc index 283693f..f685807 100644 --- a/source/server/clientfuncs.qc +++ b/source/server/clientfuncs.qc @@ -25,6 +25,34 @@ */ +#ifdef FTE + +// +// FTE_RunParticleEffect(target, particle_type, position, optional_field, optional_entity) +// Fires a CSQC_EVENT_PARTICLE to clients specified (or +// world for global). +// +void(entity target, float particle_type, vector position, float optional_field, entity optional_entity) FTE_RunParticleEffect = +{ + WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET); + WriteByte(MSG_MULTICAST, CSQC_EVENT_PARTICLE); + WriteByte(MSG_MULTICAST, particle_type); + WriteCoord(MSG_MULTICAST, position_x); + WriteCoord(MSG_MULTICAST, position_y); + WriteCoord(MSG_MULTICAST, position_z); + WriteByte(MSG_MULTICAST, optional_field); + WriteEntity(MSG_MULTICAST, optional_entity); + + if (target != world) { + msg_entity = target; + multicast(position, MULTICAST_ONE); + } else { + multicast(position, MULTICAST_ALL); + } +}; + +#endif // FTE + void() NotifyGameEnd = { #ifdef FTE @@ -104,12 +132,7 @@ void(vector org) CallExplosion = { #else - WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET); - WriteByte(MSG_MULTICAST, EVENT_EXPLOSION); - WriteCoord(MSG_MULTICAST, org_x); - WriteCoord(MSG_MULTICAST, org_y); - WriteCoord(MSG_MULTICAST, org_z); - multicast('0 0 0', MULTICAST_ALL); + FTE_RunParticleEffect(world, CSQC_PART_EXPLOSION, org, 0, world); #endif // FTE } @@ -652,14 +675,7 @@ void(vector org) Effect_Fire = #else - WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET); - WriteByte(MSG_MULTICAST, EVENT_FLAME); - WriteCoord(MSG_MULTICAST, org_x); - WriteCoord(MSG_MULTICAST, org_y); - WriteCoord(MSG_MULTICAST, org_z); - multicast('0 0 0', MULTICAST_ALL); - - //te_flamejet(self.origin, v_up*8, 10); + FTE_RunParticleEffect(world, CSQC_PART_FIRE, org, 0, world); #endif // FTE diff --git a/source/server/weapons/grenade_launcher.qc b/source/server/weapons/grenade_launcher.qc index 6f278b2..2e885ff 100644 --- a/source/server/weapons/grenade_launcher.qc +++ b/source/server/weapons/grenade_launcher.qc @@ -54,18 +54,7 @@ void(float side) W_FireGrenade = #ifdef FTE - WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET); - WriteByte(MSG_MULTICAST, EVENT_PISTOLFIRE); - WriteEntity(MSG_MULTICAST, self); - WriteFloat(MSG_MULTICAST, side); - WriteCoord(MSG_MULTICAST, 0); - WriteCoord(MSG_MULTICAST, 0); - WriteCoord(MSG_MULTICAST, 0); - WriteCoord(MSG_MULTICAST, 0); - WriteCoord(MSG_MULTICAST, 0); - WriteCoord(MSG_MULTICAST, 0); - WriteEntity(MSG_MULTICAST, world); - multicast(self.origin, MULTICAST_PHS); + FTE_RunParticleEffect(self, CSQC_PART_MUZZLEFLASH, '0 0 0', side, world); #endif // FTE } \ No newline at end of file diff --git a/source/server/weapons/weapon_core.qc b/source/server/weapons/weapon_core.qc index ccb8680..94e4ff2 100644 --- a/source/server/weapons/weapon_core.qc +++ b/source/server/weapons/weapon_core.qc @@ -560,13 +560,7 @@ void(vector org, vector vel, float damage) SpawnBlood = #ifdef FTE - WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET); - WriteByte(MSG_MULTICAST, EVENT_BLOOD); - WriteCoord(MSG_MULTICAST, org_x); - WriteCoord(MSG_MULTICAST, org_y); - WriteCoord(MSG_MULTICAST, org_z); - msg_entity = self; - multicast('0 0 0', MULTICAST_ONE); + FTE_RunParticleEffect(world, CSQC_PART_BLOODIMPACT, org, 0, world); #else @@ -586,12 +580,7 @@ void(vector where) spawn_gibs = { #else - WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET); - WriteByte(MSG_MULTICAST, EVENT_LIMBGIB); - WriteCoord(MSG_MULTICAST, where_x); - WriteCoord(MSG_MULTICAST, where_y); - WriteCoord(MSG_MULTICAST, where_z); - multicast('0 0 0', MULTICAST_ALL); + FTE_RunParticleEffect(world, CSQC_PART_ZOMBIEGIB, where, 0, world); #endif // FTE @@ -824,18 +813,7 @@ void(float damage, vector dir, vector org, vector plane, entity hit_ent, float s #ifdef FTE - WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET); - WriteByte(MSG_MULTICAST, EVENT_PISTOLFIRE); - WriteEntity(MSG_MULTICAST, self); - WriteFloat(MSG_MULTICAST, side); - WriteCoord(MSG_MULTICAST, org_x); - WriteCoord(MSG_MULTICAST, org_y); - WriteCoord(MSG_MULTICAST, org_z); - WriteCoord(MSG_MULTICAST, plane_x); - WriteCoord(MSG_MULTICAST, plane_y); - WriteCoord(MSG_MULTICAST, plane_z); - WriteEntity(MSG_MULTICAST, hit_ent); - multicast(trace_endpos, MULTICAST_PHS); + FTE_RunParticleEffect(self, CSQC_PART_MUZZLEFLASH, org, side, hit_ent); #endif // FTE diff --git a/source/shared/defs/custom.qc b/source/shared/defs/custom.qc index 3e417c9..635f101 100644 --- a/source/shared/defs/custom.qc +++ b/source/shared/defs/custom.qc @@ -28,37 +28,51 @@ #define true 1 #define false 0 -#define EVENT_PISTOLFIRE 10 +// +// CSQC Particle Types +// +#define CSQC_PART_MUZZLEFLASH 0 // View Entity Muzzleflashes. +#define CSQC_PART_EXPLOSION 1 // Explosion effect. +#define CSQC_PART_BLOODIMPACT 2 // Blood Impact effect. +#define CSQC_PART_ZOMBIEGIB 3 // Zombie Gib effect. +#define CSQC_PART_FIRE 4 // Fire/flame effect. + +// +// CSQC Event Types +// +#define CSQC_EVENT_PARTICLE 10 // particle_type + // pos_x + // pos_y + // pos_z + // optional_field + // optional_entity + #define EVENT_USEPRINT 11 #define EVENT_NEWROUND 12 #define EVENT_SETROUND 13 -#define EVENT_PERK 22 -#define EVENT_UPDATE 23 -#define EVENT_BROADCAST 24 -#define EVENT_POINTUPDATE 25 -#define EVENT_BLACKOUT 26 -#define EVENT_SCROLLTEXT 28 -#define EVENT_WORLDDATA 29 -#define EVENT_ACHIEVEMENT 30 -#define EVENT_PLAYERUPDATE 31 -#define EVENT_WEAPONUPDATE 32 -#define EVENT_HUDUPDATE 33 -#define EVENT_EXPLOSION 34 -#define EVENT_BLOOD 35 -#define EVENT_ACHIEVEMENTPROGRESS 36 -#define EVENT_REVIVEON 37 -#define EVENT_REVIVEOFF 38 -#define EVENT_REVIVECHANGE 39 -#define EVENT_WEAPONRECOIL 40 -#define EVENT_SONGPLAY 41 -#define EVENT_GRENADEPULSE 42 -#define EVENT_BETTYPROMPT 43 -#define EVENT_LIMBGIB 44 -#define EVENT_CHATMESSAGE 45 -#define EVENT_DOUBLETAPUPDATE 46 -#define EVENT_FLAME 47 -#define EVENT_ENDGAME 48 -#define EVENT_MAPTYPE 49 +#define EVENT_PERK 14 +#define EVENT_UPDATE 15 +#define EVENT_BROADCAST 16 +#define EVENT_POINTUPDATE 17 +#define EVENT_BLACKOUT 18 +#define EVENT_SCROLLTEXT 19 +#define EVENT_WORLDDATA 20 +#define EVENT_ACHIEVEMENT 21 +#define EVENT_PLAYERUPDATE 22 +#define EVENT_WEAPONUPDATE 23 +#define EVENT_HUDUPDATE 24 +#define EVENT_ACHIEVEMENTPROGRESS 25 +#define EVENT_REVIVEON 26 +#define EVENT_REVIVEOFF 27 +#define EVENT_REVIVECHANGE 28 +#define EVENT_WEAPONRECOIL 29 +#define EVENT_SONGPLAY 30 +#define EVENT_GRENADEPULSE 31 +#define EVENT_BETTYPROMPT 32 +#define EVENT_CHATMESSAGE 33 +#define EVENT_DOUBLETAPUPDATE 34 +#define EVENT_ENDGAME 35 +#define EVENT_MAPTYPE 36 // Define our FTE platform #ifndef STANDARD