From 6653d18417d7ec1f91593278aa8e50084bbe3138 Mon Sep 17 00:00:00 2001 From: Marco Hladik Date: Wed, 11 May 2022 13:18:02 -0700 Subject: [PATCH] Forgot to add these files (NSClient/Player/Spectator) --- src/shared/NSClient.h | 38 ++ src/shared/NSClient.qc | 84 +++ src/shared/NSClientPlayer.h | 122 ++++ src/shared/NSClientPlayer.qc | 951 ++++++++++++++++++++++++++++++++ src/shared/NSClientSpectator.h | 65 +++ src/shared/NSClientSpectator.qc | 483 ++++++++++++++++ 6 files changed, 1743 insertions(+) create mode 100644 src/shared/NSClient.h create mode 100644 src/shared/NSClient.qc create mode 100644 src/shared/NSClientPlayer.h create mode 100644 src/shared/NSClientPlayer.qc create mode 100644 src/shared/NSClientSpectator.h create mode 100644 src/shared/NSClientSpectator.qc diff --git a/src/shared/NSClient.h b/src/shared/NSClient.h new file mode 100644 index 00000000..206fb8ae --- /dev/null +++ b/src/shared/NSClient.h @@ -0,0 +1,38 @@ +/* both NSClientPlayer and base_NSClientSpectator are based off this class */ +class +NSClient:NSSurfacePropEntity +{ + vector origin_net; + vector velocity_net; + + NSXRSpace m_xrSpace; + NSXRInput m_xrInputHead; + NSXRInput m_xrInputLeft; + NSXRInput m_xrInputRight; + + void(void) NSClient; + + /* final input handling of the client */ + virtual void(void) ClientInput; + + virtual void(void) PreFrame; + virtual void(void) PostFrame; + + virtual bool(void) IsFakeSpectator; + virtual bool(void) IsRealSpectator; + virtual bool(void) IsDead; + virtual bool(void) IsPlayer; + + virtual void(void) OnRemoveEntity; + +#ifdef CLIENT + /* gives the chance to override input variables before networking */ + virtual void(void) ClientInputFrame; + + /* our camera when we're dead */ + virtual void(void) UpdateDeathcam; + + /* run every frame before renderscene() */ + virtual float(void) predraw; +#endif +}; diff --git a/src/shared/NSClient.qc b/src/shared/NSClient.qc new file mode 100644 index 00000000..25354dc7 --- /dev/null +++ b/src/shared/NSClient.qc @@ -0,0 +1,84 @@ +void +NSClient::OnRemoveEntity(void) +{ + XR_Shutdown(this); +} + +void +NSClient::ClientInput(void) +{ +} + +void +NSClient::PreFrame(void) +{ +} + +void +NSClient::PostFrame(void) +{ +} + +bool +NSClient::IsFakeSpectator(void) +{ + return (false); +} + +bool +NSClient::IsRealSpectator(void) +{ + return (false); +} + +bool +NSClient::IsDead(void) +{ + return (false); +} + +bool +NSClient::IsPlayer(void) +{ + return (false); +} + +#ifdef CLIENT +void +NSClient::ClientInputFrame(void) +{ +} + +void +NSClient::UpdateDeathcam(void) +{ + /* death cam */ + view_angles[2] = 45.0f; + setproperty(VF_ORIGIN, pSeat->m_vecPredictedOrigin); + setproperty(VF_CL_VIEWANGLES, view_angles); + setproperty(VF_ANGLES, view_angles); +} + +float +NSClient::predraw(void) +{ + return (PREDRAW_NEXT); +} +#endif + +void +NSClient::NSClient(void) +{ + XR_Init(this); +} + + +float +Client_InIntermission(void) +{ +#ifdef CLIENT + return g_iIntermission; +#else + return (float)g_grMode.InIntermission(); +#endif +} diff --git a/src/shared/NSClientPlayer.h b/src/shared/NSClientPlayer.h new file mode 100644 index 00000000..7b537b05 --- /dev/null +++ b/src/shared/NSClientPlayer.h @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2016-2021 Marco Cawthorne + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +class +NSClientPlayer:NSClientSpectator +{ +#ifdef SERVER + PREDICTED_INT_N(weaponframe); +#else + PREDICTED_INT(weaponframe); + PREDICTED_FLOAT(vehicle_entnum); +#endif + PREDICTED_FLOAT(health); + PREDICTED_FLOAT(armor); + + PREDICTED_FLOAT_N(colormap); + PREDICTED_FLOAT_N(gflags); + PREDICTED_FLOAT(viewzoom); + PREDICTED_VECTOR_N(view_ofs); + PREDICTED_VECTOR(v_angle); + PREDICTED_FLOAT_N(pmove_flags); + + PREDICTED_FLOAT(w_attack_next); + PREDICTED_FLOAT(w_idle_next); + PREDICTED_FLOAT(teleport_time); + PREDICTED_FLOAT(weapontime); + PREDICTED_VECTOR(punchangle); + + /* We can't use the default .items field, because FTE will assume + * effects of some bits. Such as invisibility, quad, etc. + * also, modders probably want 32 bits for items. */ + PREDICTED_INT(g_items); + PREDICTED_FLOAT(activeweapon); + + /* vehicle info */ + PREDICTED_ENT(vehicle); + + /* these are NOT networked */ + int a_ammo1; + int a_ammo2; + int a_ammo3; + + /* any mods that use hooks */ + entity hook; + + void(void) NSClientPlayer; + + virtual void(void) ClientInput; + + virtual void(void) PreFrame; + virtual void(void) PostFrame; + + virtual void(float) Physics_Fall; + virtual void(void) Physics_Crouch; + virtual void(void) Physics_Jump; + virtual void(float) Physics_CheckJump; + virtual void(void) Physics_SetViewParms; + virtual void(void) Physics_WaterJump; + virtual void(void) Physics_WaterMove; + virtual float(void) Physics_MaxSpeed; + virtual void(void) Physics_InputPreMove; + virtual void(void) Physics_InputPostMove; + virtual void(void) Physics_Run; + + virtual bool(void) IsFakeSpectator; + virtual bool(void) IsRealSpectator; + virtual bool(void) IsDead; + virtual bool(void) IsPlayer; + +#ifdef CLIENT + int sequence; + + /* external weapon model */ + entity p_model; + int p_hand_bone; + int p_model_bone; + float lastweapon; + + virtual void(void) VehicleRelink; + virtual void(void) OnRemoveEntity; + virtual void(float, float) ReceiveEntity; + virtual void(void) PredictPreFrame; + virtual void(void) PredictPostFrame; + virtual void(void) ClientInputFrame; +#else + int voted; + int step; + float step_time; + + float underwater_time; + float underwater_dmg; + float pain_time; + + entity last_used; + + virtual void(float) Save; + virtual void(string,string) Restore; + virtual void(void) Respawn; + virtual void(void) EvaluateEntity; + virtual float(entity, float) SendEntity; + + virtual void(void) Death; + virtual void(void) MakePlayer; + virtual void(void) MakeTempSpectator; + + virtual void(void) InputUse_Down; + virtual void(void) InputUse_Up; +#endif +}; diff --git a/src/shared/NSClientPlayer.qc b/src/shared/NSClientPlayer.qc new file mode 100644 index 00000000..36314c0c --- /dev/null +++ b/src/shared/NSClientPlayer.qc @@ -0,0 +1,951 @@ +/* + * Copyright (c) 2016-2021 Marco Cawthorne + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +bool +NSClientPlayer::IsRealSpectator(void) +{ + return (false); +} + +bool +NSClientPlayer::IsDead(void) +{ + if (health > 0) + return (false); + else + return (true); +} + +bool +NSClientPlayer::IsPlayer(void) +{ + return (false); +} + +bool +NSClientPlayer::IsFakeSpectator(void) +{ + if (GetFlags() & FL_FAKESPEC) + return (true); + + return (false); +} + +void +NSClientPlayer::PreFrame(void) +{ +#ifdef CLIENT + /* this is where a game/mod would decide to add more prediction rollback + * information. */ + PredictPreFrame(); + + if (vehicle) { + NSVehicle veh = (NSVehicle)vehicle; + veh.PredictPreFrame(); + } + + /* run physics code for all the input frames which we've not heard back + * from yet. This continues on in Player_ReceiveEntity! */ + for (int i = sequence + 1; i <= clientcommandframe; i++) { + float flSuccess = getinputstate(i); + if (flSuccess == FALSE) { + continue; + } + + /* don't do partial frames, aka incomplete input packets */ + if (input_timelength == 0) { + break; + } + + if (i==clientcommandframe){ + CSQC_Input_Frame(); + } + + /* this global is for our shared random number seed */ + input_sequence = i; + + /* run our custom physics */ + Physics_Run(); + } +#endif +} +void +NSClientPlayer::PostFrame(void) +{ +#ifdef CLIENT + /* give the game/mod a chance to roll back its values too */ + PredictPostFrame(); + setorigin(this, origin); /* update bounds */ + + if (vehicle) { + NSVehicle veh = (NSVehicle)vehicle; + veh.PredictPostFrame(); + setorigin(veh, veh.origin); + } +#endif +} + + +void +NSClientPlayer::ClientInput(void) +{ + XR_InputFrame(this); + + if (!Client_InIntermission() && IsFakeSpectator()) { + NSClientSpectator::ClientInput(); + SpectatorTrackPlayer(); + return; + } + + /* allow vehicles to prevent weapon logic from happening */ + if (vehicle) { + NSVehicle veh = (NSVehicle)vehicle; + + if (veh.PlayerInput) + veh.PlayerInput(); + } + + /* weapon/item logic of what the player controls */ + Game_Input((player)this); +} + +#ifdef CLIENT +void +NSClientPlayer::VehicleRelink(void) +{ + if (!vehicle_entnum) + vehicle = __NULL__; + else + vehicle = findentity(world, ::entnum, vehicle_entnum); +} + +void +NSClientPlayer::OnRemoveEntity(void) +{ + if (p_model) + remove(p_model); + + super::OnRemoveEntity(); +} + +/* +================= +NSClientPlayer::ClientInputFrame + +This is basically CSQC_Input_Frame! So games can override this as they please. +================= +*/ +void +NSClientPlayer::ClientInputFrame(void) +{ + if (IsFakeSpectator()) { + NSClientSpectator::ClientInputFrame(); + return; + } + + /* If we are inside a VGUI, don't let the client do stuff outside */ + if (VGUI_Active()) { + input_impulse = 0; + input_buttons = 0; + return; + } + + /* background maps have no input */ + if (serverkeyfloat("background") == 1) + return; + + if (pSeat->m_iInputAttack2 == TRUE) { + input_buttons |= INPUT_BUTTON3; + } + + if (pSeat->m_iInputReload == TRUE) { + input_buttons |= INPUT_BUTTON4; + } + + if (pSeat->m_iInputUse == TRUE) { + input_buttons |= INPUT_BUTTON5; + } + + if (pSeat->m_iInputDuck == TRUE) { + input_buttons |= INPUT_BUTTON8; + } + + /* The HUD needs more time */ + if (pSeat->m_iHUDWeaponSelected) { + if ((input_buttons & INPUT_BUTTON0)) + HUD_DrawWeaponSelect_Trigger(); + else if ((input_buttons & INPUT_BUTTON3)) + pSeat->m_iHUDWeaponSelected = pSeat->m_flHUDWeaponSelectTime = 0; + + pSeat->m_flInputBlockTime = time + 0.2; + } + + /* prevent accidental input packets */ + if (pSeat->m_flInputBlockTime > time) { + input_buttons &= ~INPUT_BUTTON0; + input_buttons &= ~INPUT_BUTTON3; + pSeat->m_iInputAttack2 = FALSE; + return; + } + + /* some input overrides for XR */ + if (XR_Available(this)) { + if (pSeat->m_bMoveForward) { + input_movevalues[0] = 100; + } + + if (pSeat->m_iInputAttack) { + input_buttons |= INPUT_BUTTON0; + } + } + + /* compat*/ + if (input_impulse == 201) { + sendevent("Spraylogo", ""); + } + + if (pSeat->m_flCameraTime > time) { + /* TODO: Supress the changing of view_angles/input_angles. */ + } +} + +/* +================= +NSClientPlayer::ReceiveEntity + +Receive the generic client attributes from the server. +If you want to override this, do not call this +at the top of player::ReceiveEntity +================= +*/ +void +NSClientPlayer::ReceiveEntity(float new, float fl) +{ + /* store which input sequence we're on, this helps us + * later when we run prediction again between last/latest + * servercommandframe */ + sequence = servercommandframe; + + /* HACK: we need to make this more reliable */ + if (fl == UPDATE_ALL) { + /* we respawned */ + gravity = 1.0f; + } + + if (fl & PLAYER_MODELINDEX) { + modelindex = readshort(); + } + + if (fl & PLAYER_ORIGIN) { + origin[0] = readcoord(); + origin[1] = readcoord(); + } + + if (fl & PLAYER_ORIGIN_Z) + origin[2] = readcoord(); + if (fl & PLAYER_ANGLES_X) { + v_angle[0] = readshort() / (32767 / 360); + v_angle[1] = readshort() / (32767 / 360); + v_angle[2] = readshort() / (32767 / 360); + } + if (fl & PLAYER_ANGLES_Y) { + angles[0] = readshort() / (32767 / 360); + angles[1] = readshort() / (32767 / 360); + angles[2] = readshort() / (32767 / 360); + } + if (fl & PLAYER_COLORMAP) + colormap = readbyte(); + + if (fl & PLAYER_VELOCITY) { + velocity[0] = readcoord(); + velocity[1] = readcoord(); + } + + if (fl & PLAYER_VELOCITY_Z) + velocity[2] = readcoord(); + if (fl & PLAYER_FLAGS) { + flags = readfloat(); + gflags = readfloat(); + pmove_flags = readfloat(); + + /* mainly used for other players receiving us */ + if (flags & FL_CROUCHING) + setsize(self, PHY_HULL_CROUCHED_MIN, PHY_HULL_CROUCHED_MAX); + else + setsize(self, PHY_HULL_MIN, PHY_HULL_MAX); + } + if (fl & PLAYER_WEAPON) { + activeweapon = readbyte(); + weaponframe = (int)readbyte(); + } + if (fl & PLAYER_ITEMS) + g_items = (__variant)readfloat(); + if (fl & PLAYER_HEALTH) + health = readbyte(); + if (fl & PLAYER_ARMOR) + armor = readbyte(); + if (fl & PLAYER_MOVETYPE) { + movetype = readbyte(); + solid = readbyte(); + } + if (fl & PLAYER_VIEWOFS) + view_ofs[2] = readfloat(); + + /* TO OPTIMISE */ + teleport_time = readfloat(); + viewzoom = readfloat(); + weapontime = readfloat(); + w_attack_next = readfloat(); + w_idle_next = readfloat(); + punchangle[0] = readfloat(); + punchangle[1] = readfloat(); + punchangle[2] = readfloat(); + vehicle_entnum = readentitynum(); + VehicleRelink(); + + /* FIXME: Make this temp spec only */ + spec_ent = readbyte(); + spec_mode = readbyte(); + spec_flags = readbyte(); + + PredictPreFrame(); +} + +/* +================= +NSClientPlayer::PredictPreFrame + +Save the state of the last server-confirmed attributes. +If you want to override this, do not call this +at the top of player::PredictPreFrame +================= +*/ +void +NSClientPlayer::PredictPreFrame(void) +{ + /* base player attributes/fields we're going to roll back */ + SAVE_STATE(modelindex); + SAVE_STATE(origin); + SAVE_STATE(angles); + SAVE_STATE(v_angle); + SAVE_STATE(colormap); + SAVE_STATE(velocity); + SAVE_STATE(flags); + SAVE_STATE(gflags); + SAVE_STATE(pmove_flags); + SAVE_STATE(activeweapon); + SAVE_STATE(g_items); + SAVE_STATE(health); + SAVE_STATE(armor); + SAVE_STATE(movetype); + SAVE_STATE(solid); + SAVE_STATE(view_ofs); + + /* TO OPTIMISE */ + SAVE_STATE(teleport_time); + SAVE_STATE(viewzoom); + SAVE_STATE(weaponframe); + SAVE_STATE(weapontime); + SAVE_STATE(w_attack_next); + SAVE_STATE(w_idle_next); + SAVE_STATE(punchangle); + SAVE_STATE(vehicle_entnum); + + SAVE_STATE(spec_ent); + SAVE_STATE(spec_mode); + SAVE_STATE(spec_flags); +} + +/* +================= +NSClientPlayer::PredictPostFrame + +After running prediction on the client, roll back the values +to the server's confirmed saved attributes from PredictPreFrame. +If you want to override this, do not call this +at the top of player::PredictPostFrame +================= +*/ +void +NSClientPlayer::PredictPostFrame(void) +{ + /* finally roll the values back */ + ROLL_BACK(modelindex); + ROLL_BACK(origin); + ROLL_BACK(angles); + ROLL_BACK(v_angle); + ROLL_BACK(colormap); + ROLL_BACK(velocity); + ROLL_BACK(flags); + ROLL_BACK(gflags); + ROLL_BACK(pmove_flags); + ROLL_BACK(activeweapon); + ROLL_BACK(g_items); + ROLL_BACK(health); + ROLL_BACK(armor); + ROLL_BACK(movetype); + ROLL_BACK(solid); + ROLL_BACK(view_ofs); + + /* TO OPTIMISE */ + ROLL_BACK(teleport_time); + ROLL_BACK(viewzoom); + ROLL_BACK(weaponframe); + ROLL_BACK(weapontime); + ROLL_BACK(w_attack_next); + ROLL_BACK(w_idle_next); + ROLL_BACK(punchangle); + ROLL_BACK(vehicle_entnum); + + ROLL_BACK(spec_ent); + ROLL_BACK(spec_mode); + ROLL_BACK(spec_flags); +} +#else +void +NSClientPlayer::Save(float handle) +{ + SaveFloat(handle, "health", health); + SaveFloat(handle, "armor", armor); + SaveFloat(handle, "modelindex", modelindex); + SaveVector(handle, "origin", origin); + SaveVector(handle, "velocity", velocity); + SaveVector(handle, "angles", angles); + SaveFloat(handle, "colormap", colormap); + SaveFloat(handle, "flags", flags); + SaveFloat(handle, "gflags", gflags); + SaveFloat(handle, "viewzoom", viewzoom); + SaveVector(handle, "view_ofs", view_ofs); + SaveVector(handle, "v_angle", v_angle); + SaveVector(handle, "punchangle", punchangle); + SaveFloat(handle, "movetype", movetype); + SaveFloat(handle, "pmove_flags", pmove_flags); + SaveFloat(handle, "w_attack_next", w_attack_next); + SaveFloat(handle, "w_idle_next", w_idle_next); + SaveFloat(handle, "teleport_time", teleport_time); + SaveInt(handle, "weaponframe", weaponframe); + SaveFloat(handle, "weapontime", weapontime); + SaveInt(handle, "g_items", g_items); + SaveFloat(handle, "activeweapon", activeweapon); + SaveFloat(handle, "vehicle", num_for_edict(vehicle)); +} + +void +NSClientPlayer::Restore(string strKey, string strValue) +{ + switch (strKey) { + case "health": + health = ReadFloat(strValue); + break; + case "armor": + armor = ReadFloat(strValue); + break; + case "modelindex": + modelindex = ReadFloat(strValue); + break; + case "origin": + origin = ReadVector(strValue); + break; + case "velocity": + velocity = ReadVector(strValue); + break; + case "angles": + angles = ReadVector(strValue); + break; + case "colormap": + colormap = ReadFloat(strValue); + break; + case "flags": + flags = ReadFloat(strValue); + break; + case "gflags": + gflags = ReadFloat(strValue); + break; + case "view_ofs": + view_ofs = ReadVector(strValue); + break; + case "v_angle": + v_angle = ReadVector(strValue); + break; + case "punchangle": + punchangle = ReadVector(strValue); + break; + case "movetype": + movetype = ReadFloat(strValue); + break; + case "pmove_flags": + pmove_flags = ReadFloat(strValue); + break; + case "w_attack_next": + w_attack_next = ReadFloat(strValue); + break; + case "w_idle_next": + w_idle_next = ReadFloat(strValue); + break; + case "teleport_time": + teleport_time = ReadFloat(strValue); + break; + case "weaponframe": + weaponframe = ReadInt(strValue); + break; + case "weapontime": + weapontime = ReadFloat(strValue); + break; + case "g_items": + g_items = ReadInt(strValue); + break; + case "activeweapon": + activeweapon = ReadFloat(strValue); + break; + case "vehicle": + vehicle = edict_num(ReadFloat(strValue)); + break; + default: + super::Restore(strKey, strValue); + } +} + +/* +================= +NSClientPlayer::Respawn + +it'd be pretty unfortunate if 'sv respawn_ents' or something called this +================= +*/ +void +NSClientPlayer::Respawn(void) +{ + /* make sure nothing happens here */ +} + +/* +================= +NSClientPlayer::MakeTempSpectator + +This is what dead players in round matches become, or when we spawn +for the first time before selecting a loadout or something. +================= + */ +void +NSClientPlayer::MakeTempSpectator(void) +{ + classname = "player"; + flags = FL_CLIENT; + SetModelindex(0); + SetSolid(SOLID_NOT); + SetMovetype(MOVETYPE_NOCLIP); + SetTakedamage(DAMAGE_NO); + maxspeed = 250; + flags |= FL_FAKESPEC; + max_health = health = 0; + armor = 0; + g_items = 0; + activeweapon = 0; + effects = 0; + alpha = 0.0f; +} + +/* +================= +NSClientPlayer::MakeDead + +Sets all the appropriate attributes to make sure we're dead +================= + */ +void +NSClientPlayer::Death(void) +{ + classname = "player"; + health = max_health = 0; + armor = 0; + g_items = 0; + activeweapon = 0; + effects = 0; + alpha = 1.0f; + SetModelindex(0); + SetMovetype(MOVETYPE_NONE); + SetSolid(SOLID_NOT); + SetTakedamage(DAMAGE_NO); + viewzoom = 1.0; + view_ofs = [0,0,0]; + vehicle = __NULL__; + SetVelocity([0,0,0]); + SetGravity(1.0f); + customphysics = Empty; + iBleeds = FALSE; + forceinfokey(this, "*deaths", ftos(deaths)); + setsize(this, [0,0,0], [0,0,0]); +} + +/* +================= +NSClientPlayer::MakePlayer + +True participating player, can walk around and everything. +================= + */ +void +NSClientPlayer::MakePlayer(void) +{ + classname = "player"; + flags = FL_CLIENT; + health = max_health = 100; + armor = 0; + g_items = 0; + activeweapon = 0; + effects = 0; + alpha = 1.0f; + SetSolid(SOLID_SLIDEBOX); + SetMovetype(MOVETYPE_WALK); + SetTakedamage(DAMAGE_YES); + SetVelocity([0,0,0]); + viewzoom = 1.0; + vehicle = __NULL__; + SetGravity(1.0f); + SendFlags = UPDATE_ALL; + customphysics = Empty; + iBleeds = TRUE; + forceinfokey(this, "*deaths", ftos(deaths)); + SetSize(VEC_HULL_MIN, VEC_HULL_MAX); +} + +/* +================= +NSClientPlayer::EvaluateEntity + +Check which attributes have changed and flag the ones that did. +If you want to override this, do not call this +at the top of player::EvaluateEntity +================= +*/ +void +NSClientPlayer::EvaluateEntity(void) +{ + SetSendFlags(PLAYER_KEEPALIVE); + + if (ATTR_CHANGED(modelindex)) + SetSendFlags(PLAYER_MODELINDEX); + + if (VEC_CHANGED(origin, 0)) + SetSendFlags(PLAYER_ORIGIN); + + if (VEC_CHANGED(origin, 1)) + SetSendFlags(PLAYER_ORIGIN); + + if (VEC_CHANGED(origin, 2)) + SetSendFlags(PLAYER_ORIGIN_Z); + + if (VEC_CHANGED(v_angle, 0) || VEC_CHANGED(v_angle, 1) || VEC_CHANGED(v_angle, 2)) + SetSendFlags(PLAYER_ANGLES_X); + + if (VEC_CHANGED(angles, 0) || VEC_CHANGED(angles, 1) || VEC_CHANGED(angles, 2)) + SetSendFlags(PLAYER_ANGLES_Y); + + if (ATTR_CHANGED(colormap)) + SetSendFlags(PLAYER_COLORMAP); + + if (VEC_CHANGED(velocity, 0)) + SetSendFlags(PLAYER_VELOCITY); + + if (VEC_CHANGED(velocity, 1)) + SetSendFlags(PLAYER_VELOCITY); + + if (VEC_CHANGED(velocity, 2)) + SetSendFlags(PLAYER_VELOCITY_Z); + + if (ATTR_CHANGED(flags)) + SetSendFlags(PLAYER_FLAGS); + + if (ATTR_CHANGED(gflags)) + SetSendFlags(PLAYER_FLAGS); + + if (ATTR_CHANGED(pmove_flags)) + SetSendFlags(PLAYER_FLAGS); + + if (ATTR_CHANGED(weaponframe)) + SetSendFlags(PLAYER_WEAPON); + + if (ATTR_CHANGED(activeweapon)) + SetSendFlags(PLAYER_WEAPON); + + if (ATTR_CHANGED(g_items)) + SetSendFlags(PLAYER_ITEMS); + + if (ATTR_CHANGED(health)) + SetSendFlags(PLAYER_HEALTH); + + if (ATTR_CHANGED(armor)) + SetSendFlags(PLAYER_ARMOR); + + if (ATTR_CHANGED(movetype)) + SetSendFlags(PLAYER_MOVETYPE); + + if (ATTR_CHANGED(solid)) + SetSendFlags(PLAYER_MOVETYPE); + + if (ATTR_CHANGED(view_ofs)) + SetSendFlags(PLAYER_VIEWOFS); + + SAVE_STATE(modelindex); + SAVE_STATE(origin); + SAVE_STATE(angles); + SAVE_STATE(colormap); + SAVE_STATE(velocity); + SAVE_STATE(flags); + SAVE_STATE(gflags); + SAVE_STATE(pmove_flags); + SAVE_STATE(activeweapon); + SAVE_STATE(g_items); + SAVE_STATE(health); + SAVE_STATE(armor); + SAVE_STATE(movetype); + SAVE_STATE(solid); + SAVE_STATE(view_ofs); + + /* TO OPTIMISE */ + SAVE_STATE(teleport_time); + SAVE_STATE(viewzoom); + SAVE_STATE(weaponframe); + SAVE_STATE(weapontime); + SAVE_STATE(w_attack_next); + SAVE_STATE(w_idle_next); + SAVE_STATE(punchangle); + SAVE_STATE(vehicle); + + /* FIXME: Make this temp spec only */ + SAVE_STATE(spec_ent); + SAVE_STATE(spec_mode); + SAVE_STATE(spec_flags); +} + +/* +================= +NSClientPlayer::SendEntity + +Network any attributes that have been flagged for networking. +If you want to override this, do not call this +at the top of player::SendEntity +================= +*/ +float +NSClientPlayer::SendEntity(entity ePEnt, float fChanged) +{ + /* really trying to get our moneys worth with 23 bits of mantissa */ + if (fChanged & PLAYER_MODELINDEX) { + WriteShort(MSG_ENTITY, modelindex); + } + + /* if origin[0] changes, it's very likely [1] changes too, since + * we rarely ever walk in a straight line on the world grid */ + if (fChanged & PLAYER_ORIGIN) { + WriteCoord(MSG_ENTITY, origin[0]); + WriteCoord(MSG_ENTITY, origin[1]); + } + /* the height doesn't change as much */ + if (fChanged & PLAYER_ORIGIN_Z) + WriteCoord(MSG_ENTITY, origin[2]); + + if (fChanged & PLAYER_ANGLES_X) { + WriteShort(MSG_ENTITY, v_angle[0] * 32767 / 360); + WriteShort(MSG_ENTITY, v_angle[1] * 32767 / 360); + WriteShort(MSG_ENTITY, v_angle[2] * 32767 / 360); + } + if (fChanged & PLAYER_ANGLES_Y) { + WriteShort(MSG_ENTITY, angles[0] * 32767 / 360); + WriteShort(MSG_ENTITY, angles[1] * 32767 / 360); + WriteShort(MSG_ENTITY, angles[2] * 32767 / 360); + } + if (fChanged & PLAYER_COLORMAP) + WriteByte(MSG_ENTITY, colormap); + + /* similar as with origin, we separate x/y from z */ + if (fChanged & PLAYER_VELOCITY) { + WriteCoord(MSG_ENTITY, velocity[0]); + WriteCoord(MSG_ENTITY, velocity[1]); + } + if (fChanged & PLAYER_VELOCITY_Z) + WriteCoord(MSG_ENTITY, velocity[2]); + + if (fChanged & PLAYER_FLAGS) { + WriteFloat(MSG_ENTITY, flags); + WriteFloat(MSG_ENTITY, gflags); + WriteFloat(MSG_ENTITY, pmove_flags); + } + if (fChanged & PLAYER_WEAPON) { + WriteByte(MSG_ENTITY, activeweapon); + WriteByte(MSG_ENTITY, weaponframe); + } + + /* g_items is a proper integer, so we can't let WriteFloat truncate it (hence __variant) */ + if (fChanged & PLAYER_ITEMS) + WriteFloat(MSG_ENTITY, (__variant)g_items); + + /* only got byte precision, clamp to avoid weird values on the client-side */ + if (fChanged & PLAYER_HEALTH) + WriteByte(MSG_ENTITY, bound(0, health, 255)); + if (fChanged & PLAYER_ARMOR) + WriteByte(MSG_ENTITY, bound(0, armor, 255)); + + if (fChanged & PLAYER_MOVETYPE) { + WriteByte(MSG_ENTITY, movetype); + WriteByte(MSG_ENTITY, solid); + } + + /* the view_ofs[0] and [1] are rarely changed */ + if (fChanged & PLAYER_VIEWOFS) + WriteFloat(MSG_ENTITY, view_ofs[2]); + + /* TO OPTIMISE */ + WriteFloat(MSG_ENTITY, teleport_time); + WriteFloat(MSG_ENTITY, viewzoom); + WriteFloat(MSG_ENTITY, weapontime); + WriteFloat(MSG_ENTITY, w_attack_next); + WriteFloat(MSG_ENTITY, w_idle_next); + WriteFloat(MSG_ENTITY, punchangle[0]); + WriteFloat(MSG_ENTITY, punchangle[1]); + WriteFloat(MSG_ENTITY, punchangle[2]); + + if (vehicle) + WriteEntity(MSG_ENTITY, vehicle); + else + WriteEntity(MSG_ENTITY, __NULL__); + + /* FIXME: Make this fake NSClientSpectator only. */ + WriteByte(MSG_ENTITY, spec_ent); + WriteByte(MSG_ENTITY, spec_mode); + WriteByte(MSG_ENTITY, spec_flags); + + return (1); +} + +/* +==================== +_NSClientPlayer_useworkaround + +A wrapper to cleanly reset 'self' as to not mess up the QC VM +==================== +*/ +void +_NSClientPlayer_useworkaround(entity eTarget) +{ + eActivator = self; + entity eOldSelf = self; + self = eTarget; + self.PlayerUse(); + self = eOldSelf; +} + +/* +==================== +_NSClientPlayer_useworkaround + +A wrapper to cleanly reset 'self' as to not mess up the QC VM +==================== +*/ +void +_NSClientPlayer_unuseworkaround(entity eTarget) +{ + eActivator = self; + entity eOldSelf = self; + self = eTarget; + if (self.PlayerUseUnpressed) + self.PlayerUseUnpressed(); + self = eOldSelf; +} + +/* +================= +NSClientPlayer:: InputUse_Down + +Called when we hold down the +use button for the first time, +looks for an entity that has the .PlayerUse field set to a function and calls it. +================= +*/ +void +NSClientPlayer::InputUse_Down(void) +{ + if (health <= 0) { + return; + } else if (!(flags & FL_USE_RELEASED)) { + return; + } + + vector vecSource; + entity eRad; + bool found_use = false; + + makevectors(v_angle); + vecSource = origin + view_ofs; + traceline(vecSource, vecSource + (v_forward * 64), MOVE_EVERYTHING, this); + + /* first see if we traced something head-on, else we'll findradius something */ + if (trace_ent.PlayerUse) { + found_use = true; + eRad = trace_ent; + } else { + /* find anything in a 8 unit radius, including certain non-solids (func_door, func_rot_button etc. */ + eRad = findradius(trace_endpos, 8); + + /* loop through our chain and just pick the first valid one */ + while (eRad) { + if (eRad.PlayerUse) { + found_use = true; + break; + } + eRad = eRad.chain; + } + } + + /* TODO: maybe eRad will return something in the future that'll suppress a successfull use? */ + if (eRad && found_use == true) { + flags &= ~FL_USE_RELEASED; + _NSClientPlayer_useworkaround(eRad); + last_used = eRad; + + /* Some entities want to support Use spamming */ + if (!(flags & FL_USE_RELEASED)) { + sound(this, CHAN_ITEM, "common/wpn_select.wav", 0.25, ATTN_IDLE); + } + } else { + sound(this, CHAN_ITEM, "common/wpn_denyselect.wav", 0.25, ATTN_IDLE); + flags &= ~FL_USE_RELEASED; + } +} + +/* +================= +NSClientPlayer:: InputUse_Down + +Called when we let go of the +use button +================= +*/ +void +NSClientPlayer::InputUse_Up(void) +{ + if (!(flags & FL_USE_RELEASED)) { + _NSClientPlayer_unuseworkaround(last_used); + last_used = world; + flags |= FL_USE_RELEASED; + } +} +#endif + +void +NSClientPlayer::NSClientPlayer(void) +{ + super::NSClientSpectator(); + vehicle = __NULL__; +} diff --git a/src/shared/NSClientSpectator.h b/src/shared/NSClientSpectator.h new file mode 100644 index 00000000..c9e8f64c --- /dev/null +++ b/src/shared/NSClientSpectator.h @@ -0,0 +1,65 @@ +typedef enumflags +{ + SPECFL_ORIGIN, + SPECFL_VELOCITY, + SPECFL_TARGET, + SPECFL_MODE, + SPECFL_FLAGS +} NSClientSpectatorFlags_t; + +typedef enum +{ + SPECMODE_FREE, + SPECMODE_THIRDPERSON, + SPECMODE_FIRSTPERSON, + SPECMODE_OVERVIEW +} NSClientSpectatorMode_t; + +typedef enumflags +{ + SPECFLAG_BUTTON_RELEASED, +}; + +class NSClientSpectator:NSClient +{ + PREDICTED_FLOAT(spec_ent); + PREDICTED_FLOAT(spec_flags); + NSClientSpectatorMode_t spec_mode; NSClientSpectatorMode_t spec_mode_net; + + vector spec_org; + + int sequence; + + void(void) NSClientSpectator; + + virtual void(void) ClientInput; + + virtual void(void) InputNext; + virtual void(void) InputPrevious; + virtual void(void) InputMode; + + virtual void(void) WarpToTarget; + + virtual void(void) PreFrame; + virtual void(void) PostFrame; + virtual void(void) SpectatorTrackPlayer; + + virtual bool(void) IsFakeSpectator; + virtual bool(void) IsRealSpectator; + virtual bool(void) IsDead; + virtual bool(void) IsPlayer; + +#ifdef SERVER + virtual void(void) EvaluateEntity; + virtual float(entity, float) SendEntity; + virtual void(void) RunClientCommand; +#else + virtual void(void) ClientInputFrame; + virtual void(float,float) ReceiveEntity; + virtual float(void) predraw; +#endif +}; + +#ifdef CLIENT +void Spectator_ReadEntity(float new); +#endif diff --git a/src/shared/NSClientSpectator.qc b/src/shared/NSClientSpectator.qc new file mode 100644 index 00000000..8342d2d8 --- /dev/null +++ b/src/shared/NSClientSpectator.qc @@ -0,0 +1,483 @@ +/* + * Copyright (c) 2016-2021 Marco Cawthorne + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + + +bool +NSClientSpectator::IsRealSpectator(void) +{ + return (true); +} + +bool +NSClientSpectator::IsDead(void) +{ + return (false); +} + +bool +NSClientSpectator::IsPlayer(void) +{ + return (false); +} + +bool +NSClientSpectator::IsFakeSpectator(void) +{ + return (false); +} + +void +NSClientSpectator::ClientInput(void) +{ + if (input_buttons & INPUT_BUTTON0) { + InputNext(); + } else if (input_buttons & INPUT_BUTTON3) { + InputPrevious(); + } else if (input_buttons & INPUT_BUTTON2) { + InputMode(); + } else { + spec_flags &= ~SPECFLAG_BUTTON_RELEASED; + } + + input_buttons = 0; + //crossprint(sprintf("%d %d %d\n", spec_ent, spec_mode, spec_flags)); +} + +void +NSClientSpectator::WarpToTarget(void) +{ + entity b = edict_num(spec_ent); + + setorigin(this, b.origin); +} + +#ifdef SERVER +float +NSClientSpectator::SendEntity(entity ePVSent, float flChangedFlags) +{ + if (this != ePVSent) { + return (0); + } + + if (clienttype(ePVSent) != CLIENTTYPE_REAL) { + return (0); + } + + WriteByte(MSG_ENTITY, ENT_SPECTATOR); + WriteFloat(MSG_ENTITY, flChangedFlags); + + if (flChangedFlags & SPECFL_ORIGIN) { + WriteCoord(MSG_ENTITY, origin[0]); + WriteCoord(MSG_ENTITY, origin[1]); + WriteCoord(MSG_ENTITY, origin[2]); + } + + if (flChangedFlags & SPECFL_VELOCITY) { + WriteFloat(MSG_ENTITY, velocity[0]); + WriteFloat(MSG_ENTITY, velocity[1]); + WriteFloat(MSG_ENTITY, velocity[2]); + } + + if (flChangedFlags & SPECFL_TARGET) + WriteByte(MSG_ENTITY, spec_ent); + + if (flChangedFlags & SPECFL_MODE) + WriteByte(MSG_ENTITY, spec_mode); + + if (flChangedFlags & SPECFL_FLAGS) + WriteByte(MSG_ENTITY, spec_flags); + + return (1); +} + +void +NSClientSpectator::RunClientCommand(void) +{ + runstandardplayerphysics(this); + ClientInput(); +} + +#else +void +NSClientSpectator::ClientInputFrame(void) +{ + /* If we are inside a VGUI, don't let the client do stuff outside */ + if (VGUI_Active()) { + input_impulse = 0; + input_buttons = 0; + return; + } + + /* background maps have no input */ + if (serverkeyfloat("background") == 1) + return; +} + +void +NSClientSpectator::ReceiveEntity(float new, float fl) +{ + if (new == FALSE) { + /* Go through all the physics code between the last received frame + * and the newest frame and keep the changes this time around instead + * of rolling back, because we'll apply the new server-verified values + * right after anyway. */ + /* FIXME: splitscreen */ + if (entnum == player_localentnum) { + /* FIXME: splitscreen */ + pSeat = &g_seats[0]; + + for (int i = sequence+1; i <= servercommandframe; i++) { + /* ...maybe the input state is too old? */ + if (!getinputstate(i)) { + break; + } + input_sequence = i; + runstandardplayerphysics(this); + ClientInput(); + } + + /* any differences in things that are read below are now + * officially from prediction misses. */ + } + } + + /* seed for our prediction table */ + sequence = servercommandframe; + + if (fl & SPECFL_ORIGIN) { + origin[0] = readcoord(); + origin[1] = readcoord(); + origin[2] = readcoord(); + } + + if (fl & SPECFL_VELOCITY) { + velocity[0] = readfloat(); + velocity[1] = readfloat(); + velocity[2] = readfloat(); + } + + if (fl & SPECFL_TARGET) + spec_ent = readbyte(); + + if (fl & SPECFL_MODE) + spec_mode = readbyte(); + + if (fl & SPECFL_FLAGS) + spec_flags = readbyte(); +}; +float +NSClientSpectator::predraw(void) +{ + addentity(this); + return (PREDRAW_NEXT); +} +#endif + +void +NSClientSpectator::InputNext(void) +{ + if (spec_flags & SPECFLAG_BUTTON_RELEASED) + return; + +#if 0 + float max_edict; + + max_edict = serverkeyfloat("sv_playerslots"); + + spec_ent++; + + if (spec_ent > max_edict) + spec_ent = 1; + + print(sprintf("edict: %d\n", spec_ent)); +#else + float max_edict; + float sep = spec_ent; + float best = 0; + NSClient cl; + + max_edict = serverkeyfloat("sv_playerslots"); + + for (float i = 1; i <= max_edict; i++) { + entity f; + + if (i <= sep && best == 0) { + f = edict_num(i); + if (f && f.classname == "player" && f != this) { + cl = (NSClient)f; + if (!cl.IsFakeSpectator()) + best = i; + } + } + + if (i > sep) { + f = edict_num(i); + if (f && f.classname == "player" && f != this) { + cl = (NSClient)f; + if (!cl.IsFakeSpectator()) { + best = i; + break; + } + } + } + } + + if (best == 0) + return; + + spec_ent = best; +#endif + spec_flags |= SPECFLAG_BUTTON_RELEASED; + WarpToTarget(); + + if (spec_mode == SPECMODE_FREE) + spec_mode = SPECMODE_THIRDPERSON; +} + +void +NSClientSpectator::InputPrevious(void) +{ + if (spec_flags & SPECFLAG_BUTTON_RELEASED) + return; +#if 0 + float max_edict; + + max_edict = serverkeyfloat("sv_playerslots"); + + spec_ent--; + + if (spec_ent < 1) + spec_ent = max_edict; +#else + float max_edict; + float sep = spec_ent; + float best = 0; + NSClient cl; + + max_edict = serverkeyfloat("sv_playerslots"); + + for (float i = max_edict; i > 0; i--) { + entity f; + + /* remember the first valid one here */ + if (i >= sep && best == 0) { + f = edict_num(i); + + if (f && f.classname == "player") { + cl = (NSClient)f; + + if (!cl.IsFakeSpectator()) + best = i; + } + } + + /* find the first good one and take it */ + if (i < sep) { + f = edict_num(i); + if (f && f.classname == "player") { + cl = (NSClient)f; + if (!cl.IsFakeSpectator()) { + best = i; + break; + } + } + } + } + + if (best == 0) + return; + + spec_ent = best; +#endif + + spec_flags |= SPECFLAG_BUTTON_RELEASED; + + WarpToTarget(); + + if (spec_mode == SPECMODE_FREE) + spec_mode = SPECMODE_THIRDPERSON; +} + +void +NSClientSpectator::InputMode(void) +{ + if (spec_flags & SPECFLAG_BUTTON_RELEASED) + return; + + NSClient f; +#ifdef CLIENT + f = (NSClient)findfloat(world, ::entnum, spec_ent); +#else + f = (NSClient)edict_num(spec_ent); +#endif + + if (f == this || f.classname != "player") + spec_mode = SPECMODE_FREE; + else { + spec_mode++; + + if (spec_mode > SPECMODE_FIRSTPERSON) + spec_mode = SPECMODE_FREE; + } + + spec_flags |= SPECFLAG_BUTTON_RELEASED; +} + +void +NSClientSpectator::PreFrame(void) +{ +#ifdef CLIENT + /* base player attributes/fields we're going to roll back */ + SAVE_STATE(origin); + SAVE_STATE(velocity); + SAVE_STATE(spec_ent); + SAVE_STATE(spec_mode); + SAVE_STATE(spec_flags); + + /* run physics code for all the input frames which we've not heard back + * from yet. This continues on in Player_ReceiveEntity! */ + for (int i = sequence + 1; i <= clientcommandframe; i++) { + float flSuccess = getinputstate(i); + if (flSuccess == FALSE) { + continue; + } + + if (i==clientcommandframe){ + CSQC_Input_Frame(); + } + + /* don't do partial frames, aka incomplete input packets */ + if (input_timelength == 0) { + break; + } + + /* this global is for our shared random number seed */ + input_sequence = i; + + /* run our custom physics */ + runstandardplayerphysics(this); + ClientInput(); + } +#endif + + SpectatorTrackPlayer(); +} + +void +NSClientSpectator::SpectatorTrackPlayer(void) +{ + if (spec_mode == SPECMODE_THIRDPERSON || spec_mode == SPECMODE_FIRSTPERSON ) { + NSClient b; + + #ifdef CLIENT + b = (NSClient)findfloat(world, ::entnum, spec_ent); + #else + b = (NSClient)edict_num(spec_ent); + #endif + + if (b && b.classname == "player") + if (b.IsFakeSpectator()) { + b = 0; + spec_mode = SPECMODE_FREE; + InputNext(); + } + + /* if the ent is dead... or not available in this current frame + just warp to the last 'good' one */ + if (b) { + setorigin(this, b.origin); + spec_org = b.origin; + } else { + setorigin(this, spec_org); + } + } +} + +#ifdef SERVER +void +NSClientSpectator::EvaluateEntity(void) +{ + /* check for which values have changed in this frame + and announce to network said changes */ + if (origin != origin_net) + SetSendFlags(SPECFL_ORIGIN); + + if (velocity != velocity_net) + SetSendFlags(SPECFL_VELOCITY); + + if (spec_ent != spec_ent_net) + SetSendFlags(SPECFL_TARGET); + + if (spec_mode != spec_mode_net) + SetSendFlags(SPECFL_MODE); + + if (spec_flags != spec_flags_net) + SetSendFlags(SPECFL_FLAGS); + + SAVE_STATE(origin); + SAVE_STATE(velocity); + SAVE_STATE(spec_ent); + SAVE_STATE(spec_mode); + SAVE_STATE(spec_flags); +} +#endif + +void +NSClientSpectator::PostFrame(void) +{ +#ifdef CLIENT + ROLL_BACK(origin); + ROLL_BACK(velocity); + ROLL_BACK(spec_ent); + ROLL_BACK(spec_mode); + ROLL_BACK(spec_flags); +#endif +} + +void +NSClientSpectator::NSClientSpectator(void) +{ + super::NSClient(); + modelindex = 0; + flags = FL_CLIENT; + SetSolid(SOLID_NOT); + SetMovetype(MOVETYPE_NOCLIP); + think = __NULL__; + nextthink = 0.0f; + maxspeed = 250; + spec_ent = 0; + spec_mode = 0; +} + +#ifdef CLIENT +void +Spectator_ReadEntity(float new) +{ + NSClientSpectator spec = (NSClientSpectator)self; + + if (new || self.classname != "spectator") { + spawnfunc_NSClientSpectator(); + spec.classname = "spectator"; + spec.solid = SOLID_NOT; + spec.drawmask = MASK_ENGINE; + spec.customphysics = Empty; + setsize(spec, [0,0,0], [0,0,0]); + } + + float flags = readfloat(); + spec.ReceiveEntity(new, flags); +} +#endif