From ef39a73480a0a3588dcf096fd7d5bf6af9a7e185 Mon Sep 17 00:00:00 2001 From: g-cont Date: Fri, 2 Mar 2012 00:00:00 +0400 Subject: [PATCH] 02 Mar 2012 --- change.log | 2 + engine/client/cl_demo.c | 1058 ++++++++++++++++++++++++----------- engine/client/cl_frame.c | 28 +- engine/client/cl_game.c | 2 +- engine/client/cl_main.c | 105 ++-- engine/client/cl_parse.c | 21 +- engine/client/cl_pmove.c | 8 +- engine/client/client.h | 13 +- engine/client/gl_decals.c | 17 +- engine/client/gl_rlight.c | 5 - engine/client/gl_rsurf.c | 3 +- engine/client/gl_studio.c | 12 +- engine/client/s_vox.c | 2 +- engine/common/cmd.c | 2 +- engine/common/common.h | 4 + engine/common/filesystem.c | 26 +- engine/common/host.c | 38 +- engine/common/net_chan.c | 14 +- engine/common/net_encode.c | 32 +- engine/common/pm_studio.c | 934 +++++++++++++++++++++++++++++++ engine/common/sys_win.c | 7 +- engine/server/sv_game.c | 9 +- engine/server/sv_save.c | 26 +- engine/server/sv_studio.c | 1063 ++++++++++++++++++++++++++++++++++++ game_launch/game.dsp | 4 +- game_launch/game.ncb | Bin 0 -> 33792 bytes game_launch/game.opt | Bin 0 -> 52736 bytes game_launch/hl.exe | Bin 6144 -> 6144 bytes 28 files changed, 2969 insertions(+), 466 deletions(-) create mode 100644 engine/common/pm_studio.c create mode 100644 engine/server/sv_studio.c create mode 100644 game_launch/game.ncb create mode 100644 game_launch/game.opt diff --git a/change.log b/change.log index 80ca7fd5..8d088411 100644 --- a/change.log +++ b/change.log @@ -24,6 +24,8 @@ Server: clear savedir when new game is started GameUI: loading maps.lst from basedir while mod doesn't contain multiplayer maps Network: implemented QueryCvarValue and QueryCvarValue2 Physic: new pm-trace code, new server trace code, new studio hitbox trace code +Client: rewrite demo record and playback +Render: add support for STUDIO_NF_FULLBRIGHT build 1770 diff --git a/engine/client/cl_demo.c b/engine/client/cl_demo.c index 7cdc5ffa..b60647b8 100644 --- a/engine/client/cl_demo.c +++ b/engine/client/cl_demo.c @@ -17,40 +17,210 @@ GNU General Public License for more details. #include "client.h" #include "net_encode.h" -#define dem_cmd 0 -#define dem_read 1 -#define dem_set 2 +#define dem_norewind 1 // startup message +#define dem_read 2 // it's a normal network packet +#define dem_jumptime 3 // move the demostart time value forward by this amount +#define dem_userdata 4 // userdata from the client.dll +#define dem_usercmd 5 // read usercmd_t +#define dem_stop 6 // end of time +#define dem_lastcmd dem_stop + +#define DEMO_STARTUP 0 // this lump contains startup info needed to spawn into the server +#define DEMO_NORMAL 1 // this lump contains playback info of messages, etc., needed during playback. + +#define IDEMOHEADER (('M'<<24)+('E'<<16)+('D'<<8)+'I') // little-endian "IDEM" +#define DEMO_PROTOCOL 1 + +const char *demo_cmd[dem_lastcmd+1] = +{ + "dem_unknown", + "dem_norewind", + "dem_read", + "dem_jumptime", + "dem_userdata", + "dem_usercmd", + "dem_stop", + +}; + +typedef struct +{ + int id; // should be IDEM + int dem_protocol; // should be DEMO_PROTOCOL + int net_protocol; // should be PROTOCOL_VERSION + char mapname[64]; // name of map + char gamedir[64]; // name of game directory (FS_Gamedir()) + int directory_offset; // offset of Entry Directory. +} demoheader_t; + +typedef struct +{ + int entrytype; // DEMO_STARTUP or DEMO_NORMAL + float playback_time; // time of track + int playback_frames; // # of frames in track + int offset; // file offset of track data + int length; // length of track +} demoentry_t; + +typedef struct +{ + demoentry_t *entries; // track entry info + int numentries; // number of tracks +} demodirectory_t; + +// private demo states +struct +{ + demoheader_t header; + demoentry_t *entry; + demodirectory_t directory; + int framecount; + float starttime; + int entryIndex; +} demo; /* ==================== -CL_WriteDemoCmd +CL_StartupDemoHeader + +spooling demo header in case +we record a demo on this level +==================== +*/ +void CL_StartupDemoHeader( void ) +{ + if( cls.demoheader ) + { + FS_Close( cls.demoheader ); + } + + // Note: this is replacing tmpfile() + cls.demoheader = FS_Open( "demoheader.tmp", "w+b", true ); + + if( !cls.demoheader ) + { + MsgDev( D_ERROR, "couldn't open temporary header file.\n" ); + return; + } + + MsgDev( D_INFO, "Spooling demo header.\n" ); +} + +/* +==================== +CL_CloseDemoHeader + +close demoheader file on engine shutdown +==================== +*/ +void CL_CloseDemoHeader( void ) +{ + if( !cls.demoheader ) + return; + + FS_Close( cls.demoheader ); +} + +float CL_GetDemoRecordClock( void ) +{ + return cl.mtime[0]; +} + +float CL_GetDemoPlaybackClock( void ) +{ + return host.realtime + host.frametime; +} + +/* +==================== +CL_WriteDemoCmdHeader + +Writes the demo command header and time-delta +==================== +*/ +void CL_WriteDemoCmdHeader( byte cmd, file_t *file ) +{ + float dt; + + ASSERT( cmd >= 1 && cmd <= dem_lastcmd ); + if( !file ) return; + + // command + FS_Write( file, &cmd, sizeof( byte )); + + // time offset + dt = (float)(CL_GetDemoRecordClock() - demo.starttime); + FS_Write( file, &dt, sizeof( float )); +} + +/* +==================== +CL_WriteDemoJumpTime + +Update level time on a next level +==================== +*/ +void CL_WriteDemoJumpTime( void ) +{ + if( cls.demowaiting || !cls.demofile ) return; + + demo.starttime = CL_GetDemoRecordClock(); // setup the demo starttime + + // demo playback should read this as an incoming message. + // write the client's realtime value out so we can synchronize the reads. + CL_WriteDemoCmdHeader( dem_jumptime, cls.demofile ); +} + +/* +==================== +CL_WriteDemoUserCmd Writes the current user cmd ==================== */ -void CL_WriteDemoCmd( usercmd_t *pcmd ) +void CL_WriteDemoUserCmd( int cmdnumber ) { - int i; - float fl; - usercmd_t cmd; - byte c; + sizebuf_t buf; + word bytes; + byte data[1024]; - fl = (float)host.realtime; - FS_Write( cls.demofile, &fl, sizeof( fl )); + if( !cls.demorecording || !cls.demofile ) + return; - c = dem_cmd; - FS_Write( cls.demofile, &c, sizeof( c )); + CL_WriteDemoCmdHeader( dem_usercmd, cls.demofile ); - // correct for byte order, bytes don't matter - cmd = *pcmd; + FS_Write( cls.demofile, &cls.netchan.outgoing_sequence, sizeof( int )); + FS_Write( cls.demofile, &cmdnumber, sizeof( int )); - FS_Write( cls.demofile, &cmd, sizeof( cmd )); + // write usercmd_t + BF_Init( &buf, "UserCmd", data, sizeof( data )); + CL_WriteUsercmd( &buf, -1, cmdnumber ); // always no delta - for( i = 0; i < 3; i++ ) - { - fl = cl.refdef.cl_viewangles[i]; - FS_Write( cls.demofile, &fl, sizeof( fl )); - } + bytes = BF_GetNumBytesWritten( &buf ); + + FS_Write( cls.demofile, &bytes, sizeof( word )); + FS_Write( cls.demofile, data, bytes ); +} + +/* +==================== +CL_WriteDemoSequence + +Save state of cls.netchan sequences +so that we can play the demo correctly. +==================== +*/ +void CL_WriteDemoSequence( file_t *file ) +{ + ASSERT( file ); + + FS_Write( file, &cls.netchan.incoming_sequence, sizeof( int )); + FS_Write( file, &cls.netchan.incoming_acknowledged, sizeof( int )); + FS_Write( file, &cls.netchan.incoming_reliable_acknowledged, sizeof( int )); + FS_Write( file, &cls.netchan.incoming_reliable_sequence, sizeof( int )); + FS_Write( file, &cls.netchan.outgoing_sequence, sizeof( int )); + FS_Write( file, &cls.netchan.reliable_sequence, sizeof( int )); + FS_Write( file, &cls.netchan.last_reliable_sequence, sizeof( int )); } /* @@ -60,247 +230,199 @@ CL_WriteDemoMessage Dumps the current net message, prefixed by the length ==================== */ -void CL_WriteDemoMessage( sizebuf_t *msg, int head_size ) +void CL_WriteDemoMessage( qboolean startup, int start, sizebuf_t *msg ) { + byte c; int swlen; + file_t *file = startup ? cls.demoheader : cls.demofile; - if( !cls.demofile ) return; - if( cl.refdef.paused || cls.key_dest != key_game ) + if( !file ) return; + + // past the start but not recording a demo. + if( !startup && !cls.demorecording ) return; - // the first eight bytes are just packet sequencing stuff - swlen = BF_GetNumBytesWritten( msg ) - head_size; + swlen = BF_GetNumBytesWritten( msg ) - start; + if( swlen <= 0 ) return; - if( !swlen ) return; // ignore null messages - FS_Write( cls.demofile, &swlen, 4 ); - FS_Write( cls.demofile, BF_GetData( msg ) + head_size, swlen ); + if( !startup ) + { + demo.framecount++; + } + + // demo playback should read this as an incoming message. + c = (cls.state != ca_active) ? dem_norewind : dem_read; + + CL_WriteDemoCmdHeader( c, file ); + CL_WriteDemoSequence( file ); + + // write the length out. + FS_Write( file, &swlen, sizeof( int )); + + // output the buffer. Skip the network packet stuff. + FS_Write( file, BF_GetData( msg ) + start, swlen ); } +/* +==================== +CL_WriteDemoUserMessage + +Dumps the user message (demoaction) +==================== +*/ +void CL_WriteDemoUserMessage( const byte *buffer, size_t size ) +{ + if( !cls.demorecording || cls.demowaiting ) + return; + + if( !cls.demofile || !buffer || size <= 0 ) + return; + + CL_WriteDemoCmdHeader( dem_userdata, cls.demofile ); + + // write the length out. + FS_Write( cls.demofile, &size, sizeof( int )); + + // output the buffer. + FS_Write( cls.demofile, buffer, size ); +} + +/* +==================== +CL_WriteDemoHeader + +Write demo header +==================== +*/ void CL_WriteDemoHeader( const char *name ) { - int i, j, len; - char buf_data[NET_MAX_PAYLOAD]; - entity_state_t *state, nullstate; - movevars_t nullmovevars; - sizebuf_t buf; - delta_info_t *dt; - + fs_offset_t copysize; + fs_offset_t savepos; + fs_offset_t curpos; + MsgDev( D_INFO, "recording to %s.\n", name ); cls.demofile = FS_Open( name, "wb", false ); + if( !cls.demofile ) { - MsgDev(D_ERROR, "CL_Record: unable to create %s\n", name ); + MsgDev( D_ERROR, "couldn't open %s.\n", name ); return; } cls.demorecording = true; + cls.demowaiting = true; // don't start saving messages until a non-delta compressed message is received - // don't start saving messages until a non-delta compressed message is received - cls.demowaiting = true; + Q_memset( &demo.header, 0, sizeof( demo.header )); - // write out messages to hold the startup information - BF_Init( &buf, "DemoWrite", buf_data, sizeof( buf_data )); + demo.header.id = IDEMOHEADER; + demo.header.dem_protocol = DEMO_PROTOCOL; + demo.header.net_protocol = PROTOCOL_VERSION; + Q_strncpy( demo.header.mapname, clgame.mapname, sizeof( demo.header.mapname )); + Q_strncpy( demo.header.gamedir, FS_Gamedir(), sizeof( demo.header.gamedir )); - // send the serverdata - BF_WriteByte( &buf, svc_serverdata ); - BF_WriteLong( &buf, PROTOCOL_VERSION ); - BF_WriteLong( &buf, cl.servercount ); - BF_WriteLong( &buf, cl.checksum ); - BF_WriteByte( &buf, cl.playernum|( cl.spectator ? 128 : 0 )); - BF_WriteByte( &buf, cl.maxclients ); - BF_WriteWord( &buf, clgame.maxEntities ); - BF_WriteString( &buf, clgame.mapname ); - BF_WriteString( &buf, clgame.maptitle ); - BF_WriteOneBit( &buf, cl.background ); - BF_WriteString( &buf, GI->gamefolder ); + // write header + FS_Write( cls.demofile, &demo.header, sizeof( demo.header )); - // write modellist - for( i = 0; i < MAX_MODELS; i++ ) - { - if( cl.model_precache[i][0] ) - { - BF_WriteByte( &buf, svc_modelindex ); - BF_WriteUBitLong( &buf, i, MAX_MODEL_BITS ); - BF_WriteString( &buf, cl.model_precache[i] ); + demo.directory.numentries = 2; + demo.directory.entries = Mem_Alloc( cls.mempool, sizeof( demoentry_t ) * demo.directory.numentries ); - if( BF_GetNumBytesWritten( &buf ) > ( BF_GetMaxBytes( &buf ) / 2 )) - { - // write it out - len = BF_GetNumBytesWritten( &buf ); - FS_Write( cls.demofile, &len, 4 ); - FS_Write( cls.demofile, BF_GetData( &buf ), len ); - BF_Clear( &buf ); - } - } - } + // DIRECTORY ENTRY # 0 + demo.entry = &demo.directory.entries[0]; // only one here. + demo.entry->entrytype = DEMO_STARTUP; + demo.entry->playback_time = 0.0f; // startup takes 0 time. + demo.entry->offset = FS_Tell( cls.demofile ); // position for this chunk. - // write soundlist - for( i = 0; i < MAX_SOUNDS; i++ ) - { - if( cl.sound_precache[i][0] ) - { - BF_WriteByte( &buf, svc_soundindex ); - BF_WriteUBitLong( &buf, i, MAX_SOUND_BITS ); - BF_WriteString( &buf, cl.sound_precache[i] ); + // finish off the startup info. + CL_WriteDemoCmdHeader( dem_stop, cls.demoheader ); - if( BF_GetNumBytesWritten( &buf ) > ( BF_GetMaxBytes( &buf ) / 2 )) - { - // write it out - len = BF_GetNumBytesWritten( &buf ); - FS_Write( cls.demofile, &len, 4 ); - FS_Write( cls.demofile, BF_GetData( &buf ), len ); - BF_Clear( &buf ); - } - } - } + // now copy the stuff we cached from the server. + copysize = savepos = FS_Tell( cls.demoheader ); - // write eventlist - for( i = 0; i < MAX_EVENTS; i++ ) - { - if( cl.event_precache[i][0] ) - { - BF_WriteByte( &buf, svc_eventindex ); - BF_WriteUBitLong( &buf, i, MAX_EVENT_BITS ); - BF_WriteString( &buf, cl.event_precache[i] ); + FS_Seek( cls.demoheader, 0, SEEK_SET ); - if( BF_GetNumBytesWritten( &buf ) > ( BF_GetMaxBytes( &buf ) / 2 )) - { - // write it out - len = BF_GetNumBytesWritten( &buf ); - FS_Write( cls.demofile, &len, 4 ); - FS_Write( cls.demofile, BF_GetData( &buf ), len ); - BF_Clear( &buf ); - } - } - } + FS_FileCopy( cls.demofile, cls.demoheader, copysize ); - // write lightstyles - for( i = 0; i < MAX_LIGHTSTYLES; i++ ) - { - if( cl.lightstyles[i].pattern[0] ) - { - BF_WriteByte( &buf, svc_lightstyle ); - BF_WriteByte( &buf, i ); - BF_WriteString( &buf, cl.lightstyles[i].pattern ); + // jump back to end, in case we record another demo for this session. + FS_Seek( cls.demoheader, savepos, SEEK_SET ); - if( BF_GetNumBytesWritten( &buf ) > ( BF_GetMaxBytes( &buf ) / 2 )) - { - // write it out - len = BF_GetNumBytesWritten( &buf ); - FS_Write( cls.demofile, &len, 4 ); - FS_Write( cls.demofile, BF_GetData( &buf ), len ); - BF_Clear( &buf ); - } - } - } + demo.starttime = CL_GetDemoRecordClock(); // setup the demo starttime + demo.framecount = 0; - // user messages - for( i = 0; i < MAX_USER_MESSAGES; i++ ) - { - if( clgame.msg[i].name[0] && clgame.msg[i].number >= svc_lastmsg ) - { - BF_WriteByte( &buf, svc_usermessage ); - BF_WriteByte( &buf, clgame.msg[i].number ); - BF_WriteByte( &buf, (byte)clgame.msg[i].size ); - BF_WriteString( &buf, clgame.msg[i].name ); + // now move on to entry # 1, the first data chunk. + curpos = FS_Tell( cls.demofile ); + demo.entry->length = curpos - demo.entry->offset; - if( BF_GetNumBytesWritten( &buf ) > ( BF_GetMaxBytes( &buf ) / 2 )) - { - // write it out - len = BF_GetNumBytesWritten( &buf ); - FS_Write( cls.demofile, &len, 4 ); - FS_Write( cls.demofile, BF_GetData( &buf ), len ); - BF_Clear( &buf ); - } - } - } + // now we are writing the first real lump. + demo.entry = &demo.directory.entries[1]; // first real data lump + demo.entry->entrytype = DEMO_NORMAL; + demo.entry->playback_time = 0.0f; // startup takes 0 time. - // delta tables - for( i = 0; i < Delta_NumTables( ); i++ ) - { - dt = Delta_FindStructByIndex( i ); + demo.entry->offset = FS_Tell( cls.demofile ); - for( j = 0; j < dt->numFields; j++ ) - { - Delta_WriteTableField( &buf, i, &dt->pFields[j] ); + // demo playback should read this as an incoming message. + // write the client's realtime value out so we can synchronize the reads. + CL_WriteDemoCmdHeader( dem_jumptime, cls.demofile ); - if( BF_GetNumBytesWritten( &buf ) > ( BF_GetMaxBytes( &buf ) / 2 )) - { - // write it out - len = BF_GetNumBytesWritten( &buf ); - FS_Write( cls.demofile, &len, 4 ); - FS_Write( cls.demofile, BF_GetData( &buf ), len ); - BF_Clear( &buf ); - } - } - } - - // baselines - Q_memset( &nullstate, 0, sizeof( nullstate )); - Q_memset( &nullmovevars, 0, sizeof( nullmovevars )); - - MSG_WriteDeltaMovevars( &buf, &nullmovevars, &clgame.movevars ); - - for( i = 0; i < clgame.maxEntities; i++ ) - { - state = &clgame.entities[i].baseline; - if( !state->number ) continue; - if( !state->modelindex || state->effects == EF_NODRAW ) - continue; - - BF_WriteByte( &buf, svc_spawnbaseline ); - MSG_WriteDeltaEntity( &nullstate, state, &buf, true, CL_IsPlayerIndex( state->number ), cl.mtime[0] ); - - if( BF_GetNumBytesWritten( &buf ) > ( BF_GetMaxBytes( &buf ) / 2 )) - { - // write it out - len = BF_GetNumBytesWritten( &buf ); - FS_Write( cls.demofile, &len, 4 ); - FS_Write( cls.demofile, BF_GetData( &buf ), len ); - BF_Clear( &buf ); - } - } - - BF_WriteByte( &buf, svc_stufftext ); - BF_WriteString( &buf, "precache\n" ); - - BF_WriteByte( &buf, svc_setview ); - BF_WriteWord( &buf, cl.refdef.viewentity ); - - // write all clients userinfo - for( i = 0; i < cl.maxclients; i++ ) - { - player_info_t *pi; - - BF_WriteByte( &buf, svc_updateuserinfo ); - BF_WriteUBitLong( &buf, i, MAX_CLIENT_BITS ); - pi = &cl.players[i]; - - if( pi->name[0] ) - { - BF_WriteOneBit( &buf, 1 ); - BF_WriteString( &buf, pi->userinfo ); - } - else BF_WriteOneBit( &buf, 0 ); - } - - // write it to the demo file - len = BF_GetNumBytesWritten( &buf ); - FS_Write( cls.demofile, &len, 4 ); - FS_Write( cls.demofile, BF_GetData( &buf ), len ); - - // force client.dll update - Cmd_ExecuteString( "cmd fullupdate\n", src_command ); if( clgame.hInstance ) clgame.dllFuncs.pfnReset(); - cl.validsequence = 0; // haven't gotten a valid frame update yet - cl.delta_sequence = -1; // we'll request a full delta from the baseline - cls.lastoutgoingcommand = -1; // we don't have a backed up cmd history yet - cls.nextcmdtime = host.realtime; // we can send a cmd right away + Cbuf_InsertText( "fullupdate\n" ); + Cbuf_Execute(); - // UNDONE: current demo implementation is completely wrong - // it's support only uncompressed demos at he moment - Cvar_SetFloat( "cl_nodelta", 1.0f ); + // resend the ambient sounds + Host_RestartAmbientSounds(); +} + +/* +================= +CL_StopRecord + +finish recording demo +================= +*/ +void CL_StopRecord( void ) +{ + int i, curpos; + float stoptime; + + if( !cls.demorecording ) return; + + // demo playback should read this as an incoming message. + CL_WriteDemoCmdHeader( dem_stop, cls.demofile ); + + stoptime = CL_GetDemoRecordClock(); + + // close down the hud for now. + // g-cont. is this need??? + if( clgame.hInstance ) clgame.dllFuncs.pfnReset(); + + curpos = FS_Tell( cls.demofile ); + demo.entry->length = curpos - demo.entry->offset; + demo.entry->playback_time = stoptime - demo.starttime; + demo.entry->playback_frames = demo.framecount; + + // Now write out the directory and free it and touch up the demo header. + FS_Write( cls.demofile, &demo.directory.numentries, sizeof( int )); + + for( i = 0; i < demo.directory.numentries; i++ ) + { + FS_Write( cls.demofile, &demo.directory.entries[i], sizeof( demoentry_t )); + } + + Mem_Free( demo.directory.entries ); + demo.directory.numentries = 0; + + demo.header.directory_offset = curpos; + FS_Seek( cls.demofile, 0, SEEK_SET ); + FS_Write( cls.demofile, &demo.header, sizeof( demo.header )); + + FS_Close( cls.demofile ); + cls.demofile = NULL; + cls.demorecording = false; + cls.demoname[0] = '\0'; + menu.globals->demoname[0] = '\0'; + + Msg( "Completed demo\n" ); + MsgDev( D_INFO, "Recording time %.2f\n", stoptime - demo.starttime ); } /* @@ -333,39 +455,95 @@ CLIENT SIDE DEMO PLAYBACK ======================================================================= */ /* -================== -CL_NextDemo +================= +CL_ReadDemoCmdHeader -Called when a demo or cinematic finishes -If the "nextdemo" cvar is set, that command will be issued -================== +read the demo command +================= */ -qboolean CL_NextDemo( void ) +void CL_ReadDemoCmdHeader( byte *cmd, float *dt ) { - string str; + // read the command + FS_Read( cls.demofile, cmd, sizeof( byte )); + ASSERT( *cmd >= 1 && *cmd <= dem_lastcmd ); - if( cls.demonum == -1 ) - return false; // don't play demos + // read the timestamp + FS_Read( cls.demofile, dt, sizeof( float )); +} - S_StopAllSounds(); +/* +================= +CL_ReadDemoUserCmd - if( !cls.demos[cls.demonum][0] || cls.demonum == MAX_DEMOS ) +read the demo usercmd for predicting +and smooth movement during playback the demo +================= +*/ +void CL_ReadDemoUserCmd( qboolean discard ) +{ + byte data[1024]; + int cmdnumber; + int outgoing_sequence; + word bytes; + + FS_Read( cls.demofile, &outgoing_sequence, sizeof( int )); + FS_Read( cls.demofile, &cmdnumber, sizeof( int )); + FS_Read( cls.demofile, &bytes, sizeof( short )); + FS_Read( cls.demofile, data, bytes ); + + if( !discard ) { - cls.demonum = 0; - if( !cls.demos[cls.demonum][0] ) - { - MsgDev( D_INFO, "no demos listed with startdemos\n" ); - cls.demonum = -1; - return false; - } + usercmd_t nullcmd; + sizebuf_t buf; + + Q_memset( &nullcmd, 0, sizeof( nullcmd )); + BF_Init( &buf, "UserCmd", data, sizeof( data )); + + // always delta'ing from null + cl.refdef.cmd = &cl.cmds[cmdnumber & CL_UPDATE_MASK ]; + + MSG_ReadDeltaUsercmd( &buf, &nullcmd, cl.refdef.cmd ); + + // NOTE: we need to have the current outgoing sequence correct + // so we can do prediction correctly during playback + cls.netchan.outgoing_sequence = outgoing_sequence; } +} - Q_snprintf( str, MAX_STRING, "playdemo %s\n", cls.demos[cls.demonum] ); +/* +================= +CL_ReadDemoSequence - Cbuf_InsertText( str ); - cls.demonum++; +read netchan sequences +================= +*/ +void CL_ReadDemoSequence( qboolean discard ) +{ + int incoming_sequence; + int incoming_acknowledged; + int incoming_reliable_acknowledged; + int incoming_reliable_sequence; + int outgoing_sequence; + int reliable_sequence; + int last_reliable_sequence; - return true; + FS_Read( cls.demofile, &incoming_sequence, sizeof( int )); + FS_Read( cls.demofile, &incoming_acknowledged, sizeof( int )); + FS_Read( cls.demofile, &incoming_reliable_acknowledged, sizeof( int )); + FS_Read( cls.demofile, &incoming_reliable_sequence, sizeof( int )); + FS_Read( cls.demofile, &outgoing_sequence, sizeof( int )); + FS_Read( cls.demofile, &reliable_sequence, sizeof( int )); + FS_Read( cls.demofile, &last_reliable_sequence, sizeof( int )); + + if( discard ) return; + + cls.netchan.incoming_sequence = incoming_sequence; + cls.netchan.incoming_acknowledged = incoming_acknowledged; + cls.netchan.incoming_reliable_acknowledged = incoming_reliable_acknowledged; + cls.netchan.incoming_reliable_sequence = incoming_reliable_sequence; + cls.netchan.outgoing_sequence = outgoing_sequence; + cls.netchan.reliable_sequence = reliable_sequence; + cls.netchan.last_reliable_sequence = last_reliable_sequence; } /* @@ -383,65 +561,209 @@ void CL_DemoCompleted( void ) /* ================= -CL_ReadDemoMessage +CL_DemoMoveToNextSection + +returns true on success, false on failure +g-cont. probably captain obvious mode is ON +================= +*/ +qboolean CL_DemoMoveToNextSection( void ) +{ + if( ++demo.entryIndex >= demo.directory.numentries ) + { + // done + CL_DemoCompleted(); + return false; + } + + // switch to next section, we got a dem_stop + demo.entry = &demo.directory.entries[demo.entryIndex]; + + // ready to continue reading, reset clock. + FS_Seek( cls.demofile, demo.entry->offset, SEEK_SET ); + + // time is now relative to this chunk's clock. + demo.starttime = CL_GetDemoPlaybackClock(); + demo.framecount = 0; + + return true; +} + +qboolean CL_ReadRawNetworkData( byte *buffer, size_t *length ) +{ + int msglen = 0; + + ASSERT( buffer != NULL ); + ASSERT( length != NULL ); + + *length = 0; // assume we fail + FS_Read( cls.demofile, &msglen, sizeof( int )); + + if( msglen < 0 ) + { + MsgDev( D_ERROR, "Demo message length < 0\n" ); + CL_DemoCompleted(); + return false; + } + + if( msglen < 8 ) + { + MsgDev( D_NOTE, "read runt demo message\n" ); + } + + if( msglen > NET_MAX_PAYLOAD ) + { + MsgDev( D_ERROR, "Demo message %i > %i\n", msglen, NET_MAX_PAYLOAD ); + CL_DemoCompleted(); + return false; + } + + if( msglen > 0 ) + { + if( FS_Read( cls.demofile, buffer, msglen ) != msglen ) + { + MsgDev( D_ERROR, "Error reading demo message data\n" ); + CL_DemoCompleted(); + return false; + } + } + + *length = msglen; + + if( cls.state != ca_active ) + Cbuf_Execute(); + + return true; +} + +/* +================= +CL_DemoReadMessage reads demo data and write it to client ================= */ -void CL_ReadDemoMessage( void ) +qboolean CL_DemoReadMessage( byte *buffer, size_t *length ) { - sizebuf_t buf; - char buf_data[NET_MAX_PAYLOAD]; - int r, curSize; + float f = 0.0f; + long curpos = 0; + float fElapsedTime = 0.0f; + qboolean swallowmessages = true; + qboolean forceskip = false; + byte *userbuf = NULL; + size_t size; + byte cmd; if( !cls.demofile ) { + MsgDev( D_ERROR, "tried to read a demo message with no demo file\n" ); CL_DemoCompleted(); - return; + return false; } + if( cl.refdef.paused || cls.key_dest != key_game ) + { + demo.starttime += host.frametime; + return false; // paused + } + + do + { + qboolean bSkipMessage = false; + + if( !cls.demofile ) break; + curpos = FS_Tell( cls.demofile ); + + CL_ReadDemoCmdHeader( &cmd, &f ); + + fElapsedTime = CL_GetDemoPlaybackClock() - demo.starttime; + bSkipMessage = (f >= fElapsedTime) ? true : false; + + if( cls.changelevel ) + demo.framecount = 1; + + // HACKHACK: changelevel issues + if( demo.framecount <= 10 && ( fElapsedTime - f ) > host.frametime ) + { +// Msg( "cmd %s, f %g, fElapsedTime %f, starttime %f\n", demo_cmd[cmd], f, fElapsedTime, demo.starttime ); + demo.starttime = CL_GetDemoPlaybackClock(); + } + + // not ready for a message yet, put it back on the file. + if( cmd != dem_norewind && cmd != dem_stop && bSkipMessage ) + { + // never skip first message + if( demo.framecount != 0 ) + { + FS_Seek( cls.demofile, curpos, SEEK_SET ); + return false; // not time yet. + } + } + + // COMMAND HANDLERS + switch( cmd ) + { + case dem_jumptime: + demo.starttime = CL_GetDemoPlaybackClock(); + break; + case dem_stop: + CL_DemoMoveToNextSection(); + break; + case dem_userdata: + FS_Read( cls.demofile, &size, sizeof( int )); + userbuf = Mem_Alloc( cls.mempool, size ); + FS_Read( cls.demofile, userbuf, size ); + + if( clgame.hInstance ) + clgame.dllFuncs.pfnDemo_ReadBuffer( size, userbuf ); + Mem_Free( userbuf ); + userbuf = NULL; + break; + case dem_usercmd: + CL_ReadDemoUserCmd( false ); + break; + default: + swallowmessages = false; + break; + } + } while( swallowmessages ); + + if( !cls.demofile ) + return false; + + // if not on "LOADING" section, check a few things + if( demo.entryIndex ) + { + // We are now on the second frame of a new section, + // if so, reset start time (unless in a timedemo) + if( demo.framecount == 1 ) + { + // cheat by moving the relative start time forward. + demo.starttime = CL_GetDemoPlaybackClock(); + } + } + + demo.framecount++; + CL_ReadDemoSequence( false ); + + return CL_ReadRawNetworkData( buffer, length ); +} + +/* +================= +CL_ReadDemoMessage + +obsolete +================= +*/ +void CL_ReadDemoMessage( void ) +{ if( cl.refdef.paused || cls.key_dest != key_game ) return; // don't need another message yet if(( cl.time + host.frametime ) <= cl.mtime[0] ) return; - - // get the length - r = FS_Read( cls.demofile, &curSize, 4 ); - if( r != 4 ) - { - CL_DemoCompleted(); - return; - } - - if( curSize == -1 ) - { - CL_DemoCompleted(); - return; - } - - if( curSize > sizeof( buf_data )) - { - Host_Error( "CL_ReadDemoMessage: demoMsglen > NET_MAX_PAYLOAD\n" ); - return; - } - - // init the message (set maxsize to cursize so overflow check will be working properly) - BF_Init( &buf, "DemoRead", buf_data, curSize ); - r = FS_Read( cls.demofile, buf.pData, curSize ); - - if( r != curSize ) - { - MsgDev( D_ERROR, "CL_ReadDemoMessage: demo file was truncated( %d )\n", cls.state ); - CL_DemoCompleted(); - return; - } - - cls.connect_time = host.realtime; - BF_Clear( &buf ); // reset curpos - - CL_ParseServerMessage( &buf ); } /* @@ -458,33 +780,18 @@ void CL_StopPlayback( void ) // release demofile FS_Close( cls.demofile ); cls.demoplayback = false; + demo.framecount = 0; cls.demofile = NULL; + Mem_Free( demo.directory.entries ); + demo.directory.numentries = 0; + demo.directory.entries = NULL; + demo.entry = NULL; + // let game known about movie state cls.state = ca_disconnected; cls.demoname[0] = '\0'; // clear demoname too menu.globals->demoname[0] = '\0'; - - if( clgame.hInstance ) clgame.dllFuncs.pfnReset(); // end of demos, stop the client -} - -void CL_StopRecord( void ) -{ - int len = -1; - - if( !cls.demorecording ) return; - - // finish up - FS_Write( cls.demofile, &len, 4 ); - FS_Close( cls.demofile ); - cls.demofile = NULL; - cls.demorecording = false; - cls.demoname[0] = '\0'; - menu.globals->demoname[0] = '\0'; - - // it's support only uncompressed demos at he moment - // enable delta-compression here at end of the demo record - Cvar_SetFloat( "cl_nodelta", 0.0f ); } /* @@ -578,6 +885,42 @@ qboolean CL_GetComment( const char *demoname, char *comment ) return true; } +/* +================== +CL_NextDemo + +Called when a demo or cinematic finishes +If the "nextdemo" cvar is set, that command will be issued +================== +*/ +qboolean CL_NextDemo( void ) +{ + string str; + + if( cls.demonum == -1 ) + return false; // don't play demos + + S_StopAllSounds(); + + if( !cls.demos[cls.demonum][0] || cls.demonum == MAX_DEMOS ) + { + cls.demonum = 0; + if( !cls.demos[cls.demonum][0] ) + { + MsgDev( D_INFO, "no demos listed with startdemos\n" ); + cls.demonum = -1; + return false; + } + } + + Q_snprintf( str, MAX_STRING, "playdemo %s\n", cls.demos[cls.demonum] ); + + Cbuf_InsertText( str ); + cls.demonum++; + + return true; +} + /* ================== CL_DemoGetName @@ -616,9 +959,13 @@ void CL_Record_f( void ) int n; if( Cmd_Argc() == 1 ) + { name = "new"; + } else if( Cmd_Argc() == 2 ) + { name = Cmd_Argv( 1 ); + } else { Msg( "Usage: record \n" ); @@ -627,13 +974,19 @@ void CL_Record_f( void ) if( cls.demorecording ) { - Msg("CL_Record: already recording.\n"); + Msg( "Already recording.\n"); return; } - if( cls.state != ca_active ) + if( cls.demoplayback ) { - Msg ("You must be in a level to record.\n"); + Msg( "Can't record during demo playback.\n"); + return; + } + + if( !cls.demoheader || cls.state != ca_active ) + { + Msg( "You must be in a level to record.\n"); return; } @@ -643,9 +996,10 @@ void CL_Record_f( void ) for( n = 0; n < 100; n++ ) { CL_DemoGetName( n, demoname ); - if( !FS_FileExists( va( "demos/%s.dem", demoname ), false )) + if( !FS_FileExists( va( "demos/%s.dem", demoname ), true )) break; } + if( n == 100 ) { Msg( "^3ERROR: no free slots for demo recording\n" ); @@ -681,6 +1035,7 @@ void CL_PlayDemo_f( void ) { string filename; string demoname; + int i; if( Cmd_Argc() != 2 ) { @@ -689,12 +1044,8 @@ void CL_PlayDemo_f( void ) } Q_strncpy( demoname, Cmd_Argv( 1 ), sizeof( demoname ) - 1 ); - - // shutdown any game or cinematic server - CL_Disconnect(); - Host_ShutdownServer(); - Q_snprintf( filename, sizeof( filename ), "demos/%s.dem", demoname ); + if( !FS_FileExists( filename, true )) { MsgDev( D_ERROR, "couldn't open %s\n", filename ); @@ -706,11 +1057,80 @@ void CL_PlayDemo_f( void ) Q_strncpy( cls.demoname, demoname, sizeof( cls.demoname )); Q_strncpy( menu.globals->demoname, demoname, sizeof( menu.globals->demoname )); + // read in the m_DemoHeader + FS_Read( cls.demofile, &demo.header, sizeof( demoheader_t )); + + if( demo.header.id != IDEMOHEADER ) + { + MsgDev( D_ERROR, "%s is not a demo file\n", filename ); + FS_Close( cls.demofile ); + cls.demofile = NULL; + cls.demonum = -1; // stop demo loop + return; + } + + if( demo.header.net_protocol != PROTOCOL_VERSION || demo.header.dem_protocol != DEMO_PROTOCOL ) + { + MsgDev( D_ERROR, "demo protocol outdated\n" + "Demo file protocols Network(%i), Demo(%i)\n" + "Server protocol is at Network(%i), Demo(%i)\n", + demo.header.net_protocol, + demo.header.dem_protocol, + PROTOCOL_VERSION, + DEMO_PROTOCOL + ); + + FS_Close( cls.demofile ); + cls.demofile = NULL; + cls.demonum = -1; // stop demo loop + return; + } + + // now read in the directory structure. + FS_Seek( cls.demofile, demo.header.directory_offset, SEEK_SET ); + FS_Read( cls.demofile, &demo.directory.numentries, sizeof( int )); + + if( demo.directory.numentries < 1 || demo.directory.numentries > 1024 ) + { + MsgDev( D_ERROR, "demo had bogus # of directory entries: %i\n", demo.directory.numentries ); + FS_Close( cls.demofile ); + cls.demofile = NULL; + cls.demonum = -1; // stop demo loop + return; + } + + // NOTE: at this point demo is still valid + CL_Disconnect(); + Host_ShutdownServer(); + Con_Close(); UI_SetActiveMenu( false ); + // allocate demo entries + demo.directory.entries = Mem_Alloc( cls.mempool, sizeof( demoentry_t ) * demo.directory.numentries ); + + for( i = 0; i < demo.directory.numentries; i++ ) + { + FS_Read( cls.demofile, &demo.directory.entries[i], sizeof( demoentry_t )); + } + + demo.entryIndex = 0; + demo.entry = &demo.directory.entries[demo.entryIndex]; + + FS_Seek( cls.demofile, demo.entry->offset, SEEK_SET ); + cls.demoplayback = true; cls.state = ca_connected; + + demo.starttime = CL_GetDemoPlaybackClock(); // for determining whether to read another message + + Netchan_Setup( NS_CLIENT, &cls.netchan, net_from, Cvar_VariableValue( "net_qport" )); + + demo.framecount = 0; + cls.lastoutgoingcommand = -1; + cls.nextcmdtime = host.realtime; + + // g-cont. is this need? Q_strncpy( cls.servername, demoname, sizeof( cls.servername )); // begin a playback demo diff --git a/engine/client/cl_frame.c b/engine/client/cl_frame.c index 561092b2..c80be4b3 100644 --- a/engine/client/cl_frame.c +++ b/engine/client/cl_frame.c @@ -213,25 +213,19 @@ qboolean CL_AddVisibleEntity( cl_entity_t *ent, int entityType ) // add in muzzleflash effect if( ent->curstate.effects & EF_MUZZLEFLASH ) { - vec3_t pos; + dlight_t *dl; if( ent == &clgame.viewent ) ent->curstate.effects &= ~EF_MUZZLEFLASH; - VectorCopy( ent->attachment[0], pos ); + dl = CL_AllocElight( 0 ); - // make sure what attachment is valid - if( !VectorCompare( pos, ent->origin )) - { - dlight_t *dl = CL_AllocElight( 0 ); - - VectorCopy( pos, dl->origin ); - dl->die = cl.time + 0.05f; - dl->color.r = 255; - dl->color.g = 180; - dl->color.b = 64; - dl->radius = 100; - } + VectorCopy( ent->attachment[0], dl->origin ); + dl->die = cl.time + 0.05f; + dl->color.r = 255; + dl->color.g = 180; + dl->color.b = 64; + dl->radius = 100; } // add light effect @@ -576,6 +570,10 @@ void CL_ParsePacketEntities( sizebuf_t *msg, qboolean delta ) entity_state_t *oldent; int i, count; + // save first uncompressed packet as timestamp + if( cls.changelevel && !delta && cls.demorecording ) + CL_WriteDemoJumpTime(); + // first, allocate packet for new frame count = BF_ReadWord( msg ); @@ -622,7 +620,7 @@ void CL_ParsePacketEntities( sizebuf_t *msg, qboolean delta ) // this is a full update that we can start delta compressing from now oldframe = NULL; - oldpacket = -1; // delta too old or is initial message + oldpacket = -1; // delta too old or is initial message cl.force_send_usercmd = true; // send reply cls.demowaiting = false; // we can start recording now } diff --git a/engine/client/cl_game.c b/engine/client/cl_game.c index 14f198a0..4d90fa5d 100644 --- a/engine/client/cl_game.c +++ b/engine/client/cl_game.c @@ -2948,7 +2948,7 @@ Demo_WriteBuffer */ static void Demo_WriteBuffer( int size, byte *buffer ) { - // TODO: implement + CL_WriteDemoUserMessage( buffer, size ); } /* diff --git a/engine/client/cl_main.c b/engine/client/cl_main.c index f27080f4..ffee33c4 100644 --- a/engine/client/cl_main.c +++ b/engine/client/cl_main.c @@ -224,7 +224,7 @@ CLIENT MOVEMENT COMMUNICATION CL_CreateCmd ================= */ -usercmd_t CL_CreateCmd( void ) +void CL_CreateCmd( void ) { usercmd_t cmd; color24 color; @@ -257,9 +257,16 @@ usercmd_t CL_CreateCmd( void ) // because it may contain leftover inputs // from the last level if( ++cl.movemessages <= 10 ) - return cmd; + { + if( !cls.demoplayback ) + { + cl.refdef.cmd = &cl.cmds[cls.netchan.outgoing_sequence & CL_UPDATE_MASK]; + *cl.refdef.cmd = cmd; + } + return; + } - active = ( cls.state == ca_active && !cl.refdef.paused ); + active = ( cls.state == ca_active && !cl.refdef.paused && !cls.demoplayback ); clgame.dllFuncs.CL_CreateMove( cl.time - cl.oldtime, &cmd, active ); R_LightForPoint( cl.frame.local.client.origin, &color, false, false, 128.0f ); @@ -271,13 +278,20 @@ usercmd_t CL_CreateCmd( void ) V_ProcessOverviewCmds( &cmd ); - if( cl.background || cls.demoplayback || gl_overview->integer ) + if( cl.background || gl_overview->integer ) { VectorCopy( angles, cl.refdef.cl_viewangles ); VectorCopy( angles, cmd.viewangles ); cmd.msec = 0; } - return cmd; + + // demo always have commands + // so don't overwrite them + if( !cls.demoplayback ) + { + cl.refdef.cmd = &cl.cmds[cls.netchan.outgoing_sequence & CL_UPDATE_MASK]; + *cl.refdef.cmd = cmd; + } } void CL_WriteUsercmd( sizebuf_t *msg, int from, int to ) @@ -361,7 +375,7 @@ void CL_WritePacket( void ) if(( cls.netchan.outgoing_sequence - cls.netchan.incoming_acknowledged ) >= CL_UPDATE_MASK ) { - if(( host.realtime - cls.netchan.last_received ) > CONNECTION_PROBLEM_TIME && !cls.demoplayback ) + if(( host.realtime - cls.netchan.last_received ) > CONNECTION_PROBLEM_TIME ) { Con_NPrintf( 1, "^3Warning:^1 Connection Problem^7\n" ); cl.validsequence = 0; @@ -475,6 +489,13 @@ void CL_WritePacket( void ) cls.netchan.outgoing_sequence++; } + if( cls.demorecording ) + { + // Back up one because we've incremented outgoing_sequence each frame by 1 unit + cmdnumber = ( cls.netchan.outgoing_sequence - 1 ) & CL_UPDATE_MASK; + CL_WriteDemoUserCmd( cmdnumber ); + } + // update download/upload slider. Netchan_UpdateProgress( &cls.netchan ); } @@ -489,8 +510,7 @@ Called every frame to builds and sends a command packet to the server. void CL_SendCmd( void ) { // we create commands even if a demo is playing, - cl.refdef.cmd = &cl.cmds[cls.netchan.outgoing_sequence & CL_UPDATE_MASK]; - *cl.refdef.cmd = CL_CreateCmd(); + CL_CreateCmd(); // clc_move, userinfo etc CL_WritePacket(); @@ -917,13 +937,6 @@ void CL_Reconnect_f( void ) S_StopAllSounds (); - if( cls.demoplayback ) - { - // demo issues changelevel - cls.changelevel = true; - return; - } - if( cls.state == ca_connected ) { cls.demonum = cls.movienum = -1; // not in the demo loop now @@ -940,6 +953,8 @@ void CL_Reconnect_f( void ) cl.delta_sequence = -1; // we'll request a full delta from the baseline cls.lastoutgoingcommand = -1; // we don't have a backed up cmd history yet cls.nextcmdtime = host.realtime; // we can send a cmd right away + + CL_StartupDemoHeader (); return; } @@ -1226,6 +1241,8 @@ void CL_ConnectionlessPacket( netadr_t from, sizebuf_t *msg ) cls.lastoutgoingcommand = -1; // we don't have a backed up cmd history yet cls.nextcmdtime = host.realtime; // we can send a cmd right away + CL_StartupDemoHeader (); + UI_SetActiveMenu( false ); } else if( !Q_strcmp( c, "info" )) @@ -1317,6 +1334,27 @@ void CL_ConnectionlessPacket( netadr_t from, sizebuf_t *msg ) else MsgDev( D_ERROR, "bad connectionless packet from %s:\n%s\n", NET_AdrToString( from ), args ); } +/* +==================== +CL_GetMessage + +Handles recording and playback of demos, on top of NET_ code +==================== +*/ +int CL_GetMessage( byte *data, size_t *length ) +{ + if( cls.demoplayback ) + { + if( CL_DemoReadMessage( data, length )) + return true; + return false; + } + + if( NET_GetPacket( NS_CLIENT, &net_from, data, length )) + return true; + return false; +} + /* ================= CL_ReadNetMessage @@ -1324,13 +1362,10 @@ CL_ReadNetMessage */ void CL_ReadNetMessage( void ) { - int curSize; + size_t curSize; - while( NET_GetPacket( NS_CLIENT, &net_from, net_message_buffer, &curSize )) + while( CL_GetMessage( net_message_buffer, &curSize )) { - if( host.type == HOST_DEDICATED || cls.demoplayback ) - return; - BF_Init( &net_message, "ServerData", net_message_buffer, curSize ); // check for connectionless packet (0xffffffff) first @@ -1350,24 +1385,16 @@ void CL_ReadNetMessage( void ) } // packet from server - if( !NET_CompareAdr( net_from, cls.netchan.remote_address )) + if( !cls.demoplayback && !NET_CompareAdr( net_from, cls.netchan.remote_address )) { MsgDev( D_ERROR, "CL_ReadPackets: %s:sequenced packet without connection\n", NET_AdrToString( net_from )); continue; } - if( Netchan_Process( &cls.netchan, &net_message )) - { - // the header is different lengths for reliable and unreliable messages - int headerBytes = BF_GetNumBytesRead( &net_message ); + if( !cls.demoplayback && !Netchan_Process( &cls.netchan, &net_message )) + continue; // wasn't accepted for some reason - CL_ParseServerMessage( &net_message ); - - // we don't know if it is ok to save a demo message until - // after we have parsed the frame - if( cls.demorecording && !cls.demowaiting ) - CL_WriteDemoMessage( &net_message, headerBytes ); - } + CL_ParseServerMessage( &net_message ); } // check for fragmentation/reassembly related packets. @@ -1380,11 +1407,6 @@ void CL_ReadNetMessage( void ) if( Netchan_CopyNormalFragments( &cls.netchan, &net_message )) { CL_ParseServerMessage( &net_message ); - - // we don't know if it is ok to save a demo message until - // after we have parsed the frame - if( cls.demorecording && !cls.demowaiting ) - CL_WriteDemoMessage( &net_message, headerBytes ); } if( Netchan_CopyFileFragments( &cls.netchan, &net_message )) @@ -1399,9 +1421,7 @@ void CL_ReadNetMessage( void ) void CL_ReadPackets( void ) { - if( cls.demoplayback ) - CL_ReadDemoMessage(); - else CL_ReadNetMessage(); + CL_ReadNetMessage(); cl.lerpFrac = CL_LerpPoint(); cl.thirdperson = clgame.dllFuncs.CL_IsThirdPerson(); @@ -1492,8 +1512,6 @@ void CL_Precache_f( void ) CL_PrepSound(); CL_PrepVideo(); - if( cls.demoplayback ) return; // not really connected - BF_WriteByte( &cls.netchan.message, clc_stringcmd ); BF_WriteString( &cls.netchan.message, va( "begin %i\n", spawncount )); } @@ -1646,7 +1664,7 @@ Host_ClientFrame void Host_ClientFrame( void ) { // if client is not active, do nothing - if ( !cls.initialized ) return; + if( !cls.initialized ) return; // decide the simulation time cl.oldtime = cl.time; @@ -1748,6 +1766,7 @@ void CL_Shutdown( void ) Host_WriteOpenGLConfig (); Host_WriteVideoConfig (); + CL_CloseDemoHeader(); IN_Shutdown (); SCR_Shutdown (); CL_UnloadProgs (); diff --git a/engine/client/cl_parse.c b/engine/client/cl_parse.c index 7a6d5b0a..03422dbf 100644 --- a/engine/client/cl_parse.c +++ b/engine/client/cl_parse.c @@ -479,7 +479,8 @@ void CL_ParseServerData( sizebuf_t *msg ) MsgDev( D_NOTE, "Serverdata packet received.\n" ); - clgame.load_sequence++; // now all hud sprites are invalid + cls.demowaiting = false; // server is changed + clgame.load_sequence++; // now all hud sprites are invalid // wipe the client_t struct if( !cls.changelevel ) CL_ClearState(); @@ -1348,7 +1349,9 @@ void CL_ParseServerMessage( sizebuf_t *msg ) CL_ClearState (); CL_InitEdicts (); // re-arrange edicts - cls.state = ca_connecting; + if( cls.demoplayback ) + cls.state = ca_connected; + else cls.state = ca_connecting; cls.connect_time = MAX_HEARTBEAT; // CL_CheckForResend() will fire immediately break; case svc_setview: @@ -1512,4 +1515,18 @@ void CL_ParseServerMessage( sizebuf_t *msg ) } cls_message_debug.parsing = false; // done + + // we don't know if it is ok to save a demo message until + // after we have parsed the frame + if( !cls.demoplayback ) + { + if( cls.demorecording && !cls.demowaiting ) + { + CL_WriteDemoMessage( false, starting_count, msg ); + } + else if( cls.state != ca_active ) + { + CL_WriteDemoMessage( true, starting_count, msg ); + } + } } \ No newline at end of file diff --git a/engine/client/cl_pmove.c b/engine/client/cl_pmove.c index 17d5339a..e44e87c1 100644 --- a/engine/client/cl_pmove.c +++ b/engine/client/cl_pmove.c @@ -771,12 +771,10 @@ void CL_PredictMovement( void ) viewent = CL_GetEntityByIndex( cl.refdef.viewentity ); cd = &cl.frame.local.client; - if( cls.demoplayback && viewent ) + if( cls.demoplayback && cl.refdef.cmd != NULL ) { - // restore viewangles from angles - cl.refdef.cl_viewangles[PITCH] = viewent->angles[PITCH] * 6; - cl.refdef.cl_viewangles[YAW] = viewent->angles[YAW]; - cl.refdef.cl_viewangles[ROLL] = 0; // roll will be computed in view.cpp + // restore viewangles from cmd.angles + VectorCopy( cl.refdef.cmd->viewangles, cl.refdef.cl_viewangles ); } // unpredicted pure angled values converted into axis diff --git a/engine/client/client.h b/engine/client/client.h index 368eb075..800b922f 100644 --- a/engine/client/client.h +++ b/engine/client/client.h @@ -509,6 +509,7 @@ typedef struct string demoname; // for demo looping file_t *demofile; + file_t *demoheader; // contain demo startup info in case we record a demo on this level } client_static_t; #ifdef __cplusplus @@ -579,12 +580,13 @@ void SCR_Viewpos_f( void ); void SCR_TimeRefresh_f( void ); // -// cl_main +// cl_main.c // void CL_Init( void ); void CL_SendCommand( void ); void CL_Disconnect_f( void ); void CL_ProcessFile( qboolean successfully_received, const char *filename ); +void CL_WriteUsercmd( sizebuf_t *msg, int from, int to ); void CL_GetChallengePacket( void ); void CL_PingServers_f( void ); void CL_ClearState( void ); @@ -592,9 +594,14 @@ void CL_ClearState( void ); // // cl_demo.c // +void CL_StartupDemoHeader( void ); void CL_DrawDemoRecording( void ); -void CL_WriteDemoMessage( sizebuf_t *msg, int head_size ); -void CL_ReadDemoMessage( void ); +void CL_WriteDemoUserCmd( int cmdnumber ); +void CL_WriteDemoMessage( qboolean startup, int start, sizebuf_t *msg ); +void CL_WriteDemoUserMessage( const byte *buffer, size_t size ); +qboolean CL_DemoReadMessage( byte *buffer, size_t *length ); +void CL_WriteDemoJumpTime( void ); +void CL_CloseDemoHeader( void ); void CL_StopPlayback( void ); void CL_StopRecord( void ); void CL_PlayDemo_f( void ); diff --git a/engine/client/gl_decals.c b/engine/client/gl_decals.c index 2d2e7e0c..ec1e46c8 100644 --- a/engine/client/gl_decals.c +++ b/engine/client/gl_decals.c @@ -18,6 +18,7 @@ GNU General Public License for more details. #include "gl_local.h" #include "cl_tent.h" +#define DECAL_OVERLAP_DISTANCE 2 #define DECAL_DISTANCE 4 // too big values produce more clipped polygons #define MAX_DECALCLIPVERT 32 // produced vertexes of fragmented decal #define DECAL_CACHEENTRY 256 // MUST BE POWER OF 2 or code below needs to change! @@ -459,19 +460,7 @@ static float *R_DecalVertsNoclip( decal_t *pdecal, msurface_t *surf, int texture return vlist; } -//----------------------------------------------------------------------------- -// Purpose: Check for intersecting decals on this surface -// Input : *psurf - -// *pcount - -// x - -// y - -// Output : static decal_t -//----------------------------------------------------------------------------- -// UNDONE: This probably doesn't work quite right any more -// we should base overlap on the new decal basis matrix -// decal basis is constant per plane, perhaps we should store it (unscaled) in the shared plane struct -// BRJ: Note, decal basis is not constant when decals need to specify an s direction -// but that certainly isn't the majority case +// Check for intersecting decals on this surface static decal_t *R_DecalIntersect( decalinfo_t *decalinfo, msurface_t *surf, int *pcount ) { int texture; @@ -1015,7 +1004,7 @@ static int DecalListAdd( decallist_t *pList, int count ) { VectorSubtract( pdecal->position, pList[i].position, tmp ); // Merge - if( VectorLength( tmp ) < 2 ) // UNDONE: Tune this '2' constant + if( VectorLength( tmp ) < DECAL_OVERLAP_DISTANCE ) return count; } } diff --git a/engine/client/gl_rlight.c b/engine/client/gl_rlight.c index 95ce707f..3c9cebc4 100644 --- a/engine/client/gl_rlight.c +++ b/engine/client/gl_rlight.c @@ -313,11 +313,6 @@ int R_LightTraceFilter( physent_t *pe ) if( !pe || pe->solid != SOLID_BSP ) return 1; - // optimization. Ignore world to avoid - // unneeded transformations - if( pe->info == 0 ) - return 1; - return 0; } diff --git a/engine/client/gl_rsurf.c b/engine/client/gl_rsurf.c index cc5161a2..8f408c17 100644 --- a/engine/client/gl_rsurf.c +++ b/engine/client/gl_rsurf.c @@ -1025,7 +1025,8 @@ void R_RenderBrushPoly( msurface_t *fa ) fullbright_polys[t->fb_texturenum] = fa->polys; draw_fullbrights = true; } - else if( r_detailtextures->integer && t->dt_texturenum ) + + if( r_detailtextures->integer && t->dt_texturenum ) { mextrasurf_t *es = SURF_INFO( fa, RI.currentmodel ); es->detailchain = detail_surfaces[t->dt_texturenum]; diff --git a/engine/client/gl_studio.c b/engine/client/gl_studio.c index 0a955299..196c2a1c 100644 --- a/engine/client/gl_studio.c +++ b/engine/client/gl_studio.c @@ -960,7 +960,6 @@ void R_StudioCalcRotations( cl_entity_t *e, float pos[][3], vec4_t *q, mstudiose } else if( f < -0.01f ) { - // BUGBUG ( somewhere else ) but this code should validate this data. // this could cause a crash if the frame # is negative, so we'll go ahead // and clamp it here MsgDev( D_ERROR, "StudioCalcRotations: f = %g\n", f ); @@ -1429,6 +1428,9 @@ void R_StudioDynamicLight( cl_entity_t *ent, alight_t *lightinfo ) } else VectorSet( plight->lightvec, 0.0f, 0.0f, -1.0f ); + if( VectorIsNull( plight->lightvec )) + VectorSet( plight->lightvec, 0.0f, 0.0f, -1.0f ); + VectorCopy( plight->lightvec, lightinfo->plightvec ); // setup ambient lighting @@ -1649,14 +1651,14 @@ void R_StudioLighting( float *lv, int bone, int flags, vec3_t normal ) for( i = 0; i < plight->numdlights; i++) { lightcos = -DotProduct( normal, plight->dlightvec[i][bone] ); - if( lightcos > 0 ) VectorMA( illum, lightcos, plight->dlightcolor[i], illum ); + if( lightcos > 0.0f ) VectorMA( illum, lightcos, plight->dlightcolor[i], illum ); } // now add all entity lights for( i = 0; i < plight->numelights; i++) { lightcos = -DotProduct( normal, plight->elightvec[i][bone] ); - if( lightcos > 0 ) VectorMA( illum, lightcos, plight->elightcolor[i], illum ); + if( lightcos > 0.0f ) VectorMA( illum, lightcos, plight->elightcolor[i], illum ); } } @@ -1962,6 +1964,10 @@ static void R_StudioDrawPoints( void ) clr = &RI.currententity->curstate.rendercolor; pglColor4ub( clr->r, clr->g, clr->b, alpha * 255 ); } + else if( g_nFaceFlags & STUDIO_NF_FULLBRIGHT ) + { + pglColor4f( 1.0f, 1.0f, 1.0f, 1.0f ); + } else { lv = (float *)g_lightvalues[ptricmds[1]]; diff --git a/engine/client/s_vox.c b/engine/client/s_vox.c index b0a9f18d..78d6d674 100644 --- a/engine/client/s_vox.c +++ b/engine/client/s_vox.c @@ -378,7 +378,7 @@ void VOX_FreeWord( channel_t *pchan ) pchan->currentWord = NULL; // sentence is finished Q_memset( &pchan->pMixer, 0, sizeof( pchan->pMixer )); - // UNDONE: release unused sounds ? + // release unused sounds ? #if 0 if( pchan->words[pchan->wordIndex].sfx ) { diff --git a/engine/common/cmd.c b/engine/common/cmd.c index 5df03860..71b81b06 100644 --- a/engine/common/cmd.c +++ b/engine/common/cmd.c @@ -731,7 +731,7 @@ void Cmd_ForwardToServer( void ) if( cls.demoplayback ) { - if( !Q_stricmp( Cmd_Argv( 1 ), "pause" )) + if( !Q_stricmp( Cmd_Argv( 0 ), "pause" )) cl.refdef.paused ^= 1; return; } diff --git a/engine/common/common.h b/engine/common/common.h index ea8af547..6b47bfde 100644 --- a/engine/common/common.h +++ b/engine/common/common.h @@ -266,6 +266,7 @@ typedef struct float volume; float attenuation; qboolean looping; + int channel; int pitch; } soundlist_t; @@ -310,6 +311,7 @@ typedef struct host_parm_s qboolean overview_loading; // another nasty hack to tell imagelib about ovierview qboolean force_draw_version; // used when fraps is loaded qboolean write_to_clipboard; // put image to clipboard instead of disk + qboolean crashed; // set to true if crashed char rootdir[256]; // member root directory char gamefolder[64]; // it's a default gamefolder @@ -370,6 +372,7 @@ fs_offset_t FS_FileTime( const char *filename, qboolean gamedironly ); int FS_Print( file_t *file, const char *msg ); qboolean FS_Rename( const char *oldname, const char *newname ); qboolean FS_FileExists( const char *filename, qboolean gamedironly ); +void FS_FileCopy( file_t *pOutput, file_t *pInput, int fileSize ); qboolean FS_Delete( const char *path ); int FS_UnGetc( file_t *file, byte c ); void FS_StripExtension( char *path ); @@ -569,6 +572,7 @@ void Host_NewInstance( const char *name, const char *finalmsg ); qboolean Host_NewGame( const char *mapName, qboolean loadGame ); void Host_EndGame( const char *message, ... ); void Host_AbortCurrentFrame( void ); +void Host_RestartAmbientSounds( void ); qboolean CL_ChangeGame( const char *gamefolder, qboolean bReset ); void Host_WriteServerConfig( const char *name ); void Host_WriteOpenGLConfig( void ); diff --git a/engine/common/filesystem.c b/engine/common/filesystem.c index 9c00c663..b8746dc3 100644 --- a/engine/common/filesystem.c +++ b/engine/common/filesystem.c @@ -2008,7 +2008,7 @@ fs_offset_t FS_Read( file_t *file, void *buffer, size_t buffersize ) done += nb; file->position += nb; // Purge cached data - FS_Purge (file); + FS_Purge( file ); } } else @@ -2618,6 +2618,30 @@ qboolean FS_Delete( const char *path ) return (iRet == 0); } +/* +================== +FS_FileCopy + +================== +*/ +void FS_FileCopy( file_t *pOutput, file_t *pInput, int fileSize ) +{ + char buf[MAX_SYSPATH]; // A small buffer for the copy + int size; + + while( fileSize > 0 ) + { + if( fileSize > MAX_SYSPATH ) + size = MAX_SYSPATH; + else size = fileSize; + + FS_Read( pInput, buf, size ); + FS_Write( pOutput, buf, size ); + + fileSize -= size; + } +} + /* =========== FS_Search diff --git a/engine/common/host.c b/engine/common/host.c index 23751afc..3363141e 100644 --- a/engine/common/host.c +++ b/engine/common/host.c @@ -318,6 +318,37 @@ void Host_InitDecals( void ) MsgDev( D_NOTE, "InitDecals: %i decals\n", num_decals ); } +/* +================= +Host_RestartAmbientSounds + +Write ambient sounds into demo +================= +*/ +void Host_RestartAmbientSounds( void ) +{ + soundlist_t soundInfo[64]; + int i, nSounds; + + if( !SV_Active( )) + { + return; + } + + nSounds = S_GetCurrentStaticSounds( soundInfo, 64 ); + + for( i = 0; i < nSounds; i++ ) + { + if( !soundInfo[i].looping || soundInfo[i].entnum == -1 ) + continue; + + Msg( "Restarting sound %s...\n", soundInfo[i].name ); + S_StopSound( soundInfo[i].entnum, soundInfo[i].channel, soundInfo[i].name ); + SV_StartSound( pfnPEntityOfEntIndex( soundInfo[i].entnum ), CHAN_STATIC, soundInfo[i].name, + soundInfo[i].volume, soundInfo[i].attenuation, 0, soundInfo[i].pitch ); + } +} + /* =================== Host_GetConsoleCommands @@ -373,7 +404,7 @@ qboolean Host_FilterTime( float time ) host.realframetime = bound( MIN_FRAMETIME, host.frametime, MAX_FRAMETIME ); oldtime = host.realtime; - if( host_framerate->value > 0 && ( Host_IsLocalGame() || CL_IsPlaybackDemo() )) + if( host_framerate->value > 0 && ( Host_IsLocalGame()/* || CL_IsPlaybackDemo() */)) { float fps = host_framerate->value; if( fps > 1 ) fps = 1.0f / fps; @@ -782,8 +813,6 @@ int EXPORT Host_Main( const char *progname, int bChangeGame, pfnChangeGame func break; } - SCR_CheckStartupVids(); // must be last - host.change_game = false; // done Cmd_RemoveCommand( "setr" ); // remove potentially backdoor for change render settings Cmd_RemoveCommand( "setgl" ); @@ -791,9 +820,10 @@ int EXPORT Host_Main( const char *progname, int bChangeGame, pfnChangeGame func // we need to execute it again here Cmd_ExecuteString( "exec config.cfg\n", src_command ); oldtime = Sys_DoubleTime(); + SCR_CheckStartupVids(); // must be last // main window message loop - while( 1 ) + while( !host.crashed ) { newtime = Sys_DoubleTime (); Host_Frame( newtime - oldtime ); diff --git a/engine/common/net_chan.c b/engine/common/net_chan.c index 0b16de35..21d9c745 100644 --- a/engine/common/net_chan.c +++ b/engine/common/net_chan.c @@ -353,8 +353,11 @@ void Netchan_OutOfBand( int net_socket, netadr_t adr, int length, byte *data ) BF_WriteLong( &send, -1 ); // -1 sequence means out of band BF_WriteBytes( &send, data, length ); - // send the datagram - NET_SendPacket( net_socket, BF_GetNumBytesWritten( &send ), BF_GetData( &send ), adr ); + if( !CL_IsPlaybackDemo( )) + { + // send the datagram + NET_SendPacket( net_socket, BF_GetNumBytesWritten( &send ), BF_GetData( &send ), adr ); + } } /* @@ -1429,7 +1432,10 @@ void Netchan_TransmitBits( netchan_t *chan, int length, byte *data ) chan->total_sended_uncompressed += size1; // send the datagram - NET_SendPacket( chan->sock, BF_GetNumBytesWritten( &send ), BF_GetData( &send ), chan->remote_address ); + if( !CL_IsPlaybackDemo( )) + { + NET_SendPacket( chan->sock, BF_GetNumBytesWritten( &send ), BF_GetData( &send ), chan->remote_address ); + } fRate = 1.0f / chan->rate; @@ -1492,7 +1498,7 @@ qboolean Netchan_Process( netchan_t *chan, sizebuf_t *msg ) size_t size1, size2; int i, qport; - if( !NET_CompareAdr( net_from, chan->remote_address )) + if( !CL_IsPlaybackDemo() && !NET_CompareAdr( net_from, chan->remote_address )) { return false; } diff --git a/engine/common/net_encode.c b/engine/common/net_encode.c index 12fb4f11..82eab32b 100644 --- a/engine/common/net_encode.c +++ b/engine/common/net_encode.c @@ -32,22 +32,22 @@ static qboolean delta_init = false; // list of all the struct names static const delta_field_t cmd_fields[] = { -{ UCMD_DEF(lerp_msec) }, -{ UCMD_DEF(msec) }, -{ UCMD_DEF(viewangles[0]) }, -{ UCMD_DEF(viewangles[1]) }, -{ UCMD_DEF(viewangles[2]) }, -{ UCMD_DEF(forwardmove) }, -{ UCMD_DEF(sidemove) }, -{ UCMD_DEF(upmove) }, -{ UCMD_DEF(lightlevel) }, -{ UCMD_DEF(buttons) }, -{ UCMD_DEF(impulse) }, -{ UCMD_DEF(weaponselect) }, -{ UCMD_DEF(impact_index) }, -{ UCMD_DEF(impact_position[0])}, -{ UCMD_DEF(impact_position[1])}, -{ UCMD_DEF(impact_position[2])}, +{ UCMD_DEF( lerp_msec ) }, +{ UCMD_DEF( msec ) }, +{ UCMD_DEF( viewangles[0] ) }, +{ UCMD_DEF( viewangles[1] ) }, +{ UCMD_DEF( viewangles[2] ) }, +{ UCMD_DEF( forwardmove ) }, +{ UCMD_DEF( sidemove ) }, +{ UCMD_DEF( upmove ) }, +{ UCMD_DEF( lightlevel ) }, +{ UCMD_DEF( buttons ) }, +{ UCMD_DEF( impulse ) }, +{ UCMD_DEF( weaponselect ) }, +{ UCMD_DEF( impact_index ) }, +{ UCMD_DEF( impact_position[0] ) }, +{ UCMD_DEF( impact_position[1] ) }, +{ UCMD_DEF( impact_position[2] ) }, { NULL }, }; diff --git a/engine/common/pm_studio.c b/engine/common/pm_studio.c new file mode 100644 index 00000000..5f9637fa --- /dev/null +++ b/engine/common/pm_studio.c @@ -0,0 +1,934 @@ +/* +pm_studio.c - stduio models tracing +Copyright (C) 2010 Uncle Mike + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +*/ + +#include "common.h" +#include "studio.h" +#include "mathlib.h" +#include "mod_local.h" +#include "pm_local.h" +#include "pm_movevars.h" +#include "world.h" + +static studiohdr_t *pm_studiohdr; +static mplane_t pm_hitboxplanes[6]; // there a temp hitbox +static matrix3x4 pm_studiomatrix; +static matrix3x4 pm_studiobones[MAXSTUDIOBONES]; +typedef qboolean (*pfnTrace)( pmtrace_t *trace ); +static float trace_realfraction; +static vec3_t trace_startmins, trace_endmins; +static vec3_t trace_startmaxs, trace_endmaxs; +static vec3_t trace_absmins, trace_absmaxs; + +/* +==================== +PM_InitStudioHull +==================== +*/ +void PM_InitStudioHull( void ) +{ + int i, side; + mplane_t *p; + + for( i = 0; i < 6; i++ ) + { + side = i & 1; + + // planes + p = &pm_hitboxplanes[i]; + VectorClear( p->normal ); + + if( side ) + { + p->type = PLANE_NONAXIAL; + p->normal[i>>1] = -1.0f; + p->signbits = (1<<(i>>1)); + } + else + { + p->type = i>>1; + p->normal[i>>1] = 1.0f; + p->signbits = 0; + } + } +} + +/* +==================== +PM_HullForHitbox +==================== +*/ +static void PM_HullForHitbox( const vec3_t mins, const vec3_t maxs ) +{ + pm_hitboxplanes[0].dist = maxs[0]; + pm_hitboxplanes[1].dist = -mins[0]; + pm_hitboxplanes[2].dist = maxs[1]; + pm_hitboxplanes[3].dist = -mins[1]; + pm_hitboxplanes[4].dist = maxs[2]; + pm_hitboxplanes[5].dist = -mins[2]; +} + +/* +=============================================================================== + + STUDIO MODELS TRACING + +=============================================================================== +*/ +/* +==================== +StudioPlayerBlend + +==================== +*/ +void PM_StudioPlayerBlend( mstudioseqdesc_t *pseqdesc, int *pBlend, float *pPitch ) +{ + // calc up/down pointing + *pBlend = (*pPitch * 3); + + if( *pBlend < pseqdesc->blendstart[0] ) + { + *pPitch -= pseqdesc->blendstart[0] / 3.0f; + *pBlend = 0; + } + else if( *pBlend > pseqdesc->blendend[0] ) + { + *pPitch -= pseqdesc->blendend[0] / 3.0f; + *pBlend = 255; + } + else + { + if( pseqdesc->blendend[0] - pseqdesc->blendstart[0] < 0.1f ) // catch qc error + *pBlend = 127; + else *pBlend = 255.0f * (*pBlend - pseqdesc->blendstart[0]) / (pseqdesc->blendend[0] - pseqdesc->blendstart[0]); + *pPitch = 0; + } +} + +/* +==================== +StudioCalcBoneAdj + +==================== +*/ +static void PM_StudioCalcBoneAdj( float *adj, const byte *pcontroller ) +{ + int i, j; + float value; + mstudiobonecontroller_t *pbonecontroller; + + pbonecontroller = (mstudiobonecontroller_t *)((byte *)pm_studiohdr + pm_studiohdr->bonecontrollerindex); + + for( j = 0; j < pm_studiohdr->numbonecontrollers; j++ ) + { + i = pbonecontroller[j].index; + + if( i == 4 ) continue; // ignore mouth + if( i <= MAXSTUDIOCONTROLLERS ) + { + // check for 360% wrapping + if( pbonecontroller[j].type & STUDIO_RLOOP ) + { + value = pcontroller[i] * (360.0/256.0) + pbonecontroller[j].start; + } + else + { + value = pcontroller[i] / 255.0f; + if( value < 0.0f ) value = 0.0f; + if( value > 1.0f ) value = 1.0f; + value = (1.0f - value) * pbonecontroller[j].start + value * pbonecontroller[j].end; + } + } + + switch( pbonecontroller[j].type & STUDIO_TYPES ) + { + case STUDIO_XR: + case STUDIO_YR: + case STUDIO_ZR: + adj[j] = value * (M_PI / 180.0); + break; + case STUDIO_X: + case STUDIO_Y: + case STUDIO_Z: + adj[j] = value; + break; + } + } +} + +/* +==================== +StudioCalcBoneQuaterion + +==================== +*/ +static void PM_StudioCalcBoneQuaterion( int frame, float s, mstudiobone_t *pbone, mstudioanim_t *panim, float *adj, float *q ) +{ + int j, k; + vec4_t q1, q2; + vec3_t angle1, angle2; + mstudioanimvalue_t *panimvalue; + + for( j = 0; j < 3; j++ ) + { + if( panim->offset[j+3] == 0 ) + { + angle2[j] = angle1[j] = pbone->value[j+3]; // default; + } + else + { + panimvalue = (mstudioanimvalue_t *)((byte *)panim + panim->offset[j+3]); + k = frame; + + // debug + if( panimvalue->num.total < panimvalue->num.valid ) + k = 0; + + while( panimvalue->num.total <= k ) + { + k -= panimvalue->num.total; + panimvalue += panimvalue->num.valid + 1; + // DEBUG + if( panimvalue->num.total < panimvalue->num.valid ) + k = 0; + } + // Bah, missing blend! + if( panimvalue->num.valid > k ) + { + angle1[j] = panimvalue[k+1].value; + + if( panimvalue->num.valid > k + 1 ) + { + angle2[j] = panimvalue[k+2].value; + } + else + { + if( panimvalue->num.total > k + 1 ) + angle2[j] = angle1[j]; + else angle2[j] = panimvalue[panimvalue->num.valid+2].value; + } + } + else + { + angle1[j] = panimvalue[panimvalue->num.valid].value; + if( panimvalue->num.total > k + 1 ) + { + angle2[j] = angle1[j]; + } + else + { + angle2[j] = panimvalue[panimvalue->num.valid + 2].value; + } + } + angle1[j] = pbone->value[j+3] + angle1[j] * pbone->scale[j+3]; + angle2[j] = pbone->value[j+3] + angle2[j] * pbone->scale[j+3]; + } + + if( pbone->bonecontroller[j+3] != -1 ) + { + angle1[j] += adj[pbone->bonecontroller[j+3]]; + angle2[j] += adj[pbone->bonecontroller[j+3]]; + } + } + + if( !VectorCompare( angle1, angle2 )) + { + AngleQuaternion( angle1, q1 ); + AngleQuaternion( angle2, q2 ); + QuaternionSlerp( q1, q2, s, q ); + } + else + { + AngleQuaternion( angle1, q ); + } +} + +/* +==================== +StudioCalcBonePosition + +==================== +*/ +static void PM_StudioCalcBonePosition( int frame, float s, mstudiobone_t *pbone, mstudioanim_t *panim, float *adj, float *pos ) +{ + int j, k; + mstudioanimvalue_t *panimvalue; + + for( j = 0; j < 3; j++ ) + { + pos[j] = pbone->value[j]; // default; + if( panim->offset[j] != 0.0f ) + { + panimvalue = (mstudioanimvalue_t *)((byte *)panim + panim->offset[j]); + + k = frame; + + // debug + if( panimvalue->num.total < panimvalue->num.valid ) + k = 0; + + // find span of values that includes the frame we want + while( panimvalue->num.total <= k ) + { + k -= panimvalue->num.total; + panimvalue += panimvalue->num.valid + 1; + + // DEBUG + if( panimvalue->num.total < panimvalue->num.valid ) + k = 0; + } + + // if we're inside the span + if( panimvalue->num.valid > k ) + { + // and there's more data in the span + if( panimvalue->num.valid > k + 1 ) + { + pos[j] += (panimvalue[k+1].value * (1.0f - s) + s * panimvalue[k+2].value) * pbone->scale[j]; + } + else + { + pos[j] += panimvalue[k+1].value * pbone->scale[j]; + } + } + else + { + // are we at the end of the repeating values section and there's another section with data? + if( panimvalue->num.total <= k + 1 ) + { + pos[j] += (panimvalue[panimvalue->num.valid].value * (1.0f - s) + s * panimvalue[panimvalue->num.valid + 2].value) * pbone->scale[j]; + } + else + { + pos[j] += panimvalue[panimvalue->num.valid].value * pbone->scale[j]; + } + } + } + + if( pbone->bonecontroller[j] != -1 && adj ) + { + pos[j] += adj[pbone->bonecontroller[j]]; + } + } +} + +/* +==================== +StudioCalcRotations + +==================== +*/ +static void PM_StudioCalcRotations( physent_t *pe, const byte *pcontroller, float pos[][3], vec4_t *q, mstudioseqdesc_t *pseqdesc, mstudioanim_t *panim, float f ) +{ + int i, frame; + mstudiobone_t *pbone; + float adj[MAXSTUDIOCONTROLLERS]; + float s; + + if( f > pseqdesc->numframes - 1 ) + f = 0; + else if( f < -0.01f ) + f = -0.01f; + + frame = (int)f; + s = (f - frame); + + // add in programtic controllers + pbone = (mstudiobone_t *)((byte *)pm_studiohdr + pm_studiohdr->boneindex); + + PM_StudioCalcBoneAdj( adj, pcontroller ); + + for( i = 0; i < pm_studiohdr->numbones; i++, pbone++, panim++ ) + { + PM_StudioCalcBoneQuaterion( frame, s, pbone, panim, adj, q[i] ); + PM_StudioCalcBonePosition( frame, s, pbone, panim, adj, pos[i] ); + } + + if( pseqdesc->motiontype & STUDIO_X ) pos[pseqdesc->motionbone][0] = 0.0f; + if( pseqdesc->motiontype & STUDIO_Y ) pos[pseqdesc->motionbone][1] = 0.0f; + if( pseqdesc->motiontype & STUDIO_Z ) pos[pseqdesc->motionbone][2] = 0.0f; + + s = 0 * ((1.0 - (f - (int)(f))) / (pseqdesc->numframes)) * 1.0f; // framerate + + if( pseqdesc->motiontype & STUDIO_LX ) pos[pseqdesc->motionbone][0] += s * pseqdesc->linearmovement[0]; + if( pseqdesc->motiontype & STUDIO_LY ) pos[pseqdesc->motionbone][1] += s * pseqdesc->linearmovement[1]; + if( pseqdesc->motiontype & STUDIO_LZ ) pos[pseqdesc->motionbone][2] += s * pseqdesc->linearmovement[2]; +} + +/* +==================== +StudioEstimateFrame + +==================== +*/ +static float PM_StudioEstimateFrame( physent_t *pe, mstudioseqdesc_t *pseqdesc ) +{ + double f; + + if( pseqdesc->numframes <= 1 ) + f = 0; + else f = (pe->frame * (pseqdesc->numframes - 1)) / 256.0; + + if( pseqdesc->flags & STUDIO_LOOPING ) + { + if( pseqdesc->numframes > 1 ) + f -= (int)(f / (pseqdesc->numframes - 1)) * (pseqdesc->numframes - 1); + if( f < 0 ) f += (pseqdesc->numframes - 1); + } + else + { + if( f >= pseqdesc->numframes - 1.001 ) + f = pseqdesc->numframes - 1.001; + if( f < 0.0 ) f = 0.0; + } + return f; +} + +/* +==================== +StudioSlerpBones + +==================== +*/ +static void PM_StudioSlerpBones( vec4_t q1[], float pos1[][3], vec4_t q2[], float pos2[][3], float s ) +{ + int i; + vec4_t q3; + float s1; + + s = bound( 0.0f, s, 1.0f ); + s1 = 1.0f - s; + + for( i = 0; i < pm_studiohdr->numbones; i++ ) + { + QuaternionSlerp( q1[i], q2[i], s, q3 ); + q1[i][0] = q3[0]; + q1[i][1] = q3[1]; + q1[i][2] = q3[2]; + q1[i][3] = q3[3]; + pos1[i][0] = pos1[i][0] * s1 + pos2[i][0] * s; + pos1[i][1] = pos1[i][1] * s1 + pos2[i][1] * s; + pos1[i][2] = pos1[i][2] * s1 + pos2[i][2] * s; + } +} + +/* +==================== +PM_StudioGetAnim + +==================== +*/ +static mstudioanim_t *PM_StudioGetAnim( model_t *m_pSubModel, mstudioseqdesc_t *pseqdesc ) +{ + mstudioseqgroup_t *pseqgroup; + cache_user_t *paSequences; + size_t filesize; + byte *buf; + + pseqgroup = (mstudioseqgroup_t *)((byte *)pm_studiohdr + pm_studiohdr->seqgroupindex) + pseqdesc->seqgroup; + if( pseqdesc->seqgroup == 0 ) + return (mstudioanim_t *)((byte *)pm_studiohdr + pseqgroup->data + pseqdesc->animindex); + + paSequences = (cache_user_t *)m_pSubModel->submodels; + + if( paSequences == NULL ) + { + paSequences = (cache_user_t *)Mem_Alloc( com_studiocache, MAXSTUDIOGROUPS * sizeof( cache_user_t )); + m_pSubModel->submodels = (void *)paSequences; + } + + // check for already loaded + if( !Mod_CacheCheck(( cache_user_t *)&( paSequences[pseqdesc->seqgroup] ))) + { + string filepath, modelname, modelpath; + + FS_FileBase( m_pSubModel->name, modelname ); + FS_ExtractFilePath( m_pSubModel->name, modelpath ); + Q_snprintf( filepath, sizeof( filepath ), "%s/%s%i%i.mdl", modelpath, modelname, pseqdesc->seqgroup / 10, pseqdesc->seqgroup % 10 ); + + buf = FS_LoadFile( filepath, &filesize, false ); + if( !buf || !filesize ) Host_Error( "StudioGetAnim: can't load %s\n", filepath ); + if( IDSEQGRPHEADER != *(uint *)buf ) + Host_Error( "StudioGetAnim: %s is corrupted\n", filepath ); + + MsgDev( D_INFO, "loading: %s\n", filepath ); + + paSequences[pseqdesc->seqgroup].data = Mem_Alloc( com_studiocache, filesize ); + Q_memcpy( paSequences[pseqdesc->seqgroup].data, buf, filesize ); + Mem_Free( buf ); + } + return (mstudioanim_t *)((byte *)paSequences[pseqdesc->seqgroup].data + pseqdesc->animindex); +} + +/* +==================== +PM_StudioSetupBones +==================== +*/ +static void PM_StudioSetupBones( playermove_t *pmove, physent_t *pe, const vec3_t angles, const vec3_t origin, const byte *pcontroller, const byte *pblending ) +{ + int i, oldseq; + float scale = 1.0f; + double f; + + mstudiobone_t *pbones; + mstudioseqdesc_t *pseqdesc; + mstudioanim_t *panim; + + static float pos[MAXSTUDIOBONES][3]; + static vec4_t q[MAXSTUDIOBONES]; + matrix3x4 bonematrix; + + static float pos2[MAXSTUDIOBONES][3]; + static vec4_t q2[MAXSTUDIOBONES]; + static float pos3[MAXSTUDIOBONES][3]; + static vec4_t q3[MAXSTUDIOBONES]; + static float pos4[MAXSTUDIOBONES][3]; + static vec4_t q4[MAXSTUDIOBONES]; + + oldseq = pe->sequence; // TraceCode can't change sequence + + if( pe->sequence >= pm_studiohdr->numseq ) pe->sequence = 0; + pseqdesc = (mstudioseqdesc_t *)((byte *)pm_studiohdr + pm_studiohdr->seqindex) + pe->sequence; + + f = PM_StudioEstimateFrame( pe, pseqdesc ); + + panim = PM_StudioGetAnim( pe->studiomodel, pseqdesc ); + PM_StudioCalcRotations( pe, pcontroller, pos, q, pseqdesc, panim, f ); + + if( pseqdesc->numblends > 1 ) + { + float s; + + panim += pm_studiohdr->numbones; + PM_StudioCalcRotations( pe, pcontroller, pos2, q2, pseqdesc, panim, f ); + + s = (float)pe->blending[0] / 255.0f; + + PM_StudioSlerpBones( q, pos, q2, pos2, s ); + + if( pseqdesc->numblends == 4 ) + { + panim += pm_studiohdr->numbones; + PM_StudioCalcRotations( pe, pcontroller, pos3, q3, pseqdesc, panim, f ); + + panim += pm_studiohdr->numbones; + PM_StudioCalcRotations( pe, pcontroller, pos4, q4, pseqdesc, panim, f ); + + s = (float)pe->blending[0] / 255.0f; + PM_StudioSlerpBones( q3, pos3, q4, pos4, s ); + + s = (float)pe->blending[1] / 255.0f; + PM_StudioSlerpBones( q, pos, q3, pos3, s ); + } + } + + if( pmove->movevars->studio_scale && pe->fuser1 > 0.0f ) + scale = pe->fuser1; + else if( pe->player && pmove->movevars->clienttrace != 0.0f ) + scale = pmove->movevars->clienttrace * 0.5f; + + Matrix3x4_CreateFromEntity( pm_studiomatrix, angles, origin, scale ); + + pbones = (mstudiobone_t *)((byte *)pm_studiohdr + pm_studiohdr->boneindex); + + for( i = 0; i < pm_studiohdr->numbones; i++ ) + { + Matrix3x4_FromOriginQuat( bonematrix, q[i], pos[i] ); + if( pbones[i].parent == -1 ) + Matrix3x4_ConcatTransforms( pm_studiobones[i], pm_studiomatrix, bonematrix ); + else Matrix3x4_ConcatTransforms( pm_studiobones[i], pm_studiobones[pbones[i].parent], bonematrix ); + } + + pe->sequence = oldseq; // restore original value +} + +static qboolean PM_StudioSetupModel( playermove_t *pmove, physent_t *pe ) +{ + model_t *mod = pe->studiomodel; + vec3_t angles; + + if( !mod || !mod->cache.data ) + return false; + + pm_studiohdr = (studiohdr_t *)mod->cache.data; + VectorCopy( pe->angles, angles ); + + // calc blending for player + if( pe->player ) + { + mstudioseqdesc_t *pseqdesc; + byte controller[4]; + byte blending[2]; + int iBlend; + + pseqdesc = (mstudioseqdesc_t *)((byte *)pm_studiohdr + pm_studiohdr->seqindex) + pe->sequence; + + PM_StudioPlayerBlend( pseqdesc, &iBlend, &angles[PITCH] ); + + controller[0] = controller[1] = controller[2] = controller[3] = 0x7F; + blending[0] = (byte)iBlend; + blending[1] = 0; + + PM_StudioSetupBones( pmove, pe, angles, pe->origin, controller, blending ); + } + else + { + PM_StudioSetupBones( pmove, pe, angles, pe->origin, pe->controller, pe->blending ); + } + + return true; +} + +qboolean PM_StudioExtractBbox( playermove_t *pmove, physent_t *pe, model_t *mod, int sequence, float *mins, float *maxs ) +{ + mstudioseqdesc_t *pseqdesc; + studiohdr_t *phdr; + float scale = 1.0f; + + ASSERT( mod != NULL ); + + if( mod->type != mod_studio || !mod->cache.data ) + return false; + + phdr = (studiohdr_t *)mod->cache.data; + if( !phdr->numhitboxes ) return false; + + pseqdesc = (mstudioseqdesc_t *)((byte *)phdr + phdr->seqindex); + + if( sequence < 0 || sequence >= phdr->numseq ) + return false; + + if( pmove->movevars->studio_scale && pe->fuser1 > 0.0f ) + scale = pe->fuser1; + + VectorScale( pseqdesc[sequence].bbmin, scale, mins ); + VectorScale( pseqdesc[sequence].bbmax, scale, maxs ); + + return true; +} + +/* +================ +PM_ClipBoxToHitbox + +trace hitbox +================ +*/ +qboolean PM_ClipBoxToHitbox( pmtrace_t *trace ) +{ + int i; + mplane_t *p, *clipplane; + float enterfrac, leavefrac, distfrac; + float d, d1, d2; + qboolean getout, startout; + float f; + + enterfrac = -1.0f; + leavefrac = 1.0f; + clipplane = NULL; + + getout = false; + startout = false; + + for( i = 0; i < 6; i++ ) + { + p = &pm_hitboxplanes[i]; + + // push the plane out apropriately for mins/maxs + if( p->type < 3 ) + { + d1 = trace_startmins[p->type] - p->dist; + d2 = trace_endmins[p->type] - p->dist; + } + else + { + switch( p->signbits ) + { + case 0: + d1 = p->normal[0]*trace_startmins[0] + p->normal[1]*trace_startmins[1] + p->normal[2]*trace_startmins[2] - p->dist; + d2 = p->normal[0]*trace_endmins[0] + p->normal[1]*trace_endmins[1] + p->normal[2]*trace_endmins[2] - p->dist; + break; + case 1: + d1 = p->normal[0]*trace_startmaxs[0] + p->normal[1]*trace_startmins[1] + p->normal[2]*trace_startmins[2] - p->dist; + d2 = p->normal[0]*trace_endmaxs[0] + p->normal[1]*trace_endmins[1] + p->normal[2]*trace_endmins[2] - p->dist; + break; + case 2: + d1 = p->normal[0]*trace_startmins[0] + p->normal[1]*trace_startmaxs[1] + p->normal[2]*trace_startmins[2] - p->dist; + d2 = p->normal[0]*trace_endmins[0] + p->normal[1]*trace_endmaxs[1] + p->normal[2]*trace_endmins[2] - p->dist; + break; + case 3: + d1 = p->normal[0]*trace_startmaxs[0] + p->normal[1]*trace_startmaxs[1] + p->normal[2]*trace_startmins[2] - p->dist; + d2 = p->normal[0]*trace_endmaxs[0] + p->normal[1]*trace_endmaxs[1] + p->normal[2]*trace_endmins[2] - p->dist; + break; + case 4: + d1 = p->normal[0]*trace_startmins[0] + p->normal[1]*trace_startmins[1] + p->normal[2]*trace_startmaxs[2] - p->dist; + d2 = p->normal[0]*trace_endmins[0] + p->normal[1]*trace_endmins[1] + p->normal[2]*trace_endmaxs[2] - p->dist; + break; + case 5: + d1 = p->normal[0]*trace_startmaxs[0] + p->normal[1]*trace_startmins[1] + p->normal[2]*trace_startmaxs[2] - p->dist; + d2 = p->normal[0]*trace_endmaxs[0] + p->normal[1]*trace_endmins[1] + p->normal[2]*trace_endmaxs[2] - p->dist; + break; + case 6: + d1 = p->normal[0]*trace_startmins[0] + p->normal[1]*trace_startmaxs[1] + p->normal[2]*trace_startmaxs[2] - p->dist; + d2 = p->normal[0]*trace_endmins[0] + p->normal[1]*trace_endmaxs[1] + p->normal[2]*trace_endmaxs[2] - p->dist; + break; + case 7: + d1 = p->normal[0]*trace_startmaxs[0] + p->normal[1]*trace_startmaxs[1] + p->normal[2]*trace_startmaxs[2] - p->dist; + d2 = p->normal[0]*trace_endmaxs[0] + p->normal[1]*trace_endmaxs[1] + p->normal[2]*trace_endmaxs[2] - p->dist; + break; + default: + d1 = d2 = 0; // shut up compiler + break; + } + } + + if( d2 > 0 ) getout = true; // endpoint is not in solid + if( d1 > 0 ) startout = true; + + // if completely in front of face, no intersection + if( d1 > 0 && d2 >= d1 ) + return false; + + if( d1 <= 0 && d2 <= 0 ) + continue; + + // crosses face + d = 1.0f / ( d1 - d2 ); + f = d1 * d; + + if( d > 0 ) + { + // enter + if( f > enterfrac ) + { + distfrac = d; + enterfrac = f; + clipplane = p; + } + } + else if( d < 0 ) + { + // leave + if( f < leavefrac ) + leavefrac = f; + } + } + + if( !startout ) + { + // original point was inside hitbox + trace->startsolid = true; + if( !getout ) trace->allsolid = true; + return true; + } + + if( enterfrac - FRAC_EPSILON <= leavefrac ) + { + if( enterfrac > -1.0f && enterfrac < trace_realfraction ) + { + if( enterfrac < 0 ) + enterfrac = 0; + trace_realfraction = enterfrac; + trace->fraction = enterfrac - DIST_EPSILON * distfrac; + VectorCopy( clipplane->normal, trace->plane.normal ); + trace->plane.dist = clipplane->dist; + return true; + } + } + return false; +} + +/* +================ +PM_TestBoxInHitbox + +test point trace in hibox +================ +*/ +qboolean PM_TestBoxInHitbox( pmtrace_t *trace ) +{ + int i; + mplane_t *p; + + for( i = 0; i < 6; i++ ) + { + p = &pm_hitboxplanes[i]; + + // push the plane out apropriately for mins/maxs + // if completely in front of face, no intersection + if( p->type < 3 ) + { + if( trace_startmins[p->type] > p->dist ) + return false; + } + else + { + switch( p->signbits ) + { + case 0: + if( p->normal[0]*trace_startmins[0] + p->normal[1]*trace_startmins[1] + p->normal[2]*trace_startmins[2] > p->dist ) + return false; + break; + case 1: + if( p->normal[0]*trace_startmaxs[0] + p->normal[1]*trace_startmins[1] + p->normal[2]*trace_startmins[2] > p->dist ) + return false; + break; + case 2: + if( p->normal[0]*trace_startmins[0] + p->normal[1]*trace_startmaxs[1] + p->normal[2]*trace_startmins[2] > p->dist ) + return false; + break; + case 3: + if( p->normal[0]*trace_startmaxs[0] + p->normal[1]*trace_startmaxs[1] + p->normal[2]*trace_startmins[2] > p->dist ) + return false; + break; + case 4: + if( p->normal[0]*trace_startmins[0] + p->normal[1]*trace_startmins[1] + p->normal[2]*trace_startmaxs[2] > p->dist ) + return false; + break; + case 5: + if( p->normal[0]*trace_startmaxs[0] + p->normal[1]*trace_startmins[1] + p->normal[2]*trace_startmaxs[2] > p->dist ) + return false; + break; + case 6: + if( p->normal[0]*trace_startmins[0] + p->normal[1]*trace_startmaxs[1] + p->normal[2]*trace_startmaxs[2] > p->dist ) + return false; + break; + case 7: + if( p->normal[0]*trace_startmaxs[0] + p->normal[1]*trace_startmaxs[1] + p->normal[2]*trace_startmaxs[2] > p->dist ) + return false; + break; + default: + return false; + } + } + } + + // inside this hitbox + trace->fraction = trace_realfraction = 0; + trace->startsolid = trace->allsolid = true; + + return true; +} + +/* +================ +PM_StudioIntersect + +testing for potentially intersection of trace and animation bboxes +================ +*/ +static qboolean PM_StudioIntersect( playermove_t *pmove, physent_t *pe, const vec3_t start, vec3_t mins, vec3_t maxs, const vec3_t end ) +{ + vec3_t trace_mins, trace_maxs; + vec3_t anim_mins, anim_maxs; + + // create the bounding box of the entire move + World_MoveBounds( start, mins, maxs, end, trace_mins, trace_maxs ); + + if( !PM_StudioExtractBbox( pmove, pe, pe->studiomodel, pe->sequence, anim_mins, anim_maxs )) + return false; // invalid sequence + + if( !VectorIsNull( pe->angles )) + { + // expand for rotation + float max, v; + int i; + + for( i = 0, max = 0.0f; i < 3; i++ ) + { + v = fabs( anim_mins[i] ); + if( v > max ) max = v; + v = fabs( anim_maxs[i] ); + if( v > max ) max = v; + } + + for( i = 0; i < 3; i++ ) + { + anim_mins[i] = pe->origin[i] - max; + anim_maxs[i] = pe->origin[i] + max; + } + } + else + { + VectorAdd( anim_mins, pe->origin, anim_mins ); + VectorAdd( anim_maxs, pe->origin, anim_maxs ); + } + + // check intersection with trace entire move and animation bbox + return BoundsIntersect( trace_mins, trace_maxs, anim_mins, anim_maxs ); +} + +qboolean PM_StudioTrace( playermove_t *pmove, physent_t *pe, const vec3_t start, vec3_t mins, vec3_t maxs, const vec3_t end, pmtrace_t *ptr ) +{ + vec3_t start_l, end_l; + int i, outBone = -1; + pfnTrace StudioTrace = NULL; + + // assume we didn't hit anything + Q_memset( ptr, 0, sizeof( pmtrace_t )); + VectorCopy( end, ptr->endpos ); + ptr->fraction = trace_realfraction = 1.0f; + ptr->hitgroup = -1; + ptr->ent = -1; + + if( !PM_StudioIntersect( pmove, pe, start, mins, maxs, end )) + return false; + + if( !PM_StudioSetupModel( pmove, pe )) + return false; + + if( VectorCompare( start, end )) + StudioTrace = PM_TestBoxInHitbox; + else StudioTrace = PM_ClipBoxToHitbox; + + // go to check individual hitboxes + for( i = 0; i < pm_studiohdr->numhitboxes; i++ ) + { + mstudiobbox_t *phitbox = (mstudiobbox_t *)((byte*)pm_studiohdr + pm_studiohdr->hitboxindex) + i; + + // transform traceline into local bone space + Matrix3x4_VectorITransform( pm_studiobones[phitbox->bone], start, start_l ); + Matrix3x4_VectorITransform( pm_studiobones[phitbox->bone], end, end_l ); + + PM_HullForHitbox( phitbox->bbmin, phitbox->bbmax ); + + VectorAdd( start_l, mins, trace_startmins ); + VectorAdd( start_l, maxs, trace_startmaxs ); + VectorAdd( end_l, mins, trace_endmins ); + VectorAdd( end_l, maxs, trace_endmaxs ); + + if( StudioTrace( ptr )) + { + outBone = phitbox->bone; + ptr->hitgroup = phitbox->group; + } + + if( ptr->allsolid ) + break; + } + + // all hitboxes were swept, get trace result + if( outBone >= 0 ) + { + vec3_t temp; + + VectorCopy( ptr->plane.normal, temp ); + ptr->fraction = bound( 0, ptr->fraction, 1.0f ); + VectorLerp( start, ptr->fraction, end, ptr->endpos ); + Matrix3x4_TransformPositivePlane( pm_studiobones[outBone], temp, ptr->plane.dist, ptr->plane.normal, &ptr->plane.dist ); + return true; + } + return false; +} \ No newline at end of file diff --git a/engine/common/sys_win.c b/engine/common/sys_win.c index 35aca53e..731867f7 100644 --- a/engine/common/sys_win.c +++ b/engine/common/sys_win.c @@ -396,10 +396,13 @@ long _stdcall Sys_Crash( PEXCEPTION_POINTERS pInfo ) { // check to avoid recursive call error_on_exit = true; + host.crashed = true; + + if( host.type == HOST_NORMAL ) + CL_Crashed(); // tell client about crash + else host.state = HOST_CRASHED; - if( host.type == HOST_NORMAL ) CL_Crashed(); // tell client about crash Msg( "Sys_Crash: call %p at address %p\n", pInfo->ExceptionRecord->ExceptionAddress, pInfo->ExceptionRecord->ExceptionCode ); - host.state = HOST_CRASHED; if( host.developer <= 0 ) { diff --git a/engine/server/sv_game.c b/engine/server/sv_game.c index 272e78f4..8cd4213f 100644 --- a/engine/server/sv_game.c +++ b/engine/server/sv_game.c @@ -4110,22 +4110,22 @@ int pfnGetLocalizedStringLength( const char *label ) ============= pfnRegisterTutorMessageShown +only exists in PlayStation version ============= */ void pfnRegisterTutorMessageShown( int mid ) { - // UNDONE: no description } /* ============= pfnGetTimesTutorMessageShown +only exists in PlayStation version ============= */ int pfnGetTimesTutorMessageShown( int mid ) { - // UNDONE: no description return 0; } @@ -4133,28 +4133,29 @@ int pfnGetTimesTutorMessageShown( int mid ) ============= pfnProcessTutorMessageDecayBuffer +only exists in PlayStation version ============= */ void pfnProcessTutorMessageDecayBuffer( int *buffer, int bufferLength ) { - // UNDONE: no description } /* ============= pfnConstructTutorMessageDecayBuffer +only exists in PlayStation version ============= */ void pfnConstructTutorMessageDecayBuffer( int *buffer, int bufferLength ) { - // UNDONE: no description } /* ============= pfnResetTutorMessageDecayData +only exists in PlayStation version ============= */ void pfnResetTutorMessageDecayData( void ) diff --git a/engine/server/sv_save.c b/engine/server/sv_save.c index 223e0a1f..18002cbe 100644 --- a/engine/server/sv_save.c +++ b/engine/server/sv_save.c @@ -509,6 +509,10 @@ int SV_IsValidSave( void ) if( sv.background ) return 0; + // don't parse autosave/transition save/restores during playback! + if( CL_IsPlaybackDemo( )) + return 0; + if( !svs.initialized || sv.state != ss_active ) { Msg( "Not playing a local game.\n" ); @@ -592,24 +596,6 @@ void SV_AgeSaveList( const char *pName, int count ) } } -void SV_FileCopy( file_t *pOutput, file_t *pInput, int fileSize ) -{ - char buf[MAX_SYSPATH]; // A small buffer for the copy - int size; - - while( fileSize > 0 ) - { - if( fileSize > MAX_SYSPATH ) - size = MAX_SYSPATH; - else size = fileSize; - - FS_Read( pInput, buf, size ); - FS_Write( pOutput, buf, size ); - - fileSize -= size; - } -} - void SV_DirectoryCopy( const char *pPath, file_t *pFile ) { search_t *t; @@ -630,7 +616,7 @@ void SV_DirectoryCopy( const char *pPath, file_t *pFile ) Q_strncpy( szName, FS_FileWithoutPath( t->filenames[i] ), SAVENAME_LENGTH ); FS_Write( pFile, szName, SAVENAME_LENGTH ); FS_Write( pFile, &fileSize, sizeof( int )); - SV_FileCopy( pFile, pCopy, fileSize ); + FS_FileCopy( pFile, pCopy, fileSize ); FS_Close( pCopy ); } Mem_Free( t ); @@ -650,7 +636,7 @@ void SV_DirectoryExtract( file_t *pFile, int fileCount ) Q_snprintf( szName, sizeof( szName ), "save/%s", fileName ); pCopy = FS_Open( szName, "wb", false ); - SV_FileCopy( pCopy, pFile, fileSize ); + FS_FileCopy( pCopy, pFile, fileSize ); FS_Close( pCopy ); } } diff --git a/engine/server/sv_studio.c b/engine/server/sv_studio.c new file mode 100644 index 00000000..a424f4b6 --- /dev/null +++ b/engine/server/sv_studio.c @@ -0,0 +1,1063 @@ +/* +sv_studio.c - server studio utilities +Copyright (C) 2010 Uncle Mike + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +*/ + +#include "common.h" +#include "server.h" +#include "studio.h" +#include "r_studioint.h" +#include "library.h" + +typedef int (*STUDIOAPI)( int, sv_blending_interface_t*, server_studio_api_t*, matrix3x4, matrix3x4[MAXSTUDIOBONES] ); + +static studiohdr_t *sv_studiohdr; +static mplane_t sv_hitboxplanes[6]; // there a temp hitbox +static matrix3x4 sv_studiomatrix; +static matrix3x4 sv_studiobones[MAXSTUDIOBONES]; +typedef qboolean (*pfnHitboxTrace)( trace_t *trace ); +static vec3_t trace_startmins, trace_endmins; +static vec3_t trace_startmaxs, trace_endmaxs; +static vec3_t trace_absmins, trace_absmaxs; +static float trace_realfraction; +static sv_blending_interface_t *pBlendAPI; + +/* +==================== +SV_InitStudioHull +==================== +*/ +void SV_InitStudioHull( void ) +{ + int i, side; + mplane_t *p; + + for( i = 0; i < 6; i++ ) + { + side = i & 1; + + // planes + p = &sv_hitboxplanes[i]; + VectorClear( p->normal ); + + if( side ) + { + p->type = PLANE_NONAXIAL; + p->normal[i>>1] = -1.0f; + p->signbits = (1<<(i>>1)); + } + else + { + p->type = i>>1; + p->normal[i>>1] = 1.0f; + p->signbits = 0; + } + } +} + +/* +==================== +SV_HullForHitbox +==================== +*/ +static void SV_HullForHitbox( const vec3_t mins, const vec3_t maxs ) +{ + sv_hitboxplanes[0].dist = maxs[0]; + sv_hitboxplanes[1].dist = -mins[0]; + sv_hitboxplanes[2].dist = maxs[1]; + sv_hitboxplanes[3].dist = -mins[1]; + sv_hitboxplanes[4].dist = maxs[2]; + sv_hitboxplanes[5].dist = -mins[2]; +} + +/* +=============================================================================== + + STUDIO MODELS TRACING + +=============================================================================== +*/ +/* +==================== +StudioPlayerBlend + +==================== +*/ +void SV_StudioPlayerBlend( mstudioseqdesc_t *pseqdesc, int *pBlend, float *pPitch ) +{ + // calc up/down pointing + *pBlend = (*pPitch * 3); + + if( *pBlend < pseqdesc->blendstart[0] ) + { + *pPitch -= pseqdesc->blendstart[0] / 3.0f; + *pBlend = 0; + } + else if( *pBlend > pseqdesc->blendend[0] ) + { + *pPitch -= pseqdesc->blendend[0] / 3.0f; + *pBlend = 255; + } + else + { + if( pseqdesc->blendend[0] - pseqdesc->blendstart[0] < 0.1f ) // catch qc error + *pBlend = 127; + else *pBlend = 255.0f * (*pBlend - pseqdesc->blendstart[0]) / (pseqdesc->blendend[0] - pseqdesc->blendstart[0]); + *pPitch = 0; + } +} + +/* +==================== +StudioCalcBoneAdj + +==================== +*/ +static void SV_StudioCalcBoneAdj( float *adj, const byte *pcontroller ) +{ + int i, j; + float value; + mstudiobonecontroller_t *pbonecontroller; + + pbonecontroller = (mstudiobonecontroller_t *)((byte *)sv_studiohdr + sv_studiohdr->bonecontrollerindex); + + for( j = 0; j < sv_studiohdr->numbonecontrollers; j++ ) + { + i = pbonecontroller[j].index; + + if( i == 4 ) continue; // ignore mouth + if( i <= MAXSTUDIOCONTROLLERS ) + { + // check for 360% wrapping + if( pbonecontroller[j].type & STUDIO_RLOOP ) + { + value = pcontroller[i] * (360.0f / 256.0f) + pbonecontroller[j].start; + } + else + { + value = pcontroller[i] / 255.0f; + if( value < 0.0f ) value = 0.0f; + if( value > 1.0f ) value = 1.0f; + value = (1.0f - value) * pbonecontroller[j].start + value * pbonecontroller[j].end; + } + } + + switch( pbonecontroller[j].type & STUDIO_TYPES ) + { + case STUDIO_XR: + case STUDIO_YR: + case STUDIO_ZR: + adj[j] = value * (M_PI / 180.0f); + break; + case STUDIO_X: + case STUDIO_Y: + case STUDIO_Z: + adj[j] = value; + break; + } + } +} + +/* +==================== +StudioCalcBoneQuaterion + +==================== +*/ +static void SV_StudioCalcBoneQuaterion( int frame, float s, mstudiobone_t *pbone, mstudioanim_t *panim, float *adj, float *q ) +{ + int j, k; + vec4_t q1, q2; + vec3_t angle1, angle2; + mstudioanimvalue_t *panimvalue; + + for( j = 0; j < 3; j++ ) + { + if( panim->offset[j+3] == 0 ) + { + angle2[j] = angle1[j] = pbone->value[j+3]; // default; + } + else + { + panimvalue = (mstudioanimvalue_t *)((byte *)panim + panim->offset[j+3]); + k = frame; + + // debug + if( panimvalue->num.total < panimvalue->num.valid ) + k = 0; + + while( panimvalue->num.total <= k ) + { + k -= panimvalue->num.total; + panimvalue += panimvalue->num.valid + 1; + // DEBUG + if( panimvalue->num.total < panimvalue->num.valid ) + k = 0; + } + // Bah, missing blend! + if( panimvalue->num.valid > k ) + { + angle1[j] = panimvalue[k+1].value; + + if( panimvalue->num.valid > k + 1 ) + { + angle2[j] = panimvalue[k+2].value; + } + else + { + if( panimvalue->num.total > k + 1 ) + angle2[j] = angle1[j]; + else angle2[j] = panimvalue[panimvalue->num.valid+2].value; + } + } + else + { + angle1[j] = panimvalue[panimvalue->num.valid].value; + if( panimvalue->num.total > k + 1 ) + { + angle2[j] = angle1[j]; + } + else + { + angle2[j] = panimvalue[panimvalue->num.valid + 2].value; + } + } + angle1[j] = pbone->value[j+3] + angle1[j] * pbone->scale[j+3]; + angle2[j] = pbone->value[j+3] + angle2[j] * pbone->scale[j+3]; + } + + if( pbone->bonecontroller[j+3] != -1 ) + { + angle1[j] += adj[pbone->bonecontroller[j+3]]; + angle2[j] += adj[pbone->bonecontroller[j+3]]; + } + } + + if( !VectorCompare( angle1, angle2 )) + { + AngleQuaternion( angle1, q1 ); + AngleQuaternion( angle2, q2 ); + QuaternionSlerp( q1, q2, s, q ); + } + else + { + AngleQuaternion( angle1, q ); + } +} + +/* +==================== +StudioCalcBonePosition + +==================== +*/ +static void SV_StudioCalcBonePosition( int frame, float s, mstudiobone_t *pbone, mstudioanim_t *panim, float *adj, float *pos ) +{ + int j, k; + mstudioanimvalue_t *panimvalue; + + for( j = 0; j < 3; j++ ) + { + pos[j] = pbone->value[j]; // default; + if( panim->offset[j] != 0.0f ) + { + panimvalue = (mstudioanimvalue_t *)((byte *)panim + panim->offset[j]); + + k = frame; + + // debug + if( panimvalue->num.total < panimvalue->num.valid ) + k = 0; + + // find span of values that includes the frame we want + while( panimvalue->num.total <= k ) + { + k -= panimvalue->num.total; + panimvalue += panimvalue->num.valid + 1; + + // DEBUG + if( panimvalue->num.total < panimvalue->num.valid ) + k = 0; + } + + // if we're inside the span + if( panimvalue->num.valid > k ) + { + // and there's more data in the span + if( panimvalue->num.valid > k + 1 ) + { + pos[j] += (panimvalue[k+1].value * (1.0f - s) + s * panimvalue[k+2].value) * pbone->scale[j]; + } + else + { + pos[j] += panimvalue[k+1].value * pbone->scale[j]; + } + } + else + { + // are we at the end of the repeating values section and there's another section with data? + if( panimvalue->num.total <= k + 1 ) + { + pos[j] += (panimvalue[panimvalue->num.valid].value * (1.0f - s) + s * panimvalue[panimvalue->num.valid + 2].value) * pbone->scale[j]; + } + else + { + pos[j] += panimvalue[panimvalue->num.valid].value * pbone->scale[j]; + } + } + } + + if( pbone->bonecontroller[j] != -1 && adj ) + { + pos[j] += adj[pbone->bonecontroller[j]]; + } + } +} + +/* +==================== +StudioCalcRotations + +==================== +*/ +static void SV_StudioCalcRotations( const edict_t *ent, int boneused[], int numbones, const byte *pcontroller, float pos[][3], vec4_t *q, mstudioseqdesc_t *pseqdesc, mstudioanim_t *panim, float f ) +{ + int i, j, frame; + mstudiobone_t *pbone; + float adj[MAXSTUDIOCONTROLLERS]; + float s; + + if( f > pseqdesc->numframes - 1 ) + f = 0; + else if( f < -0.01f ) + f = -0.01f; + + frame = (int)f; + s = (f - frame); + + // add in programtic controllers + pbone = (mstudiobone_t *)((byte *)sv_studiohdr + sv_studiohdr->boneindex); + + SV_StudioCalcBoneAdj( adj, pcontroller ); + + for( j = numbones - 1; j >= 0; j-- ) + { + i = boneused[j]; + SV_StudioCalcBoneQuaterion( frame, s, &pbone[i], &panim[i], adj, q[i] ); + SV_StudioCalcBonePosition( frame, s, &pbone[i], &panim[i], adj, pos[i] ); + } + + if( pseqdesc->motiontype & STUDIO_X ) pos[pseqdesc->motionbone][0] = 0.0f; + if( pseqdesc->motiontype & STUDIO_Y ) pos[pseqdesc->motionbone][1] = 0.0f; + if( pseqdesc->motiontype & STUDIO_Z ) pos[pseqdesc->motionbone][2] = 0.0f; + + s = 0 * ((1.0f - (f - (int)(f))) / (pseqdesc->numframes)) * ent->v.framerate; + + if( pseqdesc->motiontype & STUDIO_LX ) pos[pseqdesc->motionbone][0] += s * pseqdesc->linearmovement[0]; + if( pseqdesc->motiontype & STUDIO_LY ) pos[pseqdesc->motionbone][1] += s * pseqdesc->linearmovement[1]; + if( pseqdesc->motiontype & STUDIO_LZ ) pos[pseqdesc->motionbone][2] += s * pseqdesc->linearmovement[2]; +} + +/* +==================== +StudioEstimateFrame + +==================== +*/ +static float SV_StudioEstimateFrame( float frame, mstudioseqdesc_t *pseqdesc ) +{ + double f; + + if( pseqdesc->numframes <= 1 ) + f = 0; + else f = ( frame * ( pseqdesc->numframes - 1 )) / 256.0; + + if( pseqdesc->flags & STUDIO_LOOPING ) + { + if( pseqdesc->numframes > 1 ) + f -= (int)(f / (pseqdesc->numframes - 1)) * (pseqdesc->numframes - 1); + if( f < 0 ) f += (pseqdesc->numframes - 1); + } + else + { + if( f >= pseqdesc->numframes - 1.001 ) + f = pseqdesc->numframes - 1.001; + if( f < 0.0 ) f = 0.0; + } + return f; +} + +/* +==================== +StudioSlerpBones + +==================== +*/ +static void SV_StudioSlerpBones( vec4_t q1[], float pos1[][3], vec4_t q2[], float pos2[][3], float s ) +{ + int i; + vec4_t q3; + float s1; + + s = bound( 0.0f, s, 1.0f ); + s1 = 1.0f - s; + + for( i = 0; i < sv_studiohdr->numbones; i++ ) + { + QuaternionSlerp( q1[i], q2[i], s, q3 ); + q1[i][0] = q3[0]; + q1[i][1] = q3[1]; + q1[i][2] = q3[2]; + q1[i][3] = q3[3]; + pos1[i][0] = pos1[i][0] * s1 + pos2[i][0] * s; + pos1[i][1] = pos1[i][1] * s1 + pos2[i][1] * s; + pos1[i][2] = pos1[i][2] * s1 + pos2[i][2] * s; + } +} + +/* +==================== +StudioGetAnim + +==================== +*/ +static mstudioanim_t *SV_StudioGetAnim( model_t *m_pSubModel, mstudioseqdesc_t *pseqdesc ) +{ + mstudioseqgroup_t *pseqgroup; + cache_user_t *paSequences; + size_t filesize; + byte *buf; + + pseqgroup = (mstudioseqgroup_t *)((byte *)sv_studiohdr + sv_studiohdr->seqgroupindex) + pseqdesc->seqgroup; + if( pseqdesc->seqgroup == 0 ) + return (mstudioanim_t *)((byte *)sv_studiohdr + pseqgroup->data + pseqdesc->animindex); + + paSequences = (cache_user_t *)m_pSubModel->submodels; + + if( paSequences == NULL ) + { + paSequences = (cache_user_t *)Mem_Alloc( com_studiocache, MAXSTUDIOGROUPS * sizeof( cache_user_t )); + m_pSubModel->submodels = (void *)paSequences; + } + + // check for already loaded + if( !Mod_CacheCheck(( cache_user_t *)&( paSequences[pseqdesc->seqgroup] ))) + { + string filepath, modelname, modelpath; + + FS_FileBase( m_pSubModel->name, modelname ); + FS_ExtractFilePath( m_pSubModel->name, modelpath ); + Q_snprintf( filepath, sizeof( filepath ), "%s/%s%i%i.mdl", modelpath, modelname, pseqdesc->seqgroup / 10, pseqdesc->seqgroup % 10 ); + + buf = FS_LoadFile( filepath, &filesize, false ); + if( !buf || !filesize ) Host_Error( "StudioGetAnim: can't load %s\n", filepath ); + if( IDSEQGRPHEADER != *(uint *)buf ) + Host_Error( "StudioGetAnim: %s is corrupted\n", filepath ); + + MsgDev( D_INFO, "loading: %s\n", filepath ); + + paSequences[pseqdesc->seqgroup].data = Mem_Alloc( com_studiocache, filesize ); + Q_memcpy( paSequences[pseqdesc->seqgroup].data, buf, filesize ); + Mem_Free( buf ); + } + return (mstudioanim_t *)((byte *)paSequences[pseqdesc->seqgroup].data + pseqdesc->animindex); +} + +/* +==================== +StudioSetupBones + +NOTE: pEdict is unused +==================== +*/ +static void SV_StudioSetupBones( model_t *pModel, float frame, int sequence, const vec3_t angles, const vec3_t origin, + const byte *pcontroller, const byte *pblending, int iBone, const edict_t *pEdict ) +{ + int i, j, numbones = 0; + int boneused[MAXSTUDIOBONES]; + float scale = 1.0f; + double f; + + mstudiobone_t *pbones; + mstudioseqdesc_t *pseqdesc; + mstudioanim_t *panim; + + static float pos[MAXSTUDIOBONES][3]; + static vec4_t q[MAXSTUDIOBONES]; + matrix3x4 bonematrix; + + static float pos2[MAXSTUDIOBONES][3]; + static vec4_t q2[MAXSTUDIOBONES]; + static float pos3[MAXSTUDIOBONES][3]; + static vec4_t q3[MAXSTUDIOBONES]; + static float pos4[MAXSTUDIOBONES][3]; + static vec4_t q4[MAXSTUDIOBONES]; + + if( sequence < 0 || sequence >= sv_studiohdr->numseq ) + { + MsgDev( D_WARN, "SV_StudioSetupBones: sequence %i/%i out of range for model %s\n", sequence, sv_studiohdr->numseq, sv_studiohdr->name ); + sequence = 0; + } + + pseqdesc = (mstudioseqdesc_t *)((byte *)sv_studiohdr + sv_studiohdr->seqindex) + sequence; + pbones = (mstudiobone_t *)((byte *)sv_studiohdr + sv_studiohdr->boneindex); + panim = SV_StudioGetAnim( pModel, pseqdesc ); + + if( iBone < -1 || iBone >= sv_studiohdr->numbones ) + iBone = 0; + + if( iBone == -1 ) + { + numbones = sv_studiohdr->numbones; + for( i = 0; i < sv_studiohdr->numbones; i++ ) + boneused[(numbones - i) - 1] = i; + } + else + { + // only the parent bones + for( i = iBone; i != -1; i = pbones[i].parent ) + boneused[numbones++] = i; + } + + f = SV_StudioEstimateFrame( frame, pseqdesc ); + SV_StudioCalcRotations( pEdict, boneused, numbones, pcontroller, pos, q, pseqdesc, panim, f ); + + if( pseqdesc->numblends > 1 ) + { + float s; + + panim += sv_studiohdr->numbones; + SV_StudioCalcRotations( pEdict, boneused, numbones, pcontroller, pos2, q2, pseqdesc, panim, f ); + + s = (float)pblending[0] / 255.0f; + + SV_StudioSlerpBones( q, pos, q2, pos2, s ); + + if( pseqdesc->numblends == 4 ) + { + panim += sv_studiohdr->numbones; + SV_StudioCalcRotations( pEdict, boneused, numbones, pcontroller, pos3, q3, pseqdesc, panim, f ); + + panim += sv_studiohdr->numbones; + SV_StudioCalcRotations( pEdict, boneused, numbones, pcontroller, pos4, q4, pseqdesc, panim, f ); + + s = (float)pblending[0] / 255.0f; + SV_StudioSlerpBones( q3, pos3, q4, pos4, s ); + + s = (float)pblending[1] / 255.0f; + SV_StudioSlerpBones( q, pos, q3, pos3, s ); + } + } + + if( SV_IsValidEdict( pEdict ) && pEdict->v.scale != 0.0f && sv_allow_studio_scaling->integer ) + scale = pEdict->v.scale; + else if( SV_IsValidEdict( pEdict ) && (pEdict->v.flags & FL_CLIENT) && sv_clienttrace->value != 0.0f ) + scale = sv_clienttrace->value * 0.5f; + + Matrix3x4_CreateFromEntity( sv_studiomatrix, angles, origin, scale ); + + for( j = numbones - 1; j >= 0; j-- ) + { + i = boneused[j]; + + Matrix3x4_FromOriginQuat( bonematrix, q[i], pos[i] ); + if( pbones[i].parent == -1 ) + Matrix3x4_ConcatTransforms( sv_studiobones[i], sv_studiomatrix, bonematrix ); + else Matrix3x4_ConcatTransforms( sv_studiobones[i], sv_studiobones[pbones[i].parent], bonematrix ); + } +} + +/* +==================== +StudioSetupModel + +==================== +*/ +static qboolean SV_StudioSetupModel( edict_t *ent, int iBone, qboolean bInversePitch ) +{ + model_t *mod = Mod_Handle( ent->v.modelindex ); + void *hdr = Mod_Extradata( mod ); + vec3_t angles; + + if( !hdr ) return false; + + sv_studiohdr = (studiohdr_t *)hdr; + VectorCopy( ent->v.angles, angles ); + + if( bInversePitch ) + { + angles[PITCH] = -angles[PITCH]; + } + + // calc blending for player + if( ent->v.flags & FL_CLIENT ) + { + mstudioseqdesc_t *pseqdesc; + byte controller[4]; + byte blending[2]; + int iBlend; + + pseqdesc = (mstudioseqdesc_t *)((byte *)sv_studiohdr + sv_studiohdr->seqindex) + ent->v.sequence; + + SV_StudioPlayerBlend( pseqdesc, &iBlend, &angles[PITCH] ); + + controller[0] = controller[1] = controller[2] = controller[3] = 0x7F; + blending[0] = (byte)iBlend; + blending[1] = 0; + + pBlendAPI->SV_StudioSetupBones( mod, ent->v.frame, ent->v.sequence, angles, ent->v.origin, + controller, blending, iBone, ent ); + } + else + { + pBlendAPI->SV_StudioSetupBones( mod, ent->v.frame, ent->v.sequence, angles, ent->v.origin, + ent->v.controller, ent->v.blending, iBone, ent ); + } + + return true; +} + +qboolean SV_StudioExtractBbox( edict_t *e, model_t *mod, int sequence, float *mins, float *maxs ) +{ + mstudioseqdesc_t *pseqdesc; + studiohdr_t *phdr; + float scale = 1.0f; + + ASSERT( mod != NULL ); + + if( mod->type != mod_studio || !mod->cache.data ) + return false; + + phdr = (studiohdr_t *)mod->cache.data; + if( !phdr->numhitboxes ) return false; + + pseqdesc = (mstudioseqdesc_t *)((byte *)phdr + phdr->seqindex); + + if( sequence < 0 || sequence >= phdr->numseq ) + return false; + + if( e->v.scale > 0.0f && sv_allow_studio_scaling->integer ) + scale = e->v.scale; + + VectorScale( pseqdesc[sequence].bbmin, scale, mins ); + VectorScale( pseqdesc[sequence].bbmax, scale, maxs ); + + return true; +} + +/* +================ +StudioTestToHitbox + +test point trace in hitbox +================ +*/ +static qboolean SV_StudioTestToHitbox( trace_t *trace ) +{ + int i; + mplane_t *p; + + for( i = 0; i < 6; i++ ) + { + p = &sv_hitboxplanes[i]; + + // push the plane out apropriately for mins/maxs + // if completely in front of face, no intersection + if( p->type < 3 ) + { + if( trace_startmins[p->type] > p->dist ) + return false; + } + else + { + switch( p->signbits ) + { + case 0: + if( p->normal[0]*trace_startmins[0] + p->normal[1]*trace_startmins[1] + p->normal[2]*trace_startmins[2] > p->dist ) + return false; + break; + case 1: + if( p->normal[0]*trace_startmaxs[0] + p->normal[1]*trace_startmins[1] + p->normal[2]*trace_startmins[2] > p->dist ) + return false; + break; + case 2: + if( p->normal[0]*trace_startmins[0] + p->normal[1]*trace_startmaxs[1] + p->normal[2]*trace_startmins[2] > p->dist ) + return false; + break; + case 3: + if( p->normal[0]*trace_startmaxs[0] + p->normal[1]*trace_startmaxs[1] + p->normal[2]*trace_startmins[2] > p->dist ) + return false; + break; + case 4: + if( p->normal[0]*trace_startmins[0] + p->normal[1]*trace_startmins[1] + p->normal[2]*trace_startmaxs[2] > p->dist ) + return false; + break; + case 5: + if( p->normal[0]*trace_startmaxs[0] + p->normal[1]*trace_startmins[1] + p->normal[2]*trace_startmaxs[2] > p->dist ) + return false; + break; + case 6: + if( p->normal[0]*trace_startmins[0] + p->normal[1]*trace_startmaxs[1] + p->normal[2]*trace_startmaxs[2] > p->dist ) + return false; + break; + case 7: + if( p->normal[0]*trace_startmaxs[0] + p->normal[1]*trace_startmaxs[1] + p->normal[2]*trace_startmaxs[2] > p->dist ) + return false; + break; + default: + return false; + } + } + } + + // inside this hitbox + trace->fraction = trace_realfraction = 0; + trace->startsolid = trace->allsolid = true; + + return true; +} + +/* +================ +StudioClipToHitbox + +trace hitbox +================ +*/ +static qboolean SV_StudioClipToHitbox( trace_t *trace ) +{ + int i; + mplane_t *p, *clipplane; + float enterfrac, leavefrac, distfrac; + float d, d1, d2; + qboolean getout, startout; + float f; + + enterfrac = -1.0f; + leavefrac = 1.0f; + clipplane = NULL; + + getout = false; + startout = false; + + for( i = 0; i < 6; i++ ) + { + p = &sv_hitboxplanes[i]; + + // push the plane out apropriately for mins/maxs + if( p->type < 3 ) + { + d1 = trace_startmins[p->type] - p->dist; + d2 = trace_endmins[p->type] - p->dist; + } + else + { + switch( p->signbits ) + { + case 0: + d1 = p->normal[0]*trace_startmins[0] + p->normal[1]*trace_startmins[1] + p->normal[2]*trace_startmins[2] - p->dist; + d2 = p->normal[0]*trace_endmins[0] + p->normal[1]*trace_endmins[1] + p->normal[2]*trace_endmins[2] - p->dist; + break; + case 1: + d1 = p->normal[0]*trace_startmaxs[0] + p->normal[1]*trace_startmins[1] + p->normal[2]*trace_startmins[2] - p->dist; + d2 = p->normal[0]*trace_endmaxs[0] + p->normal[1]*trace_endmins[1] + p->normal[2]*trace_endmins[2] - p->dist; + break; + case 2: + d1 = p->normal[0]*trace_startmins[0] + p->normal[1]*trace_startmaxs[1] + p->normal[2]*trace_startmins[2] - p->dist; + d2 = p->normal[0]*trace_endmins[0] + p->normal[1]*trace_endmaxs[1] + p->normal[2]*trace_endmins[2] - p->dist; + break; + case 3: + d1 = p->normal[0]*trace_startmaxs[0] + p->normal[1]*trace_startmaxs[1] + p->normal[2]*trace_startmins[2] - p->dist; + d2 = p->normal[0]*trace_endmaxs[0] + p->normal[1]*trace_endmaxs[1] + p->normal[2]*trace_endmins[2] - p->dist; + break; + case 4: + d1 = p->normal[0]*trace_startmins[0] + p->normal[1]*trace_startmins[1] + p->normal[2]*trace_startmaxs[2] - p->dist; + d2 = p->normal[0]*trace_endmins[0] + p->normal[1]*trace_endmins[1] + p->normal[2]*trace_endmaxs[2] - p->dist; + break; + case 5: + d1 = p->normal[0]*trace_startmaxs[0] + p->normal[1]*trace_startmins[1] + p->normal[2]*trace_startmaxs[2] - p->dist; + d2 = p->normal[0]*trace_endmaxs[0] + p->normal[1]*trace_endmins[1] + p->normal[2]*trace_endmaxs[2] - p->dist; + break; + case 6: + d1 = p->normal[0]*trace_startmins[0] + p->normal[1]*trace_startmaxs[1] + p->normal[2]*trace_startmaxs[2] - p->dist; + d2 = p->normal[0]*trace_endmins[0] + p->normal[1]*trace_endmaxs[1] + p->normal[2]*trace_endmaxs[2] - p->dist; + break; + case 7: + d1 = p->normal[0]*trace_startmaxs[0] + p->normal[1]*trace_startmaxs[1] + p->normal[2]*trace_startmaxs[2] - p->dist; + d2 = p->normal[0]*trace_endmaxs[0] + p->normal[1]*trace_endmaxs[1] + p->normal[2]*trace_endmaxs[2] - p->dist; + break; + default: + d1 = d2 = 0; // shut up compiler + break; + } + } + + if( d2 > 0 ) getout = true; // endpoint is not in solid + if( d1 > 0 ) startout = true; + + // if completely in front of face, no intersection + if( d1 > 0 && d2 >= d1 ) + return false; + + if( d1 <= 0 && d2 <= 0 ) + continue; + + // crosses face + d = 1.0f / ( d1 - d2 ); + f = d1 * d; + + if( d > 0 ) + { + // enter + if( f > enterfrac ) + { + distfrac = d; + enterfrac = f; + clipplane = p; + } + } + else if( d < 0 ) + { + // leave + if( f < leavefrac ) + leavefrac = f; + } + } + + if( !startout ) + { + // original point was inside hitbox + trace->startsolid = true; + if( !getout ) trace->allsolid = true; + return true; + } + + if( enterfrac - FRAC_EPSILON <= leavefrac ) + { + if( enterfrac > -1.0f && enterfrac < trace_realfraction ) + { + if( enterfrac < 0 ) + enterfrac = 0; + trace_realfraction = enterfrac; + trace->fraction = enterfrac - DIST_EPSILON * distfrac; + VectorCopy( clipplane->normal, trace->plane.normal ); + trace->plane.dist = clipplane->dist; + return true; + } + } + return false; +} + +/* +================ +SV_StudioIntersect + +testing for potentially intersection of trace and animation bboxes +================ +*/ +static qboolean SV_StudioIntersect( edict_t *ent, const vec3_t start, vec3_t mins, vec3_t maxs, const vec3_t end ) +{ + vec3_t trace_mins, trace_maxs; + vec3_t anim_mins, anim_maxs; + model_t *mod = Mod_Handle( ent->v.modelindex ); + + // create the bounding box of the entire move + World_MoveBounds( start, mins, maxs, end, trace_mins, trace_maxs ); + + if( !SV_StudioExtractBbox( ent, mod, ent->v.sequence, anim_mins, anim_maxs )) + return false; // invalid sequence + + if( !VectorIsNull( ent->v.angles )) + { + // expand for rotation + float max, v; + int i; + + for( i = 0, max = 0.0f; i < 3; i++ ) + { + v = fabs( anim_mins[i] ); + if( v > max ) max = v; + v = fabs( anim_maxs[i] ); + if( v > max ) max = v; + } + + for( i = 0; i < 3; i++ ) + { + anim_mins[i] = ent->v.origin[i] - max; + anim_maxs[i] = ent->v.origin[i] + max; + } + } + else + { + VectorAdd( anim_mins, ent->v.origin, anim_mins ); + VectorAdd( anim_maxs, ent->v.origin, anim_maxs ); + } + + // check intersection with trace entire move and animation bbox + return BoundsIntersect( trace_mins, trace_maxs, anim_mins, anim_maxs ); +} + +trace_t SV_TraceHitbox( edict_t *ent, const vec3_t start, vec3_t mins, vec3_t maxs, const vec3_t end ) +{ + vec3_t start_l, end_l; + int i, outBone = -1; + pfnHitboxTrace TraceHitbox = NULL; + trace_t trace; + + // assume we didn't hit anything + Q_memset( &trace, 0, sizeof( trace_t )); + VectorCopy( end, trace.endpos ); + trace.fraction = trace_realfraction = 1.0f; + trace.hitgroup = -1; + + if( !SV_StudioIntersect( ent, start, mins, maxs, end )) + return trace; + + if( !SV_StudioSetupModel( ent, -1, false )) // all bones used + return trace; + + if( VectorCompare( start, end )) + TraceHitbox = SV_StudioTestToHitbox; // special case for test position + else TraceHitbox = SV_StudioClipToHitbox; + + // go to check individual hitboxes + for( i = 0; i < sv_studiohdr->numhitboxes; i++ ) + { + mstudiobbox_t *phitbox = (mstudiobbox_t *)((byte*)sv_studiohdr + sv_studiohdr->hitboxindex) + i; + + // transform traceline into local bone space + Matrix3x4_VectorITransform( sv_studiobones[phitbox->bone], start, start_l ); + Matrix3x4_VectorITransform( sv_studiobones[phitbox->bone], end, end_l ); + + SV_HullForHitbox( phitbox->bbmin, phitbox->bbmax ); + + VectorAdd( start_l, mins, trace_startmins ); + VectorAdd( start_l, maxs, trace_startmaxs ); + VectorAdd( end_l, mins, trace_endmins ); + VectorAdd( end_l, maxs, trace_endmaxs ); + + if( TraceHitbox( &trace )) + { + outBone = phitbox->bone; + trace.hitgroup = phitbox->group; + } + + if( trace.allsolid ) + break; + } + + // all hitboxes were swept, get trace result + if( outBone >= 0 ) + { + vec3_t temp; + + trace.ent = ent; + VectorCopy( trace.plane.normal, temp ); + trace.fraction = bound( 0, trace.fraction, 1.0f ); + VectorLerp( start, trace.fraction, end, trace.endpos ); + Matrix3x4_TransformPositivePlane( sv_studiobones[outBone], temp, trace.plane.dist, trace.plane.normal, &trace.plane.dist ); + } + return trace; +} + +void SV_StudioGetAttachment( edict_t *e, int iAttachment, float *org, float *ang ) +{ + mstudioattachment_t *pAtt; + vec3_t forward, bonepos; + vec3_t localOrg, localAng; + void *hdr; + + hdr = Mod_Extradata( Mod_Handle( e->v.modelindex )); + if( !hdr ) return; + + sv_studiohdr = (studiohdr_t *)hdr; + if( sv_studiohdr->numattachments <= 0 ) + return; + + if( sv_studiohdr->numattachments > MAXSTUDIOATTACHMENTS ) + { + sv_studiohdr->numattachments = MAXSTUDIOATTACHMENTS; // reduce it + MsgDev( D_WARN, "SV_StudioGetAttahment: too many attachments on %s\n", sv_studiohdr->name ); + } + + iAttachment = bound( 0, iAttachment, sv_studiohdr->numattachments ); + + // calculate attachment origin and angles + pAtt = (mstudioattachment_t *)((byte *)sv_studiohdr + sv_studiohdr->attachmentindex); + + SV_StudioSetupModel( e, pAtt[iAttachment].bone, true ); + + // compute pos and angles + Matrix3x4_VectorTransform( sv_studiobones[pAtt[iAttachment].bone], pAtt[iAttachment].org, localOrg ); + if( org ) VectorCopy( localOrg, org ); + + if( sv_allow_studio_attachment_angles->integer ) + { + Matrix3x4_OriginFromMatrix( sv_studiobones[pAtt[iAttachment].bone], bonepos ); + VectorSubtract( localOrg, bonepos, forward ); // make forward + VectorNormalizeFast( forward ); + VectorAngles( forward, localAng ); + + if( ang ) VectorCopy( localAng, ang ); + } +} + +void SV_GetBonePosition( edict_t *e, int iBone, float *org, float *ang ) +{ + if( !SV_StudioSetupModel( e, iBone, false ) || sv_studiohdr->numbones <= 0 ) + return; + + iBone = bound( 0, iBone, sv_studiohdr->numbones ); + + if( org ) Matrix3x4_OriginFromMatrix( sv_studiobones[iBone], org ); + if( ang ) VectorAngles( sv_studiobones[iBone][0], ang ); // bone forward to angles +} + +static sv_blending_interface_t gBlendAPI = +{ + SV_BLENDING_INTERFACE_VERSION, + SV_StudioSetupBones, +}; + +static server_studio_api_t gStudioAPI = +{ + Mod_Calloc, + Mod_CacheCheck, + Mod_LoadCacheFile, + Mod_Extradata, +}; + +/* +=============== +SV_InitStudioAPI + +Initialize server studio (blending interface) +=============== +*/ +qboolean SV_InitStudioAPI( void ) +{ + static STUDIOAPI pBlendIface; + + pBlendAPI = &gBlendAPI; + + pBlendIface = (STUDIOAPI)Com_GetProcAddress( svgame.hInstance, "Server_GetBlendingInterface" ); + if( pBlendIface && pBlendIface( SV_BLENDING_INTERFACE_VERSION, pBlendAPI, &gStudioAPI, sv_studiomatrix, sv_studiobones )) + return true; + + // NOTE: we always return true even if game interface was not correct + // because SetupBones is used for hitbox tracing on the server-side + // just restore pointer to builtin function + pBlendAPI = &gBlendAPI; + + return true; +} \ No newline at end of file diff --git a/game_launch/game.dsp b/game_launch/game.dsp index c3550707..5a49064b 100644 --- a/game_launch/game.dsp +++ b/game_launch/game.dsp @@ -39,7 +39,7 @@ RSC=rc.exe # PROP Ignore_Export_Lib 0 # PROP Target_Dir "" # ADD BASE CPP /nologo /W3 /GX /O2 /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /D "_MBCS" /YX /FD /c -# ADD CPP /nologo /W3 /GX /O2 /Ob0 /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /FD /c +# ADD CPP /nologo /MT /W3 /GX /O2 /Ob0 /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /FD /c # SUBTRACT CPP /YX # ADD BASE MTL /nologo /D "NDEBUG" /mktyplib203 /win32 # ADD MTL /nologo /D "NDEBUG" /mktyplib203 /win32 @@ -50,7 +50,7 @@ BSC32=bscmake.exe # ADD BSC32 /nologo LINK32=link.exe # ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /subsystem:windows /machine:I386 /opt:nowin98 -# ADD LINK32 msvcrt.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /subsystem:windows /pdb:none /machine:I386 /nodefaultlib:"libc.lib" /out:"hl.exe" /opt:nowin98 +# ADD LINK32 msvcrt.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /subsystem:windows /pdb:none /machine:I386 /nodefaultlib:"libcmt.lib" /out:"hl.exe" /opt:nowin98 # Begin Custom Build InputPath=.\hl.exe SOURCE="$(InputPath)" diff --git a/game_launch/game.ncb b/game_launch/game.ncb new file mode 100644 index 0000000000000000000000000000000000000000..e7e750a9d217c3150b438b6eb18127f941f926f2 GIT binary patch literal 33792 zcmeI5-ES0C7{=c%6%hF-AMz!MgMrWjwzVV#i8O_6g_0JVZvBd@o9XPdJIU@$Gc#?) zTa7U>dgqPu2ap)!orvMG-s?X=h~9W%0x?{-KF{oKS(-vjzzf>voxJ#np1XDchXIKr?0!WciWbOmoJ)GyIIZ-+sD}L zxV(@%1S=5%5qOpfY`wE25djep0TB=Z5fA|p5CIVo0TB=Z5fA|p5CIVo0TB=Z5fA|p z5CIVo0TB=Z5qPEvnEmriw_lAH0TB=Z5fA|p5CIVo0TB=Z5fA|p5CIVo0TB=Z5fA|p z5CIVo0TB=Z5fA|pSX%1GUl;5un!|rE*n#a2Sp3Zg{G9E>wl~-G+iX9!>&QnE^4sl< zU9kJh{zf~5t`J1iJPTOVrpYi?MY|w`7QxtArqQBh^V(W#!DXV{n z?Z);u%5S>P{0`fL?FjP^)8ABev2LdO5>CIj1KlfJaSH5+BJuuKYiokj$fZtC3erEsYbp-bM;f1XG z-#J#d_h5J0uJwvX=Pd$jn!xR>x8JlQZDwtD(<-nz`;)_Ci3o_m8YiH!{u=*)b$TN3 z7y|15kAb5s5m@5{*#GxJc^`gTcCO2F{ekSpK=b^w3q_l?cXS|S0C(fFgaO`{-`p(o zlywEmcpT}bvwy_acG#-ud zJ*K|^&Eko$h#6+>X8IN=&xQNAogonc5qJav_7#Z;h`{F&~8GXg=w# z^c!jzML`R~)Hy#kJalDZ{C&GLe5EjP{z`txsSJln>V}@*nl={ubIa)?V}*%vr#h3b zl}Ccm&ngPx6e`iY6|N?we8ml?{flUb%|$`kya-h~Ud4@_<9vou$Q%}YJFG@-c_^G_ zHJ73!EoCj6nF@!)AkB&l#c>orRD;iyB#nb`y8fO*Wj4jhg@*UF?yqQu(^)EcW#6kh z$B%Y(wzjFul26#$!2m~ME02EgTHl!uv*V9EXkew;&?c+w>>jR`=i8MLdF0Vf0{lrd z^+EHxEPu8*)_Aznn?co2ilfVqetIWJX5Fe&NN3AIRLn=US`-#X3xodL>t~7=@PXr_ zJ=ND8dXrC7ix;O)Tt{unbJJ>8%{hEyIn3jduGOTu%!{F|<}i`N)Rn^FB&Wb!4#pwp zO5yO7OT@TTjKfn3mx@t1RBkR8V7RM&sO~hev(FUiyH*!f@OcI z@$P!*p8tCJXgOlfs@I{cz?>f^K@?_f;M8F|l$eg4#tSW!QvtfnAA)k~LE$ooeNav< zzJ+r7z_OfDT!m^V1PGG#MSyebPAH$}w;Mfawe>$YFfs2%K0DAClfUM!!4AOR4}0(P zh1_4TzkLOtJ+X5k#}4*0%CWoyf49AamA!k>`SxH1G1u)LK#oKNL_h>YKmYKm@GmqOV7dSR literal 0 HcmV?d00001 diff --git a/game_launch/game.opt b/game_launch/game.opt new file mode 100644 index 0000000000000000000000000000000000000000..2727d69dd1306f38b0c20cb6aa92f468ad0ad02f GIT binary patch literal 52736 zcmeI53wRXAo#3lS4@Mw_gaiU1!|)bZ2!UiQ%NPtJAqhQ^WWe@dX_#rGF{7FB%t%Ps zPH^IQvEwB8k%SQNWBnp)XOTc6ICc`0^PS1X-yXZmo!P_>cH?k9uP^JzIhGR=@2|Rh zq!t#DWS{r@_H&oMpZee3_59bPt81#NPydg!bC339{j<2ptPyeI&mSj=MALNzS)!R# z!TpavmU8SUWiDCfKamFhL|k*ow>XH01dw??0ql?j6JZj_e4flP1*QN+K%~J`NQVrV z2GU+8#~CmavS1cu!(}iV=70m{!aSG{3t%BEg2f>H*~D=vTn@Rg4Dw((gA+=i6gGggSI)5lHbNy-K{eDsE!2UuCuu-# zgzI24Y=NzCJ=_2{g0#1d<4w>6&ESUZ-~lhFAnpA}nR*fAFG^mNiU7y3=pqJVH`6IG zvAEhsaaPy**z14)_DhOPU(D(jLJy0bA|gWQ!bAEUqJen0LF!^O;}ABj%*wJW<1w0j zd0&gCEUcLIgdP;T2wz1SzeyD~B@Sl_j5YjZ8H^ErDY@<=tx^l0#-tFwtgkX`X~yEm zbZJReHjIMyWZdIh+->UOl493ZccitT#1#p9n%dmHfJooH^F~6_fy3?X zaEE*aEA#xm?T$#R>h~LR(60_j?)Ip0(%$a#YIcL(?VgwgL{G>#Ssn=bgDpYF@<{vk zNLM7PwiWO41-!vs5y$e7cYARl7*HL{+uWX3UqCIcC@5Oxz=>Dg;coXwiA3={{(0J> zGLU(W<-zu7@w`@lp4zF(vrrTd@b9W3$HaM`NF{-VTt+JAhE>1nj;MNagWGSOH!cTZ zH8O9aXstae)hm9dETg<7D~`iADE4#5wQG+&+#`B>do)@8J86<%GRbF6@*5`k9g}>) zB-{Ij{2cV$5Rz%B3y8;AO6$I3Jz9G$Z{_=b6PdBKR_3VI&L>C#6E7>&CCr1Di@L)f zbZc#P%t|~h`&uPgd0|SqvUl zZiY;}*z^3*x%YQslXNfZTj-}pD2@F`JtCPW=eeyX#I?xM48_xS$#{zIe?IX{A)X=P`6l_i zcVD&g=7}C58O==KiN8y+D>J%Bw`=xm8l~V%(%rv_O`U{!y2tE3UhE%d{+&&%(=uy# zjtxJHg)j3vCYz5V<=-@V0}q(udHCq?cy^C7p51+)PyX5H4eX^JWj|J-NPXJGUU3iS zh2Q&;$oX}T__DZ1+)MuMjg~5*ggCMKqI8@ZXF48H%0Cx<+bD;R67L1K>*+`nnc^|d zi`Mjr!yt`}`{U2*^2v94M7JUDf44_GZ^*gGrwrLK{irx&$OFjd4EbK`TS@21^)~6r&uv!D zUDKts&hJt}_AX`5|Ls&}oas~?Pj)JHKYrcZsT9#7rLO8!2C_R95!b04K7(J+xs|%d z+{&K2-O6d~oo#R{`&Z-lWVcfO`)1{xSDTgA7n&9O)9@hjM@?F~PaP$!-(WYTQ#qN| zscc=`shn8ashnQlsqEPxYifmt~-WqJ^@gz15|9act{WzXt2_x8#!d&IeVUAF(EM_jv2%01!@>}nxAJs>=9=~ma>|D#K09BmFB09X+^pD z)gE!Dw1;WFr(5hZ?zr-y5YlbJzKZ z);}4c-=W*M`2VnA2XBt0-Vb)aKF6?P{ieSZR`WCef7-KHSTQ9Gyp}#<`?va>97({8 zknS9>mTlVmo^DD0|9o`(rsqE{Ec@q`t|282N{PZFdTC(}(;hWHprK+j_?92MT$B<=xzt`l?ev|yTN$xhu&zs~^CfOXGl+(m+@ig^$ z&wuF=&G*)5@!9`#+Bw6GjK@2MEaQ>Ey_j*B0#~L&O6ELy4>wOB2FSFi~GdSiFf&*X=^j9d4B0ewgIX4>FJRD)mZ%) z?Y(R7huy>8P13|6@pnf4*wKwNIq^qDh9OJ6I1E|pCD)Lpy&_~AntfMEw&gx`T_aySQhWuvmxTrGZf#%~PWXRIZZ6
  • F*CZF2WOJOPEaNqkc*Q^4r|GYrc+DbScH(7!OAqHlMW65* zezb1v6Cp$1$MxF`dEm-E@rWs$XH4=5ll+27K5ddmidVAC*ByGi))KE2;&t|yy<*=% z`a_t5t9!)-!w>s4vjgtmVV|^G&-O2Q^;qJHY+)tFXa3L=bZ;?l|`H{=3MqkTmQ9IS-|-c&i8ZP zJzcL)Fa2Y;IAg@2=uEdbXUNT5e>*0>&@EDr50!&#lbmCc3r+I1CV8ZE-9Wm|ywa_u zD~)uyh*!>UYLx*)$1gNiDt^O{{Oc-}PD9?x^*asO(X&z6XUMMnO68y-pF%!t$nPw# zRC*10#&;`~lZKpeN2T(TA@4z!aZC^yN;UC{^*Jsc*NBtP&qVE!>czTbE90a4xH?t3 zH%zYPnf}v2%-$8W1}S=r5VIy?_GFhLW-sR7pe2M;@EwrO$6`+gKbk$PP#DH$Pui7! z#zr}<@jsLypF`I8pRR|C>xnh~N8uR3SmS?Yr5LTG`4V!h?Y-!-R^KizXV&f-Cc81`pt2S0Kmvtty|LWadcetU^RT6gZ zYHSUwZf~9^6#73#iCfYU^m!f5GXM5QwKH1k^+k(|8`P-N9af`t!AOp?F6awH9g9Ls zW;lyo4Ssir+l6IUS+p~%dZW^-rOpJ_%4i`&sd^%G(_9IzMyNf z^Qt0O^#-S_BH;12dsWxE4O>g9s%5cLJfu`X9_%edUhIp@;Y zyy~Yb#IYX<IwUu=26s7wL@?-&Xn7X%aHzT&b*Nr-`y0cm>Z)&CSyDk8 zDE--CG=6ckThjR@Do?wQd<+Kt+udPTe8C)@|<;bzttwS^E;~_HqDRWUlbL!_ja~z@Q zlCc)G%jHCQOmdlfw;GMAjsQSU4&_v~fW>-M#gya|{e7>88r>!Ye z#VUZvjYCaNUYQz|*K(F@yZVydk0%)OSU{v^IR_uOJZ6iFKdp{)@yh`zCB8_=@9vWQ zqV}*l?|K@P`r3`9&c+7Umf8)4uIh@q>UDLlk^;G?LMrt+TuD>+i9VI1xY$wJQMJB3 z8Vv@n3*}r_QPb$4@Kvf^kz9vfJr+4ava6L#EeURS`#1a4T}vGydAM}f%g%3ma>mP41lC8sVl{GbeT9wKEj5LYYg}m;lS}?})5G#~YcN2}aqR`Y1nA;1fLH&``7}Q&e zSl5)+;*w=!6U~EMUY1f;+Suf*TG!CfRI{$Sbd1G)v6d*3)&7ZO4K;t%9@&c>D%*o~ zO(yFyk|k4?u*2u~N6d;Ds}FbKTKW*@^^d!$ka?UW`~6xczQ(IP5@lc8vBBq8BZD0| zN9}gmZ}oN1n&}Up}!h^5u50k0Z1?$qPY7^+H@ z(R)kW9%eC+I%?DVI!U>fz!LQ$I^M#LOJR!!$LmZcZ=$ohMr*A7{`ovtjOV?6aIDJ9 zm&>YLp0@8XM$?_{lQu6GAo=uL*!jI}Bbz5_I{Qi`Gw)LOJ49!irntZj9C z=5`_&@bUZ`Pj(D;z-t5JaVA?PsTEtRx`N`b^}s0XMkzNkvwE{r%gNYFWLt+v=4R`3 znRLulS%rAzaWr`73Cez2WcZ7eeBE0|kHb|~RZ|uWXm5W~C{u%U6J+<6hXc<#IXa)< zm1b<6;#BO*K4cs&#@J@5vDYH+c-15ui?|glR*W&ASmQVm?HTn$*5Zp)s=XZQ>O=>z z$}!DZU9xpkyBh8)E-ne8-IkJYFeG1D2D^|kk4hR!t9;vCG5b=JHyN)N%8CWJ82)xS z^pZn2x}jWlqu);>VVFfDK9w2k*gy|I$64ZBq!Z$l$mtWUL;~9M+-%FKz zk)s4yF%g6$u|ctm_h~Ikl_IWh6f2bXlpJNg$W&&DeqQnWS=*nd+)6kV+FN{@v6BB`uyZX5CPF%7z%-Zy$uJpi#H|aqfsOMxh=&A7gb8qj zH1xnRu;YIcJcaxNxQ%q)4qt*V!ydQ;z5-u`JK<|^7yK3Mg}dP%_&UrJA2S4Smbjc{ zj_q0YMv!uz(#X9O!kGfhW+hH%nIO4L``bylerSUL1R(^sKo}wrg?8wGUC;?#up4aX zQ$Z7P4o9&Uge!3EpkCTN1M;m2L@SFjiEhI`=aa4&oVz6sxg zeQ+P#4-dd>ae)0B4hRSNUO|2rP;MNY&xK6VJ=K=SI^Qtyy9w&iO(pTHBwsU?X~cCp zddSpt^a1+%K-y6f#Ba&_1K2r0nLa>XA0V#}kirAlm;N3QeaaH$I4GRiAf0eBU>Zz^ zOqc;PA%XP*iLg>^A%0twLbw7}!Ie-1S3xm6NLlO#d=tI}``|vfA0B||>=Ka)GoTz@R=`H6gaZ65 zgezbbDB??SJKTo;ZYRyRq4!4g-pFnRdCCrGfmZOr&9D=aus0Esk>`@viR{8L0p_Cr zIPrbqBCKt26C~hoB20j6@?nbjC-UKT@OFa2Ea$6Y^l zw4eIaPniAGOW8*BQ~vwO+kVQvY)ks_ub+JFCtv%?*M9P~pM32{7yam>AHSccoqC#Z zo<@Ee`3a7P2=5T#9YXKt(ffJsKg0EB#J!a4Z@@RbUlG?|p@%H=kVU!^(d}kYg^sJCp1f~>WUfty6qo|bxR(dZA(itqmD-mhB@GXblhgZG?)&V@CTlE zsno?v%E)Z3EqsMM`8{RlUCQda9N#3b&Qq?=z?UgU@k+e-HTk*`|2I-E${~w9j8`Uc z{aUoN4z581YhWsMBb8lUUgO&9P(Yb2gezbbTnR;xj=ePOJw}~*9BkTb2 zn8P&(OvQdWWWYL}=j*`<3%R!l3eaaE+&~;|glw)|2D71*>l`a6V;xLW#Y>qQHI?&BrsHJw-!OiGkC-|Wa zlCYPKy+a)5bNmK=d=nOPo&(uj`##6H93SBNgYW~+7jga=<>GO80-l7Y;4u6E{sx|g zXW)nMw{Qfyp$Cq_G3bRpI1WF86Ywnj7@mXY;V1A@NT;l1z%*HAAQR@Hk+bN)PJEss zUJE%sfzF?VtI*F%ae;dMI7C5(E5#!0&B9(YZYNXDC!n7Rv=DZn=g9d0drKW+Z>d8J z{v8r|V55%2izBofM`%HAq-gCdL_)pkD2&Ug zD=agi80}mQe<1#UgbVN|_%nPAf_|X_Hi!ekT24XFO^CZej-AdU)bs4Xah_GO=V@oo zQ;N=u9nb=;;DbC`yXEktaL`81qa@`i8}Kg$c?uMf&y%=60ql?jn@RH)aKSeC0dCW= z8;|V-SWaH#!yMA*fVnV*^d^c|MLTKi00;71m4v zfN3xtGGPYHge;f^+3^3!gN@{s+_h&qWb({Nqo=bT|DBLVo;^gF_^;Hh{{}C@Dfk8a zclaf|1QXD*9p>ZTL|Wq&l)Yry2Rmgo30}j_>u?U(eNM6Ql)8-PaR&MKF#bORkHTNW zWAHc}7dO!U-JmGqG;NKXA$pp!E{~!aS2#~VPC)({{-1naBQP za->qAL-vkE{#+F;|{;f6h6C<=zG5C&pHUDdr8GEr|Yis`3;M_}V{+DSI zt2O_N3B7|q-dXd%MxJ+O&Hu9Ie_8XthGv@^lM>~R`quofOUxj%=6}f5?nGn8&Gxji8gZGj%f>@#eKE{H|fLD918~MNf~U{-0@{*7{F5bdRyte`;%*<@=4b z{&U!(Z)^SM(Bf%p{ij@LY+Cnat^fQ>8wbdS)mr~KwEcv&{*%{RYyIblTa#GpKdtqj zvBk;O`p=7Z-ZAeFV6FemAKVb=KlA!eYy8g||Fg#bWFPqpKmNyh7&+L7CX|t;G0KUZ z*7%=kSkD^&v-W?x)Qh(@|1VN*ftf`{6A~{pSAznD4Wm?&2X~zf3x;~ zW4Bpr{-2ye?)i&5$XWCMtoeU`sf<|j|E&3crbTSl_@6cYXN~`Tp~wFe*8j>GdLMtR z&Bk;3BOCnQ1~wIr4F_u4;kGsYXU+d(>*5iA2DHBapnto=_+p1O{%4KSM%Xn<|Bm+~*4FxOYyG$N{a^D}=B@Al zTHpVb-3iQm>-)a}J`QMo|5pg>`@jC-6JxFUpVs_OYyM|wZ~}%k{%ei@GI6c9PrPxg zRt{B&{Iot{H^w~Wd4@@LnB-iOTx61^8?&+T6{wj?vMv7EKGCVf@wLlzzP*xcv&Y8? z`&)XL7b^OM*YKltW1k2a@;tzZWxSG!SMCly zUTcY03h_Go%U-eXV5g>wgR6VR1;Y>fHNC>#XXuFQ8HOyD^opXGjK3u&xy~fJOmeG9 z9w}aHiC6ZzUQJ)Q#A`0`8puB;&f4O{Qew_mf^4Ud&mL`7ayVbe`3cTD57hE4G8^AI zO12&5y!Bsel?9wH;e0>m-P3EdbiMSC-QtW9hoUpx;+!EjbN%g@{6e=#J+7x$#_@}~ z|6`&y`?D7fSmS?}+)>in|Ir;=yK2q<9DT>m!S07wy<()#)YtMZ-x7AWRcjkQx`v-x z`#*kSzNEGPqqYBI`;HxIxH0JT`>g#R$2bcnHqp-7|Iyn2Q9f1d3A_r?(X;k{bXog9 z8hh6mD^_Gvp?yWOB<$W*7xV?9HC}B6gY2g0+oQW0qWTZHj@s=vvv0YtLv=Jn+vWH| zt#ehrt0C-hm8d)1?S7Wn)|Z9dq1F;#B;hWjZ^#oL3 zyRp>S*x<6}e-^towC~>C<=X6zXexC!I<*!5Qi<05&oMsv20P%^_@6cYXN~{KaY<|Z OPY7%LPcW7`-26LwzGP(p literal 0 HcmV?d00001 diff --git a/game_launch/hl.exe b/game_launch/hl.exe index 636278b2e559ac48ddd4cfd4afbfe1ec924f2007..60d8feb9db215ab674e62321928aba94ab0268e2 100644 GIT binary patch delta 15 WcmZoLXfT-Yf#rdnt^dX^JmLT}9R^nb delta 15 XcmZoLXfT-Yfo1O5RlXa)@Q4EdIC2LL