1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853 |
- // Copyright (c) ZeniMax Media Inc.
- // Licensed under the GNU General Public License 2.0.
- #include "g_local.h"
- #include "m_player.h"
- #include "bots/bot_includes.h"
- void SP_misc_teleporter_dest(edict_t *ent);
- THINK(info_player_start_drop) (edict_t *self) -> void
- {
- // allow them to drop
- self->solid = SOLID_TRIGGER;
- self->movetype = MOVETYPE_TOSS;
- self->mins = PLAYER_MINS;
- self->maxs = PLAYER_MAXS;
- gi.linkentity(self);
- }
- /*QUAKED info_player_start (1 0 0) (-16 -16 -24) (16 16 32)
- The normal starting point for a level.
- */
- void SP_info_player_start(edict_t *self)
- {
- // fix stuck spawn points
- if (gi.trace(self->s.origin, PLAYER_MINS, PLAYER_MAXS, self->s.origin, self, MASK_SOLID).startsolid)
- G_FixStuckObject(self, self->s.origin);
- // [Paril-KEX] on n64, since these can spawn riding elevators,
- // allow them to "ride" the elevators so respawning works
- if (level.is_n64)
- {
- self->think = info_player_start_drop;
- self->nextthink = level.time + FRAME_TIME_S;
- }
- }
- /*QUAKED info_player_deathmatch (1 0 1) (-16 -16 -24) (16 16 32)
- potential spawning position for deathmatch games
- */
- void SP_info_player_deathmatch(edict_t *self)
- {
- if (!deathmatch->integer)
- {
- G_FreeEdict(self);
- return;
- }
- SP_misc_teleporter_dest(self);
- }
- /*QUAKED info_player_coop (1 0 1) (-16 -16 -24) (16 16 32)
- potential spawning position for coop games
- */
- void SP_info_player_coop(edict_t *self)
- {
- if (!coop->integer)
- {
- G_FreeEdict(self);
- return;
- }
- SP_info_player_start(self);
- }
- /*QUAKED info_player_coop_lava (1 0 1) (-16 -16 -24) (16 16 32)
- potential spawning position for coop games on rmine2 where lava level
- needs to be checked
- */
- void SP_info_player_coop_lava(edict_t *self)
- {
- if (!coop->integer)
- {
- G_FreeEdict(self);
- return;
- }
- // fix stuck spawn points
- if (gi.trace(self->s.origin, PLAYER_MINS, PLAYER_MAXS, self->s.origin, self, MASK_SOLID).startsolid)
- G_FixStuckObject(self, self->s.origin);
- }
- /*QUAKED info_player_intermission (1 0 1) (-16 -16 -24) (16 16 32)
- The deathmatch intermission point will be at one of these
- Use 'angles' instead of 'angle', so you can set pitch or roll as well as yaw. 'pitch yaw roll'
- */
- void SP_info_player_intermission(edict_t *ent)
- {
- }
- // [Paril-KEX] whether instanced items should be used or not
- bool P_UseCoopInstancedItems()
- {
- // squad respawn forces instanced items on, since we don't
- // want players to need to backtrack just to get their stuff.
- return g_coop_instanced_items->integer || g_coop_squad_respawn->integer;
- }
- //=======================================================================
- void ClientObituary(edict_t *self, edict_t *inflictor, edict_t *attacker, mod_t mod)
- {
- const char *base = nullptr;
- if (coop->integer && attacker->client)
- mod.friendly_fire = true;
- switch (mod.id)
- {
- case MOD_SUICIDE:
- base = "$g_mod_generic_suicide";
- break;
- case MOD_FALLING:
- base = "$g_mod_generic_falling";
- break;
- case MOD_CRUSH:
- base = "$g_mod_generic_crush";
- break;
- case MOD_WATER:
- base = "$g_mod_generic_water";
- break;
- case MOD_SLIME:
- base = "$g_mod_generic_slime";
- break;
- case MOD_LAVA:
- base = "$g_mod_generic_lava";
- break;
- case MOD_EXPLOSIVE:
- case MOD_BARREL:
- base = "$g_mod_generic_explosive";
- break;
- case MOD_EXIT:
- base = "$g_mod_generic_exit";
- break;
- case MOD_TARGET_LASER:
- base = "$g_mod_generic_laser";
- break;
- case MOD_TARGET_BLASTER:
- base = "$g_mod_generic_blaster";
- break;
- case MOD_BOMB:
- case MOD_SPLASH:
- case MOD_TRIGGER_HURT:
- base = "$g_mod_generic_hurt";
- break;
- // RAFAEL
- case MOD_GEKK:
- case MOD_BRAINTENTACLE:
- base = "$g_mod_generic_gekk";
- break;
- // RAFAEL
- default:
- base = nullptr;
- break;
- }
- if (attacker == self)
- {
- switch (mod.id)
- {
- case MOD_HELD_GRENADE:
- base = "$g_mod_self_held_grenade";
- break;
- case MOD_HG_SPLASH:
- case MOD_G_SPLASH:
- base = "$g_mod_self_grenade_splash";
- break;
- case MOD_R_SPLASH:
- base = "$g_mod_self_rocket_splash";
- break;
- case MOD_BFG_BLAST:
- base = "$g_mod_self_bfg_blast";
- break;
- // RAFAEL 03-MAY-98
- case MOD_TRAP:
- base = "$g_mod_self_trap";
- break;
- // RAFAEL
- // ROGUE
- case MOD_DOPPLE_EXPLODE:
- base = "$g_mod_self_dopple_explode";
- break;
- // ROGUE
- default:
- base = "$g_mod_self_default";
- break;
- }
- }
- // send generic/self
- if (base)
- {
- gi.LocBroadcast_Print(PRINT_MEDIUM, base, self->client->pers.netname);
- if (deathmatch->integer && !mod.no_point_loss)
- {
- self->client->resp.score--;
- if (teamplay->integer)
- G_AdjustTeamScore(self->client->resp.ctf_team, -1);
- }
- self->enemy = nullptr;
- return;
- }
- // has a killer
- self->enemy = attacker;
- if (attacker && attacker->client)
- {
- switch (mod.id)
- {
- case MOD_BLASTER:
- base = "$g_mod_kill_blaster";
- break;
- case MOD_SHOTGUN:
- base = "$g_mod_kill_shotgun";
- break;
- case MOD_SSHOTGUN:
- base = "$g_mod_kill_sshotgun";
- break;
- case MOD_MACHINEGUN:
- base = "$g_mod_kill_machinegun";
- break;
- case MOD_CHAINGUN:
- base = "$g_mod_kill_chaingun";
- break;
- case MOD_GRENADE:
- base = "$g_mod_kill_grenade";
- break;
- case MOD_G_SPLASH:
- base = "$g_mod_kill_grenade_splash";
- break;
- case MOD_ROCKET:
- base = "$g_mod_kill_rocket";
- break;
- case MOD_R_SPLASH:
- base = "$g_mod_kill_rocket_splash";
- break;
- case MOD_HYPERBLASTER:
- base = "$g_mod_kill_hyperblaster";
- break;
- case MOD_RAILGUN:
- base = "$g_mod_kill_railgun";
- break;
- case MOD_BFG_LASER:
- base = "$g_mod_kill_bfg_laser";
- break;
- case MOD_BFG_BLAST:
- base = "$g_mod_kill_bfg_blast";
- break;
- case MOD_BFG_EFFECT:
- base = "$g_mod_kill_bfg_effect";
- break;
- case MOD_HANDGRENADE:
- base = "$g_mod_kill_handgrenade";
- break;
- case MOD_HG_SPLASH:
- base = "$g_mod_kill_handgrenade_splash";
- break;
- case MOD_HELD_GRENADE:
- base = "$g_mod_kill_held_grenade";
- break;
- case MOD_TELEFRAG:
- case MOD_TELEFRAG_SPAWN:
- base = "$g_mod_kill_telefrag";
- break;
- // RAFAEL 14-APR-98
- case MOD_RIPPER:
- base = "$g_mod_kill_ripper";
- break;
- case MOD_PHALANX:
- base = "$g_mod_kill_phalanx";
- break;
- case MOD_TRAP:
- base = "$g_mod_kill_trap";
- break;
- // RAFAEL
- //===============
- // ROGUE
- case MOD_CHAINFIST:
- base = "$g_mod_kill_chainfist";
- break;
- case MOD_DISINTEGRATOR:
- base = "$g_mod_kill_disintegrator";
- break;
- case MOD_ETF_RIFLE:
- base = "$g_mod_kill_etf_rifle";
- break;
- case MOD_HEATBEAM:
- base = "$g_mod_kill_heatbeam";
- break;
- case MOD_TESLA:
- base = "$g_mod_kill_tesla";
- break;
- case MOD_PROX:
- base = "$g_mod_kill_prox";
- break;
- case MOD_NUKE:
- base = "$g_mod_kill_nuke";
- break;
- case MOD_VENGEANCE_SPHERE:
- base = "$g_mod_kill_vengeance_sphere";
- break;
- case MOD_DEFENDER_SPHERE:
- base = "$g_mod_kill_defender_sphere";
- break;
- case MOD_HUNTER_SPHERE:
- base = "$g_mod_kill_hunter_sphere";
- break;
- case MOD_TRACKER:
- base = "$g_mod_kill_tracker";
- break;
- case MOD_DOPPLE_EXPLODE:
- base = "$g_mod_kill_dopple_explode";
- break;
- case MOD_DOPPLE_VENGEANCE:
- base = "$g_mod_kill_dopple_vengeance";
- break;
- case MOD_DOPPLE_HUNTER:
- base = "$g_mod_kill_dopple_hunter";
- break;
- // ROGUE
- //===============
- // ZOID
- case MOD_GRAPPLE:
- base = "$g_mod_kill_grapple";
- break;
- // ZOID
- default:
- base = "$g_mod_kill_generic";
- break;
- }
- gi.LocBroadcast_Print(PRINT_MEDIUM, base, self->client->pers.netname, attacker->client->pers.netname);
- if (G_TeamplayEnabled())
- {
- // ZOID
- // if at start and same team, clear.
- // [Paril-KEX] moved here so it's not an outlier in player_die.
- if (mod.id == MOD_TELEFRAG_SPAWN &&
- self->client->resp.ctf_state < 2 &&
- self->client->resp.ctf_team == attacker->client->resp.ctf_team)
- {
- self->client->resp.ctf_state = 0;
- return;
- }
- }
- // ROGUE
- if (gamerules->integer)
- {
- if (DMGame.Score)
- {
- if (mod.friendly_fire)
- {
- if (!mod.no_point_loss)
- DMGame.Score(attacker, self, -1, mod);
- }
- else
- DMGame.Score(attacker, self, 1, mod);
- }
- return;
- }
- // ROGUE
- if (deathmatch->integer)
- {
- if (mod.friendly_fire)
- {
- if (!mod.no_point_loss)
- {
- attacker->client->resp.score--;
- if (teamplay->integer)
- G_AdjustTeamScore(attacker->client->resp.ctf_team, -1);
- }
- }
- else
- {
- attacker->client->resp.score++;
- if (teamplay->integer)
- G_AdjustTeamScore(attacker->client->resp.ctf_team, 1);
- }
- }
- else if (!coop->integer)
- self->client->resp.score--;
- return;
- }
- gi.LocBroadcast_Print(PRINT_MEDIUM, "$g_mod_generic_died", self->client->pers.netname);
- if (deathmatch->integer && !mod.no_point_loss)
- // ROGUE
- {
- if (gamerules->integer)
- {
- if (DMGame.Score)
- {
- DMGame.Score(self, self, -1, mod);
- }
- return;
- }
- else
- {
- self->client->resp.score--;
- if (teamplay->integer)
- G_AdjustTeamScore(attacker->client->resp.ctf_team, -1);
- }
- }
- // ROGUE
- }
- void TossClientWeapon(edict_t *self)
- {
- gitem_t *item;
- edict_t *drop;
- bool quad;
- // RAFAEL
- bool quadfire;
- // RAFAEL
- float spread;
- if (!deathmatch->integer)
- return;
- item = self->client->pers.weapon;
- if (item && g_instagib->integer)
- item = nullptr;
- if (item && !self->client->pers.inventory[self->client->pers.weapon->ammo])
- item = nullptr;
- if (item && !item->drop)
- item = nullptr;
- if (g_dm_no_quad_drop->integer)
- quad = false;
- else
- quad = (self->client->quad_time > (level.time + 1_sec));
- // RAFAEL
- if (g_dm_no_quadfire_drop->integer)
- quadfire = false;
- else
- quadfire = (self->client->quadfire_time > (level.time + 1_sec));
- // RAFAEL
- if (item && quad)
- spread = 22.5;
- // RAFAEL
- else if (item && quadfire)
- spread = 12.5;
- // RAFAEL
- else
- spread = 0.0;
- if (item)
- {
- self->client->v_angle[YAW] -= spread;
- drop = Drop_Item(self, item);
- self->client->v_angle[YAW] += spread;
- drop->spawnflags |= SPAWNFLAG_ITEM_DROPPED_PLAYER;
- drop->spawnflags &= ~SPAWNFLAG_ITEM_DROPPED;
- drop->svflags &= ~SVF_INSTANCED;
- }
- if (quad)
- {
- self->client->v_angle[YAW] += spread;
- drop = Drop_Item(self, GetItemByIndex(IT_ITEM_QUAD));
- self->client->v_angle[YAW] -= spread;
- drop->spawnflags |= SPAWNFLAG_ITEM_DROPPED_PLAYER;
- drop->spawnflags &= ~SPAWNFLAG_ITEM_DROPPED;
- drop->svflags &= ~SVF_INSTANCED;
- drop->touch = Touch_Item;
- drop->nextthink = self->client->quad_time;
- drop->think = G_FreeEdict;
- }
- // RAFAEL
- if (quadfire)
- {
- self->client->v_angle[YAW] += spread;
- drop = Drop_Item(self, GetItemByIndex(IT_ITEM_QUADFIRE));
- self->client->v_angle[YAW] -= spread;
- drop->spawnflags |= SPAWNFLAG_ITEM_DROPPED_PLAYER;
- drop->spawnflags &= ~SPAWNFLAG_ITEM_DROPPED;
- drop->svflags &= ~SVF_INSTANCED;
- drop->touch = Touch_Item;
- drop->nextthink = self->client->quadfire_time;
- drop->think = G_FreeEdict;
- }
- // RAFAEL
- }
- /*
- ==================
- LookAtKiller
- ==================
- */
- void LookAtKiller(edict_t *self, edict_t *inflictor, edict_t *attacker)
- {
- vec3_t dir;
- if (attacker && attacker != world && attacker != self)
- {
- dir = attacker->s.origin - self->s.origin;
- }
- else if (inflictor && inflictor != world && inflictor != self)
- {
- dir = inflictor->s.origin - self->s.origin;
- }
- else
- {
- self->client->killer_yaw = self->s.angles[YAW];
- return;
- }
- // PMM - fixed to correct for pitch of 0
- if (dir[0])
- self->client->killer_yaw = 180 / PIf * atan2f(dir[1], dir[0]);
- else if (dir[1] > 0)
- self->client->killer_yaw = 90;
- else if (dir[1] < 0)
- self->client->killer_yaw = 270;
- else
- self->client->killer_yaw = 0;
- }
- /*
- ==================
- player_die
- ==================
- */
- DIE(player_die) (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, const vec3_t &point, const mod_t &mod) -> void
- {
- PlayerTrail_Destroy(self);
- self->avelocity = {};
- self->takedamage = true;
- self->movetype = MOVETYPE_TOSS;
- self->s.modelindex2 = 0; // remove linked weapon model
- // ZOID
- self->s.modelindex3 = 0; // remove linked ctf flag
- // ZOID
- self->s.angles[0] = 0;
- self->s.angles[2] = 0;
- self->s.sound = 0;
- self->client->weapon_sound = 0;
- self->maxs[2] = -8;
- // self->solid = SOLID_NOT;
- self->svflags |= SVF_DEADMONSTER;
- if (!self->deadflag)
- {
- self->client->respawn_time = ( level.time + 1_sec );
- if ( deathmatch->integer && g_dm_force_respawn_time->integer ) {
- self->client->respawn_time = ( level.time + gtime_t::from_sec( g_dm_force_respawn_time->value ) );
- }
- LookAtKiller(self, inflictor, attacker);
- self->client->ps.pmove.pm_type = PM_DEAD;
- ClientObituary(self, inflictor, attacker, mod);
- CTFFragBonuses(self, inflictor, attacker);
- // ZOID
- TossClientWeapon(self);
- // ZOID
- CTFPlayerResetGrapple(self);
- CTFDeadDropFlag(self);
- CTFDeadDropTech(self);
- // ZOID
- if (deathmatch->integer && !self->client->showscores)
- Cmd_Help_f(self); // show scores
- if (coop->integer && !P_UseCoopInstancedItems())
- {
- // clear inventory
- // this is kind of ugly, but it's how we want to handle keys in coop
- for (int n = 0; n < IT_TOTAL; n++)
- {
- if (coop->integer && (itemlist[n].flags & IF_KEY))
- self->client->resp.coop_respawn.inventory[n] = self->client->pers.inventory[n];
- self->client->pers.inventory[n] = 0;
- }
- }
- }
- if (gamerules->integer) // if we're in a dm game, alert the game
- {
- if (DMGame.PlayerDeath)
- DMGame.PlayerDeath(self, inflictor, attacker);
- }
- // remove powerups
- self->client->quad_time = 0_ms;
- self->client->invincible_time = 0_ms;
- self->client->breather_time = 0_ms;
- self->client->enviro_time = 0_ms;
- self->client->invisible_time = 0_ms;
- self->flags &= ~FL_POWER_ARMOR;
- // clear inventory
- if (G_TeamplayEnabled())
- self->client->pers.inventory.fill(0);
- // RAFAEL
- self->client->quadfire_time = 0_ms;
- // RAFAEL
- //==============
- // ROGUE stuff
- self->client->double_time = 0_ms;
- // if there's a sphere around, let it know the player died.
- // vengeance and hunter will die if they're not attacking,
- // defender should always die
- if (self->client->owned_sphere)
- {
- edict_t *sphere;
- sphere = self->client->owned_sphere;
- sphere->die(sphere, self, self, 0, vec3_origin, mod);
- }
- // if we've been killed by the tracker, GIB!
- if (mod.id == MOD_TRACKER)
- {
- self->health = -100;
- damage = 400;
- }
- // make sure no trackers are still hurting us.
- if (self->client->tracker_pain_time)
- {
- RemoveAttackingPainDaemons(self);
- }
- // if we got obliterated by the nuke, don't gib
- if ((self->health < -80) && (mod.id == MOD_NUKE))
- self->flags |= FL_NOGIB;
- // ROGUE
- //==============
- if (self->health < -40)
- {
- // PMM
- // don't toss gibs if we got vaped by the nuke
- if (!(self->flags & FL_NOGIB))
- {
- // pmm
- // gib
- gi.sound(self, CHAN_BODY, gi.soundindex("misc/udeath.wav"), 1, ATTN_NORM, 0);
- // more meaty gibs for your dollar!
- if (deathmatch->integer && (self->health < -80))
- ThrowGibs(self, damage, { { 4, "models/objects/gibs/sm_meat/tris.md2" } });
-
- ThrowGibs(self, damage, { { 4, "models/objects/gibs/sm_meat/tris.md2" } });
- // PMM
- }
- self->flags &= ~FL_NOGIB;
- // pmm
- ThrowClientHead(self, damage);
- // ZOID
- self->client->anim_priority = ANIM_DEATH;
- self->client->anim_end = 0;
- // ZOID
- self->takedamage = false;
- }
- else
- { // normal death
- if (!self->deadflag)
- {
- // start a death animation
- self->client->anim_priority = ANIM_DEATH;
- if (self->client->ps.pmove.pm_flags & PMF_DUCKED)
- {
- self->s.frame = FRAME_crdeath1 - 1;
- self->client->anim_end = FRAME_crdeath5;
- }
- else
- {
- switch (irandom(3))
- {
- case 0:
- self->s.frame = FRAME_death101 - 1;
- self->client->anim_end = FRAME_death106;
- break;
- case 1:
- self->s.frame = FRAME_death201 - 1;
- self->client->anim_end = FRAME_death206;
- break;
- case 2:
- self->s.frame = FRAME_death301 - 1;
- self->client->anim_end = FRAME_death308;
- break;
- }
- }
- static constexpr const char *death_sounds[] = {
- "*death1.wav",
- "*death2.wav",
- "*death3.wav",
- "*death4.wav"
- };
- gi.sound(self, CHAN_VOICE, gi.soundindex(random_element(death_sounds)), 1, ATTN_NORM, 0);
- self->client->anim_time = 0_ms;
- }
- }
- if (!self->deadflag)
- {
- if (coop->integer && (g_coop_squad_respawn->integer || g_coop_enable_lives->integer))
- {
- if (g_coop_enable_lives->integer && self->client->pers.lives)
- {
- self->client->pers.lives--;
- self->client->resp.coop_respawn.lives--;
- }
- bool allPlayersDead = true;
- for (auto player : active_players())
- if (player->health > 0 || (!level.deadly_kill_box && g_coop_enable_lives->integer && player->client->pers.lives > 0))
- {
- allPlayersDead = false;
- break;
- }
- if (allPlayersDead) // allow respawns for telefrags and weird shit
- {
- level.coop_level_restart_time = level.time + 5_sec;
- for (auto player : active_players())
- gi.LocCenter_Print(player, "$g_coop_lose");
- }
-
- // in 3 seconds, attempt a respawn or put us into
- // spectator mode
- if (!level.coop_level_restart_time)
- self->client->respawn_time = level.time + 3_sec;
- }
- }
- self->deadflag = true;
- gi.linkentity(self);
- }
- //=======================================================================
- #include <string>
- #include <sstream>
- // [Paril-KEX]
- static void Player_GiveStartItems(edict_t *ent, const char *ptr)
- {
- char token_copy[MAX_TOKEN_CHARS];
- const char *token;
- while (*(token = COM_ParseEx(&ptr, ";")))
- {
- Q_strlcpy(token_copy, token, sizeof(token_copy));
- const char *ptr_copy = token_copy;
- const char *item_name = COM_Parse(&ptr_copy);
- gitem_t *item = FindItemByClassname(item_name);
- if (!item || !item->pickup)
- gi.Com_ErrorFmt("Invalid g_start_item entry: {}\n", item_name);
- int32_t count = 1;
- if (*ptr_copy)
- count = atoi(COM_Parse(&ptr_copy));
- if (count == 0)
- {
- ent->client->pers.inventory[item->id] = 0;
- continue;
- }
- edict_t *dummy = G_Spawn();
- dummy->item = item;
- dummy->count = count;
- dummy->spawnflags |= SPAWNFLAG_ITEM_DROPPED;
- item->pickup(dummy, ent);
- G_FreeEdict(dummy);
- }
- }
- /*
- ==============
- InitClientPersistant
- This is only called when the game first initializes in single player,
- but is called after each death and level change in deathmatch
- ==============
- */
- void InitClientPersistant(edict_t *ent, gclient_t *client)
- {
- // backup & restore userinfo
- char userinfo[MAX_INFO_STRING];
- Q_strlcpy(userinfo, client->pers.userinfo, sizeof(userinfo));
- memset(&client->pers, 0, sizeof(client->pers));
- ClientUserinfoChanged(ent, userinfo);
- client->pers.health = 100;
- client->pers.max_health = 100;
- // don't give us weapons if we shouldn't have any
- if ((G_TeamplayEnabled() && client->resp.ctf_team != CTF_NOTEAM) ||
- (!G_TeamplayEnabled() && !client->resp.spectator))
- {
- // in coop, if there's already a player in the game and we're new,
- // steal their loadout. this would fix a potential softlock where a new
- // player may not have weapons at all.
- bool taken_loadout = false;
- if (coop->integer)
- {
- for (auto player : active_players())
- {
- if (player == ent || !player->client->pers.spawned ||
- player->client->resp.spectator || player->movetype == MOVETYPE_NOCLIP)
- continue;
- client->pers.inventory = player->client->pers.inventory;
- client->pers.max_ammo = player->client->pers.max_ammo;
- client->pers.power_cubes = player->client->pers.power_cubes;
- taken_loadout = true;
- break;
- }
- }
- if (!taken_loadout)
- {
- // fill with 50s, since it's our most common value
- client->pers.max_ammo.fill(50);
- client->pers.max_ammo[AMMO_BULLETS] = 200;
- client->pers.max_ammo[AMMO_SHELLS] = 100;
- client->pers.max_ammo[AMMO_CELLS] = 200;
- // RAFAEL
- client->pers.max_ammo[AMMO_TRAP] = 5;
- // RAFAEL
- // ROGUE
- client->pers.max_ammo[AMMO_FLECHETTES] = 200;
- client->pers.max_ammo[AMMO_DISRUPTOR] = 12;
- client->pers.max_ammo[AMMO_TESLA] = 5;
- // ROGUE
- if (!deathmatch->integer || !g_instagib->integer)
- client->pers.inventory[IT_WEAPON_BLASTER] = 1;
- // [Kex]
- // start items!
- if (*g_start_items->string)
- Player_GiveStartItems(ent, g_start_items->string);
- else if (deathmatch->integer && g_instagib->integer)
- {
- client->pers.inventory[IT_WEAPON_RAILGUN] = 1;
- client->pers.inventory[IT_AMMO_SLUGS] = 99;
- }
- if (level.start_items && *level.start_items)
- Player_GiveStartItems(ent, level.start_items);
- if (!deathmatch->integer)
- client->pers.inventory[IT_ITEM_COMPASS] = 1;
- // ZOID
- bool give_grapple = (!strcmp(g_allow_grapple->string, "auto")) ?
- (ctf->integer ? !level.no_grapple : 0) :
- g_allow_grapple->integer;
- if (give_grapple)
- client->pers.inventory[IT_WEAPON_GRAPPLE] = 1;
- // ZOID
- }
- NoAmmoWeaponChange(ent, false);
- client->pers.weapon = client->newweapon;
- if (client->newweapon)
- client->pers.selected_item = client->newweapon->id;
- client->newweapon = nullptr;
- // ZOID
- client->pers.lastweapon = client->pers.weapon;
- // ZOID
- }
- if (coop->value && g_coop_enable_lives->integer)
- client->pers.lives = g_coop_num_lives->integer + 1;
- if (ent->client->pers.autoshield >= AUTO_SHIELD_AUTO)
- ent->flags |= FL_WANTS_POWER_ARMOR;
- client->pers.connected = true;
- client->pers.spawned = true;
- }
- void InitClientResp(gclient_t *client)
- {
- // ZOID
- ctfteam_t ctf_team = client->resp.ctf_team;
- bool id_state = client->resp.id_state;
- // ZOID
- memset(&client->resp, 0, sizeof(client->resp));
- // ZOID
- client->resp.ctf_team = ctf_team;
- client->resp.id_state = id_state;
- // ZOID
- client->resp.entertime = level.time;
- client->resp.coop_respawn = client->pers;
- }
- /*
- ==================
- SaveClientData
- Some information that should be persistant, like health,
- is still stored in the edict structure, so it needs to
- be mirrored out to the client structure before all the
- edicts are wiped.
- ==================
- */
- void SaveClientData()
- {
- edict_t *ent;
- for (uint32_t i = 0; i < game.maxclients; i++)
- {
- ent = &g_edicts[1 + i];
- if (!ent->inuse)
- continue;
- game.clients[i].pers.health = ent->health;
- game.clients[i].pers.max_health = ent->max_health;
- game.clients[i].pers.savedFlags = (ent->flags & (FL_FLASHLIGHT | FL_GODMODE | FL_NOTARGET | FL_POWER_ARMOR | FL_WANTS_POWER_ARMOR));
- if (coop->integer)
- game.clients[i].pers.score = ent->client->resp.score;
- }
- }
- void FetchClientEntData(edict_t *ent)
- {
- ent->health = ent->client->pers.health;
- ent->max_health = ent->client->pers.max_health;
- ent->flags |= ent->client->pers.savedFlags;
- if (coop->integer)
- ent->client->resp.score = ent->client->pers.score;
- }
- /*
- =======================================================================
- SelectSpawnPoint
- =======================================================================
- */
- /*
- ================
- PlayersRangeFromSpot
- Returns the distance to the nearest player from the given spot
- ================
- */
- float PlayersRangeFromSpot(edict_t *spot)
- {
- edict_t *player;
- float bestplayerdistance;
- vec3_t v;
- float playerdistance;
- bestplayerdistance = 9999999;
- for (uint32_t n = 1; n <= game.maxclients; n++)
- {
- player = &g_edicts[n];
- if (!player->inuse)
- continue;
- if (player->health <= 0)
- continue;
- v = spot->s.origin - player->s.origin;
- playerdistance = v.length();
- if (playerdistance < bestplayerdistance)
- bestplayerdistance = playerdistance;
- }
- return bestplayerdistance;
- }
- bool SpawnPointClear(edict_t *spot)
- {
- vec3_t p = spot->s.origin + vec3_t{0, 0, 9.f};
- return !gi.trace(p, PLAYER_MINS, PLAYER_MAXS, p, spot, CONTENTS_PLAYER | CONTENTS_MONSTER).startsolid;
- }
- select_spawn_result_t SelectDeathmatchSpawnPoint(bool farthest, bool force_spawn, bool fallback_to_ctf_or_start)
- {
- struct spawn_point_t
- {
- edict_t *point;
- float dist;
- };
- static std::vector<spawn_point_t> spawn_points;
- spawn_points.clear();
- // gather all spawn points
- edict_t *spot = nullptr;
- while ((spot = G_FindByString<&edict_t::classname>(spot, "info_player_deathmatch")) != nullptr)
- spawn_points.push_back({ spot, PlayersRangeFromSpot(spot) });
- // no points
- if (spawn_points.size() == 0)
- {
- // try CTF spawns...
- if (fallback_to_ctf_or_start)
- {
- spot = nullptr;
- while ((spot = G_FindByString<&edict_t::classname>(spot, "info_player_team1")) != nullptr)
- spawn_points.push_back({ spot, PlayersRangeFromSpot(spot) });
- spot = nullptr;
- while ((spot = G_FindByString<&edict_t::classname>(spot, "info_player_team2")) != nullptr)
- spawn_points.push_back({ spot, PlayersRangeFromSpot(spot) });
- // we only have an info_player_start then
- if (spawn_points.size() == 0)
- {
- spot = G_FindByString<&edict_t::classname>(nullptr, "info_player_start");
- if (spot)
- spawn_points.push_back({ spot, PlayersRangeFromSpot(spot) });
- // map is malformed
- if (spawn_points.size() == 0)
- return { nullptr, false };
- }
- }
- else
- return { nullptr, false };
- }
- // if there's only one spawn point, that's the one.
- if (spawn_points.size() == 1)
- {
- if (force_spawn || SpawnPointClear(spawn_points[0].point))
- return { spawn_points[0].point, true };
- return { nullptr, true };
- }
- // order by distances ascending (top of list has closest players to point)
- std::sort(spawn_points.begin(), spawn_points.end(), [](const spawn_point_t &a, const spawn_point_t &b) { return a.dist < b.dist; });
- // farthest spawn is simple
- if (farthest)
- {
- for (int32_t i = spawn_points.size() - 1; i >= 0; --i)
- {
- if (SpawnPointClear(spawn_points[i].point))
- return { spawn_points[i].point, true };
- }
- // none clear
- }
- else
- {
- // for random, select a random point other than the two
- // that are closest to the player if possible.
- // shuffle the non-distance-related spawn points
- std::shuffle(spawn_points.begin() + 2, spawn_points.end(), mt_rand);
- // run down the list and pick the first one that we can use
- for (auto it = spawn_points.begin() + 2; it != spawn_points.end(); ++it)
- {
- auto spot = it->point;
- if (SpawnPointClear(spot))
- return { spot, true };
- }
- // none clear, so we have to pick one of the other two
- if (SpawnPointClear(spawn_points[1].point))
- return { spawn_points[1].point, true };
- else if (SpawnPointClear(spawn_points[0].point))
- return { spawn_points[0].point, true };
- }
-
- if (force_spawn)
- return { random_element(spawn_points).point, true };
- return { nullptr, true };
- }
- //===============
- // ROGUE
- edict_t *SelectLavaCoopSpawnPoint(edict_t *ent)
- {
- int index;
- edict_t *spot = nullptr;
- float lavatop;
- edict_t *lava;
- edict_t *pointWithLeastLava;
- float lowest;
- edict_t *spawnPoints[64];
- vec3_t center;
- int numPoints;
- edict_t *highestlava;
- lavatop = -99999;
- highestlava = nullptr;
- // first, find the highest lava
- // remember that some will stop moving when they've filled their
- // areas...
- lava = nullptr;
- while (1)
- {
- lava = G_FindByString<&edict_t::classname>(lava, "func_water");
- if (!lava)
- break;
- center = lava->absmax + lava->absmin;
- center *= 0.5f;
- if (lava->spawnflags.has(SPAWNFLAG_WATER_SMART) && (gi.pointcontents(center) & MASK_WATER))
- {
- if (lava->absmax[2] > lavatop)
- {
- lavatop = lava->absmax[2];
- highestlava = lava;
- }
- }
- }
- // if we didn't find ANY lava, then return nullptr
- if (!highestlava)
- return nullptr;
- // find the top of the lava and include a small margin of error (plus bbox size)
- lavatop = highestlava->absmax[2] + 64;
- // find all the lava spawn points and store them in spawnPoints[]
- spot = nullptr;
- numPoints = 0;
- while ((spot = G_FindByString<&edict_t::classname>(spot, "info_player_coop_lava")))
- {
- if (numPoints == 64)
- break;
- spawnPoints[numPoints++] = spot;
- }
- // walk up the sorted list and return the lowest, open, non-lava spawn point
- spot = nullptr;
- lowest = 999999;
- pointWithLeastLava = nullptr;
- for (index = 0; index < numPoints; index++)
- {
- if (spawnPoints[index]->s.origin[2] < lavatop)
- continue;
- if (PlayersRangeFromSpot(spawnPoints[index]) > 32)
- {
- if (spawnPoints[index]->s.origin[2] < lowest)
- {
- // save the last point
- pointWithLeastLava = spawnPoints[index];
- lowest = spawnPoints[index]->s.origin[2];
- }
- }
- }
- return pointWithLeastLava;
- }
- // ROGUE
- //===============
- // [Paril-KEX]
- static edict_t *SelectSingleSpawnPoint(edict_t *ent)
- {
- edict_t *spot = nullptr;
- while ((spot = G_FindByString<&edict_t::classname>(spot, "info_player_start")) != nullptr)
- {
- if (!game.spawnpoint[0] && !spot->targetname)
- break;
- if (!game.spawnpoint[0] || !spot->targetname)
- continue;
- if (Q_strcasecmp(game.spawnpoint, spot->targetname) == 0)
- break;
- }
- if (!spot)
- {
- // there wasn't a matching targeted spawnpoint, use one that has no targetname
- while ((spot = G_FindByString<&edict_t::classname>(spot, "info_player_start")) != nullptr)
- if (!spot->targetname)
- return spot;
- }
- // none at all, so just pick any
- if (!spot)
- return G_FindByString<&edict_t::classname>(spot, "info_player_start");
- return spot;
- }
- // [Paril-KEX]
- static edict_t *G_UnsafeSpawnPosition(vec3_t spot, bool check_players)
- {
- contents_t mask = MASK_PLAYERSOLID;
- if (!check_players)
- mask &= ~CONTENTS_PLAYER;
- trace_t tr = gi.trace(spot, PLAYER_MINS, PLAYER_MAXS, spot, nullptr, mask);
- // sometimes the spot is too close to the ground, give it a bit of slack
- if (tr.startsolid && !tr.ent->client)
- {
- spot[2] += 1;
- tr = gi.trace(spot, PLAYER_MINS, PLAYER_MAXS, spot, nullptr, mask);
- }
- // no idea why this happens in some maps..
- if (tr.startsolid && !tr.ent->client)
- {
- // try a nudge
- if (G_FixStuckObject_Generic(spot, PLAYER_MINS, PLAYER_MAXS, [mask] (const vec3_t &start, const vec3_t &mins, const vec3_t &maxs, const vec3_t &end) {
- return gi.trace(start, mins, maxs, end, nullptr, mask);
- }) == stuck_result_t::NO_GOOD_POSITION)
- return tr.ent; // what do we do here...?
- trace_t tr = gi.trace(spot, PLAYER_MINS, PLAYER_MAXS, spot, nullptr, mask);
- if (tr.startsolid && !tr.ent->client)
- return tr.ent; // what do we do here...?
- }
- if (tr.fraction == 1.f)
- return nullptr;
- else if (check_players && tr.ent && tr.ent->client)
- return tr.ent;
- return nullptr;
- }
- edict_t *SelectCoopSpawnPoint(edict_t *ent, bool force_spawn, bool check_players)
- {
- edict_t *spot = nullptr;
- const char *target;
- // ROGUE
- // rogue hack, but not too gross...
- if (!Q_strcasecmp(level.mapname, "rmine2"))
- return SelectLavaCoopSpawnPoint(ent);
- // ROGUE
- // try the main spawn point first
- spot = SelectSingleSpawnPoint(ent);
- if (spot && !G_UnsafeSpawnPosition(spot->s.origin, check_players))
- return spot;
- spot = nullptr;
- // assume there are four coop spots at each spawnpoint
- int32_t num_valid_spots = 0;
- while (1)
- {
- spot = G_FindByString<&edict_t::classname>(spot, "info_player_coop");
- if (!spot)
- break; // we didn't have enough...
- target = spot->targetname;
- if (!target)
- target = "";
- if (Q_strcasecmp(game.spawnpoint, target) == 0)
- { // this is a coop spawn point for one of the clients here
- num_valid_spots++;
- if (!G_UnsafeSpawnPosition(spot->s.origin, check_players))
- return spot; // this is it
- }
- }
- bool use_targetname = true;
- // if we didn't find any spots, map is probably set up wrong.
- // use empty targetname ones.
- if (!num_valid_spots)
- {
- use_targetname = false;
- while (1)
- {
- spot = G_FindByString<&edict_t::classname>(spot, "info_player_coop");
- if (!spot)
- break; // we didn't have enough...
- target = spot->targetname;
- if (!target)
- {
- // this is a coop spawn point for one of the clients here
- num_valid_spots++;
- if (!G_UnsafeSpawnPosition(spot->s.origin, check_players))
- return spot; // this is it
- }
- }
- }
- // if player collision is disabled, just pick a random spot
- if (!g_coop_player_collision->integer)
- {
- spot = nullptr;
- num_valid_spots = irandom(num_valid_spots);
- while (1)
- {
- spot = G_FindByString<&edict_t::classname>(spot, "info_player_coop");
- if (!spot)
- break; // we didn't have enough...
- target = spot->targetname;
- if (use_targetname && !target)
- target = "";
- if (use_targetname ? (Q_strcasecmp(game.spawnpoint, target) == 0) : !target)
- { // this is a coop spawn point for one of the clients here
- num_valid_spots++;
- if (!num_valid_spots)
- return spot;
- --num_valid_spots;
- }
- }
- // if this fails, just fall through to some other spawn.
- }
- // no safe spots..?
- if (force_spawn || !g_coop_player_collision->integer)
- return SelectSingleSpawnPoint(spot);
-
- return nullptr;
- }
- bool TryLandmarkSpawn(edict_t* ent, vec3_t& origin, vec3_t& angles)
- {
- // if transitioning from another level with a landmark seamless transition
- // just set the location here
- if (!ent->client->landmark_name || !strlen(ent->client->landmark_name))
- {
- return false;
- }
- edict_t* landmark = G_PickTarget(ent->client->landmark_name);
- if (!landmark)
- {
- return false;
- }
- vec3_t old_origin = origin;
- vec3_t spot_origin = origin;
- origin = ent->client->landmark_rel_pos;
-
- // rotate our relative landmark into our new landmark's frame of reference
- origin = RotatePointAroundVector({ 1, 0, 0 }, origin, landmark->s.angles[0]);
- origin = RotatePointAroundVector({ 0, 1, 0 }, origin, landmark->s.angles[2]);
- origin = RotatePointAroundVector({ 0, 0, 1 }, origin, landmark->s.angles[1]);
- origin += landmark->s.origin;
- angles = ent->client->oldviewangles + landmark->s.angles;
- if (landmark->spawnflags.has(SPAWNFLAG_LANDMARK_KEEP_Z))
- origin[2] = spot_origin[2];
- // sometimes, landmark spawns can cause slight inconsistencies in collision;
- // we'll do a bit of tracing to make sure the bbox is clear
- if (G_FixStuckObject_Generic(origin, PLAYER_MINS, PLAYER_MAXS, [ent] (const vec3_t &start, const vec3_t &mins, const vec3_t &maxs, const vec3_t &end) {
- return gi.trace(start, mins, maxs, end, ent, MASK_PLAYERSOLID & ~CONTENTS_PLAYER);
- }) == stuck_result_t::NO_GOOD_POSITION)
- {
- origin = old_origin;
- return false;
- }
- ent->s.origin = origin;
- // rotate the velocity that we grabbed from the map
- if (ent->velocity)
- {
- ent->velocity = RotatePointAroundVector({ 1, 0, 0 }, ent->velocity, landmark->s.angles[0]);
- ent->velocity = RotatePointAroundVector({ 0, 1, 0 }, ent->velocity, landmark->s.angles[2]);
- ent->velocity = RotatePointAroundVector({ 0, 0, 1 }, ent->velocity, landmark->s.angles[1]);
- }
- return true;
- }
- /*
- ===========
- SelectSpawnPoint
- Chooses a player start, deathmatch start, coop start, etc
- ============
- */
- bool SelectSpawnPoint(edict_t *ent, vec3_t &origin, vec3_t &angles, bool force_spawn, bool &landmark)
- {
- edict_t *spot = nullptr;
- // DM spots are simple
- if (deathmatch->integer)
- {
- if (G_TeamplayEnabled())
- spot = SelectCTFSpawnPoint(ent, force_spawn);
- else
- {
- select_spawn_result_t result = SelectDeathmatchSpawnPoint(g_dm_spawn_farthest->integer, force_spawn, true);
- if (!result.any_valid)
- gi.Com_Error("no valid spawn points found");
- spot = result.spot;
- }
- if (spot)
- {
- origin = spot->s.origin + vec3_t{ 0, 0, 9 };
- angles = spot->s.angles;
- return true;
- }
- return false;
- }
-
- if (coop->integer)
- {
- spot = SelectCoopSpawnPoint(ent, force_spawn, true);
- if (!spot)
- spot = SelectCoopSpawnPoint(ent, force_spawn, false);
- // no open spot yet
- if (!spot)
- {
- // in worst case scenario in coop during intermission, just spawn us at intermission
- // spot. this only happens for a single frame, and won't break
- // anything if they come back.
- if (level.intermissiontime)
- {
- origin = level.intermission_origin;
- angles = level.intermission_angle;
- return true;
- }
- return false;
- }
- }
- else
- {
- spot = SelectSingleSpawnPoint(ent);
- // in SP, just put us at the origin if spawn fails
- if (!spot)
- {
- gi.Com_PrintFmt("Couldn't find spawn point {}\n", game.spawnpoint);
- origin = { 0, 0, 0 };
- angles = { 0, 0, 0 };
- return true;
- }
- }
- // spot should always be non-null here
-
- origin = spot->s.origin;
- angles = spot->s.angles;
- // check landmark
- if (TryLandmarkSpawn(ent, origin, angles))
- landmark = true;
- return true;
- }
- //======================================================================
- void InitBodyQue()
- {
- int i;
- edict_t *ent;
- level.body_que = 0;
- for (i = 0; i < BODY_QUEUE_SIZE; i++)
- {
- ent = G_Spawn();
- ent->classname = "bodyque";
- }
- }
- DIE(body_die) (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, const vec3_t &point, const mod_t &mod) -> void
- {
- if (self->s.modelindex == MODELINDEX_PLAYER &&
- self->health < self->gib_health)
- {
- gi.sound(self, CHAN_BODY, gi.soundindex("misc/udeath.wav"), 1, ATTN_NORM, 0);
- ThrowGibs(self, damage, { { 4, "models/objects/gibs/sm_meat/tris.md2" } });
- self->s.origin[2] -= 48;
- ThrowClientHead(self, damage);
- }
- if (mod.id == MOD_CRUSH)
- {
- // prevent explosion singularities
- self->svflags = SVF_NOCLIENT;
- self->takedamage = false;
- self->solid = SOLID_NOT;
- self->movetype = MOVETYPE_NOCLIP;
- gi.linkentity(self);
- }
- }
- void CopyToBodyQue(edict_t *ent)
- {
- // if we were completely removed, don't bother with a body
- if (!ent->s.modelindex)
- return;
- edict_t *body;
- // grab a body que and cycle to the next one
- body = &g_edicts[game.maxclients + level.body_que + 1];
- level.body_que = (level.body_que + 1) % BODY_QUEUE_SIZE;
- // FIXME: send an effect on the removed body
- gi.unlinkentity(ent);
- gi.unlinkentity(body);
- body->s = ent->s;
- body->s.number = body - g_edicts;
- body->s.skinnum = ent->s.skinnum & 0xFF; // only copy the client #
- body->s.effects = EF_NONE;
- body->s.renderfx = RF_NONE;
- body->svflags = ent->svflags;
- body->absmin = ent->absmin;
- body->absmax = ent->absmax;
- body->size = ent->size;
- body->solid = ent->solid;
- body->clipmask = ent->clipmask;
- body->owner = ent->owner;
- body->movetype = ent->movetype;
- body->health = ent->health;
- body->gib_health = ent->gib_health;
- body->s.event = EV_OTHER_TELEPORT;
- body->velocity = ent->velocity;
- body->avelocity = ent->avelocity;
- body->groundentity = ent->groundentity;
- body->groundentity_linkcount = ent->groundentity_linkcount;
- if (ent->takedamage)
- {
- body->mins = ent->mins;
- body->maxs = ent->maxs;
- }
- else
- body->mins = body->maxs = {};
- body->die = body_die;
- body->takedamage = true;
- gi.linkentity(body);
- }
- void G_PostRespawn(edict_t *self)
- {
- if (self->svflags & SVF_NOCLIENT)
- return;
- // add a teleportation effect
- self->s.event = EV_PLAYER_TELEPORT;
- // hold in place briefly
- self->client->ps.pmove.pm_flags = PMF_TIME_TELEPORT;
- self->client->ps.pmove.pm_time = 112;
- self->client->respawn_time = level.time;
- }
- void respawn(edict_t *self)
- {
- if (deathmatch->integer || coop->integer)
- {
- // spectators don't leave bodies
- if (!self->client->resp.spectator)
- CopyToBodyQue(self);
- self->svflags &= ~SVF_NOCLIENT;
- PutClientInServer(self);
- G_PostRespawn(self);
- return;
- }
- // restart the entire server
- gi.AddCommandString("menu_loadgame\n");
- }
- /*
- * only called when pers.spectator changes
- * note that resp.spectator should be the opposite of pers.spectator here
- */
- void spectator_respawn(edict_t *ent)
- {
- uint32_t i, numspec;
- // if the user wants to become a spectator, make sure he doesn't
- // exceed max_spectators
- if (ent->client->pers.spectator)
- {
- char value[MAX_INFO_VALUE] = { 0 };
- gi.Info_ValueForKey(ent->client->pers.userinfo, "spectator", value, sizeof(value));
- if (*spectator_password->string &&
- strcmp(spectator_password->string, "none") &&
- strcmp(spectator_password->string, value))
- {
- gi.LocClient_Print(ent, PRINT_HIGH, "Spectator password incorrect.\n");
- ent->client->pers.spectator = false;
- gi.WriteByte(svc_stufftext);
- gi.WriteString("spectator 0\n");
- gi.unicast(ent, true);
- return;
- }
- // count spectators
- for (i = 1, numspec = 0; i <= game.maxclients; i++)
- if (g_edicts[i].inuse && g_edicts[i].client->pers.spectator)
- numspec++;
- if (numspec >= (uint32_t) maxspectators->integer)
- {
- gi.LocClient_Print(ent, PRINT_HIGH, "Server spectator limit is full.");
- ent->client->pers.spectator = false;
- // reset his spectator var
- gi.WriteByte(svc_stufftext);
- gi.WriteString("spectator 0\n");
- gi.unicast(ent, true);
- return;
- }
- }
- else
- {
- // he was a spectator and wants to join the game
- // he must have the right password
- char value[MAX_INFO_VALUE] = { 0 };
- gi.Info_ValueForKey(ent->client->pers.userinfo, "password", value, sizeof(value));
- if (*password->string && strcmp(password->string, "none") &&
- strcmp(password->string, value))
- {
- gi.LocClient_Print(ent, PRINT_HIGH, "Password incorrect.\n");
- ent->client->pers.spectator = true;
- gi.WriteByte(svc_stufftext);
- gi.WriteString("spectator 1\n");
- gi.unicast(ent, true);
- return;
- }
- }
- // clear score on respawn
- ent->client->resp.score = ent->client->pers.score = 0;
- // move us to no team
- ent->client->resp.ctf_team = CTF_NOTEAM;
- // change spectator mode
- ent->client->resp.spectator = ent->client->pers.spectator;
- ent->svflags &= ~SVF_NOCLIENT;
- PutClientInServer(ent);
- // add a teleportation effect
- if (!ent->client->pers.spectator)
- {
- // send effect
- gi.WriteByte(svc_muzzleflash);
- gi.WriteEntity(ent);
- gi.WriteByte(MZ_LOGIN);
- gi.multicast(ent->s.origin, MULTICAST_PVS, false);
- // hold in place briefly
- ent->client->ps.pmove.pm_flags = PMF_TIME_TELEPORT;
- ent->client->ps.pmove.pm_time = 112;
- }
- ent->client->respawn_time = level.time;
- if (ent->client->pers.spectator)
- gi.LocBroadcast_Print(PRINT_HIGH, "$g_observing", ent->client->pers.netname);
- else
- gi.LocBroadcast_Print(PRINT_HIGH, "$g_joined_game", ent->client->pers.netname);
- }
- //==============================================================
- // [Paril-KEX]
- // skinnum was historically used to pack data
- // so we're going to build onto that.
- void P_AssignClientSkinnum(edict_t *ent)
- {
- if (ent->s.modelindex != 255)
- return;
- player_skinnum_t packed;
- packed.client_num = ent->client - game.clients;
- if (ent->client->pers.weapon)
- packed.vwep_index = ent->client->pers.weapon->vwep_index - level.vwep_offset + 1;
- else
- packed.vwep_index = 0;
- packed.viewheight = ent->client->ps.viewoffset.z + ent->client->ps.pmove.viewheight;
-
- if (coop->value)
- packed.team_index = 1; // all players are teamed in coop
- else if (G_TeamplayEnabled())
- packed.team_index = ent->client->resp.ctf_team;
- else
- packed.team_index = 0;
- if (ent->deadflag)
- packed.poi_icon = 1;
- else
- packed.poi_icon = 0;
- ent->s.skinnum = packed.skinnum;
- }
- // [Paril-KEX] send player level POI
- void P_SendLevelPOI(edict_t *ent)
- {
- if (!level.valid_poi)
- return;
- gi.WriteByte(svc_poi);
- gi.WriteShort(POI_OBJECTIVE);
- gi.WriteShort(10000);
- gi.WritePosition(ent->client->help_poi_location);
- gi.WriteShort(ent->client->help_poi_image);
- gi.WriteByte(208);
- gi.WriteByte(POI_FLAG_NONE);
- gi.unicast(ent, true);
- }
- // [Paril-KEX] force the fog transition on the given player,
- // optionally instantaneously (ignore any transition time)
- void P_ForceFogTransition(edict_t *ent, bool instant)
- {
- // sanity check; if we're not changing the values, don't bother
- if (ent->client->fog == ent->client->pers.wanted_fog &&
- ent->client->heightfog == ent->client->pers.wanted_heightfog)
- return;
- svc_fog_data_t fog {};
-
- // check regular fog
- if (ent->client->pers.wanted_fog[0] != ent->client->fog[0] ||
- ent->client->pers.wanted_fog[4] != ent->client->fog[4])
- {
- fog.bits |= svc_fog_data_t::BIT_DENSITY;
- fog.density = ent->client->pers.wanted_fog[0];
- fog.skyfactor = ent->client->pers.wanted_fog[4] * 255.f;
- }
- if (ent->client->pers.wanted_fog[1] != ent->client->fog[1])
- {
- fog.bits |= svc_fog_data_t::BIT_R;
- fog.red = ent->client->pers.wanted_fog[1] * 255.f;
- }
- if (ent->client->pers.wanted_fog[2] != ent->client->fog[2])
- {
- fog.bits |= svc_fog_data_t::BIT_G;
- fog.green = ent->client->pers.wanted_fog[2] * 255.f;
- }
- if (ent->client->pers.wanted_fog[3] != ent->client->fog[3])
- {
- fog.bits |= svc_fog_data_t::BIT_B;
- fog.blue = ent->client->pers.wanted_fog[3] * 255.f;
- }
- if (!instant && ent->client->pers.fog_transition_time)
- {
- fog.bits |= svc_fog_data_t::BIT_TIME;
- fog.time = clamp(ent->client->pers.fog_transition_time.milliseconds(), (int64_t) 0, (int64_t) std::numeric_limits<uint16_t>::max());
- }
-
- // check heightfog stuff
- auto &hf = ent->client->heightfog;
- const auto &wanted_hf = ent->client->pers.wanted_heightfog;
-
- if (hf.falloff != wanted_hf.falloff)
- {
- fog.bits |= svc_fog_data_t::BIT_HEIGHTFOG_FALLOFF;
- if (!wanted_hf.falloff)
- fog.hf_falloff = 0;
- else
- fog.hf_falloff = wanted_hf.falloff;
- }
- if (hf.density != wanted_hf.density)
- {
- fog.bits |= svc_fog_data_t::BIT_HEIGHTFOG_DENSITY;
- if (!wanted_hf.density)
- fog.hf_density = 0;
- else
- fog.hf_density = wanted_hf.density;
- }
- if (hf.start[0] != wanted_hf.start[0])
- {
- fog.bits |= svc_fog_data_t::BIT_HEIGHTFOG_START_R;
- fog.hf_start_r = wanted_hf.start[0] * 255.f;
- }
- if (hf.start[1] != wanted_hf.start[1])
- {
- fog.bits |= svc_fog_data_t::BIT_HEIGHTFOG_START_G;
- fog.hf_start_g = wanted_hf.start[1] * 255.f;
- }
- if (hf.start[2] != wanted_hf.start[2])
- {
- fog.bits |= svc_fog_data_t::BIT_HEIGHTFOG_START_B;
- fog.hf_start_b = wanted_hf.start[2] * 255.f;
- }
- if (hf.start[3] != wanted_hf.start[3])
- {
- fog.bits |= svc_fog_data_t::BIT_HEIGHTFOG_START_DIST;
- fog.hf_start_dist = wanted_hf.start[3];
- }
- if (hf.end[0] != wanted_hf.end[0])
- {
- fog.bits |= svc_fog_data_t::BIT_HEIGHTFOG_END_R;
- fog.hf_end_r = wanted_hf.end[0] * 255.f;
- }
- if (hf.end[1] != wanted_hf.end[1])
- {
- fog.bits |= svc_fog_data_t::BIT_HEIGHTFOG_END_G;
- fog.hf_end_g = wanted_hf.end[1] * 255.f;
- }
- if (hf.end[2] != wanted_hf.end[2])
- {
- fog.bits |= svc_fog_data_t::BIT_HEIGHTFOG_END_B;
- fog.hf_end_b = wanted_hf.end[2] * 255.f;
- }
- if (hf.end[3] != wanted_hf.end[3])
- {
- fog.bits |= svc_fog_data_t::BIT_HEIGHTFOG_END_DIST;
- fog.hf_end_dist = wanted_hf.end[3];
- }
- if (fog.bits & 0xFF00)
- fog.bits |= svc_fog_data_t::BIT_MORE_BITS;
- gi.WriteByte(svc_fog);
- if (fog.bits & svc_fog_data_t::BIT_MORE_BITS)
- gi.WriteShort(fog.bits);
- else
- gi.WriteByte(fog.bits);
-
- if (fog.bits & svc_fog_data_t::BIT_DENSITY)
- {
- gi.WriteFloat(fog.density);
- gi.WriteByte(fog.skyfactor);
- }
- if (fog.bits & svc_fog_data_t::BIT_R)
- gi.WriteByte(fog.red);
- if (fog.bits & svc_fog_data_t::BIT_G)
- gi.WriteByte(fog.green);
- if (fog.bits & svc_fog_data_t::BIT_B)
- gi.WriteByte(fog.blue);
- if (fog.bits & svc_fog_data_t::BIT_TIME)
- gi.WriteShort(fog.time);
-
- if (fog.bits & svc_fog_data_t::BIT_HEIGHTFOG_FALLOFF)
- gi.WriteFloat(fog.hf_falloff);
- if (fog.bits & svc_fog_data_t::BIT_HEIGHTFOG_DENSITY)
- gi.WriteFloat(fog.hf_density);
- if (fog.bits & svc_fog_data_t::BIT_HEIGHTFOG_START_R)
- gi.WriteByte(fog.hf_start_r);
- if (fog.bits & svc_fog_data_t::BIT_HEIGHTFOG_START_G)
- gi.WriteByte(fog.hf_start_g);
- if (fog.bits & svc_fog_data_t::BIT_HEIGHTFOG_START_B)
- gi.WriteByte(fog.hf_start_b);
- if (fog.bits & svc_fog_data_t::BIT_HEIGHTFOG_START_DIST)
- gi.WriteLong(fog.hf_start_dist);
- if (fog.bits & svc_fog_data_t::BIT_HEIGHTFOG_END_R)
- gi.WriteByte(fog.hf_end_r);
- if (fog.bits & svc_fog_data_t::BIT_HEIGHTFOG_END_G)
- gi.WriteByte(fog.hf_end_g);
- if (fog.bits & svc_fog_data_t::BIT_HEIGHTFOG_END_B)
- gi.WriteByte(fog.hf_end_b);
- if (fog.bits & svc_fog_data_t::BIT_HEIGHTFOG_END_DIST)
- gi.WriteLong(fog.hf_end_dist);
- gi.unicast(ent, true);
- ent->client->fog = ent->client->pers.wanted_fog;
- hf = wanted_hf;
- }
- // [Paril-KEX] ugly global to handle squad respawn origin
- static bool use_squad_respawn = false;
- static bool spawn_from_begin = false;
- static vec3_t squad_respawn_position, squad_respawn_angles;
- inline void PutClientOnSpawnPoint(edict_t *ent, const vec3_t &spawn_origin, const vec3_t &spawn_angles)
- {
- gclient_t *client = ent->client;
- client->ps.pmove.origin = spawn_origin;
- ent->s.origin = spawn_origin;
- if (!use_squad_respawn)
- ent->s.origin[2] += 1; // make sure off ground
- ent->s.old_origin = ent->s.origin;
- // set the delta angle
- client->ps.pmove.delta_angles = spawn_angles - client->resp.cmd_angles;
- ent->s.angles = spawn_angles;
- ent->s.angles[PITCH] /= 3;
- client->ps.viewangles = ent->s.angles;
- client->v_angle = ent->s.angles;
- AngleVectors(client->v_angle, client->v_forward, nullptr, nullptr);
- }
- /*
- ===========
- PutClientInServer
- Called when a player connects to a server or respawns in
- a deathmatch.
- ============
- */
- void PutClientInServer(edict_t *ent)
- {
- int index;
- vec3_t spawn_origin, spawn_angles;
- gclient_t *client;
- client_persistant_t saved;
- client_respawn_t resp;
- index = ent - g_edicts - 1;
- client = ent->client;
- // clear velocity now, since landmark may change it
- ent->velocity = {};
- bool keepVelocity = client->landmark_name != nullptr;
- if (keepVelocity)
- ent->velocity = client->oldvelocity;
- // find a spawn point
- // do it before setting health back up, so farthest
- // ranging doesn't count this client
- bool valid_spawn = false;
- bool force_spawn = client->awaiting_respawn && level.time > client->respawn_timeout;
- bool is_landmark = false;
- if (use_squad_respawn)
- {
- spawn_origin = squad_respawn_position;
- spawn_angles = squad_respawn_angles;
- valid_spawn = true;
- }
- else if (gamerules->integer && DMGame.SelectSpawnPoint) // PGM
- valid_spawn = DMGame.SelectSpawnPoint(ent, spawn_origin, spawn_angles, force_spawn); // PGM
- else // PGM
- valid_spawn = SelectSpawnPoint(ent, spawn_origin, spawn_angles, force_spawn, is_landmark);
- // [Paril-KEX] if we didn't get a valid spawn, hold us in
- // limbo for a while until we do get one
- if (!valid_spawn)
- {
- // only do this once per spawn
- if (!client->awaiting_respawn)
- {
- char userinfo[MAX_INFO_STRING];
- memcpy(userinfo, client->pers.userinfo, sizeof(userinfo));
- ClientUserinfoChanged(ent, userinfo);
- client->respawn_timeout = level.time + 3_sec;
- }
- // find a spot to place us
- if (!level.respawn_intermission)
- {
- // find an intermission spot
- edict_t *pt = G_FindByString<&edict_t::classname>(nullptr, "info_player_intermission");
- if (!pt)
- { // the map creator forgot to put in an intermission point...
- pt = G_FindByString<&edict_t::classname>(nullptr, "info_player_start");
- if (!pt)
- pt = G_FindByString<&edict_t::classname>(nullptr, "info_player_deathmatch");
- }
- else
- { // choose one of four spots
- int32_t i = irandom(4);
- while (i--)
- {
- pt = G_FindByString<&edict_t::classname>(pt, "info_player_intermission");
- if (!pt) // wrap around the list
- pt = G_FindByString<&edict_t::classname>(pt, "info_player_intermission");
- }
- }
- level.intermission_origin = pt->s.origin;
- level.intermission_angle = pt->s.angles;
- level.respawn_intermission = true;
- }
- ent->s.origin = level.intermission_origin;
- ent->client->ps.pmove.origin = level.intermission_origin;
- ent->client->ps.viewangles = level.intermission_angle;
- client->awaiting_respawn = true;
- client->ps.pmove.pm_type = PM_FREEZE;
- client->ps.rdflags = RDF_NONE;
- ent->deadflag = false;
- ent->solid = SOLID_NOT;
- ent->movetype = MOVETYPE_NOCLIP;
- ent->s.modelindex = 0;
- ent->svflags |= SVF_NOCLIENT;
- ent->client->ps.team_id = ent->client->resp.ctf_team;
- gi.linkentity(ent);
- return;
- }
-
- client->resp.ctf_state++;
- bool was_waiting_for_respawn = client->awaiting_respawn;
- if (client->awaiting_respawn)
- ent->svflags &= ~SVF_NOCLIENT;
- client->awaiting_respawn = false;
- client->respawn_timeout = 0_ms;
- char social_id[MAX_INFO_VALUE];
- Q_strlcpy(social_id, ent->client->pers.social_id, sizeof(social_id));
- // deathmatch wipes most client data every spawn
- if (deathmatch->integer)
- {
- client->pers.health = 0;
- resp = client->resp;
- }
- else
- {
- // [Kex] Maintain user info in singleplayer to keep the player skin.
- char userinfo[MAX_INFO_STRING];
- memcpy(userinfo, client->pers.userinfo, sizeof(userinfo));
- if (coop->integer)
- {
- resp = client->resp;
- if (!P_UseCoopInstancedItems())
- {
- resp.coop_respawn.game_help1changed = client->pers.game_help1changed;
- resp.coop_respawn.game_help2changed = client->pers.game_help2changed;
- resp.coop_respawn.helpchanged = client->pers.helpchanged;
- client->pers = resp.coop_respawn;
- }
- else
- {
- // fix weapon
- if (!client->pers.weapon)
- client->pers.weapon = client->pers.lastweapon;
- }
- }
- ClientUserinfoChanged(ent, userinfo);
- if (coop->integer)
- {
- if (resp.score > client->pers.score)
- client->pers.score = resp.score;
- }
- else
- memset(&resp, 0, sizeof(resp));
- }
- // clear everything but the persistant data
- saved = client->pers;
- memset(client, 0, sizeof(*client));
- client->pers = saved;
- client->resp = resp;
- // on a new, fresh spawn (always in DM, clear inventory
- // or new spawns in SP/coop)
- if (client->pers.health <= 0)
- InitClientPersistant(ent, client);
- // restore social ID
- Q_strlcpy(ent->client->pers.social_id, social_id, sizeof(social_id));
- // fix level switch issue
- ent->client->pers.connected = true;
- // slow time will be unset here
- globals.server_flags &= ~SERVER_FLAG_SLOW_TIME;
- // copy some data from the client to the entity
- FetchClientEntData(ent);
- // clear entity values
- ent->groundentity = nullptr;
- ent->client = &game.clients[index];
- ent->takedamage = true;
- ent->movetype = MOVETYPE_WALK;
- ent->viewheight = 22;
- ent->inuse = true;
- ent->classname = "player";
- ent->mass = 200;
- ent->solid = SOLID_BBOX;
- ent->deadflag = false;
- ent->air_finished = level.time + 12_sec;
- ent->clipmask = MASK_PLAYERSOLID;
- ent->model = "players/male/tris.md2";
- ent->die = player_die;
- ent->waterlevel = WATER_NONE;
- ent->watertype = CONTENTS_NONE;
- ent->flags &= ~( FL_NO_KNOCKBACK | FL_ALIVE_KNOCKBACK_ONLY | FL_NO_DAMAGE_EFFECTS );
- ent->svflags &= ~SVF_DEADMONSTER;
- ent->svflags |= SVF_PLAYER;
- ent->flags &= ~FL_SAM_RAIMI; // PGM - turn off sam raimi flag
- ent->mins = PLAYER_MINS;
- ent->maxs = PLAYER_MAXS;
- // clear playerstate values
- memset(&ent->client->ps, 0, sizeof(client->ps));
- char val[MAX_INFO_VALUE];
- gi.Info_ValueForKey(ent->client->pers.userinfo, "fov", val, sizeof(val));
- ent->client->ps.fov = clamp((float) atoi(val), 1.f, 160.f);
- ent->client->ps.pmove.viewheight = ent->viewheight;
- ent->client->ps.team_id = ent->client->resp.ctf_team;
- if (!G_ShouldPlayersCollide(false))
- ent->clipmask &= ~CONTENTS_PLAYER;
- // PGM
- if (client->pers.weapon)
- client->ps.gunindex = gi.modelindex(client->pers.weapon->view_model);
- else
- client->ps.gunindex = 0;
- client->ps.gunskin = 0;
- // PGM
- // clear entity state values
- ent->s.effects = EF_NONE;
- ent->s.modelindex = MODELINDEX_PLAYER; // will use the skin specified model
- ent->s.modelindex2 = MODELINDEX_PLAYER; // custom gun model
- // sknum is player num and weapon number
- // weapon number will be added in changeweapon
- P_AssignClientSkinnum(ent);
- ent->s.frame = 0;
- PutClientOnSpawnPoint(ent, spawn_origin, spawn_angles);
- // [Paril-KEX] set up world fog & send it instantly
- ent->client->pers.wanted_fog = {
- world->fog.density,
- world->fog.color[0],
- world->fog.color[1],
- world->fog.color[2],
- world->fog.sky_factor
- };
- ent->client->pers.wanted_heightfog = {
- { world->heightfog.start_color[0], world->heightfog.start_color[1], world->heightfog.start_color[2], world->heightfog.start_dist },
- { world->heightfog.end_color[0], world->heightfog.end_color[1], world->heightfog.end_color[2], world->heightfog.end_dist },
- world->heightfog.falloff,
- world->heightfog.density
- };
- P_ForceFogTransition(ent, true);
- // ZOID
- if (CTFStartClient(ent))
- return;
- // ZOID
- // spawn a spectator
- if (client->pers.spectator)
- {
- client->chase_target = nullptr;
- client->resp.spectator = true;
- ent->movetype = MOVETYPE_NOCLIP;
- ent->solid = SOLID_NOT;
- ent->svflags |= SVF_NOCLIENT;
- ent->client->ps.gunindex = 0;
- ent->client->ps.gunskin = 0;
- gi.linkentity(ent);
- return;
- }
- client->resp.spectator = false;
- // [Paril-KEX] a bit of a hack, but landmark spawns can sometimes cause
- // intersecting spawns, so we'll do a sanity check here...
- if (spawn_from_begin)
- {
- if (coop->integer)
- {
- edict_t *collision = G_UnsafeSpawnPosition(ent->s.origin, true);
- if (collision)
- {
- gi.linkentity(ent);
- if (collision->client)
- {
- // we spawned in somebody else, so we're going to change their spawn position
- bool lm = false;
- SelectSpawnPoint(collision, spawn_origin, spawn_angles, true, lm);
- PutClientOnSpawnPoint(collision, spawn_origin, spawn_angles);
- }
- // else, no choice but to accept where ever we spawned :(
- }
- }
- // give us one (1) free fall ticket even if
- // we didn't spawn from landmark
- ent->client->landmark_free_fall = true;
- }
- gi.linkentity(ent);
- if (!KillBox(ent, true, MOD_TELEFRAG_SPAWN))
- { // could't spawn in?
- }
- // my tribute to cash's level-specific hacks. I hope I live
- // up to his trailblazing cheese.
- if (Q_strcasecmp(level.mapname, "rboss") == 0)
- {
- // if you get on to rboss in single player or coop, ensure
- // the player has the nuke key. (not in DM)
- if (!deathmatch->integer)
- client->pers.inventory[IT_KEY_NUKE] = 1;
- }
- // force the current weapon up
- client->newweapon = client->pers.weapon;
- ChangeWeapon(ent);
- if (was_waiting_for_respawn)
- G_PostRespawn(ent);
- }
- /*
- =====================
- ClientBeginDeathmatch
- A client has just connected to the server in
- deathmatch mode, so clear everything out before starting them.
- =====================
- */
- void ClientBeginDeathmatch(edict_t *ent)
- {
- G_InitEdict(ent);
-
- // make sure we have a known default
- ent->svflags |= SVF_PLAYER;
- InitClientResp(ent->client);
- // ZOID
- if (G_TeamplayEnabled() && ent->client->resp.ctf_team < CTF_TEAM1)
- CTFAssignTeam(ent->client);
- // ZOID
- // PGM
- if (gamerules->integer && DMGame.ClientBegin)
- {
- DMGame.ClientBegin(ent);
- }
- // PGM
- // locate ent at a spawn point
- PutClientInServer(ent);
- if (level.intermissiontime)
- {
- MoveClientToIntermission(ent);
- }
- else
- {
- if (!(ent->svflags & SVF_NOCLIENT))
- {
- // send effect
- gi.WriteByte(svc_muzzleflash);
- gi.WriteEntity(ent);
- gi.WriteByte(MZ_LOGIN);
- gi.multicast(ent->s.origin, MULTICAST_PVS, false);
- }
- }
- gi.LocBroadcast_Print(PRINT_HIGH, "$g_entered_game", ent->client->pers.netname);
- // make sure all view stuff is valid
- ClientEndServerFrame(ent);
- }
- static void G_SetLevelEntry()
- {
- if (deathmatch->integer)
- return;
- // map is a hub map, so we shouldn't bother tracking any of this.
- // the next map will pick up as the start.
- else if (level.hub_map)
- return;
- level_entry_t *found_entry = nullptr;
- int32_t highest_order = 0;
- for (size_t i = 0; i < MAX_LEVELS_PER_UNIT; i++)
- {
- level_entry_t *entry = &game.level_entries[i];
- highest_order = max(highest_order, entry->visit_order);
- if (!strcmp(entry->map_name, level.mapname) || !*entry->map_name)
- {
- found_entry = entry;
- break;
- }
- }
- if (!found_entry)
- {
- gi.Com_PrintFmt("WARNING: more than {} maps in unit, can't track the rest\n", MAX_LEVELS_PER_UNIT);
- return;
- }
- level.entry = found_entry;
- Q_strlcpy(level.entry->map_name, level.mapname, sizeof(level.entry->map_name));
- // we're visiting this map for the first time, so
- // mark it in our order as being recent
- if (!*level.entry->pretty_name)
- {
- Q_strlcpy(level.entry->pretty_name, level.level_name, sizeof(level.entry->pretty_name));
- level.entry->visit_order = highest_order + 1;
- // give all of the clients an extra life back
- if (g_coop_enable_lives->integer)
- for (size_t i = 0; i < game.maxclients; i++)
- game.clients[i].pers.lives = min(g_coop_num_lives->integer + 1, game.clients[i].pers.lives + 1);
- }
- // scan for all new maps we can go to, for secret levels
- edict_t *changelevel = nullptr;
- while ((changelevel = G_FindByString<&edict_t::classname>(changelevel, "target_changelevel")))
- {
- if (!changelevel->map || !*changelevel->map)
- continue;
- // next unit map, don't count it
- if (strchr(changelevel->map, '*'))
- continue;
- const char *level = strchr(changelevel->map, '+');
- if (level)
- level++;
- else
- level = changelevel->map;
- // don't include end screen levels
- if (strstr(level, ".cin") || strstr(level, ".pcx"))
- continue;
- size_t level_length;
- const char *spawnpoint = strchr(level, '$');
- if (spawnpoint)
- level_length = spawnpoint - level;
- else
- level_length = strlen(level);
- // make an entry for this level that we may or may not visit
- level_entry_t *found_entry = nullptr;
- for (size_t i = 0; i < MAX_LEVELS_PER_UNIT; i++)
- {
- level_entry_t *entry = &game.level_entries[i];
- if (!*entry->map_name || !strncmp(entry->map_name, level, level_length))
- {
- found_entry = entry;
- break;
- }
- }
- if (!found_entry)
- {
- gi.Com_PrintFmt("WARNING: more than {} maps in unit, can't track the rest\n", MAX_LEVELS_PER_UNIT);
- return;
- }
- Q_strlcpy(found_entry->map_name, level, min(level_length + 1, sizeof(found_entry->map_name)));
- }
- }
- /*
- ===========
- ClientBegin
- called when a client has finished connecting, and is ready
- to be placed into the game. This will happen every level load.
- ============
- */
- void ClientBegin(edict_t *ent)
- {
- ent->client = game.clients + (ent - g_edicts - 1);
- ent->client->awaiting_respawn = false;
- ent->client->respawn_timeout = 0_ms;
- // [Paril-KEX] we're always connected by this point...
- ent->client->pers.connected = true;
- if (deathmatch->integer)
- {
- ClientBeginDeathmatch(ent);
- return;
- }
- // [Paril-KEX] set enter time now, so we can send messages slightly
- // after somebody first joins
- ent->client->resp.entertime = level.time;
- ent->client->pers.spawned = true;
- // if there is already a body waiting for us (a loadgame), just
- // take it, otherwise spawn one from scratch
- if (ent->inuse)
- {
- // the client has cleared the client side viewangles upon
- // connecting to the server, which is different than the
- // state when the game is saved, so we need to compensate
- // with deltaangles
- ent->client->ps.pmove.delta_angles = ent->client->ps.viewangles;
- }
- else
- {
- // a spawn point will completely reinitialize the entity
- // except for the persistant data that was initialized at
- // ClientConnect() time
- G_InitEdict(ent);
- ent->classname = "player";
- InitClientResp(ent->client);
- spawn_from_begin = true;
- PutClientInServer(ent);
- spawn_from_begin = false;
- }
-
- // make sure we have a known default
- ent->svflags |= SVF_PLAYER;
- if (level.intermissiontime)
- {
- MoveClientToIntermission(ent);
- }
- else
- {
- // send effect if in a multiplayer game
- if (game.maxclients > 1 && !(ent->svflags & SVF_NOCLIENT))
- gi.LocBroadcast_Print(PRINT_HIGH, "$g_entered_game", ent->client->pers.netname);
- }
- level.coop_scale_players++;
- G_Monster_CheckCoopHealthScaling();
- // make sure all view stuff is valid
- ClientEndServerFrame(ent);
- // [Paril-KEX] send them goal, if needed
- G_PlayerNotifyGoal(ent);
- // [Paril-KEX] we're going to set this here just to be certain
- // that the level entry timer only starts when a player is actually
- // *in* the level
- G_SetLevelEntry();
- }
- /*
- ================
- P_GetLobbyUserNum
- ================
- */
- unsigned int P_GetLobbyUserNum( const edict_t * player ) {
- unsigned int playerNum = 0;
- if ( player > g_edicts && player < g_edicts + MAX_EDICTS ) {
- playerNum = ( player - g_edicts ) - 1;
- if ( playerNum >= MAX_CLIENTS ) {
- playerNum = 0;
- }
- }
- return playerNum;
- }
- /*
- ================
- G_EncodedPlayerName
- Gets a token version of the players "name" to be decoded on the client.
- ================
- */
- std::string G_EncodedPlayerName(edict_t* player)
- {
- unsigned int playernum = P_GetLobbyUserNum( player );
- return std::string("##P") + std::to_string(playernum);
- }
- /*
- ===========
- ClientUserInfoChanged
- called whenever the player updates a userinfo variable.
- ============
- */
- void ClientUserinfoChanged(edict_t *ent, const char *userinfo)
- {
- // set name
- if (!gi.Info_ValueForKey(userinfo, "name", ent->client->pers.netname, sizeof(ent->client->pers.netname)))
- Q_strlcpy(ent->client->pers.netname, "badinfo", sizeof(ent->client->pers.netname));
- // set spectator
- char val[MAX_INFO_VALUE] = { 0 };
- gi.Info_ValueForKey(userinfo, "spectator", val, sizeof(val));
- // spectators are only supported in deathmatch
- if (deathmatch->integer && !G_TeamplayEnabled() && *val && strcmp(val, "0"))
- ent->client->pers.spectator = true;
- else
- ent->client->pers.spectator = false;
- // set skin
- if (!gi.Info_ValueForKey(userinfo, "skin", val, sizeof(val)))
- Q_strlcpy(val, "male/grunt", sizeof(val));
- int playernum = ent - g_edicts - 1;
- // combine name and skin into a configstring
- // ZOID
- if (G_TeamplayEnabled())
- CTFAssignSkin(ent, val);
- else
- {
- // set dogtag
- char dogtag[MAX_INFO_VALUE] = { 0 };
- gi.Info_ValueForKey(userinfo, "dogtag", dogtag, sizeof(dogtag));
- // ZOID
- gi.configstring(CS_PLAYERSKINS + playernum, G_Fmt("{}\\{}\\{}", ent->client->pers.netname, val, dogtag).data());
- }
- // ZOID
- // set player name field (used in id_state view)
- gi.configstring(CONFIG_CTF_PLAYER_NAME + playernum, ent->client->pers.netname);
- // ZOID
- // [Kex] netname is used for a couple of other things, so we update this after those.
- if ( ( ent->svflags & SVF_BOT ) == 0 ) {
- Q_strlcpy( ent->client->pers.netname, G_EncodedPlayerName( ent ).c_str(), sizeof( ent->client->pers.netname ) );
- }
- // fov
- gi.Info_ValueForKey(userinfo, "fov", val, sizeof(val));
- ent->client->ps.fov = clamp((float) atoi(val), 1.f, 160.f);
- // handedness
- if (gi.Info_ValueForKey(userinfo, "hand", val, sizeof(val)))
- {
- ent->client->pers.hand = static_cast<handedness_t>(clamp(atoi(val), (int32_t) RIGHT_HANDED, (int32_t) CENTER_HANDED));
- }
- else
- {
- ent->client->pers.hand = RIGHT_HANDED;
- }
- // [Paril-KEX] auto-switch
- if (gi.Info_ValueForKey(userinfo, "autoswitch", val, sizeof(val)))
- {
- ent->client->pers.autoswitch = static_cast<auto_switch_t>(clamp(atoi(val), (int32_t)auto_switch_t::SMART, (int32_t)auto_switch_t::NEVER));
- }
- else
- {
- ent->client->pers.autoswitch = auto_switch_t::SMART;
- }
- if (gi.Info_ValueForKey(userinfo, "autoshield", val, sizeof(val)))
- {
- ent->client->pers.autoshield = atoi(val);
- }
- else
- {
- ent->client->pers.autoshield = -1;
- }
- // [Paril-KEX] wants bob
- if (gi.Info_ValueForKey(userinfo, "bobskip", val, sizeof(val)))
- {
- ent->client->pers.bob_skip = val[0] == '1';
- }
- else
- {
- ent->client->pers.bob_skip = false;
- }
- // save off the userinfo in case we want to check something later
- Q_strlcpy(ent->client->pers.userinfo, userinfo, sizeof(ent->client->pers.userinfo));
- }
- inline bool IsSlotIgnored(edict_t *slot, edict_t **ignore, size_t num_ignore)
- {
- for (size_t i = 0; i < num_ignore; i++)
- if (slot == ignore[i])
- return true;
- return false;
- }
- inline edict_t *ClientChooseSlot_Any(edict_t **ignore, size_t num_ignore)
- {
- for (size_t i = 0; i < game.maxclients; i++)
- if (!IsSlotIgnored(globals.edicts + i + 1, ignore, num_ignore) && !game.clients[i].pers.connected)
- return globals.edicts + i + 1;
- return nullptr;
- }
- inline edict_t *ClientChooseSlot_Coop(const char *userinfo, const char *social_id, bool isBot, edict_t **ignore, size_t num_ignore)
- {
- char name[MAX_INFO_VALUE] = { 0 };
- gi.Info_ValueForKey(userinfo, "name", name, sizeof(name));
- // the host should always occupy slot 0, some systems rely on this
- // (CHECK: is this true? is it just bots?)
- {
- size_t num_players = 0;
- for (size_t i = 0; i < game.maxclients; i++)
- if (IsSlotIgnored(globals.edicts + i + 1, ignore, num_ignore) || game.clients[i].pers.connected)
- num_players++;
- if (!num_players)
- {
- gi.Com_PrintFmt("coop slot {} is host {}+{}\n", 1, name, social_id);
- return globals.edicts + 1;
- }
- }
- // grab matches from players that we have connected
- using match_type_t = int32_t;
- enum {
- MATCH_USERNAME,
- MATCH_SOCIAL,
- MATCH_BOTH,
- MATCH_TYPES
- };
- struct {
- edict_t *slot = nullptr;
- size_t total = 0;
- } matches[MATCH_TYPES];
- for (size_t i = 0; i < game.maxclients; i++)
- {
- if (IsSlotIgnored(globals.edicts + i + 1, ignore, num_ignore) || game.clients[i].pers.connected)
- continue;
- char check_name[MAX_INFO_VALUE] = { 0 };
- gi.Info_ValueForKey(game.clients[i].pers.userinfo, "name", check_name, sizeof(check_name));
- bool username_match = game.clients[i].pers.userinfo[0] &&
- !strcmp(check_name, name);
- bool social_match = social_id && game.clients[i].pers.social_id[0] &&
- !strcmp(game.clients[i].pers.social_id, social_id);
- match_type_t type = (match_type_t) 0;
- if (username_match)
- type |= MATCH_USERNAME;
- if (social_match)
- type |= MATCH_SOCIAL;
- if (!type)
- continue;
- matches[type].slot = globals.edicts + i + 1;
- matches[type].total++;
- }
- // pick matches in descending order, only if the total matches
- // is 1 in the particular set; this will prefer to pick
- // social+username matches first, then social, then username last.
- for (int32_t i = 2; i >= 0; i--)
- {
- if (matches[i].total == 1)
- {
- gi.Com_PrintFmt("coop slot {} restored for {}+{}\n", (ptrdiff_t) (matches[i].slot - globals.edicts), name, social_id);
- // spawn us a ghost now since we're gonna spawn eventually
- if (!matches[i].slot->inuse)
- {
- matches[i].slot->s.modelindex = MODELINDEX_PLAYER;
- matches[i].slot->solid = SOLID_BBOX;
- G_InitEdict(matches[i].slot);
- matches[i].slot->classname = "player";
- InitClientResp(matches[i].slot->client);
- spawn_from_begin = true;
- PutClientInServer(matches[i].slot);
- spawn_from_begin = false;
- // make sure we have a known default
- matches[i].slot->svflags |= SVF_PLAYER;
- matches[i].slot->sv.init = false;
- matches[i].slot->classname = "player";
- matches[i].slot->client->pers.connected = true;
- matches[i].slot->client->pers.spawned = true;
- P_AssignClientSkinnum(matches[i].slot);
- gi.linkentity(matches[i].slot);
- }
- return matches[i].slot;
- }
- }
- // in the case where we can't find a match, we're probably a new
- // player, so pick a slot that hasn't been occupied yet
- for (size_t i = 0; i < game.maxclients; i++)
- if (!IsSlotIgnored(globals.edicts + i + 1, ignore, num_ignore) && !game.clients[i].pers.userinfo[0])
- {
- gi.Com_PrintFmt("coop slot {} issuing new for {}+{}\n", i + 1, name, social_id);
- return globals.edicts + i + 1;
- }
- // all slots have some player data in them, we're forced to replace one.
- edict_t *any_slot = ClientChooseSlot_Any(ignore, num_ignore);
- gi.Com_PrintFmt("coop slot {} any slot for {}+{}\n", !any_slot ? -1 : (ptrdiff_t) (any_slot - globals.edicts), name, social_id);
- return any_slot;
- }
- // [Paril-KEX] for coop, we want to try to ensure that players will always get their
- // proper slot back when they connect.
- edict_t *ClientChooseSlot(const char *userinfo, const char *social_id, bool isBot, edict_t **ignore, size_t num_ignore, bool cinematic)
- {
- // coop and non-bots is the only thing that we need to do special behavior on
- if (!cinematic && coop->integer && !isBot)
- return ClientChooseSlot_Coop(userinfo, social_id, isBot, ignore, num_ignore);
- // just find any free slot
- return ClientChooseSlot_Any(ignore, num_ignore);
- }
- /*
- ===========
- ClientConnect
- Called when a player begins connecting to the server.
- The game can refuse entrance to a client by returning false.
- If the client is allowed, the connection process will continue
- and eventually get to ClientBegin()
- Changing levels will NOT cause this to be called again, but
- loadgames will.
- ============
- */
- bool ClientConnect(edict_t *ent, char *userinfo, const char *social_id, bool isBot)
- {
- // check to see if they are on the banned IP list
- #if 0
- value = Info_ValueForKey(userinfo, "ip");
- if (SV_FilterPacket(value))
- {
- Info_SetValueForKey(userinfo, "rejmsg", "Banned.");
- return false;
- }
- #endif
- // check for a spectator
- char value[MAX_INFO_VALUE] = { 0 };
- gi.Info_ValueForKey(userinfo, "spectator", value, sizeof(value));
- if (deathmatch->integer && *value && strcmp(value, "0"))
- {
- uint32_t i, numspec;
- if (*spectator_password->string &&
- strcmp(spectator_password->string, "none") &&
- strcmp(spectator_password->string, value))
- {
- gi.Info_SetValueForKey(userinfo, "rejmsg", "Spectator password required or incorrect.");
- return false;
- }
- // count spectators
- for (i = numspec = 0; i < game.maxclients; i++)
- if (g_edicts[i + 1].inuse && g_edicts[i + 1].client->pers.spectator)
- numspec++;
- if (numspec >= (uint32_t) maxspectators->integer)
- {
- gi.Info_SetValueForKey(userinfo, "rejmsg", "Server spectator limit is full.");
- return false;
- }
- }
- else
- {
- // check for a password ( if not a bot! )
- gi.Info_ValueForKey(userinfo, "password", value, sizeof(value));
- if ( !isBot && *password->string && strcmp(password->string, "none") &&
- strcmp(password->string, value))
- {
- gi.Info_SetValueForKey(userinfo, "rejmsg", "Password required or incorrect.");
- return false;
- }
- }
- // they can connect
- ent->client = game.clients + (ent - g_edicts - 1);
- // set up userinfo early
- ClientUserinfoChanged(ent, userinfo);
- // if there is already a body waiting for us (a loadgame), just
- // take it, otherwise spawn one from scratch
- if (ent->inuse == false)
- {
- // clear the respawning variables
- // ZOID -- force team join
- ent->client->resp.ctf_team = CTF_NOTEAM;
- ent->client->resp.id_state = true;
- // ZOID
- InitClientResp(ent->client);
- if (!game.autosaved || !ent->client->pers.weapon)
- InitClientPersistant(ent, ent->client);
- }
- // make sure we start with known default(s)
- ent->svflags = SVF_PLAYER;
- if ( isBot ) {
- ent->svflags |= SVF_BOT;
- }
- Q_strlcpy(ent->client->pers.social_id, social_id, sizeof(ent->client->pers.social_id));
- if (game.maxclients > 1)
- {
- // [Paril-KEX] fetch name because now netname is kinda unsuitable
- gi.Info_ValueForKey(userinfo, "name", value, sizeof(value));
- gi.LocClient_Print(nullptr, PRINT_HIGH, "$g_player_connected", value);
- }
- ent->client->pers.connected = true;
- // [Paril-KEX] force a state update
- ent->sv.init = false;
- return true;
- }
- /*
- ===========
- ClientDisconnect
- Called when a player drops from the server.
- Will not be called between levels.
- ============
- */
- void ClientDisconnect(edict_t *ent)
- {
- if (!ent->client)
- return;
- // ZOID
- CTFDeadDropFlag(ent);
- CTFDeadDropTech(ent);
- // ZOID
- PlayerTrail_Destroy(ent);
- //============
- // ROGUE
- // make sure no trackers are still hurting us.
- if (ent->client->tracker_pain_time)
- RemoveAttackingPainDaemons(ent);
- if (ent->client->owned_sphere)
- {
- if (ent->client->owned_sphere->inuse)
- G_FreeEdict(ent->client->owned_sphere);
- ent->client->owned_sphere = nullptr;
- }
- if (gamerules->integer)
- {
- if (DMGame.PlayerDisconnect)
- DMGame.PlayerDisconnect(ent);
- }
- // ROGUE
- //============
- // send effect
- if (!(ent->svflags & SVF_NOCLIENT))
- {
- gi.WriteByte(svc_muzzleflash);
- gi.WriteEntity(ent);
- gi.WriteByte(MZ_LOGOUT);
- gi.multicast(ent->s.origin, MULTICAST_PVS, false);
- }
- gi.unlinkentity(ent);
- ent->s.modelindex = 0;
- ent->solid = SOLID_NOT;
- ent->inuse = false;
- ent->sv.init = false;
- ent->classname = "disconnected";
- ent->client->pers.connected = false;
- ent->client->pers.spawned = false;
- ent->timestamp = level.time + 1_sec;
- // update active scoreboards
- if (deathmatch->integer)
- for (auto player : active_players())
- if (player->client->showscores)
- player->client->menutime = level.time;
- }
- //==============================================================
- trace_t SV_PM_Clip(const vec3_t &start, const vec3_t *mins, const vec3_t *maxs, const vec3_t &end, contents_t mask)
- {
- return gi.game_import_t::clip(world, start, mins, maxs, end, mask);
- }
- bool G_ShouldPlayersCollide(bool weaponry)
- {
- if (g_disable_player_collision->integer)
- return false; // only for debugging.
- // always collide on dm
- if (!coop->integer)
- return true;
- // weaponry collides if friendly fire is enabled
- if (weaponry && g_friendly_fire->integer)
- return true;
- // check collision cvar
- return g_coop_player_collision->integer;
- }
- /*
- =================
- P_FallingDamage
- Paril-KEX: this is moved here and now reacts directly
- to ClientThink rather than being delayed.
- =================
- */
- void P_FallingDamage(edict_t *ent, const pmove_t &pm)
- {
- int damage;
- vec3_t dir;
- // dead stuff can't crater
- if (ent->health <= 0 || ent->deadflag)
- return;
- if (ent->s.modelindex != MODELINDEX_PLAYER)
- return; // not in the player model
- if (ent->movetype == MOVETYPE_NOCLIP)
- return;
- // never take falling damage if completely underwater
- if (pm.waterlevel == WATER_UNDER)
- return;
- // ZOID
- // never take damage if just release grapple or on grapple
- if (ent->client->ctf_grapplereleasetime >= level.time ||
- (ent->client->ctf_grapple &&
- ent->client->ctf_grapplestate > CTF_GRAPPLE_STATE_FLY))
- return;
- // ZOID
- float delta = pm.impact_delta;
- delta = delta * delta * 0.0001f;
- if (pm.waterlevel == WATER_WAIST)
- delta *= 0.25f;
- if (pm.waterlevel == WATER_FEET)
- delta *= 0.5f;
- if (delta < 1)
- return;
- // restart footstep timer
- ent->client->bobtime = 0;
- if (ent->client->landmark_free_fall)
- {
- delta = min(30.f, delta);
- ent->client->landmark_free_fall = false;
- ent->client->landmark_noise_time = level.time + 100_ms;
- }
- if (delta < 15)
- {
- if (!(pm.s.pm_flags & PMF_ON_LADDER))
- ent->s.event = EV_FOOTSTEP;
- return;
- }
- ent->client->fall_value = delta * 0.5f;
- if (ent->client->fall_value > 40)
- ent->client->fall_value = 40;
- ent->client->fall_time = level.time + FALL_TIME();
- if (delta > 30)
- {
- if (delta >= 55)
- ent->s.event = EV_FALLFAR;
- else
- ent->s.event = EV_FALL;
- ent->pain_debounce_time = level.time + FRAME_TIME_S; // no normal pain sound
- damage = (int) ((delta - 30) / 2);
- if (damage < 1)
- damage = 1;
- dir = { 0, 0, 1 };
- if (!deathmatch->integer || !g_dm_no_fall_damage->integer)
- T_Damage(ent, world, world, dir, ent->s.origin, vec3_origin, damage, 0, DAMAGE_NONE, MOD_FALLING);
- }
- else
- ent->s.event = EV_FALLSHORT;
- // Paril: falling damage noises alert monsters
- if (ent->health)
- PlayerNoise(ent, pm.s.origin, PNOISE_SELF);
- }
- bool HandleMenuMovement(edict_t *ent, usercmd_t *ucmd)
- {
- if (!ent->client->menu)
- return false;
- // [Paril-KEX] handle menu movement
- int32_t menu_sign = ucmd->forwardmove > 0 ? 1 : ucmd->forwardmove < 0 ? -1 : 0;
- if (ent->client->menu_sign != menu_sign)
- {
- ent->client->menu_sign = menu_sign;
- if (menu_sign > 0)
- {
- PMenu_Prev(ent);
- return true;
- }
- else if (menu_sign < 0)
- {
- PMenu_Next(ent);
- return true;
- }
- }
- if (ent->client->latched_buttons & (BUTTON_ATTACK | BUTTON_JUMP))
- {
- PMenu_Select(ent);
- return true;
- }
- return false;
- }
- /*
- ==============
- ClientThink
- This will be called once for each client frame, which will
- usually be a couple times for each server frame.
- ==============
- */
- void ClientThink(edict_t *ent, usercmd_t *ucmd)
- {
- gclient_t *client;
- edict_t *other;
- uint32_t i;
- pmove_t pm;
- level.current_entity = ent;
- client = ent->client;
- // [Paril-KEX] pass buttons through even if we are in intermission or
- // chasing.
- client->oldbuttons = client->buttons;
- client->buttons = ucmd->buttons;
- client->latched_buttons |= client->buttons & ~client->oldbuttons;
- client->cmd = *ucmd;
- if ((ucmd->buttons & BUTTON_CROUCH) && pm_config.n64_physics)
- {
- if (client->pers.n64_crouch_warn_times < 12 &&
- client->pers.n64_crouch_warning < level.time &&
- (++client->pers.n64_crouch_warn_times % 3) == 0)
- {
- client->pers.n64_crouch_warning = level.time + 10_sec;
- gi.LocClient_Print(ent, PRINT_CENTER, "$g_n64_crouching");
- }
- }
- if (level.intermissiontime || ent->client->awaiting_respawn)
- {
- client->ps.pmove.pm_type = PM_FREEZE;
- bool n64_sp = false;
- if (level.intermissiontime)
- {
- n64_sp = !deathmatch->integer && level.is_n64;
- // can exit intermission after five seconds
- // Paril: except in N64. the camera handles it.
- // Paril again: except on unit exits, we can leave immediately after camera finishes
- if (level.changemap && (!n64_sp || level.level_intermission_set) && level.time > level.intermissiontime + 5_sec && (ucmd->buttons & BUTTON_ANY))
- level.exitintermission = true;
- }
- if (!n64_sp)
- client->ps.pmove.viewheight = ent->viewheight = 22;
- else
- client->ps.pmove.viewheight = ent->viewheight = 0;
- ent->movetype = MOVETYPE_NOCLIP;
- return;
- }
- if (ent->client->chase_target)
- {
- client->resp.cmd_angles = ucmd->angles;
- ent->movetype = MOVETYPE_NOCLIP;
- }
- else
- {
- // set up for pmove
- memset(&pm, 0, sizeof(pm));
-
- if (ent->movetype == MOVETYPE_NOCLIP)
- {
- if (ent->client->menu)
- {
- client->ps.pmove.pm_type = PM_FREEZE;
-
- // [Paril-KEX] handle menu movement
- HandleMenuMovement(ent, ucmd);
- }
- else if (ent->client->awaiting_respawn)
- client->ps.pmove.pm_type = PM_FREEZE;
- else if (ent->client->resp.spectator || (G_TeamplayEnabled() && ent->client->resp.ctf_team == CTF_NOTEAM))
- client->ps.pmove.pm_type = PM_SPECTATOR;
- else
- client->ps.pmove.pm_type = PM_NOCLIP;
- }
- else if (ent->s.modelindex != MODELINDEX_PLAYER)
- client->ps.pmove.pm_type = PM_GIB;
- else if (ent->deadflag)
- client->ps.pmove.pm_type = PM_DEAD;
- else if (ent->client->ctf_grapplestate >= CTF_GRAPPLE_STATE_PULL)
- client->ps.pmove.pm_type = PM_GRAPPLE;
- else
- client->ps.pmove.pm_type = PM_NORMAL;
- // [Paril-KEX]
- if (!G_ShouldPlayersCollide(false) ||
- (coop->integer && !(ent->clipmask & CONTENTS_PLAYER)) // if player collision is on and we're temporarily ghostly...
- )
- client->ps.pmove.pm_flags |= PMF_IGNORE_PLAYER_COLLISION;
- else
- client->ps.pmove.pm_flags &= ~PMF_IGNORE_PLAYER_COLLISION;
- // PGM trigger_gravity support
- client->ps.pmove.gravity = (short) (level.gravity * ent->gravity);
- pm.s = client->ps.pmove;
- pm.s.origin = ent->s.origin;
- pm.s.velocity = ent->velocity;
- if (memcmp(&client->old_pmove, &pm.s, sizeof(pm.s)))
- pm.snapinitial = true;
- pm.cmd = *ucmd;
- pm.player = ent;
- pm.trace = gi.game_import_t::trace;
- pm.clip = SV_PM_Clip;
- pm.pointcontents = gi.pointcontents;
- pm.viewoffset = ent->client->ps.viewoffset;
- // perform a pmove
- Pmove(&pm);
- if (pm.groundentity && ent->groundentity)
- {
- float stepsize = fabs(ent->s.origin[2] - pm.s.origin[2]);
- if (stepsize > 4.f && stepsize < STEPSIZE)
- {
- ent->s.renderfx |= RF_STAIR_STEP;
- ent->client->step_frame = gi.ServerFrame() + 1;
- }
- }
- P_FallingDamage(ent, pm);
- if (ent->client->landmark_free_fall && pm.groundentity)
- {
- ent->client->landmark_free_fall = false;
- ent->client->landmark_noise_time = level.time + 100_ms;
- }
- // [Paril-KEX] save old position for G_TouchProjectiles
- vec3_t old_origin = ent->s.origin;
- ent->s.origin = pm.s.origin;
- ent->velocity = pm.s.velocity;
- // [Paril-KEX] if we stepped onto/off of a ladder, reset the
- // last ladder pos
- if ((pm.s.pm_flags & PMF_ON_LADDER) != (client->ps.pmove.pm_flags & PMF_ON_LADDER))
- {
- client->last_ladder_pos = ent->s.origin;
- if (pm.s.pm_flags & PMF_ON_LADDER)
- {
- if (!deathmatch->integer &&
- client->last_ladder_sound < level.time)
- {
- ent->s.event = EV_LADDER_STEP;
- client->last_ladder_sound = level.time + LADDER_SOUND_TIME;
- }
- }
- }
- // save results of pmove
- client->ps.pmove = pm.s;
- client->old_pmove = pm.s;
- ent->mins = pm.mins;
- ent->maxs = pm.maxs;
- if (!ent->client->menu)
- client->resp.cmd_angles = ucmd->angles;
- if (pm.jump_sound && !(pm.s.pm_flags & PMF_ON_LADDER))
- {
- gi.sound(ent, CHAN_VOICE, gi.soundindex("*jump1.wav"), 1, ATTN_NORM, 0);
- // Paril: removed to make ambushes more effective and to
- // not have monsters around corners come to jumps
- // PlayerNoise(ent, ent->s.origin, PNOISE_SELF);
- }
- // ROGUE sam raimi cam support
- if (ent->flags & FL_SAM_RAIMI)
- ent->viewheight = 8;
- else
- ent->viewheight = (int) pm.s.viewheight;
- // ROGUE
- ent->waterlevel = pm.waterlevel;
- ent->watertype = pm.watertype;
- ent->groundentity = pm.groundentity;
- if (pm.groundentity)
- ent->groundentity_linkcount = pm.groundentity->linkcount;
- if (ent->deadflag)
- {
- client->ps.viewangles[ROLL] = 40;
- client->ps.viewangles[PITCH] = -15;
- client->ps.viewangles[YAW] = client->killer_yaw;
- }
- else if (!ent->client->menu)
- {
- client->v_angle = pm.viewangles;
- client->ps.viewangles = pm.viewangles;
- AngleVectors(client->v_angle, client->v_forward, nullptr, nullptr);
- }
- // ZOID
- if (client->ctf_grapple)
- CTFGrapplePull(client->ctf_grapple);
- // ZOID
- gi.linkentity(ent);
- // PGM trigger_gravity support
- ent->gravity = 1.0;
- // PGM
- if (ent->movetype != MOVETYPE_NOCLIP)
- {
- G_TouchTriggers(ent);
- G_TouchProjectiles(ent, old_origin);
- }
- // touch other objects
- for (i = 0; i < pm.touch.num; i++)
- {
- trace_t &tr = pm.touch.traces[i];
- other = tr.ent;
- if (other->touch)
- other->touch(other, ent, tr, true);
- }
- }
- // fire weapon from final position if needed
- if (client->latched_buttons & BUTTON_ATTACK)
- {
- if (client->resp.spectator)
- {
- client->latched_buttons = BUTTON_NONE;
- if (client->chase_target)
- {
- client->chase_target = nullptr;
- client->ps.pmove.pm_flags &= ~(PMF_NO_POSITIONAL_PREDICTION | PMF_NO_ANGULAR_PREDICTION);
- }
- else
- GetChaseTarget(ent);
- }
- else if (!ent->client->weapon_thunk)
- {
- // we can only do this during a ready state and
- // if enough time has passed from last fire
- if (ent->client->weaponstate == WEAPON_READY)
- {
- ent->client->weapon_fire_buffered = true;
- if (ent->client->weapon_fire_finished <= level.time)
- {
- ent->client->weapon_thunk = true;
- Think_Weapon(ent);
- }
- }
- }
- }
- if (client->resp.spectator)
- {
- if (!HandleMenuMovement(ent, ucmd))
- {
- if (ucmd->buttons & BUTTON_JUMP)
- {
- if (!(client->ps.pmove.pm_flags & PMF_JUMP_HELD))
- {
- client->ps.pmove.pm_flags |= PMF_JUMP_HELD;
- if (client->chase_target)
- ChaseNext(ent);
- else
- GetChaseTarget(ent);
- }
- }
- else
- client->ps.pmove.pm_flags &= ~PMF_JUMP_HELD;
- }
- }
- // update chase cam if being followed
- for (i = 1; i <= game.maxclients; i++)
- {
- other = g_edicts + i;
- if (other->inuse && other->client->chase_target == ent)
- UpdateChaseCam(other);
- }
- }
- // active monsters
- struct active_monsters_filter_t
- {
- inline bool operator()(edict_t *ent) const
- {
- return (ent->inuse && (ent->svflags & SVF_MONSTER) && ent->health > 0);
- }
- };
- inline entity_iterable_t<active_monsters_filter_t> active_monsters()
- {
- return entity_iterable_t<active_monsters_filter_t> { game.maxclients + (uint32_t)BODY_QUEUE_SIZE + 1U };
- }
- inline bool G_MonstersSearchingFor(edict_t *player)
- {
- for (auto ent : active_monsters())
- {
- // check for *any* player target
- if (player == nullptr && ent->enemy && !ent->enemy->client)
- continue;
- // they're not targeting us, so who cares
- else if (player != nullptr && ent->enemy != player)
- continue;
- // they lost sight of us
- if ((ent->monsterinfo.aiflags & AI_LOST_SIGHT) && level.time > ent->monsterinfo.trail_time + 5_sec)
- continue;
- // no sir
- return true;
- }
- // yes sir
- return false;
- }
- // [Paril-KEX] from the given player, find a good spot to
- // spawn a player
- inline bool G_FindRespawnSpot(edict_t *player, vec3_t &spot)
- {
- // sanity check; make sure there's enough room for ourselves.
- // (crouching in a small area, etc)
- trace_t tr = gi.trace(player->s.origin, PLAYER_MINS, PLAYER_MAXS, player->s.origin, player, MASK_PLAYERSOLID);
- if (tr.startsolid || tr.allsolid)
- return false;
- // throw five boxes a short-ish distance from the player and see if they land in a good, visible spot
- constexpr float yaw_spread[] = { 0, 90, 45, -45, -90 };
- constexpr float back_distance = 128.f;
- constexpr float up_distance = 128.f;
- constexpr float player_viewheight = 22.f;
- // we don't want to spawn inside of these
- contents_t mask = MASK_PLAYERSOLID | CONTENTS_LAVA | CONTENTS_SLIME;
- for (auto &yaw : yaw_spread)
- {
- vec3_t angles = { 0, (player->s.angles[YAW] + 180) + yaw, 0 };
- // throw the box three times:
- // one up & back
- // one back
- // one up, then back
- // pick the one that went the farthest
- vec3_t start = player->s.origin;
- vec3_t end = start + vec3_t { 0, 0, up_distance };
- tr = gi.trace(start, PLAYER_MINS, PLAYER_MAXS, end, player, mask);
- // stuck
- if (tr.startsolid || tr.allsolid || (tr.contents & (CONTENTS_LAVA | CONTENTS_SLIME)))
- continue;
- vec3_t fwd;
- AngleVectors(angles, fwd, nullptr, nullptr);
- start = tr.endpos;
- end = start + fwd * back_distance;
- tr = gi.trace(start, PLAYER_MINS, PLAYER_MAXS, end, player, mask);
- // stuck
- if (tr.startsolid || tr.allsolid || (tr.contents & (CONTENTS_LAVA | CONTENTS_SLIME)))
- continue;
- // plop us down now
- start = tr.endpos;
- end = tr.endpos - vec3_t { 0, 0, up_distance * 4 };
- tr = gi.trace(start, PLAYER_MINS, PLAYER_MAXS, end, player, mask);
- // stuck, or floating, or touching some other entity
- if (tr.startsolid || tr.allsolid || (tr.contents & (CONTENTS_LAVA | CONTENTS_SLIME)) || tr.fraction == 1.0f || tr.ent != world)
- continue;
- // don't spawn us *inside* liquids
- if (gi.pointcontents(tr.endpos + vec3_t{0, 0, player_viewheight}) & MASK_WATER)
- continue;
- // don't spawn us on steep slopes
- if (tr.plane.normal.z < 0.7f)
- continue;
- spot = tr.endpos;
- float z_diff = fabsf(player->s.origin[2] - tr.endpos[2]);
- // 5 steps is way too many steps
- if (z_diff > STEPSIZE * 4.f)
- continue;
- // if we went up or down 1 step, make sure we can still see their origin and their head
- if (z_diff > STEPSIZE)
- {
- tr = gi.traceline(player->s.origin, tr.endpos, player, mask);
- if (tr.fraction != 1.0f)
- continue;
- tr = gi.traceline(player->s.origin + vec3_t{0, 0, player_viewheight}, tr.endpos + vec3_t{0, 0, player_viewheight}, player, mask);
- if (tr.fraction != 1.0f)
- continue;
- }
- // good spot!
- return true;
- }
- return false;
- }
- // [Paril-KEX] check each player to find a good
- // respawn target & position
- inline std::tuple<edict_t *, vec3_t> G_FindSquadRespawnTarget()
- {
- bool monsters_searching_for_anybody = G_MonstersSearchingFor(nullptr);
- for (auto player : active_players())
- {
- // no dead players
- if (player->deadflag)
- continue;
-
- // check combat state; we can't have taken damage recently
- if (player->client->last_damage_time >= level.time)
- {
- player->client->coop_respawn_state = COOP_RESPAWN_IN_COMBAT;
- continue;
- }
- // check if any monsters are currently targeting us
- // or searching for us
- if (G_MonstersSearchingFor(player))
- {
- player->client->coop_respawn_state = COOP_RESPAWN_IN_COMBAT;
- continue;
- }
- // check firing state; if any enemies are mad at any players,
- // don't respawn until everybody has cooled down
- if (monsters_searching_for_anybody && player->client->last_firing_time >= level.time)
- {
- player->client->coop_respawn_state = COOP_RESPAWN_IN_COMBAT;
- continue;
- }
- // check positioning; we must be on world ground
- if (player->groundentity != world)
- {
- player->client->coop_respawn_state = COOP_RESPAWN_BAD_AREA;
- continue;
- }
- // can't be in liquid
- if (player->waterlevel >= WATER_UNDER)
- {
- player->client->coop_respawn_state = COOP_RESPAWN_BAD_AREA;
- continue;
- }
- // good player; pick a spot
- vec3_t spot;
-
- if (!G_FindRespawnSpot(player, spot))
- {
- player->client->coop_respawn_state = COOP_RESPAWN_BLOCKED;
- continue;
- }
- // good player most likely
- return { player, spot };
- }
- // no good player
- return { nullptr, {} };
- }
- enum respawn_state_t
- {
- RESPAWN_NONE, // invalid state
- RESPAWN_SPECTATE, // move to spectator
- RESPAWN_SQUAD, // move to good squad point
- RESPAWN_START // move to start of map
- };
- // [Paril-KEX] return false to fall back to click-to-respawn behavior.
- // note that this is only called if they are allowed to respawn (not
- // restarting the level due to all being dead)
- static bool G_CoopRespawn(edict_t *ent)
- {
- // don't do this in non-coop
- if (!coop->integer)
- return false;
- // if we don't have squad or lives, it doesn't matter
- else if (!g_coop_squad_respawn->integer && !g_coop_enable_lives->integer)
- return false;
- respawn_state_t state = RESPAWN_NONE;
- // first pass: if we have no lives left, just move to spectator
- if (g_coop_enable_lives->integer)
- {
- if (ent->client->pers.lives == 0)
- {
- state = RESPAWN_SPECTATE;
- ent->client->coop_respawn_state = COOP_RESPAWN_NO_LIVES;
- }
- }
- // second pass: check for where to spawn
- if (state == RESPAWN_NONE)
- {
- // if squad respawn, don't respawn until we can find a good player to spawn on.
- if (g_coop_squad_respawn->integer)
- {
- bool allDead = true;
- for (auto player : active_players())
- {
- if (player->health > 0)
- {
- allDead = false;
- break;
- }
- }
- // all dead, so if we ever get here we have lives enabled;
- // we should just respawn at the start of the level
- if (allDead)
- state = RESPAWN_START;
- else
- {
- auto [ good_player, good_spot ] = G_FindSquadRespawnTarget();
- if (good_player) {
- state = RESPAWN_SQUAD;
- squad_respawn_position = good_spot;
- squad_respawn_angles = good_player->s.angles;
- squad_respawn_angles[2] = 0;
- use_squad_respawn = true;
- } else {
- state = RESPAWN_SPECTATE;
- }
- }
- }
- else
- state = RESPAWN_START;
- }
- if (state == RESPAWN_SQUAD || state == RESPAWN_START)
- {
- // give us our max health back since it will reset
- // to pers.health; in instanced items we'd lose the items
- // we touched so we always want to respawn with our max.
- if (P_UseCoopInstancedItems())
- ent->client->pers.health = ent->client->pers.max_health = ent->max_health;
- respawn(ent);
- ent->client->latched_buttons = BUTTON_NONE;
- use_squad_respawn = false;
- }
- else if (state == RESPAWN_SPECTATE)
- {
- if (!ent->client->coop_respawn_state)
- ent->client->coop_respawn_state = COOP_RESPAWN_WAITING;
- if (!ent->client->resp.spectator)
- {
- // move us to spectate just so we don't have to twiddle
- // our thumbs forever
- CopyToBodyQue(ent);
- ent->client->resp.spectator = true;
- ent->solid = SOLID_NOT;
- ent->takedamage = false;
- ent->s.modelindex = 0;
- ent->svflags |= SVF_NOCLIENT;
- ent->client->ps.damage_blend[3] = ent->client->ps.screen_blend[3] = 0;
- ent->client->ps.rdflags = RDF_NONE;
- ent->movetype = MOVETYPE_NOCLIP;
- // TODO: check if anything else needs to be reset
- gi.linkentity(ent);
- GetChaseTarget(ent);
- }
- }
- return true;
- }
- /*
- ==============
- ClientBeginServerFrame
- This will be called once for each server frame, before running
- any other entities in the world.
- ==============
- */
- void ClientBeginServerFrame(edict_t *ent)
- {
- gclient_t *client;
- int buttonMask;
- if (gi.ServerFrame() != ent->client->step_frame)
- ent->s.renderfx &= ~RF_STAIR_STEP;
- if (level.intermissiontime)
- return;
- client = ent->client;
- if (client->awaiting_respawn)
- {
- if ((level.time.milliseconds() % 500) == 0)
- PutClientInServer(ent);
- return;
- }
- if ( ( ent->svflags & SVF_BOT ) != 0 ) {
- Bot_BeginFrame( ent );
- }
- if (deathmatch->integer && !G_TeamplayEnabled() &&
- client->pers.spectator != client->resp.spectator &&
- (level.time - client->respawn_time) >= 5_sec)
- {
- spectator_respawn(ent);
- return;
- }
- // run weapon animations if it hasn't been done by a ucmd_t
- if (!client->weapon_thunk && !client->resp.spectator)
- Think_Weapon(ent);
- else
- client->weapon_thunk = false;
- if (ent->deadflag)
- {
- // don't respawn if level is waiting to restart
- if (level.time > client->respawn_time && !level.coop_level_restart_time)
- {
- // check for coop handling
- if (!G_CoopRespawn(ent))
- {
- // in deathmatch, only wait for attack button
- if (deathmatch->integer)
- buttonMask = BUTTON_ATTACK;
- else
- buttonMask = -1;
- if ((client->latched_buttons & buttonMask) ||
- (deathmatch->integer && g_dm_force_respawn->integer))
- {
- respawn(ent);
- client->latched_buttons = BUTTON_NONE;
- }
- }
- }
- return;
- }
- // add player trail so monsters can follow
- if (!deathmatch->integer)
- PlayerTrail_Add(ent);
- client->latched_buttons = BUTTON_NONE;
- }
- /*
- ==============
- RemoveAttackingPainDaemons
- This is called to clean up the pain daemons that the disruptor attaches
- to clients to damage them.
- ==============
- */
- void RemoveAttackingPainDaemons(edict_t *self)
- {
- edict_t *tracker;
- tracker = G_FindByString<&edict_t::classname>(nullptr, "pain daemon");
- while (tracker)
- {
- if (tracker->enemy == self)
- G_FreeEdict(tracker);
- tracker = G_FindByString<&edict_t::classname>(tracker, "pain daemon");
- }
- if (self->client)
- self->client->tracker_pain_time = 0_ms;
- }
|