Basic spectator implementation for all games.

This commit is contained in:
Marco Cawthorne 2021-03-24 07:50:30 +01:00
parent f918e9ddb9
commit 95739c7a20
13 changed files with 642 additions and 64 deletions

View File

@ -33,6 +33,7 @@ Bot_AddQuick(void)
{
/* we've got no nodes, so generate some fake waypoints */
if (!g_nodes_present) {
print("^1Bot_AddQuick^7: Can't add bot. No waypoints.\n");
return world;
}

View File

@ -31,6 +31,8 @@ Chat_Draw(void)
{
int i;
drawfont = FONT_CON;
/* the voting stuff resides here too, for now */
if (serverkey("vote_cmd")) {
string tempstr;

View File

@ -62,6 +62,7 @@ var int MUZZLE_RIFLE;
var int MUZZLE_WEIRD;
var int SHELL_DEFAULT;
var int SHELL_SHOTGUN;
/* misc globals */
vector video_mins;

View File

@ -140,6 +140,7 @@ void
CSQC_UpdateView(float w, float h, float focus)
{
player pl;
spectator spec;
int s;
if (w == 0 || h == 0) {
@ -210,50 +211,58 @@ CSQC_UpdateView(float w, float h, float focus)
continue;
}
pl = (player)self;
if (self.classname == "player") {
pl = (player)self;
Predict_PlayerPreFrame(pl);
Predict_PreFrame((player)self);
pSeat->m_vecPredictedOrigin = pl.origin;
pSeat->m_vecPredictedVelocity = pl.velocity;
pSeat->m_flPredictedFlags = pl.flags;
pSeat->m_vecPredictedOrigin = pl.origin;
pSeat->m_vecPredictedVelocity = pl.velocity;
pSeat->m_flPredictedFlags = pl.flags;
/* Don't hide the player entity */
if (autocvar_cl_thirdperson == TRUE && pl.health) {
setproperty(VF_VIEWENTITY, (float)0);
} else {
setproperty(VF_VIEWENTITY, (float)player_localentnum);
}
float oldzoom = pl.viewzoom;
if (pl.viewzoom == 1.0f) {
pl.viewzoom = 1.0 - (0.5 * pSeat->m_flZoomTime);
/* +zoomin requested by Slacer */
if (pSeat->m_iZoomed) {
pSeat->m_flZoomTime += clframetime * 15;
/* Don't hide the player entity */
if (autocvar_cl_thirdperson == TRUE && pl.health) {
setproperty(VF_VIEWENTITY, (float)0);
} else {
pSeat->m_flZoomTime -= clframetime * 15;
setproperty(VF_VIEWENTITY, (float)player_localentnum);
}
pSeat->m_flZoomTime = bound(0, pSeat->m_flZoomTime, 1);
float oldzoom = pl.viewzoom;
if (pl.viewzoom == 1.0f) {
pl.viewzoom = 1.0 - (0.5 * pSeat->m_flZoomTime);
/* +zoomin requested by Slacer */
if (pSeat->m_iZoomed) {
pSeat->m_flZoomTime += clframetime * 15;
} else {
pSeat->m_flZoomTime -= clframetime * 15;
}
pSeat->m_flZoomTime = bound(0, pSeat->m_flZoomTime, 1);
}
setproperty(VF_AFOV, cvar("fov") * pl.viewzoom);
if (autocvar_zoom_sensitivity && pl.viewzoom < 1.0f) {
setsensitivityscaler(pl.viewzoom * autocvar_zoom_sensitivity);
} else {
setsensitivityscaler(pl.viewzoom);
}
if (pl.viewzoom <= 0.0f) {
setsensitivityscaler(1.0f);
}
pl.viewzoom = oldzoom;
View_PreDraw();
} else if (self.classname == "spectator") {
spec = (spectator)self;
Predict_SpectatorPreFrame(spec);
pSeat->m_vecPredictedOrigin = spec.origin;
pSeat->m_vecPredictedVelocity = spec.velocity;
pSeat->m_flPredictedFlags = spec.flags;
}
setproperty(VF_AFOV, cvar("fov") * pl.viewzoom);
if (autocvar_zoom_sensitivity && pl.viewzoom < 1.0f) {
setsensitivityscaler(pl.viewzoom * autocvar_zoom_sensitivity);
} else {
setsensitivityscaler(pl.viewzoom);
}
if (pl.viewzoom <= 0.0f) {
setsensitivityscaler(1.0f);
}
pl.viewzoom = oldzoom;
View_PreDraw();
if (pSeat->m_pWeaponFX) {
CBaseFX p = (CBaseFX)pSeat->m_pWeaponFX;
p.Draw();
@ -267,18 +276,58 @@ CSQC_UpdateView(float w, float h, float focus)
setproperty(VF_CL_VIEWANGLES, view_angles);
setproperty(VF_ANGLES, view_angles);
} else {
if (pl.health) {
if (autocvar_cl_thirdperson == TRUE) {
makevectors(view_angles);
vector vStart = [pSeat->m_vecPredictedOrigin[0], pSeat->m_vecPredictedOrigin[1], pSeat->m_vecPredictedOrigin[2] + 16] + (v_right * 4);
vector vEnd = vStart + (v_forward * -48) + [0,0,16] + (v_right * 4);
traceline(vStart, vEnd, FALSE, self);
setproperty(VF_ORIGIN, trace_endpos + (v_forward * 5));
if (self.classname == "player") {
if (pl.health) {
if (autocvar_cl_thirdperson == TRUE) {
makevectors(view_angles);
vector vStart = [pSeat->m_vecPredictedOrigin[0], pSeat->m_vecPredictedOrigin[1], pSeat->m_vecPredictedOrigin[2] + 16] + (v_right * 4);
vector vEnd = vStart + (v_forward * -48) + [0,0,16] + (v_right * 4);
traceline(vStart, vEnd, FALSE, self);
setproperty(VF_ORIGIN, trace_endpos + (v_forward * 5));
} else {
setproperty(VF_ORIGIN, pSeat->m_vecPredictedOrigin + pl.view_ofs);
}
} else {
setproperty(VF_ORIGIN, pSeat->m_vecPredictedOrigin + pl.view_ofs);
setproperty(VF_ORIGIN, pSeat->m_vecPredictedOrigin);
}
if (pSeat->m_flShakeDuration > 0.0) {
vector vecShake = [0,0,0];
vecShake[0] += random() * 3;
vecShake[1] += random() * 3;
vecShake[2] += random() * 3;
pl.punchangle += (vecShake * pSeat->m_flShakeAmp) * (pSeat->m_flShakeDuration / pSeat->m_flShakeTime);
pSeat->m_flShakeDuration -= clframetime;
}
setproperty(VF_ANGLES, view_angles + pl.punchangle);
} else if (self.classname == "spectator") {
switch (spec.spec_mode) {
case SPECMODE_THIRDPERSON:
makevectors(view_angles);
vector vecStart;
vecStart[0] = pSeat->m_vecPredictedOrigin[0];
vecStart[1] = pSeat->m_vecPredictedOrigin[1];
vecStart[2] = pSeat->m_vecPredictedOrigin[2] + 16;
vecStart += (v_right * 4);
vector vecEnd = vecStart + (v_forward * -48) + [0,0,16] + (v_right * 4);
traceline(vecStart, vecEnd, FALSE, self);
setproperty(VF_ORIGIN, trace_endpos + (v_forward * 5));
break;
case SPECMODE_FIRSTPERSON:
entity b;
b = findfloat(world, ::entnum, spec.spec_ent);
if (b.classname == "player") {
player bp = (player)b;
setproperty(VF_ORIGIN, pSeat->m_vecPredictedOrigin + bp.view_ofs);
setproperty(VF_ANGLES, bp.v_angle);
setproperty(VF_CL_VIEWANGLES, [bp.pitch, bp.angles[1], bp.angles[2]]);
}
break;
default:
setproperty(VF_ORIGIN, pSeat->m_vecPredictedOrigin);
}
} else {
setproperty(VF_ORIGIN, pSeat->m_vecPredictedOrigin);
}
if (g_iIntermission) {
@ -287,16 +336,6 @@ CSQC_UpdateView(float w, float h, float focus)
setproperty(VF_ORIGIN, pSeat->m_vecCameraOrigin);
setproperty(VF_CL_VIEWANGLES, view_angles);
}
if (pSeat->m_flShakeDuration > 0.0) {
vector vecShake = [0,0,0];
vecShake[0] += random() * 3;
vecShake[1] += random() * 3;
vecShake[2] += random() * 3;
pl.punchangle += (vecShake * pSeat->m_flShakeAmp) * (pSeat->m_flShakeDuration / pSeat->m_flShakeTime);
pSeat->m_flShakeDuration -= clframetime;
}
setproperty(VF_ANGLES, view_angles + pl.punchangle);
}
setproperty(VF_DRAWWORLD, 1);
@ -335,9 +374,9 @@ CSQC_UpdateView(float w, float h, float focus)
GameText_Draw();
PointMessage_Draw();
if (getplayerkeyvalue(player_localnum, "*spec") != "0") {
if (self.classname == "spectator") {
HUD_DrawSpectator();
} else {
} else if (self.classname == "player") {
HUD_Draw();
}
@ -354,7 +393,10 @@ CSQC_UpdateView(float w, float h, float focus)
}
}
Predict_PostFrame((player)self);
if (self.classname == "player")
Predict_PlayerPostFrame((player)self);
else if (self.classname == "spectator")
Predict_SpectatorPostFrame((spectator)self);
}
DSP_UpdateListener();
@ -588,10 +630,13 @@ CSQC_Parse_Event(void)
setproperty(VF_ANGLES, a);
break;
case EV_SHAKE:
if (self.classname == "spectator")
break;
pSeat->m_flShakeDuration = readfloat();
pSeat->m_flShakeAmp = readfloat();
pSeat->m_flShakeFreq = readfloat();
pSeat->m_flShakeTime = pSeat->m_flShakeDuration;
break;
default:
ClientGame_EventParse(fHeader);
}
@ -833,7 +878,7 @@ CSQC_Ent_Update(float new)
break;
case ENT_PLAYER:
player pl = (player)self;
if (new) {
if (new || self.classname != "player") {
spawnfunc_player();
pl.classname = "player";
pl.solid = SOLID_SLIDEBOX;
@ -843,6 +888,18 @@ CSQC_Ent_Update(float new)
}
pl.ReceiveEntity(new);
break;
case ENT_SPECTATOR:
spectator spec = (spectator)self;
if (new || self.classname != "spectator") {
spawnfunc_spectator();
spec.classname = "spectator";
spec.solid = SOLID_SLIDEBOX;
spec.drawmask = MASK_ENGINE;
spec.customphysics = Empty;
setsize(spec, [0,0,0], [0,0,0]);
}
spec.ReceiveEntity(new);
break;
case ENT_SPRITE:
env_sprite spr = (env_sprite)self;
if (new) {

View File

@ -24,7 +24,7 @@ Propagate our pmove state to whatever the current frame before its stomped on
=================
*/
void
Predict_PreFrame(player pl)
Predict_PlayerPreFrame(player pl)
{
/* base player attributes/fields we're going to roll back */
pl.net_origin = pl.origin;
@ -78,7 +78,7 @@ Rewind our pmove state back to before we started predicting.
=================
*/
void
Predict_PostFrame(player pl)
Predict_PlayerPostFrame(player pl)
{
/* finally roll the values back */
pl.origin = pl.net_origin;
@ -99,3 +99,33 @@ Predict_PostFrame(player pl)
/* update bounds */
setorigin(pl, pl.origin);
}
/*
=================
Predict_PreFrame
We're part way through parsing new player data.
Propagate our pmove state to whatever the current frame before its stomped on
(so any non-networked state updates locally).
=================
*/
void
Predict_SpectatorPreFrame(spectator pl)
{
pl.PreFrame();
}
/*
=================
Predict_SpectatorPostFrame
We're part way through parsing new player data.
Rewind our pmove state back to before we started predicting.
(to give consistent state instead of accumulating errors)
=================
*/
void
Predict_SpectatorPostFrame(spectator pl)
{
pl.PostFrame();
}

View File

@ -40,6 +40,7 @@ View_Init(void)
MUZZLE_SMALL = (int)getmodelindex("sprites/muzzleflash2.spr");
MUZZLE_WEIRD = (int)getmodelindex("sprites/muzzleflash3.spr");
SHELL_DEFAULT = (int)getmodelindex("models/shell.mdl");
SHELL_SHOTGUN = (int)getmodelindex("models/shotgunshell.mdl");
}
void

View File

@ -123,6 +123,13 @@ void
SpectatorThink(void)
{
Game_SpectatorThink();
if (self.classname == "spectator") {
spectator spec = (spectator)self;
spec.PreFrame();
spec.PostFrame();
return;
}
}
/*
@ -137,6 +144,7 @@ void
SpectatorConnect(void)
{
Game_SpectatorConnect();
spawnfunc_spectator();
}
/*
@ -194,6 +202,12 @@ times the amount of players in a given game.
void
PlayerPreThink(void)
{
if (self.classname == "spectator") {
//spectator spec = (spectator)self;
//spec.PreFrame();
return;
}
if (self.classname != "player") {
return;
}
@ -220,6 +234,11 @@ times the amount of players in a given game.
void
PlayerPostThink(void)
{
if (self.classname == "spectator") {
SpectatorThink();
return;
}
if (self.classname != "player") {
return;
}
@ -289,6 +308,11 @@ with the input_X globals being set to the appropriate data.
void
SV_RunClientCommand(void)
{
if (self.classname == "spectator") {
spectator spec = (spectator)self;
spec.RunClientCommand();
}
if (self.classname != "player") {
return;
}
@ -325,6 +349,23 @@ SV_ParseClientCommand(string cmd)
Game_ParseClientCommand(cmd);
else
Game_ParseClientCommand(newcmd);
tokenize(cmd);
switch (argv(0)) {
case "spectate":
if (self.classname != "player")
break;
ClientKill();
spawnfunc_spectator();
break;
case "play":
if (self.classname != "spectator")
break;
spawnfunc_player();
PutClientInServer();
break;
}
}
/*
@ -542,6 +583,16 @@ ConsoleCmd(string cmd)
}
}
}
if (!self) {
for ( other = world; ( other = find( other, classname, "spectator" ) ); ) {
if ( clienttype( other ) == CLIENTTYPE_REAL ) {
self = other;
break;
}
}
}
pl = (player)self;
/* give the game-mode a chance to override us */

View File

@ -27,6 +27,7 @@
#include "sound.h"
#include "pmove.h"
#include "memory.h"
#include "spectator.h"
#define BSPVER_PREREL 28
#define BSPVER_Q1 29

View File

@ -20,6 +20,7 @@ enum
ENT_NONE,
ENT_ENTITY,
ENT_PLAYER,
ENT_SPECTATOR,
ENT_AMBIENTSOUND,
ENT_DLIGHT,
ENT_PROJECTEDTEXTURE,

View File

@ -1,4 +1,5 @@
#includelist
spectator.qc
pmove.qc
sound.qc
math.qc

View File

@ -30,6 +30,8 @@ class base_player
vector view_ofs;
float weapontime;
vector v_angle;
/* any mods that use hooks */
entity hook;

52
src/shared/spectator.h Normal file
View File

@ -0,0 +1,52 @@
enumflags
{
SPECFL_ORIGIN,
SPECFL_VELOCITY,
SPECFL_TARGET,
SPECFL_MODE,
SPECFL_FLAGS
};
enum
{
SPECMODE_FREE,
SPECMODE_THIRDPERSON,
SPECMODE_FIRSTPERSON,
SPECMODE_OVERVIEW
};
#ifdef SERVER
class spectator:CBaseEntity
#else
class spectator
#endif
{
vector origin_net;
vector velocity_net;
float spec_ent; float spec_ent_net;
float spec_mode; float spec_mode_net;
float spec_flags; float spec_flags_net;
int sequence;
void(void) spectator;
virtual void(void) playernext;
virtual void(void) playerprevious;
virtual void(void) modeswitch;
virtual void(void) PreFrame;
virtual void(void) PostFrame;
virtual void(void) Input;
virtual void(void) WarpToTarget;
#ifdef SERVER
virtual float(entity, float) SendEntity;
virtual void(void) RunClientCommand;
#else
virtual void(float) ReceiveEntity;
virtual float(void) predraw;
#endif
};

378
src/shared/spectator.qc Normal file
View File

@ -0,0 +1,378 @@
/*
* Copyright (c) 2016-2021 Marco Hladik <marco@icculus.org>
*
* 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.
*/
void
spectator::WarpToTarget(void)
{
entity b = edict_num(spec_ent);
setorigin(this, b.origin);
}
void
spectator::Input(void)
{
if (input_buttons & INPUT_BUTTON0) {
playernext();
} else if (input_buttons & INPUT_BUTTON3) {
playerprevious();
} else if (input_buttons & INPUT_BUTTON2) {
if (spec_flags & GF_SEMI_TOGGLED)
return;
spec_mode++;
if (spec_mode > SPECMODE_FIRSTPERSON)
spec_mode = SPECMODE_FREE;
spec_flags |= GF_SEMI_TOGGLED;
} else {
spec_flags &= ~GF_SEMI_TOGGLED;
}
input_buttons = 0;
}
#ifdef SERVER
float
spectator::SendEntity(entity ePVSent, float flChangedFlags)
{
if (this != ePVSent) {
return FALSE;
}
if (clienttype(ePVSent) != CLIENTTYPE_REAL) {
return FALSE;
}
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 TRUE;
}
void
spectator::RunClientCommand(void)
{
runstandardplayerphysics(this);
Input();
}
#else
void
spectator::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);
Input();
}
/* any differences in things that are read below are now
* officially from prediction misses. */
}
}
/* seed for our prediction table */
sequence = servercommandframe;
fl = readfloat();
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
spectator::predraw(void)
{
addentity(this);
return PREDRAW_NEXT;
}
#endif
void
spectator::playernext(void)
{
if (spec_flags & GF_SEMI_TOGGLED)
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;
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") {
best = i;
}
}
if (i > sep) {
f = edict_num(i);
if (f && f.classname == "player") {
best = i;
break;
}
}
}
if (best == 0)
return;
spec_ent = best;
#endif
spec_flags |= GF_SEMI_TOGGLED;
WarpToTarget();
if (spec_mode == SPECMODE_FREE)
spec_mode = SPECMODE_THIRDPERSON;
}
void
spectator::playerprevious(void)
{
if (spec_flags & GF_SEMI_TOGGLED)
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;
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") {
best = i;
}
}
/* find the first good one and take it */
if (i < sep) {
f = edict_num(i);
if (f && f.classname == "player") {
best = i;
break;
}
}
}
if (best == 0)
return;
spec_ent = best;
#endif
print(sprintf("dddd: %d\n", spec_ent));
spec_flags |= GF_SEMI_TOGGLED;
WarpToTarget();
if (spec_mode == SPECMODE_FREE)
spec_mode = SPECMODE_THIRDPERSON;
}
void
spectator::modeswitch(void)
{
}
void
spectator::PreFrame(void)
{
#ifdef SERVER
#else
/* base player attributes/fields we're going to roll back */
origin_net = origin;
velocity_net = velocity;
spec_ent_net = spec_ent;
spec_mode_net = spec_mode;
spec_flags_net = 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);
Input();
}
#endif
if (spec_mode == SPECMODE_THIRDPERSON || spec_mode == SPECMODE_FIRSTPERSON ) {
entity b;
#ifdef CLIENT
b = findfloat(world, ::entnum, spec_ent);
#else
b = edict_num(spec_ent);
#endif
setorigin(this, b.origin);
}
}
void
spectator::PostFrame(void)
{
#ifdef SERVER
/* check for which values have changed in this frame
and announce to network said changes */
if (origin != origin_net)
SendFlags |= SPECFL_ORIGIN;
if (velocity != velocity_net)
SendFlags |= SPECFL_VELOCITY;
if (spec_ent != spec_ent_net)
SendFlags |= SPECFL_TARGET;
if (spec_mode != spec_mode_net)
SendFlags |= SPECFL_MODE;
if (spec_flags != spec_flags_net)
SendFlags |= SPECFL_FLAGS;
origin_net = origin;
velocity_net = velocity;
spec_ent_net = spec_ent;
spec_mode_net = spec_mode;
spec_flags_net = spec_flags;
#else
/* finally roll the values back */
origin = origin_net;
velocity = velocity_net;
spec_ent = spec_ent_net;
spec_mode = spec_mode_net;
spec_flags = spec_flags_net;
setorigin(this, origin);
#endif
}
void
spectator::spectator(void)
{
modelindex = 0;
flags = 0;
solid = SOLID_NOT;
movetype = MOVETYPE_NOCLIP;
think = __NULL__;
nextthink = 0.0f;
maxspeed = 250;
#ifdef SERVER
forceinfokey(this, "*spec", "1");
#endif
}